페이지

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

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클래스 차례입니다.