Study/밑바닥부터 시작하는 딥러닝
Ch6 학습 관련 기술들
Bonseong
2021. 1. 25. 10:56
6.1 매개변수 갱신¶
- 신경망 학습의 목적 : 손실 함수의 값을 가능한 낮추는 매개변수를 찾는 것 (최적화)
- 여태까지 미분을 통해 매개변수의 값을 갱신했음 (확률적 경사 하강법)
6.1.1 확률적 경사 하강법 (SGD)¶
In [1]:
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의 단점¶
- 비등상성 함수 (방향에 따라 성질, 기울기가 달라지는 함수) 에서느 탐색 경로가 비효율적
6.1.3 모멘텀¶
- 운동량을 뜻하는 단어
In [3]:
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와 비교했을 때 지그재그의 정도가 덜함
- 예시에서는 x축의 힘은 아주 작지만 방향은 변하지 않아 한 방향으로 일정하게 가속
- y축의 힘은 크지만 번갈아 받아 상충하며 y축의 속도는 안정적이지 않음
6.1.4 AdaGrad¶
- 학습률 감소 : 학습을 진행하면서 학습률을 점차 줄여가는 방법
- 매개변수 전체의 학습률 값을 일괄적으로 낮추는 방법
In [6]:
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) # 0이 담겨져 있다고 해도 0으로 나누는 것을 막아줌
- 최솟값을 향해 효율적으로 움직임
- y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정
- y축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어듬
6.1.5 Adam¶
- 직관적으로 모멘텀과 AdaGrad를 융합한 방법
- 하이퍼파라미터의 편향 보정이 진행됨
6.1.6 갱신 방법 선택¶
- 가장 뛰어난 방법은 아직까진 없음
- 각 문제의 상황에 따라 방법 선택
6.2 가중치의 초깃값¶
6.2.1 초깃값을 0으로 하면?¶
- 가중치 감소 : 오버피팅을 억제해 범용 성능을 높이는 테크닉
- 가중치를 균일한 값으로 설정하면 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문에 좋지 않음
- 가중치가 고르게 되어버리는 상황을 막으려면 초깃값을 무작위로 설정해야 함
6.2.2 은닉층의 활성화값 분포¶
In [11]:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1/(1+np.exp(-x))
x = np.random.randn(1000,100)
node_num = 100 # 각층의 뉴런은 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
In [12]:
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과 1에 몰려 있음
- 기울기 소실 : 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기욹리 앖이 점점 작아지다가 사라짐
In [13]:
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
w = np.random.randn(node_num, node_num) * 0.01
a = np.dot(x,w)
z = sigmoid(a)
activations[i] = z
In [14]:
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.5 부근에 집중되어 있음
- 다수의 뉴런이 거의 같은 값을 출력하고 있음 -> 뉴런을 여러개 둔 의미가 없어짐
- 활성화값들이 치우치면 표현력을 제한한다는 관점에서 문제가 됨
- Xavier 초깃값 : 일반적인 딥러닝 프레임워크에서 표준적으로 사용하는 초깃값
- 활성화값들을 광범위하게 분포시킬 목적, 앞 계층의 노드가 n개라면 표준편차가 1/sqrt(n)인 분포를 사용하면 된다는 결론
In [19]:
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
node_num=100
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
a = np.dot(x,w)
z = sigmoid(a)
activations[i] = z
In [20]:
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()
- 층이 깊어지면서 형태가 다소 일그러지지만, 넓게 분포됨
- 시그모이드 함수의 표현력도 제한받지 않음 (일그러짐 현상은 tanh함수를 사용하면 해결 됨)
6.2.3 ReLU를 사용할 때의 가중치 초깃값¶
- sigmoid 함수와 tanh 함수는 좌우 대칭이라 중앙 부근 선형인 함수 -> Xavier 초깃값
- ReLU는 He 초깃값 사용
- He 초깃값 : 앞 계층의 노드가 n개 일 때, 표준편차가 sqrt(2/n) 인 정규분포 사용
6.3 배치 정규화¶
- 배치 정규화 : 각 층이 활성화를 적당히 퍼뜨리도록 강제함
6.3.1 배치 정규화 알고리즘¶
- 학습을 빨리 진행할 수 있음
- 초깃값에 크게 의존하지 않음
- 오버피팅 억제
- 학습 시 미니배치를 단위로 정규화
- 데이터 분포가 표준정규분포를 따르도록 함 (데이터 분포가 덜 치우치게 할 수 있음)
- 각 계층마다 정규화 데이터에 고유한 확대화 이동 변환을 수행
6.4 바른 학습을 위해¶
- 오버피팅 : 신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태
6.4.1 오버피팅¶
- 매개변수가 많고 표현력이 높거나
- 훈련 데이터가 적을 때 오버피팅 발생
In [35]:
import os
import sys
from dataset.mnist import load_mnist
from common.multi_layer_net import MultiLayerNet
from common.optimizer import SGD
sys.path.append(os.pardir)
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
x_train = x_train[:300]
t_train = t_train[:300]
In [39]:
#weight_decay_lambda = 0
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)
print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
In [40]:
# 그래프 그리기==========
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
- 훈련 데이터에 대해 정확도가 100%임
6.4.2 가중치 감소¶
- 큰 가중치에 대해 그에 상응하는 패널티를 부과해 오버피팅 억제
In [41]:
weight_decay_lambda = 0.1
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)
print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
In [42]:
# 그래프 그리기==========
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
- 오버피팅이 억제되었음
- 정확도가 100%에 도달하지 못했음
6.4.3 드롭아웃¶
- 드롭아웃 : 뉴런을 임의로 삭제하면서 학습
- 은닉층의 뉴런을 무작위로 골라 삭제
- 훈련 떄는 데이터를 흘릴 대마다 삭제할 뉴런을 무작위로 삭제, 시험 때는 모든 뉴런에 신호 전달 (각 뉴런의 출력에 훈련 때 삭제 안 한 비율을 곱하여 출력)
In [43]:
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 적절한 하이퍼파라미터 찾기¶
6.5.1 검증 데이터¶
- 하이퍼파라미터의 성능을 평가할 때는 시험 데이터를 사용해서는 안됨 (시험데이터에만 적합하도록 조정됨)
- 검증데이터 : 하이퍼파라미터 조정용 데이터
In [45]:
def shuffle_dataset(x, t):
permutation = np.random.permutation(x.shape[0])
x = x[permutation, :] if x.ndim == 2 else x[permutation, :, :, :]
t = t[permutation]
return x, t
In [50]:
(x_train, t_train), (x_test, t_test) = load_mnist()
x_train, t_train = shuffle_dataset(x_train, t_train)
validation_rate = 0.2
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 = x_train[validation_num:]
6.5.2 하이퍼파라미터 최적화¶
- 하이퍼파라미터의 최적 값이 존재하는 범위를 조금씩 줄여나감
- 범위를 줄이려면 우선 대략적인 범위를 설정하고 그 범위에서 무작위로 하이퍼파라미터 값을 골라낸 후, 그 값으로 정확도를 평가
- 정확도를 잘 살피면서 이 작업을 반복하여 하이퍼파라미터의 최적 값의 범위를 좁혀나감
- 하이퍼파라미터 최적화는 아주 오래걸리기 때문에 학습을 위한 에폭을 작게 하여 1회 평가에 걸리는 시간을 단축하는 것이 효과적
- 0단계 : 하이퍼파라미터의 값의 범위를 설정
- 1단계 : 설정된 범위에서 하이퍼파라미터의 값을 무작위로 추출
- 2단계 : 1단계에서 샘플링한 하이퍼퍼라미터 값을 사용하여 학습하고, 검증 데이터로 정확도를 평가 (에폭은 작게 설정)
- 3단계 : 1단계와 2단계를 특정 횟수 반복하여 그 정확도를 알아보고, 하이퍼파라미터의 범위를 좁힘
- 베이즈 최적화 : 베이즈정리를 중심으로 최적화 수행
6.5.3 하이퍼파라미터 최적화 구현하기¶
In [56]:
import sys
import os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.multi_layer_net import MultiLayerNet
from common.util import shuffle_dataset
from common.trainer import Trainer
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 결과를 빠르게 얻기 위해 훈련 데이터를 줄임
x_train = x_train[:500]
t_train = t_train[:500]
# 20%를 검증 데이터로 분할
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_train, t_train = shuffle_dataset(x_train, t_train)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]
def __train(lr, weight_decay, epocs=50):
network = MultiLayerNet(input_size=784,
hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, weight_decay_lambda=weight_decay)
trainer = Trainer(network, x_train, t_train, x_val, t_val,
epochs=epocs, mini_batch_size=100,
optimizer='sgd',
optimizer_param={'lr': lr}, verbose=False)
trainer.train()
return trainer.test_acc_list, trainer.train_acc_list
# 하이퍼파라미터 무작위 탐색======================================
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
In [57]:
# 그래프 그리기========================================================
print("=========== Hyper-Parameter Optimization Result ===========")
graph_draw_num = 20
col_num = 5
row_num = int(np.ceil(graph_draw_num / col_num))
i = 0
for key, val_acc_list in sorted(results_val.items(), key=lambda x: x[1][-1], reverse=True):
print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)
plt.subplot(row_num, col_num, i+1)
plt.title("Best-" + str(i+1))
plt.ylim(0.0, 1.0)
if i % 5:
plt.yticks([])
plt.xticks([])
x = np.arange(len(val_acc_list))
plt.plot(x, val_acc_list)
plt.plot(x, results_train[key], "--")
i += 1
if i >= graph_draw_num:
break
plt.show()
6.6 정리¶
- 매개변수 갱신 방법에는 확률적 경사하강법 외에 모멘텀, AdaGrad, Adam 등이 있음
- 가중치 초깃값을 정하는 방법은 올바른 학습을 하는 데 매우 중요
- 가중치 초깃값으로는 Xavier 초깃값과 He 초깃값이 효과적
- 배치정규화를 이용하면 정규화 기술로는 가중치 감소와 드롭아웃이 있음
- 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히면서 하는 것이 효과적