페이지

2022년 8월 12일 금요일

7.1 역전파 자동화의 시작

 역전파 자동화로 가는 길은 변수와 함수의 '관계'를 이해하는 데서 출발합니다. 우선 함수 관점에서 '함수는 변수를 어떻게 바라볼까'를 생각해봅시다. 함수 입장에서 변수는 '입력'과 '출력'에 쓰입니다. 즉, [그림 7-2]의 왼쪽과 같이 함수에서 변수는 '입력 변수 (input)'와 '출력 변수(output)'로서 존재합니다(그림의 점선은 참조(reference)를 뜻합니다).


변수 관점에서 함수는 어떤 존재일까요? 여기서 눈여겨볼 점은 변수는 함수에 의해 '만들어진다'라는 것입니다. 즉, 변수에게 있어 함수는 '창조자(creator)'혹은 '부모'입니다. 창조자인 함수가 존재하지 않는 변수는 함수 이외의 존재, 예컨대 사용자에 의해 만들어진 변수로 간주됩니다.

일단 [그림 7-2]와 같은 함수와 변수의 관계를 DeZero코드에 녹여볼까요? 여기에서는 일반적인 계산(순전파)이 이루어지는 시점에 '관계'를 맺어주도록(즉, 함수와 변수를 연결 짓도록) 만들겠습니다. 이를 위해 우선 Variable 클래스에 다음 코드를 추가합니다.

class Variable:
  def __init__(selfdata):
    self.data = data
    self.grad = None
    self.creator = None
  
  def set_creator(selffunc):
    self.creator = func
    

creator 라는 인스턴스 변수를 추가했습니다. 그리고 creator 를 설정할 수 있도록 set_creator 메서드로 추가합니다. 이어서 Function클래스에 다음 코드를 추가합니다.

class Function:
  def __call__(selfinput):
    x = input.data
    y = self.forward(x)
    output = Variable(y)
    output.set_creator(self)  # 출력 변수에 창조자를 설정한다.
    self.input = input
    self.output = output # 출력도 저장한다.
    return output
  

순전파를 계산하면 그 결과로 output이라는 Variable 인스턴스가 생성됩니다. 이때 생성된 output에 '내가 너의 창조자임'을 기억시킵니다. 이 부분이 '연결'을 동적으로 만드는 기법의 핵심입니다. 그런 다음 앞으로를 위해 output을 인스턴스 변수에 저장했습니다.


DeZero의 동적 계산 그래프(Dynamic Computational Graph)는 실제 계산이 이루어질 때 변수(상자)에 관련 '연결'을 기록하는 방식으로 만들어집니다. 체이너와 파이토치의 방식도 이와 비슷합니다.


이와 같이'연결' 된 Variable과 Function이 있다면 계산 그래프를 거꾸로 거슬러 올라갈 수 있습니다. 구체적인 코드로 나타내면 다음과 같습니다.

A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
#계산 그래프의 노드들을 거꾸로 거슬러 올라간다.

assert y.creator == C
assert y.creator.input == b
assert y.creator.input.creator == B
assert y.creator.input.creator.input == a
assert y.creator.input.creator.input.creator == A
assert y.creator.input.creator.input.creator.input == x

우선 assert문이 무엇이지 설명해야겠네요. 먼저 'assert'는 우리말로 '단호하게 주장하다', '단언하다'라는 뜻입니다. assert 문은 assert ... 형태로 사용합니다. 여기서 ... 부분이 '주장'에 해당하는 내용으로, 그 평가 결과가 True가 아니면 예외가 발생합니다. 따라서 assert문은 조건을 충족하는지 여부를 확인하는데 사용할 수 있습니다. 참고로 앞의 코드는 문제없이(예외가 발생하지 않고) 실행되므로 assert문의 조건을 모두 충족함을 알 수 있습니다.

앞의 코드가 보여주듯 Variable의 인스턴스 변수 creator 에서 바로 앞의 Function으로 건너갑니다. 그리고 그 Function의 인스턴스 변수 input에서 다시 하나 더 앞의 Variable로 건너가죠. [그림 7-3]은 이 관계를 잘 보여줍니다.


[그림 7-3]과 같이 우리 계산 그래프는 함수와 변수 사이의 연결로 구성됩니다. 그리고 중요한 점은 이 '연결'이 실제로 계산을 수행하는 싲넘에(순전파로 데이터를 흘려보낸 때) 만들어진다는 것입니다. 이러한 특성에 이름을 붙인 것이 Define-by-Run입니다. 데이터를 흘려보냄으로써(Run함으로써)연결이 규정된다는 (Define된다는) 뜻입니다.

또한 [그림 7-3]과 같이 노드들의 연결로 이루어진 데이터 구조를 '링크드 리스트(linked list)라고 합니다. 노드는 그래프를 구성하는 요소이며, 링크(link)는 다른 노드를 가리키는 참조를 뜻합니다. 결국 우리는 '링크드 리스트'라는 데이터 구조를 이용해 계산 그래프를 표현하고 있는 것입니다.

댓글 없음: