목차
👀, 🤷♀️ , 📜
이 아이콘들을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
게이트가 추가된 RNN
RNN 더 알아보러 가기
[RNN의 장점]
은 순환 경로를 포함하며 과거의 정보를 기억 가능.
구조가 단순 하여 구현도 쉬움.
[단점]
하지만 안타깝게도 성능이 좋지 못함.
➡ 그 원인은 시계열 데이터에서 시간적으로 멀리 떨어진, 장기 long term 의존 관계를 잘 학습할 수 없음
요즘에는 RNN 대신 LSTM이나 GRU라는 계층이 주로 쓰임.
실제로 그냥 ‘RNN’이라고 하면 앞 장의 RNN이 아니라 LSTM을 가리키는 경우도 흔함
오히려 오리지널 RNN을 명시적으로 가리킬 때 ‘기본(적인) RNN’이라 하기도 함
LSTM이나 GRU
- ‘게이트 gate ’라는 구조가 더해져 있음
- 시계열 데이터의 장기 의존 관계를 학습 가능.
- 게이트가 추가된 RNN
RNN의 문제점
앞 장에서 설명한 RNN은 시계열 데이터의 장기 의존 관계를 학습하기 어려움
그 원인은 BPTT에서 기울기 소실 혹은 기울기 폭발이 일어나기 때문
RNN
기울기 소실 또는 기울기 폭발
언어 모델은 주어진 단어들을 기초로 다음에 출현할 단어를 예측하는 일을 함.
앞 장에서는 RNN을 사용해 언어 모델을 구현했습니다(RNNLM이라 불렀죠).
- 앞에서도 나왔듯이 “?”에 들어가는 단어는 “Tom”
[RNNLM]
- 현재 맥락에서 “Tom이 방에서 TV를 보고 있음”과 “그 방에 Mary가 들어옴”이란 정보를 기억해둬야 함
-
이런 정보를 RNN 계층의 은닉 상태에 인코딩해 보관해둬야 함
- 여기에서는 정답 레이블로 “Tom”이라는 단어가 주어졌을 때, RNNLM에서 기울기가 어떻게 전파되는 방식 (학습은 BPTT로 수행)
따라서 정답 레이블이 “Tom”이라고 주어진 시점으로부터 과거 방향으로 기울기를 전달.
- 정답 레이블이 “Tom”임을 학습할 때 중요한 것이 바로 RNN 계층의 존재.(빨간색 화살표 필수)
- RNN 계층이 과거 방향으로 ‘의미 있는 기울기’를 전달함으로써 시간 방향의 의존 관계를 학습할 수 있는 것.
이때 기울기는 (원래대로라면) 학습해야 할 의미가 있는 정보가 들어 있음
➡ 그것을 과거로 전달함으로써 장기 의존 관계를 학습.
하지만 만약 이 기울기가 중간에 사그라들면(거의 아무런 정보도 남지 않게 되면) 가중치 매개변수는 전혀 갱신되지 않게 된다.
➡ 즉, 장기 의존 관계를 학습할 수 없게 된다.
안타깝지만, 현재의 단순한 RNN 계층에서는 시간을 거슬러 올라갈수록
- (기울기 소실) 기울기가 계속 작아짐
- (기울기 폭발) 기울기가 계속 커짐
대부분 둘 중 하나의 운명
기울기 소실, 폭발 원인
[기울기 소실 원인]
RNN 계층에서의 시간 방향 기울기 전파에만 주목해보면,
길이가 T인 시계열 데이터를 가정, T번째 정답 레이블로부터 전해지는 기울기의 변화는?
(앞의 문제에 대입하면 T번째 정답 레이블이 “Tom”인 경우에 해당)
이때 시간 방향 기울기에 주목하면 역전파로 전해지는 기울기는 차례로 ‘tanh’, ‘+’, ‘MatMul (행렬 곱)’ 연산을 통과
- ‘+’의 역전파: 상류에서 전해지는 기울기를 그대로 하류로 흘려보냄 -> 기울기 불변
- ‘tanh’[태그, 링크 삽입] 미분: \(∂y/ ∂x = = 1− y^2\)
이때 y = tanh (x )의 값과 그 미분 값을 각각 그래프로 그리면,
- 미분값은 1.0 이하이고, x가 0으 로부터 멀어질수록 작아짐
- 달리 말하면, 역전파에서는 기울기가 tanh 노드를 지날 때마다 값은 계속 작아짐
- tanh 함수를 T번 통과하면 기울기도 T번 반복해서 작아짐.
📜 RNN 계층의 활성화 함수
RNN 계층의 활성화 함수로는 주로 tanh 함수를 사용하는데, 이를 ReLU로 바꾸면 기울기 소실을 줄일 수 있음
- ReLU는 입력 x가 0 이상이면, 역전파 시 상류의 기울기를 그대로 하류에 흘려보내기 때문
- 즉, 기울기가 작아지지 않음
[기울기 폭발 원인]
MatMul (행렬 곱) 미분: 이야기를 단순하게 하기 위해 tanh 노드를 무시하면
상류로부터 dh라는 기울기가 흘러온다고 가정.
이때 MatMul 노드에서의 역전파는 \((dhW_h)^T\)
- 이 행렬 곱셈에서는 매번 똑같은 가중치인 \(W_h\)가 사용
👀 기울기의 크기 변화 관찰 코드 보기
import numpy as np
import matplotlib.pyplot as plt
N = 2 # 미니배치 크기
H = 3 # 은닉 상태 벡터의 차원 수
T = 20 # 시계열 데이터의 길이
# dh를 np.ones ( )로 초기화
# (np.ones ( )는 모든 원소가 1인 행렬을 반환).
dh = np.ones((N, H))
np.random.seed(3) # 재현할 수 있도록 난수의 시드 고정
Wh = np.random.randn(H, H)
norm_list = []
# 역전파의 MatMul 노드 수(T )만큼 dh를 갱신
for t in range(T):
dh = np.matmul(dh, Wh.T)
norm = np.sqrt(np.sum(dh**2)) / N
#각 단계에서 dh 크기(노름) 를 norm_list에 추가
norm_list.append(norm)
- 미니배치(N개)의 평균 ‘L2 노름’을 구해 dh 크기로 사용.
- L2 노름: 각각의 원소를 제곱해 모두 더하고 제곱근을 취한 값
그림 밑의 기울기 dh는 시간 크기에 비례하여 지수적으로 증가
- 기울기 폭발 exploding gradients
- 기울기 폭발이 일어나면 결국 오버플로를 일으켜 NaN Not a Number 같은 값을 발생
- 따라서 신경망 학습을 제대로 수행할 수 없게 됨.
(그러면 Wh의 초깃값을 다음과 같이 변경한 후 두 번째 실험)
# Wh = np.random.randn(H, H) # 변경 전
Wh = np.random.randn(H, H) * 0.5 # 변경 후
이번에는 기울기가 지수적으로 감소.
- 이것이 기울기 소실 vanishing gradients
- 기울기 소실이 일어나면 기울기가 매우 빠르게 작아짐
그리고 기울기가 일정 수준 이하로 작아지면 가중치 매개변수가 더 이상 갱신X
장기 의존 관계를 학습할 수 없게 됩니다.
[기울기 폭발, 소실의 이유]
행렬 Wh를 T번 반복해서 ‘곱’ 했기 때문
만약 Wh가 스칼라라면 이야기는 단순해지는데
- Wh가 1보다 크면 지수적으로 증가하고,
- 1보다 작으면 지수 적으로 감소합니다.
그럼 Wh가 스칼라가 아니라 행렬
- 이 경우, 행렬의 특잇값’이 척도가 된다
📜 행렬의 특잇값이란
간단히 말하면 데이터가 얼마나 퍼져 있는지를 나타냄
이 특잇값의 값(더 정확하게는 여러 특잇값 중 최댓값)이 1보다 큰지 여부를 보면 기울기 크기가 어떻게 변할지 예측 가능
WARNING
특잇값의 최댓값이 1보다 크면 지수적으로 증가하고, 1보다 작으면 지수적으로 감소할 가능 성이 높다고 예측.
그럴 가능성이 높을 뿐, 특잇값이 1보다 크다고 해서 기울기가 반드시 폭발하는 것은 아님.
즉, 필요조건일 뿐 충분조건은 아님.
기울기 폭발 대책
기울기 클리핑 gradients clipping
기울기 폭발의 대책으로는 전통적인 기법
기울기 클리핑은 매우 단순
알고리즘을 의사 코드로 쓰면 다음과 같음
- \(ĝ\): 여기에서는 신경망에서 사용되는 모든 매개변수에 대한 기울기를 하나로 처리한다고 가정, 이 기울기
- threshold: 문턱값 설정.
이때 기울기의 L2 노름(수식에서는 \(∥ĝ∥\))이 문턱값을 초과하면 두 번째 줄의 수식과 같이 기울기를 수정
WARNING
\(ĝ\): 신경망에서 사용되는 모든 매개변수의 기울기를 하나로 모은 것.
ex) 두 개의 가중치 W1과 W2 매개변수를 사용하는 모델이 있다면, 이 두 매개변수에 대한 기울기 dW1과 dW2를 결합한 것이 \(ĝ\)
[기울기 클리핑을 파이썬으로 구현]
기울기 클리핑은 clip_grads(grads, max_norm )
이라는 함수로 구현.
grads
: 기울기의 리스트max_norm
: 문턱값을 뜻함.
👀코드 보기
import numpy as np
dW1 = np.random.rand(3, 3) * 10
dW2 = np.random.rand(3, 3) * 10
grads = [dW1, dW2]
max _norm = 5.0
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
RNNLM을 학습시키는 RnnlmTrainer 클래스를 제공.
이 클래스 내부에서도 이 기울기 클리핑 함수를 이용해 기울기 폭발을 방지.
RnnlmTrainer 클래스에서의 기울기 클리핑에 대해서는 ‘LSTM을 사용한 언어 모델’에서 추후 설명
기울기 소실과 LSTM
RNN 학습에서는 기울기 소실도 큰 문제.
그리고 이 문제를 해결하려면 RNN 계층의 아키텍처를 근본부터 뜯어고쳐야 함.
여기서 등장하는 것이, 이번 장의 핵심 주제인 ‘게이트가 추가된 RNN’
게이트가 추가된 RNN으로는 많은 아키텍처(신경망 구성)가 제안되어 있음
그 대표격으로 LSTM과 GRU.
[LSTM의 의미]
LSTM은 Long Short-Term Memory를 줄인 이름.
이 용어는 단기 기억 short-term memory 을 긴 long 시간 지속할 수 있음을 의미
LSTM의 인터페이스
[사전 준비]
앞으로를 위해 계산 그래프를 단순화하는 도법을 하나 도입
행렬 계산 등을 하나의 직사 각형 노드로 정리해 그리는 방식
\(tanh\)라는 직사각형 노드 하나
\(tanh(h_{t-1}W_h + x_t* W_x +b )\) 계산 의미
- \(h_t-1\) 과 \(x_t\) 는 행벡터
- 이 직사각형 노드 안에 행렬 곱과 편향의 합, 그리고 tanh 함수에 의한 변환이 모두 포함된 것
[구축 시작]
LSTM의 인터페이스(입출력)를 RNN과 비교하는 것부터 시작
LSTM 계층의 인터페이스에는 c라는 경로가 있다는 차이 존재
c
- 기억 셀 memory cell (혹은 단순히‘셀’)
- LSTM 전용의 기억 메커니즘
- 데이터를 자기 자신으로만(LSTM 계층 내에서만) 주고받는다는 것
- 즉, LSTM 계층 내에서만 완결되고, 다른 계층으로는 출력 안함
상태 h
- RNN 계층과 마찬가지로 다른 계층으로(위쪽으로) 출력
NOTE LSTM의 출력을 받는 쪽에서 보면 LSTM의 출력은 은닉 상태 벡터 h뿐
기억 셀 c는 외부 에서는 안보임 ➡ 그래서 그 존재 자체를 생각할 필요가 없음
[LSTM 계층 조립]
LSTM에는 기억 셀 \(c_t\)
- 시각 t에서의 LSTM의 기억이 저장돼 있음
- 과거로부터 시각 t까지에 필요한 모든 정보가 저장돼 있다고 가정(혹은 그렇게 되도록 학습을 수행함)
- 그리고 필요한 정보를 모두 간직한 이 기억을 바탕으로, 외부 계층에(그리고 다음 시각의 LSTM에) 은닉 상태 \(h_t\) 를 출력
- 이때 출력하는 \(h_t\)는 기억 셀의 값을 tanh 함수로 변환한 값
- \(c_t\)은 tanh 함수(곱셈)을 거치지 않음 ➡ 기울기 소실, 폭발 위험 없음
현재의 기억 셀 \(c_t\) 는 3개의 입력\((c_t-1 , h_t-1 , x_t)\)으로부터 ‘어떤 계산’을 수행하여 구할 수 있음
핵심) 갱신된 \(c_t\) 를사용해 은닉 상태 \(h_t\)를 계산
또한 이 계산은 \(h_t = tanh (c_t)\)인데, 이는 \(c_t\) 의 각요소에 tanh 함수를 적용한다는 뜻
WARNING
기억 셀 \(c_t\)와 은닉 상태 \(h_t\)의 관계는 \(c_t\)의 각 원소에 tanh 함수를 적용한 것일 뿐.
➡ 이 말이 뜻하는 바는 기억 셀 \(c_t\)와 은닉 상태 \(h_t\)의 원소 수는 같다는 것.
output 게이트
‘게이트’기능
- 게이트 우리말로는 ‘문’을 의미하는 단어.
- 문은 열거나 닫을 수 있듯이, 게이트는 데이터의 흐름을 제어
[바로 앞 내용]
은닉 상태 \(h_t\) 는 기억 셀 \(c_t\) 에 단순히 tanh 함수를 적용했을 뿐
[이번 내용]
이번 절에서는** \(tanh(c_t)\)에 게이트를 적용**
- 즉, \(tanh (c_t)\)의 각 원소에 대해 ‘그것이 다음 시각의 은닉 상태에 얼마나 중요한가’를 조정.
LSTM에서 사용하는 게이트는 ‘열기/닫기’뿐 아니라, 어느 정도 열지를 조절 가능‘어느 정도’를 ‘열림 상태 openness ’라 부름
물이 흐르는 양을 0.0~1.0 범위에서 제어한다.
게이트의 열림 상태는 0.0~1.0 사이의 실수로 나타냄
그 값이 다음으로 흐르는 물의 양을 결정
여기서 중요한 것은 ‘게이트를 얼마나 열까’ 라는 것도 데이터로부터 (자동으로) 학습
📜 게이트의 열림 상태를 제어
게이트는 게이트의 열림 상태를 제어하기 위해서 전용 가중치 매개변수를 이용, 이 가중치 매개변수는 학습 데이터로부터 갱신.
- 게이트의 열림 상태를 구할 때는 시그모이드 함수를 사용
- 시그모이드 함수의 출력이 마침 0.0~1.0 사이의 실수이기 때문
[output 게이트 (출력 게이트)]
이 게이트는 다음 은닉 상태 \(h_t\) 의 출력을 담당하는 게이트이므로 output 게이트라고 부름
구하는 법
output 게이트의 열림 상태(다음 몇 %만 흘려보낼까)는 입력 \(x_t\) 와 이전 상태 \(h_{t-1}\) 로부터 구함.
이때의 계산.
- o : 게이트, 여기서 사용하는 가중치 매개변수와 편향 에는 output의 첫 글자인 o를 첨자로 추가
- 이후에도 마찬가지로 첨자를 붙여 게이트임을 표시할것임
- **σ( ) ** : 시그모이드 함수
(식 해석)
[1]
입력 \(x_t\) 에는 가중치 \((W_x)^{(0)}\)가,
이전 시각의 은닉 상태 \(h_t-1\) 에는 가중치 \((W_h)^{(0)}\)가 붙어있음 (\(x_t\) 와 \(h_t-1\)은 행벡터)
[2]
그리고 이 행렬들의 곱과 편향 \(b^{(o)}\) 를 모두 더한 다음
[3]
시그모이드 함수를 거쳐 출력 게이트의 출력 o를 구함.
[4]
마지막으로 이 o와 \(tanh(c_t)\)의 원소별 곱을 \(h_t\) 로 출력
이상의 계산을 계산 그래프로 그리면,
output 게이트에서 수행하는 계산을‘σ’로 표기
그리고 σ의 출력을 o라고 하면, \(h_t\) 는 \(o\)와 \(tanh (c_t )\)의 곱으로 계산된다.
- 여기서 말하는 ‘곱’이란 원소별 곱
- 이것을 아다마르 곱 Hadamard product 이라고 함.
📜 아다마르 곱
- 기호로는 ⊙ 으로 나타냄
- \(h_t = o ⊙ tanh(c_t )\) 이를 수행
WARNING
tanh의 출력은 -1.0~1.0의 실수입니다.
이 -1.0~1.0의 수치를 그 안에 인코딩된 ‘정보’의 강약(정도)을 표시한다고 해석할 수 있음.
한편 시그모이드 함수의 출력은 0.0~1.0의 실수이며, 데이터를 얼마만큼 통과시킬지를 정하는 비율을 뜻합니다.
- (주로) 게이트에서는
시그모이드 함수
가 - 실질적인 ‘정보’를 지니는 데이터에는
tanh 함수
가 활성화 함수로 사용됨.
forget 게이트
기억 셀 갱신
우리가 다음에 해야 할 일은 기억 셀에 ‘무엇을 잊을까’를 명확하게 지시하는 것
이런 일도 물론 게이트를 사용해 해결
[forget 게이트 (망각 게이트)]
\(c_(t-1)\) 의 기억 중에서 불필요한 기억을 잊게 해주는 게이트추가
forget 게이트를 LSTM 계층에 추가할 때 계산 그래프,
forget 게이트가 수행하는 일련의 계산을 σ 노드로 표기.
- 이 σ 안에는 forget 게이트 전용의 가중치 매개변수가 있으며,
- 다음 식의 계산을 수행합니다.
- \(f\): forget 게이트의 출력
- 이 \(f\)와 이전 기억 셀인 \(c_t-1\)과의 원소별 곱,
즉 \(c_t = f ⊙ c_(t-1)\) 을 계산하여 \(c_t\) 를 구함.
새로운 기억 셀
⛔ 위 연산의 문제
forget 게이트를 거치면서 이전 시각의 기억 셀로부터 잊어야 할 기억이 삭제됨.
그런데 이 상태로는 기억 셀이 잊는 것밖에 하지 못함.
⭕ solve
그래서 새로 기억해야 할 정보를 기억 셀에 추가해야 함.
그러기 위해서 tanh 노드
를 추가
tanh 노드가 계산한 결과가 이전 시각의 기억 셀 \(c_(t-1)\)에 더해짐.
- 기억 셀에 새로운 ‘정보’가 추가된 것.
- 이 tanh 노드는
‘게이트’가 아니며, 새로운 ‘정보’를 기억 셀에 추가하는 것이 목적 - 따라서 활성화 함수로는
시그모이드 함수가 아닌 tanh 함수 사용
이 tanh 노드에서 수행하는 계산은 다음
- \(g\): 기억 셀에 추가하는 새로운 기억 이는 이전 시각의 기억 셀인 \(c_(t-1)\) 에 더해짐으로써 새로운 기억이 생겨남
input 게이트
마지막으로 g에 게이트를 하나 추가
- 여기에서 새롭게 추가하는 게이트를 input 게이트 (입력 게이트)라고 함.
input 게이트를 추가하면 계산 그래프,
[역할]
input 게이트는 g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지를 판단
새 정보를 무비판적으로 수용하는 게 아니라, 적절히 취사선택하는 것이 이 게이트의 역할
다른 관점에서 보면, input 게이트에 의해 가중된 정보가 새로 추가되는 셈
이때 수행하는 계산
- σ :input 게이트
- i :그 출력
그런 다음 i와 g의 원소별 곱 결과를 기억 셀에 추가.
WARNING
LSTM에는 ‘변종’이 몇 가지 있음.
지금까지 설명한 LSTM이 대표적인 LSTM이지만,
이 외에도 게이트 연결 방법이 (약간) 다른 계층도 볼 수 있음.
LSTM의 기울기 흐름, 구현
[LSTM가 기울기 소실을 없애주는 원리]
기억 셀 c의 역전파에 주목하면 보임.
기억 셀의 역전파 (기억 셀에만 집중하여, 그 역전파의 흐름을 그림)
이때 기억 셀의 역전파에서는 ‘+’와 ‘×’ 노드만을 지남
- ‘+’ 노드: 상류에서 전해지는 기울기를 그대로 흘림(기울기 변화(감소)X)
- ‘×’ 노드: 이 노드는
‘행렬 곱’이 아닌 ‘원소별 곱(아마다르 곱)’을 계산 - ‘행렬 곱’: RNN의 역전파에서의 기울기 소실(또는 기울기 폭발)의 원인
- 반면, 이번 LSTM의 역전파에서는 ‘행렬 곱’이 아닌 ‘원소별 곱’-> 매 시각 다른 게이트 값을 이용해 원소별 곱을 계산
-
이처럼 매번 새로운 게이트 값을 이용하므로 곱셈의 효과가 누적되지 않아 기울기 소실이 일어나지 않음 (혹은 일어나기 어려움)
- ‘×’ 노드의 계산은 forget 게이트가 제어(그리고 매 시각 다른 게이트 값을 출력).
- 그리고 forget 게이트가 ‘잊어야 한다’고 판단한 기억 셀의 원소에 대해서는그 기울기가 작아지는 것.
- 한편, forget 게이트가 ‘잊어서는 안 된다’고 판단한 원소에 대해서는 그 기울기가 약화되지 않은 채로 과거 방향으로 전해짐.
- 따라서 기억 셀의 기울기가 (오래 기억해야 할 정보일 경우) 소실 없이 전파가능
[LSTM 구현]
1) 최초의 한 단계만 처리하는 LSTM 클래스를 구현한 다음,
2) 이어서 T개의 단계를 한꺼번에 처리하는 Time LSTM 클래스를 구현
LSTM 에서 수행하는 계산을 정리한 수식들
- 여기에서의 ‘아핀 변환’이란 행렬 변환과 평행 이동(편향)을 결합한 형태, 즉 \(xW_x +hW_h +b\)형태의 식
위의 네 수식에서는 아핀 변환을 개별적으로 수행하지만, 이를 하나의 식으로 정리해 계산 가능
- 4개의 가중치(또는 편향)를 하나로 모을 수 있음
- 그렇게 하면 원래 개별적으로 총 4번을 수행하던 아핀 변환을 단 1회의 계산으로 연산 가능
- 계산 속도가 빨라짐
- 일반적으로 행렬 라이브러리는 ‘큰 행렬’을 한꺼번에 계산할 때가 각각을
따로 계산할 때보다 빠르기 때문 - 가중치를 한 데로 모아 관리하게 되어 소스 코드도 간결해짐
\(W_x\) , \(W_h\) , \(b\) 각각에 4개분의 가중치(혹은 편향)가 포함되어 있다고 가정하고, 이때의 LSTM을 계산 그래프 그리면, 1) 처음 4개분의 아핀 변환을 한꺼번에 수행.
2) 그리고 slice 노드를 통해 그 4개의 결과를 꺼냄.
- slice: 아핀 변환의 결과(행렬)를 균등하게 네조각으로 나눠서 꺼내주는 단순한 노드.
3) slice 노드 다음에는 활성화 함수(시그모이드 함수 또는 tanh 함수)를 거쳐 앞 절에서 설명한 계산을 수행.
[LSTM 클래스를 구현]
(초기화 구현)
👀 초기화 코드 보기
초기화 인수
- 가중치 매개변수인 \(Wx\)와 \(Wh\)
- 그리고 편향을 뜻하는 \(b\).
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
(코드 설명)
앞에서도 말한 것처럼 이 가중치(와 편향)에는 4개분의 가중치가 담김
1) self.params
이 인수들을 인스턴스 변수인 params에 할당하고,
2) self.grads
이에 대응하는 형태로 기울기도 초기화.
3) self.cache
순전파때 중간 결과를 보관했다가 역전파 계산에 사용하려는 용도의 인스턴스 변수
(순전파 구현)
forward (x, h_prev, c_prev )
메서드로 구현.
👀코드 보기
인수
- x: 현 시각의 입력
- \(h_prev\): 이전 시각의 은닉 상태
- \(c_prev\); 이전 시각의 기억셀
def forward(self, x, h_prev, c_prev):
Wx, Wh, b = self.params
N, H = h_prev.shape
# 아핀 변환
A = np.matmul(x, Wx) + np.matmul(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
👀코드 설명 보기
1️⃣ 가장 먼저 아핀 변환을 함
- N: 미니배치 수
- D: 입력 데이터의 차원 수
- H: 기억 셀과 은닉 상태의 차원 수
A: 계산 결과(네 개분의 아핀 변환 결과가 저장)
- 따라서 이 결과로부터 데이터를 꺼낼 때
A[:, :H ]
나A[:, H:2 * H ]
형태로 슬라이스 해서 꺼냄,- 꺼낸 데이터를 다음 연산 노드에 분배.
WARNING
LSTM 계층은 4개분의 가중치를 하나로 모아서 보관.
그 덕분에 LSTM 계층은 매개변수를 총 3개만 관리하면 된다(\(W_x\) , \(W_h\) , \(b\)).
참고로 RNN 계층도 똑같이 \(W_x\) , \(W_h\) , \(b\), 라는 3개의 매개변수만 사용
따라서 LSTM 계층과 RNN 계층의 매개변수 수는 같지만, 그 형상이 다름
[slice 노드의 역전파]
이 slice 노드는 행렬을 네 조각으로 나눠서 분배함
따라서 그 역전파에서는 반대로 4 개의 기울기 결합 필요.
slice 노드의 역전파에서는 4개의 행렬을 연결
- 4개의 기울기 df, dg, di, do를 연결해서 dA를 만듦.
이는 넘파이의 np.hstack ( )
메서드를 이용
np.hstack ( )
: 인수로 주어진 배열들을 가로로 연결 (세로로 연결은np.vstack ( )
)
따라서 이 처리를 다음의 단 한 줄로 끝낼 수 있음
dA = np.hstack((df, dg, di, do))
[Time LSTM 구현]
Time LSTM은 T개분의 시계열 데이터를 한꺼번에 처리하는 계층
전체 그림은 T개의 LSTM 계층으로 구성됩니다. 그런데 앞서 말한 것처럼 RNN에서는 학습할 때 Truncated BPTT를 수행
- Truncated BPTT는 역전파의 연결은 적당한 길이로 끊지만,
- 순전파의 흐름은 그대로 유지.
그러니 은닉 상태와 기억 셀을 인스턴스 변수로 유지 이렇게 하여 다음번에 forward ( )
가 불렸을 때, 이전 시각의 은닉 상태(와 기억 셀)에서부터 시작 가능
이미 구현한 Time RNN 계층과 비슷하게 구현하면 됨
👀 Time LSTM 계층 구현 코드 보기
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):
layer = LSTM(*self.params)
self.h, self.c = layer.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)):
layer = self.layers[t]
dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc) dxs[:, t, :] = dx
for i, grad in enumerate(layer.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
LSTM은 은닉 상태 h와 함께 기억 셀 c도 이용하지만,
TimeLSTM 클래스의 구현은 TimeRNN 클래스와 흡사.
여기에서도 인수 stateful로 상태를 유지할지를 지정함.
LSTM을 사용한 언어 모델
여기서 구현하는 언어 모델은 앞 장에서 구현한 언어 모델과 거의 같음.
앞 장에서는 Time RNN 계층이 차지하던 부분이 Time LSTM 계층으로 바뀌었는데, 이것이 유일한 차이
[언어 모델의 신경망 구성]
- 왼쪽은 앞 장에서 작성한 Time RNN을 이용한 모델,
- 오른쪽은 이번 장에서 작성 하는 Time LSTM을 이용한 모델
[오른쪽 신경망을 Rnnlm이라는 클래스로 구현]
Rnnlm 클래스는앞 장에서 설명한 SimpleRnnlm 클래스와 거의 같고, 새로운 메서드를 몇 개 더 제공.
👀코드 보기
import sys
sys.path.append('..')
from common.time _layers import
* import pickle
class Rnnlm:
def __init __(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
# 가중치 초기화
embed_W = (rn(V, D) / 100).astype('f')
lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b = np.zeros(4 * H).astype('f')
affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
affine_b = np.zeros(V).astype('f')
# 계층 생성
self.layers = [
TimeEmbedding(embed_W),
TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
TimeAffine(affine_W, affine_b) ]
self.loss_layer = TimeSoftmaxWithLoss()
self.lstm_layer = self.layers[1]
# 모든 가중치와 기울기를 리스트에 모은다 .
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
# 추가된 매서드
def predict(self, xs):
for layer in self.layers:
xs = layer.forward(xs)
return xs
def forward(self, xs, ts):
score = self.predict(xs)
loss = self.loss_layer.forward(score, ts)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
def reset_state(self):
self.lstm_layer.reset _state()
def save_params(self, file_name='Rnnlm.pkl'):
with open(file_name, 'wb') as f:
pickle.dump(self.params, f)
def load_params(self, file_name='Rnnlm.pkl'):
with open(file_name, 'rb') as f:
self.params = pickle.load(f)
(코드 설명)
추가된 매서드
predict ( )
메서드: Rnnlm 클래스에는 Softmax 계층 직전까지를 처리- 메서드는 7장에서 수행하는 문장 생성에 사용된다.
load _params ( )
: 매개변수 읽기를 처리save _params ( )
: 매개변수 쓰기를 처리
나머지는 앞 장의 SimpleRnnlm 클래스와 같음.
NOTE common/base_model.py에는 BaseModel 클래스가 따로 있음
이 클래스에는 save_ params ( )와 load_params ( ) 메서드가 구현되어 있어서, BaseModel 클래스를 상속하는 것만으로 매개변수를 읽고 쓰는 기능을 얻기 가능.
나아가 BaseModel은 GPU 대응이나 비트 감소(16비트의부동소수점 수로 저장) 같은 최적화도 수행
[이 신경망을 사용해 PTB 데이터셋을 학습]
이번에는 PTB 데이터셋의 훈련 데이터 전부를 사용하여 학습
(앞 장에서는 PTB 데이터셋의 일부만 이용).
👀코드 보기
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
from rnnlm import Rnnlm
# 하이퍼파라미터 설정
batch_size = 20
wordvec_size = 100
hidden_size = 100 # RNN 의 은닉 상태 벡터의 원소 수
time_size = 35 # RNN 을 펼치는 크기
lr = 20.0
max_epoch = 4
max_grad = 0.25
# 학습 데이터 읽기
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]
# 모델 생성
model = Rnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)
# ❶ 기울기 클리핑을 적용하여 학습
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad, eval_interval=20)
trainer.plot(ylim=(0, 500))
# ❷ 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print(' 테스트 퍼플렉서티 : ', ppl_test)
# ❸ 매개변수 저장
model.save_params()
👀'기본적인' RNN과의 차이점
이 코드는 앞 장의 코드와 상당 부분이 같음.
앞 장과의 차이를 중심으로 설명
❶ 에서 RnnlmTrainer 클래스를 사용해 모델을 학습시킴
- RnnlmTrainer 클래스의
fit ( )
메서드- 모델의 기울기를 구해 모델의 매개변수를 갱신.
- 이때 인수로
max _grad
를 지정해 기울기 클리핑을 적용 - 참고로 fit ( ) 메서드 내부는 다음처럼 구현(실제 코드가 아닌 의사 코드입니다).
# 기울기 구하기
model.forward(...)
model.backward(...)
params, grads = model.params, model.grads
# 기울기 클리핑
if max _grad is not None:
clip _grads(grads, max _grad)
# 매개변수 갱신
optimizer.update(params, grads)
clip_grads (grads, max_grad )
: 기울기 클리핑을 수행하는 메서드
eval_interval=20
: 20번째 반복마다 퍼플렉서티를 평가하 라는 뜻.- 이번에는 데이터가 크기 때문에
모든 에폭에서 평가하지 않고, 20번 반복될 때마다 평가하도록 함. plot ( )
메서드를 호출: 그 결과를 그래프로 그림
❷ 에서는 학습이 끝난 후 테스트 데이터를 사용해 퍼플렉서티를 평가.
- 이때 모델 상태 (LSTM의 은닉 상태와 기억 셀)를 재설정하여 평가를 수행
eval _perplexity ( )
: 퍼플렉서티 평가에 이용한 메서드 함수- 코드를 보면 구현이 매우 단순함을 확인 가능.
❸ 에서 학습이 완료된 매개변수들을 파일로 저장
- 다음 장에서 학습된 가중치 매개변수를 사용해 문장을 생성할 때 이 파일을 사용할 것임.
[터미널 출력 결과]
(결과 해석)
- iter : 20번째 반복의 퍼플렉서티 값이 출력 됨.
- 첫 번째 퍼플렉서티의 값이 10000.84인데,
- 이는 (직관적으로는) 다음에 나올 수 있는 후보 단어 수를10,000개 정도로 좁혔다는 뜻
- 이번 데이터셋의 어휘 수가 10,000개이므로 아직 아무 것도 학습하지 않은 상태라는 말임
- 그래서 예측을 대충 수행하게 됨.
- 하지만 학습을 계속하면서 기대한 대로 퍼플렉서티가 좋아지고 있음
- 실제로 반복이 300회를 넘자 퍼플렉 서티가 400을 밑돌기 시작함
(퍼플렉서티의 추이) 이번 실험에서는 총 4에폭(반복으로 환산하면 1327×4회)의 학습을 수행
- 퍼플렉서티가 순조롭게 낮아져서 최종적으로는 100 정도가 됨
- 테스트 데이터로 수행한 최종 평가(소스 코드의 ❷ 부분)는 약 136.07.
- 이 결과는 실행할 때마다 달라지지만 대략 136 전후가 될 것.
- 즉, 우리 모델은 다음에 나올 단어의 후보를 (총 10,000개 중에서) 136개 정도로 줄일 때까지 개선된 것
[problem]
136이 사실 그다지 좋은 결과는 아님
2017년 기준 최첨단 연구 에서는 PTB 데이터셋의 퍼플렉서티가 60을 밑돌고 있음
현재의 RNNLM을 한층 더 개선 필요
RNNLM 추가 개선
[INTRO]
1) 현재의 RNNLM의 개선 포인트 3가지를 설명
2) 그리고 그 개선들을 구현
3) 마지막에는 실제로 얼마나 좋아졌는지를 평가
LSTM 계층 다층화
RNNLM으로 정확한 모델을 만들고자 한다면,
많은 경우 LSTM 계층을 깊게 쌓아(계층을 여러겹 쌓아) 효과를 볼 수 있음.
지금까지 우리는 LSTM 계층을 1층만 사용했지만 이를 2층, 3층 식으로 여러 겹 쌓으면 언어 모델의 정확도가 향상 기대 가능
- 첫 번째 LSTM 계층의 은닉 상태가 두 번째 LSTM 계층에 입력됨
- 이와 같은 요령으로 LSTM 계층을 몇 층이라도 쌓을 수 있음
그 결과 더 복잡한 패턴을 학습할 수 있게 된다
- 피드포워드 신경망에서 계층을 쌓는 이야기와 같음.
- Affine 계층이나 합성곱 계층 등을 더 깊게 쌓을수록 모델의 표현력이 좋아짐
[그렇다면 몇 층 쌓아야 할까?]
그건 하이퍼파라미터에 관한 문제
쌓는 층 수는 하이퍼파라미터이므로 처리할 문제의 복잡도나 준비된 학습 데이터의 양에 따라 적절하게 결정해야 함.
- 참고로, PTB 데이터셋의 언어 모델에서는 LSTM의 층 수는 2~4 정도일 때좋은 결과를 얻음
📜 구글 번역에서 사용하는 GNMT 모델의 계층 수
LSTM을 8층이나 쌓은 신경망.
즉, 처리할 문제가 복잡하고 학습 데이터를 대량으로 준비할 수 있다면 LSTM 계층을 ‘깊게’ 쌓는 것이 정확도 향상을 이끎
드롭아웃에 의한 과적합 억제
LSTM 계층을 다층화하면 시계열 데이터의 복잡한 의존 관계를 학습할 수 있을 것이라 기대 가능
다르게 표현하자면, 층을 깊게 쌓음으로써 표현력이 풍부한 모델을 만들 수 있음.
[PROBLEM]
이런 모델은 종종 과적합 overfitting, 과대적합을 일으킴
- RNN은 일반적인 피드포워드 신경망보다 쉽게 과적합을 일으킴.
따라서 RNN의 과적합 대책은 중요하고, 현재도 활발하게 연구되는 주제임
📜 과적합이란?
과적합이란 훈련 데이터에만 너무 치중해 학습된 상태를 말합니다. 즉, 일반화 능력이 결여된 상태.
우리가 바라는 것은 일반화 능력이 높은 모델입니다.
이런 모델을 얻으려면 훈련 데이터로 수행한 평가와 검증 데이터로 한 평가를 비교하여, 과적합이 일어나지 않았는지를 판단해가며 모델을 설계해야 함.
[SOLVE]
과적합을 억제하는 전통적인 방법.
1) ‘훈련 데이터의 양 늘리기’
2) ‘모델의 복잡도 줄이기’
그 외
3) 정규화 normalization: 모델의 복잡도에 페널티를 줌
- 예컨대 L2 정규화는 가중치가 너무 커지면 페널티를 부과
4) 드롭아웃 dropout: 훈련 시 계층 내의 뉴런 몇 개(예컨대 50% 등)를 무작위로 무시하고 학습하는 방법(일종의 정규화)
[DROPOUT]
- 드롭아웃은 무작위로 뉴런을 선택하여 선택한 뉴런을 무시
- 무시: 그 앞 계층으로부터의 신호 전달을 막는다는 뜻
- ‘무작위한 무시’가 제약이 되어 신경망의 일반화 성능을 개선
[RNN에서의 드롭아웃 계층의 삽입 위치]
첫번째 후보(나쁜 예)
- LSTM 계층의 시계열 방향으로 삽입하는 것.
- RNN에서 시계열 방향으로 드롭아웃을 넣어버리면 (학습 시) 시간이 흐름에 따라 정보가 사라질 수 있음.
- 흐르는 시간에 비례해 드롭아웃에 의한 노이즈가 축적.
두번째 후보(좋은 예)
드롭아웃 계층을 깊이 방향(상하 방향)으로 삽입
- 이렇게 구성하면 시간 방향(좌우 방향)으로 아무리 진행해도 정보를 잃지 않음
- 드롭아웃이 시간축과는 독립적으로 깊이 방향(상하 방향)에만 영향을 줌
지금까지 이야기한 것처럼(깊이 방향에 적용), ‘일반적인 드롭아웃’은 시간 방향에는 적합하지 않음
[변형 드롭아웃]
깊이 방향은 물론 시간 방향에도 이용할 수 있어서 언어 모델의 정확도를 한층 더 향상시킬 수 있음.
같은 계층에 속한 드롭아웃들은 같은 마스크 mask 를 공유.
- ‘마스크’: 데이터의 ‘통과/차단’을 결정하는 이진 binary 형태의 무작위 패턴
보듯 같은 계층의 드롭아웃끼리 마스크를 공유함으로써 마스크가 ‘고정’됨
그 결과 정보를 잃게 되는 방법도 ‘고정’되므로, 일반적인 드롭아웃 때와 달리 정보가 지수적으로 손실되는 사태를 피할 수 있음
WARNING
변형 드롭아웃은 일반 드롭아웃보다 결과가 좋음
하지만 이번 장에서는 변형 드롭 아웃을 이용하지 않고, 일반적인 드롭아웃을 사용
가중치 공유 weight tying
언어 모델을 개선하는 아주 간단한 트릭
두 계층이 가중치를 공유함으로써,
- 학습하는 매개변수 수가 크게 줄어듦
- 정확도도 향상
그럼, 가중치 공유를 구현 관점에서 생각해보면
- V: 어휘 수
-
H: LSTM의 은닉 상태의 차원 수
- V×H: Embedding 계층의 가중치는 형상
- H×V: Affine 계층의 가중치 형상
이때 가중치 공유를 적용하려면 Embedding 계층의 가중치를 전치하여 Affine 계층의 가중치로 설정하기만 하면 된다.
[가중치 공유가 효과가 있는 이유]
직관적으로는 가중치를 공유함으로써 학습해야할 매개변수 수 줄일 수 있음
- 매개변수 수가 줄어든다 함은 과적합이 억제로 이어짐
- 그 결과 학습하기가 더 쉬워짐
개선된 RNNLM 구현
지금까지 RNNLM의 개선점 3개 배움.
- LSTM계층의 다층화
- 드롭아웃 사용
- 가중치 공유(Embedding 계층과 Affine 계층에서 가중치 공유)
[BetterRnnlm 클래스]
👀코드 보기
import sys
sys.path.append('..')
from common.time _layers import *
from common.np import *
from common.base _model import BaseModel
class BetterRnnlm(BaseModel):
def __init __(self, vocab_size=10000, wordvec_size=650, hidden_size=650, dropout_ratio=0.5):
V, D, H = vocab _size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D) / 100).astype('f') lstm _Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b1 = np.zeros(4 * H).astype('f')
lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b2 = np.zeros(4 * H).astype('f')
affine_b = np.zeros(V).astype('f')
# 세 가지 개선 !
self.layers = [
TimeEmbedding(embed_W),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
TimeDropout(dropout_ratio),
TimeAffine(embed_W.T, affine_b) # 가중치 공유 !!
]
self.loss_layer = TimeSoftmaxWithLoss()
self.lstm_layers = [self.layers[2], self.layers[4]]
self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
def predict(self, xs, train_flg=False):
for layer in self.drop _layers:
layer.train _flg = train _flg
for layer in self.layers:
xs = layer.forward(xs)
return xs
def forward(self, xs, ts, train_flg=True):
score = self.predict(xs, train_flg)
loss = self.loss_layer.forward(score, ts)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
def reset_state(self):
for layer in self.lstm_layers:
layer.reset_state()
세 가지 개선이 이뤄지는 계층 설명
# 세 가지 개선 !
self.layers = [
TimeEmbedding(embed_W),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
TimeDropout(dropout_ratio),
TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
TimeDropout(dropout_ratio),
TimeAffine(embed_W.T, affine_b) # 가중치 공유 !!
]
- TimeLSTM 계층을 2개 겹치고,
- 사이사이에 TimeDropout 계층을 사용
- 그리고 TimeEmbedding 계층과 TimeAffine 계층에서 가중치를 공유
[다음은 이 개선된 BetterRnnlm 클래스를 학습시킬 차례]
기능 더 추가해보기
매 에폭에서 검증 데이터로 퍼플렉서티를 평가하고, 그 값이 나빠졌을 경우에만 학습률을 낮추는 것.
이 기술은 실전에서 자주 쓰이며, 더 좋은 결과로 이어지는 경우가 많음.
👀코드 보기
import sys
sys.path.append('..')
from common import config
# GPU 에서 실행하려면 아래 주석을 해제하세요 ( 쿠파이 필요 ).
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval _perplexity
from dataset import ptb
from better _rnnlm import BetterRnnlm
# 하이퍼파라미터 설정
batch_size = 20
wordvec_size = 650
hidden_size = 650
time_size = 35
lr = 20.0
max_epoch = 40
max_grad = 0.25
dropout = 0.5
# 학습 데이터 읽기
corpus, word_to_id, id_to_word = ptb.load_data('train') corpus_val, _, _ = ptb.load_data('val')
corpus_test, _, _ = ptb.load_data('test')
vocab _size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]
model = BetterRnnlm(vocab_size, wordvec_size, hidden_size, dropout)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)
best_ppl = float('inf')
for epoch in range(max_epoch):
trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size, time_size=time_size, max_grad=max_grad)
model.reset_state()
ppl = eval_perplexity(model, corpus_val)
print(' 검증 퍼플렉서티 : ', ppl)
if best_ppl > ppl:
best_ppl = ppl
model.save_params()
else:
lr /= 4.0
optimizer.lr = lr
model.reset _state()
print('-' * 50)
학습을 진행하면서 매 에폭마다 검증 데이터로 퍼플렉서티를 평가하고,
그 값이 기존 퍼플렉서티(best _ppl )보다 낮으면 학습률을 1/4로 줄임.
이를 위해,
- RnnlmTrainer 클래스의 fit ( ) 메서드를 이용해 1에폭분의 학습을 수행한 다음,
- 검증 데이터로 퍼플렉서티의 평가하는 처리를 for 문에서 반복.
이 학습에는 상당한 시간이 소요됩니다. CPU에서 실행하면 2일 정도가 걸림
이 코드를 실행하면 퍼플렉서티가 순조롭게 낮아짐.
그리고 테스트 데이터로 얻은 최종 퍼플렉서티는 75.76 정도가 될 겁니다(실행할 때마다 결과가 달라집니다).
개선 전 RNNLM의 퍼플렉서티가 약 136이었음을 생각하면 상당히 개선됨.
LSTM의 다층화로 표현력을 높이고, 드롭아웃으로 범용성을 향상시켰으며, 가중치 공유로 가중치를 효율적으로 이용함으로써 이런 큰 폭의 정확도 향상을 달성할 수 있음
정리
● 단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.
● 기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM과 GRU 등)이 효과적이다.
● LSTM에는 input 게이트, forget 게이트, output 게이트 등 3개의 게이트가 있다.
● 게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용하여 0.0~1.0 사이의 실수를 출력한다.
● 언어 모델 개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.
● RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.