본문 바로가기
AI/딥러닝 프레임워크 개발

6~10단계) 수동 역전파 , 자동 역전파 , 재귀에서 반복문 , 간소화 , 테스트

by 채채씨 2021. 5. 13.
728x90
반응형

이전 포스팅에서 다루었던 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

 

728x90
반응형

댓글