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

30~32단계) 고차 미분

by 채채씨 2021. 7. 9.
728x90
반응형

DeZero는 1차 미분에 한해서 미분을 자동으로 계산할 수 있다. 이번 단계에서는 DeZero가 2차, 3차, 4차 등 고차 미분까지 자동으로 계산할 수 있도록 확장할 것이다.

 

1. 현재 고차 미분을 할 수 없는 이유

foward계산과 달리 backward계산은 계산 그래프가 그려지지 않는다.

먼저 forward계산은 계산 그래프가 그려지는 것을 이해하기 위해 아래 Function클래스를 보자.

 

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]

 

예를 들어, a = sin(x)함수의 forward계산을 보자. 함수의 인자로 들어가는 input은 variable클래스이다. forward계산을 통해 도출된 output도 variable클래스를 적용하였다. 즉 sin(x)함수를 계산하면 그 input과 output의 연결고리가 이어진다.

 

 

다음으로 backward계산은 계산 그래프가 그려지지 않는 것을 이해하기 위해 아래 Variable클래스를 보자.

 

class Variable:
	
    #생략
    
    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 += gx
                    
                if x.creator is not None:
                	add_func(x.creator)
                    
            if not retain_grad:
            	for y in f.outputs:
                	y().grad = None

 

Sin클래스의 backward메서드에서 미분을 계산하는 식은 gx = gy * cos(x)이다. 이때 cos(x)는 input.data값이므로 ndarray이고, gy도 np.ones_like(self.data)에 의해 self.data의 타입과 같은 ndarray이다. 즉, Sin의 backward는 인자 gy와 반환값 gx모두 variable클래스가 아닌 ndarray이므로 계산 그래프가 만들어질 수 없다.

 


 

2. 고차 미분을 하기 위해 수정해야 하는 부분

역전파 계산에서도 순전파 계산과 마찬가지로 계산 그래프가 그려지도록 즉, 함수와 변수간 연결이 되도록 수정하면 된다. 그러기 위해 계산 그래프를 만들어주는 Variable클래스를 이용할 수 있다.

 


 

3. 고차 미분 구현

gx = gy * cos(x)에서 gy와 gx를 Variable클래스로 바꾸기 위해서는 gy를 Variable로 바꾸면 연산자 오버로드에 의해 gx도 Variable이 된다.

 

class Variable:

	#생략
    
    def backward(self, retain_grad=False):
    	if self.grad is None:
        	#self.grad = np.ones_like(self.data)
           	self.grad = Variable(np.ones_like(self.data))
            
            #생략

 

gy를 설정하는 np.ones_like에 Variable클래스를 적용함으로써 간단하게 해결할 수 있다. 이제 미분값을 자동으로 저장하는 부분에서 self.grad가 Variable클래스를 담는다.

 


 

4. 함수 클래스의 backward 메서드 수정

 

[수정 전]

class Mul(Function):
	
    #생략
    
    def backward(self, gy):
    	x0 = self.inputs[0].data
        x1 = self.inputs[1].data
        return gy * x1, gy * x0

 

[수정 후]

class Mul(Function):
	
    #생략
    
    def backward(self, gy):
    	x0, x1 = self.inputs
        return gy * x1, gy * x0

 

수정 전에는 inputs이라는 Variable인스턴스 안에 있는 ndarray인스턴스인 data를 꺼내야 했지만, 수정 후에는 inputs이라는 Variable인스턴스를 그대로 사용하였다.

 


 

5. 역전파를 더 효율적으로 (모드 추가)

 

만약 역전파를 처음 한 번 이후로 다시 할 일이 없다면 1회 후 역전파 계산을 '역전파 비활성 모드'로 실행하려 한다.

모드를 추가하기 위해 Variable클래스의 backward메서드에 코드를 추가할 것이다.

 

def backward(self, retain_grad=False, create_graph=False): #마지막 인자 추가
	
    #생략
    
    while funcs:
    	f = funcs.pop()
        gys = [output().grad for output in f.outputs]
        
        with using_config('enable_backprop', create_graph):
        	gxs = f.backward(*gys) #메인 backward
            if not isinstace(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:
     				add_func(x.creator)

 

2차 이상의 미분이 필요하다면 create_graph=True로 설정한다. with블록을 통과할 때는 Function클래스에서 if Config.enable_backprop: 블록이 실행되지 않는다.

 


 

6. __init__.py 변경

 

지금까지의 수정을 새로운 dezero/core.py에 저장할 것이다. 앞으로는 core_simple.py 대신 core.py를 사용할 것이다.

따라서 dezero/__init__.py를 다음과 같이 수정한다.

 

is_simple_core = False #step23.py ~ step32.py까지는 simple_core이용

if is_simple_core:
	from dezero.core_simple import Variable
    from dezero.core_simple import Function
    from dezero.core_simple import using_config
    from dezero.core_simple import no_grad
    from dezero.core_simple import as_array
    from dezero.core_simple import as_variable
    from dezero.core_simple import setup_variable
    
else:
	from dezero.core import Variable
    from dezero.core import Function
    from dezero.core import using_config
    from dezero.core import no_grad
    from dezero.core import as_array
    from dezero.core import as_variable
    from dezero.core import setup_variable

 

728x90
반응형

댓글