밑바닥부터 시작하는 딥러닝 - 게이트가 추가된 RNN(4)

LSTM 계층 다층화

RNNLM으로 정확한 모델을 만들고자 한다면 많은 경우 LSTM 계층을 깊게 쌓아 효과를 볼 수 있다.

4

LSTM을 2층, 3층 식으로 여러 겹 쌓으면 언어 모델의 정확도가 향상될 수 있다. LSTM을 2층으로 쌓아 RNNLM을 만든다고 하면 위의 그림처럼 된다. 첫 번째 LSTM 계층의 은닉 상태가 두 번째 LSTM 계층에 입력된다. 이와 같은 요령으로 LSTM 계층을 몇 층이라도 쌓을 수 있으며, 그 결과 더 복잡한 패턴을 학습할 수 있게 된다. 피드포워드 신경망에서 계층을 쌓는 것이다. 몇 층이나 쌓는 거는 하이퍼파라미터에 관한 문제이다. 쌓는 층 수는 하이퍼파라미터이므로 처리할 문제의 복잡도나 준비된 학습 데이터의 양에 따라 적절하게 결정해야 한다.


드롭아웃에 의한 과적합 억제

LSTM 계층을 다층화하면 시계열 데이터의 복잡한 의존 관계를 학습할 수 있을 것이라 기대한다. 그러나 이런 모델은 종종 과적합을 일으킨다.

더보기

과적합이란 훈련 데이터에만 너무 치중해 학습된 상태를 말한다. 즉, 일반화 능력이 결여된 상태이다. 일반화 능력이 높은 모델을 얻으려면 훈련 데이터로 수행한 평가와 검증 데이터로 한 평가를 비교하여, 과적합이 일어나지 않았는지를 판단해가며 모델을 설계해야 한다.

과적합을 억제하는 방법에는 '훈련 데이터의 양 늘리기'와 '모델의 복잡도 줄이기'가 있다. 이 외에도 모델의 복잡도에 페널티를 주는 '정규화'도 있다. L2 정규화는 가중치가 너무 커지면 페널티를 부과한다.

또, 드롭아웃처럼 훈련 시 계층 내의 뉴런 몇 개를 무작위로 무시하고 학습하는 방법도 일종의 정규화라고 할 수 있다.

위의 그림과 같이 드롭아웃은 무작위로 뉴런을 선택하여 선택한 뉴런을 무시한다. 무시한다는 것은 그 앞 계층으로부터의 신호 전달을 막는다는 뜻이다. '무작위한 무시'가 제약이 되어 신경망의 일반화 성능을 개선하는 것이다.

RNN에서 시계열 방향으로 드롭아웃을 넣어버리면 시간이 흐름에 따라 정보가 사라질 수 있다. 흐르는 시간에 비례해 드롭아웃에 의한 노이즈가 축적된다. 노이즈 축적을 고려하면, 시간축 방향으로의 드롭아웃은 멈추는 것이 좋다.


가중치 공유

언어 모델을 개선하는 아주 간단한 트릭 중 가중치 공유가 있다.

위의 그림처럼 Embedding 계층의 가중치와 Affine 계층의 가중치를 연결하는 기법이 가중치 공유이다. 두 계층이 가중치를 공유함으로써 학습하는 매개변수 수가 크게 줄어드는 동시에 정확도도 향상될 수 있다.

가중치 공유를 구현 관점에서 생각해보면, 어휘 수를 V로, LSTM의 은닉 상태의 차원 수를 H라 해보자.

그러면 Embedding 계층의 가중치는 형상이 V * H이며, Affine 계층의 가중치 형상은 H * V가 된다. 이때 가중치 공유를 적용하려면 Embedding 계층의 가중치를 전치하여 Affine 계층의 가중치로 설정하기만 하면 된다.

간단한 트릭으로 훌륭한 결과를 얻을 수 있다.


개선된 RNNLM 구현

  1. LSTM 계층의 다층화
  2. 드롭아웃 사용
  3. 가중치 공유

위의 3가지가 개선점이다.

그렇다면 BetterRnnlm 클래스를 구현해보자.

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()

세 가지 개선이 이뤄지고 있다. TimeLSTM 계층을 2개 겹치고, 사이사이에 TimeDropout 계층을 사용한다. 그리고 TimeEmbedding 계층과 TimeAffine 계층에서 가중치를 공유한다.

다음은 개선된 BetterRnnlm 클래스를 학습시킬 차례이다. 그 전에 매 에폭에서 검증 데이터로 퍼플렉서티를 평가하고, 그 값이 나빠졌을 경우에만 학습률을 낮추는 것을 해볼 것이다.

from common import config
from common.optimizer import SGD
from common.util import eval_perplexity
from common.trainer import RnnlmTrainer
from dataset import ptb

# 하이퍼 파라미터 설정
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)

이 코드는 학습을 진행하면서 매 에폭마다 검증 데이터로 퍼플렉서티를 평가하고, 그 값이 기존 퍼플렉서티보다 낮으면 학습률을 1/4로 줄인다. 이를 위해 이 코드에서는 RnnlmTrainer 클래스의 fit() 메서드를 이용해 1에폭분의 학습을 수행한 다음 검증 데이터로 퍼플렉서티의 평가하는 처리를 for문에서 반복한다.

이 코드를 실행하면 퍼플렉서티가 순조롭게 낮아진다.

TAGS.

Comments