记录学习《手动学习深度学习》这本书的笔记(六)
看到第九章:现代循环神经网络了,循环神经网络这块真的有点难,而且老师也没有细讲这块,只能自己慢慢理解。
第九章:现代循环神经网络
9.1 门控循环单元(GRU)
这一节介绍了一个循环神经网络的“变体”,虽然长短期记忆网络(LSTM)出现得更早,但是门控循环单元(GRU)更为简单。
比起最初的循环神经网络,门控神经单元中多了两个门:重置门和更新门。每一个时间步都有这两个门,每经过一个时间步就要计算两个门的值,然后根据两个门更新隐状态和计算输出。
每一个时间步大致就是这样的计算过程,其中每个门和参数的计算如下:
重置门: (表示候选隐状态与上一隐状态的相关度)
更新门: (表示不更新隐状态的“概率”)
候选隐状态:
隐状态:
其中表示按元素乘积,表示sigmoid激活函数,保证门在(0, 1)间,表示tanh激活函数,保证候选隐状态在(-1, 1)间。
当前步的输出和普通循环神经网络一样计算。
输出:
一共要初始化9(上面公式的参数)+2(输出层照原样)个参数,还有隐状态。
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device)*0.01
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))
W_xz, W_hz, b_z = three() # 更新门参数
W_xr, W_hr, b_r = three() # 重置门参数
W_xh, W_hh, b_h = three() # 候选隐状态参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
初始化隐状态:
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
按照公式生成前向传播:
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,)
然后就可以将函数代入之前的循环神经网络代码进行训练和预测。(略)
或者如果直接调用模型简洁实现的话:
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
9.2 长短期记忆网络(LSTM)
在各种地方见过这个概念但一直不明白是什么东西这回总算给我认识到了盒盒盒盒盒盒盒盒盒盒盒……
其实很简单,和门控循环单元(GRU)类似,也是循环神经网络的一种变体。
比GRU要稍微复杂一点。
一个时间步总体流程如下:
这里引入了一个和隐状态类似的记忆元,有些地方把记忆元也称为特殊的隐状态。
接着就是熟悉的各种门,遗忘门、输入门、输出门,没有候选隐状态了只有候选记忆。
其中每个门和参数的计算如下:
输入门: (表示记忆元更新“概率”)
遗忘门: (表示记忆元与上一记忆元相同“概率”)
输出门: (代表隐状态与记忆元的关系)
候选记忆元:
记忆元:
隐状态:
其中,隐状态是记忆元的tanh的门控版本,其值始终在(-1, 1)间。
当前步的输出和普通循环神经网络一样计算。
输出:
因为输出门需要使用上一时间步的隐状态,所以当前隐状态还是与上一隐状态相关的,并且仍需存储起来,到下一个时间步使用。
LSTM的模型参数有12(上面的公式)+2(和之前一样,还有计算当前时间步输出的参数)个,还有隐状态和记忆元需要初始化。
初始化参数:
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return np.random.normal(scale=0.01, size=shape, ctx=device)
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
np.zeros(num_hiddens, ctx=device))
W_xi, W_hi, b_i = three() # 输入门参数
W_xf, W_hf, b_f = three() # 遗忘门参数
W_xo, W_ho, b_o = three() # 输出门参数
W_xc, W_hc, b_c = three() # 候选记忆元参数
# 输出层参数
W_hq = normal((num_hiddens, num_outputs))
b_q = np.zeros(num_outputs, ctx=device)
# 附加梯度
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]
for param in params:
param.attach_grad()
return params
初始化隐状态和记忆元:
def init_lstm_state(batch_size, num_hiddens, device):
return (np.zeros((batch_size, num_hiddens), ctx=device),
np.zeros((batch_size, num_hiddens), ctx=device))
按照公式前向传播:
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 = npx.sigmoid(np.dot(X, W_xi) + np.dot(H, W_hi) + b_i)
F = npx.sigmoid(np.dot(X, W_xf) + np.dot(H, W_hf) + b_f)
O = npx.sigmoid(np.dot(X, W_xo) + np.dot(H, W_ho) + b_o)
C_tilda = np.tanh(np.dot(X, W_xc) + np.dot(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * np.tanh(C)
Y = np.dot(H, W_hq) + b_q
outputs.append(Y)
return np.concatenate(outputs, axis=0), (H, C)
然后就是训练和预测(略)。
简洁实现:
lstm_layer = rnn.LSTM(num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
9.3 深度循环神经网络
之前的普通循环神经网络只有一个隐藏层h,之前提到可以有多层隐藏层但是计算时会太复杂所以一般不用,但还是可以实现的。
多层循环神经网络be like:
公式和单层的差不多,只是中间层是通过前一层的隐状态h计算而不是输入x。
比如第 l 层第 t 个时间步:
其中 表示第 l 层的激活函数。
而且每一层也可以是GRU或LSTM,不一定是上述普通循环神经网络层。
可以直接调参进行简洁实现。
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
device = d2l.try_gpu()
lstm_layer = rnn.LSTM(num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
多层神经网络并不常用,需要大量调参确保合适收敛,模型初始化也要谨慎。
9.4 双向循环神经网络
以往我们的循环神经网络都是单向预测,预测往后的数据,然而在一些词语填充任务,我们也可以进行双向预测填补空缺。
在之前我们的模型是这样的:
这是一个前向递归,保证每次选取的值都是最佳的,然后就可以进行后续预测。
双向就是这样:
既可以从前往后,又可以从后往前。
每一时间步计算如下:
其中和分别是前向递归的隐状态和后向递归的隐状态。
将它们连接起来就是总的隐状态,根据连接后的隐状态即可计算当前输出。
双层循环神经网络在实践中的应用非常少,仅应用于部分场景,比如填充缺失单词。并且它的运算速度非常慢。
简洁实现时,只需要修改模型的参数即可:
from mxnet import npx
from mxnet.gluon import rnn
from d2l import mxnet as d2l
npx.set_np()
# 加载数据
batch_size, num_steps, device = 32, 35, d2l.try_gpu()
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 通过设置“bidirective=True”来定义双向LSTM模型
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2
lstm_layer = rnn.LSTM(num_hiddens, num_layers, bidirectional=True)
model = d2l.RNNModel(lstm_layer, len(vocab))
# 训练模型
num_epochs, lr = 500, 1
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
而且用这个模型只往前推效果也不好。
9.5 机器翻译与数据集
机器翻译是一种序列转换模型。
在使用神经网络进行端到端学习兴起前。使用统计学方法在这一领域一直占据主导地位,那种叫统计机器翻译,而我们要讲的是另一种基于神经网络方法的神经机器翻译。
我们以英语-法语翻译为例。
加载数据集“英语-法语”,数据集中每一行为制表符分隔的文本序列对。
#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
'94646ad1522d915e7b0f9296181140edcf86a4f5')
#@save
def read_data_nmt():
"""载入“英语-法语”数据集"""
data_dir = d2l.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()
print(raw_text[:75])
Go. Va ! Hi. Salut ! Run! Cours ! Run! Courez ! Who? Qui ? Wow! Ça alors !
预处理,比如使用空格代替不间断空格,字符全部改成小写,在单词和标点间插入空格。
#@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)
text = preprocess_nmt(raw_text)
print(text[:80])
go . va ! hi . salut ! run ! cours ! run ! courez ! who ? qui ? wow ! ça alors !
将数据词元化(单词级),将其改写为英语和法语两个列表,元素一一对应。
#@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.append(parts[0].split(' '))
target.append(parts[1].split(' '))
return source, target
source, target = tokenize_nmt(text)
source[:6], target[:6]
([['go', '.'], ['hi', '.'], ['run', '!'], ['run', '!'], ['who', '?'], ['wow', '!']], [['va', '!'], ['salut', '!'], ['cours', '!'], ['courez', '!'], ['qui', '?'], ['ça', 'alors', '!']])
然后我们使用先前建立词表的函数对数据建立词表,这里除了原有数据,我们还要往词表中添加一些特殊词:
<unk>:出现次数少于2次的低频词
<pad>:填充词(马上会讲到)
<bos>:开始词,添加在文本开头,表示文本开始
<eos>:结束词,添加在文本末尾,表示文本结束
为了提高计算效率,我们将文本划分为小批量,每次只对小批量文本进行处理,但是从上面的数据可以看出,每段文本的长度不一定相等,于是我们需要预先处理数据,对其进行适当截断或填充。
我们设计一个函数,判断如果词元数小于num_steps则填充<pad>词元,直到达到num_steps;如果大于,则截断文本序列。
#@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['<pad>'])
然后可以定义一个将文本序列全部转化为一定长度训练数据的函数:
#@save
def build_array_nmt(lines, vocab, num_steps):
"""将机器翻译的文本序列转换成小批量"""
lines = [vocab[l] for l in lines]
lines = [l + [vocab['<eos>']] for l in lines]
array = torch.tensor([truncate_pad(
l, num_steps, vocab['<pad>']) for l in lines])
valid_len = (array != vocab['<pad>']).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=['<pad>', '<bos>', '<eos>'])
tgt_vocab = d2l.Vocab(target, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
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
9.6 编码器-解码器架构
好了即将迎来本章最难理解的部分——编码器解码器架构。
这个架构的意思是先将输入通过编码器生成输出和当前状态,然后再用当前状态和输入放进解码器生成输出。
比如翻译"They are watching ."这个英文句子为法语,那么我们首先将数据处理成小批量(上一节说的处理数据为相同长度),"They""are""watching""."。编码器首先通过一系列比如LSTM操作将这个数据编码,得到最后的状态,然后将这个状态和开始字符<bos>放入解码器中得到输出“Ils”"regordent""."。
编码器基本构造:
from mxnet.gluon import nn
#@save
class Encoder(nn.Block):
"""编码器-解码器架构的基本编码器接口"""
def __init__(self, **kwargs):
super(Encoder, self).__init__(**kwargs)
def forward(self, X, *args):
raise NotImplementedError
解码器基本构造:
#@save
class Decoder(nn.Block):
"""编码器-解码器架构的基本解码器接口"""
def __init__(self, **kwargs):
super(Decoder, self).__init__(**kwargs)
def init_state(self, enc_outputs, *args):
raise NotImplementedError
def forward(self, X, state):
raise NotImplementedError
编码器-解码器基本构造:
#@save
class EncoderDecoder(nn.Block):
"""编码器-解码器架构的基类"""
def __init__(self, encoder, decoder, **kwargs):
super(EncoderDecoder, self).__init__(**kwargs)
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)
9.7 序列到序列学习(seq2seq)
这一节是对编码器-解码器架构的实现,用它来实现seq2seq类的学习任务。
我们需要实现的是上图这样的翻译任务,其中,<eos>和<bos>代表开始和结束,再解码器中,我们输入<bos>,然后让它生成翻译后的文本,一直到它预测出<eos>就结束预测。
其中需要注意的是,编码器的用途在于遍历输入文本,得到最终的状态(根据之前的知识,最终状态包含输入文本的信息),编码器最终的状态作为解码器最开始的状态,然后通过解码器预测实现任务。
先实现编码器:
编码器输入x1、x2……xT,与循环神经网络一样,编码器将在第t个时间步遍历输入xt,并生成相应隐状态ht。
一般来说,最后要将所有隐状态结合:
我们这里使用=hT,也就是上下文变量直接取最后时间步的状态。
在这一步之前,我们还要实现一个嵌入层(直接调用模型embedding),将词元转化为特征向量。
#@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)
# 在循环神经网络模型中,第一个轴对应于时间步
X = X.permute(1, 0, 2)
# 如果未提及状态,则默认为0
output, state = self.rnn(X)
# output的形状:(num_steps,batch_size,num_hiddens)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state
其中,GRU会返回一个输出和一个最后时间步的隐状态,这里我们只需要后者。
然后实现解码器:
解码器输入训练数据的输出序列y1、y2、……yT和编码器最后的状态。
(注意这里训练时是传训练数据的输出序列,如果是预测就传入<bos>)
设解码器隐状态为s。
解码器第一个时间步的隐状态是由编码器最后时间步的隐状态初始化的,这就要求编码器解码器使用相同数量的层和隐藏单元。
上下文变量c在所有时间步与解码器输入进行连接。
并且解码器比编码器多了一个全连接层变换隐状态,由于预测输出词元概率分布。
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)
# 广播context,使其具有与X相同的num_steps
context = state[-1].repeat(X.shape[0], 1, 1)
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的形状:(num_layers,batch_size,num_hiddens)
return output, state
这里选取的上下文变量c是编码器最后一层的最后一个时间步的状态state,代码中将其复制到了每一个时间步。
总之实现的整个结构如下:
然后实现损失函数,要注意的是我们之前为了凑文本长度填充的eps不能算进损失函数中,所以我们通过零值化屏蔽不相关预测,将有效长度后面全部清零。
实现将非有效行转为value的函数:
#@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]
X[~mask] = value
return X
X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))
然后实现计算损失:
#@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)
weights = sequence_mask(weights, valid_len)
self.reduction='none'
unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
pred.permute(0, 2, 1), label)
weighted_loss = (unweighted_loss * weights).mean(dim=1)
return weighted_loss
这里设置了权重weight,将有效的文本处设为1,无效文本设为0,然后计算不加权的loss损失(需要变换维度),乘以weight就可以计算有效损失。
然后就可以训练:
#@save
def train_seq2seq(net, data_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.to(device)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = MaskedSoftmaxCELoss()
net.train()
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[10, num_epochs])
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['<bos>']] * Y.shape[0],
device=device).reshape(-1, 1)
dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学
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)
if (epoch + 1) % 10 == 0:
animator.add(epoch + 1, (metric[0] / metric[1],))
print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
f'tokens/sec on {str(device)}')
训练过程中需要输入训练数据的X、Y和对应有效长度,其中X、Y分别是翻译目标和翻译结果。
然后将X传入net的编码器,<bos>+Y(需要设置开始词元)传入net的解码器,将预测结果和实际对比计算损失,和以前的做法差不多。
训练时:
预测时是这样:
(这里借用了评论区修改的图,原文应该有错误)
#@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['<eos>']]
enc_valid_len = torch.tensor([len(src_tokens)], device=device)
src_tokens = d2l.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
这段代码将数据依次经过编码器和解码器,解码器时预测到终止符<eos>就停止预测。
评估方法有点特殊,是BLEU方法,这个评估方法将预测值比真实值短的部分长度也计算在了惩罚权重里(长度相等或预测值比长真实值长则这个权值为0),然后利用乘积计算每个预测的概率,并且为了避免长度越大的文本预测损失更可能会越大,将长度短也计算在了惩罚里。
评估代码:
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
(这一节真的是目前看到最难的一节了……)
9.8 束搜索
之前我们一直是用贪心搜索,每一步取目前概率最大的那个字母作为预测值,但是在一些情况下,这样取得的字符串概率可能并不是最大的。
这就引出了不同的搜索方法,寻找真实概率最大的字符串。
如果使用穷举搜索,每一种字符串组合都计算一遍概率,那运算难度显然太大了。
于是我们提出束搜索,每经过一个时间步选取当前概率最大的 n 个字符进行后续预测。
图上已经表达得很明白了。