앞에서 본 RNN은 장기 의존 관계를 잘 학습하지 못함
- 기울기 소실 or 기울기 폭발
요즘에는 단순한 RNN 대신 LSTM이나 GRU 계층 사용
- 게이트 구조로 장기 의존 관계 학습
게이트가 추가된 RNN을 알아보자!
6.1 RNN의 문제점
6.1.1 RNN 복습
- RNN 계층은 순환 경로 가짐 (= 순환을 펼기면 옆으로 길게 뻗은 신경망)
- 시계열 데이터인 x_t를 입력받아 h_t 출력(RNN 계층의 은닉 상태, 과거 정보 저장)
RNN 계층 순전파
- 행렬의 곱과 합
- 활성화 함수인 tanh 함수에 의한 변환
문제점
- 장기 기억에 취약
6.1.2 기울기 소실 또는 기울기 폭발
- 언어 모델은 주어진 단어들을 기초로 다음에 출현할 단어 예측
- 정보를 RNN 계층의 은닉 상태에 인코딩해 보관해둬야 함
- 기울기는 학습해야 할 의미가 있는 정보를 과거로 전달하여 장기 의존 관계 학습
→ 기울기가 중간에 사그라들면 장기 의존 관계 학습할 수 없음
현재의 문제
- 기울기 소실 & 기울기 폭발
6.1.3 기울기 소실과 기울기 폭발의 원인
RNN 계층에서 기울기 소실(혹은 기울기 폭발)이 일어나는 원인을 알아보자
길이가 T인 시계열 데이터를 가정, 전해지는 기울기 변화
- 시간 방향 기울기에 주목하면, 역전파로 전해지는 기울기는 차례로 tanh, +, MatMul(행렬 곱) 연산 통과
+의 역전파
- 기울기를 그대로 하류로 흘려보냄
- 기울기 변하지 않음
tanh의 역전파
- 점선이 y = tanh(x)의 미분, 값은 1.0 이하이고 x가 0으로부터 멀어질수록 작아짐
- 기울기가 tanh 노드를 지날 때마다 값은 계속 작아짐
- tanh 함수를 T번 통과하면 기울기도 T번 반복해서 작아짐
* NOTE
- RNN 계층의 활성화 함수로는 주로 tanh 함수 사용
- 이를 ReLU로 바꾸면 기울기 소실 줄일 수 있음
→ ReLU는 입력 x가 0 이상이면, 역전파 시 상류의 기울기 그대로 하류로, 기울기 작아지지 않음
MatMul(행렬 곱) 노드의 역전파(tanh를 무시한다 치면)
- 상류로부터 dh라는 기울기 흘러들어온다고 가정
- MatMul 노드에서의 역전파는 dhWb^T라는 행렬 곱으로 기울기 계산
- 같은 계산을 시계열 데이터의 시간 크기만큼 반복
- 행렬 곱셈에서는 매번 똑같은 가중치인 Wh가 사용
기울기 문제
기울기 폭발
- 기울기가 시간에 비례해 지수적으로 증가
- 오버플로를 일으켜 NaN 같은 값을 발생, 신경망 학습 제대로 수행되지 않음
기울기 소실
- 기울기가 시간에 비례해 지수적으로 감소
- 기울기가 일정 수준 이하로 작아지면 가중치 매개변수가 더 이상 갱신되지 않으므로, 장기 의존 관계 학습 불가능
위의 이유
- 행렬 Wh를 T번 반복해서 '곱'했기 때문에
- Wh가 1보다 크면 지수적으로 증가, 1보다 작으면 지수적으로 감소
- Wh가 스칼라가 아니라 행렬이라고 해도, 행렬의 '특잇값'을 척도로 기울기 크기 변화가 예측됨(소실/폭발)
6.1.4 기울기 폭발 대책
기울기 폭발의 대책으로는 전통적인 기법
기울기 클리핑(gradients clipping)
- 매우 단순하지만 많은 경우 잘 작동
- 기호 g : 신경망에서 사용되는 모든 매개변수에 대한 기울기를 하나로 처리한다고 가정
ex) 두 개의 가중치 W1, W2 매개변수를 사용한다면, 기울기 dW1과 dW2를 결합한 것 g - threshold : 문턱값
- 기울기의 L2 노름(수식에서 ||g||)이 문턱값을 초과하면 두 번째 줄의 수식과 같이 기울기 수정
- 기울기 클리핑 함수를 이용해 기울기 폭발 방지
# coding: utf-8
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
print('before:', dW1.flatten())
clip_grads(grads, max_norm)
print('after:', dW1.flatten())
6.2 기울기 소실과 LSTM
기울기 소실을 해결하기 위해 RNN 계층의 아키텍처 근본을 뜯어고쳐야 함
- 게이트가 추가된 RNN : LSTM, GRU
6.2.1 LSTM의 인터페이스
계산 그래프 단순화
- tanh(hW + xW + b) 계산을 tanh라는 직사각형 노드 하나로 그림
LSTM의 인터페이스(입출력)를 RNN과 비교
- LSTM 계층의 인터페이스에는 c라는 경로 있음
* 기억 셀 c
- LSTM 전용의 기억 메커니즘
- 데이터를 자기 자신으로만(LSTM 계층 내에서만) 주고받는다는 것
= LSTM 계층 내에서만 완결, 다른 계츠으로 출력하지 않음 - 상태 h는 RNN 계층과 마찬가지로 다른 계층(위쪽으로) 출력
* NOTE
- LSTM의 출력을 받는 쪽에서 보면, 은닉 상태 벡터 h 뿐
- 기억 셀 c는 외부에서 보이지 않으므로, 존재 생각하지 않아도 됨
6.2.2 LSTM 계층 조립하기
LSTM의 부품을 하나씩 조립
LSTM
- 기억 셀 c_t : 시각 t에서의 LSTM의 기억이 저장, 과거로부터 시각 t까지에 필요한 모든 정보가 저장됐다고 가정
- 이 기억을 바탕으로, 외부 계층에(그리고 다음 시각의 LSTM에) 은닉 상태 h_t 출력
- h_t : [그림 6-12]와 같이 기억 셀의 값을 tanh 함수로 변환
핵심
- 갱신된 c_t를 사용해 은닉 상태 h_t 계산
- h_t = tanh(c_t) → c_t의 각 요소에 tanh 함수를 적용한다는 뜻
* WARNING
- 기억 셀 c_t와 은닉 상태 h_t의 원소 수는 같음
게이트
- 문을 의미, 데이터의 흐름 제어
- '열기/닫기' 뿐만 아니라, '어느 정도' 열지 조절
- '어느 정도' = 열림 상태 - 열림 상태는 0.0 ~ 1.0 사이의 실수
- 중요 : '게이트를 얼마나 열까'라는 것도 데이터로부터 (자동으로) 학습
* NOTE
- 게이트의 열림 상태를 제어하기 위해 전용 가중치 매개변수 이용
- 학습 데이터로부터 가중치 갱신
- 열림 상태를 구할 때 시그모이드 함수 사용 → 출력이 0.0 ~ 1.0 사이의 실수이므로
6.2.3 output 게이트
tanh(c_t) 게이트 적용
- tanh(c_t)의 각 원소에 대해 '그것이 다음 시각의 은닉 상태에 얼마나 중요한가'를 조정
- 이 게이트는 다음 은닉 상태 h_t의 출력을 담당하는 게이트 → output 게이트(출력 게이트)
- output 게이트의 열림 상태(다음 몇 %만 흘려보낼까)는 입력 x_t와 이전 상태 h_t-1로부터 구함
- 가중치 매개변수와 편향에는 output의 첫 글자 o를 첨자로 추가
- 시그모이드 함수는 σ()로 표기
- 입력 x_t에는 가중치 Wx, 이전 시각의 은닉 상태 h_t-1에는 가중치 Wh 가 붙어 있음
- 행렬들의 곱과 편향 b를 모두 더한 후, 시그모이드 함수를 거쳐 출력 게이트의 출력 o 구함
- o와 tanh(c_t)의 원소별 곱을 h_t로 출력
- output 게이트에서 수행하는 [식 6.1]의 계산 σ 로 표기
- σ 출력 o라고 하면, h_t 는 o와 tanh(c_t)의 곱으로 계산
* 이 곱은 아다마르 곱(원소별 곱)
→ LSTM의 output 게이트
* WARNING
- 게이트에서는 시그모이드 함수가 활성화 함수 → 출력 0.0, ~ 1.0 (통과 비율 의미)
- 실질적인 '정보'를 지니는 데이터에는 tanh 함수가 활성화 함수 → 출력 -1.0 ~ 1.0 (인코딩된 '정보'의 강약)
6.2.4 forget 게이트
망각은 더 나은 전진
기억 셀에 '무엇을 잊을까'를 명확하게 지시 → 게이트 사용
forget 게이트(망각 게이트)
- c_t-1의 기억 중에서 불필요한 기억을 잊게 해주는 게이트
- forget 게이트가 수행하는 일련의 계산 σ 노드로 표시
- σ 안에 forget 게이트 전용의 가중치 매개변수 있음 & 다음의 계산 수행
- 이를 통해 forget 게이트의 출력 f가 구해짐
- 이 f와 이전 기억 셀 c_t-1과의 원소별 곱, c_t = f⊙c_t-1을 계산하여 c_t 구함
6.2.5 새로운 기억 셀
새로 기억해야 할 정보를 기억셀에 추가
- forget 게이트를 거치며 잊어야 할 기억 삭제됨
- [그림 6-17]과 같이 tanh 노드 추가
- tanh 노드가 계산한 결과가 이전 시각의 기억 셀 c_t-1에 더해짐
- tanh 노드는 '게이트'가 아니며, 새로운 '정보'를 기억 셀에 추가하는 것이 목적
- 활성화 함수로 tanh 함수 사용(시그모이드 x)
- tanh 노드에서 [식 6.4] 계산 수행
- 기억 셀에 추가하는 새로운 기억 g로 표기
- g가 이전 시각의 기억 셀 c_t-1에 더해지며 새로운 기억 생겨남
6.2.6 input 게이트
- g에 게이트 하나 추가 → input 게이트(입력 게이트)
- input 게이트는 g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지 판단
- 새 정보를 무비판적으로 수용 x, 적절히 취사선택하는 것이 게이트의 역할
- input 게이트에 의해 가중된 정보가 새로 추가
- input 게이트를 σ로, 그 출력 i로 표기
- i와 g의 원소별 곱 결과를 기억 셀에 추가
6.2.7 LSTM의 기울기 흐름
- LSTM은 어떤 원리로 기울기 소실을 없애주는 것일까? → 기억 셀 c의 역전파에 주목
- 기억 셀의 역전파에서는 +와 × 노드만을 지나게 됨
+ 노드
- 상류에서 전해지는 기울기 그대로 흘림
→ 기울기 변화(감소) x
× 노드
- '행렬 곱'이 아닌 '원소별 곱(아마다르 곱)'을 계산
→ 앞서 RNN의 역전파는 똑같은 가중치 행렬을 사용하여 '행렬 곱' 반복 & 기울기 소실(기울기 폭발)
→ LSTM의 역전파에서는 '원소별 곱', 매 시각 다른 게이트 값을 이용해 원소별 곱 계산, 곱셈 효과 누적되지 않아 기울기 소실 x
× 노드의 계산은 forget 게이트가 제어
- '잊어야 한다' 판단 : 기울기 작아짐
- '잊어서는 안됨' 판단 : 기울기 약화되지 않은 채 과거 방향으로 전해짐
→ 소실 없이 기울기 전파
- 이러한 LSTM의 기억 셀을 통해, 장기 의존 관계를 유지(학습)하리라 기대할 수 있음
6.3 LSTM 구현
- 최초 한 단계만 처리하는 LSTM 클래스 구현
- T개의 단계 한꺼번에 처리하는 Time LSTM 클래스 구현
LSTM에서 수행하는 계산 수식
- [식 6.6]의 네 수식에 포함된 아핀 변환(affine transformation)
* 아핀 변환 : 행렬 변환과 평행 이동(편향)을 결합한 형태, xW + hW + b 형태의 식 - 이를 하나의 식으로 정리해 계산할 수 있음 [그림 6-20]
- 위와 같이 총 4번을 수행하던 아핀 변환을 단 1회의 계산으로 끝마칠 수 있음 = 계산 속도 빨라짐
(행렬 라이브러리는 '큰 행렬' 한꺼번에 계산할 때가 각각 계산보다 빠름, 가중치 한데로 모아 관리하여 소스코드 간결)
LSTM 계산 그래프 그림
(Wx, Wh, b 각각에 4개분의 가중치(혹은 편향)가 포함되어 있다고 가정)
- 처음 4개분의 아핀 변환 한꺼번에 수행
- slice 노드를 통해 그 4개의 결과 꺼냄
* slice : 아핀 변환의 결과(행렬)를 균등하게 네 조각으로 나눠서 꺼내주는 단순한 노드 - 이후 활성화 함수(시그모이드 or tanh)를 거쳐 앞 절에서 설명한 계산 수행
LSTM 클래스 구현
초기화 코드
class LSTM:
def __init__(self, Wx, Wh, b):
'''
Parameters
----------
Wx: 입력 x에 대한 가중치 매개변수(4개분의 가중치가 담겨 있음)
Wh: 은닉 상태 h에 대한 가장추 매개변수(4개분의 가중치가 담겨 있음)
b: 편향(4개분의 편향이 담겨 있음)
'''
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.cache = None
- cache : 순전파 때 중간 결과를 보관했다가 역전파 계산에 사용하려는 용도의 인스턴스 변수
순전파 구현
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
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
- 가장 먼저 아핀 변환
- 인스턴스 변수 Wx, Wh, b에는 각각 4개분의 매개변수 저장
- 행렬 형상과 함께 그림
- 미니배치 수 N
- 입력 데이터의 차원 수 D
- 기억 셀과 은닉 상태의 차원수 H
- 계산 결과인 A에 네 개분의 아핀 변환 결과 저장
- 데이터 꺼낼 때 A[:, :H]나 A[:, H:2*] 형태로 슬라이스
- 꺼낸 데이터를 다음 연산 노드에 분배
* WARNING
- LSTM 계층은 4개분 가중치 하나로 모아서 보관
→ Wx, Wh, b 3개의 매개변수만 관리하면 됨
LSTM의 역전파
slice 노드 역전파
- 순전파 : 행렬을 네 조각으로 나눠서 분배
- 역전파 : 4개의 기울기를 결합 [그림 6-23]
- slice 노드의 역전파에서 4개의 행렬을 연결 → 4개의 기울기 df, dg, di, do 연결해서 dA 만듦
6.3.1 Time LSTM 구현
T개분의 시계열 데이터를 한꺼번에 처리
- T개의 LSTM 계층으로 구성
- RNN에서 학습할 때 Truncated BPTT 수행
- Truncated BPTT는 역전파의 연결은 적당한 길이로 끊음, 순전파의 흐름은 그대로 유지
- 그러니, 은닉 상태와 기억 셀을 인스턴스 변수로 유지 [그림 6-25]
→ 다음 forward()가 불렸을 때, 이전 시각의 은닉 상태(와 기억 셀)에서부터 시작
6.4 LSTM을 사용한 언어 모델
본래 주제인 '언어 모델' 구현
- 앞 언어 모델과 차이 : LSTM 사용
- 오른쪽 신경망을 Rnnlm 클래스로 구현
# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from common.base_model import BaseModel
class Rnnlm(BaseModel):
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()
predict() 메서드 추가 : Softmax 계층 직전까지 처리
load_params(), save_params() 메서드 : 매개변수 읽기/쓰기를 처리
PTB 데이터셋 학습 코드
# coding: utf-8
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)
# 1. 기울기 클리핑을 적용하여 학습
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad,
eval_interval=20)
trainer.plot(ylim=(0, 500))
# 2. 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)
# 3. 매개변수 저장
model.save_params()
1. RnnlmTrainer 클래스를 사용해 모델 학습
- RnnlmTrainer 클래스의 fit() 메서드 : 모델의 기울기를 구해 모델의 매개변수 갱신
- 인수 max_grad : 기울기 클리핑 적용 (6.1.4 기울기 폭발 대책)
- eval_interval=20 : 20번째 반복될 때마다 평가
- plot() 메서드 호출하여 결과 그래프
2. 학습이 끝난 후 테스트 데이터를 사용해 퍼플렉서티 평가
- 모델 상태(LSTM의 은닉 상태와 기억 셀)를 재설정하여 평가를 수행
3. 학습이 완료된 매개변수들을 파일로 저장
- 학습된 가중치 매개변수를 사용해 문장을 생성할 때 이 파일 사용
- 매 20번째 반복의 퍼플렉서티 값 출력
- 첫 퍼플렉서티 값 10000.84 = 다음에 나올 수 있는 후보 단어 수 10,000개 정도로 좁힘
- 학습을 계속하며 퍼플렉서티 좋아짐
- 현 코드에서 퍼플렉서티 136 정도 (17년 기준 60 밑돎)
6.5 RNNLM 추가 개선
6.5.1 LSTM 계층 다층화
RNNLM으로 정확한 모델 만들기
- LSTM 계층깊게 쌓아(계층을 여러 겹 쌓아) 효과
- LSTM 계층을 몇 층이라도 쌓아, 더 복잡한 패턴 학습 가능
= 피트포워드 신경망에서 계층 쌓기
몇 층 쌓아야 할까?
- = 하이퍼파라미터 관련 문제
- 처리할 문제의 복잡도나 준비된 학습 데이터의 양에 따라 적절하게 결정
* NOTE
- 구글 번역 GNMT 모델은 LSTM 8층이나 쌓은 신경망
- 처리할 문제가 복잡하고 학습 데이터를 대량으로 준비할 수 있다면 LSTM 계층을 '깊게' 쌒는 것이 정확도 향상 이끔
6.5.2 드롭아웃에 의한 과적합 억제
LSTM 계층을 다층화하면, 종종 과적합(overfitting) 일으킴
RNN은 일반 피드포워드 신경망보다 쉽게 과적합
* NOTE : 과적합
- 훈련 데이터에만 너무 치중해 학습된 상태
- 일반화 능력이 높은 모델 얻으려면, 훈련 데이터로 수행한 평가와 검증 데이터로 한 평가 비교하여, 과적합 일어나지 않았는지 판단하며 설계
과적합 억제하는 방법
- 전통적 : '훈련 데이터의 양 늘리기', '모델의 복잡도 줄이기'
- '정규화' : 모델의 복잡도에 패널티 부여
- '드롭아웃'
: 훈련 시 계층 내의 뉴런 몇 개(예컨대 50%)를 무작위로 무시하고 학습
= 그 앞 계층으로부터 신호 전달 막음, 신경망의 일반화 성능 개선
RNN 모델에는 드롭아웃 계층 어디에 삽입해야 할까?
1. LSTM 계층에 드롭아웃 계층을 시계열 방향 삽입 (나쁜 예)
- (학습 시) 시간이 프름에 따라 정보가 사라질 수 있음
= 흐르는 시간에 비례해 드롭아웃에 의한 노이즈 축적
2. LSTM 계층에 드롭아웃 계층을 깊이 방향(상하 방향) 삽입(좋은 예)
- 시간 방향(좌우 방향)으로 아무리 진행해도 정보 잃지 않음
- 드롭아웃이 시간축과는 독립적으로 깊이 방향(상하 방향)에만 영향
* NOTE
- 피드포워드 신경망 드롭아웃도 '깊이 방향'으로 적용
- 동일한 방식으로 RNN에도 드롭아웃을 '깊이 방향'으로 적용하여 과적합 억제
변형 드롭아웃(Variational Dropout)
- 깊이 방향, 시간 방향 모두 이용 가능
- 정확도 한층 향상
- 같은 계층에 속한 드롭아웃들은 같은 마스트(mask) 공유
* 마스크 : 데이터의 '통과/차단' 결정하는 이진 형태의 무작위 패턴
- 드롭아웃끼리 마스크 공유 = 마스크 '고정'
→ 정보를 잃게 되는 방법도 '고정' = 일반적 드롭아웃과 달리 정보가 지수적으로 손실되는 것 피함
* WARNING
- 변형 드롭아웃은 일반 드롭아웃보다 결과 좋음
6.5.3 가중치 공유
가중치 공유(weight tying)
- 언어 모델을 개선하는 아주 간단한 트릭
- Embedding 계층의 가중치와 Affine 계층의 가중치를 연결하는(공유하는) 기법
- 두 계층이 가중치를 공유함으로써 학습하는 매개변수 수가 크게 줆 & 정확도 향상
- Embedding 계층의 가중치 형상 [V × H], Affine 계층의 가중치 형상 [H × V]
(어휘 수 V, LSTM의 은닉 상태 차원 수 H)
→ 가중치 공유를 적용 : Embedding 계층의 가중치를 전치하여 Affine 계층의 가중치로 설정
* NOTE : 가중치 공유 효과의 이유
- 가중치를 공유함으로써 학습해야 할 매개변수 수를 줄일 수 있고, 그 결과 학습하기 더 쉬워지므로
- 매개변수 수가 줄어든다 함은 과적합이 억제되는 혜택으로 이어짐
6.5.4 개선된 RNNLM 구현
RNNLM의 개선점 3개 설명, BetterRnnlm 클래스로 구현
개선점 세 가지
- LSTM 계층의 다층화(여기에서는 2층)
- 드롭아웃 사용(깊이 방향으로만 적용)
- 가중치 공유(Emedding 계층과 Affine 계층에서 가중치 공유)
BetterRnnlm 클래스 구현
# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from common.np import * # import numpy as np
from common.base_model import BaseModel
class BetterRnnlm(BaseModel):
'''
LSTM 계층을 2개 사용하고 각 층에 드롭아웃을 적용한 모델이다.
아래 [1]에서 제안한 모델을 기초로 하였고, [2]와 [3]의 가중치 공유(weight tying)를 적용했다.
[1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
[2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
[3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
'''
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()
- TimeLSTM 계층 2개 겹치고, 사이사이 TimeDropout 계층 사용
- TimeEmbedding 계층과 TimeAffine 계층에서 가중치 공유
추가 개선
- 매 에폭에서 검증 데이터로 퍼플렉서티 평가, 그 값이 나빠졌을 경우에만 학습률 낮춤
- 실전에서 자주 사용, 더 좋은 결과로 이어짐
# coding: utf-8
import sys
sys.path.append('..')
from common import config
# GPU에서 실행하려면 아래 주석을 해제하세요(CuPy 필요).
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity, to_gpu
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')
if config.GPU:
corpus = to_gpu(corpus)
corpus_val = to_gpu(corpus_val)
corpus_test = to_gpu(corpus_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)
# 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)
- 학습하며, 매 에폭마다 검증 데이터로 퍼플렉서티 평가
- 그 값이 기존 퍼플렉서티(best_ppl)보다 낮으면 학습률 1/4로 줄임
- RnnlmTrainer 클래스의 fit() 메서드를 이용해 1에폭분의 학습 수행
- 검증 데이터로 퍼플렉서티의 평가하는 처리 for문 반복
- 이 코드를 통해 퍼플렉서티가 순조롭게 낮아짐 → 100대에서 75.76 정도까지 개선
정확도 향상을 위한 기법
- LSTM의 다층화 - 표현력 높임
- 드롭아웃 - 범용성 향상
- 가중치 공유 - 가중치 효율적으로 이용
=> 큰 폭의 정확도 향상
6.5.5 첨단 연구로
첨단 연구들은 훨씬 앞섬
- 새로운 기법이 나타나며 퍼플렉서티도 점차 내려감
(요즘에는...? 찾아보기!) - 한층 더 세련된 최적화나 정규화 기법 적용 & 하이퍼파라미터 튜닝 정밀화
6.6 정리
- 단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.
- 기울기 폭발에서는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM과 GRU 등)이 효과적이다.
- LSTM에는 input 게이트, forget 게이트, output 게이트 등 3개의 게이트가 있다.
- 게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용하여 0.0 ~ 1.0 사이의 실수를 출력한다.
- 언어 모델 개선에는 LSTM 계층 다층과, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.
- RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.
'AI > NLP 기초' 카테고리의 다른 글
[NLP 기초] 8. 어텐션 (0) | 2024.02.23 |
---|---|
[NLP 기초] 7. RNN을 사용한 문장 생성 (1) | 2024.02.22 |
[NLP 기초] 5. 순환 신경망(RNN) (0) | 2024.02.19 |
[NLP 기초] 4. word2vec 속도 개선 (1) | 2024.02.14 |
[NLP 기초] 3. word2vec (1) | 2024.02.13 |