TensorFlow 1.x와 TensorFlow 2 - 동작 및 API

TensorFlow.org에서보기 Google Colab에서 실행하기 GitHub에서 소스 보기 노트북 다운로드하기

TensorFlow 2는 실제로는 TF1.x와 근본적으로 다른 프로그래밍 패러다임을 따릅니다.

이 가이드는 동작 및 API 측면에서 TF1.x와 TF2의 근본적인 차이점과 이러한 차이점이 마이그레이션 작업와 어떠한 관련이 있는지 설명합니다.

주요 변경 사항에 대한 고수준 요약

기본적으로 TF1.x와 TF2는 실행(TF2에서는 즉시 실행), 변수, 제어 흐름, 텐서 형상 및 텐서 동등성 비교 시 서로 다른 런타임 동작 세트를 사용합니다. TF2와 호환되려면 코드가 전체 TF2 동작 세트와 호환되어야 합니다. 마이그레이션하는 동안 tf.compat.v1.enable_* 또는 tf.compat.v1.disable_* API를 통해 이러한 동작 대부분을 개별적으로 사용하거나 사용 중지할 수 있습니다. 한 가지 예외는 즉시 실행 활성화/비활성화의 부작용인 모음 제거입니다.

고수준에서 TensorFlow 2는 다음과 같습니다.

  • 중복 API를 제거합니다.
  • 통합 RNN통합 옵티마이저와 같은 API의 일관성을 높입니다.
  • 세션보다 함수를 선호하고 그래프와 컴파일에 대한 자동 제어 종속성을 제공하는 tf.function과 함께 기본적으로 사용 설정된 Eager execution을 사용하여 Python 런타임과 더 잘 통합합니다.
  • 전역 그래프 모음의 사용을 중단합니다.
  • ReferenceVariables 대신 ResourceVariables를 사용하여 변수 동시성 의미 체계를 변경합니다.
  • 함수 기반 및 차별화 가능한 제어 흐름(제어 흐름 v2)을 지원합니다.
  • tf.compat.v1.Dimension 객체 대신 int를 보유하도록 TensorShape API를 간소화합니다.
  • 텐서 동등성 역학을 업데이트합니다. TF1.x에서 텐서 및 변수의 == 연산자는 객체 참조 동등성을 확인합니다. TF2에서는 값이 같은지 확인합니다. 또한 텐서/변수는 더 이상 해시 가능하지 않지만 세트에서 또는 dict 키로 사용해야 하는 경우 var.ref()를 통해 해시 가능한 개체 참조를 가져올 수 있습니다.

아래 섹션에서는 TF1.x와 TF2의 차이점에 대한 더 자세한 컨텍스트를 제공합니다. TF2 이면의 설계 프로세스에 대한 자세한 내용은 RFC설계 문서를 읽어보세요.

API 클린업

TF2에서 많은 API가 사라지거나 이동되었습니다. 주요 변경 사항으로는 tf.contrib에 있는 새 오픈 소스인 absl-py, 이동 프로젝트(rehoming projects)의 지원과, 덜 사용하는 함수를 tf.math와 같은 하위 패키지로 이동하여 기본 tf.* 네임스페이스를 정리하기 위한 tf.app, tf.flags, tf.logging의 제거 등이 있습니다. 일부 API는 tf.summary, tf.keras.metrics, tf.keras.optimizers 등의 TF2로 교체되었습니다.

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 동작(예: tf.compat.v1.losses.mean_squared_error)이 활성화된 상태에서도 계속 작동하기 때문에 TF2와 호환될 수 있습니다. 다른 항목은 TF2와 호환되지 않습니다(예: tf.compat.v1.metrics.accuracy). 전부는 아니지만 많은 compat.v1 기호에는 TF2 동작과의 호환성 정도와 TF2 API로 마이그레이션하는 방법을 설명하는 전용 마이그레이션 정보가 해당 문서에 포함되어 있습니다.

TF2 업그레이드 스크립트는 많은 compat.v1 API 기호를 해당 API 기호가 별칭이거나, 동일한 인수를 갖지만 순서가 다른 경우 동등한 TF2 API에 매핑할 수 있습니다. 업그레이드 스크립트를 사용하여 TF1.x API의 이름을 자동으로 변경할 수도 있습니다.

거짓 친구 API

TF2 tf 네임스페이스(compat.v1 아래가 아님)에는 TF2 내부 동작을 실제로 무시하는 "거짓 친구"(false-friend) 기호 세트가 있습니다. 이것은 전체 TF2 동작 세트와 완전히 호환되지 않을 수도 있습니다. 따라서 이러한 API는 잠재적으로 자동 방식으로 TF2 코드와 오작동할 가능성이 있습니다.

  • tf.estimator.*: Estimator는 내부적으로 그래프와 세션을 생성하고 사용합니다. 따라서 TF2와 호환되는 것으로 간주해서는 안 됩니다. 코드가 Estimator를 실행하는 경우 TF2 동작을 사용하고 있지 않는 것입니다.
  • keras.Model.model_to_estimator(...): 위에서 언급한 바와 같이 TF2와 호환되지 않는 Estimator를 내부에서 생성합니다.
  • 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 실행

TF1.x에서는 tf.* API를 호출하여 추상 구문 트리(그래프)를 수동으로 결합한 후에 session.run 호출에 출력 텐서와 입력 텐서 세트를 전달하여 추상 구문 트리를 수동으로 컴파일해야 했습니다. TF2는 즉시 실행되고(일반적인 Python 작업과 같이) 그래프와 세션이 구현의 세부 사항처럼 느껴지도록 합니다.

즉시 실행으로 인한 부수 효과 중 하나는 더 이상 tf.control_dependencies가 필요하지 않다는 것입니다. 모든 코드 라인이 순서대로 실행되기 때문입니다(tf.function 내에서 부수 효과가 있는 코드가 작성된 순서로 실행됨).

전역 개념 삭제

TF1.x는 암시적인 전역 네임스페이스 및 모음에 크게 의존했습니다. tf.Variable을 호출하면 기본 그래프의 모음에 배치되고, 이를 가리키는 Python 변수를 추적하지 못하더라도 그대로 유지됩니다. 그러면 해당 tf.Variable을 복구할 수 있지만, 이 작업은 생성된 이름을 아는 경우에만 가능합니다. 변수 생성을 제어하지 않을 때에는 이 작업을 수행하기 어려웠습니다. 이로 인해 변수를 다시 찾고 프레임워크가 사용자가 만든 변수를 찾는 데 도움이 되는 모든 종류의 메커니즘이 급증했습니다. 여기에는 변수 범위, 전역 모음, tf.get_global_steptf.global_variables_initializer와 같은 도우미 메서드, 훈련 가능한 모든 변수에 대해 암시적으로 그래디언트를 계산하는 옵티마이저 등이 포함됩니다. TF2는 기본 메커니즘을 선호하기에 이러한 메커니즘(변수 2.0 RFC)을 모두 제거하고 변수를 추적합니다. tf.Variable을 추적하지 못하면 가비지를 수집합니다.

변수를 추적해야 한다는 요구 사항으로 인해 약간의 추가 작업을 수행해야 할 수 있지만 모델링 shim과 같은 도구와 tf.Moduletf.keras.layers.Layer의 암시적 객체 지향 변수 모음과 같은 동작을 사용하면 부담이 최소화됩니다.

세션이 아닌 함수

session.run은 거의 함수 호출과 비슷합니다. 호출할 입력과 함수를 지정하면 일련의 출력을 얻습니다. TF2에서는 tf.function로 Python 함수를 데코레이팅할 수 있습니다. 이렇게 하면 TensorFlow가 이 함수를 단일 그래프(함수 2.0 RFC)로 실행할 수 있게 JIT 컴파일용으로 표시할 수 있습니다. 이러한 메커니즘 덕분에 TF2에서 그래프 모드의 장점을 모두 얻을 수 있습니다.

  • 성능: 함수를 최적화할 수 있습니다(노드 가지치기(pruning), 커널 융합(kernel fusion) 등)
  • 이식성: 함수를 내보내거나 다시 가져올 수 있으므로(SavedModel 2.0 RFC) 모듈식 TensorFlow 함수를 다시 사용하고 공유할 수 있습니다.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

Python과 TensorFlow 코드를 자유롭게 배치할 수 있는 기능을 통해 Python의 표현력을 활용할 수 있습니다. 단, 이식 가능한 TensorFlow는 모바일, C++ 및 JavaScript와 같은 Python 인터프리터 없이 컨텍스트에서 실행됩니다. tf.function을 추가할 때 코드를 다시 작성하지 않으려면 AutoGraph를 사용하여 Python 구성의 하위 집합을 TensorFlow에 상응하는 구성으로 변환하세요.

  • for/while -> tf.while_loop (breakcontinue 지원됨)
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

오토그래프는 임의의 중첩된 제어 흐름도 지원합니다. 시퀀스(sequence) 모델, 강화 학습(reinforcement learning), 독자적인 훈련 루프 등 복잡한 머신러닝 프로그램을 간결하면서 높은 성능을 내도록 구현할 수 있습니다.

TF 2.x 동작 변경 사항에 적응하기

TF2 로의 마이그레이션은 전체 TF2 동작 세트로 마이그레이션한 후에만 완료됩니다. 전체 동작 세트는 tf.compat.v1.enable_v2_behaviorstf.compat.v1.disable_v2_behaviors를 통해 활성화하거나 비활성화할 수 있습니다. 아래 섹션에서는 각각의 주요 동작 변경 사항에 대해 자세히 설명합니다.

tf.function 사용하기

마이그레이션하는 동안 프로그램에서 가장 큰 변화는 그래프 및 세션에서 즉시 실행 및 tf.function으로의 근본적인 프로그래밍 모델 패러다임 전환에서 비롯될 가능성이 높습니다. 즉시 실행 및 tf.function과 호환되지 않는 API에서 호환되는 API로 이동하는 방법에 대해 자세히 알아보려면 TF2 마이그레이션 가이드를 참조하세요.

참고: 마이그레이션하는 동안 tf.compat.v1.enable_eager_executiontf.compat.v1.disable_eager_execution을 사용하여 즉시 실행을 직접 활성화 및 비활성화하도록 선택할 수 있지만 이는 프로그램 수명 동안 한 번만 수행될 수 있습니다.

다음은 tf.Graphtf.compat.v1.Session에서 tf.function을 사용하는 즉시 실행으로 전환할 때 문제를 일으킬 수 있는 API에 연결되지 않은 몇 가지 일반적인 프로그램 패턴입니다.

패턴 1: 한 번만 수행되어야 하는 Python 객체 조작 및 변수 생성이 여러 번 실행됨

그래프와 세션에 의존하는 TF1.x 프로그램에서는 일반적으로 프로그램의 모든 Python 논리가 한 번만 실행될 것이라 예상합니다. 그러나 즉시 실행 및 tf.function을 사용하는 경우에는 Python 논리가 최소 한 번 이상 실행될 것이라고 예상하는 것이 타당합니다(즉시 여러 번 또는 서로 다른 tf.function에서 여러 번). 때로는 tf.function가 동일한 입력에서 두 번 추적하여 예기치 않은 동작을 유발하기도 합니다(예제 1 및 2 참조). 자세한 내용은 tf.function 가이드를 참조하세요.

참고: 이 패턴은 일반적으로 tf.function 없이 즉시 실행될 때 코드가 조용히 오작동되도록 하지만, 일반적으로 tf.function 내부에서 문제가 있는 코드를 래핑하려고 할 때 InaccessibleTensorError이나 ValueError를 발생시킵니다. 이 문제를 발견하고 디버깅하려면 초기에 tf.function으로 코드를 래핑하고 pdb 또는 대화형 디버깅을 사용하여 InaccessibleTensorError의 소스를 식별하는 것이 좋습니다.

예제 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 재추적으로 인한 범위 밖의 Tensor

예제 1에서 볼 수 있듯이 tf.function은 첫 번째 호출에서 변수 생성을 감지하면 재추적합니다. 두 개의 추적이 두 개의 그래프를 생성하기 때문에 추가 혼란이 발생할 수 있습니다. 재추적의 두 번째 그래프가 첫 번째 추적에서 생성한 그래프의 Tensor에 액세스하려고 시도하면 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 논리가 즉시 실행을 통해 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(즉시)로 전환하면 실수로 초기 값으로 캐시됩니다.

이 패턴은 일반적으로 tf.functions 외부에서 즉시 실행할 때 코드가 자동으로 오작동하게 하지만 초기 값 캐싱이 tf.function 내부에서 이루어지면 InaccessibleTensorError를 발생시킵니다. 그러나 위의 패턴 1을 피하기 위해 이 초기 값 캐싱을 tf.function의 외부에서 수행하여 오류를 발생시키는 방식으로 코드 구조를 실수로 구성하는 경우가 많습니다. 따라서 프로그램이 이 패턴에 취약할 수 있다는 것을 알고 있다면 각별히 주의하세요.

이 패턴에 대한 일반적인 해결책은 코드를 재구성하거나 필요한 경우 Python callables를 사용하여 값을 실수로 캐싱하는 대신 매번 다시 계산하도록 하는 것입니다.

예제 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로 전환하려고 할 때 다음의 의도한 일정을 따르지 않고 한 번만 학습률을 계산하고 재사용하는 결과가 발생하지 않도록 주의해야 합니다.

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 callable을 학습률 및 기타 초매개용 훈련 인수로 지원합니다.

예제 2: 객체 속성으로 할당된 다음 포인터를 통해 재사용되는 상징적 난수 초기화는 Eager로 전환할 때 실수로 캐싱됨

다음 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)
...

이 문제를 해결하려면 매번 동일한 텐서 객체를 참조하는 대신 새로운 무작위 텐서가 필요할 때마다 tf.random.normal을 호출하도록 Noise Adder를 리팩터링합니다.

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 내부에서 이루어져야 합니다. 실제 생성된 이름은 동일한 tf.function 내에서도 TF1.x와 TF2 간에 다를 가능성이 매우 높으며 API 보장은 TF 버전 간에 생성된 이름의 안정성을 보장하지 않습니다.

참고: 변수 이름은 tf.function 외부에서도 계속 생성되지만 모델 매핑 가이드의 관련 섹션을 따르는 경우를 제외하고 TF1.x와 TF2 간의 이름 일치를 보장하지 않습니다.

패턴 4: 생성된 그래프의 일부만 선택적으로 실행하는 TF1.x 세션

TF1.x에서는 그래프를 구성한 다음 그래프의 모든 연산을 실행할 필요가 없는 입력 및 출력 세트를 선택하여 세션으로 그래프의 하위 세트만 선택적으로 실행하도록 선택할 수 있습니다.

예를 들어, 단일 그래프 내부에 생성기와 판별기가 모두 있을 수 있으며 별도의 tf.compat.v1.Session.run 호출을 사용하여 판별기만 교육하거나 생성자만 훈련할 수 있습니다.

TF2에서는 즉시 실행과 tf.function의 자동 제어 종속성으로 인해 tf.function 추적의 선택적 프루닝이 없습니다. 모든 변수 업데이트를 포함하는 전체 그래프는 예를 들어 판별기 또는 생성기의 출력만 tf.function에서 출력되는 경우에도 실행됩니다.

따라서 프로그램의 다른 부분을 포함하는 여러 tf.function을 사용하거나 실제로 실행하고 싶은 것만 실행하기 위해 분기하는 tf.function에 대한 조건부 인수를 사용해야 합니다.

모음 제거

즉시 실행이 활성화되면 그래프 모음 관련 compat.v1 API(tf.compat.v1.trainable_variables와 같이 내부에서 모음을 읽거나 쓰는 API 포함) API를 더 이상 사용할 수 없습니다. 일부는 ValueError를 발생시킬 수 있고 다른 일부는 자동으로 빈 목록을 반환할 수 있습니다.

TF1.x에서 모음의 가장 표준적인 사용법은 BatchNormalization 레이어와 같이 실행해야 하는 이니셜라이저, 전역 단계, 가중치, 정규화 손실, 모델 출력 손실 및 변수 업데이트를 유지하는 것입니다.

이러한 각 표준 사용법을 처리하려면 다음을 수행합니다.

  1. 이니셜라이저 - 무시합니다. 즉시 실행이 활성화되었을 때에는 수동 변수 이니셜라이저가 필요하지 않습니다.
  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. 가중치 업데이트 - 이 모음은 무시합니다. 즉시 실행 및 tf.function(AutoGraph 및 자동 제어 종속성 포함)은 모든 변수 업데이트가 자동으로 실행됨을 의미합니다. 이는 마지막에 모든 가중치 업데이트를 명시적으로 실행할 필요는 없지만 제어 종속성을 사용하는 방법에 따라 가중치 업데이트가 TF1.x 코드에서 수행한 것과 다른 시간에 발생할 수 있음을 의미합니다.
  7. 요약 - 마이그레이션 요약 API 가이드를 참조합니다.

더 복잡한 모음 사용(예: 사용자 정의 모음 사용)에서는 자체 전역 스토어를 유지하거나 전역 스토어에 전혀 의존하지 않도록 코드를 리팩토링해야 할 수도 있습니다.

ReferenceVariables를 대신하는 ResourceVariables

ResourceVariablesReferenceVariables보다 더 강력한 읽기-쓰기 일관성을 보장합니다. 이것은 변수를 사용할 때 이전 쓰기의 결과를 관찰할지 여부에 대해 더 예측 가능하고 추론하기 쉬운 의미 체계로 이어집니다. 이 변경으로 인해 기존 코드에서 오류가 발생하거나 자동으로 중단될 가능성은 거의 없습니다.

그러나 이러한 강력한 일관성 보장으로 인해 특정 프로그램의 메모리 사용량이 증가할 가능성은 거의 없을 것입니다. 이러한 예외 상황을 발견한 경우에는 해당 이슈를 제출해주세요. 또한, 변수 읽기에 해당하는 그래프의 연산자 이름에 대한 정확한 문자열 비교에 의존하는 단위 테스트가 있는 경우 리소스 변수를 활성화하면 이러한 연산자의 이름이 약간 변경될 수 있습니다.

이 동작 변경이 코드에 영향을 미치지 않도록 하려면, 즉시 실행이 비활성화된 경우 tf.compat.v1.disable_resource_variables()tf.compat.v1.enable_resource_variables()를 사용하여 전체적으로 비활성화하거나 이 동작 변경을 활성화할 수 있습니다. ResourceVariables는 즉시 실행이 활성화된 경우 항상 사용됩니다.

제어 흐름 v2

TF1.x에서 tf.condtf.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 클래스는 tf.compat.v1.Dimension 객체 대신 int를 보유하도록 단순화되었습니다. 따라서 int를 얻기 위해 .value를 호출할 필요가 없습니다.

개별 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
2022-12-14 20:44:36.791157: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 20:44:36.791254: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 20:44:36.791263: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
# 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 동작 변경으로 인해 코드가 자동으로 중단되지는 않습니다. 그러나 형상 관련 코드가 intNonetf.compat.v1.Dimension과 동일한 속성이 없기 때문에 AttributeError를 발생시키기 시작하는 것을 볼 수 있습니다. 다음은 이러한 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'

값별 텐서 동등성

변수 및 텐서에 대한 바이너리 ==!= 연산자는 TF1.x에서처럼 객체 참조로 비교하는 대신 TF2에서 값으로 비교하도록 변경되었습니다. 또한 텐서와 변수는 값으로 해시하는 것이 불가능할 수 있으므로 더 이상 세트 또는 사전 키에서 직접 해시하거나 사용할 수 없습니다. 대신, 텐서 또는 변수에 대한 해시 가능한 참조를 가져오는 데 사용할 수 있는 .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 == yTrue를 반환합니다.

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에서 객체 참조로 비교해야 하는 경우 isis not을 사용해야 합니다.

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

x is y
False

해싱 텐서 및 변수

TF1.x 동작을 사용하는 경우 setdict 키와 같이 해싱이 필요한 데이터 구조에 변수와 텐서를 직접 추가할 수 있었습니다.

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 variable.ref() as the key. (Variable: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>)

따라서 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.Tensor: shape=(), dtype=float32, numpy=2.0>>,
 <Reference wrapping <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.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>

리소스 및 추가 읽을거리

  • TF1.x에서 TF2로 마이그레이션하는 방법에 대한 자세한 내용은 TF2로 마이그레이션하기 섹션을 참조하세요.
  • TF2에서 직접 작동하도록 TF1.x 모델을 매핑하는 방법에 대한 자세한 내용은 모델 매핑 가이드를 읽어보세요.