DeZero¶
- 해당 내용의 오리지널 프레임워크
- 해당 내용은 DeZero를 60단계로 나누어, 조금씩 완성하도록 구성함
1.1 Variable?¶
- 상자에 데이터를 넣는 그림에서, 상자의 역할이 변수
- 상자와 데이터는 별개
- 상자에는 데이터가 들어감(대입 or 할당)
- 상자 속을 들여다보면 데이터를 알 수 있음(참조)
1.2 Variable class 구현¶
- 파이썬에서는 클래스의 첫 글자 이름을 보통 대문자로 함(PEP8)
- Variable 클래스가 상자가 되도록 구현
In [1]:
class Variable:
def __init__(self, data):
self.data = data
- 초기화 함수
__init__
에 주어진 인수를 인스턴스 변수 data에 대입함. - 간단한 코드지만, 이를 통해 Variable 클래스를 상자로 사용할 수 있음.
- 실제 데이터가 Variable의 data에 보관되기 때문.
In [2]:
import numpy as np
data = np.array(1.0)
x = Variable(data)
print(x.data)
- 위 예에서 상자에 넣는 데이터로는 넘파이의 다차원 배열을 사용함
- 이 때 x는 Variable 인스턴스이며, 실제 데이터는 x 안에 담겨 있음.
- 즉, x는 데이터 자체가 아니라 데이터를 담은 상자가 됨
- 머신러닝 시스템은 기본 데이터 구조로 '다차원 배열'을 사용함.
- 넘파이의 다차원 배열 클래스는
numpy.ndarray
이며,np.ndarray
로 줄임 - 넘파이 배열은
np.array
함수를 이용해 생성할 수 있음
In [3]:
x.data = np.array(2.0)
print(x.data)
- x.data = ... 형태로 쓰면 새로운 데이터가 대입 됨. 이제 Variable 클래스를 상자로 사용할 수 있게 됨
2. 변수를 낳는 함수¶
- 앞 단계에서 Variable 클래스를 상자로 사용할 수 있게 함.
- 하지만 지금 이대로는 그냥 상자!
- 이를 단순한 상자에서 마법의 상자로 바꾸는 장치가 필요한데, 이 열쇠가 '함수'
2.1 Fucntion?¶
- 어떤 변수로부터 다른 변수로의 대응 관계를 정한 것 f(x) = y -> x를 f에 입력해서 y가 출력
2.2 Fucntion class 구현¶
- Variable 인스턴스를 변수로 다룰 수 있는 함수를 Function 클래스로 구현
- 주의할 점은 두 가지
- Function 클래스는 Variable 인스턴스를 입력받아 Variable 인스턴스를 출력
- Variable 인스턴스의 실제 데이터는 인스턴스 변수인 data에 있음
In [4]:
class Function:
def __call__(self, input):
x = input.data # 데이터를 꺼냄
y = x ** 2 # 실제 계산
output = Variable(y) # Variable 형태로 되돌림
return output
__call__
메서드의 인수 input은 Variable 인스턴스라고 가정__call__
은 파이썬의 특수 메서드- 이 메서드를 정의하면 f = Function()형태로 함수의 인스턴스를 변수 f에 대입해두고, 나중에 f(...) 형태로
__call__
메서드를 호출할 수 있음
- 따라서 실제 데이터는 input.data에 존재
- 데이터를 꺼낸 후 원하는 계산을 하고, 결과를 Variable이라는 '상자'에 담아 돌려줌
2.3 Function 클래스 이용¶
- Function 클래스를 실제로 사용.
- Variable 인스턴스인 x를 fucntion 인스턴스인 f에 입력해보자
In [5]:
x = Variable(np.array(10))
f = Function()
y = f(x)
print(type(y))
print(y.data)
- 이와 같이 Variable과 Funcion을 연계할 수 있음
- 실행 결과를 보면 y의 클래스는 Variable이며, 데이터는 y.data에 저장되어 있음을 알 수 있음
- 그런데 방금 구현한 Function 클래스는 용도가 '입력값의 제곱'으로 고정된 함수
- 따라서 Square라는 명확한 이름이 더 어울림
- 앞으로 Sin, Exp라는 다양한 함수가 필요하다는 점을 고려하면 Function 클래스는 기반 클래스로 두고 DeZero의 모든 함수가 공통적으로 제공하는 기능만 담아두는 것이 좋을듯
- DeZero함수는 다음 두 사항을 충족하도록 구현함
- Function 클래스는 기반 클래스로서, 모든 함수에 공통되는 기능을 구현함
- 구체적인 함수는 Function 클래스를 상송한 클래스에서 구현함
- 이를 위해 Function 클래스를 다음처럼 수정함
In [6]:
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()
__call__
을 살짝 수정하고,forward
라는 메서드를 추가함__call__
메서드는 'Variable에서 데이터 찾기'와 '계산 결과를 Variable에 포장하기' 라는 두 가지 일을 함- 그리고 그 사이의 구체적인 계산은 forward 메서드를 호출하여 수행함
- 마지막으로 forward 메서드의 구체적인 로직은 하위 클래스에서 구현함
- Function 클래스의
forward
메서드에서 예외를 발생시킴 - 이렇게 해두면 Function 클래스의 forward 메서드를 직접 호출한 사람에게 이 메서드는 상속하여 구현해야 한다는 사실을 말해줄 수 있음
- 이어서 Function 클래스를 상속하여 입력값을 제곱하는 클래스를 구현
- 클래스 이름은 Square라고 짓고 아래와 같이 구현
In [7]:
class Square(Function):
def forward(self, x):
return x ** 2
- Square 클래스는 Function 클래스를 상속하기 때문에
__call__
메서드는 그대로 계승됨 - 따라서
forward
메서드에 구체적인 계산 로직을 작성해 넣는 것만으로도 구현은 끝 - 실제로 확인
In [8]:
x = Variable(np.array(10))
f = Square()
y = f(x)
print(type(y))
print(y.data)
3. 함수 연결¶
- 지금까지 DeZero의 '변수'와 '함수'를 만들고, 2단계에서 Square라는 제곱 계산용 함수 클래스를 구현함
- 이번 단계에서는 또 다른 함수를 구현하고 여러 함수를 조합해 계산할 수 있게 만듦
3.1 Exp 함수 구현¶
- $ y = e^x$ 라는 계산하는 함수를 구현
In [10]:
class Exp(Function):
def forward(self, x):
return np.exp(x)
- Square 클래스와 마찬가지로 Function 클래스를 상속한 다음 forward 메서드에서 원하는 계산을 구현함
- Square 클래스와의 차이는 forward 메서드의 내용이 x ** 2에서 np.exp(x)로 변경된 것
3.2 함수 연결¶
- Function 클래스의
__call__
메서드는 입력과 출력이 모두 Variable 인스턴스이므로 자연스럽게 DeZero 함수들을 연이어 사용할 수 있음 - $y=(e^{x^2})^2$ 이라는 계산을 예로 들면 아래와 같음
In [13]:
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data)
- 3개의 함수 A,B,C를 연이어 적용함
- 중요한 점은 4개의 변수 x,a,b,y가 모두 Variable 인스턴스라는 것. Function 클래스의
__call__
메서드의 입출력이 Variable 인스턴스로 통일되어있는 덕분 - 이처럼 여러 함수로 구성된 함수를 합성함수라고 함
- 계산그래프를 이용하면 각 변수에 대한 미분을 효율적으로 계산할 수 있음
- 변수별 미분을 계산하는 알고리즘이 역전파
- 다음 단계부터 역전파를 구현할 수 있도록 DeZero 확장
4. 수치 미분¶
- 지금까지 Variable 클래스와 Function 클래스를 구현함
- 이 클래스를 구현한 이유는 미분을 자동으로 계산하기 위함
- 본격적인 구현에 앞서 이번 단계에서 미분이 무엇인지 복습하고 수치 미분이라는 간단한 방법으로 미분을 계산
- 그런 다음 5단계에서 수치 미분을 대신하는 더 효율적인 알고리즘(역전파)를 구현
4.1 미분¶
- pass
4.2 수치 미분 구현¶
- 컴퓨터는 극한을 취급할 수 없어 극한과 비슷한 값으로 대체함
- 예를 들어, h=0.0001(1e-4)과 같은 작은 값을 이용하여 미분 식을 계산함
- 이런 미세한 차이를 이용하여 함수의 변화량을 구하는 방법을 수치 미분이라고 함
- 수치 미분은 작은 값을 사용하여 실제 미분을 근사함. 따라서, 어쩔 수 없이 오차가 포함됨
- 이 근사오차를 줄이는 방법으로 '중앙 차분(Centered difference)'가 있음
- 중앙차분은 f(x)와 f(x+h)의 차이(전진차분)를 구하는 대신 f(x-h)와 f(x+h)의 차이를 구하는 것
- 중앙차분에서의 직선의 기울기는 $\frac{f(x+h)-f(x-h)}{2h}$ (분모가 2h임에 주의)
- 전진차분보다 중앙차분이 실제 미분값에 가깝다는 사실은 테일러 급수를 이용해 증명할 수 있음
- 중앙차분을 이용하여 수치미분을 계산하는 함수를
numerical_diff(f, x, eps=1e-4)
라는 이름으로 구현- 첫 번째 인수 f: 미분의 대상이 되는 함수, 앞에서 구현한 Function의 인스턴스
- 두 번째 인수 x: 미분을 계산하는 변수. Variable의 인스턴스
- 세 번째 인수 eps: 작은 값, 디폴트는 1e-4
In [14]:
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)
- 실제 데이터는 Variable의 인스턴스 변수인 data에 들어있다는 것만 주의하면 특별히 조심할 점은 없어 보임
- 3단계에서 구현한 Square 클래스를 대상으로 미분
In [15]:
f = Square()
x = Variable(np.array(2.0))
dy = numerical_diff(f, x)
print(dy)
- 이렇게 함수 $y=x^2$ 에서 x=2.0 일때 수치 미분한 결과를 구함
- 약간의 오차가 있지만 거의 비슷한 값
- 미분을 해석학적으로 계산할 수 도 있음. 해석적으로 계산한다는 뜻은 수식 변형만으로 답을 유도한다는 것
4.3 합성 함수의 미분¶
- 지금까지는 $y=x^2$ 이라는 단순한 함수를 다룸
- 이어서 합성함수를 미분. $y=(e^{x^2})^2$ 이라는 계산에 대한 미분을 계산
In [16]:
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)
- 위 코드는 일련의 계산을 f라는 함수로 정리함
- 파이썬에서는 함수도 객체이므로 다른 함수에 인수로 전달될 수 있음
- 실제로 앞의 코드에서는 numerical_diff 함수에 함수 f를 전달함
- 이상으로 미분을 '자동으로' 계산하는데 성공함
- 원하는 계산을 파이썬 코드로 표현한 다음(위 예에서는 f로 정의) 미분하라고 프로그램에 요청
- 위 방식으로 복잡한 함수라도 미분을 자동으로 계산 가능
- 이러한 방식으로 함수의 종류를 늘려가면 미분이 가능한 함수라면 어떠한 함수도 미분이 가능함
- 하지만 수치 미분에는 문제가 있다!
4.3 수치 미분의 문제점¶
- 수치 미분의 결과에는 오차가 포함됨
- 대부분의 경우 오차는 매우 작지만 어떤 계산이냐에 따라 커질수도 있음
- 또한, 계산량이 많다는게 더 심각한 문제. 변수가 여러 개인 계산을 미분할 경우 변수 각각을 미분해야 하기 때문
- 신경망에서는 매개변수를 수백만 개 이상 사용하는 일도 많으므로 이 모두를 수치 미분으로 구하는 것은 비현실적
- 그래서 등장한 것이 역전파(backpropagation)
- 수치미분은 구현하기 쉽고 거의 정확한 값을 얻지만, 역전파는 복잡한 알고리즘이므로 구현하면 버그가 섞여 들어가기 쉬움
- 따라서 역전파를 정확히 구현했는지 확인하기 위해 수치 미분의 결과를 이용하곤 함
- 이를 기울기 확인(gradient check)라고 하는데, 단순히 수치 미분의 결과와 역전파의 결과를 비교하는 것.
이후 내용 pass
In [ ]:
'기계학습 > 밑바닥딥러닝3 오독오독 씹기' 카테고리의 다른 글
Chapter 2. 자연스러운 코드로(step 17~19)/ 연산자 오버로드 (0) | 2021.01.04 |
---|---|
Chapter 2. 자연스러운 코드로(step 17~19) (0) | 2020.12.29 |
Chapter 2. 자연스로운 코드로(step 16) (0) | 2020.12.16 |
Chapter 2. 자연스러운 코드로(step 11~14) (0) | 2020.12.07 |
Chapter 1. Auto-grad(자동미분) step 6~9: 수동 역전파/ 역전파 자동화/ 재귀, 반복문/ 파이썬 함수 활용 (0) | 2020.12.02 |