1. 程式人生 > >通過PyTorch實現對抗自編碼器

通過PyTorch實現對抗自編碼器

「大多數人類和動物學習是無監督學習。如果智慧是一塊蛋糕,無監督學習是蛋糕的坯子,有監督學習是蛋糕上的糖衣,而強化學習則是蛋糕上的櫻桃。我們知道如何做糖衣和櫻桃,但我們不知道如何做蛋糕。」

Facebook 人工智慧研究部門負責人 Yann LeCun 教授在講話中多次提及這一類比。對於無監督學習,他引用了「機器對環境進行建模、預測可能的未來、並通過觀察和行動來了解世界如何運作的能力」。

深度生成模型(deep generative model)是嘗試解決機器學習中無監督學習問題的技術之一。在此框架下,需要一個機器學習系統來發現未標記資料中的隱藏結構。深度生成模型在許多應用中有許多廣泛的應用,如密度估計、影象/音訊去噪、壓縮、場景理解(scene understanding)、表徵學習(representation learning)和半監督分類(semi-supervised classification)。

變分自編碼器(Variational Autoencoder/VAE)使得我們可以在概率圖形模型(probabilistic graphical model)的框架下將這個問題形式化,在此框架下我們可以最大化資料的對數似然值的下界。在本文中,我們將介紹一種最新開發的架構,即對抗自編碼器(Adversarial Autoencoder),它由 VAE 啟發,但它在資料到潛在維度的對映方式中(如果現在還不清楚,不要擔心,我們將在本文中重新提到這個想法)有更大的靈活性。關於對抗自編碼器最有趣的想法之一是如何通過使用對抗學習(adversarial learning)將先驗分佈(prior distribution)運用到神經網路的輸出中。

如果想將深入瞭解 Pytorch 程式碼,請訪問 GitHub repo(https://github.com/fducau/AAE_pytorch)。在本系列中,我們將首先介紹降噪自編碼器和變分自編碼器的一些背景,然後轉到對抗自編碼器,之後是 Pytorch 實現和訓練過程以及 MNIST 資料集使用過程中一些關於消糾纏(disentanglement)和半監督學習的實驗。

背景

降噪自編碼器(DAE)

我們可在自編碼器(autoencoder)的最簡版本之中訓練一個網路以重建其輸入。換句話說,我們希望網路以某種方式學習恆等函式(identity function)f(x)= x。為了簡化這個問題,我們將此條件通過一箇中間層(潛在空間)施加於網路,這個中間層的維度遠低於輸入的維度。有了這個瓶頸條件,網路必須壓縮輸入資訊。因此,網路分為兩部分:「編碼器」用於接收輸入並建立一個「潛在」或「隱藏」的表徵(representation);「解碼器」使用這個中間表徵,並重建輸入。自編碼器的損失函式稱為「重建損失函式(reconstruction loss)」,它可以簡單地定義為輸入和生成樣本之間的平方誤差:

640.png

當輸入標準化為在 [0,1] N 範圍內時,另一種廣泛使用的重建損失函式是交叉熵(cross-entropy loss)。

變分自編碼器(VAE)

變分自編碼器對如何構造隱藏表徵施加了第二個約束。現在,潛在程式碼的先驗分佈由設計好的某概率函式 p(x)定義。換句話說,編碼器不能自由地使用整個潛在空間,而是必須限制產生的隱藏程式碼,使其可能服從先驗分佈 p(x)。例如,如果潛在程式碼上的先驗分佈是具有平均值 0 和標準差 1 的高斯分佈,則生成值為 1000 的潛在程式碼應該是不可能的。

這可以被看作是可以儲存在潛在程式碼中的資訊量的第二類正則化。這樣做的好處是現在我們可以作為一個生成模型使用該系統。為了建立一個服從資料分佈 p(x)的新樣本,我們只需要從 p(z)進行取樣,並通過解碼器來執行該樣本以重建一個新影象。如果不施加這種條件,則潛在程式碼在潛在空間中的分佈是隨意的,因此不可能取樣出有效的潛在程式碼來直接產生輸出。

為了強制執行此屬性,將第二項以先驗分佈與編碼器建立分佈之間的 KL 散度(Kullback-Liebler divergence)的形式新增到損失函式中。由於 VAE 基於概率解釋,所使用的重建損失函式是前面提到的交叉熵損失函式。把它們放在一起我們有:

640-2.png


640-3.png

其中 q(z|x) 是我們網路的編碼器,p(z) 是施加在潛在程式碼上的先驗分佈。現在這個架構可以使用反向傳播(backpropagation)聯合訓練。

對抗自編碼器(AAE)

作為生成模型的對抗自編碼器

變分自編碼器的主要缺點之一是,除了少數分佈之外,KL 散度項的積分不具有封閉形式的分析解法。此外,對於潛在程式碼 z 使用離散分佈並不直接。這是因為通過離散變數的反向傳播通常是不可能的,使得模型難以有效地訓練。這篇論文介紹了在 VAE 環境中執行此操作的一種方法(https://arxiv.org/abs/1609.02200)。

對抗自編碼器通過使用對抗學習(adversarial learning)避免了使用 KL 散度。在該架構中,訓練一個新網路來有區分地預測樣本是來自自編碼器的隱藏程式碼還是來自使用者確定的先驗分佈 p(z)。編碼器的損失函式現在由重建損失函式與判別器網路(discriminator network)的損失函式組成。

圖中顯示了當我們在潛在程式碼中使用高斯先驗(儘管該方法是通用的並且可以使用任何分佈)時 AAE 的工作原理。最上面一行相當於 VAE。首先,根據生成網路 q(z|x) 抽取樣本 z,然後將該樣本傳送到根據 z 產生 x' 的解碼器。在 x 和 x' 之間計算重建損失函式,並且相應地通過 p 和 q 反向推導梯度,並更新其權重。

640-6.jpeg

圖 1. AAE 的基本架構最上面一行是自編碼器,而最下面一行是對抗網路,迫使到編碼器的輸出服從分佈 p(z)。

在對抗正則化部分,判別器收到來自分佈為 q(z|x)的 z 和來自真實先驗 p(z) 的 z' 取樣,併為每個來自 p(z)的樣本附加概率。發生的損失函式通過判別器反向傳播,以更新其權重。然後重複該過程,同時生成器更新其引數。

我們現在可以使用對抗網路(它是自編碼器的編碼器)的生成器產生的損失函式而不是 KL 散度,以便學習如何根據分佈 p(z)生成樣本。這種修改使我們能夠使用更廣泛的分佈作為潛在程式碼的先驗。

判別器的損失函式是

640-4.png

其中 m 是微批尺寸(minibatch size),z 由編碼器生成,z' 是來自真實先驗的樣本。

對於對抗生成器,我們有

640-5.png

通過檢視方程式和曲線,你應該明白,以這種方式定義的損失函式將強制判別器能夠識別假樣本,同時推動生成器欺騙判別器。

定義網路

在進入這個模型的訓練過程之前,我們來看一下如何在 Pytorch 中實現我們現在所做的工作。對於編碼器、解碼器和判別器網路,我們將使用 3 個帶有 ReLU 非線性函式與概率為 0.2 的 dropout 的 1000 隱藏狀態層的簡單前饋神經網路(feed forward neural network)。

在進入這個模型的訓練過程之前,我們來看一下如何在 Pytorch 中實現我們現在所做的工作。對於編碼器、解碼器和判別器網路,我們將使用 3 個帶有 ReLU 非線性函式與概率為 0.2 的 dropout 的 1000 隱藏狀態層的簡單前饋神經網路(feed forward neural network)。

#Encoderclass Q_net(nn.Module):      def __init__(self):        super(Q_net, self).__init__()        self.lin1 = nn.Linear(X_dim, N)        self.lin2 = nn.Linear(N, N)        self.lin3gauss = nn.Linear(N, z_dim)    def forward(self, x):        x = F.droppout(self.lin1(x), p=0.25, training=self.training)        x = F.relu(x)        x = F.droppout(self.lin2(x), p=0.25, training=self.training)        x = F.relu(x)        xgauss = self.lin3gauss(x)        return xgauss

# Decoderclass P_net(nn.Module):      def __init__(self):        super(P_net, self).__init__()        self.lin1 = nn.Linear(z_dim, N)        self.lin2 = nn.Linear(N, N)        self.lin3 = nn.Linear(N, X_dim)    def forward(self, x):        x = self.lin1(x)        x = F.dropout(x, p=0.25, training=self.training)        x = F.relu(x)        x = self.lin2(x)        x = F.dropout(x, p=0.25, training=self.training)        x = self.lin3(x)        return F.sigmoid(x)

# Discriminatorclass D_net_gauss(nn.Module):      def __init__(self):        super(D_net_gauss, self).__init__()        self.lin1 = nn.Linear(z_dim, N)        self.lin2 = nn.Linear(N, N)        self.lin3 = nn.Linear(N, 1)    def forward(self, x):        x = F.dropout(self.lin1(x), p=0.2, training=self.training)        x = F.relu(x)        x = F.dropout(self.lin2(x), p=0.2, training=self.training)        x = F.relu(x)        return F.sigmoid(self.lin3(x))

從這個定義可以注意到一些事情。首先,由於編碼器的輸出必須服從高斯分佈,我們在最後一層不使用任何非線性定義。解碼器的輸出具有 S 形非線性,這是因為我們使用以其值在 0 和 1 範圍內的標準化輸入。判別器網路的輸出僅為 0 和 1 之間的一個數字,表示來自真正先驗分佈的輸入概率。

一旦網路的類(class)定義完成,我們建立每個類的例項並定義要使用的優化器。為了在編碼器(這也是對抗網路的生成器)的優化過程中具有獨立性,我們為網路的這一部分定義了兩個優化器,如下所示:

torch.manual_seed(10)   Q, P = Q_net() = Q_net(), P_net(0)     # Encoder/Decoder  D_gauss = D_net_gauss()                # Discriminator adversarial  if torch.cuda.is_available():      Q = Q.cuda()    P = P.cuda()    D_cat = D_gauss.cuda()    D_gauss = D_net_gauss().cuda()# Set learning ratesgen_lr, reg_lr = 0.0006, 0.0008  # Set optimizatorsP_decoder = optim.Adam(P.parameters(), lr=gen_lr)   Q_encoder = optim.Adam(Q.parameters(), lr=gen_lr)   Q_generator = optim.Adam(Q.parameters(), lr=reg_lr)   D_gauss_solver = optim.Adam(D_gauss.parameters(), lr=reg_lr)  

訓練步驟

每個微批處理的架構的訓練步驟如下:

1)通過編碼器/解碼器部分進行前向路徑(forward path)計算,計算重建損失並更新編碼器 Q 和解碼器 P 網路的引數。

 z_sample = Q(X)    X_sample = P(z_sample)    recon_loss = F.binary_cross_entropy(X_sample + TINY,                                        X.resize(train_batch_size, X_dim) + TINY)    recon_loss.backward()    P_decoder.step()    Q_encoder.step()

2)建立潛在表徵 z = Q(x),並從先驗函式的 p(z) 取樣本 z',通過判別器執行每個樣本,並計算分配給每個 (D(z) 和 D(z')) 的分數。

   Q.eval()        z_real_gauss = Variable(torch.randn(train_batch_size, z_dim) * 5)   # Sample from N(0,5)    if torch.cuda.is_available():        z_real_gauss = z_real_gauss.cuda()    z_fake_gauss = Q(X)

3)計算判別器的損失函式,並通過判別器網路反向傳播更新其權重。在程式碼中,

# Compute discriminator outputs and loss    D_real_gauss, D_fake_gauss = D_gauss(z_real_gauss), D_gauss(z_fake_gauss)    D_loss_gauss = -torch.mean(torch.log(D_real_gauss + TINY) + torch.log(1 - D_fake_gauss + TINY))    D_loss.backward()       # Backpropagate loss    D_gauss_solver.step()   # Apply optimization step

4)計算生成網路的損失函式並相應地更新 Q 網路。

# GeneratorQ.train()   # Back to use dropout  z_fake_gauss = Q(X)  D_fake_gauss = D_gauss(z_fake_gauss)G_loss = -torch.mean(torch.log(D_fake_gauss + TINY))   G_loss.backward()   Q_generator.step()  

生成影象

現在我們嘗試視覺化 AAE 是如何將影象編碼成具有標準偏差為 5 的 2 維高斯潛在表徵的。為此,我們首先用 2 維隱藏狀態訓練模型。然後,我們在(-10,-10)(左上角)到(10,10)(右下角)的潛在空間上產生均勻點,並將其在解碼器網路上執行。

640-7.jpeg

潛在空間。同時在 x 和 y 軸上從 -10 到 10 均勻地探索 2 維潛在空間時的影象重建。

AAE 學習消糾纏表徵(disentangled representation)

資料的理想的中間表徵將能夠捕獲產生觀測資料變異的潛在因素。Yoshua Bengio 及其同事在一篇論文中(http://www.cl.uni-heidelberg.de/courses/ws14/deepl/BengioETAL12.pdf)中註明:「我們希望我們的表徵能夠消糾纏(解釋)變異因素。在輸入分佈中,不同的資料解釋因素傾向於彼此獨立地變化」。他們還提到「最魯棒的特徵學習方法是儘可能多地解釋因素,儘可能少地丟棄關於資料的資訊」。

在 MNIST(http://yann.lecun.com/exdb/mnist/)資料(這是關於手寫數字的大資料集)下,我們可以定義兩個潛在的因果性因素,一方面是生成的數字,另一方面是書寫的風格或方式。

監督式方法

在這部分中,我們比以前的架構進一步,並嘗試在潛在程式碼 z 中強加某些結構。特別地,我們希望架構能夠在完全監督的場景中將類別資訊與字跡風格分開。為此,我們將以前的架構擴充套件到下圖中。我們將潛在維度分為兩部分:第一個 z 類似於上一個例子;隱藏程式碼的第二部分現在是一個獨熱向量(one-hot vector)y 表示饋送到自編碼器的數字的身份。

640-8.jpeg

監督式對抗自編碼器架構。

在該設定中,解碼器使用獨熱向量 y 和隱藏程式碼 z 來重建原始影象。編碼器的任務是編寫 z 中的風格資訊。在下面的圖片中,我們可以看到用 10000 個標籤的 MNIST 樣本來訓練這個架構的結果。該圖顯示了重建影象,其中對於每行,隱藏程式碼 z 被固定為特定值,類別標籤 y 的範圍從 0 到 9。字跡風格在列的維度上有效地儲存了下來。

640-9.jpeg

通過探索潛在程式碼 y 並保持 z 從左到右固定重建影象。

半監督式方法

作為我們最後一個實驗,我們找到一種替代方法來獲得類似的消糾纏結果,在這種情況下,我們只有很少的標籤資訊樣本。我們可以修改之前的架構,使得 AAE 產生一個潛在的程式碼,它由表示類別或標籤(使用 Softmax)的向量 y 和連續的潛在變數 z(使用線性層)連線組成。由於我們希望向量 y 表現為一個獨熱向量,我們通過使用第二個帶有判別器 Dcat 的對抗網路迫使其遵從分類分佈。編碼器現在是 q(z,y|x)。解碼器使用類別標籤和連續隱藏程式碼重建影象。

640-10.jpeg

半監督式對抗自編碼器架構。

基於重建損失函式建立隱藏程式碼和改進無需標籤資訊的生成器和判別器網路,未標記的資料通過這種方式改進編碼器以促進訓練過程。

640-11.jpeg

用半監督式方法得到消糾纏結果。

值得注意的是,現在不僅可以通過較少標籤資訊生成影象,還可以通過檢視潛在程式碼 y 並選擇具有最高價值的影象來分類我們沒有標籤的影象。通過目前的設定,使用 100 個標籤樣本和 47000 個未標記的樣本,分類誤差約為 3%。

關於 GPU 訓練

最後,我們將在 Paperspace 平臺上為兩個不同 GPU 和 CPU 中的最後一個演算法做一個訓練時間方面的簡短比較。即使這種架構不是很複雜,而且由很少的線性層組成,但是在使用 GPU 加速時,訓練時間的改善是巨大的。經過 500 epoch 的訓練時間,從 CPU 的近 4 小時降至使用 Nvidia Quadro M4000 的 9 分鐘,使用 Nvidia Quadro P5000 進一步下降到 6 分鐘。

640-6.png

有無 GPU 加速的訓練時間對比

更多資料

  • What is a variational autoencoder (https://jaan.io/what-is-variational-autoencoder-vae-tutorial) (Tutorial)

  • Auto-encoding Variational Bayes (https://arxiv.org/abs/1312.6114) (original paper)

  • Adversarial Autoencoders (https://arxiv.org/abs/1511.05644) (original paper)

  • Building Machines that Imagine and Reason: Principles and Applications of Deep Generative Models (http://videolectures.net/deeplearning2016_mohamed_generative_models/) (Video Lecture)