[NLP 기초] 7. RNN을 사용한 문장 생성
7.1 언어 모델을 사용한 문장 생성
- 언어 모델로 문장 생성
7.1.1 RNN을 사용한 문장 생성의 순서
언어 모델에게 문장을 생성시키는 순서
ex) you say goodbye and I say hello 말뭉치로 학습
- I를 입력했을 때 다음의 확률분포 출력
다음 단어를 새로 생성하려면?
- 확률이 가장 높은 단어를 선택
= 확률이 가장 높은 단어 선택할 뿐. 결과가 일정하게 정해지는 '결정적'인 방법 - '확률적'으로 선택하는 방법
= 각 후보 단어의 확률에 맞게 선택, 확률 높은 단어일수록 선택되기 쉬움, 샘플링 단어 매번 다를 수 있음
매번 다른 문장 생성을 위해, 2번의 '확률적' 방법 사용
- say가 샘플링될 확률 가장 높음
- 필연적 x, 확률적 o
- 다른 단어들도 확률에 따라 샘플링될 가능성 있음
두번째 단어 샘플링
- 방금 생성한 단어인 'say'를 언어 모델에 입력하여 다음 단어의 확률분포 얻음
- 얻은 확률분포 기초로 다음 출현 단어 샘플링
이러한 작업을 원하는 만큼 반복
- 생성한 문장은 훈련 데이터에 존재하지 않는 생성 문장
→ 언어 모델은 훈련 데이터를 암기한 것이 아니라, 훈련 데이터에서 사용된 단어의 정렬 패턴 학습한 것
7.1.2 문장 생성 구현
문장 생성 코드 구현
- 앞 장 Rnnlm 클래스를 상속해 RnnlmGen 클래스 만들고, 생성 메서드 추가
* NOTE : 클래스 상속
- 기존 클래스를 계승하여 새로운 클래스를 만드는 매커니즘
- ex) 기반 클래스 이름 'Base'이고, 새로 정의할 클래스 이름 'New'라면 class New(Base)라고 씀
RnnlmGen 클래스 구현
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.functions import softmax
from ch06.rnnlm import Rnnlm
from ch06.better_rnnlm import BetterRnnlm
class RnnlmGen(Rnnlm):
def generate(self, start_id, skip_ids=None, sample_size=100):
word_ids = [start_id]
x = start_id
while len(word_ids) < sample_size:
x = np.array(x).reshape(1, 1)
score = self.predict(x)
p = softmax(score.flatten())
sampled = np.random.choice(len(p), size=1, p=p)
if (skip_ids is None) or (sampled not in skip_ids):
x = sampled
word_ids.append(int(x))
return word_ids
def get_state(self):
return self.lstm_layer.h, self.lstm_layer.c
def set_state(self, state):
self.lstm_layer.set_state(*state)
class BetterRnnlmGen(BetterRnnlm):
def generate(self, start_id, skip_ids=None, sample_size=100):
word_ids = [start_id]
x = start_id
while len(word_ids) < sample_size:
x = np.array(x).reshape(1, 1)
score = self.predict(x).flatten()
p = softmax(score).flatten()
sampled = np.random.choice(len(p), size=1, p=p)
if (skip_ids is None) or (sampled not in skip_ids):
x = sampled
word_ids.append(int(x))
return word_ids
def get_state(self):
states = []
for layer in self.lstm_layers:
states.append((layer.h, layer.c))
return states
def set_state(self, states):
for layer, state in zip(self.lstm_layers, states):
layer.set_state(*state)
generate(start_id, skip_ids, sample_size) : 문장 생성 수행 메서드
- start_id : 최초로 주는 단어의 ID
- sample_size : 샘플링하는 단어의 수
- skip_ids : 단어 ID의 리스트 (이 리스트에 속하는 단어 ID는 샘플링되지 않도록)
generate() 메서드 내부
- model.predict(x)를 호출해 각 단어의 점수 출력
- p=softmax(score) 코드에서 이 점수들 소프트맥스 함수를 이용해 정규화 → 확률분포 p 얻음
- 확률분포 p로부터 다음 단어 샘플링
RnnlmGen 클래스를 사용해 문장 생성
# coding: utf-8
import sys
sys.path.append('..')
from rnnlm_gen import RnnlmGen
from dataset import ptb
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
corpus_size = len(corpus)
model = RnnlmGen()
model.load_params('../ch06/Rnnlm.pkl')
# start 문자와 skip 문자 설정
start_word = 'you' # 첫 단어
start_id = word_to_id[start_word]
skip_words = ['N', '<unk>', '$'] # 샘플링하지 않을 단어
skip_ids = [word_to_id[w] for w in skip_words]
# 문장 생성
word_ids = model.generate(start_id, skip_ids)
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace(' <eos>', '.\n')
print(txt)
- 처음에는 엉터리로 나열한 글이 출력
- 학습을 수행한 후에는 점차 그럴듯한 문장 출력
- 그러나 여전히 결과에서 개선해야 할 여지가 보임 → 더 나은 모델 사용
7.1.3 더 좋은 문장으로
- 앞 장에서 구현한 더 좋은 언어 모델 BetterRnnlm 클래스로 구현
- 이 클래스를 상속하고, 문장 생성 기능 추가
- 전보다 자연스러운 결과
7.2 seq2seq
시계열 데이터를 또 다른 시계열 데이터로 변환하는 문제 다뤄보자
- 2개의 RNN을 사용하는 seq2seq 방법
7.2.1 seq2se의 원리
seq2seq를 Encoder-Decoder 모델이라고도 함
- Encoder : 입력 데이터를 인코딩(부호화)
- Decoder : 인코딩된 데이터를 디코딩 (복호화)
* NOTE
- 인코딩(부호화) : 정보를 어떤 규칙에 따라 변환
- 디코딩(복호화) : 인코딩된 정보를 원래의 정보로 되돌림
ex) '나는 고양이로소이다' 문장을 'I am a cat' 으로 변역
- Encoder : 인코딩한 정보에는 번역에 필요한 정보가 조밀하게 응출
- Decoder : 조밀하게 응축된 이 정보를 바탕으로 도착어 문장 생성
→ Encoder와 Decoder가 협력하여 시계열 데이터를 다른 시계열 데이터로 변환, Encoder와 Decoder로 RNN 사용
Encoder
- RNN을 이용해 시계열 데이터를 h라는 은닉 상태 벡터로 변환
(여기서는 RNN으로 LSTM 사용) - Encoder가 출력하는 벡터 h는 LSTM 계층의 마지막 은닉 상태
- 은닉 상태 h에 입력 문장(출발어)를 번역하는데 필요한 정보 인코딩
- LSTM의 은닉 상태 h는 고정 길이 벡터
→ 인코딩한다 = 임의 길이의 문장을 고정 길이 벡터로 변환
Decoder
- Decoder는 인코딩된 벡터를 어떻게 요리하여 도착어 문장 생성?
→ 앞 절에서 다룬 문장 생성 모델 그대로 이용 - Decoder는 앞 절의 신경망과 완전히 같은 구성
!= LSTM 계층이 벡터 h를 입력받는다는 점이 다름 (앞 절 언어모델에서는 LSTM 계층 아무것도 받지 않음) - seq2seq는 LSTM 두 개로 구성(Encoder & Decoder)
- LSTM의 은닉 상태 h가 Encoder와 Decoder를 이어주는 '가교' 역할
- 순전파 : Encoder에서 인코딩된 정보가 LSTM 계층의 은닉 상태를 통해 Decoder에 전해짐
- 역전파 : 가교(h)를 통해 기울기가 Decoder로부터 Encoder로 전해짐
7.2.2 시계열 데이터 변환용 장난감 문제
장난감 문제
- 머신러닝을 평가하고자 만든 간단한 문제
시계열 변환 문제의 예로 '더하기' 다뤄보자
- '덧셈 문제'에서는 단어가 아닌 '문자' 단위로 분할
7.2.3 가변 길이 시계열 데이터
'덧셈'은 가변 길이 시계열 데이터
신경망 학습 시 '미니배치 처리'를 위해 무언가 추가 노력
* NOTE
- 미니배치로 학습할 때는 다수의 샘플 한꺼번에 처리
- 샘플의 데이터 형상 모두 같아야 함
방안
패딩
- 원래의 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일하게 맞추는 기법
- 0~ 999 사이의 숫자 2개만 더함 →최대 4 문자
- 데이터에 패딩을 수행해 모든 샘플 데이터 길이 통일
- 질문과 정답을 구분하기 위해 출력 앞에 구분자로 밑줄(_) → 총 5문자로 통일, Decoder 문자열 생성 신호로 사용
패딩의 장단점
- 장점 : 패딩로 데이터 크기를 통일하면 가변 길이 시계열 데이터 처리할 수 있음
- 단점 : 존재하지 않던 패딩용 문자까지 seq2seq가 처리하게 됨
seq2seq에 패딩 전용 처리 추가
- Decoder에 입력된 데이터가 패딩이라면, 손실 결과에 반영하지 않음 (Softmax with Loss 계층에 '마스크' 기능 추가)
- Encoder에 입력된 데이터가 패딩이라면, LSTM 계층이 이전 시각의 입력을 그대로 출력
= LSTM 계층은 마치 처음부터 패딩 존재하지 않았던 것처럼 인코딩
7.2.4 덧셈 데이터셋
예제 학습 데이터(덧셈 예 총 50,000개)
# coding: utf-8
import sys
sys.path.append('..')
from dataset import sequence
(x_train, t_train), (x_test, t_test) = \
sequence.load_data('addition.txt', seed=1984)
char_to_id, id_to_char = sequence.get_vocab()
print(x_train.shape, t_train.shape)
print(x_test.shape, t_test.shape)
# (45000, 7) (45000, 5)
# (5000, 7) (5000, 5)
print(x_train[0])
print(t_train[0])
# [ 3 0 2 0 0 11 5]
# [ 6 0 11 7 5]
print(''.join([id_to_char[c] for c in x_train[0]]))
print(''.join([id_to_char[c] for c in t_train[0]]))
# 71+118
# _189
- load_data(file_name, seed)는 file_name으로 지정한 텍스트 파일 읽어 문자 ID로 변환
- 훈련과 데이트 데이터로 나눠 반환
- seed : 메서드 내부 무작위수의 초깃값
- get_vocab() 메서드 : 문자와 문자 ID의 대응 관계를 담은 딕셔너리
- sequence 모듈 : seq2seq 용 데이터 간단히 읽어들임
- x_train, t_train : 문자 ID 저장
- char_to_id & id_to_char : 상호 변환용
7.3 seq2seq 구현
- seq2seq는 2개의 RNN을 연결한 신경망
- Encoder 클래스와 Decoder 클래스로 각각 구현
- 두 클래스 연결하는 Seq2seq 클래스 구현
7.3.1 Encoder 클래스
문자열을 받아 벡터 h로 변환
RNN을 이용해 Encoder를 구성 → Embedding 계층 & LSTM 계층
- Embedding 계층
: 문자(정확히 문자 ID)를 문자 벡터로 변환 & LSTM 계층으로 입력 - LSTM 계층
: 오른쪽(시간 방향)으로 은닉 상태와 셀 출력 & 위쪽으로는 은닉 상태만 출력
(더 위에 다른 계층 없으므로 LSTM 계층의 위쪽 출력 폐기) - Encoder에서 마지막 문자를 처리한 후 LSTM 계층의 은닉 상태 h 출력, h를 Decoder로 전달
* WARNING
- Encoder에서는 LSTM의 은닉 상태만 Decoder에 전달
시간 방향을 한꺼번에 처리하는 계층 : Time LSTM, Time Embedding
Encoder 클래스 코드
# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from common.base_model import BaseModel
class Encoder:
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')
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')
self.embed = TimeEmbedding(embed_W)
# Time LSTM 계층이 상태를 유지하지 않으므로 stateful=False로 설정
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)
self.params = self.embed.params + self.lstm.params
self.grads = self.embed.grads + self.lstm.grads
self.hs = None
def forward(self, xs):
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
self.hs = hs
return hs[:, -1, :]
def backward(self, dh):
dhs = np.zeros_like(self.hs)
dhs[:, -1, :] = dh
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
초기화, 순전파, 역전파 메서드 제공
- 순전파
- Time Embeddimg 계층과 Time LSTM 계층의 forward() 메서드 호출
- 마지막 시각의 은닉 상태 추출해 출력으로 반환 - 역전파
- LSTM 계층의 마지막 은닉 상태에 대한 기울기가 dh 인수로 전해짐 (* dh 인수 : Decoder가 전해주는 기울기)
- 원소가 모두 0인 텐서 dhs를 생성, dh를 dhs의 해당 위치에 할당
- Time LSTM 계층과 Time Embedding 계층의 backward() 메서드 호출
* WARNING
- 5, 6장 모델은 '긴 시계열 데이터' 하나뿐인 문제 = stateful을 True로 설정, 은닉 상태 유지하여 긴 데이터 처리
- 이번에는 '짧은 시계열 데이터' 여러 개인 문제 = 문제마다 LSTM의 은닉 상태를 다시 초기화한 상태(영벡터)로 설정
7.3.2 Decoder 클래스
Decoder 클래스
- Encoder 클래스가 출력한 h를 받아 목적으로 하는 다른 문자열을 출력
- RNN으로 구현(여기에서는 LSTM)
Decoder의 학습 시 계층 구성
* WARNING : RNN으로 문장 생성 시, 학습과 생성의 데이터 부여 방법
- 학습 : 정답을 알고 있기 때문에 시계열 방향의 데이터 한꺼번에 줄 수 있음
- 추론 : 최초 시작을 알리는 구분 문자(EX. _) 하나만 줌
- 출력으로부터 문자를 하나를 샘플링하여, 그 샘플링한 문자를 다음 입력으로 사용하는 과정 반복
이번 문제는 '덧셈'
- '확률적'이 아닌 '결정적'으로 선택 : 점수가 가장 높은 문자 하나만
Decoder가 문자열을 생성시키는 흐름
- argmax 노드 : 최댓값을 가진 원소의 인덱스(ex. 문자 ID)를 선택하는 노드
- Softmax 계층이 아닌 Affine 계층이 출력하는 점수가 가장 큰 문자 ID 선택
* WARNING
- Softmax 계층은 입력된 벡터를 정규화 → 대소 관계는 바뀌지 않음
- 그러므로 [그림 7-18]의 경우 Softmax 계층 생략
- Softmax with Loss 계층은 이후에 구혀하는 seq2seq 클래스에서 처리
Decoder 클래스 구현
- Time Embedding, Time LSTM, Time Affine의 3가지 계층으로 구성
- 초기화, 순전파, 역전파
class Decoder:
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')
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.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.affine = TimeAffine(affine_W, affine_b)
self.params, self.grads = [], []
for layer in (self.embed, self.lstm, self.affine):
self.params += layer.params
self.grads += layer.grads
def forward(self, xs, h):
self.lstm.set_state(h)
out = self.embed.forward(xs)
out = self.lstm.forward(out)
score = self.affine.forward(out)
return score
def backward(self, dscore):
dout = self.affine.backward(dscore)
dout = self.lstm.backward(dout)
dout = self.embed.backward(dout)
dh = self.lstm.dh
return dh
def generate(self, h, start_id, sample_size):
sampled = []
sample_id = start_id
self.lstm.set_state(h)
for _ in range(sample_size):
x = np.array(sample_id).reshape((1, 1))
out = self.embed.forward(x)
out = self.lstm.forward(out)
score = self.affine.forward(out)
sample_id = np.argmax(score.flatten())
sampled.append(int(sample_id))
return sampled
backward() 메서드
- 위쪽의 Softmax with Loss 계층으로부터 기울기 dscore를 받아 Time Affine 계층, Time LSTM 계층, Time Embedding 계층 순서로 전파
- Time LSTM 계층의 시간 방향으로의 기울기는 TimeLSTM 클래스의 인스턴스 변수 dh에 저장
- 시간 방향 기울기 dh를 꺼내서 Decoder 클래스의 backward()의 출력으로 반환
generate() 메서드
- Decoder 클래스에 문장 생성
- 문자를 1개씩 주고 Affine 계층이 출력하는 점수가 가장 큰 문자 ID 선택
* WARNING
- 이번 문제에서는 Encoder의 출력 h를 Decoder의 Time LSTM 계층의 상태로 설정(stateful)
- 즉, Encoder의 h를 유지하면서 순전파 이뤄짐
7.3.3 Seq2seq 클래스
- Encoder 클래스와 Decoder 클래스 연결
- Time Softmax with Loss 계층을 이용해 손실 계산
- 기능들을 제대로 연결
class Seq2seq(BaseModel):
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
self.encoder = Encoder(V, D, H)
self.decoder = Decoder(V, D, H)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
def forward(self, xs, ts):
decoder_xs, decoder_ts = ts[:, :-1], ts[:, 1:]
h = self.encoder.forward(xs)
score = self.decoder.forward(decoder_xs, h)
loss = self.softmax.forward(score, decoder_ts)
return loss
def backward(self, dout=1):
dout = self.softmax.backward(dout)
dh = self.decoder.backward(dout)
dout = self.encoder.backward(dh)
return dout
def generate(self, xs, start_id, sample_size):
h = self.encoder.forward(xs)
sampled = self.decoder.generate(h, start_id, sample_size)
return sampled
7.3.4 seq2seq 평가
seq2seq의 학습은 기본적인 신경망의 학습과 같은 흐름
- 학습 데이터에서 미니배치를 선택
- 미니배치로부터 기울기 계산
- 기울기를 사용하여 매개변수 갱신
- Trainer 클래스를 사용해 위 규칙대로 작업 수행
- 매 에폭마다 seq2seq가 테스트 데이터를 풀게 하여 학습 중간중간 정답률 측정
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from dataset import sequence
from common.optimizer import Adam
from common.trainer import Trainer
from common.util import eval_seq2seq
from seq2seq import Seq2seq
from peeky_seq2seq import PeekySeq2seq
# 데이터셋 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
char_to_id, id_to_char = sequence.get_vocab()
# 입력 반전 여부 설정 =============================================
is_reverse = False # True
if is_reverse:
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
# ================================================================
# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 128
batch_size = 128
max_epoch = 25
max_grad = 5.0
# 일반 혹은 엿보기(Peeky) 설정 =====================================
model = Seq2seq(vocab_size, wordvec_size, hidden_size)
# model = PeekySeq2seq(vocab_size, wordvec_size, hidden_size)
# ================================================================
optimizer = Adam()
trainer = Trainer(model, optimizer)
acc_list = []
for epoch in range(max_epoch):
trainer.fit(x_train, t_train, max_epoch=1,
batch_size=batch_size, max_grad=max_grad)
correct_num = 0
for i in range(len(x_test)):
question, correct = x_test[[i]], t_test[[i]]
verbose = i < 10
correct_num += eval_seq2seq(model, question, correct,
id_to_char, verbose, is_reverse)
acc = float(correct_num) / len(x_test)
acc_list.append(acc)
print('검증 정확도 %.3f%%' % (acc * 100))
# 그래프 그리기
x = np.arange(len(acc_list))
plt.plot(x, acc_list, marker='o')
plt.xlabel('에폭')
plt.ylabel('정확도')
plt.ylim(0, 1.0)
plt.show()
- 평가 척도로 정답률 사용 (* 정답률 : 에폭마다 테스트 데이터의 문제 중 몇 개를 풀게 하여 올바르게 답했는지 채점)
- seq2seq는 초기에는 정답을 잘 맞히지 못했으나, 학습을 거듭할수록 조금씩 정답에 가까워짐
- 에폭을 거듭함에 따라 정답률이 착실하게 상승
7.4 seq2seq 개선
seq2seq를 세분화하여 학습 '속도' 개선
7.4.1 입력 데이터 반전(Reverse)
개선1
- 입력 데이터의 순서 반전
- 학습 진행이 빨라져서 결과적으로 최종 정확도가 좋아짐
- 정답률을 높여줌, 학습 진행 개선
왜 입력 데이터 반전으로 학습 진행이 빨라지고 정확도가 향상되는가?
- 기울기 전파가 원활해지기 때문
ex) 나는 고양이로소이다 - I am a cat
- 나 - I 로의 변환을 위해서는 네 단계 분량의 LSTM 계층 거쳐야 함
- 역전파 시 I로부터 전해지는 기울기가 나에 도달하기까지, 먼 거리만큼 영향을 더 받음
- 입력문을 반전하면(이다 로소 고양이 는) 나와 I는 바로 옆이 되어 기울기 직접 전해짐
- 입력 문장의 첫 부분에서는 반전 덕분에 대응하는 변환 후 단어와 가까움
- 기울기가 더 잘 전해져서 학습 효율이 좋아짐
- 다만, 입력 데이터를 반전해도 단어 사이의 '평균'적인 거리는 그대로
7.4.2 엿보기(Peeky)
개선2
인코딩된 정보를 Decoder의 다른 계층에도 전해주는 기법
- Encoder는 입력 문장(문제 문장)을 고정 길이 벡터 h로 변환
- h 안에는 Decoder에게 필요한 정보가 모두 담김
- h가 Decoder에게 유일한 정보, 그러나 최초 시각의 LSTM 게층만이 벡터 h 이용
→ 중요한 정보가 담긴 Encoder의 출력 h를 Decoder의 다른 계층에도 전해줌
- 모든 시각의 Affine 계층과 LSTM 계층에 Encoder의 출력 h를 전함
- 하나의 LSTM만이 소유하던 중요 정보 h를 여러 계층(이 예에서 총 8개 계층)이 공유
ex) 집단 지성에 비유 : 중요한 사람을 한 사람의 독점이 아닌 많은 사람과 공유 = 올바른 결정
- LSTM 계층과 Affine 계층에 입력되는 벡터 2개씩이 됨 = 실제로는 두 벡터가 연결(concatenate)
- concat 노드 : 두 벡터를 연결
class PeekyDecoder:
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')
lstm_Wx = (rn(H + D, 4 * H) / np.sqrt(H + 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 + H, V) / np.sqrt(H + H)).astype('f')
affine_b = np.zeros(V).astype('f')
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.affine = TimeAffine(affine_W, affine_b)
self.params, self.grads = [], []
for layer in (self.embed, self.lstm, self.affine):
self.params += layer.params
self.grads += layer.grads
self.cache = None
def forward(self, xs, h):
N, T = xs.shape
N, H = h.shape
self.lstm.set_state(h)
out = self.embed.forward(xs)
hs = np.repeat(h, T, axis=0).reshape(N, T, H)
out = np.concatenate((hs, out), axis=2)
out = self.lstm.forward(out)
out = np.concatenate((hs, out), axis=2)
score = self.affine.forward(out)
self.cache = H
return score
초기화
- LSTM 계층의 가중치와 Affine 계층의 가중치의 형상이 달라짐
순전파
- h를 np.repeat()로 시계열만큼 복제해 hs에 저장
- np.concatenate()를 이용해 hs와 Enbedding 계층의 출력을 연결
- 이를 LSTM 계층에 입력
- Affine 계층에도 hs와 LSTM 계층의 출력을 연결한 것 입력
입력 반전과 엿보기를 적용한 결과
Peeky를 추가로 적용하자 seq2seq의 결과가 월등히 좋아짐
- Reverse : 입력 문장 반전
- Peeky : Encoder의 정보를 널리 퍼지게
=> 만족할 만한 결과
7.5seq2seq를 이용하는 애플리케이션
seq2seq : 한 시계열 데이터를 다른 시계열 데이터로 변환
ex)
- 기계 번역 : 한 언어의 문장을 다른 언어의 문장으로 변환
- 자동 요약 : 긴 문장을 짧게 요약된 문장으로 변환
- 질의응답 : 질문을 응답으로 변환
- 메일 자동 응답 : 받은 메일의 문장을 답변 글로 변환
자연어 외에도 음성, 영상 등에도 이용할 수 있음
7.5.1 챗봇
= 사람과 컴퓨터가 텍스트로 대화를 나누는 프로그램
- '상대의 말'을 '자신의 말'로 변환하는 문제
- 대화의 텍스트 데이터가 준비되면 그것으로 seq2seq 학습
7.5.2 알고리즘 학습
- 고차원적인 파이썬 코리 처리를 수행할 수 있음
- 소스 코드도 문자로 쓰여진 시계열 데이터
- 소스 코드를 그대로 seq2seq에 입력, 원하는 답과 대조하여 학습
7.5.3 이미지 캡셔닝
= 이미지를 문장으로 변환
- Encoder가 LSTM에서 합성곱 신경망(CNN)으로 바뀜
- Decoder는 지금까지 똑같은 신경망을 이용
- 이미지의 인코딩을 CNN이 수행
- CNN의 최종 출력은 특징 맵 (* 특징맵 : 3차원 - 높이, 폭, 채널)
- 이를 Decoder의 LSTM이 처리할 수 있도록 손질
- 특징 맵을 1차원으로 평탄화
- 완전연결인 Affine 계층에서 변환 - 변환된 데이터를 Decoder에 전달
이미지 캡셔닝 수행 예
7.6 정리
- RNN을 이용한 언어 모델은 새로운 문장을 생성할 수 있다.
- 문장을 생성할 때는 하나의 단어(혹은 문자)를 주고 모델의 출력(확률분포)에서 샘플링하는 과정을 반복한다.
- RNN을 2개 조합함으로써 시계열 데이터를 다른 시계열 데이터로 변환할 수 있다.
- seq2seq는 Encoder가 출발어 입력문을 인코딩하고, 인코딩된 정보를 Decoder가 받아 디코딩하여 도착어 출력문을 얻는다.
- 입력문을 반전시키는 기법(Reverse), 도는 인코딩된 정보를 Decoder의 여러 계층에 전달하는 기법(Peeky)은 seq2seq의 정확도 향상에 효과적이다.
- 기계 번역, 챗봇, 이미지 캡셔닝 등 seq2seq는 다양한 애플리케이션에 이용할 수 있다.