어텐션을 갖춘 seq2seq 구현
Encoder 구현
앞의 Encoder 클래스는 LSTM의 계층의 마지막 은닉 상태 벡터를 반환한 반면, 모든 은닉 상태를 반환하여 구현한다.
# coding: utf-8
import sys
sys.path.append('..')
from time_layers import *
from seq2seq import Encoder, Seq2seq
from attention_layer import TimeAttention
class AttentionEncoder(Encoder):
def forward(self, xs):
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
return hs #모든 은닉상태 벡터
def backward(self, dhs):
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
Decoder 구현
class AttentionDecoder:
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(2*H, V) / np.sqrt(2*H)).astype('f')
affine_b = np.zeros(V).astype('f')
#layer 초기화
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.attention = TimeAttention() #time attention추가
self.affine = TimeAffine(affine_W, affine_b)
layers = [self.embed, self.lstm, self.attention, self.affine]
self.params, self.grads = [], []
for layer in layers:
self.params += layer.params #파라미터와 기울기 저장
self.grads += layer.grads
def forward(self, xs, enc_hs):
h = enc_hs[:,-1] #lstm 계층 은닉상태 벡터가져오기
self.lstm.set_state(h)
out = self.embed.forward(xs)
dec_hs = self.lstm.forward(out) #decoder의 lstm 은닉벡터
c = self.attention.forward(enc_hs, dec_hs) 디코더 lstm레이어 은닉벡터, 하나는 인코더의 은닉벡터
out = np.concatenate((c, dec_hs), axis=2) #두 출력 연결
score = self.affine.forward(out)#연결하여 affine 계층
return score
def backward(self, dscore):
dout = self.affine.backward(dscore)#affine의 그래디언트 계산
N, T, H2 = dout.shape
H = H2 // 2
dc, ddec_hs0 = dout[:,:,:H], dout[:,:,H:] # 그래디언트 계산 이용
denc_hs, ddec_hs1 = self.attention.backward(dc) #어텐션 기울기
ddec_hs = ddec_hs0 + ddec_hs1
dout = self.lstm.backward(ddec_hs) #lstm 기울기
dh = self.lstm.dh
denc_hs[:, -1] += dh
self.embed.backward(dout) #임베딩 그래디언트
return denc_hs
def generate(self, enc_hs, start_id, sample_size): #새로운 단어열 생성
sampled = []
sample_id = start_id
h = enc_hs[:, -1]
self.lstm.set_state(h)
for _ in range(sample_size):
x = np.array([sample_id]).reshape((1, 1))
out = self.embed.forward(x)
dec_hs = self.lstm.forward(out)
c = self.attention.forward(enc_hs, dec_hs)
out = np.concatenate((c, dec_hs), axis=2)
score = self.affine.forward(out)
sample_id = np.argmax(score.flatten())
sampled.append(sample_id)
return sampled
seq2seq 구현
attentionseq2seq 클래스 구현도 앞 장에서본 seq2seq와 같다. 다른점이 있다면 encoder 대신 attentionEncoder, decoder 대신 attentionDecoder 클래스를 사용한다. AttentionSeq2seq 클래스를 구현하면 다음과 같다.
class AttentionSeq2seq(Seq2seq):
def __init__(self, vocab_size, wordvec_size, hidden_size):
args = vocab_size, wordvec_size, hidden_size
self.encoder = AttentionEncoder(*args)
self.decoder = AttentionDecoder(*args)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
어텐션 평가
어텐션의 효과를 날짜 형식을 통해 평가한다.
날짜 형식 변환 문제
september 27 , 1994 -> 1994-09-27
JUN 17, 2013 -> 2013-06-17
2/10/93 -> 1993-02-10
날짜 형식 변환 문제를 통해 평가하는 이유는 다양한 변형이 존재하여 변환 규칙이 나름 복잡하고, 문제의 입력과 출력사이에 알기 쉬운 대응 관계가 있기 때문이다
september 27, 1994 _1994-09-27
August 19, 2003 _2003-08-19
2/10/93 _1993-02-10
10/31/90 _1990-10-31
TUESDAY, SEPTEMBER 25, 1984 _1984-09-25
JUN 17, 2013 _2013-06-17
april 3, 1996 _1996-04-03
October 24, 1974 _1974-10-24
AUGUST 11, 1986 _1986-08-11
February 16, 2015 _2015-02-16
October 12, 1988 _1988-10-12
6/3/73 _1973-06-03
Sep 30, 1981 _1981-09-30
June 19, 1977 _1977-06-19
OCTOBER 22, 2005 _2005-10-22
데이터의 형식을 보면 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩하였고, 입력과 출력의 구분 문자로는 _ (밑줄)을 사용했다. 출력의 문자수는 0000-00-00으로 모두 일정하다.
어텐션을 갖춘 seq2seq의 학습
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from dataset import sequence
from optimizer import Adam
from trainer import Trainer
from util import eval_seq2seq
from attention_seq2seq import AttentionSeq2seq
from seq2seq import Seq2seq
from peeky_seq2seq import PeekySeq2seq
# 데이터 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('date.txt')
char_to_id, id_to_char = sequence.get_vocab()
# 입력 문장 반전
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1] #문장 반전
# 하이퍼 파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 256
batch_size = 128
max_epoch = 10
max_grad = 5.0
model = AttentionSeq2seq(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=True)
acc = float(correct_num) / len(x_test)
acc_list.append(acc)
print('val acc %.3f%%' % (acc * 100))
model.save_params()
어텐션을 갖춘 seq2seq는 학습을 거둘수록 똑똑해진다.
에폭이 지날수록 빠르게 정답률 100%에 다다랐다.
이전의 peeky 모델과 비교해봐도 attention 모델이 더 우세함을 알 수 있다.
어텐션 시각화
attention 계층은 각 시각의 어텐션 가중치를 인스턴스 변수로 보관하고 있다.
Time attention 계층에 있는 인스턴스 변수 attention_weights에 각 시각의 어텐션 가중치가 저장된다. 입력 문장과 출력 문장의 단어 대응 관계를 2차원 을 그릴 수 있다.
import sys
sys.path.append('..')
import numpy as np
from dataset import sequence
import matplotlib.pyplot as plt
from attention_seq2seq import AttentionSeq2seq
(x_train, t_train), (x_test, t_test) = \
sequence.load_data('date.txt')
char_to_id, id_to_char = sequence.get_vocab()
# Reverse input
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 256
model = AttentionSeq2seq(vocab_size, wordvec_size, hidden_size)
model.load_params()
_idx = 0
def visualize(attention_map, row_labels, column_labels):
fig, ax = plt.subplots()
ax.pcolor(attention_map, cmap=plt.cm.Greys_r, vmin=0.0, vmax=1.0)
ax.patch.set_facecolor('black')
ax.set_yticks(np.arange(attention_map.shape[0])+0.5, minor=False)
ax.set_xticks(np.arange(attention_map.shape[1])+0.5, minor=False)
ax.invert_yaxis()
ax.set_xticklabels(row_labels, minor=False)
ax.set_yticklabels(column_labels, minor=False)
global _idx
_idx += 1
plt.show()
np.random.seed(1984)
for _ in range(5):
idx = [np.random.randint(0, len(x_test))]
x = x_test[idx]
t = t_test[idx]
model.forward(x, t)
d = model.decoder.attention.attention_weights
d = np.array(d)
attention_map = d.reshape(d.shape[0], d.shape[2])
# reverse for print
attention_map = attention_map[:,::-1]
x = x[:,::-1]
row_labels = [id_to_char[i] for i in x[0]]
column_labels = [id_to_char[i] for i in t[0]]
column_labels = column_labels[1:]
visualize(attention_map, row_labels, column_labels)
가로축은 입력, 세로축은 출력, 맵의 각 원소는 밝을수록 값이 크다(1에 가깝다)
AUGUST와 08이 대응하고 있고, 1983과 26이 서로 잘 대응하고 있다는 것을 알 수 있다.
'Deep Learning > from scratch II' 카테고리의 다른 글
[밑바닥부터 시작하는 딥러닝 2] - 8장 어텐션 III (0) | 2024.02.23 |
---|---|
[밑바닥부터 시작하는 딥러닝 2] - 8장 어텐션 I (0) | 2024.02.22 |
[밑바닥부터 시작하는 딥러닝 2 ] - 7장 RNN을 사용한 문장 생성 II (2) | 2024.02.21 |
[밑바닥부터 시작하는 딥러닝 2] - 7장 RNN을 사용한 문장 생성 I (1) | 2024.02.21 |
[밑바닥부터 시작하는 딥러닝 2] - 6장 게이트가 추가된 RNN II (1) | 2024.02.20 |