DeepDream

Veja no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Este tutorial contém uma implementação mínima do DeepDream, conforme descrito nesta postagem do blog de Alexander Mordvintsev.

DeepDream é um experimento que visualiza os padrões aprendidos por uma rede neural. Semelhante a quando uma criança observa nuvens e tenta interpretar formas aleatórias, o DeepDream interpreta e aprimora os padrões que vê em uma imagem.

Ele faz isso encaminhando uma imagem pela rede e calculando o gradiente da imagem em relação às ativações de uma determinada camada. A imagem é então modificada para aumentar essas ativações, realçando os padrões vistos pela rede e resultando em uma imagem onírica. Este processo foi apelidado de "Inceptionism" (uma referência ao InceptionNet e ao filme Inception).

Vamos demonstrar como você pode fazer uma rede neural "sonhar" e aprimorar os padrões surreais que ela vê em uma imagem.

Dogception

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.Image

Escolha uma imagem para sonhar

Para este tutorial, vamos usar uma imagem de um 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

Preparar o modelo de extração de recursos

Baixe e prepare um modelo de classificação de imagem pré-treinado. Você usará o InceptionV3 , que é semelhante ao modelo originalmente usado no DeepDream. Observe que qualquer modelo pré-treinado funcionará, embora você precise ajustar os nomes das camadas abaixo se alterar isso.

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

A ideia no DeepDream é escolher uma camada (ou camadas) e maximizar a "perda" de forma que a imagem cada vez mais "excite" as camadas. A complexidade dos recursos incorporados depende das camadas escolhidas por você, ou seja, as camadas inferiores produzem traços ou padrões simples, enquanto as camadas mais profundas fornecem recursos sofisticados em imagens, ou mesmo objetos inteiros.

A arquitetura InceptionV3 é bastante grande (para um gráfico da arquitetura do modelo, consulte o repositório de pesquisa do TensorFlow ). Para DeepDream, as camadas de interesse são aquelas onde as convoluções são concatenadas. Existem 11 dessas camadas no InceptionV3, chamadas 'mixed0' embora 'mixed10'. O uso de diferentes camadas resultará em diferentes imagens de sonho. As camadas mais profundas respondem a recursos de nível superior (como olhos e rostos), enquanto as camadas anteriores respondem a recursos mais simples (como bordas, formas e texturas). Sinta-se à vontade para experimentar as camadas selecionadas abaixo, mas lembre-se de que as camadas mais profundas (aquelas com um índice mais alto) levarão mais tempo para treinar, pois a computação do gradiente é mais profunda.

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

Calcular perda

A perda é a soma das ativações nas camadas escolhidas. A perda é normalizada em cada camada para que a contribuição das camadas maiores não supere as camadas menores. Normalmente, a perda é uma quantidade que você deseja minimizar através da descida do gradiente. No DeepDream, você maximizará essa perda por meio da subida do gradiente.

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)

Subida de gradiente

Depois de ter calculado a perda para as camadas escolhidas, tudo o que resta é calcular os gradientes em relação à imagem e adicioná-los à imagem original.

Adicionar os gradientes à imagem aprimora os padrões vistos pela rede. A cada passo, você terá criado uma imagem que excita cada vez mais as ativações de determinadas camadas da rede.

O método que faz isso, abaixo, é encapsulado em um tf.function para desempenho. Ele usa um input_signature para garantir que a função não seja rastreada para diferentes tamanhos de imagem ou valores de steps / step_size . Consulte o guia de funções de concreto para obter detalhes.

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)

Circuito Principal

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

Levando-o até uma oitava

Muito bom, mas há alguns problemas com esta primeira tentativa:

  1. A saída é ruidosa (isso pode ser resolvido com uma perda tf.image.total_variation ).
  2. A imagem é de baixa resolução.
  3. Os padrões parecem estar acontecendo na mesma granularidade.

Uma abordagem que aborda todos esses problemas é aplicar a subida do gradiente em diferentes escalas. Isso permitirá que padrões gerados em escalas menores sejam incorporados a padrões em escalas mais altas e preenchidos com detalhes adicionais.

Para fazer isso, você pode executar a abordagem anterior de subida de gradiente, aumentar o tamanho da imagem (que é chamada de oitava) e repetir esse processo para várias oitavas.

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

Opcional: Ampliação com ladrilhos

Uma coisa a considerar é que, à medida que a imagem aumenta de tamanho, também aumentam o tempo e a memória necessários para realizar o cálculo do gradiente. A implementação de oitava acima não funcionará em imagens muito grandes ou muitas oitavas.

Para evitar esse problema, você pode dividir a imagem em blocos e calcular o gradiente para cada bloco.

A aplicação de deslocamentos aleatórios à imagem antes de cada cálculo lado a lado evita que as costuras dos blocos apareçam.

Comece implementando o turno aleatório:

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

Aqui está um equivalente lado a lado da função deepdream definida anteriormente:

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)

Juntar isso fornece uma implementação de deepdream escalável e com reconhecimento de oitava:

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

Muito melhor! Brinque com o número de oitavas, escala de oitavas e camadas ativadas para alterar a aparência da sua imagem do DeepDream.

Os leitores também podem se interessar pelo TensorFlow Lucid, que expande as ideias apresentadas neste tutorial para visualizar e interpretar redes neurais.