CS231n/課題3/スタイル変換 (TensorFlow)

今回は、Stanford University/CS231n/Assignment3/Style Transfer(スタイル変換)のTensorFlow(テンソルフロー)編をやる。

このノートブックでは、1由来のスタイル変換テクの実装をやる。大まかな考え方は、2つの画像のうちの一つからは内容。もう一つからは芸術的作風を抽出してそれらを反映させて別の画像を作り出すということ。このことを、多層ネットワークの特徴スペースにある、それぞれめいめいの画像内容とスタイルにマッチする損失関数を最初に作成してから、画像自体の画素に勾配降下法を実行することで実現する。

feature extractor(特徴抽出器)として今回使用する深層ネットワークは、ImageNetで訓練された小規模モデルであるSqueezeNet。どんなネットワークを使っても一向に構わないが、その小さなサイズと効率性からここではSqueezeNetを選んでいる。

このエクササイズが終わるまでに生成可能になる画像の一例を示しておく。
alt text

Setup

from scipy.misc import imread, imresize
import numpy as np
from scipy.misc import imread
import matplotlib.pyplot as plt
# Helper functions to deal with image preprocessing
from cs231n.image_utils import load_image, preprocess_image, deprocess_image

def get_session():
    """Create a session that dynamically allocates memory."""
    # See: https://www.tensorflow.org/tutorials/using_gpu#allowing_gpu_memory_growth
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    session = tf.Session(config=config)
    return session

def rel_error(x,y):
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

# Older versions of scipy.misc.imresize yield different results
# from newer versions, so we check to make sure scipy is up to date.
#def check_scipy():
#    import scipy
#    vnum = int(scipy.__version__.split('.')[1])
#    assert vnum >= 16, "You must install SciPy >= 0.16.0 to complete this notebook."
#check_scipy()
%load_ext autoreload
%autoreload 2
%matplotlib inline
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

事前学習済みSqueezeNetモデルをロードする。このモデルは、PyTorchから移植されている。モデル構造に関してはcs231n/classifiers/squeezenet.pyを参照のこと。

SqueezeNetを使用するには、先ずcs231n/datasetsディレクトリからsqueezenet_tf.shを実行して重みをダウンロードする必要がある。get_assignment3_data.shを既に実行している場合はSqueezeNetはダウンロード済み。

from cs231n.classifiers.squeezenet import SqueezeNet
import tensorflow as tf

tf.reset_default_graph() # remove all existing variables in the graph 
sess = get_session() # start a new Session

# Load pretrained SqueezeNet model
SAVE_PATH = 'cs231n/datasets/squeezenet.ckpt'
#if not os.path.exists(SAVE_PATH):
#    raise ValueError("You need to download SqueezeNet!")
model = SqueezeNet(save_path=SAVE_PATH, sess=sess)

# Load data for testing
content_img_test = preprocess_image(load_image('styles/tubingen.jpg', size=192))[None]
style_img_test = preprocess_image(load_image('styles/starry_night.jpg', size=192))[None]
answers = np.load('style-transfer-checks-tf.npz')
INFO:tensorflow:Restoring parameters from cs231n/datasets/squeezenet.ckpt

Computing Loss

先ずは損失関数の3つの要素を算出する。この損失関数は、content loss + style loss + total variation lossの3項の加重和のこと。これからこれらの加重項を算出する関数を作成していく。

Content loss

一つの画像の内容ともう一つの画像のスタイルの両方を損失関数に組み込むことで、この両者を反映する画像を生成することができる。内容画像の内容偏差とスタイル画像のスタイル偏差にペナルティを与えてから、モデルのパラメーターではなく原画像のピクセル値に勾配降下法を実行するのに、このハイブリッド損失関数を使うことができる。

最初にcontent loss function(内容損失関数)を作成する。内容損失は、生成された画像の特徴マップがどのくらい元画像の特徴マップと異なるのかを測定する。$C_\ell$が層$\ell$のフィルタ/チャンネルかつ$H_\ell$と$W_\ell$が高さと幅である特徴マップ$A^\ell \in \mathbb{R}^{1 \times C_\ell \times H_\ell \times W_\ell}$を持つネットワークの1層(例えば$\ell$層)のcontent representation(内容表現)のみに関心を払う。全空間位置を一つの次元に混合しているこれらの特徴マップのreshaped versions(再形成版)を使う。$F^\ell \in \mathbb{R}^{N_\ell \times M_\ell}$を現画像の特徴マップ、$P^\ell \in \mathbb{R}^{N_\ell \times M_\ell}$を、$M_\ell=H_\ell\times W_\ell$が各特徴マップの要素数である内容元画像の特徴マップとする。$F^\ell$や$P^\ell$の各列は、特定のフィルターのベクトル化活性(vectorized activations)を表し、画像の全ポジションに対して畳み込まれている。最後に、$w_c$を損失関数の内容損失項の重みにする。その結果、コンテンツロスは$L_c = w_c \times \sum_{i,j} (F_{ij}^{\ell} – P_{ij}^{\ell})^2$によって与えられる。

def content_loss(content_weight, content_current, content_original):
    """
    Compute the content loss for style transfer.
    
    Inputs:
    - content_weight: scalar constant we multiply the content_loss by.
    - content_current: features of the current image, Tensor with shape [1, height, width, channels]
    - content_target: features of the content image, Tensor with shape [1, height, width, channels]
    
    Returns:
    - scalar content loss
    """
    loss = content_weight*tf.reduce_sum(tf.pow(content_current-content_original, 2))
    return loss

content loss(コンテンツロス)をテストする。エラーは0.001未満。

def content_loss_test(correct):
    content_layer = 3
    content_weight = 6e-2
    c_feats = sess.run(model.extract_features()[content_layer], {model.image: content_img_test})
    bad_img = tf.zeros(content_img_test.shape)
    feats = model.extract_features(bad_img)[content_layer]
    student_output = sess.run(content_loss(content_weight, c_feats, feats))
    error = rel_error(correct, student_output)
    print('Maximum error is {:.7f}'.format(error))

content_loss_test(answers['cl_out'])
Maximum error is 0.0000014

Style loss

今度はスタイル損失に取り組む。所定の層$\ell$に対するスタイル損失は以下のようにして定義付けられる。

先ず、Fが上述のように各フィルタ反応間の相関を表すGram matrix(グラム行列)Gを計算する。グラム行列はcovariance matrix(共分散行列)に近似している。スタイル画像のactivation statistics(活性化統計)にマッチさせるための生成した画像の活性化統計が必要で、(近似)共分散をマッチングさせることが、そのことを実現するための一つの方法になっている。これを行う方法は色々あるが、グラム行列は計算が簡単な上に実際に良好な結果を出しているので良い選択肢である。

shape $(1, C_\ell, M_\ell)$の特徴マップ$F^\ell$とすれば、グラム行列はshape $(1, C_\ell, C_\ell)$を持ち、その要素は以下によって与えられる。
$$G_{ij}^\ell = \sum_k F^{\ell}_{ik} F^{\ell}_{jk}$$

$G^\ell$が現画像の特徴マップのグラム行列、$A^\ell$は元スタイル画像の特徴マップのグラム行列、$w_\ell$がスカラー重み項と仮定すれば、層$\ell$に対するスタイル損失は、単に、下記の2つのグラム行列間の加重ユークリッド距離に過ぎない。
$$L_s^\ell = w_\ell \sum_{i, j} \left(G^\ell_{ij} – A^\ell_{ij}\right)^2$$

実際には通常は、単に単層$\ell$だけではなく一連の層$\mathcal{L}$でスタイルロスを算出する。その場合、総スタイルロスは、下記の各層でのスタイルロスの合計になる。
$$L_s = \sum_{\ell \in \mathcal{L}} L_s^\ell$$

先ずは下のセルにグラム行列演算を実装する事から始める。

def gram_matrix(features, normalize=True):
    """
    Compute the Gram matrix from features.
    
    Inputs:
    - features: Tensor of shape (1, H, W, C) giving features for
      a single image.
    - normalize: optional, whether to normalize the Gram matrix
        If True, divide the Gram matrix by the number of neurons (H * W * C)
    
    Returns:
    - gram: Tensor of shape (C, C) giving the (optionally normalized)
      Gram matrices for the input image.
    """
    shapes = tf.shape(features)
    feat_reshape = tf.reshape(features, [1, -1, shapes[3]])
    gram = tf.matmul(tf.transpose(feat_reshape, perm=[0, 2, 1]), feat_reshape)
    if normalize:
        n_neurons = tf.cast(shapes[1]*shapes[2]*shapes[3], tf.float32)
        gram_norm = gram/n_neurons
        return gram_norm
    else:
        return gram

グラム行列コードをテストする。エラーは0.001未満になるはず。

def gram_matrix_test(correct):
    gram = gram_matrix(model.extract_features()[5])
    student_output = sess.run(gram, {model.image: style_img_test})
    error = rel_error(correct, student_output)
    print('Maximum error is {:.6f}'.format(error))

gram_matrix_test(answers['gm_out'])
Maximum error is 0.000000

次に、スタイルロスを実装する。

def style_loss(feats, style_layers, style_targets, style_weights):
    """
    Computes the style loss at a set of layers.
    
    Inputs:
    - feats: list of the features at every layer of the current image, as produced by
      the extract_features function.
    - style_layers: List of layer indices into feats giving the layers to include in the
      style loss.
    - style_targets: List of the same length as style_layers, where style_targets[i] is
      a Tensor giving the Gram matrix the source style image computed at
      layer style_layers[i].
    - style_weights: List of the same length as style_layers, where style_weights[i]
      is a scalar giving the weight for the style loss at layer style_layers[i].
      
    Returns:
    - style_loss: A Tensor contataining the scalar style loss.
    """
    # Hint: you can do this with one for loop over the style layers, and should
    # not be very much code (~5 lines). You will need to use your gram_matrix function.
    style_loss = tf.constant(0.0)
    for i in range(len(style_layers)):
        gram = gram_matrix(feats[style_layers[i]])
        style_loss += style_weights[i]*tf.reduce_sum(tf.pow(gram-style_targets[i], 2))
    return style_loss

スタイルロス実装をテストする。エラーは0.001未満になる。

def style_loss_test(correct):
    style_layers = [1, 4, 6, 7]
    style_weights = [300000, 1000, 15, 3]
    
    feats = model.extract_features()
    style_target_vars = []
    for idx in style_layers:
        style_target_vars.append(gram_matrix(feats[idx]))
    style_targets = sess.run(style_target_vars,
                             {model.image: style_img_test})
                             
    s_loss = style_loss(feats, style_layers, style_targets, style_weights)
    student_output = sess.run(s_loss, {model.image: content_img_test})
    error = rel_error(correct, student_output)
    print('Error is {:.3f}'.format(error))

style_loss_test(answers['sl_out'])
Error is 0.000

Total-variation regularization

画像の滑らかさを促進することも有益であることが分かっている。ピクセル値の変動もしくはtotal variation(全変動)にペナルティーを与える損失に別の項を加えることでこのことを達成できる。

(水平か垂直に)隣同士のピクセルの全ペアに対するピクセル値の差の二乗和として全変動を算出することができる。ここでは、3つの入力チャンネル(RGB)毎の全変動正則化を合計し、全変動重み$w_t$で総積算ロスを加重する。
$L_{tv} = w_t \times \sum_{c=1}^3\sum_{i=1}^{H-1} \sum_{j=1}^{W-1} \left( (x_{i,j+1, c} – x_{i,j,c})^2 + (x_{i+1, j,c} – x_{i,j,c})^2 \right)$

次のセルでTV損失項の定義を完成させる。実装にループを用いてはならない。

def tv_loss(img, tv_weight):
    """
    Compute total variation loss.
    
    Inputs:
    - img: Tensor of shape (1, H, W, 3) holding an input image.
    - tv_weight: Scalar giving the weight w_t to use for the TV loss.
    
    Returns:
    - loss: Tensor holding a scalar giving the total variation loss
      for img weighted by tv_weight.
    """
    # Your implementation should be vectorized and not require any loops!
    w_variance = tf.reduce_sum(tf.pow(img[:,:,:-1,:]-img[:,:,1:,:],2))
    h_variance = tf.reduce_sum(tf.pow(img[:,:-1,:,:]-img[:,1:,:,:],2))
    loss = tv_weight*(h_variance+w_variance)
    return loss

TVロス実装をテストする。エラーは0.001未満になる。

def tv_loss_test(correct):
    tv_weight = 2e-2
    t_loss = tv_loss(model.image, tv_weight)
    student_output = sess.run(t_loss, {model.image: content_img_test})
    error = rel_error(correct, student_output)
    print('Error is {:.3f}'.format(error))

tv_loss_test(answers['tv_out'])
Error is 0.000

Style Transfer

コードを一つにまとめ上げてからいくつかの美しいイメージを作る。下のスタイル変換関数は、上でコーディングした全ての損失を組み合わせ、総損失を極小化する画像のための最適化をする。

def style_transfer(content_image, style_image, image_size, style_size, content_layer, content_weight,
                   style_layers, style_weights, tv_weight, init_random = False):
    """Run style transfer!
    
    Inputs:
    - content_image: filename of content image
    - style_image: filename of style image
    - image_size: size of smallest image dimension (used for content loss and generated image)
    - style_size: size of smallest style image dimension
    - content_layer: layer to use for content loss
    - content_weight: weighting on content loss
    - style_layers: list of layers to use for style loss
    - style_weights: list of weights to use for each layer in style_layers
    - tv_weight: weight of total variation regularization term
    - init_random: initialize the starting image to uniform random noise
    """
    # Extract features from the content image
    content_img = preprocess_image(load_image(content_image, size=image_size))
    feats = model.extract_features(model.image)
    content_target = sess.run(feats[content_layer],
                              {model.image: content_img[None]})

    # Extract features from the style image
    style_img = preprocess_image(load_image(style_image, size=style_size))
    style_feat_vars = [feats[idx] for idx in style_layers]
    style_target_vars = []
    # Compute list of TensorFlow Gram matrices
    for style_feat_var in style_feat_vars:
        style_target_vars.append(gram_matrix(style_feat_var))
    # Compute list of NumPy Gram matrices by evaluating the TensorFlow graph on the style image
    style_targets = sess.run(style_target_vars, {model.image: style_img[None]})

    # Initialize generated image to content image
    
    if init_random:
        img_var = tf.Variable(tf.random_uniform(content_img[None].shape, 0, 1), name="image")
    else:
        img_var = tf.Variable(content_img[None], name="image")

    # Extract features on generated image
    feats = model.extract_features(img_var)
    # Compute loss
    c_loss = content_loss(content_weight, feats[content_layer], content_target)
    s_loss = style_loss(feats, style_layers, style_targets, style_weights)
    t_loss = tv_loss(img_var, tv_weight)
    loss = c_loss + s_loss + t_loss
    
    # Set up optimization hyperparameters
    initial_lr = 3.0
    decayed_lr = 0.1
    decay_lr_at = 180
    max_iter = 200

    # Create and initialize the Adam optimizer
    lr_var = tf.Variable(initial_lr, name="lr")
    # Create train_op that updates the generated image when run
    with tf.variable_scope("optimizer") as opt_scope:
        train_op = tf.train.AdamOptimizer(lr_var).minimize(loss, var_list=[img_var])
    # Initialize the generated image and optimization variables
    opt_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=opt_scope.name)
    sess.run(tf.variables_initializer([lr_var, img_var] + opt_vars))
    # Create an op that will clamp the image values when run
    clamp_image_op = tf.assign(img_var, tf.clip_by_value(img_var, -1.5, 1.5))
    
    f, axarr = plt.subplots(1,2)
    axarr[0].axis('off')
    axarr[1].axis('off')
    axarr[0].set_title('Content Source Img.')
    axarr[1].set_title('Style Source Img.')
    axarr[0].imshow(deprocess_image(content_img))
    axarr[1].imshow(deprocess_image(style_img))
    plt.show()
    plt.figure()
    
    # Hardcoded handcrafted 
    for t in range(max_iter):
        # Take an optimization step to update img_var
        sess.run(train_op)
        if t < decay_lr_at:
            sess.run(clamp_image_op)
        if t == decay_lr_at:
            sess.run(tf.assign(lr_var, decayed_lr))
        if t % 100 == 0:
            print('Iteration {}'.format(t))
            img = sess.run(img_var)
            plt.imshow(deprocess_image(img[0], rescale=True))
            plt.axis('off')
            plt.show()
    print('Iteration {}'.format(t))
    img = sess.run(img_var)        
    plt.imshow(deprocess_image(img[0], rescale=True))
    plt.axis('off')
    plt.show()

Generate some pretty pictures!

下で設定している3種類のパラメーターを用いてスタイル変換を試す。3つのセルを全て確実に実行する。自分のパラメーターを自由に追加しても良いが、3番目のパラメーターセット(starry night)でのスタイル変換の結果を忘れずに含めること。

  • content_imageは、コンテンツ画像のファイルネーム。
  • style_imageは、スタイル画像のファイルネーム。
  • image_sizeは、コンテンツ画像の最小画像面積の大きさ(内容ロスと生成画像用に使用される)。
  • style_sizeは、最小スタイル画像面積の大きさ。
  • content_layerは、どの層をコンテンツロス用に使用するかを指定する。
  • content_weightは、総合的損失関数に含まれるコンテンツロスに重み付けする。このパラメーターの値を増やすと完成画像をより現実的に仕上げる(オリジナルコンテンツにより近付ける)。
  • style_layersは、スタイルロス用にどの層を使うかのリストを指定する。
  • style_weightsは、style_layersの(それぞれが項を総スタイルロスに提供する)各層に使う重みのリストを指定する。通常は、それらがテクスチャにとって大きな受容野に対する特徴よりも重要である、よりローカルでより小さなスケール特徴を表現するので、初期のスタイル層にはより高い重みを使用する。一般に、これらの重みを増すと生成画像はオリジナルコンテンツとは似つかなくなり、スタイル画像の外観へとより歪められていく。
  • tv_weightは、全損失関数の全変動正則化の重み付けを指定する。この値を増大させると、スタイルとコンテンツに対する忠実度を犠牲にして、生成画像をより滑らかにしてギザギザを減らす。

下の次の3つのセル(ハイパーパラメーターを変えるべきではない)で、自由にパラメーターをコピペして、それらがどう生成画像に変化を与えるかを調べる。

plt.rcParams['figure.figsize'] = 18, 12
plt.rcParams["font.size"] = "16"

# Composition VII + Tubingen
params1 = {
    'content_image' : 'styles/tubingen.jpg',
    'style_image' : 'styles/composition_vii.jpg',
    'image_size' : 192,
    'style_size' : 512,
    'content_layer' : 3,
    'content_weight' : 5e-2, 
    'style_layers' : (1, 4, 6, 7),
    'style_weights' : (20000, 500, 12, 1),
    'tv_weight' : 5e-2
}

style_transfer(**params1)
Iteration 0
Iteration 100
Iteration 199
# Scream + Tubingen
params2 = {
    'content_image':'styles/tubingen.jpg',
    'style_image':'styles/the_scream.jpg',
    'image_size':192,
    'style_size':224,
    'content_layer':3,
    'content_weight':3e-2,
    'style_layers':[1, 4, 6, 7],
    'style_weights':[200000, 800, 12, 1],
    'tv_weight':2e-2
}

style_transfer(**params2)
Iteration 0
Iteration 100
Iteration 199
# Starry Night + Tubingen
params3 = {
    'content_image' : 'styles/tubingen.jpg',
    'style_image' : 'styles/starry_night.jpg',
    'image_size' : 192,
    'style_size' : 192,
    'content_layer' : 3,
    'content_weight' : 6e-2,
    'style_layers' : [1, 4, 6, 7],
    'style_weights' : [300000, 1000, 15, 3],
    'tv_weight' : 2e-2
}

style_transfer(**params3)
Iteration 0
Iteration 100
Iteration 199

Feature Inversion

ここで書いたコードは別の凄いことも実現できる。畳み込みネットワークが認識することを学習する特徴の種類を理解するために、最近の論文2が、画像をその画像の特徴表現から再現することを試みている。この考え方は、まさに上でやったことである(が2つの異なる特徴表現を用いて)、事前学習済みネットワークの画像勾配を使うことで簡単に実装できる。

次に、スタイル重みが全て0になるように設定し、開始画像をコンテンツ原画像の代わりにランダム・ノイズへ初期化すると、コンテンツソース画像の特徴表現から画像を再構築できる。先ず総ノイズからスタートするが、原画像に酷似している生成画像に行き着く必要がある。

(同様に、内容重みをゼロに設定して、開始画像をランダム・ノイズへ初期化した場合texture synthesis(テクスチャ合成)をゼロから行うことができるが、ここではそこまでやることは求めない。)

# Feature Inversion -- Starry Night + Tubingen
params_inv = {
    'content_image' : 'styles/tubingen.jpg',
    'style_image' : 'styles/starry_night.jpg',
    'image_size' : 192,
    'style_size' : 192,
    'content_layer' : 3,
    'content_weight' : 6e-2,
    'style_layers' : [1, 4, 6, 7],
    'style_weights' : [0, 0, 0, 0], # we discard any contributions from style to the loss
    'tv_weight' : 2e-2,
    'init_random': True # we want to initialize our image to be random
}

style_transfer(**params_inv)
Iteration 0
Iteration 100
Iteration 199
参考サイトhttps://github.com/

  1. “Image Style Transfer Using Convolutional Neural Networks” (Gatys et al., CVPR 2015)
  2. Aravindh Mahendran, Andrea Vedaldi, “Understanding Deep Image Representations by Inverting them”, CVPR 2015