目录
Part1 视频学习
1 传统神经网络vs卷积神经网络
2 基本组成结构
3 卷积神经网络典型结构
Part2 代码练习
1 MNIST数据集分类
2 CIFAR10数据集分类
3 使用VGG16对CIFAR10分类
Part3 问题思考
1 dataloader 里面 shuffle 取不同值有什么区别?
2 transform 里,取了不同值,这个有什么区别?
3 epoch和batch的区别?
4 1x1的卷积和FC有什么区别?主要起什么作用?
5 residual leanring为什么能够提升准确率?
6 代码练习二里,网络和1989年Lecun提出的LeNet有什么区别?
7 代码练习二里,卷积以后feature map 尺寸会变小,如何应用 Residual Learning?
8 有什么方法可以进一步提升准确率?
卷积神经网络的基本应用:分类、检索、检测、分割、识别、图像生成、风格迁移、自动驾驶等
损失函数用于衡量预测结果和真实结果的吻合程度,能够帮助卷积神经网络调整参数/权重W,以达到更好的训练效果
传统神经网络核卷积神经网络都采用了层级结构,但传统神经网络是全连接网络,几乎每个神经元都会和图片的全部像素信息进行连接,使得权重矩阵的参数过多,容易过拟合;而卷积网络通过局部关联、参数共享解决了这个问题,它的每个神经元只和图片中的某个区域进行连接,卷积核在滑动的过程中参数不变,这进一步减小了参数规模
一维卷积:应用于信号处理中,用于计算信号的延迟累积
卷积:卷积是对两个实变函数的一种数学操作,在图像处理中,图像是以二维形式输入到神经网络的,因此需要二维卷积
卷积的相关概念
卷积的可视化:输出某一层的特征图,观察该层学习到了什么样的特征
池化(Pooling):结构和操作与卷积相似,一般位于卷积层与卷积层之间,或者全连接层与全连接层之间,在保留主要特征的同时减少参数和计算量,防止过拟合,提高模型泛化能力
池化的类型
全连接层(FC layer):通常在卷积神经网络尾部,两层之间所有神经元都有权重连接,参数量很大
模型结构:CONV1+MAXPOOL1+NORM1+CONV2+MAXPOOL2+NORM2+CONV3+CONV4+CONV5+MAXPOOL3+FC6+FC7+FC8
模型特点
逐层分析
网络结构与AlexNet相同,将卷积层1中的感受野大小由11*11改为7*7,步长由4改为2;卷积层3,4,5中的滤波器个数由384,384,256改为512,512,1024
VGG是个更深的网络,AlexNet有8层,VGG有16-19层,迁移学习中常用VGG
网络结构:
16层网络的节点信息:
网络总体结构:包含22个带参数的层(考虑pooling层就是27层),独立成块的层总共有约100个,参数量大约为AlexNet的一半,没有全连接层
Inception模块作用:多卷积核增加特征多样性
Inception V2:插入1*1卷积进行降维,解决了深度加深参数数量增长过快的问题
Inception V3:用小的卷积核替代大的卷积核,参数数量进一步降低;同时增加非线性激活函数使得网络产生更多独立特征,增强了表征能力,训练更快
Stem部分(stem network):卷积-池化-卷积-卷积-池化
残差学习网络(deep residual learning network),除了输出层之外没有其他全连接层,结构灵活,可以训练非常深的网络
残差的思想:去掉相同的主体部分,突出微小的变化
代码链接:(colab)MNIST数据集分类
深度卷积神经网络具有以下特性:
PyTorch里包含了 MNIST, CIFAR10 等常用数据集,调用 torchvision.datasets 即可把这些数据由远程下载到本地,以MNIST的使用为例:
torchvision.datasets.MNIST(root, train=True, transform=None, target_transform=None, download=False)
DataLoader是一个比较重要的类,提供的常用操作有:
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")
print(device)
# out: cude:0
input_size = 28*28 # MNIST上的图像尺寸
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)
)
# forward函数用于指定网络的运行过程
def forward(self,x):
# view一般出现在model类的forward函数中,用于改变输入或输出的形状
# 代码指定二维数据的列数为input_size=784,行数-1表示由电脑自己计算对应的数字
# batch_size是64,所以x的行数是64
x = x.view(-1,self.input_size) # 多维的数据展成二维
# print(x.cpu().numpy().shape) # 输出(64,784)
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()
# 值最大的那个即对应着分类结果,然后把分类结果保存在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)
# 卷积神经网络训练
# 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通过卷积核池化,能更好地提取信息
卷积和池化操作都是基于图像地局部进行的,能提取到像素的位置关系,此时尝试把图像中的像素打乱顺序
# 打乱像素顺序的演示
perm = torch.randperm(784) # 给定参数n,返回一个从0到n-1的随机整数排列
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')
打乱像素顺序后的效果:
打乱像素顺序的函数:
# 对每个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
在全连接网络上训练和测试:
# 打乱像素顺序训练全连接网络
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)
由此可知,打乱像素顺序后,卷积神经网络性能下降了,像素间的局部关系对卷积神经网络来说是个很重要的训练信息
代码链接:(colab)CIFAR10数据集分类
CIFAR10数据集包含10个类别,图像尺寸为3*32*32,可以使用torchsivion加载,torchvision数据集的输出范围是[0,1]的PILImage,使用前需要先进行归一化操作,转换为[-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')
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%200==0:
print('Epoch: %d Minibatch: %5d loss: %.3f' %(epoch+1,i+1,loss.item()))
print('Finished Training')
训练结果:
# 得到一组图像
images,labels = iter(testloader).next()
# 展示图像
imshow(torchvision.utils.make_grid(images))
# 展示图像的标签
for j in range(8):
print(classes[labels[j]])
outputs = net(images.to(device))
_,predicted = torch.max(outputs,1)
print("预测结果:")
# 展示预测的结果
for j in range(8):
print(classes[predicted[j]])
发现有少量的识别错误
准确率较低,有待提高
代码链接:(colab)VGG_CIFAR10
相比代码练习二,这里的归一化操作的参数有了一些改变
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
# 简化版的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(2048,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)
# 网络放到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')
训练过程的loss:
相比代码练习二中使用CNN训练得到的准确率,使用VGG得到的准确率提高了20点,VGG比代码练习二的CNN深度更深,非线性变换的可能情况更多,更有利于VGG找到更适合的非线性变换来解决数据分类问题
shuffle是bool类型的参数,当shuffle为True时,加载数据集数据时会将数据打乱,shuffle为False时不打乱,打乱顺序会使得每轮训练中的数据序列都不一样,消除了数据排列对训练效果的影响
此处的transform定义了一些常用的数据预处理操作,包括数据归一化,随机裁剪、翻转等,可以用于数据增强,充分利用数据样本,提高训练模型的泛化能力
代码练习中用到的transforms.normalize()用于逐个channel对图像进行标准化,使得数据服从均值为0,标准差为1的分布,加快模型的收敛速度,基本实现公式为x=(x-mean)/std,其中mean是数据本身的均值,std是数据本身的标准差,这两个值需要事先计算得到
normalize之前,数据在[0,1],normalize时,如果是normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),则是对数据进行归一化,如果是normalize(mean, std),则是将数据处理为均值为0,标准差为1的分布
epoch是训练数据集的轮数,一个epoch相当于全部跑完一次数据集;bantch是一个epoch内一次批量训练的样本数
1*1卷积是二维卷积的一种特殊情况,可以起到降维的作用,在应对多维度的输入时,可以调节通道数,减少参数,帮助捕捉深度上的pattern,还可以增加非线性,代替FC作为分类器。与FC相比,1*1卷积能实现权值共享,参数量较同等功能的fc层相比少,使用了位置信息,并且fc层对于训练样本要求统一尺寸,但是1*1的卷积不受这种规定的限制
在训练网络的过程中,深度越深,参数越复杂,网络也就越复杂,但分类任务具有过程未知性,并且深度的神经网络很难实现恒等映射,导致网络很难学习到更优的参数,引入残差学习后,网络能实现恒等映射了,训练过程中可以根据实际效果跳过其中的几个层,灵活性更大,因此可以提高准确率
LeNet结构大致如下:
代码练习二中使用的是最大池化和ReLU激活函数,而LeNet使用的是平均池化,激活函数是sigmoid
可以参考50层以上的ResNet网络的BottleNeck的设计,使用1*1卷积来调整维度和feature map尺寸