Structuring Your TensorFlow Models
📅 April 20, 2018
•⏱️4 min read
이 글은 저자의 허락을 받아 번역한 글 입니다. 원문 링크
TensorFlow에서 모델을 정의하다보면 어느새 많은 양의 코드가 생성된 경험이 있을 것 입니다. 어떻게 하면 가독성과 재사용성이 높은 코드로 구성할 수 있을까요? 여기에서 실제 동작하는 예시 코드를 확인하실 수 있습니다. Gist Link
Defining the Compute Graph
모델 당 하나의 클래스부터 시작하는 것이 좋습니다. 그 클래스의 인터페이스는 무엇인가요? 일반적으로 모델은 input data와 target placeholder를 연결하며 training, evaluation 그리고 inference 관련 함수를 제공합니다.
위의 코드가 기본적으로 TensorFlow codebase에서 모델이 정의되는 방식입니다. 그러나 여기에도 몇 가지 문제가 있습니다. 가장 중요한 문제는 전체 그래프가 단일 함수 생성자로 정의된다는 점 입니다. 이렇게 되면 가독성이 떨어지며 재사용이 어렵습니다.
Using Properties
함수가 호출 될 때마다 그래프는 확장되기 때문에 함수로 분할하는 것만으로는 부족합니다. 따라서 함수를 처음 호출하는 시점에 operation들이 그래프에 추가되도록 해야합니다. 이러한 방식을 기본적으로 lazy-loading이라고 합니다.
class Model:
def __init__(self, data, target):
data_size = int(data.get_shape()[1])
target_size = int(target.get_shape()[1])
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(data, weight) + bias
self._prediction = tf.nn.softmax(incoming)
cross_entropy = -tf.reduce_sum(target, tf.log(self._prediction))
self._optimize = tf.train.RMSPropOptimizer(0.03).minimize(cross_entropy)
mistakes = tf.not_equal(
tf.argmax(target, 1), tf.argmax(self._prediction, 1))
self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))
@property
def prediction(self):
return self._prediction
@property
def optimize(self):
return self._optimize
@property
def error(self):
return self._error
위의 방식이 첫 번째 예제보다 훨씬 좋습니다. 이제 코드는 독립적인 함수로 구성되어 있습니다. 그러나 아직 코드는 lazy-loading으로 인해 약간 복잡해보입니다. 이를 어떻게 개선 할 수 있는지 보도록 하겠습니다.
Lazy Property Decorator
파이썬은 아주 유연한 언어입니다. 이제 마지막 예제에서 중복 코드를 제거하는 방법을 보여드리겠습니다. 우리는 @property
처럼 동작하지만 한번만 함수를 평가하는 decorator를 사용할 것입니다. decorator는 함수(접두사를 앞에 붙임)의 이름을 따서 멤버에 결과를 저장하고 나중에 호출되는 시점에 해당 값을 반환합니다. custom decorator를 아직 사용해본적이 없다면, 이 가이드를 참고하시면 됩니다.
class Model:
def __init__(self, data, target):
self.data = data
self.target = target
self._prediction = None
self._optimize = None
self._error = None
@property
def prediction(self):
if not self._prediction:
data_size = int(self.data.get_shape()[1])
target_size = int(self.target.get_shape()[1])
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(self.data, weight) + bias
self._prediction = tf.nn.softmax(incoming)
return self._prediction
@property
def optimize(self):
if not self._optimize:
cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))
optimizer = tf.train.RMSPropOptimizer(0.03)
self._optimize = optimizer.minimize(cross_entropy)
return self._optimize
@property
def error(self):
if not self._error:
mistakes = tf.not_equal(
tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))
self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))
return self._error
위의 decorator를 사용해서 예시 코드는 아래와 같이 간결해졌습니다.
import functools
def lazy_property(function):
attribute = '_cache_' + function.__name__
@property
@functools.wraps(function)
def decorator(self):
if not hasattr(self, attribute):
setattr(self, attribute, function(self))
return getattr(self, attribute)
return decorator
생성자에서 property를 언급했다는 부분이 중요합니다. 이렇게 구성한다면 tf.initialize_variables()
를 실행할 때 전체 그래프가 정의됩니다.
Organizing the Graph with Scopes
이제 코드에서 모델을 정의하는 부분은 깔끔해졌지만, 그래프의 연산 부분은 여전히 복잡합니다. 만일 그래프를 시각화한다면, 서로 연결되어 있는 노드가 많이 나타날 것 입니다. 이를 해결하기 위한 방법은 tf.name_scope('name')
또는 tf.variable_scope('name')
을 사용하여 각 함수의 내용을 래핑하는 것 입니다. 이렇게 하면 노드들은 그래프 상에서 그룹화되어 있을 것 입니다. 우리는 이전에 만들었던 decorator를 이용하여 이를 자동으로 적용시켜보겠습니다.
class Model:
def __init__(self, data, target):
self.data = data
self.target = target
self.prediction
self.optimize
self.error
@lazy_property
def prediction(self):
data_size = int(self.data.get_shape()[1])
target_size = int(self.target.get_shape()[1])
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(self.data, weight) + bias
return tf.nn.softmax(incoming)
@lazy_property
def optimize(self):
cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))
optimizer = tf.train.RMSPropOptimizer(0.03)
return optimizer.minimize(cross_entropy)
@lazy_property
def error(self):
mistakes = tf.not_equal(
tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))
return tf.reduce_mean(tf.cast(mistakes, tf.float32))
lazy caching 이외에도 TensorFlow의 기능을 포함시키므로 decorator에 새로운 이름을 지정했습니다. 그 외의 나머지 부분은 이전과 동일합니다.
이제 @define_scope
decorator를 통해 tf.variable_scope()
에 인자를 전달할 수 있습니다. 예를 들어 해당 scope에 default initializer를 정의할 수 있습니다. 이 부분이 더 궁금하다면 전체 예제 코드를 확인해보시면 됩니다.
Reference
https://danijar.com/structuring-your-tensorflow-models/