매개변수 갱신
신경망 학습의 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것이다.
이 과정을 최적화(optimization)이라고 한다.
지금까지는 최적의 매개변수 값을 찾는 단서로 미분을 이용했다. 매개변수의 기울기를 구해 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복해 점점 최적의 값에 다가갔다.
이것이 확률적 경사 하강법(SGD)이란 단순한 방법이다.
최적화를 해야하는 상황을 모험가 이야기에 비유해 보았다.
색다른 모험가가 있다. 광활한 메마른 산맥을 여행하면서 날마다 깊은 골짜기를 찾아 발걸음을 옮긴다.
그는 전설에 나오는 세상에서 기징 깊고 낮은 골짜기, '깊은 곳'을 찾아가려 한다.
그것이 그의 여행 목적이다. 게다가 그는 엄격한 '제약' 2개로 자신을 옭아맸다.
하나는 지도를 보지 않을 것. 또 다른 하나는 눈가리개를 쓰는 것이엿다.
모험가는 어떻게 해야 효율적으로 '깊은 곳'을 찾아갈 수 잇을까?
확률적 경사 하강법(SGD)를 복습해보자.
W는 갱신할 가중치 매개변수고 dL,dW는 W에 대한 손실 함수의 기울기이다.
n은 학습률을 의미하는데, 실제로는 0.1,0.01과 같은 값을 미리 정해서 사용한다.
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]
lr은 learning rate(학습률)을 의미한다. 이 학습률을 인스턴스 변수로 유지한다.
update() 메서드는 SGD과정에서 반복해서 불린다. params, grads는 딕셔너리 변수이다.
SGD클래스를 사용하면 신경망 매개변수의 진행을 다음과 같이 수행할 수 있다.
network = TwoLayerNet(...)
optimizer = SGD()
for i in range(10000):
...
x_batch, t_batch = get_mini_batch(...) #mini batch
grads = network.gradient(x_batch,t_batch)
params = network.params
optimizer.update(params,grads)
이 코드는 실제로 동작하지 않는 수도 코드이다.
대부분의 딥러닝 프레임워크는 다양한 최적화 기법을 구현해 제공하며, 원하는 기법으로 쉽게 바꿀 수 있는 구조로 되어있다. 예를 들어 Lasagne이라는 딥러닝 프레임워크는 다양한 최적화 기법을 구현해놨다.
SGD의 단점
sGD는 단순하고 구현도 쉽지만, 문제에 따라서는 비효율적일 때가 있다.
다음 함수의 최솟값을 구하는 문제를 생각해보자.
이 함수의 모습은 다음과 같다.
함수의 기울기를 그려보면 다음처럼 된다.
이 기울기는 y축 방향은 크고 x축 방향은 작다는 것이 특징이다.
y축 방향은 가파른데, x축 방향은 완만한 것이다.
위의 식이 최솟값이 되는 장소는 (x,y) = (0,0)이지만, 아개 그림이 보여주는 기울기 대부분은 (0,0)방향을 가리키지 않는다.
위 함수에 SGD를 적용해보자. 탐색을 시작하는 초깃값은 (x,y) = (-7.0,2.0)으로 하게 되면, 그림은 다음과 같다.
SGD는 그림과 같이 심한 움직임을 보여준다.
즉, SGD의 단점은 비등방성 함수(anisotropy, 방향에 따라 성질,즉 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이다.
SGD가 지그재그로 탐색하는 근본 원인은 기울어진 방향이 본래의 최솟값과 다른 방향을 가리켜서이다.
이러한 SGD의 단점을 개선해주는 모멘텀, AdaGrad, Adam의 세 방법이 있다.
모멘텀
모멘텀(Momentum)은 운동량을 뜻하는 단어로, 물리와 관계가 있다. 수식은 다음과 같다.
W는 갱신할 가중치 매개변수, dL/dW는 W에 대한 손실함수의 기울기, n은 학습률이다.
v는 물리에서 말하는 속도(Velocity)에 해당한다.
첫번째 식은 기울기 방향으로 힘을 받아 물체가 가속된다는 물리 법칙을 나타낸다.
av항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다.(0.9 등의 값으로 보통 설정한다.)
물리에서의 지면 마찰이나 공기 저항에 해당한다.
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 parmas.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]
인스턴스 변수 v가 물체의 속도이다.
v는 초기화 떄는 아무 값도 담지 않고, update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장한다.
위의 식을 모멘텀을 사용하여 최적화해보면, 결과는 다음과 같이 된다.
그림에서 보듯 모멘텀의 갱신 경로는 공이 그릇 바닥을 구르듯 움직인다.
SGD와 비교하면 지그재그 정도가 덜하다는 것을 알 수 있다.
이는 x축의힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문이다.
거꾸로 y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 y축 방향의 속도는 안정적이지 않다.
전체적으로는 SGD보다 x측 방향으로 빠르게 다가가 지그재그 움직임이 줄어든다.
AdaGrad
신경망 학습에서는 학습률 값이 중요하다. 값이 너무 작으면 학습시간이 길어지고, 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다.
학습률을 정하는 효과적 기술로 학습률 감소(learning rate decay)가 있다.
이는 학습을 진행하면서 학습률을 점차 줄여가는 방법이다. 처음에는 크게 학습하다가 조금씩 작게 학습한다는 얘기로, 실제 신경망 학습에 자주 쓰인다.
학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 전체 학습률 값을 일괄적으로 낮추는 것이다.
이것을 더욱 발전시킨 것이 AdaGrad이다. AdaGrad는 각각의 매개변수에 맞춤형 값을 만들어준다.
수식으로는 다음과 같다.
W는 갱신할 가중치 매개변수, dL/aW는 W에 대한 손실 함수의 기울기, n은 학습률을 뜻한다.
h라는 새로운 변수가 등장했는데, 첫번째 식에서 보듯 기울기 값을 제곱하여 계속 더해준다.(행렬의 원소별 곱셈)
그 후 매개변수를 갱신할 때 1/h^1/2을 곱해 학습률을 조정한다.
매개변수의 원소 중에서 많이 움직인(크게 갱신된) 원소는 학습률이 낮아진다는 뜻인데, 다시 말해 학습률 감소가 매개변수의 원소마다 다르게 적용됨을 뜻한다.
코드로 구현하면 다음과 같다.
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-7라는 작은 값을 더해주는 이유는, self.h[key]에 0이 담겨있다 해도 0으로 나누는 사태를 막아준다.
위 구현을 통해 최적화를 진행하면, 아래의 그림처럼 결과가 나온다.
그림을 보면, 최솟값을 향해 효율적으로 움직이는 것을 알 수 있다.
y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다.
그래서 y축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어든다.
Adam
모멘텀과 AdaGrad 기법을 융합하면 어떻게 될까? 이 생각에서 출발한 기법이 바로 Adam이다.
Adam 갱신 과정도 그릇 바닥을 구르듯 움직인다. 모멘텀과 비슷한 패턴인데, 모멘텀 때보다 공의 좌우 흔들림이 적다.
이는 학습의 갱신 강도를 적응적으로 조정했기 때문이다.
지금까지 매개변수의 갱신 방법을 4개 살펴봤다.
그림만 보면 AdaGrad가 가장 좋은것 같지만, 결과는 풀어야 할 문제가 무엇이냐에 따라 달라지므로 주의해야 한다.
당연하지만 학습률 등의 하이퍼파라미터를 어떻게 설정하느냐에 따라 결과가 바뀐다.
지금도 많은 연구에서 SGD를 사용하고 있다. 모멘텀과 AdaGrad도 시도해볼 만한 가치가 충분하다.
책에서는 주로 SGD, Adam을 사용한다.
MNIST dataset으로 본 갱신 방법 비교
# coding: utf-8
import os
import sys
import matplotlib.pyplot as plt
import numpy as np
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *
# 0. MNIST 데이터 읽기==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000
# 1. 실험용 설정==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
# optimizers['RMSprop'] = RMSprop()
networks = {}
train_loss = {}
for key in optimizers.keys():
networks[key] = MultiLayerNet(
input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10)
train_loss[key] = []
# 2. 훈련 시작==========
for i in range(max_iterations):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for key in optimizers.keys():
grads = networks[key].gradient(x_batch, t_batch)
optimizers[key].update(networks[key].params, grads)
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
if i % 100 == 0:
print("===========" + "iteration:" + str(i) + "===========")
for key in optimizers.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 3. 그래프 그리기==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key],
markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()
이 실험은 각 층이 100개의 뉴런으로 구성된 5층 신경망에서 ReLU를 활성화 함수로 사용해 측정했다.
결과를 보면 SGD의 학습 진도가 제일 느리다. AdaGrad가 미세하게 빠르다.
이 실험에서 주의할 점은 하이퍼파라미터인 학습률과 신경망의 구조(층 깊이 등)에 따라 결과가 달라진다.
일반적으로 SGD보다 다른 세 기법이 빠르게 학습하고, 때로는 최종 정확도도 높게 나타난다.
가중치의 초깃값
신경망에서 특히 중요한 것이 가중치의 초깃값이다. 가중치의 초깃값을 무엇으로 설정하느냐가 신경망 학습의 성패가 가르는 일이 실제로 자주 있다.
초기값을 0으로 하면?
가중치의 초깃값을 모두 0으로 하면 어떨까? 이는 나쁜 아이디어다.
실제제로 0으로 설정하면 학습이 올바로 이뤄지지 않는다.
이 이유는 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문이다.
가중치가 고르게 되어버리는 상황을 막으려면(가중치의 대칭적인 구조를 무너뜨리려면) 초깃값을 무작위로 설정해야 한다.
은닉층의 활성화값 분포
은닉층의 활성화값(활성화 함수의 출력 데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있다.
활성화 함수로 sigmoid를 사용하는 5층 신경망에 무작위로 생성한 입력 데이터를 흘리며 각 층의 활성화값 분포를
히스토그램으로 그려보자.
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
이 코드에서는 가중치의 분포에 주의해야 한다.
표준편차가 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(30,1))
plt.show()
코드를 이용해 히스토그램을 출력해보자.
각 층의 활성화값들이 0,1에 치우쳐저 분포되어 있다.
여기서 사용한 sigmoid는 출력이 0 또는 1에 가까워지자 미분은 0에 다가간다.
그래서 데이터가 0,1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다.
이것이 기울기 소실(gradient vanishing)이라고 알려진 문제이다.
층을 깊게 하는 딥러닝에서는 기울기 소실은 더 심각한 문제가 될 수 있다.
이번에는 기울기 표준편차를 0.01로 바꿔 출력을 해보자.
이번에는 0.5부근에집중되었다.
기울기 소실 문제는 일어나지 않지만, 활성화값이; 치우쳤다는 것은 표현력 관점에서는 큰 문제가 될 수 있다.
이게 무슨 뜻이냐면, 다수의 뉴런이 거의 같은 값을 출력하고 있으니 뉴런을 여러개 둔 의미가 없어진다는 뜻이다.
그래서 활성화값들이 치우치면 표현력을 제한한다는 관점에서 문제가 된다.
다음은 사비에르 글로로트, 요슈아 벤지오의 논문에서 권장하는 가중치 초깃값인 Xavier 초깃값을 적용해보자.
현재 일반적인 딥러닝 프레임워크들이 Xavier 초깃값을 표준적으로 이용하고 있다.
앞 계층의 노드가 n개라면 표준편차가 1/(n^1/2)인 분포를 사용하면 된다는 결론을 이끈 것이다.
다음은 Xavier 초깃값을 이용한 각 층의 활성화값 분포이다.
ReLU를 사용할 때의 가중치 초깃값
Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과이다. sigmoid와 tanh함수는 좌우대칭이라 중앙 부근이 선형인 함수로 볼 수 있다.
반면 ReLU를 이용할 때는 ReLU에 특화된 초깃값, He 초깃값을 이용한다.
He 초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 (2/n)^1/2인 정규분표를 사용한다.
ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 2배의 계수가 필요하다고 해석할 수 있다.
다음의 분포를 보자.
결과를 보면 std = 0.01일 때 각 층의 활성화값들은 아주 작은 값들이다.
신경망에 아주 작은 데이터가 흐른다는 것은 역전파 때 가중치의 기울기 역시 작아진다는 뜻이다.
이는 중대한 문제이며, 실제로도 학습이 거의 이뤄지지 않을 것이다.
이어서 Xavier 초깃값 결과를 보면 이쪽은 층이 깊어지면서 치우침이 조금씩 커진다. 실제로 층이 깊어지면 활성화값들의 치우침도 커지고, 학습할 때 '기울기 소실' 문제를 일으킨다.
He 초깃값은 모든 층에서 균일하게 분포되었다. 층이 깊어져도 분포가 균일하게 유지되기에 역전파 때도 적절한 값이 나올 것으로 기대할 수 있다.
MNIST 데이터셋으로 본 가중치 초깃값 비교
실제 데이터를 가지고 가중치의 초깃값을 주는 방법이 신경망 학습에 얼마나 영향을 주는지 확인해보자.
세 경우(std = 0.01, Xavier, He) 모두 실험해본다.
#weight_init_compare.py
import os
import sys
import numpy as np
import matplotlib.pyplot as plt
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import SGD
# 0. MNIST 데이터 읽기==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000
# 1. 실험용 설정==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)
networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10, weight_init_std=weight_type)
train_loss[key] = []
# 2. 훈련 시작==========
for i in range(max_iterations):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for key in weight_init_types.keys():
grads = networks[key].gradient(x_batch, t_batch)
optimizer.update(networks[key].params, grads)
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
if i % 100 == 0:
print("===========" + "iteration:" + str(i) + "===========")
for key in weight_init_types.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 3. 그래프 그리기==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 2.5)
plt.legend()
plt.show()
이 실험은 층별 뉴런 수가 100개인 5층 신경망에서 활성화 함수로 ReLU를 사용했다.
위의 결과값을 보면 std=0.01일 때는 학습이 전혀 이뤄지지 않았다. 앞서 보았듯 순전파 때 너무 작은 값(0 근처로 밀집한 데이터)이 흐르기 때문이다.
반대로 Xavier와 He 초깃값의 경우는 학습이 순조롭게 이뤄지고 있다. 학습 진도는 He 초깃값쪽이 더 빠르다.
배치 정규화
각 층의 활성화값 분포를 관찰하며, 가중치의 초깃값을 적절히 설정하면 각 층의 활성화값 분포가 적당히 퍼지면서
학습이 원활하게 수행됨을 배웠다.
각 층이 활성화를 적당히 퍼뜨리도록 '강제'해보면 어떨까?
배치 정규화(Batch Normalization)가 그런 아이디어에서 출발한 방법이다.
배치 정규화 알고리즘
배치 정규화는 2015년 제안된 방법이다.
배치 정규화가 주목받는 이유는 다음과 같다.
- 학습을 빨리 진행할 수 있다(학습 속도 개선)
- 초깃값에 크게 의존하지 않는다
- overfitting을 억제한다
배치 정규화의 기본 아이디어는 앞에서 말했듯이 각 층의 활성화값이 적당히 분포되도록 조정하는 것이다. 그래서 아래 그림과 같이 데이터 분포를 정규화하는 '배치 정규화 계층'을 신경망에 삽입한다.
배치 정규화는 학습시 미니배치를 단위로 정규화한다. 구체적으로는 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화한다. 수식으로는 다음과 같다.
여기서 미니배치 B는 m개의 입력 데이터 집합에 대해 평균과 분산을 구한다.
그리고 입력 데이터를 평균이 0, 분산이 1이 되게(적절한 분포가 되게) 정규화한다.
epsilon은 작은 값(ex: 10e-7)으로, 0으로 나누는 일을 방지하는 역할을 한다.
위 식은 결론적으로, 미니배치 입력 데이터 B를 평균 0,분산 1인 데이터로 변환하는 일을 한다.
이를 통해 데이터 분포가 덜 치우치게 할 수있다.
배치 정규화 계층마다 정규화된 데이터에 고유한 확대(scale)와 이동(shift)변환을 수행한다.
수식은 다음과 같다.
이 식에서는 gamma가 확대를, beta가 이동을 담당한다.
두 값은 처음에는 gamma = 1, beta = 0으로 시작하고, 학습하면서 적합한 값으로 조정해간다.
gamma=1은 1배 확대를 뜻하고 beta = 0 은 이동하지 않음을 뜻한다.
즉, 처음에는 원본 그대로에서 시작한다는 뜻이다.
이러한 배치 정규화 알고리즘은 신경망에서 순전파 때 적용된다.
이를 계산 그래프로 표현하면 다음과 같이 표현할 수 있다.
배치 정규화의 효과
MNIST 데이터셋을 사용해 배치 정규화 계층을 사용할 때와 사용하지 않을 때 학습 진도가 어떻게 달라지는지 구현하여 확인해보자.
다음은 가중치 초깃값의 표준편차를 다양하게 바꿔가며 학습 경과를 관찰한 코드, 결과이다.
#batch_norm_test.py
import sys
import os
import numpy as np
import matplotlib.pyplot as plt
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
from common.multi_layer_net_extend import MultiLayerNetExtend
from common.optimizer import SGD, Adam
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 학습 데이터를 줄임
x_train = x_train[:1000]
t_train = t_train[:1000]
max_epochs = 20
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01
def __train(weight_init_std):
bn_network = MultiLayerNetExtend(input_size=784,
hidden_size_list=[100, 100, 100, 100, 100],
output_size=10,
weight_init_std=weight_init_std,
use_batchnorm=True)
network = MultiLayerNetExtend(input_size=784,
hidden_size_list=[100, 100, 100, 100, 100],
output_size=10,
weight_init_std=weight_init_std)
optimizer = SGD(lr=learning_rate)
train_acc_list = []
bn_train_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]
for _network in (bn_network, network):
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)
bn_train_acc = bn_network.accuracy(x_train, t_train)
train_acc_list.append(train_acc)
bn_train_acc_list.append(bn_train_acc)
print("epoch:" + str(epoch_cnt) + " | " + str(train_acc) + " - "
+ str(bn_train_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
return train_acc_list, bn_train_acc_list
# 그래프 그리기==========
weight_scale_list = np.logspace(0, -4, num=16)
x = np.arange(max_epochs)
for i, w in enumerate(weight_scale_list):
print("============== " + str(i+1) + "/16" + " ==============")
train_acc_list, bn_train_acc_list = __train(w)
plt.subplot(4, 4, i+1)
plt.title("W:" + str(w))
if i == 15:
plt.plot(x, bn_train_acc_list,
label='Batch Normalization', markevery=2)
plt.plot(x, train_acc_list, linestyle="--",
label='Normal(without BatchNorm)', markevery=2)
else:
plt.plot(x, bn_train_acc_list, markevery=2)
plt.plot(x, train_acc_list, linestyle="--", markevery=2)
plt.ylim(0, 1.0)
if i % 4:
plt.yticks([])
else:
plt.ylabel("accuracy")
if i < 12:
plt.xticks([])
else:
plt.xlabel("epochs")
plt.legend(loc='lower right')
plt.show()
거의 모든 경우에서 배치 정규화를 사용할 때 학습 진도가 빠른 것으로 나타난다.
실제로, 배치 정규화를 이용하지 않은 경우엔 초깃값이 잘 분포되어 있지 않으면 학습이 전혀 진행되지 않는 모습도 확인할 수 있다.
바른 학습을 위해
기계학습에서는 overfitting이 문제가 되는 일이 많다.
overfitting이란 신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태이다.
아직 보지 못한 데이터가 주어져도 바르게 식별해내는 모델이 바람직하다.
overfitting은 주로 다음의 두 경우에 일어난다
- 매개변수가 많고 표현력이 높은 모델
- 훈련 데이터가 적음
이번엔 일부러 위 두 조건을 충족하여 overfitting을 일으켜보자.
6만개의 MNIST dataset의 훈련 데이터중 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)
print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
각각의 list에는 epoch 단위의 정확도를 저장한다.
그래프 실행 결과는 다음과 같다.
훈련 데이터를 사용하여 측정한 정확도는 100에폭을 지나는 무렵부터 거의 100%이다.
그러나 시험 데이터에 대해서는 큰 차이를 보인다.
이처럼 정확도가 크게 벌어지는 것은 훈련 데이터에만 적응(fitting)해버린 결과이다.
훈련 때 사용하지 않은 범용(시험 데이터)에는 제대로 대응하지 못하는 것을 확인할 수 있다.
가중치 감소
overfitting 억제용으로 많이 이용해온 방법 중 가중치 감소(weight decay)가 있다.
이는 학습과정에서 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 overfitting을 억제한다.
원래 overfitting은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문이다.
복습을 해보자.
신경망 학습의 목적은 손실함수의 값을 줄이는 것이다.
예를 들어 가중치의 제곱 norm(L2 norm)을 손실 함수에 더한다.
그러면 가중치가 커지는 것을 억제할 수 있다. L2 norm에 따른 가중치 W의 감소는 1/2(lambda * W^2)가 되고,
이 값을 손실 함수에 더한다.
lambda는 정규화의 세기를 조절하는 하이퍼파라미터이다. lambda를 크게 설정할수록 큰 가중치에 대한 페널티가 커진다.
또한 식 앞에 1/2는 전체식을 미분 결과인 lambda * W를 조정하는 역할의 상수이다.
가중치 감소는 모든 가중치 각각의 손실 함수에 1/2(lambda * W^2)를 더한다.
따라서 가중치의 기울기를 구하는 계산에서는 그동안의 오차역전파법에 따른 결과에 정규화 항을 미분한 lambda * W를 더한다.
방금 수행한 실험에서 lambda = 0.1로 가중치 감소를 적용한다.
실행 결과는 다음과 같다.
여전히 훈련 데이터와 시험 데이터에 대한 정확도에 차이가 있지만,
lambda가 0일 때와 비교해보면 차이가 줄었다.
다시 말해 overfitting이 억제되었다는 뜻이다. 또한, 훈련 데이터에 대한 정확도가 100%에 도달하지 못한 점도 주목하자.
드롭아웃
앞 절에서는 overfitting을 억제하는 방식으로 손실 함수에 가중치의 L2 norm을 더한 가중치 감소 방법에 대해 설명했다.
가중치 감소는 간단하게 구현할 수 있고 어느정도 지나친 학습을 억제할 수 있다.
그러나 신경망 모델이 복잡해지면 가중치 감소만으로는 대응이 어려워진다.
이럴때는 흔히 드롭아웃(Dropout)이라는 기법을 이용한다.
드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법이다.
훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다.
삭제한 뉴런은 다음 그림과 같이 신호를 전달하지 않게 된다. 훈련때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험때는 모든 뉴런에 신호를 전달한다. 단, 시험때는 각 뉴런의 훈련 때 삭제 안한 비율을 곱하여 출력한다.
드롭아웃을 구현해본다.
순전파 forward에서는 훈련때(train_flg = True일 때)만 잘 계산해두면 시험 때는 단순히 데이터를 흘리기만 하면 된다.
삭제 안한 비율은 곱하지 않아도 좋다. 실제 딥러닝 프레이워크들도 비율을 곱하지 않는다.
더 효율적인 구현이 궁금하면 Chainer Framework(http://chainer.org/)의 드롭아웃 구현을 참고하면 좋을 것이다.
class Dropout:
def __init__(self,dropout_ratio =0.5):
self.dropout_ration = 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
여기서 핵심은 훈련시에는 순전파때마다 self.mask에 삭제할 뉴런을 False로 표시한다.
self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정한다.
역전파 때의 동작은 ReLU와 같다.
즉, 순전파 때 신호를 통과시키는 뉴런은 역전파때도 그대로 신호를 통과시키고, 순전파때 통과시키지 않은 뉴런은 역전파때도 신호를 차단한다.
책의 소스 코드에서는 Trainer Class를 이용하여 구현을 간소화했다. 아래 코드를 참고하자.
# common/train.py
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.optimizer import *
class Trainer:
"""신경망 훈련을 대신 해주는 클래스
"""
def __init__(self, network, x_train, t_train, x_test, t_test,
epochs=20, mini_batch_size=100,
optimizer='SGD', optimizer_param={'lr':0.01},
evaluate_sample_num_per_epoch=None, verbose=True):
self.network = network
self.verbose = verbose
self.x_train = x_train
self.t_train = t_train
self.x_test = x_test
self.t_test = t_test
self.epochs = epochs
self.batch_size = mini_batch_size
self.evaluate_sample_num_per_epoch = evaluate_sample_num_per_epoch
# optimzer
optimizer_class_dict = {'sgd':SGD, 'momentum':Momentum, 'nesterov':Nesterov,
'adagrad':AdaGrad, 'rmsprpo':RMSprop, 'adam':Adam}
self.optimizer = optimizer_class_dict[optimizer.lower()](**optimizer_param)
self.train_size = x_train.shape[0]
self.iter_per_epoch = max(self.train_size / mini_batch_size, 1)
self.max_iter = int(epochs * self.iter_per_epoch)
self.current_iter = 0
self.current_epoch = 0
self.train_loss_list = []
self.train_acc_list = []
self.test_acc_list = []
def train_step(self):
batch_mask = np.random.choice(self.train_size, self.batch_size)
x_batch = self.x_train[batch_mask]
t_batch = self.t_train[batch_mask]
grads = self.network.gradient(x_batch, t_batch)
self.optimizer.update(self.network.params, grads)
loss = self.network.loss(x_batch, t_batch)
self.train_loss_list.append(loss)
if self.verbose: print("train loss:" + str(loss))
if self.current_iter % self.iter_per_epoch == 0:
self.current_epoch += 1
x_train_sample, t_train_sample = self.x_train, self.t_train
x_test_sample, t_test_sample = self.x_test, self.t_test
if not self.evaluate_sample_num_per_epoch is None:
t = self.evaluate_sample_num_per_epoch
x_train_sample, t_train_sample = self.x_train[:t], self.t_train[:t]
x_test_sample, t_test_sample = self.x_test[:t], self.t_test[:t]
train_acc = self.network.accuracy(x_train_sample, t_train_sample)
test_acc = self.network.accuracy(x_test_sample, t_test_sample)
self.train_acc_list.append(train_acc)
self.test_acc_list.append(test_acc)
if self.verbose:
print("=== epoch:" + str(self.current_epoch) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc) + " ===")
self.current_iter += 1
def train(self):
for i in range(self.max_iter):
self.train_step()
test_acc = self.network.accuracy(self.x_test, self.t_test)
if self.verbose:
print("=============== Final Test Accuracy ===============")
print("test acc:" + str(test_acc))
#overfit_dropout.py
import os
import sys
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_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]
# 드롭아웃 사용 유무와 비울 설정 ========================
use_dropout = True # 드롭아웃을 쓰지 않을 때는 False
dropout_ratio = 0.2
# ====================================================
network = MultiLayerNetExtend(input_size=784,
hidden_size_list=[100, 100, 100, 100, 100, 100],
output_size=10, use_dropout=use_dropout,
dropout_ration=dropout_ratio)
trainer = Trainer(network, x_train, t_train, x_test, t_test,
epochs=301, mini_batch_size=100,
optimizer='sgd', optimizer_param={'lr': 0.01}, verbose=True)
trainer.train()
train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list
# 그래프 그리기==========
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='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
# epoch:301, train acc:0.73, test acc:0.6315
드롭아웃 실험은 마찬가지로 7층 네트워크(뉴런수 각층 100개, 활성화함수 ReLU)를 써서 진행했다.
드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었다.
또, 훈련 데이터에 대한 정확도가 100%에 도달하지도 않게 도었다. 이처럼 드롭아웃을 이용하면 표현력을 높이면서도 overfitting을 억제할 수 있다.
기계학습에서는 앙상블 학습(ensemble)을 애용한다. 앙상블 학습은 개별적으로 학습시킨 여려 모델의 출력을 평균내어 측정한다.
이는 드롭아웃과 밀접하다. 드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위;를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문이다.그리고 추론 때는 뉴런의 출력에 삭제한 비율을 곱함으로써 앙상블 학습에서 여러모델의 평균을 내는 효과를 얻는 것이다.
즉, 드롭아웃은 앙상블 학습과 같은 효과를 하나의 네트워크로 구현했다고 생각할 수 있다.
적절한 하이퍼파라미터 값 찾기
신경망에는 하이퍼파라미터가 다수 등장한다.
각층의 뉴런 수, 배치 크기, 매개변수 갱신 시 학습률과 가중치 감소 등이다. 이러한 하이퍼파라미터 값을 적절히 설정하지 않으면 모델의 성능이 크게 떨어지기도 한다.
이 값은 매우 중요하지만 그 값을 결정하기까지는 일반적으로 많은 시행착오를 겪는다. 하이퍼파라미터를 최대한 효율적으로 탐색하는 방법을 보자.
검증 데이터
지금까지는 데이터셋을 훈련 데이터와 시험 데이터라는 두가지로 분리해 이용했다.
훈련 데이터로 학습을 하고, 시험 데이터로 성능을 평가한다.
주의할점은 하이퍼파라미터의 성능을 평가할 때는 성능 데이터를 사용해서는 안된다.
시험 데이터를 사용하여 하이퍼파라미터를 조정하면 이 값이 시험 데이터에 overfitting되기 때문이다. 바꿔말하면, 하이퍼파라미터 값의 '좋음'을 시험 데이터로 확인하게 되므로 시험 데이터에만 적합하도록 모델이 조정되어 버린다. 이렇게 되면 다른 데이터에는 적응하지 못하니 범용 성능이 떨어지는 모델이 될지도 모른다.
그래서 하이퍼파라미터를 조정할 때는 전용 확인 데이터가 필요하다.
하이퍼파라미터 조정용 데이터를 검증 데이터(validation data)라고 부른다.
MNIST는 훈련,시험데이터로만 분리를 해두었기 때문에, 검증 데이터를 얻는 가장 간단한 방법은 훈련 데이터중 20%정도를 검증 데이터로 먼저 분리하는 것이다.
(x_train, t_train), (x_test, t_test) = load_mnist()
x_train, t_train = shuffle_dataset(x_train, t_train)
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:]
훈련 데이터를 분리하기 전에 입력 데이터와 정답 레이블을 뒤섞는다.
하이퍼파라미터 최적화
하이퍼파라미터 최적화 핵심은 하이퍼파라미터의 최적값이 존재하는 범위를 조금씩 줄여나가는 것이다.
범위를 조금씩 줄이려면 우선 대략적인 범위를 설정하고 그 범위에서 무작위로 하이퍼파라미터 값을 샘플링 후, 그 값으로 정확도를 평가한다.
정확도를 잘 살피며 여러번 반복하여 범위를 좁혀간다.
신경망의 하이퍼파라미터 최적화에서는 그리드(Grid Search)같은 규칙적인 탐색보다는 무작위로 샘플링해 탐색하는 편이 좋은 결과를 낸다고 알려져 있다. 이는 최종 정확도에 미치는 영향력이 하이퍼파라미터마다 다르기 때문이다.
대략적으로 지정하는 것이 효과적인데, 실제로도 0.001에서 1000사이와 같이 '10의 거듭제곱' 단위로 범위를 지정한다.
이를 로그 스케일(log scale)로 지정한다고 한다.
하이퍼파라미터를 최적화 할 때는 딥러닝 학습에서는 오랜 기간(며칠, 혹은 몇주 이상)이 걸린다는 점을 기억해야 한다.
따라서 나쁠 것 같은 값은 일찍 포기하는 것이 좋다.
학습을 위한 epoch을 작게 하여, 1회 평가에 걸리는 시간을 단축하는 것이 효과적이다.
지금까지의 이야기를 정리하면 다음과 같다.
- 0단계: 하이퍼파라미터 값의 범위를 설정한다
- 1단계: 설정된 범위에서 하이퍼파라미터의 값을 무작위로 추출한다.
- 2단계: 1단계에서 샘플링한 하이퍼파라미터 값을 이용해 학습하고, 검증 데이터로 정확도를 평가한다.(단, epoch을 작게 설정한다.)
- 3단계: 1~2단계를 특정횟수 반복하여 정확도 결과를 보고 하이퍼파라미터범위를 좁힌다.
위에서 설명한 방법은 실용적인 방법이다. 과학적이기보다는 수행자의 지혜와 직관에 의존하는 경향이 있다.
더 세련된 방법을 원한다면 베이즈 최적화(Bayesian optimizaion)이 있다. 베이즈 정리를 중심으로 한 수학 이론을 구사하여 더 엄밀하고 효율적으로 최적화를 수행한다. 자세한 것은 <Practical Bayesian Optimization of Machine Learning Algorihms> 논문을 참고하자.
하이퍼파라미터 최적화 구현하기
MNIST dataset을 이용하여 하이퍼파라미터를 최적화해보자.
학습률과 가중치 감수의 세기를 조절하는 계수(가중치 감소 계수)를 탐색하는 문제를 풀어보자.
앞에서 말한 대로, 하이퍼파라미터의 검증은 0.001~1000 사이 같은 로그 스케일 범위에서 무작위로 추출해 수행한다.
10 ** np.random.uniform(-3,3)처럼 작성할 수 있다.
이 예에서는 가중치 감소 계수를 10^-8~10^-4, 학습률을 10^-6~10^-2 범위부터 시작한다.
이 경우 무작위 추출 코드를 다음과 같이 작성할 수 있다.
weight_decay = 10**np.random.uniform(-8,-4)
lr = 10 ** np.random.uniform(-6,-2)
다음으로 가중치 감소 계수를 10^-8~10^-4, 학습률을 10^-6~10^-2 범위 로 하여 실험하는 구현 코드를 보자.
# hyperparameter_optimization.py
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
# 그래프 그리기========================================================
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()
Best6까지의 하이퍼파라미터 값(학습률과 가중치 감소 계수)를 살펴보자.
위 결과를 보면 학습이 잘 진행될 때의 학습률은 0.001~0.01, 가중치 감소 계수는 10^-8~10^-6 라는 것을 알 수 있다.
이처럼 잘될 것 같은 값의 범위를 관찰하고 범위를 좁혀간다. 이 과정을 반복하는 것이다.
특정 단계에 도달하면 최종 하이퍼파라미터 값을 하나 선택한다.
정리
- 매개변수 갱신 방법에는 확률적 경사 하강법(SGD) 외에도 모멘텀, AdaGrad, Adam 등이 있다.
- 가중치 초깃값을 정하는 방법은 올바른 학습을 하는데 매우 중요하다.
- 가중치 초깃값으로는 Xavier, He가 효과적이다.
- 배치 정규화를 이용하면 학습을 빠르게 진행할 수 있으며, 초깃값에 영향을 덜 받게 된다.
- overfitting을 억제하는 정규화 기술로는 가중치 감소와 드롭아웃이 있다.
- 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히면서 하는 것이 효과적이다.
※dataset 및 코드 제공 - 밑바닥부터 시작하는 딥러닝 GitHub: https://github.com/kchcoo/WegraLee-deep-learning-from-
GitHub - kchcoo/WegraLee-deep-learning-from-scratch
Contribute to kchcoo/WegraLee-deep-learning-from-scratch development by creating an account on GitHub.
github.com
'공부 > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
밑바닥부터 시작하는 딥러닝1 - Chap8. 딥러닝 (1) | 2024.12.10 |
---|---|
밑바닥부터 시작하는 딥러닝1 - Chap7. 합성곱 신경망(CNN) (4) | 2024.12.06 |
밑바닥부터 시작하는 딥러닝1 - Chap5. 오차역전파법 (1) | 2024.11.27 |
밑바닥부터 시작하는 딥러닝1 - Chap4. 신경망 학습 (2) | 2024.11.20 |
밑바닥부터 시작하는 딥러닝1 - Chap3. 신경망 (1) | 2024.11.20 |