前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用Pytorch实现风格迁移(Neural-Transfer)

使用Pytorch实现风格迁移(Neural-Transfer)

作者头像
用户1621951
发布2021-09-02 14:49:48
4.5K0
发布2021-09-02 14:49:48
举报
文章被收录于专栏:数据魔术师数据魔术师

# 前言

本文主要向大家分享一个小编刚刚学习的神经网络应用的实例:风格迁移(Neural-Transfer)。这是一个由 Leon A. Gatys,Alexander S. Ecker和Matthias Bethge提出的算法。通过这个算法,我们可以用一种新的风格对指定图片进行重构,更通俗一点即:风格图片+内容图片=输出图片,即:

如上图所示,神经风格迁移可以将内容图像的内容、风格图像的风格混合在一起,使得输出的图片看起来像内容图像,但采用了风格图像的风格。这就生成了一个带有《星空》风格的风景图,是不是很有趣呢?下面让我们一起来看看它是怎么实现的吧!

01

基本原理

原理很简单:我们定义了两个距离,一个为内容D_c和一个用于样式D_s。D_c 测量两个图像之间的内容有多大不同,而 D_s衡量两个图像之间风格的差异。然后,我们生成第三张图片,并利用D_c和D_s来优化这张图片,使其与内容图片的内容差别和风格图片的风格差别最小化。当然,我们的第一步是利用卷积神经网络提取出图片的特征,如图所示:

02

读入图片

现在我们导入content和style的原图,我们需要使用PIL中的Image来读取内存中的图片(PS:opencv也可以,但PIL的效果更好一点),再用torchvision中的transforms将读入图片转化为tensor以便之后的操作。另外,我们还需要定义一个用于输出图像的函数,以便输出最终所得到的图片,该函数会将tensor转化为图像。

代码如下:

代码语言:javascript
复制
# load_img模块
import PIL.Image as Image
import torch
import torchvision.transforms as transforms

img_size = 512 if torch.cuda.is_available() else 128#根据设备选择改变后项数大小
def load_img(img_path):#图像读入
    img = Image.open(img_path).convert('RGB')#将图像读入并转换成RGB形式
    img = img.resize(img_size, img_size)#调整读入图像像素大小
    img = transforms.ToTensor()(img)#将图像转化为tensor
    img = img.unsqueeze(0)#在0维上增加一个维度
    return img

def show_img(img):#图像输出
    img = img.squeeze(0)#将多余的0维通道删去
    img = transforms.ToPILImage()(img)#将tensor转化为图像
    img.show()

03

损失函数

下一步是定义我们的损失函数,为了实现神经风格迁移,我们需要定义一个关于生成图像(Generated image)G的损失函数,用于评价生成图像的好坏。通过最小化损失函数的方式,来生成所要的图像。损失函数需要分成两部分,一个是内容损失函数,它是关于生成图像G与内容图像C的函数,用于衡量生成图像与内容图像在内容上有多相似;一个是风格损失函数,关于生成图像G与风格图像S的函数,用于衡量生成图像与风格图像在风格上的相似度。最后需要用两个超参数α和β来确定两个函数之间的权重,我们的总损失是它们两个的加权和,即

那么我们的关键就在于先单独求出内容损失和风格损失,计算它们的损失也很简单,首先我们看一下内容损失,我们使用最简单的均方误差:

上式是内容损失函数的定义。其中l代表第l层的特征表示,p是原始内容图片特征图,x是生成图片特征图。公式的含义就是对于每一层,原始图片的特征图(feature map)和生成图片的特征图的一一对应做差值平方和。

然后我们需要对损失梯度下降求导来优化参数

对于风格损失我们还需要引入Gram矩阵来帮助我们表示图像的风格特征,我们读入图像卷积层的输出形状为C × H × W ,C是卷积核的通道数,每个卷积核学习图像不同特征,每个卷积核输出H × W 代表这张图像的一个feature map,读入RGB图像的三色通道相当于三个feature map,我们用Gram矩阵来计算feature map间的相似性,得到图像的风格特征。

关于Gram矩阵

由于计算风格损失需要Gram矩阵,所以我们先来了解一下它吧。

Gram矩阵的定义:

Gram矩阵计算公式:

F表示生成图像的feature map。上面式子的含义:第l层的Gram矩阵第i行,第j列的数值等于把生成图像在第l层的第i个feature map与第j个feature map分别拉成一维后相乘求和,即Gram矩阵中的每个值都是每个通道 i 的feature map与每个通道 j 的feature map的内积,由于内积可以判断两个向量之间的夹角和方向关系(如:若a·b>0,则二者方向相同,夹角在0°到90°之间),所以Gram矩阵中的值可以反映出两个feature map之间的某种关系。

Gram矩阵可以看是feature之间的偏心协方差矩阵,feature map中的每个数字都来自于一个特定滤波器在特定位置的卷积,因此每个数字代表一个特征的强度,而Gram计算的实际上是两两特征之间的相关性。Gram的对角线元素提供了不同特征图各自的信息,而其余元素提供了不同特征图之间的相关信息,因此,Gram有助于把握整个图像的大体风格。有了表示风格的Gram矩阵,要度量两个图像风格的差异,只需比较他们Gram矩阵的差异即可。

然后我们就可以利用Gram矩阵提取的风格特征计算损失了,这里我们仍然使用均方误差,并进行归一化操作。

上面是第l层的风格损失函数,N是指生成图feature map数量,M是图片宽乘高,G是生成图像的Gram矩阵,A是风格图像的Gram矩阵。

然后是梯度下降:

最终的风格损失函数为每一层的风格损失加权求和可得。式中,a是指风格图像,x是指生成图像,w是指权重:

下面我们看看如何用代码实现它:

代码语言:javascript
复制
# loss板块
import torch.nn as nn
import torch

class Content_Loss(nn.Module):#内容损失
    def __init__(self, target, weight):
        super(Content_Loss, self).__init__()#继承父类的初始化
        self.weight = weight
        self.target = target.detach() * self.weight
        # 必须要用detach来分离出target,这时候target不再是一个Variable,这是为了动态计算梯度,否则forward会出错,不能向前传播
        self.criterion = nn.MSELoss()#利用均方误差计算损失
        
    def forward(self, input):#向前计算损失
        self.loss = self.criterion(input * self.weight, self.target)
        out = input.clone()
        return out
        
    def backward(self, retain_graph=True):#反向求导
        self.loss.backward(retain_graph=retain_graph)
        return self.loss
        
        
class Gram(nn.Module):#定义Gram矩阵
    def __init__(self):
        super(Gram, self).__init__()
        
    def forward(self, input):#向前计算Gram矩阵
        a, b, c, d = input.size()#a为批量大小,b为feature map的数量,c*d为feature map的大小
        feature = input.view(a * b, c * d)
        gram = torch.mm(feature, feature.t())
        gram /= (a * b * c * d)
        return gram
        
        
class Style_Loss(nn.Module):#风格损失
    def __init__(self, target, weight):
        super(Style_Loss, self).__init__()
        self.weight = weight
        self.target = target.detach() * self.weight
        self.gram = Gram()
        self.criterion = nn.MSELoss()
        
    def forward(self, input):
        G = self.gram(input) * self.weight
        self.loss = self.criterion(G, self.target)
        out = input.clone()
        return out
        
    def backward(self, retain_graph=True):
        self.loss.backward(retain_graph=retain_graph)
        return self.loss

04

模型构建

接下来就该构建我们的模型啦!虽然我们用的是已经预训练好的vgg框架,但我们还需要对它做一些“改造”:把我们之前构造好的损失函数加进去。毕竟“白嫖”也是有限度的嘛!话不多说,上代码!

代码语言:javascript
复制
# build_model模块
import torch.nn as nn
import torch
import torchvision.models as models
import loss  # 指的是上文中已经写好的loss模块

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")#选择运行设备,如果你的电脑有gpu就在gpu上运行,否则在cpu上运行
vgg = models.vgg19(pretrained=True).features.to(device)#这里我们使用预训练好的vgg19模型
'''所需的深度层来计算风格/内容损失:'''
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

def get_style_model_and_loss(style_img,
                             content_img,
                             cnn=vgg,
                             style_weight=1000,
                             content_weight=1,
                             content_layers=content_layers_default,
                             style_layers=style_layers_default):
    content_loss_list = [] #内容损失
    style_loss_list = [] #风格损失
    model = nn.Sequential() #创建一个model,按顺序放入layer
    model = model.to(device)
    gram = loss.Gram().to(device)
    
    '''把vgg19中的layer、content_loss以及style_loss按顺序加入到model中:'''
    i = 1
    for layer in cnn:
        if isinstance(layer, nn.Conv2d):
            name = 'conv_' + str(i)
            model.add_module(name, layer)
            if name in content_layers_default:
                target = model(content_img)
                content_loss = loss.Content_Loss(target, content_weight)
                model.add_module('content_loss_' + str(i), content_loss)
                content_loss_list.append(content_loss)
            if name in style_layers_default:
                target = model(style_img)
                target = gram(target)
                style_loss = loss.Style_Loss(target, style_weight)
                model.add_module('style_loss_' + str(i), style_loss)
                style_loss_list.append(style_loss)
            i += 1
        if isinstance(layer, nn.MaxPool2d):
            name = 'pool_' + str(i)
            model.add_module(name, layer)
        if isinstance(layer, nn.ReLU):
            name = 'relu' + str(i)
            model.add_module(name, layer)
            
    return model, style_loss_list, content_loss_list

05

执行

一切工作准备就绪,我们就可以开始定义我们的run_code模块了,经历过这么多操作终于走到了最后一步,是不是有点小激动呢?我们的执行模块也很简单,先定义一个LBFGS优化器,然后就可以开始我们一次一次的训练了。这里我们定义每训练50次输出一次我们的损失来评估学习效果。

代码语言:javascript
复制
# run_code模块
import torch.nn as nn
import torch.optim as optim
from build_model import get_style_model_and_loss

def get_input_param_optimier(input_img):
    """input_img is a Variable"""
    input_param = nn.Parameter(input_img.data)#获取参数
    optimizer = optim.LBFGS([input_param])#用LBFGS优化参数
    return input_param, optimizer
    
def run_style_transfer(content_img, style_img, input_img, num_epoches=300):
    print('Building the style transfer model..')
    model, style_loss_list, content_loss_list = get_style_model_and_loss(
        style_img, content_img)
    input_param, optimizer = get_input_param_optimier(input_img)
    print('Opimizing...')
    epoch = [0]
    while epoch[0] < num_epoches:#每隔50次输出一次loss
        def closure():
            input_param.data.clamp_(0, 1)#修正输入图像的值
            model(input_param)
            style_score = 0
            content_score = 0
            optimizer.zero_grad()
            for sl in style_loss_list:
                style_score += sl.backward()
            for cl in content_loss_list:
                content_score += cl.backward()
            epoch[0] += 1
            if epoch[0] % 50 == 0:
                print('run {}'.format(epoch))
                print('Style Loss: {:.4f} Content Loss: {:.4f}'.format(
                    style_score.item(), content_score.item()))
                print()
            return style_score + content_score
        optimizer.step(closure)
        input_param.data.clamp_(0, 1)#再次修正
    return input_param.data

06

运行

最最最最最激动人心的时刻就要到了!敲了这么多代码,现在我们就可以开始验证我们的成果了!现在我们只需要挑选一张中意的图片,再为它寻找一个你想要的风格图片,只需短短几分钟(甚至几十秒),你,就创造出了属于自己的艺术画作(其实一般)!!!

代码语言:javascript
复制
# start模块
from torch.autograd import Variable
from torchvision import transforms
import torch
from run_code import run_style_transfer
from load_img import load_img, show_img

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
style_img = load_img('./picture/style1.png')#风格图片地址
style_img = Variable(style_img).to(device)
content_img = load_img('./picture/content5.png')#内容图片地址
content_img = Variable(content_img).to(device)
input_img = content_img.clone()

out = run_style_transfer(content_img, style_img, input_img, num_epoches=200)#进行200次训练
save_pic = transforms.ToPILImage()(out.cpu().squeeze(0))
save_pic.save('./picture/result4.png')#选择你要保存的地址
save_pic.show()

下面我们做几组演示:

输入:

输出:

输入:

输出:

最后我们试一试人像:

输出:

下面是其中一组loss的输出:

代码语言:javascript
复制
Opimizing...
run [50]
Style Loss: 0.7299 Content Loss: 3.7480
run [100]
Style Loss: 0.3702 Content Loss: 3.3560
run [150]
Style Loss: 0.2747 Content Loss: 3.1843
run [200]
Style Loss: 0.2058 Content Loss: 3.0981

我们可以看出内容损失还是挺大的,损失的大小不仅跟我们的模型有关,我们选择的图片也会对损失有很大的影响,不得不说有的图片特征确实难以捕获,小编曾经还有过训练1000次仍然有40多损失的惨痛经历,这种情况就不是单纯增加训练次数就能解决的了,我们需要更强大的模型去捕获信息,大家也可以自己试试改进一下模型,也许会有意外的收获呦!

reference

1] Gatys L A, Ecker A S, Bethge M. A neural algorithm of artistic style[J]. arXiv preprint arXiv:1508.06576, 2015.

2] https://pytorch.org/tutorials/advanced/neural_style_tutorial.html#importing-packages-and-selecting-a-device

1

END

1

文案&排版:王心怡(华中科技大学管理学院本科一年级)

潘云飞(华中科技大学管理学院本科一年级)

指导老师:秦虎老师(华中科技大学管理学院)

审稿:张宇(华中科技大学管理学院本科二年级)

如对文中内容有疑问,欢迎交流。PS:部分资料来自网络。

如有需求,可以联系:

秦虎老师(华中科技大学管理学院:tigerqin1980@qq.com)

王心怡(华中科技大学管理学院本科一年级:3348619379@qq.com)

潘云飞(华中科技大学管理学院本科一年级:1401914932@qq.com)

张宇(华中科技大学管理学院本科二年级:8611452@qq.com)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据魔术师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档