페이지

2022년 8월 6일 토요일

STEP4 수치미분

 지금까지 Variable 클래스와 Function 클래스를 구현했습니다. 이 클래스를 구현한 이유는 미분을 자동으로 계산하기 위해서 입니다. 본격적인 구현에 앞서 이번 단계에서는 미분이 무엇인지 복습하고 수치 미분이라는 간단한 방법으로 미분을 계산해보겠습니다. 그런 다음 5단계에서 수치 미분을 대신하는 더 효율적인 알고리즘(역전파)을 구현할 계획입니다.

머신러닝 외에도 많은 분야에서 미분을 사용합니다. 유체 역학, 금융 공학, 기상 시뮬레이션, 엔지니어링 설계 최적화 등 정말 많죠. 이런 다양한 분야에서 자동 미분계산 기능이 실제로 사용되고 있습니다.

3.2 함수 연결

 Function클래스의 __call__메서드는 입력과 출력이 모두 Variable 인스턴스이므로 자연스럽게 DeZero 함수들을 연이어 사용할 수 있습니다. y= (eX**2)**2 이라는 계산을 예로 생각해보죠. 

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

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data)

1.648721270700128

3개의 함수 A, B, C를 연이어 적용했습니다. 여기서 중요한 점은 중간에 등장하는 4개의 변수 x, a, b, y가 모두 Variable 인스턴스라는 것입니다. Function 클래스의 __call__메서드의 입출력이 Variable 인스턴스로 통일되어 있는 덕분에 이와 같이 여러 함수를 연속하여 적용할 수 있는것이죠. 참고로 방금 한 계산은 [그림 3-1]과 같이 함수와 변수가 교대로 늘어선 계산 그래프로 표현할 수 있습니다.





[그림 3-1]과 같이 여러 함수를 순서대로 적용하여 만들어진 변환 전체를 하나의 큰 함수로 볼 수도 있습니다. 이처럼 여러 함수로 구성된 함수를 합성 함수(compuosite function)라고 합니다. 합성함수를 구성하는 각 함수의 계산은 간단하더라도, 연속으로 적용하면 더 복잡한 계산도 해낼 수 있다는 사실을 기억하세요.

그런데 일련의 계산을 '계산 그래프'로 보여드린 이유는 무엇일까요? 그 이우는 계산 그래프를 이용하면 각 변수에 대한 미분을 효율적으로 계산할 수 있기 때문이랍니다(정확하게는 그럴 준비가 됩니다). 그리고 변수별 미분을 계산하는 알고리즘이 바로 역전파입니다. 다음 단계부터는 역전파를 구현할 수 있도록 DeZero를 확장하겠습니다.

3.1 Exp 함수 구현

 운선 DeZero에 새로운 함수를 하나 구현하겠습니다. 바로 y=eX이라는 계산을 하는 함수입니다. 여기서 e는 자연로그의 밑(base of the natural logarithm)으로 구체적인 값은 2.718..입니다(오일러의 수(Euler's number)혹은 네이피어 상수(Napier's constant)라고도 합니다). 


class Exp(Function):
  def forward(selfx):
    return np.exp(x)

Square클래스와 마찬가지로 Function 클래스를 상속한 다음 forward 메서드에서 원하는 계산을 구현했습니다. Square클래스의 차이는 forward메서드의 내용이 x ** 2에서 np.exp(x)로 바꾼점입니다.

STEP3 함수 연결

 지금까지 DeZero의 '변수'와 '함수'를 만들어봤습니ㅏㄷ. 그리고 2단계에서는 Square라는 제곱계산용 함수 클래스를 구현했습니다. 이번 단계에서는 또 다른 함수를 구현하고 여러 함수를 조합해 계산할 수 있도록 하겠습니다.

2.3 Function 클래스 이용

 Function 클래스를 실제로 사용해보죠. Variable 인스턴스인 x를 Function인스턴스인 f 에 입력해보겠습니다.

x = Variable(np.array(10))
f = Function()
y = f(x)

print(type(y)) # type() 함수는 객체의 클래스를 알려준다.
print(y.data)

이와 같이 Variable과 Function을 연계할 수 있습니다. 실행 결과를 보면 y의 클래스는 Variable이며, 데이터는 y.data에 잘 저장되어 있음을 알 수 있습니다.

그런데 방금 구현한 Function 클래스는 용도가 '입력값의 제곱'으로 고정된 함수입니다. 따라서 Sequare라는 명확한 이름이 더 어울립니다. 앞으로 Sin, Exp 등 당야한 함수가 필요하다는 점을 고려하면 Function클래스는 기반 크래스로 두고 DeZero의 모든 함수가 공통적으로 게족하는 기능만 담아주는 것이 좋겠습니다. 그래서 앞으로 모든 DeZero함수는 다음의 두 사항을 만족하도록 구현하겠습니다.

1) Function 클래스는 기반 클래스로서, 모든 함수에 공통되는 기능을 구현합니다.

2) 구체적인 함수는 Function 클래스를 상속한 캘래스에서 구현합니다.

이를 위해 Function 클래스를 다음처럼 수정합니다.

class Function:
  def __call__(selfinput):
    x = input.data
    y = self.forward(x) #구체적인 계산은 forward 메서드에서 한다.
    output = Variable(y)
    return output
  
  def forward(selfx):
    raise NotImplementedError()


__call__살짝 수정하고 forward라는 메서드를 추가했습니다. __call__메서드는 'Variable에서 ㅁ데이터 찾기'와 '계산 결과를 Variable에 포장하기'라는 두 가지 일을 합니다. 그리고 그 사이의 구체적인 계산은 forward메서드를 호출하여 수행합니다. 마지막으로 forward 메서드의 구체적인 로직은 하위 클래스에서 구현합니다.


Function 클래스의  forward메서드는 예외를 발생시킵니다. 이렇게 해두면 Function클래스의 forward메서드를 직접 호출한 사람에게 '이 메서드는 상속하여 구현해야 한다'는 사실을 알려줄 수 있습니다.


이러서 Function 클래스를 상속하여 입력값을 제곱하는 클래스를 구현하겠습니다. 클래스이름은 Squarea 라고 짓고 다음과 같이 구현합니다.

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

Square클래스는 Function클래스를 상속하기 때문에 __call__메서드는 그대로 계승됩니다. 따라서 forward메서드에 구체적인 계산 로직을 작성해 넣는 것만으로 구현은 끝입니다. 실제로 잘 동작하는지 Square 클래스를 사용하여 Variable을 처리하는 모습을 보시죠.

x = Variable(np.array(10))
f = Square()
y = f(x)
print(type(y))
print(y.data)

<class '__main__.Variable'> 100

2-2 Function 클래스 구현

 그러면 [그림2-1]의 함수를 프로그래밍 관점에서 생각해봅시다. 구체적으로는, 앞서 구현한 Variable인스턴슬르 변수로 다룰 수 있는 함수를 Function 클래스로 구현합니다. 여기서 주의할 점은 다음 두 가지입니다.

1) Function 클래스는 Variable인스턴스를 입력받아 Variable인스턴스를 출력합니다.

2) Variable 인스턴스의 실제 데이터는 인스턴스 변수인 data에 있습니다.

이 두 가지에 유의하여 Function 클래스를 다음과 같이 구현 합니다.

class Function:
  def __call__(selfinput):
    x = input.data # 데이터를 꺼낸다.
    y = x ** 2 #실제 계산
    output = Variable(y) # Variable 형태로 되돌린다.
    return output

__call__ 메서드의 인수 input은 Variable 인스턴스라고 가정합니다. 따라서 실제 데이터는 input.data에 존재합니다. 데이터를 커낸 후 원하는 계산(여기서는 제곱)을 하고, 결과를  Variable이라는 '상자'에 담아 돌려 줍니다.

__call__메서드는 파이썬의 특수 메서드입니다. 이 메서드를 정의하면 f = Function()형태로 함수의 인스턴스를 변수 f에 대입해주고, 나중에 f(...)형태로 __call__ 메서드를 호출할 수 있습니다.

2.1 함수란

 함수란 무엇일까요? 조금 딱딱하게 표현하면 '어떤 변수로부터 다른 변수로의 대응 관곌르 정한 것'이라고 할 수 있씁니다. 구체적인 예가 있으면 좋겠군요. 제곱을 계산하는 함수 f(x) = x2이 있다고 해봅시다. 이때 y=f(x)라고 하면 변수 y와 x의 관계가 함수 f에 의해 결정됩니다. 즉, 함수 f에 의해 'y는 x의 제곱이다'라는 관계가 성립됩니다.

이와 같은 변수 사이의 대응 관곌르 정하는 역할을 함수가 맡게 되며, [그림 2-1]은 그 의미를 시각적으로 표현한 모습니다.

[그림 2-1]은 변수 x 와 y, 그리고 함수 f의 관계를 보여줍니다. 이처럼 원(o)과 사각형 모양의 노드을 화살표로 연결해 계산 과정을 표현한 그림은 계산 그래프(computational graph)라고 합니다. 이 책에서는 변수를 동그라미 으로, 함수를 사각형으로 표시하겠습니다.