33. CV练习: 验证码识别

原文连接:mp.weixin.qq.com/s/yZ-FZMnIp...

Hi, 你好。我是茶桁。

上一节课,我给大家留了个作业,内容是对验证码进行识别。

咱们的再把练习内容重复看一下:

  1. 练习内容: 训练一个模型,对验证码中的字符进行分类识别,并最终完成验证码识别的任务。

  2. 数据集: 数据集内包含0-9以及A-Z一共36个字符,训练集中每个字符有50张图片,验证集中每个字符有10张图片,验证码数据集是由随机去除的4个字符图片拼接而成。

  3. 需要的相关知识:

    • 数据读取
    • 使用torch搭建、训练、验证模型
    • 模型预测于图片切分

好,让我们来看看具体的,我们该怎么完成这个练习。

问题分析

首先,我们需要一步步的确定我们的问题。第一个问题,肯定是要先建立字符对照表,第二个问题,要定义一个datasets和一个dataloader。 第三个问题,是需要定义网络结构。 第四个问题,就是定义模型训练函数。 最后,就是验证我们的训练结果。

先从第一个问题分析,我们可以通过遍历字典,将每一对键值反转,并存储于新的字典中。我们可以按如下方式去做:

python 复制代码
new_dict = {v:k for k, v in old_dict.items()}

那么第二个问题就简单了,在opencv-python中,可以使用image = cv2.medianBlur(image, kernel_size)进行中值滤波。

第三个问题,在torch中,卷积于前连接层的定义方法可以是:

python 复制代码
conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
fc = nn.Linear(in_features, out_features, bias)

具体的,还可以参看PyTorch的相关手册。 用这两个方法进行组合,就可以定义出一个卷积神经网络结构。

接下来第四个问题,要定义模型训练函数。那么torch框架的模型训练过程会包含清空梯度、前向传播、计算损失、计算梯度、更新权重等操作。

  • 清空梯度:目的是消除step与step之间的干扰,即每次都只用一个batch的数据损失计算梯度并更新权重。一般可以放在最前或最后;
  • 前向传播:使用一个batch的数据跑一边前向传播的过程,生成模型输出结果;
  • 计算损失:使用定义好的损失函数、模型输出结果以及label计算单个batch的损失值;
  • 计算梯度:根据损失值,计算模型所有权中在本次优化中所需的梯度值;
  • 更新权重:使用计算好的梯度值,更新所有权重的值。

模拟一下单词流程代码,样例如下:

python 复制代码
>>> optimizer.zero_grad() # 清空梯度(也可以放在最后一行)
>>> output = model(data) # 前向传播
>>> loss = loss_fn(output, target) # 计算损失
>>> optimizer.step() # 更新权重

好,步骤分析完了,接着咱们来进行实现,这次我们所用到的代码库如下,将其都引入进来:

python 复制代码
import cv2 
import torch 
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms
import pickle 
import matplotlib.pyplot as plt
from PIL import Image 

然后我们来看一下数据集,上一课末尾我给过数据集了,有需要的自己去找一下。先将需要用到的数据路径都定义出来:

python 复制代码
train_data_dir = 'data/train_data.bin'
val_data_dir = 'data/val_data.bin'
verification_code_dir = 'data/verification_code_data.bin'

这个数据集保存在二进制文件中,我们需要定义一个函数,读取二进制文件中的图片:

python 复制代码
def load_file(file_name):
    with open(file_name, mode='rb') as f:
        result = pickle.load(f)
    return result

来让我们查看一下数据集:

python 复制代码
train_data = load_file(train_data_dir)
img_test = list()
for i in range(1,1800,50):
    img_test.append(train_data[i][1])
plt.figure()
for i in range(1,37):
    plt.subplot(6,6,i)
    plt.imshow(img_test[i-1])
    plt.xticks([])
    plt.yticks([])
plt.show()

放大其中单张图:

python 复制代码
plt.imshow(train_data[500][1])
plt.xticks([])
plt.yticks([])
plt.show()

观察这张图片,我们可以看到字符图片中含有大量的噪声,而噪声会对模型预测结果产生不良影响,因此我们可以在数据预处理时,使用特定的滤波器,消除图片噪声。

简单观察可知,刚才定义字符字典中,键与值都没有重复项,因此可以将字典中的键与值进行反转,以便我们用值查找键(将模型预测结果转换成可读字符)。将字典中的键与值进行反转(例:dict={'A':10,'B':11}反转后得到new_dict={10:'A',11:'B'}

python 复制代码
char_dict = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,\
            'A':10,'B':11,'C':12,'D':13,'E':14,'F':15,'G':16,'H':17,'I':18,'J':19,'K':20,'L':21,'M':22,\
            'N':23,'O':24,'P':25,'Q':26,'R':27,'S':28,'T':29,'U':30,'V':31,'W':32,'X':33,'Y':34,'Z':35}
new_char_dict = {v : k for k, v in char_dict.items()}

然后我们就要定义datasetsdataloader了, 我们需要使用torch.utils.data.Dataset作为父类,定义自己的datasets,以便规范自己的数据集。

python 复制代码
class MyDataset(Dataset):
    def __init__(self, file_name, transforms):
        self.file_name = file_name # 文件名称
        self.image_label_arr = load_file(self.file_name) # 读入二进制文件
        self.transforms = transforms # 图片转换器

    def __getitem__(self, index):
        label, img = self.image_label_arr[index]
        img = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) # 将图片转为灰度图
        img = cv2.medianBlur(img, 5) # 使用中值模糊除去图片噪音
        img = self.transforms(img) # 对图片进行转换
        return img, char_dict[label[0]] 

    def __len__(self):
        return len(self.image_label_arr)

接着我们来定义transformdataloader

python 复制代码
transform = transforms.Compose([transforms.ToPILImage(), 
                                transforms.Resize([28,28]), # 将图片尺寸调整为28*28
                                transforms.ToTensor(), # 将图片转为tensor
                                transforms.Normalize(mean=[0.5],std=[0.5])]) # 进行归一化处理

train_datasets = MyDataset(train_data_dir, transform)
train_loader = DataLoader(dataset=train_datasets,batch_size=32,shuffle=True)

val_datasets = MyDataset(val_data_dir, transform)
val_loader = DataLoader(dataset=val_datasets,batch_size=32,shuffle=True)

在数据准备好之后,我们需要定义一个简单的卷积神经网络,神经网络的输入是[batchsize,chanel(1),w(28),h(28)],输出是36个分类。

我们的神经网络将使用2个卷积层搭配2个全连接层,这四层的参数设置如下表所示(未标注的直接使用默认参数即可):

  • conv1: in_chanel=1, out_chanel=10, kernel_size=5
  • conv2: in_chanel=10, out_chanel=20, kernel_size=3
  • fc1: in_feature=2000, out_feature=500
  • fc2: in_feature=500, out_feature=36
python 复制代码
class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(1, 10, 5) 
        self.conv2 = nn.Conv2d(10, 20, 3) 
        self.fc1 = nn.Linear(20 * 10 * 10, 500)
        self.fc2 = nn.Linear(500, 36)
        
    def forward(self, x):
        # inputsize:[b,1,28,28]
        in_size = x.size(0) # b
        out= self.conv1(x) # inputsize:[b,1,28,28] -> outputsize:[b,10,24,24]
        out = F.relu(out)
        out = F.max_pool2d(out, 2, 2) # inputsize:[b,10,24,24] -> outputsize:[b,10,12,12]
        out = self.conv2(out) # inputsize:[b,10,12,12] -> outputsize:[b,20,10,10]
        out = F.relu(out)
        out = out.view(in_size, -1) # inputsize:[b,20,10,10] -> outputsize:[b,2000]
        out = self.fc1(out) # inputsize:[b,2000] -> outputsize:[b,500]
        out = F.relu(out)
        out = self.fc2(out) # inputsize:[b,500] -> outputsize:[b,36]
        out = F.log_softmax(out, dim = 1)
        return out

接着,我们来定义模型训练函数,需要实现下面四项操作:

  1. 清空梯度
  2. 前向传播
  3. 计算梯度
  4. 更新权重
python 复制代码
def train(model, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if (batch_idx + 1) % 10 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))

再来定义模型测试函数:

python 复制代码
def test(model, test_loader):
    model.eval()
    test_loss =0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction = 'sum')
            pred = output.max(1, keepdim = True)[1]
            correct += pred.eq(target.view_as(pred)).sum().item()
    test_loss /= len(test_loader.dataset)
    print("\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%) \n".format(
        test_loss, correct, len(test_loader.dataset),
        100.* correct / len(test_loader.dataset)))

接着是定义模型及优化器,我们将刚刚搭建好的模型结构定义为model,并选择使用Adam优化器。

python 复制代码
model = ConvNet()
optimizer = optim.Adam(model.parameters())

终于可以来进行模型训练与测试了。我们可以先设置epochs数为3,进行模型训练,看看模型精度是多少,是否满足验证码识别的要求。如果模型精度不够,你还可以尝试调整epochs数,重新进行训练。

python 复制代码
EPOCHS = 3
for epoch in range(1, EPOCHS + 1):
    train(model, train_loader, optimizer, epoch)
    test(model, val_loader)

---
Train Epoch: 1 [288/1800 (16%)]	Loss: 3.454732
Train Epoch: 1 [608/1800 (33%)]	Loss: 2.911864
Train Epoch: 1 [928/1800 (51%)]	Loss: 1.960211
Train Epoch: 1 [1248/1800 (68%)]	Loss: 0.972134
Train Epoch: 1 [1568/1800 (86%)]	Loss: 0.420529

Test set: Average loss: 0.3054, Accuracy: 335/360 (93%) 

Train Epoch: 2 [288/1800 (16%)]	Loss: 0.094786
Train Epoch: 2 [608/1800 (33%)]	Loss: 0.120601
Train Epoch: 2 [928/1800 (51%)]	Loss: 0.073640
Train Epoch: 2 [1248/1800 (68%)]	Loss: 0.058856
Train Epoch: 2 [1568/1800 (86%)]	Loss: 0.007260

Test set: Average loss: 0.0139, Accuracy: 359/360 (100%) 

Train Epoch: 3 [288/1800 (16%)]	Loss: 0.002425
Train Epoch: 3 [608/1800 (33%)]	Loss: 0.004629
Train Epoch: 3 [928/1800 (51%)]	Loss: 0.005880
Train Epoch: 3 [1248/1800 (68%)]	Loss: 0.008300
Train Epoch: 3 [1568/1800 (86%)]	Loss: 0.004973

Test set: Average loss: 0.0039, Accuracy: 360/360 (100%) 

训练结果看起来相当不错。那么下面我们自然就是要开始进行验证码识别了,我们需要先导入验证码数据集:

python 复制代码
verification_code_data = load_file(verification_code_dir)

下面我们随便选一张图(图3),看看这个验证码长什么样。

python 复制代码
image = verification_code_data[3]
IMG = Image.fromarray(cv2.cvtColor(image.copy(), cv2.COLOR_BGR2RGB))
plt.imshow(IMG)

再来看看中值滤波能对验证码图片产生什么效果。

python 复制代码
# 中值滤波效果
img = cv2.medianBlur(image.copy(), 5)
plt.imshow(img)

好,最后来让我们看看识别的实际情况如何:

python 复制代码
# 查看实际识别结果
IMAGES = list()
NUMS = list()

for img in verification_code_data:
    IMAGES.append(img)
    img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    image_1 = img[:, :80]
    image_2 = img[:, 80:160]
    image_3 = img[:, 160:240]
    image_4 = img[:, 240:320]
    img_list = [image_1, image_2, image_3, image_4]

    nums = []
    for one_img in img_list:
        one_img = transform(one_img)
        one_img = one_img.unsqueeze(0)
        output = model(one_img)
        nums.append(new_char_dict[torch.argmax(output).item()])
    NUMS.append('Verification_code: '+''.join(nums))

plt.figure(figsize=(20, 20))
plt.subplots_adjust(wspace=0.2, hspace=0.5)
for i in range(1, 11):
    plt.subplot(5,2,i)
    plt.title(NUMS[i-1], fontsize=25, color='red')
    plt.imshow(IMAGES[i-1])
    plt.xticks([])
    plt.yticks([])
plt.show()

相当令人满意的结果。

本次练习我主要是讲解思路,大家看完课程之后要多加练习,自己反复敲打代码,去琢磨其中的一些逻辑关系,要结合我们之前课程中讲过的知识。

还有就是,关于一些第三方库,大家要习惯于自行查看手册。这才是正确的学习打开方式。

好,那这个练习就讲到这里,大家下来记得多练习。

相关推荐
KevinRay_21 分钟前
Python超能力:高级技巧让你的代码飞起来
网络·人工智能·python·lambda表达式·列表推导式·python高级技巧
Captain823Jack1 小时前
nlp新词发现——浅析 TF·IDF
人工智能·python·深度学习·神经网络·算法·自然语言处理
资源补给站1 小时前
大恒相机开发(2)—Python软触发调用采集图像
开发语言·python·数码相机
Captain823Jack1 小时前
w04_nlp大模型训练·中文分词
人工智能·python·深度学习·神经网络·算法·自然语言处理·中文分词
PieroPc2 小时前
Python 自动化 打开网站 填表登陆 例子
运维·python·自动化
VinciYan3 小时前
基于Jenkins+Docker的自动化部署实践——整合Git与Python脚本实现远程部署
python·ubuntu·docker·自动化·jenkins·.net·运维开发
测试老哥3 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
終不似少年遊*3 小时前
美国加州房价数据分析01
人工智能·python·机器学习·数据挖掘·数据分析·回归算法
如若1233 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
西猫雷婶4 小时前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv