8.1. 어텐션의 구조
✔️ seq2seq의 문제점
seq2seq에서 Encoder의 출력은 '고정 길이의 벡터'였다.
고정길이벡터는 입력 문장의 길이에 관계없이(아무리 길어도) 항상 같은 길이의 벡터로 변환한다는 뜻이다.
이렇게 되면 필요한 정보가 벡터에 모두 담기지 못하게 된다.
✔️ Encoder 개선
Encoder의 출력 길이는 입력 문장의 길이에 따라 바꿔주는게 좋다.
이를 위해 그림 8-2처럼 시각별 LSTM 계층의 은닉 상태 벡터를 모두 이용한다.
각 시각별 LSTM 계층의 은닉 상태이기 때문에 hs 행렬은 각 단어에 해당하는 벡터들의 집합이라고 볼 수 있다.
✔️ Decoder 개선(1)
Decoder의 LSTM 계층의 '첫' 은닉 상태는 Encoder LSTM 계층의 '마지막' 은닉상태이다.
즉 그림 8-5와 같이 hs에서 마지막줄만 빼내어 Decoder에 전달하는 것이다.
우리가 번역을 할 때 'I am a cat'이라는 문장이 있다면 'I = 나', 'cat = 고양이'라는 지식을 사용한다.
이처럼 도착어 단어와 대응 관계에 있는 출발어 단어의 정보를 골라내어 번역을 수행하도록 개선해보자.
(얼라인먼트(alignment) : 단어(혹은 문구)의 대응 관계를 나타내는 정보)
즉 필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 어텐션을 수행할 것이다.
구조는 그림 8-6과 같다. '어떤 계산'을 추가할 건데, Encoder로부터 받는 hs, 시각별 LSTM계층의 은닉상태를 입력으로 받는다.
여기서 필요한 정보만 골라 위쪽의 Affine 계층으로 출력한다.
그런데 각 시각에서 Decoder에 입력된 단어와 대응 관계인 단어의 벡터 hs를 골라내는 작업(얼라인먼트)은 미분할 수가 없다.
이는 '선택한다'라는 작업를 '하나를 선택'하는게 아니라 '모든 것을 선택'하여 각 단어의 중요도(기여도)를 나타내는 '가중치'를 별도로 계산하여 해결할 수 있다.
그림 8-8처럼 각 단어의 중요도를 나타내는 '가중치(a)'와 각 단어의 벡터 hs로부터 가중치 합(weighted sum)을 구하여 우리가 원하는 벡터인 '맥락 벡터(c)'를 구한다.
이를 구현해보면 가중치 a를 reshape와 repeat을 거쳐 형상을 변환(ar)한다.
그 후 hs * ar하여 가중치 합을 구한다.
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=-1)
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의 역전파 = repeat
dar = dt * hs
dhs = dt * ar
da = np.sum(dar, axis=2) # repeat노드에 대한 역전퍄 = sum
return dhs, da
✔️ Decoder 개선(2)
그렇다면 '맥락 벡터'를 얻기 위한 가중치 a는 어떻게 구할까?
'두 벡터가 얼마나 같은 방향을 향하고 있는가(유사도)'를 나타내는 내적을 이용한다.
그림 8-13처럼 내적을 이용해 hs와 h의 각 단어 벡터와의 유사도(점수)를 구한다.
그 후 소프트맥스 함수를 이용해 정규화를 거쳐 가중치(확률)를 구한다.(그림 8-14)
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 개선(3)
개선안 2가지를 결합해보자.
Attention Weight 계층은 Encoder가 출력하는 각 단어의 벡터 hs를 활용해 해당 단어의 가중치 a를 구한다.
그 다음 Weight Sum 계층이 a와 hs의 가중합을 구하여 맥락 벡터 c로 출력한다.
우리는 이를 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
최종적으로 그림 8-6에서 '어떤 계산(LSTM계층과 Affine 계층 사이)'에 Attention 계층을 삽입하면 된다.
각 시각의 Attention 계층은 Encoder의 출력인 hs와 LSTM 계층의 은닉 상태 벡터가 입력되어 맥락 벡터를 출력한다.
또한, LSTM 계층의 은닉 상태 벡터는 개선 전과 같이 Affine 계층에도 입력한다.(그림 8-18)
즉, 개선 전 Decoder에 Attention 계층의 맥락 벡터를 추가한 것이다.(그림 8-19)
다수의 Attention 계층을 Time Attention 계층으로 모아 구현하면 그림 8-20과 같다.
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
8.2. 어텐션을 갖춘 seq2seq 구현
구체적인 구현 코드는 https://github.com/ExcelsiorCJH/DLFromScratch2.git 참고
8.4. 어텐션에 관한 남은 이야기
✔️ 양방향 RNN
이 때까지 seq2seq Encoder는 LSTM의 각 시각의 은닉 상태 벡터를 hs로 모은다.
그래서 hs 각 행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있다.
그런데 글을 왼쪽에서 오른쪽으로 읽는 것처럼 '고양이'에 대응하는 벡터에는 '나', '는', '고양이' 총 3단어의 정보가 인코딩 되어 들어간다.
전체적인 균형을 생각하면 '고양이'의 '주변'정보를 균형있게 담는 것이 좋다.
이를 위해 LSTM을 양방향으로 처리하는 양방향 LSTM(양방향 RNN)을 생각할 수 있다.
구현할 때는 입력 문장을 '왼쪽부터 오른쪽으로' 처리하는 일반적인 LSTM 계층과 입력문장의 단어들을 반대순서로 나열하는 또 하나의 LSTM 계층을 사용하면 된다.
즉 원래 문장이 'A B C D'였다면 'D C B A'로 순서를 바꿔 '오른쪽에서 왼쪽으로' 처리하는 것이다.
✔️ seq2seq 심층화와 skip 연결
풀어야 할 문제가 복잡해짐에 따라 RNN 계층(LSTM 계층)을 깊게 쌓아 더 높은 표현력을 요구할 수 있다.
층을 깊게 할 때 사용되는 기법 중 하나로 skip 연결(skip connection, 잔차 연결(residual connection), 숏컷(short-cut))이 있다.
그림 8-34와 같이 skip 연결은 '계층을 건너뛰는 연결'로, 2개의 출력이 '더해'진다.
덧셈은 역전파 시 기울기를 '그대로 흘려'보내므로 skip 연결의 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐른다.
따라서 층이 깊어져도 기울기가 소실(혹은 폭발)되지 않고 전파된다.
8.5. 어텐션 응용
✔️ 구글 신경망 기계 번역(GNMT)
✔️ 트랜스포머
RNN은 이전 시각에 계산한 결과를 이용해 순서대로 계산한다.
따라서 RNN의 계산을 시간 방향으로 병렬 계산하기는 불가능하다.
이를 해결하기 위한 모델 중 하나가 트랜스포머(Transformer) 모델이다.
트랜스포머는 어텐션으로 구성되는데, 그 중 셀프어텐션(Self-Attention)이라는 기술을 이용한다.
하나의 시계열 데이터를 대상으로 한 어텐션으로, '하나의 시계열 데이터 내에서' 각 원소가 다른 원소들과 어떻게 관련되는지를 살펴본다.
지금까지 우리는 어텐션에서 '번역'과 같이 2개의 시계열 데이터 사이의 대응관계를 구하였다.
그림 8-37의 왼쪽처럼 서로 다른 두 시계열 데이터가 입력되지만 Self-Attention은 그림 8-37의 오른쪽처럼 두 입력선이 모두 하나의 시계열 데이터로부터 온다.
이렇게 하면 하나의 시계열 데이터 내에서의 원소 간 대응 관계가 구해진다.
트랜스포머는 RNN 대신 어텐션을 사용하며, Encoder, Decoder 모두 셀프어텐션을 사용한다.
Feed Forward 계층은 피드포워드 신경망(시간 방향으로 독립적으로 처리하는 신경망)을 나타낸다.
은닉층이 1개, 활성화 함수로 ReLU를 이용한 완전연결계층 신경망을 이용한다.
Nx는 회색의 계층들을 N겹 쌓았다는 의미이다.
✔️ 뉴럴 튜링 머신(NTM)
인간은 복잡한 문제를 풀 때 '종이'와 '펜'이라는 외부의 '저장 장치'로 능력을 확장한다.
신경망에도 이와 같이 '외부 메모리'를 이용하여 새로운 힘을 부여할 수 있다.
어텐션을 갖춘 seq2seq에서는 Encoder가 필요한 정보를 메모리에 쓰고, Decoder는 그 메모리로부터 필요한 정보를 읽어들인다.
그렇다면 RNN의 외부에 정보 저장용 메모리 기능을 배치하고, 어텐션을 이용하여 그 메모리로부터 필요한 정보를 읽거나 쓸 수 있다.
이러한 방법이 뉴럴 튜링 머신(Neural Turing Machine, NTM)이다.
먼저, LSTM 계층이 '컨트롤러'가 되어 NTM의 주된 처리를 수행한다.
그리고 각 시각에서 LSTM 계층의 은닉 상태를 Write Head 계층이 받아서 필요한 정보를 메모리에 쓴다.
그 후 Read Head 계층이 메모리로부터 중요한 정보를 읽어 다음 시각의 LSTM 계층으로 전달한다.
여기서, Write Head 계층과 Read Head 계층 또한 어텐션을 사용한다.
NTM은 컴퓨터의 메모리 조작을 모방하기 위해 '콘텐츠 기반 어텐션', '위치 기반 어텐션'을 사용한다.
콘텐츠 기반 어텐션은 입력으로 입력으로 주어진 벡터(질의 query 벡터)와 비슷한 벡터를 메모리로부터 찾아내는 용도,
위치 기반 어텐션은 이전 시각에서 주목한 메모리의 위치(메모리의 각 위치에 대한 가중치)를 기준으로 그 전후로 이동하는 용도로 사용한다.
8.6. 정리
- 번역이나 음성 인식 등 한 시계열 데이터를 다른 시계열 데이터로 변환하는 작업에서는 시계열 데이터 사이의 대응 관계가 존재하는 경우가 많다.
- 어텐션은 두 시계열 데이터 사이의 대응 관계를 데이터로부터 학습한다.
- 어텐션에서는 (하나의 방법으로서) 벡터의 내적을 사용해 벡터 사이의 유사도를 구하고, 그 유사도를 이용한 가중합 벡터가 어텐션의 출력이 된다.
- 어텐션에서 사용하는 연산은 미분 가능하기 때문에 오차역전파법으로 학습할 수 있다.
- 어텐션이 산출하는 가중치(확률)를 시각화하면 입출력의 대응 관계를 볼 수 있다.
- 외부 메모리를 활용한 신경망 확장 연구 예에서는 메모리를 읽고 쓰는 데 어텐션을 사용했다.
'Deep Learning' 카테고리의 다른 글
YOLOv10 Multi-GPU DDP 사용하기 (0) | 2024.09.21 |
---|---|
YOLOv7 사용 및 학습해보기 (1) | 2024.09.12 |
[밑바닥부터시작하는딥러닝2] Chapter 7. RNN을 사용한 문장 생성 (0) | 2024.04.22 |
[밑바닥부터시작하는딥러닝2] Chapter 6. 게이트가 추가된 RNN (0) | 2024.04.21 |
[밑바닥부터시작하는딥러닝2] Chapter 5. 순환 신경망(RNN) (0) | 2024.04.20 |