퍼셉트론과 신경망(딥러닝)의 가장 큰 차이는 데이터로부터 기계가 스스로 학습이 가능하다는 점이다.
데이터를 직선으로 분리가 가능한, 즉 선형 분리가 가능한 문제에서는 퍼셉트론 수렴 정리를 통해 유한 번의 학습으로 해결할 수 있다는 것이 증명되었다. 하지만 대부분의 문제는 비선형 분리 문제이고 이를 퍼셉트론으로는 자동으로 학습할 수 없다.
기계학습은 데이터가 핵심인 문제이다.
사람이 중심이 되어 해결하는 것이 아니라 데이터가 중심이 되어 문제를 해결한다.
이런 방식은 사람의 개입을 최소화할 수 있다.
앞서 다뤘던 손글씨 인식 문제(MNIST dataset)를 생각해보자.
손글씨 이미지를 보고 이를 분류하는 알고리즘을 짠다고 생각해보면 어떻게 해야 할지 쉽사리 떠오르지 않는다.
주어진 데이터를 활용한다면 이를 보다 쉽게 해결이 가능하다. 기계학습에서는 데이터로부터 규칙을 찾아내는 역할을 기계가 담당한다.
다만, 이미지를 벡터로 변환할 때 사용하는 특징은 사람이 정해준다. 적절한 특징을 사람이 정해주지 않으면 좋은 결과를 기대하기 어렵다.
특징은 이미지를 분류하기 위해 기계가 고려할 parameter와 같은 역할을 한다.
신경망(딥러닝)은 기계학습과는 달리, 이미지(데이터)로부터 중요한 특징(feature)마저도 기계가 스스로 학습한다.
데이터를 온전히 학습하고, 주어진 문제의 패턴을 발견하려 시도한다.
훈련 데이터를 활용하여 데이터를 학습하며 최적의 매개변수를 찾는다. 그러면 접해보지 않은 다른 데이터(시험데이터)로 학습된 정도를 판별한다.
만일 데이터셋 하나로만 반복적으로 학습하면 한 데이터셋에 지나치게 최적화된다. 이런 상태를 오버피팅이라고 한다.
오버피팅을 피하는 것은 기계학습에서 중요한 과제이며 이를 위해 다양한 데이터셋을 학습시키는 것이 매우 중요하다.
데이터를 학습하여 찾은 매개변수가 적합한지를 판별할 지표가 필요하다. 신경망에서 이를 손실 함수(loss function or cost function)라고 한다. 일반적으로 쓰이는 함수는 평균 제곱 오차(mean squared error; MSE)와 교차 엔트로피 오차(cross entropy error)이다.
평균 제곱 오차를 수식으로 나타내면 다음과 같다.
# y_k : 신경망의 출력(신경망이 추정한 값)
# t_k : 정답 레이블
# k : 데이터의 차원 수
# 수식에서 1/2은 convention(관습)으로서 일반적으로 사용되는 값이다. 해당 값을 사용하는 이유는 MSE를 미분했을 때 제곱 역할을 하는 지수가 전체 식에 상수 2로서 곱해지기 때문에 1/2을 곱하여 이를 제거하기 위함이다.
1/2이 없거나 다른 값이어도 전혀 문제될 것은 없다. 임의로 지정이 가능한 부분이다.
이해를 위해 간단한 예를 들어보자.
앞서 손글씨 숫자 인식문제에서 y_k와 t_k는 다음과 같은 데이터 형식을 가진다.
y_k = [0.1, 0.05, 0.6, 0., 0.05, 0.1, 0., 0.1, 0., 0.]
t_k = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0,]
이 배열에서 원소들은 순서대로 0~9의 숫자에 대한 값이다.
y는 소프트맥스함수의 출력이라고 하면 이는 각 숫자가 정답과 일치할 확률을 의미한다.
t는 0과 1로만 나타태는 원-핫 인코딩의 형식이다. 여기서 3의 인덱스를 가지는 숫자는 2이며, 2가 정답임을 의미한다.
가장 큰 값을 가지는 인덱스를 정답으로 인식했음을 알 수 있다.
이제 평균 제곱 오차를 이용하여 오차를 직접 구해보자.
이때 비교를 위해 다른 배열에 대해 구해보자.
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)
# 첫 번째 예
y_k = [0.1, 0.05, 0.6, 0., 0.05, 0.1, 0., 0.1, 0., 0.]
t_k = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0,]
error = mean_squared_error(np.array(y_k), np.array(t_k))
print("ex1)", error)
# 두 번째 예
y_k = [0.1, 0.05, 0.1, 0., 0.05, 0.6, 0., 0.1, 0., 0.]
t_k = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0,]
error = mean_squared_error(np.array(y_k), np.array(t_k))
print("ex2)", error)
ex1) 0.09750000000000003
ex2) 0.5975
두 번째 예에서는 y에서 숫자 6에 해당하는 위치에 가장 높은 확률인 0.6을 부여하였다.
정답은 2이므로, 첫 번째에 비해 오차가 매우 커진 것을 알 수 있다.
둘 중 첫 번째 배열이 정답에 더 가까울 것이라는 것을 오차함수를 통해 구한 오차를 통해 알 수 있다.
다음은 교차 엔트로피 오차(cross entropy error) 함수다. 수식은 다음과 같다.
# y_k : 신경망의 출력(신경망이 추정한 값)
# t_k : 정답 레이블 (원-핫 인코딩의 형태)
# k : 데이터의 차원 수
# log : 밑이 e인 자연로그
평균 제곱 오차와 교차 엔트로피 오차의 차이는 다음과 같다.
전자는 y의 모든 원소가 오차 함수의 결과에 영향을 미친다.
후자는 y의 원소 중 정답에 해당하는 원소의 값만이 오차 함수의 결과에 영향을 미친다.
교차 엔트로피는 정답에 해당하는 출력이 커질수록(1에 가까워질수록) 0에 다가가다가, 그 출력이 1일 때 0(오차가 0)이 된다. 반대로 출력이 작아질수록(0에 가까워질수록) 값(오차)이 커진다.
이제 앞과 같이 이해를 위해 간단한 예를 들어 오차를 비교해보자.
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t*np.log(y+delta))
# 첫 번째 예
y_k = [0.1, 0.05, 0.6, 0., 0.05, 0.1, 0., 0.1, 0., 0.]
t_k = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0,]
error = cross_entropy_error(np.array(y_k), np.array(t_k))
print("ex1)", error)
# 두 번째 예
y_k = [0.1, 0.05, 0.1, 0., 0.05, 0.1, 0., 0.6, 0., 0.]
t_k = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0,]
error = cross_entropy_error(np.array(y_k), np.array(t_k))
print("ex2)", error)
ex1) 0.510825457099338
ex2) 2.302584092994546
cross_entropy_error함수를 보면 np.log내에서 y에 delta라는 매우 작은 임의의 값을 더해주는 것을 확인할 수 있다.
실제로 0이라는 값이 log함수에 대입되면 -inf가 출력되어 더이상 계산을 수행하지 않기 때문에 delta는 이를 방지하는 값이다.
결과를 살펴보면 두 번째 예에서 무려 2가 넘는 오차를 보인다.
교차 엔트로피 오차 역시 앞과 마찬가지로 첫번째 예가 더 정확함을 나타내고 있다.
앞서 3장에서 배치처리에 대해 알아보았다.
이를 이용하여 데이터가 100개라면 100개 모두를 하나의 배치(batch)로 생각해 전체 데이터의 손실함수를 구할 수 있다.
이를 수식으로 다음과 같이 나타낼 수 있다.
수식에서는 데이터의 수 N으로 나눠줌으로써 정규화하여 평균 손실 함수를 구하고 있다.
하지만 이는 실제 문제에서 부적합하다. 앞서 살펴봤던 MNIST 문제만 해도 데이터가 60000개다. 이를 하나하나 모두 계산해주기에는 cost(비용)가 너무 많이 든다. 빅데이터라면 수백만, 수천만이 넘는 데이터가 존재하기도 한다.
이런 경우를 해결하기 위해 훈련 데이터 중 일부만을 가져와 학습하고 이를 전체 데이터셋으로 근사시키기도 한다. 이때 전체에서 가져온 일부의 데이터를 미니배치(mini-batch)라고 한다. 그리고 이렇게 학습하는 방법을 미니배치 학습이라고 한다.
이제 훈련 데이터에서 랜덤하게 데이터를 골라내는 코드를 작성해보자. 이번에도 MNIST데이터셋을 사용할 것이다.
# MNIST 데이터셋 읽어오기
import sys, os
sys.path.append(".../ML_scratch/deep-learning-from-scratch-master/deep-learning-from-scratch-master/ch03")
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)
# os.pardir를 이용하여 부모 디렉터리의 경로에 접근하여 파일에 가져오게 하는 것 역시 가능하다. 나는 경로를 append하는 방식으로 했지만 각자 편한 방법을 사용하면 된다.
x_train의 데이터의 형태를 보면 훈련 데이터는 60000개고 입력 데이터는 784열(28 X 28 데이터에 flatten=True를 적용했기 떄문에)인 이미지 데이터임을 알 수 있다.
정답 레이블에 해당하는 t_train은 마찬가지로 60000개이고 10열(0~9의 숫자)인 데이터다.
무작위로 10개를 뽑아 미니배치를 만들려면 np.random.choice() 함수를 이용하면 된다. 이 함수의 두 개의 인수를 숫자로 받는다. 0이상 첫번째 인수 미만의 범위에서 두번째 인수만큼 랜덤하게 숫자를 추출한다.
# 10개를 랜덤으로 추출하여 만든 미니 배치
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size) # 배열의 index를 batch_size만큼 랜덤하게 추출
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
print(x_batch.shape)
print(t_batch.shape)
(10, 784)
(10, 10)
배치 처리를 적용한 교차 엔트로피 오차를 구현하는 것은 간단하다.
기존의 함수를 약간만 수정하면 된다.
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]
delta = 1e-7
return -np.sum(np.log(y[np.arange(batch_size), t] + delta)) / batch_size
여기서는 t*np.log(y) 대신에 np.log(y[np.arange(batch_size), t])를 사용한다.
원-핫 인코딩을 적용하였기 때문에 t가 0인 값은 교차 엔트로피 오차가 모두 0이므로, 그 계산을 무시함으로써 연산 속도를 더욱 빠르게 한 것이다.
4장의 뒷부분에서는 numerical differentiation과 partial differentiation, gradient, gradient descent method에 대해 설명하는 부분이 있다. 이는 이 블로그의 gradient descent method 알고리즘을 구현한 포스팅으로 대체하겠다.
## 링크
지금까지 여러 개념들을 배웠는데, 신경망 학습의 순서를 정리하면 다음과 같다.
전제
: 신경망에는 적용 가능한 가중치와 편향이 존재한다. 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라고 한다.
신경망 학습의 4단계는 다음과 같다.
1단계 - 미니배치
: 훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 이 미니배치의 cost function 값을 줄이는 것이 목표다.
2단계 - 기울기 산출
: 미니배치의 cost function 값을 줄이기 위해 각 weight parameter의 gradient를 구한다. gradient는 cost function을 가장 작게 하는 방향을 제시한다.
3단계 - 매개변수 갱신
: 앞서 구한 gradient의 방향으로 weight parameter를 hyper parameter(학습률; learning rate)만큼 갱신시킨다. 이때 갱신되는 값은 아주 작다.
4단계 - 반복
: 1~3단계를 반복한다.
위의 단계들은 gradient descent method을 사용할 때이며, 이때 미니배치를 무작위로 선정하기 때문에 stochastic gradient descent(확률적 경사 하강법)이라고 한다.
대부분의 딥러닝 프레임워크는 stochastic gradient descent의 머리글자를 딴 SGD라는 함수로 이 기능을 구현한다.
지금까지 배운 것들을 이용하여 신경망 클래스를 구현할 수 있다. 처음에는 가장 간단한 형태인 2층 신경망 클래스를 구현하는 것부터 시작한다.
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
from common.functions import *
from 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) # 은닉층 크기만큼 parameter 생성
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size) # 출력층 크기만큼 parameter 생성
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) # sigmoid function을 활성화함수로 하여 은닉층의 출력 계산
a2 = np.dot(z1, W2) + b2
y = softmax(a2) # 출력
return y
# x : 입력 데이터, t : 정답 레이블
def loss(self, x, t):
y = self.predict(x) # y는 예측 레이블, t는 정답 레이블
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]) # y==t인 경우 True를 반환하면 1로 인식되므로 np.sum을 하면 정답의 개수가 됨
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']) # w1 parameter에 대한 편미분
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 # gradient, 즉 기울기
def gradient(self, x, t): # 다음 장에서 구현 예정. numerical_gradient보다 성능이 업그레이드 된 버전
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)
return grads
클래스의 코드가 길어 복잡해보일 수 있으므로, 책에서 정리한 표를 가져왔다.
TwoLayerNet 클래스의 인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수이다.
MNIST 손글씨 데이터셋에서는 크기가 28 X 28인 이미지가 총 784개이고, 출력은 10개가 된다. input_size = 784, output_size=10이 되며 hidden_size에는 적당한 값을 설정한다.
또한 위의 코드에서는 가중치 매개변수를 정규분포를 따르는 난수로, 편향은 0으로 초기화하였지만, 실제로는 weight parameter를 어떤 값으로 초기화하느냐가 신경망 학습의 성패를 결정짓기도 한다. 이에 대한 자세한 내용은 뒤에서 다루게 될 것이다.
미니배치를 이용한 학습 코드도 구현할 수 있다.
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
# from two_layer_net import TwoLayerNet
# 데이터 읽기
(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 # 반복 횟수를 적절히 설정한다.
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)
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.numerical_gradient(x_batch, t_batch)
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)
# 1에폭당 정확도 계산
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))
# 그래프 그리기
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
train acc, test acc | 0.11435, 0.1157
train acc, test acc | 0.7850333333333334, 0.7884
train acc, test acc | 0.8753833333333333, 0.8811
train acc, test acc | 0.8959166666666667, 0.8998
train acc, test acc | 0.9079666666666667, 0.9122
train acc, test acc | 0.9155, 0.9185
train acc, test acc | 0.92, 0.9224
train acc, test acc | 0.92515, 0.9268
train acc, test acc | 0.9284, 0.9293
train acc, test acc | 0.9311, 0.9318
train acc, test acc | 0.9346, 0.9353
train acc, test acc | 0.9375166666666667, 0.937
train acc, test acc | 0.9399833333333333, 0.939
train acc, test acc | 0.9422333333333334, 0.9404
train acc, test acc | 0.9435666666666667, 0.9428
train acc, test acc | 0.9457666666666666, 0.945
train acc, test acc | 0.9471666666666667, 0.9472

# epoch(에폭) : 배치의 크기에 따라 달라지는 하나의 단위. 예를 들어 10000개의 데이터가 있을 때 100개의 미니배치로 학습한다면, 이를 100회 반복하면 모든 훈련 데이터를 소진한 것이 된다. 이때 100회의 반복횟수가 1 epoch이 된다.
위의 그림은 1 epoch마다 정확도를 계산하여 그 추이를 나타낸 그래프이다. 훈련 데이터와 시험 데이터의 정확도가 모두 좋아지고 있으며, 두 정확도의 차이가 거의 없는 것을 보아 이 학습은 오버피팅 없이 정상적으로 학습이 진행되었음을 알 수 있다.
'딥러닝' 카테고리의 다른 글
[Incarnate the Algorithm] PCA & K-means Clustering (0) | 2023.05.23 |
---|---|
[Deep Learning from scratch] 5. 오차역전파법 (0) | 2023.05.15 |
[Incarnate the Algorithm] Linear Classification (2) | 2023.04.11 |
[Incarnate the Algorithm] Linear Regression (0) | 2023.04.07 |
[Incarnate the Algorithm] Gradient Descent (0) | 2023.04.07 |