Se usó la API de Cloud Translation para traducir esta página.
Switch to English

DeepDream

Ver en TensorFlow.org Ejecutar en Google Colab Ver código fuente en GitHub Descargar cuaderno

Este tutorial contiene una implementación mínima de DeepDream, como se describe en esta publicación de blog de Alexander Mordvintsev.

DeepDream es un experimento que visualiza los patrones aprendidos por una red neuronal. Al igual que cuando un niño mira nubes e intenta interpretar formas aleatorias, DeepDream sobre interpreta y mejora los patrones que ve en una imagen.

Lo hace reenviando una imagen a través de la red, luego calculando el gradiente de la imagen con respecto a las activaciones de una capa en particular. La imagen se modifica para aumentar estas activaciones, mejorar los patrones vistos por la red y dar como resultado una imagen de ensueño. Este proceso se denominó "Inceptionism" (una referencia a InceptionNet y la película Inception).

Demostremos cómo puede hacer que una red neuronal "sueñe" y mejorar los patrones surrealistas que ve en una imagen.

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
 

Elige una imagen para soñar

Para este tutorial, usemos una imagen de 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

Prepare el modelo de extracción de características

Descargue y prepare un modelo de clasificación de imágenes previamente entrenado. Utilizará InceptionV3, que es similar al modelo utilizado originalmente en DeepDream. Tenga en cuenta que cualquier modelo previamente entrenado funcionará, aunque tendrá que ajustar los nombres de capa a continuación si cambia esto.

 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 [==============================] - 4s 0us/step

La idea en DeepDream es elegir una capa (o capas) y maximizar la "pérdida" de manera que la imagen "excite" cada vez más las capas. La complejidad de las características incorporadas depende de las capas elegidas por usted, es decir, las capas inferiores producen trazos o patrones simples, mientras que las capas más profundas proporcionan características sofisticadas en las imágenes, o incluso en objetos completos.

La arquitectura InceptionV3 es bastante grande (para un gráfico de la arquitectura del modelo, consulte el repositorio de investigación de TensorFlow). Para DeepDream, las capas de interés son aquellas donde las convoluciones se concatenan. Hay 11 de estas capas en InceptionV3, llamadas 'mixed0' aunque 'mixed10'. El uso de diferentes capas dará como resultado diferentes imágenes de ensueño. Las capas más profundas responden a características de nivel superior (como ojos y caras), mientras que las capas anteriores responden a características más simples (como bordes, formas y texturas). Siéntase libre de experimentar con las capas seleccionadas a continuación, pero tenga en cuenta que las capas más profundas (aquellas con un índice más alto) tomarán más tiempo en entrenarse ya que el cálculo del gradiente es más profundo.

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

Calcular la pérdida

La pérdida es la suma de las activaciones en las capas elegidas. La pérdida se normaliza en cada capa, por lo que la contribución de las capas más grandes no supera las capas más pequeñas. Normalmente, la pérdida es una cantidad que desea minimizar a través del descenso de gradiente. En DeepDream, maximizará esta pérdida a través del ascenso en gradiente.

 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)
 

Ascenso gradiente

Una vez que haya calculado la pérdida para las capas elegidas, todo lo que queda es calcular los gradientes con respecto a la imagen y agregarlos a la imagen original.

Agregar los degradados a la imagen mejora los patrones vistos por la red. En cada paso, habrá creado una imagen que excita cada vez más las activaciones de ciertas capas en la red.

El método que hace esto, a continuación, está envuelto en una función tf.function para el rendimiento. Utiliza un input_signature para asegurar que la función no se volvió sobre los diferentes tamaños de las imágenes o steps / step_size valores. Consulte la guía de funciones concretas para más detalles.

 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)
 

Bucle principal

 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

Tomando una octava

Bastante bien, pero hay algunos problemas con este primer intento:

  1. La salida es ruidosa (esto podría solucionarse con una pérdida tf.image.total_variation ).
  2. La imagen es de baja resolución.
  3. Los patrones parecen estar sucediendo con la misma granularidad.

Un enfoque que aborda todos estos problemas es aplicar el ascenso en gradiente a diferentes escalas. Esto permitirá que los patrones generados a escalas más pequeñas se incorporen a patrones a escalas más altas y se completen con detalles adicionales.

Para hacer esto, puede realizar el enfoque de ascenso de gradiente anterior, luego aumentar el tamaño de la imagen (que se conoce como una octava) y repetir este proceso para varias octavas.

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

Opcional: ampliación con mosaicos

Una cosa a tener en cuenta es que a medida que la imagen aumenta de tamaño, también lo hará el tiempo y la memoria necesarios para realizar el cálculo del gradiente. La implementación de octava anterior no funcionará en imágenes muy grandes o en muchas octavas.

Para evitar este problema, puede dividir la imagen en mosaicos y calcular el gradiente de cada mosaico.

La aplicación de cambios aleatorios a la imagen antes de cada cálculo en mosaico evita que aparezcan costuras de mosaico.

Comience implementando el cambio aleatorio:

 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)
  shift_down, shift_right = shift[0],shift[1] 
  img_rolled = tf.roll(tf.roll(img, shift_right, axis=1), shift_down, axis=0)
  return shift_down, shift_right, img_rolled
 
 shift_down, shift_right, img_rolled = random_roll(np.array(original_img), 512)
show(img_rolled)
 

png

Aquí hay un equivalente en mosaico de la función deepdream definida anteriormente:

 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_down, shift_right, 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(tf.roll(gradients, -shift_right, axis=1), -shift_down, axis=0)

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

    return gradients 
 
 get_tiled_gradients = TiledGradients(dream_model)
 

Al juntar esto, se obtiene una implementación de deepdream escalable y con reconocimiento de octava:

 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

¡Mucho mejor! Juega con el número de octavas, la escala de octava y las capas activadas para cambiar la apariencia de tu imagen DeepDream.

Los lectores también podrían estar interesados ​​en TensorFlow Lucid, que amplía las ideas presentadas en este tutorial para visualizar e interpretar redes neuronales.