O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

DeepDream

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

Este tutorial contém uma implementação mínima de DeepDream, conforme descrito neste post por Alexander Mordvintsev.

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

Ele faz isso encaminhando uma imagem pela rede e, em seguida, calculando o gradiente da imagem em relação às ativações de uma camada específica. A imagem é então modificada para aumentar essas ativações, realçando os padrões vistos pela rede e resultando em uma imagem de sonho. Este processo foi apelidado de "Inceptionism" (uma referência a InceptionNet , eo 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

from tensorflow.keras.preprocessing import 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

Prepare o modelo de extração de recursos

Baixe e prepare um modelo de classificação de imagem pré-treinado. Você vai usar InceptionV3 que é semelhante ao modelo usado originalmente na DeepDream. Note que qualquer modelo pré-treinados irá funcionar, embora você terá que ajustar os nomes de camadas abaixo, se você mudar 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 [==============================] - 4s 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 em objetos inteiros.

A arquitetura InceptionV3 é bastante grande (para um gráfico da arquitetura modelo ver de TensorFlow repo pesquisa ). Para DeepDream, as camadas de interesse são aquelas em que as convoluções são concatenadas. Existem 11 dessas camadas no InceptionV3, chamadas 'mixed0' embora 'mixed10'. O uso de camadas diferentes resultará em imagens de sonho diferentes. 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 o cálculo do gradiente é mais profundo.

# 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 a perda

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

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

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

O método que faz isso, a seguir, é envolto numa tf.function para o desempenho. Ele usa um input_signature para garantir que a função não é refez para diferentes tamanhos de imagem ou steps / step_size valores. Veja as funções de concreto guiar para mais 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)

Loop 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

Subindo uma oitava

Muito bom, mas existem alguns problemas com esta primeira tentativa:

  1. A saída é barulhento (que pode ser abordado com um tf.image.total_variation perda).
  2. A imagem é de baixa resolução.
  3. Os padrões aparecem como se estivessem todos acontecendo na mesma granularidade.

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

Para fazer isso, você pode executar a abordagem de subida gradiente anterior, aumentar o tamanho da imagem (que é referido como uma oitava) e repetir este 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

7.049884080886841

Opcional: ampliação com blocos

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

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

Aplicar deslocamentos aleatórios à imagem antes de cada cálculo lado a lado evita que as junções dos blocos apareçam.

Comece implementando a mudança aleatória:

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 azulejos equivalente do deepdream função 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=[], 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)

Juntar tudo isso fornece uma implementação de deepdream escalonável e ciente da 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.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

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

Os leitores também pode estar interessado em TensorFlow Lucid que se expande em idéias introduzidas neste tutorial para visualizar e interpretar redes neurais.