반응형

출처: https://sacko.tistory.com/41?category=632408

 

오차역전파법은 계층 형태로 순전파와 역전파를 메서드로 구현하여 효율적으로 기울기를 계산할 수 있도록 모듈화하여 신경망의 layer를 자유롭게 쌓고 쉽게 만들며 계산 속도를 빠르게 해준다. 딥러닝의 신경망 학습 모형은 이러한 layer들과 그 안의 함수들을 모듈로서 레고 블럭을 조립하듯이 조립하여 신경망을 구현할 수 있다.

 

 

오차역전파법 실습 2

문과생도 이해하는 딥러닝 (7)

앞선 "신경망 학습 실습" 포스팅에서는 순전파만을 고려하였고 기울기 계산을 수치 미분으로 하면서 계산이 오래 걸려서 결국.... 결과를 내지 못했다. 다행히 이번 오차역전파를 적용한 실습에서는 계산이 빨라 신경망 학습에 대한 결과까지 그래프로 확인할 수 있었다. 학습의 정확도는 약 98%, 테스트의 정확도는 약 97%까지 나왔다.

 

코드에 대한 설명은 추후 추가

 

from scratch.common.layers import * from scratch.common.gradient import numerical_gradient from collections import OrderedDict

 

class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # initializa Weights 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) # Build Layers self.layers = OrderedDict() self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1']) self.layers['Relu1'] = Relu() self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2']) self.lastLayer = SoftmaxWithLoss() def predict(self, x): for layer in self.layers.values(): x = layer.forward(x) return x def loss(self, x, t): y = self.predict(x) return self.lastLayer.forward(y, t) def accuracy(self, x, t): y = self.predict(x) y = np.argmax(y, axis=1) if t.ndim != 1 : t = np.argmax(t, axis=1) accuracy = np.sum(y==t) / float(x.shape[0]) return accuracy 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 def gradient(self, x, t): # foward self.loss(x, t) # backward dout = 1 dout = self.lastLayer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) grads = {} grads['W1'] = self.layers['Affine1'].dW grads['b1'] = self.layers['Affine1'].db grads['W2'] = self.layers['Affine2'].dW grads['b2'] = self.layers['Affine2'].db return grads

 

from scratch.dataset.mnist import load_mnist

 

(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True) network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) iters_num = 10000 print('number of iterations is', iters_num) train_size = x_train.shape[0] print('train size is', train_size) batch_size = 100 print('batch size is', batch_size) learning_rate = 0.1 train_loss_list = [] train_acc_list = [] test_acc_list = [] iter_per_epoch = max(train_size / batch_size, 1) print('='*20 + '>') epoch = 0 for i in range(iters_num): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] grad = network.gradient(x_batch, t_batch) for key in ('W1', 'b1', 'W2', 'b2'): network.params[key] -= learning_rate * grad[key] loss = network.loss(x_batch, t_batch) train_loss_list.append(loss) if i % iter_per_epoch == 0: epoch += 1 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('Epoch', epoch, ': ',train_acc,'\t', test_acc)

 

number of iterations is 10000 train size is 60000 batch size is 100 ====================> Epoch 1 : 0.102966666667 0.1055 Epoch 2 : 0.904416666667 0.9074 Epoch 3 : 0.9235 0.9252 Epoch 4 : 0.936166666667 0.9344 Epoch 5 : 0.9447 0.9432 Epoch 6 : 0.951983333333 0.9506 Epoch 7 : 0.955983333333 0.9537 Epoch 8 : 0.960733333333 0.9562 Epoch 9 : 0.965166666667 0.9602 Epoch 10 : 0.967366666667 0.9616 Epoch 11 : 0.969633333333 0.9652 Epoch 12 : 0.9721 0.966 Epoch 13 : 0.973833333333 0.9676 Epoch 14 : 0.975983333333 0.9686 Epoch 15 : 0.976883333333 0.9703 Epoch 16 : 0.9778 0.9694 Epoch 17 : 0.97895 0.9702

 

import matplotlib.pyplot as plt plt.figure(figsize=(20,8)) plt.plot(train_loss_list[:1000], linewidth=0.5) plt.title('Train Loss Graph') plt.xlabel('iteration') plt.ylabel('loss') plt.show()

 

 

plt.figure(figsize=(20,8)) plt.plot(train_acc_list, linewidth=1) plt.plot(test_acc_list, '-.' ,linewidth=1) plt.legend(['train acc', 'test acc'], loc=0) plt.xlabel('epochs') plt.ylabel('accuracy') plt.show()

 

 

 

 

반응형
반응형

출처: https://sacko.tistory.com/39?category=632408

 

지난 시간에는 구축한 신경망을 어떻게 학습해야 하는지 학습에 필요한 개념들에 대해서 다루었고 실제로 학습을 진행해보았다. 경사하강법을 이용해 수치미분으로 기울기를 계산하는 것은 계산 시간이 너무나 오래 걸려서 모델의 학습 시간이 굉장히 길었다. 학습을 할 때 사용되는 하이퍼파라미터(hyperparameter) 들에 대해서 알 수 있었으며 왜 가중치 갱신을 할 때 기울기를 구하는지, 손실함수는 왜 계산하는지, epoch의 정확한 의미는 무엇인지, 미니배치를 왜 사용하는지 등등 딥러닝의 핵심 개념들을 제대로 알 수 있었다.

 

이번에는 지난 포스팅에서 신경망을 학습하면서 수치미분으로 기울기를 계산하면서 학습시간이 굉장히 길어졌는데 이를 극복할 방법으로 오차역전파법을 살펴보고자 한다.

 

 

 

오차역전파법 실습 1

문과생도 이해하는 딥러닝 (6)

 지난 오차역전파 관련 포스팅에서는 오차역전파법이 순전파(foward propagation)로 가중치 학습이 되고 이를 갱신하기 위해서 오차를 반영하여 반대 방향에서 다시 가중치를 업데이트 한다는 식으로만 설명을 했다. 역전파를 사용하는 또 다른 중요한 이유는 역전파를 통해서 '미분'을 효율적으로 계산할 수 있다는 것이다.

 

 

먼저 순전파와 역전파 기능을 가진 클래스를 구현한다

 

 

class MulLayer: def __init__(self): self.x = None self.y = None def forward(self, x, y): self.x = x self.y = y out = x * y return out def backward(self, dout): dx = dout * self.y dy = dout * self.x return dx, dy

 

 

순전파 방식으로 구매한 사과 가격을 구하는 예시

 

apple = 100 apple_num = 2 tax = 1.1 mul_apple_layer = MulLayer() mul_tax_layer = MulLayer() # forward propagation apple_price = mul_apple_layer.forward(apple, apple_num) price = mul_tax_layer.forward(apple_price, tax) print(price)

 

220.00000000000003

 

# back propagation dprice = 1 dapple_price, dtax = mul_tax_layer.backward(dprice) dapple, dapple_num = mul_apple_layer.backward(dapple_price) print(dapple, dapple_num, dtax)

 

 

2.2 110.00000000000001 200

 

 

 

 

본 교재에서는 아래와 같은 계산 그래프 방식으로 신경망 학습 모델을 구현하였다.

신경망의 층을 하나의 클래스로 인스턴스를 만들면서 추가하는 방식이다.

 

class AddLayer: def __init__(self): pass def forward(self, x, y): out = x + y return out def backward(self, dout): dx = dout * 1 dy = dout * 1 return dx, dy

 

 

 

 

 

 

 

1. ReLU 오차역전파법 구현

ReLU는 활성화 함수로

x > 0 일 때, x를 그대로 반환하고, x <= 0 일 때, 0을 반환한다.

따라서 이를 미분해주면 x -> 1 이 되므로 오차역전파에서는

x > 0 일 때, 1을 전파하고 x <= 0 일 때, 을 전파한다.

 

 

class Relu: def __init__(self): self.mask = None def forward(self, x): ''' 순전파 시 x <= 0 일 때 값을 0으로 치환한다 기본적으로 x는 numpy 배열임을 가정한다 ''' self.mask = (x <= 0) out = x.copy() out[self.mask] = 0 return out def backward(self, dout): dout[self.mask] = 0 dx = dout return dx

 

 

 

 

 

2. Sigmoid 오차역전파법 구현

시그모이드 함수는 미분을 하면 dout * y(1-y)로 결과가 나온다. 출력값 y만으로 오차 역전파를 계산할 수 있다.

 

 

class Sigmoid: def __init__(self): self.out = None def forward(self, x): out = 1 / (1 + np.exp(-x)) self.out = out return out def backward(self, dout): dx = dout * (1.0 - self.out) * self.out return dx

 

 

 

3. Affine/Softmax 오차역전파법 구현

 

Affine 계층

Affine이라 함은 순전파에서 수행하는 행렬의 내적을 기하학에서 부르는 말로 Input 값과 Weight 값들을 행렬 곱하여 계산하고 거기에 편향(Bias)를 추가하여 출력값 Y를 최종적으로 반환하는 첫 포스팅(퍼셉트론)에서 배웠던 내용에 대한 것을 생각하면 된다

 

# Affine 계층 X = np.random.rand(2) # Input W = np.random.rand(2, 3) # Weight B = np.random.rand(3) # Bias print(X.shape) print(W.shape) print(B.shape) Y = np.dot(X,W) + B

 

(2,) (2, 3) (3,)

 

class Affine: def __init__(self, W, b): self.W = W self.b = b self.x = None self.dW = None self.db = None def forward(self, x): self.x = x out = np.dot(x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis = 0) return dx

 

 

 

Softmax-with-Loss 계층

output layer에서 사용하는 소프트맥수 함수는 입력 값을 확률의 형태로 normalize하여 결과값을 출력한다.

 

신경망에는 1)학습, 2)추론 이 있다.

 

추론에는 softmax 함수 같은 활성화함수를 사용하지 않고 affine layer에서 나온 결과 값을 그대로 사용하는 것이 일반적이며 우리가 흔히 말하는 score라고 한다. 추론에는 답을 내리기만 하면 되기 때문에 다른 활성화함수 들을 거쳐 정규화할 필요가 없고 높은 점수가 무엇인지만 파악할 수 있으면 되기 때문이다.

 

학습에는 softmax 함수를 사용하는데 이는 정규화한 출력 값을 이용해서 모델을 다시 업데이트해야 하기 때문에 정규화가 필요하다.

 

 

Softmax with Loss는 마지막 출력 층인 소프트맥스 함수의 결과 값을 손실함수로 오차도 계산하겠다는 의미이다.

 

 

<< 활성화함수 별 적합한 손실함수 >>

    • Softmax Function ==> Cross Entropy Error
    • Identity Function ==> Mean Squared Error

활성화 함수마다 적합한 손실함수가 있어(그렇게 설계되어 있음) 오차의 역전파가 말끔하게 계산된다.

 

 

class SoftmaxWithLoss: def __init__(self): self.loss = None # Loss self.y = None # Output self.t = None # Target def foward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] dx = (self.y - self.t) / batch_size return dx

 

 

 

 

 

반응형
반응형

출처:https://swalloow.github.io/bagging-boosting


오늘은 머신러닝 성능을 최대로 끌어올릴 수 있는 앙상블 기법에 대해 정리해보았습니다.

Ensemble, Hybrid Method

앙상블 기법은 동일한 학습 알고리즘을 사용해서 여러 모델을 학습하는 개념입니다. Weak learner를 결합한다면, Single learner보다 더 나은 성능을 얻을 수 있다는 아이디어입니다. Bagging 과 Boosting 이 이에 해당합니다.

동일한 학습 알고리즘을 사용하는 방법을 앙상블이라고 한다면, 서로 다른 모델을 결합하여 새로운 모델을 만들어내는 방법도 있습니다. 대표적으로 Stacking 이 있으며, 최근 Kaggle 에서 많이 소개된 바 있습니다.

Bagging

Bagging은 샘플을 여러 번 뽑아 각 모델을 학습시켜 결과를 집계(Aggregating) 하는 방법입니다. 아래의 그림을 통해 자세히 알아보겠습니다.

먼저 대상 데이터로부터 복원 랜덤 샘플링을 합니다. 이렇게 추출한 데이터가 일종의 표본 집단이 됩니다. 이제 여기에 동일한 모델을 학습시킵니다. 그리고 학습된 모델의 예측변수들을 집계하여 그 결과로 모델을 생성해냅니다.

이러한 방식을 Bootstrap Aggregating 이라고 부릅니다.

이렇게 하는 이유는 “알고리즘의 안정성과 정확성을 향상시키기 위해서” 입니다. 대부분 학습에서 나타나는 오류는 다음과 같습니다.

  1. 높은 bias로 인한 Underfitting
  2. 높은 Variance로 인한 Overfitting

앙상블 기법은 이러한 오류를 최소화하는데 도움이 됩니다. 특히 Bagging은 각 샘플에서 나타난 결과를 일종의 중간값으로 맞추어 주기 때문에, Overfitting을 피할 수 있습니다.

일반적으로 Categorical Data인 경우, 투표 방식 (Voting)으로 집계하며 Continuous Data인 경우, 평균 (Average)으로 집계합니다.

대표적인 Bagging 알고리즘으로 RandomForest 모델이 있습니다. 원래 단일 DecisionTree 모델은 boundary가 discrete 한 모양일 수 밖에 없지만, RandomForest는 여러 트리 모델을 결합하여 이를 넘어설 수 있게 되었습니다.

결과는 아래와 같습니다.

Boosting

Bagging이 일반적인 모델을 만드는데 집중되어있다면, Boosting은 맞추기 어려운 문제를 맞추는데 초점이 맞춰져 있습니다.

수학 문제를 푸는데 9번 문제가 엄청 어려워서 계속 틀렸다고 가정해보겠습니다. Boosting 방식은 9번 문제에 가중치를 부여해서 9번 문제를 잘 맞춘 모델을 최종 모델로 선정합니다. 아래 그림을 통해 자세히 알아보겠습니다.

Boosting도 Bagging과 동일하게 복원 랜덤 샘플링을 하지만, 가중치를 부여한다는 차이점이 있습니다. Bagging이 병렬로 학습하는 반면, Boosting은 순차적으로 학습시킵니다. 학습이 끝나면 나온 결과에 따라 가중치가 재분배됩니다.

오답에 대해 높은 가중치를 부여하고, 정답에 대해 낮은 가중치를 부여하기 때문에 오답에 더욱 집중할 수 있게 되는 것 입니다. Boosting 기법의 경우, 정확도가 높게 나타납니다. 하지만, 그만큼 Outlier에 취약하기도 합니다.

AdaBoost, XGBoost, GradientBoost 등 다양한 모델이 있습니다. 그 중에서도 XGBoost 모델은 강력한 성능을 보여줍니다. 최근 대부분의 Kaggle 대회 우승 알고리즘이기도 합니다.

Stacking

Meta Modeling 이라고 불리기도 하는 이 방법은 위의 2가지 방식과는 조금 다릅니다. “Two heads are better than one” 이라는 아이디어에서 출발합니다.

Stacking은 서로 다른 모델들을 조합해서 최고의 성능을 내는 모델을 생성합니다. 여기에서 사용되는 모델은 SVM, RandomForest, KNN 등 다양한 알고리즘을 사용할 수 있습니다. 이러한 조합을 통해 서로의 장점은 취하고 약점을 보완할 수 있게 되는 것 입니다.

Stacking은 이미 느끼셨겠지만 필요한 연산량이 어마어마합니다. 적용해보고 싶다면 아래의 StackNet을 사용하시는 걸 강력하게 추천합니다.

https://github.com/kaz-Anova/StackNet

문제에 따라 정확도를 요구하기도 하지만, 안정성을 요구하기도 합니다. 따라서, 주어진 문제에 적절한 모델을 선택하는 것이 중요합니다.

반응형
반응형

출처: https://sacko.tistory.com/38?category=632408

 

신경망 알고리즘으로 모델을 만들고 학습하는 방법에 대해 간단하게 실습을 진행하였다. 그리고 학습할 때 필요한 필수 개념인 손실함수, 배치, 기울기, 학습률에 대해서 알아보았다. 수학적 개념만으로는 이해하기 어려운 부분들이 다소 있었으나 코드를 보면서 진행하니 확실히 각 개념과 역할에 대해서 이해하기 수월하였다.

 

 

 

신경망 학습 Learning 실습

문과생도 이해하는 딥러닝 (5)

인공신경망(Neural Network)도 다른 머신러닝 알고리즘의 모델들 같이 모델에 대한 학습이 필요하다. 지금까지 배운 것은 모델을 구축하기 위해 필요한 개념이나 기능 등에 대한 것이었다면 설계한 모델을 학습용 데이터를 가지고 어떻게 학습할 것인지 인공신경망 방식은 어떻게 학습하는 것인지 알아볼 것이다.

 

기본적으로 머신러닝 모델의 학습 방법은

 

  1. 데이터를 탐색한 후 전처리하고
  2. 데이터 변수 등을 분석하여 전체 훈련용, 테스트용 데이터 셋을 구성한 다음
  3. 해결하고자 하는 문제에 맞는 알고리즘을 선택하여 모델을 만든 후
  4. 훈련용 데이터 셋으로 모델을 학습시키고
  5. k-folds 교차검증 및 테스트용 데이터 셋 으로 모델 간 검증을 진행하고
  6. 최고의 성능을 보이는 모델을 최종 배치한다.

 

일반적으로 머신러닝 모델은 위와 같이 문제 해결을 위한 모델을 개발하며 딥러닝에서도 이와 크게 다르지 않다. 다만 차이점은 위에서 진행한 2번의 순서가 사라지는 특징이 있다. 다른 머신러닝은 데이터를 구성하기 위해서 변수를 선택하기도 하고 사람이 생각한 특징(features)에 따라 데이터에 대한 작업을 한 번 더 진행하지만, 신경망(딥러닝)은 데이터 그 자체에서 스스로 중요한 특징을 찾아간다는 end-to-end learning이 가능한 것이 큰 차이다. 규칙을 기계가 스스로 찾는다는 점에서 인공신경망이 더욱 재미있고 이전에 풀지 못한 문제를 풀 수 있을 것이라는 기대를 할 수 있게 되는 것이다.

 

딥러닝도 머신러닝과 비슷하게 학습과 검증을 해야 하기 때문에 이 기법(technique)에 대해서 좀 더 다루면 좋겠지만 다른 포스팅 시리즈에서 이 부분에 대해 더 다루는 것으로 하고 이번 포스팅 시리즈에서는 핵심개념 위주로 정리하는 식으로 다룰 것이다.

 

1. 손실 함수 Loss Function

딥러닝을 공부하다보면 손실 함수라는 말이 엄청 자주 나온다. 처음 모델링을 하기 위해 딥러닝을 접했을 때 이 용어가 이해를 많이 방해했었다.

 

손실함수는 신경망을 학습할 때 학습 상태에 대해 측정하는 하나의 지표로 사용한다. 신경망의 가중치 매개변수들이 스스로 특징을 찾아 가기에 이 가중치 값의 최적이 될 수 있도록 해야 하며 잘 찾아가고 있는지 볼 때 손실 함수를 보는 것이다.

 

 

1) 평균제곱오차 Mean Squared Error

평균제곱오차는 손실 함수로 가장 많이 쓰이며 통계를 공부한 사람이라면 자주 듣는 용어 중 하나일 것이다. 간단하게 설명하면 예측하는 값이랑 실제 값의 차이(error)를 제곱하여 평균을 낸 것이 평균제곱오차이다. 

 

예측 값과 실제 값의 차이가 클수록 평균제곱오차의 값도 커진다는 것은 이 값이 작을 수록 예측력이 좋다고 할 수 있다.

 

 

평균제곱오차 Mean Squared Error

 

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

 

#정답은 2 t = [0,0,1,0,0,0,0,0,0,0] y1 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] print(mean_squared_error(np.array(y1), np.array(t))) y2 = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] print(mean_squared_error(np.array(y2), np.array(t)))

 

0.0975 0.5975

 

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

교차엔트로피는 로그의 밑이 e인 자연로그를 예측값에 씌워서 실제 값과 곱한 후 전체 값을 합한 후 음수로 변환한다. 실제 값이 원핫인코딩(one-hot encoding; 더미변수처럼 1~9까지 범주로 했을 때 정답이 2일 경우 2에는 '1'을 나머지 범주에는 '0'으로) 방식 일경우에는 2를 제외한 나머지는 무조건 0이 나오므로 실제값일 때의 예측값에 대한 자연로그를 계산하는 식이 된다. 실제 값이 2인데 0.6으로 예측했다면 교차 엔트로피 오차는 -log(1*0.6) = -log0.6 이 된다. = 0.51

 

교차 엔트로피는 출력이 1일 때 0이 되며 x가 커질수록 0에 가까워지고 x가 작아질수록(0에 가까워질수록) 값이 작아진다(음의방향).

 

 

교차 엔트로피 오차 Cross Entropy Error

 

def cross_entropy_error(y,t): delta = 1e-7 return -np.sum(t * np.log(y+delta))

 

y + delta인 이유는 만약 y가 0일 때는 infinite 값을 반환하므로 계산이 안되기 때문에 아주 작은 임의의 값을 입력하여 값이 -inf가 되는 것을 막는다

 

t = [0,0,1,0,0,0,0,0,0,0] y1 = [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(y1), np.array(t))) y2 = [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(y2), np.array(t)))

 

0.510825457099 2.30258409299

 

 

평균제곱오차에서 계산한 것과 동일하게 처음의 예측이 더 잘맞았음을 교차엔트로피 오차로 확인할 수 있다.

 

 

 

2. 미니배치 학습 Mini-Batch

기계학습은 앞에서 말한 것처럼 가장 예측을 잘하는 모델을 찾는 것이라고도 할 수 있다. 딥러닝에서는 가중치 매개변수에 대한 손실함수 값이 가장 작은 것은 찾아야 하므로 훈련용 데이터 셋을 대상으로 손실 함수 값을 구한다. 그리고 구한 값이 하나의 지표로 모델의 성능을 판단할 때 활용한다.

 

앞의 교차 엔트로피 오차에서 구한 것은 하나의 케이스(instance)에 대한 손실함수를 구한 것으로 만약 mnist 데이터와 같이 약 6만개 정도의 케이스에 대해서 학습을 할 때는 교차 엔트로피 오차를 6만번 계산해야되게 된다. 만약 실제 빅데이터 환경에 간다면 6만이 아니라 6백만, 6천만, 6억개의 데이터가 될 수도 있으며 일일이 계산한다면 굉장히 오랜 시간이 걸리게 될 것이다. 따라서 한번에 하나만 계산하는게 아니라 일부를 조금씩 가져와서 전체의 '근사치'로 이용하여 일분만 계속 사용하여 학습을 수행하는 방법을 이용한다. 그 일부를 미니배치 mini-batch 라고 한다. 훈련 데이터에서 일부를 무작위로 뽑아 학습하는 것은 미니배치 학습이다.

 

미니배치는 무작위로 추출하는 것으로 표본을 무작위로 샘플링하는 것과 개념적으로 유사하다.

 

 

미니배치 학습 Mini-Batch Learning

 

from 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)

 

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(train_size, batch_size)

 

array([24632, 13808, 21266, 43860, 34652, 15548, 58670, 47186, 30791, 22296])

 

10개에 대한 인덱스가 무작위로 추출된 모습이다

 

 

미니배치를 위한 교차 엔트로피 오차 구현

 

# 타겟이 one-hot encoding 방식일 경우 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.reshpae(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

 

 

손실함수를 사용하는 이유는 궁극적으로 높은 정확도를 가진 가중치 매개변수를 구하기 위함이다. 최적의 가중치와 편향을 구할 때 손실함수의 값을 가능한 한 작게 하는 값을 찾으며 이때 미분(기울)을 계산하고 그 미분 값을 통해서 가중치와 편향에 대한 매개변수의 값을 서서히 갱신하는 과정을 반복한다.

 

손실함수의 미분은 가중치 매개변수의 값을 변화시켰을 때의 손실함수의 변화를 의미한다. 손실함수의 값이 음수이면 양의방향으로 값을 줄이도록 이동하고, 양수이면 음의방향으로 값을 줄이도록 이동하며 미분 값이 0이 되어 변화가 없을 때 가중치 매개변수의 갱신을 멈춘다.

 

매개변수의 작은 변화에 반응하여 손실함수의 값도 함께 변화해야하므로 정확도로 보는 것은 적합하지 않다. 계단함수가 0의 값을 많이 가져 이를 시그모이드 함수 같은 활성화함수로 매끄럽게 변화시키는 것과 같은 이치이다.

 

 

 

 

3. 미분

경사법에서는 기울기 값을 기준으로 방향을 정한다. 

 

미분은 한 순간의 변화량을 계산한 것이다. 미분에 대한 것은 따로 수학에 대해서 다루고 있는 다른 포스팅 시리즈를 참고하는 것이 좋다.

 

 

 

1) 수치미분

 

h는 시간을 뜻하고 이를 한 없이 0에 가깝게 한다는 의미로 lim를 주었다. 분자는 a에 대한 변화량을 나타낸다.

 

이와 같은 방식으로 미분을 구하는 것은 수치 미분이라고 한다. 차분(; 임의의 두 점에서 함수 값들의 차이)으로 미분을 구하기 때문이다. 수치 미분은 오차가 포함될 수 밖에 없다. 오차를 줄이기 위해서는 x를 중심으로 h 만큼의 함수 f의 차분을 계산하여 구하기도 하며 이를 중심 차분 또는 중앙 차분이라고 한다. 위의 식은 전방 차분을 의미한다.

 

해석학적으로 미분을 구하는 것은 보통 그 테크닉을 알고 있는 방법으로 수식을 이용해서 구하는 것이다.

 

 

미분 계산 예시 (수치미분)

 

# 중심차분, 중앙차분 def numercial_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 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 일 때, 함수의 미분 계산 print(numercial_diff(function_1, 5)) # x = 10 일 때, 함수의 미분 계산 print(numercial_diff(function_1, 10))

 

1.9999999999908982e-09 2.999999999986347e-09

 

이렇게 계산한 미분 값이 x에 대한 f(x)의 변화량이다. 함수의 기울기를 의미한다. 해석학적으로 이를 풀이해도 0.2와 0.3으로 오차가 매우 작음을 알 수 있다. 거의 같은 값이다.

 

 

2) 편미분

변수가 여러 개인 함수에 대한 미분을 편미분이라고 한다. 

 

편미분 역시 변수가 하나인 미분과 동일하게 특정 장소에 대한 기울기를 구한다. 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정한다.

 

 

미분계산 편미분 예시

 

def function_2(x): return x[0]**2 + x[1]**2

 

# x0 = 3, x1 = 4 일 때, x0에 대한 편미분 def function_tmp1(x0): return x0*x0 + 4.0**2.0 print(numercial_diff(function_tmp1, 3.0)) # x0 = 3, x1 = 4 일 때, x1에 대한 편미분 def function_tmp2(x1): return 3.0**2.0 + x1*x1 print(numercial_diff(function_tmp2, 4.0))

 

6.000000000003781e-08 7.999999999999119e-08

 

 

 

4. 기울기 Gradient

모든 변수의 편미분을 벡터로 정리한 것을 기울기 gradient라고 한다. 다른 포스팅에서 경사하강법에 대해 설명하면서 기울기에 대해 다루기도 했었다. 

 

기울기는 가장 낮은 장소를 가리키지만 각 지점에서 낮아지는 방향을 의미한다. 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 줄이는 방향이라고 할 수 있다.

 

 

1) 경사하강법

최적의 가중치와 편향을 찾기위해서 학습을 진행하며 손실함수가 최솟값을 갖도록 하는 것이다. 가중치의 차원이 커질수록 이 손실함수의 최솟값을 갖는 지점을 찾는 것은 어려워진다. 기울기를 이용해서 함수의 최솟값이 어디에 있는지 찾는 것이며 이를 경사하강법이라고 한다.

 

기울기를 통해서 손실함수의 최솟값이 그쪽에 있는지 보는 것이며 최소한의 방향을 나타낸다. 최솟값이 되는 지점에서는 기울기가 0이 된다. 주의할 것은 기울기가 0이라고 반드시 그 지점이 최솟값이라고 할 수 없다. 경사하강법에서 발생할 수 있는 문제이기도 하다.

 

 

위의 그림을 보면 plateau라는 지점에서 기울기가 0이 되어 최솟값이 아님에도 최솟값으로 인식하고 학습이 중지된 상태다.

 

기울기 구하기

 

def numerical_gradient(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] = 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

 

# 임의의 세 점에서의 기울기 계산 (수치미분) print(numerical_gradient(function_2, np.array([3.0, 4.0]))) print(numerical_gradient(function_2, np.array([0.0, 2.0]))) print(numerical_gradient(function_2, np.array([3.0, 0.0])))

 

[ 6. 8.] [ 0. 4.] [ 6. 0.]

 

Learning Rate

아래는 학습률 Learning Rate라고 하며 한 번 학습할 때 얼마만큼 학습해야 하는지 학습 양을 의미하며 한 번의 학습량으로 학습한 이후에 가중치 매개변수가 갱신된다.

 

 

학습률 값은 미리 0.01, 0.001과 같이 특정 값을 정해두어야 하며 일반적으로 이 값이 너무 크거나 작으면 적합한 지점으로 찾아가기가 어렵다. 신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인한다.

 

학습률이 너무 크면 큰 값을 반환하고, 너무 작으면 거의 갱신되지 않고 학습이 끝나버린다.

 

학습률은 하이퍼파라미터 hyperparameter라고 부르며 가중치와 편향 같은 신경망의 매개변수와는 다르다. 가중치 등은 훈련용 데이터 셋과 학습 알고리즘에 따라 자동적으로 생성되는 것임에 비해서 학습률 같은 하이퍼파라미터는 사람이 수동적으로 설정해야 하기 때문에 시험을 통해서 가장 적절한 값을 찾아가야 한다.

 

기울기를 계산한 후에 기울기 값을 기존 가중치(기울기)에 더하거나 빼서 가중치를 업데이트하는데 이때 계산된 기울기에 미리 설정해둔 학습률을 곱해서 나온 값으로 가중치를 업데이트하도록 한다. 예를들어 내가 계산한 기울기 값이 +6이어서 기존의 가중치 값이 5이면 가중치의 업데이트 이후에는 -1이 된다. 하지만 학습률을 0.1로 설정하게 되면 +6 * 0.1로 0.6이 되고 이 때의 업데이트 결과는 5 - 0.6으로 4.4가 된다.

 

학습률이 너무 작으면 (0.00001), 5 - 0.00006 = 4.99994 가 되므로 가중치의 변화가 거의 없어 학습으로 가중치가 제대로 갱신도 되지 않고 끝나버리게 된다. 따라서 학습률을 어떻게 잡는지는 중요한 이슈이다.

 

학습률 Learning Rate

 

def gradient_descent(f, init_x, lr= 0.01, step_num=100): ''' f는 최적화려는 함수 init_x는 초깃값 lr은 학습률 step_num은 경사법에 따른 반복 횟수 함수의 기울기는 앞서 정의한 numerical_gradient로 구하고 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 setp_num번 반복 ''' x = init_x for i in range(step_num): grad = numerical_gradient(f, x) x -= lr * grad return x

 

학습률이 너무 크거나 작으면 좋은 결과를 얻기가 힘들다

 

# 학습률이 너무 큰 경우 lr = 10.0 init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)

 

array([ -2.58983747e+13, -1.29524862e+12])

 

# 학습률이 너무 작은 경우 lr = 1e-10 init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)

 

array([-2.99999994, 3.99999992])

 

 

2) 신경망에서의 기울기

신경망 학습도 기울기를 구해야 하며 여기서의 기울기는 가중치 매개변수에 대한 손실 함수의 기울기이다. 

 

 

신경망에서의 기울기

 

from scratch.common.functions import softmax, cross_entropy_error from scratch.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

 

net = simpleNet() print(net.W)

 

[[-0.98857721 -1.19498403 1.7798275 ] [ 1.92945432 2.08198445 0.48024869]]

 

x = np.array([0.6, 0.9]) p = net.predict(x) print(p) np.argmax(p)

 

[ 1.14336256 1.15679559 1.50012032]

 

2

 

t = np.array([0,0,1]) net.loss(x, t)

 

0.87935693550588223

 

def f(W): return net.loss(x, t) dW = numerical_gradient(f, net.W) print(dW)

 

 

[[ 0.17430645 0.17666371 -0.35097016] [ 0.26145968 0.26499557 -0.52645524]]

 

 

 

 

 

5. 학습 알고리즘 구현

1) 전제

신경망에는 적응 가능한 가중치와 편향... 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 것이 '학습'.

 

2) 1단계: 미니배치

훈련 데이터 중 일부 무작위 추출. 미니배치의 손실 함수를 줄이는 것을 목표

 

3) 2단계: 기울기 산출

미니배치 손실 함수 값을 줄이기 위해 가중치 매개변수의 기울기 구함. 기울기는 손실 함수의 값을 최소화하는 방향을 가리킴

 

4) 3단계: 매개변수 갱신

가중치 매개변수 기울기 방향으로 조금 갱신

 

5) 1~3 단계 반복

 

이러한 학습 방법을 stochastic gradient descent(확률적 경사 하강법)이라고 하며 많이들 들어봤을 것이다.

 

 

학습 횟수가 늘어날수록 손실 함수의 값이 떨어진다면 이는 신경망의 가중치들이 학습 데이터에 잘 맞아들고 있음을 의미하며 이를 학습이 잘되고 있다고 볼 수 있다.

 

 

신경망 모델 클래스 생성

 

2층 신경망 클래스 구현

 

from scratch.common.functions import * from scratch.common.gradient import numerical_gradient class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): 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 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 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

 

net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) net.params['W1'].shape # (784, 100) net.params['b1'].shape # (100,) net.params['W2'].shape # (100, 10) net.params['b2'].shape # (10,)

 

(10,)

 

 

예제로 2층 신경망 학습시켜보기

nuemrical_gradient는 수치미분으로 기울기를 계산하기 때문에 계산이 오래 걸린다. 신경망이 그렇게 복잡하지 않음에도 상당한 시간이 소요됐다.

 

 

x = np.random.rand(100, 784) t = np.random.rand(100, 10) %time grads = net.numerical_gradient(x, t) print(grads['W1'].shape) print(grads['b1'].shape) print(grads['W2'].shape) print(grads['b2'].shape)

 

CPU times: user 3min 49s, sys: 564 ms, total: 3min 50s Wall time: 57.7 s (784, 100) (100,) (100, 10) (10,)

 

 

검증 testing

하지만 오버피팅의 문제가 있을 수 있으므로 이를 확인하기 위해서 테스트 데이터를 통해서 일반적으로 잘 들어맞는지 확인할 필요가 있다. 오버피팅overfitting은 학습한 모델이 학습 데이터에만 잘들어 맞고 다른 실제 상황에서 잘 맞지 않음을 말한다.

 

이를 위해서 하나의 epoch 단위 별로 훈련 데이터와 테스트 데이터에 대한 모델의 정확도를 확인할 필요가 있다.

 

 

Epoch 에포

에포는 하나의 학습 단위인데 1 에포는 학습에서 훈련 데이터를 가지고 훈련을 모두 완료했을 때를 말하며, 즉 학습 횟수를 의미한다. 예를 들어 1000개에 대한 훈련 데이터를 10개의 미니배치로 학습한다면 확률적 경사하강법을 100회 반복해서 가중치를 계산하고 나서 100*10=1000으로 학습이 완료되고 이 때의 100회가 1 에포가 되며 학습이 한 번 됐다고 하는 것이다.

 

 

미니배치를 위한 학습을 구현했을 때 변수에 대해서 설명하자면

 

  • iter_num : 가중치 갱신을 몇 번 하는지
  • train_size : train 데이터의 총 개수
  • batch_size : 한 번에 학습을 다하기 어려우니 나누어서 갱신을 하는데 이때 미니배치를 사용하며 배치 사이즈는 몇 개씩 뽑아서 쓸 것인지... 한 번의 기울기 갱신에 몇 개의 train 데이터를 사용할 것인지 그 개수를 의미
  • epoch : 하나의 단위로 train_size/batch_size로 구하며 하나의 미니배치에 대해서 갱신을 몇 번 해야 전체 train 데이터에 대해서 한 번의 학습이 가능한지를 나타낸다. 100개의 train_size에 batch_size가 10이면 10번의 iter로 한 번의 학습이 가능하므로 epoch는 10이 된다. 10번 미니배치를 추출해서 기울기 계산과 가중치를 업데이트하면 한 번 전체 train 데이터에 대해서 학습했다고 볼 수 있다.
  • # of epochs : 총 iter(기울기 계산, 가중치 업데이트) 횟수를 epoch의 단위로 나누면 전체 train 데이터에 대해서 몇 번 학습했는지 알 수 있다. 예를 들어 10,000개의 데이터에 대해서 가중지 업데이트를 1,000번 한다고 했을 때, 배치 사이즈를 1,000이라고 한다면 epoch 단위는 10이 된다. 따라서 # of epochs는 100번(= 1,000/10)이 된다. 학습은 총 10번 하게 되는 것이다.

 

 

 

예제로 미니배치와 에포를 이용해서 학습을 한 결과는 학습이 끝나고 업로드하겠다.

생각보다 계산량이 많아서 기울기 계산과 가중치 업데이트에 시간이 오래걸린다.

 

 


 

 

 

 

 

기본적인 신경망이 학습이 정말 오래 걸린다는 것을 깨달았다. 이러한 문제를 해결하기 위해서 오차역전파 방법이 빠르게 기울기를 계산할 수 있도록 한다고 하니 다음 포스팅에서 이를 다루도록 하겠다.

반응형

+ Recent posts