一 前言
卷积神经网络(CNN)可谓是深度学习网络的经典代表之一,吹水的话就不多说了,我们直接说正事。其实学习过CNN的人都应该知道,其实CNN理解并不难,但是往往认为自己懂的时候,我觉得更应该多问自己一些理解模糊的问题,那这篇文章就是带领大家再大概了解一下CNN到底做了什么事,使得图像识别的准确率提高那么多。
二 CNN的基本操作
我们所了解到的神经网络模型,基本都可以认为,神经网络就是在自动帮助我们提取特征。CNN也同样。
1.相片的滤镜
首先我们先介绍卷积核,其实就是和相片的滤镜意思差不多,就比如下面这个图片,我拿一个小的滤镜一个一个取,所以再拼做成和原来一样大小的图片,新图片的像素便是CNN提取的新特征。
在这里,读者应该会有两个疑问,第一,卷积核怎么取?第二,怎么拼成新的图片?第三,最外一层是什么?我一一解答。
第一,卷积核也是由正太分布初始化的
第二,怎么拼?我们引用唐迪宇老师的资料来讲。
w0就是我们的三个卷积核,分别于对用通道进行矩阵的内积(就是对用位置相乘)然后再把这个矩阵的所有元素相加,因为是彩色照片,是三通道,所以会有得到三个数,再将这三个数相加,便可以得到Output Volume框起来的-3。其他的也是同样道理。
第三,在第二的时候大家都看到最外一层就是0,为什么这么做呢,这就是padding。其实就是为了确保周围的像素和中间的像素受到同等的对待。padding=1就是1层,实验证明padding是一层还是两层对结果影响不大。
2.池化
关于池化层,就更加简单了,我们参考《深度学习人工智能实践应用》这书。
这就是2x2的池化层,他会使得图像变小,但不会变多,卷积层刚好相反。在应用过程中,一般都会一层卷积加激活函数或者两层后面就是跟池化层。
3.全连接
接下来就是进行普通的神经网络,进行拉直,用softmax函数进行输出。
三 细节
-你知道为什么卷积神经网络参数与传统神经网络参数相差这么大吗
简单来说,我们在做卷积层的时候,参数就是卷积核的尺寸大小,但是如果我们做全连接层的时候,就不是这个大小了
四 用pytorch实战CNN
我们还是拿minst这个深度学习入门砖来敲,但是我个人觉得minst也是有点害人的,因为正是他是敲门转,所以几个常用深度学习框架对他都有优待,使得读取预处理操作非常之简便。
用CNN能达到百分之九十九点多,比人眼认还厉害,其实能突破以前的上限也是正常的,因为CNN更接近用人的观察图片方式去预测,因为人看图片肯定是一张一张看的嘛,不可能把像素拉平了再看,那么CNN的卷积层就很好的做到了这一点。
import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.optim as optim
import torch.nn.functional as F
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
# 定义超参数
image_size = 28 #图像的总尺寸28*28
num_classes = 10 #标签的种类数
num_epochs = 20 #训练的总循环周期
batch_size = 64 #一个撮(批次)的大小,64张图片
# 加载MINIST数据,如果没有下载过,就会在当前路径下新建/data子目录,并把文件存放其中
# MNIST数据是属于torchvision包自带的数据,所以可以直接调用。
# 在调用自己的数据的时候,我们可以用torchvision.datasets.ImageFolder或者torch.utils.data.TensorDataset来加载
train_dataset = dsets.MNIST(root='./data', #文件存放路径
train=True, #提取训练集
transform=transforms.ToTensor(), #将图像转化为Tensor,在加载数据的时候,就可以对图像做预处理
download=True) #当找不到文件的时候,自动下载
# 加载测试数据集
test_dataset = dsets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor())
# 训练数据集的加载器,自动将数据分割成batch,顺序随机打乱
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
'''我们希望将测试数据分成两部分,一部分作为校验数据,一部分作为测试数据。
校验数据用于检测模型是否过拟合,并调整参数,测试数据检验整个模型的工作'''
# 首先,我们定义下标数组indices,它相当于对所有test_dataset中数据的编码
# 然后定义下标indices_val来表示校验集数据的那些下标,indices_test表示测试集的下标
indices = range(len(test_dataset))
indices_val = indices[:5000]
indices_test = indices[5000:]
# 根据这些下标,构造两个数据集的SubsetRandomSampler采样器,它会对下标进行采样
sampler_val = torch.utils.data.sampler.SubsetRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler.SubsetRandomSampler(indices_test)
# 根据两个采样器来定义加载器,注意将sampler_val和sampler_test分别赋值给了validation_loader和test_loader
validation_loader = torch.utils.data.DataLoader(dataset =test_dataset,
batch_size = batch_size,
sampler = sampler_val
)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
sampler = sampler_test
)
#定义卷积神经网络:4和8为人为指定的两个卷积层的厚度(feature map的数量)
depth = [4, 8]
class ConvNet(nn.Module):
def __init__(self):
# 该函数在创建一个ConvNet对象的时候,即调用如下语句:net=ConvNet(),就会被调用
# 首先调用父类相应的构造函数
super(ConvNet, self).__init__()
# 其次构造ConvNet需要用到的各个神经模块。
'''注意,定义组件并没有真正搭建这些组件,只是把基本建筑砖块先找好'''
self.conv1 = nn.Conv2d(1, 4, 5, padding = 2) #定义一个卷积层,输入通道为1,输出通道为4,窗口大小为5,padding为2
self.pool = nn.MaxPool2d(2, 2) #定义一个Pooling层,一个窗口为2*2的pooling运算
self.conv2 = nn.Conv2d(depth[0], depth[1], 5, padding = 2) #第二层卷积,输入通道为depth[0],
#输出通道为depth[1],窗口为5,padding为2
self.fc1 = nn.Linear(image_size // 4 * image_size // 4 * depth[1] , 512)
#一个线性连接层,输入尺寸为最后一层立方体的平铺,输出层512个节点
self.fc2 = nn.Linear(512, num_classes) #最后一层线性分类单元,输入为512,输出为要做分类的类别数
def forward(self, x):
#该函数完成神经网络真正的前向运算,我们会在这里把各个组件进行实际的拼装
#x的尺寸:(batch_size, image_channels, image_width, image_height)
x = F.relu(self.conv1(x)) #第一层卷积,激活函数用ReLu,为了防止过拟合
#x的尺寸:(batch_size, num_filters, image_width, image_height)
x = self.pool(x) #第二层pooling,将图片变小
#x的尺寸:(batch_size, depth[0], image_width/2, image_height/2)
x = F.relu(self.conv2(x)) #第三层又是卷积,窗口为5,输入输出通道分别为depth[0]=4, depth[1]=8
#x的尺寸:(batch_size, depth[1], image_width/2, image_height/2)
x = self.pool(x) #第四层pooling,将图片缩小到原大小的1/4
#x的尺寸:(batch_size, depth[1], image_width/4, image_height/4)
# 将立体的特征图Tensor,压成一个一维的向量
# view这个函数可以将一个tensor按指定的方式重新排布。
# 下面这个命令就是要让x按照batch_size * (image_size//4)^2*depth[1]的方式来排布向量
x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
#x的尺寸:(batch_size, depth[1]*image_width/4*image_height/4)
x = F.relu(self.fc1(x)) #第五层为全链接,ReLu激活函数
#x的尺寸:(batch_size, 512)
x = F.dropout(x, training=self.training) #以默认为0.5的概率对这一层进行dropout操作,为了防止过拟合
x = self.fc2(x) #全链接
#x的尺寸:(batch_size, num_classes)
x = F.log_softmax(x, dim = 0) #输出层为log_softmax,即概率对数值log(p(x))。采用log_softmax可以使得后面的交叉熵计算更快
return x
def retrieve_features(self, x):
#该函数专门用于提取卷积神经网络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图
feature_map1 = F.relu(self.conv1(x)) #完成第一层卷积
x = self.pool(feature_map1) # 完成第一层pooling
feature_map2 = F.relu(self.conv2(x)) #第二层卷积,两层特征图都存储到了feature_map1, feature_map2中
return (feature_map1, feature_map2)
#定义卷积神经网络:4和8为人为指定的两个卷积层的厚度(feature map的数量)
depth = [4, 8]
class ConvNet(nn.Module):
def __init__(self):
# 该函数在创建一个ConvNet对象的时候,即调用如下语句:net=ConvNet(),就会被调用
# 首先调用父类相应的构造函数
super(ConvNet, self).__init__()
# 其次构造ConvNet需要用到的各个神经模块。
'''注意,定义组件并没有真正搭建这些组件,只是把基本建筑砖块先找好'''
self.conv1 = nn.Conv2d(1, 4, 5, padding = 2) #定义一个卷积层,输入通道为1,输出通道为4,窗口大小为5,padding为2
self.pool = nn.MaxPool2d(2, 2) #定义一个Pooling层,一个窗口为2*2的pooling运算
self.conv2 = nn.Conv2d(depth[0], depth[1], 5, padding = 2) #第二层卷积,输入通道为depth[0],
#输出通道为depth[1],窗口为5,padding为2
self.fc1 = nn.Linear(image_size // 4 * image_size // 4 * depth[1] , 512)
#一个线性连接层,输入尺寸为最后一层立方体的平铺,输出层512个节点
self.fc2 = nn.Linear(512, num_classes) #最后一层线性分类单元,输入为512,输出为要做分类的类别数
def forward(self, x):
#该函数完成神经网络真正的前向运算,我们会在这里把各个组件进行实际的拼装
#x的尺寸:(batch_size, image_channels, image_width, image_height)
x = F.relu(self.conv1(x)) #第一层卷积,激活函数用ReLu,为了防止过拟合
#x的尺寸:(batch_size, num_filters, image_width, image_height)
x = self.pool(x) #第二层pooling,将图片变小
#x的尺寸:(batch_size, depth[0], image_width/2, image_height/2)
x = F.relu(self.conv2(x)) #第三层又是卷积,窗口为5,输入输出通道分别为depth[0]=4, depth[1]=8
#x的尺寸:(batch_size, depth[1], image_width/2, image_height/2)
x = self.pool(x) #第四层pooling,将图片缩小到原大小的1/4
#x的尺寸:(batch_size, depth[1], image_width/4, image_height/4)
# 将立体的特征图Tensor,压成一个一维的向量
# view这个函数可以将一个tensor按指定的方式重新排布。
# 下面这个命令就是要让x按照batch_size * (image_size//4)^2*depth[1]的方式来排布向量
x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
#x的尺寸:(batch_size, depth[1]*image_width/4*image_height/4)
x = F.relu(self.fc1(x)) #第五层为全链接,ReLu激活函数
#x的尺寸:(batch_size, 512)
x = F.dropout(x, training=self.training) #以默认为0.5的概率对这一层进行dropout操作,为了防止过拟合
x = self.fc2(x) #全链接
#x的尺寸:(batch_size, num_classes)
x = F.log_softmax(x, dim = 0) #输出层为log_softmax,即概率对数值log(p(x))。采用log_softmax可以使得后面的交叉熵计算更快
return x
def retrieve_features(self, x):
#该函数专门用于提取卷积神经网络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图
feature_map1 = F.relu(self.conv1(x)) #完成第一层卷积
x = self.pool(feature_map1) # 完成第一层pooling
feature_map2 = F.relu(self.conv2(x)) #第二层卷积,两层特征图都存储到了feature_map1, feature_map2中
return (feature_map1, feature_map2)
net = ConvNet() #新建一个卷积神经网络的实例,此时ConvNet的__init__函数就会被自动调用
criterion = nn.CrossEntropyLoss() #Loss函数的定义,交叉熵
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) #定义优化器,普通的随机梯度下降算法
record = [] #记录准确率等数值的容器
weights = [] #每若干步就记录一次卷积核
#开始训练循环
for epoch in range(num_epochs):
train_rights = [] #记录训练数据集准确率的容器
''' 下面的enumerate是构造一个枚举器的作用。就是我们在对train_loader做循环迭代的时候,enumerate会自动吐出一个数字指示我们循环了几次
这个数字就被记录在了batch_idx之中,它就等于0,1,2,……
train_loader每迭代一次,就会吐出来一对数据data和target,分别对应着一个batch中的手写数字图,以及对应的标签。'''
for batch_idx, (data, target) in enumerate(train_loader): #针对容器中的每一个批进行循环
data, target = data.clone().requires_grad_(True), target.clone().detach() #data为一批图像,target为一批标签
net.train() # 给网络模型做标记,标志说模型正在训练集上训练,
#这种区分主要是为了打开关闭net的training标志,从而决定是否运行dropout
output = net(data) #神经网络完成一次前馈的计算过程,得到预测输出output
loss = criterion(output, target) #将output与标签target比较,计算误差
optimizer.zero_grad() #清空梯度
loss.backward() #反向传播
optimizer.step() #一步随机梯度下降算法
right = rightness(output, target) #计算准确率所需数值,返回数值为(正确样例数,总样本数)
train_rights.append(right) #将计算结果装到列表容器train_rights中
if batch_idx % 100 == 0: #每间隔100个batch执行一次打印等操作
net.eval() # 给网络模型做标记,标志说模型在训练集上训练
val_rights = [] #记录校验数据集准确率的容器
'''开始在校验数据集上做循环,计算校验集上面的准确度'''
for (data, target) in validation_loader:
data, target = data.clone().requires_grad_(True), target.clone().detach()
output = net(data) #完成一次前馈计算过程,得到目前训练得到的模型net在校验数据集上的表现
right = rightness(output, target) #计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
val_rights.append(right)
# 分别计算在目前已经计算过的测试数据集,以及全部校验集上模型的表现:分类准确率
#train_r为一个二元组,分别记录目前已经经历过的所有训练集中分类正确的数量和该集合中总的样本数,
#train_r[0]/train_r[1]就是训练集的分类准确度,同样,val_r[0]/val_r[1]就是校验集上的分类准确度
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
#val_r为一个二元组,分别记录校验集中分类正确的数量和该集合中总的样本数
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
#打印准确率等数值,其中正确率为本训练周期Epoch开始后到目前撮的正确率的平均值
print(val_r)
print('训练周期: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t训练正确率: {:.2f}%\t校验正确率: {:.2f}%'.format(
epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.data,
100. * train_r[0].numpy() / train_r[1],
100. * val_r[0].numpy() / val_r[1]))
#将准确率和权重等数值加载到容器中,以方便后续处理
record.append((100 - 100. * train_r[0] / train_r[1], 100 - 100. * val_r[0] / val_r[1]))
# weights记录了训练周期中所有卷积核的演化过程。net.conv1.weight就提取出了第一层卷积核的权重
# clone的意思就是将weight.data中的数据做一个拷贝放到列表中,否则当weight.data变化的时候,列表中的每一项数值也会联动
'''这里使用clone这个函数很重要'''
weights.append([net.conv1.weight.data.clone(), net.conv1.bias.data.clone(),
net.conv2.weight.data.clone(), net.conv2.bias.data.clone()])
#绘制训练过程的误差曲线,校验集和测试集上的错误率。
plt.figure(figsize = (10, 7))
plt.plot(record) #record记载了每一个打印周期记录的训练和校验数据集上的准确度
plt.xlabel('Steps')
plt.ylabel('Error rate')
#在测试集上分批运行,并计算总的正确率
net.eval() #标志模型当前为运行阶段
vals = [] #记录准确率所用列表
#对测试数据集进行循环
for data, target in test_loader:
data, target = data.clone().detach().requires_grad_(True), target.clone().detach()
output = net(data) #将特征数据喂入网络,得到分类的输出
val = rightness(output, target) #获得正确样本数以及总样本数
vals.append(val) #记录结果
#计算准确率
rights = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
right_rate = 100. * rights[0].numpy() / rights[1]
print(right_rate)