Eager execution

Смотрите на TensorFlow.org Запустите в Google Colab Изучайте код на GitHub Скачайте ноутбук

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

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

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

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

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

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

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 включено по умолчанию.

tf.executing_eagerly()
True

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

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

Включение 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.

a = tf.constant([[1, 2],
                 [3, 4]])
print(a)
tf.Tensor(
[[1 2]
 [3 4]], shape=(2, 2), dtype=int32)

# Поддержка вещания
b = tf.add(a, 1)
print(b)
tf.Tensor(
[[2 3]
 [4 5]], shape=(2, 2), dtype=int32)

# Поддерживается перегрузка операторов
print(a * b)
tf.Tensor(
[[ 2  6]
 [12 20]], shape=(2, 2), dtype=int32)

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

c = np.multiply(a, b)
print(c)
[[ 2  6]
 [12 20]]

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

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

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

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
fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

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

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

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

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

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

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

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)
tf.Tensor([[2.]], shape=(1, 1), dtype=float32)

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

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

# Получим и отформатируем данные 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)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step

# Построим модель
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:

for images,labels in dataset.take(1):
  print("Logits: ", mnist_model(images[0:1]).numpy())
Logits:  [[ 0.0061647  -0.03851003  0.01807109 -0.01619026  0.02618449 -0.05828483
  -0.01640506 -0.01885324 -0.02285448  0.00243616]]

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

optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

loss_history = []

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

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))
def train(epochs):
  for epoch in range(epochs):
    for (batch, (images, labels)) in enumerate(dataset):
      train_step(images, labels)
    print ('Epoch {} finished'.format(epoch))
train(epochs = 3)
Epoch 0 finished
Epoch 1 finished
Epoch 2 finished

import matplotlib.pyplot as plt

plt.plot(loss_history)
plt.xlabel('Batch #')
plt.ylabel('Loss [entropy]')
Text(0, 0.5, 'Loss [entropy]')

png

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

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

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

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

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
# Игрушечный датасет точек вокруг 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. Стратегия обновления переменных, основанная на производных.
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)))
Initial loss: 69.602
Loss at step 000: 66.862
Loss at step 020: 30.131
Loss at step 040: 13.866
Loss at step 060: 6.662
Loss at step 080: 3.472
Loss at step 100: 2.059
Loss at step 120: 1.433
Loss at step 140: 1.156
Loss at step 160: 1.033
Loss at step 180: 0.978
Loss at step 200: 0.954
Loss at step 220: 0.943
Loss at step 240: 0.939
Loss at step 260: 0.937
Loss at step 280: 0.936

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

print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))
W = 3.0192530155181885, B = 2.0055668354034424

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

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

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

model.save_weights('weights')
status = model.load_weights('weights')

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

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

x = tf.Variable(10.)
checkpoint = tf.train.Checkpoint(x=x)
x.assign(2.)   # Присвоим новое значение переменной и сохраним.
checkpoint_path = './ckpt/'
checkpoint.save('./ckpt/')
'./ckpt/-1'
x.assign(11.)  # Изменим переменную после сохранения.

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

print(x)  # => 2.0
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=2.0>

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

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))
<tensorflow.python.training.tracking.util.CheckpointLoadStatus at 0x7f1f60062b00>

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

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

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

m = tf.keras.metrics.Mean("loss")
m(0)
m(5)
m.result()  # => 2.5
m([8, 9])
m.result()  # => 5.5
<tf.Tensor: shape=(), dtype=float32, numpy=5.5>

Сводки и TensorBoard

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

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

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)
ls tb/
events.out.tfevents.1600798613.kokoro-gcp-ubuntu-prod-1727334007.322.619697.v2

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

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

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

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

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

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

@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

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

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)

# Вычисление градиента хорошо работает при x = 0.
grad_log1pexp(tf.constant(0.)).numpy()
0.5
# Однако, x = 100 терпит неудачу из-за числовой нестабильности.
grad_log1pexp(tf.constant(100.)).numpy()
nan

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

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

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

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

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

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: не найдено")
Время на умножение (1000, 1000) матрицы на себя 200 раз:
CPU: 0.7265706062316895 secs
GPU: 0.04129743576049805 secs

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

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
WARNING:tensorflow:From <ipython-input-1-876293b5769c>:4: _EagerTensorBase.gpu (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.identity instead.
WARNING:tensorflow:From <ipython-input-1-876293b5769c>:5: _EagerTensorBase.cpu (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.identity instead.

Бенчмарки

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

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

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