기본기 다지기/CNN부터 Attention까지 구현

[기본이론] Attention 구현(1/3)

syveany 2024. 9. 26. 20:40

Attention 구현(1/3)

참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter8. 어텐션

~ 목차 ~
0. 서론- 왜 Attention을 구현하고 있는가

  1. Attention 개념
    1.1 seq2seq의 문제점
    1.2 Encoder 개선
    1.3 Decoder 개선1- 가중합(Weight Sum) 계층 (c)
    1.4 Decoder 개선2- 가중치(Attention Weight) 계층 (a)
    1.5 Decoder 개선3- 가중합 계층, 가중치 계층 합치기

0. 서론- 왜 Attention을 구현하고 있는가

  • 이것 역시 연휴맞이 기본기 다지기의 일환이다. 기본기라고 하기엔 너무 기본적인 이론인가 싶지만.. 쌓아가야지 뭐
  • 이번에도 국룰 책 『밑바닥부터 시작하는 딥러닝2』의 Chapter8. 어텐션을 참고했다.
  • 너무 길어져서 3번에 걸쳐서 끊어가기로 했다.

1. Attention 개념

  • Attention: 필요한 정보에만 주목해서 그 정보로부터 시계열 변환을 수행하는 구조
    (이 설명으로만 봐선 크게 와닿지는 않는다..)

1.1 seq2seq의 문제점

  • Encoder의 출력이 입력 문장의 길이에 관계없이 고정길이벡터라는 점
    → 항상 같은 길이의 벡터로 변환되기 때문에 정보 손실이 일어날 수 있음

1.2 Encoder 개선

  • 입력문장의 길이와 Encoder의 출력길이가 비례하도록 하기 위해 Encoder LSTM 계층의 모든 은닉상태벡터를 가져와서 아래 그림처럼 행렬 hs로 출력할 수 있음
  • LSTM이 출력한 각각의 은닉상태벡터들은 직전에 입력한 단어의 영향을 가장 크게 받기 때문에, 각각의 은닉상태벡터들은 각 단어에 해당하는 벡터들의 집합이라고 볼 수 있음

image.png

1.3 Decoder 개선1- 가중합(Weight Sum) 계층 (c)

  • 개선 전: hs의 마지막 줄만 빼서 Decoder에 전달한 뒤 나온 은닉상태를 그대로 Affine 계층으로 출력
  • 개선 후: Affine 계층 전에 '어떤 계산'(입력값: hs전부, 은닉상태)을 수행하는 계층을 추가하고 필요한 정보만 골라서 Affine 계층으로 출력

image.png

  • '어떤 계산'의 역할: 단어의 alignment(대응관계) 추출. 원래는 사람이 alignment를 지정했지만, 그림의' 어떤 계산'을 통해서 alignment를 자동화하고자 함. 즉, Decoder에 입력된 단어와 대응관계인 벡터를 hs에서 선택하기
    ex) 'I'가 입력되었을 때 hs에서 '나'에 대응하는 벡터를 선택
    • but '나'벡터를 선택작업은 미분이 불가능하므로 다른 방법을 사용해야 함
      어떤 방법을 사용할까?
      모든 단어를 선택(hs)한 뒤 가중치(a)와의 원소별 곱을 계산하여 가중합을 구해서, 맥락벡터(c)를 구하는 방법!
      • [그림8-6]을 보면 '나'에 대응하는 가중치가 0.8로, 맥락벡터 c에는 '나'벡터의 성분이 많이 포함되어 있다는 걸 알 수 있음. 즉 '나'벡터를 선택했다고 볼 수 있음

image.png

  • [그림8-11]을 구현하면 다음과 같음(특별할 것 없이 그냥 설명 그대로 구현한거임)
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)
    dar = dt * hs
    dhs = dt * ar
    da = np.sum(dar, axis=2)

    return dhs, da

1.4 Decoder 개선2- 가중치(Attention Weight) 계층 (a)

  • Decoder의 첫 번째 LSTM 계층이 은닉상태벡터를 출력할 때의 처리는 아래 그림과 같음

image.png

  • 목표: LSTM 계층의 은닉상태벡터 hhs의 각 단어벡터와 얼마나 비슷한가, 즉 유사도를 수치로 나타내기
    → 방법: 내적 사용할거임(다른 방법도 사용 가능함)
  • 먼저 hs와 h를 내적해서 s를 얻은 뒤, 이를 softmax함수를 통해 정규화해서 가중치 a를 얻음
  • 과정을 그림으로 나타내면 [그림8-15]가 됨

image.png

  • [그림8-15]을 구현하면 다음과 같음(특별할 것 없이 그냥 설명 그대로 구현한거임)
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

1.5 Decoder 개선3- 가중합 계층, 가중치 계층 합치기

  • 위에서 구현한 Weight Sum 계층과 Attention Weight 계층을 합쳐서 Attention 계층을 구현할 수 있음. 아래 그림은 합쳐진 설계도..

image.png

  • 이를 코드로 구현하면 다음과 같음(그냥 그대로 합치면 됨, 특별한 건 없음)
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)
    # 각 단어의 가중치를 나중에 참조할 수 있도록 attention_weight에 저장
    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이었다!