這篇文章将會教你怎樣用機器學習來僞造假資料,題材還是人臉,以下六張人臉裡面,有兩張是假的,猜猜是哪兩張😎?

生成假人臉使用的網絡是對抗生成網絡 (GAN - Generative adversarial network),這個網絡與之前介紹的比起來相當特殊,雖然看起來不算複雜,但訓練起來極其困難,以下将從基礎原理開始一直講到具體代碼,還會引入一些之前沒有講過的元件和訓練方法😨。
所謂生成網絡就是用于生成文章,音頻,圖檔,甚至代碼等資料的機器學習模型,例如我們可以給出一個需求讓網絡生成一份代碼,如果網絡足夠強大,生成的代碼品質足夠好并且能滿足需求,那碼農們就要面臨失業了😱。當然,目前機器學習模型可以生成的資料比較有限并且品質都很一般,碼農們的飯碗還是能保住一段時間的。
生成網絡和普通的模型一樣,要求有輸入和輸出,假設我們可以傳入一些條件讓網絡生成符合條件的圖檔:
看起來非常好用,但訓練這樣的模型需要一個龐大的資料集,并且得一張張圖檔去标記它們的屬性,實作起來會累死人。這篇文章介紹的對抗生成網絡屬于無監督學習,可以完全不需要給資料打标簽,你隻需要給模型認識一些真實資料,就可以讓模型輸出類似真實資料的假資料。對抗生成網絡分為兩部分,第一部分是生成器 (Generator),第二部分是識别器 (Discriminator),生成器負責根據随機條件生成資料,識别器負責識别資料是否為真。
訓練對抗生成網絡有兩大目标,這兩大目标是沖突的,這就是為什麼我們叫對抗生成網絡:
生成器需要生成騙過識别器 (輸出為真) 的資料
識别器需要不被生成器騙過去 (針對生成器生成的資料輸出為假,針對真實資料輸出為真)
對抗生成網絡的訓練流程大緻如下,需要循環訓練生成器和識别器:
簡單通俗一點我們可以用造假皮包為例來了解,好了解了吧🤗:
和現實造假皮包一樣,生成器會生成越來越接近真實資料的假資料,最後會生成和真實資料一模一樣的資料,但這樣反而就遠離我們建構生成網絡的目的了(不如直接用真實資料)。使用生成網絡通常是為了達到以下的目的:
要求大量看上去是真的,但稍微不一樣的資料
要求沒有版權保護的資料 (假資料沒來的版權🤒)
生成想要但是現實沒有的資料 (需要更進一步的工作)
看以上的流程你可能會發現,因為對抗生成網絡是無監督學習,不需要标簽,我們隻能給模型傳入随機的條件來讓它生成資料,模型生成出來的資料看起來可能像真的但不一定是我們想要的。如果我們想要指定具體的條件,則需要在訓練完成以後分析随機條件對生成結果的影響,例如随機生成的第二個數字代表性别,第六個數字代表年齡,第八個數字代表頭發的數量,這樣我們就可以調整這些條件來讓模型生成想要的圖檔。
還記得上一篇人臉識别的模型不?人臉識别的模型會把圖檔轉換為某個長度的向量,訓練完成以後這個向量的值會代表人物的屬性,而這一篇是反過來,把某個長度的向量轉換回圖檔,訓練成功以後這個向量同樣會代表人物的各個屬性。當然,兩種的向量表現形式是不同的,把人臉識别輸出的向量交給對抗生成網絡,生成的圖檔和原有的圖檔可能會相差很遠,把人臉識别輸出的向量還原回去的方法後面再研究吧🤕。
在第八篇介紹 CNN 的文章中,我們了解過卷積層運算 (Conv2d) 的實作原理,CNN 模型會利用卷積層來把圖檔的長寬逐漸縮小,通道數逐漸擴大,最後扁平化輸出一個代表圖檔特征的向量:
而在對抗生成網絡的生成器中,我們需要實作反向的操作,即把向量當作一個 (向量長度, 1, 1) 的圖檔,然後把長寬逐漸擴大,通道數 (最開始是向量長度) 逐漸縮小,最後變為 (3, 圖檔長度, 圖檔寬度) 的圖檔 (3 代表 RGB)。
實作反向操作需要反卷積層 (ConvTranspose2d),反卷積層簡單的來說就是在參數數量相同的情況下,把輸出大小的資料還原為輸入大小的資料:
要了解反卷積層的具體運算方式,我們可以把卷積層拆解為簡單的矩陣乘法:
可以看到卷積層計算的時候可以根據核心參數和輸入大小生成一個矩陣,然後計算輸入與這個矩陣的乘積來得到輸出結果。
而反卷積層則會計算輸入與轉置 (Transpose) 後的矩陣的乘積得到輸出結果:
可以看到卷積層與反卷積層的差別隻在于是否轉置計算使用的矩陣。此外,通道數量轉換的計算方式也是一樣的。
測試反卷積層的代碼如下:
需要注意的是,不一定存在一個反卷積層可以把卷積層的輸出還原到輸入,這是因為卷積層的計算是不可逆的,即使存在一個可以把輸出還原到輸入的矩陣,這個矩陣也不一定有一個等效的反卷積層的核心參數。
接下來我們看一下生成器的定義,原始介紹 GAN 的論文給出了生成 64x64 圖檔的網絡,而這裡給出的是生成 80x80 圖檔的網絡,其實差別隻在于一開始的輸出通道數量 (論文是 4, 這裡是 5)
表現如下:
其中批次正規化 (BatchNorm) 用于控制參數值範圍,防止層數過多 (後面會結合識别器訓練) 導緻梯度爆炸問題。
還有一個要點是生成器輸出的範圍會在 -1 ~ 1,也就是使用 -1 ~ 1 代表 0 ~ 255 的顔色值,這跟我們之前處理圖檔的時候把值除以 255 使得範圍在 0 ~ 1 不一樣。使用 -1 ~ 1 可以提升輸出顔色的精度 (減少浮點數的精度損失)。
我們再看以下識别器的定義,基本上就是前面生成器的相反流程:
看到這裡你可能會有幾個疑問:
為什麼用 LeakyReLU: 這是為了防止層數疊加次數過多導緻的梯度消失問題,參考第三篇,LeakyReLU 對于負數輸入不會傳回 0,而是傳回 <code>輸入 * slope</code>,這裡的 <code>slope</code> 指定為 0.2
為什麼第一層不加批次正規化 (BatchNorm): 原有論文中提到實際測試中,如果在所有層添加批次正規化會讓模型訓練結果不穩定,生成器的最後一層和識别器的第一層拿掉以後效果會好一些
為什麼不加池化層: 添加池化層以後可逆性将會降低,例如識别器針對假資料傳回接近 0 的數值時,判斷哪些部分導緻這個輸出的依據會減少
接下來就是訓練生成器和識别器,生成器和識别器需要分别訓練,訓練識别器的時候不能動生成器的參數,訓練生成器的時候不能動識别器的參數,使用的代碼大緻如下:
上述例子應該可以幫助你了解大緻的訓練流程和隻訓練識别器或生成器的方法,但是直接這麼做效果會很差🤕,接下來我們會看看對抗生成網絡的問題,并且給出優化方案,後面的完整代碼會跟上述例子有一些不同。
如果對原始論文有興趣可以參考這裡,原始的對抗生成網絡又稱 DCGAN (Deep Convolutional GAN)。
看完以上的内容你可能會覺得,嘿嘿,還是挺簡單的。不🤕,雖然原理看上去挺好了解,模型本身也不複雜,但對抗生成網絡是目前介紹過的模型裡面訓練難度最高的,這是因為對抗生成網絡建立在沖突上,沒有一個明确的目标 (之前的模型目标都是針對未學習過的資料預測正确率盡可能接近 100%)。如果生成器生成 100% 可以騙過識别器的資料,那可能代表識别器根本沒正常工作,或者生成器生成的資料跟真實資料 100% 相同,沒實用價值;而如果識别器 100% 可以識别生成器生成的資料,那代表生成器生成的資料太垃圾,一個都騙不過。本篇介紹的例子使用了最蠢最簡單的方法,把每一輪學習後生成器生成的資料輸出到硬碟,然後人工鑒定生成的效果怎樣🤒,同時還會每 100 輪訓練記錄一次模型狀态,供訓練完以後復原使用 (最後一個模型狀态效果不會是最好的,後面會說明)。
另一個問題是識别器和生成器不能同時訓練,怎樣安排訓練過程對訓練結果的影響非常大😮,理想的過程是:識别器稍微領先生成器,生成器跟着識别器慢慢的生成越來越精準的資料。舉例來說,識别器首先會識别膚色占比較多的圖檔為人臉,接下來生成器會生成全部都是膚色的圖檔,然後識别器會識别有兩個看上去是眼睛的圖檔為人臉,接下來生成器會加上兩個看上去是眼睛的形狀到圖檔,之後識别器會識别帶有五官的圖檔為人臉,接下來生成器會加上剩餘的五官到圖檔,最後識别器會識别五官和臉形狀比較正常的人為人臉,生成器會盡量調整五官和人臉形狀接近正常水準。而不理想的過程是識别器大幅領先生成器,例如識别器很早就達到了接近 100% 的正确率,而生成器因為找不到學習的方向正确率會一直原地踏步;另一個不理想的過程是生成器領先識别器,這時會出現識别器找不到學習的方向,生成器也找不到學習的方向而原地轉的情況。實作識别器稍微領先生成器,可以增加識别器的訓練次數,常見的方法是每訓練 n 次識别器就訓練 1 次生成器,而本文後面會介紹根據正确率動态調整識别器和生成器學習次數的方法,參考後面的代碼吧。
對抗生成網絡最大的問題是模式崩潰 (Mode Collapse) 問題,這個問題所有訓練對抗生成網絡的人都會面對,并且目前沒有 100% 的方法避免😭。簡單的來說就是生成器學會偷懶作弊,隻會輸出一到幾個與真實資料幾乎一模一樣的虛假資料,因為生成的資料同質化非常嚴重,即使可以騙過識别器也沒什麼實用價值。發生模式崩潰以後的輸出例子如下,可以看到很多人臉都非常接近:
為了盡量避免模式崩潰問題,以下幾個改進的模型被發明了出來,這就是人民群衆的智慧啊😡。
模式崩潰問題的原因之一就是部分模型參數會随着訓練固化 (達到本地最優),因為原始的對抗生成網絡會讓識别器輸出盡可能接近 1 或者 0 的值,如果值已經是 0 或者 1 那麼參數就不會被調整。WGAN (Wasserstein GAN) 的解決方式是不限制識别器輸出的值範圍,隻要求識别器針對真實資料輸出的值大于虛假資料輸出的值,和要求生成器生成可以讓識别器輸出更大的值的資料。
第一個修改是拿掉識别器最後的 Sigmoid,這樣識别器輸出的值就不會限制在 0 ~ 1 的範圍内。
第二個修改是修改計算損失的方式:
這麼修改以後會出現一個問題,識别器輸出的值範圍會随着訓練越來越大 (生成器提高虛假資料的輸出值,接下來識别器提高真實資料的輸出值,循環下去輸出值就會越來越大😱),進而導緻梯度爆炸問題。為了解決這個問題 WGAN 對識别器參數的可取範圍做出了限制,也就是在調整完參數以後裁剪參數,第三個修改如下:
如果有興趣可以參考 WGAN 的原始論文,裡面一大堆數學公式可以把人吓壞😱,但主要的部分隻有上面提到的三點。
WGAN 為了防止梯度爆炸問題對識别器參數的可取範圍做出了限制,但這個做法比較粗暴,WGAN-GP (Wasserstein GAN Gradient Penalty) 提出了一個更優雅的方法,即限制導函數值的範圍,如果導函數值偏移某個指定的值則通過損失給與模型懲罰。
具體實作如下,看起來比較複雜但做的事情隻是計算識别器輸入資料的導函數值,然後判斷所有通道合計的導函數值的 L2 合計與常量 1 相差多少,相差越大就傳回越高的損失,這樣識别器模型參數自然會控制在某個水準。
然後再修改計算識别器損失的方法:
最後把識别器中的批次正規化 (BatchNorm) 删掉或者改為執行個體正規化 (InstanceNorm) 就完了。InstanceNorm 和 BatchNorm 的差別在于計算平均值和标準差的時候不會根據整個批次計算,而是隻根據各個樣本自身計算,關于 BatchNorm 的計算方式可以參考第四篇。
如果有興趣可以參考 WGAN-GP 的原始論文。
又到完整代碼的時間了🤗,這份代碼同時包含了原始的 GAN 模型 (DCGAN),WGAN 和 WGAN-GP 的實作,後面還會比較它們之間的效果相差多少。
使用的資料集連結如下,前一篇的人臉識别文章也用到了這個資料集:
https://www.kaggle.com/atulanandjha/lfwpeople
需要注意的是人臉圖檔數量越多就越容易出現模式崩潰問題,這也是對抗生成網絡訓練的難點之一🤒,這份代碼隻會随機選取 2000 張圖檔用于訓練。
這份代碼還會根據正确率動态調整生成器和識别器的訓練比例,如果識别器比生成器更強則訓練 1 次生成器,如果生成器比識别器更強則訓練 5 次識别器,這麼做可以省去手動調整訓練比例的麻煩,經實驗效果也不錯🥳。
儲存代碼到 <code>gan.py</code>,然後執行以下指令即可開始訓練:
同樣訓練 2000 輪以後,DCGAN, WGAN, WGAN-GP 輸出的樣本如下:
DCGAN
WGAN
WGAN-GP
可以看到 WGAN-GP 受模式崩潰問題影響最少,并且效果也更好😤。
WGAN-GP 訓練到 3000 次以後輸出的樣本如下:
WGAN-GP 訓練到 10000 次以後輸出的樣本如下:
随着訓練次數增多,WGAN-GP 一樣無法避免模式崩潰問題,這就是為什麼以上代碼會記錄每一輪訓練後輸出的樣本,并在每 100 輪訓練以後儲存單獨的模型狀态,這樣訓練結束以後我們可以通過評價輸出的樣本找到效果最好的批次,然後使用該批次的模型狀态。
上述的例子效果最好的狀态是訓練 3000 次以後的狀态。
你可能發現輸出的樣本中夾雜了一些畸形🥴,這是因為生成器沒有覆寫到輸入的向量空間,最主要的原因是随機輸入中包含了很多接近 0 的值,避免這個問題簡單的做法是生成随機輸入時限制值必須小于或大于某個值。原則上給反卷積層設定 Bias 也可以避免這個問題,但會更容易陷入模式崩潰問題。
使用訓練好的模型生成人臉就比較簡單了:
額外的,我做了一個可以動态調整參數捏臉的網頁,html 代碼如下:
儲存到 <code>gan_eval.html</code> 以後執行以下指令即可啟動伺服器:
浏覽器打開 <code>http://localhost:8666</code> 以後會顯示以下界面,點選随機生成按鈕可以随機生成人臉,拉動左邊的參數條可以動态調整參數:
一些捏臉的網站會分析各個參數的含義,看看哪些參數代表膚色,那些參數代表表情,哪些參數代表脫發程度,我比較懶就隻給出各個參數的序号了🤒。
又摸完一個新的模型了,跟到這篇的人也越來越少了,估計這個系列再寫一兩篇就會結束 (VAE, 強化學習)。
前一篇論文我提到了可能會開一個新的系列介紹 .NET 的機器學習,但我決定不開了。經過試驗發現沒有達到可用的水準,文檔基本等于沒有,社群氣氛也不行 (大會 PPT 倒是做的挺好的)。畢竟語言隻是個工具,不是老祖宗,還是看開一點吧。學 python 再做機器學習會輕松很多,就像長遠來說學一點基礎英語再程式設計比完全隻用中文程式設計 (先把基礎架構類庫系統接口的英文全部翻譯成中文,再用中文寫) 簡單很多,對叭😎。