简介:基于Python与CNN神经网络的验证码识别项目,完整提供源码与文档说明,适合作为毕业设计、期末大作业或课程设计的高分参考。项目采用端到端卷积神经网络模型,对纯数字验证码的识别准确率可达99.99%,且代码包含详细注释,覆盖验证码生成、图像预处理、one-hot编码、模型训练、预测与测试全流程,可帮助开发者快速理解CNN在图像识别任务中的实际搭建与调参方法。资源压缩包共50个文件,以40张png验证码样本、8个Python脚本为主体,配套1份README说明文档与gitignore配置,包体约592KB,整体结构清晰。其中验证码生成与训练预测模块均可独立调用,便于按数据集、模型、训练和预测等模块查阅使用。截至当前已有115人学习浏览该资源,项目经过调试验证可运行,既可作为论文写作前的实验原型,也能直接用于答辩演示或期末项目展示。
1. Python用CNN识别验证码:不是贴个模型那么简单
看到这个标题,多数人的第一反应是“CNN识别验证码嘛,网上开源项目一大把,拉下来跑通就完事”。真正上手过的人知道,验证码识别是一条流水线:样本从哪来、预处理怎么统一、网络结构怎么搭、训练到什么程度算好、最后怎么接到自动化流程里,每一步都有细节在等你。标题里的“项目源码+文档说明”听起来像个开箱即用的包,但把别人的源码跑通只是开始,能把它改造成你手头数据可用的方案,才是这个方向真正的价值所在。
这篇文章适合三类人:做自动化测试需要绕过图形验证码的测试工程师;想用Python入门深度学习的同学,验证码是一个样本量小、标注成本低、效果反馈快的练手场景;以及要把OCR前置能力集成进爬虫或RPA流程的开发者。先给结论:识别率卡在80%上不去的时候,问题通常不在网络结构,而在样本质量和预处理一致性上。这个判断,后面每一步都能印证。
2. 从样本到输入张量:验证码数据集怎么造、怎么洗
2.1 生成训练样本:用Pillow批量造出带噪、带扭曲的验证码
训练CNN第一步是攒数据。真实验证码标注成本高,四种字符混合、上千张手工标注一下午就没了,而且真实样本的版权和合规问题也麻烦。常见做法是用Pillow自己生成,字体、尺寸、噪声、扭曲程度全部可控,一套脚本几分钟就能造出上万张。下面这段是我常用的生成器:
# sample_gen.py —— 批量生成带干扰的 4 位验证码样本 import os import random from PIL import Image, ImageDraw, ImageFilter CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789' # 参与训练的字符表 OUT_DIR = 'dataset/train' os.makedirs(OUT_DIR, exist_ok=True) def make_sample(save_path: str, size=(120, 40), font_size=28): text = ''.join(random.choices(CHARS, k=4)) # 随机取 4 个字符 img = Image.new('RGB', size, (255, 255, 255)) # 白底 draw = ImageDraw.Draw(img) # 随机干扰线:每条线两个随机端点 for _ in range(3): draw.line( [random.randint(0, size[0]), random.randint(0, size[1]), random.randint(0, size[0]), random.randint(0, size[1])], fill=(random.randint(0, 150), random.randint(0, 150), random.randint(0, 150)), width=1 ) # 逐字符绘制,x 坐标递增,y 坐标上下浮动 x = random.randint(4, 8) for ch in text: y = random.randint(2, 6) draw.text((x, y), ch, fill=(30, 30, 30), font=None) x += random.randint(22, 26) # 控制字符间距 # 轻微扭曲 + 压缩噪点 img = img.filter(ImageFilter.SMOOTH) img = img.filter(ImageFilter.GaussianBlur(radius=0.5)) img.save(save_path) with open(save_path.replace('.png', '.txt'), 'w') as f: f.write(text) for i in range(12000): make_sample(f'{OUT_DIR}/sample_{i:05d}.png') if i % 1000 == 0: print(f'generated {i}')逻辑说明:脚本每次随机生成4个字符,写进图片同时把标签存成同名txt,训练时读文件名或txt都能对齐。干扰线、高斯模糊、字符上下浮动模拟的是真实验证码最常见的三种干扰。关键参数是size=(120, 40)、font_size=28和字符间距22~26——这三个值决定了后面CNN输入张量的尺寸,也决定了字符是否粘连。
参数说明:图片尺寸不宜过大,120x40是速度和精度之间的折中,再大就浪费算力;字符间距22到26像素是模拟“轻微粘连但不完全重叠”的常见状态,间距太小生成的样本难度过高,模型会学得很痛苦。GaussianBlur(radius=0.5)模拟的是图片压缩后的模糊感,半径超过1.5会让字符边缘糊成一团,慎调。如果你要复现,先跑1000张,打开图片看一遍再批量生成,这是省时间的重点。
2.2 预处理与两种识别路线:整图直读 vs 单字符分割
样本造好之后,下一步是把图片转成CNN能吃的张量。OpenCV做这一步最顺手。以灰度图输入为例:
# preprocess.py —— 统一图片尺寸与灰度 import cv2 import imutils def load_and_preprocess(path: str, target_size=(120, 40)) -> 'numpy.ndarray': # 读成单通道灰度图 img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f'cannot read image: {path}') # 统一尺寸:等比缩放后填充到目标尺寸,避免字符变形 img = imutils.resize(img, width=target_size[0]) h, w = img.shape top = max(0, (target_size[1] - h) // 2) bottom = target_size[1] - h - top img = cv2.copyMakeBorder(img, top, bottom, 0, 0, cv2.BORDER_CONSTANT, value=255) # OTSU 二值化:把前景字符和背景噪声分离 _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 开运算:去掉孤立的盐噪声点 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return binary.astype('float32') / 255.0逻辑说明:这段预处理的核心是“消除输入分布差异”。不管图片来源是生成器还是真实请求,都要落到相同的尺寸、相同的通道顺序、相同的像素取值范围。copyMakeBorder用白色填充而不是黑色,是因为验证码背景是浅色,填充黑色会把网络训练时关注的边界区域搞乱。OTSU阈值自动找分割点,比固定127强,尤其适合背景有渐变色的验证码。
参数说明:kernel=(2, 2)的开运算核很有讲究。核太小去不掉噪点,核太大(比如3x3以上)会把细字符笔画也腐蚀掉,导致识别率下降。这类形态学操作的参数,每次换数据集都要重新验证,不要一套参数走天下。二值化后除以255归一化到0~1,是CNN训练的基本操作,不做这一步梯度下降会非常不稳定。
预处理之后要选识别路线。定长4位验证码有两种主流做法:
| 路线 | 输入输出形态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 整图直读 | 整张图进CNN,输出4x36的分类 | 无需分割,管线简单 | 对字符粘连、重叠敏感 | 字符间距清晰的验证码 |
| 单字符分割 | 先把图切成4块,逐块识别 | 每个字符识别率稳定 | 分割出错则全错,定位算法要调 | 字符分离、无粘连的验证码 |
我一般优先做整图直读。原因是分割路线的“分割准确率”是乘法关系——即使单字识别率有98%,四字全对也只有92%;而整图直读让CNN自己学字符位置,只要样本多样,鲁棒性反而更高。分割路线只在字符严重粘连、整图识别率明显偏低时才回头考虑,配合垂直投影找字符边界。
2.3 样本质量决定了训练上限:清洗与均衡的四个检查点
很多项目死在“样本集看起来没问题”。生成完数据要做的第一件事不是训练,而是检查分布。以下四个检查点是我踩过坑后才固定下来的流程:
字符均衡:random.choices生成字符,理论上分布均匀,但量小的时候会有偏差。训练前统计一下各字符出现次数,低于平均数的字符补样本。CNN对样本少的类别会无脑偏向高频类,这是黑匣子里的常见翻车点。
异常样本过滤:生成过程中如果字体库缺失,某些字符可能渲染成方框或空白。批量生成的脚本里加一步:对每张图做连通域分析,字符数不等于4就删掉。这个检查能帮你省掉大量“训练集loss极低但测试全灭”的排查时间。
训练集和验证集同分布:从同一个生成器出来的数据分train/val,分布天然一致,但这不代表真实场景。真实验证码如果带彩色噪点、环形干扰、字符旋转,而你生成的样本没有,验证集再高也没用。
标签与图像严格对齐:保存图片和标签时,文件名对应关系一旦错位,整个数据集就是毒药。建议生成时把标签写进文件名或txt,并用脚本抽查20张图人工核对标签内容,不要相信“生成逻辑这么简单所以不会错”。
3. 搭建CNN识别模型:网络结构、参数与训练入口
3.1 选型理由:为什么CNN而不是全连接或传统OCR
传统OCR方案先做字符分割,再对单个字符做模板匹配或特征提取。这套技术在印刷体扫描件上很成熟,但对付扭曲、带干扰线、字符间距不定的验证码就力不从心——模板匹配的本质是找“相似区域”,而验证码设计的初衷就是让你找不到稳定特征。Tesseract在这类图像上的表现,基本是拆盲盒级别。
CNN卷积神经网络的优势在于“局部平移不变性”。卷积核在整张图上滑动,字符往左偏两个像素、上移三个像素,对激活值的影响很小;配合池化层的下采样,网络学到的是“这里有一个圆圈、那里有一条斜线”这种局部结构,而不是像素级的精确位置。这正是验证码识别最需要的性质:干扰线随便画,只要核心笔画结构在,卷积核就能响应。全连接网络没有这个性质,输入尺寸一变就失效,参数量大且极易过拟合。所以这个场景下CNN是首选,不是因为它新,而是因为它和任务性质匹配。
3.2 一套能跑的Keras模型:Conv→BN→MaxPool结构
模型结构不需要很复杂,验证码不是ImageNet,过深的网络反而在少量样本上过拟合。下面这套结构是验证码识别里比较经典的卷积堆叠方式:
# model.py —— 定义一个适用于 120x40 定长验证码的 CNN from tensorflow.keras import layers, models INPUT_SHAPE = (40, 120, 1) # 注意通道维在最后,与 TF 默认布局一致 NUM_CLASSES = 36 # 单字符类别数:26 字母 + 10 数字 CAPTCHA_LEN = 4 # 验证码长度(定长才有这个输出设计) def build_cnn(): model = models.Sequential(name='captcha_cnn') model.add(layers.Input(shape=INPUT_SHAPE)) # 第一组卷积:提取低级边缘、笔画特征 model.add(layers.Conv2D(32, (3, 3), padding='same', activation='relu')) model.add(layers.BatchNormalization()) model.add(layers.MaxPooling2D(pool_size=(2, 2))) # 第二组卷积:组合成局部结构特征 model.add(layers.Conv2D(64, (3, 3), padding='same', activation='relu')) model.add(layers.BatchNormalization()) model.add(layers.MaxPooling2D(pool_size=(2, 2))) # 第三组卷积:抽象出字符部件 model.add(layers.Conv2D(128, (3, 3), padding='same', activation='relu')) model.add(layers.BatchNormalization()) # 全局池化替代 Flatten,减少参数量 model.add(layers.GlobalAveragePooling2D()) model.add(layers.Dense(128, activation='relu')) model.add(layers.Dropout(0.5)) # 输出层:4 个字符 x 36 类 = 144,用 softmax 展开成 (4, 36) model.add(layers.Dense(CAPTCHA_LEN * NUM_CLASSES, activation='softmax')) return model model = build_cnn() model.summary()逻辑说明:三层卷积逐级抽象特征——底层看边缘,中层看笔画组合,高层看部件结构。每次卷积后都接BatchNormalization,作用是让激活值分布稳定,训练收敛快不少。池化层放在卷积后面下采样,特征图从120x40缩到60x20再缩到30x10,分辨率降低的同时感受野变大。最后用GlobalAveragePooling2D而不是Flatten,把每个特征图压成一个数值,参数量小一个量级,过拟合风险明显降低。
参数说明:(3,3)卷积核是最稳的起点,5x5感受野更大但细节保留差;padding='same'保证特征图尺寸不缩水,池化时才统一减半。Dropout 0.5放在全连接层前是防过拟合的关键位置,训练集较小的时候不建议低于0.3。最后一层输出4 * 36 = 144维,前4维代表第0个字符的36类概率,依次类推。这个输出设计只适用于定长验证码,变长场景要改用CTC损失,做法会复杂很多。
3.3 损失函数与输出层:定长验证码的标签编码
网络输出是144维向量,标签不能直接用字符串丢给模型,需要编码。常见做法是把每个字符映射成one-hot向量,再把4个字符的one-hot拼接:
# encode.py —— 字符串标签转成模型输出格式 import numpy as np CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789' CHAR2IDX = {ch: i for i, ch in enumerate(CHARS)} def encode_text(text: str) -> np.ndarray: """ 'a1b2' -> shape (4, 36) 的 one-hot 矩阵 """ vec = np.zeros((CAPTCHA_LEN, len(CHARS)), dtype='float32') for pos, ch in enumerate(text): vec[pos, CHAR2IDX[ch]] = 1.0 return vec def decode_pred(pred: np.ndarray) -> str: """ 把 (4, 36) 概率矩阵解码回字符串 """ idx = pred.argmax(axis=1) return ''.join(CHARS[i] for i in idx)逻辑说明:encode_text做的是定长对齐,不足4位的字符串在训练数据里本身就是脏数据,应该直接过滤。decode_pred拿的是已经reshape成(4, 36)的概率矩阵,逐位取最大概率的字符索引。这两个函数是训练和推理的数据桥,方向相反,必须保证字符表顺序完全一致。
参数说明:损失函数用categorical_crossentropy时,y传给fit的shape必须是(batch, 144)而不是(batch, 4, 36),Keras会自动处理还是报错取决于后端版本,我一般在训练脚本里显式reshape(-1, 144)。字符表里字母和数字的顺序无所谓,但一旦定了就不要改,否则训好的模型全部废掉——因为这个模型记住了“索引0是字符a”。
4. 训练、评估与调参:识别率从80%到98%的实际路径
4.1 训练脚本最小实现:数据加载、切分、Checkpoint
数据集和模型就绪,训练脚本要处理三件事:把图片批量读成numpy数组、划分验证集、训练时自动保存最优权重。下面是一份可以直接落地的骨架:
# train.py —— 训练主流程 import os import glob import numpy as np from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint from tensorflow.keras.optimizers import Adam def load_dataset(img_dir: str): img_paths = sorted(glob.glob(os.path.join(img_dir, '*.png'))) X, y = [], [] for p in img_paths: arr = load_and_preprocess(p) # 复用 2.2 的预处理函数 label = os.path.basename(p).split('_')[-1].replace('.png', '') X.append(arr) y.append(encode_text(label).reshape(-1)) X = np.stack(X)[..., np.newaxis] # (n, 40, 120, 1) y = np.stack(y) # (n, 144) return X, y X, y = load_dataset('dataset/train') perm = np.random.permutation(len(X)) split = int(len(X) * 0.9) X_train, X_val = X[perm[:split]], X[perm[split:]] y_train, y_val = y[perm[:split]], y[perm[split:]] model = build_cnn() model.compile( optimizer=Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'] ) callbacks = [ ModelCheckpoint('best.h5', monitor='val_loss', save_best_only=True), EarlyStopping(monitor='val_loss', patience=8, restore_best_weights=True), ] model.fit( X_train, y_train, validation_data=(X_val, y_val), batch_size=64, epochs=60, callbacks=callbacks, verbose=1 )逻辑说明:load_dataset把每张图走一遍2.2的预处理管道,同时从文件名解析标签。perm先打乱再切分,避免生成器按顺序写入导致同字符连续出现。训练时ModelCheckpoint盯着验证集loss存最优权重,EarlyStopping在验证集连续8个epoch不提升时自动停掉并恢复最优权重。这套组合保证你就算挂机去吃饭,回来拿的也是训练过程中的最佳状态,而不是最后一轮可能已经过拟合的状态。
参数说明:学习率1e-3配Adam是通用起点。验证集比例10%,12000张样本留1200张做验证,够用且不会太挤占训练数据。batch_size=64在显存允许的情况下比32收敛更平滑,但如果发现loss震荡厉害,先降到32试试。patience=8意味着模型在验证集上连续8轮无提升就停止,过小容易在loss平台期提前结束,过大则浪费时间。
4.2 三个必调的参数:batch size、学习率、数据增强
这三个参数是识别率从80%往98%爬的关键。我调参的顺序是固定的:
学习率。1e-3跑不动(loss停在2.0以上不降)就降到5e-4或3e-4;如果loss下降但验证集波动剧烈,考虑在训练中途衰减。最简单的方式是配合ReduceLROnPlateau回调,验证集不提升就自动乘以0.2。学习率是这套模型里最敏感的参数,没有之一。
batch size。64起步,loss曲线震荡就回调32;batch size加倍(128)能让训练加快,但在小数据集上容易让梯度方向过于“平均”,陷入局部最优。
数据增强。训练集只有一万张时,增强是防过拟合最有效的招。ImageDataGenerator里四个参数值得调:
from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rotation_range=5, # 随机旋转正负5度,再大字符语义会变 width_shift_range=0.05, # 水平平移5% height_shift_range=0.05, # 垂直平移5% zoom_range=0.05, # 随机缩放5% brightness_range=(0.9, 1.1), # 亮度轻微扰动,模拟不同渲染背景 fill_mode='constant', # 平移后填充白色,和验证码底色一致 cval=255 ) train_flow = datagen.flow(X_train, y_train, batch_size=64) model.fit(train_flow, validation_data=(X_val, y_val), epochs=60, callbacks=callbacks)逻辑说明:增强的本质是“让模型见过更多变形”。rotation_range超过10度会让“6”看起来像“9”、“z”像“2”,语义混淆得不偿失。平移和缩放模拟的是真实验证码字符位置随机浮动。brightness_range应对的是一些网站用浅灰字体渲染字符的情况。fill_mode='constant'配合cval=255让空白区域填充白色,否则黑色填充会引入不存在的背景结构。
这里有一个常见误用:做增强时把原始X_train也喂给模型,等于训练集和验证集分布不一致。增强只放在训练侧,验证集永远用原始清洗后的数据。
4.3 评估不能只看准确率:按字符、按样本类型拆开看
训练结束别急着部署。整体准确率是个黑匣子——它可能高达96%,但“8”这个字符识别率只有60%,刚好在生产流量里“8”出现频次最高,你的模型在线上就会翻车。评估要拆开看:
# evaluate.py —— 逐字符准确率与失败样本可视化 from sklearn.metrics import confusion_matrix, classification_report pred = model.predict(X_val, verbose=0) # shape (n, 144) pred = pred.reshape(-1, CAPTCHA_LEN, len(CHARS)) pred_texts = [decode_pred(p) for p in pred] true_texts = [decode_pred(y.reshape(-1, CAPTCHA_LEN, len(CHARS))[i]) for i in range(len(y))] # 逐字符对比,统计每个字符的识别情况 char_true, char_pred = [], [] for t, p in zip(true_texts, pred_texts): for ct, cp in zip(t, p): char_true.append(ct) char_pred.append(cp) print(classification_report(char_true, char_pred)) # 找出整图预测失败的前 20 张,人工检查是预处理还是网络问题 miss = [(i, t, p) for i, (t, p) in enumerate(zip(true_texts, pred_texts)) if t != p] for i, t, p in miss[:20]: print(f'idx={i} true={t} pred={p}')逻辑说明:classification_report直接给出每个字符的精确率、召回率、F1值。如果发现个别字符F1明显低于均值,不用急着改网络,先看失败图片——通常是该字符在字符表里和另一个字符形态相似,或者训练样本里这个字符数量偏少。打印前20个失败样本时,对比原始图片确认预处理有没有把它们搞坏。
参数说明:这里reshape(-1, 4, 36)必须和编码时完全对应,写错一位得到的评估结果全是废数据。整图准确率和逐字符准确率是有区别的:假设每字符识别率98%,4位整图准确率理论值大约92%;如果你的整图准确率远低于这个乘积估算,说明错误集中出现在特定字符组合或特定样张类型上,这才是要深挖的方向。
5. 验证码识别避坑:五个常见翻车现场与排查思路
5.1 现象:训练loss居高不下,模型完全没学进去
训练了20个epoch,训练集loss还在2.5以上(相当于随机猜36类),准确率在10%到20%之间徘徊。先别调网络,查三处:第一,标签和图片对不对得上——之前就见过有人脱敏时把文件名和标签对应关系弄乱,模型看到的是“字符A的图配字符B的标签”,神仙网络也学不出来;第二,预处理输出形状和INPUT_SHAPE是否一致——(120, 40, 1)和(40, 120, 1)差一个transpose,模型能跑但学到的全是无效信息;第三,归一化是否完成——像素值没缩放到0~1之间,梯度的量级就会异常。解决:写一个20张样本的迷你测试集,人工核对标签和输入shape,确认无误再启动全量训练。
5.2 现象:训练集准确率99%,验证集只有70%
这就是过拟合的典型症状,模型把训练样本的噪声细节背下来了。解决路径按优先级排列:先加Dropout(0.3提到0.5),再加数据增强(旋转5度、平移5%),最后减小模型容量——把第三层卷积从128降到64。如果这三步做完验证集还是远低于训练集,检查验证集本身:是不是把同一个生成批次的数据切到了两边,导致数据泄露。很多看起来“玄学”的过拟合,根源是数据划分不随机。
5.3 现象:训练和验证都很好,一上真实验证码全灭
这种翻车最伤士气,原因只有一个:训练样本和线上样本分布不一致。真实验证码可能是彩底、带波纹、字符比例不同、字体跟你用的完全不一样。解决:从线上真实采集500~1000张样本,人工标注,混进训练集再训练一次。这是从“能跑demo”到“能上线”必经的一步,逃避不了。很多项目源码直接跑,训练集是作者生成的数据,换到你的目标站点就失效,根因就在这。
5.4 现象:单张识别要200毫秒,跑批量任务慢得离谱
排查时先分清耗时在哪一段。常见原因有三个:一是预处理里用了cv2.imread等待磁盘I/O,批量时建议用cv2.imdecode直接读内存字节流;二是模型权重太大,把best.h5换成量化版或用TensorRT转换;三是在每个请求里重复load_model——模型加载一次可能要几百毫秒,放在服务启动时加载,运行时只调predict。最容易被忽略的是预测时又做了一次OTSU和开运算,而训练时预处理已经做过一遍,再加上一次等于双重处理,特征分布直接错掉。
5.5 现象:定长模型识别变长或含中文的验证码,输出全是乱的
标题里的模型结构是定长设计,输出维度是“长度x类别数”,遇到4位变5位、或混入中文的验证码,结构本身就不适用。解决:要么预处理阶段做长度归一化——把变长验证码裁成固定长度(比如最多6位,不足补白);要么换模型——输出层改成序列模型,或者直接用CTC loss做不定长识别。这两条路工作量差别很大,如果你的目标验证码长度固定,不要为了“可能的变长”提前上CTC,先把定长做扎实。
6. 把模型接进自动化流程:预测脚本、批处理与验证方法
训练和评估都过关之后,最后一步是把模型集成到真实业务环境里。这里给出一份轻量级的预测服务片段,重点在于“输入格式和训练时完全一致”:
# predictor.py —— 加载模型,对外提供单张图片识别 import cv2 import numpy as np from tensorflow.keras.models import load_model MODEL_PATH = 'best.h5' class CaptchaSolver: def __init__(self, model_path: str = MODEL_PATH): self.model = load_model(model_path) def predict_bytes(self, img_bytes: bytes) -> str: # 直接从内存读取图片字节,避免磁盘 I/O nparr = np.frombuffer(img_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE) # 复用训练时完全相同的预处理逻辑 img = resize_to_target(img, (120, 40)) _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))) x = binary.astype('float32') / 255.0 x = x[np.newaxis, ..., np.newaxis] # (1, 40, 120, 1) pred = self.model.predict(x, verbose=0).reshape(-1, 4, 36) return decode_pred(pred[0]) solver = CaptchaSolver() print(solver.predict_bytes(open('real_sample.png', 'rb').read()))逻辑说明:预测脚本的核心是“与训练时保持输入分布一致”——灰度、二值化、开运算、归一化、通道顺序,每一步都不能省略,少一步识别率就会跳水。类封装的好处是模型只加载一次,后续每个请求只走前向推理。
参数说明:binary[np.newaxis, ..., np.newaxis]这个索引操作容易写错,展开就是(1, 40, 120, 1),batch维放前面,通道维放最后。如果你训练时用的是channels_first布局,这里就要换成(1, 1, 40, 120),两个布局的模型权重不通用,换布局等于重新训练。
验证方法上,我会固定保留50张线上真实样本从不参与训练,每次改模型或改预处理后跑一遍,记录“整图识别率”和“单字符识别率”两个指标。整图识别率低于80%的不允许进自动化流程,因为批量任务里一次失败就要重试,重试成本往往比想象高。另外建议记录每张预测的置信度——softmax输出的最大值。低于0.7的样本不要直接采用,标记出来人工复核。
最后说一个我的个人教训:刚开始做验证码识别时,我花了两周调网络结构,把卷积层从3层加到6层,识别率纹丝不动;后来重新审视预处理,发现真实数据和训练数据的二值化阈值算法不同(真实图用固定127,训练图用OTSU),统一之后识别率直接涨了15个点。从那之后,我再也不先动模型,永远是先对齐样本和预处理,再谈调参。这条路从样本生成、预处理、模型结构、训练评估到部署,每一步都是可以量化验证的,希望帮到你。
本文还有配套的精品资源,点击获取