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 podobny do snu. Ten proces 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>'))

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ą elementom wyższego poziomu (takim jak oczy i twarze), podczas gdy wcześniejsze warstwy odpowiadają prostszym elementom (takim jak krawędzie, kształty i tekstury). Możesz eksperymentować z warstwami wybranymi poniżej, ale pamiętaj, że głębsze warstwy (te z wyższym indeksem) 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. Strata jest normalizowana 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 jest kilka problemów z tą pierwszą próbą:

  1. Wyjście jest zaszumione (problem ten można rozwiązać tf.image.total_variation utratę wartości 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 stosowanie 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.453245639801025

Opcjonalnie: skalowanie w górę za pomocą kafelków

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

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

Połączenie tego daje skalowalną, uwzględniającą oktawę implementację Deepdream:

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ą oktawową 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.