воскресенье, 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 более детально.