ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ResNet+DenseNet双骨干OCR:验证码识别实战与优化

ResNet+DenseNet双骨干OCR:验证码识别实战与优化 简介本资源是一套基于深度学习实现的验证码识别OCRPython 源码项目采用 ResNet 与 DenseNet 两种经典卷积网络算法面向计算机、人工智能、信息安全等专业的学生与开发者可用于课程设计、毕业设计、大作业或入门级项目立项演示帮助解决验证码自动识别这一典型 OCR 任务。压缩包共 1084 个文件以 1071 个 png 验证码样本图片为主另含 5 个 xml 标注文件、4 个 py 源码脚本、1 个 ttf 字体及 iml、name、md 等工程配置与说明文件整体约 8.84MB结构清晰便于直接运行与二次开发。目前已有 423 人学习下载。项目代码经过测试运行成功读者可据此理解数据标注、模型搭建、训练与推理的完整流程并在此基础上修改网络结构或扩充数据集实现其他识别功能。1. 从一张扭曲的验证码说起ResNetDenseNet 双骨干 OCR 到底解决什么问题做爬虫、做自动化测试、做数据采集的人几乎都绕不开验证码这道坎。字符粘连、随机旋转、干扰线、背景噪点传统 OCR 引擎在这种图上经常直接摆烂识别率掉到个位数。基于深度学习实现的验证码识别模型OCRpython 源码ResNetDenseNet 算法这个方向核心思路就是用卷积神经网络做端到端的字符识别把「切分 单字分类」的老路子换成「整图输入、序列输出」的新范式。ResNet 负责提取深层残差特征DenseNet 负责特征复用和梯度流通两者组合能显著提升小样本、强干扰场景下的识别鲁棒性。这套方案适合有一定 PyTorch 基础、想自己训练验证码识别模型的工程师也适合需要把 OCR 能力嵌入业务系统的团队。下面从数据构造、模型搭建、训练调参到部署推理一步步拆开讲。2. 验证码 OCR 的数据管线从原始图片到可训练张量2.1 为什么验证码识别不能直接套通用 OCR 流程通用 OCR 面对的是印刷体、规整排版、清晰背景而验证码天生就是反 OCR 的。字符之间有随机间距甚至重叠字体经过拉伸旋转背景有噪点和干扰线有些还会做颜色渐变。直接拿 PaddleOCR 或 Tesseract 去跑结果往往惨不忍睹。常见做法是自己构造数据集用固定字符集比如 0-9、a-z 去掉易混淆字符固定长度4 位或 6 位然后针对性地做数据增强。这里的关键决策是用 CTC 损失做不定长识别还是用固定长度分类做定长识别。验证码绝大多数是定长的所以用定长分类更简单、收敛更快不需要引入 CTC 那套对齐逻辑。2.2 生成训练数据用 Captcha 库批量造样本from captcha.image import ImageCaptcha import os import random import string # 字符集去掉 0/O、1/l/I 等易混淆字符 CHARS 23456789abcdefghjkmnpqrstuvwxyz CAPTCHA_LEN 4 SAVE_DIR ./dataset/train os.makedirs(SAVE_DIR, exist_okTrue) image ImageCaptcha(width160, height60, fontsNone) for i in range(50000): text .join(random.choices(CHARS, kCAPTCHA_LEN)) img image.generate_image(text) # 加入随机旋转和噪点模拟真实场景 img img.rotate(random.uniform(-10, 10), fillcolor(255, 255, 255)) img.save(os.path.join(SAVE_DIR, f{text}_{i}.png))这段代码用ImageCaptcha生成 5 万张训练图每张图文件名里带标签方便后续解析。width160, height60是常见验证码尺寸太小会导致字符粘连严重太大增加计算量。rotate加 ±10 度随机旋转是为了让模型见过一定角度的形变。注意fontsNone时会用默认字体实际项目中建议指定 3-5 种不同字体增加多样性。生成 5 万张大约需要几分钟磁盘占用在 500MB 左右。2.3 Dataset 与 DataLoader把图片转成模型要的张量import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import os class CaptchaDataset(Dataset): def __init__(self, root_dir, char2idx, transformNone): self.samples [] self.char2idx char2idx self.transform transform for fname in os.listdir(root_dir): label fname.split(_)[0] self.samples.append((os.path.join(root_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) # 把标签转成索引序列 target torch.tensor([self.char2idx[c] for c in label], dtypetorch.long) return img, target char2idx {c: i for i, c in enumerate(CHARS)} dataset CaptchaDataset(SAVE_DIR, char2idx, transformtransforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.5]*3, std[0.5]*3) ])) loader DataLoader(dataset, batch_size128, shuffleTrue, num_workers4)CaptchaDataset的核心逻辑是从文件名解析标签然后把每个字符映射成索引。char2idx的构建顺序必须和训练、推理时一致否则解码会全错。Normalize用 0.5 均值和标准差是常见做法把像素值拉到 [-1, 1] 区间有助于收敛。batch_size128在 8GB 显存的卡上跑 160x60 的图基本没问题如果显存不够就降到 64。num_workers4根据 CPU 核数调整太少会拖慢数据加载太多会占满内存。3. ResNet DenseNet 双骨干模型搭建结构设计与前向逻辑3.1 为什么要把 ResNet 和 DenseNet 拼在一起ResNet 的核心是残差连接解决深层网络梯度消失问题让网络可以堆到上百层。DenseNet 的核心是密集连接每一层的输入来自前面所有层的输出特征复用率极高参数效率好。单独用 ResNet 做验证码识别已经能到不错的精度但遇到字符粘连严重、背景复杂的样本时浅层纹理特征容易丢失。DenseNet 的密集连接能把浅层边缘特征一路传到深层弥补这个短板。常见做法是用 ResNet 的前几个 stage 做主干特征提取再接一个 DenseNet 的 dense block 做特征融合最后接全连接层输出每个字符位置的分类结果。这样既保留了 ResNet 的深度优势又利用了 DenseNet 的特征复用能力。3.2 模型代码双骨干融合与多字符分类头import torch.nn as nn import torchvision.models as models class CaptchaModel(nn.Module): def __init__(self, num_chars, captcha_len4): super().__init__() # ResNet18 作为主干去掉最后的全连接层 resnet models.resnet18(pretrainedTrue) self.resnet_features nn.Sequential(*list(resnet.children())[:-2]) # DenseNet121 的 dense block 做特征融合 densenet models.densenet121(pretrainedTrue).features self.dense_features nn.Sequential(*list(densenet.children())[:8]) # 融合后的通道数ResNet18 输出 512DenseNet 输出 512 self.fusion nn.Sequential( nn.Conv2d(1024, 512, kernel_size1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue) ) self.pool nn.AdaptiveAvgPool2d((1, captcha_len)) self.fc nn.Linear(512, num_chars) def forward(self, x): f1 self.resnet_features(x) # [B, 512, H/32, W/32] f2 self.dense_features(x) # [B, 512, H/32, W/32] f torch.cat([f1, f2], dim1) # [B, 1024, H/32, W/32] f self.fusion(f) # [B, 512, H/32, W/32] f self.pool(f) # [B, 512, 1, captcha_len] f f.squeeze(2).permute(0, 2, 1) # [B, captcha_len, 512] out self.fc(f) # [B, captcha_len, num_chars] return outresnet_features取的是 ResNet18 去掉最后两层平均池化和全连接的部分输出空间维度是输入的 1/32。dense_features取 DenseNet121 的前 8 个子模块输出空间维度也是 1/32这样两个特征图可以直接在通道维度拼接。fusion用 1x1 卷积把 1024 通道压回 512减少后续计算量。AdaptiveAvgPool2d((1, captcha_len))是关键设计把宽度方向池化成 captcha_len 个位置每个位置对应一个字符的预测。最后fc把每个位置的 512 维特征映射到字符集大小。整个模型参数量大约 15M推理一张图在 GPU 上不到 5ms。3.3 训练循环与损失函数选择import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model CaptchaModel(num_charslen(CHARS), captcha_lenCAPTCHA_LEN).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() total_loss 0 for imgs, targets in loader: imgs, targets imgs.to(device), targets.to(device) optimizer.zero_grad() outputs model(imgs) # [B, captcha_len, num_chars] # 把每个位置的预测和标签对齐算 loss loss sum(criterion(outputs[:, i, :], targets[:, i]) for i in range(CAPTCHA_LEN)) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch1}, Loss: {total_loss/len(loader):.4f})损失函数用CrossEntropyLoss对每个字符位置单独算 loss 再求和。这里没有用 CTC因为验证码是定长的每个位置和标签一一对应不需要对齐。AdamW比Adam多了权重衰减的解耦泛化更好。CosineAnnealingLR让学习率按余弦曲线下降前期快速收敛后期精细调参。训练 50 个 epoch在 5 万张图上单卡 2080Ti 大约需要 2-3 小时。如果 loss 降到 0.05 以下基本可以认为收敛了。4. 训练完的模型怎么用推理、解码与置信度过滤4.1 单张图片推理与字符解码from PIL import Image import torchvision.transforms as transforms def predict(image_path, model, char2idx, device): model.eval() img Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.5]*3, std[0.5]*3) ]) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): output model(img_tensor) # [1, captcha_len, num_chars] probs torch.softmax(output, dim2) conf, preds torch.max(probs, dim2) idx2char {i: c for c, i in char2idx.items()} text .join(idx2char[p.item()] for p in preds[0]) avg_conf conf.mean().item() return text, avg_conf推理时把图片做和训练一致的预处理然后取每个位置概率最大的字符。avg_conf是四个位置置信度的平均值可以用来做过滤低于 0.9 的样本可以丢弃或人工复核。实际部署中这个置信度阈值要根据业务容忍度调整宁可漏识别也不要错识别。4.2 批量推理与性能优化def batch_predict(image_paths, model, char2idx, device, batch_size64): model.eval() results [] transform transforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.5]*3, std[0.5]*3) ]) for i in range(0, len(image_paths), batch_size): batch_paths image_paths[i:ibatch_size] imgs torch.stack([transform(Image.open(p).convert(RGB)) for p in batch_paths]).to(device) with torch.no_grad(): output model(imgs) preds output.argmax(dim2) idx2char {i: c for c, i in char2idx.items()} for j in range(len(batch_paths)): text .join(idx2char[p.item()] for p in preds[j]) results.append((batch_paths[j], text)) return results批量推理比单张循环快 5-10 倍因为 GPU 利用率上去了。batch_size64在 8GB 显存上跑 160x60 的图没问题。如果要做成服务可以用 ONNX 导出模型再用 ONNX Runtime 做 CPU 推理单张延迟能控制在 20ms 以内适合并发不高的场景。4.3 模型导出与部署注意事项# 导出 ONNX dummy_input torch.randn(1, 3, 60, 160).to(device) torch.onnx.export(model, dummy_input, captcha_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})导出 ONNX 时用dynamic_axes指定 batch 维度可变这样部署时可以动态调整 batch size。注意 ONNX 导出后要验证输出和 PyTorch 一致常见坑是AdaptiveAvgPool2d在 ONNX 里的行为差异建议导出后用 onnxruntime 跑一遍对比结果。部署时如果目标机器没有 GPU用onnxruntime的 CPU 版本即可模型文件大约 60MB。5. 避坑与排查训练验证码识别模型时最容易翻车的 5 个点5.1 损失降不下去准确率卡在 20% 左右现象训练几个 epoch 后 loss 在 2.0 附近震荡准确率上不去。原因最常见的是标签映射错位。char2idx在训练和验证时用了不同的字符集顺序或者文件名解析时把下划线后面的序号也当成了标签。解决打印前 10 个样本的标签和对应索引确认映射一致。另外检查targets[:, i]的维度是否和outputs[:, i, :]对齐维度错了 loss 会算错但不报错。5.2 模型在训练集上过拟合验证集准确率掉得厉害现象训练集准确率 99%验证集只有 70%。原因训练数据多样性不够比如只用了 1 种字体、旋转角度范围太小。解决增加字体种类到 5 种以上旋转角度扩到 ±15 度加入随机高斯噪声和颜色抖动。另外可以在fc前加Dropout(0.3)或者用weight_decay1e-3加大正则。5.3 推理时单张图识别正确批量推理结果全乱现象单张预测没问题batch_predict出来的结果和单张对不上。原因torch.stack时图片顺序和batch_paths不一致或者transform里有随机操作比如RandomRotation在推理时没关掉。解决推理时的transform必须和验证集一致不能带随机增强。检查model.eval()是否调用BatchNorm在训练和推理模式下的行为不同。5.4 GPU 显存溢出batch_size 调到 16 还是 OOM现象CUDA out of memory降低 batch_size 后仍然报错。原因DenseNet 的密集连接会缓存中间特征图显存占用比同参数量的 ResNet 大不少。另外num_workers过多会导致每个 worker 都复制一份数据到显存。解决把num_workers降到 2用torch.cuda.empty_cache()清理缓存。如果还不够把 DenseNet 部分换成densenet121的前 6 个子模块而不是 8 个减少特征图缓存。5.5 部署到生产环境后识别率骤降现象本地测试准确率 95%上线后只有 60%。原因生产环境的验证码和训练数据分布不一致比如字体、颜色、干扰线风格不同。解决从生产环境采集 500-1000 张真实样本人工标注后做 fine-tune。学习率调到 1e-4训练 10 个 epoch 即可。如果标注成本高可以用半监督方式先用模型预测人工只修正错误的样本。6. 进阶技巧用测试时增强和模型集成把准确率再拉 3 个百分点训练完一个模型后如果准确率卡在 95% 上不去可以试试测试时增强TTA。具体做法是对同一张测试图做多次轻微变换比如 ±5 度旋转、亮度微调分别推理后对每个位置的 logits 求平均再取 argmax。这个技巧不需要重新训练推理成本增加 3-5 倍但准确率通常能提升 1-2 个百分点。def predict_with_tta(image_path, model, char2idx, device, n_aug5): model.eval() img Image.open(image_path).convert(RGB) base_transform transforms.Compose([ transforms.Resize((60, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.5]*3, std[0.5]*3) ]) all_logits [] for _ in range(n_aug): aug_img img.rotate(random.uniform(-5, 5), fillcolor(255, 255, 255)) tensor base_transform(aug_img).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) # [1, captcha_len, num_chars] all_logits.append(logits) avg_logits torch.mean(torch.cat(all_logits, dim0), dim0, keepdimTrue) preds avg_logits.argmax(dim2) idx2char {i: c for c, i in char2idx.items()} return .join(idx2char[p.item()] for p in preds[0])另一个技巧是模型集成训练 3 个不同初始化的模型推理时对 logits 求平均。集成能把准确率再拉 1-2 个百分点但推理成本翻 3 倍。实际项目中如果业务对准确率要求极高比如 99.5% 以上建议用 TTA 双模型集成单张推理延迟控制在 50ms 以内大多数场景都能接受。我自己的习惯是先用单模型 TTA 跑一版基线看置信度分布。如果低置信度样本占比超过 5%再考虑加集成。不要一上来就堆模型先把数据质量和增强策略调好收益比换模型大得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表