Sogno profondo

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza l'origine su GitHub Scarica quaderno

Questo tutorial contiene un'implementazione minima di DeepDream, come descritto in questo post sul blog di Alexander Mordvintsev.

DeepDream è un esperimento che visualizza gli schemi appresi da una rete neurale. Simile a quando un bambino guarda le nuvole e cerca di interpretare forme casuali, DeepDream sovra-interpreta e migliora i modelli che vede in un'immagine.

Lo fa inoltrando un'immagine attraverso la rete, quindi calcolando il gradiente dell'immagine rispetto alle attivazioni di un particolare livello. L'immagine viene quindi modificata per aumentare queste attivazioni, migliorando gli schemi visti dalla rete e risultando in un'immagine onirica. Questo processo è stato soprannominato "Inceptionism" (un riferimento a InceptionNet e al film Inception).

Dimostriamo come puoi far "sognare" una rete neurale e migliorare gli schemi surreali che vede in un'immagine.

Dogcezione

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.Image

Scegli un'immagine per sognare

Per questo tutorial, usiamo un'immagine di 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

Preparare il modello di estrazione delle caratteristiche

Scarica e prepara un modello di classificazione delle immagini pre-addestrato. Utilizzerai InceptionV3 che è simile al modello originariamente utilizzato in DeepDream. Nota che qualsiasi modello pre-addestrato funzionerà, anche se dovrai regolare i nomi dei livelli di seguito se lo cambi.

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'idea in DeepDream è quella di scegliere uno o più livelli e massimizzare la "perdita" in modo che l'immagine "entusiasmi" sempre più i livelli. La complessità delle funzioni incorporate dipende dai livelli scelti dall'utente, ad esempio, i livelli inferiori producono tratti o motivi semplici, mentre i livelli più profondi danno caratteristiche sofisticate alle immagini o persino a oggetti interi.

L'architettura InceptionV3 è piuttosto grande (per un grafico dell'architettura del modello vedere il repository di ricerca di TensorFlow). Per DeepDream, i livelli di interesse sono quelli in cui le convoluzioni sono concatenate. Ci sono 11 di questi livelli in InceptionV3, chiamati 'mixed0' anche se 'mixed10'. L'uso di diversi livelli si tradurrà in diverse immagini oniriche. I livelli più profondi rispondono a funzionalità di livello superiore (come occhi e volti), mentre i livelli precedenti rispondono a funzionalità più semplici (come bordi, forme e trame). Sentiti libero di sperimentare con i livelli selezionati di seguito, ma tieni presente che gli strati più profondi (quelli con un indice più alto) impiegheranno più tempo per allenarsi poiché il calcolo del gradiente è più profondo.

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

Calcola la perdita

La perdita è la somma delle attivazioni nei livelli scelti. La perdita è normalizzata a ogni livello, quindi il contributo degli strati più grandi non supera gli strati più piccoli. Normalmente, la perdita è una quantità che si desidera ridurre al minimo tramite la discesa del gradiente. In DeepDream, massimizzerai questa perdita tramite la salita in pendenza.

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)

Dislivello in salita

Una volta calcolata la perdita per i livelli scelti, non resta che calcolare i gradienti rispetto all'immagine e aggiungerli all'immagine originale.

L'aggiunta dei gradienti all'immagine migliora i modelli visti dalla rete. Ad ogni passaggio, avrai creato un'immagine che eccita sempre di più le attivazioni di determinati livelli nella rete.

Il metodo che esegue questa operazione, di seguito, è racchiuso in una tf.function . per le prestazioni. Utilizza un input_signature per garantire che la funzione non venga ritracciata per dimensioni dell'immagine o valori di steps / step_size . Vedere la guida alle funzioni Concrete per i dettagli.

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)

Ciclo 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

Prendendolo un'ottava

Abbastanza buono, ma ci sono alcuni problemi con questo primo tentativo:

  1. L'uscita è rumorosa (questo potrebbe essere risolto con una perdita di tf.image.total_variation ).
  2. L'immagine è a bassa risoluzione.
  3. Gli schemi sembrano accadere tutti con la stessa granularità.

Un approccio che affronta tutti questi problemi consiste nell'applicare l'ascesa del gradiente a scale diverse. Ciò consentirà ai modelli generati su scale più piccole di essere incorporati in modelli su scale più elevate e riempiti con dettagli aggiuntivi.

Per fare ciò puoi eseguire il precedente approccio all'ascesa del gradiente, quindi aumentare la dimensione dell'immagine (che viene definita ottava) e ripetere questo processo per più ottave.

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

Opzionale: scalare con le tessere

Una cosa da considerare è che all'aumentare delle dimensioni dell'immagine, aumentano anche il tempo e la memoria necessari per eseguire il calcolo del gradiente. L'implementazione dell'ottava sopra non funzionerà su immagini molto grandi o molte ottave.

Per evitare questo problema, puoi dividere l'immagine in riquadri e calcolare il gradiente per ogni riquadro.

L'applicazione di spostamenti casuali all'immagine prima di ogni calcolo affiancato impedisce la visualizzazione delle giunzioni delle piastrelle.

Inizia implementando il turno casuale:

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

Ecco un equivalente piastrellato della funzione deepdream definita in precedenza:

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)

Mettendo questo insieme si ottiene un'implementazione deepdream scalabile e sensibile all'ottava:

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

Molto meglio! Gioca con il numero di ottave, la scala di ottava e i livelli attivati ​​per cambiare l'aspetto della tua immagine DeepDream.

I lettori potrebbero anche essere interessati a TensorFlow Lucid che espande le idee introdotte in questo tutorial per visualizzare e interpretare le reti neurali.