페이지

2022년 8월 19일 금요일

STEP 14 같은 변수 반복 사용

 현재 DeZeror에는 문제가 있습니다. 같은 변수를 반복해서 사용할 경우 의도대로 동작하지 않을 수 있다는 문제입니다. y = add(x, x) 계산을 예로 생각해봅시다.


DeZero는 [그림 14-1]처럼 동일한 변수를 사용하여 덧셈을 하면 제대로 미분하지 못합니다. 실제로 어떤 결과가 나오는지 직접 확인해 보겠습니다.

x = Variable(np.array(3.0))
y = add(x, x)
print('y', y.data)

y.backward()
print('x.grad', x.grad)

y 6.0 x.grad 1.0

x = 3.0으로 설정한 후 더 해봤습니다. y의 값은 6.0이라고 재대로 계산했군요. 그러나 x에 대한 미분값(x.grad)을 구하니 1.0이라는 잘못된 결과가 나왔습니다. 제대로 계산한다면 y = x + x일때 y = 2x이니 미분값은 dy/dx = 2 가 됩니다.


13.3 Square 클래스 구현

 지금까지 Variable과 Function클래스가 가변 길이 입출력을 지원하도록 개선했습니다. 그리고 구체적인 함수로서 Add 클래스를 구현했습니다. 마지막으로 Square클래스도 새로운 Variable과 Function클래스에 맞게 수정하겠습니다. 수정할 곳은 단 하나뿐입니다. 다음 코드에서 음영을 입힌 부분이죠.

class Square(Function):
  def forward(selfx):
    y = x ** 2
    return y

  def backward(selfgy):
    x = self.inputs[0].data #수정전: x = self.input.data
    gx = 2 * x * gy
    return gx

Function 클래스의 인스턴스 변수 이름이 단수형인 input에서 복수형인 inputs로 변경되었으니 바뀐 변수에서 입력 변수 x 를 가져오도록 코드를 수정해주면 됩니다. 이것으로 새로운 Square 클래스도 완성입니다. 그럼 add 함수와 sequare 함수를 실제로 사용해봅시다. 다음은 z = x**2 + y**2를 계산하는 코드입니다.

x = Variable(np.array(2.0))
y = Variable(np.array(3.0))

z = add(square(x), square(y))
z.backward()
print(z.data)
print(x.grad)
print(y.grad)

13.0 4.0 6.0


보다시피 DeZero를 사용하여 z = x**2  + y**2이라는 계산을 z = add(square(x), square(y))라는 코드로 풀어냈습니다. 그런 다음 z.backward()를 호출하기만 하면 미분 계산이 자동으로 이루어집니다!

이상에서 복수의 입출력에 대응한 자동 미분 구조를 완성했습니다. 이제 다른 함수들도 적절히 구현해주면 더 복잡한 계산도 가능할 것입니다. 그러나 사실 지금의 DeZero에는 몇 가지 문제가 숨어 있습니다. 다음 단계에서는 이 문제들을 먼저 해결하겠습니다.

13.2 Variable 클래스 수정

 그럼 Variable 클래스의 backward메서드를 살펴보겠습니다. 복습할 겸 Variable클래스의 현재 코드를 먼저 보여드리죠

class Variable:
  def __init__(selfdata):
    if data is not None:
      if not isinstance(data, np.ndarray):
        raise TypeError('{}은(는) 지원하지 않습니다.' .format(type(data)))
    self.data = data
    self.grad = None
    self.creator = None

  def set_creator(selffunc):
    self.creator = func
    
  def backward(self):
    if self.grad is None:
      self.grad = np.ones_like(self.data)
    
    funcs = [self.creator]
    while funcs:
      f = funcs.pop()
      x, y = f.input, f.output  # 1 함수의 입출력을 얻는다.
      x.grad = f.backward(y.grad)   # 2 backward 메서드를 호출한다.

      if x.creator is not None:
        funcs.append(x.creator)

여기서 주목할 곳은 음영 부분입니다. 우선 while 블럭 안의 1)에서 함수의 입출력 변수를 꺼냅니다. 그리고 2)에서 함수의 backward메서드를 호출 합니다. 지금까지 우리는 1)에서 함수의 입출력이 하나씩이라고 한정했습니다. 이부분을 여러 개의 변수에 대응할 수 있도록 수정하겠습니다.

class Variable:

  def __init__(selfdata):
    if data is not None:
      if not isinstance(data, np.ndarray):
        raise TypeError('{}은(는) 지원하지 않습니다.' .format(type(data)))
    self.data = data
    self.grad = None
    self.creator = None

  def set_creator(selffunc):
    self.creator = func

    
  def backward(self):
    if self.grad is None:
      self.grad = np.ones_like(self.data)

    funcs = [self.creator]
    while funcs:
      f = funcs.pop()
      gys = [output.grad for output in f.outputs]   # 1)
      gxs = f.backward(*gys)    # 2)
      if not isinstance(gxs, tuple):  # 3)
        gxs = (gxs,)
      
      for x, gx in zip(f.inputs, gxs):  # 4)
        x.grad = gx

        if x.creator is not None:
          funcs.append(x.creator)


총 네 군데를 수정했습니다. 우선 1)에서 출력 변수인 outputs에 담계 잇는 미분값들을 리스트에 담습니다. 그리고 2)에서 함수 f의 역전파를 호출합니다. 이때 f.backward(*gys)처럼 인수에 별표를 붙여 호출하여 리스트를 풀어줍니다(리스트 언팩). 3) 에서 gxs가 튜플이 아니라면 튜플로 변환합니다.


2)와 3)은 이전 단계에서 순전파 개선 시 활용한 관례와 같습니다. 2)에서 Add클래스의 backward메서드를 호출할 때  인술르 플어서 전달합니다. 3)에서는 Add 클래스의 backward 메서드가 튜플이 아닌 해당 원소를 직접 반환할 수 있게 합니다.


4)에서는  역전파로 전파되는 미분값을 Variable의 인스턴스 변수 grad에 저장해둡니다. 여리게서 gxs와 f.inputs의 각 원소는 서로 대응 관계에 있습니다. 더 정확히 말하면 i번째 원소에 대해 f.inputs[i]의 미분값은 gxs[i]에 대응합니다. zip 함수와 for 문을 이용해서 모든 Variable  인스턴스 각각에 알맞은 미분값을 설정한 것입니다. 이상이 Variable 클래스의 새로운 backward 메서드입니다.

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는 더 쓰기 편한 프레임워크가 되었습니다. 이상으로 두 번째 개선을 마무리합니다.