본문 바로가기
밑바닥 딥러닝/밑바닥부터 시작하는 딥러닝1

6장

by sxlvxrjxms2s2itsmes2s2 2023. 6. 22.

6.1 매개변수 갱신

6.2 가중치의 초기값

6.3 배치정규화

6.4 바른학습을 위해

6.5 정리

 

6.1 매개변수 갱신

신경망의 학습 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것 = 매개변수의 최적값을 찾는 문제 = 최적화

 

매개변수 공간은 매우 넓고 복잡해 최적화는 어려운 문제이다.

 

우리는 지금까지 최적의 매개변수 값을 찾는 단서로 매개변수의 기울기(미분)를 이용했다. 매개변수의 기울기를 구해 기울어진 방향으로 매개변수 값을 갱신하는 일을 진행해 최적의 값으로 다가간다. 이것이 확률적 경사 하강법(SGD)이다.

SGD외의 다른 최적화 기법을 소개한다.

 

1) 확률적 경사 하강법 (SGD)

  • W : 갱신할 가중치 매개변수
  • aL/aW은 W에 대한 손실함수의 기울기
  • η : 학습률
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr #학습률
    
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

- SGD의 단점

단순하고 구현하기 쉽지만 문제에 따라 비효율적일 때가 있다.

다음 식의 최솟값을 구하는 문제를 생각해본다.

 

이함수의 기울기는 y축 방향은 크고 x축 방향은 작다는 것이 특징이다.

이 경우 상당히 비효율적인 움직임을 보여준다.

SGD의 단점은 비등방성(방향에 따라 성질, 여기에서는 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이라는 것이다.

이를 개선할 방법을 살펴볼 수 있다.

 

2) 모멘텀

모멘텀은 관성을 가지고 있어서 이전 기울기가 진행하던 방향으로 진행하려는 성질을 가진다.

기울기가 크면 그만큼 가중치 갱신치가 크고, 기울기가 작으면 그만큼 가중치 갱신을 작게하는 방법이다.

  • W : 갱신할 가중치 매개변수
  • aL/aW은 W에 대한 손실함수의 기울기
  • η : 학습률
  • v : 속도 (a=0.9등의 값으로 설정 = 1보다 작은 임의의 값을 곱해서 기울기를 줄여준다)
class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
    
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]

SGD와 비교하여 볼 때 지그재그 정도가 덜한 것을 알 수 있다.

처음엔 기울기가 크니 가중치를 크게 갱신, 그 후 기울기가 감소할수록 가중치를 작게 갱신한다.

 

3) AdaGrad

신경망 학습에서 학습률의 값이 너무 작으면 학습 시간이 너무 길어지고, 반대로 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다.

 

이 학습률을 정하는 기술로 학습률 감소(learning rate decay)가 있다. 이는 학습을 진행하면서 학습률을 점차 줄여가는 방법이다.

 

AdaGrad에서는 각각의 매개변수에 맞춤형 학습률을 만들어준다.

 

  • W : 갱신할 가중치 매개변수
  • aL/aW : W에 대한 손실함수의 기울기
  • η : 학습률
  • h : 기존 기울기 값을 제곱해 계속 더해줌, 또한 매개변수를 갱신할 때는 1/route(h)를 곱해 학습률을 조정

매개변수의 원소 중 많이 움직인(크게 갱신된)원소는 학습률이 낮아진다는 뜻

= 즉 학습률 감소가 매개변수의 원소마다 다르게 적용됨을 말한다.

 

AdaGrad는 과거의 기울기를 제곱하여 계속 더해나가기 때문에 학습을 진행할 수록 갱신 강도가 약해진다. 그래서 무한대로 학습을 해나가면 어느 순간 갱신량이 0이 되어 전혀 갱신이 되지 않느다. 이 문제를 개선한 기법으로 RMSProp 기법이 있다. 이것은 과거의 모든 기울기를 균일하게 더해가는 것이 아니라, 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영한다. 지수이동평균이라고도 하며 과거 기울기의 반영 규모를 기하급수적으로 감소시킨다.


AdaGrad를 소스코드로 구현하면 다음과 같다.

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

마지막에 1e-y이라는 작은 값을 더해서 0으로 나누는 상황을 막는다는 것이 중요한 포인트이다. AdaGrad에 의한 최적화 갱신 경로는 다음과 같아진다.

최솟값을 향해 효율적으로 움직이는 것을 알 수 있다.

 

4) Adam

모멘텀과 AdaGrad를 융합한 기법이다.

매개변수 공간을 효율적으로 탐색하면서 하이퍼파라미터의 편향 보정이 진행된다. 

class Adam:
		def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

Adam은 하이퍼 파라미터를 3개를 설정한다. 지금까지의 학습률, 일차 모멘트용 계수, 이차 모멘트용 계수가 그것들이다.

모든 문제에서 뛰어난 기법은 아직 없고 문제와 상황을 고려해 여러가지를 시도해보아야 한다.

 

5) MNIST 데이터셋으로 본 갱신 방법 비교

각 층이 100개의 뉴런으로 구성된 5층 신경망에서 ReLU를 활성화 함수로 사용해 측정한다.

일반적으로 SGD보다 다른 기법들이 빠르게 학습하고 최종 정확도도 높다.

 

6.2 가중치의 초기값

신경망의 가중치의 초기값을 무엇으로 설정하느냐가 신경망 학습의 성패를 가르는 일이 자주 있다.

 

1) 초기값이 0일 때

  • 가중치 감소 기법 : 가중치 매개변수의 값이 작아지도록 하여 오버피팅이 일어나지 않도록 하는 기법
  • 가중치를 작게 만들고 싶으면 초깃값도 작은 값에서 시작 --> 0으로 설정은 안됨
  • 초깃값을 0으로 설정하게 되면 오차역전파에서 모든 가중치값이 독같이 갱신되기 때문이다.
  • 순전파 때 모든 뉴런에 같은 값이 입력되므로, 역전파 두번째 층의 가중치가 모두 똑같이 갱신된다는 뜻(학습이 안됨)
  • (가중치를 균일한 값으로 설정X) 가중치 초깃값을 무작위로 설정해야 한다.

 

2) 은닉층의 활성화 값 분포

 

은닉층의 활성화값(활성화 함수의 출력데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있다.

가중치 초깃값에 따라 은닉층 활성화값들이 어떻게 변화하는지 실험해 본다.

  • 시그모이드 함수 사용 5층 신경망에 무작위 생성 입력데이터를 흘리며각 층의 활성화 값 분포를 히스토그램으로 그린다.
import numpy as np
import matplotlib.pyplot as plt
 
def sigmoid(x):
  return 1 / (1 + np.exp(-x))
 
x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층 노드의 수
hidden_layer_size = 5 # 은닉층 5개
activations = {} # 활성화 값 저장할 공간
 
for i in range(hidden_layer_size):
  if i != 0:
    x = activations[i-1]
 
  w = np.random.randn(node_num, node_num) * 1 # 가중치 분포의 표준편차 = 1
  a = np.dot(x, w)
  z = sigmoid(a)
  activations[i] = z
# 히스토그램 그리기
for i, a in activations.items():
  plt.subplot(1, len(activations), i + 1)
  plt.title(str(i+1) + '-layer')
  plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

 

2-1) 표준편차가 1인 정규분포 이용

각 층의 활성화값들이 0과 1에 치우쳐 분포되어 있다. 시그모이드 함수는 그 출력이 0에 가까워지자 (또는 1) 그 미분은 0에 다가간다.

이렇게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다. 이것이 기울기 소실(gradient vanishing) 문제이다.

 

 

2-2) 가중치의 표준편차를 0.01로 바꿔 초기화한다.

활성화 값들이 0.5 부근에 집중된다.

이 경우 다수의 뉴런이 거의 같은 값을 출력하고 있으니 뉴런을 여러개 둔 의미가 없어진다는 뜻이다.

 

3) Xavier 초깃값

앞 계층의 노드가 n개라면 표준편차가 sqrt(1/n)인 분포를 사용한다.

Xavier 초깃값을 사용하면 앞 층의 노드가 많을수록 대상 노드의 초긱값으로 설정하는 가중치가 좁게 퍼진다.

Xavier 초깃값을 이용할 때의 각 층의 활성화값 분포

 

4) ReLU를 사용할 때의 가중치 초기값

  • ReLU 이용 시에는 ReLU에 특화된 초깃값 이용 권장
  • He 초깃값 : 앞 층의 노드가 n개 일 때, 표준편차가 sqrt(2/n)인 정규분포 사용
  • ReLU는 음의 영역이 0이기 때문에 더 넓게 분포시키기 위해 2배의 계수가 필요
  • ReLU를 이용한 경우 활성화 값 분포 (0.01std, Xavier, He)

  • std=0.01일 때의 각 층의 활성화 값들은 아주 작은 값들이다.
  • 신경망에 아주 작은 데이터가 흐른다는 것은 가중치의 기울기 역시 작아져 학습이 제대로 이루어지지 않는다.
  • Xavier는 층이 깊어지면서 치우침이 조금씩 커짐 = 기울기 소실문제 일으킴
  • He초깃값은 모든 층에서 균일분포 = 역전파 때도 적절값 나올 가능성 높음

따라서 ReLU 사용 시 He, sigmoid, tanh 등의 S자 모양 곡선에서는 Xavier초깃값 사용이 모범 사례이다.

 

5) MNIST 데이터셋으로 본 가중치 초깃값 비교

 

  • 층별 뉴런수 100개 / 5층 신경망 / 활성화 함수 = ReLU 사용
  • std=0.01에서는 전혀 학습되지 않음 (순전파 때 너무 작은 값이 흐르기 때문)
  • Xavier와 He는 학습 원할, 학습 진도는 He가 빠름
  • 가중치 초깃값에 따라 학습의 성패가 갈리는 경우 많음 = 초깃값 중요!

 

6.3 배치 정규화

가중치의 초깃값을 적절히 설정하면 각 층의 활성화값 분포가 적당히 퍼지면서 학습이 원할하게 수행된다. 이런 생각으로 각 층의 활성화를 고의로 퍼뜨리도록 하는 것이 배치 정규화(batch normalization)이다.

 

1) 배치정규화가 주목받는 이유

- 학습속도 개선

- 초깃값에 크게 의존하지 않음

- 오버피팅 억제

 

배치정규화의 삽입 위치

배치정규화는 학습 시 미니배치를 단위로 정규화한다. (데이터 분포 평균 0, 분산이 1이 되도록)

 

  • 미니배치 B = {x1, x2 ... , xm}이라는 m개의 입력 데이터 집합에 대해 평균(uB)와 분산(a2B) 구함
  • 평균 0, 분산 1이 되도록 정규화
  • //normalize 단계에서 e(삼지창기호, epsilon)는 매우 작은 값으로, 0으로 나누는 사태를 예방
  • 배치처리 층을 활성화 함수 앞 또는 뒤에 삽입하여 데이터분포가 덜 치우치게 함
  • 배치 정규화 계층마다 정규화데이터에 고유한 확대와 이동변환 수행 (마지막 줄)
  • 단순히 평균0, 분산1로 만들어주면 활성화 함수의 비선형성이 없어질 수 있기 때문에 확대 및 이동변환 실시

2) 배치 정규화의 효과

이 외 거의 모든 경우에서 배치 정규화를 사용할 때의 학습 진도가 빠른 것으로 나타난다.

따라서 배치정규화의 장점은 학습 속도를 높인다. 그리고 가중치 초깃값에 크게 의존하지 않을 수 있다.

 

6.4 바른 학습을 위해

1) 오버피팅

오버피팅은 신경망이 훈련데이터에만 지나치게 적응되어 그 외의 데이터는 제대로 대응 못하는 상태를 말한다.

오버피팅은 주로 다음의 두 경우에 일어난다.

  • 매개변수가 많고 표현력이 높은 모델
  • 훈련 데이터가 적음

 

2) 오버피팅의 예시

import os
import sys
sys.path.append('./deep-learning-from-scratch-master')
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.multi_layer_net_extend import MultiLayerNetExtend
from common.trainer import Trainer

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

#오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train = x_train[:300]
t_train = t_train[:300]

network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10)
optimizer = SGD(lr=0.01)

max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100

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

iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0

for i in range(100000):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)
    
    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)
        
        epoch_cnt += 1
        if epoch_cnt >= max_epochs:
            break
# 그래프 그리기
markers = {'train' : 'o', 'test' : 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='o', label='test', markevery=10)
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.ylim(0, 1.1)
plt.legend(loc='lower right')
plt.show()

그래프에서 보는것처럼 train_set의 정확도는 거의 100퍼센트지만 test_set의 정확도는 그 보다 한 참 못미친다. 이는 훈련 데이터에만 적응해버린 결과이며 훈련 때 사용하지 않은 데이터에는 제대로 대응하지 못한 것이다. 이것이 오버피팅이다.

 

3) 가중치 감소(weight decay)

오버피팅 억제용으로 가중치 감소를 사용하기도 한다. 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많다. 따라서 가중치를 일부러 작게 학습되도록 유도하는데 이러한 방법이 가중치 감소이다.

 

가중치 감소에서는 모든 가중치 각각의 손실 함수에 가중치의 제곱에 비례하는 값을 더해준다. 즉 가중치가 크면 페널티를 받는 것이다.

아래 그래프를 보면 가중치 감소를 사용했을 때가 쓰지 않았을 때보다 train과 test 사이의 정확도의 차이가 조금은 줄어든다.

 

4) 드롭아웃

드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법이다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제하고, 시험 때는 모든 뉴런에 신호를 전달한다.

class Dropout:
    def __init__(self, dropout_ratio=0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None
        
    def forward(self, x, train_flg = True):
        if train_flg:
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio # dropout_ratio보다 큰 값만 True이다.
            return x * self.mask # 출력층의 일부가 꺼진다.
        else:
            return x * (1.0 - self.dropout_ratio) # 시험때는 dropout 비율만큼 뺀값을 출력값에 곱해준다.

왼쪽은 드롭아웃 없이, 오른쪽은 드롭아웃을 적용한 결과이다.
확실히 학습셋과 테스트셋의 정확도 차이가 줄었고, 학습셋의 정확도는 100%가 아니다.

 

5) 적절한 하이퍼파라미터 값 찾기

하이퍼파라미터는 각 층의 뉴런 수, 배치크기 매개변수 갱신 시의 학습률과 가중치 감소 등이다.

이를 효율적으로 탐색하는 방법을 설명한다.

 

5-1) 검증데이터

학습 데이터, 시험 데이터에 추가로 검증 데이터를 만든다. 이 검증 데이터는 적절한 하이퍼파라미터를 찾는데 사용하게 된다.

하이퍼파라미터를 대상으로 성능평가를 할 떄는 시험 데이터를 사용하면 안된다.

그 이유는 시험 데이터를 사용해 하이퍼파라미터 값을 조정하면 하이퍼파라미더 값이 시험 데이터에 오버피팅 되기 떄문이다.

따라서 검증데이터로 하이퍼파라미터 값을 찾아야한다.

 

5-2) 하이퍼파라미터 최적화

  • 하이퍼파라미터 최적화의 핵심은 하이퍼파라미터의 '최적 값'이 존재하는 범위를 조금씩 줄여간다는 것이다
  • 대략적인 범위 정하고 그 범위에서 무작위로 값을 골라낸 후, 정확도 평가
  • 하이퍼파라미터 최적화 단계
    1. 하이퍼파라미터 값의 범위 설정
    2. 설정된 범위에서 하이퍼파라미터 값 무작위 추출
    3. 1단계에서 샘플링한 하이퍼파라미터 값 사용하여 학습, 검증데이터로 정확도 평가 (에폭은 작게 설정)
    4. 1단계와 2단계를 특정횟수 반복하며, 정확도 결과보고 하이퍼파라미터의 범위 좁힘

 

6) 하이퍼파라미터 최적화 구현

# 로그스케일 내 무작위추출 구현
weight_decay = 10 ** np.random.uniform(-8, -4) # 가중치 감소계수 10^-8 ~ 10^-4 범위
lr = 10 ** np.random.uniform(-6, -2) # 학습률 10^-6 ~ 10^-2 범위

이처럼 잘될 것 같은 값의 범위를 관찰하고 범위를 좁혀 나가고 그 축소된 범위로 똑같은 작ㅂ업을 반복한다. 반복하다가 특정 단계에서 최종 파라미터 값을 하나 선택한다.

 

6.5 정리

  • 매개변수 갱신 방법에는 SGD외에도 모멘텀, AdaGrad, Adam등이 있다.
  • 가중치 초깃값을 정하는 방법은 올바른 학습을 하는 데 매우 중요하다.
  • 가중치의 초깃값으로는 Xaiver초기값과 He초기값이 효과적이다.
  • 배치 정규화를 이용하면 학습을 빠르게 진행할 수 있고 초기값에 영향을 덜 받는다
  • 오버피팅을 억제하는 정규화 기술로는 가중치 감소와 드롭 아웃이 있다.
  • 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히며 하는 것이 효과적이다.

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

8장  (0) 2023.06.29
7장  (0) 2023.06.29
5장  (0) 2023.06.02
4장  (0) 2023.06.01
손글씨 숫자 인식  (0) 2023.05.13