In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Eager execution

TensorFlow eager execution —— это императивная программная среда, которая вычисляет операции немедленно, без построения графов: операции возвращают конкретные значения вместо построения вычислительного графа для последующего запуска. Это облегчает начало работы с TensorFlow и отладкой моделей, а также шаблонный код. Чтобы следовать этому руководству, выполните приведенныее ниже примеры кода в интерактивном интерпретаторе python.

Eager execution —— это гибкая платформа машинного обучения для исследований и экспериментов, обеспечивающая:

  • Интуитивный интерфейс—Структурируйте ваш код естественным образом и используйте структуры данных Python. Быстро итерируйте по небольшим моделям и данным.
  • Более простая отладка—Вызывайте операции напрямую, чтобы проверять работающие модели и тестируйте изменения. Используйте стандартные инструменты Python для немедленных сообщений об ошибках.
  • Естественный порядок выполнения—Используйте порядок выполнения Python вместо порядка выполнения графа, упрощая спецификации динамических моделей.

Eager execution поддерживает большинство операций TensorFlow и акселерацию GPU.

Замечание: Некоторые модели могут испытывать повышенную нагрузку при включенном eager execution. Мы продолжаем работать над улучшением производительности, но пожалуйста сообщите об ошибке если вы обнаружите проблему и поделитесь своим бенчмарком.

Установка и базовое использование


In [ ]:
from __future__ import absolute_import, division, print_function, unicode_literals
import os

try:
  # %tensorflow_version существует только в Colab.
  %tensorflow_version 2.x  #gpu
except Exception:
  pass
import tensorflow as tf

import cProfile

В Tensorflow 2.0, eager execution включено по умолчанию.


In [ ]:
tf.executing_eagerly()

Сейчас вы можете запускать операции TensorFlow и получать результаты немедленно:


In [ ]:
x = [[2.]]
m = tf.matmul(x, x)
print("hello, {}".format(m))

Включение eager execution меняет поведение операций TensorFlow—сейчас они немедленно выполняются и возвращают свои значения в Python. Объекты tf.Tensor ссылаются на конкретные значения вместо символьных дескрипторов на узлы в вычислительном графе. Так как нет вычислительного графа, который нужно построить и выполнить позже в сессии, легко можно проверить результаты используя print() или отладчик. Оценка, печать, и проверка значений тензора не нарушают последовательность вычислений градиентов.

Eager execution прекрасно работает NumPy. Операции NumPy принимают аргументы tf.Tensor. Операция TensorFlow tf.math конвертирует объекты Python и массивы NumPy в объекты tf.Tensor. Метод tf.Tensor.numpy возвращает значение объекта в виде NumPy ndarray.


In [ ]:
a = tf.constant([[1, 2],
                 [3, 4]])
print(a)

In [ ]:
# Поддержка вещания
b = tf.add(a, 1)
print(b)

In [ ]:
# Поддерживается перегрузка операторов
print(a * b)

In [ ]:
# Используем значения NumPy
import numpy as np

c = np.multiply(a, b)
print(c)

In [ ]:
# Получи значение numpy из тензора:
print(a.numpy())
# => [[1 2]
#     [3 4]]

Динамический порядок выполнения

Основым преимуществом eager execution является то, что все функциональныее возможности основного языка доступны во время выполнения модели. Поэтому, например, легко написать fizzbuzz:


In [ ]:
def fizzbuzz(max_num):
  counter = tf.constant(0)
  max_num = tf.convert_to_tensor(max_num)
  for num in range(1, max_num.numpy()+1):
    num = tf.constant(num)
    if int(num % 3) == 0 and int(num % 5) == 0:
      print('FizzBuzz')
    elif int(num % 3) == 0:
      print('Fizz')
    elif int(num % 5) == 0:
      print('Buzz')
    else:
      print(num.numpy())
    counter += 1

In [ ]:
fizzbuzz(15)

Здесь есть условия зависящие от значения тензора, эти значения выводятся во время выполнения.

Режим обучения eager training

Вычисление градиентов

Автоматическое дифференцирование полезно для реализации алгоритмов машинного обучения, таких как обратное распространение для обучения нейронных сетей. Во время eager execution, используйте tf.GradientTape чтобы отслеживать операции для последующего вычисления градиента.

Вы можете использовать tf.GradientTape для обучения и/или вычисления градиентов в eager. Это особенно полезно для сложных тренировочных циклов.

Поскольку во время каждого вызова могут выполняться разные операции, все операции прямого прохода записываются на "ленту". Чтобы вычислить градиент, проиграйте ленту назад, а затем сбросьте. A Конкретный tf.GradientTape может вычислить только один градиент; последующие вызовы выдадут runtime error.


In [ ]:
w = tf.Variable([[1.0]])
with tf.GradientTape() as tape:
  loss = w * w

grad = tape.gradient(loss, w)
print(grad)  # => tf.Tensor([[ 2.]], shape=(1, 1), dtype=float32)

Обучение модели

В следующем примере создается многослойная модель, которая классифицирует стандартные рукописные цифры MNIST. В примере демонстрируется оптимизатор и API слоев для построения обучаемых графов в среде eager execution.


In [ ]:
# Получим и отформатируем данные mnist
(mnist_images, mnist_labels), _ = tf.keras.datasets.mnist.load_data()

dataset = tf.data.Dataset.from_tensor_slices(
  (tf.cast(mnist_images[...,tf.newaxis]/255, tf.float32),
   tf.cast(mnist_labels,tf.int64)))
dataset = dataset.shuffle(1000).batch(32)

In [ ]:
# Построим модель
mnist_model = tf.keras.Sequential([
  tf.keras.layers.Conv2D(16,[3,3], activation='relu',
                         input_shape=(None, None, 1)),
  tf.keras.layers.Conv2D(16,[3,3], activation='relu'),
  tf.keras.layers.GlobalAveragePooling2D(),
  tf.keras.layers.Dense(10)
])

Даже без обучения вызовем модель и проверим выходные данные в eager execution:


In [ ]:
for images,labels in dataset.take(1):
  print("Logits: ", mnist_model(images[0:1]).numpy())

Хотя у моделей keras есть встроенный цикл обучения (использование метода fit), иногда вам нужна большая кастомизация. Вот пример цикла обучения реализованного с eager:


In [ ]:
optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

loss_history = []

Замечание: Используйте функцию assert в tf.debugging чтобы проверить выполнение условия. Это работает в eager и graph execution.


In [ ]:
def train_step(images, labels):
  with tf.GradientTape() as tape:
    logits = mnist_model(images, training=True)
    
    # Добавим assert-ы для проверки размеров выходных данных.
    tf.debugging.assert_equal(logits.shape, (32, 10))
    
    loss_value = loss_object(labels, logits)

  loss_history.append(loss_value.numpy().mean())
  grads = tape.gradient(loss_value, mnist_model.trainable_variables)
  optimizer.apply_gradients(zip(grads, mnist_model.trainable_variables))

In [ ]:
def train(epochs):
  for epoch in range(epochs):
    for (batch, (images, labels)) in enumerate(dataset):
      train_step(images, labels)
    print ('Epoch {} finished'.format(epoch))

In [ ]:
train(epochs = 3)

In [ ]:
import matplotlib.pyplot as plt

plt.plot(loss_history)
plt.xlabel('Batch #')
plt.ylabel('Loss [entropy]')

Переменные и оптимизаторы

Объекты tf.Variable хранят изменяемые значения типа tf.Tensor, доступные во время обучения, чтобы упростить автомматическое дифференцирование.

Наборы переменных могут быть инкапсулированы в слои или модели вместе с методами которые работают на них. См. Кастомные слои и модели Keras для подробностей. Основная разница между слоями и моделями это то, что модели добавляют методы такие, как Model.fit, Model.evaluate и Model.save.

Например приведенный выше пример автоматического дифференцирования может быть переписан так:


In [ ]:
class Linear(tf.keras.Model):
  def __init__(self):
    super(Linear, self).__init__()
    self.W = tf.Variable(5., name='weight')
    self.B = tf.Variable(10., name='bias')
  def call(self, inputs):
    return inputs * self.W + self.B

In [ ]:
# Игрушечный датасет точек вокруг 3 * x + 2
NUM_EXAMPLES = 2000
training_inputs = tf.random.normal([NUM_EXAMPLES])
noise = tf.random.normal([NUM_EXAMPLES])
training_outputs = training_inputs * 3 + 2 + noise

# The loss function to be optimized
def loss(model, inputs, targets):
  error = model(inputs) - targets
  return tf.reduce_mean(tf.square(error))

def grad(model, inputs, targets):
  with tf.GradientTape() as tape:
    loss_value = loss(model, inputs, targets)
  return tape.gradient(loss_value, [model.W, model.B])

Далее:

  1. Создание модели.
  2. Производные функции потерь относительно параметров модели.
  3. Стратегия обновления переменных, основанная на производных.

In [ ]:
model = Linear()
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)

print("Initial loss: {:.3f}".format(loss(model, training_inputs, training_outputs)))

steps = 300
for i in range(steps):
  grads = grad(model, training_inputs, training_outputs)
  optimizer.apply_gradients(zip(grads, [model.W, model.B]))
  if i % 20 == 0:
    print("Loss at step {:03d}: {:.3f}".format(i, loss(model, training_inputs, training_outputs)))

In [ ]:
print("Final loss: {:.3f}".format(loss(model, training_inputs, training_outputs)))

In [ ]:
print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))

Примечание: Переменные хранятся до тех пор, пока не будет удалена последняя ссылка на объект python, с которой удалится и переменная.

Объектно-ориентированное сохранение

tf.keras.Model включает в себя удобный метод save_weights позволяющий вам легко создавать чекпоинт:


In [ ]:
model.save_weights('weights')
status = model.load_weights('weights')

Используя tf.train.Checkpoint вы можете получить полный контроль над процессом.

Этот раздел является сокращенной версией руководства чекпоинтов обучения.


In [ ]:
x = tf.Variable(10.)
checkpoint = tf.train.Checkpoint(x=x)

In [ ]:
x.assign(2.)   # Присвоим новое значение переменной и сохраним.
checkpoint_path = './ckpt/'
checkpoint.save('./ckpt/')

In [ ]:
x.assign(11.)  # Изменим переменную после сохранения.

# Восстановим значения из чекпоинта
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_path))

print(x)  # => 2.0

Чтобы сохранять и загружать модели tf.train.Checkpoint хранит внутреннее состояние объектов, не требуя скрытых переменных. Чтобы записать состояние модели model, optimizer и глобальный шаг передайте их в tf.train.Checkpoint:


In [ ]:
model = tf.keras.Sequential([
  tf.keras.layers.Conv2D(16,[3,3], activation='relu'),
  tf.keras.layers.GlobalAveragePooling2D(),
  tf.keras.layers.Dense(10)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
checkpoint_dir = 'path/to/model_dir'
if not os.path.exists(checkpoint_dir):
  os.makedirs(checkpoint_dir)
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
root = tf.train.Checkpoint(optimizer=optimizer,
                           model=model)

root.save(checkpoint_prefix)
root.restore(tf.train.latest_checkpoint(checkpoint_dir))

Замечание: Во многих обучающих циклах переменные создаются после вызова tf.train.Checkpoint.restore. Эти переменные будут восстановлены сразу же после создания и проверки того, что контрольная точка была загружена полностью. Подробнее см. руководство по чекпоинтам обучения.

Oбъектно-ориентированные метрики

tf.keras.metrics хранятся как объекты. Обновите метрику передав новые данные в вызываемый объект, и получите результат, используя метод tf.keras.metrics.result, например:


In [ ]:
m = tf.keras.metrics.Mean("loss")
m(0)
m(5)
m.result()  # => 2.5
m([8, 9])
m.result()  # => 5.5

Сводки и TensorBoard

TensorBoard это инструмент визуализации для понимания, отладки и оптимизации процесса обучения модели. Он использует события summary которые записываются во время работы программы.

Вы можете использовать tf.summary чтобы писать сводку переменной в eager execution. Например, чтобы записать сводные данные loss каждые 100 шагов обучения:


In [ ]:
logdir = "./tb/"
writer = tf.summary.create_file_writer(logdir)

steps = 1000
with writer.as_default():  # или вызовите writer.set_as_default() перед циклом.
  for i in range(steps):
    step = i + 1
    # Посчитайте потери с вашей реальной функцией обучения.
    loss = 1 - 0.001 * step
    if step % 100 == 0:
      tf.summary.scalar('loss', loss, step=step)

In [ ]:
!ls tb/

Продвинутые темы автоматического дифференцирования

Динамические модели

tf.GradientTape может быть также использован в динамических моделях. Это пример для backtracking line search несмотря на сложный порядок выполнения, алгоритм выглядит как обычный код NumPy, за исключением того что, там есть алгоритмы и дифференцирование:


In [ ]:
def line_search_step(fn, init_x, rate=1.0):
  with tf.GradientTape() as tape:
    # Переменные автоматически отслеживаются.
    # Но чтобы посчитать градиент от тензора, вам надо его `посмотреть (watch)`.
    tape.watch(init_x)
    value = fn(init_x)
  grad = tape.gradient(value, init_x)
  grad_norm = tf.reduce_sum(grad * grad)
  init_value = value
  while value > init_value - rate * grad_norm:
    x = init_x - rate * grad
    value = fn(x)
    rate /= 2.0
  return x, value

Кастомные градиенты

Кастомные градиенты это простой способ переписать градиенты. В функции прямого прохода, определите градиент относительно входных, выходных или промежуточных результатов. Например, вот легкий способ обрезать градиенты в обратном распространении:


In [ ]:
@tf.custom_gradient
def clip_gradient_by_norm(x, norm):
  y = tf.identity(x)
  def grad_fn(dresult):
    return [tf.clip_by_norm(dresult, norm), None]
  return y, grad_fn

Кастомные градиенты обычно используются для обеспечения численно стабильного градиента для последовательности операций:


In [ ]:
def log1pexp(x):
  return tf.math.log(1 + tf.exp(x))

def grad_log1pexp(x):
  with tf.GradientTape() as tape:
    tape.watch(x)
    value = log1pexp(x)
  return tape.gradient(value, x)

In [ ]:
# Вычисление градиента хорошо работает при x = 0.
grad_log1pexp(tf.constant(0.)).numpy()

In [ ]:
# Однако, x = 100 терпит неудачу из-за числовой нестабильности.
grad_log1pexp(tf.constant(100.)).numpy()

Здесь функция log1pexp может быть аналитически упрощена с помощью кастомного градиента. Нижеприведенная реализация переиспользует значение для tf.exp(x) которое вычисляется во время прямого прохода, делая ее эффективнее за счет исключения избыточных вычислений:


In [ ]:
@tf.custom_gradient
def log1pexp(x):
  e = tf.exp(x)
  def grad(dy):
    return dy * (1 - 1 / (1 + e))
  return tf.math.log(1 + e), grad

def grad_log1pexp(x):
  with tf.GradientTape() as tape:
    tape.watch(x)
    value = log1pexp(x)
  return tape.gradient(value, x)

In [ ]:
# Как и ранее вычисление градиента работает хорошо при x = 0.
grad_log1pexp(tf.constant(0.)).numpy()

In [ ]:
# И вычисление градиента также работает хорошо при x = 100.
grad_log1pexp(tf.constant(100.)).numpy()

Производительность

Вычисление автоматически выгружается в GPU во время eager execution. Если вы хотите контролировать, где выполняется вычисление, вы можете заключить его в блок tf.device('/gpu:0') (или эквивалент для CPU):


In [ ]:
import time

def measure(x, steps):
  # TensorFlow инициализирует GPU при первом использовании, исключим из времени.
  tf.matmul(x, x)
  start = time.time()
  for i in range(steps):
    x = tf.matmul(x, x)
  # tf.matmul может возвращаться до завершения умножения матрицы
  # (например, может возвращаться после включения операции в поток CUDA).
  # Вызов x.numpy() ниже гарантирует, что все операции в очереди 
  # были завершены (и также скопирует результат в память хоста,
  # поэтому мы включаем немного больше, чем просто время
  # операции matmul).
  _ = x.numpy()
  end = time.time()
  return end - start

shape = (1000, 1000)
steps = 200
print("Время на умножение {} матрицы на себя {} раз:".format(shape, steps))

# Выполнение на CPU:
with tf.device("/cpu:0"):
  print("CPU: {} secs".format(measure(tf.random.normal(shape), steps)))

# Выполнение на GPU, если возможно:
if tf.config.experimental.list_physical_devices("GPU"):
  with tf.device("/gpu:0"):
    print("GPU: {} secs".format(measure(tf.random.normal(shape), steps)))
else:
  print("GPU: не найдено")

Объект tf.Tensor может быть скопирован на другое устройство для выполнения его операции:


In [ ]:
if tf.config.experimental.list_physical_devices("GPU"):
  x = tf.random.normal([10, 10])

  x_gpu0 = x.gpu()
  x_cpu = x.cpu()

  _ = tf.matmul(x_cpu, x_cpu)    # Runs on CPU
  _ = tf.matmul(x_gpu0, x_gpu0)  # Runs on GPU:0

Бенчмарки

Для сложных вычислительных моделей, таких как ResNet50 обучение на GPU, производительность eager execution сравнима с выполнением tf.function. Но разрыв становится больше для моделей с меньшим числом вычислений и необходимо проделать работу по оптимизации кода для моделей с большим количеством маленьких операций.

Работа с функциями

Хоть eager execution делает разработку и отладку более интерактивной, выполнение графа в стиле TensorFlow 1.x имеет преимущества при распределенном обучении, оптимизации производительности и запуске в продакшн. Чтобы преодолеть этот пробел, TensorFlow 2.0 вводит function посредством API tf.function. Для дополнительной информации, см. руководство tf.function.