Variable 인스턴스 a와 b가 있을 때, y = a * b처럼 연산자에 대응하는 작업이 필요하다. 먼저 곱셈을 수행하는 클래스 Mul을 구현할 것이다.
연산자 오버로드(1)
1. Mul 클래스 구현
class Mul(Function):
def forward(self, x0, x1):
y = x0 * x1
return y
def backward(self, gy):
x0, x1 = self.inputs[0].data, self.inputs[1].data
return gy * x1, gy * x0
Mul 클래스를 파이썬 함수로 사용할 수 있도록 한다.
def mul(x0, x1):
return Mul()(x0, x1)
이제 mul함수를 사용하여 곱셈을 할 수 있다.
a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
c = Variable(np.array(1.0))
y = add(mul(a, b), c)
y.backward()
print(y) #variable(7.0)
print(a.grad) #2.0
print(b.grad) #3.0
그러나 매번 y = add(mul(a, b), c)처럼 코딩하기는 번거로우므로, y = a * b + c형태로 사용할 수 있게끔 Variable을 확장할 것이다. 이를 위해 연산자 오버로드(operator overload)를 이용할 것이다. 연산자 오버로드를 사용하면 +와 *같은 연산자 사용 시 사용자가 설정한 함수가 호출된다.
2. 연산자 오버로드
곱셈의 특수 메서드는 __mul__(self, other)이다. __mul__ 메서드를 구현하면 *연산자를 사용할 때 __mul__ 메서드가 호출된다. 즉, mul대신 *를 사용하여 연산할 수 있게 된다.
class Variable:
#생략
def __mul__(self, other):
return mul(self, other)
a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
y = a * b
print(y) #variable(6.0)
연산자 *의 왼쪽 a가 인수 self에 전달되고, 오른쪽의 b가 other에 전달된다.
이 코드를 아래와 같이 간단히 처리할 수도 있다.
class Variable:
#생략
Variable.__mul__ = mul
Variable.__add__ = add
덧셈 연산자 + 특수 메서드인 __add__ 도 설정하였다. 이제 +와 *연산자를 사용하여 계산할 수 있다.
a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
c = Variable(np.array(1.0))
#y = add(mul(a, b), c)
y = a * b + c
y.backward()
print(y) #variable(7.0)
print(a.grad) #2.0
print(b.grad) #3.0
연산자 오버로드(2)
이제 Variable 인스턴스에 a, b에 대해 a * b 또는 a + b같은 연산을 할 수 있다. 그러나 a * np.array(2.0)이나 3 + b처럼 ndarray 인스턴스 또는 수치 데이터와 함께 사용할 수 없다. 이번 단계에서는 Variable 인스턴스와 ndarray 인스턴스, 그리고 int나 float같은 수치 데이터도 같이 사용할 수 있게 할 것이다.
1. ndarray와 함께 사용하기
전략은 a * np.array(2.0)일 때, ndarray 인스턴스를 Variable 인스턴스로 변환하는 것이다. 즉, Variable(np.array(2.0))로 변환 할 것이다. 이를 위해, 주어진 객체를 Variable 인스턴스로 변환해주는 함수 as_variable를 구현할 것이다.
def as_variable(obj):
if isinstance(obj, Variable):
return obj
return Variable(obj)
이제 Function 클래스의 __call__ 메서드가 as_variable 함수를 이용하도록 코드를 추가한다.
class Function:
def __call__(self, *inputs):
inputs = [as_variable(x) for x in inputs]
xs = [x.data for x in inputs]
ys = self.forward(*ys)
#생략
확장된 DeZero를 사용해서 계산을 하면 다음과 같다.
x = Variable(np.array(2.0))
y = x + np.array(3.0)
print(y) #variable(5.0)
2. 수치 데이터 float, int와 함께 사용하기
x가 Variable 인스턴스일 때 x + 3.0을 실행할 수 있도록 하기 위해서, add 함수에 코드를 추가할 것이다.
def add(x0, x1):
x1 = as_array(x1)
return Add()(x0, x1)
as_array를 사용하여 ndarray 인스턴스로 변환한다. 그 후 Function 클래스에서 Variable로 변환되는 것이다.
3. DeZero 연산의 문제점
1)문제점1 : 첫 번째 인수가 float이나 int인 경우
현재 DeZero는 x * 2.0은 실행할 수 있지만 2.0 * x는 오류를 띄운다. 그 이유는 2.0 * x의 처리 과정을 보면 알 수 있다.
1. 연산자 왼쪽에 있는 2.0의 __mul__ 메서드 호출을 시도
2. 그러나 2.0은 float이므로 __mul__ 메서드가 구현되어있지 않음
3. 연산자 오른쪽에 있는 x의 특수 메서드 호출을 시도
4. x가 오른쪽에 있으므로 __mul__ 이 아닌 __rmul__ 메서드 호출을 시도
5. Variable 인스턴스에는 __rmul__ 메서드가 없음
따라서 __rmul__ 메서드를 구현하면 해결된다.
class Variable
#생략
Variable.__add__ = add
Variable.__radd__ = add #좌항과 우항을 바꾸어 더해도 결과가 같기 때문
Variable.__mul__ = mul
Variable.__rmul__ = mul #좌항과 우항을 바꾸어 곱해도 결과가 같기 때문
2) 문제점2: 좌항이 ndarray 인스턴스인 경우
ndarray 인스턴스가 좌항이고 Variable 인스턴스가 우항인 경우, 좌항의 ndarray 인스턴스의 __add__ 메서드가 호출된다. Variable 인스턴스의 __radd__ 메서드가 호출되기를 원하므로 '연산자 우선순위'를 지정해야 한다. 이를 위해 Variable 인스턴스 속성에 __array_priority__ 에 큰 값을 설정하여 추가한다.
class Variable:
__array_priority__ = 200
#생략
이제 우항인 Variable 인스턴스의 연산자 메서드가 우선적으로 호출된다.
연산자 오버로드(3)
*와 +연산자 이외에 다른 연산자를 DeZero에 추가할 것이다. 인수가 두 개인 것은 x0를 Variable 인스턴스로 가정하고, x1를 수치 데이터로 가정하겠다.
1. 음수(부호 변환)
class Neg(Function):
def forward(self, x):
return -x
def backward(self, gy):
return -gy
def neg(x):
return Neg()(x)
Variable.__neg__ = neg
x = Variable(np.array(2.0))
y = -x
print(y) #variable(-2.0)
2. 뺄셈
class Sub(Function):
def forward(self, x0, x1):
y = x0 - x1
return y
def backward(self, gy):
return gy, -gy
def sub(x0, x1):
x1 = as_array(x1)
return Sub()(x0, x1)
Variable.__sub__ = sub
뺄셈은 좌항, 우항이 있는 이항 연산자이므로 rsub 메서드를 구현해주어야 한다.
def rsub(x0, x1):
x1 = as_array(x1):
return Sub()(x1, x0) #부호를 올바르게 유지하도록 x0과 x1의 순서를 바꾼다
Variable.__rsub__ = rsub
x = Variable(np.array(2.0))
y1 = 2.0 - x
y2 = x - 1.0
print(y1) #variable(0.0)
print(y2) #variable(1.0)
3. 나눗셈
class Div(Function):
def forward(self, x0, x1):
y = x0 / x1
return y
def backward(self, gy):
x0, x1 = self.inputs[0].data, self.inputs[1].data
gx0 = gy / x1
gx1 = gy * (-x0 / x1 ** 2)
return gx0, gx1
def div(x0, x1):
x1 = as_array(x1)
return Div()(x0, x1)
def rdiv(x0, x1):
x1 = as_array(x1)
return Div()(x1, x0) #분자와 분모를 올바르게 유지하기 위해 x0, x1 순서 바꿈
Variable.__truediv__ = div
Variable.__rtruediv__ = rdiv
4. 거듭제곱
class Pow(Function):
def __init__(self, c):
self.c = c
def forward(self, x):
y = x ** self.c
return y
def backward(self, gy):
x = self.inputs[0].data
c = self.c
gx = c * x ** (c - 1) * gy
return gx
def pow(x, c):
return Pow(c)(x)
Variable.__pow__ = pow
x = Variable(np.array(2.0))
y = x ** 3
print(y) #variable(8.0)
'AI > 딥러닝 프레임워크 개발' 카테고리의 다른 글
25~26단계) 계산 그래프 시각화 , DOT (0) | 2021.06.29 |
---|---|
23~24단계) 패키지로 정리 , 복잡한 함수의 미분 (0) | 2021.06.29 |
19단계) 변수 사용성 개선 (0) | 2021.06.19 |
17~18단계) 메모리 관리 방식 , 순환 참조 , 메모리 절약 모드 (0) | 2021.06.19 |
15~16단계) 복잡한 계산 그래프의 이론 및 구현 (0) | 2021.06.15 |
댓글