花式解釋AutoEncoder與VAE
什么是自動編碼器
自動編碼器(AutoEncoder)最開始作為一種數據的壓縮方法,其特點有:
1)跟數據相關程度很高,這意味着自動編碼器只能壓縮與訓練數據相似的數據,這個其實比較顯然,因為使用神經網絡提取的特征一般是高度相關於原始的訓練集,使用人臉訓練出來的自動編碼器在壓縮自然界動物的圖片是表現就會比較差,因為它只學習到了人臉的特征,而沒有能夠學習到自然界圖片的特征;
2)壓縮后數據是有損的,這是因為在降維的過程中不可避免的要丟失掉信息;
到了2012年,人們發現在卷積網絡中使用自動編碼器做逐層預訓練可以訓練更加深層的網絡,但是很快人們發現良好的初始化策略要比費勁的逐層預訓練有效地多,2014年出現的Batch Normalization技術也是的更深的網絡能夠被被有效訓練,到了15年底,通過殘差(ResNet)我們基本可以訓練任意深度的神經網絡。
所以現在自動編碼器主要應用有兩個方面,第一是數據去噪,第二是進行可視化降維。然而自動編碼器還有着一個功能就是生成數據。
我們之前講過GAN,它與GAN相比有着一些好處,同時也有着一些缺點。我們先來講講其跟GAN相比有着哪些優點。
第一點,我們使用GAN來生成圖片有個很不好的缺點就是我們生成圖片使用的隨機高斯噪聲,這意味着我們並不能生成任意我們指定類型的圖片,也就是說我們沒辦法決定使用哪種隨機噪聲能夠產生我們想要的圖片,除非我們能夠把初始分布全部試一遍。但是使用自動編碼器我們就能夠通過輸出圖片的編碼過程得到這種類型圖片的編碼之后的分布,相當於我們是知道每種圖片對應的噪聲分布,我們就能夠通過選擇特定的噪聲來生成我們想要生成的圖片。
第二點,這既是生成網絡的優點同時又有着一定的局限性,這就是生成網絡通過對抗過程來區分“真”的圖片和“假”的圖片,然而這樣得到的圖片只是盡可能像真的,但是這並不能保證圖片的內容是我們想要的,換句話說,有可能生成網絡盡可能的去生成一些背景圖案使得其盡可能真,但是里面沒有實際的物體。
自動編碼器的結構
首先我們給出自動編碼器的一般結構

從上面的圖中,我們能夠看到兩個部分,第一個部分是編碼器(Encoder),第二個部分是解碼器(Decoder),編碼器和解碼器都可以是任意的模型,通常我們使用神經網絡模型作為編碼器和解碼器。輸入的數據經過神經網絡降維到一個編碼(code),接着又通過另外一個神經網絡去解碼得到一個與輸入原數據一模一樣的生成數據,然后通過去比較這兩個數據,最小化他們之間的差異來訓練這個網絡中編碼器和解碼器的參數。當這個過程訓練完之后,我們可以拿出這個解碼器,隨機傳入一個編碼(code),希望通過解碼器能夠生成一個和原數據差不多的數據,上面這種圖這個例子就是希望能夠生成一張差不多的圖片。

這件事情能不能實現呢?其實是可以的,下面我們會用PyTorch來簡單的實現一個自動編碼器。
首先我們構建一個簡單的多層感知器來實現一下。
class autoencoder(nn.Module): def __init__(self): super(autoencoder, self).__init__() self.encoder = nn.Sequential( nn.Linear(28*28, 128), nn.ReLU(True), nn.Linear(128, 64), nn.ReLU(True), nn.Linear(64, 12), nn.ReLU(True), nn.Linear(12, 3) ) self.decoder = nn.Sequential( nn.Linear(3, 12), nn.ReLU(True), nn.Linear(12, 64), nn.ReLU(True), nn.Linear(64, 128), nn.ReLU(True), nn.Linear(128, 28*28), nn.Tanh() ) def forward(self, x): x = self.encoder(x) x = self.decoder(x) return x
這里我們定義了一個簡單的4層網絡作為編碼器,中間使用ReLU激活函數,最后輸出的維度是3維的,定義的解碼器,輸入三維的編碼,輸出一個28x28的圖像數據,特別要注意最后使用的激活函數是Tanh,這個激活函數能夠將最后的輸出轉換到-1 ~1之間,這是因為我們輸入的圖片已經變換到了-1~1之間了,這里的輸出必須和其對應。
訓練過程也比較簡單,我們使用最小均方誤差來作為損失函數,比較生成的圖片與原始圖片的每個像素點的差異。
同時我們也可以將多層感知器換成卷積神經網絡,這樣對圖片的特征提取有着更好的效果。
class autoencoder(nn.Module):
def __init__(self):
super(autoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 3, stride=3, padding=1), # b, 16, 10, 10
nn.ReLU(True),
nn.MaxPool2d(2, stride=2), # b, 16, 5, 5
nn.Conv2d(16, 8, 3, stride=2, padding=1), # b, 8, 3, 3
nn.ReLU(True),
nn.MaxPool2d(2, stride=1) # b, 8, 2, 2
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(8, 16, 3, stride=2), # b, 16, 5, 5
nn.ReLU(True),
nn.ConvTranspose2d(16, 8, 5, stride=3, padding=1), # b, 8, 15, 15
nn.ReLU(True),
nn.ConvTranspose2d(8, 1, 2, stride=2, padding=1), # b, 1, 28, 28
nn.Tanh()
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x
這里使用了nn.ConvTranspose2d(),這可以看作是卷積的反操作,可以在某種意義上看作是反卷積。
我們使用卷積網絡得到的最后生成的圖片效果會更好,具體的圖片效果我就不再這里放了,可以在我們的github上看到圖片的展示。
變分自動編碼器(Variational Autoencoder)
變分編碼器是自動編碼器的升級版本,其結構跟自動編碼器是類似的,也由編碼器和解碼器構成。
回憶一下我們在自動編碼器中所做的事,我們需要輸入一張圖片,然后將一張圖片編碼之后得到一個隱含向量,這比我們隨機取一個隨機噪聲更好,因為這包含着原圖片的信息,然后我們隱含向量解碼得到與原圖片對應的照片。
但是這樣我們其實並不能任意生成圖片,因為我們沒有辦法自己去構造隱藏向量,我們需要通過一張圖片輸入編碼我們才知道得到的隱含向量是什么,這時我們就可以通過變分自動編碼器來解決這個問題。
其實原理特別簡單,只需要在編碼過程給它增加一些限制,迫使其生成的隱含向量能夠粗略的遵循一個標准正態分布,這就是其與一般的自動編碼器最大的不同。
這樣我們生成一張新圖片就很簡單了,我們只需要給它一個標准正態分布的隨機隱含向量,這樣通過解碼器就能夠生成我們想要的圖片,而不需要給它一張原始圖片先編碼。
在實際情況中,我們需要在模型的准確率上與隱含向量服從標准正態分布之間做一個權衡,所謂模型的准確率就是指解碼器生成的圖片與原圖片的相似程度。我們可以讓網絡自己來做這個決定,非常簡單,我們只需要將這兩者都做一個loss,然后在將他們求和作為總的loss,這樣網絡就能夠自己選擇如何才能夠使得這個總的loss下降。另外我們要衡量兩種分布的相似程度,如何看過之前一片GAN的數學推導,你就知道會有一個東西叫KL divergence來衡量兩種分布的相似程度,這里我們就是用KL divergence來表示隱含向量與標准正態分布之間差異的loss,另外一個loss仍然使用生成圖片與原圖片的均方誤差來表示。
我們可以給出KL divergence 的公式

這里變分編碼器使用了一個技巧“重新參數化”來解決KL divergence的計算問題。

這時不再是每次產生一個隱含向量,而是生成兩個向量,一個表示均值,一個表示標准差,然后通過這兩個統計量來合成隱含向量,這也非常簡單,用一個標准正態分布先乘上標准差再加上均值就行了,這里我們默認編碼之后的隱含向量是服從一個正態分布的。這個時候我們是想讓均值盡可能接近0,標准差盡可能接近1。而論文里面有詳細的推導如何得到這個loss的計算公式,有興趣的同學可以去看看推導
下面是PyTorch的實現
reconstruction_function = nn.BCELoss(size_average=False) # mse loss def loss_function(recon_x, x, mu, logvar): """ recon_x: generating images x: origin images mu: latent mean logvar: latent log variance """ BCE = reconstruction_function(recon_x, x) # loss = 0.5 * sum(1 + log(sigma^2) - mu^2 - sigma^2) KLD_element = mu.pow(2).add_(logvar.exp()).mul_(-1).add_(1).add_(logvar) KLD = torch.sum(KLD_element).mul_(-0.5) # KL divergence return BCE + KLD
另外變分編碼器除了可以讓我們隨機生成隱含變量,還能夠提高網絡的泛化能力。
最后是VAE的代碼實現
class VAE(nn.Module): def __init__(self): super(VAE, self).__init__() self.fc1 = nn.Linear(784, 400) self.fc21 = nn.Linear(400, 20) self.fc22 = nn.Linear(400, 20) self.fc3 = nn.Linear(20, 400) self.fc4 = nn.Linear(400, 784) def encode(self, x): h1 = F.relu(self.fc1(x)) return self.fc21(h1), self.fc22(h1) def reparametrize(self, mu, logvar): std = logvar.mul(0.5).exp_() if torch.cuda.is_available(): eps = torch.cuda.FloatTensor(std.size()).normal_() else: eps = torch.FloatTensor(std.size()).normal_() eps = Variable(eps) return eps.mul(std).add_(mu) def decode(self, z): h3 = F.relu(self.fc3(z)) return F.sigmoid(self.fc4(h3)) def forward(self, x): mu, logvar = self.encode(x) z = self.reparametrize(mu, logvar