如何用Keras打造出“風格遷移”的AI藝術作品

景略集智發表於2018-05-23

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

如何用Keras打造出“風格遷移”的AI藝術作品

“風格遷移”是展示神經網路強大能力的一個很有趣的途徑。2015 年,德國和美國的一組研究人員釋出了一篇論文《A Neural Algorithm of Artistic Style》 詳細討論了深度卷積神經網路如何區分照片中的“內容”和“風格”。論文作者展示了 CNN如何能夠將一張照片的藝術風格應用在另一張照片上,生成一張全新的令人眼前一亮的照片。而且他們的方法不需要訓練一個新的神經網路,使用來自 ImageNet 這類資料集中的預訓練權重就有很好的效果。

在本文,我(作者 Walid Ahmad——譯者注)會展示如何用流行的 Python 程式庫 Keras 創作“風格遷移”的 AI 作品,整體思路和上面這篇論文的方法一致。本文的全部程式碼點選這裡獲取。

使用兩張基本的影像素材,我們就能創造出下面這樣的 AI 藝術作品:

如何用Keras打造出“風格遷移”的AI藝術作品

我們要解決的這個問題是現在有了兩張基本影像素材,我們想把它們“合併”在一起。其中一張照片的內容我們希望能夠保留,我們把這張照片稱為 p。在我舉的這個例子中,我從谷歌上隨便搜了一張可愛的貓咪照片:

如何用Keras打造出“風格遷移”的AI藝術作品

另一張基本影像的藝術風格我們希望能夠保留,我們稱它為 a。我選了一張巴洛克風格的著名照片:《Violin on Palette》。

如何用Keras打造出“風格遷移”的AI藝術作品

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

##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
## 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的特徵表示之間的均方誤差。內容損失函式為:

如何用Keras打造出“風格遷移”的AI藝術作品

在這裡,

  • 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的風格特徵。論文作者並未利用特徵表示之間的不同,而是利用選定層中的格拉姆矩陣的不同之處,其中格拉姆矩陣定義如下:

如何用Keras打造出“風格遷移”的AI藝術作品

格拉姆矩陣是一個正方矩陣,包含層級L中每個向量過濾器(vectorized filter)之間的點積。因此該矩陣可以看作層級L中過濾器的一個非規整矩陣。

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

那麼我們可以將給定層L中的風格損失函式定義為:

如何用Keras打造出“風格遷移”的AI藝術作品

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

在大多數卷積神經網路中如VGG,提升層(ascending layer)的感受野(receptive field)會越來越大。隨著感受野不斷變大,輸入影像的更大規模的特徵也得以儲存下來。正因如此,我們應該選擇多個層級用於“風格遷移”,將區域性和全域性的風格質量進行合併。為了讓這些層之間連線順暢,我們可以為每個層賦予一個權重w,將整個風格損失函式定義為:

如何用Keras打造出“風格遷移”的AI藝術作品

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
複製程式碼

整合兩個函式

最後,我們只需分別為內容損失函式和風格損失函式賦予加權係數,然後大功告成!

如何用Keras打造出“風格遷移”的AI藝術作品

終於得到一個整潔優美的函式公式,能讓我們利用⍺和 ß在生成照片上調整內容照片和風格照片兩者的相對影響。根據那篇論文的建議以及我自己的經驗,讓⍺= 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打造出“風格遷移”的AI藝術作品
我們開始看見若隱若現地出現一個立體主義畫派版的小貓咪!等演算法再迭代上幾次後:

如何用Keras打造出“風格遷移”的AI藝術作品

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

如何用Keras打造出“風格遷移”的AI藝術作品

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

如何用Keras打造出“風格遷移”的AI藝術作品

風格遷移後的作品:

如何用Keras打造出“風格遷移”的AI藝術作品

總結

在本文我們探究瞭如何用Keras應用“風格遷移”技術,不過我們還可以做很多工作,創造出更加迷人的作品:

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

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

  • 嘗試用多張內容照片和風格照片:你可以為損失函式增加幾張風格照片,混合多張照片或多種藝術風格。增加內容照片或許會帶來更有意思的藝術效果。

  • 增加總變分去噪方法:如果你仔細看看上面我得到的照片,你會發現上面有些顆粒狀圖案——小小的顏色旋渦。用神經網路處理照片通常都會有這個問題,其中一個原因就是照片的有失真壓縮被帶進了特徵圖譜裡。新增總變分去噪可以有效減輕這個問題,點選檢視這一步的程式碼

下面是我參考的一些資料,大家可以去看一看:

參考資料1

參考資料2


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

相關文章