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 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 sobreinterpreta y mejora los patrones que ve en una imagen.

Lo hace enviando una imagen a través de la red y luego calculando el gradiente de la imagen con respecto a las activaciones de una capa en particular. Luego, la imagen se modifica para aumentar estas activaciones, mejorando los patrones vistos por la red y dando 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 las capas 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 [==============================] - 2s 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 a 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 brindan características sofisticadas en imágenes o incluso objetos completos.

La arquitectura de InceptionV3 es bastante grande (para ver 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 en las que se concatenan las convoluciones. 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) tardarán más 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 para que la contribución de las capas más grandes no supere a 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 en pendiente

Una vez que hayas calculado la pérdida de las capas elegidas, solo queda calcular los degradados con respecto a la imagen y agregarlos a la imagen original.

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

El método que hace esto, a continuación, está envuelto en una función tf.function para el rendimiento. Utiliza una input_signature para garantizar que la función no se input_signature a input_signature para diferentes tamaños de imagen o valores de steps / step_size . Consulte la guía de funciones de hormigón para obtener 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

Subiendo 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 todos 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 en 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

5.453245639801025

Opcional: ampliación con mosaicos

Una cosa a considerar es que a medida que la imagen aumenta de tamaño, también aumentará 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 degradado para cada mosaico.

La aplicación de cambios aleatorios a la imagen antes de cada cálculo de mosaico evita que aparezcan las juntas 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 ensueño profundo escalable y con reconocimiento de octavas:

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 octavas y las capas activadas para cambiar el aspecto de tu imagen de DeepDream-ed.

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