영넌 개발로그

[밑시딥3] 미분 자동 계산 1 - 변수, 함수, 수치 미분 본문

코딩/ML , Deep

[밑시딥3] 미분 자동 계산 1 - 변수, 함수, 수치 미분

영넌 2023. 8. 15. 18:41

개인 공부 정리를 위한 포스팅

 

변수 (Variable)?

상자(box) 안에 데이터를 넣는 그림을 상상하기

'상자' 가 변수

- 상자에는 데이터가 들어간다 : 대입 or 할당

- 상자 속을 들여다보면 데이터를 알 수 있다 : 참조

class Variable:
    def __init__(self, data):
    	self.data = data

 

ex) x는 Variable 인스턴스 / 실제 데이터는 x 안에 

x는 데이터 자체가 아니라 데이터를 담은 상자

import numpy as np

data = np.array(1.0)
x = Variable(data)
#######################
x.data = np.array(2.0)

 

넘파이의 다차원 배열 (텐서, tensor)

숫자 등의 원소가 일정하게 모여 있는 데이터 구조
0차원 배열(스칼라), 1차원 배열(벡터), 2차원 배열(행렬)

넘파이의 ndarray 인스턴스에는 ndim 이라는 변수 존재
 number of dimensions,  '차원 수' 를 의미)
 

 

함수 (Function)?

어떤 변수로부터 다른 변수로의 대응 관계를 정한 것

계산 그래프

class Function:
    def __call__(self, input):
    	x = input.data
        y = x ** 2
        output = Variable(y)
        return output

- Function 클래스는 Variable 인스턴스를 입력받아 Variable 인스턴스를 출력

- Variable 인스턴스의 실제 데이터는 인스턴스 변수인 data에 있음

__call__ 메서드

파이썬의 특수 메서드
함수를 호출하는 것처럼 클래스의 객체도 호출 가능하도록 만들어줌
f = Function() 형태로 함수의 인스턴스를 변수 f 에 대입한 후, 이후에 f(...) 형태로 __call__ 메서드 호출 가능

위에서 만든 클래스 사용

x = Variable(np.array(10))
f = Function()
y = f(x)

print(type(y))   # -> <class '__main__.Variable>
print(y.data)    # -> 100

 

- Function 클리스를 기반 클래스로서, 모든 함수에 공통되는 기능 구현으로 수정

- 구체적인 함수는 Function 클래스를 상속한 클래스에서 구현 

class Function:
    def __call__(self, input):
        x = input.data
        y = self.forward(x) #구체적인 계산은 forward 함수에서
        output = Variable(y)
        return output
        
    def forward(self, x):
        raise NotImplementedError()
raise NotImplementedError()

raise 키워드와 NotImplementedError를 조합하여 사용하면 "아직 구현되지 않은 부분입니다"라는 오류를 강제로 발생시키는 것이 가능하다.

forward 메서드가 예외를 발생시키는 이유는 이 클래스의 forward 메서드를 직접 호출한 사람에게 '이 메서드는 상속하여 구현해야 한다'는 사실을 알려주기 위함이다.

class Square(Function):
    def forward(self, x):
        return x**2
        
#동작 확인 코드
x = Variable(np.array(10))
f = Square()
y = f(x)

print(type(y))   # -> <class '__main__.Variable>
print(y.data)    # -> 100
상속 : class 자식클래스(부모클래스)
물려주는 클래스(Parent, Super)의 내용(속성과 메소드)을 물려받는 클래스(Child, sub)가 가지게 되는 것
다중상속 가능

메소드 오버라이딩
부모 클래스의 메소드를 자식 클래스에서 재정의 하는 것
#y=e^x 함수 구현
class Exp(Function):
    def forward(self, x):
        return np.exp(x)

 

합성 함수 (composite function)?

여러 함수로 구성된 함수 (순서대로  적용하여 변환 전체를 하나의 큰 함수로 보는 것도 가능)

Function 클래스의 __call__ 메서드는 입출력이 모두 Variable 인스턴스 이므로 연이어 사용할 수 있음

#y = (e^(x^2))^2 계산

A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data) #-> 1.648721270

합성함수 그래프

 

미분?

'변화율'을 의미, 극한으로 짧은 시간(순간)에서의 변화량

미분 수식
곡선 y=f(x) 위의 두 점을 지나는 직선

위 그림에서 폭 h 를 0에 가깝게 줄여 x의 변화 비율을 구하면 그 값이 y=f(x)의 미분

y=f(x) 가 어떤 구간에서 미분 가능하다면 해당 구간의 모든 x에 대하여 미분 가능이 성립

f'(x)는 f(x) 의 도함수

 

수치 미분 (numerical differentiation)?

미세한 차이를 이용하여 함수의 변화량을 구하는 방법

작은 값을 사용하여 '진정한 미분'을 근사 함 ==> 오차 포함

 

컴퓨터는 극한을 취급할 수 없으니 h를 극한과 비슷한 값으로 대체 (h=1e-4, 매우 작은 값)

- 그러나 너무 작은 값이어서 반올림 오차(rounding error)로 인해 소수점 8자리 이상은 생략 되어 0.0으로 나타남

 

진정한 미분 : x 위치의 함수의 기울기 (접선)

이번 구현에서의 미분 : ( x + h ) 와 x 사이의 기울기

​==> 미분과 이번 구현의 값은 엄밀히 일치하지 않음 (무한히 h를 0으로 좁히지 못하기 때문에 발생)

 

"수치 미분" : 아주 작은 차분으로 미분하는 것
"해석적 미분" : 수식을 전개해 미분하는 것 / 오차를 포함하지 않는 “ 진정한 미분 “ 값

간단히 말해 해석적 미분은 우리가 수학 시간에 배운 바로 그 미분이고, 수치 미분은 이를 근사치로 계산하는 방법이다.
수치해석학은 해석학 문제에서 수치적인 근삿값을 구하는 알고리즘을 연구하는 학문이다

출처: https://ce-notepad.tistory.com/9

 

근사 오차를 줄이는 방법

중앙 차분

x 와 ( x + h ) 차이를 구하는 대신 ( x + h ) 와 ( x - h ) 일 때의 함수의 차분을 계산하는 방법

x를 중심으로 그 전후의 차분을 계산한다는 의미

* 전진 차분 : x 와 ( x + h ) 지점에서의 기울기를 구하는 방법

 

전진차분보다 중앙차분이 진정한 미분값에 가깝다는 사실은 테일러 급수를 이용해 증명 가능

출처 : https://pseudo-code.tistory.com/135

미분, 전진차분, 중앙차분 차이

첫 번째 인수 f : 미분의 대상이 되는 함수, Function의 인스턴스

두 번째 인수 x : 미분을 계산하는 변수, Variable 인스턴스

세 번재 인수 eps : 엡실론, 작은 값, 기본값은 1e-4

def numerical_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)
    
    
 #확인
 f = Square()
 x = Variable(np.array(2.0))
 dy = numerical_diff(f,x)
 print(dy)

 

합성 함수의 미분

def f(x):
    A = Square()
    B = Exp()
    C = Square()
    return C(B(A(x)))
    
x = Variable(np.array(0.5))
dy = numerical_diff(f,x)
print(dy)

* 파이썬에서는 함수도 객체이기 때문에 다른 함수에 인수로 전달 가능

 

 

수치 미분은 구현하기 쉽고 거의 정확한 값을 얻을 수 있음을 코드를 통해 알 수 있음

수치 미분의 문제점

1. 포함된 오차는 대부분의 경우 매우 작지만 어떤 계산이냐에 따라 달라질 수 있음

  - 자릿수 누락이 원인

2. 계산량이 많다. 

  - 변수가 여러 개인 계산을 미분할 경우 변수 각각을 미분해야함 (신경망에서는 매개변수가 수백만 개 이상)

 

그래서 등장한 것이 역전파 !

복잡한 알고리즘이라서 구현하면서 버그가 섞여 들어가기 쉬움

기울기 확인 (grdient checking) : 역전파를 정확하게 구현했는지 확인하기 위해 수치 미분의 결과를 이용

 

Comments