Глубокий сон

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

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

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

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

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

Догцепция

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.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>'))

png

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

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

base_model = tf.keras.applications.InceptionV3(include_top=False, weights='imagenet')
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/inception_v3/inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5
87916544/87910968 [==============================] - 0s 0us/step
87924736/87910968 [==============================] - 0s 0us/step

Идея 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 . Он использует input_signature , чтобы гарантировать, что функция не повторяется для разных размеров изображения или значений steps / step_size . Подробнее см. в руководстве по функциям Concrete .

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

6.38355278968811

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

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

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

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

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

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=[2], dtype=tf.int32),
        tf.TensorSpec(shape=[], dtype=tf.int32),)
  )
  def __call__(self, img, img_size, 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_size[1], tile_size)[:-1]
    if not tf.cast(len(xs), bool):
      xs = tf.constant([0])
    ys = tf.range(0, img_size[0], 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[y:y+tile_size, x:x+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.utils.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)
    new_size = tf.cast(new_size, tf.int32)
    img = tf.image.resize(img, new_size)

    for step in range(steps_per_octave):
      gradients = get_tiled_gradients(img, new_size)
      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 , который расширяет идеи, представленные в этом руководстве, для визуализации и интерпретации нейронных сетей.