本文为稀土技能社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!

一、前言

在曾经,AI被认为是与艺术无关,AI也无法创作艺术著作。而GAN宗族、风格搬迁、Diffusion模型的出现,让AI也能创作有艺术风格的图画。其中最简略的便是风格搬迁。风格搬迁是一种把图画A的风格搬迁到图画B的一种算法,运用该算法能够将“梵高的风格”搬迁到其它恣意图片上。今日咱们要共享的便是风格搬迁的完成。

二、风格搬迁

2.1 风格搬迁介绍

风格搬迁是一种将目标图画的风格转移到特定图画的算法。在该算法中,有两个输入图画,一个图画负责奉献内容,一个图画负责奉献风格。在风格搬迁中,一个重要的问题是如何界说风格。

风格比较笼统,在某些例子中能够很容易理解。比方梵高的《星空》、莫奈的《形象 日出》,虽然无法用文字描述,但是咱们能够感觉到其中的风格。

生成梵高风格的图片

在后面咱们会用特征图解说风格的具体意义。

在风格搬迁里面,咱们要做的便是把一幅不属于梵高的著作,变得像梵高的著作;一幅不像莫奈的著作,变得像莫奈的著作。比方下面的例子:

生成梵高风格的图片

2.2 完成原理

2.2.1 特征图

假如输入风格图画A和内容图画B,那么咱们希望输出图画C的风格与A类似,内容与B类似。这儿的类似需要有一个衡量的依据,一个简略的主意便是像素值。不过这个明显是不可行的,不管是内容还是风格,用像素值作为衡量依据都会导致泛化才干非常差,因而需要改用其它方法。

在深度学习中,咱们喜欢用卷积神经网络提取的特征图来作为衡量依据。特征图有许多良好的性质,比方对位置不灵敏、多标准特征。

VGG是比较常用的一种卷积神经网络,在本例中咱们运用卷积神经网络来提取图片特征。下图是VGG19的网络结构:

生成梵高风格的图片

在VGG19中,有5组卷积神经网络。左边的卷积层提取纹路、边缘等初级特征,越往右提取的特征越笼统。

2.2.2 内容丢失

风格搬迁任务不同于其它网络,在以往任务中,咱们做的是预备数据,更新网络权重。而在本例中,咱们要做的是运用卷积提取图片特征,然后通过内容类似度、风格类似度的信息来修改输入的图片。所以咱们需要有一个函数用来评估内容类似度,这儿咱们称为内容丢失。

内容是图片全体的信息,比较笼统,需要在较右的卷积核才干提取,这儿挑选conv4_1的输出作为图片内容。这一层右N(512)个特征图,因而把内容丢失界说为:

LC=12ND∑ij(fijo−fijc)2L_C=\frac{1}{2ND}\sum_{ij}({f_{ij}^{o} – f_{ij}^{c}})^2

其中f为向量化后的特征图,D为特征图的大小,这儿便是核算两个图片的特征图的均方差错。

2.2.3 风格丢失

风格丢失则更杂乱一点。风格不只反映了图片的纹路,也反映了图画的全体风格。因而在考虑风格丢失的时分,需要考虑各层特征图的内容。

通常用同一层不同特征图的Gram矩阵来表示。Gram矩阵界说为:N个特征图,将特征图向量化后记为f,则第i和j个向量的内积便是Gram矩阵的第(i, j)个元素:

Gramij=∑kfik−fjkGram_{ij}=\sum_k{f_{ik}-f_{jk}}

此时某一层的风格丢失能够界说为:

LSl=14N2D2(Gijol−Gijsl)2L_S^l=\frac{1}{4N^2D^2}(G_{ij}^{ol}-G_{ij}^{sl})^2

由于风格丢失需要考虑多层,因而全体的风格丢失能够界说为:

LS=∑lLwlLSlL_S=\sum_l^L{w_lL_S^l}

其值为各层特征图的加权和。终究咱们能够用Ls和Lc的加权和作为终究的丢失:

L=LS+LCL=\alpha L_S+\betaL_C

三、代码完成

下面运用PyTorch完成搬迁学习。

3.1 加载图片

首先模型需要三个输入,分别是内容图片、风格图片、成果图片。成果图片是用来更新的图片,咱们能够用随机噪声来生成,也能够用内容图片来替代,加载图片的代码如下:

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设置为核算梯度,由于后续咱们需要依据梯度更新该图片。

3.2 提取特征

提取特征咱们运用的是预练习的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便是咱们需要特征图。

3.3 Gram矩阵

对于风格丢失,咱们还需要核算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矩阵的内容。

3.4 更新图片

终究咱们要做的便是界说练习的过程,以往咱们练习是更新网络权重,今日咱们练习是更新成果图片,也便是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次后的成果图画。从成果来看,咱们确实完成了风格搬迁。

代码参阅自:blog.csdn.net/Brikie/arti…