자연어처리(AI학습 60)

이유진·2024년 7월 8일

--41.Attention.ipynb--

Attention

  • 신경망들의 성능을 높이기 위한 모듈
  • transformer 의 중요 모듈

Seq2Seq 의 문제점

  • 하나의 고정길이 벡터에 모든 정보를 압축 -> 정보 손실 발생
  • RNN 의 문제점. 기울기 소실 문제가 발생
    • 기계번역 분야에서 입력문장이 길어지면 번역품질이 떨어지는 현상
  • 위 문제점을 개선하기 위해 Attention Mechainsm 탄생!
    • 입력시퀀스가 길어지면 출력시퀀스의 정확도가 떨어지는 것을 보정

Attention 의 아이디어!

  • 디코더가 출력단어를 예측하는 매 timestep 마다 인코더의 전체 입력문장을 다시 한번 참조한다
  • 이때, 전체 입력문장을 단순히(동일비율로) 참조 하는것이 아니라, 예측할 단어와 연관이 있는 단어를 집중(attention)해서 참조한다.

Attention Function

어텐션 함수

  • 어텐션을 함수로 표현하면

    • Attention(Q, K, V) = Attention Value
    • Q: Query
    • K: Key
    • V: Value
  • 어텐션 함수는 주어진 '쿼리(Query)'에 대해서 모든 '키(Key)'와의 유사도를 각각 구합니다.

  • 그리고 구해낸 이 유사도를 키와 맵핑되어있는 각각의 '값(Value)'에 반영해줍니다.

  • 그리고 유사도가 반영된 '값(Value)'을 모두 더해서 리턴합니다.

    • 이를 (편의상) 어텐션 값(Attention Value)이라고 하겠습니다.
  • 지금부터 배우게 되는 Seq2Seq + 어텐션 모델에서 Q, K, V에 해당되는 각각의 Query, Keys, Values는 각각 다음과 같습니다.

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

Attention Mechanism 과정

  • dot-porduct attention 을 통해 배워봅시다.
  • Attention Mechanism 과정 (단계)
    1. attention score 계산
    2. 소프트맥스 함수를 통한 attention distribution 계산
    3. 각 인코더의 어텐션 가중치와 은닉 상태를 가중합하여 어텐션 값 계산
    4. 어텐션 값과 디코더의 t 시점의 은닉 상태를 연결
    5. 출력층 연산의 입력이 되는 s~t\tilde{s}_t 계산
    6. s~t\tilde{s}_t 를 출력층의 입력으로 사용합니다

[전체적인 개요(예시)]

  • 디코더의 '세번째 LSTM 셀'에서 출력 단어를 '예측'할 때,
    어텐션 메커니즘을 사용하는 모습을 보여줍니다.

  • 디코더의 첫번째, 두번째 LSTM 셀은 이미 어텐션 메커니즘을 통해 je와 suis를 예측하는 과정을 거쳤다고 가정합니다.

  • 디코더의 세번째 LSTM 셀은 출력 단어를 예측하기 위해서 인코더의 모든 입력 단어들의 정보를 다시 한번 참고하고자 합니다.

  • 중간 과정에 대한 설명은 현재는 생략, 주목할 것은 인코더의 소프트맥스 함수 !!.

  • 소프트맥스 함수를 통해 나온 결과값은 I, am, a, student 단어 각각이 '출력 단어를 예측'할 때 '얼마나 도움'이 되는지의 정도를 수치화한 값.

  • 빨간 직사각형의 크기로 소프트맥스 함수의 결과값의 크기를 표현. 직사각형의 크기가 클 수록 도움이 되는 정도의 크기가 큽니다.

  • 각 입력 단어가 디코더의 예측에 도움이 되는 정도가 수치화하여 측정되면 이를 '하나의 정보'로 담아서 디코더로 전송됩니다. 그림에서는 초록색 삼각형이 이에 해당됩니다.

  • 결과적으로, 디코더는 출력 단어를 더 정확하게 예측할 확률이 높아진다.

1) Attention Score 계산

  • 인코더의 각 시점의 은닉 상태는 hnh_n 라고 합시다

  • 디코더의 현재 시점 (timestep) t의 디코더 은닉 상태를 sts_t 라고 합시다

  • 이전 장에서 배웠던 내용!

    • 현재 시점(timestep) t에서 필요한 입력값을 다시 상기해보면.
    • 시점 t에서 출력 단어를 예측하기 위해서 디코더의 셀은 두 개의 입력값을 필요로 했다
      • 이전 시점 t-1의 은닉 상태 와
      • 이전 시점 t-1에 나온 출력 단어.
  • 그런데 어텐션 메커니즘에서는 출력 단어 예측에 또 다른 값을 필요하다.

    • 바로 어텐션 값(Attention Value)이라는 새로운 값입니다.
    • t번째 타임스텝의 단어를 예측하기 위한 어텐션 값을 ata_t
      이라고 정의하자.
  • 어텐션 값 이 현재 시전 t 에서의 출력 예측에 구체적으로 어떻게 반영되는지는 나중에 설명하고.

  • 지금부터 배우는 모든 과정은 ata_t 를 구하기 위한 여정입니다. 그리고 그 여정의 첫 걸음은 바로 어텐션 스코어(Attention Score)를 구하는 일입니다.

  • 어텐션 스코어란 현재 디코더의 타임스텝 t에서 단어를 예측하기 위해, 인코더의 모든 은닉 상태 각각이 디코더의 현 시점의 은닉 상태 sts_t 와 얼마나 유사한지를 판단하는 스코어값입니다

  • 여기서는 dot-product attention 에선 이렇게 계산한다

  • 'sts_t 를 전치(transpose)' 하고(결과 stTs_t^T) '각 은닉상태 hnh_n'와 내적(dot product)를 수행합니다. 즉 모든 어텐션 스코어 값은 '스칼라' 입니다.
  • 예를 들어 sts_t 와 인코더의 ii번째 은닉상태와 어텐션 스코어의 계산 방법은 아래와 같다.
  • 이 '어텐션 스코어 함수'를 정의해보면 다음과 같다

    score(st,hi)=stThiscore(s_t, h_i) = s_t^Th_i
  • sts_t와 인코더의 모든 은닉 상태의 어텐션 스코어의 모음값을 ete^t 라고 정의하면 → attention scores = et=[stTh1,stTh2,...,stThn]e^t = [s_t^Th_1, s_t^Th_2, ... , s_t^Th_n]

2) 소프트 맥스 함수를 통한 attention distribution 계산

  • et=[stTh1,stTh2,...,stThne^t = [s_t^Th_1, s_t^Th_2, ... , s_t^Th_n] 에 softmax를 적용해 '각각의 확률값'들을 계산
  • 이 값들은 합하면 1이 되는 확률분포 다.
  • 이를 어텐션 분포(Attention Distribution) 라고 하고
  • 각각의 값은 어텐션 가중치(Attention Weight) 라고 한다
  • 예를 들어 소프트맥스 함수를 적용하여 얻은 출력값인 I, am, a, student의 어텐션 가중치를 각각 0.1, 0.4, 0.1, 0.4라고 합시다. 이들의 합은 1입니다. 위의 그림은 각 인코더의 은닉 상태에서의 어텐션 가중치의 크기를 직사각형의 크기를 통해 시각화하였습니다. 즉, 어텐션 가중치가 클수록 직사각형이 큽니다.
  • 내적이 크다면 확률값이 높아질 것이고, 작다면 확률값이 낮아짐
  • 결국 확률값을 구하는 것은 예측할 단어와 연관이 있는 단어를 찾는 것
  • 디코더의 시점 t 의 가중치 모음값인 어텐션 분포를 αtα^t이라고 하면, 다음과 같은 식이 성립함
    $$ α^t = softmax(e^t) $$

3) 각 인코더의 어텐션 가중치와 은닉항태를 가중합 하여 '어텐션 값' 계산

  • 이제 지금까지 준비해온 정보들을 하나로 합치는 단계다.

  • 어텐션의 최종 결과값을 얻기 위해서 각 인코더의 은닉 상태와 어텐션 가중치들을 곱하고, 최종적으로 모두 더합니다.

    • 즉! 가중합(Weighted Sum) 을 진행합니다
  • 어텐션의 최종 결과. 즉, 어텐션 함수의 출력값인 어텐션 값(Attention Value) ata_t
    에 대한 식을 보여줍니다.

    at=i=1Naithia_t = \sum^N_{i=1}{a^t_ih_i}

  • 이러한 어텐션 값 ata_t은 종종 인코더의 문맥을 포함하고 있다고하여, 컨텍스트 벡터(context vector)라고도 불립니다. 앞서 배운 가장 기본적인 seq2seq에서는 인코더의 마지막 은닉 상태를 컨텍스트 벡터라고 부르는 것과 대조됩니다.

4) 어텐션 값과 디코더의 t 시점의 은닉상태들 연결 (concatenate)

  • 어텐션 함수의 최종값인 어텐션 값 ata_t를 구했다면
  • ata_tsts_t와 연결(concatenate)하여 '예측' 연산에 사용할 하나의 벡터 vtv_t를 만들어 낸다.
  • vtv_t는 기존과는 다르게 '인코더의 정보'를 가지고 있고
  • vtv_ty^\hat y 예측연산의 입력으로 사용하게되므로 좀 더 좋은 성능의 예측을 수행할 수 있게 된다.
  • 이것이 어텐션 메커니즘의 핵심이다!

5) 출력층 연산의 입력이 되는 s~t\tilde{s}_t (Latex 레이텍 문법)

  • 다음은 논문에서의 내용입니다

  • vtv_t 를 바로 출력층으로 보내기 전에 신경망 연산을 한 번 더 추가하였습니다.

  • 가중치 행렬과 곱한 후에 하이퍼볼릭탄젠트(tanhtanh) 함수를 지나도록 하여 출력층 연산을 위한 새로운 벡터인 s~t\tilde{s}_t
    를 얻습니다.

  • 어텐션 메커니즘을 사용하지 않는 seq2seq에서는 출력층의 입력이 t시점의 은닉 상태인 sts_t
    였던 반면, 어텐션 메커니즘에서는 출력층의 입력이 s~t\tilde{s}_t 가 되는 셈입니다.

  • 이를 식으로 표현하면 다음과 같습니다.

    • s~t=tanh(Wc[atst]+bc)\tilde{s}_t = tanh(W_c[a_t \cdotp s_t] + b_c)
      • WcW_c는 학습 가능한 가중치 행렬,
      • bcb_c는 bias(편향, 절편) (위 그림에선 편향은 생략)

6) s~t\tilde{s}_t 를 출력층의 입력으로 사용

s~t\tilde{s}_t 를 출력층의 입력으로 사용하여 예측 벡터를 얻습니다.

y^t=Softmax(Wys~t+by)\hat{y}_t = Softmax(W_y\tilde{s}_t + b_y)

Attention Mechanism 종류

  • attention mechanism에는 어텐션 스코어 계산 방식의 차이에 따라 다양한 종류가 존재
이 름스 코 어                                                                
dotscore(st,hi)=stThiscore(s_t, h_i) = s_t^Th_i
scaled dotscore(st,hi)=stThinscore(s_t, h_i) = \frac{s_t^Th_i}{\sqrt n}
generalscore(st,hi)=stTWahiscore(s_t, h_i) = s_t^TW_ah_i
concatscore(st,hi)=WaTtanh(Wb[st;hi])score(s_t, h_i) = W_a^Ttanh(W_b[s_t;h_i])
location - baseat=softmax(Wast)a_t = softmax(W_as_t)
  • sts_t : querys(t 시점에서의 디코더 셀의 은닉 상태)
  • hih_i : keys(모든 시점의 인코더 셀 은닉 상태)
  • Wa,WbW_a, W_b : 학습 가능한 가중치 행렬

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

import tensorflow as tf
from tensorflow import keras

tf.keras.utils.set_random_seed(42)
tf.config.experimental.enable_op_determinism()

base_path = r'/content/drive/MyDrive/dataset'

데이터 가져오기

import requests

request header 에 '브라우저' 인척(?) 해야 한다.

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

def download_zip(url, output_path):
response = requests.get(url, headers=headers, stream=True)
if response.status_code == 200:
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"ZIP file downloaded to {output_path}")
else:
print(f"Failed to download. HTTP Response Code: {response.status_code}")

url = "http://www.manythings.org/anki/fra-eng.zip"
output_path = "fra-eng.zip"

download_zip(url, output_path)

import zipfile

압축파일 풀기

path = os.getcwd()
zipfilename = os.path.join(path, output_path)

with zipfile.ZipFile(zipfilename, 'r') as zip_ref:
zip_ref.extractall(path)

lines = pd.read_csv('fra.txt', names=['src', 'tar', 'lic'], sep='\t')
del lines['lic'] # 불필요한 컬럼은 제거
lines = lines.loc[:, 'src':'tar']
lines = lines[0:60000]
lines.tar = lines.tar.apply(lambda x : '\t '+ x + ' \n') # 와 대신 \t를 시작 심볼, \n을 종료 심볼로 간주하여 추가

문자 집합(사전) 구축

src_vocab = set()
for line in lines.src: # 1줄씩 읽음
for char in line: # 1개의 문자씩 읽음
src_vocab.add(char)

tar_vocab = set()
for line in lines.tar:
for char in line:
tar_vocab.add(char)

사전 크기

src_vocab_size = len(src_vocab) + 1
tar_vocab_size = len(tar_vocab) + 1

사전 정렬

src_vocab = sorted(list(src_vocab))
tar_vocab = sorted(list(tar_vocab))

인덱스 구성을 위해..

src_to_index = dict([(word, i+1) for i, word in enumerate(src_vocab)])
tar_to_index = dict([(word, i+1) for i, word in enumerate(tar_vocab)])

인코더 입력데이터 구성 (정수 인코딩)

encoder_input = []
for line in lines.src: # 입력데이터(src) 문장라인
encoder_input.append([src_to_index[char] for char in line]) # 라인에서 글자를 인덱스로 변환하여 넣어줌

target 문장의 정수 인코딩

decoder_input = []
for line in lines.tar:
decoder_input.append([tar_to_index[char] for char in line]) # 목표 데이터에 해당하는 사전 사죵

target 문장 레이블의 정수 인코딩

decoder_target = []
for line in lines.tar:
timestep = 0
encoded_line = []
for char in line:
if timestep > 0:
encoded_line.append(tar_to_index[char])
timestep = timestep + 1
decoder_target.append(encoded_line)

from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical

max_src_len = max([len(line) for line in lines.src])
max_tar_len = max([len(line) for line in lines.tar])

★ 한번만 실행하기!

encoder_input = pad_sequences(encoder_input, maxlen=max_src_len, padding='post')
decoder_input = pad_sequences(decoder_input, maxlen=max_tar_len, padding='post')
decoder_target = pad_sequences(decoder_target, maxlen=max_tar_len, padding='post')

encoder_input = to_categorical(encoder_input)
decoder_input = to_categorical(decoder_input)
decoder_target = to_categorical(decoder_target)


Attention Mechanism 모델

from tensorflow.keras.layers import Input, LSTM, Embedding, Dense
from tensorflow.keras.models import Model

인코더(Encoder)

encoder_inputs = Input(shape=(None, src_vocab_size)) # 글자 사전의 크기만큼 입력된다
encoder_lstm = LSTM(256, return_state=True) # True로 해줘야 h, c 리턴한다.
encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs)
encoder_states = [state_h, state_c]

디코더(Decoder)

  • 디코더에서는 seq2seq 와 다르게 Attention layer 추가
  • S_ 는 은닉상태와 디코더의 최종 출력을 연결한 결과. 연결할 때 shape를 맞추기 위해 axis를 추가
  • Attention layer 는 디코더의 은닉상태와 인코더 은닉상태 전체를 받아 컨텍스트 벡터를 생성해냄

from keras.layers import Attention

decoderinputs = Input(shape=(None, tar_vocab_size)) # target 사전크기 만큼의 one-hot 벡터가 입력
decoder_lstm = LSTM(256, return_sequences=True, return_state=True)
decoder_outputs,
, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)

Attention

S_ 는 은닉상태와 디코더의 최종 출력을 연결한 결과. 연결할 때 shape를 맞추기 위해 axis를 추가

S_ = tf.concat([state_h[:, tf.newaxis, :], decoder_outputs[:, :-1, :]], axis=1)

attention = Attention()
contextvector = attention([S, encoder_outputs]) # 컨텍스트 벡터 생성!

decoder 출력과 attention 출력 합하기

concat = tf.concat([decoder_outputs, context_vector], axis=-1)
decoder_softmax_layer = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_softmax_layer(concat)

모델 구성, 학습

model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

model.fit([encoder_input, decoder_input], decoder_target,

epoch=25, batch_size=64, validation_split=0.2)

모델 저장

model.save_weights(os.path.join(base_path, 'out', 'eng2fra_attention.keras'))

학습한 모델 불러오기

model.load_weights(os.path.join(base_path, 'eng2fra_attention.keras'))

예측

1. 인코더 정의

★ encoder와 decoder를 분리해주었기 때문에 디코더에서 인코더의 은닉상태와 최종 은닉상태를 따로 입력받아야함.

encoder_model = Model(encoder_inputs,
[encoder_outputs, encoder_states])

2. 디코더 설계

이전 시점의 상태들을 저장하는 텐서

decoder_state_input_h = Input(shape=(256,))
decoder_state_input_c = Input(shape=(256,))

★추가★

encoder_state_h = Input(shape=(256,))
encoder_outputs = Input(shape=(256,))

decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]

축을 추가해서 합쳐주기

S = tf.concat([encoder_state_h[:, tf.newaxis, :], decoder_outputs[:, :-1, :]], axis=1 )
context_vector = attention([S
, encoder_outputs])
decoder_concat = tf.concat([decoder_outputs, context_vector], axis=-1)

decoder_outputs = decoder_softmax_layer(decoder_concat)

decoder_model = Model(inputs=[decoder_inputs, encoder_state_h, encoder_outputs] + decoder_states_inputs,
outputs=[decoder_outputs] + decoder_states)

인덱스로부터 단어를 얻을 수 있는 dict 준비

index_to_src = dict((i, char) for char, i in src_to_index.items())
index_to_tar = dict((i, char) for char, i in tar_to_index.items())

기존 seq2seq 코드

def decode_sequence(input_seq):

입력으로부터 인코더의 상태를 얻음

outputs_input, states_value = encoder_model.predict(input_seq)

에 해당하는 원-핫 벡터 생성

target_seq = np.zeros((1, 1, tar_vocab_size))
target_seq[0, 0, tar_to_index['\t']] = 1.

stop_condition = False
decoded_sentence = ""

stop_condition이 True가 될 때까지 루프 반복

while not stop_condition:

# 이점 시점의 상태 states_value를 현 시점의 초기 상태로 사용
output_tokens, h, c = decoder_model.predict([target_seq, states_value[0], outputs_input] + states_value)

# 예측 결과를 문자로 변환
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = index_to_tar[sampled_token_index]

# 현재 시점의 예측 문자를 예측 문장에 추가
decoded_sentence += sampled_char

# <eos>에 도달하거나 최대 길이를 넘으면 중단.
if (sampled_char == '\n' or
    len(decoded_sentence) > max_tar_len):
    stop_condition = True

# 현재 시점의 예측 결과를 다음 시점의 입력으로 사용하기 위해 저장
target_seq = np.zeros((1, 1, tar_vocab_size))
target_seq[0, 0, sampled_token_index] = 1.

# 현재 시점의 상태를 다음 시점의 상태로 사용하기 위해 저장
states_value = [h, c]

return decoded_sentence

input_idx = [3, 5, 100, 300, 1001] # 입력문장의 인덱스들

for idx in input_idx:
print(lines.src[idx])

for seq_index in [3, 50, 100, 300, 1001]:
input_seq = encoder_input[seq_index:seq_index+1]
decoded_sentence = decode_sequence(input_seq)

print("-" * 35)
print("입력문장:", lines.src[seq_index])
print("정답문장:", lines.tar[seq_index]2:len(lines.tar[seq_index]) - 1])
print("번역문장:", decoded_sentence[1:len(decoded_sentence) - 1])

profile
독해지자

0개의 댓글