文章目錄
- 前言
- 1 自編碼器
- 1.1 原理
- 1.2 PyTorch實作圖片重塑
- 1.2.1 Fashion MNIST 資料集
- 1.2.2 網路結構
- 1.2.3 訓練
- 1.2.4 圖片重塑
- 2 自編碼器變種
- 2.1 降噪自編碼器(DAE)
- 2.2 對抗自編碼器(AAE)
- 2.3 變分自編碼器(VAE)
- 2.3.1 原理
- 2.3.2 VAE圖片生成
前言
??目前我們可以通過爬蟲等方式獲取海量的樣本資料𝒙,如照片、語音、文本等,是相對容易的,但困難的是獲取這些資料所對應的標簽資訊,例如機器翻譯,除了收集源語言的對話文本外,還需要待翻譯的目標語言文本資料,資料的標注作業目前主要還是依賴人的先驗知識來完成,因此,面對海量的無標注資料,我們需要從中學習到資料的分布𝑃(𝒙)的演算法,而無監督演算法模型就是針對這類問題而發展的,特別地,如果演算法把𝒙作為監督信號來學習,這類演算法稱為自監督學習,本博客介紹的自編碼器就屬于自監督學習范疇,
1 自編碼器
1.1 原理
??自編碼器是通過對輸入
x
x
x進行編碼后得到一個低維的向量
z
z
z,然后根據 這個向量還原出輸入
x
x
x,通過對比
x
x
x與
x
ˉ
\bar{x}
xˉ的誤差,再利用神經網路去訓練使得誤差逐漸減小,從而達到非監督學習的目的,結構如下圖所示,

??其中我們將資料𝒙本身作為監督信號來指導網路的訓練,即希望神經網路能夠學習到映射
𝑓
θ
{𝑓_θ}
fθ?:
x
x
x →
x
x
x,我們把網路𝑓𝜃切分為兩個部分,前面的子網路嘗試學習映射關系:
g
θ
1
g_{θ1}
gθ1?:
x
x
x →
z
z
z,后面的子網路嘗試學習映射關系:
h
θ
2
h_{θ2}
hθ2?:
z
z
z →
x
x
x,我們把
g
θ
1
g_{θ1}
gθ1?看成一個資料編碼(Encode)的程序,作用就是將輸入
x
x
x編碼成低緯度的隱藏變數
z
z
z,
h
θ
2
h_{θ2}
hθ2?看成一個資料解碼(Dncode)的程序,作用是將隱藏變數
z
z
z重塑成高緯度的
x
x
x,編碼器和解碼器共同完成了輸入資料
x
x
x的編碼和解碼程序,我們把整個網路模型
𝑓
θ
{𝑓_θ}
fθ?叫做自動編碼器(Auto-Encoder),如果網路含有多個隱藏層,則稱為深度自編碼器(Deep Auto-encoder),
??自編碼器的編碼器通過編碼器壓縮得到的隱藏變數
z
z
z重塑
x
ˉ
\bar{x}
xˉ,我們希望解碼器的輸出能夠完美地或者近似恢復出原來的輸入,即
x
x
x約等于
x
ˉ
\bar{x}
xˉ,則自編碼器的損失函式可定義為
M
i
n
i
m
i
z
e
L
=
d
i
s
t
(
x
,
x
ˉ
)
x
ˉ
=
h
θ
2
(
g
θ
1
(
x
)
)
\begin{aligned} &Minimize ? = dist(x, \bar{x})\\ &\bar{x} = h_{θ2}(g_{θ1}(x))\\ \end{aligned}
?MinimizeL=dist(x,xˉ)xˉ=hθ2?(gθ1?(x))?
??其中
d
i
s
t
(
x
,
x
ˉ
)
dist(x, \bar{x})
dist(x,xˉ)表示$x與 \bar{x}的距離,常見的距離度量函式為歐氏距離(也即均方差):
L
=
∑
i
(
x
?
x
ˉ
)
2
\begin{aligned} &? = \sum_{i}(x - \bar{x})^2\\ \end{aligned}
?L=i∑?(x?xˉ)2?
1.2 PyTorch實作圖片重塑
1.2.1 Fashion MNIST 資料集
??Fashion MNIST 是一個定位在比 MNIST 圖片識別問題稍復雜的資料集,它的設定與MNIST 幾乎完全一樣,包含了 10 類不同型別的衣服、鞋子、包等灰度圖片,圖片大小為28 × 28,共 70000 張圖片,其中 60000 張用于訓練集,10000 張用于測驗集,Fashion MNIST 除了圖片內容與 MNIST 不一樣,其它設定都相同,大部分情況可以直接替換掉原來基于 MNIST 訓練的演算法代碼,而不需要額外修改,由于 Fashion MNIST 圖片識別相對于 MNIST 圖片更難,因此可以用于測驗稍復雜的演算法性能,

??在PyTorch中可以直接使用torchvision包進行在線下載,
dataset = datasets.FashionMNIST(root = 'data2',
train = False,
download = True,
transform = transforms.ToTensor())
1.2.2 網路結構
??我們利用編碼器將輸入圖片
x
∈
𝑅
784
降
維
到
較
低
維
度
的
隱
藏
向
量
z
∈
𝑅
20
x ∈ 𝑅^{784}降維到較低維度的隱藏向量z∈ 𝑅^{20}
x∈R784降維到較低維度的隱藏向量z∈R20,并基于隱藏向量 利用解碼器重建圖片,自編碼器模型如圖所示,編碼器由 3 層全連接層網路組成,輸出節點數分別為 256、128、20,解碼器同樣由 3 層全連接網路組成,輸出節點數分別為 128、256、784,

class AE(nn.Module) :
def __init__(self):
super(AE, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(256, 128)
self.relu2 = nn.ReLU()
self.fc3 = nn.Linear(128, 20)
self.fc4 = nn.Linear(20, 128)
self.relu3 = nn.ReLU()
self.fc5 = nn.Linear(128, 256)
self.relu4 = nn.ReLU()
self.fc6 = nn.Linear(256, 784)
def Encoder(self, x):
h1 = self.relu1(self.fc1(x))
h2 = self.relu2(self.fc2(h1))
return self.fc3(h2)
def Decoder(self, z):
h1 = self.relu3(self.fc4(z))
h2 = self.relu4(self.fc5(h1))
return F.sigmoid(self.fc6(h2))
def forward(self, x):
z = self.Encoder(x)
# print(z.shape)
x_reconst = self.Decoder(z)
return x_reconst
1.2.3 訓練
??自編碼器的訓練程序與分類器的基本一致,通過誤差函式計算出重建向量 x ˉ 與 原 始 輸 入 向 量 x \bar{x} 與原始輸入向量x xˉ與原始輸入向量x之間的距離,
for epoch in range(MAX_EPOCH) :
model.train()
for i, (x, _) in enumerate(data_loader) :
x = x.to(device)
optimizer.zero_grad()
x = x.view(-1, image_size)
x_reconst = model(x)
# 重構損失,使用二元分類損失
reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False)
reconst_loss.backward()
optimizer.step()
1.2.4 圖片重塑
??與分類問題不同的是,自編碼器的模型性能一般不好量化評價,盡管?值可以在一定程度上代表網路的學習效果,但我們最終希望獲得還原度較高、樣式較豐富的重建樣本,對于圖片來說,一般依賴于人工主觀的評估,在這次實踐中正確做法應是將資料集劃分為訓練集和測驗集,用測驗集來進行圖片重塑對比,但我為了方便就直接使用訓練集來進行重塑了,
with torch.no_grad() :
out = model(x)
#將與原圖與重塑影像進行拼接,奇數列為原影像,偶數列為重塑影像
x_concat = torch.cat([x.view(-1, 1, 28, 28), out.view(-1, 1, 28, 28)], dim = 3)
save_image(x_concat, os.path.join(samples_dir, f'reconst-{epoch

??重塑影像如上圖,其中奇數列為原影像,偶數列為重塑影像,可以看到,第一個 Epoch 時,圖片重建效果較差,圖片非常模糊,逼真度較差;隨著訓練的進行,重建圖片邊緣越來越清晰,第 100 個 Epoch時,重建的圖片效果已經比較接近真實圖片,
全部代碼:
import os
import torch
import cv2 as cv
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.datasets as datasets
from torch.utils.data import DataLoader, Dataset
from torchvision.utils import save_image
from torchvision import transforms
MAX_EPOCH = 100
lr_learning = 0.001
batch_size = 64
image_size = 784
os.makedirs('samples_AE', exist_ok = True)
samples_dir = 'samples_AE'
dataset = datasets.FashionMNIST(root = 'data2',
train = False,
download = True,
transform = transforms.ToTensor())
data_loader = DataLoader(dataset, shuffle = True, batch_size = batch_size, drop_last = True)
class AE(nn.Module) :
def __init__(self):
super(AE, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(256, 128)
self.relu2 = nn.ReLU()
self.fc3 = nn.Linear(128, 20)
self.fc4 = nn.Linear(20, 128)
self.relu3 = nn.ReLU()
self.fc5 = nn.Linear(128, 256)
self.relu4 = nn.ReLU()
self.fc6 = nn.Linear(256, 784)
def Encoder(self, x):
h1 = self.relu1(self.fc1(x))
h2 = self.relu2(self.fc2(h1))
return self.fc3(h2)
def Decoder(self, z):
h1 = self.relu3(self.fc4(z))
h2 = self.relu4(self.fc5(h1))
return F.sigmoid(self.fc6(h2))
def forward(self, x):
z = self.Encoder(x)
# print(z.shape)
x_reconst = self.Decoder(z)
return x_reconst
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AE().to(device)
optimizer = optim.Adam(model.parameters(), lr = lr_learning)
for epoch in range(MAX_EPOCH) :
model.train()
for i, (x, _) in enumerate(data_loader) :
x = x.to(device)
optimizer.zero_grad()
x = x.view(-1, image_size)
x_reconst = model(x)
# 重構損失,使用二元分類損失
reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False)
reconst_loss.backward()
optimizer.step()
with torch.no_grad() :
out = model(x)
#將與原圖與重塑影像進行拼接,奇數列為原影像,偶數列為重塑影像
x_concat = torch.cat([x.view(-1, 1, 28, 28), out.view(-1, 1, 28, 28)], dim = 3)
save_image(x_concat, os.path.join(samples_dir, f'reconst-{epoch + 1}.png'))
img1 = cv.imread('samples_AE/reconst-1.png')
img2 = cv.imread('samples_AE/reconst-50.png')
img3 = cv.imread('samples_AE/reconst-100.png')
images = [img1, img2, img3]
xlabels = ['epoch : 1', 'epoch : 50', 'epoch : 100']
for i in range(3) :
plt.subplot(1, 3, i + 1)
plt.imshow(images[i], 'gray')
plt.axis('off')
plt.title(xlabels[i])
plt.tight_layout()
plt.show()
2 自編碼器變種
2.1 降噪自編碼器(DAE)
??DAE是通過改變重構誤差項來獲得一個能學到有用資訊的自編碼器,對于傳統的自編碼器最小優化目標:
θ
?
=
a
r
g
m
i
n
θ
d
i
s
t
(
h
θ
2
(
𝑔
θ
1
(
x
)
)
,
x
)
\begin{aligned} &θ^? = argmin _θ \quad\ dist(?θ2(𝑔θ1(x)), x)\\ \end{aligned}
?θ?=argminθ? dist(hθ2(gθ1(x)),x)?
??對于這個函式如果模型被賦予過大的容量,損失函式僅僅使得 g ? f 學成一個恒等函式,也即網路會簡單地復制輸入,網路沒有學習特征的能力,DAE給網路輸入
x
x
x添加采樣自高斯分布的噪聲
α
\alpha
α:
x
′
=
x
+
α
,
α
∈
𝒩
(
0
,
v
a
r
)
\begin{aligned} &x^\prime = x + \alpha, \alpha∈𝒩(0, var)\\ \end{aligned}
?x′=x+α,α∈N(0,var)?
則優化目標變成:
θ
?
=
a
r
g
m
i
n
θ
d
i
s
t
(
h
θ
2
(
𝑔
θ
1
(
x
′
)
)
,
x
)
\begin{aligned} &θ^? = argmin _θ \quad\ dist(?θ2(𝑔θ1(x^\prime)), x)\\ \end{aligned}
?θ?=argminθ? dist(hθ2(gθ1(x′)),x)?
??其中
x
′
x^\prime
x′是被某種噪聲損壞的
x
x
x的副本,因此去噪自編碼器必須撤消這些損壞,而不是簡單地復制輸入,

2.2 對抗自編碼器(AAE)
??為了能夠方便地從某個已知的先驗分布中𝑝(𝒛)采樣隱藏變數𝒛,方便利用𝑝(𝒛)來重建輸 入,對抗自編碼器利用額外的判別器網路(Discriminator,簡稱 D網路)來判定降維的隱藏變數𝒛是否采樣自先驗分布𝑝(𝒛),判別器網路的輸出為一個屬于[0,1]區間的變數,表征隱藏向量是否采樣自先驗分布𝑝(𝒛):所有采樣自先驗分布𝑝(𝒛)的𝒛標注為真,采樣自編碼器的條件概率𝑞(𝒛|𝒙)的𝒛標注為假,通過這種方式訓練,除了可以重建樣本,還可以約束條件概率分布𝑞(𝒛|𝒙)逼近先驗分布𝑝(𝒛),

2.3 變分自編碼器(VAE)
2.3.1 原理
??自編碼器因不能隨意產生合理的潛在變數,從而導致它無法產生新的內容,因為潛在變數
z
z
z都是編碼器從原始圖片中產生的,為解決這一問題,研究人員對潛在空間
z
z
z(潛在變數對應的空間)增加一些約束,使
z
z
z滿足正態分布,由此就出現了VAE模型,VAE對編碼器添加約束,就是強迫它產生服從單位正態分布的潛在變數,正是這種約束,把VAE和自編碼器區分開來,
??從神經網路的角度來看,VAE 相對于自編碼器模型,同樣具有編碼器和解碼器兩個子網路,解碼器接受輸入
x
x
x,輸出為隱變數
z
z
z;解碼器負責將隱變數
z
z
z解碼為重建的
x
x
x,不同的是,VAE 模型對隱變數
z
z
z的分布有顯式地約束,希望隱變數
z
z
z符合預設的先驗分布P(
z
z
z),因此,在損失函式的設計上,除了原有的重建誤差項外,還添加了隱變數
z
z
z分布的約束項,也即我們優化目標希望
z
z
z的分布接近于正態分布,度量影像的相似度一般采用交叉熵(如nn.BCELoss),度量兩個分布 的相似度一般采用KL散度(Kullback-Leibler divergence),這兩個度量的和 構成了整個模型的損失函式,變分自編碼器的結構如下:

模塊1:把輸入樣本
x
x
x通過編碼器輸出兩個m維向量(mu、log_var),這兩個向量是潛在空間(假設滿足正態分布)的兩個引數(相當于均值和方差),
模塊2:從標準正態分布N(0,I)中采樣 一個ε,
模塊3:使得
z
z
z=mu+exp(log_var)*ε,
模塊4:
z
z
z通過解碼器生成一個樣本
x
ˉ
\bar{x}
xˉ,
損失函式的具體代碼如下,推導程序:https://arxiv.org/pdf/1606.05908.pdf
# 定義重構損失函式及KL散度
reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False)
kl_div = - 0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
#兩者相加得總損失 loss= reconst_loss+ kl_div
2.3.2 VAE圖片生成
??此次我們基于 VAE 模型實戰MNIST手寫數字圖片的重建與生成,輸入為 MNIST手寫數字圖片向量,經過 3 個全連接層后得到隱向量𝐳的均值與方差,分別用兩個輸出節點數為 20 的全連接層表示,FC2 的 20 個輸出節點表示 20 個特征分布的均值向量,FC3 的 20 個輸出節點表示 20 個特征分布的取log后的方差向量,采樣獲得長度為 20 的隱向量𝐳,并通過 FC4 和 FC5 重建出樣本圖片,
??VAE 作為生成模型,除了可以重建輸入樣本,還可以單獨使用解碼器生成樣本,通過從先驗分布𝑝(𝐳)中直接采樣獲得隱向量𝐳,經過解碼后可以產生生成的樣本,

??此程序的實作與圖片的重塑程序相差不大,主要差異在損失函式部分和潛在變數
z
z
z的采樣部分,
import cv2 as cv
import numpy as np
import os
import torch
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.datasets as datasets
from torch.utils.data import DataLoader, Dataset
from torchvision.utils import save_image
from torchvision import transforms
MAX_EPOCH = 100
lr_learning = 0.001
batch_size = 64
hidden_size = 400
z_size = 20
image_size = 784
os.makedirs('samples', exist_ok = True)
samples_dir = 'samples'
dataset=datasets.MNIST( root = 'data',
train = False,
download = True,
transform = transforms.ToTensor())
data_loader = DataLoader(dataset, shuffle = True, batch_size = batch_size, drop_last = True)
class VAE(nn.Module) :
def __init__(self, image_size, hidden_size, z_size):
super(VAE, self).__init__()
self.fc1 = nn.Linear(image_size, hidden_size)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, z_size)
self.fc3 = nn.Linear(hidden_size, z_size)
self.fc4 = nn.Linear(z_size, hidden_size)
self.relu2 = nn.ReLU()
self.fc5 = nn.Linear(hidden_size, image_size)
def Encoder(self, x):
h = self.relu1(self.fc1(x))
return self.fc2(h), self.fc3(h)
def Reparameterize(self, mu, Log_var):
std = torch.exp(Log_var / 2)
eps = torch.randn_like((std))
return mu + eps * std
def Decoder(self, z):
h = self.relu2(self.fc4(z))
return F.sigmoid(self.fc5(h))
def forward(self, x):
mu, Log_var = self.Encoder(x)
z = self.Reparameterize(mu, Log_var)
x_reconst = self.Decoder(z)
return x_reconst, mu, Log_var
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = VAE(image_size, hidden_size, z_size).to(device)
optimizer = optim.Adam(model.parameters(), lr = lr_learning)
for epoch in range(MAX_EPOCH) :
model.train()
for i, (x, _) in enumerate(data_loader) :
x = x.to(device)
optimizer.zero_grad()
x = x.view(-1, image_size)
x_reconst, mu, Log_var = model(x)
# 計算重構損失和KL散度
# 重構損失
reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False)
# KL散度
kl_div = - 0.5 * torch.sum(1 + Log_var - mu.pow(2) - Log_var.exp())
loss = reconst_loss + kl_div
loss.backward()
optimizer.step()
# if i % 10 == 0 :
# print(f'reconst_loss : {reconst_loss : 0.3f}, kl_div : {kl_div : 0.3f}')
with torch.no_grad() :
#圖片生成
z = torch.randn(batch_size, z_size).to(device)
out = model.Decoder(z).view(-1, 1, 28, 28)
save_image(out, os.path.join(samples_dir, f'sampled-{epoch + 1}.png'))
#圖片重塑
out, _, _ = model(x)
print(x.shape)
x_concat = torch.cat([x.view(-1, 1, 28, 28), out.view(-1, 1, 28, 28)], dim = 3)
save_image(x_concat, os.path.join(samples_dir, f'reconst-{epoch + 1}.png'))
img1 = cv.imread('samples/sampled-1.png')
img2 = cv.imread('samples/sampled-50.png')
img3 = cv.imread('samples/sampled-100.png')
img4 = cv.imread('samples/reconst-1.png')
img5 = cv.imread('samples/reconst-50.png')
img6 = cv.imread('samples/reconst-100.png')
images = [img1, img2, img3, img4, img5, img6]
xlabels = ['images sample epoch : 1', 'epoch : 50', 'epoch : 100', 'images reconst epoch : 1', 'epoch : 50', 'epoch : 100']
plt.figure(figsize = (15, 10))
for i in range(6) :
plt.subplot(2, 3, i + 1)
plt.imshow(images[i], 'gray')
plt.axis('off')
plt.title(xlabels[i])
plt.tight_layout()
plt.show()
??效果如下,其中重塑圖片中奇數列是原圖,偶數列為重塑影像,由潛在空間點
z
z
z生成的影像隨著epoch的增加是越來越清晰的,

參考
《TensorFlow深度學習》
《Python深度學習基于PyTorch》吳茂貴
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/420450.html
標籤:AI
