[딥러닝] 기계 번역 : Seq2Seq와 Attention (+ 모델 학습시켜 다국어 번역해보기)
이번 포스팅에서는 RNN 기반의 자연어 처리(NLP)를 위한 모델 중 번역을 위해 만들어진 Seq2Seq를 알아볼 것이다.
그리고 Seq2Seq의 RNN 기반 모델의 문제점을 완화하기 위해 고안되었으면서, Transformer의 중요한 메커니즘으로 사용되는 Attention에 대해서 살펴보려고 한다.
마지막으로 Seq2Seq+ LSTM / Seq2Seq + Attention으로 번역기를 한번 만들어보자.
자연어 처리에 대한 기본적인 개념들이 사용되므로 아래의 포스팅을 참고해도 좋을 것 같다.
https://sjh9708.tistory.com/227
LSTM의 구조
셀 상태 (Cell State) : 장기 기억
- 추가적인 가중치 연산 없이 정보가 흐를 수 있게 하여 중요한 정보를 장기간 유지할 수 있다.
- 시퀀스의 길이에 관계없이 정보를 계속 전달할 수 있는 경로로서 그래디언트 소실 문제를 완화한다.
- Cell State는 이전 Cell State의 일부를 잊고(Forget Gate)와 새로운 정보를 추가하여(Input Gate)를 통해 업데이트된다.
은닉 상태 (Hidden State) : 단기 기억
- 현재 시점에서 LSTM의 출력으로, 다음 시점의 입력으로도 사용된다.
- 단기적인 정보를 유지하고, Cell State와 결합하여 시퀀스의 각 단계에서 중요한 정보를 출력한다.
- Hidden State는 Output Gate를 통해서 현재 Cell State의 정보를 기반으로 결정된다.
- Forget Gate 이전 Cell memory \(C_{t-1}\)의 영향도를 결정, 즉 얼마만큼 삭제할지 결정
- Input Gate : 입력 𝑥𝑡를 Cell memory 𝐶𝑡−1에 추가할지 말지를 결정하고 추가할 거라면 얼마만큼 추가할지 결정
- Output Gate : 이전 Cell의 Hidden state ℎ𝑡−1, 입력 𝑥𝑡, Cell memory 𝐶𝑡−1을 취합해서 이 Cell의 Hidden state ℎ𝑡를 결정
https://sjh9708.tistory.com/226
전통적인 RNN 기반의 번역
위의 그림은 LSTM을 사용하여 "나는 학교에 갔다"라는 시퀀스를 번역하는 시뮬레이션의 예시이다. 하지만 여기에는 몇 가지 문제점들이 보인다.
1. 다양한 입력과 출력 시퀀스 길이 간의 매핑에 제약이 있다.
- "나는 학교에 간다 -> I go to school"으로 매핑하는 데에 제약이 걸린다. 각각의 입력마다 출력을 뱉어내는 구조이기 때문에 3개의 입력에서 4개의 출력으로 매핑하기 힘들다.
2. 각각의 입력에 대한 출력으로 대응되기 때문에 문맥의 정보를 충분히 반영하지 못할 수 있다.
- 위의 시뮬레이션에서는 "I school go"로 어색한 결과가 매칭되었다.
- 단순 단방향 RNN 모델은 시퀀스를 한 방향으로만 처리하기 때문에 이전 문맥을 고려하더라도 양방향의 문맥 정보를 충분히 반영할 수 없다.
이런 문제점을 해결하기 위해서 발전 형태인 Seq2Seq 모델이 도입되었다.
Seq2Seq (Sequence-to-Sequence)
Seq2Seq(Sequence-to-Sequence) 모델은 주로 기계 번역에서 사용된다.
전통적인 RNN 기반 모델과 달리 가변 길이의 시퀀스를 처리할 수 있으며, 양방향의 문맥 정보를 효과적으로 활용할 수 있어 더 자연스러운 번역이 가능하다.
아래의 대략적인 구조를 살펴보면서 알아보자.
- 인코더(Encoder): 입력 시퀀스를 처리하고 이를 고정된 크기의 Context Vector로 변환한다.
- 컨텍스트 벡터(Context Vector): Encoder가 순차적으로 입력 시퀀스를 처리한 후의 마지막 Hidden State로서, 입력 시퀀스의 단어들의 의미와 순서를 요약한 정보이다. 해당 정보가 디코더가 출력을 내는 데에 참고되어 사용된다. 컨텍스트 벡터에는 입력 시퀀스에 대한 아래와 같은 정보들이 포함되어 디코더가 해석할 때 도움을 준다.
- 의미 정보: 각 단어의 의미와 문장의 전체 의미
- 순서 정보: 단어들이 문장에서 나오는 순서와 관계
- 문법 정보: 문법적 구조와 관계
- 디코더(Decoder): 인코더에서 생성된 Context Vector를 초기 State로 사용하여 출력 시퀀스를 생성한다.
상세 구조
인코더 아키텍처와 디코더 아키텍처의 내부는 사실 RNN 기반 아키텍처(LSTM, GRU)가 사용한다.
"I go to school"이라는 입력 시퀀스를 번역하는 과정을 한번 묘사해 보겠다.
- 인코더: 자연어는 비정형 데이터이기 때문에 입력으로 처리하기 위한 과정을 거쳐야 한다고 이전 NLP 포스팅에서 언급했었다. 따라서 입력 문장은 단어 단위로 토큰화하고, Embedding Vector 형태로 변환하여 인코더에 순차적으로 입력으로 들어오게 된다.
Encoder의 LSTM은 시퀀스의 각 단어를 차례대로 처리하여 Cell State와 Hidden State를 업데이트한다. - 단계의 Cell State에 참고되어 사용된다.
초기 Decoder의 State는 Context Vector로 설정한다.
RNN(LSTM/GRU) 기반 모델의 한계
1. 장기 의존성 문제(Long-term dependency problem)
- LSTM과 GRU의 등장으로 장기 의존성 문제가 많이 완화되었지만 구조적으로 완전히 해결할 수 없다.
- 여전히 긴 문장들에 대해서 Gradient 소실과 폭주가 발생할 수 있다.
2. Seq2Seq
- 입출력의 크기의 제약 완화 : LSTM의 동일한 입출력의 크기의 제약을 해결하였다.
- 고정된 Context Vector에 소스 문장의 정보 압축 : 병목 발생 : 고정된 길이에 입력 시퀀스의 모든 정보를 담지 못하고 손실될 수 있다. 특히 긴 입력 시퀀스의 경우 정보 손실이 더 심각하다. 이는 모델의 성능 하락으로 이어진다.
이러한 한계점으로 인해 Attention 메커니즘과 같은 기술이 도입되었다.
Attention은 디코더가 인코더의 각 입력 단계에 주의를 기울일 수 있도록 하는 것으로, 더 많은 정보를 고려할 수 있도록 돕는다. 이는 2024년 기점으로 NLP에서 뛰어난 성능을 보이는 Transformer의 "Attention is all you need"의 근간이 되었다.
Attention
1. 고정된 Context Vector를 사용하는 대신 Decoder에서 출력 단어를 예측하는 Time step마다 인코더에서의 전체 입력 문장을 다시 참고한다.
- 기존 seq2seq 모델에서 하나의 고정된 Context vector(= Encoder의 마지막 Hidden state)를 사용했다.
- 각 디코더의 Time step마다 인코더의 모든 Hidden state를 참고하도록 한다.
2. 모든 입력 토큰을 모두 동일한 비율로 참고하지 않고 예측해야 할 단어와 연관이 있는 입력을 집중(Attention)하여 참고하게 된다.
- 연관이 있는 입력 토큰에 더 큰 가중치를 부여하는 메커니즘을 사용한다.
- 각 디코더 타임 스텝에서 인코더의 각 출력(Hidden state)에 대해 가중치(Attention score)를 계산
- 중요한 Hidden state에 더 많은 주의를 기울여서 context vector를 동적으로 생성한다.
Attention 메커니즘을 살펴보기 이전에 Attention 함수에 대해서 이해해야 한다.
Attention(Q, K, V) = Attention Value
Q = Query : t 시점의 디코더 셀에서의 은닉 상태
K = Keys : 모든 시점의 인코더 셀의 은닉 상태들
V = Values : 모든 시점의 인코더 셀의 은닉 상태들
이를 LSTM 기반 Encoder & Decoder 아키텍처에서 전체적인 도식으로 보면 다음과 같다.
1. Decoder은 현재 예측해야 하는 단어에 대해서 Encoder에 Query를 보낸다.
2. Encoder은 Query에 대해서 모든 Key와의 유사도를 각각 구한다.
3. 유사도를 Key와 매핑된 각각의 Value에 반영한다.
- 소프트맥스 함수를 통해 나온 결과값은 I, am, a, student 단어 각각이 현재 출력 단어를 예측할 때 얼마나 도움이 되는지의 정도를 수치화한 값이다.
4. Attention Value 출력 : 유사도가 반영된 Value을 모두 취합해서 Decoder에게 반환한다.
- 각 입력 단어가 디코더의 예측에 도움이 되는 정도가 수치화하여 측정되면 이를 하나의 정보(Attention Value)로 담아서 디코더로 전송한다.
이를 좀 더 깊게 살펴보도록 하자.
1. Attention Score 구하기
현재 Decoder의 Hidden state을 Encoder의 모든 Hidden state에 대해서 계산하여 유사도 측정
Attention Score : 현재 디코더의 시점 t에서 단어를 예측하기 위해 인코더의 모든 Key(= 모든 Hidden State) 각각이 Decoder의 현 시점의 은닉 상태 \(s_t\)와 얼마나 유사한지를 판단하는 값이다.
즉 디코더가 현재 예측해야 하는 단어에 대해서 입력했던 값들과의 유사도를 측정하는 과정이다.
Attention Score를 구하기 위해서는 현재 Decoder의 Hidden State를 전치시킨 Vector와, 입력에 대한 모든 Hidden State \(h_i\)에 대해서 내적시킨다.
왼쪽과 같은 연산을 모든 입력값(Encoder의 Hidden State)에 대해서 수행한다.
2. Attention Distribute(분포) 구하기
Attention Value에 Softmax 적용
각각의 Attention Score에 Softmax 함수를 적용하여 확률 분포인 Attention 분포를 얻어낸다.
분포의 각각의 값들은 Attention Weight(어텐션 가중치)라고 한다.
- 예를 들어 I, am, a, student의 어텐션 가중치가 각각 0.1, 0.4, 0.1, 0.4이 나왔으면 현재 출력을 예측하는 데에 영향을 주는 입력은 am, student일 것이다.
3. Attention Value 구하기
인코더 각각의 Hidden State와 Attention Weight들을 가중합
Attention Value : 인코더 각각의 Hidden State와 Attention Weight들을 가중합시킨다.
해당 Attention Value는 Encoder의 문맥을 포함하고 있기 때문에 Context Vector로도 불린다.
- seq2seq에서는 Encoder의 마지막 Hidden State를 Context vector라고 불렀다.
4. Attention Value를 Decoder의 Hidden State에 반영
Attention Value \(a_t\)를 현재 디코더의 Hidden State\(s_t\)와 결합하여 하나의 벡터로 만드는 작업을 수행한다.
해당 벡터를 기반으로 Dense 신경망 및 Softmax를 거쳐서 가장 가능성이 높은 단어를 출력하게 된다.
Seq2Seq 모델을 학습시켜서 번역기 만들어보기
1. 이번에는 실제로 Encoder, Decoder를 만들어서 Seq2Seq 모델로 조립한 후, 프랑스어와 영어로 매칭된 사전을 학습시켜서 번역기를 만들어 보자.
2. 그리고 Seq2Seq 모델을 RNN 기반 모델 대신 Attention을 사용해서 구성해서도 해보자.
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))
from google.colab import drive
drive.mount('/content/drive')
import os
os.chdir('drive/MyDrive/DL2024_201810776/week12')
%load_ext autoreload
%autoreload 2
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).
import pandas as pd
import tensorflow as tf
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
1. 데이터 읽고 전처리¶
- 영어와 프랑스어가 매칭되어있는 데이터셋을 사용하였다.
- src에 영어, tar에 프랑스어가 매칭되어 있는 구조이다.
- https://www.kaggle.com/code/jasoncallaway/fra-txt-details
lines = pd.read_csv('dataset/fra.txt', names=['src', 'tar', 'lic'], sep='\t')
del lines['lic']
print('전체 샘플의 개수 :',len(lines))
전체 샘플의 개수 : 232736
lines = lines.loc[:, 'src':'tar']
lines = lines[0:30000] # 5만개만 사용
lines.sample(10)
src | tar | |
---|---|---|
2882 | Be cheerful. | Soyez heureux. |
15503 | I have the list. | J'ai la liste. |
15183 | How large is it? | C'est grand comment ? |
22912 | That's not a lie. | Ce n'est pas un mensonge. |
19549 | Do I look thirty? | Ai-je l'air d'avoir trente ans ? |
2888 | Be creative. | Soyez créatif ! |
8153 | I trusted you. | Je vous faisais confiance. |
25492 | Can you go faster? | Es-tu en mesure d'aller plus vite ? |
16597 | It's a dead end. | C'est un cul-de-sac. |
9772 | Tom, hurry up. | Tom, dépêche-toi. |
lines.tar = lines.tar.apply(lambda x : '\t '+ x + ' \n')
lines.sample(10)
src | tar | |
---|---|---|
16196 | I'm feeling fit. | \t Je me sens en forme. \n |
20858 | I hope you agree. | \t J'espère que tu es d'accord. \n |
5368 | I must study. | \t Je dois étudier. \n |
11156 | I am off today. | \t Je ne suis pas de service, aujourd'hui. \n |
6388 | They'll call. | \t Ils appelleront. \n |
13094 | This is a fact. | \t C'est un fait. \n |
22 | Who? | \t Qui ? \n |
94 | Get up. | \t Lève-toi. \n |
4627 | You're nuts! | \t Vous êtes givré ! \n |
25832 | Don't forget that. | \t N'oublie pas cela. \n |
문자 집합 구축하기¶
데이터셋으로부터 문자 단위로 Set을 구축한다
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
print('source 문장의 char 집합 :',src_vocab_size)
print('target 문장의 char 집합 :',tar_vocab_size)
source 문장의 char 집합 : 77 target 문장의 char 집합 : 102
src_vocab = sorted(list(src_vocab))
tar_vocab = sorted(list(tar_vocab))
print(src_vocab[45:75])
print(tar_vocab[45:75])
['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] ['V', 'W', 'X', 'Y', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
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)])
print(src_to_index)
print(tar_to_index)
{' ': 1, '!': 2, '"': 3, '$': 4, '%': 5, '&': 6, "'": 7, ',': 8, '-': 9, '.': 10, '/': 11, '0': 12, '1': 13, '2': 14, '3': 15, '4': 16, '5': 17, '6': 18, '7': 19, '8': 20, '9': 21, ':': 22, '?': 23, 'A': 24, 'B': 25, 'C': 26, 'D': 27, 'E': 28, 'F': 29, 'G': 30, 'H': 31, 'I': 32, 'J': 33, 'K': 34, 'L': 35, 'M': 36, 'N': 37, 'O': 38, 'P': 39, 'Q': 40, 'R': 41, 'S': 42, 'T': 43, 'U': 44, 'V': 45, 'W': 46, 'X': 47, 'Y': 48, 'Z': 49, 'a': 50, 'b': 51, 'c': 52, 'd': 53, 'e': 54, 'f': 55, 'g': 56, 'h': 57, 'i': 58, 'j': 59, 'k': 60, 'l': 61, 'm': 62, 'n': 63, 'o': 64, 'p': 65, 'q': 66, 'r': 67, 's': 68, 't': 69, 'u': 70, 'v': 71, 'w': 72, 'x': 73, 'y': 74, 'z': 75, 'é': 76} {'\t': 1, '\n': 2, ' ': 3, '!': 4, '"': 5, '$': 6, '%': 7, '&': 8, "'": 9, ',': 10, '-': 11, '.': 12, '0': 13, '1': 14, '2': 15, '3': 16, '4': 17, '5': 18, '6': 19, '7': 20, '8': 21, '9': 22, ':': 23, '?': 24, 'A': 25, 'B': 26, 'C': 27, 'D': 28, 'E': 29, 'F': 30, 'G': 31, 'H': 32, 'I': 33, 'J': 34, 'K': 35, 'L': 36, 'M': 37, 'N': 38, 'O': 39, 'P': 40, 'Q': 41, 'R': 42, 'S': 43, 'T': 44, 'U': 45, 'V': 46, 'W': 47, 'X': 48, 'Y': 49, 'a': 50, 'b': 51, 'c': 52, 'd': 53, 'e': 54, 'f': 55, 'g': 56, 'h': 57, 'i': 58, 'j': 59, 'k': 60, 'l': 61, 'm': 62, 'n': 63, 'o': 64, 'p': 65, 'q': 66, 'r': 67, 's': 68, 't': 69, 'u': 70, 'v': 71, 'w': 72, 'x': 73, 'y': 74, 'z': 75, '\xa0': 76, '«': 77, '»': 78, 'À': 79, 'Ç': 80, 'É': 81, 'Ê': 82, 'Ô': 83, 'à': 84, 'â': 85, 'ç': 86, 'è': 87, 'é': 88, 'ê': 89, 'ë': 90, 'î': 91, 'ï': 92, 'ô': 93, 'ù': 94, 'û': 95, 'œ': 96, '\u2009': 97, '‘': 98, '’': 99, '\u202f': 100, '‽': 101}
Character Embedding¶
새로운 단어와 희귀한 단어가 자주 등장하므로 Character Embedding을 택하였고, 문장을 문자 단위로 인코딩해주자.
encoder_input = []
# 1개의 문장
for line in lines.src:
encoded_line = []
# 각 줄에서 1개의 char (Tom came after me)
for char in line:
# 각 char을 정수로 변환 (T, o, m, ,c ...)
encoded_line.append(src_to_index[char])
encoder_input.append(encoded_line)
print('원래 source 문장 :', lines.src[:5])
print('source 문장의 정수 인코딩 :',encoder_input[:5])
원래 source 문장 : 0 Go. 1 Go. 2 Go. 3 Go. 4 Hi. Name: src, dtype: object source 문장의 정수 인코딩 : [[30, 64, 10], [30, 64, 10], [30, 64, 10], [30, 64, 10], [31, 58, 10]]
decoder_input = []
for line in lines.tar:
encoded_line = []
for char in line:
encoded_line.append(tar_to_index[char])
decoder_input.append(encoded_line)
print('target 문장의 정수 인코딩 :',decoder_input[:5])
target 문장의 정수 인코딩 : [[1, 3, 46, 50, 3, 4, 3, 2], [1, 3, 37, 50, 67, 52, 57, 54, 12, 3, 2], [1, 3, 29, 63, 3, 67, 64, 70, 69, 54, 3, 4, 3, 2], [1, 3, 26, 64, 70, 56, 54, 3, 4, 3, 2], [1, 3, 43, 50, 61, 70, 69, 3, 4, 3, 2]]
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)
print('target 문장 레이블의 정수 인코딩 :',decoder_target[:5])
target 문장 레이블의 정수 인코딩 : [[3, 46, 50, 3, 4, 3, 2], [3, 37, 50, 67, 52, 57, 54, 12, 3, 2], [3, 29, 63, 3, 67, 64, 70, 69, 54, 3, 4, 3, 2], [3, 26, 64, 70, 56, 54, 3, 4, 3, 2], [3, 43, 50, 61, 70, 69, 3, 4, 3, 2]]
max_src_len = max([len(line) for line in lines.src])
max_tar_len = max([len(line) for line in lines.tar])
print('source 문장의 최대 길이 :',max_src_len)
print('target 문장의 최대 길이 :',max_tar_len)
source 문장의 최대 길이 : 18 target 문장의 최대 길이 : 61
입력 크기를 통일시키기 위해서 패딩을 추가해준다.¶
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)
2. 모델 생성¶
from tensorflow.keras.layers import Input, LSTM, Embedding, Dense, Concatenate, Attention
from tensorflow.keras.models import Model
import numpy as np
2-1. Encoder¶
Encoder의 LSTM에는 return_state를 설정하고 return_sequence를 설정하지 않는다(출력이 필요하지 않기 때문)
encoder_inputs = Input(shape=(None, src_vocab_size))
encoder_lstm = LSTM(units=256, return_state=True) # Encoder은 return_state를 설정한 LSTM 선언
encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs) # Encoder 출력 (encoder_outputs, state_h, state_c)에 저장
encoder_states = [state_h, state_c] # LSTM은 상태가 두 개. 은닉 상태와 셀 상태.
2-2. Decoder¶
- Decoder LSTM에는 return_state와 함께 return_sequence를 설정하여 출력 시퀀스를 받을 수 있도록 한다.
- 출력에 대해서 softmax를 적용하여 확률이 높은 결과를 출력하도록 한다.
decoder_inputs = Input(shape=(None, tar_vocab_size))
# Decoder은 return_state와 return sequence를 설정한 LSTM 선언
decoder_lstm = LSTM(units=256, return_sequences=True, return_state=True)
# Decoder에게 Encoder의 은닉 상태, 셀 상태를 전달.
decoder_outputs, _, _= decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_softmax_layer = Dense(tar_vocab_size, activation='softmax')
seq2seq_outputs = decoder_softmax_layer(decoder_outputs)
2-3 Seq2Seq¶
#모델 선언
model = Model([encoder_inputs, decoder_inputs], seq2seq_outputs)
model.compile(optimizer="rmsprop", loss="categorical_crossentropy")
model.summary()
Model: "model" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== input_1 (InputLayer) [(None, None, 77)] 0 [] input_2 (InputLayer) [(None, None, 102)] 0 [] lstm (LSTM) [(None, 256), 342016 ['input_1[0][0]'] (None, 256), (None, 256)] lstm_1 (LSTM) [(None, None, 256), 367616 ['input_2[0][0]', (None, 256), 'lstm[0][1]', (None, 256)] 'lstm[0][2]'] dense (Dense) (None, None, 102) 26214 ['lstm_1[0][0]'] ================================================================================================== Total params: 735846 (2.81 MB) Trainable params: 735846 (2.81 MB) Non-trainable params: 0 (0.00 Byte) __________________________________________________________________________________________________
3. 모델 학습¶
model.fit(x=[encoder_input, decoder_input], y=decoder_target, batch_size=64, epochs=40, validation_split=0.2)
Epoch 1/40 375/375 [==============================] - 6s 16ms/step - loss: 0.4566 - val_loss: 0.5638 Epoch 2/40 375/375 [==============================] - 5s 13ms/step - loss: 0.4405 - val_loss: 0.5488 Epoch 3/40 375/375 [==============================] - 5s 13ms/step - loss: 0.4267 - val_loss: 0.5319 Epoch 4/40 375/375 [==============================] - 7s 18ms/step - loss: 0.4141 - val_loss: 0.5188 Epoch 5/40 375/375 [==============================] - 5s 13ms/step - loss: 0.4027 - val_loss: 0.5110 Epoch 6/40 375/375 [==============================] - 6s 15ms/step - loss: 0.3921 - val_loss: 0.5039 Epoch 7/40 375/375 [==============================] - 5s 14ms/step - loss: 0.3825 - val_loss: 0.4929 Epoch 8/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3736 - val_loss: 0.4818 Epoch 9/40 375/375 [==============================] - 6s 16ms/step - loss: 0.3655 - val_loss: 0.4753 Epoch 10/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3577 - val_loss: 0.4682 Epoch 11/40 375/375 [==============================] - 5s 15ms/step - loss: 0.3505 - val_loss: 0.4638 Epoch 12/40 375/375 [==============================] - 6s 16ms/step - loss: 0.3437 - val_loss: 0.4598 Epoch 13/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3371 - val_loss: 0.4538 Epoch 14/40 375/375 [==============================] - 5s 15ms/step - loss: 0.3310 - val_loss: 0.4483 Epoch 15/40 375/375 [==============================] - 5s 14ms/step - loss: 0.3252 - val_loss: 0.4429 Epoch 16/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3198 - val_loss: 0.4443 Epoch 17/40 375/375 [==============================] - 6s 16ms/step - loss: 0.3145 - val_loss: 0.4404 Epoch 18/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3096 - val_loss: 0.4346 Epoch 19/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3048 - val_loss: 0.4352 Epoch 20/40 375/375 [==============================] - 6s 16ms/step - loss: 0.3001 - val_loss: 0.4331 Epoch 21/40 375/375 [==============================] - 6s 16ms/step - loss: 0.2959 - val_loss: 0.4284 Epoch 22/40 375/375 [==============================] - 7s 18ms/step - loss: 0.2915 - val_loss: 0.4295 Epoch 23/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2874 - val_loss: 0.4262 Epoch 24/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2834 - val_loss: 0.4242 Epoch 25/40 375/375 [==============================] - 6s 16ms/step - loss: 0.2796 - val_loss: 0.4246 Epoch 26/40 375/375 [==============================] - 5s 15ms/step - loss: 0.2757 - val_loss: 0.4209 Epoch 27/40 375/375 [==============================] - 6s 16ms/step - loss: 0.2721 - val_loss: 0.4233 Epoch 28/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2685 - val_loss: 0.4195 Epoch 29/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2650 - val_loss: 0.4199 Epoch 30/40 375/375 [==============================] - 6s 17ms/step - loss: 0.2613 - val_loss: 0.4202 Epoch 31/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2581 - val_loss: 0.4188 Epoch 32/40 375/375 [==============================] - 6s 17ms/step - loss: 0.2547 - val_loss: 0.4212 Epoch 33/40 375/375 [==============================] - 8s 21ms/step - loss: 0.2514 - val_loss: 0.4197 Epoch 34/40 375/375 [==============================] - 6s 17ms/step - loss: 0.2484 - val_loss: 0.4199 Epoch 35/40 375/375 [==============================] - 7s 18ms/step - loss: 0.2453 - val_loss: 0.4188 Epoch 36/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2424 - val_loss: 0.4193 Epoch 37/40 375/375 [==============================] - 7s 18ms/step - loss: 0.2391 - val_loss: 0.4207 Epoch 38/40 375/375 [==============================] - 6s 16ms/step - loss: 0.2362 - val_loss: 0.4225 Epoch 39/40 375/375 [==============================] - 7s 19ms/step - loss: 0.2335 - val_loss: 0.4199 Epoch 40/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2306 - val_loss: 0.4227
<keras.src.callbacks.History at 0x79609a1d1d80>
4. 모델 결과 확인¶
훈련된 모델을 사용해서 입력 시퀀스에 대한 번역을 생성하기 위한 추론 모델을 구성한다.
- encoder_model: 인코더 모델을 구성. 인코더의 입력을 주면 인코더의 상태를 출력
- decoder_model: 디코더 모델을 구성. 디코더의 초기 상태와 입력을 주면 디코더의 출력과 상태를 출력
encoder_model = Model(inputs=encoder_inputs, outputs=encoder_states)
# 이전 시점의 상태들을 저장하는 텐서
decoder_state_input_h = Input(shape=(256,))
decoder_state_input_c = Input(shape=(256,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
# 문장의 다음 단어를 예측하기 위해서 초기 상태(initial_state)를 이전 시점의 상태로 사용
# decoder_lstm을 사용하면서 state_h, state_c 같이 저장하기
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)
# 훈련 과정에서와 달리 LSTM의 리턴하는 은닉 상태와 셀 상태를 버리지 않음.
decoder_states = [state_h, state_c]
decoder_outputs = decoder_softmax_layer(decoder_outputs)
decoder_model = Model(inputs=[decoder_inputs] + decoder_states_inputs, outputs=[decoder_outputs] + decoder_states)
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())
번역 생성¶
decode_sequence: 주어진 입력 시퀀스에 대해 번역을 생성하는 함수.
- 인코더를 통해 초기 상태를 얻고, 그 상태를 디코더의 초기 상태로 사용하여 디코더를 반복적으로 실행하여 출력을 생성한다.
def decode_sequence(input_seq):
# 입력으로부터 인코더의 상태를 얻음
states_value = encoder_model.predict(input_seq)
# <SOS>에 해당하는 원-핫 벡터 생성
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)
# 예측 결과를 문자로 변환
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
번역 결과 확인¶
(대충 엉성하지만 번역 자체가 된다는 것을 의의로 두자! 이정도만 해서 잘 되는게 이상하다!)
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]) # '\t'와 '\n'을 빼고 출력
print('번역 문장:', decoded_sentence[1:len(decoded_sentence)-1]) # '\n'을 빼고 출력
1/1 [==============================] - 0s 445ms/step 1/1 [==============================] - 0s 429ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 28ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 29ms/step ----------------------------------- 입력 문장: Go. 정답 문장: Bouge ! 번역 문장: Continuez ! 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 36ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 25ms/step ----------------------------------- 입력 문장: Hello! 정답 문장: Bonjour ! 번역 문장: Soyez prudente ! 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 42ms/step 1/1 [==============================] - 0s 40ms/step 1/1 [==============================] - 0s 72ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 34ms/step 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 24ms/step ----------------------------------- 입력 문장: Got it! 정답 문장: J'ai pigé ! 번역 문장: Comme courigue ! 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 28ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 29ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 28ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 31ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 24ms/step ----------------------------------- 입력 문장: Go home. 정답 문장: Rentre à la maison. 번역 문장: Allez chercher ! 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 27ms/step 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 37ms/step 1/1 [==============================] - 0s 35ms/step 1/1 [==============================] - 0s 37ms/step 1/1 [==============================] - 0s 36ms/step 1/1 [==============================] - 0s 35ms/step 1/1 [==============================] - 0s 38ms/step 1/1 [==============================] - 0s 55ms/step 1/1 [==============================] - 0s 38ms/step ----------------------------------- 입력 문장: Forget me. 정답 문장: Oublie-moi. 번역 문장: Oubliez-le.
Seq2Seq 모델 + Attention
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))
from google.colab import drive
drive.mount('/content/drive')
import os
os.chdir('drive/MyDrive/DL2024_201810776/week12')
%load_ext autoreload
%autoreload 2
Mounted at /content/drive
import pandas as pd
import tensorflow as tf
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
1. 데이터 읽고 전처리¶
- 영어와 프랑스어가 매칭되어있는 데이터셋을 사용하였다.
- src에 영어, tar에 프랑스어가 매칭되어 있는 구조이다.
- https://www.kaggle.com/code/jasoncallaway/fra-txt-details
lines = pd.read_csv('dataset/fra.txt', names=['src', 'tar', 'lic'], sep='\t')
del lines['lic']
print('전체 샘플의 개수 :',len(lines))
전체 샘플의 개수 : 232736
lines = lines.loc[:, 'src':'tar']
lines = lines[0:30000] # 5만개만 사용
lines.sample(10)
src | tar | |
---|---|---|
8566 | Is that sweet? | Est-ce que c'est sucré ? |
9451 | This is weird. | C'est bizarre. |
9418 | They're weird. | Ils sont bizarres. |
924 | Be strong. | Sois puissante ! |
17877 | Tom is a jockey. | Tom est un jockey. |
26890 | I have a solution. | J'ai une solution. |
6127 | Plug this in. | Branche ça. |
8201 | I was pleased. | J'ai été contente. |
16126 | I'm a carpenter. | Je suis menuisier. |
7861 | I had no idea. | Je n'en avais aucune idée. |
lines.tar = lines.tar.apply(lambda x : '\t '+ x + ' \n')
lines.sample(10)
src | tar | |
---|---|---|
21889 | Is this your car? | \t Est-ce votre voiture ? \n |
17306 | Take your pills. | \t Prenez vos médicaments. \n |
17724 | This is reality. | \t C'est la réalité. \n |
16312 | I'm ready to go. | \t Je suis prêt à y aller. \n |
15176 | How is it going? | \t Comment vas-tu ? \n |
10495 | Buy me a drink. | \t Offre-moi un verre ! \n |
24949 | You were tricked. | \t Vous vous êtes fait avoir. \n |
10499 | Buy me a drink. | \t Offrez-moi un coup ! \n |
24294 | We've got a leak. | \t Nous avons une fuite. \n |
29473 | The party is over. | \t La fête est finie. \n |
문자 집합 구축하기¶
데이터셋으로부터 문자 단위로 Set을 구축한다
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
print('source 문장의 char 집합 :',src_vocab_size)
print('target 문장의 char 집합 :',tar_vocab_size)
source 문장의 char 집합 : 77 target 문장의 char 집합 : 102
src_vocab = sorted(list(src_vocab))
tar_vocab = sorted(list(tar_vocab))
print(src_vocab[45:75])
print(tar_vocab[45:75])
['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] ['V', 'W', 'X', 'Y', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
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)])
print(src_to_index)
print(tar_to_index)
{' ': 1, '!': 2, '"': 3, '$': 4, '%': 5, '&': 6, "'": 7, ',': 8, '-': 9, '.': 10, '/': 11, '0': 12, '1': 13, '2': 14, '3': 15, '4': 16, '5': 17, '6': 18, '7': 19, '8': 20, '9': 21, ':': 22, '?': 23, 'A': 24, 'B': 25, 'C': 26, 'D': 27, 'E': 28, 'F': 29, 'G': 30, 'H': 31, 'I': 32, 'J': 33, 'K': 34, 'L': 35, 'M': 36, 'N': 37, 'O': 38, 'P': 39, 'Q': 40, 'R': 41, 'S': 42, 'T': 43, 'U': 44, 'V': 45, 'W': 46, 'X': 47, 'Y': 48, 'Z': 49, 'a': 50, 'b': 51, 'c': 52, 'd': 53, 'e': 54, 'f': 55, 'g': 56, 'h': 57, 'i': 58, 'j': 59, 'k': 60, 'l': 61, 'm': 62, 'n': 63, 'o': 64, 'p': 65, 'q': 66, 'r': 67, 's': 68, 't': 69, 'u': 70, 'v': 71, 'w': 72, 'x': 73, 'y': 74, 'z': 75, 'é': 76} {'\t': 1, '\n': 2, ' ': 3, '!': 4, '"': 5, '$': 6, '%': 7, '&': 8, "'": 9, ',': 10, '-': 11, '.': 12, '0': 13, '1': 14, '2': 15, '3': 16, '4': 17, '5': 18, '6': 19, '7': 20, '8': 21, '9': 22, ':': 23, '?': 24, 'A': 25, 'B': 26, 'C': 27, 'D': 28, 'E': 29, 'F': 30, 'G': 31, 'H': 32, 'I': 33, 'J': 34, 'K': 35, 'L': 36, 'M': 37, 'N': 38, 'O': 39, 'P': 40, 'Q': 41, 'R': 42, 'S': 43, 'T': 44, 'U': 45, 'V': 46, 'W': 47, 'X': 48, 'Y': 49, 'a': 50, 'b': 51, 'c': 52, 'd': 53, 'e': 54, 'f': 55, 'g': 56, 'h': 57, 'i': 58, 'j': 59, 'k': 60, 'l': 61, 'm': 62, 'n': 63, 'o': 64, 'p': 65, 'q': 66, 'r': 67, 's': 68, 't': 69, 'u': 70, 'v': 71, 'w': 72, 'x': 73, 'y': 74, 'z': 75, '\xa0': 76, '«': 77, '»': 78, 'À': 79, 'Ç': 80, 'É': 81, 'Ê': 82, 'Ô': 83, 'à': 84, 'â': 85, 'ç': 86, 'è': 87, 'é': 88, 'ê': 89, 'ë': 90, 'î': 91, 'ï': 92, 'ô': 93, 'ù': 94, 'û': 95, 'œ': 96, '\u2009': 97, '‘': 98, '’': 99, '\u202f': 100, '‽': 101}
Character Embedding¶
새로운 단어와 희귀한 단어가 자주 등장하므로 Character Embedding을 택하였고, 문장을 문자 단위로 인코딩해주자.
encoder_input = []
# 1개의 문장
for line in lines.src:
encoded_line = []
# 각 줄에서 1개의 char (Tom came after me)
for char in line:
# 각 char을 정수로 변환 (T, o, m, ,c ...)
encoded_line.append(src_to_index[char])
encoder_input.append(encoded_line)
print('원래 source 문장 :', lines.src[:5])
print('source 문장의 정수 인코딩 :',encoder_input[:5])
원래 source 문장 : 0 Go. 1 Go. 2 Go. 3 Go. 4 Hi. Name: src, dtype: object source 문장의 정수 인코딩 : [[30, 64, 10], [30, 64, 10], [30, 64, 10], [30, 64, 10], [31, 58, 10]]
decoder_input = []
for line in lines.tar:
encoded_line = []
for char in line:
encoded_line.append(tar_to_index[char])
decoder_input.append(encoded_line)
print('target 문장의 정수 인코딩 :',decoder_input[:5])
target 문장의 정수 인코딩 : [[1, 3, 46, 50, 3, 4, 3, 2], [1, 3, 37, 50, 67, 52, 57, 54, 12, 3, 2], [1, 3, 29, 63, 3, 67, 64, 70, 69, 54, 3, 4, 3, 2], [1, 3, 26, 64, 70, 56, 54, 3, 4, 3, 2], [1, 3, 43, 50, 61, 70, 69, 3, 4, 3, 2]]
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)
print('target 문장 레이블의 정수 인코딩 :',decoder_target[:5])
target 문장 레이블의 정수 인코딩 : [[3, 46, 50, 3, 4, 3, 2], [3, 37, 50, 67, 52, 57, 54, 12, 3, 2], [3, 29, 63, 3, 67, 64, 70, 69, 54, 3, 4, 3, 2], [3, 26, 64, 70, 56, 54, 3, 4, 3, 2], [3, 43, 50, 61, 70, 69, 3, 4, 3, 2]]
max_src_len = max([len(line) for line in lines.src])
max_tar_len = max([len(line) for line in lines.tar])
print('source 문장의 최대 길이 :',max_src_len)
print('target 문장의 최대 길이 :',max_tar_len)
source 문장의 최대 길이 : 18 target 문장의 최대 길이 : 61
입력 크기를 통일시키기 위해서 패딩을 추가해준다.¶
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)
2. 모델 생성¶
Attention layer를 추가한 Seq2Seq 모델 사용하기¶
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, LSTM, Dense, Concatenate, Dot, Activation, Lambda, Softmax
from tensorflow.keras.optimizers import RMSprop
import numpy as np
import tensorflow as tf
2-1. Attention Layer 정의¶
# 어텐션 레이어
class AttentionLayer(tf.keras.layers.Layer):
def __init__(self):
super(AttentionLayer, self).__init__()
def call(self, query, key, value):
scores = tf.matmul(query, key, transpose_b=True)
attention_weights = Softmax(axis=-1)(scores)
context_vector = tf.matmul(attention_weights, value)
return context_vector, attention_weights
2-2. Encoder와 Decoder 정의¶
- 기존 seq2seq만 사용했을 때와 달리 Encoder에도 return_sequence를 설정한다.
# 인코더 정의
encoder_inputs = Input(shape=(None, src_vocab_size))
encoder_lstm = LSTM(256, return_sequences=True, return_state=True)
encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs)
# 디코더 정의
decoder_inputs = Input(shape=(None, tar_vocab_size))
decoder_lstm = LSTM(256, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=[state_h, state_c])
2-3. Encoder, Decoder, Attention Layer 조립한 seq2seq 생성¶
- Attention을 추가한 모델에서는 디코더의 각 시점에서 인코더의 출력에 대한 가중치(어텐션 가중치)를 계산하여 사용한다.
- Attention 레이어는 디코더의 출력과 인코더의 출력을 입력으로 받아 어텐션 가중치를 계산한다.
- Attention을 추가한 모델에서는 어텐션 메커니즘을 통해 계산된 컨텍스트 벡터와 디코더의 출력을 연결(concatenate)하여 디코더의 입력으로 사용
# 어텐션 레이어 추가
# 1) AttentioLayer 선언
attention_layer = AttentionLayer()
# 2) context_vector, attention_weights 에 출력 담기
context_vector, attention_weights = attention_layer(decoder_outputs, encoder_outputs, encoder_outputs)
# 컨텍스트 벡터와 디코더 출력을 연결
decoder_concat_input = Concatenate(axis=-1)([context_vector, decoder_outputs])
# 출력 레이어
decoder_dense = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_dense(decoder_concat_input)
# 전체 모델 정의
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
# 모델 컴파일
model.compile(optimizer=RMSprop(), loss='categorical_crossentropy', metrics=['accuracy'])
# 모델 요약
model.summary()
Model: "model" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== input_1 (InputLayer) [(None, None, 77)] 0 [] input_2 (InputLayer) [(None, None, 102)] 0 [] lstm (LSTM) [(None, None, 256), 342016 ['input_1[0][0]'] (None, 256), (None, 256)] lstm_1 (LSTM) [(None, None, 256), 367616 ['input_2[0][0]', (None, 256), 'lstm[0][1]', (None, 256)] 'lstm[0][2]'] attention_layer (Attention ((None, None, 256), 0 ['lstm_1[0][0]', Layer) (None, None, None)) 'lstm[0][0]', 'lstm[0][0]'] concatenate (Concatenate) (None, None, 512) 0 ['attention_layer[0][0]', 'lstm_1[0][0]'] dense (Dense) (None, None, 102) 52326 ['concatenate[0][0]'] ================================================================================================== Total params: 761958 (2.91 MB) Trainable params: 761958 (2.91 MB) Non-trainable params: 0 (0.00 Byte) __________________________________________________________________________________________________
3. 모델 학습¶
# 모델 학습
model.fit(
[encoder_input, decoder_input],
decoder_target,
batch_size=64,
epochs=40,
validation_split=0.2
)
Epoch 1/40 375/375 [==============================] - 11s 17ms/step - loss: 1.1066 - accuracy: 0.7147 - val_loss: 0.9820 - val_accuracy: 0.7169 Epoch 2/40 375/375 [==============================] - 5s 14ms/step - loss: 0.7415 - accuracy: 0.7846 - val_loss: 0.8149 - val_accuracy: 0.7608 Epoch 3/40 375/375 [==============================] - 5s 12ms/step - loss: 0.6405 - accuracy: 0.8122 - val_loss: 0.7400 - val_accuracy: 0.7799 Epoch 4/40 375/375 [==============================] - 4s 11ms/step - loss: 0.5853 - accuracy: 0.8275 - val_loss: 0.6899 - val_accuracy: 0.7937 Epoch 5/40 375/375 [==============================] - 5s 13ms/step - loss: 0.5464 - accuracy: 0.8383 - val_loss: 0.6537 - val_accuracy: 0.8052 Epoch 6/40 375/375 [==============================] - 5s 12ms/step - loss: 0.5141 - accuracy: 0.8476 - val_loss: 0.6200 - val_accuracy: 0.8144 Epoch 7/40 375/375 [==============================] - 4s 12ms/step - loss: 0.4873 - accuracy: 0.8552 - val_loss: 0.5943 - val_accuracy: 0.8229 Epoch 8/40 375/375 [==============================] - 6s 15ms/step - loss: 0.4650 - accuracy: 0.8617 - val_loss: 0.5709 - val_accuracy: 0.8296 Epoch 9/40 375/375 [==============================] - 5s 14ms/step - loss: 0.4454 - accuracy: 0.8672 - val_loss: 0.5523 - val_accuracy: 0.8348 Epoch 10/40 375/375 [==============================] - 5s 12ms/step - loss: 0.4285 - accuracy: 0.8722 - val_loss: 0.5307 - val_accuracy: 0.8416 Epoch 11/40 375/375 [==============================] - 5s 14ms/step - loss: 0.4135 - accuracy: 0.8766 - val_loss: 0.5188 - val_accuracy: 0.8449 Epoch 12/40 375/375 [==============================] - 4s 12ms/step - loss: 0.4003 - accuracy: 0.8805 - val_loss: 0.5139 - val_accuracy: 0.8473 Epoch 13/40 375/375 [==============================] - 4s 12ms/step - loss: 0.3880 - accuracy: 0.8843 - val_loss: 0.5014 - val_accuracy: 0.8507 Epoch 14/40 375/375 [==============================] - 6s 16ms/step - loss: 0.3769 - accuracy: 0.8875 - val_loss: 0.4889 - val_accuracy: 0.8546 Epoch 15/40 375/375 [==============================] - 5s 12ms/step - loss: 0.3665 - accuracy: 0.8906 - val_loss: 0.4795 - val_accuracy: 0.8575 Epoch 16/40 375/375 [==============================] - 5s 12ms/step - loss: 0.3570 - accuracy: 0.8933 - val_loss: 0.4695 - val_accuracy: 0.8605 Epoch 17/40 375/375 [==============================] - 5s 14ms/step - loss: 0.3481 - accuracy: 0.8960 - val_loss: 0.4628 - val_accuracy: 0.8627 Epoch 18/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3398 - accuracy: 0.8983 - val_loss: 0.4587 - val_accuracy: 0.8638 Epoch 19/40 375/375 [==============================] - 5s 14ms/step - loss: 0.3319 - accuracy: 0.9006 - val_loss: 0.4532 - val_accuracy: 0.8656 Epoch 20/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3245 - accuracy: 0.9027 - val_loss: 0.4461 - val_accuracy: 0.8678 Epoch 21/40 375/375 [==============================] - 5s 13ms/step - loss: 0.3178 - accuracy: 0.9047 - val_loss: 0.4423 - val_accuracy: 0.8688 Epoch 22/40 375/375 [==============================] - 7s 18ms/step - loss: 0.3113 - accuracy: 0.9065 - val_loss: 0.4409 - val_accuracy: 0.8696 Epoch 23/40 375/375 [==============================] - 4s 12ms/step - loss: 0.3050 - accuracy: 0.9084 - val_loss: 0.4380 - val_accuracy: 0.8706 Epoch 24/40 375/375 [==============================] - 4s 12ms/step - loss: 0.2992 - accuracy: 0.9102 - val_loss: 0.4367 - val_accuracy: 0.8717 Epoch 25/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2933 - accuracy: 0.9118 - val_loss: 0.4320 - val_accuracy: 0.8728 Epoch 26/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2879 - accuracy: 0.9133 - val_loss: 0.4288 - val_accuracy: 0.8743 Epoch 27/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2829 - accuracy: 0.9148 - val_loss: 0.4269 - val_accuracy: 0.8744 Epoch 28/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2777 - accuracy: 0.9162 - val_loss: 0.4259 - val_accuracy: 0.8756 Epoch 29/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2729 - accuracy: 0.9176 - val_loss: 0.4237 - val_accuracy: 0.8759 Epoch 30/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2684 - accuracy: 0.9189 - val_loss: 0.4251 - val_accuracy: 0.8758 Epoch 31/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2635 - accuracy: 0.9203 - val_loss: 0.4251 - val_accuracy: 0.8774 Epoch 32/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2594 - accuracy: 0.9215 - val_loss: 0.4224 - val_accuracy: 0.8773 Epoch 33/40 375/375 [==============================] - 5s 14ms/step - loss: 0.2551 - accuracy: 0.9228 - val_loss: 0.4199 - val_accuracy: 0.8782 Epoch 34/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2509 - accuracy: 0.9239 - val_loss: 0.4208 - val_accuracy: 0.8792 Epoch 35/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2469 - accuracy: 0.9252 - val_loss: 0.4218 - val_accuracy: 0.8789 Epoch 36/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2431 - accuracy: 0.9262 - val_loss: 0.4215 - val_accuracy: 0.8792 Epoch 37/40 375/375 [==============================] - 5s 13ms/step - loss: 0.2392 - accuracy: 0.9274 - val_loss: 0.4204 - val_accuracy: 0.8793 Epoch 38/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2356 - accuracy: 0.9284 - val_loss: 0.4221 - val_accuracy: 0.8799 Epoch 39/40 375/375 [==============================] - 6s 16ms/step - loss: 0.2320 - accuracy: 0.9293 - val_loss: 0.4195 - val_accuracy: 0.8805 Epoch 40/40 375/375 [==============================] - 5s 12ms/step - loss: 0.2284 - accuracy: 0.9306 - val_loss: 0.4210 - val_accuracy: 0.8809
<keras.src.callbacks.History at 0x7dcffb9398d0>
4. 모델 결과 확인¶
훈련된 모델을 사용해서 입력 시퀀스에 대한 번역을 생성하기 위한 추론 모델을 구성한다.
- encoder_model: 인코더 모델을 구성. 인코더의 입력을 주면 Attention value 생성
- decoder_model: 디코더 모델을 구성. 디코더의 출력과 인코더의 Attention value를 결합하여 출력
# 인코더 모델
encoder_model = Model(encoder_inputs, [encoder_outputs, state_h, state_c])
# 디코더
# 입력 정의
decoder_state_input_h = Input(shape=(256,), name="decoder_state_input_h")
decoder_state_input_c = Input(shape=(256,), name="decoder_state_input_c")
# 디코더 LSTM
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=[decoder_state_input_h, decoder_state_input_c])
# 어텐션 레이어 추가
context_vector, attention_weights = attention_layer(decoder_outputs, encoder_outputs, encoder_outputs)
# 컨텍스트 벡터와 디코더 출력을 결합
decoder_concat_input = Concatenate(name="concatenate_layer")([context_vector, decoder_outputs])
# 최종 출력 레이어
decoder_final_output = decoder_dense(decoder_concat_input)
# 디코더 모델 생성
decoder_model = Model(
inputs=[decoder_inputs, encoder_outputs, decoder_state_input_h, decoder_state_input_c],
outputs=[decoder_final_output, state_h, state_c, attention_weights]
)
# 오류 메시지에 나타난 문제를 디버깅하기 위해 모델의 개요를 출력
encoder_model.summary()
decoder_model.summary()
Model: "model_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, None, 77)] 0 lstm (LSTM) [(None, None, 256), 342016 (None, 256), (None, 256)] ================================================================= Total params: 342016 (1.30 MB) Trainable params: 342016 (1.30 MB) Non-trainable params: 0 (0.00 Byte) _________________________________________________________________ Model: "model_2" __________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== input_2 (InputLayer) [(None, None, 102)] 0 [] decoder_state_input_h (Inp [(None, 256)] 0 [] utLayer) decoder_state_input_c (Inp [(None, 256)] 0 [] utLayer) lstm_1 (LSTM) [(None, None, 256), 367616 ['input_2[0][0]', (None, 256), 'decoder_state_input_h[0][0]' (None, 256)] , 'decoder_state_input_c[0][0] '] input_3 (InputLayer) [(None, None, 256)] 0 [] attention_layer (Attention ((None, None, 256), 0 ['lstm_1[2][0]', Layer) (None, None, None)) 'input_3[0][0]', 'input_3[0][0]'] concatenate_layer (Concate (None, None, 512) 0 ['attention_layer[2][0]', nate) 'lstm_1[2][0]'] dense (Dense) (None, None, 102) 52326 ['concatenate_layer[1][0]'] ================================================================================================== Total params: 419942 (1.60 MB) Trainable params: 419942 (1.60 MB) Non-trainable params: 0 (0.00 Byte) __________________________________________________________________________________________________
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())
번역 생성¶
decode_sequence: 주어진 입력 시퀀스에 대해 번역을 생성하는 함수.
- 인코더를 통해 초기 상태를 얻고, 그 상태를 디코더의 초기 상태로 사용하여 디코더를 반복적으로 실행하여 출력을 생성한다.
# 번역 결과를 디코딩하는 함수
def decode_sequence(input_seq):
# 인코더의 상태를 얻음
encoder_output, state_h, state_c = 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 = ''
while not stop_condition:
output_tokens, h, c, a = decoder_model.predict([target_seq, encoder_output, state_h, state_c])
# 샘플링
sampled_token_index = np.argmax(output_tokens[0, -1, :])
sampled_char = index_to_tar[sampled_token_index]
decoded_sentence += sampled_char
# 종료 조건: 최대 길이 초과 또는 종료 심볼
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.
# 상태 업데이트
state_h, state_c = h, c
return decoded_sentence
# 테스트 데이터 사용 예시
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]) # '\t'와 '\n'을 빼고 출력
print('번역 문장:', decoded_sentence[1:len(decoded_sentence)-1]) # '\n'을 빼고 출력
1/1 [==============================] - 0s 407ms/step 1/1 [==============================] - 0s 366ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step ----------------------------------- 입력 문장: Go. 정답 문장: Bouge ! 번역 문장: Va ! 1/1 [==============================] - 0s 26ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 28ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step ----------------------------------- 입력 문장: Hello! 정답 문장: Bonjour ! 번역 문장: Aidez-vous ! 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 30ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 23ms/step ----------------------------------- 입력 문장: Got it! 정답 문장: J'ai pigé ! 번역 문장: Allez ! 1/1 [==============================] - 0s 18ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 23ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step ----------------------------------- 입력 문장: Go home. 정답 문장: Rentre à la maison. 번역 문장: Allez ! 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 24ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 25ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 21ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 22ms/step 1/1 [==============================] - 0s 27ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 20ms/step 1/1 [==============================] - 0s 19ms/step 1/1 [==============================] - 0s 22ms/step ----------------------------------- 입력 문장: Forget me. 정답 문장: Oublie-moi. 번역 문장: Allons-y ransser !
References : https://wikidocs.net/22893
해당 포스팅의 내용은 "상명대학교 민경하 교수님 "인공지능" 수업, 상명대학교 김승현 교수님 "딥러닝"수업을 기반으로 작성하였으며, 포스팅 자료는 해당 내용을 기반으로 재구성하여 만들어 사용하였습니다.
'Data Science > 머신러닝 & 딥러닝' 카테고리의 다른 글
[딥러닝] 생성형 모델 : VAE / GAN / Diffusion (+ 이미지 생성 및 복원해보기) (0) | 2024.06.17 |
---|---|
[딥러닝] Transformer : 소개와 동작 원리 (+ 간단한 챗봇 만들기) (0) | 2024.06.11 |
[딥러닝] NLP : 자연어 처리 기본 (+ 영화 리뷰글 긍정/부정 판단해보기) (0) | 2024.06.09 |
[딥러닝] 기억하는 신경망 : RNN, 그리고 개선 모델 (LSTM, GRU) (0) | 2024.06.08 |
[딥러닝] CNN : ResNet 모델로 동물 이미지 분류하기(CIFAR 이미지셋) (0) | 2024.06.08 |