seq2seq

( 참고 : “딥러닝을 이용한 자연어 처리 입문” (https://wikidocs.net/book/2155) )

1. Introduction

  • 입력 sequence로부터 다른 도메인의 sequence를 출력하는 모델이다

    ex) 챗봇, 기계 번역

크게 두 개의 아키텍쳐로 구성되어 있다 ( Encoder & Decoder )

1) Encoder : input 데이터를 context vector로 압축시키는 역할을 한다

2) Decoder : Encoder에서 출력된 context vector를 받아서, 번역된 단어들을 순차적으로 출력한다


Encoder와 Decoder의 내부는 RNN, LSTM, GRU 등의 셀로 구성되어 있다.

( RNN, LSTM, GRU 등에 대한 내용은 이미 알고 있다 가정하고 생략한다 )


2. Encoder & Decoder of seq2seq

(1) Encoder

  • 각각의 단어는 embedding을 통해 vector로 바뀐 뒤에 Encoder의 입력으로 들어가게 된다.



  • 하나의 cell은 각각의 시점에 2개의 입력을 받는다

    • 1 ) t-1 시점에서의 hidden state (은닉 상태)
  • 2 ) t 점에서의 input vector


(2) Decoder

  • Encoder의 마지막 cell의 hidden state인 ‘context vector’를 첫 번째 hidden state의 값으로 사용한다.

  • Decoder의 cell이 출력하는 output은, Dense Layer를 거치고 마지막으로 Softmax 함수를 통해

    “각 단어들이 나올 확률”을 예측값으로 반환한다.



Teacher Forcing (교사 강요 )

  • 개념 : “train 시”, decoder의 입력으로 이전 decoder cell의 output이 아닌 “실제 값”을 입력값으로 하는 방법
  • 이유 : 만약 예측이 틀렸는데도 불구하고, 계속 다음 cell의 입력으로 사용할 경우, 전체적인 예측을 어렵게 할 수 있기 때문이다!

3. seq2seq with python

  • seq2seq을 이용한 NMT 생성하기
  • character-level NMT


1) import dataset

  • 총 170,651개의 문장 ( 30000개의 문장만 sample하여 진행 )
  • source langauge(영어) & target language(프랑스어) pair
import pandas as pd
lines = pd.read_csv('fra.txt', names=['src','tar'], sep='\t')
lines = lines.sample(30000)


2) data preprocessing

번역 문장(target language)인 French에는 sos(start of sequence)eos(end of sequence)를 넣어줘야 한다
( sos로는 ‘\t’, eos로는 ‘\n’ )

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


글자 집합 생성 : token 단위가 ‘단어’가 아닌 ‘글자’이므로

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)


영어의 고유 글자는 101개, 프랑스어의 고유 글자는 73개다 ( 특수 문자 등 전부 포함하여 )

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


각 글자에 index를 부여한다

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

print(src2index)
{' ': 1, '!': 2, '"': 3, '$': 4, '%': 5, "'": 6, ',': 7, '-': 8, '.': 9, '0': 10, '1': 11, '2': 12, '3': 13, '4': 14, '5': 15, '6': 16, '7': 17, '8': 18, '9': 19, ':': 20, ';': 21, '?': 22, 'A': 23, 'B': 24, 'C': 25, 'D': 26, 'E': 27, 'F': 28, 'G': 29, 'H': 30, 'I': 31, 'J': 32, 'K': 33, 'L': 34, 'M': 35, 'N': 36, 'O': 37, 'P': 38, 'Q': 39, 'R': 40, 'S': 41, 'T': 42, 'U': 43, 'V': 44, 'W': 45, 'X': 46, 'Y': 47, 'Z': 48, 'a': 49, 'b': 50, 'c': 51, 'd': 52, 'e': 53, 'f': 54, 'g': 55, 'h': 56, 'i': 57, 'j': 58, 'k': 59, 'l': 60, 'm': 61, 'n': 62, 'o': 63, 'p': 64, 'q': 65, 'r': 66, 's': 67, 't': 68, 'u': 69, 'v': 70, 'w': 71, 'x': 72, 'y': 73, 'z': 74, '\xa0': 75, '«': 76, '»': 77, 'À': 78, 'Â': 79, 'Ç': 80, 'É': 81, 'Ê': 82, 'Ô': 83, 'à': 84, 'á': 85, 'â': 86, 'ç': 87, 'è': 88, 'é': 89, 'ê': 90, 'ë': 91, 'î': 92, 'ï': 93, 'ô': 94, 'ù': 95, 'û': 96, 'œ': 97, '\u2009': 98, '\u200b': 99, '’': 100, '\u202f': 101}


encoder 및 decoder의 input으로 넣기 전에, “정수 인코딩”을 해준다

encoder_input = []

for line in lines.src:
    temp_X = []
    for w in line: 
        temp_X.append(src2index[w])
    encoder_input.append(temp_X)
decoder_input = []

for line in lines.tar:
    temp_X = []
    for w in line:
        temp_X.append(tar2index[w])
    decoder_input.append(temp_X)
print(encoder_input[:3])

[[42, 63, 61, 1, 66, 63, 62, 54, 60, 53, 9], [38, 53, 62, 67, 53, 67, 8, 68, 69, 1, 65, 69, 53, 1, 58, 53, 1, 64, 63, 69, 66, 66, 49, 57, 67, 1, 64, 49, 66, 60, 53, 66, 1, 84, 1, 42, 63, 61, 75, 22], [28, 49, 57, 68, 8, 57, 60, 1, 49, 69, 67, 67, 57, 1, 51, 56, 49, 69, 52, 1, 51, 56, 49, 65, 69, 53, 1, 58, 63, 69, 66, 101, 22]]


target word 앞에 붙어 있는 ‘\t’를 제거한다

( ‘\t’의 index =1 -> 모든 문장 앞에서 제거하기 )

decoder_target = []

for line in lines.tar:
    t=0
    temp_X = []
    for w in line:
        if t>0:
            temp_X.append(tar2index[w])
        t=t+1     
    decoder_target.append(temp_X)
print(decoder_target[:3])

[[23, 23, 8, 22, 45, 3, 12, 9, 10, 3, 6, 26, 65, 48, 61, 50, 52, 7, 3, 21, 67, 67, 65, 56, 49, 68, 67, 56, 62, 61, 20, 3, 67, 48, 67, 62, 52, 49, 48, 9, 62, 65, 54, 3, 4, 12, 11, 11, 11, 11, 15, 13, 3, 6, 23, 31, 7, 3, 5, 3, 4, 15, 12, 16, 15, 14, 13, 18, 3, 6, 63, 56, 67, 56, 67, 61, 48, 67, 62, 59, 52, 7, 2], [23, 23, 8, 22, 45, 3, 12, 9, 10, 3, 6, 26, 65, 48, 61, 50, 52, 7, 3, 21, 67, 67, 65, 56, 49, 68, 67, 56, 62, 61, 20, 3, 67, 48, 67, 62, 52, 49, 48, 9, 62, 65, 54, 3, 4, 13, 15, 12, 14, 13, 14, 15, 3, 6, 23, 31, 7, 3, 5, 3, 4, 15, 17, 18, 13, 12, 10, 18, 3, 6, 21, 56, 57, 56, 7, 2], [23, 23, 8, 22, 45, 3, 12, 9, 10, 3, 6, 26, 65, 48, 61, 50, 52, 7, 3, 21, 67, 67, 65, 56, 49, 68, 67, 56, 62, 61, 20, 3, 67, 48, 67, 62, 52, 49, 48, 9, 62, 65, 54, 3, 4, 13, 12, 12, 14, 10, 11, 3, 6, 23, 31, 7, 3, 5, 3, 4, 18, 16, 19, 10, 14, 15, 3, 6, 66, 48, 50, 65, 52, 51, 50, 52, 59, 67, 56, 50, 7, 2]]


seq2seq의 input으로 들어간 ‘영어’ 문장의 길이와, output으로 나온 ‘프랑스어’ 최대 문장의 길이는 다르다

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)
print(max_tar_len)

325
102


하지만, padding을 해 줄 때 두 언어의 길이를 반드시 동일하게 해줄 필요는 없다.

padding 이후, One-hot Encoding 해준다.

from tensorflow.keras.preprocessing.sequence import pad_sequences

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


3) seq2seq 훈련

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


[ Encoder input ]

encoder_inputs = Input(shape=(None, src_vocab_size))
encoder_lstm = LSTM(units=256, return_state=True)
_, state_h, state_c = encoder_lstm(encoder_inputs) 
encoder_states = [state_h, state_c]


Decoder input

decoder_inputs = Input(shape=(None, tar_vocab_size))
decoder_lstm = LSTM(units=256, return_sequences=True, return_state=True)
decoder_outputs, _, _= decoder_lstm(decoder_inputs, initial_state=encoder_states)
decoder_softmax_layer = Dense(tar_vocab_size, activation='softmax')
decoder_outputs = decoder_softmax_layer(decoder_outputs)


Encoder와 Decoder을 만든 이후, 이 둘을 합친 model을 생성

model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer="adam", loss="categorical_crossentropy")

model.fit(x=[encoder_input, decoder_input], 
          y=decoder_target, 
          batch_size=64, epochs=20, validation_split=0.2)


4) seq2seq 동작

seq2seq 훈련을 시키는 것과, 동작시키는 것은 다르다! (‘teacher forcing’)

[ 동작 단계 ]

  • step 1) 번역하고자 하는 input sequence가 encoder에 들어가서 hidden, cell state 얻음

  • step 2) ‘\t’ (sos)를 decoder로 보냄

  • step 3) ‘\n’ (eos)이 나올 때 까지 계속 예측함

    ( 이때, decoder의 input으로는 실제 정답이 아니라, 이전 decoder의 예측값이 input으로 들어간다 )


[ Encoder ]

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


[ Decoder ]

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]

decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)

# 문장의 다음 단어를 예측하기 위해서 초기 상태를 이전 상태로 사용
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)


[ 동작 시키기 ]

index2src = dict((i, char) for char, i in src2index.items())
index2tar = dict((i, char) for char, i in tar2index.items())
def decode_seq(input_seq):
    # state of Encoder
    states_value = encoder_model.predict(input_seq)
    
    # <SOS> One-hot Encoding
    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 = 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

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

        # 길이가 1인 타겟 시퀀스를 업데이트 
        target_seq = np.zeros((1, 1, tar_vocab_size))
        target_seq[0, 0, sampled_token_index] = 1.

        # 상태를 업데이트 
        states_value = [h, c]

    return decoded_sentence
import numpy as np

for seq_index in [1,10,100,100]: # 입력 문장의 인덱스
    input_seq = encoder_input[seq_index: seq_index + 1]
    decoded_sentence = decode_seq(input_seq)
    print(35 * "-")
    print('Input Sentence:', lines.src[seq_index])
    print('Actual Sentence:', lines.tar[seq_index][1:len(lines.tar[seq_index])-1]) # '\t'와 '\n'을 빼고 출력
    print('Predicted Sentence:', decoded_sentence[:len(decoded_sentence)-1]) # '\n'을 빼고 출력