TensorFlow 2.0 Beta : 上級 Tutorials : 画像生成 :- 画風変換 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/10/2019
* 本ページは、TensorFlow の本家サイトの TF 2.0 Beta – Advanced Tutorials – Image generation の以下のページを翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
画像生成 :- 画風変換
このチュートリアルは一つの画像をもう一つの画像のスタイルで構成するために深層学習を使用します (Picasso や Van Gogh のように描けたらと思うことがありませんか?)。これは画風変換 (= neural style transfer) として知られこのテクニックは A Neural Algorithm of Artistic Style (Gatys et al.) で概説されています。
画風変換は 2 つの画像 — コンテンツ画像と (有名な画家によるアートワークのような) スタイル参照画像 — を取りそしてそれらを一緒に混ぜ合わせるために使用される最適化テクニックです、その結果出力画像はコンテンツ画像のように見えますが、スタイル参照画像のスタイルで「描かれて」います。
これはコンテンツ画像のコンテンツ統計とスタイル参照画像のスタイル統計が適合するように出力画像を最適化するように実装されます。これらの統計は畳み込みニューラルネットワークを使用して画像から抽出されます。
例として、このウミガメと Wassily Kandinsky のコンポジション 7 の画像を取りましょう :
Green Sea Turtle の画像 -By P.Lindgren [CC BY-SA 3.0]((https://creativecommons.org/licenses/by-sa/3.0), from Wikimedia Common
さて Kandinsky がこのウミガメの絵をこのスタイルだけで書くことを決めたとしたらそれはどのように見えるでしょう?このようなものでしょうか?
Setup
モジュールをインポートして configure する
from __future__ import absolute_import, division, print_function, unicode_literals
!pip install -q tensorflow-gpu==2.0.0-beta1 import tensorflow as tf
import IPython.display as display import matplotlib.pyplot as plt import matplotlib as mpl mpl.rcParams['figure.figsize'] = (12,12) mpl.rcParams['axes.grid'] = False import numpy as np import time import functools
画像をダウンロードしてスタイル画像とコンテンツ画像を選択します :
content_path = tf.keras.utils.get_file('turtle.jpg','https://storage.googleapis.com/download.tensorflow.org/example_images/Green_Sea_Turtle_grazing_seagrass.jpg') style_path = tf.keras.utils.get_file('kandinsky.jpg','https://storage.googleapis.com/download.tensorflow.org/example_images/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg')
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/Green_Sea_Turtle_grazing_seagrass.jpg 32768/29042 [=================================] - 0s 0us/step Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg 196608/195196 [==============================] - 0s 0us/step
入力を可視化する
画像をロードするための関数を定義してその最大次元を 512 ピクセルに制限します。
def load_img(path_to_img): max_dim = 512 img = tf.io.read_file(path_to_img) img = tf.image.decode_image(img, channels=3) img = tf.image.convert_image_dtype(img, tf.float32) shape = tf.cast(tf.shape(img)[:-1], tf.float32) long_dim = max(shape) scale = max_dim / long_dim new_shape = tf.cast(shape * scale, tf.int32) img = tf.image.resize(img, new_shape) img = img[tf.newaxis, :] return img
画像を表示するための単純な関数を作成します :
def imshow(image, title=None): if len(image.shape) > 3: image = tf.squeeze(image, axis=0) plt.imshow(image) if title: plt.title(title)
content_image = load_img(content_path) style_image = load_img(style_path) plt.subplot(1, 2, 1) imshow(content_image, 'Content Image') plt.subplot(1, 2, 2) imshow(style_image, 'Style Image')
コンテンツとスタイル表現を定義する
画像のコンテンツとスタイル表現を得るためにモデルの中間層を使用します。ネットワークの入力層から始めて、最初の幾つかの層活性はエッジやテクスチャーのような低位特徴を表します。ネットワークを通り抜けるにつれて、最後の幾つかの層は高位特徴を表します — 車輪や目のような物体パーツ。このケースでは、貴方は VGG19 ネットワーク・アーキテクチャを使用しています、事前訓練された画像分類ネットワークです。これらの中間層は画像からのコンテンツとスタイルの表現を定義するために必要です。入力画像について、これらの中間層で対応するスタイルとコンテンツ・ターゲット表現をマッチさせようとします。
VGG19 をロードしてそれが正しく使用されていることを確かにするためにそれを画像上でテスト実行します。
x = tf.keras.applications.vgg19.preprocess_input(content_image*255) x = tf.image.resize(x, (224, 224)) vgg = tf.keras.applications.VGG19(include_top=True, weights='imagenet') prediction_probabilities = vgg(x) prediction_probabilities.shape
TensorShape([1, 1000])
predicted_top_5 = tf.keras.applications.vgg19.decode_predictions(prediction_probabilities.numpy())[0] [(class_name, prob) for (number, class_name, prob) in predicted_top_5]
[('loggerhead', 0.74297667), ('leatherback_turtle', 0.11357909), ('hermit_crab', 0.054411974), ('terrapin', 0.039235227), ('mud_turtle', 0.012614701)]
今は分類ヘッドなしで VGG19 をロードして、層名をリストします。
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') print() for layer in vgg.layers: print(layer.name)
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5 80142336/80134624 [==============================] - 2s 0us/step input_2 block1_conv1 block1_conv2 block1_pool block2_conv1 block2_conv2 block2_pool block3_conv1 block3_conv2 block3_conv3 block3_conv4 block3_pool block4_conv1 block4_conv2 block4_conv3 block4_conv4 block4_pool block5_conv1 block5_conv2 block5_conv3 block5_conv4 block5_pool
画像のスタイルとコンテンツを表わすためにネットワークから中間層を選択します :
# Content layer where will pull our feature maps content_layers = ['block5_conv2'] # Style layer of interest style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] num_content_layers = len(content_layers) num_style_layers = len(style_layers)
スタイルとコンテンツのための中間層
それでは何故、事前訓練された画像分類ネットワーク内のこれらの中間層出力はスタイルとコンテンツ表現を定義することを可能にするのでしょう。
高位では、ネットワークが (そのために訓練された) 画像分類を遂行するために、それは画像を理解しなければなりません。これは生画像を入力ピクセルとして取り、生画像ピクセルを画像内の特徴表現の複雑な理解へと変換する内部表現を構築することを必要とします。
これはまた何故畳み込みニューラルネットワークが上手く一般化できるかの理由でもあります : それらは不変性を獲得することができてクラス内 (e.g. 猫 vs. 犬) で (背景ノイズと他の妨害には不可知論である) 特徴を定義します。こうして、生画像がモデルに供給されるところと出力分類ラベルの間のどこかで、モデルは複雑な特徴抽出器としてサーブします。モデルの中間層にアクセスすることにより、入力画像のコンテンツとスタイルを記述することができます。
モデルを構築する
tf.keras.applications でネットワークは設計されますので Keras functional API を使用して中間層値を容易に抽出できます。
functional API を使用してモデルを定義するには、入力と出力を指定します :
model = Model(inputs, outputs)
次の関数は中間層出力のリストを返す VGG19 モデルを構築します。
def vgg_layers(layer_names): """ Creates a vgg model that returns a list of intermediate output values.""" # Load our model. Load pretrained VGG, trained on imagenet data vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet') vgg.trainable = False outputs = [vgg.get_layer(name).output for name in layer_names] model = tf.keras.Model([vgg.input], outputs) return model
そしてモデルを作成するために :
style_extractor = vgg_layers(style_layers) style_outputs = style_extractor(style_image*255) #Look at the statistics of each layer's output for name, output in zip(style_layers, style_outputs): print(name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean()) print()
block1_conv1 shape: (1, 336, 512, 64) min: 0.0 max: 835.5256 mean: 33.97525 block2_conv1 shape: (1, 168, 256, 128) min: 0.0 max: 4625.8857 mean: 199.82687 block3_conv1 shape: (1, 84, 128, 256) min: 0.0 max: 8789.239 mean: 230.78099 block4_conv1 shape: (1, 42, 64, 512) min: 0.0 max: 21566.135 mean: 791.24005 block5_conv1 shape: (1, 21, 32, 512) min: 0.0 max: 3189.2542 mean: 59.179478
スタイルを計算する
画像のコンテンツは中間特徴マップの値により表わされます。
画像のスタイルは異なる特徴マップに渡る平均と相関により記述できることが判明しています。各位置で特徴ベクトルの外積を取り、総ての位置に渡りその外積を平均することによりこの情報を含むグラム行列を計算します。このグラム行列は特定の層のために次のように計算できます :
これは tf.linalg.einsum 関数を使用して簡潔に実装できます :
def gram_matrix(input_tensor): result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32) return result/(num_locations)
スタイルとコンテンツを抽出する
スタイルとコンテンツ tensor を返すモデルを構築します。
class StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() self.vgg = vgg_layers(style_layers + content_layers) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) self.vgg.trainable = False def call(self, inputs): "Expects float input in [0,1]" inputs = inputs*255.0 preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs) outputs = self.vgg(preprocessed_input) style_outputs, content_outputs = (outputs[:self.num_style_layers], outputs[self.num_style_layers:]) style_outputs = [gram_matrix(style_output) for style_output in style_outputs] content_dict = {content_name:value for content_name, value in zip(self.content_layers, content_outputs)} style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)} return {'content':content_dict, 'style':style_dict}
画像上で呼び出されたとき、このモデルは style_layers のグラム行列 (style) と content_layers のコンテンツを返します :
extractor = StyleContentModel(style_layers, content_layers) results = extractor(tf.constant(content_image)) style_results = results['style'] print('Styles:') for name, output in sorted(results['style'].items()): print(" ", name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean()) print() print("Contents:") for name, output in sorted(results['content'].items()): print(" ", name) print(" shape: ", output.numpy().shape) print(" min: ", output.numpy().min()) print(" max: ", output.numpy().max()) print(" mean: ", output.numpy().mean())
Styles: block1_conv1 shape: (1, 64, 64) min: 0.034881677 max: 26723.18 mean: 780.96204 block2_conv1 shape: (1, 128, 128) min: 0.0 max: 95840.37 mean: 11674.933 block3_conv1 shape: (1, 256, 256) min: 0.0 max: 296185.97 mean: 7241.375 block4_conv1 shape: (1, 512, 512) min: 0.0 max: 3164085.0 mean: 104884.49 block5_conv1 shape: (1, 512, 512) min: 0.0 max: 66307.836 mean: 650.05994 Contents: block5_conv2 shape: (1, 24, 32, 512) min: 0.0 max: 939.0783 mean: 8.983593
勾配降下を実行する
このスタイルとコンテンツ抽出器により、今では画風変換アルゴリズム実装できます。各ターゲットに対する画像の出力のための mean square error を計算することによりこれを行ない、それからこれらの損失の重み付けられた総計を取ります。
スタイルとコンテンツ・ターゲット値を設定します :
style_targets = extractor(style_image)['style'] content_targets = extractor(content_image)['content']
最適化するための画像を含む tf.Variable を定義します。これを迅速に行なうために、それをコンテンツ画像で初期化します (tf.Variable はコンテンツ画像と同じ shape でなければなりません) :
image = tf.Variable(content_image)
これは float 画像ですので、ピクセル値を 0 と 1 の間に保持する関数を定義します :
def clip_0_1(image): return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)
optimizer を作成します。ペーパーは LBFGS を勧めていますが、Adam もまた問題なく動作します :
opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)
これを最適化するために、総計損失を得るために 2 つの損失の重み付けられた結合を使用します :
style_weight=1e-2 content_weight=1e4
def style_content_loss(outputs): style_outputs = outputs['style'] content_outputs = outputs['content'] style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()]) style_loss *= style_weight / num_style_layers content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()]) content_loss *= content_weight / num_content_layers loss = style_loss + content_loss return loss
画像を更新するために tf.GradientTape を使用します。
@tf.function() def train_step(image): with tf.GradientTape() as tape: outputs = extractor(image) loss = style_content_loss(outputs) grad = tape.gradient(loss, image) opt.apply_gradients([(grad, image)]) image.assign(clip_0_1(image))
今はテストのために数ステップ実行します :
train_step(image) train_step(image) train_step(image) plt.imshow(image.read_value()[0])
WARNING: Logging before flag parsing goes to stderr. W0628 03:57:17.008671 140611849496320 deprecation.py:323] From /tmpfs/src/tf_docs_env/lib/python3.5/site-packages/tensorflow/python/ops/math_grad.py:1205: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version. Instructions for updating: Use tf.where in 2.0, which has the same broadcast rule as np.where <matplotlib.image.AxesImage at 0x7fe1d0541390>
それは動作していますので、より長い最適化を遂行します :
import time start = time.time() epochs = 10 steps_per_epoch = 100 step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 train_step(image) print(".", end='') display.clear_output(wait=True) imshow(image.read_value()) plt.title("Train step: {}".format(step)) plt.show() end = time.time() print("Total time: {:.1f}".format(end-start))
Total time: 22.0
総計 variation 損失
この基本的実装の一つの不都合な点はそれは多くの高周波な人工物 (= artifacts) を生成することです。画像の高周波成分上で明示的な正則化項を使用してこれらを減じます。画風変換では、これはしばしば総計 variation 損失と呼ばれます :
def high_pass_x_y(image): x_var = image[:,:,1:,:] - image[:,:,:-1,:] y_var = image[:,1:,:,:] - image[:,:-1,:,:] return x_var, y_var
x_deltas, y_deltas = high_pass_x_y(content_image) plt.figure(figsize=(14,10)) plt.subplot(2,2,1) imshow(clip_0_1(2*y_deltas+0.5), "Horizontal Deltas: Original") plt.subplot(2,2,2) imshow(clip_0_1(2*x_deltas+0.5), "Vertical Deltas: Original") x_deltas, y_deltas = high_pass_x_y(image) plt.subplot(2,2,3) imshow(clip_0_1(2*y_deltas+0.5), "Horizontal Deltas: Styled") plt.subplot(2,2,4) imshow(clip_0_1(2*x_deltas+0.5), "Vertical Deltas: Styled")
これは高周波成分がどのように増加したかを示します。
また、この高周波成分は基本的にはエッジ検出器です。例えば、Sobel エッジ検出器から類似の出力を得ることができます :
plt.figure(figsize=(14,10)) sobel = tf.image.sobel_edges(content_image) plt.subplot(1,2,1) imshow(clip_0_1(sobel[...,0]/4+0.5), "Horizontal Sobel-edges") plt.subplot(1,2,2) imshow(clip_0_1(sobel[...,1]/4+0.5), "Vertical Sobel-edges")
これに関連する正則化損失は値の二乗の総計です :
def total_variation_loss(image): x_deltas, y_deltas = high_pass_x_y(image) return tf.reduce_mean(x_deltas**2) + tf.reduce_mean(y_deltas**2)
最適化を再実行する
total_variation_loss のための重みを選択します :
total_variation_weight=1e8
今はそれを train_step 関数に含めます :
@tf.function() def train_step(image): with tf.GradientTape() as tape: outputs = extractor(image) loss = style_content_loss(outputs) loss += total_variation_weight*total_variation_loss(image) grad = tape.gradient(loss, image) opt.apply_gradients([(grad, image)]) image.assign(clip_0_1(image))
最適化変数を再初期化します :
image = tf.Variable(content_image)
そして最適化を実行します :
import time start = time.time() epochs = 10 steps_per_epoch = 100 step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 train_step(image) print(".", end='') display.clear_output(wait=True) imshow(image.read_value()) plt.title("Train step: {}".format(step)) plt.show() end = time.time() print("Total time: {:.1f}".format(end-start))
Total time: 24.8
最後に、結果をセーブします :
file_name = 'kadinsky-turtle.png' mpl.image.imsave(file_name, image[0]) try: from google.colab import files except ImportError: pass else: files.download(file_name)
以上