본문 바로가기
자연어처리(NLP) & CHAT GPT/NLP

[NLP] Seq2Seq - 실습

by 11car28z 2023. 8. 8.

Seq2Seq + 문자 단위로 번역

seq2seq : 챗봇, 번역기, 내용 요약, 음성 -> 텍스트 or 이미지 => CNN => 상황 분류

seq2seq 참고: https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html

 

A ten-minute introduction to sequence-to-sequence learning in Keras

Fri 29 September 2017 By Francois Chollet In Tutorials. Note: this post is from 2017. See this tutorial for an up-to-date version of the code used here. I see this question a lot -- how to implement RNN sequence-to-sequence learning in Keras? Here is a sho

blog.keras.io

코퍼스 참고: http://www.manythings.org/anki

 

Tab-delimited Bilingual Sentence Pairs from the Tatoeba Project (Good for Anki and Similar Flashcard Applications)

Introducing Anki If you don't already use Anki, vist the website at http://ankisrs.net/ to download this free application for Macintosh, Windows or Linux. About These Files Any flashcard program that can import tab-delimited text files, such as Anki (free)

www.manythings.org

from google.colab import drive
drive.mount('/content/drive')

 

#imageNet 대회에서 순위권인 만들어진 모델 import
#from tensorflow.keras.applications.

 

import pandas as pd
import urllib3
import tensorflow as tf
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical

 

tf.__version__ #2.12.0

 

df = pd.read_csv('/content/drive/MyDrive/NLP/fra.txt', names=['src', 'tar', 'lic'], sep='\t')
#,가 없어서 하나의 데이터로 본다. sep="\t" : 현재 자료는 tab 기준으로 구분되어 있음.
#names: 헤더를 지정해 준다.

del df['lic']
print('전체 샘플의 개수 :',len(df)) #전체 샘플의 개수 : 227815

 

df = df.loc[:, 'src':'tar']
df = df[0:60000] # 6만개만 저장
df.sample(10)

 

sos: \t, eos: \n 추가 (문장 시작과 끝)

df.tar = df.tar.apply(lambda x : '\t '+ x + ' \n')#sos: \t, eos: \n
#해당 컬럼에 apply 안의 지시를 모두 적용해라
#lambda x에는 df['tar']의 문장이 하나씩 들어온다.

#apply, applymap, filter, lambda, map

df.sample(10)

 

1.인코더 : 단어 입력받아 문장에 대한 것을 cv로 표현

#입력문장 분리해서 문자단위 분리
#voc 구성

 

# 글자 집합 구축
src_vocab = set() #중복 없게 하기 위해 set사용
for line in df.src: # 1줄씩 읽음
    for char in line: # 1개의 글자씩 읽음
        src_vocab.add(char)

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

 

#1번부터 사용하여 번호 주기 -> 0번을 버리게 되니 사이즈 +1해주어야함.
src_vocab_size = len(src_vocab)+1#0번 사용x
tar_vocab_size = len(tar_vocab)+1#0번 사용x
print('source 문장의 char 집합 :',src_vocab_size)#source 문장의 char 집합 : 80
print('target 문장의 char 집합 :',tar_vocab_size)#target 문장의 char 집합 : 104

 

src_vocab = sorted(list(src_vocab))#아스키 코드 값으로 정렬되어서 출력
tar_vocab = sorted(list(tar_vocab))
print(src_vocab[45:75])
print(tar_vocab[45:75])

 

#enumerate(tarVocab) : 인덱스 번호와 문자
#(w,i+1)를 []하고 그것을 dict으로 변환
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)#sorted: 아스키 코드 기준으로 정렬되었다.

 

#입력 문장에 대해 정수 인코딩
encoder_input = []

# 1개의 문장
for line in df.src:
  enc_line = []
  # 각 줄에서 1개의 char
  for char in line:
    # 각 char을 정수로 변환
    enc_line.append(src_to_index[char])#문자의 지정 번호를 알 수 있음.
  encoder_input.append(enc_line)
print('source 문장의 정수 인코딩 :',encoder_input[:5])

 

dec_input = []
for line in df.tar:
  enc_line = []
  for char in line:
    enc_line.append(tar_to_index[char])#문자의 지정 번호를 알 수 있음.
  dec_input.append(enc_line)
print('target 문장의 정수 인코딩 :',dec_input[:5])

 

#정수 인코딩 과정에서 <sos>를 제거 - 실제 값에는 있을 필요없음.
#참고: <sos> 기호는 디코더의 초기 숨겨진 상태를 돕고 출력 시퀀스 생성을 위한 컨텍스트를 설정하기 때문에 교육 중에 유지하는 것이 중요합니다.
#       그러나 추론 중에는 디코더가 처음부터 시퀀스를 예측하기 위해 고정된 초기 상태 또는 0 벡터로 시작하기 때문에 일반적으로 <sos> 기호가 필요하지 않습니다.

dec_target = []
for line in df.tar:
  timestep = 0
  enc_line = []
  for char in line:
    if timestep > 0: # 0번째 문자가 sos이기때문에 1번째부터 적용
      enc_line.append(tar_to_index[char])
    timestep = timestep + 1
  dec_target.append(enc_line)
print('target 문장 레이블의 정수 인코딩 :',dec_target[:5]) #'\t'가 1번이기때문에 1이  다 사라진것을 볼 수 있음.

 

max_src_len = max([len(line) for line in df.src])
max_tar_len = max([len(line) for line in df.tar])
print('source 문장의 최대 길이 :',max_src_len) #source 문장의 최대 길이 : 22
print('target 문장의 최대 길이 :',max_tar_len) #target 문장의 최대 길이 : 76

 

#padding: 모든 샘플의 길이 동일하게
encoder_input = pad_sequences(encoder_input, maxlen=max_src_len, padding='post')
dec_input = pad_sequences(dec_input, maxlen=max_tar_len, padding='post')
dec_target = pad_sequences(dec_target, maxlen=max_tar_len, padding='post')# 디코더의 정답레이블

 

#원핫인코딩
# 총 데이터수, 입력으로 올라가는 단어의 최대 개수(최대 문장 길이, LSTM 셀 개수), 차원(각 단어의 차원 = 종류의 수)
encoder_input = to_categorical(encoder_input)#(60000, 22, 80)
dec_input = to_categorical(dec_input)
dec_target = to_categorical(dec_target)

 

모델 만들기

keras 모델 만드는 방법

1)
model=Sequential()
model.add(Dense(10, input_dim=100, activation='relu'))
model.add(Dense(5, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
...

2)functional API
Input(shape=(None , srcVocabSize))#입력 문장의 개수는 정해져있지 않음, 차원은 단어개수

 

2.컨텍스트 벡터 : 문장이 너무 길면 컨텍스트 벡터로 담아도 한계가 있음. -> 정보 손실

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

 

#인코더
encoder_inputs = Input(shape=(None, src_vocab_size)) #입력 문장의 개수는 정해져있지 않음, 차원은 단어 개수
encoder_lstm = LSTM(units=256, return_state=True) #상태정보 = 컨텍스트 벡터
#중요!!!!!! #return_state=True: 인코더의 내부 상태를 디코더로 넘겨주기위해 사용, 인코더에 입력을 넣으면 내부 상태를 리턴


# encoder_outputs은 여기서는 불필요
encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs)
#state_h: 은닉 상태 정보, state_c: 셀 상태 정보

#-----------------------------------------------------------------------------------

#컨텍스트 벡터
# LSTM은 바닐라 RNN과는 달리 상태가 두 개. 은닉 상태와 셀 상태.
encoder_states = [state_h, state_c]

 

3.디코더 : 컨텍스트 벡터와 입력된 문장을 교사 강요 알고리즘을 이용해 학습중 잘못 예측된 결과를 다음 문장예측에 사용하지 못하도록 답을 알려준다.

 

dec_inputs = Input(shape=(None, tar_vocab_size))#입력 문장의 개수는 정해져있지 않음, 차원은 단어 개수
dec_lstm = LSTM(units=256, return_sequences=True, return_state=True)
#units=256 : 출력 크기
#return_state=True: 인코더의 내부 상태를 디코더로 넘겨주기위해 사용,
#                   인코더에 입력을 넣으면 내부 상태를 리턴, 무조건 마지막 셀 값 리턴
#return_sequences = True: 모든 셀에서 결과를 출력해주기 위해



# 디코더에게 인코더의 은닉 상태, 셀 상태를 전달.
#인코더 =>정보 => 디코더 : 인코더에서 전달해준 상태 정보를 받는 디코더
dec_outputs, _, _= dec_lstm(dec_inputs, initial_state=encoder_states)
#인코더가 아니라 state_h, state_c 상태정보는 불필요
#initial_state=encoder_states: 디코더의 초기 상태 정보

#인코더 256노드와 덴스 104노드가 일대일로 연결
dec_softmax_layer = Dense(tar_vocab_size, activation='softmax')#출력 차원: tarVocabSize / 활성함수: softmax
dec_outputs = dec_softmax_layer(dec_outputs)

 

#모델을 새롭게 만들고 학습 -> Model부터 수행
#기존 모델에 추가 학습 -> fit만

 

model = Model([encoder_inputs, dec_inputs], dec_outputs)#decoder_outputs : 실제 정답
model.compile(optimizer="rmsprop", loss="categorical_crossentropy")

 

#epochs=10 정도로 변경하기
model.fit(x=[encoder_input, dec_input], y=dec_target, batch_size=64, epochs=1, validation_split=0.2)

 

seq2seq 기계 번역기 동작시키기

# 입력 문자: 영문
# 정답 문장: 한글
# 에측 번역 문장: 한글

# 영문 입력 -> 인코딩 -> 컨텍스트 벡터 -> 셀에대한 정보가 디코더로 넘어 온다.-> 예측할때 디코더에서 그전에 예측된것이 그다음의 입력으로 들어가야한다.(eos 나올때까지 반복)

 

#seq2seq는 훈련할 때와 동작할 때의 방식이 다르다
#디코터는 eos가 나올때까지 예측하는 것을 반복수행

 

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

 

encoder_model.summary()

 

#상태 정보 저장 변수 # 이전 시점의 상태들을 저장하는 텐서
dec_state_input_h = Input(shape=(256,))
dec_state_input_c = Input(shape=(256,))
dec_states_inputs = [dec_state_input_h, dec_state_input_c]

# 문장의 다음 단어를 예측하기 위해서 초기 상태(initial_state)를 이전 시점의 상태로 사용.
# 뒤의 함수 decode_sequence()에 동작을 구현 예정
dec_outputs, state_h, state_c = dec_lstm(dec_inputs, initial_state=dec_states_inputs)

#차이!!!!!!!!!! # 훈련 과정에서와 달리 LSTM의 리턴하는 은닉 상태와 셀 상태를 버리지 않음.
dec_states = [state_h, state_c]
dec_outputs = dec_softmax_layer(dec_outputs)
dec_model = Model(inputs=[dec_inputs] + dec_states_inputs, outputs=[dec_outputs] + dec_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)

  # <SOS>에 해당하는 원-핫 벡터 생성
  target_seq = np.zeros((1, 1, tar_vocab_size))
  target_seq[0, 0, tar_to_index['\t']] = 1.

  stop_condition = False
  dec_sentence = ""

  # stop_condition이 True가 될 때까지 루프 반복
  #<eos>가 나오기 전까지 반복해라
  while not stop_condition:
    # 이점 시점의 상태 states_value를 현 시점의 초기 상태로 사용
    output_tokens, h, c = dec_model.predict([target_seq] + states_value)

    # 예측 결과를 문자로 변환
    #(104차원)output 토큰 최대값 인덱스!!!!! 예측하는 문자의 인덱스를 알아야함.
    sampled_token_index = np.argmax(output_tokens[0, -1, :])
    #찾은 인덱스 값으로 해당 문자를 찾아서
    sampled_char = index_to_tar[sampled_token_index]

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

    # <eos>에 도달하거나 최대 길이를 넘으면 중단.
    if (sampled_char == '\n' or
        len(dec_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 dec_sentence

 

for seq_index in [3,50,100,300,1001]: # 입력 문장의 인덱스
  input_seq = encoder_input[seq_index:seq_index+1]
  dec_sentence = decode_sequence(input_seq)
  print(35 * "-")
  print('입력 문장:', df.src[seq_index])
  print('정답 문장:', df.tar[seq_index][2:len(df.tar[seq_index])-1]) # '\t'와 '\n'을 빼고 출력
  print('번역 문장:', dec_sentence[1:len(dec_sentence)-1]) # '\n'을 빼고 출력