首页 > 技术知识 > 正文

一、介绍

将一张图片的艺术风格应用在另外一张图片上 使用深度卷积网络CNN提取一张图片的内容和提取一张图片的风格, 然后将两者结合起来得到最后的结果

二、 方法

我们知道 CNN 可以捕捉图像的高层次特征,如上图所示,内容图片经过CNN可以得到对应的图像表述(representation, 就是经过卷积操作的feature map),然后经过重构可以得到近似原图的效果 特别是前面几层经过重构得到的结果和原图更接近,也说明前几层保留的图片细节会更多,因为后面还有pooling层,自然会丢弃调一些信息 这里的网络使用的是VGG-16 (如下图),包含 13 个卷积层,3 个全连接层

1、内容损失

假设一个卷积层包含 ${N_l}$ 个过滤器 filters,则可以得到 ${N_l}$ 个feature maps,假设 feature map的大小是 $M_l$ (长乘宽),则可以通过一个矩阵来存储 l 层的数据 $$F^l \in R^{N_l \times M_l} $$ $F^l_{i,j}$ 表示第 l 层的第 i 个filter 在 j 位置上的激活值 所以现在一张内容图片$\overrightarrow p$,一张生成图片$\overrightarrow x$(初始值为高斯分布), 经过一层卷积层l可以得到其对应的特征表示:$P^l$ 和 $Fl$, 则对应的损失采用均方误差: $$L{content}(\overrightarrow p, \overrightarrow x, l) = {1 \over 2} \sum{ij}(F^l{ij}-P^l_{ij})^2$$ $F$ 和 $P$是两个矩阵,大小是$N_l \times M_l$,即l层过滤器的个数 和 feature map 的长乘宽的值

2、风格损失

风格的表示这里采用格拉姆矩阵(Gram Matrix): $G^l \in R^{N_l \times Nl}$ $$G^l{ij} = {\sumk F^l{ik}F^l_{jk}}$$

格拉姆矩阵计算的是两两特征的相关性 , 即哪两个特征是同时出现的,哪两个特征是此消彼长的等,能够保留图像的风格 ( 比如一幅画中有人和树,它们可以出现在任意位置,格拉姆矩阵可以衡量它们之间的关系,可以认为是这幅画的风格信息 )

假设$\overrightarrow a$是风格图像,$\overrightarrow x$是生成图像,$A^l$ 和 $G^l$ 表示在 $l$ 层的格拉姆矩阵,则这一层的损失为:$$E_l = {1 \over 4N^2_lM^2l}{\sum{i,j} (G^l{ij}-A^l{ij})^2}$$

提取风格信息是我们会使用多个卷积层的输出,所以总损失为:$$L_{style}(\overrightarrow a, \overrightarrow x) = {\sum^L_lw_lE_l}$$

这里$w_l$是每一层损失的权重

3、总损失函数

通过白噪声初始化

(就是高斯分布)一个输出的结果,然后通过网络对这个结果进行风格和内容两方面的约束进行修正

$$L{total}(\overrightarrow p,\overrightarrow a,\overrightarrow x)=\alpha L{content}(\overrightarrow p, \overrightarrow x) +\beta L_{style}(\overrightarrow a, \overrightarrow x)$$

三、代码实现

1、说明

全部代码:点击查看 图像使用一张建筑图和梵高的星空

2、加载并预处理图片和初始化输出图片

输出图片采用高斯分布初始化 import numpy as np from keras import backend as K from keras.applications.vgg16 import preprocess_input from keras.preprocessing.image import load_img, img_to_array from keras.applications import VGG16 from scipy.optimize import fmin_l_bfgs_b from matplotlib import pyplot as plt 图片路径 content_image_path = ./data/buildings.jpg style_image_path = ./data/starry-sky.jpg generate_image_path = ./data/output.jpg 加载图片并初始化输出图片 target_height = 512 target_width = 512 target_size = (target_height, target_width) content_image = load_img(content_image_path, target_size=target_size) content_image_array = img_to_array(content_image) content_image_array = K.variable(preprocess_input(np.expand_dims(content_image_array, 0)), dtype=float32) style_image = load_img(style_image_path, target_size=target_size) style_image_array = img_to_array(style_image) style_image_array = K.variable(preprocess_input(np.expand_dims(style_image_array, 0)), dtype=float32) generate_image = np.random.randint(256, size=(target_width, target_height, 3)).astype(float64) generate_image = preprocess_input(np.expand_dims(generate_image, 0)) generate_image_placeholder = K.placeholder(shape=(1, target_width, target_height, 3))
<

3、获取网络中对应层的输出

def get_feature_represent(x, layer_names, model): 图片的特征图表示 参数 ———————————————- x : 输入, 这里并没有使用,可以看作一个输入的标识 layer_names : list CNN网络层的名字 model : CNN模型 返回值 ———————————————- feature_matrices : list 经过CNN卷积层的特征表示,这里大小是(filter个数, feature map的长*宽) feature_matrices = [] for ln in layer_names: select_layer = model.get_layer(ln) feature_raw = select_layer.output feature_raw_shape = K.shape(feature_raw).eval(session=tf_session) N_l = feature_raw_shape[-1] M_l = feature_raw_shape[1]*feature_raw_shape[2] feature_matrix = K.reshape(feature_raw, (M_l, N_l)) feature_matrix = K.transpose(feature_matrix) feature_matrices.append(feature_matrix) return feature_matrices
<

4、内容损失函数

def get_content_loss(F, P): 计算内容损失 参数 ————————————— F : tensor, float32 生成图片特征图矩阵 P : tensor, float32 内容图片特征图矩阵 返回值 ————————————— content_loss : tensor, float32 内容损失 content_loss = 0.5*K.sum(K.square(F-P)) return content_loss

5、Gram矩阵和风格损失

def get_gram_matrix(F): 计算gram矩阵 G = K.dot(F, K.transpose(F)) return G def get_style_loss(ws, Gs, As): 计算风格损失 参数 ————————————— ws : array 每一层layer的权重 Gs : list 生成图片每一层得到的特征表示组成的list As : list 风格图片每一层得到的特征表示组成的list style_loss = 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) style_loss += w*0.25*K.sum(K.square(G_gram-A_gram))/(N_l**2*M_l**2) return style_loss
<

6、总损失

def get_total_loss(generate_image_placeholder, alpha=1.0, beta=10000.0): 总损失 F = get_feature_represent(generate_image_placeholder, layer_names=[content_layer_name], model=gModel)[0] Gs = get_feature_represent(generate_image_placeholder, layer_names=style_layer_names, model=gModel) content_loss = get_content_loss(F, P) style_loss = get_style_loss(ws, Gs, As) total_loss = alpha*content_loss + beta*style_loss return total_loss def calculate_loss(gen_image_array): 调用总损失函数,计算得到总损失数值 if gen_image_array != (1, target_width, target_height, 3): gen_image_array = gen_image_array.reshape((1, target_width, target_height, 3)) loss_fn = K.function(inputs=[gModel.input], outputs=[get_total_loss(gModel.input)]) return loss_fn([gen_image_array])[0].astype(float64)

7、损失函数梯度

def get_grad(gen_image_array): 计算损失函数的梯度 if gen_image_array != (1, target_width, target_height, 3): gen_image_array = gen_image_array.reshape((1, target_width, target_height, 3)) grad_fn = K.function([gModel.input], K.gradients(get_total_loss(gModel.input), [gModel.input])) grad = grad_fn([gen_image_array])[0].flatten().astype(float64) return grad

8、生成结果后处理

因为之前preprocess_input函数中做了处理,这里进行逆处理还原

def postprocess_array(x): 生成图片后处理,因为之前preprocess_input函数中做了处理,这里进行逆处理还原 if x.shape != (target_width, target_height, 3): x = x.reshape((target_width, target_height, 3)) x[…, 0] += 103.939 x[…, 1] += 116.779 x[…, 2] += 123.68 x = x[…, ::-1] # BGR–>RGB x = np.clip(x, 0, 255) x = x.astype(uint8) return x

9、定义模型并优化

定义VGG模型 tf_session = K.get_session() cModel = VGG16(include_top=False, input_tensor=content_image_array) sModel = VGG16(include_top=False, input_tensor=style_image_array) gModel = VGG16(include_top=False, input_tensor=generate_image_placeholder) content_layer_name = block4_conv2 style_layer_names = [ block1_conv1, block2_conv1, block3_conv1, block4_conv1 ] 得到对应的representation矩阵 P = get_feature_represent(x=content_image_array, layer_names=[content_layer_name], model=cModel)[0] As = get_feature_represent(x=style_image_array, layer_names=style_layer_names, model=sModel) ws = np.ones(len(style_layer_names))/float(len(style_layer_names)) 使用fmin_l_bfgs_b进行损失函数优化 iterations = 600 x_val = generate_image.flatten() xopt, f_val, info = fmin_l_bfgs_b(func=calculate_loss, x0=x_val, fprime=get_grad, maxiter=iterations, disp=True) x_out = postprocess_array(xopt)
<

10、输出结果

初始化输出图片

迭代200次,${\beta \over \alpha} = 10^3$

迭代500轮,${\beta \over \alpha} = 10^4$

四、总结

style tranfer通过白噪声初始化(就是高斯分布)一个输出的结果,然后通过优化损失对这个结果进行风格内容两方面的约束修正 图片的风格信息使用的是 Gram矩阵来表示 其中超参数风格损失的权重ws、内容损失和风格损失的权重$\alpha$, $\beta$可以进行调整查看结果 论文给出的${\beta \over \alpha} = 10^3或10^4$结果较好,可以自己适当增加看看最后的结果

Reference

Paper: https://arxiv.org/pdf/1508.06576.pdf https://www.cs.toronto.edu/~frossard/post/vgg16/ https://zhuanlan.zhihu.com/p/33910138

猜你喜欢