Skip to content

Structuring Your TensorFlow Models

  • DataScience

📅 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/


← PrevNext →
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow