简介:这份下载包提供基于onehot编码与CNN网络的5位数验证码识别完整Python实现,面向计算机相关专业的毕设、课程设计及深度学习入门者,解决验证码图像分类与字符识别问题。包内共2000个文件、约43.25MB,以1980张jpg验证码图片组成的数据集为主体,另含8个xml文件、6个py源码、txt说明及md笔记等,源码带详细注释,便于读者理解onehot标签构建、数据增强与卷积网络训练流程。目前已有184人学习浏览。项目代码经测试可正常运行,既可直接复现5位数验证码识别效果,也可按需调整网络结构或扩充数据集,迁移到其他字符识别场景。对于希望快速上手CNN图像识别、完成毕业设计或积累项目经验的读者,这份内容详实的资源具有较高的借鉴和二次开发价值。
1. 5位数字验证码识别:一个把onehot编码、CNN和毕设串起来的完整项目
做爬虫采集、做自动化测试、做数据标注工具的人,估计都绕不过验证码这道坎。5位纯数字验证码算是门槛最低的一类,但它恰好能把一条技术链路完整走通:拿到样本图片、把标签转成onehot编码、搭建CNN卷积网络做训练、再把模型输出解码成字符串。这套资源就是把这条链路整体打包好的一个Python工程,源码、数据集、详细注释都在压缩包里,测试过能跑通,这也是我拆完觉得最值的地方。对打算做毕设、课程设计或大作业的同学来说尤其对口——难度不高不低,既能展示网络结构设计,又能体现数据处理和工程封装能力,还不用在调参上耗掉整个学期。
2. 数据准备与onehot编码:验证码图片如何变成训练样本
这份资源的核心是5位数字验证码识别,所以数据侧的一切设计都围绕“图片 + 5位标签”展开。理解这一章,后面看模型定义和训练循环都会顺很多。
2.1 数据集结构:jpg图片与“文件名即标签”的约定
解压资源后能看到的几类东西:图片都是jpg格式,文件名本身就是5位验证码字符串。项目里直接出现了wmpmp.jpg、ydd3g.jpg这类文件,说明数据集的组织方式基本就是“文件名即标签”。这样做的好处是很省事——不需要维护额外的CSV或JSON标注文件,写数据加载代码时,直接从文件名里把目标字符串切出来就行。
一份能跑的验证码识别工程,目录组织一般是这样的:
验证码识别/ ├── data/ │ ├── train/ # 训练样本,文件名即标签,如 wmpmp.jpg │ ├── val/ # 验证样本,结构与训练集一致 │ └── test/ # 测试样本,同样用验证码字符串命名 ├── train.py # 模型训练入口 ├── predict.py # 单张图片预测入口 ├── model.py # CNN网络结构定义 └── requirements.txt # 依赖清单:torch、pillow、numpy 等这里的关键是训练目录里的图片命名必须规范,不能带空格或中文,否则解析标签时还得加一套清洗逻辑。我拿到数据集的第一件事,就是统计一下train目录里每个数字的样本量分布,确认0-9十个数字都够用。如果某个数字只有几十张,后面训练时模型很容易把它学偏。
预处理上,常规操作是读图后转灰度。验证码的颜色信息对识别几乎没有帮助,反而会放大不同样本之间的色差干扰,而灰度图能直接降低计算量。尺寸方面,先用PIL统一缩放到固定宽高,再转成numpy数组并归一化。
2.2 onehot编码:为什么标签是5×10矩阵而不是一个整数
很多人第一次接触onehot编码,是在文本分类或线性模型里,但验证码识别里的用法略有不同。这个项目要输出的是5位字符,每一位都是0-9中的一个数字。从分类角度看,就是5个并行的10分类问题。CNN最后一层输出的不是“一个数字”,而是5组概率分布,每组长度为10。要让损失函数算得起来,标签也得是同样的形状——5×10的矩阵。
比如“52384”这个标签,编码出来的矩阵第0行是数字5的onehot,第1行是数字2的onehot,依此类推:
import numpy as np def onehot_encode(label_str, num_classes=10): """ 把 '52384' 这类5位数字标签编码成 5x10 的 onehot 矩阵。 矩阵每一行对应一个字符位置,列对应数字类别。 """ matrix = np.zeros((len(label_str), num_classes), dtype=np.float32) for i, ch in enumerate(label_str): digit = int(ch) matrix[i, digit] = 1.0 return matrix label = "52384" encoded = onehot_encode(label) print(encoded.shape) # (5, 10) print(encoded)这段代码的逻辑很直接:遍历标签字符串的每个字符,转成整数后,在对应行把对应列置为1。dtype用float32是因为网络输出的概率值也是float32,后面算交叉熵损失时不会出现类型不匹配。num_classes固定为10,代表0-9十个数字;如果后面想扩展字母识别,这个参数要改成36。
有人会问,为什么不直接把标签当整数回归来做,非要onehot编码?因为验证码识别本质是分类而非回归,数字之间没有“距离”关系,用onehot加softmax才能表达“每一类概率分布”的语义,训练也更稳定。
2.3 数据加载与预处理:Dataset类与训练验证划分
在PyTorch里,数据加载一般通过继承Dataset类来实现。这里把文件名解析、图片读取、预处理和onehot编码整合到一起:
import os from PIL import Image import numpy as np from torch.utils.data import Dataset class CaptchaDataset(Dataset): def __init__(self, img_dir, img_size=(100, 30)): self.img_dir = img_dir self.img_size = img_size self.samples = [f for f in os.listdir(img_dir) if f.lower().endswith('.jpg')] # 校验文件名格式:必须是5位数字 + .jpg self.samples = [f for f in self.samples if len(f.split('.')[0]) == 5 and f.split('.')[0].isdigit()] def __len__(self): return len(self.samples) def __getitem__(self, idx): fname = self.samples[idx] label_str = fname.split('.')[0] img = Image.open(os.path.join(self.img_dir, fname)).convert('L') img = img.resize(self.img_size) arr = np.array(img, dtype=np.float32) / 255.0 arr = arr.reshape(1, self.img_size[1], self.img_size[0]) label_arr = onehot_encode(label_str) return arr, label_arr这段代码里有几个值得注意的点。resize统一了所有图片的尺寸,convert('L')转灰度,/255.0把像素从0-255缩放到0-1区间,这是CNN收敛速度的分水岭。reshape(1, H, W)保留了通道维,因为PyTorch卷积层默认输入是(batch, channel, height, width)。
Dataset类里面加了一道文件名合法性校验:只保留“5位数字 + .jpg”的文件。校验看似多余,实际作用很大——万一测试环境里混入了一张系统生成的缩略图或者隐藏文件,会在训练中途爆出诡异的shape错误。
数据划分建议按8:1:1拆成训练、验证、测试三份。验证集用来观察过拟合,测试集只在最终评估时碰一次。很多人在小数据集上翻车,就是因为调参时反复用测试集,最后测试集准确率虚高,换个新验证码就露馅。
3. 搭建CNN网络识别5位验证码:结构设计与训练参数
CNN是这份资源的核心模型。这一章从“为什么是CNN”讲到“网络每一层怎么设置”,最后落到训练循环和参数选择上。
3.1 卷积网络在验证码图片上的优势:局部特征与参数共享
验证码图片和自然图像最大的区别是内容高度结构化——字符由横、竖、折、弧这些基本笔画组成。CNN的卷积核天然关注局部区域,一个3×3的卷积核能捕捉“这里是竖线”“那里是横线”这类微观模式,多个卷积层堆叠后,就能组合出“这个笔画组合看起来像数字2”“那个像数字5”这样的高维特征。
相比全连接网络直接吃像素,CNN的另一个关键优势是参数共享。同一个卷积核在整张图上滑动,参数量远远小于全连接层,这在样本量只有几千张的验证码任务里尤其重要——参数越少,越不容易过拟合。
验证码通常还带噪点、细干扰线,这些噪声在像素级上表现是孤立的小亮块。卷积加池化的组合天然有抑制噪声的作用:池化取局部最大值或平均值,能抹掉单像素噪声;卷积层的局部感受野也不会因为远处一个噪点就影响当前笔画的判断。
3.2 完整网络结构:三层卷积加全连接的具体实现
针对5位数字验证码,一个比较通用的结构是三层卷积用于特征提取,一层全连接做特征组合,最后输出5×10的分类结果:
import torch import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, num_chars=5, num_classes=10, fc_dim=256): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2) ) self.fc = nn.Sequential( nn.Linear(128 * 4 * 15, fc_dim), nn.ReLU(inplace=True), nn.Dropout(0.3) ) self.output = nn.Linear(fc_dim, num_chars * num_classes) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.fc(x) x = self.output(x) x = x.view(-1, self.num_chars, self.num_classes) return x网络里有个容易模糊的地方:最后一行view输出为(batch, num_chars, num_classes)。这个形状意味着每个字符位置都有独立的10类概率分布。训练时对每个位置分别算交叉熵,预测时对每个位置取argmax,整条逻辑是闭合的。
fc这里用了Dropout(0.3),作用是在训练时随机丢弃一部分神经元,防止模型死记训练集中的噪声模式。验证码数据集通常不大,Dropout几乎是标配。
128 * 4 * 15这个数值是根据输入尺寸100×30推算出来的。三次MaxPool2d(2,2)后,宽高分别除以8,100/8≈12,30/8≈3.75。实际项目里,我一般先打印一下池化后特征图的shape再填这个数字,避免手算错了导致线性层维度对不上。如果换图片尺寸,这个数字必须同步调整。
3.3 训练配置:损失函数、优化器与学习率调度
训练阶段的核心代码逻辑是这样的:
import torch.optim as optim model = CaptchaCNN(num_chars=5, num_classes=10) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=4 ) for epoch in range(50): model.train() running_loss = 0.0 for images, labels_onehot in train_loader: optimizer.zero_grad() outputs = model(images) # 从onehot矩阵转为类别索引,(B, 5) -> 用于CrossEntropyLoss labels_idx = labels_onehot.argmax(dim=-1) loss = 0.0 for t in range(5): loss += criterion(outputs[:, t, :], labels_idx[:, t]) loss.backward() optimizer.step() running_loss += loss.item() # 验证阶段 model.eval() val_loss = 0.0 with torch.no_grad(): for images, labels_onehot in val_loader: outputs = model(images) labels_idx = labels_onehot.argmax(dim=-1) for t in range(5): val_loss += criterion(outputs[:, t, :], labels_idx[:, t]).item() scheduler.step(val_loss)这里最容易踩坑的地方是labels_onehot.argmax(dim=-1)。CrossEntropyLoss在PyTorch里接收的是类别索引,不是onehot向量。直接拿5×10的onehot矩阵当标签喂进去,loss算出来的数值是错的且几乎不下降,这是新手最常见的“训练不收敛”原因之一。
学习率用Adam默认的1e-3起步,在这个任务规模下表现比较稳。ReduceLROnPlateau是个很实用的调度器:当验证集loss连续4个epoch不降时,学习率自动减半,比手动盯着曲线改参数省心得多。50个epoch在这个项目规模下一般够用,如果loss还在稳定下降,可以继续加大epoch数,不必死守这个值。
训练过程中的监控指标,我建议同时记录训练loss和验证loss。train loss持续下降但val loss不降反升,说明过拟合信号出现,这时应该看第5章的调整方案,而不是硬着头皮继续训练。
4. 模型推理与解码:从CNN输出到可用的验证码字符串
训练完成后,模型要解决的实际问题变成:给一张新验证码图片,返回5位字符串。这一步看着简单,但推理时的很多细节和训练不同。
4.1 单张图片的预测流程:eval模式与argmax解码
推理时首先要把模型切换到eval模式,然后关闭梯度计算,再对模型输出做argmax:
def predict_single(model, img_path, img_size=(100, 30)): model.eval() img = Image.open(img_path).convert('L') img = img.resize(img_size) arr = np.array(img, dtype=np.float32) / 255.0 arr = arr.reshape(1, 1, img_size[1], img_size[0]) tensor = torch.from_numpy(arr) with torch.no_grad(): outputs = model(tensor) # (1, 5, 10) pred_idx = outputs.argmax(dim=-1) # (1, 5) chars = [str(i) for i in pred_idx.squeeze(0).tolist()] return ''.join(chars)注意两个必须存在的操作。model.eval()会关闭Dropout和BatchNorm的训练行为,否则同样的图片每次预测结果都可能不同,这在验证码识别这种对稳定性要求高的场景里是硬伤。torch.no_grad()则不构建计算图,推理速度更快,内存占用更小。
argmax(dim=-1)在最后一个维度上找概率最大的索引,正好对应0-9的数字。如果以后扩展字符集,这里的数字要对上字符映射表。
4.2 整条准确率与字符级准确率:评估口径要分清
验证码识别的评估指标有一个常见的口径混淆——字符级准确率和整条准确率。字符级准确率是把所有预测字符拼在一起统计,比如100张图共500个字符,预测对490个,准确率98%。整条准确率则要求5位全部正确才算对,100张图里有95张五位全对,整条准确率95%。
这两种口径在业务上差别很大。实际使用时,验证码识别最终要的是“这一串能不能用”,5位里错1位整串就废了。所以项目评估以整条准确率为准更合理:
def evaluate_model(model, loader): model.eval() total = 0 correct = 0 with torch.no_grad(): for images, labels_onehot in loader: outputs = model(images) pred_idx = outputs.argmax(dim=-1) labels_idx = labels_onehot.argmax(dim=-1) correct += (pred_idx == labels_idx).all(dim=-1).sum().item() total += images.size(0) return correct / total核心在all(dim=-1)这一句:5个位置的预测索引必须全部等于标签索引,这一条样本才计数为正确。如果只写pred_idx == labels_idx再sum,统计的是字符数,得到的就是字符级准确率,会被“大部分位对、个位错”的情况拉高,掩盖真实可用性。
在测试集上,这个项目的整条准确率通常能到95%以上。资源里的数据集如果包含干扰线和噪点,会适当下探,但一般不影响演示和毕设答辩。
4.3 错例分析:从错误图片反推问题
评估之后别急着收工,把预测错误的图片单独拎出来看,是验证码识别项目里性价比最高的诊断手段。做法很简单:预测时记录下图片路径、真实标签、预测标签,把预测错误的文件复制到一个error_samples目录。
错例分析通常能发现两类问题。一类是特定数字混淆,比如6和8、5和3在字形上接近,模型容易混,这说明对应数字的训练样本不够或图片本身模糊。另一类是某种干扰样式导致的系统性错误,比如带贯穿横线的验证码模型看不准,这需要在数据增强时针对性加入类似干扰。看错图这一步往往比盲目加epoch更有效。
模型保存时,建议把输入尺寸一并记住。最简单的方式是固定一个全局变量或配置参数,让训练和推理引用同一个值,而不是在推理脚本里重新硬编码。很多“模型加载后预测结果全错”的翻车现场,根因都是训练和推理用的图片尺寸不一致。
5. 验证码识别避坑指南:数据、训练与解码中的典型问题
这一章记录的都是实际运行时会遇到的典型问题,很多坑我在其他项目里也反复踩过。每条按现象、原因、解决的思路展开,方便排错时对照。
5.1 训练loss不下降:onehot标签直接喂给了CrossEntropyLoss
现象:训练开始后loss一直停留在2.3左右,几十个epoch过去纹丝不动,准确率始终在10%上下徘徊。
原因:2.3这个数值有很强的指征意义——它约等于ln(10),也就是随机猜10类时的理论loss。出现这个数值,基本可以断定模型没有学到任何有效模式。最常见的原因是标签编码和损失函数不匹配:onehot是5×10矩阵,而PyTorch的CrossEntropyLoss期望输入的是类别索引,即每个字符位置一个0-9的整数。
解决:在算loss之前,对labels做一次labels_onehot.argmax(dim=-1),转成类别索引再喂给损失函数。同时检查模型输出层后面不能加softmax,CrossEntropyLoss内部已经包含softmax计算,再加就相当于算了两次,也会导致loss不降。
5.2 训练集准确率高、测试集低:过拟合的四个调整方向
现象:训练集准确率很快冲到97%以上,但测试集准确率只有85%左右,且两者差距持续拉大。
原因:验证码数据集通常只有几千张,网络参数量相对偏多,模型记住了训练图片里字符笔画之外的噪声和排版特征,而不是泛化到了字符本身的结构。
解决:按优先级依次尝试四个方向。第一,在fc层加大Dropout比例,从0.3提到0.5,让模型无法死记。第二,减小网络容量,通道数从(32,64,128)降到(16,32,64),fc维度从256降到128。第三,做数据增强,对图片做轻微随机平移和缩放,幅度控制在±5%以内,不要加旋转,验证码是水平的,旋转会引入错误的模式。第四,检查训练集和测试集的图片来源是否一致,如果训练集是程序生成的规则字体、测试集是人工标注的带干扰线图片,那数据分布本身就是错位的。
5.3 解码结果乱序或重复:view与reshape的差异被忽略
现象:单个字符预测正确率很高,但整条预测出来的验证码字符顺序是乱的,或某个数字在5个位置上重复出现。
原因:模型输出从全连接层展平成(B, 5*10)后,转成(B, 5, 10)的方式如果用了reshape而不是view,在PyTorch中reshape在非连续内存上会触发数据重排,造成不同位置的概率分布错乱。另一个常见原因是训练和推理时用的是不同的转换逻辑,一个按行优先拆分,一个按列优先拆分,解码自然对不上。
解决:统一在模型forward中使用x.view(B, num_chars, num_classes),推理解码时对输出做同样的view。另外,解码时不要直接对整体50维argmax,而应该先view成5×10,再在第二个维度上逐个argmax,这样保证每个位置独立取概率最大者。
5.4 训练时显存或内存溢出:三个压缩手段
现象:训练刚开始跑第一个batch就报OOM(Out of Memory),或者中途内存一路飙升直到进程被杀。
原因:输入图片分辨率过大、batch_size过大、卷积通道数过多,三者叠加导致中间特征图占满显存。验证码项目常用的图片尺寸并不大,但有些自带数据集里图片原始分辨率偏高,加载时没有统一缩放就直接送进网络。
解决:先把输入图片统一缩放,目标到100×30或类似的小尺寸,配合预处理解决。然后把batch_size从32降到16或8,PyTorch一个小训练循环里batch_size=8对显存的需求很低。最后看模型结构,三层卷积通道数如果设置到了256或512,对这个小数据集来说纯属浪费,降到(16,32,64)级别即可。
5.5 加载模型时shape不匹配:训练和推理尺寸不一致
现象:加载训练好的state_dict时报错,提示size mismatch for linear.0.weight之类。
原因:训练时图片缩放到100×30,推理时传入的图片是120×40,卷积层不受影响,但池化后特征图尺寸变了,全连接层的输入维度被改掉,模型的权重shape和当前网络结构对不上。
解决:把图片尺寸的定义收敛到一个配置常量里,训练脚本和推理脚本都从同一个配置读取,比如config.py中写IMG_SIZE = (100, 30)。模型保存时也可以把输入尺寸写入一个meta字段,加载模型后再来设置Dataset的img_size,这样即使换了机器拆开使用,也不会出现尺寸不一致的问题。
6. 进阶改造:把5位数字扩展到其他位数或字母数字混合
拿到这份资源跑通之后,改造方向其实很清晰:改位数,改字符集。这一步能帮助把源码吃透,也有实际用途。
6.1 改位数:从5位到4位或6位
位数变化只影响两个地方:num_chars参数和数据集文件名。模型定义里CaptchaCNN(num_chars=5)改成4或6,最后一个输出层的维度自动跟着变。onehot编码函数不需要动,它按标签字符串长度生成矩阵。数据集方面,把训练、验证、测试图片换成对应位数的命名即可。4位验证码的识别难度会明显下降,准确率通常比5位更高;6位则相反,训练样本量和epoch数都要适当增加。
6.2 改字符集:加入小写字母
如果把字符集从10个数字扩展到36个(数字+小写字母),改动点有三个:num_classes从10改成36、onehot编码函数里字符和索引的映射、argmax解码后的索引到字符的还原。
解码部分需要一个小映射表:
CHARS = "0123456789abcdefghijklmnopqrstuvwxyz" def idx_to_char(idx): return CHARS[idx] def decode_pred(pred_idx): return ''.join(idx_to_char(i) for i in pred_idx)训练时要重点检查字符分布是否均衡。数字0-9在样本里出现了多少个,字母a-z是否都有足够样本,如果某些字符只有几十张,模型会直接把它们学成“永远不预测”。一般做法是在生成数据集时保证每个字符出现次数接近,低于某一个阈值就补样本。
我最初接手类似项目时,嫌麻烦跳过了一致性检查,直接改了位数开训,结果全连接层维度对不上,报了好一会儿错才反应过来。从那以后,我每次动位数或字符集都会在全部数据集上先跑一遍统计脚本,确认标签长度、字符集合、网络输出三层完全对齐,再进训练循环。这份资源本身已经把5位数字的链路走通了,你要做的就是在它基础上安全地扩展。希望帮到你。
本文还有配套的精品资源,点击获取