LeNet-5的结构如下图所示,网络一共有7层(不包含输入层):
图中的Convolutions代表卷积、Subsampling代表池化、Full connection代表全连接层。
卷积层的作用为提取图像特征,输入为1×32×32的原始图像,卷积核大小为5×5,得到的feature map大小为28×28。卷积核个数为6,卷积得到feature map的通道也为6,所以得到的feature map为6×28×28。
池化层用于压缩数据和参数的量。输入为C1的输出6×28×28的feature map,按步幅2进行2 × 2的Max池化,输出的feature map大小是14×14。而池化不影响通道数,所以得到的feature map为6×14×14。
输入为S2的输出6×14×14的feature map,卷积核大小为5×5,卷积得到的输出feature map大小为10。卷积核个数为16,卷积得到feature map的通道也为16,所以得到的feature map为16×10×10。
输入为C1的输出16×10×10的feature map,按步幅2进行2 × 2的Max池化,输出的feature map大小是5×5。而池化不影响通道数,所以得到的feature map为16×5×5。
该层的输入为S4层输出的16×5×5大小的feature map,卷积核大小为5×5,卷积核个数为120,卷积得到feature map的通道也为16,所以得到的feature map为120×1×1。其实也可以理解成全连接层,因为这里5×5的输入,用5×5的卷积核做卷积,最后输出1×1
该层的输入为C5的120维向量计算方式:计算输入向量和权重向量之间的点积,再加上一个偏置,结果通过sigmoid函数输出。
Output层也是全连接层,共有10个节点,分别代表0~9。
LeNet-5是一种用于手写体字符识别的非常高效的卷积神经网络,它被美国银行用于手写数字识别。卷积神经网络能够很好的利用图像的结构信息。卷积层的参数较少,这也是由卷积层的主要特性即局部连接和共享权重所决定。下图是数组3的识别过程。
模型的建立稍与LeNet-5有所不同。我们在卷积层C1使用16个卷积核,卷积层C3使用32个卷积核。而C5我们不选择用120个卷积核,而选择展开32×5×5用全连接层得到120×1×1。
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
完整实现如下:
import torch.nn as nn
import torch.nn.functional as F
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
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
下载CIFAR-10数据集。第一次使用时要将download设置为True会自动去下载数据集,下载完成后将True改成False。root指的是下载路径。train为True会导入训练集图片。transforms表示对图像进行预处理。
transforms.ToTensor():
Converts a PIL Image or numpy.ndarray (H x W x C) in the range [0, 255]
to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0]
简单来说就是将其他图片格式转化成torch.FloatTensor格式,闭区间0~1范围内C x H x W
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)):
Given mean: ``(mean[1],...,mean[n])`` and std: ``(std[1],..,std[n])`` for ``n``
channels, this transform will normalize each channel of the input
简单来说就是根据给定的均值和标准差来标准化
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=True, transform=transform)
下载完成数据集后,导入数据集:
# 50000张训练图片
# 第一次使用时要将download设置为True才会自动去下载数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=False, transform=transform)
# batch_size为每批随机拿出36张图片训练 shuffle=True是否打乱 num 线程数 windows=0 linux自己定义
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=True, 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_loader转化成迭代器
val_image, val_label = val_data_iter.__next__() # 通过next获取数据
导入之后我们可以打印几张测试集图片看一下,注意batch_size设置小一点,32*32的像素有点小糊:
def imshow(img):
# unnormalize
# 标准化是output=(input-0.5)/0.5
# 反标准化是input=output*0.5+0.5
img = img / 2 + 0.5
# 转成np
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 10000张验证图片
# 第一次使用时要将download设置为True才会自动去下载数据集
val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=4,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader) # 将val_loader转化成迭代器
val_image, val_label = val_data_iter.__next__() # 通过next获取数据
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# print labels
print(' '.join(f'{classes[val_label[j]]:5s}' for j in range(4)))
# print image
imshow(torchvision.utils.make_grid(val_image))
接下来建立model:
net = LeNet() # 模型
loss_function = nn.CrossEntropyLoss() # 损失函数 包含softmax
optimizer = optim.Adam(net.parameters(), lr=0.001) # 优化器adam 训练参数 学习率
开始训练,每500步输出一下准确率,实际上,每个epoch有1389步,50000/36:
for epoch in range(5): # 迭代5次训练集
running_loss = 0.0
for step, data in enumerate(train_loader, start=0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
# zero the parameter gradients
# 梯度累加就是,每次获取1个batch的数据,计算1次梯度,梯度不清空,不断累加,累加一定次数后,根据累加的梯度更新网络参数,然后清空梯度,进行下一次循环。
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
print(step)
# print statistics
running_loss += loss.item()
if step % 500 == 499: # print every 500 mini-batches
with torch.no_grad(): # with是一个上下文管理器 不进行误差损失梯度
outputs = net(val_image) # [batch, 10]
predict_y = torch.max(outputs, dim=1)[1]
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))
running_loss = 0.0
print('Finished Training')
最后的识别率训练集可以达到83%,测试集达到68%,已经是一个很不错的结果,毕竟这个模型是1998年提出的,最后保存:
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)
完整实现代码:
import numpy as np
import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
def imshow(img):
# unnormalize
# 标准化是output=(input-0.5)/0.5
# 反标准化是input=output*0.5+0.5
img = img / 2 + 0.5
# 转成np
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
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)
# batch_size为每批随机拿出36张图片训练 shuffle=True是否打乱 num 线程数 windows=0 linux自己定义
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=True, 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_loader转化成迭代器
val_image, val_label = val_data_iter.__next__() # 通过next获取数据
# classes = ('plane', 'car', 'bird', 'cat',
# 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# print labels
#print(' '.join(f'{classes[val_label[j]]:5s}' for j in range(4)))
# print image
#imshow(torchvision.utils.make_grid(val_image))
net = LeNet() # 模型
loss_function = nn.CrossEntropyLoss() # 损失函数 包含softmax
optimizer = optim.Adam(net.parameters(), lr=0.001) # 优化器adam 训练参数 学习率
for epoch in range(5): # 迭代5次训练集
running_loss = 0.0
for step, data in enumerate(train_loader, start=0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
# zero the parameter gradients
# 梯度累加就是,每次获取1个batch的数据,计算1次梯度,梯度不清空,不断累加,累加一定次数后,根据累加的梯度更新网络参数,然后清空梯度,进行下一次循环。
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
print(step)
# print statistics
running_loss += loss.item()
if step % 500 == 499: # print every 500 mini-batches
with torch.no_grad(): # with是一个上下文管理器 不进行误差损失梯度
outputs = net(val_image) # [batch, 10]
predict_y = torch.max(outputs, dim=1)[1]
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))
running_loss = 0.0
print('Finished Training')
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)
if __name__ == '__main__':
main()
从网上找两张图片进行测试:
图像处理这里多了一步resize,因为训练集图片大小都是32x32。导入训练好的模型进行识别。
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
def main():
transform = transforms.Compose(
[transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
net = LeNet()
net.load_state_dict(torch.load('Lenet.pth'))
im = Image.open('1.jpg')
im = transform(im) # [C, H, W]
im = torch.unsqueeze(im, dim=0) # [N, C, H, W]
with torch.no_grad():
outputs = net(im)
predict = torch.max(outputs, dim=1)[1].numpy()
print(classes[int(predict)])
if __name__ == '__main__':
main()
成功识别出图片的类别: