среда, 5 февраля 2020 г.

Перевод R interface to TensorFlow Dataset API

R interface to TensorFlow Dataset API

Перевод https://tensorflow.rstudio.com/tools/tfdatasets/articles/introduction.html

API TensorFlow Dataset обеспечивает возможности создания масштабируемых пайплайнов для передачи данных в модели TensorFlow, в том числе:
  • чтение данных из различных форматов, включая CSV и TFRecords (стандартный бинарный формат входных данных для TensorFlow);
  • преобразования наборов данных, включая применение к ним произвольных функций;
  • перемешивание наборов данных, разбивка на батчи и повторение набора по числу эпох;
  • интерфейс потоковой передачи для чтения сколь угодно больших наборов данных;
  • чтение и преобразование данных являются операциями в составе вычислительного графа TensorFlow, поэтому выполняются кодом на С++ параллельно с обучением модели.
R-интерфейс к TensorFlow Datasets предоставляет доступ к API, включая высокоуровневые функции для удобной интеграции с R-пакетом keras (а также tfestimators, документация по которому на момент создания перевода удалена с официального сайта - прим. пер.).
Исходник этого документа и файлы примеров можно найти в репозитории.

Установка

Для использования tfdatasets нужно установить библиотеку TensorFlow и соответствующий R-пакет.
Сперва установите tfdatasets с GitHub:
# devtools::install_github("rstudio/tfdatasets")
remotes::install_github("rstudio/tfdatasets")
Затем используйте функцию install_tensorflow() для установки TensorFlow:
library(tfdatasets)
install_tensorflow()
Не забывайте перед использованием R-пакета tensorflow указать, куда установлена Python-овская библиотека, например, reticulate::use_condaenv("r-reticulate") - прим. пер.

Создание набора данных

Наборы данных создаются из текстовых файлов, файлов в формате tfrecords или из данных в ОЗУ при помощи соответствующих функций.

Текстовые файлы

Например, для создания набора данных из текстового файла сперва нужно создать спецификацию того, как записи будут декодироваться при чтении из файла, а затем вызвать text_line_dataset() с именем файла и спецификацией:
library(tfdatasets)

# создание спецификации для парсинга файла
iris_spec <- csv_record_spec("data/iris.csv")

# чтение набора данных
dataset <- text_line_dataset("data/iris.csv", record_spec = iris_spec) 

# структура полученного набора данных
str(dataset)
## <MapDataset shapes: {Sepal.Length: (), Sepal.Width: (), Petal.Length: (), Petal.Width: (), Species: ()}, types: {Sepal.Length: tf.float32, Sepal.Width: tf.float32, Petal.Length: tf.float32, Petal.Width: tf.float32, Species: tf.string}>
В этом примере функция csv_record_spec() обрабатывает файл с образцом данных, который используется для автоматического определения имен и типов столбцов (для этого читается до 1000 первый строк в файле). Вы также можете явно задать имена и/или типы данных для столбцов при помощи параметров names и types (обратите внимание, что файл-образец при этом не нужен):
# задаем имена и типы данных
iris_spec <- csv_record_spec(
  names = c("SepalLength", "SepalWidth", "PetalLength", "PetalWidth", 
            "Species"),
  types = c("double", "double", "double", "double", 
            "character"), 
  skip = 1
)

# чтение набора данных
dataset <- text_line_dataset("data/iris.csv", record_spec = iris_spec)
Отметим, что мы также указали skip = 1, чтобы пропустить первую строку в файле, содержащую имена столбцов.
Поддерживаемые типы: целое число (integer), число с плавающей точкой (double) и строка (character). Вы также можете указывать типы в более компактной форме посредством односимвольных аббревиатур (например, types = "dddi"):
mtcars_spec <- csv_record_spec("data/mtcars.csv", types = "dididddiiii")

Параллельный парсинг

Парсинг текстовых строк в требуемый формат может быть вычислительно затратным. Вы можете выполнять эти вычисления в параллельной режиме при помощи параметра parallel_record. Например:
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec, 
                             parallel_records = 4)
Также вы можете распараллелить чтение данных с диска при помощи буфера для предварительно загружаемых данных. Функция, которая позволяет импортировать данные подобным образом - dataset_prefetch():
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4) %>% 
  dataset_batch(128) %>% 
  dataset_prefetch(1)
Результатом будет предварительная загрузка батча данных в фоновом потоке, то есть параллельно с операциями обучения модели.
При наличии нескольких входных файлов вы можете распараллелить их чтение по нескольким ПК (шардирование) и/или по нескольким потокам на одном ПК (параллельное чтение с чередованием). Подробнее об этом см. в разделе “Чтение множественных файлов”.

Файлы tfrecords

Вы можете загружать данные из файлов в формате tfrecords при помощи функции tfrecord_dataset().
Часто бывает нужно отобразить записи из набора данных в последовательность именованных столбцов. Это можно сделать при помощи вызова dataset_map() в сочетании с функцией tf$parse_single_example():
# Создание набора данных, который читает наблюдения из двух файлов и 
# извлекает изображение с меткой класса
filenames <- c("/var/data/file1.tfrecord", 
               "/var/data/file2.tfrecord")
dataset <- tfrecord_dataset(filenames) %>%
  dataset_map(function(example_proto) {
    features <- list(
      image = tf$FixedLenFeature(shape(), tf$string),
      label = tf$FixedLenFeature(shape(), tf$int32)
    )
    tf$parse_single_example(example_proto, features)
  })
Чтение файлов tfrecords также можно распараллелить при помощи параметра num_parallel_reads:
filenames <- c("/var/data/file1.tfrecord", 
               "/var/data/file2.tfrecord")
dataset <- tfrecord_dataset(filenames, num_parallel_reads = 4)

Базы данных SQLite

Чтение наборов данных из БД SQLite осуществляется при помощи функции sqlite_dataset(). Для этого нужно указать имя файла с базой данный, SQL-запрос и спецификацию sql_record_spec(), которая описывает имена и типы данных для столбцов, участвующих в запросе. Например:
record_spec <- sql_record_spec(
  names = c("disp", "drat", "vs", "gear", "mpg", 
            "qsec", "hp", "am", "wt",  "carb", "cyl"),
  types = c(tf$float64, tf$int32, tf$float64, tf$int32, tf$float64, 
            tf$float64, tf$float64, tf$int32, tf$int32, tf$int32, tf$int32)
)

dataset <- sqlite_dataset(
  "data/mtcars.sqlite3",
  "select * from mtcars",
  record_spec
)

dataset
## <MapDataset shapes: {disp: (), drat: (), vs: (), gear: (), mpg: (), qsec: (), hp: (), am: (), wt: (), carb: (), cyl: ()}, types: {disp: tf.float64, drat: tf.int32, vs: tf.float64, gear: tf.int32, mpg: tf.float64, qsec: tf.float64, hp: tf.float64, am: tf.int32, wt: tf.int32, carb: tf.int32, cyl: tf.int32}>
Обратите внимание: для чисел с плавающей точкой должны нужно использовать tf$float64, поскольку tf$float32не поддерживаются при чтении из БД SQLite.

Преобразования

Применение преобразований

Вы можете применить произвольные функции для преобразования данных к записях в наборе данных при помощи dataset_map(). Например, преобразовать столбец “Species” при помощи прямого кодирования (one-hot encoding) можно следующим образом:
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map(function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  })

dataset
## <MapDataset shapes: {SepalLength: (), SepalWidth: (), PetalLength: (), PetalWidth: (), Species: (3,)}, types: {SepalLength: tf.float32, SepalWidth: tf.float32, PetalLength: tf.float32, PetalWidth: tf.float32, Species: tf.float32}>
Обратите внимание, что dataset_map() реализована как функция на языке R, но некоторые специальные ограничения, налагаемые на нее, позволяют ее выполнять не в интерпретаторе R, а как часть вычислительного графа TensorFlow.
Для набора данных, созданного при помощи функции text_line_dataset(), передаваемая запись будет представлять собой именованный список тензоров (один тензор для каждого столбца). Возвращаемое значение должно быть другим набором тензоров, которые создаются при помощи функций TensorFlow (например, tf$one_hot()). Вызов функции dataset_map() конвертируется в операции вычислительного графа TensorFlow, которые выполняют пребразования с использованием нативного кода.

Преобразования в параллельной режиме

Вычислительно затратные преобразования могут выполняться в несколько потоков при помощи параметра num_parallel_calls:
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map(num_parallel_calls = 4, function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  })
Вы можете контролировать максимальное количество буферизируемых элементов при помощи dataset_prefetch():
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map(num_parallel_calls = 4, function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  }) %>% 
  dataset_prefetch(1)
При использовании батчей по время обучения можно оптимизировать производительность c помощью функции dataset_map_and_batch(), объединяющей операции преобразования данных и создания батча:
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map_and_batch(batch_size = 128, function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  }) %>% 
  dataset_prefetch(1)

Выбор элементов

Вы можете отбирать элементы в наборе данных по условия при помощи функции dataset_filter(), которая принимает на вход предикат и возвращает булев тензор для записей, которые должны быть включены в выдачу:
mtcars_spec <- csv_record_spec("data/mtcars.csv", types = "dididddiiii")

dataset <- text_line_dataset("data/mtcars.csv",
                             record_spec = mtcars_spec) %>%
  dataset_filter(function(record) {
    record$mpg >= 20
})

dataset <- text_line_dataset("data/mtcars.csv",
                             record_spec = mtcars_spec) %>%
  dataset_filter(function(record) {
    record$mpg >= 20 & record$cyl >= 6L
  })
Заметим, что функции внутри предиката должны быть тензорными операциями (tf\(not_equal, tf\)less и т.д.). Предоставляются соответствующие методы для стандартных операторов сравнения (<, >, <=) и логических операторов (!, &, |).

Признаки и целевая переменная

Распространенным преобразованием является превращение таблицы, созданной при помощи text_line_dataset() или tfrecord_dataset(), в список из двух элементов: “x” (признаки) и “y” (целевая переменная). Для этих целей служит функция dataset_prepare():
mtcars_dataset <- text_line_dataset("data/mtcars.csv", 
                                    record_spec = mtcars_spec) %>% 
  dataset_prepare(x = c(mpg, disp), y = cyl)

iris_dataset <- text_line_dataset("data/iris.csv", 
                                  record_spec = iris_spec) %>% 
  dataset_prepare(x = -Species, y = Species)
Функция dataset_prepare() также также работает со стандартным формульным интерфейсом R:
mtcars_dataset <- text_line_dataset("data/mtcars.csv", 
                                    record_spec = mtcars_spec) %>% 
  dataset_prepare(cyl ~ mpg + disp)
Если при обучении вы используете батчи, при помощи параметра batch_size можно объединить этапы dataset_prepare() и dataset_batch(), что, как правило, ускоряет работу:
mtcars_dataset <- text_line_dataset("data/mtcars.csv", 
                                    record_spec = mtcars_spec) %>% 
  dataset_prepare(cyl ~ mpg + disp, batch_size = 16)

Перемешивание данных и формирование батчей

Существует несколько функций, управляющих формированием батчей. Например, следующий код задает формирование батчей по 128 элементов из наблюдений, перемешиваемых внутри скользящего окна размером 1000; набор данных повторяется 10 раз для обучения в течение 10 эпох:
dataset <- dataset %>% 
  dataset_shuffle(1000) %>%
  dataset_repeat(10) %>% 
  dataset_batch(128)
Ранее производительность можно было оптимизировать путем объединения перемешивания и повторения данных в один шаг при помощу функции dataset_shuffle_and_repeat(), но теперь так делать не рекомендуется:
dataset <- dataset %>% 
  dataset_shuffle_and_repeat(buffer_size = 1000, count = 10) %>%
  dataset_batch(128)
# shuffle_and_repeat (from tensorflow.python.data.experimental.ops.shuffle_ops) 
# is deprecated and will be removed in a future version.
# Instructions for updating:
# Use `tf.data.Dataset.shuffle(buffer_size, seed)` followed by 
# `tf.data.Dataset.repeat(count)`. Static tf.data optimizations will 
# take care of using the fused implementation.

Предварительная загрузка

Ранее мы упоминали функцию dataset_prefetch(), позволяющую предварительно загружать указанное количество элементов (или батчей, если таковые используются). Например:
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map_and_batch(batch_size = 128, function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  }) %>% 
  dataset_prefetch(1)
При использовании GPU данные могут быть предварительно загружены в память GPU с помощью dataset_prefetch_to_device():
dataset <- text_line_dataset("data/iris.csv", 
                             record_spec = iris_spec,
                             parallel_records = 4)

dataset <- dataset %>% 
  dataset_map_and_batch(batch_size = 128, function(record) {
    record$Species <- tf$one_hot(
      tf$strings$to_number(record$Species, tf$int32), 
      3L)
    record
  }) %>% 
  dataset_prefetch_to_device("/gpu:0")
В последнем примере размер буфера для предварительной загрузки определяется автоматически (вручную его можно задать параметром buffer_size).

Полный пример

Ниже приводится полный пример совместного использования различных преобразований: фильтрация наблюдений, разбивка на “x” и “y”, перемешивание и деление на батчи.
dataset <- text_line_dataset("data/mtcars.csv", 
                             record_spec = mtcars_spec) %>%
  dataset_filter(function(record) {
    record$mpg >= 20 & record$cyl >= 6L
  }) %>% 
  dataset_shuffle_and_repeat(buffer_size = 1000, count = 10) %>% 
  dataset_prepare(cyl ~ mpg + disp, batch_size = 128) %>% 
  dataset_prefetch(1)

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

Методы чтения данных различаются в зависимости от API, используемых при построении моделей. Наборы данных tfdatasets могут быть использованы совместно с Keras примерно так же, как хранящиеся в ОЗУ матрицы и массивы; при использовании низкоуровневого интерфейса TensorFlow потребуется явно вызывать функцию-итератор.

В этом разделе приводятся примеры для обоих вариантов.


В этом разделе приводятся примеры для обоих вариантов.

Пакет keras

ВАЖНОЕ ПРИМЕЧАНИЕ: для этих примеров требуются актуальные версии Keras (>=2.2) и TensorFlow (>=1.9). Установить их можно стандартным способом:
library(keras)
install_keras()
Модели Keras часто обучаются путем передачи хранящихся в памяти массивов в функцию fit():
model %>% fit(
  x_train, y_train, 
  epochs = 30, 
  batch_size = 128
)
Это требует предварительной загрузки данных в таблицу или матрицу. Можно использовать train_on_batch() для передачи данных в виде батчей, но и при этом все преобразования выполняются на стороне R, а не в нативном коде.
Альтернативой является передача в функции fit() и evaluate() наборов данных, созданных при помощи tfdatasets.
Ниже рассмотрен пример для классической задачи классификации MNIST.
Создать файлы в формате tfrecords можно при помощи данного скрипта, заменив в нем tf.python_io.TFRecordWriter на tf.io.TFRecordWriter и указав numFiles = 1 - прим. пер.
library(keras)
library(tfdatasets)

batch_size = 128
steps_per_epoch = 500

# Функция для чтения и обработки набора данных MNIST
mnist_dataset <- function(filename) {
  dataset <- tfrecord_dataset(filename) %>%
    dataset_map(function(example_proto) {

      # Парсинг 
      features <- tf$io$parse_single_example(
        example_proto,
        features = list(
          height = tf$io$FixedLenFeature(shape(), tf$int64),
          width = tf$io$FixedLenFeature(shape(), tf$int64),
          img_string = tf$io$VarLenFeature(tf$float32),
          label = tf$io$FixedLenFeature(shape(), tf$int64)
        )
      )

      # Обработка изображения
      image <- tf$divide(features$img_string, 255)
      image <- tf$sparse$to_dense(image)
      # image <- tf$reshape(image, c(features$height, features$width))
      # image <- tf$expand_dims(image, -1L)
      
      # one-hot кодирование метки класса
      label <- tf$one_hot(tf$cast(features$label, dtype = tf$int32), 10L)

      list(image, label)
    }) %>%
    dataset_repeat() %>%
    dataset_shuffle(1000) %>%
    dataset_batch(batch_size, drop_remainder = TRUE) %>%
    dataset_prefetch(1)
}

# Проверка
# iter <- dataset %>% make_iterator_one_shot()
# iterator_get_next(iter)
  
model <- keras_model_sequential() %>%
  layer_dense(units = 256, activation = "relu", input_shape = c(784)) %>%
  layer_dropout(rate = 0.4) %>%
  layer_dense(units = 128, activation = "relu") %>%
  layer_dropout(rate = 0.3) %>%
  layer_dense(units = 10, activation = "softmax")

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_rmsprop(),
  metrics = c('accuracy')
)

history <- model %>% fit(
  mnist_dataset("data/MNIST_train_data_strings_0.tfrecord"),
  steps_per_epoch = steps_per_epoch,
  epochs = 20,
  validation_data = mnist_dataset("data/MNIST_train_data_strings_0.tfrecord"),
  validation_steps = steps_per_epoch
)

score <- model %>% evaluate(
  mnist_dataset("mnist/test.tfrecords"),
  steps = steps_per_epoch
)

print(score)
Обратите внимание, что все предварительная обработка выполняется внутри dataset_map(). Мы указываем drop_remainder = TRUE, чтобы все батчи получались одинакового размера, как того требует Keras.

Пакет tensorflow

Для получения батчей в виде тензоров служат функции make_iterator_one_shot() и iterator_get_next():
dataset <- text_line_dataset("data/mtcars.csv", 
                             record_spec = mtcars_spec) %>% 
  dataset_prepare(cyl ~ mpg + disp) %>% 
  dataset_shuffle(20) %>% 
  dataset_batch(5)

iter <- make_iterator_one_shot(dataset)
next_batch <- iterator_get_next(iter)
next_batch
## $x
## tf.Tensor(
## [[ 33.9  71.1]
##  [ 19.2 167.6]
##  [ 32.4  78.7]
##  [ 15.2 275.8]
##  [ 15.2 304. ]], shape=(5, 2), dtype=float32)
## 
## $y
## tf.Tensor([4 6 4 8 8], shape=(5,), dtype=int32)
При их использовании нужно указывать, на каком этапе прекращать генерацию новых батчей. Один из подходов состоит в бесконечном повторении набора данных при помощи функции dataset_repeat() с указанием требуемого количества итераций (steps <- 200):
mtcars_spec <- csv_record_spec("data/mtcars.csv")
dataset <- text_line_dataset("data/mtcars.csv", 
                             record_spec = mtcars_spec) %>% 
  dataset_shuffle(5000) %>% 
  dataset_repeat() %>% # повторять бесконечно
  dataset_prepare(x = c(mpg, disp), y = cyl) %>% 
  dataset_batch(128)  

iter <- make_iterator_one_shot(dataset)

steps <- 200
for (i in 1:steps) {
  # обучение модели
}
Вместо указания количества итераций в явном виде можно задать критерий останова, например, на основании выхода кривой обучения на плато.
Другой подход заключается в определении момента, когда все батчи были получены из набора данных. После исчерпания батчей возникает исключение out of range, которое может быть перехвачено при помощи out_of_range_handler и функции tryCatch():
tryCatch({
  while(TRUE) {
    batch <- iterator_get_next(iter)
    str(batch)
  }
}, error = out_of_range_handler)
Этот код можно переписать более изящно, используя функцию until_out_of_range():
until_out_of_range({
  batch <- iterator_get_next(iter)
  str(batch)
})

Чтение множественных файлов

Множественные файлы могут обрабатываться параллельно на одном или нескольких ПК. Возможности для этого предоставляет функция read_files().
Пример с чтением всех CSV-файлов в папке посредством функции text_line_dataset():
dataset <- read_files("data/*.csv", 
                      text_line_dataset, 
                      record_spec = mtcars_spec,
                      parallel_files = 4, 
                      parallel_interleave = 16) %>% 
  dataset_prefetch(5000) %>% 
  dataset_shuffle_and_repeat(buffer_size = 1000, count = 3) %>% 
  dataset_batch(128)
parallel_files = 4 задает параллельное чтение 4 файлов, а parallel_interleave = 16 обеспечивает наличие в итоговом наборе данных блоков из 16 последовательных записей из каждого входного файла.

Использование нескольких ПК

При обучении на нескольких ПК можно использовать параллельную загрузку данных на каждом из них (шардирование):
# Флаги скрипта для обучения (информация о шардировании предоставляется 
# управляющим кодом, который запускает обучение)
FLAGS <- flags(
  flag_integer("num_shards", 1),
  flag_integer("shard_index", 1)
)

dataset <- read_files("data/*.csv", 
                      text_line_dataset, 
                      record_spec = mtcars_spec,
                      parallel_files = 4, 
                      parallel_interleave = 16,
                      num_shards = FLAGS$num_shards, 
                      shard_index = FLAGS$shard_index) %>% 
  dataset_shuffle_and_repeat(buffer_size = 1000, count = 3) %>% 
  dataset_batch(128) %>% 
  dataset_prefetch(1)

Комментариев нет:

Отправить комментарий