자연어처리(AI학습 59)

이유진·2024년 7월 8일

--40.Seq2Seq.ipynb--

Sequence-to-Sequence

  • Sequeuce-to-Sequence(Seq2Seq) 는 입력된 시퀀스로부터 다른 도메인의 시퀀스를 출력하는 모델

    • 기계번역 (machine translation)
      • '한국어 도메인' 을 가지는 문장을 입력하면 '영어 도메인' 에 해당하는 문장을 얻을수 있다.
      • 구글 번역기, 파파고...
    • 내용 용약 (Text Summarization)
    • Speech to Text(STT)
  • Seq2Seq 는 새로운 모델이 아니다. 기존의 순환신경망 2개를 조합해서 만든다.

  • Seq2Seq 는 '인코더' 와 '디코더' 라는 모듈로 구성됨.
  • 인코더
    • 입력문장의 모든 단어들을 순차적으로 입력 반은뒤
    • 마지막에 이 모든 단어 정보들을 압축하여 하나의 벡터를 만든다. 이를 컨텍스트 벡터(context vector) 라 함
    • 이를 디코더로 전손한다.
  • 디코더
    • 컨텍스트 벡터 를 받아서 번역된 단어를 한개씩 순차적으로 출력.

인코더와 디코더의 아키텍쳐

  • 입력문장은 단어 토큰화 되어 쪼개어 지고
  • 단어토큰 각각은 RNN셀의 각 타임스텝의 입력이 된다.
  • 인코더의 마지막 시점의 은닉상태(hidden state) 가 디코더로 넘겨진다 (이게 바로 context vector)
  • 이 context vector 는 디코더 RNN셀의 첫번째 은닉상태에 사용된다.
  • 우선 디코더는 초기 입력으로 문장의 시작을 의미하는 토큰 sos 이 들어감
  • 디코더는 sos 가 입력되면 다음에 등장할 확률이 높은 단어를 예측.
  • 첫번째 타입스텝의 디코더 RNN셀은 다음에 등장할 단어로 je를 예측.
  • 그렇게 예측한 단어 je 를 다음 타임스텝의 RNN 셀 입력으로 입력
  • .. 다음에응 suis, 다음에는 etudiant..
  • 이런식으로 디코더는 타임스텝을 거듭하면서 다음의 단어들을 예측한다. 언제까지?
  • 이 반복행위는 문장의 끝을 의미하는 토큰 eos 가 다음단어로 예측될때까지 반복됨.

예측 과정에선?

  • Seq2Seq 는 훈련과정과 예측과정의 작동방식이 다릅니다.

  • 훈련과정에선

    • 디코더에게 인코더가 보낸 context vector 와 실제 정답인 상황인 [sos] je suis etudiant 을 입력받았을때
    • je suis etudiant [eos] 가 나와야 된다고 정답을 알려주며 훈련합니다
  • 예측과정에선!

    • 디코더는 오직 context vecotr 와 [sos] 만을 입력받은 후에
    • 다음에 올 단어를 예측하여 다음 타임스텝의 RNN셀의 입력으로 넣는 행위 반복

  • 각 타임스텝의 출력 단어로 나올수 있는 단어는 다양하다
  • 이를 예측하기 위해 softmax 함수 사용됨

기계 번역

'문자' 레벨 기계 번역

병렬 corpus 데이터

기계번역기를 훈련시키기 위해서는 훈련데이터를 병렬 코퍼스 (parrel corpus) 형태로 구성된 데이터가 필요하다.

srctar
Watch meRegradez-moi
Go.Marche.
  • '일반적인' 자연어 처리의 경우, 입력시퀀스와 출력시퀀스의 길이가 동일하나..
  • Seq2Seq 입력시퀀스와 출력시퀀스의 길이는 다를수 있다!
  • Seq2Seq 는 다음의 데이터들이 필요하다
    1. 인코더에 입력 데이터
    2. 디코더의 입력 데이터
    3. 디코더의 출력 데이터

데이터 준비

import numpy as np
import pandas as pd
import tensorflow as tf
import os
import matplotlib.pyplot as plt
import shutil
import requests
import zipfile
import pandas as pd

import requests

브라우저 인 척(?)을 하기위한 헤더.

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 파일 다운로드 완료 -> {output_path}')

else:
print(f'다운로드 실패 -> {response.status_code}')

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

download_zip(url, output_path)

!ls -lh *.zip

압축파일 풀기

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

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

!ls -lh *.txt

lines = pd.read_csv('fra.txt', names=['src', 'tar', 'lic'], sep='\t')
lines.head()

lines.sample(10)

lines.shape

불필요한 컬럼 제거

del lines['lic']

lines.head()

lines = lines[:60000]

lines.sample(10)

"""
번역 문장에 해당되는 프랑스어 데이터는 앞서 배웠듯이 시작을 의미하는 심볼 과
종료를 의미하는 심볼 을 넣어주어야 합니다.
여기서는 와 대신 \t를 시작 심볼, \n을 종료 심볼로 간주하여 추가하고
다시 데이터를 출력해보겠습니다.
"""
None

lines.tar = lines.tar.apply(lambda x : '\t' + x + '\n')

lines.sample(10)

글자사전, 인덱스 구성

이번 예제의 번역기는 토큰 단위가 '단어' 가 아니라 '문자' 다.

'글자단위' 로 예측하기 위해선, 글자 집합(사전)을 구축해야 함.

문자집합 구축

src_vocab = set()
for line in lines.src:
for char in line:
src_vocab.add(char)

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

print(src_vocab)
print(tar_vocab)

사전 크기

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

print(src_vocab_size, tar_vocab_size)

사전 정렬하여 순서 정한뒤 인덱스 부여

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

print(src_vocab[45:75])
print(tar_vocab[45:75])

인덱스 부여

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)

인코더에 입력할 데이터 구성

  • 문장의 글자 하나씩을 사전을 이용해 인덱스로 변환해 리스트에 넣음

encoder_input = []
for line in lines.src:
encoder_input.append([src_to_index[char] for char in line])

print(lines.src.values[48:51])
print(encoder_input[48:51])

디코더에 입력할 데이터 구성

  • 인코더 입력데이터 처리와 동일하나, target 데이터에 해당하는 사전을 적용해 주어야 함

decoder_input = []
for line in lines.tar:
decoder_input.append([tar_to_index[char] for char in line])

print(lines.tar.values[48:51])
print(decoder_input[48:51])

디코더의 출력 데이터 구성

  • 디코더의 출력과 비교할 target 데이터 구성
  • 디코더의 입력데이터를 구성할 때와 동일하나, '시작토큰 sos' 는 제외해주어야 함.

decoder_target = []

for line in lines.tar:
timestep = 0
encoded_line = []
for char in line:
if timestep > 0: # 시작토큰 sos '\t' 은 제외
encoded_line.append(tar_to_index[char])
timestep += 1

decoder_target.append(encoded_line)

print(decoder_input[:5])
print(decoder_target[:5])

패딩처리

  • 길이를 맞춰줄때는 해당데이터의 최대 길이 로 맞춰줌

from tensorflow.keras.preprocessing.sequence import pad_sequences

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

print(max_src_len, max_tar_len)

패딩하기전에 확인해보기

print(encoder_input[0])
print(decoder_input[0])
print(decoder_target[0])

한번만 실행!★

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')

print(encoder_input.shape)
print(decoder_input.shape)
print(decoder_target.shape)

print(lines.src[0])
print(encoder_input[0])

One-hot encoding

  • 문자 단위 번역기이므로 word embedding 안함.
  • '실제값' 뿐 아니라 '입력값' 도 one-hot 벡터로 만들어야 한다 -> 오차측정 하기 위함

from tensorflow.keras.utils import to_categorical

★한번만 실행하기!

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

print(src_vocab_size, tar_vocab_size)

print(encoder_input.shape)
print(decoder_input.shape)
print(decoder_target.shape)

모델 Seq2Seq

인코더 모델 구성

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

encoder_inputs = Input(shape=(None, src_vocab_size)) # 글자사전의 크기만큼의 원핫 벡터가 입력된다
encoder_lstm = LSTM(units=256, return_state=True)

hidden state(은닉상태) 와 cell state(셀상태) 리턴됨 (return_state=True)

encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs)

encoder_states = [state_h, state_c]

이 encoder_states 가 '디코더' 에 전달된다. 이것이 '컨텍스트 벡터' 입니다.

디코더 구성

  • 디코더의 처음 입력은 <sos> 토큰. (start of sequence)

  • 이 토큰 다음에 등장할 단어 예측

  • 예측한 결과를 다음 타임스텍의 디코더 입력으로 사용

  • 위 과정을 반복한다. <eos> 토큰이 예측될때까지!

  • 결국decoder는 => encoder 에서 받은 context vector 를 활용해 sequence 를 만들어 냄

  • 모델의 구성은 encoder 와 유사 하나,
  • LSTM 에서 아래 설정을 해주어야 한다
    • return_state=True
    • return_sequences=True
    • 출력을 시퀀스로 반환하기 위해 필요
  • 디코더 LSTM을 사용할 때는 initial_state= 를 인코더가 넘겨준 state로 설정해야 합 (context vector)
  • 마지막으로 Dense, softmax 사용하여 예측 글자에 해당하는 인덱스를 리턴하도록 구성

Teacher Forcing (교사 강요)

  • 앞서 설명한 seq2seq 모델을 보면, 디코더의 입력이 필요하지 않아 보인다.
  • 예측이 잘못됐을 경우. 잘못된 예측이 다음 타임스텝으로 입력되면? -> 연쇄적으로 잘못된 예측을 하게 될거다!
  • 이를 해결하기 위해 디코더의 다음 타임스텝의 입력으로 이전 시점의 출력이 아닌, '정답' 을 주어 이를 방지함
    • "이게 정답이야" 라는 식으로 개입해주는 거다

decoder_inputs = Input(shape = (None, tar_vocab_size))
decoder_lstm = LSTM(units=256, return_state=True, return_sequences=True)

디코더에게 인코더의 은닉상태, 셀상태 전달 (context vector!)

decoderoutputs, , _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)

decoder_softmax_layer = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_softmax_layer(decoder_outputs)

Seq2Seq 모델 생성

from keras.models import Model

두개의 input 을 주고, output 준 뒤에 그대로 compile 하면 된다.

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

학습

model.fit(x=[encoder_input, decoder_input], y=decoder_target,

batch_size=64, epochs=40, validation_split=0.2)

모델 저장, 불러오기

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

model.save(os.path.join(base_path, 'eng2fra_ch.keras'))

불러오기 할때

tf.keras.models.load() 사용하지 말것!

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

예측

  • 예측 프로세스는 훈련 때와 다르다
  • 예측할때는 인덱스 하나씩을 예측하게 되며, 예측한 인덱스를 저장하고 이를 다시 입력으로 사용해 종료 토큰이 나올때까지 반복
  • 마지막으로 예측한 인덱스들을 사전을 통해 글자로 변환하면 최종 예측한 문장을 얻음.

전체적인 번역 동작 단계

1. 번역하고자 하는 입력 문장이 인코더에 들어가서 '은닉상태' 와 '셀 상태' 를 얻는다.

2. 상태 와 에 해당하는 \t 를 디코더로 보냄.

3. 디코더는 에 해당하는 \n 이 아놀 때까지 다음 문자를 예측하는 행동 반복.

1. 우선 인코더 정의, encoder_inputs 와 encoder_states 는 훈련과정에서 이미 정의한 것들을 재사용

encoder_model = Model(inputs=encoder_inputs, outputs=encoder_states)

2. 디코더 설계

이전 상태들을 저장할 텐서

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_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() )

def decode_sequence(input_seq):

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

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)

# 예측 결과를 문자로 변환
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, 50, 100, 300, 1001]

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

for seq_index in [3, 5, 100, 300, 10001]:
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' 를 빼고 출력

profile
독해지자

0개의 댓글