文 / 軟件工程實習生 Raymond Yuanhtml
來源 | TensorFlow 公衆號git
在本教程中,咱們將學習如何使用深度學習來以另外一幅圖像的風格創做圖像(有沒有但願本身能夠像畢加索或梵高同樣做畫?)。這就是咱們所說的神經風格遷移!Leon A. Gatys 在其論文《一種藝術風格的神經網絡算法》(A Neural Algorithm of Artistic Style)中概要介紹了這項技術,文章很是值得一讀,千萬不要錯過。 注:教程連接 colab.research.google.com/github/tens…github
神經風格遷移是一項優化技術,可用於選取三幅圖像,即一幅內容圖像、一幅風格參考圖像(例如一幅名家做品),以及您想要設定風格的輸入圖像,而後將它們融合在一塊兒,這樣輸入圖像轉化後就會看起來與內容圖像類似,但其呈現的是風格圖像的風格。算法
例如,咱們選取一張烏龜的圖像和 Katsushika Hokusai 的 《神奈川衝浪裏》:編程
若是 Hokusai 決定將他做品中海浪的紋理或風格添加到海龜圖像中,這幅圖看起來會是什麼樣?會不會是這樣?bash
這是魔法嗎?又或者只是深度學習?幸運的是,這和魔法沒有任何關係:風格遷移是一項好玩又有趣的技術,能夠展示神經網絡的能力和內部表現形式。網絡
神經風格遷移的原理是定義兩個距離函數,一個描述兩幅圖像的不一樣之處,即 Lcontent 函數,另外一個描述兩幅圖像的風格差別,即 Lstyle 函數。而後,給定三幅圖像,一幅所需的風格圖像、一幅所需的內容圖像,還有一幅輸入圖像(用內容圖像進行初始化)。咱們努力轉換輸入圖像,藉助內容圖像將內容距離最小化,並藉助風格圖像將風格距離最小化。架構
簡而言之,咱們會選取基本輸入圖像、咱們想要匹配的內容圖像以及想要匹配的風格圖像。咱們將使用反向傳播算法最小化內容和風格距離(損失),以轉換基本輸入圖像,建立與內容圖像的內容和風格圖像的風格相匹配的圖像。app
在此過程當中,咱們會圍繞下列概念積累實際經驗,造成直覺認識:機器學習
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()
複製代碼
定義內容和風格表徵
爲了獲取咱們圖像的內容和風格表徵,咱們先來看看模型內的一些中間層。中間層表明着特徵圖,這些特徵圖將隨着您的深刻變得愈來愈有序。在本例中,咱們會使用 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
觀察這一迭代過程隨時間發生的變化:
下面有一些關於神經風格遷移用途的很棒示例。快來看看吧!
試試用本身的圖像!
咱們的學習內容:
爲了最大限度地減小這些損失,咱們構建了幾個不一樣的損失函數,並使用反向傳播技術來轉化咱們的輸入圖像。
爲了進行此項操做,咱們載入了一個預訓練模型,並使用該模型已學過的特徵圖來描述咱們圖像的內容和風格表徵。
咱們的主要損失函數主要根據這些不一樣的表徵計算距離。
咱們使用自定義模型和 Eager Execution 來進行計算。
咱們使用功能 API 構建了咱們的自定義模型。
Eager Execution 讓咱們可使用天然的 Python 控制流來動態地使用張量。
咱們能夠直接操控張量,這使調試和使用張量都更加輕鬆。
經過使用 tf.gradient,咱們能夠運用優化器更新規則來迭代更新咱們的圖像。優化器能夠最小化與咱們輸入圖像有關的既定損失。