페이지

2022년 8월 31일 수요일

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)

2022년 8월 28일 일요일

22.1 음수(부호 변환)

 음수의 미분은 y = -x 일때 dy/dx = -1입니다. 따라서 역전파는 상류(출력 쪽)에서 전해지는 미분에 -1을 곱하여 하류로 흘려보내주면 됩니다. 그러면 다음과 같이 구현할 수 있습니다.


이와 같이 Neg 클래스를 구현한 다음, 파이썬 함수로 사용할 수 있도록 neg 함수도 구현합니다. 그리고 특수 메서드인 __neg__에 neg를 대입하면 완성입니다. 이제 다음 코드를 실행할 수 있습니다.



다음은 '뺄셈' 차례입니다.



2022년 8월 27일 토요일

STEP 22 연산자 오버로드(3)

 이전 단계에서는 DeZero가 *와 +연산자를 지원하도록 확장했는데, 연산자는 이밖에도 많습니다. 그래서 이번 단계에서는 [표 22-1]의 연산자들을 추가하겠습니다.


[표 22-1]의 첫 번째 메서드인 __neg__(self)는 양수를 음수로, 혹은 음수를 양수로 바꿔주는 부호 변환 연산자입니다. 또한 다른 연산자들과 달리 항이 하나뿐인 '단항 연산자'입니다. 그래서 특수 메서드인 인수도 하나뿐이죠, 나머지 연산자들은 차례로 뺄셈, 나눗셈, 거듭제곱으로, 모두 이항 연산자입니다( a - b, a / b등), 따라서 적용 대상이 우항이냐 좌항이냐에 따라 2개의 특수 메서드 중 하나가 선별되어 호출됩니다.단, 거듭제곱은 x ** 3 처럼 좌항이 Variable 인스턴스이고 우항이 상수(2, 3 등의 int)인 경우만을 고려하겠습니다.

파이썬 연산자는 [표22-1]외에도 몇 가지 더 있습니다. 옐르 들어 a // b 와 a % b 같은 연산자도 있고, a +=1 과 a -= 2같은 연산자도 있습니다. 이 책에서는 자주 사용하는 연산자만 선별하여 구현할 테니, 다른 연산자는 독자 여러분이 필요하면 추가하기 바랍니다. 한 가지 더, 이번 단계는 단조로운 작업이기 때문에 생략해도 무방합니다.


그러면 작업을 시작해 볼까요? 우선 복습도 할 겸 새로운 연산자를 추가하는 순서를 살펴보죠,

1. Function 클래스를 상속하여 원하는 함수 클래스를 구현합니다(예: Mul클래스).

2. 파이썬 함수로 사용할 수 있도록 합니다(예: mul 함수).

3. Variable 클래스의 연산자를 오버로드합니다(예: Variable.__mul__ = mul).

이번 단계에서도 똑같은 과정을 거쳐 새로운 연ㅅ간자들을 추가할 것입니다. '부호 변환'부터 시작해 보죠.

21.4 문제점 2: 좌항이 ndarray인스턴스인 경우

 남은 문제는 ndarray 인스턴스가 좌항이고 Variable 인스턴스가 우항인 경우입니다. 예를 들어 다음과 같은 코드입니다.

x = Variable(np.array([1.0]))
y = np.array([2.0] * x)

이 예에서 좌항은 ndarray 인스턴스이고 우항은 Variable 인스턴스입니다. 이렇게 되면 좌항인  ndarray 인스턴스의 __add__ 메서드가 호출됩니다. 하지만 우리는 우항인 Variable 인스턴스의 __radd__ 메서드가 호출되길 원합니다. 그러면서 '연산자 우선순위'를 지정해야 합니다. 구체적으로 Variable 인스턴스의 속성에 __array_priority__를 추가하고 그 값을 큰 정수로 설정해야 합니다. 다음처럼 마리죠.

class Variable:
  __array_priority__ = 200

이렇게 하면 Variable 인스턴스의 연산자 우선순위를 ndarray 인스턴스의 연산자 우선순위보다 높일 수 있습니다. 그 결과 좌항이 ndarray 인스턴스라 해도 우한인 Variable 인스턴스의 연산자 메서드가 우선적으로 호출됩니다.

이상이 연산자 오버로드 시 조심해야 할 핵심입니다. 마침내 DeZero는 *와 + 연산자를 서로 다른 타입과 섞어 사용할 수 있게 되었습니다. 이어서 다음 단계에서는 /와 - 같은 다른 연사자를 추가하겠습니다.

21.3 문제점1: 첫 번째 인수가 flaot나 int인 경우

 현재의 DeZero는 x * 2.0이라는 코드를 제대로 실행할 수 있습니다(x는 Variable 인스턴스). 하지만 2.0 * x를 실행하면 오류가 납니다. 어떤 오류가 나는지 볼까요?

y = 2.0 * x

원인은 2.0 * x를 실행했을 때 오류가 발생하는 과정을 보면 알수 있습니다. 2.0 * x 는 다음 순서로 처리됩니다.

1. 연산자 왼쪽에 있는 2.0의 __mul__메서드를 호출하려 시도한다.

2. 하지만 2.0은  float 타입이므로 __mul__ 메서드는 구현되어 있지 않다.

3. 다음은 * 연산자 오른쪽에 있는 x의 특수 메서드를 호출하려 시도한다.

4. x가 오른쪽에 있기 때문에 (__mul__대신) __mul__ 메서드를 호출하려 시도한다.

5. 하지만 Variable 인스턴스에는 __mul__메서드가 구현되어 있지 않다.

이상이 오류 발생 과정입니다. 핵심은 * 같은 이항 연산자의 경우 피연산자(항)의 위치에 따라 호출되는 특수 메서드가 다르다는 것입니다. 곱셈의 경우 피연사자가 촤항이면 __mul__메서드가 호출되고, 우항이면 __rmul__메서드가 호출됩니다.

따라서 이번 문제는 __rmul__메서드를 구현하면 해결됩니다. 이때 __rmul__메서드의 인수는 [그림 21-1]처럼 전달됩니다.

Variable.__add__ = add
Variable._radd__ =  add
Variable.__mul__ = mul
Variable.__rmul__ = mul

[그림 21-1]과 같이 __rmul__(self, other)의 인수 중 self는 자신인 x에 대응하고, other는  다른 쪽 항인 2.0에 대응합니다. 그런데 곱셈에서는 좌항과 우항을 바꿔도 결과가 같기 때문에 둘을 구별할 필요가 없습니다(2.0 * x 와 x * 2.0의 결과가 똑같죠). 덧셈도 마찬가지이므로 + 와 * 의 특수 메서드는 다음처럼 설정하면 됩니다.


이제 float와 int를 자류롭게 계산할 수 있습니다. 연습삼아 실제로 해보죠

x = Variable(np.array(2.0))
y = 3.0 * x + 1.0
print(y)

이제 Variable 인스턴스와 float, int 를 함께 사용할 수 있습니다. 이어서 나머지 문제 하나를 마저 해결해보죠.




21.2 float, int와 함께 사용하기

 이어서 파이썬의 float와 int, 그리고 np.float64와 같은 타입과도 함께 사용할 수 있도록 하겠습니다. x가 Variable 인스턴스일 때 x + 3.0 같은 코드를 실행할 수 있도록 하러면 어떻게 해야 할까요? 한가지 방법으로 add 함수에 다음의 음영부분의 코드를 추가하는 방법이 떠오르는군요.

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

여기에서는 as_array 함술르 사용했씁니다. 9단계에서 구현한 함수죠. as_array를 사용하면 x1이 float나 int인 경우 ndarray인스턴스로 변환됩니다. 그리고 ndarray 인스턴스는 (이후에)Function 클래스에서 Variable 인스턴스로 변환됩니다. 이것으로 다음과 같은 코드르 작성할 수 있게 됐습니다.

x = Variable(np.array(2.0))
y = x + 3.0
#y = add(x, 3.0)
print(y)

이와같이 float와 Variable인스턴스를 조합한 계산이 가능해졌습니다. 여기서는 add함수만 대표로 수정해봤지만, mul과 같은 다른 함수들도 같은 방식으로 수정할 수 있습니다. 다 수정하고 나면 +나 *로 Variable 인스턴스, float, int를 조합하여 계산할 수 있습니다. 그런데 실은 지금의 방식에는 두가지 문제가 남아 있습니다.