深度學習之Autoencoder及其在圖像去噪上的應用

導言:“自編碼器”(Autoencoder)是一種無監督的學習方法(更準確地用語應該是自監督),主要用於數據的降維或者特徵的抽取。在作用上有點類似於PCA、字典學習,或者壓縮感知。這裏的數據降維,也可以理解爲數據壓縮,總之就是給高維的原始數據產生一個低維的表達,並要求這個低維表達最大程度地保持原始數據中的重要信息。可以類比於PCA中提取的主成分。但Autoencoder的壓縮功能有三個特點:1)這種壓縮是有損的;2)這種壓縮或特徵提取是針對特定數據的;3)這種壓縮需要從數據中自動學習。另外,在幾乎所有使用術語“自編碼器”的語境下,數據壓縮(包括解壓縮)或特徵提取都是通過神經網絡實現的。

 

一、理論模型

更具體地說,我們有一些原始數據(通常是高維的),例如圖像。然後用一個神經網絡來對其進行壓縮。這樣的神經網絡也被稱爲“編碼器”(Encoder),原始數據經編碼器處理後得到一個壓縮的低維表達,如下圖所示。

與編碼器相對應的,還有“解碼器”(Decoder),如下圖所示。解碼器也是一個神經網絡,它把低維表達(也就是壓縮後的信息)還原成與原始信息維度相同的復原信息。因爲是有損壓縮,所以這裏的復原信息與原始信息是不會完全相同的。而我們所期望的是二者之間的差距儘可能地小。

最後,把編碼器和解碼器整合起來,就得到了如下圖所示的完整的Autoencoder框架。事實上,在神經網絡訓練階段,編碼器和解碼器是同時構建的。這一點,在後面的實例中,也會看到。

假設,我們面對的原始數據是MNIST數據庫中的一幅圖像。可以知道,原始數據的維度有784。經過自編碼器壓縮後,數據可以降維到幾十甚至更低。如下圖所示,經過還原得到的圖像仍然是可以辨識的。圖像中絕大部分主要信息都得以保留。

但正如開篇導言中所所說的,這種基於自編碼器的壓縮有若干特點,這與傳統意義上的壓縮是存在區別的:

  1. 自編碼器是針對特定數據的,這意味着它們只能壓縮那些同訓練集中數據類似的數據。舉例來說,傳統的JPEG圖像壓縮算法可以通用於任何圖像,而非特定類型的圖像。但如果我們用於訓練Autoencoder的數據集是MNIST,那麼該Autoencoder就只能用來對這種手寫數字產生低維表達。因此,在人臉圖像上訓練得到的自編碼器如果用於壓縮花草樹木類圖像,那麼其表現可想而知是相當差的。
  2. 自編碼器是有損的,這意味着與原始輸入相比,解壓縮後得到的輸出結果質量較原始數據相比會有所劣化(類似於MP3或JPEG壓縮)。這與無損壓縮方法(例如LZW編碼算法)不同。
  3. 自編碼器是從數據實例中自動學習的,這是一個有用的屬性:這意味着經特定數據集訓練而得的模型將在特定類型的輸入上表現良好。而且這樣過程並不需要複雜的人工干預,後面給出的基於MNIST的實例會演示這一點。

要構建自編碼器,您需要三件事:編碼器,解碼器,以及用於衡量原始數據與解壓縮後的恢復數據之間信息丟失量的距離函數(即“損失”函數)。編碼器和解碼器通常都是神經網絡,並且相對於距離函數是可微的,因此可以使用隨機梯度下降等優化算法來求得編碼/解碼器的參數,以最大程度地減少重建損失。

二、一個問題

我們反覆用到了壓縮這個詞,讀者可能會問Autoencoder擅長數據壓縮嗎?或者說,它能用來取代傳統的JPEG或者MP3算法嗎?通常情況下答案是否定的。例如,在圖片壓縮中,很難訓練出比JPEG等基本算法做得更好的自編碼器。而且,Autoencoder的用武之地,通常需要限定處理的圖像是非常特定的圖片類型(而且是JPEG效果顯然不佳時)。自編碼器需要針對特定數據的事實使它們通常對於現實世界中的數據壓縮問題不切實際,這也是我們反覆強調的,你只能將它們用於與訓練數據相似的數據上。如要使其更加通用,可能需要大量的訓練數據。當然,或許未來隨着技術的進步,這一點上可能會有所改善,當目前這仍然是它的一個侷限。

三、自編碼器的優點

自編碼器的兩個有趣的實際應用是數據去噪(我們將在本文後面的實例中加以演示)和用於數據可視化的降維。有了適當的尺寸和稀疏性約束,自編碼器可以學習到比PCA或其他基本技術更有趣的數據映射。特別是對於2D可視化,t-SNE可能是已知的最好的算法,但它通常需要相對低維的數據。因此,可視化高維數據中相似關係的一種好策略是開始使用自編碼器將數據壓縮到低維空間(例如32維),然後再使用t-SNE將壓縮後的數據映射到2D平面。

自編碼器不是真正的無監督學習技術(這將意味着完全不同的學習過程),儘管它在訓練時不需要“標籤”,但更準確地說,它們是一種自監督技術,是從輸入數據生成目標的有監督學習的特定實例。爲了讓自監督的模型學習到有意義的功能,你必須提出一個有意義的綜合目標和損失函數,這就是問題的所在:僅學習細緻地重建輸入可能不是正確的選擇(或者不是最好的選擇)。試想一下,在準確地對輸入進行重構中的同時,更能對我們感興趣的主要任務(例如分類或定位等)上實現更高的性能或表現,豈不更好。更進一步,即使損失前者(也就是重構表現欠佳),但提取後的特徵使得我們感興趣的主要任務(例如分類或定位等)有較大突破,這或許才真應該是我們所追求的。

四、在MNIST上基於Keras完成的實例

下面以MNIST數據集爲例,來演示Autoencoder的構建。在Keras上實現Autoencoder是非常容易的。首先,還是導入必要的包和數據集。

import keras
from keras.layers import Input, Dense
from keras.models import Model
from keras import regularizers

import matplotlib.pyplot as plt
%matplotlib inline

from keras.datasets import mnist
import numpy as np
(x_train, _), (x_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:])))

作爲一個簡單的開始,我們採用最簡單的單層全連接神經網絡來作爲編/解碼器。優化器選擇Adadelta,損失函數選擇per-pixel binary crossentropy loss。經過編碼器,原始784維的數據被壓縮成了一個32維的數據。

# this is the size of our encoded representations
encoding_dim = 32  # 32 floats -> compression of factor 24.5, assuming the input is 784 floats

# this is our input placeholder
input_img = Input(shape=(784,))
# "encoded" is the encoded representation of the input
encoded = Dense(encoding_dim, activation='relu')(input_img)
# "decoded" is the lossy reconstruction of the input
decoded = Dense(784, activation='sigmoid')(encoded)

# this model maps an input to its reconstruction
autoencoder = Model(input_img, decoded)

# this model maps an input to its encoded representation
encoder = Model(input_img, encoded)

# create a placeholder for an encoded (32-dimensional) input
encoded_input = Input(shape=(encoding_dim,))
# retrieve the last layer of the autoencoder model
decoder_layer = autoencoder.layers[-1]
# create the decoder model
decoder = Model(encoded_input, decoder_layer(encoded_input))

autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

訓練模型50個epochs。

autoencoder.fit(x_train, x_train,
                epochs=50,
                batch_size=256,
                shuffle=True,
                validation_data=(x_test, x_test))

最後,來展示一下原始圖像與對應的恢復圖像。

encoded_imgs = encoder.predict(x_test)
decoded_imgs = decoder.predict(encoded_imgs)

n = 10  # how many digits we will display
plt.figure(figsize=(20, 4))
for i in range(n):
    # display original
    ax = plt.subplot(2, n, i + 1)
    plt.imshow(x_test[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # display reconstruction
    ax = plt.subplot(2, n, i + 1 + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

如下圖所示,可見恢復後的圖像很好地保持了原圖中最重要的信息,同時,較原始圖像而言,恢復後的圖像質量有所劣化。

在前面的例子中,數據的低維表示僅僅限制於一個32維的向量,也就是說,我們只從向量維度這個角度對數據的低維表示做要求。這樣做的通常結果是,神經網絡的隱藏層僅僅學習到了一個與PCA相近似結果。除此之外,我們還可添加新的限制從而要求數據的低維表示滿足稀疏性的要求。在Keras中,我們對Dense層添加一個activity_regularizer即可實現上述要求。

# add a Dense layer with a L1 activity regularizer
encoded = Dense(encoding_dim, activation='relu',
                activity_regularizer=regularizers.l1(10e-8))(input_img)

注意,在Keras中的官方教程【2】中,上述代碼裏使用的參數是10e-5,但經測試這樣收斂的速度很慢,於是建議將其改爲10e-8。即使這樣,還是需要增加模型訓練的epochs至100個。我個人測試的結果顯示,訓練loss和驗證loss會分別收斂於0.101和0.996左右。下圖給出的對比效果,顯示添加了稀疏性限制之後的圖像復原結果與之前給出的效果相比並無明顯差異。但低維表示的稀疏性是有所增加的,這一點讀者可以自己驗證一下。

更進一步,還可以增加Dense層的層數,例如在構建編/解碼器時,用下面的代碼替換原程序中的對應代碼:

# this is the size of our encoded representations
encoding_dim = 32  # 32 floats -> compression of factor 24.5, assuming the input is 784 floats

# this is our input placeholder
input_img = Input(shape=(784,))
encoded = Dense(128, activation='relu')(input_img)
encoded = Dense(64, activation='relu')(encoded)
encoder_output = Dense(encoding_dim, activation='relu')(encoded)

decoded = Dense(64, activation='relu')(encoder_output)
decoded = Dense(128, activation='relu')(decoded)
decoded = Dense(784, activation='sigmoid')(decoded)

# this model maps an input to its reconstruction
autoencoder = Model(input_img, decoded)

# this model maps an input to its encoded representation
encoder = Model(input_img, encoder_output)

# autoencoder.summary()

# create a placeholder for an encoded (32-dimensional) input
encoded_input = Input(shape=(encoding_dim,))

# create the decoder model
deco = autoencoder.layers[-3](encoded_input)
deco = autoencoder.layers[-2](deco)
deco = autoencoder.layers[-1](deco)
# create the decoder model
decoder = Model(encoded_input, deco)

#decoder.summary()

autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

autoencoder.fit(x_train, x_train,
                epochs=100,
                batch_size=256,
                shuffle=True,
                validation_data=(x_test, x_test))

需要注意的是decoder的實現部分,原始代碼 works for single-layer because only last layer is decoder, for 3-layer encoders and decoders, you have to call all 3 layers for defining decoder。下面輸出的重構結果顯示增加網絡隱藏層層數使得復原圖像的質量有所提高。

注意到我們這裏處理的是圖像數據,前面一直使用的是全連接神經網絡(Fully-connected NN),但實踐中在處理圖像時採用更多的是卷積神經網絡(CNN)。於是下面,我們對之前的程序繼續進行改寫,轉而採用CNN來構建Autoencoder。首先,還是讀入必要的package以及數據集。由於接下來會採用CNN,所以這裏數據維度轉換的部分會跟之前代碼的處理方式不同。

from keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model
from keras import backend as K

import matplotlib.pyplot as plt
%matplotlib inline

from keras.datasets import mnist
import numpy as np

(x_train, _), (x_test, _) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))  # adapt this if using `channels_first` image data format
x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))  # adapt this if using `channels_first` image data format

下面是神經網絡結構構建的部分。注意在構建decoder的時候,我們使用了UpSampling2D,即上採樣函數。其中的參數(2,2)分別給出了行放大倍數(這裏取2的話代表原來的一行變成了兩行,就是一行那麼粗,變成了兩行那麼粗)和列放大倍數(這裏取2的話代表原來的一列變成了兩行,就是一列那麼粗,變成了兩列那麼粗)。所以,(2,2)其實就等於將原圖放大四倍(水平兩倍,垂直兩倍),例如32*32 變成 62*64的圖像。

input_img = Input(shape=(28, 28, 1))  # adapt this if using `channels_first` image data format

x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)

# at this point the representation is (4, 4, 8) i.e. 128-dimensional

x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)

# this model maps an input to its encoded representation
encoder = Model(input_img, encoded)

autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

train_history = autoencoder.fit(x_train, x_train,
                epochs=100,
                batch_size=128,
                shuffle=True,
                validation_data=(x_test, x_test))

在訓練100個epochs之後,我們來看看重構結果與原始圖像的對比效果。

decoded_imgs = autoencoder.predict(x_test)

n = 10
plt.figure(figsize=(20, 4))
for i in range(1, 10):
    # display original
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # display reconstruction
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

如下圖所示,可見採用CNN的autoencoder可以得到更高質量的復原圖像。

還可以可視化地展示訓練歷史中模型在訓練集與驗證集上loss的變化情況。

plt.plot(train_history.history['loss'])
plt.plot(train_history.history['val_loss'])
plt.title('Model Loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Validation'], loc='upper right')
plt.show()

如下圖所示,模型在訓練集和驗證集上最終的loss會收斂到0.1以下。

如果你有興趣,還可以把圖像的編碼表達(encoded representations)繪製出來。前面的代碼中已經指出,這個表達的維度是(4,4,8),也就是總計 128維。爲了把它以圖像的形式繪製出來,這裏將其轉化成 4x32的灰度圖像並做展示。

encoded_imgs = encoder.predict(x_test)

n = 10
plt.figure(figsize=(10, 4))
for i in range(n):
    ax = plt.subplot(1, n, i+1)
    plt.imshow(encoded_imgs[i].reshape(4, 4 * 8).T)
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

結果如下圖所示。

最後,我們來見識一下Autoencoder的神奇的降噪能力。首先讀入數據,並引入高斯噪聲。

from keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model
from keras import backend as K

import matplotlib.pyplot as plt
%matplotlib inline

from keras.datasets import mnist
import numpy as np

(x_train, _), (x_test, _) = mnist.load_data()

x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))  # adapt this if using `channels_first` image data format
x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))  # adapt this if using `channels_first` image data format

noise_factor = 0.5
x_train_noisy = x_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_train.shape) 
x_test_noisy = x_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_test.shape) 

x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)

可以把帶有噪聲的圖像繪製出來。

n = 10
plt.figure(figsize=(20, 2))
for i in range(n):
    ax = plt.subplot(1, n, i+1)
    plt.imshow(x_test_noisy[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

可見,我們加入的噪聲對圖像影響較大,圖像的質量已經嚴重劣化。

爲了獲得更好的效果,將之前的代碼略作修改。注意,我們仍然採用CNN版的Autoencoder,但每個卷積層裏設置更多的filters。

input_img = Input(shape=(28, 28, 1))  # adapt this if using `channels_first` image data format

x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)

# at this point the representation is (7, 7, 32)

x = Conv2D(32, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)

autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

train_history = autoencoder.fit(x_train_noisy, x_train,
                epochs=100,
                batch_size=128,
                shuffle=True,
                validation_data=(x_test_noisy, x_test))

由於Autoencoder是在沒有噪聲的MNIST數據集上訓練出來的,所以它會更多地學到“乾淨”的手寫數字圖像特徵。在利用已經訓練好的模型進行圖像重構時,輸入有噪聲的圖像,Autoencoder會把它們儘量還原成跟訓練數據集相近的樣子(因爲這是神經網絡已經掌握和熟悉的知識)。於是,便起到了去噪的作用。

decoded_imgs = autoencoder.predict(x_test_noisy)

n = 10
plt.figure(figsize=(20, 2))
for i in range(n):
    ax = plt.subplot(1, n, i+1)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

下圖是去噪後的效果圖。注意,這是傳統的去躁方法(高斯模糊、中值濾波、小波變換、JPEG去噪)都無法企及的效果!但是,但是,一定要注意,我們最開始討論過的Autoencoder的限制,即它是data-specific的,也就是隻能針對特定類型的數據。如果你用剛剛這個Autoencoder來一張含有噪聲的花花草草圖像做處理,是沒有任何意義的。

最後需要補充的是,Autoencoder還有很多新的發展,例如基於LSTM的自編碼器,有興趣的讀者不妨參考相關文獻以瞭解更多。

 

*本文中所有的完整代碼之Jupyter Notebook文件可以從【雲盤鏈接】 下載得到(提取碼: wvni)


 

參考文獻

【1】How to reduce image noises by autoencoder (文中部分示例圖片引用自該文)

【2】Building Autoencoders in Keras (文中部分示例圖片和代碼引用自該文,但示例中代碼執行有誤,筆者有修改)

【3】字典學習與圖像去噪實踐

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章