
简介这份资源面向深度学习入门与计算机视觉实践者聚焦字符型图片数字验证码的识别任务提供从数据准备、模型构建到训练评估的完整实现路径。内容围绕卷积神经网络与循环神经网络展开涵盖图像归一化、数据增强、one-hot编码、迁移学习与集成思路等关键环节适合希望系统掌握验证码识别流程、并借助Python代码动手复现的读者。资源包共1210个文件以978个png与202个jpg验证码样本图片为主体另含17个py脚本、2个txt说明及zip、rst等辅助文件压缩包约9.58MB样本与代码配套便于直接训练调试。目前已有1543人学习下载。读者可据此获得一套可运行的训练与预测源码理解CNN特征提取与LSTM序列建模的组合方式并参考数据处理、模型保存与准确率评估的实践细节为后续迁移到更复杂验证码场景打下基础。1. 字符型验证码识别从一张 120×40 的图说起登录、注册、找回密码很多系统的第一道门就是一张 120×40 的字符型验证码。它不像滑块那样依赖交互轨迹也不像点选那样依赖语义本质就是一张被加了噪点、干扰线、轻微扭曲的短字符串图片。对做深度学习落地的人来说这类资源的价值不在于“识别验证码”这个动作本身而在于它把一条完整的图像分类流水线拆得足够干净数据生成、标签编码、CNN 建模、训练调参、推理解码每一步都能单独拿出来复用。这份基于深度学习字符型图片数字验证码识别完整过程及 Python 实现的资源适合两类人一类是刚学完卷积网络、想找一个能跑通全流程的小项目练手的新手另一类是要在内部系统做自动化测试、需要批量处理固定格式字符图片的从业者。它不解决通用 OCR但对固定长度、固定字符集的场景性价比很高。2. 先想清楚字符集与标签编码多分类还是多标签动手写模型之前最容易翻车的地方不是网络结构而是标签怎么表示。字符型验证码的识别本质上是“一张图对应 N 个字符”每个字符来自一个有限字符集。这里有两个流派把整串当成一个类别做多分类或者把每个字符位置当成一个独立的多分类任务。前者类别数是字符集大小的 N 次方四个数字就是 10000 类五个字符直接爆炸后者每个位置独立预测参数量和样本需求都线性增长。我一般会选后者也就是多标签多分类的思路输出层按位置拆头。2.1 字符集定义与标签映射先固定字符集不要边训练边改。数字验证码就是 0-9字符型再加上大小写字母但要注意易混淆字符0/O、1/l/I是否要保留。下面这段是标签编码的核心逻辑字符集和长度都做成常量后面所有脚本共用。# charset.py CHARSET 0123456789 # 字符集按需扩展为字母 CHAR2IDX {c: i for i, c in enumerate(CHARSET)} IDX2CHAR {i: c for c, i in CHAR2IDX.items()} CAPTCHA_LEN 4 # 固定长度训练和推理必须一致 NUM_CLASSES len(CHARSET) def encode_label(text): 把字符串标签转成定长索引列表长度不符直接报错 if len(text) ! CAPTCHA_LEN: raise ValueError(flabel length {len(text)} ! {CAPTCHA_LEN}) return [CHAR2IDX[c] for c in text] def decode_pred(indices): 把模型输出的索引列表还原成字符串 return .join(IDX2CHAR[i] for i in indices)这段代码的关键不在函数本身而在两个约束CAPTCHA_LEN必须和模型输出头数量一致CHARSET的顺序一旦确定就不能在训练中途调整否则历史权重全部作废。常见做法是把字符集写进配置文件训练脚本和推理脚本都从同一份配置读取避免“训练时是 10 类、推理时按 36 类解码”这种低级错误。标签编码完成后建议先抽 20 张图人工核对一遍映射确认没有把字符顺序读反。2.2 数据生成与增强的边界如果手头没有现成样本可以用图像库合成一批带噪点、干扰线和轻微旋转的字符图。合成的好处是标签绝对准确坏处是合成分布和真实分布有差距。我的经验是合成数据只用来冷启动真实样本哪怕只有几百张也要按比例混进去微调。增强方面灰度化、轻度高斯噪声、小角度旋转是安全的大幅裁剪、强透视变换会破坏字符结构反而让模型学偏。下面是一个最小可用的合成脚本片段重点看参数范围。from PIL import Image, ImageDraw, ImageFont import random def make_captcha(text, size(120, 40)): img Image.new(RGB, size, (255, 255, 255)) draw ImageDraw.Draw(img) font ImageFont.truetype(arial.ttf, 28) x 8 for ch in text: # 每个字符轻微上下偏移模拟真实扭曲 y random.randint(2, 8) draw.text((x, y), ch, fill(0, 0, 0), fontfont) x 26 # 干扰线数量控制在 2~3 条过多会淹没字符 for _ in range(random.randint(2, 3)): draw.line([(random.randint(0, 120), random.randint(0, 40)), (random.randint(0, 120), random.randint(0, 40))], fill(random.randint(100, 200),) * 3, width1) return img参数上字符间距 26 像素、字号 28 是 120×40 画布下的经验值间距太小字符会粘连太大又浪费横向空间。干扰线灰度控制在 100-200 之间纯黑干扰线会让模型把线和字符混淆。合成脚本跑完后务必用Image.open回读几张确认字体文件路径有效否则会静默生成空白图训练 loss 一直不降还找不到原因。3. CNN 建模与训练输出头怎么拆、loss 怎么算网络结构不需要太深字符型验证码的特征相对低级四到六个卷积块足够。真正需要想清楚的是输出头如果长度是 4、字符集 10 类输出层就是 4 个并行的 10 分类头总输出维度 40。loss 用每个位置交叉熵求和或求平均不要用整串的 one-hot 交叉熵。下面给一个基于常见深度学习框架的模型定义和训练循环骨架参数按 120×40 输入设定。3.1 模型结构与输出头拆分import torch import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, num_classes10, captcha_len4): super().__init__() self.backbone nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 60x20 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 30x10 nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 15x5 nn.AdaptiveAvgPool2d((1, 1)) ) self.heads nn.ModuleList([ nn.Linear(128, num_classes) for _ in range(captcha_len) ]) def forward(self, x): feat self.backbone(x).flatten(1) # 每个位置独立输出 logits返回列表便于逐位算 loss return [head(feat) for head in self.heads]输入必须是单通道灰度图尺寸统一到 120×40训练前用transforms.Grayscale和Resize固定下来。三个池化后特征图降到 15×5再全局池化到 1×1这个设计对短字符串够用如果字符长度增加到 6 以上建议保留横向维度用自适应池化到(1, W)再接全连接否则横向位置信息丢得太多。heads用ModuleList而不是单个大线性层是为了让每个位置的梯度独立回传调试时可以单独看某一位的准确率。3.2 训练循环与逐位 lossdef train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 for imgs, labels in loader: imgs imgs.to(device) labels labels.to(device) # shape: [B, captcha_len] optimizer.zero_grad() logits_list model(imgs) loss 0.0 for i, logits in enumerate(logits_list): loss criterion(logits, labels[:, i]) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader)labels[:, i]取的是第 i 个位置的标签和heads[i]一一对应顺序不能错。criterion 用nn.CrossEntropyLoss()即可它内部带 softmax。学习率从 1e-3 起步batch size 32 或 64训练 20 到 30 轮后看验证集逐位准确率。如果某一位准确率明显低于其他位大概率是数据里该位置字符分布不均或者该位置恰好被干扰线覆盖得多需要针对性补样本。评估时不要只看整串完全匹配率逐位准确率更能定位问题。4. 推理与解码整串匹配率才是最终指标训练 loss 降下去不代表能用推理阶段要把四个头的 argmax 拼起来再和真值做整串比对。很多新手在这里踩坑逐位准确率 95%整串匹配率可能只有 80%因为四个位置全对的概率是逐位准确率的乘积。所以验收标准一定要用整串匹配率并且按字符长度和字符集分别统计。4.1 批量推理与置信度过滤torch.no_grad() def predict(model, imgs, device): model.eval() imgs imgs.to(device) logits_list model(imgs) preds, confs [], [] for logits in logits_list: prob torch.softmax(logits, dim1) conf, idx prob.max(dim1) preds.append(idx) confs.append(conf) # 按位置堆叠转成 [B, captcha_len] pred_idx torch.stack(preds, dim1).cpu().numpy() pred_conf torch.stack(confs, dim1).cpu().numpy() texts [.join(IDX2CHAR[i] for i in row) for row in pred_idx] return texts, pred_conf置信度过滤是落地时的后悔药整串最低置信度低于阈值比如 0.6的样本直接打回人工或重试不要硬用。实际系统里识别错误比识别不出更麻烦前者会污染下游数据。pred_conf取每个位置的最大 softmax 值再取最小值作为整串置信度这个策略简单但有效。批量推理时注意model.eval()和torch.no_grad()都要加否则显存会随 batch 累积。4.2 整串匹配率评估脚本def eval_accuracy(model, loader, device): total, exact 0, 0 for imgs, labels in loader: texts, _ predict(model, imgs, device) gt [.join(IDX2CHAR[i] for i in row) for row in labels.numpy()] total len(gt) exact sum(t g for t, g in zip(texts, gt)) return exact / total这个脚本跑出来的数字才是对外汇报的数字。建议同时记录混淆矩阵看看哪些字符对经常互认比如 0 和 O、1 和 7如果字符集里同时包含这些考虑在数据增强里加针对性扰动或者干脆从字符集里剔除。5. 避坑与排查五个真实翻车现场5.1 训练 loss 不降检查标签和输入通道现象跑了十几轮 loss 在 2.3 附近震荡。原因多半是输入通道不对模型第一层是Conv2d(1, ...)但数据加载出来是 RGB 三通道或者标签编码时字符集顺序和推理脚本不一致。解决在 dataloader 里打印一个 batch 的 shape 和对应标签确认是[B, 1, 40, 120]且标签在 0 到 9 之间。这个检查我每次换数据集都会做一遍两分钟省半天。5.2 逐位准确率高但整串匹配率低现象单位准确率 96%整串只有 78%。原因是四个位置独立预测误差累积。解决不要盲目加深网络先看是不是某一位拖后腿针对性补该位置的样本其次考虑在 loss 里给不同位置加权或者引入位置间的轻量序列建模。字符型验证码长度固定时简单加权通常够用。5.3 合成数据训练、真实数据崩盘现象合成集上整串匹配率 99%换真实样本掉到 40%。原因是合成图的噪声分布、字体、抗锯齿和真实图差异大。解决合成数据只做预训练真实样本哪怕几百张也要参与微调学习率调小到 1e-4。另外检查真实图是否被平台压缩过压缩伪影会让边缘变糊训练时加一点 JPEG 质量扰动做增强。5.4 推理速度慢GPU 利用率低现象单张推理几十毫秒批量反而更慢。原因是没有做 batch 拼接或者每张图都重新初始化模型。解决推理脚本里模型只加载一次图片预处理用固定尺寸的 tensor 批量堆叠如果部署在 CPU 上把模型转成推理模式并考虑量化字符型小模型量化后精度损失通常可控。5.5 字符集扩展后旧模型直接失效现象从纯数字扩展到字母数字混合旧权重加载后输出维度对不上。原因是输出头数量和字符集绑定。解决字符集变更视为新任务重新训练如果必须复用冻结 backbone 只重训 heads但效果通常不如从头训。我的习惯是把字符集版本号写进模型文件名加载时校验避免拿错权重。6. 进阶技巧把验证码识别做成可复用的推理服务单脚本跑通只是第一步真正落地时我更关心“换一批图能不能快速验证”。一个实用技巧是把预处理、模型加载、解码封装成一个类对外只暴露predict(image_bytes)内部固定字符集和长度置信度低于阈值返回空串。这样上游系统不用关心模型细节换模型只改一个配置。另一个技巧是维护一个小型回归集每次改数据增强或网络结构先在这个集上跑整串匹配率低于基线就不合并。回归集不用大两三百张覆盖各字符组合即可但必须包含真实样本。下面是一个服务封装的骨架重点在配置校验和阈值出口。class CaptchaRecognizer: def __init__(self, weight_path, charset, captcha_len, conf_threshold0.6): self.charset charset self.captcha_len captcha_len self.conf_threshold conf_threshold self.model CaptchaCNN(len(charset), captcha_len) state torch.load(weight_path, map_locationcpu) # 加载前校验输出头数量防止字符集不匹配 assert len(state[heads.0.weight]) len(charset), charset mismatch self.model.load_state_dict(state) self.model.eval() def predict(self, image_bytes): img preprocess(image_bytes, self.captcha_len) texts, confs predict(self.model, img.unsqueeze(0), cpu) if confs.min() self.conf_threshold: return return texts[0]assert那行是血泪经验字符集改过但权重没换的情况在多人协作里太常见宁可启动时报错也不要静默输出错位结果。阈值 0.6 不是万能值按业务容忍度调宁可漏识别也不要错识别。从那以后我每次上线新权重都强制先跑一遍回归集并对比整串匹配率再走灰度。希望帮到你。本文还有配套的精品资源点击获取