TensorFlow 1.x против TensorFlow 2 - Поведение и API

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

Под капотом TensorFlow 2 следует принципиально отличной от TF1.x парадигме программирования.

В этом руководстве описываются фундаментальные различия между TF1.x и TF2 с точки зрения поведения и API, а также то, как все это относится к вашему путешествию по миграции.

Краткое изложение основных изменений

По сути, TF1.x и TF2 используют разные наборы поведения во время выполнения в отношении выполнения (особенно в TF2), переменных, потока управления, тензорных форм и тензорных сравнений на равенство. Чтобы быть совместимым с TF2, ваш код должен быть совместим с полным набором поведений TF2. Во время миграции вы можете включать или отключать большинство этих режимов по отдельности через tf.compat.v1.enable_* или tf.compat.v1.disable_* . Единственным исключением является удаление коллекций, что является побочным эффектом включения/отключения нетерпеливого выполнения.

На высоком уровне TensorFlow 2:

  • Удаляет избыточные API .
  • Делает API более согласованными — например, Unified RNN и Unified Optimizers .
  • Предпочитает функции сеансам и лучше интегрируется со средой выполнения Python с включенным по умолчанию выполнением Eager вместе с tf.function , который обеспечивает автоматические зависимости управления для графиков и компиляции.
  • Устаревает глобальные коллекции графов.
  • Изменяет семантику параллелизма переменных, используя ResourceVariables вместо ReferenceVariables .
  • Поддерживает функциональный и дифференцируемый поток управления (Control Flow v2).
  • Упрощает API TensorShape для хранения int вместо объектов tf.compat.v1.Dimension .
  • Обновлена ​​механика равенства тензоров. В TF1.x оператор == для тензоров и переменных проверяет равенство ссылок на объекты. В TF2 он проверяет равенство значений. Кроме того, тензоры/переменные больше не хэшируются, но вы можете получить хэшируемые ссылки на них через var.ref() , если вам нужно использовать их в наборах или в качестве ключей dict .

В разделах ниже содержится более подробная информация о различиях между TF1.x и TF2. Чтобы узнать больше о процессе разработки TF2, прочитайте RFC и документацию по дизайну .

Очистка API

Многие API либо ушли, либо перемещены в TF2. Некоторые из основных изменений включают удаление tf.app , tf.flags и tf.logging в пользу absl-py с открытым исходным кодом, перемещение проектов, которые жили в tf.contrib , и очистку основного пространства имен tf.* с помощью перемещение менее используемых функций в подпакеты, такие как tf.math . Некоторые API были заменены их эквивалентами из TF2 — tf.summary , tf.keras.metrics и tf.keras.optimizers .

tf.compat.v1 : Устаревшие и совместимые конечные точки API

Символы в пространствах имен tf.compat и tf.compat.v1 не считаются API TF2. Эти пространства имен предоставляют сочетание символов совместимости, а также устаревшие конечные точки API из TF 1.x. Они предназначены для помощи при переходе с TF1.x на TF2. Однако, поскольку ни один из этих API compat.v1 не является идиоматическим API TF2, не используйте их для написания совершенно нового кода TF2.

Отдельные символы tf.compat.v1 могут быть совместимы с TF2, поскольку они продолжают работать даже при включенном поведении TF2 (например, tf.compat.v1.losses.mean_squared_error ), в то время как другие несовместимы с TF2 (например, tf.compat.v1.metrics.accuracy ). Многие символы compat.v1 (хотя и не все) содержат в своей документации специальную информацию о миграции, которая объясняет степень их совместимости с поведением TF2, а также способы их переноса в API TF2.

Сценарий обновления TF2 может сопоставлять многие символы API compat.v1 с эквивалентными API TF2 в случае, если они являются псевдонимами или имеют те же аргументы, но в другом порядке. Вы также можете использовать сценарий обновления для автоматического переименования API TF1.x.

API-интерфейсы ложных друзей

В пространстве имен TF2 tf (не в compat.v1 ) есть набор символов «ложных друзей», которые фактически игнорируют поведение TF2 «под капотом» и/или не полностью совместимы с полным набором поведения TF2. Таким образом, эти API-интерфейсы могут неправильно работать с кодом TF2, возможно, скрытым образом.

  • tf.estimator.* : Оценщики создают и используют графики и сеансы «под капотом». Таким образом, их нельзя считать совместимыми с TF2. Если ваш код использует оценщики, он не использует поведение TF2.
  • keras.Model.model_to_estimator(...) : это создает Estimator под капотом, который, как упоминалось выше, несовместим с TF2.
  • tf.Graph().as_default() : это вводит поведение графика TF1.x и не соответствует стандартному поведению tf.function , совместимому с TF2. Код, который вводит подобные графы, обычно запускает их через сеансы и не должен считаться совместимым с TF2.
  • tf.feature_column.* API-интерфейсы столбцов функций обычно основаны на создании переменных tf.compat.v1.get_variable в стиле TF1 и предполагают, что доступ к созданным переменным будет осуществляться через глобальные коллекции. Поскольку TF2 не поддерживает коллекции, API-интерфейсы могут работать некорректно при их запуске с включенным поведением TF2.

Другие изменения API

  • В TF2 значительно улучшены алгоритмы размещения устройств, что делает использование tf.colocate_with ненужным. Если его удаление приводит к снижению производительности , отправьте сообщение об ошибке .

  • Замените все использование tf.v1.ConfigProto эквивалентными функциями из tf.config .

Нетерпеливое исполнение

TF1.x требовал, чтобы вы вручную сшили абстрактное синтаксическое дерево (граф), выполнив вызовы tf.* API, а затем вручную скомпилировали абстрактное синтаксическое дерево, передав набор выходных тензоров и входных тензоров вызову session.run . TF2 выполняется быстро (как обычно Python) и делает графики и сеансы похожими на детали реализации.

Одним заметным побочным продуктом нетерпеливого выполнения является то, что tf.control_dependencies больше не требуется, так как все строки кода выполняются по порядку (внутри tf.function код с побочными эффектами выполняется в том порядке, в котором он написан).

Нет больше глобальных

TF1.x в значительной степени полагался на неявные глобальные пространства имен и коллекции. Когда вы вызывали tf.Variable , она помещалась в коллекцию графа по умолчанию и оставалась там, даже если вы потеряли след переменной Python, указывающей на нее. Затем вы можете восстановить этот tf.Variable , но только если вы знаете имя, с которым он был создан. Это было трудно сделать, если вы не контролировали создание переменной. В результате появилось множество всевозможных механизмов, пытающихся помочь вам снова найти ваши переменные, а фреймворкам — найти переменные, созданные пользователем. Некоторые из них включают: области видимости переменных, глобальные коллекции, вспомогательные методы, такие как tf.get_global_step и tf.global_variables_initializer , оптимизаторы, неявно вычисляющие градиенты для всех обучаемых переменных, и так далее. TF2 устраняет все эти механизмы ( Variables 2.0 RFC ) в пользу механизма по умолчанию — вы отслеживаете свои переменные. Если вы потеряете tf.Variable , он будет собран мусором.

Требование отслеживать переменные создает некоторую дополнительную работу, но с такими инструментами, как прокладки моделирования и поведения, такие как неявные объектно-ориентированные коллекции переменных в tf.Module s и tf.keras.layers.Layer s , нагрузка сводится к минимуму.

Функции, а не сеансы

Вызов session.run почти аналогичен вызову функции: вы указываете входные данные и вызываемую функцию и получаете набор выходных данных. В TF2 вы можете декорировать функцию Python с помощью tf.function , чтобы пометить ее для JIT-компиляции, чтобы TensorFlow запускал ее как единый граф ( RFC Functions 2.0 ). Этот механизм позволяет TF2 получить все преимущества графического режима:

  • Производительность: функция может быть оптимизирована (удаление узлов, слияние ядер и т. д.).
  • Переносимость: функцию можно экспортировать/повторно импортировать ( SavedModel 2.0 RFC ), что позволяет повторно использовать модульные функции TensorFlow и делиться ими.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Благодаря возможности свободно чередовать код Python и TensorFlow, вы можете воспользоваться преимуществами выразительности Python. Однако переносимый TensorFlow выполняется в контекстах без интерпретатора Python, таких как мобильные устройства, C++ и JavaScript. Чтобы избежать переписывания кода при добавлении tf.function , используйте AutoGraph для преобразования подмножества конструкций Python в их эквиваленты TensorFlow:

  • for / while -> tf.while_loop (поддерживаются break и continue )
  • if -> tf.cond
  • for _ in dataset -> набор dataset.reduce

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

Адаптация к изменениям поведения TF 2.x

Ваш переход на TF2 будет завершен только после того, как вы перейдете на полный набор поведений TF2. Полный набор поведений можно включить или отключить с помощью tf.compat.v1.enable_v2_behaviors и tf.compat.v1.disable_v2_behaviors . В следующих разделах подробно рассматривается каждое основное изменение поведения.

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

Наибольшие изменения в ваших программах во время миграции, скорее всего, произойдут из-за смены парадигмы фундаментальной модели программирования с графов и сеансов на активное выполнение и tf.function . Обратитесь к руководствам по миграции TF2, чтобы узнать больше о переходе от API, несовместимых с немедленным выполнением и tf.function , к API, совместимым с ними.

Ниже приведены некоторые общие программные шаблоны, не привязанные к какому-либо одному API, которые могут вызвать проблемы при переключении с tf.Graph и tf.compat.v1.Session на активное выполнение с tf.function s.

Шаблон 1: манипулирование объектами Python и создание переменных, предназначенные для выполнения только один раз, выполняются несколько раз.

В программах TF1.x, которые полагаются на графы и сеансы, обычно ожидается, что вся логика Python в вашей программе будет запущена только один раз. Однако при нетерпеливом выполнении и tf.function справедливо ожидать, что ваша логика Python будет запущена хотя бы один раз, но, возможно, и больше раз (либо несколько раз с нетерпением, либо несколько раз по разным tf.function ). Иногда tf.function даже дважды отслеживает одни и те же входные данные, что приводит к неожиданному поведению (см. примеры 1 и 2). Обратитесь к руководству по tf.function для более подробной информации.

Пример 1: Создание переменной

Рассмотрим пример ниже, где функция создает переменную при вызове:

def f():
  v = tf.Variable(1.0)
  return v

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    res = f()
    sess.run(tf.compat.v1.global_variables_initializer())
    sess.run(res)

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

@tf.function
def f():
  print("trace") # This will print twice because the python body is run twice
  v = tf.Variable(1.0)
  return v

try:
  f()
except ValueError as e:
  print(e)

Обходной путь заключается в кэшировании и повторном использовании переменной после ее создания при первом вызове.

class Model(tf.Module):
  def __init__(self):
    self.v = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    return self.v

m = Model()
m()

Пример 2: тензоры вне области видимости из-за повторной трассировки tf.function

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

class Model(tf.Module):
  def __init__(self):
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print once: only traced once
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return next(it)

m = Model()
m()

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

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
try:
  m()
except TypeError as e:
  print(e) # <tf.Tensor ...> is out of scope and cannot be used here.

Наиболее простым решением является обеспечение того, чтобы создание переменных и создание набора данных находились вне вызова tf.funciton . Например:

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    if self.v is None:
      self.v = tf.Variable(0)

  @tf.function
  def __call__(self):
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

Однако иногда нельзя избежать создания переменных в tf.function (например, переменные слота в некоторых оптимизаторах TF keras ). Тем не менее, мы можем просто перенести создание набора данных за пределы вызова tf.function . Причина, по которой мы можем на это положиться, заключается в том, что tf.function получит набор данных как неявный ввод, и оба графа смогут получить к нему правильный доступ.

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])

  @tf.function
  def __call__(self):
    if self.v is None:
      self.v = tf.Variable(0)
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

Пример 3: Неожиданное повторное создание объекта Tensorflow из-за использования dict

tf.function имеет очень плохую поддержку побочных эффектов Python, таких как добавление в список или проверка/добавление в словарь. Более подробная информация находится в разделе «Улучшение производительности с помощью tf.function» . В приведенном ниже примере код использует словари для кэширования наборов данных и итераторов. Для одного и того же ключа каждый вызов модели будет возвращать один и тот же итератор набора данных.

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.compat.v1.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = self.datasets[key].make_initializable_iterator()
    return self.iterators[key]

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    m = Model()
    it = m('a')
    sess.run(it.initializer)
    for _ in range(3):
      print(sess.run(it.get_next())) # prints 1, 2, 3

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

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 1, 1

Мы можем использовать tf.init_scope , чтобы поднять набор данных и создание итератора за пределы графа, чтобы добиться ожидаемого поведения:

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      # Lifts ops out of function-building graphs
      with tf.init_scope():
        self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
        self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 2, 3

Общее эмпирическое правило заключается в том, чтобы не полагаться на побочные эффекты Python в своей логике и использовать их только для отладки трассировок.

Пример 4: Управление глобальным списком Python

Следующий код TF1.x использует глобальный список потерь, который используется только для ведения списка потерь, сгенерированных на текущем шаге обучения. Обратите внимание, что логика Python, которая добавляет потери в список, будет вызываться только один раз, независимо от того, сколько шагов обучения выполняется в сеансе.

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

g = tf.Graph()
with g.as_default():
  ...
  # initialize all objects
  model = Model()
  optimizer = ...
  ...
  # train step
  model(...)
  total_loss = tf.reduce_sum(all_losses)
  optimizer.minimize(total_loss)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)  

Однако, если эта логика Python наивно отображается на TF2 с нетерпеливым выполнением, к глобальному списку потерь будут добавляться новые значения на каждом этапе обучения. Это означает, что код шага обучения, который ранее ожидал, что список будет содержать только потери из текущего шага обучения, теперь фактически видит список потерь из всех шагов обучения, выполненных до сих пор. Это непреднамеренное изменение поведения, и список необходимо либо очищать в начале каждого шага, либо делать локальным для шага обучения.

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

# initialize all objects
model = Model()
optimizer = ...

def train_step(...)
  ...
  model(...)
  total_loss = tf.reduce_sum(all_losses) # global list is never cleared,
  # Accidentally accumulates sum loss across all training steps
  optimizer.minimize(total_loss)
  ...

Паттерн 2: символический тензор, который должен пересчитываться каждый шаг в TF1.x, случайно кэшируется с начальным значением при переключении на нетерпеливый.

Этот шаблон обычно приводит к неправильному поведению вашего кода при активном выполнении вне tf.functions, но вызывает InaccessibleTensorError , если кэширование начального значения происходит внутри tf.function . Однако имейте в виду, что для того, чтобы избежать приведенного выше шаблона 1 , вы часто непреднамеренно структурируете свой код таким образом, что это кэширование начального значения происходит вне любой tf.function , которая может вызвать ошибку. Поэтому будьте особенно осторожны, если вы знаете, что ваша программа может быть восприимчива к этому шаблону.

Общее решение для этого шаблона состоит в том, чтобы реструктурировать код или использовать вызываемые объекты Python, если это необходимо, чтобы убедиться, что значение каждый раз пересчитывается, а не случайно кэшируется.

Пример 1: скорость обучения/гиперпараметр/и т. д. расписания, зависящие от глобального шага

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

g = tf.Graph()
with g.as_default():
  ...
  global_step = tf.Variable(0)
  learning_rate = 1.0 / global_step
  opt = tf.compat.v1.train.GradientDescentOptimizer(learning_rate)
  ...
  global_step.assign_add(1)
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

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

global_step = tf.Variable(0)
learning_rate = 1.0 / global_step # Wrong! Only computed once!
opt = tf.keras.optimizers.SGD(learning_rate)

def train_step(...):
  ...
  opt.apply_gradients(...)
  global_step.assign_add(1)
  ...

Поскольку этот конкретный пример является распространенным шаблоном, а оптимизаторы должны быть инициализированы только один раз, а не на каждом шаге обучения, оптимизаторы TF2 поддерживают расписания tf.keras.optimizers.schedules.LearningRateSchedule или вызываемые объекты Python в качестве аргументов для скорости обучения и других гиперпараметров.

Пример 2. Инициализации символьных случайных чисел, назначенные в качестве атрибутов объекта, а затем повторно используемые с помощью указателя, случайно кэшируются при переключении на нетерпеливый

Рассмотрим следующий модуль NoiseAdder :

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution + input) * self.trainable_scale

Использование его следующим образом в TF1.x будет вычислять новый тензор случайного шума каждый раз при запуске сеанса:

g = tf.Graph()
with g.as_default():
  ...
  # initialize all variable-containing objects
  noise_adder = NoiseAdder(shape, mean)
  ...
  # computation pass
  x_with_noise = noise_adder.add_noise(x)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

Однако в TF2 инициализация noise_adder в начале приведет к тому, что noise_distribution будет вычисляться только один раз и будет заморожен для всех этапов обучения:

...
# initialize all variable-containing objects
noise_adder = NoiseAdder(shape, mean) # Freezes `self.noise_distribution`!
...
# computation pass
x_with_noise = noise_adder.add_noise(x)
...

Чтобы исправить это, рефакторинг NoiseAdder для вызова tf.random.normal каждый раз, когда требуется новый случайный тензор, вместо того, чтобы каждый раз обращаться к одному и тому же объекту тензора.

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = lambda: tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution() + input) * self.trainable_scale

Паттерн 3: код TF1.x напрямую использует тензоры и ищет их по имени

Обычно тесты кода TF1.x полагаются на проверку того, какие тензоры или операции присутствуют в графе. В некоторых редких случаях код моделирования также будет полагаться на эти поиски по имени.

Имена тензоров вообще не генерируются при активном выполнении вне tf.function , поэтому все использования tf.Tensor.name должны происходить внутри tf.function . Имейте в виду, что фактически сгенерированные имена, скорее всего, будут отличаться в TF1.x и TF2 даже в пределах одной и той же tf.function , а гарантии API не гарантируют стабильности сгенерированных имен в разных версиях TF.

Паттерн 4: сеанс TF1.x выборочно запускает только часть сгенерированного графа

В TF1.x вы можете построить граф, а затем выбрать выборочный запуск только его подмножества с сеансом, выбрав набор входных и выходных данных, которые не требуют запуска каждой операции в графе.

Например, у вас может быть и генератор, и дискриминатор внутри одного графа, и вы можете использовать отдельные вызовы tf.compat.v1.Session.run для переключения между обучением только дискриминатора или только обучением генератора.

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

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

Удаление коллекций

Когда активное выполнение включено, API-интерфейсы compat.v1 , связанные с коллекциями графов (включая те, которые считывают или записывают коллекции под капотом, такие как tf.compat.v1.trainable_variables ), больше не доступны. Некоторые могут вызывать ValueError , а другие могут молча возвращать пустые списки.

Наиболее стандартное использование коллекций в TF1.x — поддержка инициализаторов, глобального шага, весов, потерь при регуляризации, потерь на выходе модели и обновлений переменных, которые необходимо запускать, например, из слоев BatchNormalization .

Для обработки каждого из этих стандартных способов использования:

  1. Инициализаторы - игнорировать. Ручная инициализация переменных не требуется, если включено активное выполнение.
  2. Глобальный шаг. Инструкции по миграции см. в документации tf.compat.v1.train.get_or_create_global_step .
  3. Веса — сопоставьте свои модели с tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s, следуя указаниям в руководстве по сопоставлению моделей, а затем используйте их соответствующие механизмы отслеживания веса, такие как tf.module.trainable_variables .
  4. Потери регуляризации. Сопоставьте свои модели с tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s, следуя указаниям в руководстве по сопоставлению моделей, а затем используйте tf.keras.losses . Кроме того, вы также можете вручную отслеживать свои потери от регуляризации.
  5. Моделируйте выходные потери — используйте механизмы управления потерями tf.keras.Model или отдельно отслеживайте свои потери без использования коллекций.
  6. Обновления веса — игнорировать эту коллекцию. Стремительное выполнение и tf.function (с зависимостями autograph и auto-control) означают, что все обновления переменных будут выполняться автоматически. Таким образом, вам не нужно явно запускать все обновления весов в конце, но обратите внимание, что это означает, что обновления весов могут происходить в другое время, чем в вашем коде TF1.x, в зависимости от того, как вы использовали управляющие зависимости.
  7. Сводки — см. руководство по переносу сводных API .

Более сложное использование коллекций (например, использование пользовательских коллекций) может потребовать от вас рефакторинга кода либо для поддержки собственных глобальных хранилищ, либо для того, чтобы он вообще не зависел от глобальных хранилищ.

ResourceVariables вместо ReferenceVariables

ResourceVariables имеют более сильные гарантии согласованности чтения-записи, чем ReferenceVariables . Это приводит к более предсказуемой и простой для понимания семантике того, будете ли вы наблюдать результат предыдущей записи при использовании ваших переменных. Крайне маловероятно, что это изменение приведет к тому, что существующий код вызовет ошибки или автоматически сломается.

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

Чтобы изолировать влияние этого изменения поведения на ваш код, если ускоренное выполнение отключено, вы можете использовать tf.compat.v1.disable_resource_variables() и tf.compat.v1.enable_resource_variables() для глобального отключения или включения этого изменения поведения. ResourceVariables всегда будут использоваться, если включено активное выполнение.

Поток управления v2

В TF1.x операции потока управления, такие как tf.cond и tf.while_loop , встроены в низкоуровневые операции, такие как Switch , Merge и т. д. TF2 предоставляет улучшенные операции потока функционального управления, которые реализованы с отдельными tf.function для каждой ветви и поддержки. дифференциация более высокого порядка.

Чтобы изолировать влияние этого изменения поведения на ваш код, если активное выполнение отключено, вы можете использовать tf.compat.v1.disable_control_flow_v2() и tf.compat.v1.enable_control_flow_v2() , чтобы глобально отключить или включить это изменение поведения. Однако вы можете отключить поток управления v2 только в том случае, если нетерпеливое выполнение также отключено. Если он включен, всегда будет использоваться поток управления v2.

Это изменение поведения может кардинально изменить структуру сгенерированных программ TF, использующих поток управления, поскольку они будут содержать несколько вложенных трассировок функций, а не один плоский график. Таким образом, любой код, сильно зависящий от точной семантики создаваемых трассировок, может потребовать некоторой модификации. Это включает в себя:

  • Код, основанный на именах операторов и тензоров
  • Код, относящийся к тензорам, созданным в ветке потока управления TensorFlow из-за пределов этой ветки. Это может привести к InaccessibleTensorError .

Это изменение поведения должно быть нейтральным или положительным с точки зрения производительности, но если вы столкнетесь с проблемой, когда поток управления v2 работает для вас хуже, чем поток управления TF1.x, отправьте сообщение о проблеме с этапами воспроизведения.

Изменения поведения TensorShape API

Класс TensorShape был упрощен для хранения int вместо объектов tf.compat.v1.Dimension . Поэтому нет необходимости вызывать .value для получения int .

Отдельные объекты tf.compat.v1.Dimension по-прежнему доступны из tf.TensorShape.dims .

Чтобы изолировать влияние этого изменения поведения на ваш код, вы можете использовать tf.compat.v1.disable_v2_tensorshape() и tf.compat.v1.enable_v2_tensorshape() для глобального отключения или включения этого изменения поведения.

Ниже показаны различия между TF1.x и TF2.

import tensorflow as tf
# Create a shape and choose an index
i = 0
shape = tf.TensorShape([16, None, 256])
shape
TensorShape([16, None, 256])

Если у вас было это в TF1.x:

value = shape[i].value

Затем сделайте это в TF2:

value = shape[i]
value
16

Если у вас было это в TF1.x:

for dim in shape:
    value = dim.value
    print(value)

Затем сделайте это в TF2:

for value in shape:
  print(value)
16
None
256

Если у вас было это в TF1.x (или вы использовали любой другой метод измерения):

dim = shape[i]
dim.assert_is_compatible_with(other_dim)

Затем сделайте это в TF2:

other_dim = 16
Dimension = tf.compat.v1.Dimension

if shape.rank is None:
  dim = Dimension(None)
else:
  dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
True
shape = tf.TensorShape(None)

if shape:
  dim = shape.dims[i]
  dim.is_compatible_with(other_dim) # or any other dimension method

Логическое значение tf.TensorShape равно True , если ранг известен, и False в противном случае.

print(bool(tf.TensorShape([])))      # Scalar
print(bool(tf.TensorShape([0])))     # 0-length vector
print(bool(tf.TensorShape([1])))     # 1-length vector
print(bool(tf.TensorShape([None])))  # Unknown-length vector
print(bool(tf.TensorShape([1, 10, 100])))       # 3D tensor
print(bool(tf.TensorShape([None, None, None]))) # 3D tensor with no known dimensions
print()
print(bool(tf.TensorShape(None)))  # A tensor with unknown rank.
True
True
True
True
True
True

False

Возможные ошибки из-за изменений TensorShape

Маловероятно, что изменения в поведении TensorShape незаметно сломают ваш код. Однако вы можете увидеть, что код, связанный с формой, начинает вызывать AttributeError s, поскольку int s и None не имеют тех же атрибутов, что и tf.compat.v1.Dimension s. Ниже приведены некоторые примеры этих AttributeError :

try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  value = shape[0].value
except AttributeError as e:
  # 'int' object has no attribute 'value'
  print(e)
'int' object has no attribute 'value'
try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  dim = shape[1]
  other_dim = shape[2]
  dim.assert_is_compatible_with(other_dim)
except AttributeError as e:
  # 'NoneType' object has no attribute 'assert_is_compatible_with'
  print(e)
'NoneType' object has no attribute 'assert_is_compatible_with'

Тензорное равенство по значению

Бинарные операторы == и != для переменных и тензоров были изменены для сравнения по значению в TF2, а не по ссылке на объект, как в TF1.x. Кроме того, тензоры и переменные больше нельзя напрямую хэшировать или использовать в наборах или ключах dict, потому что может быть невозможно хэшировать их по значению. Вместо этого они предоставляют метод .ref() , который можно использовать для получения хэшируемой ссылки на тензор или переменную.

Чтобы изолировать влияние этого изменения поведения, вы можете использовать tf.compat.v1.disable_tensor_equality() и tf.compat.v1.enable_tensor_equality() для глобального отключения или включения этого изменения поведения.

Например, в TF1.x две переменные с одинаковым значением вернут false при использовании оператора == :

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
False

В то время как в TF2 с включенной проверкой равенства тензоров, x == y вернет True .

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
<tf.Tensor: shape=(), dtype=bool, numpy=True>

Итак, в TF2, если вам нужно сравнить по ссылке на объект, обязательно используйте is и is not

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x is y
False

Хэширование тензоров и переменных

Раньше с поведением TF1.x вы могли напрямую добавлять переменные и тензоры в структуры данных, требующие хеширования, такие как ключи set и dict .

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
set([x, tf.constant(2.0)])
{<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.0>}

Однако в TF2 с включенным равенством тензоров тензоры и переменные становятся нехэшируемыми из-за того, что семантика операторов == и != меняется на проверку равенства значений.

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

try:
  set([x, tf.constant(2.0)])
except TypeError as e:
  # TypeError: Variable is unhashable. Instead, use tensor.ref() as the key.
  print(e)
Variable is unhashable. Instead, use tensor.ref() as the key.

Итак, в TF2, если вам нужно использовать тензорные или переменные объекты в качестве ключей или set содержимое, вы можете использовать tensor.ref() для получения хешируемой ссылки, которую можно использовать в качестве ключа:

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

tensor_set = set([x.ref(), tf.constant(2.0).ref()])
assert x.ref() in tensor_set

tensor_set
{<Reference wrapping <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>>,
 <Reference wrapping <tf.Tensor: shape=(), dtype=float32, numpy=2.0>>}

При необходимости вы также можете получить тензор или переменную из ссылки, используя reference.deref() :

referenced_var = x.ref().deref()
assert referenced_var is x
referenced_var
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>

Ресурсы и дополнительная литература

  • Посетите раздел Миграция в TF2 , чтобы узнать больше о переходе на TF2 с TF1.x.
  • Прочтите руководство по сопоставлению моделей, чтобы узнать больше о сопоставлении ваших моделей TF1.x для работы непосредственно в TF2.