본문 바로가기

딥러닝/딥러닝을 이용한 자연어처리 입문

[딥러닝 NLP] 15. Attention(Dot-Product Attention, Bahdanau Attention)

seq2seq 모델은 고정된 크기의 벡터로 압축하는 과정에서 정보가 손실되고, RNN의 고질적 문제인 기울기 소실 문제가 존재한다. 이로 인해 입력 시퀀스가 길어질수록 출력 시퀀스의 정확도가 떨어진다. 이를 보정하기 위한 attention 기법을 알아보자.

 

# 15-01 Attention Mechanism

1. Attention이란?

디코더에서 출력 단어를 예측하는 매 시점마다 인코더의 전체 입력 문장을 다시 참고하는 것.

이 때, 전체 입력 문장 중에서도 해당 시점에서 예측할 단어와 관련된 입력 단어에 더 집중(attention)한다.

여러 종류의 Attention이 있지만, Attention Score를 구하는 방법만 다르다.

다양한 종류의 Attention

'dot' attention은 루옹 어텐션이라고도 하며, 'concat' attention은 바다나우 어텐션이라고도 한다.

 

2. Attention Function

Attention(Q, K, V) = Attention Value

어텐션 함수는 주어진 Q(Query)에 대한 모든 K(Key)와의 유사도를 구한다.

각 유사도는 K와 매핑되어있는 각 V(Value)에 반영한다.

유사도가 반영된 V들을 모두 더해 리턴한 것이 바로 어텐션 값(Attention Value)

Q = Query : t 시점의 디코더 셀에서의 은닉 상태
K = Keys : 모든 시점의 인코더 셀의 은닉 상태들
V = Values : 모든 시점의 인코더 셀의 은닉 상태들

 

3. Dot-Product Attention(루옹 어텐션)

다양한 Attention 종류 중에서 가장 이해하기 쉬운 Dot-Product Attention을 먼저 공부한다.

Dot-Product Attention 메커니즘

위 그림에서, 디코더(초록색)의 각 LSTM 셀은 출력단어를 예측하기 위해 인코더의 모든 입력단어 정보를 다시 참고한다.

참고할 입력단어들은 소프트맥스 함수를 거친 각 단어의 결과값들의 묶음(초록 삼각형)이다.

구체적으로 단계를 살펴보자.

 

(1) Attention Score 구하기

Attention Score = t시점에서 디코더의 은닉상태 s가 인코더의 모든 은닉상태 h와 각각 얼마나 유사한지 판단하는 스코어값으로, s를 전치하고 각 h와 내적을 수행한 스칼라값임

i번째 h와의 Attention Score

 

Attention Score들의 묶음을 e라고 한다.

Attention Score들의 묶음

 

(2) Attention Distribution 구하기

Attention Distribution = e에 소프트맥스를 적용해 얻은 확률 분포

Attention Weight = 어텐션 분포 속 각각의 값

어텐션 가중치가 높을수록 직사각형의 크기가 큼

 

Attention Distribution을 알파라고 한다.

Attention Distribution

 

(3) Attention Value(=Context vector) 구하기

Attention Value = 인코더의 각 은닉상태 h와 각 어텐션 가중치값들을 곱하고 모두 더한 가중합

최종 Attention Value는 알파와 h의 가중합

 

최종 Attenton Value를 a라고 한며, context vector라고도 한다.

Attention Value

 

(4) Attention Value와 디코더의 은닉상태 s를 연결

최종 어텐션 값 a와 디코더의 은닉상태 s를 결합(concatenate)하여 하나의 벡터 v로 만든다.

 

(5) 출력층으로 갈 최종 입력값 연산

최종적으로 신경망 연산을 한 번 더 추가한다. v에 가중치행렬을 곱하고 tanh 함수를 지나게 한다.

 

4. Bahdanau Attention(바다나우 어텐션)

Attention(Q, K, V) = Attention Value 에서 Query가 t-1 시점의 디코더의 은닉상태라는 점이 다름. 즉, Attention score를 구할 때 st 대신 st-1 사용

(1) Attention score 구하기

H는 h들의 행렬
1. 주황은 WH, 초록은 Ws
2. 하이퍼볼릭탄젠트
3. 어텐션 스코어 벡터 e

 

그 이후의 작업은 똑같다. 어텐션 분포 -> 어텐션 값 -> 최종 입력값

(2) Attention Distribution 구하기

어텐션 분포 구하기

(3) Attention Value(=Context vector) 구하기

어텐션 값(context vector) 구하기

(4) Attention Value와 디코더의 은닉상태 st-1를 연결  

(5) 출력층으로 갈 최종 입력값 연산

어텐션 값과 t시점의 임베딩벡터를 연결 후, 그것과  t-1시점의 은닉상태 st-1로부터 최종 입력값 st 구함

 

마지막으로 최종 입력값 st는 출력층으로 전달되고, t시점의 예측값 y를 구한다.

 

# 15-02 Attention으로 리뷰 감성분류 실습

텍스트분류에서 어텐션 메커니즘을 사용하는 이유: RNN이 time step을 지나며 손실했던 정보들을 다시 참고하기 위해

 

1. Bahdanau Attention

데이터 전처리는 마쳤다는 가정 하에 코드를 이어간다.

import tensorflow as tf

class BahdanauAttention(tf.keras.Model):
  def __init__(self, units):
    super(BahdanauAttention, self).__init__()
    self.W1 = Dense(units)
    self.W2 = Dense(units)
    self.V = Dense(1)

  def call(self, values, query): # 단, key와 value는 같음
    # query shape == (batch_size, hidden size)
    # hidden_with_time_axis shape == (batch_size, 1, hidden size)
    # score 계산을 위해 뒤에서 할 덧셈을 위해서 차원을 변경해줍니다.
    hidden_with_time_axis = tf.expand_dims(query, 1)

    # score shape == (batch_size, max_length, 1)
    # we get 1 at the last axis because we are applying score to self.V
    # the shape of the tensor before applying self.V is (batch_size, max_length, units)
    score = self.V(tf.nn.tanh(
        self.W1(values) + self.W2(hidden_with_time_axis)))

    # attention_weights shape == (batch_size, max_length, 1)
    attention_weights = tf.nn.softmax(score, axis=1)

    # context_vector shape after sum == (batch_size, hidden_size)
    context_vector = attention_weights * values
    context_vector = tf.reduce_sum(context_vector, axis=1)

    return context_vector, attention_weights

 

2. BiLSTM+Attention

from tensorflow.keras.layers import Dense, Embedding, Bidirectional, LSTM, Concatenate, Dropout
from tensorflow.keras import Input, Model
from tensorflow.keras import optimizers
import os

# 모델 설계
# 입력층 설계
sequence_input = Input(shape=(max_len,), dtype='int32')
# 임베딩층 설계
embedded_sequences = Embedding(vocab_size, 128, input_length=max_len, mask_zero = True)(sequence_input)
# BiLSTM 의 첫번째층 설계
lstm = Bidirectional(LSTM(64, dropout=0.5, return_sequences = True))(embedded_sequences)
# BiLSTM 의 두번째층 설계
lstm, forward_h, forward_c, backward_h, backward_c = Bidirectional \
  (LSTM(64, dropout=0.5, return_sequences=True, return_state=True))(lstm)
# 순방향과 역방향의 은닉상태와 셀상태를 연결
state_h = Concatenate()([forward_h, backward_h]) # 은닉 상태
state_c = Concatenate()([forward_c, backward_c]) # 셀 상태
# 어텐션 값, 어텐션 가중치 계산
attention = BahdanauAttention(64) # 가중치 크기 정의
context_vector, attention_weights = attention(lstm, state_h)
# 출력층 설계
dense1 = Dense(20, activation="relu")(context_vector)
dropout = Dropout(0.5)(dense1)
output = Dense(1, activation="sigmoid")(dropout)
model = Model(inputs=sequence_input, outputs=output)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# 모델 훈련
history = model.fit(X_train, y_train, epochs = 3, batch_size = 256, validation_data=(X_test, y_test), verbose=1)
# 테스트 데이터에 대해 모델 평가
print(model.evaluate(X_test, y_test)[1]) # 0.8793