版權聲明:本文爲原創文章,未經博主允許不得用於商業用途。
圖片和算法來源於李宏毅老師的機器學習課程,李宏毅老師主頁鏈接,裏面有許多優質的課程課件和習題
2. VAE
2.1 原理
概述
VAE(Variational Autoencoder)即變分自動編碼器,在AutoEncoder的基礎上做了一些修改使其成爲生成模型。模型結構如下:
即編碼器會產生兩個相同維度的輸出向量,其中m可以理解爲均值,可以理解爲噪聲的方差爲了取正因此加上了指數,則是隨機產生的符合正態分佈的噪聲。因此decoder的輸入即爲:
新的編碼層輸出可以看作encoder實際上產生的是在編碼空間的一個小區域,大小由控制,這個空間內的編碼對應該樣本的概率符合高斯分佈。(高斯混合模型)
在訓練模型時,優化目標除了重構誤差外增加了一項:
可以理解爲前半部分保證了方差不會太小(取最小值時=1),後半部分則相當於是對編碼輸出進行了L2規範化防止過擬合。
Warning Of Math
如上文所說,假設樣本空間的概率分佈函數爲,編碼空間符合高斯分佈,則對於每一個編碼空間的採樣z都對應一個高斯函數,這些高斯函數共同組成了,如下圖:
即
則可以通過訓練一個神經網絡爲每一個z產生高斯函數的均值和方差,優化目標爲,即重構效果最好,可以對應decoder:
反過來,存在另一個函數產生x的編碼在編碼空間的高斯函數,對應encoder,同樣可以通過神經網絡訓練:
對於優化目標:
其中KL divergence表示兩個分佈的相似程度,當同分布時取最小值0。因此是優化下界,理想情況時輸入等於輸出,KL=0
對於前一項,,即使得encoder產生的z分佈和編碼空間的高斯分佈接近,並且:
即之前添加的優化目標。
對於後一項,,即在z由x產生的條件下對z產生的輸出概率求期望,可以理解爲編碼-解碼後的概率:
因此對後一項取最大值即使得編碼-解碼後的輸出和輸入儘可能相似,即降低重構誤差,顯然高斯函數取中軸得時候概率最大。
2.2 數據集
數據採用了這篇文章提供的動漫人臉圖片。
原文提供了五萬多張96*96的動漫人臉,我隨機選取了1044個圖片,並且縮放到了30*30大小。本來我使用了143種常用顏色對原圖進行了編碼,不過再次出現了之前Pokemon數據集編碼後由於編碼顏色不連續導致輸入維度過高的問題,我嘗試使用VAE進行訓練,結果收斂速度十分緩慢,並且效果也差強人意。
爲了加速訓練,我將1044張彩色圖片轉化爲255色彩空間種的灰度圖儲存在數組中。由於灰度值爲連續的,因此可以直接使用原圖作爲輸入,即輸入輸出維度爲30*30
2.3 構建網絡
我分別使用了全連接層和卷積層構造了兩種網絡,顯然卷積層的參數更少,但是在實際訓練時發現效果不如全連接層。
全連接層網絡
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
self.ZDim=10
self.encoder = nn.Sequential(
nn.Linear(30*30,512),
nn.ReLU(True),
nn.Linear(512,96),
nn.ReLU(True),
nn.Linear(96,25),
)
self.fcmu=nn.Linear(25,self.ZDim)
self.fcvar=nn.Linear(25,self.ZDim)
self.decoder = nn.Sequential(
nn.Linear(self.ZDim,25),
nn.ReLU(True),
nn.Linear(25,96),
nn.ReLU(True),
nn.Linear(96,512),
nn.ReLU(True),
nn.Linear(512,30*30),
)
def reparameterize(self, mu, logvar):
#z=exp(loavar)*eps+mu
eps = torch.randn(mu.size(0),mu.size(1))
z=mu+eps*torch.exp(logvar/2)
return z
def forward(self, x):
x=self.encoder(x)
logvar=self.fcvar(x)
mu=self.fcmu(x)
z=self.reparameterize(mu,logvar)
#return reconstructed sample, mu and logvar
return self.decoder(z),mu,logvar
#需要使用自定義的損失函數進行訓練
def loss_func(recon_x, x, mu, logvar):
BCE = criterion(recon_x.float(), x.float())
#Minimize{1+logvar-(mu)^2-exp(logvar)}
KLD=-0.5* torch.sum(1+logvar-mu.pow(2)-logvar.exp())
return BCE+KLD
使用卷積層的網絡
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
self.ZDim=10
self.encoder = nn.Sequential(
nn.Conv2d(1,10,5,stride=1,padding=0), #30*30=>10*26*26
nn.ReLU(True),
nn.MaxPool2d(2,2), #10*26*26=>19*13*13
nn.Conv2d(10,5,5,stride=2,padding=1), #10*13*13=>5*6*6
nn.ReLU(True),
nn.MaxPool2d(2,2), #3*4*4=>5*3*3
)
self.fcmu=nn.Linear(5*3*3,self.ZDim)
self.fcvar=nn.Linear(5*3*3,self.ZDim)
self.fc2=nn.Linear(self.ZDim,5*3*3)
#這裏偷懶了沒有用MaxUnpool2d
self.decodeConv = nn.Sequential(
nn.ConvTranspose2d(5,10,4,stride=2,padding=0), #5*3*3=>5*6*6
nn.ReLU(),
nn.ConvTranspose2d(10,3,6,stride=3,padding=0), #5*6*6=>10*13*13
nn.ReLU(),
nn.ConvTranspose2d(3,1,4,stride=1,padding=0), #10*26*26=>1*30*30
nn.ReLU(),
)
def decoder(self,x):
x=self.fc2(x)
x=x.view(x.shape[0],5,3,3)
x=self.decodeConv(x)
return x
def reparameterize(self, mu, logvar):
#z=exp(loavar)*eps+mu
eps = torch.randn(mu.size(0),mu.size(1))
z=mu+eps*torch.exp(logvar/2)
return z
def forward(self, x):
x=self.encoder(x)
x=x.view(x.shape[0],45)
logvar=self.fcvar(x)
mu=self.fcmu(x)
z=self.reparameterize(mu,logvar)
#return reconstructed sample, mu and logvar
return self.decoder(z),mu,logvar
2.4 訓練結果
相比於GAN,VAE得生成結果會很模糊,並且可能會出現比較明顯的噪點。我在實際訓練時發現Conv生成得圖片更模糊,可能是因爲沒有加入反向池化。
編碼-解碼
上圖是全連接的VAE對18張隨機的人臉編碼-解碼後的結果,可以看出對顏色的還原比較精準,但是對人臉傾斜角度的還原不是很好。
這張則是加入卷積層後的結果,有的人臉(最後一行第三張)已經模糊到無法辨認了。
生成
上圖是隨機選取兩個樣本編碼及其高維線段中間的七個等分點產生的臉,兩端是輸入圖片,可以看到人臉的髮色逐漸變淺,人臉的朝向也從向右逐漸轉爲向左。
上圖則是在編碼空間使用numpy.random.normal隨機產生的一些人臉,可以看到7.png出現了之前所說的明顯噪點,而21則出現了較明顯的問題。
編碼空間
我試圖解讀每一個維度的作用,所以按照編碼空間大小產生了十個序列的圖片,分別對應十個維度的變化,如下圖:
事實證明這種非條件的學習不能韓浩的解讀大部分維度。
-
第一、二、六、八、十維看起來就對生成結果沒有什麼影響。
-
第三維度增加時頭髮變短
-
第四維增加時陰影從左邊跑到右邊,不過不知道這是什麼
-
第九維增加時頭髮顏色變淺
-
大部分維度都對臉的朝向有影響
2.5 進一步訓練
看到訓練結果以後覺得黑白圖片不是很好看,並且很糊,於是就想能不能補救一下,所以我又建立了一個網絡將這些訓練成的灰度圖轉換回彩色圖片,並且試圖去除一些噪點。
2.5.1 網絡結構
其實這個網絡就是隨手搭建的,所以沒有什麼技巧可言。
大概思路就是先用反捲積映射到高維空間,再降維變成三個通道圖片。訓練的時候爲了只完成着色,防止直接修改原圖增加了和原圖對比的MSELoss,而且在輸入時增加了高斯噪聲防止過擬合。代碼如下:
class StackNN(nn.Module):
def __init__(self):
super(StackNN, self).__init__()
self.remap = nn.Sequential(
nn.ConvTranspose2d(1,10,5,stride=2,padding=0),
nn.ReLU(),
nn.Conv2d(10,5,5),#3*59*59
nn.ReLU(),
)
self.fc = nn.Sequential(
nn.Linear(5*59*59,5*30*30),
nn.Linear(5*30*30,3*30*30),
)
def forward(self, x):
#加入高斯噪聲
eps = torch.randn(x.size(0),x.size(1),x.size(2),x.size(3))
x=x+eps
x=self.remap(x)
x=x.view(x.shape[0],5*59*59)
x=self.fc(x)
x=x.view(x.shape[0],3,30,30)
return x
def loss_func(recon_x, x, target):
colorloss = criterion(recon_x.float(), target.float())
gray = (recon_x[:,0,:,:]*30+recon_x[:,1,:,:]*59+recon_x[:,2,:,:]*11)/100
gray = gray.view(gray.shape[0],1,30,30)
originloss = criterion(gray.float(),x.float()/255)
#這一步是將顏色映射到[0,1]實數域,爲兩部分誤差賦相同的權重
return colorloss+originloss
2.5.2 訓練結果
使用最基本的MSELoss和Adam更新梯度,我迭代了40個epoch之後效果已經出來了,對之前隨機產生的數據進行着色結果如下:
可以看到臉部、眼睛和頭髮都有很好的上色,不過由於VAE產生的結果背景都很模糊所以着色後依然很模糊。
結合之前對編碼空間的分析重新着色後:
源碼見github