четверг, 13 декабря 2018 г.

TensorFlow Core: графы и сессии

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

Этот пост будет вам наиболее полезен, если вы намерены использовать модель низкоуровневого программирования напрямую. Высокоуровневые API, такие как tf.estimator.Estimator и Keras, скрывают детали графов и сессий от конечного пользователя, но этот пост может быть также полезен, если вы хотите понять как эти API реализованы.

Почему графы потоков данных?

Поток данных (Dataflow) - это общая модель программирования для параллельного вычисления. В графе потока данных узлы представляют элементы вычисления. Например, в TensorFlow графе, tf.matmul операция будет соотвествовать единственному узлу с двумя входящими краями (матрицами, которыми будет манипулирование) и одному выходящему узлу (результат умножения).

Поток данных (Dataflow) имеет несколько преимуществ, которые TensorFlow использует при исполнении вашей программы:

  • Параллелизм. Благодаря использованию явных краев для представления зависимостей между операциями, для системы облегчается задача определения операций, которые могут исполняться параллельно.
  • Распределенное исполнение. Благодаря использованию явных краев для представления значений, которые передаются между операциями, для TensorFlow становится возможным разделить на части вашу программу между несколькими устройствами (CPU, GPU, и TPU) присоединенными к разным машинам. TensorFlow вводит необходимое общение и координацию между устройствами.
  • Компиляция. TensorFlow XLA компилятор может использовать информацию в вашем графе потока данных для генерирования более быстрого кода, например, сплавливая воедино подходящие операции.
  • Возможность портирования. Граф потока данных - ээто независимое от языка представление кода в вашей модели. Вы можете построить граф потока данных в Python, сохранив его в SavedModel, и восстановить его в C++ программе для исполнения с низким уровнем задержки.

Что такое tf.Graph?

tf.Graph содержит два уникальных типа информации:

  • Структура графа. Узлы и края графа, отображающие как отдельные операции составляются вместе, но нет предписывающие как они должны быть использованы. Структура графа это как код сборки (код билда)(assembly code): его просмотр может открыть некоторую полезную информацию, но он не содержит всего необходимого контекста, который имеет код источника (source code).
  • Коллекции графа. TensorFlow предоставляет общий механизм для сохранения коллекций метаданных в tf.Graph. tf.add_to_collection функция позволяет вам связывать список объектов с ключом (где tf.GraphKeys определяет некоторые стандартные ключи), а tf.get_collection позволяет вам просматривать все коллекции, связанные с ключом. Многие части TensorFlow библиотеки используют эту возможность: например, когда вы создаете tf.Variable, она по умолчанию добавляется в коллекции представляющие "глобальные переменные" и "тренируемые переменные". Когда вы позже приходите для того чтобы создать tf.train.Saver или tf.train.Optimizer, переменные в этих коллекциях используются как аргументы по умолчанию.

Построение tf.Graph

Большинство TensorFlow программ стартуют с фазы создания графа потока данных. На этой фазе вы вызываете TensorFlow API функции, которые создают новые tf.Operation (узел) и tf.Tensor (край) объекты и добавляют их в экземпляр tf.Graph. TensorFlow предоставляет граф по умолчанию, который является неявным аргументом ко всем функциям API в том же контексте. Например:

  • Вызов tf.constant(42.0) создает единичную tf.Operation, которая производит значение 42.0, добавляет его в граф по умолчанию, и возвращает tf.Tensor, который представляет значение константы.
  • Вызов tf.matmul(x, y) создает единичную tf.Operation, которая умножает значения tf.Tensor объектов x и y, добавляет это в граф по умолчанию, и возвращает tf.Tensor, который представляет результат умножения.
  • Исполнение v = tf.Variable(0) добавляет в граф tf.Operation, который будет хранить записываемое значение тензора, которое сохраняется между вызовами tf.Session.run. tf.Variable объект оборачивает эту операцию, и может быть использован как тензор, который будет читать текущее значение хранимого значения. tf.Variable объект также имеет методы, такие как tf.Variable.assign и tf.Variable.assign_add, которые создают tf.Operation объекты, которые, при исполнении, обновляют хранимое значение.
  • Вызов tf.train.Optimizer.minimize будет добавлять операции и тензоры в граф по умолчанию, который вычисляет градиенты, и возвращает tf.Operation, которая, при исполнении, применяет эти градиенты к набору переменных.

Большинство программ полагаются исключительно на граф по умолчанию. Высокоуровневые API, такие как tf.estimator.Estimator API, управляют графом по умолчанию от вашего имени, и, например, могут создать различные графы для тренировки и оценки.

Следует отметить, что вызов большиства функций в TensorFlow API просто добавляет операции и тензоры в граф по умолчанию, но не выполняют на самом деле вычисление. Вместо этого, вы составляете эти функции до тех пор, пока у вас есть tf.Tensor или tf.Operation, который представляет все вычисление, такое как выполнение одного шага градиентного спуска, и затем передаете этот объект в tf.Session для выполнения вычисления.

Операции с именами

tf.Graph объект задает область имен (namespace) для tf.Operation объектов, которые он содержит. TensorFlow автоматически выбирает уникальное имя для каждой операции в вашем графе, но задание операциям описательных имен может сделать вашу программу проще для чтения и отладки. TensorFlow API предоставляет два способа переопределения имени операции:

  • Каждая API функция, которая создает новую tf.Operation или возвращает новый tf.Tensor принимает опциональный name аргумент. Например, tf.constant(42.0, name="answer") создает новую tf.Operation названную "answer" и возвращает tf.Tensor названный "answer:0". Если граф по умолчанию уже содержит операцию названную "answer", тогда TensorFlow добавит "_1", "_2", и так далее к имени, для того чтобы сделать имя уникальным.
  • tf.name_scope функция делает возможным добавлять префикс области видимости имен ко всем операциям, созданным в отдельном контексте. Текущий префикс области видимости имен - это "/"-разделенный список имен всех активных tf.name_scope контекстных менеджеров. Если область видимости имен уже была использована в текущем контексте, TensorFlow добавит "_1", "_2", и т.д. Например:

c_0 = tf.constant(0, name="c")  
# => операция названа "c"

# Уже существующие имена будут "уникализованы".
c_1 = tf.constant(2, name="c")  
# => операция названа "c_1"

# Области видимости имен добавляют префикс ко всем операциям, 
# созданным в том же контексте.
with tf.name_scope("outer"):
  c_2 = tf.constant(2, name="c")  
  # => операция названа "outer/c"

  # Области видимости имен складываются друг в друга
  # как пути в иерархической файловой системе.
  with tf.name_scope("inner"):
    c_3 = tf.constant(3, name="c")  
    # => операция названа "outer/inner/c"

  # Выход из конекста области видимости имен
  # будет возвращать предыдущий префикс.
  c_4 = tf.constant(4, name="c")  
  # => операция названа "outer/c_1"

  # Уже существующие области видимости имен будут "уникализованы".
  with tf.name_scope("inner"):
    c_5 = tf.constant(5, name="c")  
    # => операция названа "outer/inner_1/c"

Визуализатор графов использует области видимости имен для группирования операций и уменьшения визуальной сложности графа.

Отметим, что tf.Tensor объекты неявно названы согласно tf.Operation, которая производит этот тензор как вывод. Имя тензора имеет форму "<OP_NAME>:<i>", где:

  • "<OP_NAME>" - это имя операции, которая производит его.
  • "<i>" - это число, представляющее индекс этого тензора среди выводов операции.

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

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

Спецификация устройства имеет следующую форму:

/job:<JOB_NAME>/task:<TASK_INDEX>/device:<DEVICE_TYPE>:<DEVICE_INDEX>

где:

  • <JOB_NAME> - это алфавитно-числовая строка, которая не начинается с цифры.
  • <DEVICE_TYPE> - это зарегистрированный тип устройства (такой как GPU или CPU).
  • <TASK_INDEX> - неотрицательное число, представляющее индекс задачи в работе (job) с именем <JOB_NAME>.
  • <DEVICE_INDEX> - это неотрицательное число, представляющее индекс устройства, например, для разделения между разными GPU устройствами, используемыми в одном процессе.

Вам не требуется определять каждую часть спецификации устройства. Например, если вы исполняете программу в конфигурации с одной машиной и одним GPU, вы можете использовать tf.device, для того чтобы прикрепить некоторые операции к CPU и GPU:

# Операции созданные вне какого-либо контекста
# будут выполняться на "наиболее возможном" устройстве.
# Например, если у вас есть доступные GPU и CPU,
# а операция имеет GPU реализацию, тогда TensorFlow выберет GPU
weights = tf.random_normal(...)

with tf.device("/device:CPU:0"):
  # Операции, созданные в этом контексте,
  # будут прикрепленны к CPU.
  img = tf.decode_jpeg(tf.read_file("img.jpg"))

with tf.device("/device:GPU:0"):
  # Операции, созданные в этом контексте,
  # будут прикрепленны к GPU.
  result = tf.matmul(weights, img)

Если вы запускаете TensorFlow в типичной распределенной конфигурации, вы можете определить имя работы (job) и ID задачи (task), для того чтобы разместить переменные на задаче в параметре server работы (job) ("/job:ps"), а другие операции на задаче в работе исполнителя (worker job) ("/job:worker"):

with tf.device("/job:ps/task:0"):
  weights_1 = tf.Variable(tf.truncated_normal([784, 100]))
  biases_1 = tf.Variable(tf.zeroes([100]))

with tf.device("/job:ps/task:1"):
  weights_2 = tf.Variable(tf.truncated_normal([100, 10]))
  biases_2 = tf.Variable(tf.zeroes([10]))

with tf.device("/job:worker"):
  layer_1 = tf.matmul(train_batch, weights_1) + biases_1
  layer_2 = tf.matmul(train_batch, weights_2) + biases_2

tf.device дает вам большую долю гибкости для выбора размещения для отдельных операций или широких регионов TensorFlow графа. Во многих случаях, существуют простые эвристики, которые работают хорошо. Например, tf.train.replica_device_setter API может быть использован с tf.device для размещения операций для распределенной тренировки с параллельными данными. Например, следующий фрагмент кода показывает как tf.train.replica_device_setter применяет различные политики размещения tf.Variable объектов и других операций:

with tf.device(tf.train.replica_device_setter(ps_tasks=3)):
  # tf.Variable объекты, по умолчанию, размещены на задачах
  # "/job:ps" в круговой манере
  w_0 = tf.Variable(...)  # размещено на "/job:ps/task:0"
  b_0 = tf.Variable(...)  # размещено на "/job:ps/task:1"
  w_1 = tf.Variable(...)  # размещено на "/job:ps/task:2"
  b_1 = tf.Variable(...)  # размещено на "/job:ps/task:0"

  input_data = tf.placeholder(tf.float32)     
  # размещено на "/job:worker"

  layer_0 = tf.matmul(input_data, w_0) + b_0  
  # размещено на "/job:worker"

  layer_1 = tf.matmul(layer_0, w_1) + b_1     
  # размещено на "/job:worker"


Тензор-подобные объекты

Многие TensorFlow операции принимают один или более tf.Tensor объектов как аргументы. Например, tf.matmul принимает два tf.Tensor объекта, а tf.add_n принимает список n tf.Tensor объектов. Для удобства эти функции будут принимать тензор-подобный объект вместо tf.Tensor, и неявням образом преобразовывать его в tf.Tensor, используя tf.convert_to_tensor метод. Тензор-подобные объекты включают элементы следующих типов:

  • tf.Tensor
  • tf.Variable
  • numpy.ndarray
  • list (лист, список) (и списки тензор-подобных объектов)
  • Скалярные Python типы: bool, float, int, str

Вы можете регистрировать дополнительные тензор-подобные типы, используя tf.register_tensor_conversion_function.

Следует отметить, что по умолчанию TensorFlow будет создавать новый tf.Tensor каждый раз, когда вы используете тот же тензор-подобный объект. Если тензор-подобный объект большой (например, numpy.ndarray, содержащий набор тренировочных примеров) и вы используете его много раз, это может вызвать переполненние памяти. Чтобы избежать этого, вручную вызовите tf.convert_to_tensor на тензор-подобном объекте единожды и используйте возвращенный tf.Tensor вместо первоначального объкета.

Исполнение графа в tf.Session

TensorFlow использует tf.Session класс для представления соединения между клиентской программой - обычно Python программой, хотя схожий интерфейс доступен в других языках - и C++ рабочей средой исполнения. tf.Session объект предоставляет доступ к утройствам на локальной машине и удаленным устройствам, используя распределенную TensorFlow среду исполнения. Он также кеширует информацию о ваших tf.Graph, таким образом вы можете эффективно выполнять одни и те же вычисления много раз.

Создание tf.Session

Если вы используете низкоуровневое TensorFlow API, вы можете создать tf.Session для текущего графа по умолчанию следующим образом:

# Создаем внутрипроцессовую сессию по умолчанию
with tf.Session() as sess:
  # ...

# Cоздаем удаленную сессию
with tf.Session("grpc://example.org:2222"):
  # ...

Ввиду того, что tf.Session имеет физические ресурсы (такие как GPU и сетевые соединения), также обычно она используется как менеджер контекста (внутри with блока), который автоматически закрывает сессию, когда вы выходите из блока. Также возможно создать сессию без использования with блока, но вам следует явно вызывать tf.Session.close, когда вы заканчиваете использовать ее, для освобождения ресурсов.

Отметим: высокоуровневые API, такие как tf.train.MonitoredTrainingSession или tf.estimator.Estimator, будут создавать и управлять tf.Session для вас. Эти API принимают опциональные target и config аргументы (напрямую или как часть tf.estimator.RunConfig объекта), с тем же значением как описано выше.

tf.Session.init принимает три опциональных аргумента:

  • target. Если аргумент оставлен пустым (по умолчанию), сессия будет использовать устройства только на локальной машине. Однако, вы можете также определить grpc:// URL для определения адреса TensorFlow сервера, который дает сессии доступ ко всем устройствам на машинах, которые контролирует этот сервер. Например, в общей межграфовой конфигурации репликации, tf.Session подключается к tf.train.Server в том же процессе, что и клиент.
  • graph. По умолчанию, новая tf.Session будет привязана к текущему графу по умолчанию и способна выполнять операции только в нем. Если вы используете несколько графов в вашей программе, вы можете явно определить tf.Graph при создании сессии.
  • config. Этот аргумент позволяет вам определять tf.ConfigProto, который контролирует поведение сессии. Например, вот некоторые конфигурационные опции:
    • allow_soft_placement. Установите в True для включения "soft" алгоритма размещения устройства, которая игнорирует tf.device аннотации, которые пытаются размещать операции с GPU устройства предназначенные для CPU, и размещает их на CPU.
    • cluster_def. При использовании распределенного TensorFlow, эта опция позволяет вам определить какие машины использовать в вычислении, и предоставлять картирование (mapping) между имена работ (job names), индексами задач, и сетевыми адресами.
    • graph_options.optimizer_options. Предоставляет контроль над оптимизациями, которые TensorFlow выполняет в вашем графе перед его исполнением.
    • gpu_options.allow_growth. Установите в True, чтобы изменить аллокатор (выделитель) GPU памяти таким образом, чтобы он постепенно увеличивал размер занимаемой памяти, вместо занятия большей части памяти при сразу при запуске.

Использование tf.Session.run для выполнения операций

tf.Session.run метод является основным механизмом запуска tf.Operation или оценки tf.Tensor. Вы можете передать один или несколько tf.Operation или tf.Tensor объектов tf.Session.run, и TensorFlow выполнит операции, необходимые для вычисления результата.

tf.Session.run требует, чтобы вы указали список выборок, которые определяют возвращаемые значения и могут быть tf.Operation, tf.Tensor или тензор-подобного типа, такие как tf.Variable. Эти выборки определяют, какой подграф из всего tf.Graph должен быть исполнен для получения результата: это подграф, который содержит все операции, указанные в списке выборки плюс все операции, выходные данные которых используются для вычисления значения выборок. Например, следующий фрагмент кода показывает, как различные аргументы tf.Session.run приводят к выполнению различных подграфов:

x = tf.constant([[37.0, -23.0], [1.0, 4.0]])
w = tf.Variable(tf.random_uniform([2, 2]))
y = tf.matmul(x, w)
output = tf.nn.softmax(y)
init_op = w.initializer

with tf.Session() as sess:
  # Выполняем инициализатор на w.
  sess.run(init_op)

  # Оцениваем output. `sess.run(output)`вернет NumPy массив,
  # содержащий результат вычисления.
  print(sess.run(output))

  # Оцениваем y и output. Отметим, что y будет вычислено только единожды,
  # и его результат использован в обоих случаях:
  # чтобы вернуть y_val и как ввод для tf.nn.softmax() операции.
  # Оба y_val и output_val будут NumPy массивами.
  y_val, output_val = sess.run([y, output])

tf.Session.run также опционально принимает словарь вводов, который является картой (mapping) tf.Tensor объектов (обычно tf.placeholder тензоры) к значениям (как правило, Python скаляры, списки (lists), или NumPy массивы), которые будут заменены для этих тензоров в ходе исполнения. Например:

# Определяем placeholder. который ожидает вектор из трех
# нецельночисловых значений, и вычисление, которое зависит от него
x = tf.placeholder(tf.float32, shape=[3])
y = tf.square(x)

with tf.Session() as sess:
  # Передача значения изменяет результат
  # котороый возвращается, когда вы оцениваете y.
  print(sess.run(y, {x: [1.0, 2.0, 3.0]}))  # => "[1.0, 4.0, 9.0]"
  print(sess.run(y, {x: [0.0, 0.0, 5.0]}))  # => "[0.0, 0.0, 25.0]"

  # Вызывает tf.errors.InvalidArgumentError, 
  # потому что вы должны передавать значение для tf.placeholder()
  # при оценке тензора, который зависит от него.
  sess.run(y)

  # Вызывает ValueError, потому что форма 37.0
  # не соотвествует форме заполнителя x.
  sess.run(y, {x: 37.0})

tf.Session.run также принимает необязательный аргумент options, который позволяет вам указать параметры вызова и необязательный аргумент run_metadata, который позволяет собирать метаданные о выполнении. Например, вы можете использовать эти опции вместе, чтобы собрать информацию трассировки о выполнении:

y = tf.matmul([[37.0, -23.0], [1.0, 4.0]], tf.random_uniform([2, 2]))

with tf.Session() as sess:
  # Определяем опции для sess.run() вызова.
  options = tf.RunOptions()
  options.output_partition_graphs = True
  options.trace_level = tf.RunOptions.FULL_TRACE

  # Определяем контейнер для возвращенных метаданных.
  metadata = tf.RunMetadata()

  sess.run(y, options=options, run_metadata=metadata)

  # Печатаем подграфы, которые выполнены на каждом устройстве.
  print(metadata.partition_graphs)

  # Печатаем время каждой выполненной операции.
  print(metadata.step_stats)


Визуализация вашего графа

TensorFlow включает в себя инструменты, которые могут помочь вам понять код в графе. Визуализатор графа - это компонент TensorBoard, который отображает структуру вашего графа визуально в браузере. Самый простой способ создать визуализацию - передать tf.Graph при создании tf.summary.FileWriter:

# Строим граф.
x = tf.constant([[37.0, -23.0], [1.0, 4.0]])
w = tf.Variable(tf.random_uniform([2, 2]))
y = tf.matmul(x, w)
# ...
loss = ...
train_op = tf.train.AdagradOptimizer(0.01).minimize(loss)

with tf.Session() as sess:
  # sess.graph предоставляет доступ к графу, 
  # использованному в tf.Session.
  writer = tf.summary.FileWriter("/tmp/log/...", sess.graph)

  # Выполняем вычисление...
  for i in range(1000):
    sess.run(train_op)
    # ...

  writer.close()

Примечание: Если вы используете tf.estimator.Estimator, граф (и любые другие итоги) будут автоматически записаны в model_dir, который вы указали при создании оценщика (estimator).

Затем вы можете открыть журнал в tensorboard, перейти в "Graph" вкладку и увидеть высокоуровневую визуализацию структуры вашего графа. Обратите внимание, что типичный TensorFlow граф - особенно тренировочные графы с автоматически вычисляемыми градиентами - имеет слишком много узлов для одновременной визуализации. Визуализатор графов использует области имен для группировки связанных операций в «super» узлы. Вы можете нажать на оранжевый "+" кнопку на любом из этих супер узлов, чтобы развернуть подграф, находящийся внутри.


Программирование с несколькими графами

Примечание: При обучении модели, обычный способ организации вашего кода заключается в использовании одного графа для обучения вашей модели, и отдельного графа для оценки или выдачи прогноза с обученной моделью. Во многих случаях граф вывода будет отличаться от тренировочного графа: например, техники, такие как отсев (dropout) и пакетная нормализация используют разные операции в каждом случае. Кроме того, стандартные утилиты, такие как tf.train.Saver, используют имена tf.Variable объектов (имена которых основаны на подлежащей tf.Operation) для идентификации каждой переменной в сохраненной контрольной точке. При программировании таким образом, вы можете либо использовать отдельные Python процессы для построения и выполнения графов, либо вы можете использовать несколько графов в одном процессе. В этом посте описан вариант использования нескольких графов в одном процессе.

Как отмечалось выше, TensorFlow предоставляет «граф по умолчанию», который неявно передается для всех API функций в том же контексте. Для многих приложений одного графа достаточно. Тем не менее, TensorFlow также предоставляет методы для манипулирования графом по умолчанию, что может быть полезно в более сложных случаях использования. Например:

  • tf.Graph определяет пространство имен для tf.Operation объектов: каждая операция в одном графе должна иметь уникальное имя. TensorFlow будет "уникализовывать" имена операций путем добавления "_1", "_2" и т. д. к их именам, если запрошенное имя уже занято. Использование нескольких явно созданных графов дает вам больше контроля над тем, какое имя дается каждой операции.

  • В графе по умолчанию хранится информация о каждом tf.Operation и tf.Tensor, который когда-либо был добавлен в него. Если ваша программа создает большое количество неподключенных подграфов, более эффективным может быть использование другого tf.Graph для создания каждого подграфа, чтобы несвязанное состояние могло быть собрано сборщиком мусора.

Вы можете установить другой tf.Graph в качестве графа по умолчанию, используя tf.Graph.as_default менеджер контекста:

g_1 = tf.Graph()
with g_1.as_default():
  # Операции, созданные в этой области будут добавлены к g_1.
  c = tf.constant("Node in g_1")

  # Операции, созданные в этой области будут выполнять операции из g_1.
  sess_1 = tf.Session()

g_2 = tf.Graph()
with g_2.as_default():
  # Операции, созданные в этой области будут добавлены к g_2.
  d = tf.constant("Node in g_2")

# Альтернативно, вы можете передать граф при создании tf.Session:
# sess_2 будет выполнять ьоперации из g_2.
sess_2 = tf.Session(graph=g_2)

assert c.graph is g_1
assert sess_1.graph is g_1

assert d.graph is g_2
assert sess_2.graph is g_2

Для того чтобы инспектировать текущий граф по умолчанию, вызовите tf.get_default_graph, который вернет tf.Graph объект:

# Речатаем все операции в графе по умолчанию.
g = tf.get_default_graph()
print(g.get_operations())


Читайте также другие статьи по этой теме в нашем блоге:

Основы TensorFlow Core

TensorFlow Core: тензоры (tensors)

TensorFlow Core: переменные (variables)