In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
|
|
|
Note: 이 문서는 텐서플로 커뮤니티에서 번역했습니다. 커뮤니티 번역 활동의 특성상 정확한 번역과 최신 내용을 반영하기 위해 노력함에도 불구하고 공식 영문 문서의 내용과 일치하지 않을 수 있습니다. 이 번역에 개선할 부분이 있다면 tensorflow/docs-l10n 깃헙 저장소로 풀 리퀘스트를 보내주시기 바랍니다. 문서 번역이나 리뷰에 참여하려면 docs-ko@tensorflow.org로 메일을 보내주시기 바랍니다.
텐서플로 2에서는 즉시 실행(eager execution)이 기본적으로 활성화되어 있습니다. 직관적이고 유연한 사용자 인터페이스를 제공하지만 성능과 배포에 비용이 더 듭니다(하나의 연산을 실행할 때는 훨씬 간단하고 빠릅니다).
성능을 높이고 이식성이 좋은 모델을 만들려면 tf.function
을 사용해 그래프로 변환하세요.
하지만 조심해야 할 점이 있습니다. tf.function
은 무조건 속도를 높여주는 마법의 은총알이 아닙니다!
이 가이드는 tf.function
의 이면에 있는 개념을 이해하고 사용법을 완전히 터득할 수 있도록 도울 것입니다.
여기서 배울 주요 내용과 권고 사항은 다음과 같습니다:
@tf.function
으로 데코레이팅하세요.tf.function
은 텐서플로 연산과 가장 잘 동작합니다: 넘파이와 파이썬 호출은 상수로 바뀝니다.
In [ ]:
import tensorflow as tf
에러 출력을 위한 헬퍼 함수를 정의합니다:
In [ ]:
import traceback
import contextlib
# 에러 출력을 위한 헬퍼 함수
@contextlib.contextmanager
def assert_raises(error_class):
try:
yield
except error_class as e:
print('기대하는 예외 발생 \n {}:'.format(error_class))
traceback.print_exc(limit=2)
except Exception as e:
raise e
else:
raise Exception('{}를 기대했지만 아무런 에러도 발생되지 않았습니다!'.format(
error_class))
tf.function
으로 정의한 함수는 기본 텐서플로 연산과 같습니다. 즉시 실행 모드로 실행하거나 그레이디언트를 계산할 수 있습니다.
In [ ]:
@tf.function
def add(a, b):
return a + b
add(tf.ones([2, 2]), tf.ones([2, 2])) # [[2., 2.], [2., 2.]]
In [ ]:
v = tf.Variable(1.0)
with tf.GradientTape() as tape:
result = add(v, 1.0)
tape.gradient(result, v)
다른 함수 내부에 사용할 수 있습니다.
In [ ]:
@tf.function
def dense_layer(x, w, b):
return add(tf.matmul(x, w), b)
dense_layer(tf.ones([3, 2]), tf.ones([2, 2]), tf.ones([2]))
tf.function
은 즉시 실행 모드 보다 빠릅니다. 특히 그래프에 작은 연산이 많을 때 그렇습니다. 하지만 (합성곱처럼) 계산량이 많은 연산 몇 개로 이루어진 그래프는 속도 향상이 크지 않습니다.
In [ ]:
import timeit
conv_layer = tf.keras.layers.Conv2D(100, 3)
@tf.function
def conv_fn(image):
return conv_layer(image)
image = tf.zeros([1, 200, 200, 100])
# 워밍 업
conv_layer(image); conv_fn(image)
print("즉시 실행 합성곱:", timeit.timeit(lambda: conv_layer(image), number=10))
print("tf.function 합성곱:", timeit.timeit(lambda: conv_fn(image), number=10))
print("합성곱 연산 속도에 큰 차이가 없습니다.")
일반적으로 tf.function
보다 즉시 실행 모드가 디버깅하기 쉽습니다.
tf.function
으로 데코레이팅하기 전에 즉시 실행 모드에서 에러가 없는지 확인하세요.
디버깅 과정을 위해 tf.config.run_functions_eagerly(True)
으로 전체 tf.function
을 비활성화하고 나중에 다시 활성화할 수 있습니다.
tf.function
함수에서 버그를 추적할 때 다음 팁을 참고하세요:
print
함수는 트레이싱(tracing)하는 동안에만 호출되므로 함수가 (재)트레이싱될 때 추적하는데 도움이 됩니다.tf.print
함수는 언제나 실행되므로 실행하는 동안 중간 값을 추적할 때 도움이 됩니다.tf.debugging.enable_check_numerics
을 사용하면 쉽게 NaN과 Inf가 발생되는 곳을 추적할 수 있습니다.pdb
는 어떻게 트레이싱이 일어나는지 이해하는데 도움이 됩니다(주의: pdb
는 오토그래프(AutoGraph)가 변환한 소스 코드를 보여줄 것입니다).
In [ ]:
# 함수와 다형성
@tf.function
def double(a):
print("트레이싱:", a)
return a + a
print(double(tf.constant(1)))
print()
print(double(tf.constant(1.1)))
print()
print(double(tf.constant("a")))
print()
트레이싱 동작을 제어하기 위해 다음 기법을 사용할 수 있습니다:
새로운 tf.function
을 만듭니다. 별도의 tf.function
객체는 트레이싱이 따로 일어납니다.
In [ ]:
def f():
print('트레이싱!')
tf.print('실행')
tf.function(f)()
tf.function(f)()
get_concrete_function
메서드를 사용해 트레이싱된 특정 함수를 얻을 수 있습니다.
In [ ]:
print("콘크리트 함수 얻기")
double_strings = double.get_concrete_function(tf.TensorSpec(shape=None, dtype=tf.string))
print("트레이싱된 함수 실행")
print(double_strings(tf.constant("a")))
print(double_strings(a=tf.constant("b")))
print("콘크리트 함수에 다른 타입을 사용하면 예외가 발생합니다")
with assert_raises(tf.errors.InvalidArgumentError):
double_strings(tf.constant(1))
tf.function
에 input_signature
를 지정하여 트레이싱을 제한할 수도 있습니다.
In [ ]:
@tf.function(input_signature=(tf.TensorSpec(shape=[None], dtype=tf.int32),))
def next_collatz(x):
print("트레이싱", x)
return tf.where(x % 2 == 0, x // 2, 3 * x + 1)
print(next_collatz(tf.constant([1, 2])))
# input_signature에 1-D 텐서를 지정했기 때문에 다음은 실패합니다.
with assert_raises(ValueError):
next_collatz(tf.constant([[1, 2], [3, 4]]))
다형성을 지원하는 tf.function
은 트레이싱으로 생성된 콘크리트 함수를 캐싱합니다.
이 캐시의 키는 함수의 위치 매개변수(args)와 키워드 매개변수(kwargs)에서 생성된 키의 튜플입니다.
tf.Tensor
매개변수를 위해 생성된 키는 차원 개수와 타입이 됩니다.
파이썬 기본 자료형(정수, 실수, 문자열, 불리언)으로 생성된 키는 해당 변수의 값이 됩니다.
그외 다른 파이썬 타입에서 키는 id()
를 기반으로 합니다.
따라서 클래스 메서드는 인스턴스마다 독립적으로 트레이싱됩니다.
향후 텐서플로는 파이썬 객체를 안전하게 텐서로 변환하기 위한 고급 캐싱 기능을 제공할 수 있습니다.
콘크리트 함수를 참고하세요.
하이퍼파라미터 조작하고 그래프를 구성하기 위해 파이썬 매개변수가 자주 사용됩니다.
예를 들면 num_layers=10
이나 training=True
, nonlinearity='relu'
입니다.
파이썬 매개변수가 바뀌면 그래프가 다시 트레이싱됩니다.
하지만 파이썬 매개변수가 그래프 구성에 사용되지 않을 수 있습니다. 이런 경우 파이썬 값이 변하면 불필요한 재트레이싱을 일으킵니다. 예를 들어 다음은 오토그래프가 동적으로 펼치는 훈련 반복 루프입니다. 다중 트레이싱이 되었지만 생성된 그래프는 실제로 동일하기 때문에 조금 비효율적입니다.
In [ ]:
def train_one_step():
pass
@tf.function
def train(num_steps):
print("트레이싱 num_steps = {}".format(num_steps))
for _ in tf.range(num_steps):
train_one_step()
train(num_steps=10)
train(num_steps=20)
이를 해결하는 간단한 방법은 생성된 그래프에 영향을 미치지 않도록 매개변수를 Tensor
로 바꾸는 것입니다.
In [ ]:
train(num_steps=tf.constant(10))
train(num_steps=tf.constant(20))
tf.function
의 부수 효과일반적으로 (출력이나 객체 변경 같은) 파이썬 부수 효과(side effect)는 트레이싱 동안에만 일어납니다.
어떻게 tf.function
에서 안정적으로 부수 효과를 일으킬 수 있을까요?
일반적인 규칙은 파이썬 부수 효과만을 사용하여 트레이싱을 디버깅하는 것입니다.
그외에는 tf.Variable.assign
, tf.print
, tf.summary
같은 텐서플로 연산이 텐서플로 런타임에 의해 코드가 트레이싱되고 실행되는지 확인하는 가장 좋은 방법입니다.
일반적으로 함수 스타일을 사용하는 것이 가장 좋습니다.
In [ ]:
@tf.function
def f(x):
print("트레이싱", x)
tf.print("실행", x)
f(1)
f(1)
f(2)
tf.function
을 호출할 때마다 파이썬 코드를 실행하려면 tf.py_function
이 해결책입니다.
tf.py_function
의 단점은 이식성과 성능이 좋지 않고 분산 환경(다중 GPU나 다중 TPU)에서 잘 동작하지 않는다는 것입니다.
또한 tf.py_function
은 미분 가능하도록 그래프를 만들기 때문에 모든 입력/출력을 텐서로 변환합니다.
In [ ]:
external_list = []
def side_effect(x):
print('파이썬 부수 효과')
external_list.append(x)
@tf.function
def f(x):
tf.py_function(side_effect, inp=[x], Tout=[])
f(1)
f(1)
f(1)
assert len(external_list) == 3
# py_function이 1을 tf.constant(1)로 바꾸기 때문에 .numpy()를 호출해야 합니다.
assert external_list[0].numpy() == 1
In [ ]:
external_var = tf.Variable(0)
@tf.function
def buggy_consume_next(iterator):
external_var.assign_add(next(iterator))
tf.print("external_var의 값:", external_var)
iterator = iter([0, 1, 2, 3])
buggy_consume_next(iterator)
# 다음은 반복자의 다음 값을 추출하지 않고 첫 번째 값을 재사용합니다.
buggy_consume_next(iterator)
buggy_consume_next(iterator)
In [ ]:
@tf.function
def f(x):
v = tf.Variable(1.0)
v.assign_add(x)
return v
with assert_raises(ValueError):
f(1.0)
하지만 모호하지 않은 코드는 괜찮습니다.
In [ ]:
v = tf.Variable(1.0)
@tf.function
def f(x):
return v.assign_add(x)
print(f(1.0)) # 2.0
print(f(2.0)) # 4.0
함수가 처음 호출될 때만 변수가 생성되도록 tf.function
안에서 변수를 생성할 수 있습니다.
In [ ]:
class C:
pass
obj = C()
obj.v = None
@tf.function
def g(x):
if obj.v is None:
obj.v = tf.Variable(1.0)
return obj.v.assign_add(x)
print(g(1.0)) # 2.0
print(g(2.0)) # 4.0
변수 초기화가 함수 매개변수와 다른 변수 값에 의존할 수 있습니다. 올바른 초기화 순서를 찾기 위해 제어 의존성을 생성하는 메서드를 사용할 수 있습니다.
In [ ]:
state = []
@tf.function
def fn(x):
if not state:
state.append(tf.Variable(2.0 * x))
state.append(tf.Variable(state[0] * 3.0))
return state[0] * x * state[1]
print(fn(tf.constant(1.0)))
print(fn(tf.constant(3.0)))
In [ ]:
# 간단한 루프
@tf.function
def f(x):
while tf.reduce_sum(x) > 1:
tf.print(x)
x = tf.tanh(x)
return x
f(tf.random.uniform([5]))
관심있다면 오토그래프가 생성한 코드를 확인해 볼 수 있습니다.
In [ ]:
print(tf.autograph.to_code(f.python_function))
오토그래프는 if <condition>
문장을 이와 대등한 tf.cond
호출로 변경합니다.
이런 대체는 <condition>
이 텐서일 때 수행됩니다.
그렇지 않다면 if
문장은 파이썬 조건문으로 실행됩니다.
트레이싱하는 동안 파이썬 조건문을 실행하기 때문에 정확히 하나의 조건 분기만 그래프에 추가됩니다. 오토그래프가 없다면 이렇게 트레이싱된 그래프는 데이터에 따라 제어 흐름을 바꿀 수 없습니다.
tf.cond
는 조건 분기를 트레이싱하고 그래프에 추가하여 실행시 동적으로 분기를 선택합니다. 트레이싱때문에 의도치 않은 부수 효과가 발생될 수 있습니다. 더 자세한 내용은 오토그래프 트레이싱 효과를 참고하세요.
In [ ]:
@tf.function
def fizzbuzz(n):
for i in tf.range(1, n + 1):
print('루프 트레이싱')
if i % 15 == 0:
print('fizzbuzz 브랜치 트레이싱')
tf.print('fizzbuzz')
elif i % 3 == 0:
print('fizz 브랜치 트레이싱')
tf.print('fizz')
elif i % 5 == 0:
print('buzz 브랜치 트레이싱')
tf.print('buzz')
else:
print('디폴트 브랜치 트레이싱')
tf.print(i)
fizzbuzz(tf.constant(5))
fizzbuzz(tf.constant(20))
오토그래프가 변환한 if 문장에 대한 추가 제약 사항에 대해서는 레퍼런스 문서를 참고하세요.
오토그래프는 일부 for
와 while
문장을 tf.while_loop
와 같은 동등한 텐서플로 반복 연산으로 바꿉니다.
변환되지 않으면 파이썬 반복문으로 for
와 while
반복문이 실행됩니다.
이런 대체는 다음과 같은 경우에 일어납니다:
for x in y
: y
가 텐서이면 tf.while_loop
로 변환됩니다. 특별히 y
가 tf.data.Dataset
인 경우에는 tf.data.Dataset
연산의 조합이 생성됩니다.while <condition>
: <condition>
이 텐서라면 tf.while_loop
로 변환됩니다.파이썬 반복문이 트레이싱 동안 실행되므로 매 반복마다 tf.Graph
에 추가적인 연산이 포함됩니다.
텐서플로는 반복문 블럭을 트레이싱하여 실행시 얼마나 많은 반복이 수행될지 동적으로 선택합니다. 반복문 블럭은 생성된 tf.Graph
에 한 번만 포함됩니다.
오토그래프가 변환한 for
와 while
문장에 대한 추가 제약 사항에 대해서는 레퍼런스 문서를 참고하세요.
In [ ]:
def measure_graph_size(f, *args):
g = f.get_concrete_function(*args).graph
print("{}({})는 그래프에 {}개의 노드를 포함합니다".format(
f.__name__, ', '.join(map(str, args)), len(g.as_graph_def().node)))
@tf.function
def train(dataset):
loss = tf.constant(0)
for x, y in dataset:
loss += tf.abs(y - x) # 의미없는 연산
return loss
small_data = [(1, 1)] * 3
big_data = [(1, 1)] * 10
measure_graph_size(train, small_data)
measure_graph_size(train, big_data)
measure_graph_size(train, tf.data.Dataset.from_generator(
lambda: small_data, (tf.int32, tf.int32)))
measure_graph_size(train, tf.data.Dataset.from_generator(
lambda: big_data, (tf.int32, tf.int32)))
데이터셋으로 파이썬/넘파이 데이터를 감쌀 때 tf.data.Dataset.from_generator
와 tf.data.Dataset.from_tensors
의 차이를 주의하세요.
전자는 파이썬에서 데이터를 유지하고 tf.py_function
으로 데이터를 가져오므로 성능에 영향을 미칠 수 있습니다.
후자는 그래프에 있는 하나의 큰 tf.constant()
노드로 데이터를 복사하므로 메모리에 영향을 미칠 수 있습니다.
TFRecordDataset
, CsvDataset
등으로 파일에서 데이터를 읽는 것이 가장 효율적으로 데이터를 소비하는 방법입니다. 텐서플로는 파이썬을 거치지 않고 비동기적으로 데이터를 적재하고 프리페칭할 수 있기 때문입니다. 조금 더 자세한 정보는 tf.data guide를 참고하세요.
In [ ]:
batch_size = 2
seq_len = 3
feature_size = 4
def rnn_step(inp, state):
return inp + state
@tf.function
def dynamic_rnn(rnn_step, input_data, initial_state):
# [batch, time, features] -> [time, batch, features]
input_data = tf.transpose(input_data, [1, 0, 2])
max_seq_len = input_data.shape[0]
states = tf.TensorArray(tf.float32, size=max_seq_len)
state = initial_state
for i in tf.range(max_seq_len):
state = rnn_step(input_data[i], state)
states = states.write(i, state)
return tf.transpose(states.stack(), [1, 0, 2])
dynamic_rnn(rnn_step,
tf.random.uniform([batch_size, seq_len, feature_size]),
tf.zeros([batch_size, feature_size]))
tf.function
을 트레이싱한 후 수행되는 그래프 최적화에 자세히 알고 싶다면 그래플러(Grappler) 가이드를 참고하세요. 데이터 파이프라인을 최적화하고 모델 프로파일링 방법에 대해 알고 싶다면 프로파일러(Profiler) 가이드를 참고하세요.