페이지

2022년 10월 4일 화요일

STEP 30 고차 미분(준비편)

 현재의 DeZero는 미분을 자동으로 계산할 수 있지만 1차 미분 한정입니다. 그래서 이번 단계에서는 2차 미분도 자동으로 계산할 수 있도록, 나아가 3차 미분, 4차 미분,... 형태의 모든 고차 미분까지 자동으로 계산할 수 있도록 DeZero를 확장할 것입니다.

그러려면 DeZero를 사용하여 2차 미분을 계산하려는 현재의 역전파 구현을 근본적으로 재검토해야 합니다. DeZero의 역전파는 Variable과 Function 클래스에 기초해 동작합니다. 그래서 우선 Variable과 Function의 현재 구현부터 간단히 되돌아 보려 합니다. 앞으로의 이야기는 조금 길어지므로 3개 절로 나눠 하나씩 확인하겠습니다.


2022년 10월 3일 월요일

29.2 뉴턴 방법을 활용한 최적화 구현

 그럼 뉴턴 방법을 구현해봅시다. 뉴턴 방법을 활용한 최적화를 구현하려면 [식 29.5]를 구현하기만 하면 됩니다. 하지만 DeZero는 아쉽게도 2차 미분은 자동으로 구하지 못하므로 다음과 같이 수동으로 2차 미분을 구하기로 합시다.

y = x**4 - 2x**2

dy/dx = 4x**3 - 4x

d2y/dx2 = 12x**2 - 4


이 결과를 사용하면 뉴턴 방법을 활용한 최적화를 다음처럼 구현할 수 있습니다.



이와 같이 1차 미분은 지금까지처럼 역전파로 구하고 2차 미분은 수동으로 코딩해 구합니다. 그런 다음 뉴턴 방법의 갱신 수식에 따라 x를 갱신합니다. 이 코드를 실행하면 x값의 갱신 과정이 다음과 같이 출력됩니다.



이 문제의 답(최솟값)은 1입니다. 앞의 결과를 보면 목적지까지 빠르게 도달했음을 알 수 있ㅅ브니다. 단 7회의 갱신만으로 최솟값에 도달했죠. 한편 경사하강법은 최적값에 도달하는데 오랜 시간이 걸립니다. [그림 29-5]는 두 기법의 갱신 경로를 비교한 모습입니다.


이처럼 경사하강법으로는 갱신을 여러번 해야 합니다. 참고로 [그림 29-5]에서 경사하강법의 모습은 학습률을 0.01로 했을 때의 결과입니다. 이때 x = 1.0 과의 절대오차가 0.001 이하로 좁혀지기까지는 124번이나 갱신해야 했습니다. 그에 반해 뉴턴 방법은 불과 7번 입니다.

이상이 뉴턴 방법의 이론과 구현입니다. 이번 단계에서는 뉴턴 방법을 활용한 최적화를 구현하고 구체적인 문제를 풀어봤습니다. 그리고 실제로 좋은 결과를 얻어냈습니다. 그러나 구현시 2차 미분을 수동으로 계산했다는 한계가 있군요(2차 미ㅜㄴ을 계산하기 위해 수식을 손으로 써내려갔고, 그 결과를 하드코딩했습니ㅏㄷ). 이쯤이면 자연스럽게 집작이 되시겠죠? 다로 다음으로 정복할 목표ㅕ는 자동화하는 것입니다.




2022년 10월 2일 일요일

29.1 뉴턴 방법을 활용한 최적화 이론

 이번 단계의 목표는 뉴턴 방법을 활요한 최적화를 구현하는 것입니다. 경사하강법 대신 뉴턴 방법을 사용하여 실제로 더 빨리 수렴하는지 확인할 것입니다. 참고로 설명을 단순화하기 위해 변수를 하나만 받는 함수를 예로 들겠습니다(로젠브록 함수는 두 개의 변수를 입력받는 함수였습니다).

그렇다면 뉴턴 방법은 최적의 값을 어떤 원리로 찾아내는 것일까요? y = f(x)라는 함수의 최솟값을 구하는 문제를 생각해보죠, 뉴턴 방법으로 최적화하려면 데일러 급수에 따라 y = f(x) 를 다음과 같이 변환합니다.

f(x) = f(a) + f'(a)(x-a) + 1/2!f''(a)(x-a)**2 + 1/3!f'''(a)(x-a)**3 + ...

테일러 급수에 따라 어떤 점 aㄹ르 기점으로 f를 x의 다항식으로 나타낼 수 있습니다(테일러 급수에 대해서는 27단계에서 설명했습니다). 이때 1차 미분, 2차 미분, 3차 미분....형태로 항이 증가하는데, 증가하는 걸 어느 시점에 중단하면 f(x)를 근사적으로 나타낼 수 있습니다. 여기에서는 다음과 같이 2차 미분에서 중단하겠습니다.

f(x) = f(a) + f'(a)(x-a) + 1/2f''(a)(x-a)**2

[식 29.2]와 같이 y = f(x) 함수를 2차 미분항까지 사용하여 근사했습니다. 변수 x를 기준으로, 이 식 x의 2차 함수입을 알 수 있습니다. 즉, y = f(x)는 '어떤 함수'를 x의 2차 함수로 근사한 것입니다(그래서 2차 근사라고 합니다). 참고로 이 작업을 [그림 29-2]로 표현할 수 있습니다.



[그림 29-2]와 같이 근사한 2차 함수는 a 에서  y = f(x)에 접하는 곡선입니다. 다행이 2차 함수의 최솟값은 해석적으로 구할 수 있습니다. 2차 함수의 미분 결과가 0인 위치를 확인하면 되죠. 수식으로는 다음과 같습니다.

d(f(fa) + f'(a)(x-a) + 1/2f''(a)(x-a)**2)/dx =0

f'(a) + f''(a)(x-a) = 0

x = a - f'(a)/f''(a)


이 결과로부터 근사한 2차 함수의 최솟값은 x = a - f'(a)/f''(a); 위치에 있을을 알 수있습니다. 즉, [그림 29-3]처럼 a의 위치를 -f'(a)/f''(a)만큼 갱신하면 됩니다.

[그림 29-3] 처럼 a의 위치를 갱신합니다. 그리고 갱신된 a의 위치에서 같은 작업을 반복합니다. 이것이 뉴턴 방법에 의한 최적화입니다. 이 뉴턴 방법을 경사하강법과 비교해보면 특성이 명확히 드러납니다. 다음 식을 살펴보시죠.

x <- x - af'(x)

x <- x - f'(x)/f''(x)

[식 29.4]가 경사하강법이고 [식 29.5]가 뉴턴 방법입니다. 보시는 것처럼 두 방법 모두 x를 갱신하지만 방법이 다릅니다. 경사하강법에서는 a라는 계수를 사람이 수동으로 설정하고 a의 값만큼 기울기(1차 미분)방향으로 진행하여 x의 값을 갱신합니다. 이에 반해 뉴턴 방법은 2차 미분을 이용하여 경사하가업에서 말하는 a를 자동으로 조정합니다. 즉, 뉴턴 방법은 a = 1/f''(a)로 대치한 방법이라고 생각할 수 있습니다.

지금까지 함수의 입력이 '스칼라'일때의 뉴턴     방법을 설명했는데, 입력이 '벡터'인 경우로도 자연스럽게 확장할 수 있습니다. 다른 점은 벡터인 경우 1차 미분으로 기울기를 사용하고, 2차 미분으로 헤세행렬(Hesian matrix)을 사용하는 점입니다. 자세한 내용은 '칼럼:뉴턴 방법과 double backprop 보충 학습'에서 설명합니다.


지금까지의 이야기를 정리하면 경사하강법은 1차 미분만의 정보를 사용하는 반면 뉴턴 방법을 활용한 최적화는 2차 미분의 정보도 이용합니다. 물리 세계에서 예ㅒ를 들면 속도 정보만 사용하는 것이 경사하강법이고, 속도와 가속도 정보까지 사용하는 것이 뉴턴 방법입니다. 뉴턴 방법은 추가된 2차 미분 정보 덕에 효율적인 탐색을 기대할 수 있으며, 결과저4ㄱ으로 목적지에 더 빨리 도달할 확률이 커집니다.

자, 그렇다면 뉴턴 방법을 이용하여 구체적인 문제를 풀업볼 차례입니다. y = x**4 - 2x**2이라는 수식의 최적화를 해볼까 합니다. 이 함수의 모양은 [그림 29-4]와 같이 '오목'한 부분이 두곳이며, 최솟값 x가 각각 -1과 1인 위치입니다. 초깃값을 x =2로 설정한 후 최솟값 중 하나인  x =1에 도달할 수 있는지 검증해볼 것입니다.








STEP 29 뉴턴 방법으로 푸는 최적화(수동 계산)

 이전 단계에서는 로젠브록 함수의 최솟값을 경사하강법으로 구해봤는데, 기울기를 구하는 작업을 5만번 가까이 반복하고 나서야 겨우 목적지에 도달했습니다. 그 예에서 알 수 있듯이 경사하강법은 일반적으로 수렴이 느리다는 단점이 있습니다.

경사하강법을 대체할 수 있는, 수렴이 더 빠른 방법은 여러 가지가 있습니다. 그 중에서 유명한 것이 뉴턴 방법(Newton's method)입니다. 뉴턴 방법으로 최적화하면 더 적은 단계로 최적의 결과를 얻을 가능성이 높아집니다. 예를 들어 이전 단계에서 푼 문제를 뉴턴 방법으로 풀면[그림 29 -1] 의 오른쪽과 같은 결과를 얻을 수 있습니다.


[그림 29-1]을 보면 경사하강법이 '계곡'에서 고전하면서 서서히 목표값에 접근해가는 반면 뉴턴 방법은 계곡을 뛰어넘어 단번에 목적지에 도착합니다. 갱신 횟수는 불과 6회입니다. 경사하강법에서 5만번 가까이 필요했던 갱시이 불과 6회만에 끝나다니, 그적인 차이가 아닐 수 없습니다.

로젠브록 함수에서는 경사하강법과 뉴턴 방법의 갱신 횟수 차이가 아주 크게 나왔습니ㅏㄷ. 물론 이 횟수는 초깃값이나 학습률 등을 어떻게 설정하느냐에 크게 좌우되며, 이렇게 까지 큰차이를 볼 수 없는 경우도 많습니다. 일반적으로 초깃값이 정답에 가까우면 뉴턴 방법이 더 빨리 수령합니다.


28.3 경사하강법 구현

 복잡한 형상의 함수라면 기울기가 가리키는 방향에 반드시 최댓값이 존재한다고는 볼 수 없습니다(마찬가지로 반대 방향에 최솟값이 존재한다고 볼 수도 없습니다). 그러나 국소적으로 보면 기울기는 함수의 출력을 가장 크게 하는 방향을 나타냅니다. 그래서 기울기 방향으로 일정거리만큼 이동하여 다시 기울기를 구하는 작업을 반복하면 점차 원하는 지점(최댓값 혹은 최솟값)에 접근하리라 기대할 수 있습니다. 이것이 경사하강법(gradient descent)입니다. 알맞은 지점에서 시작하면 (좋은 초긱값을 주면) 경사하강법은 우리 목적지까지 효율적으로 안내해줍니다.

그러면 경사하강법을 우리 문제에 적용해보죠. 여기에서 문제는 로젠브록 함수의 '최솟값'찾기 입니다. 따라서 기울기 방향에 마이너스를 곱한 방향으로 이동합니다. 코드는 다음과 같습니다.


이와 같이 반복 횟수를 iters로 설정합니다(iters는 iterations의 약자입니다). 또한 기울기에 곱하는 값을 미리 설정해두고 있는데, 앞의 예에서는 lr = 0.001로 설정했습니다(lr은 learning rate의 머리글자로, 학습률을 의미합니다).


앞 코드의 for문에서는 x0와 x1이라는 Variable 인스턴스를 반복 사용하여 미분값을 구합니다. 이때 x0 grad와 x1 grad에는 미분값이 계속 누적되기 때문에 새롭게 미분할 때는 지금까지 누적된 값을 초기화 해야 합니다. 그래서 역전파하기 전에 각 변수의 cleargrad메서드를 호출하여 미분값을 초기화한 것입니다.


코드를 실행해보면 (x0, x1)값이 갱신되는 과정을 볼 수 있습니다. 실제로 터미널에 다음과 같은 결과가 출력됩니다.


출발점(0.0, 2.0)에서 시작하여 위치가 계속 갱싢되는 모습을 볼수 있습니다. [그림 28-2]는 이 결과를 플롯한 모습입니다.


[그림 28-2]롤 보면 모적지인 별의 위치에 서서히 접근하고 있음을 알 수 있습니다. 다만 도중에 멈춘 것 같군요, 그래서 반복 횟수를 iters = 10000으로 늘려 다시 실행해봤습니ㅏㄷ. 그 겨롸가 [그림 28-3]입니다.


[그림 28-3] 에서는 목적지에 더욱 가까워졌습니다. 이때 (x0, x1) 값은 (0.99449622, 0.98900063) 입니다. 참고로 횟수를 더 늘려서. 예컨대 iters = 50000으로 설정해 실행하면 실재로 (1.0, 1.0)위치에 간신히 도착합니다.


이것으로 이번 댄계도 끝입니다. 이번 단계에서는 DeZero를 사용하여 경사하강법을 구현했고, 이를 이용해 로젠브록 합수의 최솟값 위치를 찾을 수 있었습니다. 그러나 50,000번이나 반복한다는 것은 너무 과해보입니다. 사실 경사하강법은 로젠브록 함수 같이 골짜기가 길게 뻗은 함수에는 잘 대응하지 못합니다. 그래서 다음 단계에서는 또 다른 최적화 기법을 소개하고 구현할 예정입니다.


28.2 미분 계산하기

 가장 먼저 로젠브록 함수의(x0, x1) = (0.0, 2.0)에서의 미분(dy/dx0 와 dy/dx1)을 계산해보죠. DeZero를 이용하면 다음과 같이 구현할 수 있습니다.

import numpy as np

from dezero import Variable


def rosenbrock(x0, x1):

    y = 100 * (x1 - x0 ** 2)  ** 2 + ( 1 - x0 ) ** 2

    return y 


x0 = Variable(np.array(0.0))

x1 = Variable(np.array(2.0))


y = rosenbrock(x0, x1)

y.backward()

print(x0.grad, x1.grad)


variable(-2.0) variable(400.0)


이와 같이 수치 데이터(ndarray 인스턴스)를 Variable로 감싸서 건네주기만 하면 그다음은 수식을 따라 코딩하면 됩니다. 그리고 마지막에 y.backward()ㄹ르 호출하면 자동으로 미분을 계산할 수 있습니다.

이 코드를 실행하면 x0와 x1의 미분은 각각 -2.0과 400.0이라고 나옵니다. 이때 두 미분값을 모든 값, 즉 (-2.0, 400.0) 벡터를 기울기(gradient)혹은 기울기 벡터라고 합니다. 기울기는 각 지점에서 함수의 출력을 가장 크게 하는 방향을 가리킵니다. 지금 예에서는 (x0, x1) = (0.0, 2.0) 지점에서 y값을 가장 크게 늘려주는 방향이 (-2.0, 400.0)이라는 의미입니다. 반대로 기울기에 마이너스를 곱한 (2.0 -400.0) 방향은 y값을 가장 작게 줄여주는 방향을 뜻합니다.



2022년 10월 1일 토요일

28.1 로젠브록 함수

 이번 단계에서는 로젠브록 합수(Rosenbrock function)를 다룹니다. 수식으로는 [식 28.1]로 표현되며, 모양은 [그림 28-1]과 같습니다.

y = 100(x1 -x0 ** 2) ** 2 + (1 - x0) ** 2

[그림 28-1 ]을 보면 포물선 모양으로 길게 뻗은 골짜기가 보입니다. 참고로 [ 그림 28-1]의 '산'에 등고선을 그리면 그 모양이 바나나를 닮았다고 하여 로젠브록 함수를 바나나 함수 (Banana function)라고도 합니다.


이번 단계의 목표는 로젠브록 함수의 출력이 최소가 되는 x0와 x1을 찾는 것입니다. 답부터 말하면, 로젠브록 함수가 최소값이 되는 지점은 (x0, x1) = (1, 1)입니다. 이번 단계에서는 DeZero를 상요하여 이 최솟값을 실제로 찾아낼 수 있는지 확인합니다.

로젠브록 함수의 올바른 정의는 a,b 가 정수일때 f(x0, x1) = b(x1 - x0**2)입니다. 그래서 [식 28-1]과 [그림 28-1]은 a = 1, b = 100일때의 로젠브록 함수에 해당합니다. 로젠브록 함수는 최적화 문제의 벤치마크 함수로 자주 사용되며, 지금 예처럼 a - 1, b = 100으로 설정하여 벤치마크하는 것이 일반적입니다.