[밑바닥부터 시작하는 딥러닝2] 06. 언어 모델

권유진·2022년 1월 26일
0

피드포워드 신경망(FeedForward Network)

  • 흐름이 단방향인 신경망
    • 시계열 데이터를 다루지 못함
    • 순환신경망(RNN; Recurrent Neural Network) 등장
    • W2V: wt1w_{t-1}, wt+1w_{t+1}wtw_{t} 예측 L=logP(wtwt1,wt+1)\rarr L=-\log{P(w_{t}|w_{t-1},w_{t+1}})
    • 언어모델: wt2w_{t-2}, wt1w_{t-1}wtw_{t} 예측 L=logP(wtwt1,wt2)\rarr L=-\log{P(w_{t}|w_{t-1},w_{t-2}})

언어 모델

  • 단어 나열에 확률을 부여한 후, 특정 단어의 sequence에 대해 해당 sequence가 일어날 확률을 평가하는 모델
  • 기계번역, 음성인식 등에 다양하게 응용
  • 문장 생성의 용도로 사용 가능 (\because 단어 순서의 자연스러움을 확률로 평가하기 때문)
  • 마르코프 연쇄: 미래의 상태가 현재 상태에만 의존해 결정되는 것
    • 2개의 맥락 고려 시, 2층 마르코프 연쇄
P(w1,w2,,wn)=P(wnwn1,,w1)P(wn1wn2,,w1)P(w2w1)P(w1)=t=1nP(wtw1,,wt1)P(w_1,w_2,\cdots,w_n) = P(w_n|w_{n-1},\cdots,w_1)P(w_{n-1}|w_{n-2},\cdots,w_1)\cdots P(w_2|w_1)P(w_1)\\ = \prod_{t=1}^n{P(w_t|w_1,\cdots,w_{t-1})}

Word2Vec - CBOW 사용

  • 맥락 안의 순서가 무시된다.
    \rarr 보완하기 위해 맥락의 단어 벡터를 은닉층에서 연결 \Rarr RNN!

순환신경망(RNN; Recurrent Neural Network)

  • 데이터가 순환하기 위해서는 닫힌 경로/순환하는 경로 필요
    • 데이터가 같은 장소 반복해 왕래 (과거 정보 기억)
    • 순환하면 데이터 끊임없이 갱신
  • 그 계층의 입력과 1개 전의 RNN 계층의 출력값(ht1h_{t-1})을 입력값으로 받음
    • RNN은 hh라는 '상태' 보유 \Rarr '상태를 갖는 계층', '메모리가 있는 계층'
    • ht\therefore h_t: 은닉 상태
ht=tanh(Wxxt+Wyht1+b)\therefore h_t = \tanh(W_xx_t + W_yh_{t-1}+b)
  • BPTT(Backpropagation Through Time)
    • 시간 방향으로 펼친 신경망의 오차역전파법
    • 시간의 크기가 커질수록 컴퓨팅 자원 증가 및 역전파 시 기울기 불안정
      \therefore Truncated BPTT 사용
      - 역전파의 시간연결을 적당한 길이로 절단 후 수행
      1. 적당한 길이로 절단해 블록을 나눔
      2. 첫번째 블록 순전파 수행 후 역전파 수행
      3. 다음 블록 순전파 수행 후 역전파 수행 (순전파 시에는 이전 블록의 은닉 상태 필요)
      4. 3번 과정 반복
      \rarr 미니 배치 적용 시, 데이터 주는 위치 = 미니배치 시작 위치

      (x0x1x9x500x501x509)(x10x11x19x510x511x519)\begin{pmatrix} x_0&x_1&\dots&x_9\\ x_{500}&x_{501}&\dots&x_{509} \end{pmatrix} \rarr \begin{pmatrix} x_{10}&x_{11}&\dots&x_{19}\\ x_{510}&x_{511}&\dots&x_{519} \end{pmatrix} \rarr \dots
    • RNN은 순전파 시, hth_t를 분기하는데, 역전파에서는 각 기울기가 합산되어 계산
      dhprev=dht+dhnextdh_{prev} = dh_t + dh_{next}
      dht1=Σk=tndhkdh_{t-1} = \Sigma_{k=t}^ndh_k
    • RNN의 모든 RNN계층은 같은 가중치 공유
      \rarr \,\therefore 최종 가중치는 각 RNN 계층의 가중치의 기울기 합
    • 언어모델의 평가: 혼란도(perplexity) 이용
      - 확률의 역수
      - 분기 수(다음에 취할 수 있는 선택사항 수)로 해석
      perplexity=eL(L=1NΣnΣktnklogynk)perplexity = e^L\\(L = -\cfrac{1}{N}\Sigma_n\Sigma_kt_{nk}\log{y_{nk}})
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)
        return h_next
    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache
        
        dt = dh_next * (1 - dh_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[1][...] = dWh
        self.grads[2][...] = db
        return dx, dh_prev
        
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 # RNN 계층을 리스트로 저장
        
        self.h, self.dh = None, None
        self.stateful = stateful # True 시, 은닉상태 유지
    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: # 처음 호출 시 0으로 초기화
            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
        
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

  • Vanila RNN의 장기 의존 관계가 잘 학습되지 않는 단점 보완
    • \because 기울기 소실/폭발(Graident Vanishing/Exploding)
      \rarr 활성화 함수 ReLU로 바꾸면 기울기 소실 개선 가능
    • 기울기 클리핑으로 기울기 폭발 개선 가능
      y^=thresholdy^y^(if,y^threshold)y^=기울기(y^)L2norm\hat y = \cfrac{threshold}{||\hat y||}\hat y \,\,\,(if,\,\, ||\hat y|| \ge threshold)\\ ||\hat y|| = 기울기(\hat y)의\,\, L2\,\, norm
  1. LSTM
  • RNN에 기억셀(cc)이 추가됨
    • 기억셀은 LSTM 계층 내에서만 주고 받음(다른 계층으로 출력 x)
    • 은닉상태 hh는 다른 계층으로도 출력
  • 게이트: 데이터의 흐름 제어, 다음으로 흐르는 데이터 양 결정 (0~1)
    1. output 게이트: 다음 은닉상태 hth_t의 출력 담당
            o=σ(xtWx(o)+ht1Wh(o)+b(o))        ht=otanh(ct)        :elementwise  dot\;\;\;\;o = \sigma( x_tW_x^{(o)} + h_{t-1}W_h^{(o)} + b^{(o)})\\\;\;\;\;h_t = o \odot \tanh(c_t)\\\;\;\;\;\odot: element-wise\;dot
    2. forget 게이트: 불필요한 기억을 잊게 해주는 게이트
            f=σ(xtWt(f)+ht1Wh(f)+b(f))\;\;\;\;f = \sigma(x_tW_t^{(f)} + h_{t-1}W_h^{(f)} + b^{(f)})
    3. input 게이트: 서로 추가되는 정보로써의 가치가 얼마나 큰지 판단
            i=σ(xtWx(i)+Ht1Wh(i)+b(i))\;\;\;\;i = \sigma(x_tW_x^{(i)}+H_{t-1}W_h^{(i)}+b^{(i)})
    새로 추가되는 정보 y=tanh(xtWx(y)+ht1Wh(y)+b(y))y = \tanh(x_tW_x^{(y)}+h_{t-1}W_h^{(y)}+b^{(y)})
  • ct=ig+f+ct1ht=otanhctc_t = i \odot g + f \odot + c_{t-1}\\ h_t = o \odot \tanh{c_t}
  • 모든 계산이 xWx+hWh+bxW_x+hW_h+b의 형태를 띈다.
    • concatenate을 통해 한번에 계산 가능
class LSTM:
    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, c_prev):
        Wx, Wh, b = self.params
        N, H = h_prev.shape
        
        A = np.matmul(x,Wx) + np.matmul(h_prev, Wh) + b
        
        # slice
        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(f)
        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, h_next, c_next):
        '''
        생략
        '''
        dA = np.hstack((df, dg, di, do))
        '''
        생략
        '''
        
  1. GRU
  • LSTM에서 매개변수 줄여 계산시간 단축
  • 기억셀(cc) 사용하지 않고 은닉상태(hh)만 사용
  • 게이트
    1. reset 게이트: 과거 은닉상태를 얼마나 무시할 지 결정
            r=σ(xtWx(r)+ht1Wh(r)+b(r))\;\;\;\;r = \sigma(x_tW_x^{(r)}+h_{t-1}W_h^{(r)}+b^{(r)})
    2. update 게이트: 은닉 상태를 갱신하는 게이트(forget+input 게이트)
            z=σ(xtWx(z)+ht1Wh(z)+b(z))\;\;\;\;z = \sigma(x_tW_x^{(z)}+h_{t-1}W_h^{(z)}+b^{(z)})
  • h~=tanh(xtWx+(rht1)Wh+b)ht=(1z)ht1+zh~\tilde h = \tanh(x_tW_x+(r\odot h_{t-1})W_h+b)\\ h_t = (1-z)\odot h_{t-1}+z\odot \tilde h

RNN 주의사항

  1. 시계열 방향으로 드롭아웃 적용 시, 정보 손실
      \rarr\;\therefore 시간 축과 독립적으로 깊이 방향으로 드롭아웃 적용
    \rarr 변형 드롭아웃: 시간 축에 같은 마스크(mask)를 공유하며 드롭아웃 적용
  2. 가중치 공유 시, 매개변수가 크게 줄고 성능 향상 (\because 과적합 억제)

참고
밑바닥부터 시작하는 딥러닝2 (사이토 고키)

profile
데이터사이언스를 공부하는 권유진입니다.

0개의 댓글