четверг, 1 ноября 2018 г.

TensorFlow: Datasets для Estimators

tf.data модуль содержит коллекцию классов, которые позволяют легко загружать данные, манипулировать ими, и направлять их в модель. В этом посте мы рассмотрим два простых примера:

  • Чтение данных в память из numpy массивов.
  • Чтение строк из csv файла.

Базовый ввод

Взятие частей данных из массива - это самый простой способ начать использовать tf.data.

В предыдущих постах была описана следующая train_input_fn функция, из iris_data.py, передающая данные в Estimator:

def train_input_fn(features, labels, batch_size):
    """Функция ввода для тренировки"""
    # Преобразуем ввод в Dataset.
    dataset = tf.data.Dataset.from_tensor_slices(
                                   (dict(features), labels))

    # Перемешиваем, повторяем, и пакетизируем примеры.
    dataset = dataset.shuffle(1000).repeat().batch(batch_size)

    # Возвращаем dataset.
    return dataset

Рассмотрим подробней.

Аргументы

Эта функция предполагает три аргумента. Аргументы ожидаются "массивами", но могут принимать почти все что угодно, что может быть преобразовано к массиву с использованием numpy.array. Единственное исключение - это tuple, который имеет специальное значение для Datasets.

  • features: {'feature_name':array} словарь (или DataFrame), содержащий сырые вводные свойства.
  • labels: массив, содержащий метку для каждого примера.
  • batch_size: число, обозначающее желаемый размер пакета.

В premade_estimator.py мы извлекаем Iris данные, используя iris_data.load_data() функцию.

import iris_data

# Извлекаем данные 
train, test = iris_data.load_data()
features, labels = train

Затем мы передаем эти данные функции ввода:

batch_size=100
iris_data.train_input_fn(features, labels, batch_size)

Рассмотрим train_input_fn().

Части (slices)

Функция начинается использованием tf.data.Dataset.from_tensor_slices функции для создания tf.data.Dataset, представляющего части массива. Массив разделен на части по первому измерению. Например, массив содержит MNIST тренировочные данные, имеющие форму (60000, 28, 28). Передав его from_tensor_slices получим Dataset объект, содержащий 60000 частей, каждый из которых изображение 28x28.

train, test = tf.keras.datasets.mnist.load_data()
mnist_x, mnist_y = train

mnist_ds = tf.data.Dataset.from_tensor_slices(mnist_x)
print(mnist_ds)

Это распечатает следующий вывод, показывающий формы и типы элементов в датасете. Следует отметить, что Dataset не знает сколько элементов он содержит.

<TensorSliceDataset shapes: (28,28), types: tf.uint8>

Dataset выше представляет простую коллекцию массивов, но датасеты способны на большее, чем это. Dataset может обрабатывать любые вложенные комбинации словарей или таплов (tuples) (или namedtuple).

Например, после преобразования свойств ирисов к стандартному Python словарю, можно конвертировать словарь массивов в Dataset словарей следующим образом:

dataset = tf.data.Dataset.from_tensor_slices(dict(features))
print(dataset)

<TensorSliceDataset

  shapes: {
    SepalLength: (), PetalWidth: (),
    PetalLength: (), SepalWidth: ()},

  types: {
      SepalLength: tf.float64, PetalWidth: tf.float64,
      PetalLength: tf.float64, SepalWidth: tf.float64}
>

Здесь мы видим, что, когда Dataset содержит структурированные элементы, формы и типы Dataset берет той же структуры. Этот датасет содержит словари скаляров, все типа tf.float64.

Первая строка iris train_input_fn использует ту же функциональность, но добавляет другой уровень структуры. Она создает датасет, содержащий (features_dict, label) пары.

Следующий код показывает, что метка - это скаляр с типом int64.

# Преобразуем ввод в Dataset.
dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels))
print(dataset)

<TensorSliceDataset
    shapes: (
        {
          SepalLength: (), PetalWidth: (),
          PetalLength: (), SepalWidth: ()},
        ()),

    types: (
        {
          SepalLength: tf.float64, PetalWidth: tf.float64,
          PetalLength: tf.float64, SepalWidth: tf.float64},
        tf.int64)>

Манипуляция

На текущий момент Dataset будет проходить данные только единожды, в фиксированном порядке, и произведет только один элемент за раз. Требуется дальнейшая обработка перед использованием в тренировке. К счастью, tf.data.Dataset класс предоставляет методы для более качественной обработки данных перед тренировкой. Следующий код функции ввода использует преимущество использования нескольких из этих методов:

# Перемешиваем, повторяем, и пакетизируем примеры.
dataset = dataset.shuffle(1000).repeat().batch(batch_size)

tf.data.Dataset.shuffle метод использует буфер фиксированного размера для перемешивания элементов, в то время как они проходят через метод. В этом случае buffer_size больше, чем количество примеров в Dataset.

tf.data.Dataset.repeat метод рестартует Dataset, когда он достигает конца. Для ограничения количества эпох, задавайте count аргумент.

tf.data.Dataset.batch метод собирает ряд примеров и складывает их друг на друга для создания пакетов. Это добавляет измерение к их форме. Новое измерение добавляется как первое измерение. Следующий код использует batch метод на MNIST Dataset. В результате получается Dataset, содержащий 3D массивы, представляющие стеки (28,28) изображений:

print(mnist_ds.batch(100))

<BatchDataset
  shapes: (?, 28, 28),
  types: tf.uint8>

Отметим, что датасет имеет неизвестный размер пакета, потому что последний пакет будет иметь меньше элементов.

В train_input_fn, после пакетизации Dataset содержит 1D вектора элементов, где ранее были скаляры:

print(dataset)

<TensorSliceDataset
    shapes: (
        {
          SepalLength: (?,), PetalWidth: (?,),
          PetalLength: (?,), SepalWidth: (?,)},
        (?,)),

    types: (
        {
          SepalLength: tf.float64, PetalWidth: tf.float64,
          PetalLength: tf.float64, SepalWidth: tf.float64},
        tf.int64)>

Вывод (return)

В этой точке Dataset содержит (features_dict, labels) пары. Это формат, ожидаемый train и evaluate методами, таким образом input_fn возвращает датасет.

Метки могут/должны быть пропущены при использовании predict метода.


Чтение CSV файла

Наиболее распространенный способ применения Dataset класса - это создание потока данных с файлов на диске. tf.data включает различные функции чтения файлов. Рассмотрим получение Iris датасета из csv файла с использованием Dataset.

Следующий вызов iris_data.maybe_download функции загружает данные если необходимо и возвращает путь результирующих файлов:

import iris_data
train_path, test_path = iris_data.maybe_download()

iris_data.csv_input_fn функция содержит альтернативную реализацию, которая обрабатывает csv файлы, используя Dataset.

Рассмотрим как построить совместимую с Estimator функцию ввода, которая производит чтение с локальных файлов.

Строим Dataset

Мы начнем с построения tf.data.TextLineDataset объекта для чтения файла по одной строке за раз. Затем мы вызываем tf.data.Dataset.skip метод, чтобы пропустить первую строку файла, которая содержит заголовок, а не пример:

ds = tf.data.TextLineDataset(train_path).skip(1)

Строим парсер csv файла

Мы начнем с построения функции для парсинга одной строки.

Следующая функция iris_data.parse_line выполняет такую задачу, используя tf.decode_csv функцию, и немного python кода.

Мы должны парсить каждую строку в датасете для того, чтобы сгенерировать необходимые (свойства, метка)(features, label) пары. Следующая _parse_line функция вызывает tf.decode_csv, чтобы разбирать одну строку на ее свойства и метку. Ввиду того, что Estimators требуют, чтобы свойства были представлены как словарь, мы полагаемся на Python dict и zip функции для построения этого словаря. Имена свойств - это ключи этого словаря. Затем мы вызываем pop метод словаря, чтобы удалить поле метки из словаря свойств:

# Метеданные, описывающие текстовые колонки
COLUMNS = ['SepalLength', 'SepalWidth',
           'PetalLength', 'PetalWidth',
           'label']
FIELD_DEFAULTS = [[0.0], [0.0], [0.0], [0.0], [0]]
def _parse_line(line):
    # Декодируем строку в ее поля 
    fields = tf.decode_csv(line, FIELD_DEFAULTS)

    # Упаковываем результат в словарь
    features = dict(zip(COLUMNS,fields))

    # Отделяем метку от свойств
    label = features.pop('label')

    return features, label

Разбираем строки

Datasets имеет много методов для манипулирования данными во время прохождения их к модели. Наиболее используемый метод - это tf.data.Dataset.map, который применяет трансформацию к каждому элементу в Dataset.

map метод принимает map_func аргумент, который описывает как каждый элемент в Dataset должен быть трансформирован.

tf.data.Dataset.map метод применяет `map_func` для трансформации каждого элемента в Dataset.

Таким образом, при разборе строки идут потоком из csv файла, мы передаем нашу _parse_line функцию map методу:

ds = ds.map(_parse_line)
print(ds)

<MapDataset
shapes: (
    {SepalLength: (), PetalWidth: (), ...},
    ()),
types: (
    {SepalLength: tf.float32, PetalWidth: tf.float32, ...},
    tf.int32)>

Теперь вместо простых скалярных строк датасет содержит (features, label) пары.

Попробуйте это

Эта функция может быть использована как замещение для iris_data.train_input_fn. Она может быть использована для наполнения estimator'а:

train_path, test_path = iris_data.maybe_download()

# Все вводы - числовые
feature_columns = [
    tf.feature_column.numeric_column(name)
    for name in iris_data.CSV_COLUMN_NAMES[:-1]]

# Строим estimator
est = tf.estimator.LinearClassifier(feature_columns,
                                    n_classes=3)
# Тренируем estimator
batch_size = 100
est.train(
    steps=1000,
    input_fn=lambda : iris_data.csv_input_fn(train_path, batch_size))

Estimators ожидают, что input_fn не имеет аргументов. Для того чтобы обойти это ограничение используем lambda, чтобы захватить аргументы и предоставить ожидаемый интерфейс.

Заключение

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