목차
👀, 🤷♀️ , 📜
이 아이콘들을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
INTRO
- seq2seq의 가능성
- RNN의 가능성을 한걸음 더 깊이 탐구
어텐션은 최근의 딥러닝 분야에서 틀림없이 중요한 기술 중 하나
어텐션의 구조
seq2seq를 한층 더 강력하게 하는 어텐션 메커니즘
- seq2seq는 (우리 인간처럼) 필요한 정보 에만 ‘주목’ 할 수 있게 됨.
- 지금까지의 seq2seq가 안고 있던 문제도 해결가능
Encoder 개선
‘고정 길이의 벡터’
아무리 긴 문장이 입력되더라도 항상 똑같은 길이의 벡터에 밀어 넣어야 함
- 이렇게 하면 한계가 찾아옴.
- 필요한 정보가 벡터에다 담기지 못함.
[개선]
- Encoder 개선: 지금까지 LSTM 계층의 마지막 은닉 상태만 을 Decoder에 전달
- But: Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는 게 좋음.
- 시각별 LSTM 계층의 은닉 상태 벡터를 모두 이용: 각 시각 (각 단어 )의 은닉 상태 벡터를 모두 이용하면 입력된 단어와 같은 수의 벡터를 얻을 수 있음
위의 예에서는 5개의 단어가 입력되었고, 이때 Encoder는 5개의 벡터를 출력.
이것으로 Encoder는 ‘하나의 고정 길이 벡터’라는 제약으로부터 해방
딥러닝 프레임워크에서는 RNN 계층(혹은 LSTM 계층과 GRU 계층 등)을 초기화할 때,
- ‘모든 시각의 은닉 상태 벡터 반환’
- ‘마지막 은닉 상태 벡터만 반환’ 중 선택 가능
📜 시각별 LSTM 계층의 은닉 상태의 정보 내용
각 시각의 은닉 상태에는 직전에 입력된 단어에 대한 정보가 많이 포함되어 있음
ex) 예컨대 “고양이” 단어를 입력했을 때의 LSTM 계층의 출력(은닉 상태)은 직전에 입력한 “고양이”라는 단어의 영향을 가장 크게 받음.
➡ 이 은닉 상태 벡터는 “고양이”의 ‘성분’이 많이 들어간 벡터라고 생각 가능
즉, Encoder가 출력하는 hs 행렬
- 각 단어에 해당하는 벡터들의 집합
- 단어의 수 만큼 벡터를 포함
- 각각의 벡터안에는 해당 단어에 대한 정보를 많이 포함함
WARNING
Encoder는 왼쪽에서 오른쪽으로 처리하므로, 방금 전의 “고양이” 벡터에는 정확히 총 3개 단어 (“나”, “는”, “고양이”)의 정보가 담겨있음.
그런데 전체의 균형을 생각하여 “고양이” 단어의 ‘주 변’ 정보를 균형 있게 담아야 할 때도 있을 텐데, 그런 경우엔 시계열 데이터를 양방향으로 처리하는 양방향 RNN (혹은 양방향 LSTM )이 효과적(아직 안배움)
[평가]
단지 Encoder의 은닉 상태를 모든 시각만큼 꺼냈을 뿐이지만,
이 작은 개선 덕분에 Encoder는 입력 문장의 길이에 비례한 정보를 인코딩 가능.
Decoder 개선 ①
Encoder, Decoder 관계
1) 각 단어에 대응하는 LSTM 계층의 은닉 상태 벡터를 hs로 모아 출력
2) 그리고 이 hs가 Decoder에 전달되어 시계열 변환이 이뤄짐
[개선 전 seq2seq]
Encoder의 마지막 은닉 상태 벡터만을 Decoder에 넘김.
- Encoder의 LSTM 계층의 ‘마지막’ 은닉 상태를 Decoder의 LSTM 계층의 ‘첫’ 은닉 상태로 설정
이 hs 전부를 활용할 수 있도록 Decoder를 개선
[개선점]
‘입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는가’라는 대응 관계를 seq2seq에게 학습시킴
Ex) ‘나’는 ‘I’와 ‘고양이’는 ‘cat’과 관련있음
📜 얼라인먼트 alignment
단어(혹은 문구)의 대응 관계를 나타내는 정보
기계 번역의 역사를 보면 ‘고양이 = cat’과 같은 단어의 대응 관계 지식을 이용하는 연구는 많이 이뤄져 왔음
지금까지는 얼라인먼트를 주로 사람이 수작업으로 만듦
그러나 어텐션 기술은 얼라인먼트라는 아이디어를 seq2seq에 자동으로 도입하는 데 성공.
[어텐션]
어텐션: 필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 것
- ‘도착어 단어’와 대응 관계에 있는 ‘출발어 단어’의 정보를 골라내는 것
- 그리고 그 정보를 이용하여 번역을 수행하는 것.
즉 우리의 목표인 어텐션의 전체 틀을 보자면,
- ‘어떤 계산’을 수행하는 계층을 추가
- ‘어떤 계산’이 받는 입력(2개)
- hs: Encoder로부터 받음
- 시각별 LSTM 계층의 은닉 상태
- ‘어떤 계산’이 받는 입력(2개)
- 여기에서 필요한 정보만 골라 위쪽의 Affine 계층으로 출력
- 지금까지와 똑같이 Encoder의 마지막 은닉 상태 벡터는 Decoder의 첫 번째 LSTM 계층에 전달
[위 신경망의 목표]
얼라인먼트 추출
- 각 시각에서 Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내겠다는 뜻.
- ex) Decoder가 “I”를 출력할 때, hs에서 “나”에 대응하는 벡터를 선택
- 이러한 ‘선택’ 작업을 ‘어떤 계산’으로 해내겠다는 것
⛔ problem
선택하는 작업(여러 대상으로부터 몇 개를 선택하는 작업)은 미분 불가
WARNING 신경망의 학습은 (일반적으로) 오차역전파법으로 이뤄짐.
따라서 미분 가능한 연산으로 신경망을 구축하면 오차역전파법의 틀 안에서 학습을 수행 가능
반대로 미분 가능한 연산을 이용 하지 않으면 (기본적으로는) 오차역전파법을 사용 불가
⭕ solution
‘선택한다’라는 작업을 미분 가능한 연산으로 대체 방법
1) ‘하나를 선택’하는 게 아니라, ‘모든 것을 선택’
2) 이 때 각 단어의 중요도(기여도) 를 나타내는 ‘가중치’를 별도로 계산
- 각 단어의 중요도를 나타내는 ‘가중치’(기호 a )를 이용
- a는 확률분포처럼 각 원소가 0.0~1.0 사이의 스칼라(단일 원소)이며, 모든 원소의 총합은 1.
3) 맥락 벡터© 구하기
- 각 단어의 중요도를 나타내는 가중치 a와 각 단어의 벡터 hs의 가중합 weighted sum 로부터 우리가 원하는 벡터(맥락 벡터(c))를 얻음.
- ex) “나”에 대응하는 가중치가 0.8
- 맥락 벡터 c에 “나” 벡터의 성분이 많이 포함되어 있다는 것
- 즉, “나” 벡터를 ‘선택’하는 작업을 이 가중합으로 대체 가능.
- 예컨대 “나”에 대응하는 가중치가 1이고 그 외에는 0이라면, “나” 벡터를 ‘선택’한다고 해석 가능
[코드 구현]
- Encoder가 출력하는 hs와 각 단어의 가중치 a를 적당하게 작성
- 그 가중합을 구하는 구현
- 다차원 배열의 형상에 주의
👀코드 보기
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)
print(ar.shape)
# (5, 4)
t = hs * ar
print(t.shape)
# (5, 4)
c = np.sum(t, axis=0)
print(c.shape)
# (4,)
📜 코드 설명 보기
(코드 설명)
- 시계열의 길이를 T = 5
- 은닉 상태 벡터의 원소 수를 H = 4
T와H로 가중합을 구하는 과정을 보여줌.
ar = a.reshape (5, 1 ).repeat (4, axis=1 )
형상이 (5,)인 a를 복사하여, (5, 4 )짜리 배열을 만드는 것
1) 그래서 원래 형상이 (5,)인 a를 a.reshape (5, 1 )을 거쳐
2) (5, 1 ) 형상으로 성형한 다음,
3) 이 배열의 한 축을 네 번 반복하여 형상이 (5, 4 )인 배열을 생성
x.repeat (rep, axis )
다차원 배열의 원소를 복사하여 새로운 다차원 배열을 생성
- x: 넘파이 다차원 배열일 때
- rep: 복사를 반복하는 횟수
- axis: 반복하는 축(차원)을 지정
- ex) x의 형상이 (X, Y, Z )일 때, x. repeat (3, axis=1 )
- 실행 하면 x에서 인덱스가 1인 축(1차원 방향)이 복사되어 형상이 (X, 3 * Y, Z )인 다차원 배열이 만들어짐
➡ repeat ( ) 메서드 대신 넘파이의 브로드캐스트 사용 가능
T = hs*ar
- 구현 효율을 생각하면 repeat ( ) 메서드보단 넘파이의 브로드캐스트 이용
- 다만, 이렇게 하면 다차원 배열의 원소가 복사(반복)되고 있다고 점이 우리 눈에 보이지 않으니 유의
- 이 계산의 역전파는 repeat노드에 해당(1.3.4 계산 그래프)
c = np.sum (hs * ar, axis=0 )
합을 구함
- axis 인수: 어느 축(차원) 방향으로 합을 계산할지를 지정.
WARNING_ 가중합 계산은 ‘행렬 곱’을 사용하는 편이 가장 간단하고 효율적.
앞의 예로 말하자면 np.matmul (a, hs )라는 한 줄만으로 원하는 결과를 얻을 수 있음.
다만, 이 방법은 하나의 데이터(샘플) 만 처리하며, 미니배치 처리로 확장 어려움
즉 이코드에서 구현한 처리를 간단히 보면,
[미니배치 처리용 가중합 구현]
- hs와 a는 무작위로 생성
👀코드 보기
N, T, H = 10, 5, 4
hs = np.random.randn(N, T, H)
a = np.random.randn(N, T)
ar = a.reshape(N, T, 1).repeat(H, axis=2)
# ar = a.reshape(N, T, 1) # 브로드캐스트를 사용하는 경우
t = hs * ar
print(t.shape)
# (10, 5, 4)
c = np.sum(t, axis=1)
print(c.shape)
# (10, 4)
지금까지의 정리도 겸해, 가중합 계산,
1) Repeat 노드를 사용해 a를 복제.
2) ‘×’ 노드로 원소별 곱을 계산
3) Sum 노드로 합을 구함
[역전파 구현]
1) Repeat의 역전파는 Sum
2) Sum의 역전파는 Repeat
계산 그래프를 계층으로 구현(Weight Sum 계층)
👀코드 보기
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
이 계층은 학습하는 매개변수가 없음
➡ self.params = []로 설정.
Decoder 개선 ②
각 단어의 중요도를 나타내는 가중치 a가 있다면, 가중합을 이용해 ‘맥락 벡터’를 얻을 수 있음.
[a를 구하는 법]
(intro)
우선 Decoder 의 첫 번째(시각) LSTM 계층이 은닉 상태 벡터를 출력할 때까지의 처리부터 보면
(목표)
h가 hs의 각 단어 벡터와 얼마나 ‘비슷한가’를 수치로 나타내는 것
(solution)
벡터의 ‘내적’을 이용.
내적의 계산
두벡터 \(a=(a_1 ,a_2 ,…,a_n )\)와 \(b=(b_1 ,b_2 ,…,b_n )\)의 내적을 구해보면,
\(a·b = a_1* b_1 + a_2* b_2 + … + a_n* b_n\)
직관적인 의미
- ‘두 벡터가 얼마나 같은 방향을 향하고 있는가’
- 따라서 두 벡터의 ‘유사도’를 표현하는 척도로 내적을 이용하는 것은 자연스러운 선택임
내적을 이용해 벡터 사이의 유사도를 산출할 때까지의 처리
1) 벡터의 내적을 이용해 h와 hs의 각 단어 벡터와의 유사도를 구함
2) s는 그 결과
- s는 정규화하기 전의 값
- ‘점수 score ’라고도 함
3) 계속해서 s를 정규화하기 위해서는 일반적으로 소프트맥스 함수를 적용
4) 가중치를 나타내는 a 구하기 완료
👀코드 보기
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)
# hr = h.reshape(N, 1, H) # 브로드캐스트를 사용하는 경우
t = hs * hr
print(t.shape)
# (10, 5, 4)
s = np.sum(t, axis=2)
print(s.shape)
# (10, 5)
softmax = Softmax() a = softmax.forward(s)
print(a.shape)
# (10, 5)
이 구현은 미니배치 처리를 수행할 때의 코드임
[계산 그래프 구현 AttentionWeight]
- Repeat 노드
- 원소별 곱을 뜻하는×노드
- Sum 노드
- 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
Decoder 개선 ③
[지금까지 Decoder 개선안]
- Attention Weight 계층
- Encoder가 출력하는 각 단어의 벡터 hs에 주목하여 해당 단어의 가중치 a를 구함
- Weight Sum 계층
- 계층이 a와 hs의 가중합을 구하고, 그 결과를 맥락 벡터 c로
이제 이 두 계층을 하나로 결합(Attention 계층)
Attention 계층
- Encoder가 건네주는 정보 hs에서 중요한 원소에 주목 하여, 그것을 바탕으로 맥락 벡터를 구해 위쪽 계층으로 전파
- (우리의 경우, 위쪽에는 Affine 계층이 기다리고 있음)
👀Attention 계층을 구현한 코드 보기
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
: 이때 각 단어의 가중치를 나중에 참조할 수 있도록 이 인스턴스 변수에 저장.
LSTM 계층의 은닉 상태 벡터를 Affine 계층에 입력.
- Decoder에 어텐션 정보를 ‘추가’할 수 있기 때문
- Decoder에 Attention 계층이 구한 맥락 벡터 정보를 ‘추가’ 한 것
-
Affine 계층에는 기존과 마찬가지로 LSTM 계층의 은닉 상태 벡터를 주고, 여기에 더해 Attention 계층의 맥락 벡터까지 입력
- (앞에서도 설명한 것처럼) 두 벡터를 ‘연결한 벡터’를 Affine 계층에 입력한다는 뜻
➕ 위의 시계열 방향으로 펼쳐진 다수의 Attention 계층을 Time Attention 계층으로 모아 구현
- Time Attention 계층은 다수의 Attention 계층을 모았을 뿐
어텐션을 갖춘 seq2seq 구현
- AttentionEncoder
- AttentionDecoder
- AttentionSeq2seq
[Encoder 구현]
앞 장에서 구현한 Encoder 클래 스와 거의 같음
- Encoder 클래스의
forward ( )
메서드: LSTM 계층의 마지막 은닉 상태 벡터만을 반환 - AttentionEncoder: 이번에는 모든 은닉 상태를 반환
👀코드 보기
# 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 구현] Softmax 계층(정확히는 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)
이 구현은 Time Attention 계층이 새롭게 사용되는 것을 제외하면 앞 장의 Decoder 클래스와 크게 다르지 않음
forward ( )
메서드에서 Time Attention 계층의 출력과 LSTM 계층의 출력을 연결한다는 점만 주의- 두 출력을 연결할 때는
np.concatenate ( )
메서드를 사용
[seq2seq 구현]
Encoder 대신 AttentionEncoder 클래스
Decoder 대신 AttentionDecoder 클래스를 사용
- 따라서 앞 장의 Seq2seq 클래스를 상속하고
- 초기화 메서드를 수정하는 것만으로 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
어텐션 평가
날짜 형식을 변경하는 문제
- 데이터 크기가 작고
- 어느 쪽인가를 맞추는 인위적인 문제)로 어텐션을 갖춘 seq2seq의 효과를 확인해보려 함
[날짜 형식 변환 문제]
“september 27, 1994” 등의 날짜 데이터를 “1994-09-27” 같은 표준 형식으로 변환
(채택 이유)
- 입력되는 날짜 데이터에는 다양한 변형이 존재하여 겉보기만큼 간단하지 않음.
- 문제의 입력(질문)과 출력(답변) 사이에 알기 쉬운 대응 관계가 있기 때문
- 어텐션이 각각의 원소에 올바르게 주목하고 있는지를 확인 가능
- 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩,
- 입력과 출력의 구분 문자로는
_
(밑줄)을 사용 - 그리고 이 문제에서는 출력의 문자 수는 일정하기 때문에 출력의 끝을 알리는
구분 문자는 따로 사용 안함
[어텐션을 갖춘 seq2seq의 학습]
그럼, 날짜 변환용 데이터셋으로 AttentionSeq2seq를 학습시킴
👀코드 보기
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 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('val acc %.3f%%' % (acc * 100))
model.save_params()
다른 점
- 학습 데이터가 날짜 데이터라는 것
- 모델로 AttentionSeq2seq를 사용한다는 것.
- 입력 문장을 반전시키는 기법(Reverse )도 적용
그런 다음 학습을 수행하면서, 에폭마다 테스트 데이터를 사용하여 정답률을 측정
이때 처음 10 문제의 결과는 (질문 문장과 정답도 함께) 터미널에 출력
(결과)
위에서 보듯 어텐션을 갖춘 seq2seq는 학습을 거듭할수록 점점 똑똑해짐
얼마 지나지 않아 문제 대부분을 정확히 맞춤
그리고 테스트 데이터로 얻은 정답률
아주 좋은 결과
(개선 전과 비교)
[어텐션 시각화]
어텐션이 시계열 변환을 수행할 때,
어느 원소에 주의를 기울이는지를 눈으로 살펴보려는 시도.
- Attention 계층은 각 시각의 어텐션 가중치를 인스턴스 변수로 보관하고 있으므로, 이를 시각화하기란 아주 간단
우리 구현에서는 Time Attention 계층에 있는 인스턴스 변수 attention_weights에 각 시각의 어텐션 가중치가 저장됨.
- 이것을 사용하면 입력 문장과 출력 문장의 단어 대응 관계를 2차원 맵으로 그릴 수 있음
- 이번 절에서는 학습이 끝난 AttentionSeq2seq로 날짜 변환을 수행할 때의 어텐션 가중치를 시각화
(결과)
ex) seq2seq가 최초의 “1”을 출력할 때는 입력 문장의 “1” 위치에 표시.
여기서 주목할 점은 년·월·일의 대응 관계
- 세로축(출력)의 “1983”과 “26”이 가로축(입력)의 “1983”과 “26”에 훌륭하게 대응
- 월을 뜻하는 “08”에 입력 문장의 “AUGUST”가 대응
- seq2seq 는 “August”가 “8월”에 대응한다는 사실을 데이터만 가지고 학습해낸 것
어텐션에 관한 남은 이야기
양방향 RNN
이번 절은 seq2seq의 Encoder에 초점을 둠
[원래 Encoder]
- LSTM의 각 시각의 은닉 상태 벡터는 hs로 모아짐.
- Encoder가 출력하는 hs의 각 행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있음
[주목할 점]
우리는 글을 왼쪽에서 오른쪽으로 읽음
- “고양이”에 대응하는 벡터에 “나”, “는”, “고양이”까지 총 세 단어의 정보가 인코딩되어 들어감.
- 여기에서 전체적인 균형을 생각하면, “고양이” 단어의 ‘주변’ 정보를 균형 있게 담고 싶을 것임
WARNING
이번 번역 문제에서 우리는 시계열 데이터(번역해야 할 문장)를 한꺼번에 건네줌.
따라서 문장을 오른쪽부터 읽도록(처리하도록) 할 수도 있음
[개선] 양방향 LSTM (양방향 RNN)
그래서 LSTM을 양방향으로 처리하는 방법을 생각가능
양방향 LSTM에서는 지금까지의 LSTM 계층에 더해 역방향으로 처리 하는 LSTM 계층도 추가
각 시각: 이 두 LSTM 계층의 은닉 상태를 연결 시킨 벡터를 최종 은닉 상태로 처리(‘연결’ 외에도, ‘합’하거나 ‘평균’내는 방법 등도 존재).
이처럼 양방향으로 처리함으로써, 각 단어에 대응하는 은닉 상태 벡터에는 좌와 우 양쪽 방향 으로부터의 정보를 집약가능
- 균형 잡힌 정보가 인코딩 됨
[구현]
2개의 LSTM 계층(우리의 경우는 Time LSTM 계층)을 사용하여 각각의 계층에 주는 단어의 순서 조정
1) LSTM 계층 하나는 지금까지와 똑같음
- 입력 문장을 ‘왼쪽부터 오른쪽으로’ 처리하는 일반적인 LSTM 계층
2) 또 하나의 LSTM 계층에는 입력 문장의 단어들을 반대 순서로 나열.
- 만약 원래 문장이 “A B C D”였다면 “D C B A”로 순서를 바꾸는 것
- 두 번째 LSTM 계층은 입력문을 ‘오른쪽에서 왼쪽으로’ 처리하게 됨
3) 마지막으로 이 두 LSTM 계층의 출력을 연결하기만 하면 양방향 LSTM 계층이 완성
Attention 계층 사용 방법
앞 절까지 사용해온 어텐션을 갖춘 seq2seq의 계층 구성
- Attention 계층을 LSTM 계층과 Affine 계층 사이에 삽입.
- Attention 계층을 이용하는 장소가 반드시 이 그림과 같을 필요는 없음
[Attention 계층의 다른 사용 예]
이렇게 구성하면 LSTM 계층이 맥락 벡터의 정보를 이용할 수 있음
- 우리가 구현한 모델은 Affine 계층이 맥락 벡터를 이용
[개선 후 영향]
그 답은 해보지 않으면 모름.
현실은 실제 데이터를 사용해 검증할 수밖에 없음
다만, 앞의 두모델은 모두 맥락 벡터를 잘 활용하는 구성이라서 큰 차이가 없을지도 모름
[구현]
또한, 구현 관점에서는 전자의 구성(LSTM 계층과 Affine 계층 사이에 Attention 계층을 삽입) 쪽이 구현하기 쉬움.
그리고 전자의 구성에서는 Decoder의 데이터 흐름이 아래에서 위로 가는 한 방향이기 때문에 Attention 계층을 쉽게 모듈화할 수 있습니다. 실제로도 우리는 Time Attention 계층으로 간단히 모듈화 가능
정리
- 번역이나 음성 인식 등, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 작업에서는 시계열 데이터 사이의 대응 관계가 존재하는 경우가 많다.
- 어텐션은 두 시계열 데이터 사이의 대응 관계를 데이터로부터 학습한다.
- 어텐션에서는 (하나의 방법으로서) 벡터의 내적을 사용해 벡터 사이의 유사도를 구하고, 그 유사도를 이용한 가중합 벡터가 어텐션의 출력이 된다.
- 어텐션에서 사용하는 연산은 미분 가능하기 때문에 오차역전파법으로 학습할 수 있다.
- 어텐션이 산출하는 가중치(확률)를 시각화하면 입출력의 대응 관계를 볼 수 있다.
- 외부 메모리를 활용한 신경망 확장 연구 예에서는 메모리를 읽고 쓰는 데 어텐션을 사용했다.