torch.__version__
版本爲-1.1.0
1.首先引入包,定義 填充符 PAD_token、開始符 SOS_token 、結束符 EOS_token
# 在開頭加上from __future__ import print_function這句之後,如果你的python版本是python2.X,你也得按照python3.X那樣使用這些函數。
# Python提供了__future__模塊,把下一個新版本的特性導入到當前版本,於是我們就可以在當前版本中測試一些新版本的特性。
# division 精確除法
# print_function 打印函數
# unicode_literals 這個是對字符串使用unicode字符
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import torch
import torch.nn as nn
import torch.nn.functional as F
import re
import os
import unicodedata
import numpy as np
device = torch.device("cpu")
MAX_LENGTH = 10 # Maximum sentence length
# Default word tokens
PAD_token = 0 # Used for padding short sentences
SOS_token = 1 # Start-of-sentence token
EOS_token = 2 # End-of-sentence token
2. 模型介紹
seq2seq模型用於輸入是可變長度序列,輸出也是可變長度序列的情況。
Encoder:每一個時間步輸出一個輸出向量和一個隱含向量,隱含向量以此傳遞,輸出向量則被記錄。
Decoder:使用上下文向量和Decoder的隱含向量生成下一個詞,直到輸出“EOS_token”爲止。解碼器中使用注意機制(Global attention)來幫助它在生成輸出時“注意”輸入的某些部分。
全局注意力機制論文:https://arxiv.org/abs/1508.04025
3.數據處理
將文本的每個token生成一個詞彙表,映射爲數字。
normalizeString 函數將字符串中的所有字符轉換爲小寫並刪除所有非字母字符。
indicesFromSentence函數 將單詞的句子返回相應的單詞索引序列。
class Voc:
def __init__(self, name):
self.name = name
self.trimmed = False
self.word2index = {}
self.word2count = {}
self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
self.num_words = 3 # Count SOS, EOS, PAD-0,1,2
# 分詞
def addSentence(self, sentence):
for word in sentence.split(' '):
self.addWord(word)
# 三個詞典的填充 word2index、word2count、index2word
def addWord(self, word):
if word not in self.word2index:
self.word2index[word] = self.num_words
self.word2count[word] = 1
self.index2word[self.num_words] = word
self.num_words += 1
else:
self.word2count[word] += 1
# 刪除低於設定值的單詞 trim修剪
def trim(self, min_count):
if self.trimmed:
return
self.trimmed = True
keep_words = []
for k, v in self.word2count.items():
if v >= min_count:
keep_words.append(k)
print('keep_words {} / {} = {:.4f}'.format(
len(keep_words), len(self.word2index), len(keep_words) / len(self.word2index)
))
# 重新初始化詞典
self.word2index = {}
self.word2count = {}
self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
self.num_words = 3 # Count default tokens
for word in keep_words:
self.addWord(word)
# 全部改成小寫字符,刪除非字母字符
def normalizeString(s):
# 小寫字符
s = s.lower()
# 刪除非字母字符
# re.sub用於替換字符串中的匹配項 re.sub(正則中的模式字符串, 替換的字符串, 原始字符串)
s = re.sub(r"([.!?])", r" \1", s)
# [^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
return s
# 將文本每一段的詞轉換成序號 word2index,並且添加結尾符號 EOS_token
def indexesFromSentence(voc, sentence):
return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]
4.定義編碼器
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
# 初始化 GRU
# input_size、hidden_size都設置爲 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):
# 將單詞索引 轉換爲 embeddings
embedded = self.embedding(input_seq)
# pack sequence -> recurrent network -> unpack sequence
# 按列壓緊,B x T x *,壓縮的理解如下圖
packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
# Forward GRU
outputs, hidden = self.gru(packed, hidden)
# 填充回原始排列,pack_padded_sequence()的逆操作,返回T x B x *(T最長序列的長度,B-batch size)
outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
# 雙向GRU輸出的和 Sum bidirectional GRU outputs
outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
# 返回輸出,最終隱藏狀態
return outputs, hidden
理解 torch.nn.utils.rnn.pack_padded_sequence:
5.定義解碼器的注意模塊
注意力通過評分函數來獲取每個編碼器隱藏狀態的分數(標量)。評分函數在本程序中有三種:
將得分放到softmax函數層,使softmax得分(標量)之和爲1
Soft Attention
是對所有的信息進行加權求和。Hard Attention
是選擇最大信息的那一個。
本程序使用Soft Attention
:將每個編碼器的隱藏狀態與其softmaxed得分(標量)相乘,就能獲得對齊向量。這就是對齊機制
上下文向量:將對齊向量聚合起來。
# Luong 注意力層
class Attn(nn.Module):
def __init__(self, method, hidden_size):
# 如果子類重寫了父類的構造方法,那麼子類的構造方法必須調用父類的構造方法
super(Attn, self).__init__()
self.method = method
if self.method not in ['dot', 'general', 'concat']:
# raise 引發一個異常
raise ValueError(self.method, "is not an appropriate attention method.")
self.hidden_size = hidden_size
# self.attn 爲不同的注意力計算方法,選擇不同的線性層
# torch.nn.Linear(in_features, out_features, bias=True)
if self.method == 'general':
self.attn = nn.Linear(self.hidden_size, hidden_size)
elif self.method == 'concat':
# 連接操作之後維度擴大
self.attn = nn.Linear(self.hidden_size * 2, hidden_size)
# torch.FloatTensor torch.cuda.FloatTensor
# nn.Parameter理解爲類型轉換函數
# 將一個不可訓練的類型Tensor轉換成可以訓練的類型parameter並將這個parameter綁定到這個module裏面,所以在參數優化的時候可以進行優化的
self.v = nn.Parameter(torch.FloatTensor(hidden_size))
# 乘法-求和
def dot_score(self, hidden, encoder_output):
return torch.sum(hidden * encoder_output, dim=2)
# 線性層-乘法-求和
def general_score(self, hidden, encoder_output):
energy = self.attn(encoder_output)
return torch.sum(hidden * energy, dim=2)
# 擴大維度-拼接-線性層-tanh-乘法-求和
def concat_score(self, hidden, encoder_output):
# expand單個維度擴大爲更大的尺寸,擴展的是encoder_output.size(0)
# cat連接操作,cat(inputs, dimension=0),在給定維度上對輸入的張量序列進行連接操作
energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh()
return torch.sum(self.v * energy, dim=2)
def forward(self, hidden, encoder_outputs):
# 根據給定的方法,計算注意力分數
if self.method == 'general':
attn_energies = self.general_score(hidden, encoder_outputs)
elif self.method == 'concat':
attn_energies = self.concat_score(hidden, encoder_outputs)
elif self.method == 'dot':
attn_energies = self.dot_score(hidden, encoder_outputs)
# 轉置max_length和batch_size維度
attn_energies = attn_energies.t()
# 返回softmax歸一化概率分數(增加維度)
return F.softmax(attn_energies, dim=1).unsqueeze(1)
unsqueeze和tanh的用法如下所示:
6.定義解碼器