上一篇中介紹的VAE自動編碼器具有了必定程度的創造特徵,可以「無中生有」的由一組隨機數向量生成手寫字符的圖片。
這個「創造能力」咱們在模型中分爲編碼器和解碼器兩個部分。其能力來源其實是大量樣本通過學習編碼後,在數字層面對編碼結果進行微調,再解碼生成圖片的過程。所生成的圖片,是對原樣本圖的某種變形模仿。python
今天的要介紹的生成對抗網絡(GAN)也具有很相似的功能,所創建的模型,可以生成很是接近樣本圖片的結果。
相對於VAE,生成對抗網絡GAN更接近一種思想,並不是針對機器視覺領域,而是一種很通用的機器學習理念。git
讓咱們用一個例子來理解生成對抗網絡:
好比咱們想學習英語朗讀。一開始,咱們的朗讀能力確定不好,每次考試都是不及格。這時候,咱們會努力的學習。固然人的學習是經過各類可能手段,聽錄音、看視頻、找外教。學習一段時間後,再去參加考試,若是成績依然不好,咱們回來繼續學習。一直到咱們獲得了一個本身滿意的成績。
這個例子中有幾個重要的因素:學習者本人就是機器學習中的神經網絡,負責生成某個結果,好比朗讀;考官負責判斷咱們朗讀的英語是否達到了水平要求。考官實際也是一個網絡模型,自己並不能知道什麼樣的朗讀叫好,什麼樣的朗讀叫差,其判斷依據來自於對「好的朗讀」樣本的學習;學習者不斷學習提升的過程,這個就至關於網絡模型不斷的訓練迭代。算法
回到咱們的圖片生成過程。圖片生成是一個模型,負責生成所須要的圖片;
(圖片來自官方文檔)
「考官」負責檢查樣本和生成圖。這裏有一個區別於VAE模型的重點,VAE是直接比較樣本和生成圖,以二者的差距做爲代價。
而GAN中,考官自己的學習,自動爲樣本圖添加標註1,爲生成圖添加標註0。完成學習後,若是生成的圖片,考官會判斷爲真實樣本,說明所生成的圖片達到了應有的水準。
(圖片來自官方文檔)
這樣的機器學習方式,能夠不使用通過標註的樣本數據,可以大量節省成本。雖然會帶來學習過程的加長和大量算力需求,但一般來講,算力仍是更容易得到的。
另外一個角度上說,VAE直接比較樣本圖片和生成圖片,大量的數據和複雜性,致使VAE的損失函數的代碼量大,計算速度也慢。GAN只有真、僞兩個判斷結果,模型輸出簡單,代價函數也容易的多。因此在同一組數據上,使用VAE算法每每會比GAN略慢一些。
看起來若是隻是生成圖片這一個維度的結果,GAN彷佛更有優點,但若是考慮到輸出結果的可控性等因素,VAE在機器視覺領域的應用仍然是很普遍。
不過GAN的思想是比較判斷結果而非原圖,是「裁判」,因此這種思想很容易推廣到多個應用領域,而不只僅是機器視覺範疇。數組
本篇咱們嘗試使用時尚單品的樣本庫做爲訓練數據,最終讓模型能夠由隨機的種子向量,生成時尚單品的圖片。
咱們前面已經作過介紹,時尚單品的樣本也是28x28單色圖片,同MNIST手寫數字樣本是徹底相同的格式。所以換用手寫數字的圖片樣本,只要把載入樣本數據的部分替換掉就能夠,其它代碼無需修改。
緩存
樣本數據載入的部分:bash
(train_images, _), (_, _) = keras.datasets.fashion_mnist.load_data()
咱們實際只須要了訓練的數據集,測試集和兩個數據集的標註咱們都直接拋棄了。GAN是典型的非監督學習,並不須要標註。網絡
源碼中方法make_generator_model用來創建圖片生成模型;make_discriminator_model方法用來創建辨別模型,辨別模型也就是咱們剛纔說的「考官」。
兩個模型都使用keras.Sequential幫助創建,結構並不複雜。
模型的學習必定要關注輸入和輸出,中間的部分若是沒有理論基礎,反而能夠並非很在乎。由於算法的研究會關注模型,軟件開發工程師更關心使用。app
生成網絡輸入隨機數種子向量序列,輸出是28x28x1的圖片序列。一次調用能夠生成多幅圖片。
辨別模型輸入是28x28x1的序列圖片,輸出只有1維。輸出值接近0表明辨別結果是僞圖片,輸出值接近1表示辨別結果是真實樣本圖片。
其中的卷積網絡層,咱們在上一個系列中作了仔細的介紹,這裏能夠再稍微複習一下關於卷積的輸出維度。卷積層的輸入必須是寬x高x色深的多維數組。輸出的色深部分,同卷積層的節點數相同。寬、高則同卷積核的步長數相關,通常是乘的關係。好比:dom
...假設本層輸入爲7x7x256... layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False)) ...由於節點數爲128,步長是1... ...因此輸出維度是7x7x128... layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False)) ...輸出維度爲14x14x64...
使用Keras以後,這些細節通常都不須要本身去算了。但在這種圖片做爲輸入、輸出參數的模型中,爲了保證結果圖片是指定分辨率,這樣的計算仍是難以免的。機器學習
兩個模型分別使用兩個不一樣的代價函數,生成模型的代價函數很簡單。咱們指望生成網絡的圖片,通過辨別模型後,結果無限接近1,也就是真實樣本的水平:
# 生成模型的損失函數 def generator_loss(fake_output): # 生成模型指望最終的結果愈來愈接近1,也就是真實樣本 return cross_entropy(tf.ones_like(fake_output), fake_output)
辨別模型的代價函數,則是要對全部的樣本圖片人爲指定標註結果是1,對全部生成的圖片,則人爲指定標註結果0。這目的是訓練辨別模型對於辨別真僞的能力愈來愈強,從而能夠判斷生成的圖片,是否能無限接近真實樣本圖片的水平。這個過程,其實就是「對抗」的過程。
# 辨別模型損失函數 def discriminator_loss(real_output, fake_output): # 樣本圖但願結果趨近1 real_loss = cross_entropy(tf.ones_like(real_output), real_output) # 本身生成的圖但願結果趨近0 fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) # 總損失 total_loss = real_loss + fake_loss return total_loss
程序的其它部分,都同一般的機器學習項目很是相似,應當讀起來沒有難度了。
#!/usr/bin/env python3 from __future__ import absolute_import, division, print_function, unicode_literals import tensorflow as tf from tensorflow import keras import glob import imageio import matplotlib.pyplot as plt import numpy as np import os import PIL from tensorflow.keras import layers import time import sys # 若是使用train參數運行則進入訓練模式 TRAIN = False if len(sys.argv) == 2 and sys.argv[1] == 'train': TRAIN = True # 使用手寫字體樣本作訓練 # (train_images, _), (_, _) = keras.datasets.mnist.load_data() # 使用時尚單品樣本作訓練 (train_images, _), (_, _) = keras.datasets.fashion_mnist.load_data() # 由於卷積層的需求,增長色深維度 train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32') # 規範化爲-1 - +1 train_images = (train_images - 127.5) / 127.5 BUFFER_SIZE = 60000 BATCH_SIZE = 256 train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE) # 圖片生成模型 def make_generator_model(): model = tf.keras.Sequential() model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,))) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Reshape((7, 7, 256))) assert model.output_shape == (None, 7, 7, 256) # Note: None is the batch size model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False)) assert model.output_shape == (None, 7, 7, 128) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False)) assert model.output_shape == (None, 14, 14, 64) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')) assert model.output_shape == (None, 28, 28, 1) return model generator = make_generator_model() # 原圖、生成圖辨別網絡 def make_discriminator_model(): model = tf.keras.Sequential() model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1])) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same')) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Flatten()) model.add(layers.Dense(1)) return model discriminator = make_discriminator_model() # 隨機生成一個向量,用於生成圖片 noise = tf.random.normal([1, 100]) # 生成一張,此時模型未經訓練,圖片爲噪點 generated_image = generator(noise, training=False) # plt.imshow(generated_image[0, :, :, 0], cmap='gray') # 判斷結果 decision = discriminator(generated_image) # 此時的結果應當應當趨近於0,表示爲僞造圖片 print(decision) # 交叉熵損失函數 cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True) # 辨別模型損失函數 def discriminator_loss(real_output, fake_output): # 樣本圖但願結果趨近1 real_loss = cross_entropy(tf.ones_like(real_output), real_output) # 本身生成的圖但願結果趨近0 fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) # 總損失 total_loss = real_loss + fake_loss return total_loss # 生成模型的損失函數 def generator_loss(fake_output): # 生成模型指望最終的結果愈來愈接近1,也就是真實樣本 return cross_entropy(tf.ones_like(fake_output), fake_output) generator_optimizer = tf.keras.optimizers.Adam(1e-4) discriminator_optimizer = tf.keras.optimizers.Adam(1e-4) # 訓練結果保存 checkpoint_dir = 'dcgan_training_checkpoints' checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt") checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer, discriminator_optimizer=discriminator_optimizer, generator=generator, discriminator=discriminator) EPOCHS = 100 noise_dim = 100 num_examples_to_generate = 16 # 初始化16個種子向量,用於生成4x4的圖片 seed = tf.random.normal([num_examples_to_generate, noise_dim]) # @tf.function表示TensorFlow編譯、緩存此函數,用於在訓練中快速調用 @tf.function def train_step(images): # 隨機生成一個批次的種子向量 noise = tf.random.normal([BATCH_SIZE, noise_dim]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: # 生成一個批次的圖片 generated_images = generator(noise, training=True) # 辨別一個批次的真實樣本 real_output = discriminator(images, training=True) # 辨別一個批次的生成圖片 fake_output = discriminator(generated_images, training=True) # 計算兩個損失值 gen_loss = generator_loss(fake_output) disc_loss = discriminator_loss(real_output, fake_output) # 根據損失值調整模型的權重參量 gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables) gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables) # 計算出的參量應用到模型 generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables)) def train(dataset, epochs): for epoch in range(epochs+1): start = time.time() for image_batch in dataset: train_step(image_batch) # 每一個訓練批次生成一張圖片做爲階段成功 print("=======================================") generate_and_save_images( generator, epoch + 1, seed) # 每20次迭代保存一次訓練數據 if (epoch + 1) % 20 == 0: checkpoint.save(file_prefix=checkpoint_prefix) print('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start)) def generate_and_save_images(model, epoch, test_input): # 設置爲非訓練狀態,生成一組圖片 predictions = model(test_input, training=False) fig = plt.figure(figsize=(4,4)) # 4格x4格拼接 for i in range(predictions.shape[0]): plt.subplot(4, 4, i+1) plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray') plt.axis('off') # 保存爲png plt.savefig('image_at_epoch_{:04d}.png'.format(epoch)) # plt.show() plt.close() # 遍歷全部png圖片,彙總爲gif動圖 def write_gif(): anim_file = 'dcgan.gif' with imageio.get_writer(anim_file, mode='I') as writer: filenames = glob.glob('image*.png') filenames = sorted(filenames) last = -1 for i, filename in enumerate(filenames): frame = 2*(i**0.5) if round(frame) > round(last): last = frame else: continue image = imageio.imread(filename) writer.append_data(image) image = imageio.imread(filename) writer.append_data(image) # 生成一張初始狀態的4格圖片,應當是噪點 generate_and_save_images( generator, 0000, seed) if TRAIN: # 以訓練模式運行,進入訓練狀態 train(train_dataset, EPOCHS) write_gif() else: # 非訓練模式,恢復訓練數據 checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir)) print("After training:") # 顯示訓練完成後,生成圖片的辨別結果 generated_image = generator(noise, training=False) decision = discriminator(generated_image) # 結果應當趨近1 print(decision) # 從新生成隨機值,生成一組圖片保存 seed = tf.random.normal([num_examples_to_generate, noise_dim]) generate_and_save_images( generator, 9999, seed)
程序通過100次迭代,最終生成的的圖片相似這個樣子:
仍是老話,看起來的確通常,但應當說已經有一些神似真實的樣本了。
而把完整的訓練過程連續起來做爲一張動圖,同VAE同樣,是一幅從噪聲到清晰,緩慢的漸進過程。由於GAN網絡並不是直接比較圖片結果,沒法更直接的指出圖片差距,所以在漸進過程當中,能看到一些反覆和跳動。這說明,在機器視覺領域GAN的可控性並不如VAE。
在全部模型未經訓練的時候,咱們隨機生成了一幅圖片,使用辨別器進行了判斷。在訓練完成以後,咱們再次重複這一過程。經過命令行的輸出,咱們能夠看到相似這樣的結果:
tf.Tensor([[-4.8871705e-05]], shape=(1, 1), dtype=float32) After training: tf.Tensor([[-1.5235078]], shape=(1, 1), dtype=float32)
一開始是一個很趨近於0的值,這是由於那張徹底是噪點組成的生成圖片,同真實樣本圖片徹底沒有類似點,雖然辨別模型並未訓練,但這依然是很低的得分。
在訓練完成後,所生成的圖片,從辨別器的眼中看來,已經很接近真實樣本,所以咱們得到了一個較高的得分。
GAN參考論文:《NIPS 2016 Tutorial: Generative Adversarial Networks>
(待續...)