☰
Python深度学习实战:CNN实现字符型数字验证码识别全流程
2026/11/2 2:02:44 网站建设 项目流程

简介:面向深度学习入门与计算机视觉实践者的字符型图片数字验证码识别完整项目,围绕卷积神经网络与循环神经网络结合的思路,讲解从数据准备、预处理到模型构建、训练评估的全流程,并附Python源码帮助读者动手复现。资源包共1210个文件,以978个png、202个jpg验证码样本图片为主,配合17个py脚本、少量txt说明与rst文档、zip压缩包及bmp、jpeg等格式样本,压缩包约9.58MB,目录结构便于按数据与代码模块检索。已有1543人学习下载。内容涵盖图片归一化、one-hot编码、数据增强、VGG或ResNet特征提取、LSTM/GRU序列建模与softmax输出等关键环节,源码可直接运行调试,适合作为课程设计、毕业项目或安全方向练手参考,帮助读者理解验证码识别中数据处理、模型设计与训练评估的完整实践路径。

1. 从一张糊掉的验证码说起:这个项目到底解决什么问题

登录后台时最烦的场景之一,是验证码里的数字被干扰线切得七零八落,人眼都要凑近看两秒。这个项目干的事很直接:用深度学习把这种字符型数字验证码的识别流程完整走一遍,从数据生成、模型搭建、训练到推理,全部用 Python 实现,源码可跑可改。它适合两类人——想入门深度学习但被 MNIST 教程劝退的开发者,以及手上真有验证码识别需求、想找个能二次开发底座的工程同学。项目不追求花哨的 SOTA 结构,重点在“完整过程”四个字:数据怎么来、标签怎么对齐、模型怎么训、效果怎么验,每一环都有对应代码。基于深度学习的字符识别听起来玄学,拆开看其实就是一套可复现的工程流水线,下面按能跑起来、能看懂、能改动的顺序讲。

2. 项目定位与技术选型:为什么是 CNN 而不是别的

2.1 任务本质:多字符分类还是序列识别

先要把问题定义清楚。字符型数字验证码通常是固定长度(比如 4 位或 6 位),每位是 0-9 的数字,偶尔混字母。这类任务有两种建模思路:一种是把整张图当成一个多标签分类问题,输出层直接给 N×10 个 logits,N 是字符位数;另一种是走 CRNN + CTC 的序列识别路线,适合变长、字符间距不固定的场景。

这个项目选的是前者,原因很实际:固定长度数字验证码的字符位置基本稳定,切分成本低,多标签分类的实现和调试门槛都低得多。常见做法是把输出层设计成num_digits * num_classes个节点,训练时把标签做 one-hot 展开,推理时按位取 argmax 再拼字符串。CTC 那套虽然更通用,但对新手来说解码逻辑和 blank 标签的处理容易翻车,项目定位是教学 + 可落地,所以没往复杂方向走。

提示:如果你的验证码字符位数不固定,或者字符之间有严重粘连,多标签分类会吃力,这时候才需要考虑序列模型。选型前先统计一批样本的字符位分布。

2.2 技术栈与依赖:最小可跑集合

项目用的是 Python + PyTorch 的组合,图像处理走 OpenCV 和 Pillow,数据增强用 torchvision,训练过程用 tqdm 打进度。这套组合的好处是生态成熟、报错信息友好,社区里随便一个坑都能搜到答案。依赖清单大致如下:

依赖用途备注
torch / torchvision模型定义、训练、数据加载建议 1.10 以上
opencv-python图像读取、预处理、干扰线处理注意 BGR/RGB 顺序
Pillow验证码生成、字体渲染生成数据时用
numpy数组运算、标签编码基础依赖
tqdm训练进度可视化可选但强烈建议
matplotlib损失曲线、样本可视化调试用

安装命令不复杂,但有个血泪经验:torch 的版本要和 CUDA 驱动匹配,否则训练时会在.cuda()那行直接报错。没有 GPU 的机器用 CPU 也能跑,只是训练轮次要拉长。

pip install torch torchvision opencv-python pillow numpy tqdm matplotlib

参数说明:如果机器有 NVIDIA 显卡,去官方渠道按 CUDA 版本选对应 wheel;纯 CPU 环境直接装默认包即可,代码里用torch.cuda.is_available()做设备判断,不用改逻辑。

2.3 目录结构:源码怎么组织

项目目录按职责分层,读代码时顺着这个结构走不会迷路:

captcha_recognition/ ├── data/ │ ├── generate.py # 验证码生成脚本 │ └── dataset.py # Dataset 与 DataLoader 封装 ├── models/ │ └── cnn.py # 网络结构定义 ├── train.py # 训练入口 ├── predict.py # 单张/批量推理 ├── config.py # 超参与路径配置 └── utils/ ├── preprocess.py # 图像预处理 └── metrics.py # 准确率统计

config.py集中管理字符集、图片尺寸、批次大小、学习率这些参数,改实验时只动一个文件,不用满项目找数字。这种组织方式在中小型 CV 项目里很常见,好处是训练和推理共用同一套预处理逻辑,避免“训练时一种归一化、推理时另一种”的经典翻车。

3. 数据与模型:从生成验证码到 CNN 前向传播

3.1 验证码生成:自己造数据才可控

真实验证码样本往往不好批量获取,项目自带生成脚本,用 Pillow 渲染数字、加干扰线和噪点。核心逻辑是随机选字符、随机字体大小和位置、叠加干扰元素,最后存成图片并同步写标签文件。

from PIL import Image, ImageDraw, ImageFont import random, os CHARS = "0123456789" WIDTH, HEIGHT = 120, 40 def generate_one(save_path, label_path, idx): img = Image.new("RGB", (WIDTH, HEIGHT), (255, 255, 255)) draw = ImageDraw.Draw(img) label = "".join(random.choice(CHARS) for _ in range(4)) # 逐字符绘制,位置带随机偏移,模拟真实场景的字符抖动 for i, ch in enumerate(label): font = ImageFont.truetype("arial.ttf", random.randint(24, 30)) x = 10 + i * 26 + random.randint(-3, 3) y = random.randint(2, 8) draw.text((x, y), ch, font=font, fill=(0, 0, 0)) # 叠加干扰线,增强模型鲁棒性 for _ in range(random.randint(2, 4)): x1, y1 = random.randint(0, WIDTH), random.randint(0, HEIGHT) x2, y2 = random.randint(0, WIDTH), random.randint(0, HEIGHT) draw.line((x1, y1, x2, y2), fill=(random.randint(0, 200),)*3, width=1) img.save(os.path.join(save_path, f"{idx}.png")) with open(label_path, "a") as f: f.write(f"{idx}.png,{label}\n")

逻辑说明:label是 4 位数字字符串,逐字符绘制时给 x、y 加随机偏移,是为了让模型不依赖固定位置特征;干扰线颜色控制在偏灰范围,避免把字符完全盖住导致标签和图像对不上。参数上,字体大小 24-30、偏移 ±3 像素、干扰线 2-4 条,这几个值可以根据目标验证码的复杂度调,调太狠会让训练集本身变成噪声,模型学不动。

3.2 Dataset 封装:标签怎么编码

生成完数据要喂给模型,中间靠 Dataset 做图像读取和标签编码。多标签分类的标签处理是关键一步:把 “1234” 这种字符串转成 4 个独立的类别索引,训练时用交叉熵逐位计算损失。

import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class CaptchaDataset(Dataset): def __init__(self, label_file, transform=None): self.samples = [] with open(label_file) as f: for line in f: name, label = line.strip().split(",") self.samples.append((name, label)) self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): name, label = self.samples[idx] img = Image.open(f"data/images/{name}").convert("RGB") if self.transform: img = self.transform(img) # 标签转成 4 个 long 型索引,供逐位交叉熵使用 target = torch.tensor([int(c) for c in label], dtype=torch.long) return img, target

逻辑说明:target是形状为[4]的张量,不是 one-hot,因为 PyTorch 的CrossEntropyLoss接受类别索引。这里有个容易踩的点——如果标签里有字母,int(c)会直接崩,字符集扩展时要改成字典映射char2idx。transform 一般包含 Resize 到固定尺寸、ToTensor、Normalize,训练集额外加随机旋转或亮度扰动。

3.3 CNN 结构:轻量但够用

网络没有堆很深,三层卷积 + 池化接全连接,输出维度是4 * 10。这种规模在验证码任务上收敛快,单卡几十分钟能出效果。

import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, num_digits=4, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 5 * 15, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_digits * num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) # reshape 成 [batch, 4, 10],方便逐位算损失 return x.view(-1, 4, 10)

逻辑说明:输入按 120×40 算,三次 2 倍池化后特征图是 15×5,所以全连接输入是128*5*15。forward最后 reshape 成[batch, 4, 10],训练时对第 1 维逐位取交叉熵。参数上,Dropout 0.3 是防过拟合的常规值,如果训练集只有几千张,可以加到 0.5;卷积通道 32/64/128 是轻量配置,样本量大时可以翻倍。

3.4 训练循环:损失怎么算、指标怎么看

训练入口把数据、模型、优化器串起来,损失用逐位交叉熵求和,准确率按“4 位全对”统计,这个指标比单字符准确率更贴近实际使用。

import torch from torch.utils.data import DataLoader from torchvision import transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") transform = transforms.Compose([ transforms.Resize((40, 120)), transforms.ToTensor(), transforms.Normalize((0.5,)*3, (0.5,)*3), ]) dataset = CaptchaDataset("data/labels.txt", transform) loader = DataLoader(dataset, batch_size=64, shuffle=True) model = CaptchaCNN().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = torch.nn.CrossEntropyLoss() for epoch in range(20): model.train() total, correct = 0, 0 for imgs, targets in loader: imgs, targets = imgs.to(device), targets.to(device) logits = model(imgs) # [B, 4, 10] loss = sum(criterion(logits[:, i, :], targets[:, i]) for i in range(4)) optimizer.zero_grad() loss.backward() optimizer.step() preds = logits.argmax(dim=2) # [B, 4] correct += (preds == targets).all(dim=1).sum().item() total += imgs.size(0) print(f"epoch {epoch}, acc {correct/total:.4f}")

逻辑说明:sum(... for i in range(4))把 4 个位置的损失相加,等价于把多标签任务拆成 4 个分类任务联合优化;(preds == targets).all(dim=1)要求 4 位全对才算正确,这个口径偏严格,但能真实反映上线效果。学习率 1e-3 配 Adam 是稳妥起点,loss 震荡时可以降到 5e-4。

4. 推理与效果验证:模型训完怎么用

4.1 单张推理:预处理必须和训练一致

推理脚本最容易出的问题是预处理不一致。训练时用了 Normalize,推理时忘了,模型输出会完全乱掉。正确做法是把 transform 抽成公共函数,训练和推理都调它。

from PIL import Image import torch def predict(image_path, model, transform, device): model.eval() img = Image.open(image_path).convert("RGB") x = transform(img).unsqueeze(0).to(device) # 增加 batch 维 with torch.no_grad(): logits = model(x) # [1, 4, 10] preds = logits.argmax(dim=2).squeeze(0) # [4] return "".join(str(p.item()) for p in preds)

逻辑说明:unsqueeze(0)补 batch 维,torch.no_grad()关梯度省显存,argmax(dim=2)在类别维取最大值。返回的字符串直接就是识别结果。参数上,transform必须和训练时同一个对象或同参数构造,这是硬约束。

4.2 批量评估:混淆矩阵看短板

只看整体准确率不够,要知道模型在哪些数字上容易错。用混淆矩阵按位统计,能发现比如“1 和 7 混淆”“0 和 8 混淆”这类系统性问题。

from sklearn.metrics import confusion_matrix import numpy as np all_preds, all_targets = [], [] model.eval() with torch.no_grad(): for imgs, targets in loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=2).cpu().numpy() all_preds.append(preds) all_targets.append(targets.numpy()) preds = np.concatenate(all_preds).flatten() targets = np.concatenate(all_targets).flatten() cm = confusion_matrix(targets, preds, labels=list(range(10))) print(cm)

逻辑说明:把 4 个位置的预测展平后统一算混淆矩阵,对角线是正确数,非对角线能看出混淆方向。如果某类错误率明显偏高,优先补该类样本或调整字体渲染参数,比盲目加训练轮次有效。

4.3 效果预期与调优方向

在自生成数据上,这个结构通常几轮就能到 90% 以上的全对率;换成真实验证码,准确率会掉一截,因为真实数据的干扰样式、字体、扭曲程度和生成器有分布差异。调优顺序建议是:先扩真实样本、再做针对性增强(旋转、透视、噪声)、最后才动网络结构。很多新手一上来就换 ResNet,结果发现瓶颈在数据不在模型,白折腾。

5. 避坑与排查:那些让我返工的细节

5.1 准确率虚高但实际识别全错

现象:训练集准确率 99%,拿真实图片一测全错。原因通常是数据分布不一致——生成器字体单一、背景纯白,真实验证码有彩色背景和扭曲。解决:生成器里加随机背景色、字体池、透视变换,或者直接混入少量真实样本做微调。

5.2 训练 loss 不降反升

现象:前几轮 loss 正常下降,后面突然飙高。原因多是学习率过大或标签错位,比如标签文件和图片名对不上,模型在学噪声。解决:先小批量过拟合测试——取 100 张图训到 loss 接近 0,如果做不到,说明数据管道有问题,重点查标签读取和 transform 顺序。

5.3 推理时报维度不匹配

现象:view(-1, 4, 10)那行报 size 错误。原因是输入图片尺寸和训练时不一致,池化后特征图大小变了,全连接层输入对不上。解决:推理前强制 Resize 到训练尺寸,或者把全连接前的 Flatten 换成AdaptiveAvgPool2d,让网络对输入尺寸不敏感。

5.4 GPU 显存够却报 OOM

现象:显存明明有空余,训练中途 OOM。常见原因是 batch 里混入了超大图,或者验证阶段没关梯度导致计算图累积。解决:DataLoader 里加collate_fn统一尺寸,验证和推理统一包torch.no_grad()。

5.5 字符集扩展后模型崩掉

现象:从纯数字扩到数字加字母,准确率断崖下跌。原因是输出维度从 10 变成 36,但标签编码还是int(c),字母直接抛异常或编码错乱。解决:建char2idx映射表,输出层维度同步改成num_digits * len(charset),旧模型不能直接复用,需要重训或只微调分类头。

6. 二次开发:把 demo 改成能上生产的识别服务

拿到这份源码后,最实际的改造方向是把它包成一个可调用的识别接口,而不是每次跑脚本。我的做法是拆成三层:预处理层、模型层、服务层。预处理层把图像解码、尺寸归一、通道转换固定下来;模型层加载权重后常驻内存,避免每次请求重新加载;服务层用轻量 Web 框架暴露一个 POST 接口,入参是图片 base64,出参是识别字符串和置信度。

改造时有个技巧值得说:给每位字符输出置信度,取 4 位概率的最小值作为整体置信度,低于阈值(比如 0.6)的结果标记为“待人工复核”。这样在真实业务里能挡住大部分低质量输入,比单纯返回结果可靠得多。

import torch.nn.functional as F def predict_with_score(image_path, model, transform, device, threshold=0.6): model.eval() img = Image.open(image_path).convert("RGB") x = transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(x) # [1, 4, 10] probs = F.softmax(logits, dim=2) # 转概率 conf, preds = probs.max(dim=2) # 每位最大概率及索引 text = "".join(str(p.item()) for p in preds.squeeze(0)) score = conf.min().item() # 取最弱一位作为整体置信度 return text, score, score >= threshold

逻辑说明:softmax把 logits 转成概率,max(dim=2)同时拿到置信度和预测索引,conf.min()取 4 位里最没把握的那位,这个口径偏保守但对线上服务友好。阈值 0.6 是经验值,业务对误识容忍度低就往上调,对召回要求高就往下调。

验证改造效果时,我一般准备三份数据:训练同分布的生成数据、真实业务采样数据、边界样本(模糊、遮挡、极端扭曲)。三份分别跑准确率和置信度分布,如果真实数据上高置信度样本的准确率明显高于低置信度样本,说明置信度机制有效,可以放心接自动流程。

还有一个容易忽略的点是模型版本管理。二次开发后权重文件会迭代,接口层要记录当前加载的模型版本号,出问题时能快速回滚。我吃过亏——某次换了增强策略重训,没留旧权重,线上效果变差时只能从头再训一遍。从那以后我每次训练完都把权重、配置文件、训练日志打成一个包存档,命名带日期和关键参数,强制走一遍这个流程才允许上线。希望这些拆解能帮到你,少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询