본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝 2

5. 순환 신경망(RNN)

728x90

밑바닥부터 시작하는 딥러닝 2를 바탕으로 제작하였습니다.

 

지금까지 본 신경망은 피드 포워드(feed forward, 앞먹임)라는 유형의 신경망입니다. 피드 포워드란 흐름이 단방향인 신경망을 말합니다. 다시 말해, 입력 신호가 다음 층(중간층)으로 전달되고, 그 신호를 받은 층은 그다음 층으로 전달하고, 다시 다음 층으로 전달하는 식으로 한 방향으로만 신호가 전달됩니다. 이러한 방식은 시계열 데이터를 잘 다루지 못한다는 문제가 있습니다. 단순한 피드 포워드는 신경망에서는 시계열 데이터의 성질(패턴)을 충분히 학습할 수 없습니다. 그래서 순환 신경망(Recurrent Neural Network, RNN)이 등장합니다.

 

5.1. 확률과 언어 모델

 ■ word2vec을 확률 관점에서 바라보기

 

 

CBOW 모델은 이와 같이 맥락(w(t-1), w(t+1))이 제공되고 그 토대로 타깃(w(t)) 단어를 추측합니다. 그럼 타깃이 w(t)일 확률을 수식으로 표현하면 P(w(t) | w(t-1), w(t+1))이 됩니다. 이 식을 풀어서 설명하면 'w(t-1), w(t+1)이 주어졌을 때, w(t)가 일어날 확률'을 뜻합니다.

이번에는 맥락을 왼쪽 윈도우만으로 한정해보겠습니다.

 

 

이와 같이 왼쪽 두 단어만을 맥락으로 생각하게 됩니다. 그리고 확률은 P(w(t) | w(t-1), w(t-2))처럼 됩니다. CBOW 모델의 학습으로 수행하는 일은 손실 함수를 최소화하는 가중치 매개변수를 찾는 것입니다. 이러한 가중치 매개변수가 발견되면 CBOW 모델은 맥락으로부터 타깃을 더 정확하게 추측할 수 있게 됩니다.

 

 ■ 언어 모델

언어 모델(Language Model)은 단어 나열에 확률을 부여합니다. 특정한 단어의 시퀀스에 대해서, 그 시퀀스가 일어날 가능성이 어느 정도인지(얼마나 자연스러운 단어 순서인지)를 확률로 평가하는 것입니다. 예를 들어 "you say goodbye

"라는 단어 시퀀스에는 높은 확률을 출력하고, "you say good die"에는 낮은 확률을 출력합니다.

이 언어 모델은 다양하게 응용되는데, 대표적으로 기계 번역과 음성인식이 있습니다. 예를 들어 음성 인식 시스템의 경우, 사람의 음성으로부터 몇 개의 문장을 후보로 생성합니다. 그다음 언어 모델을 사용하여 후보 문장이 '문장으로써 자연스러운지'를 기준으로 순서를 매길 수 있습니다.

그럼 이번에는 모델을 수식으로 표현해보겠습니다.

 

 

먼저 w1, w2, ... , wm이라는 m개 단어로 된 문장을 생각해보겠습니다. 이때 단어가 w1, ... , wm이라는 순서로 등장할 확률을 P(w1, ... , wm)으로 나타냅니다. 이 확률은 여러 사건이 동시에 일어날 확률이므로 동시 확률이라고 합니다. 이 동시 확률 P(w1, ... , wm)은 사후 확률을 사용하여 위와 같은 식으로 표현할 수 있습니다. 위 식에서 기호 Π(파이)는 모든 원소를 곱하는 '총곱'을 뜻합니다(총합은 시그마로 나타냅니다). 즉 동시 확률은 사후 확률의 총곱으로 나타낼 수 있습니다.

이를 바탕으로 w(t)를 타깃으로 하고 그 전에 등장한 모든 단어를 맥락(조건)으로 했을 때 확률은

 

 

이와 같이 표현할 수 있습니다. 이와 같인 방법으로 확률을 계산할 수 있다면 언어 모델의 동시 확률을 구할 수 있습니다.

 

 ■ CBOW 모델을 언어 모델로

word2vec의 CBOW 모델을 언어 모델에 적용하려면 맥락의 크기를 특정 값으로 한정하여 근사적으로 나타낼 수 있습니다. 예를 들어 맥락을 왼쪽 2개의 단어로 한정을 하면 CBOW 모델에 따라 근사적으로 나타낼 수 있습니다. 하지만 문제가 있습니다. 어떠한 값을 맥락의 크기로 설정해도 상관없지만 결국 특정 길이로 맥락의 크기는 고정됩니다. 예를 들어 왼쪽 10개의 단어를 맥락을 CBOW 모델을 만든다고 하면, 그 맥락보다 더 왼쪽에 있는 단어의 정보는 무시됩니다.

그렇다면 맥락의 크기를 크게 늘리면 이러한 문제가 해결된다고 생각할 수 있지만 CBOW 모델에서는 맥락 안의 단어 순서가 무시된다는 한계가 있습니다(CBOW 모델은 순서가 아닌 분포를 사용합니다). 결과적으로 우리는 맥락의 단어 순서도 고려한 모델을 사용하고 싶은데 이를 해결하기 위해 우리는 RNN을 사용할 것입니다. RNN은 맥락이 아무리 길어도 그 맥락의 정보를 기억하는 메커니즘을 갖추고 있습니다.

 

5.2. RNN이란

RNN(Recurrent Neural Network)의 'Recurrent'는 라틴어에서 온 말로, '몇 번이나 반복해서 일어나는 일'을 뜻합니다. 우리말로는 '재발한다', '주기적으로 일어난다', '순환한다' 등으로 번역됩니다. 그래서 RNN을 직역하면 '순환하는 신경망'이 됩니다. 주의할 점이 있는데 Recursive Neural Network(재귀 신경망)라는 신경망도 있습니다. 이는 주로 트리 구조의 데이터를 처리하기 위한 신경망으로, 순환 신경망과는 다른 것입니다.

 

 ■ 순환하는 신경망

순환하는 것은 반복해서 되돌아가는 것을 의미하는데 이를 위해 '닫힌 경로'가 필요합니다. '닫힌 경로' 혹은 '순환하는 경로'가 존재해야 데이터가 같은 장소를 반복해 왕래할 수 있습니다. RNN은 이러한 순환하는 경로가 있고 경로를 따라 데이터는 끊임없이 순환할 수 있습니다. 그리고 데이터가 순환되기 때문에 과거의 정보를 기억하는 동시에 최신 데이터로 갱신될 수 있습니다.

 

 

RNN 계층은 위와 같이 표현됩니다. 그림과 같이 RNN 계층은 순환하는 경로를 포함합니다. 이 순환 경로를 따라 데이터를 계층 안에서 순환시킬 수 있습니다. 그림에서 xt를 입력으로 받는데, t는 시각을 의미합니다. 이는 시계열 데이터가 RNN 계층에 입력됨을 표현한 것입니다. 그리고 그 입력에 대응하여 출력 ht(h0, h1, h2 ...)가 나옵니다. 또한, 각 시각에 입력되는 xt는 벡터라고 가정합니다. 문장을 다루는 경우를 예로 든다면 각 단어의 분산 표현이 xt가 되며, 이 분산 표현이 순서대로 하나씩 RNN 계층에 입력되는 것입니다.

 

 ■ 순환 구조 펼치기

RNN 계층의 순호나 구조에 대해 자세하게 살펴보겠습니다.

 

 

그림을 보면 알 수 있듯이 각 시각의 RNN 계층은 그 계층으로의 입력과 1개 전의 RNN 계층으로부터의 출력을 받습니다. 그리고 이 두 정보를 바탕으로 현 시각의 출력을 계산합니다. 이때 수행하는 계산의 수식은

 

 

이와 같습니다. RNN에서는 가중치가 2개 있습니다. 하나는 입력 x를 출력 h로 변환하기 위한 가중치 Wx이고, 다른 하나는 1개의 RNN 출력을 다음 시각의 출력으로 변환하기 위한 가중치 Wh입니다. 또한 편향 b도 있습니다.

행렬 곱을 계산하고, 그 합을 tanh(hyperbolic tangent) 함수를 이용해 변환합니다. 그 결과가 시각 t의 출력 ht가 됩니다. 이 ht는 다른 계층을 향해 위쪽으로 출력되는 동시에, 다음 시각의 RNN 계층을 향해 오른쪽으로도 출력됩니다. RNN을 보면 현재의 출력 ht는 한 시각 이전 출력에 기초해 계산됩니다. 그래서 RNN 계층을 '상태를 가지는 계층' 혹은 '메모리가 있는 계층'이라 표현합니다.

 

 ■ BPTT

 

 

위 그림은 순환 구조를 펼친 후의 RNN의 일반적인 오차역전파법을 나타낸 모습입니다. 여기서의 오차역전파법은 '시간 방향으로 펼친 신경망의 오차역전파법'이란 뜻으로 BPTT(Backpropagation Through Time)라고 합니다. 이 BPTT를 사용해 RNN을 학습할 수 있습니다.

하지만 긴 시계열 데이터를 학습하면, 시간 크기가 커지는 것에 비례하여 BPTT가 소비하는 컴퓨팅 자원도 증가하고 역전파 시의 기울기가 불안정해집니다.

 

 ■ Truncated BPTT

큰 시계열 데이터를 취급할 때는 흔히 신경망 연결을 적당한 길이로 끊습니다. 시간축 방향으로 너무 길어진 신경망을 적당한 지점에서 잘라내어 작은 신경망 여러 개로 만든다는 아이디어입니다. 그리고 잘라낸 작은 신경망에서 오차역전파법을 수행합니다. 이게 Truncated BPTT입니다.

Truncated BPTT에서는 신경망의 연결을 끊습니다만, 제대로 구현하려면 '역전파'의 연결만 끊어야 합니다. 순전파의 견결은 반드시 그대로 유지해야 합니다. 예시를 들어 길이가 1000인 시계열 데이터가 있다고 하겠습니다. 자연어 문제에서라면 단어 1000개짜리 말뭉치에 해당합니다. 이러한 말뭉치를 다루면서 RNN 계층을 펼치면 계층이 가로로 1000개나 늘어선 신경망이 됩니다. 물론 계층이 아무리 늘어나도 오차역전파법으로 기울기를 계산할 수 있지만 메모리 사용량 등이 문제가 되고 신경망을 통과할 때마다 기울기 값이 조금씩 작아져서 이전 시각 t까지 역전파되기 전에 0이 되어 소멸할 수도 있습니다. 이러한 이유로 역전파에서는 연결을 적당한 길이로 끊을 것입니다.

 

위 그림을 보면 RNN 계층을 길이 10개 단위로 학습할 수 있도록 역전파의 연결을 끊었습니다. 이처럼 역전파의 연결을 잘라버리면, 그보다 미래의 데이터에 대해서는 생각할 필요가 없어집니다. 따라서 각각의 블록 단위로, 미래의 블록과는 독립적으로 오차역전파법을 완결시킬 수 있습니다. 여기서 반드시 기억할 점은 역전파의 연결은 끊어지지만, 순전파의 연결은 끊어지지 않는다는 점입니다. 그러므로 RNN을 학습시킬 때는 순전파가 연결된다는 점을 고려해야 합니다. 즉 데이터를 순서대로 입력해야만 합니다.

 

 ■ Truncated BPTT의 미니배치 학습

지금까지는 미니배치가 어떤 식으로 이루어지는 지에 대해서는 생각하지 않았습니다. 미니배치 학습을 고려해 생각해보겠습니다. 미니배치 학습을 수행하기 위해서 원래대로면 구체적인 배치 방식을 고려해 데이터를 순서대로 입력해야 합니다. 그렇게 하려면 데이터를 주는 시작 위치를 각 미니배치의 시작 위치로 '옮겨줘야' 합니다. 예를 들어 길이가 1000인 시계열 데이터에 대해서 시각의 길이를 10개 단위로 잘라 Truncated BPTT로 학습하는 경우를 보겠습니다. 미니배치의 수를 두 개로 구성해 학습해보겠습니다. RNN 계층의 입력 데이터로, 첫 번째 미니배치 때는 처음부터 순서대로 데이터를 제공합니다. 그리고 두 번째 미니배치 때는 500번째의 데이터를 시작 위치로 정하고, 그 위치부터 다시 순서대로 데이터를 제공하는 것입니다(즉, 시작 위치를 500만큼 '옮겨'줍니다).

 

 

위 그림처럼 첫 번째 미니배치 원소는 x0, ... , x9가 되고, 두 번째 미니배치 원소는 x500, ... , x509가 됩니다. 그리고 이 미니배치 데이터를 RNN의 입력 데이터로 사용해 학습을 수행합니다. 이후로는 순서대로 진행되므로 다음에 넘길 데이터는 각각 시계열 데이터의 10~19번째 데이터와 510~519번째의 데이터가 되는 식입니다. 이처럼 미니배치 학습을 수행할 때는 각 미니배치의 시작 위치를 오프셋으로 옮겨준 후 순서대로 제공하면 됩니다.

 

5.3. RNN 구현

 

 

RNN은 위와 같은 그림으로 그릴 수 있습니다. 그림에서 보듯 우리가 다룰 신경망은 길이가 T인 시계열 데이터를 받습니다(T는 임의 값). 그리고 각 시각의 은닉 상태를 T개 출력합니다.

 

 

위 그림과 같이 상하 방향의 입력과 출력을 각각 하나로 묶으면 옆으로 늘어선 일련의 계층을 하나의 계층으로 간주할 수 있습니다. 즉, (x0, x1, ... , xT-1)을 묶은 xs를 입력하면 (h0, h1, ... ,hT-1)을 묶은 hs를 출력하는 단일 계층으로 볼 수 있습니다. 이때 Time RNN 계층 내에서 한 단계의 작업을 수행하는 계층을 'RNN 계층'이라 하고, T개 단계분의 작업을 한꺼번에 처리하는 계층을 'Time RNN계층'이라 합니다.

 

 ■ RNN 계층 구현

 

 

행렬 계산할 때는 행렬의 형상이 중요합니다. 여기서 미니 배치 크기가 N, 입력 벡터의 차원 수가 D, 은닉 상태 벡터의 차원 수가 H입니다. 이를 바탕으로 RNN 클래스의 초기화와 순전파 메서드를 구현해보겠습니다.

 

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

 

RNN의 초기화 메서드는 가중치 2개와 편향 1개를 인수로 받습니다. 각 매개변수에 대응하는 형태로 기울기를 초기화한 후 grads에 저장합니다. 마지막 역전파 계산 시 사용하는 중간 데이터를 담은 cache를 None으로 초기화합니다.

순전파인 forward(x, h_prev) 메서드에서는 인수 2개를 받습니다. 하나 앞의 RNN 계층으로부터 받는 입력이 h_prev이고, 현 시각 RNN 계층으로부터의 출력은 h_next입니다.

이번에는 RNN의 역전파와 순전파를 계산 그래프로 그리면

 

 

이와 같습니다. 이를 토대로 backward() 코드를 작성해보겠습니다.

 

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

 

이렇게 backward 함수를 구현할 수 있습니다.

 

 ■ Time RNN 계층 구현

Time RNN 계층은 T개의 RNN 계층으로 구성됩니다. RNN 계층 T개를 연결한 신경망입니다.

 

 

Time RNN 클래스로 구현을 해보겠습니다. 일단 RNN 계층의 은닉 상태 h를 인스턴스 변수로 유지하겠습니다. 이렇게 하면 Time RNN 사용자는 RNN 계층 사이에서 은닉 상태를 '인계하는 작업'을 생각하지 않아도 된다는 장점이 있습니다.

 

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

    def set_state(self, h):
        self.h = h

    def reset_state(self):
        self.h = None

 

먼저 Time RNN 계층의 코드입니다. 먼저 초기화 메서드와 다른 두 메서드를 보겠습니다. 초기화 메서드는 가중치와 편향, 그리고 stateful이라는 boolean 값을 인수로 받습니다. 인스턴스 변수 중 layers가 보이는데, 이 변수는 다수의 RNN 계층을 리스트로 저장하는 용도입니다. 그리고 인스턴스 변수 h는 forward() 메서드를 불렀을 때의 마지막 RNN 계층의 은닉 상태를 저장하고, dh는 backward() 메서드를 불렀을 때 하나 앞 블록의 은닉 상태의 기울기를 저장하겠습니다. stateful이 True일 때, Time RNN 계층이 은닉 상태를 유지한다는 뜻입니다. stateful 이 False인 경우 Time RNN 계층은 은닉 상태를 영행렬(모든 요소가 0인 행렬)로 초기화합니다.

이제 순전파를 구현하겠습니다.

 

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

 

순전파 메서드인 forward(xs)는 입력 xs를 받습니다. xs는 T개 분량의 시계열 데이터를 하나로 모은 것입니다. 따라서 미니배치 크기를 N, 입력 벡터의 차원 수를 D라고 하면, xs의 형상은 (N, T, D)가 됩니다. RNN 계층의 은닉 상태 h는 처음 호출 시 (self.h가 None일 때)에는 원소가 모두 0인 영행렬로 초기화됩니다. 그리고 stateful이 False일 때도 항상 영행렬로 초기화합니다. hs = np.empty((N, T, H), dtype = 'f') 문장에서 출력 값을 담을 그릇(hs)을 준비합니다. 이어서 총 T회 반복되는 for 문 안에서 RNN 계층을 생성하여 인스턴스 변수 layer에 추가합니다. 그 사이에 RNN 계층이 각 시각 t의 은닉 상태 h를 계산하고, 이를 hs에 해당 인덱스의 값으로 설정합니다.

RNN 계층의 순전파에서는 출력이 2개로 분기되고, 역전파에서는 각 기울기가 합산되어 전해집니다. 따라서 역전파 시 RNN 계층에는 합산된 기울기가 입력으로 오느데 이를 주의해야 합니다. 이번에는 backward를 구현하겠습니다.

 

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

 

하류로 흘려보낼 기울기를 담을 그릇인 dxs를 만듭니다. 그리고 순전파 때와는 반대 순서로 RNN 계층의 backward() 메서드를 호출하여, 각 시각의 기울기 dx를 구해 dxs의 해당 인덱스에 저장합니다. 그리고 가중치 매개변수에 대해서도 각 RNN 계층의 가중치 기울기를 합산하여 최종 결과를 멤버 변수 self.grads에 덮어씁니다.

 

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

 ■ RNNLM(RNN Language Model)의 전체 그림

 

 

이 그림은 단순한 RNNLM의 신경망을 그린 모습입니다. 왼쪽은 RNNLM의 계층 구성이고, 오른쪽에는 이를 시간축으로 펼친 신경망입니다.

첫 번째 층은 Embedding 계층입니다. 이 계층은 단어 ID를 단어의 분산 표현(단어 벡터)으로 변환합니다. 그리고 그 분산 표현이 RNN 계층으로 입력되고, RNN 계층은 은닉 상태를 다음 층으로(위쪽으로) 출력함과 동시에 다음 시각의 RNN 계층으로(오른쪽으로) 출력합니다. 그리고 RNN 계층이 위로 출력한 은닉 상태는 Affine 계층을 거쳐 Softmax 계층으로 전해집니다.

RNNLM은 지금까지 입력된 단어를 기억하고, 그것을 바탕으로 다음에 출현할 단어를 예측합니다.  RNN 계층이 과거에서 현재로 데이터를 계속 흘려보내 줌으로써 과거의 정보를 인코딩해 저장할 수 있습니다.

 

 ■ Time 계층 구현

지금까지는 시계열 데이터를 한꺼번에 처리하는 계층을 Time RNN이라는 이름의 계층으로 구현했습니다. 이번에도 시계열 데이터를 한꺼번에 처리하는 계층을 Time Embedding, Time Affine 형태의 이름으로 구현하겠습니다.

Time Embedding 계층은 순전파 시에 T개의 Embedding 계층을 준비하고 각 Embedding 계층이 각 시각의 데이터를 처리합니다.

Time Affine 계층은 Affine 계층을 T개 준비해서, 각 시각의 데이터를 개별적으로 처리하면 됩니다.

이어서 Softmax를 살펴보겠습니다. Softmax 계층을 구현할 때 손실 오차를 구하는 Cross Entropy Error 계층도 함께 구현할 것이고 여기서는 Time Softmax with Loss 계층으로 구현할 것입니다.

 

 

위 그림과 같이 표현할 수 있습니다. 여기서 x0나 xt 등의 데이터는 아래층에서부터 전해지는 '점수'를 나타냅니다. 또한 t0나 t1 등의 데이터는 정답 레이블을 의미합니다. T개의 Softmax with Loss 계층 각각이 손실을 산출합니다. 그리고 그 손실을 합산해 평균한 값이 최종 손실이 됩니다.

 

5.5. RNNLM 학습

먼저 RNNLM에서 사용하는 신경망을 SimpleRnnlm이라는 이름의 class로 구현하겠습니다. 4개의 Time 계층을 쌓은 신경망으로 만들겠습니다.

 

# coding: utf-8
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
        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로 설정했습니다. 그 결과 Time RNN 계층은 이전 시각의 은닉 상태를 계층 할 수 있게 됩니다. 또한 초기화 코드는 RNN 계층과 Affine 계층에서 'Xavier 초깃값'을 이용했습니다.

계속해서 forward와 backward는 그대로 각각의 계층에서 순전파와 역전파를 진행합니다.

마지막으로 RNNLM의 학습을 하는 코드를 작성해보겠습니다.

 

# coding: utf-8
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)

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

 

이와 같이 작성할 수 있습니다. 미니배치에서 데이터를 읽는 시작 위치를 조정하는 부분도 구현되어 있습니다. offsets의 각 원소에 데이터를 읽는 시작 위치가 담기게 됩니다.