티스토리 뷰
0. 개요
최근 많은 자연어처리 튜토리얼이나 딥러닝 튜토리얼 강의를 들어가보면 챗봇에 대한 수업을 많이 진행되는것 같습니다. pytorch 공식 튜토리얼 사이트에 괜찮은 챗봇 튜토리얼이 있어 pytorch도 익힐 겸 가볍게 경험해 보았습니다, 본 포스팅은 파이토치 챗봇 튜토리얼 사이트의 글과 코드를 기반으로 작성되었음을 밝힙니다. (제가 진행하였을 때는 1.0 버전이었는데 2019.05.11 기준 1.1 버전으로 바뀌었습니다.)
1. 모델
챗봇 튜토리얼의 모델은 인코더와 디코더를 가진 sequence to sequence 모델입니다. 인코더는 GRU(양방향)를 사용하며 2개의 층으로 구성되어 있습니다. 디코더는 GRU(단방향)를 사용하며, 2개의 층으로 구성되어 있습니다. 또한 다음 단어를 예측하는데 집중할 단어에 대해 가중치를 부여하는 어텐션 메커니즘(Attention Mechanism)을 사용하는 것을 볼 수 있습니다.
1.1 인코더(Encoder)
인코더는 입력 데이터에 대한 문맥 정보를 일정한 길이의 벡터로 바꿔주는 역할을 합니다. 시퀀스의 요약된 정보를 디코더로 잘 넘기는 것이 목표입니다. 이 모델의 인코더는 양방향 GRU(Bidirectional-Gated Recurrent Unit)를 사용하여 2개 층(layer)으로 구성되어있으며, 입력으로 input_seq(배치사이즈), input_lengths(각 입력 시퀀스 길이), hidden(히든 스테이트)를 받습니다. 코드와 모델 그림을 보며 좀 더 자세히 살펴보도록 하겠습니다.
class 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 # Initialize GRU; the input_size and hidden_size params are both set to 'hidden_size' # because our input size is a word embedding with number of features == hidden_size self.gru = nn.GRU(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): # Convert word indexes to embeddings embedded = self.embedding(input_seq) # Pack padded batch of sequences for RNN module packed = nn.utils.rnn.pack_padded_sequence(embedded, input_lengths) # Forward pass through GRU outputs, hidden = self.gru(packed, hidden) # Unpack padding outputs, _ = nn.utils.rnn.pad_packed_sequence(outputs) # Sum bidirectional GRU outputs outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:] # Return output and final hidden state return outputs, hidden
편의를 위해 첫 번째 양방향 GRU layer를 layer-1, 두 번째 양방향 GRU layer를 layer-2 라고 하겠습니다. 인코더가 값을 받아 계산하는 부분인 forward 부분이 어떻게 돌아가는지 살펴보겠습니다.
1) 임베딩된 입력 데이터는 layer-1으로 보내집니다. 효율적인 계산을 위해 packed_padded_sequence를 사용하였습니다.
2) 각 타임스텝에서 만들어진 히든 스테이트 벡터는 layer-2로 들어가게 됩니다. (코드에서는 layer-1 과 layer-2가 한번에 계산됩니다.)
3) 최종적으로 layer-2까지 통과한 각 타임스텝의 히든 스테이트 벡터 outputs 그리고
4) 각 층과 방향에서 마지막 히든스테이트에서 나온 문맥 벡터(1-Forward, 1-Backward, 2-Forward, 2-Backward = hidden)를 얻을 수 있습니다.
1.2 디코더(Decoder)
디코더는 인코더가 압축한 문맥 벡터를 받아 결과값을 도출합니다. 디코더의 처음 상태는 인코더의 마지막 스테이트 hidden입니다. 챗봇 튜토리얼의 디코더 모델은 단방향 GRU를 사용하며, 2개의 층으로 이루어져 있습니다.
튜토리얼에서 한 번에 모든 계산을 끝내는 인코더 코드와는 다르게, 디코더는 반복문을 사용하여 한 번에 하나의 타임스텝을 계산하는 방식으로 코딩 되어있습니다. 이 얘기는 디코더의 입력은 하나의 단어이며 출력도 하나의 단어라는 말로도 해석할 수 있습니다.
class LuongAttnDecoderRNN(nn.Module): def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.1): super(LuongAttnDecoderRNN, self).__init__() # Keep for reference self.attn_model = attn_model self.hidden_size = hidden_size self.output_size = output_size self.n_layers = n_layers self.dropout = dropout # Define layers 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): # Note: we run this one step (word) at a time # Get embedding of current input word embedded = self.embedding(input_step) embedded = self.embedding_dropout(embedded) # Forward through unidirectional GRU rnn_output, hidden = self.gru(embedded, last_hidden) # Calculate attention weights from the current GRU output attn_weights = self.attn(rnn_output, encoder_outputs) # Multiply attention weights to encoder outputs to get new "weighted sum" context vector context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # Concatenate weighted context vector and GRU output using Luong eq. 5 rnn_output = rnn_output.squeeze(0) context = context.squeeze(1) concat_input = torch.cat((rnn_output, context), 1) concat_output = torch.tanh(self.concat(concat_input)) # Predict next word using Luong eq. 6 output = self.out(concat_output) output = F.softmax(output, dim=1) # Return output and final hidden state return output, hidden
1) 디코더는 입력 토큰(input_step) 이전 시간의 히든 스테이트(last_hidden) 그리고 인코더에서 두 개의 gru 레이어를 거친 히든 스테이트(encoder_outputs)를 입력으로 받습니다.
2) 임베딩을 거친 입력은 이전 시간의 히든 스테이트와 함께 gru의 입력으로 들어갑니다. (gru 레이어의 다른 출력 hidden은 다음 디코더의 입력으로 들어갑니다.
3) 디코더 gru 레이어의 출력(rnn_output)과 인코더의 히든 스테이트로 어텐션 메커니즘을 진행하여 어텐션 가중치(attn_weights)를 구합니다.
4) 인코더의 히든 스테이트는 어텐션 가중치와 weighted sum 되어 좀 더 집중하여 볼 단어에 높은 가중치 값을 얻게 됩니다. (context)
5) 가중치 값을 얻은 인코더의 히든 스테이트와 디코더의 출력값은 concat과 tanh 연산을 거친 후 output 레이어로 들어갑니다.
6) output 레이어의 출력값은 softmax함수를 거쳐 나온 확률 값으로, size는 [배치사이즈 x 사전의 크기]와 같습니다.
<전체 모델 그림, t = 2>
2. 학습
지금까지 대화 시스템에서 가장 중요한 부분인 인코더 디코더 모델에 관해 알아보았습니다. 이제부터는 대화 시스템이 어떤 방식으로 학습이 되는지, 인코더와 디코더가 어디서 어떻게 사용되는지 등, 학습 절차에 관해 알아보도록 하겠습니다. 처음 이 튜토리얼의 코드를 봤을 때 전처리 부터 모델, 학습 부분이 하나의 파일로 이루어져 있어 익숙해 지는 데 시간이 조금 걸렸습니다.
코드를 보면 train이 라는 단어가 들어간 두 개의 함수를 볼 수 있습니다. trainIters와 train입니다. 기능을 간단히 설명하자면 trainIters는 데이터 불러오기, train 실행, 학습 상황 출력 등 실제 학습에 관여하기 보다는, 학습에 필요한 도구들을 준비하는 역할을 합니다. train은 위에서 살펴 보았던 인코더와 디코더를 통해 학습을 진행하고, teacher forcing, Masked nagative log likely loss 등의 방법을 사용하여 실제로 대화 시스템을 학습 시키는 역할을 합니다.
2.1 batch2TrainData 함수 (전처리)
batch2TrainData는 데이터 전처리 함수입니다. 실제 데이터를 바로 입력할 수 없으므로, 이 함수를 이용하여 데이터를 전처리 후 인코더에 입력으로 넣어줍니다. 입력으로는 단어 사전(voc)과 pair data(pair_batch)를 받습니다.
# Returns all items for a given batch of pairs def batch2TrainData(voc, pair_batch): pair_batch.sort(key=lambda x: len(x[0].split(" ")), reverse=True) input_batch, output_batch = [], [] for pair in pair_batch: input_batch.append(pair[0]) output_batch.append(pair[1]) inp, lengths = inputVar(input_batch, voc) output, mask, max_target_len = outputVar(output_batch, voc) return inp, lengths, output, mask, max_target_len
pair_batch는 배치 사이즈 만큼의 (input, target)페어 데이터 입니다.
1) pair_batch를 input sequence의 길이 순으로 정렬합니다.
2) 정렬된 에서 하나의 pair를 가져와 input sequence(pair[0])는 에 넣어주고, output sequence(pair[1])는 output_batch에 넣어줍니다. (pair 에서 input sequence와 output sequence 분리)
3) inputVar함수를 통해 word단위로 indexing을 해준 결과와 , sequence별 길이를 얻는다. (가장 긴 sequence의 길이만큼 padding을 해줍니다.)
4) outputVar함수는 output_batch을 받아 inputVar과 같은 작업을 해줍니다. inputVar과 한 가지 다른 점은 mask를 만들어줍니다. mask는 padding과 indexing이 완료된 output sequence(output)에서 값이 있는 경우 1, 값이 없는 경우(padding) 0으로 설정해 준 matrix입니다.
배치 사이즈 = 5, sequence 최대 길이 = 10일 때의 return 예시를 보여드리겠습니다.
2.2 train 함수
trainIters에서 한 번의 iteration마다 한 번의 train이 실행됩니다.
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): # Zero gradients encoder_optimizer.zero_grad() decoder_optimizer.zero_grad() # Set device options input_variable = input_variable.to(device) lengths = lengths.to(device) target_variable = target_variable.to(device) mask = mask.to(device) # Initialize variables loss = 0 print_losses = [] n_totals = 0 # Forward pass through encoder encoder_outputs, encoder_hidden = encoder(input_variable, lengths) # Create initial decoder input (start with SOS tokens for each sentence) decoder_input = torch.LongTensor([[SOS_token for _ in range(batch_size)]]) decoder_input = decoder_input.to(device) # Set initial decoder hidden state to the encoder's final hidden state decoder_hidden = encoder_hidden[:decoder.n_layers] # Determine if we are using teacher forcing this iteration use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False # Forward batch of sequences through decoder one time step at a time if use_teacher_forcing: for t in range(max_target_len): decoder_output, decoder_hidden = decoder( decoder_input, decoder_hidden, encoder_outputs ) # Teacher forcing: next input is current target decoder_input = target_variable[t].view(1, -1) # Calculate and accumulate loss 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 ) # No teacher forcing: next input is decoder's own current output _, topi = decoder_output.topk(1) decoder_input = torch.LongTensor([[topi[i][0] for i in range(batch_size)]]) decoder_input = decoder_input.to(device) # Calculate and accumulate loss mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t]) loss += mask_loss print_losses.append(mask_loss.item() * nTotal) n_totals += nTotal # Perform backpropatation loss.backward() # Clip gradients: gradients are modified in place _ = nn.utils.clip_grad_norm_(encoder.parameters(), clip) _ = nn.utils.clip_grad_norm_(decoder.parameters(), clip) # Adjust model weights encoder_optimizer.step() decoder_optimizer.step() return sum(print_losses) / n_totals
train의 입력으로는 꽤 많은 변수들이 들어갑니다. input_variable, lengths, target_variable, mask, max_target_len는 batch2TrainData함수의 출력값 입니다. 위 모델 부분에서 설명한 것처럼 인코더는 한 번에 배치 사이즈 만큼의 sequence가 계산되고, 디코더는 한번에 한 타임 스텝 즉, 단어 단위로 진행됩니다. 예를 들면 디코더에서 t=2 일 경우, target_variable의 두 번째 단어들이 계산됩니다.
1) zero_grad()로 역전파를 실행하기 전 gradient를 0으로 설정해 줍니다.
2) to(device)는 변수를 정해진 device(cpu, gpu)로 보내는 작업을 합니다.
3) encoder로 인코더 레이어의 마지막 히든 스테이트 벡터(decoder_hidden)와 각 타임 스텝의 히든 스테이트 벡터(encoder_outputs)를 얻습니다. (한줄로 인코더의 모든 계산이 끝나버립니다.)
4) 디코더의 초기 입력 토큰으로 넣어줄 decoder_input를 정의합니다. SOS token(Start Of Sequence)를 디코더 초기 입력으로 넣어줍니다.
5) 인코더는 2개의 층에 양방향이고, 디코더는 2개의 층에 단방향이므로 encoder_hidden에서 첫 번째 층의 히든 스테이트 벡터만 입력으로 넣어줍니다.
6) 이번 iteration에서의 techer_forcing 여부를 결정합니다.
7) 디코딩 과정에서 teacher forcing과 mask nll loss가 사용됩니다.
8) 역전파로 loss값을 구해줍니다. loss값은 mask_loss는 cross Entropy를 사용한 loss값이고 nTotal는 토큰 개수입니다.
9) print_losses과 n_totals은 이번 iteration에서 지금까지 진행된 loss의 누적된 값과 토큰 개수입니다.
10) train의 return 값은 loss의 평균입니다.
2.3 Teacher Forcing
Teacher Forcing이란 말 그대로 선생님이 답을 알려주는 듯한 효과를 주는 트릭입니다. 위 디코더 설명대로라면 학습 중에, 현재 디코더의 히든 스테이트에는 이전 타임 스텝에서 예측한 단어(토큰)이 입력으로 들어가 주어야 하지만 Teacher Forcing을 사용하면, 예측한 단어 대신 실제 정답이 들어가게 됩니다. 공식 튜토리얼 홈페이지 에서는 이전 시간에 예측한 단어로 현재에 단어를 예측하는데 충분한 기회가 주어지지 않는 문제가 있어 적정한 비율로 Teacher Forcing 여부를 조절했다고 합니다. 실제로 어떻게 사용하는지 코드를 보면서 알아보겠습니다.
if use_teacher_forcing: for t in range(max_target_len): decoder_output, decoder_hidden = decoder( decoder_input, decoder_hidden, encoder_outputs ) # Teacher forcing: next input is current target decoder_input = target_variable[t].view(1, -1) # Calculate and accumulate loss mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t]) loss += mask_loss print_losses.append(mask_loss.item() * nTotal) n_totals += nTotal
우선 Teacher forcing을 사용하는 경우입니다. decoder_input을 보시면 target_variable, 즉 실제 정답이 다음 디코더의 입력으로 들어가는 것을 볼 수 있습니다.
else: for t in range(max_target_len): decoder_output, decoder_hidden = decoder( decoder_input, decoder_hidden, encoder_outputs ) # No teacher forcing: next input is decoder's own current output _, topi = decoder_output.topk(1) decoder_input = torch.LongTensor([[topi[i][0] for i in range(batch_size)]]) decoder_input = decoder_input.to(device) # Calculate and accumulate loss mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t]) loss += mask_loss print_losses.append(mask_loss.item() * nTotal) n_totals += nTotal
그 다음은 Teacher forcing을 사용하지 않는 경우입니다. topk()로 가장 높은 확률값을 가진 순으로 정렬합니다. topk()의 결과는 (실제 값, 인덱스) 형태의 페어이며, topi는 그 중 인덱스 값만 저장합니다. 디코더의 ourput과 사전의 크기가 같으므로, 결과적으로 topi[i][0]는 현재 디코더에서 예측한 단어의 '단어 사전 인덱스'와 같은 값입니다.
3. 궁금한 점
3.1 두 번째 레이어가 아닌 첫 번째 인코더 레이어를 디코더에 넣어준다.
3.2 배치 데이터를 만들 때 random.choice를 사용.
4. 참조
https://pytorch.org/tutorials/beginner/chatbot_tutorial.html
https://www.cs.cornell.edu/~cristian/Cornell_Movie-Dialogs_Corpus.html
- Total
- Today
- Yesterday
- pytorch 튜토리얼
- rnn
- 퍼셉트론
- Visualization
- DeepLearning
- rnn 모델
- pytorch
- 어텐션 메커니즘
- attention
- 챗봇
- deep learning
- 시각화
- recurrent
- matplotlib
- attention mechanism
- 파이토치
- perceptron
- 셀프어텐션
- self attention
- Chatbot
- 어텐션
- 딥러닝
- sequence to sequence
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |