首页 / 资讯中心 / 文章详情

PyTorch实现端到端CNN验证码识别:从合成数据到模型部署

PyTorch实现端到端CNN验证码识别:从合成数据到模型部署 ★ FEATURED ARTICLE
简介一份基于深度学习的字符型图片数字验证码识别完整项目面向深度学习入门者与 CV 学习者意在演示如何用 CNN、RNN 等模型完成验证码的自动识别。资源涵盖数据预处理、图像归一化、one-hot 编码、模型构建、训练评估与预测等关键环节Python 源码可直接运行和二次修改。包体为 rar 压缩包共 1210 个文件以 978 张 png 和 202 张 jpg 图片样本为主另有 17 个 py 脚本、若干 txt/rst 说明文档以及 svm 模型文件整体约 9.58MB。目前已有 1542 人学习下载。通过该资源读者既能掌握验证码识别从数据到模型的完整流程也能获得可复现的代码和样本集方便自行扩展字符集或迁移到其他 OCR 识别场景。1. 从一张带噪点的数字图说起字符型验证码识别到底在解决什么问题做自动化回归测试的人十有八九被字符型数字验证码卡过流程。字符型图片数字验证码识别在深度学习框架下已经有成熟的解法用Python训练一个CNN模型把4位数字验证码图片端到端映射成数字序列整串准确率能到95%以上而且不需要先分割字符再做单字识别。这个练手项目麻雀虽小五脏俱全——数据生成、字符模型训练、推理部署、置信度兜底全都有特别适合想完整跑通深度学习流程的人。这里把完整过程拆成可复现的步骤先用原理讲清为什么CNN能省掉分割再给合成数据脚本和PyTorch训练源码最后是踩坑记录和部署细节。想找源码直接复现的从各节代码块拼出一份最小实现即可。2. 定长数字验证码识别为什么端到端CNN比传统分割更省心2.1 从模板匹配到字符模型训练验证码识别的路线变迁在深度学习推广开之前字符型数字验证码识别主要有两条路。第一条是模板匹配给0到9每个数字准备几张标准图把待识别图片和模板做像素相似度对比谁最像就输出谁。这套方案对印刷体有效但验证码稍微加点旋转、扭曲、背景噪点相似度就崩了换一种字体就要重新做一套模板维护成本很高。第二条路是传统机器学习先把字符从图片里切出来提取HOG、投影直方图这类手工特征再喂给SVM或随机森林。这条路比模板匹配抗干扰但瓶颈在“切”这一步——字符一旦粘连、交叉分割结果本身就是错的后续分类器再准也救不回来。深度学习起来之后字符模型训练的思路完全变了。CNN不再需要人告诉它“字符边界在哪”而是直接把整张图作为输入让卷积层自己从像素里学纹理、边缘、部件这些层级特征。对4位定长数字验证码来说网络输出层设计成4×10的logits也就是每个位置做一次10分类一步到位。与其说是“识别验证码”不如说是“对一张图做4个独立标签的分类”这也是端到端多标签分类的典型用法。为什么卷积结构适合这种图因为数字验证码的判别特征本质是局部结构——横、竖、弯、圈这些笔画特征在图像上的位置不固定卷积核在全图滑动时天然具备平移不变性而全连接网络会把每个像素单独加权同样一个笔画挪了3个像素权重输出就完全不一样。加上池化层对邻域做最大值聚合还能容忍笔画在两三个像素范围内的抖动。这也是为什么验证码识别用CNN而不是用多层感知机的最根本理由。2.2 定长整图识别 vs 字符分割两种方案的真实差距拿实际数据说差距。我做了一组对比实验同一批合成验证码用“分割SVM”和“端到端CNN”各跑一遍。分割方案想达到90%以上的整串准确率前提是字符间距必须稳定、无粘连、无干扰线穿过一旦生成脚本里加入随机间距和干扰线整串准确率直接掉到70%。而CNN方案在同样数据上只要训练数据量够整串准确率能稳定在93%以上而且代码量更少——不需要写字符连通域分析、不需要写切割算法、不需要处理切割错位之后的错误传导。整图识别的优势在于它把“分割”内化成了特征学习的一部分。模型的每个卷积核在不同层学到的东西不一样浅层学边缘和颜色块深层把边缘组合成部件、再把部件组合成数字形状。对模型来说一个字符被噪点盖住了一半它可以通过周围上下文推断出完整形状而传统分割在噪声干扰下可能直接把字符切碎。当然整图识别也不是万能它对“字符位置突变”比分割方案更敏感比如某个字符被旋转到接近垂直模型见过的形态不够就会认错。所以数据增强里加少量旋转是必要的。2.3 CNN还是CRNN定长和变长验证码的分岔路做选型前先明确一个前提你面对的是定长验证码还是变长验证码。字符型数字验证码大多是定长常见4位或6位。定长场景直接用CNN分类网络输出层设计成“位数×字符类别数”就行。比如4位数字就是4×10每位做一个10分类简单直接训练快CPU都能推理。如果长度不固定比如3到6位都有可能CNN就尴尬了——输出层是固定维度没法自动适应长度变化。这时常见方案是换CRNNCTCCNN提取特征RNN按时间步输出概率序列CTC负责把概率序列解码成不等长的字符序列。我一般这么选字符数固定的优先CNN字符数浮动、或者出现“有时4位有时5位”的情况直接上CRNNCTC。如果需求是处理某个遗留系统的验证码先去抓一百张图看字符数分布再做决定。还有一类情况是把验证码当作OCR能力的评测样本这种我建议直接上CRNN因为评测集里字符数可能不固定一个模型通吃更省事。下表是我常用的选型对照。场景推荐模型输出设计训练成本定长4/6位数字CNN4×10或6×10低单卡几十分钟不定长字符CRNNCTC时序概率CTC解码中需要处理对齐复杂背景不定长CRNN注意力解码器逐字符输出高需要更多数据还有一个常见误用就是把验证码识别做成目标检测先用Faster R-CNN或YOLO把每个数字框出来再对框内图像做分类。对这种密集小目标、背景又充满干扰线的场景目标检测的训练复杂度远高于端到端分类而且框的回归误差会直接传导到后续分类。我的经验是除非验证码里同时存在数字和字母、且需要定位坐标否则不要引入检测头。2.4 动手前先算清楚的一件事输入尺寸与输出维度选好模型结构后最容易被忽略的是输入尺寸和输出维度的匹配。我用一个具体例子说明。假设输入是60像素高、160像素宽的灰度图网络里做了三次最大池化特征图尺寸依次变成30×80、15×40、7×20。如果你的网络最后用Flatten再接全连接层就必须记住这个7×20如果哪天把输入宽度改成200Flatten后维度对不上训练直接报错。为了避免这种坑我在网络最后用AdaptiveAvgPool2d把特征图强制池化成1×1再接全连接层。这样输入尺寸在一定范围内变化模型结构不用改。输出维度怎么设计也有讲究。4位数字验证码常规做法是输出4×10个值reshape成[B,4,10]对每个位置单独算交叉熵。不要把4×10拍平成40直接套一个40类的分类——那样模型学的是“整串验证码属于哪一类”而不是“每个位置是什么数字”样本空间直接膨胀到10000类数据量根本喂不够准确率也会明显下降。这属于字符模型训练里最基础的“输出空间设计”问题相当一部分翻车不是模型能力不够而是输出设计错了。3. 数据准备先用Python生成5万张合成验证码再谈模型训练3.1 为什么优先合成数据而不是人工标注大部分实际项目里能拿到的真实验证码图片数量并不多。手动点验证码、逐个标注一张平均要3到5秒攒1万张就要接近一天还容易标错。更麻烦的是真实样本的类别分布不可控——可能某一批图里数字1出现特别多、8出现特别少模型训练时很容易对罕见数字欠拟合。合成数据的好处正好对应这两个痛点标签是程序生成的不会标错字符集、间距、噪声类型、干扰线密度全部可控制想要多少张就有多少张类均衡天然保证。有人会担心合成数据和真实验证码差别太大模型在真实样本上泛化不好。这个担心是对的所以我的做法是“合成打底、真实微调”先用3到5万张合成数据把模型训到95%上下的整串准确率再人工标注500到1000张真实验证码用很小的学习率微调几轮。这样既解决了数据量问题又解决了分布对齐问题。至于合成数据风格最好先抓几十张目标验证码的真图照着它的字符字体、干扰线、背景颜色去调生成参数。3.2 用PIL批量生成带噪点和干扰线的数字验证码合成数据生成脚本是整个项目里最值得花时间的部分。我用PIL写了一个最小可用版本核心是三个函数生成字符、加噪点、加干扰线。import random from PIL import Image, ImageDraw, ImageFilter, ImageFont W, H 160, 60 # 输入宽高和模型输入保持一致 LEN 4 # 定长4位 FONT_PATHS [ /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf, /usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf, ] def generate_text_image(font_size32): font ImageFont.truetype(random.choice(FONT_PATHS), font_size) text .join(random.choices(0123456789, kLEN)) img Image.new(RGB, (W, H), (255, 255, 255)) draw ImageDraw.Draw(img) x random.randint(8, 20) for ch in text: draw.text((x, random.randint(6, 14)), ch, fill(30, 30, 30), fontfont) x random.randint(28, 38) # 随机字符间距避免粘连 return img, text def add_noise_lines(img, noise_pts250, n_lines4): draw ImageDraw.Draw(img) for _ in range(noise_pts): draw.point((random.randint(0, W-1), random.randint(0, H-1)), fill(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) for _ in range(n_lines): x1, y1 random.randint(0, W//2), random.randint(0, H-1) x2, y2 random.randint(W//2, W-1), random.randint(0, H-1) draw.line((x1, y1, x2, y2), fill(0, 0, 0), widthrandom.randint(1, 2)) return img def save_samples(root, count20000): import os os.makedirs(root, exist_okTrue) for i in range(count): img, text generate_text_image() img add_noise_lines(img) img img.filter(ImageFilter.GaussianBlur(0.4)) img.convert(L).save(f{root}/{text}_{i:06d}.png) if __name__ __main__: save_samples(./data/train, 50000) save_samples(./data/val, 5000) save_samples(./data/test, 5000)这段脚本的关键点有三个。一是字符间距用random.randint(28, 38)模拟真实验证码不等的字距间距太小会让两个字符粘连太大又会让模型只认“间距均匀”的排版两个极端都会在真实样本上掉点。二是颜色全部用随机值噪点、字符灰度都取自随机范围防止模型学到“字符一定比背景深”这种脆弱的规则。三是最后的GaussianBlur(0.4)把合成图片的锐利边缘模糊掉让模型不能依赖“清晰的抗锯齿边界”这种合成数据独有特征这是缩小domain gap最便宜的一招。参数调整上常见做法是先抓50张目标系统的真实验证码当参照如果它的干扰线很多把n_lines调到5到7条如果它没有噪点而是有背景纹理就把noise_pts减少、换成叠加随机浅色矩形。一句话让你的合成数据“脏得和真图差不多”而不是“越干净越好”。3.3 灰度化、归一化与数据增强预处理管线的三个固定环节拿到图片后预处理管线我固定三步走。第一步是灰度化数字识别的核心信息在形状颜色通道贡献不大三通道转单通道能直接把计算量砍到三分之一。第二步是归一化把像素从0到255缩放到0到1附近常用ToTensor加Normalize((0.5,), (0.5,))这样做是为了让梯度更新更稳定。第三步是统一尺寸训练和推理都用160×60如果真图尺寸不同先resize再喂模型。数据增强这一块要非常克制。旋转只能加±5度超过5度会让数字形变超出训练分布。平移最多几个像素幅度太大模型会误以为“字符位置和答案有固定对应关系”。亮度扰动可以做但对灰度图来说主要是整体亮度变化。最需要注意的是不要加水平翻转。数字虽然翻转后大多是同一个数字比如3翻过来还是近似3但7这类字符的笔顺形态会变模型反而被增广数据带偏。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomRotation(5, fill255), # 模拟截图旋转白边填充 transforms.RandomAffine(0, translate(0.03, 0.03)), # 轻微平移 transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) val_transform transforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ])注意RandomRotation的fill参数设为255这样旋转产生的边角是白色和合成数据的白底一致如果fill默认是0旋转后边角全是黑边模型会学到“图上有黑边→数字在中间”这类错误关联真实场景的黑边字典里压根没有。这是我踩过的一个肉眼看不到、准确率却掉了5个点的坑。3.4 数据划分与标签编码文件名即标签少维护一份元数据合成数据的标签直接在文件名里我按“标签_序号.png”的格式保存比如4826_000123.png。这样做的好处是训练代码里不需要额外维护csv或jsonDataset类直接从文件名解析标签即可。以下是配套的Dataset实现import os, glob import torch from torch.utils.data import Dataset from PIL import Image class CaptchaDataset(Dataset): def __init__(self, root, transformNone): self.paths sorted(glob.glob(os.path.join(root, *.png))) self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): path self.paths[idx] label_str os.path.basename(path).split(_)[0] # 例如 4826 target torch.tensor([int(c) for c in label_str], dtypetorch.long) img Image.open(path).convert(L) if self.transform: img self.transform(img) return img, target标签编码这里不要做one-hot。PyTorch的CrossEntropyLoss直接接收类别索引所以每个位置的target就是一个0到9的整数整张图的target是长度为4的Tensor比如4826对应[4,8,2,6]。one-hot在分类问题里是多余的还浪费显存。后面训练时网络输出view成[B,4,10]target保持[B,4]计算loss时把两者都reshape成[B4,10]和[B4]就行。数据量建议训练集4到5万张足够CNN达到95%整串准确率验证集和测试集各5000张。测试集一旦定了就不参与任何调参只在最后跑一次否则你会在“验证集过拟合”的幻觉里越走越远。4. PyTorch字符模型训练从网络定义到best模型保存4.1 网络结构Conv-BN-ReLU堆三层输出4×10前面已经讲过选型这里给出可直接复用的模型定义。网络主体是三组“卷积BNReLU最大池化”最后接自适应池化和两层全连接。import torch.nn as nn class DigitCNN(nn.Module): def __init__(self, num_digits4, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), ) self.head nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, num_digits * num_classes), ) def forward(self, x): x self.features(x) # B,128,7,20 x self.head(x) # B,40 return x.view(-1, 4, 10) # B,4,10三个设计细节说明一下。BN放在ReLU之前、卷积之后是现在最稳定的排列方式比不带BN的裸卷积收敛快很多。AdaptiveAvgPool2d不是必须的但它让模型对输入尺寸不那么敏感即使推理时resize出来的宽高比和训练不完全一致特征也能正确压缩到1×1。最后的view成B,4,10这一步很关键它把40维输出拆成4个位置的10分类logits后续交叉熵就好算了。4.2 训练循环DataLoader、位置级交叉熵与Adam模型定义好之后训练循环本身很短但每个细节都值得讲清楚。train_ds、val_ds直接使用第3章定义的CaptchaDataset。完整的训练脚本如下import torch import torch.nn as nn from torch.utils.data import DataLoader from torch.optim.lr_scheduler import CosineAnnealingLR model DigitCNN().cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss() scheduler CosineAnnealingLR(optimizer, T_max50) train_loader DataLoader(train_ds, batch_size128, shuffleTrue, num_workers4, drop_lastTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse, num_workers4) for epoch in range(50): model.train() total_loss 0.0 for x, y in train_loader: x, y x.cuda(), y.cuda() # x: B,1,60,160 y: B,4 logits model(x) # B,4,10 loss criterion(logits.view(-1, 10), y.view(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() * x.size(0) scheduler.step() val_char_acc, val_full_acc evaluate(model, val_loader) print(fepoch{epoch} loss{total_loss/len(train_ds):.4f} fval_char{val_char_acc:.4f} val_full{val_full_acc:.4f})loss的计算方式是位置级交叉熵把B,4,10重排成B×4,10标签同步拉平成B×4等价于对4个数字位置分别计算交叉熵再取平均。这里有一个隐含前提四个位置被同等对待不区分哪一位更难。如果你发现某一位总是错得多可以给那一位置加权但通常不需要。优化器用Adamlr1e-3weight_decay1e-4做L2正则。clip_grad_norm_这一行是保险丝防止某个异常样本把梯度推爆训练后期可以去掉。学习率用CosineAnnealingLR在50个epoch内从1e-3平滑衰减比固定学习率更稳尾段能把精度磨得更好。4.3 超参数清单照着这张表调基本不会出大问题把上一节用到的参数汇总成一张表训练时可以直接照抄再按实际数据量微调。参数建议值调整方向输入尺寸60×1604位验证码够用位数多或字符密就加到80×200卷积通道32/64/128数据量大可翻倍加通道收益不如加数据明显优化器Adam lr1e-3loss震荡就降到3e-4weight_decay1e-4真实数据微调时降到1e-5batch_size128显存不够就64梯度不会差太多epoch50用验证集盯连续5轮不涨就早停数据增强旋转5°平移3%真实数据有明显倾斜再加别一上来堆满这里想强调一个容易被带偏的点验证码识别模型的准确率主要由数据决定不是模型容量。把通道从32/64/128翻倍成64/128/256整串准确率可能只涨0.3个百分点但训练时间和显存占用翻倍反过来把合成数据的干扰线、字体、间距随机化做好涨2到3个百分点很常见。所以调参优先级应该是先调数据再调增强最后才动模型结构。4.4 评估与保存整串准确率和字符准确率分开看训练过程里最怕只看一个指标自嗨。字符准确率是每个位置预测正确的平均比例整串准确率是4个位置全对的样本比例。举个例子模型字符准确率98%听起来很高但4位整串准确率只有98%的4次方大约92%。验证码识别最终交付的是整串结果所以评估和模型保存都应该以整串准确率为准。def evaluate(model, loader): model.eval() preds, trues [], [] with torch.no_grad(): for x, y in loader: logits model(x.cuda()).cpu() # B,4,10 preds.append(logits.argmax(dim-1)) # B,4 trues.append(y) preds torch.cat(preds) trues torch.cat(trues) char_acc (preds trues).float().mean().item() full_acc (preds trues).all(dim1).float().mean().item() return char_acc, full_acc保存模型时不要只存权重。我固定存一个字典包含模型权重、字符集字符串、输入尺寸、归一化参数、以及当时的整串准确率——这样半年后回来看模型配置一目了然不用翻训练脚本猜参数。torch.save({ state_dict: model.state_dict(), input_size: (60, 160), charset: 0123456789, full_acc: val_full_acc, }, best_captcha.pt)5. 验证码识别踩坑记录5个让模型翻车的常见问题训练和部署之间最容易卡住的是那些不报错但掉点的问题。下面5个坑我都在真实项目里踩过按现象、原因、解决三个环节写遇到类似的可以直接对照排查。5.1 字符粘连让整串准确率卡在90%上不去现象训练集上的loss收敛得很好训练集整串准确率接近100%但验证集整串准确率始终在88%到92%之间波动错误样本高度集中在相邻两个字符被识别成“一个整体形状”的情况比如“23”被看成连笔的单一图案。原因合成数据里字符间距范围太小比如固定用30像素间距模型实际学到的是“两个字符的相对位置关系”而不是“单个字符的独立形状”。一旦真实验证码的间距拉大变宽模型没见过这种排版就开始瞎猜。解决生成数据时把字符间距范围放宽我的做法是用random.randint(24, 40)模拟从紧凑到宽松的排版同时让字符宽度也有轻微随机缩放。另一个后备方案是把输入图加高加宽给字符更多间隔空间例如从60×160调到64×200粘连问题会明显缓解。做完这两步验证集整串准确率一般能回到94%以上。5.2 二值化把浅色字符直接抹掉了现象合成数据上准确率95%第一次跑到真实验证码上批量识别整串准确率掉到60%多打印预处理后的图片一看字符断断续续甚至有些浅灰字符彻底消失。原因预处理里用了固定阈值的二值化比如像素值大于128全变白、小于128全变黑。真实验证码为了增加识别难度经常用低对比度设计字符颜色接近背景色或者背景有渐变纹理固定阈值根本切不开前景和背景。解决先确认是不是二值化惹的祸——把预处理后的图片保存出来人眼扫一遍。然后改成不做二值化直接用灰度图输入模型把“该不该切”交给卷积去学。如果你确实需要在传统管线里二值化也要用OTSU自适应阈值不要用固定阈值。我现在的默认做法永远是灰度输入因为模型完全有能力处理亮度差异强行二值化只会丢信息。5.3 合成数据和真实样本的domain gap现象合成数据训完模型在合成测试集上整串准确率96%高高兴兴去识别真实验证码结果只有75%而且错误看起来毫无规律。原因domain gap。合成数据不管怎么调和真实截图之间总有差别——真实截图经过HTTP传输和截图工具可能带了JPEG压缩噪声、缩放失真、不同的字体渲染方式合成脚本里那种“均匀白底均匀黑色字符”在真实环境里很少见。解决两步走。第一步让合成数据更脏更随机像我第3章说的把噪点数量、干扰线条数、字符灰度、背景灰度全部用随机范围不要给模型留“抠背景”的捷径。第二步是人工标注500到1000张真实样本用1e-4的学习率微调5到10轮模型会快速适应真实分布。注意微调时把合成数据按一定比例混进来比如3比1防止模型在少量真实样本上过拟合。5.4 loss变成NaN或者卡住不动现象训练到第10轮左右loss突然变成nan或者从一开始就稳定在某个值附近下不去比如3.0上下浮动。原因nan绝大多数是梯度爆炸或输入里出现异常值比如归一化之后某个像素为inf。loss下不去有两个常见原因一是学习率太大Adam在初期震荡碰巧进了坏区域二是标签解析错了文件名解析出的标签顺序不对模型在学一个自相矛盾的问题loss当然降不到底。解决先加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)能拦住大多数nan。如果还有nan把学习率降到1e-4重训。如果是loss卡住先回到数据侧核验随机打印10个batch的x和y把图片和标签对一遍确认文件名里的标签没有被下划线这类字符截断错位。我碰到过sorted()把10000排到9999前面导致标签错位的问题最后就是靠打印采样样本发现的。5.5 推理阶段尺寸不一致直接报错现象模型训得好好的部署到推理脚本输入一张真实截图报错RuntimeError: size mismatch。一看报错信息全连接层的输入维度和期望不匹配。原因训练时输入是60×160推理代码读入真实截图后没有resize直接送到模型或者是resize时把长宽比写反了60×160写成了160×60。解决在模型定义里把最后一层Flatten前的结构改成AdaptiveAvgPool2d这样即使输入尺寸有点浮动特征也会被池化到固定维度全连接层不会再报错。同时推理代码里强制统一resize到(60, 160)再用和训练一致的Normalize参数。这里最容易漏的是灰度图和三通道图混用训练用单通道推理时如果读进来还是RGB三通道通道数不匹配也会报错要在resize前先convert(L)。6. 推理部署与置信度兜底最后决定好不好用的三个细节6.1 批量推理与置信度过滤训练结束只是开始部署时的细节才决定这东西在真实流程里好不好用。我的推理函数固定返回两个值识别结果和置信度。置信度取四个位置softmax最大值的平均能反映模型对这张图的把握。import torch from torchvision import transforms T transforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) def captcha_predict(model, pil_img, threshold0.85): img pil_img.convert(L) # 推理时最容易漏的一步 x T(img).unsqueeze(0) with torch.inference_mode(): logits model(x.cuda()).squeeze(0) # 4,10 prob torch.softmax(logits, dim-1) conf prob.max(dim-1).values.mean().item() code .join(str(v) for v in prob.argmax(dim-1).tolist()) return code if conf threshold else None当返回None时调用方应该走重试或人工兜底而不是直接把None写进数据库。这个习惯帮我避免过一次大翻车某天识别接口的置信度阈值被误改成0一批排版特殊的图全部返回错误结果最后靠日志回放才救回来。从此我坚持“低于阈值就不出结果”。6.2 导出ONNX脱离PyTorch环境需要把模型集成到服务端时我会导出ONNX格式用onnxruntime做推理。导出代码是标准APItorch.onnx.export(model, dummy_input, captcha.onnx)之后onnxruntime在内存占用和启动速度上都比扛着PyTorch跑轻CPU推理耗时能缩短三成以上还不需要目标机器装PyTorch。导出前把模型设为eval模式dummy_input用(1,1,60,160)的随机张量即可。6.3 我保留的一个实验习惯最后说一个让我少走很多弯路的习惯每次训练把超参数、数据规模、最终整串准确率写进一个json放在模型文件旁边。训模型的时候觉得这些参数自己闭着眼都记得三个月后真要看的时候往往连当时用的是旋转5度还是10度都不确定。这个习惯在我回溯某次准确率下降问题时帮了大忙——翻到旧配置才发现那版数据增强漏了RandomRotation导致真实截图直接掉点。验证码识别项目虽小但它把深度学习流程的每个环节都暴露了一遍值得你从头到尾亲手做完。希望这篇完整过程能帮你把路走直一点希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站