人人都是毕加索



基于 Pytorch 和 VGG19 模型实现图片风格迁移。

相关 Pytorch 官方教程

相关 Github 源码


版权声明:本文为 frendy 原创文章,可以随意转载,但请务必在明确位置注明出处。


welcome.png

实践出真知,其实该 Demo 基本是参考 Pytorch 官方教程实现,frendy 在这里只是按照自己的学习路径实践记录了一遍,并提取部分重点跟大家做分享,还是墙裂建议大家去看看官方教程的,文章前面已附上链接地址。好吧,稍微啰嗦一下,笔者的传统学习路径是:浏览概念不求甚解;跑 Demo;回头啃概念或论文(捂脸,目前啃得都不好哎)。

文中涉及到的相关概念如有偏颇或错误,请各位大神不吝赐教,批评指出,这里先谢过。


为什么选 Pytorch



Pytorch 来自 Facebook,我们先来看看它的宣传语吧:

Matlab is so 2012.
Caffe is so 2013.
Theano is so 2014.
Torch is so 2015.
TensorFlow is so 2016.
‏It's 2017 now.

实际上呢?frendy 还不敢妄言,学习和实践也不是很透彻。不过感觉 Pytorch 比 TensorFlow 要容易上手一点,而且是动态图,比起 TensorFlow 的静态图要灵活一点。道听途说,TensorFlow 依然拥有最大的社区,而 Pytorch 则最近增长比较快。


环境配置



可以到下面的官网获取相应配置的安装命令(官方目前只支持 Linux 和 OSX):

http://pytorch.org/

frendy 这里是 Win10,侥幸找到一个别人编译好的版本,更侥幸的是他的电脑配置跟我的基本一样,可以直接使用(百度云下载地址):

conda install pytorch-0.1.12-py36_0.1.12cu80.tar.bz2
pip install torchvision

其中 conda 的版本为 Anaconda3 (with Python 3.6)。


原理分析



提取图片 A 的内容,提取图片 B 的风格,合成一张新图:

Image_A (content) + Image_B (style) = Image_C (result)
  • 使得 A 和 C 的内容差异尽可能小;
  • 使得 B 和 C 的风格差异尽可能小。

好了,目标明确,但是该怎么定义或者说是衡量内容差异和风格差异呢?frendy 觉得这就是风格迁移的核心问题所在了。

内容差异

其实比较容易可以想到的就是比较两张图片每个像素点。怎么比较呢?也就是求一下差。在 torch 里,我们可以直接调用 nn.MSELoss 来计算输入和目标之间的均方误差。

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, inputs):
        self.loss = self.criterion(inputs*self.weight, self.target)
        self.outputs = inputs
        return self.outputs

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

其中 forward 是根据图往前计算,backward 是反向传播优化权重。Pytorch 封装了细节,支持自动求导。

风格差异

风格是一个挺抽象的概念。什么是风格?有个性。恩,自己的特点越突出,别人的越不突出最好。巨人们的论文告诉我们可以用 Gram 矩阵来衡量风格。

004.png

从上图可以看出,内积之后,对角线元素(即不同特征图各自的信息)就放大了,同时其余元素则提供了不同特征图之间的相关信息。于是,在一个Gram矩阵中,既能体现出有哪些特征,又能体现出不同特征间的紧密程度。那么,接下来我们就可以定量分析风格啦:

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)
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, inputs):
        self.output = inputs.clone()
        self.G = self.gram(inputs)
        self.G.mul_(self.weight)
        self.loss = self.criterion(self.G, self.target)
        return self.output

    def backward(self, retain_variables=True):
        self.loss.backward(retain_variables=retain_variables)
        return self.loss
模型搭建

这里使用 19 层的 vgg 作为提取特征的卷积网络,并定义了哪几层为需要的特征。

def get_model_and_losses(style_img, content_img,
                               style_weight=1000, content_weight=1):

    content_layers = ['conv_4']
    style_layers = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

    use_cuda = torch.cuda.is_available()

    cnn = models.vgg19(pretrained=True).features
    if use_cuda:
        cnn = cnn.cuda()
    cnn = copy.deepcopy(cnn)

    content_losses = []
    style_losses = []

    model = nn.Sequential()
    gram = GramMatrix()

    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
模型训练

这里使用 L-BFGS(Limited-memory Broyden–Fletcher–Goldfarb–Shanno) 算法来跑梯度下降,并通过 backward 反向优化权重,不断缩小 A 和 C 的内容差异、缩小 B 和 C 的风格差异。

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

def train():
    ...
    model, style_losses, content_losses = get_model_and_losses(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 + style_score

        optimizer.step(closure)
    input_param.data.clamp_(0, 1)
    output = input_param.data

效果图


  • 莫奈(900 次迭代的结果)

001.png

  • 毕加索(900 次迭代的结果)

002.png

  • 毕加索(300 次迭代的结果)

003.png


这 300 次的结果是不是感觉比 900 次的结果要好看?!


后话:谷歌最近又开源了一个库 Tensor2Tensor,号称 One Model To Learn Them All;苹果的 Core ML 几行代码就可以导入并使用训练好的模型...进击的巨人们啊!


qrcode_card.png


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容