1.VAE和GAN
- 變分自編碼器(VAE,variatinal autoencoder)
- 生成式對抗網絡(GAN,generative adversarial network)
兩者不僅適用於圖像,還可以探索聲音、音樂甚至文本的潛在空間;
- VAE非常適合用於學習具有良好結構的潛在空間,其中特定方向表示數據中有意義的變化軸;
- GAN生成的圖像可能非常逼真,但它的潛在空間可能沒有良好結構,也沒有足夠的連續型。
自編碼,簡單來說就是把輸入數據進行一個壓縮和解壓縮的過程。 原來有很多 Feature,壓縮成幾個來代表原來的數據,解壓之后恢復成原來的維度,再和原數據進行比較。它是一種非監督算法,只需要輸入數據,解壓縮之后的結果與原數據本身進行比較。
在實踐中,這種經典的自編碼器不會得到特別有用或具有良好結構的潛在空間。它們也沒有對數據做多少壓縮。因此,它們已經基本上過時了(Keras 0.x版本還有AutoEncoder這個層,后來直接都刪了)。但是,VAE向自編碼器添加了一點統計魔法,迫使其學習連續的、高度結構化的潛在空間。這使得VAE已成為圖像生成的強大工具。變分編碼器和自動編碼器的區別就在於,傳統自動編碼器的隱變量z的分布是不知道的,因此我們無法采樣得到新的z,也就無法通過解碼器得到新的x。下面我們來變分,我們現在不要從x中直接得到z,而是得到z的均值和方差,然后再迫使它逼近正態分布的均值和方差,則網絡變成下面的樣子:
然而上面這個網絡最大的問題是,它是斷開的。前半截是從數據集估計z的分布,后半截是從一個z的樣本重構輸入。最關鍵的采樣這一步,恰好不是一個我們傳統意義上的操作。這個網絡沒法求導,因為梯度傳到f(z)以后沒辦法往前走了。為了使得整個網絡得以訓練,使用一種叫reparemerization的trick,使得網絡對均值和方差可導,把網絡連起來。這個trick的idea見下圖:
實際上,這是將原來的單輸入模型改為二輸入模型了。因為服從標准正態分布,所以它乘以估計的方差加上估計的均值,效果跟上上圖直接從高斯分布里抽樣本結果是一樣的。這樣,梯度就可以通上圖紅線的方向回傳,整個網絡就變的可訓練了。
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) --->
|
#潛在空間采樣的函數
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
|
|
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的輸出值),看看能不能生成一個相似的圖片 |
參考文獻:
【2】變分自編碼器(Variational Autoencoder, VAE)通俗教程
【5】vae 名人數據集的使用