探索笔记
杂谈2026年9月14日

CNN验证码识别杂记

本页目录
  1. 从 0% 开始认验证码
  2. 标到 746 张的时候,第一个模型直接给了个 0%
  3. Medium:最先排除掉的是“再堆一点容量就好了”
  4. Position 是第一个真正的转折
  5. 标签多起来以后
  6. 再往后,Position 自己开始显得太小了
  7. 最终数据集

从 0% 开始认验证码

这个项目最初没打算做成一个研究。他缘起于Animeko 里有几个媒体源会弹四位数字验证码,图片很小,128×40,看上去也谈不上复杂:四个数字,固定长度,字符集只有 0-9。不过真实拿到数据后发现数字会旋转,会粘在一起,灰度有变化,不同来源的生成风格也有一点差别。到这里似乎就逐渐变成了一个研究问题:如何在小模型体积的情况下保持低泛化能力的高精度识别模型。

不过最麻烦的其实是标签。验证码没有现成的数据集,只能自己抓,自己一张张看,把四位数字打进去。后来公开出来的数据有 3203 张,但那是项目结束以后才有的规模。真正开发的时候,整个过程是动态的,我们不可能先安安静静把三千多张全部标完,再从第一版模型开始做。

实际过程更像是:手里标出一批,就先跑;模型暴露一个问题,再改;改完的时候标签又多了一批,再拿新数据试。回过头看,模型和数据集几乎是一起长出来的。

标到 746 张的时候,第一个模型直接给了个 0%

那天上午,手里能拿来做实验的标签大约有 746 张。

思路其实很自然。既然验证码固定四位,也没有必要先把四个字符切开。整张图送进 CNN,经过几层卷积以后把 feature map 展平,最后一个线性层直接输出 40 个 logits:四个位置,每个位置十分类。

当时我对这版的预期并不低。任务长度固定、输出空间又很小,感觉怎么都应该先学出一个能看的 baseline。但是结果不怎么好看,字符准确率只有 14.68%,整码准确率是0%,对,一个都没有。

事后复盘的时候发现这里有个细节非常重要:14.68% 说明模型并非完全没学到东西。它已经比彻底乱猜多知道了一点什么,只是这些信息没有被组织成稳定的四位输出。

Medium:最先排除掉的是“再堆一点容量就好了”

于是紧接着做了第二版。这一版没有改变整个任务的表达方式。还是整图输入,还是把卷积后的信息做成一个全局表示,再一次性输出四个数字。主要变化就是把网络做大一点:更多通道、更多池化、再加隐藏层。

如果上一版失败只是因为容量太小,这一版至少好一点了。CharAcc 从 14.68% 到了 20.96%,ExactAcc:基本还是不能用。

这里有个很奇怪的现象正在出现:网络对单个字符的判断能力是在上涨的,可是一旦要求第一位、第二位、第三位、第四位全部稳定落到自己的位置上,性能就几乎崩掉。

于是我开始回头看 Flatten 这件事本身。卷积层原本是有空间结构的:一张验证码从左到右四个数字的位置关系非常稳定。但 feature map 一旦被整个 flatten,再交给一个全局头,我们实际上把“这是第几个数字”这件事也交给了网络自己去学。

Position 是第一个真正的转折

第三版叫 PositionCaptchaCNN。它其实没有回到传统 OCR 那种“先把四个字符切出来”的路线。这一点当时想得很明确:字符会旋转、粘连,输入空间硬切四刀本身就会制造新的错误。切错以后,后面的分类器再强也救不回来。后续复盘的时候查文献,实际上早期对于这个地方的处理具有这两种不同的方法,对于切割处理有不同的切割法,对于整体处理也有不同的优化法。不过当时这里没有选择继续尝试切割实际上只是直觉罢了。

变化发生在 feature map 上:把卷积后的特征沿横向保留成四个 slot。可以粗略理解成:

这四个框不是四张裁出来的字符图。它们只是告诉模型:横向有四个稳定的位置。另外我们还进一步进行了一些小的修改:小范围平移、约两度旋转、轻微缩放、亮度和对比度扰动,再加一点低概率模糊和噪声。但是这个变化直接导致CharAcc 到了 47.15%,ExactAcc 到 5.50%。

这一版第一次让我觉得方向找到了。这时候问题开始从:

CNN 到底够不够大?

变成:

这个任务到底应该给 CNN 什么先验?

后来论文里把它叫做 position-aware representation、位置保持、归纳偏置。实际做的时候没有这么漂亮,就是因为前两版实在太差,才被逼着去检查模型到底把什么信息扔掉了。

标签多起来以后

等到手里的数据已经能拿出 1618 张训练,同时再留 250 张的时候,我们没有再做随机五折。

验证码是成批生成的。同一个来源、同一个时间段的图片,很可能共享字体、背景、旋转方式甚至某些生成参数。如果把一批很相似的图随机拆进 train 和 validation,验证集可能并没有我们想象中那么“新”。

于是我第一次故意把后面采集的图留出来。这一次 CharAcc 65.90%,ExactAcc 19.60%。

再往后,Position 自己开始显得太小了

到了这个阶段,Position 的另一个问题开始冒出来:它只有四千多个参数。在 746 张的时候,小模型训练快,方便我们快速判断位置结构有没有用。但现在位置这件事已经基本确定,数据也翻了几倍,模型容量本身终于开始成为真实瓶颈。

于是我们重新设计了 WidePositionCaptchaCNN(也就是后来的alpha1):Position 不动,把 backbone 加宽,这一版大概四万参数。

在这版中我们把 Wide Position 训练了三个随机种子,然后直接平均 logits。

跨批次留出的结果第一次到了一个看起来像样的数字:

  • CharAcc 91.65%
  • ExactAcc 72.06%

一半验证码可以四位全部认对。到了这个阶段基本上实验已经做完方案已经确定能进行真正的训练了。

最终数据集

最终冻结下来的数据是 3203 张。三个正式批次各 1000 张,再加 203 张更早的预检样本。所以最终阶段重新划分: 2447 张训练,275 张验证, 481 张多来源 holdout。

最后模型名称叫做 Position-DS:Position 最核心的东西没动,完整图像进去,横向四个位置出来。普通卷积被换成更适合轻量部署的深度可分离残差块,通道做成 32、48、72,最后一个阶段在横向加 dilation,把感受野再往左右拉一点;池化最后直接收成 (1,4),然后四个位置各自一个小分类头,单成员大约九万多参数。

训练还加进了 EMA、困难样本重放、略微不一样的位置 loss 权重,481 张 holdout 上完整识别正确 462 张(ExactAcc 96.05%)。

到这里,这个模型基本上已经能够成为高精准度模型了。在真实的业务环境中,我设计的逻辑实际上会根据响应码不同进行3次重试,这么看来有至少一次成功识别验证码的概率是99.99%,也就是说几乎不会失败回退(在后来的用户反馈中实际情况也确实如此,高速准确,识别时间几乎无体感,小模型还是太牛逼了)。