해당 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 공부하고 정리, 요약한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️ 5.1 확률과 언어 모델
◾ 5.1.1 word2vec을 확률 관점에서 바라보다
지금까지 살펴본 신경망은 피드포워드라는 유형의 신경망이다. 피드포워드란 흐름이 단방향인 신경망을 말한다.
입력 신호가 다음 층(중간층)으로 전달되고 그 신호를 받은 층은 그다음 층으로 전달하고 다시 다음 층으로.. 식으로 한 방향으로만 신호가 전달된다.
시계열 데이터를 잘 다루지 못하는게 단점이기에 순환 신경망(RNN)이 등장하게 된다.
CBOW 모델이 출력할 확률식은 다음과 같이 된다.
위 식을 교차 엔트로피 오차에 의해 유도한 결과는 다음처럼 쓸 수 있다.
CBOW 모델의 학습으로 수행하는 일은 위의 손실 함수를 최소화하는 가중치 매개변수를 찾는 것이다.
CBOW 모델을 학습시키는 본래 목적은 맥락으로부터 타깃을 정확하게 추측하는 것이다. 이 목적을 위해 학습을 진행하면 단어의 의미가 인코딩된 단어의 분산 표현을 얻을 수 있다.
여기서 언어 모델이 등장하게 된다.
◾ 5.1.2 언어 모델
언어 모델은 단어 나열에 확률을 부여한다. 특정한 단어의 시퀀스에 대해서 그 시퀀스가 일어날 가능성이 어느 정도인지를 확률로 평가하는 것이다.
언어 모델은 새로운 문장을 생성하는 용도로도 이용할 수 있다. 그 확률분포에 따라 다음으로 적합한 단어를 자아낼 수 있기 때문이다.
이 동시 확률 P(w1, ..., wm)은 사후 확률을 사용하여 다음과 같이 분해하여 쓸 수 있다.
위 식에서 파이 기호는 모든 원소를 곱하는 총곱을 뜻한다.
동시 확률은 사후 확률의 총곱으로 나타낼 수 있다. 위 식의 결과는 확률의 곱셈정리로부터 유도할 수 있다.
확률의 곱셈 정리는 다음 식으로 표현된다.
이 정리가 의미하는 바는 A와 B가 모두 일어날 확률 P(A, B)는 B가 일어날 확률 P(B)와 B가 일어난 후 A가 일어날 확률 P(A|B)를 곱한 값과 같다는 것이다.
이 곱셈정리를 사용하면 m개 단어의 동시 확률 P(w1, ..., wm)을 사후 확률로 나타낼 수 있다.
이를 알기 쉽게 나타내면 다음과 같다.
단어 시퀄스를 하나씩 줄여가면서 매번 사후 확률로 분해해 간다.
동시 확률 P(w1, ..., wm)은 사후 확률의 총곱인 파이로 대표될 수 있다. 이 사후 확률은 타깃 단어보다 왼쪽에 있는 모든 단어를 맥락으로 했을 때의 확률이라는 것이다. 그림으로는 다음처럼 된다.
◾ 5.1.3 CBOW 모델을 언어 모델로?
word2vec의 CBOW 모델을 억지로 언어 모델에 적용하려면 맥락의 크기를 특정 값으로 한정해 근사적으로 나타낼 수 있다.
이 맥락의 크기는 임의 길이로 설정할 수 있으나 임의 길이로 설정할 수 있다해도 결국 특정 길이로 고정된다.
맥락보다 더 왼쪽에 있는 단어의 정보는 무시된다. 이게 문제가 될 때가 있는데 이 예시가 아래 그림과 같다.
만약 CBOW 모델의 맥락이 10개까지였다면 이 문제에 제대로 답을 할 수 없을 것이다. CBOW 모델에서는 맥락 안의 단어 순서가 무시된다는 한계가 있다.
맥락의 단어 순서가 무시되는 문제의 구체적인 예로, 맥락으로 2개의 단어를 다루는 경우 CBOW 모델에서는 이 2개의 단어 벡터의 합이 은닉층에 온다.
위 그림의 왼쪽 그림과 같이 CBOW 모델의 은닉층에서는 단어 벡터들이 더해지므로 맥락의 단어 순서는 무시된다.
위 그림의 오른쪽처럼 맥락의 단어 벡터를 은닉층에서 연결하는 방식을 생각할 수 있다.
RNN은 맥락이 아무리 길더라도 그 맥락의 정보를 기억하는 매커니즘을 갖추고 있다. RNN을 사용하면 아무리 긴 시계열 데이터에라도 대응할 수 있다.
◼️ 5.2 RNN이란
RNN의 Recurrent 는 라틴어에서 온 말로 몇 번이나 반복해서 일어나는 일을 뜻한다. 그래서 RNN을 직역하면 순환하는 신경망이 된다.
◾ 5.2.1 순환하는 신경망
어느 한 지점에서 시작한 것이 시간을 지나 다시 원래 장소로 돌아오는 것, 그리고 이 과정을 반복하는 것이 바로 순환이다.
순환하기 위해서는 닫힌 경로가 필요하다. 데이터가 같은 장소를 반복해 왕래할 수 있다. 그리고 데이터가 순환하면서 정보가 끊임없이 갱신되게 된다.
RNN의 특징은 순환하는 경로(닫힌 경로)가 있다는 것이다. 이 순환 경로를 따라 데이터는 끊임없이 순환할 수 있다. 그리고 데이터가 순환되기 때문에 과거의 정보를 기억하는 동시에 최신 데이터로 갱신될 수 있는 것이다.
◾ 5.2.2 순환 구조 펼치기
RNN의 순환 구조는 지금까지 신경망에는 존재하지 않던 구조이다.
RNN 계층의 순환 구조를 펼침으로써 오른쪽으로 성장하는 긴 신경망으로 변신시킬 수 있다.
RNN 계층은 그 계층으로의 입력과 1개 전의 RNN 계층으로부터의 출력을 받는다. 이 두 정보를 바탕으로 현 시각의 출력을 계산한다. 이때 수행하는 계산의 수식은 다음과 같다.
위 식에서는 행렬 곱을 계산하고 그 합을 tanh 함수를 이용해 변환한다. 그 결과가 시각 t의 출력 ht가 된다. 이 ht는 다른 계층을 향해 취쪽으로 출력되는 동시에 다음 시각의 RNN 계층(자기 자신)을 향해 오른쪽으로도 출력된다.
RNN은 h라는 상태를 가지고 있으며 식 5.9의 형태로 갱신된다고 해석할 수 있다. 그래서 RNN 계층을 상태를 가지는 계층 혹은 메모리가 있는 계층이라고 한다.
◾ 5.2.3 BPTT
위 그림에서 보듯 순환 구조를 펼친 후의 RNN에는 오차역전파법을 적용할 수 있다.
여기서의 오차역전파법은 시간 방향으로 펼친 신경망의 오차역전파법이란 뜻으로 BPTT(Backpropagation Through Time)이라고 한다.
긴 시계열 데이터를 학습할 때의 문제로는 시계열 데이터의 시간 크기가 커지는 것에 비례하여 BPTT가 소비하는 컴퓨팅 자원도 증가한다는 게 있다.
◾ 5.2.4 Truncated BPTT
큰 시계열 데이터를 취급할 때는 흔히 신경망을 연결을 적당한 길이로 끊는다. 이 잘라낸 작은 신경망에서 오차역전파법을 수행하게 된다. 이게 Truncated BPTT라는 기법이다.
Truncated BPTT에서는 신경망의 연결을 끊는다. 순전파의 흐름은 끊어지지 않고 전파된다. 역전파의 연결은 적당한 길이로 잘라내, 그 잘라낸 신경망 단위로 학습을 수행한다.
하지만 그 단위가 너무 길면 계산량과 메모리 사용량 등이 문제가 되기 때문에 아래 그림처럼 길게 뻗은 신경망의 역전파에서는 연결을 적당한 길이로 끊을 생각을 하게 된다.
각각의 블록 단위로 미래의 블록과는 독립적으로 오차역전파법을 완결시킬 수 있다.
그러므로 RNN을 학습시킬 때는 순전파가 연결된다는 점을 고려해야 한다. 데이터를 순서대로 입력해야 한다는 뜻이다.
◾ 5.2.5 Truncated BPTT의 미니배치 학습
배치 방식을 고려해 그림 5-14처럼 데이터를 순서대로 입력해야 한다. 그렇게 하려면 데이터를 주는 시작 위치를 각 미니배치의 시작 위치로 옮겨줘야 한다.
미니배치의 수를 두 개로 구성해 학습하려면 어떻게 해야 할까?
이 경우 RNN 계층의 입력 데이터로 첫 번째 미니배치 때는 처음부터 순서대로 데이터를 제공한다. 두번째 미니배치는 500번째의 데이터를 시작 위치로 정하고 그 위치부터 다시 순서대로 데이터를 제공하는 것이다.
미니배치 학습을 수행할 때는 각 미니배치의 시작 위치를 오프셋으로 옮겨준 후 순서대로 제공하면 된다. 또한 데이터를 순서대로 입력하다가 끝에 도달하면 다시 처음부터 입력하도록 한다.
데이터 제공 방법 면에서는 몇 가지를 주의해야 한다. 구체적으로는 데이터를 순서대로 제공하기와 미니배치별로 데이터를 제공하는 시작 위치를 옮기기 이다.
◼️ 5.3 RNN 구현
이때 Time RNN 계층 내에서 한 단계의 작업을 수행하는 계층을 RNN 계층이라고 하고 T개 단계분의 작업을 한꺼번에 처리하는 계층을 Time RNN 계층이라고 한다.
◾ 5.3.1 RNN 계층 구현
RNN의 순전파는 다음과 같다.
데이터를 미니 배치로 모아 처리하며 xt와 ht에는 각 샘플 데이터를 행 방향에 저장한다.
# common/time_layers.py
import sys
sys.path.append('..')
from common.np import *
from common.layers import *
from common.functions import sigmoid
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
RNN 계층의 순전파는 위와 같이 계산 그래프로 나타낼 수 있다. 여기서 수행하는 계산은 행렬의 곱인 'MatMul'과 덧셈인 +, tanh라는 3개의 연산으로 구성된다.
역전파는 다음과 같다.
def backward(self, dh_next):
Wx, Wh, b = self.params
x, h_prev, h_next = self.cache
dt = dh_next * (1 - h_next ** 2) # tanh 미분
db = np.sum(dt, axis=0)
dWh = np.dot(h_prev.T, dt) # shape: (H, N) x (N, H) = (H, H)
dh_prev = np.dot(dt, Wh.T) # shape: (N, H) x (H, H) = (N, H)
dWx = np.dot(x.T, dt) # shape: (D, N) x (N, H) = (D, H)
dx = np.dot(dt, Wx.T) # shape: (N, H) x (H, D) = (N, D)
self.grads[0][...] = dWx
self.grads[1][...] = dWh
self.grads[2][...] = db
return dx, dh_prev
◾ 5.3.2 Time RNN 계층 구현
Time RNN 계층은 T개의 RNN 계층으로 구성된다.
# common/time_layers.py
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
def set_state(self, h):
'''hidden state(h)를 설정하는 메서드'''
self.h = h
def reset_state(self):
'''hidden state(h)를 초기화하는 메서드'''
self.h = None
인수 중 stateful은 상태가 있는이라는 뜻의 단어이다.
stateful이 True 일 때 Time RNN 계층은 '상태가 있다'라고 말한다. 아무리 긴 시계열 데이터라도 Time RNN 계층의 순전파를 끊지 않고 전파한다는 의미가 된다.
stateful이 Fasle 일 때의 Time RNN 계층은 은닉 상태를 영행렬로 초기화하며 이를 상태가 없는 모드는 '무상태'라고 한다.
def forward(self, xs):
Wx, Wh, b = self.params
N, T, D = xs.shape # N(batch), T(time steps), D(input size)
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
Truncated BPTT를 수행하기 때문에 이 블록의 이전 시각 역전파는 필요하지 않는다.
위 그림의 Time RNN 계층에서 이뤄지는 역전파의 전체 그림이다.
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
◼️ 5.4 시계열 데이터 처리 계층 구현
RNN을 사용한 언어 모델은 영어로 RNN Language Model이므로 앞으로 RNNLM이라 칭하게 된다.
◾ 5.4.1 RNNLM의 전체 그림
첫번째 층은 Embedding 계층이다. 이 계층은 단어 ID를 단어의 분산 표현으로 변환한다. 그 분산 표현이 RNN 계층으로 입력이 된다. RNN 계층은 은닉 상태를 다음 층으로 출력함과 동시에 다음 시각의 RNN 계층으로 출력한다. RNN 계층이 위로 출력한 은닉 상태는 Affine 계층을 거쳐 softmax 계층으로 전해진다.
RNNLM은 지금까지 입력된 단어를 기억하고 그것을 바탕으로 다음에 출현할 단어를 예측한다. 이 일을 가능하게 하는 비결이 바로 RNN 계층의 존재이다. RNN 계층이 과거에서 현재로 데이터를 계속 흘려보내줌으로써 과거의 정보를 인코딩 해 저장할 수 있는 것이다.
◾ 5.4.2 Time 계층 구현
시계열 데이터를 한꺼번에 처리하는 계층을 Time Embedding, Time Affine 형태의 이름으로 구현한다.
Time Affine 계층은 위 그림처럼 Aiffne 계층을 T개 준비해서 각 시각의 데이터를 개별적으로 처리하면 된다.
Time Embedding 계층 역시 순전파 시에 T개의 Embedding 계층을 준비하고 각 Embedding 계층이 각 시각의 데이터를 처리한다.
Time softmax with Loss 계층도 시계열에 대한 평균을 구하는 것으로 데이터 1개 당 평균 손실을 구해 최종 출력으로 내보낸다.
◼️ 5.5 RNNLM 학습과 평가
◾ 5.5.1 RNNLM 구현
# chap05/simple_rnnlm.py
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
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()
그 결과 Time RNN 계층은 이전 시각의 은닉 상태를 계승할 수 있게 된다.
◾ 5.5.2 언어 모델의 평가
언어 모델의 예측 성능을 평가하는 척도로 퍼플렉서티(perplexity)를 자주 이용한다.
퍼플렉서티는 간단히 말해 확률의 역수이다.
위 그림에서 오른쪽 모델로 정답인 say의 확률이 0이라면 나쁜 예측이며 퍼플렉서티는 1/0.2 = 5가 된다.
퍼플렉서티는 작을수록 좋으며 이 5.0은 분기 수로 해석할 수 있다.
분기 수란 다음에 취할 수 있는 선택사항의 수를 말한다. 좋은 모델이 예측한 분기 수가 1.25라는 것은 다음에 출현할 수 있는 단어의 후보를 1개 정도로 좁혔다는 뜻이 된다. 반면에 나쁜 모델에서는 후보가 아직 5개 된다는 의미이다.
입력 데이터가 여러 개 일때는 아래 공식에 따라 계산된다.
◾ 5.5.3 RNNLM 학습 코드
%matplotlib inline
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
time_size = 5
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:] # 출력(정답 레이블)
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)
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):
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
ppl = np.exp(total_loss / loss_count)
print('| 에폭 %d | 퍼플렉서티 %.2f'
% (epoch+1, ppl))
ppl_list.append(float(ppl))
total_loss, loss_count = 0, 0
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
학습을 진행할 수록 퍼플렉서티가 순조롭게 낮아짐을 알 수 있다.
◾ 5.5.4 RNNLM의 Trainer 클래스
# chap05/train.py
%matplotlib inline
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
time_size = 5
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()
1. 미니배치를 순차적으로 만들어
2. 모델의 순전파와 역전파를 호출하고
3. 옵티마이저로 가중치를 갱신하고
4. 퍼플렉서티를 구한다.
◼️ 5.6 정리
- RNN은 순환하는 경로가 있고 이를 통해 내부에 은닉 상태를 기억할 수 있다.
- RNN의 순환 경로를 펼침으로써 다수의 RNN 계층이 연결 된 신경망으로 해석할 수 있으며 보통의 오차 역전팝버으로 학습할 수 있다.
- 긴 시계열 데이터를 학습할 때는 데이터를 적당한 길이씩 모으로 블록 단위로 BPTT에 의한 학습을 수행한다.
- Truncated BPTT 에서는 역전파의 연결만 끊으며 순전파의 연결을 유지하기 위해 데이터를 순차적으로 입력해야 한다.
- 언어 모델은 시퀀스를 확률로 해석한다.
- RNN 계층을 이용한 조건부 언어 모델은 그때까지 등장한 모든 단어의 정보를 기억할 수 있다.