TensorFlow 1.x 與 TensorFlow 2 - 行為和 API

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視 下載筆記本

TensorFlow 2 在底層遵循與 TF1.x 截然不同的程式設計範例。

本指南說明 TF1.x 與 TF2 在行為和 API 方面的基本差異,以及這些差異與您的遷移歷程有何關聯。

主要變更的高階摘要

從根本上說,TF1.x 和 TF2 針對執行 (TF2 中的 Eager Execution)、變數、控制流程、張量形狀和張量等式比較使用一組不同的執行階段行為。若要與 TF2 相容,您的程式碼必須與整組 TF2 行為相容。在遷移期間,您可以透過 tf.compat.v1.enable_*tf.compat.v1.disable_* API 個別啟用或停用大部分行為。唯一的例外是移除集合,這是啟用/停用 Eager Execution 的副作用。

在高階層次上,TensorFlow 2

以下章節提供 TF1.x 和 TF2 之間差異的更多背景資訊。若要深入瞭解 TF2 背後的設計流程,請參閱 RFC設計文件

API 清理

許多 API 在 TF2 中已消失或移動。一些主要變更包括移除 tf.apptf.flagstf.logging,改用現在開放原始碼的 absl-py、重新安置位於 tf.contrib 中的專案,以及透過將較少使用的函式移至子套件 (例如 tf.math) 來清理主要的 tf.* 命名空間。某些 API 已由其 TF2 對等項目取代 - tf.summarytf.keras.metricstf.keras.optimizers

tf.compat.v1:舊版和相容性 API 端點

tf.compattf.compat.v1 命名空間下的符號不視為 TF2 API。這些命名空間公開了相容性符號以及 TF 1.x 的舊版 API 端點的組合。這些旨在協助從 TF1.x 遷移至 TF2。不過,由於這些 compat.v1 API 都不是慣用的 TF2 API,因此請勿使用它們來編寫全新的 TF2 程式碼。

個別 tf.compat.v1 符號可能是與 TF2 相容的,因為即使在啟用 TF2 行為的情況下,它們仍可繼續運作 (例如 tf.compat.v1.losses.mean_squared_error),而其他符號則與 TF2 不相容 (例如 tf.compat.v1.metrics.accuracy)。許多 compat.v1 符號 (但並非全部) 在其文件中包含專用的遷移資訊,說明其與 TF2 行為的相容程度,以及如何將其遷移至 TF2 API。

TF2 升級指令碼 中,如果 compat.v1 API 符號是別名,或具有相同引數但順序不同,則可將許多此類符號對應至對等的 TF2 API。您也可以使用升級指令碼自動重新命名 TF1.x API。

假朋友 API

在 TF2 tf 命名空間 (而非 compat.v1 下) 中找到一組「假朋友」符號,這些符號實際上會忽略底層的 TF2 行為,和/或與整組 TF2 行為不完全相容。因此,這些 API 很可能在 TF2 程式碼中發生錯誤行為,而且可能是靜默錯誤。

  • tf.estimator.*:估算器會在底層建立並使用圖表和工作階段。因此,這些不應視為與 TF2 相容。如果您的程式碼正在執行估算器,則表示它未使用 TF2 行為。
  • keras.Model.model_to_estimator(...):這會在底層建立估算器,如上所述,估算器與 TF2 不相容。
  • tf.Graph().as_default():這會進入 TF1.x 圖表行為,且不遵循標準的 TF2 相容 tf.function 行為。像這樣進入圖表的程式碼通常會透過工作階段執行,且不應視為與 TF2 相容。
  • tf.feature_column.* 特徵欄 API 通常依賴 TF1 樣式的 tf.compat.v1.get_variable 變數建立,並假設建立的變數將透過全域集合存取。由於 TF2 不支援集合,因此在啟用 TF2 行為的情況下執行 API 時,API 可能無法正常運作。

其他 API 變更

  • TF2 在裝置放置演算法方面有顯著改進,這使得 tf.colocate_with 的使用變得不必要。如果移除它導致效能降低,請提交錯誤

  • 將所有 tf.v1.ConfigProto 用法替換為 tf.config 中的對等函式。

Eager Execution

TF1.x 要求您透過發出 tf.* API 呼叫來手動拼接抽象語法樹狀結構 (圖表),然後透過將一組輸出張量和輸入張量傳遞至 session.run 呼叫來手動編譯抽象語法樹狀結構。TF2 會以 Eager Execution 方式執行 (如同 Python 的一般執行方式),並讓圖表和工作階段感覺像是實作細節。

Eager Execution 的一個顯著副產品是不再需要 tf.control_dependencies,因為所有程式碼行都依序執行 (在 tf.function 內,具有副作用的程式碼會以撰寫順序執行)。

不再有全域變數

TF1.x 非常依賴隱含的全域命名空間和集合。當您呼叫 tf.Variable 時,它會被放入預設圖表中的集合中,並保留在那裡,即使您遺失指向它的 Python 變數的追蹤記錄。然後您可以復原該 tf.Variable,但前提是您知道它建立時使用的名稱。如果您無法控制變數的建立,這會很難做到。因此,各種機制激增,試圖協助您再次找到您的變數,並讓架構找到使用者建立的變數。其中一些包括:變數範圍、全域集合、協助程式方法 (例如 tf.get_global_steptf.global_variables_initializer)、最佳化工具隱含地計算所有可訓練變數的梯度,等等。TF2 淘汰了所有這些機制 (Variables 2.0 RFC),改用預設機制 - 您追蹤您的變數。如果您遺失 tf.Variable 的追蹤記錄,它就會被垃圾收集。

追蹤變數的要求會產生一些額外的工作,但使用 模型填充碼tf.Moduletf.keras.layers.Layer 中隱含的物件導向變數集合等工具和行為,可將負擔降到最低。

函式,而非工作階段

session.run 呼叫幾乎就像函式呼叫:您指定輸入和要呼叫的函式,然後您會取回一組輸出。在 TF2 中,您可以使用 tf.function 裝飾 Python 函式,以將其標記為 JIT 編譯,以便 TensorFlow 將其作為單一圖表執行 (Functions 2.0 RFC)。此機制可讓 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 (支援 breakcontinue)
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

AutoGraph 支援控制流程的任意巢狀結構,這使得高效且簡潔地實作許多複雜的 ML 程式 (例如序列模型、強化學習、自訂訓練迴圈等) 成為可能。

調整為 TF 2.x 行為變更

只有在您遷移到整組 TF2 行為後,您的 TF2 遷移才算完成。整組行為可以透過 tf.compat.v1.enable_v2_behaviorstf.compat.v1.disable_v2_behaviors 啟用或停用。以下章節將詳細討論每個主要的行為變更。

使用 tf.function

遷移期間程式的最大變更,可能來自從圖表和工作階段到 Eager Execution 和 tf.function 的基本程式設計模型範例轉變。請參閱 TF2 遷移指南,以深入瞭解如何從與 Eager Execution 和 tf.function 不相容的 API 移轉至與它們相容的 API。

以下是一些常見的程式模式 (未繫結至任何單一 API),這些模式可能會在從 tf.Graphtf.compat.v1.Session 轉換為搭配 tf.function 的 Eager Execution 時造成問題。

模式 1:預期只執行一次的 Python 物件操作和變數建立會多次執行

在依賴圖表和工作階段的 TF1.x 程式中,通常預期程式中的所有 Python 邏輯只會執行一次。不過,透過 Eager Execution 和 tf.function,可以合理預期您的 Python 邏輯至少會執行一次,但可能會執行更多次 (以 Eager Execution 方式多次執行,或跨不同的 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.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])
    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:由於 dict 用法而導致的非預期 Tensorflow 物件重新建立

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 邏輯簡單地對應到具有 Eager Execution 的 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 中預期在每個步驟中重新計算的符號張量,在切換到 Eager Execution 時意外地與初始值一起快取。

當在 tf.functions 外部以 Eager Execution 方式執行時,此模式通常會導致您的程式碼靜默地發生錯誤行為,但如果初始值快取發生在 tf.function 內部,則會引發 InaccessibleTensorError。不過請注意,為了避免上方的模式 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(...)

不過,當嘗試切換到 Eager Execution 時,請注意最終可能會僅計算一次學習率然後重複使用,而不是遵循預期的排程

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:當切換到 Eager Execution 時,指定為物件屬性然後透過指標重複使用的符號隨機數初始化會意外地快取

請考慮以下 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 外部以 eager 方式執行時,不會產生張量名稱,因此所有 tf.Tensor.name 的用法都必須在 tf.function 內部進行。請注意,即使在同一個 tf.function 內部,TF1.x 和 TF2 之間實際產生的名稱也很可能不同,且 API 保證不確保跨 TF 版本產生的名稱穩定性。

模式 4:TF1.x 工作階段選擇性地僅執行生成圖的一部分

在 TF1.x 中,您可以建構一個圖,然後透過選擇一組不需要執行圖中每個運算的輸入和輸出,使用工作階段選擇性地僅執行其子集。

例如,您可能在單一圖中同時擁有生成器和鑑別器,並使用個別的 tf.compat.v1.Session.run 呼叫,以在僅訓練鑑別器或僅訓練生成器之間交替。

在 TF2 中,由於 tf.function 中的自動控制依賴項和 eager 執行,因此不會選擇性地修剪 tf.function 追蹤。即使例如僅從 tf.function 輸出鑑別器或生成器的輸出,也將執行包含所有變數更新的完整圖。

因此,您需要使用包含程式不同部分的多個 tf.function,或使用 tf.function 的條件參數來進行分支,以便僅執行您實際想要執行的內容。

移除集合

當啟用 eager 執行時,與圖集合相關的 compat.v1 API(包括那些在底層讀取或寫入集合的 API,例如 tf.compat.v1.trainable_variables)將不再可用。某些可能會引發 ValueError,而其他可能會靜默地返回空列表。

在 TF1.x 中,集合最標準的用法是維護初始化器、全域步驟、權重、正規化損失、模型輸出損失,以及需要運行的變數更新,例如來自 BatchNormalization 層的更新。

為了處理這些標準用法中的每一個

  1. 初始化器 - 忽略。啟用 eager 執行時,不需要手動變數初始化。
  2. 全域步驟 - 請參閱 tf.compat.v1.train.get_or_create_global_step 的文件以取得遷移說明。
  3. 權重 - 依照模型對應指南中的指引,將您的模型對應到 tf.Module / tf.keras.layers.Layer / tf.keras.Model,然後使用它們各自的權重追蹤機制,例如 tf.module.trainable_variables
  4. 正規化損失 - 依照模型對應指南中的指引,將您的模型對應到 tf.Module / tf.keras.layers.Layer / tf.keras.Model,然後使用 tf.keras.losses。或者,您也可以手動追蹤您的正規化損失。
  5. 模型輸出損失 - 使用 tf.keras.Model 損失管理機制,或在不使用集合的情況下單獨追蹤您的損失。
  6. 權重更新 - 忽略此集合。Eager 執行和 tf.function(使用 autograph 和自動控制依賴項)表示所有變數更新都將自動運行。因此,您無需在最後明確運行所有權重更新,但請注意,這表示權重更新可能發生在與 TF1.x 程式碼中不同的時間,具體取決於您如何使用控制依賴項。
  7. 摘要 - 請參閱遷移摘要 API 指南

更複雜的集合用法(例如使用自訂集合)可能需要您重構程式碼,以維護您自己的全域儲存,或使其完全不依賴全域儲存。

使用 ResourceVariables 而非 ReferenceVariables

ResourceVariablesReferenceVariables 具有更強的讀寫一致性保證。這使得在使用變數時,是否會觀察到先前寫入的結果,其語義更可預測且更容易理解。此變更極不可能導致現有程式碼引發錯誤或靜默中斷。

然而,雖然不太可能,但這些更強的一致性保證可能會增加您特定程式的記憶體用量。如果您發現這種情況,請提交 issue。此外,如果您的單元測試依賴於針對圖中與變數讀取相對應的運算符名稱進行精確字串比較,請注意,啟用資源變數可能會稍微更改這些運算符的名稱。

為了隔離此行為變更對程式碼的影響,如果停用 eager 執行,您可以使用 tf.compat.v1.disable_resource_variables()tf.compat.v1.enable_resource_variables() 來全域停用或啟用此行為變更。如果啟用 eager 執行,則始終會使用 ResourceVariables

控制流 v2

在 TF1.x 中,控制流運算,例如 tf.condtf.while_loop,會內聯底層運算,例如 SwitchMerge 等。TF2 提供了改進的功能性控制流運算,這些運算使用每個分支的單獨 tf.function 追蹤來實現,並支援高階微分。

為了隔離此行為變更對程式碼的影響,如果停用 eager 執行,您可以使用 tf.compat.v1.disable_control_flow_v2()tf.compat.v1.enable_control_flow_v2() 來全域停用或啟用此行為變更。但是,只有在也停用 eager 執行時,才能停用控制流 v2。如果啟用 eager 執行,則始終會使用控制流 v2。

此行為變更可能會大幅改變使用控制流的生成 TF 程式的結構,因為它們將包含多個巢狀函數追蹤,而不是一個平面圖。因此,任何高度依賴產生追蹤的確切語義的程式碼都可能需要進行一些修改。這包括

  • 依賴運算符和張量名稱的程式碼
  • 從 TensorFlow 控制流分支外部引用在該分支內部建立的張量的程式碼。這很可能會產生 InaccessibleTensorError

此行為變更旨在實現性能中性到正面的效果,但是如果您遇到控制流 v2 的性能比 TF1.x 控制流更差的問題,請提交 issue 並附上重現步驟。

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

如果您在 TF1.x 中有這個

value = shape[i].value

那麼在 TF2 中執行這個

value = shape[i]
value

如果您在 TF1.x 中有這個

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

然後,在 TF2 中執行這個

for value in shape:
  print(value)

如果您在 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
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.

TensorShape 變更可能導致的錯誤

TensorShape 行為變更不太可能靜默中斷您的程式碼。但是,您可能會看到與形狀相關的程式碼開始引發 AttributeError,因為 intNone 沒有與 tf.compat.v1.Dimension 相同的屬性。以下是一些 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)
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)

依值比較張量相等性

TF2 中變數和張量的二元 ==!= 運算符已更改為依值比較,而不是像 TF1.x 中那樣依物件參考比較。此外,張量和變數不再可直接雜湊或在集合或字典鍵中使用,因為可能無法依值雜湊它們。相反,它們公開了一個 .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

而在啟用張量相等性檢查的 TF2 中,x == y 將返回 True

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

x == y

因此,在 TF2 中,如果您需要依物件參考進行比較,請確保使用 isis not

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

x is y

雜湊張量和變數

使用 TF1.x 行為,您可以直接將變數和張量新增到需要雜湊的資料結構中,例如 setdict 鍵。

x = tf.Variable(0.0)
set([x, tf.constant(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)

因此,在 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.deref() 從參考中取得張量或變數

referenced_var = x.ref().deref()
assert referenced_var is x
referenced_var

資源和延伸閱讀

  • 請造訪遷移至 TF2章節,以閱讀有關從 TF1.x 遷移至 TF2 的更多資訊。
  • 請閱讀模型對應指南,以了解更多關於將您的 TF1.x 模型對應到 TF2 中直接運作的資訊。