Deep Learning/from scratch II

[밑바닥부터 시작하는 딥러닝 2] - 6장 게이트가 추가된 RNN II

해파리냉채무침 2024. 2. 20. 01:40

LSTM을 사용한 언어 모델

LSTM 계층을 사용하는 Rnnlm 클래스 구현

import sys
sys.path.append('..')
from time_layers import *
from base_model import BaseModel


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) #softmax 계층 직전까지 처리하는 predict 추가됨
        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()

위의 신경망을 이용해서 PTB 데이터셋을 학습해보았다

import sys
sys.path.append('..')
from optimizer import SGD
from trainer import RnnlmTrainer
from 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) #eval_interval =20은 20번째 반복마다 퍼플렉서티 평가
trainer.plot(ylim=(0, 500)) #평가를 그래프로 그림

# 테스트 데이터로 평가
model.reset_state() #lstm의 은닉상태와 기억셀을 재설정하여 평가 수행 
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)

# 매개변수 저장
model.save_params()

 

 20번째 반복마다 퍼플렉서티 값이 출력된다. 

https://yerimoh.github.io/DL17/

첫번째 퍼플렉서티가 10000에서 시작했지만 반복이 300회를 넘자 퍼플렉서티가 400을 밑돌기 시작한다. 학습을 하면 할수록 성능이 좋아지고 있다. 

https://yerimoh.github.io/DL17/

추이를 보면 퍼플렉서티가 최종적으로 100단위까지 갔다. 초기에 비하면 많은 개선이 되었다고 할 수 있다.

RNNLM 추가 개선

LSTM 계층 다층화

https://velog.io/@dscwinterstudy/%EB%B0%91%EB%B0%94%EB%8B%A5%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%9D2-6%EC%9E%A5

정확한 모델을 만들고자 한다면 LSTM 계층을 여러 겹 쌓으면 복잡한 패턴을 학습할 수 있는 효과를 볼 수 있다. 쌓는 층 수는 하이퍼파라미터 이기 때문에 처리할 문제의 복잡도나 학습 데이터 양에 따라 결정한다. PTB 언어 데이터셋의 언어모델에서는 2층~4층일때 좋은 결과를 얻는다. 

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

훈련 시 무작위로 뉴런을 선택하고, 선택한 뉴런을 무시하여 드롭아웃을 한다. 드롭아웃을 하여 오버피팅이 되는것을 막는다. 

https://velog.io/@dscwinterstudy/%EB%B0%91%EB%B0%94%EB%8B%A5%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%9D2-6%EC%9E%A5

드롭아웃을 어느 방향에 삽입해야 하는가? 

시계열 방향으로 삽입시 (좌우방향) 시간이 흐름에 따라 정보가 사라질 수 있다. 즉 흐르는 시간에 비례해 드롭아웃에 의한 노이즈가 축적되기 때문에 좋은 방법이 아니다.

상하방향으로 삽입시(위의 사진) 좌우방향(시간방향)으로 진행해도 정보를 잃지 않는다. 드롭아웃이 독립적으로 상하 방향에만 영향을 주게 되기 때문이다. 

 

하지만 이 변형 드롭아웃을 사용하면 시간, 상하 방향에도 이용할 수 있다. 변형 드롭아웃이 일반 드롭아웃보다 결과가 좋다고 알려져 있다. 

같은 계층에 속한 드롭아웃들은 같은 마스크를 공유한다. 마스크는 데이터의 통과 또는 차단을 결정하는 binary 형태의 무작위 패턴을 가리킨다. 같은 계층 드롭아웃의 마스크와 정보를 잃게 되는 방법 둘다 고정되므로 일반적인 드롭아웃때와 달리 지수적으로 손실되는 사태를 피할 수 있다

가중치 공유

embedding 계층과 affine 계층이 가중치를 공유한다. 두 계층이 가중치를 공유함으로서 학습하는 매개변수 수가 크게 줄어드는 동시에 정확도도 향상된다.  

어휘수가 V, LSTM의 은닉상태의 차원수를 H라고 할때, embedding 계층의 가중치는 형상은 VxH이고, affine 계층의 가중치 형상은 HxV가 된다. embedding 계층의 가중치를 전치하여 affine 계층의 가중치로 설정하면 된다. 

개선된 RNNLM 구현

import sys
sys.path.append('..')
from time_layers import *
import numpy as np
from 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')
        
       #세가지 개선(lstm 다층화, 드롭아웃 사용, 가중치 공유)

        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개, 사이사이 time dropout 계층을 사용한다. 이후 time embedding 계층과 time affine 계층에서 가중치를 공유한다.  

 

BetterRnnlm 클래스는 매 에폭에서 검증데이터로 퍼플렉시티를 평가하고, 그 값이 나빠질 경우에만 학습률을 낮출수 있도록 하였다. 그 값이 기존 퍼플렉서티(best_ppl)보다 낮으면 학습률을 1/4로 줄인다. 

import sys
sys.path.append('..')
GPU = True

from optimizer import SGD
from trainer import RnnlmTrainer
from 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 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('valid perplexity: ', ppl)

    if best_ppl > ppl:
        best_ppl = ppl
        model.save_params()
    else:
        lr /= 4.0
        optimizer.lr = lr

    model.reset_state()
    print('-' * 50)

해당 모델을 돌리는데 상당한 시간이 소요돼서 결과만 이야기 하자면, 테스트 데이터로 얻은 최종 퍼플렉서티는 75.76으로 앞서 100 언저리 였던것에 비하면 많은 성능개선이 이루어졌다고 할 수 있다.