简介一套完整的字符型图片数字验证码识别项目资料基于深度学习技术实现面向正在学习图像识别、神经网络或网络安全反自动化攻防的开发者。内容覆盖验证码数据集构建、图像预处理、卷积神经网络与循环神经网络模型搭建、训练评估及Python推理全流程适合作为深度学习的实战入门与进阶参考。压缩包共1210个文件大小约9.58MB。其中978张png与200余张jpg构成多类型验证码样本集17个py脚本对应数据增强、模型定义、训练和识别调用等核心环节另有txt说明、html页面及模型文件可直接对照运行。目前已有1540人学习使用。通过该资料可快速搭建可复现的验证码识别实验环境理解CNN提取字符特征、RNN处理序列位置、softmax分类的完整链路并能基于自带源码更换数据集进行扩展试验对积累CV落地经验有直接帮助。 如果你正在学深度学习第一个想练手的实战项目大概率是图像分类但对着MNIST跑完一遍总觉得差点意思。我自己带过不少新人发现真正能把图像处理、数据构造、卷积神经网络、训练调参整条链路一次串起来的入门项目其实是验证码识别。这个项目看起来不太起眼但作为深度学习入门练习样本它几乎是完美的载体输入是真实的彩色图片标签是多字符序列模型需要自己从像素里学出特征最后还要完成训练、评估、推理的完整闭环。今天这篇就把整个项目从零到一的完整过程写出来包括数据怎么造、模型怎么搭、代码怎么跑附带一套可以直接运行的Python源码。适合正在入门深度学习、手头缺一个综合项目的朋友。1. 项目整体设计与思路拆解1.1 验证码识别是一个什么级别的问题先把这个项目的本质说清楚。验证码图片识别本质上是一个OCR光学字符识别问题而且是一个被刻意加了噪声和干扰的OCR问题。和通用的场景文字识别相比它的字符数量少、字体类型固定、排版相对规则所以非常适合用来学习图像识别的基本流程同时又比MNIST那种单字符分类多了一层“多字符序列输出”的复杂度。这个“多字符”才是项目真正的价值所在。假如你只是做一个数字单字识别训练好一个分类模型就结束了含金量有限。验证码项目不同一张图里有4个字符模型输出的是一整串标签这就逼着你去思考“怎么把序列信息和图像特征结合起来”也为以后接触目标检测、CTC损失、注意力机制这些进阶方向打了底。我在实际带项目的过程中几乎所有能把图像分类理解透的人都是先从这个多字符模型跑通的。1.2 为什么选深度学习而不是传统OCR这里先给个清醒的判断如果目标是生产系统里的验证码识别传统OCR方案其实也有一定效果Tesseract就是一个老牌开源引擎对于规则、无干扰的字体准确率并不低。但问题是真实验证码几乎都带扭曲、干扰线、噪点、颜色噪块传统方法需要做大量预处理比如去噪、二值化、字符切割每一步都会有误差累计最后效果很难稳定下来。深度学习方案的核心优势是端到端学习。输入一张原始图片卷积网络自动提取边缘、纹理、字符形状等特征不需要手动设计特征工程。而且它天然适应字符粘连的情况因为你可以不进行物理切割直接用整张图参与训练。我做了一个简单对比可以直观看到两者的区别。对比维度传统OCRTesseract等深度学习CNN特征提取人工设计依赖预处理自动学习端到端抗干扰能力弱需要比较干净的输入强可适应噪点、扭曲字符分割必须先切分干净可整体识别不必须切割开发复杂度调参点多各环节耦合统一训练代码集中适合场景扫描件、清晰文档复杂背景、带干扰图片从入门学习角度看深度学习这条路还有一个隐性优势代码量其实更少权重更新全交给反向传播你需要管的只有网络结构、数据、超参和损失函数这四件事。尤其是当你以后要做更复杂的图像任务时这套“数据模型训练”的思维可以直接复用而不是每次换任务都要重新设计一堆图像处理规则。2. 环境准备与训练数据构造2.1 Python环境与依赖库安装这个项目对环境要求不高Windows、Linux、macOS都行显卡不是必需我用纯CPU跑过二十轮训练大约十几分钟就能完成。建议用Python 3.8以上版本先建一个虚拟环境再安装依赖库。核心需要四个库torch、torchvision、pillow、numpy其中torch和torchvision是深度学习的基础pillow负责图像生成和读取numpy用于向量计算。pip install torch torchvision pillow numpy这里提醒一个坑torch的CPU版本和GPU版本安装命令不同如果你只是学习跑项目CPU版本完全够用不用为了这个项目专门配CUDA环境。我平时甚至会在没有显卡的机器上跑这个模型的推理速度照样很快因为它本身是个小模型计算量远没有很多生产级网络那么大。只要把torch装好后面几乎不会遇到环境问题。2.2 自制训练数据集用PIL批量生成验证码真实验证码数据很不好搞一是数据归属第三方平台直接抓取涉及合规问题二是手工标注成本太高。最靠谱的入门办法是自己造数据。自己生成数据的好处是标签完全准确数量可以无限扩充还能通过调节干扰线、噪点、字符偏移来控制难度这比在网上找数据集要灵活得多。生成数据的基本思路是创建一张随机背景色的底图在上面画随机噪点、干扰线再逐个字符画上去做轻微模糊最后让文件名携带标签信息。下面是完整代码我习惯把它放在gen_data.py文件里。import os import random from PIL import Image, ImageDraw, ImageFont, ImageFilter char_set 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ IMG_WIDTH, IMG_HEIGHT 160, 60 CHAR_COUNT 4 def random_color(min_v0, max_v255): return (random.randint(min_v, max_v), random.randint(min_v, max_v), random.randint(min_v, max_v)) def generate_one(): img Image.new(RGB, (IMG_WIDTH, IMG_HEIGHT), random_color(180, 255)) draw ImageDraw.Draw(img) # 随机噪点 for _ in range(300): draw.point((random.randint(0, IMG_WIDTH - 1), random.randint(0, IMG_HEIGHT - 1)), fillrandom_color(0, 255)) # 随机干扰线 for _ in range(random.randint(1, 3)): start (random.randint(0, IMG_WIDTH // 2), random.randint(0, IMG_HEIGHT)) end (random.randint(IMG_WIDTH // 2, IMG_WIDTH), random.randint(0, IMG_HEIGHT)) draw.line([start, end], fillrandom_color(0, 255), width2) label font ImageFont.truetype(arial.ttf, 36) char_w IMG_WIDTH // CHAR_COUNT for i in range(CHAR_COUNT): c random.choice(char_set) label c x i * char_w random.randint(0, 10) y random.randint(2, 15) draw.text((x, y), c, fontfont, fillrandom_color(50, 200)) img img.filter(ImageFilter.GaussianBlur(radius0.5)) return img, label if __name__ __main__: os.makedirs(data/train, exist_okTrue) os.makedirs(data/val, exist_okTrue) for i in range(20000): img, label generate_one() img.save(fdata/train/{i:05d}_{label}.png) for i in range(2000): img, label generate_one() img.save(fdata/val/{i:05d}_{label}.png) print(数据集生成完成)如果你用的是Linux系统默认可能没有arial.ttf可以把字体路径换成/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf效果差别不大。我在字符位置这里加了随机偏移颜色也是每个字符单独随机这样能模拟真实验证码的“不规整感”。模型学过这种偏移后实际推理时的鲁棒性会好很多这比字符整整齐齐排列的数据更有训练价值。3. 模型设计与核心细节解析3.1 CNN网络结构设计思路这个项目我选择的是一个轻量CNN三层卷积加两层全连接。结构上刻意保持简单因为验证码识别的难点不在模型深度而在数据质量和训练策略。网络定义我写在model.py里后续训练和推理都会引用它。import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, num_chars36, max_len4): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Linear(128 * 20 * 7, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_chars * max_len), ) self.num_chars num_chars self.max_len max_len def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x.view(x.size(0), self.max_len, self.num_chars)现在解释一下这个网络的关键参数。输入图片是160x60x3经过三次最大池化后高度变成60除以2的三次方也就是60/2/2/2结果是7向下取整宽度变成160/2/2/220。所以展平后的特征维度是12820717920这就是第一个全连接层输入维度的由来。如果你改了图片尺寸这里的数字一定要同步更新否则模型会直接报维度不匹配的错误。为什么每个卷积层后面都加BatchNorm因为卷积层出来的特征分布不稳定BatchNorm会把每层输入拉回标准分布明显加快收敛速度。这对新手很友好损失函数很容易降下来不用反复调参。Dropout放在全连接层之前设置为0.5是因为全连接层参数量大是过拟合高发区训练时随机丢弃一半神经元相当于隐式做了模型集成能显著提升验证集准确率。3.2 多标签分类与损失函数设计验证码有4个字符最直接的建模方式是把它拆成4个独立的36分类任务。模型最后输出的形状是[B, 4, 36]第2维表示字符位置第3维是每个候选字符的概率。这么做的好处是结构简单、训练稳定也不需要在生成数据时保证四个字符互不重复随机抽样即可。损失函数用CrossEntropyLoss它把Softmax和交叉熵合并在一起梯度计算更稳定。这里有一个必须注意的PyTorch细节CrossEntropyLoss期望输入是[B, C, ...]的形状所以要把网络输出的[B, 4, 36]转成[B, 36, 4]让通道维放外面target直接传[B, 4]的索引数组就行。这个转换会在训练脚本里执行看代码就明白。准确率的评价标准我用了最严格的方式4个字符全部预测正确才算一张图识别成功。同时也会观察字符级准确率因为训练早期字符级准确率会先爬到九十多能帮你判断模型是否还在正常学习。如果你只在意“每个位置对不对”容易被表面的高准确率误导尤其是当某些字符类被模型忽略时整体准确率会明显下降。4. 实操过程训练、评估与推理4.1 数据加载与训练脚本训练前的准备工作是把图片路径、标签对上传给DataLoader。这里我自定义了一个Dataset类从文件名的固定格式“编号_标签.png”中解析出真实标签再通过字符映射表把字符转成索引数组。需要注意图片训练时的预处理必须和推理时保持一致否则会出现训练很准、预测单图却全错的情况我见过太多人踩这个坑。完整训练脚本train.py如下。import os import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image from model import CaptchaCNN char_set 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ class CaptchaDataset(Dataset): def __init__(self, root, transformNone): self.paths, self.labels [], [] for fname in os.listdir(root): if not fname.endswith(.png): continue self.paths.append(os.path.join(root, fname)) self.labels.append(fname.split(_)[1].split(.)[0]) self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) if self.transform: img self.transform(img) label_idx [char_set.index(c) for c in self.labels[idx]] return img, torch.tensor(label_idx, dtypetorch.long) transform transforms.Compose([ transforms.Resize((160, 60)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) device torch.device(cuda if torch.cuda.is_available() else cpu) model CaptchaCNN().to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) train_ds CaptchaDataset(data/train, transformtransform) val_ds CaptchaDataset(data/val, transformtransform) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) def evaluate(model, loader): model.eval() correct 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) with torch.no_grad(): preds model(imgs).argmax(dim-1) correct (preds labels).all(dim1).sum().item() return correct / len(loader.dataset) for epoch in range(20): model.train() total_loss 0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) # [B, 4, 36] loss criterion(outputs.permute(0, 2, 1), labels) # 转成 [B, 36, 4] optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) val_acc evaluate(model, val_loader) print(fepoch {epoch 1:02d} / loss {total_loss / len(train_ds):.4f} / val_acc {val_acc:.4f}) torch.save(model.state_dict(), captcha_cnn.pt)我实际训练时的日志大致是这样的第1轮loss在1.2左右val_acc不到三成到第4轮val_acc冲到85%以上第10轮以后基本稳定在97%以上。这个速度对入门项目很友好不用长时间等结果。学习率用固定的1e-3就行如果还想再压榨一点精度可以在第12轮后手动降到5e-4继续跑几轮。这里再多说一句归一化。我用的mean和std是ImageNet预训练模型的统计值很多教程在自建数据集时也会照搬实际用下来效果不错。它的作用是把输入像素分布拉到接近零均值和单位方差让模型在反向传播时梯度更稳定。如果你看到loss一直震荡不下降先检查这一步有没有做。4.2 推理脚本与效果验证训练完成后关键是能拿一张新图预测出字符。推理脚本要做的事和训练时序保持完全一致读取图片、Resize到160x60、转Tensor、归一化再喂给模型。最容易出的错就是训练时先Resize再Normalize推理时却忘了同一个transform导致像素范围不一致模型输出结果直接崩掉。import torch from torchvision import transforms from PIL import Image from model import CaptchaCNN char_set 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ device torch.device(cuda if torch.cuda.is_available() else cpu) transform transforms.Compose([ transforms.Resize((160, 60)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) model CaptchaCNN().to(device) model.load_state_dict(torch.load(captcha_cnn.pt, map_locationdevice)) model.eval() img Image.open(test.png).convert(RGB) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): pred_idx model(x)[0].argmax(dim-1).tolist() pred_label .join(char_set[i] for i in pred_idx) print(识别结果:, pred_label)如果预测结果不对先检查三个点图片预处理和训练时是否一致字符集映射顺序是否变动模型加载后有没有调用eval()。eval()会关闭Dropout和BatchNorm的统计更新如果你漏了这一步推理结果会带随机性同一张图可能每次预测不同这个问题我在实际调试里遇到过很多回。设置好这几个细节后整个推理流程基本可以稳定复现训练时的准确率表现。5. 常见问题与排查技巧实录5.1 训练中高频踩坑点第一个问题是loss不下降。如果你把学习率设到1e-1大概率会看到loss上下乱跳这是典型的步长过大。改成1e-3甚至1e-4就好新手建议直接从1e-3起步。另一个常见原因是没做归一化直接把0到255的像素值喂进网络反向传播中梯度容易不稳定这个坑出现频率非常高所以我在数据处理里会强制保留ToTensor加Normalize。第二个问题是过拟合。表现是训练集准确率很高验证集始终差一截。我早期做这个项目时把Dropout去掉结果训练准确率99%验证集只有88%。把Dropout加回0.5后验证集很快爬到96%以上。如果还不够可以生成更多训练数据或者把干扰线强度加大让模型被迫学习更本质的特征而不是死记硬背图像细节。第三个问题是预测单张图全错。大部分情况不是模型坏了而是预处理和训练不一致最常见的是训练用RGB三通道推理时转成灰度图再归一化通道数都对不上。这个项目我在推理脚本里特意保留了RGB转换建议你从一开始就固定一套transform模板训练和推理共用不要各自写一份。5.2 易混淆字符与数据分布验证码字符集中最容易混淆的是0和O、1和I、2和Z这类形状接近的字符。如果直接用全字符集训练模型会在这些字符上互相打架。针对入门项目有两条路可选一是直接从字符集里去掉这些易混淆项二是在生成数据时给它们增加比例。实际做下来去掉易混淆项最省事准确率能凭空调高一点。等到你后续有余力了再去挑战更难的字符集也不迟。还有一个容易被忽视的点是样本均衡。如果char_set是36个字符生成数据时用random.choice均匀抽样那问题不大。一旦你手工调整了抽样权重记得检查每个字符的样本数分布别让模型对低频字符产生系统性偏见。分类模型对样本量少的类别很容易欠拟合这在验证码这种多类别任务里会直接影响最终整图准确率。我把排查问题整理成了速查表方便你直接对照定位。现象可能原因解决办法Loss不下降学习率过大 / 未归一化调低学习率用ToTensor加Normalize训练准验证差过拟合加Dropout、增大数据量准确率卡死字符不均衡 / 易混淆字符多去掉O、I等字符均衡抽样推理单图全错预处理不一致 / 模型未eval统一transform加model.eval()训练很慢无GPU调小batch_size或图片尺寸标签解析报错文件名格式不一致统一命名“编号_标签.png”最后再说两句个人体会。这个项目的价值不在“识别验证码”本身而在于它把深度学习中最高频的几个环节全部过了一遍你亲手造了数据定义了网络跑了反向传播调了损失函数最后还能把训练好的模型应用在一张真实感很强的图片上。这一套流程跑通之后再去做图像分类、目标检测这些经典方向你会发现自己对“模型在干什么”有了完全不同的理解。一个小技巧分享给准备扩展的人如果之后想挑战更复杂的数据可以在生成阶段叠加旋转、仿射变换、颜色抖动模型对形变的抗性会明显增强。最后再强调一点这篇里所有代码都用于学习和实验请别拿它去绕开任何平台的验证码机制技术圈里这点共识要记牢。本文还有配套的精品资源点击获取