ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于CNN的验证码识别:从数据生成到端到端模型训练与部署

基于CNN的验证码识别:从数据生成到端到端模型训练与部署 简介这是一套基于CNN神经网络的验证码识别项目完整源码与文档面向需要完成毕业设计、期末大作业或课程设计的学生也适合对深度学习图像识别感兴趣的初学者参考。项目采用“端到端”训练思路涵盖数据集生成、模型构建、训练、预测与测试全流程纯数字识别率可达99.99%代码附有注释便于理解与二次开发。资源包共50个文件以Python脚本为核心包含captcha_gen、captcha_train、captcha_predict等模块另含40张示例图片、README说明文档及配置文件整体仅592KB轻量易部署。已有115人学习浏览具备一定参考热度。下载后可直接运行便于快速体验CNN在验证码识别上的完整落地流程也能帮助梳理从数据预处理到模型评估的工程实现思路适合作为高分项目模板或课设演示基础。1. 被验证码卡住之前先看懂这套 CNN 识别方案验证码是爬虫、自动化脚本和课程设计里绕不过去的一道坎。市面上大部分方案要么依赖收费接口要么用传统图像处理去分割字符遇到带干扰线、噪点、扭曲的验证码就翻车。这个项目不一样的地方在于它把「识别验证码」这件事简化成了一条端到端的 CNN 神经网络流水线——从生成训练数据、定义卷积模型、训练参数到输出预测结果全部代码都摊开在你面前纯数字验证码识别率能跑到 99.99%。新手拿到手能跟着跑通全流程正在做毕业设计或期末大作业的人可以直接拿它做项目骨架。适合四类人想搞懂 CNN 图像识别原理的学生、需要快速完成课程设计的开发者、被验证码卡住自动化的脚本党以及想扩展成字母数字混合识别的研究者。2. 数据先行造验证码图片到 dataset 的完整流程2.1 验证码生成器先解决「没有数据」的问题训练 CNN 模型需要一个数量足够大的带标签数据集。真实网站验证码收集起来慢而且标注成本高项目里用了一个更聪明的做法——先写一个验证码生成器让机器自己造数。打开captcha_gen.py核心逻辑是用 Python 的 captcha 库随机生成指定长度的验证码图片同时记录下图片里实际的字符序列作为标签。这样你可以轻松生成几万张图片数量完全由自己控制。from captcha.image import ImageCaptcha import random import string # 定义验证码字符集纯数字 CHAR_SET string.digits # 验证码长度 CAPTCHA_LEN 4 def generate_captcha(num1000, output_dir./train_images/): image ImageCaptcha(width160, height60, fonts[./font/Arial.ttf]) for i in range(num): # 随机生成一个长度为4的数字串作为标签 label .join(random.choices(CHAR_SET, kCAPTCHA_LEN)) # 生成图片并保存 image.write(label, f{output_dir}{label}_{i}.png) print(f已生成 {num} 张验证码图片)这里width160, height60是验证码图片的固定尺寸CNN 输入层的尺寸必须和它保持一致。label_{i}.png的命名方式把真实标签写在文件名里后面my_dataset.py读取时就不需要额外维护一份标注文件。实际训练时建议把图片数量放大到 1 万张以上数据量越大模型的泛化能力越强。2.2 数据集构建从图片文件夹到内存张量图片生成之后需要一个脚本把所有图片读进来、做预处理、整理成 PyTorch 的 Dataset 对象。这个任务由my_dataset.py完成。它的工作流程分三步扫描文件夹里的所有 png 图片从文件名里解析出真实标签数字串把每张图片转成灰度图并缩放到统一尺寸转为 numpy 数组最后把标签也转成 numpy 数组和图片数据一一对应。import os import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class CaptchaDataset(Dataset): def __init__(self, img_dir, transformNone): self.img_dir img_dir self.img_paths [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.endswith(.png)] self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] # 从文件名解析标签例如 1234_0.png - 1234 label_str os.path.basename(img_path).split(_)[0] # 转灰度、缩放到 60x160 img Image.open(img_path).convert(L).resize((160, 60)) img_array np.array(img, dtypenp.float32) / 255.0 # 由字符串标签转成数字列表 label_array np.array([int(c) for c in label_str], dtypenp.int64) return torch.tensor(img_array).unsqueeze(0), torch.tensor(label_array)这段代码的关键点在于convert(L)做灰度化把三通道彩色图压成单通道直接减少模型需要学习的参数总量。/ 255.0归一化把像素值压缩到 0~1 区间让梯度下降过程更稳定。unsqueeze(0)把形状从(60, 160)变成(1, 60, 160)因为 PyTorch 卷积层默认接受(batch, channel, height, width)四维输入。注意这里的 resize 尺寸一定要和生成器、配置文件的尺寸对齐否则训练时会直接报维度不匹配错误。2.3 配置与 one-hot 编码定长假设下的维度设计打开captcha_setting.py里面只定义了几个全局常量但整个项目的维度设计都从这里展开。通常包含CAPTCHA_LEN4和CHAR_SET_LEN10两个关键值一个代表验证码长度一个代表字符集大小。CNN 模型最后的全连接输出维度 验证码长度 × 字符集大小也就是 4×1040 个节点。这个设计的含义是模型不直接输出一个 4 位数字而是为每一位输出 10 个类别的置信度分数。one_hot_encoding.py负责把标签转成 one-hot 形式。训练时计算的损失是「把 4 个位置的预测结果分别和真实字符的 one-hot 向量做交叉熵」。这个定长假设是项目的核心约束模型输入的验证码必须是固定 4 位长度变了输出维度就要重新设计。训练数据、模型结构、损失函数三者的维度都围绕这个假设对齐。def one_hot_encode(label_list, num_classes10): # label_list: [3, 5, 7, 2] one_hot np.zeros((len(label_list), num_classes)) for i, label in enumerate(label_list): one_hot[i, label] 1 return one_hot这种编码方式比直接输出单个数字值更适合分类任务。数字 3 和数字 8 之间的差异不是数值上差 5而是两类像素模式的本质差异交叉熵损失能更好地刻画这种离散类别关系。3. 模型选型与训练把网络结构落实成可跑的脚本3.1 网络结构小卷积核叠加比大卷积核更高效captcha_cnn_model.py里定义的模型是典型的 CNN 分类器结构上遵循「卷积提取特征 → 池化压缩尺寸 → 全连接分类」的经典路线。验证码图片是 60×160 的灰度图字符边缘、笔画走向是最重要的判别特征卷积层的局部感受野正好适合捕捉这类空间局部模式。import torch.nn as nn class CaptchaCNN(nn.Module): def __init__(self, captcha_len4, char_set_len10): super(CaptchaCNN, self).__init__() self.conv1 nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2) # 60x160 - 30x80 ) self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2) # 30x80 - 15x40 ) self.conv3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2) # 15x40 - 7x20 ) self.fc nn.Sequential( nn.Linear(128 * 7 * 20, 1024), nn.ReLU(), nn.Dropout(0.3), nn.Linear(1024, captcha_len * char_set_len) # 40 ) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x x.view(x.size(0), -1) # 展平 x self.fc(x) return x两个细节值得说明。一是全部用 3×3 小卷积核两层 3×3 堆叠等效于一层 5×5 的感受野但参数量更少、非线性更强这是近代 CNN 设计的通行做法。二是只在最后一个全连接层前加了 Dropout比例 0.3防止模型死记训练图片。forward里的view操作把三维特征图展平成向量这是卷积层过渡到全连接层的必经一步。每一层池化后特征图尺寸如何变化注释里都标了调结构时可以按这个规律推算后面全连接的输入维度。3.2 训练脚本损失函数、优化器与训练循环captcha_train.py把数据加载、模型初始化、训练循环全部串起来。训练过程里有两个重要细节图片数据在每次迭代时要做随机打乱防止模型学到数据顺序损失函数用交叉熵优化器用 Adam。Adam 在验证码这种中小规模数据集上收敛速度明显优于 SGD初始学习率 0.001 是一个经过大量项目检验的安全起点。import torch.optim as optim from torch.utils.data import DataLoader, random_split # 创建 dataset 并按 9:1 划分训练集和验证集 dataset CaptchaDataset(./train_images/) train_size int(len(dataset) * 0.9) val_size len(dataset) - train_size train_dataset, val_dataset random_split(dataset, [train_size, val_size]) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) model CaptchaCNN() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 30 for epoch in range(epochs): model.train() total_loss 0 for imgs, labels in train_loader: optimizer.zero_grad() output model(imgs) # (batch, 40) # 把 4 位预测拆开分别参与损失计算 loss 0 for i in range(4): loss criterion(output[:, i*10:(i1)*10], labels[:, i]) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})代码里最核心的一步是损失计算方式把 40 维输出切成 4 段每段 10 维分别和标签的每一位做交叉熵。理论上也可以把输出 reshape 成(batch, 4, 10)整体算损失两者数学上是等价的但逐位累加更容易定位是哪一位字符训练不到位。batch_size 在 32~128 之间都合理太大会导致显存溢出太小则梯度抖动剧烈。3.3 端到端识别为什么不做字符分割传统的验证码识别思路是先做字符分割把 4 个字符切分成 4 张单字符图片再用 CNN 或模板匹配分别识别。这个思路在验证码没有干扰线、字符间距合理的时候效果还行一旦遇到字符重叠、旋转、粘连分割本身就是一道送命题——切错一个字符后面全错。这个项目走的是端到端路线输入一整张验证码图片输出直接是整个 4 位数字串中间没有显式分割。端到端方案的本质是让卷积层自己去「隐式地」学会定位每个字符的位置。浅层卷积看到的是笔画和边缘深层卷积通过更大的感受野把多个笔画组织成完整的数字形状。省掉分割步骤既避免了误差累积也让整个流程变得非常简洁。这是这个项目能在 99.99% 准确率附近徘徊的关键设计决策。4. 落地推理与测试让模型读出一张验证码的数字4.1 预测脚本预处理、模型加载与 argmax 解码训练完成的模型最终要用在真实的验证码图片上。captcha_predict.py做的事情是把输入图片做和训练数据完全一致的预处理加载保存好的 model 权重前向推理拿到 40 维输出再按位置解码成 4 位数字字符串。项目根目录下的number.png和number2.png就是用来验证这个脚本的样例图。import torch from PIL import Image import numpy as np def predict_single(model, img_path): img Image.open(img_path).convert(L).resize((160, 60)) img_array np.array(img, dtypenp.float32) / 255.0 x torch.tensor(img_array).unsqueeze(0).unsqueeze(0) # 推理模式关闭 dropout model.eval() with torch.no_grad(): output model(x) # shape: (1, 40) output output.view(-1, 10) # shape: (4, 10) pred torch.argmax(output, dim1) return .join([str(p.item()) for p in pred]) result predict_single(model, number.png) print(f识别结果: {result})这段代码有几个细节值得注意。model.eval()必须调用否则训练阶段开启的 Dropout 会在推理时继续随机丢弃神经元同样的输入每次输出都可能不同。torch.no_grad()关闭梯度计算图推理时不需要反向传播显存占用和耗时都会明显下降。view(-1, 10)把输出拆成 4 组每组 10 个置信度分数argmax取每组里分数最高的类别索引就是这一位最可能的字符。4.2 测试脚本批量评估模型真实水平captcha_test.py解决的是「这个模型到底行不行」的问题。单张图片的识别结果存在偶然性可能是运气好猜对了。批量测试的思路是取一批没有参与训练的验证码图片或者直接现场生成一批新的逐张调用预测函数统计识别正确的图片占比。def evaluate(model, test_dir, total1000): correct 0 files [os.path.join(test_dir, f) for f in os.listdir(test_dir) if f.endswith(.png)][:total] for img_path in files: label os.path.basename(img_path).split(_)[0] pred predict_single(model, img_path) if pred label: correct 1 print(f准确率: {correct/len(files)*100:.2f}%) evaluate(model, ./test_images/)这里的 test 集和 train 集必须严格分开否则测出来的准确率虚高。一个更严格的测试方法是直接用captcha_gen.py现场生成全新图片因为模型从未见过这些数据评估结果更接近真实上线水平。建议测试集图片里混合几种不同字体的验证码这样才能暴露模型是否存在过拟合某种字体风格的问题。5. 避坑与排查五个真实问题背后的现象、原因与解决5.1 训练损失不降反升现象训练循环跑了好几个 epochloss 一直在原地波动甚至比第一个 epoch 还高。原因最常见的是学习率设置过高Adam 优化器在 lr0.01 以上时很容易在损失平面震荡跨过了最优点。另一个可能原因是数据没做归一化像素值保持在 0~255 范围梯度量级偏大。解决先把学习率降到 0.001 或 0.0005确认数据 pipeline 里有没有astype(np.float32) / 255.0这一步。如果仍然不降检查一下CaptchaDataset返回的 tensor 形状是不是(1, 60, 160)形状不对卷积层根本不会报错但输出会变成噪声。5.2 模型把任何输入都预测成同一个数字串现象拿不同的验证码图片测试输出结果永远是同一组字符。原因模型陷入了局部最优全连接层的偏置主导了整个输出所有类别的 logits 都被某个固定模式压住了。常见于训练数据量太小、模型复杂度偏高、字符类别极其不平衡的场景。解决先检查训练集里 0~9 十个数字的分布是否均匀。然后把 Dropout 比例从 0.3 调到 0.5多丢弃一些神经元强制模型学习冗余特征。最后把数据量翻倍——5000 张以下的数据量出这种问题的概率非常高生成一万张重训基本能解决。5.3 训练集准确率高但测试集准确率低现象训练最后几个 epoch训练集损失很低、准确率接近 100%但是测试集准确率只有 80% 左右。原因过拟合。模型记住了训练图片的背景纹理、噪点位置甚至字体间距而不是学习字符本身的形状。解决一方面增加数据量最直接的方法是修改captcha_gen.py的字体列表混入 3~4 种不同字体模型对不同书写风格的适应能力会明显增强。另一方面加入数据增强——随机平移几个像素、轻微旋转 5 度以内、调整对比度都是对验证码场景友好的增强策略。5.4 验证码识别对干扰线特别敏感现象测试自己生成的验证码时准确率很高但换到有大量弧线干扰的验证码图片上准确率断崖式下跌。原因训练数据里干扰线的形态太单一模型可能把「干扰线附近的特定像素模式」当成了字符特征的一部分没有真正学到字符的骨架结构。解决改captcha_gen.py加上多条干扰线、随机噪点、字符颜色随机化等参数。注意生成的图片里干扰线不要每次都画在相同位置随机变化才能迫使模型学会忽略这些噪声。5.5 验证码长度不固定导致模型结构失效现象遇到 5 位或 6 位的验证码模型直接报维度错误。原因模型输出维度固定为captcha_len × char_set_len长度变了维度就对不上。解决要么预处理时把非 4 位验证码全部截断或补齐到 4 位这是很多生产系统的实际妥协方案要么改captcha_setting.py的CAPTCHA_LEN并让最后一层全连接输出维度随之变化。改完后需要重新训练模型原有权重无法复用。6. 从纯数字到字母数字混合顺着三条线改造整个项目如果想验证自己是不是真读懂了这套代码推荐做一个改造实验把纯数字识别改成字母数字混合识别。这个改动看起来复杂实际上只需要沿着三条线推进。第一条线是字符集在captcha_gen.py里把CHAR_SET从string.digits换成string.digits string.ascii_uppercase生成器马上就产出包含大写字母的验证码图片。第二条线是配置captcha_setting.py里CHAR_SET_LEN从 10 改成 36。第三条线不需要动——CaptchaCNN的最后一层输出维度是从captcha_len * char_set_len算出来的只要你改了char_set_len的值模型在实例化时自动适配新的输出维度。# captcha_setting.py 改动示例 CHAR_SET_LEN 36 # 数字10个 大写字母26个然后需要同步修改两处数据处理逻辑my_dataset.py解析标签时int(c)遇到字母A会直接抛异常正确做法是建立一张「字符 → 索引」的映射表captcha_predict.py解码时要做反向映射把索引 10 翻译成字符A。这两处是新手最容易遗漏的坑。整个改造跑通之后你会对「输入维度对齐」这个概念有更立体的理解——模型结构、数据标签、预测解码三者的字符集字典必须始终一致任何一处独立改动都会让训练或推理报错。我自己做这个实验时吃的亏很有代表性只改了生成器和配置没改标签映射训练到第八个 epoch 突然 Loss 变成 NaN。排查了半天才发现是int(A)抛异常导致整个 batch 数据错位。从那以后我每次改字符集都强制自己先跑一次「生成 100 张图 → 加载 dataset → 打印一条样本的输入输出形状」的冒烟测试确认全链路维度一致再进入完整训练。这个习惯帮我省下了大量排错时间也希望帮到你。本文还有配套的精品资源点击获取
返回列表