본문 바로가기

Deep learning from scratch

LSTM (Long Short-Term Memory)


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

 

 

3. Reference code

 

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

        '''
        각 gate에는 공통적읜 입력이 사용되므로 행렬 연산을 통해 Affine transform을 한번에 수행한다.
        '''
        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b

        '''
        Affine transform 결과를 4개로 분할한다.
        '''
        f = A[:, :H]
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]

        '''
        각 행렬을 forget gate, input gate(g,  i), output gate에 할당하고
        대응하는 활성화 함수에 대한 입력으로 연산한다.
        '''
        f = sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)

        '''
        c_next는 다음 LSTM layer에서의 기억 셀 c_prev로 할당된다.
        f 를 통해 prev LSTM layer의 은닉 상태에 대한 정보를 조정한다.
        g 를 통해 현재 시점의 입력 데이터를 입력 받으며, i 를 통해 그 강도를 조절한다.
        '''
        c_next = f * c_prev + g * i
        
        '''
        h_next는 다음 LSTM layer에서의 은닉 상태 h_prev로 할당된다.
        앞서 연산된 c_next의 강도를 조정하여 다음 LSTM layer에서 사용될 은닉 상태의 강도를 조절한다.
        '''
        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

        '''
        c_next에 tanh 함수를 거쳐 o와 연산 후 h_next로 순전파가 진행되므로
        역전파 계산을 위해 np.tang(c_next)가 필요하다.
        '''
        tanh_c_next = np.tanh(c_next)

        '''
        행렬곱의 역전파 연산은 아래 순서와 같다.
        1. dh_next * o 가 수행된다.
        2. 그 후 활성화 함수 tanh의 미분 값을 곱하여 그래디언트를 구한다.
        3. 그리고 앞 LSTM layer에서 들어온 dc_next와 곱하여 그래디언트를 구한다.
        '''
        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)

        '''
        4. 마지막으로 순전파때 이전 기억 셀에 대한 정보를
           제어하기 위하여 사용되었던 f와 곱하여 그래디언트를 구한다. 
        '''
        dc_prev = ds * f

        '''
        순전파에서 c_next 행렬에 tanh 활성화 함수로 연산한 결과와 o를
        행렬곱 노드로 연결하였으므로 앞 LSTM layer에서 들어온 dh_next와
        tanh_c_next를 곱하여 그래디언트를 구한다.
        '''
        do = dh_next * tanh_c_next
        
        '''
        순전파에서 행렬곱 노드를 통해 연산한 결과를
        ds 와 더하기 노드로 연결하였으므로 ds의 그래디언트 값을 그대로 사용한다.
        순전파에서 i 와 g는 행렬곱 노드를 통해 연산하였으므로
        역전파에서는 ds에 각각 g, i 행렬을 곱하여 그래디언트를 구한다.
        '''
        di = ds * g
        dg = ds * i

        '''
        순전파에서 c_prev에 f를 행렬곱 노드를 통해 연결하였으므로
        역전파에서는 c_prev와 ds 행렬을 곱하여 그래디언트를 구한다.
        '''
        df = ds * c_prev

        '''
        각 활성화 함수에 대한 미분 값을 곱하여 그래디언트를 구한다.
        '''
        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)

        '''
        앞서 순전파 과정에서 4 개의 행렬을 한번에 affine transform 하였으므로
        각 그래디언트를 동일한 방향으로 쌓아준다.
        '''
        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

        '''
        A를 구성하는 가중치 중 역전파에 의해 다음 LSTM layer에 전해질 그래디언트를 구한다.
        '''
        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)

        return dx, dh_prev, dc_prev

 

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

        """
        LSTM layer 순전파의 시작점일 경우 클래스 변수에 h와 c를 생성한다.
        """
        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')
        """
        정의된 시간 T 만큼 LSTM layer를 생성하고 시계열 데이터를 입력한다.
        """
        for t in range(T):
            layer = LSTM(*self.params)
            """
            연속적으로 LSTM layer의 클래스 인스턴스인 은닉 상태 h와 기억 셀 c를 갱신하여
            xs[:, t, :]와 함께 다음 LSTM layer의 입력으로 넣어준다. 
            """
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            
            """
            은닉 상태 h를 저장한다.
            """
            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

        """
        역전파를 수행하기 위하여 그래디언트([Wx, Wh, b])를 초기화 한다.
        """
        
        grads = [0, 0, 0]
        
        """
        Inverse order로 역전파를 수행한다.
        """
        for t in reversed(range(T)):
            layer = self.layers[t]
            """
            해당 LSTM layer에 들어오는 그래디언트 값을 통해서 역전파를 수행한다.
            개별적으로 들어오는 dhs에 대해서는 시간축에 따라 그래디언트를 할당한다.
            그래디언트 dh와 dc는 Time LSTM layer 내부에서 연속적으로 업데이트 되며,
            다음 LSTM layer의 역전파를 수행하기 위해 사용되므로 매번 새롭게 할당한다.           
            """
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            dxs[:, t, :] = dx

            """
            Time LSTM layer 내부에서 계산된 그래디언트를 합산한다.
            """
            for i, grad in enumerate(layer.grads):
                grads[i] += grad

        """
        합산한 그래디언트를 클래스 인스턴스 grads로 할당한다.
        """
        for i, grad in enumerate(grads):
            self.grads[i][...] = grad

        """
        클래스 인스턴스 dh에 dh를 할당한다.
        """
        self.dh = dh

        return dxs