使用Pytorch
识别字符验证码
之前已经学习过利用Keras
搭建神经网络模型来识别字符验证码,近期又学习了pytorch
实现卷积神经网络相关的技术,正好遇到一个验证码识别的需求,所以尝试使用pytorch
来实现。
数据预处理
要训练的验证码如下所示:
其为中文汉字的简单运算,实际上仅包括零壹贰叁肆伍陆柒捌玖加减乘等于,这15个汉字,等于可以不识别(其实识别也完全没问题,只不过问题能简化就尽量简化嘛),那最后也就是总共要识别13个汉字,分类数就是13。
另外我们可以用’0123456789+-x’来代替汉字,避免文件名称无法使用汉字(windows下open-cv不能读取带有中文路径或文件名称)的问题。
下载并标注了1000张验证码,观察其字体颜色和干扰线、点均多变,无法根据特定规则将其区分;另外,尝试中值模糊、均值模糊和高斯模糊,均得不到较好的效果(肉眼观察)。只有灰度化和二值化后,感觉稍微变得清晰了一些。
另外,针对数据集我还统计了一下各个类别的数量是否均衡(主要怕有的文字训练样本太少,训练效果差)。
1 | {'捌': 206, '减': 346, '肆': 220, '柒': 205, '零': 200, '伍': 214, '加': 358, |
数据集的分布情况如上数据,总体还算均衡,那就表示可以开始处理数据和进行训练了。
在进一步处理数据之前,先划分数据集,800个训练集、验证集和测试集分别100,划分完数据集后,我也是统计了下各个数据集下的分类数量是否均衡(毕竟以前犯过划分数据集有问题的错误)。
以上都是一些简单的操作,下面仅展示后续将图片和标签转换成numpy
矩阵的代码:
1 | import os |
关于代码核心的地方,在代码中都有注释。
另外需要注意的一点是,如果输入到神经网络中的图片为三维,则
images_list.append(np.reshape(rr_img, (1, self.height, self.width)))
要替换为
images_list.append(np.transpose(img, (2, 0, 1)))
否则reshape会导致整个数据错乱。
搭建模型
之前使用Keras
做字符验证码识别的时候,得到的经验就是针对这种比较简单的字符验证码,无需过于复杂的模型,几层CNN就够了。
1 | import torch |
关于模型代码,有以下几点说明:
使用几层卷积、卷积核的数量、池化操作和dropout等并不是固定的,这要根据你的训练情况逐步调整;
全连接层的地方的输入值是需要计算的,是由输入到全连接层的输出通道数量x你的图片经过你的卷积和池化层后得到的尺寸,比如这里输出通道数量为64,原始输入图片尺寸为
40x90
,经过padding=1的卷积层尺寸不变,经过三次(2, 2)的池化层,变为5x11
。40x90 --> 20x45 --> 10x22 --> 5x11
并且在全连接层之前要把feature转换为(batch, )形状的二维tensor。
如何控制每个样本有3个输出值,这里是我遇到的难题,因为之前学习都是每个样本一个类型。
这里经过咨询有经验的同事得知,实际上就是利用相同的线性层计算得到三个值,同时返回。
不过需要注意的是,即使这三个输出值是经过了相同的线性层,就像这里的
1
2
3
4
5nn.Sequential(
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, num_classes),
)但是一定是三个独立定义的层(层名称无所谓),如果均使用同一个层,那么输出的这三个值永远都是一样的(亲身踩坑)
编写训练代码
1 | from image_process import ImageProcess |
关于训练代码,有以下几点说明:
这里批量加载训练集和验证集是我在前面数据预处理部分特别开发好的,我觉得还是蛮巧妙地;
pytorch中都是使用tensor,所以需要将加载的数据(numpy矩阵)转换成tensor:
torch.from_numpy(inputs)
最重要的一点是三个输出的情况下,如何计算损失,这是我开发过程中遇到的另一个难题。
经过咨询有经验的同事得知,实际上就是将三个输出的损失加在一起,但是你要根据神经网络的数据输出格式和你自己的标签格式,将正确的数据输入到损失函数中进行计算,且要注意
CrossEntropyLoss
的输入参数格式。关于
CrossEntropyLoss
使用方式的介绍:1
2
3
4
5
6
7
8import torch
from torch import nn
x = torch.tensor([[0.2, 0.3, 0.5, 0.1], [0.3, 0.01, 0.02, 0.4]])
y = torch.tensor([2, 3])
criterion = nn.CrossEntropyLoss()
loss = criterion(x, y)
print(loss)其y参数只需要标签即可, 不需要传one-hot向量,这也就是前面数据预处理时没有采用one-hot编码来处理标签的原因。另外out_puts的输出shape我在代码中也有注释。
记录一些训练过程中遇到的情况
训练到第30轮,训练损失才开始明显下降,一度让我以为程序哪里有问题,经过上网查资料发现:损失函数(loss)在最初的几个epochs时没有下降,可能的原因是学习率设置的太低、正则参数太高和陷入局部最小值。
我当时设置的学习率lr=0.0001,确实比较小,我尝试调整为lr=0.001再训练,发现在第20轮时损失就开始下降了,果然是学习率设置的太低。
另外我觉得dropout的太多有可能也是导致损失延迟降低的原因,所以我尝试将dropout的值缩小,也能提前几轮损失开始下降,但是最后的训练效果却不如dropout较大的时候。
在较前面的轮次,val_loss远小于train_loss,这是因为在网络中添加了dropout层,而dropout仅在训练时生效,测试时是不会dropout的。
1
2
3net.train() # 训练与测试,BN和Dropout有区别
net.eval() # 验证部分也就是这两行代码的作用,执行后告诉神经网络接下来将进入训练模式还是测试模型;另外
BN
层也是仅在训练时生效,在测试时不使用。关于如何设计出较好的模型,目前是我能力欠缺的一个地方,上面代码使用的模型架构(经过80轮的训练准确率能达到80%,经过250轮的训练准确率能达到90%),是参考大佬的模型,而我自己设计的模型准确率最高仅能达到70%,并且我也经过多次调整和训练,效果也并没有显著提升。
测试
1 | import torch |
这里的测试是批量测试,与实际的预测方法还有区别,但是大同小异,只不过在预测的方法中要注意针对单张图片再增加一个维度表示批次,否则传入神经网络的数据格式会出问题。
尝试进一步优化
添加
BN
层1
2
3
4
5
6
7
8
9
10self.convin = nn.Sequential(
nn.Conv2d(channel, 64, kernel_size=(3, 3), padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=(3, 3), padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Dropout(0.25)
)添加批规范化层后,训练得到的模型效果并没有不添加之前好,不过训练损失则在第2~3轮就开始明显下降了,不使用
BN
层的话,要训练15~20轮,训练损失才开始明显下降。考虑到
BN
层的目标就是防止梯度消失或爆炸、加快训练速度,所以损失下降比较快就体现了BN层
的用处,但是针对我这个项目,整体效果却并没有提升。旋转图片
因为观察验证码会稍微有些倾斜,倾斜幅度很小,所以想着能不能利用数据增强(旋转一个很小的角度)来进一步提升准确率。
为了进行数据增强,我是直接在process_image方法中,对每一张图片进行旋转,然后生成一张新的直接添加 到训练集中,另外标签也要添加两遍,这样我的训练集就变成了1600张,这种方法有个缺点就是一张图片和他的旋转图是挨着的两个样本,在训练时如果可以彻底打乱比较好,而且不知道是不是这个原因,再次训练时,损失吃吃降不下来了(到60轮没有下降,我就停了),但是添加BN
层后快速下降(但最终效果没有提升)。
这里记录下旋转用到的技术:
1 |
|
后来我觉得进行数据增强实际上可以对训练集操作然后直接生成相应的图片保存下来,然后在读取的时候打乱数据集比较方便,感兴趣的可以自己尝试。