ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [밑시딥2] 순환 신경망 (RNN)
    A.I/Study 2023. 7. 13. 02:47

    확률과 언어 모델

    word2vec을 확률 관점으로

    CBOW 모델은 맥락 w_t-1와 w_t+1로 부터 타깃 w_t를 추측하는 일을 수행한다.

    w_t-1과 w_t+1이 주어졌을 때, w_t가 될 확률은 아래와 같다.

    $$ P(w_t|w_{t-1}, w_{t+1}) $$

    이것은 윈도우 크기가 1일 때의 CBOW 모델이다.

     

    맥락을 좌우 대칭이 아니라 왼쪽 윈도우로만 한정시킬 경우

    $$ P(w_t|w_{t-2}, w_{t-1}) $$

     

    CBOW 모델의 손실함수 (교차 엔트로피 오차에 의해 유도된 결과)

     

    $$ L = -\log P(w_t|w_{t-2}, w_{t-1}) $$

     

    이 손실함수의 값을 최소화하는 가중치 매개변수를 찾는 것이 학습의 목적

    CBOW를 맥락으로부터 타깃을 정확하게 추측하기위해 학습을 진행하면, 그 결과로 단어의 의미가 인코딩된 '단어의 분산표현'을 얻음

     

    언어 모델

    언어 모델 (Language Model)은 단어 나열에 확률을 부여하여 특정한 단어의 시퀀스에 대해 그 시퀀스가 일어날 가능성이 어느정도인지 확률로 평가한다.

    기계 번역이나 음성 인식이 대표적인 예이다.

     

    단어가 w_1, .... w_t 까지 순서대로 출현할 확률은 아래와 같다.

    $$ P(w_1, \cdots, w_t) $$

    이를 사후확률을 이용하여 분해하면 다음과 같다.

    $$ P(w_1, \cdots, w_t) = P(w_t| w_1 \cdots, w_{t-1})P(w_{t-1}| w_1, \cdots, w_{t-2}) \cdots P(w_3 | w_2, w_1)P(w_2|w_1)P(w_1) $$

    $$ = \prod_{n=1}^{t} P(w_n| w_1, \cdots , w_{n-1}) $$

     

    우리의 목표는 P(w_t| w_1 \cdots, w_{t-1})이라는 확률을 구하는 것이다.

    이것을 구할 수 있다면 처음의 수식 P(w_1, \cdots, w_t)를 구할 수 있다.

     

    CBOW 모델을 언어 모델로

    CBOW 모델을 억지로 언어 모델에 적용하려면, 맥락의 크기를 특정 값으로 한정하여 근사적으로 나타내면 된다.

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

     

    하지만 이 맥락의 크기는 결국 특정 길이로 고정된다.

    이는 더 왼쪽에 있는 단어의 정보가 무시된다는 의미이다.

     

    문맥에 맞게 맥락 크기를 키우더라도 단어의 순서가 무시될 수 있다는 한계가 있다.

     

    이상적으로는 맥락의 단어 순서도 고려한 모델이 바람직할 것이다.

    단어 벡터를 concatenate하는 방식을 생각할 수 있지만, 맥락의 크기에 비례해 파라미터 수가 증가할 수 있다.

     

    이를 위해 필요한 것이 RNN인데 이 모델은 아무리 맥락이 길어도 그 맥락의 정보를 기억하는 메커니증을 가지고 있어, 아무리 긴 시계열 데이터라도 대응할 수 있다.

     

    RNN 이란

    Recurrent Neural Network, 일명 RNN은 몇 번이나 반복해서 일을 수행하는 순환 신경망이다.

    순환하는 신경망

    순환한다의 의미는 반복해서 되돌아간다는 의미를 가지고 있다.

    이를 위해 RNN은 순환을 위한 경로가 별도로 존재한다.

    이 덕분에 과거의 정보를 기억하는 동시에 최신 데이터로 갱신될 수 있다.

    RNN 레이어

    순환 구조 펼치기

    RNN 레이어의 순환 구조를 펼치면, 우리에게 익숙한 오른쪽으로 성장하는 긴 피드포워드 신경망 같은 모습이 나온다.

    순환 구조를 펼친 모습

    하지만, 모든 레이어가 같은 레이어라는 것이 다르다.

     

    각 시각의 RNN 레이어는 그 레이어로의 입력과 1개 전의 RNN 레이어로부터 출력을 받는다.

    그리고 이 두 정보를 바탕으로 현 시각의 출력을 계산한다.

     

    $$ h_t = \tanh (h_{t-1}W_h+x_tW_x+b) $$

    W_x는 x를 h로 변환하기위한 가중치이고, W_h는  RNN 출력을 다음 시각의 출력을 변환하기위한 가중치이다.

    행렬곱과 합들을 계산하고, tanh 함수를 통해 변환한다.

     

    RNN은 h라는 상태를 가지고 있고 위 수식을 이용해 갱신된다.

    그래서 RNN 레이어를 상태를 가지는 레이어 또는 메모리가 있는 레이어라고도 한다.

     

    BPTT

    시간 방향으로 펼친 신경망의 오차 역전파법, Backpropagation Through Time이다.

    이것을 이용하면 RNN을 학습할 수 있다. 

    하지만 그전에 해결해아할 문제가 있다.

    시계열 데이터의 시간 크기가 커지는 것에 비례해서 BPTT가 소비하는 컴퓨팅 자원이 증가하고, 역전파시 기울기가 불안정해지는 문제를 먼저 해결해야한다.

    Truncated BPTT

    너무 길어진 신경망을 적당한 길이에서 잘라서 작은 신경망을 여러개 만드는 전략이다.

    순전파의 연결은 보존하되 역전파의 연결을 적당히 잘라서 각각의 블록 단위로 미래의 블록과는 독립적을 오차역전파법을 수행한다.

     

    데이터를 순서대로 입력해 학습하는 모습

    Truncated BPTT의 미니배치 학습

    미니배치 학습을 진행하려면 데이터를 어떻게 순서대로 주입할지 고민해야한다.

    데이터를 시간 순서대로 주는 방법은 시작위치를 각 미니배치의 시작위치로 옮겨주는 방법을 사용하면된다.

    미니배치 학습

    RNN 구현

    RNN 순전파/역전파 구조

    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.matmul(h_prev, Wh) + np.matmul(x, Wx) + b
            h_next = np.tanh(t)
            
            self.cache = (x, h_prev, h_next)
            reutrn h_next
        
        def backward(self, dh_next):
            Wx, Wh, b = self.params
            x, h_prev, h_next = self.cache
    
            dt = dh_next * (1 - h_next ** 2)
            db = np.sum(dt, axis=0)
            dWh = np.matmul(h_prev.T, dt)
            dh_prev = np.matmul(dt, Wh.T)
            dWx = np.matmul(x.T, dt)
            dx = np.matmul(dt, Wx.T)
    
            self.grads[0][...] = dWx
            self.grads[0][...] = dWh
            self.grads[0][...] = db
            
            return dx, dh_prev

    Time RNN 레이어 구현

    TimeRNN 구조

    Time RNN은 RNN 레이어를 T개 연결한 신경망이다.

    여기에는 RNN 레이어의 은닉 상태 h를 인스턴스 변수로 유지한다.

    이 변수를 은닉 상태를 인계받는 용도로 이용한다.

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

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

    RNNLM의 전체 그림

    RNNLM의 레이어 구조

    Embedding 레이어를 통해 분산 표현으로 바꾼 뒤 RNN으로 학습한다.

    Affine 레이어를 통해 한번 처리하고 Softmax를 통해 최종적으로 다음에 올 수 있는 단어에 대한 확률을 표시한다.

    Time 레이어 구현

    T개의 시계열 데이터를 한번에 처리하는 레이어를 Time 레이어라고 부른다.

    여러 레이어를 생성해서 사용하는 대신 행렬 연산을 통해 구현한다.

    Loss 레이어 전체 그림

    RNNLM 학습과 평가

    RNNLM 구현

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

     

    RNN 레이어와 Affine 레이어에서 Xavier 초기값을 사용했다.

    이는 이전 레이어의 노드가 n개라면 표준편차가 1/sqrt(n) 이너 분포로 값들을 초기화한다.

    언어 모델의 평가

    언어 모델은 주어진 과거 단어로부터 다음에 출현할 단어의 확률분포를 출력한다.

    언어 모델의 예측 성능을 평가하는 척도로 퍼플렉서티(perplexity)를 이용한다.

     

    퍼플렉서티는 확률의 역수이며, 작을수록 좋다.

    이 값은 직관적으로 다음 노드에서 취할 수 있는 선택사항의 수, 즉, 분기 수(number of branches)라고 해석할 수 있다.

     

    $$ L = - \frac{1}{N} \sum_n \sum_n t_{nk} \log y_{nk} $$

    $$ perplexity = e^L $$

    RNNLM의 학습 코드

    # 하이퍼파라미터 설정
    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()

     

    레퍼런스

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

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

     

Designed by Tistory.