如何用Keras打造出「風格遷移」的AI藝術做品

過去幾年,卷積神經網絡(CNN)成爲一種前沿的計算機視覺工具,在業界和學界普遍應用。除了人臉識別和無人駕駛領域,CNN 這幾年還在藝術領域廣受歡迎,其中衍生出一個表明性技術就是「風格遷移」,根據這項技術誕生了不少美圖應用,好比 2016 年大火的 Prisma APP。git

「風格遷移」是展現神經網絡強大能力的一個頗有趣的途徑。2015 年,德國和美國的一組研究人員發佈了一篇論文《A Neural Algorithm of Artistic Style》 詳細討論了深度卷積神經網絡如何區分照片中的「內容」和「風格」。論文做者展現了 CNN如何可以將一張照片的藝術風格應用在另外一張照片上,生成一張全新的使人眼前一亮的照片。並且他們的方法不須要訓練一個新的神經網絡,使用來自 ImageNet 這類數據集中的預訓練權重就有很好的效果。github

在本文,我(做者 Walid Ahmad——譯者注)會展現如何用流行的 Python 程序庫 Keras 創做「風格遷移」的 AI 做品,總體思路和上面這篇論文的方法一致。本文的所有代碼點擊這裏獲取。算法

使用兩張基本的圖像素材,咱們就能創造出下面這樣的 AI 藝術做品:後端

咱們要解決的這個問題是如今有了兩張基本圖像素材,咱們想把它們「合併」在一塊兒。其中一張照片的內容咱們但願可以保留,咱們把這張照片稱爲 p。在我舉的這個例子中,我從谷歌上隨便搜了一張可愛的貓咪照片:bash

另外一張基本圖像的藝術風格咱們但願可以保留,咱們稱它爲 a。我選了一張巴洛克風格的著名照片:《Violin on Palette》。網絡

最後,咱們會獲得一張生成照片 x,並用隨機的顏色數值將它初始化。隨着咱們最小化內容和風格的損失函數,這張照片會隨之不斷變化。session

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
## Specify paths for 1) content image 2) style image and 3) generated image
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

cImPath = './data/base_images/cat.jpg'
sImPath = './data/base_images/violin_and_palette.jpg'
genImOutputPath = './results/output.jpg'

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
## 圖像處理
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
from keras import backend as K
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing.image import load_img, img_to_array

targetHeight = 512
targetWidth = 512
targetSize = (targetHeight, targetWidth)

cImage = load_img(path=cImPath, target_size=targetSize)
cImArr = img_to_array(cImage)
cImArr = K.variable(preprocess_input(np.expand_dims(cImArr, axis=0)), dtype='float32')

sImage = load_img(path=sImPath, target_size=targetSize)
sImArr = img_to_array(sImage)
sImArr = K.variable(preprocess_input(np.expand_dims(sImArr, axis=0)), dtype='float32')

gIm0 = np.random.randint(256, size=(targetWidth, targetHeight, 3)).astype('float64')
gIm0 = preprocess_input(np.expand_dims(gIm0, axis=0))
gImPlaceholder = K.placeholder(shape=(1, targetWidth, targetHeight, 3))
複製代碼

注意,咱們這裏爲了後面的優化,將glm0初始化爲 float64。並且爲了不GPU的內存錯誤,咱們將cImArr和slmArr保持爲float32.架構

內容損失

內容損失的目標是確保生成的照片x仍能保留內容照片p的「全局」風格。好比,在咱們的這個例子中,咱們但願最終生成的圖像能看起來仍是照片p中的貓咪。這意味着,貓咪的臉、耳朵、眼睛等這些都是能夠識別出的。要想達到這個目標,內容損失函數會分別在給定層L中定義爲p和x的特徵表示之間的均方偏差。內容損失函數爲:app

在這裏,dom

  • F和P是兩個矩陣,包含N個行和M個列

  • N是給定層L中的過濾器數量,M是給定層I的特徵圖譜(高度乘以寬度)中空間元素的數量

  • F包含給定層L中X的特徵表示

  • P包含給定層L中p的特徵表示

def get_feature_reps(x, layer_names, model):
    """ Get feature representations of input x for one or more layers in a given model. """
    featMatrices = []
    for ln in layer_names:
        selectedLayer = model.get_layer(ln)
        featRaw = selectedLayer.output
        featRawShape = K.shape(featRaw).eval(session=tf_session)
        N_l = featRawShape[-1]
        M_l = featRawShape[1]*featRawShape[2]
        featMatrix = K.reshape(featRaw, (M_l, N_l))
        featMatrix = K.transpose(featMatrix)
        featMatrices.append(featMatrix)
    return featMatrices

def get_content_loss(F, P):
    cLoss = 0.5*K.sum(K.square(F - P))
    return cLoss
複製代碼

風格損失

風格損失須要保存風格照片a的風格特徵。論文做者並未利用特徵表示之間的不一樣,而是利用選定層中的格拉姆矩陣的不一樣之處,其中格拉姆矩陣定義以下:

格拉姆矩陣是一個正方矩陣,包含層級L中每一個矢量過濾器(vectorized filter)之間的點積。所以該矩陣能夠看做層級L中過濾器的一個非規整矩陣。

def get_Gram_matrix(F):
    G = K.dot(F, K.transpose(F))
    return G
複製代碼

那麼咱們能夠將給定層L中的風格損失函數定義爲:

其中A是風格照片a的格拉姆矩陣,G爲生成照片x的格拉姆矩陣。

在大多數卷積神經網絡中如VGG,提高層(ascending layer)的感覺野(receptive field)會愈來愈大。隨着感覺野不斷變大,輸入圖像的更大規模的特徵也得以保存下來。正因如此,咱們應該選擇多個層級用於「風格遷移」,將局部和全局的風格質量進行合併。爲了讓這些層之間鏈接順暢,咱們能夠爲每一個層賦予一個權重w,將整個風格損失函數定義爲:

def get_style_loss(ws, Gs, As):
    sLoss = K.variable(0.)
    for w, G, A in zip(ws, Gs, As):
        M_l = K.int_shape(G)[1]
        N_l = K.int_shape(G)[0]
        G_gram = get_Gram_matrix(G)
        A_gram = get_Gram_matrix(A)
        sLoss+= w*0.25*K.sum(K.square(G_gram - A_gram))/ (N_l**2 * M_l**2)
    return sLoss
複製代碼

整合兩個函數

最後,咱們只需分別爲內容損失函數和風格損失函數賦予加權係數,而後大功告成!

終於獲得一個整潔優美的函數公式,能讓咱們利用⍺和 ß在生成照片上調整內容照片和風格照片二者的相對影響。根據那篇論文的建議以及我本身的經驗,讓⍺= 1 ,ß = 10,000 效果會很好。

def get_total_loss(gImPlaceholder, alpha=1.0, beta=10000.0):
    F = get_feature_reps(gImPlaceholder, layer_names=[cLayerName], model=gModel)[0]
    Gs = get_feature_reps(gImPlaceholder, layer_names=sLayerNames, model=gModel)
    contentLoss = get_content_loss(F, P)
    styleLoss = get_style_loss(ws, Gs, As)
    totalLoss = alpha*contentLoss + beta*styleLoss
    return totalLoss
複製代碼

模型應用詳情

要想開始改變咱們的生成圖像以最小化損失函數,咱們必須用scipy和Keras後端再定義兩個函數。首先,用一個函數計算總體損失,其次,用另外一個函數計算梯度。二者計算後獲得的結果會分別做爲目標函數和梯度函數輸入到Scipy優化函數中。在這裏,咱們使用L-BFGS算法(limited-memory BFGS)。

對於每張內容照片和風格照片,咱們會提取特徵表示,用來構建P和A(對於每一個選中的風格層),而後爲風格層賦給相同的權重。在實際操做中,一般用L-BFGS算法進行超過500次迭代後,產生的結果就比較可信了。

def calculate_loss(gImArr):
  """ Calculate total loss using K.function """
    if gImArr.shape != (1, targetWidth, targetWidth, 3):
        gImArr = gImArr.reshape((1, targetWidth, targetHeight, 3))
    loss_fcn = K.function([gModel.input], [get_total_loss(gModel.input)])
    return loss_fcn([gImArr])[0].astype('float64')

def get_grad(gImArr):
  """ Calculate the gradient of the loss function with respect to the generated image """
    if gImArr.shape != (1, targetWidth, targetHeight, 3):
        gImArr = gImArr.reshape((1, targetWidth, targetHeight, 3))
    grad_fcn = K.function([gModel.input], 
                          K.gradients(get_total_loss(gModel.input), [gModel.input]))
    grad = grad_fcn([gImArr])[0].flatten().astype('float64')
    return grad

from keras.applications import VGG16
from scipy.optimize import fmin_l_bfgs_b

tf_session = K.get_session()
cModel = VGG16(include_top=False, weights='imagenet', input_tensor=cImArr)
sModel = VGG16(include_top=False, weights='imagenet', input_tensor=sImArr)
gModel = VGG16(include_top=False, weights='imagenet', input_tensor=gImPlaceholder)
cLayerName = 'block4_conv2'
sLayerNames = [
                'block1_conv1',
                'block2_conv1',
                'block3_conv1',
                'block4_conv1',
                ]

P = get_feature_reps(x=cImArr, layer_names=[cLayerName], model=cModel)[0]
As = get_feature_reps(x=sImArr, layer_names=sLayerNames, model=sModel)
ws = np.ones(len(sLayerNames))/float(len(sLayerNames))

iterations = 600
x_val = gIm0.flatten()
xopt, f_val, info= fmin_l_bfgs_b(calculate_loss, x_val, fprime=get_grad,
                            maxiter=iterations, disp=True)
複製代碼

雖然過程有點慢,但能保證效果···

咱們開始看見若隱若現地出現一個立體主義畫派版的小貓咪!等算法再迭代上幾回後:

咱們能夠根據貓咪原圖的大小對照片略做修改,將兩張圖並列在一塊兒。很容易看到貓咪的主要特徵,好比眼睛、鼻子和爪爪都維持在原來的狀態。不過,爲了匹配照片風格,它們都被扁平化了,並且棱角分明——但這正是咱們想要的結果啊!

咱們用一樣的方法但是試試其餘照片。好比我從谷歌上找了一張建築圖,而後選了梵高的名畫《羅納河上的星夜》:

風格遷移後的做品:

總結

在本文咱們探究瞭如何用Keras應用「風格遷移」技術,不過咱們還能夠作不少工做,創造出更加迷人的做品:

  • 嘗試用不一樣的權重:不一樣的照片混合可能須要調整風格損失權重w或不斷優化⍺和 ß的值。例如,在有些例子中,ß/⍺的比例值爲10⁵ 效果會更好。

  • 嘗試用更多的風格層級:這會消耗更多的計算資源,但可以更順暢地對風格進行遷移。你能夠試試VGG19,而不是VGG16,或者將不一樣的神經網絡架構結合在一塊兒。

  • 嘗試用多張內容照片和風格照片:你能夠爲損失函數增長几張風格照片,混合多張照片或多種藝術風格。增長內容照片或許會帶來更有意思的藝術效果。

  • 增長總變分去噪方法:若是你仔細看看上面我獲得的照片,你會發現上面有些顆粒狀圖案——小小的顏色旋渦。用神經網絡處理照片一般都會有這個問題,其中一個緣由就是照片的有損壓縮被帶進了特徵圖譜裏。添加總變分去噪能夠有效減輕這個問題,點擊查看這一步的代碼

下面是我參考的一些資料,你們能夠去看一看:

參考資料1

參考資料2


歡迎關注咱們,學習資源,AI教程,論文解讀,趣味科普,你想看的都在這裏!

相關文章
相關標籤/搜索