نقل النمط العصبي

عرض على TensorFlow.org تشغيل في Google Colab عرض على جيثب تحميل دفتر انظر نموذج TF Hub

يستخدم هذا البرنامج التعليمي التعلم العميق لتكوين صورة واحدة بأسلوب صورة أخرى (هل تتمنى أن ترسم مثل بيكاسو أو فان جوخ؟). يُعرف هذا باسم نقل النمط العصبي وقد تم تحديد التقنية في A Neural Algorithm of Artistic Style (Gatys et al.).

للحصول على تطبيق بسيط لنقل النمط ، راجع هذا البرنامج التعليمي لمعرفة المزيد حول كيفية استخدام نموذج Stylization الصورة التعسفي الذي تم اختباره مسبقًا من TensorFlow Hub أو كيفية استخدام نموذج نقل النمط مع TensorFlow Lite .

يُعد نقل النمط العصبي تقنية تحسين تُستخدم لالتقاط صورتين - صورة محتوى وصورة مرجعية للنمط (مثل عمل فني لرسام مشهور) - ودمجهما معًا بحيث تبدو الصورة الناتجة مثل صورة المحتوى ، ولكن "مرسومة" في نمط الصورة المرجعية للنمط.

يتم تنفيذ ذلك عن طريق تحسين صورة الإخراج لمطابقة إحصائيات محتوى صورة المحتوى وإحصائيات النمط للصورة المرجعية للنمط. يتم استخراج هذه الإحصائيات من الصور باستخدام شبكة تلافيفية.

على سبيل المثال ، لنأخذ صورة لهذا الكلب والتكوين 7 لفاسيلي كاندينسكي:

Yellow Labrador looking ، من ويكيميديا ​​كومنز بواسطة 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')

بي إن جي

نقل سريع للنمط باستخدام 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)

بي إن جي

تحديد تمثيلات المحتوى والأسلوب

استخدم الطبقات الوسيطة للنموذج للحصول على المحتوى وتمثيل النمط للصورة. بدءًا من طبقة إدخال الشبكة ، تمثل عمليات تنشيط الطبقة القليلة الأولى ميزات منخفضة المستوى مثل الحواف والأنسجة. أثناء التنقل عبر الشبكة ، تمثل الطبقات القليلة الأخيرة ميزات ذات مستوى أعلى - أجزاء كائن مثل العجلات أو العيون . في هذه الحالة ، فأنت تستخدم بنية شبكة 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 بحيث يمكنك بسهولة استخراج قيم الطبقة المتوسطة باستخدام واجهة برمجة تطبيقات Keras الوظيفية.

لتحديد نموذج باستخدام واجهة برمجة التطبيقات الوظيفية ، حدد المدخلات والمخرجات:

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_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 باستخدام صورة المحتوى (يجب أن يكون متغير tf بنفس شكل صورة المحتوى):

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)

بي إن جي

نظرًا لأنه يعمل ، قم بإجراء تحسين أطول:

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

بي إن جي

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

بي إن جي

يوضح هذا كيف زادت مكونات التردد العالي.

أيضًا ، هذا المكون عالي التردد هو في الأساس كاشف حافة. يمكنك الحصول على مخرجات مماثلة من كاشف الحواف 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_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))

بي إن جي

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 .