본문 바로가기
ML & DL/책 & 강의

[밑시딥2] CHAPTER 6 게이트가 추가된 RNN

by 공부하는 무니 2024. 7. 29.
반응형
Blessed are the forgetful: for they get the better even of their blunders.
망각은 더 나은 전진을 낳는다 - 니체

 

RNN -> 장기 기억을 못한다. 그래서 나온 것들이 LSTM, GRU.

가끔 RNN이라고 말하면 LSTM이나 GRU같은 RNN 계열을 가리키는 경우도 있어서, 그냥 RNN을 말할때는 바닐라 RNN 혹은 기본 RNN 이라고 하기도 한다.

LSTM, GRU에는 기본 RNN에 게이트가 추가된다. 이 게이트 덕분에 장기기억을 할 수 있게 된다. 

6.1  RNN의 문제점

왜 기본 RNN은 장기 기억을 못할까? 원인은 기울기 소실 혹은 기울기 폭발 때문!

6.1.1 RNN 복습

  시계열 데이터 xt를 입력하면 ht가 출력되고, 이 ht가  RNN계층의 히든 스테이트(은닉 상태). 이 히든 스테이트가 기억을 담당한다. 

RNN은 바로 직전 히든스테이트를 이용한다. 

6.1.2 기울기 소실 또는 기울기 폭발

언어 모델은 주어진 단어들을 기초로 다음에 출현할 단어를 예측한다. 

정답은 "Tom",

이 문제에 제대로 대답하려면

1. Tom이 방에서 TV를 보고 있음

2. 그 방에 Mary가 들어옴

두 정보를 기억하고 있어야 한다.  -> RNN계층의 히든 스테이트에 이 정보를 인코딩해서 보관해두어야 함

Tom 이라는 정답이 들어왔을 때, 과거 방향으로 '의미 있는 기울기'가 전달되어야 과거와 현재의 의존 관계를 학습할 수 있다.

만약 기울기가 사라진다면 가중치는 전혀 갱신이 안될거고(장기 의존 관계 학습 x)

기울기가 점점 커져서 폭발한다면 수렴이 안됨

6.1.3 기울기 소실과 기울기 폭발의 원인

길이가 T인 시계열 데이터, T번째 정답 레이블로부터 전해지는 기울기가 어떻게 변하는지 보자.

tanh -> + -> matmul -> ...

여기서 +노드 역전파는 기울기를 변화시키지 않는다.

tanh 는?

미분값을 보면 x가 0에서부터 멀어질수록 작아진다. -> 역전파에서는 기울기가 tanh 노드를 지날 때마다 계속 작아진다는 뜻이다. -> tanh를 T번 반복하면 T번 반복해서 작아짐 

그래서 활성화 함수를 ReLU로 바꾸면 기울기 소실이 줄어든다.

 

MatMul은?

dhWhT 계산을 계속 반복한다. Wh는 고정이다. 

아래 코드를 실행해서 이때 기울기 변화가 어떻게 되는지 살펴보자

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
font_path = 'C:/Windows/Fonts/malgun.ttf'
font_name = fm.FontProperties(fname=font_path).get_name()
plt.rc('font', family=font_name)


N = 2  # 미니배치 크기
H = 3  # hidden state 벡터의 차원 수
T = 20  # 시계열 데이터의 길이(= timestep)

dh = np.ones((N, H))
np.random.seed(3)
# Wh = np.random.randn(H, H) 
Wh = np.random.randn(H, H) * 0.5 

norm_list = []
for t in range(T):
    dh = np.matmul(dh, Wh.T)
    norm = np.sqrt(np.sum(dh**2)) / N
    norm_list.append(norm)
    
u, s, vh = np.linalg.svd(Wh)
print(s)

# 그래프 그리기
plt.plot(np.arange(len(norm_list)), norm_list)
plt.xticks([0, 4, 9, 14, 19], [1, 5, 10, 15, 20])
plt.xlabel('시간 크기(time step)')
plt.ylabel('노름(norm)')
plt.show()

기울기 폭발 발생! -> 오버플로가 일어나서 로스가 NaN이 뜬다

 

Wh초기값을 변경하면? 랜덤으로하고 *0.5 곱해보자

기울기가 지수적으로 감소 -> 기울기 소실 

0에 가깝게 작아지면 더이상 학습이 안됨 -> 장기 의존관계를 학습할 수 없음

 

왜 이런 지수적인 변화가 일어날까? Wh를 T번 반복해서 곱했기 때문. 

6.1.4 기울기 폭발 대책

기울기 폭발의 대책: 기울기 클리핑

기울기 값이 thershold를 초과하면 기울기를 더 작게 수정한다. 

# chap06/clip_grads.py
import numpy as np

dW1 = np.random.rand(3, 3) * 10
dW2 = np.random.rand(3, 3) * 10
grads = [dW1, dW2]
max_norm = 5.0  # threshold

def clip_grads(grds, max_norm):
    total_norm = 0
    for grad in grads:
        total_norm += np.sum(grad ** 2)
    
    total_norm = np.sqrt(total_norm)
    
    rate = max_norm / (total_norm + 1e-6)
    if rate < 1:
        for grad in grads:
            grad *= rate


print('before:', dW1.flatten())
clip_grads(grads, max_norm)
print('after:', dW1.flatten())

6.2 기울기 소실과 LSTM

기울기 소실을 해결하려면 RNN 게층의 아키텍처를 뜯어 고쳐야 한다. 

그래서 나온 것들이 게이트들이다.

LSTM이 어떻게 게이트로 기울기 소실을 해결하는지 보자.

6.2.1 LSTM의 인터페이스

LSTM 에는 c라는 경로가 하나 추가 되었다. c는 기억 셀이다. LSTM 전용 기억 공간

기억 셀의 특징은 데이터를 LSTM계층 내에서만 주고받고 출력을 하진 않는다.

6.2.2 LSTM 계층 조립하기

LSTM에는 기억 셀 c가 있다. ct에는 시각 t에서의 LSTM의 기억(과거로부터 시각 t까지에 필요한 모든 정보)이 저장되어있다. 그리고 이 기억 ct를 바탕으로 외부 계층과 다음 시각의 LSTM에 은닉상태 ht(기억 셀을  tanh  통과시킨 값)를 출력한다. 

ct는 3개의 입력을 받아 어떤 계산을 수행해서 구해진다. ct-1, ht-1, xt

어떤 계산인지는 뒤에서 설명한다.

 

게이트가 뭘까?

이 게이트의 열림 정도를 파라미터로 두어서 학습하게 한다.

6.2.3 output 게이트

히든 스테이트 ht는 기억 셀 ct에 tanh를 적용한 것이다.  

tanh(ct) 에 게이트를 적용하자 -> 그것이 다음 시각의 히든 스테이트에 얼마나 중요한가'를 조정

히든 스테이트가 출력이므로 이 게이트의 이름이 output게이트다.

xt와 ht-1에 output 게이트 전용 파라미터가 있어서 어파인 계산 후 시그모이드를 하게 된다.

o는 아웃풋 게이트 웨이트 값을 적용하여 계산한 시그모이드의 출력값.

ht는 o와 tanh(x)의 원소별 곱(아다마르 곱) 으로 계산된다. 

6.2.4 forget 게이트

니체가 말했듯 망각은 더 나은 진전을 낳는다. 기억 셀에 '무엇을 잊을까'도 명확히 지시해야 한다. 물론 게이트를 통해.

ct-1의 기억 중에서 불필요한 기억은 잊게 해주는 게이트 -> forget 게이트

xt와 ht-1에 forget게이트 전용 파라미터가 있어서 어파인 계산 후 시그모이드를 하게 된다.

그 결과 값이 f. 이 f와 이전 기억셀인 ct-1을 원소별 곱을 수행 하면 ct가 구해진다. 

6.2.5 새로운 기억 셀

forget 게이트로는 잊는 것 밖에 못한다.

새로 기억해야 할 정보를 기억 셀에 추가해야 한다.

기억 셀에 추가되는 새로운 기억을 g로 표기함. 

g가 이전 시각의 기억 셀인 ct-1에 더해짐으로써 새로운 기억을 받아들인다고 할 수 있따.

더하는 거기때문에 게이트가 아니다. 그래서 시그모이드를 안쓰고 tanh를 씀

g는 ht-1, xt를 전용 파라미터로 어파인 변환을 한 후 tanh를 거쳐서 계산된다.  

6.2.6  input 게이트

g에 input 게이트 추가.

input 게이트는 g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지 판단. 

6.2.7 LSTM의 기울기 흐름

LSTM이 어떻게 기울기 소실을 없애주는걸까? -> 기억 셀 c의 역전파에 주목해보자

기억 셀의 역전파에서는 +노드와 x 노드만 지난다. 

- +노드의 역전파: 상류에서 전해지는 기울기를 그대로 흘린다. -> 기울기 변화가 이루어지지 않음

- x 노드의 역전파: 여기서 x노드는 행렬곱이 아니라 원소별 곱(아다마르 곱)이다. 원소별 곱에서 매번 다른 forget 게이트 값이 이용된다 - > 곱셈의 효과가 누적되지 않아서 기울기 소실이 일어나기 어렵다.

LSTM: Long Short-Term Memory -> 단기 기억을 오래 한다

6.3 LSTM 구현

LSTM을 구현해보자

1. 한 단계만 처리하는 LSTM 구현

2. T개의 단계를 한번에 처리하는 TimeLSTM 구현

 

LSTM식의 가중치들을 정리하면 다음과 같다.

4개의 가중치 계산을 하나의 아핀 변환으로 끝마칠 수 있다. 계산 속도가 빨라진다!

처음 4개분의 아핀변환을 한꺼번에 수행한다. 

slice(아핀 변환의 결과를 균등하게 네 조각으로 나눔) 노드를 통해 결과를 꺼낸다. 

slice의 역전파는 np.hstack() 사용

class LSTM:
    def __init__(self, Wx, Wh, b):
        '''
        Parameters
        ----------
        Wx: 입력 x에 대한 가중치 매개변수(4개분의 가중치가 담겨 있음)
        Wh: 은닉 상태 h에 대한 가장추 매개변수(4개분의 가중치가 담겨 있음)
        b: 편향(4개분의 편향이 담겨 있음)  
        '''
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None
        
    def forward(self, x, h_prev, c_prev):
        Wx, Wh, b = self.params
        N, H = h_prev.shape
        
        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b
        
        f = A[:, :H]
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]
        
        f = sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)
        
        c_next = f * c_prev + g * i  # Ct
        h_next = o * np.tanh(c_next)
        
        self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
        return h_next, c_next
    
    def backward(self, dh_next, dc_next):
        Wx, Wh, b = self.params
        x, h_prev, c_prev, i, f, g, o, c_next = self.cache
        
        tanh_c_next = np.tanh(c_next)
        
        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)
        
        dc_prev = ds * f
        
        di = ds * g
        df = ds * c_prev
        do = dh_next * tanh_c_next
        dg = ds * i
        
        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)
        
        dA = np.hstack((df, dg, di, do))
        
        dWh = np.dot(h_prev.T, dA)
        dWx = np.dot(x.T, dA)
        db = dA.sum(axis=0)
        
        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db
        
        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)
        
        return dx, dh_prev, dc_prev

6.3.1 Time LSTM 구현

Time LSTM은 T개의 시계열 데이터를 한꺼번에 처리하는 계층.

그런데 앞 에서 다루었듯이 RNN은 학습할 때 Truncated BPTT를 수행한다. 

Truncated BPTT: 역전파의 연결은 적당한 길이로 끊고, 순전파의 흐름은 그대로 유지한다.

그래서 은닉상태와 기억 셀을 인스턴스 변수로 유지해서 다음번에 forward()가 부렸을 때 이전 시각의 은닉상태와 기억셀에서부터 시작할 수 있게 한다.

TimeLSTM을 구현해보자.

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

 

6.4 LSTM을 사용한 언어 모델

Time LSTM을 구현했으니 이제 언어모델로 구현해보자. 

앞 장에서 구현한 RNN언어모델과 비슷. Time RNN -> Time LSTM 차이이다.

위 그림의 오른쪽 신경망을 Rnnlm 이라는 클래스로 구현하자.

Rnnlm 클래스는 SimpleRnnlm 클래스와 거의 같고 새로운 메서드를 몇 개 더 제공한다.

- predict(): 7장에서 수행할 문장 생성에 사용됨

- load_params(): 파라미터 읽기 -> common/base_model.py 에서 상속

- save_params():  파라미터 쓰기 -> common/base_model.py 에서 상속

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


class Rnnlm(BaseModel):
    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.reset_state()

이제 이 신경망으로 PTB 데이터셋을 학습해보자.

import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.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)
trainer.plot(ylim=(0, 500))

# 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)

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

5장의 train.py 와 달라진 부분 

- RnnlmTrainer클래스를 사용하여 학습. RnnlmTrainer.fit()을 이용해서 학습하는데 인수로 들어가는 max_grad가 기울기 클리핑을 한다. 또 인수 eval_interval=20은 20번째 반복마다 퍼플렉서티 평가하라는 뜻이다. 

- 학습이 끝나고 테스트 데이터로 퍼플렉서티 평가 

- 학습이 완료된 파라미터를 파일로 저장. 다음 장에서 이 저장한 웨이트 파라미터를 사용해서 문장을 생성할 예정.

첫 반복에서 퍼플렉서티가 10000.84 -> 다음에 나올 수 있는 후보 단어 수를 10,000개 정도로 좁혔다는 뜻. 데이터 셋 수가 10,000개니까 거의 학습이 안된 상태다. 

학습을 계속 하면서 퍼플렉시티가 작아지고 있음.  

테스트 데이터로 평가한 퍼플렉시티는 136 전후.. 

6.5 RNNLM 추가 개선

RNNLM 개선 포인트 3개를 설명하고 구현하고 얼마나 좋아졌는지 확인해보자

6.5.1 LSTM 계층 다층화

LSTM계층을 깊게 쌓아서 정확도를 높이자

LSTM을 2층으로 쌓았다. 첫 번째 LSTM 계층의 히든스테이트가 두 번째 LSTM 계층에 입력된다. 

몇 층으로 쌓을지는 하이퍼파라미터이다. PTB 데이터셋에서는 LSTM 층 수가 2-4일때 좋다고 알려져있고 구글 번역에서 사용했던 GNMT 모델(지금은 PaLM 2 사용한다고 함)은 LSTM을 8층 쌓은 거라고 한다.

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

LSTM을 다층화하면 표현력이 풍부한 모델이 나올 수 있지만, 종종 층이 깊어지면 과적합을 일으킨다. 

게다가 RNN은 일반적인 피드포워드 신경망보다 과적합이 더 쉽게 일어난다! (시간종속성 때문이라는 말이 있음) 따라서 RNN 의 과적합 대책은 중요하고 현재도 활발하게 연구되고 있다.

과적합을 억제하는 방법들이 여러가지가 있다.

- 훈련 데이터 양 늘리기

- 모델의 복잡도 줄이기 -> 정규화, 드롭아웃

여기서는 드롭아웃을 RNN에 적용해보자.

위와 같이 드롭아웃은 무작위로 뉴런을 선택해서 무시한다. 

일반적인 피드포워드 신경망에서는 드롭아웃을 활성화 함수 뒤에 삽입할 수 있다. 

그러면 RNN에서는 어디에 드롭아웃을 삽입할까?

이렇게 넣어버리면 시간이 흐르면서 더 많이 정보가 사라지게 된다. 그래서 시간 축 방향의 드롭아웃은 안된다!

이렇게 깊이 방향으로 드롭아웃을 넣게 되면 시간이 흘러도 정보를 잃지 않는다.

물론 시간 방향으로 정규화를 하는 방법도 연구되고 있다. 변형 드롭아웃이 그 예다.

같은 계층의 드롭아웃끼리 마스크를 공유! '우리는 죽어도 같이 죽는다!' -> 정보를 잃게 되는게 고정되므로 일반적인 드롭아웃때보다 정보가 지수적으로 손실되는 사태를 피할 수 있다. 학습이 더 잘될듯 하다..

6.5.3 가중치 공유

가중치 공유: 그림과 같이 Embedding  계층의 가중치와 Affine 계층의 가중치를 연결(공유)하는 기법

두 계층이 가중치를 공유함으로써 학습하는 파라미터의 수가 크게 줄어들면서도 정확해진다.

왜?
1. 과적합 방지
2. 정보의 일관성 유지
Embedding 계층과 Affine 계층 간의 가중치를 공유하면, 두 계층이 동일한 정보를 기반으로 학습하게 되어 정보의 일관성을 유지할 수 있습니다. 이는 특히 단어 임베딩(word embedding)과 같은 자연어 처리 작업에서 중요합니다. 예를 들어, 동일한 단어가 입력으로 주어졌을 때 동일한 벡터 표현을 가지게 되어 일관된 학습이 가능해집니다

 

6.5.4 개선된 RNNLM 구현

지금까지 개선점 3개가 나왔다.

- LSTM 계층의 다층화(여기서는 2층)

- 드롭아웃 사용(깊이 방향으로만 적용)

- 가중치 공유(Embedding 계층과 Affine 계층에서의 가중치 공유)

이 효과를 확인해보자.

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


class BetterRnnlm(BaseModel):
    '''
     LSTM 계층을 2개 사용하고 각 층에 드롭아웃을 적용한 모델이다.
     아래 [1]에서 제안한 모델을 기초로 하였고, [2]와 [3]의 가중치 공유(weight tying)를 적용했다.
     [1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
     [2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
     [3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
    '''
    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)  # weight tying!!
        ]
        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()

여기에 한 가지 더해보자. 매 에폭에서 검증 데이터로 퍼플렉서티를 평가하고 그 값이 나빠졌을 경우에만 러닝레이트를 낮추는 것이다. 

import sys
sys.path.append('..')
from common import config
# GPU에서 실행하려면 아래 주석을 해제하세요(CuPy 필요).
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.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 config.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('검증 퍼플렉서티: ', ppl)

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

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


# 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)

이렇게 학습시키면 퍼플렉서티: 75.76

개선 전에는 136이니까 상당히 개선되었다!

6.5.5 첨단 연구로

이렇게 RNNLM 을 개선했다. PTB 데이터셋에서 퍼플렉서티 75정도면 상당히 좋은 결과이지만, 최신 연구들은 더 앞서있다.

 

위 표의 마지막 모델(그때 당시 SOTA모델)에서는 드롭아웃 기반 정규화도 수행하고, 하이퍼파라미터 튜닝도 했고, 

이름에 있는 'continuous cache pointer'는 8장에서 다룰 어텐션⭐을 기반으로 한 기술이다. 

 

https://paperswithcode.com/sota/language-modelling-on-penn-treebank-word

 

 

Papers with Code - Penn Treebank (Word Level) Benchmark (Language Modelling)

The current state-of-the-art on Penn Treebank (Word Level) is GPT-3 (Zero-Shot). See a full comparison of 43 papers with code.

paperswithcode.com

6.6 정리

- 단순한 RNN의 학습에서는 기울기 소실과 기울기 폭발이 문제가 된다.

- 기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM과 GRU 등)이 효과적이다.

- LSTM에는 input 게이트, forget 게이트, output 게이트 등 3개의 게이트가 있다.

- 게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용하여 0.0 ~ 1.0 사이의 실수를 출력한다.

- 언어 모델 개선에는 LSTM 계층 다층화, 드롭아웃, 가중치 공유 등의 기법이 효과적이다.

- RNN의 정규화는 중요한 주제이며, 드롭아웃 기반의 다양한 기법이 제안되고 있다.

반응형

댓글