본 글은 "바닥부터 시작하는 딥러닝" 책을 바탕으로 작성되었습니다.
6.1 매개변수 갱신
- 신경망 학습 목적은 손실함수의 값을 가능한 낮추는 매개변수를 찾는 것이다.
- 매개변수의 최적 값을 찾는 것을 '최적화'라고 한다.
- 최적의 매개변수 값을 찾기 위해, 매개변수의 기울기(미분)를 구하고 기울어진 방향대로 이동하면서 매개변수 값을 갱신하였다. 이러한 방법을 '확률적 경사 하강법(SGD)'라고 한다.
6.1.1 확률적 경사 하강법 (SGD)
- 그림 1의 수식에서 w는 갱신할 가중치 매개변수를 의미하고, 분수는 W에 대한 손실 함수의 기울기이다.
- η는 학습률을 의미하는데, 실제는 0.01이나 0.001과 같은 값을 미리 정해서 사용한다.
- 화살표는 우변 값으로 좌변 값을 갱신한다는 의미이다.
- SGD는 기울어진 방향으로 일정 거리만큼 가겠다는 방식으로, 파이썬으로 구현하면 아래와 같다.
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]
6.1.2 SGD의 단점
- SGD는 단순하고 구현도 쉽지만, 문제에 따라 비효율적이다.
- SGD는 비등방성 함수(방향에 따른 성질)에서는 탐색 경로가 비효율적이다.
- SGD 단점을 개선하기 위해 모멘텀, AdaGrad, Adam 3가지 기법이 존재한다.
6.1.3 모멘텀
- 그림2의 수식은 기울기 방향으로 힘을 받아, 물체가 가속된다는 물리 법칙을 나타낸다.
- v는 물리에서 말하는 속도를 의미하며, αv는 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다.
- 모멘텀을 파이썬으로 구현하면 아래와 같다.
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]
- x축의 힘은 아주 작지만, 방향은 변하지 않아 한 방향으로 일정하게 가속한다.
- y축의 힘은 크지만, 위 아래로 번갈아 받아 상충하기 때문에 y축 방향의 속도는 안정적이지 않다.
- SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 줄어드는 장점이 있다.
6.1.4 AdaGrad
- η(학습률)은 값이 작으면 학습이 길어지고, 값이 많으면 학습이 제대로 이뤄지지 않기 때문에 신경망 학습에서 학습률 값이 중요한다.
- 이는 '학습률 감소' 기술을 이용하여 조절하는데, 학습을 진행하면서 학습률을 점차 줄여가는 방법이다.
(처음에는 크게 학습하다가 조금씩 작게 학습하는 것으로, 실제 신경망 학습에서 많이 쓰인다.)
- 학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 전체의 학습률 값을 일괄적으로 낮추는 것이다.
- AdaGrad는 개별 매개변수에 적응적으로 학습률을 조정하면서 학습을 진행한다.
- 그림 3 수식에서 h에는 기존 기울기 값을 제곱하여 계속 더해준다. (⨀기호는 행렬의 원소별 곱셈을 의미)
- 가중치 매개변수를 갱신할 때, 1/ \sqrt{h} 을 곱해서 학습률을 조정한다.
- AdaGrad는 과거 기울기를 제곱하여 계속 더해가기 때문에, 학습을 진행할수록 갱신 강도가 약해진다.
무한히 계속 학습하면, 갱신량이 0이 되어 갱신할 수 없는 문제가 있다. 이를 개선하기 위해, RMSProp 방식이 있다
- AdaGrad를 파이썬으로 구현하면, 아래와 같다.
- 1e-7이라는 작은 값을 더해주는 이유는 self.h[key]에 0이 담겨 있어도 0으로 나누는 문제를 막아주기 위해서이다.
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.v[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)
- y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다. 이를 통해, y축 방향으로 갱신 강도가 빠르게 약해지고 지그재그 움직임도 줄어든다.
6.1.5 Adam
- 모멘텀과 AdaGrad를 융합시킨 기법이다.
6.2 가중치의 초깃값
6.2.1 초깃값을 0으로 하면?
- 오버피팅을 억제해 범용 성능을 높이는 테크닉에는 '가중치 감소' 기법이 존재하는데, 이는 가중치 매개변수의 값이 작아지도록 학습하는 기법이다.
- 초깃값을 0으로 설정하면 안되는 이유는 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문이다.
(가중치 값을 균일한 값으로 설정하면 안된다.)
6.2.2 은닉층의 활성화값 분포
- 가중치 초깃값에 따라 은닉층 활성화값(활성화 함수의 출력 데이터)들이 어떻게 변화하는지 확인하는 예제이다.
- 활성화 함수는 시그모이드 함수로, 5층 신경망에 무작위 입력 데이터를 넣으며 각 층의 활성화 값 분포를 히스토그램으로 그려 확인하는 예제이다.
- 전체 소스코드는 아래 링크 참고! 본 글의 내용은 부분만 존재한다.
(https://github.com/kchcoo/WegraLee-deep-learning-from-scratch/blob/master/ch06/weight_init_activation_histogram.py )
- 아래 코드에서는 가중치 분포 시, 표준 편차가 1인 정규분포를 이용하였다.
해당 표준편차를 바꿔가면서 활성화 값들의 분포가 어떻게 변화하는지 관찰하는 것이 실습의 목적이다.
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
a = np.dot(x, w)
z = sigmoid(a)
activations[i] = z
- 위 코드를 통해, activations에 저장된 각 층의 활성화 값 데이터를 히스토그램으로 그린 결과는 아래와 같다.
- 각 층의 활성화값들이 0과 1에 치우쳐 분포 된 것을 확인할 수 있다.
- 예제에서 사용한 시그모이드 함수는 출력이 0이나 1에 가까워지자 그 미분은 0 또는 1에 다가간다.
- 데이터가 0과 1에 치우쳐 분포하게 되면, 역전파의 기울기 값이 점점 작아지다가 사라지는데, 이를 "기울기 소실"이라고 한다. (층이 깊게하는 딥러닝에서는 기울기 소실은 심각한 문제이다.)
# 히스토그램 그리기
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()
- 가중치의 표준 편차를 0.01로 바꿔 출력하면 아래와 같다.
- 각 층의 활성화 값들이 0.5부근에 집중된 것을 확인할 수 있다.
- 앞 예제의 문제인 기울기 소실이 발생하지 않았으나, 활성화 값이 치우치는 것은 다수의 뉴런이 거의 같은 값을 출력하고 있는 것으로 뉴런을 여러개 둔 의미가 사라진다.
- 활성화 값들이 치우치면 "표현력 제한" 문제가 발생한다. (각 층의 활성화 값들은 적당히 고루 분포되야 한다.)
w = np.random.randn(node_num, node_num) * 0.01
- 사비에르 글로로트와 요슈아 벤지오의 논문에서 권장하는 가중치 초기값인 Xavier 초깃값을 사용하면 아래와 같다.
(현재 딥러닝 프레임워크들은 표준적으로 Xavier 초깃값을 사용한다.)
- 해당 논문에서는 각 층의 활성화 값들을 광범위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾는 요점으로,
앞 계층의 노드가 n개라면 표준편차가 1/√n 인 분포를 사용하면 된다는 결론이 작성되어 있다.
- Xavier 초기값을 사용하면, 앞 층에 노드가 많을수록 대상 노드의 초깃값으로 설정하는 가중치가 좁게 퍼진다.
- 결과를 보면 확실히 넓게 분포되었고 각 층에 흐르는 데이터는 적당히 퍼져있으므로, 시그모이드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질 것이 예상된다.
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
6.2.3 ReLU를 사용할 때의 가중치 초깃값
- sigmoid함수와 tanh 함수는 좌우 대칭이라 중아 부근이 선형인 함수로 볼 수 있다.
Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 나온 결과이므로, sigmoid함수와 tanh 함수를 사용할 때는 적당하다.
- ReLU의 함수의 경우에는 ReLU에 특화된 초깃값을 이용하라고 권장하는데, 이는 'He 초깃값'이다.
- He 초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 \sqrt{2/n}인 정규분포를 사용한다.
(ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해, Xqvier보다 2배의 계수가 필요하다.)
- He 초깃값은 모든 층에서 균일하게 분포되기 때문에, 역전파 때도 적절한 값이 나올 것이라고 기대할 수 있다.
6.2.4 MNIST 데이터셋으로 본 가중치 초깃값 비교
- 3가지 가중치의 초깃값을 줘보고, 신경망 학습에 어떤 영향을 주는지 확인하는 예제이다.
(std = 0.01, Xavier 초깃값, He 초깃값)
- 뉴런 수가 100개인 5층 신경망에서 활성화 함수로 ReLU를 사용하였다.
- 코드: https://github.com/kchcoo/WegraLee-deep-learning-from-scratch/blob/master/ch06/weight_init_compare.py
- std=0.01일 때는 학습이 전혀 이뤄지지 않을 것을 볼 수 있다. 이는 순전파 때 너무 작은 값이 흐르기 때문이다.
- Xavier와 He 초깃값의 경우 학습이 순조롭게 이뤄지는 것을 볼 수 있으며, 학습진도는 He 초깃값이 더 빠르다.
6.3 배치 정규화
- 각 층에 활성화 값이 적당히 분배되도록 강제하는 것을 "배치 정규화"라고 한다.
6.3.1 배치 정규화 알고리즘
- 배치 정규화가 주목받는 이유는 학습이 빠르고, 초깃값에 크게 의존하지 않으며, 오버피팅을 억제하기 때문이다.
- 배치 정규화는 학습시 미니배치를 단위로 정규화하는데, 구체적으로는 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화한다.
- 배치 정규화 계층마다 정규화된 데이터에 고유한 확대와 이동 변환을 수행한다.
6.4 바른 학습을 위해
6.4.1 오버피팅
- 오버피팅은 주로 '매개변수가 많고 표현력이 높은 모델'과 '훈련 데이터가 적은 경우' 발생한다.
- 위 두 경우를 충족시켜, 일부로 오버피팅을 진행하는 예제이다.
- 예제는 60,000개의 MNIST 데이터셋의 훈련 데이터 중 300개만 사용하고, 7층 네트워크로 네트워크 복잡성을 높인다.
각 층 뉴런은 100개, 활성화 함수는 ReLU를 사용한다.
- 아래는 데이터를 읽는 코드이다.
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 오버피팅을 재현하기 위해 학습 데이터 수 감소
x_train = x_train[:300]
t_train = t_train[:300]
- 아래는 훈련을 수행하는 코드로, 에폭마다 모든 훈련데이터아 모든 시험 데이터 각각에서 정확도를 산출한다.
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10,
weight_decay_lambda=weight_decay_lambda)
optimizer = SGD(lr=0.01) # 학습률이 0.01인 SGD로 매개변수 갱신
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(1000000000):
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
6.4.2 가중치 감소
- 오버피팅 억제용으로 많이 사용한 방식 중 '가중치 감소'가 있다.
- 가중치 감소는 모든 가중치 각각의 손실 함수에 1/2 λw^2을 더한다.
- 결과를 확인해보면 훈련데이터와 시험 데이터의 차이가 있지만, 그림8보다는 차이가 줄어든 것을 볼 수 있다.
- 다만, 그림 8과 달리 훈련 데이터에 대한 정확도가 100%(0.1)에 도달하지 못하는 것을 볼 수 있다.
6.4.3 드롭아웃
- 신경망 모델이 복잡해지면 가중치 감소만으로 대응하기 어려워지는데, 이때 "드롭아웃" 기법을 많이 사용한다.
- '드롭아웃'은 뉴런을 임의로 삭제하면서 학습하는 방법으로, 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다.
- 훈련 때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험 때는 모든 뉴런에 신호를 전달한다.
(단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제 안 한 비율을 곱하여 출력한다.)
- 아래는 드롭아웃을 구현한 코드이다.
- 훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한다.
- self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값을 dropout_ratio보다 큰 원소만 True로 설정한다.
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
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
- 전체 코드: https://github.com/kchcoo/WegraLee-deep-learning-from-scratch/blob/master/ch06/overfit_dropout.py
- 드롭아웃을 적용하니, 훈련 데이터와 시험 데이터에 대한 정확도 차이는 줄었다.
- 훈련 데이터에 대한 정확도가 100% 도달하지 않은 것을 확인할 수 있다.
6.5 적절한 하이퍼파라미터 값 찾기
6.5.1 검증 데이터
- 이전까지 데이터셋을 훈련 데이터와 시험 데이터 2가지로 분리하여, 훈련 때는 훈련 데이터를 사용하고 범용 성능 평가에서는 시험 데이터를 사용하였다.
- 시험데이터에 하이퍼파라미터 값이 오버피팅되기 때문에, 하이퍼파라미터 성능 평가시에는 시험 데이터를 사용하면 안 된다.
- 하이퍼파라미터 조정 시 전용 확인 데이터가 필요한데, 이것을 '검증 데이터'라고 부른다.
- MNIST 데이터셋은 훈련 데이터와 시험 데이터로만 분리되어 있다.
- MNIST 데이터셋에서 검증 데이터를 얻기 위한 가장 간단한 방법은 '훈련 데이터 중 20% 정도를 검증 데이터로 먼저 분리하는 것'이다.
import os
import sys
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist
from common.util import shuffle_dataset
(x_train, t_train), (x_text, t_test) = load_mnist()
# 훈련 데이터를 뒤섞는다.
x_train, t_train = shuffle_dataset(x_train, t_train)
# 20%를 검증 데이터로 분할
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]
6.5.2 하이퍼파라미터 최적화
- 하이퍼파라미터의 최적값이 존재하는 범위를 조금씩 줄여가는 것이 핵심이다.
- 대략적인 범위를 설정하고, 해당 범위에서 무작위 하이퍼파라미터 값을 골라내 정확도를 확인하면서 하이퍼파라미터의 최적값의 범위를 좁혀간다.
- 하이퍼파라미터 최적화할 때 딥러닝 학습에는 오랜 시간이 걸리므로, 학습을 위한 에폭을 작게 하하여 1회 평가 시간을 단축하는 것이 효과적이다.
- 하이퍼파라미터 최적화 과정
0단계) 하이퍼 값 범위 설정(대략적으로)
1단계) 설정범위 내에서 하이퍼파라미터값 무작위 추출
2단계) 1단계에 추출한 값을 사용하여 학습하고, 검증 데이터로 정확도 평가 (에폭은 작게)
3단계) 1~2단계를 특정 획수만큼 반복하며, 정확도 보고 하이퍼 파라미터 범위 최소화
'AI' 카테고리의 다른 글
Building Systems with the ChatGPT API (0) | 2024.08.03 |
---|---|
Chapter 5. 오차역전파법 (0) | 2024.06.30 |
Chapter 4. 신경망 학습 (0) | 2024.06.23 |
Chapter 3. 신경망 (0) | 2024.06.18 |
Chapter2. 퍼셉트론 (1) | 2024.06.17 |