انتقال سبک عصبی

مشاهده در TensorFlow.org در Google Colab اجرا شود در GitHub مشاهده کنید دانلود دفترچه یادداشت مدل TF Hub را ببینید

این آموزش از یادگیری عمیق برای نوشتن یک تصویر به سبک تصویر دیگر استفاده می کند (تا حالا ای کاش می توانستید مانند پیکاسو یا ون گوگ نقاشی کنید؟). این به عنوان انتقال سبک عصبی شناخته می شود و این تکنیک در الگوریتم عصبی سبک هنری (گیتس و همکاران) تشریح شده است.

برای استفاده ساده از انتقال سبک، این آموزش را بررسی کنید تا در مورد نحوه استفاده از مدل سبک‌سازی تصویر دلخواه از TensorFlow Hub یا نحوه استفاده از مدل انتقال سبک با TensorFlow Lite بیشتر بدانید.

انتقال سبک عصبی یک تکنیک بهینه‌سازی است که برای گرفتن دو تصویر - یک تصویر محتوا و یک تصویر مرجع سبک (مانند یک اثر هنری توسط یک نقاش معروف) - و ترکیب آنها با هم استفاده می‌شود تا تصویر خروجی شبیه تصویر محتوا به نظر برسد، اما "نقاشی" شده است. به سبک تصویر مرجع سبک.

این امر با بهینه سازی تصویر خروجی برای مطابقت با آمار محتوای تصویر محتوا و آمار سبک تصویر مرجع سبک اجرا می شود. این آمار با استفاده از یک شبکه کانولوشن از تصاویر استخراج شده است.

برای مثال، بیایید تصویری از این سگ و ترکیب 7 واسیلی کاندینسکی بگیریم:

لابرادور زرد به دنبال , از Wikimedia Commons توسط Elf . مجوز CC BY-SA 3.0

حالا اگر کاندینسکی تصمیم بگیرد تصویر این سگ را منحصراً با این سبک نقاشی کند چگونه به نظر می رسد؟ چیزی مثل این؟

برپایی

وارد کردن و پیکربندی ماژول ها

import os
import tensorflow as tf
# Load compressed models from tensorflow_hub
os.environ['TFHUB_MODEL_LOAD_FORMAT'] = 'COMPRESSED'
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 PIL.Image
import time
import functools
def tensor_to_image(tensor):
  tensor = tensor*255
  tensor = np.array(tensor, dtype=np.uint8)
  if np.ndim(tensor)>3:
    assert tensor.shape[0] == 1
    tensor = tensor[0]
  return PIL.Image.fromarray(tensor)

تصاویر را دانلود کنید و یک تصویر سبک و یک تصویر محتوا را انتخاب کنید:

content_path = tf.keras.utils.get_file('YellowLabradorLooking_new.jpg', 'https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg')
style_path = tf.keras.utils.get_file('kandinsky5.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/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg
196608/195196 [==============================] - 0s 0us/step
204800/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')

png

انتقال سریع سبک با استفاده از TF-Hub

این آموزش الگوریتم اصلی انتقال سبک را نشان می دهد که محتوای تصویر را به یک سبک خاص بهینه می کند. قبل از پرداختن به جزئیات، بیایید ببینیم که چگونه مدل TensorFlow Hub این کار را انجام می دهد:

import tensorflow_hub as hub
hub_model = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')
stylized_image = hub_model(tf.constant(content_image), tf.constant(style_image))[0]
tensor_to_image(stylized_image)

png

بازنمایی محتوا و سبک را تعریف کنید

از لایه های میانی مدل برای بدست آوردن نمایش محتوا و سبک تصویر استفاده کنید. با شروع از لایه ورودی شبکه، چند فعال سازی لایه اول نشان دهنده ویژگی های سطح پایین مانند لبه ها و بافت ها هستند. همانطور که در شبکه قدم می گذارید، چند لایه نهایی نمایانگر ویژگی های سطح بالاتر هستند - بخش های شی مانند چرخ ها یا چشم ها . در این مورد، شما از معماری شبکه 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
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels.h5
574717952/574710816 [==============================] - 17s 0us/step
574726144/574710816 [==============================] - 17s 0us/step
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]
[('Labrador_retriever', 0.493171),
 ('golden_retriever', 0.2366529),
 ('kuvasz', 0.036357544),
 ('Chesapeake_Bay_retriever', 0.024182785),
 ('Greater_Swiss_Mountain_dog', 0.0186461)]

اکنون یک VGG19 را بدون سر طبقه بندی بارگذاری کنید و نام لایه ها را فهرست کنید

vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')

print()
for layer in vgg.layers:
  print(layer.name)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5
80142336/80134624 [==============================] - 2s 0us/step
80150528/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_layers = ['block5_conv2'] 

style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1', 
                'block4_conv1', 
                'block5_conv1']

num_content_layers = len(content_layers)
num_style_layers = len(style_layers)

لایه های میانی برای سبک و محتوا

پس چرا این خروجی‌های میانی در شبکه طبقه‌بندی تصویر از پیش آموزش‌دیده ما به ما اجازه می‌دهند تا بازنمایی‌های سبک و محتوا را تعریف کنیم؟

در سطح بالا، برای اینکه یک شبکه بتواند طبقه بندی تصاویر را انجام دهد (که این شبکه برای انجام آن آموزش دیده است)، باید تصویر را درک کند. این امر مستلزم گرفتن تصویر خام به عنوان پیکسل های ورودی و ساختن یک نمایش داخلی است که پیکسل های تصویر خام را به درک پیچیده ای از ویژگی های موجود در تصویر تبدیل می کند.

همچنین این دلیلی است که چرا شبکه‌های عصبی کانولوشنال قادر به تعمیم خوبی هستند: آنها می‌توانند تغییرناپذیری‌ها و ویژگی‌های تعریف‌کننده در کلاس‌ها (مثلاً گربه‌ها در مقابل سگ) را که نسبت به نویز پس‌زمینه و سایر مزاحمت‌ها آگنوستیک هستند، ثبت کنند. بنابراین، جایی بین جایی که تصویر خام به مدل وارد می شود و برچسب طبقه بندی خروجی، مدل به عنوان یک استخراج کننده ویژگی پیچیده عمل می کند. با دسترسی به لایه‌های میانی مدل، می‌توانید محتوا و سبک تصاویر ورودی را توصیف کنید.

مدل را بسازید

شبکه های موجود در tf.keras.applications طوری طراحی شده اند که بتوانید مقادیر لایه میانی را با استفاده از API عملکردی Keras به راحتی استخراج کنید.

برای تعریف یک مدل با استفاده از 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

محاسبه سبک

محتوای یک تصویر با مقادیر نقشه های ویژگی میانی نشان داده می شود.

به نظر می رسد، سبک یک تصویر را می توان با ابزارها و همبستگی ها در بین نقشه های ویژگی های مختلف توصیف کرد. ماتریس گرمی را که شامل این اطلاعات است، با گرفتن حاصل ضرب بیرونی بردار ویژگی با خود در هر مکان، و میانگین گرفتن آن محصول بیرونی در همه مکان‌ها، محاسبه کنید. این ماتریس گرم را می توان برای یک لایه خاص به صورت زیر محاسبه کرد:

\[G^l_{cd} = \frac{\sum_{ij} F^l_{ijc}(x)F^l_{ijd}(x)}{IJ}\]

این را می توان به طور خلاصه با استفاده از تابع 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)

سبک و محتوا را استخراج کنید

مدلی بسازید که سبک و تانسور محتوا را برمی گرداند.

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 و محتوای لایه‌های content را content_layers :

extractor = StyleContentModel(style_layers, content_layers)

results = extractor(tf.constant(content_image))

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.0055228462
    max:  28014.557
    mean:  263.79022

   block2_conv1
    shape:  (1, 128, 128)
    min:  0.0
    max:  61479.496
    mean:  9100.949

   block3_conv1
    shape:  (1, 256, 256)
    min:  0.0
    max:  545623.44
    mean:  7660.976

   block4_conv1
    shape:  (1, 512, 512)
    min:  0.0
    max:  4320502.0
    mean:  134288.84

   block5_conv1
    shape:  (1, 512, 512)
    min:  0.0
    max:  110005.37
    mean:  1487.0378

Contents:
   block5_conv2
    shape:  (1, 26, 32, 512)
    min:  0.0
    max:  2410.8796
    mean:  13.764149

نزول گرادیان را اجرا کنید

با این استخراج کننده سبک و محتوا، اکنون می توانید الگوریتم انتقال سبک را پیاده سازی کنید. این کار را با محاسبه میانگین مربع خطا برای خروجی تصویر نسبت به هر هدف انجام دهید، سپس مجموع وزنی این تلفات را بگیرید.

مقادیر هدف سبک و محتوای خود را تنظیم کنید:

style_targets = extractor(style_image)['style']
content_targets = extractor(content_image)['content']

یک tf.Variable برای بهینه سازی تصویر تعریف کنید. برای انجام این کار سریع، آن را با تصویر محتوا مقداردهی اولیه کنید ( tf.Variable باید همان شکل تصویر محتوا باشد):

image = tf.Variable(content_image)

از آنجایی که این یک تصویر شناور است، تابعی را برای حفظ مقادیر پیکسل بین 0 و 1 تعریف کنید:

def clip_0_1(image):
  return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)

یک بهینه ساز ایجاد کنید. مقاله LBFGS را توصیه می کند، اما Adam نیز خوب کار می کند:

opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)

برای بهینه سازی این، از ترکیب وزنی از دو ضرر استفاده کنید تا کل ضرر را بدست آورید:

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)
tensor_to_image(image)

png

از آنجایی که کار می کند، بهینه سازی طولانی تری انجام دهید:

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='', flush=True)
  display.clear_output(wait=True)
  display.display(tensor_to_image(image))
  print("Train step: {}".format(step))

end = time.time()
print("Total time: {:.1f}".format(end-start))

png

Train step: 1000
Total time: 21.3

از دست دادن کل تغییرات

یکی از نکات منفی این پیاده سازی اساسی این است که مصنوعات فرکانس بالایی تولید می کند. این موارد را با استفاده از یک عبارت منظم سازی صریح در اجزای فرکانس بالای تصویر کاهش دهید. در انتقال سبک، اغلب به این افت تغییرات کل گفته می شود:

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")

png

این نشان می دهد که چگونه مولفه های فرکانس بالا افزایش یافته اند.

همچنین، این جزء فرکانس بالا اساسا یک آشکارساز لبه است. می توانید خروجی مشابهی را از آشکارساز لبه 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")

png

از دست دادن منظم مرتبط با این مجموع مجذور مقادیر است:

def total_variation_loss(image):
  x_deltas, y_deltas = high_pass_x_y(image)
  return tf.reduce_sum(tf.abs(x_deltas)) + tf.reduce_sum(tf.abs(y_deltas))
total_variation_loss(image).numpy()
149402.94

این نشان داد که چه کاری انجام می دهد. اما نیازی نیست خودتان آن را پیاده سازی کنید، TensorFlow شامل یک پیاده سازی استاندارد است:

tf.image.total_variation(image).numpy()
array([149402.94], dtype=float32)

بهینه سازی را دوباره اجرا کنید

وزنی را برای total_variation_loss انتخاب کنید:

total_variation_weight=30

اکنون آن را در تابع 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*tf.image.total_variation(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='', flush=True)
  display.clear_output(wait=True)
  display.display(tensor_to_image(image))
  print("Train step: {}".format(step))

end = time.time()
print("Total time: {:.1f}".format(end-start))

png

Train step: 1000
Total time: 22.4

در نهایت، نتیجه را ذخیره کنید:

file_name = 'stylized-image.png'
tensor_to_image(image).save(file_name)

try:
  from google.colab import files
except ImportError:
   pass
else:
  files.download(file_name)

بیشتر بدانید

این آموزش الگوریتم اصلی انتقال سبک را نشان می دهد. برای کاربرد ساده انتقال سبک، این آموزش را بررسی کنید تا درباره نحوه استفاده از مدل انتقال سبک تصویر دلخواه از TensorFlow Hub بیشتر بدانید.