Deep Learning/from scratch II

[밑바닥부터 시작하는 딥러닝 2] - 5. 순환신경망(RNN) II

해파리냉채무침 2024. 2. 17. 16:29

시계열 데이터 처리 계층 구현

RNNLM의 전체 그림

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-5%EC%9E%A5-gnk6bhirc3#541-rnnlm%EC%9D%98-%EC%A0%84%EC%B2%B4-%EA%B7%B8%EB%A6%BC

맨 아래 embedding 계층은 단어 ID를 단어 벡터로 변환 ->분산 표현  RNN 계층 입력 -> 은닉상태 위쪽 & 오른쪽으로 출력 -> 위로 출력한 은닉상태 affine 계층 -> softmax

https://yerimoh.github.io/DL16/

you 를 입력했을 때, say가 가장 높게 나옴. 제대로 예측하려면 좋은 가중치를 사용해야함.

say를 입력했을 때, goodbye와  hello가 높게 나옴. 'you say goodye', 'you say hello'  모두 자연스러운 문장인 것으로 보아 RNN 계층은 'you say'라는 맥락을 기억하고 있다. you say 라는 과거의 정보를 은닉상태 벡터로 저장해둔 것이다. 

RNN 계층이 과거에서 현재로 데이터를 흘려보내줌으로써 과거의 정보를 인코딩해 저장할 수 있는것이다.

Time 계층 구현

T개분의 시계열 데이터를 한꺼번에 처리하는 계층을 Time xx 계층이라고 한다. Time affine, time embedding 계층 등등 T개의 affine, embedding 계층을 준비해서 각 시각의 데이터를 처리한다. 

시계열 버전의 time softmax with loss 계층은 다음과 같이 그려진다. 

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-5%EC%9E%A5-gnk6bhirc3#542-time-%EA%B3%84%EC%B8%B5-%EA%B5%AC%ED%98%84

xo x1 .. xt-1의 데이터는 아래층에서부터 전해지는 점수(확률로 정규화 되기전)를 의미한다. t0이나 t1의 데이터는 정답 레이블을 나타낸다. 손실들을 합산해서 평균한 값이 최종 손실이 된다. 

데이터 N개 짜리 미니배치면, 그 N개의 손실을 더해서 다시 N개로 나눠 데이터 1개당 평균 손실을 구한다. 

RNNLM 학습과 평가

smpleRnnlm은 다음과 같은 기전으로 이루어진다. 

https://yerimoh.github.io/DL16/

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


class SimpleRnnlm:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 가중치 초기화
        embed_W = (rn(V, D) / 100).astype('f')
        rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f') #xavier 초깃값 
        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),#은닉상태계층(stateful)
            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

xavier 초기값은 이전 계층의 노드가 n개라면 표준편차가 (1/sqrt(n))인 분포로 값들을 초기화한다.

가중치 초기값을 어떻게 설정하느냐에 따라 학습이 진행되는 방법과 최종 정확도가 크게 달라진다. 

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

언어 모델의 평가

언어 모델으이 예측 성능을 평가하는 척도로 퍼플렉시티 (perplexity, 혼란도)를 이용한다. 

퍼플렉시티 -> 확률의 역수 

https://yerimoh.github.io/DL16/

첫번째 모델에서 say가 나올 확률은 0.8, 퍼플렉시티는 1/0.8 =1.25

두번째 모델에서 say가 나올 확률은 0.2 , 퍼플렉시티는 1/0.2 = 5

그러므로 확률과 퍼플렉시티는 반비례하고, 퍼플렉시티가 작을수록 좋다. 분기수가 1.25이면 다음에 출현할 수 있는 단어의 후보가 1개정도로 좁혔다는 의미다.  나쁜 모델에서 후보가 아직 5개가 된다는 의미이다.

 

입력데이터가 여러개 일때의 퍼플렉시티는 다음과 같이 계산한다. 

 

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-5%EC%9E%A5-gnk6bhirc3#542-time-%EA%B3%84%EC%B8%B5-%EA%B5%AC%ED%98%84

N: 데이터의 총개수

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

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

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

L : 신경망의 손실(cross entropy eorror와 같은 식)

import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from 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)

xs = corpus[:-1]  # 입력
ts = corpus[1:]  # 출력(정답 레이블)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %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 # 인덱스를 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('| epoch %d | perplexity %.2f' #에폭마다 손실의 평균을 구해서 퍼플렉서티 구하기
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0

데이터 제공 방법에서 Truncated BPTT 방식으로 학습을 수행한다. 

그러므로 데이터는 순차적으로 주고미니배치에서 데이터를 읽는 시작 위치 조정해야함.

https://yerimoh.github.io/DL16/

학습을 진행할 수록 퍼플렉서티가 순조롭게 낮아짐을 알 수 있음. 300이었던 값이 거의 1에 근접함을 알 수있음.

 

RNNLM Trainer 클래스

from common.trainer import RnnlmTrainer

model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

trainer.fit(xs, ts, max_epoch, batch_size, time_size)

RnnlmTrainer 클래스에 model과 optimizer를 주어 초기화 하고, fit를 호출해 학습을 수행한다.

RNNLM의 과정을 요약하면 다음과 같다.

1.미니매치를 순차적으로 만들기

2. 순전파 역전파 호출

3. 옵티마이저로 가중치 갱신

4. 퍼플렉서티 구하기