PyTorch 學習筆記(九):自動編碼器(AutoEncoder)

一. 生成模型

生成模型(Generative Model)這一概念屬於概率統計與機器學習,是指一系列用於隨機生成可觀測預測數據得模型。簡而言之,就是 “生成” 的樣本和 “真實” 的樣本儘可能地相似。生成模型的兩個主要功能就是學習一個概率分佈Pmodel(X)P_{model}(X)和生成數據,這是非常重要的,不僅可以用在無監督學習中,還可以用在監督學習中。

無監督學習的發展一直比較緩慢,生成模型希望能夠讓無監督學習取得比較大的進展。

二. 自動編碼器——自監督學習

自動編碼器(AutoEncoder)最開始作爲一種數據的壓縮方法,其特點有:

  1. 跟數據相關程度很高,這意味着自動編碼器只能壓縮與訓練數據相似的數據,因爲使用神經網絡提取的特徵一般是高度相關於原始的訓練集,使用人臉訓練出的自動編碼器在壓縮自然界動物的圖片時就會表現的很差,因爲它只學習到了人臉的特徵,而沒有學習到自然界圖片的特徵。
  2. 壓縮後數據是有損的,這是因爲降維的過程中不可避免地丟失信息,解壓之後的輸出和原始的輸入相比是退化的。

到了2012年,人們發現在卷積神經網絡中使用自動編碼器做逐層預訓練可以訓練更深層的網絡,但是人們很快發現,良好的初始化策略要比複雜的逐層預訓練有效得多,2014年的Batch Normalization技術也使得更深的網絡能夠被有效訓練,到了2015年年底,通過ResNet基本可以訓練任意深度的神經網絡了。

現在自動編碼器主要應用在兩個方面:第一是數據去噪,第二是進行可視化降維。自動編碼器還有一個功能,即生成數據

那麼自動編碼器是如何對深層網絡做分層訓練的呢?我們先來看看自動編碼器的一般結構:
在這裏插入圖片描述
從上圖可以看到兩個部分:第一個部分是編碼器(Encoder),第二個部分是解碼器(Decoder),編碼器和解碼器都可以是任意的模型,通常使用神經網絡作爲編碼器和解碼器。輸入的數據經過神經網絡降維到一個編碼(code),接着又通過另外一個神經網絡去解碼得到一個與輸入原數據一模一樣的生成數據,然後通過比較這兩個數據,最小化它們之間的差異來訓練這個網絡中編碼器和解碼器的參數。

看完了網絡結構,我們再來看看自動編碼器是如何對深層網絡做分層訓練,如下圖所示,我們將input輸入一個encoder編碼器,就會得到一個code,這個code也就是輸入的一個表示,那麼我們怎麼知道這個code表示的就是input呢?我們加一個decoder解碼器,這時候decoder就會輸出一個信息,如果輸出的這個信息和一開始的輸入信號input是很像的(理想情況下就是一樣的),那很明顯,我們就有理由相信這個code是靠譜的。所以,我們就通過調整encoder和decoder的參數,使得重構誤差最小,這時候我們就得到了輸入input信號的第一個表示了,也就是編碼code了。因爲是無標籤數據,所以誤差的來源就是直接重構後與原輸入相比得到。我們的重構誤差最小讓我們相信這個code就是原輸入信號的良好表達了,或者牽強點說,它和原信號是一模一樣的。接着,我們將第一層輸出的code當成第二層的輸入信號,同樣最小化重構誤差,就會得到第二層的參數,並且得到第二層輸出的code,也就是原輸入信息的第二個表達了。其他層就同樣的方法炮製就行了(訓練這一層,前面層的參數都是固定的,並且他們的decoder已經沒用了,都不需要了)。

需要注意的是,整個網絡的訓練不是一蹴而就的,而是逐層進行。如果按n→m→k
結構,實際上我們是先訓練網絡n→m→n,得到n→m的變換,然後再訓練m→k→m,得到m→k的變換。最終堆疊成SAE,即爲n→m→k的結果,整個過程就像一層層往上蓋房子,這便是大名鼎鼎的 layer-wise unsuperwised pre-training (逐層非監督預訓練),正是導致深度學習(神經網絡)在2006年第3次興起的核心技術。
在這裏插入圖片描述
爲了儘量學到有意義的表達,我們會給code加入一定的約束。從數據維度來看,常見以下兩種情況:

  • 如果input的維度大於code的維度,也就是說從inputcodeinput\rightarrow code的變換是一種降維的操作,網絡試圖以更小的維度來描述原始數據而儘量不損失數據信息。實際上,當兩層之間的變換均爲線性,且損失函數爲平方差損失函數時,該網絡等價於PCA;
  • 如果input的維度小於等於code的維度。這又有什麼用呢?其實不好說,但比如我們同時約束code的表達儘量稀疏(有大量維度爲0,未被激活),此時的編碼器便是大名鼎鼎的“稀疏自編碼器”。可爲什麼稀疏的表達就是好的呢?這就說來話長了,有人試圖從人腦機理對比,即人類神經系統在某刺激下,大部分神經元是被抑制的。個人覺得,從特徵的角度來看直接些,稀疏的表達意味着系統在嘗試去特徵選擇,找出大量維度中真正重要的若干維度。

三. 自編碼器的一般形式

構建一個自動編碼器並當對其完成訓練完之後,拿出這個解碼器,隨機傳入一個編碼(code),通過解碼器能夠生成一個和原始數據差不多的數據,就是生成數據
在這裏插入圖片描述
下面我們將用PyTorch簡單地實現一個自動編碼器實現“生成數據”:

import torch
from torch import nn, optim
from torch.autograd import Variable
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torchvision.utils import save_image
import os
import matplotlib.pyplot as plt

# 加載數據集
def get_data():
    # 將像素點轉換到[-1, 1]之間,使得輸入變成一個比較對稱的分佈,訓練容易收斂
    data_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
    train_dataset = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
    train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size, drop_last=True)
    return train_loader

def to_img(x):
    x = (x + 1.) * 0.5
    x = x.clamp(0, 1)
    x = x.view(x.size(0), 1, 28, 28)
    return x
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):
        encode = self.encoder(x)
        decode = self.decoder(encode)
        return encode, decode

if __name__ == "__main__":
    # 超參數設置
    batch_size = 128
    lr = 1e-2
    weight_decay = 1e-5
    epoches = 40
    model = autoencoder()
    # x = Variable(torch.randn(1, 28*28))
    # encode, decode = model(x)
    # print(encode.shape)
    train_data = get_data()
    criterion = nn.MSELoss()
    optimizier = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
    if torch.cuda.is_available():
        model.cuda()
    for epoch in range(epoches):
        if epoch in [epoches * 0.25, epoches * 0.5]:
            for param_group in optimizier.param_groups:
                param_group['lr'] *= 0.1
        for img, _ in train_data:
            img = img.view(img.size(0), -1)
            img = Variable(img.cuda())
            # forward
            _, output = model(img)
            loss = criterion(output, img)
            # backward
            optimizier.zero_grad()
            loss.backward()
            optimizier.step()
        print("epoch=", epoch, loss.data.float())
        for param_group in optimizier.param_groups:
            print(param_group['lr'])
        if (epoch+1) % 5 == 0:
            print("epoch: {}, loss is {}".format((epoch+1), loss.data))
            pic = to_img(output.cpu().data)
            if not os.path.exists('./simple_autoencoder'):
                os.mkdir('./simple_autoencoder')
            save_image(pic, './simple_autoencoder/image_{}.png'.format(epoch + 1))
    # torch.save(model, './autoencoder.pth')
    # model = torch.load('./autoencoder.pth')
    code = Variable(torch.FloatTensor([[1.19, -3.36, 2.06]]).cuda())
    decode = model.decoder(code)
    decode_img = to_img(decode).squeeze()
    decode_img = decode_img.data.cpu().numpy() * 255
    plt.imshow(decode_img.astype('uint8'), cmap='gray')
    plt.show()

運行100個epoch之後的數據結果:
在這裏插入圖片描述

給訓練後的autoencoder隨機給一個code爲[[1.19, -3.36, 2.06]](其實這裏不嚴謹,我們並不知道給的這個隨機向量是否包含有數字的信息,所以有可能你賦值的隨機向量decoder之後的圖片並不是一張數字圖片),用decode解碼得到圖片:
在這裏插入圖片描述
可以看出來,解碼得到的圖片相當模糊,可能是因爲我們code的維度太低了,導致encoder的過程中損失的信息過多,decoder得到的圖片與原始圖片相比就會比較模糊。

考慮到全連接網絡的表徵能力有限,下面將其換爲卷積網絡試試。

import torch
from torch import nn, optim
from torch.autograd import Variable
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
from torchvision.utils import save_image
import os
import matplotlib.pyplot as plt

# 加載數據集
def get_data():
    # 將像素點轉換到[-1, 1]之間,使得輸入變成一個比較對稱的分佈,訓練容易收斂
    data_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
    train_dataset = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
    train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size, drop_last=True)
    return train_loader

def to_img(x):
    x = (x + 1.) * 0.5
    x = x.clamp(0, 1)
    x = x.view(x.size(0), 1, 28, 28)
    return x
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):
        encode = self.encoder(x)
        decode = self.decoder(encode)
        return encode, decode

if __name__ == "__main__":
    # 超參數設置
    batch_size = 128
    lr = 1e-2
    weight_decay = 1e-5
    epoches = 100
    model = autoencoder()
    # x = Variable(torch.randn(1, 28*28))
    # encode, decode = model(x)
    # print(encode.shape)
    train_data = get_data()
    criterion = nn.MSELoss()
    optimizier = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
    if torch.cuda.is_available():
        model.cuda()
    for epoch in range(epoches):
        if epoch in [epoches * 0.25, epoches * 0.5]:
            for param_group in optimizier.param_groups:
                param_group['lr'] *= 0.1
        for img, _ in train_data:
            # img = img.view(img.size(0), -1)
            img = Variable(img.cuda())
            # forward
            _, output = model(img)
            loss = criterion(output, img)
            # backward
            optimizier.zero_grad()
            loss.backward()
            optimizier.step()
        print("epoch=", epoch, loss.data.float())
        for param_group in optimizier.param_groups:
            print(param_group['lr'])
        if (epoch+1) % 5 == 0:
            print("epoch: {}, loss is {}".format((epoch+1), loss.data))
            pic = to_img(output.cpu().data)
            if not os.path.exists('./simple_autoencoder'):
                os.mkdir('./simple_autoencoder')
            save_image(pic, './simple_autoencoder/image_{}.png'.format(epoch + 1))
    torch.save(model, './autoencoder.pth')
    # model = torch.load('./autoencoder.pth')
    code = Variable(torch.FloatTensor([[1.19, -3.36, 2.06]]).cuda())
    decode = model.decoder(code)
    decode_img = to_img(decode).squeeze()
    decode_img = decode_img.data.cpu().data * 255
    plt.imshow(decode_img.numpy().astype('uint8'), cmap='gray')
    save_image(decode_img, './simple_autoencoder/image_code.png')
    plt.show()

運行了40個epoches後,結果:
在這裏插入圖片描述

這裏用到了反捲積的操作:https://zhuanlan.zhihu.com/p/48501100

四. 自編碼器的變種形式

學習完自編碼器的一般形式——堆疊自編碼器後,我們可能會有一個問題:code維度到底如何確定?爲什麼稀疏的特徵比較好?或者更準確來講,code怎麼才能稱得上是對input的一個好的表達?

事實上,這個答案並不唯一,也正是從不同的角度去思考這個問題,導致了自編碼器的各種變種形式出現。目前常見的幾種模型總結如下:
在這裏插入圖片描述

1. 變分自動編碼器

變分自動編碼器(Variational AutoEncoder)是自動編碼器的升級版本,它的結構和自動編碼器是相似的,也是由編碼器和解碼器構成的。

在自動編碼器中,需要一個輸入圖片,然後將輸入圖片編碼之後得到一個隱含向量,這比隨機取一個隨機向量好,因爲這包含着原圖片的信息,然後將隱含信息解碼得到與原圖片對應的照片。但是這樣並不能生成任意圖片,因爲沒辦法構造隱含向量,需要通過一張圖片輸入編碼才知道得到的隱含向量是什麼,這時就可以通過變分自動編碼器解決這個問題。

其實原理很簡單,只需要在編碼過程中給它增加一些限制,迫使它生成的隱含向量能夠粗略地遵循一個標準正態分佈,這就是它與一般的自動編碼器最大的不同。這樣生成一張新圖片就很簡單了,只需要給它一個標準正態分佈的隨機隱含向量,通過解碼器就能夠生成想要的圖片,而不需要先給它一張原始圖片編碼。

在實際情況中,需要在模型的準確率和隱含向量服從標準正態分佈之間做一個權衡,所謂模型的準確率就是指解碼器生成的圖片與原始圖片的相似程度。可以讓神經網絡自己做這個決定,只需要將兩者都做一個loss,然後求和作爲總的loss,這樣網絡就能夠自己選擇如何做才能使這個總的loss下降。另外要衡量兩種分佈的相似程度,需要引入一個新的概念,KL divergence,這是用來衡量兩種分佈相似程度的統計量,它越小,表示兩種概率分佈越接近。

對於離散的概率分佈,定義如下:DKL(PQ)=iP(i)logP(i)Q(i)D_{KL}(P||Q)=\sum _{i}P(i)log\frac{P(i)}{Q(i)}
對於連續的概率分佈,定義如下:DKL(PQ)=p(x)logp(x)q(x)dxD_{KL}(P||Q)=\int_{-\infty }^{\infty }p(x)log\frac{p(x)}{q(x)}dx

這裏就是用KL divergence 表示隱含向量與標準正態分佈之間差異的loss,另外一個loss仍然使用生成圖片與原圖片的均方誤差來表示。

這裏的變分編碼器使用了一個技巧——“重新參數化”來解決KL divergence的計算問題。這時不再是每次生成一個隱含向量,而是生成兩個向量:一個表示均值,一個表示標準差,然後通過這兩個統計量合成隱含向量,用一個標準正太分佈先乘標準差再加上均值就行了,這裏默認編碼之後的隱含向量是服從一個正態分佈的。這個時候要讓均值儘可能接近0,標準差儘可能接近1。

關於VAE的詳細瞭解參見:https://zhuanlan.zhihu.com/p/34998569

代碼如下:

import os
import torch
from torch import nn, optim
from torch.autograd import Variable
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
from torchvision.utils import save_image
# 加載數據集
def get_data():
    data_tf = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
    train_data = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
    train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size, drop_last=True)
    return train_loader

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 encoder(self, x):
        h1 = F.relu(self.fc1(x))
        mu = self.fc21(h1)
        logvar = self.fc22(h1)
        return mu, logvar

    def decoder(self, z):
        h3 = F.relu(self.fc3(z))
        x = F.tanh(self.fc4(h3))
        return x
    # 重新參數化
    def reparametrize(self, mu, logvar):
        std = logvar.mul(0.5).exp_()   # 計算標準差
        if torch.cuda.is_available():
            eps = torch.cuda.FloatTensor(std.size()).normal_()    # 從標準的正態分佈中隨機採樣一個eps
        else:
            eps = torch.FloatTensor(std.size()).normal_()
        eps = Variable(eps)
        return eps.mul(std).add_(mu)

    def forward(self, x):
        mu, logvar = self.encoder(x)
        z = self.reparametrize(mu, logvar)
        return self.decoder(z), mu, logvar

def loss_function(recon_x, x, mu, logvar):
    MSE = 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 MSE + KLD

def to_img(x):
    x = (x + 1.) * 0.5
    x = x.clamp(0, 1)
    x = x.view(x.size(0), 1, 28, 28)
    return x

if __name__ == '__main__':
    # 超參數設置
    batch_size = 128
    lr = 1e-3
    epoches = 100

    model = VAE()
    if torch.cuda.is_available():
        model.cuda()

    train_data = get_data()

    reconstruction_function = nn.MSELoss(reduction='sum')
    optimizer = optim.Adam(model.parameters(), lr=lr)

    for epoch in range(epoches):
        for img, _ in train_data:
            img = img.view(img.size(0), -1)
            img = Variable(img)
            if torch.cuda.is_available():
                img = img.cuda()
            # forward
            output, mu, logvar = model(img)
            loss = loss_function(output, img, mu, logvar)/img.size(0)
            # backward
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        print("epoch=", epoch, loss.data.float())
        if (epoch+1) % 10 == 0:
            print("epoch = {}, loss is {}".format(epoch+1, loss.data))
            pic = to_img(output.cpu().data)
            if not os.path.exists('./vae_img1'):
                os.mkdir('./vae_img1')
            save_image(pic, './vae_img1/image_{}.png'.format(epoch + 1))
    torch.save(model, './vae.pth')

運行100個eopch之後,結果如下,可以看出來結果比上面的自動編碼器清晰一點,本質上VAE就是在encoder的結果添加了高斯噪聲,通過訓練要使得decoder對噪聲有一定的魯棒性,這樣的話我們生成一張圖片就沒有必須用一張圖片先做編碼了,可以想象,我們只需要利用訓練好的encoder對一張圖片編碼得到其分佈後,符合這個分佈的隱含向量理論上都可以通過decoder得到類似這張圖片的圖片。

KL越小,噪聲越大(可以這麼理解,我們強行讓z的分佈符合正態分佈,其和N(0,1)越接近,KL越小,相當於我們添加的噪聲越大),所以直覺上來想loss合併後的訓練過程:

  • 當 decoder 還沒有訓練好時(重構誤差遠大於 KL loss),就會適當降低噪聲(KL loss 增加),使得擬合起來容易一些(重構誤差開始下降);
  • 反之,如果 decoder 訓練得還不錯時(重構誤差小於 KL loss),這時候噪聲就會增加(KL loss 減少),使得擬合更加困難了(重構誤差又開始增加),這時候 decoder 就要想辦法提高它的生成能力了。
    在這裏插入圖片描述

我們實現上面的代碼時遇到了一個坑,就是nn.MSE()函數,需要聲明的是nn.MSE(reduction=‘sum’)求的是每個batchsize上的loss值,最後除以batchsize就可以得到每個輸入的loss均值;nn.MSE(reduction=‘mean’)求的是所有元素的均值,而非每個輸入的均值,因爲每個輸入是28*28的向量,使用nn.MSE(reduction=‘mean’)計算出來的是nn.MSE(reduction=‘sum’)/(784 * batchsize)。所以我們這裏用的是nn.MSE(reduction=‘sum’。)

參考

https://zhuanlan.zhihu.com/p/34998569
《深度學習入門之PyTorch》一書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章