LeNet的网络模型框架如下图所示:
本文运行的模型是基于LeNet做简单修改,层数很浅。
Lenet包含输入共有8层,每层都包含可训练参数。分为卷积层块和全连接层块两个部分:
第一层:输入层。输入图像的尺寸为32*32的RGB图像
第二层:C1卷积层。它由16个特征图Feature Map构成,5*5大小的卷积核。而特征图中每个神经元与输入中5*5的邻域相连。特征图的大小为28*28(32-5+1=28),所以每个特征图上有28*28个神经元。
第三层:S2层是一个下采样层,有16个14*14的特征图。特征图中的每个单元与C1中相对应特征图的2*2邻域相连接(S2层每个单元的4个输入相加,乘以一个可训练参数,再加上一个可训练偏置,结果通过sigmoid函数计算而得)。S2中每个特征图的大小是C1中特征图大小的1/4(行和列各1/2),所以S2中每一个特征图都有14*14个神经元。
第四层:C3层是第二个卷积层。它同样通过5x5的卷积核去卷积层S2,然后得到的特征map就只有10x10个神经元,但是它有16种不同的卷积核,所以就存在16个特征map了。
第五层:S4层是一个下采样层,由32个5*5大小的特征图构成。特征图中的每个单元与C3中相应特征图的2*2邻域相连接,跟C1和S2之间的连接一样。
第六层:C5层是一个卷积层,有120个特征图。每个单元与S4层的全部32个特征图的5*5领域相连。由于S4层特征图的大小也为5*5(同滤波器一样),故C5特征图的大小为1*1,这构成了S4和C5之间的全连接。
第七层:F6层有84个单元(之所以选这个数字的原因来自于输出层的设计),与C5层全相连。
第八层:输出层由欧式径向基函数单元组成,每类一个单元,每个有84个输入。换句话说,每个输出RBF单元计算输入向量和参数向量之间的欧式距离。
代码:
1. model.py
# 使用torch.nn包来构建神经网络.
import torch.nn as nn
import torch.nn.functional as F
class LeNet(nn.Module): # 继承于nn.Module这个父类
def __init__(self): # 初始化网络结构
super(LeNet, self).__init__() # 多继承需用到super函数
self.conv1 = nn.Conv2d(3, 16, 5)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x): # 正向传播过程
x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28)
x = self.pool1(x) # output(16, 14, 14)
x = F.relu(self.conv2(x)) # output(32, 10, 10)
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5)
x = F.relu(self.fc1(x)) # output(120)
x = F.relu(self.fc2(x)) # output(84)
x = self.fc3(x) # output(10)
return x
1.1 卷积 Conv2d
我们常用的卷积(Conv2d)在pytorch中对应的函数是:
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
in_channel: 输入数据的通道数,例RGB图片通道数为3;
out_channel: 输出数据的通道数,这个根据模型调整(卷积核个数);
kennel_size: 卷积核大小,可以是int,或tuple;kennel_size=2,意味着卷积大小2, (kennel_size=(2,3),意味着卷积在第一维度大小为2,在第二维度大小为3);
stride:步长,默认为1,与kennel_size类似,stride=2,意味在所有维度步长为2, (stride=(2,3),意味着在第一维度步长为2,意味着在第二维度步长为3);
padding:零填充
例:第一个输出元素及其计算所使用的输入和核数组元素: 0×0+1×1+3×2+4×3=19
代码即:
self.conv1 = nn.Conv2d(3, 16, 5)
另外,经卷积后的输出层尺寸计算公式为:
- 输入图片大小 W×W(一般情况下Width=Height)
- Filter大小 F×F
- 步长 S
- padding的像素数 P
x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28)
即:
16为卷积核个数,即输出的深度
1.2 池化 MaxPool2d
最大池化(MaxPool2d)在 pytorch 中对应的函数是:
class torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
kernel_size(int or tuple) - max pooling的窗口大小
stride(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
padding(int or tuple, optional) - 输入的每一条边补充0的层数
dilation(int or tuple, optional) – 一个控制窗口中元素步幅的参数
return_indices - 如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
ceil_mode - 如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作
取这个窗口覆盖元素中的最大值(3x3)。
即:
self.pool1 = nn.MaxPool2d(2, 2)
1.3 Tensor的展平:view()
在经过第二个池化层后,数据还是一个三维的Tensor (32, 5, 5),需要先经过展平后(32*5*5)再传到全连接层:
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5)
x = F.relu(self.fc1(x)) # output(120)
在函数的参数中经常可以看到-1例如x.view(-1, 4)
这里-1表示一个不确定的数,就是你如果不确定你想要reshape成几行,但是你很肯定要reshape成4列,那不确定的地方就可以写成-1
例如一个长度的16向量x,
x.view(-1, 4)等价于x.view(4, 4)
x.view(-1, 2)等价于x.view(8,2)
1.4 全连接 Linear
全连接( Linear)在 pytorch 中对应的函数是:
Linear(in_features, out_features, bias=True)
in_features输入的是二维张量的大小,输入[N, Hin]作为全连接层的输入,N一般指的是batch_size(样本数量),Him指的的是每一个样本的维度大小
out_features指的是输出的二维张量的大小,输出(N,Hout)N一般指的是batch_size(样本数量),Hout指的是每一个样本输出的维度大小,也代表了该全连接层的神经元个数
相当于一个输入为[batch_size, in_features]的张量变换成了[batch_size, out_features]的输出张量
代码即:
self.fc1 = nn.Linear(32*5*5, 120)
2. train.py
import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
def main():
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 50000张训练图片
# 第一次使用时要将download设置为True才会自动去下载数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=False, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
shuffle=True, num_workers=0)
# 10000张验证图片
# 第一次使用时要将download设置为True才会自动去下载数据集
val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=False, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader)
val_image, val_label = val_data_iter.next()
# classes = ('plane', 'car', 'bird', 'cat',
# 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
net = LeNet()# 定义训练的网络模型
loss_function = nn.CrossEntropyLoss()# 定义损失函数为交叉熵损失函数
optimizer = optim.Adam(net.parameters(), lr=0.001)# 定义优化器(训练参数,学习率)
for epoch in range(5): # 一个epoch即对整个训练集进行一次训练
running_loss = 0.0
for step, data in enumerate(train_loader, start=0): # 遍历训练集,step从0开始
inputs, labels = data # 将数据分离成输入的图像和其所对应的标签
optimizer.zero_grad() # 清除历史梯度
outputs = net(inputs) # 正向传播,将图片数据输入模型中
loss = loss_function(outputs, labels) # 计算损失,传入预测值和真实值,计算当前损失值
loss.backward() # 反向传播,计算当前梯度
optimizer.step() # 优化器更新参数,根据梯度更新网络参数
# 打印耗时、损失、准确率等数据
running_loss += loss.item()# 计算该轮的总损失,因为loss是tensor类型,所以需要用item()取具体值
if step % 500 == 499: # 每500步打印一次数据
with torch.no_grad():# 在以下步骤中(验证过程中)不用计算每个节点的损失梯度,防止内存占用
outputs = net(val_image) # 测试集传入网网络(test_batch_size=10000),output维度为[500,10]
predict_y = torch.max(outputs, dim=1)[1] # 以output中值最大位置对应的索引(标签)作为预测输出
accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)
print('[%d, %5d] train_loss: %.3f test_accuracy: %.3f' %
(epoch + 1, step + 1, running_loss / 500, accuracy))# 打印epoch,step,loss,accuracy
running_loss = 0.0
print('Finished Training')
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)#将网络的所有参数进行保存
2.1 数据预处理
对输入的图像数据做预处理,即由shape (H x W x C) in the range [0, 255] → shape (C x H x W) in the range [0.0, 1.0]
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
transforms.ToTensor()的操作对象有PIL格式的图像以及numpy(即cv2读取的图像也可以)这两种。对象不能是tensor格式的,因为是要转换为tensor的
ToTensor()的[0,1]只是范围改变了, 并没有改变分布,mean和std处理后可以让数据正态分布
- 先由HWC转置为CHW格式;
- 再转为float类型;
- 最后,每个像素除以255。
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
Pytorch图像预处理时,通常使用transforms.Normalize(mean, std)
对图像按通道进行标准化,即减去均值,再除以方差。这样做可以加快模型的收敛速度。其中参数mean和std分别表示图像每个通道的均值和方差序列。
2.2 数据加载
利用torchvision.datasets
函数可以在线导入pytorch中的数据集。此demo用的是CIFAR10数据集,一个用于识别普适物体的小型数据集,一共包含 10 个类别的 RGB 彩色图片。
torchvision.datasets.CIFAR10
CIFAR-10 是由 Hinton 的学生 Alex Krizhevsky 和 Ilya Sutskever 整理的一个用于识别普适物体的小型数据集。一共包含 10 个类别的 RGB 彩色图 片:飞机( plane )、汽车( automobile )、鸟类( bird )、猫( cat )、鹿( deer )、狗( dog )、蛙类( frog )、马( horse )、船( ship )和卡车( truck )。图片的尺寸为 32×32 ,数据集中一共有 50000 张训练圄片和 10000 张测试图片。 CIFAR-10 的图片样例如图所示。
:
导入、加载 训练集
# 导入50000张训练图片
train_set = torchvision.datasets.CIFAR10(root='./data', # 数据集存放目录
train=True, # 表示是数据集中的训练集
download=True, # 第一次运行时为True,下载数据集,下载完成后改为False
transform=transform) # 预处理过程
# 加载训练集,实际过程需要分批次(batch)训练
train_loader = torch.utils.data.DataLoader(train_set, # 导入的训练集
batch_size=50, # 每批训练的样本数
shuffle=False, # 是否打乱训练集
num_workers=0) # 使用线程数,在windows下设置为0
导入、加载 测试集
# 导入10000张测试图片
test_set = torchvision.datasets.CIFAR10(root='./data',
train=False, # 表示是数据集中的测试集
download=False,transform=transform)
# 加载测试集
test_loader = torch.utils.data.DataLoader(test_set,
batch_size=10000, # 每批用于验证的样本数
shuffle=False, num_workers=0)
# 获取测试集中的图像和标签,用于accuracy计算
test_data_iter = iter(test_loader)
test_image, test_label = test_data_iter.next()
2.3 训练模型
(1)训练过程
Epoch, Batch, Iteration
换算关系
(2)
遍历训练集
enumerate()
函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for
循环当中。
enumerate(sequence, start=0)
sequence
:列表、元组或字符串start
:开始计数的初始下标
(3)清除历史梯度
optimizer.zero_grad()
每个batch必定执行的操作,当网络参量进行反馈时,梯度是被积累的而不是被替换掉。但是在每一个batch时毫无疑问并不需要将两个batch的梯度混合起来累积,因此这里就需要每个batch设置一遍zero_grad 了
with torch.no_grad()
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭/线程中锁的自动获取和释放等。
在with torch.no_grad()模块下,所有计算得出的tensor的requires_grad都自动设置为False,实现不计算梯度。
(4) 损失函数
衡量模型输出与其真是标签的差异
nn.CrossEntropyLoss(
weight, 各类别的loss设置权值
size_average,
ignore_index,忽略某个类别
reduce,
reduction:计算模式:none逐个元素计算,sum:所有元素求和 mean:加权求和
)
先定义loss:loss_function=nn.CrossEntropyLoss()
然后在训练的时候调用loss_function:loss_function(outputs,labels)
loss_function = nn.CrossEntropyLoss()
loss = loss_function(outputs, labels)#用定义的函数计算损失,参数为:网络预测的值,输入图片对应的真是标签
loss.backward()#将loss进行反向传播
optimizer.step()#进行参数的更新
running_loss += loss.item()#每计算一个loss就将其追加到变量中
(5)测试结果
predict_y = torch.max(outputs, dim=1)[1]
寻找输出最大可能的标签类别 dim=1是在第一个维度寻找,结果为(batch, 10)的形式
max这个函数返回[最大值, 最大值索引],我们只需要取索引就行了,所以用[1]
运行如下:
3. predict.py
# 导入包
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
# 数据预处理
transform = transforms.Compose(
[transforms.Resize((32, 32)), # 首先需resize成跟训练集图像一样的大小
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 导入要测试的图像(自己找的,不在数据集中),放在源文件目录下
im = Image.open('horse.jpg')
im = transform(im) # [C, H, W]
im = torch.unsqueeze(im, dim=0) # 对数据增加一个新维度,因为tensor的参数是[batch, channel, height, width]
# 实例化网络,加载训练好的模型参数
net = LeNet()
net.load_state_dict(torch.load('Lenet.pth'))
# 预测
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
with torch.no_grad():
outputs = net(im)
predict = torch.max(outputs, dim=1)[1].data.numpy()
print(classes[int(predict)])
3.1 图片处理 及加载
im = torch.unsqueeze(im, dim=0)
增加一个维度,(channels, height, width) >(batch, channels, height, width),pytorch要求必须输入这样的shape
net.load_state_dict(torch.load('Lenet.pth'))
在pytorch中构建好一个模型后,一般需要进行预训练权重中加载。torch.load_state_dict()函数就是用于将预训练的参数权重加载到新的模型之中。
当strict=True,要求预训练权重层数的键值与新构建的模型中的权重层数名称完全吻合;如果新构建的模型在层数上进行了部分微调,则上述代码就会报错:说key对应不上。此时,如果我们采用strict=False 就能够完美的解决这个问题。也即,与训练权重中与新构建网络中匹配层的键值就进行使用,没有的就默认初始化。
其他部分与训练模型相似。
预测结果:
也不是很准。