11. 가변 길이 인수(순전파편)¶
- 지금까지 우리는 함수에 입출력 변수가 하나씩인 경우만 생각함
- 그러나 함수에 따라 여러 변수를 입력받기도 함(반대로 출력이 여러개이기도)
- 이를 고려하여 DeZero가 가변 길이 입출력을 처리할 수 있도록 확장
11.0 지금까지의 구현¶
In [1]:
import numpy as np
In [2]:
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
def square(x):
return Square()(x)
def exp(x):
return Exp()(x)
In [3]:
class Variable:
def __init__(self, data):
if data is not None: # 해당 코드 추가
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funcs.pop()
x, y = f.input, f.output
x.grad = f.backward(y.grad)
if x.creator is not None:
funcs.append(x.creator)
In [4]:
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(as_array(y))
output.set_creator(self)
self.input = input
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, x):
raise NotImplementedError()
In [5]:
class Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, dy):
x = self.input.data
dx = 2 * x * dy
return dx
In [6]:
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
11.1 Function 클래스 수정¶
- 가변 길이 입출력을 표현하려면 변수들을 리스트(또는 튜플)에 넣어 처리하면 편함
- 즉, Function 클래스는 지금까지처럼 '하나의 인수'만 받고 '하나의 값'만 반환
- 대신 인수와 반환값의 타입을 리스트로 변경하고, 필요한 변수들을 이 리스트에 넣음
- 현재의 Function 클래스 구현 확인
In [7]:
class Function:
def __call__(self, input):
x = input.data # 1.
y = self.forward(x) # 2.
output = Variable(as_array(y)) # 3.
output.set_creator(self) # 4.
self.input = input
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, x):
raise NotImplementedError()
- Function의
__call__
메서드는- Variable이라는 '상자'에서 실제 데이터를 꺼낸 다음
forward
메서드에서 구체적인 계산을 함- 계산 결과를 Variable에 넣고
- 자신이 '창조자'라고 원산지 표시를 함.
- 이상의 로직을 염두해 두고
__call__
메서드의 인수와 반환값을 리스트로 바꿔보자
In [8]:
class Function:
def __call__(self, inputs):
xs = [x.data for x in inputs]
ys = self.forward(xs)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs
def forward(self, xs):
raise NotImplementedError()
def backward(self, dys):
raise NotImplementedError()
- 인수와 반환값을 리스트로 변경함. 변수를 리스트에 담아 취급한다는 점을 제외하고는 달라진게 없음
- 참고로 앞의 코드에서는 리스트를 생성할 때 리스트 컴프리헨션(List Comprehension)을 사용함
- 이어서 새로운 Function 클래스를 사용하여 구체적인 함수를 구현
- 첫 번째는 덧셈을 해주는 Add 클래스
11.2 Add 클래스 구현¶
- Add 클래스의 Forward 메서드를 구현함
- 주의할 점은 인수와 반환값이 리스트(또는 튜플)여야 한다는 것. 이 조건을 반영하여 구현
In [9]:
class Add(Function):
def forward(self, xs):
x0, x1 = xs
y = x0 + x1
return (y,) # 쉼표를 써서 튜플을 유지함
- Add 클래스의 인수는 변수가 두 개 담긴 리스트
- 따라서,
x0, x1 = xs
형태로 리스트 xs에서 원소 두 개를 꺼냄. 그런 다음 꺼낸 원소들을 사용하여 계산 - 결과를 반환할때는
return (y,)
형태로 튜플을 반환함 (return y,
처럼 괄호는 생략가능)
In [10]:
xs = [Variable(np.array(2)), Variable(np.array(3))] # 리스트로 준비
f = Add()
ys = f(xs)
y = ys[0]
print(y.data)
- 이처럼 DeZero도 덧셈 계산을 제대로 처리할 수 있게 됨. 입력을 리스트로 바꿔서 여러 개의 변수를 다룰 수 있게 하였고, 출력은 튜플로 바꿔서 역시 여러 개의 변수에 대응할 수 있게 만듦
- 이제 순전파에 한해서는 가변 길이 인수와 반환값에 대응할 수 있는 것
- 이를 조금 더 자연스로운 코드로 쓸 수 있도록 구현을 개선함
12. 가변 길이 인수(개선 편)¶
- DeZero를 더 쉽게 사용할 수 있도록 두 가지를 개선함
- Add 클래스(혹은 다른 구체적인 함수 클래스)를 '사용하는 사람'을 위한 개선
- '구현하는 사람'을 위한 개선
12.1 첫 번째 개선: 함수를 사용하기 쉽게¶
- 리스트나 튜플을 거치지 않고 인수와 직접 결과를 주고 받는 것이 더 자연스러움
- 코드를 이와 같은 형태로 작성할 수 있게 해주는 것이 첫 번째 개선
- 이를 위해 Function 클래스를 수정함
In [11]:
class Function:
def __call__(self, *inputs): # 1. 별표를 붙임
xs = [x.data for x in inputs]
ys = self.forward(xs)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
# 2. 리스트의 원소가 하나라면 첫 번째 원소를 반환함
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, xs):
raise NotImplementedError()
def backward(self, dys):
raise NotImplementedError()
- #2 부분부터 설명하면, 함수의 반환값이 하나라면 해당 변수를 직접 돌려줌
- #1 부분의 경우, 인수 앞에
*
를 붙이면 리스트를 사용하면 대신 임의 개수의 인수(가변 길이 인수)를 건네 함수를 호출할 수 있음 - 첫 번째 개선 후 사용
In [12]:
'''###################################
흐름과는 상관없이 오류 때문에 재생한 코드
###################################'''
class Add(Function):
def forward(self, xs):
x0, x1 = xs
y = x0 + x1
return (y,) # 쉼표를 써서 튜플을 유지함
In [13]:
x0 = Variable(np.array(2))
x1 = Variable(np.array(5))
f = Add()
y = f(x0, x1)
print(y.data)
12.2 두 번째 개선: 함수를 구현하기 쉽도록¶
- Add 클래스를 '구현하는 사람'을 위한 개선
- 현재 인수가 리스트로 전달되고, 결과로 튜플을 반환하는 형태에서 입력도 변수를 직접 받고 결과도 변수를 직접돌려주는 형태로 변경
In [14]:
class Function:
def __call__(self, *inputs):
xs = [x.data for x in inputs]
ys = self.forward(*xs) # 1. 별표를 붙여 언팩
if not isinstance(ys, tuple): # 2. 튜플이 아닌 경우 추가 지원
ys = (ys,)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
def forward(self, xs):
raise NotImplementedError()
def backward(self, dys):
raise NotImplementedError()
- #1 에서 함수를 '호출'할 때
*
을 붙였는데, 이런 경우 리스트 언팩(list unpack)이 이루어짐. 언팩은 리스트의 원소를 낱개로 풀어서 전달하는 기법- ex) xs = [x0, x1] 일 때,
self.forward(*xs)
를 하면 self.forward(x0, x1)으로 호출하는 것과 동일하게 동작함
- ex) xs = [x0, x1] 일 때,
- #2 에서는 ys가 튜플이 아닌 경우 튜플로 변경함. 이제
forward
메서드는 반환 원소가 하나뿐이라면 해당 원소를 직접 반환
In [15]:
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
- 이제 위처럼 순전파 메서드를
def forward(self, x0, x1):
이라고 정의할 수 있음. 결과는return y
처럼 하여 원소 하나만 반환 - 이상으로 Add 클래스를 구현하는 사람에게 DeZero는 더 쓰기 편한 프레임워크가 됨. 이상으로 두 번째 개선 마무리
12.3 add 함수 구현¶
- Add 클래스를 '파이썬 함수'로 사용할 수 있는 코드를 추가하여 계산 코드 개선
In [16]:
def add(x0, x1):
return Add()(x0, x1)
In [17]:
x0 = Variable(np.array(3))
x1 = Variable(np.array(7))
y = add(x0, x1)
print(y.data)
- 덧셈만 구현했지만, 곱셈, 나눗셈 등도 같은 방식으로 구현 가능함.
- 그러나 가변 길이 인수를 다룰 수 있는 것은 현재까진 순전파이므로, 역전파까지 다음 단계에서 구현해보자
13. 가변 길이 인수(역전파 편)¶
13.1 가변 길이 인수에 대응한 Add 클래스의 역전파¶
- 순전파는 입력이 2개, 출력이 1개 이지만, 역전파는 그 반대가 되어 입력이 1개, 출력이 2개
- 수식으로 확인하면 y=x0+x1 일 때 미분하면, ∂y∂x0=1,∂y∂x1=1 이 구해짐
- 덧셈의 역전파는 출력쪽에서 전해지는 미분값에 1을 곱한 값이 입력 변수(x0,x1)의 미분
- 즉, 상류에서 흘러오는 미분값을 '그대로 흘려보내는 것'이 덧셈의 역전파
- 이를 반영하여 Add 클래스에 구현
In [18]:
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
def backward(self, dy):
return dy, dy
- 이와 같이 backward 메서드는 입력이 1개, 출력이 2개임
- 이 코드처럼 여러 개의 값을 반환할 수 있게 하려면 역전파의 핵심 구현을 변경해야함
- DeZero에서는 Variable 클래스의 Backward 메서드를 수정하기로 함
13.2 Variable 클래스 수정¶
- 현재 Variable 클래스
In [19]:
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funcs.pop()
''' 해당 부분 수정 '''
x, y = f.input, f.output # 1. 함수의 입출력을 얻음
x.grad = f.backward(y.grad) # 2. backward 메서드를 호출
if x.creator is not None:
funcs.append(x.creator)
- 우선 while 블록 안의 #1에서 함수의 입출력 변수를 꺼내고, #2 에서 backward 메서드를 호출함
- 지금까지 우리는 #1에서 함수의 입출력이 하나씩이라고 한정함
- 이 부분을 여러 개의 변수에 대응할 수 있도록 수정
In [20]:
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funcs.pop()
gys = [output.grad for output in f.outputs] #해당 부분 추가 # 1.
gxs = f.backward(*gys) # 2.
if not isinstance(gxs, tuple): # 3.
gxs = (gxs,)
for x, gx in zip(f.inputs, gxs): # 4.
x.grad = gx
if x.creator is not None:
funcs.append(x.creator)
- 총 4 곳을 수정함.
- 우선, #1 에서 출력 변수인 output에 담겨 있는 미분값들을 리스트에 담음
- 그리고, #2 에서 함수 f의 역전파를 호출
- 이 때 f.backward(*gxs)처럼 인수에 별표를 붙여 호출하여 리스트를 풀어줌(리스트 언팩)
- #3 에서는 gxs가 튜플이 아니라면 튜플로 변환
- #2와 #3은 이전 단계에서 순전파 개선 시 활용한 관례와 같음. #2에서 Add 클래스의
backward
메서드를 호출할 때 인수를 풀어서 전달 - #3에서는 Add 클래스의
backward
메서드가 튜플이 아닌 해당 원소를 직접 변환할 수 있게 함
- #2와 #3은 이전 단계에서 순전파 개선 시 활용한 관례와 같음. #2에서 Add 클래스의
- #4 에서는 역전파로 전파되는 미분값을 Variable의 인스턴스 변수 grad에 저장함
- 여기에서 gxs와 f.inputs의 각 원소는 서로 대응 관계에 있음
- 더 정확히 말하면, i번째 원소에 대해 f.inputs[i]의 미분값은 gxs[i]에 대응함
- zip 함수와 for 문을 이용해서 모든 Variable 인스턴스 각각에 알맞은 미분값을 설정한 것
- 이상이 Variable 클래스의 새로운
backward
메서드
13.3 Square 클래스 구현¶
- 지금까지 Variable과 Function 클래스가 가변 길이 입출력을 지원하도록 개선함
- 그리고 구체적인 함수로써 Add 클래스를 구현함
- 마지막으로 Square 클래스도 새로운 Variable과 Function 클래스에 맞게 수정
In [21]:
class Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, gy):
x = self.inputs[0].data # 수정 전: self.input.data
gx = 2 * x * gy
return gx
- Function 클래스의 인스턴스 변수 이름이 단수형인 input에서 복수형인 inputs로 변경되었으니, 바뀐 변수에서 입력 변수 x를 가져오도록 코드를 수정
- 이것으로 새로운 Square 클래스도 완성
- 그럼 add 함수와 square 함수를 실제로 사용.
- 다음은 z=x2+y2을 계산하는 코드
In [22]:
x = Variable(np.array(2.0))
y = Variable(np.array(3.0))
z = add(square(x), square(y))
z.backward()
print(z.data)
print(x.grad)
print(y.grad)
- 보다시피 DeZero를 사용하여 z=x2+y2이라는 계산을
z=add(square(x)+square(y))
라는 코드로 풀어냄 - 그런 다음
z.backward()
를 호출하기면 하면 미분 계산이 자동으로 이루어짐 - 이상에서 복수의 입출력에 대응한 자동 미분 구조를 완성함
- 이제 다른 함수들도 적절히 구현하면 더 복잡한 계산도 가능함
- 그러나 사실 지금의 DeZero에는 몇 가지 문제가 숨어 있음. 다음 단계에서는 이 문제들을 먼저 해결!
14. 같은 변수 반복 사용¶
- 현재 DeZero에는 같은 변수를 반복해서 사용할 경우 의도대로 동작하지 않을 수 있는 문제가 있음
In [23]:
x = Variable(np.array(3.0))
y = add(x, x)
print('y', y.data)
y.backward()
print('x.grad', x.grad)
- y값은 제대로 계산했으나, x에 대한 미분값(x.gard)에서는 1.0이라는 잘못된 결과가 나옴. 제대로 계산했으면 y=x+x=2x이므로, 미분값은 2가 나와야 함
14.1 문제의 원인¶
- Variable 클래스의 다음 위치에서의 문제 때문에 x.grad가 틀리게 나옴
In [24]:
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f.funcs.pop()
gys = [output.grad for output in outputs]
gxs = f.backward(*gys)
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x in zip(f.inputs, gxs):
x.grad = gx ### 이 부분이 실수
if x.grad is not None:
funcs.append(x.creator)
- 현재 구현에서는 출력 쪽에서 전해지는 미분값을 그대로 대입함
- 따라서 같은 변수를 반복해서 사용하면 전파되는 미분값이 덮어써짐
In [25]:
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [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:
funcs.append(x.creator)
- 이와 같이 미분값(grad)을 처음 설정하는 경우에는 지금까지와 똑같이 출력 쪽에서 전해지는 미분 값을 그대로 대입
- 이 다음부터는 전달된 미분값을 '더해'주도록 수정
x.grad += gx
처럼 써도 되지만, 이렇게할 경우 문제가 되는 경우도 있음. 그 이유와 배경은 부록에서 따로 설명
In [26]:
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad)
In [27]:
x = Variable(np.array(3.0))
y = add(add(x, x), x)
y.backward()
print(x.grad)
14.3 미분값 재설정¶
- 방금 변경으로 새로운 주의사항이 생김. 바로 같은 변수를 사용하여 '다른' 계산을 할 경우 계산이 꼬이는 문제
In [28]:
# 첫 번째 게산
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad)
# 두 번째 계산(같은 x를 사용하여 다른 계산을 수행)
y = add(add(x, x), x)
y.backward()
print(x.grad)
- 앞의 코드는 서로 다른 두 가지 미분 계산을 수행했는데, 두 번째 x의 미분값에 첫 번째 미분 값이 더해짐(3이 나와야하는데 5가 나옴)
- 위 문제를 해결하기 위해 Variable 클래스에 미분값을 초기화하는
cleargrad
메서드를 추가함
In [29]:
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않아요. ndarray로 입력하세요.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [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:
funcs.append(x.creator)
def cleargrad(self): # 해당코드 추가
self.grad = None
In [30]:
# 첫 번째 게산
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad)
# 두 번째 계산(같은 x를 사용하여 다른 계산을 수행)
x.cleargrad() # 미분값 초기화
y = add(add(x, x), x)
y.backward()
print(x.grad)
- 이것으로 다른 계산에 똑같은 변수를 재사용할 때 생기던 문제가 사라짐
- 그러나 아직 중요한 문제가 조금 남아있음. 이를 다음 단계에서 해결해나가며 Variable 클래스를 완성시킴
In [ ]:
'기계학습 > 밑바닥딥러닝3 오독오독 씹기' 카테고리의 다른 글
Chapter 2. 자연스러운 코드로(step 17~19)/ 연산자 오버로드 (0) | 2021.01.04 |
---|---|
Chapter 2. 자연스러운 코드로(step 17~19) (0) | 2020.12.29 |
Chapter 2. 자연스로운 코드로(step 16) (0) | 2020.12.16 |
Chapter 1. Auto-grad(자동미분) step 6~9: 수동 역전파/ 역전파 자동화/ 재귀, 반복문/ 파이썬 함수 활용 (0) | 2020.12.02 |
1. Auto Gradient(자동미분): 변수/ 함수/ 수치미분/ 역전파 (0) | 2020.12.01 |