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

Посмотреть на 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

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

С графиком у вас есть большая гибкость. Вы можете использовать граф 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 .