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

訓練真正能用的 GAN

GAN 成功時驚艷,失敗時令人抓狂——本篇剖析它為何崩壞,以及馴服它的優雅解法。

GAN 為何難馴:不穩定性問題

在這個系列前面的內容裡,訓練網路只有一件事:挑一個損失,然後把它往下推,直到推不動為止。生成對抗網路卻把這份安穩丟掉了。它讓兩個網路彼此對抗——一個生成器努力偽造逼真的影像,一個判別器努力抓出假貨——而且沒有單一的損失可以最小化。兩者追逐的,是一個脆弱的平衡點:在這個平衡上,生成器的假貨好到讓判別器只能像擲硬幣一樣猜。麻煩在於,這種平衡點很容易衝過頭,也很容易完全錯過。

實際操作起來是什麼感覺?一般網路的損失曲線會滑下來、然後變平——你看得到它收斂。但 GAN 的兩條對抗損失通常會永遠來回震盪:生成器的損失下降、判別器的損失上升,接著兩者互換,一次又一次,像一場永不結束的拔河。樣本品質也跟著擺盪——這個 epoch 是漂亮的人臉,下個 epoch 就成了融化的惡夢。有時候訓練乾脆直接發散:其中一方把另一方壓制得太徹底,梯度爆炸或消失,這次訓練就死了。這些都不是你程式碼裡的 bug,而是雙人賽局與生俱來的行為。

這份指南接下來,就是帶你逐一走過這份平衡會以哪些具體方式崩壞,並搭配每一種的修法。我們會從最惡名昭彰的失敗開始——模式崩潰,也就是生成器偷懶。接著深入挖掘 GAN 在訓練初期為什麼會給出糟糕訊號的數學原因,這會很自然地引出 Wasserstein GAN,也就是最重要的單一穩定化想法。之後是一箱比較小、但很實用的技巧,以及該怎麼真正衡量成功。最後我們會談條件式 GAN,這個升級讓你能告訴生成器「要做什麼」——也是通往最後一份指南的橋樑。

模式崩潰:當偽造者偷懶

想像一個偽鈔犯,他唯一的工作就是讓假鈔騙過一位檢查員。經過反覆嘗試,這個偽造者發現了一張鈔票——比方說某張特定的假 20 元——檢查員每一次都會放行。理性的偽造者接下來會怎麼做?不會去拓展二十元、五十元、一百元的版圖,而是把那張「神奇的 20 元」一印再印。這正是模式崩潰生成器發現了少數幾種(有時真的只有一種)能可靠騙過偵探的輸出,於是就懶得再產生別的東西了。

為什麼這個賽局會主動獎勵這種偷懶?仔細看生成器被優化的目標:每一步它只需要在「當下」騙過判別器。它的損失裡沒有任何一條寫著「要涵蓋真實資料的所有多樣性」。如果判別器此刻有一個盲點——某一塊它判定為「非常真」的影像空間——那麼降低生成器損失最快的方法,就是把所有輸出全堆進那個盲點裡。生成器既不記得自己曾經產生過的多樣性,也不會因為放棄多樣性而受罰。它只是短視地在「利用」判別器,而不是誠實地在「建模」這個世界。

只要知道徵兆,診斷其實很直接。給訓練好的生成器餵入許多「不同」的隨機雜訊向量,把輸出並排來看。健康的生成器會給你一整面多樣的畫廊;崩潰的生成器則不管你怎麼改輸入,都吐出幾乎一模一樣的影像——雜訊進去了,多樣性卻沒出來。部分崩潰更隱微:你可能只拿得到正面人臉、永遠沒有側臉,或是一個數字生成器只會產生 1、7、9,卻一個 4 都生不出來。

回想本系列第一份指南的流形概念:某一類的真實影像並不會填滿整個像素空間——它們落在其中一張薄薄的、彎曲的曲面(流形)上,而所謂「生成」,就是學會降落到那張曲面上的任何地方。用這套語言來說,模式崩潰意味著生成器只學會降落在真實流形的一小塊上。它產生的點確實「在」曲面上(所以單看每一張都很真),卻丟掉了曲面幾乎所有的廣度。「夠真但不夠多樣」正是崩潰的招牌特徵。

把生成看成在資料流形上取樣:每個隨機輸入都應該降落到真實曲面上不同的點。模式崩潰卻把每個輸入都擠進同一塊小區域。

示意圖:隨機潛在輸入經過生成器,對應到散布在彎曲資料流形上的各個點。

根本原因:當分布互不重疊

模式崩潰是看得見的症狀;現在讓我們掀開引擎蓋,看更深一層的不穩定,因為它能解釋為什麼「原始」的 GAN 損失這麼難訓練。訓練初期,生成器爛得徹底——它的假貨跟真影像八竿子打不著。於是假分布和真分布幾乎落在影像空間裡完全分開的兩個角落,重疊極少。面對這樣兩堆涇渭分明的資料,判別器的工作輕鬆得很:它可以畫出一條乾淨的界線,把真的判成真、假的判成假,準確率近乎完美、信心也近乎百分之百。

殘酷的轉折在這裡。生成器要進步,靠的是「穿過」判別器回流的梯度——這個訊號是它唯一的指南針,告訴它往哪個方向能讓假貨更像真。但一旦判別器幾乎完美、又信心爆棚,它在生成器目前所處的整片區域上,輸出全被釘死在「假=0」。一個處處等於 0 的平坦函數,斜率(幾乎)為零,所以回傳給生成器的梯度小到趨近於無。它被斬釘截鐵地告知「錯了」,卻得不到任何「哪邊比較暖」的提示。想像一位老師把每個答案都批「錯」,又拒絕說你有沒有接近——你根本不知道怎麼改進。一個太強的判別器,正是這種幫不上忙的老師。

這件事可以講得很精確。標準的 GAN 目標,在判別器被訓練到最優時,數學上等價於逼著生成器去最小化假分布與真分布之間的 Jensen–Shannon 散度——這是一種衡量兩個機率分布差多少的特定指標。這個指標的致命特性是:當兩個分布幾乎不重疊時,它會飽和到一個常數(它的最大值)而不再變化。一個卡在常數上的量,梯度為零——所以當兩者的支撐集越漂越遠,這個損失對「該怎麼把它們拉近」根本「什麼都沒說」。這就是平坦梯度問題的數學根源,也是模式崩潰的溫床,因為一個訊號微弱、暈頭轉向的生成器,很容易滑進「重複一個好輸出」這種廉價的勝利。

Wasserstein GAN:更平滑的距離可供學習

Wasserstein GAN(WGAN)把那個惹麻煩的距離換成一個溫和得多的距離。把每個分布想成地上的一堆土:真實資料是一種形狀的土堆,生成資料是另一種形狀。Wasserstein 距離——外號「推土機距離」——指的是把假土堆重新鏟成真土堆所需的最小總工作量,這裡「工作量」等於你搬動多少土、乘上每一鏟推了多遠。兩堆離得遠,合併起來代價就大;兩堆幾乎疊在一起,代價就幾乎為零。Wasserstein GAN訓練生成器去縮小這個推土的代價。

魔法就發生在兩堆完全不重疊時——這正是把原始損失搞垮的那種情況。Jensen–Shannon 散度只會回報「差到最大」然後拉成一條水平線。推土機距離卻會回報一個真實的數字,而且這個數字會隨著兩堆相距多遠而平滑地變化:把假土堆往真土堆挪近一點,代價就降一點,每次都是如此。這種平滑、永遠帶有資訊的回應,正是生成器原本求之不得的「非零梯度」。現在,就算在第一步,生成器也永遠知道哪邊比較「暖」。

W \;\approx\; \mathbb{E}_{x\sim p_{\text{data}}}\!\big[\,D(x)\,\big] \;-\; \mathbb{E}_{z\sim p_z}\!\big[\,D\big(G(z)\big)\,\big]

WGAN 的價值:評論家 D 想要最大化它,生成器 G 想要最小化它。

我們慢慢讀。D 不再是一個吐機率的偵探;在 WGAN 裡我們改叫它評論家(critic),它輸出的是一個單純的實數分數——「這看起來有多真」——沒有把它壓進 0 到 1 的 sigmoid。第一項是 D(x) 對「從資料分布 p_data 抽出的真實影像 x」取平均,也就是評論家在真照片上的平均分數。第二項是 D(G(z)) 對「從先驗 p_z 抽出的雜訊向量 z」取平均,也就是它在假貨上的平均分數,其中 z 是隨機雜訊向量,G(z) 是生成器用它造出的影像。W 就是這兩個平均的差距。評論家想要「最大化」這個差距——真的打高分、假的打低分——而生成器想要「最小化」它,辦法是做出能拿高分的假貨。具體一點:若評論家給真照片平均 +8、給目前的假貨平均 −5,那麼 W 大約是 13;隨著生成器進步、它的假貨開始拿到比方說 +6,差距就朝 0 縮小——這個逐漸縮小的數字,就是生成器的進展。還有一個討喜的紅利:因為這個分數是真正的距離估計、而不是會飽和的機率,它的數值其實會「跟著」樣本品質走,於是損失數字第一次會與影像變好同步往下走——一個真正的進度表,而不是毫無意義的震盪。

有一個附帶條件,讓推土機詮釋得以成立:評論家必須是 1-Lipschitz 的。白話說,就是評論家不准隨著輸入改變而把分數變得太快——兩張影像很接近,分數就必須接近;輸入每變動一單位,分數最多只能升一單位。少了這條韁繩,評論家大可以把分數灌到正負無限大,讓那個差距失去意義,平滑距離的保證也就蒸發了。最初的 WGAN 用很粗糙的權重裁剪(weight clipping)來強制它(把每個權重壓進像 −0.01 到 0.01 這樣的小範圍)。更好、現在已成標準的做法是梯度懲罰(gradient penalty):在評論家的損失裡加一項,只要它對輸入的梯度大小偏離 1 就懲罰它。這會柔性地把評論家釘在 1-Lipschitz 條件上,讓推土機的意義保持誠實,也正是讓梯度平滑、可訓練的關鍵。

# One WGAN-GP step. The critic is updated several times per generator update.
for _ in range(n_critic):                 # e.g. 5 critic updates per G update
    z    = sample_noise(batch)            # latent vectors   z ~ p_z
    real = sample_data(batch)             # real images      x ~ p_data
    fake = G(z)                           # generator images G(z)

    # Critic wants real scores HIGH and fake scores LOW -> maximize the gap,
    # so we minimize the NEGATIVE gap.
    gap = critic(real).mean() - critic(fake).mean()

    # Gradient penalty: pull the input-gradient norm toward 1 (the 1-Lipschitz leash)
    x_hat = interpolate(real, fake)       # random points between real and fake
    grad  = input_gradient(critic, x_hat) # d critic(x_hat) / d x_hat
    gp    = ((grad.norm(dim=1) - 1.0) ** 2).mean()

    critic_loss = -gap + lambda_gp * gp   # lambda_gp ~ 10
    update(critic, critic_loss)

# Now update the generator: it wants its fakes to score HIGH
z = sample_noise(batch)
gen_loss = -critic(G(z)).mean()
update(G, gen_loss)
帶梯度懲罰的 WGAN:評論家比生成器更常被訓練,而懲罰項讓它維持 1-Lipschitz。

穩定訓練的工具箱

Wasserstein GAN馴服了最大的不穩定來源,但老練的實作者還會準備一小箱互補的技巧。單獨拿出來都不是魔法;合在一起,卻能把剩下的搖晃撫平。以下是真正派得上用場的幾招,每招都附上它管用的理由。

  1. 單邊標籤平滑(one-sided label smoothing)——別用「1.0=真」這種硬目標去訓練判別器,改用像 0.9 這樣放軟的目標。把它釘在完美的 1.0,會招來「根本原因」那節看到的過度自信(輸出飽和、梯度死掉);稍微軟一點的目標能讓判別器保持謙虛、梯度還活著。「單邊」指的是只放軟「真」的標籤,不動「假」的標籤(它仍維持 0)。
  2. 譜歸一化(spectral normalization)——另一種非常乾淨的方式來約束判別器。它用每一層權重矩陣的最大奇異值去重新縮放該矩陣,從而限制這一層能把輸入放大多少,進而控制整個網路的 Lipschitz 常數。它追求的目標跟梯度懲罰一樣——「別讓評論家變得太快」——只是靠結構直接做到,而不是另外加一項損失,而且不論搭配標準損失或 Wasserstein 損失都很合得來。
  3. 雙時間尺度更新規則(TTUR)——給生成器和判別器不同的學習率(常見是判別器快一點)。因為兩者是在彼此適應,讓其中一方稍微先穩定下來,可以避免這場追逐失控盤旋;而且它背後有理論支持,說這有助於兩者收斂。
  4. 平衡更新頻率——與此密切相關,你還要決定每位玩家「多常」更新一次。例如 WGAN 通常每更新生成器一次,就更新評論家好幾次(常見是五次),好讓評論家維持為一個夠格的裁判、給生成器值得信賴的訊號。評論家步數太少,訊號會很吵;太多,生成器又會跟不上。

現在來到更難的問題:你到底怎麼知道它在運作?我們已經確認,單一的對抗損失數字並不可信——它會震盪,而在普通 GAN 裡更是完全不反映品質。所以這個領域改用專門的指標來評斷生成器,這些指標問的,正是我們真正在乎的兩件事:生成的影像看起來真不真,「而且」夠不夠多樣(別讓模式崩潰躲在平均值底下)?

Fréchet Inception 距離(FID)是今日的預設。想法是:把你的真實影像和生成影像都丟進一個預訓練的影像網路(一個「Inception」分類器),各自讀出一個高層特徵向量——也就是對「畫面裡有什麼」的精簡數值摘要。現在你手上有兩團特徵向量雲,一團真、一團假。FID 量的是這兩團雲之間的距離,把每一團都當成一個鐘形團塊,同時比較它們的「中心」與「散布範圍」。越低越好:低 FID 表示假的特徵雲剛好疊在真的上面——生成影像既逼真、又跟真實資料一樣多樣。關鍵在於,如果生成器模式崩潰,它的特徵雲會縮小,散布範圍就對不上,FID 隨之升高——所以 FID 會懲罰那個被普通損失藏起來的失敗。

比較老的 Inception 分數(IS)用的是同一個預訓練網路,但問了一個更聰明、而且手邊不需要真實影像的問題。對每一張生成影像,它看分類器的標籤預測:一張逼真的影像應該得到一個有信心、很尖銳的預測(「明顯是狗」),而「所有」生成影像的「集合」則應該橫跨許多不同標籤(「有狗、有貓、有車……」)。高 IS 獎勵的正是這個組合——每張影像信心夠尖,加上整批之間夠多樣。它的盲點是從不去看你的真實資料,所以可能被騙;這也是為什麼直接拿真實影像來比的 FID 大致成了黃金標準。實務上你會兩個都報,而且仍然會親眼檢視樣本。

條件式 GAN:告訴生成器要做什麼

到目前為止,生成器像一台吃角子老虎:餵它隨機雜訊,它就回你「一張」逼真影像——但你沒辦法指定要哪一張。條件式 GAN(cGAN)的解法,是給兩個網路都多餵一個輸入:一個條件 y,例如像「貓」這樣的類別標籤。現在生成器把「雜訊」加上一個「請求」變成影像,而你可以操控它——把 y 設成「貓」就得到貓,設成「狗」就得到狗,全都出自同一個訓練好的模型。

x_{\text{fake}} = G(z,\,y) \qquad\text{and}\qquad D(x,\,y)

兩位玩家都收到條件 y:生成器依它來生成,判別器依它來評斷。

逐項來讀:x_fake = G(z, y) 說生成器現在吃「兩個」輸入——隨機雜訊 z(提供多樣性,所以你還是會得到許多不同的貓)和條件 y(固定要做「哪一種」影像)——然後產生假影像 x_fake。而 D(x, y) 說判別器也看得到條件:它是把影像 x「連同」它理應匹配的標籤 y 一起評斷。在一個數字生成器裡,y 可能是「7」的 one-hot 編碼;z 則決定你拿到的是斜體的 7 還是有橫槓的 7。

為什麼「兩位」玩家都必須拿到 y?假設只有生成器看得到標籤、判別器看不到。那麼判別器唯一的問題就是「這看起來像不像一個真的數字?」——它從不檢查這個數字有沒有符合請求。於是當你要「7」時,生成器大可開開心心生出一個漂亮的「3」而不受任何懲罰,因為唯一的裁判根本不在乎標籤。條件就被悄悄無視了。把 y 也餵給判別器,等於把它的問題改成「這是一張真影像,『而且』它符合 y 嗎?」一個掛著「7」標籤卻是「3」的影像,現在會被標記成假——一對對不上的組合——這就逼著生成器真的去服從條件。

對抗賽局加上了條件:雜訊 z 與標籤 y 進入生成器;判別器則對(影像、標籤)這一對,同時評斷它的逼真度與一致性。

條件式 GAN 示意圖:生成器接收雜訊與標籤後產生影像,判別器再連同同一個標籤一起評估這張影像。

用經典示範把它變具體:一個在手寫數字上訓練的 cGAN,給你一個 0 到 9 的旋鈕。把它設成「4」再重新取樣雜訊,你會得到無窮無盡、各不相同又逼真的 4;設成「8」就得到 8——控制與多樣性兼得。而且這個條件不一定要是類別標籤。如果 y 本身是一張「影像」——一張素描、一張分割圖、一張白天的照片——條件式 GAN 就會學會把一張圖翻譯成另一張圖。這個單一的推廣,正是影像對影像翻譯模型 pix2pix 與 CycleGAN 的種子,而本系列最後一份指南,就會在你現在已經學會的所有「維持訓練穩定」的本事之上,把它們建起來。