신경망 학습

Sirius·2024년 6월 25일

학습

  • 학습 : 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것

학습패러다임의 전환

신경망은 데이터를 "있는 그대로" 학습한다.
즉 신경망은 이미지에 포함된 중요한 특징까지도 "기계"가 스스로 학습한다.

  • 종단간 기계학습: 딥러닝의 학습을 뜻한다.(사람의 개입없이 결과를 얻음)

훈련 데이터와 시험 데이터

훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾는다.
학습된 모델의 실력을 시험 데이터를 통해 평가한다,

  • Q: 왜 훈련데이터와 시험 데이터를 나눌까?
    A: 범용능력(아직 보지 못한 데이터로도 올바르게 문제를 해결할 수 있는 능력)을 제대로 평가하기 위해서이다.

  • 오버피팅: 한 데이터셋에서만 지나치게 최적화된 상태

손실함수

손실함수는 최적의 매개변수를 탐색하는 "지표"가 된다.
손실함수로 오차제곱합과 교차 엔트로피 오차를 사용한다.

1) 오차제곱합(SSE:Sum of Squares for Error)

E=12k=1M(yktk)2E = \frac{1}{2} \sum_{k=1}^M (y_k - t_k)^2

def sum_squares_error(y, t):
	return 0.5 * np.sum((y-t)**2)

1> yky_k는 신경망의 출력(추정한 값)
2> tkt_k는 정답 레이블
3> kk는 데이터의 차원수

>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] #원핫인코딩
>>> sum_squares_error(np.array(y), np.array(t))
0.097500...

>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> sum_squares_error(np.array(y), np.array(t))
0.597500...

여기서 정답은 2이다(0, 1, 2) 현재 y(확률: 소프트맥스의 출력)를 보면 신경망 모델이 2가 정답일 것이라고 맞게 추정하였다. 따라서 오차가 매우 작다.
반면 2번째 예제에서는 답이 틀렸기에 오차가 매우 큰 것을 볼 수 있다.

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

E=k=1MtklogykE = - \sum_{k=1}^M t_k \log y_k

tkt_k는 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0(원핫인코딩)이어야 함
따라서 정답빼고는 다 0이 곱해지기에, 정답일 때의 추정에 자연로그를 계산하는 식
ex> '2'가 정답이고 신경망 출력이 0.6이면 CEE는 log0.6=0.51-log0.6=0.51이 된다.

그래프를 보면 0<=x<=1이기 때문에 x의 값이 클수록 값은 작다. (앞에 minus 붙임)
즉 정답에 가장 높은 확률을 부여하면 값은 작아진다.

def cross_entropy_error(y, t):
	delta = 1e-7 # 매우 작은 값
    return -np.sum(t * np.log(y+delta))
  • 아주 작은값(delta)을 더하는 이유는 np.log()함수에 0을 입력하면 -inf가 되어 더 이상 계산을 진행할 수 없기 때문이다.
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] #원핫인코딩
>>> cross_entropy_error(np.array(y), np.array(t))
0.510825...

>>> 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.3025840...

3) 미니배치 학습

기계학습 문제는 모든 훈련 데이터를 대상으로 손실 함수를 구해야 한다.
지금까지는 1개의 손실함수 값을 구했지만 이제는 100개, 1000개, 10000개의 손실함수 값을 지표로 삼아야 한다는 것이다.

이제부터 손실함수는 교차엔트로피 오차를 쓴다고 가정한다.


E=1Nn=1Nk=1MtnklogynkE = - \frac{1}{N}\sum_{n=1}^N\sum_{k=1}^M t_{nk} \log y_{nk}

사실 매우 단순하다.
추정한 N개의 손실함수 값을 모두 더하고 N으로 다시 나누어 "평균 손실 함수"를 구하는 것이다.

그러나 모든 데이터를 대상으로 손실 함수의 합을 구하려면 시간이 걸린다. 이 경우 일부를 추려 '근사치'로 이용할 수 있다.
이렇게 신경망 학습에서도 훈련 데이터로부터 일부만 골라서 학습하는 것을 미니배치 학습이라고 한다.

>>> np.random.chice(60000, 10)
array([8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260, 21411])

np.random.choice()는 0이상 60000미만의 수 중에서 무작위로 10개를 골라낸다. 이렇게 무작위로 선택한 이 인덱스를 이용해 미니배치를 뽑아내면 된다.

배치용 교차 엔트로피 오차 구현

1) y가 1차원이면 ex>[1,2,3,4,5...], ([[1,2,3,4,5...]]) 강제적으로 2차원으로 reshape한다.
2) y.shape[0]을 통해 배치사이즈를 가지고 온다.
ex>(2,10) -> ([[1,2,3,4,5,6,7,8,9,10], [11,12,13,14,15,16,17,18,19,20]])의 경우에 배치 사이즈는 2이다.
3) 교차엔트로피오차의 총합과 평균을 구한다.

4) 왜 손실함수를 사용하는가?

정확도라는 지표를 사용하지 않고 손실함수를 사용하는 이유는 미분의 역할 때문이다.

정확도를 지표로 삼아서는 안되는 이유는 미분값이 대부분의 장소에서 0이 되어 매개변수를 갱신할 수 없기 때문이다.

예를 들어 한 신경망이 100장의 훈련 데이터 중 32장을 올바로 인식한다고 해보자, 이러면 정확도는 32%이다.
이 상황에서 매개변수를 약간만 조정해도 정확도는 32.0134와 같은 연속적인 값으로 변화하지 않고 33%, 34%처럼 불연속적으로 띄엄띄엄 값으로 바뀐다.

즉 정확도는 매개변수의 미세한 변화에는 반응조차 하지 않고 반응이 있더라도 그 값이 불연속적으로 확 변화한다.

  • 이는 활성화 함수로 계단함수를 택하지 않는 원리와 동일하다.
    (계단 함수는 대부분의 장소에서 기울기가 0)

수치 미분

1) 미분

미분은 한 순간의 변화량을 표시한 것이다.
ex> 처음부터 10분에서 2km씩 달렸다. -> 속도는 2 / 10 = 0.2[km/분]

10분이라는 시간(h)을 가능할 정도로 줄여(직전 1분에 달린거리, 직전 1초에 달린거리 ...)서 한 순간의 변화량을 얻는 것이다.

df(x)dx=limh0f(x+h)f(x)h\frac{df(x)}{dx} = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

1) xx의 작은변화가 함수 f(x)f(x)를 얼마나 변화 시키느냐를 의미한다.
2) 시간을 뜻하는 hh를 한없이 0에 가깝게 한다는 의미도 가진다.

수치 미분의 구현

가. 잘못된 구현

def numerical_diff(f,x):
	h = 10e-50
    return (f(x+h) - f(x)) / h

1) 반올림 오차가 일어난다. 10e-50은 0.0이 된다.
-> h로 1e-4를 사용해야한다.
2) 전방 차분으로 계산된다.((x+h)(x+h)xx의 차분)

-> 중심 차분으로 계산해야한다.((x+h)(x+h)(xh)(x-h)의 차분)

나. 올바른 표현

def numerical_diff(f,x):
	h = 1e-4
    return (f(x+h) - f(x-h)) / (2*h)

2) 편미분

f x ( x 0 , y 0 ) = lim h 0 f ( x 0 + h , y 0 ) f ( x 0 , y 0 ) h f y ( x 0 , y 0 ) = lim h 0 f ( x 0 , y 0 + h ) f ( x 0 , y 0 ) h f ( x , y ) = x 2 + y 2

f(x,y)=x2+y2f(x,y) = x^2 + y^2의 그래프는 3차원으로 그려진다.

  • 편미분: 변수가 여럿인 함수에 대한 미분
    편미분은 '어느 변수에 대한 미분'인지 구별해야한다.

편미분의 구현(x0=3,x1=4)x_0=3, x_1=4)

def function_tmp1(x0):
	return x0*x0 + 4.0**2.0
numercial_diff(finction_tmp1, 3.0)

1) x0x_0를 기준으로 원래 함수(function_tmp1)로 바꾼다.
f(x0,x1)=x02+x12f(x_0, x_1) = x_0^2 + x_1^2 -> f(x0,x1)=x02+16f(x_0, x_1) = x_0^2 + 16
2) function_tmp1을 수치미분하고 3.0을 대입한다.

기울기(Gradient)

앞서 x0x_0x1x_1의 편미분을 변수별로 따로 계산했다.
만약 (fx0 ,fx1)\left(\frac{\partial f}{\partial x_0}\ , \frac{\partial f}{\partial x_1}\right) 처럼 동시에 계산하고 싶다면?
이때 (fx0 ,fx1)\left(\frac{\partial f}{\partial x_0}\ , \frac{\partial f}{\partial x_1}\right) 와 같이 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 한다.

def numerical_gradient(f, x):
	h = 1e-4
    grad = np.zeros_like(x) # x와 형상이 같은 배열 생성 [0,0,0..]
    
    for idx in range(x.size):
    	tmp_val = x[idx]
        
        # f(x+h) 계산
        x[idx] = 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 function_2(x):
	return x[0]**2 + x[1]**2

>>> numercial_gradient(function_2, np.array([3.0, 4.0])
array([6., 8.])

이처럼 (x0,x1)(x_0, x_1)의 각 점에서의 기울기를 계산할 수 있다.

1) 경사하강법

손실함수(그래프)가 최솟값(f(x0,x1)f(x_0, x_1))이 될 때의
매개변수 값(x0x_0, x1x_1)을 학습하면서 찾는다.
이때 기울기를 이용한다.

Q: 기울기가 가리키는 곳에 정말 함수의 최솟값이 존재?
A: 보장할 수 없다.

x 0 = x 0 η f x 0 x 1 = x 1 η f x 1

기호 ηη(에타)는 갱신하는 양을 나타낸다.
이를 신경망 학습에서는 학습률(learning rate)이라고 한다.
이 단계를 반복하여 서서히 매개변수의 값(x0x_0, x1x_1)을 줄인다.
(기울기의 반대방향으로 가야 최솟값이 존재한다.)
학습률 값은 0.01, 0.001 등 미리 특정 값으로 정해둔다.(너무 커도, 너무 작아도 좋은장소를 찾아갈 수 없다.)

매개변수 값이 점점 증가 (음수인 기울기에 마이너스 붙어서 계속 증가하는 방향이 됨)
위의 2차 함수에서는 최솟값이 나오지만 복잡한 함수에서는 최솟값이 아니라 극솟값일 수 도 있다.

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
def function_2(x):
	return x[0]**2 + x[1]**2
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x, lr=0.1, step_num=100)
array([-6.1110793e-10, 8.14814391e-10])

1) 초깃값 (-3.0, 4.0)으로 설정 후 최솟값 탐색 시작
2) 최종결과는 거의 (0,0)에 가까움(경사법으로 거의 정확한 값을 얻음)

2) 신경망에서의 기울기

신경망에서의 기울기는 각 가중치 매개변수에 대한 손실 함수의 기울기이다.
즉 가중치와 똑같은 형태를 가진다.
밑의 LW\frac{\partial L}{\partial W}WW와 똑같은 형태이다.

W = ( w 11 w 12 w 13 w 21 w 22 w 23 ) L W = ( L w 11 L w 12 L w 13 L w 21 L w 22 L w 23 )

LW\frac{\partial L}{\partial W}의 각 원소는 각각의 원소에 관한 편미분이다.

신경망 기울기 구현

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
>>> net = simpleNet()
>>> print(net.W)
[[0.047..., 0.997..., 0.846...],
[0.855..., 0.0356..., 0.6947...]]

def f(W):
	return net.loss(x, t)
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[0.029..., 0.143..., -0.362...],
[0.328..., 0.215..., -0.544...]]

dW역시 W와 마찬가지로 2x3의 2차원 배열이다.
편미분을 기반으로 하여 각자 다 기울기가 다르다.

  • 람다문법
f = lambda w: net.loss(x, t) # w는 사용되지 않고 함수호출만 함
dW = numerical_gradient(f, net.W)

학습 알고리즘 구현

신경망 학습의 순서

미니배치 기반의 학습을 확률적 경사 하강법(Stochastic Gradient Descent: SGD)라고 한다.
"확률적으로 무작위로 골라낸 데이터"에 대해 수행하는 경사하강법이라는 의미이다.

1) 미니배치

훈련데이터 중 일부를 무작위로 가져온다.
이 미니배치값의 손실을 줄이는 것이 목표이다.

2) 기울기 산출

각 가중치 매개변수의 기울기를 구한다.

3) 매개변수 갱신

기울기의 반대 방향으로 가중치 매개변수를 갱신한다.

4) 반복

1 ~ 3 단계를 반복한다.

1) 2층 신경망 클래스 구현

class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 가중치 초기화
		# input_size: 입력층 노드 수, hidden_size: 은닉층 노드 수,output_size: 출력층 노드 수
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    # x : 입력 데이터, t : 정답 레이블
    def loss(self, x, t):
        y = self.predict(x)

        return cross_entropy_error(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # x : 입력 데이터, t : 정답 레이블
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

        return grads
  • 생성자 관련 설명
    1) weight_init_std: 가중치 초기화의 표준편차를 나타내며, 기본값은 0.01이다.
    2) weight_init_std * np.random.randn(input_size, hidden_size)는 평균이 0이고 표준편차가 weight_init_std인 정규분포를 따르는 난수를 생성하여 가중치를 초기화한다.
    3) np.random.randn(input_size, hidden_size)는 (input_size, hidden_size) 형상의 행렬을 생성한다.

2) 미니배치 학습 구현

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)

# 1)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

train_loss_list = []
train_acc_list = []
test_acc_list = []


iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1


# 2) 에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    # 3)
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
	
    # 4)
    grad = network.numerical_gradient(x_batch, t_batch)

    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate*grad[key]
    
    # 5)
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
	
    # 6)
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc: " + str(train_acc)+","+str(test_acc))

1) 기본적인 하이퍼파라미터 변수들을 설정하고 네트워크 객체를 초기화 한다.
2) 1에폭당 반복 수를 설정한다. 이는 굳이 train_loss와 test_loss를 매번 반복할때마다 기록하는게 아니라(이러면 10000번 다 기록함) 그 추이정도만 알기 위해서 만든다.
3) np.random.choice를 통해 0~60000범위에엇 100개의 랜덤한 값을 뽑는다. 그리고 이를 x_train, t_train의 인덱스로 넣어 랜덤한 100개의 값으로 만든다.
4) 기울기를 구하고 그 기울기의 반대방향으로 각각의 가중치를 옮긴다.
5) loss를 기록한다.
6) 아까 특별히 만든 iter_per_epoch의 배수이면 train 정확도와 test 정확도를 기록한다.(이러면 10000/600= 16, 즉 16번의 기록이 생긴다.)

# 5) 손실함수 값의 변화

# 6) 훈련데이터와 시험데이터의 정확도 추이

0개의 댓글