Ta strona została przetłumaczona przez Cloud Translation API.
Switch to English

DeepDream

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

Ten samouczek zawiera minimalną implementację DeepDream, jak opisano w tym poście na blogu Alexandra Mordvintseva.

DeepDream to eksperyment, który wizualizuje wzorce wyuczone przez sieć neuronową. Podobnie jak wtedy, gdy dziecko obserwuje chmury i próbuje zinterpretować przypadkowe kształty, DeepDream nadinterpretuje i ulepsza wzory, które widzi na obrazie.

Odbywa się to poprzez przekazywanie obrazu przez sieć, a następnie obliczanie gradientu obrazu w odniesieniu do aktywacji określonej warstwy. Obraz jest następnie modyfikowany, aby zwiększyć te aktywacje, wzmacniając wzorce widziane przez sieć i dając w rezultacie obraz przypominający sen. Proces ten nazwano „Incepcjalizmem” (nawiązanie do InceptionNet i filmu Incepcja).

Pokażmy, jak można sprawić, by sieć neuronowa „śniła” i wzmocniła surrealistyczne wzorce, jakie widzi na obrazie.

Dogcepcja

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

Wybierz obraz do snu

W tym samouczku użyjmy obrazu labradora .

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>'))
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg
90112/83281 [================================] - 0s 0us/step

png

Przygotuj model wyodrębniania cech

Pobierz i przygotuj wstępnie przeszkolony model klasyfikacji obrazu. Będziesz używać InceptionV3, który jest podobny do modelu pierwotnie używanego w DeepDream. Zwróć uwagę, że każdy wstępnie wytrenowany model będzie działał, chociaż będziesz musiał dostosować nazwy warstw poniżej, jeśli to zmienisz.

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

Ideą DeepDream jest wybranie warstwy (lub warstw) i zmaksymalizowanie „strat” w taki sposób, aby obraz coraz bardziej „podniecał” warstwy. Złożoność włączonych funkcji zależy od wybranych przez Ciebie warstw, tj. Niższe warstwy tworzą pociągnięcia lub proste wzory, podczas gdy głębsze warstwy zapewniają wyrafinowane funkcje na obrazach, a nawet całych obiektach.

Architektura InceptionV3 jest dość duża (wykres architektury modelu można znaleźć w repozytorium badawczym TensorFlow). W przypadku DeepDream interesujące są warstwy, w których zwoje są łączone. W InceptionV3 jest 11 takich warstw, nazwane „mieszane0”, ale „mieszane10”. Używanie różnych warstw da w rezultacie różne obrazy przypominające sen. Głębsze warstwy odpowiadają funkcjom wyższego poziomu (takim jak oczy i twarze), podczas gdy wcześniejsze warstwy odpowiadają prostszym funkcjom (takim jak krawędzie, kształty i tekstury). Możesz poeksperymentować z warstwami wybranymi poniżej, ale pamiętaj, że głębsze warstwy (te o wyższym indeksie) będą wymagały więcej czasu, ponieważ obliczenia gradientu są głębsze.

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

Oblicz stratę

Strata to suma aktywacji w wybranych warstwach. Straty są normalizowane w każdej warstwie, więc wkład większych warstw nie przeważa nad mniejszymi warstwami. Zwykle strata to wielkość, którą chcesz zminimalizować za pomocą gradientu. W DeepDream zmaksymalizujesz tę stratę poprzez wznoszenie gradientu.

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)

Gradientowe wznoszenie

Po obliczeniu strat dla wybranych warstw pozostaje tylko obliczyć gradienty w odniesieniu do obrazu i dodać je do oryginalnego obrazu.

Dodanie gradientów do obrazu poprawia wzorce widoczne w sieci. Na każdym kroku utworzysz obraz, który coraz bardziej podnieca aktywacje niektórych warstw w sieci.

Metoda, która to robi, poniżej jest opakowana w funkcję tf.function celu tf.function wydajności. Używa input_signature aby zapewnić, że funkcja nie zostanie odtworzona dla różnych rozmiarów obrazu lub wartości steps / step_size . Szczegółowe informacje zawiera przewodnik po funkcjach betonu .

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)

Główna pętla

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

Podnosząc oktawę

Całkiem nieźle, ale przy pierwszej próbie jest kilka problemów:

  1. Wyjście jest zaszumione (można temu tf.image.total_variation utratą tf.image.total_variation ).
  2. Obraz ma niską rozdzielczość.
  3. Wygląda na to, że wzorce zachodzą w tej samej ziarnistości.

Jednym z podejść, które rozwiązuje wszystkie te problemy, jest zastosowanie wznoszenia gradientowego w różnych skalach. Pozwoli to na włączenie wzorców generowanych w mniejszych skalach do wzorców w wyższych skalach i wypełnienie dodatkowymi szczegółami.

Aby to zrobić, możesz wykonać poprzednią metodę wznoszenia gradientowego, następnie zwiększyć rozmiar obrazu (określany jako oktawa) i powtórzyć ten proces dla wielu oktaw.

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

Opcjonalnie: skalowanie w górę za pomocą płytek

Jedną rzeczą do rozważenia jest to, że wraz ze wzrostem rozmiaru obrazu rośnie czas i pamięć niezbędne do wykonania obliczenia gradientu. Powyższa implementacja oktaw nie będzie działać w przypadku bardzo dużych obrazów lub wielu oktaw.

Aby uniknąć tego problemu, możesz podzielić obraz na kafelki i obliczyć gradient dla każdego kafelka.

Stosowanie losowych przesunięć do obrazu przed każdym obliczeniem kafelków zapobiega pojawianiu się szwów kafelków.

Zacznij od wprowadzenia losowego przesunięcia:

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

Oto kafelkowy odpowiednik zdefiniowanej wcześniej funkcji deepdream :

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)

Połączenie tego daje skalowalną implementację deepdream uwzględniającą oktawy:

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

Dużo lepiej! Baw się liczbą oktaw, skalą oktaw i aktywowanymi warstwami, aby zmienić wygląd obrazu w technologii DeepDream.

Czytelników może również zainteresować TensorFlow Lucid, który rozszerza pomysły przedstawione w tym samouczku dotyczące wizualizacji i interpretacji sieci neuronowych.