前幾節用代碼介紹了生成型對抗性網絡的實現,但後來我覺得代碼的實現七拐八彎,很多不必要的煩瑣會增加讀者的理解負擔,於是花時間把代碼進行強力精簡,希望由此能幫助有需要的讀者更順利的入門生成型對抗性網絡。
顧名思義,該網絡有一種“對抗”性質。它實際上由兩個子網絡組成,一個網絡叫生成者,一個網絡叫鑑別者,後者類似於老師的作用。根據我們自己的學習經驗得知,老師的作用除了告訴你“怎麼做”之外,最重要的是告訴你“錯在哪”,人本身有強大的模仿能力但卻沒有足夠的糾錯能力,如果在學習時有老師及時指出或糾正你的錯誤,那麼你的學習效果將大大增加。鑑別者網絡其實就是生成者的老師,他有兩個個功能,一個功能是學習特定目標的內在特徵,另一個功能是校正生成者的錯誤,讓生成者不斷提升對學習目標的認知能力。
舉個具體實例,學生跟老師學畫畫,那麼學生就是生成者,老師就是鑑別者。跟普通的師徒不同在於,老師一開始也不懂如何畫畫,他先自學一段時間,等到掌握了一定技巧後,他讓學生自己先畫,然後他根據自己當前的能力指出學生那裏畫錯,學生改正後自己的能力也得到提升。接着老師繼續升級自己的繪畫技能,只有自己水平提高了才能更好的指導學生,於是老師自己不斷進步,然後被他調教的學生也在不斷進步,當老師成爲大師後,如果學生畫出來的話老師也挑不出錯誤,那麼學生也成爲了大師。
我們看看網絡的結構圖:
我們看看如何在數學上執行“把錯誤信息傳遞給生成者”,網絡本質上是一個函數,他接收輸入數據然後給出輸出,真實圖像其實對應二維數組,鑑別者網絡接收該數組後輸出一個值,0表示圖像來自生成者,1表示圖像來自真實圖像。一開始我們將真實圖像輸入鑑別者網絡,調整期內部參數,讓輸出結果儘可能趨近與1,然後將生成者生成的圖片輸入鑑別者網絡,調整其內部參數讓它輸出結果儘可能接近0,這樣生成的圖像和真實圖像相應的信息就會被“寄存”在鑑別者網絡的內部參數。
鑑別者如何“調教”生成者呢,這裏需要借鑑間套函數求導的思路。對於函數D(G(z))中的變量z求導時結果爲D’(G(z))*G’(z),如果我們把G對應生成者,D對應鑑別者,那麼D’(G(z))就等價於鑑別者網絡告訴生成者“錯在哪”,G’(z)對應生成者自己知道錯在哪,於是兩種信息結合在一起就能讓生成者調整內部參數,使得它的輸出越來越能通過鑑別者的識別,由於鑑別者經過訓練後能準確識別真實圖像,如果生成者的生成圖像能通過識別,那意味着生成者的生成結果越來越接近真實圖像,接下來我們看看代碼實現,首先我們使用谷歌提供的一筆畫圖像數據來進行訓練,其獲取路徑在本課堂附件或是如下鏈接:
鏈接:https://pan.baidu.com/s/11Urnrd8QoALLnxaDlu0YPA 密碼:1qqk
首先使用代碼加載圖片資源:
import numpy as np
import os
from os import walk
def load_data(path):
txt_name_list = []
for (dirpath, dirnames, filenames) in walk(path) :#遍歷給定目錄下所有文件和子目錄
for f in filenames:
if f != '.DS_Store':
txt_name_list.append(f)
break
slice_train = int(80000/len(txt_name_list))
i = 0
seed = np.random.randint(1, 10e6)
for txt_name in txt_name_list:
txt_path = os.path.join(path, txt_name) #獲得文件完全路徑
x = np.load(txt_path)#加載npy文件
x = (x.astype('float32') - 127.5) / 127.5 #將數值轉換爲[0,1]之間
x = x.reshape(x.shape[0], 28, 28, 1) #將數值轉換爲圖片規格
y = [i] * len(x)
np.random.seed(seed)
np.random.shuffle(x)
np.random.seed(seed)
np.random.shuffle(y)
x = x[: slice_train]
y = y[: slice_train]
if i != 0:
xtotal = np.concatenate((x, xtotal), axis = 0)
ytotal = np.concatenate((y, ytotal), axis = 0)
else:
xtotal = x
ytotal = y
i += 1
return xtotal, ytotal
path = '/content/drive/My Drive/camel/dataset'
(x_train, y_train) = load_data(path)
print(x_train.shape)
import matplotlib.pyplot as plt
print(np.shape(x_train[200, :,:,:]))
plt.imshow(x_train[200, :,:,0], cmap = 'gray')
上面代碼執行後生成圖像如下:
我們的任務就是訓練生成者網絡,讓它學會繪製上面分割的圖像。下面我們看看兩個網絡的實現代碼:
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from tensorflow.keras import layers
import time
from IPython import display
BUFFER_SIZE = 80000
BATCH_SIZE = 256
EPOCHS = 100
# 批量化和打亂數據
train_dataset = tf.data.Dataset.from_tensor_slices(x_train).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
class Model(tf.keras.Model):
def __init__(self):
super(Model, self).__init__()
self.model_name = "Model"
self.model_layers = []
def call(self, x):
x = tf.convert_to_tensor(x, dtype = tf.float32)
for layer in self.model_layers:
x = layer(x)
return x
class Generator(Model):
def __init__(self):
super(Generator, self).__init__()
self.model_name = "generator"
self.generator_layers = []
self.generator_layers.append(tf.keras.layers.Dense(7*7*256, use_bias = False))
self.generator_layers.append(tf.keras.layers.BatchNormalization())
self.generator_layers.append(tf.keras.layers.LeakyReLU())
self.generator_layers.append(tf.keras.layers.Reshape((7, 7, 256)))
self.generator_layers.append(tf.keras.layers.Conv2DTranspose(128, (5, 5),
padding = 'same',
use_bias = False))
self.generator_layers.append(tf.keras.layers.BatchNormalization())
self.generator_layers.append(tf.keras.layers.LeakyReLU())
self.generator_layers.append(tf.keras.layers.Conv2DTranspose(64, (5,5), strides = (2,2),
padding = 'same',
use_bias = False))
self.generator_layers.append(tf.keras.layers.BatchNormalization())
self.generator_layers.append(tf.keras.layers.LeakyReLU())
self.generator_layers.append(tf.keras.layers.Conv2DTranspose(1, (5,5), strides = (2,2),
padding = 'same',
use_bias = False,
activation = 'tanh'))
self.model_layers = self.generator_layers
def create_variables(self, z_dim):
x = np.random.normal(0, 1, (1, z_dim))
x = self.call(x)
class Discriminator(Model):
def __init__(self):
super(Discriminator, self).__init__()
self.model_name = "discriminator"
self.discriminator_layers = []
self.discriminator_layers.append(tf.keras.layers.Conv2D(64, (5,5), strides = (2,2),
padding = 'same'))
self.discriminator_layers.append(tf.keras.layers.LeakyReLU())
self.discriminator_layers.append(tf.keras.layers.Dropout(0.3))
self.discriminator_layers.append(tf.keras.layers.Conv2D(128, (5,5), strides = (2,2),
padding = 'same'))
self.discriminator_layers.append(tf.keras.layers.LeakyReLU())
self.discriminator_layers.append(tf.keras.layers.Dropout(0.3))
self.discriminator_layers.append(tf.keras.layers.Flatten())
self.discriminator_layers.append(tf.keras.layers.Dense(1))
self.model_layers = self.discriminator_layers
def create_variables(self): #必須要調用一次call網絡纔會實例化
x = np.expand_dims(x_train[200, :,:,:], axis = 0)
self.call(x)
代碼中的網絡層需要簡單描述一下,Conv2D實際上是將維度高,數量大的數據轉換爲維度第,數量小的數據,例如給定一個含有100個元素的向量,如果將其乘以維度爲(80, 100)的矩陣,那麼所得結果就是含有80個元素的向量,於是向量的維度或分量個數減少了,因此它的作用是將輸入的二維數據不斷縮小,抽取其內在規律的“精華”,而Conv2DTranspose相反,它增大輸入數據的維度或分量個數,例如一維向量含有80個分量,那麼乘以維度爲(100,80)的數組後得到含有100個分量的向量,該函數做的就是這個工作,只不過用於相乘的矩陣裏面的分量要經過訓練得到。
接下來我們看訓練流程:
class GAN():
def __init__(self, z_dim):
self.epoch = 0
self.z_dim = z_dim #關鍵向量的維度
#設置生成者和鑑別者網絡的優化函數
self.discriminator_optimizer = tf.train.AdamOptimizer(1e-4)
self.generator_optimizer = tf.train.AdamOptimizer(1e-4)
self.generator = Generator()
self.generator.create_variables(z_dim)
self.discriminator = Discriminator()
self.discriminator.create_variables()
self.seed = tf.random.normal([16, z_dim])
def train_discriminator(self, image_batch):
'''
訓練鑑別師網絡,它的訓練分兩步驟,首先是輸入正確圖片,讓網絡有識別正確圖片的能力。
然後使用生成者網絡構造圖片,並告知鑑別師網絡圖片爲假,讓網絡具有識別生成者網絡僞造圖片的能力
'''
with tf.GradientTape(watch_accessed_variables=False) as tape: #只修改鑑別者網絡的內部參數
tape.watch(self.discriminator.trainable_variables)
noise = tf.random.normal([len(image_batch), self.z_dim])
start = time.time()
true_logits = self.discriminator(image_batch, training = True)
gen_imgs = self.generator(noise, training = True) #讓生成者網絡根據關鍵向量生成圖片
fake_logits = self.discriminator(gen_imgs, training = True)
d_loss_real = tf.nn.sigmoid_cross_entropy_with_logits(labels = tf.ones_like(true_logits), logits = true_logits)
d_loss_fake = tf.nn.sigmoid_cross_entropy_with_logits(labels = tf.zeros_like(fake_logits), logits = fake_logits)
d_loss = d_loss_real + d_loss_fake
grads = tape.gradient(d_loss , self.discriminator.trainable_variables)
self.discriminator_optimizer.apply_gradients(zip(grads, self.discriminator.trainable_variables)) #改進鑑別者網絡內部參數
def train_generator(self, batch_size): #訓練生成者網絡
'''
生成者網絡訓練的目的是讓它生成的圖像儘可能通過鑑別者網絡的審查
'''
with tf.GradientTape(watch_accessed_variables=False) as tape: #只能修改生成者網絡的內部參數不能修改鑑別者網絡的內部參數
tape.watch(self.generator.trainable_variables)
noise = tf.random.normal([batch_size, self.z_dim])
gen_imgs = self.generator(noise, training = True) #生成僞造的圖片
d_logits = self.discriminator(gen_imgs,training = True)
verify_loss = tf.nn.sigmoid_cross_entropy_with_logits(labels = tf.ones_like(d_logits),
logits = d_logits)
grads = tape.gradient(verify_loss, self.generator.trainable_variables) #調整生成者網絡內部參數使得它生成的圖片儘可能通過鑑別者網絡的識別
self.generator_optimizer.apply_gradients(zip(grads, self.generator.trainable_variables))
@tf.function
def train_step(self, image_batch):
self.train_discriminator(image_batch)
self.train_generator(len(image_batch))
def train(self, epochs, run_folder):#啓動訓練流程
for epoch in range(EPOCHS):
start = time.time()
self.epoch = epoch
for image_batch in train_dataset:
self.train_step(image_batch)
display.clear_output(wait=True)
self.sample_images(run_folder) #將生成者構造的圖像繪製出來
self.save_model(run_folder) #存儲兩個網絡的內部參數
print("time for epoc:{} is {} seconds".format(epoch, time.time() - start))
def sample_images(self, run_folder): #繪製生成者構建的圖像
predictions = self.generator(self.seed)
predictions = predictions.numpy()
fig = plt.figure(figsize=(4,4))
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')
plt.savefig('/content/drive/My Drive/camel/images/sample{:04d}.png'.format(self.epoch))
plt.show()
def save_model(self, run_folder): #保持網絡內部參數
self.discriminator.save_weights(os.path.join(run_folder, 'discriminator.h5'))
self.generator.save_weights(os.path.join(run_folder, 'generator.h5'))
def load_model(self, run_folder):
self.discriminator.load_weights(os.path.join(run_folder, 'discriminator.h5'))
self.generator.load_weights(os.path.join(run_folder, 'generator.h5'))
gan = GAN(z_dim = 100)
gan.train(epochs = EPOCHS, run_folder = '/content/drive/My Drive/camel')
注意到train_discriminator函數中,訓練鑑別者網絡時它需要接受兩種數據,一種來自真實圖像,一種來自生成者網絡的圖像,它要訓練的識別真實圖像時返回值越來越接近於1,識別生成者圖像時輸出結果越來越接近0.在train_generator函數中,代碼先讓生成者生成圖像,然後把生成的圖像輸入鑑別者,這就類似於前面提到的間套函數,然後調整生成者內部參數,使得它生成的數據輸入鑑別者後,後者輸出的結果要儘可能的接近1,如此一來生成者產生的圖像纔可能越來越接近真實圖像。這裏還需要非常注意的是在調用網絡時,一定要將training參數設置爲True,這是因爲我們在構造網絡時使用了兩個特殊網絡層,分別是BatchNormalization,和Dropout,這兩個網絡層對網絡的訓練穩定性至關重要,如果不設置training參數爲True,框架就不會執行這兩個網絡對應的運算,這樣就會導致訓練識別,筆者在開始時沒有注意這個問題,因此在調試上浪費了很多時間。
上面代碼運行半個小時後輸出結果如下:
從生成圖片結果看,生成者構造的圖片與前面加載顯示的真實圖片其實沒有太大區別。
更詳細的講解和代碼調試演示過程,請點擊鏈接](https://study.163.com/provider/7600199/course.htm?share=2&shareId=7600199)
更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆號: