圖像轉譯是GAN其中一個應用,它是用某個轉譯器(生成器)把一種影像轉成另一種影像。
就是把影像從一個域(domain,可想成是特定的風格或形式)映射到另外一個域。
要讓模型學會將白天的街景轉為晚上,就得找幾組相同場景的白天及晚上照片給模型學習;如果想將平面的素描變成立體的彩色實品,就得在訓練集內多放幾組素描與對應的實品圖片。
也就是說,每個(原始域)影像都要附上對應的(目標域)標籤影像,GAN才能從中學習如何轉譯。
以黑白影像上色為例,訓練集通常可以用濾鏡將彩色影像轉為黑白影像,轉換前後的影像就能成為一對(各有不同的域)。這樣才能確保兩個域的影像能百分之百對應,以便讓GAN能歸納出轉譯邏輯,就能用訓練好的GAN來將任何黑白圖片上色了。
只要先從一個域轉換到另一個域,然後再轉回,來回比對即可,不須準備相對應的圖片即可完成圖像轉譯。
例如將某公園的夏日景象(A域)轉換成冬日景象(B域),然後再轉回夏日(A域),這樣就能建立一個基本的循環(cycle)。
理想情況下,原始圖片(a)會跟轉回來的圖片(â)一模一樣;若兩者不一樣,則可透過比較兩者像素的差異來估計CycleGAN的第一種損失:來回一致損失。
這跟反向翻譯有點像:先把中文翻成英文,再翻回中文,最後得出的句子應該要跟剛開始的一樣,如果結果不一樣,那只要比較原來的中文與翻譯回來的中文之間的差異,便能估計出來回一致損失
要計算來回一致損失,需要準備兩個生成器
一個將A轉譯成B (稱為Gᴀʙ,或G)
一個將B轉譯為A (稱為Gʙᴀ,或F)
生成器Gᴀʙ與Gʙᴀ各有一個對應的鑑別器:Dʙ與Dᴀ,它們是用來驗證輸入影像是否屬於該域:當從B域轉譯成A域時,就用Dᴀ確認是否與A域相似,反過來就用Dʙ。
此損失因為有兩個鑑別器,所以會有兩個對抗損失。每次轉譯後都得用鑑別器確保影像符合目標域的特徵:例如把蘋果換成柳丁後,要看看是不是真的變成柳丁;再從柳丁換回蘋果也依樣,要確認是不是真的像蘋果。
對抗損失就是為了確保轉譯出的影像沒問題,這是CycleGAN能否順利運作的關鍵。
特質損失可以強迫CycleGan保留圖片的整體色彩結構(color structure),是一種常規化(regularization)技巧,使生成圖片的色調與原始圖片保持一致。
計算特質損失的原理,就是將已有的A域影像輸進Gʙᴀ(將B域轉譯成A域的生成器),但此影像本來就屬於A域,CycleGAN夠聰明的話就不會對本來就屬於A域的影像做太大更動。
也就是說,這個損失會針對不必要的修改做出懲罰。
CycleGAN的基本架構
輸入至鑑別器(Discriminator A),判斷A是否為A域影像。
(a)輸入生成器A2B(Gᴀʙ),轉譯為B域。
(b)用B域影像專用的鑑別器Discriminator B判斷是否為該域的影像。
(c)將影像轉譯回A域,測量來回一致損失(Cycle-consistency loss)
!mkdir ./datasets #建立資料夾以儲存下載的檔案
!mkdir ./images
!mkdir ./images/apple2orange #建立資料夾以儲存CycleGAN生成的圖片
!pip install scipy==1.2.1
!pip install git+https://www.github.com/keras-team/keras-contrib.git #安裝keras_contrib
import tensorflow as tf
下載 apple2orange資料集
%%bash
FILE=apple2orange
URL=https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/$FILE.zip
ZIP_FILE=./datasets/$FILE.zip
TARGET_DIR=./datasets/$FILE/
wget -N $URL -O $ZIP_FILE
mkdir $TARGET_DIR
unzip $ZIP_FILE -d ./datasets/
rm $ZIP_FILE
匯入模型、函式庫
from __future__ import print_function, division
from tensorflow.keras.datasets import mnist
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
from keras.layers import Input, Dense, Reshape, Flatten, Dropout, Concatenate
from keras.layers import BatchNormalization, Activation, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from tensorflow.keras.optimizers import Adam
import datetime
import matplotlib.pyplot as plt
import sys
import numpy as np
import os
import scipy.misc
定義CycleGAN類別
class CycleGAN():
def __init__(self):
self.img_rows = 128
self.img_cols = 128
self.channels = 3 #樣本圖片的維度
self.img_shape = (self.img_rows , self.img_cols , self.channels)
self.dataset_name = 'apple2orange' #設定要載入的資料集名稱
self.data_loader = DataLoader(dataset_name=self.dataset_name , img_res=(self.img_rows , self.img_cols)) #建立DataLoader物件來載入預先處理好的資料集
patch = int(self.img_rows / 2**4) #計算鑑別器(patchGAN的架構)的輸出維度
self.disc_patch = (patch , patch ,1) #鑑別器輸出的shape為(8,8,1)
self.gf = 32 #生成器第一層的過濾器數量
self.df = 64 #鑑別器第一層的過濾器數量
self.lambda_cycle = 10.0 #來回一致損失的權重
self.lambda_id = 0.9 * self.lambda_cycle #特質損失的權重
optimizer = Adam(0.0002 , 0.5)
lambda_cycle:來回一致損失的權重,把它設大一點,可以確保原圖和重建圖片盡可能相似。
lambda_id:特質損失的權重,模型對這個參數的變動極為敏感;若是設太小,反而會導致意料之外的結果,像是顏色變過頭。
patchGAN:在GAN中,鑑別器會輸出0~1之間的數字,代表是真實圖片的機率;而在patchGAN則是輸出NxN的矩陣X,每一個元素x[i][j]表示一個patch,並求所有patch的平均值作為鑑別器的最終輸出。
優化器(optimizer):使用Adam(Adaptive Moment Estimation)演算法做優化,此優化器是基於梯度下降法的進階優化器。Adam的第一個參數0.0002代表學習率,它控制了權重的更新比率,
新增並編譯兩個鑑別器:Dᴀ與Dʙ
新增兩個生成器:
a.分別用Gᴀʙ、Gʙᴀ表示
b.為每個生成器準備影像輸入層
c.分別將輸入的影像轉譯到另一個域
d.分別重建(轉譯)回原始域
e.分別計算特質損失
f.鎖定2個鑑別器的參數(以免在訓練生成器時被修改)
g.編譯這兩個生成器
在__init__()中繼續建立及編譯4個神經網路
self.d_A = self.build_discriminator()
self.d_B = self.build_discriminator()
self.d_A.compile(loss='mse' , optimizer = optimizer , metrics =['accuracy']) #編譯: 選擇損失函數、優化方法及成效衡量方式
self.d_B.compile(loss='mse' , optimizer = optimizer , metrics =['accuracy']) #建立並編譯鑑別器
self.g_AB = self.build_generator()
self.g_BA = self.build_generator() #建立生成器
img_A = Input(shape=self.img_shape)
img_B = Input(shape=self.img_shape) #建立兩個域圖片的輸入層
fake_B = self.g_AB(img_A)
fake_A = self.g_BA(img_B) #將影像轉譯到另一個域
reconstr_A = self.g_BA(fake_B)
reconstr_B = self.g_AB(fake_A) #將影像轉譯回原域
img_A_id = self.g_BA(img_A)
img_B_id = self.g_AB(img_B) #影像的特質映射(identity mapping),以便用它來計算特質損失
self.d_A.trainable =False
self.d_B.trainable =False #鎖定鑑別器(因為這個組合模型只用來訓練生成器)
valid_A = self.d_A(fake_A)
valid_B = self.d_B(fake_B) #用鑑別器來鑑定轉譯影像
self.combined = Model(inputs = [img_A , img_B] , outputs = [valid_A , valid_B , reconstr_A ,reconstr_B , img_A_id , img_B_id])
#建立「有2個輸入、6個輸出」的組合模型來訓練生成器,以學習如何騙過鑑別器
self.combined.compile(loss =['mse','mse','mae','mae','mae','mae'],loss_weights = [1,1,self.lambda_cycle,self.lambda_cycle,self.lambda_id,self.lambda_id] , optimizer=optimizer)
成效衡量指標(metrics):用來在訓練過程中監測一些性能指標。
鎖定鑑別器:避免生成器受到鑑別器的影響。
組合模型的輸出會有六個,因為必須根據對抗性損失(影像的真實化,由鑑別器估計)、來回一致損失、特質損失來進行優化。
均方誤差(mean-square error、MSE):計算方法是求預測值與真實值之間距離的平方和,用於前兩項的鑑別器輸出機率,以計算對抗損失。
平均絕對值誤差(Mean Absolute Error、MAE):目標值和預測值之差的絕對值之和,用於後四項的輸出影像,以計算來回一致損失與特質損失。
CycleGAN的降採樣與升採樣,是要先將影像濃縮出最具代表性的特徵,然後在還原時額外將一些原始細節利用跳接添加回去。
先定義2個輔助函式:
conv2d()函式可以建立一個特殊的神經層,內涵結構如下:
a. 標準的2D卷積層
b. ReLU激活函數
c. 實例正規化(Instance normalization):分別對每個通道內的每個特徵圖做正規化,可以為風格轉換或圖像轉譯提供更高畫質的影像。
*特徵圖(Feature map):在卷積神經網路(Convolutional Neural Networks,CNN)中,整個卷積操作的流程是用濾鏡(filter)把上一層傳來的輸入資料從頭掃過一遍,濾鏡每移動一步,都會對所覆蓋的資料算出一個激活值(activation value),當掃完所有資料後,所有的激活值就能構成一張特徵圖,最後再把所有濾鏡輸出的特徵圖疊在一起,便能生出立體的輸出資料。
deconv2d()函式可以建立轉置卷積(transposed convolution)層:減少深度,增加寬度和高度。內涵結構如下:
a. 將輸入的特徵圖做升採樣,使用UpSampling2D
b. 可選擇是否使用丟棄法(當dropout rate不為0時就會使用)。
*dropout:在訓練過程中,藉由隨機捨棄部分神經元連接來防止過度擬合的正則化(regularization)技術。
下層的神經元會因為隨機捨棄一部分上層傳來的資料,而被迫減少它們與某些上層神經元之間的過度依賴性。
c. 一律套用實例正規化
d. 用跳接將本層與對應的降採樣區塊串聯起來
*藉由一層層跳接(skip connection),資訊就能以「保存較多特徵」的方式在神經網路中傳遞。
接著開始建立生成器:
建立輸入層將影像(128 X 128 X 3 )輸入到d0層。
將d0 ( 128 X 128 X 3) 輸入 conv2d,輸出 64 X 64 X 32 (d1)。
將d1 ( 64 X 64 X 32 ) 輸入conv2d,輸出 32 X 32 X 64 (d2)。
將d2 ( 32 X 32 X 64 ) 輸入conv2d,輸出 16 X 16 X 128 (d3)。
將d3 (16 X 16 X 128 )輸入conv2d,輸出 8 X 8 X 256 (d4)。
u1 :從 d4 做升採樣,並在d3 與 u1 之間做跳接。
u2 :從 u1 做升採樣,並在d2 與 u2 之間做跳接。
u3 :從 u2 做升採樣,並在d1 與 u3 之間做跳接。
u4 :直接做升採樣變成 128 X 128 X 64 影像
用標準2D卷積把多餘的特徵圖深度去掉,只留下 128 X 128 X 3 (高 X 寬 X 顏色通道 )
#建立生成器的method
def build_generator(self):
"""建立 U-Net 結構的生成器"""
def conv2d(layer_input , filters , f_size=4):
"""降採樣的神經層"""
d = Conv2D(filters , kernel_size=f_size , strides=2 , padding='same')(layer_input) #標準的2D卷積層
d=LeakyReLU(alpha=0.2)(d) #LeakyReLU激活函數
d=InstanceNormalization()(d) #實例正規化
return d
def deconv2d(layer_input ,skip_input, filters , f_size=4 , dropout_rate=0):
"""升採樣的神經層"""
u = UpSampling2D(size=2)(layer_input)
u = Conv2D(filters , kernel_size = f_size , strides=1 , padding='same' , activation='relu')(u)
if dropout_rate:
u = Dropout(dropout_rate)(u)
u = InstanceNormalization()(u)
u = Concatenate()([u,skip_input]) #跳接
return u
d0 = Input(shape=self.img_shape) #影像輸入層
#------------以下降採樣------------#
d1 = conv2d(d0 , self.gf)
d2 = conv2d(d1 , self.gf *2)
d3 = conv2d(d2 , self.gf *4)
d4 = conv2d(d3 , self.gf *8)
#------------以上降採樣------------#
#------------以下升採樣------------#
u1 = deconv2d(d4,d3,self.gf*4) #此函式會將d4及d3都接到u1上,因此d3會與u1跳接
u2 = deconv2d(u1,d2,self.gf*2)
u3 = deconv2d(u2,d1,self.gf)
#------------以上升採樣------------#
u4 = UpSampling2D(size=2)(u3)
output_img = Conv2D(self.channels , kernel_size=4 , strides=1 , padding='same' , activation='tanh')(u4)
return Model(d0 , output_img)
Conv2D之參數:
(過濾器數量 ,
kernel_size=卷積核大小,便於尋找中心點 ,
strides=滑動步長,計算滑動視窗時移動的格數 ,
padding=補零方式,卷積層取週邊kernel_size的滑動視窗時,若超越邊界時,是否要放棄這個output點(valid)、一律補零(same)、還是不計算超越邊界的Input值(causal)。
)
Leaky ReLU:激活函數,將輸入用一個不太大的非零斜率來轉換,以防止梯度在訓練過程中消失,進而改善訓練結果。
包含一個輔助函式,它可建立一個結合2D卷積層、LeakyReLU、實例正規化的神經層。
將輸入影像 ( 128 X 128 X 3) 傳遞到 d1 ( 64 X 64 X 61 ) 。
從 d1 ( 64 X 64 X 64 ) 傳遞到 d2 ( 32 X 32 X 128 )。
從 d2 ( 32 X 32 X 128 ) 傳遞到 d3 ( 16 X 16 X 256 )。
從 d3 ( 16 X 16 X 256 ) 傳遞到 d4 ( 8 X 8 X 512 ) 。
從 d4 (8 X 8 X 512 ) 用 conv2d 壓成 8 X 8 X 1 。
#建立鑑別器的method
def build_discriminator(self):
def d_layer(layer_input , filters , f_size =4 , normalization = True):
"""建立鑑別器的神經層"""
d = Conv2D(filters , kernel_size = f_size , strides=2 ,padding='same')(layer_input)
d = LeakyReLU(alpha=0.2)(d)
if normalization:
d = InstanceNormalization()(d)
return d
img = Input(shape=self.img_shape)
#------------以下降採樣------------#
d1 = d_layer(img , self.df , normalization=False)
d2 = d_layer(d1 , self.df*2)
d3 = d_layer(d2 , self.df*4)
d4 = d_layer(d3 , self.df*8)
#------------以上降採樣------------#
validity = Conv2D( 1 , kernel_size=4 , strides = 1 , padding='same')(d4)
return Model(img , validity)
for each 訓練迭代 do
1. 訓練鑑別器:
a.隨機從A、B域各取一小批次的真樣本:(imgsA與imgsB)
b.用生成器Gᴀʙ將imgsA轉譯到B域,用Gʙᴀ將imgsB轉譯到A域。
c.用鑑別器Dᴀ,分別計算原域影像與從B域轉譯來的影像造成的損失:Dᴀ(imgsA,1)與Dᴀ(Gʙᴀ(imgsB),0),然後將兩損失相加。Dᴀ小括號中的1與0是標籤。
d.用鑑別器Dʙ,分別計算原域影像與從A域轉譯來的影像造成的損失:Dʙ(imgsB,1)與Dʙ(Gᴀʙ(imgsA),0),然後將兩損失相加。Dʙ小括號中的1與0是標籤。
e.將步驟c與d的損失相加,得到鑑別器總損失。
2. 訓練生成器:
a.這裡使用組合模型:
*分別輸入A域(imgsA)與B域(imgsB)的影像
*輸出為:
1.B域轉譯為A域後,與A域的近似程度:Dᴀ(Gʙᴀ(imgsB))
2.A域轉譯為B域後,與B域的近似程度:Dʙ(Gᴀʙ(imgsA))
3.A域影像來回轉譯後的還原結果:Gʙᴀ(Gᴀʙ(imgsA))
4.B域影像來回轉譯後的還原結果:Gᴀʙ(Gʙᴀ(imgsB))
5.A域影像的特質映射:Gʙᴀ(imgsA)
6.B域影像的特質映射:Gᴀʙ(imgsB)
b.分別計算1及2的對抗損失、3及4的來回一致損失、5及6的特質損失,然後用這些損失來優化兩組生成器的參數。有兩種計算方式:
*MSE(均方誤差)用於前2項的鑑別器輸出機率,以計算對抗損失
*NAE(平均絕對誤差)用於後4項的輸出影像,以計算來回一致損失與特質損失
End for
#CycleGAN的訓練method
def train(self , epochs , batch_size =1 ,sample_interval=50):
start_time = datetime.datetime.now()
valid = np.ones((batch_size,) + self.disc_patch) #建立對抗損失的標籤
fake = np.zeros((batch_size,) + self.disc_patch) #建立對抗損失的標籤
for epoch in range(epochs):
for batch_i ,(imgs_A , imgs_B) in enumerate(self.data_loader.load_batch(batch_size)):
fake_B = self.g_AB.predict(imgs_A) #先把影像轉譯到另一個域
fake_A = self.g_BA.predict(imgs_B) #先把影像轉譯到另一個域
#------以下訓練鑑別器(原影像=real、轉譯影像=fake)----#
dA_loss_real = self.d_A.train_on_batch(imgs_A , valid)
dA_loss_fake = self.d_A.train_on_batch(fake_A , fake)
dA_loss = 0.5 * np.add(dA_loss_real , dA_loss_fake)
dB_loss_real = self.d_B.train_on_batch(imgs_B , valid)
dB_loss_fake = self.d_B.train_on_batch(fake_B , fake)
dB_loss = 0.5 * np.add(dB_loss_real , dB_loss_fake)
#------以上訓練鑑別器(原影像=real、轉譯影像=fake)----#
d_loss = 0.5*np.add(dA_loss , dB_loss) #鑑別器總損失
g_loss = self.combined.train_on_batch([imgs_A,imgs_B],
[valid , valid,
imgs_A , imgs_B,
imgs_A , imgs_B]) #訓練生成器
# If at save interval => save generated image samples
if batch_i % sample_interval == 0:
self.sample_images(epoch, batch_i)
elapsed_time = datetime.datetime.now() - start_time
# Plot the progress
print ("[Epoch %d/%d] [Batch %d/%d] [D loss: %f, acc: %3d%%] [G loss: %05f, adv: %05f, recon: %05f, id: %05f]" \
%( epoch,epochs,batch_i,self.data_loader.n_batches,d_loss[0],100*d_loss[1],g_loss[0],np.mean(g_loss[1:3]),np.mean(g_loss[3:5]),np.mean(g_loss[5:6])))
if __name__ == '__main__':
cycle_gan = CycleGAN()
cycle_gan.train(epochs=200, batch_size=64, sample_interval=10)
訓練第1次
訓練第50次
訓練第100次
訓練第200次
訓練第1次
訓練第50次
訓練第100次
訓練第200次