본문 바로가기
Study 공부할레나

[스터디/밑시딥3] 제 2고지 자연스러운 코드로_21. 연산자 오버로드(2)

by LENA-cfg 2023. 8. 27.

현재 DeZero는 a * np.array(2.0)처럼 ndarray 인스턴스와 함께 사용할 수 없습니다.

또한, 3+b처럼 수치 데이터도 함께 사용 할 수 없습니다.

 

이번 단계는

1. Variable 인스턴스와 ndarray 인스턴스를 함께 사용하기

2. float, int와 함께 사용하기를 구현하겠습니다.

21.1 ndarray와 함께 사용하기

전략: a가 Variable 인스턴스 일때 a * np.array(2.0)이면 ndarray 인스턴스를 Variable 인스턴스로 변환

 

 

ndarray 인스턴스를 Variable로 바꿔주는 as_variable 함수를 만들어줍니다.

def as_variable(obj):
    if isinstance(obj, Variable):
        return obj
    return Variable(obj)

 

as_variable 함수는 인수로 obj를 받아 obj가 Variable의 인스턴스인지 확인하고

Variable의 인스턴스이면 obj를 그대로 반환, 인스턴스가 아니라면 Variable 상자에 담아 리턴합니다.

 

이 함수를 이용하려면 Function 클래스의  __call__메서드에 코드를 추가해야 합니다.

 

왜 __call__메서드에서 as_variable 함수를 사용할까요?

DeZero에서 사용하는 모든 함수(연산)는 Function 클래스를 상속하기 때문에 실제 연산이 Function 클래스의 __call__메서드에서 진행됩니다.

그래서 __call__메서드에서 수정된 부분이 DeZero에서 사용되는 모든 함수에 적용됩니다. 

 

Function 클래스 inputs 코드 추가

class Function:
    def __call__(self, *inputs):
        inputs = [as_variable(x) for x in inputs] # 코드 추가

        xs = [x.data for x in inputs]
        ys = self.forward(*xs)
        if not isinstance(ys, tuple):
            ys = (ys,)
        outputs = [Variable(as_array(y)) for y in ys]

        if Config.enable_backprop:
            self.generation = max([x.generation for x in inputs])
            for output in outputs:
                output.set_creator(self)
            self.inputs = inputs
            self.outputs = [weakref.ref(output) for output in outputs]

        return outputs if len(outputs) > 1 else outputs[0]

inputs = [as_variable(x) for x in inputs]

추가된 코드는 인풋들을 반복문으로 돌려서 나온 x를 as_variable 함수에 인자로 넣어준후 리스트 안에 보관합니다.

inputs = [Variable(x1), Variable(x2) ...] 이러한 모양이 될 것입니다.

 

 

실제 적용

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

21.2 float, int와 함께 사용하기

파이썬 자료형 float, int와 넘파이 자료형 np.float64, np.int64같은 타입도 사용할 수 있도록 코드를 수정해줍니다.

def as_array(x):
    if np.isscalar(x):
        return np.array(x)
    return x
    
def add(x0, x1):
    x1 = as_array(x1)
    return Add()(x0, x1)
    
def mul(x0, x1):
    x1 = as_array(x1)
    return Mul()(x0, x1)

생각보다 간단하게 수정되었는데요.

기존에 as_array함수를 사용해 인수로 들어온 x가 넘파이 스칼라(=단순한 숫자값)이면 np.array()에 x를 넣어 넘파이 배열로 반환합니다. x가 이미 배열이라면 x를 그대로 반환합니다.

 

as_array 함수를 각각의 연산 함수 안에 추가하여 x1값에 대해 넘파이 배열로 받을 수 있게 수정해줍니다.

 

실제 적용

x = Variable(np.array(2.0))
y = x + int(3)
print('y:', y)

y1 = x + float(3.0)
print('y1:', y1)

x1 = np.array([1.0, 2.0, 3.0], dtype=np.float64)
y3 = x1 + np.array(3)
print('y3:', y3)

x2 = np.array([1, 2, 3], dtype=np.int64)
y4 = x2 + np.array(3)
print('y4:', y4)

21.3 첫번째 문제점: 첫 번째 인수가 float이나 int인경우

y = 2.0 * x 일때 처음 받는 인수가 float이나 int면 바로 오류가 발생합니다.

 

오류 발생 과정

1) *의 왼쪽 2.0이 __mul__호출 시도

2) 2.0은 float타입이라 __mul__메서드가 없음 => 실패

3) * 오른쪽 x의 특수 메서드(=__rmul__) 호출 시도 

4) Variable 인스턴스에 __rmul__메서드 구현 안됨 => 실패

 

해결방법

항 위치에 따라 호출되는 특수 메서드를 다 구현하면 됩니다.

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

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

21.4 두번째 문제점: 좌항이 ndarray 인스턴스인 경우

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

위처럼 좌항이 ndarray 인스턴스이고 우항이 Variable 인스턴스 일때 

좌항 ndarray 인스턴스의 __add__ 메서드가 호출되는데

Variable 인스턴스 __radd__가 호출되기 원하기 때문에

 

'연산자 우선순위'를 지정해줍니다.

class Variable:
    __array_priority__ = 200

연산자 우선 순위는 Variable 클래스에 __array_priority__를 추가하고 큰 정수값을 설정해주면

Variable 인스턴스의 연산자 메서드가 우선적으로 호출됩니다.

 

즉, 이렇게 되면 ndarray의 인스턴스가 앞에 나와도 __array_priority__값이 높은 Variable 객체의 __add__메서드가 호출됩니다.