DeepDream

Xem trên TensorFlow.org Chạy trong Google Colab Xem nguồn trên GitHub Tải xuống sổ ghi chép

Hướng dẫn này chứa một cách triển khai tối thiểu của DeepDream, như được mô tả trong bài đăng trên blog này của Alexander Mordvintsev.

DeepDream là một thử nghiệm trực quan hóa các mẫu được học bởi mạng nơ-ron. Tương tự như khi một đứa trẻ xem các đám mây và cố gắng giải thích các hình dạng ngẫu nhiên, DeepDream sẽ giải thích quá mức và nâng cao các mẫu mà nó nhìn thấy trong một hình ảnh.

Nó làm như vậy bằng cách chuyển tiếp một hình ảnh qua mạng, sau đó tính toán độ dốc của hình ảnh liên quan đến các kích hoạt của một lớp cụ thể. Sau đó, hình ảnh được sửa đổi để tăng các kích hoạt này, nâng cao các mẫu mà mạng nhìn thấy và dẫn đến hình ảnh giống như trong mơ. Quá trình này được đặt tên là "Inceptionism" (tham chiếu đến InceptionNetphim Inception).

Hãy chứng minh cách bạn có thể biến một mạng nơ-ron thành "giấc mơ" và nâng cao các mẫu siêu thực mà nó nhìn thấy trong một hình ảnh.

Dogception

import tensorflow as tf
import numpy as np

import matplotlib as mpl

import IPython.display as display
import PIL.Image

Chọn một hình ảnh để mơ

Đối với hướng dẫn này, hãy sử dụng hình ảnh của một con 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

Chuẩn bị mô hình trích xuất tính năng

Tải xuống và chuẩn bị mô hình phân loại hình ảnh được đào tạo trước. Bạn sẽ sử dụng InceptionV3 tương tự như mô hình ban đầu được sử dụng trong DeepDream. Lưu ý rằng bất kỳ mô hình nào được đào tạo trước sẽ hoạt động, mặc dù bạn sẽ phải điều chỉnh tên lớp bên dưới nếu bạn thay đổi điều này.

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

Ý tưởng trong DeepDream là chọn một lớp (hoặc các lớp) và tối đa hóa sự "mất mát" theo cách mà hình ảnh ngày càng "kích thích" các lớp. Độ phức tạp của các tính năng được kết hợp phụ thuộc vào các lớp do bạn chọn, tức là các lớp thấp hơn tạo ra các nét hoặc các mẫu đơn giản, trong khi các lớp sâu hơn tạo ra các tính năng phức tạp trong hình ảnh hoặc thậm chí toàn bộ đối tượng.

Kiến trúc InceptionV3 khá lớn (để biết đồ thị của kiến ​​trúc mô hình, hãy xem repo nghiên cứu của TensorFlow). Đối với DeepDream, các lớp quan tâm là những lớp mà các tập hợp được nối với nhau. Có 11 trong số các lớp này trong InceptionV3, được đặt tên là 'hỗn hợp0' mặc dù 'hỗn hợp10'. Sử dụng các lớp khác nhau sẽ tạo ra các hình ảnh giống như trong mơ khác nhau. Các lớp sâu hơn phản hồi các tính năng cấp cao hơn (chẳng hạn như mắt và khuôn mặt), trong khi các lớp trước đó phản hồi các tính năng đơn giản hơn (chẳng hạn như các cạnh, hình dạng và kết cấu). Hãy thử nghiệm với các lớp được chọn bên dưới, nhưng hãy nhớ rằng các lớp sâu hơn (những lớp có chỉ số cao hơn) sẽ mất nhiều thời gian hơn để đào tạo vì tính toán gradient sâu hơn.

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

Tính toán tổn thất

Tổn thất là tổng số lần kích hoạt trong các lớp đã chọn. Sự mất mát được bình thường hóa ở mỗi lớp vì vậy đóng góp từ các lớp lớn hơn không nhiều hơn các lớp nhỏ hơn. Thông thường, tổn thất là số lượng bạn muốn giảm thiểu thông qua giảm độ dốc. Trong DeepDream, bạn sẽ tối đa hóa sự mất mát này thông qua sự đi lên của gradient.

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)

Đi lên dốc

Khi bạn đã tính toán sự mất mát cho các lớp đã chọn, tất cả những gì còn lại là tính toán các độ dốc đối với hình ảnh và thêm chúng vào hình ảnh ban đầu.

Việc thêm các gradient vào hình ảnh sẽ tăng cường các mẫu mà mạng nhìn thấy. Ở mỗi bước, bạn sẽ tạo ra một hình ảnh ngày càng kích thích sự kích hoạt của các lớp nhất định trong mạng.

Phương pháp thực hiện điều này, bên dưới, được bao bọc trong một tf.function . để thực hiện. Nó sử dụng một input_signature để đảm bảo rằng hàm không bị kiểm tra lại đối với các kích thước hình ảnh khác nhau hoặc các giá trị steps / step_size . Xem hướng dẫn các chức năng Bê tông để biết thêm chi tiết.

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)

Vòng lặp chính

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

Mất một quãng tám

Khá tốt, nhưng có một số vấn đề với lần thử đầu tiên này:

  1. Đầu ra bị nhiễu (điều này có thể được giải quyết bằng mất tf.image.total_variation ).
  2. Hình ảnh có độ phân giải thấp.
  3. Các mẫu có vẻ như tất cả chúng đều diễn ra ở cùng một mức độ chi tiết.

Một cách tiếp cận giải quyết tất cả những vấn đề này là áp dụng gradient đi lên ở các tỷ lệ khác nhau. Điều này sẽ cho phép các mẫu được tạo ra ở tỷ lệ nhỏ hơn được kết hợp thành các mẫu ở tỷ lệ cao hơn và được lấp đầy bằng các chi tiết bổ sung.

Để thực hiện điều này, bạn có thể thực hiện phương pháp tiếp cận tăng độ dốc trước đó, sau đó tăng kích thước của hình ảnh (được gọi là một quãng tám) và lặp lại quá trình này trong nhiều quãng tám.

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

Tùy chọn: Mở rộng quy mô với các ô

Một điều cần lưu ý là khi hình ảnh tăng kích thước, thì thời gian và bộ nhớ cần thiết để thực hiện phép tính gradient cũng sẽ tăng theo. Việc triển khai quãng tám ở trên sẽ không hoạt động trên hình ảnh rất lớn hoặc nhiều quãng tám.

Để tránh vấn đề này, bạn có thể chia hình ảnh thành các ô và tính toán độ dốc cho mỗi ô.

Việc áp dụng các thay đổi ngẫu nhiên cho hình ảnh trước mỗi lần tính toán lát gạch sẽ ngăn không cho các đường nối gạch xuất hiện.

Bắt đầu bằng cách thực hiện sự thay đổi ngẫu nhiên:

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

Đây là một hàm tương đương với hàm deepdream được định nghĩa trước đó:

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)

Kết hợp điều này lại với nhau mang lại một triển khai luồng sâu có thể mở rộng, nhận biết quãng tám:

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

Tốt hơn nhiều! Thử nghiệm với số quãng tám, thang quãng tám và các lớp được kích hoạt để thay đổi hình ảnh DeepDream-ed của bạn trông như thế nào.

Người đọc cũng có thể quan tâm đến TensorFlow Lucid mở rộng các ý tưởng được giới thiệu trong hướng dẫn này để hình dung và giải thích các mạng nơ-ron.