想把馬變成斑馬嗎?製作DIY動漫人物或名人?生成對抗網絡(GAN)就是一個幫你實現的新朋友。"GAN是過去10年機器學習中最有趣的想法。" - Facebook AI人工智慧研究總監Yann LeCun
本文的大部分內容將涉及編碼GAN以及對GAN的一些更高級實現的廣泛介紹。
GAN的簡短回顧
本文將講述GAN如何使用pandas實例工作。
最近引入了生成對抗網作為訓練生成模型的新方法,即創建能夠生成數據的模型。它們由兩個"對抗"模式:生成模型G獲得數據和判別模型D來估計訓練數據提供的樣本的準確性。G和D可能是一個非線性映射函數,如多層感知。
在生成對抗網絡(GAN)中,我們有兩個神經網絡在零和遊戲中相互對抗,其中第一個網絡,即生成器,其任務是欺騙第二個網絡,即鑑別器。生成器創建"假的"數據,在這種情況下,是大熊貓的圖像,試圖欺騙鑑別器將圖像分類為真正的熊貓。我們可以逐步迭代地改進這兩個網絡,以便生成逼真的圖像以及執行其他應用程式。
最初,圖像可能是相當明顯的偽造,但隨著訓練變得更好,區分真實和虛假圖像變得更加困難,即使對於人類也是如此!
這兩個網絡可以被認為是黑盒子,代表一些任意的複雜的特徵,應用於噪聲或真實數據。生成器的輸入是一些隨機噪聲,產生偽圖像,鑑別器的輸入既是偽樣本,也是來自真實數據集的樣本。然後,鑑別器做出關於所提供的圖像z是真實圖像D(z)= 1還是假圖像D(z)= 0的二元判定。
為了訓練兩個網絡,我們必須具有丟失特徵,並且每個網絡的丟失特徵取決於第二網絡。為了訓練網絡,我們進行反向傳播,同時凍結其他網絡的神經元權重。
重要的是要注意,通常,鑑別器和生成器網絡可以是任何形式的映射函數,例如支持向量機。正是GAN的這種概括通常被稱為圖靈學習。然而,在實踐中,神經網絡是最常見的,因為它們是任意非線性函數的廣義函數逼近器。
本文現在將討論GAN的一些最酷的應用程式,然後再討論一些更高級的主題,以及GAN的代碼演練,旨在生成名人面孔和動漫角色。
GAN應用程式
在本節中,本文將簡要介紹一下在數據科學研究過程中發現的一些最有趣的GAN應用。最常見的主題是:
(有條件)合成 - 包括字體生成,Text2Image以及3D對象生成。數據擴充 - 旨在減少對標記數據的需求(GAN僅用作增強其他模型的訓練過程的工具)。風格遷移和操縱 - 面部老化,繪畫,姿勢估計和操作,修復和混合。信號超解析度 -人為地提高圖像的解析度。條件合成
條件合成是相當不可思議的。可以說,條件合成最迷人的應用是Image2Text和Text2Image,它能夠將圖片翻譯成文字,反之亦然。
這種應用影響深遠,如果不僅僅是用於分析醫學圖像來描述圖像的特徵,從而消除了醫生對圖像的主觀分析。
這也是另一種方式使圖像可以純粹從單詞生成。以下是用於執行此文本到圖像條件合成的多條件GAN(MC-GAN)的示例:
mc-gan的實現,用於將單詞翻譯成圖像
數據擴充
GAN像在查看VAE時一樣學習數據生成分布。因此,我們可以從我們的發生器中採樣並生成其他樣本,我們可以使用這些樣本來增強我們的訓練集。因此,GAN提供了另外的方法來執行數據增加(除了旋轉和扭曲圖像之外)。
風格遷移和操縱
風格遷移涉及將一個圖像的"樣式"遷移到另一個圖像上。這與神經風格遷移非常相似。
使用GAN進行樣式傳輸的示例
這對於背景場景非常有效,並且類似於圖像過濾,除了我們可以操縱實際圖像的各個方面(比較上面圖像中的雲以用於輸入和輸出)。
GAN如何在動物或水果等其他物體上表現?
看起來相當不錯!如果我對GAN一無所知,我可能會認為馬的形象實際上是風格遷移後的斑馬。
我們還可以改變風景來操縱季節,這對於視頻遊戲和虛擬實境模擬器等事物來說可能是有用的操作。
我們還可以使用GAN更改景深。
我們還可以操縱繪圖,將它們變成真實的物體,相對容易。
自動駕駛汽車以與下圖相似的視角看世界,這樣可以以更加對比的方式觀察物體。
我們甚至可以進行風格轉換來渲染圖像,如俠盜獵車手的環境。
我們也可以以同樣的方式將白天遷移到夜晚。
關於風格遷移和圖像處理就足夠了。這有很多很好的應用,但我們已經看到了該技術的幾種惡意使用,人們冒充政治人物並製造虛假的電話交談,電子郵件等。
這實際上是一個問題,美國軍方正在開發一個新的取證領域來研究視頻和媒體的類似例子,以確定它們是否是由GAN產生的。
圖像超解析度
圖像超解析度(SR)是指從低解析度(LR)圖像中恢復高解析度(HR)圖像的過程,是計算機視覺和圖像處理中一類重要的圖像處理技術。
通常,該問題非常具有挑戰性並且有固有地不適合的地方,因為總是存在對應於單個LR圖像的多個HR圖像。
已經應用各種深度學習方法來處理SR任務,範圍從早期基於卷積神經網絡(CNN)的方法(例如,SRCNN)到最近使用GAN的有希望的SR方法(例如,SRGAN)。通常,使用深度學習技術的SR算法族在以下主要方面彼此不同:不同類型的網絡架構,不同類型的損失函數,不同類型的學習原理和策略等。實現這一目的的過程實際上非常複雜,所以本文中不會詳細介紹。
GAN問題
以前我們討論了GAN的一些最基本的問題,主要是需要大量的計算能力,大圖像訓練的難度,靈敏度以及模態崩潰。本文想重申這些問題,因為訓練GAN非常困難且耗時。
振蕩
當發生器和鑑別器共同尋找平衡時,可能發生振蕩,但模型更新是獨立的。沒有理論上的收斂性保證,事實上,結果可能會發生變化。
對此的解決方案是廣泛的超參數搜索,有時可能需要手動幹預。一個廣泛的超參數搜索已經需要10個小時才能運行的東西並不是本文推薦的。
消失的梯度
鑑別器可能變得太強而不能為發生器提供信號。這是什麼意思?如果生成器太快太好,生成器可以學會一致地欺騙鑑別器,並且不再需要學習任何東西。
解決方案不是預先訓練鑑別器,或者與發生器相比降低其學習率。也可以在每次迭代時更改生成器/鑑別器的更新次數。
很容易看出GAN何時融合,因為兩個網絡的穩定性將發生在中間地帶的某個地方。
模態崩潰
生成器可以摺疊,以便始終生成相同的樣本。當生成器被約束到小子空間並因此開始生成低分集的樣本時,這可能發生。
以上生成的圖像中的五個看起來相同,並且其他幾個看起來成對出現。
對此的解決方案是通過小批量區分(將整個批次呈現給鑑別器以供審查)或通過特徵匹配(即為低多樣性添加生成器懲罰)或使用多個GAN來鼓勵多樣性。
評估指標
GAN仍然在非常定性的基礎上進行評估 - 這個圖像看起來很好嗎?定義有些客觀的適當指標令人驚訝地具有挑戰性。"好"的生成器看起來如何?
對此沒有明確的解決方案,它仍然是一個活躍的研究領域,並且具有特定領域的特定領域。強分類模型通常用於判斷生成樣本的質量。使用的兩個常見分數是初始分數和TSTR分數(Train on Synthetic,Test on Real)。
其他類型的GAN
還有許多其他類型的GAN已經出現,用於解決特定於域的問題以及不同類型的數據(例如,時間序列,圖像或普通的csv樣式數據)。
本文將在本節討論的GAN類型是:
Wasserstein GANCycleGAN條件GANWasserstein GAN
在我看來,這是最重要的GAN類型,所以要注意!
使用標準的GAN配方,我們已經觀察到訓練非常不穩定。鑑別器通常改進太快以至於生成器不能趕上,這就是為什麼我們需要調節學習速率或在兩個網絡之一上執行多個時期。為了獲得合適的輸出,我們需要仔細平衡,即使這樣,模態崩潰也是非常頻繁的。
一般而言,生成模型試圖最小化實際和學習分布之間的距離。Wasserstein(也稱為EM,Earth-Mover)距離,非正式地指的是當分布被解釋為在區域D上堆積一定量汙垢的兩種不同方式.Wasserstein距離是轉動一堆的最小成本進入另一個; 假設成本是移動的汙垢量乘以它移動的距離。
不幸的是,在這種情況下,精確的計算是難以處理的。但是,我們可以使用CNN來估算Wasserstein距離。在這裡,我們重用了鑑別器,其輸出現在是無界的。我們定義了與Wasserstein損失相對應的自定義損失函數:
我們可以儘可能地對一種類型進行預測,對儘可能小的其他類型進行預測。
Wasserstein論文的作者聲稱:
在訓練期間更高的穩定性,更少需要仔細平衡生成器和鑑別器。有意義的損失度量,與樣本質量很好地相關。模態崩潰很少見。在Keras實施Wasserstein GAN的提示。
使鑑別器輸出無限制,即應用線性激活。使用小權重進行初始化,以便從一開始就不會遇到剪切問題。請記住運行足夠的鑑別器更新。這在WGAN設置中至關重要。你可以使用Wasserstein代理損失實施。通過實現你自己的Keras約束來剪輯鑑別器權重。
這是一個複雜的主題,實施Wasserstein GAN並不是一項簡單的任務。
CycleGAN
還記得我們看到一匹馬與斑馬交換的第一張圖片嗎?這是一個CycleGAN。CycleGAN將樣式傳輸到圖像。
舉一個例子,想像一下著名的,如金門大橋的照片,然後從另一幅圖像中提取風格,這可能是一幅著名的畫作,並以所述著名畫作的風格重新繪製橋梁的圖片。
將GAN應用於這些類型的問題相對簡單,它本質上是圖像重建。我們使用第一個網絡G將圖像x轉換為y。我們用另一個深度網絡F來反轉該過程以重建圖像。然後,我們使用均方誤差MSE來指導G和F的訓練。
這裡的不同之處在於我們實際上並不關心重建圖像,我們試圖混合兩個圖像的樣式。在GAN實現中,將鑑別器D添加到現有設計中以指導發電機網絡更好地執行。D充當訓練樣本和生成的圖像之間的批評者。通過這種批評,我們使用反向傳播來修改生成器以產生圖像,以解決由鑑別器識別的缺點。在這個問題中,我們引入了一個鑑別器D,以確保Y類似於梵谷的繪畫。
CycleGAN將圖片從一個域傳輸到另一個域。在真實圖像和梵谷繪畫之間轉換圖片。我們建立了三個網絡。
生成器G將真實圖像轉換為梵谷風格圖片。生成器F將梵谷風格的圖片轉換為真實圖像。鑑別器D用於識別真實或生成的梵谷圖片。對於反方向,我們只是反轉數據流並構建一個額外的鑑別器來識別真實圖像。
創建斑馬/馬圖像的此實現的示例如下所示。
有條件的GAN
與在VAE中一樣,GAN可以簡單地用於生成特定的數據模式。
如果生成器和鑑別器都以某些額外信息c為條件,我們可以將GAN的生成模型擴展到條件模型。該c可以是任何類型的輔助信息,例如類標籤或來自其他模態的數據。我們可以通過將c作為附加輸入層饋入鑑別器和發生器來執行調節。
在生成器中,先前輸入噪聲p(z)和c在聯合隱藏表示中組合,並且對抗訓練框架允許在如何組成該隱藏表示時具有相當大的靈活性。在鑑別器中,x和c表示為輸入和判別函數。
GAN故障排除
簡要概述我們到目前為止討論過的所有故障排除方法。
模型。確保正確定義模型。你可以通過訓練香草圖像分類任務來單獨調試鑑別器。數據。將輸入正確歸一化為[-1,1]。在這種情況下,確保使用tanh作為生成器的最終激活。噪音。嘗試從正態分布(不均勻)中採樣噪聲向量。規範化。儘可能應用BatchNorm,並在單獨的小批量中發送真實和假冒樣本。激活。使用LeakyRelu而不是Relu。平滑。應用標籤平滑以避免在更新鑑別器時過度自信,即將實際圖像的目標設置為小於1。診斷。不斷監測梯度的大小。消失的梯度。如果鑑別器變得太強(鑑別器丟失= 0),嘗試降低其學習速率或更頻繁地更新發生器。現在讓我們進入有趣的部分,實際上構建一個GAN。
構建圖像GAN
正如我們已經多次討論過的那樣,訓練GAN可能會令人沮喪且耗費時間。在代碼示例中,如果不仔細調整參數,則不會超過圖像生成的這個級別(見下文):
網絡拍攝圖像[H,W,C]並輸出[M]的矢量,分類(分類)或單個分數量化照片寫實。可以是任何圖像分類網絡,例如ResNet或DenseNet。我們使用簡約的自定義架構。
採用噪聲矢量[N]並輸出[H,W,C]的圖像。網絡必須執行綜合。同樣,我們使用非常簡約的自定義架構。
在Keras中正確定義模型非常重要,這樣可以在正確的時間固定各個模型的權重。
定義鑑別器模型,並進行編譯。定義生成器模型,無需編譯。定義由這兩者組成的整體模型,在編譯之前將鑑別器設置為不可訓練:
在最簡單的形式中,這就是訓練GAN所需要做的一切
訓練循環必須手動執行:
從訓練集中選擇R實像。通過對大小為N的隨機向量進行採樣,並使用生成器從它們預測圖像來生成F偽圖像。使用train_on_batch訓練鑑別器:分別為R真實圖像批次和F偽圖像調用它,地面實況分別為1和0。採樣大小為N的新隨機向量。使用train_on_batch訓練新模型的完整模型,目標為1.這將更新生成器。編碼實施
現在,我們將使用著名的CelebA數據集上的Keras以代碼格式進行上述簡約實現。如果你對代碼的結構方式感到困惑,可能需要參考上述過程。
請注意,在這段代碼中我沒有以任何方式優化它,我忽略了幾條經驗法則。我將在第3部分中更多地討論這些內容,這將涉及對GAN代碼的更深入的討論。
輸入
首先,我們導入必要的包。
import kerasfrom keras.layers import * from keras.datasets import cifar10 import glob, cv2, os import numpy as np import matplotlib.pyplot as plt %matplotlib inline from IPython.display import clear_output
全局參數
最好在開始時指定這些參數以避免以後混淆,因為這些網絡可能會變得相當混亂和參與。
SPATIAL_DIM = 64 # Spatial dimensions of the images.LATENT_DIM = 100 # Dimensionality of the noise vector. BATCH_SIZE = 32 # Batchsize to use for training. DISC_UPDATES = 1 # Number of discriminator updates per training iteration. GEN_UPDATES = 1 # Nmber of generator updates per training iteration. FILTER_SIZE = 5 # Filter size to be applied throughout all convolutional layers. NUM_LOAD = 10000 # Number of images to load from CelebA. Fit also according to the available memory on your machine. NET_CAPACITY = 16 # General factor to globally change the number of convolutional filters. PROGRESS_INTERVAL = 80 # Number of iterations after which current samples will be plotted. ROOT_DIR = 'visualization' # Directory where generated samples should be saved to. if not os.path.isdir(ROOT_DIR): os.mkdir(ROOT_DIR)
準備數據
我們現在進行一些圖像預處理以標準化圖像,並繪製圖像以確保我們的實現正常工作。
def plot_image(x):plt.imshow(x * 0.5 + 0.5)X = [] # Reference to CelebA dataset here. I recommend downloading from the Harvard 2019 ComputeFest GitHub page (there is also some good coding tutorials here)faces = glob.glob('../Harvard/ComputeFest 2019/celeba/img_align_celeba/*.jpg') for i, f in enumerate(faces): img = cv2.imread(f) img = cv2.resize(img, (SPATIAL_DIM, SPATIAL_DIM)) img = np.flip(img, axis=2) img = img.astype(np.float32) / 127.5 - 1.0 X.append(img) if i >= NUM_LOAD - 1: break X = np.array(X) plot_image(X[4]) X.shape, X.min(), X.max())
定義架構
Keras格式的架構非常簡單。最好用塊編寫代碼以保持儘可能簡單。
首先,我們添加編碼器塊部分。請注意,我們使用"相同"填充,以便輸入和輸出尺寸相同,以及批量標準化和洩漏ReLU。Stride主要是可選的,因為洩漏的ReLU參數的大小也是如此。我放在這裡的值沒有優化,但確實給了我一個合理的結果。
def add_encoder_block(x, filters, filter_size):x = Conv2D(filters, filter_size, padding='same')(x) x = BatchNormalization()(x) x = Conv2D(filters, filter_size, padding='same', strides=2)(x) x = BatchNormalization()(x) x = LeakyReLU(0.3)(x) return x
接下來是鑑別器本身 - 注意我們如何回收編碼器塊段並逐漸增加濾波器大小以解決我們之前討論的用於(大)圖像訓練的問題(對所有圖像執行此操作的最佳實踐)。
def build_discriminator(start_filters, spatial_dim, filter_size):inp = Input(shape=(spatial_dim, spatial_dim, 3)) # Encoding blocks downsample the image. x = add_encoder_block(inp, start_filters, filter_size) x = add_encoder_block(x, start_filters * 2, filter_size) x = add_encoder_block(x, start_filters * 4, filter_size) x = add_encoder_block(x, start_filters * 8, filter_size) x = GlobalAveragePooling2D()(x) x = Dense(1, activation='sigmoid')(x) return keras.Model(inputs=inp, outputs=x)現在為解碼器塊段。這次我們正在執行卷積層的反面,即反卷積。請注意,為了便於實現,步幅和填充是相同的,我們再次使用批量標準化和洩漏ReLU。
def add_decoder_block(x, filters, filter_size):x = Deconvolution2D(filters, filter_size, strides=2, padding='same')(x) x = BatchNormalization()(x) x = LeakyReLU(0.3)(x) return x
現在構建生成器,注意這次我們使用解碼器塊並逐漸減小過濾器大小。
def build_generator(start_filters, filter_size, latent_dim):inp = Input(shape=(latent_dim,)) # Projection. x = Dense(4 * 4 * (start_filters * 8), input_dim=latent_dim)(inp) x = BatchNormalization()(x) x = Reshape(target_shape=(4, 4, start_filters * 8))(x) # Decoding blocks upsample the image. x = add_decoder_block(x, start_filters * 4, filter_size) x = add_decoder_block(x, start_filters * 2, filter_size) x = add_decoder_block(x, start_filters, filter_size) x = add_decoder_block(x, start_filters, filter_size) x = Conv2D(3, kernel_size=5, padding='same', activation='tanh')(x) return keras.Model(inputs=inp, outputs=x)
訓練
現在我們已經建立了網絡架構,我們可以概述訓練過程,這可能是人們容易混淆的地方。我想這可能是因為在更多特徵中的函數內部有函數。
def construct_models(verbose=False):# 1. Build discriminator. discriminator = build_discriminator(NET_CAPACITY, SPATIAL_DIM, FILTER_SIZE) discriminator.compile(loss='binary_crossentropy', optimizer=keras.optimizers.Adam(lr=0.0002), metrics=['mae']) # 2. Build generator. generator = build_generator(NET_CAPACITY, FILTER_SIZE, LATENT_DIM) # 3. Build full GAN setup by stacking generator and discriminator. gan = keras.Sequential() gan.add(generator) gan.add(discriminator) discriminator.trainable = False # Fix the discriminator part in the full setup. gan.compile(loss='binary_crossentropy', optimizer=keras.optimizers.Adam(lr=0.0002), metrics=['mae']) if verbose: # Print model summaries for debugging purposes. generator.summary() discriminator.summary() gan.summary() return generator, discriminator, gan
基本上,我們上面所做的是設計一個基於我們的全局參數創建GAN模型的函數。請注意,我們編譯鑑別器,但不編譯生成器,但我們最後編譯GAN作為一個整體。
另外,請注意我們已經將鑑別器設置為不可訓練,這是人們在構建GAN時常常忘記的事情!
這是如此重要的原因是你不能同時訓練兩個網絡,就像試圖校正多個變量變化的東西,你會得到零星的結果。在訓練任何單個網絡時,你需要對模型的其餘部分進行固定設置。
現在構建了完整的模型,我們可以繼續進行訓練。
def run_training(start_it=0, num_epochs=1000):# Save configuration file with global parametersconfig_name = 'gan_cap' + str(NET_CAPACITY) + '_batch' + str(BATCH_SIZE) + '_filt' + str(FILTER_SIZE) + '_disc' + str(DISC_UPDATES) + '_gen' + str(GEN_UPDATES) folder = os.path.join(ROOT_DIR, config_name) if not os.path.isdir(folder): os.mkdir(folder)# Initiate loop variables avg_loss_discriminator = [] avg_loss_generator = [] total_it = start_it # Start of training loop for epoch in range(num_epochs): loss_discriminator = [] loss_generator = [] for it in range(200): # Update discriminator. for i in range(DISC_UPDATES): # Fetch real examples (you could sample unique entries, too). imgs_real = X[np.random.randint(0, X.shape[0], size=BATCH_SIZE)] # Generate fake examples. noise = np.random.randn(BATCH_SIZE, LATENT_DIM) imgs_fake = generator.predict(noise) d_loss_real = discriminator.train_on_batch(imgs_real, np.ones([BATCH_SIZE]))[1] d_loss_fake = discriminator.train_on_batch(imgs_fake, np.zeros([BATCH_SIZE]))[1] # Progress visualizations. if total_it % PROGRESS_INTERVAL == 0: plt.figure(figsize=(5,2)) # We sample separate images. num_vis = min(BATCH_SIZE, 8) imgs_real = X[np.random.randint(0, X.shape[0], size=num_vis)] noise = np.random.randn(num_vis, LATENT_DIM) imgs_fake = generator.predict(noise) for obj_plot in [imgs_fake, imgs_real]: plt.figure(figsize=(num_vis * 3, 3)) for b in range(num_vis): disc_score = float(discriminator.predict(np.expand_dims(obj_plot[b], axis=0))[0]) plt.subplot(1, num_vis, b + 1) plt.title(str(round(disc_score, 3))) plot_image(obj_plot[b]) if obj_plot is imgs_fake: plt.savefig(os.path.join(folder, str(total_it).zfill(10) + '.jpg'), format='jpg', bbox_inches='tight') plt.show() # Update generator. loss = 0 y = np.ones([BATCH_SIZE, 1]) for j in range(GEN_UPDATES): noise = np.random.randn(BATCH_SIZE, LATENT_DIM) loss += gan.train_on_batch(noise, y)[1] loss_discriminator.append((d_loss_real + d_loss_fake) / 2.) loss_generator.append(loss / GEN_UPDATES) total_it += 1 # Progress visualization. clear_output(True) print('Epoch', epoch) avg_loss_discriminator.append(np.mean(loss_discriminator)) avg_loss_generator.append(np.mean(loss_generator)) plt.plot(range(len(avg_loss_discriminator)), avg_loss_discriminator) plt.plot(range(len(avg_loss_generator)), avg_loss_generator) plt.legend(['discriminator loss', 'generator loss']) plt.show()
上面的代碼可能看起來很混亂。上面的代碼中有幾個項目只對管理GAN的運行有幫助。例如,第一部分設置配置文件並保存,因此你可以在將來引用它並確切知道網絡的體系結構和超參數是什麼。
還有進度可視化步驟,可實時列印筆記本的輸出,以便你可以訪問GAN的當前性能。
如果忽略配置文件和進度可視化,則代碼相對簡單。首先,我們更新鑑別器,然後更新生成器,然後我們在這兩個場景之間進行迭代。
現在,由於我們巧妙地使用了特徵,我們可以將模型分為兩行。
generator, discriminator, gan = construct_models(verbose=True)run_training()
剩下要做的就是等待,然後測試網絡的輸出。
來自訓練後的GAN的樣本
現在,在等待網絡完成訓練後,我們可以從網絡中獲取大量樣本。
NUM_SAMPLES = 7plt.figure(figsize=(NUM_SAMPLES * 3, 3)) for i in range(NUM_SAMPLES): noise = np.random.randn(1, LATENT_DIM) pred_raw = generator.predict(noise)[0] pred = pred_raw * 0.5 + 0.5 plt.subplot(1, NUM_SAMPLES, i + 1) plt.imshow(pred) plt.show()Samples from trained GAN
來自訓練有素的GAN的樣本
這是我們第一次實現基本的GAN。這可能是構建特徵齊全的GAN的最簡單方法。