20. 연산자 오버로드(1)¶
- 이전 단계부터 Variable을 '투명한 상자'로 만드는 작업ㅇ르 시행했지만 아직 + 와 * 같은 연산자에 대응하는 작업이 남아 있음.
- 예컨대 Variable 인스턴스 a와 b가 있을 때 y = a*b 처럼 코딩할 수 있으면 아주 유용한데, 이렇게 확장하는 것이 이번 장의 목표
- 궁극적인 목표는 Variable 인스턴스를 ndarray 인스턴스처럼 '보이게' 만드는 것. 이렇게 하면 DeZero를 평범한 넘파이 코드를 작성하듯 사용할 수 있어 넘파이를 사용해본 사람들이 아주 쉽게 배울 수 있음
- 이제부터 +와 * 연산자를 지원하도록 Variable을 확장함. 그 첫 번째로 곱셈을 수행하는 함수를 구현함 (덧셈은 11단계에서 구현함).
20.0 지금까지의 구현¶
In [1]:
import weakref
import numpy as np
import contextlib
In [2]:
class Config:
enable_backprop = True
In [3]:
@contextlib.contextmanager
def using_config(name, value):
old_value = getattr(Config, name)
setattr(Config, name, value)
try:
yield
finally:
setattr(Config, name, old_value)
In [4]:
def no_grad():
return using_config('enable_backprop', False)
In [5]:
class Variable:
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.name = name
self.grad = None
self.creator = None
self.generation = 0
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def size(self):
return self.data.size
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
if self.data is None:
return 'variable(None)'
p = str(self.data).replace('\n', '\n' + ' ' * 9)
return 'variable(' + p + ')'
def set_creator(self, func):
self.creator = func
self.generation = func.generation + 1
def cleargrad(self):
self.grad = None
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = []
seen_set = set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f)
funcs.sort(key=lambda x: x.generation)
add_func(self.creator)
while funcs:
f = funcs.pop()
gys = [output().grad for output in f.outputs]
gxs = f.backward(*gys)
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x, gx in zip(f.inputs, gxs):
if x.grad is None:
x.grad = gx
else:
x.grad = x.grad + gx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for y in f.outputs:
y().grad = None
In [6]:
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
In [7]:
class Function:
def __call__(self, *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]
def forward(self, xs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
In [8]:
class Exp(Function):
def forward(self, x):
y = np.exp(x)
return y
def backward(self, dy):
x = self.input.data
dx = np.exp(x) * dy
return dx
def exp(x):
return Exp()(x)
In [9]:
class Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, gy):
x = self.inputs[0].data
gx = 2 * x * gy
return gx
def square(x):
return Square()(x)
In [10]:
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
def backward(self, gy):
return gy, gy
def add(x0, x1):
return Add()(x0, x1)
20.1 Mul 클래스 구현¶
- 곱셈의 미분은 $y=x_0 \times x_1$ 일 때 $\frac{\partial y}{\partial x_0} = x_1, \frac{\partial y}{\partial x_1} = x_0$가 됨.
- 이에 대한 역전파는 최종 출력을 $L$ 이라고 할 경우 $L$의 미분을, 정확하게는 $L$의 각 변수에 대한 미분을 전파함.
- 이 때 변수 $x_0$와 $x_1$에 대한 미분은 각각 $\frac{\partial L}{\partial x_0} = x_1 \frac{\partial L}{\partial y}$과 $\frac{\partial L}{\partial x_1} = x_0 \frac{\partial L}{\partial y}$
- 우리는 스칼라를 출력하는 합성 함수에 관심이 있음. 그래서 마지막에 $L$이라는 스칼라를 출력하는 합성함수를 가정함. 여기서 $L$은 오차, 다른 말로 손실(loss)를 뜻함
- 이를 참고하여 Mul 클래스를 구현
In [11]:
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
def mul(x0, x1):
return Mul()(x0, x1)
- 이제 Mul 함수를 사용하여 곱셈이 가능함. 예를 들어 다음과 같이 코드 작성 가능
In [12]:
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)
print(a.grad)
print(b.grad)
- 이와 같이 add 함수와 mul 함수를 함께 사용할 수 있게 됨. 이 때 미분도 자동으로 이루어짐. 다만, 매번 y=add(mul(a,b),c)처럼 코딩하기는 번거로움. 지금보다는 y = a*b+c 형태가 훨씬 깔끔함.
- 그래서 +와 * 연산자를 사용할 수 있도록 Variable을 확장함. 이를 위해 연산자 오버로드를 이용함
- 연산자를 오버로드하면 +와 *같은 연산자 사용 시 사용자사 설정한 함수가 호출됨. 파이썬에선
__add__
와__mul__
같은 특수 메서드를 정의함으로써 사용자 지정 함수가 호출되도록 함
- 연산자를 오버로드하면 +와 *같은 연산자 사용 시 사용자사 설정한 함수가 호출됨. 파이썬에선
20.2 연산자 오버로드¶
- 먼저 곱셈 연산자 를 오버로드함. 곱셈의 특수 메서드는
__mul__(self, other)
임. 해당 메서드를 정의(구현)하면 연산자를 사용할 때 메서드가 호출됨.
In [13]:
class Variable:
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.name = name
self.grad = None
self.creator = None
self.generation = 0
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def size(self):
return self.data.size
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
if self.data is None:
return 'variable(None)'
p = str(self.data).replace('\n', '\n' + ' ' * 9)
return 'variable(' + p + ')'
def set_creator(self, func):
self.creator = func
self.generation = func.generation + 1
def cleargrad(self):
self.grad = None
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = []
seen_set = set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f)
funcs.sort(key=lambda x: x.generation)
add_func(self.creator)
while funcs:
f = funcs.pop()
gys = [output().grad for output in f.outputs]
gxs = f.backward(*gys)
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x, gx in zip(f.inputs, gxs):
if x.grad is None:
x.grad = gx
else:
x.grad = x.grad + gx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for y in f.outputs:
y().grad = None
def __mul__(self, other): # 해당 부분 추가
return mul(self, other)
In [14]:
a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
y = a*b
print(y)
- 해당 코드를 더 간단하게 구현 가능함. 덧셈도 같이 구현
In [15]:
Variable.__mul__ = mul
Variable.__add__ = add
- Variable 클래스를 정의한 후
Variable.__mul__ = mul
이라고 작성하면 끝. 파이썬에서는 함수도 객체이므로 이와 같이 함수 자체를 할당할 수 있음.
In [16]:
a = Variable(np.array(3.0))
b = Variable(np.array(2.0))
c = Variable(np.array(1.0))
y = a*b+c
y.backward()
print(y)
print(a.grad)
print(b.grad)
print(c.grad)
21. 연산자 오버로드(2)¶
- Variable 인스턴스 a와 b가 있을 때 ab 혹은 a+b 같은 코드도 작성할 수 있음. 하지만 anp.numpy(2.0)처럼 ndarray 인스턴스와 함께 사용할 수는 없음. 3+b처럼 수치 데이터도 함께 사용할 수 없음.
- ndarray 인스턴스와 수치 데이터와도 함께 사용할 수 있으면 Dezero가 더욱 편리해질 것. 따라서 이번 단계에서는 Variable 인스턴스와 ndarray 인스턴스, int나 float 등도 함께 사용할 수 있도록 작업
21.1 ndarray와 함께 사용하기¶
- 우선 Variable을 ndarray 인스턴스와 함께 사용할 수 있게 함. 예를 들어, a가 Variable 인스턴스일 때 a * np.array(2.0)이라는 코드를 만나면 ndarray 인스턴스를 자동으로 Variable 인스턴스로 변환하는 것. 즉, Varaible(np.array(2.0))으로 변환하면 그 다음 계산은 지금까지와 같음
- 이를 위한 사전 준비로
as_variable
이라는 편의 함수를 준비함. 인수로 주어진 객체를 Variable 인스턴스로 변환해주는 함수.
In [17]:
def as_variable(obj):
if isinstance(obj, Variable):
return obj
return Variable(obj)
- 이 함수는 인수 obj가 Variable 인스턴스 또는 ndarray 인스턴스라고 가정함. obj가 Variable 인스턴스면 아무것도 손보지 않고 그대로 반환하고, 그렇지 않으면 Variable 인스턴스로 변환하여 반환
- 그럼 Function 클래스의
__call__
메서드가as_variable
함수를 이용하도록 다음 코드를 추가함.
In [18]:
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]
def forward(self, xs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
- 이와 같이 인수 inputs에 담긴 각각의 원소 x를 Variable 인스턴스로 변환함. 따라서 ndarray 인스턴스가 주어지면 Variable 인스턴스로 변환됨. 그러면 이후의 처리는 모든 변수가 Variable 인스턴스인 상태로 진행됨.
- DeZero에서 사용하는 함수(연산)는 Function 클래스를 상속하므로 실제 연산은 Function 클래스의
__call__
메서드에서 이루어짐. 따라서 위 메서드에서 가한 수정은 DeZero에서 사용하는 모든 함수에 적용됨
- DeZero에서 사용하는 함수(연산)는 Function 클래스를 상속하므로 실제 연산은 Function 클래스의
- 새로운 DeZero를 사용하여 계산 해봄
In [19]:
'''###################################
흐름과는 상관없이 오류 때문에 재생한 코드
###################################'''
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
def backward(self, gy):
return gy, gy
def add(x0, x1):
return Add()(x0, x1)
In [20]:
x = Variable(np.array(2.0))
y = x + np.array(3.0)
print(y)
21.2 float, int와 함께 사용하기¶
- 이어서 파이썬의 float와 int, 그리고 np.float64와 np.int64 같은 타입과도 함께 사용할 수 있도록 함.
- x가 Variable 인스넌스 일 때 x + 3.0 같은 코드를 실행할 수 있도록 하려면 add 함수에 다음 코드를 추가하는 방법이 있음
In [21]:
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
def backward(self, gy):
return gy, gy
def add(x0, x1):
x1 = as_array(x1) # 해당 부분 추가
return Add()(x0, x1)
- mul도 추가
In [22]:
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
def mul(x0, x1):
x1 = as_array(x1) # 해당 부분 추가
return Mul()(x0, x1)
In [23]:
'''###################################
흐름과는 상관없이 오류 때문에 재생한 코드
###################################'''
Variable.__mul__ = mul
Variable.__add__ = add
In [24]:
x = Variable(np.array(2.0))
y = x + 3.0
print(y)
21.3 문제점 1: 첫 번째 인수가 float나 int 인 경우¶
- x 2.0은 계산 가능하지만 2.0 x는 계산 불가능함
- 이를 해결하기 위해
__rmul__
,__radd__
메서드 추가
In [25]:
Variable.__mul__ = mul
Variable.__add__ = add
Variable.__rmul__ = mul
Variable.__radd__ = add
In [26]:
x = Variable(np.array(2.0))
y = 3.0 * x + 1.0
print(y)
21.4 문제점 2: 좌항이 ndarray 인스턴스인 경우¶
- ndarray 인스턴스가 좌항이고 Variable 인스턴스가 우항인 경우에 문제 발생
- 이렇게 되면 좌항인 ndarray 인스턴스의
__add__
메서드가 호출됨. 하지만 우리는 우항인 Varaible 인스턴스의__radd__
메서드가 호출되길 원하므로, 연산자 우선순위를 지정해야 함. - 구체적으로는 Variable 인스턴스의 속성에
__array_priority__
를 추가하고 그 값을 큰 수로 설정함.
In [27]:
class Variable:
__array_priority__ = 200 # 해당 부분 추가
def __init__(self, data, name=None):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.name = name
self.grad = None
self.creator = None
self.generation = 0
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def size(self):
return self.data.size
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
if self.data is None:
return 'variable(None)'
p = str(self.data).replace('\n', '\n' + ' ' * 9)
return 'variable(' + p + ')'
def set_creator(self, func):
self.creator = func
self.generation = func.generation + 1
def cleargrad(self):
self.grad = None
def backward(self, retain_grad=False):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = []
seen_set = set()
def add_func(f):
if f not in seen_set:
funcs.append(f)
seen_set.add(f)
funcs.sort(key=lambda x: x.generation)
add_func(self.creator)
while funcs:
f = funcs.pop()
gys = [output().grad for output in f.outputs]
gxs = f.backward(*gys)
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x, gx in zip(f.inputs, gxs):
if x.grad is None:
x.grad = gx
else:
x.grad = x.grad + gx
if x.creator is not None:
add_func(x.creator)
if not retain_grad:
for y in f.outputs:
y().grad = None
- 이렇게 하면 Variable 인스턴스의 연산자 우선순위를 ndarray 인스턴스의 연산자 우선순위보다 높일 수 있음. 그 결과 좌항이 ndarray 인스턴스라 해도 우항인 Variable 인스턴스의 연산자 메서드가 우선적으로 호출됨.
- 이상이 연산자 오버로드의 조심해야 할 핵심. 마침내 DeZero는 * 와 + 연산자를 서로 다른 타입과 섞어 사용할 수 있게 됨. 다음 단계에서는 / 와 - 와 같은 다른 연산자를 추가함
22. 연산자 오버로드(3)¶
- 이전 단계에서는 DeZero가 * 와 + 연산자를 지원하도록 확장했는데, 연산자는 이 밖에도 많이 존재함.
- 연산자를 추가하는 순서
- Function 클래스를 상속하여 원하는 함수 클래스를 구현함(ex: Mul 클래스)
- 파이썬 함수로 사용할 수 있도록 함(ex: mul 함수)
- Variable 클래스의 연산자를 오버로드함(ex: Variable.mul = mul)
22.1 음수(부호변환)¶
- 음수의 미분은 $y = -x$ 일때 $\frac{\partial y}{\partial x} = -1$ 임. 따라서 역전파는 상류(출력 쪽)에서 전해지는 미분에 -1을 곱하여 하류로 흘려보내 주면 됨. 그러면 다음과 같이 구현
In [28]:
class Neg(Function):
def forward(self, x):
return -x
def backward(self, gy):
return -gy
def neg(x):
return Neg()(x)
Variable.__neg__ = neg
In [29]:
x = Variable(np.array(2.0))
y = -x # 부호 바꿈
print(y)
22.2 뺄셈¶
- 뺄셈의 미분은 $y=x_0 - x_1$ 일 때 $\frac{\partial y}{\partial x_0} = 1, \frac{\partial y}{\partial x_1} = -1$ 임. 따라서 역전파는 상류에서 전해지는 미분값에 1을 곱한 값이 $x_0$ 의 미분 결과가 되며, -1을 곱한 뒤 값이 $x_1$의 미분 결과가 됨.
In [30]:
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)
def rsub(x0, x1):
x1 = as_array(x1)
return Sub()(x1, x0)
Variable.__sub__ = sub
Variable.__rsub__ = rsub
- 덧셈과 곱셈은 좌항과 우항의 순서를 바꿔도 결과가 같으므로 둘을 구별할 필요가 없지만, 뺄셈에서는 구별해야 함. 따라서 rsub 함수를 별도로 준비
In [31]:
x = Variable(np.array(2.0))
y1 = 2.0 - x
y2 = x - 1.0
print(y1)
print(y2)
22.3 나눗셈¶
- 나눗셈의 미분은 $\frac{x_0}{x_1}$ 일 때 $\frac{\partial y}{\partial x_0} = \frac{1}{x_0}, \frac{\partial y}{\partial x_1} = -\frac{x_0}{(x_1)^2}$
In [32]:
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)
Variable.__truediv__ = div
Variable.__rtruediv__ = div
- 나눗셈도 뺄셈과 마찬가지로 좌/우항 중 어느 것에 적용할지에 따라 적용되는 함수가 다름.
22.4 거듭제곱¶
- 거듭제곱은 $y=x^c$ 형태로 표현됨. 이 때 x를 밑이라 하고 c를 지수라 함. 거듭제곱의 미분은 $\frac{\partial y}{\partial x} = cx^{c-1}$이 됨. 지수 c는 상수로 취급하여 따로 미분을 계산하지 않기로 하고 구현
In [33]:
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
- 코드를 보면 Pow 클래스를 초기화 할 때 지수 c를 제공할 수 있음. 그리고 순전파 메서드인
forward(x)
는 밑에 해당하는 x만(즉, 하나의 항만) 받게 함.
In [34]:
x = Variable(np.array(2.0))
y = x ** 3
print(y)
- 이상으로 목표한 연산자를 모두 추가함. 이번 단계를 통해 DeZero의 유용성을 크게 향상함.
In [ ]:
'기계학습 > 밑바닥딥러닝3 오독오독 씹기' 카테고리의 다른 글
Chapter 3. 고차 미분 계산(step 25~26)/ 계산 그래프 시각화 (0) | 2021.01.26 |
---|---|
Chapter 2. 자연스러운 코드로(step 23~24)/복잡한 함수 미분 (1) | 2021.01.21 |
Chapter 2. 자연스러운 코드로(step 17~19) (0) | 2020.12.29 |
Chapter 2. 자연스로운 코드로(step 16) (0) | 2020.12.16 |
Chapter 2. 자연스러운 코드로(step 11~14) (0) | 2020.12.07 |