이전 포스팅에서 다루었던 Variable과 Function 및 여러 함수 클래스를 확장하여 역전파를 이용한 미분을 구현해볼 것이다.
1. 수동 역전파
■ Variable 클래스
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
data 인스턴스와 더불어 그 data값에 대응하는 grad(미분값) 인스턴스 변수를 추가하여 Variable 클래스를 확장하였다. 여기서 grad를 None으로 초기화해두고 나중에 실제로 역전파를 하여 미분값을 계산하여 대입할 것이다.
■ Function 클래스
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(y)
self.input = input #입력 변수 보관
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
기존에 구현했던 특정 함수 클래스에 미분을 계산하는 역전파 backward 메서드 기능을 추가하였다. 그리고 __call__ 메서드에 입력데이터 input을 인스턴스 변수인 self.input에 저장하여 나중에 backward 메서드에서 Function에 입력한 변수(Variable 인스턴스)가 필요할 때 self.input에서 가져올 수 있도록 하였다.
■ Square 클래스
class Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, gy):
x = self.input.data
gx = 2 * x * gy
return gx
gy는 출력쪽에서 전해지는 미분값이다. 이 미분값에 x**2의 미분을 곱한 값이 backward의 결과가 된다.
■ Exp 클래스
class Exp(Function):
def forward(self, x):
y = np.exp(x)
return y
def backward(self, gy):
x = self.input.data
gx = np.exp(x) * gy
return gx
■ 역전파 구현
먼저 순전파를 구해보면 아래와 같다.
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
c = C(b)
이어서 역전파로 y를 미분하면 아래와 같다.
y.grad = np.array(1.0)
b.grad = C.backward(y.grad)
a.grad = B.backward(b.grad)
x.grad = A.backward(a.grad)
print(x.grad) #3.297442541400256
지난 포스팅에서 수치 미분으로 구한 값이 3.2974426293330694와 비교하면 거의 같은 값이므로 역전파를 제대로 구하였음을 알 수 있다.
2. 역전파 자동화
앞에서 역전파의 순서에 맞추어 일일이 함수를 호출하여 작성했던 것을 자동화해볼 것이다. 자동화하기 위해서는 계산 지점들을 연결시켜야 한다. 함수와 변수가 연결되는 것이므로 함수의 관계를 먼저 이해해야 한다.
■ Variable 클래스 (+함수와 변수 연결고리 추가)
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
creator라는 인스턴스 변수를 추가하고, creator을 세팅할 수 있도록 set_creator메서드를 추가하였다.
■ Function 클래스 (+함수와 변수 연결)
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(y)
output.set_creator(self) #출력 변수에 creator 설정
self.input = input
self.output = output
return output
순전파를 계산했을 때 얻는 Variable 인스턴스인 output에 어떤 함수가 창조자(creator)인지 기억하도록 output.set_creator(self)를 추가했다. 이 부분이 '연결'을 만든 부분이다.
<헷갈렸던 것>
output.set_creator(self)의 self는 set_creator(self, func)함수에서 func에 해당하는 인자이다. 이때 self는 (Square, Exp와 같은)특정 함수의 인스턴스이므로, Variable 인스턴스인 self.creator의 값은 None에서 그 특정 함수 클래스로 바뀐다.
★★Function클래스는 Variable인스턴스를 입력받아 Variable인스턴스를 출력한다는 것과 self.input = input이 Variable인스턴스임을 기억해야한다!!
이처럼 연결된 Variable과 Function이 있으면, 계산 그래프를 거꾸로 올라갈 수 있다. 구체적으로 보면 다음과 같다.
#순전파
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
c = C(b)
#계산 그래프의 노드들을 거꾸로 거슬러 올라감
assert y.creator == C
assert y.creator.input == b
assert y.creator.input.creator == B
assert y.creator.input.creator.input == a
assert y.creator.input.creator.input.creator == A
assert y.creator.input.creator.input.creator.input == x
assert문은 조건을 충족하는지 여부를 확인하는 데 사용하며, 결과가 True가 아닐 경우 예외를 발생시킨다.
■ 역전파 구현
y.grad = np.array(1.0)
C = y.creator
b = C.input
b.grad = C.backward(y.grad)
B = b.creator
a = B.input
a.grad = B.backward(b.grad)
A = a.creator
x = A.input
x.grad = A.backward(a.grad)
print(x.grad) #3.297442541400256
이런식으로 함수와 변수가 연결되어 grad를 거꾸로 넘긴다. 이제 이 과정을 Variable클래스에 backward메서드로 만들어 자동화시킬 것이다.
■ Variable 클래스 (+backward추가)
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
f = self.creator
if f is not None:
x = f.input
x.grad = f.backward(self.grad)
x.backward()
만든 것으로 역전파를 자동으로 실행시켜보면 아래와 같다.
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
c = C(b)
#역전파
y.grad = np.array(1.0)
y.backward()
print(x.grad) #3.29744251400256
3. 재귀에서 반복문으로
재귀보다 반복문이 효율적이므로 backward메서드의 구현 방식을 바꾸어볼 것이다.
먼저 이전에 재귀를 사용하여 구현했던 방식은 아래와 같다.
class Variable:
#생략
def backward(self):
f = self.creator
if f is not None:
x = f.input
x.grad = f.backward(self.grad)
x.backward()
다음은 반복문을 사용하여 구현해볼 것이다.
class Variable:
#생략
def backward(self):
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)
4. 함수를 편리하게 사용하도록 수정/추가 작업
■ Square, Exp 함수 클래스 간소화
Square클래스를 사용하려면 아래와 같이 인스턴스를 생성하고, 인스턴스를 호출하는 두 단계의 코드를 작성해야 했다.
x = Variable(np.array(0.5))
f = Square()
y = f(x)
복잡한 과정을 간소화하기 위해 square, exp라는 파이썬 함수를 따로 만들어서 인스턴스 생성과 호출 단계를 자동으로 처리할 것이다.
#square함수 작성
def square(x):
f = Square()
return f(x)
#square함수 한 줄로 작성
def square(x):
return Square()(x)
#exp함수 작성
def exp(x):
f = Exp()
return f(x)
#exp함수 한 줄로 작성
def exp(x):
return Exp()(x)
아래처럼 순전파, 역전파를 하여 기울기를 구하는 과정이 간소화되었다.
x = Variable(np.array(0.5))
y = square(exp(square(x)))
y.grad = np.array(1.0)
y.backward()
print(x.grad) #3.297442541400256
■ y.grad = np.array(1.0) 코드 생략
class Varible
#생략
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)
np.ones_like(self.data)는 self.data와 데이터 타입과 형상이 같은 ndarray인스턴스를 생성하며 그 원소는 모두 1이다.
이제 순전파와 역전파를 돌려 기울기를 구하고자할 때, y.grad = np.array(1.0)을 따로 설정하지 않아도 된다.
x = Variable(np.array(0.5))
y = square(exp(square(x)))
y.backward()
print(x.grad) #3.297442541400256
■ 입력 데이터로 ndarray만취급하기
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{}은(는) 지원하지 않습니다.'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
#생략
isinstance(인스턴스, 클래스/데이터타입)은 인스턴스가 해당 클래스/데이터타입과 일치하면 True를, 불일치하면 False를 반환한다.
※문제점!
0차원의 np.array 인스턴스를 사용하여 계산하면 그 결과의 데이터 타입이 numpy.float64나 numpy.float32가 된다. 그러나 Variable은 입력 데이터를 항상 ndarray인스턴스라고 가정하고 있으므로 이 문제를 해결해야 한다. 이를 해결하기 위해 as_array라는 함수를 만들 것이다.
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
np.isscalar는 입력 데이터가 numpy.float64같은 스칼라 타입인지 확인해주는 함수이다. 만약 float64 같은 타입이 들어오면 np.array로 바꾸어준다.
이 해결책을 Function클래스에 추가한다.
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(as_array(y)) #as_array로 데이터 타입을 np.array로 유지
output.set_creator(self)
self.input = input
self.output = output
return output
#생략
5. 테스트
■ Square클래스의 forward테스트
import unittest
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = Square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
■Square클래스의 backward테스트
class SquareTest(unittest.TestCase):
# 생략
def backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad, expected)
미분값을 손으로 구하여 입력하지 않고 수치 미분의 결과와 역전파의 결과를 비교하는 gradient checking을 하여 그 차이가 크면 문제가 있다고 판단하는 테스트기를 만들 것이다.
def numericla_diff(f, x, eps=1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2 * eps)
class SquareTest(unittest.TestCase):
#생략
def test_gradient_check(self):
x = Variable(np.random.rand(0.1))
y = Square(x)
y.backward()
num_grad = numerical_diff(square, x)
flg = np.allclose(x.grad, num_grad)
self.assertTrue(flg)
np.allclose(a, b)는 ndarray의 인스턴스인 a와 b의 값이 가까운지를 판정한다. np.allclose(a, b, rtol=1e-05, atol=1e-08)과 같이 rtol, atol을 지정하여 가깝다는 것을 정의할 수 있다.
위와 같이 테스트 코드를 작성한 후, 실행할 때는 아래와 같은 명령어를 터미널에 입력하면 된다.
#테스트 코드가 test/test.py파일에 있다고 가정
$ python -m unittest test/test.py
'AI > 딥러닝 프레임워크 개발' 카테고리의 다른 글
19단계) 변수 사용성 개선 (0) | 2021.06.19 |
---|---|
17~18단계) 메모리 관리 방식 , 순환 참조 , 메모리 절약 모드 (0) | 2021.06.19 |
15~16단계) 복잡한 계산 그래프의 이론 및 구현 (0) | 2021.06.15 |
11~14단계) 가변 길이 인수 대응 ( 순전파 , 역전파 ) (0) | 2021.06.15 |
1~5단계) Variable 클래스 , Function 클래스 , 수치 미분 , 역전파 이론 (0) | 2021.05.10 |
댓글