自编码AutoEncoder 及PyTorch 实现
自編碼AutoEncoder是一種無監督學習的算法,他利用反向傳播算法,讓目標值等于輸入值。什么意思呢,下面舉個例子:
有一個神經網絡,它在做的事情是,輸入一張圖片,通過一個Encoder神經網絡,輸出一個比較"濃縮的"feature map。之后將這個feature map通過一個Decoder網絡,結果又將這張圖片還原回去了
你也可以這么理解,整個Encoder+Decoder是一個神經網絡,中間的code只是暫存的數據
感覺就像是,現在有一鍋紅糖水,你不停的煮它,最終水都被煮干了,只剩下紅糖,這個紅糖就是上圖的"Code"。然后你再向紅糖里面注水、加熱,結果又還原回了一鍋紅糖水
假設上面的神經網絡展開如下圖所示,可以看出,圖片經過了一個壓縮,再解壓的工序。當壓縮的時候,原有的圖片質量被縮減。解壓時,用信息量少卻包含所有關鍵信息的文件恢復出了原本的圖片。為什么要這樣做呢?
因為有時候神經網絡要接受大量的輸入信息,比如說輸入信息是高清圖片時,信息量可能高達上千萬,讓神經網絡直接從上千萬個信息中進行學習是很吃力的。所以,為何不壓縮一下,提取出原圖片中最具代表性的信息,縮減輸入信息量,再把縮減后的信息帶入進行網絡學習。這樣學習起來就輕松多了
How to Train?
下圖是一個AutoEncoder的三層模型,其中,沒有什么含義,僅僅是個變量名字而已,用來區分,你也可以管叫
Vincent在2010年的論文(http://jmlr.org/papers/volume11/vincent10a/vincent10a.pdf)中做了研究,發現只要單組就可以了,即。和稱為Tied Weights(綁定的權重),實驗證明,真的只是在打醬油,完全沒有必要去訓練
如果是實數作為輸入,Loss function就是
如果輸入是binary的,即01值,那么就是
PCA V.S. AutoEncoder
同樣都是降維,PCA和AutoEncoder誰的效果更好呢?
首先從直覺上分析,PCA本質上是線性的變換,所以它是有局限性的。而AutoEncoder是基于DNN的,由于有activation function的存在,所以可以進行非線性變換,使用范圍更廣
下圖展示了MNIST數據集分別經過PCA和AutoEncoder降維再還原后的效果。第二行是是使用AutoEncoder的方法,可以看到幾乎沒什么太大變化;而第四行的圖片很多都變得非常模糊了。說明PCA的效果是不如AutoEncoder的
Denoising AutoEncoders
Vincent在2008的論文(http://www.iro.umontreal.ca/~lisa/publications2/index.php/publications/show/217)中提出了AutoEncoder的改良版——dA,論文標題叫"Extracting and Composing Robust Features",譯成中文就是"提取、編碼出具有魯棒性的特征"
首先我們考慮,為什么會產生這樣的變種AutoEncoder。如果我們僅僅只是在像素級別對一張圖片進行Encode,然后再重建,這樣就無法發現更深層次的信息,很有可能會導致網絡記住了一些特征。為了防止這種情況產生,我們可以給輸入圖片加一些噪聲,比方說生成和圖片同樣大小的高斯分布的數據,然后和圖像的像素值相加(見下圖)。如果這樣都能重建原來的圖片,意味著這個網絡能從這些混亂的信息中發現真正有用的特征,此時的Code才能代表輸入圖片的"精華"
關鍵是,這樣胡亂給原始圖片加噪聲真的科學嗎?Vincent從大腦認知角度給了解釋。Paper中說到:
人類具有認識被阻擋的破損圖像的能力,源于我們高等的聯想記憶感受機能
就是說,我們能以多種形式去記憶(比如圖像、聲音),所以即便是數據破損丟失,我們也能回想起來
Dropout AutoEncoders
其實這個沒什么特別的,平時我們不論是CNN還是RNN幾乎都會用到Dropout。據說Dropout是當時Hilton在給學生上課的時候提到的,用來幫助提升神經網路訓練效果的小Trick。具體關于Dropout的講解可以看我的這篇文章(https://wmathor.com/index.php/archives/1377/)
Adversarial AutoEncoders
在AutoEncoder中可能存在這樣一個問題,圖片經過Encode之后的vector并不符合我們希望的分布(例如高斯分布),他的分布很有可能如下圖所示。這其實是令我們不太滿意的(雖然我并不知道Code滿足分布到底有重要,但是既然別人認為很重要那就重要吧),那么有什么解決辦法呢?
由University of Toronto、Google Brain和OpenAI合作的文章Adversarial Autoencoders(AAE)(https://arxiv.org/pdf/1511.05644.pdf)提出了一個使用Autoencoder進行對抗學習的idea,某種程度上對上述問題提供了一些新思路
AAE的核心其實就是利用GAN的思想,利用一個生成器G和一個判別器D進行對抗學習,以區分Real data和Fake data。具體思路是這樣的,我現在需要一個滿足概率分布的向量,但是實際上滿足分布。那么我就首先生成一個滿足分布的向量,打上Real data的標簽,然后將向量打上Fake data的標簽,將它們倆送入判別器D。判別器D通過不斷學習,預測輸入input是來自于Real data(服從預定義的分布)還是Fake data(服從分布)。由于這里的可以是我們定義的任何一個概率分布,因此整個對抗學習的過程實際上可以認為是通過調整Encoder不斷讓其產生數據的概率分布接近我們預定義的
基本上AAE的原理就講完了,還剩最后一個問題,AAE的Loss function是什么?
其中
如果和分布非常接近,那么。回過頭看上面的Loss function就很好理解了,因為我們需要minimize loss,而且要求分布趨近于分布,而KL散度的作用正好是計算兩個分布的相似程度
下面簡述KL散度的公式推導。假設和均是服從和的隨機變量的概率密度函數,則
更加詳細的推導過程可以看這篇文章(https://hsinjhao.github.io/2019/05/22/KL-DivergenceIntroduction/)
Variational AutoEncoders
前面的各種AutoEncoder都是將輸入數據轉換為vector,其中每個維度代表學習到的數據。而Variational AutoEncoders(VAE)提供了一種概率分布的描述形式,VAE中Encoder描述的是每個潛在屬性的概率分布,而不是直接輸出一個值
舉例來說,假設我們已經在一個AutoEncoder上訓練了一個6維的vector,這個6維的vector將學習面部的一些屬性,例如膚色、是否戴眼鏡等
在上面的示例中,我們使用單個值來描述輸入圖像的潛在屬性。但是,我們可能更喜歡將每個潛在屬性表示為一個范圍。VAE就可以實現這個功能,如下圖所示
通過這種方法,我們現在將給定輸入的每個潛在屬性表示為概率分布。從狀態解碼(Decode)時,我們將從每個潛在狀態分布中隨機采樣以生成向量來作為解碼器的輸入
現在問題來了,sample()是不能求導的,那我們如何反向傳播?幸運的是,我們可以利用一個聰明的辦法——"reparameterization trick"。這個辦法的具體思想如下圖,假設我們要讓服從正態分布,我們可以生成一個標準正態,對這個標準正態進行平移以及伸縮變換,生成均值為,方差為的正態分布
通過reparameterization,we can now optimize the parameters of the distribution, while still maintaing?。就是說,我們保持不變,因為不需要學習修正,我們更新的只有和
AutoEncoder的PyTorch實現
其實AutoEncoder就是非常簡單的DNN。在encoder中神經元隨著層數的增加逐漸變少,也就是降維的過程。而在decoder中神經元隨著層數的增加逐漸變多,也就是升維的過程
class AE(nn.Module):def __init__(self):super(AE, self).__init__()self.encoder = nn.Sequential(# [b, 784] => [b, 256]nn.Linear(784, 256),nn.ReLU(),# [b, 256] => [b, 64]nn.Linear(256, 64),nn.ReLU(),# [b, 64] => [b, 20]nn.Linear(64, 20),nn.ReLU())self.decoder = nn.Sequential(# [b, 20] => [b, 64]nn.Linear(20, 64),nn.ReLU(),# [b, 64] => [b, 256]nn.Linear(64, 256),nn.ReLU(),# [b, 256] => [b, 784]nn.Linear(256, 784),nn.Sigmoid())def forward(self, x):""":param [b, 1, 28, 28]::return [b, 1, 28, 28]:"""batchsz = x.size(0)# flattenx = x.view(batchsz, -1)# encodex = self.encoder(x)# decodex = self.decoder(x)# reshapex = x.view(batchsz, 1, 28, 28)return x上面代碼都是基本操作,有一個地方需要特別注意,在decoder網絡中,最后跟的不是ReLU而是Sigmoid函數,因為我們想要將圖片打印出來看一下,而使用的數據集是MNIST,所以要將tensor里面的值最終都壓縮到0-1之間
然后定義訓練集和測試集,將它們分別帶入到DataLoader中
mnist_train = datasets.MNIST('mnist', train=True, transform=transforms.Compose([transforms.ToTensor() ]), download=True) mnist_train = DataLoader(mnist_train, batch_size=32, shuffle=True)mnist_test = datasets.MNIST('mnist', train=False, transform=transforms.Compose([transforms.ToTensor() ]), download=True) mnist_test = DataLoader(mnist_test, batch_size=32)由于input是0-1之間的實數,所以Loss function選擇MSE
epochs = 1000 lr = 1e-3 model = AE() criteon = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=lr) print(model)在通常(監督學習)情況下,我們需要將網絡的輸出output和訓練集的label進行對比,計算loss。但AutoEncoder是無監督學習,不需要label,我們只需要將網絡的輸出output和網絡的輸入input進行對比,計算loss即可
viz = visdom.Visdom()for epoch in range(epochs):# 不需要label,所以用一個占位符"_"代替for batchidx, (x, _) in enumerate(mnist_train):x_hat = model(x)loss = criteon(x_hat, x)# backpropoptimizer.zero_grad()loss.backward()optimizer.step()if epoch % 10 == 0:print(epoch, 'loss:', loss.item())x, _ = iter(mnist_test).next()with torch.no_grad():x_hat = model(x)viz.images(x, nrow=8, win='x', opts=dict(title='x'))viz.images(x_hat, nrow=8, win='x_hat', opts=dict(title='x_hat'))到這里,最簡單的AutoEncoder代碼已經寫完了,完整代碼如下:
import torch import visdom from torch.utils.data import DataLoader from torchvision import transforms, datasets from torch import nn, optimclass AE(nn.Module):def __init__(self):super(AE, self).__init__()self.encoder = nn.Sequential(# [b, 784] => [b, 256]nn.Linear(784, 256),nn.ReLU(),# [b, 256] => [b, 64]nn.Linear(256, 64),nn.ReLU(),# [b, 64] => [b, 20]nn.Linear(64, 20),nn.ReLU())self.decoder = nn.Sequential(# [b, 20] => [b, 64]nn.Linear(20, 64),nn.ReLU(),# [b, 64] => [b, 256]nn.Linear(64, 256),nn.ReLU(),# [b, 256] => [b, 784]nn.Linear(256, 784),nn.Sigmoid())def forward(self, x):""":param [b, 1, 28, 28]::return [b, 1, 28, 28]:"""batchsz = x.size(0)# flattenx = x.view(batchsz, -1)# encoderx = self.encoder(x)# decoderx = self.decoder(x)# reshapex = x.view(batchsz, 1, 28, 28)return xdef main():mnist_train = datasets.MNIST('mnist', train=True, transform=transforms.Compose([transforms.ToTensor()]), download=True)mnist_train = DataLoader(mnist_train, batch_size=32, shuffle=True)mnist_test = datasets.MNIST('mnist', train=False, transform=transforms.Compose([transforms.ToTensor()]), download=True)mnist_test = DataLoader(mnist_test, batch_size=32)epochs = 1000lr = 1e-3model = AE()criteon = nn.MSELoss()optimizer = optim.Adam(model.parameters(), lr=lr)print(model)viz = visdom.Visdom()for epoch in range(epochs):# 不需要label,所以用一個占位符"_"代替for batchidx, (x, _) in enumerate(mnist_train):x_hat = model(x)loss = criteon(x_hat, x)# backpropoptimizer.zero_grad()loss.backward()optimizer.step()if epoch % 10 == 0:print(epoch, 'loss:', loss.item())x, _ = iter(mnist_test).next()with torch.no_grad():x_hat = model(x)viz.images(x, nrow=8, win='x', opts=dict(title='x'))viz.images(x_hat, nrow=8, win='x_hat', opts=dict(title='x_hat'))if __name__ == '__main__':main()得到的效果如下圖所示,普通的AutoEncoder還是差了一點,可以看到很多圖片已經看不清具體代表的數字了
Variational AutoEncoders
AutoEncoder的shape變化是[b, 784] => [b, 20] => [b, 784],雖然VAE也是這樣,但其中的20并不一樣,對于VAE來說,[b, 20]要分成兩個[b, 10],分別是和,具體形式見下圖
最主要先關注一下定義網絡的部分
class VAE(nn.Module):def __init__(self):super(VAE, self).__init__()# [b, 784] => [b, 20]# u: [b, 10]# sigma: [b, 10]self.encoder = nn.Sequential(# [b, 784] => [b, 256]nn.Linear(784, 256),nn.ReLU(),# [b, 256] => [b, 64]nn.Linear(256, 64),nn.ReLU(),# [b, 64] => [b, 20]nn.Linear(64, 20),nn.ReLU())self.decoder = nn.Sequential(# [b, 10] => [b, 64]nn.Linear(10, 64),nn.ReLU(),# [b, 64] => [b, 256]nn.Linear(64, 256),nn.ReLU(),# [b, 256] => [b, 784]nn.Linear(256, 784),nn.Sigmoid())def forward(self, x):""":param [b, 1, 28, 28]::return [b, 1, 28, 28]:"""batchsz = x.size(0)# flattenx = x.view(batchsz, -1)# encoder# [b, 20] including mean and sigmaq = self.encoder(x)# [b, 20] => [b, 10] and [b, 10]mu, sigma = q.chunk(2, dim=1)# reparameterize trick, epsilon~N(0, 1)q = mu + sigma * torch.randn_like(sigma)# decoderx_hat = self.decoder(q)# reshapex_hat = x_hat.view(batchsz, 1, 28, 28)# KLkld = 0.5 * torch.sum(torch.pow(mu, 2) +torch.pow(sigma, 2) -torch.log(1e-8 + torch.pow(sigma, 2)) - 1) / (batchsz*28*28)return x_hat, kldEncode以后的變量要分成兩半兒,利用h.chunk(num, dim)實現,num表示要分成幾塊,dim值表示在什么維度上進行。然后隨機采樣出標準正態分布的數據,用和對其進行變換。這里的kld指的是KL Divergence,它是Loss的一部分,其計算過程如下:
import torch import visdom import numpy as np from torch import nn, optim from torch.utils.data import DataLoader from torchvision import transforms, datasetsclass VAE(nn.Module):def __init__(self):super(VAE, self).__init__()# [b, 784] => [b, 20]# u: [b, 10]# sigma: [b, 10]self.encoder = nn.Sequential(# [b, 784] => [b, 256]nn.Linear(784, 256),nn.ReLU(),# [b, 256] => [b, 64]nn.Linear(256, 64),nn.ReLU(),# [b, 64] => [b, 20]nn.Linear(64, 20),nn.ReLU())self.decoder = nn.Sequential(# [b, 10] => [b, 64]nn.Linear(10, 64),nn.ReLU(),# [b, 64] => [b, 256]nn.Linear(64, 256),nn.ReLU(),# [b, 256] => [b, 784]nn.Linear(256, 784),nn.Sigmoid())def forward(self, x):""":param [b, 1, 28, 28]::return [b, 1, 28, 28]:"""batchsz = x.size(0)# flattenx = x.view(batchsz, -1)# encoder# [b, 20] including mean and sigmaq = self.encoder(x)# [b, 20] => [b, 10] and [b, 10]mu, sigma = q.chunk(2, dim=1)# reparameterize trick, epsilon~N(0, 1)q = mu + sigma * torch.randn_like(sigma)# decoderx_hat = self.decoder(q)# reshapex_hat = x_hat.view(batchsz, 1, 28, 28)# KLkld = 0.5 * torch.sum(torch.pow(mu, 2) +torch.pow(sigma, 2) -torch.log(1e-8 + torch.pow(sigma, 2)) - 1) / (batchsz*28*28)return x_hat, klddef main():mnist_train = datasets.MNIST('mnist', train=True, transform=transforms.Compose([transforms.ToTensor()]), download=True)mnist_train = DataLoader(mnist_train, batch_size=32, shuffle=True)mnist_test = datasets.MNIST('mnist', train=False, transform=transforms.Compose([transforms.ToTensor()]), download=True)mnist_test = DataLoader(mnist_test, batch_size=32)epochs = 1000lr = 1e-3model = VAE()criteon = nn.MSELoss()optimizer = optim.Adam(model.parameters(), lr=lr)print(model)viz = visdom.Visdom()for epoch in range(epochs):# 不需要label,所以用一個占位符"_"代替for batchidx, (x, _) in enumerate(mnist_train):x_hat, kld = model(x)loss = criteon(x_hat, x)if kld is not None:elbo = loss + 1.0 * kldloss = elbo# backpropoptimizer.zero_grad()loss.backward()optimizer.step()if epoch % 10 == 0:print(epoch, 'loss:', loss.item(), 'kld', kld.item())x, _ = iter(mnist_test).next()with torch.no_grad():x_hat, kld = model(x)viz.images(x, nrow=8, win='x', opts=dict(title='x'))viz.images(x_hat, nrow=8, win='x_hat', opts=dict(title='x_hat'))if __name__ == '__main__':main()往期精彩回顧適合初學者入門人工智能的路線及資料下載機器學習在線手冊深度學習在線手冊AI基礎下載(pdf更新到25集)備注:加入本站微信群或者qq群,請回復“加群”獲取一折本站知識星球優惠券,請回復“知識星球”喜歡文章,點個在看
總結
以上是生活随笔為你收集整理的自编码AutoEncoder 及PyTorch 实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 入门:现实世界中的推荐系统(术语、技术等
- 下一篇: BERT, ELMo, GPT-2: 这