门控循环单元包含重置门和更新门两种,输入是由当前时间步的输入和前一时间步的隐状态给出。两个门的输出是由使用sigmoid激活函数的两个全连接层给出。
二者的特点:
两种门的数学表达式:
重置门Rt与常规隐状态更新机制集成后,可以得到时间步t的候选隐状态H_hat(n x h的大小):
得到候选隐状态H_hat后,与更新们Zt结合:
得到最终的Ht。
代码实现
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
特点:可以缓解梯度消失和梯度爆炸
LSTM引入了记忆元,同时为了控制记忆元,又使用了输入门、输出门、遗忘门三个门,注意它们由三个具有sigmoid激活函数的全连接层处理。
三种门的数学表达式为
根据上一时刻的隐状态和当前时刻的输入,可以得到候选记忆元C~:
根据候选记忆单元和遗忘门,输入门以及上一时刻的记忆元可以得到当前时刻的记忆元:
最后需要计算一下隐状态:
代码实现
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
C = F * C + I * C_tilda
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H, C)
注意L是隐藏层数,T为时间步
其中隐藏层数目L和隐藏单元数目h都是超参数,其数学表达式为:
Ht的大小一直不变注意。
到目前为止介绍的模型都是进行预测下一个词元的
这种模型可以实现根据上下文内容,对中间的某个词元进行预测。
具有单个隐藏层的双向循环神经网络的架构
数学表达式为:(注意一些矩阵大小的变化):
注意,双向循环神经网络对预测未来词元的能力,有很大的缺陷,即使困惑度看起来很合理。同时因为梯度链更长,所以双向循环神经网络的训练代价非常高。
#@save
def preprocess_nmt(text):
"""预处理“英语-法语”数据集"""
def no_space(char, prev_char):
return char in set(',.!?') and prev_char != ' '
# 使用空格替换不间断空格
# 使用小写字母替换大写字母
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)
这里进行的是单词级词元化。
#@save
def tokenize_nmt(text, num_examples=None): # num_examples用来控制对多少个文本序列处理
"""词元化“英语-法语”数据数据集"""
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.append(parts[0].split(' ')) # 存放英语:【单词,标点】
target.append(parts[1].split(' ')) # 存放法语:【法语,标点】
return source, target
把出现次数少于2次的低频率词元视为相同的未知词元,并且指定了额外的特定词元,如用来填充到相同的填充词元,序列开始词元结束词元
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['' , '' , '' ])
len(src_vocab)
通过截断(truncation)和 填充(padding)方式实现一次只处理一个小批量的文本序列。
#@save
def truncate_pad(line, num_steps, padding_token):
"""截断或填充文本序列"""
if len(line) > num_steps:
return line[:num_steps] # 截断
return line + [padding_token] * (num_steps - len(line)) # 填充
truncate_pad(src_vocab[source[0]], 10, src_vocab['' ])
然后定义 文本序列转换为小批量数据集训练的函数,为每一行文本序列结尾都添加一个的结束词元。valid_len是每个文本序列的长度,他不包含填充词元:
#@save
def build_array_nmt(lines, vocab, num_steps):
"""将机器翻译的文本序列转换成小批量"""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['' ]] for l in lines]
array = torch.tensor([truncate_pad(
l, num_steps, vocab['' ]) for l in lines])
valid_len = (array != vocab['' ]).type(torch.int32).sum(1)
return array, valid_len
#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):
"""返回翻译数据集的迭代器和词表"""
text = preprocess_nmt(read_data_nmt())
source, target = tokenize_nmt(text, num_examples)
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['' , '' , '' ])
tgt_vocab = d2l.Vocab(target, min_freq=2,
reserved_tokens=['' , '' , '' ])
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 = d2l.load_array(data_arrays, batch_size)
return data_iter, src_vocab, tgt_vocab
机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出,第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。
循环神经网络效果:
编码器将长度可变的输入序列转换成 形状固定的上下文变量 c , 并且将输入序列的信息在该上下文变量中进行编码,可以得到下面的数学公式:
实现循环神经网络编码器时,使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量,他的大小为(输入词表的大小(vocab_size) X 特征向量的大小(embed_size)),对于任意输入词元的索引 i , 嵌入层获取权重矩阵的第 i 行(从 0 开始)以返回其特征向量。
#@save
class Seq2SeqEncoder(d2l.Encoder):
"""用于序列到序列学习的循环神经网络编码器"""
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
# 嵌入层
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
dropout=dropout)
def forward(self, X, *args):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X)# 假如输入为(4,7),经过嵌入层变为了(4,7,8)
# 在循环神经网络模型中,第一个轴对应于时间步
X = X.permute(1, 0, 2)# 第一维和第二维交换了下位置,(7,4,8)
# 如果未提及状态,则默认为0
output, state = self.rnn(X)# output为(7,4,16),state为(2,4,16)
# output的形状:(num_steps,batch_size,num_hiddens)
# state[0]的形状:(num_layers,batch_size,num_hiddens)
return output, state
TIPS:嵌入层替换了one-hot的方法,可以节省很多的内存
class Seq2SeqDecoder(d2l.Decoder):
"""用于序列到序列学习的循环神经网络解码器"""
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
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) # (num_steps,batch_size,embed_size)
# 广播context,使其具有与X相同的num_steps
context = state[-1].repeat(X.shape[0], 1, 1) # 用state的最后一个词元应该是来填充,同时s大小为(X,shape[0],state[1],state[2])
X_and_context = torch.cat((X, context), 2)#
output, state = self.rnn(X_and_context, state)
output = self.dense(output).permute(1, 0, 2)
# output的形状:(batch_size,num_steps,vocab_size)
# state[0]的形状:(num_layers,batch_size,num_hiddens)
return output, state
"""调试输出结果
X's size:torch.Size([7, 4, 8])
state's size:torch.Size([2, 4, 16])
context's size:torch.Size([7, 4, 16])
X_and_context's size:torch.Size([7, 4, 24])
output's size:torch.Size([4, 7, 10])
state's size:torch.Size([2, 4, 16])
"""
类似于语言模型,可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化,并且因为我们给序列填充了特殊词元,所以我们应该把这些给排除在外,当计算loss时。以下代码通过0值化屏蔽不想关的项,来使后面任何不想干预测的计算都是与零的乘积,结果都等于零。
#@save
def sequence_mask(X, valid_len, value=0):
"""在序列中屏蔽不相关的项"""
maxlen = X.size(1)
mask = torch.arange((maxlen), dtype=torch.float32,
device=X.device)[None, :] < valid_len[:, None]
# 这里的代码 [:,None]来增加维度,比如[0,1,2,3,4],则[:,None]的结果为[[0],[1],[2]],[None,:]的结果为[[0 1 2 3 4]]
X[~mask] = value
return X
特定的序列开始词元(“”)和 原始的输出序列(不包括序列结束词元“”) 拼接在一起作为解码器的输入。 这被称为强制教学(teacher forcing), 因为原始的输出序列(词元的标签)被送入解码器。 或者,将来自上一个时间步的预测得到的词元作为解码器的当前输入。
代码实现(部分)
for epoch in range(num_epochs):
timer = d2l.Timer()
metric = d2l.Accumulator(2) # 训练损失总和,词元数量
for batch in data_iter:
optimizer.zero_grad()
X,X_valid_len,Y,Y_valid_len = [x.to(device) for x in batch]
bos = torch.tensor([tgt_vocab['' ]] * Y.shape[0],
device=device).reshape(-1,1)
print("bos'size: " + str(bos.shape))
print("Y'size: " +str(Y.shape))
dec_input = torch.cat([bos,Y[:,:-1]],1) # 强制教学,这里Y[:,:-1]的shape为(64,9),就是不取最后一列
print("dec_input'ssize: " + str(dec_input.shape))
Y_hat,_ = net(X,dec_input,X_valid_len)
l = loss(Y_hat,Y,Y_valid_len)
l.sum().backward() # 损失函数的标量进行“反向传播”
d2l.grad_clipping(net,1)
num_tokens = Y_valid_len.sum()
optimizer.step()
with torch.no_grad():
metric.add(l.sum(),num_tokens)
"""
bos'size: torch.Size([64, 1])
Y'size: torch.Size([64, 10])
dec_input'ssize: torch.Size([64, 10]) 编码器输入
"""
这个部分的代码我是在不是特别懂,尤其是那个for _ in range(num_steps)
的循环。。过两天再研究吧。
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
device, save_attention_weights=False):
"""序列到序列模型的预测"""
# 在预测时将net设置为评估模式
net.eval()
src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
src_vocab['' ]]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['' ])
# 添加批量轴
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['' ]], 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['' ]:
break
output_seq.append(pred)
return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
BLEU的评估是这个n元语法是否出现在标签序列中,定义为:
当预测序列与标签序列完全相同时,BLEU为1.
代码的实现
def bleu(pred_seq, label_seq, k): #@save
"""计算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
score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
return score