Посмотреть на TensorFlow.org | Запустить в Google Colab | Посмотреть исходный код на GitHub | Скачать блокнот |
Обзор
Это руководство раскрывает суть TensorFlow и Keras, чтобы продемонстрировать, как работает TensorFlow. Если вместо этого вы хотите сразу же приступить к работе с Keras, ознакомьтесь с коллекцией руководств по Keras .
В этом руководстве вы узнаете, как TensorFlow позволяет вносить простые изменения в код для получения графиков, как хранятся и представляются графики, а также как вы можете использовать их для ускорения своих моделей.
Это общий обзор, показывающий, как tf.function
позволяет переключаться с нетерпеливого выполнения на графовое выполнение. Более полную спецификацию tf.function
смотрите в руководстве по tf.function
.
Что такое графики?
В предыдущих трех руководствах вы жадно запускали TensorFlow. Это означает, что операции TensorFlow выполняются Python, операция за операцией, и возвращают результаты обратно в Python.
В то время как нетерпеливое выполнение имеет несколько уникальных преимуществ, выполнение графа обеспечивает переносимость за пределы Python и, как правило, обеспечивает более высокую производительность. Выполнение графа означает, что тензорные вычисления выполняются как граф TensorFlow, иногда называемый tf.Graph
или просто «графом».
Графы — это структуры данных, содержащие набор объектов tf.Operation
, представляющих единицы вычислений; и объекты tf.Tensor
, которые представляют единицы данных, которые передаются между операциями. Они определены в контексте tf.Graph
. Поскольку эти графики являются структурами данных, их можно сохранять, запускать и восстанавливать без исходного кода Python.
Вот как выглядит граф TensorFlow, представляющий двухслойную нейронную сеть, при визуализации в TensorBoard.
Преимущества графиков
С графиком у вас есть большая гибкость. Вы можете использовать граф TensorFlow в средах, где нет интерпретатора Python, например в мобильных приложениях, встроенных устройствах и внутренних серверах. TensorFlow использует графики в качестве формата для сохраненных моделей при их экспорте из Python.
Графики также легко оптимизируются, что позволяет компилятору выполнять такие преобразования, как:
- Статически выводите значение тензоров, складывая константные узлы в своих вычислениях («константное складывание») .
- Отделяйте независимые части вычислений и разделяйте их между потоками или устройствами.
- Упростите арифметические операции, исключив общие подвыражения.
Для выполнения этого и других ускорений существует целая система оптимизации Grappler .
Короче говоря, графики чрезвычайно полезны и позволяют вашему TensorFlow работать быстро , параллельно и эффективно на нескольких устройствах .
Однако вы по-прежнему хотите определять свои модели машинного обучения (или другие вычисления) в Python для удобства, а затем автоматически строить графики, когда они вам нужны.
Настраивать
import tensorflow as tf
import timeit
from datetime import datetime
Использование графиков
Вы создаете и запускаете граф в TensorFlow, используя tf.function
либо как прямой вызов, либо как декоратор. tf.function
принимает на вход обычную функцию и возвращает Function
. Function
— это вызываемый объект Python, который строит графики TensorFlow из функции Python. Вы используете Function
так же, как и ее эквивалент в Python.
# Define a Python function.
def a_regular_function(x, y, b):
x = tf.matmul(x, y)
x = x + b
return x
# `a_function_that_uses_a_graph` is a TensorFlow `Function`.
a_function_that_uses_a_graph = tf.function(a_regular_function)
# Make some tensors.
x1 = tf.constant([[1.0, 2.0]])
y1 = tf.constant([[2.0], [3.0]])
b1 = tf.constant(4.0)
orig_value = a_regular_function(x1, y1, b1).numpy()
# Call a `Function` like a Python function.
tf_function_value = a_function_that_uses_a_graph(x1, y1, b1).numpy()
assert(orig_value == tf_function_value)
Внешне Function
выглядит как обычная функция, которую вы пишете с помощью операций TensorFlow. Однако внизу все совсем по-другому . Function
инкапсулирует несколько tf.Graph
за одним API . Вот как Function
может дать вам преимущества выполнения графа , такие как скорость и возможность развертывания.
tf.function
применяется к функции и всем другим функциям, которые она вызывает :
def inner_function(x, y, b):
x = tf.matmul(x, y)
x = x + b
return x
# Use the decorator to make `outer_function` a `Function`.
@tf.function
def outer_function(x):
y = tf.constant([[2.0], [3.0]])
b = tf.constant(4.0)
return inner_function(x, y, b)
# Note that the callable will create a graph that
# includes `inner_function` as well as `outer_function`.
outer_function(tf.constant([[1.0, 2.0]])).numpy()
array([[12.]], dtype=float32)
Если вы использовали TensorFlow 1.x, вы заметите, что вам никогда не нужно было определять Placeholder
или tf.Session
.
Преобразование функций Python в графики
Любая функция, которую вы пишете с помощью TensorFlow, будет содержать смесь встроенных операций TF и логики Python, таких как предложения if-then
, циклы, break
, return
, continue
и многое другое. В то время как операции TensorFlow легко фиксируются с помощью tf.Graph
, специфичная для Python логика должна пройти дополнительный шаг, чтобы стать частью графа. tf.function
использует библиотеку AutoGraph ( tf.autograph
) для преобразования кода Python в код, генерирующий графы.
def simple_relu(x):
if tf.greater(x, 0):
return x
else:
return 0
# `tf_simple_relu` is a TensorFlow `Function` that wraps `simple_relu`.
tf_simple_relu = tf.function(simple_relu)
print("First branch, with graph:", tf_simple_relu(tf.constant(1)).numpy())
print("Second branch, with graph:", tf_simple_relu(tf.constant(-1)).numpy())
First branch, with graph: 1 Second branch, with graph: 0
Хотя маловероятно, что вам потребуется напрямую просматривать графики, вы можете просмотреть выходные данные, чтобы проверить точные результаты. Их нелегко читать, поэтому не нужно слишком внимательно смотреть!
# This is the graph-generating output of AutoGraph.
print(tf.autograph.to_code(simple_relu))
def tf__simple_relu(x): with ag__.FunctionScope('simple_relu', 'fscope', ag__.ConversionOptions(recursive=True, user_requested=True, optional_features=(), internal_convert_user_code=True)) as fscope: do_return = False retval_ = ag__.UndefinedReturnValue() def get_state(): return (do_return, retval_) def set_state(vars_): nonlocal retval_, do_return (do_return, retval_) = vars_ def if_body(): nonlocal retval_, do_return try: do_return = True retval_ = ag__.ld(x) except: do_return = False raise def else_body(): nonlocal retval_, do_return try: do_return = True retval_ = 0 except: do_return = False raise ag__.if_stmt(ag__.converted_call(ag__.ld(tf).greater, (ag__.ld(x), 0), None, fscope), if_body, else_body, get_state, set_state, ('do_return', 'retval_'), 2) return fscope.ret(retval_, do_return)
# This is the graph itself.
print(tf_simple_relu.get_concrete_function(tf.constant(1)).graph.as_graph_def())
node { name: "x" op: "Placeholder" attr { key: "_user_specified_name" value { s: "x" } } attr { key: "dtype" value { type: DT_INT32 } } attr { key: "shape" value { shape { } } } } node { name: "Greater/y" op: "Const" attr { key: "dtype" value { type: DT_INT32 } } attr { key: "value" value { tensor { dtype: DT_INT32 tensor_shape { } int_val: 0 } } } } node { name: "Greater" op: "Greater" input: "x" input: "Greater/y" attr { key: "T" value { type: DT_INT32 } } } node { name: "cond" op: "StatelessIf" input: "Greater" input: "x" attr { key: "Tcond" value { type: DT_BOOL } } attr { key: "Tin" value { list { type: DT_INT32 } } } attr { key: "Tout" value { list { type: DT_BOOL type: DT_INT32 } } } attr { key: "_lower_using_switch_merge" value { b: true } } attr { key: "_read_only_resource_inputs" value { list { } } } attr { key: "else_branch" value { func { name: "cond_false_34" } } } attr { key: "output_shapes" value { list { shape { } shape { } } } } attr { key: "then_branch" value { func { name: "cond_true_33" } } } } node { name: "cond/Identity" op: "Identity" input: "cond" attr { key: "T" value { type: DT_BOOL } } } node { name: "cond/Identity_1" op: "Identity" input: "cond:1" attr { key: "T" value { type: DT_INT32 } } } node { name: "Identity" op: "Identity" input: "cond/Identity_1" attr { key: "T" value { type: DT_INT32 } } } library { function { signature { name: "cond_false_34" input_arg { name: "cond_placeholder" type: DT_INT32 } output_arg { name: "cond_identity" type: DT_BOOL } output_arg { name: "cond_identity_1" type: DT_INT32 } } node_def { name: "cond/Const" op: "Const" attr { key: "dtype" value { type: DT_BOOL } } attr { key: "value" value { tensor { dtype: DT_BOOL tensor_shape { } bool_val: true } } } } node_def { name: "cond/Const_1" op: "Const" attr { key: "dtype" value { type: DT_BOOL } } attr { key: "value" value { tensor { dtype: DT_BOOL tensor_shape { } bool_val: true } } } } node_def { name: "cond/Const_2" op: "Const" attr { key: "dtype" value { type: DT_INT32 } } attr { key: "value" value { tensor { dtype: DT_INT32 tensor_shape { } int_val: 0 } } } } node_def { name: "cond/Const_3" op: "Const" attr { key: "dtype" value { type: DT_BOOL } } attr { key: "value" value { tensor { dtype: DT_BOOL tensor_shape { } bool_val: true } } } } node_def { name: "cond/Identity" op: "Identity" input: "cond/Const_3:output:0" attr { key: "T" value { type: DT_BOOL } } } node_def { name: "cond/Const_4" op: "Const" attr { key: "dtype" value { type: DT_INT32 } } attr { key: "value" value { tensor { dtype: DT_INT32 tensor_shape { } int_val: 0 } } } } node_def { name: "cond/Identity_1" op: "Identity" input: "cond/Const_4:output:0" attr { key: "T" value { type: DT_INT32 } } } ret { key: "cond_identity" value: "cond/Identity:output:0" } ret { key: "cond_identity_1" value: "cond/Identity_1:output:0" } attr { key: "_construction_context" value { s: "kEagerRuntime" } } arg_attr { key: 0 value { attr { key: "_output_shapes" value { list { shape { } } } } } } } function { signature { name: "cond_true_33" input_arg { name: "cond_identity_1_x" type: DT_INT32 } output_arg { name: "cond_identity" type: DT_BOOL } output_arg { name: "cond_identity_1" type: DT_INT32 } } node_def { name: "cond/Const" op: "Const" attr { key: "dtype" value { type: DT_BOOL } } attr { key: "value" value { tensor { dtype: DT_BOOL tensor_shape { } bool_val: true } } } } node_def { name: "cond/Identity" op: "Identity" input: "cond/Const:output:0" attr { key: "T" value { type: DT_BOOL } } } node_def { name: "cond/Identity_1" op: "Identity" input: "cond_identity_1_x" attr { key: "T" value { type: DT_INT32 } } } ret { key: "cond_identity" value: "cond/Identity:output:0" } ret { key: "cond_identity_1" value: "cond/Identity_1:output:0" } attr { key: "_construction_context" value { s: "kEagerRuntime" } } arg_attr { key: 0 value { attr { key: "_output_shapes" value { list { shape { } } } } } } } } versions { producer: 898 min_consumer: 12 }
Большую часть времени tf.function
будет работать без особых соображений. Однако есть некоторые предостережения, и здесь может помочь руководство по tf.function , а также полный справочник по AutoGraph.
Полиморфизм: одна Function
, много графов
tf.Graph
специализирован для определенного типа входных данных (например, тензоры с определенным dtype
или объекты с одним и тем же id()
).
Каждый раз, когда вы вызываете Function
с новыми dtypes
и shape в своих аргументах, Function
создает новый tf.Graph
для новых аргументов. dtypes
и формы входных данных tf.Graph
известны как входная подпись или просто подпись .
Function
сохраняет tf.Graph
, соответствующий этой подписи, в ConcreteFunction
. ConcreteFunction
— это оболочка вокруг tf.Graph
.
@tf.function
def my_relu(x):
return tf.maximum(0., x)
# `my_relu` creates new graphs as it observes more signatures.
print(my_relu(tf.constant(5.5)))
print(my_relu([1, -1]))
print(my_relu(tf.constant([3., -3.])))
tf.Tensor(5.5, shape=(), dtype=float32) tf.Tensor([1. 0.], shape=(2,), dtype=float32) tf.Tensor([3. 0.], shape=(2,), dtype=float32)
Если Function
уже была вызвана с этой сигнатурой, Function
не создает новый tf.Graph
.
# These two calls do *not* create new graphs.
print(my_relu(tf.constant(-2.5))) # Signature matches `tf.constant(5.5)`.
print(my_relu(tf.constant([-1., 1.]))) # Signature matches `tf.constant([3., -3.])`.
tf.Tensor(0.0, shape=(), dtype=float32) tf.Tensor([0. 1.], shape=(2,), dtype=float32)
Поскольку Function
поддерживается несколькими графами, она является полиморфной . Это позволяет ему поддерживать больше типов ввода, чем может представить один tf.Graph
, а также оптимизировать каждый tf.Graph
для повышения производительности.
# There are three `ConcreteFunction`s (one for each graph) in `my_relu`.
# The `ConcreteFunction` also knows the return type and shape!
print(my_relu.pretty_printed_concrete_signatures())
my_relu(x) Args: x: float32 Tensor, shape=() Returns: float32 Tensor, shape=() my_relu(x=[1, -1]) Returns: float32 Tensor, shape=(2,) my_relu(x) Args: x: float32 Tensor, shape=(2,) Returns: float32 Tensor, shape=(2,)
Использование tf.function
До сих пор вы узнали, как преобразовать функцию Python в граф, просто используя tf.function
в качестве декоратора или оболочки. Но на практике заставить tf.function
работать правильно может быть сложно! В следующих разделах вы узнаете, как заставить ваш код работать должным образом с помощью tf.function
.
Выполнение графика против нетерпеливого выполнения
Код в Function
может выполняться как жадно, так и в виде графика. По умолчанию Function
выполняет свой код в виде графа:
@tf.function
def get_MSE(y_true, y_pred):
sq_diff = tf.pow(y_true - y_pred, 2)
return tf.reduce_mean(sq_diff)
y_true = tf.random.uniform([5], maxval=10, dtype=tf.int32)
y_pred = tf.random.uniform([5], maxval=10, dtype=tf.int32)
print(y_true)
print(y_pred)
tf.Tensor([1 0 4 4 7], shape=(5,), dtype=int32) tf.Tensor([3 6 3 0 6], shape=(5,), dtype=int32)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=11>
Чтобы убедиться, что граф вашей Function
выполняет те же вычисления, что и ее эквивалентная функция Python, вы можете заставить ее выполняться с нетерпением с помощью tf.config.run_functions_eagerly(True)
. Это переключатель, который отключает возможность Function
создавать и запускать графики , вместо обычного выполнения кода.
tf.config.run_functions_eagerly(True)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=11>
# Don't forget to set it back when you are done.
tf.config.run_functions_eagerly(False)
Однако Function
может вести себя по-разному при графическом и энергичном выполнении. Функция print
Python — один из примеров того, чем отличаются эти два режима. Давайте посмотрим, что происходит, когда вы вставляете оператор print
в свою функцию и многократно вызываете ее.
@tf.function
def get_MSE(y_true, y_pred):
print("Calculating MSE!")
sq_diff = tf.pow(y_true - y_pred, 2)
return tf.reduce_mean(sq_diff)
Обратите внимание на то, что напечатано:
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!
Вывод удивителен? get_MSE
печатается только один раз, хотя вызывается трижды .
Чтобы объяснить, оператор print
выполняется, когда Function
запускает исходный код для создания графика в процессе, известном как «трассировка» . Трассировка фиксирует операции TensorFlow в графе, а print
не фиксируется в графе. Затем этот график выполняется для всех трех вызовов без повторного запуска кода Python .
В качестве проверки работоспособности давайте отключим выполнение графа для сравнения:
# Now, globally set everything to run eagerly to force eager execution.
tf.config.run_functions_eagerly(True)
# Observe what is printed below.
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE! Calculating MSE! Calculating MSE!
tf.config.run_functions_eagerly(False)
print
-- это побочный эффект Python , и есть и другие отличия , о которых следует помнить при преобразовании функции в Function
. Узнайте больше в разделе « Ограничения » руководства « Повышение производительности с помощью tf.function» .
Нестрогое исполнение
Выполнение графа выполняет только операции, необходимые для получения наблюдаемых эффектов, в том числе:
- Возвращаемое значение функции
- Задокументированы известные побочные эффекты, такие как:
- Операции ввода/вывода, такие как
tf.print
- Операции отладки, такие как функции assert в
tf.debugging
- Мутации
tf.Variable
- Операции ввода/вывода, такие как
Такое поведение обычно известно как «нестрогое выполнение» и отличается от энергичного выполнения, при котором выполняются все операции программы, необходимые или нет.
В частности, проверка ошибок во время выполнения не считается наблюдаемым эффектом. Если операция пропущена из-за того, что она не нужна, она не может вызвать никаких ошибок времени выполнения.
В следующем примере «ненужная» операция tf.gather
пропускается во время выполнения графа, поэтому ошибка времени выполнения InvalidArgumentError
не возникает, как это было бы при активном выполнении. Не полагайтесь на ошибку, возникающую при выполнении графа.
def unused_return_eager(x):
# Get index 1 will fail when `len(x) == 1`
tf.gather(x, [1]) # unused
return x
try:
print(unused_return_eager(tf.constant([0.0])))
except tf.errors.InvalidArgumentError as e:
# All operations are run during eager execution so an error is raised.
print(f'{type(e).__name__}: {e}')
tf.Tensor([0.], shape=(1,), dtype=float32)
@tf.function
def unused_return_graph(x):
tf.gather(x, [1]) # unused
return x
# Only needed operations are run during graph exection. The error is not raised.
print(unused_return_graph(tf.constant([0.0])))
tf.Tensor([0.], shape=(1,), dtype=float32)
Лучшие практики tf.function
Может потребоваться некоторое время, чтобы привыкнуть к поведению Function
. Чтобы быстро приступить к работе, начинающие пользователи должны поэкспериментировать с декорированием игрушечных функций с помощью @tf.function
, чтобы получить опыт перехода от нетерпеливого к графическому выполнению.
Разработка для tf.function
может быть лучшим выбором для написания программ TensorFlow, совместимых с графами. Вот несколько советов:
- Быстро и часто переключайтесь между энергичным и графическим выполнением с помощью
tf.config.run_functions_eagerly
, чтобы точно определить, расходятся ли/когда эти два режима. - Создайте
tf.Variable
вне функции Python и измените их внутри. То же самое касается объектов, использующихtf.Variable
, таких какkeras.layers
,keras.Model
иtf.optimizers
. - Избегайте написания функций, зависящих от внешних переменных Python , за исключением
tf.Variable
и Keras. - Предпочитаю писать функции, которые принимают на вход тензоры и другие типы TensorFlow. Вы можете передать другие типы объектов, но будьте осторожны !
- Включите как можно больше вычислений в
tf.function
, чтобы максимизировать прирост производительности. Например, украсьте весь тренировочный шаг или весь тренировочный цикл.
Увидев ускорение
tf.function
обычно улучшает производительность вашего кода, но степень ускорения зависит от типа вычислений, которые вы выполняете. В небольших вычислениях могут преобладать накладные расходы на вызов графа. Вы можете измерить разницу в производительности следующим образом:
x = tf.random.uniform(shape=[10, 10], minval=-1, maxval=2, dtype=tf.dtypes.int32)
def power(x, y):
result = tf.eye(10, dtype=tf.dtypes.int32)
for _ in range(y):
result = tf.matmul(x, result)
return result
print("Eager execution:", timeit.timeit(lambda: power(x, 100), number=1000))
Eager execution: 2.5637862179974036
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.6832536700021592
tf.function
обычно используется для ускорения обучающих циклов, и вы можете узнать больше об этом в статье Написание обучающего цикла с нуля с помощью Keras.
Производительность и компромиссы
Графики могут ускорить ваш код, но процесс их создания сопряжен с некоторыми накладными расходами. Для некоторых функций создание графика занимает больше времени, чем выполнение графика. Эти вложения обычно быстро окупаются за счет повышения производительности последующих запусков, но важно помнить, что первые несколько шагов обучения любой крупной модели могут выполняться медленнее из-за трассировки.
Независимо от того, насколько велика ваша модель, вам следует избегать частой трассировки. В руководстве по tf.function
обсуждается , как установить входные спецификации и использовать тензорные аргументы , чтобы избежать повторного отслеживания. Если вы обнаружите, что у вас необычно низкая производительность, рекомендуется проверить, не происходит ли случайное восстановление.
Когда выполняется трассировка Function
?
Чтобы выяснить, когда ваша Function
выполняет трассировку, добавьте в ее код оператор print
. Как правило, Function
будет выполнять оператор print
каждый раз, когда трассирует.
@tf.function
def a_function_with_python_side_effect(x):
print("Tracing!") # An eager-only side effect.
return x * x + tf.constant(2)
# This is traced the first time.
print(a_function_with_python_side_effect(tf.constant(2)))
# The second time through, you won't see the side effect.
print(a_function_with_python_side_effect(tf.constant(3)))
Tracing! tf.Tensor(6, shape=(), dtype=int32) tf.Tensor(11, shape=(), dtype=int32)
# This retraces each time the Python argument changes,
# as a Python argument could be an epoch count or other
# hyperparameter.
print(a_function_with_python_side_effect(2))
print(a_function_with_python_side_effect(3))
Tracing! tf.Tensor(6, shape=(), dtype=int32) Tracing! tf.Tensor(11, shape=(), dtype=int32)
Новые аргументы Python всегда вызывают создание нового графа, следовательно, дополнительная трассировка.
Следующие шаги
Вы можете узнать больше о tf.function
на справочной странице API и в руководстве по повышению производительности с помощью tf.function
.