[밑딥] 6장. 학습 관련 기술들

Speedwell🍀·2022년 5월 31일
0

  1. 신경망 학습에서 중요한 주제

    • 가중치 매개변수의 최적값을 탐색하는 최적화 방법
    • 가중치 매개변수 초깃값
    • 하이퍼파라미터 설정 방법
  2. 오버피팅의 대응책인 가중치 감소와 드롭아웃 등의 정규화 방법

  3. 배치 정규화

➡️ 이번 장에서 설명하는 기법을 이용하면 신경망(딥러닝) 학습의 효율과 정확도를 높일 수 있다.


1. 매개변수 갱신

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

신경망 최적화는 굉장히 어려운 문제


지금까지 최적의 매개변수 값을 찾는 단서로 매개변수의 기울기(미분)를 이용
(매개변수의 기울기를 구해, 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복해서 점점 최적의 값에 다가가는 방식)
➡️ 확률적 경사 하강법(SGD)

SGD의 단점을 알아보고 SGD보다 똑똑한 최적화 기법을 알아보자!


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

SGD의 전략은 가장 크게 기울어진 방향으로 가는 것!

SGD 복습

W: 갱신할 가중치 매개변수
ŋ: 학습률 (실제로는 0.01이나 0.001과 같은 값을 미리 정해서 사용)
əL/əW: W에 대한 손실 함수의 기울기
➡️ 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]
  • 위의 코드에서 학습률 lr은 인스턴스 변스로 유지한다.
  • update(params, grads) 메서드는 SGD 과정에서 반복해서 불린다.
    • params와 grads는 (지금까지의 신경망 구현과 마찬가지로) 딕셔너리 변수

SGD 클래스를 사용한 신경망 매개변수 진행의 의사코드를 작성해보자.

network = TwoLayerNet(...)
optimizer = SGD()

for i in range(10000):
	...
    x_batch, t_batch = get_mini_batch(...) # 미니배치
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads)
    ...

매개변수 갱신은 optimizer가 책임지고 수행하니 우리는 optimizer에 매개변수와 기울기 정보만 넘겨주면 된다!

📌 이처럼 최적화를 담당하는 클래스를 분리해 구현하면 기능을 모듈화하기 좋다.
ex) 곧 소개할 모멘텀이라는 최적화 기법 역시 update(params, grads)라는 공통의 메서드를 갖도록 구현
따라서 optimizer = SGD(), optimizer = Momentum()처럼 필요에 따라 바꿀 수 있음!



SGD의 단점

SGD는 단순하고 구현도 쉽지만, 문제에 따라서 비효율적

예를 들어 아래의 함수의 최솟값을 구하는 문제를 생각해보자.

아래는 함수의 기울기를 그린 것이다.
y축 방향은 크고 x축 방향이 작다는 특징!

💥주의할 점) 최솟값이 되는 장소는 (x,y)=(0,0)이지만, 아래 그림에서 보여주는 기울기 대부분은 (0,0) 방향을 가리키지 않는다!

이 함수에 SGD를 적용해보자.
탐색을 시작하는 장소(초기값)는 (x, y) = (-7.0, 2,0)

위의 그림에서 볼 수 있듯이, SGD에 의한 최적화 갱신 경로를 살펴보면 최솟값인 (0,0)까지 상당히 비효율적으로 지그재그로 이동한다.

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

SGD가 지그재그로 탐색하는 근본 원인은 기울어진 방향이 본래의 최솟값과 다른 방향을 가리키기 때문!


SGD의 단점을 개선해주는 방법을 살펴보자!

  1. 모멘텀
  2. AdaGrad
  3. Adam


1-2) 모멘텀 (Momentum)

모멘텀은 '운동량'을 뜻하는 단어로, 물리와 관계가 있다.
모멘텀은 공이 그릇의 곡면(기울기)을 따라 구르듯 움직이는 이미지를 떠올리면 된다.


SGD와 달리 새로 생긴 변수 v가 있는데, 이는 물리에서 말하는 속도(velocity)
➡️ 위의 모멘텀 수식 첫 줄은 기울기 방향으로 힘을 받아 물체가 가속된다는 물리 법칙을 나타낸다.

αv 항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할
α는 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 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]

물체의 속도를 의미하는 인스턴스 변수 v는 초기화 때는 아무 값도 담지 않고, 대신 update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장한다.


위의 그림을 SGD와 비교하면 지그재그의 정도가 덜한 것을 볼 수 있다.
➡️ x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문!
(반대로 y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 y축 방향의 속도는 안정적이지 않다.)

전체적으로 SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 줄어든다.



1-3) AdaGrad

신경망 학습에서는 학습률(ŋ) 값이 중요!!

이 값이 너무 작으면 학습 시간이 너무 길어지고, 반대로 너무 크면 발산하여 학습이 제대로 이루어지지 않는다.

📌 이 학습률을 정하는 효과적 기술 중에 학습률 감소(learning rate decay)
: 학습을 진행하면서 학습률을 점차 줄여가는 방법
➡️ 처음에는 크게 학습하다가 조금씩 작게 학습한다는 얘기로, 실제 신경망 학습에 자주 쓰인다.


📌 AdaGrad는 개별 매개변수에 적응적으로(adaptive) 학습률을 조정하면서 학습을 진행한다.

새로 생긴 변수 h는 기존 기울기 값을 제곱하여 계속 더해준다. (⊙기호는 행렬의 원소별 곱셈)

매개변수를 갱신할 때 1/루트(h)을 곱해 학습률을 조정한다.
매개변수의 원소 중에서 많이 움직인(크게 갱신된) 원소는 학습률이 낮아진다는 뜻!
➡️ 학습률 감소가 매개변수의 원소마다 다르게 적용


AdaGrad는 과거의 기울기를 계속 더해간다. 그래서 학습을 진행할수록 갱신 강도가 약해진다.
무한히 계속 학습한다면 어느 순간 갱신량이 0이 되어 전혀 갱신되지 않게 된다.
➡️ 이런 문제를 개선한 기법이 RMSProp

RMSProp은 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영한다.
이를 지수이동평균(Exponential Moving Average; EMA)이라 하여, 과거 기울기의 반영 규모를 기하급수적으로 감소시킨다.


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축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어든다.



1-4) Adam

직관적으로는 모멘텀과 AdaGrad를 융합한 듯한 방법
하이퍼파라미터의 편향 보정이 진행된다는 특징!

모멘텀과 비슷한 패턴인데, 모멘텀 때보다 공의 좌우 흔들림이 적다.
📌 이는 학습의 갱신 강도를 적응적으로 조정해서 얻는 혜택!



1-5) 어느 갱신 방법을 이용할 것인가?

그림을 보면 알 수 있듯이 사용한 기법에 따라 갱신 경로가 다르다.
각자의 장단이 있어 상황을 고려해서 시도해봐야한다.

많은 연구에서 SGD 사용.
모멘텀과 AdaGrad도 시도해볼만한 가치가 있음.
요즘에는 Adam 많이 사용.



1-6) MNIST 데이터셋으로 본 갱신 방법 비교

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

SGD의 학습 진도가 가장 느리고, 나머지 세 기법의 진도는 비슷해보인다.
💥주의할 점은 하이퍼파라미터인 학습률과 신경망의 구조(층 깊이 등)에 따라 결과가 달라진다는 것!
(일반적으로 SGD보다 다른 세 기법이 빠르게 학습하고, 때로는 최종 정확도도 높게 나타난다.)



2. 가중치의 초깃값

신경망 학습에서 특히 중요한 것이 가중치의 초깃값!
가중치의 초깃값을 무엇으로 설정하느냐가 신경망 학습의 성패를 가를 때가 자주 있음

권장 초깃값에 대해 알아보고, 실험을 통해 실제로 신경망 학습이 신속하게 이뤄지는 모습을 확인해보자!


2-1) 초깃값을 0으로 하면?

이제부터 가중치 감소 기법을 알아보자!

가중치 감소(weight decay): 오버피팅을 억제해 범용 성능을 높이는 테크닉
간단히 말하자면 가중치 매개변수의 값이 작아지도록 학습하는 방법
➡️ 가중치 값을 작게 하여 오버피팅이 일어나지 않게 하는 것!


지금까지 가중치 초깃값은 0.01 * np.random.randn(10, 100)처럼 정규분포에서 생성되는 값을 0.01배 한 작은 값(표준편차가 0.01인 정규분포)


🤔 만약 가중치의 초깃값을 모두 0으로 설정하면?
❗ 학습이 올바로 이루어지지 않는다!

가중치를 균일한 값으로 설정하면 안된다!
그 이유는 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문!

두 번째 층의 모든 뉴런에 같은 값이 입력된다는 것은 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 뜻! (곱셈 노드의 역전파를 떠올려보기)
➡️ 그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지

이는 가중치를 여러 개 갖는 의미를 사라지게 한다.
➡️ 가중치의 대칭적인 구조를 무너뜨리려면 초깃값을 무작위로 설정해야 한다.


2-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
    a = np.dot(x, w)
    z = sigmoid(a)
    activations[i] = z

💥 이 코드에서는 가중치의 분포에 주의해야 한다.
이번에는 표준편차가 1인 정규분포를 이용했는데, 이 분포된 정도(표준편차)를 바꿔가며 활성화값들의 분포가 어떻게 변화하는지 관찰하는 것이 이 실험의 목적이다.


activations에 저장된 각 층의 활성화값 데이터를 히스토그램으로 그려보자!

# 히스토그램 그리기
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()

위의 그림은 가중치를 표준편차가 1인 정규분포로 초기화할 때의 각 층의 활성화값 분포이다.

여기에서 사용한 시그모이드 함수는 그 출력이 0이나 1에 가까워지자 그 미분은 0에 다가간다.
➡️ 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다.
➡️ 기울기 소실(gradient vanishing) 문제




이번에는 가중치의 표준편차를 0.01로 바꿔 같은 실험을 반복해보자.
앞의 코드에서 가중치 초깃값 설정 부분만 아래와 같이 바꾸면 된다.

# w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01

이번에는 0.5 부근에 집중!
➡️ 0과 1로 치우치진 않았으니 기울기 소실 문제는 일어나지 않지만, 활성화값들이 치우쳤다는 것은 표현력을 제한한다는 관점에서 문제!

이 상황에서는 다수의 뉴런이 거의 같은 값을 출력하고 있기 때문에, 뉴런을 여러 개 둔 의미가 없어진다!

📌 각 층의 활성화값은 적당히 고루 분포되어야 한다.
층과 층 사이에 적당하게 다양한 데이터가 흐르게 해야 신경망 학습이 효율적으로 이뤄지기 때문!


Xavier 초깃값

  • 일반적인 딥러닝 프레임워크들이 표준적으로 이용하고 있는 가중치 초깃값
  • 사비에르 글로로트(Xavier Glorot) & 요슈아 벤지오(Yoshua Bengio)의 논문에서 권장하는 가중치 초깃값
    • 이 논문은 각 층의 활성화값들을 광범위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾고자 했다.

      • 앞 계층의 노드가 n개라면 표준편차가 1/루트(n)인 분포를 사용하면 된다는 결론!

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


앞의 코드에서 가중치 초깃값 설정 부분을 아래와 같이 고쳐보자. (모든 층의 노드 수가 100개라고 단순화 가정)

node_num = 100 # 앞 층의 노드 수
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)

앞에서 본 방식보다 확실히 넓게 분포되어 있다.
각 층에 흐르는 데이터가 적절히 퍼져 있으므로, 시그모이드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질 것!

참고) 위의 그림을 보면 층이 깊어질수록 형태가 일그러지는 것을 볼 수 있다. 이는 sigmoid 함수 대신 tanh 함수(쌍곡선 함수)를 이용하면 개선된다. (tanh 함수를 이용하면 말끔한 종 모양으로 분포)

tanh 함수도 sigmoid 함수와 같은 'S'자 모양 곡선 함수이지만, tanh 함수는 원점에서 대칭, sigmoid 함수는 (x,y)=(0,0.5)에서 대칭인 S 곡선. (활성화 함수용으로는 원점에서 대칭인 함수가 바람직)


2-3) ReLU를 사용할 때의 가중치 초깃값

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과

  • sigmoid와 tanh 함수는 좌우 대칭이라 중앙 부근이 선형인 함수
    ➡️ Xavier 초깃값이 적당

ReLU를 이용할 때는 ReLU에 특화된 초깃값 권장!
➡️ He 초깃값

He 초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 루트(2/n)인 정규분포를 사용

  • Xavier는 루트(1/n) ➡️ ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 2배의 계수가 필요


위의 그림은 활성화 함수로 ReLU를 이용한 경우의 활성화값 분포

  1. 표준편차가 0.01인 정규분포(std=0.01)를 초깃값으로 사용한 경우

    • 결과를 보면 각 층의 활성화값들은 아주 작은 값
      → 신경망에 작은 데이터가 흐른다는 것은 역전파 때 가중치의 기울기 역시 작아진다는 뜻 (중대한 문제)
      → 실제로 학습이 거의 이뤄지지 않는다.
  2. Xavier 초깃값을 사용한 경우

    • 층이 깊어지면 서활성화값들의 치우침이 커진다. → 학습할 때 '기울기 소실' 문제
  3. ReLU 전용 He 초깃값을 사용한 경우

    • 모든 층에서 균일하게 분포
      ➡️ 역전파 때도 적절한 값이 나올 것!

결론

활성화 함수로 ReLU를 사용할 때는 He 초깃값, sigmoid나 tanh 등의 S자 모양 곡선일 때는 Xavier 초깃값 사용



3. 배치 정규화 (Batch Normalization)

앞 절에서는 가중치의 초깃값을 적절히 설정하면 각 층의 활성화값 분포가 적당히 퍼지면서 학습이 원활하게 수행됨을 배웠다.

🤔 각 층이 활성화를 적당히 퍼뜨리도록 '강제'해보는 건?
➡️ 배치 정규화

3-1) 배치 정규화 알고리즘

장점

  1. 학습을 빨리 진행할 수 있다.
  2. 초깃값에 크게 의존하지 않는다.
  3. 오버피팅을 억제한다.

배치 정규와의 기본 아이디어는 "각 층에서의 활성화값이 적당히 분포되도록 조정"하는 것
➡️ 데이터 분포를 정규화하는 배치 정규화(Batch Norm) 계층을 신경망에 삽입!


배치 정규화는 미니배치를 단위로 정규화
구체적으로는 분포가 0, 분산이 1이 되도록 정규화

미니배치 B={x₁, x₂, ... x_m}이라는 m개의 입력 데이터의 집합에 데해 평균 m_B분산 (σ_B)²을 구한다.
그리고 입력 데이터를 평균이 0, 분산이 1인 데이터 {^x1, ^x2, ... , ^x_m}이 되게 정규화한다.
ɛ(epsilon)은 작은 값(ex. 10e-7)으로, 0으로 나누는 사태를 예방한다.


미니배치 입력 데이터를 평균 0, 분산 1인 데이터로 변환하는 처리를 활성화 함수 앞or뒤에 삽입함으로써 데이터 분포가 덜 치우치게 할 수 있다.

또한 배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대(scale)와 이동(shift) 변환을 수행한다.
수식은 아래와 같다. ɤ가 확대, ß가 이동. (ɤ=1, ß=0; 처음에는 원본 그대로에서 시작한다는 의미)부터 시작하고, 학습하면서 적절한 값으로 조정해간다.


배치 정규화 알고리즘은 신경망에서 순전파 때 적용된다.
배치 정규화를 계산 그래프로 나타내면 아래와 같다. 배치 정규화의 역전파 유도는 프레드릭 크레저트의 블로그에서 참고!


3-2) 배치 정규화의 효과

MNIST 데이터셋을 사용하여 배치 정규화 계층을 사용할 때와 사용하지 않을 때의 학습 진도가 어떻게 달라지는지 보자!

위의 그래프를 보면 배치 정규화가 학습 속도를 높인다는 것을 알 수 있다.


이번에는 초깃값 분포를 다양하게 줘가며 학습 진행이 어떻게 달라지는지 보자!
아래는 가중치 초깃값의 표준편차를 다양하게 바꿔가며 학습 경과를 관찰한 그래프이다.
(실선: 배치 정규화를 사용한 경우, 점선: 배치 정규화를 사용하지 않은 경우, 그래프: 가중치 초깃값의 표준편차)

그래프를 보면 거의 모든 경우에서 배치 정규화를 사용할 때의 학습 진도가 빠른 것을 알 수 있다.
실제로 배치 정규화를 이용하지 않는 경우엔, 초깃값이 잘 분포되어 있지 않으면 학습이 전혀 진행되지 않는 것을 볼 수 있다.

정리) 배치 정규화를 사용하면 학습이 빨라지며, 가중치 초깃값에 크게 의존하지 않아도 된다.



4. 바른 학습을 위해

기계학습은 범용 성능을 지향한다.
➡️ 오버피팅을 억제하는 기술이 중요!


4-1) 오버피팅

오버피팅이 주로 일어나는 경우

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

일부러 오버피팅을 일으켜보자.
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]

훈련을 수행하는 코드
(지금까지의 코드와 같지만 에폭마다 훈련 데이터와 모든 시험 데이터 각각에서 정확도를 산출)

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

# train_acc_list와 test_acc_list에 에폭 단위(모든 훈련 데이터를 한 번씩 본 단위)의 정확도를 저장
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

위의 그래프를 보면 훈련 데이터와 시험 데이터를 사용하여 측정한 정확도가 크게 벌어지고 있다.
이는 훈련 데이터에만 적응(fitting)해버린 결과!
➡️ 훈련 때 사용하지 않은 범용 데이터(시험 데이터)에는 제대로 대응하지 못하고 있다.


4-2) 가중치 감소 (weight decay)

: 학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 패널티를 부과하여 오버피팅을 억제
(원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문)


신경망 학습의 목적은 손실 함수의 값을 줄이는 것이었다.
이때 가중치의 제곱 norm(L2 norm)을 손실 함수에 더해 가중치가 커지는 것을 억제할 수 있다.

L2 norm

가중치 감소는 모든 가중치 각각의 손실 함수에 ½λW²을 더한다.
➡️ 가중치의 기울기를 구하는 계산에서는 그동안의 오차역전파법에 따른 결과에 정규화 항을 미분한 λW을 더한다.

가중치를 W라 하면 L2 norm에 따른 가중치 감소는 ½λW²

  • λ는 정규화의 세기를 조절하는 하이퍼파라미터
    - λ를 크게 설정할수록 큰 가중치에 대한 패널티가 커진다.
  • ½은 ½λW²의 미분 결과인 λW를 조정하는 역할의 상수

L1 norm: 각 원소의 절댓값의 합
L2 norm: 각 원소의 제곱들의 합
L∞ norm(Max norm): 각 원소의 절댓값 중 가장 큰 것


앞서 한 실험에서 λ=0.1로 가중치 감소를 적용해보자.

훈련 데이터에 대한 정확도와 시험 데이터에 대한 정확도에는 여전히 차이가 있지만, 가중치 감소를 이용하지 않았을 때와 비교하면 그 차이가 줄었다. → "오버피팅 억제됨"

💥하지만 앞서와 달리 훈련 데이터에 대한 정확도가 100%(1.0)에 도달하지 못했다는 점도 주목!


4-3) 드롭아웃 (Dropout)

가중치 감소는 간단하게 구현할 수 있고 어느 정도 지나친 학습을 억제할 수 있다.
하지만 신경망 모델이 복잡해지면 가중치 감소만으로는 대응하기 어렵다. ➡️ 이럴 땐 드롭아웃 적용!


드롭아웃: 뉴런을 임의로 삭제하면서 학습하는 방법

  • 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제
    ➡️ 삭제된 뉴런은 아래 그림과 같이 신호를 전달하지 않게 된다.

  • 시험 때는 모든 뉴런에 신호를 전달

    • 단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제 안 한 비율을 곱하여 출력

드롭아웃을 구현해보자! (쉽게)

순전파를 담당하는 forward 메서드에서는 훈련 때(train_flg = True일 때)만 잘 계산해두면 시험 때는 단순히 데이터를 흘리기만 하면 된다.
삭제 안 한 비율은 곱하지 않아도 된다. (실제 딥러닝 프레임워크들도 비율을 곱하지 않는다.)

더 효율적인 구현은 체이너(Chainer) 프레임워크의 드롭아웃 구현을 참고하면 좋다.

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

📌 훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시

  • self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정
  • 역전파 때의 동작은 ReLU와 같이 순전파 때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 순전파 때 통과시키지 않은 뉴런은 역전파 때도 신호를 차단

위의 그림과 같이 드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었다는 것을 알 수 있다.
또한, 훈련 데이터에 대한 정확도가 100%에 도달하지 않게 되었다.

정리) 드롭아웃을 이용하면 표현력을 높이면서도 오버피팅을 억제할 수 있다.


참고) 앙상블 학습 (ensemble learning)

: 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식

앙상블 학습을 수행하면 신경망의 정확도가 개선된다.

드롭아웃 vs. 앙상블 학습

드롭아웃은 앙상블 학습과 밀접하다.

  • 드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문!

  • 드롭아웃이 추론 때 뉴런의 출력에 삭제한 비율을 곱함으로써 앙상블 학습에서 여러 모델의 평균을 내는 것과 같은 효과를 얻을 수 있기 때문!

➡️ 드롭아웃은 앙상블 같은 효과를 (대략) 하나의 네트워크로 구현!



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

하이퍼파라미터 값을 최대한 효율적으로 탐색하는 방법을 알아보자!


5-1) 검증 데이터

지금까지는 데이터셋을 훈련/시험 데이터 두 가지로 분리

  • 훈련 데이터로 학습
  • 시험 데이터로 범용 성능 평가

💥💥💥하이퍼파라미터의 성능을 평가할 때는 시험 데이터를 사용하면 안 된다!💥💥💥

시험데이터를 사용하여 하이퍼파라미터를 조정하면 하이퍼파라미터 값이 시험 데이터에 오버피팅되기 때문!
→ 범용 성능이 떨어지는 모델이 될 수 있다.


검증 데이터(validation data): 하이퍼파라미터 조정용 데이터; 하이퍼파라미터의 적절성을 평가하는 데이터

  • 훈련 데이터: 매개변수 학습
  • 검증 데이터: 하이퍼파라미터 성능 평가
  • 시험 데이터: 신경망의 범용 성능 평가

MNIST 데이터셋은 훈련/시험 데이터로만 분리되어 있었다.
→ 이런 경우엔 사용자가 직접 데이터를 분리

검증 데이터를 얻는 가장 간단한 방법은 훈련 데이터 중 20% 정도를 검증 데이터로 먼저 분리하는 것!

(x_train, t_train), (x_test, 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:]

코드 설명
훈련 데이터를 분리하기 전에 입력 데이터와 정답 레이블을 뒤섞는다.
(데이터셋 안의 데이터가 치우쳐 있을 수 있으므로 ex.'0'~'9' 정렬)

여기서 사용한 shuffle_dataset 함수는 np.random.shuffle을 이용한 것. (구현은 common/util.py)


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

검증 데이터를 사용하여 하이퍼파라미터를 최적화하는 기법을 살펴보자!

📌 핵심은 하이퍼파라미터의 '최적 값'이 존재하는 범위를 조금씩 줄여간다는 것!


하이퍼파라미터의 최적화 과정

0단계

  • 하이퍼파라미터 값의 범위 설정

1단계

  • 설정된 범위에서 하이퍼파라미터의 값을 무작위로 추출

2단계

  • 1단계에서 샘플링한 하이퍼파라미터 값을 사용하여 학습하고, 검증 데이터로 정학도 평가
    (단, 에폭은 작게 설정)

3단계

  • 1단계와 2단계를 특정 횟수 반복하며, 그 정확도의 결과를 보고 하이퍼파라미터의 범위를 좁힌다.

신경망의 하이퍼파라미터 최적화에서는 grid search 같은 규칙적인 탐색보다는 무작위로 샘플링해 탐색하는 편이 좋은 결과
최종 정확도에 미치는 영향력이 하이퍼파라미터마다 다르기 때문!

하이퍼파라미터의 범위는 대략적으로 지정하는 것이 효과적!
로그 스케일(log scale)로 지정
ex) 0.001 ~ 1,000 (10^-3 ~ 10^3)

📌 학습을 위한 에폭을 작게 하여, 1회 평가에 걸리는 시간을 단축하는 것이 효과적
나쁠 듯한 값은 일찍 포기

참고) 지금까지 설명한 하이퍼파라미터 최적화 방법은 실용적인 방법

베이즈 최적화(Bayesian optimization): 베이즈 정리(Bayes' theorem)를 중심으로 한 수학 이론을 구사하여 더 엄밀하고 효율적으로 최적화 수행 (논문)


5-3) 하이퍼파라미터 최적화 구현하기

MNIST 데이터셋을 사용하여 하이퍼파라미터를 최적화해보자.
학습률과 가중치 감소의 세기를 조절하는 계수(가중치 감소 계수)를 탐색해보는 문제를 풀어보자!

앞에서 말했듯이, 하이퍼파라미터 값은 로그 스케일 범위에서 무작위로 추출해 수행
➡️ 파이썬에서 10 ** np.random.uniform(-3, 3)처럼 작성


하이퍼파라미터의 무작위 추출 코드

weight_dacay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)

위와 같이 가중치 감소 계수와 학습률의 범위를 지정하여 실험한 결과는 아래와 같다.

위의 그림은 검증 데이터의 학습 추이를 정확도가 높은 순서로 나열한 것이다.
'Best-5' 정도까지는 학습이 순조롭게 진행되고 있다.
➡️ 이를 바탕으로 'Best-5'까지의 하이퍼파라미터의 값(학습률 & 가중치 감소 계수)을 살펴보자!

Best-1 (val acc:0.83) | lr:0.0092, weight decay: 3.86e-07
Best-2 (val acc:0.78) | lr:0.00956, weight decay: 6.04e-07
Best-3 (val acc:0.77) | lr:0.00571, weight decay: 1.27e-06
Best-4 (val acc:0.74) | lr:0.00626, weight decay: 1.43e-05
Best-5 (val acc:0.73) | lr:0.0052, weight decay: 8.97e-06

결과를 보면 학습이 잘 진행될 때의 학습률은 0.001~0.01, 가중치 감소 계수는 10^-8~10^-6
이처럼 잘될 것 같은 값의 범위를 관찰하고 범위를 좁혀간다.
그런 다음 그 축소된 범위로 똑같은 작업을 반복!
➡️ 이렇게 적절한 값이 위치한 범위를 좁혀가다가 특정 단계에서 최종 하이퍼파라미터 값을 하나 선택!


6. 정리

  • 매개변수 갱신 방법

    • 확률적 경사 하강법(SGD)
    • 모멘텀
    • AdaGrad
    • Adam
      ...
  • 가중치 초깃값을 정하는 방법은 올바른 학습을 하는 데 매우 중요!

    • 효과적인 가중치 초깃값

      • Xavier 초깃값
      • He 초깃값
  • 배치 정규화를 이용하면 학습을 빠르게 진행할 수 있으며, 초깃값에 영향을 덜 받게 된다.

  • 오버피팅을 억제하는 정규화 기술

    • 가중치 감소
    • 드롭아웃
  • 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히면서 하는 것이 효과적!


참고)
https://deep-learning-study.tistory.com/156
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=lego7407&logNo=221681014509
https://velog.io/@dscwinterstudy/Chapter6-%ED%95%99%EC%8A%B5-%EA%B4%80%EB%A0%A8-%EA%B8%B0%EC%88%A0%EB%93%A4-%EA%B0%80%EC%A4%91%EC%B9%98%EC%9D%98-%EC%B4%88%EA%B9%83%EA%B0%92-a1k5xd5fsl
https://koreanfoodie.me/218
https://slideplayer.com/slide/3345346/
https://velog.io/@kimkihoon0515/%EB%B0%91%EB%B0%94%ED%83%81%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-6%EC%9E%A5

0개의 댓글