目次
Style Transferとは
Style Transfer(スタイル転送)とは、ある画像の「スタイル」(例:色、質感、パターン)を別の画像の「内容」(形状、構図)に適用する技術です。これにより、元の内容を保持しつつ、別の画像のアートスタイルを適用した新しい画像を生成できます。
この技術は主にディープラーニングを用いたアルゴリズムによって実現され、特にコンボリューショナル・ニューラル・ネットワーク(CNN)を利用します。代表的な例として、ある風景写真にゴッホやモネの絵画のスタイルを適用し、その風景がまるでその画家が描いたかのように見えるようにすることが挙げられます
アーキテクチャ
プロセスの流れ
- 内容画像、スタイル画像、生成画像の3つの画像を用意します。
- VGGネットワークを使用して、内容画像とスタイル画像から特徴マップを抽出します。
- 内容損失とスタイル損失を定義し、総損失を計算します。
- 生成画像のピクセル値を最適化して、損失関数を最小化します。
- 最適化が完了すると、生成画像が内容画像の構図を保ちつつ、スタイル画像のスタイルを適用したものになります。
アーキテクチャの概要
ネットワークの選定
- VGGネットワーク(例:VGG-19)は、事前にImageNetデータセットでトレーニングされており、さまざまなレベルの抽象化を持つ特徴マップを提供します。このネットワークを使用して、内容画像とスタイル画像から特徴を抽出します。
特徴抽出
- 内容特徴(Content Features): 内容画像をVGGネットワークに入力し、中間層(通常は高次層、例:
conv4_2
)から特徴マップを取得します。これが内容画像の特徴となります。 - スタイル特徴(Style Features): スタイル画像を同じVGGネットワークに入力し、複数の中間層(例:
conv1_1
,conv2_1
,conv3_1
,conv4_1
,conv5_1
)から特徴マップを取得します。これらの特徴マップを用いてスタイルを表現します。
損失関数の設計
- 内容損失(Content Loss): 生成画像の特徴マップと内容画像の特徴マップの差異を計算します。通常、二乗誤差(Mean Squared Error, MSE)を使用します。
- スタイル損失(Style Loss): スタイル損失は、個々の層でカーネルを適用した出力結果(特徴マップ)についてそれぞれの間で相関を取ることで、特徴ごとの類似度、つまりその画像らしさを表した表現を使います。相関を取るには2つの特徴マップ間で内積を取ることになりますが、これはグラム行列と呼ばれます。つまり、生成画像の特徴マップとスタイル画像の特徴マップからグラム行列(Gram Matrix)を計算し、その差異を計算します。
ここで、G:生成画像のグラム行列、A:スタイル画像のグラム行列です。 - 総損失(Total Loss): 内容損失とスタイル損失を加重平均して、総損失を計算します。
ここで、αとβはそれぞれ内容損失とスタイル損失の重みを調整するハイパーパラメータです。
あとは層ごとの損失の線形和をスタイルの合計損失とします
最適化
- 生成画像をランダムなノイズ画像として初期化し、損失関数を最小化するように最適化を行います。通常、勾配降下法(Gradient Descent)やその変種(例:L-BFGS)を使用します。
実行結果
では実行結果を下に示す。Contentsが元画像で、Styleがどのような作風にしたいか、最終結果をResultsの欄にGifで学習している過程を示しました。いくつかの事例を見てみよう。
元画像は、以下の画像を用いている。
Parameterは、
- total_variation_weight: 1e-6, style_weight: 1e-6, content_weight: 2.5e-8 (一番上の画像)
- 元画像の影響(重み)を小さくしてStyleの重みを大きくした
total_variation_weight: 5e-6, style_weight: 4.5e-6, content_weight:0.1e-8
を用いている。
Style | Results |
---|---|
上と同じ | |
ソースコード
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.applications import vgg19
import numpy as np
base_image_path = keras.utils.get_file("paris.jpg", "https://i.imgur.com/F28w3Ac.jpg")
style_reference_image_path = keras.utils.get_file("test1.jpg", "https://storage.googleapis.com/download.tensorflow.org/example_images/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg")
#style_reference_image_path = keras.utils.get_file("monet.jpg", "https://i.imgur.com/M9C5Mil.jpeg")
#style_reference_image_path = keras.utils.get_file("scream.jpg", "https://i.imgur.com/90Mk6XX.jpeg")
#style_reference_image_path = keras.utils.get_file("starry_night.jpg", "https://i.imgur.com/9ooB60I.jpg")
result_prefix = "paris_generated"
# Weights of the different loss components
total_variation_weight = 1e-6
style_weight = 1e-6
content_weight = 2.5e-8
# Dimensions of the generated picture.
width, height = keras.preprocessing.image.load_img(base_image_path).size
img_nrows = 400
img_ncols = int(width * img_nrows / height)
from IPython.display import Image, display
display(Image(base_image_path))
display(Image(style_reference_image_path))
def preprocess_image(image_path):
# Util function to open, resize and format pictures into appropriate tensors
img = keras.preprocessing.image.load_img(
image_path, target_size=(img_nrows, img_ncols)
)
img = keras.preprocessing.image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = vgg19.preprocess_input(img)
return tf.convert_to_tensor(img)
def deprocess_image(x):
# Util function to convert a tensor into a valid image
x = x.reshape((img_nrows, img_ncols, 3))
# Remove zero-center by mean pixel
x[:, :, 0] += 103.939
x[:, :, 1] += 116.779
x[:, :, 2] += 123.68
# 'BGR'->'RGB'
x = x[:, :, ::-1]
x = np.clip(x, 0, 255).astype("uint8")
return x
# The gram matrix of an image tensor (feature-wise outer product)
def gram_matrix(x):
x = tf.transpose(x, (2, 0, 1))
features = tf.reshape(x, (tf.shape(x)[0], -1))
gram = tf.matmul(features, tf.transpose(features))
return gram
# The "style loss" is designed to maintain
# the style of the reference image in the generated image.
# It is based on the gram matrices (which capture style) of
# feature maps from the style reference image
# and from the generated image
def style_loss(style, combination):
S = gram_matrix(style)
C = gram_matrix(combination)
channels = 3
size = img_nrows * img_ncols
return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))
# An auxiliary loss function
# designed to maintain the "content" of the
# base image in the generated image
def content_loss(base, combination):
return tf.reduce_sum(tf.square(combination - base))
# The 3rd loss function, total variation loss,
# designed to keep the generated image locally coherent
def total_variation_loss(x):
a = tf.square(
x[:, : img_nrows - 1, : img_ncols - 1, :] - x[:, 1:, : img_ncols - 1, :]
)
b = tf.square(
x[:, : img_nrows - 1, : img_ncols - 1, :] - x[:, : img_nrows - 1, 1:, :]
)
return tf.reduce_sum(tf.pow(a + b, 1.25))
# Build a VGG19 model loaded with pre-trained ImageNet weights
model = vgg19.VGG19(weights="imagenet", include_top=False)
# Get the symbolic outputs of each "key" layer (we gave them unique names).
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers])
# Set up a model that returns the activation values for every layer in
# VGG19 (as a dict).
feature_extractor = keras.Model(inputs=model.inputs, outputs=outputs_dict)
# List of layers to use for the style loss.
style_layer_names = [
"block1_conv1",
"block2_conv1",
"block3_conv1",
"block4_conv1",
"block5_conv1",
]
# The layer to use for the content loss.
content_layer_name = "block5_conv2"
def compute_loss(combination_image, base_image, style_reference_image):
input_tensor = tf.concat(
[base_image, style_reference_image, combination_image], axis=0
)
features = feature_extractor(input_tensor)
# Initialize the loss
loss = tf.zeros(shape=())
# Add content loss
layer_features = features[content_layer_name]
base_image_features = layer_features[0, :, :, :]
combination_features = layer_features[2, :, :, :]
loss = loss + content_weight * content_loss(
base_image_features, combination_features
)
# Add style loss
for layer_name in style_layer_names:
layer_features = features[layer_name]
style_reference_features = layer_features[1, :, :, :]
combination_features = layer_features[2, :, :, :]
sl = style_loss(style_reference_features, combination_features)
loss += (style_weight / len(style_layer_names)) * sl
# Add total variation loss
loss += total_variation_weight * total_variation_loss(combination_image)
return loss
@tf.function
def compute_loss_and_grads(combination_image, base_image, style_reference_image):
with tf.GradientTape() as tape:
loss = compute_loss(combination_image, base_image, style_reference_image)
grads = tape.gradient(loss, combination_image)
return loss, grads
optimizer = keras.optimizers.SGD(
keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate=100.0, decay_steps=100, decay_rate=0.96
)
)
base_image = preprocess_image(base_image_path)
style_reference_image = preprocess_image(style_reference_image_path)
combination_image = tf.Variable(preprocess_image(base_image_path))
iterations = 4000
for i in range(1, iterations + 1):
loss, grads = compute_loss_and_grads(
combination_image, base_image, style_reference_image
)
optimizer.apply_gradients([(grads, combination_image)])
if i % 100 == 0:
print("Iteration %d: loss=%.2f" % (i, loss))
img = deprocess_image(combination_image.numpy())
fname = result_prefix + "_at_iteration_%d.png" % i
keras.preprocessing.image.save_img(fname, img)
display(Image(result_prefix + "_at_iteration_4000.png"))
コメント