当前位置: 首页 > article >正文

基于卷积神经网络的验证码识别

模式识别(Pattern Recognition)是人工智能领域的一个重要分支,它涉及研究如何让计算机系统能够识别和处理数据中的模式。模式识别技术试图从数据中提取有用的信息,识别出数据中的规律性或模式,并据此做出决策或预测。

在写爬虫脚本的时候,经常会遇到要输入图形验证码的场景。从图片格式的验证码到文本格式其实就是一种模式识别。为了能够有效的识别验证码,本作业设计了一个基于卷积神经网络的验证码识别模型,并通过Pytorch进行代码实现。

配置参数说明

我们采用4位验证码,字符集包括数字和小写字母混合。

CAPTCHA_LEN = 4  # 验证码长度
CAPTCHA_WIDTH = 160  # 验证码图片宽度
CAPTCHA_HEIGHT = 60  # 验证码图片高度
CHAR_SET = 'abcdefghijklmnopqrstuvwxyz0123456789'  # 字符集
CHAR_LEN = len(CHAR_SET)

数据集准备

验证码生成

Python的captcha库中有一个ImageCaptcha模块,可以用于生成图像验证码。默认生成的验证码图片尺寸为160*60,如下图所示。这个验证码为491e。

我们首先随机生成4位的验证码文本作为标签,然后用ImageCaptcha生成对应的验证码图片,并把验证码文本作为图片的名称。例如上面那个图片的名称就是“491e.jpg”。利用这个库,我们随机生成15w张验证码用于训练,1w张验证码用于测试。

生成数据集的代码如下所示:

from captcha.image import ImageCaptcha
import random
import os
from config import *

pic_path = './pic'  # 验证码保存文件夹


def random_captcha_text(length: int):
    captcha_text = []
    for i in range(length):
        c = random.choice(CHAR_SET)
        captcha_text.append(c)
    return ''.join(captcha_text)


def gen_one_captcha(path):
    image = ImageCaptcha(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT)
    captcha_text = random_captcha_text(CAPTCHA_LEN)
    img_path = os.path.join(path, f'{captcha_text}.jpg')
    image.write(captcha_text, img_path)


train_path = os.path.join(pic_path, 'train')
test_path = os.path.join(pic_path, 'test')
if not os.path.exists(train_path):
    os.mkdir(train_path)
if not os.path.exists(test_path):
    os.mkdir(test_path)

# 训练集生成150000张验证码
train_list = next(os.walk(train_path))[2]
cnt = len(train_list)
while cnt < 150000:
    gen_one_captcha(train_path)
    cnt += 1

# 测试集生成10000张验证码
test_list = next(os.walk(test_path))[2]
cnt = len(test_list)
while cnt < 10000:
    gen_one_captcha(test_path)
    cnt += 1

标签表示

标签就是长度为4的数字和字母组合的文本信息,我们需要对每个文本信息进行编码。具体来说,对于每个文本,我们采用字符集中的索引来表示。例如,“491e”就可以表示为长度为4的向量[30, 35, 27, 4]。再进一步对每个数字展开,采用其one hot编码(长度为36)。因此,每个标签最后可以表示为长度为4*36的向量,也就是4个one hot拼接之后的结果。

上述过程的代码如下:

def text2onehot(text):
    vector = np.zeros(CAPTCHA_LEN * CHAR_LEN)

    for i, c in enumerate(text):
        idx = CHAR_SET.index(c)
        vector[i * CHAR_LEN + idx] = 1
    return vector

定义数据集类

在定义数据集类之前,先确定图片读取进来之后要进行的预处理。由于颜色是不必要的信息,所以我们读取图片之后转为灰度模式。这样也能降低数据的复杂度。然后对图片进行二值化,突出图片里的主要信息。

img = cv2.imread('./491e.jpg', 0)  # 灰度模式读取图片
_, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)  # 二值化

例如,之前的验证码经过处理后就如下图所示。

然后,就可以通过继承Pytorch提供的Dataset类来定义MyDataset:

import os
import cv2
import torch
import numpy as np
from config import *
from torch.utils.data import Dataset


def text2onehot(text):
    vector = np.zeros(CAPTCHA_LEN * CHAR_LEN)

    for i, c in enumerate(text):
        idx = CHAR_SET.index(c)
        vector[i * CHAR_LEN + idx] = 1
    return vector


class MyDataset(Dataset):

    def __init__(self, dir):
        self.dir = dir
        self.img_name = next(os.walk(dir))[2]  # 获取图片目录下所有图片的名称

    def __getitem__(self, index):
        img_path = os.path.join(self.dir, self.img_name[index])
        img = cv2.imread(img_path, 0)  # 灰度模式读取图片
        _, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)  # 二值化
        img = torch.from_numpy(img).float()  # 转换为张量
        img = img.unsqueeze(0)  # 增加一个通道维度
        label = text2onehot(self.img_name[index].split('.')[0])  # 获取onehot标签
        label = torch.FloatTensor(label)
        return img, label

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

其中,__getitem__方法返回处理好的图片张量和标签向量。

定义模型

模型结构如下图所示:

模型代码如下:

import torch.nn as nn
from config import *


class CNN_Network(nn.Module):

    def __init__(self):
        super(CNN_Network, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 16, stride=1, kernel_size=3, padding=1),
            nn.BatchNorm2d(16), nn.ReLU())
        self.layer2 = nn.Sequential(
            nn.Conv2d(16, 32, stride=1, kernel_size=3, padding=1),
            nn.MaxPool2d(stride=2, kernel_size=2),  # 30*80
            nn.BatchNorm2d(32),
            nn.ReLU())
        self.layer3 = nn.Sequential(
            nn.Conv2d(32, 64, stride=1, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.Conv2d(64, 128, stride=1, kernel_size=3, padding=1),
            nn.MaxPool2d(kernel_size=2, stride=2),  # 15*40
            nn.BatchNorm2d(128),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(128, 256, stride=1, kernel_size=3, padding=1),
            nn.BatchNorm2d(256), nn.ReLU())

        self.fc = nn.Sequential(nn.Linear(256 * 15 * 40, 2048), nn.ReLU(),
                                nn.Linear(2048, 1024), nn.ReLU(),
                                nn.Linear(1024, CAPTCHA_LEN * CHAR_LEN))

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

训练模型

训练模型之前首先加载数据。利用之前定义的数据集类分别加载训练集和测试集:

train_dataset = MyDataset('./pic/train')
test_dataset = MyDataset('./pic/test')
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

我们采用多标签软边界损失作为损失函数。多标签软边界损失(MultiLabelSoftMarginLoss)是一种在PyTorch中用于多标签分类问题的损失函数。它基于最大熵原理,通过优化一个针对每个类别的一对多(one-vs-all)损失来进行计算。

我们采用Adam作为优化器,并设置batch大小为200,学习率为1e-4,训练15个epoch。训练代码如下:

def train(train_loader):
    net = CNN_Network().to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    criterion = nn.MultiLabelSoftMarginLoss()  #多分类损失函数
    best_loss = math.inf
    loss_per_epoch = []
    train_acc, test_acc = [], []

    net.train()
    for epoch in range(n_epochs):
        loss_record = []
        labels, preds = [], []
        train_pbar = tqdm(train_loader)

        for img, label in train_pbar:
            img, label = img.to(device), label.to(device)
            labels += tensor2captcha(label)
            optimizer.zero_grad()

            pred = net(img)
            preds += tensor2captcha(pred)
            loss = criterion(pred, label)
            loss.backward()
            optimizer.step()
            loss_record.append(loss.detach().item())

            train_pbar.set_description(f'Epoch [{epoch+1}/{n_epochs}]')
            train_pbar.set_postfix({'loss': loss.detach().item()})

        mean_loss = sum(loss_record) / len(loss_record)
        acc = get_acc(labels, preds)

        loss_per_epoch.append(mean_loss)
        train_acc.append(acc)

        if mean_loss < best_loss:
            best_loss = mean_loss
            torch.save(net.state_dict(), "captcha.pth")
            print(
                f'Saving model with loss {best_loss:.4f}...,  accuracy {acc}')

        acc = test(test_loader)
        test_acc.append(acc)

    draw_loss_curve(loss_per_epoch)
    draw_acc(train_acc, test_acc)

测试模型

测试模型就是看看模型在测试集上的表现如何。在测试模型之前已经将之前训练好的模型保存了下来。测试模型的时候读取模型并设置为评估模式,计算模型在整个测试集上的准确率。具体代码如下:

def test(test_loader):
    model = CNN_Network().to(device)
    model.load_state_dict(torch.load('captcha.pth'))
    model.eval()

    labels, preds = [], []
    for img, label in tqdm(test_loader):
        img, label = img.to(device), label.to(device)
        labels += tensor2captcha(label)

        pred = model(img)
        preds += tensor2captcha(pred)

    acc = get_acc(labels, preds)
    return acc

数据可视化

除此之外,我们需要观察模型的效果怎么样,所以需要可视化一些数据,从而方便我们调整网络结构以及相应的参数,比如损失值,准确率等。

首先需要将模型输出的张量转化为4个字符的验证码。由于标签是用4个one hot向量拼接在一起的,所以需要按照one hot向量的长度将张量分割开。然后在每个切片中找到数值最大的对应的索引。数值最大的我们就认为是one hot中的1。最后,根据索引去字符集中找到对应的字符即可。

def tensor2captcha(tensor):
    tot_len = tensor.shape[1]
    assert CAPTCHA_LEN * CHAR_LEN == tot_len

    chars = np.array(list(CHAR_SET))

    captcha = []
    for i in range(CAPTCHA_LEN):
        slice = tensor[:, i * CHAR_LEN:(i + 1) * CHAR_LEN]
        idx = torch.argmax(slice, dim=1).detach().cpu().numpy()
        col = chars[idx][:, np.newaxis]
        captcha.append(col)
    captcha = list(np.hstack(captcha))
    captcha = [''.join(row) for row in captcha]

    return captcha

计算准确率就很简单了,预测值和标签一样就认为是预测正确。

def get_acc(label, pred):
    assert len(label) == len(pred)
    label, pred = np.array(label), np.array(pred)
    return sum(label == pred) / len(label)

最后是绘制损失曲线和准确率曲线。

def draw_loss_curve(loss_per_epoch):
    plt.figure()
    plt.plot(list(range(1, len(loss_per_epoch) + 1)), loss_per_epoch)
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Loss Curve')
    plt.savefig('loss.png')


def draw_acc(train_acc, test_acc):
    assert len(train_acc) == len(test_acc)

    x = list(range(1, len(train_acc) + 1))
    plt.clf()
    plt.plot(x, train_acc, label='train_acc')
    plt.plot(x, test_acc, label='test_acc')
    plt.legend()
    plt.title("acc goes by epoch")
    plt.xlabel('eopch')
    plt.ylabel('acc_value')
    plt.savefig('acc.png')

训练/测试结果

经过15轮的训练,最终在测试集上取得了93.77%的准确率。

在每轮训练中,训练集和测试集上的准确率如下图所示。

可以看到训练集上的准确率接近100%,测试集上的准确率略逊一筹,为93.77%。整个训练过程的损失变化如下图所示。

使用模型识别验证码

训练好模型之后就可以拿来使用了。首先读取图片并预处理,然后加载模型,最后模型输出识别到的验证码。

import torch
import cv2
from model import *
from utils import *

# 读取图片并预处理
filename = '491e.jpg'
img = cv2.imread(filename, 0)  # 灰度模式读取图片
_, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)  # 二值化
img = torch.from_numpy(img).float()  # 转换为张量
img = img.unsqueeze(0)  # 增加一个通道维度
img = img.unsqueeze(0)  # 增加一个batch维度

# 加载模型
device = 'cuda:1' if torch.cuda.is_available() else 'cpu'
model = CNN_Network().to(device)
model.load_state_dict(torch.load('captcha.pth'))
model.eval()

# 识别验证码
img = img.to(device)
pred = model(img)
captcha = tensor2captcha(pred)
print(f'识别到{filename}的验证码为{captcha}')

例如,上面的491e.jpg就可以被模型正确识别。


http://www.kler.cn/a/514041.html

相关文章:

  • postman的使用
  • 基于Python django的音乐用户偏好分析及可视化系统设计与实现
  • 基于STM32的智能门锁安防系统(开源)
  • 代码随想录刷题day13|(链表篇)24.两两交换链表中的结点
  • Ubuntu cuda-cudnn中断安装如何卸载
  • Zabbix监控山特UPS电源:实现高效监控与告警
  • oneplus3t-lineageos-16.1编译-android9,
  • 机器学习有哪些应用场景
  • Java后端Controller参数校验的一些干货及问题~
  • element-plus中的table为什么相同的数据并没有合并成一个
  • Ollama能本地部署Llama 3等大模型的原因解析(ollama核心架构、技术特性、实际应用)
  • html转义符+h5提供的新标签
  • HTML `<head>` 元素详解
  • PHP同城配送小程序
  • 《LT8712X》Type-c转HDMI2.0芯片
  • Spring Boot AOP实现动态数据脱敏
  • vue3 通过ref 进行数据响应
  • Vue 引入及简单示例
  • Java中的错误与异常详解
  • Excel 实现文本拼接方法
  • 【elasticsearch】elasticsearch基本知识
  • Vue3+Elementplus物流订单信息跟踪管理
  • 【环境搭建】conda及pip配置清华镜像源
  • delete the Node
  • Ubuntu 24.04 LTS 通过 docker 安装 nextcloud 搭建个人网盘
  • 3D 模型格式转换之 STP 转 STL 深度解析