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

Time LSTM 구현

다음으로 Time LSTM 구현을 해볼 것이다. Time LSTM은 T개분의 시계열 데이터를 한꺼번에 처리하는 계층이다. 전체 그림은 아래 그림과 같다.

RNN에서는 학습할 때 Truncated BPTT를 수행했다. Truncated BPTT는 역전파의 연결은 적당한 길이로 끊었지만, 순전파의 흐름은 그대로 유지한다. 위의 그림처럼 은닉 상태와 기억 셀을 인스턴스 변수로 유지할 것이다. 이렇게 하여 다음번에 forward()가 불렸을 때, 이전 시각의 은닉 상태에서부터 시작할 수 있다.

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을 사용한 언어 모델

앞에서 구현한 언어 모델과의 차이는 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.retet_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)

Rnnlm 클래스에는 Softmax 계층 직전까지를 처리하는 predict() 메서드가 추가되었다. 이 메서드는 다음장에서 수행하는 문장 생성에 사용될 것이다. 그리고 매개변수 읽기/쓰기를 처리하는 load_params()와 save_params() 메서드도 추가되었다.

이제 이 신경망을 사용해 PTB 데이터셋을 학습할 것이다.

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

# 하이퍼파라미터 설정
batch_size = 20
wordvec_size = 100
hidden_size = 100
time_size = 35
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()

RnnlmTrainer 클래스를 사용해 모델을 학습시킨다. RnnlmTrainer 클래스의 fit() 메서드는 모델의 기울기를 구해 모델의 매개변수를 갱신한다. 이때 인수로 max_grad를 지정해 기울기 클리핑을 적용한다. eval_interval=20은 20번째 반복마다 퍼플렉서티를 평가하는 의미이다. 데이터가 크기 때문에 모든 에폭에서 평가하지 않고, 20번 반복될 때마다 평가하도록 설정했다. 그리고 plot() 메서드를 호출해 결과를 그래프로 그린다.

학습이 끝난 후 테스트 데이터를 사용해 퍼플렉서티를 평가한다. 마지막으로는 학습이 완료된 매개변수들을 파일로 저장한다.

매 20번째 반복의 퍼플렉서티 값이 출력된다. 첫 번째 퍼플렉서티의 값은 10002.16인데, 이는 다음에 나올 수 있는 후보 단어 수를 좁혔다는 뜻이다. PTB 데이터셋의 어휘 수가 10,000개 이므로 아무것도 학습하지 않은 상태를 뜻한다.

이번 모델에서는 총 4에폭의 학습을 수행했다. 퍼플렉서티가 순조롭게 낮아져서 최종적으로 100 정도가 되었다.

 

TAGS.

Comments