ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [밑시딥2] 게이트가 추가된 RNN
    A.I/Study 2023. 7. 14. 04:12

    RNN의 문제점

    기본적인 RNN은 장기 의존 관계를 학습하기 어렵다.
    BPTT에서 기울기 소실 혹은 폭발이 일어나기 때문이다.

    RNN 복습

    RNN 레이어는 순환 경로를 가지고 있고, 이를 펼치면 옆으로 길게 뻗은 신경망이 된다.
    시계열 데이터인 x_i를 입력하면 h_t를 출력하며, 이 h_t는 은닉 상태라고 한다.
    이 은닉상태를 통해 과거 정보를 저장한다.

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

    문장이 길어지먄 길어질수록 이후에 앞의 단어에 대한 기울기가 점점 옅어진다.
    이렇게 된다면, 가중치 파라미터가 전혀 갱신되지 않게 된다.
    이와 반대로 커지는 경우도 발생하는데, 이를 기울기 폭발이라고 한다.

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

    tanh 미분시 x와 멀어질수록 0에 가까워진다.

    tanh 그래프

    역전파에서 tanh노드를 지날 때마다 점점 기울기도 감속한다.

     

    또한 MatMul을 통과하는 과정에서도 문제가 발생한다.

    L2 norm을 구해서 시간에 따른 크기를 시각화하면 다음과 같다.

    norm이 1보다 클 때의 변화량
    norm이 1보다 작을 때의 변화량

    반복해서 곱 할수록 기울기가 점점 커지거나 작아진다.

    기울기가 지수적으로 증가하는 것을 exploding gradient(기울기 폭발)라고 하며 점점 감소해서 0에 가까워지는 것을 vanishing gradient(기울기 소실)이라고 한다.

     

    위는 scalar 값을 사용했지만, matrix의 경우 특잇값이 척도가 된다.

    기울기 폭발 대책

    기울기 폭발 대책으로 기울기 클립핑(gradients clipping)을 사용한다.

    기울기 클리핑 수식

    기울기가 threshold보다 크다면 아래의 수식 같이 기울기를 수정한다.

    기울기 소실과 LSTM

    LSTM의 인터페이스

    RNN과 LSTM의 인터페이스 비교

    인터페이스 비교

    c라는 경로가 있다는 것이 차이이며 이를 기억 셀(memory cell)이라고 한다.

    이 기억 셀의 특징은 데이터를 LSTM 레이어 내에서만 주고 받는다는 것이다.

     

    output 게이트

    다음 은닉 상태 h_t의 출력을 담당하는 게이트이다.

    tanh(c_t)의 각 원소에 대해 그것이 다음 시각의 은닉 상태에 얼마나 중요한가를 조정한다.

    output 게이트 수식

    RNN의 현재 시각의 은닉 상태를 구하는 것과 동일하지만, 시그모이드 함수를 거쳐 출력게이트로 표현한다는 것이 다르다.

    h_t는 o와 tanh(c_t)의 곱으로 계산될 수 있는데 이는 element-wise 곱 즉 hadamard product라고 한다.

    현재 시각의 은닉 상태 수식

    forget 게이트

    c_{t-1}의 기억 중 불필요한 기억을 잊게 해주는 게이트이다.

    forget 게이트 수식

    위와 비슷하게 c_t를 f와 c_{t-1}의 hadamard product를 이용해서 구한다.

    새로운 기억 셀

    새로운 기억 셀의 수식

    tanh의 결과가 이전 시각의 기억 셀 c_{t-1}에 더해진다.

    활성화 함수로 sigmoid가 아닌 tanh가 사용된다.

    Input 게이트

    g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지를 판단한다.

    input 게이트 수식

    i와 g의 원소별 곱 결과를 기억 셀에 추가한다.

    LSTM의 기울기 흐름

    RNN과 다르게 역전파과 과정에서 원소별 곱이 이루어지므로서 행렬 곱을 했을 때, 발생하는 기울기 소실/폭발 문제를 예방한다.

    LSTM의 구현

    LSTM 레이어

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

    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

    TimeRNN과 마찬가지로 stateful 상태를 제어할 수 있다.

    레퍼런스

    밑바닥부터 시작하는 딥러닝 2 (사이토 고키 지음, 개앞맵시 옮김)

    개앞맵시님의 밑시딥2 저장소: WegraLee/deep-learning-from-scratch-2: 『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019) (github.com)

Designed by Tistory.