안녕, 세상!

(2)-5-3 RnnLM 구현 본문

It공부/Deep learning

(2)-5-3 RnnLM 구현

dev_Lumin 2021. 2. 10. 21:43

(1) RNNLM 구현

구현할 RNNLM의 형태는 다음과 같습니다.

 

이를 코드로 나타내면 다음과 같습니다.

#import sys
#sys.path.append('..')
import numpy as np
#from common.time_layers import *


class SimpleRnnlm:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size  # wordvec_size는 입력벡터차원수(특정단어 분산표현)
        rn = np.random.randn

        # 가중치 초기화
        embed_W = (rn(V, D) / 100).astype('f')
        rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')
        rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
        rnn_b = np.zeros(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),
            TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.rnn_layer = self.layers[1]

        # 모든 가중치와 기울기를 리스트에 모은다.
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def forward(self, xs, ts):
        for layer in self.layers:
            xs = layer.forward(xs)
        loss = self.loss_layer.forward(xs, 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.rnn_layer.reset_state()

생성자에서는 각 계층에서 사용하는 가중치와 편향들을 랜덤으로 초기화하고 필요한 계층을 생성합니다.

Truncated BPTT로 학습한다고 가정하여 Time RNN 계층의 stateful을 True로 설정했습니다.

그럼으로써 Time RNN 계층은 이전 시각의 은닉 상태를 계승할 수 있습니다.

 

또한 RNN 계층과 Affine 계층에서 'Xavier(사바에르) 초깃값을 이용했습니다.

Xavier 초깃값에서는 이전 계층의 노드가 n개라면 

표준편차가 1/(n^(1/2))인 분포로 값들을 초기화 합니다.

표준편차는 데이터의 차이를 직관적으로 나타내는 척도입니다.

 

다음과 같이 가중치의 초깃값을 설정하는 이유는 가중치 초깃값 설정이 중요하기 때문입니다.

 

forward(), backward() 부분은 앞서 구축한 TimeRNN과 SoftmaxwithLoss계층의 forward(), backward()를 이용하여 코드를 구축한 것입니다.

 

reset_state()는 신경망의 상태를 초기화하는 메서드입니다.

 

 

(2) 언어 모델 평가

이제 이 신경망에 데이터를 주고 학습을 수행하기 전, 언어 모델의 '평가 방법'에 관해 알아보겠습니다.

언어 모델은 주어진 과거 단어(정보)로부터 다음에 출현할 단어의 확률분포를 출력합니다.

이때 언어 모델의 예측 성능을 평가하는 척도로 perplexity(혼란도)를 자주 이용합니다.

퍼플렉서티는 간단하게 말하면 '확률의 역수'입니다.

 

앞서 'you say goodbye and I say hello .'말뭉치 중 you를 RNNLM에 넣었을 때 다음 단어에 대한 확률분포가 나옵니다.

이때 you 다음으로 say가 나올 확률이 0.8이라고 분포되어있고 정답이 'say'라면, 이때의 확률은 0.8입니다.

퍼플렉서티는 이 확률의 역수 1/(0.8) = 1.25라고 할 수 있습니다.

만약 say가 나올 확률이 0.2라고 나왔다면 퍼플렉서티는 5일 것입니다.

 

이를 통해 퍼플렉서티는 작은 값일 수록 잘 예측되었다는 것을 알 수 있습니다.

이러한 퍼플렉서티는 직관적으로 분기수(number of branches)로 해석할 수 있습니다.

즉 예측한 분기수가 1.25라면 'you'라는 단어 다음에 출현할 수 있는 단어의 후보를 1개로 좁혔다는 뜻이고,

5라면 해당 단어 다음에 출현할 수 있는 단어의 후보가 5개나 된다는 뜻입니다.

 

지금까지는 입력 데이터가 하나일 때의 퍼플렉서티를 말했지만, 여러 개일 경우 다음 공식을 따릅니다.

N : 데이터의 총 개수

tn : 원핫 벡터로 나타낸 정답 레이블

tnk : n 개째 데이터의 k번째 값을 의미

ynk : 확률분포(신경망에서는 softmax의 출력)

 

위의 손실 함수를 의미하는 L은 사실 교차 엔트로피 오차식과 완전 같습니다.

퍼플렉서티는 위에 식이 나와있듯이 e^L입니다.

 

위의 식은 N개에 대해서 나타냈으며 데이터가 하나일 때 설명한 '확률의 역수', '분기 수', '선택사항의 수'같은 개념이 그대로 적용됩니다.

 

 

(3) RNNLM의 학습 코드

PTB 데이터셋을 이용해 RNNLM 학습을 수행한 코드입니다.

단 PTB 데이터셋 전부를 대상으로 학습하면 전혀 좋은 결과를 낼 수 없기 때문에 처음 1000개 단어만 이용하겠습니다. ( 이 문제는 다음 장에서 개선합니다.)

 

import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 하이퍼파라미터 설정
batch_size = 10
wordvec_size = 100
hidden_size = 100 # RNN의 은닉 상태 벡터의 원소 수
time_size = 5     # Truncated BPTT가 한 번에 펼치는 시간 크기
lr = 0.1
max_epoch = 100

# 학습 데이터 읽기(전체 중 1000개만)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)  # 단어종류, corpus는 0부터 매겨질 것이므로 +1을 해주는 것임

xs = corpus[:-1]  # 입력 (맨 마지막 값빼고를 의미)
ts = corpus[1:]   # 출력(정답 레이블) ( 맨 첫번째 값 빼고를 의미 : 정답이라는 것은 그 다음 올 단어이므로)
data_size = len(xs)
print('말뭉치 크기: %d, 어휘 수: %d' % (corpus_size, vocab_size))

# 학습 시 사용하는 변수
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

# 모델 생성
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)

# 미니배치의 각 샘플의 읽기 시작 위치를 계산
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
    for iter in range(max_iters):
        # 미니배치 취득
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1

        # 기울기를 구하여 매개변수 갱신
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1

    # 에폭마다 퍼플렉서티 평가
    ppl = np.exp(total_loss / loss_count)
    print('| 에폭 %d | 퍼플렉서티 %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0

# 그래프 그리기
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()

 

 

학습을 진행할수록 퍼플렉서티가 순조롭게 낮아지는 것을 확인할 수 있습니다.

다만 크기가 작은 말뭉치로 실험을 한 것이며, 큰 말뭉치에 대해 대응할 수 없습니다.

큰 말뭉치에 대해선 다음 장에서 개선하겠습니다.

 

 

 

(위의 내용은 밑바닥부터 시작하는 딥러닝 2 (한빛미디어)를 바탕으로 글을 작성하였습니다)

 

'It공부 > Deep learning' 카테고리의 다른 글

transformer  (0) 2021.06.30
(2)-5-2 RNN 구현  (0) 2021.01.22
(2)-5-1 순환 신경망(RNN)  (3) 2021.01.21
(2)-4-2 개선된 word2vec 학습  (0) 2021.01.20
(2)-4-1 word2vec 속도 개선  (0) 2021.01.19
Comments