風格遷移
風格遷移算法經歷多次定義和更新,現在應用在許多智能手機APP上。
風格遷移在保留目標圖片內容的基礎上,將圖片風格引用在目標圖片上。
風格本質上是指在各種空間尺度上圖像中的紋理,顏色和視覺圖案;內容是圖像的高級宏觀結構。
實現風格遷移背后的關鍵概念與所有深度學習算法的核心相同:定義了一個損失函數來指定想要實現的目標,並最大限度地減少這種損失。
知道自己想要實現的目標:在采用參考圖像的樣式的同時保留原始圖像的內容。如果我們能夠在數學上定義內容和樣式,那么最小化的適當損失函數將是以下內容:
loss = distance(style(reference_image) - style(generated_image)) +
distance(content(original_image) - content(generated_image))
distance是一個如L2范數的函數,content計算圖片內容表示的函數;style計算圖片風格表示的函數。最小化損失函數導致style(generated_image)和引用圖片style(reference_image)盡可能接近,而content(generated_image))與內容圖片content(original_image)盡可能接近,最終達到風格遷移的目標。
內容損失函數
我們已經知道網絡模型前幾層的激活函數值表示圖片的局部信息,高層網絡激活值包括全局性、抽象性的特征信息。換言之,卷積網的不同層的激活值提供了在不同空間尺度上圖像內容的分解。因此,期望通過convnet中上層的表示捕獲更全局和抽象的圖像內容。
內容損失函數的另一種選擇是在目標圖像上計算的預訓練的網絡中的上層的激活與生成的圖像上計算的相同層的激活之間的L2范數。這保證從上層看生成的圖像看起來與原始目標圖像類似。假設卷積網的上層看到的是輸入圖像的內容,那么這就是保存圖像內容的一種方式。
風格損失函數
內容損失函數僅使用單個上層,但是Gatys定義的風格損失函數使用多個convnet層:嘗試捕獲由convnet提取的所有空間比例的樣式參考圖像的外觀,而不僅僅是單個比例。對於風格的損失,Gatys使用圖層激活的Gram矩陣:給定圖層的要素圖的內積。該內積可以理解為表示層的特征之間的相關性的圖。這些特征相關性捕獲特定空間尺度的模式的統計數據,其在經驗上對應於在該尺度下找到的紋理的外觀。
因此,風格損失旨在在風格參考圖像和生成的圖像之間保持不同層的激活內的類似內部相關性。反過來,這保證了在不同空間尺度上找到的紋理在樣式參考圖像和生成的圖像中看起來相似。
可以使用預訓練好的網絡模型定義損失函數:
- 通過在目標內容圖像和生成的圖像之間保持類似的高級圖層激活來保留內容。卷積網應該“看到”目標圖像和生成的圖像包含相同的內容;
- 通過在低級圖層和高級圖層的激活中保持類似的相關性來保留樣式。特征相關性捕獲紋理:生成的圖像和樣式參考圖像應在不同的空間尺度共享相同的紋理。
Keras實現
使用VGG19網絡模型實現風格遷移。流程:
- 設置一個網絡,同時為風格參考圖像,目標圖像和生成圖像計算VGG19圖層激活函數值;
- 使用在這三個圖像上計算的圖層激活值來定義前面描述的損失函數,可以將其最小化以實現風格遷移;
- 設置梯度下降過程以最小化此損失函數。
定義風格圖片、目標圖片路徑地址;為了確保兩張處理圖片尺寸相同(尺寸不同會增加處理難度),對兩張圖片進行resize操作,大小為400px。
定義初始變量
from keras.preprocessing.image import load_img,img_to_array
target_image_path = 'img/protrait.jpg'
style_reference_image_path = 'img/transfer_style_reference.jpg'
width, height = load_img(target_image_path).size
img_height = 400
img_width = int(width*img_height/height)
定義輔助函數方便加載、預處理、后期處理VGG19卷積網絡接收和產生的圖片。
輔助函數
import numpy as np
from 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):
x[:,:,0] += 103.939#zero-centering 0中心化:減去均值
x[:,:,1] += 116.779
x[:,:,2] += 123.68
x = x[:,:,::-1]#BGR---> RGB
x = np.clip(x,0,255).astype('uint8')
return x
設置vgg19網絡。以風格圖片、目標圖片、生成圖片的placeholder三張圖片的batch作為輸入。
加載預訓練VGG19模型,應用
from keras import backend as K
target_image = K.constant(preprocess_image(target_image_path))
style_reference_image=K.constant(preprocess_image(style_reference_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)
print("Model loaded.")
定義內容損失,確保目標圖片和生成圖片在VGG19卷積網絡的上層網絡中相似。
內容損失
def content_loss(base,combination):
return K.sum(K.square(combination-base))
定義風格損失。使用輔助函數計算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.*(channels ** 2) * (size ** 2))
除了這兩種損失函數外,加第三種:總變異損失,其對所生成的組合圖像的像素進行操作。它鼓勵生成的圖像中的空間連續性,從而避免過度像素化的結果。可以將其解釋為正則化損失。
變異損失
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))
最小化損失函數是三種損失函數的加權平均。為了計算內容損失,只需要使用一個上層網絡--block5_conv2網絡層;計算風格損失,需要使用多個網絡層:從底層網絡到高層網絡。最后加上變異損失。
依賴於使用風格圖片和內容圖片,可能需要微調content_weight系數(內容損失對全部損失的貢獻程度)。大content_weight意味着目標內容在生成圖片中更容易識別。
定義最終損失函數
output_dict = dict([(layer.name,layer.output) for layer in model.layers])
content_layer = 'block5_conv2'
style_layers = ['block1_conv1','block2_conv1',
'block3_conv1','block4_conv1','block5_conv1']
total_variation_weight = 1e-4
style_weight = 1.
content_weight = 0.025
loss = K.variable(0.)#最終損失值
layer_features = outputs_dict[content_layer]
target_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
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 += (style_weight / len(style_layers)) * sl
#加變異損失,得到最終損失函數值
loss += total_variation_weight * total_variation_loss(combination_image)
最后,使用梯度下降算法。Gatys論文中使用L-BFGS算法。L-BFGS算法包含在SciPy包中,在SciPy實現中有兩個限制:
- 要求的損失函數值、梯度函數值作為兩個獨立的函數傳遞;
- 必須flat展開向量,而圖片數組是3D。
單獨計算損失函數的值和梯度的值是低效的,因為這樣做會導致兩者之間的大量冗余計算;這個過程幾乎是共同計算過程的兩倍。要繞過這個,將設置一個名為Evaluator的Python類,它同時計算損失值和梯度值,在第一次調用時返回損失值,並緩存下一次調用的梯度。
梯度更新
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.grad_values = grad_values
return self.loss_value
def grads(self, x):
assert self.loss_value is not None
grad_values = np.copy(self.grad_values)
self.loss_value = None
self.grad_values = None
return grad_values
evaluator = Evaluator()
最終,使用SciPy的L-BFGS算法運行梯度下降,每次迭代過程中保存當前生成的圖片。
風格遷移循環
from scipy.optimize import fmin_l_bfgs_b
from scipy.misc import imsave
import time
result_prefix = 'my_result'
iterations = 20
x = preprocess_image(target_image_path)#目標圖片路徑
x = x.flatten()#展開,應用l-bfgs
for i in range(iterations):
print('Start of iteration', i)
start_time = time.time()
#在生成圖片上運行L-BFGS優化;注意傳遞計算損失和梯度值必須為兩個不同函數作為參數
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_prefix + '_at_iteration_%d.png' % i
imsave(fname, img)
print('Image saved as', fname)
end_time = time.time()
print('Iteration %d completed in %ds' % (i, end_time - start_time))
請記住,這種技術所實現的僅僅是圖像重新構造或紋理轉移的一種形式。它最適用於具有強烈紋理和高度自相似性的樣式參考圖像,並且內容目標不需要高級別的細節以便可識別。它通常無法實現相當抽象的功能,例如將一幅肖像的風格轉移到另一幅肖像。該算法更接近經典信號處理而不是AI。
另外,請注意運行此風格遷移算法很慢。但是,由設置操作的轉換非常簡單,只要有適當的訓練數據,它就可以通過一個小型,快速的前饋卷積網絡學習。因此,可以通過首先花費大量計算周期來生成固定樣式參考圖像的輸入輸出訓練示例,使用概述的方法,然后訓練一個簡單的convnet來學習這種特定於樣式的轉換,從而實現快速樣式轉換。一旦完成,對給定圖像進行風格化是即時的:它只是這個小小的一個前向傳遞。
小結
- 風格遷移包括創建新圖像,該圖像保留目標圖像的內容,同時還捕獲參考圖像的樣式;
- 內容可以通過卷積網絡的高層網絡捕獲;
- 風格通過卷積網絡的不同網絡層激活函數的內部相關性計算;
- 因此,深度學習允許將風格遷移表達為使用由預訓練的convnet對定義的損失進行優化的過程。