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的用法如下所示: