0x01 缘起
之前进行深度学习建模时,基本就是套模型,微调参数,基本也能解决问题。不过最近进行OCR识别,大模型效果其实已经不错了,但是还是有些比较明显的场景下却是没有识别到,而大模型本身也比较笨重,基于上面去调可能效果未必好,于是想建立一个简单的模型来进行识别,因为那些识别不到的不少是一些单独的在单元格里的数字。
于是萌生了开辟一个深度学习实践的系列,这是开篇。对于深度学习,网络结构很多,我们就从最简单的一个开始。
我所使用的环境,python 3.6.9,pytorch版本:
torch 1.5.0+cu101
torchtext 0.6.0
torchvision 0.6.0+cu101所有实验基于notebook进行,该系列可能需要一点深度学习的基础。
0x02 关于LeNet
LeNet神经网络由深度学习三巨头之一的Yan LeCun提出,他同时也是卷积神经网络 (CNN,Convolutional Neural Networks)之父。LeNet主要用来进行手写字符的识别与分类,并在美国的银行中投入了使用。LeNet的实现确立了CNN的结构,现在神经网络中的许多内容在LeNet的网络结构中都能看到,例如卷积层,Pooling层,ReLU层。
其他关于该网络的介绍非常多,一搜一大堆,有兴趣可以自己去找。

0x03 加载数据
Pytorch已经为我们准备了多个学习用的数据集,手写体识别数据集就是其中之一,加载数据集:
import torch
import torchvision as tv
import torchvision.transforms as transforms
# 超参数设置
BATCH_SIZE = 64 # 批处理尺寸(batch_size)
# 定义数据预处理方式
transform = transforms.ToTensor()
# 定义训练数据集
trainset = tv.datasets.MNIST(
root='./data/',
train=True,
download=True,
transform=transform)
# 定义训练批处理数据
trainloader = torch.utils.data.DataLoader(
trainset,
batch_size=BATCH_SIZE,
shuffle=True)
# 定义测试数据集
testset = tv.datasets.MNIST(
root='./data/',
train=False,
download=True,
transform=transform)
# 定义测试批处理数据
testloader = torch.utils.data.DataLoader(
testset,
batch_size=BATCH_SIZE,
shuffle=False)上面代码就是将MNIST数据集加载进来,生成训练集和测试集,不准备对其详细说明。下面我们了解一下数据的基本情况:
数据量及图像size:

训练集有60000个图像,测试集有10000个图像,每个图像是一个28*28的灰度图。每个图像大概长下面的样子:

每个图像都标注了相应的标签:

0x04 定义LeNet网络结构
关于网络结构的说明有很多,这里直接给出代码:
import torch
import torch.nn as nn
class LeNet(nn.Module):
"""定义网络结构"""
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Sequential( # input: 28*28*1
# filter 大小 5×5,filter 深度(个数)为 6,padding为2,卷积步长s=1
# 输出矩阵大小为 28×28×6
nn.Conv2d(1, 6, 5, 1, 2),
nn.ReLU(),
# filter 大小 2×2(即 f=2),步长 s=2,no padding
# 输出:14*14*6
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.conv2 = nn.Sequential(
# filter 大小 5×5,filter 个数为 16,padding 为 0, 卷积步长 s=1
# 输出矩阵大小为 10×10×16
nn.Conv2d(6, 16, 5),
nn.ReLU(),
# 输出:5*5*16
nn.MaxPool2d(2, 2)
)
# 全连接层
self.fc1 = nn.Sequential(
nn.Linear(16 * 5 * 5, 120),
nn.ReLU()
)
# 全连接层
self.fc2 = nn.Sequential(
nn.Linear(120, 84),
nn.ReLU()
)
# 全连接层,输出神经元数量为 10,代表 0~9 十个数字类别
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
"""定义前向传播过程,输入为x"""
x = self.conv1(x)
x = self.conv2(x)
# nn.Linear()的输入输出都是维度为一的值,所以要把多维度的tensor展平成一维
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x看上面代码,共有5个网络层(分组之后的):
第一层:卷积层conv1
第一层包含Conv2d,ReLu和MaxPool2d三个层:
Conv2d:

ReLu:
MaxPool2d:
第二层:卷积层conv2
这一层跟前面一层类似,不详述,重点是要计算清楚,这层最后的输出特征图是:5*5*16。输入的是6通道,输出的是16通道。这层的理解和前一层的理解差不多,不过,这里的参数量计算却是有点复杂,网上很多文章基本都是摘自大神的论文:

行是6个通道,列是16个通道,参数计算:
6*(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1*(6*5*5+1)=1516
第一部分是输入相邻的3个通道组合输出6个通道,第二部分是输入相邻4个通道组合输出6个通道,第三部分是输入两两不相邻的4个通道组合输出3个 通道,第4部分是输入的6个通道组合输出1个通道。
(下面这段有很大的猜测成分,待验证)
原始大神的网络,参数的计算应该是这样的,大神可能是精心设计了上面的通道组合方式,因为没有看过原始的时候,不好判断。不过,对于我们的上面的实现,参数量应该不是这样的计算方式。这里一个卷积核的参数量应该是:5*5*6,这里6是输入的通道数,所以这里卷积层总的参数量应该是:
(5*5*6+1)*16 = 2416
第三层:全连接层fc1
全连接层理解上比较简单,顾名思义就是输入和输出的任意神经元之间都有连接。输入的神经元数量就是上一层的输出5*5*16 = 800,输出定义的是120,这层的参数量就比较多,计算也容易:(800+1)*120
第四层:全连接层fc2
输入120,输出84.
第五层:全连接层fc3
输入84,输出10。这是最后的输出层,输出的10个神经元对应我们的10个分类。
0x05 模型训练
代码:
import torch.optim as optim
EPOCH = 10 # 遍历数据集次数
LR = 0.001 # 学习率
# 定义是否使用GPU
print("cuda" if torch.cuda.is_available() else "cpu")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 定义损失函数loss function 和优化方式(采用SGD)
net = LeNet().to(device)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数,通常用于多分类问题上
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)
for epoch in range(EPOCH):
sum_loss = 0.0
# 数据读取
for i, data in enumerate(trainloader):
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
# 梯度清零
optimizer.zero_grad()
# forward + backward
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 每训练100个batch打印一次平均loss
sum_loss += loss.item()
if i % 100 == 99:
print('[%d, %d] loss: %.03f' % (epoch + 1, i + 1, sum_loss / 100))
sum_loss = 0.0
# 每跑完一次epoch测试一下准确率
with torch.no_grad():
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
# 取得分最高的那个类
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum()
print('第%d个epoch的识别准确率为:%.2f%%,显存占用:%dm' % (epoch + 1, (100 * int(correct) / total),
torch.cuda.max_memory_allocated()//(1024*1024)))
# 保存模型
torch.save(net.state_dict(), 'models/lenet/lenet_%03d.pth' % epoch)
# 释放显存占用
torch.cuda.empty_cache()模型训练无非就是前向计算输出,计算损失,然后反向计算梯度,更新权重,这样不断迭代优化网络,最后loss降到一定程度,计算识别准确率即可。
训练输出如下:
......
[9, 800] loss: 0.078
[9, 900] loss: 0.086
第9个epoch的识别准确率为:97.98%,显存占用:5m
[10, 100] loss: 0.079
[10, 200] loss: 0.073
[10, 300] loss: 0.071
[10, 400] loss: 0.080
[10, 500] loss: 0.080
[10, 600] loss: 0.071
[10, 700] loss: 0.082
[10, 800] loss: 0.079
[10, 900] loss: 0.084
第10个epoch的识别准确率为:97.90%,显存占用:5m训练10个epoch,准确率就接近98%了,识别效果可是很不错了。
后记:
关于LeNet,后续还得补一篇才能完整。