Define-and-run vs Define-by-run#

<밑바닥부터 시작하는 딥러닝3>을 읽다가 내용이 너무 좋아서 정리했습니다.

딥러닝 프레임워크, 더 일반적으로는 자동 미분 (auto-differentiation) 프레임워크는 크게 2가지 방식으로 구현되어 있다. 이 두 가지 방식은 계산 그래프 (computational graph)가 정의되는 시점으로 구분된다. 첫 번째 define-and-run은 계산 그래프가 먼저 정의된 상태에서 데이터가 흘러들어간다. 반면, define-by-run은 데이터가 흘러가면서 계산 그래프가 정의된다.



Define-and-run#

Define-and-run은 정적 계산 그래프 방식이라고도 부른다. 가장 대표적인 정적 계산 그래프 방식으로 텐서플로우가 있다. 정적 계산 그래프 방식에서는 먼저 사용자가 계산 그래프를 정의해줘야 한다. 그래프를 만드는 동안 실제 데이터 (수치)가 아닌 기호를 사용하여 코딩을 해야 하는데, 이와 같은 방식을 기호 프로그래밍 (symbolic programming)이라고 한다. 가상의 수도 코드는 다음과 같다 (밑바닥부터 시작하는 딥러닝3에서 발췌).

# 계산 그래프 정의
a = Variable('a')
b = Variable('b')
c = a * b
d = c + Constant(1)

# 계산 그래프 컴파일
f = compile(d)

# 데이터 흘려보내기
d = f(a=np.array(2), b=np.array(3))

위의 # 계산 그래프 정의 단계를 보면, 수치와 수치 사이의 연산이 아니라 ab 변수를 사용하여 기호와 기호 사이의 연산으로 구현되어 있다. 프레임워크 자체의 규칙을 따라서 계산 그래프를 만들어야하기 때문에 규칙에 익숙해지기까지 오랜 시간이 필요하다. 예를 들어, 조건문을 필요로 하는 계산 그래프를 만들 때 프로그래밍 언어의 if문을 사용하는 것이 아니라 프레임워크 자체의 함수를 사용해야 한다. 텐서플로우 같은 경우 tf.cond 함수가 이에 해당한다.


정적 계산 그래프 방식의 장점으로 계산 그래프를 직접 만들기 때문에 연산 과정을 최적화할 수 있는 여지가 있다는 것이다. 즉, 연산 성능이 더 좋을 수 있다. 또한 만든 모델을 서빙 (배포)하는 것이 용이하다는 장점도 있다. 예를 들어, 파이썬에서 텐서플로우로 계산 그래프를 정의했다고 하자. 이 계산 그래프를 다른 기기/언어에 호환가능하도록 컴파일할 수 있을 것이다.


정적 계산 그래프 방식은 디버깅이 어렵다는 단점이 있다. 버그는 데이터를 흘려보낼 때 발견되지만 문제의 원인은 주로 “계산 그래프 정의”에 있는 경우가 대부분이기 때문이다. 위에서 언급했듯이 프레임워크만의 규칙을 공부해야 한다는 것도 큰 단점이다.



Define-by-run#

Define-by-run은 동적 계산 그래프 방식이라고도 한다. 2015년 Chainer에 의해 처음 제창되었고, 이후 대표적인 프레임워크로는 파이토치, MXNet, DyNet 등이 있다. 동적 계산 그래프 방식에서는 데이터가 forward하는 동안에 거쳤던 연산과 결과물을 연결(참조)하며 계산 그래프가 만들어진다. 주로 연결 리스트 자료구조를 이용해서 구현된다.


동적 계산 그래프 방식의 장점은 프레임워크 자체 규칙을 배우지 않아도 된다. 기존 프로그래밍 언어 (파이썬)으로 연산을 정의함으로서 계산 그래프를 만들 수 있다. 파이썬 구문 (if문 등)을 사용해서 연산을 할 수도 있다. 그리고 디버깅에도 유리하다. 디버깅 메세지가 우리가 익숙한 형태로 나오기 때문이다.



마치며#

두 가지 방식에는 장,단점이 있기 때문에 어느 것이 더 좋다고 할 수 없다. 두 방식 모두를 지원하는 프레임워크도 많다고 한다. 파이토치에서도 TorchScript라는 것을 사용하면 정적 계산 그래프 방식도 할 수 있다고 한다. 텐서플로우는 2.0부터 eager execution이라는 동적 계산 그래프 방식이 표준으로 채택 되었다고 한다.