天天看點

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

前言

神經風格遷移一經提出,便引起了業界的巨大興趣,一些網站允許使用者上傳照片以進行風格遷移,甚至有一些網站将其用于商品銷售(例如“DIY數字油畫定制照片”等等)。

神經風格遷移

圖像可以分解為内容和風格,内容描述了圖像中的構成,例如圖像中的花草樹木,風格是指圖檔的細節,例如湖面的紋理和樹木的顔色。在一天的不同時間同一建築的照片具有不同的色調和亮度,可以被視為具有相同的内容但風格不同。

在Gatys等人發表的論文中,使用CNN将一幅圖像的藝術風格轉移到另一幅圖像:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

與大多數需要大量訓練資料的深度學習模型不同,神經風格遷移僅需要兩個圖像——内容圖像和樣式圖像。可以使用經過訓練的CNN(例如VGG)将風格從風格圖像遷移到内容圖像上。

如上圖所示,(A)是内容圖像,(B)–(D)展示了是風格圖像和風格化後的内容圖像,結果令人驚異!有些人甚至使用該算法來創作和出售藝術品。有些網站和應用程式可以上傳照片來進行風格遷移,而無需了解底層的原理,但作為技術人員,我們當然希望自己實作此模型。

使用VGG提取特征

分類器CNN可以分為兩部分:第一部分稱為特征提取器 (feature extractor),主要由卷積層組成;後一部分由幾個全連接配接層組成,輸出類機率得分,稱為分類器頭 (classifier head)。在ImageNet上為分類任務預先訓練的CNN也可以用于其他任務,這就是所謂的遷移學習 (transfer learning),我們可以轉移或重用一些學到的知識到新的網絡或應用中。

在CNN中,圖像重建的兩個步驟如下:

1. 通過CNN向前計算圖像以提取特征。

2. 使用随機初始化的輸入,并進行訓練,以便其重建與步驟1中的參考特征最比對的特征。

在正常的網絡訓練中,輸入圖像是固定的,并且使用反向傳播的梯度來更新網絡權重。在神經風格遷移中,所有網絡層都被當機,而我們使用梯度來修改輸入。在原始的論文使用的是 VGG19, Keras 有一個可以使用的預訓練模型。VGG 的特征提取器由五個塊組成,每個塊的末尾都有一個下采樣。每個塊都有2~4個卷積層,整個 VGG19 具有 16 個卷積層和 3 個全連接配接層。

在下文中,我們将實作内容重構,同時将其擴充以執行風格遷移。以下是使用預訓練的 VGG 提取 block4_conv2 的輸出層的代碼:

# 因為我們隻需要提取特征,是以在執行個體化VGG模型時使用include_top = False當機網絡參數
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
content_layers = ['block4_conv2']
content_outputs = [vgg.get_layer(x).output for x in content_layers]
model = Model(vgg.input, content_outputs)      

預訓練的 Keras CNN 模型分為兩部分。底部由卷積層組成,通常稱為特征提取器,而頂部是由全連接配接層組成的分類器頭。因為我們隻想提取特征而不關心分類器,是以在執行個體化VGG模型時将設定 include_top = False。

圖像加載

首先需要加載内容圖像和風格圖像:

def scale_image(image):
    MAX_DIM = 512
    scale = np.max(image.shape)/MAX_DIM
    print(image.shape)
    new_shape = tf.cast(image.shape[:2]/scale, tf.int32)
    image = tf.image.resize(image, new_shape)
    return image
content_image = scale_image(np.asarray(Image.open('7.jpg')))
style_image = scale_image(np.asarray(Image.open('starry-night.jpg')))      

VGG預處理

Keras 預訓練模型期望輸入圖像的 BGR 範圍為 [0, 255] 。是以,第一步是反轉顔色通道,以将 RGB 轉換為 BGR。 VGG 對不同的顔色通道使用不同的平均值,可以使用 tf.keras.applications.vgg19.preprocess_input() 進行預處理,在 preprocess_input() 内部,分别為B,G和R通道的像素值減去 103.939、116.779 和 123.68。

以下是前向計算代碼,在對圖像進行前向計算之前先對其進行預處理,然後再将其輸入模型以傳回内容特征。然後,我們提取内容特征并将其用作我們的目标:  

def extract_features(image):
    image = tf.keras.applications.vgg19。preprocess_input(image *255.)
    content_ref = model(image)
    return content_ref
content_image = tf.reverse(content_image, axis=[-1])
content_ref = extract_features(content_image)      

在代碼中,由于圖像已标準化為 [0., 1.],是以我們需要通過将其乘以 255 将其恢複為 [0.,255.]。然後建立一個随機初始化的輸入,該輸入也将成為風格化的圖像:

image = tf.Variable(tf.random.normal( shape=content_image.shape))      

接下來,我們将使用反向傳播從内容特征中重建圖像。

重建内容

在訓練步驟中,我們将圖像饋送到當機的 VGG 中以提取内容特征,然後使用$L_2$損失針對目标内容特征進行度量,用于計算每個特征層的L2損失:

def calc_loss(y_true, y_pred):
    loss = [tf.reduce_sum((x-y)**2) for x, y in zip(y_pred, y_true)]
    return tf.reduce_mean(loss)      

使用 tf.GradientTape() 計算梯度。在正常的神經網絡訓練中,将梯度更新應用于可訓練變量,即神經網絡的權重。但是,在神經風格遷移中,将梯度應用于圖像。之後,将圖像值剪裁在 [0., 1.] 之間,如下所示:

for i in range(1,steps+1):
    with tf.GradientTape() as tape:
        content_features = self.extract_features(image)
        loss = calc_loss(content_features, content_ref)
    grad = tape.gradient(loss, image)
    optimizer.apply_gradients([(grad, image)])
    image.assign(tf.clip_by_value(image, 0., 1.))      

使用 block1_1 重建圖像,訓練了 2000 步後,得到重構後的内容圖像:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

使用 block4_1 重建圖像,訓練了 2000 步後,得到重構後的内容圖像:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

可以看到使用層 block4_1 時,開始丢失細節,例如樹葉的形狀。當我們使用 block5_1 時,我們看到幾乎所有細節都消失了,并充滿了一些随機噪聲:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

如果我們仔細觀察,樹葉的結構和邊緣仍然得到保留,并在其應有的位置。現在,我們已經提取了内容,提取内容特征後,下一步是提取樣式特征。

用Gram矩陣重建風格

在内容重建中可以看出,特征圖(尤其是前幾層)既包含風格又包含内容。那麼我們如何從圖像中提取風格特征呢?方法是使用 Gram 矩陣,該矩陣可計算不同濾波器響應之間的相關性。假設卷積層1的激活形狀為 (H, W, C),其中 H 和 W 是空間尺寸,C 是通道數,等于濾波器的數量,每個濾波器檢測不同的圖像特征。

當具有一些共同的特征(例如顔色和邊緣)時,則認為它們具有相同的紋理。例如,如果我們将草地的圖像輸入到卷積層中,則檢測垂直線和綠色的濾波器将在其特征圖中産生更大的響應。是以,我們可以使用特征圖之間的相關性來表示圖像中的紋理。

要通過形狀為 (H, W, C) 的激活來建立 Gram 矩陣,我們首先将其重塑為 C 個向量。每個向量都是大小為 H×W 的一維特征圖。對 C 個向量執行點積運算,以獲得對稱的 C×C Gram 矩陣。在 TensorFlow 中計算 Gram 矩陣的詳細步驟如下:

1. 使用 tf.squeeze() 将批尺寸 (1, H, W, C) 修改為 (H, W, C);

2. 轉置張量以将形狀從 (H, W, C) 轉換為 (C, H, W);

3. 将最後兩個次元展平為 (C, H×W);

4. 執行特征的點積以建立形狀為 (C, C) 的 Gram 矩陣;

5. 通過将矩陣除以每個展平的特征圖中的元素數 (H×W) 進行歸一化。

計算 Gram 矩陣的代碼如下:

def gram_matrix(x):
    x = tf.transpose(tf.squeeze(x), (2,0,1));
    x = tf.keras.backend.batch_flatten(x)
    num_points = x.shape[-1]
    gram = tf.linalg.matmul(x, tf.transpose(x))/num_points
    return gram      

可以使用此函數為指定的樣式層的每個 VGG 層擷取 Gram 矩陣。然後,我們對來自目标圖像和參考圖像的 Gram 矩陣使用 L2 損失。損失函數與内容重建相同。建立 Gram 矩陣清單的代碼如下:

def extract_features(image):
    image = tf.keras.applications.vgg19.preprocess_input(image *255.)
    styles = self.model(image)
    styles = [self.gram_matrix(s) for s in styles]
    return styles      

以下圖像是從不同 VGG 圖層的風格特征中重構得到的:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

在從 block1_1 重建的風格圖像中,内容資訊完全消失,僅顯示高頻紋理細節。較高的層 block3_1,顯示了一些卷曲的形狀:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

這些形狀捕獲了輸入圖像中風格的較高層次。Gram 矩陣的損失函數是平方誤差之和而不是均方誤差。是以,層次風格較高的層具有較高的固有權重。這允許傳輸更進階的風格表示形式,例如筆觸。如果使用均方誤差,則低層次的風格特征(例如紋理)将在視覺上更加突出,并且可能看起來像高頻噪聲。

實作神經風格轉換

現在,我們可以合并内容和風格重構中的代碼,以執行神經樣式轉移。

我們首先建立一個模型,該模型提取兩個特征塊,一個用于内容,另一個用于樣式。内容重建使用 block5_conv1 層,從 block1_conv1 到 block5_conv1 的五層用于捕獲來自不同層次結構的風格,如下所示:  

vgg = tf.keras.applications.VGG19(include_top=False,   weights='imagenet')
default_content_layers = ['block5_conv1']
default_style_layers = ['block1_conv1',
                        'block2_conv1',
                        'block3_conv1',
                        'block4_conv1',
                        'block5_conv1']
content_layers = content_layers if content_layers else default_content_layers
style_layers = style_layers if style_layers else default_style_layers
self.content_outputs = [vgg.get_layer(x).output for x in content_layers]
self.style_outputs = [vgg.get_layer(x).output for x in style_layers]
self.model = Model(vgg.input, [self.content_outputs, self.style_outputs])      

在訓練循環開始之前,我們從各自的圖像中提取内容和風格特征以用作目标。雖然我們可以使用随機初始化的輸入來進行内容和風格重建,但從内容圖像開始進行訓練會更快:

content_ref, _ = self.extract_features(content_image)
_, style_ref = self.extract_features(style_image)      

然後,我們計算并添加内容和風格損失:

def train_step(self, image, content_ref, style_ref):
    with tf.GradientTape() as tape:
        content_features, style_features = self.extract_features(image)
        content_loss = self.content_weight * self.calc_loss(content_ref, content_features)
        style_loss = self.style_weight*self.calc_loss( style_ref, style_features)
        loss = content_loss + style_loss
    grad = tape.gradient(loss, image)
    self.optimizer.apply_gradients([(grad, image)])
    image.assign(tf.clip_by_value(image, 0., 1.))
    return content_loss, style_loss      

效果展示

以下是使用不同權重和内容層生成的4個風格化圖像:

TensorFlow2實作神經風格遷移,DIY數字油畫定制照片前言神經風格遷移使用VGG提取特征實作神經風格轉換效果展示

可以通過更改權重和層以建立所需的樣式。

當然此模型也存在産生一張圖檔需要幾分鐘的時間的缺點,不能做到實時遷移,對于相關改進模型将在之後進行探讨。

繼續閱讀