Python 深度學習之神經風格遷移

CDFMLR 2021-08-15 21:13:21 阅读数:979

本文一共[544]字,预计阅读时长:1分钟~
python 深度 之神

這是我參與8月更文挑戰的第15天,活動詳情查看:8月更文挑戰

Deep Learning with Python

這篇文章是我學習《Deep Learning with Python》(第二版,François Chollet 著) 時寫的系列筆記之一。文章的內容是從 Jupyter notebooks 轉成 Markdown 的,你可以去 GitHubGitee 找到原始的 .ipynb 筆記本。

你可以去這個網站在線閱讀這本書的正版原文(英文)。這本書的作者也給出了配套的 Jupyter notebooks

本文為 第8章 生成式深度學習 (Chapter 8. Generative deep learning) 的筆記之一。

神經風格遷移

8.3 Neural style transfer

神經風格遷移(neural style transfer),基於深度學習的神經網絡,將參考圖像的風格應用於目標圖像,同時保留目標圖像的內容,創造出新的圖像。

一個風格遷移的示例

神經風格遷移的思想很簡單:定義一個損失函數來指定要實現的目標,然後將這個損失最小化。這裏的目標就是保存原始圖像的內容,同時采用參考圖像的風格。

假設有函數 content 和 style 分別可以計算出輸入圖像的內容和風格,以及有範式函數 distance,則神經風格遷移的損失可以錶達為:

loss = distance(content(original_image) - content(generated_image)) +
distance(style(reference_image) - style(generated_image))
複制代碼

事實上,利用深度卷積神經網絡,是可以從數學上定義 style 和 content 函數的。

損失定義

  1. 內容損失

卷積神經網絡靠底部(前面)的層激活包含關於圖像的局部信息,靠近頂部(後面)的層則包含更加全局、抽象的信息。內容就是圖像的全局、抽象的信息,所以可以用卷積神經網絡靠頂部的層激活來錶示圖像的內容。

因此,給定一個預訓練的卷積神經網絡,選定一個靠頂部的層,內容損失可以使用「該層在目標圖像上的激活」和「該層在生成圖像上的激活」之間的 L2 範數。

  1. 風格損失

不同於內容只用一個層即可錶達,風格需要多個層才能定義。風格是多種方面的,比如筆觸、線條、紋理、顏色等等,這些內容會出現在不同的抽象程度上。所以風格的錶達就需要捕捉所有空間尺度上提取的外觀,而不僅僅是在單一尺度上。

在這種思想下,風格損失的錶達,可以借助於層激活的 Gram 矩陣。這個 Gram 矩陣就是某一層的各個特征圖的內積,錶達了層的特征間相互關系(correlation)的映射,它就對應於這個尺度上找到的紋理(texture)的外觀。而在不同的層激活內保存相似的內部相互關系,就可以認為是“風格”了。

那麼,我們就可以用生成圖像和風格參考圖像在不同層上保持的紋理,來定義風格損失了。

神經風格遷移的 Keras 實現

神經風格遷移可以用任何預訓練卷積神經網絡來實現,這裏選用 VGG19。

神經風格遷移的步驟如下:

  1. 創建一個網絡,同時計算風格參考圖像、目標圖像和生成圖像的 VGG19 層激活;
  2. 使用這三張圖像上計算的層激活來定義之前所述的損失函數;
  3. 梯度下降來將這個損失函數最小化。

這個例子需要關閉 Tensorflow 2.x 的及時執行模式:

import tensorflow as tf
tf.compat.v1.disable_eager_execution()
複制代碼

在開始構建網絡前,先定義風格參考圖像和目標圖像的路徑。如果圖像尺寸差异很大,風格遷移會比較困難,所以這裏我們還統一定義一下尺寸:

from tensorflow.keras.preprocessing.image import load_img, img_to_array
target_image_path = '/home/CDFMLR/img/portrait.jpg'
style_referencce_image_path = 'img/transfer_style_reference.jpg'
width, height = load_img(target_image_path).size
img_height = 400
img_width = width * img_height // height
複制代碼

這裏圖片我選擇了:

  • transfer_style_reference: 文森特·梵高《麥田裏的絲柏樹》(A Wheatfield, with Cypresses),1889年,收藏於紐約大都會博物館。
  • portrait: 保羅·高更《不列塔尼牧人》(The Swineherd, Brittany),1888年,收藏於美國加州洛杉磯郡立美術館。

麥田裏的絲柏樹與不列塔尼牧人

接下來,我們需要一些輔助函數,用於圖像的加載、處理。

import numpy as np
from tensorflow.keras.applications import vgg19
def preprocess_image(image_path):
img = load_img(image_path, target_size=(img_height, img_width))
img = img_to_array(img)
img = np.expand_dims(img, axis=0)
img = vgg19.preprocess_input(img)
return img
def deprocess_image(x):
# vgg19.preprocess_input 會减去ImageNet的平均像素值,使其中心為0。這裏做逆操作:
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.680
# BGR -> RGB
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype('uint8')
return x
複制代碼

下面構建 VGG19 網絡:接收三張圖像的 batch 作為輸入,三張圖像分別是風格參考圖像、 目標圖像的 constant 和一個用於保存生成圖像的 placeholder。

from tensorflow.keras import backend as K
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image = K.constant(preprocess_image(style_referencce_image_path))
combination_image = K.placeholder((1, img_height, img_width, 3))
input_tensor = K.concatenate([target_image,
style_reference_image,
combination_image], axis=0)
model = vgg19.VGG19(input_tensor=input_tensor,
weights='imagenet',
include_top=False)
複制代碼

定義內容損失,保證目標圖像和生成圖像在網絡頂層的結果相似:

def content_loss(base, combination):
return K.sum(K.square(combination - base))
複制代碼

然後是風格損失,計算輸入矩陣的 Gram 矩陣,借助用 Gram 矩陣計算風格損失:

def gram_matrix(x):
features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
gram = K.dot(features, K.transpose(features))
return gram
def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_height * img_width
return K.sum(K.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))
複制代碼

這裏我們再額外定義一個「總變差損失」(total variation loss),促使生成圖像具有空間連續性,避免結果過度像素化,相當於一個正則化。

def total_variation_loss(x):
a = K.square(
x[:, :img_height - 1, :img_width - 1, :] -
x[:, 1:, :img_width - 1, :])
b = K.square(
x[:, :img_height - 1, :img_width - 1, :] -
x[:, :img_height - 1, 1:, :])
return K.sum(K.pow(a + b, 1.25))
複制代碼

現在考慮具體的損失計算:在計算內容損失時,我們需要一個靠頂部的層;對於風格損失,我們需要使用一系列層,既包括頂層也包括底層;最後還需要添加總變差損失。最終的損失就是這三類損失的加權平均。

定義需要最小化的最終損失:

outputs_dict = {layer.name: layer.output for layer in model.layers}
content_layer = 'block5_conv2'
style_layers = [f'block{i}_conv1' for i in range(1, 6)]
total_variation_weight = 1e-4
style_weight = 1.0
content_weight = 0.025 # content_weight越大,目標內容更容易在生成圖像中越容易識別
# 內容損失
loss = K.variable(0.)
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(target_image_features, combination_features)
# 風格損失
for layer_name in style_layers:
layer_features = outputs_dict[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss = loss + (style_weight / len(style_layers)) * sl
# 總變差損失
loss = loss + total_variation_weight * total_variation_loss(combination_image)
複制代碼

最後就是梯度下降過程了。這裏調用 scipy,用 L-BFGS 算法進行最優化。

為了快速計算,我們創建一個 Evaluator 類,同時計算損失值和梯度值,在第一次調用時會返回損失值,同時緩存梯度值用於下一次調用。

grads = K.gradients(loss, combination_image)[0]
fetch_loss_and_grads = K.function([combination_image], [loss, grads])
class Evaluator(object):
def __init__(self):
self.loss_value = None
self.grads_values = None
def loss(self, x):
assert self.loss_value is None
x = x.reshape((1, img_height, img_width, 3))
outs = fetch_loss_and_grads([x])
loss_value = outs[0]
grad_values = outs[1].flatten().astype('float64')
self.loss_value = loss_value
self.grads_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grads_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
複制代碼

最後的最後,調用 SciPy 的 L-BFGS 算法來運行梯度上昇過程,每一次迭代(20 步梯度上昇)後都保存當前的生成圖像:

from scipy.optimize import fmin_l_bfgs_b
from imageio import imsave
import time
iterations = 20
def result_fname(iteration):
return f'results/result_at_iteration_{iteration}.png'
x = preprocess_image(target_image_path)
x = x.flatten()
for i in range(iterations):
print('Start of iteration', i)
start_time = time.time()
x, min_val, info = fmin_l_bfgs_b(evaluator.loss,
x,
fprime=evaluator.grads,
maxfun=20)
print(' Current loss value:', min_val)
img = x.copy().reshape((img_height, img_width, 3))
img = deprocess_image(img)
fname = result_fname(i)
imsave(fname, img)
print(' Image saved as', fname)
end_time = time.time()
print(f' Iteration {i} completed in {end_time - start_time} s')
複制代碼

輸出信息:

Start of iteration 0
Current loss value: 442468450.0
Image saved as results/result_at_iteration_0.png
Iteration 0 completed in 177.57321500778198 s
...
Start of iteration 19
Current loss value: 44762796.0
Image saved as results/result_at_iteration_19.png
Iteration 19 completed in 177.95070385932922 s
複制代碼

在完成之後,我們把生成的結果和原圖放在一起比較一下:

結果1

嗯,還是有意思的!

再玩一次 再看一個例子:

風格參考還是用梵高的《麥田裏的絲柏樹》,內容用米勒的《拾穗者》(Des glaneuses,1857年,巴黎奧塞美術館)。

比較有意思的是,梵高本人畫過一幅部分模仿《拾穗者》的《夕陽下兩比特農婦開掘積雪覆蓋的田地》(Zwei grabende Bäuerinnen auf schneebedecktem Feld):

結果2

可以看到,我們的機器只是簡單粗暴的風格遷移,而大師本人會在模仿中再創作。

最後,補充一點。這個風格遷移算法的運行比較慢,但足够簡單。要實現快速風格遷移,可以考慮:

  • 首先利用這裏介紹的方法,固定一張風格參考圖像,給不同的內容圖像,生成一大堆「輸入-輸出」訓練樣例
  • 拿這些「輸入-輸出」去訓練一個簡單的卷積神經網絡來學習這個特定風格的變換(輸入->輸出)。
  • 完成之後,對一張圖像進行特定風格的遷移就非常快了,做一次前向傳遞就完成了。
版权声明:本文为[CDFMLR]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815211248049v.html