度量學習:Siamese 網路與 Triplet Loss - 人臉門禁的少樣本比對

度量學習:Siamese 網路與 Triplet Loss - 人臉門禁的少樣本比對

新員工到職只有一張註冊照,分類器束手無策——因為門禁要的不是「認得誰」,是「會認人」。本篇介紹度量學習:Siamese 雙塔架構、Contrastive 與 Triplet Loss、FaceNet 的難例挖掘,並完整公開一段訓練踩坑實錄:從輸給 30 年前的特徵臉,到嵌入塌縮,再到 BatchNorm 逆轉登頂。


核心貢獻者

Jane Bromley 與 Yann LeCun (AT&T 貝爾實驗室) 於 1993 年為「支票簽名驗證」發明了 Siamese 網路——連體嬰般共享權重的雙塔架構,名字就取自連體雙胞胎。沉寂二十年後,Florian Schroff 等人 (Google) 於 2015 年發表 FaceNet,用 Triplet Loss 加上難例挖掘,在人臉驗證任務上一舉超越人類水準 (99.63% on LFW),奠定了至今所有人臉門禁系統的技術底座。


為什麼門禁不能用分類器?

假設用 Day 13 的 CNN 做 40 個員工的人臉分類器,馬上撞牆:

  1. 新員工到職怎麼辦? 分類器的輸出層寫死 40 類,加一個人就要改架構、重新訓練。
  2. 每人只有一張註冊照:分類器要每類幾百張才學得動,HR 只會給你一張大頭照。
  3. 離職員工怎麼辦? 又要重訓。

問題出在提問方式。分類器學的是「這是誰」,但門禁真正需要的是「這兩張是不是同一個人」——前者綁死名單,後者是一種通用能力。度量學習 (Metric Learning) 學的就是後者:把照片映射到一個嵌入空間,同一人的照片距離近、不同人距離遠。學會之後,任何陌生人都適用:新員工到職,把註冊照的嵌入向量存進資料庫就完事,模型一個參數都不用動


1. 資料集來源

資料集:Olivetti Faces (AT&T)

備註:sklearn.datasets.fetch_olivetti_faces 一行載入。40 人、每人 10 張、64×64 灰階——正好是「小工廠門禁」的規模感。

實驗設計:關鍵在「陌生人」

  • 訓練集:前 30 人 (300 張)——模型只從他們身上學「怎麼比較人臉」。
  • 測試集:後 10 人 (100 張)——模型從沒見過的陌生人,考兩個門禁任務:
    1. 驗證 (1:1):兩張照片是不是同一人?(450 對本人 + 450 對冒充者,看 AUC)
    2. 單張註冊識別 (1:N):每人只用第 1 張照片當註冊照,其餘 90 張當刷臉查詢,看 Rank-1 命中率。
  • 對照組:原始像素距離、PCA-50 特徵臉 (Day 08 的老朋友,1991 年的技術)。

2. 原理

2.1 Siamese 架構:一個網路,照兩次鏡子

配對範例

兩張照片分別通過同一個 (共享權重的) 嵌入網路 fθf_\theta,各得到一個向量,然後只比較距離:

d(x1,x2)=fθ(x1)fθ(x2)2d(x_1, x_2) = \| f_\theta(x_1) - f_\theta(x_2) \|_2

「雙塔」其實是同一座塔照兩次——這保證了嵌入空間的一致性:比較的標準對所有人都一樣。

2.2 Contrastive Loss:拉近本人,推開陌生人

L=yd2+(1y)max(0,  md)2L = y \cdot d^2 + (1-y) \cdot \max(0,\; m - d)^2

  • y=1y=1 (同一人):最小化 d2d^2,把兩者拉近
  • y=0y=0 (不同人):只要距離小於邊界 mm 就懲罰,把兩者推開到 m 之外——超過 m 就不管了,不浪費力氣。

2.3 Triplet Loss:三人行必有我師

FaceNet 的升級版一次看三張:錨點 (Anchor)、同一人的正例 (Positive)、別人的負例 (Negative):

L=max(0,  d(a,p)d(a,n)+m)L = \max(0,\; d(a, p) - d(a, n) + m)

  • 目標:本人距離要比陌生人距離近至少 m——學的是「相對排序」而不是絕對距離,更貼近門禁的實際決策。
  • 難例挖掘 (Batch-Hard Mining) 是靈魂:隨機抽的三元組大多太簡單 (損失為 0,白算)。FaceNet 的做法是每批抽 P 人 × K 張,對每個錨點只挑最遠的本人最近的陌生人來學——專攻長得像的陌生人與狀態差很多的本人。

2.4 訓練踩坑實錄:一段誠實的調參記

這篇的模型不是一次到位的,四個版本的心電圖值得公開:

版本配方陌生人驗證 AUC診斷
v1Contrastive、隨機配對、30 輪0.876欠訓練,輸給 1991 年的特徵臉
v2同上、300 輪 + 翻轉增強0.896損失有降,泛化沒跟上——隨機配對太簡單
v3Batch-Hard Triplet、450 迭代0.916追平基準;但損失長期卡在 0.20 (= margin)
v4v3 + BatchNorm + 900 迭代0.962反超登頂
  • v3 的症狀是經典教材:損失恰好停在 margin 值,代表嵌入塌縮 (Collapse)——網路把所有照片映射到幾乎同一個點,d(a,p)d(a,n)0d(a,p) \approx d(a,n) \approx 0,損失自然等於 m。
  • v4 的解方:卷積層之間插入 BatchNorm,穩住特徵分布,塌縮立刻解除 (300 迭代後訓練損失歸零)。從頭訓練嵌入網路,BatchNorm 不是選配

3. 實戰

Python 程式碼實作

class EmbedNet(nn.Module):
    def __init__(self, dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
            nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, dim))

    def forward(self, x):
        return nn.functional.normalize(self.net(x), dim=1)   # 單位球面上比距離

def batch_hard_triplet(Z, labels, margin=0.2):
    """FaceNet 式難例挖掘:每個錨點只學最難的正例與負例"""
    D = torch.cdist(Z, Z)
    same = labels[:, None] == labels[None, :]
    hardest_pos = D.masked_fill(~same, 0).max(1).values            # 最遠的本人
    hardest_neg = D.masked_fill(same, float('inf')).min(1).values  # 最近的陌生人
    return (hardest_pos - hardest_neg + margin).clamp(min=0).mean()

程式碼重點:

  • 嵌入向量做 L2 正規化,所有人臉都住在單位球面上,距離範圍固定 (0 到 2),門檻才好訂。
  • 批次組成必須是 P 人 × K 張 (本篇 15×4),難例挖掘才有料可挖。

4. 模型評估

三方法 × 兩任務總成績 (10 個陌生人)

方法驗證 AUC (1:1)單張註冊識別 Rank-1 (1:N)
Raw pixels0.91580.6556
PCA-50 特徵臉 (1991)0.91810.7000
Siamese embedding (v4)0.96220.7778

距離分布:門禁決策的全貌

距離分布對比
  • 左圖 (原始像素):綠 (本人) 紅 (冒充者) 大面積重疊——不管門檻切哪裡,都是大量誤放行或誤拒門。
  • 右圖 (Siamese):兩座山明顯分開,門檻切在 0.8 附近就能兼顧。門禁系統的品質,就是這兩座山的距離

嵌入空間:從沒見過的人,自動各聚一團

嵌入空間視覺化

10 個陌生人的 100 張照片投影到 2D——模型從未見過任何一位,他們卻在嵌入空間自動聚成一團一團。這就是度量學習的本質:它學會的不是 40 張臉,是「人臉相似性」這個概念本身。

那個 1.11 的難例

回頭看配對範例圖:第一組明明是同一人,學到的距離卻高達 1.11——比第四組陌生人配對 (1.00) 還遠。眼鏡反光加表情變化,連模型都會看走眼。工安落地的對應設計:門檻內直接放行、門檻外不是拒絕而是「請再刷一次/轉人工」——誤拒率每提高 1%,員工每天早上的怨氣都會告訴你。

工安視角的解讀

  • 承攬商管理是真正的主場:廠區每天進出的施工承攬商,每人只有進場申請時的一張證件照——這正是「單張註冊識別」場景,分類器無解,度量學習原生支援。
  • 個資的意外好處:資料庫存的是 64 維嵌入向量,不是照片本身;向量無法還原成人臉,對《個資法》的遵循比存照片友善得多。
  • 誠實的提醒:本篇 0.96 的 AUC 是教學規模;產線等級的門禁 (FaceNet、ArcFace) 靠的是百萬級身分的預訓練——自建門檻高,商用方案的內核也是同一套數學。

5. 總結

我們學習了度量學習與 Siamese 網路:

  • 換一個問題:不學「這是誰」,學「像不像」——新員工註冊一張照片即用,模型零重訓。
  • 兩代損失函數:Contrastive (拉近/推開) 到 Triplet (相對排序),而難例挖掘才是 FaceNet 超越人類的關鍵。
  • 踩坑實錄:隨機配對學不動、損失卡在 margin 是塌縮警報、BatchNorm 是從頭訓練的救命符——AUC 從 0.876 一路修到 0.962。
  • 工安啟示:門禁與承攬商管理天生是少樣本問題;嵌入向量比照片更省、更快、更合規。

下一篇把「不用標籤」推到極致:監視器每天錄下數萬張無人標註的畫面,能不能讓模型自己教自己?自監督與對比學習 (SimCLR、DINO)——標註費太貴,就讓資料自己當老師。