[밑바닥부터 시작하는 딥러닝] 어텐션 메커니즘
본 내용은 밑바닥부터 시작하는 딥러닝 2도서를 참고하여 작성되었습니다.
밑바닥부터 시작하는 딥러닝 2 - 예스24
직접 구현하면서 배우는 본격 딥러닝 입문서 이번에는 순환 신경망과 자연어 처리다! 이 책은 『밑바닥부터 시작하는 딥러닝』에서 다루지 못했던 순환 신경망(RNN)을 자연어 처리와 시계열 데
www.yes24.com
드디어, 어텐션이다.
현재의 AI 시대를 만든 트랜스포머에 발을 담가볼 수 있게 되었다.
그 근간에 있는 어텐션에 대한 학습을 시작해보자.
어텐션의 구조
어텐션 매커니즘은 seq2seq를 필요한 정보에만 '주목'시킨다.
그럼 먼저 seq2seq의 현재 문제점을 살펴보고, 이를 개선한 어텐션 매커니즘을 이해해보자.
seq2seq의 문제점
seq2seq는 Encoder가 시계열 데이터를 인코딩한다.
그리고 인코딩된 정보를 Decoder로 전달한다.
이때 Encoder의 출력은 '고정 길이의 벡터'였다.
즉, 입력 문장의 길이에 관계없이(아무리 길어도), 항상 같은 길이의 벡터로 변환한다.
앞 장의 번역 예로 설명하면, 아무리 긴 문장이 입력되더라도 항상 똑같은 길이의 벡터에 밀어 넣어야 한다.
이러면 언젠가 한계가 찾아올 것이 분명하다.
눌러 담은 쓰레기통이 결국 가득 차듯, 필요한 정보가 벡터에 다 담기지 못하게 된다.
Encoder 개선
지금까지 우리는 LSTM 계층의 마지막 은닉 상태만을 Deccoder에 전달했다.
그러나 Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는 게 좋다.
구체적으로는, 아래 그림처럼 LSTM 시각별 계층의 은닉 상태 벡터를 모두 이용하는 것이다.
위 그림처럼 각 시각(각 단어)의 은닉 상태 벡터를 모두 이용하면 입력된 단어와 같은 수의 벡터를 얻을 수 있다.
이것으로 Encoder 는 '하나의 고정 길이 벡터'라는 제약으로부터 해방된다.
그런데 위 그림에서 주목할 것은 LSTM 계층의 '내용'이다.
시각별 LSTM계층의 은닉 상태에는 어떤 정보가 담겨 있을까?
아마, 각 행 별로 가장 최근에 받은 단어에 대한 정보가 가장 많이 들어있을 것이다.
이렇게 생각하면, Encoder가 출력하는 hs 행렬은 각 단어에 해당하는 벡터들의 집합이라고 볼 수 있다.
그럼 이걸 어떻게 Decoder로 요리해야 할까?
Encoder 는 왼쪽에서 오른쪽으로 처리하므로, 방금 전의 '고양이' 벡터에는 정확히 총 3개의 단어('나', '는', '고양이')의 정보가 담겨 있다.
그런데 전체의 균형을 생각하여 "고양이"단어의 '주변' 정보를 균형 있게 담하야 할 때도 있을 텐데, 그런 경우엔 시계열 데이터를 양방향으로 처리하는 양방향 RNN(혹은 양방향 LSTM)이 효과적이다.
Decoder 개선 1
Encoder는 각 단어에 대응하는 LSTM 계층의 은닉 상태 벡터를 hs로 모아 출력한다.
그리고 이 hs가 Decoder에 전달되어 시계열 변환이 이뤄진다.
참고로, 앞 장에서 본 가장 단순한 seq2seq에서는 Encoder의 마지막 은닉 상태 벡터만을 넘겼다.
더 정확하게 말하면, Encoder의 LSTM 계층의 '마지막' 은닉 상태를 Decoder의 LSTM계층의 '첫' 은닉 상태로 설정했다.
이를 그림으로 나타내면 아래와 같다.
마치, 우리가 앞서 이야기했던 hs에서 맨 마지막 줄만 빼내어 Decoder에 전달한 것이다.
그럼 이제부터 hs 전체를 다루기 위해 어떤 작업이 추가되었는지 설명을 시작하겠다.
먼저 전체 그림을 보자면 아래와 같다.
LSTM과 Affine 사이에있는 특정 계층이 각 시각별로 hs를 사용한다.
사람이 문장을 번역할 때는 머릿속에서 어떤 일이 일어날까?
예를 들어 "나는 고양이로소이다"를 영어로 번역하려 한다면 아마 대부분이 '나=I'나 '고양이=cat'이라는 지식을 이용할 것이다.
즉, 우리는 '어떤 단어(혹은 단어의 집합)'에 주목하여 그 단어의 변환을 수시로 하게 될 것이다.
그럼 이와 같은 과정을 seq2seq로 재현할 수는 없을까?
더 정확하게는, 입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는가라는 대응 관계를 seq2seq에 학습시킬 수는 없을까?
앞으로 우리의 목표는 '도착어 단어'와 대응 관계에 있는 '출발어 단어'의 정보를 골라내는 것, 그리고 그 정보를 이용하여 번역을 수행하는 것이다.
다시 말해, 필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 것이 목표이다.
이 구조를 어텐션이라 부른다.
위 그림처럼 여기에서는 새롭게 '어떤 계산'을 수행하는 계층을 추가할 것이다.
이 '어떤 계산'이 받는 입력은 두 가지로, 하나는 Encoder로부터 받는 hs이고, 다른 하나는 시각별 LSTM계층의 은닉 상태이다.
참고로 지금까지와 똑같이 Encoder의 마지막 은닉 상태 벡터는 Decoder의 첫 번째 LSTM 계층에 전달한다.
그런데 '어떤 계산'으로 하고 싶은 일은 단어들의 얼라인먼트(대응 관계) 추출이다.
각 시각에서 Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내겠다는 뜻이다.
그런데 여기서 문제가 발생한다.
바로 선택하는 작업(여러 대상으로부터 몇 개를 선택하는 작업)은 미분할 수 없다는 것이다.
'선택한다' 라는 작업을 미분 가능한 연산으로 대체할 수는 없을까?
사실 이 문제를 해결할 아이디어는 아주 단순하다.(다만, 콜럼버스의 달걀처럼 처음으로 생각해내기가 어려운 것이다.)
바로 '하나를 선택'하는 게 아니라, '모든 것을 선택'한다는 것이다.
그리고 이때 아래 그림과 같이 각 단어의 중요도를 나타내는 '가중치'를 별도로 계산하도록 한다.
위 그림에서 보듯, 여기에서는 각 단어의 중요도를 나타내는 '가중치'(기호 a)를 사용한다.
a는 확률분포처럼 각 원소가 0~1 사이의 스칼라이며, 모든 원소의 총합은 1이 된다.
그리고 각 단어의 중요도를 나타내는 가중치 a와 각 단어의 벡터 hs로부터 가중 합(weighted sum)을 구하여, 우리가 원하는 벡터를 얻는다.
이 일련의 계산을 그려보면 아래 그림처럼 된다.
그 결과를 '맥락 벡터'라고 부르고, 기호로는 c로 표기한다.
즉, 이 맥락 벡터는 현 시각의 변환(번역)을 수행하는 데 필요한 정보가 담겨 있다.
더 정확하게 말하면, 그렇게 되도록 데이터부터 학습하는 것이다.
그럼 먼저 Encoder가 출력하는 hs와 각 단어의 가중치 a를 적당하게 작성하고, 그 가중합을 구하는 구현을 살펴보자.
axis를 0으로 해서, 첫번째 축을 기준으로 더했다.
따라서 첫번째 축이 지워졌다.
가중 합 계산은 '행렬 곱'을 사용하는 편이 가장 간단하고 효율적이다.
np.matmul(a, hs)라는 한 줄 만으로 원하는 결과를 얻을 수 있다.
다만, 이 방법은 하나의 데이터(샘플)만 처리하며, 미니배치 처리로 확장하기가 쉽지 않다.
할 수 있지만, "텐서 곱(dyadic product)" 계산이 필요해져서 설명이 복잡해지기에,
repeat()와 sum() 메소드를 이용해 가중합을 구해보자.
계속해서 미니배치 처리용 가중합을 구현한다.
다음 코드처럼 하면 된다.(hs와 a는 무작위로 생성했다.)
배열의 형상을 잘 보면, repeat()와 sum()에서 어느 차원(축)을 지정해야 하는지 곧바로 알 수 있을 것이다.
그럼 지금까지 수행한 가중합 계산을 '계산 그래프'로 표현해보겠다.
이제 이 역전파를 생각해보자.
참고로
- Repeat 노드의 역전파는 Sum이다.
- Sum 노드의 역전파는 Repeat이다.
텐서의 형상에 주의하여 살펴보면 어떤 축에 대한 Sum인지, 어느 축에 대한 Repeat인지도 금방 알 수 있다.
그럼 가중합 계층을 구현해보자.
위 계산 그래프를 그대로 코드로 옮겼다.
이제 다음 단계로 넘어가자.
Decoder 개선 2
각 단어의 중요도를 나타내는 가중치 a가 있다면, hs와의 가중합을 이용해 '맥락 벡터'를 얻을 수 있다.
그런데 이 a는 어떻게 구해야 할까?
그럼, 각 단어의 가중치 a를 구하는 방법을 살펴보자.
이 방법을 설명하려면, 우선 Decoder의 첫 번째(시각) LSTM 계층이 은닉 상태 벡터를 출력할 때까지의 처리부터 알아봐야 한다.
위 그림에서는 Decoder의 LSTM 계층의 은닉 상태 벡터를 h라 했다.
지금 목표는 이 h가 hs의 각 단어 벡터와 얼마나 '비슷한가'를 수치로 나타내는 것이다.
방법은 여러가지가 있지만, 여기에서는 가장 단순한 방법인 벡터의 '내적'을 이용하고자 한다.
내적은 '두 벡터가 얼마나 같은 방향을 향하고 있는가'이다.
따라서 두 벡터의 '유사도'를 표현하는 척도로 내적은 적절한 도구이다.
그럼 이제 벡터를 이용한 유사도 산출을 그림으로 살펴보자.
위 그림에서 보듯, 여기에서는 벡터의 내적을 이용해 h와 hs 내 각 단어 벡터와의 유사도를 구한다.
그리고 s는 그 결과이다.
참고로 s는 정규화하기 전의 값이며, '점수(score)'라고도 불린다.
이제 s를 정규화하기 위해서는 일반적으로 소프트맥스 함수를 적용한다.
소프트맥스 함수를 이용하면 그 출력인 a의 각 원소는 0.0~1.0 사이의 값이 되고, 모든 원소의 총합은 1이 된다.
이상으로 각 단어의 가중치를 나타내는 a를 구했다.
그럼, 지금까지의 과정을 코드로 살펴보자.
이 코드를 계산 그래프로 나타내면 아래와 같다.
이제 이 계산 그래프가 표현하는 처리를 AttentionWeight 클래스로 구현해보자.
Decoder 개선 3
지금까지 Decoder 개선안을 두 가지로 나눠 설명했다.
- WeightSum
- AttentionWeight
이제 이 둘을 하나로 결합해보자.
다시 한번 정리하자면,
- Attention Weight 계층
- Encoder가 출력하는 각 단어의 벡터 hs에 주목하여 해당 단어의 가중치 a를 구한다.
- Weight Sum 계층
- a와 hs의 가중합을 구하고
- 그 결과를 맥락 벡터 c로 출력한다.
이제 이 일련의 계산을 수행하는 계층을 Attention 계층이라고 부르자.
이제 이걸 코드로 구현해보자.
이 코드는 2개의 계층에 의한 순전파와 역전파를 수행할 뿐이다.
이때 각 단어의 가중치를 나중에 참조할 수 있도록 attention_weight라는 인스턴스 변수에 저장한다.
이상으로 Attention 계층의 구현은 끝이다.
이제 아래와 같이 Attention 계층을 LSTM계층과 Affine계층 사이에 삽입하면 된다.
추가로 잘 보면, 디코더 LSTM 계층의 은닉 상태 벡터가 Attention 계층을 넘어 Affine 계층까지 자연스럽게 들어가는 것을 볼 수 있다.
즉, Affine 계층에 "맥락 벡터"와 "은닉 상태 벡터" 두개가 함께 입력되었다.
이는 두 벡터를 "연결한 벡터"를 Affine 계층에 입력한다는 뜻이다.
마지막으로 시계열 방향으로 펼쳐진 다수의 Attention 계층을 Time Attention 계층으로 모아 구현하겠다.
이를 코드로 나타내면 아래와 같다.
지금까지의 코드를 보면 어텐션 계층에 경사 하강법으로 학습되는 가중치가 없는 것을 확인할 수 있었다.
이는 트랜스포머류에 사용되는 Scaled Dot-Product + Multi-Head + Wq/Wk/Wv 어텐션과는 조금 다르다.
현재 구현한 이 어텐션은 단순하고, 속도가 빠른 장점이 있다.
참고로 요새 많이 사용되는 트랜스포머류 어텐션은 아래의 형태를 띈다.
- Q (Query) →
h
(디코더의 현재 hidden state)- 코드 위치:
AttentionWeight.forward()
에서hr = h.reshape(N, 1, H)
- 코드 위치:
- K (Key) →
hs
(인코더의 모든 타임스텝 hidden states)- 코드 위치:
AttentionWeight.forward()
에서 파라미터로 들어오는hs
- 코드 위치:
- V (Value) →
hs
(인코더 hidden states 그대로)- 코드 위치:
WeightSum.forward()
에서c = np.sum(hs * ar, axis=1)
해당 Q, K, V는 각자 경사 하강법으로 학습되는 가중치 W를 갖는다.어텐션을 갖춘 seq2seq 구현
- 코드 위치:
이제 여태까지 구현한 어텐션 계층을 가지고, 어텐션을 갖춘 seq2seq를 구현해보자.
- AttentionEncoder
- AttentionDecoder
- AttentionSeq2seq
Encoder 구현
이 클래스는 이전에 구현했던 Encoder 클래스와 거의 같다.
다만, forward() 메소드가 모든 은닉 상태를 반환하도록 했다.
Decoder 구현
이어서 Attention 계층을 이용한 Decoder의 구현이다.
구성은 아래와 같다.
그림에서 보듯, 앞 장의 구현과 마찬가지로 Time Softmax with Loss 계층의 앞까지를 Decoder로 정의하겠다.
역시 앞 장과 마찬가지로, 다음 세가지 메소드 형태로 구현한다.
- 순전파 forward()
- 역전파 backward()
- 시퀀스 생성 generate()
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
이 구현은 Time Attention 계층이 새롭게 사용되는 것을 제외하면 앞장의 Decoder 클래스와 크게 다르지 않다.
forward() 메소드에서 Time Attention 계층의 출력과 LSTM 계층의 출력을 np.concatenate()
메소드를 이용해 연결한다는 점만 주의하면 된다.
seq2seq 구현
마지막으로 seq2seq 구현이다.
기존 Seq2seq를 상속받고, 초기화 메소드에서 AttentionEncoder와 AttentionDecoder를 사용하기로만 만들면 된다.
어텐션 평가
이제 어텐션 평가를 수행해보자.
원래대로면 번역 문제를 가져와 어텐션의 효과를 확인해보고 싶지만, 아쉽게도 번역용 데이터셋은 크기가 좀 크다.
가장 유명한 번역용 데이터셋 WMT는 크기가 20GB 정도 된다.
그래서 날짜 형식을 변환하는 문제를 풀어볼까 한다.
날짜 형식 변환 문제
우리는 영어권에서 사용되는 다양한 날짜 형식을 표준 형식으로 변환할 것이다.
날짜 형식 변환 문제를 채용한 데는 두 가지 이유가 있다.
- 입력되는 날짜 데이터에는 다양한 변형이 존재하여 변환 규칙이 나름 복잡해진다.
- 문제의 입력(질문)과 출력(답변) 사이에 알기 쉬운 대응 관계까 있다.
- 따라서 어텐션이 각각의 원소에 올바르게 주목하고 있는지를 확인할 수 있다.(확인해볼 예정이다.)
주어진 데이터셋은 다음과 같은 형태를 띄고 있다.
이 데이터셋의 특징을 간단하게 살펴보자.
- 입력 문장의 길이를 통일하기 위해 공백 문자 패딩이 들어가있다.
- 입력과 출력의 구분 문자로는 '_'를 사용했다.
- 이 문제에서는 출력의 문자 수는 일정하기 때문에 출력의 끝을 알리는 구분 문자는 따로 사용하지 않았다.
어텐션을 갖춘 seq2seq의 학습
이제 학습을 시작해보자.
아래는 학습 코드이다.
from common.np import *
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)
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()
x = np.arange(len(acc_list))
plt.plot(x, acc_list, marker='o')
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.ylim(-0.05, 1.05)
plt.show()
이 코드는 앞 장에서 본 '덧셈'의 학습용 코드와 거의 같다.
이제 코드를 실행해보자. 그러면 학습이 진행되면서 다음과 같은 결과가 나타난다.
그림에서 보듯, 어텐션을 갖춘 seq2seq는 학습을 거듭할수록 점점 똑똑해진다.
얼마 지나지 않아 문제 대부분을 정확히 맞추게 된다.
그리고 테스트 데이터로 얻은 정답률을 그래프로 그리면 아래 그래프가 된다.
매우 뛰어난 것을 확인할 수 있었다.
어텐션 시각화
신경망 내부에서 어떠한 처리가 이뤄지고 있는지는 인간이 이해할 수 없는게 일반적이다.
반면 어텐션은 '인간이 이해할 수 있는 구조나 의미'를 모델에 제공한다.
아래 예에서는 어텐션을 사용함으로써 단어와 단어의 관련성을 볼 수 있다.
어텐션에 관한 남은 이야기
이제 어텐션에 대해 남은 이야기를 마무리 지어보자.
양방향 RNN
앞 절까지의 Encoder는 아래와 같이 표현할 수 있다.
그런데 여기서 주목할 것은 우리는 글을 왼쪽에서 오른쪽으로 읽는다는 점이다.
따라서 위 예에서는 "고양이"에 대응하는 벡터에 "나", "는", "고양이" 이 세 단어의 정보만 들어간다.
여기서 전체적인 균형을 생각하면, "고양이" 단어의 '주변' 정보를 균형 있게 담고 싶을 것이다.
우리는 현재 번역 문제라서 시계열 데이터를 한꺼번에 건네준다. 따라서 문장을 오른쪽부터 처리하도록 할 수 있다.
그래서 LSTM을 양방향으로 처리하는 방법을 생각할 수 있다.
이것이 양방향 LSTM이다.
위 그림에서 보듯, 양방향 LSTM에서는 지금까지의 LSTM 계층에 더해 역방향으로 처리하는 LSTM 계층도 추가한다.
그러고 두 LSTM 계층의 은닉 상태를 연결시키고 이를 최종 은닉 상태로 처리한다.
'연결' 외에도 '합'하거나 '평균' 내는것도 가능하다.
이는 구현하기도 쉽다.
한 가지 방법은, 2개의 LSTM 계층을 사용하여 각각의 계층에 주는 단어를 조정하면 된다.
Attention 계층 사용 방법
우리는 LSTM 계층을 아래와 같이 사용했다.
하지만 이렇게 써도 된다.
이렇게 써서 얻는게 뭐냐고?
모른다!
해보기 전까진 모른다.
근거를 가지고 가설을 세운 뒤, 실험해보고 평가해보자.
seq2seq 심층화와 skip connection
번역 등 현실에서의 애플리케이션들은 풀어야 할 문제가 훨씬 복잡하다.
그렇다면 어텐션을 갖춘 seq2seq에서도 더 높은 표현력이 요구될 것이다.
이때 우선 생각해야 할 것은 RNN 계층을 깊게 쌓는 방법이다.
위 모델에서는 Encoder와 Decoder로 3층 LSTM 계층을 사용하고 있다.
이 예처럼 Encoder와 Decoder에서는 같은 층수의 LSTM 계층을 이용하는 것이 일반적이다.
한편, Attention 계층은 사용법이 여러 변형이 있을 수 있다.
여기선 Decoder의 LSTM 계층의 은닉 상태를 Attention 계층에 입력하고, Attention 계층의 출력인 맥락 벡터를 Decoder의 여러 계층(LSTM 계층과 Affine 계층)으로 전파한다.
Skip connection
- skip connection
- 잔차 연결(Residual connection)
- 숏컷(short-cut)
다양한 이름으로 불리는 skip connection은 아래 그림처럼 계층을 넘어 '선을 연결'하는 단순한 기법이다.
이때 skip connection의 접속부에서는 2개의 출력이 더해진다.
이 덧셈이 핵심이다.
왜냐하면 층이 깊어질수록 기울기 소실 가능성이 높아지는데, 이 방식으로 층이 깊어져도 기울기가 소실(혹은 폭발)하지 않고 전파되어, 결과적으로 좋은 학습을 기대할 수 있다.
RNN 계층의 역전파에서는 시간 방향(우리 그림의 가로축)에서의 기울기 소실 혹은 폭발이 일어날 수 있었다.
지금 이야기하는 것은 깊이 방향(우리 그림의 세로축)에서의 기울기 소실 혹은 폭발의 관점이다.
어텐션 응용 - 트랜스포머
Attention is all you need
트랜스포머는 RNN의 병렬 처리 불가능 문제를 피하고자, RNN을 없앤 어텐션을 사용해 처리한다.
트랜스포머는 어텐션으로 구성되는데, 그중 Self-Attention이라는 기술을 이용하는 것이 핵심이다.
이는 직역하면 '자신에 대한 주목'이 된다.
즉, 하나의 시계열 데이터를 대상으로 한 어텐션으로, '하나의 시계열 데이터 내에서' 각 원소가 다른 원소들과 어떻게 관련되는지를 살펴보자는 취지이다.
우리의 Time Attention을 기준으로 그려보면, 아래와 같이 표현된다.
지금까지 우리는 어텐션에서 '번역'과 같이 2개의 시계열 데이터 사이의 대응 관계를 구해왔다.
이때 Time Attention 계층에서는 왼쪽처럼 서로 다른 두 시계열 데이터가 어텐션 계층에 입력된다.
이렇게 서로 다른 두 시계열 데이터 내 원소 간의 대응 관계가 구해진다.
반면, self 어텐션은 두 입력선이 모두 하나의 시계열 데이터로부터 나온다.
이렇게 하면 하나의 시계열 데이터 내에서 원소 간 대응 관계가 구해진다.
트랜스포머에서는 RNN 대신 어텐션을 사용한다.
위 그림에서 Nx는 회색 배경으로 둘러싸인 계층들을 N겹 쌓았다는 것이다.
이런 방식으로 트랜스포머를 구성한다.
트랜스포머를 이용하면 계산량을 줄이고 GPU를 이용한 병렬 계산의 혜택도 더 많이 누릴 수 있다.
이번 장에서 배운 것
- 번역이나 음성 인식 등, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 작업에서는 시계열 데이터 사이의 대응 관계가 존재하는 경우가 많다.
- 어텐션은 두 시계열 데이터 사이의 대응 관계를 데이터로부터 학습한다.
- 어텐션에서는 (하나의 방법으로서) 벡터의 내적을 사용해 벡터 사이의 유사도를 구하고, 그 유사도를 이용한 가중합 벡터가 어텐션의 출력이 된다.
- 어텐션에서 사용하는 연산은 미분 가능하기 때문에 오차역전파법으로 학습할 수 있다.
- 어텐션이 산출하는 가중치(확률)를 시각화하면 입출력의 대응 관계를 볼 수 있다.
- 외부 메모리를 활용한 신경망 확장 연구 예에서는 메모리를 읽고 쓰는 데 어텐션을 사용했다.