페이지

2022년 8월 17일 수요일

13.1 가변 길이 인수에 대응한 Add클래스의 역전파

 역전파를 구현하기에 앞서 [그림 13-1]의 덧셈 계산 그래프를 살펴봅시다.



[그림 13-1]에서 덧셈의 순전파는 입력이 2개, 출력이 1개입니다. 역전파는 그 반대가 되어 입력이 1개, 출력이 2개입니다. 수식으로 확인하면 y = x0 + x1일때 미분하면 dy/dx0 = 1, dy/dx1 = 1이 구해집니다.

y = x0 + x1계산(함수)와 입력 변수는 2개입니다. 이처럼 압력 변수가 여러 개인 함수를 다변수 함수라고 합니다. 다변수 함수에서 하나의 입력 변수에만 주목하여(다른 변수는 상수로 취급) 미분하는 것을 편미분이라고 합니다. 편미분에서는 미분 기호로 를 사용합니다. 예를 들어 dy/dx0  는 x0 이외의 변수를 상수로 생각하고 x0에만 주목하여 미분한다는 뜻입니다. 이 책에서는 앞으로 편미분을 따로 구분하지 않고 '미분'으로 통일해 부르겠습니다. 또한 변수가 하나인 경우에도 수식에서  d 기호를 사용하겠습니다.


덧셈의 역전파는 출력쪽에서 전해지는 미분값에 1을 곱한 값이 입력 변수(x0, x1)의 미분입니다. 즉, 상유에서 흘러오는 미분값을 '그대로 흘려보내는 것'이 덧셈의 역전파입니다. 이상을 반영하여 Add클래스를 다음과 같이 구현합니다.

class Add(Function):
  def forward(selfx0x1):
    y = x0 + x1
    return y

  def backward(selfgy):
    return gy, gy

이와 같이 backward메서드는 입력이 1개, 출력이 2개입니다. 물론 이 코드처럼 여러 개의 값을 반환할 수 있게 하려면 역전파의 핵심 구현을 변경해야 합니다. DeZero에서는 Variable클래스의 backward메서드를 수정하기로 했습니다.

2022년 8월 15일 월요일

STEP 13 가변 길이 인수(역전파 편)

 이전 단계까지의 변경으로 함수가 여러 개의 변수를 입력받고 반환할 수 있게 되었습니다. 이를 순전파 구현에 반영하고 계산이 오라로 이뤄지는지도 확인했습니다. 순전파 다음은 당영히 '역전파'입니다. 그래서 이번 단계에서는 역전파를 구현합니다.

12.3 add 함수 구현

 마지막으로 Add클래스를 '파이썬 함수'로 사용할 수 있는 코드를 추가하겠습니다.

def add(x0x1):
  return Add()(x0, x1)

이 add함수를 상요하면 계산 코드를 다음처럼 작성할 수 있습니다.

x0 = Variable(np.array(2))
x1 = Variable(np.array(3))
y = add(x0, x1)     # Add클래스 생성 과정의 감춰짐
print(y.data)

5

이상으로 함수가 가변 길이 인수를 더 자연스럽게 다룰 수 있게 해봤습니다. 여기에서는 '덧셈'만을 구현했지만 '곱셈'과 '나눗셈'도 같은 방식으로 구현할 수 있습니다. 그러나 가변길이 인수를 다룰 수 있는 것은 '순전파'뿐입니다. 예상하셨듯이 '역전파'는 바로 다음 단계에서 구현할 것입니다.

12.2 두 번째 개선: 함수를 구현하기 쉽도록

 두 번째는 Add 클래스를 '구현하는 사람'을 위한 개선입니다. 현재 Add 클래스를 구현하려면 [그림 12-2]의 왼쪽 처럼 작성해야 합니다.


왼쪽은 Add 클래스의 forward메서드의 코드입니다. 인수는 리스트로 전달되고 결과는 튜플을 반환하고 있습니다. 물론 오른쪽 코드가 더 바람직해 보입니다. 입력도 변수를 직접 받고 결과도 변수를 직접 돌려주는 것이죠. 이것이 두 번째 개선에서 할 일입니다.


두 번째 개선을 위해 Function클래스에서 다음 부분을 수정합니다.

class Function:
  def __call__(self, *inputs):
    xs = [x.data for x in inputs]
    ys = self.forward(*xs)  # 1 별표를 붙여 언팩
    if not isinstance(ys, tuple):   # 2 튜플이 아닌 경우 추가 지원
      ys = (ys,)
    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 if len(outputs) > 1 else outputs[0]

우선 1의 self.forward(*xs) 부분을 보죠. 함수를 '호출'할 때 별표를 붙였는데, 이렇게 하면 리스트 언팩(list unpack)이 이루어집니다. 언팩은 리스트의 원소를 낱개로 풀어서 전달하는 기법입니다. 예를 들어 xs = [x0, x1]일때 self.forward(*xs)를 하면 self.forward(x0, x1)로 호출하는 것과 동일하게 동작합니다.

이어서 2에서는 ys 가 튜플이 아닌 경우 튜플로 변경합니다. 이제 forward메서드는 반환원소가 하나뿐이라면 해당 원소를 직접 반환합니다. 이상의 수정으로 Add클래스를 다음처럼 구현할 수 있습니다.

class Add(Function):
  def forward(selfx0x1):
    y  = x0 + x1
    return y


이와 같이 순전파 메서드를 def forward(self, x0, x1): 이라고 정의할 수 있습니다. 결과는 return y처럼 하여 원소 하나만 반환하죠. 이제 Add클래스를 구현하는 사람에게DeZero는 더 쓰기 편한 프레임워크가 되었습니다. 이상으로 두 번째 개선을 마무리합니다.

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클래스를 사용하는 사람에게 입력 변수를 리스트에 담아 건네주라고 요구하거나 반환값으로 튜플을 받게 하는 것은 자연스럽지 않기 때문입니다. 그래서 다음 단계에서는 더 자연스러운 코드로 쓸 수 있도록 지금의 구현을 개선하겠습니다.