RNN

15 minute read

언어모델

RNN을 알아보기 전에 언어모델 에 대하여 먼저 알아보자. 언어모델 이란 단어 나열에 확률을 부여하여 특정한 단어의 시퀀스에 대해서, 그 시퀀스가 일어날 가능성이 어느 정도인지를 확률로 평하하는 것 이다.
이러한 언어모델을 수식으로 살펴보면 아래와 같다.
\(w_1, .., w_m\)이라는 m개의 단어로 된 문장이 있다고 하였을때, \(w_1, .., w_m\)이 차례로 출현할 확률은 P(\(w_1, .., w_m\))이다.
위의 식을 풀어서 쓰면 아래와 같다.

$$ \begin{multline} P(w_1, .., w_m) \\ = P(w_m|w_1, .., w_{m-1})P(w_{m-1}|w_1, .., w_{m-2}) ... P(w_2|w_1)P(w_1) \end{multline} $$

$$=\prod_{t=1}^m P(w_t|w_1, ..., w_{t-1})$$

위의 식을 정리하게 전에 아래 확률곱셈정리를 보게 되면 아래와 같은 식이 된다.

$$P(A,B) = P(A|B)P(B)$$

위의 의미는 즉 A와 B가 모두 일어날 확률(P(A,B)) 는 B가 일어날 확률 P(B)와 B가 일어난 후 A가 일어날 확률 P(A|B)를 곱한 값과 같다는 것이다.
위의 식을 사용하게 되면 아래의 식을 다음과 같이 정리할 수 있다.

$$P(w_1, ..., w_{m-1},w_m) = P(A,w_m) = P(w_m|A)P(A)$$

위의 식에서 \(P(w_m)\)을 사후 확률이라 하고 \(P(A)\)를 타겟 확률이라 할때 사후확률은 타깃단어보다 왼쪽에 있는 모든 단어를 맥락으로 했을 때의 확률 이다.

이러한 언어 모델을 사용하기 위하여 지난 POST에서 올릴 CBOW Model을 살펴보게 되면 아래와 같이 나타낼 수 있다.

$$ \begin{multline} P(w_1, .., w_m) \\ = \prod_{t=1}^m P(w_t|w_1, ..., w_{t-1}) \approx \prod_{t=1}^m P(w_t|w_{t-2},w_{t-1}) \end{multline} $$

위와 같이 CBOW를 언어 Model로서 사용하게 되면 Window size 때문에 이전정보는 가져올 수 없다는 단점과 맥락안의 단어 순서 무시가 되는 단점이 발생한다.
위와 같은 단점을 해결하기 위한 Model이 RNN이다.


RNN

RNN(Recurrent Neural Network)이란 순차적인 정보를 처리하는 데 있다.
즉, 이전 까지 반복하였던 상관없는 두 변수간의 값으로 인한 정보를 처리하는 것이 아닌 한 정보에 대한 특정 Domain의 값을 나타내는 정보를 처리하는 것 이다.
예를 들어, 문장에서 다음에 나올 단어를 추측하고 싶다면 이전에 나온 단어들을 아는 것이 큰 도움이 될 것이다.
또한 집의 가격이 어떻게 변할지에 대하여 한 집의 가격을 계속 관찰하게 되면 집의 가격이 시간(Domain)에 따라 어떻게 변하는지(시계열 데이터) 예측할 수 있을 것이다.
위의 예시가 가능한 이유는 동일한 Task에 대하여 하나의 Hidden Layer를 계속하여 Trainning 하기 떄문이다.
출력 결과는 이전의 계산 결과에 영향을 받기 때문에 RNN은 현재지 계산된 결과에 대한 “메모리” 정보를 갖고 있다고 볼 수도 있다.
RNN의 구조는 아래와 같이 나타낸다.

  • \(x_t\)는 시간 스텝(time step) t 에서의 입력값이다.
  • \(x_t\)는 시간 스텝(time step) t 에서의 Hidden state이다. 네트워크의 “메모리” 부분으로서, 이전 시간의 스텝의 hidden state 값과 현재 시간 스텝의 입력값에 의해 계산된다.
    \(s_t = f(Ux_t + Ws_{t-1}) \text{ f는 tanh or ReLU}\)
  • \(o_t\)는 시간 스텝(time step)에서의 출력값이다. 예를 들어 다음 단어를 추축하고 싶다면 단어 수만큼의 차원의 확률 벡터가 될 것이다. \(o_t = softmax(Vs_t)\)

몇 가지 짚어두고 넘어갈 점이 있다.

  • Hidden state s_t는 네트워크의 메모리라고 생각할 수 있다. - s_t는 과거의 시간 스텝들에서 일어난 일들에 대한 정보를 전부 담고 있고, 출력값 o_t는 오로지 현재 시간 스텝 t의 메모리에만 의존한다. 하지만 위에서 잠깐 언급했듯이, 실제 구현에서는 너무 먼 과거에 일어난 일들은 잘 기억하지 못한다.
  • 각 layer마다의 파라미터 값들이 전부 다 다른 기존의 deep한 신경망 구조와 달리, RNN은 모든 시간 스텝에 대해 파라미터 값을 전부 공유하고 있다 (위 그림의 U, V, W). 이는 RNN이 각 스텝마다 입력값만 다를 뿐 거의 똑같은 계산을 하고 있다는 것을 보여준다. 이는 학습해야 하는 파라미터 수를 많이 줄여준다.
  • 위 다이어그램에서는 매 시간 스텝마다 출력값을 내지만, 문제에 따라 달라질 수도 있다. 예를 들어, 문장에서 긍정/부정적인 감정을 추측하고 싶다면 굳이 모든 단어 위치에 대해 추측값을 내지 않고 최종 추측값 하나만 내서 판단하는 것이 더 유용할 수도 있다. 마찬가지로, 입력값 역시 매 시간 스텝마다 꼭 다 필요한 것은 아니다. RNN에서의 핵심은 시퀀스 정보에 대해 어떠한 정보를 추출해 주는 hidden state이기 때문이다.
    출처: aikorea

위와 같은 RNN Forward는 아래와 같은 Code로서 나타낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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 BackPropagation

RNN을 아래와 같은 그림으로 같단히 나타내어 보자.


위와 같은 그림에서 다음과 같은 식을 정의하고 가자
Activation Function: tanh
Classifier: Softmax

$$s_t = tanh(Ux_t + Ws_{t-1})$$

$$\hat{y_t} = softmax(Vs_t) $$

$$y_t \text{: 시간 스텝 t 에서 실제 단어, } \hat{y_t} \text{: 예측값}$$

Loss Function: Cross Entropy

$$E(y_t,\hat{y_t}) = -y_t log(\hat{y_t})$$

$$E(y,\hat{y}) = -\sum_t{E(y_t,\hat{y_t})}$$

$$= -\sum_t{-y_t log(\hat{y_t})}$$

Parameter U, V, W 에 대한 Error 의 Gradient 를 계산하고 SGD를 이용하여 Parameter를 최적화 하여 Loss를 적게 만드는 것이 목표이다.

1. Parameter V

$$\frac{\partial E_3}{\partial V} = \frac{\partial E_3}{\partial \hat{y_3}} \frac{\partial \hat{y_3}}{\partial V}$$

$$= \frac{\partial E_3}{\partial \hat{y_3}} \frac{\partial \hat{y_3}}{\partial z_3} \frac{\partial z_3}{\partial V} $$

$$= (\hat{y_3} - y_3) \bigotimes s_3$$

$$ z_3 = Vs_3$$

위의 식에서 \(\frac{\partial E_3}{\partial \hat{y+3}} \frac{\partial \hat{y_3}}{\partial z_3}\)의 경우 Softmax-with-Loss의 역전파로서 계산 과정을 건너 뛰었다.
Softmax-with-Loss의 역전파

위의 식에서 주목해야 할 점은 \(\frac{\partial E_3}{\partial V}\)은 현재 시간 스탭의 \(\hat{y_3}, y_3, s_3\)에만 의존한다는 것이다.
V Parameter를 갱신하는 것은 현재 시간 스탭의 값만 알아도 수행할 수 있다는 점 이다.

2. Parameter W, U
W, U에 대해서 정리하면 V처럼 현재 시간 스탭의 값만 알아도 수행할 수 없다는 것을 알 수 있다.
아래의 식으로서 살펴보자

$$\frac{\partial E_3}{\partial W} = \frac{\partial E_3}{\partial \hat{y_3}} \frac{\partial \hat{y_3}}{\partial s_3} \frac{\partial s_3}{\partial W}$$

여기서 \(s_t = tanh(Ux_t + Ws_{t-1})\)이므로 \(s_3\)\(s_2\)에 의존하고 \(s_2\)\(s_1\)에 의존하는 현상이 발생하게 된다.
이러한 상황으로 인하여 Chain Rule이 계속해서 이어지는 것을 알 수 있다.

아래 식을 살펴보게 되면 Chain Rule을 적용한 식을 알 수 있다.

$$\frac{\partial E_3}{\partial W} = \sum_{k=0}^{3} \frac{\partial E_3}{\partial \hat{y_3}} \frac{\partial \hat{y_3}}{\partial s_3} \frac{\partial s_3}{\partial s_k} \frac{\partial s_k}{\partial W} $$

위의 식을 살펴보게 되면 각 시간 스텝이 gradient에 기여하는 것을 전부 더해준다.
즉, W는 우리가 현재 처리중인 출력 부분까지의 모든 시간 스템에서 사용되기 때문에, t=3 부터 t=0 까지 gradient들을 전부 backpropagat해 주어야 한다.


위를 살펴보게 되면 기존 Neural Network에서 적용되는 backpropagate의 과정과 같은 것을 알 수 있다.

$$z_t = Ux_t + WS_{t-1} \text{이라고 치환}$$

$$\delta_3^3 = \frac{\partial E_3}{\partial z_3}$$

$$ = \frac{\partial E_3}{\partial \hat{y_3}} \frac{\partial \hat{y_3}}{\partial z_3}$$

$$\text{softmax_with_crossentropy backpropagation을 적용하면}$$

$$ = (\hat{y_3} - y_3)s_3$$

$$\delta_2^3 = \frac{\partial E_3}{\partial z_2}$$

$$ = \frac{\partial E_3}{\partial z_3} \frac{\partial z_3}{\partial s_2} \frac{\partial s_2}{\partial z_2}$$

$$= \delta_3^3 \frac{\partial z_3}{\partial s_2} \frac{\partial s_2}{\partial z_2}$$

$$\delta_1^3 = \frac{\partial E_3}{\partial z_1}$$

$$ = \frac{\partial E_3}{\partial z_2} \frac{\partial z_2}{\partial s_1} \frac{\partial s_1}{\partial z_1}$$

$$= \delta_2^3 \frac{\partial z_2}{\partial s_1} \frac{\partial s_1}{\partial z_1}$$

위의 식을 유도하였으면 아래와 같은 식을 최종적으로 얻어 낼 수 있다.
i 는 특정 시간 스탭이라고 하면

$$\frac{\partial E_3}{\partial U} = \delta_i^3 x_i^{T}$$

$$\frac{\partial E_3}{\partial W} = \delta_i^3 s_{i-1}^{T}$$

RNN이므로 계산된 값을 모두 더해주는 것을 말고는 Neural Network의 Backpropagation과 같은 식이 유도되는 것을 알 수 있다.
위와 같은 식에서 Activation Function을 tanh을 사용하면 Backpropagation은 아래와 같이 나타낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache

        dt = dh_next * (1 - h_next ** 2) #tanh 미분
        db = np.sum(dt, axis=0) # 더하기 미분
        dWh = np.dot(h_prev.T, dt) # x 미분 (h_{t-1} x w_h)
        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



Truncated BPTT

위와 같은 쭉 이어진 RNN의 Backpropagation을 BPTT(Backpropagation Through Time)이라고 한다.
위와같은 Backpropagation은 문제가 발생하게 된다.

  1. Gradinet가 무한대로 발산하거나, 무한히 작아질 수 있다(vainshing and exploding gradients)
  2. RNN의 길이가 많이 길 수록 Error가 이전 시간으로 전달될 때 많이 희석된다.

이러한 문제를 해결하기 위하여 장기 기억을 적절한 단위로 쪼개서 학습시키자는 아이디어. 이는 vanishing and exploding gradients문제를 해결하면서, 동시에 특정 인풋에 적합한 시간 window를 사용하자는 것으로 해석이 가능하다.

이러한 대표적인 방식으로 Truncated BPTT가 존재한다.
아래 그림을 살펴보게 되면 Truncated BPTT를 어떻게 구현하는지 알 수 있다.


위의 그림과 같이 시계열 데이터를 특정 시간으로 잘라 옮겨주는 작업을 통해 구현될 수 있다.
Forward는 반드시 이어질 것을 요구하지만 Forward의 경우에도 특정 길이 이상은 Long Term Dependency가 보장될 확률이 매우 낮으므로 Batch처리에서 아래 그림과 같이 Forward의 경우에도 잘라서 학습을 진행한다.


Backward는 Forward와 달리 bainshing and exploding gradients문제 때문에 적절한 Window로 잘라서 학습시키는 것이 좋은 성능을 보여준다. 아래 그림은 Truncated BPTT의 Backporpagation이다.

Truncated BPTT는 아래와 같이 구현될 수 있다.
Batch Truncated BPTT 초기화

  • stateful: Truncated BPTT를 유지할 것인지 아닌지를 결정
  • layers: 다수의 RNN계층을 리스트로 저장(Batch 처리)
  • h: forward() Method호출 시 마지막 RNN 계층의 은닉상태 저장
  • dh: backward() Method호출 시 하나 앞 블록의 은닉 상태의 기울기를 저장
1
2
3
4
5
6
7
8
9
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

Forward 구현

  • N: 미니 배치 크기
  • T: RNN Layer를 한번에 처리하는 갯수
  • D: 입력 벡터의 차원 수
  • H: Hidden Layer의 Size
  • hs: Batch 처리와 RNN Layer를 묶은 Output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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)

Backward 구현

  • N: 미니 배치 크기
  • T: RNN Layer를 한번에 처리하는 갯수
  • D: 입력 벡터의 차원 수
  • H: Hidden Layer의 Size
  • hs: Batch 처리와 RNN Layer를 묶은 Output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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

현재 Layer에서는 RNN BackPropagation에서 softmax를 지나기 전이므로 Update 시켜야 하는 Parameter는 W, U이다. 이러한 두 Parameter는 현재 처리중인 출력 부분까지의 모든 시간 Step에서 사용되기 때문에 BackPropataion의 값을 계속해서 전달해야 한다.
여기에서는 Batch Truncated BPTT를 사용하였으므로 시계열 크기(T)에 대해서만 생각한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    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



Truncated BPTT 구현

위에서는 하나의 Layer를 통하여 Truncated BPTT를 어떻게 구성할지에 대해서 알아보았다.
이제 실질적으로 위에서 선언한 Layer를 활용하여 어떻게 Train을 하고 Loss를 구하여 Weight를 Update시킬지 알아보자.
최종적으로 구현하고자 하는 신경망은 아래 그림과 같다.


위에서 구현한 Layer에 들어가기전 Input을 이전 Post에서 다루었던 Embedding 계층을 통하여 단어 분산 표현으로서 나타낸다.
Embedding 계층을 거친 Input은 Layer를 통하여 최종적인 Softmax계층을 통하여 어떤 Output을 뽑아낼지 결정하게 된다.
위와같은 RNN의 구조는 CBOW로서는 할 수 없었던 과거에서 현재로 데이터를 계속 흘려보내줌으로써 과거의 정보를 인코딩해 저장할 수 있게 된다.

최종적인 Loss를 구할시에도 위에서 구한 Loss를 사용하기에는 무리가 있다.
결국에 T개의 RNN Layer를 합하여 하나의 새로운 Layer를 구성하였으므로 최종적인 Layer는 일반 ANN에서의 Loss와 마찬가지로 아래 식으로서 표현할 수 있다.

$$L = \frac{1}{T}(L_0 + L_1 + ... + L_{T-1})$$

최종적인 Layer를 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 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()

위에서의 Code에서 가중치의 초기화는 Activation Function이 tanh이므로 Xavier 초기값을 사용하였다.
Xavier 자세한 내용


언어 Model 평가 방법

대표적인 언어 Model의 평가 방법은 Perplexity(퍼플렉시티, PPL)이 있다.
PPL은 정량 평가 방법의 하나로서 언어모델 상에서 테스트 문장들의 점수를 구하고, 이를 기반으로 언어모델의 성늘을 측정한다.
PPL은 문장의 확률에 길이에 대해서 normalization한 값이라고 볼 수 있다.
PPL은 아래와 같은 식으로서 정리된다.

$$PPL(w_1, w_2, ..., w_n) = P(w_1, w_2, ..., w_n)^{-\frac{1}{n}}$$

$$= \sqrt[n] {\frac{1}{P(w_1, w_2, ..., w_n)}}$$

위의 식을 위에서 정리한 CBOW Model이 n개의 window size가 적용한다면 아래 식으로서 표현할 수 있다.

$$\approx \sqrt[n] {\frac{1}{\prod_{i=1}^n P(w_i|w_{i-n+1}, ..., w_{i-1})}}$$

위와 같은 PPL은 아래와 같은 예시로서 의미를 파악할 수 있다.
만약 Text = you say goodbye and i say hello를 corpus를 생각한다면
you를 넣었을때 say가 나올 확률이 각각 0.2, 0.8이라고 가정하자

$$PPL(0.2) =\frac{1}{0.2} = 5$$

$$PPL(0.8) =\frac{1}{0.8} = 1.25$$

즉 PPL의 값은 낮을수록 좋은 Model이라는 것을 알 수 있고 PPL의 값은 뻗어나갈 수 있는 branch의 숫자를 의미한 다는 것을 알 수 있다.

우리의 최종적인 목표는 Loss Function을 설정하여 Weight를 Update를 시키는 것 이다.
Weight를 Update시키기 위하여 언어 Model에서 Cross Entropy와 PPL의 관계를 살펴보자.

언어모델의 분포 \(P(x)\) 또는 출현 가능한 문장들의 집합 \(W\) 에서 길이 n의 문장 \(w_{1:n}\) 을 샘플링 하였을 때, 우리의 언어모델 분포 \(P_\theta(x)\) 의 엔트로피를 나타내면 아래와 같습니다.

$$H_n(P,P_\theta) = -\sum_{w_{1:n} \in W} P(w_{1:n})logP(w_{1:n}) ...(1)$$

$$\approx -\frac{1}{k}\sum_{i=1}^{k}logP_\theta(w^{i}_{1:n}) ...(2)$$

$$\approx -logP_\theta(w_{1:n})$$

$$= -\sum_{i=1}^{n}logP_\theta(w_i|w_{ < i }) ...(3)$$

$$\approx -\frac{1}{n}\sim_{w_{1:n} \in W}\sum_{i=1}P(w_i|w_{ < i })logP_\theta(w_i|w_{ < i }) ...(4)$$

$$\approx -\frac{1}{n}\sum_{i=1}^n logP(w_i|w_{ < i })$$

$$= -\frac{1}{n}log \prod_{i=1}^n P_\theta(w_i|w_{ < i })$$

$$=\sqrt[-\frac{1}{n}]{log(\prod_{i=1}^n P_\theta(w_i|w_{ < i }))}$$

$$=\sqrt[n]{log(\frac{1}{\prod_{i=1}^n P_\theta(w_i|w_{ < i })})}$$

위와 같은 식을 아래 PPL식과 비료하여서 살펴보게 되면

$$PPL(w_{1:n}) = \sqrt[n] {\frac{1}{\prod_{i=1}^n P(w_i|w_{i-n+1}, ..., w_{i-1})}}$$

최종적인 식으로서 아래식을 얻을 수 있다.

PPL = exp(CrossEntropy)

(1)식에서 (2)식, (3)식에서 (4)식으로 넘어갈때 몬테카를로 샘플링을 통하여 근사화한 값으로 표현한 것이다.
몬테카를로 샘플링이란 아래 식을 의미한다.

$$\int f(x)p(x), dx \approx \sum_{i=1}^{N}\frac{f(X_i)}{N}$$

위의 식에서 각각의 Parameter는 다음과 같다.

  • \(f(x)\): 함수
  • \(p(x)\): 확률분포
  • \(N\): 함수: 표본의 개수
  • \(X_i\): i번째 표본값

즉 위의 식의 의미는 표본은 확률분포를 반영하므로, 전체 표본에서 각 상태의 출연 횟수는 자신의 확률분포 값에 비례하게 된다. 즉, \(X_i\)가 표본이기 때문에, 샘플이기 때문에 \(p(x)\)를 곱한 효과를 낼 수 있다.
몬테카를로에 대한 자세한 내용은 아래 참고
몬테카를로 자세한 내용

위와같은 과정을 Code로서 Trainner를 구성하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class RnnlmTrainer:
    def __init__(self, model, optimizer):
        self.model = model
        self.optimizer = optimizer
        self.time_idx = None
        self.ppl_list = None
        self.eval_interval = None
        self.current_epoch = 0

    def get_batch(self, x, t, batch_size, time_size):
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')

        data_size = len(x)
        jump = data_size // batch_size
        offsets = [i * jump for i in range(batch_size)]  # 배치에서 각 샘플을 읽기 시작하는 위치

        for time in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, time] = x[(offset + self.time_idx) % data_size]
                batch_t[i, time] = t[(offset + self.time_idx) % data_size]
            self.time_idx += 1
        return batch_x, batch_t

    def fit(self, xs, ts, max_epoch=10, batch_size=20, time_size=35,
            max_grad=None, eval_interval=20):
        data_size = len(xs)
        max_iters = data_size // (batch_size * time_size)
        self.time_idx = 0
        self.ppl_list = []
        self.eval_interval = eval_interval
        model, optimizer = self.model, self.optimizer
        total_loss = 0
        loss_count = 0

        start_time = time.time()
        for epoch in range(max_epoch):
            for iters in range(max_iters):
                batch_x, batch_t = self.get_batch(xs, ts, batch_size, time_size)

                # 기울기를 구해 매개변수 갱신
                loss = model.forward(batch_x, batch_t)
                model.backward()
                params, grads = remove_duplicate(model.params, model.grads)  # 공유된 가중치를 하나로 모음
                if max_grad is not None:
                    clip_grads(grads, max_grad)
                optimizer.update(params, grads)
                total_loss += loss
                loss_count += 1

                # 퍼플렉서티 평가
                if (eval_interval is not None) and (iters % eval_interval) == 0:
                    ppl = np.exp(total_loss / loss_count)
                    elapsed_time = time.time() - start_time
                    print('| 에폭 %d |  반복 %d / %d | 시간 %d[s] | 퍼플렉서티 %.2f'
                          % (self.current_epoch + 1, iters + 1, max_iters, elapsed_time, ppl))
                    self.ppl_list.append(float(ppl))
                    total_loss, loss_count = 0, 0

            self.current_epoch += 1

    def plot(self, ylim=None):
        x = numpy.arange(len(self.ppl_list))
        if ylim is not None:
            plt.ylim(*ylim)
        plt.plot(x, self.ppl_list, label='train')
        plt.xlabel('반복 (x' + str(self.eval_interval) + ')')
        plt.ylabel('퍼플렉서티')
        plt.show()



결과 확인

최종적으로 Train할 Layer는 다음 Code와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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()

Train 결과 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 하이퍼파라미터 설정
batch_size = 10
wordvec_size = 100
hidden_size = 100  # RNN의 은닉 상태 벡터의 원소 수
time_size = 5  # RNN을 펼치는 크기
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:]  # 출력(정답 레이블)

# 모델 생성
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)
trainer.plot()



참조: 원본코드
참조: Chanwoo Timothy Lee Youtube
참조: aikorea
참조:ratsgo Blog
참조: 밑바닥부터 시작하는 딥러닝2
코드에 문제가 있거나 궁금한 점이 있으면 wjddyd66@naver.com으로 Mail을 남겨주세요.

Categories:

Updated:

Leave a comment