본문 바로가기

Deep learning from scratch

RNN (Recurrent Neural Network)


먼저 해당 섹션의 포스팅들은 사이토 고키의 밑바닥부터 시작하는 딥러닝 2를 참조하여 작성되었음을 밝힙니다. 영상처리 분야와 더불어 기계 번역, 음성 인식 등 실생활에 큰 영향을 주는 기술의 근간에는 자연어 처리 분야가 활용되고 있는 것을 볼 수 있습니다. 더 나아가 멀티 모달리티 분야에서 딥러닝을 적용하여 문제를 해결하기 위해서는 자연어 처리와 시계열 데이터 처리에 대하여 추가로 학습할 필요가 있다고 생각하였기에 밑바닥부터 시작하는 딥러닝 2과 Github 레포지토리를 참조하여 공부해보고자 합니다.

 

1. RNN (Recurrent Neural Network)의 필요성

앞서 공부하였던 word2vec과 같이 단어의 분산 표현을 얻어내기 위한 신경망은 언어모델로 사용하였을 경우 큰 문제점이 존재합니다. 그것은 바로 시계열 데이터를 잘 다루지 못한다는 것입니다. 먼저 word2vec과 같은 모델은 고정 길이의 window size를 사용하기 때문에 지정된 크기를 넘어서는 단어의 정보는 고려할 수 없습니다. 이는 시계열 데이터를 학습하는데 치명적인 문제가 됩니다. 또한 해당 모델은 각 단어의 맥락 정보를 가중치 파라미터를 통해 연산한 후에 모두 더하여 평균을 내어 은닉층으로 전달하기 때문에 맥락 속 단어의 순서를 고려할 수 없다는 문제도 가지고 있습니다. 이러한 문제들을 해결하기 위하여 RNN(Recurrent Neural Network)이 제안되었습니다. 

 

2. RNN과  Truncated BPTT (Backpropagation Through Time)

그림 1. RNN layer의 순환 구조 도식

RNN는 recurrent라는 단어의 의미에 따라 신경망이 순환하는 것이 특징입니다. 순환을 하기 위해서는 닫힌 경로가 필요하며, 이러한 경로는 그림 1.의 RNN layer와 같이 시간 축을 통해 지정됩니다. 이에 따라 데이터는 은닉 상태(hidden state) $h$에 저장되고, 경로를 순환하며 갱신됩니다. 데이터가 순환함으로써 과거의 정보를 사용하며, 최근의 정보를 계속적으로 저장할 수 있습니다. RNN layer의 순전파 연산은 $\mathbf{h}_{t} = \tanh{\left( \mathbf{h}_{t-1} \mathbf{W}_{\mathbf{h}} + \mathbf{x}_{t} \mathbf{W}_{\mathbf{x}} + \mathbf{b} \right)}$을 연속적으로 수행하여 진행됩니다. 즉, RNN layer은 시계열 데이터인 $x_t$와 이전 layer의 은닉 상태에 해당하는 $h_{t-1}$를 입력으로 받아 가중치와 편향을 통해 연산하고, $tanh$를 통해 은닉 상태 $h_t$를 출력합니다. 마찬가지로 다음 셀에서는 이전 은닉 상태 $h_t$와 입력 데이터 $x_{t+1}$을 통해 연산을 연속적으로 진행합니다. 

 

그림 2. RNN layer의  Truncated BPTT (Backpropagation Through Time) 도식

RNN의 오차 역전파는 그림 2.와 같은 순서로 진행됩니다. 이를 시간을 통한 오차 역전파라고 하여 줄여서 BPTT라고 부릅니다. RNN은 시계열 데이터를 입력으로 받아 닫힌 경로를 지정하고, 시간 축에 따라 연산을 수행하는 것이 특징입니다. 만약 RNN layer에서 매우 큰 시계열 데이터를 사용한다고 한다면, 오차 역전파 과정에서 많은 연산량을 소모할 것이며, 기울기 소실 문제가 발생할 수 있습니다. 이를 해결하기 위하여 역전파를 수행할 때는 그림 2.와 같이 부분을 나누어 BPTT를 수행하게 됩니다. RNN이 학습하는 가중치 파라미터 이전 layer을 통해 들어온 은닉 상태에 대한 $W_h$, 현재 시점에 입력되는 시계열 데이터에 대한 $W_x$, $b$으로 그림 3.과 같습니다.

그림 3. RNN layer의 오차 역전파(좌)와 전체적인 순환 구조 도식(우)

 

3. Reference code

해당 코드는  Github 레포지토리를 참조하였습니다.

class TimeRNN:
    def __init__(self, Wx, Wh, b, stateful=False):
        """
        RNN 내부의 가중치 파라미터와 그래디언트를 초기화 한다.
        """
        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 forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        D, H = Wx.shape

        self.layers = []
        """
        Time 축에 따른 hidden state들이 저장될 텐서를 초기화 한다.
        """
        hs = np.empty((N, T, H), dtype='f')
		
        """
        첫 입력 데이터에 대하여 hidden state 행렬을 초기화 한다.
        Hidden state 행렬의 전달을 확인한다.
        """
        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype='f')
		
        """
        정의된 시간 T 만큼 RNN layer를 통과시킨다.
        """
        for t in range(T):        
            """
            RNN layer에 가중치 파라미터를 매개변수로 넣어준다.
            (type(self.params) == list)
            """
            layer = RNN(*self.params)
            """
            Hidden state는 업데이트 되며, 과거의 상태는 hs 텐서에 저장된다.
            """
            self.h = layer.forward(xs[:, t, :], self.h)
            hs[:, t, :] = self.h
            self.layers.append(layer)

        return hs

    """
    Truncated BPTT
    """
    def backward(self, dhs):
    	"""
        RNN 내부의 가중치 파라미터 초기화
        """
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        """
        D, H 확인 필요
        """
        D, H = Wx.shape


        dxs = np.empty((N, T, D), dtype='f')
        dh = 0
        """
        BPTT를 수행하며 그래디언트([Wx, Wh, b])를 초기화 한다.
        """
        grads = [0, 0, 0]
        
        """
        Inverse order로 진행한다.
        """
        for t in reversed(range(T)):
            layer = self.layers[t]
            """
            뒷단의 RNN layer에서 들어온 gradeint와 현재 layer의 그래디언트의 합을 역전파 한다.
            dx (dx_prev) -> 해당 RNN layer w_x의 그래디언트의
            dh (dh_prev) -> 오차 역전파 과정에서 앞단의 RNN layer로 들어갈 그래디언트의
            """
            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
        
        """
        다음 Truncated BPTT를 수행하기 위해 사용될 dh를 클래스 인스턴스로 저장한다.
        """
        self.dh = dh

        return dxs

 

 

4. RNN의 문제점

RNN의 역전파 과정에서는 같이 그림 3.에서 $tanh$ 노드와 더하기 노드 행렬곱 노드를 차례로 거치게 됩니다. $tanh$ 함수의 미분 값은 항상 0과 1사이의 값이 되기 때문에 연속적으로 truncated BPTT를 수행하게 되면, 그래디언트 값이 작아지게 될 것입니다.  또한 행렬곱 노드에서 다음 RNN layer에 전해질 그래디언트는 항상 $W_h$과 곱해지게 됩니다. 이에 따라 그래디언트 값이 매우 커지거나 작아질 것입니다. 즉, 이러한 인하여 RNN으로 해결하고자 하였던 시계열 데이터 처리를 제대로 수행할 수 없게 됩니다. 이를 해결하고자 하여 제안된 것이 게이트를 추가한 RNN입니다. 다음 포스팅에서는 그 종류 중 하나인 LSTM에 대하여 살펴보겠습니다.

 


이렇게 RNN에 대하여 살펴보았습니다. 질문이나 지적 사항은 댓글로 남겨주시면 감사하겠습니다.