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 имеет простые и удобные методы для применения широкого спектра стандартных и кастомных преобразований.