유진의 코딩스토리

Python DL 실습 2 [신경망학습] 본문

Azure 실습/Azure deep learnig

Python DL 실습 2 [신경망학습]

놀고먹는 유진 2024. 10. 23. 22:05

손실함수 (Loss function)

모델의 예측값과 실제값 사이의 차이를 나타내는 함수

모델의 성능을 평가하는 중요한 지표로 모델의 가중치를 업데이트하는데 사용한다.

 

 

▶ 손실함수의 종류

  • 평균 제곱 오차(Mean Square Error)
  • 교차 엔트로피 오차(Cross Entropy Error)

   손실함수의 종류에는 여러가지가 있지만 많이 사용되는 두가지만 확인해보자.

 

평균 제곱 오차(Mean Square Error)

 

오차의 제곱합은 가장 많이 사용되는 손실함수이다.

위 식에서 y값은 신경망의 출력(예측값), t는 정답 레이블(실제값)이며 k는 데이터 차원의 수를 나타낸다.

 

import numpy as np

def sum_squares_error(y, t):
    return 0.5 * np.sum((y - t) ** 2)
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]

print(sum_squares_error(np.array(y), np.array(t)))

y=[0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]

print(sum_squares_error(np.array(y), np.array(t)))

[출력값]

0.09750000000000003

0.5975

 

해당 배열의 원소가 첫번째 인덱스부터 숫자 0,1,2...로 매칭되는 값으로 현재 t를 보면 세번째 원소만 1로 되어있으며 나머지는 0인 원-핫 인코딩으로 되어있다.

y는 신경망의 출력값이다.실제 값은 2인 경우 2일 확률이 가장 높다고 추정한 경우 각 노드별 실제값과 예측값의 차이가 모두 작다.

하지만 실제값은 2인데 7일 확률이 가장 높다고 추정한 경우 세번째 노드와 8번째 노드의 실제값와 예측값의 차이가 매우 크다. 그러므로 이를 제곱의 합을 계산하면 예측을 다르게 할수록 MSE가 커지는 결과를 얻을 수 있다.

 

 

 

 

교차 엔트로피 오차(Cross Entropy Error)

 

교차 엔트로피 오차는 분류문제에서 가장 많이 사용되는 손실함수로 다중 클래스 분류 문제에 효과적이다.

여기에서 log는 밑이 e인 자연로그이며 y값은 신경망의 출력(예측값), t는 정답 레이블(실제값)이며 k는 데이터 차원의 수를 나타낸다. 

 

정답에 해당하는 출력이 커질수록 0에 다가가며 반대로 정답일 때의 출력이 작아질수록 오차는 커진다.

그러므로 위 식은 실질적으로 정답일 때의 추정의 자연로그를 계산하는 식이다.

import numpy as np
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t *np.log(y + delta))
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]

print(cross_entropy_error(np.array(y), np.array(t)))

y=[0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]

print(cross_entropy_error(np.array(y), np.array(t)))

[출력값]

0.510825457099338

2.302584092994546

 

 

왜 손실함수를 설정하는가?

정확도 지표 대신 손실함수를 택하는 이유는 신경망 학습에서의 미분의 역할 때문이다.

신경망 학습 시 최적의 매개변수(W, bias)를 탐색할 때 손실함수의 값이 가능한 작게하는 매개변수를 찾는 것이 목적이다.

 

 

미분값이 음수 → 

 

 

수치미분

 

함수의 미분값을 함수의 기울기(변화율)을 정확한 수학적 공식을 사용하지 않고 컴퓨터를 이용해 수치적으로 근사하여 계산하는 방법

h 값을 거의 0에 가까운 10^-4로 설정하여 최대한 근사한 접선의 미분값을 구한다.

 

# y = 0.01x^2 + 0.1x 의 x좌표 5에서 수치미분 접선을 그리는 코드
import numpy as np
import matplotlib.pyplot as plt


# 수치미분함수
def numerical_diff(f,x):
    h = 1e-4
    return (f(x+h) - f(x-h)) / (2*h)

# 미분할 함수
def function_1(x):
    return 0.01*x**2 + 0.1*x

# 접선을 구하는 함수
def tangent_line(f,x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()

 

 

 

 

 

 

기울기

 

다변수 미분

 

한개의 변수에 대해 미분하는 것을 일변수 미분이며

아래와 같이 함수의 변수가  2개 (x0,x1)일 때 각 축에 대해 미분하는 것을 편미분이라고 한다.

# f(x0, x1)=x0^2 +x1^2의 각 좌표의 접선의 기울기를 2차원 평면에 표현

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(f, x):
    h = 1e-4
    grad = np.zeros_like(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)   # 2차원의 빈 평면을 만드는 함수
    
    # 2차원 평면을 1차원으로 평탄화
    X = X.flatten()
    Y = Y.flatten()

    print(X)
    print(Y)

    grad = numerical_gradient(function_2, np.array([X, Y]))
    print(grad)

    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.legend()
    plt.draw()
    plt.show()

 

오른쪽 그림과 같은 3차원 평면에서 각 지점에서의 기울기의 세기를 표현한 것이 왼쪽의 결과이다.

가장자리로 갈수록 기울기가 매우 가파른 것을 볼 수 있는데 이는 왼쪽 그래프에서 긴 화살표로 표현되어있으며 중심으로 갈수록 기울기가 급격하게 완만해져 가운데는 거의 없는 수준을 볼 수 있다.

 

결국 손실함수를 이용해서 Weight값이 가장 작은 지점이자 기울기가 0이 되는 하는 지점을 찾는 것이 학습의 목표이다.

 

 

 

 

 

 

DNN학습 

순전파(Forward Propagation) 

input 방향에서 output방향으로 출력값을 계산하는 과정.

각 층에서 가중치와 bias를 이용해 계산해가 계산 후 실제값과 예측값과의 차이인 오차를 계산.

 

 

역전파(Back Propagation)

output 방향에서 input 방향으로 가중치를 갱신하는 과정.

오차를 기반으로 역방향으로 가면서 오차가 줄어드는 방향으로 가중치를 조정해가며 학습하는 과정이다.

 

 

 

 

경사하강법(Gradient Descent)

역전파 시 오차가 최소가 되는 방향으로 가중치를 갱신하는데 이 때의 가중치 갱신 방법으로 경사하강법을 사용한다.

 

 

 

 

경사하강법 동작 원리

 

먼저 모델의 가중치들을 임의의 값으로 초기화한다. 그 지점이 위 그림에서의 1번이라고 하면 해당 지점에서의 가중치에 대한 손실함수의 미분값(기울기)를 계산한다.

손실함수의  미분값(기울기)가 음수이면 손실함수의 값이 감소하므로 가중치를 증가시켜야한다. 반대로 기울기가 양수이면 손실함수의 값이 증가하므로 가중치를 감소시켜야한다. 

 

 

가중치는 위 사진의 식을 통해 갱신되는데 특히, 위 사진에서 빨간 네모 친 부분은 학습률로 가중치를 얼마나 크게 변경할지를 결정하는 하이퍼파라미터이다.

학습률이 너무 크면 최적의 값에 도달하지 못하고 이리저리 튀는 현상이 나타나며 너무 작을 경우 학습 속도가 너무 느리다는 단점이 있으므로 적절한 학습률을 조정하는 것이 중요하다.

파란 네모를 친 부분은 해당 지점의 미분값으로 해당 지점의 가중치에서 학습률만큼만 반영한 미분값을 빼준 값으로 가중치를 갱신하여 가중치 값을 줄인다. 

 

위 과정을 반복하여 기울기가 0에 가까워질 때까지 가중치를 계속 업데이트하며 기울기가 0에 도달하면 손실함수가 최솟값에 도달했음을 의미한다.

 

# f(x0, x1)=x0^2+x1^2의 Gradient descent 과정을 2차원 평면에 표현
import os, sys
print(os.getcwd())

import numpy as np
import matplotlib.pyplot as plt

# from gradient_2d import numerical_gradient
# 수치 미분을 계산하는 원소 함수
def _numerical_gradient_no_batch(f, x):
    h = 1e-4
    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:  # 변수가 1개일 때의 수치미분
        return _numerical_gradient_no_batch(f, X)
    else:  # 변수가 2개 이상일 때의 수치미분
        grad = np.zeros_like(X)

        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad

# 수치미분으로 구한 기울기를 이용해 중심으로 이동    
def gradient_descent(f, init_x, lr = 0.01, step_num = 100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append(x.copy())

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)

# 미분 전 원래 함수
def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])

lr = 0.05  # 학습률
step_num = 20  # 반복수
# 기울기 값을 이용해 중심으로 이동시키는 함수 호출
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot([-5, 5], [0, 0], '--b')
plt.plot([0, 0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel('x0')
plt.ylabel('x1')
plt.show()

 

# 정규분포로 초기화된 가중치(2x3)에 Cross entropy error 손실함수를 써서 새로운 가중치를 산출
import os, sys
print(os.getcwd())
current_dir = os.path.dirname(os.getcwd())
print(current_dir)
os.chdir(current_dir)


import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    # 신경망 출력값 계산
    def predict(self, x):
        return np.dot(x, self.W)
    

    # 손실값 계산
    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y,t)
        return loss
    

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
# 손실값을 미분하여 가중치의 기울기 계산
dW = numerical_gradient(f, net.W)
print(dW)

[출력값]
[[ 0.13155689  0.40482228 -0.53637917]
 [ 0.19733534  0.60723342 -0.80456876]]

 

 

 

 

 

학습 알고리즘 구현하기

 

학습 알고리즘의 4단계

 

 

 

▶ 배치(Batch)

전체 데이터셋 내의 데이터들 한 개씩 학습할 경우 메모리와 연산비용이 많이 듦

   → 데이터 셋을 여러 묶음(mini batch)로 나눠 처리하는 것이 효율적이다.


 
 에폭(Epoch)

전체 데이터셋의 순전파+역전파를 한번 완료한 횟수 즉, 전체 데이터셋을 한번 학습시킨 횟수

한번의 에폭만으로는 학습이 충분하지 않으므로 여러번 학습해준다.

이때, 에폭이 너무 작으면 underfitting문제, 에폭이 너무 크면 overfitting문제를 야기한다.

 

  반복(Iteration)

한번의 batch가 학습되는 과정으로 하나의 배치를 모델에 넣어 가중치를 업데이트하는 과정이 한번의 iteration이다.

즉, iteration은 mini batch의 갯수이다.

 

 

 

 

경사하강법의 3가지 종류

 

 

배치 경사하강법 (Batch Gradient Descent)

 

확률적 경사하강법 (Stochastic Gradient Descent, SGD)

 

미니 배치 경사하강법 (Mini-batch Gradient Descent)