DeepDream

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

Ten samouczek zawiera minimalną implementację DeepDream, opisaną w tym poście na blogu przez Aleksandra Mordvintseva.

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

Czyni to, przesyłając obraz przez sieć, a następnie obliczając gradient obrazu w odniesieniu do aktywacji konkretnej warstwy. Obraz jest następnie modyfikowany, aby zwiększyć te aktywacje, wzmacniając wzory widziane przez sieć i dając w rezultacie obraz przypominający sen. Proces ten został nazwany „Incepcją” (odniesienie do InceptionNet i filmu Incepcja).

Zademonstrujmy, jak można sprawić, by sieć neuronowa „śniła” i uwydatnić surrealistyczne wzory, które widzi na obrazie.

Dogcepcja

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.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 wytrenowany model klasyfikacji obrazów. Użyjesz InceptionV3 , który jest podobny do modelu pierwotnie używanego w DeepDream. Pamiętaj, że każdy wstępnie wytrenowany model będzie działał, chociaż będziesz musiał dostosować poniższe nazwy warstw, 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 [==============================] - 0s 0us/step
87924736/87910968 [==============================] - 0s 0us/step

Ideą DeepDream jest wybranie warstwy (lub warstw) i zmaksymalizowanie „straty” w taki sposób, aby obraz coraz bardziej „podniecał” warstwy. Złożoność wbudowanych funkcji zależy od wybranych przez Ciebie warstw, tj. niższe warstwy tworzą pociągnięcia lub proste wzory, podczas gdy głębsze warstwy dają 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 warstwy to te, w których konwolucje są łączone. W InceptionV3 jest 11 takich warstw, nazwanych „mixed0” i „mixed10”. Używanie różnych warstw spowoduje powstanie różnych obrazów przypominających sen. Głębsze warstwy reagują na elementy wyższego poziomu (takie jak oczy i twarze), podczas gdy wcześniejsze warstwy reagują na prostsze elementy (takie jak krawędzie, kształty i tekstury). Możesz swobodnie poeksperymentować z warstwami wybranymi poniżej, ale pamiętaj, że głębsze warstwy (te z wyższym indeksem) będą wymagać więcej czasu na trenowanie, ponieważ obliczanie gradientu jest 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 znormalizowana w każdej warstwie, więc wkład z większych warstw nie przeważa nad mniejszymi. Zwykle strata to wielkość, którą chcesz zminimalizować poprzez opadanie gradientu. W DeepDream zmaksymalizujesz tę stratę poprzez wznoszenie gradientowe.

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)

Wznoszenie gradientowe

Po obliczeniu strat dla wybranych warstw pozostaje tylko obliczenie gradientów w odniesieniu do obrazu i dodanie ich do oryginalnego obrazu.

Dodanie gradientów do obrazu uwydatnia wzory widziane przez sieć. Na każdym kroku utworzysz obraz, który w coraz większym stopniu pobudza aktywacje niektórych warstw w sieci.

Metoda, która to robi, poniżej, jest opakowana w tf.function dla wydajności. Używa input_signature , aby zapewnić, że funkcja nie jest odtwarzana dla różnych rozmiarów obrazu lub wartości steps / step_size . Szczegółowe informacje można znaleźć w przewodniku 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 to o oktawę

Całkiem dobrze, ale pierwsza próba wiąże się z kilkoma problemami:

  1. Dane wyjściowe są zaszumione (można to rozwiązać za pomocą utraty tf.image.total_variation ).
  2. Obraz ma niską rozdzielczość.
  3. Wzorce wyglądają tak, jakby wszystkie zachodziły na tym samym poziomie szczegółowości.

Jednym z rozwiązań, które rozwiązuje wszystkie te problemy, jest zastosowanie wznoszenia gradientowego w różnych skalach. Umożliwi to włączenie wzorów wygenerowanych w mniejszych skalach do wzorów w wyższych skalach i wypełnienie ich dodatkowymi szczegółami.

Aby to zrobić, możesz wykonać poprzednie podejście gradientu, a 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

6.38355278968811

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

Jedną z rzeczy do rozważenia jest to, że wraz ze wzrostem rozmiaru obrazu, czas i pamięć potrzebne do wykonania obliczeń gradientu również się zwiększą. Powyższa implementacja oktawy nie będzie działać na bardzo dużych obrazach lub wielu oktawach.

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 sąsiadującym zapobiega pojawianiu się połączeń sąsiadujących.

Zacznij od wprowadzenia losowej zmiany:

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=[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)

Zestawienie tego razem daje skalowalną, obsługują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.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

Dużo lepiej! Pobaw 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 rozwija pomysły przedstawione w tym samouczku dotyczące wizualizacji i interpretacji sieci neuronowych.