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

Основы TensorFlow Core

В это посте мы рассмотрим основы использования низкоуровневых TensorFlow API (TensorFlow Core), вы узнаете как:

  • Манипулировать TensorFlow программой (tf.Graph) и рабочей средой исполнения TensorFlow (tf.Session), вместо того чтобы полагаться на Estimator и на его управление данными компонентами.
  • Выполнять TensorFlow операции, используя tf.Session.
  • Использовать высокоуровневые компоненты (datasets, layers, и feature_columns) в низкоуровневом окружении.
  • Строить собственный тренировочный цикл, вместо использования одного из предоставляемых Estimators.

В большинстве случаев, когда это возможно, рекомендуется использовать высокоуровневые API для построения моделей, такие как TensorFlow Estimators. Однако, знакомство с TensorFlow Core полезно по следующим причинам:

  • Экспериментирование и отладка проще при использовании низкоуровневых TensorFlow API напрямую.
  • Это дает понимание того как механизмы высокоуровневых API работают внутри.

Установка

Перед использованием примеров в данном посте установите TensorFlow.

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

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

from future import absolute_import
from future import division
from future import print_function

import numpy as np
import tensorflow as tf

Значения тензора

Центральный элемент данных в TensorFlow - это тензор (tensor). Тензор состоит из набора примитивных значений, сформированных в массив с любым количеством измерений. Разряд тензора - это количество его измерений, в то время как его форма - это тапл (tuple) чисел, определяющий длину массива в каждом измерении. Вот несколько примеров значений тензора:

3. # тензор 0 разряда; скаляр с формой [],
[1., 2., 3.] # тензор 1 разряда; вектор с формой [3]
[[1., 2., 3.], [4., 5., 6.]] # тензор 2 разряда; матрица с формой [2, 3]
[[[1., 2., 3.]], [[7., 8., 9.]]] # тензор 3 разряда с формой [2, 1, 3]

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

Прохождение TensorFlow Core

Можно считать, что TensorFlow Core программы состоят из двух отдельных частей:

  • Построение вычислительного графа (tf.Graph).
  • Выполнение вычислительного графа (используя tf.Session).

Граф

Вычислительный граф - это серии TensorFlow операций, расположенные в графе. Граф состоит из объектов двух типов.

  • tf.Operation (или "ops") - это узлы графа (nodes). Операции описывают вычисления, которые потребляют и производят тензоры.
  • tf.Tensor - это края в графе. Они представляют значения, которые будут проходить через граф. Большинство TensorFlow функций возвращают tf.Tensors.

Важно отметить, что tf.Tensors не имеют значений, они являются просто ручками (рычагами)(handle) для элементов в вычислительном графе.

Построим простой вычислительный граф. Самая базовая операция - это constant. Python функция, которая строит эту операцию, принимает значение тензора как ввод. Получающаяся операция не принимает вводов. При исполнении она выводит значение, которое было передано конструктору. Мы создадим нецельночисловые константы a и b:

a = tf.constant(3.0, dtype=tf.float32)
b = tf.constant(4.0) # также tf.float32
total = a + b
print(a)
print(b)
print(total)

print выражения выводят следующее:

Tensor("Const:0", shape=(), dtype=float32)
Tensor("Const_1:0", shape=(), dtype=float32)
Tensor("add:0", shape=(), dtype=float32)

Отметим, что печать тензоров не выводит значения 3.0, 4.0, и 7.0, как можно было предположить. Приведенные выше выражения только строят вычислительный граф. Эти tf.Tensor объекты просто представляют результаты операций, которые будут выполнены.

Каждой операции в графе дано уникальное имя. Это имя независимо от имен объектов, назначаемых к ним в Python. Тензоры имеют имя согласно операции, которая производит их, следуемое индексом вывода, как в "add:0" выше.

TensorBoard

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

Сначала вы сохраняете вычислительный граф в TensorBoard резюме файле (summary file):

writer = tf.summary.FileWriter('.')
writer.add_graph(tf.get_default_graph())

Эта команда производит файл событий в текущей директории с именем следующего формата:

events.out.tfevents.{timestamp}.{hostname}

Теперь, в новом терминале, запустите TensorBoard следующей командой:

tensorboard --logdir .

Затем откройте TensorBoard страницу графов в вашем браузере, и вы должны увидеть граф, похожий на следующий:


Сессия (Session)

Чтобы оценить тензоры, создайте инстанс объекта tf.Session, также известный как сессия. Сессия инкапсулирует состояние среды исполнения TensorFlow и выполняет TensorFlow операции. Если tf.Graph - это как .py файл, то tf.Session - это как исполняемый python код.

Следующий код создает tf.Session объект и затем вызывает его run метод для оценки total тензора, который мы создали выше:

sess = tf.Session()
print(sess.run(total))

Когда запрашиваете вывод с узла с Session.run, TensorFlow проходит обратный путь по графу и выполняет все узлы, которые предоставляют ввод для запрашиваемого узла вывода. Таким образом печатается ожидаемое значение 7.0:

7.0

Можно передавать несколько тензоров tf.Session.run. run метод обрабатывает любые комбинации таплов или словарей, как в следующем примере:

print(sess.run({'ab':(a, b), 'total':total}))

который возвращает результаты в структуре такого же формата:

{'total': 7.0, 'ab': (3.0, 4.0)}

В ходе вызова tf.Session.run любой tf.Tensor имеет только единственное значение. Например, следующий код вызывает tf.random_uniform, чтобы произвести tf.Tensor, который генерирует случайный 3-элементный вектор:

vec = tf.random_uniform(shape=(3,))
out1 = vec + 1
out2 = vec + 2
print(sess.run(vec))
print(sess.run(vec))
print(sess.run((out1, out2)))

Результат показывает различное случайное значение на каждом вызове исполнения, но согласующееся значение в ходе единичного исполнения (out1 и out2 получают одинаковый случайный ввод):

[ 0.52917576  0.64076328  0.68353939]
[ 0.66192627  0.89126778  0.06254101]
(
  array([ 1.88408756,  1.87149239,  1.84057522], dtype=float32),
  array([ 2.88408756,  2.87149239,  2.84057522], dtype=float32)
)

Некоторые TensorFlow функции возвращают tf.Operations вместо tf.Tensors. Результат вызова исполнения на Operation - это None. Вы можете исполнять операцию для получения стороннего эффекта, но не для извлечения значения. Примеры такого поведения включают инициализацию, и тренировочные операции продемонстрированные далее.

Подача данных (feeding)

Приведенный тензор не особенно интересен, поскольку он всегда производит постоянный результат. Граф может быть параметризирован, чтобы принимать внешние вводы, известные как заполнители (placeholders). Заполнитель - это обещание предоставить значение позже, как аргумент функции.

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)
z = x + y

Предыдущие три строки - это как функция, в которой мы определяем два вводных параметра (x и y) и затем операция с ними. Мы можем оценить этот граф с несколькими вводами, используя feed_dict аргумент tf.Session.run метода для того чтобы передать конкретные значения для заполнителей:

print(sess.run(z, feed_dict={x: 3, y: 4.5}))
print(sess.run(z, feed_dict={x: [1, 3], y: [2, 4]}))

Это приводит к следующему выводу:

7.5
[ 3.  7.]

Также отметим, что feed_dict аргумент может быть использован для перезаписи любого тензора в графе. Единственная разница между заполнителями и другими tf.Tensors в том, что заполнители выбрасывают ошибку если к ним не было передано никакого значения.

Наборы данных (Datasets)

Заполнители работают для простых экспериментов, но методы tf.data модуля более предпочтительны для передачи данных в модель.

Для того чтобы получить исполняемый tf.Tensor из Dataset, необходимо сначала преобразовать его в tf.data.Iterator, и затем вызвать метод tf.data.Iterator.get_next.

Простейший способ создать Iterator - это tf.data.Dataset.make_one_shot_iterator метод. Например, в следующем коде next_item тензор вернет ряд (row) из my_data массива на каждое выполнение вызова:

my_data = [
    [0, 1,],
    [2, 3,],
    [4, 5,],
    [6, 7,],
]
slices = tf.data.Dataset.from_tensor_slices(my_data)
next_item = slices.make_one_shot_iterator().get_next()

Достижение конца потока данных вызывает то, что Dataset выбрасывает tf.errors.OutOfRangeError. Например, следующий код считывает next_item до тех пор пока нет больших данных для считывания:

while True:
  try:
    print(sess.run(next_item))
  except tf.errors.OutOfRangeError:
    break

Если Dataset зависит от операции с сохранением состояния, необходимо инициализировать итератор до его использования, как показано ниже:

r = tf.random_normal([10,3])
dataset = tf.data.Dataset.from_tensor_slices(r)
iterator = dataset.make_initializable_iterator()
next_row = iterator.get_next()

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

Слои (Layers)

Тренируемая модель должна изменять значения в графе для того чтобы получить новые выводы при том же вводе. tf.layers - это предпочтительный путь для добавления тренируемых параметров в граф.

Layers упаковывают вместе переменные и операции, которые выполняются с переменными. Например тесно-связанный слой (tf.layers.Dense) выполняет взвешенное суммирование по всем вводам для каждого вывода и применяет опциональную функцию активации. Соединительные веса и смещения управляются объектом слоя (layer object).

Создание слоев

Следующий код создает tf.layers.Dense слой, который принимает в качестве ввода пакет векторов и производит единственный вывод для каждого из них. Чтобы применить слой к вводу, вызываем слой как функцию. Например:

x = tf.placeholder(tf.float32, shape=[None, 3])
linear_model = tf.layers.Dense(units=1)
y = linear_model(x)

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

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

Инициализация слоев

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

init = tf.global_variables_initializer()
sess.run(init)

Важно отметить, что вызов tf.global_variables_initializer только создает и возвращает рычаг к TensorFlow операции. Эта операция инициализирует все глобальные переменные, когда мы выполняем ее с tf.Session.run.

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

Исполнение слоев

Теперь, когда слой инициализирован, мы можем оценить выводной тензор linear_model как любой другой тензор. Например, следующий код:

print(sess.run(y, {x: [[1, 2, 3],[4, 5, 6]]}))

сгенерирует двухэлементный выводной вектор, как следующий:

[[-3.41378999]
 [-9.14999008]]

Сокращенные функции слоя (Layer Function)

Для каждого класса слоя (как tf.layers.Dense) TensorFlow также предоставляет сокращенную функцию (как tf.layers.dense). Единственная разница в том, что версии сокращенной функции создают и запускают слой в единственный вызов. Например, следующий код эквивалентен предыдущей версии:

x = tf.placeholder(tf.float32, shape=[None, 3])
y = tf.layers.dense(x, units=1)

init = tf.global_variables_initializer()
sess.run(init)

print(sess.run(y, {x: [[1, 2, 3], [4, 5, 6]]}))

Хотя это удобно, но этот подход не позволяет получить доступ к tf.layers.Layer объекту. Это делает анализ и отладку намного труднее, а переиспользование слоя невозможным.

Колонки свойств (Feature columns)

Простейший путь для экспериментирования с колонками свойств - это использование tf.feature_column.input_layer функции. Эта функция принимает тесносвязанные колонки как вводы, таким образом, для того чтобы увидеть результат категориальной колонки, вам необходимо обернуть ее в tf.feature_column.indicator_column. Например:

features = {
    'sales' : [[5], [10], [8], [9]],
    'department': ['sports', 'sports', 'gardening', 'gardening']}

department_column = \
        tf.feature_column.categorical_column_with_vocabulary_list(
                              'department', ['sports', 'gardening'])
department_column = tf.feature_column.indicator_column(
                                                  department_column)

columns = [
    tf.feature_column.numeric_column('sales'),
    department_column
]

inputs = tf.feature_column.input_layer(features, columns)

Исполнение inputs тензора обработает features (свойства) в пакет векторов.

Колонки свойств могут иметь внутреннее состояние, как слои, таким образом они часто нуждаются в инициализации. Категориальные колонки используют tf.contrib.lookup внутри и они требуют отдельной операции инициализации, tf.tables_initializer.

var_init = tf.global_variables_initializer()
table_init = tf.tables_initializer()
sess = tf.Session()
sess.run((var_init, table_init))

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

print(sess.run(inputs))

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

[[  1.   0.   5.]
 [  1.   0.  10.]
 [  0.   1.   8.]
 [  0.   1.   9.]]


Тренировка

Теперь, когда мы познакомились с основами TensorFlow Core, давайте натренируем небольшую модель регрессии вручную.

Определяем данные

Сначала зададим несколько вводов, x, и предполагаемый вывод для каждого ввода, y_true:

x = tf.constant([[1], [2], [3], [4]], dtype=tf.float32)
y_true = tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32)

Определяем модель

Затем построим простую линейную модель с одним вводом:

linear_model = tf.layers.Dense(units=1)

y_pred = linear_model(x)

Можем оценить прогнозы следующим образом:

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

print(sess.run(y_pred))

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

[[ 0.02631879]
 [ 0.05263758]
 [ 0.07895637]
 [ 0.10527515]]

Потеря (Loss)

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

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

loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)

print(sess.run(loss))

Это произведет значение потери:

2.23962

Тренировка

TensorFlow предоставляет оптимизаторы, реализующие стандартные оптимизационные алгоритмы. Они реализованы как подклассы tf.train.Optimizer. Они последовательно изменяют каждое значение с целью уменьшения потери. Простеший оптимизационный алгоритм - это градиентный спуск, реализованный в tf.train.GradientDescentOptimizer. Он измеяет каждую переменную согласно величине производной потери с учетом этой переменной. Например:

optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

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

for i in range(100):
  _, loss_value = sess.run((train, loss))
  print(loss_value)

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

1.35659
1.00412
0.759167
0.588829
0.470264
0.387626
0.329918
0.289511
0.261112
0.241046
...

Полная программа

x = tf.constant([[1], [2], [3], [4]], dtype=tf.float32)
y_true = tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32)

linear_model = tf.layers.Dense(units=1)

y_pred = linear_model(x)
loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred)

optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

init = tf.global_variables_initializer()

sess = tf.Session()
sess.run(init)
for i in range(100):
  _, loss_value = sess.run((train, loss))
  print(loss_value)

print(sess.run(y_pred))

В дальнеших постах мы рассмотрим компоненты TensorFlow Core более детально.

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

TensorFlow: использование GPU

Поддерживаемые устройства

На типичной системе присутствует несколько вычислительных устройств. В TensorFlow поддерживаются устройства типов CPU и GPU. Они представляются как строки. Например:

  • "/cpu:0": CPU вашей машины.
  • "/device:GPU:0": GPU вашей машины, если он у вас присутствует.
  • "/device:GPU:1": второй GPU вашей машины и т.д.

Если TensorFlow операция имеет обе, CPU и GPU, реализации, то приоритет будет отдан GPU устройствам, когда операция назначена на устройство. Например, matmul имеет CPU и GPU ядра. На системе с устройствами cpu:0 и gpu:0, gpu:0 будет выбрано для выполнения matmul.

Логирование размещения на устройства

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

# Создаем граф
a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
# Создаем сессию с log_device_placement установленным в True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Выполняем операцию.
print(sess.run(c))

Должен быть следующий вывод:

Device mapping:
/job:localhost/replica:0/task:0/device:GPU:0 
-> device: 0, name: Tesla K40c, pci bus
id: 0000:05:00.0
b: /job:localhost/replica:0/task:0/device:GPU:0
a: /job:localhost/replica:0/task:0/device:GPU:0
MatMul: /job:localhost/replica:0/task:0/device:GPU:0
[[ 22.  28.]
 [ 49.  64.]]

Ручное размещение на устройствах

Если вы бы хотели, чтобы определенная операция выполнялась на устройстве по вашему выбору, вместо автоматически выбранного, используйте tf.device, чтобы создать контекст устройства, так чтобы все операции внутри этого контекста имели размещение на одном и том же устройстве.

# Создаем граф.
with tf.device('/cpu:0'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
# Создаем сессию с log_device_placement установленным в True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Выполняем операцию.
print(sess.run(c))

Теперь будет видно, что a и b назначены на cpu:0. Ввиду того, что устройство не определено явно в MatMul операции, окружение исполнения TensorFlow выберет устройство, основываясь на операции и доступных устройствах (gpu:0 в этом примере) и автоматически скопирует тензоры между устройствами если необходимо.

Device mapping:
/job:localhost/replica:0/task:0/device:GPU:0 
-> device: 0, name: Tesla K40c, pci bus
id: 0000:05:00.0
b: /job:localhost/replica:0/task:0/cpu:0
a: /job:localhost/replica:0/task:0/cpu:0
MatMul: /job:localhost/replica:0/task:0/device:GPU:0
[[ 22.  28.]
 [ 49.  64.]]

Позволение роста памяти GPU

По умолчанию, TensorFlow картирует почти все из элементов GPU памяти всех GPU (объект CUDA_VISIBLE_DEVICES) видимых процессу. Это сделано для более эффективного использования относительно точных ресурсов GPU памяти на устройствах, за счет уменьшения фрагментации памяти.

В некоторых случаях для процесса желательно занимать только часть доступной памяти, или наращивать использование памяти только когда процесс требуется в этом. TensorFlow предоставляет две Config опции на Session для контролирования этого.

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

config = tf.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.Session(config=config, ...)

Второй метод - это per_process_gpu_memory_fraction опция, которая определяет долю общего объема памяти, которую должен выделять каждый видимый GPU. Например, вы можете сказать TensorFlow выделять только 40% всей памяти каждого GPU:

config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.4
session = tf.Session(config=config, ...)

Это полезно, если вы хотите по-настоящему ограничить количество GPU памяти, доступной для TensorFlow процесса.

Использование единственного GPU в системе с многими GPU

Если существует более одного GPU в вашей системе, GPU с наименьшим ID будет выбран по умолчанию. Если вы хотели бы использовать исполнение на другом GPU, вам необходимо задать предпочтение явно:

# Создаем граф.
with tf.device('/device:GPU:2'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
  c = tf.matmul(a, b)
# Создаем сессию с log_device_placement установленным в True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Выполняем операцию.
print(sess.run(c))

Если устройство, которое вы определили не существует, вы получите InvalidArgumentError:

InvalidArgumentError: Invalid argument: 
Cannot assign a device to node 'b':
Could not satisfy explicit device specification '/device:GPU:2'
   [[{ {node b}} = Const[dtype=DT_FLOAT, 
   value=Tensor, _device="/device:GPU:2"]()]]

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

# Создаем граф.
with tf.device('/device:GPU:2'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
  c = tf.matmul(a, b)
# Создаем сессию с allow_soft_placement и log_device_placement 
# установленными в True.
sess = tf.Session(config=tf.ConfigProto(
      allow_soft_placement=True, log_device_placement=True))
# Выполняем операцию.
print(sess.run(c))

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

Если вы хотели бы, чтобы TensorFlow выполнялся на нескольких GPU, вы можете конструировать вашу модель в многомерной манере, где каждый уровень назначен к различным GPU. Например:

# Создаем граф.
c = []
for d in ['/device:GPU:2', '/device:GPU:3']:
  with tf.device(d):
    a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3])
    b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2])
    c.append(tf.matmul(a, b))
with tf.device('/cpu:0'):
  sum = tf.add_n(c)
# Создаем сессию с log_device_placement установленными в True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Выполняем операцию.
print(sess.run(sum))

Вы увидите следующий вывод:

Device mapping:
/job:localhost/replica:0/task:0/device:GPU:0 
-> device: 0, name: Tesla K20m, pci bus
id: 0000:02:00.0
/job:localhost/replica:0/task:0/device:GPU:1 
-> device: 1, name: Tesla K20m, pci bus
id: 0000:03:00.0
/job:localhost/replica:0/task:0/device:GPU:2 
-> device: 2, name: Tesla K20m, pci bus
id: 0000:83:00.0
/job:localhost/replica:0/task:0/device:GPU:3 
-> device: 3, name: Tesla K20m, pci bus
id: 0000:84:00.0
Const_3: /job:localhost/replica:0/task:0/device:GPU:3
Const_2: /job:localhost/replica:0/task:0/device:GPU:3
MatMul_1: /job:localhost/replica:0/task:0/device:GPU:3
Const_1: /job:localhost/replica:0/task:0/device:GPU:2
Const: /job:localhost/replica:0/task:0/device:GPU:2
MatMul: /job:localhost/replica:0/task:0/device:GPU:2
AddN: /job:localhost/replica:0/task:0/cpu:0
[[  44.   56.]
 [  98.  128.]]

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

TensorFlow: создание кастомных Estimators

В этом посте мы рассмотрим кастомные Estimators. В частности, продемонстрируем как создавать кастомный tf.estimator.Estimator, который имитирует поведение предсозданного Estimator tf.estimator.DNNClassifier в решении классической задачи классификации ирисов. Детали использования предсозданных Estimators для Iris задачи в этом посте.

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

git clone https://github.com/tensorflow/models/
cd models/samples/core/get_started

В этом посте мы рассмотрим custom_estimator.py. Запустить его можно простой командой:

python custom_estimator.py

При желании можно легко сранивать результаты custom_estimator.py и premade_estimator.py (который находится в этой же директории).

Предсозданные vs. кастомные Estimators

Как показано на следующей иллюстрации предсозданные Estimators - это подклассы tf.estimator.Estimator базового класса, в то время как кастомные Estimators - это инстансы tf.estimator.Estimator:

Предсозданные и кастомные Estimators - это все Estimators.


Предсозданные Estimators полностью готовы для использованиня. Иногда, однако, необходим больший контроль над поведением Estimator. Вот где приходят на помощь кастомные Estimators. Вы можете создать кастомный Estimator, выполняющий все что угодно. Если вы хотите скрытые слои, связанные в необычной манере, напишите кастомный Estimator. Если вы хотите вычислять уникальную метрику для модели, напишите кастомный Estimator. В общем, если вы хотите Estimator, оптимизированный для вашей специфичной проблемы, напишите кастомный Estimator.

Функция модели (model function) (или model_fn) реализует алгоритм машинного обучения. Единственное различие между работой предсозданных Estimators и кастомных Estimators это:

  • В предсозданных Estimators кто-то уже написал функцию модели для вас.
  • В кастомных Estimators необходимо написать функцию модели самому.

Ваша функция модели может реализовывать широкий спектр алгоритмов, определяя все виды скрытых слоев и метрик. Как и функции ввода, все функции модели должны принимать стандартную группу вводных параметров и возвращать стандартную группу значений вывода. Также как функции ввода используют механизмы Dataset API, так и функции модели используют Layers API и Metrics API.

Рассмотрим как разрешить Iris задачу с кастомным Estimator. В качестве напоминания - вот организация Iris модели, которую мы пробуем имитировать:

Наша реализация Iris содержит четыре свойства, два скрытых слоя и слой вывода.

Напишем функцию ввода

Наша кастомная реализация Estimator использует ту же функцию ввода, что и реализация предсозданного Estimator, из iris_data.py. А именно:

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)

    # Возвращаем конец пайплайна для чтения.
    return dataset.make_one_shot_iterator().get_next()

Эта функция ввода строит пайплайн ввода, который выводит пакеты (features, labels) пар, где features - это словарные свойства.

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

Как уже рассказывалось в предыдущих постах о Estimators, необходимо определить колонки свойств вашей модели для того, чтобы задать как модели следует использовать каждое свойство. Работаем ли мы с предсозданными или кастомными Estimators - задание колонок свойств происходит одинаковым образом.

Следующий код создает простую numeric_column для каждого вводного свойства, обозначая, что значение вводного свойства следует использовать напрямую как ввод для модели:

# Колонки свойств описывают как использовать ввод.
my_feature_columns = []
for key in train_x.keys():
    my_feature_columns.append(tf.feature_column.numeric_column(key=key))

Пишем функцию модели

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

def my_model_fn(
   features, # Это batch_features из input_fn
   labels,   # Это batch_labels из input_fn
   mode,     # Инстанс tf.estimator.ModeKeys
   params):  # Дополнительная конфигурация

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

Вызывающий может передать параметры к конструктору Estimstor'а. Любые параметры, переданные конструктору, в свою очередь, передаются в model_fn. В custom_estimator.py следующие строки создают estimator и устанавливают параметры для конфигурации модели. Этот шаг конфигурации схож с тем как конфигурировался tf.estimator.DNNClassifier в предсозданных Estimators.

classifier = tf.estimator.Estimator(
    model_fn=my_model_fn,
    params={
        'feature_columns': my_feature_columns,
        # Два скрытых слоя по 10 узлов каждый.
        'hidden_units': [10, 10],
        # Модель должны сделать выбор между 3 классами.
        'n_classes': 3,
    })

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

  • Определить модель.
  • Определить дополнительные вычисления для каждого из трех различных режимов:
    • Прогнозирование
    • Оценка
    • Тренировка

Определяем модель

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

  • Слой ввода
  • Один или больше скрытых слоев
  • Слой вывода

Определяем слой ввода

Первая строка model_fn вызывает tf.feature_column.input_layer, чтобы преобразовать словарь свойств и feature_columns во ввод для вашей модели:

# Используем input_layer для применения колонок свойств.
net = tf.feature_column.input_layer(features, params['feature_columns'])

Предыдущий код применяет преобразования, определенные колонками свойств, при создании слоя ввода модели.


Скрытые слои

Если вы создаете глубокую нейронную сеть, вы должны определить один или больше скрытых слоев. Layers API предоставляет богатый набор функций для определения всех типов скрытых слоев, включая сверточные, пуловые и дропаут слои. Для Iris мы просто собираемся вызвать tf.layers.dense, чтобы создать скрытые слои, с измерениями, определенными параметром params['hidden_layers']. В тесносвязанном (dense) слое каждый узел связан с каждым узлом в предыдущем слое.

# Строим скрытые слои, размером согласно 'hidden_units' параметру.
for units in params['hidden_units']:
    net = tf.layers.dense(net, units=units, activation=tf.nn.relu)

  • units параметр определяет количество нейронов вывода в данном слое.
  • activation параметр определяет функцию активации - Relu в данном случае.

Переменная net означает здесь текущий верхний слой сети. В ходе первой итерации net обозначает слой ввода. На каждой итерации цикла tf.layers.dense создает новый слой, который принимает вывод предыдущего слоя как свой ввод.

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

Следует отметить что tf.layers.dense предоставляет много дополнительных возможностей, включая возможность устанавливать множество параметров регуляризации. Для простоты, однако, мы примем дефолтные значения для параметров.

Слой вывода

Мы определим слой вывода, вызвав tf.layers.dense еще раз, на этот раз без функции активации:

logits = tf.layers.dense(net, params['n_classes'], activation=None)

Здесь net обозначает финальный слой вывода. Более того, полный набор слоев сейчас связан следующим образом:

Финальный скрытый слой передает результат в слой вывода.


При определении слоя вывода units параметр задает количество выводов. Таким образом, с установкой units равным params['n_classes'], модель производит одно значение вывода на класс. Каждый элемент вектора вывода будет содержать счет, или "logit", рассчитанный для соответствующего класса ириса: Setosa, Versicolor, или Virginica, соответственно.

Позже эти logits будут преобразованы в вероятности tf.nn.softmax функцией.

Реализуем тренировку, оценку, и прогнозирование.

Финальный шаг в создании функции модели - это написание ветвящегося кода, который реализует прогнозирование, оценку, и тренировку.

Функция модели вызывается когда кто-либо вызывает методы train, evaluate, или predict Estimator'а. Напомним, что сигнатура для функции модели выглядит следующим образом:

def my_model_fn(
   features, # Это batch_features из input_fn
   labels,   # Это batch_labels из input_fn
   mode,     # Инстанс tf.estimator.ModeKeys
   params):  # Дополнительная конфигурация

Сфокусируемся на третьем аргументе, mode. Как показывает следующая таблица, когда кто-то вызывает train, evaluate, или predict, Estimator вызывает функцию модели с mode параметром, установленным следующим образом:

Например, предположим вы создали кастомный Estimator, чтобы сгенерировать объект, названный classifier.

classifier = tf.estimator.Estimator(...)
classifier.train(input_fn=lambda: my_input_fn(FILE_TRAIN, True, 500))

Estimator затем вызывает функцию модели с аргументом mode установленный в ModeKeys.TRAIN.

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

Predict mode (Прогнозирование)

Когда вызывается predict метод Estimator'а, model_fn получает mode = ModeKeys.PREDICT. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий прогноз.

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

Код для генерации прогноза для этой модели выглядит следующим образом:

# Вычисляем прогнозы.
predicted_classes = tf.argmax(logits, 1)
if mode == tf.estimator.ModeKeys.PREDICT:
    predictions = {
        'class_ids': predicted_classes[:, tf.newaxis],
        'probabilities': tf.nn.softmax(logits),
        'logits': logits,
    }
    return tf.estimator.EstimatorSpec(mode, predictions=predictions)

Словарь predictions содержит все, что модель возвращает, когды выполняется в predict mode.

predictions содержит следующие три ключ/значение пары:

  • class_ids содержит идентификаторы классов (0, 1, или 2), представляющие прогноз модели наиболее вероятного вида ирисов для этого примера.
  • probabilities содержит три вероятности (в этом примере, 0.02, 0.95, и 0.03)
  • logit содержит сырые logit значения (в этом примере, -1.3, 2.6, и -0.9)

Мы возвращаем этот словарь вызывающему через predictions параметр tf.estimator.EstimatorSpec. Метод tf.estimator.Estimator.predict будет выводить этот словарь.

Рассчет потери

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

Мы можем рассчитать потерю, вызвав tf.losses.sparse_softmax_cross_entropy. Значение, возвращенное этой функцией будет приближаться к нулю, когда вероятность правильного класса (в индекс метке) будет равна почти 1.0. Возвращаемое значение потери будет прогрессивно расти с уменьшением вероятности правильного класса.

Эта функция возвращает среднее значение для целого пакета.

# Вычисляем потерю.
loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, 
                                              logits=logits)

Evaluate mode (Оценка)

Когда вызывается evaluate метод Estimator'а, model_fn получает mode = ModeKeys.EVAL. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий потерю модели и опционально одну или несколько метрик.

Хотя возвращение метрик опционально, большинство кастомных Estimators возвращают по крайней мере одну метрику. TensorFlow предоставляет Metrics модуль tf.metrics для вычисления общих метрик. В целях краткости, мы будем возвращать только аккуратность. tf.metrics.accuracy функция сравнивает наши прогнозы с настоящими значениями, то есть, с метками, предоставленными вводной функцией. tf.metrics.accuracy функция требует, чтобы метки и прогнозы имели одинаковую форму. Вот вызов tf.metrics.accuracy:

# Вычисляем оценку метрик.
accuracy = tf.metrics.accuracy(labels=labels,
                               predictions=predicted_classes,
                               name='acc_op')

tf.estimator.EstimatorSpec возвращенный для оценки обычно содержит следующую информацию:

  • loss - потеря модели
  • eval_metric_ops - опциональный словарь метрик

Таким образом, мы будем создавать словарь, содержащий единственную метрику. Если бы мы рассчитали другие метрики, мы бы добавили их как дополнительные ключ/значение пары к этому же словарю. Затем, мы передадим этот словарь в eval_metric_ops аргумент tf.estimator.EstimatorSpec.

metrics = {'accuracy': accuracy}
tf.summary.scalar('accuracy', accuracy[1])

if mode == tf.estimator.ModeKeys.EVAL:
    return tf.estimator.EstimatorSpec(
        mode, loss=loss, eval_metric_ops=metrics)

tf.summary.scalar сделает аккуратность доступной для TensorBoard в обоих TRAIN и EVAL режимах(modes).

Train mode (Тренировка)

Когда вызывается train метод Estimator'а, model_fn получает mode = ModeKeys.TRAIN. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий потерю и тренировочную операцию.

Построение тренировочной операции потребует оптимизатор. Мы будем использовать tf.train.AdagradOptimizer, потому что мы имитируем DNNClassifier, который также использует Adagrad по умолчанию. tf.train пакет также предоставляет много других оптимизаторов.

Вот код, который создает оптимизатор:

optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)

Далее мы построим тренировочную операцию, используя метод tf.train.Optimizer.minimize оптимизатора на потере, которую мы рассчитали ранее.

minimize метод принимает global_step параметр. TensorFlow использует этот параметр для счета количества произведенных тренировочных шагов (для того чтобы знать когда тренировка должна остановиться). Более того global_step необходим для корректной работы TensorBoard графов. Просто вызываем tf.train.get_global_step и передаем результат как global_step аргумент minimize метода.

Вот код тренировки модели:

train_op = optimizer.minimize(loss, 
                              global_step=tf.train.get_global_step())

tf.estimator.EstimatorSpec возвращаемый для тренировки должен иметь следующий набор полей:

  • loss, содержащее значение потери модели.
  • train_op, которое исполняет тренировочный шаг.

Вот код для вызова EstimatorSpec:

return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)

Кастомный Estimator

Создадим инстанс кастомного Estimator'а через базовый Estimator класс:

# Строим 2 скрытых слоя с 10 узлами в каждом.
classifier = tf.estimator.Estimator(
    model_fn=my_model_fn,
    params={
       'feature_columns': my_feature_columns,
       # 2 скрытых слоя с 10 узлами в каждом
       'hidden_units': [10, 10],
       # Модель должна выбирать между 3 классами.
       'n_classes': 3,
    })

Здесь params словарь служит той же цели, что и key-word аргументы DNNClassifier; то есть, params словарь позволяет конфигурировать Estimator без изменения кода в model_fn.

Остальной код для тренировки, оценки и выполнения прогнозов такой же как и в предсозданных Estimators. Например, следующий код будет тренировать модель:

# Тренируем модель
classifier.train(
    input_fn=lambda:iris_data.train_input_fn(train_x, 
                                             train_y, 
                                             args.batch_size),
    steps=args.train_steps)


TensorBoard

Вы можете просматривать результаты тренировки для кастомного Estimator'а в TensorBoard. Для того чтобы увидеть результаты, запустите TensorBoard из командной строки следующим образом:

# Замените PATH настоящим путем, переданным как model_dir
tensorboard --logdir=PATH

Затем откройте TensorBoard в браузере по адресу http://localhost:6006

Все предсозданные Estimators автоматически логируют множество информации в TensorBoard. С кастомными Estimators, однако, TensorBoard предоставляет только один лог по умолчанию (граф потери) плюс информацию, которую вы явно передадите на логирование в TensorBoard. Для кастомного Estimator, который мы только что создали, TensorBoard генерирует следующее:

TensorBoard показывает три графа.

Вкратце, вот что эти три графа говорят вам:

  • global_step/sec: Индикатор производительности, показывающий как много пакетов (градиентных апдейтов) мы обработали за секунду в ходе тренировки модели.
  • loss: отчет о потере.
  • accuracy: Аккуратность записанная следующими двумя строками:
    • eval_metric_ops={'my_accuracy': accuracy}, в ходе оценки.
    • tf.summary.scalar('accuracy', accuracy[1]), в ходе тренировки.

Эти TensorBoard графы - одна из главных причин, по которой важно передавать аргумент global_step в метод minimize оптимизатора. Модель не может записывать x-координаты для этих графов без этого аргумента.

Отметим следующее в my_accuracy и loss графах:

  • Оранжевая линия представляет тренировку.
  • Синяя точка представляет оценку.

В ходе тренировки заключения (оранжевая линия) записаны периодично с ходом обработки пакетов, которые поэтому становятся графом растянутым по x-оси.

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

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


Резюме

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

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