1.1 图像风格迁移
图像风格迁移是指把图像A的风格变成图像B的风格。例如把梵高的星空的风格用作于画蒙娜丽莎的微笑。这里利用卷积神经网络成功实现图像的风格迁移。下图是一个风格迁移的例子。
2 卷积神经网络简介
2.1 输入层
该层是对输入的图像数据进行预处理,使其满足后续处理的要求,包括:
去均值:其目的是把图像数据在坐标轴上以原点为中心,如下图所示
归一化:输入的内容图片和风格图片的取值范围可能不同,会产生误差。归一化就是把两者的取值范围都变成0-1,如下图所示。
其他处理:如改变输入的图像的数据类型,改变大小等,目的是方便所选择的方法在后续的处理
下面给出使用的部分预处理代码。
#图像预处理,主要是把图像变为合适的tensor
def preprocess(img, size=512):
transform = T.Compose([
T.Scale(size), #对图像进行等比缩放,最小边长度为size
T.ToTensor(), #转化为torch.tensor
#标准化,每个通道内的数减去通道内平均值,然后除以标准差
# 中心化,减去平均值,使得样本的的中心在坐标原点
T.Normalize(mean=SQUEEZENET_MEAN.tolist(),
std=SQUEEZENET_STD.tolist()),
#将tensor升高一维度,如a.shape = (1, 2),
T.Lambda(lambda x: x[None]), a[None].shape = (1, 1, 2)
])
return transform(img)
#反处理,跟上面相反
def deprocess(img):
transform = T.Compose([
T.Lambda(lambda x: x[0]), #降维
T.Normalize(mean=[0, 0, 0], std=[1.0 / s for s in SQUEEZENET_STD.tolist()]), #相当于乘标准差
T.Normalize(mean=[-m for m in SQUEEZENET_MEAN.tolist()], std=[1, 1, 1]), #相当于加回原本的平均值
T.Lambda(rescale), #
T.ToPILImage(),
])
return transform(img)
#归一化,把范围都变成0-1,减少各维度数据取值范围的差异而带来的干扰
def rescale(x):
low, high = x.min(), x.max()
x_rescaled = (x - low) / (high - low)
return x_rescaled
#计算x, y的相对误差
def rel_error(x,y):
return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))
2.2 卷积层
该层是卷积神经网络最重要的一部分。通过卷积层可以提取图像的特征。
为什么卷积可以提取图像特征?
某一图像与对应特征的卷积进行点积会得到较大的值,而对于不能反应特征的卷积得到的点积会非常小,那么对应的卷积就能反应特征值。事先准备好的卷积,通过与原图像点积得到的结果,能提取、反应出该图像的某一特征。
通过下面的表格简单解释一下。看上面两张表,左是卷积权重,很明显代表一条斜线,右是图像的二维表示,两者的点积是40。而下面的两张表,右的图像变成方向相反的斜线,两者点积是0。所以该卷积可以提取、反应向着右下角的斜线这一特征。
1 | 0 | 0 | 0 | 10 | 0 | 0 | 0 | |
0 | 1 | 0 | 0 | 0 | 10 | 0 | 0 | |
0 | 0 | 1 | 0 | 0 | 0 | 10 | 0 | |
0 | 0 | 0 | 1 | 0 | 0 | 0 | 10 |
————————————————————————————————
1 | 0 | 0 | 0 | 0 | 0 | 0 | 10 | |
0 | 1 | 0 | 0 | 0 | 0 | 10 | 0 | |
0 | 0 | 1 | 0 | 0 | 10 | 0 | 0 | |
0 | 0 | 0 | 1 | 10 | 0 | 0 | 0 |
这里使用的卷积神经网络包含了多个卷积层,每层的权重参数是固定不变的,每层只关注特定的特征,这些所有的卷积层就可以把图像的特征提取完全。一般来说层数越高提取的特征是抽象的轮廓、大小等,层数低的提取出局部的细节文理。卷积层也可以认为是一个神经网络,但它是已经训练好的,只是把它当作工具提取特征。
#用squeezeNet轻量级卷积神经网络提取特征,每经过一层网络就将结果存到features
#每层网络的输出提取的是不同层次的特征
def extract_features(x, cnn):
features = []
prev_feat = x
for i, module in enumerate(cnn._modules.values()):
# 使用上一层训练的结果作为这一层的输入
next_feat = module(prev_feat)
features.append(next_feat)
# 让下一层训练这一层得到的结果
prev_feat = next_feat
return features
2.3 激励层
卷积层输出结果做非线性映射。一般都会采用ReLU,它的特点是收敛快,求梯度简单,但较脆弱。这里没有过多讲述的必要。
2.4 池化层
池化层夹在连续的卷积层中间, 用于压缩数据和参数的量,减小过拟合。这里输入的是图像,那么池化层的最主要作用就是压缩图像。这里只是简单介绍一下池化层。
池化层的具体作用:
⑴特征不变性,也就是我们在图像处理中经常提到的特征的尺度不变性。图像压缩时去掉的信息只是一些无关紧要的信息,而留下的信息则是具有尺度不变性的特征,是最能表达图像的特征。例如把一张狗的图片压缩后,我们仍然能看出这是狗。
⑵特征降维,我们知道一幅图像含有的信息是很大的,特征也很多,但是有些信息对于我们做图像任务时没有太多用途或者有重复,我们可以把这类冗余信息去除,把最重要的特征抽取出来。
⑶在一定程度上防止过拟合,更方便优化。
2.5 全连接层
和传统神经网络的神经元结构几乎是一样的,由多层神经元构成,两层之间所有神经元都有权重连接。在这里进行前后传递、更新参数,从而完成任务目标。
3 更细节的介绍
介绍完卷积神经网络,再简单的介绍一下风格迁移的基本流程。当训练开始时,根据内容图片和噪声,生成一张噪声图片。将噪声图片传送给网络,计算loss,再根据loss调整噪声图片。将调整后的图片发给网络,重新计算loss,再调整,再计算,直到达到指定迭代次数或者效果达到预期要求,这时,噪声图片已兼具内容图片的内容和风格图片的风格,进行保存即可。
3.1 格拉姆矩阵
这里使用gram矩阵计算图像三个通道取得的特征值的内积。gram 矩阵的每个值可以说是代表三个通道的互相关程度。对于一个大小为F(m, n, c)的图像,先变换格式变成大小为F(m*n, c)的矩阵,那么它的格拉姆矩阵就是:,其中k是通道数,Gram矩阵在一定程度上可以体现图片的风格。
3.2 损失函数
风格迁移的损失分为内容损失和风格损失,分别对应噪声图片与内容、风格图片在内容、风格上的差异。内容损失较简单,就是两图像对应点的L2范数乘上权重的和,如 ∑w[i, j] * (F[i, j] – G[i, j])2 。风格损失要用到卷积层的结果,卷积层已经提取了图片的各种特征,我们再对每层的卷积的结果求取格拉姆矩阵。多层的风格损失是单层风格损失的加权累加。那么风格损失就是两图像的格拉姆矩阵的L2范数乘上权重的和,如。最终的loss就是内容损失 + 风格损失。根据反向传播,调整参数使得loss最小,那么噪声图片就是得到的最终结果。这里我们额外增加了一个图像自身的光滑损失,用于判断噪声图像是否足够光滑,避免生成有割裂感的图像。
下面是损失函数的相关代码。
#内容损失很简单,就是将生成图像和内容图像均表示为torch.tensor
#shape是一样的,都是(3, height, width),然后求L2范数
# 在内容方面的相似度,这个内容相似度一般是用于比较内容图片和结果图片
def content_loss(content_weight, content_current, content_target):
N, C, H, W = content_current.shape
L = content_weight * (content_target - content_current).pow(2).sum()
return L
# 风格损失:比较风格图片和结果图片的风格
def style_loss(feats, style_layers, style_targets, style_weights):
style_loss = 0
#feats为生成图像所有卷积层的输出
#风格图像的某些层的格拉姆矩阵为style_targets,层的索引位于style_layers
#计算特定层(style_layers)生成图像与风格图像的Gram Matrix差值的L2范数并加权求和
for i in range(len(style_layers)):
l = style_layers[i]
G = gram_matrix(feats[l])
# 风格损失:两个格拉姆矩阵的L范数
# 多层的风格损失是单层风格损失的加权累加。
style_loss += style_weights[i] * ((G - style_targets[i]) ** 2).sum()
return style_loss
4 成果展示
下面是生成噪声图像并不断迭代更新的过程的代码。
def style_transfer(content_image, style_image, image_size, style_size, content_layer, content_weight,
style_layers, style_weights, tv_weight, init_random = False):
# Extract features for the content image
#读取内容图像并提取特征
content_img = preprocess(PIL.Image.open(content_image), size=image_size)
content_img_var = Variable(content_img.type(dtype))
feats = extract_features(content_img_var, cnn)
content_target = feats[content_layer].clone()
# Extract features for the style image
#读取风格图像并提取特征,把某些层(style_layers, 函数参数)的输出的格拉姆矩阵作为style_targets
style_img = preprocess(PIL.Image.open(style_image), size=style_size)
style_img_var = Variable(style_img.type(dtype))
feats = extract_features(style_img_var, cnn)
style_targets = []
for idx in style_layers:
style_targets.append(gram_matrix(feats[idx].clone()))
# Initialize output image to content image or nois
#初始化一张噪声图像
if init_random:
img = torch.Tensor(content_img.size()).uniform_(0, 1)
else:
img = content_img.clone().type(dtype)
# We do want the gradient computed on our image!
img_var = Variable(img, requires_grad=True)
# Set up optimization hyperparameters
#初始学习率与学习率衰减参数设定
initial_lr = 3.0
decayed_lr = 0.1
decay_lr_at = 180
# Note that we are optimizing the pixel values of the image by passing
# in the img_var Torch variable, whose requires_grad flag is set to True
#Adam优化器
optimizer = torch.optim.Adam([img_var], lr=initial_lr)
#显示内容图像和风格图像,这里的content_img已经经过预处理,为torch.tensor,所以需要deprocess还原为图像
f, axarr = plt.subplots(1,2)
axarr[0].axis('off')
axarr[1].axis('off')
axarr[0].set_title('Content Source Img.')
axarr[1].set_title('Style Source Img.')
axarr[0].imshow(deprocess(content_img.cpu()))
axarr[1].imshow(deprocess(style_img.cpu()))
plt.show()
plt.figure()
#迭代
for t in range(200):
#截断函数,将不在[-1.5, 1.5]范围的数限制在这个范围,小于-1.5就变为-1.5,超过1.5就变为1.5(我也不知道为什么要这么做)
if t < 190:
img.clamp_(-1.5, 1.5)
optimizer.zero_grad()
#提取目标图像的特征
feats = extract_features(img_var, cnn)
# Compute loss
#计算各种损失并反向传播
c_loss = content_loss(content_weight, feats[content_layer], content_target)
s_loss = style_loss(feats, style_layers, style_targets, style_weights)
t_loss = tv_loss(img_var, tv_weight)
loss = c_loss + s_loss + t_loss
loss.backward()
# Perform gradient descents on our image values
if t == decay_lr_at:
optimizer = torch.optim.Adam([img_var], lr=decayed_lr)
optimizer.step()
if t % 10 == 0:
print('Iteration {}'.format(t))
plt.axis('off')
plt.imshow(deprocess(img.cpu()))
plt.show()
print('Iteration {}'.format(t))
plt.axis('off')
plt.imshow(deprocess(img.cpu()))
plt.show()
接下来展示一下代码结果,这里每50次展示一下生产的噪声图像。
根据结果,大概进行两百次操作就可以较好地实现图片的风格迁移。
卷积神经网络参考文献:
机器学习算法之——卷积神经网络(CNN)原理讲解 - 知乎 (zhihu.com)