-
[밑시딥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 레이어는 그 레이어로의 입력과 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 구현
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 레이어 구현
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의 전체 그림
Embedding 레이어를 통해 분산 표현으로 바꾼 뒤 RNN으로 학습한다.
Affine 레이어를 통해 한번 처리하고 Softmax를 통해 최종적으로 다음에 올 수 있는 단어에 대한 확률을 표시한다.
Time 레이어 구현
T개의 시계열 데이터를 한번에 처리하는 레이어를 Time 레이어라고 부른다.
여러 레이어를 생성해서 사용하는 대신 행렬 연산을 통해 구현한다.
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)
'A.I > Study' 카테고리의 다른 글
[Math for Deeplearning] More Probability (0) 2023.07.21 [밑시딥2] 게이트가 추가된 RNN (0) 2023.07.14 [Math for Deeplearning] Linear Algebra (0) 2023.07.08 [밑시딥2] word2vec 속도 개선 (0) 2023.07.02 [Math for Deeplearning] Probability (0) 2023.07.01