вторник, 18 сентября 2018 г.

TensorFlow: стремительное исполнение (eager execution), базовое использование

Стремительное исполнение (Eager Execution)

Стремительное исполнение в TensorFlow - это императивное программное окружение, которое оценивает операции незамедлительно, без построения графов: операции возвращают конкретные значения, вместо построения вычислительного графа для дальнейшего исполнения. Такое окружение упрощает начало работы с TensorFlow и отладку моделей. Примеры кода в этом руководстве следует исполнять в интерактивном python интерпретаторе.

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

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

Стремительное исполнение поддерживает большинство TensorFlow операций и GPU ускорение.

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

Обновляем TensorFlow до последней версии

$ pip install --upgrade tensorflow

Для начала использования стремительного исполнения добавим tf.enable_eager_execution() в начало программы или консольной сессии. Не добавляйте эту операцию в другие модули, которые вызывает программа.

from future import absolute_import, division, print_function

import tensorflow as tf

tf.enable_eager_execution()

Теперь можно выполнять TensorFlow операции и результаты будут возвращаться незамедлительно:

tf.executing_eagerly()        # => True

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

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

Стремительное исполнение хорошо работает с NumPy. NumPy операции принимают tf.Tensor аргументы. TensorFlow математические операции преобразуют Python объекты и NumPy массивы в tf.Tensor объекты. tf.Tensor.numpy метод возвращает значение объекта как NumPy ndarray.

При исполнении кода с русскоязычным комментарием не забывайте удалять комментарий либо выставлять строку # -*- coding: utf-8 -*- в начале скрипта.

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

# Поддержка broadcasting
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]]

tf.contrib.eager модуль содержит символы, доступные для обоих окружений - стремительного исполнения и графового исполнения - этот модуль удобен при написания кода для работы с графами:

tfe = tf.contrib.eager

Динамический поток контроля

Главное преимущество стремительного исполнения в том, что вся функциональность языка хоста доступна во время исполнения модели. Так, например, можно легко написать fizzbuzz скрипт (классическая тестовая программа для разделения чисел, передаваемых ей, как кратных заданным критериям (в данном случае 3 и 5)):

def fizzbuzz(max_num):
  counter = tf.constant(0)
  max_num = tf.convert_to_tensor(max_num)
  for num in range(max_num.numpy()):
    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)
    counter += 1
  return counter

Эта программа имеет условные выражения, которые зависят от значений тензора, и печатает эти значения во время исполнения.

Создание модели

Многие модели машинного обучения создаются составлением слоев друг на друга. При использовании TensorFlow со стремительным исполнением можно либо писать собственные слои, либо использовать слои, предоставленные в tf.keras.layers пакете.

Хотя можно использовать любой Python объект для представления слоя, TensorFlow имеет tf.keras.layers.Layer в качестве удобного базового класса. Наследуемся от него для реализации собственного слоя:

class MySimpleLayer(tf.keras.layers.Layer):
  def __init__(self, output_units):
    super(MySimpleLayer, self).__init__()
    self.output_units = output_units

  def build(self, input_shape):
    # build метод вызывается только при первом использовании слоя.
    # Создание переменных в build() позволяет сделать их форму 
    # зависимой от вводной формы и следовательно устраняет
    # необходимость для пользователя определять полные формы.
    # Возможно создать переменные в __init__()
    # если полные формы уже известны.
    self.kernel = self.add_variable(
      "kernel", [input_shape[-1], self.output_units])

  def call(self, input):
    # Переопределим call() вместо __call__ так мы сможем выполнить
    # некоторый подсчет.
    return tf.matmul(input, self.kernel)

Используем tf.keras.layers.Dense слой вместо MySimpleLayer выше, поскольку он имеет набор необходимой функциональности (он также может добавлять смещение).

При составлении слоев в модели можно использовать tf.keras.Sequential для определения моделей, которые представляют собой линейный стек слоев. Это можно легко использовать для базовых моделей:

model = tf.keras.Sequential([
  # необходимо определить вводную форму
  tf.keras.layers.Dense(10, input_shape=(784,)),
  tf.keras.layers.Dense(10)
])

Альтернативно, можно организовывать модели в классы наследованием от tf.keras.Model. Это контейнер для слоев, который сам является слоем, позволяя tf.keras.Model объектам содержать другие tf.keras.Model объекты.

class MNISTModel(tf.keras.Model):
  def init(self):
    super(MNISTModel, self).init()
    self.dense1 = tf.keras.layers.Dense(units=10)
    self.dense2 = tf.keras.layers.Dense(units=10)

  def call(self, input):
    """Исполняет модель."""
    result = self.dense1(input)
    result = self.dense2(result)
    # переиспользуем переменные для dense2 слоя
    result = self.dense2(result)
    return result

model = MNISTModel()

Не обязательно устанавливать вводную форму для tf.keras.Model класса, ввиду того, что параметры устанавливают первый ввод, переданный слою.

tf.keras.layers классы создают и содержат их собственные переменные модели, которые связаны со временем жизни объектов их слоя. Чтобы разделять переменные слоя, разделять их объекты.

Тренировка при стремительном исполнении

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

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

tf.GradientTape - это отключаемое свойство для предоставления максимальной производительности, когда нет отслеживания. Ввиду того, что разные операции могут выполниться в ходе каждого вызова, все проходящие операции записаны на "ленту" ("tape"). Для вычисления градиента идем по ленте назад и затем останавливаемся. Отдельный tf.GradientTape может вычислить только один градиент; последовательные вызовы выбрасывают ошибки исполнения.

w = tfe.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.GradientTape, которая записывает прошедшие операции для тренировки простой модели:

# Маленький набор данных точек вокруг 3 * x + 2
NUM_EXAMPLES = 1000
training_inputs = tf.random_normal([NUM_EXAMPLES])
noise = tf.random_normal([NUM_EXAMPLES])
training_outputs = training_inputs * 3 + 2 + noise

def prediction(input, weight, bias):
  return input * weight + bias

# Функция потери, использующая среднюю квадратичную ошибку
def loss(weights, biases):
  error = prediction(training_inputs, weights, biases) - training_outputs
  return tf.reduce_mean(tf.square(error))

# Возвращает производную потери с учетом веса и смещения
def grad(weights, biases):
  with tf.GradientTape() as tape:
    loss_value = loss(weights, biases)
  return tape.gradient(loss_value, [weights, biases])

train_steps = 200
learning_rate = 0.01
#  Начинаем с произвольных значений для W и B в том же пакете данных
W = tfe.Variable(5.)
B = tfe.Variable(10.)

print("Initial loss: {:.3f}".format(loss(W, B)))

for i in range(train_steps):
  dW, dB = grad(W, B)
  W.assign_sub(dW * learning_rate)
  B.assign_sub(dB * learning_rate)
  if i % 20 == 0:
    print("Loss at step {:03d}: {:.3f}".format(i, loss(W, B)))

print("Final loss: {:.3f}".format(loss(W, B)))
print("W = {}, B = {}".format(W.numpy(), B.numpy()))

Вывод (точные значения могут отличаться):

Initial loss: 71.204
Loss at step 000: 68.333
Loss at step 020: 30.222
Loss at step 040: 13.691
Loss at step 060: 6.508
Loss at step 080: 3.382
Loss at step 100: 2.018
Loss at step 120: 1.422
Loss at step 140: 1.161
Loss at step 160: 1.046
Loss at step 180: 0.996
Final loss: 0.974
W = 3.01582956314, B = 2.1191945076

Перезапустим tf.GradientTape чтобы вычислить градиенты и применить их в тренировочном цикле. Здесь продемонстрирован отрывок из mnist_eager.py примера:

dataset = tf.data.Dataset.from_tensor_slices((data.train.images,
                                              data.train.labels))
...
for (batch, (images, labels)) in enumerate(dataset):
  ...
  with tf.GradientTape() as tape:
    logits = model(images, training=True)
    loss_value = loss(logits, labels)
  ...
  grads = tape.gradient(loss_value, model.variables)
  optimizer.apply_gradients(zip(grads, model.variables),
                       global_step=tf.train.get_or_create_global_step())

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

Тренировка модели

Даже без тренировки вызовем модель и оценим вывод при стремительном исполнении:

# Создаем тензор, представляющий пустое изобржение
batch = tf.zeros([1, 1, 784])
print(batch.shape)  # => (1, 1, 784)

result = model(batch)
# => tf.Tensor([[[ 0.  0., ..., 0.]]], shape=(1, 1, 10), dtype=float32)

Этот пример использует dataset.py модуль и TensorFlow MNIST примера; загрузим этот файл в локальную директорию. Выполним следующее для загрузки MNIST файлов данных в рабочую директорию и подготовим tf.data.Dataset для тренировки:

import dataset # загружаем dataset.py файл
dataset_train = dataset.train('./datasets').shuffle(60000).repeat(4).batch(32)

Для тренировки модели определим функцию потери для оптимизации и затем вычислим градиенты. Используем оптимизатор для обновления переменных:

def loss(model, x, y):
  prediction = model(x)
  return tf.losses.sparse_softmax_cross_entropy(labels=y, 
                                                logits=prediction)

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

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)

x, y = iter(dataset_train).next()
print("Initial loss: {:.3f}".format(loss(model, x, y)))

# тренировочный цикл
for (i, (x, y)) in enumerate(dataset_train):
  # Вычисляем производные вводной функции с учетом ее параметров.
  grads = grad(model, x, y)
  # Применяем градиенты к модели
  optimizer.apply_gradients(zip(grads, model.variables),
                       global_step=tf.train.get_or_create_global_step())
  if i % 200 == 0:
    print("Loss at step {:04d}: {:.3f}".format(i, loss(model, x, y)))

print("Final loss: {:.3f}".format(loss(model, x, y)))

Вывод (точные значения могут отличаться):

Initial loss: 2.674
Loss at step 0000: 2.593
Loss at step 0200: 2.143
Loss at step 0400: 2.009
Loss at step 0600: 2.103
Loss at step 0800: 1.621
Loss at step 1000: 1.695
...
Loss at step 6600: 0.602
Loss at step 6800: 0.557
Loss at step 7000: 0.499
Loss at step 7200: 0.744
Loss at step 7400: 0.681
Final loss: 0.670

А для более быстрой тренировки перенесем вычисления на GPU:

with tf.device("/gpu:0"):
  for (i, (x, y)) in enumerate(dataset_train):
    # minimize() это эквивалент grad() и apply_gradients() вызовов.
    optimizer.minimize(lambda: loss(model, x, y),
                       global_step=tf.train.get_or_create_global_step())

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

tfe.Variable объекты хранят изменяемые tf.Tensor значения, к которым происходит обращение в ходе тренировки, чтобы сделать автоматическую дифференциацию более простой. Параметры модели могут быть инкапсулированы в классы как переменные.

Лучше инкапсулировать параметры модели, используя tfe.Variable с tf.GradientTape. Пример автоматической дифференциации выше может быть переписан:

class Model(tf.keras.Model):
  def __init__(self):
    super(Model, self).__init__()
    self.W = tfe.Variable(5., name='weight')
    self.B = tfe.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

# Функция потери для оптимизации
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 = Model()
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)

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

# тренировочный цикл
for i in range(300):
  grads = grad(model, training_inputs, training_outputs)
  optimizer.apply_gradients(zip(grads, [model.W, model.B]),
                     global_step=tf.train.get_or_create_global_step())
  if i % 20 == 0:
    print("Loss at step {:03d}: {:.3f}".format(i, loss(model, 
          training_inputs, 
          training_outputs)))

print("Final loss: {:.3f}".format(loss(model, training_inputs, 
      training_outputs)))
print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))

Вывод (точные значения могут отличаться):

Initial loss: 69.066
Loss at step 000: 66.368
Loss at step 020: 30.107
Loss at step 040: 13.959
Loss at step 060: 6.769
Loss at step 080: 3.567
Loss at step 100: 2.141
Loss at step 120: 1.506
Loss at step 140: 1.223
Loss at step 160: 1.097
Loss at step 180: 1.041
Loss at step 200: 1.016
Loss at step 220: 1.005
Loss at step 240: 1.000
Loss at step 260: 0.998
Loss at step 280: 0.997
Final loss: 0.996
W = 2.99431324005, B = 2.02129220963

Другие примеры использования стремительного исполнения в TensorFlow читайте в следующем посте.