NIRVANA
[밑바닥부터 시작하는 딥러닝2] 8장. 어텐션 본문
어텐션의 구조
- 어텐션 메커니즘 덕분에 seq2seq는 (인간처럼) 필요한 정보에만 주목할 수 있게 됨
- seq2seq가 갖고 있던 문제 해결도 가능
seq2seq의 문제점
- Encoder의 출력은 '고정 길이의 벡터'가 됨
- 고정 길이 벡터는 입력 문장의 길이에 상관없이 항상 같은 길이의 벡터로 변환한다는 뜻
- 긴 문장도 같은 길이의 벡터에 입력되므로 필요한 정보가 벡터에 다 담기지 못하게 될 수 있음
Encoder 개선
- Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는 것이 좋음
- 각 시간의 은닉 상태에는 직전에 입력된 단어에 대한 정보가 많이 포함되어 있음
- "고양이"라는 단어를 입력했을 때, LSTM 계층의 출력은 직전에 입력한 고양이라는 단어의 영향을 가장 크게 받음
- 따라서 Encoder가 출력하는 hs 행렬은 각 단어에 해당하는 벡터들의 집합이라고 볼 수 있음
Decoder 개선 ①
- Encoder는 각 단어에 대응하는 LSTM 계층의 은닉 상태 벡터를 hs로 모아 출력하고, 이 hs가 Decoder에 전달되어 시계열 변환이 이루어짐
- 이전에는, Encoder의 LSTM 계층의 마지막 은닉 상태를 Decoder의 LSTM 계층의 첫 은닉 상태로 설정했음
- hs를 전부 활용할 수 있도록 Decoder 개선 진행
우리의 목표: 필요한 정보에만 주목하여 정보로부터 시계열 변환을 수행하는 것이 목표
- 목표를 위해 어떤 계산을 수행하는 계층을 추가함 → 두 가지의 입력을 받음
- Encdoer로부터 받는 hs(Encoder의 마지막 은닉 상태)
- 시각별 LSTM 계층의 은닉 상태
- 새로 생성된 신경망으로 하고 싶은 일 = 단어들의 얼라인먼트 추출
- Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내는 것
- 선택 작업은 미분을 하지 못한다는 문제점 발생
- '하나를 선택'하는 것이 아니라, '모든 것을 선택'함 → 이때, 각 단어의 중요도(기여도)를 나타내는 가중치를 별도로 계산을 하게 함
- 각 단어의 중요도를 나타내는 가중치 a와 각 단어의 벡터 hs로부터 가중합을 구하여 우리가 원하는 벡터를 얻음 => 맥락 벡터
- Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내는 것
class WeightSum:
def __init__(self):
self.params, self.grads = [], []
self.cache = None
def forward(self, hs, a):
N, T, H = hs.shape
ar = a.reshape(N, T, 1).repeat(H, axis=2)
t = hs * ar
c = np.sum(t, axis=1)
self.cache = (hs, ar)
return c
def backward(self, dc):
hs, ar = self.cache
N, T, H = hs.shape
dt = dc.reshape(N, 1, H).repeat(T, axis=1) #sum의 역전파
dar = dt * hs
dhs = dt * ar
da = np.sum(dar, axis=2) #repeat의 역전파
return dhs, da
Decoder 개선 ②
- 각 단어의 중요도를 나타내는 가중치 a가 있다면 가중합을 사용하여 '맥락 벡터'를 얻을 수 있음
- h(Decoder의 LSTM 계층의 은닉 상태 벡터)hs의 각 단어 벡터와 얼마나 '비슷한가'를 수치로 나타내는 것이 중요함
- 벡터의 내적을 사용하여서 구함
- 내적: 두 벡터가 얼마나 같은 방향을 향하고 있는가 - 벡터의 유사도를 의미
- h와 hs의 내적을 구하여 두 벡터가 비슷한지를 수치로 표현
- h와 hs의 내적 결과 s는 점수라고도 하며, s를 정규화하기 위해서는 일반적으로 소프트맥스 함수를 사용함
import sys
sys.path.append('..')
from common.layers.import Softmax
import numpy as np
N, T, H = 10, 5, 4
hs = np.random.randn(N, T, H)
h = np.random.randn(N, H)
hr = h.reshape(N, 1, H).repeat(T, axis=1)
t = hs * hr
print(t.shape)
s = np.sum(t, axis=2)
print(s.shape)
softmax = Softmax()
a = softmax.forward(s)
print(a.shape)
- reshpae과 reapeat() 메서드를 사용하여 적합한 형상의 hr를 생성
- 넘파이 브로드캐스츠트를 사용한다면 repeat() 메서드는 필요 없음!
class AttentionWeight:
def __init__(self):
self.params, self.grads = [], []
self.softmax = Softmax()
self.cache = None
def forward(self, hs, h):
N, T, H = hs.shape
hr = h.reshape(N, 1, H)#.repeat(T, axis=1)
t = hs * hr
s = np.sum(t, axis=2)
a = self.softmax.forward(s)
self.cache = (hs, hr)
return a
def backward(self, da):
hs, hr = self.cache
N, T, H = hs.shape
ds = self.softmax.backward(da)
dt = ds.reshape(N, T, 1).repeat(H, axis=2)
dhs = dt * hr
dhr = dt * hs
dh = np.sum(dhr, axis=1)
return dhs, dh
Decoder 개선 ③
- 맥락 벡터를 구할 때, 우리는 weight sum 계층과 Attention Weight 계층 2개로 나누어서 구현함
- Attention Weight 계층: Encoder가 출력하는 각 단어의 벡터 hs에 주목하여 단어의 가중치 a를 구함
- Weight sum 계층: a와 hs의 가중합을 구하고, 결과로 맥락 벡터 c로 출력
- 어텐션은 Encoder가 건네주는 정보 hs에서 중요한 원소에 주목하여, 그를 바탕으로 맥락 벡터를 구해 위쪽 계층으로 전파함
class Attention:
def __init__(self):
self.params, self.grads = [], []
self.attention_weight_layer = AttentionWeight()
self.weight_sum_layer = WeightSum()
self.attention_weight = None
def forward(self, hs, h):
a = self.attention_weight_layer.forward(hs, h)
out = self.weight_sum_layer.forward(hs, a)
self.attention_weight = a
return out
def backward(self, dout):
dhs0, da = self.weight_sum_layer.backward(dout)
dhs1, dh = self.attention_weight_layer.backward(da)
dhs = dhs0 + dhs1
return dhs, dh
- 2개의 계층(Weight sum 계층과 Attention Weight 계층)에 의한 순전파와 역전파를 수행
- 이때, 각 단어의 가중치를 나중에 참조할 수 있도록 attention_weight라는 인스턴스 변수에 저장
- Attention 계층에는 Encoder의 출력인 hs가 입력됨
- LSTM 계층의 은닉 상태 벡터를 Affine 계층에 입력하여 Decoder에 어텐션 정보를 추가할 수 있음
- Affine 계층에는 기존과 마찬가지로 LSTM 계층의 은닉 상태 벡터를 주고, 이에 더해 Attention 계층의 멕락 벡터도 입력
Time Attention 계층의 구현
class TimeAttention:
def __init__(self):
self.params, self.grads = [], []
self.layers = None
self.attention_weights = None
def forward(self, hs_enc, hs_dec):
N, T, H = hs_dec.shape
out = np.empty_like(hs_dec)
self.layers = []
self.attention_weights = []
for t in range(T):
layer = Attention()
out[:, t, :] = layer.forward(hs_enc, hs_dec[:,t,:])
self.layers.append(layer)
self.attention_weights.append(layer.attention_weight)
return out
def backward(self, dout):
N, T, H = dout.shape
dhs_enc = 0
dhs_dec = np.empty_like(dout)
for t in range(T):
layer = self.layers[t]
dhs, dh = layer.backward(dout[:, t, :])
dhs_enc += dhs
dhs_dec[:,t,:] = dh
return dhs_enc, dhs_dec
- Attention 계층을 필요한 수 만큼 만들고, 각각이 순전파와 역전파를 수행하게 함
- 각 Attention 계층의 각 단어 가중치를 attention_weights 리스트에서 보관
어텐션을 갖춘 seq2seq 구현
Encoder 구현
- 앞 장의 Encoder 클래스의 forward() 메서드가 LSTM 계층의 마지막 은닉 상태 벡터만을 반환한 것에 반해, 이번에는 모든 은닉 상태를 반환함
# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from ch07.seq2seq import Encoder, Seq2seq
from ch08.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')
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.attention = TimeAttention()
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]
self.lstm.set_state(h)
out = self.embed.forward(xs)
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)
return score
def backward(self, dscore):
dout = self.affine.backward(dscore)
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)
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
- forward()메서드 구현 시, np.concatenate() 메서드를 사용하여 Time Attention 계층의 출력과 LSTM 계층의 출력을 연결함
seq2seq 구현
- seq2seq 클래스를 상속하고, 초기화 메서드를 수정하여 AttetnionSeq2seq 구현 가능
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
어텐션 평가
날짜 형식 변환 문제
- 영어권에서 사용되는다양한 날짜 형식을 표준 형식으로 변환하는 것이 목표
- 입력되는 날짜 데이터에는 다양한 변형이 존재, 변환 규칙이 나름 복잡
- 문제의 입력(질문)과 출력(답변) 사이에 알기 쉬운 대응 관계가 존재함
어텐션을 갖춘 Seq2seq의 학습
# coding: utf-8
import sys
sys.path.append('..')
sys.path.append('../ch07')
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 attention_seq2seq import AttentionSeq2seq
from ch07.seq2seq import Seq2seq
from ch07.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)
# 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=True)
acc = float(correct_num) / len(x_test)
acc_list.append(acc)
print('정확도 %.3f%%' % (acc * 100))
model.save_params()
# 그래프 그리기
x = np.arange(len(acc_list))
plt.plot(x, acc_list, marker='o')
plt.xlabel('에폭')
plt.ylabel('정확도')
plt.ylim(-0.05, 1.05)
plt.show()
어텐션에 관한 남은 이야기
양방향 RNN
- LSTM의 각 시각의 은닉 상태 벡터는 hs로 모아지게 되고, hs의 각행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있음
- 우리는 글을 왼쪽에서 오른 쪽으로 읽기 때문에, 단어의 주변 정보를 균형있게 담아야할 필요가 있음
- LSTM을 양방향으로 처리하는 방법을 고민해봐야 함!
- 양방향 LSTM에서는 지금까지의 LSTM 계층에 더해 역방향으로 처리하는 LSTM 계층도 추가하고, 각 시각에서 두 LSTM 계층의 은닉 상태를 연결시킨 벡터를 최종 은닉 상태로 처리함
- 양방향 처리를 통해, 각 단어에 대응하는 은닉 상태 벡터에는 좌와 우 양쪽 방향으로부터의 정보를 집약할 수 있게 됨
- 균형 잡힌 정보 인코딩 가능
구현 방법
- 2개의 LSTM 계층을 사용하여 각각의 계층에 주는 단어의 순서를 조정
- 첫번째 계층: 입력 문장을 '왼쪽에서 오른쪽'으로 처리하는 일반적인 LSTM 계층
- 두번째 계층: 입력 문장의 단어들을 반대 순서로 나열, 즉 입력문을 '오른쪽에서 왼쪽으로' 처리하는 계층
Attention 계층 사용 방법
- 기존에는 attention 계층을 LSTM 계층과 Affine 계층 사이에 삽입하여 사용
- 그러나 꼭 Affine과 LSTM 계층 사이에 attetnion 계층을 사용할 필요는 없음
seq2seq2 심층화와 skip연결
- 층을 깊게 할 때 사용되는 중요한 기법 중 한 가지, 계층을 건너 뛰는 연결을 의미함
- skip 연결의 접속부에서는 2개의 출력이 더해짐 → 덧셈은 역전파 시, 기울기를 그대로 흘려보내므로 skip 연결의 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐르게함
- 즉, 층이 깊어져도 기울기가 소실(혹은 폭발) 되지 않고 전파되게 하므로 좋은 학습을 기대할 수 있게함
- skip 연결의 접속부에서는 2개의 출력이 더해짐 → 덧셈은 역전파 시, 기울기를 그대로 흘려보내므로 skip 연결의 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐르게함
- Encdoer와 Decoder에서는 같은 층수의 LSTM 계층을 이용하는 것이 일반적
어텐션 응용
구글 신경망 기계 번역(GNMT)
- Encoder와 Decoder, Attention으로 구성
- 번역 정확도를 높이기 위해 LSTM 계층의 다양화, 양방향 LSTM(Encoder의 첫 계층만), skip연결 등의 여러 개선을 더함
- 학습 시간 단축을 위한 다수의 GPU를 사용한 분산학습 수행
트랜스포머
- RNN은 이전 시각에 계산한 결과를 이용, 순서대로 계산을 진행하므로 RNN의 계산을 시간 방향으로 병렬 계산하기는 (기본적으로) 불가능
- 트랜스포머: 셀프 어텐션이라는 기술을 이용한, RNN이 아닌 어텐션을 사용하여 처리하는 방법
- 기존은 서로 다른 두 시계열 데이터를 입력함
- 반면 트랜스포머는 두 입력선이 모두 하나의 시계열 데이터로부터 나옴 → 시계열 데이터 내에서의 원소 간 대응 관계를 구할 수 있게됨
- Encoder와 Decoder 모두 어텐션을 사용하여 구현됨
- 계산량을 줄이고, GPU를 이용한 병렬 계산의 혜택도 더 많이 누릴 수 있음
뉴럴 튜링 머신
- 어텐션을 통해 Encoder와 Decoder는 메모리 조작과 같은 작업을 수행하게 됨
- Encoder가 필요한 정보를 메모리에 쓰고, Decoder는 그 메모리부터 필요한 정보를 읽어 들이는 것으로 해석 가능
- 뉴럴 튜링 머신: RNN의 외부에 정보 저장용 메모리 기능을 배치하고, 어텐션을 이용하여 그 메모리로부터 필요한 정보를 읽거나 쓰는 방법
- NTM은 외부 메모리를 읽고 쓰면서 시계열 데이터를 처리
- 메모리 조작을 미분 가능한 계산으로 구축했다는 특징을 가짐
- LSTM 계층이 '컨트롤러'가 되어 NTM의 주된 처리를 수행하게 됨
- LSTM 계층의 은닉 상태를 Write Head계층이 받아서 필요한 정보를 메모리에작성
- Read Head 계층이 메모리로부터 중요한 정보를 읽어 들여, 다음 시각의 LSTM 계층으로 전달
- 어텐션을 사용하여 메모리 조작
- NTM은 컴퓨터의 메모리 조작을 모방하기 위해 2개의 어텐션을 사용
- 콘텐츠 기반 어텐션: 입력으로 주어진 어느 벡터와 비슷한 벡터를 메모리로부터 찾아내는 용도로 사용됨
- 위치 기반 어텐션: 이전 시각에서 주목한 메모리의 위치를 기준으로, 그 전후로 이동하는 용도로 사용됨
- 일차원의 합성곱 연산으로 구현
'AI' 카테고리의 다른 글
[밑바닥부터 시작하는 딥러닝2] 7장. RNN을 사용한 문장 생성 (0) | 2024.03.21 |
---|---|
[밑바닥부터 시작하는 딥러닝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 |