딥러닝

밑바닥부터 시작하는 딥러닝 - 순환 신경망(RNN)(2)

Leesemo 2021. 10. 27. 21:08

Time RNN 계층 구현

Time RNN 계층은 T개의 RNN 계층으로 구성된다. Time RNN 계층은 아래 그림처럼 표현된다.

위에 그림에서 보면, Time RNN 계층은 RNN 계층 T개를 연결한 신경망이다. 이 신경망으로 Time RNN 클래스로 구현할 것이다. 여기에서는 RNN 계층의 은닉 상태 h를 인스턴스 변수로 유지한다.

RNN 계층의 은닉 상태를 Time RNN 계층에서 관리한다. 이렇게 하면 Time RNN 사용자는 RNN 계층 사이에서 은닉 상태를 '인계하는 작업'을 생각하지 않아도 된다는 장점이 생긴다.

다음은 Time 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

초기화 메서드는 가중치와 편향, 그리고 stateful이라는 boolean 값을 인수로 받는다. 인스턴스 변수 중 layers가 보인다. 이 변수는 다수의 RNN 계층을 리스트로 저장하는 용도이다. 그리고 인스턴스 변수 h는 forward() 메서드를 불렀을 때의 마지막 RNN 계층의 은닉 상태를 저장하고, dh는 backward()를 불렀을 때 하나 앞 블록의 은닉 상태의 기울기를 저장한다.

 

앞의 인수 중 stateful은 '상태가 있는'이란 뜻의 단어이다. True일 경우 '상태가 있다'란, Time RNN 계층이 은닉 상태를 유지한다는 뜻이다.

아무리 긴 시계열 데이터라도 Time RNN 계층의 순전파를 끊지 않고 전파한다는 의미이다.

stateful이 False일 때의 Time RNN 계층은 은닉 상태를 '영행렬'로 초기화한다.

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 계층을 생성하여 인스턴스 변수 layers에 추가한다. 그 사이에 RNN 계층이 각 시각 t의 은닉 상태 h를 계산하고, 이를 hs에 해당 인덱스의 값으로 설정한다.

Time RNN 계층의 역전파 구현이다. 기울기를 dhs로 쓰고, 내보내는 기울기를 dxs로 쓴다.

t번째 RNN 계층에서는 위로부터의 기울기 dh(t)와 '한 시각 뒤(미래) 계층'으로부터의 기울기 dh(next)가 전해진다. 여기에서의 주의점은 RNN 계층의 순전파에서는 출력이 2개로 분기된다는 것이다.

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, grads in enumerate(grads):
            self.grads[i][...] = grad
        self.dh = dh
        
        return dxs

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


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

여기서부턴 시계열 데이터를 처리하는 계층을 몇 개 더 만들어 볼 것이다.

위의 그림은 RNNLM의 신경망이다. 첫 번째 층은 Embedding 계층이다. 이 계층은 단어 ID를 단어의 분산 표현으로 변환한다. 그리고 그 분산 표현이 RNN 계층으로 입력된다. RNN 계층은 은닉 상태를 다음 층으로 출력함과 동시에, 다음 시각의 RNN 계층으로 출력한다. 그리고 RNN 계층이 위로 출력한 은닉 상태는 Affine 계층을 거쳐 Softmax 계층으로 전해진다.

첫 단어가 단어 ID가 0인 'you'가 입력된다. 이때 Softmax 계층이 출력하는 확률분포를 보면 'say'에서 가장 높게 나온 것을 알 수 있다. 'you' 다음에 출현하는 단어가 'say'라는 것을 올바르게 예측했다.

두 번째 단어인 'say'를 입력하는 부분에서 Softmax 계층 출력은 'goodbye'와 'hello', 두 개에서 높게 나왔다.

두 단어 모두 자연스러운 문장으로 이어지기 때문에 높은 확률이 나왔다.

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


Time 계층 구현

Time 계층은 간단하게 구현할 수 있다. Time Affine 계층은 Affine 계층을 T개 준비해서, 각 시각의 데이터를 개별적으로 처리하면 된다.

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

 


RNNLM 학습과 평가

RNNLM 구현

RNNLM에서 사용하는 신경망을 SimpleRnnlm이라는 클래스로 구현할 것이다.

계층 구성은 다음과 같다.

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

초기화 메서드는 각 계층에서 사용하는 매개변수를 초기화하고 필요한 계층을 생성한다. 이 초기화 코드는 RNN 계층과 Affine 계층과 Affine 계층에서 '사비에르 초깃값'을 이용했다. 사비에르 초깃값에서는 이전 계층의 노드가 n개라면 표준편차가 1 / √n인 분포로 값들을 초기화한다.

계속해서 forward(), backward(), reset_state() 메서의 구현을 살펴보자.

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

순전파와 역전파를 적절히 구현했다. 적절한 순서로 호출했다. reset_state()는 신경망의 상태를 초기화하는 편의 메서드이다.