본문 바로가기
HRDI_AI/[인공지능] RNN을 이용한 자연어처리 모델 구현

2. 자연어처리 인공신경망 - 4(Attention)

by Toddler_AD 2025. 12. 5.

2-4. Attention

  • Seq2Seq의 문제점
    • 인코더가 입력 시퀀스를 하나의 벡터로 압축하는 과정에서 입력 시퀀스의 일부 정보 손실
      • RNN 구조의 근본적인 문제점
      • 경사 소실(Vanishing Gradient)이 발생할 가능성이 있음
      • 입력 데이터의 길이가 길어지면 성능이 저하되는 현상이 발생할 가능성이 있음

  • Attention
    • 어텐션 함수는 Q(Query), K(Keys), V(Values)를 매개변수로 사용
      • Q(Query)는 특정 시점에서의 디코더 셀의 은닉 상태
      • K(Key)는 모든 시점에서의 인코더 셀의 Q를 반영하기 전 은닉 상태
      • V(Value)는 모든 시점에서의 인코더 셀의 Q 반영 후 은닉 상태

  • 어텐션 함수는 주어진 질의(Query)에 대해서 모든 키(Key)와 각각의 유사도를 계산
  • 계산된 유사도를 키와 매핑되어 있는 각각의 값(Value)에 반영
  • 유사도가 반영된 값(Value)을 모두 어텐션 값(Attention Value)으로 반환

  • Attention 적용 과정

  • 실습 - Attention을 이용한 영-한 번역기

# 순서
* 모델 구축
* 입력값 토큰화, 임베딩
* 훈련을 정의하고 학습
* 평가
* 예측하기 위한 데이터를 토큰화, 임베딩후 predict
* 라벨 출력
import tensorflow as tf
from tensorflow.keras import Model, layers

# 인코더 입력 정의: 영문 단어가 입력, 각 단어는 4자 길이, 모든 문자의 수는 171개
enc_input = layers.Input(shape=(4, 171))

# 인코더 LSTM 정의: 모든 타임스텝의 출력을 반환하도록 설정
enc_output, state_h, state_c = layers.LSTM(128, return_sequences=True, return_state=True)(enc_input)

# 디코더 입력 정의
dec_input = layers.Input(shape=(3, 171))  # 한글 단어는 2자 길이 + <Start> 토큰, 모든 문자의 수는 171개

# 디코더 LSTM 정의: 모든 시퀀스의 출력을 반환하도록 설정
dec_lstm_output, _, _ = layers.LSTM(128, return_sequences=True, return_state=True)(dec_input, initial_state=[state_h, state_c])

# 어텐션 메커니즘 정의
context_vector = layers.Attention()([dec_lstm_output, enc_output])

# 컨텍스트 벡터와 디코더 LSTM 출력을 결합
context_and_lstm_output = layers.Concatenate()([context_vector, dec_lstm_output])

# 디코더 출력층 정의: 출력 크기는 모든 문자의 수인 171, softmax 활성화 함수를 사용
output = layers.Dense(171, activation='softmax')(context_and_lstm_output)

# 모델 정의: 인코더 입력(enc_input)과 디코더 입력(dec_input)을 모델의 입력으로, 디코더 출력을 모델의 출력으로 설정
model = Model(inputs=[enc_input, dec_input], outputs=[output])

# 모델 요약 출력
model.summary()
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ input_layer_2       │ (None, 4, 171)    │          0 │ -                 │
│ (InputLayer)        │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ input_layer_3       │ (None, 3, 171)    │          0 │ -                 │
│ (InputLayer)        │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ lstm_2 (LSTM)       │ [(None, 4, 128),  │    153,600 │ input_layer_2[0]… │
│                     │ (None, 128),      │            │                   │
│                     │ (None, 128)]      │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ lstm_3 (LSTM)       │ [(None, 3, 128),  │    153,600 │ input_layer_3[0]… │
│                     │ (None, 128),      │            │ lstm_2[0][1],     │
│                     │ (None, 128)]      │            │ lstm_2[0][2]      │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ attention_1         │ (None, 3, 128)    │          0 │ lstm_3[0][0],     │
│ (Attention)         │                   │            │ lstm_2[0][0]      │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ concatenate_1       │ (None, 3, 256)    │          0 │ attention_1[0][0… │
│ (Concatenate)       │                   │            │ lstm_3[0][0]      │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ dense_1 (Dense)     │ (None, 3, 171)    │     43,947 │ concatenate_1[0]… │
└─────────────────────┴───────────────────┴────────────┴───────────────────┘
 Total params: 351,147 (1.34 MB)
 Trainable params: 351,147 (1.34 MB)
 Non-trainable params: 0 (0.00 B)
model.compile(loss='sparse_categorical_crossentropy',
              optimizer='adam') # metrics가 없으면 훈련시 loss만 출력, predict()를 사용 못함
import pandas as pd
import numpy as np
# <START>, <END>, <PAD>
arr1 = [c for c in 'SEPabcdefghijklmnopqrstuvwxyz']
arr2 = pd.read_csv('./data/korean.csv', header=None)
# print(arr2[0].values.tolist())
num_to_char = arr1 + arr2[0].values.tolist()
print(len(num_to_char))
171
char_to_num = {char:i for i, char in enumerate(num_to_char)}
print(char_to_num)
{'S': 0, 'E': 1, 'P': 2, 'a': 3, 'b': 4, 'c': 5, 'd': 6, 'e': 7, 'f': 8, 'g': 9, 'h': 10, 
'i': 11, 'j': 12, 'k': 13, 'l': 14, 'm': 15, 'n': 16, 'o': 17, 'p': 18, 'q': 19, 'r': 20, 
's': 21, 't': 22, 'u': 23, 'v': 24, 'w': 25, 'x': 26, 'y': 27, 'z': 28, '가': 29, '각': 30, 
'간': 31, '감': 32, '개': 33, '거': 34, '것': 35, '게': 36, '계': 37, '고': 38, '관': 39, 
'광': 40, '구': 41, '굴': 42, '규': 43, '그': 44, '금': 45, '기': 46, '깊': 47, '나': 48, 
'날': 49, '남': 50, '내': 51, '넓': 52, '녀': 53, '노': 54, '놀': 55, '농': 56, '높': 57, 
'뉴': 58, '늦': 59, '다': 60, '단': 61, '도': 62, '동': 63, '들': 64, '람': 65, '랑': 66, 
'래': 67, '램': 68, '류': 69, '름': 70, '릎': 71, '리': 72, '많': 73, '망': 74, '매': 75, 
'머': 76, '먼': 77, '멍': 78, '메': 79, '명': 80, '모': 81, '목': 82, '무': 83, '물': 84, 
'미': 85, '바': 86, '반': 87, '방': 88, '번': 89, '복': 90, '부': 91, '분': 92, '붕': 93, 
'비': 94, '뿌': 95, '사': 96, '상': 97, '색': 98, '생': 99, '서': 100, '선': 101, '소': 102, 
'손': 103, '수': 104, '쉽': 105, '스': 106, '시': 107, '식': 108, '실': 109, '싸': 110,
'아': 111, '약': 112, '얇': 113, '어': 114, '언': 115, '얼': 116, '여': 117, '연': 118, 
'오': 119, '옥': 120, '왼': 121, '요': 122, '용': 123, '우': 124, '운': 125, '움': 126, 
'위': 127, '유': 128, '은': 129, '을': 130, '음': 131, '의': 132, '이': 133, '익': 134,
'인': 135, '읽': 136, '입': 137, '자': 138, '작': 139, '장': 140, '적': 141, '제': 142, 
'좋': 143, '주': 144, '지': 145, '짜': 146, '쪽': 147, '찾': 148, '책': 149, '출': 150, 
'칙': 151, '크': 152, '키': 153, '탈': 154, '택': 155, '통': 156, '파': 157, '팔': 158, 
'편': 159, '피': 160, '핑': 161, '한': 162, '합': 163, '해': 164, '행': 165, '험': 166, 
'회': 167, '획': 168, '휴': 169, '흐': 170}
raw = pd.read_csv('./data/translate.csv', header=None)
eng_kor = raw.values.tolist()
print(len(eng_kor))
110
temp_eng = 'love'
temp_eng_n = [char_to_num[c] for c in temp_eng]
print(temp_eng_n)
temp_kor = '사랑'
np.eye(171)[temp_eng_n].shape
[14, 17, 24, 7]
(4, 171)
def encode(eng_kor):
    enc_in = []
    dec_in = []
    rnn_out = [] # decoder output
    for seq in eng_kor:
        eng = [char_to_num[c] for c in seq[0]]
        enc_in.append(np.eye(171)[eng])

        kor = [char_to_num[c] for c in ('S'+seq[1])]
        dec_in.append(np.eye(171)[kor])

        target = [char_to_num[c] for c in (seq[1] + 'E')]
        rnn_out.append(target)

    enc_in = np.array(enc_in)
    dec_in = np.array(dec_in)
    rnn_out = np.array(rnn_out)
    rnn_out = np.expand_dims(rnn_out, axis=2)
    return enc_in, dec_in, rnn_out
sample = [['word', '단어']]
encode(sample)[2]
array([[[ 61],
        [114],
        [  1]]])
X_enc, X_dec, y_rnn = encode(eng_kor)
%%time
model.fit([X_enc, X_dec], y_rnn, epochs=500)
Epoch 1/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step - loss: 5.1289
Epoch 2/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 5.0848 
Epoch 3/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 5.0284 
Epoch 4/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 4.9383 
Epoch 5/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - loss: 4.7703 
Epoch 6/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 4.4225 
Epoch 7/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 3.7855 
Epoch 8/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 3.5323 
Epoch 9/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - loss: 3.5805 
Epoch 10/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 3.3992 
Epoch 11/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 3.3742 
Epoch 12/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 3.3587 
Epoch 13/500
...
Epoch 500/500
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.0028 
CPU times: total: 1min 7s
Wall time: 24.1 s
enc_in, dec_in, _ = encode([['help', 'PP']])
# print(enc_in)
pred = model.predict([enc_in, dec_in])
# print(pred.shape)
word = np.argmax(pred[0], axis=-1)
# print(word)
num_to_char[word[0]], num_to_char[word[1]]
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 136ms/step
('도', '움')
from numpy.random import randint
pick = randint(0, len(eng_kor), 5)
choose = [ [eng_kor[i][0], 'PP'] for i in pick]
print(choose)
[['feet', 'PP'], ['very', 'PP'], ['knee', 'PP'], ['size', 'PP'], ['face', 'PP']]
enc_in, dec_in, _ = encode(choose)
pred = model.predict([enc_in, dec_in])
print(pred.shape)
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 132ms/step
(5, 3, 171)
for i in range(len(choose)):
    eng = choose[i][0]
    word = np.argmax(pred[i], axis=-1)
    kor = ''
    for j in range(2):
        kor = kor + num_to_char[word[j]]
    print(eng, kor)
feet 다리
very 매우
knee 무릎
size 크기
face 얼굴