본문 바로가기
AI/딥러닝 프레임워크 개발

20~22단계) 연산자 오버로드

by 채채씨 2021. 6. 19.
728x90
반응형

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)

 

728x90
반응형

댓글