简介:面向深度学习学习者的字符型数字验证码识别完整实践包,也适合毕业设计或课程设计参考,旨在帮助读者系统掌握图像分类与序列建模相结合的项目开发流程。资源共1210个文件,以近1200张包含颜色、扭曲和噪声差异的png/jpg验证码样本图像为主,配有Python训练脚本、文本说明、SVM模型文件以及展示HTML等辅助材料,压缩包整体仅9.58MB。目前已有1542人学习,可作为从理论到工程落地的有效对照案例。内容覆盖验证码样本的归一化与数据增强、one-hot标签编码、基于CNN的图像特征提取、LSTM/GRU循环神经网络序列建模、softmax分类器输出以及交叉熵损失与Adam优化器训练等完整环节,并附可直接运行的源码、测试样例和已保存模型,方便快速验证效果。项目目录结构清晰,从数据预处理、模型构建、训练评估到最终识别调用均有对应模块,读者可按步骤复现并迁移到类似OCR识别任务中。
1. 字符型图片数字验证码识别:深度学习方案到底解决什么问题
字符型图片数字验证码识别,就是把一张带有数字的图片,用深度学习模型自动读出里面的数字串。登录、注册、自动化测试这些场景里,验证码是最常见的拦截关卡,传统OCR方案一遇到扭曲、干扰线和字符粘连就掉链子,而深度学习方案——通常是卷积神经网络——直接端到端地学习"图片到数字"的映射,不需要先分割字符,识别率能稳定拉到99%以上。这篇文章适合两类人:一类是用Python做自动化流程、被验证码卡住、想找一套完整方案的工程师;另一类是刚入门深度学习、手里缺一个能跑通的实战项目、想从数据到训练到部署完整走一遍的新手。文中会给出可复现的Python源码、关键参数和避坑记录,照着做就能跑起来。
2. 识别思路与模型选型:数字验证码为什么首选CNN而不是传统OCR
2.1 识别任务的本质:定长数字串是多标签分类
先把手里的问题定义清楚。数字验证码通常是4位或6位,长度固定,字符集只有0到9共10个数字。这种任务在深度学习里不叫多分类,而叫多标签分类:一张图要输出4个预测,每一位对应一个10分类问题。以4位验证码为例,模型的设计就是共享同一套特征提取网络,后面接4个并列的分类头,每个分类头输出10个类别的概率,概率最大的那个数就是当前位的预测结果。
这种定式的好处是结构简单,训练稳定。如果你接触过MNIST手写数字识别,只需要把最后的单分类头换成多分类头,就有了一个能跑的验证码识别网络。很多初学者一上来就想做"不定长识别",直接上CRNN加CTC,对固定长度的数字验证码属于过度设计,训练复杂度和调参成本都翻倍。我一般建议先确认手里验证码到底是不是固定长度,绝大多数内部系统都是固定4位数字,用多标签分类半天时间就能训练到能上线的水平。
把它命名为"数字模型"或者"字符模型",本质上都是同一件事:用卷积网络做特征提取,用全连接层做分类。固定长度场景里,不需要引入序列模型。
2.2 传统OCR为什么在验证码上翻车
在深度学习普及之前,OCR的主流流程是:图像预处理、灰度化、去噪、二值化,再分割出单个字符,最后用模板匹配或SVM这类分类器逐字识别。这套流程在印刷体文档上效果不错,但搬到验证码上就处处碰壁,最致命的是分割环节。
验证码的设计目标就是"让人难认、让机器更难认",字符之间通常有粘连、倾斜、重叠,还穿插干扰线。一旦字符分割不干净,后续识别就没有任何意义。就算侥幸把字符切开了,模板匹配对旋转、缩放、颜色变化非常敏感,同一个数字在不同验证码里可能长得完全不一样。传统OCR在这个任务上的识别率普遍在70%到85%之间,实际使用中经常要靠多次重试才能通过。
深度学习方案把"先分割再识别"改成"端到端识别":卷积神经网络直接从原始图片里提取特征,模型自己学会区分每一位数字。分割动作被卷积核和池化层隐式完成,不再依赖一个显式的、会出错的步骤。这也是它识别率能超过99%的根本原因——整个问题是一体化优化,而不是多个阶段的串联,误差不会逐级放大。
2.3 CNN架构怎么选:从LeNet到ResNet
网络结构的选择取决于验证码的复杂度,这里没有银弹,但有一个清晰的优先级。
最简单的情况——白底、纯数字、无干扰线、字符规整——用LeNet-5就够了。LeNet-5是1998年的经典卷积网络,两个卷积层加三个全连接层,参数量不大,CPU上推理毫无压力。用到验证码识别上,只需把最后的单分类层改成多个分类头,识别率就能达到99%以上。
中等难度——带轻微扭曲、细干扰线、颜色有变化——用三到四层卷积加全连接就够。这里有个经验值:验证码图片分辨率通常只有60像素高、160像素宽,输入尺寸很小,太深的网络经过几次下采样后特征图直接消失,反而容易过拟合。如果字符集扩大为数字加字母,36类甚至62类,我倾向于在卷积层后加Dropout,让模型不至于把训练集里背景噪声也记住。复杂场景才需要上ResNet这类残差结构,残差连接能缓解深层网络梯度消失的问题,代价是训练时间和显存占用成倍增加。先浅后深、以验证集准确率为准绳,是最省时间的路线。
三者的对比可以这样记:定长多标签CNN适合固定位数,训练快、部署简单;CRNN加CTC适合不定长,但是调参难度高;传统OCR适合通用文档,在验证码上先天不足。
2.4 不定长验证码:CRNN加CTC的适用范围
如果验证码长度不固定,比如有时候4位有时候6位,多标签分类就不好使了。这时候主流的做法是CRNN加CTC:卷积网络提取空间特征,特征序列送入双向LSTM,最后用CTC损失函数完成输入序列和输出序列的对齐。CTC会引入一个"空白"标记,允许网络输出任意长度的序列,通过动态规划找到概率最高的字符路径。训练时不需要标注每位字符在图中的具体位置,只需要告诉模型"这张图里是1234"就够了。
CTC方案有个明显的学习曲线:学习率稍微调大就容易不收敛,序列长度的设定也必须和图片宽度匹配,否则会出现重复字符或者漏识别。如果验证码本身长度固定,就没必要引入这层复杂度。最常见的做法是把前面说的定长多标签方案作为首选,把CRNN加CTC留给真正需要不定长识别的项目。
后面所有代码都围绕定长数字验证码展开,这是性价比最高的起点,也是最容易让新手建立完整闭环的路径。
3. 数据准备与预处理:从验证码图片到可训练数据集
3.1 数据从哪里来:程序生成是新手最稳的路径
深度学习建模的第一步是数据。验证码识别的数据来源有三条常见路径:程序生成、真实环境采集、公开数据集。对新手来说,程序生成是最稳的起点——你用脚本生成验证码图片,答案就在生成时写在手里,不需要人工标注,而且数据量可以无限扩充。更重要的是,这个做法完全受你控制,也方便复现。
用Python生成验证码,最常见的是Pillow库。随机生成4位数字,指定字体大小、颜色、背景、干扰线数量,保存成图片。生成数据时有一个核心原则:生成风格要和真实验证码尽量贴近。如果真实验证码是红底白字、字符带弧度,你却用默认白底黑字生成,最后训练出来的模型在现场大概率翻车。很多实际项目里,我都是先拿一批真实截图,分析字符颜色、背景色、干扰形式,再照着复刻一个生成脚本。
真实环境采集也常见,但要把法律合规边界放在最前面:只在自己有权限、有授权的系统和技术验证场景里做。采集到的图片如果不知道怎么标注,可以先按文件名规则保存,再配合已知答案自动生成标签。公开数据集也是一种选择,但风格可能和你的目标验证码差异较大,迁移效果未必好。
3.2 预处理流水线:灰度化、尺寸统一与归一化
拿到原始图片后,不要急着喂给模型。验证码来源五花八门,图片尺寸可能不一致,格式可能有JPEG、PNG甚至GIF,通道数也可能从三通道到四通道。预处理的目标是把所有图片转换成模型输入的一致格式,这一步做不好,后面训练再努力也是白费。
我一般把预处理拆成三步:统一通道、统一尺寸、归一化。第一步必须统一成RGB三通道,尤其要处理好PNG的RGBA四通道格式,否则模型输入维度不一致,训练直接报错。第二步把尺寸缩放到固定大小,比如高度60、宽度160。第三步归一化到-1到1,让梯度更稳定。
下面是一个可复用的预处理函数:
import torch from PIL import Image def preprocess_image(image_path, resize=(60, 160)): # 统一RGB三通道,避免PNG的RGBA四通道问题 img = Image.open(image_path).convert('RGB') # 统一缩放,PIL的resize参数顺序是(width, height) img = img.resize(resize, Image.LANCZOS) return img # 基础transform:转张量并归一化 import torchvision.transforms as T base_transform = T.Compose([ T.ToTensor(), # 0-255 转成 0-1,且变为CHW T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ])逻辑说明:convert('RGB')把所有图片统一成三通道,直接从根上解决四通道问题;resize传参顺序是宽再高,网上很多人在这里写反,导致图片横向拉伸变形,字符信息被压缩;ToTensor把HWC转成CHW,像素值缩放到0到1,这是PyTorch的默认输入布局;Normalize用0.5的均值和标准差把数值映射到-1到1之间,能加速收敛。
关于尺寸选择:宽度160、高度60是我在数字验证码上常用的起点。如果字符间距宽,适当加宽度;如果字符高瘦,调整高度。尺寸太大会拖慢训练,太小字符细节丢失,识别率反而下降。
预处理里还有一个容易纠结的问题:要不要做灰度化。我的经验是,如果验证码本身颜色单一,灰度化可以减少输入通道、加快训练;如果验证码用颜色做干扰,比如数字是白色、背景是红色、干扰线是蓝色,颜色本身是有效区分特征,转灰度等于主动丢掉信息,这种情况保留RGB反而更好。建议两种方案都跑一个小实验比较验证集准确率,用数据说话,不要凭感觉定。
3.3 数据增强:用两三千张原始图片扩充出可训练的数据集
深度学习模型需要足够多样本学泛化,数据增强是成本最低的扩样手段。对验证码图片做小幅旋转、平移、缩放、加噪声、调亮度,每张原始图可以变出多张变体,把几千张扩充到几万张。
增强幅度是一个非常玄学的参数,设小了没效果,设大了字符变形到无法辨认,模型学了一堆错误特征。旋转角度我一般控制在±10度,平移不超过5%,缩放范围0.9到1.1,颜色扰动幅度根据背景颜色复杂度来调。这些参数不是随便拍的,标准是增强后的图片人眼仍能轻松辨识数字。
增强的代码可以直接用torchvision自带transforms:
train_transforms = T.Compose([ T.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), T.RandomAffine(degrees=10, translate=(0.05, 0.05), scale=(0.9, 1.1)), T.ToTensor(), T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ])ColorJitter模拟光照变化,RandomAffine模拟字符轻微旋转和位移,后面的ToTensor和Normalize与基础transform保持一致。注意有一个非常容易踩的坑:验证集和训练集必须用两套transform,验证集不做ColorJitter和RandomAffine,只做张量化和归一化,否则验证指标被随机扰动,看起来loss忽高忽低,根本没法判断模型真实水平。
如果目标验证码风格非常固定,比如永远同一种字体、同一种底色,关闭增强反而可能更好。因为模型只需要学一种固定分布,引入不必要的扰动等于人为制造难度。上线前先用小验证集做一次增强开和关的对比,哪个准确率高就选哪个,不要凭直觉定。
4. 模型训练全流程:用Python把字符模型训练跑通
4.1 数据加载:继承Dataset写一个验证码数据集类
预处理做完,下一步是把图片和标签配对交给模型。下面这部分是整套识别流程的核心源码,从数据集的加载开始逐步展开。PyTorch标准做法是自定义一个继承torch.utils.data.Dataset的类,实现__len__和__getitem__两个方法。验证码的标签不是一个数字,而是一个包含4个数字的向量,后续损失计算时每个分类头各取一位。
import torch from torch.utils.data import Dataset class CaptchaDataset(Dataset): def __init__(self, image_paths, labels, transform=None): self.image_paths = image_paths self.labels = labels # 每项是长度为4的list,如 [1, 3, 2, 5] self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # preprocess_image来自前面章节,返回PIL Image img = preprocess_image(self.image_paths[idx]) if self.transform: img = self.transform(img) label = torch.tensor(self.labels[idx], dtype=torch.long) return img, label逻辑说明:__getitem__每次按索引读一张图片、做预处理、转成tensor,同时把标签转成long类型的tensor。标签不用float的原因在于CrossEntropyLoss要求目标必须是整数索引。如果原始标签是字符串"1357",要先拆成数字列表再转tensor。
加载器设置如下:
from torch.utils.data import DataLoader train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=128, shuffle=False, num_workers=2)batch_size选64是个折中值。太小梯度更新频繁,训练不稳定;太大占显存多,收敛速度不一定更快。shuffle=True只用于训练集,验证集保持顺序即可。num_workers在Linux下可以设2到4,Windows下有时会触发子进程异常,设成0最稳妥。
4.2 模型定义:把单分类头改成4个并列的分类头
定长数字验证码等价于4个10分类任务,模型设计上,前半部分用卷积层提取公共特征,后半部分分裂成4个分支,每个分支独立输出对应位置的10个类别概率。
import torch.nn as nn class CaptchaNet(nn.Module): def __init__(self, num_digits=4, num_classes=10): super().__init__() # 共享特征提取:卷积+池化,逐步下采样 self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 60x160 -> 30x80 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 30x80 -> 15x40 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), # 15x40 -> 7x20 ) # 4个并列分类头,每个对应一位数字 self.heads = nn.ModuleList([ nn.Sequential( nn.Flatten(), nn.Linear(128 * 7 * 20, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes), ) for _ in range(num_digits) ]) def forward(self, x): feat = self.features(x) # 每个头部独立输出logits,shape为(batch, num_classes) return [head(feat) for head in self.heads]关键点有两个。一是nn.ModuleList,它让每个分支的权重都被自动注册到模型里,model.parameters()能收集全部参数,如果只是用普通Python list,训练时梯度根本传不到这些分支。二是forward返回一个list,而不是单个tensor,这是为了分别计算每个位置的交叉熵损失。inplace=True的ReLU省显存,但如果你后面要导出ONNX遇到兼容问题,可以改成默认的inplace=False。
卷积配置为什么是32→64→128,用3×3卷积核?3×3是目前性价比最高的卷积核,两层堆叠等效于一个5×5但参数量更少。通道数翻倍是计算量和信息容量之间的常见折中。如果验证码图片更复杂,可以再加一个卷积池化层;如果图片很简单,把128改成64训练更快。
4.3 训练循环:损失函数、优化器和学习率设置
训练循环本身不复杂:前向计算、算损失、反向传播、更新参数。由于模型输出是4个logits,损失先用4个分类头的交叉熵求和,再统一反传。
import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5) for epoch in range(30): model.train() total_loss = 0.0 for images, labels in train_loader: outputs = model(images) # list,每项 (batch, 10) loss = 0 for i in range(4): loss += criterion(outputs[i], labels[:, i]) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f"epoch {epoch + 1}, loss {total_loss / len(train_loader):.4f}")CrossEntropyLoss内部已经包含softmax,所以模型输出的是logits而不是概率,不要再额外套softmax,否则梯度会异常。labels[:, i]取所有样本第i位的数字,对应第i个分类头的监督信号。四个头的损失直接相加是常见做法,不需要加权。
优化器默认Adam,学习率1e-3。这个组合在大多数小模型上不需要怎么调就能收敛,比SGD省心。StepLR每10个epoch把学习率减半,让模型在训练后期用更小的步长精细优化。如果换成更大batch或更深网络,学习率可以降到5e-4。训练中只盯loss不够,真正的验收标准是验证集准确率。
4.4 训练过程监控与checkpoint管理
训练过程中建议记两个数字:每个epoch的训练loss和验证集准确率。准确率的计算要严格对齐部署时的判定标准,即4位数字全部预测正确才算这张验证码识别成功。
def evaluate(model, val_loader): model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) preds = torch.stack([torch.argmax(o, dim=1) for o in outputs], dim=1) correct += (preds == labels).all(dim=1).sum().item() total += labels.size(0) return correct / totaltorch.stack把4个分支的预测结果拼成(batch, 4),(preds == labels).all(dim=1)检查4个位置是否全部相等,正好对应"整串验证码识别正确"这个部署标准。torch.no_grad()关闭梯度记录,推理模式下显存占用和计算量都大幅下降。
保存模型时,我习惯保存验证集准确率最高的那个epoch,而不是最后一个epoch。因为过拟合通常发生在训练后期,最后一个epoch未必是泛化能力最强的。保存代码如下:
if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pt")加载时先实例化同样的网络结构再load_state_dict,保存state_dict而不是整个model,跨机器、跨Python环境部署更灵活。代码文件和数据文件分开存放,路径全部用相对路径,换机器不用改代码。
5. 训练过程中避坑指南:识别率上不去的四个常见原因
有了前面四章的完整流程,训练本身不难,难的是模型效果不符合预期时怎么排查。以下是几条血泪经验,每条按现象、原因、解决的顺序写。
5.1 现象:损失降不下去,训练集都学不会
训练几个epoch后loss一直高位徘徊,甚至训练集上的准确率也上不去。先不要怀疑模型不够深,先检查代码逻辑。
原因:最常见的是标签和图片没有对齐,数据配错;其次是图片预处理出来的维度不对,卷积输入通道数和模型定义不一致;再就是标签类型错了,比如用浮点型喂给CrossEntropyLoss。
解决:先用一个batch做单元测试,打印images.shape确认是(64, 3, 60, 160),再核对模型输出和标签的shape。这些都对上之后,把学习率临时调到1e-2跑一个epoch,观察loss有没有明显下降趋势,以此判断是学习率太低还是网络结构有问题。还有一个快速诊断法是用一个batch反复训练数十步,如果连一个batch都学不进去,几乎可以断定是代码逻辑问题,不是模型容量问题。
5.2 现象:训练集准确率很高,验证集却差一大截
这是最常见的过拟合。训练集上99%,验证集上70%,模型在背答案,没有学到可泛化的特征。
原因:训练数据太少;增强力度太大导致字符形态失真;验证集和训练集分布不一致,比如训练集是白底黑字,验证集是蓝底白字;模型容量过大,顺手记住了训练集噪声。
解决:先看增强参数,把旋转角度和噪声强度调回温和区间。再看数据量,定长数字验证码建议至少准备5000张原始图片,配合适度增强扩充到两万张左右,这个量级对前面这种小模型足够稳定。分布不一致的问题要从源头解决,尽量让生成数据贴近真实风格;实在拿不到真实样本,可以在预处理阶段统一对比度,比如做一次灰度拉伸,削弱背景色差异的影响。
5.3 现象:输入尺寸写反,字符被压扁或拉长
PIL的resize参数顺序是(width, height),很多人习惯写成(height, width),结果宽高对调,字符被拉伸或压缩,识别率直接掉到无法使用。
原因:对numpy数组和PIL图片的坐标系混淆。numpy的数组索引是[height, width],PIL的resize参数却是(width, height),两套坐标习惯完全不同。图片照样能显示出来,肉眼不易察觉,所以排查时特别容易被忽略。
解决:在预处理函数里加一条断言,写成:
img = img.resize((160, 60), Image.LANCZOS) assert img.size == (160, 60)等于给所有的后续代码上了一道保险。字符比例不对,卷积网络提取的特征必然混乱,模型再大也救不回来。
5.4 现象:单字符识别都对,整串验证码就是不对
有时单独看每位数字的准确率都有90%以上,整串准确率却只有60%左右。这不是bug,是概率的乘法效应:假设每位准确率95%,4位全部正确的概率是0.95的4次方,约81%,误差率被放大。
原因:模型对某一位存在系统性偏差,比如3和8、0和6这类易混字符,在训练数据里样本量不平衡,模型倾向于把难分的字符猜成同一个答案;或者某一位置恰好被干扰线长期遮挡,特征不稳定。
解决:不要直接改网络结构,先看混淆矩阵,把"3被预测成8"这类高发错误拎出来,检查对应样本的图片质量。如果是样本量偏少,针对性多生成一些难例补充训练;如果是干扰线问题,增强里加入相似形式的干扰。部署时还可以加一个兜底策略:4位预测的置信度低于阈值就重新识别一次,这个简单的重试机制能挽回不少识别率。
5.5 现象:训练时间长得离谱,等得不耐烦
验证码图片分辨率小、模型规模不大,CPU上都应该半小时内完成训练。如果训练极慢,优先怀疑数据加载环节。
原因:num_workers设置太低或为0,预处理函数里做了大量重复的PIL解码和resize;或者batch设置过大,频繁触发内存交换;再或者模型意外定义得很深,明明不需要那么多层。
解决:把预处理能缓存的计算提前做掉,比如先把图片统一缩放到目标尺寸并存成数组,训练时直接读取数组,省去每轮PIL解码开销。num_workers在Linux下可以试着调到4,Windows下遇到问题就设0。还有一个很容易忽略的点:验证集DataLoader也开了shuffle=True,每次验证都白白浪费时间,验证集保持shuffle=False就好。
提示:排查顺序建议固定为"数据 → 预处理 → 模型结构 → 超参数",和前面章节的内容一一对应,不要一上来就调学习率或者换网络,那是在没确认前置条件时做无用功。
6. 从训练到上线的最后一步:模型验证与推理加速技巧
训练完模型只是第一步,真正要用起来还要想清楚两件事:模型到底好在哪、跑起来有多快。
先谈验证。除了整体准确率,我习惯把预测错误的图片和真实标签拼成网格图,肉眼扫一遍。这种做法总能发现准确率数字暴露不了的问题,比如某种字体下7总被认成1,或者图片左侧边缘被裁掉导致第一位数字识别率偏低。错误样本看得多了,数据生成脚本该往哪个方向调,心里基本就有数了。
再谈推理加速。训练时用PyTorch没问题,但上线环境里Python调用开销不小。我一般会把模型导出成ONNX格式,用onnxruntime做推理。一个小模型导出后,CPU上单张图片推理延迟能降到毫秒级,部署机器也不必再装PyTorch。导出代码大致如下:
import torch import onnxruntime as ort model.eval() dummy = torch.randn(1, 3, 60, 160) # 导出ONNX,注意输出名要和forward的4个分支对应 torch.onnx.export(model, dummy, "captcha.onnx", input_names=["input"], output_names=["output0", "output1", "output2", "output3"]) # 加载并对比输出维度,提前暴露算子兼容问题 session = ort.InferenceSession("captcha.onnx") outputs = session.run(None, {session.get_inputs()[0].name: dummy.numpy()}) print([o.shape for o in outputs])导出后务必用onnxruntime加载跑一次推理,对比PyTorch输出是否一致。如果导出时报错,多半是模型里用了ONNX不支持的算子,把对应层换成标准卷积和全连接就好;如果forward返回list导致导出报错,把返回类型改成tuple再试。
一个我踩过多次的教训:训练和推理的预处理参数必须完全一致。灰度公式不一样、归一化均值差一点、resize插值算法不同,都会让推理精度明显下滑。我现在的工作习惯是写一个统一的预处理模块,训练和推理都调用同一份代码,禁止两头各写一套。另一个习惯是归档:生成脚本、预处理参数、训练日志、模型文件整理成清单,保留生成数据的原始参数,模型失效时才能判断到底是数据分布漂移了还是模型本身退化。
验证码识别的完整过程,说到底就是数据、模型、工程三者循环迭代,每一项都做到可复现,项目才算真正落地。希望帮到你。
本文还有配套的精品资源,点击获取