본문 바로가기
밑바닥 딥러닝/밑바닥부터 시작하는 딥러닝2

6장

by sxlvxrjxms2s2itsmes2s2 2023. 8. 14.

6.1 RNN의 문제점

6.2 기울기 소실과 LSTM

6.3 LSTM 구현

6.4 LSTM을 사용한 언어 모델

6.5 RNNLM 추가 개선

6.6 정리

 

5장에서 본 RNN은 장기 의존 관계를 잘 학습할 수 없다.

이번 장에서는 RNN을 대신하는 계층으로써 LSTM과 GRU와 같은 게이트가 추가된 RNN을 소개한다.

 

6.1 RNN의 문제점

BPTT에서 기울기 소실 혹은 기울기 폭발이 일어나기 때문에 RNN은 장기 의존 관계를 잘 학습하지 못한다.

 

1) RNN 복습

 

RNN은 순환 경로를 가지고 있다.

RNN 계층은 시계열 데이터인 x_t를 입력하면 h_t를 출력한다.

h_t는 RNN 계층의 은닉 상태라고 하여 과거 정보를 저장한다.

 

RNN의 특징은 바로 이전 시각의 은닉 상태를 이용한다는 점이다.

 

 

2) 기울기 소실 또는 기울기 폭발

Tom was watching TV in his room. Mary came into the room. Mary said hi to  ? (Tom)

?에 들어갈 단어는 Tom이다.

현재 맥락에서 "Tom이 방에서 TV를 보고 있음"과 "그 방에 Mary가 들어옴"이란 정보를 기억해야한다.

다시 말해 이런 정보를 RNN 계층의 은닉 상태에 인코딩해 보관해둬야 한다.

 

RNN이 이 단어를 예측하기 위해서 수행하는 순전파, 역전파 과정을 도식화해보면 아래와 같다.

학습할 때, 예측값과 정답 간의 차이를 비교하면서 그 차이값(손실 함수 값)을 작게 만드는 방향으로 기울기 값을 역방향으로 전달하는 역전파 과정(빨간 색 선)을 수행할 것이다.

 

RNN 계층이 과거 방향으로 의미 있는 기울기를 전달함으로써 시간 방향의 의존 관계를 학습할 수 있다.

 

그러나 현재의 단순한 RNN 계층에서는 시간을 거슬러 올라갈수록 기울기가 작아지거나(소실) 혹은 커질 수 있으며(폭발) 대부분 둘 중 하나의 운명을 걷게된다.

 

 

3) 기울기 소설과 기울기 폭발의 원인

 

이 원인을 살펴보자

길이가 T인 시계열 데이터를 가정한다.

 

T번째 정답 레이블이 Tom인 경우에 해당한다.

 

역전파로 전해지는 기울기는 차례로 tanh, +, MatMul 연산을 통과한다는 것을 알 수 있다.

 

tanh MatMul 연산에서 문제가 생긴다.

 

점선이 y=tanh(x)의 미분이다. 그 값은 1.0 이하이고, x가 0으로부터 멀어질수록 작아진다.

역전파에서는 기울기가 tanh 노드를 지날 때마다 값은 계속 작아진다는 뜻이다.

 

MatMul 노드를 보자

상류로부터 dh라는 기울기가 흘러온다고 가정한다.

MatMul 노드에서의 역전파는

이라는 행렬 곱으로 기울기를 계산한다.

 

이 계산을 시계열 데이터의 시간 크기만큼 반복한다.

 

주목할 점은 매번 똑같은 가중치인 Wh가 사용된다는 것이다.

 

코드로 실험해보자

import numpy as np
import matplotlib.pyplot as plt

N = 2   # 미니배치 크기
H = 3   # 은닉 상태 벡터의 차원 수
T = 20  # 시계열 데이터의 길이

dh = np.ones((N, H))  # dh를 np.ones()로 초기화 - 모든 원소가 1인 행렬을 반환
np.random.seed(3) # 재현할 수 있도록 난수의 시드 고정
Wh = np.random.randn(H, H) # WH 초깃값

norm_list = []
for t in range(T):
    dh = np.dot(dh, Wh.T)  # MatMul노드 수(T)만큼 dh를 갱신
    norm = np.sqrt(np.sum(dh**2)) / N  # 미니배치 N개 의 평균 'L2 노름'을 구해 dh 크기로 사용
    norm_list.append(norm)      # 각 단계에서 dh크기(노름)을 list로 추가

기울기 dh는 시간 크기에 비례해 지수적으로 증가

기울기 크기가 지수적으로 증가 => 기울기 폭발 => 오버플로우 => NaN 발생

 

Wh의 초깃값을 변경 한 후의 실험

Wh = np.random.randn(H, H)             # 변경 전
Wh = np.random.randn(H, H) * 0.5       # 변경 후

이번에는 기울기가 지수적으로 감소한다

기울기가 지수적으로 감소 => 기울기 소실 => 장기 의존 관계 학습 불가

 

지수적인 변화가 일어난 이유

- 행렬 Wh를 T번 반복하여 곱했기 때문이다.

- Wh가 행렬이기에 행렬의 특잇값이 척도가 된다.

(행렬의 특잇값? = 데이터가 얼마나 퍼져 있는지)

 

 

4) 기울기 폭발 대책

 

  • 기울기 클리핑 기법

threshold : 문턱값   

g^ : 신경망에서 사용되는 모든 매개변수의 기울기를 하나로 모은 것

 

" ||g|| (기울기의 L2노름) > 문턱값" 이면 기울기를 수정하자. = 기울기 클리핑

 

 

코드로 구현

# 기울기 클리핑 구현 

import numpy as np


dW1 = np.random.rand(3, 3) * 10
dW2 = np.random.rand(3, 3) * 10
grads = [dW1, dW2]
max_norm = 5.0


def clip_grads(grads, max_norm):
    total_norm = 0
    for grad in grads:
        total_norm += np.sum(grad ** 2)
    total_norm = np.sqrt(total_norm)

    rate = max_norm / (total_norm + 1e-6)
    if rate < 1:
        for grad in grads:
            grad *= rate

 

6.2 기울기 소실과 LSTM

기울기 소실을 해결하려면 RNN 계층의 아키텍처를 근본부터 고쳐야 한다.

여기서 등장하는 것이 게이트가 추가된 RNN이다.

그 대표격으로 LSTM과 GRU가 있다.

 

 

1) LSTM의 인터페이스

단순화 도법을 적용한 RNN 계층

tanh라는 직사각형 노드에 행렬 곱과 편향의 합, tanh에 의한 변환을 모두 포함시켰다.

 

LSTM에는 c라는 경로 존재. (c는 기억 셀이라고 부르며 LSTM 전용 기억 메커니즘이다.)

기억 셀의 특징은 데이터를 자기 자신(LSTM 계층) 내에서만 주고 받는다는 것이다.

은닉 상태 h는 RNN 계층과 마찬가지로 다른 계층으로(위쪽으로) 출력된다.

 

 

2) LSTM 계층 조립하기

 

c_t : 과거로부터 시각 t까지에 필요한 모든 정보 저장

→ 이 기억 c를 바탕으로 다음 시각 LSTM에 은닉상태 h_t 출력

 

h_t : 기억 셀의 값을 tanh 함수로 변환한 값

 

게이트란

  • 문을 의미하는 단어
  • 게이트는 데이터의 흐름을 제어한다.
  • 물의 흐름을 멈추거나 배출하는 것이 게이트의 역할
  • 다음 단계로 흘려보낼 물의 양을 제어할 수 있다.

 

 

3) output 게이트

 

output 게이트 = 다음 은닉 상태 h의 출력을 담당하는 게이트

  • 열림 상태(다음 몇 %만 흘려보낼까)는 입력 x_t와 이전 상태 h_t-1으로부터 구한다.
  • 가중치 매개변수와 편향에는 o를 첨자로 추가한다.

output 게이트

 

입력 x_t에는 가중치가, 이전 시각의 은닉 상태 h_t-1에는 또다른 가중치가 붙어있다. 그리고 이 행렬들의 곱과 편향 b를 모두 더하고 시그모이드 함수를 거쳐 출력 o를 구한다.

 

(시그모이드 함수 사용하는 이유 : 0.0~1.0 의 실수이므로 데이터를 얼마나 통과시킬지 정하는 비율로 사용)

 

o와 tanh(c_t)의 원소 별 곱을 h_t로 출력하는 것이다. (원소별 곱 = 아다마르 곱이라고도 한다.)

 

 

4) forget 게이트

다음 우리가 할 일 은 기억 셀에 무엇을 잊을까를 명확하게 지시하는 것이다.

 

c_t-1의 기억 중 불필요한 기억을 잊게 해주는 게이트를 forget 게이트라고 부르자.

forget 게이트가 수행하는 계산을 σ 노드로 표기한다.

forget 게이트가 수행하는 계산

출력 f가 구해진다. 이 f와 이전 기억 셀인 c_t-1과의 원소 별 곱, 즉

c_t ⊙ c_t-1 을 이용해 c_t를 구한다.

 

 

5) 새로운 기억 셀

forget 게이트를 거쳐서 이전 기억 셀로부터 잊어야 할 기억이 삭제되었다.

기억셀 c가 새로 기억해야 할 정보를 추가해줘야 한다.

tanh노드를 추가한다. tanh노드가 계산한 결과가 이전 시각의 기억 셀 c_t-1에 더해진다.

(tanh 함수를 사용하는 이유 : tanh 출력은 -1.0 ~ 1.0 으로, 실질적 정보를 가지는 데이터의 인코딩 정도를 표시할 수 있다.)

 

tanh 노드에서 수행하는 계산

기억 셀에 추가하는 새로운 기억을 g로 표기한다. g가 이전 시각의 기억 셀인 c_t-1에 더해짐으로써 새로운 기억이 생겨난다.

 

 

6) input 게이트

마지막으로 g에 게이트를 하나 추가한다.

이를 input 게이트라고 한다.

 

input 게이트는 g의 각 원소가 새로 추가되는 정보로써의 가치가 얼마나 큰지를 판단한다.

적절히 취사선택하여 새 정보를 수용하기 위함이다.

 

input 게이트를 σ로, 그 출력을 i로 표기한다.

이때 수행하는 계산이다.

그런 다음 ig의 원소별 곱 결과를 기억 셀에 추가한다.

이상이 LSTM 안에서 이뤄지는 처리이다.

 

 

7) LSTM의 기울기 흐름

어떤 원리로 기울기 소실을 없애주는 걸까.

그 원리는 기억 셀 c의 역전파에 주목하면 보인다.

  • 기억 셀의 역전파에서는 +와 x노드만을 지나게 된다.

 

  • + 노드
    • 기울기 그대로 보낸다 = 기울기 변화(손실)가 없다.
  • x 노드
    • RNN 역전파에서는 똑같은 가중치 행렬을 사용해 행렬 곱 반복 = 기울기 소실/ 폭발
    • LSTM 역전파에서는 행렬곱X , 원소별 곱 (아마다르 곱) = 매 시각 다른 게이트값 이용 (곱셈 소실/폭발 누적 안됨) 
  • x 노드의 계산은 forget 게이트가 제어한다
    • forget 게이트가 잊어야 한다고 판단한 기억 셀의 원소에 대해서는 그 기울기가 작아진다.
    • 잊어서는 안된다라고 판단한 원소에 대해서는 기울기가 약화되지 않고 과거 방향으로 전해진다.

 

6.3 LSTM 구현

 

LSTM에서 수행하는 계산들

 

Affine 변환 식(xW_x + hW_h + b) 4개 한번에 정리하기

 

이렇게 하면 계산 속도가 빨라진다.

Affine 변환의 결과를 slice 노드를 통해 균등하게 조각내어 활성화함수에 입력해준다.

 

 

LSTM 클래스를 구현해보자

 

LSTM 클래스의 초기화 코드

# LSTM 클래스 구현
class LSTM:
    def __init__(self, Wx, Wh, b):   #(가중치 매개변수Wx,Wh,편향 - 4개)
    
        self.params = [Wx, Wh, b] # 인스턴스 변수 params에 할당
        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):   # (현 시각의 입력x, 이전시각 은닉상태h, 이전 시각 기억셀c)
        Wx, Wh, b = self.params # 4개의 매개변수 저장되어 있음
        N, H = h_prev.shape

        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b   # 아핀 변환

        f = A[:, :H]       # slice
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]

        f = sigmoid(f)   # 각각의 활성화 함수에 입력된다.
        g = np.tanh(g)
        i = sigmoid(i)
        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
  • 가장 먼저 Affine 변환을 한다.
  • 이때 인스턴스 변수 W_x, W_h, b에는 각각 4개분의 매개변수가 저장되어 있으며 이 변환을 행렬의 형상과 함께 그려보면 이렇다.

N : 미니배치 수

D : 입력 데이터의 차원 수

H : 기억 셀과 은닉 상태의 차원수

계산 결과 A에는 네 개분의 affine 변환 결과 저장

 

slice노드는 네 조각으로 행렬을 나눠서 분배했다.

따라서 역전파에서는 반대로 4개의 기울기를 결합해야 한다.

역전파 처리는 인수로 주어진 배열을 가로로 연결하는 메서드를 사용하면 끝난다.

dA = np.hstack((df, dg, di, do))

 

 

1) Time LSTM 구현

 

Time LSTM의 입출력

Truncated BPTT를 이용하므로 역전파의 연결은 끊고 순전파의 흐름은 그대로 유지해야한다.

따라서 그림 6-25처럼 은닉 상태와 기억 셀을 인스턴스 변수로 유지해야 다음 번 forward()가 불렸을 때, 이전 시각의 은닉 상태에서부터 시작할 수 있다.

 

class TimeLSTM:
    def __init__(self, Wx, Wh, b, stateful=False):   # 인수 stateful 상태 유지 지정
        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')

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

        for t in range(T):
            layer = LSTM(*self.params)
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            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

        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            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

    def set_state(self, h, c=None):
        self.h, self.c = h, c

    def reset_state(self):
        self.h, self.c = None, None

 

6.4 LSTM을 사용한 언어 모델

 

Time RNN / Time LSTM

# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from common.base_model import BaseModel


class Rnnlm(BaseModel):
    def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 가중치 초기화
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * 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),
            TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layer = self.layers[1]

        # 모든 가중치와 기울기를 리스트에 모은다.
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts):
        score = self.predict(xs)
        loss = self.loss_layer.forward(score, 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.lstm_layer.reset_state()
    
    # 매개변수 읽기/쓰기
    def save_params(self,file_name='Rnnlm.pkl'):
        with open(file_name,'wb') as f:
            pickle.dump(self.params,f)
            
    def load_params(self,file_name='Rnnlm.pkl'):
        with open(file_name,'rb') as f:
            self.params = pickle.load(f)

 

신경망 학습 - PTB 데이터

# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
#from rnnlm import Rnnlm


# 하이퍼파라미터 설정
batch_size = 20
wordvec_size = 100
hidden_size = 100  # RNN의 은닉 상태 벡터의 원소 수
time_size = 35     # RNN을 펼치는 크기
lr = 20.0
max_epoch = 4
max_grad = 0.25

# 학습 데이터 읽기
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

# 모델 생성
model = Rnnlm(vocab_size, wordvec_size, hidden_size) # Rnnlm 사용
optimizer = SGD(lr) 
trainer = RnnlmTrainer(model, optimizer) # RnnlmTrainer사용

# 기울기 클리핑을 적용하여 학습 (1)
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad, # 모델의 기울기를 구해 매개변수 갱신한다.
            eval_interval=20) #20번째 반복마다 퍼플렉서티를 평가하라     # max_grad : 기울기 클리핑 적용
trainer.plot(ylim=(0, 500))

# 테스트 데이터로 평가 (2)
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)

# 매개변수 저장 (3)
model.save_params()

(2)에서 학습이 끝난 후 테스트 데이터를 사용해 퍼플렉서티 평가 (이때 모델 상태=LSTM의 은닉 상태와 기억 셀을 재설정하여 평가를 수행한다.)

- 데이터셋 어휘 수 10,000개 중 후보 단어를 136개로 줄였다.

- 더 줄여야 성능이 좋다고 할 수 있다.

 

6.5 RNNLM 추가 개선

 

1) LSTM 계층 다층화

LSTM을 여러겹 쌓아 모델 정확도를 향상시키자.

 

첫번째 LSTM 계층의 은닉상태가 두번째 LSTM 계층에 입력된다.

 

몇 층 정도로 쌓을까? - 하이퍼파라미터에 관한 문제

2~4층 정도가 적당하다고 책은 제시한다.

 

2) 드롭아웃에 의한 과적합 억제

층을 깊게 쌒을 수록 표현력이 풍부하지만, 과적합을 일으킬 수 있다.

 

과적합 억제하는 전통적 방법

  1. 훈련 데이터양 늘리기
  2. 모델 복잡도 줄이기
  3. 모델 복잡도에 페널티 주는 정규화(normalization)
  4. 드롭아웃 : 훈련 시 계층 내의 뉴런 몇개를 무시하며 학습한다.

드롭아웃은 무작위로 뉴런을 선택해 선택한 뉴런을 무시한다.

피드포워드 신경망에 드롭아웃을 적용하는 예

 

RNN에서의 드롭아웃 계층

시간축 방향으로의 드롭아웃은 시간의 흐름에 따라 정보가 사라질 수 있다.

그림 6-33처럼 드롭아웃 계층을 상하 방향으로 삽입하는 방법이 좋다.

=> 시간축과는 독립적으로 깊이 방향(상하 방향)에만 영향을 주기 때문이다.

 

또다른 연구에서는 변형 드롭아웃을 제안했다.

변형 드롭 아웃 : 깊이 방향은 물론 시간 방향에도 사용 가능

  • 색이 같은 드롭아웃끼리는 같은 마스크를 이용
  • 같은 계층에 적용되는 드롭아웃끼리는 공통의 마스크를 이용함으로 시간 방향 드롭아웃도 효과적으로 작동

 

 

3) 가중치 공유

 

가중치를 공유하는 효과를 준다.

언어 모델에서의 가중치 공유

Embedding 계층의 가중치과 Affine 계층의 가중치를 공유하는 기법이다.

이를 통해 매개변수 수가 크게 줄어들고 정확도도 향상된다.

 

V : 어휘 수 

H : 은닉 상태 차원 수

 

Embedding 가중치는 V x H

Affine 가중치는 H x V

Embedding 계층의 가중치를 전치하여 Affine 계층의 가중치로 설정하기만 하면 된다.

 

 

4) 개선된 RNNLM 구현

개선점

  1. LSTM 계층 다층화
  2. 드롭아웃 사용 (깊이 방향)
  3. 가중치 공유 (Embedding 계층과 Affine 계층에서 가중치 공유)
# 세 가지 개선
    self.layers = [
        TimeEmbedding(embed_W),
        TimeDropout(dropout_ratio),
        TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful = True),
        TimeDropout(dropout_ratio),
        TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful = True),
        TimeDropout(dropout_ratio),
        TimeAffine(embed_W.T, affine_b)
    ]

세가지 개선을 코드에 적용한 부분이다.

TimeLSTM 계층을 2개 겹치고

사이사이 TimeDropout 계층을 사용한다.

TimeEmbedding 계층과 TimeAffine 계층에서 가중치를 공유한다.

 

이제 학습시켜보자

# coding: utf-8
import sys
sys.path.append('..')
from common import config
# GPU에서 실행하려면 아래 주석을 해제하세요(CuPy 필요).
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity, to_gpu
from dataset import ptb
#from better_rnnlm import BetterRnnlm


# 하이퍼파라미터 설정
batch_size = 20
wordvec_size = 650
hidden_size = 650
time_size = 35
lr = 20.0
max_epoch = 40
max_grad = 0.25
dropout = 0.5

# 학습 데이터 읽기
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_val, _, _ = ptb.load_data('val')
corpus_test, _, _ = ptb.load_data('test')

if config.GPU:
    corpus = to_gpu(corpus)
    corpus_val = to_gpu(corpus_val)
    corpus_test = to_gpu(corpus_test)

vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

model = BetterRnnlm(vocab_size, wordvec_size, hidden_size, dropout)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

best_ppl = float('inf')
for epoch in range(max_epoch):
    trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size,
                time_size=time_size, max_grad=max_grad)

    model.reset_state()
    ppl = eval_perplexity(model, corpus_val)
    print('검증 퍼플렉서티: ', ppl) # 매 에폭마다 검증데이터로 퍼플렉서티 평가

    if best_ppl > ppl:
        best_ppl = ppl
        model.save_params()
    else:
        lr /= 4.0       # 기존 퍼플렉서티보다 낮으면 학습률을 1/4로 줄인다.
        optimizer.lr = lr

    model.reset_state()
    print('-' * 50)


# 테스트 데이터로 평가
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('테스트 퍼플렉서티: ', ppl_test)

 

 

정리

  • 단순한 RNN에서는 기울기 폭발/소실 문제가 있었다.
  • 기울기 폭발에는 기울기 클리핑, 기울기 소실에는 게이트가 추가된 RNN(LSTM, GRU 등)이 효과적이다.
  • LSTM에는 input 게이트, forget 게이트, output 게이트 등 3개의 게이트가 있다.
  • 게이트에는 전용 가중치가 있으며, 시그모이드 함수를 사용해 0.0~1.0 사이의 실수를 출력한다.
  • LSTM 다층화, 드롭아웃, 가중치 공유 기법을 적용하여 정확도를 향상시켰다.

'밑바닥 딥러닝 > 밑바닥부터 시작하는 딥러닝2' 카테고리의 다른 글

8장  (0) 2023.08.28
7장  (0) 2023.08.21
5장  (0) 2023.08.13
4장  (0) 2023.08.07
3장  (0) 2023.07.20