在以前,AI被认为是与艺术无关,AI也无法创作艺术作品。而GAN家族、风格迁移、Diffusion模型的出现,让AI也能创作有艺术风格的图像。其中最简单的就是风格迁移。风格迁移是一种把图像A的风格迁移到图像B的一种算法,利用该算法可以将“梵高的风格”迁移到其它任意图片上。今天我们要分享的就是风格迁移的实现。
风格迁移是一种将目标图像的风格转移到特定图像的算法。在该算法中,有两个输入图像,一个图像负责贡献内容,一个图像负责贡献风格。在风格迁移中,一个重要的问题是如何定义风格。
风格比较抽象,在某些例子中可以很容易理解。比如梵高的《星空》、莫奈的《印象 日出》,虽然无法用文字描述,但是我们可以感觉到其中的风格。
在后面我们会用特征图解释风格的具体含义。
在风格迁移里面,我们要做的就是把一幅不属于梵高的作品,变得像梵高的作品;一幅不像莫奈的作品,变得像莫奈的作品。比如下面的例子:
假如输入风格图像A和内容图像B,那么我们希望输出图像C的风格与A相似,内容与B相似。这里的相似需要有一个衡量的依据,一个简单的想法就是像素值。不过这个明显是不可行的,不管是内容还是风格,用像素值作为衡量依据都会导致泛化能力非常差,因此需要改用其它方法。
在深度学习中,我们喜欢用卷积神经网络提取的特征图来作为衡量依据。特征图有许多良好的性质,比如对位置不敏感、多尺度特征。
VGG是比较常用的一种卷积神经网络,在本例中我们使用卷积神经网络来提取图片特征。下图是VGG19的网络结构:
在VGG19中,有5组卷积神经网络。左边的卷积层提取纹理、边缘等低级特征,越往右提取的特征越抽象。
风格迁移任务不同于其它网络,在以往任务中,我们做的是准备数据,更新网络权重。而在本例中,我们要做的是利用卷积提取图片特征,然后通过内容相似度、风格相似度的信息来修改输入的图片。所以我们需要有一个函数用来评估内容相似度,这里我们称为内容损失。
内容是图片整体的信息,比较抽象,需要在较右的卷积核才能提取,这里选择conv4_1的输出作为图片内容。这一层右N(512)个特征图,因此把内容损失定义为:
其中f为向量化后的特征图,D为特征图的大小,这里就是计算两个图片的特征图的均方误差。
风格损失则更复杂一点。风格不只反映了图片的纹理,也反映了图像的整体风格。因此在考虑风格损失的时候,需要考虑各层特征图的内容。
通常用同一层不同特征图的Gram矩阵来表示。Gram矩阵定义为:N个特征图,将特征图向量化后记为f,则第i和j个向量的内积就是Gram矩阵的第(i, j)个元素:
此时某一层的风格损失可以定义为:
因为风格损失需要考虑多层,因此整体的风格损失可以定义为:
其值为各层特征图的加权和。最后我们可以用Ls和Lc的加权和作为最终的损失:
下面使用PyTorch实现迁移学习。
首先模型需要三个输入,分别是内容图片、风格图片、结果图片。结果图片是用来更新的图片,我们可以用随机噪声来生成,也可以用内容图片来代替,加载图片的代码如下:
import cv2
import torch
import torchvision.models as models
import torch.nn.functional as F
import torch.nn as nn
from PIL import Image
from torchvision.transforms import Compose, ToTensor, Resize
transform = Compose([
Resize((512, 512)),
ToTensor(),
])
def load_images(content_path, style_path):
content_img = Image.open(content_path)
image_size = content_img.size
content_img = transform(content_img).unsqueeze(0).cuda()
style_img = Image.open(style_path)
style_img = transform(style_img).unsqueeze(0).cuda()
var_img = content_img.clone()
var_img.requires_grad = True
return content_img, style_img, var_img, image_size
content_img, style_img, var_img, image_size = load_images('content.jpeg', 'style.png')
这里就是读取了两个图片,并复制了内容图片,另外我们将结果图片var_img设置为计算梯度,因为后续我们需要根据梯度更新该图片。
提取特征我们使用的是预训练的vgg19网络,而且不需要更新网络权重,另外我们提取的特征包括用于计算内容损失和风格损失的,为了方便,这里构造一个类用于提取特征:
# 加载预训练模型,使其不计算梯度
model = models.vgg19(pretrained=True).cuda()
batch_size = 1
for params in model.parameters():
params.requires_grad = False
model.eval()
# 用于归一化和反归一化
mu = torch.Tensor([0.485, 0.456, 0.406]).unsqueeze(-1).unsqueeze(-1).cuda()
std = torch.Tensor([0.229, 0.224, 0.225]).unsqueeze(-1).unsqueeze(-1).cuda()
unnormalize = lambda x: x * std + mu
normalize = lambda x: (x - mu) / std
# 构造层用于提取特征
class FeatureExtractor(nn.Module):
def __init__(self, model):
super().__init__()
self.module = model.features.cuda().eval()
self.con_layers = [22]
self.sty_layers = [1, 6, 11, 20, 29]
for name, layer in self.module.named_children():
if isinstance(layer, nn.MaxPool2d):
self.module[int(name)] = nn.AvgPool2d(kernel_size=2, stride=2)
def forward(self, tensor: torch.Tensor) -> dict:
sty_feat_maps = []
con_feat_maps = []
x = normalize(tensor)
for name, layer in self.module.named_children():
x = layer(x)
if int(name) in self.con_layers: con_feat_maps.append(x)
if int(name) in self.sty_layers: sty_feat_maps.append(x)
return {"content_features": con_feat_maps, "style_features": sty_feat_maps}
model = FeatureExtractor(model)
style_target = model(style_img)["style_features"]
content_target = model(content_img)["content_features"]
这里提取的两个target就是我们需要特征图。
对于风格损失,我们还需要计算grim矩阵。即计算风格特征图两两之间的点积,代码如下:
gram_target = []
for i in range(len(style_target)):
b, c, h, w = style_target [i].size()
tensor_ = style_target[i].view(b * c, h * w)
gram_i = torch.mm(tensor_, tensor_.t()).div(b * c * h * w)
gram_target.append(gram_i)
gram_target里面就存储了Gram矩阵的内容。
最后我们要做的就是定义训练的过程,以往我们训练是更新网络权重,今天我们训练是更新结果图片,也就是var_img,代码与以往更新基本一致:
optimizer = torch.optim.Adam([var_img], lr=0.01, betas=(0.9, 0.999), eps=1e-8)
lam1 = 1e-3
lam2 = 1e7
lam3 = 5e-3
for itera in range(20001):
optimizer.zero_grad()
output = model(var_img)
sty_output = output["style_features"]
con_output = output["content_features"]
con_loss = torch.tensor([0]).cuda().float()
# 计算内容图片和结果图像的内容损失
for i in range(len(con_output)):
con_loss = con_loss + F.mse_loss(con_output[i], con_target[i])
# 计算风格图片和结果图像的风格损失
sty_loss = torch.tensor([0]).cuda().float()
for i in range(len(sty_output)):
b, c, h, w = sty_output[i].size()
tensor_ = sty_output[i].view(b * c, h * w)
gram_i = torch.mm(tensor_, tensor_.t()).div(b * c * h * w)
sty_loss = sty_loss + F.mse_loss(gram_i, gram_target[i])
b, c, h, w = style_img.size()
TV_loss = (torch.sum(torch.abs(style_img[:, :, :, :-1] - style_img[:, :, :, 1:])) +
torch.sum(torch.abs(style_img[:, :, :-1, :] - style_img[:, :, 1:, :]))) / (b * c * h * w)
loss = con_loss * lam1 + sty_loss * lam2 + TV_loss * lam3
loss.backward()
var_img.data.clamp_(0, 1)
optimizer.step()
if itera % 100 == 0:
print('itera: %d, con_loss: %.4f, sty_loss: %.4f, TV_loss: %.4f' % (itera,
con_loss.item() * lam1,
sty_loss.item() * lam2,
TV_loss.item() * lam3), '\n\t total loss:',
loss.item())
print('var_img mean:%.4f, std:%.4f' % (var_img.mean().item(), var_img.std().item()))
if itera % 1000 == 0:
save_img = var_img.clone()
save_img = torch.clamp(save_img, 0, 1)
save_img = save_img[0].permute(1, 2, 0).data.cpu().numpy() * 255
save_img = save_img[..., ::-1].astype('uint8')
save_img = cv2.resize(save_img, image_size)
cv2.imwrite('outputs/transfer%d.jpg' % itera, save_img)
最后我们运行即可。下面是一些效果图片:
从左到右依次是风格图像,内容图像,迭代1000次后的结果图像,迭代3000次后的结果图像。从结果来看,我们确实完成了风格迁移。
阅读量:1674
点赞量:0
收藏量:0