本博客为OUC2022秋季软件工程第三次作业
本次课程主要讲解了绪论、基本组成结构、卷积神经网络的典型结构。卷积神经网络应用于分类、识别、检测等,如一些比较成熟的技术人脸识别、表情识别、自动驾驶等等。深度学习三部曲:1. 搭建神经网络结构 2. 找到一个合适的损失函数 3. 找到一个合适的优化函数更新参数。卷积神经网络是由卷积层、池化层、全连接层交叉堆叠而成, 卷积是对两个实变函数的一种数学操作。池化是保留了主要特征的同时减少参数和计算量,防止过拟合,提高模型泛化的能力。它一般处于卷积层与卷积层之间,全连接层与全连接层之间,一般有最大值池化和平均值池化。全连接就是前一层网络的每个节点都与后面的节点相连。卷积神经网络结构有Alexnet、ZFNet、VGG、GoogleNet、ResNet。
- AlexNet :大数据训练、非线性激活函数(ReLU)、防止过拟合、双GPU实现
- ZFNet: 网路结构和AlexNet相同,仅仅是做了一些调参
- VGG: 是一个更深网络, 8layers -> 16 ,错误率从11.7% -> 7.3%
- GoogleNet:网络包含22个参数层,独立成块的层有100个,没有FC层
- ResNet: 残差学习网络,深度152层,错误率6.7% -> 3.57%
加载数据(MINIST)
显示数据集中的部分:
创建网络:
定义训练和测试函数:
在小型全连接网络上训练(Fully-connected network):
在卷积神经网络上训练:
打乱像素顺序再次在两个网络上训练与测试:
在全连接网络上训练与测试:
在卷积神经网络上训练与测试:
从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是 卷积神经网络的性能明显下降。
这是因为对于卷积神经网络,会利用像素的局部关系,但是打乱顺序以后,这些像素间的关系将无法得到利用。
准确率还可以,通过改进网络结构,性能还可以进一步提升。在 Kaggle 的LeaderBoard上,准确率高的达到95%以上。
网络训练:
报错cfg没有定义,检查代码发现前面是self.cfg, 改为self.cfg 没有报错。
但是在正向传播+反向传播+优化这里发生了错误,猜测可能是之前的改的不对,又将之前的self去了,但是还是不对,询问同学后知道不仅要加self而且要将2048改为512, 我修改后跑出结果如下:
然后进行测试, 测试结果为
可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 64%,提升到 84.92%。
dataloder是一个类,而shuffle(bool)是一个进行随机打乱顺序的函数, 参数为bool类型,默认情况下参数是false,此时每次迭代训练数据集时不会将输入数据顺序打乱;如果是true时每次迭代训练时都会将数据重新打乱。
transform可以对数据进行变换,可用于PIL Image 或者 Tensor Image,可以对图像进行裁剪、翻转和旋转、图像变换、图像格式转换、系列变换、通用变换、组合变化,如transforms.CenterCrop(size)可以裁剪图像,进行中心裁剪为size * size 大小的图像;transforms.RandomHorizontalFlip§ 水平翻转,默认值为0.5, 大于0.5就会左右翻转。
Batch大小是在更新模型之前处理的多个样本。Epoch数是通过训练数据集的完整传递次数。批处理的大小必须大于或等于1且小于或等于训练数据集中的样本数。可以将epoch设置为1和无穷大之间的整数值。可以根据需要运行算法,甚至可以使用除固定数量的epoch之外的其他条件来停止算法。它们都是整数值,并且它们都是学习算法的超参数,例如学习过程的参数,而不是学习过程找到的内部模型参数。必须为学习算法指定batch大小和epoch数。
FC作用:将前一层的输出通过加权隐射到label上去
1x2卷积作用:1*1的卷积可以用于降维(减少通道数),升维(增加通道数),代替fc成为一个分类器
区别:权值共享,参数量较同等功能的fc层相比少,使用了位置信息;fc层对于训练样本要求统一尺寸,但是1*1的卷积不会受该规定的限制
通过使用residual learning我们可以加深网络层数,我们知道在不断加深神经网络深度时,模型的准确率会先上升然后达到饱和,此时如果再持续增加网络深度会使准确率下降,但如果在后面增加几个恒等映射层,这样即使增加网络深度,误差也不会增加,同时它也可以有效解决梯度消失问题。
激活函数不同,LeNet使用sigmoid函数作为激活函数,代码练习二中使用ReLu作为激活函数。
以使用带padding的大小为1*1的卷积来改变feature map尺寸使其可以应用Residual learning。
- 加深网络层数
- 更换激活函数、损失函数
- 增大数据集
### 卷积神经网络(Convolutional Neural Network)
深度学习三部曲
step1. 搭建神经网络结构
step2. 找到一个合适的损失函数
损失函数是衡量吻合度的,可以调整参数/权重W,使得映射的结果和实际类别吻合
传统神经网络也可以应用到计算机视觉,但是会有如下问题:参数太多,会造成过拟合(泛化性能差)
卷积神经网络:局部关联,参数共享
step3. 找到一个合适的优化函数,更新参数
相同之处:分层:卷积层,激活层,池化层,全连接层
卷积:卷积是对两个实变函数的一种数学操作 实变函数:以实数为自变量的函数
二维卷积
一维卷积:常用在信号处理中,用于计算信号的延迟累计
滤波器(filter)
卷积核:信息的衰减率
基本概念:
池化:(pooling layer):保留了主要特征的同时减少参数和计算量,防止过拟合,提高模型泛化的能力。它一般处于卷积层与卷积层之间,全连接层与全连接层之间。
全连接:(FC layer)两层之间的所有神经元都有权重链接,通常全连接层在卷积神经网络的尾部,全连接层参数量通常最大
非线性激活函数:ReLU函数:优点:
网络结构与AlexNet相同
VGG是一个更深网络 8->16
加载数据(MINIST)
创建网络
在小型全连接网络上训练(Fully-connected network)
# Training settings
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
train(model_cnn)
test(model_cnn)
通过上面的测试结果,可以发现,含有相同参数的 CNN 效果要明显优于 简单的全连接网络,是因为 CNN 能够更好的挖掘图像中的信息,主要通过两个手段:
打乱像素顺序再次在两个网络上训练与测试
在全连接网络上训练与测试
Number of parameters: 6442
Train: [0/60000 (0%)] Loss: 2.264858
Train: [6400/60000 (11%)] Loss: 2.041490
Train: [12800/60000 (21%)] Loss: 1.552290
Train: [19200/60000 (32%)] Loss: 1.093537
Train: [25600/60000 (43%)] Loss: 0.799383
Train: [32000/60000 (53%)] Loss: 0.847221
Train: [38400/60000 (64%)] Loss: 0.756004
Train: [44800/60000 (75%)] Loss: 0.723714
Train: [51200/60000 (85%)] Loss: 0.438829
Train: [57600/60000 (96%)] Loss: 0.474032
Test set: Average loss: 0.5751, Accuracy: 8348/10000 (83%)
在卷积神经网络上训练与测试:
Number of parameters: 6422
Train: [0/60000 (0%)] Loss: 2.300903
Train: [6400/60000 (11%)] Loss: 2.282535
Train: [12800/60000 (21%)] Loss: 2.261807
Train: [19200/60000 (32%)] Loss: 2.111837
Train: [25600/60000 (43%)] Loss: 1.717916
Train: [32000/60000 (53%)] Loss: 1.320999
Train: [38400/60000 (64%)] Loss: 0.960259
Train: [44800/60000 (75%)] Loss: 0.961738
Train: [51200/60000 (85%)] Loss: 0.636504
Train: [57600/60000 (96%)] Loss: 0.507474
Test set: Average loss: 0.6227, Accuracy: 8003/10000 (80%)
从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是 卷积神经网络的性能明显下降。
这是因为对于卷积神经网络,会利用像素的局部关系,但是打乱顺序以后,这些像素间的关系将无法得到利用。
接下来定义网络,损失函数和优化器:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
# 网络放到GPU上
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
训练网络:
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
print('Finished Training')
网络在整个数据集上的表现:
Accuracy of the network on the 10000 test images: 62 %
VGG是由Simonyan 和Zisserman在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组(Visual Geometry Group)的缩写。
该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。
定义dataloader
需要注意的是,这里的 transform,dataloader 和之前定义的有所不同
定义VGG网络
网络训练
训练结果
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %.2f %%' % (
100 * correct / total))
Accuracy of the network on the 10000 test images: 84.92 %
可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 64%,提升到 84.92%
dataloader 里面 shuffle 取不同值有什么区别?
Dataloader中设置了shuffle是True,所以每次加载的数据都是随机的。
洗牌。默认设置为False。在每次迭代训练时是否将数据洗牌,默认设置是False。将输入数据的顺序打乱,是为了使数据更有独立 性,但如果数据是有序列特征的,就不要设置成True了
transform 里,取了不同值,这个有什么区别?
transform的作用:对图像进行一定的预处理,相当于扩展数据集。
transform的处理方法
1 裁剪-Crop
2 翻转和旋转——Flip and Rotation
3 图像变换
4 对transforms操作,使数据增强更灵活
epoch 和 batch 的区别?
epoch:当一个完整的数据集经过神经网络一次,并返回一次,这个过程称为一个epoch。
batch:当数据集很大的时候,对于每个epoch,很难将所有的数据集一次读入到内存中,这是需要将数据集分为几次读入,每次称为一个batch。
1x1的卷积和 FC 有什么区别?主要起什么作用?
1*1的卷积:
即保持特征图尺寸不发生变化且维持上一层的通道数,在增加网络的深度的同时令网络能够学习更为复杂的函数(特征信息)。
FC(全连接层):起到分类器的作用。对前层的特征进行一个加权和,(卷积层是将数据输入映射到隐层特征空间)将特征空间通过线性变换映射到样本标记空间(也就是label)
区别:
全连接是把特征图拆开组成一个一维向量,再乘以一个权重向量,这两个向量中的元素一一对应所以输出结果是一个值。
11的卷积核那就是表明只由一个权重组成,如果特征图尺寸也是11的话,那输出就是一个值,此时与全连接完全一样。但是如果特征图尺寸不是11,而是wh的话,那么11的卷积输出就不是一个值而是wh的一个矩阵。
用11卷积代替全连接应该是基于输入尺寸的考虑,全连接的输入是特征图所有元素乘以权重再求和,但是这个权重向量是在设计网络的时候就需要固定的,所以全连接没办法适应输入尺寸的变化只能固定。但是11卷积的输出与输入尺寸是一样大的,输出尺寸可以随着输入尺寸的变化而变化,所以1*1卷积无需固定输出尺寸。
residual leanring 为什么能够提升准确率?
残差的思想都是去掉相同的主体部分,从而突出微小的变化,引入残差后的映射对输出的变化更敏感。很明显,在残差网络中输出的变化对权重的调整影响更大,也就是说反向传播的梯度值更大,训练就更加容易。
代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?
有什么方法可以进一步提升准确率?
增加更多数据
2.处理缺失值和异常值
特征工程学:这一步骤有助于从现有数据中提取更多信息。新信息作为新特征被提取出来。这些特征可能会更好地解释训练集中的差异变化。因此能改善模型的准确率。
特征选择:特征选择是寻找众多属性的哪个子集合,能够最好的解释目标变量与各个自变量的关系的过程。
你可以根据多种标准选取有用的特征
使用多种算法
交叉验证:如果想解决这个问题,我们必须使用交叉验证技术(cross validation)。交叉验证是数据建模领域最重要的概念之一。它是指,保留一部分数据样本不用来训练模型,而是在完成模型前用来验证。
第一个现代卷积网络是AlexNet,也正是AlexNet在ImageNet竞赛上的成功,使得卷积神经网络以及深度学习开始风靡,引发了这一轮的深度学习热潮。AlexNet的基本模式同LeNet相同,其成功的关键原因有两个,一是网络时代大量增加的数据,二是GPU提供的算力支持了大型网络的训练。在LeNet的时代,网络尚未完全普及,在图像领域的机器学习研究通常使用小样本,而且样本的维度也不大。在这样的情况下,神经网络并不比传统的机器学习方法更有优势,而且神经网络的训练和调参难度也比传统机器学习方法大,因此神经网络没有获得多少青睐。而后来互联网的普及使得数据集的规模有了增大的可能,而gpu的使用,使得训练更大更深的神经网络更加容易。在这两个条件的基础上,AlexNet取得了成功。
下一个学习的网络结构是有名的VGG Net。其创新点在于提出了卷积块的概念,将若干个卷积层以及激活函数打包成一个块,通过多个块的堆叠构造神经网络,方便了网络结构的设计,并且也取得了很好的效果。其另一个发现是多个33的卷积堆叠,效果要好于使用大尺寸的卷积,例如77,11*11,即使用窄且深的网络比宽且浅的网络好。
一个vgg块包含若干个卷积和ReLU,每个块的第一个卷积会将输入的通道进行改变(或不改变),后续的所有卷积都采用尺寸为3,步长为1,padding为1的卷积,输入的通道和输出的通道数一致,因此在一个vgg块内部,特征图的尺寸不会发生变化,直到块的末尾。
如果AlexNet让深度学习风靡,那么ResNet带来了真正的深度。残差连接思想的提出使得我们训练极深的网络成为了可能,残差连接几乎成为了现在深度学习不可缺少的技术。
残差链接使得网络层数的加深和复杂可以包含原有的网络,即模型的效果最坏不会坏于加深之前的效果。具体的计算方式是,每次将输入直接与输出相加,使得网络变成x+f(x)的形式,这样网络最差也是恒等映射,y=x,不会让效果变得更差。更重要的是,残差连接使得梯度可以通过“捷径”从深处传递到前面,缓解了梯度弥散的问题,使得深层的网络更容易训练。
训练前准备:引入pytorch包 计算模型中有多少参数 同时更改训练为GPU。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy
# 一个函数,用来计算模型中有多少参数
def get_n_params(model):
np=0
for p in list(model.parameters()):
np += p.nelement()
return np
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
①加载数据集(MNIST)
使用datasets.MNIST方法 从training.pt创建数据集,放入root文件夹下。两个batch大小不同,但均将数据集打乱。
input_size = 28*28 # MNIST上的图像尺寸是 28x28
output_size = 10 # 类别为 0 到 9 的数字,因此为十类
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=1000, shuffle=True)
显示数据集中的部分图像
plt.figure(figsize=(8, 5))
for i in range(20):
plt.subplot(4, 5, i + 1)
image, _ = train_loader.dataset.__getitem__(i)
plt.imshow(image.squeeze().numpy(),'gray')
plt.axis('off');
②创建网络
定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数init中。
class FC2Layer(nn.Module):
def __init__(self, input_size, n_hidden, output_size):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__init__(self)
super(FC2Layer, self).__init__()
self.input_size = input_size
# 这里直接用 Sequential 就定义了网络,注意要和下面 CNN 的代码区分开
self.network = nn.Sequential(
nn.Linear(input_size, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, output_size),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
# view一般出现在model类的forward函数中,用于改变输入或输出的形状
# x.view(-1, self.input_size) 的意思是多维的数据展成二维
# 代码指定二维数据的列数为 input_size=784,行数 -1 表示我们不想算,电脑会自己计算对应的数字
# 在 DataLoader 部分,我们可以看到 batch_size 是64,所以得到 x 的行数是64
# 大家可以加一行代码:print(x.cpu().numpy().shape)
# 训练过程中,就会看到 (64, 784) 的输出,和我们的预期是一致的
# forward 函数的作用是,指定网络的运行过程,这个全连接网络可能看不啥意义,
# 下面的CNN网络可以看出 forward 的作用。
x = x.view(-1, self.input_size)
return self.network(x)
class CNN(nn.Module):
def __init__(self, input_size, n_feature, output_size):
# 执行父类的构造函数,所有的网络都要这么写
super(CNN, self).__init__()
# 下面是网络里典型结构的一些定义,一般就是卷积和全连接
# 池化、ReLU一类的不用在这里定义
self.n_feature = n_feature
self.conv1 = nn.Conv2d(in_channels=1, out_channels=n_feature, kernel_size=5)
self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
self.fc1 = nn.Linear(n_feature*4*4, 50)
self.fc2 = nn.Linear(50, 10)
# 下面的 forward 函数,定义了网络的结构,按照一定顺序,把上面构建的一些结构组织起来
# 意思就是,conv1, conv2 等等的,可以多次重用
def forward(self, x, verbose=False):
x = self.conv1(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = x.view(-1, self.n_feature*4*4)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
定义训练和测试函数
# 训练函数
def train(model):
model.train()
# 主里从train_loader里,64个样本一个batch为单位提取样本进行训练
for batch_idx, (data, target) in enumerate(train_loader):
# 把数据送到GPU中
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
def test(model):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
# 把数据送到GPU中
data, target = data.to(device), target.to(device)
# 把数据送入模型,得到预测结果
output = model(data)
# 计算本次batch的损失,并加到 test_loss 中
test_loss += F.nll_loss(output, target, reduction='sum').item()
# get the index of the max log-probability,最后一层输出10个数,
# 值最大的那个即对应着分类结果,然后把分类结果保存在 pred 里
pred = output.data.max(1, keepdim=True)[1]
# 将 pred 与 target 相比,得到正确预测结果的数量,并加到 correct 中
# 这里需要注意一下 view_as ,意思是把 target 变成维度和 pred 一样的意思
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))
③在小型全连接网络上训练(Fully-connected network)。
n_hidden = 8 # number of hidden units
model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))
train(model_fnn)
test(model_fnn)
在卷积神经网络上训练。
# Training settings
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
train(model_cnn)
test(model_cnn)
通过上面的测试结果,可以发现,含有相同参数的 CNN 效果要明显优于简单的全连接网络,是因为 CNN 能够更好的挖掘图像中的信息,主要通过两个手段:
④打乱像素顺序再次在两个网络上训练与测试
考虑到CNN在卷积与池化上的优良特性,如果我们把图像中的像素打乱顺序,这样卷积和池化就难以发挥作用了,为了验证这个想法,我们把图像中的像素打乱顺序再试试。
# 这里解释一下 torch.randperm 函数,给定参数n,返回一个从0到n-1的随机整数排列
perm = torch.randperm(784)
plt.figure(figsize=(8, 4))
for i in range(10):
image, _ = train_loader.dataset.__getitem__(i)
# permute pixels
image_perm = image.view(-1, 28*28).clone()
image_perm = image_perm[:, perm]
image_perm = image_perm.view(-1, 1, 28, 28)
plt.subplot(4, 5, i + 1)
plt.imshow(image.squeeze().numpy(), 'gray')
plt.axis('off')
plt.subplot(4, 5, i + 11)
plt.imshow(image_perm.squeeze().numpy(), 'gray')
plt.axis('off')
重新定义训练与测试函数,我们写了两个函数 train_perm 和 test_perm,分别对应着加入像素打乱顺序的训练函数与测试函数。
# 对每个 batch 里的数据,打乱像素顺序的函数
def perm_pixel(data, perm):
# 转化为二维矩阵
data_new = data.view(-1, 28*28)
# 打乱像素顺序
data_new = data_new[:, perm]
# 恢复为原来4维的 tensor
data_new = data_new.view(-1, 1, 28, 28)
return data_new
# 训练函数
def train_perm(model, perm):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
# 像素打乱顺序
data = perm_pixel(data, perm)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
# 测试函数
def test_perm(model, perm):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
data, target = data.to(device), target.to(device)
# 像素打乱顺序
data = perm_pixel(data, perm)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.data.max(1, keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))
在全连接网络上训练与测试:
perm = torch.randperm(784)
n_hidden = 8 # number of hidden units
model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))
train_perm(model_fnn, perm)
test_perm(model_fnn, perm)
在卷积神经网络上训练与测试:
perm = torch.randperm(784)
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
train_perm(model_cnn, perm)
test_perm(model_cnn, perm)
从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是卷积神经网络的性能明显下降。
这是因为对于卷积神经网络,会利用像素的局部关系,但是打乱顺序以后,这些像素间的关系将无法得到利用。
对于视觉数据,PyTorch 创建了一个叫做 totchvision 的包。
首先,加载并归一化 CIFAR10 使用 torchvision 。torchvision 数据集的输出是范围在[0,1]之间的 PILImage,我们将他们转换成归一化范围为[-1,1]之间的张量 Tensors。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 注意下面代码中:训练的 shuffle 是 True,测试的 shuffle 是 false
# 训练时可以打乱顺序增加多样性,测试是没有必要
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
下面展示 CIFAR10 里面的一些图片:
def imshow(img):
plt.figure(figsize=(8,8))
img = img / 2 + 0.5 # 转换到 [0,1] 之间
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 得到一组图像
images, labels = iter(trainloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示第一行图像的标签
for j in range(8):
print(classes[labels[j]])
接下来定义网络,损失函数和优化器:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
# 网络放到GPU上
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
训练网络:
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
print('Finished Training')
现在我们从测试集中取出8张图片:
# 得到一组图像
images, labels = iter(testloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示图像的标签
for j in range(8):
print(classes[labels[j]])
我们把图片输入模型,看看CNN把这些图片识别成什么:
outputs = net(images.to(device))
_, predicted = torch.max(outputs, 1)
# 展示预测的结果
for j in range(8):
print(classes[predicted[j]])
可以看到,有几个都识别错了。
让我们看看网络在整个数据集上的表现:
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
准确率还可以,通过改进网络结构,性能还可以进一步提升。在 Kaggle 的LeaderBoard上,准确率高的达到95%以上。
①定义 dataloader
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
②VGG 网络定义
现在的结构基本上是:
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
def _make_layers(self, cfg):
layers = []
in_channels = 3
for x in cfg:
if x == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
nn.BatchNorm2d(x),
nn.ReLU(inplace=True)]
in_channels = x
layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
return nn.Sequential(*layers)
上述代码要做一些修改 如cfg改为self.cfg,2048改为512。
初始化网络,根据实际需要,修改分类层。因为 tiny-imagenet 是对200类图像分类,这里把输出修改为200。
# 网络放到GPU上
net = VGG().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
③网络训练
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
print('Finished Training')
④测试验证准确率:
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %.2f %%' % (
100 * correct / total))
可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 63%,提升到 83.77%
1、dataloader 里面 shuffle 取不同值有什么区别?
DataLoader中的shuffle参数为在训练中是否打乱数据集,若该值为true,则在每一轮训练中都会打乱该数据集,然后分成大小为n的若干个mini-batch。
2、transform 里,取了不同值,这个有什么区别?
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
Compose函数的作用是把多个步骤整合在一起,这里把ToTensor和Normalize整合到一起;
ToTensor()将shape为(H, W, C)的nump.ndarray或img转为shape为(C, H, W)的tensor;
Normalize(mean, std, inplace=False)],对于每个通道,使用公式"(x - mean) / std",x为image,mean是均值,std是标准差,inplace代表是否原地操作
不同值主要是均值和标准差的设置,维度不同主要是对不同通道进行设置。
3、epoch 和 batch 的区别?
Batch用于定义在更新内部模型参数之前要处理的样本数。
Epoch用于定义了学习算法在整个训练数据集中的工作次数。
假设一个包含200个样本的数据集,并且选择了大小为5的Batch和1000个Epoch。则该数据集将分为40个Batch,每个Batch有5个样本。有1000个Epoch,模型将暴露或传递整个数据集1000次。
4、1x1的卷积和 FC 有什么区别?主要起什么作用?
1x1卷积只是把原始数据乘以一个倍数(卷积核的数值)。1x1卷积一般只改变输出通道数(channels),而不改变输出的宽度和高度。
当输入的feature map的尺寸是1×1时,两者从数学原理上来看,没有区别。假设输入为c×1×1,输出为n×1×1,那么全连接可以认为是一个c维的向量和n×c大小的矩阵相乘。卷积层可以理解为n个c×1×1的卷积核,分别与输入做内积,跟计算矩阵向量乘没有区别。
当输入为c×w×h时,卷积层和全连接层的输出尺寸就不一样了,1×1的卷积输出为n×w×h,全连接的输出是n×1×1。此时,全连接可以等价于n个c×w×h卷积核的卷积层。
全连接层和卷积层最大的区别就是输入尺寸是否可变,全连接层的输入尺寸是固定的,卷积层的输入尺寸是任意的。
5、residual leanring 为什么能够提升准确率?
当网络层数急剧增加,BP算法中导数累乘还是容易出现梯度消失,梯度爆炸等问题。而深度残差学习网络可以说是根治了这种问题。推导BP过程,很容易就发现,在求导过程中会出现一个随着层数增加而累乘导数的现象,我们尽可能控制每个导数的值,使它尽量接近1,这样累乘结果不会太小,也不会太大。
6、代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?
一开始最经典的神经网络是LeNet,它与后续的LeNet-5非常类似,只是深度、池化、输出形式、训练方法有所出入。
- 激活函数不同,LeNet用的激活函数为tanh的变种,实际的为x = 1.7159 * tanh(2/3*x)。而代码练习二里面的为ReLU
- output层,在H3层卷积结束,进入全连接层范围,后续的LeNet-5中或者更新的神经网络结构,会用到softmax实现分类和归一化。
7、代码练习二里,卷积以后feature map 尺寸会变小,如何应用 Residual Learning?
将Soft Mask Branch与Trunk Branch输出的feature map进行element-wised的乘法,就得到了一个weighted Attention map,之后又与原来Trunk Branch的feature map进行了一个element-wised的操作。输出为下面这个式子:
其中M(x)为Soft Mask Branch的输出,F(x)为Trunk Branch的输出,那么当M(x)=0时,该层的输入就等于F(x),因此该层的效果不可能比原始的F(x)差,这一点也借鉴了ResNet中恒等映射的思想,同时这样的加法,也使得Trunk Branch输出的feature map中显著的特征更加显著,增加了特征的判别性。这样,优化的问题解决了,性能的问题也解决了,因此通过将这种残差结构进行堆叠,就能够很容易的将模型的深度达到很深的层次,具有非常好的性能。
8、有什么方法可以进一步提升准确率?
学习专知课程《卷积神经网络》,主要内容包括:
- CNN的基本结构:卷积、池化、全连接
- 典型的网络结构:AlexNet、VGG、GoogleNet、ResNet
在本次的视频学习中,需要学习以下内容
绪论
1.卷积神经网络的应用2.传统神经网络VS卷积神经网络
基本组成结构
1.卷积2.池化3.全连接
卷积神经网络典型结构
1.AlexNet 2.ZFNet 3.VGG 4.GoogleNet 5.ResNet
代码实战
Tensorflow-CNN
总结
1.参考文献2.代码3.作业
深度学习三部曲
Step 1:Neural Network
Step 2:Cost Function
Step 3:OptimizationStep1.搭建神经网络结构
Step2.找到一个合适的损失函数
交叉熵损失(cross entropy loss),均方误差(MSE)
Step3.找到一个合适的优化函数,更新参数
反向传播(BP),随机梯度下降(SGD)
损失函数
给定W,可以由像素映射到类目得分
损失函数是用来衡量吻合度的
可以调整参数/权重W,使得映射的结果和实际类别吻合
常用分类损失:
交叉熵损失和常用回归损失
全连接网络处理图像的问题:
参数太多:权重矩阵的参数太多一>过拟合
卷积神经网络的解决方式:
局部关联,参数共享
卷积是什么?
convolution is an operation on two functions of a real-valued argument.
卷积是对两个实变函数的一种数学操作。
在图像处理中,图像是以二维矩阵的形式输入到神经网络的,因此我们需要二维卷积。
池化一Pooling Layer
Pooling:
保留了主要特征的同时减少参数和计算量,防止过拟合,提高模型泛化能力。
它一般处于卷积层与卷积层之间,全连接层与全连接层之间
Pooling的类型:
Max pooling:最大值池化
Average pooling:平均池化
全连接一Fully Connected Layer
卷积神经网络
小结
一个典型的卷积网络是由卷积层、池化层、全连接层交叉堆
叠而成
卷积是对两个实变函数的一种数学操作。
局部关联,参数共享
未加padding时输出的特征图大小:(N-F)/stride+1
有padding时输出的特征图大小:(N+padding*2-F)/stride+1
Pooling的类型:Max pooling:最大值池化,Average pooling:平均池化
全连接:通常全连接层在卷积神经网络尾部
卷积神经网络典型结构–AlexNet
AlexNet之所以能够成功,
深度学习之所以能够重回历史舞台,原因在于:
大数据训练:百万级ImageNet图像数据
非线性激活函数:ReLU
防止过拟合:Dropout,Data augmentation
其他:双GPU实现
优点:
解决了梯度消失的问题(在正区间)
计算速度特别快,只需要判断输入是否大于0
收敛速度远快于sigmoid
卷积神经网络典型结构一VGG
VGG是一个更深网络
8 layers (AlexNet)->16-19(VGG)
ILSVRC top5错误率从11.7%->7.3%
卷积神经网络典型结构一GoogleNet
2014年ImageNet图像分类竞赛的冠军
ImageNet top 5 error:11.7%->6.7%网络总体结构:
网络包含22个带参数的层
(如果考虑pooling层就是27层),独立成块的层总共
有约有100个;
参数量大概是Alexnet的1/12
没有FC层
ResNet
残差学习网络(deep residual learning network)
2015年EILSVRC竞赛冠军,top5错误率从6.7%->3.57%
深度有152层
需要使用谷歌的 Colab ,大家有任何问题可以随时在群里 AT 我。有部分同学已经做过这部分代码练习,可以略过。
MNIST 数据集分类:构建简单的CNN对 mnist 数据集进行分类。同时,还会在实验中学习池化与卷积操作的基本作用。
卷积神经网络(CNN)
Outline
深度卷积神经网络中,有如下特性
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy
# 一个函数,用来计算模型中有多少参数
def get_n_params(model):
np=0
for p in list(model.parameters()):
np += p.nelement()
return np
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
PyTorch里包含了 MNIST, CIFAR10 等常用数据集,调用 torchvision.datasets 即可把这些数据由远程下载到本地,下面给出MNIST的使用方法:
torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)
另外值得注意的是,DataLoader是一个比较重要的类,提供的常用操作有:batch_size(每个batch的大小), shuffle(是否进行随机打乱顺序的操作), num_workers(加载数据的时候使用几个子进程)
input_size = 28*28 # MNIST上的图像尺寸是 28x28
output_size = 10 # 类别为 0 到 9 的数字,因此为十类
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])),
batch_size=1000, shuffle=True)
显示数据集中的部分图像
plt.figure(figsize=(8, 5))
for i in range(20):
plt.subplot(4, 5, i + 1)
image, _ = train_loader.dataset.__getitem__(i)
plt.imshow(image.squeeze().numpy(),'gray')
plt.axis('off');
定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数init中。
只要在nn.Module的子类中定义了forward函数,backward函数就会自动被实现(利用autograd)。
class FC2Layer(nn.Module):
def __init__(self, input_size, n_hidden, output_size):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__init__(self)
super(FC2Layer, self).__init__()
self.input_size = input_size
# 这里直接用 Sequential 就定义了网络,注意要和下面 CNN 的代码区分开
self.network = nn.Sequential(
nn.Linear(input_size, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(),
nn.Linear(n_hidden, output_size),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
# view一般出现在model类的forward函数中,用于改变输入或输出的形状
# x.view(-1, self.input_size) 的意思是多维的数据展成二维
# 代码指定二维数据的列数为 input_size=784,行数 -1 表示我们不想算,电脑会自己计算对应的数字
# 在 DataLoader 部分,我们可以看到 batch_size 是64,所以得到 x 的行数是64
# 大家可以加一行代码:print(x.cpu().numpy().shape)
# 训练过程中,就会看到 (64, 784) 的输出,和我们的预期是一致的
# forward 函数的作用是,指定网络的运行过程,这个全连接网络可能看不啥意义,
# 下面的CNN网络可以看出 forward 的作用。
x = x.view(-1, self.input_size)
return self.network(x)
class CNN(nn.Module):
def __init__(self, input_size, n_feature, output_size):
# 执行父类的构造函数,所有的网络都要这么写
super(CNN, self).__init__()
# 下面是网络里典型结构的一些定义,一般就是卷积和全连接
# 池化、ReLU一类的不用在这里定义
self.n_feature = n_feature
self.conv1 = nn.Conv2d(in_channels=1, out_channels=n_feature, kernel_size=5)
self.conv2 = nn.Conv2d(n_feature, n_feature, kernel_size=5)
self.fc1 = nn.Linear(n_feature*4*4, 50)
self.fc2 = nn.Linear(50, 10)
# 下面的 forward 函数,定义了网络的结构,按照一定顺序,把上面构建的一些结构组织起来
# 意思就是,conv1, conv2 等等的,可以多次重用
def forward(self, x, verbose=False):
x = self.conv1(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = x.view(-1, self.n_feature*4*4)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
定义训练和测试函数
# 训练函数
def train(model):
model.train()
# 主里从train_loader里,64个样本一个batch为单位提取样本进行训练
for batch_idx, (data, target) in enumerate(train_loader):
# 把数据送到GPU中
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
def test(model):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
# 把数据送到GPU中
data, target = data.to(device), target.to(device)
# 把数据送入模型,得到预测结果
output = model(data)
# 计算本次batch的损失,并加到 test_loss 中
test_loss += F.nll_loss(output, target, reduction='sum').item()
# get the index of the max log-probability,最后一层输出10个数,
# 值最大的那个即对应着分类结果,然后把分类结果保存在 pred 里
pred = output.data.max(1, keepdim=True)[1]
# 将 pred 与 target 相比,得到正确预测结果的数量,并加到 correct 中
# 这里需要注意一下 view_as ,意思是把 target 变成维度和 pred 一样的意思
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))
n_hidden = 8 # number of hidden units
model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))
train(model_fnn)
test(model_fnn)
需要注意的是,上在定义的CNN和全连接网络,拥有相同数量的模型参数
# Training settings
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
train(model_cnn)
test(model_cnn)
通过上面的测试结果,可以发现,含有相同参数的 CNN 效果要明显优于 简单的全连接网络,是因为 CNN 能够更好的挖掘图像中的信息,主要通过两个手段:
考虑到CNN在卷积与池化上的优良特性,如果我们把图像中的像素打乱顺序,这样 卷积 和 池化 就难以发挥作用了,为了验证这个想法,我们把图像中的像素打乱顺序再试试。
首先下面代码展示随机打乱像素顺序后,图像的形态:
# 这里解释一下 torch.randperm 函数,给定参数n,返回一个从0到n-1的随机整数排列
perm = torch.randperm(784)
plt.figure(figsize=(8, 4))
for i in range(10):
image, _ = train_loader.dataset.__getitem__(i)
# permute pixels
image_perm = image.view(-1, 28*28).clone()
image_perm = image_perm[:, perm]
image_perm = image_perm.view(-1, 1, 28, 28)
plt.subplot(4, 5, i + 1)
plt.imshow(image.squeeze().numpy(), 'gray')
plt.axis('off')
plt.subplot(4, 5, i + 11)
plt.imshow(image_perm.squeeze().numpy(), 'gray')
plt.axis('off')
重新定义训练与测试函数,我们写了两个函数 train_perm 和 test_perm,分别对应着加入像素打乱顺序的训练函数与测试函数。
与之前的训练与测试函数基本上完全相同,只是对 data 加入了打乱顺序操作。
# 对每个 batch 里的数据,打乱像素顺序的函数
def perm_pixel(data, perm):
# 转化为二维矩阵
data_new = data.view(-1, 28*28)
# 打乱像素顺序
data_new = data_new[:, perm]
# 恢复为原来4维的 tensor
data_new = data_new.view(-1, 1, 28, 28)
return data_new
# 训练函数
def train_perm(model, perm):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
# 像素打乱顺序
data = perm_pixel(data, perm)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
# 测试函数
def test_perm(model, perm):
model.eval()
test_loss = 0
correct = 0
for data, target in test_loader:
data, target = data.to(device), target.to(device)
# 像素打乱顺序
data = perm_pixel(data, perm)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.data.max(1, keepdim=True)[1]
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
accuracy))
在全连接网络上训练与测试:
perm = torch.randperm(784)
n_hidden = 8 # number of hidden units
model_fnn = FC2Layer(input_size, n_hidden, output_size)
model_fnn.to(device)
optimizer = optim.SGD(model_fnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_fnn)))
train_perm(model_fnn, perm)
test_perm(model_fnn, perm)
在卷积神经网络上训练与测试:
perm = torch.randperm(784)
n_features = 6 # number of feature maps
model_cnn = CNN(input_size, n_features, output_size)
model_cnn.to(device)
optimizer = optim.SGD(model_cnn.parameters(), lr=0.01, momentum=0.5)
print('Number of parameters: {}'.format(get_n_params(model_cnn)))
train_perm(model_cnn, perm)
test_perm(model_cnn, perm)
从打乱像素顺序的实验结果来看,全连接网络的性能基本上没有发生变化,但是 卷积神经网络的性能明显下降。
这是因为对于卷积神经网络,会利用像素的局部关系,但是打乱顺序以后,这些像素间的关系将无法得到利用。
对于视觉数据,PyTorch 创建了一个叫做 totchvision 的包,该包含有支持加载类似Imagenet,CIFAR10,MNIST 等公共数据集的数据加载模块 torchvision.datasets 和支持加载图像数据数据转换模块 torch.utils.data.DataLoader。
下面将使用CIFAR10数据集,它包含十个类别:‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10 中的图像尺寸为3x32x32,也就是RGB的3层颜色通道,每层通道内的尺寸为32*32。
首先,加载并归一化 CIFAR10 使用 torchvision 。torchvision 数据集的输出是范围在[0,1]之间的 PILImage,我们将他们转换成归一化范围为[-1,1]之间的张量 Tensors。
大家肯定好奇,下面代码中说的是 0.5,怎么就变化到[-1,1]之间了?PyTorch源码中是这么写的:
input[channel] = (input[channel] - mean[channel]) / std[channel]
这样就是:((0,1)-0.5)/0.5=(-1,1)。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 注意下面代码中:训练的 shuffle 是 True,测试的 shuffle 是 false
# 训练时可以打乱顺序增加多样性,测试是没有必要
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=8,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
下面展示 CIFAR10 里面的一些图片:
def imshow(img):
plt.figure(figsize=(8,8))
img = img / 2 + 0.5 # 转换到 [0,1] 之间
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 得到一组图像
images, labels = iter(trainloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示第一行图像的标签
for j in range(8):
print(classes[labels[j]])
接下来定义网络,损失函数和优化器:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
# 网络放到GPU上
net = Net().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
训练网络:
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
print('Finished Training')
现在我们从测试集中取出8张图片:
# 得到一组图像
images, labels = iter(testloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示图像的标签
for j in range(8):
print(classes[labels[j]])
我们把图片输入模型,看看CNN把这些图片识别成什么:
outputs = net(images.to(device))
_, predicted = torch.max(outputs, 1)
# 展示预测的结果
for j in range(8):
print(classes[predicted[j]])
可以看到,有几个都识别错了~~~~~ 让我们看看网络在整个数据集上的表现:
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
准确率还可以,通过改进网络结构,性能还可以进一步提升。在 Kaggle 的LeaderBoard上,准确率高的达到95%以上。
VGG是由Simonyan 和Zisserman在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组(Visual Geometry Group)的缩写。
该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。
需要注意的是,这里的 transform,dataloader 和之前定义的有所不同
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
下面定义VGG网络,参数太多,我手动改简单了些~~~~~
现在的结构基本上是:
64 conv, maxpooling,
128 conv, maxpooling,
256 conv, 256 conv, maxpooling,
512 conv, 512 conv, maxpooling,
512 conv, 512 conv, maxpooling,
softmax
可能有同学要问,为什么这么设置?
其实不为什么,就是觉得对称,我自己随便改的。。。
下面是模型的实现代码:
这一步老师的代码报错了,需要将
self.features = self._make_layers(cfg)
self.classifier = nn.Linear(2048, 10)
改为
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)
改好的代码如下:
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.cfg = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M']
self.features = self._make_layers(self.cfg)
self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
def _make_layers(self, cfg):
layers = []
in_channels = 3
for x in cfg:
if x == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
nn.BatchNorm2d(x),
nn.ReLU(inplace=True)]
in_channels = x
layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
return nn.Sequential(*layers)
初始化网络,根据实际需要,修改分类层。因为 tiny-imagenet 是对200类图像分类,这里把输出修改为200。
# 网络放到GPU上
net = VGG().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)
训练的代码和以前是完全一样的:
for epoch in range(10): # 重复多轮训练
for i, (inputs, labels) in enumerate(trainloader):
inputs = inputs.to(device)
labels = labels.to(device)
# 优化器梯度归零
optimizer.zero_grad()
# 正向传播 + 反向传播 + 优化
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 输出统计信息
if i % 100 == 0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch + 1, i + 1, loss.item()))
print('Finished Training')
测试的代码和之前也是完全一样的。
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %.2f %%' % (
100 * correct / total))
可以看到,使用一个简化版的 VGG 网络,就能够显著地将准确率由 63%,提升到 83.17%
1、dataloader 里面 shuffle 取不同值有什么区别?
dataloader 里面 shuffle 表示是否对数据进行打乱洗牌,可取值为True或False。
当shuffle取值为True时,会随机选取数据集中的数据,数据的输入顺序会被打乱,然后以batch为单位从头到尾按顺序取用数据;
当shuffer=False(默认值)表示不打乱数据的顺序,然后以batch为单位从头到尾按顺序取用数据。
通常需要对训练集打乱洗牌,测试集可以不打乱。
2、transform 里,取了不同值,这个有什么区别?
transform是常用的数据预处理方法,对数据进行某种统一处理,进行标准化、降维、归一化、正则化等变换操作,提高泛化能力。
代码练习中用到的有:
transforms.ToTensor() 作用是转换为tensor格式,这个格式可以直接输入进神经网络;
transforms.Normalize()是对像素值进行归一化处理,使得数据服从均值为0,标准差为1的分布;
transforms.RandomCrop()是在图片的随机位置上进行裁剪并返回新的图片;
transforms.RandomHorizontalFlip()是依据概率p对图片进行水平翻转,p默认0.5;
transforms.Compose()是将一系列的transforms有序组合,实现时按照这些方法依次对图像操作。
我上网查找资料找到了其他的一些函数
①裁剪
transforms.CenterCrop中心裁剪
transforms.RandomCrop随机裁剪
transforms.RandomResizedCrop随机长宽比裁剪
transforms.FiveCrop上下左右中心裁剪
transforms.TenCrop上下左右中心裁剪后翻转
②翻转
transforms.RandomHorizontalFlip(p=0.5)依概率p水平翻转
transforms.RandomVerticalFlip(p=0.5)依概率p垂直翻转
③旋转
transforms.RandomRotation随机旋转
④图像变换
transforms.Resize调整尺寸
transforms.Normalize标准化
transforms.ToTensor转为tensor
transforms.Pad 填充
transforms.ColorJitter修改亮度、对比度和饱和度
transforms.Grayscale变为灰度图
transforms.LinearTransformation()线性变换
transforms.RandomAffine仿射变换
transforms.RandomGrayscale按照概率p转为灰度图
⑤数据增强
transforms.RandomChoice从给定的选一个进行操作
transforms.RandomApply按照概率进行操作
transforms.RandomOrder将t操作随机化
3、epoch 和 batch 的区别?
①batch大小是在更新模型之前处理的多个样本。epoch数是通过训练数据集的完整传递次数。batch大小必须大于或等于1且小于或等于训练数据集中的样本数。而epoch可以设置为1和无穷大之间的整数值。
②epoch和batch都是神经网络训练中的超参数,epoch表示神经网络训练的轮数,batch用于定义在更新内部模型参数之前要处理的样本数,一次epoch至少要训练完成一个batch.
③当一个完整的数据集通过了神经网络一次并且返回了一次,这个过程称为一个 epoch。然而,当一个 epoch 对于计算机而言太庞大的时候,就需要把它分成多个小块(batch)。
④在不能将数据一次性通过神经网络的时候,就需要将数据集分成几个 batch。另外注意,batch size是一个 batch 中的样本总数,而number of batches也就是迭代次数。
比如对于一个有 2000 个训练样本的数据集。将 2000 个样本分成大小为 500 的 batch,那么完成一个 epoch 需要 4 个 iteration。⑤epoch : 使用训练集的全部数据对模型进行了一次完整的训练,被称为一代训练;
batch : 使用训练集的一小部分样本对模型权重进行一次反向传播的参数更新,这一小部分样本被称为一批数据。
4、1x1的卷积和 FC 有什么区别?主要起什么作用?
区别:
1x1通常用于实现降维,用作非线性变化,FC可以看作全局卷积,可将最后一层卷积得到的特征图(矩阵)展开成一维向量,并为分类器提供输入。1x1卷积可以替代FC,FC主要起到线性变化和分类的作用。
②1×1卷积核是对输入的每一个特征图进行线性组合,在一个局部区域,输出是图像长宽w*h的一个矩阵,而全连接层是对输入的每一个数进行线性组合,把特征图拆开组成一个一维向量,再乘以一个权重向量,这两个向量中的元素一一对应,输出结果是一个值。
③1x1的卷积层采用参数共享方式,需要的参数量会比FC层所使用的参数量少,计算速度更快;1x1的卷积可以用于降维(减少通道数),升维(增加通道数),代替 FC成为一个分类器;另外,FC层对于训练样本要求统一尺寸,但是1x1的卷积不会受该规定的限制。
作用:
①将前面经过多次卷积后高度抽象化的特征进行整合,然后可以进行归一化,对各种分类情况都输出一个概率,之后的分类器可以根据全连接得到的概率进行分类。
②1*1卷积在大多数情况下是用于升/降特征的维度(通道数),而不改变图片的宽和高。全连接做不到。全连接层的作用是可以将卷积得到的局部特征连接起来,综合考虑整个图像。
5、residual leanring 为什么能够提升准确率?
残差网络结构图中,通过“shortcut connections(捷径连接)”的方式,直接把输入x传到输出作为初始结果,输出结果为H(x)=F(x)+x,当F(x)=0时,那么H(x)=x。于是,ResNet相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(X)和x的差值,也就是所谓的残差F(x) := H(x)-x,因此,后面的训练目标就是要将残差结果逼近于0,使到随着网络加深,准确率不下降。
换言之, 残差网络通过增加网络深度来提升准确率,通过直接将输入信息绕道传到输出,保护信息的完整性,整个网络只需要学习输入和输出有差别的那一部分,这样就简化了学习目标以及难度。普通网络结构是一个连乘的形式,残差多了加和,缓解了梯度消失的问题。
6、代码练习二里,网络和1989年 Lecun 提出的 LeNet 有什么区别?
①激活函数不同。代码练习二中激活函数为ReLU,而LeNet激活函数是Sigmoid。
ReLU函数比sigmoid函数计算上更为简单,且ReLU函数在不同的参数初始化方法下可以让模型更容易训练。②模型构造不同。
7、代码练习二里,卷积以后feature map 尺寸会变小,如何应用 Residual Learning?
使用线性变换的操作,即Residual Learning在shortcut中采用1x1的卷积,设置步长为2;1x1在卷积的时候设置输出维度为残差连接相同的维度,进而对大小不同的feature map进行调整。
8、有什么方法可以进一步提升准确率?
①优化数据预处理。使用特征更明显、分类更合理、数据量更大的数据集。
②选择合适的优化器、损失函数、激活函数。
③改进网络结构。选择性能最优的神经网络结构,例如: VGG、Resnet、谷歌的 Inception 网络等。
④增加网络深度
⑤增加训练轮数