文 / 軟體工程實習生 Raymond Yuan
來源 | TensorFlow 公眾號
在本教程中,我們將學習如何使用深度學習來以另一幅影象的風格創作影象(有沒有希望自己可以像畢加索或梵高一樣作畫?)。這就是我們所說的神經風格遷移!Leon A. Gatys 在其論文《一種藝術風格的神經網路演算法》(A Neural Algorithm of Artistic Style)中概要介紹了這項技術,文章非常值得一讀,千萬不要錯過。 注:教程連結 colab.research.google.com/github/tens…
神經風格遷移是一項優化技術,可用於選取三幅影象,即一幅內容影象、一幅風格參考影象(例如一幅名家作品),以及您想要設定風格的輸入影象,然後將它們融合在一起,這樣輸入影象轉化後就會看起來與內容影象相似,但其呈現的是風格影象的風格。
例如,我們選取一張烏龜的影象和 Katsushika Hokusai 的 《神奈川衝浪裡》:
P. Lindgren 拍攝的《綠海龜》,影象來自 Wikimedia Commons如果 Hokusai 決定將他作品中海浪的紋理或風格新增到海龜影象中,這幅圖看起來會是什麼樣?會不會是這樣?
這是魔法嗎?又或者只是深度學習?幸運的是,這和魔法沒有任何關係:風格遷移是一項好玩又有趣的技術,可以展現神經網路的能力和內部表現形式。
神經風格遷移的原理是定義兩個距離函式,一個描述兩幅影象的不同之處,即 Lcontent 函式,另一個描述兩幅影象的風格差異,即 Lstyle 函式。然後,給定三幅影象,一幅所需的風格影象、一幅所需的內容影象,還有一幅輸入影象(用內容影象進行初始化)。我們努力轉換輸入影象,藉助內容影象將內容距離最小化,並藉助風格影象將風格距離最小化。
簡而言之,我們會選取基本輸入影象、我們想要匹配的內容影象以及想要匹配的風格影象。我們將使用反向傳播演算法最小化內容和風格距離(損失),以轉換基本輸入影象,建立與內容影象的內容和風格影象的風格相匹配的影象。
下文要提及的特定概念有:
在此過程中,我們會圍繞下列概念積累實際經驗,形成直覺認識:
-
Eager Execution — 使用 TensorFlow 的指令式程式設計環境,該環境可以立即評估操作
-
瞭解更多有關 Eager Execution 的資訊
-
檢視動態教程(許多教程都可以在 Colaboratory 中執行)
-
使用功能 API 來定義模型 — 我們會構建一個模型的子集,由其賦予我們使用功能 API 訪問必要的中間啟用的許可權
-
利用預訓練模型的特徵圖 — 學習如何使用預訓練模型及其特徵圖
-
建立自定義訓練迴圈 — 我們會研究如何設定優化器,以最小化輸入引數的既定損失
我們會按照下列常規步驟來進行風格遷移:
-
視覺化資料
-
對我們的資料進行基本的預處理/準備
-
設定損失函式
-
建立模型
-
優化損失函式
受眾:
此篇博文面向熟知機器學習基本概念的中級使用者。要充分利用此博文,您需要:
閱讀 Gaty 的論文 — 我們會有全程講解,但這篇論文有助您更加透徹地理解這一任務 瞭解梯度下降法 注:Gaty 的論文連結 arxiv.org/abs/1508.06… 瞭解梯度下降法連結 developers.google.com/machine-lea…
預計所需時間:60 分鐘
程式碼: 您可以點選此連結獲取本文中的完整程式碼。如想單步除錯此示例,您可以點選此處,開啟 Colab。 注:此連結 github.com/tensorflow/… 此處連結 colab.research.google.com/github/tens…
實現
首先,我們要啟用 Eager Execution。藉助 Eager Execution,我們可以最清晰易讀的方式學習這項技術
1 tf.enable_eager_execution()
2 print("Eager execution: {}".format(tf.executing_eagerly()))
3
4 Here are the content and style images we will use:
5 plt.figure(figsize=(10,10))
6
7 content = load_img(content_path).astype('uint8')
8 style = load_img(style_path)
9
10 plt.subplot(1, 2, 1)
11 imshow(content, 'Content Image')
12
13 plt.subplot(1, 2, 2)
14 imshow(style, 'Style Image')
15 plt.show()
複製程式碼
P. Lindgren 拍攝的《綠海龜》圖,影象來自 Wikimedia Commons,以及 Katsushika Hokusai 創作的《神奈川衝浪裡》,影象來自公共領域
定義內容和風格表徵
為了獲取我們影象的內容和風格表徵,我們先來看看模型內的一些中間層。中間層代表著特徵圖,這些特徵圖將隨著您的深入變得越來越有序。在本例中,我們會使用 VGG19 網路架構,這是一個預訓練影象分類網路。要定義我們影象的內容和風格表徵,這些中間層必不可少。對於輸入影象,我們會努力匹配這些中間層的相應風格和內容的目標表徵。
為什麼是中間層?
您可能會好奇,為什麼預訓練影象分類網路中的中間輸出允許我們定義風格和內容表徵。從較高的層面來看,我們可以通過這樣的事實來解釋這一現象,即網路必須要理解影象才能執行影象分類(我們的網路已接受過這樣的訓練)。這包括選取原始影象作為輸入畫素,並通過轉換構建內部表徵,轉換就是將原始影象畫素變為對影象中所呈現特徵的複雜理解。這也可以部分解釋卷積神經網路為何能夠很好地概括影象:它們能夠捕捉不同類別的不變性,並定義其中的特徵(例如貓與狗),而且不受背景噪聲和其他因素的影響。因此,在輸入原始影象和輸出類別標籤之間的某個位置,模型發揮著複雜特徵提取器的作用。通過訪問中間層,我們可以描述輸入影象的內容和風格。
具體而言,我們會從我們的網路中抽取出這些中間層:
1 # Content layer where will pull our feature maps
2 content_layers = ['block5_conv2']
3
4 # Style layer we are interested in
5 style_layers = ['block1_conv1',
6 'block2_conv1',
7 'block3_conv1',
8 'block4_conv1',
9 'block5_conv1'
10 ]
11
12 num_content_layers = len(content_layers)
13 num_style_layers = len(style_layers)
複製程式碼
模型
在本例中,我們將載入 VGG19,並將輸入張量輸入模型中。這樣,我們就可以提取內容影象、風格影象和所生成影象的特徵圖(隨後提取內容和風格表徵)。
依照論文中的建議,我們使用 VGG19 模型。此外,由於 VGG19 是一個較為簡單的模型(與 ResNet、Inception 等模型相比),其特徵圖實際更適用於風格遷移。
為了訪問與我們的風格和內容特徵圖相對應的中間層,我們需要使用 Keras 功能 API 來獲取相應的輸出,從而使用所需的輸出啟用定義我們的模型。
藉助功能 API,定義模型時僅需定義輸入和輸出即可:model = Model(inputs, outputs)。
1 def get_model():
2 """ Creates our model with access to intermediate layers.
3
4 This function will load the VGG19 model and access the intermediate layers.
5 These layers will then be used to create a new model that will take input image
6 and return the outputs from these intermediate layers from the VGG model.
7 Returns:
8 returns a keras model that takes image inputs and outputs the style and
9 content intermediate layers.
10 """
11 # Load our model. We load pretrained VGG, trained on imagenet data (weights=’imagenet’)
12 vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet')
13 vgg.trainable = False
14 # Get output layers corresponding to style and content layers
15 style_outputs = [vgg.get_layer(name).output for name in style_layers]
16 content_outputs = [vgg.get_layer(name).output 17 for name in content_layers]
18 model_outputs = style_outputs + content_outputs
19 # Build model
20 return models.Model(vgg.input, model_outputs)
複製程式碼
在上圖的程式碼片段中,我們將載入預訓練影象分類網路。然後,我們會抓取此前定義的興趣層。之後,我們將定義一個模型,將模型的輸入設定為影象,將輸出設定為風格層和內容層的輸出。換言之,我們建立的模型將接受輸入影象並輸出內容和風格中間層!
定義和建立我們的損失函式(內容和風格距離)
內容損失: 我們的內容損失定義實際上相當簡單。我們將向網路傳遞所需的內容影象和基本輸入影象,這樣,我們的模型會返回中間層輸出(自上文定義的層)。然後,我們只需選取這些影象的兩個中間表徵之間的歐氏距離。
更正式地講,內容損失是一個函式,用於描述內容與我們的輸入影象 x 和內容影象 p 之間的距離。設 Cₙₙ 為預訓練深度卷積神經網路。再次強調,我們在本例中使用 VGG19。設 X 為任意影象,則 Cₙₙ(x) 為 X 饋送的網路。用 Fˡᵢⱼ(x)∈ Cₙₙ(x) 和 Pˡᵢⱼ(x) ∈ Cₙₙ(x) 分別描述網路在 l 層上輸入為 x 和 p 的中間層表徵。之後,我們可以將內容距離(損失)正式描述為:
我們以常規方式執行反向傳播演算法,以便將內容損失降至最低。這樣,我們可以更改初始影象,直至其在某個層(在 content_layer 中定義)中生成與原始內容影象相似的響應。
該操作非常容易實現。同樣地,在我們的輸入影象 x 和內容影象 p 饋送的網路中,其會將 L 層的輸入特徵圖視為輸入影象,然後返回內容距離。
1 def get_content_loss(base_content, target):
2 return tf.reduce_mean(tf.square(base_content - target))
複製程式碼
風格損失:
計算風格損失時涉及的內容較多,但遵循相同的原則,這次我們要為網路提供基本輸入影象和風格影象。但我們要比較的是這兩個輸出的格拉姆矩陣,而非基本輸入影象和風格影象的原始中間輸出。
在數學上,我們將基本輸入影象 x 和風格影象 a 的風格損失描述為這些影象的風格表徵(格拉姆矩陣)之間的距離。我們將影象的風格表徵描述為由格拉姆矩陣 Gˡ 給定的不同過濾響應間的相關關係,其中 Gˡᵢⱼ 為 l 層中向量化特徵圖 i 和 j 之間的內積。我們可以看到,針對特定影象的特徵圖生成的 Gˡᵢⱼ 表示特徵圖 i 和 j 之間的相關關係。
要為我們的基本輸入影象生成風格,我們需要對內容影象執行梯度下降法,將其轉換為與原始影象的風格表徵匹配的影象。我們通過最小化風格影象與輸入影象的特徵相關圖之間的均方距離來進行此項操作。每層對總風格損失的貢獻用以下公式描述
其中 Gˡᵢⱼ 和 Aˡᵢⱼ 分別為輸入影象 x 和風格影象 a 在 l 層的風格表徵。Nl 表示特徵圖的數量,每個圖的大小為 Ml= 高度 ∗ 寬度。因此,每層的總風格損失為
其中,我們用係數 wl 來衡量每層損失的貢獻。在這個例子中,我們平均地衡量每個層:
這實施起來很簡單:
1 def gram_matrix(input_tensor):
2 # We make the image channels first
3 channels = int(input_tensor.shape[-1])
4 a = tf.reshape(input_tensor, [-1, channels])
5 n = tf.shape(a)[0]
6 gram = tf.matmul(a, a, transpose_a=True)
7 return gram / tf.cast(n, tf.float32)
8
9 def get_style_loss(base_style, gram_target):
10 """Expects two images of dimension h, w, c"""
11 # height, width, num filters of each layer
12 height, width, channels = base_style.get_shape().as_list()
13 gram_style = gram_matrix(base_style)
14 return tf.reduce_mean(tf.square(gram_style - 15 gram_target))
複製程式碼
執行梯度下降法
如果您對梯度下降法/反向傳播演算法不熟悉,或需要複習一下,那您一定要檢視此資源。
在本例中,我們使用 Adam 優化器來最小化我們的損失。我們迭代更新輸出影象,以最大限度地減少損失:我們不是更新與網路有關的權重,而是訓練我們的輸入影象以使損失最小化。為此,我們必須知道如何計算損失和梯度。請注意,我們推薦使用 L-BFGS 優化器(如果您熟悉此演算法的話),但本教程並未使用該優化器,因為本教程旨在闡述使用 Eager Execution 的最佳實踐。通過使用 Adam,我們可以藉助自定義訓練迴圈來說明 autograd/梯度帶的功能。
計算損失和梯度
我們會定義一些輔助函式,這些函式會載入我們的內容和風格影象,通過網路將它們向前饋送,然後從我們的模型輸出內容和風格的特點表徵。
1 def get_feature_representations(model, content_path, style_path):
2 """Helper function to compute our content and style feature representations.
3
4 This function will simply load and preprocess both the content and style
5 images from their path. Then it will feed them through the network to obtain
6 the outputs of the intermediate layers.
7
8 Arguments:
9 model: The model that we are using.
10 content_path: The path to the content image. 11 style_path: The path to the style image
12
13 Returns:
14 returns the style features and the content features.
15 """
16 # Load our images in
17 content_image = load_and_process_img(content_path)
18 style_image = load_and_process_img(style_path)
19
20 # batch compute content and style features
21 stack_images = np.concatenate([style_image, content_image], axis=0)
22 model_outputs = model(stack_images)
23 # Get the style and content feature representations from our model
24
25 style_features = [style_layer[0] for style_layer in model_outputs[:num_style_layers]]
26 content_features = [content_layer[1] for content_layer in model_outputs[num_style_layers:]]
27 return style_features, content_features
複製程式碼
這裡我們使用 tf.GradientTape 來計算梯度。這樣,我們可以通過追蹤操作來利用可用的自動微分,以便之後計算梯度。它會記錄正向傳遞期間的操作,並能夠計算關於向後傳遞的輸入影象的損失函式梯度。
1 def compute_loss(model, loss_weights, init_image, gram_style_features, content_features):
2 """This function will compute the loss total loss.
3
4 Arguments:
5 model: The model that will give us access to the intermediate layers
6 loss_weights: The weights of each contribution of each loss function.
7 (style weight, content weight, and total variation weight)
8 init_image: Our initial base image. This image is what we are updating with
9 our optimization process. We apply the gradients wrt the loss we are
10 calculating to this image.
11 gram_style_features: Precomputed gram matrices corresponding to the
12 defined style layers of interest.
13 content_features: Precomputed outputs from defined content layers of
14 interest.
15
16 Returns:
17 returns the total loss, style loss, content loss, and total variational loss
18 """
19 style_weight, content_weight, total_variation_weight = loss_weights
20
21 # Feed our init image through our model. This will give us the content and
22 # style representations at our desired layers. Since we're using eager
23 # our model is callable just like any other function!
24 model_outputs = model(init_image)
25
26 style_output_features = model_outputs[:num_style_layers]
27 content_output_features = model_outputs[num_style_layers:]
28
29 style_score = 0
30 content_score = 0
31
32 # Accumulate style losses from all layers
33 # Here, we equally weight each contribution of each loss layer
34 weight_per_style_layer = 1.0 / float(num_style_layers)
35 for target_style, comb_style in zip(gram_style_features, style_output_features):
36 style_score += weight_per_style_layer * get_style_loss(comb_style[0], target_style)
37
38 # Accumulate content losses from all layers
39 weight_per_content_layer = 1.0 / float(num_content_layers)
40 for target_content, comb_content in zip(content_features, content_output_features):
41 content_score += weight_per_content_layer* get_content_loss(comb_content[0], target_content)
42
43 style_score *= style_weight
44 content_score *= content_weight
45 total_variation_score = total_variation_weight * total_variation_loss(init_image)
46
47 # Get total loss
48 loss = style_score + content_score + total_variation_score
49 return loss, style_score, content_score, total_variation_score
複製程式碼
然後計算梯度就很簡單了:
1 def compute_grads(cfg):
2 with tf.GradientTape() as tape:
3 all_loss = compute_loss(**cfg)
4 # Compute gradients wrt input image
5 total_loss = all_loss[0]
6 return tape.gradient(total_loss, cfg['init_image']), all_loss
複製程式碼
應用並執行風格遷移流程 要實際進行風格遷移:
1 def run_style_transfer(content_path,
2 style_path,
3 num_iterations=1000,
4 content_weight=1e3,
5 style_weight = 1e-2):
6 display_num = 100
7 # We don't need to (or want to) train any layers of our model, so we set their trainability
8 # to false.
9 model = get_model()
10 for layer in model.layers:
11 layer.trainable = False
12
13 # Get the style and content feature representations (from our specified intermediate layers)
14 style_features, content_features = get_feature_representations(model, content_path, style_path)
15 gram_style_features = [gram_matrix(style_feature) for style_feature in style_features]
16
17 # Set initial image
18 init_image = load_and_process_img(content_path)
19 init_image = tfe.Variable(init_image, dtype=tf.float32)
20 # Create our optimizer
21 opt = tf.train.AdamOptimizer(learning_rate=10.0)
22
23 # For displaying intermediate images
24 iter_count = 1
25
26 # Store our best result
27 best_loss, best_img = float('inf'), None
28
29 # Create a nice config
30 loss_weights = (style_weight, content_weight)
31 cfg = {
32 'model': model,
33 'loss_weights': loss_weights,
34 'init_image': init_image,
35 'gram_style_features': gram_style_features,
36 'content_features': content_features
37 }
38
39 # For displaying
40 plt.figure(figsize=(15, 15))
41 num_rows = (num_iterations / display_num) // 5
42 start_time = time.time()
43 global_start = time.time()
44
45 norm_means = np.array([103.939, 116.779, 123.68])
46 min_vals = -norm_means
47 max_vals = 255 - norm_means
48 for i in range(num_iterations):
49 grads, all_loss = compute_grads(cfg)
50 loss, style_score, content_score = all_loss
51 # grads, _ = tf.clip_by_global_norm(grads, 5.0)
52 opt.apply_gradients([(grads, init_image)])
53 clipped = tf.clip_by_value(init_image, min_vals, max_vals)
54 init_image.assign(clipped)
55 end_time = time.time()
56
57 if loss < best_loss:
58 # Update best loss and best image from total loss.
59 best_loss = loss
60 best_img = init_image.numpy()
61
62 if i % display_num == 0:
63 print('Iteration: {}'.format(i))
64 print('Total loss: {:.4e}, '
65 'style loss: {:.4e}, '
66 'content loss: {:.4e}, '
67 'time: {:.4f}s'.format(loss, style_score, content_score, time.time() - start_time))
68 start_time = time.time()
69
70 # Display intermediate images
71 if iter_count > num_rows * 5: continue
72 plt.subplot(num_rows, 5, iter_count)
73 # Use the .numpy() method to get the concrete numpy array
74 plot_img = init_image.numpy()
75 plot_img = deprocess_img(plot_img)
76 plt.imshow(plot_img)
77 plt.title('Iteration {}'.format(i + 1))
78
79 iter_count += 1
80 print('Total time: {:.4f}s'.format(time.time() - global_start))
81
82 return best_img, best_loss
複製程式碼
就是這樣!
我們在海龜影象和 Hokusai 的 《神奈川衝浪裡》上執行該流程:
1 best, best_loss = run_style_transfer(content_path,
2 style_path,
3 verbose=True,
4 show_intermediates=True)
複製程式碼
P.Lindgren 拍攝的《綠海龜》圖 [CC BY-SA 3.0 (creativecommons.org/licenses/by…)],圖片來自 Wikimedia Common
觀察這一迭代過程隨時間發生的變化:
下面有一些關於神經風格遷移用途的很棒示例。快來看看吧!
圖賓根的影象 — 拍攝者:Andreas Praefcke [GFDL (www.gnu.org/copyleft/fd…) 或 CC BY 3.0 (creativecommons.org/licenses/by…)],影象來自 Wikimedia Commons;以及梵高的《星月夜》,影象來自公共領域 圖賓根的影象 — 拍攝者:Andreas Praefcke [GFDL (www.gnu.org/copyleft/fd…) 或 CC BY 3.0 (creativecommons.org/licenses/by…)],圖片來自 Wikimedia Commons,和 Vassily Kandinsky 所作的《構圖 7》,圖片來自公共領域 圖賓根的影象 — 拍攝者:Andreas Praefcke [GFDL (www.gnu.org/copyleft/fd…) 或 CC BY 3.0 (creativecommons.org/licenses/by…)],影象來自 Wikimedia Commons 和 NASA、ESA 以及 Hubble Heritage Team 創作的《創生之柱》(Pillars of Creation),影象來自公共領域試試用自己的影象!
關鍵知識點
我們的學習內容:
-
為了最大限度地減少這些損失,我們構建了幾個不同的損失函式,並使用反向傳播技術來轉化我們的輸入影象。
-
為了進行此項操作,我們載入了一個預訓練模型,並使用該模型已學過的特徵圖來描述我們影象的內容和風格表徵。
-
我們的主要損失函式主要根據這些不同的表徵計算距離。
-
我們使用自定義模型和 Eager Execution 來進行計算。
-
我們使用功能 API 構建了我們的自定義模型。
-
Eager Execution 讓我們可以使用自然的 Python 控制流來動態地使用張量。
-
我們可以直接操控張量,這使除錯和使用張量都更加輕鬆。
通過使用 tf.gradient,我們可以運用優化器更新規則來迭代更新我們的影象。優化器可以最小化與我們輸入影象有關的既定損失。