
简介一套基于Python孪生神经网络实现的点选验证码识别方案面向希望入门深度学习或完成毕设、课程设计、工程实训的初中级学习者。项目在4090显卡上训练100轮后测试准确率可达98.6%模型结构与训练流程完整可帮助读者掌握孪生网络在图像相似度对比与点选坐标预测中的核心思路。资源共计13个文件、67.23MB主要包含7个Python脚本模型定义、训练、预测及工具函数、2张示例图片、1个数据集压缩包、配置文件、依赖清单与说明文档配置文件与依赖清单可降低环境搭建门槛说明文档对数据格式和训练配置给出指引。已有150人学习适合作为复现实验与二次开发的起点。压缩包内提供数据集、配置项与运行说明能够串联从环境搭建、数据准备、模型训练到结果验证的完整链路同时代码仅作参考资料需要具备一定基础能够自行调试、解决报错并扩展功能。1. 孪生神经网络做点选识别为什么判断两张图是否同类比单张分类更实用做点选识别项目我最早也以为核心难点是检测把图里的小目标框出来就完事了。实际跑下来才发现检测只是第一步真正难的是判断候选小图里哪些和题干目标是同一类这在思路上完全是个度量问题而不是分类问题。基于 Python 的孪生神经网络方案把两两图片输入共享权重的双分支网络用特征向量之间的距离决定“是不是同类”。这份资源带了完整数据集摘要里给的结果是 4090 上训练 100 轮、测试集准确率 98.6% 以上是一套能跑通的完整闭环适合做毕设、课程设计、工程实训的从业者拿来做样板工程。需要提醒的是资源定位是参考资料代码价值在于能看懂、能改、能排错而不是无脑复制。2. Python 3.8 环境与数据集准备用成对样本把训练链路先跑起来动手训练之前先把环境和数据准备好。这一步最容易被新手跳过但几乎所有 loss 不降、报错不断的问题最后都能追溯到环境版本不一致或者数据没按规范整理。这一章落地三件事conda 环境、数据目录规则、预处理逻辑。2.1 conda 环境与依赖清单项目指定的是 Python 3.8。为什么强调版本因为 torch、torchvision、opencv 这几个核心库在 Python 3.8 下有大量现成的编译好轮子踩坑最少。我的习惯是先建一个独立的 conda 环境再在环境里 pip 安装依赖不建议直接塞进 base 环境后面换项目版本冲突时非常难受。conda create -n geetest python3.8 conda activate geetest pip3 install -r requirement.txt第一行创建名为 geetest 的环境并指定 Python 3.8第二行激活它第三行按 requirement.txt 安装依赖。requirement.txt 里一般包含 torch、torchvision、opencv-python、numpy、pillow 这几类。如果机器有 NVIDIA GPU建议先单独装和 CUDA 版本匹配的 torch再执行 requirement.txt否则很可能装成 CPU 版本训练速度差出几十倍。装完先运行 python -c import torch;print(torch.cuda.is_available()) 确认能识别 GPU别等到训练跑了一小时才发现在用 CPU。2.2 数据集的目录规则与成对样本设计data.zip 解压后就是 data 目录里面是裁剪好的小图。命名规范是 id_序号.jpg 或 pngid 可以用 uuid 生成同一个对象的两张图共用同一个 id序号分别是 1 和 2。举例xxxxx_1.jpg 和 xxxxx_2.jpg 就是同一对象的一对样本。这个命名规则直接决定了之后样本对怎么生成也是整个训练的根基。文件id序号含义a3f2_1.jpga3f21对象 A 的第一张截图a3f2_2.jpga3f22对象 A 的第二张截图9c1e_1.jpg9c1e1对象 B 的第一张截图训练时同一 id 的两张图组成正样本对随机挑两个不同 id 组成负样本对。这个设计决定孪生网络学到的不是“这是什么对象”而是“这两张图是不是同一个对象”。如果数据命名错乱、拼接出错、或者同一对象只裁了一张图那你训练出来的模型基本是废的。我第一次整理数据就栽在这里偷懒用 glob 顺序生成 id同一对象的第二张图被分配了新 id整个训练集没有一对正样本loss 怎么训都降不下去。如果你手里的原始图没按这个规范命名可以像我一样先按对象分组再统一分配 idimport uuid, shutil, os # 按对象分组同一对象的图片放在同一个列表里 groups { objA: [raw/A1.jpg, raw/A2.jpg], objB: [raw/B1.jpg, raw/B2.jpg], } os.makedirs(data, exist_okTrue) for gid, paths in groups.items(): fid uuid.uuid4().hex[:8] # 同一个对象共用一个id for idx, p in enumerate(paths, 1): # 序号从1开始 dst fdata/{fid}_{idx}.jpg shutil.copy(p, dst)这段脚本先把原始图片按对象分组每组分配同一个 uuid 片段作为 id同组内按 1、2 顺序编号。关键点在于 fid 在组内只生成一次而不是每张图生成一个。如果你要扩到三个对象就往 groups 字典里继续加 key 就行。整理完务必检查每个 id 下的图片数量正常情况下恰好两张出现一张或三张说明分组有遗漏。2.3 数据预处理统一尺寸、通道与归一化模型训练前要把图片统一成相同尺寸。VGG16 系列一般用 224×224网络结构决定了输入 shape 必须固定不能有的图 128、有的图 512 直接往里塞。另外还要统一通道灰度图转三通道png 的 alpha 通道要处理掉。常见做法是先 resize 再归一化归一化参数沿用 ImageNet 的均值 mean[0.485, 0.456, 0.406] 和标准差 std[0.229, 0.224, 0.225]。pre.py 里基本都是把这套逻辑封装成一个函数训练和推理共用同一份。import cv2 def load_image(path, size(224, 224)): img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 统一转RGB img cv2.resize(img, size) # 统一尺寸 img img.astype(float32) / 255.0 mean [0.485, 0.456, 0.406] std [0.229, 0.224, 0.225] for c in range(3): img[..., c] (img[..., c] - mean[c]) / std[c] return img这段代码做了三件事转 RGB、resize 到固定尺寸、按通道做标准化。灰度图会在 cvtColor 这一步被转成三通道RGBA 图也会被丢弃 alpha 通道保证所有进网络的图片都是稳定的 H×W×3。这里要特别强调训练和推理必须调用同一个 load_image。我见过太多项目训练时做归一化、测试时直接读原始图准确率直接从 90% 掉到 60%这种翻车根本不是模型的问题是预处理不一致。3. 孪生网络与 VGG16 拆解siamese.py、vgg16.py、utils.py 各自干了什么模型部分是整个项目的核心。很多人一上来就 python train.py把孪生网络当黑匣子用遇到问题就完全不知道从哪下手。其实这个项目的代码结构非常清晰vgg16.py 负责从单张图提取特征siamese.py 把两个 VGG16 封装成共享权重的双分支网络utils.py 负责生成样本对并打包成 batch。这一章按文件逐个拆。3.1 孪生网络为什么用共享权重核心思想一句话让网络学会比较而不是学会分类。孪生网络有两个输入 x1、x2分别进入两个分支但这两个分支的权重完全共享。共享的意思是“同一个网络加载了同一份权重、跑两遍”而不是两个独立网络各学各的。为什么必须共享如果两个分支独立训练模型完全可以把 x1 映射到空间 A、x2 映射到空间 B再靠后面的融合层硬生生学出“配对正确”这样学到的特征根本不具备可比性换一张没见过的图就失效。共享权重强制模型用同一种映射方式处理两张图特征向量之间的距离才真正反映图片内容本身的相似度。而且共享权重直接把参数量减半在小数据集上不容易过拟合。训练时喂进去的是一个个图片对正样本对目标是让特征距离小负样本对目标是拉开距离这就是度量学习的标准思路。3.2 VGG16 作为特征提取骨干vgg16.py 做的事是把 VGG16 最后的全连接分类头去掉只保留卷积部分作为特征提取器。输入 224×224 图片输出一个固定维度的特征向量。VGG16 的卷积层在 ImageNet 上预训练过直接拿来提取通用视觉特征是性价比很高的选择这也是这个项目选 VGG16 而不是自己从头写 CNN 的原因——你不需要几百万张图去训一个基础特征提取器预训练权重已经帮你解决了这个问题。拿到卷积特征后常见做法是全局池化或展平再接一个全连接层把维度压到比如 128 维。这个低维向量就是 embedding是后续相似度计算的依据。vgg16.py 里做的无非就是“加载预训练权重 削掉分类头 接上目标维度全连接”。3.3 siamese.py 的核心结构看 siamese.py 时重点看 forward 方法是怎么组织双分支的。典型结构如下import torch.nn as nn class SiameseNetwork(nn.Module): def __init__(self, backbone, embed_dim128): super().__init__() self.backbone backbone self.fc nn.Linear(backbone.out_dim, embed_dim) def forward_one(self, x): feat self.backbone(x) # 卷积特征 [B, 512] return self.fc(feat) # 128维embedding def forward(self, x1, x2): f1 self.forward_one(x1) # 第一张图的embedding f2 self.forward_one(x2) # 第二张图的embedding dist nn.functional.pairwise_distance(f1, f2) return dist核心就三行forward_one 把单张图变成 128 维向量forward 里对两张图分别调用同一个 forward_one然后算两个向量的欧氏距离。pairwise_distance 计算每个 batch 内对应样本对的距离距离越小说明两张图越可能是同一对象。实际仓库里的代码可能把 backbone 换成实例化的 VGG16或者加一层 L2 归一化但整体结构就是上面这个骨架。损失函数常见有两种。一种是对比损失 contrastive loss直接约束同类距离小、异类距离大另一种是把相似度再过一层 sigmoid 当二分类用交叉熵训练。区别在于前者学的是度量空间后者学的是概率输出。度量空间在业务侧更好用你可以根据自己的容忍度去调阈值二分类则更直观输出直接是同类概率。这个项目用的是哪种读 siamese.py 里 forward 返回值和 loss 计算的部分就能确定。3.4 utils.py 的样本对生成逻辑utils.py 负责把 data 目录读进来按相同 id 生成正样本对随机组合不同 id 生成负样本对再转成 tensor 打包。负样本怎么生成非常影响训练质量常见做法是每个 batch 里一半正样本对、一半负样本对比例失衡模型会倾向于把所有输入都判成同类或异类。另一个值得注意的点同一对象的两张图如果太像比如几乎是同一帧截图模型学到的是“像素完全一致就是同类”推理时遇到同对象但角度、光照变化的情况就翻车。所以在生成训练对之后加一点随机裁剪、旋转或亮度抖动能明显提升鲁棒性。这一步可以在 utils.py 里做也可以在数据加载时做。如果仓库里没带数据增强我建议你自己加上对最终准确率的影响比调学习率大得多。4. 训练实操与常见问题排查从 config.py 到 98.6% 准确率的完整过程训练是大多数人真正发怵的地方。这一章先按 config.py 把参数配置好、把训练跑起来然后重点列出我在实际运行中遇到的高频翻车点每条按现象、原因、解决三个维度来讲最后给出测试集准确率的验证方式。4.1 config.py 参数解析与最小配置config.py 是训练入口的公共配置。摘要里说“一般来说只需要配置 gpu 就行”说明仓库的默认参数在常见环境下已经能跑但你需要知道每个参数是干什么的才能在显存不够或者 loss 不降时做出调整参数常见取值说明GPU0指定使用的 GPU 编号EPOCH100训练轮数BATCH_SIZE32每次迭代喂入的样本对数量IMG_SIZE224输入图片边长LR1e-4初始学习率DATA_ROOTdata数据集目录GPU 参数要和机器实际编号对上跑 nvidia-smi 看一眼再填。显存只够跑 16 或 8 的时候优先把 BATCH_SIZE 调小这比强行开 32 然后被 OOM 打断要实际得多。损失剧烈震荡可以把 LR 降到 3e-5 试一下。启动训练的命令很简单python train.py训练脚本读 config.py初始化网络和数据加载器然后在训练循环里不断喂样本对、计算损失、反向传播。我第一次跑这个项目时直接把 EPOCH 改成 10 验证链路确认数据加载、前向传播、反向传播、模型保存都没问题之后再改回 100 跑全量。如果 10 轮都报错问题通常不在训练本身而在数据或环境先回头查第 2 章的内容。4.2 常见问题排查清单五个高频翻车点翻车点一CUDA out of memory。 现象训练开始没几步就报显存不足进程被杀。原因BATCH_SIZE 和 IMG_SIZE 太大超出显卡显存。解决先把 BATCH_SIZE 降到 16 或 8还不行就把 IMG_SIZE 降到 192 或 160。这个项目的默认参数是按 4090 这种大显存卡调的普通 8G 显存卡要按这个方式降配跑起来一样能收敛只是每个 epoch 的时间会长一些。翻车点二loss 基本不降或者卡在 0.69 附近。 现象训练了几十轮loss 几乎不动或者长期贴着一个固定值。原因最常见的是正样本对全部配错了也就是数据命名错乱另一个常见原因是样本对没有 shuffle模型一直在看同一批数据。解决先打印一个 batch人工确认每一对图片的标签是否符合预期再确认数据加载器里 shuffleTrue最后确认 loss 计算的是两张图的特征距离而不是单张图的输出。翻车点三训练集准确率接近 100%测试集掉到 70%。 现象训练时指标非常漂亮一测试就打回原形。原因大多不是过拟合而是训练和测试的预处理不一致训练时做了归一化和统一 resize测试时直接读了原始图。解决把第 2 章的 load_image 抽成公共函数train.py、predict.py、验证脚本三处引用同一份代码。另外检查测试集图片尺寸是否和训练一致不一致的图要先 resize 再进网络。翻车点四数据命名错乱正样本对变成不同对象的两张图。 现象loss 在下降但模型判断结果完全没意义。原因数据整理时同一对象的第二张图被分配了新 id导致训练集里找不到任何一对正样本。解决整理数据时按对象分组再分配 uuid整理完统计每个 id 下的图片数量正常情况下恰好两张出现一张或三张的就是整理错了。翻车点五灰度图和彩色图混用导致 shape 错误。 现象训练到一半报维度不匹配比如期望 3 通道但输入是 1 通道。原因数据里有灰度图或者带了透明通道的 png它们和 RGB 图的通道数不一致。解决在 load_image 里统一做 cvtColor灰度图转 BGR、RGBA 去 alpha保证所有进网络的图片都是稳定的三通道。这五个问题里前两个几乎每个跑这个项目的人都会撞见。遇到报错不要急着改网络结构按数据、预处理、超参数、代码 Bug 这个顺序排查效率高得多。改网络结构通常是最后一步大多数情况下根本不需要动。4.3 测试集准确率验证摘要里写“4090 训练 100 轮测试集准确率在 98.6% 以上”这个指标是通过计算测试集上样本对判断正确的比例得到的。写一个快速验证脚本from siamese import SiameseNetwork from utils import load_test_pairs import torch model SiameseNetwork(backbone).eval() model.load_state_dict(torch.load(checkpoints/best.pth)) pairs, labels load_test_pairs(data) correct 0 for (x1, x2), y in zip(pairs, labels): with torch.no_grad(): d model(x1.unsqueeze(0), x2.unsqueeze(0)).item() pred 1 if d threshold else 0 correct (pred int(y)) print(acc:, correct / len(pairs))验证脚本不再计算梯度、统一走 model.eval()然后按相似度阈值判定样本对是不是同类最后统计预测正确的比例。threshold 通常取训练完成后在验证集上扫出来的最优值不是随便拍脑袋定的。准确率超过 98% 说明模型已经能稳定区分同类和异类至于放到具体业务场景好不好用还得看检测环节把候选图裁得干不干净。如果候选框裁得歪七扭八后面相似度判断再准也白搭。5. 推理管线与进阶技巧YOLOv3-tiny 候选框与孪生判同怎么配合落地predict.py 做的事情不只是加载模型权重跑一次 forward。完整链路是先用 yolov3-tiny.cfg 对应的检测网络把大图中的候选小图抠出来再把候选小图与题干模板图两两组成样本对输入孪生网络得到相似度最后按阈值筛选出匹配项输出坐标和序号。仓库里带 yolov3-tiny.cfg 就是为了这一步候选框提取它和孪生网络是前后串联的关系不是二选一。阅读 predict.py 时重点看两个地方。一是模板图从哪来有的版本是用户指定一张目标图有的版本是直接从检测结果里挑置信度最高的框当模板。二是阈值的处理方式判断是不是同类最终都落在“距离小于阈值就算匹配”这个逻辑上。可以加一个小技巧把特征向量做 L2 归一化后用余弦相似度代替欧氏距离点选场景里候选图的光照和缩放变化比较大余弦相似度对向量模长不敏感鲁棒性更好。阈值最好不要自己拍脑袋定。训练完可以在验证集上扫一遍best_t, best_acc 0.0, 0.0 for t in np.arange(0.3, 1.0, 0.05): acc evaluate(model, val_pairs, t) if acc best_acc: best_t, best_acc t, acc print(best_t, best_acc)这段代码用不同阈值在验证集上做评估挑出准确率最高的 threshold。距离在阈值附近的样本对宁可不放行也不要误判打乱点选顺序这类场景漏判比错判更容易接受。我之前翻过车训练阶段的 load_image 里做了归一化predict.py 里直接读了原始图推理结果一塌糊涂。从那以后我每次换数据集、换机器都强制先跑一遍最小规模的 predict确认预处理一致再开始正式训练。这套资源带数据集、带检测配置、带训练和推理脚本麻雀虽小五脏俱全适合当样板工程研究希望帮到你。本文还有配套的精品资源点击获取