過去幾年,卷積神經網路(CNN)成為一種前沿的計算機視覺工具,在業界和學界廣泛應用。除了人臉識別和無人駕駛領域,CNN 這幾年還在藝術領域廣受歡迎,其中衍生出一個代表性技術就是“風格遷移”,根據這項技術誕生了很多美圖應用,比如 2016 年大火的 Prisma APP。
“風格遷移”是展示神經網路強大能力的一個很有趣的途徑。2015 年,德國和美國的一組研究人員釋出了一篇論文《A Neural Algorithm of Artistic Style》 詳細討論了深度卷積神經網路如何區分照片中的“內容”和“風格”。論文作者展示了 CNN如何能夠將一張照片的藝術風格應用在另一張照片上,生成一張全新的令人眼前一亮的照片。而且他們的方法不需要訓練一個新的神經網路,使用來自 ImageNet 這類資料集中的預訓練權重就有很好的效果。
在本文,我(作者 Walid Ahmad——譯者注)會展示如何用流行的 Python 程式庫 Keras 創作“風格遷移”的 AI 作品,整體思路和上面這篇論文的方法一致。本文的全部程式碼點選這裡獲取。
使用兩張基本的影像素材,我們就能創造出下面這樣的 AI 藝術作品:
我們要解決的這個問題是現在有了兩張基本影像素材,我們想把它們“合併”在一起。其中一張照片的內容我們希望能夠保留,我們把這張照片稱為 p。在我舉的這個例子中,我從谷歌上隨便搜了一張可愛的貓咪照片:
另一張基本影像的藝術風格我們希望能夠保留,我們稱它為 a。我選了一張巴洛克風格的著名照片:《Violin on Palette》。
最後,我們會得到一張生成照片 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的特徵表示之間的均方誤差。內容損失函式為:
在這裡,
-
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,或者將不同的神經網路架構結合在一起。
-
嘗試用多張內容照片和風格照片:你可以為損失函式增加幾張風格照片,混合多張照片或多種藝術風格。增加內容照片或許會帶來更有意思的藝術效果。
-
增加總變分去噪方法:如果你仔細看看上面我得到的照片,你會發現上面有些顆粒狀圖案——小小的顏色旋渦。用神經網路處理照片通常都會有這個問題,其中一個原因就是照片的有失真壓縮被帶進了特徵圖譜裡。新增總變分去噪可以有效減輕這個問題,點選檢視這一步的程式碼。
下面是我參考的一些資料,大家可以去看一看:
歡迎關注我們,學習資源,AI教程,論文解讀,趣味科普,你想看的都在這裡!