你會有貓的:AI幫你生成各種各樣的喵。

是不是經常羨慕某些朋友年級輕輕就貓狗雙全,自己只能雲吸貓,然而即便是雲吸也只能吸別人家的貓。不過告訴大家一個好消息,我們完全可以讓 AI 給自己生成各種各樣的貓咪出來!

回到 2014 年,那年人工智慧領域的頂級專家 Ian Goodfellow 帶領團隊發表了生成式對抗網路(GAN)的研究論文,成為生成模型領域的里程碑事件。機器學習大神 Yann Lecun 更是將生成式對抗網路稱為「過去 20 年來機器學習領域最酷的點子。

今天我(作者Thomas Simonini——譯者注)就教大家如何藉助生成式對抗網路架構,搭建一個AI,為我們生成非常真實的貓咪照片。

上圖是 AI 在努力的生成貓咪圖像,看著還不賴。

點擊下面這個 GitHub 庫,可以查看本項目的全部代碼:

github.com/simoninithom

如果你懂得 Python 編程,對 TensorFlow、深度學習、卷積神經網路這些已經有所了解,那麼本項目涉及的東西對你來說非常簡單。

玩兒懂深度學習Part 1:傳統機器學習的回顧 - 集智專欄?

jizhi.im圖標

何為DCGAN?

深度卷積生成式對抗網路(DCGAN)是一種深度學習架構,可以生成與訓練集中數據相似的輸出。

這個模型以卷積層取代了生成式對抗網路中的全連接層。

為了能更形象的解釋 DCGAN 的工作原理,我們以藝術家和偽造者這兩個身份做個比喻。

偽造者(也就是「生成器」generator)會試著製造假冒的梵高的畫作,把它們當成真的進行傳遞。

另一方面,藝術家(即「判別器」discriminator)會利用自己對真實的梵高畫作的理解,去儘力「揪」出偽造者。

隨著時間推移,藝術家在檢測冒牌畫方面越來越在行,而相應的偽造者製作假畫的技術也越來越高明。

我們可以看到,DCGAN 由兩個單獨的深度學習網路組成,這兩個網路會相互對抗和博弈。

  • 生成器就是像偽造者一樣,孜孜不倦的生成看起來很真實的數據。它並不清楚真實的數據是什麼樣子,但是會根據其它模型的反饋學習如何調整生成數據。
  • 判別器就是像檢查員一樣,儘力不判斷出真實數據的假陽性案例。該模型的輸出結果會用於生成器的反向傳播。

  • 生成器會用一個隨機噪音向量(noise vector)生成一個圖像。
  • 然後這張圖像會輸入判別器中,後者會將圖像和訓練集中的圖像進行對比。
  • 判別器返回一個介於0(假圖像)和1(真圖像)之間的數字。

創建一個DCGAN

現在我們創建生成貓咪的 AI 模型。

在這部分,我們會重點關注 AI 模型的主要元素。如果你想查看全部代碼,可以點擊這裡:

github.com/simoninithom

輸入

這裡我們創建輸入佔位符 inputs_real 用於判別器,inputs_z 用於生成器。

注意我們用了兩個學習率,一個用於生成器,一個判別器。

DCGAN 對於超參數非常敏感,所以精確調參非常重要。

判別器和生成器

我們使用 tf.variable_scope 有兩個原因。

首先,我們想確保所有的變數名都以生成器/判別器開頭,這在後面訓練這兩個神經網路時會對我們大有益處。

其次,我們想以不同的輸入再利用這兩個神經網路:

  • 對於生成器:我們會對它進行訓練,但也會在訓練後從中對假圖像取樣。
  • 對於判別器:我們需要在假圖像和輸入圖像之間分享變數。

現在我們創建判別器,它會讓輸入當成真實或虛假的圖像,並輸出一個分數。

備註:

  • 原則就是在每個卷積層上將過濾器大小加倍。
  • 不建議使用下採樣。相反,我們使用只帶步長的卷積層(strided convolutional layer)。我們在每一層(除了輸入層)使用批歸一化(batch normalization)因為它能減少協方差位移(covariance shift)的情況。所謂covariance shift,就是指在模型訓練的時候我們一般都會做樣本歸一化,在往多層神經網路傳播時,前面層參數的改變,使得後面層的輸入分布發生改變時,就叫covariance shift。
  • 我們用 Leaky ReLU函數作為激活函數,因為它會幫助我們避免梯度消失帶來的影響。

def discriminator(x, is_reuse=False, alpha = 0.2): Build the discriminator network. Arguments --------- x : Input tensor for the discriminator n_units: Number of units in hidden layer reuse : Reuse the variables with tf.variable_scope alpha : leak parameter for leaky ReLU Returns ------- out, logits: with tf.variable_scope("discriminator", reuse = is_reuse): # Input layer 128*128*3 --> 64x64x64 # Conv --> BatchNorm --> LeakyReLU conv1 = tf.layers.conv2d(inputs = x, filters = 64, kernel_size = [5,5], strides = [2,2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name=conv1) batch_norm1 = tf.layers.batch_normalization(conv1, training = True, epsilon = 1e-5, name = batch_norm1) conv1_out = tf.nn.leaky_relu(batch_norm1, alpha=alpha, name="conv1_out") # 64x64x64--> 32x32x128 # Conv --> BatchNorm --> LeakyReLU conv2 = tf.layers.conv2d(inputs = conv1_out, filters = 128, kernel_size = [5, 5], strides = [2, 2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name=conv2) batch_norm2 = tf.layers.batch_normalization(conv2, training = True, epsilon = 1e-5, name = batch_norm2) conv2_out = tf.nn.leaky_relu(batch_norm2, alpha=alpha, name="conv2_out") # 32x32x128 --> 16x16x256 # Conv --> BatchNorm --> LeakyReLU conv3 = tf.layers.conv2d(inputs = conv2_out, filters = 256, kernel_size = [5, 5], strides = [2, 2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name=conv3) batch_norm3 = tf.layers.batch_normalization(conv3, training = True, epsilon = 1e-5, name = batch_norm3) conv3_out = tf.nn.leaky_relu(batch_norm3, alpha=alpha, name="conv3_out") # 16x16x256 --> 16x16x512 # Conv --> BatchNorm --> LeakyReLU conv4 = tf.layers.conv2d(inputs = conv3_out, filters = 512, kernel_size = [5, 5], strides = [1, 1], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name=conv4) batch_norm4 = tf.layers.batch_normalization(conv4, training = True, epsilon = 1e-5, name = batch_norm4) conv4_out = tf.nn.leaky_relu(batch_norm4, alpha=alpha, name="conv4_out") # 16x16x512 --> 8x8x1024 # Conv --> BatchNorm --> LeakyReLU conv5 = tf.layers.conv2d(inputs = conv4_out, filters = 1024, kernel_size = [5, 5], strides = [2, 2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name=conv5) batch_norm5 = tf.layers.batch_normalization(conv5, training = True, epsilon = 1e-5, name = batch_norm5) conv5_out = tf.nn.leaky_relu(batch_norm5, alpha=alpha, name="conv5_out") # Flatten it flatten = tf.reshape(conv5_out, (-1, 8*8*1024)) # Logits logits = tf.layers.dense(inputs = flatten, units = 1, activation = None) out = tf.sigmoid(logits)return out, logits

接著我們創建生成器,它是將一個隨機噪音向量(Z)作為輸入,輸出一個假圖像,因為它內置有反卷積層(transposed convolution layer)。

主要想法就是在每一層我們將過濾器大小減半,將圖像大小翻倍。

此前發現生成器用tanh作為輸出激活函數時效果最好。

def generator(z, output_channel_dim, is_train=True): Build the generator network. Arguments --------- z : Input tensor for the generator output_channel_dim : Shape of the generator output n_units : Number of units in hidden layer reuse : Reuse the variables with tf.variable_scope alpha : leak parameter for leaky ReLU Returns ------- out: with tf.variable_scope("generator", reuse= not is_train): # First FC layer --> 8x8x1024 fc1 = tf.layers.dense(z, 8*8*1024) # Reshape it fc1 = tf.reshape(fc1, (-1, 8, 8, 1024)) # Leaky ReLU fc1 = tf.nn.leaky_relu(fc1, alpha=alpha) # Transposed conv 1 --> BatchNorm --> LeakyReLU # 8x8x1024 --> 16x16x512 trans_conv1 = tf.layers.conv2d_transpose(inputs = fc1, filters = 512, kernel_size = [5,5], strides = [2,2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name="trans_conv1") batch_trans_conv1 = tf.layers.batch_normalization(inputs = trans_conv1, training=is_train, epsilon=1e-5, name="batch_trans_conv1") trans_conv1_out = tf.nn.leaky_relu(batch_trans_conv1, alpha=alpha, name="trans_conv1_out") # Transposed conv 2 --> BatchNorm --> LeakyReLU # 16x16x512 --> 32x32x256 trans_conv2 = tf.layers.conv2d_transpose(inputs = trans_conv1_out, filters = 256, kernel_size = [5,5], strides = [2,2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name="trans_conv2") batch_trans_conv2 = tf.layers.batch_normalization(inputs = trans_conv2, training=is_train, epsilon=1e-5, name="batch_trans_conv2") trans_conv2_out = tf.nn.leaky_relu(batch_trans_conv2, alpha=alpha, name="trans_conv2_out") # Transposed conv 3 --> BatchNorm --> LeakyReLU # 32x32x256 --> 64x64x128 trans_conv3 = tf.layers.conv2d_transpose(inputs = trans_conv2_out, filters = 128, kernel_size = [5,5], strides = [2,2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name="trans_conv3") batch_trans_conv3 = tf.layers.batch_normalization(inputs = trans_conv3, training=is_train, epsilon=1e-5, name="batch_trans_conv3") trans_conv3_out = tf.nn.leaky_relu(batch_trans_conv3, alpha=alpha, name="trans_conv3_out") # Transposed conv 4 --> BatchNorm --> LeakyReLU # 64x64x128 --> 128x128x64 trans_conv4 = tf.layers.conv2d_transpose(inputs = trans_conv3_out, filters = 64, kernel_size = [5,5], strides = [2,2], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name="trans_conv4") batch_trans_conv4 = tf.layers.batch_normalization(inputs = trans_conv4, training=is_train, epsilon=1e-5, name="batch_trans_conv4") trans_conv4_out = tf.nn.leaky_relu(batch_trans_conv4, alpha=alpha, name="trans_conv4_out") # Transposed conv 5 --> tanh # 128x128x64 --> 128x128x3 logits = tf.layers.conv2d_transpose(inputs = trans_conv4_out, filters = 3, kernel_size = [5,5], strides = [1,1], padding = "SAME", kernel_initializer=tf.truncated_normal_initializer(stddev=0.02), name="logits") out = tf.tanh(logits, name="out") return out

判別器和生成器損失

因為我們同時訓練生成器和判別器,我們需要計算出這兩個神經網路的損失。

我們想讓判別器「認為」圖像為真時,輸出 1,當圖像為假時,輸出 0。因此,我們需要設置損失值反映出來。

判別器損失是真圖像和假圖像的損失之和:

d_loss = d_loss_real + d_loss_fake

d_loss_real 是當判別器預測圖像為假但實際上為真時的損失,計算它的方式如下:

  • 使用 d_logits_real,標籤全為 1(因為所有的真實數據都為真)
  • labels = tf.ones_like(tensor) * (1 - smooth) 我們使用標籤平滑:它意味著將標籤降低一點,從 1 降到 0.9,目的是為了幫判別器更好的泛化。

d_loss_fake 是當判別器預測圖像為真但實際上為假時的損失。

  • 使用 d_logits_fake,標籤為0。

生成器損失再次使用來自判別器中的 d_logits_fake。這次標籤全為 1,因此生成器想「忽悠」判別器。

def model_loss(input_real, input_z, output_channel_dim, alpha): """ Get the loss for the discriminator and generator :param input_real: Images from the real dataset :param input_z: Z input :param out_channel_dim: The number of channels in the output image :return: A tuple of (discriminator loss, generator loss) """ # Generator network here g_model = generator(input_z, output_channel_dim) # g_model is the generator output # Discriminator network here d_model_real, d_logits_real = discriminator(input_real, alpha=alpha) d_model_fake, d_logits_fake = discriminator(g_model,is_reuse=True, alpha=alpha) # Calculate losses d_loss_real = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_real, labels=tf.ones_like(d_model_real))) d_loss_fake = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake, labels=tf.zeros_like(d_model_fake))) d_loss = d_loss_real + d_loss_fake g_loss = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake, labels=tf.ones_like(d_model_fake)))return d_loss, g_loss

優化器

在計算損失後,我們需要分別更新生成器和判別器。

為了實現這一點,我們需要用 tf.trainable_varianle() 獲取它們各自的變數。這會為我們之前定義的所有變數創建一個列表。

def model_optimizers(d_loss, g_loss, lr_D, lr_G, beta1): """ Get optimization operations :param d_loss: Discriminator loss Tensor :param g_loss: Generator loss Tensor :param learning_rate: Learning Rate Placeholder :param beta1: The exponential decay rate for the 1st moment in the optimizer :return: A tuple of (discriminator training operation, generator training operation) """ # Get the trainable_variables, split into G and D parts t_vars = tf.trainable_variables() g_vars = [var for var in t_vars if var.name.startswith("generator")] d_vars = [var for var in t_vars if var.name.startswith("discriminator")] update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) # Generator update gen_updates = [op for op in update_ops if op.name.startswith(generator)] # Optimizers with tf.control_dependencies(gen_updates): d_train_opt = tf.train.AdamOptimizer(learning_rate=lr_D, beta1=beta1).minimize(d_loss, var_list=d_vars) g_train_opt = tf.train.AdamOptimizer(learning_rate=lr_G, beta1=beta1).minimize(g_loss, var_list=g_vars)return d_train_opt, g_train_opt

訓練

在這部分,我們應用訓練函數。

理念也比較簡單:

  • 我們每 5 個周期就保存模型。
  • 我們每 10 個訓練批次就在圖像文件夾中保存一張圖像。
  • 我們每 15 個周期會顯示 g_loss,d_loss 和生成的圖像。這麼做的原因也很簡單:如果顯示太過的圖像 Jupyter notebook 會出現 bug。
  • 或者我們也可通過載入已保存的模型來直接生成圖像(這會為你節省 20 個小時的訓練時間)。

def train(epoch_count, batch_size, z_dim, learning_rate_D, learning_rate_G, beta1, get_batches, data_shape, data_image_mode, alpha): """ Train the GAN :param epoch_count: Number of epochs :param batch_size: Batch Size :param z_dim: Z dimension :param learning_rate: Learning Rate :param beta1: The exponential decay rate for the 1st moment in the optimizer :param get_batches: Function to get batches :param data_shape: Shape of the data :param data_image_mode: The image mode to use for images ("RGB" or "L") """ # Create our input placeholders input_images, input_z, lr_G, lr_D = model_inputs(data_shape[1:], z_dim) # Losses d_loss, g_loss = model_loss(input_images, input_z, data_shape[3], alpha) # Optimizers d_opt, g_opt = model_optimizers(d_loss, g_loss, lr_D, lr_G, beta1) i = 0 version = "firstTrain" with tf.Session() as sess: sess.run(tf.global_variables_initializer()) # Saver saver = tf.train.Saver() num_epoch = 0 if from_checkpoint == True: saver.restore(sess, "./models/model.ckpt") show_generator_output(sess, 4, input_z, data_shape[3], data_image_mode, image_path, True, False) else: for epoch_i in range(epoch_count): num_epoch += 1 if num_epoch % 5 == 0: # Save model every 5 epochs #if not os.path.exists("models/" + version): # os.makedirs("models/" + version) save_path = saver.save(sess, "./models/model.ckpt") print("Model saved") for batch_images in get_batches(batch_size): # Random noise batch_z = np.random.uniform(-1, 1, size=(batch_size, z_dim)) i += 1 # Run optimizers _ = sess.run(d_opt, feed_dict={input_images: batch_images, input_z: batch_z, lr_D: learning_rate_D}) _ = sess.run(g_opt, feed_dict={input_images: batch_images, input_z: batch_z, lr_G: learning_rate_G}) if i % 10 == 0: train_loss_d = d_loss.eval({input_z: batch_z, input_images: batch_images}) train_loss_g = g_loss.eval({input_z: batch_z}) # Save it image_name = str(i) + ".jpg" image_path = "./images/" + image_name show_generator_output(sess, 4, input_z, data_shape[3], data_image_mode, image_path, True, False) # Print every 5 epochs (for stability overwize the jupyter notebook will bug) if i % 1500 == 0: image_name = str(i) + ".jpg" image_path = "./images/" + image_name print("Epoch {}/{}...".format(epoch_i+1, epochs), "Discriminator Loss: {:.4f}...".format(train_loss_d), "Generator Loss: {:.4f}".format(train_loss_g)) show_generator_output(sess, 4, input_z, data_shape[3], data_image_mode, image_path, False, True)return losses, samples

怎樣運行模型

你可以在自己的電腦上運行搭建的模型,不過要有 GPU,不然訓練時間會長到嚇人!而且也要用到雲 GPU 服務,比如 AWS 和 FloydHub。

就我自己而言,我用微軟的 Azure 和 Deep Learning Virtual Machine 將這個貓咪生成模型訓練了大約 20 小時(聲明:可不是給微軟打廣告啊,只是覺得他們產品的自定義服務做得很好!)。

如果在使用微軟的 Virtual Machine 遇到的問題,可以參考這篇文章:

medium.com/@manikantaya

最終,AI 模型為我生成了好多貓咪!

就是這些,希望本篇教程能幫到你,讓 AI 也給你生成各種各樣的喵星人。

對了....如果你家裡已經有了一位喵主子,那就要小心遇上這種情況:


參考資料:

medium.freecodecamp.org

推薦閱讀:

機器學習頂會NIPS要改名了
LeCun親授的深度學習入門課:從飛行器的發明到卷積神經網路
6個案例故事一窺人工智慧和機器學習帶來的巨變
人工智慧產品除了智能機器人還有哪些
計算機視覺與影視業邂逅

TAG:人工智慧 | 生成對抗網路GAN | 景略集智 |