
简介基于Python孪生神经网络的点选识别项目面向希望入门深度学习或准备毕业设计、课程设计的开发者和学习者。工程围绕点选验证码场景包含完整训练与预测代码据描述在高端显卡上训练一百轮后测试集准确率可达百分之九十八点六以上能帮助理解孪生网络结构、特征提取与类别比对流程。压缩包共十三个文件大小约六十七兆字节以七个脚本文件为代码核心覆盖模型训练、预测、数据预处理等环节另附模型结构示意图、参数配置、运行环境依赖清单、说明文档以及按编号成对命名的图片数据集便于构造训练样本。已有150人学习下载。开发者可获得可运行的工程框架与配套数据通过配置计算设备即可复现训练流程对于研究验证码识别或相似度匹配任务的读者还可参考基于经典卷积网络的特征提取思路与目标检测配置作为项目起步或算法调优的参考资料。1. 点选识别为什么非要用孪生神经网络一个动态类别问题的解点选识别的实际任务长这样给定一张背景图外加一句提示词“请点击图中所有红绿灯”需要在图上找出所有符合语义的目标位置。这个任务放到固定类别的分类或检测网络里会持续翻车——提示词是随时换的今天点红绿灯明天点沙发网络的类别参数在训练时已经写死新增语义就要重新标注、重新训练。基于 python 孪生神经网络实现的点选识别含数据集换了一个思路不学“物体叫什么名字”而是学“两张图是不是同一个语义”。提示词用一张参考图表示与背景图里的候选区域一起送进共享权重的孪生网络比对相似度相似的目标就是答案。这篇笔记会从任务原理、合成数据集、模型训练一直写到落地排查和验证方法适合在做验证码识别、图形点击类任务或小样本匹配检测的从业者参考。2. 孪生网络与点选识别的任务拆解从固定分类到语义匹配2.1 分类网络为什么让点选识别集体翻车普通分类网络的输出层是一个固定维度的 softmax训练时定下多少个类别推理时就只能回答多少个类别。点选验证码是提示词动态下发运营方可以随时往提示词池里加新词。每加一个类别分类网络都要重新标注数据、重新训练这个节奏在真实业务里根本跑不动。更别扭的是输出形式。点选任务要的是坐标分类网络天然不输出位置。常见补救办法是滑窗把图裁成一个个小窗口逐个送分类器打分。代价是计算量暴涨而且每个窗口都要做全类别打分重叠区域被反复计算。检测网络虽然自带位置输出但类别头同样是写死的对“只给一个参考图就要找到同类”的小样本场景几乎没有支持。所以点选识别真正难的地方不是“识别得准”而是语义集合不可预知。它本质上是 one-shot 检测给定一个语义示例在图中找出所有同语义的实例。能用一张参考图描述提示词再和图里的候选区域做相似度比较这条路线天然比“背类别名”的路子稳定。孪生网络正好是为这种“比较”而生的结构。2.2 孪生网络两个分支为什么要共享权重孪生网络的结构描述起来很直接两个输入一个编码器两个输出。输入 A 是提示词对应的参考图输入 B 是待检图中的候选区域。参考图一般用数据集里按类别裁剪好的物体图候选区域则是滑窗或检测器给出的框。两个输入统一缩放到 128×128经过同一个卷积骨干网络各自得到一条 embedding 向量。关键点是“同一个网络”。两个分支共享权重参考图和候选区域才会被映射到同一个特征空间之后算余弦相似度才有意义。如果两路各用各的网络两个 embedding 空间的坐标基准是错位的相似度数值完全不可比模型很难收敛。共享权重既减少了参数量也强制模型去学“同一语义在不同图里长什么样”。embedding 最后一定要做 L2 归一化让所有特征落在单位球面上。这样推理时相似度就等于两个向量的点积数值范围稳定在 [-1,1]。归一化这步省掉的话训练和推理时的距离量级不稳定后面调阈值就变成玄学。这类“类别不可枚举”的任务里孪生网络是成熟方案行人重识别ReID要判断两张抓拍是不是同一个人签名验证要判断两个签名是否出自同一人用的都是这个套路。还有一个工程细节参考图不是只能有一张。推理时如果只有文字提示没有示例图需要在离线阶段建一张“提示词→参考图”的映射表每个常见词人工准备 3 到 5 张不同角度、不同光照的示例图。推理时把这几张参考图分别编码取平均得到的参考 embedding 比单张更稳。这一点在后面的验证环节会再提。2.3 对比损失拉近同类推开异类训练目标用对比损失Contrastive Loss。给定一对样本 x1、x2y1 表示同语义y0 表示不同语义距离记为 d||f(x1)-f(x2)||₂损失写为L 0.5·y·d² 0.5·(1-y)·max(0, margin-d)²y1 时希望 d 趋近 0y0 时希望 d 至少大于 margin。margin 是负样本对要拉开的最小距离常用 0.5~1.0。margin 太小负样本挤在一起区分度差margin 太大收敛反而慢。实际训练时可以参考验证集上的相似度分布来调这个值。正负样本对的构造有一条铁律anchor 和 positive 不能来自同一张裁剪图。如果提示词参考图和正样本候选图是同一张图模型根本不用学语义直接复制粘贴特征就够了测试集一换图就露馅。负样本对里也不能全是背景块要混入“其他类别物体”的配对否则模型只学会区分前景和背景没学会区分不同语义。常见比例是负样本里一半背景、一半其他类别物体。配对采样还要注意类别均衡。构造 batch 时如果某个高频类别占了一大半loss 会被这个类别主导低频类别的匹配能力会变差。常见做法是按类别轮流采样保证每个 batch 里出现的类别数尽量均匀。另外可以顺手对参考图做一点随机增强比如亮度抖动、轻微旋转模拟提示图不清晰的情况能提升匹配鲁棒性。2.4 方案选型孪生网络、YOLO 还是 CLIP做点选识别时团队通常会先在这几个方案里纠结一遍。固定类别检测器YOLO 系列在类别固定的场景很强但点选提示词是动态的新增语义就要动模型结构CLIP 这类图文匹配模型做零样本识别可以覆盖开放语义但模型体积大还得拉外部权重离线小项目里部署成本偏高。孪生网络在动态语义、模型体积、离线训练三个维度上最均衡。方案动态语义新增提示词模型规模离线训练分类网络滑窗不支持需重训加类重训小可以YOLO 检测器仅支持固定类合并数据重训中可以CLIP 图文匹配支持换文本即可大依赖外部权重孪生网络支持准备一张参考图小完全离线选型也有边界。如果业务方明确只有 10 个固定类别且半年不换直接训 YOLO 更省事如果团队已经有现成的 CLIP 服务采纳文本匹配方案的工程成本可能更低。选型不迷信优先考虑的是未来三个月谁维护起来不累。孪生网络的优势在于拿到一个新提示词不需要重训只需要一张参考图就能开始推理。后面所有环节都围绕这个能力展开。3. 合成点选验证码数据集脚本、标注格式与类别隔离3.1 为什么先做合成数据而不是采集真实数据真实点选验证码很难拿到大规模带语义标签的数据。就算能拿到标注成本也很高要框出每个目标、标类别、写提示词几千张图就够标注团队忙几周。常见做法是用公开的实例分割数据集COCO、OpenImages里已有的掩码和类别名把目标物体从原图抠出来贴到随机背景图上自动生成“验证码图片 提示词 目标位置”。合成数据能完全控制类别数量、光照、遮挡、尺寸训练时还能随时产新样本天然抗过拟合。用 COCO 这类数据做前景库还有个好处类别名是现成的80 个常见类别足够覆盖大多数点选验证码的语义范围。如果业务里出现特殊类别再单独收集几十张该类物体图做补采就行。合成阶段不需要追求像素级真实只要物体边缘自然、位置随机、遮挡合理训练出来的匹配能力迁移到真实场景时就不会太虚。3.2 合成脚本把前景物体贴到背景图下面这段是数据合成的主流程脚本骨架省掉了 mask 多边形解码等细节但保留了核心逻辑。# make_point_click_dataset.py import json import random from pathlib import Path import cv2 import numpy as np def load_fg_bank(coco_json_path, img_root, min_area2500): 读取 COCO 格式标注按类别产出带 mask 的前景物体。 with open(coco_json_path) as f: coco json.load(f) fg_bank {} for ann in coco[annotations]: cat_id ann[category_id] x, y, w, h [int(v) for v in ann[bbox]] if w * h min_area: continue # 过滤太小的目标避免贴上去看不清 # mask 用 pycocotools.maskUtils.decode 从 RLE 转出来 # 原图路径由 coco[images] 索引 fg_bank.setdefault(cat_id, []).append({ img_path: img_file, mask: mask, bbox: [x, y, w, h], }) return fg_bank def paste_fg(bg, fg_img, fg_mask, rng): 把一个前景贴到背景图上返回贴图位置。 fg_h, fg_w fg_img.shape[:2] scale rng.uniform(0.15, 0.35) # 缩放物体约占图宽 15%~35% new_w, new_h int(fg_w * scale), int(fg_h * scale) fg_resized cv2.resize(fg_img, (new_w, new_h)) mask_resized cv2.resize(fg_mask, (new_w, new_h)) angle rng.uniform(-15, 15) # 小角度旋转 M cv2.getRotationMatrix2D((new_w / 2, new_h / 2), angle, 1.0) fg_rot cv2.warpAffine(fg_resized, M, (new_w, new_h)) mask_rot cv2.warpAffine(mask_resized, M, (new_w, new_h)) # 在背景上随机找一个不越界的位置 x rng.randint(0, bg.shape[1] - new_w) y rng.randint(0, bg.shape[0] - new_h) # 按 mask 做 alpha 融合物体边缘自然过渡 roi bg[y:y new_h, x:x new_w] mask_3c cv2.cvtColor(mask_rot, cv2.COLOR_GRAY2BGR) / 255.0 bg[y:y new_h, x:x new_w] fg_rot * mask_3c roi * (1 - mask_3c) return x, y, new_w, new_h def compose_one(bg_path, fg_bank, rng): 生成一张 640x400 的合成图返回图片和标注。 bg cv2.imread(bg_path) bg cv2.resize(bg, (640, 400)) n_obj rng.randint(3, 8) # 每张图 3~8 个物体 labels [] for _ in range(n_obj): cat_id rng.choice(list(fg_bank.keys())) item rng.choice(fg_bank[cat_id]) x, y, w, h paste_fg(bg, cv2.imread(item[img_path]), item[mask], rng) labels.append({category: cat_id, bbox: [x, y, w, h], center: [x w // 2, y h // 2]}) prompt rng.choice([l[category] for l in labels]) return bg, {prompt: prompt, targets: labels}这段脚本的核心逻辑是“抠图—缩放—旋转—融合”。min_area 参数过滤掉原图里太小的物体避免贴上去后难以辨认scale 范围 0.15~0.35 保证物体在整张图里的占比接近真实验证码中目标的大小旋转控制在 ±15° 是为了模拟自然拍摄角度同时不让物体变形到语义难辨。背景图在合成前统一 resize 到 640×400保证所有样本尺寸一致后面训练时不需要再做随机的 padding。合成时还有一个容易忽略的点背景图和前景图不能来自同一张原图。如果用了同一张图模型会学到“同一个物体周围的背景纹理”这种伪线索测试时一换背景就失效。实现上可以把 COCO 的图片按 7:3 分成两组一组只做前景库一组只做背景库从根上切断这种泄漏。3.3 标注 JSON 格式与参考图目录合成脚本输出的标注文件建议用下面这样的 JSON 格式字段清晰训练和推理都方便读取。字段类型说明imagestr合成图片相对路径width / heightint图片宽高固定 640 / 400promptstr本张图的提示词即目标类别名targetslist所有目标物体每项含 category、bbox、centertargets 里每个目标的 bbox 是 [x, y, w, h]center 是 [x w//2, y h//2]。落地上需要返回点击坐标直接用 center 即可。同时dataset 目录下还要维护一个 reference_imgs/ 文件夹每个类别放 3~5 张从原图裁剪出的“干净物体图”这些图就是孪生网络的参考分支输入。裁剪时要把物体完整包含进去四周留一点边不要贴物体贴到正好卡边。生成 JSON 时顺手统计一下各类别的样本数量。如果某些类别的目标数量明显偏少就在后续合成时加大该类别的采样权重。点选识别里每个类别至少要保证 200 次出现在训练样本中否则相似度匹配会偏向高频类。3.4 数据划分必须按类别隔离不能按图片随机切很多第一次做这个任务的人会在这里踩坑把图片随机按 8:2 划分训练集和测试集结果测试准确率很高一上线换新提示词就崩。原因很简单——红绿灯在训练集里出现过模型已经记住了红绿灯长什么样测试只是换了一张图而已并没有真正考验“按提示词找语义”的能力。正确做法是本文还有配套的精品资源点击获取