Neural Style Transfer (画風変換)

“Image Style Transfer Using Convolutional Neural Networks.”の実装

はじめに

このチュートリアルでは,”Image Style Transfer Using Convolutional Neural Networks,” Gatys et al. 2016.[1]に基づいて,ニューラルネットワークによる画風変換(Style Transfer)を実装します(論文のプレプリント版は2015年に公開されています[2].).

このアルゴリズムを用いると,下の例のように,ある画像のスタイル(画風)を他の画像に転写させることができます.(図はGatys et al.[1]より.A:オリジナルのコンテンツ画像.B-F:生成された画像.それぞれの画像の左下には,生成に用いたスタイル画像が表示されています.)

手法

この節では,アルゴリズムの手法を簡単に紹介します.
下図はアルゴリズムの概略図です[1].

概要

アルゴリズムのキーとなるアイデアは,画像のコンテンツのスタイルからの分離です.画像のコンテンツ表現とスタイル表現を用いることで,画像間のコンテンツとスタイルの類似度(損失)を別々に求めることができます.したがって,この問題はコンテンツとスタイルの損失を最小化する最適化問題として解くことができます.Gatysらは,ホワイトノイズ画像に対して勾配降下法を適用し,入力のコンテンツ/スタイル画像のコンテンツ/スタイル表現にそれぞれ合致する画像を探索しました.
\vec{p} \vec{a} をそれぞれ入力のコンテンツ画像とスタイル画像とし, \vec{x} を生成される画像とすると,この最適化問題は以下の式で表現することができます.
\begin{equation*} \vec{x}^{*}=\mathop{\rm argmin}\limits_{\vec{x}}(\alpha\mathcal{L}_{content}(\vec{p},\vec{x})+\beta\mathcal{L}_{style}(\vec{a},\vec{x})). \end{equation*}

それでは,どのようにしてコンテンツとスタイルの表現を別々に抽出するのでしょうか.Gatysらは,畳込みニューラルネットワーク(CNN)から得られる特徴空間に着目しました.

Content representation

一般的に,物体認識用に学習されたCNNにおいては,ネットワークのより高いレイヤーが画像のより高次のコンテンツをとらえているといわれます.よって,Gatysらは,ネットワークの高いレイヤーの特徴出力をcontent representationと呼びました.
ここで,ネットワーク内のレイヤー l は, N_l 個のフィルタ(特徴マップ)を有し,それぞれの特徴マップのサイズ(幅 \times 高さ)は M_l であるとします.そして, \vec{p} \vec{x} をそれぞれオリジナルのコンテンツ画像,生成される画像し, P^l F^l をレイヤー l におけるそれぞれの特徴表現とすると,2つの特徴表現の間の二乗誤差を定義することができます.
\begin{equation*} \mathcal{L}_{content} (\vec{p},\vec{x},l)=\frac{1}{2} \sum_{i,j}(F^l_{ij}-P^l_{ij})^2. \end{equation*}

ここで, F^l_{ij}\in \mathcal{R}^{N_l\times M_l} はレイヤー l i 番目のフィルタ内の位置 j における活性度(特徴出力)を示します.

Style representation

一方,画像のスタイルは,各レイヤー内の個々のフィルタ出力の相関で表現されます.この特徴相関は,グラム行列 G^l\in \mathcal{R}^{N_l \times N_l} にて与えられます.ここで, G^l_{ij} はレイヤー l におけるベクトル化した特徴マップ i j の内積であり,次のように表されます.

\begin{equation*} G^l_{ij}=\sum_k F^l_{ik}F^l_{jk}. \end{equation*}

そして, \vec{a} \vec{x} をオリジナルのスタイル画像と生成される画像とし, A^l G^l をレイヤー l におけるそれぞれのスタイル表現とすると,スタイル損失に対するレイヤー l の寄与は,

\begin{equation*} E_l=\frac{1}{4N_l^2 M_l^2} \sum_{i,j}(G^l_{ij}-A^l_{ij})^2 \end{equation*}

さらにトータルのスタイル損失は,

\begin{equation*} \mathcal{L}_{style}(\vec{a},\vec{x})=\sum_{l=0}^L w_l E_l, \end{equation*}

となります.ここで, w_l は各レイヤーの重みを示します.

損失関数と最適化

最終的に,最小化すべき損失関数は,以下のようになります.

\begin{equation*} \mathcal{L}_{total}(\vec{p},\vec{a},\vec{x})=\alpha\mathcal{L}_{content}(\vec{p},\vec{x})+\beta\mathcal{L}_{style}(\vec{a},\vec{x}). \end{equation*}

最適化計算においては,損失関数を最小化するように画像 \vec{x} の画素値が更新されていきます.画素値に対する勾配 \frac{\partial \mathcal{L}_{total}}{\partial \vec{x}} は通常の誤差逆伝播法を使って計算できるので,L-BFGS,Adam,SGD等の勾配降下法を適用することができます.

オプションとして,total variation lossを上記損失関数に加えることもできます.この損失は,生成画像を滑らかにする正則化の役割を果たします.

Required Libaries

  • matplotlib 2.0.2
  • numpy 1.12.1
  • pillow 4.2.1
それでは,アルゴリズムをReNomで実装していきましょう.
まず始めに,下記のライブラリが必要です.
もしGPUが利用できる環境であるなら,使用することをおすすめします.
In [1]:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
from __future__ import division, print_function
import time
import os
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

import renom as rm
from renom.optimizer import Adam
from renom.cuda import set_cuda_active
# If you would like to use GPU, set True, otherwise you should be set to False.
set_cuda_active(True)

np.random.seed(10)

設定

アルゴリズムを実行するためにいくつかの設定を行います.

入出力に関する設定

In [2]:
# Input image path.
img_path_content = "./img/content/wolf.jpg"
img_path_style = "./img/style/starry_night.jpg"

# Output path and suffix.
img_path_results = "./img/results/results00/" # The generated images are saved here.
suffix_results = "result" # {suffix_results}_{iter}.jpg
try:
    os.mkdir(img_path_results)
except OSError:
    pass

# Size of images input to VGG model.
input_width = 400
input_size = (input_width, input_width)

損失関数に関する設定

In [3]:
# Model used to extract the feature maps. ("VGG16" or "VGG19")
modelname = "VGG16"
# modelname = "VGG19"

# Layers used to calculate the style loss and their weights.
layers_style = ['conv1_1', 'conv2_1', 'conv3_1', 'conv4_1', 'conv5_1']
w_style = [1./len(layers_style)] * len(layers_style) # equal

# Layers used to calculate the content loss and their weights.
layers_content = ['conv5_2',] # In Gatys's paper, 'conv4_2' is recommended.
w_content = rm.Node(np.array([2**i for i in range(len(layers_content))]))
w_content = w_content / rm.sum(w_content) # bigger weight in higher layer

# Ratio of content loss to style loss (alpha/beta)
lam = 1e-4

# Ratio of total variation loss to style loss
eta = 1e-6

最適化に関する設定

In [4]:
epoch = 2000
optimizer = Adam(lr=5.0)

# Initialization method of the generated image. (content image/random image)
init_mode = "content"
# init_mode = "rand"

VGGネットワークモデルの定義

上で述べたように,コンテンツ表現とスタイル表現を抽出するために,CNNモデルを用います.ここでは,Gatysらと同様に,VGGネットワークモデル[3]を使用しますが,NIN[4]やGoogLeNet[5]などの他の物体認識用のモデルも使用することができます.下の図は,VGG-16のネットワーク構造です.

もしこのチュートリアルを実行するのが初めてであれば,VGGモデルの学習済みの重みを用意する必要があります.重みはCaffeモデルとして, こちら で公開されています.Caffeモデルの重みをReNom用に変換する方法については,チュートリアル「Caffeの学習済みモデルの利用」にて紹介されています.

VGGモデルの畳込み層のみあればよいので,上部の全結合(Dense)層を除いたモデルを読み込みます.また,GatysらはMax PooingよりもAverage Pooingの方がやや良好な結果を示したと述べているため,Average Pooingを用います.

In [5]:
class VGG16_without_top(rm.Model):

    def __init__(self):
        super(VGG16_without_top, self).__init__()
        self.conv1_1 = rm.Conv2d(channel=64, padding=1, filter=3)
        self.conv1_2 = rm.Conv2d(channel=64, padding=1, filter=3)
        self.conv2_1 = rm.Conv2d(channel=128, padding=1, filter=3)
        self.conv2_2 = rm.Conv2d(channel=128, padding=1, filter=3)
        self.conv3_1 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv3_2 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv3_3 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv4_1 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv4_2 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv4_3 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_1 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_2 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_3 = rm.Conv2d(channel=512, padding=1, filter=3)
        self._pool = rm.AveragePool2d(filter=2, stride=2)
        # self._pool = rm.MaxPool2d(filter=2, stride=2)

    def forward(self, x):
        c1_1 = rm.relu(self.conv1_1(x))
        c1_2 = rm.relu(self.conv1_2(c1_1))
        p1 = self._pool(c1_2)

        c2_1 = rm.relu(self.conv2_1(p1))
        c2_2 = rm.relu(self.conv2_2(c2_1))
        p2 = self._pool(c2_2)

        c3_1 = rm.relu(self.conv3_1(p2))
        c3_2 = rm.relu(self.conv3_2(c3_1))
        c3_3 = rm.relu(self.conv3_3(c3_2))
        p3 = self._pool(c3_3)

        c4_1 = rm.relu(self.conv4_1(p3))
        c4_2 = rm.relu(self.conv4_2(c4_1))
        c4_3 = rm.relu(self.conv4_3(c4_2))
        p4 = self._pool(c4_3)

        c5_1 = rm.relu(self.conv5_1(p4))
        c5_2 = rm.relu(self.conv5_2(c5_1))
        c5_3 = rm.relu(self.conv5_3(c5_2))
        #p5 = self._pool(c5_3)

        k = ['conv1_1','conv1_2','conv2_1','conv2_2',
             'conv3_1','conv3_2','conv3_3',
             'conv4_1','conv4_2','conv4_3',
             'conv5_1','conv5_2','conv5_3',
             ]
        v = [c1_1, c1_2, c2_1, c2_2,
             c3_1, c3_2, c3_3,
             c4_1, c4_2, c4_3,
             c5_1, c5_2, c5_3,
             ]

        return dict(zip(k,v))
In [6]:
class VGG19_without_top(rm.Model):

    def __init__(self):
        super(VGG19_without_top, self).__init__()
        self.conv1_1 = rm.Conv2d(channel=64, padding=1, filter=3)
        self.conv1_2 = rm.Conv2d(channel=64, padding=1, filter=3)
        self.conv2_1 = rm.Conv2d(channel=128, padding=1, filter=3)
        self.conv2_2 = rm.Conv2d(channel=128, padding=1, filter=3)
        self.conv3_1 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv3_2 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv3_3 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv3_4 = rm.Conv2d(channel=256, padding=1, filter=3)
        self.conv4_1 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv4_2 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv4_3 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv4_4 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_1 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_2 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_3 = rm.Conv2d(channel=512, padding=1, filter=3)
        self.conv5_4 = rm.Conv2d(channel=512, padding=1, filter=3)
        self._pool = rm.AveragePool2d(filter=2, stride=2)
        # self._pool = rm.MaxPool2d(filter=2, stride=2)

    def forward(self, x):
        c1_1 = rm.relu(self.conv1_1(x))
        c1_2 = rm.relu(self.conv1_2(c1_1))
        p1 = self._pool(c1_2)

        c2_1 = rm.relu(self.conv2_1(p1))
        c2_2 = rm.relu(self.conv2_2(c2_1))
        p2 = self._pool(c2_2)

        c3_1 = rm.relu(self.conv3_1(p2))
        c3_2 = rm.relu(self.conv3_2(c3_1))
        c3_3 = rm.relu(self.conv3_3(c3_2))
        c3_4 = rm.relu(self.conv3_4(c3_3))
        p3 = self._pool(c3_4)

        c4_1 = rm.relu(self.conv4_1(p3))
        c4_2 = rm.relu(self.conv4_2(c4_1))
        c4_3 = rm.relu(self.conv4_3(c4_2))
        c4_4 = rm.relu(self.conv4_4(c4_3))
        p4 = self._pool(c4_4)

        c5_1 = rm.relu(self.conv5_1(p4))
        c5_2 = rm.relu(self.conv5_2(c5_1))
        c5_3 = rm.relu(self.conv5_3(c5_2))
        c5_4 = rm.relu(self.conv5_4(c5_3))
        #p5 = self._pool(c5_4)

        k = ['conv1_1','conv1_2','conv2_1','conv2_2',
             'conv3_1','conv3_2','conv3_3','conv3_4',
             'conv4_1','conv4_2','conv4_3','conv4_4',
             'conv5_1','conv5_2','conv5_3','conv5_4',
             ]
        v = [c1_1, c1_2, c2_1, c2_2,
             c3_1, c3_2, c3_3, c3_4,
             c4_1, c4_2, c4_3, c4_4,
             c5_1, c5_2, c5_3, c5_4,
             ]

        return dict(zip(k,v))
In [7]:
# Instantiate VGG model.
assert modelname in ["VGG16", "VGG19"], "modelname must be 'VGG16' or 'VGG19'"
if modelname == "VGG16":
    model = VGG16_without_top()
    model.load("./vgg/weights_vgg16_without_top.h5")
elif modelname == "VGG19":
    model = VGG19_without_top()
    model.load("./vgg/weights_vgg19_without_top.h5")

model.set_models(inference=True)
model.set_prevent_update(True) # Parameters of VGG are NOT updated.

ユーティリティ関数の定義

学習済みのVGGモデルを使用するため,入力画像を前処理する必要があります.

In [8]:
mean = np.array([103.939, 116.779, 123.68]) # Mean pixel of ImageNet dataset.

def preprocess_img_vgg(img, size=(400, 400)):
    # PIL image -> numpy array for VGG model
    img = img.resize(size)
    x = np.asarray(img, dtype='float32')
    x = x[:,:,::-1] # RGB -> BGR
    x -= mean
    x = x.transpose((2,0,1))
    x = x[np.newaxis,...]
    return x

def deprocess_img_vgg(x, size=None):
    # numpy array for VGG model -> PIL image
    if x.ndim == 4:
        x = np.squeeze(x)
    x = x.transpose((1,2,0))
    x += mean
    x = x[:,:,::-1] # BGR -> RGB
    x = np.clip(x, 0, 255).astype('uint8')
    img = Image.fromarray(x, mode="RGB")
    if size:
        img = img.resize(size)
    return img

また,gram_mat関数は,スタイル表現としてグラム行列を計算するために用いられます.

In [9]:
def gram_mat(x):
    # Calc gram matrix as style features.
    _, channels, height, width = x.shape
    size = height*width
    feature = rm.reshape(x, (channels, size))
    mat = rm.reshape(rm.dot(feature, feature.T), (1, channels, channels))
    return mat
In [10]:
def del_params(opt, shape):
    keys = list(opt._params.keys())
    for k in keys:
        if opt._params[k]['u'].shape != shape:
            del opt._params[k]

画像のロードと初期化

In [11]:
# Load style/content image
img_content_orig = Image.open(img_path_content).convert("RGB")
img_style_orig = Image.open(img_path_style).convert("RGB")
img_content_size = img_content_orig.size

img_content = preprocess_img_vgg(img_content_orig, size=input_size)
img_style = preprocess_img_vgg(img_style_orig, size=input_size)

# Extract style/content representations from input style/content images
f = model(img_style)
f_style = {ls:gram_mat(f[ls].copy()) for ls in layers_style}
f = model(img_content)
f_content = {lc:f[lc].copy() for lc in layers_content}
In [12]:
# Initialize the image
assert init_mode in ["rand", "content"], "init_mode must be 'rand' or 'content'"
if init_mode == "rand": # init by random image
    img_gen = rm.Variable(np.random.uniform(-16, 15, \
                          (1, 3, input_width, input_width)))
elif init_mode == "content": # init by content image
    img_gen = rm.Variable(img_content.copy())

最適化計算の実行と画像生成

In [13]:
learning_curve_total = []
learning_curve_style = []
learning_curve_content = []
learning_curve_tv = []
ts = time.time()
for i in range(epoch):
    # Encode the image that is generated  with VGG model.
    f_gen = model(img_gen)

    l = 0.
    # Style loss.
    for (layer,w) in zip(layers_style, w_style):
        _, channels, height, width = f_gen[layer].shape
        gram_gen = gram_mat(f_gen[layer])
        l += w * rm.mean_squared_error(gram_gen, f_style[layer])\
            / (2. * (channels*height*width)**2)
    l1 = l.as_ndarray()
    # Content loss.
    for (layer,w) in zip(layers_content, w_content):
        l += lam * w * rm.mean_squared_error(f_gen[layer], f_content[layer])
    l2 = l.as_ndarray() - l1
    # Total variation loss.
    tv1 = (img_gen[:,:,:-1,:-1] - img_gen[:,:,1:,:-1])**2
    tv2 = (img_gen[:,:,:-1,:-1] - img_gen[:,:,:-1,1:])**2
    l +=  eta * rm.sum(tv1+tv2)
    l3 = l.as_ndarray() - l2 - l1

    # Calc the gradient and Update the pixel values of the image.
    grad = l.grad()
    grad.update(optimizer)
    del_params(optimizer, img_gen.shape)

    loss = l.as_ndarray()
    learning_curve_total.append(loss)
    learning_curve_style.append(l1)
    learning_curve_content.append(l2)
    learning_curve_tv.append(l3)

    if i%50 == 0 or i == epoch-1:
        generated_image = deprocess_img_vgg(img_gen.as_ndarray(), img_content_size)
        path_out = os.path.join(img_path_results, suffix_results+"_{0:04d}.jpg".format(i))
        generated_image.save(path_out)
        print("epoch:%5d/%d, loss: %.0f (style: %.0f, content: %.0f, tv:%.0f)" % (i, epoch-1, loss, l1, l2, l3))

# Print time elapsed.
t_elapsed = int(time.time() - ts)
t_h = t_elapsed // 3600
t_m = (t_elapsed - t_h*3600) // 60
t_s = t_elapsed - t_h*3600 - t_m*60
print("finished!")
print("time elapsed --- {0:02d}:{1:02d}:{2:02d}".format(t_h, t_m, t_s))
epoch:    0/1999, loss: 34417192 (style: 34416996, content: 0, tv:196)
epoch:   50/1999, loss: 251800 (style: 251012, content: 356, tv:431)
epoch:  100/1999, loss: 102507 (style: 101699, content: 372, tv:437)
epoch:  150/1999, loss: 58834 (style: 58011, content: 380, tv:443)
epoch:  200/1999, loss: 38465 (style: 37633, content: 384, tv:447)
epoch:  250/1999, loss: 27533 (style: 26696, content: 388, tv:450)
epoch:  300/1999, loss: 20988 (style: 20148, content: 389, tv:451)
epoch:  350/1999, loss: 16784 (style: 15943, content: 390, tv:451)
epoch:  400/1999, loss: 13933 (style: 13093, content: 390, tv:451)
epoch:  450/1999, loss: 11922 (style: 11083, content: 390, tv:450)
epoch:  500/1999, loss: 10459 (style: 9621, content: 389, tv:448)
epoch:  550/1999, loss: 9355 (style: 8520, content: 389, tv:446)
epoch:  600/1999, loss: 8499 (style: 7668, content: 388, tv:444)
epoch:  650/1999, loss: 7816 (style: 6988, content: 387, tv:441)
epoch:  700/1999, loss: 7259 (style: 6435, content: 386, tv:438)
epoch:  750/1999, loss: 6794 (style: 5974, content: 385, tv:435)
epoch:  800/1999, loss: 6397 (style: 5582, content: 384, tv:431)
epoch:  850/1999, loss: 6055 (style: 5244, content: 383, tv:428)
epoch:  900/1999, loss: 5756 (style: 4951, content: 382, tv:424)
epoch:  950/1999, loss: 5496 (style: 4695, content: 381, tv:420)
epoch: 1000/1999, loss: 5266 (style: 4471, content: 379, tv:416)
epoch: 1050/1999, loss: 5062 (style: 4271, content: 378, tv:412)
epoch: 1100/1999, loss: 4878 (style: 4093, content: 377, tv:408)
epoch: 1150/1999, loss: 5970 (style: 5188, content: 377, tv:404)
epoch: 1200/1999, loss: 4600 (style: 3825, content: 375, tv:400)
epoch: 1250/1999, loss: 12025 (style: 11253, content: 375, tv:397)
epoch: 1300/1999, loss: 4393 (style: 3627, content: 373, tv:392)
epoch: 1350/1999, loss: 14586 (style: 13822, content: 374, tv:390)
epoch: 1400/1999, loss: 4262 (style: 3506, content: 371, tv:385)
epoch: 1450/1999, loss: 4032 (style: 3282, content: 370, tv:381)
epoch: 1500/1999, loss: 4040 (style: 3294, content: 369, tv:377)
epoch: 1550/1999, loss: 3868 (style: 3126, content: 367, tv:374)
epoch: 1600/1999, loss: 5097 (style: 4361, content: 366, tv:370)
epoch: 1650/1999, loss: 15024 (style: 14289, content: 367, tv:368)
epoch: 1700/1999, loss: 3863 (style: 3136, content: 364, tv:363)
epoch: 1750/1999, loss: 3629 (style: 2906, content: 363, tv:360)
epoch: 1800/1999, loss: 3883 (style: 3160, content: 365, tv:358)
epoch: 1850/1999, loss: 3587 (style: 2869, content: 362, tv:355)
epoch: 1900/1999, loss: 6792 (style: 6078, content: 362, tv:352)
epoch: 1950/1999, loss: 3508 (style: 2799, content: 360, tv:349)
epoch: 1999/1999, loss: 3702 (style: 2996, content: 360, tv:346)
finished!
time elapsed --- 00:11:49

結果の出力

In [14]:
# Generated image.
plt.figure(figsize=(10,10))
plt.imshow(np.asarray(generated_image))
plt.title('generated image')
plt.axis('off')
plt.show()

# Original content/style image.
fig, ax = plt.subplots(ncols=2, nrows=1, figsize=(10,8))
ax[0].set_title('content image')
ax[0].imshow(np.asarray(img_content_orig))
ax[0].axis('off')
ax[1].set_title('style image')
ax[1].imshow(np.asarray(img_style_orig))
ax[1].axis('off')
plt.tight_layout()
plt.show()

# Learning curve.
plt.plot(learning_curve_total, linewidth=3, label='total loss')
plt.plot(learning_curve_style, linewidth=2, label='style loss')
plt.plot(learning_curve_content, linewidth=2, label='content loss')
plt.plot(learning_curve_tv, linewidth=2, label='total variation loss')
plt.title("Learning curve")
plt.ylabel("error")
plt.xlabel("epoch")
plt.yscale("log")
plt.legend()
plt.grid()
plt.show()
../../../_images/notebooks_image_processing_neural-style-transfer_notebook_26_0.png
../../../_images/notebooks_image_processing_neural-style-transfer_notebook_26_1.png
../../../_images/notebooks_image_processing_neural-style-transfer_notebook_26_2.png

(Style image: “The Starry Night” , Vincent van Gogh, 1889. Content image: Photo by Pixabay .)

分析

下図はVGGの各畳込み層において,スタイル画像とコンテンツ画像がどのように表現されているかを表しています.第1行目はオリジナルのスタイル画像とコンテンツ画像を示し,第2行目以降は各レイヤーの特徴マップの出力のみを使って再構成された画像を表示しています(Gatysらが用いた再構成の方法は,Mahendran et al. 2014[6]に基づくものです.簡単にいうと,コンテンツの再構成においては,目的とするレイヤーのコンテンツ損失を最適化するように勾配降下法が実行されます.詳しくは,下記の附録を参照してください.).

スタイル表現(左側)については,高いレイヤーとなるほどテクスチャのパターンがより大域的になっている様子が読み取れます.右の列からは,細かなピクセル単位の情報は失われつつも,高いレイヤーにおいてもコンテンツの情報は保持されていることがわかります.

続いて,下図にて,生成画像と入力のコンテンツ/スタイル画像との間で,コンテンツ表現とスタイル表現を比較してみます.各画像は最適化に用いた損失関数の計算に使われたレイヤー(スタイルについてはconv1_1〜conv5_1,コンテンツについてはconv5_2)から再構成されたものです.図を見ると,生成された画像のスタイル表現は,スタイル画像のそれとほぼ一致しています.コンテンツ表現については,全体的な様子はやや異なるものの,再構成された両画像からはコンテンツ(狼)を確認することができます.

その他の結果例

(A: “Portrait of Mademoiselle Irene Cahen d’Anvers” by Pierre-Auguste Renoir, 1880. B: “The Great Wave off Kanagawa” by Katsushika Hokusai, 1847. C: “Blue Painting” by Wassily Kandinsky, 1924. D-G: Images by Pixabay .)

参考文献

[1] Gatys, Leon A., Alexander S. Ecker, and Matthias Bethge. “Image style transfer using convolutional neural networks.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2016.
[2] Gatys, Leon A., Alexander S. Ecker, and Matthias Bethge. “A Neural Algorithm of Artistic Style.” arXiv preprint arXiv:1508.06576. 2015.
[3] Simonyan, K. and Zisserman, A. “Very Deep Convolutional Networks for Large-Scale Image Recognition”, arXiv:1409.1556
[4] Min Lin, Qiang Chen, Shuicheng Yan. “Network In Network.” arXiv:1312.4400
[5] Szegedy, Christian, et al. “Going deeper with convolutions.” Proceedings of the IEEE conference on computer vision and pattern recognition (CVPR). 2015.
[6] Mahendran, Aravindh, and Andrea Vedaldi. “Understanding deep image representations by inverting them.” arXiv:1412.0035

附録:Style/Content Reconstruction

この項では,CNNの特徴マップから画像を再構成する方法についてお伝えします.手法はGatysらによって説明されており,Mahendranら[6]に基づいています.

コンテンツの再構成においては,ホワイトノイズ画像に対する勾配降下法を実行し,オリジナル画像の特徴マップ出力とマッチする画像を探索します.すなわち,損失関数は次のようになります.

\begin{equation*} \mathcal{L}_{content}(\vec{p},\vec{x},l)=\frac{1}{2} \sum_{i,j}(F^l_{ij}-P^l_{ij})^2. \end{equation*}

なお,ここでは,スタイルの損失は用いません.

スタイルの再構成においても勾配降下法が用いられます.損失関数はスタイル損失のみで構成されています.

\begin{equation*} \mathcal{L}_{style}(\vec{a},\vec{x},l)=\frac{1}{4N_l^2 M_l^2} \sum_{i,j}(G^l_{ij}-A^l_{ij})^2 \end{equation*}

もし複数のレイヤーの情報から画像を再構成したければ,損失関数は,各レイヤーに対する上記 \mathcal{L}_{style} の重み付き和をとるとよいでしょう.

よって,上で示したStyle Transfer用のコードを再利用することができます.
下にその変更すべき箇所を示しました.

コンテンツ再構成用のコード(抜粋)

  • 損失関数に関する設定

# Layers used to calculate the content loss and their weights.
layers_content = ['conv5_2',] ##CHANGE## Select the target layers
w_content = rm.Node(np.array([2**i for i in range(len(layers_content))]))
w_content = w_content / rm.sum(w_content)
  • 最適化に関する設定

# Initialization method of the generated image. (content image/random image)
# init_mode = "content"
init_mode = "rand" ##CHANGE## Init by random image.
  • 最適化計算の実行と画像生成

for i in range(epoch):
    f_gen = model(img_gen)

    l = 0.
    # Style loss. ##CHANGE## Use only content loss
    #for (layer,w) in zip(layers_style, w_style):
    #    _, channels, height, width = f_gen[layer].shape
    #    gram_gen = gram_mat(f_gen[layer])
    #    l += w * rm.mean_squared_error(gram_gen, f_style[layer])\
    #        / (2. * (channels*height*width)**2)
    l1 = l.as_ndarray()
    # Content loss.
    for (layer,w) in zip(layers_content, w_content):
        l += lam * w * rm.mean_squared_error(f_gen[layer], f_content[layer])
    l2 = l.as_ndarray() - l1

    grad = l.grad()
    grad.update(optimizer)

スタイル再構成用のコード(抜粋)

  • 損失関数に関する設定

# Layers used to calculate the style loss and their weights.
layers_style = ['conv1_1',] ##CHANGE## Select the target layers
w_style = [1./len(layers_style)] * len(layers_style)
  • 最適化に関する設定

# Initialization method of the generated image. (content image/random image)
# init_mode = "content"
init_mode = "rand" ##CHANGE## Init by random image.
  • 最適化計算の実行と画像生成

for i in range(epoch):
    f_gen = model(img_gen)

    l = 0.
    # Style loss.
    for (layer,w) in zip(layers_style, w_style):
        _, channels, height, width = f_gen[layer].shape
        gram_gen = gram_mat(f_gen[layer])
        l += w * rm.mean_squared_error(gram_gen, f_style[layer])\
            / (2. * (channels*height*width)**2)
    l1 = l.as_ndarray()
    # Content loss. ##CHANGE## Use only style loss
    #for (layer,w) in zip(layers_content, w_content):
    #    l += lam * w * rm.mean_squared_error(f_gen[layer], f_content[layer])
    l2 = l.as_ndarray() - l1

    grad = l.grad()
    grad.update(optimizer)