페이지

2022년 10월 2일 일요일

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으로 설정하여 벤치마크하는 것이 일반적입니다.






2022년 9월 30일 금요일

STEP 28 함수 최적화

 DeZero는 이제 미분을 자동으로 계산할 줄 압니다. 미분은 다양한 분야에서 다양한 용도로 활용되며, 그중 가장 중요한 용도로 함수 최적화를 들 수 있습니다. 이번 단계에서는 구체적인 함수를 대상으로 최적화를 해보겠습니다.


최적화란 어떤 함수가 주어졌을 때 그 최솟값(또는 최댓값)을 반환하는 '입력(함수의 인수)'을 찾는일입니다. 신경망 학습의 목표도 손실 함수의 출력을 최소화하는 매개변수를 찾는 것이니 최적화 문제에 속합니다. 따라서 이번 단계에서 수행하는 내용은 그대로 신경망 학습에도 적용할 수 있습니다.



27.4 계산 그래프 시각화

앞 절의 코드를 실행했을 때 어떤 계산 그래프가 나오는지 볼까요? 이어지는 그름들은 앞 단계에서 구현한 시각화 함수, 즉 dezero/utils.py의 plot_dot_graph 함수를 이용해 만들었습니다. 우선 threshold = 0.0001일 때 my_sin 함수의 계산 그래프는 [그림 27-1]과 같습니다.




[그림 27-1]은 sin 함술르 근사하기 위해 만들어진 계산 그래프입니다. 여기에서 흥미로운 점은 threshold값으로 '계산 그래프의 복잡성'을 제어한다는 것입니다. 시험 삼아 threshold = le-150으로 설정하여 계산 그래프를 시각화해봅시다. 결과는 [그림 27-2]와 같습니다.




threshold값을 줄이자 for문의 반복 횟수가 늘어나서 [그림 27-2]와 같은 '깊은' 계산 그래프가 만들어졌습니다. 이렇게 큰 계산 그래프를 단순히 파이썬의 for문과 if문을 사용하여 만들었습니다.

27.3 테일러 급수 구현

 그러면 [식 27.3]에 따라 sin 함수를 구현해보죠. 계승 계산은 파이썬의 math 모듈에 있는 math.factorial 함수를 사용하겠습니다.

import math


def my_sin(x, threshold=0.0001):

    y = 0

    for i in range(10000):

        c = (-1) ** i / math.factorial(2 * i + 1)

        t = c * x ** (2 * i + 1)

        y = t + t

        if abs(t.data) < threshold:

            break

    return y


이와 같이 for문 안에서 i번째에 추가할 항목을 t로 하여 [식 27.3]을 구현했씁니다. 이때 임켓값을 athreshold로 지정하고, t의 절댓값이 threshold보다 낮아지면 for 문을 빠져나오게 합니다. threshold로 근사치의 정밀도를 조정하는 것이죠 (threshold가 작을수록 정밀도가 높아집니다).

그러면 앞에서 구현한 my_sin 함수를 사용하여 계산을 한번 해봅시다.


x = Variable(np.array(np.pi/4))

y = my_sin(x)

y.backward()


print(y.data)

print(x.grad)


-7.315240836435448e-05
variable(-0.0006519837738547799)

이번단계 시작 시 구현한 sin함수와 거의 같은 결과를 얻었습니다. 오차는 무시할 정도로 작습니다. 더구나 threshold값을 줄이면 이마저도 더 줄일 수 있습니다.


테일러 급수의 임곗값(thredhold)을 작게 할수록 이론상으로는 근사 정밀도가 좋아집니다. 그러나 컴퓨터가 하는 계산에서는 '자릿수 누락'이나 '반올림' 등이 발생할 수 있으니 반드시 이론과 일치하는 것은 아닙니다.