Chap.06 학습 관련 기술들
6-0. Intro
이번 장의 주제
- 가중치 매개변수의 최적값을 탐색하는 최적화 방법
- 가중치 매개변수 초깃값
- 하이퍼파라미터 설정방법
- 오버피팅의 대응 책 (가중치 감소, 드롭아웃)
✔️ 이번장의 목표 : 신경망 학습의 효율과 정확도를 높이기!
6-1. 매개변수 갱신
신경망 학습의 목적은 손실 함수의 값을 최소화 하기위한 매개변수 찾기이다. 이러한 문제를 해결하는 방법을 최적화(optimization)이라고 한다.
우리는 지금가지 매개변수의 기울기를 이용해 손실함수의 값을 최소화 시키는 확률적경사하강법(SGD)방법을 살펴보았다. 이제 SGD의 단점들을 살펴보고 다른 최적화방법들을 살펴보자.
확률적 경사 하강법(SGD)
확률적 경사 하강법이 무엇인지 잊어버렸을 수 도 있으니 복습해보자.

class SGD:
"""확률적 경사 하강법(Stochastic Gradient Descent)"""
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의 수식과 코드이다. 수식과 코드를보면 가중체에 대한 손실 함수의 기울기를 구한 뒤 학습률을 곱해 그것을 기존 가중치에 빼서 가중치를 갱신 시킨다.
SGD의 단점

위와 같은 수식을 SGD의 적용해보자.

위의 수식을 그래프와 등고선으로 나타낸 것이다. 이것의 기울기를 나타내면 다음과 같다.

위의 수식은 (0, 0)에서 최솟값을 같지만 대부분의 방향은 (0, 0)을 가리키지 않는다. 그럼 이제 SGD를 적용해 보자.

SGD는 위와 같이 비등방성 함수이다. SGD의 이러한 성질 때문에 SGD가 탐색경로에서 비효율적이라는 것을 보여준다. 또한 SGD는 무작정 기울어진 방향으로만 진행하고 기울어진 방향이 본래의 최솟값과 가른 방향을 가리켜서 지그재그로 탐색한다.
모멘텀(Momentum)
최적화방법 중 하나인 모멘텀을 살펴보자.
💡 모멘텀이란?
운동량을 뜻하는 단어로, 물리와 관계있다. 또한 모멘텀에서 중요한 점은 속도이다.


class Momentum:
"""모멘텀 SGD"""
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에서 추가적으로 av라는 것을 사용하는데 여기서 v는 속도를 나타내고 a는 momentum 즉 속도를 조절해주는 역할을 한다.

모멘텀은 x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하게되 SGD의 단점인 지그재그로 학습하는 방법을 개선하였다. 위 그림을 보면 SGD보다 x축 방향으로 더 빠르게 다가가는 것을 확인 할 수 있다.
AdaGrad
이번에는 다른 최적화 방법인 AdaGrad라는 것을 살펴보자.
신경망 학습에서는 학습률이라는 것이 중요한데 지금까지는 살펴본 SGD, 모멘텀은 일정한 학습률을 사용하였다.(lr = 0.01) 하지만 학습률이 너무 작으면 학습이 오래 걸리고 크면 발산하여 학습이 잘 이뤄지지 않는다.
그렇다면 적절한 학습률을 정하는 방법에는 무엇이 있는지 살펴보자.
적절한 학습률을 정하는 방법으로는 학습률 감소가 있다.
💡 학습률 감소란?
학습을 진행하면서 학습률을 줄여나가는 방법이다.
이러한 학습률 감소를 적용하고 발전시킨 방법이 AdaGrad 방법이다.


위엥 수식은 AdaGrad방식을 수식으로 나타낸 것이다. h라는 변수에 기울기를 제곱한 값을 더하고 그것을 루트를 씌어 학습률에 나눠준다. 이렇게 하면 기울기가 변함에 따라 학습률은 작아지게 된다.
class AdaGrad:
"""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)
AdaGrad의 h도 모멘텀의 v처럼 초기값을 주지않고 update()가 진행될 때 매개변수와 같은 구조를 같게 된다. 또한 매개변수마다 기울기값이 다르기 때문에 update가 되면 서로 다른 학습률을 가지게 된다.
AdaGrad는 학습률 감소라는 방식을 사용하기 때문에 점차 낮은 학습률을 가지게 되고 이는 매개변수의 갱신 강도가 낮아지는 것을 의미하게 된다. 이러한 AdaGrad의 단점을 보완한 방법이 RMSProp방식인데 RMSProp방식은 과거의 모든 기울기를 균일하게 더해나가는 것이 아니라, 먼과거의 기울기는 서서히 잊고 새로운 정보를 크게 반영한다.
<참고>
class RMSprop:
"""RMSprop"""
def __init__(self, lr=0.01, decay_rate = 0.99):
self.lr = lr
self.decay_rate = decay_rate
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] *= self.decay_rate
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
AdaGrad의 최적화 경신 경로를 표현하면 다음과 같다.

기존 SGD, 모멘텀과는 달리 빠르고 간결하게 수행되는 것을 확인 할 수 있다. 이것은 학습률이 작아져(매개변수의 갱신 강도가 낮아져) 지그재그 경로를 줄이기 때문이다.
Adam
우리는 지금까지 최적화 방법으로 SGD, 모멘텀, AdaGrad 방식을 살펴보았다. 지금 살펴볼 최적화 방법인 Adam은 모멘텀의 속도 조절과 Adagrad의 학습률 감소 방법을 합친 방법이다.
Adam 방식은 하이퍼파라미터의 편향값을 조절해준다.
class Adam:
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
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 방식의 코드와 최적화 갱신 경로이다. Adam은 전반적으로 모멘텀과 비슷한 모습을 보이지만 모멘텀의 비해 y축의 이동결로가 많이 단축된것을 볼 수 있다.
Mnist 데이터로 최적화 방법 분석
지금까지 살펴본 SGD, 모멘텀, AdaGrad, Adam방식을 각각 mnist데이터를 활용하여 학습시켜 보자.

mnist데이터 학습에서 SGD가 가장 느린성능을 보이고 있고 AdaGrad가 미세하게 더 빠른 성능을 보여주고 있다. 하지만 이러한 성능차이는 하이퍼파라미터인 학습률과 신경망 구조에 따라 결과 값이 달라진다.
6-2. 가중치의 초기값
신경망에서 최적화 방식도 중요하지만 가중치의 초기값을 어떻게 하는지도 중요하다.
지금까지 우리는 가중치의 초기값을 정규분포에서 생성되는 값을 0.01배 한 작은 값을 사용하였다. 그렇다면 가중치를 모두 0으로 설정하면 어떨까? 이는 매우 나쁜 아이디어이다. (정확히는 가중치를 균일 값으로 설정해서는 안된다.)
은닉측의 활성화값 분포
그렇다면 가중치의 초기값에 따라 은닉층의 활성화값이 어떻게 달라지는 지 살펴보자.
input_data = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5 # 은닉층이 5개
activations = {} # 이곳에 활성화 결과를 저장
x = input_data
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
다음은 각 층의 뉴런 100개, 입력데이터 1000개를 표준편차가 1인 정규분포를 가중치의 초긱밧으로한 신경망에 학습시킨 것이다.
이 신경망 학습에서 각 층의 활성화값 분포는 다음과 같다.

다음 히스토그램을 보면 0, 1에 치우쳐져 있는 것을 볼 수 있다. 이는 여기서 사용한 활성화 함수인 시그모이드 함수에서 0, 1에 가까워지면 미분이 0에 가까워져 점차 사라진다는 것을 의미한다. 이것을 기울기 소실이라고 한다.
그러면 기존에 사용하던 표준편차 0.01인 정규분포를 사용해보자.

이번에는 활성화값이 가운데 몰려있는 것을 확인할 수 있는데 이는 다수에 뉴런이 거의 같은 값을 출력하여 뉴런을 여러개을 둔 의미가 사라진다는 것을 의미하고 이를 표현력을 제한한다고 말한다.
Sigmoid 함수의 가중치 초기값
이렇게 가중치의 초기값은 신경망 학습에서 중요한문제인데 그렇다면 어떻게 설정해야할까?
우리는 활성화값을 고루 분포시키는 가중치의 초기값을 찾는 것이 목적인데 Xavier 초기값은 이 문제를 해결해준다.
💡 Xavier 초기값이란?
앞 계층의 노드가 n개라면 표준편차가 루트1/n인 분포를 사용하는 방법이다.
# w = np.random.randn(node_num, node_num) * 1
# w = np.random.randn(node_num, node_num) * 0.01
w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
Xavier 초기값은 위와 같이 코드를 조금만 바꿔 구현할 수 있다.

Xavier 초기값을 사용했더니 활성화값의 분포가 고루 펼쳐진것을 볼 수 있다.
활성화값의 분포가 오른쪽(층이 깊어질 수록)으로 진행할 수록 일그러지는데 이는 시그모이드 함수 대신 tanh함수를 사용하면 해결할 수 있다.
ReLU함수의 가중치 초기값
Xavier 초기값은 선형함수인 시그모이드 함수와 tanh함수에서 활용가능 하다. 하지만 ReLU와 같은 비선형 함수에서는 다른 초기값을 사용해야한다.
He 초기값은 ReLU의 특화된 가중치 초기값 선정 방식이다. He 초기값은 앞 계층의 노드가 n개라면 표준편차가 루트2/n인 분포를 사용하는 방법이다.
그럼 활성화 함수로 ReLU를 사용한 경우 표준편차가 0.01인 정규분포, Xavier 초기값, He 초기값을 사용한 경우 각각의 활성화분포 변화를 살펴보자.

한 눈에 볼수 있듯이 활성화 함수로 ReLU를 사용한 경우 He 초기값을 사용하면 활성화값이 고루 분포되는 것을 볼 수있다.
Mnist 데이터로 가중치 초기값 비교
그럼 지금까지 배운 가중치 초기값 선정방법을 mnist 데이터엥 적용해보자. 활성화 함수로는 ReLU를 사용하였다.

표준편차가 0.01인 정규분포를 가중치 초기값으로주면 거의 학습이 일뤄지지 않는 것을 볼 수있다. 반면에 Xavier, He는 학습이 진행됨에 따라 손실 함수의 값이 줄어드는 것을 볼 수있다.
6-3. 배치 정규화
💡배치 정규화란?
각 층의 활성화를 적당히 강제로 펼치는 것
배치 정규화의 장점은 다음과 같다.
- 학습을 빨리 진행할 수 있다. (학습 속도 개선)
- 초기값에 크게 의존하지 않는다. (골치 아픈 초기값 선택 장애 제거)
- 오버피팅을 억제한다 (드롭아웃 등의 필요성 감소)
그럼 신경망에서 배치 정규화를 어떻게 사용하는 지 살펴보자.

배치 정규화는 정규화 층을 만들어 활성화 함수의 앞or 뒤에 배치하여 사용한다. 배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대와 이동 변환을 수행한다.

배치 정규화의 성능은 위의 그림을 보면 확인할 수 있다.
6-4. 오버피팅
기계학습을 할 때 오버피팅이 문제가 되는 일이 많다.
💡 오버피팅이란?
신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태
오버피팅의 원인은 다음과 같다.
- 매개변수가 많고 표현력이 높은 모델
- 훈련 데이터가 적음
가중치 감소
💡가중치 감소란?
학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 패널티를 부과하여 오버피팅을 억제하는 방법
가중치 감소 방법은 다음과 같다.
- 가중치 감소를 구한다 : 12ΛW212ΛW2
- 손실 함수 <- 손실함수 + 12ΛW2
- 여기서 람다는 정규화의 세기를 조정하는 하이퍼파라미터이다. 또한 람다를 크게 설정할수록 큰 가중치에 대한 패널티가 커진다.
- 앞의 1/2은 미분시 ΛW2 이 값이 나오게 하기 위한 상수이다.
가중치 감소는 모든 가중치 각각의 손실 함수에 12ΛW2를 더한다. 따라서 가중치의 기울기를 구하려면은, 오차역전파법에 따른 결과에 정규화 항을 미분한 ΛW2를 더한다.

위 그래프를 보면 오버피팅이 억제된것을 볼 수 있다.
드롭아웃
지금까지 오버피팅을 억제할 수 있는 방법인 가중치 감소방법을 설명하였다. 하지만 신경망 모델이 복잡해지면 대응하기 어려워진다.
이때 사용할 수 있는 방법이 드롭아웃 기법이다.
💡 드롭아웃이란?
뉴런을 임의로 삭제하면서 학습하는 방법이다. 드롭아웃 기법은 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다. 삭제된 뉴런은 신호를 전달하지 않는다.

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
6-5. 적절한 하이퍼파라미터 값 찾기
신경망에는 각 층의 뉴런수, 배치 크기, 학습률, 가중치 감소등의 많은 하이퍼파라미터가 존재한다. 이러한 하이퍼파라미터의 값을 효율적으로 찾기 위해서는 어떻게 해야 될까?
검증 데이터 (vaildation data)
하이퍼파라미터를 조절 할 때 시험 데이터를 사용하면 안된다. 이는 시험 데이터에 오버피팅이 일어날 수 있기 때문이다. 이러한 문제 때문에 검증데이터(validation data)라는 새로운 데이터를 사용해야 한다.
훈련 데이터 : 매개변수 학습
검증 데이터 : 하이퍼파라미터 성능 평가
시험 데이터 : 신경망의 범용 성능 평가
검증 데이터를 얻는 가장 쉬운 방법은 훈련 데이터의 20%를 검증 데이터로 사용하는 것이다.
하이퍼파라미터 최적화
하이퍼파라미터를 탐색할 때 그리드서치와 같은 규칙적인 탐색보다는 무작위로 샘플링해 탐색하는 편이 좋은 결과를 낸다고 알려져 있다.
그럼 하이퍼파라미터의 최적화 방법을 알아보자.
- 하이퍼파라미터 값의 범위를 설정한다.
- 설정된 범위에서 하이퍼파라미터 값을 무작위로 추출한다.
- 2단계에서 샘플링한 하이퍼파라미터 값을 사용하여 학습하고, 검증 데이터로 정확도를 평가한다(단 에폭은 작게 설정한다.)
- 2단계와 3단계를 특정 횟수(100회 등) 반복하며, 그 정확도의 결과흫 보고 하이퍼파라미터의 범위를 좁힌다.
하이퍼파라미터 최적화 구현
그럼이제 코드로 구현해보자
# 하이퍼파라미터 무작위 탐색======================================
optimization_trial = 100
results_val = {}
results_train = {}
for _ in range(optimization_trial):
# 탐색한 하이퍼파라미터의 범위 지정===============
# 가중치 감소의 범위
weight_decay = 10 ** np.random.uniform(-8, -4)
# 학습률의 범위
lr = 10 ** np.random.uniform(-6, -2)
# ================================================
val_acc_list, train_acc_list = __train(lr, weight_decay)
print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
results_val[key] = val_acc_list
results_train[key] = train_acc_list
Chapter. 6 정리
* 매개변수 갱신 방법에는 확률적 경사 하강법(SGD) 외에도 모멘텀, AdaGrad, Adam 등이 있다.
* 가중치 초깃값을 정하는 방법은 올바른 학습을 하는 데 매우 중요하다.
*가중치의 초깃값으로는 ‘Xavier 초깃값’과 ‘He 초깃값’이 효과적이다.
* 배치 정규화를 이용하면 학습을 빠르게 진행할 수 있으며, 초깃값에 영향을 덜 받게 된다.
* 오버피팅을 억제하는 정규화 기술로는 가중치 감소와 드롭아웃이 있다.
* 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히면서 하는 것이 효과적이다.
출처: 사이토 고키, 『밑바닥부터 시작하는 딥러닝』, 한빛미디어(2017)
'Deep Learning > deep learning from scratch' 카테고리의 다른 글
| Chap.08 딥러닝 (0) | 2022.01.10 |
|---|---|
| Chap.07 합성곱 신경망(CNN) (0) | 2022.01.10 |
| Chap.05 오차역전파법 (0) | 2022.01.07 |
| Chap.04 신경망 학습 (0) | 2022.01.07 |
| Chap.03 신경망 (0) | 2022.01.05 |



