StyleTransfer
以下示例受TensorFlow 官方教程的启发,同时也参考了这篇博客文章。使用 CNTK 框架进行风格迁移的另一个优秀示例可以在这里找到。这是关于艺术风格迁移的原始论文。
风格迁移的主要思想如下:
- 从白噪声开始,我们尝试优化当前图像 $x$,以最小化某个损失函数
- 损失函数由三个部分组成 $\mathcal{L(x)} = \alpha\mathcal{L}_c(x,i) + \beta\mathcal{L}_s(x,s)+\gamma\mathcal{L}_t(x)$
- $\mathcal{L}_c$ - 内容损失 - 表示当前图像 $x$ 与原始图像 $i$ 的接近程度
- $\mathcal{L}_s$ - 风格损失 - 表示当前图像 $x$ 与风格图像 $s$ 的接近程度
- $\mathcal{L}_t$ - 总变分损失(在我们的示例中不会考虑) - 确保生成的图像是平滑的,即表示图像 $x$ 的相邻像素之间的均方误差
这些损失函数需要以巧妙的方式设计,例如风格损失应对应于图像风格的相似性,而不是实际内容。为此,我们将比较一个 CNN 的一些深层特征层,这些特征层会对图像进行分析。
让我们先加载几张图片:
!mkdir -p images
!curl https://cdn.pixabay.com/photo/2016/05/18/00/27/franz-marc-1399594_960_720.jpg > images/style.jpg
!curl https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Golden_tabby_and_white_kitten_n01.jpg/1280px-Golden_tabby_and_white_kitten_n01.jpg > images/image.jpg % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 210k 100 210k 0 0 2670k 0 --:--:-- --:--:-- --:--:-- 2670k
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 131k 100 131k 0 0 459k 0 --:--:-- --:--:-- --:--:-- 459k
import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import tensorflow as tf
from tensorflow.keras.applications.vgg16 import preprocess_input
import IPython.display as display让我们加载这些图像并将它们调整为 $512\times512$。同时,我们将生成结果图像 img_result 作为一个随机数组。
img_size = 256
def load_image(fn):
x = cv2.imread(fn)
return cv2.cvtColor(x, cv2.COLOR_BGR2RGB)
img_style = load_image('images/style.jpg')
img_content = load_image('images/image.jpg')
img_content = img_content[:,200:200+857,:]
img_content = cv2.resize(img_content,(img_size,img_size))
img_style = img_style[:,200:200+671,:]
img_style = cv2.resize(img_style,(img_size,img_size))
img_result = np.random.uniform(size=(img_size,img_size,3))
matplotlib.rcParams['figure.figsize'] = (12, 12)
matplotlib.rcParams['axes.grid'] = False
fig,ax = plt.subplots(1,3)
ax[0].imshow(img_content)
ax[1].imshow(img_style)
ax[2].imshow((255*img_result).astype(int))
plt.show()要计算风格损失和内容损失,我们需要在由CNN提取的特征空间中进行操作。我们可以使用不同的CNN架构,但为了简单起见,在我们的案例中我们将选择预训练于ImageNet的VGG-19。
vgg = tf.keras.applications.VGG16(include_top=False, weights='imagenet')
vgg.trainable = False让我们来看看模型架构:
vgg.summary()Model: "vgg16"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_3 (InputLayer) [(None, None, None, 3)] 0
block1_conv1 (Conv2D) (None, None, None, 64) 1792
block1_conv2 (Conv2D) (None, None, None, 64) 36928
block1_pool (MaxPooling2D) (None, None, None, 64) 0
block2_conv1 (Conv2D) (None, None, None, 128) 73856
block2_conv2 (Conv2D) (None, None, None, 128) 147584
block2_pool (MaxPooling2D) (None, None, None, 128) 0
block3_conv1 (Conv2D) (None, None, None, 256) 295168
block3_conv2 (Conv2D) (None, None, None, 256) 590080
block3_conv3 (Conv2D) (None, None, None, 256) 590080
block3_pool (MaxPooling2D) (None, None, None, 256) 0
block4_conv1 (Conv2D) (None, None, None, 512) 1180160
block4_conv2 (Conv2D) (None, None, None, 512) 2359808
block4_conv3 (Conv2D) (None, None, None, 512) 2359808
block4_pool (MaxPooling2D) (None, None, None, 512) 0
block5_conv1 (Conv2D) (None, None, None, 512) 2359808
block5_conv2 (Conv2D) (None, None, None, 512) 2359808
block5_conv3 (Conv2D) (None, None, None, 512) 2359808
block5_pool (MaxPooling2D) (None, None, None, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 0
Non-trainable params: 14,714,688
_________________________________________________________________
让我们定义一个函数,以便我们从VGG网络中提取中间特征:
def layer_extractor(layers):
outputs = [vgg.get_layer(x).output for x in layers]
model = tf.keras.Model([vgg.input],outputs)
return model 内容损失#
内容损失 用于衡量当前图像 $x$ 与原始图像的接近程度。它通过查看卷积神经网络(CNN)的中间特征层,并计算平方误差。在第 $l$ 层的内容损失定义为: $$ \mathcal{L}c = {1\over2}\sum{i,j} (F_{ij}^{(l)}-P_{ij}^{(l)})^2 $$ 其中 $F^{(l)}$ 和 $P^{(l)}$ 分别表示第 $l$ 层的特征。
content_layers = ['block4_conv2']
content_extractor = layer_extractor(content_layers)
content_target = content_extractor(preprocess_input(tf.expand_dims(img_content,axis=0)))
def content_loss(img):
z = content_extractor(preprocess_input(tf.expand_dims(255*img,axis=0)))
return 0.5*tf.reduce_sum((z-content_target)**2)现在我们将实现风格迁移的主要技巧——优化。我们将从随机图像开始,然后使用 TensorFlow 优化器调整这张图像,以最小化内容损失。
重要提示:在我们的案例中,所有计算都是通过支持 GPU 的 TensorFlow 框架进行的,这使得代码在 GPU 上运行效率更高。
img = tf.Variable(img_result)
opt = tf.optimizers.Adam(learning_rate=0.002, beta_1=0.99, epsilon=1e-1)
clip = lambda x : tf.clip_by_value(x,clip_value_min=0,clip_value_max=1)
def optimize(img,loss_fn):
with tf.GradientTape() as tape:
loss = loss_fn(img)
grad = tape.gradient(loss,img)
opt.apply_gradients([(grad,img)])
#img.assign(tf.clip_by_value(img,clip_value_min=0,clip_value_max=1))
def train(img,loss_fn,epochs=10,steps_per_epoch=100):
for _ in range(epochs):
display.clear_output(wait=True)
plt.imshow((255*clip(img)).numpy().astype(int))
plt.show()
for _ in range(steps_per_epoch):
optimize(img,loss_fn=loss_fn)
train(img,content_loss)练习:尝试在网络中使用不同的层,看看会发生什么。你也可以尝试同时优化多个层,但需要稍微修改一下
content_loss的代码。
风格损失#
风格损失是风格迁移的核心思想。我们比较的不是实际的特征,而是它们的 Gram 矩阵,定义为 $$G=A\times A^T$$。
Gram 矩阵类似于相关矩阵,它展示了某些滤波器之间的依赖关系。风格损失是从不同层计算的损失之和,这些损失通常会乘以加权系数。
风格迁移的总损失函数是 内容损失 和 风格损失 的总和。
def gram_matrix(x):
result = tf.linalg.einsum('bijc,bijd->bcd', x, x)
input_shape = tf.shape(x)
num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
return result/(num_locations)
style_layers = ['block1_conv1','block2_conv1','block3_conv1','block4_conv1']
def style_extractor(img):
return [gram_matrix(x) for x in layer_extractor(style_layers)(img)]
style_target = style_extractor(preprocess_input(tf.expand_dims(img_style,axis=0)))
def style_loss(img):
z = style_extractor(preprocess_input(tf.expand_dims(255*img,axis=0)))
loss = tf.add_n([tf.reduce_mean((x-target)**2)
for x,target in zip(z,style_target)])
return loss / len(style_layers)
综合起来#
我们将定义 total_loss 函数来计算综合损失,并执行优化:
def total_loss(img):
return 2*content_loss(img)+style_loss(img)
img.assign(img_result)
train(img,loss_fn=total_loss)下面的代码执行实际的损失优化。请注意,即使使用 GPU,优化也需要相当长的时间。您可以多次运行下面的单元格以改善结果。
添加变化损失#
变化损失可以减少图像中的噪点,通过最小化相邻像素之间的差异来实现。
我们还将从原始内容图像开始优化,这样可以在图像中保留更多的内容细节,而不会使内容损失函数变得复杂。不过,我们会添加一些噪点。
def variation_loss(img):
img = tf.cast(img,tf.float32)
x_var = img[ :, 1:, :] - img[ :, :-1, :]
y_var = img[ 1:, :, :] - img[ :-1, :, :]
return tf.reduce_sum(tf.abs(x_var)) + tf.reduce_sum(tf.abs(y_var))
def total_loss_var(img):
return content_loss(img)+150*style_loss(img)+30*variation_loss(img)
img.assign(clip(np.random.normal(-0.3,0.3,size=img_content.shape)+img_content/255.0))
train(img,loss_fn=total_loss_var)cv2.imwrite('result.jpg',(img.numpy()[:,:,::-1]*255))(256, 256, 3)
True免责声明:
本文档使用AI翻译服务 Co-op Translator 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。我们对因使用此翻译而引起的任何误解或误读不承担责任。