Data Science/Neural Network

Neural Machine Translation (seq2seq) Tutorial

준나이 2018. 12. 8. 17:48

Neural Machine Translation (seq2seq) Tutorial



1. Background on Neural Machine Translation

과거에 전통적인 phrase-based translation system들은 source 문장들을 여러개의 chunks로 쪼개고 그 다음 phrase-by-phrase로 해석했다. 이방법은 번역결과에 있어서 disfluency를 초래했는데 사람이 해석하는 것처럼 정확하게 작동하지 않았다. NMT는 기존의 방식에서 벗어나 사람이 전체 문장을 읽고 그 의미를 이해한 다음에 번역 결과를 생성하는 것처럼 번역을 한다.



[Enc-dec 구조] : encodersource 문장meaning vector변환하고 번역을 위해 decoder에 보낸다. (NMT의 일반적인 접근방식)


자세히 보면 NMT 시스템은 먼저 encoder를 이용해서 문장을 읽어서 thought vector (문장의 의미를 내포하고 있는 숫자들의 sequence / sentence vector, meaning vector 모두 같음)를 만든다. 그 다음 decoder에서는 위의 sentence vector를 이용하여 번역 결과를 내보내며 encoder-decoder 구조라고도 불린다. 


이러한 방식으로 NMT는 전통적인 phrase-based 방식에서 발생하는 local translation problem을 해결한다. 이를 통해 언어에 존재하는 long-range dependencies(gender agreements, syntax structures)를 잡아 낼 수 있고, 더 유창한 번역결과를 만들 수 있다.

NMT 모델 내에 존재하는 component들은 상황에 따라 다르게 구성될 수 있다. NN에서는 문장과 같은 sequential data를 다루기 위해서는 일반적으로 RNN을 사용하고, 대부분의 NMT에서도 encoder, decoder를 구성하는데 RNN이 사용된다. RNN 모델은 또한 directionality(unidirectional/bidirectional), depth(single/multi), type(vanlilla RNN/LSTM/GRU)에 따라서 다르게 구성할 수 있다.(여기서는 undirectional/multi/LSTM RNN을 이용하여 설명한다.)

밑에 그림에서 볼 수 있듯이 "I am a student"라는 단어로부터 "Je suis etudiant"이라는 문장을 만들어 낸다. high level에서 보면 NMT 모델은 2개의 RNN이루어져 있다. 아무런 prediction 없이 단순히 input 단어들만 소비하는 encoder RNN다음 단어를 예측하면서 완성된 문장을 만들어내는 decoder RNN이다.





[NMT] deep RNN 모델 : I am a student" -> "Je suis etudiant". 

여기서 <s>는 decoding process의 시작을 의미하는 토큰이고 </s>는 종료를 의미하는 토큰이다.



2. Training – How to build our first NMT system



encoder, decode는 여러가지 input을 받는다. 우선 원 문장(source sentence)와 encoder에서 decoder로 넘어가는 것을 알리는 토큰 <s>, 그리고 번역된 문장(target sentence)이다. training을 하는 동안 NMT 시스템에 다음과 같은 tensors를 제공해야한다. (time-major format)

encoder_inputs [max_encoder_time, batch_size] : source input words
decoder_inputs [max_encoder_time, batch_size] : target input words
decoder_outputs [max_encoder_time, batch_size] : target output words. (이것은 decoder_inputs에서 왼쪽으로 1개씩 shift되고 문장의 끝을 알리는 토큰 </s>가 덧붙여진 형태이다.)


효율성을 위해 다수의 문장을 한번에 train하는데 (batch_size) testing은 약간 다르다. (이부분은 뒤에서 더 설명할 예정)


1) Embedding

embedding layer가 작동하기 위해선 각각의 언어에 맞는 vocabulary가 선택 되야한다. (영어, 불어가 존재하면 2개의 별도로 분리된 vocabulary 필요) 보통 vocabulary size는 텍스트 중에 가장 자주 등장한 단어 V개를 기준으로 선택된다. 이외의 모든 단어들은 "unknown" token이 되고 모두 같은 embedding 또한 갖게된다. embedding 가중치는 언어당 하나씩 설정되고 traing하면서 학습하게 된다.

# Embedding
embedding_encoder = variable_scope.get_variable(
"embedding_encoder", [src_vocab_size, embedding_size], ...)
# Look up embedding:
# encoder_inputs: [max_time, batch_size]
# encoder_emb_inp: [max_time, batch_size, embedding_size]
encoder_emb_inp = embedding_ops.embedding_lookup(
embedding_encoder, encoder_inputs)

이와 유사하게 embedding_decoder와 decoder_emb_inp도 만들 수 있다. 또한 GloVe, word2vec 같은 이미 학습된 word vector로 초기상태를 설정 할 수 있다. 일반적으로 많은 양의 데이터가 주어지면 처음부터 이러한 임베딩을 직접 학습 시킨다.


2) Encoder

word를 word embedding으로 바꾸고나면, main network인 encoder RNN에 원 문장으로 decoder RNN에 번역된 문장으로 집어 넣는다. 이 두 RNN은 같은 내부에 가중치를 공유할 수도 있지만 일반적으로는 다른 RNN 파라미터를 사용한다. encoder RNN은 zero vector로 초기화해서 사용한다.

# Build RNN cell
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

# Run Dynamic RNN
# encoder_outputs: [max_time, batch_size, num_units]
# encoder_state: [batch_size, num_units]
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
encoder_cell, encoder_emb_inp,
sequence_length=source_sequence_length, time_major=True)

컴퓨팅 시간을 절약하기 위해 각기 다른길이의 input sentences를 사용한다. 이를 위해 dynamic_dnn에 source_sequence_length를 통해 각 문장의 길이를 알려줘야 한다. 또한, input이 time major이면 time_major=True를 설정해준다. 여기서는 single-LSTM layer로만 encoder를 구성했지만 뒤에서 multi-LSTM layer와 dropot, attention 또한 구성할 것이다


3) Decoder

decoder는 또한 source information에 접근할 필요가 있고 쉬운 방법 중 하나는 encoder의 마지막 hidden state인 encoder_state로 초기화 하는 것이다. ("student"의 마지막 hidden state를 decoder에 넘김)

# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

# Helper
helper = tf.contrib.seq2seq.TrainingHelper(
decoder_emb_inp, decoder_lengths, time_major=True)
# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_cell, helper, encoder_state,
output_layer=projection_layer)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
logits = outputs.rnn_output

여기서 decoder인 BasicDecoder의 중심 부분이다. encoder_cell 처럼 decode_cell을 받고, helper와 이전 encoder_state를 inputs으로 받는다. decoder와 helper를 분리시킴으로써 다른 codebases를 이용 할 수 있게 된다. (e.g. TrainingHelper는 greedy decoding을 위한 GreedyEmbeddingHelper로 교체 될 수 있다.)

마지막으로 projection_layer는 dense matrix로 hidden states를 길이가 V인 logit vector로 바꾸는 역할을 한다.

projection_layer = layers_core.Dense(
tgt_vocab_size, use_bias=False)



4) Loss

위에서 얻은 logits 을 통해 training_loss를 계산하게 된다.

crossent = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=decoder_outputs, logits=logits)
train_loss = (tf.reduce_sum(crossent * target_weights) /
batch_size)

여기서 target_weights는 decoder_outputs과 사이즈가 같은 zero-one matrix 인데, target sequence 보다 큰 값을 0으로 채움으로써 mask하는 효과를 갖는다. 이렇게 되면 불필요한 vector는 loss를 계산할때 포함하지 않게된다.

batch_size로 loss를 나누는 것은 매우 중요하고, 그래야 hyperparameters가 batch_size에 변함이 없어진다. 몇몇 사람은 loss를 batch_size * num_time_steps로 나누는데 이는 짧은 문장에 대한 오류를 낮춘다.


5) Gradient computation & optimisation

지금까지 NMT의 foward pass를 정의했다. back-prop을 계산하는 것은 코드 몇 줄이면 된다.

# Calculate and clip gradients
params = tf.trainable_variables()
gradients = tf.gradients(train_loss, params)
clipped_gradients, _ = tf.clip_by_global_norm(
gradients, max_gradient_norm)

RNN을 학습시키는 중요한 단계 중 하나는 global norm으로 자르는 gradient_clipping이다. 가장 큰 값인 max_gradient_norm은 종종 5나 1같은 값이 선택된다. 마지막 방법은 optimiser를선정하는 법인다 lr은 보통 0.0001 에서 0.001이고 training이 진행 될 수록 감소 되게 설정할 수 있다.

# Optimization
optimizer = tf.train.AdamOptimizer(learning_rate)
update_step = optimizer.apply_gradients(
zip(clipped_gradients, params))

이 실험에서는 learning rate가 시간에 지나면서 점점 감소하는 SGD를 사용하는 것이 더 좋은 성능을 냈다.


6) Inference – How to generate translations

training과 inference(testing)은 확연하게 다른 점이 있은 source sentence(encoder_inputs)만 이용한다는 것이다. decoding하는 방법에는 greedy, sampling, beam-search decoding 등 여러가지가 존재한다. (여기서는 ㅎreedy decoding을 다룬다.)

아이디어는 간단하고 밑에 그림에 나타나 있다.


step-1) 학습했던 방식과 같은 방식으로 source sentence를 통해 encoder_state를 얻어내고 이 encoder_state는 decoder를 초기화하기 위해 사용된다.


step-2) decoding(번역) 과정은 starting symbol(tgt-sos-id)인 <s>를 받자마자 시작된다.

step-3) decoder의 각각의 스텝마다 RNN의 결과를 하나의 logits으로 다룬다. logits 중에서 가장 높은 값을 가진 logit을 여기서 가장 확률이 높은 단어를 선택해서 내보낸다. (greedy behaviour) 예를 들어, 밑 그림에서 첫 decoding step 중 "moi"는 가장 높은 확률을 가진다. 그 다음 "moi"는 다음 step에서 input으로 사용된다.

step-4) 이 과정은 마지막 end-of-sentence 토큰인 "</s>"(tgt_eos_id)가 output으로 나올 때까지 계속된다.


[greedy decoidng] 

NMT 모델이 원 문장으로 부터 "Je suis etudiant"를 greedy search를 사용해서 만들어 내는 예시


step-3에서 training과 inference가 달라지게 된다. target words를 항상 input으로 제공하는 것과 다르게 inference에서는 모델이 예측한 단어들을 사용한다. 이것은 greedy decoding하는 문장이고 이것은 decoder를 training하는것과 매우 비슷하다.

# Helper
helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
embedding_decoder,
tf.fill([batch_size], tgt_sos_id), tgt_eos_id)

# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_cell, helper, encoder_state,
output_layer=projection_layer)

# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(
decoder, maximum_iterations=maximum_iterations)
translations = outputs.sample_id

여기서는 target sequence length를 모르기 때문에 TrainingHelper 대신 GreedyEmbeddingHelper가 사용됐고, 번역 길이를 제한하기 위해 maximum_iterations를 사용했다. 주로 휴리스틱하게 원 문장 길이의 2배까지로 제한한다.


maximum_iterations = tf.round(tf.reduce_max(source_sequence_length) * 2)

cat > /tmp/my_infer_file.vi
# (copy and paste some sentences from /tmp/nmt_data/tst2013.vi)

python -m nmt.nmt \
--out_dir=/tmp/nmt_model \
--inference_input_file=/tmp/my_infer_file.vi \
--inference_output_file=/tmp/nmt_model/output_infer

cat /tmp/nmt_model/output_infer # To view the inference as output



3. Intermediate

attention mechanism의 주요한 아이디어는 번역할 때 관련있는 source content에 "attention"을 줌으로써 target 문장과 source 문장 사이에 connection을 만드는 것이다. attention mechanism에서 얻은 또 다른 이점은 쉽게 source와 target과의 관계를 쉽게 시각화할 수 있다는 것이다.

[attention visualisation]


attention이 적용안된 vanilla seq2seq 모델에서는 decoding이 시작되기 전에 마지막 source state만 encoder에서 decoder로 전달한다. 이러한 모델은 짧거나 길지 않은 문장에서는 잘 동작하지만 문장이 길어지면 meaning vector로 사용되는 고정된 길이의 hidden state는 bottleneck이 된다. attention mechanism은 마지막 source state 뿐만 아니라 각각의 source RNN에 있는 hidden states를 무시하지 않고 decoder가 번역을 할때 주요하게 접근해야할 단어가 무엇인지를 알려준다. 현재 attention mechanism은 정석처럼 쓰이고 많은 태스크에서 성공적으로 적용된다 (image caption generation, speech recognition, text summarisation)


1) Background on the Attention Mechanism


[attention mechanism] attention-based NMT : attention이 계산되는 첫번째 단계 강조함

위 그림 처럼, attention 계산은 decoder 매 timestep 마다 일어난다. 다음과 같은 단계를 거친다.

step-1 현재 target hidden state는 모든 source state와 함께 비교되고 attention weights를 유도한다. (fig 4처럼 시각화 될 수 있음)

step-2 attention weights를 기반으로 source states의 가중 평균인 context vector를 계산한다.

step-3 context vector와 현재의 hidden state를 조합하여 최종 attention vector를 만든다.

step-4 attention vector는 다음 step의 input으로 사용된다. 




scoreht(target hidden state)와 ^hs(source hidden state)를 비교하기 위해 사용된다. 결과는 attention weights로 정규화 된다. 많은 종류의 scoring function 이 존재하는데 그 중에서 multiplicative 과 additive 형태가 자주 쓰인다. attention vector가 계산되고 난 이후에, at는 softmax logit과 loss를 유도하기 위해 사용된다. 이것은 vanilla seq2seq모델의 tartget hidden state와 비슷하다. 또한 function f는 다른 형태를 가질 수 있다.



2) What matters in the attention mechanism?

위의 식에서 말했듯이 많은 종류의 attention들이 존재한다. 이 attention들은 scoring functionattention function에 의해 달라지고 이전 상태 ht-1은 ht를 대신해서 scoring function에서 사용된다. 


많은 실험을 통해 이와 관련된 몇가지 특이 사항이 확인됐는데, 첫 번째 attention의 기본 형태 (source와 target간의 직접적인 연결)는 제시되야 된다. 두번째로 과거의 attention에 관한 네트워크를 알리기위해 attention vector를 다음 step에 제공하는 것은 중요하다. 마지막으로 scoring function의 선택들은 종종 다른 결과를 낳는다.




REF

https://github.com/tensorflow/nmt