PyTorch进行神经风格转换/迁移以及原理(Neural-Transfer:图像风格迁移)

Song • 2447 次浏览 • 0 个回复 • 2018年03月01日

一、神经是什么?

神经风格或神经转移是一种算法,将输入的内容图像(例如tortle),风格图像(例如waves)作为参考,并返回绘制后的风格图片: 神经风格

二、它是如何工作的?

原理很简单:我们定义了两个差距,一个是内容(Dc),另一个是风格(Ds)。Dc测量两幅图像之间内容的差异程度,而Ds测量两幅图像之间风格的差异程度。然后,我们拍摄第三张图像,输入(例如有噪音的图像),然后对其进行变形,以便将内容与内容图像的差距以及与风格图像的风格差距最小化。

三、PyTorch实现风格迁移

pytorch风格偏移基于很多数学公式,如果你不理解;我们可以来实现风格迁移用于了解他,如果你正在学习pytorch,你可以先阅读一下pytorch中文文档

1、使用到的工具包

我们将会使用以下软件包:

  • torchtorch.nnnumpy(PyTorch神经网络包)
  • torch.autograd.Variable (动态计算变量的梯度)
  • torch.optim (有效梯度下降)
  • PILPIL.Imagematplotlib.pyplot(加载和显示图像)
  • torchvision.transforms (处理PIL图像并转换成Torch张量)
  • torchvision.models (训练或加载预先训练的模型)
from __future__ import print_function

import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.optim as optim

from PIL import Image
import matplotlib.pyplot as plt

import torchvision.transforms as transforms
import torchvision.models as models

import copy

2、CUDA

如果您的计算机上有GPU,则最好在其上运行算法,尤其是如果您想尝试更大的网络(如VGG)。我们可以使用torch.cuda.is_available()判断电脑是否支持GPU运算。然后我们就可以使用.cuda()CPU中的模块CPU移动到GPU。当我们想把这个模块移回到CPU(例如使用numpy)时,我们可以使用.cpu()法。最后:.type(dtype)torch.FloatTensor转换到torch.cuda.FloatTensor提供的GPU进程中。具体可以参考pytorch通过torch.cuda使用GPU加速运算且比较GPU与CPU运算效果以及应用场景

use_cuda = torch.cuda.is_available()
dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor

3、加载图像

为了方便实现,我们导入相同维度的样式和内容图像。然后,我们将它们缩放到所需的输出图像尺寸(在本例中为128512,取决于gpu的可用性)并将它们转换为Torch张量,准备提供给神经网络:

注意 通过以下链接下载到运行教程所需的图像: picasso.jpgdancing.jpg。下载这两张图片并将其添加到文件夹下的images目录

# 想要输出图像大小,没有GUPU建议使用128
imsize = 512 if use_cuda else 128

loader = transforms.Compose([
    transforms.Scale(imsize),  # 导入图片
    transforms.ToTensor()])  # 转变为torch张量

def image_loader(image_name):
    image = Image.open(image_name)
    image = Variable(loader(image))
    # 批量虚拟需要适应网络的输入尺寸的维度
    image = image.unsqueeze(0)
    return image

style_img = image_loader("images/picasso.jpg").type(dtype)
content_img = image_loader("images/dancing.jpg").type(dtype)

assert style_img.size() == content_img.size(), \
    "we need to import style and content images of the same size"

导入的PIL图像的值介于0255之间。转换为值在01之间的Torch张量。注意:Torch库的神经网络用0-1张量图像进行训练。如果你想要提供给网络0-255张量的图片,激活的特征映射将没有反应。Caffe库以前训练过的网络并相同:他们接受0-255张量图像训练。

4、显示图像

为了使用plt.imshow来显示图像。所以我们提前重新把他们转换成PIL图像:

unloader = transforms.ToPILImage()  # 重新转换成PIL图像

plt.ion()

def imshow(tensor, title=None):
    image = tensor.clone().cpu()  # 我们克隆张量以不对其进行修改
    image = image.view(3, imsize, imsize)  # 删除批量处理维度
    image = unloader(image)
    plt.imshow(image)
    if title is not None:
        plt.title(title)
    plt.pause(0.001) # pause a bit so that plots are updated

plt.figure()
imshow(style_img.data, title='Style Image')

plt.figure()
imshow(content_img.data, title='Content Image')

pytorch风格迁移-加载图片

5、Content loss

class ContentLoss(nn.Module):

    def __init__(self, target, weight):
        super(ContentLoss, self).__init__()
        # we 'detach' the target content from the tree used
        self.target = target.detach() * weight
        # to dynamically compute the gradient: this is a stated value,
        # not a variable. Otherwise the forward method of the criterion
        # will throw an error.
        self.weight = weight
        self.criterion = nn.MSELoss()

    def forward(self, input):
        self.loss = self.criterion(input * self.weight, self.target)
        self.output = input
        return self.output

    def backward(self, retain_graph=True):
        self.loss.backward(retain_graph=retain_graph)
        return self.loss

注意
重要细节:这个模块虽然被命名,但Content Loss并不是真正的PyTorch Loss功能。如果你想将你的Content Loss定义为PyTorch Loss,你必须手动在backward创建一个PyTorch autograd函数来重新计算/执行梯度。

6、风格损失

class GramMatrix(nn.Module):

    def forward(self, input):
        a, b, c, d = input.size()  # a=batch size(=1)
        # b=number of feature maps
        # (c,d)=dimensions of a f. map (N=c*d)

        features = input.view(a * b, c * d)  # resise F_XL into \hat F_XL

        G = torch.mm(features, features.t())  # compute the gram product

        # we 'normalize' the values of the gram matrix
        # by dividing by the number of element in each feature maps.
        return G.div(a * b * c * d)

N特征映射维数越长,gram矩阵的值越大。因此,如果我们不用进行N归一化,则在梯度下降期间,在第一层(在合并层之前)计算的损失将具有更重要的意义。我们不希望这样,因为最有趣的风格特征在最深层!

然后,风格损失模块的实现方式与内容丢失模块完全相同,但我们必须添加一个gramMatrix参数:

class StyleLoss(nn.Module):

    def __init__(self, target, weight):
        super(StyleLoss, self).__init__()
        self.target = target.detach() * weight
        self.weight = weight
        self.gram = GramMatrix()
        self.criterion = nn.MSELoss()

    def forward(self, input):
        self.output = input.clone()
        self.G = self.gram(input)
        self.G.mul_(self.weight)
        self.loss = self.criterion(self.G, self.target)
        return self.output

    def backward(self, retain_graph=True):
        self.loss.backward(retain_graph=retain_graph)
        return self.loss

7、加载神经网络

现在,我们必须导入一个预先训练好的神经网络。就像在这篇论文中,我们将使用一个19层的预训练VGG网络(VGG19)。

PyTorchVGG实现是一个模块,分为两个子Sequential模块:( features包含卷积和合并层)和classifier(包含完全连接的层)。我们只对features感兴趣:

cnn = models.vgg19(pretrained=True).features

# move it to the GPU if possible:
if use_cuda:
    cnn = cnn.cuda()

Sequential模块包含一个有序的子模块列表。例如,vgg19.features包含以正确的深度顺序对齐的序列(Conv2d,ReLU,Maxpool2d,Conv2d,ReLU ...)。正如我们在Content loss部分所说的,我们在我们的网络中以理想的深度添加我们的样式和内容损失模块作为附加“透明”层。为此,我们构建了一个新Sequential模块,我们将vgg19按照正确的顺序添加模块和我们的损失模块:

# desired depth layers to compute style/content losses :
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

def get_style_model_and_losses(cnn, style_img, content_img,
                               style_weight=1000, content_weight=1,
                               content_layers=content_layers_default,
                               style_layers=style_layers_default):
    cnn = copy.deepcopy(cnn)

    # just in order to have an iterable access to or list of content/syle
    # losses
    content_losses = []
    style_losses = []

    model = nn.Sequential()  # the new Sequential module network
    gram = GramMatrix()  # we need a gram module in order to compute style targets

    # move these modules to the GPU if possible:
    if use_cuda:
        model = model.cuda()
        gram = gram.cuda()

    i = 1
    for layer in list(cnn):
        if isinstance(layer, nn.Conv2d):
            name = "conv_" + str(i)
            model.add_module(name, layer)

            if name in content_layers:
                # add content loss:
                target = model(content_img).clone()
                content_loss = ContentLoss(target, content_weight)
                model.add_module("content_loss_" + str(i), content_loss)
                content_losses.append(content_loss)

            if name in style_layers:
                # add style loss:
                target_feature = model(style_img).clone()
                target_feature_gram = gram(target_feature)
                style_loss = StyleLoss(target_feature_gram, style_weight)
                model.add_module("style_loss_" + str(i), style_loss)
                style_losses.append(style_loss)

        if isinstance(layer, nn.ReLU):
            name = "relu_" + str(i)
            model.add_module(name, layer)

            if name in content_layers:
                # add content loss:
                target = model(content_img).clone()
                content_loss = ContentLoss(target, content_weight)
                model.add_module("content_loss_" + str(i), content_loss)
                content_losses.append(content_loss)

            if name in style_layers:
                # add style loss:
                target_feature = model(style_img).clone()
                target_feature_gram = gram(target_feature)
                style_loss = StyleLoss(target_feature_gram, style_weight)
                model.add_module("style_loss_" + str(i), style_loss)
                style_losses.append(style_loss)

            i += 1

        if isinstance(layer, nn.MaxPool2d):
            name = "pool_" + str(i)
            model.add_module(name, layer)  # ***

    return model, style_losses, content_losses

注意

在本文中,他们建议将最大池层更改为平均池。与AlexNet相比,这是一个小型网络,与本文中使用的VGG19相比,我们不会看到结果的质量差异。但是,如果你想替换,则可以使用这些行代替:

#avgpool = nn.AvgPool2d(kernel_size = layer.kernel_size,
#stride = layer.stride,padding = layer.padding)
#model.add_module(name,avgpool)

8、输入图像

为了再次简化代码,我们拍摄图像尺寸与内容和样式相同的图像。此图像可能是白色噪点,也可以复制内容图像。

input_img = content_img.clone()
# if you want to use a white noise instead uncomment the below line:
# input_img = Variable(torch.randn(content_img.data.size())).type(dtype)

# add the original input image to the figure:
plt.figure()
imshow(input_img.data, title='Input Image')

pytorch风格偏移-图片输入

9、梯度下降

正如算法作者Leon Gatys这里所建议的那样 ,我们将使用L-BFGS算法来运行我们的梯度下降。与训练网络不同,我们希望对输入图像进行训练,以尽量减少content/style的损失。我们只想创建一个PyTorch L-BFGS优化器,将我们的图像作为变量进行优化。但是optim.LBFGS首先需要一个梯度下降的PyTorch Variable列表 。我们的输入图像是一个Variable但不是需要计算梯度的tree。为了显示这个变量需要一个梯度,可能会Parameter从输入图像构造一个对象。然后,我们只是将包含这个的列表提供给Parameter优化器的构造函数:

def get_input_param_optimizer(input_img):
    # this line to show that input is a parameter that requires a gradient
    input_param = nn.Parameter(input_img.data)
    optimizer = optim.LBFGS([input_param])
    return input_param, optimizer

最后一步:梯度循环下降。在每一步中,我们必须向网络提供更新后的输入以计算新的损失,我们必须运行每个损失的backward方法来动态计算它们的梯度并执行梯度下降的操作。优化器需要一个"闭包"作为参数:一个重新评估模型并返回损失的函数。

然而,这有一个小问题。优化后的图像可能取值在±∞之间,而不是保持在0和1之间。换句话说,图像可能会被很好地优化并且返回无价值的值。事实上,我们必须在约束条件下进行优化,以保持对我们输入图像的正确识别。有一个简单的解决方案:在每个步骤中,纠正图像以将其值保持在0-1之间。

def run_style_transfer(cnn, content_img, style_img, input_img, num_steps=300,
                       style_weight=1000, content_weight=1):
    """Run the style transfer."""
    print('Building the style transfer model..')
    model, style_losses, content_losses = get_style_model_and_losses(cnn,
        style_img, content_img, style_weight, content_weight)
    input_param, optimizer = get_input_param_optimizer(input_img)

    print('Optimizing..')
    run = [0]
    while run[0] <= num_steps:

        def closure():
            # correct the values of updated input image
            input_param.data.clamp_(0, 1)

            optimizer.zero_grad()
            model(input_param)
            style_score = 0
            content_score = 0

            for sl in style_losses:
                style_score += sl.backward()
            for cl in content_losses:
                content_score += cl.backward()

            run[0] += 1
            if run[0] % 50 == 0:
                print("run {}:".format(run))
                print('Style Loss : {:4f} Content Loss: {:4f}'.format(
                    style_score.data[0], content_score.data[0]))
                print()

            return style_score + content_score

        optimizer.step(closure)

    # a last correction...
    input_param.data.clamp_(0, 1)

    return input_param.data

最后,运行算法

output = run_style_transfer(cnn, content_img, style_img, input_img)

plt.figure()
imshow(output, title='Output Image')

# sphinx_gallery_thumbnail_number = 4
plt.ioff()
plt.show()

pytorch风格迁移结果

Building the style transfer model..
Optimizing..
run [50]:
Style Loss : 0.147848 Content Loss: 0.470452

run [100]:
Style Loss : 0.043477 Content Loss: 0.343297

run [150]:
Style Loss : 0.035841 Content Loss: 0.315429

run [200]:
Style Loss : 0.031865 Content Loss: 0.306764

run [250]:
Style Loss : 0.030837 Content Loss: 0.302101

run [300]:
Style Loss : 0.030777 Content Loss: 0.299595

三、项目源码


原创文章,转载请注明 :PyTorch进行神经风格转换/迁移以及原理(Neural-Transfer:图像风格迁移) - pytorch中文网
原文出处: https://ptorch.com/news/133.html
问题交流群 :168117787
提交评论
要回复文章请先登录注册
用户评论
  • 没有评论
Pytorch是什么?关于Pytorch! 用PyTorch实现3D人体姿势估计(弱监督方法pytorch-pose-hg-3d)