看待資料的兩種方式:判別式與生成式
先停下來,注意一下你目前讀完的每一個電腦視覺章節有個共通點。影像分類問的是「這是貓還是狗?」;物件偵測問的是「這條街上的車在哪裡?」;語意分割問的是「究竟哪些像素屬於道路?」。形式各不相同,底層的問題卻是同一個:給定這張影像,裡面有什麼? 你總是把一張圖交給機器,要它讀出一個答案。這類任務稱為 判別式(discriminative)——它們在「分辨、區別」這個中性的舊義上,把影像可能包含的各種可能性彼此分開。
現在把這個問題整個翻轉過來。不要問「這張影像裡有什麼?」,而要問「一張看起來合理的影像,究竟長什麼樣子?」這是一個 生成式(generative) 的問題,也是整個章節的核心。請隨身帶著這個比喻:判別式模型是一位藝術評論家。給評論家看一幅畫,他會告訴你這是莫內的作品,或是透視畫錯了——他在「評判」。而 生成式模型 是藝術家本人。藝術家可以坐在一張空白畫布前,創作出一幅從未存在過的全新畫作。「評判」與「創作」是兩種根本不同的能力,而我們現在正要求機器做出這個跨越。
一張對比圖:辨識流程把影像映射成標籤,而生成流程把一段代碼映射成一張新影像。
分類器學的東西,對比生成器學的東西。
讓我們慢慢讀這兩個式子,因為它們之間的鴻溝正是整個章節存在的理由。這裡 x 是一張影像(把它想成整片像素網格),y 是一個標籤(例如「貓」),而 p(\cdot) 表示「……的機率」。左邊的 p(y \mid x) 讀作「在給定影像 x 的條件下,標籤 y 的機率」——那條豎線表示「在已經有了……的前提下」。這正是你建過的每一個分類器在估計的東西:把像素交給它,它告訴你每個標籤有多可能。右邊的 p(x) 讀作「影像 x 根本存在的機率」——也就是這種特定的像素排列,有多可能是一張真實、自然的影像,而不是隨機雜訊。為 p(x) 建模,意味著要捕捉「究竟是什麼讓一張影像看起來真實」:天空偏藍且在上方、臉孔有兩隻眼睛在鼻子上方、邊緣通常清晰銳利。分類器永遠不需要知道這些;它只需要一條把貓和狗分開的界線。但生成器需要掌握整個「真實」的形狀。
所有影像的空間與它隱藏的旋鈕
為了對 p(x) 建立直覺,請想像「所有可能影像」所構成的空間。一張 256×256 的彩色影像有 256 × 256 個像素,每個像素帶有紅、綠、藍三個數值——加起來是 196,608 個數字。你可以把一張影像想成一個「點」,它的地址由 196,608 個座標組成。「所有影像的空間」就是那個大到無法想像的房間,裡面裝著每一個這樣的可能點:拍過的每一張照片,但也包括你能產生的每一片隨機彩色雜訊。這個房間,就是 p(x) 棲身的舞台。
關鍵的事實在這裡:那個房間裡幾乎每一個點都毫無意義。如果你隨機設定那 196,608 個數字,得到的會是電視雪花——永遠不會是一張臉、一片海灘,或任何東西。真實、自然的影像在這個空間中稀少到趨近於零,而且它們並非均勻散布,而是聚集在一張漂浮於巨大房間內、薄薄的、彎曲的「面」上。數學家把這樣的面稱為 流形(manifold)——一個低維的曲面,蜿蜒穿行於高維空間之中,就像一張揉皺的紙本質上是個 2D 的平面物,卻活在 3D 空間裡一樣。整個生成的遊戲,就是學會「待在這張面上」:產生落在真實影像流形上的點,而不要掉進它四周那片雜訊的汪洋裡。
一個標註了 RGB 數值的像素網格,說明一張影像就是一個極高維空間中的一個點。
但這裡有個充滿希望的轉折。儘管這個房間有 196,608 個維度,真實影像的流形卻遠比這薄——你並不需要那麼多個彼此獨立的旋鈕,就能描述一張真實的圖。想想電玩裡的角色創建器。螢幕上的角色由成千上萬個像素組成,但身為玩家的你,實際碰到的只有寥寥幾個滑桿:臉的寬度、髮色、眼睛大小、笑容多燦爛。拉動這幾個滑桿,成千上萬個像素就會以協調、合理的方式一起更新;你永遠不可能不小心產生出雪花,因為這些滑桿只會沿著「有效臉孔」的流形移動。這幾個隱藏的控制器,正是我們所說的 潛在變數(latent variable)——一小組在幕後支配你所見一切的底層因素。
現在你應該能感覺到整個章節的計畫正在成形。如果我們能找出那些隱藏的旋鈕——如果一個 生成式模型 能學會那一小組沿著流形移動的潛在變數——那麼「生成一張影像」就會坍縮成幾乎不費吹灰之力的事:把旋鈕設成某些值,搖一下曲柄,一張嶄新而真實的影像就出來了。 我們接下來研究的每一種方法(自編碼器、VAE、GAN),歸根結底都是「如何找到這些旋鈕、並讓搖曲柄這件事可靠」的不同策略。請牢牢記住這個畫面;它是後續一切的脊梁。
自編碼器:壓縮再重建
那麼,當沒有人事先知道這些隱藏旋鈕該是什麼的時候,機器要怎麼自己把它們找出來?第一個具體的答案,是一台優雅而簡單的機器,叫做 自編碼器(autoencoder)。它的把戲是:刻意逼網路通過一個瓶頸,然後挑戰它把失去的東西復原。如果網路能在被擠過一個極小的開口之後重建出影像,那麼能熬過這次擠壓而留存下來的,必定就是這張影像真正本質的描述——也就是那些旋鈕本身。
自編碼器由三個依序排列的部分組成。第一是 編碼器(encoder):它接收完整的影像,逐步把它壓縮成一小串數字。第二是 瓶頸(bottleneck):這串短短的數字,稱為「代碼」或「潛在向量」,是整條路最窄的地方——也許只有 32 或 128 個數字,要代表將近 20 萬個像素。這些數字就是網路自己私有的 潛在變數。第三是 解碼器(decoder):它接過這小小一段代碼,把它再展開回去,試圖重建出原本的影像。整台機器以「讓輸出符合輸入」這唯一目標,從頭到尾一起訓練。關鍵在於:沒有人告訴編碼器這些旋鈕該代表什麼意思。 我們絕不會把某個數字標成「髮色」。瓶頸就是窄到無法把整張圖背下來,所以唯一能取勝的辦法,就是留下真正重要的東西、把其餘的都丟掉——而「真正重要的東西」,恰恰就是我們一直在獵捕的那組隱藏因素。
你其實已經擁有建造這台機器所需的全部零件了。回想 CNN 章節:帶有步幅(stride)或池化(pooling)的卷積層會 降採樣(downsample)——它們在萃取內容的同時,縮小特徵圖的空間尺寸。那正是編碼器:一個把影像降採樣成代碼的 CNN。而解碼器只是同樣的想法反過來跑——那些 升採樣(upsample) 的層,把一小段代碼再長回成全解析度的影像(也就是「轉置卷積」或「上卷積」)。用一個比喻來讓它變得具體:在電話裡向別人描述一位朋友的臉,會逼你把它壓縮成寥寥幾個字(「圓臉、黑色捲髮、笑容很大」)——那就是編碼器把影像擠進代碼。電話另一頭的人是一位素描畫家,聽到這些字,把那張臉畫回來——那就是解碼器從代碼重建出影像。
一張沙漏形狀的圖:寬的影像經編碼器收窄到中央一小段潛在代碼,再經解碼器拓寬回一張重建的影像。
# An autoencoder as three function calls, trained to copy its own input. # encoder and decoder are both small CNNs (downsample / upsample). z = encoder(x) # x: the input image -> z: the tiny latent code (the 'knobs') x_hat = decoder(z) # z -> x_hat: the network's reconstruction of x loss = mean((x - x_hat) ** 2) # per-pixel squared difference, averaged # Training nudges encoder+decoder so x_hat looks more and more like x. # The bottleneck (z is tiny) is what forces it to learn a useful, compact code.
重建損失:重建出的影像離原圖有多遠。
讓我們逐個符號拆解這個式子,因為它正是訓練整台機器的引擎。x 是輸入影像。\text{encode}(x) 把它送過編碼器,產生潛在代碼,我們稱之為 z。接著 \text{decode}(z) = \hat{x}——讀作「x-hat」——是解碼器的重建,也就是它從代碼把 x 重建出來的最佳嘗試。式子 \lVert x - \hat{x}\rVert^2 的意思是:把重建影像和原圖逐像素相減,把每個差值平方(這樣不論誤差是正是負都算作誤差),再把所有像素的結果全部加總。\mathcal{L} 就是算出來的 損失(loss)——一個衡量「總共錯了多少」的單一數字。用白話說:網路因為讓重建影像吻合原圖而受到獎勵,因為每一個畫錯的像素而受到懲罰。舉個小小的數值例子:如果某個像素的亮度應該是 0.8,但解碼器輸出 0.5,那這個像素對損失的貢獻就是 (0.8 - 0.5)^2 = 0.09;若完全畫對,貢獻就是 0。而且請注意——這其實毫無新意。它正是你在迴歸裡早已見過的 均方誤差(mean-squared error),只是逐像素套用、再對整張影像加總而已。訓練所做的,不過是把 \mathcal{L} 往下推,而那等同於說「讓重建變得更好」。
為什麼普通自編碼器不會「做夢」
驅動本章節其餘部分的那份失望,現在登場了。一個訓練好的 自編碼器 在重建上確實令人印象深刻:餵它一張真實的臉,它壓成代碼再重建出一張認得出來的臉。於是你冒出一個令人興奮的點子——何不乾脆跳過編碼器,憑空捏造一段隨機代碼,直接交給解碼器,讓它夢出一張全新的臉?你試了。結果出來的卻是一團垃圾:糊成一片的顏色、破碎的形狀,完全不像一張臉。
為什麼會失敗?因為普通自編碼器自始至終只因一件事受到獎勵:重建它被訓練過的那些特定影像。重建損失裡,沒有任何一項告訴編碼器該把每段代碼擺在哪裡。於是它就隨便擺,哪裡方便就擺哪裡——這張臉的代碼落在這邊,那張臉的代碼落到老遠那邊,而它們之間那片廣大的空間被留成一片空白。結果就是一個布滿 空洞與縫隙 的潛在空間:那些區域不對應任何訓練影像,解碼器也從來不曾被要求去處理它們。你隨機挑的一段代碼,幾乎必定落進其中一個空洞,而解碼器面對一個它從未見過的輸入,就只能吐出一堆胡言亂語。系統裡根本沒有任何規則去規定「有效的代碼」長什麼樣子——這些 潛在變數 純粹是「碰巧」被組織起來的。
想像一座圖書館,書本被擺在完全隨機的位置,沒有任何編目系統。如果你早已知道想要的那本書確切在哪一排哪一格,你可以直接走過去把它抽出來——那就是重建,編碼器把精準的代碼交到你手上。但若你想隨意瀏覽——隨機挑一排書架,伸手去拿擺在那裡的東西——大多數時候你抓到的會是一片空氣,或一堆毫無意義的雜物。普通自編碼器正是這樣一座圖書館:當你握有索書號時,檢索一流;但要走進去隨意閒逛、發現新東西,卻毫無用處。而生成,本質上正是一種「瀏覽」的行為——我們想伸手探進這個空間,抓住任意一個點,並在那裡找到有意義的東西。
通往生成的兩條路:VAE 與 GAN
現在我們有了一個明確的目標和一道清楚的阻礙。目標:學會影像流形的隱藏旋鈕,好讓轉動它們就能產生真實的圖。阻礙:普通自編碼器把潛在空間留得滿是空洞,所以隨機代碼只會解碼成垃圾。本章節接下來會沿著兩大家族走下去,它們以相反的精神來解決這道難題。現在先給它們各一句話的預告,讓你在細節登場前,腦中先有一張地圖。
第一條路是 變分自編碼器(variational autoencoder),也就是 VAE——下一篇指南的主角。它的招式是:保留你剛學到的「編碼器—解碼器」結構,但加上一條機率的規則,逼迫潛在空間變得整齊、不留縫隙:它不再把每張影像對應到一個僵硬的點,而是對應到一小團模糊的雲,並溫和地把所有這些雲都往一個共享、性質良好的區域擠。一旦這個空間被緊密地、毫無空洞地填滿,你就能隨機挑一個點、把它解碼,並可靠地得到一張看起來合理的影像。VAE 以一種有原理、有數學根據的方式,讓「搖曲柄」這件事真正運作起來。
第二條路是 生成對抗網路(generative adversarial network),也就是 GAN——第 3 與第 4 篇指南的主題。它把編碼器和重建損失整個丟掉,改而安排兩個網路之間的一場較量。其中一個是生成器,它拿一段隨機代碼,試圖畫出一張令人信服的假影像。另一個是判別器——還記得最開頭那節的藝術評論家嗎?——它試圖把真影像和生成器的假貨分辨開來。它們彼此對抗著訓練:每當評論家變得更敏銳,偽造者就被逼著變得更厲害,一回合又一回合,假貨步步逼近以假亂真的逼真度。沒有任何人寫下「是什麼讓影像真實」;那份知識,是從這場對決本身湧現出來的。
事先了解它們的「性格」很有價值,因為它們之間是個如假包換的權衡。VAE 有原理但偏模糊:它在機率上的整齊,帶來平滑、組織良好的潛在空間和穩定的訓練,但那份維持空間整齊的「平均化」往往會把細節抹柔,所以樣本看起來可能有點褪色、不夠銳利。GAN 銳利但脾氣難搞:對抗賽局能產生出驚人清晰、足以亂真的影像,但它的訓練以善變著稱——可能來回震盪、崩潰、或就是不肯收斂,這也正是為什麼有一整篇指南(〈訓練真正能用的 GAN〉)專門在馴服它。甚至還有第三條路,我們在此只先點個名:擴散(diffusion) 模型,它從純雜訊出發、一步一步把雜訊去掉來生成;如今許多頂尖的影像生成器都靠它驅動,而它會有自己專屬的後續章節。在本章節裡,VAE 與 GAN 就是我們要攀登的兩座高峰。
一張變分自編碼器的示意圖:把影像映射成潛在空間中的一團機率雲,再把採樣解碼回影像。
一張生成對抗網路的示意圖:生成器產生假影像,判別器判斷真偽。