Есть вопрос? Присоединяйтесь к сообществу на форуме TensorFlow. Посетите форум.

DeepDream

Посмотреть на TensorFlow.org Запустить в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Это руководство содержит минимальную реализацию DeepDream, как описано в этом сообщении блога Александра Мордвинцева.

DeepDream - это эксперимент, который визуализирует закономерности, полученные нейронной сетью. Подобно тому, как ребенок наблюдает за облаками и пытается интерпретировать случайные формы, DeepDream переоценивает и усиливает узоры, которые он видит на изображении.

Это делается путем пересылки изображения по сети с последующим вычислением градиента изображения по отношению к активациям определенного слоя. Затем изображение модифицируется, чтобы усилить эти активации, усилить паттерны, видимые сетью, и в результате получить образ, похожий на сон. Этот процесс получил название «Inceptionism» (отсылка к InceptionNet и фильму « Начало»).

Давайте продемонстрируем, как сделать нейронную сеть «мечтой» и усилить сюрреалистические паттерны, которые она видит на изображении.

Dogception

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.Image

from tensorflow.keras.preprocessing import image

Выберите образ, чтобы мечтать

Для этого урока воспользуемся изображением лабрадора .

url = 'https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg'
# Download an image and read it into a NumPy array.
def download(url, max_dim=None):
  name = url.split('/')[-1]
  image_path = tf.keras.utils.get_file(name, origin=url)
  img = PIL.Image.open(image_path)
  if max_dim:
    img.thumbnail((max_dim, max_dim))
  return np.array(img)

# Normalize an image
def deprocess(img):
  img = 255*(img + 1.0)/2.0
  return tf.cast(img, tf.uint8)

# Display an image
def show(img):
  display.display(PIL.Image.fromarray(np.array(img)))


# Downsizing the image makes it easier to work with.
original_img = download(url, max_dim=500)
show(original_img)
display.display(display.HTML('Image cc-by: <a "href=https://commons.wikimedia.org/wiki/File:Felis_catus-cat_on_snow.jpg">Von.grzanka</a>'))
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg
90112/83281 [================================] - 0s 0us/step

PNG

Подготовьте модель извлечения признаков

Загрузите и подготовьте предварительно обученную модель классификации изображений. Вы будете использовать InceptionV3, который похож на модель, изначально использовавшуюся в DeepDream. Обратите внимание, что любая предварительно обученная модель будет работать, хотя вам придется изменить имена слоев ниже, если вы измените это.

base_model = tf.keras.applications.InceptionV3(include_top=False, weights='imagenet')

Идея DeepDream состоит в том, чтобы выбрать слой (или слои) и максимизировать «потери» таким образом, чтобы изображение все больше «возбуждало» слои. Сложность включенных функций зависит от выбранных вами слоев, т. Е. Нижние слои создают штрихи или простые узоры, а более глубокие слои придают сложные функции изображениям или даже целым объектам.

Архитектура InceptionV3 довольно велика (график архитектуры модели см. В исследовательском репозитории TensorFlow ). Для DeepDream интересными являются слои, в которых свертки объединены. В InceptionV3 есть 11 таких слоев, которые называются от «mixed0» до «mixed10». Использование разных слоев приведет к получению разных сказочных изображений. Более глубокие слои реагируют на элементы более высокого уровня (например, глаза и лица), тогда как более ранние слои реагируют на более простые элементы (такие как края, формы и текстуры). Не стесняйтесь экспериментировать со слоями, выбранными ниже, но имейте в виду, что более глубокие слои (с более высоким индексом) потребуют больше времени для обучения, поскольку вычисление градиента более глубокое.

# Maximize the activations of these layers
names = ['mixed3', 'mixed5']
layers = [base_model.get_layer(name).output for name in names]

# Create the feature extraction model
dream_model = tf.keras.Model(inputs=base_model.input, outputs=layers)

Рассчитать убыток

Убыток - это сумма активаций в выбранных слоях. Потери нормализованы на каждом слое, поэтому вклад более крупных слоев не перевешивает более мелкие слои. Обычно потери - это величина, которую вы хотите минимизировать с помощью градиентного спуска. В DeepDream вы максимизируете эту потерю с помощью градиентного подъема.

def calc_loss(img, model):
  # Pass forward the image through the model to retrieve the activations.
  # Converts the image into a batch of size 1.
  img_batch = tf.expand_dims(img, axis=0)
  layer_activations = model(img_batch)
  if len(layer_activations) == 1:
    layer_activations = [layer_activations]

  losses = []
  for act in layer_activations:
    loss = tf.math.reduce_mean(act)
    losses.append(loss)

  return  tf.reduce_sum(losses)

Градиентный подъем

После того, как вы рассчитали потери для выбранных слоев, все, что осталось, - это вычислить градиенты по отношению к изображению и добавить их к исходному изображению.

Добавление градиентов к изображению усиливает узоры, видимые сетью. На каждом этапе вы будете создавать изображение, которое все больше и больше возбуждает активацию определенных слоев в сети.

Метод, который делает это, ниже, для tf.function производительности заключен в tf.function . Он использует input_signature чтобы гарантировать, что функция не будет восстанавливаться для разных размеров изображения или значений steps / step_size . Подробную информацию см. В Руководстве по конкретным функциям .

class DeepDream(tf.Module):
  def __init__(self, model):
    self.model = model

  @tf.function(
      input_signature=(
        tf.TensorSpec(shape=[None,None,3], dtype=tf.float32),
        tf.TensorSpec(shape=[], dtype=tf.int32),
        tf.TensorSpec(shape=[], dtype=tf.float32),)
  )
  def __call__(self, img, steps, step_size):
      print("Tracing")
      loss = tf.constant(0.0)
      for n in tf.range(steps):
        with tf.GradientTape() as tape:
          # This needs gradients relative to `img`
          # `GradientTape` only watches `tf.Variable`s by default
          tape.watch(img)
          loss = calc_loss(img, self.model)

        # Calculate the gradient of the loss with respect to the pixels of the input image.
        gradients = tape.gradient(loss, img)

        # Normalize the gradients.
        gradients /= tf.math.reduce_std(gradients) + 1e-8 

        # In gradient ascent, the "loss" is maximized so that the input image increasingly "excites" the layers.
        # You can update the image by directly adding the gradients (because they're the same shape!)
        img = img + gradients*step_size
        img = tf.clip_by_value(img, -1, 1)

      return loss, img
deepdream = DeepDream(dream_model)

Основной цикл

def run_deep_dream_simple(img, steps=100, step_size=0.01):
  # Convert from uint8 to the range expected by the model.
  img = tf.keras.applications.inception_v3.preprocess_input(img)
  img = tf.convert_to_tensor(img)
  step_size = tf.convert_to_tensor(step_size)
  steps_remaining = steps
  step = 0
  while steps_remaining:
    if steps_remaining>100:
      run_steps = tf.constant(100)
    else:
      run_steps = tf.constant(steps_remaining)
    steps_remaining -= run_steps
    step += run_steps

    loss, img = deepdream(img, run_steps, tf.constant(step_size))

    display.clear_output(wait=True)
    show(deprocess(img))
    print ("Step {}, loss {}".format(step, loss))


  result = deprocess(img)
  display.clear_output(wait=True)
  show(result)

  return result
dream_img = run_deep_dream_simple(img=original_img, 
                                  steps=100, step_size=0.01)

PNG

Поднимая октаву

Довольно неплохо, но с этой первой попыткой есть несколько проблем:

  1. Вывод зашумлен (это можно tf.image.total_variation с помощью потери tf.image.total_variation ).
  2. Изображение низкого разрешения.
  3. Шаблоны выглядят так, как будто все они происходят с одинаковой степенью детализации.

Один из подходов, который решает все эти проблемы, - это применение градиентного подъема на разных уровнях. Это позволит объединить узоры, созданные в меньших масштабах, в шаблоны более высоких масштабов и заполнить их дополнительными деталями.

Для этого вы можете выполнить предыдущий подход градиентного подъема, затем увеличить размер изображения (который называется октавой) и повторить этот процесс для нескольких октав.

import time
start = time.time()

OCTAVE_SCALE = 1.30

img = tf.constant(np.array(original_img))
base_shape = tf.shape(img)[:-1]
float_base_shape = tf.cast(base_shape, tf.float32)

for n in range(-2, 3):
  new_shape = tf.cast(float_base_shape*(OCTAVE_SCALE**n), tf.int32)

  img = tf.image.resize(img, new_shape).numpy()

  img = run_deep_dream_simple(img=img, steps=50, step_size=0.01)

display.clear_output(wait=True)
img = tf.image.resize(img, base_shape)
img = tf.image.convert_image_dtype(img/255.0, dtype=tf.uint8)
show(img)

end = time.time()
end-start

PNG

7.397704601287842

Необязательно: масштабирование с помощью плиток

Следует учитывать, что по мере увеличения размера изображения увеличиваются время и память, необходимые для выполнения вычисления градиента. Вышеупомянутая реализация октав не будет работать с очень большими изображениями или многими октавами.

Чтобы избежать этой проблемы, вы можете разделить изображение на плитки и вычислить градиент для каждой плитки.

Применение случайных сдвигов к изображению перед каждым вычислением мозаики предотвращает появление стыков плитки.

Начнем с реализации случайного сдвига:

def random_roll(img, maxroll):
  # Randomly shift the image to avoid tiled boundaries.
  shift = tf.random.uniform(shape=[2], minval=-maxroll, maxval=maxroll, dtype=tf.int32)
  img_rolled = tf.roll(img, shift=shift, axis=[0,1])
  return shift, img_rolled
shift, img_rolled = random_roll(np.array(original_img), 512)
show(img_rolled)

PNG

Вот плиточный эквивалент функции deepdream определенной ранее:

class TiledGradients(tf.Module):
  def __init__(self, model):
    self.model = model

  @tf.function(
      input_signature=(
        tf.TensorSpec(shape=[None,None,3], dtype=tf.float32),
        tf.TensorSpec(shape=[], dtype=tf.int32),)
  )
  def __call__(self, img, tile_size=512):
    shift, img_rolled = random_roll(img, tile_size)

    # Initialize the image gradients to zero.
    gradients = tf.zeros_like(img_rolled)

    # Skip the last tile, unless there's only one tile.
    xs = tf.range(0, img_rolled.shape[0], tile_size)[:-1]
    if not tf.cast(len(xs), bool):
      xs = tf.constant([0])
    ys = tf.range(0, img_rolled.shape[1], tile_size)[:-1]
    if not tf.cast(len(ys), bool):
      ys = tf.constant([0])

    for x in xs:
      for y in ys:
        # Calculate the gradients for this tile.
        with tf.GradientTape() as tape:
          # This needs gradients relative to `img_rolled`.
          # `GradientTape` only watches `tf.Variable`s by default.
          tape.watch(img_rolled)

          # Extract a tile out of the image.
          img_tile = img_rolled[x:x+tile_size, y:y+tile_size]
          loss = calc_loss(img_tile, self.model)

        # Update the image gradients for this tile.
        gradients = gradients + tape.gradient(loss, img_rolled)

    # Undo the random shift applied to the image and its gradients.
    gradients = tf.roll(gradients, shift=-shift, axis=[0,1])

    # Normalize the gradients.
    gradients /= tf.math.reduce_std(gradients) + 1e-8 

    return gradients
get_tiled_gradients = TiledGradients(dream_model)

Объединение всего этого дает масштабируемую реализацию deepdream с учетом октавы:

def run_deep_dream_with_octaves(img, steps_per_octave=100, step_size=0.01, 
                                octaves=range(-2,3), octave_scale=1.3):
  base_shape = tf.shape(img)
  img = tf.keras.preprocessing.image.img_to_array(img)
  img = tf.keras.applications.inception_v3.preprocess_input(img)

  initial_shape = img.shape[:-1]
  img = tf.image.resize(img, initial_shape)
  for octave in octaves:
    # Scale the image based on the octave
    new_size = tf.cast(tf.convert_to_tensor(base_shape[:-1]), tf.float32)*(octave_scale**octave)
    img = tf.image.resize(img, tf.cast(new_size, tf.int32))

    for step in range(steps_per_octave):
      gradients = get_tiled_gradients(img)
      img = img + gradients*step_size
      img = tf.clip_by_value(img, -1, 1)

      if step % 10 == 0:
        display.clear_output(wait=True)
        show(deprocess(img))
        print ("Octave {}, Step {}".format(octave, step))

  result = deprocess(img)
  return result
img = run_deep_dream_with_octaves(img=original_img, step_size=0.01)

display.clear_output(wait=True)
img = tf.image.resize(img, base_shape)
img = tf.image.convert_image_dtype(img/255.0, dtype=tf.uint8)
show(img)

PNG

Намного лучше! Поиграйте с количеством октав, октавной шкалой и активированными слоями, чтобы изменить внешний вид вашего изображения в DeepDream.

Читатели также могут быть заинтересованы в TensorFlow Lucid, который расширяет идеи, представленные в этом руководстве, для визуализации и интерпретации нейронных сетей.