背景
LeNET-5是最早的卷积神经网络之一,曾广泛用于美国银行。手写数字识别正确率在99%以上。
PyTorch是Facebook 人工智能研究院在2017年1月,基于Torch退出的一个Python深度学习的库。他是一个基于Python的可续计算包,提供两个高级功能:
1、具有强大的GPU加速的张量运算(如NumPy)。
2、包含自动求导系统的深度神经网络。
LeNet 5网络介绍
LeNet 5的网络结构图如下
虽然被称为LeNet 5,但这个网络不包括输入的话,一共有7层,1卷积层、2池化层、3卷积层、4池化层、5全连接层、6全连接层、7分类层(之后接输出)。
虽然LeNet 5 网络不大,但是包含了深度学习的基本模块:卷积层、池化层和全连接层,每一层都包含可训练参数,每个层有多个特征图,每个特征图 是由一个卷积滤波器提取出来的一种特征,然后每个特征图有多个神经元。
输入:LeNet 5网络因为有全连接层(全连接层就是)的存在,所以输入是固定的,是28*28的二维图片。
关于全连接层的知识,可以参考这篇博客
输出:输出的为分类结果,0-9之间的一个数,这个是由最后的softmax输出的。
关于softmax的具体知识,可以参考这篇文章。
原始的LeNet5
体验
不像网上的那些博客那样,上来就堆一大堆代码,我觉得一开始的体验是很有必要的,你直接下载我的这个代码,就可以无痛体验手写数字的分类结果。
先体验这个,运行Test.py即可。
会在窗口中显示下面的界面。
红框中的这个数字就是对手写数字的识别率了,96.24%,不是特别高,这是因为我只训练了10个epoch的缘故,你要是训练100个epoch的话,应该可以达到99%以上的epoch。
体验完这个Test.py之后,有没有感觉,只有一个黑框和数字的话,看起来很不爽,那么可以试一下下面这个Test_vis.py,直接以可视化的形式实时显示你的识别结果。(visdom或者plot的形式来展现这个结果)
LeNet 5训练的代码
import torch import torch.nn as nn # 用nn来创建神经网络的各个层、损失函数和激活函数。 import torch.optim as optime # 导入PyTorch的优化器包 from torchvision import datasets, transforms # datasets是 常见视觉数据集的数据加载器;# transforms可以进行常见的图像变换,如随机裁剪、旋转等 from torch.autograd import Variable # 自动求导数用 from torch.utils.data import DataLoader # 读取数据用的,用来将自定义的数据或者其他数据封装成Tensor。 class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(1, 6, 3, 1, 2), nn.ReLU(), nn.MaxPool2d(2, 2) ) self.conv2 = nn.Sequential( nn.Conv2d(6, 16, 5), nn.ReLU(), nn.MaxPool2d(2, 2) ) self.fc1 = nn.Sequential( nn.Linear(16 * 5 * 5, 120), # 输出的维度是120 nn.BatchNorm1d(120), # 那么这一层对应的输入就是120了 nn.ReLU() ) self.fc2 = nn.Sequential( nn.Linear(120, 84), nn.BatchNorm1d(84), # 批标准化,可以加快神经网络的收敛速度(注:批标准化一般放在全连接层后面,激活函数层的前面) nn.ReLU() ) self.fc3 = nn.Linear(84, 10) def forward(self, x): x1 = self.conv1(x) x2 = self.conv2(x1) x3 = x2.view(x2.size()[0], -1) # 调整张量的维度 x4 = self.fc1(x3) x5 = self.fc2(x4) x6 = self.fc3(x5) return x6 def train_net(): global device, batch_size, LR, Momentum, train_dataset, test_dataset, train_loader, test_loader global net, criterion, optimizer, epochs for epoch in range(epochs): # 走一个循环 sum_loss = 0.0 # 损失函数一开始的时候设为0 print(epoch) for i, data in enumerate(train_loader): # 通过enumerate的迭代,从训练集中取数据,i为索引,data为train_loader中的数据 # train_loader的长度为928,那么938*64=60032,正好是训练集数据的大小 # 这个循环一共会进行928次,到第900之后,就不会有数据打印出来了 inputs, labels = data # input为输入的数据,label为标签 inputs = inputs.cuda() labels = labels.cuda() inputs, labels = Variable(inputs), Variable(labels) # 将Tensor数据转换为Variable类型 optimizer.zero_grad() # 将梯度归零 outputs = net(inputs) # 将数据传入网络进行前向运算 loss = criterion(outputs, labels) # criterion是用torch.nn创建的交叉熵函数, # 利用输出的预测值和标签之间的差值,得到损失函数的计算值 # vis.line(Y=torch.FloatTensor([loss]), win='loss',update='append' if i > 0 else None) loss.backward() # loss反向传播 optimizer.step() # 使用优化器,对模型权重进行更新 sum_loss = sum_loss + loss.item() # 将这次bitch的误差进行累计 if i % 100 == 0: # 每100次输出一下当前的loss print('[{}-{} loss:{}]'.format(epoch + 1, i + 1, sum_loss / 100)) sum_loss = 0 torch.save(net, 'net-10 epochs.pth') return net def save_net(net, net_filename): torch.save(net, net_filename) def init(): global device, batch_size, LR, Momentum, train_dataset, train_loader global net, criterion, optimizer, epochs device = torch.device('cuda') # device设为用CPU进行训练 batch_size = 10000 # 每一次训练所取的样本数是64个 LR = 0.001 # 学习率 Momentum = 0.9 # 动量是0.9 net = LeNet().to(device) # 实例化一个LeNet网络,同时设定设备是CPU还是GPU criterion = nn.CrossEntropyLoss() # 定义损失函数,这个是用的torch.nn方法建立的交叉熵函数 optimizer = optime.SGD(net.parameters(), lr=LR, momentum=Momentum) # 用的是SGD随机梯度下降算法, # 里面定义好网络的参数,学习率以及动量的大小 # 将网络的各种配置参数存入到优化器中,以便之后更新网络用 epochs = 10 # 使用训练集的全部数据对模型进行一次完整训练,被称之为一代训练 train_dataset = datasets.MNIST(root='./data', # 载入训练的数据集 train=True, # 设定这个是训练用的 transform=transforms.ToTensor(), # 将载入的数据集转变为tensor download=True) # 询问是否要进行数据集的下载,若是下载好了的话,就不用再下载了 # 建立一个数据迭代器,将数据按照batch_size的设置分成若干个小块儿 train_loader = torch.utils.data.DataLoader(dataset=train_dataset, # 将读取的数据转变为TenSor batch_size=batch_size, # 将Tensor按照之前的batch_size进行分类 shuffle=True) def main(): init() train_net() main()
训练代码如上所示,我也是新手,我们就一步一步来看。
代码主要分为导入包、参数初始化、定义LeNet 5的网络、网络训练以及保存模型5部分。
1.导入包
这几个包的用途,我都在后面详细做了详细注释。
import torch import torch.nn as nn # 用nn来创建神经网络的各个层、损失函数和激活函数。 import torch.optim as optime # 导入PyTorch的优化器包,可以从这个包中选择预先定义的SGD等优化函数 from torchvision import datasets, transforms # datasets是 常见视觉数据集的数据加载器;# transforms可以进行常见的图像变换,如随机裁剪、旋转等 from torch.autograd import Variable # 自动求导数用 from torch.utils.data import DataLoader # 读取数据用的,用来将自定义的数据或者其他数据封装成Tensor。
2.参数初始化
这里,我将参数初始化来写到这个init()函数中了,初始化的,主要有
batch_size,就是网络每一次运行的时候,加载多少数据,你可以类比,batch size就是公交汽车拉人的数量,一次拉的人的数量越多,肯定效率越高,但是这个又不能超过汽车的运载能力。因为这里跑的是MNIST数据集,里面的图片比较小,所以我设的比较大,你可以根据自己电脑的配置来选择自己的batch size。
LR,学习率。想象一个场景,你在指挥一个盲人往前去一个目标,你每次都只能告诉他应该往前还是往后,这个学习率对应的就是盲人每次移动的步伐。步伐太大,可能会错过目的地点,步伐太小,又会走得太慢。因此正确的做法是,在一开始距离目标比较远的时候,将这个步伐设的大一点儿,等距离目标比较近的时候,将这个步伐设的小一点儿。实际上,在深度学习中,学习率也是这样设定的。
Momentum,动量值,这个值是用来设定SGD的重力加速度的。
epoch,训练次数。
train_dataset ,是下载或者读取本地的MNIST数据集。因为后面download的参数设置的为True,因此这个函数实现的功能就是:若是本地已经有这个MNIST数据集的话,那么就读取本地的数据集;若是本地没有这个MNIST的数据集的话,那么就先下载这个数据集再进行读取。
train_loader,这个是在读取的MNIST数据集train_dataset的基础上,建立一个数据迭代器,将数据按照batch_size的大小,分成若干个小块儿,MNIST的训练集的大小为60000,若是batch_size的大小为100的话,那么这个train_loader就会被分割为600个小块儿;若是batch size的大小为1000的话,那么这个train_loader就会被分割为60个小块儿。
criterion,定义损失函数的类型,这里使用的是torch.nn自带的交叉熵损失函数CrossEntropyLoss()
def init(): global device, batch_size, LR, Momentum, train_dataset, train_loader global net, criterion, optimizer, epochs device = torch.device('cuda') # device设为用CPU进行训练 batch_size = 10000 # 每一次训练所取的样本数是64个 LR = 0.001 # 学习率 Momentum = 0.9 # 动量是0.9 net = LeNet().to(device) # 实例化一个LeNet网络,同时设定设备是CPU还是GPU criterion = nn.CrossEntropyLoss() # 定义损失函数,这个是用的torch.nn方法建立的交叉熵函数 optimizer = optime.SGD(net.parameters(), lr=LR, momentum=Momentum) # 用的是SGD随机梯度下降算法, # 里面定义好网络的参数,学习率以及动量的大小 # 将网络的各种配置参数存入到优化器中,以便之后更新网络用 epochs = 10 # 使用训练集的全部数据对模型进行一次完整训练,被称之为一代训练 train_dataset = datasets.MNIST(root='./data', # 载入训练的数据集 train=True, # 设定这个是训练用的 transform=transforms.ToTensor(), # 将载入的数据集转变为tensor download=True) # 询问是否要进行数据集的下载,若是下载好了的话,就不用再下载了 # 建立一个数据迭代器,将数据按照batch_size的设置分成若干个小块儿 train_loader = torch.utils.data.DataLoader(dataset=train_dataset, # 将读取的数据转变为TenSor batch_size=batch_size, # 将Tensor按照之前的batch_size进行分类 shuffle=True)
3.定义网络
class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(1, 6, 3, 1, 2), nn.ReLU(), nn.MaxPool2d(2, 2) ) self.conv2 = nn.Sequential( nn.Conv2d(6, 16, 5), nn.ReLU(), nn.MaxPool2d(2, 2) ) self.fc1 = nn.Sequential( nn.Linear(16 * 5 * 5, 120), # 输出的维度是120 nn.BatchNorm1d(120), # 那么这一层对应的输入就是120了 nn.ReLU() ) self.fc2 = nn.Sequential( nn.Linear(120, 84), nn.BatchNorm1d(84), # 批标准化,可以加快神经网络的收敛速度(注:批标准化一般放在全连接层后面,激活函数层的前面) nn.ReLU() ) self.fc3 = nn.Linear(84, 10) def forward(self, x): x1 = self.conv1(x) x2 = self.conv2(x1) x3 = x2.view(x2.size()[0], -1) # 调整张量的维度 x4 = self.fc1(x3) x5 = self.fc2(x4) x6 = self.fc3(x5) return x6
这个LeNet5代码的话,网络参数分别为(注意这里的层数是说的网络实际上的小层数,卷积和池化都算作是分别的层,而不是在定义的时候组合出的大层):
第1层:卷积层,nn.Conv2d(1, 6, 3, 1, 2),表示输入的为1通道,输出的为6通道,卷积核的大小为3,卷积步长为1,padding为2,原始输入为,28 * 28,卷积过后的计算公式为,(n + 2p - f)/s + 1,其中n为输入图像的大小,p为padding,f为卷积的大小,s为步长,那么这一层之后的特征图大小为(28+ 2*2 - 3)/1+1 = 30,其实是有6个卷积核,那么这一层之后输出最终为,30 * 30* 6。
第2层:池化层,nn.MaxPool2d(2, 2),表示池化的卷积核大小为2,窗口移动的步长为2,没有padding。还是套那个卷积的公式(n + 2p - f)/s + 1,(30+ 2 *0 - 2) / 2 +1 = 15,相当于将上一层输送过来的特征图隔两个取一个点,最终输出的为 15 * 15 * 6.
第3层:卷积层,nn.Conv2d(6, 16, 5),输入为6通道,输出为16通道,卷积核的大小为5,步长s为1,padding为0,输入为 15 * 15,那么输出为(15 + 2*0 - 5)/1 +1 = 11,特征图为 16 * 11 * 11
第4层:池化层,n.MaxPool2d(2, 2),输入为11 * 11,经过池化层之后,会得到小数5.5,一般这种情况,就是取整了,不过是向下取整,也就是5,因此这一层之后最终输出的特征图为 16 * 5 * 5
第5层:全连接层,也叫做线性层,Linear(16 * 5 * 5, 120),输入为 16 * 5 * 5 = 400个神经元(之前上一层输出的特征图上面的每一个点都称为一个神经元),输出为120个神经元。
第6层:全连接层,nn.Linear(120, 84),输入为 120 个神经元,输出为84个神经元。
第7层:全连接层,nn.Linear(84, 10),输入为84个神经元,输出为10个神经元,对应的就是那10个手写数字了。
x3 = x2.view(x2.size()[0], -1) # 调整张量的维度,将其来铺成线性单维度的向量,
2为batch size的大小,400则为特征图铺成一个向量的神经元的数量。
最后的这三层全连接层可视化之后就是下面这种效果。黑色区域不是真的黑色区域,而是因为这些个线太密了,所以看上去才像是黑色。
这个是搭建网络过程中的各个子函数的具体对应及其参数
torch.nn.Conv2d
普通卷积
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
in_channels:输入通道数
out_channels:输出通道数(卷积核的个数,有几个卷积核就会输出几个通道的维度)
kernel_size:卷积核的尺寸
stride:卷积步长(默认值为1)
padding:输入在每一条边补充0的层数(默认不补充)
dilation:卷积核元素之间的间距(默认值为1)
groups:从输入通道到输出通道的阻塞连接数(默认值是1)
bias:是否添加偏置(默认是True)
torch.nn.MaxPool2d
最大池化
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
kernel_size:池化的窗口大小
stride:窗口移动的步长,默认值是kernel_size
padding:输入的每一条边补充0的层数
dilation:一个控制窗口中元素步幅的参数
return_indices:如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
ceil_mode:如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作
torch.nn.Linear
线性变换
torch.nn.Linear(in_features,out_features,bias = True)
in_features:每个输入样本的大小
out_features: 每个输出样本的大小
bias:如果设置为False,则图层不会学习附加偏差。默认值:True。
def __init__(self)是对网络进行定义,而def forward(self, x)则是将网络进行前向传播,给定一个28*28的图像,他会沿着这个网络进行传播,最终输出的为10个神经元。
4.训练
训练的时候按照epoch进行,有多少个epoch就会进行多少个循环。
进入epoch循环之后,先将损失函数的计算值设为0,每一次进行epoch之后,损失函数的值都需要先清为0。
进入epoch大循环之后,是一个以batch size为单位的小循环,enumerate后面跟的是训练集,
enumerate是一个迭代器,通过enumerate的迭代,从训练集中取数据,i为索引,data为train_loader中的数据,不过这个i是其对应的在train_loader中的索引,在这个网络的训练中其实用不到。
(感觉这个代码也没有什么好讲解的呀,所有的内容自己都已经是写在了这个程序注释里面,再说的话,就是废话了)
data是一个list列表,长度为2,也就是说data里面有两个数据,第一个数据是手写数字机的图像inputs,为Tensor,第二个数据是图像对应的标签,为0-9之间的数字。inputs里面图片的个数,和batch size的大小相同(data最原始就是由batch size划分出来的,大小肯定是和batch size相同啦~)。
下面为data的打印值
此时这个inputs和labels还知识普通的Tensor,需要将其转换为cuda类型的Tensor,这样才可以调用GPU。
optimizer梯度优化器,简单来说就是你要以什么样的方式来将这个网络的梯度降低,自己这里选用的是SGD,可以选用的还有Adam,AdamW。在进行梯度传播之前,需要先将这个梯度清零才行。
梯度清零之后,终于可以将inputs这个Tensor来输入到网络net中进行传播了,输出的outputs为Tensor,多维度的,有几个batch size,这个outputs里面就会有几个Tensor。每个Tensor其实是输出的为
loss = criterion(outputs, labels)
每一次一次传播之后,通过交叉熵损失函数来计算输出预测值与实际标签之间的差距,然后返回为一个值,这个值就叫做loss。
其中预测值和标签的值显示分别如下:
注意,是预测值在前面,标签在后面才行,顺序反了的话,是会报错的。
optimizer.step()
之后再将这个梯度反向传播回网络的各层中,网络各层中的各个神经元,根据这个loss的值来相应地进行自身权值的调整。如下图所示
optimizer.step()
不过将loss反向传播到网络中后,知识具备了调整权值的条件,但具体怎么调整权值,以什么方式来调整权值,则是由这个optimizer优化器函数来决定的,optimizer.step()就是使用内置的规则SGD,根据LOSS来对模型的权值进行调整。调整完权值之后,至此,一次网络的训练过程完毕。
sum_loss = sum_loss + loss.item()
下面将loss累加一下,其实这个loss累加,只是为了可视化,实际上对于网络训练来说,并没有什么实质性的作用,因此,即使将其删掉,也问题不大。
而至于为什么要用loss的item呢?item()这个函数可以将
测试部分
_, predicted = torch.max(outputs, 1)
这个torch.max输出出来的为
也就是说输出的Tensor里面,包含着两个Tensor,第一个Tensor为具体的最大的值,第二个Tensor为这个Tensor所在位置的索引。
(待更)