現代黑科技版“指鹿爲馬":使用CycleGAN實現男女“無痛變性”

在秦朝末期,奸臣趙高一手遮天,爲了顯示自己的權勢與力量,他在衆人面前指着一頭鹿說那是馬,大家畏懼趙高的權勢,明知那是鹿卻不得不配合趙高說那是馬,這就是經典成語”指鹿爲馬“的出處。

在光天化日之下,罔顧事實強行將A說成B,除非你有權有勢,別人都依附於你,你纔有可能做得到,要不然大家都會認爲你傻逼。例如像我這樣的平頭百姓在大街上指着一個五大三粗,滿臉鬍渣子的大男人說那是個窈窕大美女,你會不會覺得我傻逼呢?在現代人工智能技術的加持下,我還真有指鹿爲馬,指男爲女的”超能力“。

本節我們介紹一種功能強大的對抗性網絡叫CycleGAN,它的特點是能將物體A平和的轉變爲物體B,例如下圖就是CycleGAN的功能實現:

17-14.jpg

從上圖可以看到,訓練好的網絡能將馬變成斑馬,將蘋果與橘子互換,當然我們要實現更強大的功能,那就是男人與女人互換。CycleGAN的實現比前面介紹的對抗性網絡在結構和算法上要複雜很多,首先它有兩個生成者網絡和鑑別者網絡,因爲我們想把物體A變成B,那麼網絡必須有識別和生成物體A和B的能力,因此CycleGAN要使用一組生成者和鑑別者網絡來識別和生成物體A,使用第二組生成者和鑑別者網絡識別和生成物體B,這點跟我們前面描述的對抗性網絡一樣,因此CycleGAN有如下結構特點:

17-15png.png

不同之處在於兩組網絡要把自己掌握的信息與對方溝通,這樣兩組網絡能共同掌握物體A和B的特性,這也是Cycle的由來。接下來是CycleGAN的算法關鍵所在,如下圖所示:
17-16.png
從上圖可見,兩個生成者網絡互相交互形成一個循環。由於第一個生成者網絡Generator_AB用於接收圖片A然後產出圖片B,第二個生成者網絡Generator_BA接收圖片B然後生成圖片A,如果第一個網絡生成的圖片B質量足夠好,那麼將它僞造的圖片B輸出給第二個生成者網絡,後者生僞造的圖片A就應該能獲得好質量,因此判斷第一個網絡生成結果好壞的標準之一就是將它生成的結果用於第二個網絡,看看後者能不能得到好結果,因此這就形成一個循環。

同理第二個生成者網絡接收圖片B後僞造圖片A,如果它僞裝出A的質量足夠好,那麼將它僞裝的結果輸入到第一個生成者網絡,後者僞裝出的圖片B質量就應該足夠好,於是這又形成一個循環。

這種循環訓練的好處在於兩個生成者網絡能使用各自對相應圖片的識別能力去訓練另一個網絡。例如一開始算法使用大量真實圖片A來訓練Generator_AB,於是它就掌握了物體A的內在特徵,當Generator_BA將其僞裝的圖片A輸入到Generator_AB,如此就形成了一條輸入鏈,信息由Generator_BA—>Generator_AB,在訓練時信息會反向傳導變成Generator_AB->Generator_BA,於是前者就把自己對物體A特徵的掌握和識別傳導給後者,這樣後者就能改進自己的構造能力,提升它僞造的圖片質量,同理算法也可以形成Generator_AB—>Generator_BA的閉環,讓後者僞裝的圖片A質量越來越好。

網絡還有第二個循環,那就是對於接收圖片B僞裝圖片A的生成者網絡Generator_BA而言,算法要讓它接收圖片A,然後僞造的圖片A質量也要足夠好,其過程如下圖所示:

圖17-17.png

該循環訓練流程本質上是讓網絡Generator_AB和Generator_BA也學會識別圖片A和B的特徵,這樣才有利於網絡去提升他們僞造的圖片質量。接下來我們看看算法代碼的部分實現,首先是對訓練數據的加載:

celeba_train = tfds.load(name="celeb_a", data_dir = '/content/drive/My Drive/tfds_celeba',split="train") #data_dir指向數據存儲路徑
celeba_test = tfds.load(name="celeb_a", data_dir = '/content/drive/My Drive/tfds_celeba',split="test")
assert isinstance(celeba_train, tf.data.Dataset)
import matplotlib.pyplot as plt
for data in celeba_train.take(1): #利用take接口獲取數據
    print(data) #數據其實是Dict對象,它包含了數據的所有相關屬性
    print(data["attributes"]["Male"])
    plt.imshow(data['image'])

首先我們使用Tensorflow提供的數據集接口加載Celeba人臉圖像數據,然後將圖片分爲男女兩個類別,上面代碼運行後所得結果如下:
下載 (1).png

接下來我們看看Generator網絡的代碼實現:

class Generator(tf.keras.Model):
    def  __init__(self):
        super(Generator, self).__init__()
        self.downsample_layers = []
        self.upsample_layers = []
        self.last_layers = []
        self.resnet_block_layers = []
        self.channels = 3
        self.resnet_block_count = 9
        self.build_self()
    def  build_self(self):
        self.down_sample(filters = 64, kernel_size = 7, strides = 1) 
        self.down_sample(filters = 128, kernel_size = 3)
        self.down_sample(filters = 256, kernel_size = 3)
        for i in range(self.resnet_block_count):
            self.resnet_block()
        self.up_sample(filters = 128, kernel_size = 3) #構造U網絡右邊網絡層
        self.up_sample(filters = 64, kernel_size = 3)
        self.last_layers.append(tf.keras.layers.Conv2D(filters = self.channels, kernel_size = 7, strides = 1,
                                   padding = 'same', activation = 'tanh'))
    def  down_sample(self, filters, kernel_size = 4, strides = 2): #構造U型網絡的左邊
        down_sample_layers = []
        down_sample_layers.append(tf.keras.layers.Conv2D(filters = filters, kernel_size = kernel_size,
                                    strides = strides, padding = 'same'))
        down_sample_layers.append(tfa.layers.InstanceNormalization(axis = -1, center = False, scale = False))
        down_sample_layers.append(tf.keras.layers.ReLU())
        self.downsample_layers.append(down_sample_layers)
    def  up_sample(self, filters, kernel_size = 4, strides = 2):
        up_sample_layers = []
        up_sample_layers.append(tf.keras.layers.Conv2DTranspose(filters = filters, kernel_size = kernel_size,
                                                       strides = strides, padding = 'same'))
        up_sample_layers.append(tfa.layers.InstanceNormalization(axis = -1,
                                                                      center = False,
                                                                      scale = False))
        up_sample_layers.append(tf.keras.layers.ReLU())
        self.upsample_layers.append(up_sample_layers)
    def  resnet_block(self):
        renset_block_layers = []
        renset_block_layers.append(tf.keras.layers.Conv2D(filters = 256, kernel_size = 3,
                                                         strides = 1, padding = 'same'))
        renset_block_layers.append(tfa.layers.InstanceNormalization(axis = -1,
                                                                      center = False,
                                                                      scale = False))
        renset_block_layers.append(tf.keras.layers.ReLU())
        renset_block_layers.append(tf.keras.layers.Conv2D(filters = 256, kernel_size = 3,
                                                         strides = 1, padding = 'same'))
        renset_block_layers.append(tfa.layers.InstanceNormalization(axis = -1,
                                                                      center = False,
                                                                      scale = False))
        self.resnet_block_layers.append(renset_block_layers)
    def  call(self, x):
        x = tf.convert_to_tensor(x, dtype = tf.float32)
        left_layer_results = []
        for layers in self.downsample_layers:
            for layer in layers:
                x = layer(x)
        last_layer = x
        for layers in self.resnet_block_layers:
            for layer in layers:#實現殘餘網絡層
                x = layer(x)
            x = tf.keras.layers.add([last_layer, x])
            last_layer = x
        for layers in self.upsample_layers:
            for layer in layers:
                x = layer(x)
        for layer in self.last_layers:
            x = layer(x)
        return x
    def  create_variables(self, x): #實例化網絡層參數
        x = np.expand_dims(x, axis = 0)
        self.call(x)

該網絡使用了一種叫ResNet的結構,其具體原理請參看我的視頻講解,最後我們給出網絡的訓練流程代碼實現:

 def  train_discriminators(self, imgs_A, imgs_B, valid, fake):
        '''
        訓練discriminator_A識別來自數據集A的圖片以及generator_BA僞造的圖片,訓練discriminoatr_B識別來自數據集B的圖片以及generator_AB僞造的圖片
        訓練的方法是將圖片分成64等分,真實數據每一等分賦值1,僞造數據每一等分賦值0,disriminator接收真實數據後輸出每一等分的
        概率要儘可能接近1,接收僞造數據時輸出每一等分的概率要接近0,valid和fake是規格爲(64,64)的二維數組,元素分別爲1和0
        '''
        fake_B = self.generator_AB(imgs_A, training = True)#將來自數據集A的圖片僞造成數據集B的圖片
        fake_A = self.generator_BA(imgs_B, training = True)#將來自數據集B的圖片僞造成數據集A的圖片
        loss_obj = tf.keras.losses.MSE
        with tf.GradientTape(watch_accessed_variables=False) as tape: #訓練discriminator_A識別真實圖片
            tape.watch(self.discriminator_A.trainable_variables)
            d_A = self.discriminator_A(imgs_A, training = True)
            A_valid_loss = loss_obj(tf.ones_like(d_A), d_A)
            fake_d_A = self.discriminator_A(fake_A, training = True) 
            A_fake_loss = loss_obj(tf.zeros_like(fake_d_A), fake_d_A)
            total_loss = (A_fake_loss + A_valid_loss) * 0.5
        grads = tape.gradient(total_loss, self.discriminator_A.trainable_variables)
        self.discriminator_A_optimizer.apply_gradients(zip(grads, self.discriminator_A.trainable_variables))
        with tf.GradientTape(watch_accessed_variables=False) as tape:#訓練dicriminator_B識別真實圖片
            tape.watch(self.discriminator_B.trainable_variables)
            d_B = self.discriminator_B(imgs_B, training = True)
            B_valid_loss = loss_obj(tf.ones_like(d_B), d_B)
            fake_d_B = self.discriminator_B(fake_B, training = True)
            B_fake_loss = loss_obj(tf.zeros_like(fake_d_B), fake_d_B)
            total_loss = (B_valid_loss + B_fake_loss) * 0.5
        grads = tape.gradient(total_loss, self.discriminator_B.trainable_variables)
        self.discriminator_B_optimizer.apply_gradients(zip(grads, self.discriminator_B.trainable_variables))
    def  train_generators(self, imgs_A, imgs_B, valid):
        '''
        generator的訓練要滿足三個層次,1,generator生成的僞造圖片要儘可能通過discrimator的識別;
        2,先由generator_A將來自數據集A的圖片僞造成數據集B的圖片,然後再將其輸入generator_B,所還原
        的圖片要與來自數據集A的圖片儘可能相似;3,將來自數據集B的圖片輸入generator_AB後所得結果要與數據集B
        的圖片儘可能相同,將來自數據集A的圖片輸入generator_BA後所得結果要儘可能與來自數據集A的數據相同
        '''
        loss_obj = tf.keras.losses.MSE
        with tf.GradientTape(watch_accessed_variables=False) as tape_A,tf.GradientTape(watch_accessed_variables=False) as tape_B:
            tape_A.watch(self.generator_AB.trainable_variables)
            tape_B.watch(self.generator_BA.trainable_variables)
            fake_B = self.generator_AB(imgs_A, training = True) 
            d_B = self.discriminator_B(fake_B, training = True)
            fake_B_loss = loss_obj(tf.ones_like(d_B), d_B)
            fake_A = self.generator_BA(imgs_B, training = True)
            d_A = self.discriminator_A(fake_A, training = True)
            fake_A_loss = loss_obj(tf.ones_like(d_A), d_A)#這段對應圖17-15所示的運算流程
            reconstructB = self.generator_AB(fake_A, training = True)#B->A->B
            reconstruct_B_loss = tf.reduce_mean(tf.abs(reconstructB - imgs_B))
            reconstructA = self.generator_BA(fake_B, training = True)#A->B->A
            reconstruct_A_loss = tf.reduce_mean(tf.abs(reconstructA - imgs_A))
            cycle_loss_BAB = self.reconstruction_weight * reconstruct_B_loss 
            cycle_loss_ABA = self.reconstruction_weight * reconstruct_A_loss
            total_cycle_loss = cycle_loss_BAB + cycle_loss_ABA#這裏對應圖17-16所示的運算流程
            img_B_id = self.generator_AB(imgs_B, training = True) #B->B
            img_B_identity_loss = tf.reduce_mean(tf.abs(img_B_id - imgs_B))
            img_A_id = self.generator_BA(imgs_A, training = True) #A->A
            img_A_identity_loss = tf.reduce_mean(tf.abs(img_A_id - imgs_A))#這段對應圖17-17所示運算流程
            generator_AB_loss = self.validation_weight * fake_B_loss + total_cycle_loss + self.identification_weight * img_B_identity_loss
            gernator_BA_loss = self.validation_weight * fake_A_loss + total_cycle_loss + self.identification_weight * img_A_identity_loss
        grads_AB = tape_A.gradient(generator_AB_loss, self.generator_AB.trainable_variables)
        grads_BA = tape_B.gradient(gernator_BA_loss, self.generator_BA.trainable_variables)
        self.generator_AB_optimizer.apply_gradients(zip(grads_AB, self.generator_AB.trainable_variables))
        self.generator_BA_optimizer.apply_gradients(zip(grads_BA, self.generator_BA.trainable_variables))
    def train(self, data_loader, run_folder, epochs, test_A_file, test_B_file, batch_size = 1, sample_interval = 100):
        dummy = np.zeros((batch_size, self.patch, self.patch, 1))
        valid = tf.ones_like(dummy, dtype = tf.float32)
        fake = tf.zeros_like(dummy, dtype = tf.float32)
        for epoch in range(epochs):
            start = time.time()
            self.epoch = epoch
            batch_count = 0
            for batch_i, (imgs_A, imgs_B) in enumerate(data_loader.load_batch()):
                self.train_discriminators(imgs_A, imgs_B, valid, fake)
                self.train_generators(imgs_A, imgs_B, valid)
                if  batch_i % sample_interval == 0:
                    self.save_model(run_folder) #存儲網絡內部參數
                    display.clear_output(wait=True)
                    info = "[Epoch {}/{}/ batch {}]".format(self.epoch, epochs, batch_count)
                    batch_count += 1
                    self.sample_images(data_loader, batch_i, run_folder, test_A_file, test_B_file, info) #顯示訓練效果

由於篇幅原因,筆者沒有將所有實現細節的代碼都貼出來,感興趣的讀者請參看我的教學課程。

代碼運行後會將大量的男女圖片輸入兩個網絡,於是能讓網絡具備將男人轉換爲女人,女人轉換爲男人的能力,我們先看網絡將男人變成女人的能力:
屏幕快照 2020-05-14 上午11.23.56.png
其中左邊是男性圖片,右邊是”變性“後的女性圖片,比較發現女性特徵是臉部表情更柔和,更具有女性的柔軟,我們再看看將女性變成男性的結果:

屏幕快照 2020-05-14 上午11.25.56.png

上圖效果就更加明顯,可以看到的是右邊男性面孔臉部輪廓曲線與左邊女性基本相同,男性臉部特徵就在於皮膚比較粗糙,同時線條比較粗狂和硬朗,從顯示結果看,網絡具備了將男變女,女變男的超能力。

更詳細的講解和完整代碼調試演示過程,請點擊鏈接

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆號:
這裏寫圖片描述

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