페이지

2022년 9월 2일 금요일

23.3 연산자 오버로드

 이것으로 step22.py의 코드 대부분이 옮겨졌습니다. 이제부터는 오버로드한 연산자들을 dezero로 옮기겠습니다. 이를 위해 코어 파일인 dezero/core_simple.py에 다음 함수들을 추가합니다.

def setup_variable():

    Variable.__add__ = add

    Varaibel.__radd__ = add

    Variable.__mul__ = mul

    Variable.__rmul__ = mul

    Variable.__neg__ = neg

    Variable.__sub__ = sub

    Variable.__rsub__ = rsub

    Variable.__truediv__ = div

    Variable.__rtruediv__ = rdiv

    Variable.__pow__ = pow


setup_variable은 Variable의 연산자들을 오버로드해주는 함수입니다. 이 함수를 호출하면 Variable의 연산자들이 설정됩니다. 그렇다면 이 함수는 어디에서 호출하면 좋을까요? 바로 dezero/__init__.py 파일입니다.


__init__.py는 모듈을 임포트할 때 가장 먼저 실행되는 파일입니다. 우리의 경우 dezero패키지에 솏한 모듈을 임포트할 때 dezero/__init__.py의 코드가 첫 번째로 호출됩니다. 그래서 dezero/__init__.py에 다음 코드를 작성해 넣어야 합니다.

from dezero.core_simple import Variable

from dezero.core_simple import Function

from dezero.core_simple import using_config

from dezero.core_simple import no_grad

from dezero.core_simple import as_array

from dezero.core_simple import as_variable

from dezero.core_simple import setup_variable


setup_variable()

이와 같이 setup_variable 함수를 임포트해 호출하도록 합니다. 이렇게 함으로써 dezero 패키지를 이용하는 사용자는 반드시 연산자 오버로드가 이루어진 상태에서 Variable을 사용할 수 있습니다.

한편 __init__.py의 시작이 from dezero.core_simple import Variable인데, 이 문장이 실행됨으로써 dezero 패키지에 Variable 클래스를 곧바로 임포트할 수 있습니다. 옐르 들어 다음과 같이 이용할 수 있습니다.

# dezero를 이용하는 사용자의 코드


# from dezero.core_simple import Variable

from dezero import Variable


즉, 지금까지 from dezero.core_simple import Variable이라고 작성한 것을 from dezero import Variable처럼 짧게 줄일 수 있습니다. 마찬가지로 dezero/__init__.py의 임포트문들 덕분에 사용자는 나머지 Function이나 using_config 등도 '간소화된' 임포트를 이용할 수 있게 됩니다.



23.2 코어 클래스로 옮기기

 dezero 디렉터에 파일을 추가해보죠. 목표는 이전 단계의 step22.py 코드를 dezero/core_simple.py라는 코어(core,핵심)파일로 옮기는 것입니다. 파일 이름에 core를 붙인 이유는 지금까지 구현한 기능들이 DeZero의 핵심이라고 보기 때문입니다. 그리고 뒤에 가서는 최종 형태인 core.py로 교체할 계획이라서 당장은 core_simple.py로 시작하겠습니다.


그럼 step22.py에 정의된 다음 클래스들을 코어 파일로 복사해봅시다.

- Config

- Variable

- Function

- Add(Function)

- Mul(Function)

- Neg(Function)

- Sub(Function)

- Div(Function)

- Pow(Function)


여기에서 Add(Function)의 (Function)은 Function 클래스를 상속했다는 뜻입니다. 보다시피 Config, Variable, Function 클래스가 있고, Function 클래스를 상속한 함수(DeZero함수 클래스)가 여럿 개 있습니다. 이어서  step22.py에 정의한 파이썬 함수들도ㅓ 정리해야 합니다. 즉, 다음 함수들을 코어 파일로 옮길 것입니다.


- using_config

- no_grad

- as_array

- as_variable

- add

- mul

- neg

- sub

- rsub

- div

- rdiv

- pow


처음 두 함수는 DeZero설정 함수로, 역전파의 활성/비활성을 전환하는 데 상요합니다. 그다음의 as_array와 as_variable은 인수로 주어진 객체를 ndarray 또는 Variable로 변환하는 함수입니다. 나머지는 DeZero에서 사용하는 함수입니다. 자, 우선 step22.py에 담긴 클래스와 함수를 그대로 코어 파일에 복사합니다.


지금까지 Exp 클래스와 Square클래스 그리고 exp 함수와 square함수 등 DeZero에서 사용하는 구체적인 함수들도 구현했습니다. 하지만 이 코드들은 코어 파일에 넣지 않겠습니다. 이 코득들은 나중에 dezero/functions.py에 추가 할 겁니다.


이제 외부의 파이썬 파일에서 다음과 같이 dezero를 임포트할 수 있습니다.

import numpy as np

from dezero.core_simple import Variable


x = Variable(np.array(1.0))

print(x)


이와 같이 from dezero.core_simple import Variable 줄을 추가하여 Variable 클래스를 임포트할 수 있습니다. dezero.core_simple 처럼 파일 이름까지 명시한 점에 주의하세요. 바로 뒤에서 core_simple을 생략하고 from dezero import Variable로 사용할 수 있는 구조를 도입할 겁니다.


from ... import ... 구문을 사용하면 모듈 내의 클레스나 함수 등을 직접 임포트할 수 있습니다. 또한 import XXX is A라고 쓰면 XXX라는 모듈을 A라는 이름으로 임포트할 수 있습니다. 옐르 들어 import dezero.core_simple as dx 쓰면 dezero.core_simple모듈을 dz라는 이름으로 임포트합니다. 그런 다음 Variable 클래스를 사용하려면 dz.Variable이라고 쓰면 됩니다.

2022년 8월 31일 수요일

23.1 파일 구성

 파일 구성부터 확인하겠습니다. 지금까지는 step01.py, step02.py,.. 처럼 각 step파일 코드를 작성했습니다. 이제부터는 이 step파일 모두에서 DeZero를 이용할 수 있도록 dezero라는 공통의 디렉터리를 하나 만들겠습니다. 그래서 최종 파일 구성은 다음과 같습니다.


이와 같이 구성한 뒤 dezero 디렉터리에 모듈을 추가하는 것입니다. 그리하여 dezero라는 페키지가 만들어지는데, 이 패키지가 바로 우리가 만드는 프레임워크입니다. 앞으로 주로 이 dezero디렉터리에 있는 파일들에 코드를 추가할 것입니다.

STEP 23 패키지로 정리

 지금까지는 단계마다 내용전체를 파일 하나에 담았습니다. step01.py에서 시작하여 step22.py까지 도달했죠, 그런데 어느덧 우리의 DeZero는 크게 '성장'했습니다. 그래서 이번 단계에서는 지금까지의 성과를 재상요할 수 있도록 패키지로 정리할 생각입니다.

참고로 파이썬에서는 '도듈', '패키지', '라이브러리'라는 용어를 사용하는데, 보통 다음의 의미로 통용됩니다.


1) 모듈: 

모듈은 파이썬 파일입니다. 특히 다른 파이썬 프로그램에서 임포트(import)하여 사용하는 것을 가정하고 만들어진 파이썬 파일을 '모듈'이라고 합니다.

2) 패키지:

패키지는 여러 모듈을 묶은 것입니다. 패키지를 만들려면 먼저 디렉터리를 만들고 그 안에 모듈(파이썬 파일)을 추가합니다.

3) 라이브러리

라이브러리는 여러 패키지를 묶은 것입니다. 그래서 하나 이상의 디렉터리로 구성되죠. 때로는 패키지를 가리켜 '라이브러리'라고 부르기도 합니다.

22.4 거듭제곱

 거듭제곱은 y = x **  c 형태로 표현됩니다. 이때 x를 밑이라고 하고 c를 지수라 합니다. 거듭제곱의 미분은 미분 공식으로부터 dy/dx = cx ** ( c - 1)이 됩니다. dy/dc의 값도 구할 수는 있지만 실정에서는 거의 사용되지 않으니 이 책에서는 밑이 x인 경우만 미분해보겠습니다. 즉, 지수 c는 상수로 취급하여 따로 미분을 계산하지 않기로 합니다. 다음은 이를 구현한 코드입니다.

class Pow(Function):
  def __init__(selfc):
    self.c = c

  def forward(selfx):
    y = y ** self.c
    return y

  
  def backward(Selfgy):
    x = self.inputs[0].data
    c = self.c
    gx = c * x ** ( c - 1 )  * gy
    return gx


def pow(xc):
  return Pow(c)(x)

Variable.__pow__ = pow

코들르 보면 Pow클래스를 초기화할 때 지수 c를 제공할 수 있습니다. 그리고 순전파 메서드인 forward(x)는 밑에 해당하는 x만(즉, 하나의 항만)받게 합니다. 그런 다음 특수 메서드인 __pow__에 함수 pow를 할당합니다. 이제 ** 연산자를 사용하여 거듭제곱을 계산할 수 있습니다.

x = Variable(np.array(2.0))
y = x ** 3

print(y)

이상으로 목표한 연산자를 모두 추가했습니다. 이번 단곈느 다소 단조로운 작업의 연속이었지만 그 덕분에 DeZero의 유용성은 크게 향상됐습니다. 사칙연산 연산자들을 자유롭게 계산에 활용할 수 있게 된거죠. 거듭제ㅐ곱도 가능하기 때문에 제법 고급 계산까지 표현할 수 있답니다.

22.3 나눗셈

 나눗셈의 미분은 y = x0/x1 일때 dy/dx0 = 1/x1, dy/dx1 = -x0/(x1) ** 2 입니다. 코드로는 다음처럼 구현할 수 있습니다.

from os.path import getsize
class Div(Function):
  def forward(selfx0x1):
    y = x0 / x1
    return y

  def backward(selfgy):
    x0, x1 = self.inputs[0].data, self.inputs[1].data
    gx0 = gy / x1
    gx1 = gy * (-x0 / x1 ** 2)
    return gx0, gx1

def div(x0x1):
  x1 = as_array(x1)
  return Div()(x0, x1)

def rdiv(x0x1):
  x1 = as_array(x1)
  return Div()(x1, x0)    # x0와 x1의 순서를 바꾼다.

Variable.__truediv__ = div
Variable.__rtruediv__ = rdiv

나눗셈도 뺄셈과 마찬가지로 좌/우항 중 어느, 것에 적용할지에 따라 적용되는 함수가 다릅니다. 그되에는 특별히 어려운 점은 없을 것입니다.

그럼 마지막으로 '거듭제곱'을 살펴보겠습니다.

22.2 뺄셈

 뺄셈의 미분은 y = x0 - x1일 때 dy/dx0 = 1, dy/dx1 = -1 입니다. 따라서 역전파는 상류에서 전해지는 미분값에 1을 곱한 값이 x0의 미분 결과가 되며, -1을 곱한 값이 x1의 미분 결과가  됩ㄴㅣ다. 코드로는 다음처럼 구현할 수 있습니다.

class Sub(Function):
  def forward(selfx0x1):
    y = x0 - x1
    return y

  def backward(selfgy):
    return gy, -gy

def sub(x0x1):
  x1 = as_array(x1)
  return Sub()(x0, x1)

Variable.__sub__ = sub

이제 x0와 x1이 Variable 인스턴스라면 y0 = x0 - x1 계산을 수행할 수 있습니다. 그러나 x0가 Variable 인스턴스가 아닌 경우, 예컨데 y = 2.0 - x 같은 코드는 제대로 처리할 수 없습니다. x의 __rsub__ 메서드가 호출되어 인수가 [그림 22-1]형태로 전달되기 때문이죠.


[그림 22-1]과 같이 __rsub__(self, other)가 호출될 땐느 우항인 x가 인수 self에 전달됩니다. 따라서 다음처럼 구현해야 합니다.

def rsub(x0x1):
  x1 = as_array(x1)
  return Sub()(x1, x0)    # x0 와 x1의 순서를 바꾼다.

Variable.__rsub__ = rsub

보다시피 함수 rsub(x0, x1)을 정의하고 인수의 순서를 바꿔서 Sub()(x1, x0)를 호출하게 합니다. 그런 다음 특수 메서드인 __rsub__에 함수 rsub를 할당합니다.


덧셈과 곰셈은 좌항과 우항의 순석를 바꿔도 결과가 같기 때문에 둘을 구별할 필요가 없었ㅅ브니다. 하지만 뺄셈에서는 좌우를 구별해야 합니다. (x0 - x1과 x1 - x0의 값은 다릅니다). 따라서 우항을 대상으로 했을 때 적용할 함수인 rsub(x0, x1)을 별도로 준비해야 합니다.


이상으로 뺄셈도 할 수 있게 되었습니다. 이제 다음 코드가 잘 작동합니다.

x = Variable(np.array(2.0))
y1 = 2.0 - x
y2 = x - 1.0
print(y1)
print(y2)