【参考资料】
【1】https://github.com/walid0925/AI_Artistry
【2】A Neural Algorithm of Artistic Style
Note: 本文主要是对论文及参考文献【1】中代码的理解
概述
该算法的本质是利用深度卷积网络对图像输入的抽象,主要是三部分:
- 将风格图像输入卷积神经网络,将某些层输出作为风格特征(做一次);
- 将内容图像输入卷积神经网络,将某些层输出作为内容特征(做一次);
- 不断优化一个随机图像,使得它在该卷积神经网络的对应层输出不断接近上述两个图像的风格和内容特征(迭代);
如下图所示:
VGG16
VGG网络是牛津大学计算机视觉组和Google Deepmind研发的一种深度卷积网络。其特点在于反复的利用3x3的小型卷积核以及2x2的池化层。VGG16即16层的VGG网络,我们可以在keras-applications/vgg16.py中找到其模型实现。分析如下:
- 在vgg16的全连接层之间,总共分了5个block的卷积层
- block1由两层64个3x3卷积层,以及一个2x2的最大池化层,分别命名为block1_conv1、block1_conv2和block1_pool。源码如下(后续block代码类似不再赘述):
# Block 1
x = layers.Conv2D(64, (3, 3),
activation='relu',
padding='same',
name='block1_conv1')(img_input)
x = layers.Conv2D(64, (3, 3),
activation='relu',
padding='same',
name='block1_conv2')(x)
x = layers.MaxPooling2D((2, 2), strides=(2, 2), name='block1_pool')(x)
- block2由两层128个3x3卷积层和一个2x2最大池化层组成,分别命名为block2_conv1、block2_conv2和block2_pool
- block3由三层256个3x3卷积层和一个2x2最大池化层组成,分别命名为block3_conv1、block3_conv2、block3_conv3和block3_pool
- block4由三层512个3x3卷积层和一个2x2最大池化层组成,分别命名为block4_conv1、block4_conv2、block4_conv3和block4_pool
- block5由三层512个3x3卷积层和一个2x2最大池化层组成,分别命名为block5_conv1、block5_conv2、block5_conv3和block5_pool
- 最后是三个全连接层以softmax作为分类输出(在图像迁移中我们用不到这些层),如下:
x = layers.Flatten(name='flatten')(x)
x = layers.Dense(4096, activation='relu', name='fc1')(x)
x = layers.Dense(4096, activation='relu', name='fc2')(x)
x = layers.Dense(classes, activation='softmax', name = 'predictions')(x)
具体代码流程
1. 构建VGG16
利用VGG16构建三个神经网络,分别对应内容图像输入、风格图像输入和白噪声图像
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)
2. 利用VGG16的部分层输出获取风格特征和内容特征
内容特征获取层为’block4_conv2’
风格特征获取层为’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)
其中get_feature_rep函数就是获取神经网络在某些层的输出,注意的是这里对于风格特征需要将若干层拼接起来,而对于内容特征只取了其中一个维度,应表示RGB其中一种颜色。
for ln in layer_names:
selectedLayer = model.get_layer(ln)
featRaw = selectedLayer.output #获取该层的输出
3. 训练并输出
xopt, f_val, info= fmin_l_bfgs_b(calculate_loss, x_val, fprime=get_grad, maxiter=iterations, disp=True)
xOut = postprocess_array(xopt)
xIm = save_original_size(xOut)
核心的训练函数是这句,x_val即白噪声图像的输出。根据《A Neural Algorithm of Artistic Style》一文中的定义的损失函数和梯度计算方法,白噪声图像被不断优化,在一定迭代后,它的VGG16的对应层输出会不断接近风格图像和内容图像的对应层输出,因此形成了最终的效果。下面来看损失函数和梯度的计算方式:
备注:其他一些优化的paper基本思路都类似,只是在所选择卷积神经网路模型以及损失函数的定义上作了优化。
3.1 calculate_loss
def get_total_loss(gImPlaceholder, alpha=1.0, beta=10000.0):
#这里关键的几个步骤:
1. gImPlaceholder 就是白噪声图像,作为gModel的输入;这个输入应该在每次迭代都会被更新;
2. get_content_loss 计算其与内容特征的差异
3. get_style_loss 计算其余与风格内容特征的差异
4. 将上述差异计算总的损失值
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
def calculate_loss(gImArr):
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')
3.1 calculate_loss
F是噪声图像的内容特征输出;P是内容图像的特征输出
def get_content_loss(F, P):
cLoss = 0.5*K.sum(K.square(F - P))
return cLoss
3.2 get_style_loss
- Gs是噪声图像的风格特征输出,As是风格图像的特征输出
- get_Gram_matrix 构造的是一个Gram矩阵
Gram矩阵为其向量话特征的内积:
计算风格损失函数(每层):
总的损失函数:
其中
是每层的权重因子,本代码中为全1
def get_Gram_matrix(F):
G = K.dot(F, K.transpose(F))
return G
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
3.3 get_grad
def get_grad(gImArr):
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