머신러닝, 딥러닝에 필요한 기초 수학 - 수치미분과 자동미분(1)

TS2·2021년 7월 22일
0

헉... 파이썬에 무료로 미분을 해주는 라이브러리가 있었다.

우선 아래 예제를 "sympy" 라는 라이브러리로 미분해보자

f(x)=(x2+2x)logxf(x) = (x^2+2x)\log x
import sympy

x = sympy.Symbol('x')
f = (x**2 + 2*x) * sympy.log(x)
df = sympy.diff(f, x)
df

sympy.simplify(df) //df결과를 심플하게 정리해준다
## >>>> x + 2*(x+1)*log(x) + 2

깔끔하진 않지만, 의도한 결과 (2x+2)logx+(x+2)(2x+2)\log x + (x+2)가 나왔다.
이를 x=1을 대입하여 미분계수를 구하면

import numpy as np

f = lambda x : (x**2 + 2*x)*np.log(x)
df = lambda x : 2 * (x + 1)*np.log(x) + (x + 2) //도함soo

print(f(1))
 >> 0.0
print(df(1))
 >> 3.0

1. 수치미분

근데, 이렇게 손으로 써주지 않고도 그냥 미분계수만 필요한 경우 '수치미분' 방식으로 결과값만 구할 수 있다.

수치미분은 독립변수의 변화 Δx\Delta x로 종속변수의 변화 Δy\Delta y를 실제로 계산하고 이 둘을 나눠서 특정 점에서 미분계수를 근사하는 방식이다.

그러나, 수치미분을 통해 계산된 값은 근사치이다. 근사치로 인한 오차를 '절단오차(truncation error)'라 하는데, 수치미분 방법의 근사식에 의한 근본적인 한계이다.

이를 해결하기 위해, Δx\Delta x의 변화를 더더욱 작게 줄이는 과정에서 컴퓨터가 가진 수치적 한계로 인해 '반올림 오차가(round-off error)'가 발생하게 된다.

(1) 전방 차분법과 중앙 차분법 비교

수치미분은 전방 차분법과 중앙 차분법이란 근사식을 통해 계산하게 된다.

1) 전방 차분법

fxif(x1,...,xi+Δxi,...,xn)f(x1,...,xi,...,xn)Δxi\frac{\partial f}{\partial x_i} \approx \frac{f(x_1, ..., x_i + \Delta x_i, ..., x_n) - f(x_1, ..., x_i, ..., x_n)}{\Delta x_i}

2) 중앙 차분법

fxif(x1,...,xi,+12Δxi,...,xn)f(x1,...,xi,12Δxi...,xn)Δxi\frac{\partial f}{\partial x_i} \approx \frac{f(x_1, ..., x_i, + \frac{1}{2}\Delta x_i, ..., x_n) - f(x_1, ..., x_i,- \frac{1}{2}\Delta x_i ..., x_n)}{\Delta x_i}

파이썬 코드로 구현해보자.

## 전방 차분법과 중앙 차분법
import sympy
import numpy as np

def numer_deriv(f, x, h=0.001, method="center"):
    """
    {f(x+h) -f(x)} / h 를 수치적으로 계산한다.
    
    f     : 미분할 함수로 주어진 위치에서 함숫값 계산을 위해 사용
    x     : 미분계수를 구할 변수의 위치로
            일변수인 경우 int 또는 float
            다변수인 경우 넘파이 어레이(d,) 벡터
    h     : 비율을 구할 작은 구간
    """
    if type(x) in (float, int) :
        grad = [0.0]
        x_ = [x]
        var_type = 'scalar'
    else : 
        grad = np.zeros(x.shape)
        x_ = x.copy().astype('float32')
        var_type = 'vector'
        
    for i, xi in enumerate(x_) :
        original_value = x_[i]
        
        if method == 'forward' :
            x_[i] = original_value + h
        else : 
            x_[i] = original_value + (h/2)
            
        if var_type == 'scalar' :
            gradplus = f(x_[i])
        else : 
            gradplus = f(x_)
            
        if method == 'forward' :
            x_[i] = original_value
        else :
            x_[i] = original_value - (h/2)
            
        if var_type == 'scalar' :
            gradminus = f(x_[i])
        else : 
            gradminus = f(x_)
            
        grad[i] = (gradplus - gradminus) / h
    
    if var_type == 'scalar' :
        return grad[0]
    else :
        return grad
1) 도함수의 미분계수 구하기
f = lambda x : (x**2 + 2*x) * np.log(x)

print(numer_deriv(f, 1))
 >> 2.999999999999666
print(numer_deriv(f, 1, h=0.5, method="forward"))
 >> 4.257383635135726
print(numer_deriv(f, 1, h=0.5, method="center"))
 >> 2.9997299032915508

numer_deriv 함수 자체는, 전방 차분법과 중앙 차분법을 입력값에 따라 나눠 계산하는 함수다.

print로 출력한 첫번째 값이 f 함수에 직접 1을 대입하여 계산한 값이고
두번째가 전방 차분법으로 계산한 근사값, 세번째가 중앙 차분법으로 계산한 근사값이다.

중앙차분법이 실제 미분계수와 거의 유사(하지만 차이가 있다)하다. 비교적 전방 차분법보다 오차가 작다.

2) 다변수 함수의 편미분계수 구하기

위 함수를 통해서 다변수 함수의 편미분계수도 array로 계산할 수 있게 된다.

f(x,y)=(x2+2x)logyf(x,y) = (x^2 +2x)\log y
f_xy = lambda x : (x[0]**2 +2* x[0])*np.log(x[1])
numer_deriv(f_xy, np.array([1, 2]))
 >> array([2.77255299, 1.49889143])

위 결과를 sympy로 직접 편미분해보면

편미분계수는 각각 2.7726, 1.5 로 수치미분(중앙차분법)의 값이 실제 편미분계수과의 오차가 현저히 줄어든 것을 확인할 수 있다.

3) 결론

수치미분(결국, 중앙 차분법)을 통한 수치미분은 직접 미분을 하기 힘들더라도 미분계수가 필요한 경우, 가장 먼저 선택할 수 있는 방법이다.
그러나, 루프문을 통해 구현되므로 인공지능 분야에서 한두개의 변수가 아닌 수십, 수백, 수천개의 변수를 루프문으로 구현하기에는 속도 이슈가 발생하게 된다.

따라서, 수치미분은 인공지능 분야에선 '자동미분'을 통한 결과값을 검증할 때 사용한다.

-- 자동미분에서 계속 --

profile
샴푸아닙니다. TSE입니다.

0개의 댓글