DeepDream

Auf TensorFlow.org ansehen In Google Colab ausführen Quelle auf GitHub anzeigen Notizbuch herunterladen

Dieses Tutorial enthält eine minimale Implementierung von DeepDream, wie in diesem Blogbeitrag von Alexander Mordvintsev beschrieben.

DeepDream ist ein Experiment, das die von einem neuronalen Netzwerk gelernten Muster visualisiert. Ähnlich wie wenn ein Kind Wolken beobachtet und versucht, zufällige Formen zu interpretieren, überinterpretiert und verbessert DeepDream die Muster, die es in einem Bild sieht.

Dazu wird ein Bild durch das Netzwerk weitergeleitet und dann der Gradient des Bildes in Bezug auf die Aktivierungen einer bestimmten Schicht berechnet. Das Bild wird dann modifiziert, um diese Aktivierungen zu verstärken, die vom Netzwerk gesehenen Muster zu verstärken und zu einem traumähnlichen Bild zu führen. Dieser Prozess wurde "Inceptionism" genannt (ein Hinweis auf InceptionNet und den Film Inception).

Lassen Sie uns demonstrieren, wie Sie ein neuronales Netzwerk zum "Traum" machen und die surrealen Muster, die es in einem Bild sieht, verbessern können.

Hunderezeption

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

Wählen Sie ein Bild zum Träumen aus

Für dieses Tutorial verwenden wir ein Bild eines Labradors .

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

Bereiten Sie das Feature-Extraktionsmodell vor

Laden Sie ein vortrainiertes Bildklassifizierungsmodell herunter und bereiten Sie es vor. Sie verwenden InceptionV3, das dem ursprünglich in DeepDream verwendeten Modell ähnelt. Beachten Sie, dass jedes vortrainierte Modell funktioniert, obwohl Sie die Layer-Namen unten anpassen müssen, wenn Sie dies ändern.

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

Die Idee in DeepDream besteht darin, eine Ebene (oder Ebenen) auszuwählen und den "Verlust" so zu maximieren, dass das Bild die Ebenen zunehmend "anregt". Die Komplexität der eingebauten Funktionen hängt von den von Ihnen gewählten Ebenen ab, dh niedrigere Ebenen erzeugen Striche oder einfache Muster, während tiefere Ebenen anspruchsvollere Funktionen in Bildern oder sogar ganzen Objekten liefern.

Die InceptionV3-Architektur ist ziemlich groß (eine Grafik der Modellarchitektur finden Sie im Forschungsrepo von TensorFlow). Für DeepDream sind die Schichten von Interesse, in denen die Faltungen verkettet sind. In InceptionV3 gibt es 11 dieser Layer, die als 'mixed0' bis 'mixed10' bezeichnet werden. Die Verwendung verschiedener Ebenen führt zu verschiedenen traumähnlichen Bildern. Tiefere Layer reagieren auf übergeordnete Features (z. B. Augen und Gesichter), während frühere Layer auf einfachere Features (z. B. Kanten, Formen und Texturen) reagieren. Experimentieren Sie mit den unten ausgewählten Layern, aber denken Sie daran, dass das Training auf tieferen Layern (mit einem höheren Index) länger dauert, da die Gradientenberechnung tiefer ist.

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

Verlust berechnen

Der Verlust ist die Summe der Aktivierungen in den ausgewählten Schichten. Der Verlust wird bei jeder Schicht normalisiert, so dass der Beitrag von größeren Schichten kleinere Schichten nicht überwiegt. Normalerweise ist Verlust eine Größe, die Sie durch Gradientenabstieg minimieren möchten. In DeepDream maximieren Sie diesen Verlust durch den Gradientenaufstieg.

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)

Gefälleaufstieg

Nachdem Sie den Verlust für die ausgewählten Ebenen berechnet haben, müssen Sie nur noch die Gradienten in Bezug auf das Bild berechnen und zum Originalbild hinzufügen.

Das Hinzufügen der Farbverläufe zum Bild verbessert die vom Netzwerk erkannten Muster. Bei jedem Schritt haben Sie ein Bild erstellt, das die Aktivierungen bestimmter Schichten im Netzwerk zunehmend anregt.

Die Methode, die dies unten tut, ist aus Leistungsgründen in eine tf.function . Es verwendet eine input_signature um sicherzustellen, dass die Funktion nicht für verschiedene Bildgrößen oder steps / step_size Werte step_size . Weitere Informationen finden Sie im Handbuch Betonfunktionen.

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)

Hauptschleife

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

Nimm es eine Oktave auf

Ziemlich gut, aber bei diesem ersten Versuch gibt es ein paar Probleme:

  1. Die Ausgabe ist verrauscht (dies könnte mit einem tf.image.total_variation Verlust tf.image.total_variation ).
  2. Das Bild hat eine niedrige Auflösung.
  3. Die Muster erscheinen, als ob sie alle mit der gleichen Granularität passierten.

Ein Ansatz, der all diese Probleme anspricht, besteht darin, den Gradientenanstieg auf verschiedenen Skalen anzuwenden. Dadurch können Muster, die in kleineren Maßstäben erzeugt wurden, in Muster in höheren Maßstäben integriert und mit zusätzlichen Details ausgefüllt werden.

Um dies zu tun, können Sie den vorherigen Gradienten-Aufstiegsansatz ausführen, dann die Größe des Bildes erhöhen (was als Oktave bezeichnet wird) und diesen Vorgang für mehrere Oktaven wiederholen.

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

Optional: Hochskalieren mit Kacheln

Zu beachten ist, dass mit zunehmender Bildgröße auch die Zeit und der Speicher, die für die Berechnung des Gradienten erforderlich sind, zunehmen. Die obige Oktavimplementierung funktioniert nicht bei sehr großen Bildern oder vielen Oktaven.

Um dieses Problem zu vermeiden, können Sie das Bild in Kacheln aufteilen und den Gradienten für jede Kachel berechnen.

Das Anwenden von zufälligen Verschiebungen auf das Bild vor jeder gekachelten Berechnung verhindert das Auftreten von Kachelnähten.

Beginnen Sie mit der Implementierung der zufälligen Verschiebung:

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

Hier ist ein gekacheltes Äquivalent der deepdream definierten deepdream Funktion:

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)

Zusammen ergibt dies eine skalierbare, oktavbewusste Deepdream-Implementierung:

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

Viel besser! Spielen Sie mit der Anzahl der Oktaven, der Oktavskala und den aktivierten Ebenen herum, um das Aussehen Ihres DeepDream-ed-Bilds zu ändern.

Leser könnten auch an TensorFlow Lucid interessiert sein, das die in diesem Tutorial vorgestellten Ideen zur Visualisierung und Interpretation neuronaler Netze erweitert.