NIRVANA
[밑바닥부터 시작하는 딥러닝2] 7장. RNN을 사용한 문장 생성 본문
언어 모델을 사용한 문장 생성
RNN을 사용한 문장 생성의 순서
"I"라는 단어를 주었을 때, 다음 단어를 생성하는 방법
- 확률이 가장 높은 단어를 선택
- 확률이 가장 높은 단어를 선택할 뿐 → 결과가 일정하게 정해지는 '결정적' 방법이 될 것
- 확률적으로 선택하는 방법
- 각 후보 단어의 확률에 맞게 선택 → 선택되는 단어가 매번 다를 수 있음
- 필연적이지 않고, '확률적'으로 결정됨을 주의 = 다른 단어들도 해당 단어의 출현 확률에 따라 정해진 비율만큼 샘플링될 가능성이 있다는 뜻
- 종결 기호가 나오기 전까지 반복하다 보면, 새로운 문장을 생성할 수 있게 됨
문장 생성 구현
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
calss RnnlmGen(Rnnlm):
#문장 생성 수행 메서드
def generate(self, start_id, skip_ids=None, sample_size=100): #start_id: 최초로 주는 단어 ID / sample_size: 샘플링하는 단어 수 / skip_ids: 단어 ID의 리스트
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
문장 생성 코드
import sys
sys.path.append('..')
from rnnlm_gen import RnnlmGen
fromd 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()
#시작 문자와 건너뜀 문자 설정
satart_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)
seq2seq
- 시계열 데이터를 다른 시계열 데이터로 변환하는 모델, 2개의 RNN을 사용함
seq2seq의 원리
- 입력 데이터를 인코딩하는 Encoder와 데이터를 디코딩하는 Decoder가 존재함
- 인코더와 디코더는 각각 시계열 데이터를 변환하는 역할을 맡음
- 인코더와 디코더가 협력하여 시계열 데이터를 다른 시계열 데이터로 변환하는 것
ex) '나는 고양이로소이다"를 " I am a cat"으로 번역하기
- '나는 고양이로소이다'라는 출발어 문장을 인코딩 진행
- 인코딩한 정보를 디코더에 전달, 디코더가 도착어 문장을 생성
- 인코더가 인코딩한 정보에는 번역에 필요한 정보가 조밀하게 응축된 상태
- 디코더는 조밀하게 응축된 이 정보를 바탕으로 도착어 문장을 생성함
인코더와 디코더로 RNN사용하기
- 인코더가 RNN을 이용해 시계열 데이터를 h라는 은닉 상태 벡터로 변환함
- 인코더가 출력하는 벡터h는 LSTM 계층의 마지막 은닉 상태가 됨
- LSTM으 은닉 상태 h는 고정 길이 벡터,
- 인코딩 한다 = 임의 길이의 문장을 고정 길이 벡터로 변환
- 디코더에서는 앞에서 배운 신경망과 완전히 같은 구성이지만, LSTM 계층이 벡터 h를 입력받는다는 점에 차이가 존재함
- seq2seq는 LSTM 두 개(Encoder의 LSTM과 Decoder의 LSTM)로 구성되고, LSTM 계층의 은닉 상태가 Encoder와 Decoder를 이어주는 가교 역할을 함
- 순전파 시에는 Encoder에서 인코딩된 정보가 LSTM 계층의 은닉 상태를 통해 Decoder에 전해짐
- seq2seq의 역전파 때는 가교를 통해 기울기가 Decoder로부터 Encoder로 전해짐
시계열 데이터 변환용 장난감 문제
- word2vec이나 언어 모델 등에서 문장을 '단어' 단위로 분할했지만, 문장을 반드시 단어로 분할할 필요는 없음
=> 문자 단위로 나누어서 덧셈 문제 풀이를 진행
가변 길이 시계열 데이터
- 가변 길이 시계열 데이터를 미니배치로 학습하기 위한 가장 단순한 방법은 패딩을 사용하는 것
- 패딩: 원래의 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일하게 맞추는 기법
- 패딩용 문자까지 seq2seq 모델이 처리하게 되면서 seq2seq에 패딩 전용 처리를 추가해야 함
- 디코더에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 함
- softmax with Loss 계층에 '마스크' 기능을 추가하여 해결 가능
- 인코더에 입력된 데이터가 패딩이라면 LSTM 계층이 이전 시각의 입력을 그대로 출력하게 함
- LSTM 계층은 처음부터 패딩이 존재하지 않았던 것처럼 인코딩 가능
- 디코더에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 함
seq2seq 구현
Encoder 클래스
- RNN-LSTM을 이용하여 구현
- 임베딩 계층에서는 문자(문자ID)를 문자 벡터로 변환 후 LSTM 계층으로 전달
- LSTM 계층에서는 오른쪽(시간 방향)으로는 은닉 상태와 셀을 출력하고, 위쪽으로는 은닉 상태만 출력
- 인코더에서 마지막 문자 처리 후 LSTM 계층의 은닉 상태 h를 출력
- 은닉 상태h를 디코더로 전달
초기화 메서드
class Encoder:
#vocab_size: 어휘 수 - 문자의 종류
#wordvec_size: 문자 벡터의 차원 수
#hidden_size: LSTM 계층의 은닉 상태 벡터 차원 수
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)
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):
#TimeEmbeddig 계층과 Time LSTM 계층의 순전파 메서드 호출
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
self.hs = hs
return hs[:, -1, :] #마지막 은닉 상태 추출, 엔코더의 순전파 메서드 출력으로 반환
def backward(self, dh): #LSTM 계층의 마지막 은닉 상태에 대한 기울기를 dh로 입력 받음 -> decoder가 전달
dhs = np.zeros_like(self.hs)
dhs[:, -1, :] = dh
#TimeLSTM 계층과 TimeEmbedding 계층의 역전파 메서드 호출
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
Decoder 클래스
- 디코더 클래스는 인코더 클래스가 출력한 h를 받아 목적으로 하는 다른 문자열을 출력함
- 인코더와 마찬가지로 RNN으로 구현 가능 - LSTM 계층 사용하면 됨
- argmax 노드: 최댓값을 가진 원소의 인덱스를 선택하는 노드
- 디코더는 학습 시와 생성시에 softmax 계층을 다르게 취급함 << 주의!
#softmax with loss계층은 이후에 구현할 seq2seq 클래스에서 처리할 예정
#사유: 디코더는 학습 시와 생성 시에 소프트맥스 계층을 다르게 취급함
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
- backforward 메서드는 위쪽의 softmax with Loss 계층으로부터 기울기 dscore를 받아 Time Affine 계층, Time LSTM 계층, Time Embedding 계층 순서로 전파 시킴
- Time LSTM 계층의 시간 방향으로의 기울기는 TimeLSTM 클래스의 인스턴스 변수 dh에 저장되어 있음
- 이 시간 방향의 기울기 dh를 꺼내서 디코더 클래스의 백워드의 출력으로 반환함
- Time LSTM 계층의 시간 방향으로의 기울기는 TimeLSTM 클래스의 인스턴스 변수 dh에 저장되어 있음
#문장 생성 담당
#h: 인코더로부터 받은 은닉 상태
#start_id: 최초로 주어지는 문자 ID
#sample_size: 생성하는 문자 수
#문자를 1개씩 주고, Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택하는 작업을 반복
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
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
seq2seq 평가
- 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()
# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 128
batch_size = 128
max_epoch = 25
max_grad = 5.0
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))
seq2seq 개선
- 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()
# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 128
batch_size = 128
max_epoch = 25
max_grad = 5.0
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()
- 기울기 전파가 원활해지기 떄문에 학습의 진행 속도가 빨라지고, 정확도가 향상되는 것으로 추정됨
엿보기(Peeky)
- Encoder는 입력문장을 고정 길이 벡터 h로 변환하는 역할을 맡음
- h안에는 Decoder에게 필요한 정보가 모두 담겨져 있음 → h가 Decoder에 있어서는 유일한 정보가 됨
- 현재의 seq2seq는 최초 시각의 LSTM 계층만이 벡터h를 이용하고 있음
- 중요한 정보가 담긴 Encoder의 출력 h를 Decoder의 다른 계층에도 전해줌
- LSTM 계층과 Affine 계층에 입력되는 벡터가 2개씩이 됨 → 실제로는 두 벡터가 연결된 것을 의미함
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) #시계열만큼 복제하여 hs에 저장
out = np.concatenate((hs, out), axis=2) #hs와 embedding 계층의 출력을 연결
out = self.lstm.forward(out) #lstm 계층에 입력
out = np.concatenate((hs, out), axis=2) #affine에도 hs와 lstm계층의 출력 연결한 값을 입력
score = self.affine.forward(out)
self.cache = H
return score
def backward(self, dscore):
H = self.cache
dout = self.affine.backward(dscore)
dout, dhs0 = dout[:, :, H:], dout[:, :, :H]
dout = self.lstm.backward(dout)
dembed, dhs1 = dout[:, :, H:], dout[:, :, :H]
self.embed.backward(dembed)
dhs = dhs0 + dhs1
dh = self.lstm.dh + np.sum(dhs, axis=1)
return dh
def generate(self, h, start_id, sample_size):
sampled = []
char_id = start_id
self.lstm.set_state(h)
H = h.shape[1]
peeky_h = h.reshape(1, 1, H)
for _ in range(sample_size):
x = np.array([char_id]).reshape((1, 1))
out = self.embed.forward(x)
out = np.concatenate((peeky_h, out), axis=2)
out = self.lstm.forward(out)
out = np.concatenate((peeky_h, out), axis=2)
score = self.affine.forward(out)
char_id = np.argmax(score.flatten())
sampled.append(char_id)
return sampled
PeekySeq2Seq 클래스 구현
class PeekySeq2seq(Seq2seq):
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 = PeekyDecoder(V, D, H)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
Seq2seq를 이용하는 애플리케이션
- seq2seq를 이용하는애플리케이션
- 기계번역: "한 언어의 문장"을 "다른 언어의 문장"으로 변환
- 알자동 요약: "긴 문장"을 "짧게 요약된 문장"으로 변환
- 질의응답: "질문"을 "응답"으로 변환
- 메일 자동 응답: "받은 메일의 문장"을 "답변 글"로 변환
- seq2seq는 짝을 이루는 시계열 데이터를 다루는 문제에 이용 가능
챗봇
- 대화는 상대의 말과 자신의 말로 구성되므로 상대의 말을 자신의 말로 변환하는 문제로 볼 수 있음
알고리즘 학습
- 소스 코드 역시 문자로 쓰여진 시계열 데이터의 일환임
이미지 캡셔닝
- '이미지'를 '문장'으로 변환하는 것
- Encoder가 LSTM에서 합성곱 신경망으로 변경됨 - Decoder는 변경 X
- CNN의 결과로 나온 특징 맵을 1차원으로 평탄화한 후, 완전연결인 Affine 계층에서 변환함
- 변환한 데이터를 Decoder에 전달
'AI' 카테고리의 다른 글
[밑바닥부터 시작하는 딥러닝2] 8장. 어텐션 (1) | 2024.03.22 |
---|---|
[밑바닥부터 시작하는 딥러닝2] 6장. 게이트가 추가된 RNN (0) | 2024.03.20 |
[밑바닥부터 시작하는 딥러닝2] 5장. 순환 신경망(RNN) (0) | 2024.03.18 |
[밑바닥부터 시작하는 딥러닝2] 4장. word2vec 속도 개선 (5) | 2024.03.18 |
[밑바닥부터 시작하는 딥러닝2] 3장 word2vec (2) | 2024.03.17 |