본문 바로가기
Drawing (AI)/DeepLearning

딥러닝 직접 구현하기 - (신경망 학습)

by 생각하는 이상훈 2023. 7. 6.
728x90

데이터 접근 방법

아래와 같은 손글씨 5에 대한 데이터를 보고 5라고 판단하기 위한 로직을 만들어보려고 하면 크게 두 가지의 접근법이 있다.

(1) 사람이 생각한 특징(SIFT, SURF, HOG 등) -> Machine Learning(SVM, KNN등)

  • 이미지에서 특징(feature)을 추출. SIFG, SURF, HOG 등의 특징 사용. 벡터로 기술.
  • Machine Learning: 데이터로부터 규칙을 찾아내는 역할

(2) 신경망(Deep Learning): 이미지를 그대로 학습


손실함수(loss function)

신경망은 어떠한 지표를 기준으로 최적의 매개변수 값을 탐색하기 위해 손실함수를 이용한다.

 

Sum of squares for error(SSE)

오차 제곱합은 아래와 같이 계산된다.

이는 간단한 연산으로 아래와 같이 구현된다.

def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)
    
import numpy as np
# 예1: '2'일 확률이 가장 높다고 추정함 (0.6)
mean_squared_error(np.array(y), np.array(t))

# 결과: 0.097500000000000031

# 예2 '7'일 확률이 가장 높다고 추정함 (0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
mean_squared_error(np.array(y), np.array(t))

# 결과: 0.59750000000000003

위의 두결과를 보면 첫번째 추정 결과가 오차가 더 적어서 정답에 더 가깝다고 판단할 수 있다.

 

Cross entropy error(CEE)

교차 엔트로피 오차는 아래와 같이 계산된다.

이는 아래와 같이 구현할 수 있다.

#자연로그 y=logx 그래프

%matplotlib inline
import matplotlib.pylab as plt

x = np.arange(0.001, 1.0, 0.001)
y = np.log(x)
plt.plot(x, y)
plt.ylim(-5.0, 0.0) # y축의 범위 지정
plt.show()

#교차 엔트로피 구현
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]
cross_entropy_error(np.array(y), np.array(t))

# 결과: 0.51082545709933802

y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t))

# 결과: 2.3025840929945458

미니배치

훈련 데이터 모두에 대한 손실함수의 평균을 구하여 해당 지표를 이용한다. 교차 엔트로피 오차(훈련 데이터 모두에 대한) 수식은 아래와 같다.

이때 빅데이터 수준으로 학습 데이터의 양이 늘어나면 수많은 데이터를 대상으로 손실 함수의 합을 구하는 것은 현실적으로 불가능하다. 이때 데이터의 일부를 이용해 전체의 근사치로 이용하여 지표를 정하고 학습할 수 있다.

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape)
print(t_train.shape)

#(60000, 784)
#(60000, 10)

load_mnist 함수로 MNIST (훈련 데이터와 시험 데이터) 데이터셋을 읽는다.

one_hot_label=True로 지정하여, 원-핫 인코딩을 진행하여 정답 위치의 원소만 1이고 나머지가 0인 배열을 얻는다.

훈련 데이터에서 무작위로 10장만 빼내려면 np.random.choice() 함수 사용한다.

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

np.random.choice(60000, 10)은 0에서 60000 미만의 수 중에서 무작위로 10개를 골라낸다.

이 함수가 출력한 배열을 미니배치로 뽑아낼 데이터의 인덱스로 사용가능하다.

np.random.choice(60000, 10)
array([ 6400, 19286,  1782,  3374, 49695, 27075, 13458, 28598, 31095, 59321])

(배치용) 교차 엔트로피 오차 구현하기

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y)) / batch_size
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

위의 두 코드에서 y는 신경망의 출력, t는 정답 레이블이다.

다만 첫번째 코드는 t가 원-핫 인코딩(one-hot encoding)되어 있는 경우를 다룬다. 따라서 t * np.log(y)에서 t가 0인 원소는 계산에서 제외되며, 실제 정답 레이블에 해당하는 클래스의 예측 확률에만 로그를 적용한다. 이 로그 값을 모두 합한 후, 배치 크기로 나누어 평균 교차 엔트로피 오차를 반환한다.

두번째 코드는 함수는 t가 레이블 인코딩(label encoding)된 경우를 다룬다. 즉, t는 각 데이터의 정답 레이블을 직접 가리키는 인덱스이다. np.log(y[np.arange(batch_size), t]) 이 부분은 각 데이터에 대해 정답 레이블에 해당하는 클래스의 예측 확률에 로그를 적용한다. 이 로그 값을 모두 합한 후, 배치 크기로 나누어 평균 교차 엔트로피 오차를 반환한다.


 

경사 하강법 구현

경사하강법(gradient descent)를 구현하기 위해서 기본적인 수치 미분식을 먼저 구현해야한다. 아래 코드의 함수인 numerical_gradient(f, x)의 동작 방식은 변수가 하나일 때의 수치 미분과 거의 같다. 이때 np.zeros_like(x)는 x와 형상이 같고 그 원소가 모두 0인 배열을 만든다.

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

위 함수를 이용하여 아래의 그래프를 화살표로 구현할 수 있다.

화살표의 길이가 짧을 수록 미분값이 0에 가까워지는 것이다.

from mpl_toolkits.mplot3d import Axes3D

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
     
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")#,headwidth=10,scale=40,color="#444444")
plt.xlim([-2, 2])
plt.ylim([-2, 2])
plt.xlabel('x0')
plt.ylabel('x1')
plt.grid()
plt.legend()
plt.draw()
plt.show()

이제 경사하강법은 다음과 같이 간단히 구현할 수 있다.

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

이는 아래의 수식을 기반으로 구현한 것이다.

이때 f는 최적화하려는 함수, init_x는 초기값, lr은 learning rate를 의미하는 학습률, step_num은 경사법 반복 회수이고

numerical_gradient(f, x)로 함수의 기울기를 구한다. 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다.

 

다음은 경사하강법을 이용한 매개변수 갱신 과정을 그래프로 표현하는 코드이다.

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)

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

lr = 0.1
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()

 

다음은 간단한 신경망에 대해 기울기를 구하는 코드이다.

import numpy as np

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 오버플로 대책
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 값 복원
        it.iternext()   
        
    return grad

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

이때 simpleNet 클래스는 형상이 2X3인 가중치 매개변수 하나를 인스턴스 변수로 가진다. predict(x) 메소드, loss(x,t) 메소드이고, 인수 x는 입력 데이터, t는 정답 레이블이다.

simplNet에 몇가지 방법으로 실행해보면 아래와 같다.

net = simpleNet()
print(net.W) # 가중치 매개변수
'''
[[-0.48896906 -0.43767281  0.94069236]
 [ 1.56181584 -0.6269286  -2.09184833]]
'''

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)
'''
[ 1.11225282 -0.82683942 -1.31824808]
'''
np.argmax(p) # 최대값의 인덱스
'''
0
'''
t = np.array([1, 0, 0]) # 정답 레이블
net.loss(x,t)
'''
0.20849859791009334
'''

아래 코드에서 w11은 대략 -0.11, w11을 h만큼 늘리면 손실함수는 -0.11h만큼 감소하고 손실함수를 줄이려면 '양의 방향' 으로 갱신해야 한다. w23은 대략 0.06, w23을 h만큼 늘리면 손실함수는 0.06h만큼 증가하고 손실함수를 줄이려면 '음의 방향' 으로 갱신해야 한다.
다만 파이썬에서는 간단한 함수는 lambda 기법을 쓰면 더 편리한 것을 볼 수 있다.

def f(W):
    return net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)
'''
[[-0.1129187   0.07005907  0.04285962]
 [-0.16937804  0.10508861  0.06428943]]
'''

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)
'''
[[-0.1129187   0.07005907  0.04285962]
 [-0.16937804  0.10508861  0.06428943]]
 '''

 

728x90

'Drawing (AI) > DeepLearning' 카테고리의 다른 글

GAN  (0) 2023.07.26
CNN for NLP  (0) 2023.07.22
딥러닝 직접 구현하기 - (신경망)  (0) 2023.07.01
딥러닝 직접 구현하기 - (퍼셉트론)  (0) 2023.06.21
Word Embedding(2)  (0) 2023.05.14