news 2026/9/7 8:56:28

深度学习入门实战:基于CNN的验证码识别完整项目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深度学习入门实战:基于CNN的验证码识别完整项目

简介:一套完整的字符型图片数字验证码识别项目资料,基于深度学习技术实现,面向正在学习图像识别、神经网络或网络安全反自动化攻防的开发者。内容覆盖验证码数据集构建、图像预处理、卷积神经网络与循环神经网络模型搭建、训练评估及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就是一个老牌开源引擎,对于规则、无干扰的字体准确率并不低。但问题是真实验证码几乎都带扭曲、干扰线、噪点、颜色噪块,传统方法需要做大量预处理,比如去噪、二值化、字符切割,每一步都会有误差累计,最后效果很难稳定下来。

深度学习方案的核心优势是端到端学习。输入一张原始图片,卷积网络自动提取边缘、纹理、字符形状等特征,不需要手动设计特征工程。而且它天然适应字符粘连的情况,因为你可以不进行物理切割,直接用整张图参与训练。我做了一个简单对比,可以直观看到两者的区别。

对比维度传统OCR(Tesseract等)深度学习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_v=0, max_v=255): 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)), fill=random_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], fill=random_color(0, 255), width=2) 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, font=font, fill=random_color(50, 200)) img = img.filter(ImageFilter.GaussianBlur(radius=0.5)) return img, label if __name__ == "__main__": os.makedirs("data/train", exist_ok=True) os.makedirs("data/val", exist_ok=True) for i in range(20000): img, label = generate_one() img.save(f"data/train/{i:05d}_{label}.png") for i in range(2000): img, label = generate_one() img.save(f"data/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_chars=36, max_len=4): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Linear(128 * 20 * 7, 512), nn.ReLU(inplace=True), 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/2=20。所以展平后的特征维度是128207=17920,这就是第一个全连接层输入维度的由来。如果你改了图片尺寸,这里的数字一定要同步更新,否则模型会直接报维度不匹配的错误。

为什么每个卷积层后面都加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, transform=None): 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, dtype=torch.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(), lr=1e-3) train_ds = CaptchaDataset("data/train", transform=transform) val_ds = CaptchaDataset("data/val", transform=transform) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False) 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(dim=1).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(f"epoch {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_location=device)) 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”

最后再说两句个人体会。这个项目的价值不在“识别验证码”本身,而在于它把深度学习中最高频的几个环节全部过了一遍:你亲手造了数据,定义了网络,跑了反向传播,调了损失函数,最后还能把训练好的模型应用在一张真实感很强的图片上。这一套流程跑通之后,再去做图像分类、目标检测这些经典方向,你会发现自己对“模型在干什么”有了完全不同的理解。

一个小技巧分享给准备扩展的人:如果之后想挑战更复杂的数据,可以在生成阶段叠加旋转、仿射变换、颜色抖动,模型对形变的抗性会明显增强。最后再强调一点,这篇里所有代码都用于学习和实验,请别拿它去绕开任何平台的验证码机制,技术圈里这点共识要记牢。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:50:33

YOLO+多模态LLM:智慧交通监控预警系统实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:49:54

MSP430与LMP90100 SPI驱动开发实战:多通道ADC采集与移植经验

简介:面向嵌入式开发者,这是TI官方的MSP430与LMP90100传感器AFE接口代码库及说明文档,用于解决高精度传感器信号链中原型搭建、初始化配置与数据读取等常见问题,尤其适合需要快速评估LMP90100性能的工程师。资源包共37个文件&…

作者头像 李华
网站建设 2026/9/7 8:49:42

Linux用户空间驱动DS1302 RTC实战:GPIO模拟时序与系统时间同步

简介:面向Linux驱动开发者,资源提供DS1302实时时钟芯片的完整驱动源码与测试程序。驱动覆盖设备树配置、I2C/SPI接口适配、BCD时间格式转换、内核timekeeper同步、掉电保护处理,以及用户空间/dev/rtc*设备节点访问等关键环节;配套…

作者头像 李华