Сохраните дату! Google I / O возвращается 18-20 мая Зарегистрируйтесь сейчас
Эта страница переведена с помощью Cloud Translation API.
Switch to English

Введение в графики и tf.функции

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Обзор

В этом руководстве подробно рассказывается о TensorFlow и Keras, чтобы увидеть, как работает TensorFlow. Если вместо этого вы хотите немедленно приступить к работе с Keras, просмотрите нашу коллекцию руководств по Keras .

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

Это общий обзор, который показывает, как tf.function позволяет переключаться с tf.function выполнения на выполнение графа. Для более полной спецификации tf.function см. Руководство tf.function .

Что такое графики?

В трех предыдущих руководствах вы видели, как активно работает TensorFlow . Это означает, что операции TensorFlow выполняются Python, операция за операцией, и возвращают результаты обратно в Python.

Несмотря на то, что активное выполнение имеет несколько уникальных преимуществ, выполнение графа обеспечивает переносимость вне Python и, как правило, обеспечивает лучшую производительность. Выполнение графа означает, что тензорные вычисления выполняются как граф tf.Graph , который иногда называют tf.Graph или просто «графом».

Графы - это структуры данных, которые содержат набор объектов tf.Operation , которые представляют единицы вычисления; и объекты tf.Tensor , которые представляют единицы данных, которые tf.Tensor между операциями. Они определены в контексте tf.Graph . Поскольку эти графики представляют собой структуры данных, их можно сохранять, запускать и восстанавливать без использования исходного кода Python.

Так выглядит граф TensorFlow, представляющий двухслойную нейронную сеть, при визуализации в TensorBoard.

Простой график TensorFlow

Преимущества графиков

График дает большую гибкость. Вы можете использовать свой граф TensorFlow в средах, в которых нет интерпретатора Python, например в мобильных приложениях, встроенных устройствах и внутренних серверах. TensorFlow использует графики в качестве формата для сохраненных моделей при их экспорте из Python.

Графики также легко оптимизируются, что позволяет компилятору выполнять такие преобразования, как:

  • Статически вывести значение тензоров путем сворачивания константных узлов в ваших вычислениях («сворачивание констант») .
  • Отделяйте независимые части вычислений и разделяйте их между потоками или устройствами.
  • Упростите арифметические операции, исключив общие подвыражения.

Для этого и других ускорений существует целая система оптимизации Grappler .

Короче говоря, графики чрезвычайно полезны и позволяют вашему TensorFlow работать быстро , параллельно и эффективно на нескольких устройствах .

Однако вы по-прежнему хотите определять наши модели машинного обучения (или другие вычисления) в Python для удобства, а затем автоматически строить графики, когда они вам нужны.

Использование графиков

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

import tensorflow as tf
import timeit
from datetime import datetime
# 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 и т. Д. Хотя операции tf.Graph легко захватываются 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 do_return, retval_
            (do_return, retval_) = vars_

        def if_body():
            nonlocal do_return, retval_
            try:
                do_return = True
                retval_ = ag__.ld(x)
            except:
                do_return = False
                raise

        def else_body():
            nonlocal do_return, retval_
            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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_1"
      }
    }
    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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_2"
      }
    }
    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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_3"
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond/Const_3:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_4"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond/Const_4:output:0"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    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
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond/Const:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond_identity_1_x"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    arg_attr {
      key: 0
      value {
        attr {
          key: "_output_shapes"
          value {
            list {
              shape {
              }
            }
          }
        }
      }
    }
  }
}
versions {
  producer: 561
  min_consumer: 12
}

В большинстве случаев tf.function будет работать без особых соображений. Однако есть некоторые предостережения, и здесь может помочь руководство tf.function , а также полный справочник по AutoGraph.

Полиморфизм: одна Function , много графиков

tf.Graph специализирован для определенного типа входных данных (например, тензоров с определеннымdtype или объектов с тем же id() ).

Каждый раз, когда вы вызываете Function с новыми dtypes и формами в аргументах, Function создает новый tf.Graph для новых аргументов. dtypes и формы tf.Graph данных 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 sees 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)
  Args:
    x: float32 Tensor, shape=(2,)
  Returns:
    float32 Tensor, shape=(2,)

my_relu(x=[1, -1])
  Returns:
    float32 Tensor, shape=(2,)

Использование tf.function

До сих пор вы видели, как можно преобразовать функцию Python в график, просто используя tf.function в качестве декоратора или оболочки. Но на практике заставить 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 2 3 6 8], shape=(5,), dtype=int32)
tf.Tensor([0 1 8 7 5], shape=(5,), dtype=int32)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=7>

Чтобы убедиться, что график вашей Function выполняет те же вычисления, что и его эквивалентная функция Python, вы можете заставить его tf.config.run_functions_eagerly(True) выполняться с помощью 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=7>
# 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 лучшие практики

Чтобы привыкнуть к поведению Function может потребоваться некоторое время. Чтобы быстро приступить к работе, @tf.function пользователи должны поиграть с украшением игрушечных функций с помощью @tf.function чтобы получить опыт перехода от желания к выполнению графа.

Разработка для tf.function может быть вашим лучшим выбором для написания программtf.function совместимых с графами. Вот несколько советов:

  • Переключайтесь между нетерпеливым и графическим исполнением раньше и часто с помощью tf.config.run_functions_eagerly чтобы точно определить, расходятся ли / когда два режима.
  • Создайте tf.Variable вне функции Python и измените их внутри. То же самое касается объектов, использующих tf.Variable , таких какkeras.layers , keras.Model s и tf.optimizers .
  • Избегайте написания функций, которые зависят от внешних переменных Python , за исключением tf.Variables и tf.Variables .
  • Предпочитайте писать функции, которые принимают на вход тензоры и другие типы 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: 1.777665522999996
power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.5308018169999968

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 всегда запускают создание нового графика.

Следующие шаги

Вы можете прочитать более подробное обсуждение как на справочной странице API tf.function и в руководстве .