PyTorch使用seq2seq+attention實現時間格式轉換
pytorch實現seq2seq+attention轉換日期
使用keras實現加入注意力機制的seq2seq比較麻煩,所以這裡我嘗試使用機器翻譯的seq2seq+attention模型實現人造日期對標準日期格式的轉換。
所copy的程式碼來自practical-pytorch教程,以及pytorch-seq2seq教程
所用的資料來自注意力機制keras實現。
python3
pytorch版本 0.4.0
可能需要GPU
import json
from matplotlib import ticker
from collections import Counter
import matplotlib.pyplot as plt
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device
device(type='cuda')
預處理
這裡先生成字元和數字相互轉換的字典,如果是句子也可以按照詞為單位。我在字典的開頭添加了4種表示。
def build_vocab(texts, n=None):
counter = Counter(''.join(texts)) # char level
char2index = {w: i for i, (w, c) in enumerate(counter.most_common(n), start=4)}
char2index['~'] = 0 # pad 不足長度的文字在後邊填充0
char2index['^'] = 1 # sos 表示句子的開頭
char2index['$'] = 2 # eos 表示句子的結尾
char2index['#' ] = 3 # unk 表示句子中出現的字典中沒有的未知詞
index2char = {i: w for w, i in char2index.items()}
return char2index, index2char
先看一下資料的格式。
pairs = json.load(open('Time Dataset.json', 'rt', encoding='utf-8'))
print(pairs[:1])
[['six hours and fifty five am', '06:55']]
我們將目標文字和原文字分開,求出兩邊句子的最大長度,然後建立兩邊各自的字典。
data = array(pairs)
src_texts = data[:, 0]
trg_texts = data[:, 1]
src_c2ix, src_ix2c = build_vocab(src_texts)
trg_c2ix, trg_ix2c = build_vocab(trg_texts)
這裡按批量跟新,定義一個隨機批量生成的函式,它能夠將文字轉換成字典中的數字表示,並同時返回batch_size個樣本和它們的長度,這些樣本按照長度降序排序。pad的長度以batch中最長的為準。這主要是為了適應pack_padded_sequence這個函式,因為輸入RNN的序列不需要將pad標誌也輸入RNN中計算,RNN只需要迴圈計算到其真實長度即可。
def indexes_from_text(text, char2index):
return [1] + [char2index[c] for c in text] + [2] # 手動新增開始結束標誌
def pad_seq(seq, max_length):
seq += [0 for _ in range(max_length - len(seq))]
return seq
max_src_len = max(list(map(len, src_texts))) + 2
max_trg_len = max(list(map(len, trg_texts))) + 2
max_src_len, max_trg_len
(43, 7)
def random_batch(batch_size, pairs, src_c2ix, trg_c2ix):
input_seqs, target_seqs = [], []
for i in random.choice(len(pairs), batch_size):
input_seqs.append(indexes_from_text(pairs[i][0], src_c2ix))
target_seqs.append(indexes_from_text(pairs[i][1], trg_c2ix))
seq_pairs = sorted(zip(input_seqs, target_seqs), key=lambda p: len(p[0]), reverse=True)
input_seqs, target_seqs = zip(*seq_pairs)
input_lengths = [len(s) for s in input_seqs]
input_padded = [pad_seq(s, max(input_lengths)) for s in input_seqs]
target_lengths = [len(s) for s in target_seqs]
target_padded = [pad_seq(s, max(target_lengths)) for s in target_seqs]
input_var = torch.LongTensor(input_padded).transpose(0, 1)
# seq_len x batch_size
target_var = torch.LongTensor(target_padded).transpose(0, 1)
input_var = input_var.to(device)
target_var = target_var.to(device)
return input_var, input_lengths, target_var, target_lengths
可以先列印一下,batch_size=3時的返回結果。注意這裡batch經過了轉置。
random_batch(3, data, src_c2ix, trg_c2ix)
(tensor([[ 1, 1, 1],
[ 12, 23, 6],
[ 7, 9, 18],
[ 27, 26, 21],
[ 10, 23, 23],
[ 4, 4, 25],
[ 16, 17, 2],
[ 7, 9, 0],
[ 27, 11, 0],
[ 10, 9, 0],
[ 19, 2, 0],
[ 4, 0, 0],
[ 13, 0, 0],
[ 8, 0, 0],
[ 32, 0, 0],
[ 4, 0, 0],
[ 6, 0, 0],
[ 31, 0, 0],
[ 5, 0, 0],
[ 8, 0, 0],
[ 6, 0, 0],
[ 20, 0, 0],
[ 4, 0, 0],
[ 12, 0, 0],
[ 14, 0, 0],
[ 28, 0, 0],
[ 5, 0, 0],
[ 4, 0, 0],
[ 13, 0, 0],
[ 12, 0, 0],
[ 6, 0, 0],
[ 5, 0, 0],
[ 10, 0, 0],
[ 4, 0, 0],
[ 8, 0, 0],
[ 7, 0, 0],
[ 7, 0, 0],
[ 8, 0, 0],
[ 2, 0, 0]], device='cuda:0'),
[39, 11, 7],
tensor([[ 1, 1, 1],
[ 6, 6, 5],
[ 13, 9, 7],
[ 4, 4, 4],
[ 7, 5, 8],
[ 9, 8, 10],
[ 2, 2, 2]], device='cuda:0'),
[7, 7, 7])
模型定義
模型分為encoder和decoder兩個部分,decoder部分比較簡單,就是一層Embedding層加上兩層GRU。之前處理的batch的格式主要是為了使用pack_padded_sequence和pad_packed_sequence這兩個類對GRU輸入輸出批量處理。一定要注意各個變數的shape。
class Encoder(nn.Module):
def __init__(self, input_dim, embedding_dim, hidden_dim, num_layers=2, dropout=0.2):
super().__init__()
self.input_dim = input_dim
self.embedding_dim = embedding_dim
self.hidden_dim = hidden_dim
self.num_layers = num_layers
self.dropout = dropout
# input_dim = vocab_size + 1
self.embedding = nn.Embedding(input_dim, embedding_dim)
self.rnn = nn.GRU(embedding_dim, hidden_dim,
num_layers=num_layers, dropout=dropout)
self.dropout = nn.Dropout(dropout)
def forward(self, input_seqs, input_lengths, hidden=None):
# src = [sent len, batch size]
embedded = self.dropout(self.embedding(input_seqs))
# embedded = [sent len, batch size, emb dim]
packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
outputs, hidden = self.rnn(packed, hidden)
outputs, output_lengths = torch.nn.utils.rnn.pad_packed_sequence(outputs)
# outputs, hidden = self.rnn(embedded, hidden)
# outputs = [sent len, batch size, hid dim * n directions]
# hidden = [n layers, batch size, hid dim]
# outputs are always from the last layer
return outputs, hidden
首先定義一下Attention層,這裡主要是對encoder的輸出進行attention操作,也可以直接對embedding層的輸出進行attention。
論文Neural Machine Translation by Jointly Learning to Align and Translate中定義了attention的計算公式。
decoder的輸出取決於decoder先前的輸出和 , 這裡 包括當前GRU輸出的hidden state(這部分已經考慮了先前的輸出) 以及attention(上下文向量,由encoder的輸出求得)。 計算公式如下:函式 非線性啟用的全連線層,輸入是 , , and 三者的拼接。
所謂的上下文向量就是對encoder的所有輸出進行加權求和, 表示輸出的第 i 個詞對encoder第 j 個輸出 的權重。
每個 通過對所有 進行softmax,而每個 是decoder的上一個hidden state 和指定的encoder的輸出 經過某些線性操作 計算得分。
此外,論文Effective Approaches to Attention-based Neural Machine Translation中提出了計算分值的不同方式。這裡用到的是第三種。