JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

當像素說謊:對抗樣本與防禦之道

幾個肉眼看不見的像素,就能讓自信的分類器淪為笑柄——了解對抗樣本為何存在、像 FGSM 這類攻擊如何打造,以及如何強化模型。

令人不安的發現

這是個讓深度學習社群初見時大為震驚的結果。拿一張熊貓的照片,餵給一個很強的影像分類器,它會以 99% 的信心回答「熊貓」——正是我們想要的。現在加上一層精心挑選、極其微弱的雜訊,弱到你的眼睛看不出任何差別:一樣的毛、一樣的竹子、一樣的黑眼圈。把這張新影像餵給同一個模型,它現在卻說「長臂猿」——而且信心高達 99.3%。不是「我不確定」,而是充滿自信、災難性地錯誤。這就是著名的「熊貓變長臂猿」故事,它並不是某個模型在某張圖上的小毛病,而是幾乎所有我們會訓練的神經網路都具有的性質。

我們先把這東西命名。對抗樣本是一種輸入:人看起來是一回事,模型卻讀成完全不同的另一回事——因為有人刻意加上了一個微小而有結構的擾動,專門設計來騙過模型。「刻意」這兩個字分量很重。這不是相機在昏暗光線下加進的隨機雜訊(那種任何像樣的模型都能輕鬆忽略),而是一個量身打造的推力:針對「這一個」特定模型計算出來,精準地指向能用最小的可見變化造成最大破壞的方向。

分類器把像素對應到一個帶信心的標籤。令人震驚的是,即使標籤翻成荒謬的答案,信心仍然居高不下。

影像分類流程圖:一張影像進入神經網路,輸出一個帶信心分數的類別標籤。

除了獵奇之外,你為什麼該在意?因為視覺模型愈來愈常做出攸關後果的決定。研究者曾在停車標誌上貼了幾張看似無害的貼紙,就讓自駕車的偵測器把它讀成速限標誌。攻擊者曾把違禁影像的像素只推動到剛好的程度,就溜過內容審查過濾器,而在人類審查員眼中看起來毫無變化。人臉辨識閘門、醫學影像分流系統、詐欺篩檢掃描器——任何由模型把關真實世界行動的地方,平均準確率與最壞情況穩健性之間的落差,就會變成一個安全漏洞。本篇就是要理解這個漏洞,並學會把它補起來。

對抗樣本為何存在?

我們很容易以為對抗樣本只代表模型沒訓練好,以為更大或更聰明的網路就不會上當。真相比這更奇怪、也更根本,所以我們要小心地把直覺建起來,而不是含糊帶過。你需要的兩個概念是:高維度,以及決策邊界。

先從維度說起。一張不大的彩色影像,224x224 像素,就有 224 x 224 x 3 ≈ 150,000 個數字。每個數字是一個維度,所以一張影像就是一個點,落在大約有 150,000 條軸的空間裡。接著是高維度的關鍵算術:如果我們把這 150,000 個數字每一個都改動一丁點——比方說一個亮度小步,小到在任何單一像素上眼睛都看不出來——這些改動在「加總起來」時並不會還是一丁點。每個像素一聲耳語,乘上 150,000 個像素、又對齊在剛好的方向上,就會變成一聲吶喊。攻擊者在每個像素上只花費了看不見的預算,卻收集到一個巨大的總推力,因為可以推的像素實在太多了。

再來看決策邊界。一個訓練好的分類器,把它的高維輸入空間切成一塊塊區域,每個類別一塊,區域之間以邊界隔開——在這些界面上,預測標籤會從比方說「熊貓」翻成「長臂猿」。令人不安的經驗事實是:真實影像往往出乎意料地「靠近」這些邊界。在局部、在任何一個輸入附近,深度網路的行為幾乎像一把直尺(在小鄰域裡大致是線性的),所以要跨過一條邊界,你不需要走遠——只要朝它直直走過去就行。把這點和上面的高維槓桿合起來:每個像素一個微小而協調的步伐,瞄準垂直於最近邊界的方向,就足以跨過它、落進錯誤的區域。

模型把空間分割成各類別的區域。一個乾淨的點(真實影像)往往就坐落在邊界附近;朝對的方向一個微小推力,就把它帶過邊界、落到不同的標籤。

二維示意圖:一條決策邊界在兩個類別區域間彎曲,一個資料點位於邊界附近,一個短箭頭把它推到另一側。

最後一塊拼圖:為什麼人類不會上當、網路卻會。你的視覺系統抓的是穩健的、語義的結構——身體的形狀、臉部的比例、四肢的配置。一個卷積網路,只被訓練去把分類誤差降到最低,便可以「順便」利用非穩健特徵:像素中那些微弱、高頻的統計規律,它們在訓練資料上剛好和正確標籤相關,卻不帶任何人類會認得的意義。這些特徵在乾淨資料上確實有預測力,這正是模型會學到它們的原因——但它們很脆弱,而且正是對抗擾動所改寫的東西。攻擊者並不是加上你看得見的「長臂猿」紋理;而是悄悄編輯了模型私下倚賴的那些看不見的特徵。

打造攻擊:快速梯度符號法(FGSM)

現在我們來打造第一個真正的攻擊,慢慢來,因為它是後面每個攻擊都依此變奏的範本。先回想訓練是怎麼運作的,因為攻擊正是它的鏡像。訓練時,我們算出損失 J——一個衡量模型預測有多錯的數字——然後算出這個損失對「權重」的梯度。梯度指向會讓損失增加最快的方向,所以訓練朝「相反」方向走一小步,輕輕調整權重讓損失變小。重複幾百萬次,模型就變好了。影像維持不動,動的是權重。

快速梯度符號法——FGSM——做的是一個優雅的反轉。把權重凍結(模型已經訓練好了,我們不能改它)。改而去算損失對「輸入像素」的梯度。這個梯度回答的是一個新問題:我該往哪個方向改動每個像素,才能讓損失「變大」——也就是讓模型更錯?接著,我們不像訓練那樣往下坡走,而是往損失的「上坡」走,把影像推向更大的誤差。朝那個方向走一步,我們可能就得到了一個 對抗樣本

x_{adv} = x + \epsilon \cdot \operatorname{sign}\big(\nabla_x J(\theta, x, y)\big)

FGSM:取損失對輸入梯度的符號,乘以一個微小預算,加到乾淨影像上。

我們把每個符號都拆開來,因為這條公式就是整篇的縮影。x 是乾淨的輸入影像(真正的熊貓)。θ(theta)是模型的權重,在這裡是「凍結」的——我們從不改動它,括號只是提醒我們損失依賴於它。y 是真實標籤(「熊貓」)。J(θ, x, y) 是損失:在給定真實標籤下,模型對這張影像錯得多離譜。∇_x J(讀作「J 對 x 的梯度」)對「每一個」像素都給出一個數字,告訴我們把那個像素往上或往下推一點點會讓損失升高多少——也就是讓模型更錯的逐像素方向。sign(·) 丟掉這些數字各自的大小,只留下它的方向:梯度為正就是 +1,為負就是 −1。最後 ε(epsilon)是擾動預算——一個固定的小數,比方在 0 到 1 的像素尺度上是 0.01——它決定我們踏出的步伐有多大。

有兩個問題值得清楚回答。第一,為什麼取「符號」而不直接用原始梯度?因為原始梯度在某些像素上很大、在另一些像素上很小,照著它走會把大部分變化都倒到少數幾個像素上——形成看得見的斑點。取符號則在每個像素上花費「相等」的微小預算 ε:每個像素恰好移動 +ε 或 −ε,絕不更多。這正是讓變化保持看不見、卻仍能把每個像素都往有利方向推的原因。這也是為什麼我們說 FGSM 在 L-無限大意義下限制了變化——沒有任何單一像素移動超過 ε(下一節我們會把這個範數講清楚)。第二,為什麼往損失的「上」走?訓練往下走是為了讓模型答對;我們往上走是為了讓它答錯。沿著損失梯度往上,依定義就是局部上通往更高誤差最快的路——直直朝最近的決策邊界。

注意 ε 控制的取捨。把 ε 調小,攻擊更看不見但也更弱——它可能沒辦法把影像一路推過邊界。把 ε 調大,攻擊更強,但開始留下淡淡的可見瑕疵。整場對抗穩健性的較量都活在這個旋鈕裡:在一個小到人類根本看不出被動過手腳的預算之內,模型能挺住多少?

import torch
import torch.nn.functional as F

def fgsm_attack(model, x, y, epsilon):
    # x: a clean image batch, y: the true labels, model: a trained (frozen) classifier
    x = x.clone().detach()
    x.requires_grad_(True)          # we want the gradient w.r.t. the INPUT, not the weights

    # 1. Forward pass: how wrong is the model on this image?
    logits = model(x)
    loss = F.cross_entropy(logits, y)

    # 2. Backprop the loss all the way back to the input pixels
    model.zero_grad()
    loss.backward()                 # fills x.grad with the per-pixel gradient of the loss

    # 3. Keep only the DIRECTION of each pixel's gradient (+1 / -1)
    perturbation = epsilon * x.grad.sign()

    # 4. Step UPHILL on the loss to make the model more wrong
    x_adv = x + perturbation
    x_adv = torch.clamp(x_adv, 0.0, 1.0)   # keep pixels in a valid image range
    return x_adv.detach()
FGSM 四個意圖步驟:前向、反向傳播到輸入、取符號、加上 epsilon 乘符號。只走一步——快速,而且刻意粗糙。

威脅模型與更強的攻擊

在打造更強的攻擊之前,我們必須先把這場較量的規則講清楚。在資安裡這叫做「威脅模型」,它回答兩個問題:攻擊者能「看見」什麼,以及攻擊者能「改變」什麼?這裡含糊了,你就會騙了自己,以為某個防禦有效,其實你只是拿它去對付一個弱攻擊者罷了。

關於攻擊者能「看見」什麼,有兩種經典設定。在「白箱」攻擊中,攻擊者知道一切:架構與確切的權重 θ,所以能直接計算 ∇_x J——這是測試時最強、最誠實的設定。在「黑箱」攻擊中,攻擊者看不到權重,只能用輸入去戳模型、讀它的輸出。黑箱聽起來對防禦者比較安全,但它對真實部署更貼近現實,而且我們將會看到,它比表面上更危險。

關於攻擊者能「改變」什麼,我們用一個範數來限制擾動。這個約束寫成 x_adv 可以偏離 x 多遠的預算。

\lVert x_{adv} - x \rVert_p \le \epsilon

被允許的擾動,住在以乾淨影像為中心、半徑 ε 的 L_p 球之內。

這樣讀:對抗影像與乾淨影像之間的差,用 L_p 範數來量,不得超過 ε。下標 p 選擇我們「如何」量大小,這影響很大。當 p = ∞(L-無限大)時,範數是所有像素中「最大」的那個變化,所以約束說的是「沒有任何單一像素可以移動超過 ε」——這正是 FGSM 在到處加 ±ε 時所遵守的預算。當 p = 2(L2,普通的歐氏距離)時,範數量的是所有像素變化的「總能量」,所以約束說的是「變化的總量加起來必須保持很小」——這允許少數像素移動多一點,只要總量仍在預算內。所有滿足約束的影像構成一個以 x 為中心、半徑 ε 的球;攻擊者可以在球內任意遊走,但不能踏出球外,這就是讓擾動保持看不見的關鍵。

現在來看 FGSM 的升級版。投影梯度下降(PGD)一句話講完,就是「綁了繩子的、反覆執行的 FGSM」。它不走一個大的符號步,而是走許多小的符號步,並在每一步之後把影像「投影」回 L_p 球之內,讓總預算永遠不被超過。因為它在每一步之後都重新計算梯度,所以能隨著地形彎曲而朝邊界轉彎,找到一個比單發直射糟糕得多的擾動。

x_{t+1} = \operatorname{Clip}_{x,\,\epsilon}\Big( x_t + \alpha \cdot \operatorname{sign}\big(\nabla_x J(\theta, x_t, y)\big) \Big)

PGD:先走一個大小為 α 的 FGSM 式步伐,再彈回 ε 預算之內。重複許多次。

解碼一下:x_t 是第 t 步時的擾動影像(我們從 x_0 = x,也就是乾淨影像開始,有時再加一點隨機抖動)。每一步我們走一個 FGSM 式的移動,但用較小的步長 α(alpha)而非整個 ε,因為我們會走很多步,不希望任何一步衝過頭。∇_x J(θ, x_t, y) 是在「當前」擾動影像 x_t 處算出的輸入梯度——這個重新瞄準,正是 PGD 比 FGSM 更強的原因。最後 Clip_{x,ε}(·) 是投影:每一步之後,任何偏離原值 x 超過 ε 的像素,都被彈回預算的邊緣。畫面是這樣的:PGD 反覆朝更高的損失走,但被繩子綁在 ε 球內,所以它搜尋得更努力卻同樣看不見。這就是為什麼 PGD 是人們用來「壓力測試」防禦的標準強攻擊——如果你的模型只能挺過 FGSM、卻在 PGD 之下崩潰,那它從來就不是真的穩健。

防禦:對抗穩健性

該來防禦了。首先我們得改變對「好」的定義。普通準確率問的是:平均而言、在典型輸入上,模型多常答對?對抗穩健性問的是更嚴苛的問題:對每一個輸入,如果允許對手在 ε 預算內任意擾動它,模型「仍然」多常答對?這是最壞情況準確率,不是平均情況。一個模型可以有 95% 的乾淨準確率、卻只有 2% 的穩健準確率——意思是攻擊者能在 98% 的影像上破解它。在對抗環境中部署、卻只報告第一個數字,正是這整條軌一再警告的那種失誤。

領先的防禦美在直接:如果模型在對抗樣本上失敗,那就「拿對抗樣本來訓練它」。這就是對抗訓練。在每個訓練步中,更新權重之前,你先跑一個攻擊(通常是 PGD)來生成當前批次的最壞擾動,然後教模型去把「那些」被破壞的影像分類正確。模型實際上等於被接種了疫苗——反覆暴露在它將面對的最強攻擊之下,於是學到能挺過這些攻擊的特徵。我們可以把這寫成一個 min-max(極小極大)目標。

\min_{\theta}\; \mathbb{E}_{(x,y)}\Big[\; \max_{\lVert \delta \rVert_p \le \epsilon}\; J(\theta,\, x + \delta,\, y) \;\Big]

穩健最佳化:內層的對手在預算內把損失最大化;外層的訓練者把那個最壞情況最小化。

從裡往外讀——這是兩個玩家的對決。內層那部分,對 δ(delta)取 max、且 ||δ||_p ≤ ε,是「攻擊者」:在給定當前權重下,找出預算內那個讓損失 J(θ, x + δ, y) 盡可能「大」的擾動 δ——也就是這一個樣本的最壞情況。(實務上我們用跑 PGD 來近似這個 max。)包在外面的 E_{(x,y)} 是「期望值」,對我們資料中所有(影像、標籤)配對取平均——所以我們在意的是整個資料集上平均起來的最壞情況,而不只是一張圖。最外層那部分,對 θ 取 min,是「防禦者」:選擇那組權重 θ,讓「就連那個最壞情況、平均起來的損失」也盡可能地「小」。白話說:為最壞情況做準備,而不是為平均情況。把模型訓練到,沒有任何預算內的微小擾動能把它推進一個錯誤。

對抗訓練是資料增強的強悍表親:它增強用的不是隨機裁切與翻轉,而是攻擊者找到的、預算內最具破壞力的那個擾動。

一張示意圖:一張來源影像被轉換成數個用於訓練的變體。

把對抗訓練看成普通資料增強的好戰表親會很有幫助。標準的資料增強給模型看隨機裁切、翻轉、色彩抖動與亮度變化,讓它學會忽略無關的變異。對抗訓練做的是同一件事,但用的是一個帶敵意、最佳化過的變異:它注入的不是隨機變化,而是攻擊者在 ε 之內能造成的「最具破壞力」的那一個變化。同樣的想法——拓寬模型見過的東西——但瞄準的是最壞情況,而非典型雜訊。

誠實地評估穩健性

我們以一個成熟度課作結,它與第二篇關於「玩弄指標」的警告相互呼應。在防禦的軍備競賽中,數十個發表過的方法宣稱有強穩健性,然後在數週內被攻破。常見的元兇有個名字:梯度遮蔽,也叫做「混淆梯度」。這種防禦其實並沒有讓模型變穩健——它只是讓損失梯度 ∇_x J 對攻擊者變得沒用:充滿雜訊、破碎、或歸零。於是像 FGSM 或 PGD 這類基於梯度的攻擊找不到方向,回報出接近零的攻擊成功率,看起來像是絕佳的 對抗穩健性。那是海市蜃樓。

為什麼是海市蜃樓?因為遮蔽梯度只是把邊界對「這個」攻擊者藏了起來,並沒有對「所有」攻擊者藏起來。決策邊界仍然就在影像旁邊;防禦只是移走了指向它的路標。換上一個更強的攻擊——一個用數值估計梯度的、或一個根本不需要梯度的黑箱攻擊、或一個在替身模型上打造出的可遷移對抗樣本——所謂的穩健性就蒸發了。看起來像 90% 的穩健準確率,原來只有 3%。

退一步,注意我們在這裡所做一切的邊界。對抗擾動是一種「最壞情況」的輸入變化:一個聰明的對手,努力最佳化,在微小的預算內,故意把你弄垮。這是資安威脅的正確模型。但大多數真實世界的失敗根本沒有對手。相機換了、光線變了、醫院用了不同的掃描儀、部署的城市有訓練集從沒見過的天氣。輸入分布只是單純地漂離了訓練——沒有人精心設計,模型卻照樣退化。

而那正是下一篇要去的地方。我們從對抗的最壞情況,走向自然的最壞情況:分布偏移——當模型遇到的世界不再是它受訓時的那個世界,會發生什麼事、如何偵測落在分布之外(out-of-distribution)的輸入、以及如何跨領域做調適。同樣令人謙卑的主題,但這次不需要反派。