[Pytorch 로 Question Generation 구현해보기] Learning to Ask : Neural Question Generation for Reading Comprehension
Pytorch는 Define-by-Run 방식으로 유연한 딥러닝 프레임워크를 제공하여, 많은 이들의 사랑을 받고 있다. 이번 글에서는 Torch와 TensorFlow 로는 구현되어 있으나, 아직 Pytorch 로 구현되 있지 않은 Du et al.,( 2017, ACL ) <https://arxiv.org/pdf/1705.00106.pdf> 을 구현해보고자 한다.
Du et al.,( 2017 ) 은 문장 또는 문단을 인풋으로 받아 자연스러운 질문을 생성하는 Question Generation ( QG ) 모델을 만드는 것을 목표로 한다. QG 모델은 이전까지 룰베이스 Mitkov and HA ( 2003 ) ; Rus et al.( 2010 ) 로 접근한 문제이다. 입력 문장을 문법에 맞춰서 재배치한 후, 미리 사람이 작성한 질문 형식에 대입하는 규칙기반의 시스템이다. Du et al.,( 2017 ) 은 처음으로 Question Generation ( QG ) 모델에 시퀀스 투 시퀀스를 적용하고, 문법 기반이 아닌 의미 기반의 질문 생성 모델을 연구한 데에 의의가 있다.
QG 모델이 처음 등장하게 된 배경은 챗봇에 필요한 데이터셋을 수집하는 작업에 어려움이 있었기 때문이다. 현재까지의 모든 QA 챗봇들은 사람이 직접 입력한 질문과 답변을 통해 개발이 되었다. 이러한 과정은 비용이 많이 드는 작업이다. 하지만, 만약 문서로부터 질문과 답변을 자동으로 추출할 수 있다면, 서비스의 유저 가이드 문서 등으로부터 바로 QA 챗봇을 만드는데 기여를 할 수 있게 된다. 챗봇 QA 데이터셋 Task는 문서로부터 질문을 생성하는 QG 모델과 질문이 주어졌을 때 문서로부터 답변을 얻는 QA 모델로 나눌 수 있다. Xiao et al.,( 2018 ).Dual Ask-Answer Network for Machine Reading Comprehension 은 QA 모델과 QG 모델을 하나로 병합하여 학습을 하는 방안을 제안하기도 하였다.
Prelims
Model Architecture
Du et al.,( 2017 ) 에서 질문 생성 모델은 LSTM Encoder-Decoder 구조 Bahdanau et al.,( 2015 ); Cho et al.,( 2014 ) 를 사용한다. 또한, 인간이 문장에서 질문을 만들 때, 특정 부분에 집중한다는 점에 착안하여, Global 어텐션 메커니즘 Luong et al., ( 2015a ) 을 채택하였다. 사람이 문장으로 부터 질문을 만드는 메커니즘과 유사하게 인풋 문장의 특정 부분에만 집중하여 질문을 생성할 수 있도록 설계되었다.
Encoder
Du et al.,( 2017 ) 에서는 Encoding 과정에서 Bi-directional LSTM 을 사용한다. Bi-directional LSTM 은 양방향으로 학습함으로써 기존의 LSTM 이 가지는 한계점을 극복하였다. 다음 두 문장에서 보듯이, 미래 시점의 정보가 현재의 정보를 파악하는 데에 모호함을 줄이고, 문맥을 더 효과적으로 이해할 수 있다는 것을 확인할 수 있다.
앞에서부터 순방향으로 He said, Teddy 를 읽었을 때, Teddy 가 의미하는게 곰인지 대통령인지 알 수 없다. Teddy 에 관한 내용이 언급되지 않았기 때문이다. 하지만 역방향으로 읽으면 Teddy 에 대한 추론이 가능하다. 이처럼 문장을 역방향으로 넣으면, 미래 시점으로부터 Teddy에 대한 정보를 알 수 있다는 점이 Bi-directional LSTM 이 출현된 배경이다.
Du et al.,( 2017 ) 에서 제안한 Bi-directional LSTM 인코더를 쓰도록 한다. torch.nn.LSTM에 bidirectional에 True 인자값을 주어 구현한다. LSTM 은 Encoder 의 마지막 hidden state 인 output과, Encoding 과정의 모든 hidden 값을 출력한다.
Decoder
Du et al.,( 2017 ) 은 Decoder 는 인풋으로 들어오는 문장 ( x ) 과 이전까지 예측한 단어 ( y<t ) 를 기반으로 다음에 올 단어 ( yt ) 를 예측하며 질문을 생성해 간다.
Attention
본 논문의 구현에는 Global Attention Mechanism Luong et al., ( 2015a )<http://aclweb.org/anthology/D15-1166> 을 사용한다. 기존 Local attention 과 다른 점은 Attention의 입력되는 hidden state의 time step이다. 기존에는 Decoder 이전 time step에서의 hidden state를 받아서 Alignment를 계산하였다. Global Attention 에서는 Decoder time step 에서의 hidden state를 이용하여 Alignment를 계산한다.
본 구현에서는 3가지 방식 중 1가지를 선택하여 Score를 계산한다. Dot 방식, General 방식, 그리고 concat 방식이 있다.
Du et al., ( 2017 ) 에서 인풋으로 들어오는 문장의 특정 부분에 집중하기 위하여 general method를 사용한다. 하지만, 본 튜토리얼에서는 위 3가지 방식을 테스트해 본 결과, 성능이 가장 좋았던 dot 방식을 사용한다. 이렇게 구한 Score를 토대로 아래의 Alignment ( a i,t )를 계산한다. 이렇게 구한 Alignment 를 Encoder 의 Output과 weighted sum 을 하여 Context Vector 를 구한다. Decoder step 에서 Context Vector 와 Decoder 의 output 과 concat 하여 tanh 을 통과시킨 후 softmax를 취한다.
Du et al.,( 2017 ) 에서 Hyperparameter 설정은 다음과 같다. LSTM 의 hidden unit 수는 600, Encoder layer와 Decoder layer 수는 2, Dropout은 0.3, 그리고 Mini-batch 사이즈는 64 이다.
Training Loop
Du et al.,( 2017 ) 는 Input 문장과 질문의 최대 길이는 50으로 하였다. Learning Rate 는 1.0으로 초기화한 후, 8 의 Epoch마다 0.5 씩 줄이었다. torch.optim.lr_scheduler StepLR 함수의 step_size 8, gamma 0.5 의 매개변수를 줌으로써 통해서 학습 속도를 조절한다. 최적화 방식은 Adam 을 사용한다.
Encoder에서 ouput과 hidden과 cell state의 tuple 형태로 받는다. 디코더에서도 마찬가지로, decoder_hidden 과 dicoder_cell_state를 tuple로 주고받는다.
A Real World Example
Du et al.,( 2017 ) 에서 사용한 데이터 소스는 SQuAD Rajpurkar et al., <https://arxiv.org/pdf/1606.05250.pdf> 이다. SQuAD 의 70,484 개의 문장과 질문의 쌍을 훈련시켰다. Xiao et al., <http://arxiv.org/pdf/1809.01997.pdf> 에서는 SQuAD 데이터 셋 뿐만 아니라 MS MARCO, WikiQA, TriviaQA 와 같은 다양한 데이터 소스를 활용하였다. 이러한 점에 착안하여, 본 실험에서는 3개의 데이터 소스를 확보하였고, 총 162,021 쌍의 문장과 질문의 쌍을 모델이 학습하도록 하였다. QA 데이터들은 아래에서 다운받을 수 있다.
Preprocessing
본 실험에서는 SQuAD 2.0, MS MARCO 와 WikiQA 데이터셋을 사용하였으며, 세가지 데이터 셋의 형식이 각각 다르므로, 이에 맞는 전처리 함수를 사용한다. SQuAD 데이터셋은 generatePairs_squad 함수를 사용하고, MS MARCO는 generatePairs_msmarco 함수, WikiQA는 generatePairs_wiki 함수를 통해서 (Sentence, Question) 쌍을 추출한다.
위에서 언급한 generatePairs_squad, generatePairs_msmarco, 그리고 generatePairs_wiki 함수로 데이터를 로드한다.
Du et al.,( 2017 ) 는 73,000 개의 단어 사전을 구축하였다. 본 실험에서는 SQuAD, MS MARCO 와 WikiQA 훈련 데이터셋으로 사전을 구축하였고, 총 123,321 개의 단어 수를 포함하고 있다. 워드임베딩은 Du et al.,( 2017 )와 마찬가지로, 사전에 훈련된 300차원의 glove 벡터를 사용한다. Glove 벡터에 차원 수가 맞지 않은 9개의 숫자 단어들은 제거하여 Float 로 변환시킬 수 있도록 하였다. glove_word_to_vec 은 단어와 단어 벡터 매핑 사전이다.
아래는 본 실험에서 생성한 질문을 사람, 룰베이스 시스템 Heilman and Smith (2010) H&S, 그리고 Du et al., 와 비교하여 평가하였다.
Sentence #1: the largest of these is the eldon square shopping centre , one of the largest city centre shopping complexes in the uk .
Human: what is one of the largest city center shopping complexes in the uk ?
H&S: what is the eldon square shopping centre one of ?
Du et al.,: what is one of the largest city centers in the uk ?
Ours: what is the largest city in UNK?
Sentence #2: tea , coffee , sisal , pyrethrum , corn , and wheat are grown in the fertile highlands , one of the most successful agricultural production regions in Africa.
Human: (1) where is the most successful agricultural prodcution regions ? (2) what is grown in the fertile highlands ?
H&S: what are grown in the fertile highlands in africa ?
Du et al.,: : what are the most successful agricultural production regions in africa ?
Ours: what country did the US have in the UNK?
Sentence #3: as an example , income inequality did fall in the united states during its high school movement from 1910 to 1940 and thereafter .
Human: during what time period did income inequality decrease in the united states ?
H&S: where did income inequality do fall during its high school movement from 1910 to 1940 and thereafter as an example ?
Du et al.,: when did income inequality fall in the us ?
Ours: what is the maximum range of UNK?
문장에서 질문을 생성하는 예제
Prelims
from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import torch from torch.jit import script, trace import torch.nn as nn from torch import optim import torch.nn.functional as F import csv import random import re import os import unicodedata import codecs from io import open import itertools import math import json from pprint import pprint import nltk from nltk import tokenize nltk.download('punkt') ################## # Device 선택 # ################## USE_CUDA = torch.cuda.is_available() device = torch.device("cuda" if USE_CUDA else "cpu") ################## # Parameter 셋팅 # ################## embedding_dim = 300Content
- Prelims
- Model Architecture
- Encoder
- Decoder
- Attention
- Training
- Training Loop
- Training Data and Batching
- A Real World Example
- Pre-processing
- Data Loading
- FastText
- Result
- Conclusion
Model Architecture
Du et al.,( 2017 ) 에서 질문 생성 모델은 LSTM Encoder-Decoder 구조 Bahdanau et al.,( 2015 ); Cho et al.,( 2014 ) 를 사용한다. 또한, 인간이 문장에서 질문을 만들 때, 특정 부분에 집중한다는 점에 착안하여, Global 어텐션 메커니즘 Luong et al., ( 2015a ) 을 채택하였다. 사람이 문장으로 부터 질문을 만드는 메커니즘과 유사하게 인풋 문장의 특정 부분에만 집중하여 질문을 생성할 수 있도록 설계되었다.
Encoder
Du et al.,( 2017 ) 에서는 Encoding 과정에서 Bi-directional LSTM 을 사용한다. Bi-directional LSTM 은 양방향으로 학습함으로써 기존의 LSTM 이 가지는 한계점을 극복하였다. 다음 두 문장에서 보듯이, 미래 시점의 정보가 현재의 정보를 파악하는 데에 모호함을 줄이고, 문맥을 더 효과적으로 이해할 수 있다는 것을 확인할 수 있다.
"He said, Teddy bears are on sale."
"He said, Teddy Roosevelt was a great President."
앞에서부터 순방향으로 He said, Teddy 를 읽었을 때, Teddy 가 의미하는게 곰인지 대통령인지 알 수 없다. Teddy 에 관한 내용이 언급되지 않았기 때문이다. 하지만 역방향으로 읽으면 Teddy 에 대한 추론이 가능하다. 이처럼 문장을 역방향으로 넣으면, 미래 시점으로부터 Teddy에 대한 정보를 알 수 있다는 점이 Bi-directional LSTM 이 출현된 배경이다.
Bi-directional LSTM
Du et al.,( 2017 ) 에서 제안한 Bi-directional LSTM 인코더를 쓰도록 한다. torch.nn.LSTM에 bidirectional에 True 인자값을 주어 구현한다. LSTM 은 Encoder 의 마지막 hidden state 인 output과, Encoding 과정의 모든 hidden 값을 출력한다.
self.lstm = nn.LSTM(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout), bidirectional=True)Encoder 전체 코드이다.
class QG_EncoderRNN(nn.Module): def __init__(self, hidden_size, embedding, n_layers=1, dropout=0): super(EncoderRNN, self).__init__() self.n_layers = n_layers self.hidden_size = hidden_size self.embedding = embedding self.lstm = nn.LSTM(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout), bidirectional=True) def forward(self, input_seq, input_lengths, hidden=None): embedded = self.embedding(input_seq) packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths) outputs, hidden = self.lstm(packed, hidden) outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs) outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:] return outputs, hidden
Du et al.,( 2017 ) 은 Decoder 는 인풋으로 들어오는 문장 ( x ) 과 이전까지 예측한 단어 ( y<t ) 를 기반으로 다음에 올 단어 ( yt ) 를 예측하며 질문을 생성해 간다.
QG Decoder
구체적으로는 다음과 같다.
QG Decoder의 수식 표현
위 수식의 ht는 Decoder 의 마지막 hidden 벡터이다. 이를 구현하면, 아래와 같다. hidden 은 Decoding step에서의 모든 hidden 벡터가 들어있으며, rnn_output은 가장 마지막 hidden 벡터를 출력하여, 수식에서의 ht 이다.
rnn_output, hidden = self.lstm(embedded, last_hidden)
이렇게 구한 rnn_output인 Decoder의 마지막 hidden state와 Encoder의 마지막 hidden state ( encoder_output )를 가중평균하여 어텐션 context vector ( ct ) 를 얻는다.
Input의 Context Vector
context vector는 input으로 들어온 문장의 의미 정보를 담고 있다. encoder_output은 Encoder 파트에 설명되어 있다. 이렇게 얻은 어텐션과 인코더 LSTM 의 마지막 output인 rnn_output ( ht ) 를 길게 concat 하여 softmax 와 tanh 함수를 써서 최종 결과를 출력한다.
class QG_AttnDecoderRNN(nn.Module): def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.3): super(QG_AttnDecoderRNN, self).__init__() self.attn_model = attn_model self.hidden_size = hidden_size self.output_size = output_size self.n_layers = n_layers self.dropout = dropout # Layer 명시 self.embedding = embedding self.embedding_dropout = nn.Dropout(dropout) self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout)) self.concat = nn.Linear(hidden_size * 2, hidden_size) self.out = nn.Linear(hidden_size, output_size) self.attn = Attn(attn_model, hidden_size) def forward(self, input_step, last_hidden, encoder_outputs): # 인풋으로 들어간 문장에 대한 단어단위의 임베딩을 불러옴. embedded = self.embedding(input_step) embedded = self.embedding_dropout(embedded) # 이전까지의 히든 스테이트와 직전의 아웃풋을 LSTM 에 넣어 현재 상태의 히든 값을 계산. rnn_output, hidden = self.gru(embedded, last_hidden) # 어텐션을 적용한 Ct 에 해당. attn_weights = self.attn(rnn_output, encoder_outputs) context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # squeeze 는 1 차원으로 되어 있는 모든 축을 지움. rnn_output = rnn_output.squeeze(0) context = context.squeeze(1) # 마지막 히든과 어텐션 concat concat_input = torch.cat((rnn_output, context), 1) # 첫번쨰 선형변환 concat_output = torch.tanh(self.concat(concat_input)) # 두번째 선현변환 output = self.out(concat_output) output = F.softmax(output, dim=1) # 아웃풋과 마지막 히든상태를 반환함. return output, hidden
Attention
본 논문의 구현에는 Global Attention Mechanism Luong et al., ( 2015a )<http://aclweb.org/anthology/D15-1166> 을 사용한다. 기존 Local attention 과 다른 점은 Attention의 입력되는 hidden state의 time step이다. 기존에는 Decoder 이전 time step에서의 hidden state를 받아서 Alignment를 계산하였다. Global Attention 에서는 Decoder time step 에서의 hidden state를 이용하여 Alignment를 계산한다.
Global Attention Mechanism
본 구현에서는 3가지 방식 중 1가지를 선택하여 Score를 계산한다. Dot 방식, General 방식, 그리고 concat 방식이 있다.
에텐션 Score 공식
Du et al., ( 2017 ) 에서 인풋으로 들어오는 문장의 특정 부분에 집중하기 위하여 general method를 사용한다. 하지만, 본 튜토리얼에서는 위 3가지 방식을 테스트해 본 결과, 성능이 가장 좋았던 dot 방식을 사용한다. 이렇게 구한 Score를 토대로 아래의 Alignment ( a i,t )를 계산한다. 이렇게 구한 Alignment 를 Encoder 의 Output과 weighted sum 을 하여 Context Vector 를 구한다. Decoder step 에서 Context Vector 와 Decoder 의 output 과 concat 하여 tanh 을 통과시킨 후 softmax를 취한다.
에텐션 가중치
#Attention layer class QG_AttnDecoderRNN(nn.Module): def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.3): super(QG_AttnDecoderRNN, self).__init__() self.attn_model = attn_model self.hidden_size = hidden_size self.output_size = output_size self.n_layers = n_layers self.dropout = dropout # Layer 명시 self.embedding = embedding self.embedding_dropout = nn.Dropout(dropout) self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout)) self.concat = nn.Linear(hidden_size * 2, hidden_size) self.out = nn.Linear(hidden_size, output_size) self.attn = Attn(attn_model, hidden_size) def forward(self, input_step, last_hidden, encoder_outputs): #input step의 값을 embedding 레이어의 입력값으로 넣음 embedded = self.embedding(input_step) embedded = self.embedding_dropout(embedded) # 이번 time step의 input값과 이전 time step의 히든레이어를 gru의 입력값으로 넣음 rnn_output, hidden = self.gru(embedded, last_hidden) # 어텐션 레이어에 현재 time step의 rnn_output과 encoder_outputs를 넣어 Attention weights(Alignments) 계산 attn_weights = self.attn(rnn_output, encoder_outputs) # 윗줄에서 구한 attn_weights를 이용하여 encoder_outputs와 Weighted sum context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # squeeze 는 차원값이 1인 영역을 지움. rnn_output = rnn_output.squeeze(0) context = context.squeeze(1) # rnn_output과 context vector를 concat하고 tanh의 입력값으로 넣음 concat_input = torch.cat((rnn_output, context), 1) concat_output = torch.tanh(self.concat(concat_input)) # output에 softmax를 통해 현재 time step의 단어를 예측 output = self.out(concat_output) output = F.softmax(output, dim=1) # 아웃풋과 마지막 히든상태를 반환함. return output, hiddenTraining
Du et al.,( 2017 ) 에서 Hyperparameter 설정은 다음과 같다. LSTM 의 hidden unit 수는 600, Encoder layer와 Decoder layer 수는 2, Dropout은 0.3, 그리고 Mini-batch 사이즈는 64 이다.
attn_model = 'general' hidden_size = 600 encoder_n_layers = 2 decoder_n_layers = 2 dropout = 0.3 batch_size = 64Encoder 와 Decoder 를 생성하고, 적합한 기기를 사용한다.
encoder = QG_EncoderRNN(hidden_size, embedding, encoder_n_layers, dropout) decoder = QG_AttnDecoderRNN(attn_model, embedding, hidden_size, voc.num_words, decoder_n_layers, dropout) if loadFilename: encoder.load_state_dict(encoder_sd) decoder.load_state_dict(decoder_sd) # 적합한 기기에 값을 올려놓습니다. encoder = encoder.to(device) decoder = decoder.to(device)
Training Loop
Du et al.,( 2017 ) 는 Input 문장과 질문의 최대 길이는 50으로 하였다. Learning Rate 는 1.0으로 초기화한 후, 8 의 Epoch마다 0.5 씩 줄이었다. torch.optim.lr_scheduler StepLR 함수의 step_size 8, gamma 0.5 의 매개변수를 줌으로써 통해서 학습 속도를 조절한다. 최적화 방식은 Adam 을 사용한다.
clip = 50.0 teacher_forcing_ratio = 1.0 learning_rate = 1.0 decoder_learning_ratio = 5.0 n_iteration = 4000 print_every = 1 save_every = 300 MAX_LENGTH = 50 encoder.train() decoder.train() print('최적화방식 셋팅') encoder_optimizer = optim.Adam(encoder.parameters(), lr=learning_rate) decoder_optimizer = optim.Adam(decoder.parameters(), lr=learning_rate) if loadFilename: encoder_optimizer.load_state_dict(encoder_optimizer_sd) decoder_optimizer.load_state_dict(decoder_optimizer_sd) # Run training iterations print("훈련 시작!") trainIters(model_name, voc, pairs, encoder, decoder, encoder_optimizer, decoder_optimizer, embedding, encoder_n_layers, decoder_n_layers, save_dir, n_iteration, batch_size, print_every, save_every, clip, corpus_name, loadFilename)훈련 함수는 4000번 반복하며, trainIters 함수는 다음과 같다.
def trainIters(model_name, voc, pairs, encoder, decoder, encoder_optimizer, decoder_optimizer, embedding, encoder_n_layers, decoder_n_layers, save_dir, n_iteration, batch_size, print_every, save_every, clip, corpus_name, loadFilename): training_batches = [batch2TrainData(voc, [random.choice(pairs) for _ in range(batch_size)]) for _ in range(n_iteration)] print('초기화') start_iteration = 1 print_loss = 0 if loadFilename: start_iteration = checkpoint['iteration'] + 1 encoder_scheduler = optim.lr_scheduler.StepLR(encoder_optimizer, step_size = 8, gamma = 0.5) decoder_scheduler = optim.lr_scheduler.StepLR(encoder_optimizer, step_size = 8, gamma = 0.5) print("훈련중") for iteration in range(start_iteration, n_iteration + 1): encoder_scheduler.step() decoder_scheduler.step() training_batch = training_batches[iteration - 1] input_variable, lengths, target_variable, mask, max_target_len = training_batch loss = train(input_variable, lengths, target_variable, mask, max_target_len, encoder, decoder, embedding, encoder_optimizer, decoder_optimizer, batch_size, clip) print_loss += loss if iteration % print_every == 0: print_loss_avg = print_loss / print_every print("반복: {}; 진행상황: {:.1f}%; 평균 손실: {:.4f}".format(iteration, iteration / n_iteration * 100, print_loss_avg)) print_loss = 0Training Data and Batching
Encoder에서 ouput과 hidden과 cell state의 tuple 형태로 받는다. 디코더에서도 마찬가지로, decoder_hidden 과 dicoder_cell_state를 tuple로 주고받는다.
def train(input_variable, lengths, target_variable, mask, max_target_len, encoder, decoder, embedding, encoder_optimizer, decoder_optimizer, batch_size, clip, max_length=MAX_LENGTH): encoder_optimizer.zero_grad() decoder_optimizer.zero_grad() input_variable = input_variable.to(device) lengths = lengths.to(device) target_variable = target_variable.to(device) mask = mask.to(device) loss = 0 print_losses = [] n_totals = 0 # 인코더 encoder_outputs, (encoder_hidden, encoder_cell_state) = encoder(input_variable, lengths) decoder_input = torch.LongTensor([[SOS_token for _ in range(batch_size)]]) decoder_input = decoder_input.to(device) # 디코더의 초기 hidden state 는 인코더의 마지막 hidden state decoder_hidden = encoder_hidden[:decoder.n_layers] decoder_cell_state = encoder_cell_state[:decoder.n_layers] # teacher forcing use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False if use_teacher_forcing: for t in range(max_target_len): decoder_output, (decoder_hidden, decoder_cell_state) = decoder( decoder_input, (decoder_hidden, decoder_cell_state), encoder_outputs ) decoder_input = target_variable[t].view(1, -1) mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t]) loss += mask_loss print_losses.append(mask_loss.item() * nTotal) n_totals += nTotal else: for t in range(max_target_len): decoder_output, decoder_hidden = decoder( decoder_input, decoder_hidden, encoder_outputs ) _, topi = decoder_output.topk(1) decoder_input = torch.LongTensor([[topi[i][0] for i in range(batch_size)]]) decoder_input = decoder_input.to(device) mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t]) loss += mask_loss print_losses.append(mask_loss.item() * nTotal) n_totals += nTotal # backpropatation 수행 loss.backward() # Clip gradients _ = torch.nn.utils.clip_grad_norm_(encoder.parameters(), clip) _ = torch.nn.utils.clip_grad_norm_(decoder.parameters(), clip) encoder_optimizer.step() decoder_optimizer.step() return sum(print_losses) / n_totals
A Real World Example
Du et al.,( 2017 ) 에서 사용한 데이터 소스는 SQuAD Rajpurkar et al., <https://arxiv.org/pdf/1606.05250.pdf> 이다. SQuAD 의 70,484 개의 문장과 질문의 쌍을 훈련시켰다. Xiao et al., <http://arxiv.org/pdf/1809.01997.pdf> 에서는 SQuAD 데이터 셋 뿐만 아니라 MS MARCO, WikiQA, TriviaQA 와 같은 다양한 데이터 소스를 활용하였다. 이러한 점에 착안하여, 본 실험에서는 3개의 데이터 소스를 확보하였고, 총 162,021 쌍의 문장과 질문의 쌍을 모델이 학습하도록 하였다. QA 데이터들은 아래에서 다운받을 수 있다.
실험 Datasets
Dataset | # (Sentence, Question) pairs |
SQuAD | 132,680 |
MS MARCO | 27,978 |
WikiQA | 1,363 |
Total | 162,021 |
- SQuAD 2.0 : https://rajpurkar.github.io/SQuAD-explorer/
- MS MARCO : http://www.msmarco.org/
- WikiQA : https://www.microsoft.com/en-us/download/details.aspx?id=52419
- TriviaQA : http://nlp.cs.washington.edu/triviaqa
Preprocessing
본 실험에서는 SQuAD 2.0, MS MARCO 와 WikiQA 데이터셋을 사용하였으며, 세가지 데이터 셋의 형식이 각각 다르므로, 이에 맞는 전처리 함수를 사용한다. SQuAD 데이터셋은 generatePairs_squad 함수를 사용하고, MS MARCO는 generatePairs_msmarco 함수, WikiQA는 generatePairs_wiki 함수를 통해서 (Sentence, Question) 쌍을 추출한다.
def loadJsonData(file, n=10): with open(file, 'rb') as datafile: jsonData = json.loads(datafile.read()) return jsonData def loadTextData(file, n=10): with open(file, 'r') as datafile: content = datafile.readlines() content = [x.strip() for x in content] return content def insertDash(string, index): return string[:index] + '+++$+++' + string[index:] #context로부터 답변이 포함된 문장을 추출한다. def findRelatedSentence(string, index): markedSentence = insertDash(string, index) sentenceList = tokenize.sent_tokenize(markedSentence) markedSentenceList = [x for x in sentenceList if '+++$+++' in x] relatedSentence = markedSentenceList[0].replace('+++$+++', '') return relatedSentence # input 문장의 가변 길이를 고정 길이로 변환하며, MAX_LENGTH는 10 def filterPair(p): # Input sequences need to preserve the last word for EOS token return len(p[0].split(' ')) < MAX_LENGTH and len(p[1].split(' ')) < MAX_LENGTH def filterPairs(pairs): return [pair for pair in pairs if filterPair(pair)] def contextToSent(contextList): sentList = [] for context in contextList: sent_ = tokenize.sent_tokenize(context) sentList.extend(sent_) return sentList #SQuAD QA 데이터 셋을 (문장 - 질문) 의 쌍으로 만드는 함수 def generatePairs_squad(squad_train): myPairs = [] contextList = [] for dataIdx, dataElem in enumerate(squad_train['data']): for paragraphIdx, paragraphElem in enumerate(dataElem['paragraphs']): myDocument = paragraphElem['context'] contextList.append(myDocument) for qasIdx, qasElem in enumerate(paragraphElem['qas']): myQuestion = qasElem['question'] if(not qasElem['is_impossible']): mySentence = findRelatedSentence(myDocument, qasElem['answers'][0]['answer_start']) myPair = [mySentence, myQuestion] else: try: mySentence = findRelatedSentence(myDocument, qasElem['plausible_answers'][0]['answer_start']) myPair = [mySentence, myQuestion] except IndexError : pass myPairs.append(myPair) myPairs = filterPairs(myPairs) print("SQuAD 데이터 셋에 총 {!s} 개의 문장, 질문의 쌍이 있습니다.".format(len(myPairs))) return myPairs, contextList #MS MARCO QA 데이터 셋을 (문장 - 질문) 의 쌍으로 만드는 함수 def generatePairs_msmarco(msmarco_train): myPairs = [] myPassage_ = [] myQuestion_ = [] for passageKey, passageVal in msmarco_train['passages'].items(): for passageCandidate in passageVal: if(passageCandidate['is_selected']): myPassage = passageCandidate['passage_text'] myPassage_.append(myPassage) for queryElem in msmarco_train['query'].items(): myQuestion = queryElem[1] myQuestion_.append(myQuestion) myPairs = list(map(list, zip(myPassage_,myQuestion_))) print("MS MARCO 데이터 셋에 총 {!s} 개의 문장, 질문의 쌍이 있습니다.".format(len(myPairs))) return myPairs #WIKI QA 데이터 셋을 (문장 - 질문) 의 쌍으로 만드는 함수 def generatePairs_wiki(wiki_train): myPairs = [] for data in wiki_train: elem = data.split('\t') if elem[2] is '1': myPairs.append([elem[1], elem[0]]) return myPairsData Loading
위에서 언급한 generatePairs_squad, generatePairs_msmarco, 그리고 generatePairs_wiki 함수로 데이터를 로드한다.
# SQuAD 데이터셋 corpus_squad_name = "squad" corpus_squad = os.path.join("inputData", corpus_squad_name) squad_train = loadTrainData(os.path.join(corpus_squad, "train-v2.0.json")) squad_dev = loadJsonData(os.path.join(corpus_squad, "dev-v2.0.json")) # MSMARCO 데이터셋 # MSMARCO의 train 데이터는 answer만 가지고 있다. corpus_msmarco_name = "msmarco" corpus_msmarco = os.path.join("inputData", corpus_msmarco_name) msmarco_dev = loadJsonData(os.path.join(corpus_msmarco, "dev_v2.1.json")) #msmarco_eval = loadTrainData(os.path.join(corpus_msmarco, "eval_v2.1_public.json")) # WikiQA 데이터셋 corpus_wiki_name = "wikiqa" corpus_wiki = os.path.join("inputData", corpus_wiki_name) wiki_train = loadTextData(os.path.join(corpus_wiki, "WikiQA-train.txt")) wiki_dev = loadTextData(os.path.join(corpus_wiki, "WikiQA-dev.txt")) wiki_test = loadTextData(os.path.join(corpus_wiki, "WikiQA-test.txt"))( Sentence, Question ) 의 쌍을 inputPairs 에 담는다.
inputPairs_squad_train, contextList = generatePairs_squad(squad_train) inputPairs_squad_dev, contextList = generatePairs_squad(squad_dev) inputPairs_msmarco_dev = generatePairs_msmarco(msmarco_dev) inputPairs_wiki_train = generatePairs_wiki(wiki_train) inputPairs_wiki_dev = generatePairs_wiki(wiki_dev) inputPairs_wiki_test = generatePairs_wiki(wiki_test)FastText
Du et al.,( 2017 ) 는 73,000 개의 단어 사전을 구축하였다. 본 실험에서는 SQuAD, MS MARCO 와 WikiQA 훈련 데이터셋으로 사전을 구축하였고, 총 123,321 개의 단어 수를 포함하고 있다. 워드임베딩은 Du et al.,( 2017 )와 마찬가지로, 사전에 훈련된 300차원의 glove 벡터를 사용한다. Glove 벡터에 차원 수가 맞지 않은 9개의 숫자 단어들은 제거하여 Float 로 변환시킬 수 있도록 하였다. glove_word_to_vec 은 단어와 단어 벡터 매핑 사전이다.
####################### # Glove EMBEDDING # ####################### import numpy as np glove_dir = './inputData/glove/' glove_word_to_vec = {} f = open(os.path.join(glove_dir, 'glove.840B.300d.txt'), encoding="utf8") for line in f: values = line.split() word = values[0] try: if(len(values[1:]) == 299): print(word) elif(len(values[1:]) == 300): coefs = np.asarray(values[1:], dtype='float32') glove_word_to_vec[word] = coefs except ValueError : diff = len(values) - 301 coefs = np.asarray(values[diff+1:], dtype='float32') glove_word_to_vec[word] = coefs f.close()위에서 생성한 glove_word_to_vec 에서 단어들의 리스트 embeddingList 를 생성하고, 단어 사전을 반환한다.
def loadPrepareGloveVoc(total_word_to_vec): embeddingList = [] voc = Voc('squad') for word in total_word_to_vec: embeddingList.append(total_word_to_vec[word]) voc.addWord(word) print("Counted words:", voc.num_words) return embeddingList, vocnn.Embedding.from_pretrained 를 사용하여 사전에 훈련된 임베딩을 얻는다.
embedding = nn.Embedding.from_pretrained(torch.FloatTensor(embeddingList))Results
아래는 본 실험에서 생성한 질문을 사람, 룰베이스 시스템 Heilman and Smith (2010) H&S, 그리고 Du et al., 와 비교하여 평가하였다.
Sentence #1: the largest of these is the eldon square shopping centre , one of the largest city centre shopping complexes in the uk .
Human: what is one of the largest city center shopping complexes in the uk ?
H&S: what is the eldon square shopping centre one of ?
Du et al.,: what is one of the largest city centers in the uk ?
Ours: what is the largest city in UNK?
Sentence #2: tea , coffee , sisal , pyrethrum , corn , and wheat are grown in the fertile highlands , one of the most successful agricultural production regions in Africa.
Human: (1) where is the most successful agricultural prodcution regions ? (2) what is grown in the fertile highlands ?
H&S: what are grown in the fertile highlands in africa ?
Du et al.,: : what are the most successful agricultural production regions in africa ?
Ours: what country did the US have in the UNK?
Sentence #3: as an example , income inequality did fall in the united states during its high school movement from 1910 to 1940 and thereafter .
Human: during what time period did income inequality decrease in the united states ?
H&S: where did income inequality do fall during its high school movement from 1910 to 1940 and thereafter as an example ?
Du et al.,: when did income inequality fall in the us ?
Ours: what is the maximum range of UNK?
Conclusion
Du et al.,( 2017 ). Learning to Ask: Neural Question Generation for Reading Comprehension 을 Pytorch 로 구현한 전체 코드는 Github <https://github.com/MinyeLee/Question-Generation-Pytorch> 에서 확인이 가능하다. 본 소스코드를 구현하는데 Pytorch 튜토리얼을 참고하였다. Dok2.QA 팀은 QG 모델과 QA 모델을 합치어 하나의 문서에 대해 Question 과 Answer를 생성하는 End-to-End 모델을 생성하는 것을 목표로 연구를 하고있다.
Du et al.,( 2017 ). Learning to Ask: Neural Question Generation for Reading Comprehension 을 Pytorch 로 구현한 전체 코드는 Github <https://github.com/MinyeLee/Question-Generation-Pytorch> 에서 확인이 가능하다. 본 소스코드를 구현하는데 Pytorch 튜토리얼을 참고하였다. Dok2.QA 팀은 QG 모델과 QA 모델을 합치어 하나의 문서에 대해 Question 과 Answer를 생성하는 End-to-End 모델을 생성하는 것을 목표로 연구를 하고있다.
댓글
댓글 쓰기