简介:这份下载包是一套基于CNN神经网络识别各类验证码的Python项目源码与文档说明,面向深度学习初学者及毕业设计、课程设计开发者,可用于快速搭建可运行的验证码识别基线方案。项目采用端到端模型训练思路,涵盖验证码图片生成、数据预处理、模型训练、预测与测试等完整流程,纯数字场景识别率高达99.99%,代码注释清楚,上手门槛较低。
压缩包共50个文件,大小约592KB,主要包含8个Python脚本、40张PNG样例图片、1份Markdown说明文档及gitignore配置;脚本覆盖训练与推理逻辑,样例图片便于核对数据效果,说明文档有助于理解项目结构。目录划分清晰,便于按模块检索学习。
目前已有115人学习/下载。整体功能完整、部署简单,可作为课程设计、期末大作业与毕业设计的高分参考,也能帮助开发者理解CNN在验证码识别中的实际应用。
1. 用 Python+CNN 识别验证码:这项目值不值得做、卡点在哪、适合谁
做爬虫数据采集、自动化测试或者毕业设计的人,大概率都撞上过验证码这道墙:字符歪歪扭扭、背景全是干扰线,传统方法识别率奇低。这个标题把方案写得很直白——用 Python 搭一个 CNN 卷积神经网络来识别各种验证码,配套项目源码和文档说明。我第一反应是:这活儿核心难点根本不在网络结构,而在数据和标签上,模型反而不是最大的坑。这个方向适合三类人:拿它做课设毕设的学生、写自动化脚本时被验证码卡住的工程师、想入门深度学习图像识别但又不想一上来就啃 ImageNet 的新手。下面按我自己的落地经验,把从零到能跑通预测的完整路径和踩过的坑讲清楚。
2. 识别方案选型:为什么是 CNN 而不是模板匹配或传统 OCR
2.1 硬刚验证码的路线演变:模板匹配、传统 OCR 到深度 CNN
早些年处理验证码,最通用的做法是模板匹配:先把图片二值化,把字符一个个切出来,再和预先准备好的字符模板做像素比对。这套路在字体规整、没有干扰的验证码上确实能跑,但遇到扭曲字体、字符粘连、背景噪声稍微上来一点就翻车,因为模板匹配本质上没有泛化能力,换一个字体就得重新做整套模板。
传统 OCR 方案是另一个方向,很多老项目用 Tesseract 这类识别引擎,Python 里有 pytesseract 封装,PHP 后端也有对应的 OCR 库能处理验证码。Tesseract 本身是为印刷体文字识别设计的,它在干净文档上的识别率非常高,但验证码的对抗思路恰好是让机器认不出——扭曲、叠加噪声、加干扰线、字符错位,这些操作会让 Tesseract 的识别率直接崩到没法用的程度,而且它依赖前置二值化质量,阈值调不好后面全白搭。
再往前推一步,有人试过前馈神经网络,把整张图的所有像素摊平喂给全连接网络(BP 神经网络)。问题很明显:一张 160×60 的图就有 9600 个输入维度,全连接层的参数数量爆炸,训练慢还容易过拟合;更致命的是它没有平移不变性,同一个字符在图上往左挪两个像素,网络可能就不认得了。验证码里字符位置本来就是随机的,这条路也走不通。
CNN 卷积神经网络解决的关键问题就是前面这些方案的死穴:卷积核在图像上滑动,权重共享,参数量大幅下降;局部感受野让它对字符位置不那么敏感;多层卷积堆叠后,浅层提取边缘和颜色,深层提取笔画特征和部件结构,最后用全连接层做组合分类。验证码这种字符任意排列、画风扭曲的图像,正是 CNN 最舒服的场景。现在商业验证码识别服务底层的模型,无一例外都是 CNN 系列结构。
2.2 训练数据从哪来:用 captcha 库自造仿真集与真实样本的取舍
做 CNN 第一步不是搭模型,是搞数据。对验证码识别来说,最省事的方案是先用 Python 的 captcha 库批量生成仿真图片。这个库可以自己控制字符集、图片尺寸、字体和干扰模式,几分钟就能生成几千张带标签的图,起步阶段完全够用。
from captcha.image import ImageCaptcha import random import os char_set = "abcdefghijklmnopqrstuvwxyz0123456789" image = ImageCaptcha(width=160, height=60) os.makedirs("train_data", exist_ok=True) for i in range(5000): text = "".join(random.choices(char_set, k=4)) image.write(text, f"train_data/{i}_{text}.png") # 标签直接写在文件名里,便于后续解析这段代码的逻辑很简单:字符集定义了模型要学的类别,随机取 4 个字符拼成标签,生成图片保存到本地。文件名里同时带上序号和标签,是为了后面写数据加载器时方便直接解析 label,不用额外维护一个映射表。参数上 width=160、height=60 是常见配置,这个尺寸对 4 字符验证码来说字符间距足够、也不会让图片过大拖慢训练。
仿真数据有个天然隐患:captcha 库生成的图片和线上真实验证码的风格差距可能非常大。之前见过一个项目,在自造数据集上识别率 99%,换到真实站点验证码上直接跌到 70%,问题就出在字体和干扰模式对不上。如果要上线生产,通常要走另一条路——把真实验证码图片批量抓下来,先让粗模型识别一遍,再人工纠错,形成一套带标签的真实样本库。这是个脏活累活,但分布真实,是模型能落地的关键。
2.3 定长验证码的 CNN 结构:一张参数表定方案
字符验证码有个特点:位数通常固定,比如 4 位或 6 位。这可以把「识别一整串字符」拆成「4 个独立的多分类问题」,每个位置预测 36 类中的一个(26 个小写字母加 10 个数字)。这个设计决策直接影响输出层结构。
下面是一套在定长 4 字符验证码上稳定有效的 CNN 结构,输入统一 resize 成 160×60×3:
| 层 | 参数 | 输出尺寸 | 作用 |
|---|---|---|---|
| Conv1 | 32 个 3×3 卷积核,padding=same,ReLU | 160×60×32 | 提取边缘、纹理 |
| MaxPool1 | 2×2,stride=2 | 80×30×32 | 降维,增强平移不变性 |
| Conv2 | 64 个 3×3 卷积核,padding=same,ReLU | 80×30×64 | 提取笔画组合 |
| MaxPool2 | 2×2,stride=2 | 40×15×64 | 降维 |
| Conv3 | 128 个 3×3 卷积核,padding=same,ReLU | 40×15×128 | 提取字符部件级特征 |
| MaxPool3 | 2×2,stride=2 | 20×7×128 | 降维 |
| Flatten | 展平 | 17920 | 接全连接 |
| Dense | 512 神经元,ReLU,Dropout=0.5 | 512 | 组合特征 |
| Dense | 144 神经元,Softmax | 144 | 4 字符 × 36 类 |
卷积核数量每层翻倍是约定俗成的做法:浅层特征少,32 个够用;深层语义复杂,需要更多卷积核表达。MaxPooling 选 2×2 stride=2 是默认配置,图缩小一半的同时不太损失关键信息,还顺手防过拟合。最后一层输出 144 而不是 36,是因为要同时预测 4 个位置,每个位置 36 类,全连接层直接把四个位置的概率一次性输出,预测时 reshape 成 (4, 36) 再分别取 argmax 就行。
这套网络的总参数量在百万量级,对验证码这种小图任务绰绰有余。实际训练时如果发现过拟合,优先从 Dropout 和训练数据量入手;如果欠拟合,才考虑加一层卷积或增大卷积核数量。
3. 从造数据到训模型:用 Keras 跑通验证码识别的完整流程
3.1 环境准备:最小依赖清单与安装命令
这个项目的依赖不算多,核心就四样:TensorFlow/Keras、captcha、Pillow、NumPy。Anaconda 环境照下面装就行:
conda create -n captcha_env python=3.9 -y conda activate captcha_env pip install tensorflow captcha pillow numpy如果你有 NVIDIA 显卡,装 TensorFlow 的时候留意下版本匹配,GPU 版识别验证码这种小图任务属于杀鸡用牛刀,但训练速度确实快很多。没有 GPU 完全不用担心,160×60 的输入分辨率很低,几千张训练图在 CPU 上一个 epoch 也就几十秒,整个训练过程十几分钟能跑完。我第一次跑这类任务的时候在 GPU 环境配置上折腾了一下午,后来发现这活儿纯 CPU 也完全能扛,机器上有啥用啥,别为了一个小验证码项目去配 CUDA 环境,那个坑更深。
3.2 准备训练数据:批量生成图片并解析标签
数据准备阶段要明确三件事:图片尺寸要统一、标签要能解析、训练集和验证集要分开。下面这段脚本把生成和解析两个逻辑都做掉了:
import os import numpy as np from PIL import Image from sklearn.model_selection import train_test_split IMAGE_WIDTH, IMAGE_HEIGHT = 160, 60 CHAR_SET = "abcdefghijklmnopqrstuvwxyz0123456789" CHAR_TO_IDX = {c: i for i, c in enumerate(CHAR_SET)} NUM_CLASSES = len(CHAR_SET) def load_data(data_dir): images, labels = [], [] for fname in os.listdir(data_dir): if not fname.endswith(".png"): continue img = Image.open(os.path.join(data_dir, fname)).convert("RGB") img = img.resize((IMAGE_WIDTH, IMAGE_HEIGHT)) images.append(np.array(img, dtype=np.float32) / 255.0) text = fname.split("_")[1].split(".")[0] # 从文件名解析出验证码内容 label = np.zeros((4, NUM_CLASSES), dtype=np.float32) for i, ch in enumerate(text): label[i, CHAR_TO_IDX[ch]] = 1.0 labels.append(label) return np.array(images), np.array(labels) images, labels = load_data("train_data") x_train, x_val, y_train, y_val = train_test_split( images, labels, test_size=0.2, random_state=42 ) print(x_train.shape, x_val.shape)这里有两个容易漏的细节。第一,图片读进来后必须统一 resize,并且转成 float32 除以 255 归一化,不做这步会导致训练时 loss 下降非常慢甚至直接发散。第二,标签维度是 (4, 36),每个字符位置独立做 one-hot,而不是把整串字符当成一个完整标签,这是定长验证码识别和普通图片分类的核心差异。train_test_split 里 test_size=0.2,20% 的数据留作验证集,random_state 固定住让每次切分结果一致,方便复现实验。
3.3 定义 CNN 模型:Keras Sequential 与关键参数
模型定义用 Keras 的 Sequential API,代码足够短也足够直观,每个卷积块的结构保持「卷积 + 池化」的固定节奏:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout model = Sequential([ Conv2D(32, (3, 3), padding="same", activation="relu", input_shape=(IMAGE_HEIGHT, IMAGE_WIDTH, 3)), MaxPooling2D(pool_size=(2, 2)), Conv2D(64, (3, 3), padding="same", activation="relu"), MaxPooling2D(pool_size=(2, 2)), Conv2D(128, (3, 3), padding="same", activation="relu"), MaxPooling2D(pool_size=(2, 2)), Flatten(), Dense(512, activation="relu"), Dropout(0.5), Dense(4 * NUM_CLASSES, activation="softmax") ]) model.compile( optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"] ) model.summary()注意 input_shape 的写法:前面 load_data 返回的图片数组是 (样本数, 高, 宽, 通道),所以这里第三维是通道数 3。如果你的数据读进来是灰度图,通道数就得改成 1,卷积第一层的 input_shape 也要跟着调。这里用 RGB 三通道是想多留一些字体颜色的信息,对识别率有一点帮助,代价是模型计算量变大,验证码任务完全可接受。
关于一维卷积:这个项目里图片是二维空间信息,必须用 Conv2D。一维卷积更多用在时间序列或者纯横向信号上,比如把一个字符的宽度方向像素当成序列去处理,那种情况才需要考虑 Conv1D,这里不需要。最后输出 144 个节点对应 4×36,softmax 会在整个 144 维上做归一化,预测时再按位置拆开取 argmax。
3.4 训练与模型保存:早停、学习率衰减和预测函数
训练阶段的配置直接决定你能不能把模型调到可用状态,核心是三个回调:EarlyStopping 防过拟合、ReduceLROnPlateau 自动降学习率、ModelCheckpoint 保存最优权重:
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks = [ EarlyStopping(monitor="val_loss", patience=6, restore_best_weights=True), ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=3, min_lr=1e-6), ModelCheckpoint("captcha_best.keras", monitor="val_accuracy", save_best_only=True) ] history = model.fit( x_train, y_train, batch_size=64, epochs=50, validation_data=(x_val, y_val), callbacks=callbacks, verbose=1 )几个参数的实际经验值:batch_size 取 64,验证码图片小、数据量不大,64 是平衡训练速度和梯度稳定性的点;epochs 设 50 但绝大多数情况用不到这么多次,EarlyStopping 会在验证集 loss 连续 6 个 epoch 不降时自动停;patience=3 的学习率衰减意味着连续 3 个 epoch 没进步就把学习率减半,从 1e-3 往 1e-4、1e-5 逐级降,这是我调 CNN 时最有用的一个回调,它能把卡在局部最优的模型救回来。模型文件保存成 .keras 格式,这是新版 Keras 的默认格式,跨环境加载兼容性比旧版 .h5 省心。
训练完还得写预测函数,否则模型训好没地方用:
def predict_captcha(model, image_path): img = Image.open(image_path).convert("RGB").resize((IMAGE_WIDTH, IMAGE_HEIGHT)) x = np.array(img, dtype=np.float32) / 255.0 x = x[np.newaxis, ...] # 增加 batch 维度 pred = model.predict(x, verbose=0)[0] # 得到 144 维向量 pred = pred.reshape(4, NUM_CLASSES) text = "".join(CHAR_SET[np.argmax(pred[i])] for i in range(4)) return text print(predict_captcha(model, "test_demo.png"))预测时最容易忘记的是x[np.newaxis, ...]——模型训练时的输入是带 batch 维度的,单张图预测必须手动补上这个维度,否则 Keras 会报维度不匹配的错。reshape 成 (4, 36) 后对每个位置单独取 argmax,再映射回字符,这四个字符拼接起来就是最终预测结果。整套流程走下来,在自造数据上准确率到 95% 以上是基本操作,如果你发现远低于这个数,问题大概率出在数据加载或标签制作上。
4. 验证码识别常见问题与避坑排查:5 条血泪踩坑记录
4.1 数据与预处理侧:标签错位、通道不一致、相似字符混战
现象:训练 loss 一直在 7 以上降不下去,但肉眼检查训练数据感觉图片画得挺正常,模型预测出来全是乱码。
原因:标签和图片对应关系错了。最常见的是文件名解析逻辑写岔,比如用 split("_") 按分隔符切片时,文件名里的序号带了特殊字符,截取出来的文本根本不是真实的验证码内容。模型等于看着 A 图学 B 标签,loss 当然降不下去。
解决:先打印几条解析出的 text 和真实文件名做对比,确认解析逻辑没问题;再用一个确定性手段验证,把同一张图的标签和图片在内存里重新对应一遍,通过断言检查 one-hot 的 sum 是否为 4。我一般会在 load_data 里加一段assert label.sum() == 4,能提前拦掉八成标签错位问题。
现象:训练时报维度错误,或者模型在验证集上准确率正常但单张预测时结果完全不对。
原因:训练数据是 RGB 三通道,预测时加载的图片是灰度图,或者反过来。PIL 打开图片后不主动 convert 时,通道数取决于源文件格式,训练集和预测集通道不一致,卷积层的输入分布就被破坏了。
解决:在数据加载和预测函数里统一写死convert("RGB"),别依赖源文件的格式;灰度图转 RGB 无损,RGB 转灰度却会丢颜色信息,稳妥做法是全部统一到 RGB,模型输入通道就锁死为 3。
现象:混淆矩阵里某些字符对互相认错,比如 0 和 O、1 和 l、8 和 B,识别率总被拉低。
原因:字符集设计时混入了太难区分的相似字形,或者这些字符在训练集里的样本数量不够。机器和人一样,两个长得很像的东西如果见得太少,自然记不住区别。
解决:要么把相似字符从字符集里去掉,只保留它们区分度更高的字形变体;要么针对这些字符单独多生成一批样本,让模型见过足够多的正反例。做验证码识别不要头铁,绕开语义上的坑比硬调参高效得多。
4.2 训练过程侧:过拟合、Loss 原地踏步、评估指标虚高
现象:训练准确率一路冲到 99%,验证集准确率卡在 85% 上不去,两者差距越来越大。训练集和验证集同源,理论上分布一致,这个差距纯粹是模型把训练数据背下来了。
原因:模型容量超出数据量能支撑的复杂度,卷积层太深、全连接层太宽、训练轮次太长,都会加剧过拟合。另一个隐蔽原因是数据增强没做,验证码训练集经常只有几千张,信息量不够模型学出真正的泛化特征。
解决:优先把 Dropout 从 0 提到 0.5,看验证集能不能跟上来;然后调小 EarlyStopping 的 patience,别让模型在过拟合区段里反复横跳;最后再上数据增强——随机旋转 5 度、轻微平移、加椒盐噪点,这些模拟真实网站验证码风格的扰动对泛化提升立竿见影。
现象:loss 在 4 左右一直震荡,训练几轮完全没进步,调大学习率反而直接变 NaN。
原因:学习率过高导致梯度震荡,或者图像没有归一化。验证码图片像素值在 0-255 范围时,梯度尺度会变得非常大,Adam 优化器也hold不住。
解决:确认图片除以 255 转 float32 这一步做了没;初始学习率固定用 1e-3,配合 ReduceLROnPlateau 让它自适应下降。出现 NaN 时先降学习率到 1e-4 重跑一轮,别急着换网络结构。
现象:模型说准确率 98%,实际拿去跑真实图片,能完全识别对的连一半都不到。
原因:评估时用的是字符级准确率,但业务需求是整串验证码完全正确。4 位验证码,每个字符准确率 98%,整串全对的概率只有 0.98 的 4 次方约 92%,这已经是最乐观的情况。如果用的是更宽松的评估口径,虚高会更夸张。
解决:评估脚本里统计「序列准确率」——只有 4 个字符全部预测正确才算对,这个指标才是能直接对标业务效果的数字。我习惯在预测函数外再包一层循环,批量跑测试集,比较完整字符串的命中率。
4.3 部署与复用侧:模型加载报错、字符分割是伪需求
现象:模型训练保存后,换到另一台机器加载 .h5 文件报错,说 layer 名称或权重形状不匹配。
原因:Keras/TensorFlow 版本不一致,旧版 .h5 格式的跨版本兼容性问题尤其多。训练环境和部署环境框架版本差一两个大版本,加载时经常翻车。
解决:保存时优先用model.save("captcha_best.keras")新格式;部署环境严格锁定和训练环境一致的 TensorFlow 版本,装环境时别图省事直接 pip 最新版,最好把版本写死在 requirements.txt 里。
现象:项目拿到手,第一反应是先做字符分割——把验证码切成单个字符再识别,切完发现切歪了,整个流程就全崩了。
原因:字符粘连、旋转、背景干扰让分割边界极难确定,分割环节的错误会一路传导到识别环节,分割错一个字符,后面全错。这条路在无干扰数字验证码上可行,但在带扭曲和干扰线的验证码上是死胡同。
解决:放弃分割,直接把整张图喂给 CNN。CNN 通过卷积核滑动天然把「哪里有什么字符」这个信息编码进了特征图,定长验证码可以直接用本文的结构,不定长验证码改用 CTC 损失函数也能端到端训练,完全不需要前置分割。
5. 把准确率从可用推到好用:验证方法、数据增强与滑块验证码延伸
5.1 用混淆矩阵和置信度阈值做一次模型体检
训练完别急着上线,先跑一段独立的测试集,输出混淆矩阵,重点看哪些字符对互相污染。这一步能直接告诉你要不要补数据、要不要改字符集。同时把模型输出的 softmax 概率当成置信度用:当 4 个位置中任一个概率低于 0.85 时,直接判定为「不确定」,触发重试或者转人工。置信度阈值是个可用可不用的调节阀,调低减少工作量但准确率下滑,调高则相反,我一般从 0.8 开始试。
5.2 再往上推两三个百分点的实用招数
第一招是数据增强,不用花哨的对抗训练,随机旋转 5 度以内、水平平移几个像素、加一点高斯噪声就够用,模拟真实网站验证码的动态风格。第二招是多模型投票,用三个不同的随机种子训练三个结构相同的模型,预测时把三份 softmax 概率做平均再取 argmax,单模型 92% 的序列准确率能稳定拉到 94% 以上。第三招最土但最有效——去目标网站的验证码里挑几类字体,在 captcha 库里换成同风格字体生成仿真数据,让训练分布贴近真实分布,效果立竿见影。
5.3 从字符验证码到滑块验证码:同一个 CNN 思路的延伸
滑块验证码是现在的主流,这已经不是字符分类问题了,而是定位问题。同样可以用 CNN 提取特征,把最后全连接层改成输出缺口中心坐标和缺口宽度,用回归或目标检测的思路来做,框架上可以接 Faster R-CNN 或 YOLO 这类检测模型,也可以直接用 CNN backbone 加一个坐标回归头。损失函数从交叉熵换成 MSE 回归损失,训练数据从字符标签换成坐标标注,整体思路和字符验证码一脉相承,本质都是让 CNN 学会从图像特征映射到业务目标。
我现在的习惯是,每训完一版模型,先用 50 张仿真图加 50 张真实图做一个快速盲测,序列准确率达标了才谈优化参数的事。调参调到头发现模型不动,先从数据分布里找原因,这一条比任何网络结构 trick 都管用。做验证码识别这半年下来,最大的感受是:CNN 只是个工具箱,真正决定项目成败的是数据质量和评估方式。这套流程能帮你把模型跑通,方向也对,剩下的就是在自己的实际场景里不断打磨数据,希望帮到你。
本文还有配套的精品资源,点击获取