深度有趣 | 06 變分自編碼器
變分自編碼器(Variational Autoencoder,VAE)是生成式模型(Generative Model)的一種,另一種常見的生成式模型是 生成式對抗網路 (Generative Adversarial Network,GAN)
這裡我們介紹下VAE的原理,並用Keras實現
原理
我們經常會有這樣的需求:根據很多個樣本,學會生成新的樣本
以MNIST為例,在看過幾千張手寫數字圖片之後,我們能進行模仿,並生成一些類似的圖片,這些圖片在原始資料中並不存在,有一些變化但是看起來相似
換言之,需要學會資料x的分佈,這樣,根據資料的分佈就能輕鬆地產生新樣本
P(X) 複製程式碼
但資料分佈的估計不是件容易的事情,尤其是當資料量不足的時候
可以使用一個隱變數z,由z經過一個複雜的對映得到x,並且假設z服從高斯分佈
x=f(z;\theta) 複製程式碼
因此只需要學習隱變數所服從高斯分佈的引數,以及對映函式,即可得到原始資料的分佈
為了學習隱變數所服從高斯分佈的引數,需要得到z足夠多的樣本
然而z的樣本並不能直接獲得,因此還需要一個對映函式(條件概率分佈),從已有的x樣本中得到對應的z樣本
z=Q(z|x) 複製程式碼
這看起來和自編碼器很相似,從資料本身,經編碼得到隱層表示,經解碼還原
但VAE和AE的區別如下:
- AE中隱層表示的分佈未知,而VAE中隱變數服從高斯分佈
- AE中學習的是encoder和decoder,VAE中還學習了隱變數的分佈,包括高斯分佈的均值和方差
- AE只能從一個x,得到對應的重構x
- VAE可以產生新的z,從而得到新的x,即生成新的樣本
損失函式
除了重構誤差,由於在VAE中我們假設隱變數z服從高斯分佈,因此encoder對應的條件概率分佈,應當和高斯分佈儘可能相似
可以用相對熵,又稱作KL散度(Kullback–Leibler Divergence),來衡量兩個分佈的差異,或者說距離,但相對熵是 非對稱 的
D(f\parallel g)=\int f(x)\log\frac{f(x)}{g(x)}dx 複製程式碼
實現
這裡以MNIST為例,學習隱變數z所服從高斯分佈的均值和方差兩個引數,從而可以從新的z生成原始資料中沒有的x
encoder和decoder各用兩層全連線層,簡單一些,主要為了說明VAE的實現
載入庫
# -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt from keras.layers import Input, Dense, Lambda from keras.models import Model from keras import backend as K from keras import objectives from keras.datasets import mnist 複製程式碼
定義一些常數
batch_size = 100 original_dim = 784 intermediate_dim = 256 latent_dim = 2 epochs = 50 複製程式碼
encoder部分,兩層全連線層,隱層表示包括均值和方差
x = Input(shape=(original_dim,)) h = Dense(intermediate_dim, activation='relu')(x) z_mean = Dense(latent_dim)(h) z_log_var = Dense(latent_dim)(h) 複製程式碼
Lambda
層不參與訓練,只參與計算,用於後面產生新的z
def sampling(args): z_mean, z_log_var = args epsilon = K.random_normal(shape=(batch_size, latent_dim), mean=0.) return z_mean + K.exp(z_log_var / 2) * epsilon z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var]) 複製程式碼
decoder部分,兩層全連線層, x_decoded_mean
為重構的輸出
decoder_h = Dense(intermediate_dim, activation='relu') decoder_mean = Dense(original_dim, activation='sigmoid') h_decoded = decoder_h(z) x_decoded_mean = decoder_mean(h_decoded) 複製程式碼
自定義總的損失函式並編譯模型
def vae_loss(x, x_decoded_mean): xent_loss = original_dim * objectives.binary_crossentropy(x, x_decoded_mean) kl_loss = -0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1) return xent_loss + kl_loss vae = Model(x, x_decoded_mean) vae.compile(optimizer='rmsprop', loss=vae_loss) 複製程式碼
載入資料並訓練,CPU訓練的速度還算能忍
(x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:]))) x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:]))) vae.fit(x_train, x_train, shuffle=True, epochs=epochs, batch_size=batch_size, validation_data=(x_test, x_test)) 複製程式碼
定義一個encoder,看看MNIST中的資料在隱層中變成了什麼樣子
encoder = Model(x, z_mean) x_test_encoded = encoder.predict(x_test, batch_size=batch_size) plt.figure(figsize=(6, 6)) plt.scatter(x_test_encoded[:, 0], x_test_encoded[:, 1], c=y_test) plt.colorbar() plt.show() 複製程式碼
結果如下,說明在二維的隱層中,不同的數字被很好地分開了

再定義一個生成器,從隱層到輸出,用於產生新的樣本
decoder_input = Input(shape=(latent_dim,)) _h_decoded = decoder_h(decoder_input) _x_decoded_mean = decoder_mean(_h_decoded) generator = Model(decoder_input, _x_decoded_mean) 複製程式碼
用網格化的方法產生一些二維資料,作為新的z輸入到生成器,並將生成的x展示出來
n = 20 digit_size = 28 figure = np.zeros((digit_size * n, digit_size * n)) grid_x = np.linspace(-4, 4, n) grid_y = np.linspace(-4, 4, n) for i, xi in enumerate(grid_x): for j, yi in enumerate(grid_y): z_sample = np.array([[yi, xi]]) x_decoded = generator.predict(z_sample) digit = x_decoded[0].reshape(digit_size, digit_size) figure[(n - i - 1) * digit_size: (n - i) * digit_size, j * digit_size: (j + 1) * digit_size] = digit plt.figure(figsize=(10, 10)) plt.imshow(figure) plt.show() 複製程式碼
結果如下,和之前看到的隱層圖是一致的,甚至能看到一些數字之間的過渡態

由於包含一些隨機因素,所以每次生成的結果會存在一些差異
如果將全連線層換成CNN,應該可以得到更好的表示結果
拓展
掌握以上內容後,用相同的方法,可以在FashionMNIST這個資料集上再跑一遍,資料集規模和MNIST完全相同

只需改動四行即可
from keras.datasets import fashion_mnist (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data() grid_x = np.linspace(-3, 3, n) grid_y = np.linspace(-3, 3, n) 複製程式碼
完整程式碼如下
# -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt from keras.layers import Input, Dense, Lambda from keras.models import Model from keras import backend as K from keras import objectives from keras.datasets import fashion_mnist batch_size = 100 original_dim = 784 intermediate_dim = 256 latent_dim = 2 epochs = 50 x = Input(shape=(original_dim,)) h = Dense(intermediate_dim, activation='relu')(x) z_mean = Dense(latent_dim)(h) z_log_var = Dense(latent_dim)(h) def sampling(args): z_mean, z_log_var = args epsilon = K.random_normal(shape=(batch_size, latent_dim), mean=0.) return z_mean + K.exp(z_log_var / 2) * epsilon z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var]) decoder_h = Dense(intermediate_dim, activation='relu') decoder_mean = Dense(original_dim, activation='sigmoid') h_decoded = decoder_h(z) x_decoded_mean = decoder_mean(h_decoded) def vae_loss(x, x_decoded_mean): xent_loss = original_dim * objectives.binary_crossentropy(x, x_decoded_mean) kl_loss = -0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1) return xent_loss + kl_loss vae = Model(x, x_decoded_mean) vae.compile(optimizer='rmsprop', loss=vae_loss) (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data() x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:]))) x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:]))) vae.fit(x_train, x_train, shuffle=True, epochs=epochs, batch_size=batch_size, validation_data=(x_test, x_test)) encoder = Model(x, z_mean) x_test_encoded = encoder.predict(x_test, batch_size=batch_size) plt.figure(figsize=(6, 6)) plt.scatter(x_test_encoded[:, 0], x_test_encoded[:, 1], c=y_test) plt.colorbar() plt.show() decoder_input = Input(shape=(latent_dim,)) _h_decoded = decoder_h(decoder_input) _x_decoded_mean = decoder_mean(_h_decoded) generator = Model(decoder_input, _x_decoded_mean) n = 20 digit_size = 28 figure = np.zeros((digit_size * n, digit_size * n)) grid_x = np.linspace(-3, 3, n) grid_y = np.linspace(-3, 3, n) for i, xi in enumerate(grid_x): for j, yi in enumerate(grid_y): z_sample = np.array([[yi, xi]]) x_decoded = generator.predict(z_sample) digit = x_decoded[0].reshape(digit_size, digit_size) figure[(n - i - 1) * digit_size: (n - i) * digit_size, j * digit_size: (j + 1) * digit_size] = digit plt.figure(figsize=(10, 10)) plt.imshow(figure) plt.show() 複製程式碼
我們來看一下隱層的表示,同樣起到了很好的分類效果

然後再來生成一些圖形,可以看到不同種類衣服之間的過渡
