728x90
반응형
SMALL
728x90
반응형
LIST
728x90
반응형
SMALL

지난 시간 손실 함수 Loss Function의 종류에 대하여 알아보았습니다.

2024.01.27 - [Programming/Deep Learning] - [Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

 

[Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

지난 신경망 내용에서 매끄럽게 변화하는 시그모이드 함수와 퍼셉트론에서는 갑자기 변화하는 계단 함수를 사용하는 신경망 학습의 차이에 대하여 알아보았습니다. 2023.11.03 - [Programming/Deep Learn

yuja-k.tistory.com

오늘은 신경망 학습의 마지막 수치 미분과 경사하강법에 대하여 설명해 보도록 하겠습니다!

이번 블로그로는 (상)&(하)로 나눠집니다. (상) 편에서 미분과 편미분에 대하여 설명하며, (하)에서 경사하강법과 신경망에서의 기울기에 대하여 알아보도록 하겠습니다. 

 

경사법(경사하강법)을 위한 미분

경사법에서는 기울기 값을 기준으로 최적화하기 위한 방향을 결정합니다. 기울기의 성질을 알아보기 앞서 미분을 먼저 살펴보겠습니다.

 

미분

우리가 트랙에서 달리는 모습을 상상해 보겠습니다.

처음부터 10분 동안 2km씩 달렸다고 가정했을 때 속도는 2(거리) / 10(시간) = 0.2(속도)만큼의 속도(변화)로 뛰었다고 해석할 수 있을 것입니다.

위의 예에서는 달린 거리가 시간에 대해서 얼마나 변화했는지를 계산한 것입니다. 다만 여기에서 10분에 2km를 뛰었다는 이야기는 정확하게는 10분 동안의 평균 속도를 구한 것이죠.

하지만 미분은 '특정 순간'의 변화량을 뜻합니다. 따라서 10분이라는 시간을 최대한 줄여 한 순간의 변화량을 얻는다고 볼 수 있습니다.

이처럼 미분은 한순간의 변화량을 표시한 것입니다.

위 식은 함수의 미분을 나타낸 식입니다.

좌변은 함수 𝑓(𝑥) 𝑥에 대한 미분(𝑥에 대한 𝑓(𝑥)의 변화량)을 나타낸 기호라고 볼 수 있습니다.

결국, 𝑥의 작은 변화가 함수 𝑓(𝑥)를 얼마나 변화시키느냐를 의미합니다. 이때 시간의 작은 변화, 즉 시간을 뜻하는 를 한없이 0에 가깝게 한다는 의미를 lim0로 나타내었습니다. 그렇다면 파이썬으로 미분 함수를 구현해 보겠습니다.

h에 최대한 작은 값을 대입해서 다음과 같이 계산이 가능합니다. 참고로  Δ𝑥로도 표현합니다.

import numpy as np

# 미분의 나쁜 구현
def numerical_diff(f, x):
  h = 10e-50 # 0.0000 0이 50개
  return (f(x+h) - f(x)) / h

 

함수의 이름은 수치 미분(numerical differentiation)에서 따왔습니다.

이 함수는 함수 𝑓와 함수 𝑓에 넘겨질 𝑥를 받아서 실제 미분을 구현하지만, 고쳐야 할 부분들이 약간 있습니다.

위의 예에서는 가능한 한 h에 작은 값을 넣기 위해 10e-50이라는 매우 작은 수를 이용했었는데, 이 방식은 반올림 오차(rounding error) 문제를 일으킵니다. 반올림 오차는 작은 값(보통 소수점 8자리)이 생략되어 최종 계산 결과에 오차가 됩니다.

즉, 너무나 작은 숫자로 나눗셈을 하게 되면, 반올림 오차가 발생하여 컴퓨팅 시스템상 부동소수점 오류가 나게 됩니다.

 

파이썬에서의 반올림 오차는 보통 다음 상황에서 발생합니다.

np.float32(10e-50)

1000 / np.float32(10e-50)

너무 작은 값을 파이썬에서 소수점으로 나타내면 0.0이 되는 모습을 확인할 수 있습니다. 너무 작은 값을 컴퓨터로 계산하면 문제가 되기 때문에 이 미세한 값 h를 10의 −4제곱 정도로 놓으면 좋은 값을 얻는다고 알려져 있습니다.

 

그다음 개선할 점은 함수 f의 차분(임의 두 점에서 함숫값들의 차이)에 관련한 것입니다. 

𝑥+ 𝑥 사이의 함수 𝑓의 차분을 계산하고 있지만, h 자체가 매우 작은 값이기 때문에 애당초 위 계산에는 오차가 있다고 생각해야 합니다.

미분 자체가 어떤 순간 지점(시간(ℎ)이 최대한 0이 되는 지점)의 기울기를 의미하기 때문에 진정한 미분은 𝑥 위치 에서의 함수의 기울기를 이야기해야 하지만, 가 절대 0이 될 수가 없기 때문에 ( 0에 가까워지기는 하지만 ) 컴퓨터를 통한 미분 계산은 언제나 오차를 동반해야만 합니다.

이 오차를 최대한 줄이기 위해 (𝑥+ℎ)(𝑥−ℎ)일 때의 함수 𝑓의 차분을 계산하는 방법을 사용하기도 합니다. 이 차분은 x를 중심으로 그 전후의 차분을 계산한다는 의미로, 중심 차분 혹은 중앙 차분이라 합니다. 참고로 (𝑥+) 𝑥의 차분은 전방 차분이라고도 합니다.

위 두 가지 개선사항을 개선한 후의 미분 함수를 구현해 보겠습니다.

# 좋은 수치미분 구현
def numerical_diff(f, x):
  h = 1e-4 # 0.0001
  return (f(x+h) - f(x-h)) / (2 * h)
  • 아주 작은 값을 의미하는 ℎ는 1e-4
  • 수학적인 기교를 이용해 컴퓨터를 활용한 미분에서의 나눗셈 오류를 해결할 수 있다 (중앙차분과 전방차분을 활용)

위처럼 아주 작은 차분으로 미분하는 것을 수치 미분이라고 합니다.

수치 미분과 기울기의 관계

 

위의 미분 함수를 이용해 실제 간단한 함수를 미분해 보겠습니다. 먼저. 다음과 같은 2차 함수가 있다고 가정합니다.

def function_1(x):
  return 0.01*x**2 + 0.1 * x
import numpy as np
import matplotlib.pylab as plt

%matplotlib inline
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)

plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()

이번엔 x가 5와 10일 때의 해당 함수의 미분값입니다.

# x가 5일 때의 미분값과 10일 때의 해당 함수의 미분값 구하기
print("x가 5일 때 미분 값 : {}".format(numerical_diff(function_1, 5)))
print("x가 10일 때 미분 값 : {}".format(numerical_diff(function_1, 10)))

def tangent_line(f, x):
    d = numerical_diff(f, x)
    y = f(x) - d*x
    return lambda t: d*t + y

fig, axes = plt.subplots(1,2, figsize=(20,8))

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
axes[0].set_xlabel("x")
axes[0].set_ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

axes[0].plot(x, y)
axes[0].plot(x, y2)
axes[0].set_title("x = 5")

axes[1].set_xlabel("x")
axes[1].set_ylabel("f(x)")

tf = tangent_line(function_1, 10)
y2 = tf(x)

axes[1].plot(x, y)
axes[1].plot(x, y2)
axes[1].set_title("x = 10")
plt.show()

해당하는 위치(x)에서의 접선이 그려지는 것이 확인됩니다.

편미분

한쪽 변수 방향으로만 미분하는 것입니다. 다른 쪽 변수는 상수로 취급합니다.

인수들이 1개가 아닌, 2개 이상의 인수가 있는 상황에서 의 미분은 편미분을 통해 해결할 수 있습니다. 다음 식을 확인해 보겠습니다.

def function_2(x):
    return x[0]**2 + x[1]**2
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111, projection='3d')

X0 = np.arange(-3, 3, 0.05)
X1 = np.arange(-3, 3, 0.05)

X0, X1 = np.meshgrid(X0, X1)
Z = function_2([X0,X1])

ax.plot_wireframe(X=X0, Y=X1, Z=Z,rstride=10, cstride=10)
ax.set_xlabel('x0')
ax.set_ylabel('x1')
ax.set_zlabel('f(x)')
plt.show()

  1. 미분을 함에 있어서 고려해야 할 방향이 2개
    • 𝑥0, 𝑥1 방향으로 고려
  2. 둘 중 하나의 방향으로만 미분을 수행 -> 편미분
    • 𝑥0의 미분을 수행할 때는 𝑥1을 상수처럼 취급 𝑓 / 𝑥0
    • 𝑥1의 미분을 수행할 때는 𝑥0을 상수처럼 취급 𝑓 / 𝑥1

 


 

𝑥0=3, 𝑥1=4일 때 𝑥0에 대한 편미분 𝑓 / 𝑥0을 구하는 코드를 구현

 

변수가 두 개인 함수의 미분은 '어느 변수에 대한 미분이냐'를 따져줘야 합니다.

즉, 𝑥0, 𝑥1 중 어느 변수에 대한 미분이냐를 구별해야 한다고 생각할 수 있는데요, 이와 같이 변수가 여럿인 함수에 대한 미분을 편미분이라고 합니다.

수식으로는 𝑓 / 𝑥0 또는 𝑓 / 𝑥1 처럼 표현합니다. 연습 삼아 편미분 문제를 풀어보겠습니다. 

𝑥0=3, 𝑥1=4 일 때 𝑥0에 대한 편미분 𝑓𝑥0를 구하는 코드를 구현해 보겠습니다.

def function_tmp1(x0):
  return x0 ** 2 + 4.0 ** 2

numerical_diff(function_tmp1, 3.0)

다음은 𝑥0=3, 𝑥1=4 일 때 𝑥1에 대한 편미분 𝑓 / 𝑥1를 구하는 코드를 구현해 보겠습니다.

def function_tmp2(x1):
  return 3.0 ** 2 + x1 ** 2

numerical_diff(function_tmp2, 4.0)

위의 문제를 풀기 위해 변수가 하나인 함수를 정의했으며, 그 함수를 미분하는 형태로 구현하여 풀어보았습니다.

가령, 𝑥1=4로 고정된 새로운 함수를 하나 정의하여 변수가 𝑥0 하나뿐인 함수에 대해 수치 미분(numerical_diff)을 적용시킵니다. 이처럼 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구해낼 수 있습니다.

단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정합니다(상수처럼 활용).

앞의 예에서는 목표 변수를 제외한 나머지를 특정 값에 고정하기 위해서 새로운 함수인 function_tmp1, function_tmp2를 정의한 것이지요! 그리고 그 새로 정의한 함수에 대해 그동안 사용한 미분값을 적용시켜 편미분을 구한 것입니다.

기울기에 대하여

앞서는 𝑥0와 𝑥1의 편미분을 변수별로 따로 계산해 봤습니다.

그렇다면 𝑥0와 𝑥1의 편미분을 동시에 계산하고 싶으면 어떻게 해야 할까요?

위의 문제처럼 𝑥0=3, 𝑥1=4일 때 (𝑥0,𝑥1) 양쪽의 편미분을 묶어서 (𝑓 / 𝑥0, 𝑓 / 𝑥1)을 계산한다고 생각해 볼 수 있습니다. (𝑓 / 𝑥0, 𝑓 / 𝑥1)처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 합니다.

기울기를 구하는 예를 구현해 보겠습니다.

∂𝑓 / ∂𝑥0 ,  ∂𝑓 / ∂𝑥1

𝑥0과 𝑥1 각각의 편미분 결과를 (𝑓 / 𝑥0, 𝑓 / 𝑥1)로 정리한 것을 기울기(gradient)라고 한다.

# 기울기를 구하는 코드 - 각각의 편미분을 배열로 만든다.
def numerical_gradient(f, x):
  h = 1e-4
  # 기울기를 저장할 배열
  grad = np.zeros_like(x) # x와 형상(shape)이 같은 배열을 생성

  for idx in range(x.size):
    # x의 값을 idx에 맞게 하나씩 빼온다.
    tmp_val = x[idx]

    # 각 좌표에서의 미분을 수행 -> 편미분

    # 1. f(x+h) 계산
    x[idx] = tmp_val + h
    fxh1 = f(x)

    # 2. f(x-h) 계산
    x[idx] = tmp_val - h
    fxh2 = f(x)

    # 3. 기울기 계산해서 grad 배열에 저장 ( 미분 수행 )
    grad[idx] = (fxh1 - fxh2) / 2*h

    # x[idx]를 원래 값으로 복구
    x[idx] = tmp_val

  return grad

numerical_gradient 함수가 약간 복잡하게 생기긴 했지만 간단하게 생각해 보면 인자로 들어온 모든 x의 값(배열)에 대한 함수 f를 미분한 코드입니다. 위 함수를 이용하면 인자가 몇 개가 들어오든 모든 기울기를 벡터로 나타내는 배열을 확인할 수 있습니다.

result = numerical_gradient(function_2, np.array([3.0, 4.0]))
print("x = [3, 4] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([0.0, 2.0]))
print("x = [0, 2] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([3.0, 0.0]))
print("x = [3, 0] 일 때의 기울기 배열 : {}".format(result))

이처럼 (𝑥2,𝑥1)의 각 점에서의 기울기를 계산할 수 있습니다. 각 값별 기울기를 시각적으로 확인해 보겠습니다.

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
    
    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x+h) 계산
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)
        
        # f(x-h) 계산
        x[idx] = tmp_val - h 
        fxh2 = f(x) 
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 값 복원
        
    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.draw()
    plt.show()

기울기에 대한 그림은 위처럼 방향을 가진 벡터(화살표)로 그려집니다.

이 그림을 보면 기울기는 함수의 '가장 낮은 장소(최솟값)'를 가리키는 것 같습니다. 한 점을 향하고 있는 것이 확인되는데요, 또한 가장 낮은 곳에서 멀어질수록 화살표의 크기가 커지는 것도 확인이 가능합니다.

중요한 것은 기울기 자체는 가장 낮은 장소를 가리키고 있으나, 실제는 반드시 그렇다고는 할 수 없습니다.

사실 기울기는 각 지점에서 낮아지는 방향을 가리키는데요, 더 확실히 말하자면 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값(결괏값)을 가장 크게 줄이는 방향입니다. 확실히 기억해야 합니다.

 

지금까지 경사하강법을 위한 미분과 편미분을 알아보며 준비를 하였습니다. 다음 블로그에서는 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.

728x90
반응형
LIST

+ Recent posts