воскресенье, 30 сентября 2018 г.

TensorFlow: импорт данных - предобработка данных

Предобработка данных с Dataset.map()

Dataset.map(f) преобразование производит новый набор данных применением переданной функции f к каждому элементу набора данных ввода. Оно основано на map() функции, которая в основном применяется к листам (и другим структурам) в функциональных языках программирования. Функция f принимает tf.Tensor объекты, которые представляют единичный элемент ввода, и возвращает tf.Tensor объекты, которые представляют единичный элемент в новом датасете. Эта реализация использует стандартные TensorFlow операции для преобразования одного элемента в другой.

Этот раздел описывает общие примеры того как использовать Dataset.map().

Парсинг сообщений буфера tf.Example протокола

Многие пайплайны ввода извлекают сообщения буфера протокола tf.train.Example из файла TFRecord формата (записанный, например, используя tf.python_io.TFRecordWriter). Каждая tf.train.Example запись содержит одно или несколько "свойств", и пайплайн ввода обычно преобразует эти свойства в тензоры.

# Преобразуем скалярную строку `example_proto` в пару скалярной строки
# и скалярное число, представляющую изображение и метку, соответственно.
def _parse_function(example_proto):
  features = 
        {"image": tf.FixedLenFeature((), tf.string, default_value=""),
         "label": tf.FixedLenFeature((), tf.int64, default_value=0)}
  parsed_features = tf.parse_single_example(example_proto, features)
  return parsed_features["image"], parsed_features["label"]

# Создаем датасет, который читает все примеры из двух файлов,
# и извлекает изображение и метку.
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(_parse_function)

Декодирование данных изображения и изменение его размера

При тренировке нейронной сети на данных настоящих изображений часто необходимо преобразовать изображения разных размеров к общему размеру, таким образом они могут быть объединены в пакеты в фиксированном размере.

# Читаем изображение из файла, декодируем его в тесносвязанный тензор
# и меняем размер к фиксированной форме
def _parse_function(filename, label):
  image_string = tf.read_file(filename)
  image_decoded = tf.image.decode_jpeg(image_string)
  image_resized = tf.image.resize_images(image_decoded, [28, 28])
  return image_resized, label

# Вектор имен файлов.
filenames = tf.constant(["/var/data/image1.jpg", 
                         "/var/data/image2.jpg", ...])

# `labels[i]` метка для изображения в `filenames[i].
labels = tf.constant([0, 37, ...])

dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.map(_parse_function)

Применение произвольной Python логики с tf.py_func()

По причинам производительности рекомендуется использовать TensorFlow операции для предобработки данных когда это возможно. Однако, иногда удобно вызывать внешние Python библиотеки при парсинге вводных данных. Чтобы выполнить это используем tf.py_func() операцию в Dataset.map() трансформации.

import cv2

# Используем кастомную OpenCV функцию для чтения изображения
# вместо стандартной TensorFlow `tf.read_file()` операции
def _read_py_function(filename, label):
  image_decoded = cv2.imread(filename.decode(), cv2.IMREAD_GRAYSCALE)
  return image_decoded, label

# Используем стандартные TensorFlow операции 
# для изменения размера изображения к фиксированной форме
def _resize_function(image_decoded, label):
  image_decoded.set_shape([None, None, None])
  image_resized = tf.image.resize_images(image_decoded, [28, 28])
  return image_resized, label

filenames = ["/var/data/image1.jpg", "/var/data/image2.jpg", ...]
labels = [0, 37, 29, 1, ...]

dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.map(
    lambda filename, label: tuple(tf.py_func(
        _read_py_function, [filename, label], [tf.uint8, label.dtype])))
dataset = dataset.map(_resize_function)

Пакетизация элементов датасета

Простая пакетизация

Простейшая пакетизация складывает n последовательных элементов датасета в один элемент. Dataset.batch() трансформация делает именно это, с теми ограничениями что и tf.stack() оператор, применяемый к каждому компоненту элементов: то есть для каждого компонента i все элементы должны иметь тензор точно такой же формы.

inc_dataset = tf.data.Dataset.range(100)
dec_dataset = tf.data.Dataset.range(0, -100, -1)
dataset = tf.data.Dataset.zip((inc_dataset, dec_dataset))
batched_dataset = dataset.batch(4)

iterator = batched_dataset.make_one_shot_iterator()
next_element = iterator.get_next()

print(sess.run(next_element))  
# ==> ([0, 1, 2,   3],   [ 0, -1,  -2,  -3])

print(sess.run(next_element))  
# ==> ([4, 5, 6,   7],   [-4, -5,  -6,  -7])

print(sess.run(next_element))  
# ==> ([8, 9, 10, 11],   [-8, -9, -10, -11])

Пакетизация тензоров с отступом

Приведенный выше пример работает для тензоров, которые имеют одинаковый размер. Однако, многие модели (например, последовательные модели) работают с вводными данными, которые могут иметь разный размер (например, последовательности различной длины). Для обработки таких случаев Dataset.padded_batch() трансформация позволяет пакетизировать тензоры различной формы определяя одно или несколько измерений, в которых может быть выставлен отступ.

dataset = tf.data.Dataset.range(100)
dataset = dataset.map(lambda x: tf.fill([tf.cast(x, tf.int32)], x))
dataset = dataset.padded_batch(4, padded_shapes=[None])

iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()

print(sess.run(next_element))  
# ==> [[0, 0, 0], [1, 0, 0], [2, 2, 0], [3, 3, 3]]

print(sess.run(next_element))  # ==> [[4, 4, 4, 4, 0, 0, 0],
                               #      [5, 5, 5, 5, 5, 0, 0],
                               #      [6, 6, 6, 6, 6, 6, 0],
                               #      [7, 7, 7, 7, 7, 7, 7]]

Dataset.padded_batch() трансформация позволяет устанавливать различный отступ для каждого измерения каждого компонента, и он может быть переменной длины (означено None в примере выше) или постоянной длины. Также возможно переопределять значение в отступе (по умолчанию оно равно нулю).

Рабочие потоки тренировки

Обработка нескольких эпох

tf.data API предлагает два основных пути для обработки нескольких эпох тех же данных.

Постейший путь итерации датасета в нескольких эпохах - это использование Dataset.repeat() трансформации. Например, чтобы создать датасет, который повторяет его ввод в течение 10 эпох:

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.repeat(10)
dataset = dataset.batch(32)

Применение Dataset.repeat() трансформации без аргументов будет повторять ввод. Dataset.repeat() трансформация соединяет аргументы без обозначения конца каждой эпохи и начала следующей эпохи.

Если необходимо получать сигнал в конце каждой эпохи можно написать тренировочный цикл, который отлавливает tf.errors.OutOfRangeError исключение в конце датасета. В этой точке можно собирать статистику (например, валидационную ошибку) для эпохи.

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.batch(32)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()

# Вычисления для 100 эпох
for _ in range(100):
  sess.run(iterator.initializer)
  while True:
    try:
      sess.run(next_element)
    except tf.errors.OutOfRangeError:
      break

  # [Выполняем вычисления в конце эпохи здесь]

Перемешивание данных ввода в случайном порядке

Dataset.shuffle() преобразование перемешивает в случайном порядке вводный датасет, используя схожий алгоритм для tf.RandomShuffleQueue: он содержит буфер фиксированного размера и выбирает следующий элемент равномерно в случайном порядке из этого буфера.

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Использование высокоуровневых API

tf.train.MonitoredTrainingSession API упрощает многие аспекты исполнения TensorFlow в распределенной установке. MonitoredTrainingSession использует tf.errors.OutOfRangeError для обозначения окончания тренировки, таким образом для его использования с tf.data API рекомендуется использовать Dataset.make_one_shot_iterator(). Например:

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.batch(32)
dataset = dataset.repeat(num_epochs)
iterator = dataset.make_one_shot_iterator()

next_example, next_label = iterator.get_next()
loss = model_function(next_example, next_label)

training_op = tf.train.AdagradOptimizer(...).minimize(loss)

with tf.train.MonitoredTrainingSession(...) as sess:
  while not sess.should_stop():
    sess.run(training_op)

Для использования Dataset в input_fn tf.estimator.Estimator просто возвращаем Dataset и фреймворк позаботится о создании итератора и его инициализировании. Например:

def dataset_input_fn():
  filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
  dataset = tf.data.TFRecordDataset(filenames)

  # Используем `tf.parse_single_example()` для извлечения данных из 
  # буфера протокола `tf.Example`, и выполнения любой дополнительной
  # предобработки для каждой записи.
  def parser(record):
    keys_to_features = {
        "image_data": tf.FixedLenFeature(
                          (), tf.string, default_value=""),
        "date_time": tf.FixedLenFeature((), tf.int64, default_value=""),
        "label": tf.FixedLenFeature((), tf.int64,
                           default_value=tf.zeros([], dtype=tf.int64)),
    }
    parsed = tf.parse_single_example(record, keys_to_features)

    # Выполняем дополнительную предобработку для полученных данных
    image = tf.image.decode_jpeg(parsed["image_data"])
    image = tf.reshape(image, [299, 299, 1])
    label = tf.cast(parsed["label"], tf.int32)

    return {"image_data": image, "date_time": parsed["date_time"]},\
           label

  # Используем `Dataset.map()`для построения пары словаря свойства 
  # и метки тензора для каждого примера.
  dataset = dataset.map(parser)
  dataset = dataset.shuffle(buffer_size=10000)
  dataset = dataset.batch(32)
  dataset = dataset.repeat(num_epochs)

  # Каждый элемент `dataset` это тапл(tuple), содержащий словарь свойств
  # (в котором каждое значение - это пакет значений для этого свойства)
  # и пакет меток.
  return dataset

TensorFlow: импорт данных - чтение данных

Чтение вводных данных

Получение NumPy массивов

Если все вводные данные помещаются в память, тогда простейший путь создать Dataset из данных - это конвертировать их в tf.Tensor объекты и использовать Dataset.from_tensor_slices().

# Загружаем тренировочные данные в два NumPy массива,
# используя np.load()
with np.load("/var/data/training_data.npy") as data:
  features = data["features"]
  labels = data["labels"]

# Предполагаем, что каждая строка массива features
# соответствует такой же строке массива labels
assert features.shape[0] == labels.shape[0]

dataset = tf.data.Dataset.from_tensor_slices((features, labels))

Следует отметить, что пример кода выше встроит массивы свойств и меток в TensorFlow граф как tf.constant() операции. Это работает хорошо на небольших датасетах, но тратит напрасно память, потому что содержимое массива будет скопировано много раз, и может достичь 2GB лимита для буфера tf.GraphDef протокола.

В качестве альтернативы, можно определить Dataset с точки зрения tf.placeholder() тензоров и передать NumPy массивы при инициализации Iterator для датасета.

# Загружаем тренировочные данные в два NumPy массива,
# используя `np.load()`.
with np.load("/var/data/training_data.npy") as data:
  features = data["features"]
  labels = data["labels"]

# Предполагаем, что каждая строка массива features
# соответствует такой же строке массива labels
assert features.shape[0] == labels.shape[0]

features_placeholder = tf.placeholder(features.dtype, features.shape)
labels_placeholder = tf.placeholder(labels.dtype, labels.shape)

dataset = tf.data.Dataset.from_tensor_slices(
                          (features_placeholder, labels_placeholder))
# [Другие трансформации `dataset`...]
dataset = ...
iterator = dataset.make_initializable_iterator()

sess.run(iterator.initializer, feed_dict={features_placeholder: features,
                                          labels_placeholder: labels})

Получение TFRecord данных

tf.data API поддерживает различные форматы файлов, так можно обрабатывать большие наборы данных, которые не помещаются в память. Например, TFRecord формат - это простой ориентированный на записи бинарный формат, который многие TensorFlow приложения используют для тренировочных данных. tf.data.TFRecordDataset класс позволяет проходить по содержимому одного или нескольких TFRecord файлов как часть пайплайна ввода.

# Создаем датасет, который читает все примеры из двух файлов.
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)

filenames (имена файлов) аргумент для TFRecordDataset инициализатора может быть строкой, списком строк, или tf.Tensor строк. Таким образом можно иметь два набора файлов для целей тренировки и валидации, можно использовать tf.placeholder(tf.string) для представления filenames (имен файлов), и инициализировать итератор из подходящих имен файлов:

filenames = tf.placeholder(tf.string, shape=[None])
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)  # Парсим запись в тензоры.
# Повторяем ввод неопределенное количество раз
dataset = dataset.repeat()  
dataset = dataset.batch(32)
iterator = dataset.make_initializable_iterator()

# Можно наполнить инициализатор подходящими именами файлов
# для текущей фазы исполнения (например, тренировки или валидации)

# Инициализируем `iterator` с тренировочными данными.
training_filenames = ["/var/data/file1.tfrecord", 
                      "/var/data/file2.tfrecord"]
sess.run(iterator.initializer, 
         feed_dict={filenames: training_filenames})

# Инициализируем `iterator` с валидационными данными.
validation_filenames = ["/var/data/validation1.tfrecord", ...]
sess.run(iterator.initializer, 
         feed_dict={filenames: validation_filenames})

Получение текстовых данных

Многие наборы данных поставляются как один или несколько текстовых файлов. tf.data.TextLineDataset предоставляет легкий путь извлечения строк из одного или нескольких файлов. Получив одно или несколько имен файлов, TextLineDataset производит один элемент строкового значения на каждую строку этих файлов. Как TFRecordDataset, TextLineDataset принимает имена файлов как tf.Tensor, таким образом можно параметризировать его передавая tf.placeholder(tf.string).

filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]
dataset = tf.data.TextLineDataset(filenames)

По умолчанию, TextLineDataset выводит каждую строку каждого файла, что может быть нежелательным, например если файл начинается с линии заголовка или содержит комментарии. Эти строки могут быть удалены использованием Dataset.skip() и Dataset.filter() преобразований. Для применения преобразований к каждому файлу отдельно используем Dataset.flat_map() для создания вложенного Dataset для каждого файла.

filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]

dataset = tf.data.Dataset.from_tensor_slices(filenames)

# Используем `Dataset.flat_map()`, чтобы преобразовать 
# каждый файл как отдельный вложенный датасет,
# и затем соединяем вместе их содержимое 
# последовательно в один "плоский" датасет.
# * Пропускаем первую строку (линия заголовка)
# * Фильтруем строки, начаниющиеся с "#" (комментарии).
dataset = dataset.flat_map(
    lambda filename: (
        tf.data.TextLineDataset(filename)
        .skip(1)
        .filter(lambda line: tf.not_equal(tf.substr(line, 0, 1), "#"))))

Получение CSV данных

CSV формат файлов - популярный формат для сохранения табличных данных в простой текст. tf.contrib.data.CsvDataset класс предоставляет путь извлечения записей из одного или нескольких CSV файлов, которые соотвествуют RFC 4180. Получив одно или несколько имен файлов и лист типа данных по умолчанию, CsvDataset производит tuple элементов, типы которых соответствуют типам, предоставленным по умолчанию, для каждой CSV записи. Как TFRecordDataset и TextLineDataset, CsvDataset принимает имена файлов как tf.Tensor, таким образом можно параметризировать его передавая tf.placeholder(tf.string).

# Создаем датасет, который читает все записи из двух CSV файлов
# каждый c 8 колонками нецельночисловых значений
filenames = ["/var/data/file1.csv", "/var/data/file2.csv"]
# 8 требуемых колонок нецельночисловых данных
record_defaults = [tf.float32] * 8   
dataset = tf.contrib.data.CsvDataset(filenames, record_defaults)

Если некоторые колонки пустые можно предоставить значения по умолчанию.

# Создаем датасет, который читает все записи из двух CSV файлов
# каждый c 4 колонками нецельночисловых значений,
# которые могут иметь пропущенные значения
record_defaults = [[0.0]] * 8
dataset = tf.contrib.data.CsvDataset(filenames, record_defaults)

По умолчанию CsvDataset выводит каждую колонку каждой строки файла, что может быть нежелательным, например если файл начинается с линии заголовка, которая должна быть пропущена, или некоторые колонки не требуются во вводе. Эти строки и поля могут быть удалены с header и select_cols аргументами соответственно.

# Создаем датасет, который читает все записи из двух CSV файлов
# с заголовками, извлевая нецельночисловые данные из 2 и 4 колонок.
# Предоставляем значения по умолчанию только для выбранных колонок.
record_defaults = [[0.0]] * 2  
dataset = tf.contrib.data.CsvDataset(filenames, record_defaults, 
                                     header=True, select_cols=[2,4])

четверг, 27 сентября 2018 г.

TensorFlow: импорт данных - базовая механика, итераторы

Импортирование данных

tf.data API позволяет создавать комплексные пайплайны ввода из простых, переиспользуемых частей. Например, пайплайн для модели изображений может собирать данные из файлов в распределенной файловой системе, применять преобразования к каждому изображению, и соединять взятые в случайном порядке изображения в пакеты для тренировки. Пайплайн для текстовой модели может включать извлечение символов из сырых текстовых данных, их преобразование для встраивания идентификаторов в поисковую таблицу, и пакетизация последовательностей различной длины. tf.data API позволяет легко обрабатывать большие количества данных, различные форматы данных, и выполнять сложные трансформации.

tf.data API вводит две новые абстракции в TensorFlow:

  • tf.data.Dataset представляет последовательность элементов, в которой каждый элемент содержит один или более Tensor объектов. Например, в пайплайне изображений элемент может быть единичным тренировочным примером с парой тензоров, представляющих данные изображения и метку. Существует два разных способа создать набор данных (dataset):
    • Создание источника (Dataset.from_tensor_slices()) конструирует набор данных (dataset) из одного или более tf.Tensor объектов.
    • Применение трансформации (Dataset.batch()) конструирует набор данных (dataset) из одного или более tf.data.Dataset объектов.
  • tf.data.Iterator предоставляет основной способ извлечения элементов из набора данных (dataset). Операция, возвращаемая Iterator.get_next() выводит следующий элемент Dataset, когда исполняется, и обычно действует как интерфейс между пайпланом ввода и моделью. Простейший итератор - это "одноразовый итератор", который связан с отдельным Dataset и итерирует по нему один раз. Для более совершенного использования, Iterator.initializer операция позволяет реинициализировать и параметризировать итератор с различными наборами данных (datasets), так что можно, например, итерировать тренировочные и валидационные данные много раз в той же программе.

Базовая механика

Эта часть описывает основы создания разных видов Dataset и Iterator объектов, и как извлекать данные из них.

Для начала пайплайна ввода необходимо определить источник. Например, чтобы сконструировать Dataset из нескольких тензоров в памяти можно использовать tf.data.Dataset.from_tensors() или tf.data.Dataset.from_tensor_slices(). С другой стороны, если вводные данные на диске в рекомендуемом TFRecord формате, можно сконструировать tf.data.TFRecordDataset.

Как только получили Dataset объект, можно трансформировать его в новый Dataset, используя цепь вызовов методов на tf.data.Dataset объекте. Например, можно применить по-элементные трансформации, такие как Dataset.map() (для применения функции к каждому элементу), и много-элементные трансформации, такие как Dataset.batch().

Наиболее общий путь для получения данных от Dataset - это сделать объект-итератор, который предоставляет доступ к одному элемену набора (dataset'а) за один раз (например, вызвав Dataset.make_one_shot_iterator()). tf.data.Iterator предоставляет две операции: Iterator.initializer, который позволяет (ре)инициализировать состояние итератора; и Iterator.get_next(), который возвращает tf.Tensor объекты, которые соответствуют символически следующему элементу.

Структура Dataset

Dataset включает в себя элементы, каждый из которых имеет одинаковую структуру. Элемент содержит один или более tf.Tensor объектов, называемых компонентами. Каждый компонент имеет tf.DType, представляющий тип элементов в тензоре, и tf.TensorShape представляющий (возможно частично определенную) статическую форму каждого элемента. Dataset.output_types и Dataset.output_shapes свойства позволяют проверять типы и формы каждого компонента элемента набора данных (dataset). Вложенная структура этих свойств соотносится со структурой элемента, который может быть единичным тензором, таплом (tuple - тип структуры данных в python) тензоров, или вложенным таплом тензоров. Например:

dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
print(dataset1.output_types)  # ==> "tf.float32"
print(dataset1.output_shapes)  # ==> "(10,)"

dataset2 = tf.data.Dataset.from_tensor_slices(
   (tf.random_uniform([4]),
    tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
print(dataset2.output_types)  # ==> "(tf.float32, tf.int32)"
print(dataset2.output_shapes)  # ==> "((), (100,))"

dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
print(dataset3.output_types)  # ==> (tf.float32, (tf.float32, tf.int32))
print(dataset3.output_shapes)  # ==> "(10, ((), (100,)))"

Часто удобно давать имена каждому компоненту элемента, например если они представляют разные свойства тренировочного примера. Вдобавок к таплам (tuples) можно использовать collections.namedtuple или словарь, картирующий строки к тензорам для представления одного элемента в Dataset.

dataset = tf.data.Dataset.from_tensor_slices(
   {"a": tf.random_uniform([4]),
    "b": tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)})
print(dataset.output_types)  # ==> "{'a': tf.float32, 'b': tf.int32}"
print(dataset.output_shapes)  # ==> "{'a': (), 'b': (100,)}"

Dataset трансформации поддерживают наборы данных (datasets) любой структуры. При использовании Dataset.map(), Dataset.flat_map(), и Dataset.filter() трансформаций, которые применяют функцию к каждому элементу, структура элементов определяется аргументами функции:

dataset1 = dataset1.map(lambda x: ...)

dataset2 = dataset2.flat_map(lambda x, y: ...)

dataset3 = dataset3.filter(lambda x, (y, z): ...)

Создание итератора

Как только построен Dataset для представления вводных данных, следующий шаг - создать Iterator для доступа к элементам из этого набора данных (dataset). tf.data API на данный момент поддерживает следующие итераторы, в порядке увеличения сложности:

  • одноразовый (one-shot)
  • инициализируемый (initializable)
  • реинициализируемый (reinitializable)
  • загрузочный (feedable)

Одноразовый (one-shot) итератор - это простейшая форма итератора, которая поддерживает только однократное итерирование по набору данных (dataset), без необходимости явной инициализации. Одноразовые итераторы обрабатывают почти все случаи, которые существуют в поддерживаемых пайплайнах ввода, основанных на очереди. Но они не поддерживают параметризацию. Пример использования Dataset.range():

dataset = tf.data.Dataset.range(100)
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()

for i in range(100):
  value = sess.run(next_element)
  assert i == value

Инициализируемый (initializable) итератор требует выполнения явной iterator.initializer операции перед его использованием. В обмен на такое неудобство это позволяет параметризировать определение набора данных (dataset), используя один или более tf.placeholder() тензоров, которые могут быть переданы при инициализации итератора. Продолжая Dataset.range() пример:

max_value = tf.placeholder(tf.int64, shape=[])
dataset = tf.data.Dataset.range(max_value)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()

# Инициализируем итератор для набора данных с 10 элементами.
sess.run(iterator.initializer, feed_dict={max_value: 10})
for i in range(10):
  value = sess.run(next_element)
  assert i == value

# Инициализируем тот же итератор для набора данных со 100 элементами.
sess.run(iterator.initializer, feed_dict={max_value: 100})
for i in range(100):
  value = sess.run(next_element)
  assert i == value

Реинициализируемый (reinitializable) итератор может быть инициализирован из нескольких различных Dataset объектов. Например, может существовать тренировочный пайплайн ввода, который использует преобразования изображений ввода, применяемые в случайном порядке, для улучшения генерализации, и валидационный пайплайн ввода, который оценивает прогнозы на немодифицированных данных. Эти пайплайны обычно используют разные Dataset объекты с одной и той же структурой (то есть одинаковые типы и совместимые формы для каждого компонента).

# Определяем тренировочный и валидационный наборы данных 
# с одинаковой структурой.
training_dataset = tf.data.Dataset.range(100).map(
    lambda x: x + tf.random_uniform([], -10, 10, tf.int64))
validation_dataset = tf.data.Dataset.range(50)

# Реинициализируемый итератор определяется этими структурами.
# Можно использовать output_types и output_shapes свойства
# объектов training_dataset или validation_dataset здесь,
# поскольку они совместимы.
iterator = tf.data.Iterator.from_structure(
                                     training_dataset.output_types,
                                     training_dataset.output_shapes)
next_element = iterator.get_next()

training_init_op = iterator.make_initializer(training_dataset)
validation_init_op = iterator.make_initializer(validation_dataset)

# Выполняем 20 эпох, в которых тренировочный набор данных итерируется,
# а следом и валидационный набор.
for _ in range(20):
  # Инициализируем итератор для тренировочного набора.
  sess.run(training_init_op)
  for _ in range(100):
    sess.run(next_element)

  # Инициализируем итератор для валидационного набора.
  sess.run(validation_init_op)
  for _ in range(50):
    sess.run(next_element)

Загружаемый (feedable) итератор может быть использован вместе с tf.placeholder для выбора того какой Iterator использовать в каждом вызове tf.Session.run посредством feed_dict механизма. Он предоставляет ту же функциональность, что и реинициализируемый итератор, но он не требует инициализации итератора со старта набора данных, когда переключаемся между итераторами. Например, используя те же тренировочный и валидационный пример как выше, можно использовать tf.data.Iterator.from_string_handle, чтобы определить загружаемый итератор, который позволяет переключаться между двумя наборами данных:

# Определяем тренировочный и валидационный датасеты 
# с одинаковой структурой
training_dataset = tf.data.Dataset.range(100).map(
    lambda x: x + tf.random_uniform([], -10, 10, tf.int64)).repeat()
validation_dataset = tf.data.Dataset.range(50)

# Загружаемый итератор определен обработчиком placeholder
# и его структурой.
# Можно использовать output_types и output_shapes свойства
# объектов training_dataset или validation_dataset здесь,
# поскольку они имеют одинаковую структуру.
handle = tf.placeholder(tf.string, shape=[])
iterator = tf.data.Iterator.from_string_handle(
    handle, training_dataset.output_types, 
            training_dataset.output_shapes)
next_element = iterator.get_next()

# Можно использовать загружаемый итератор 
# с различными другими итераторами,
# такими как одноразовый и инициализируемый
training_iterator = training_dataset.make_one_shot_iterator()
validation_iterator = validation_dataset.make_initializable_iterator()

# Метод `Iterator.string_handle()` возвращает тензор, 
# который может быть оценен
# и использован для наполнения `handle` placeholder.
training_handle = sess.run(training_iterator.string_handle())
validation_handle = sess.run(validation_iterator.string_handle())

# Итерируем постоянно, переключаясь между тренировкой и валидацией.
while True:
  # Проходим 200 шагов, используя тренировочный датасет.
  # Отметим, что тренировочный датасет бесконечный,
  # и мы продолжаем с того места, где остановились на
  # предыдущей итерации `while` цикла.
  for _ in range(200):
    sess.run(next_element, feed_dict={handle: training_handle})

  # Делаем один проход по валидационному датасету.
  sess.run(validation_iterator.initializer)
  for _ in range(50):
    sess.run(next_element, feed_dict={handle: validation_handle})

Получение значений из итератора

Метод Iterator.get_next() возвращает один или более tf.Tensor объектов, которые соотвествуют символически следующему элементу итератора. Каждый раз эти тензоры оцениваются, они принимают значение следующего элемента в подлежащем датасете. (Отметим что, как и другие объекты состояния в TensorFlow, вызываемый Iterator.get_next() не сразу продвигает итератор. Необходимо использовать возвращаемые tf.Tensor объекты в TensorFlow выражении, и передавать результат этого выражения в tf.Session.run(), чтобы получить следующиь элементы и продвинуть итератор.)

Если итератор достигает конца датасета (набора данных), исполнение Iterator.get_next() операции вызовет tf.errors.OutOfRangeError исключение. После этой точки итератор будет в неиспользуемом состоянии, и для дальнейшего использования его необходимо инициализировать снова.

dataset = tf.data.Dataset.range(5)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()

# Обычно `result` будет выводом модели,
# или тренировочной операцией оптимизиатора.
result = tf.add(next_element, next_element)

sess.run(iterator.initializer)
print(sess.run(result))  # ==> "0"
print(sess.run(result))  # ==> "2"
print(sess.run(result))  # ==> "4"
print(sess.run(result))  # ==> "6"
print(sess.run(result))  # ==> "8"
try:
  sess.run(result)
except tf.errors.OutOfRangeError:
  print("End of dataset")  # ==> "End of dataset"

Общепринятный паттерн - обернуть "тренировочный цикл" в try-except блок:

sess.run(iterator.initializer)
while True:
  try:
    sess.run(result)
  except tf.errors.OutOfRangeError:
    break

Если каждый элемент датасета имеет вложенную структуру, то возвращаемое значение Iterator.get_next() будет одним или более tf.Tensor объектов в той же вложенной структуре:

dataset1 = tf.data.Dataset.from_tensor_slices(
                                       tf.random_uniform([4, 10]))
dataset2 = tf.data.Dataset.from_tensor_slices(
            (tf.random_uniform([4]), tf.random_uniform([4, 100])))
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))

iterator = dataset3.make_initializable_iterator()

sess.run(iterator.initializer)
next1, (next2, next3) = iterator.get_next()

Отметим, что next1, next2, и next3 - это тензоры, произведенные одним и тем же опом(op)/узлом(node), созданным Iterator.get_next(). Поэтому оценка любого из этих тензоров продвинет итератор для всех компонентов. Обычный приемник будет включать все компоненты в одном выражении.

Сохранение состояние итератора

Функция tf.contrib.data.make_saveable_from_iterator создает SaveableObject из итератора, который может быть использован для сохранения и восстановления текущего состояния итератора (и целого пайплайна ввода). Сохраняемый объект, созданный таким образом, может быть добавлен в tf.train.Saver лист переменных или tf.GraphKeys.SAVEABLE_OBJECTS коллекцию для сохранения и восстановления в той же манере как tf.Variable.

# Создаем сохраняемый объект из итератора.
saveable = tf.contrib.data.make_saveable_from_iterator(iterator)

# Сохраняем состояние итератора добавлением его 
# к коллекции сохраняемых объектов.
tf.add_to_collection(tf.GraphKeys.SAVEABLE_OBJECTS, saveable)
saver = tf.train.Saver()

with tf.Session() as sess:

  if should_checkpoint:
    saver.save(path_to_checkpoint)

# Восстанавливаем состояние итератора.
with tf.Session() as sess:
  saver.restore(sess, path_to_checkpoint)

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

пятница, 21 сентября 2018 г.

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

Использование объектов состояния в течение стремительного исполнения

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

Переменные и объекты

В ходе стремительного исполнения переменные сохраняются до тех пор пока не удалена последняя ссылка на объект.

with tf.device("gpu:0"):
  v = tfe.Variable(tf.random_normal([1000, 1000]))
  v = None  # v больше не использует GPU память

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

tfe.Checkpoint может сохранять в контрольные точки и восстанавливать из контрольных точек tfe.Variables:

x = tfe.Variable(10.)

checkpoint = tfe.Checkpoint(x=x)  # save as "x"

x.assign(2.)   # Назначаем новое значение переменным и сохраняем
save_path = checkpoint.save('./ckpt/')

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

# Восстановление значений их контрольной точки
checkpoint.restore(save_path)

print(x)  # => 2.0

Для сохранения и загрузки моделей tfe.Checkpoint сохраняет внутренние объекты состояния без необходимости скрытых переменных. Чтобы записать состояние модели оптимизатор и глобальный шаг передают их (переменные) в tfe.Checkpoint:

model = MyModel()
optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
checkpoint_dir = ‘/path/to/model_dir’
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
root = tfe.Checkpoint(optimizer=optimizer,
                  model=model,
                  optimizer_step=tf.train.get_or_create_global_step())

root.save(file_prefix=checkpoint_prefix)
# или
root.restore(tf.train.latest_checkpoint(checkpoint_dir))

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

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

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

Сводки и TensorBoard

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

tf.contrib.summary совместим с обоими окружениями: стремительного и графового исполнения. Операции сводки, такие как tf.contrib.summary.scalar, вставляются в ходе создания модели. Например, для записи сводок единожды каждые 100 глобальных шагов:

writer = tf.contrib.summary.create_file_writer(logdir)
# возвращает переменную глобального шага
global_step=tf.train.get_or_create_global_step()  

writer.set_as_default()

for _ in range(iterations):
  global_step.assign_add(1)
  # Необходимо включить record_summaries метод
  with tf.contrib.summary.record_summaries_every_n_global_steps(100):
    # код модели располагается здесь
    tf.contrib.summary.scalar('loss', loss)
     ...

Сложные темы автоматического дифференцирования

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

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

def line_search_step(fn, init_x, rate=1.0):
  with tf.GradientTape() as tape:
    # Переменные автоматически записаны, а вручную смотрим тензор
    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.GradientTape - это мощный интерфейс для вычисления градиентов, но существует другое Autograd-style API доступное для автоматического дифференцирования. Эти функции полезны, если записывать математический код только с тензорами и градиентными функциями и без tfe.Variables:

  • tfe.gradients_function - Возвращает функцию, которая вычисляет производные ее вводного параметра функции с учетом ее аргументов. Вводный параметр функция должна возвращать скалярное значение. Когда возвращенная функция вызвана она возвращает лист tf.Tensor объектов: один элемент для каждого аргумента вводной функции. Ввиду того, что все должно быть передано как параметр функции, это становится громоздким если присутствует зависимость на многие тренируемые параметры.
  • tfe.value_and_gradients_function - Схожа с tfe.gradients_function, но когда возвращенная функция вызвана, она возвращает значение от вводной функции вместе с листом производных вводной функции с учетом ее аргументов.

В следующем примере tfe.gradients_function принимает квадратичную функцию как аргумент и возвращает функцию, которая вычисляет частичные производные квадрата с учетом ее вводов. Чтобы вычислить производную от квадрата 3, grad (3.0) возвращает 6.

def square(x):
  return tf.multiply(x, x)

grad = tfe.gradients_function(square)

square(3.)  # => 9.0
grad(3.)    # => [6.0]

# Производная второго порядка квадрата:
gradgrad = tfe.gradients_function(lambda x: grad(x)[0])
gradgrad(3.)  # => [2.0]

# Производная третьего порядка равна None:
gradgradgrad = tfe.gradients_function(lambda x: gradgrad(x)[0])
gradgradgrad(3.)  # => [None]


# С потоком контроля:
def abs(x):
  return x if x > 0. else -x

grad = tfe.gradients_function(abs)

grad(3.)   # => [1.0]
grad(-3.)  # => [-1.0]

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

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

@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.log(1 + tf.exp(x))
grad_log1pexp = tfe.gradients_function(log1pexp)

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

# Однако, при x = 100 проваливается из-за числовой нестабильности.
grad_log1pexp(100.)  # => [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.log(1 + e), grad

grad_log1pexp = tfe.gradients_function(log1pexp)

# Как и раньше вычисление градиента работает хорошо при x = 0.
grad_log1pexp(0.)  # => [0.5]

# И вычисление градиента также работает при x = 100.
grad_log1pexp(100.)  # => [1.0]

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

Вычисление автоматически передается на GPU в ходе стремительного исполнения. Если необходим контроль над тем где происходит вычисление, тогда можно заключить это в 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)
    # Удостоверяемся, что исполнили оп (op), 
    # а не просто включили в очередь
    _ = x.numpy()  
  end = time.time()
  return end - start

shape = (1000, 1000)
steps = 200
print("Time to multiply a {} matrix by itself {} times:".format(shape, 
                                                                steps))

# Исполняем на CPU:
with tf.device("/cpu:0"):
  print("CPU: {} secs".format(measure(tf.random_normal(shape), steps)))

# Исполняем на GPU, если доступно:
if tfe.num_gpus() > 0:
  with tf.device("/gpu:0"):
    print("GPU: {} secs".format(measure(tf.random_normal(shape), steps)))
else:
  print("GPU: not found")

Вывод (точные числа зависят от аппаратных средств):

Time to multiply a (1000, 1000) matrix by itself 200 times:
CPU: 4.614904403686523 secs
GPU: 0.5581181049346924 secs

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

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

if tfe.num_gpus() > 1:
  x_gpu1 = x.gpu(1)
  _ = tf.matmul(x_gpu1, x_gpu1)  # Runs on GPU:1

Опорные отметки (benchmarks)

Для вычислительно-емких моделей, таких как ResNet50 тренируемый на GPU, производительность стремительного исполнения сопоставима с графовым исполнением. Но эта разница становится больше для моделей с меньшими вычислениями.

Работа с графами

Хотя стремительное исполнение делает разработку и отладку более интерактивными, TensorFlow графовое исполнение имеет преимущества распреленной тренировки, оптимизаций производительности, и производственного использования. Однако, написание графового кода может отличаться от написания обычного Python кода и быть более трудным в отладке.

Для создания и тренировки граф-сконструированных моделей Python программа сначала создает граф, представляющий вычисление, затем вызывает Session.run для отправки графа на исполнение в C++ окружение. Это предоставляет:

  • Автоматическую дифференцировку, используя статический autodiff
  • Простое использование на сервере, независимо от платформы
  • Основанные на графах оптимизации (общее устранение вложенных выражений, раскладывание констант)
  • Компиляцию и ядерный поток
  • Автоматическое распределение и репликацию (размещение на узлы распределенной системы)

Производственное применение кода, написанного для стремительного исполнения, более трудоемко: либо генерирование графа из модели, либо исполнение Python интерпретатора и кода непосредственно на сервере.

Написание совместимого кода

Код, написанный для стремительного исполнения, также построит граф в ходе графового исполнения. Делаем это просто выполняя код в новой Python сессии, где стремительное исполнение не включено.

Большинство TensorFlow операций работают при стремительном исполнении, но необходимо помнить некоторые особенности:

  • Исползуйте tf.data для обработки ввода, вместо очередей. Это быстрее и проще.
  • Используйте API объектно-ориентированных слоев - такие как tf.keras.layers и tf.keras.Model - ввиду того что они имеют явное хранилище переменных.
  • Большая часть кода моделей работает одинаково при стремительном и графовом исполнении, но есть исключения. (Например, динамические модели используют Python поток контроля для изменения вычислений, основанных на вводных данных.)
  • Как только стремительное исполнение включено с помощью tf.enable_eager_execution, оно не может быть выключено. Необходимо начать новую Python сессию для возвращения к графовому исполнению.

Лучше всего писать код подходящий и для стремительного, и для графового исполнения. Это дает возможность экспериментирования и отладки при стремительном исполнении с преимуществами распределенной производительности при графовом исполнении.

Пишите, отлаживайте, итерируйте при стремительном исполнении, затем импортируйте граф модели для производственного использования. Используйте tfe.Checkpoint для сохранения и восстановления переменных моделей, это позволяет перемещаться между окружениями стремительного и графового исполнения.

Использование стремительного исполнения в графовом окружении

Выборочно включаем стремительное исполнение в TensorFlow графовом окружении, используя tfe.py_func. Это используется, когда tf.enable_eager_execution() не вызван.

def my_py_func(x):
  x = tf.matmul(x, x)  # Можно использовать tf ops
  print(x)  # but it's eager!
  return x

with tf.Session() as sess:
  x = tf.placeholder(dtype=tf.float32)
  # Вызываем функция стремительного исполнения в графе!
  pf = tfe.py_func(my_py_func, [x], tf.float32)
  sess.run(pf, feed_dict={x: [[2.0]]})  # [[4.0]]

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

вторник, 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 читайте в следующем посте.

пятница, 14 сентября 2018 г.

TensorFlow: Keras, сложные модели, сохранение, распределение

Создание сложных моделей

Функциональное (Functional) API

tf.keras.Sequential модель - это просто стек слоев, которые не могут представлять произвольные модели. Для построения моделей с комплексной топологией необходимо использовать Keras функциональное (functional) API, таких моделей как:

  • Модели с многими входами
  • Модели с многими выходами
  • Модели с общими слоями (тот же самый слой вызывается несколько раз)
  • Модели с непоследовательными потоками данных (например, остаточные соединения)

Построение модели с функциональным API:

  1. Вызывается экземпляр слоя и возвращается тензор.
  2. Вводные тензоры и выводные тензоры использованы для определения tf.keras.Model экземпляра.
  3. Эта модель тренируется так же как Sequential модель.

Следующий пример использует функциональное API для построения простой, полностью связанной сети:

# Выполняет тензор заполнитель
inputs = keras.Input(shape=(32,))  

# экземпляр слоя вызывается на тензоре и возвращает тензор
x = keras.layers.Dense(64, activation='relu')(inputs)
x = keras.layers.Dense(64, activation='relu')(x)
predictions = keras.layers.Dense(10, activation='softmax')(x)

# Воплощение модели с заданными вводами и выводами
model = keras.Model(inputs=inputs, outputs=predictions)

# Шаг компиляции определяет тренировочную конфигурацию
model.compile(optimizer=tf.train.RMSPropOptimizer(0.001),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# Тренируем в течение 5 эпох
model.fit(data, labels, batch_size=32, epochs=5)

Создание подклассов модели

Построим полностью кастомизированную модель посредством создания подкласса tf.keras.Model и определения собственного прохода вперед. Создаем слои в init методе и устанавливаем их как атрибуты экземпляра класса. Определим следующий проход в методе вызова.

Создание подклассов модели особенно полезно при активированном нетерпеливом исполнении, ввиду того, что будущий проход может быть записан императивно.

Ключевой момент: используйте правильное API для задания. Хотя создание подклассов модели предлагает гибкость, это несет цену высокой сложности и возможности пользовательской ошибки. Если возможно используйте функциональное API.

Следующий пример показывает подкласс tf.keras.Model, используя пользовательский проход вперед:

class MyModel(keras.Model):

  def init(self, num_classes=10):
    super(MyModel, self).init(name='my_model')
    self.num_classes = num_classes
    # Определяем слои здесь
    self.dense_1 = keras.layers.Dense(32, activation='relu')
    self.dense_2 = keras.layers.Dense(num_classes, activation='sigmoid')

  def call(self, inputs):
    # Определяем будущий проход здесь,
    # используя слои определенные ранее(в __init__).
    x = self.dense_1(inputs)
    return self.dense_2(x)

  def compute_output_shape(self, input_shape):
    # Необходимо переопределить эту функцию
    # чтобы использовать модель с подклассами
    # как часть модели функционального стиля.
    # В ином случае этот метод опционален.
    shape = tf.TensorShape(input_shape).as_list()
    shape[-1] = self.num_classes
    return tf.TensorShape(shape)


# Воплощаем модель с подклассами.
model = MyModel(num_classes=10)

# Шаг компиляции определяет тренировочную конфигурацию.
model.compile(optimizer=tf.train.RMSPropOptimizer(0.001),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# Тренируем в течение 5 эпох.
model.fit(data, labels, batch_size=32, epochs=5)

Кастомные слои

Создаем кастомный слой созданием подкласса tf.keras.layers.Layer и имплементируя следующие методы:

  • build: Создает веса слоя. Добавляет веса с add_weight методом.
  • call: Задает будущий проход.
  • compute_output_shape: Определяет как вычислить выводную форму слоя при данной входной форме.

Опционально слой может быть сериализован имплеметацией get_config метода и from_config классового метода.

Вот пример кастомного слоя, который имплементирует matmul входа с матрицей ядра:

class MyLayer(keras.layers.Layer):

  def init(self, output_dim, **kwargs):
    self.output_dim = output_dim
    super(MyLayer, self).init(**kwargs)

  def build(self, input_shape):
    shape = tf.TensorShape((input_shape[1], self.output_dim))
    # Создаем переменную тренируемого веса для этого слоя.
    self.kernel = self.add_weight(name='kernel',
                                  shape=shape,
                                  initializer='uniform',
                                  trainable=True)
    super(MyLayer, self).build(input_shape)

  def call(self, inputs):
    return tf.matmul(inputs, self.kernel)

  def compute_output_shape(self, input_shape):
    shape = tf.TensorShape(input_shape).as_list()
    shape[-1] = self.output_dim
    return tf.TensorShape(shape)

  def get_config(self):
    base_config = super(MyLayer, self).get_config()
    base_config['output_dim'] = self.output_dim

  @classmethod
  def from_config(cls, config):
    return cls(**config)


# Создание слоя с использованием кастомного слоя
model = keras.Sequential([MyLayer(10),
                          keras.layers.Activation('softmax')])

# Шаг компиляции определяет тренировочную конфигурацию
model.compile(optimizer=tf.train.RMSPropOptimizer(0.001),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# Тренируем в течение 5 эпох.
model.fit(data, targets, batch_size=32, epochs=5)

Колбеки (callbacks)

Колбек (callback) - это объект переданный модели для кастомизирования и расширения ее модели в ходе тренировки. Можно писать собственный кастомный колбек или использовать встроенный tf.keras.callbacks, который включает:

  • tf.keras.callbacks.ModelCheckpoint: Сохраняет контрольные точки модели с регулярными интервалами.
  • tf.keras.callbacks.LearningRateScheduler: Динамически изменяет скорость обучения.
  • tf.keras.callbacks.EarlyStopping: Прерывает тренировку, когда валидационная производительность перестает улучшаться.
  • tf.keras.callbacks.TensorBoard: Отображает поведение модели, используя TensorBoard.

Чтобы использовать tf.keras.callbacks.Callback передаем ее методу fit модели:

callbacks = [
  # Прерываем тренировку 
  # если val_loss перестает улучшаться в течение 2 эпох
  keras.callbacks.EarlyStopping(patience=2, monitor='val_loss'),
  # Записываем логи TensorBoard в ./logs директорию
  keras.callbacks.TensorBoard(log_dir='./logs')
]
model.fit(data, labels, batch_size=32, epochs=5, callbacks=callbacks,
          validation_data=(val_data, val_targets))

Сохранение и восстановление

Только веса

Сохраняем и загружаем веса модели, используя tf.keras.Model.save_weights:

# Сохраняем веса в TensorFlow Checkpoint файл
model.save_weights('./my_model')

# Восстанавливаем состояние модели,
# это требует модель с точно такой же архитектурой.
model.load_weights('my_model')

По умолчанию это сохраняет веса модели в файл формата контрольной точки TensorFlow. Веса могут быть также сохранены в Keras HDF5 формате (по умолчанию для мульти-бекенд имплементации Keras):

# Сохраняем веса в HDF5 файле
model.save_weights('my_model.h5', save_format='h5')

# Восстанавливаем состояние модели
model.load_weights('my_model.h5')

Сохранение и восстановление только конфигурации

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

# Сериализация модели в JSON формате
json_string = model.to_json()

# Пересоздание модели (со свежей инициализацией)
fresh_model = keras.models.from_json(json_string)

# Сериализация модели в YAML формате
yaml_string = model.to_yaml()

# Пересоздание модели
fresh_model = keras.models.from_yaml(yaml_string)

Предостережение: модели с подклассами не сериализуемы, потому что их архитектура определена в Python коде в теле call метода.

Сохранение и восстановление целой модели

Целая модель может быть сохранена в файл, который содержит значения весов, конфигурацию модели, и конфигурацию оптимизатора. Это позволяет создавать контрольную точку модели и возобновлять тренировку позже - с того же самого места, где она была остановлена - без необходимости доступа к изначальному коду.

# Создаем обычную модель
model = keras.Sequential([
  keras.layers.Dense(10, activation='softmax', input_shape=(32,)),
  keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(data, targets, batch_size=32, epochs=5)


# Сохраняем целую модель в HDF5 файле
model.save('my_model.h5')

# Пересоздаем ту же самую модель, включая веса и оптимизатор.
model = keras.models.load_model('my_model.h5')

Нетерпеливое исполнение

Нетерпеливое исполнение (eager execution) - это императивное программное окружение, которое применяет операции немедленно. Это не требуется для Keras, но поддерживается tf.keras и полезно для проверки программ и отладки.

Все tf.keras API для построения моделей совместимы с нетерпеливым исполнением. И, хотя Sequential и фукнциональное API могут быть использованы, нетерпеливое исполнение имеет особые преимущества при создании подклассов моделей и построении кастомных слоев.

Распределение

Estimators (оценщики)

Estimators API используется для тренировки модели в распределенных окружениях. Это целевые методы использования, такие как распределенная тренировка на крупных наборах данных, которые могут экспортировать модель в производственную среду.

tf.keras.Model может быть тренирована с tf.estimator API преобразованием модели в tf.estimator.Estimator объект с tf.keras.estimator.model_to_estimator.

model = keras.Sequential([layers.Dense(10,activation='softmax'),
                          layers.Dense(10,activation='softmax')])

model.compile(optimizer=tf.train.RMSPropOptimizer(0.001),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

estimator = keras.estimator.model_to_estimator(model)

Исполнение на многих GPU

tf.keras модели могут выполняться на нескольких GPU, используя tf.contrib.distribute.DistributionStrategy. Это API предоставляет распределенную тренировку на нескольких GPU почти без изменений в существующем коде.

На данный момент tf.contrib.distribute.MirroredStrategy единственная поддерживаемая стратегия распределения. MirroredStrategy выполняет внутри-графовую репликацию с синхронной тренировкой, используя all-reduce на единственной машине. Чтобы использовать DistributionStrategy с Keras, преобразуем tf.keras.Model в tf.estimator.Estimator с tf.keras.estimator.model_to_estimator, затем тренируем как estimator.

Следующий пример распределяет tf.keras.Model по нескольким GPU на единственной машине.

Во-первых, определим простую модель:

model = keras.Sequential()
model.add(keras.layers.Dense(16, activation='relu', input_shape=(10,)))
model.add(keras.layers.Dense(1, activation='sigmoid'))

optimizer = tf.train.GradientDescentOptimizer(0.2)

model.compile(loss='binary_crossentropy', optimizer=optimizer)
model.summary()

Определим входной пайплайн. input_fn возвращает tf.data.Dataset объект, используемый чтобы распределить данные по многим устройствам - с каждым устройством, обрабатывающим часть входного пакета.

def input_fn():
  x = np.random.random((1024, 10))
  y = np.random.randint(2, size=(1024, 1))
  x = tf.cast(x, tf.float32)
  dataset = tf.data.Dataset.from_tensor_slices((x, y))
  dataset = dataset.repeat(10)
  dataset = dataset.batch(32)
  return dataset

Затем создаем tf.estimator.RunConfig и устанавливаем train_distribute аргумент к tf.contrib.distribute.MirroredStrategy экземпляру. При создании MirroredStrategy можно определить лист устройств или установить num_gpus аргумент. По умолчанию используются все доступные GPU:

strategy = tf.contrib.distribute.MirroredStrategy()
config = tf.estimator.RunConfig(train_distribute=strategy)

Преобразуем Keras модель в tf.estimator.Estimator экземпляр:

keras_estimator = keras.estimator.model_to_estimator(
  keras_model=model,
  config=config,
  model_dir='/tmp/model_dir')

Наконец, тренируем Estimator экземпляр предоставляя input_fn и steps аргументы:

keras_estimator.train(input_fn=input_fn, steps=10)

среда, 12 сентября 2018 г.

TensorFlow: Keras, базовое использование

Keras - это высокоуровневое API для построения и тренировки моделей глубокого обучения. Оно используется для быстрого прототипирования, углубленного исследования, и в производственной среде, и вот его три главных преимущества:

  • Дружественность к пользователю

    Keras имеет простой, согласованный интерфейс, оптимизированный для случаев общего применения. Он обеспечивает четкую и эффективную обратную связь для пользовательских ошибок.

  • Модулярный и компонуемый

    Keras модели изготовлены посредством связывания конфигурируемых строительных блоков вместе, с несколькими ограничениями.

  • Простой для расширения

    Можно писать пользовательские строительные блоки для выражения новых идей для исследования. Можно создавать новые слои, функции потери, и разрабатывать современные модели.

Импорт tf.keras

tf.keras это TensorFlow реализация Keras API спецификации. Это высокоуровненое API для построения и тренировки моделей, которое включает первоклассную поддержку для TensorFlow специфичной функциональности, такой как нетерпеливое исполнение (eager execution), tf.data пайплайны, и Estimators. tf.keras делает TensorFlow проще для использования без жертвования гибкостью и производительностью.

Для начала импортируем tf.keras как часть создания TensorFlow программы:

import tensorflow as tf
from tensorflow import keras

tf.keras может исполнять любой Keras-совместимый код, но необходимо помнить:

  • Версия tf.keras в последнем TensorFlow релизе может быть не той же самой, что и последняя версия keras из PyPI. Проверяйте tf.keras.version.
  • При сохранении весов модели tf.keras по умолчанию использует формат контрольных точек. Передайте save_format='h5', чтобы использовать HDF5.

Создание простой модели

Последовательная модель (Sequential model)

В Keras для построения модели необходимо собрать слои. Модель - это (обычно) граф слоев. Наиболее общий тип модели - это стек слоев: tf.keras.Sequential модель.

Для построения простой, полностью связанной сети (то есть многослойного перцептрона):

model = keras.Sequential()
# Добавляем плотно-соединенный (densely-connected) слой 
# с 64 элементами в модели:
model.add(keras.layers.Dense(64, activation='relu'))
# Добавляем другой:
model.add(keras.layers.Dense(64, activation='relu'))
# Добавляем софтмакс слой с 10 выводными элементами:
model.add(keras.layers.Dense(10, activation='softmax'))

Конфигурирование слоев

Существует много tf.keras.layers доступных с некоторыми параметрами конструктора:

  • activation: Устанавливает функцию активации для слоя. Этот параметр определяется именем встроенной функции или как вызываемый объект. По умолчанию, активация не применяется.
  • kernel_initializer и bias_initializer: Инициализационные схемы, которые создают веса слоя (ядро и смещение). Этот параметр - это имя или вызываемый объект. По умолчанию равен "Glorot uniform" инициализатору.
  • kernel_regularizer и bias_regularizer: Регуляризационные схемы, которые применяют веса слоя (ядро и смещение), такие как L1 или L2 регуляризация. По умолчанию регуляризация не применяется.

Следующие экземпляры tf.keras.layers.Dense слоев используют аргументы конструктора:

# Создаем сигмоидный слой:
layers.Dense(64, activation='sigmoid')
# Или:
layers.Dense(64, activation=tf.sigmoid)

# Линейный слой с L1 регуляризацией фактора 0.01, 
# применяемой к матрице ядра:
layers.Dense(64, kernel_regularizer=keras.regularizers.l1(0.01))
# Линейный слой с L2 регуляризацией фактора 0.01, 
# применяемой к вектору смещения:
layers.Dense(64, bias_regularizer=keras.regularizers.l2(0.01))

# Линейный слой с ядром инициализированным 
# к случайной ортогональной матрице:
layers.Dense(64, kernel_initializer='orthogonal')
# Линейный слой с вектором смещения, инициализированным к 2.0s:
layers.Dense(64, bias_initializer=keras.initializers.constant(2.0))

tf.keras.Model.compile принимает три важных аргумента:

  • optimizer: Этот объект определяет тренировочную процедуру. Передаем этому экземпляры оптимизатора из модуля tf.train, такие как AdamOptimizer, RMSPropOptimizer, или GradientDescentOptimizer.
  • loss: Функция для минимизации потери в ходе оптимизации. Общие варианты включают среднюю квадратичную ошибку (mean square error (mse)), categorical_crossentropy, и binary_crossentropy. Функции потери определяется именем или передачей вызываемого объекта из модуля tf.keras.losses.
  • metrics: Используются для отображения тренировки. Это строковые имена или вызываемые объекты из модуля tf.keras.metrics.

Далее показаны несколько примеров конфигурирования модели для тренировки:

# Кофигурируем модель для регрессии со средней квадратичной ошибкой
model.compile(optimizer=tf.train.AdamOptimizer(0.01),
              loss='mse',       # средняя квадратичная ошибка
              metrics=['mae'])  # средняя абсолютная ошибка

# Конфигурируем модель для категорийной классификации
model.compile(optimizer=tf.train.RMSPropOptimizer(0.01),
              loss=keras.losses.categorical_crossentropy,
              metrics=[keras.metrics.categorical_accuracy])

Ввод NumPy данных

Для маленьких наборов данных используем NumPy массивы в памяти для тренировки и оценки модели. Модель обучается тренировочным данным, используя метод fit:

tf.keras.Model.fit принимает три важных аргумента:

  • epochs: Тренировка структурирована в эпохи. Эпоха - это одна итерация по всем входным данным (она выполняется маленькими пакетами).
  • batch_size: При передаче NumPy данных модель нарезает данные кусками в маленькие пакеты и итерирует по этим пакетам в течение тренировки. Это число определяет размер каждого пакета. Необходимо помнить, что последний пакет может быть меньше если общее количество примеров не делится целиком на размер пакета.
  • validation_data: При прототипировании модели необходимо проверять ее производительность на некоторых валидационных данных. Передача этого аргумента - пары из вводных данных и меток - позволяет модели показывать потерю и метрики в режиме работы модели для переданных данных в конце каждой эпохи.

Вот пример использования validation_data:

import numpy as np

data = np.random.random((1000, 32))
labels = np.random.random((1000, 10))

val_data = np.random.random((100, 32))
val_labels = np.random.random((100, 10))

model.fit(data, labels, epochs=10, batch_size=32,
          validation_data=(val_data, val_labels))

Ввод tf.data наборов данных

Используем Datasets API для больших наборов данных или тренировки на нескольких устройствах. Передаем tf.data.Dataset экземпляр fit методу:

# Создаем маленький экземпляр набора данных:
dataset = tf.data.Dataset.from_tensor_slices((data, labels))
dataset = dataset.batch(32)
dataset = dataset.repeat()

# Задаем `steps_per_epoch` при вызове `fit` на наборе данных.
model.fit(dataset, epochs=10, steps_per_epoch=30)

Здесь fit метод использует steps_per_epoch аргумент - это количество тренировочных шагов, которые выполняет модель до того как перейти к следующей эпохе. Ввиду того, что Dataset выводит данные пакетами, этот случай не требует batch_size.

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

dataset = tf.data.Dataset.from_tensor_slices((data, labels))
dataset = dataset.batch(32).repeat()

val_dataset = tf.data.Dataset.from_tensor_slices((val_data, val_labels))
val_dataset = val_dataset.batch(32).repeat()

model.fit(dataset, epochs=10, steps_per_epoch=30,
          validation_data=val_dataset,
          validation_steps=3)

Оценка и прогнозирование

tf.keras.Model.evaluate и tf.keras.Model.predict методы могут использовать NumPy данные и tf.data.Dataset.

Чтобы оценить потерю и метрики в рабочем режиме модели выполняем следующее:

model.evaluate(x, y, batch_size=32)

model.evaluate(dataset, steps=30)

А чтобы прогнозировать вывод последним слоем в рабочем режиме модели выполняем следующее:

model.predict(x, batch_size=32)

model.predict(dataset, steps=30)

В следующем посте мы рассмотрим другие аспекты использования TensorFlow Keras.

понедельник, 10 сентября 2018 г.

TensorFlow: сохранение и восстановление моделей

Прогресс модели может быть сохранен в течение или после тренировки. Это означает, что обучение модели может быть возобновлено с того места, где оно было остановлено, избежав долгого времени повторной тренировки. Сохранение также означает, что вы можете поделиться своей моделью и другие могут воссоздать вашу работу. Во время публикации исследовательских моделей и техник большинство практиков машинного обучения делятся:

  • кодом для создания модели
  • тренировочными весами, или параметрами, для модели

Предоставление этих данных позволяет другим понять как модель работает и испробовать модель самим с новыми данными.

Опции

Существуют различные пути для сохранения TensorFlow моделей - в зависимости от API, который использовать. Это руководство использует tf.keras - высокоуровневое API для построения и тренировки моделей в TensorFlow.

Настройка

Получение примерного набора данных

Мы будем использовать MNIST набор данных для тренировки нашей модели и демонстрации сохраненных весов. Чтобы ускорить демонстрацию, используем только первые 1000 примеров:

from future import absolute_import, division, print_function

import os

import tensorflow as tf
from tensorflow import keras

tf.version

'1.9.0'

(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()

train_labels = train_labels[:1000]
test_labels = test_labels[:1000]

train_images = train_images[:1000].reshape(-1, 28 * 28) / 255.0
test_images = test_images[:1000].reshape(-1, 28 * 28) / 255.0

Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
11493376/11490434 [==============================] - 2s 0us/step

Определение модели

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

# Возвращает короткую последовательную модель (sequential model)
def create_model():
  model = tf.keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.nn.relu, input_shape=(784,)),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10, activation=tf.nn.softmax)
  ])
  
  model.compile(optimizer=tf.keras.optimizers.Adam(), 
                loss=tf.keras.losses.sparse_categorical_crossentropy,
                metrics=['accuracy'])
  
  return model


# Создание инстанса базовой модели
model = create_model()
model.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 512)               401920    
_________________________________________________________________
dropout (Dropout)            (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

Сохранение контрольных точек в ходе тренировки

Приоритетный способ использования сохранения - автоматически сохранять контрольные точки в течение и в конце тренировки. Таким способом можно использовать тренированную модель без необходимости тренировать ее вновь, или начинать тренировку с того места, где она была остановлена в случае если тренировочный процесс был прерван.

tf.keras.callbacks.ModelCheckpoint - это колбек (callback) (функция обратного вызова), который выполняет эту задачу. Колбек принимает пару аргументов, чтобы сконфигурировать создание контрольных точек.

Использование колбека контрольных точек

Тренируем модель и передаем ей ModelCheckpoint колбек:

checkpoint_path = "training_1/cp.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

# Создаем колбек контрольной точки
cp_callback = tf.keras.callbacks.ModelCheckpoint(checkpoint_path, 
                                                 save_weights_only=True,
                                                 verbose=1)

model = create_model()

model.fit(train_images, train_labels,  epochs = 10, 
          validation_data = (test_images,test_labels),
          callbacks = [cp_callback])  # передаем колбек

Train on 1000 samples, validate on 1000 samples
Epoch 1/10
1000/1000 [==============================] - 0s 342us/step - loss: 1.1603 - acc: 0.6670 - val_loss: 0.6827 - val_acc: 0.7880

Epoch 00001: saving model to training_1/cp.ckpt
Epoch 2/10
1000/1000 [==============================] - 0s 129us/step - loss: 0.4071 - acc: 0.8860 - val_loss: 0.5841 - val_acc: 0.8110

Epoch 00002: saving model to training_1/cp.ckpt
Epoch 3/10
1000/1000 [==============================] - 0s 118us/step - loss: 0.2796 - acc: 0.9350 - val_loss: 0.4610 - val_acc: 0.8520

Epoch 00003: saving model to training_1/cp.ckpt

Epoch 4/10
1000/1000 [==============================] - 0s 121us/step - loss: 0.2025 - acc: 0.9570 - val_loss: 0.4324 - val_acc: 0.8610

Epoch 00004: saving model to training_1/cp.ckpt
Epoch 5/10
1000/1000 [==============================] - 0s 117us/step - loss: 0.1489 - acc: 0.9690 - val_loss: 0.4290 - val_acc: 0.8620

Epoch 00005: saving model to training_1/cp.ckpt
Epoch 6/10
1000/1000 [==============================] - 0s 127us/step - loss: 0.1194 - acc: 0.9780 - val_loss: 0.4143 - val_acc: 0.8700

Epoch 00006: saving model to training_1/cp.ckpt
Epoch 7/10
1000/1000 [==============================] - 0s 118us/step - loss: 0.0845 - acc: 0.9860 - val_loss: 0.4208 - val_acc: 0.8670

Epoch 00007: saving model to training_1/cp.ckpt
Epoch 8/10
1000/1000 [==============================] - 0s 118us/step - loss: 0.0648 - acc: 0.9910 - val_loss: 0.4078 - val_acc: 0.8680

Epoch 00008: saving model to training_1/cp.ckpt
Epoch 9/10
1000/1000 [==============================] - 0s 121us/step - loss: 0.0531 - acc: 0.9970 - val_loss: 0.4184 - val_acc: 0.8670

Epoch 00009: saving model to training_1/cp.ckpt
Epoch 10/10
1000/1000 [==============================] - 0s 121us/step - loss: 0.0391 - acc: 0.9960 - val_loss: 0.4185 - val_acc: 0.8640

Epoch 00010: saving model to training_1/cp.ckpt

Это создает единственную коллекцию файлов контрольных точек TensorFlow, которые обновляются в конце каждой эпохи:

!ls {checkpoint_dir}

checkpoint  cp.ckpt.data-00000-of-00001  cp.ckpt.index

Создадим новую нетренированную модель. При восстановлении модели только из весов необходимо иметь модель точно с такой же архитектурой, как и модель, которой принадлежали веса. Ввиду того, что это та же самая архитектура, мы можем поделиться весами, несмотря на то, что это другой экземпляр модели.

Теперь перестроим свежую, нетренированную модель, и оценим ее на тестовом наборе. Нетренированная модель будет выполняться на случайных уровнях (~10% аккуратности):

model = create_model()

loss, acc = model.evaluate(test_images, test_labels)
print("Untrained model, accuracy: {:5.2f}%".format(100*acc))

1000/1000 [==============================] - 0s 105us/step
Untrained model, accuracy:  7.20%

Затем загрузим веса из контрольной точки и переоценим:

model.load_weights(checkpoint_path)
loss,acc = model.evaluate(test_images, test_labels)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))

1000/1000 [==============================] - 0s 24us/step
Restored model, accuracy: 86.40%

Свойства колбека контрольной точки

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

Натренируем новую модель и сохраним уникально названные контрольные точки каждые 5 эпох:

# включаем эпоху в название файла (используя str.format)
checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

cp_callback = tf.keras.callbacks.ModelCheckpoint(
    checkpoint_path, verbose=1, save_weights_only=True,
    # Сохраняем веса каждые 5 эпох.
    period=5)

model = create_model()
model.fit(train_images, train_labels,
          epochs = 50, callbacks = [cp_callback],
          validation_data = (test_images,test_labels),
          verbose=0)

Epoch 00005: saving model to training_2/cp-0005.ckpt

Epoch 00010: saving model to training_2/cp-0010.ckpt

Epoch 00015: saving model to training_2/cp-0015.ckpt

Epoch 00020: saving model to training_2/cp-0020.ckpt

Epoch 00025: saving model to training_2/cp-0025.ckpt

Epoch 00030: saving model to training_2/cp-0030.ckpt

Epoch 00035: saving model to training_2/cp-0035.ckpt

Epoch 00040: saving model to training_2/cp-0040.ckpt

Epoch 00045: saving model to training_2/cp-0045.ckpt

Epoch 00050: saving model to training_2/cp-0050.ckpt

Теперь посмотрим на результирующие контрольные точки (сортируя по дате изменения):

import pathlib

# Сортируем контрольные точки по времени изменения
checkpoints = pathlib.Path(checkpoint_dir).glob("*.index")
checkpoints = sorted(checkpoints, key=lambda cp:cp.stat().st_mtime)
checkpoints = [cp.with_suffix('') for cp in checkpoints]
latest = str(checkpoints[-1])
checkpoints

[PosixPath('training_2/cp-0030.ckpt'),
 PosixPath('training_2/cp-0035.ckpt'),
 PosixPath('training_2/cp-0040.ckpt'),
 PosixPath('training_2/cp-0045.ckpt'),
 PosixPath('training_2/cp-0050.ckpt')]

Следует отметить, что по умолчанию TensorFlow сохраняет только 5 наиболее недавних контрольных точек.

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

model = create_model()
model.load_weights(latest)
loss, acc = model.evaluate(test_images, test_labels)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))

1000/1000 [==============================] - 0s 82us/step
Restored model, accuracy: 87.80%

В какие файлы сохраняются контрольные точки

Код выше сохраняет веса в коллекцию отформатированных по контрольным точкам файлов, которые содержат только тренированные веса в бинарном формате. Контрольные точки содержат:

  • Один или более осколков, которые содержат веса модели.
  • Индексный файл, который отражает какие веса сохранены в каждом осколке.

Если модель была тренирована только на одной машине, будет присутствовать только один осколок с суффиксом: .data-00000-of-00001

Ручное сохранение весов

Выше было показано как загружать веса в модель.

Ручное сохранение весов настолько же просто, используйте Model.save_weights метод.

# Сохраняем веса
model.save_weights('./checkpoints/my_checkpoint')

# Восстанавливаем веса
model = create_model()
model.load_weights('./checkpoints/my_checkpoint')

loss,acc = model.evaluate(test_images, test_labels)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))

1000/1000 [==============================] - 0s 59us/step
Restored model, accuracy: 87.80%

Сохранение модели целиком

Модель целиком может быть сохранена в файл, который содержит значения весов, конфигурацию модели, и конфигурацию оптимизатора. Это позволяет создавать контрольную точку модели и восстанавливать тренировку позже, с того же самомо места, без доступа к исходному коду.

Сохранение полностью функционирующих моделей в Keras очень удобно - можно загружать их в TensorFlow.js и затем тренировать и исполнять их в веб-браузере.

Keras предоставляет базовый формат сохранения, используя HDF5 стандарт. Для наших целей сохраненная модель может трактоваться как единый бинарный блоб (blob).

model = create_model()

model.fit(train_images, train_labels, epochs=5)

# Сохраняем модель целиком в HDF5 файле
model.save('my_model.h5')

Epoch 1/5
1000/1000 [==============================] - 0s 317us/step - loss: 1.1730 - acc: 0.6640
Epoch 2/5
1000/1000 [==============================] - 0s 109us/step - loss: 0.4257 - acc: 0.8790
Epoch 3/5
1000/1000 [==============================] - 0s 106us/step - loss: 0.2889 - acc: 0.9240
Epoch 4/5
1000/1000 [==============================] - 0s 104us/step - loss: 0.2171 - acc: 0.9390
Epoch 5/5
1000/1000 [==============================] - 0s 106us/step - loss: 0.1615 - acc: 0.9670

Теперь пересоздаем модель из этого файла:

# Пересоздаем ту же самую модель, включая веса и оптимизатор.
new_model = keras.models.load_model('my_model.h5')
new_model.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_12 (Dense)             (None, 512)               401920    
_________________________________________________________________
dropout_6 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_13 (Dense)             (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

Проверяем ее аккуратность:

loss, acc = new_model.evaluate(test_images, test_labels)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))

1000/1000 [==============================] - 0s 78us/step
Restored model, accuracy: 86.60%

Эта техника сохраняет все:

  • Значения весов
  • Конфигурацию модели (архитектуру)
  • Конфигурацию оптимизатора

Keras сохраняет модели, проверяя архитектуру. На данный момент не доступно сохранение TensorFlow оптимизаторов (из tf.train). При их использовании необходимо перекомпилировать модель после загрузки - при этом освобождается состояние оптимизатора.