지금까지 입출력 변수가 하나씩인 경우만 고려해왔는데, 이번 포스팅에서는 가변 길이 입출력에 대응할 수 있도록 DeZero를 확장할 것이다.
1. 가변 길이 인수(순전파 편)
1) Function클래스 수정
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_creators(self)
self.inputs = inputs
self.outputs = outputs
return outputs
def forward(self, xs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
인수와 반환값을 리스트로 변경했다.
위의 새로운 Function클래스를 사용하여 구체적인 함수를 구현해볼 것이다.
덧셈을 해주는 Add클래스를 구현해보자
2) Add 클래스 구현
class Add(Function):
def forward(self, xs):
x0, x1 = xs
y = x0 + x1
return (y,)
xs = [Variable(np.array(2)), Variable(np.array(3))]
f = Add()
ys = f(xs)
y = ys[0]
print(y.data) #5
Add클래스 사용자에게 입력 변수를 리스트에 담아달라고 요구하거나 반환값으로 튜플을 받게 하는 것은 자연스럽지 않으므로 다음 단계에서는 자연스러운 코드로 개선할 것이다.
2. 가변 길이 인수 (개선 편)
1) 첫 번째 개선: '사용하는 사람'을 위한 개선
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 ouputs if len(outputs) > 1 else output[0]
인수 *inputs에 별표를 붙였다. 인수에 별표를 붙이면 여러개의 인수를 넘길 수 있고, 넘긴 인수를 튜플로 모아서 처리한다.
def f(*x):
print(x)
f(1, 2, 3)
#(1, 2, 3)
f(1, 2, 3, 4, 5, 6)
#(1, 2, 3, 4, 5, 6)
Add클래스는 아래와 같이 사용할 수 있다.
x0 = Variable(np.array(2))
x1 = Variable(np.array(3))
f = Add()
y = f(x0, x1)
print (y.data) #5
2) 두 번째 개선: '구현하는 사람'을 위한 개선
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]
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return ouputs if len(outputs) > 1 else outputs[0]
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
※ add클래스를 사용하기 쉽도록 파이썬 함수로 구현
def add(x0, x1):
return Add()(x0, x1)
add함수 사용 예시
x0 = Variable(np.array(2))
x1 = Variable(np.array(3))
y = add(x0, x1)
print(y.data) #5
여기까지 가변 길이 인수를 다룰 수 있는 순전파를 구현하였다. 다음은 역전파를 구현할 것이다.
3. 가변 길이 인수(역전파 편)
class Add(Function):
def forward(self, x0, x1):
y = x0 + x1
return y
def backward(self, gy):
return gy, gy
덧셈의 역전파는 출력 쪽에서 전해지는 미분값에 1을 곱한 것이다. 따라서 상류에서 흘러오는 미분값을 그대로 흘려보내는 것이 덧셈의 역전파이다.
Add 클래스의 backward 메서드는 입력이 1개 출력이 2개이다.
현재 Variable 클래스의 backward 메서드는 아래와 같다.
class Variable:
#생략
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funs.pop()
x, y = f.input, f.output
x.grad = f.backward(y.grad)
if x.creator is not None:
funcs.append(x.creator)
while문 안에서 x, y = f.input, f.output부분에서 함수의 입출력이 하나라고 가정하였다. 이 부분을 가변 인수 길이에 대응할 수 있도록 수정할 것이다.
class Variable:
#생략
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):
x.grad = gx
if x.creator is not None:
funcs.append(x.creator)
zip함수는 서로 대응 관계에 있는 f.inputs와 gxs를 각 Variable 인스턴스에 알맞는 쌍으로 설정하기 위함이다.
여기까지 Variable, Function, Add 클래스가 가변 길이 입출력을 지원하도록 개선하였다. 이제 Square클래스도 가변 길이 인수에 대응할 수 있도록 수정할 것이다.
classs Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, gy):
x = self.inputs[0].data #수정 전: x = self.input.data
gx = 2 * x * gy
return gx
아래와 같이 개선한 add함수와 square함수를 사용할 수 있다.
x = Variable(np.array(2.0))
y = Variable(np.array(3.0))
z = add(square(x), square(y))
z.backward()
print(z.data) #13.0
print(x.grad) #4.0
print(y.grad) #6.0
4. 같은 변수 반복 사용 시 나타나는 문제 해결
1) 이전에 저장된 미분값에 새로 내려오는 미분값을 더하지 않고 덮어씌워버리는 문제
현재의 DeZero는 같은 변수를 반복해서 사용할 경우, 문제가 생긴다. 예를 들어 y = add(x, x)를 보자.
x = Variable(np.array(3.0))
y = add(x, x)
print('y', y.data) #y 6.0
y.backward()
print('x.grad', x.grad) #x.grad 1.0
y = 2x이니 x.grad는 2가 되어야 하는데 1로 도출하였다. 각 변수의 기울기 값을 서로 더해야하는데 변수 이름이 같다보니, 더하지 않고 같은 값으로 덮어씌워버린 것이다. 원인은 Variable클래스의 다음 위치에 있다.
class Variable:
#생략
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):
x.grad = gx #여기가 문제를 발생시킨 부분
if x.creator is not None:
funcs.append(x.creator)
미분값을 덮어쓰지 않고 전파되는 미분값의 합을 구할 수 있도록 개선하면 아래와 같다.
class Variable:
#생략
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 += gx
if x.creator is not None:
funcs.append(x.creator)
수정된 것으로 다시 적용해보면 아래와 같이 올바른 값을 도출한다.
#x두 번 반복
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad) #2.0
#x세 번 반복
x = Variable(np.array(3.0))
y = add(add(x, x), x)
y.backward()
print(x.grad) #3.0
2) 미분값 연달아 계산할 때를 고려하여, 미분값 초기화 필요
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad) #2.0
y = add(add(x, x), x)
y.backward()
print(x.grad) #5.0
두번째 계산에서 x.grad의 값이 3.0이 나와야 하는데, 첫번째 계산 결과가 초기화되지 않은 채로 그 값이 더해져서 5.0이 나왔다. 이 문제를 해결하기 위해 Variable 클래스에 cleargrad메서드를 추가할 것이다.
class Variable:
#생략
def cleargrad(self):
self.grad = None
이 cleargrad메서드를 이용하면 여러 가지 미분값을 연달아 계산할 때 같은 변수를 사용할 수 있다.
x = Variable(np.array(3.0))
y = add(x, x)
y.backward()
print(x.grad) #2.0
x.cleargrad() #미분값 초기화
y = add(add(x, x), x)
y.backward()
print(x.grad) #3.0
'AI > 딥러닝 프레임워크 개발' 카테고리의 다른 글
19단계) 변수 사용성 개선 (0) | 2021.06.19 |
---|---|
17~18단계) 메모리 관리 방식 , 순환 참조 , 메모리 절약 모드 (0) | 2021.06.19 |
15~16단계) 복잡한 계산 그래프의 이론 및 구현 (0) | 2021.06.15 |
6~10단계) 수동 역전파 , 자동 역전파 , 재귀에서 반복문 , 간소화 , 테스트 (0) | 2021.05.13 |
1~5단계) Variable 클래스 , Function 클래스 , 수치 미분 , 역전파 이론 (0) | 2021.05.10 |
댓글