深度循环神经网络
就是多个循环神经网络叠加(类似于多个感知机叠加)
深度rnn每一层隐藏层都需要初始化
import torch
from torch import nn
from d2l import torch as d2l
batch_size,num_steps= 32,35
train_iter, vocab = d2l.load_data_time_machine(batch_size,num_steps)
vocab_size,num_hiddens,num_layers=len(vocab),256,2
num_inputs=vocab_size
device=d2l.try_gpu()
lstm_layer=nn.LSTM(num_inputs,num_hiddens,num_layers)
#输入大小,隐藏层大小,num_layers是隐藏层数
model = d2l.RNNModel(lstm_layer,len(vocab))
model = model.to(device)
num_epochs,lr=500,2
d2l.train_ch8(model,train_iter,vocab,lr,num_epochs,device)
双向循环神经网络
因为双向循环神经网络需要知道双侧的信息,从而推出中间的信息。所以,不能预测未来(仅从过去和现在的信息,推导出未来)
import torch
from torch import nn
from d2l import torch as d2l
batch_size,num_steps,device=32,35,d2l.try_gpu()
train_iter,vocab=d2l.load_data_time_machine(batch_size,num_steps)
vocab_size,num_hiddens,num_layers=len(vocab),256,2
num_inputs=vocab_size#输入大小
lstm_layer=nn.LSTM(num_inputs,num_hiddens,num_layers,bidirectional=True)
#输入大小,隐藏层大小,隐藏层数,bidirectional是双向模型
model=d2l.RNNModel(lstm_layer,len(vocab))
#rnn模型使用lstm网络,以及输入的大小
model = model.to(device)
num_epochs,lr=500,1
d2l.train_ch8(model,train_iter,vocab,lr,num_epochs,device)
H只能(-1,1)而C没有这个限制,可以存储较大的信息
没有。把这两个数字相加在一起,才需要权重。但是这两个是contact的关系,是连在一起的
机器翻译与数据集
import torch
import os
import d2l.torch
#@save
d2l.torch.DATA_HUB['fra-eng'] = (d2l.torch.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
"""载入“英语-法语”数据集"""
#@save
def read_data_nmt():
data_dir = d2l.torch.download_extract('fra-eng')
with open(os.path.join(data_dir,'fra.txt'),'r',encoding='utf-8') as f:
return f.read()
raw_text = read_data_nmt() #注意raw_text是一个String类型
# 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
#@save
def process_nmt(text):
def no_space(char,pre_char):
return char in set(',.!?') and pre_char != ' '
#text就是一个String类型,将不间断空格用空格符替换
# 使用空格替换不间断空格
# 使用小写字母替换大写字母
text = text.replace('\u202f',' ').replace('\xa0',' ').lower()
# 在单词和标点符号之间插入空格
out = [' '+char if i>0 and no_space(char,text[i-1]) else char for i,char in enumerate(text)]
return ''.join(out)
text = process_nmt(raw_text)
#@save
def tokenize_nmt(text,num_examples=None):
"""词元化“英语-法语”数据数据集"""
source,target = [],[]
for i,line in enumerate(text.split('\n')):
if num_examples and i>num_examples:
break
parts = line.split('\t')
if len(parts) == 2 :# source里面每个元素是一个列表
#代表每行数据英文那部分,英文那部分是通过空白字符分隔形成的列表
source.append(parts[0].split(' '))
#代表每行数据法语那部分,法语那部分是通过空白字符分隔形成的列表
target.append(parts[1].split(' '))
return source,target
#将每个样本序列采取填充或者截断方式变成一个固定长度的序列用于下面数据集加载,网络训练
#@save
def truncate_padding(line,num_steps,padding_token):
"""截断或填充文本序列"""
if len(line)>num_steps:
return line[:num_steps] # 截断
return line+[padding_token]*(num_steps-len(line)) # 填充
truncate_padding(src_vocab[source[0]],10,src_vocab['<pad>'])
#@save
def build_array_nmt(lines,vocab,num_steps):
"""将机器翻译的文本序列转换成小批量"""
# vocab[data]:无论data是一个词,或者是一个列表,或者是一个list of list 类型
#返回的都是一个list,list里面是这些词对应的id
# list中每个元素是这个英文序列的每个词的token
lines = [vocab[line] for line in lines]
lines = [line+[vocab['<eos>']] for line in lines]
#给每个英文序列(一个英文句子)添加一个结束符'<eos>'
# array 是一个list of list ,里面每个元素是一个list,代表每个英文序列的token
array = torch.tensor([truncate_padding(line,num_steps,vocab['<pad>']) for line in lines])
#表明并不是每一个英文序列token都会以'<eos>'结尾,需要看这个英文序列的长度和num_steps
valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
#sum(dim=1)表示求出每个序列样本的真实长度,除开填充长度,为一个list列表,里面每个元素代码一个英文样本序列真是长度真实
return array,valid_len
#@save
def load_data_nmt(batch_size,num_steps,num_examples=600):
"""返回翻译数据集的迭代器和词表"""
# 读取txt文件
raw_text = read_data_nmt()
# 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
text = process_nmt(raw_text)
# 将每行英文和法语句子分隔开,同时将英文句子以空格分隔开,将法语句子以空格分隔开
#source,target都是list of list类型,num_examples表示读取文本数据最大行数是多少
source,target = tokenize_nmt(text,num_examples)
# 建立英文词表vocab,法语词表vocab
src_vocab = d2l.torch.Vocab(source,min_freq=2,reserved_tokens=['<pad>','<bos>','<eos>'])
tgt_vocab = d2l.torch.Vocab(target,min_freq=2,reserved_tokens=['<pad>','<bos>','<eos>'])
# 将source,target转变成对应src_vocab,tgt_vocab里面的词表id索引,同时将每个句子样本序列通过裁剪或者填充成一个固定num_steps长度的句子
src_array,src_valid_len = build_array_nmt(source,src_vocab,num_steps)
tgt_array,tgt_valid_len = build_array_nmt(target,tgt_vocab,num_steps)
data_arrays = (src_array,src_valid_len,tgt_array,tgt_valid_len)
# 生成数据集迭代器
#返回值data_iter是四个:批量X(批量序列数据集), 批量X_valid_len(批量中每个序列样本的长度),
# 批量Y(批量标签lable), 批量Y_valid_len(批量中每个目标序列样本的长度)
#返回值data_iter是一个元组,有四个值
#批量序列数据集, 批量中每个序列样本的长度, 批量标签lable, 批量中每个目标序列样本的长度
data_iter = d2l.torch.load_array(data_arrays,batch_size)
#生成数据集迭代器是将所有样本数据随机打乱然后再把batch_size个样本数据组合在一起生成一个批量样本数据
#返回数据迭代器,src_vocab,tgt_vocab
return data_iter,src_vocab,tgt_vocab
data_iter,src_vocab,tgt_vocab = load_data_nmt(batch_size=2,num_steps=8)
for X,X_valid_len,Y,Y_valid_len in data_iter:
print('X: ',X.type(torch.int32))
print('X_valid_len: ',X_valid_len.type(torch.int32))
print('Y: ',Y.type(torch.int32))
print('Y_valid_len: ',Y_valid_len.type(torch.int32))
break #只读出“英语-法语”数据集中的第一个小批量数据
编码器—解码器
注意:解码器的输入,除了编码器的特征输出外,还有解码器的额外输入
from torch import nn
"""编码器-解码器架构的基本编码器接口"""
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
def forward(self, X, *args):
raise NotImplementedError
"""编码器-解码器架构的基本解码器接口"""
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
def init_state(self, enc_outputs, *args):
raise NotImplementedError
def forward(self, X, state):
raise NotImplementedError
"""编码器-解码器架构的基类"""
class EncoderDecoder(nn.Module):
def __init__(self, encoder, decoder):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
def forward(self, enc_X, dec_X, *args):
enc_outputs = self.encoder(enc_X, *args)
dec_state = self.decoder.init_state(enc_outputs, *args)
return self.decoder(dec_X, dec_state)
Seq2seq
在上图解码器结构中特定的“eos”表示序列结束词元,在预测时一旦输出序列生成此词元,模型就会停止预测。 在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先特定的“bos”表示序列开始词元,它是解码器的输入序列的第一个词元;
其次使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态,因此这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 在其他一些设计中 将编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于语言模型的训练, 可以允许标签成为原始的输入序列, 从源序列词元“”、“Ils”、“regardent”、“.” 到新序列词元 “Ils”、“regardent”、“.”、“”来移动预测的位置。
编码器不需要全连接层
预测的序列长度很短的话,len(pred)很小,len(label)/len(pred) 会很大,导致1-len()/len会很小。所以是惩罚过短的预测
因为p肯定小于1,所以p的幂函数是递减函数。n愈大,越小,p的指数越大。所以是高权重
同时,BLEU越大越好,且因为是e的指数幂 最大为1,最小为0
import torch
import d2l.torch
import math
import collections
from torch import nn
#@save
class Seq2SeqEncoder(d2l.torch.Encoder):
"""用于序列到序列学习的循环神经网络编码器"""
def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0):
super(Seq2SeqEncoder,self).__init__()
# 嵌入层
self.embedding = nn.Embedding(vocab_size,embed_size)
self.rnn = nn.GRU(embed_size,num_hiddens,num_layers,dropout=dropout,bidirectional=False)
def forward(self,X,*args):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X)
# 在循环神经网络模型中,第一个轴对应于时间步
X = X.permute(1,0,2)
# 如果未传入状态,则默认为初始隐状态state为0
output,state = self.rnn(X)
# output的形状:(num_steps,batch_size,num_hiddens)
# state[0]的形状:(num_layers,batch_size,num_hiddens)
return output,state
#@save
class Seq2SeqDecoder(d2l.torch.Decoder):
"""用于序列到序列学习的循环神经网络解码器"""
def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0):
super(Seq2SeqDecoder,self).__init__()
self.embedding = nn.Embedding(vocab_size,embed_size)
self.rnn = nn.GRU(embed_size+num_hiddens,num_hiddens,num_layers,dropout=dropout)
self.dense = nn.Linear(num_hiddens,vocab_size)
def init_state(self,enc_outputs,*args):
return enc_outputs[1]
def forward(self,X,state):
# 'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X).permute(1,0,2)
#state[-1]表示将encoder中经过最后一个时刻后得到的两个隐状态层中最后一层拿出来作为上下文context,也就相当于这个批量序列的浓缩概括,
#形状为(batch_size,num_hidddens),然后复制num_steps次再组合起来,然后与decoder输入在embed_size维度(dim=2)上面进行连接
#因此decoder每个输入样本序列中每个词embed_size都带上了encoder最后隐状态层中最后一层上下文context
#广播context,使其具有与X相同的num_steps
context = state[-1].repeat(X.shape[0],1,1)
X_context = torch.cat((X,context),dim=2)
output,state = self.rnn(X_context,state)
y_hat = self.dense(output)
y_hat = y_hat.permute(1,0,2) #y_hat中每一个batch_size样本就是将每个英文样本翻译成法语的句子
# y_hat的形状:(batch_size,num_steps,vocab_size)
# state[0]的形状:(num_layers,batch_size,num_hiddens)
return y_hat,state
#@save
def sequence_mask(X,valid_len,value=0):
"""在序列中屏蔽不相关的项"""
#把x中每行前len之后的值都置零
maxlen = X.size(dim=1) #X.shape[1],第1维的长度
mask = torch.arange((maxlen),dtype=torch.float32,device=X.device)[None,:]< valid_len[:,None] #[:,None]在None的位置增加一维,维度大小为1,比大小使用了广播机制
#print(mask)
# ~mask表示对mask取反
X[~mask] = value
return X
#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
"""带遮蔽的softmax交叉熵损失函数"""
# pred的形状:(batch_size,num_steps,vocab_size)
# label的形状:(batch_size,num_steps)
# valid_len的形状:(batch_size,)
def forward(self,pred,label,valid_len):
weights = torch.ones_like(label)
#weight和label一样的全1矩阵
weights = sequence_mask(weights,valid_len)
#把权重矩阵中有效之外的全置零
self.reduction = 'none'
unweighted_loss = super().forward(pred.permute(0,2,1),label)
#pred.permute(0,2,1)这里将embed_size拿到第二维,将batch_size拿到第三维
#是因为Pytorch中cross entropy()中forward()函数要求pred数据需要是这种格式类型
weighted_loss = (unweighted_loss*weights).mean(dim=1)
return weighted_loss
#@save
def train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device):
"""训练序列到序列模型"""
def xavier_init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
if type(m) == nn.GRU:
for param in m._flat_weights_names:
if 'weight' in param:
nn.init.xavier_uniform_(m._parameters[param])
net.apply(xavier_init_weights)
net = net.to(device)
loss = MaskedSoftmaxCELoss()
optim = torch.optim.Adam(net.parameters(),lr=lr)
animator = d2l.torch.Animator(xlabel='epoch',ylabel='loss',xlim=[10,num_epochs])
net.train()
for epoch in range(num_epochs):
timer = d2l.torch.Timer()
accumulator = d2l.torch.Accumulator(2) # 训练损失总和,词元数量
for batch in train_iter:
optim.zero_grad()
X,X_valid_len,Y,Y_valid_len = [data.to(device) for data in batch]
bos = torch.tensor([tgt_vocab['<bos>']]*Y.shape[0],device=device).reshape(-1,1)
dec_inputs = torch.cat([bos,Y[:,:-1]],dim=1) # 强制教学
Y_hat,_= net(X,dec_inputs,X_valid_len)
l = loss(Y_hat,Y,Y_valid_len)
l.sum().backward() # 损失函数的标量进行“反向传播”
d2l.torch.grad_clipping(net,theta=1)
num_tokens = Y_valid_len.sum()
optim.step()
with torch.no_grad():
accumulator.add(l.sum(),num_tokens)
if (epoch+1)%10==0:
animator.add(epoch+1,(accumulator[0]/accumulator[1],)) #accumulator[1]为标签样本序列tokens总数
print('loss: ',accumulator[0]/accumulator[1],accumulator[1]/timer.stop(),'tokens/s',' on device:'+str(device))
batch_size,num_steps = 64,10
embed_size,num_hiddens,num_layers,dropout = 32,32,2,0.1
lr,num_epochs,device = 0.005,300,d2l.torch.try_gpu()
train_iter,src_vocab,tgt_vocab = d2l.torch.load_data_nmt(batch_size,num_steps)
encoder = Seq2SeqEncoder(len(src_vocab),embed_size,num_hiddens,num_layers,dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab),embed_size,num_hiddens,num_layers,dropout)
net = d2l.torch.EncoderDecoder(encoder,decoder)
train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device)
def predict_seq2seq(net,src_sentence,src_vocab,tgt_vocab,num_steps,device,save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为eval模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')]+[src_vocab['<eos>']]
enc_valid_len = torch.tensor([len(src_tokens)],device=device) #Encoder输入格式列表形式,里面每个元素是每个样本序列实际长度
src_tokens = d2l.torch.truncate_pad(src_tokens,num_steps,src_vocab['<pad>'])
# 添加批量轴
enc_X = torch.unsqueeze(torch.tensor(src_tokens,dtype=torch.long,device=device),dim=0)
enc_outputs = net.encoder(enc_X,enc_valid_len)
dec_state = net.decoder.init_state(enc_outputs,enc_valid_len)
# 添加批量轴
dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['<bos>']],dtype=torch.long,device=device),dim=0)
output_seq ,attention_weight_seq = [],[]
for _ in range(num_steps):
Y,dec_state = net.decoder(dec_X,dec_state)
# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
dec_X = Y.argmax(dim=2)
pred = dec_X.squeeze(dim=0).type(torch.int32).item()
# 保存注意力权重
if save_attention_weights:
attention_weight_seq.append(net.decoder.attention_weights)
# 一旦序列结束词元被预测,输出序列的生成就完成了
if pred == tgt_vocab['<eos>']:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)),attention_weight_seq #' '.join(List)表示把list列表里面的元素以空格连接起来
def bleu(pred_seq,label_seq,k):
"""计算BLEU"""
pred_tokens,label_tokens = pred_seq.split(' '),label_seq.split(' ')
len_pred,len_label = len(pred_tokens),len(label_tokens)
score = math.exp(min(0,1-len_label/len_pred))
for n in range(1,k+1):
num_matches,label_subs = 0,collections.defaultdict(int)
for i in range(len_label-n+1):
label_subs[' '.join(label_tokens[i:i+n])]+=1
for i in range(len_pred-n+1):
if label_subs[' '.join(pred_tokens[i:i+n])]>0:
num_matches+=1
label_subs[' '.join(pred_tokens[i:i+n])] -= 1
#print('......',len_pred)
score *= math.pow(num_matches/(len_pred-n+1),math.pow(0.5,n))
return score
束搜索
h(t)根据h(t-1)和x(t)来调整,是局部最优。但不是全局最优。比如,h(t)根据h(t-2)来调整才是最好的。
注意力机制
显示的加入了自主性提示: query(查询)