简介:这份资源面向希望入门深度学习图像识别的开发者与安全方向学习者,围绕字符型图片数字验证码的识别任务,完整呈现从数据准备到模型训练、再到Python代码落地的实践路径。内容涉及卷积神经网络提取图像特征、循环神经网络处理字符序列,以及数据归一化、one-hot编码、交叉熵损失与Adam优化等关键环节,帮助读者理解验证码识别这一典型场景的建模思路。压缩包共1210个文件,约9.58MB,以978个png与202个jpg验证码样本图片为主体,另含17个py脚本、少量txt说明、rst文档及bmp、jpeg等格式样本,图片与代码配套便于直接复现实验。目前已有1543人学习下载。资源提供可运行的源码与成规模的数据集,读者可据此完成模型搭建、训练评估与预测流程,并在此基础上尝试数据增强、迁移学习等改进,适合作为深度学习项目练手与验证码识别入门的参考材料。
1. 从一张糊掉的验证码说起:这套字符型图片识别方案到底能干什么
登录页上那张四位数字验证码,人眼一秒能认,脚本却经常卡在“6 和 8 分不清、0 和 O 混着来”上。很多做自动化的朋友第一反应是上通用 OCR,结果发现小图、低对比度、带干扰线的字符型验证码识别率惨不忍睹。这套基于深度学习的字符型图片数字验证码识别完整过程,解决的正是这个具体场景:给定一批固定长度(常见 4 位)的数字或字符验证码图片,训练一个端到端的分类模型,把整张图直接映射成字符序列,而不是先切分再逐字识别。它适合两类人:一是想系统走一遍“数据生成—预处理—建模—训练—评估—推理”闭环的深度学习学习者,二是手里有类似小图字符识别需求、想拿一个能跑通的 Python 实现改吧改吧就用的从业者。整套流程用 Python 实现,核心是卷积网络加多分类头,源码结构清晰,重点不在炫技,而在把每个环节的参数和坑交代明白。
2. 先想清楚建模方式:整图多分类还是字符切分
2.1 为什么字符型验证码更适合整图多分类
字符型验证码的经典难点是字符粘连和干扰。传统做法是先二值化、去噪、垂直投影切分,再对每个字符单独识别。这条路在字符间距稳定、干扰少时能用,但一旦出现字符重叠或随机噪点,切分就会错位,后面识别再准也白搭。整图多分类换了个思路:把“4 位数字”看成一个整体类别,输出层直接给 10^4 个组合打分,取最高分作为结果。这样绕开了切分这个最脆弱的环节,模型自己学字符位置和形状的联合特征。
代价也要说清楚:类别数随位数指数增长。4 位数字是 10000 类,5 位就是 100000 类,输出层参数量和训练样本需求都会迅速膨胀。所以这套方案适合固定长度、字符集较小的场景,比如纯数字 4 位、字母数字混合 4 位。如果位数不固定或字符集很大,更合适的是 CTC 或序列解码结构,那是另一套东西,这里不展开。
2.2 输出层与损失函数的选择
整图多分类本质是单标签多分类,输出层用 softmax,损失用交叉熵。这里有个容易忽略的点:类别标签不是简单的 0~9999 连续整数,而是“字符组合”到索引的映射。训练前必须建立一张稳定的映射表,保证训练和推理用的是同一套编码,否则会出现“训练准确率很高、推理全错”的玄学现象。
# 构建字符组合到类别索引的映射,固定长度、固定字符集 import itertools CHARS = "0123456789" LENGTH = 4 combos = ["".join(p) for p in itertools.product(CHARS, repeat=LENGTH)] char2idx = {c: i for i, c in enumerate(combos)} idx2char = {i: c for c, i in char2idx.items()} # 保存映射表,推理阶段必须加载同一份 import json with open("label_map.json", "w", encoding="utf-8") as f: json.dump({"char2idx": char2idx, "chars": CHARS, "length": LENGTH}, f, ensure_ascii=False)这段代码用笛卡尔积生成全部组合,顺序由itertools.product保证稳定。char2idx是训练标签编码的依据,idx2char用于推理时把预测索引还原成字符串。映射表落盘是血泪经验:很多人训练完直接拿模型去推理,标签顺序靠代码里重新生成,一旦字符集或位数改过,顺序就对不上,预测结果看起来像随机输出。参数上,CHARS和LENGTH必须和实际数据一致,改一个就要重新生成映射并重新训练。
2.3 数据从哪来:合成与真实样本的配比
公开的真实验证码数据集不好找,常见做法是先程序化合成。合成的好处是标签天然准确、数量可控,坏处是合成分布和真实分布有差距,模型容易过拟合到合成风格。我一般会按 7:3 配比:七成合成数据打底,三成真实抓取或手工标注的样本做微调。合成时重点模拟真实干扰:随机噪点、干扰线、字符旋转、颜色抖动、背景渐变。
from PIL import Image, ImageDraw, ImageFont import random def gen_captcha(text, font_path, size=(120, 40)): img = Image.new("RGB", size, (255, 255, 255)) draw = ImageDraw.Draw(img) font = ImageFont.truetype(font_path, 28) # 逐字符绘制,加入随机偏移和颜色 x = 8 for ch in text: color = (random.randint(0, 120), random.randint(0, 120), random.randint(0, 120)) y = random.randint(0, 6) draw.text((x, y), ch, font=font, fill=color) x += random.randint(24, 30) # 叠加干扰线和噪点 for _ in range(random.randint(2, 4)): draw.line([(random.randint(0, size[0]), random.randint(0, size[1])), (random.randint(0, size[0]), random.randint(0, size[1]))], fill=(random.randint(0, 200),) * 3, width=1) for _ in range(80): draw.point((random.randint(0, size[0]-1), random.randint(0, size[1]-1)), fill=(random.randint(0, 255),) * 3) return imgfont_path指向本地字体文件,建议多准备几种字体轮换,增加字形多样性。size要和后续网络输入尺寸匹配,常见 120×40 或 100×30。逐字符绘制时的x步进和y偏移模拟字符间距和基线抖动,干扰线数量控制在 2~4 条,太多会把字符完全盖住,模型学不到有效特征。噪点 80 个左右是经验值,按图片面积可调。合成脚本跑完记得人工抽检几十张,确认没有出现字符被完全遮挡的废样本,这类样本会拉低训练稳定性。
3. 预处理与网络搭建:把图喂对,把结构搭稳
3.1 图像预处理管线的固定顺序
预处理顺序错了,后面调参全是白费。推荐顺序是:灰度化 → 尺寸归一化 → 归一化到 [0,1] → 标准化。灰度化能去掉颜色干扰,字符型验证码的颜色信息对识别帮助有限,反而增加方差。尺寸统一到网络输入尺寸,避免动态 shape 带来的 batch 处理麻烦。标准化用均值和标准差,如果数据是自己合成的,直接算一遍训练集统计量即可。
import torch from torchvision import transforms train_tf = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((40, 120)), transforms.ToTensor(), # 自动除以 255 transforms.Normalize(mean=[0.5], std=[0.5]) # 单通道标准化 ])Grayscale输出单通道,所以Normalize的 mean/std 只给一个值。Resize的宽高顺序是 (H, W),别写成 (120, 40),否则图会被拉变形,字符比例失真,识别率明显下降。ToTensor已经把像素缩到 [0,1],Normalize再减均值除标准差,把分布拉到 [-1,1] 附近,配合 ReLU 系网络收敛更稳。如果换成真实样本,建议用训练集实际统计的 mean/std 替换 0.5,别照抄。
3.2 一个够用又不臃肿的 CNN 结构
字符型验证码是小图任务,不需要上很深的骨干网。三层卷积加池化,接两层全连接,参数量控制在百万级,训练快、过拟合风险低。卷积核用 3×3,通道数 32→64→128 递增,每层后接 BatchNorm 和 ReLU,池化用 2×2 最大池化。全连接前把特征图展平,中间加 Dropout 抑制过拟合。
import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 40x120 -> 20x60 nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 20x60 -> 10x30 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2) # 10x30 -> 5x15 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 5 * 15, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))padding=1保证 3×3 卷积不缩小特征图,尺寸变化只由池化决定,注释里标了每层后的尺寸,方便核对展平维度。128 * 5 * 15必须和实际输入尺寸对应,输入改成 100×30 时这个数要重算,否则Linear维度报错。Dropout(0.5)放在全连接中间,卷积层不加 Dropout,小图任务卷积层加 Dropout 容易欠拟合。num_classes由映射表长度决定,4 位数字就是 10000。
3.3 训练循环里的关键参数
优化器用 Adam,学习率 1e-3 起步,batch size 64 或 128。训练轮数不用设太大,配合学习率衰减和早停,一般 20~30 轮就能看出趋势。评估指标看验证集准确率,同时观察训练损失和验证损失的差距,差距拉大就是过拟合信号。
import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = CaptchaCNN(num_classes=len(char2idx)).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=8, gamma=0.5) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证阶段省略,核心是每轮记录 val_accStepLR每 8 轮把学习率减半,避免后期在最优解附近震荡。zero_grad必须在反向传播前调用,漏掉会导致梯度累加,损失曲线异常。验证阶段记得model.eval()并加torch.no_grad(),否则 BatchNorm 的统计量和 Dropout 会干扰评估结果,验证准确率忽高忽低。如果显存不够,优先降 batch size,别急着砍网络深度,小图任务深度本来就浅。
4. 避坑与排查:训练能跑不等于能用
4.1 训练准确率 99% 但推理全错
现象是训练集准确率很快冲到很高,拿几张新图推理却几乎全错。原因多半是标签映射不一致,训练时用的char2idx和推理时重新生成的顺序不同,或者字符集、位数改过没同步。解决方法是把映射表随模型一起保存,推理时只加载保存的那份,绝不重新生成。模型文件旁边放一个label_map.json,加载顺序固定。
4.2 损失不下降或变成 NaN
现象是训练几轮后损失卡住,或者直接 NaN。常见原因是学习率过大、输入没标准化、标签越界。先检查Normalize是否和输入通道匹配,再确认标签索引没有超过num_classes。学习率从 1e-3 降到 1e-4 试一轮,如果恢复下降就是学习率问题。另外合成数据里如果有全白或全黑的废图,也会让损失异常,训练前做一遍样本过滤。
4.3 验证集准确率远低于训练集
现象是训练集 98%,验证集只有 70% 出头。这是过拟合的典型信号,尤其在合成数据风格单一时明显。解决方向有三个:增加数据增强(随机旋转小角度、亮度抖动、轻微仿射变换),提高真实样本比例,或者加大 Dropout。别一上来就加网络深度,小图任务加深反而更容易过拟合。增强幅度要克制,旋转超过 10 度字符就变形了,模型学不到正常字形。
4.4 推理速度慢或显存爆
现象是单张推理要几百毫秒,或者 batch 推理直接显存不足。原因通常是推理时没切到 eval 模式、没关梯度,或者 batch size 设得和训练一样大。推理阶段用torch.no_grad(),batch 按显存实际情况调小,单张推理没必要凑 batch。另外模型保存时只存state_dict,加载时先实例化结构再load_state_dict,别用整模型 pickle,换环境容易出兼容问题。
5. 进阶技巧:把识别率从“能用”推到“敢用”
模型训完只是起点,真正上线前还有两件事值得做。第一件是置信度阈值加拒识。softmax 输出的最高分不是永远可信,分数低于阈值时宁可返回“识别失败”也不硬猜,后续可以走重试或人工兜底。第二件是难样本回流。把线上识别置信度低或人工纠正过的样本收集起来,定期加入训练集做增量微调,模型会逐步适配真实分布。
import torch.nn.functional as F def predict(model, img_tensor, idx2char, threshold=0.9): model.eval() with torch.no_grad(): logits = model(img_tensor.unsqueeze(0)) prob = F.softmax(logits, dim=1) conf, pred = prob.max(dim=1) if conf.item() < threshold: return None, conf.item() # 拒识,交给上层重试 return idx2char[pred.item()], conf.item()threshold建议先在验证集上画一条置信度-准确率曲线再定,0.9 是常见起点,但不同数据分布要微调。拒识比例太高会影响自动化率,太低则错误率上升,这个平衡点只能靠自己的数据试出来。难样本回流时注意别把标注错误的样本混进去,增量训练前人工过一遍,否则模型会被带偏。
还有一个容易被忽略的细节:训练和推理的图像来源不同时,预处理必须完全一致。我见过有人训练用 PIL 读图、推理用 OpenCV 读图,通道顺序和插值方式不同,识别率直接掉一截。从那以后我每次部署前都强制走一遍“同一张图、两条管线、比对输出”的检查,确认预处理对齐了再上线。希望这套流程帮到你,少走几个我踩过的坑。
本文还有配套的精品资源,点击获取