ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

机器学习验证码识别实战:从数据生成到模型部署全解析

机器学习验证码识别实战:从数据生成到模型部署全解析 简介面向计算机科学与技术、人工智能、数据科学等相关专业的在校生与开发人员这份基于机器学习算法的验证码识别脚本源码包聚焦图像验证码的自动分类与识别问题能帮助读者从零搭建一个可运行的识别项目既适合新人用于机器学习实战入门也适合作为课程设计、大作业或初期项目演示的基础模板。压缩包共两千个文件体量约十二点四二兆字节其中包含一千九百九十五张图片格式的验证码样本作为训练数据四个源文件实现核心算法流程一份说明文档提供项目说明数据、代码、文档三位一体结构简单清晰可快速运行并修改扩展。目前已有一百六十三人学习下载具备一定的参考热度。通过这套源码读者可以完整了解验证码图像从读取、预处理到特征提取、模型训练与预测的工程实现同时可依据自带样本自行扩充数据集切换不同机器学习算法对比准确率深入理解图像分类任务的关键环节说明文档还能帮助快速完成环境配置与代码走读降低上手门槛。1. 一个压缩包背后验证码识别到底在解决什么机器学习算法为什么是核心拿到一个“基于机器学习算法的验证码识别脚本完整源码说明.zip”先别急着解压找predict.py得想清楚这个包解决的问题是什么把一张带干扰、带扭曲、带噪点的图片转成一段可用的字符串。这不是给人看的识别是给程序用的——批量提交表单、自动化测试、内部系统的数据采集凡是流程卡在验证码上都需要一个自动识别入口。真正入行之后你会发现Tesseract 这类传统 OCR 在验证码场景里根本不够用稍微加两条干扰线、字符一变型识别率就崩到没法用。机器学习算法在验证码识别里的价值是把“字符长什么样”这件事交给模型去学而不是靠人写规则硬匹配。这篇笔记就按这类压缩包最常见的工程结构来讲方案怎么选、数据怎么造、模型怎么训、脚本怎么写以及真正跑起来之后那些让人翻车的坑。2. 先别写代码验证码识别的方案选型和建模边界2.1 OCR 路线的天花板Tesseract 为什么会在验证码上翻车很多新人拿到验证码识别需求第一反应是装个 Tesseract毕竟pytesseract.image_to_string()一行代码就能出结果。这种方案在扫描文档、印刷体文字上确实能跑但验证码设计出来就是专门对抗 OCR 的。字符旋转、背景噪点、干扰线、字符粘连每一项都在击穿 Tesseract 的假设——它期望输入是干净的、规整的文本行。我用 Tesseract 做过一轮对比测试纯白底黑字、无干扰的四位数字识别率能到 90% 以上一旦加上彩色噪点和两条干扰线直接掉到 40% 左右。这不是参数没调好是算法本身的特征提取方式太脆弱它对局部形状的匹配依赖太重扛不住全局噪声的干扰。也有人说用 PHP 封装 OCR 识别验证码原理上还是 Tesseract底层能力没变换语言不能解决识别率问题。所以在方案选型上除非目标验证码是那种几乎没有干扰的“老式”验证码否则 OCR 路线可以直接排除。标题里强调“机器学习算法”本质就是把字符识别当作一个图像分类问题来做让模型从数据里自己学出抗干扰的特征。2.2 机器学习路线的三种建模方式分类、检测、序列识别验证码识别在机器学习视角下有三种常见建模方式选哪种取决于验证码本身的形态。第一种是单字符分类。把整张验证码图片按位置切成多个单字符小图每个字符单独做分类。这种方式最直观实现成本最低对字符位置固定的验证码非常有效。它的前提是字符不粘连、位置相对规整否则切割这一步先崩。第二种是目标检测。把验证码里的每个字符当作一个目标框出来再对框里的内容做分类。这种方式能解决字符位置不固定、有轻微重叠的问题但需要标注字符位置的训练数据标注成本比第一种高一个量级。第三种是端到端的序列识别常见的就是 CRNN CTC。把整张图喂给模型模型直接输出字符串序列。它不需要做字符级切割抗粘连能力强但模型结构更复杂训练时间更长对数据集规模的要求也更高。对于“四位纯数字、位置大致固定、带少量干扰”的验证码我的选择一向是最简单的分类方案。不是越复杂越好而是要在识别率、开发成本和可维护性之间取平衡。一个三十层 ResNet 识别四位数字纯属杀鸡用牛刀训练慢、部署重、还不好调。2.3 怎么根据验证码形态定方案四种常见验证码和对应模型验证码的形态直接决定技术路线这里列一个我的选型对照表照着套就行。验证码类型典型特征推荐方案预期识别率四位数字、白底黑字、位置固定无干扰或轻度干扰单字符分类 CNN95% 以上字符扭曲、有旋转字符变形明显但可分割单字符分类 数据增强85% - 95%字符粘连、长度不固定难以确定切割点CRNN CTC80% - 90%点选、滑块、语义类不是字符识别问题目标检测 / 专用模型另当别论这个表背后的逻辑是先看能不能分割再看字符是否有变形。能分割就做分类分割不了就做序列识别连字符都不是的就别往 OCR 上想。需要特别提醒的是滑块验证码和点选验证码根本不是“识别”问题。滑块缺口是定位问题点选是目标检测问题用字符识别脚本去处理它们第一步就会卡死。判断验证码类型是最容易被跳过的一个环节但恰恰是它决定了后续所有工作的方向。2.4 标题里“脚本”和“说明.zip”的工程含义目录结构、运行环境和数据流这种打包发布的技术资源源码部分我会习惯按固定结构组织因为它对应了一条完整的数据流数据准备 → 预处理 → 模型训练 → 模型推理。整个流程是串起来的缺一环后面就跑不动。按工程经验这类资源至少会包含这几个模块gen_data.py用于生成训练数据、preprocess.py负责图片预处理和字符分割、train.py做模型训练、predict.py做单张推理外加一个requirements.txt列依赖库。这五个文件的依赖关系是单向的preprocess.py被训练和推理共用这是降低重复代码的关键设计。运行环境上需要确认 Python 版本和关键依赖版本。PyTorch 的 API 在 1.x 和 2.x 之间有一些变化OpenCV 的connectedComponentsWithStats接口相对稳定但也要注意安装的是opencv-python而不是opencv-contrib-python后者的头文件路径有时候会对不上。这类问题看着不起眼实际运行时第一个报错往往就出在环境上。3. 数据从哪来生成器、预处理和字符切割源码的第一个关键模块3.1 用 PIL 写一个字符验证码生成器三十行代码解决数据标注训练数据是验证码识别项目里最容易被低估的部分。很多人拿到源码第一反应是找现成的真实验证码图片但真实图片的标注成本高而且数量不够。常见做法是写一个生成器模拟目标验证码的字体、颜色、干扰线、噪点分布批量生成带标签的数据。这个思路对固定样式的验证码非常有效因为我们可以完全控制标签的准确性。生成器我一般用 PIL 写核心逻辑很简单创建空白画布 → 逐个画字符 → 画干扰线 → 加噪点 → 保存图片和标签到文本文件。字符间距是这里的关键参数它直接决定后续切割能不能成功。如果间距设置得太随意字符粘连就会成为训练数据的系统性缺陷。要注意的是字体路径的问题。Linux 下 DejaVuSans-Bold.ttf 是常见字体Windows 下路径完全不同代码里如果用硬编码路径换到不同环境就崩了。习惯的做法是启动时检查字体文件是否存在不存在就自动从系统字体目录里挑一个可用的。3.2 预处理不是必须做的灰度、二值化和去噪的取舍很多新手在预处理上用力过猛灰度 → 二值化 → 中值滤波 → 膨胀腐蚀 → 轮廓查找一套组合拳全打上去结果识别率反而下降。原因是验证码图片本身就是为了对抗这些操作而设计的过度预处理会连字符的有效信息一起抹掉。我的经验是预处理只做三件事灰度化、二值化、去掉面积过小的连通域噪点。灰度化是为了把彩色信息压缩掉把关注点集中在亮度上二值化是为了把字符和背景彻底分开方便后续切割去噪点是为了避免把干扰点切进字符区域。这三步做完就停不要额外做锐化、对比度增强这些操作模型自己能学到的特征不需要人去强行放大。对二值化cv2.threshold加THRESH_BINARY_INV | THRESH_OTSU是稳定的默认选项。OTSU 自动计算阈值不需要人调。背景色深的验证码可能要把THRESH_BINARY_INV去掉这个细节在调试时最容易被忽略。3.3 代码生成与预处理模块的完整流程与参数设置生成器脚本保存为gen_data.py直接在命令行跑# gen_data.py # 生成 4 位数字验证码训练集每张图片对应一行文本标签 from PIL import Image, ImageDraw, ImageFont, ImageFilter import random, os, argparse parser argparse.ArgumentParser(description生成验证码训练数据) parser.add_argument(--num, typeint, default10000, help生成图片数量) parser.add_argument(--width, typeint, default120, help图片宽度) parser.add_argument(--height, typeint, default40, help图片高度) parser.add_argument(--out_dir, typestr, default./dataset, help输出目录) args parser.parse_args() # Linux 常见字体路径Windows 需改成 simhei.ttf 或 arial.ttf FONT_PATH /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf CHARS 0123456789 def random_color(brightFalse): if bright: # 亮色用于字符模拟验证码字符偏亮的典型配色 return (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255)) # 暗色用于干扰线和噪点 return (random.randint(0, 120), random.randint(0, 120), random.randint(0, 120)) def gen_one(): label .join(random.choices(CHARS, k4)) img Image.new(RGB, (args.width, args.height), (255, 255, 255)) draw ImageDraw.Draw(img) font ImageFont.truetype(FONT_PATH, 28) # 画 3 到 5 条干扰线位置和颜色随机 for _ in range(random.randint(3, 5)): x1 random.randint(0, args.width) y1 random.randint(0, args.height) x2 random.randint(0, args.width) y2 random.randint(0, args.height) draw.line((x1, y1, x2, y2), fillrandom_color(), width1) # 字符逐个写入x 方向间距固定为 24上下位置带随机偏移 for i, ch in enumerate(label): x 6 i * 24 random.randint(-2, 2) y random.randint(4, 10) draw.text((x, y), ch, fontfont, fillrandom_color(brightTrue)) # 撒 60 到 150 个随机噪点 for _ in range(random.randint(60, 150)): draw.point((random.randint(0, args.width - 1), random.randint(0, args.height - 1)), fillrandom_color()) # 轻微高斯模糊让字符边缘不那么锐利更接近真实渲染效果 img img.filter(ImageFilter.GaussianBlur(radius0.6)) return img, label os.makedirs(os.path.join(args.out_dir, images), exist_okTrue) with open(os.path.join(args.out_dir, labels.txt), w) as fp: for i in range(args.num): img, label gen_one() img.save(os.path.join(args.out_dir, images, f{i:05d}.png)) fp.write(f{i:05d}.png {label}\n) print(生成完成总量, args.num)这里的核心参数是字符横坐标的起始6和间距24。这两个值需要根据目标验证码的实际字符宽度去调整间距太小会导致字符粘连太大又让切割点落在空白区域造成多余背景。生成数量默认 10000四位纯数字总共只有 10000 种组合理论上每种组合都能覆盖到但实际字符的样式变形需要靠随机偏移和噪声来扩充。预处理脚本保存为preprocess.py# preprocess.py # 灰度化、OTSU 二值化、连通域去噪、固定宽度切割 import cv2 import numpy as np def preprocess_and_split(img_path, width120, height40): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f无法读取图片: {img_path}) img cv2.resize(img, (width, height)) # OTSU 自动阈值二值化BINARY_INV 把字符反转为白色 _, bin_img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 去除面积小于 8 像素的连通域这些通常是噪点而不是字符 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(bin_img, connectivity8) clean np.zeros_like(bin_img) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] 8: clean[labels i] 255 # 固定宽度切割成 4 个字符区域切割边界要和生成器的字符坐标对齐 margins [(8, 32), (32, 56), (56, 80), (80, 104)] chars [] for x1, x2 in margins: block clean[:, x1:x2] # 每个字符统一缩放到 28x28CNN 输入尺寸必须一致 block cv2.resize(block, (28, 28)) block block.astype(np.float32) / 255.0 chars.append(block) # 返回形状为 (4, 1, 28, 28) 的数组顺序就是 4 个字符 return np.stack(chars).reshape(4, 1, 28, 28)切割边界和生成器的字符坐标必须严格对应。生成器里字符从x6开始、间距 24所以第一个字符在6~30区域对应切割边界的(8, 32)留了一点余量给随机偏移。如果生成时改了字符间距这里的 margins 也得反过来改这是预处理模块最常见的调试点。4. 训练和预测把机器学习算法跑成可用的脚本4.1 一个能落地的模型设计小 CNN 输出四个字符的联合预测模型结构不需要复杂我用的是一个两层卷积加两层全连接的小网络。输入是(4, 1, 28, 28)的四张字符图但我不想把它们当成四个独立样本分开预测而是把它们拼成一个批次维度输入模型输出端直接生成 4 个字符各自的类别概率。具体来说模型的输入形状是(batch, 4, 1, 28, 28)实际训练时可以把每张图片的 4 个字符看作一个整体样本模型输出的形状是(batch, 4, 10)表示每个字符对应 0-9 十个类别的分数。这种设计的优势在于一次前向传播就能完成整张验证码的预测而且后处理非常方便直接在最后一个维度取argmax就行。你可能问为什么不直接用四个独立的分类器那样更简单。但共享卷积层可以让模型学到通用的字符特征数据利用率更高训练参数量也更小。对四位数字验证码这种简单任务这个网络的参数量在十万级别CPU 上训练都非常快。4.2 代码train.py 训练脚本与参数说明# train.py # 训练 CaptchaNet输出模型权重 captcha_net.pt import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import os # 复用 preprocess.py 中的预处理和切割函数 from preprocess import preprocess_and_split class CaptchaDataset(Dataset): def __init__(self, root): self.samples [] # labels.txt 每行格式图片文件名 空格 4 位标签 with open(os.path.join(root, labels.txt)) as fp: for line in fp: name, label line.strip().split() self.samples.append((os.path.join(root, images, name), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] chars preprocess_and_split(path) target torch.tensor([int(c) for c in label], dtypetorch.long) # chars 的形状为 (4, 1, 28, 28)作为整体输入 return torch.from_numpy(chars), target class CaptchaNet(nn.Module): def __init__(self): super().__init__() self.conv nn.Sequential( nn.Conv2d(1, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2) ) # 输入 4x1x28x28经过两次池化后变成 4x32x7x7 self.fc nn.Sequential( nn.Linear(32 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, 40) # 4 个字符每个 10 类 ) def forward(self, x): b x.size(0) x self.conv(x).view(b, -1) x self.fc(x) # 把 40 维输出拆成 (4, 10)对应 4 个字符各自 10 类的分数 return x.view(b, 4, 10) def main(): dataset CaptchaDataset(./dataset) loader DataLoader(dataset, batch_size128, shuffleTrue, num_workers0) model CaptchaNet() optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(10): total 0 correct 0 loss_sum 0 for imgs, targets in loader: imgs imgs.float() logits model(imgs) # (batch, 4, 10) # 把 (batch, 4, 10) 展平成 (batch*4, 10)计算交叉熵 loss criterion(logits.view(-1, 10), targets.view(-1)) optimizer.zero_grad() loss.backward() optimizer.step() pred logits.argmax(dim-1) # (batch, 4) # 只有 4 个字符全部预测正确才算这张验证码识别正确 correct (pred targets).all(dim1).sum().item() total targets.size(0) loss_sum loss.item() epoch_acc correct / total print(fepoch {epoch 1} loss {loss_sum / len(loader):.4f} acc {epoch_acc:.4f}) torch.save(model.state_dict(), captcha_net.pt) print(训练完成权重已保存到 captcha_net.pt) if __name__ __main__: main()参数上有几个值得注意的细节。batch_size设 128因为数据量不大且模型很小显存和内存压力都小CPU 也能跑动。lr1e-3是 Adam 优化器在小型分类任务上的稳定起点如果 loss 震荡不下降可以降到3e-4。num_workers0是为了避免 Windows 上多进程加载数据的报错Linux 可以调到 4 加速。epoch 设 10 对验证码任务来说够了超过 15 个 epoch 一般就开始过拟合训练集 acc 接近 100% 而测试集不再提升。4.3 代码predict.py 推理脚本怎么给脚本传图片和输出结果# predict.py # 命令行推理python predict.py --image test.png [--weights captcha_net.pt] import argparse import torch from preprocess import preprocess_and_split from train import CaptchaNet # 直接复用训练脚本里的模型定义 def predict(image_path, weights_path): model CaptchaNet() model.load_state_dict(torch.load(weights_path, map_locationcpu)) model.eval() chars preprocess_and_split(image_path) # 加一个 batch 维度形状从 (4, 1, 28, 28) 变成 (1, 4, 1, 28, 28) x torch.from_numpy(chars).float().unsqueeze(0) with torch.no_grad(): logits model(x) pred logits.argmax(dim-1).squeeze(0) result .join(str(int(c)) for c in pred.tolist()) return result if __name__ __main__: parser argparse.ArgumentParser(description验证码识别推理脚本) parser.add_argument(--image, requiredTrue, help待识别的验证码图片路径) parser.add_argument(--weights, defaultcaptcha_net.pt, help模型权重路径) args parser.parse_args() result predict(args.image, args.weights) print(f识别结果: {result})推理脚本的--image参数是我最常用的交互方式因为验证码识别在真实场景里往往是批量的而批量环境里经常会用 Shell 脚本循环调用 Python。例如在一个目录里跑for f in ./test/*.png; do python predict.py --image $f; done这样就能把所有图片的识别结果串起来。map_locationcpu确保模型在有 GPU 的机器上训练、在纯 CPU 的机器上也能加载这一点在部署到服务器时特别重要。4.4 训练效果怎么看准确率、误识率和模型选择依据训练日志里有两个数字需要注意loss 和 acc。loss 是交叉熵损失反映模型对训练数据的拟合程度acc 是我计算的整串准确率即四位数字全部预测正确的比例。整串准确率这个指标更贴近业务需求因为验证码识别场景里只要有一位数字错了整串结果就不能用。合成数据的整串准确率一般能做到 95% 以上剩下的 5% 大多集中在字符模糊和干扰线压住字符的情况。如果你的目标是 99%单纯堆训练数据不如修预处理。比如把生成器的干扰线宽度从 1 改成 2让模型见过更难的样本效果比盲目把数据量翻倍更明显。模型选择上我习惯保留训练过程中准确率最高的一轮权重而不是最后一轮。做法是在训练循环里判断epoch_acc是否大于历史最高值是就保存一份备份。这能避免最后一轮恰好落在过拟合区间导致权重变差。5. 识别脚本落地最容易踩的五个坑从预处理到模型部署5.1 坑一源图背景噪声强预处理越用力识别越差现象训练时用生成器造的干净图准确率超过 95%一到真实图片就跌到 60% 以下。于是开始加各种预处理什么中值滤波、形态学开闭运算、对比度拉伸全往上堆结果识别率不涨反跌。原因验证码的干扰设计远复杂于普通噪声过度的滤波操作把字符边缘的细节抹掉了。比如验证码字符有轻微的笔画渐变中值滤波会把这种渐变当噪声削平字符的区分度反而下降。生成器的图片和真实图片的分布差异是预处理参数失效的根本原因。解决回到“最小预处理”原则只保留灰度化和 OTSU 二值化。如果字符边缘有锯齿可以加一次GaussianBlur(radius0.5)平滑但 radius 超过 1 基本就会丢信息。每次改动预处理都重新跑一遍测试集用数据说话不要凭感觉加步骤。5.2 坑二字符粘连导致固定切分全废现象固定宽度切割在训练集上表现良好但真实验证码里两个字符连在一起切割线正好切在笔画中间单个字符成了残废分类准确率断崖式下降。原因固定切割的前提是字符位置规整、间距稳定。真实验证码如果字符宽度不一致或者有轻微重叠固定间隔的切割方式必然出错。我在生成数据时把间距设为固定 24训练集永远干净但真实数据不按这个规则来。解决当固定切割失败时不要急着换 CRNN先尝试一个折中方案——滑动窗口切分。在预期的切割点附近左右平移 3 到 5 个像素生成多个候选切割版本分别送进模型预测取置信度最高的组合作为最终结果。这个办法实现成本很低代码只需要一个 for 循环很多场景下拉一把精确率就能回来。5.3 坑三合成数据太干净真实验证码一个都识别不了现象生成器造的数据训练出来准确率很高但拿真实图片测试时几乎全军覆没。字体稍微变一下、加了背景纹理、字符颜色变了模型就完全不认。原因生成器和真实验证码的风格差异太大。字体、字号、颜色、背景纹理、干扰方式这些都是模型会记住的特征如果训练分布里缺少这些变化模型学到的不是一个泛化的字符识别器而是一个“见过哪几种图”的匹配器。解决生成器要主动加变化。字体至少准备两种字符颜色从纯色改成渐变色背景从纯白改成浅色纹理干扰线宽度从 1 改到 2旋转角度从固定值改成-15~15度随机。数据增强里还可以加MixUp把两张不同标签的图和它们的标签按比例混合逼迫模型学习更本质的字符特征。代价是训练时间变长但这是值得的泛化能力是验证码识别脚本能活多久的分水岭。5.4 坑四把模型当成通用 OCR碰见点选、滑块验证码就失效现象把训练好的四位数字识别脚本扔到另一个项目里结果对方用的是滑块验证码模型输出一堆数字完全没有可用信息。原因滑块验证码的本质是“找缺口”不是字符识别。数字识别模型只学会了字符空间到标签空间的映射它对图像里的几何形状、缺口位置没有建模能力。验证码识别不是一个统一的算法而是一类任务的总称不同形态必须用不同方案。解决每次拿到新需求先问一句这个验证码的验证信号是什么数字验证码的验证信号是字符内容滑块是缺口位置点选是目标物体坐标。信号确定了技术再选型。这里的血泪教训是不要试图用一个模型打天下每个场景单独训一个专用模型才是常态。5.5 坑五合规与边界问题什么时候这套脚本才允许用现象把识别脚本直接接到线上正式环境的登录接口上跑批量刷数据结果触发风控不仅脚本失效目标系统还可能被拉黑。原因验证码识别技术本身是中立的但使用场景有边界。对没有授权的系统做自动化绕过这属于规避风控措施的行为在线上的真实业务环境里使用这种脚本本身就处于灰色地带。解决这类脚本的正确使用场景有三个。第一个是自建测试环境比如本地搭一个带验证码的测试系统用来验证识别算法效果第二个是拿到明确授权的安全测试在授权范围内验证目标系统的防护强度第三个是教育与研究在隔离的样本上做算法实验。我的习惯是在任何项目开始前先确认授权边界这个步骤能让自己避开绝大部分风险。风控对抗也是一个无底洞投入大量精力去对抗真实系统的反制措施既不稳定也不划算。6. 进阶批量预测、置信度过滤和一段抢救数据的经验谈当单个脚本跑通之后下一步就是让它批量化地工作。常见需求是对一个目录里的几百张验证码图片批量识别结果输出为一行一个文件名的格式方便后续和其他脚本对接。做法不复杂用 Python 的os.listdir遍历目录或者更直接一点用 Shell 的 for 循环逐个调用predict.py。批量预测的耗时主要花在图片读取和模型前向上CPU 上一张图大约几十毫秒几百张图只需要十几秒完全在可接受范围内。另一个值得做的小技巧是置信度过滤。训练脚本里只输出了整串准确率但推理时我们拿得到每个字符的 softmax 概率。当 4 个字符里有一个置信度低于 0.8说明这张图大概率是模型没见过的样式这时与其盲目输出错误结果不如在脚本里加一个--min-conf参数低于阈值就输出一条标记而不是结果这样后续流程可以针对性地补数据或重新识别。这就是黑匣子之外的一点可解释性。最后分享一个习惯我每次改数据生成器或预处理代码之前都会先记录当前的训练准确率和测试准确率差 1% 就值得犹豫差 3% 以上基本可以断定改动方向有问题。验证码识别项目的偶然性很强有些参数改完效果提升单独看也说不清是哪个因素起作用保留好每次实验的配置就是后悔药。这套方案从数据生成到部署大约花一个下午就能跑完成本不高但通用性很好——换一种字体、换一组字符集、换一套干扰方式只需要改生成器和标签类别数量模型结构可以原封不动用下去。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进