今回は、Stanford University/CS231n/Assignment3/Style Transfer(スタイル変換)PyTorch(パイトーチ)編をやる。
Style Transfer¶
このノートブックでは、1由来のスタイル変換テクの実装をやる。大まかな考え方は、2つの画像のうちの一つからは内容。もう一つからは芸術的作風を抽出してそれらを反映させて別の画像を作り出すということ。このことを、多層ネットワークの特徴スペースにある、それぞれめいめいの画像内容とスタイルにマッチする損失関数を最初に作成してから、画像自体の画素に勾配降下法を実行することで実現する。
feature extractor(特徴抽出器)として今回使用する深層ネットワークは、ImageNetで訓練された小規模モデルであるSqueezeNet。どんなネットワークを使っても一向に構わないが、その小さなサイズと効率性からここではSqueezeNetを選んでいる。
このエクササイズが終わるまでに生成可能になる画像の一例を示しておく。
Setup¶
import torch
import torch.nn as nn
from torch.autograd import Variable
import torchvision
import torchvision.transforms as T
import PIL
import numpy as np
from scipy.misc import imread
from collections import namedtuple
import matplotlib.pyplot as plt
from cs231n.image_utils import SQUEEZENET_MEAN, SQUEEZENET_STD
%matplotlib inline
今回の課題のこの部分に関しては、CIFAR-10データではなく、本当のJPEGを取り扱うので、画像を処理するためのヘルパー関数を提供する。
def preprocess(img, size=512):
transform = T.Compose([
T.Scale(size),
T.ToTensor(),
T.Normalize(mean=SQUEEZENET_MEAN.tolist(),
std=SQUEEZENET_STD.tolist()),
T.Lambda(lambda x: x[None]),
])
return transform(img)
def deprocess(img):
transform = T.Compose([
T.Lambda(lambda x: x[0]),
T.Normalize(mean=[0, 0, 0], std=[1.0 / s for s in SQUEEZENET_STD.tolist()]),
T.Normalize(mean=[-m for m in SQUEEZENET_MEAN.tolist()], std=[1, 1, 1]),
T.Lambda(rescale),
T.ToPILImage(),
])
return transform(img)
def rescale(x):
low, high = x.min(), x.max()
x_rescaled = (x - low) / (high - low)
return x_rescaled
def rel_error(x,y):
return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))
def features_from_img(imgpath, imgsize):
img = preprocess(PIL.Image.open(imgpath), size=imgsize)
img_var = Variable(img.type(dtype))
return extract_features(img_var, cnn), img_var
# 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('.')2)
# assert vnum >= 16, "You must install SciPy >= 0.16.0 to complete this notebook."
#check_scipy()
answers = np.load('style-transfer-checks.npz')
前回の課題と同じように、CPUかGPUのどちらか一つを選択するようにdtypeを設定する必要がある。
dtype = torch.FloatTensor
# Uncomment out the following line if you're on a machine with a GPU set up for PyTorch!
# dtype = torch.cuda.FloatTensor
# Load the pre-trained SqueezeNet model.
cnn = torchvision.models.squeezenet1_1(pretrained=True).features
cnn.type(dtype)
# We don't want to train the model any further, so we don't want PyTorch to waste computation
# computing gradients on parameters we're never going to update.
for param in cnn.parameters():
param.requires_grad = False
# We provide this helper code which takes an image, a model (cnn), and returns a list of
# feature maps, one per layer.
def extract_features(x, cnn):
"""
Use the CNN to extract features from the input image x.
Inputs:
- x: A PyTorch Variable of shape (N, C, H, W) holding a minibatch of images that
will be fed to the CNN.
- cnn: A PyTorch model that we will use to extract features.
Returns:
- features: A list of feature for the input images x extracted using the cnn model.
features[i] is a PyTorch Variable of shape (N, C_i, H_i, W_i); recall that features
from different layers of the network may have different numbers of channels (C_i) and
spatial dimensions (H_i, W_i).
"""
features = []
prev_feat = x
for i, module in enumerate(cnn._modules.values()):
next_feat = module(prev_feat)
features.append(next_feat)
prev_feat = next_feat
return features
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 giving the weighting for the content loss.
- content_current: features of the current image; this is a PyTorch Tensor of shape
(1, C_l, H_l, W_l).
- content_target: features of the content image, Tensor with shape (1, C_l, H_l, W_l).
Returns:
- scalar content loss
"""
content_loss = content_weight*torch.sum((torch.pow(content_current-content_original,2)))
return content_loss
content loss(コンテンツロス)をテストする。エラーは0.001未満。
def content_loss_test(correct):
content_image = 'styles/tubingen.jpg'
image_size = 192
content_layer = 3
content_weight = 6e-2
c_feats, content_img_var = features_from_img(content_image, image_size)
bad_img = Variable(torch.zeros(*content_img_var.data.size()))
feats = extract_features(bad_img, cnn)
student_output = content_loss(content_weight, c_feats[content_layer], feats[content_layer]).data.numpy()
error = rel_error(correct, student_output)
print('Maximum error is {:.6f}'.format(error))
content_loss_test(answers['cl_out'])
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: PyTorch Variable of shape (N, C, H, W) giving features for
a batch of N images.
- normalize: optional, whether to normalize the Gram matrix
If True, divide the Gram matrix by the number of neurons (H * W * C)
Returns:
- gram: PyTorch Variable of shape (N, C, C) giving the
(optionally normalized) Gram matrices for the N input images.
"""
N, C, H, W = features.size()
# Use torch.bmm for batch multiplication of matrices
feat_reshaped = features.view(N, C, -1)
gram = torch.bmm(feat_reshaped, feat_reshaped.transpose(1,2))
if normalize:
return gram/(H*W*C)
else:
return gram
グラム行列コードをテストする。エラーは0.001未満になるはず。
def gram_matrix_test(correct):
style_image = 'styles/starry_night.jpg'
style_size = 192
feats, _ = features_from_img(style_image, style_size)
student_output = gram_matrix(feats[5].clone()).data.numpy()
error = rel_error(correct, student_output)
print('Maximum error is {:.5f}'.format(error))
gram_matrix_test(answers['gm_out'])
次に、スタイルロスを実装する。
# Now put it together in the style_loss function...
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 PyTorch Variable 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 PyTorch Variable holding a scalar giving the 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 = Variable(torch.FloatTensor([0]))
for i in range(len(style_layers)):
gram = gram_matrix(feats[style_layers[i]])
style_loss += style_weights[i]*torch.sum(torch.pow(gram-style_targets[i], 2))
return style_loss
スタイルロス実装をテストする。エラーは0.001未満になる。
def style_loss_test(correct):
content_image = 'styles/tubingen.jpg'
style_image = 'styles/starry_night.jpg'
image_size = 192
style_size = 192
style_layers = [1, 4, 6, 7]
style_weights = [300000, 1000, 15, 3]
c_feats, _ = features_from_img(content_image, image_size)
feats, _ = features_from_img(style_image, style_size)
style_targets = []
for idx in style_layers:
style_targets.append(gram_matrix(feats[idx].clone()))
student_output = style_loss(c_feats, style_layers, style_targets, style_weights).data.numpy()
error = rel_error(correct, student_output)
print('Error is {:.3f}'.format(error))
style_loss_test(answers['sl_out'])
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: PyTorch Variable of shape (1, 3, H, W) holding an input image.
- tv_weight: Scalar giving the weight w_t to use for the TV loss.
Returns:
- loss: PyTorch Variable 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 = torch.sum(torch.pow(img[:,:,:,:-1]-img[:,:,:,1:],2))
h_variance = torch.sum(torch.pow(img[:,:,:-1,:]-img[:,:,1:,:],2))
loss = tv_weight*(h_variance+w_variance)
return loss
TVロス実装をテストする。エラーは0.001未満になる。
def tv_loss_test(correct):
content_image = 'styles/tubingen.jpg'
image_size = 192
tv_weight = 2e-2
content_img = preprocess(PIL.Image.open(content_image), size=image_size)
content_img_var = Variable(content_img.type(dtype))
student_output = tv_loss(content_img_var, tv_weight).data.numpy()
error = rel_error(correct, student_output)
print('Error is {:.6f}'.format(error))
tv_loss_test(answers['tv_out'])
全てを一つにまとめ上げる(この関数は修正する必要はない)。
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 for the content image
content_img = preprocess(PIL.Image.open(content_image), size=image_size)
content_img_var = Variable(content_img.type(dtype))
feats = extract_features(content_img_var, cnn)
content_target = feats[content_layer].clone()
# Extract features for the style image
style_img = preprocess(PIL.Image.open(style_image), size=style_size)
style_img_var = Variable(style_img.type(dtype))
feats = extract_features(style_img_var, cnn)
style_targets = []
for idx in style_layers:
style_targets.append(gram_matrix(feats[idx].clone()))
# Initialize output image to content image or nois
if init_random:
img = torch.Tensor(content_img.size()).uniform_(0, 1)
else:
img = content_img.clone().type(dtype)
# We do want the gradient computed on our image!
img_var = Variable(img, requires_grad=True)
# Set up optimization hyperparameters
initial_lr = 3.0
decayed_lr = 0.1
decay_lr_at = 180
# Note that we are optimizing the pixel values of the image by passing
# in the img_var Torch variable, whose requires_grad flag is set to True
optimizer = torch.optim.Adam([img_var], lr=initial_lr)
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(content_img.cpu()))
axarr[1].imshow(deprocess(style_img.cpu()))
plt.show()
plt.figure()
for t in range(200):
if t < 190:
img.clamp_(-1.5, 1.5)
optimizer.zero_grad()
feats = extract_features(img_var, cnn)
# 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
loss.backward()
# Perform gradient descents on our image values
if t == decay_lr_at:
optimizer = torch.optim.Adam([img_var], lr=decayed_lr)
optimizer.step()
if t % 100 == 0:
print('Iteration {}'.format(t))
plt.axis('off')
plt.imshow(deprocess(img.cpu()))
plt.show()
print('Iteration {}'.format(t))
plt.axis('off')
plt.imshow(deprocess(img.cpu()))
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)
# 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)
# 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)
Feature Inversion¶
ここで書いたコードは別の凄いことも実現できる。畳み込みネットワークが認識することを学習する特徴の種類を理解するために、最近の論文3が、画像をその画像の特徴表現から再現することを試みている。この考え方は、まさに上でやったことである(が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)
- “Image Style Transfer Using Convolutional Neural Networks” (Gatys et al., CVPR 2015)
- Aravindh Mahendran, Andrea Vedaldi, “Understanding Deep Image Representations by Inverting them”, CVPR 2015