RNN

NLP 글 목록

한빛미디어의 <밑바닥부터 시작하는 딥러닝 2>를 요약 정리한 글이다.

RNN(Recurrent Neural Network)은 시퀀스 데이터의 시간적 순서를 고려하기 위해 이전 상태 정보를 현재 계산에 반영하는 신경망이다.

일반적인 신경망은 입력이 한 방향으로만 흐르지만, RNN은 이전 시점의 은닉 상태를 다음 시점의 계산에 다시 사용한다.

따라서 과거의 정보를 기억하면서 최신 입력에 따라 상태를 갱신할 수 있다.

RNN 계층의 순환 구조

RNN 계층은 현재 입력과 이전 은닉 상태를 이용해 현재 은닉 상태를 계산한다.

RNN의 기본 구조

시간 tt에서 RNN은 입력 xt\mathbf{x}_t와 이전 은닉 상태 ht1\mathbf{h}_{t-1}를 받아 현재 은닉 상태 ht\mathbf{h}_t를 계산한다.

ht=tanh(ht1Wh+xtWx+b)\mathbf{h}_t = \tanh( \mathbf{h}_{t-1}\mathbf{W}_h + \mathbf{x}_t\mathbf{W}_x + \mathbf{b} )

여기서 사용하는 가중치는 두 종류이다.

  • Wx\mathbf{W}_x: 현재 입력 xt\mathbf{x}_t를 은닉 상태 방향으로 변환하는 가중치이다.
  • Wh\mathbf{W}_h: 이전 은닉 상태 ht1\mathbf{h}_{t-1}를 현재 은닉 상태 계산에 반영하는 가중치이다.

이 글에서는 미니배치 구현과 맞추기 위해 xt\mathbf{x}_t, ht\mathbf{h}_t를 row vector 묶음으로 생각한다.

즉, 미니배치 크기가 NN, 입력 차원이 DD, 은닉 차원이 HH이면 다음과 같은 형상을 가진다.

xtRN×D\mathbf{x}_t \in \mathbb{R}^{N \times D} ht1RN×H\mathbf{h}_{t-1} \in \mathbb{R}^{N \times H} WxRD×H\mathbf{W}_x \in \mathbb{R}^{D \times H} WhRH×H\mathbf{W}_h \in \mathbb{R}^{H \times H}

따라서 현재 은닉 상태는 다음 차원이 된다.

htRN×H\mathbf{h}_t \in \mathbb{R}^{N \times H}

순환 경로를 펼쳐 보기

RNN은 시간 방향으로 펼치면 일반적인 feedforward network처럼 볼 수 있다.

시간 방향으로 펼친 RNN

시간 방향으로 펼쳐진 RNN은 여러 계층처럼 보이지만, 실제로는 같은 계층이 시간마다 반복 사용되는 것이다.

각 시점의 RNN 계층은 현재 입력과 바로 이전 시점의 은닉 상태를 함께 받아 계산한다.

예를 들어 다음과 같이 이어진다.

h1=tanh(h0Wh+x1Wx+b)\mathbf{h}_1 = \tanh( \mathbf{h}_0\mathbf{W}_h + \mathbf{x}_1\mathbf{W}_x + \mathbf{b} ) h2=tanh(h1Wh+x2Wx+b)\mathbf{h}_2 = \tanh( \mathbf{h}_1\mathbf{W}_h + \mathbf{x}_2\mathbf{W}_x + \mathbf{b} ) ht=tanh(ht1Wh+xtWx+b)\mathbf{h}_t = \tanh( \mathbf{h}_{t-1}\mathbf{W}_h + \mathbf{x}_t\mathbf{W}_x + \mathbf{b} )

현재 출력 ht\mathbf{h}_t는 다른 계층으로 위쪽 출력되는 동시에, 다음 시점 RNN의 입력으로도 전달된다.

이 때문에 RNN은 상태를 가지는 계층이라고 볼 수 있다.

RNN의 출력 ht\mathbf{h}_t는 은닉 상태(hidden state) 또는 은닉 벡터 상태(hidden vector state)라고 부른다.

BPTT

BPTT(BackPropagation Through Time)는 시간 방향으로 펼친 RNN에 대해 역전파를 수행하는 방법이다.

BPTT

BPTT는 시간 방향으로 펼친 RNN을 뒤쪽 시점부터 거꾸로 역전파한다.

RNN을 시간 방향으로 펼치면 각 시점의 계산 그래프가 이어진 형태가 된다.

따라서 역전파도 마지막 시점에서 시작해 과거 시점 방향으로 진행된다.

하지만 긴 시계열 데이터를 그대로 BPTT로 학습하면 문제가 생긴다.

  • 시점 수 TT가 커질수록 계산량이 증가한다.
  • 각 시점의 중간 계산 결과를 저장해야 하므로 메모리 사용량이 증가한다.
  • 긴 시간 동안 gradient가 전파되면서 기울기 소실 또는 기울기 폭발이 발생할 수 있다.

이 문제를 완화하기 위해 Truncated BPTT를 사용한다.

Truncated BPTT

Truncated BPTT는 긴 시퀀스를 한 번에 역전파하지 않고, 일정 길이로 잘라 역전파하는 방법이다.

Truncated BPTT

Truncated BPTT는 긴 시계열을 작은 블록으로 나누고, 각 블록 단위로 역전파한다.

중요한 점은 순전파와 역전파의 처리 방식이 다르다는 것이다.

순전파에서는 은닉 상태를 계속 이어간다.

하지만 역전파에서는 일정 길이 이후의 연결을 끊고, 잘라낸 블록 단위로 gradient를 계산한다.

즉, Truncated BPTT는 순전파의 흐름은 유지하지만 역전파의 연결만 제한한다.

이렇게 하면 긴 시퀀스 전체를 한 번에 역전파하지 않아도 되므로 계산량과 메모리 사용량을 줄일 수 있다.

Truncated BPTT와 미니배치

Truncated BPTT를 미니배치로 사용할 때는 시계열 순서를 유지해야 한다.

Truncated BPTT 미니배치

미니배치의 각 원소는 서로 다른 위치에서 시작하지만, 각 스트림 내부의 시간 순서는 유지되어야 한다.

예를 들어 batch size가 2이고 sequence length가 10이면 다음처럼 구성할 수 있다.

batch 1:
  stream 1: x0   ~ x9
  stream 2: x500 ~ x509

batch 2:
  stream 1: x10  ~ x19
  stream 2: x510 ~ x519

여기서 batch 2의 초기 은닉 상태는 batch 1의 마지막 은닉 상태에서 이어받는다.

batch 1 → h0  ~ h9,   h500 ~ h509
batch 2 → h10 ~ h19,  h510 ~ h519

즉, gradient는 잘라서 계산하지만 hidden state는 이어서 사용한다.

만약 미니배치를 무작위로 섞으면 hidden state의 시간적 맥락이 깨진다.

따라서 Truncated BPTT에서는 일반적인 랜덤 미니배치가 아니라, 시계열 순서가 유지되는 stream batch 형태가 필요하다.

RNN 계층 구현

RNN 구현에서는 한 시점의 계산을 담당하는 RNN 계층과, 여러 시점의 계산을 묶어서 처리하는 TimeRNN 계층을 나누어 생각할 수 있다.

TimeRNN 입력과 출력

TimeRNN은 길이 T의 시계열 입력을 받아 각 시점의 은닉 상태를 출력한다.

RNN과 TimeRNN

RNN은 한 시점의 계산을 담당하고, TimeRNN은 T개의 RNN 계산을 시간 방향으로 묶는다.

한 시점 RNN의 순전파는 다음 식이다.

ht=tanh(ht1Wh+xtWx+b)\mathbf{h}_t = \tanh( \mathbf{h}_{t-1}\mathbf{W}_h + \mathbf{x}_t\mathbf{W}_x + \mathbf{b} )
class RNN:
    def __init__(self, Wx, Wh, b):
        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):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)

        self.cache = (x, h_prev, h_next)
        return h_next

    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache

        dt = dh_next * (1 - h_next ** 2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        return dx, dh_prev

RNN 순전파 계산 그래프

RNN 한 시점의 순전파는 입력 방향과 이전 은닉 상태 방향의 선형 변환을 더한 뒤 tanh를 적용한다.

RNN 역전파 계산 그래프

역전파에서는 tanh 미분과 행렬곱 역전파를 이용해 입력, 이전 은닉 상태, 가중치의 gradient를 계산한다.

RNN 역전파에서 사용하는 핵심은 행렬곱 역전파와 tanh 미분이다.

행렬곱 Y=XW\mathbf{Y} = \mathbf{X}\mathbf{W}에 대해 다음이 성립한다.

LX=LYWT\frac{\partial L}{\partial \mathbf{X}} = \frac{\partial L}{\partial \mathbf{Y}} \mathbf{W}^{T} LW=XTLY\frac{\partial L}{\partial \mathbf{W}} = \mathbf{X}^{T} \frac{\partial L}{\partial \mathbf{Y}}

tanh의 미분은 다음과 같다.

y=tanh(x)y = \tanh(x) yx=1tanh2(x)=1y2\frac{\partial y}{\partial x} = 1 - \tanh^2(x) = 1 - y^2

따라서 상류 gradient가 Ly\frac{\partial L}{\partial y}라면,

Lx=Ly(1y2)\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} (1-y^2)

이다.

TimeRNN 구현

TimeRNN은 T개의 RNN 계층을 시간 방향으로 묶은 계층이다.

한 시점의 RNN 계층은 RNN이 담당하고, 전체 시계열의 흐름과 은닉 상태 관리는 TimeRNN이 담당한다.

class TimeRNN:
    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.dh = None, None
        self.stateful = stateful

stateful은 은닉 상태를 다음 호출까지 유지할지 결정한다.

  • stateful=True: 이전 블록의 마지막 은닉 상태를 다음 블록의 초기 은닉 상태로 사용한다.
  • stateful=False: 매번 은닉 상태를 영행렬로 초기화한다.

RNN의 모든 시점은 같은 가중치 Wx\mathbf{W}_x, Wh\mathbf{W}_h, b\mathbf{b}를 공유한다.

시점마다 다른 가중치를 사용하면 시간마다 다른 함수가 적용되는 것이므로, 같은 규칙으로 시퀀스를 처리한다는 RNN의 의미가 약해진다.

TimeRNN 순전파

def forward(self, xs):
    Wx, Wh, b = self.params
    N, T, D = xs.shape
    D, H = Wx.shape

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

    for t in range(T):
        layer = RNN(*self.params)
        self.h = layer.forward(xs[:, t, :], self.h)
        hs[:, t, :] = self.h
        self.layers.append(layer)

    return hs

입력 xs의 형상은 다음과 같다.

xsRN×T×Dxs \in \mathbb{R}^{N \times T \times D}

출력 hs의 형상은 다음과 같다.

hsRN×T×Hhs \in \mathbb{R}^{N \times T \times H}

각 시점의 RNN 계층은 순전파에서 사용한 값을 cache에 저장한다.

역전파에서 Wx\mathbf{W}_x, Wh\mathbf{W}_h, b\mathbf{b}의 gradient를 계산하려면 순전파 때의 xt\mathbf{x}_t, ht1\mathbf{h}_{t-1}, ht\mathbf{h}_t가 필요하기 때문이다.

RNN에서는 ht\mathbf{h}_t가 현재 시점의 출력이면서 동시에 다음 시점으로 전달되는 hidden state이다.

작업에 따라 모든 시점의 hidden state를 사용할 수도 있고, 마지막 시점의 hidden state만 사용할 수도 있다.

  • sequence-to-sequence: [h1,h2,,hT][\mathbf{h}_1, \mathbf{h}_2, \dots, \mathbf{h}_T]를 사용한다.
  • sequence-to-one: 마지막 은닉 상태 hT\mathbf{h}_T만 사용한다.

TimeRNN 역전파

TimeRNN 역전파 개요

TimeRNN의 역전파는 시간 역순으로 각 RNN 계층의 backward를 호출한다.

TimeRNN 역전파 상세

각 시점의 은닉 상태는 출력 방향과 다음 시점 방향으로 분기되므로, 역전파에서는 두 gradient가 합산된다.
def backward(self, dhs):
    Wx, Wh, b = self.params
    N, T, H = dhs.shape
    D, H = Wx.shape

    dxs = np.empty((N, T, D), dtype='f')
    dh = 0
    grads = [0, 0, 0]

    for t in reversed(range(T)):
        layer = self.layers[t]
        dx, dh = layer.backward(dhs[:, t, :] + dh)
        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

입력 dhs는 각 시점의 출력 은닉 상태에 대한 gradient이다.

dhsRN×T×Hdhs \in \mathbb{R}^{N \times T \times H}

역전파는 마지막 시점부터 첫 시점으로 거꾸로 진행된다.

이때 각 RNN 계층으로 들어가는 gradient는 다음 두 가지를 합한 값이다.

  1. 현재 시점 출력 ht\mathbf{h}_t로부터 들어오는 gradient
  2. 다음 시점 ht+1\mathbf{h}_{t+1}로부터 거꾸로 흘러온 gradient

그래서 코드에서 다음처럼 더한다.

layer.backward(dhs[:, t, :] + dh)

순전파에서 하나의 값이 여러 방향으로 분기되면, 역전파에서는 각 방향에서 온 gradient가 합산된다.

또한 모든 시점의 RNN 계층은 같은 가중치를 공유하므로, 각 시점에서 계산된 가중치 gradient도 모두 더해야 한다.

grads[i] += grad

반복문이 끝난 뒤의 dh는 현재 블록보다 더 앞선 시점으로 흘러갈 gradient이다.

이 값은 self.dh에 저장된다.

RNNLM

RNNLM(RNN Language Model)은 RNN을 이용한 언어 모델이다.

언어 모델은 지금까지 등장한 단어들을 바탕으로 다음 단어의 확률분포를 예측한다.

RNNLM 계층 흐름

RNNLM은 Embedding, RNN, Affine, Softmax 계층으로 구성된다.

기본 흐름은 다음과 같다.

word id
→ Embedding
→ RNN
→ Affine
→ Softmax
→ next word probability

Embedding 계층은 단어 ID를 단어 벡터로 바꾼다.

RNN 계층은 지금까지 입력된 단어들의 정보를 은닉 상태 h\mathbf{h}에 저장한다.

Affine 계층은 은닉 상태를 어휘 수 크기의 score로 변환하고, softmax는 이를 다음 단어 확률분포로 바꾼다.

RNNLM의 문맥 흐름

RNN은 과거 단어 정보를 은닉 상태에 압축해 저장하고, 이를 바탕으로 다음 단어를 예측한다.

Time 계층

시계열 데이터를 효율적으로 처리하기 위해 각 계층 앞에 Time을 붙인 계층을 사용한다.

예를 들어 다음과 같은 계층이 있다.

  • TimeEmbedding
  • TimeRNN
  • TimeAffine
  • TimeSoftmaxWithLoss

Time 계층

Time 계층은 T개의 시점 데이터를 한 번에 처리하기 위해 기존 계층을 시간 방향으로 확장한 것이다.

일반적으로는 각 시점마다 같은 계층을 하나씩 적용하면 된다.

다만 TimeAffine은 각 시점의 Affine 계층을 따로 만들지 않고, 행렬 계산으로 한 번에 처리할 수 있다.

TimeSoftmaxWithLoss는 각 시점의 softmax loss를 계산한 뒤 평균 손실을 반환한다.

Time Softmax with Loss

각 시점의 손실을 계산한 뒤, 전체 시점의 평균을 최종 손실로 사용한다.

시점이 TT개일 때 최종 손실은 다음과 같다.

L=1T(L0+L1++LT1)L = \frac{1}{T} (L_0 + L_1 + \cdots + L_{T-1})

미니배치까지 포함하면 배치와 시간 축 전체에 대한 평균 손실로 볼 수 있다.

Lblock=1Nn=0N11Kt=0K1Lt(n)L_{\mathrm{block}} = \frac{1}{N} \sum_{n=0}^{N-1} \frac{1}{K} \sum_{t=0}^{K-1} L_t^{(n)}

SimpleRnnlm 구현

SimpleRnnlm은 다음 네 계층으로 구성된다.

TimeEmbedding
→ TimeRNN
→ TimeAffine
→ TimeSoftmaxWithLoss

SimpleRnnlm 구조

SimpleRnnlm은 단어 ID 시퀀스를 받아 다음 단어 예측 손실을 계산한다.

가중치 초기화에서는 RNN과 Affine 계층에 Xavier 초깃값을 사용한다.

Embedding 계층은 직접적인 행렬곱보다 lookup table 성격이 강하므로, 작은 난수로 초기화한다.

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

stateful=True로 설정하면 TimeRNN은 이전 미니배치의 마지막 hidden state를 다음 미니배치로 이어간다.

따라서 Truncated BPTT 방식으로 학습하면서도 순전파의 시계열 흐름은 유지할 수 있다.

언어 모델 평가

언어 모델은 과거 단어 정보를 바탕으로 다음 단어의 확률분포를 출력한다.

언어 모델의 성능을 평가할 때는 주로 perplexity를 사용한다.

Perplexity

Perplexity는 언어 모델이 얼마나 혼란스러워하는지를 나타내는 지표이다.

직관적으로는 정답 단어에 부여한 확률의 역수로 볼 수 있다.

Perplexity 예시

정답 단어에 높은 확률을 부여할수록 perplexity는 낮아진다.

예를 들어 정답 단어가 say일 때, 모델이 say에 0.8의 확률을 부여했다면 perplexity는 다음과 같다.

10.8=1.25\frac{1}{0.8} = 1.25

반대로 정답 단어에 0.2의 확률만 부여했다면 perplexity는 다음과 같다.

10.2=5\frac{1}{0.2} = 5

따라서 perplexity는 작을수록 좋은 모델이다.

perplexity는 다음 단어 후보가 평균적으로 몇 개 정도로 좁혀졌는지를 나타내는 분기 수(number of branches)처럼 해석할 수 있다.

입력 데이터가 여러 개인 경우 cross entropy loss LL에 대해 perplexity는 다음과 같이 계산한다.

L=1NnktnklogynkL = - \frac{1}{N} \sum_n \sum_k t_{nk} \log y_{nk} perplexity=eL\mathrm{perplexity} = e^L

여기서 tnkt_{nk}는 정답 레이블이고, ynky_{nk}는 모델이 출력한 확률이다.

perplexity는 기하평균 분기 수라고도 볼 수 있다.

RNNLM 학습

아래 코드는 PTB 데이터 일부를 사용해 SimpleRnnlm을 Truncated BPTT 방식으로 학습하는 예이다.

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
time_size = 5
lr = 0.1
max_epoch = 100

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)

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('| epoch %d | perplexity %.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()

학습이 진행되면 perplexity가 점차 감소한다.

Perplexity 학습 곡선

학습이 진행될수록 perplexity가 낮아지는 모습을 확인할 수 있다.

미니배치 내부 구조

Truncated BPTT 학습 코드에서 미니배치를 만드는 핵심은 다음 부분이다.

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

변수의 의미는 다음과 같다.

  • time_size: Truncated BPTT에서 한 번에 펼칠 시퀀스 길이이다.
  • batch_size: 한 번에 처리할 데이터 개수이다.
  • offsets: 각 배치 원소의 시작 위치이다.
  • xs: 입력 단어 ID 시퀀스이다.
  • ts: 타깃 단어 ID 시퀀스이다.
  • time_idx: 현재 시간 위치를 나타내는 전역 인덱스이다.

각 배치 원소는 서로 다른 위치에서 시작하지만, 각 원소 내부에서는 시간 순서가 유지된다.

% data_size를 사용하므로 말뭉치 끝에 도달하면 다시 처음으로 돌아간다.

이 방식은 미니배치 병렬 처리를 하면서도 RNN의 시간적 연결을 유지하기 위한 구성이다.

정리

RNN은 이전 은닉 상태를 현재 계산에 반영하여 시퀀스 데이터를 처리하는 신경망이다.

BPTT는 시간 방향으로 펼친 RNN을 역전파하는 방법이고, Truncated BPTT는 긴 시퀀스의 역전파 연결을 일정 길이로 잘라 계산량과 메모리 사용량을 줄이는 방법이다.

TimeRNN은 여러 시점의 RNN 계산을 하나의 계층처럼 처리하고, stateful=True를 통해 블록 사이의 hidden state를 이어갈 수 있다.

RNNLM은 단어 시퀀스를 입력받아 다음 단어의 확률분포를 예측하는 언어 모델이며, 성능 평가는 주로 perplexity를 사용한다.