페이지

2022년 9월 30일 금요일

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)을 작게 할수록 이론상으로는 근사 정밀도가 좋아집니다. 그러나 컴퓨터가 하는 계산에서는 '자릿수 누락'이나 '반올림' 등이 발생할 수 있으니 반드시 이론과 일치하는 것은 아닙니다.












27.2 테일러 급수 이론

 본론으로 넘어가보죠, 이제부터 sin함수의 미분을 다른 방법으로 계산해보려 합니다. 바로 테일러 급수(Taylor Series)를 이용한 방법입니다. 테일러 급수란 어떤 함수를 다항식으로 근사하는 방법으로, 수식으로는 다음과 같습니다.


이것이 점 a에서 f(x)의 테일러 급수입니다. a는 임의의값이고, f(a)는 점 a에서 f(x)의 값입니다. 또한 f`는 1차 미분, f``는 2차 미분, f```는 3차 미분을 뜻합니다. 그리고 !기호는 계승(factorial)을 뜻하며 n!, 즉 n의 계승은 1에서 n까지 모든 정수의 곱을 말합니다. 예건대 5! = 5 * 4 * 3 * 2 * 1 = 120 이 됩니다.

2차 미분은 미분한 값을 한 번 더 미분한 것입니다. 물리 세계에서 예를 찾아보면 위치의 미분(변화)은 속도이면 소도의 미분(변화)은 가속도입니다. 이때 속도가 1차 미분이고 가속도가 2차 미분에 해당합니다.


테일러 급수에 의해 f(x)는 점 a를 기점으로 [식 27.1]로 나타낼 수 있습니다. [식 27.1]은 1차 미분, 2차 미분, 3차 미분, .. 식으로 항이 무한히 계속되지만, 어느 시점에서 중단하면 f(x)의 값을 근사할 수 있습니다. 물론 항이 많아질수록 근사의 정확도가 높아집니다.

한편  a = 0일 때의 테일러 급수를 매클로린 전개(Maclaurin's series)라고도 합니다. 실제로 [식 27.1]에 a = 0 을 대입하면 다음과 같이 됩니다.

    


[식 27.2]에서  a = 0으로 한정함으로써 더 간단한 수식이 되었습니다. 이제 f(x) = sin(x)를 [식 27.2]에 적용시켜 보겠습니다. 그러면 f`(x) = cos(x), f```(x) = -sin(x), f```(x) = -cos(x), f''''(x) = sin(x), ... 형태가 반복되는데, sin(0) = 0, cos(x) = 1이기 때문에 다음식을 이끌어낼 수 있습니다.


[식27.3]에서 보듯 sin함수는 x의 거듭제곱으로 이루어진 항들이 무산이 계속되는 형태로 표현됩니다. 여기서 중요한 점은 의 i가 커질수록 근사 정밀도가 좋아진다는 것이니다. 또한 i 가 커질수록 (-1)***i(x***(2i+1)) (2i + 1)의 절댓값은 작아지므로, 이 값을 참고하여 i의 값(반복횟수)을 적절히 결정할 수 있습니다.

2022년 9월 8일 목요일

27.1 sin 함수 구현

 sin 함수의 미분은 해석적으로 풀 수 있습니다. y = sin(x)일때 그 미분은 dx/dy=cos(x)입니다. 따라서 sin클래스와 sin함수는 다음처럼 구현할 수 있습니다.

import numpy as np

from dezero import Function


class Sin(Function):

    def forward(self, x):

        y = np.sin(x)

        return y

    

    def backward(self, gy):

        x = self.inputs[0].data

        gx = gy * np.cos(x)

        return gx


    def sin(x):

        return Sin()(x)


보다시피 넘파이가 제공하는 np.sin함수와 np.cos 함수를 사용해 간단하게 구현할 수 있습니다. 이제 DeZero에서도 sin함수를 사용해 계산할 수 있게 되었군요. 시험 삼아 x = 파이/4에서 y=sin(x)를 미분해보면 다음과 같습니다.

from dezero import Variable


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

y = sin(x)

y.backward()


print(y.data)

print(x.grad)


y값과 x의 미분 모두가 0.7071067811865476이군요. 1/np.sqrt(2)와 거의 일치합니다(수식으로 1/루트2). 물론 sin(파이/4) = cos(파이/4) = 1루트2 이기 때문에 옳은 결과입니다.

STEP 27 테일러 급수 미분

 지금부터는 DeZero를 사용하여 구체적인 문제를 몇 개 풀어보겠습니다. 이번 단계의 목표는 sin 함수의 미분입니다. 아시듯이 sin의 미분은 해석적으로 풀립니다. 그러니 우선은 정공법을 써서 sin 함수를  DeZero로 구현하고, 이어서 그 미분을 테일러 급수를 이용해 계산해 보겠습니다.

2022년 9월 6일 화요일

26.4 동작 확인

 24단계에서 구현한 Goldstein-Price 함수를 시각화해보겠습니다.

import numpy as np

from dezero import Variable

from dezero.utils import plot_dot_graph


def goldstein(x, y):

    z = (1 + (x + y + 1)**2 * (19 - 14*x + 3*x**2 - 14*y + 6*x*y + 3*y**2)) * \

         (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*x**2 + 48*y - 36*x*y + 27*y**2))

    return z

         

x = Variable(np.array(1.0))

y = Variable(np.array(1.0))

z = goldstein(x, y)

z.backward()

         

x.name = 'X'

y.name = 'Y'

z.name = 'Z'

plot_dot_graph(z, verbose=False, to_file='goldstein.png')

이 코드를 실행하면 goldstein.png라는 파일이 생성됩니다. 그 결과는 [그림 26-3]과 같이 다양한 변수와 함수가 복잡하게 얽힌 계산 그래프입니다. 자세히 보면 입력 변수 x와 y에서 시작하여 최종적으로 변수 z가 출력되고 있음을 알 수 있습니다. 참고로 Goldstein-Price함수를 Dezero에서 구현할 때 수식을 거의 그대로 코드에 옮길 수 있었는데, 그 뒤편에는 [그림 26-3]처럼 복잡하게 얽힌 계산 그래프가 만들어져 있던 것입니다.

이상으로 계산 그래프를 시가화 해봤습니다. 여기에서 구현한 시각화 함수는 앞으로도 필요할 때마다 계속 이용할 것입니다.




26.3 이미지 변환까지 한번에

 get_dot_graph 함수는 계산 그래프를 DOT 언어로 변환합니다. 그런데 DOT 언어를 이미지로 변환하려면 dot 명령을 수종으로 실행해야 하므로 매번 하기에는 번거롭습니다. 그래서 dot 명령 실행까지 한번에 해주는 함수를 제공하려 합니다. 코드는 다음과 같습니다.

import os

import subprocess


def plot_dot_graph(output, verbose=True, to_file='graph.png'):

    dot_graph = get_dot_graph(output, verbose)

    

    # 1 dot 데이터를 파일에 저장

    tmp_dir = os.path.join(os.path.expanduser('~'), '.dezero')

    if not osl.path.exists(tmp_dir):    # ~/.dezero 디렉토리가 없다면 새로 생성

        os.mkdir(tmp_dir)

    graph_path = os.path.join(tmp_dir, 'tmp_graph.dot')

    

    with open(graph_path, 'w') as f:

        f.write(dot_graph)

        

    # 2 dot 명령 호출

    extension = os.path.splitext(to_file)[1][1:]   # 확장자(png, pdf 등)

    cmd = 'dot {} -T {} -o {}'.format(graph_path, extension, to_file)

    subprocess.run(cmd, shell=True)


우선 1)에서 방금 구현한 get_dot_graph 함수를 호출하여 계산 그래프를 DOT 언어(테스트)로 변환하고 파일에 저장합니다. 대상 디렉터리는 ~/.dezero이고 파일 이름은 tmp_graph.dot로 했습니다(일시적으로 사용할 파일이므로 tmp라는 이름을 썼습니다). os.path. expanduser('~')문장은 사용자의 홈 데렉터리를 뜻하는 '~'를 절대 경로로 풀어줍니다.

2)에서는 앞에서 저장한 파일 이름을 지정하여 dot명령을 호출합니다. 이때 plot_dot_graph함수의 인수인 to_file에 저장할 이미지 파일의 이름을 지정합니다. 참고로 파이썬에서 외부 프로그램을 호출하기 위해 subprocess.run함수를 사용했습니다.

실제 plot_dot_graph 함수에는 앞에서 보여드린 코드 외에 몇 줄이 더 추가되어 있습니다. 추가된 코드는 독자가 주피터 노트북에서 이 코드를 실행하는 경우 주피터 노트북의 셀에 이미지를 직접 출력해주는 기능입니다.

이상으로 계산 그래프를 시가화하는 함수를 작성했습니다. 여기에서 구현한 함수는 앞으로 다양한 장소에서 사용되기 때문에 dezero/utils.py에 추가합니다. 그러면 from dezero.utils import plot_dot_graph로 임포트 할 수 있습니다.