본문 바로가기

딥러닝/밑바닥부터 시작하는 딥러닝

4장 : 신경망 학습

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

손실함수 : 신경망이 학습할 수 있도록 해주는 지표

학습의 목표 : 손실함수의 결괏값을 가장 작게 만드는 가중치 매개변수를 찾는 것

 

4.1 데이터에서 학습한다!

4.1.1 데이터 주도 학습

데이터 -> 특징(feature) 추출 -> 패턴 학습 (기계학습: SVM, KNN) 

특징 : 입력 데이터에서 본질적인 데이터를 정확하게 추출할 수 있도록 설계된 변환기 (보통 벡터로 기술)

한계 : 특징은 여전히 사람이 설계해야 함

 

-> 신경망은 특징까지 기계가 스스로 학습 (종단간 기계학습 : 처음부터 끝까지 기계가 학습)

 

4.1.2 훈련 데이터와 시험 데이터

  • 기계학습 문제는 데이터를 훈련 데이터와 시험 데이터로 나눠 학습과 실험을 수행하는 것이 일반적
  • 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾는다.
  • 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가 
  • 한 데이터셋에만 지나치게 최적화된 상태인 오버피팅 피하는 것이 기계학습의 중요한 과제

4.2 손실 함수

  • 신경망 학습에서 최적의 매개변수 값을 탐색하는 기준 지표
  • 신경망 성능의 '나쁨'을 나타내는 지표
  • 일반적으로 오차제곱합과 교차 엔트로피 오차 사용

 

4.2.1 오차제곱합

y_k  : 신경망의 출력 (신경망이 추정한 값)

t_k  : 정답 레이블 (one-hot-encoding)

k  : 데이터의 차원 수

 

#오차제곱합 함수 정의
def sum_squares_error(y,t):
  return 0.5*np.sum((y-t)**2)
#정답 : 2
t = [0,0,1,0,0,0,0,0,0,0]  #정답 레이블 (원-핫 인코딩 표기됨)

#예1
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]  #정답에 더 가까움, 2를 0.6으로 제일 큰 확률이라고 예측
print(sum_squares_error(np.array(y), np.array(t)))

#예2
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

  • y는 소프트맥스 함수의 출력 (확률, 예: 이미지가 0,1,2,,,,,일 확률)
  • t는 정답을 가리키는 위치의 원소는 1로, 그 외에는 0으로 표기 (2가 정답, 1로 표기됨 )

 

4.2.2 교차 엔트로피 오차

y_k : 신경망의 출력 (신경망이 추정한 값)

t_k : 정답 레이블 (one-hot-encoding) 

       

-> 정답일 때의 y의 자연로그를 계산하는 식 => 정답일 때의 출력이 전체 값을 정하게 됨

(정답이 아닌 t들은 모두 0이기 때문)

#교차엔트로피오차 함수 정의
def cross_entropy_error(y,t):
  delta = 1e-7
  return -np.sum(t*np.log(y+delta))
  • 아주 작은 값인 delta를 y에 더해 줌
  • 마이너스 무한대가 발생하지 않도록 하기 위해 
#위와 똑같은 예
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

 

4.2.3 미니배치 학습

  • N개의 데이터로 확장 
  • 마지막에 N으로 나누어 정규화 -> 평균 손실 함수
    -> 모든 데이터를 대상으로 손실 함수의 합을 구하려면 상당한 시간 소요

 

  • 미니배치 : 데이터 일부를 추려 전체의 '근사치'로 이용
    미니배치 학습 : 훈련 데이터로부터 일부만 골라 학습 수행 
(X_train, t_train), (X_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

print(X_train.shape)
print(t_train.shape)

train_size = X_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)  #60000미만의 수에서 무작위로 10개 추출
X_batch = X_train[batch_mask]  #batch_mask인덱스의 X_train값
t_batch = t_train[batch_mask]  #batch_mask인덱스의 t_train값
  • np.random.choice()함수로 무작위 추출

 

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

def cross_entropy_error(y,t):
  if y.ndim ==1:  #y가 1차원인 경우, 데이터 하나씩 오차 구하는 경우
    t = t.reshape(1, t.size)  #2차원으로 바꿔줌 
    y = y.reshape(1, y.size)
  
  batch_size = y.shape[0]  #y가 1차원이 아닌 경우, y의 행 수가 배치 사이즈 
  return -np.sum(t*np.log(y+1e-7))/batch_size  #배치 사이즈로 나눔으로써 정규화 -> 데이터 1개의 평균 교차 엔트로피 오차
#정답 레이블이 원-핫 인코딩이 아니라 숫자 레이블로 주어졌을 때
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.arrange(batch_size), t]+1e-7))/batch_size
  • np.arrange(batch_size) : 0부터 batch_size -1까지 배열 생성 
    batch_size가 5이면 [0,1,2,3,4] 넘파이 배열 생성
  • y[np.arrange(batch_size), t]: t에 대한 y값(예측 확률) 추출 
    [y[0,2], y[1,7], y[2,0], y[3,9], y[4,4]]

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

  • 신경망 학습에서는 최적의 매개변수 (가중치와 편향)를 탐색할 때 손실 함수의 값을 가능한 한 작게 하는 매개변수 값을 찾음
  • 매개변수의 미분 (기울기)을 계산하고, 그 미분 값을 단서로 매개변수 값을 서서히 갱신
  • 가중치 매개변수의 손실 함수의 미분 : 가중치 매개변수의 값을 아주 조금 변화 시켰을 때, 손실 함수가 어떻게 변하나 
    • 미분 값 음수 -> 가중치 매개변수 양의 방향으로 변화 -> 손실 함수 값 줄임
  • 정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 있더라도 불연속적으로 갑자기 변화
    • 계단 함수를 활성화 함수로 사용하지 않는 이유와도 같음 
    • 계단 함수의 미분은 대부분의 장소 (0이외의 곳)에서 0 -> 손실 함수 값에 변화 x
    • 시그모이드 함수의 미분연속적으로 변화 -> 미분이 어느 장소라도 0이 아님 => 신경망이 올바르게 학습 가능

4.3 수치 미분

4.3.1 미분

미분 : 한순간의 변화량

: x의 작은 변화가 함수 f(x)를 얼마나 변화시키느냐 (h: 작은변화)

  • 구현
    • h = 1e-4 (0.0001)
    • 오차를 줄이기 위해 중심 차분 (중앙 차분) 사용 
def numerical_diff(f, x):
    h = 1e-4 #0.0001
    return (f(x+h) - f(x-h)) / (2*h)
  • 수치 미분 : 아주 작은 차분으로 미분하는 것 (해석적 미분을 근사치로 계산하는 방법)
  • 해석적 미분 : 수식을 전개해 미분하는 것 ( 수학 시간에 배운 그 미분)

 

4.3.2 수치 미분의 예

#수치미분 예
def funtion_1(x):
    return 0.01*x**2 + 0.1*x

print(numerical_diff(funtion_1, 5))
print(numerical_diff(funtion_1, 10))

 

4.3.3 편미분

: 변수가 여럿인 함수에 대한 미분

  • 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정(상수 취급)
#편미분
def funtion_2(x):
    return x[0]**2 + x[1]**2

#문제 1: x0 =3, x1=4일 때 x0에 대한 편미분 구하라
def funtion_tmp1(x0):
    return x0*x0 +4.0**2.0  #변수가 x0하나인 식으로 수정 

print(numerical_diff(funtion_tmp1, 3.0))

#문제 2: x0 = 3, x1=4일 때 x1에 대한 편미분 구하라
def funtion_tmp2(x1):
    return 3.0**2.0 + x1*x1

print(numerical_diff(funtion_tmp2, 4.0))

6.00000000000378

7.999999999999119 (해석적 미분의 결과와 거의 같음)


4.4 기울기

: 모든 변수의 편미분을 벡터로 정리한 것

def numerical_gradient(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] = 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
#예시
numerical_gradient(funtion_2, np.array([3.0, 4.0]))

array([6., 8.])

  • 기울기 의미 : 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향

 

4.4.1 경사법(경사 하강법)

  • 학습 시에 최적의 매개변수(가중치와 편향)를 찾기 위해 손실 함수가 최솟값이 되는 곳을 찾아야 함.
    -> 경사법 : 기울기를 이용해 함수의 최솟값을 찾는 것
  • 현 위치에서 기울어진 방향으로 일정 거리만큼 이동, 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 기울어진 방향으로 나아가기를 반복하는 것
  • 기계학습 최적화에 흔히 쓰는 방법
  • 신경망 학습에 많이 사용됨

  • η(에타) : 갱신하는 양, 학습률(learning rate) 
    : 한 번의 학습으로 얼마만큼 학습해야 할지, 매개변수 값을 얼마나 갱신하느냐 
  • 식처럼 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것
  • 학습률은 너무 크거나 작으면 안됨 (변경해가면서 진행) -> 하이퍼파라미터 (사람이 직접 설정해야 함)
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 : 학습률
  • step_num : 경사법에 따른 반복 횟수
#예시
def funtion_2(x):
    return x[0]**2 +x[1]**2

init_x = np.array([-3.0, 4.0])
gradient_descent(funtion_2, init_x = init_x, lr = 0.1, step_num=100)

array([-6.11110793e-10, 8.14814391e-10]) 

 

4.4.2 신경망에서의 기울기

신경망 학습에서의 기울기 : 가중치 매개변수에 대한 손실 함수의 기울기

1행 1번째 원소 편미분 = w11을 조금 변경했을 때 손실 함수 L이 얼마나 변화하느냐 

#신경망에서의 기울기
#간단한 신경망 예시로 기울기 구하는 코드 구현
import sys, os
sys.path.append("/content/drive/MyDrive/Colab Notebooks/밑바닥딥러닝/common")
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):  #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)  #t:정답 레이블 

        return loss
net = simpleNet()
print(net.W)  #가중치 매개변수

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)
print(np.argmax(p))  #최댓값의 인덱스

t = np.array([0,0,1])  #정답 레이블
net.loss(x, t)  #손실함수 값

def f(W):
    return net.loss(x, t)

dW = numerical_gradient(f, net.W)
print(dW)  #기울기

[[ 0.53901031 -2.65287024 1.01427435]

[-0.61315748 -1.30049799 -1.1916805 ]] ->가중치 매개변수 

[-0.22843555 -2.76217034 -0.46394784] -> p

0  -> 최댓값 인덱스

[[ 0.32093654 0.02547019 -0.34640673] 

[ 0.48140481 0.03820528 -0.51961009]] -> 기울기

기울기가 0.3 -> w11을 h만큼 늘리면 손실함수의 값은 0.3h만큼 증가  => 음의 방향으로 갱신

             -0.5 -> w23을 h만큼 늘리면 손실함수의 값은 0.5h만큼 감소 => 양의 방향으로 갱신 (w11보다 크게 기여)


4.5 학습 알고리즘 구현하기

  • 절차:

전제 : 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정이 학습.

신경망 학습은 다음과 같이 4단계로 수행.

 

1단계 - 미니배치

훈련 데이터 중 일부를 무작위로 가져옴. 이렇게 선별된 데이터를 미니배치라 하며, 그 미니배치의 손실 함수 값을 줄이는 것이 목표.

 

2단계 - 기울기 산출

미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구함. 기울기는 손실 함수의 값을 가장 작게 하는 방향 제시.

 

3단계 - 매개변수 갱신

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

 

4단계 - 반복

1~3단계를 반복.

 

  • 확률적 경사 하강법(stochastic gradient descent, SGD) : 미니배치로 데이터를 무작위로 선정하고 경사 하강법으로 매개변수를 갱신하는 방법

 

4.5.1 2층 신경망 클래스 구현하기

import sys, os
sys.path.append("/content/drive/MyDrive/Colab Notebooks/밑바닥딥러닝/common")
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)  #0으로 초기화
        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

    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

 

4.5.2 미니배치 학습 구현하기

  • 미니배치 크기 100
  • 매번 60000개의 훈련 데이터에서 임의로 100개의 데이터 추림
  • 100개의 미니배치를 대상으로 확률적 경사 하강법을 수행해 매개변수 갱신 
  • 갱신 횟수 10000번으로 설정, 갱신할 때마다 훈련 데이터에 대한 손실 함수를 계산, 그 값을 배열에 추가 

4.5.3 시험 데이터로 평가하기

  • 오버피팅을 확인하기 위해 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도 기록
  • 1에폭별로 정확도 기록
  • 에폭(epoch): 하나의 단위, 1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당
    예: 훈련 데이터 10000개를 100개의 미니배치로 학습할 경우, 확률적 경사 하강법을 100회 반복하면 모든 훈련데이터 소진
     -> 100회가 1에폭
  • 에폭이 진행될수록 훈련데이터와 시험데이터 정확도가 모두 좋아지면 오버피팅 x
  • 오버피팅이 있으면 시험 데이터에 대한 정확도 점점 떨어짐 -> 조기종료 사용 (6장 오버피팅 예방법)
#미니배치 학습 구현하기
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)

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

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

#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)  #60000개의 훈련 데이터에서 임의로 100개의 데이터 추리기
    x_batch = X_train[batch_mask]
    t_batch = t_train[batch_mask]

    #기울기 계산
    grad = network.gradient(x_batch, t_batch)  #성능 개선판 : 오차역전파법
    #grad = network.numerical_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()

-> 에폭 진행될수록 정확도 좋아짐


4.6 정리

  • 기계학습에서 사용하는 데이터셋은 훈련 데이터와 시험 데이터로 나눠 사용
  • 훈련 데이터로 학습한 모델의 범용 능력을 시험 데이터로 평가
  • 신경망 학습은 손실 함수를 지표로, 손실 함수의 값이 작아지는 방향으로 가중치 매개변수를 갱신
  • 가중치 매개변수를 갱신할 때는 가중치 매개변수의 기울기를 이용하고, 기울어진 방향으로 가중치의 값을 갱신하는 작업을 반복
  • 아주 작은 값을 주었을 때 차분으로 미분하는 것이 수치 미분
  • 수치 미분을 이용해 가중치 매개변수의 기울기를 구할 수 있음
  • 수치 미분을 이용한 계산에는 시간이 걸리지만, 그 구현은 간단함.
  • 다음 장에서 구현하는 오차역전파법은 기울기를 고속으로 구할 수 있음.

 


책 참고 : 밑바닥부터 시작하는 딥러닝 (한빛미디어)

책 깃허브 : https://github.com/WegraLee/deep-learning-from-scratch

 

 

'딥러닝 > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

6장 : 학습 관련 기술들  (0) 2023.09.15
5장 : 오차역전파법  (0) 2023.09.13
3장 : 신경망  (0) 2023.09.05
2장 : 퍼셉트론  (0) 2023.09.04
1장 : 헬로 파이썬  (0) 2023.09.02