《從鍋爐工到AI專家(8)》中咱們介紹了一個「圖片風格遷移」的例子。由於所引用的做品中使用了TensorFlow 1.x的代碼,算法也相對複雜,因此文中沒有仔細介紹風格遷移的原理。
今天在TensorFlow 2.0的幫助,和新算法思想的優化下,實現一樣功能的代碼量大幅減小,結構也愈加清晰。因此今天就來說講這個話題。python
「風格遷移」指的是將藝術做品的筆觸、技法等表現出來的視覺效果,應用在普通照片上,使得所生成的圖片,相似使用一樣筆觸、技法所繪製完成,但內容跟照片相同的「僞畫做」。
在神經網絡機器學習的幫助下,生成圖片的觀賞性很是高,遠非早期傳統方法獲得的圖片可比。
這裏重貼一遍前文中的例圖,讓咱們有一個更直觀的感覺。
首先是一張原程序做者的的自拍照:
接着不陌生,著名大做《星空》:
(請將以上兩圖保存至工做目錄,不要修改文件名,咱們稍晚的代碼中會用到。)
兩張圖片通過程序處理後,會獲得一幅新的圖片:
即便用《星空》風格模仿的手繪做品《黃粱一夢》:)算法
風格遷移原理基於論文《A Neural Algorithm of Artistic Style》。
雖然論文中並無明說,但採用卷積神經網絡作圖像的風格遷移應當屬於一個實驗科學的成果而非單純的理論研究。
咱們再引用一張前系列講解CNN時候的圖片:
一張圖片數據所造成的矩陣,在通過卷積網絡的時候,圖像中邊緣等視覺特徵會被放大、強化,從而造成一種特殊的輸出。一般咱們只關心數據結果,並無把這些數據還原爲圖片來觀察。而論文做者不只這樣作了,恐怕還進行了大量的實驗。
這些神經網絡中間結果圖片具備如此典型的特徵,能夠脫離出主題內容而成爲單純風格的描述。被敏銳的做者抓住深刻研究也就不奇怪了。
最終研究成果確立了卷積神經網絡進行圖片遷移的兩大基礎算法:數組
本系列文章都是盡力不出現數學公式,用代碼講原理。
在《從鍋爐工到AI專家(8)》引用的代碼中,除了構建神經網絡、訓練,主要工做是在損失函數下降到滿意程度以後,使用網絡中間層的輸出結果計算、組合成目標圖片。原文中對這部分的流程也作了簡介。
新的代碼來自TensorFlow官方文檔。除了程序升級爲TensorFlow 2.0原生代碼。在圖片的產生上也作了大幅創新:使用照片圖片訓練神經網絡,每一階梯的訓練結果,不該用回神經網絡(網絡的權重參數一直固定鎖死的),而把訓練結果應用到圖片自己。在下一次的訓練循環中,使用新的圖片再次計算損失值。這樣,當損失值最小的時候,訓練圖片自己就已是符合咱們要求的生成圖片。固然本質上,跟前一種方法同樣的。但感受上,結構清晰了不少。這個過程對比起來,大量節省了圖片生成的計算。固然,主要緣由仍是TensorFlow 2.0內置的tf.linalg.einsum方法強大好用。網絡
在特徵層的定義上,照片內容的描述使用vgg-19網絡的第5部分的第2層卷積輸出結果。藝術圖片風格特徵的描述使用了5個層,分別是vgg-19網絡的第1至第5部分第1個網絡層的輸出結果。在程序中,能夠這樣描述:app
# 定義最能表明內容特徵的網絡層 content_layers = ['block5_conv2'] # 定義最能表明風格特徵的網絡層 style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
網絡層的名稱來自於vgg-19網絡定義完成後,各層的名稱。可使用以下代碼獲得全部層的名稱:機器學習
... # 創建無需分類結果的vgg網絡 vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') # 顯示vgg中全部層的名稱 print() for layer in vgg.layers: print(layer.name) ...
一般的模型訓練,都是使用代價函數比較網絡輸出結果,和目標標註值的差別,使得差別逐漸縮小。
本例的訓練目標比較複雜,能夠描述爲兩條:函數
雖然這個代價函數略微複雜,不過比VAE的代價函數仍是簡單多了:)工具
程序中的註釋很是詳細。跟之前的程序有一點區別,就是直接使用TensorFlow內置方法讀取了圖片文件,而後調用jpg解碼還原爲矩陣。
不過TensorFlow內置的將圖像0-255整數值轉換爲浮點數的過程,會自動將數值變爲0-1的浮點小數。
這個過程其實對咱們畫蛇添足,由於咱們後續的不少計算都須要轉換回0-255。性能
#!/usr/bin/env python3 from __future__ import absolute_import, division, print_function, unicode_literals import tensorflow as tf import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np import time import functools import time from PIL import Image # 設置繪圖窗口參數,用於圖片顯示 mpl.rcParams['figure.figsize'] = (13, 10) mpl.rcParams['axes.grid'] = False # 獲取下載後本地圖片的路徑,content_path是真實照片,style_path是藝術品風格圖片 content_path = "1-content.jpg" style_path = "1-style.jpg" # 讀取一張圖片,並作預處理 def load_img(path_to_img): max_dim = 512 # 讀取二進制文件 img = tf.io.read_file(path_to_img) # 作JPEG解碼,這時候獲得寬x高x色深矩陣,數字0-255 img = tf.image.decode_jpeg(img) # 類型從int轉換到32位浮點,數值範圍0-1 img = tf.image.convert_image_dtype(img, tf.float32) # 減掉最後色深一維,獲取到的至關於圖片尺寸(整數),轉爲浮點 shape = tf.cast(tf.shape(img)[:-1], tf.float32) # 獲取圖片長端 long = max(shape) # 以長端爲比例縮放,讓圖片成爲512x??? scale = max_dim/long new_shape = tf.cast(shape*scale, tf.int32) # 實際縮放圖片 img = tf.image.resize(img, new_shape) # 再擴展一維,成爲圖片數字中的一張圖片(1,長,寬,色深) img = img[tf.newaxis, :] return img # 讀入兩張圖片 content_image = load_img(content_path) style_image = load_img(style_path) ############################################################ # 定義最能表明內容特徵的網絡層 content_layers = ['block5_conv2'] # 定義最能表明風格特徵的網絡層 style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] # 神經網絡層的數量 num_content_layers = len(content_layers) num_style_layers = len(style_layers) # 定義一個工具函數,幫助創建獲得特定中間層輸出結果的新模型 def vgg_layers(layer_names): """ Creates a vgg model that returns a list of intermediate output values.""" # 定義使用ImageNet數據訓練的vgg19網絡 vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') # 已經通過了訓練,因此鎖定各項參數避免再次訓練 vgg.trainable = False # 獲取所需層的輸出結果 outputs = [vgg.get_layer(name).output for name in layer_names] # 最終返回結果是一個模型,輸入是圖片,輸出爲所需的中間層輸出 model = tf.keras.Model([vgg.input], outputs) return model # 定義函數計算風格矩陣,這實際是由抽取出來的5個網絡層的輸出計算得來的 def gram_matrix(input_tensor): result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32) return result/(num_locations) # 自定義keras模型 class StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() # 本身的vgg模型,包含上面所列的風格抽取層和內容抽取層 self.vgg = vgg_layers(style_layers + content_layers) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) # vgg各層參數鎖定再也不參數訓練 self.vgg.trainable = False def call(self, input): # 輸入的圖片是0-1範圍浮點,轉換到0-255以符合vgg要求 input = input*255.0 # 對輸入圖片數據作預處理 preprocessed_input = tf.keras.applications.vgg19.preprocess_input(input) # 獲取風格層和內容層輸出 outputs = self.vgg(preprocessed_input) # 輸出實際是一個數組,拆分爲風格輸出和內容輸出 style_outputs, content_outputs = ( outputs[:self.num_style_layers], outputs[self.num_style_layers:]) # 計算風格矩陣 style_outputs = [gram_matrix(style_output) for style_output in style_outputs] # 轉換爲字典 content_dict = {content_name: value for content_name, value in zip(self.content_layers, content_outputs)} # 轉換爲字典 style_dict = {style_name: value for style_name, value in zip(self.style_layers, style_outputs)} # 返回內容和風格結果 return {'content': content_dict, 'style': style_dict} # 使用自定義模型創建一個抽取器 extractor = StyleContentModel(style_layers, content_layers) # 設定風格特徵的目標,即最終生成的圖片,但願風格上儘可能接近風格圖片 style_targets = extractor(style_image)['style'] # 設定內容特徵的目標,即最終生成的圖片,但願內容上儘可能接近內容圖片 content_targets = extractor(content_image)['content'] # 內容圖片轉換爲張量 image = tf.Variable(content_image) # 截取0-1的浮點數,超範圍部分被截取 def clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0) # 優化器 opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1) # 預約義風格和內容在最終結果中的權重值,用於在損失函數中計算總損失值 style_weight = 1e-2 content_weight = 1e4 # 損失函數 def style_content_loss(outputs): style_outputs = outputs['style'] content_outputs = outputs['content'] # 風格損失值,就是計算方差 style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()]) # 權重值平均到每層,計算整體風格損失值 style_loss *= style_weight/num_style_layers # 內容損失值,也是計算方差 content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()]) content_loss *= content_weight/num_content_layers # 總損失值 loss = style_loss+content_loss return loss ################################################################ # 一次訓練 @tf.function() def train_step(image): with tf.GradientTape() as tape: # 抽取風格層、內容層輸出 outputs = extractor(image) # 計算損失值 loss = style_content_loss(outputs) # 梯度降低 grad = tape.gradient(loss, image) # 應用計算後的新參數,注意這個新值不是應用到網絡 # 做爲訓練完成的vgg網絡,其參數前面已經設定不可更改 # 這個參數實際將應用於原圖 # 以求取,新圖片通過網絡後,損失值最小 opt.apply_gradients([(grad, image)]) # 更新圖片,用新圖片進行下次訓練迭代 image.assign(clip_0_1(image)) start = time.time() epochs = 10 steps_per_epoch = 100 step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 train_step(image) print(".", end='') print("") # 每100次迭代顯示一次圖片 # imshow(image.read_value()) # plt.title("Train step: {}".format(step)) # plt.show() end = time.time() print("Total time: {:.1f}".format(end-start)) ######################################## #保存結果圖片 file_name = 'newart1.png' mpl.image.imsave(file_name, image[0])
程序的輸出結果以下圖:
看起來基本達到了設計要求,不過再仔細觀察,彷佛效果雖然都有了,但畫面看上去有一點不乾淨,有不少小的噪點甚至有了干涉紋。
這是由於,在照片原圖和藝術做品原圖中,確定自然就存在有噪點以及圖片中自己應當有的小而頻繁的花紋。這些內容在經過卷積增強後,兩幅照片再疊加,這些噪聲就被強化了,從而在生成的圖片中體現的很是明顯。
這個問題若是在傳統算法中可使用高通濾波。在卷積神經網絡中則更容易,是統計整體變分損失值(Total Variation Loss),在代價函數中,讓這個損失值降到最小,就抑制了這種噪點的產生。也至關於神經網絡具備了降噪的效果。
變分損失是計算圖片中,在X方向及Y方向,相鄰像素的差值。若是像素差異不大,那差確定很小甚至趨近於0。若是差異大,固然差值就大。
請使用下面的代碼,替換上面程序中訓練的部分:學習
################################################### # 計算x方向及y方向相鄰像素差值,若是有高頻花紋,這個值確定會高, # 由於相鄰點相同差值接近0,區別越大,差值固然越大 def high_pass_x_y(image): x_var = image[:, :, 1:, :] - image[:, :, :-1, :] y_var = image[:, 1:, :, :] - image[:, :-1, :, :] return x_var, y_var # 計算整體變分損失 def total_variation_loss(image): x_deltas, y_deltas = high_pass_x_y(image) return tf.reduce_mean(x_deltas**2)+tf.reduce_mean(y_deltas**2) # 整體變分損失值在損失值中所佔權重 total_variation_weight = 1e8 # 一次訓練 @tf.function() def train_step(image): with tf.GradientTape() as tape: # 抽取風格層、內容層輸出 outputs = extractor(image) # 計算損失值 loss = style_content_loss(outputs) loss += total_variation_weight*total_variation_loss(image) # 梯度降低 grad = tape.gradient(loss, image) # 應用計算後的新參數,注意這個新值不是應用到網絡 # 做爲訓練完成的vgg網絡,其參數前面已經設定不可更改 # 這個參數實際將應用於原圖 # 以求取,新圖片通過網絡後,損失值最小 opt.apply_gradients([(grad, image)]) # 更新圖片,用新圖片進行下次訓練迭代 image.assign(clip_0_1(image)) # 內容圖片做爲逐步迭代生成的新圖片,一開始固然是原圖,這裏是轉換爲張量 image = tf.Variable(content_image) start = time.time() # 迭代10次,每次100步訓練 epochs = 10 steps = 100 step = 0 for n in range(epochs): for m in range(steps): step += 1 train_step(image) print(".", end='') print("") end = time.time() print("Total time: {:.1f}".format(end-start)) #保存結果圖片 file_name = 'newart1.png' mpl.image.imsave(file_name, image[0])
再次執行,所獲得的輸出圖片以下:
效果不錯吧?能夠換上本身的照片還有本身心儀的藝術做品來試試。
程序中限制了圖片寬、高最大值是512,若是設備性能比較好,或者有更大尺寸的需求,能夠修改程序中的常量。
(待續...)