4.keras實現-->生成式深度學習之用變分自編碼器VAE生成圖像(mnist數據集和名人頭像數據集)


1.VAE和GAN

  1. 變分自編碼器(VAE,variatinal autoencoder)   
  2. 生成式對抗網絡(GAN,generative adversarial network)

兩者不僅適用於圖像,還可以探索聲音、音樂甚至文本的潛在空間;

  1. VAE非常適合用於學習具有良好結構的潛在空間,其中特定方向表示數據中有意義的變化軸; 
  2. GAN生成的圖像可能非常逼真,但它的潛在空間可能沒有良好結構,也沒有足夠的連續型。

 

       自編碼,簡單來說就是把輸入數據進行一個壓縮和解壓縮的過程。 原來有很多 Feature,壓縮成幾個來代表原來的數據,解壓之后恢復成原來的維度,再和原數據進行比較。它是一種非監督算法,只需要輸入數據,解壓縮之后的結果與原數據本身進行比較。

  在實踐中,這種經典的自編碼器不會得到特別有用或具有良好結構的潛在空間。它們也沒有對數據做多少壓縮。因此,它們已經基本上過時了(Keras 0.x版本還有AutoEncoder這個層,后來直接都刪了)。但是,VAE向自編碼器添加了一點統計魔法,迫使其學習連續的、高度結構化的潛在空間。這使得VAE已成為圖像生成的強大工具。變分編碼器和自動編碼器的區別就在於,傳統自動編碼器的隱變量z的分布是不知道的,因此我們無法采樣得到新的z,也就無法通過解碼器得到新的x。下面我們來變分,我們現在不要從x中直接得到z,而是得到z的均值和方差,然后再迫使它逼近正態分布的均值和方差,則網絡變成下面的樣子:

       然而上面這個網絡最大的問題是,它是斷開的。前半截是從數據集估計z的分布,后半截是從一個z的樣本重構輸入。最關鍵的采樣這一步,恰好不是一個我們傳統意義上的操作。這個網絡沒法求導,因為梯度傳到f(z)以后沒辦法往前走了。為了使得整個網絡得以訓練,使用一種叫reparemerization的trick,使得網絡對均值和方差可導,把網絡連起來。這個trick的idea見下圖:

       實際上,這是將原來的單輸入模型改為二輸入模型了。因為\varepsilon 服從標准正態分布,所以它乘以估計的方差加上估計的均值,效果跟上上圖直接從高斯分布里抽樣本結果是一樣的。這樣,梯度就可以通上圖紅線的方向回傳,整個網絡就變的可訓練了。

 

 

VAE的工作原理:

(1)一個編碼器模塊將輸入樣本input_img轉換為表示潛在空間中的兩個參數z_mean和z_log_variance;

(2)我們假定潛在正態分布能夠生成輸入圖像,並從這個分布中隨機采樣一個點:z=z_mean + exp(z_log_variance)*epsilon,其中epsilon是取值很小的隨機張量;

(3)一個解碼器模塊將潛在空間的這個點映射回原始輸入圖像。

       因為epsilon是隨機的,所以這個過程可以確保,與input_img編碼的潛在位置(即z-mean)靠近的每個點都能被解碼為與input_img類似的圖像,從而迫使潛在空間能夠連續地有意義。潛在空間中任意兩個相鄰的點都會被解碼為高度相似的圖像。連續性以及潛在空間的低維度,將迫使潛在空間中的每個方向都表示數據中一個有意義的變化軸,這使得潛在空間具有非常良好的結構,因此非常適合通過概率向量來進行操作。

       VAE的參數通過兩個損失函數來進行訓練:一個是重構損失(reconstruction loss),它迫使解碼后的樣本匹配初始輸入;另一個是正則化損失(regularization loss),它有助於學習具有良好結構的潛在空間,並可以降低訓練數據上的過擬合。

 實現代碼如下:

編碼自編碼器是更現代和有趣的一種自動編碼器,它為碼字施加約束,使得編碼器學習到輸入數據的隱變量模型。
隱變量模型是連接顯變量集和隱變量集的統計模型,隱變量模型的假設是顯變量是由隱變量的狀態控制的,各個顯變量之間條件獨立。
也就是說,變分編碼器不再學習一個任意的函數,而是學習你的數據概率分布的一組參數。
通過在這個概率分布中采樣,你可以生成新的輸入數據,即變分編碼器是一個生成模型。
 
import keras
from keras import layers
from keras import backend as K
from keras.models import Model
from keras.layers import Input,Dense
import numpy as np

img_shape = (28,28,1)
latent_dim = 2 #潛在空間的維度:一個二維平面

input_img = keras.Input(shape=img_shape)

encoded = layers.Conv2D(32,3,padding='same',activation='relu')(input_img)
encoded = layers.Conv2D(64,3,padding='same',activation='relu',strides=(2,2))(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
shape_before_flattening = K.int_shape(encoded)
shape_before_flattening

  

 卷積層的輸入必須是3維的(長,寬,1或者3)  

keras不需要輸入batch的大小,fit時候再設置

shape_before_flattening

(None, 14, 14, 64)

 
encoded = layers.Flatten()(encoded)
encoded = layers.Dense(32,activation='relu')(encoded)

#輸入圖像最終被編碼為這兩個參數
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)

#編碼器 輸入圖片-->得到二維特征 encoder = Model(input_img,z_mean) 

  

    z_mean  ---> 

<tf.Tensor 'dense_5/BiasAdd:0' shape=(?, 2) dtype=float32>

 K.shape(z_mean) --->
<tf.Tensor 'Shape:0' shape=(2,) dtype=int32>

 

 
 
#潛在空間采樣的函數
def sampling(args):
    z_mean,z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0],latent_dim),mean=0.,stddev=1.)
    return z_mean + K.exp(z_log_var)*epsilon

z = layers.Lambda(sampling,output_shape=(latent_dim,))([z_mean,z_log_var])

 在keras中,任何對象都應該是一個層,如果代碼不是內置層的一部分,

我們應該將其包裝到一個Lambda層(或自定義層)中

Keras的Lambda層以一個張量函數為參數,對輸入的數據按照張量函數的要求做映射。

本質上就是Keras layer中.call()的快捷方式。先定義運算邏輯

K.int_shape(z) ---> (None,2)      None應該是batch_size

 
#VAE解碼器網絡,將潛在空間點映射為圖像
decoder_input = layers.Input(K.int_shape(z)[1:]) #將z調整為圖像大小,需要將z輸入到這里

#對輸入進行上采樣
decoded = layers.Dense(np.prod(shape_before_flattening[1:]),activation='relu')(decoder_input)

#將z轉換為特征圖,使其形狀與編碼器模型最后一個Flatten層之前的特征圖的形狀相同
decoded = layers.Reshape(shape_before_flattening[1:])(decoded)

#使用一個Conv2DTranspose層和一個Conv2D層,將z解碼為與原始輸入圖像具有相同尺寸的特征圖
decoded = layers.Conv2DTranspose(32,3,padding='same',activation='relu',strides=(2,2))(decoded)
decoder_output = layers.Conv2D(1,3,padding='same',activation='sigmoid')(decoded)

#將解碼器模型實例化,它將decoder_input轉換為解碼后的圖像 decoder = Model(decoder_input,decoder_output) 
#將這個實例應用於z,以得到解碼后的z
z_decoded = decoder(z)
 

 

 
#用於計算VAE損失的自定義層
class CustomVariationalLayer(keras.layers.Layer):
    def vae_loss(self,x,z_decoded):
        x = K.flatten(x)
        z_decoded = K.flatten(z_decoded)
        xent_loss = keras.metrics.binary_crossentropy(x,z_decoded) #正則化損失
        kl_loss = -5e-4 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var),axis=-1 ) #重構損失
        return K.mean(xent_loss + kl_loss)
    
    #編寫一個call方法,來實現自定義層
    def call(self,inputs):
        x = inputs[0]
        z_decoded = inputs[1]
        loss = self.vae_loss(x,z_decoded)
        self.add_loss(loss,inputs=inputs)
        return x #我們不適用這個輸出,但層必須要有返回值
    
#對輸入和解碼后的輸出調用自定義層,以得到最終的模型輸出
y = CustomVariationalLayer()([input_img,z_decoded])

 

正則化損失 + 重構損失

我們一般認為采樣函數的形式為loss(input,target),VAE的雙重損失不符合這種形式。

因此,損失的設置方法為:編寫一個自定義層,並在其內部使用內置的add_loss層方法

來創建一個你想要的損失

   

 

 
#訓練VAE
vae = Model(input_img,y)
vae.compile(optimizer='rmsprop',loss=None)
vae.summary()

  

 


 

 
 
from keras.datasets import mnist
(x_train,_),(x_test,y_test) = mnist.load_data()
x_train = x_train[:600]
x_test = x_test[:100]
x_train = x_train.astype('float32')/255.
print('x_train.shape',x_train.shape)
x_train =x_train.reshape(x_train.shape+(1,))
print('x_train.shape',x_train.shape)

x_test = x_test.astype('float32')/255.
print('x_test.shape',x_test.shape)
x_test = x_test.reshape(x_test.shape+(1,))
print('x_test.shape',x_test.shape)

  

 
x_train.shape (600, 28, 28)
x_train.shape (600, 28, 28, 1)
x_test.shape (100, 28, 28)
x_test.shape (100, 28, 28, 1)

 

 

 
vae.fit(x_train,None,
       shuffle=True,
       epochs=1,
       batch_size=100,
       validation_data = (x_test,None)
       )
 
 

一旦訓練好了這樣的模型,我們就可以使用decoder網絡將任意潛在空間向量

轉換為圖像

#從二維潛在空間中采樣一組點的網絡,並將其解碼為圖像
import matplotlib.pyplot as plt
from scipy.stats import norm

batch_size = 100
n = 15 #我們將顯示15*15的數字網格(共225個數字)
digit_size=28
figure = np.zeros((digit_size*n,digit_size*n))

#使用scipy的ppf函數對線性分割的坐標進行變換,以生存潛在變量z的值(因為潛在空間的先驗分布是高斯分布)
grid_x = norm.ppf(np.linspace(0.05,0.95,n))
grid_y = norm.ppf(np.linspace(0.05,0.95,n))
print(grid_x)
print(grid_y)

for i,yi in enumerate(grid_x):
    for j,xi in enumerate(grid_y):
        z_sample = np.array([[xi,yi]])
        z_sample = np.tile(z_sample,batch_size).reshape(batch_size,2)#將z多次重復,以構建一個完整的批量
        x_decoded = decoder.predict(z_sample,batch_size=batch_size)#將批量解碼為數字圖像
        digit = x_decoded[0].reshape(digit_size,digit_size)#將批量第一個數字形狀從28*28*1轉變為28*28 
        figure[i*digit_size:(i+1)*digit_size,j*digit_size:(j+1)*digit_size] = digit

plt.figure(figsize=(10,10))
plt.imshow(figure,cmap='Greys_r')
plt.show() 

  

 

 

因為訓練時候就用了600個數據,所以效果很差....電腦實在帶不動,┭┮﹏┭┮

以后有服務器再試試,7777777

小結:  用深度學習進行圖像生成,就是通過對潛在空間進行學習來實現的,這個潛在空間能夠捕捉到關於圖像數據集的統計信息。 通過對潛在空間中的點進行采樣和編碼,我們可以生成前所未見的圖像。

 網上的代碼大部分都是關於mnist數據集的,直接load_dataset就完事了,我找到了名人頭像的數據集celebrity_data,用這個數據集做vae更有趣一點。

import keras
from keras import layers
from keras import backend as K
from keras.models import Model
import numpy as np
import skimage
import glob
from skimage import io
import os
import imageio

  

 
  • skimage即是Scikit-Image。基於python腳本語言開發的數字圖片處理包,比如PIL,Pillow, opencv, scikit-image等。
  • PIL和Pillow只提供最基礎的數字圖像處理,功能有限;opencv實際上是一個c++庫,只是提供了python接口,更新速度非常慢。
  • scikit-image是基於scipy的一款圖像處理包,它將圖片作為numpy數組進行處理,正好與matlab一樣,
  • 因此,我們最終選擇scikit-image進行數字圖像處理。
 
train_imgs = glob.glob('./celebrity_data/train/*.jpg')
np.random.shuffle(train_imgs)
test_imgs = glob.glob('./celebrity_data/test/*.jpg')
np.random.shuffle(train_imgs)

nxf_image = io.imread(test_imgs[0])

 Image讀出來的是PIL的類型,而skimage.io讀出來的數據是numpy格式的

import Image as img
import os
from matplotlib import pyplot as plot
from skimage import io,transform
#Image和skimage讀圖片
img_file1 = img.open('./CXR_png/MCUCXR_0042_0.png')
img_file2 = io.imread('./CXR_png/MCUCXR_0042_0.png')

輸出可以看出Img讀圖片的大小是圖片的(width, height);而skimage的是(height,width, channel)

 
height,width = imageio.imread(train_imgs[0]).shape[:2]
center_height = int((height-width)/2)
img_xdim = 218
img_ydim = 178
z_dim = 512

  

 訓練集里面的圖片都是218*178*3的,訓練的時候我也沒有改大小,直接放進去訓練的
 
def imread(f):
    x = imageio.imread(f)
    x = x[center_height:center_height+width,:]
    x = skimage.transform.resize(x,(img_xdim,img_ydim),mode='constant')
    return x.astype(np.float32)/255 * 2 - 1

def train_data_generator(batch_size=32):
    X = []
    while True:
        np.random.shuffle(train_imgs)
        for f in train_imgs:
            X.append(imread(f))
            if len(X) == batch_size:
                X = np.array(X)
                yield X,None
                X = []

  

 

train_data_generator是訓練集圖片生成器,每次生成一個圖片

 
img_shape = (img_xdim,img_ydim,3)
latent_dim = 2 #潛在空間的維度:一個二維平面

input_img = keras.Input(shape=img_shape)

encoded = layers.Conv2D(32,3,padding='same',activation='relu')(input_img)
encoded = layers.Conv2D(64,3,padding='same',activation='relu',strides=(2,2))(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
encoded = layers.Conv2D(64,3,padding='same',activation='relu')(encoded)
shape_before_flattening = K.int_shape(encoded)

encoded = layers.Flatten()(encoded)
encoded = layers.Dense(32,activation='relu')(encoded)

#輸入圖像最終被編碼為這兩個參數
z_mean = layers.Dense(latent_dim)(encoded)
z_log_var = layers.Dense(latent_dim)(encoded)

encoder = Model(input_img,z_mean)

  

 這部分和上面基於minist數據集的encoder部分一樣
 
#將圖片轉換為二維向量

nxf_image = nxf_image.reshape((1,)+nxf_image.shape)
nxf_image_encoder = encoder.predict(nxf_image)
print('nxf_image_encoder',nxf_image_encoder)

  

 這里是我在測試encoder,隨機輸入一張圖片,輸出了二維的一個值,一個是均值,一個是方差,encoder沒有編譯,

也沒有fit,就相當於將多維圖片降維成二維的一組

 
# 潛在空間采樣的函數
def sampling(args):
    z_mean,z_log_var = args
    epsilon = K.random_normal(shape=(K.shape(z_mean)[0],latent_dim),mean=0.,stddev=1.)
    return z_mean + K.exp(z_log_var)*epsilon

z = layers.Lambda(sampling,output_shape=(latent_dim,))([z_mean,z_log_var])

#VAE解碼器網絡,將潛在空間點映射為圖像
decoder_input = layers.Input(K.int_shape(z)[1:]) #將z調整為圖像大小,需要將z輸入到這里

#對輸入進行上采樣
decoded = layers.Dense(np.prod(shape_before_flattening[1:]),activation='relu')(decoder_input)

#將z轉換為特征圖,使其形狀與編碼器模型最后一個Flatten層之前的特征圖的形狀相同
decoded = layers.Reshape(shape_before_flattening[1:])(decoded)

#使用一個Conv2DTranspose層和一個Conv2D層,將z解碼為與原始輸入圖像具有相同尺寸的特征圖
decoded = layers.Conv2DTranspose(32,3,padding='same',activation='relu',strides=(2,2))(decoded)
decoder_output = layers.Conv2D(3,3,padding='same',activation='sigmoid')(decoded)

#將解碼器模型實例化,它將decoder_input轉換為解碼后的圖像
decoder = Model(decoder_input,decoder_output)

#將這個實例應用於z,以得到解碼后的z
z_decoded = decoder(z)
# decoder.summary()

  

 這部分也是一樣的,解碼操作,隨機生成一個點(均值,方差)放入decoder中,看看生成的圖片能不能和原來的圖片一樣
 
# 用於計算VAE損失的自定義層
class CustomVariationalLayer (keras.layers.Layer):
    def vae_loss(self, x, z_decoded):
        x = K.flatten (x)
        z_decoded = K.flatten (z_decoded)
        xent_loss = keras.metrics.binary_crossentropy (x, z_decoded)  # 正則化損失
        kl_loss = -5e-4 * K.mean (1 + z_log_var - K.square (z_mean) - K.exp (z_log_var), axis=-1)  # 重構損失
        return K.mean (xent_loss + kl_loss)

    # 編寫一個call方法,來實現自定義層
    def call(self, inputs):
        x = inputs[0]
        z_decoded = inputs[1]
        loss = self.vae_loss (x, z_decoded)
        self.add_loss(loss, inputs=inputs)
        return x  # 我們不適用這個輸出,但層必須要有返回值

# 對輸入和解碼后的輸出調用自定義層,以得到最終的模型輸出
y = CustomVariationalLayer() ([input_img, z_decoded])

# 訓練VAE
vae = Model(input_img, y)
vae.compile(optimizer='rmsprop', loss=None)
# vae.summary()

  

 VAE的兩個損失,由於keras自帶的損失函數沒有同時有正則損失和重構損失,所以需要自定義一個損失層,

使用call函數來定義該損失層的功能

 
def sample(path):
    figure_nxf =  np.array(nxf_image_encoder)
    nxf_recon = decoder.predict(figure_nxf)[0]

    imageio.imwrite(path,nxf_recon)

from keras.callbacks import Callback

class Evaluate(Callback):
    def __init__(self):
        import os
        self.lowest = 1e10
        self.losses = []
        if not os.path.exists('samples'):
            os.mkdir('samples')

    def on_epoch_end(self, epoch, logs=None):
        path = 'samples/test_%s.png' % epoch
        sample(path)
        self.losses.append((epoch, logs['loss']))
        if logs['loss'] <= self.lowest:
            self.lowest = logs['loss']
            encoder.save_weights('./best_encoder.weights')

evaluator = Evaluate()
vae.fit_generator(train_data_generator(),
                  epochs=1,
                  steps_per_epoch=1,
                  callbacks=[evaluator])

  

 sample函數,我就隨機輸入兩個值(encoder的輸出值),看看能不能生成一個相似的圖片

 


參考文獻:

【1】Keras示例程序解析(4):變分編碼器VAE

【2】變分自編碼器(Variational Autoencoder, VAE)通俗教程

【3】變分自編碼器VAE:一步到位的聚類方案

【4】如何使用變分自編碼器VAE生成動漫人物形象

【5】vae 名人數據集的使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM