基于VGG16的验证码识别

智能信息处理理论及应用课程大作业

 

介绍

  本项目基于VGG16神经网络,设计搭建了一个轻量化验证码识别模型,并由代码随机生成一批验证码图片,供模型训练与测试。经过实验,搭建的模型仅需要少量的训练代价,以较高的准确率完成由4位数字和字母组成的静态验证码识别任务。

 

软硬件环境

  • windows 10
  • CPU Intel i9-10900k
  • GPU Nvidia RTX 3090
  • Anaconda3
  • Pycharm 2020.3
  • absl-py 1.0.0
  • blas 1.0
  • ca-certificates 2021.10.26
  • cachetools 5.0.0
  • captcha 0.3
  • certifi 2021.10.8
  • cudatoolkit 11.3.1
  • freetype 2.10.4
  • grpcio 1.43.0
  • idna 3.3
  • markdown 3.3.6
  • mkl 2021.4.0
  • numpy 1.21.2
  • pillow 9.0.1
  • pip 21.2.2
  • protobuf 3.19.4
  • python 3.8.12
  • pytorch 1.10.2
  • requests 2.27.1
  • rsa 4.8
  • setuptools 58.0.4
  • six 1.16.0
  • sqlite 3.37.0
  • tensorboard 2.8.0
  • tk 8.6.11
  • torchvision 0.11.3
  • vc 14.2
  • wheel 0.37.1
  • zstd 1.4.9

 

生成数据集

  搭建验证码识别模型时,需要大量的验证码图片。使用captcha模块生成验证码图片,验证码图片名称为验证码上显示的4个字符组成的字符串。利用以下代码,生成10000张训练集验证码png图片数据集与1000张测试集验证码png图片数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# common.py
import os
import random
import time

captcha_array=list("0123456789abcdefghijklmnopqrstuvwxyz")
captcha_size=4
from captcha.image import ImageCaptcha
if __name__ == '__main__':
print(captcha_array)
image =ImageCaptcha()
for i in range(10000):
image_val = "".join(random.sample(captcha_array, 4))

image_name = "datasets/train/{}_{}.png".format(image_val, int(time.time()))
print(image_name)
image.write(image_val,image_name)
生成的验证码示例

  由以上生成的验证码字符串文本较为紧凑,同时存在一定的扭曲形变,并且形变状态不定,因此模型需要能够克服该形变,方可较为准确的识别。该方法生成的验证码能在一定程度上代表日常生活中我们所常遇到的验证码识别任务。

 

数据集预处理与加载

  利用以下代码加载数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# mydataset.py
import os

from PIL import Image
from torch.utils.data import Dataset
from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
import one_hot
class mydatasets(Dataset):
def __init__(self,root_dir):
super(mydatasets, self).__init__()
self.list_image_path=[ os.path.join
(root_dir,image_name) for image_name in os.listdir(root_dir)]

self.transforms=transforms.Compose([
transforms.Resize((60,160)),
transforms.ToTensor(),
transforms.Grayscale()

])
def __getitem__(self, index):
image_path = self.list_image_path[index]
img_ = Image.open(image_path)
image_name=image_path.split("\\")[-1]
img_tesor=self.transforms(img_)
img_lable=image_name.split("_")[0]
img_lable=one_hot.text2vec(img_lable)
img_lable=img_lable.view(1,-1)[0]
return img_tesor,img_lable
def __len__(self):
return self.list_image_path.__len__()



if __name__ == '__main__':

d=mydatasets("datasets/train")
img,label=d[0]
writer=SummaryWriter("logs")
writer.add_image("img",img,1)
print(img.shape)
writer.close()
  • 利用 torchvision 将图像变换为神经网络能识别的图片格式;
  • 为防止图像大小不一致,统一resize为160*60的图像;
  • transform为灰度图;
  • 利用tensorboard查看处理完的图片,如下图:
    处理完毕的图片

 

one hot 编码label

  使用one hot编码器对类别进行“二进制化”操作,然后将其作为模型训练的特征。例如:假设“花”的特征可能的取值为daffodil(水仙)、lily(百合)、rose(玫瑰)。one hot编码将其转换为三个特征:is_daffodil、is_lily、is_rose,这些特征都是二进制的。本项目中one hot 为4列36行。one hot 编码label代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# one_hot.py
import common
import torch
import torch.nn.functional as F
def text2vec(text):
vectors=torch.zeros((common.captcha_size,
common.captcha_array.__len__()))
# vectors[0,0] = 1
# vectors[1,3] = 1
# vectors[2,4] = 1
# vectors[3, 1] = 1

for i in range(len(text)):
vectors[i,common.captcha_array.index(text[i])]=1
return vectors
def vectotext(vec):

vec=torch.argmax(vec,dim=1)

text_label=""
for v in vec:
text_label+=common.captcha_array[v]
return text_label

if __name__ == '__main__':
vec=text2vec("aaab")
print(vec.shape)


print(vectotext(vec))

 

基于VGG16的神经网络搭建

VGG16 简介

  VGG是由Simonyan 和Zisserman在文献《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷积神经网络模型,其名称来源于作者所在的牛津大学视觉几何组(Visual Geometry Group)的缩写。该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。   VGG中根据卷积核大小和卷积层数目的不同,可分为A,A-LRN,B,C,D,E共6个配置(ConvNet Configuration),其中以D,E两种配置较为常用,分别称为VGG16和VGG19。

VGG16结构示意图

  如图,VGG 16共包含: * 13个卷积层 * 3个全连接层 * 5个池化层

  其中,卷积层和全连接层具有权重系数,因此也被称为权重层,总数目为13+3=16,这即是VGG16中16的来源。(池化层不涉及权重,因此不属于权重层,不被计数)。

 

搭建网络

  通过以下代码构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# model.py
import torch
from torch import nn
import common
class mymodel(nn.Module):
def __init__(self):
super(mymodel, self).__init__()
self.layer1=nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=3,padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)#[6, 64, 30, 80]
)
self.layer2=nn.Sequential(
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2)#[6, 128, 15, 40]
)
self.layer3 = nn.Sequential(
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2) # [6, 256, 7, 20]
)

self.layer4 = nn.Sequential(
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2) # [6, 512, 3, 10]
)
# self.layer5 = nn.Sequential(
# nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
# nn.ReLU(),
# nn.MaxPool2d(2) # [6, 512, 1, 5]
# )

self.layer6 = nn.Sequential(
nn.Flatten(),#[6, 2560] [64, 15360]
nn.Linear(in_features=15360,out_features=4096),
nn.Dropout(0.2), # drop 20% of the neuron
nn.ReLU(),
nn.Linear(in_features=4096,out_features=common.captcha_size*common.captcha_array.__len__())
)
def forward(self,x):
x=self.layer1(x)
x=self.layer2(x)
x=self.layer3(x)
x=self.layer4(x)
#x=x.view(1,-1)[0]#[983040]


x=self.layer6(x)
# x = x.view(x.size(0), -1)
return x;

if __name__ == '__main__':
data=torch.ones(64,1,60,160)
model=mymodel()
x=model(data)
print(x.shape)
  • 使用pytorch框架搭建网络;
  • 输入的图片分辨率为160*60,通道数in_channel为1,即输入单通道灰度图像,使用灰度图是为了提高模型训练速度;
  • 卷积核大小为3*3,padding为1;
  • 每次卷积后使用relu激活函数;
  • 使用最大池化,池化的小矩阵是2x2,默认了也是2x2的步长。经过池化后,矩阵的长宽都降低一半;
  • 卷积和相应的池化操作后,使用Flatten(),将数据拉平,变成一维向量;
  • 添加dropout,防止过拟合,参数设置为0.2;
  • 添加线性层,最终输出字符尺寸(4)*字符序列长度(10+26)维特征,这与one_hot中编码shape一致。

 

模型训练

训练代码与超参数设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# train.py
import torch
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

import my_datasets
from model import mymodel

if __name__ == '__main__':
train_datas=my_datasets.mydatasets("datasets/train")
test_data=my_datasets.mydatasets("datasets/test")
train_dataloader=DataLoader(train_datas,
batch_size=64,shuffle=True)
test_dataloader = DataLoader(test_data,
batch_size=64, shuffle=True)
writer=SummaryWriter("logs")
m=mymodel().cuda()

loss_fn=nn.MultiLabelSoftMarginLoss().cuda()
optimizer = torch.optim.Adam(m.parameters(), lr=0.001)
w=SummaryWriter("logs")
total_step=0

for i in range(30):
for i,(imgs,targets) in enumerate(train_dataloader):
imgs=imgs.cuda()
targets=targets.cuda()
# print(imgs.shape)
# print(targets.shape)
outputs=m(imgs)
# print(outputs.shape)
loss = loss_fn(outputs, targets)
optimizer.zero_grad()

loss.backward()
optimizer.step()

if i%10==0:
total_step+=1
print("训练{}次,loss:{}".format(total_step*30,
loss.item()))
w.add_scalar("loss",loss,total_step)

# writer.add_images("imgs", imgs, i)
writer.close()

torch.save(m,"model.pth")
  • 利用torch.utils.data.Dataloader加载数据,其结合了数据集和取样器,并且可以提供多个线程处理数据集。在训练模型时使用此函数,用来把训练数据分成多个小组,此函数每次抛出一组数据,直至把所有数据都抛出;
  • 使用MultiLabelSoftMarginLoss 多标签交叉熵损失函数;
  • 选择Adam优化器;
  • 学习率为0.001;
  • 先将梯度归零;
  • 使用反向传播计算;
  • 输出模型为model.pth;
  • 使用cuda调用显卡加速。

 

训练结果

  loss在经过近400次训练后收敛稳定于0.001附近,收敛性能较好,耗时216秒。
训练loss与step关系图

 

测试数据集实验

  利用由1000张图片构成的测试集对训练出的模型进行测试。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# predict.py
from PIL import Image
from torch.utils.data import DataLoader
import one_hot
import model
import torch
import common
import my_datasets
from torchvision import transforms

def test_pred():
m = torch.load("model.pth").cuda()
m.eval()
test_data = my_datasets.mydatasets("datasets/test")

test_dataloader = DataLoader(test_data,
batch_size=1, shuffle=False)
test_length = test_data.__len__()
correct = 0;
for i, (imgs, lables) in enumerate(test_dataloader):
imgs = imgs.cuda()
lables = lables.cuda()

lables = lables.view(-1,
common.captcha_array.__len__())

lables_text = one_hot.vectotext(lables)
predict_outputs = m(imgs)
predict_outputs = predict_outputs.view(-1,
common.captcha_array.__len__())
predict_labels = one_hot.vectotext(predict_outputs)
if predict_labels == lables_text:
correct += 1
print("预测正确:正确值:{},预测值:{}"
.format(lables_text, predict_labels))
else:
print("预测失败:正确值:{},预测值:{}"
.format(lables_text, predict_labels))
# m(imgs)
print("正确率{}%".format(correct / test_length * 100))
def pred_pic(pic_path):
img=Image.open(pic_path)
tersor_img=transforms.Compose([
transforms.Grayscale(),
transforms.Resize((60,160)),
transforms.ToTensor()
])
img=tersor_img(img).cuda()
print(img.shape)
img=torch.reshape(img,(-1,1,60,160))
print(img.shape)
m = torch.load("model.pth").cuda()
outputs = m(img)
outputs=outputs.view(-1,len(common.captcha_array))
outputs_lable=one_hot.vectotext(outputs)
print(outputs_lable)


if __name__ == '__main__':
test_pred();
#pred_pic("dataset/test/7u9o_1635053946.png")


* 此模型的准确率为65%,差强人意
预测结果
  • 模型文件链接:https://pan.baidu.com/s/1WsRkX2rF7DEGAGARiIGq_A 提取码:6666