페이지

2022년 8월 15일 월요일

12.1 첫 번째 개선: 함수를 사용하기 쉽게

 이번 절에서는 Add 클래스를 사용하여 계산을 해보았는데, [그림 12-1]의 왼쪽 코드를 썼습니다.


[그림 12-1]의 왼쪽 코드에서 알 수 있듯이 현재의 Add클래스는 인수를 리스트에 모아서 받고 결과는 튜플로 반환합니다. 그러나 오른쪽 그림처럼 리스트나 튜플을 거치지 않고 인수와 결과를 직접 주고 받는편이 훨씬 자연스럽습니다. 코드를 이와 같은 형태로 작성할 수 있게 해주는 것이 첫 번째 개선입니다.

그럼 첫 번째 개선에 도전해 봅시다. 그러려면 우선 Function클래스를 수정합니다. 이전 단계와 달라진 부분에는 음영을 넣었습니다.


우선 2부분부터 설명하겠습니다. outputs에 원소가 하나뿐이면 리스트가 아니라 그 원소만을 반환합니다. 다시 말해 함수의 반환값이 하나라면 해당 변수를 직접 돌려줍니다.

이어서 1부분입니다. 함수를 정의할 때 인수 앞에 별표(*)를 붙였습니다. 이렇게 하면 리스트를 사용하는 대신 임의 개수의 인수(가변 길이 인수)를 건내 함수를 호출할 수 있습니다. 가변 길이 인수의 사용법은 다음 예를 보면 명확해질 것입니다.


이 코드에서 알 수 있듯이 함수를 '정의'할 때 인수에 별표를 붙이면 호출할 때 넘긴 인수들을 별표를 붙인 인수 하나로 모아서 받을 수 있습니다. 이상의 변경 덕에 DeZero의 함수 클래스(Add 클래스)를 다음 코드처럼 사용할 수 있게 됩니다.


이것으로 Add 클래스 사용자들에게 더 자연스러운 사용법을 제공하게 되었군요. 지금까지가 첫 번째 개선이었습니다. 이어서 두 번째 개선으로 이동합니다.

2022년 8월 14일 일요일

STEP12 가변 길이 인수(개선 편)

 이전 단계에서는 가변 길이 인수에 대응할 수 있도록 DeZero를 확장했습니다. 하지만 개선이 필요해 보였습니다. 그래서 이번 단계에서는 DeZero를 더 쉽게 사용할 수 있도록 두 가지를 개선하겠습니다. 첫 번째는 Add 클래스 (혹은 다른 구체적인 함수 클래스)를 '사용하는 사람'을 위한 개선이고, 두 번째는 '구현하는 사람'을 위한 개선입니다. 첫 번째 개선부터 시작하겠습니다.

11.2 Add 클래스 구현

 이번 절에서는 Add클래스의 forward메서드를 구현합니다. 주의할 점은 인수와 반환값이 리스트(또는 튜플)여야 한다는 것입니다. 이 조건을 반영하여 다음 처럼 구현할 수 있습니다.

class Add(Function):
  def forward(selfxs):
    x0, x1 = xs
    y = x0 + x1
    return (y,)

Add 클래스의 인수는 변수가 두개 담긴 리스트입니다. 따라서 x0, x1 = xs형태로 리스트 xs에서 원소 두 개를 꺼냈습니다. 그런 다음 꺼낸 원소들을 사용하여 계산합니다. 결과를 반환할 때는 return(y,)형태로 튜플을 반환합니다(return y,처럼 괄호는 생략해도 됩니다). 수정한 Add클래스는 다음과 같이 사용할 수 있습니다.

xs = [Variable(np.array(2)), Variable(np.array(3))]   # 리스트로 준비
f = Add()
ys = f(xs)      # ys 튜플
y = ys[0]
print(y.data)

5

보시는 것처럼 2 + 3 = 5 계산을 DeZero로 재대로 처리할 수 있게 되었습니다. 입력을 리스트로 바꿔서 여러 개의 변수를 다룰 수 있게 하였고, 출력은 튜플로 바꿔서 역시 여러 개의 변수에 대응할 수 있게 했습니다. 이제 순전파에 한해서는 가변 길이 인수와 반환값에 대응할 수 있을 것입니다. 그런데 앞의 코드를 보면 다소 귀찮은 느낌이 듭니다. 왜냐하면 Add클래스를 사용하는 사람에게 입력 변수를 리스트에 담아 건네주라고 요구하거나 반환값으로 튜플을 받게 하는 것은 자연스럽지 않기 때문입니다. 그래서 다음 단계에서는 더 자연스러운 코드로 쓸 수 있도록 지금의 구현을 개선하겠습니다.

11.1 Function 클래스 수정

 가변 길이 입출력을 표현하려면 변수들을 리스트(또는 튜플)에 넣어 처리하면 편할 것 같습니다. 즉, Function 클래스는 지금까지처럼 '하나의 인수'만 받고 '하나의 값'만 반환하는 것이죠. 대신 인수와 반환값의 타입을 리스트로 바꾸고, 필요한 변수들을 이 리스트에 넣으면 됩니다.

파이썬의 리스트와 튜플은 여러 개의 데이터를 한 줄로 저장합니다. 리스트는 [1, 2, 3]과 같이 []로 묶고 튜플은 (1, 2, 3)과 같이 ()로 묶습니다. 리스트와 튜플의 주요 차이는 원소를 변경할 수 있는지 여부입니다. 튜플의 경우 한번 생성되면 원소를 변경할 수 없습니다. 옐르 들어  x = (1, 2, 3)으로 튜플을 생성한 후 에는 x[0]  = 4 등으로 덮어 쓸 수 없는 것이죠. 반면 리스트는 원소를 변경할 수 있습니다.


그러면 현재의 Function 클래스가 어떻게 구현되어 있는지부터 확인해 보죠.

class Function:
  def __call__(selfinput):
    x = input.data  #1
    y = self.forward(x) #2
    output = Variable(as_array(y))  #3
    output.set_creator(self)  #4
    self.input = input
    self.output = output
    return output
  
  def forward(selfx):
    raise NotImplementedError()
  
  def backward(selfgy):
    raise NotImplementedError()


Function의 __call__메서드는 Variable이라는 '상자'에서 실제 데이터를 꺼낸 다음 forward메서드에서 구체적인 계산을 합니다. 그리고 계산 결과를 Variable에 넣고 자신이 '창조자'라고 원산지를 표시합니다. 이상의 로직을 염두에 두고 __call__메서드의 인수와 반환값을 리스트로 바꿔보겠습니다.

 

class Function:
  def __call__(selfinputs):
    xs = [x.data for x in inputs]
    ys = self.forward(xs)
    outputs = [Variable(as_array(y)) for y in ys ]

    for output in outputs:
      output.set_creator(self)
    self.inputs = inputs
    self.outputs = outputs
    return outputs
  
  def forward(selfxs):
    raise NotImplementedError()

  def backward(selfgys):
    raise NotImplementedError()

인수와 반환값을 리스트로 변경했습니다. 변수를 리스트에 담아 취급한다는 점을 제외하고는 달라진 게 없습니다. 참고로 앞의 코드에서는 리스트를 생성할 때 리스트 내포(list comprehension)를 사용했습니다.


리스트 내포는 xs = [x.data for x in inputs] 형태로 사용합니다. 이 코드는 inputs 리스트의 각 원소 x에 대해 각각의 데이터(x.data)를 꺼내고, 꺼낸 원소들로 구성된 새로운 리스트를 만듭니다.


이상이 새로운 Function 클래스입니다. 이어서 새로운 Function클래스를 사용하여 구체적인 함수를 구현하겠습니다. 첫 번째는 덧셈을 해주는 Add클래스 차례입니다.

STEP 11가변 길이 인수(순전파 편)

 지금까지 우리는 함수에 입출력 변수가 하나씩인 경우만 생각해왔습니다. 예를 들어 y = square(x) 와 y = exp(x) 등은 입출력 변수가 하나씩입니다. 그러나 함수에 따라 여러 개의 변수를 입력받기도 합니다. [그림 11-1]의 '덧셈'과 '곱셈'이 대표적이죠.


출력이 여러 개인 함수도 있습니다. 옐르 들어 [그림 11-2]와 같은 함수가 있을 수 있겠죠.



이상을 고려하여 DeZero가 가변 길이 입출력을 처리할 수 있도록 확장하려 합니다. 가변 길이라고 함은 인수(또는 반환값)의 수가 달라질 수 있다는 뜻입니다. Function 클래스를 조금 수정하면 가변 길이 입출력에 대응 할 수 있을 것 같습니다.

제2고지 자연스러운 코드로

 우리 Dezero가 제1고지에 올라섰습니다(특정 계산으로 한정하면) 미분을 자동으로 계산할 수 있습니다. 예를 들어 제곱과 지수 함수 같은 함수 클래스로 구성된 (그리고 분기가 없는) 계산 그래프라면 backward 메서드를 호출하는 것만으로 미분값이 자동으로 구해집니다.

이제부터 두 번째 고지로 향합니다. 이번 고지의 주된 목적은 더 복잡한 계산도 가능하도록 현재의 DeZero를 확장하는 것입니ㅏㄷ. 정확하게는 입력을 여러 개 받는 함수나 출력이 여러 개인 함수도 처리할 수 있도록 DeZero의 기반을 수정할 것입니다. 또한 계산을 더 자연스러운 코드로 표현할 수 있도록, 예를 들어 +와 * 같은 연산자를 사용할 수 있도록 하겠습니다.

자동미분

 딥러닝 프레임워크의 중심에는 역전파가 있습니다. 역전파를 문헌에 따라 '자동 미분'이라고 부르기도 합니다. 하지만 '자동미분'이라는 용어는 (특히 학술 분야에서는) 더 제한적인 방법을 뜻하므로 주의해야 합니다. 이번 칼러에서는 자동미분에 대해 한 걸음 더 들어가 보겠습니다.

자동 미분을 문자 그래도 해석하면 '자동으로 미분을 계산하는 방법(기술)입니다. '자동으로'라 함은(사람이 아니라)컴퓨터가 미분을 계산한다는 뜻이죠. 정확히 말하면 어떤 계산(함수)을 코드로 구현하면 그 계산의 미분을 컴퓨터가 자동으로 계산해주는 시스템을 가리킵니다.

컴퓨터 프로그램에서 미분을 계산하는 방법은 크게 세 가지로 나눌수 있습니다. 첫 번째는 수치미분(numerical differentiation)입니다. 수치 미분은 4단계에서 구현한 것처럼 변수에 미세한 차이를 주어 일반적인 계산(순저파)을 2회 실시하고, 두 출력의 차이로부터 근사적으로 미분을 계산합니다. 수치 미분은 구현하기 쉽지만 출려에 오차가 포함되기 쉽고, 다량의 변수를 사용하는 함술르 다룰때는 계산 비용이 높다는 단점이 있습니다.

두 번째 방법은 기호 미분(symbolic differentiation)입니다. 기호 미분은 고등학교 수학에서 배운것처럼 미분 공식을 이용하여 계산하는 방법입니다. 입력도 '수식'이고 출력도'수식'입니다(수식은 트리 데이터 구조로 표현할 수 있습니다). Mathematica와 MATLAB등에서 이용하는 방법입니다.

기호 미분의 출력은 미분된 '식'(즉, 도함수)이며, 출력 시점에는 아무런 수치 계산도 수행되지 않습니다. 대신 도함수를 얻은 후 그체적인 값(예:x = 3.0)에서의 미분을 계산하는 식입니다.

기호 미분의 단점은 수식이 크게 부풀어 오르기 쉽다는 것입니다. 특히 최적화를 고려하지 않고 구현하면 수식이 곧바로 거대해집니다(수식이 '폭발'한다고 합니다). 그런데 딥러닝에서 취급하는 계산은 수많은 변수에 대한(수식이 아닌) 미분'값'을 효율적으로 구해야 합니다. 그래서 기호 미분보다 효율적인 방법이 필요합니다.

세 번째 방법은 자동 미분(automatic differentiation)입니다. 자동 미분은 연쇄 법칙을 사용하여 미분하는 방법으로, 어떤 함수를 프로그램으로 짜서 건네주면 그 미분을 효율적이고 정밀하게 계산할 수 있습니다. 역전파 방식도 자동 미분에 속합니다. 더 정확히 말하면 자동 미분은 크게 두 가지로 나눈 수 있습니다. 바로 'forward 모드'와 'reverse 모드'죠. 역전파는 후자인 'reverse 모드 자동 미분'에 해당합니다.

역전파(reverse 모드 자동 미분)는 미분 결과를 출력 쪽으로부터 입력 쪽으로 전달합니다. 반대로 forward모드 자동 미분은 입력 쪽으로부터 출력 쪽으로 전달합니다. 두 방법 모두 연쇄법칙을 사용하여 미분값을 계산하지만 그 '경로'가 다른 것이죠. 출력이 하나뿐이고, 그 하나의 출력변수를 미분하려면 reverse 모드 자동 미분이 적합합니다. 머신 러닝은 대부분 출력이 변수 하나로 모이지는 문제를 다루기 때문에 reverse 모드 자동 미분이 사용됩니다. 이러한 이유로 이책에서는 forward 모드 자동 미분에 대해서는 더 이상 설명하지 않으니, 관심 있는 분은 따로 참고 문헌[6]과 [7]을 참고하기 바랍니다.

지금까지의 내용을 정리하면 '컴퓨터 프로그램으로 미분을 계산하는 방법'은 [그림A-1]과 같이 나뉩니다.


[그림 A-1]에서 보듯 '자동 미분'은 컴퓨터로 미분을 계산하는 여러 방법 중 하나입니다. 딥러닝 프레임워크는 그 중에서도 'reverse 모드 자동 미분'을 구현해 사용합니다. 그러나 문헌에 따라 forward모드와 reverse모드를 구분하지 않고, 역전파를 가르켜 '자동미분'이라고 부르기도 합니다.


자동 미분은 학계에서 오랫동안 연구해온 분야입니다. 오랜 역사만큼이나 중요한 지식도 많이 쌓여 있지요. 하지만 아쉽게도 지금까지 머신러닝 분야와는 그다지 교류가 없었습니다. 최근 딥러닝 붐이 일면서 자동미분 분야에 대한 관심이 높아지는 추세이며, 머신러닝과 프로그래밍 언어 등의 분야와 자동 미분 분야의 새로운 교류가 꿈틀대고 있답니다.