Data Science/Deep Learning
[Deep Learning from Scratch 2] chapter 6.1. 게이트가 추가된 RNN
sunnyshiny
2023. 4. 26. 15:14
728x90
RNN의 문제점¶
- RNN은 시계열 데이터에서 시간적으로 멀리 떨어진 장기의존 관계를 잘 학습할 수 없음 $\rightarrow$ BPTT에서 기울기 소실, 기울기 폭발이 일어나기 때문
- LSTM, GRU는 게이트라는 구조가 더해져 시계열 데이터의 장기 의존 관계를 학습할 수 있음
기울기 소실 또는 기울기 폭발¶
- RNN계층이 과거 방향으로 의미있는 기울기를 전달함으로써 시간방향의 의존관계를 학습할수 있음.
- 기울기는 학습 해야할 의미가 있는 정보가 들어있고, 과거로 전달함으로써 장기 의존관계를 학습함.
- 기울기가 중간에 사그라 들면 아무런 정보가 남지 않게 되고 가중치 매개변수는 전혀 생신되지 않음 $\rightarrow$ 장기 의존관계를 학습할 수 없음
- RNN 계층의 활성화 함수로 tanh함수를 이용하면 역전파시 기울기가 tanh 노드를 지날때마다 계속 작아짐. x가 0으로 부터 멀어질 수록 작아짐. relu를 이용하면 기울기 소실을 줄일 수 있음
- 행렬곱에서는 같은 계산을 시계열 데이터의 시간 크기만큼 반족하게 되므로 행렬곱의 특잇값의 최대값이 1보다 크면 지수적으로 증가하고 1보다 작으면 지수적으로 감소할 가능성 이 높다. 그럴 가능성이 높을뿐 반드시 기울기 폭발 소실로 이어지지는 않는다. (필요조건)
In [1]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In [5]:
N = 2 #미니배치 크기
H = 3 # 은닉상태 벡터의 차원수
T = 20 # 시계열 데이터의 길이
dh = np.ones((N, H))
np.random.seed(3)
Wh = np.random.randn(H, H)
norm_list =[]
for t in range(T):
dh = np.matmul(dh, Wh.T)
norm = np.sqrt(np.sum(dh**2))/N
norm_list.append(norm)
plt.plot(norm_list)
plt.title('Exploding Gradient')
plt.show()
- 기울기가 시간에 비례해 지수적으로 증가 $\rightarrow$ 기울기 폭발
- 기울기 폭발이 일어나면 오버플로를 일으켜 NaN같은 값을 발생시킴
In [6]:
N = 2 #미니배치 크기
H = 3 # 은닉상태 벡터의 차원수
T = 20 # 시계열 데이터의 길이
dh = np.ones((N, H))
np.random.seed(3)
Wh = np.random.randn(H, H)*0.5
norm_list =[]
for t in range(T):
dh = np.matmul(dh, Wh.T)
norm = np.sqrt(np.sum(dh**2))/N
norm_list.append(norm)
plt.plot(norm_list)
plt.title('Vanishing Gradient')
plt.show()
- 기울기가 지수적으로 감소 $\rightarrow$ 기울기 소실
- 기울기가 일정 수준 이하로 작아지면 가중치 매개변수가 더 이상 갱신되지 않으므로 장기의존 관계를 학습할 수 없음
기울기 폭발 대책¶
- 기울기 클리핑(Gradient clipping)
$$if \quad \lVert \hat{g} \rVert \ge threshold :\\ \hat{g} = \frac{threshold}{\lVert \hat{g} \rVert}\hat{g} $$
- $ \hat{g} $ :신경망 변수의 기울기
- 기울기의 L2 Norm($\lVert \hat{g} \rVert$)이 threshold를 초과하면 기울기를 수정
In [9]:
def clip_grads(grads, max_norm):
total_norm = 0
for grad in grads:
total_norm += np.sum(grad**2)
total_norm = np.sqrt(total_norm)
rate = max_norm /(total_norm +1e-6)
if rate < 1:
for grad in grads:
grad *= rate
In [10]:
dW1 = np.random.rand(3,3) * 10
dW2 = np.random.rand(3,3) * 10
grads = [dW1, dW2]
max_norm = 5.0
print('before:', dW1.flatten())
clip_grads(grads, max_norm)
print('after:', dW1.flatten())
before: [9.75995422 6.72383676 9.02834109 8.45750871 3.77994041 0.92217009 6.53410903 5.57840762 3.61564763] after: [2.00518189 1.38141178 1.85487202 1.73759455 0.77658848 0.18945978 1.34243223 1.14608344 0.74283454]
기울기 소실과 LSTM¶
- LSTM계층에는 c(Memory cell, 기억셀)경로가 있음
- 기억셀은 데이터를 LSTM계층 내에서만 주고 받음 즉, LSTM계층 내에서만 완결되고 다른 계층으로 출력하지 않음
- LSTM의 은닉상태 h는 다른계층으로 출력
LSTM계층 조립¶
- 기억 셀 $c_t$는 시간 $t$에 대한 LSTM의 기억이 저장, 과거로부터 시각t 까지 필요한 모든 정보가 저장됨
- 필요한 정보를 간직한 기억을 바탕으로 외부계층에 은닉상태 $h_t$를 출력
- $h_t$는 기억셀을 tanh함수로 변환한 값
- 갱신된 $c_t$를 사용해 은닉상태 $h_t$를 계산한다는 것 $\rightarrow$ $c_t$의 각 요소에 tanh함수를 적용한다는 뜻
- 기억셀과$c_t$ 은닉상태 $h_t$의 원소 수는 같다
gate는 데이터의 흐름을 제어, 게이트의 열림 상태는 0.0~1.0사이의 실수로 나타냄(1.0은 완전개방)
- 게이트는 게이트 열림 상태는 제어하기 위해 전용 가중치 매개변수를 이용하며 가중치 매개변수를 학습 데이터로부터 갱신됨.
- 게이트의 열림상태를 구할때는 시그모이드 함수를 사용 (시그모이드의 출력이 0.0~1.0사이의 실수)
ouput gate¶
- tanh($c_t$)는 기억셀 각 원소에 대해 다음 시각의 은닉상태에 얼마나 중요한가를 조정
- ouput gate는 다음 은닉상태 $h_t$의 출력을 담당하는 게이트
게이트의 열림상태는 입력$x_t$와 이전상태 $h_{t-1}$로 부터 구함 $$o = \sigma(x_tW_x^0 +h_{t-1}W_h^0+b^0)$$
- $W^0$, $b^0$는 ouput gate에서 사용하는 가중치 매개변수와 편향
$h_t$는 o와 tanh($c_t$)의 원소곱(아다마르 곱, Hadamard product)
- tanh출력은 -1.0~1.0의 실수로 인코딩된 정보의 강약(정도)로 표시한다고 해석
- sigmoid는 0.0~1.0실수로 데이터를 얼마만큼 통과시킬지를 정하는 비율
- 게이트에서는 시그모이드 함수, 실질적인 정보를 지나는 데이터는 tanh 함수가 활성함수로 사용
In [11]:
from IPython.display import Image
Image(filename='LSTM3-focus-o.png')
Out[11]:
forget gate¶
- 기억 셀에서 무엇을 잊을까 명확하게 지시하는 것 $$f = \sigma(x_tW_x^f +h_{t-1}W_h^f+b^f)\\ c_t = f \odot c_{t-1}$$
In [12]:
Image(filename='LSTM3-focus-f.png')
Out[12]:
In [13]:
Image(filename='LSTM3-focus-i.png')
Out[13]:
LSTM 기울기 흐름
- 기억셀의 역전파는 합과 곱 노드만 지나게 됨
- '+'(합)노드는 상류에서 전해지는 기울기를 그대로 흘림 $\rightarrow$ 기울기 변화가 일어나지 않음
- 'x'(곱)노드는 원소별 곱이 이루어지고 다른 게이트 값을 이용해 곱을 계산 $\rightarrow$ 곱셉의 효과가 누적되지 않아 기울기 소실이 일어나지 않음
- 'x'(곱)노드는 forget gate가 제어 $\rightarrow$ 잊어도 된다고 판단하는 원소에 대해서는 기울기가 작아지고 잊어서는 안되는 ㅇ원소에 대해서는 기울기가 약화되지 않은 채로 과거로 흐름 $\rightarrow$ 기울기 소실없이 전파 $\rightarrow$ 장기 의존관례를 유지하며 학습
LSTM 구현¶
- Affine transformation
$$xW_x +hW_h+b$$
- $W^f,W^g, W^i,W^o$를 한꺼번에 수행, slice 노드를 통해 affine결과를 균등하게 나누어 꺼냄
In [ ]:
class LSTM:
def __init__(self, Wx, Wh, b):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.cache = None
def forward(self, x, h_prev, c_prev):
Wx, Wh, b = self.params
N, H = h_prev.shape
A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b
#slice
f = A[:, :H]
g = A[:, H: 2*H]
i = A[:, 2*H: 3*H]
o = A[:, 3*H:]
f = sigmoid(f)
g = np.tanh(g)
i = sigmoid(i)
o = sigmoid(o)
c_next = f * c_prev + g * i
h_next = o * np.tanh(c_next)
self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
return h_next, c_next
def backward(self, dh_next, dc_next):
Wx, Wh, b = self.params
x, h_prev, c_prev, i, f, g, o, c_next = self.cache
tanh_c_next = np.tanh(c_next)
ds = dc_next + (dh_next * o) * (1- tanh_c_next**2) # y= tanh(x) 미분은 (1-y^2)
dc_prev = ds * f
di = ds * g
df = ds * c_prev
do = dh_next * tanh_c_next
dg = ds * i
di *= i * (1-i)
df *= f * (1-f)
do *= o * (1-o)
dg *= g * (1-g)
dA = np.hstack((df, dg, di, do))
dWh = np.dot(h_prev.T, dA)
dWx = np.dot(x.T, dA)
db = dA.sum(axis=0)
dx = np.dot(dA, Wx.T)
dh_prev = np.dot(dA, Wh.T)
return dx, dh_prev, dc_prev
In [ ]:
class TimeLSTM:
def __init__(self, Wx, Wh, b, stateful=False):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.layers = None
self.h, self.c = None, None
self.dh = None
self.stateful = stateful
def forward(self, xs):
Wx, Wh, b = self.params
N, T, D = xs.shape
H = Wh.shape[0]
self.layers = []
hs = np.empty((N, T, H), dtype='f')
if not self.stateful or self.h is None:
self.h = np.zeros((N, H), dtype='f')
if not self.stateful or self.c is None:
self.c = np.zeros((N, H), dtype ='f')
for t in range(T):
layers = LSTM(*self.params)
self.h, self.c = layers.forward(xs[:, t, :], self.h, self.c)
hs[:, t, :] = self.h
self.layers.append(layer)
return hs
def backward(self, dhs):
Wx, Wh, b = self.params
N, T, H = dhs.shape
D = Wx.shape[0]
dxs = np.empty((N, T, D), dtype='f')
dh, dc = 0, 0
grads =[0, 0, 0]
for t in reversed(range(T)):
layers = self.layers[t]
dx, dh dc = layers.backward(dhs[:, t, :]+dh, dc)
dxs[:, t, :] = dx
for i, grad in enumerate(layers.grads):
grads[i] += grad
for i, grad in enumerate(grads):
self.grads[i][...] = grad
self.dh = dh
return dxs
def set_state(self.h, c= None):
self.h, self.c = h, c
def reset_state(self):
self.h, self.c = None, None
RNN 추가개선¶
LSTM계층 다층화 : LSTM 계층을 깊게 쌓아 모델의 표현력을 좋게 한다.¶
정규화: 모델의 복잡도에 페널티를 주는 Normalization으로 과적합 억제¶
Dropout에 의한 과적합 억제¶
RNN은 일반적인 피드 포워드 신경망보다 쉽게 과적합을 일으킴, 훈련시 계층 내의 뉴련 몇개를 무작위로 무시하고 학습시킴. 무작위한 무시가 제약이 되어 신경망의 일반화 성능을 향상 시킴$\rightarrow$ RNN에서 시계열 방향으로 드롭아웃을 시키면 시간이 흐름에 따라 정보가 사라질수 있다. 즉 흐르는 시간에 비례해 드롭아웃 노이즈 축적 $\rightarrow$ 시간과 독립 적으로 깊이 방향으로 삽입
변형 드롭아웃으로 시간방향에도 드롭아웃을 할 수있다. 이 모델은 같은 계층에 속한 드롭아웃들이 mask를 공유하여 마스크가 고정된다. 따라서 일반적이 드롭아웃과 달이 정보가 지수적으로 손실되는 사태를 피할수 있다.
가중치 공유(weight tying)¶
embedding계층과 affine계층이 가중치를 공유하면 학습하는 매개변수가 크게 줄어들며 정확도도 향상된다. 매개변수의 감소는 과적합을 억제하는 혜택으로 이어질 수 있다.
Summary
- 단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.
- 기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM, GRU등)이 효과적이다
- LSTM에는 input gate, forget gate, ouput gate등의 3개의 게이트가 있다.
- 게이트에는 전용 가중치가 있으며 시그모이드 함수를 사용하여 0.0~1.0 사이의 실수를 출력한다.
- 언어모델개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.
- RNN정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.
728x90