본문으로 바로가기

수치미분

In [1]:
import numpy as np
In [2]:
# 수치미분 함수 1차 버전
def numerical_derivative(f, x):
    # lim에 해당하는 작은 값
    delta_x = 1e-4
    return (f(x+delta_x) - f(x-delta_x)) / (2*delta_x)

미분 코드를 구현할 때
가장 먼저 미분하고자 하는 대상의 함수 정의
-> 입력되는 대상에 대한 방정식을 먼저 만든다.

In [3]:
# 함수 이름이 바뀔 때마다 인자로 주는 함수 이름이 바뀌게 된다
def func2(x):
    return 3*x*np.exp(x)
In [4]:
# 람다가 수학적으로 좀더 직관적
# C언어로 치면 header file과 implement file을 분리하는 것
f = lambda x: func2(x)
In [5]:
ret1 = numerical_derivative(f, 2.0)
print(ret1)
66.50150507518049

편미분

입력 변수가 하나 이상인 경우!
f(x) 라고 했을 때 변수 하나만 변하는 것
그러나 대부분 변수가 두 개 이상이다.
즉 다변수 함수에서 미분하고자 하는 변수 하나를 제외한 나머지 변수들은 상수로 취급하고, 해당 변수를 미분하는 것.

편미분에서 각 입력변수를 독립변수로 본다.
f(x, y)라는 함수가 있을 때 x가 변하는 데에 y는 영향을 미치지 않고,
y가 변하는 데에 x는 영향을 미치지 않는다.
즉, x가 변할 때 y는 영향을 미치지 않기 때문에 상수로 취급해도 아무 문제가 없다.
이게 영향이 있다는 것은 (종속 변수라는 것은) 증명을 해야 한다.
영향을 미칠 수 있지만, 그 영향을 찾아내기 전까지는 독립 변수로 봐도 문제가 없다. (오차는 있을 수 있다.)
그래서 편미분을 할 때는 오차를 결정하는 것이 중요하다. 오차를 어느 범위로 둘 것인지?

확률과 오차
확률은 오차를 나타내는 또다른 말
예) 내가 로또 맞을 확률이 1%? -> 99%의 오차가 있다.
확률을 얘기할 때는 발생 가능한 가장 큰 값 (기댓값) 을 얘기해줘야 한다.
내일 비 올 확률이 50%라고 얘기하는 건 예측을 안 한 것이나 마찬가지.

우리는 수학 공식을 보는 것이 아니라 수학 공식에 내재된 pysical meaning을 찾는 작업을 계속함.

이 세상은 모두 미분을 구할 수 있다.
세상에 변하지 않는 것은 없기 때문에.

미분은 현재값을 바탕으로 미묘하게 변하는 것
어떤 입력 변수에 대해 어떤 관계가 있는지를 먼저 찾아내야 함!!
그러니까 관계식을 만드는 작업을 미분 코드를 만들기 전에 가장 먼저 해야 한다.

이 관계식이 흔히 말하는 loss function (손실함수)이다.

연쇄법칙 - chain rule

합성함수 : 여러 함수로 구성된 함수
이러한 합성함수를 미분하려면 '합성함수를 구성하는 각 함수의 미분의 곱'으로 나타내는 chain rule(연쇄법칙) 이용
chain rule이 왜 필요?
모든 것을 기본함수로 바꾸면 기본함수만 미분하면 끝이기 때문에.
chain rule의 목적은 모든 것을 기본함수로 바꾸는 것!
아무리 복잡한 함수라도 기본함수로 나타낼 수 있도록 쪼개는 작업을 하면 된다.

미분의 곱하기는 어렵다.
미분의 더하기는 어떤 함수 f(x, y, z) = x+y+z일 때는 각각에 대해서 따로 해주면 ok
곱하기는?
f(x, y, z) = xyz
양변에 log를 취해준다.
logab = loga + logb
-> 모두 log의 +로 바꿀 수 있다.
이 상태에서 미분.
log의 특성? log는 scale을 줄이는 효과 log(f(x, y, z)) = logx + logy + logz
변화값만 알고 싶으면 log값을 구해도 아무 상관이 없다.
미분할 때 웬만하면 양변을 로그를 취해서 +로 싹 바꿔준다.
나중에 확률함수는 *로 나오는데 이 경우에도 +로 바꿔준다.

확률
동전을 던졌을 때 앞면, 뒷면이라는 두 가지 가능한 상황 y(앞) = (1/2)
y(뒤) = (1/2)

동전 두개일 때 앞, 뒤 나올 확률은 1/2 * 1/2
하나의 수식으로 표현할 수 없을까?

y(앞) = (1/2)^1 (1/2)^0
= (1/2)^t
(1/2)^(1-t)
t가 1일 때는 앞면, t가 0일 때는 뒷면
동전이 여러 개면 위의 식을 계속 곱해준다.
미분하기 위해서는 log를 취해서 더하기의 식으로 바꿔준다.

수치미분 최종 버전

입력변수가 하나 이상인 다변수함수의 경우, 입력변수는 서로 독립적이기 때문에 수치미분 또한 변수의 개수만큼 개별적으로 계산해야 한다.
즉, 변수의 개수만큼 반복문으로 돌아야 한다.

In [40]:
'''
수치미분 debug version
'''
# 여기서 x는 행렬
def numerical_derivative(f, x):
    # 초기화 코드
    delta_x = 1e-4
    # create same dtype and same dimensional array
    grad = np.zeros_like(x) # gradient의 약자
    print("debug 1. initial input variable =", x)
    print("debug 2. initial grad =", grad)
    print("=======================================")
    
    # 반복문
    # 변수의 개수만큼 반복
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    
    print(x.dtype)
    
    while not it.finished:
        idx = it.multi_index
        
        print("debug 3. idx = ", idx, ", x[idx] = ", x[idx])
        
        # 원본값 보관
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + delta_x
        print(x.dtype)
        fx1 = f(x)
        
        x[idx] = tmp_val - delta_x
        fx2 = f(x)
        grad[idx] = (fx1 - fx2) / (2*delta_x)
        
        print("debug 4. grad[idx] = ", grad[idx])
        print("debug 5. grad = ", grad)
        print("=======================================")
        
        # 이 다음 편미분을 위하여 원상복귀
        x[idx] = tmp_val
        it.iternext()
        
    return grad

1변수 함수

In [9]:
def func1(W):
    x = W[0]
    return x**2

f = lambda W : func1(W)

W = np.array([3.0])

ret = numerical_derivative(f, W)

print('type(ret) = ', type(ret), ', ret_val = ', ret)
debug 1. initial input variable = [3.]
debug 2. initial grad = [0.]
=======================================
debug 3. idx =  (0,) , x[idx] =  3.0
debug 4. grad[idx] =  6.000000000012662
debug 5. grad =  [6.]
=======================================
type(ret) =  <class 'numpy.ndarray'> , ret_val =  [6.]

2변수 함수

In [10]:
def func2(W):
    x = W[0]
    y = W[1]
    
    return (2*x + 3*x*y + np.power(y, 3))

# lambda function 정의
f = lambda W : func2(W)

# (x, y) = (1.0, 2.0) 에서의 편미분 값
W = np.array([1.0, 2.0])

numerical_derivative(f, W)
debug 1. initial input variable = [1. 2.]
debug 2. initial grad = [0. 0.]
=======================================
debug 3. idx =  (0,) , x[idx] =  1.0
debug 4. grad[idx] =  7.999999999990237
debug 5. grad =  [8. 0.]
=======================================
debug 3. idx =  (1,) , x[idx] =  2.0
debug 4. grad[idx] =  15.000000010019221
debug 5. grad =  [ 8.         15.00000001]
=======================================
Out[10]:
array([ 8.        , 15.00000001])

예제 7

4변수 함수 f(w, x, y, z) 에 대한 수치미분

In [12]:
def func3(W):
    w, x, y, z = W[0, 0], W[0, 1], W[1, 0], W[1, 1]
    return (w*x + x*y*z + 3*w + z*np.power(y, 2))

f = lambda W:func3(W)

W = np.array([[1.0, 2.0],
             [3.0, 4.0]])

numerical_derivative(f, W)
debug 1. initial input variable = [[1. 2.]
 [3. 4.]]
debug 2. initial grad = [[0. 0.]
 [0. 0.]]
=======================================
debug 3. idx =  (0, 0) , x[idx] =  1.0
debug 4. grad[idx] =  5.000000000023874
debug 5. grad =  [[5. 0.]
 [0. 0.]]
=======================================
debug 3. idx =  (0, 1) , x[idx] =  2.0
debug 4. grad[idx] =  13.00000000000523
debug 5. grad =  [[ 5. 13.]
 [ 0.  0.]]
=======================================
debug 3. idx =  (1, 0) , x[idx] =  3.0
debug 4. grad[idx] =  32.00000000006753
debug 5. grad =  [[ 5. 13.]
 [32.  0.]]
=======================================
debug 3. idx =  (1, 1) , x[idx] =  4.0
debug 4. grad[idx] =  15.000000000000568
debug 5. grad =  [[ 5. 13.]
 [32. 15.]]
=======================================
Out[12]:
array([[ 5., 13.],
       [32., 15.]])

예제 8

1변수 함수 f(x) = x^2에서 미분하고자 하는 입력값을 정수 3으로 주는 경우의 미분값과 실수 3.0으로 주는 경우 미분값이 다른 이유를 설명하시오.
즉, f'(3)과 f'(3.0)을 계산하는 수치미분 코드를 구현하고 각 결과값이 나온 이유를 설명하시오.

In [41]:
def func4(W):
    x = W[0]
    return np.power(x, 2)

f = lambda W : func4(W)

W1 = np.array([3])
W2 = np.array([3.0])

print(numerical_derivative(f, W1))
print(numerical_derivative(f, W2))
debug 1. initial input variable = [3]
debug 2. initial grad = [0]
=======================================
int64
debug 3. idx =  (0,) , x[idx] =  3
int64
debug 4. grad[idx] =  25000
debug 5. grad =  [25000]
=======================================
[25000]
debug 1. initial input variable = [3.]
debug 2. initial grad = [0.]
=======================================
float64
debug 3. idx =  (0,) , x[idx] =  3.0
float64
debug 4. grad[idx] =  6.000000000012662
debug 5. grad =  [6.]
=======================================
[6.]

numpy의 array는 homogenius multidimensional array이다.
즉 numpy array는 같은 형의 원소로 구성되어야 하기 때문에 np.array에 들어갈 때 해당 array의 자료형으로 자동으로 형변환된다. (그럴 수 있는 경우)
3 -> dtype : int64
3.0 -> dtype : float64
만약 dtype=int64인 array의 내부 값을 변경할 때 실수를 넣게 되면 array가 자동으로 typecasting되는 게 아니라, 들어가는 값에 대해서 소수점 이하의 값을 버리고 정수형으로 넣게 된다!

핵심 : 데이터 타입을 명확히 하자!

미분은 미세한 값 (실수) 을 변화시킨다.
그렇기 때문에 input을 줄 때도 실수를 줘야 한다.
np.array([3])으로 주는 것이 아니라,
반드시 소수점을 저장할 수 있는 변수 np.array([3.0]) 혹은 np.array([3.0], dtype=np.float32) 이런 식으로 타입을 명시해 줘야 한다.

그래서 어제도 읽어들일 때 dtype = np.float32라고 명시해준 것이다.


방정식을 나타내는 notation은 처음에 어떤 것으로 정하든 상관없지만, 하나의 모듈에서 일정해야 한다.
그렇지 않으면 실수나기 쉽다.

수학에서 미분이 안 되는 경우?
함수가 연속적이지 않을 때, 즉 불연속일 때.
함수가 연속적이다 -> 직선의 기울기가 존재한다.
그런데 끊어져 있는 함수의 경우에는 기울기를 구하지 못하는 점이 존재한다.
뾰족하게 튀어나온 모양의 함수도 미분할 수 없다.
singular point <- 수학적으로는 미분할 수 없음.
그렇지만 수치미분으로는 미분할 수 있다.
수학적으로는 (원론적으로는) 불가능하지만 수치미분에서는 숫자만 구하면 되기 때문에 가능하다.

수치해석 프로그램은 아주 정교해야 한다.
수학적으로는 실수와 실수 간 연산에서 실수가 나왔지만, 실제 container에 넣을 때 정수형으로 들어가버림으로써 아주 치명적인 오류가 났다.

In [1]:
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))
In [ ]:
 

190929 수치미분


'교육 및 강연 > 인공지능 기초' 카테고리의 다른 글

3일차 190928 - numpy, matplotlib  (0) 2019.09.30
2일차 - 190921 python  (0) 2019.09.30