해당 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 공부하고 정리, 요약한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️8.1 어텐션의 구조
seq2seq를 한층 더 강력하게 하는 어텐션 메커니즘 아이디어를 소개한다.
이 어텐션이라는 메커니즘 덕분에 seq2seq는 필요한 정보에만 주목할 수 있게 된다.
◾ 8.1.1 seq2seq의 문제점
seq2seq에서는 Encoder가 시계열 데이터를 인코딩한다. 인코딩된 정보를 Decoder로 전달하고 Encoder의 출력은 고정 길이의 벡터였다.
고정 길이 벡터라 함은 입력 문장의 길이에 관계없이 항상 같은 길이의 벡터로 변환한다는 뜻이다.
현재의 Encoder는 아무리 긴 문장이라도 고정 길이의 벡터로 변환하지만 이는 필요한 정보가 벡터에 다 담기지 못하게 된다.
◾ 8.1.2 Encoder 개선
Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는 게 좋다. 이 점이 Encoder의 개선 포인트이다.
구체적으로는 아래 그림처럼 시각별 LSTM 계츠으이 은닉 상태 벡터를 모두 이용하는 것이다.
위 그림에서의 예에서는 5개의 단어가 입력되고 이때 Encoder는 5개의 벡터를 출력한다. Encoder는 하나의 고정 길이 벡터라는 제약으로부터 해방이 된다.
각 시각의 운늑 상태에는 직전에 입력된 단어에 대한 정보가 많이 포함되어 있다.
Encoder가 출력하는 hs 행렬은 각 단어에 해당한느 벡터들의 집합이라고 볼 수 있다.
단지 Encoder의 은닉 상태를 모든 시각만큼 꺼냈을 뿐이지만 이 작은 개선 덕분에 encoder는 입력 문장의 길이에 비례한 정보를 인코딩할 수 있게 되었다.
◾ 8.1.3 Decoder 개선 1
Encoder는 각 단어에 대응하는 LSTM 계층의 은닉 상태 벡터를 hs로 모아 출력한다.
encoder의 LSTM 계층의 마지막 은닉 상태를 decoder의 LSTM 계층의 첫 은닉 상태로 설정한 것이다. 이 decoder의 계층 구성은 아래 그림처럼 그릴 수 있다.
위 그림에서 보듯 앞 장의 decoder는 encoder의 LSTM 계층의 마지막 은닉 상태만을 이용한다. hs에서 마지막 줄만 빼내어 decoder에 전달한 것이다.
그럼 이 hs 전부를 활용할 수 있도록 decoder를 개선 할 것이다.
입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는가라는 대응 관계를 seq2seq에게 학습시킬 수 있을까?
필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 것이 목표이다. 이 구조를 어텐션이라고 부른다.
위 그림처럼 여기에서는 새롭게 어떤 계산을 수행하는 계층을 추가할 것이다.
이 어떤 계산이 받는 입력은 두 가지로 하나는 encoder로부터 받는 hs이고 다른 하나는 시각별 LSTM 계층의 은닉 상태이다.
여기에서 필요한 정보만 골라 위쪽의 Affine 계층으로 출력한다. encoder의 마지막 은닉 상태 벡터는 decoder의 첫번째 LSTM 계층에 전달한다.
위 그림 8-6의 신경망으로 하고 싶은 일은 단어들의 얼라인먼트 추출이다. 각 시각에서 decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내겠다는 뜻이다.
하지만 선택하는 작업은 미분할 수 없다는 문제가 있다.
하나를 선택하는 것이 아니라 모든 것을 선택하게 한다면 선택한다는 작업을 미분 가능한 연산으로 대체할 수 있다.
위 그림처럼 각 단어의 중요도를 나타내는 가중치를 별도로 계산하도록 한다.
각 단어의 중요도를 나타내는 가중치(기호 a)를 이용한다. a는 확률분포처럼 각 원소가 0.0 ~ 1.0 사이의 스칼라이며 모든 원소의 총합은 1이 된다. 그리고 각 단어의 중요도를 나타내는 가중치 a와 각 단어의 벡터 hs로부터 가중합을 구하여 우리가 원하는 벡터를 얻는다.
위 그림처럼 단어 벡터의 가중합을 계산한다. 그 결과를 맥락 벡터라고 부르고 기호로는 c로 표기한다. 이 그림을 잘 보면 "나"에 대응하는 가중치가 0.8이다. 이것이 의미하는 바는 맥락 벡터 c에는 "나" 벡터의 성분이 많이 포함되어 있다는 것이다.
즉, "나" 벡터를 선택하는 작업을 이 가중합으로 대체하고 있다고 할 수 있다. "나"에 대응하는 가중치가 1이고 그 외에는 0이라면 "나" 벡터를 선택한다고 해석할 수 있다.
import numpy as np
T, H = 5, 4
hs = np.random.randn(T, H)
a = np.array([0.8, 0.1, 0.03, 0.05, 0.02])
ar = a.reshape(5, 1).repeat(4, axis=1)
# ar = a.reshape(5, 1)
◾ 8.1.4 Decoder 개선 2
각 단어의 중요도를 나타내는 가중치 a가 있다면 가중합을 이용해 맥락 벡터를 얻을 수 있다.
위 그림에서는 decoder의 LSTM 계층의 은닉 상태 벡터를 h라고 했다.
목표는 이 h가 hs의 각 단어 벡터와 얼마나 비슷한가를 수치로 나타내는 것이다. 여기에서 가장 단순한 방법인 벡터의 내적을 이용하고자 한다.
내적은 위 식으로 표현되며 두 벡터가 얼마나 같은 방향을 향하고 있는가를 의미한다.
내적을 이용해 벡터 사이의 유사도를 산출할 때까지의 처리를 그림으로 나타내면 아래 그림과 같아진다.
위 그림에서 보듯 여기에서는 벡터의 내적을 이용해 h와 hs의 각 단어 벡터와의 유사도를 구한다. 그리고 s는 그 결과이다.
참고로 s는 정규화하기 전의 값이며 점수라고도 한다.
s를 정규화하기 위해 일반적으로 소프트맥스 함수를 적용한다.
소프프맥스 함수를 사용하면 그 출력인 a의 각 원소는 0.0 ~ 1.0 사이의 값이 되고 모든 원소의 총합은 1이 된다.
import sys
from common.layers import Softmax
import numpy as np
N, T, H = 10, 5, 4
hs = np.random.randn(N, T, H) # encoder output
h = np.random.randn(N, H) # decoder h_t
hr = h.reshape(N, 1, H).repeat(T, axis=1)
t = hs * hr # point-wise dot
print(f't.shape : {t.shape}')
s = np.sum(t, axis=-1)
print(f's.shape : {s.shape}')
softmax = Softmax()
a = softmax.forward(s)
print(f'a.shape : {a.shape}')
위 그림과 같이 이 계산 그래프는 repeat 노드, 원소별 곱을 뜻하는 x노드, sum 노드 그리고 softmax 계층으로 구성된다.
# chap08/attention_layer.py
import sys
sys.path.append('..')
from common.np import * # import numpy as np
from common.layers import Softmax
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
◾ 8.1.5 Decoder 개선 3
attention weight 계층과 weight sum 계층을 하나로 결합하면 아래와 같아진다.
이 계산에 따르면 attention weight 계층은 encoder가 출력하는 각 단어의 벡터 hs에 주목하여 해당 단어의 가중치 a를 구한다.
이어서 weight sum 계층이 a와 hs의 가중합을 구하고 그 결과를 맥락 벡터 c로 출력한다.
이 일련의 계산을 수행하는 계층을 attention 계층이라고 한다.
encoder가 건네주는 정보 hs에서 중요한 원소에 주목하여 그것을 바탕으로 맥락 벡터를 구해 위쪽 계층으로 전파한다.
# chap08/attention_layer.py
import sys
sys.path.append('..')
from common.np import * # import numpy as np
from common.layers import Softmax
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
위 그림에서 보듯 각 시각의 attention 계층에는 encoder의 출력인 hs가 입력된다.
LSTM 계층의 은닉 상태 벡터를 affine 계층에 입력하여 앞 장의 decoder에 어텐션 정보를 추가할 수 있게 된다.
위 그림의 오른쪽은 앞 장의 decoder에 attention 계층이 구한 맥락 벡터 정보를 추가한 것으로 생각할 수 있다.
affine 계층에는 기존과 마찬가지로 LSTM 계층의 은닉 상태 벡터를 주고 여기에 더해 attention 계층의 맥락 벡터까지 입력하는 것이다.
시계열 방향으로 펼쳐진 다수의 attention 계층을 Time Attention 계층으로 모아 구현하면 아래와 같아 진다.
Time Attention 계층은 다수의 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 리스트에서 보관한다.
◼️ 8.2 어텐션을 갖춘 seq2seq 구현
◾ 8.2.1 Encoder 구현
# chap08/attention_seq2seq.py
import sys
sys.path.append('..')
from common.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
◾8.2.2 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() # 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]
self.lstm.set_state(h)
out = self.embed.forward(xs)
dec_hs = self.lstm.forward(out)
c = self.attention.forward(enc_hs, dec_hs) # context vector
out = np.concatenate((c, dec_hs), axis=2) # context_vector & lstm h_t
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
◾ 8.2.3 seq2seq 구현
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
◼️8.3 어텐션 평가
◾ 8.3.1 날짜 형식 변환 문제
위 그림처럼 사람이 쓴 날짜 데이터를 표준 형식으로 변환하는 문제는 생각보다 간단하지 않다.
입력되는 날짜 데이터에는 다양한 변형이 존재하여 변환 규칙이 나름 복잡해지기 때문이다. 또, 문제의 입력과 출력 사이에 알기 쉬운 대응 관계까 있기 때문이기도 하다.
구체적으로 말하면 년, 월, 일의 대응 관계가 존재한다. 어텐션이 각각의 원소에 올바르게 주목하고 있는지를 확인할 수 있다.
이 데이터셋은 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩해 뒀고 입력과 출력의 구분 문자로는 밑줄을 사용했다.
◾8.3.2 어텐션을 갖춘 seq2seq의 학습
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.font_manager as fm
font_path = 'C:/Windows/Fonts/malgun.ttf'
font_name = fm.FontProperties(fname=font_path, size=10).get_name()
plt.rc('font', family=font_name, size=12)
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 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)
# 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, eval_interval=150)
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()
어텐션을 갖춘 seq2seq는 학습을 거듭할수록 점점 똑똑해진다.
테스트 데이터로 얻은 정답률을 그래프로 그리면 아래처럼 된다.
1 에폭부터 빠르게 정답률을 높여 2 에폭 째에는 이미 거의 모든 문제를 풀어낸다.
◾8.3.3 어텐션 시각화
어텐션이 시계열 변환을 수행할 때 어느 원소에 주의를 기울이는지를 눈으로 살펴보려는 시도이다.
attention 계층은 각 시각의 어텐션 가중치를 인스턴스 변수로 보관하고 있으므로 이를 시각화하기란 아주 간단하다.
import sys
import numpy as np
from dataset import sequence
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.font_manager as fm
font_path = 'C:/Windows/Fonts/malgun.ttf'
font_name = fm.FontProperties(fname=font_path, size=10).get_name()
plt.rc('font', family=font_name, size=12)
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()
# 입력 문장 반전
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])
# 출력 위해 반전
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)
위 그림은 seq2seq가 시계열 변환을 할 때의 어텐션 가중치를 시각화 한 결과이다.
seq2seq가 최초의 1을 출력할 때는 입력 문장의 1 위치에 표시된다.
세로축의 1983과 26이 가로축이 1983과 26에 훌륭하게 대응을 하고 있다.
◼️8.4 어텐션에 관한 남은 이야기
◾ 8.4.1 양방향 RNN
LSTM의 각 시각의 은닉 상태 벡터는 hs로 모아진다. Encoder가 출력하는 hs의 각 행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있다.
주목할 것은 우리는 글을 왼쪽에서 오른쪽으로 읽기 때문에 고양이에 대응하는 벡터에 "나", "는", "고양이"까지 총 세 단어의 정보가 인코딩되어 들어간다.
따라서 "고양이" 단어의 주변 정보를 균형있게 담고 싶을 것이다.
그래서 LSTM을 양방향으로 처리하는 방법을 생각할 수 있다. 이것이 양방향 LSTM(양방향 RNN)이며 그림으로는 다음과 같이 나타낼 수 있다.
양방향 LSTM에서는 지금까지의 LSTM 계층에 더해 역방향으로 처리하는 LSTM 계층도 추가한다. 그리고 각 시각에서는 이 두 LSTM 계층의 은닉 상태를 연결 시킨 벡터를 최종 은닉 상태로 처리한다.
이처럼 양방향으로 처리함으로써 각 단어에 대응하는 은닉 상태 벡터에는 좌와 우 양쪽 방향으로부터 정보를 집약할 수 있다. 이렇게 균형잡힌 정보가 인코딩되는 것이다.
양방향 LSTM은 구현하기도 쉽다. 2개의 LSTM 계층을 사용하여 각각의 계층에 주는 단어의 순서를 조정하면 된다.
즉, 입력 문장을 왼쪽부터 오른쪽으로 처리하는 일반적인 LSTM 계층이다. 또 하나의 LSTM 계층에는 입력 문장의 단어들을 반대 순서로 나열한다.
두번째 LSTM 계층은 입력문을 오른쪽에서 왼쪽으로 처리하게 된다. 마지막으로 이 두 LSTm 계층의 출력을 연결하기만 하면 양방향 LSTM 계층이 완성된다.
◾8.4.2 Attention 계층 사용 방법
attention 계층을 LSTM 계층과 affine 계층 사이에 삽입할 필요가 없다.
위 그림 8-32처럼 구성하기도 한다. attention 계층의 출력이 다음 시각의 LSTM 계층에 입력되도록 연결 됐다.
이렇게 구성하면 LSTM 계층이 맥락 벡터의 정보를 이용할 수 있다.
◾ 8.4.3 seq2seq 심층화와 skip 연결
어텐션을 갖춘 seq2seq에도 더 높은 표현력이 요구될 것이다. 우선으로 생각해야 할 것은 RNN 계층을 깊게 쌓는 방법이다.
층을 깊게 쌓으면 표현력 높은 모델을 만들 수 있고 어텐션을 갖춘 seq2seq도 다르지 않다.
위 그림에서의 모델은 encdoer와 decoder로 3층 LSTM 계층을 사용하고 있다. 이 예처럼 encoder와 decoder에서는 같은 층수의 LSTM 계층을 이용하는 것이 일반적이다.
decoder의 LSTM 계층의 은닉 상태를 attention 계층에 입력하고 attention 계층의 출력인 맥락 벡터를 decoder의 여러 계층으로 전파한다.
층을 깊게 할 때 사용되는 중요한 기법 중 skip 연결이라는게 있다. skip 연결은 아래 그림처럼 계층을 넘어 '선을 연결'하는 단순한 기법이다.
이때 skip 연결의 접속부에서는 2개의 출력이 더해지는데 여기서 이 덧셈이 핵심이다.
덧셈은 역전파 시 기울기를 그대로 흘려보내므로 skip 연결의 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐르기 때문이다.
층이 깊어져도 기울기가 소실 되지 않고 전파되어 결과적으로 좋은 학습을 기대할 수 있다.
◼️ 8.5 어텐션 응용
◾ 8.5.1 구글 신경망 기계 번역(GNMT)
GNMT는 encoder와 deocder, attention으로 구성되어 있다. 번역 정확도를 높이기 위해 LSTM 계층의 다층화, 양방향 LSTM, skip 연결 등을 수행하고 있다.
◾ 8.5.2 트랜스포머
RNN은 이전 시각에 계산한 결과를 이용하여 순서대로 계산한다. RNN의 계산을 시간 방향으로 병렬 계산하기란 불가능하다.
렬 계산을 할 수 있는 연구가 활발히 이루어 지고 있고 그 중 유명한 것이 Attention is all you need라는 논문에서 제안한 기법인 트랜스포머 모델이다.
트랜스포머는 RNN이 아닌 attention으로 구성되는데 그 중 셀프어텐션이라는 기술을 잉요하는 게 핵심이다.
self-attention은 직역하면 자신에 대한 주목이 된다. 즉, 하나의 시계열 데이터를 대상으로 한 어텐션으로 하나의 시계열 데이터 내에서 각 원소가 다른 원소들과 어떻게 관련되는지를 살펴보는 취지이다.
Time Attention 계층에는 그림 8-37의 왼쪽처럼 서로 다른 두 시계열 데이터가 입력된다. 반면 self-attention은 그림 8-37의 오른쪽처럼 두 입력선이 모두 하나의 시계열 데이터로부터 나온다.
이렇게하면 하나의 시계열 데이터 내에서의 원소 간 대응 관계가 구해진다.
트랜스포머의 구성은 아래와 같다.
encoder와 decoder 모두에서 셀프어텐션을 사용함을 알 수 있다. feed forward 계층은 피드포워드 신경망을 나타내며 은닉층 1개, 활성화 함수로는 ReLU를 이용한 완전연결계층 신경망을 이용한다.
트랜스포머를 이용하면 계산량을 줄이고 GPU를 이용한 병렬 계산의 혜택도 더 많이 누릴 수 있다.
◾ 8.5.3 뉴럴 튜링 머신(NTM)
encoder가 필요한 정보를 메모리에 쓰고 decoder는 그 메모리로부터 필요한 정보를 읽어 들인다고 해석할 수 있다.
RNN의 외부 정보 저장용 메모리 기능을 배치하고 어텐션을 이용하여 그 메모리로부터 필요한 정보를 읽거나 쓰는 방법이다.
그림 한가운데 있는 컨트롤러라는 모듈은 정보를 처리하는 모듈로 신경망을 이용하는 것으로 예상된다.
이 컨트롤러는 차례차례 흘러 들어오는 0 혹은 1 데이터를 처리하여 새로운 데이터를 출력하고 있음을 알 수 있다.
LSTM 계층이 컨트롤러가 되어 NTM이 주된 처리를 수행한다. 각 시각에서 LSTM 계층의 은닉 상태를 Write Head 계층이 받아서 필요한 정보를 메모리에 쓴다.
그런 다음 Read Head 계층이 메모리로부터 중요한 정보를 읽어 들여 다음 시각의 LSTM 계층으로 전달한다.
NTM은 컴퓨터의 메모리 조작을 모방하기 위해 2개의 어텐션을 이용한다.
콘텐츠 기반 어텐션은 지금까지 본 어텐션과 같고 입력으로 주어진 어트 벡터와 비슷한 벡터를 메모리로부터 찾아내는 용도로 이용된다.
위치 기반 어텐션은 이전 시각에서 주목한 메모리의 위치를 기준으로 그 전후로 이동하는 용도로 사용된다.
◼️ 8.6 정리
- 한 시계열 데이터를 다른 시계열 데이터로 변환하는 작업에서는 시계열 데이터 사이의 대응 관계까 존재하는 경우가 많다.
- 어텐션은 두 시계열 데이터 사이의 대응 관계를 데이터로부터 학습한다.
- 어텐션에서는 벡터의 내적을 사용해 벡터 사이의 유사도를 구하고 그 유사도를 이용한 가중합 벡터가 어텐션의 출력이 된다.
- 어텐션에서 사용하는 연산은 미분 가능하기 때문에 오차역전파법으로 학습할 수 있다.
- 어텐션이 산출하는 가중치를 시각화하면 입출력의 대응 관계를 볼 수 있다.
- 외부 메모리를 활용한 신경망 확장 연구 예에서는 메모리를 읽고 쓰는 데 어텐션을 사용했다.