Siamese(孪生网络)

1. 前言:为什么要用孪生网络?

在传统的分类任务中,我们通常训练模型识别固定的类别(如:猫、狗、汽车)。但在现实场景中,我们经常面临以下挑战:

  • 类别过多且不固定:例如人脸识别,公司每天都有新员工入职,不可能每加一个人就重新训练一遍分类模型。
  • 样本极少 (One-Shot Learning):对于某些类别,我们可能只有一个样本,传统的深度学习难以收敛。

孪生网络 (Siamese Network) 换了一个思路:它不再学习“这是谁”,而是学习“这两个样本是否相似”。通过计算两个输入的特征向量之间的距离,它能够实现强大的泛化能力。


2. 网络概述

孪生网络由两个结构完全相同共享权重的子网络组成。

2.1 核心架构

  1. 输入层:输入两个样本(X1X_1X2X_2)。
  2. 编码层(子网络):两个样本分别通过相同的 CNN 架构。
  3. 特征映射:输出两个固定长度的特征向量 f(X1)f(X_1)f(X2)f(X_2)
  4. 距离计算:计算这两个向量之间的欧几里得距离(Euclidean Distance)或余弦相似度。
  5. 损失函数:根据距离和标签(相同为0,不同为1)计算损失,引导模型缩小同类距离,扩大异类距离。

2.2 共享权重的意义

“孪生”一词的精髓在于两个分支的参数是实时同步(Shared Weights)的。这意味着模型对两个输入的提取逻辑完全一致,保证了特征空间的一致性。


3. 详细网络结构:PyTorch 实现

我们以人脸比对任务为例,使用简单的 CNN 作为子网络。

3.1 定义孪生网络模型

import torch
import torch.nn as nn
import torch.nn.functional as F

class SiameseNetwork(nn.Module):
    def __init__(self):
        super(SiameseNetwork, self).__init__()
        
        # 定义子网络:通用的特征提取器
        self.cnn = nn.Sequential(
            nn.Conv2d(1, 64, kernel_size=5),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            nn.Conv2d(64, 128, kernel_size=5),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            nn.Conv2d(128, 256, kernel_size=3),
            nn.ReLU(inplace=True)
        )
        
        # 全连接层将特征展平为向量
        self.fc = nn.Sequential(
            nn.Linear(256 * 3 * 3, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, 128) # 最终特征向量维度为 128
        )

    def forward_once(self, x):
        # 单个分支的前向传播
        output = self.cnn(x)
        output = output.view(output.size()[0], -1)
        output = self.fc(output)
        return output

    def forward(self, input1, input2):
        # 同时通过两个分支(共享权重)
        output1 = self.forward_once(input1)
        output2 = self.forward_once(input2)
        return output1, output2

4. 关键:损失函数 (Contrastive Loss)

孪生网络通常不使用交叉熵,而是使用 对比损失 (Contrastive Loss)。其公式如下: L=12Nn=1N(1Y)D2+Ymax(marginD,0)2L = \frac{1}{2N} \sum_{n=1}^N (1-Y)D^2 + Y \max(margin - D, 0)^2

  • YY: 标签。若样本相同则 Y=0Y=0,若不同则 Y=1Y=1
  • DD: 两个特征向量之间的欧式距离。
  • marginmargin: 边距阈值。当不同类样本的距离超过这个值时,不再产生损失。
class ContrastiveLoss(nn.Module):
    def __init__(self, margin=2.0):
        super(ContrastiveLoss, self).__init__()
        self.margin = margin

    def forward(self, output1, output2, label):
        # 计算欧式距离
        euclidean_distance = F.pairwise_distance(output1, output2, keepdim=True)
        
        # label=0 (同类): 损失就是距离本身
        # label=1 (异类): 距离越近损失越大,直到超过 margin
        loss_contrastive = torch.mean((1-label) * torch.pow(euclidean_distance, 2) +
                                      (label) * torch.pow(torch.clamp(self.margin - euclidean_distance, min=0.0), 2))
        return loss_contrastive

5. 训练与推理逻辑

5.1 数据准备

训练孪生网络需要构建样本对(Pairs)

  • 正样本对:从同一个人的照片中随机选两张,标签设为 0。
  • 负样本对:从两个不同的人中各选一张,标签设为 1。

5.2 推理过程

  1. 输入两个待比较的图像。
  2. 经过网络得到两个 128 维向量。
  3. 计算距离 DD
  4. 设定一个阈值(如 0.5):
    • D<0.5D < 0.5,判定为“同一人”。
    • D0.5D \ge 0.5,判定为“不同人”。

6. 总结与进阶建议

孪生网络的优势:

  • 鲁棒性强:对光照、姿态变化有一定的抗干扰能力。
  • 小样本友好:通过对比学习,模型能学到更本质的区分特征。

进阶方向:

  • 三元组损失 (Triplet Loss):由 FaceNet 引入,输入由(原图、正例、负例)组成,比双分支的 Contrastive Loss 训练更高效。
  • Swin Transformer Siamese:将子网络换成强大的 Transformer 架构。