卷积神经网络中,用卷积来代替全连接。由卷积层、汇聚层(池化层)以及最后套一个全连接层组成。
卷积层
卷积层有两个重要的特点:局部连接和权重共享。
局部连接是指卷积层中的每一个神经元都只和下一层的某个局部窗口内的神经元相连,构成一个局部连接网络,而全连接层中每个神经元都和下一层的所有神经元相连。
权重共享是指滤波器(卷积核)wL对于第L层所有的神经元都是相同的。如下图所示,所有同颜色连接的权重都是相同的。
卷积层的作用是提取一个局部区域的特征,不同的卷积核相当于不同的特征提取器。
汇聚层(池化层)
汇聚层,更普遍的叫法是池化层,pooling layer,作用主要是进行特征选择,降低特征数量,从而减少参数数量。卷积层虽然可以显著减少网络中的连接数量,但神经元个数并没有减少,如果卷积层后直接接一个分类器,那么分类器的输入维度依然很高,很容易出现过拟合,为了解决这个问题,所以需要加上汇聚层,从而降低特征维度,避免过拟合。常用的池化方法有最大池化(max pooling)和平均池化(avg pooling)。
全连接层
卷积神经网络在经过卷积和池化操作后最后连接一个全连接层用来进行分类。(根据下游任务的分类也可能变为目标提取和语义分割等任务)
一个典型的卷积神经网络结构如下:
这构成了一个前向传播网络,然后pytorch框架可以通过自动求导,进行误差反向传播,利用梯度下降等优化算法,调整参数w和b,(在卷积神经网络中,其实就是就是卷积核中的权重以及偏置),进而进行学习,当误差缩小到一个可以接受的范围内后或者学习到指定的步数后,停止学习,保存下调整后的参数,而且就是我们所学习到的模型。
至于参数的具体学习过程,可以参考很多的深度学习书籍。这里就不展开了。
代码实现
下面我们介绍一个典型的卷积神经网络:LeNet-5
LeNet-5提出的比较早,算是早期一个非常成功的卷积神经网络,典型应用就是基于LetNet-5的手写数字识别系统。LeNet-5的网络结构如下图所示:
首先输入一幅32 * 32 * 1的图像(默认是灰度图),采用5 * 5的卷积核进行卷积运算,把通道数变成6,长宽变成(32-5)/1 + 1 = 28,所以输出28 * 28 * 6的结果,经过最大池化操作,大小减半,变成14 * 14 * 6。第二层以14 * 14 * 6作为输入,采用5 * 5的卷积核进行卷积运算,把通道数变为16,长宽变成(14-5)/1 + 1 = 10,因此输出为10 * 10 * 16,再经过最大池化操作变成5 * 5 * 16。最后以5 * 5 * 16为输入,进行全连接运算,5 * 5 * 16=400,经过全连接运算后,输出10个类别,进行softmax打分,得分最高的就是输出的结果。
我们用手写数字数据集mnist和图像分类数据集cifar-10来分别训练一下LeNet-5网络。
minist和cifar-10都可以采用pytorch自带的torchvision.datasets进行下载。下面的程序展示了如何获取minst和cifar-10,并展示。
import torch.nn as nn
import torch
import torch.optim as optim
import torchvision.datasets as datasets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
# 加载MNIST数据集
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())
# 展示MNIST数据集
fig = plt.figure(figsize=(20,20))
for i in range(1,11):
ax = fig.add_subplot(1,10,i)
ax.imshow(train_dataset.data[i-1],cmap='gray')
ax.axis('off') # 关闭坐标轴
ax.set_title(train_dataset.targets[i-1].item())
plt.show()
# 加载cifar10数据集
'''下载训练集 CIFAR-10 10分类训练集'''
train_dataset_cifar = datasets.CIFAR10('./data', train=True, transform=transforms.ToTensor(), download=True)
classes=('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 展示cifar10数据集
fig = plt.figure(figsize=(20,20))
for i in range(1,11):
ax = fig.add_subplot(1,10,i)
ax.imshow(train_dataset_cifar.data[i-1],cmap='gray')
ax.axis('off') # 关闭坐标轴
ax.set_title(classes[train_dataset_cifar.targets[i-1]])
plt.show()
输出如下:
由于MINST输入是28*28的图片,而cifar10的输入是三通道的RGB图像,所以我们需要对LeNet-5网络进行微调。
import torch.nn as nn
import torch
import torch.optim as optim
import torchvision.datasets as datasets
import torchvision.transforms as transforms
# 用于MINST数据集
class LeNet5(nn.Module):
def __init__(self):
super(LeNet5, self).__init__()
# 第一个卷积模块
self.layer1 = nn.Sequential(
nn.Conv2d(1,6,5), # 输入通道数1,输出通道数6,卷积核大小5*5,所以输出28*28的二维矩阵(28-5)/1 + 1 = 24
nn.MaxPool2d(2,2) # 最大池化操作后,长宽缩小一半,所以输出是12*12的矩阵
)
# 第二个卷积模块
self.layer2 = nn.Sequential(
nn.Conv2d(6,16,5), # 输入通道数6,输出通道数16,卷积大小5*5,所以输出10*10,(12-5)/1+1 = 8
nn.MaxPool2d(2,2) # 最大池化操作后,长宽缩小一半,输出是5*5,所以输出元素是4*4*16=256
)
# 全连接模块
self.layer3 = nn.Sequential(
nn.Linear(256,120), # 全连接操作,将400个神经元输出为120个
nn.Linear(120,84), # 全连接操作,将120个神经元输出为84个
nn.Linear(84, 10) # 全连接操作,将84个神经元输出为10个,因为数字识别有是个数字,所以需要输出10个类别的打分值
)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = x.view(x.size(0), -1) # 把卷积层的输出展平成一维向量
x = self.layer3(x)
return x
# 用于CIFAR10数据集
class LeNet5_image(nn.Module):
def __init__(self):
super(LeNet5_image, self).__init__()
# 第一个卷积模块
self.layer1 = nn.Sequential(
nn.Conv2d(3,6,5), # 输入通道数3,输出通道数6,卷积核大小5*5,所以输出28*28的二维矩阵(32-5)/1 + 1 = 28
nn.MaxPool2d(2,2), # 最大池化操作后,长宽缩小一半,所以输出是14*14的矩阵
)
# 第二个卷积模块
self.layer2 = nn.Sequential(
nn.Conv2d(6,16,5), # 输入通道数6,输出通道数16,卷积大小5*5,所以输出10*10,(14-5)/1+1 = 10
nn.MaxPool2d(2,2), # 最大池化操作后,长宽缩小一半,输出是5*5,所以输出元素是5*5*16=400
)
# 全连接模块
self.layer3 = nn.Sequential(
nn.Linear(400,120), # 全连接操作,将400个神经元输出为120个
nn.Linear(120,84), # 全连接操作,将120个神经元输出为84个
nn.Linear(84, 10) # 全连接操作,将84个神经元输出为10个,因为数字识别有是个数字,所以需要输出10个类别的打分值
)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
#print(x.shape)
x = x.view(x.size(0), -1) # 把卷积层的输出展平成一维向量
#print(x.shape)
x = self.layer3(x)
return x
训练代码如下:
def train(net, data_loader, test_loader, epochs=10):
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters())
#optimizer = optim.SGD(net.parameters(), lr=3e-2, momentum=0.9)
for epoch in range(epochs):
total_loss = 0.0
for i, (images, labels) in enumerate(data_loader):
optimizer.zero_grad()
outputs = net(images)
loss = criterion(outputs, labels)
total_loss += loss.item()
loss.backward()
optimizer.step()
# if (i+1) % 100 == 0:
# print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch+1, 10, i+1, len(data_loader), loss.item()))
print('Epoch {}, Loss: {:.4f}'.format(epoch+1, total_loss/len(data_loader)))
# 测试模型
net.eval()
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_loader:
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Test Accuracy: {:.2f}%'.format(100 * correct / total))
pass
经过10个epoch的训练,mnist数据集输出:
mnist:
Epoch 10, Loss: 0.0429
Test Accuracy: 98.49%
而对于cifar10数据集,训练50个epoch,输出:
Epoch 50, Loss: 0.2990
Test Accuracy: 60.20%
可以看到,对于mnist手写数字数据集精度非常高,而对于三通道的图像,LeNet-5的精度比较一般,下次我们来看一下真正让人认识到深度学习能力的冠军模型——AlexNet。