DeepDream

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Ce didacticiel contient une implémentation minimale de DeepDream, comme décrit dans cet article de blog par Alexander Mordvintsev.

DeepDream est une expérience qui visualise les modèles appris par un réseau de neurones. Comme lorsqu'un enfant regarde des nuages ​​et essaie d'interpréter des formes aléatoires, DeepDream surinterprète et améliore les motifs qu'il voit dans une image.

Pour ce faire, il transmet une image à travers le réseau, puis calcule le gradient de l'image par rapport aux activations d'une couche particulière. L'image est ensuite modifiée pour augmenter ces activations, améliorant les motifs vus par le réseau et résultant en une image onirique. Ce processus a été surnommé "Inceptionism" (une référence à InceptionNet et au film Inception).

Démontrons comment vous pouvez faire « rêver » un réseau de neurones et améliorer les motifs surréalistes qu'il voit dans une image.

Dogception

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.Image

Choisissez une image pour rêver

Pour ce tutoriel, utilisons une image d'un labrador .

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

Préparer le modèle d'extraction de caractéristiques

Téléchargez et préparez un modèle de classification d'images pré-entraîné. Vous utiliserez InceptionV3 qui est similaire au modèle utilisé à l'origine dans DeepDream. Notez que tout modèle pré-formé fonctionnera, bien que vous deviez ajuster les noms de calque ci-dessous si vous modifiez cela.

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

L'idée dans DeepDream est de choisir un calque (ou des calques) et de maximiser la "perte" de manière à ce que l'image "excite" de plus en plus les calques. La complexité des fonctionnalités incorporées dépend des couches choisies par vous, c'est-à-dire que les couches inférieures produisent des traits ou des motifs simples, tandis que les couches plus profondes donnent des fonctionnalités sophistiquées dans les images, voire des objets entiers.

L'architecture InceptionV3 est assez volumineuse (pour un graphique de l'architecture du modèle, voir le dépôt de recherche de TensorFlow). Pour DeepDream, les couches d'intérêt sont celles où les convolutions sont concaténées. Il y a 11 de ces couches dans InceptionV3, nommées 'mixed0' bien que 'mixed10'. L'utilisation de différentes couches se traduira par différentes images oniriques. Les couches plus profondes répondent aux fonctionnalités de niveau supérieur (telles que les yeux et les visages), tandis que les couches précédentes répondent aux fonctionnalités plus simples (telles que les bords, les formes et les textures). N'hésitez pas à expérimenter avec les couches sélectionnées ci-dessous, mais gardez à l'esprit que les couches plus profondes (celles avec un indice plus élevé) prendront plus de temps à s'entraîner car le calcul du gradient est plus profond.

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

Calculer la perte

La perte est la somme des activations dans les couches choisies. La perte est normalisée à chaque couche afin que la contribution des couches plus grandes ne dépasse pas les couches plus petites. Normalement, la perte est une quantité que vous souhaitez minimiser via la descente de gradient. Dans DeepDream, vous maximiserez cette perte via l'ascension du gradient.

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)

Ascension en pente

Une fois que vous avez calculé la perte pour les calques choisis, il ne reste plus qu'à calculer les dégradés par rapport à l'image, et à les ajouter à l'image d'origine.

L'ajout de dégradés à l'image améliore les motifs vus par le réseau. A chaque étape, vous aurez créé une image qui excite de plus en plus les activations de certaines couches du réseau.

La méthode qui fait cela, ci-dessous, est enveloppée dans un tf.function pour les performances. Il utilise une input_signature pour s'assurer que la fonction n'est pas retracée pour différentes tailles d'image ou valeurs step_size steps Voir le guide des fonctions concrètes pour plus de détails.

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)

Boucle principale

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

Monter d'une octave

Assez bien, mais il y a quelques problèmes avec cette première tentative :

  1. La sortie est bruyante (cela pourrait être résolu avec une perte tf.image.total_variation ).
  2. L'image est en basse résolution.
  3. Les modèles apparaissent comme s'ils se produisaient tous avec la même granularité.

Une approche qui résout tous ces problèmes consiste à appliquer une ascension de gradient à différentes échelles. Cela permettra aux modèles générés à des échelles plus petites d'être incorporés dans des modèles à des échelles plus élevées et remplis avec des détails supplémentaires.

Pour ce faire, vous pouvez effectuer l'approche d'ascension de gradient précédente, puis augmenter la taille de l'image (appelée octave) et répéter ce processus pour plusieurs octaves.

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

Facultatif : mise à l'échelle avec des tuiles

Une chose à considérer est qu'à mesure que la taille de l'image augmente, le temps et la mémoire nécessaires pour effectuer le calcul du gradient augmentent également. L'implémentation d'octave ci-dessus ne fonctionnera pas sur de très grandes images ou sur de nombreuses octaves.

Pour éviter ce problème, vous pouvez diviser l'image en tuiles et calculer le dégradé pour chaque tuile.

L'application de décalages aléatoires à l'image avant chaque calcul en mosaïque empêche l'apparition de joints de mosaïque.

Commencez par implémenter le décalage aléatoire :

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

Voici un équivalent tuilé de la fonction deepdream définie précédemment :

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)

L'assemblage de tout cela donne une implémentation deepdream évolutive et compatible avec les octaves :

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

Bien mieux ! Jouez avec le nombre d'octaves, l'échelle d'octave et les calques activés pour modifier l'apparence de votre image DeepDream-ed.

Les lecteurs pourraient également être intéressés par TensorFlow Lucid qui développe les idées introduites dans ce didacticiel pour visualiser et interpréter les réseaux de neurones.