AI/NLP 기초

[NLP 기초] 5. 순환 신경망(RNN)

kk_______yy 2024. 2. 19. 19:02

피드포워드(feed forward, 앞먹임)

  • 지금까지의 신경망
  • 한 방향으로만 신호 전달
  • 구성 단순, 많은 문제에 응용 가능
  • 단점 : 시계열 데이터의 성질(패턴)을 충분히 학습할 수 없음
    → 순환 신경망(Recurrnet Neural Network, RNN) 등장

 

5.1 확률과 언어 모델

word2vec 복습

 

5.1.1 word2vec을 확률 관점에서 바라보다

 

CBOW 모델

  • 맥락으로부터 타깃을 추측
  • [식 5.1]의 사후확률을 모델링  (* 사후 확률 : W_t-1, W_t+1 이 주어졌을 때 W_t가 일어날 확률)
  • 윈도우 크기가 1일 때의 CBOW 모델

[식 5.1]

 

 

왼쪽 맥락만 고려

  • 맥락을 왼쪽 윈도우로만 한정
  • 왼쪽 두 단어만을 맥락으로 생각
  • [식 5.3] 손실함수를 최소화하는 가중치 매개변수를 찾는 것

[식 5.2] CBOW 모델이 출력할 확률
[식 5.3] CBOW 모델이 다루는 손실 함수

 

CBOW 모델 학습 목적

  • 맥락으로부터 타깃을 정확하게 추측하는 것
  • 왼쪽 맥락만 고려하여 '언어 모델' 등장

 

5.1.2 언어 모델

언어 모델(Language Model)

  • 단어 나열에 확률을 부여
  • 특정한 단어의 시퀀스에 대해, 그 시퀀스가 일어날 가능성이 어느 정도인지(얼마나 자연스러운 단어 순서인지) 확률로 평가
  • 기계 번역, 음성 인식, 새로운 문자를 생성 등의 용도

 

ex) 'you say goodbye' 단어 시퀀스 → 자연스러우므로 높은 확률(예: 0.092) 출력
      'you say good die' 단어 시퀀스 → 부자연스러우므로 낮은 확률 (예:0.000000000000000000000000032) 출력

 

 

언어 모델 수식

  • w_1, ..., w_m 이라는 m개 단어로 된 문장
  • 단어가 w_1, ..., w_m 이라는 순서로 출현할 확률을 P(w_1, ..., w_m)으로 나타냄
  • 여러 사건이 동시에 일어날 확률이므로 동시 확률
  • P(w_1, ..., w_m)을 사후확률을 사용하여 분해
  • ∏(파이) 기호 : 모든 원소를 곱하는 '총곱' 의미
    → 동시 확률은 사후 확률의 총곱으로 나타낼 수 있음

[식 5.4] P(w_1, ..., w_m)을 사후확률을 사용하여 분해

 

[식 5.4]의 결과를 확률의 곱셈정리(식 5.5)로부터 유도

 

 

곱셈정리

  • 확률론에서 가장 중요한 정리
  • A와 B가 모두 일어날 확률은, B가 일어날 확률과, B가 일어난 후 A가 일어날 확률을 곱한 값과 같음
  • 두가지 표현법 : P(A, B) = P(B|A)P(A)로도 표현 가능

[식 5.5]

 

  • 곱셈정리로 m개의 단어의 동시 확률 P(w_1, ..., w_m)을 사후 확률로 나타낼 수 있음

[식 5.6]

 

  • w_1, ..., w_m-1을 하나로 모아 기호 A로 나타냄
  • A(w_1, ..., w_m-1)에 대해서 다시 같은 식 변형을 수행

[식 5.7]

 

 

이처럼 단어 시퀀스를 하나씩 줄여가며, 매번 사후 확률로 분해

  • 동시 확률 P(w_1, ..., w_m)은 사후 확률의 총곱인 ∏ P(w_t | w_1, ..., w_t-1)으로 대표될 수 있음
  • 사후확률은 타깃 단어보다 왼쪽에 있는 모든 단어를 맥락(조건)으로 했을 때의 확률

 

목표는 P(w_t | w_1, ..., w_t-1)이라는 확률을 얻는 것

  • 이 확률을 계산할 수 있다면 언어 모델의 동시 확률 P(w_1, ..., w_m)을 구할 수 있음

 

* NOTE

  • P(w_t | w_1, ..., w_t-1)을 나타내는 모델 '조건부 언어 모델(Conditional Language Model)'이라고 함

 

5.1.3 CBOW 모델을 언어 모델로?

[식 5.8]

  • CBOW에서 맥락의 크기를 특정 값으로 한정하여 근사적으로 나타냄
  • 이 예에서는 맥락을 왼쪽 2개의 단어

 

* NOTE : 마르코프 연쇄, 마르코프 모델

  • 사상의 확률이 '그 직전' N개의 사건에만 의존할 때, 이를 'N층 마르코프 연쇄'라고 함
  • 이 경우에는 2층 마르코프 연쇄

 

 

 

문제점

  • 임의 길이로 설정할 수 있다 해도, 결국 특정 길이로 고정됨
  • 더 왼쪽에 있는 단어의 정보가 무시되어 문제가 됨
  • 맥락의 크기를 키우더라도, CBOW 모델에서는 맥락 안의 단어 순서가 무시됨

 

 

문제

  • CBOW 모델 은닉층에서 단어 벡터들 더해짐, 맥락의 단어 순서 무시됨 (그림 왼)

해결

  • 맥락의 단어 벡터를 은닉층에서 연결하는 방식 (그림 오른)

한계

  • 연결 방식을 취하면 맥락의 크기에 비례해 가중치 매개변수 늘어나게 됨

→ 순환 신경망 RNN의 등장, 맥락이 아무리 길더라도 그 맥락의 정보를 기억

 

 

* WARNING

  • word2vec은 단어의 분산 표현은 얻을 목적으로 고안
  • 언어 모델로 잘 사용하지 않음
  • 대응이나 단어의 분산 표현 '질' 개선을 위해 제안

 

5.2 RNN이란

RNN : 순환하는 신경망

 

5.2.1 순환하는 신경망

 

데이터가 같은 장소를 반복해 왕래하려면....

→ 닫힌 경로, 순환하는 경로 (혈액)

 

 

RNN의 특징

  • 순환하는 경로(닫힌 경로)가 있음
  • 순환 경로를 따라 데이터는 끊임없이 순환
  • 데이터가 순환하며 과거의 정보를 기억하는 동시에 최신 데이터로 갱신
  • RNN에 이용되는 계층 'RNN 계층'
    - x_t 입력, t 시각
    - 시계역 데이터 (x_0,. x-1, ..., x_t, ...)가 RNN 계층에 입력
    - 입력에 대응하여 (h_0, h_1, ..., h_t, ...) 출력
    - 각 시각에 입력되는 x_t는 벡터라고 가정, 각 단어의 분산 표현(단어 벡터)이 x_t가 되어, 하나씩 RNN 계층에 입력

 

RNN 계층을 그리는 방식 변경

  • 지면 관계상 아래에서 위로 흐리도록 그림(순환 구조에서 계층을 양옆으로 펼치고자)

 

 

5.2.2 순환 구조 펼치기

 

RNN 계층의 순환 구조를 펼치면 친숙한 신경망 모양

  • 지금까지의 피드 포워드 신경망과 같은 구조
  • 다수의 RNN 게층 모두가 실제로는 '같은 계층'인 것이 지금까지의 신경망과 다름

 

 

* WARNING

  • 시계열 데이터는 시간 방향으로 데이터 나열
  • 시간 t의 단어, 시간 t의 RNN 계층과 같은 표현 사용

 

RNN 계층 계산

  • RNN 계층은 그 계층으로의 입력과 1개 전의 RNN 계층으로부터의 출력 받음
  • 받은 두 정보를 바탕으로 현 시각의 출력 계산
  • 수행하는 계산의 수식은 다음과 같음

[식 5.9]

기호 설명

  • RNN의 가중치 2개
    Wx : 입력 x를 출력 h로 변환하기 위한 가중치
    Wh : 1개의 RNN 출력을 다음 시각의 출력으로 변환하기 위한 가중치
  • b : 편향
  • h_t-1, x_t : 행벡터

 

식 설명

  • 행렬 곱 계산, 그 합을 tanh 함수(쌍곡탄젠트 함수)를 이용해 변환
  • 그 결과가 시각 t의 출력 h_t
  • h_t는 다른 계층을 향해 위쪽으로 출력되는 동시에, 다음 시각의 RNN 계층(자기 자신)을 향해 오른쪽으로도 출력

 

RNN 계층을 '상태를 가지는 계층' or '메모리(기억력)가 있는 계층'

  • 현재의 출력(h_t)은 한 시각 이전 출력(h_t-1)에 기초해 계산
  • RNN은 h라는 '상태 가짐'
  • [식 5.9]의 형태로 갱신

 

* NOTE

  • RNN의 h는 '상태'를 기억해 시각이 1스텝 진행될 때마다 [식 5.9] 형태로 갱신
  • RNN의 출력 h_t를 은닉 상태라고 부름

 

  • RNN 계층으로부터 나가는 두 화살표가 똑같은 데이터가 복사돼 분기된다는 사실이 드러나도록 명시

 

5.2.3 BPTT

(일반적인) 오차역전파법 적용

  • 보통의 신경망과 같은 순서로 학습
  • RNN 계층은 가로로 펼친 신경망

 

오차역전파법(BPTT)

  • 시간 방향으로 펼친 신경망의 오차역전파법
  • 순전파 수행, 이어서 역전파를 수행하여 원하는 기울기 구함

 

 

문제점

  • 사진 크기가 커지는 것에 비례하여 BPTT가 소비하는 컴퓨팅 자원 증가
  • 시간 크기가 커지면 역전파 시의 기울기 불안정

 

* NOTE

  • BPTT를 이용해 기울기를 구하려면, 매 시각 RNN 계층의 중간 데이터 메모리에 유지해야 함
  • 시계열 데이터가 길어지면 메모리 사용량도 증가

 

5.2.4 Truncated BPTT

Truncatied BPTT

  • 시간축 방향으로 너무 길어진 신경망을 적당한 지점에서 잘라내어 작은 신경망 여러 개로 만든다는 아이디어
  • 잘라낸 작은 신경망에서 오차역전파법 수행
  • 제대로 구현하려면 '역전파'의 연결만 끊어야 함
    - 역전파의 연결은 적당한 길이로 잘라내, 그 잘라낸 신경망 단위로 학습
  • 순전파의 흐름은 끊어지지 않고 전파

 

ex) 길이가 1,000인 시계열 데이터에 Truncated BPTT

 

단어 1,000개짜리 말뭉치

  • 여러 문장을 연결한 것을 하나의 시계열 데이터로 취급,
  • RNN 계층을 펼치면 계층이 가로로 1,000개나 늘어선 모양이 됨

 

계층이 길어지면...

  • 오차역전파법(BPTT) 기울기 계산 가능하지만, 너무 길면 계산량과 메모리 사용량 등의 문제
  • 계층이 길어지면, 신경망 하나 통과할 때마다 기울기 값 작아짐
    →이전 시각 t까지 역전파되기 전에 0이 되어 소멸

 

  • 역전파의 연결 잘라버리면, 그보다 미래 데이터는 생각할 필요 없음
  • 각 블록 단위로, 미래 블록과 독립적으로 오차역전파법 완결
  • 역전파 연결은 끊기지만, 순전파 연결은 끊기지 않음
    → RNN 학습에는 순전파 연결됨, 순서대로 입력해야 함

 

* WARNING

  • 지금까지는 미니배치 학습에서 데이터 무작위 선택 입력 가능
  • RNN에서 Truncated BPTT 수행할 때는 데이터 '순서대로' 입력

 

Truncate BPTT 방식으로 RNN 학습

 

1. 첫 번째 블록 입력 데이터(x0, ..., x9)를 RNN 계층에 제공

  • 먼저 순전파 수행, 다음 역전파 수행
  • 원하는 기울기 구함

 

 

2. 다음 블록의 입력 데이터(x10 ~ x19)를 입력해 오차역전파법 수행

  • 순전파 계산에 앞 블록의 마지막 은닉 상태인 h9 필요
    → 순전파는 계속 연결

 

 

3. 세번째 블록을 대상으로 학습 수행

  • 2번째 블록의 마지막 은닉 상태(h19) 이용
  • RNN 학습에서는 데이터를 순서대로 입력, 은닉 상태를 계승하며 학습 수행

 

이처럼 Truncated BPTT에서는 데이터를 순서대로 입력해 학습

  • 순전파의 연결을 유지하면서 블록 단위로 오차역전파법 적용

 

5.2.5 Truncated BPTT의 미니배치 학습

  • 지금까지는 미니배치 수가 1일 때에 해당
  • 미니배치 학습 수행
    → 데이터를 주는 시작 위치를 각 미니배치의 시작 위치로 '옮겨줌'

 

ex) 길이가 1,000인 시계열 데이터, 각 길이를 10개 단위로 잘라 Truncated BPTT로 학습

  • 미니배치 수를 2개로 구성해 학습하는 경우
  • 첫 번째 미니배치(샘플 데이터) 때는 처음부터 순서대로
  • 세 번째 미니배치 때는 500번째 데이터를 시작 위치로, 다시 순서대로 500개 데이터
    = 시작 위치를 500만큼 '옮겨줌'

 

 

데이터 제공법

  • '순서대로'
  • '미니배치별로 데이터 제공 시작 위치 옮기기'

 

미니배치 학습을 수행할 때는 각 미니배치의 시작 위치를 오프셋으로 옮겨준 후 순서대로 제공

  • 데이터를 순서대로 입력하다가 끝에 도달하면 다시 처음부터 입력

 

5.3 RNN 구현

Truncated BPTT 방식의 학습으로, 가로 크기가 일정한 일련의 신경망 만듦

  • 길이가 T인 시계열 데이터 입력
  • 각 시각의 은닉 상태 T개 출력
  • 모듈화 : 옆으로 성장한 [그림 5-16]의 신경망을 '하나의 계층'으로 구현

 

상하 방향의 입력과 출력을 각각 하나로 묶으면, 옆으로 늘어선 일련의 계층을 하나의 계층으로 간주

 

RNN 계층

  • 한 단계의 작업을 수행하는 계층

Time RNN 계층

  • T개 단계분의 작업을 한꺼번에 처리하는 계층

 

5.3.1 RNN 계층 구현

 

RNN의 순전파 수식

[식 5.10]

  • 데이터를 미니배치로 모아 처리
  • x_t(와 h_t)에는 각 샘플 데이터를 행(-) 방향에 저장

 

행렬을 계산할 때 '형상 확인' 중요

  • 미니배치 크기 N, 입력 벡터의 차원 수 D, 은닉 상태 벡터의 차원 수 H
  • 행렬 형상 확인을 수행하여 구현이 올바른지 확인 가능

 

 

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.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)

        self.cache = (x, h_prev, h_next)
        return h_next

 

RNN의 초기화 메서드

  • 가중치 2개와 편향 1개 인수
  • 각 매개변수에 대응하는 형태로 기울기 초기화한 후 grads에 저장
  • 역전파 계산 시 사용하는 중간 데이터를 담을 cache를 None으로 초기화

 

순전파 forward(x, h_prev) 메서드

  • 인수 2개(아래로부터 입력 x, 왼쪽으로부터 입력 h_prev)
  • 현 시각 RNN 계층으로부터의 출력 h_next

 

 

RNN의 역전파 구현

  • RNN 계층의 순전파 (3개의 연산)
    - 행렬의 곱 MatMul
    - 덧셈 +
    - tanh
    cf) 편향 b 덧셈 브로드캐스트는, 사실 Repeat 노드

 

 

[그림 5-19]의 역전파

  • 순전파 때와 반대 방향으로 각 연산자의 역전파 수행

    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.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        return dx, dh_prev

 

5.3.2 Tinme RNN 계층 구현

Time RNN

  • RNN 계층 T개를 연결한 신경망
  • RNN 계층의 은닉 상태 h를 인스턴스 변수로 유지
    - [그림 5-22]처럼 은닉 상태 '인계'받는 용도로 이용

 

 

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(True/False) 인수로 받음
  • layers : 다수의 RNN 계층을 리스트로 저장
  • 인스턴스 변수 h : forward() 메서드를 불렀을 때 마지막 RNN 계층의 은닉 상태 저장
  • dh : backward()를 불렀을 때, 하나 앞 블록의 은닉 상태 기울기 저장

stateful

  • True일 때 Time RNN 계층이 은닉 상태 유지(순전파 끊지 않고 전파)
  • False일 때 Time RNN 계층은 은닉 상태를 '영행렬'(모든 요소가 0인 행렬)로 초기화

 

* NOTE

  • 긴 시계열 데이터를 처리할 대는 RNN의 은닉 상태 유지해야 함
  • stateful 인수를 통해 이전 시각의 은닉 상태 유지 여부 지정

 

순전파의 구현

    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
  • 입력 xs : T개 분량의 시계열 데이터를 하나로 모음, 형상(N, T, D) * 미니배치 크기 N, 입력 벡터의 차원수 D
  • RNN 계층의 은닉 상태 h
    - 첫 호출시, 원소가 모두 0인 영행렬로 초기화
    - stateful이 False일 때도 항상 영행렬로 초기화
  • 출력값 담을 그릇(hs) 준비
  • T회 반복되는 for문 안에서 RNN 계층 생성, 인스턴스 변수 layer에 추가
  • RNN 계층이 각 시각 t의 은닉 상태 h를 계산하고, 이를 hs에 해당 인덱스(시각)의 값으로 설정

 

* NOTE

  • TimeRNN 계층의 forward() 메서드가 불리면, 인스턴스 변수 h에 마지막 RNN 계층의 은닉 상태 저장
  • 다음 forward() 메서드 호출시 stateful이 True면 먼저 저장된 h 값 그대로 이용, False면 h 다시 영행렬로 초기화

 

 

Time RNN 계층의 역전파 구현

 

역전파의 계산 그래프

  • 상류(출력 쪽 층)에서부터 전해지는 기울기 dhs
  • 하류로 내보내는 기울기 dxs
  • Truncated BPTT 수행할 땐, 블록의 이전 시각 역전파 필요하지 않음

 

 

t번째 RNN 계층에서는 위로부터의 기울기 dh_t와 '한 시각 뒤(미래) 계층'으로부터의 기울기 dh_next가 전해짐

주의점

  • RNN 계층의 순전파에서는 출력이 2개로 분기

순전파 분기

  • 역전파에서 각 기울기가 합산되어 전해짐

→ 역전파 시 RNN 계층에서는 합산된 기울기(dh_t + dh_next)가 입력

 

 

 

역전파의 구현

    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
  • 하류로 흘려보낼 기울기 그릇 dxs
  • 반대 순서로 RNN 계층의 backward() 메서드를 호출
  • 각 시각의 기울기 dx를 구해 dxs의 해당 인덱스(시각)에 저장
  • 가중치 기울기를 합산하여 최종 결과를 멤버 변수 self.grads에 덮어씀

 

* WARNING

  • Time RNN 계층 안에는 RNN 계층 여러 개, 동일한 가중치 사용
  • Time RNN 계층의 (최종) 가중치의 기울기는 각 RNN 계층의 가중치 기울기를 모두 더한 것

 

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

RNN을 사용하여 '언어 모델' 구현

  • RNN을 사용한 언어 모델 RNN Language Model = RNNLM

 

5.4.1 RNNLM의 전체 그림

가장 단순한 RNNLM의 신경망

  • 왼쪽 : RNN 계층 구성
  • 오른쪽 : 이를 시간축으로 펼친 신경망

  • 첫 번째(가장 아래) 층은 Embeddimg 계층
    - 단어 ID를 단어의 분산 표현(단어 벡터)으로 변환
  • 분산 표현이 RNN 계층으로 입력
    - RNN 계층은 은닉 상태를 다음 층으로(위쪽으로) 출력
    - 동시에 다음 시각의 RNN 계층으로(오른쪽으로) 출력
  • RNN 계층이 위로 출력한 은닉 상태는 Affine 계층을 거쳐 Softmax 계층으로 전해짐

 

ex) 순전파로 'You say goodbye and I say hello.'

 

입력 데이터

  • 단어 ID의 배열

첫 단어 ID가 0인 'you' 입력

  • Softmax 계층 출력 확률분포 보면 'say'에서 가장 높음 → 올바른 예측
  • 제대로 예측하려면 좋은 가중치(잘 학습된 가중치) 사용해야 함

두번째 단어 'say' 입력

  • 'goodbye', 'hello'에서 높은 값
  • RNN 계층은 'you say'라는 맥락 기억, you say라는 과거 정보를 응집된 은닉 상태 벡터로 저장
  • RNN이 그런 정보를 더 위의 Affine 계층에, 그 다음 시각의 RNN 계층에 전달하는 역할

RNNLM은 지금까지 입력된 단어를 '기억', 그것 바탕으로 다음 출현 단어 예측

  • RNN 계층의 존재로 가능, 과거에서 현재로 데이터 흘려보내며 과거 정보를 인코딩해 저장(기억)

 

5.4.2 Time 계층 구현

Time ____

  • 시계열 데이터를 한꺼번에 처리하는 계층
  • ex) Time RNN, Time Embedding, Time Affine

 

계층들을 레고 블록처럼 조립하는 것만으로 시계열 데이터를 다루는 신경망을 완성

 

 

ex) Time Affine 계층을 T개 준비해서, 각 시각의 데이터를 개별적으로 처리

 

 

 

시계열 버전의 Softmax

  • Softmax 계층을 구현할 때는 손실 오차를 구하는 Cross Entropy Error 계층도 함께 구현
  • x0, x1 등의 데이터는 아래층에서부터 전해지는 '점수'
  • t0, t1 등의 데이터는 정답 레이블
  • T개의 Softmax with Loss 계층 각각이 손실을 산출, 이를 합산해 평균한 값이 최종 손실
  • 이를 수행하는 계산의 수식 [식 5.11]

[식 5.11]

  • 데이터 N개짜리 미니배치라면 그 N개의 손실을 더해 다시 N으로 나눠 데이터 1개당 평균 손실 구함
  • Time Softmax with Loss 계층도 시계열에 대한 평균 계산 → 데이터 1개당 평균 손실 최종 출력

 

5.5 RNNLM 학습과 평가

  • RNNLM을 구현하여 실제로 학습 & 성능 평가

 

5.5.1 RNNLM 구현

  • RNNLM에서 사용하는 신경망을 SimpleRnnlm 클래스로 구현

 

 

초기화 메서드

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.time_layers import *


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
  • 각 계층에서 사용하는 매개변수(가중치, 편향) 초기화
  • 필요한 계층 생성
  • Truncated BPTT로 학습한다는 가정 → stateful을 True로 설정
  • RNN 계층과 Affine 계층에서 'Xavier(사비에르) 초깃값' 이용

* 표준편차는 데이터의 차이를 직관적으로 나타내는 척도로 해석

 

 

* WARNING

  • Xavier 초깃값 : 이전 계층의 노드가 n개라면 표준편차가 1/√n인 분포로 값들 초기화
  • 딥러닝에서는 가중치의 초깃값 중요 → 학습 진행 방법과 최종 정확도 크게 달라짐
  • 이후로도 가중치 초깃값으로 Xavier 초깃값 사용

 

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()
  • forward() & backward() 메서드를 적절한 순서로 호출
  • reset_state() : 신경망의 상태 초기화

 

5.5.2 언어 모델의 평가

신경망에 데이터를 주고 학습을 수행

 

그에 앞서 언어 모델의 '평가 방법'

  • 언어 모델은 과거 단어(정보)로부터 다음에 출현할 단어의 확률분포 출력
  • 언어 모델의 예층 성능을 평가하는 척도로 퍼플렉서티(perplexity, 혼란도) 자주 이용

 

* 퍼플렉서티 : 확률의 역수

  • 모델 1 (좋은 예측) : 1 / 0.8 = 1.25
  • 모델 2 (나쁜 예측) : 1 / 0.2 = 5

→ 퍼플렉서티는 작을수록 좋음

 

  • 퍼플렉서티 값을 직관적으로 '분기수(number of branches'로 해석할 수 있음
    *  '분기수' = 다음에 취할 수 있는 선택사항의 수(다음에 출현할 수 있는 단어의 후보 수)
  • ex) 분기수
    - 앞에서 좋은 모델 '분기 수' 1.25 = 다음에 출현할 수 있는 단어의 후보를 1개 정도로 좁힘
    - 나쁜 모델에서는 후보가 아직 5개나 된다는 의미
  • 좋은 모델은 정답 단어를 높은 확률로 예측 → 퍼플렉서티 값 적음(최솟값은 1.0)
  • 나쁜 모델은 정답 단어를 낮은 확률로 예측 → 퍼플렉서티 값이 큼

 

입력 데이터가 여러 개인 경우

[식 5.12]
[식 5.13]

  • N : 데이터의 총개수
  • t_n : 원핫 벡터로 나타낸 정답 레이블
  • t_nk : n개째 데이터의 k번째 값
  • y_nk : 확률분포 (신경망에서는 Softmax의 출력)
  • L : 신경망의 손실, 교차 엔트로피 오차 [식 1.8]와 완전 같은 식
    → 이 L을 사용해 e^L 값을 계산한 값이 퍼플렉서티

 

* NOTE

  • 정보이론 분야에서 퍼플렉서티 = '기하평균 분기 수'
  • 데이터가 1개일 땨 설명한 '분기 수'를 데이터가 N개인 경우에서 평균한 것이라는 해석

 

5.5.3 RNNLM의 학습 코드

# coding: utf-8
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 하이퍼파라미터 설정
batch_size = 10
wordvec_size = 100
hidden_size = 100 # RNN의 은닉 상태 벡터의 원소 수
time_size = 5     # Truncated BPTT가 한 번에 펼치는 시간 크기
lr = 0.1
max_epoch = 100

# 학습 데이터 읽기(전체 중 1000개만)
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:]   # 출력(정답 레이블)
data_size = len(xs)
print('말뭉치 크기: %d, 어휘 수: %d' % (corpus_size, vocab_size))

# 학습 시 사용하는 변수
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

# 모델 생성
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)

# 1. 미니배치의 각 샘플의 읽기 시작 위치를 계산
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
    for iter in range(max_iters):
        # 2. 미니배치 취득
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1

        # 기울기를 구하여 매개변수 갱신
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1

    # 3. 에폭마다 퍼플렉서티 평가
    ppl = np.exp(total_loss / loss_count)
    print('| 에폭 %d | 퍼플렉서티 %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0

 

지금까지 학습 코드와 다른 점

  • 데이터 제공 방법
  • 퍼플렉서티 계산

 

데이터 제공 방법

  • Truncated BPTT 방식으로 학습 수행
  • 데이터는 순차적으로 주고 각각의 미니배치에서 데이터를 읽는 시작 위치 조정
  • 소스 코드 1 : 각 미니배치가 데이터를 읽기 시작하는 위치를 계산해 offsets에 저장
  • 소스 코드 2 : 데이터 순차적으로 읽음, 그릇인 batch_x와 batch_t 준비, time_idx 1씩 늘리며 데이터 얻음
  • 소스 코드 3 : [식 5.12]를 따라 퍼플렉서티 계산

 

학습 결과

  • 에폭별 퍼플렉서티 결과 perplexity_list에 저장
  • 학습을 진행할수록 퍼플렉서티 순조롭게 낮아짐

 

한계

  • 현재 모델로는 큰 말뭉치에 전혀 대응할 수 없음 → 다음 장에서 개선

 

5.5.4 RNNLM의 Trainer 클래스

RNNLM 학습을 수행해주는 RnnlmTrainer 클래스 (책에서 제공)

# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 하이퍼파라미터 설정
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()
  • model과 optimizer를 주어 초기화
  • fit() 메서드를 호출해 학습

 

[내부에서 수행되는 작업]
1. 미니배치를 '순차적'으로 만들어
2. 모델의 순전파와 역전파를 호출하고
3. 옵티마이저로 가중치를 갱신하고
4. 퍼플렉서티를 구함

 

5.6 정리

RNN

  • 데이터를 순환시킴으로써 과거에서 현재, 미래로 데이터를 계속해서 흘려보냄
  • 이를 위해 RNN 계층 내부에 '은닉 상태' 기억하는 능력 추가
  • 언어 모델 만듦, 단어 시퀀스에 확률 부여 & 지금까지 단어 시퀀스로부터 다음에 출현할 단어 확률 계산

 

  • RNN은 순환하는 경로가 있고, 이를 통해 내부에 '은닉 상태'를 기억할 수 있다.
  • RNN의 순환 경로를 펼침으로써 다수의 RNN 계층이 연결된 신경망으로 해석할 수 있으며, 보통의 오차역전파법으로 학습할 수 있다(BPTT).
  • 긴 시계열 데이터를 학습할 때는 데이터를 적당한 길이씩 모으고(이를 '블록'이라 한다). 블록 단위로 BPTT에 의한 학습을 수행한다(=Truncated BPTT).
  • Truncated BPTT에서는 역전파의 연결만 끊는다.
  • Truncated BPTT에서는 순전파의 연결을 유지하기 위해 데이터를 '순차적'으로 입력해야 한다.
  • 언어 모델은 단어 시퀀스를 확률로 해석한다.
  • RNN 계층을 이용한 조건부 언어 모델은 (이론적으로는) 그때까지 등장한 모든 단어의 정보를 기억할 수 있다.