卷积神经网络是真正的可以算得上是深度学习网络的神经网络了。毕竟传统的BP算法,不能算是真的深度学习算法。对于卷积神经网络来说,最具有代表性的网络就是Lenet5网络了。所以学习卷积神经网络,一般从Lenet5学起。对于这种复杂的网络我们完全没有必要,使用python一点一点写。有很多现成的框架我们可以使用。比如Tensorflow和Pytorch。但是Pytorch更好学习一点,于是我就是用Pytorch尝试着写了这个代码,来尝试学习卷积神经网络。
对于卷积神经网络,网上有很多资料,我完全没有必要重复造轮子。我只放一个链接吧。
https://blog.csdn.net/weixin_42398658/article/details/84392845
这儿介绍的很清楚。
一、Lenet5网络模型图

二、Lenet5详细分析
1. C1层-卷积层
输入:1*32*32(输入特征图数和大小)
卷积核:6*5*5(卷积核个数和卷积核大小)
输出:6*28*28(输出特征图数和大小)
2.S2层-池化层(下采样层)
输入:6*28*28(输入特征图数和大小)
采样区域:2*2
输出:6*14*14(输出特征图数和大小)
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果套上sigmoid 等函数
3.C3-卷积层
输入:6*14*14 不同方式组合
卷积核:16*5*5(卷积核个数和卷积核大小)
输出:16*10*10
4.S4层-池化层(下采样层)
输入:16*10*10(输入特征图数和大小)
采样区域:2*2
输出:16*5*5(输出特征图数和大小)
采样方式:4个输入相加,乘以一个可训练参数,再加上一个可训练偏置。结果套上sigmoid 等函数
5.C5-卷积层(相当于与S4全连接)
输入:16*5*5
卷积核:120*5*5(卷积核个数和卷积核大小)
输出:120*1*1
6.F6层-全连接层
输入:120*1
激活函数:sigmod
输出:84*1
7.Output层-全连接层
输入:84*1
输出:10*1
输出层由欧式径向基函数(Euclidean Radial Basis Function)单元组成,每类一个单元,每个有84个输入。换句话说,每个输出RBF单元计算输入向量和参数向量之间的欧式距离。输入离参数向量越远,RBF输出的越大。
三、模型实现
学习神经网络主要是学习神经网络的原理,所以没有必要完全复现这个完整的Lenet5。网上很多代码都是低级api,就是自己写模型类。但是我感觉 torch.nn.Sequential 这个类很强大,于是就想写了如下的模型,使用pytorch高级api搭建Lenet5。C5卷积层被视为与S4层全连接。所以S4层输出之后把结果变成400*1的向量。然后与400个输入84个输出的C5线性层全连接。另外未实现池化层S2、S4的sigmod池化,使用平均值池化代替。
激活函数使用性能更加优秀的ReLu激活函数。网络最后一层为高斯连接层。而我们为了简单起见还是用了全连接层。模型如下:
model = nn.Sequential(
# C1
nn.Conv2d(1, 6, (5, 5), 1, 2),
nn.ReLU(),
# S2
nn.AvgPool2d((2, 2)),
# C3
nn.Conv2d(6, 16, (5, 5), 1),
nn.ReLU(),
# S4
nn.AvgPool2d((2, 2)),
# C5
Flatten(),
nn.Linear(5*5*16, 120),
nn.ReLU(),
# F6
nn.Linear(120, 84),
nn.ReLU(),
# OUTPUT
nn.Linear(84, 10),
)
四、数据集描述
MNIST 数据集来自美国国家标准与技术研究所, National Institute of Standards and Technology (NIST). 训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员. 测试集(test set) 也是同样比例的手写数字数据.
MNIST数据集分为训练集和测试集。训练数据集包含 60,000 个样本, 测试数据集包含 10,000 样本. 在 MNIST 数据集中的每张图片由 28 x 28 个像素点构成, 每个像素点用一个灰度值表示.
五、详细代码
import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import numpy as np
#from logger import Logger
# 定义超参数
batch_size = 128 # 批的大小
learning_rate = 0.001 # 学习率
num_epoches = 20000 # 遍历训练集的次数
# 下载训练集 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())
# 数据集分批
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 多维向量归一化400*1一维向量
class Flatten(nn.Module):
def forward(self, input):
return input.reshape(input.size(0), -1)
# 模型定义
model = nn.Sequential(
# C1
nn.Conv2d(1, 6, (5, 5), 1, 2),
nn.ReLU(),
# S2
nn.AvgPool2d((2, 2)),
# C3
nn.Conv2d(6, 16, (5, 5), 1),
nn.ReLU(),
# S4
nn.AvgPool2d((2, 2)),
# C5 # 多维向量归一化400*1一维向量
Flatten(),
nn.Linear(5*5*16, 120),
# nn.Conv2d(16, 120, (5, 5), 1),
nn.ReLU(),
# nn.BatchNorm1d(120),# 批量准化
# nn.Dropout(),
# F6
nn.Linear(120, 84),
nn.ReLU(),
# nn.BatchNorm1d(84),
# nn.ReLU(),
# OUTPUT
# nn.Dropout(),
nn.Linear(84, 10),
# nn.Softmax(dim=1),
)
# 定义loss和optimizer
# 交叉熵损失函数
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 优化器代替手动优化使用Adam 优化方法
# 开始训练
for epoch in range(num_epoches):
print('epoch {}'.format(epoch + 1)) # .format为输出格式,formet括号里的即为左边花括号的输出
print('*' * 10)
running_loss = 0.0
running_acc = 0.0
# 遍历整个数据集
for i, data in enumerate(train_loader, 1):
img, label = data
img = Variable(img)
label = Variable(label)
# print(img.data)
# 向前传播
out = model(img)
# print(out.data.shape, img.shape, label.shape)
# 计算loss
loss = criterion(out, label)
# 计算总的loss
running_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
# 准确度
accuracy = (pred == label).float().mean()
running_acc += num_correct.item()
# 向后传播
optimizer.zero_grad()
loss.backward()
# 优化器执行
optimizer.step()
# 输出训练接的准确率和误差
print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
epoch + 1, running_loss / (len(train_dataset)), running_acc / (len(train_dataset))))
# 以下未测试集
# 模型复原
model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
# 测试集计算不再计算梯度
with torch.no_grad():
img = Variable(img)
label = Variable(label)
out = model(img)
# 计算loss
loss = criterion(out, label)
eval_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
# 输出测试集准确度和误差
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
test_dataset)), eval_acc / (len(test_dataset))))
print()
# loss小于特定值则退出训练
if running_loss / (len(train_dataset)) <= 0.005:
break
# 保存模型
# torch.save(model.state_dict(), './cnn.pth')
# torch.save(model, "cnn1.pkl")
# model = torch.load("cnn1.pkl")
# 测试保存的模型
criterion = nn.CrossEntropyLoss()
eval_loss = 0
eval_acc = 0
count = 0
for data in test_loader:
img, label = data
with torch.no_grad():
img = Variable(img)
label = Variable(label)
out = model(img)
count += 1
# print(out.shape, count)
loss = criterion(out, label)
# print(loss)
#print(label.size(0))
eval_loss += loss.item() * label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
test_dataset)), eval_acc / (len(test_dataset))))
print()
代码比较乱,可能看明白需要一点时间。基本注释已经写明,不清楚请在下方留言。
完整源代码: https://github.com/mengxiangke/Lenet5
六、结果图
对于手写数字识别,效果还是蛮好的。

代码可能需要好好看一会才能看懂,若是不明白请在下方留言。
文章评论
好