Ayuda a proteger la Gran Barrera de Coral con TensorFlow en Kaggle Únete Challenge

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, tal como se describe en esta entrada del 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, 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 se 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, vamos a utilizar 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

Preparar el modelo de extracción de características

Descargue y prepare un modelo de clasificación de imágenes previamente entrenado. Que va a utilizar InceptionV3 que es similar al modelo utilizado originalmente en DeepDream. Observe que cualquier modelo de pre-formados funcionará, aunque tendrá que ajustar los nombres de las capas inferiores 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 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 InceptionV3 es bastante grande (para un gráfico de la arquitectura modelo ver de TensorFlow repo investigación ). 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, se envuelve en un 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. Ver las funciones de hormigón guían 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

Subiendo una octava

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

  1. La salida es ruidoso (esto podría resolverse con una tf.image.total_variation pérdida).
  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

7.049884080886841

Opcional: ampliación con mosaicos

Una cosa a considerar 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 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)
  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

Aquí está alicatado equivalente del deepdream función 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, 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)

Al juntar esto, se obtiene una implementación de ensueño profundo escalable y consciente de las 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 puede estar interesado en TensorFlow lúcido que se expande en las ideas presentadas en este tutorial para visualizar e interpretar las redes neuronales.