动手学深度学习9.7. 序列到序列学习(seq2seq)-笔记练习(PyTorch)

本节课程地址:62 序列到序列学习(seq2seq)【动手学深度学习v2】_哔哩哔哩_bilibili

本节教材地址:9.7. 序列到序列学习(seq2seq) — 动手学深度学习 2.0.0 documentation

本节开源代码:...>d2l-zh>pytorch>chapter_multilayer-perceptrons>seq2s.ipynb


序列到序列学习(seq2seq)

正如我们在 9.5节 中看到的, 机器翻译中的输入序列和输出序列都是长度可变的。 为了解决这类问题,我们在 9.6节 中 设计了一个通用的”编码器-解码器“架构。 本节,我们将使用两个循环神经网络的编码器和解码器, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务 (href="https://zh.d2l.ai/chapter_references/zreferences.html#id24">Choet al., 2014,Sutskeveret al., 2014)。

遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。 为了连续生成输出序列的词元, 独立的循环神经网络解码器是基于输入序列的编码信息 和输出序列已经看见的或者生成的词元来预测下一个词元。 图9.7.1 演示了 如何在机器翻译中使用两个循环神经网络进行序列到序列学习。

在 图9.7.1 中, 特定的“<eos>”表示序列结束词元。 一旦输出序列生成此词元,模型就会停止预测。 在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先,特定的“<bos>”表示序列开始词元,它是解码器的输入序列的第一个词元。 其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。 例如,在 (Sutskeveret al., 2014) 的设计中, 正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。 在其他一些设计中 (href="https://zh.d2l.ai/chapter_references/zreferences.html#id24">Choet al., 2014) , 如 图9.7.1 所示, 编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于 8.3节 中语言模型的训练, 可以允许标签成为原始的输出序列, 从源序列词元“<bos>”“Ils”“regardent”“.” 到新序列词元 “Ils”“regardent”“.”“<eos>”来移动预测的位置。

下面,我们动手构建 图9.7.1 的设计, 并将基于 9.5节 中 介绍的“英-法”数据集来训练这个机器翻译模型。

import collections
import math
import torch
from torch import nn
from d2l import torch as d2l

编码器

从技术上讲,编码器将长度可变的输入序列转换成 形状固定的上下文变量 \mathbf{c} , 并且将输入序列的信息在该上下文变量中进行编码。 如 图9.7.1所示,可以使用循环神经网络来设计编码器,可以是双向RNN

考虑由一个序列组成的样本(批量大小是 1 )。 假设输入序列是 x_1, \ldots, x_T , 其中 x_t 是输入文本序列中的第 t 个词元。 在时间步 t ,循环神经网络将词元 x_t 的输入特征向量 \mathbf{x}_t \mathbf{h} _{t-1} (即上一时间步的隐状态) 转换为 \mathbf{h}_t (即当前步的隐状态)。 使用一个函数 f 来描述循环神经网络的循环层所做的变换:

\mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}).

总之,编码器通过选定的函数 q , 将所有时间步的隐状态转换为上下文变量:

\mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T).

比如,当选择 q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T 时 (就像 图9.7.1 中一样), 上下文变量仅仅是输入序列在最后时间步的隐状态 \mathbf{h}_T 。

到目前为止,我们使用的是一个单向循环神经网络来设计编码器, 其中隐状态只依赖于输入子序列, 这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置 (包括隐状态所在的时间步)组成。 我们也可以使用双向循环神经网络构造编码器, 其中隐状态依赖于两个输入子序列, 两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列 (包括隐状态所在的时间步), 因此隐状态对整个序列的信息都进行了编码。

现在,让我们[实现循环神经网络编码器]。 注意,我们使用了嵌入层(embedding layer) 来获得输入序列中每个词元的特征向量。 嵌入层的权重是一个矩阵, 其行数等于输入词表的大小(vocab_size), 其列数等于特征向量的维度(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)# 在循环神经网络模型中,第一个轴对应于时间步(num_steps, batch_size, embed_size)X = X.permute(1, 0, 2)# 如果未提及状态,则默认为0output, state = self.rnn(X)# output的形状:(num_steps,batch_size,num_hiddens)# state的形状:(num_layers,batch_size,num_hiddens)return output, state

循环层返回变量的说明可以参考 8.6节。

下面,我们实例化[上述编码器的实现]: 我们使用一个两层门控循环单元编码器,其隐藏单元数为 16 。 给定一小批量的输入序列 X(批量大小为 4 ,时间步为 7 )。 在完成所有时间步后, 最后一层的隐状态的输出是一个张量(output由编码器的循环层返回), 其形状为(时间步数,批量大小,隐藏单元数)。

encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape

输出结果:

torch.Size([7, 4, 16])

由于这里使用的是门控循环单元, 所以在最后一个时间步的多层隐状态的形状是 (隐藏层的数量,批量大小,隐藏单元的数量)。 如果使用长短期记忆网络,state中还将包含记忆单元信息。

state.shape

输出结果:
torch.Size([2, 4, 16])

[解码器]

正如上文提到的,编码器输出的上下文变量 \mathbf{c} 对整个输入序列 x_1, \ldots, x_T 进行编码。 来自训练数据集的输出序列 y_1, y_2, \ldots, y_{T'} , 对于每个时间步 t' (与输入序列或编码器的时间步 t 不同), 解码器输出 y_{t'} 的概率取决于先前的输出子序列 y_1, \ldots, y_{t'-1} 和上下文变量 \mathbf{c} , 即 P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}) 。

为了在序列上模型化这种条件概率, 我们可以使用另一个循环神经网络作为解码器。 在输出序列上的任意时间步 t', 循环神经网络将来自上一时间步的输出 y_{t^\prime-1} 和上下文变量 \mathbf{c} 作为其输入, 然后在当前时间步将它们和上一隐状态 \mathbf{s}_{t^\prime-1} 转换为 隐状态 \mathbf{s}_{t^\prime}。 因此,可以使用函数 g 来表示解码器的隐藏层的变换:

\mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-1}). (9.7.3)

在获得解码器的隐状态之后, 我们可以使用输出层和softmax操作 来计算在时间步 t′ 时输出 yt′ 的条件概率分布 P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}) 。

根据 图9.7.1 ,当实现解码器时, 我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。 这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。 为了进一步包含经过编码的输入序列的信息, 上下文变量在所有的时间步与解码器的输入进行拼接(concatenate)。 为了预测输出词元的概率分布, 在循环神经网络解码器的最后一层使用全连接层来变换隐状态。

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)# 解码器的输入是编码器的输出和当前的嵌入向量,因此输入维度是 embed_size + num_hiddens# encoder和decoder的隐藏层数相同self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,dropout=dropout)# 将 RNN 的输出映射到词汇表的大小,用于生成下一个单词的概率分布self.dense = nn.Linear(num_hiddens, vocab_size)def init_state(self, enc_outputs, *args):# return编码器的最后时刻所有层的隐藏状态statereturn 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# state[-1]是编码器最后时刻最后一层的隐藏状态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

下面,我们用与前面提到的编码器中相同的超参数来[实例化解码器]。 如我们所见,解码器的输出形状变为(批量大小,时间步数,词表大小), 其中张量的最后一个维度存储预测的词元分布。

decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape

输出结果:
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

总之,上述循环神经网络“编码器-解码器”模型中的各层如 图9.7.2 所示。

  • 编码器是没有输出的RNN。
  • 编码器最后时间步的隐状态用作解码器的初始隐状态。

损失函数

在每个时间步,解码器预测了输出词元的概率分布。 类似于语言模型,可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化。 回想一下9.5节 中, 特定的填充词元被添加到序列的末尾, 因此不同长度的序列可以以相同形状的小批量加载。 但是,我们应该将填充词元的预测排除在损失函数的计算之外。

为此,我们可以使用下面的sequence_mask函数 [通过零值化屏蔽不相关的项], 以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。 例如,如果两个序列的有效长度(不包括填充词元)分别为 1 和 2 , 则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。

#@save
def sequence_mask(X, valid_len, value=0):"""在序列中屏蔽不相关的项"""maxlen = X.size(1)# [None, :]在增加一个新的维度到张量前面# < valid_len[:, None]增加一个新的维度在后面# 结果是一个二维的布尔张量,形状为(batch_size, maxlen),其中每个元素表示当前位置是否小于对应序列的有效长度mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]# 使用掩码将 X 中超出有效长度的部分设置为 value=0X[~mask] = valuereturn XX = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))

输出结果:
tensor([[1, 0, 0],
[4, 5, 0]])

(我们还可以使用此函数屏蔽最后几个轴上的所有项。)如果愿意,也可以使用指定的非零值来替换这些项。

X = torch.ones(2, 3, 4)
sequence_mask(X, torch.tensor([1, 2]), value=-1)

输出结果:
tensor([[[ 1., 1., 1., 1.],
[-1., -1., -1., -1.],
[-1., -1., -1., -1.]],

[[ 1., 1., 1., 1.],
[ 1., 1., 1., 1.],
[-1., -1., -1., -1.]]])

现在,我们可以[通过扩展softmax交叉熵损失函数来遮蔽不相关的预测]。 最初,所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充词元对应的掩码将被设置为0。 最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。

#@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'# 调用父类的forward方法计算交叉熵损失# pred的形状应为(batch_size, vocab_size, num_steps)unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)weighted_loss = (unweighted_loss * weights).mean(dim=1)# 返回一个形状为 (batch_size,) 的张量,包含每个样本的加权损失return weighted_loss

我们可以创建三个相同的序列来进行[代码健全性检查], 然后分别指定这些序列的有效长度为4、2和0。 结果就是,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。

loss = MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),torch.tensor([4, 2, 0]))

输出结果:
tensor([2.3026, 1.1513, 0.0000])

[训练]

在下面的循环训练过程中,如 图9.7.1 所示, 特定的序列开始词元(“<bos>”)和 原始的输出序列(不包括序列结束词元“<eos>”) 拼接在一起作为解码器的输入。 这被称为强制教学(teacher forcing), 因为原始的输出序列(词元的标签)被送入解码器。 或者,将来自上一个时间步的预测得到的词元作为解码器的当前输入。

#@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)# 用带遮蔽的softmax交叉熵损失函数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 张量的形状为 (Y.shape[0], 1),作为序列生成任务的初始输入,用于告诉模型开始生成序列bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],device=device).reshape(-1, 1)# 在每个句首增加<bos>并去掉最后一个词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)}')

现在,在机器翻译数据集上,我们可以 [创建和训练一个循环神经网络“编码器-解码器”模型]用于序列到序列的学习。

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.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.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.019, 8916.8 tokens/sec on cpu

[预测]

为了采用一个接着一个词元的方式预测输出序列, 每个解码器当前时间步的输入都将来自于前一时间步的预测词元。 与训练类似,序列开始词元(“<bos>”) 在初始时间步被输入到解码器中。 该预测过程如 图9.7.3 所示, 当输出序列的预测遇到序列结束词元(“<eos>”)时,预测就结束了。

我们将在 9.8节 中介绍不同的序列生成策略。

#@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)# 添加批量轴# decoder最初输入为<bos>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>']:breakoutput_seq.append(pred)return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

预测序列的评估

我们可以通过与真实的标签序列进行比较来评估预测序列。 虽然 ("https://zh.d2l.ai/chapter_references/zreferences.html#id118">Papineniet al., 2002) 提出的BLEU(bilingual evaluation understudy) 最先是用于评估机器翻译的结果, 但现在它已经被广泛用于测量许多应用的输出序列的质量。 原则上说,对于预测序列中的任意 n 元语法(n-grams), BLEU的评估都是这个 n 元语法是否出现在标签序列中。

我们将BLEU定义为:

\exp\left(\min\left(0, 1 - \frac{\mathrm{len}_{\text{label}}}{\mathrm{len}_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n}, (9.7.4)

其中 lenlabel 表示标签序列中的词元数和 lenpred 表示预测序列中的词元数, k 是用于匹配的最长的 n 元语法。 另外,用 pn 表示 n 元语法的精确度,它是两个数量的比值: 第一个是预测序列与标签序列中匹配的 n 元语法的数量, 第二个是预测序列中 n 元语法的数量的比率。 具体地说,给定标签序列 A 、 B 、 C 、 D 、 E 、F 和预测序列 A 、 B 、 B 、 C 、 D , 我们有p_1 = 4/5 、 p_2 = 3/4 、p_3 = 1/3 和 p_4 = 0 。

根据 (9.7.4) 中BLEU的定义, 当预测序列与标签序列完全相同时,BLEU为 1 。 此外,由于 n 元语法越长则匹配难度越大, 所以BLEU为更长的 n 元语法的精确度分配更大的权重。 具体来说,当 p_n 固定时, p_n^{1/2^n} 会随着 n 的增长而增加(原始论文使用 p_n^{1/n} )。 而且,由于预测的序列越短获得的 pn 值越高, 所以 (9.7.4) 中乘法项之前的系数用于惩罚较短的预测序列。 例如,当 k=2 时,给定标签序列 A 、 B 、 C 、 D 、 E 、 F 和预测序列 A 、 B ,尽管 p_1 = p_2 = 1 , 惩罚因子 \exp(1-6/2) \approx 0.14 会降低BLEU。

[BLEU的代码实现]如下。

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_tokens中所有的n-gram组合,合并成一个字符串,作为字典 label_subs 的键label_subs[' '.join(label_tokens[i: i + n])] += 1for i in range(len_pred - n + 1):# 切出pred_tokens中所有的n-gram组合,并在字典label_subs 中查找,>0表示存在if label_subs[' '.join(pred_tokens[i: i + n])] > 0:num_matches += 1# 未防止重复计算,在字典中去掉已比对上的词组label_subs[' '.join(pred_tokens[i: i + n])] -= 1score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))return score

最后,利用训练好的循环神经网络“编码器-解码器”模型, [将几个英语句子翻译成法语],并计算BLEU的最终结果。

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est bon tombé ., bleu 0.548
i'm home . => je suis chez qui bien <unk> debout ., bleu 0.517

小结

  • 根据“编码器-解码器”架构的设计, 我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。
  • 在实现编码器和解码器时,我们可以使用多层循环神经网络。
  • 我们可以使用遮蔽来过滤不相关的计算,例如在计算损失时。
  • 在“编码器-解码器”训练中,强制教学方法将原始输出序列(而非预测结果)输入解码器。
  • BLEU是一种常用的评估方法,它通过测量预测序列和标签序列之间的 n 元语法的匹配度来评估预测。

练习

  1. 试着通过调整超参数来改善翻译效果。

解:
增加了嵌入层和隐藏层的单元数、隐藏层数,降低了batch_size和学习率,blue提高一些,翻译的准确率有所提高。

embed_size, num_hiddens, num_layers, dropout = 128, 256, 3, 0.1
batch_size, num_steps = 32, 10
lr, num_epochs, device = 0.001, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.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.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.018, 2039.9 tokens/sec on cpu

for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est paresseux ., bleu 0.658
i'm home . => je suis chez moi ., bleu 1.000

2. 重新运行实验并在计算损失时不使用遮蔽,可以观察到什么结果?为什么会有这个结果?

解:
不使用遮蔽时,填充的不相关项都不会被屏蔽,因此这些项的权重也会参与计算损失,会降低计算速度,且会因为不相关项导致误差,降低预测准确率。
代码如下:

# 改一下,weights不用遮蔽
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):# 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
loss = MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),torch.tensor([4, 2, 0]))

输出结果:
tensor([2.3026, 2.3026, 2.3026])

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.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.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.021, 8697.5 tokens/sec on cpu

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va au feu !, bleu 0.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il a moi c'est chiens chiens chiens chiens chiens aboient, bleu 0.000
i'm home . => je suis chez moi ., bleu 1.000

3. 如果编码器和解码器的层数或者隐藏单元数不同,那么如何初始化解码器的隐状态?

解:
首先确保编码器和解码器的层数相匹配,如果编码器层数大于解码器层数,取最后 num_layers_dec 层,如果编码器的层数少于解码器,则填充;然后通过线性变换来调整编码器隐状态的维度,使其匹配解码器的隐藏单元数。
代码如下:

class Seq2SeqDecoder_2(d2l.Decoder):# 编码器和解码器的层数或者隐藏单元数不同def __init__(self, vocab_size, embed_size, num_hiddens_enc, num_hiddens_dec, num_layers_dec, dropout=0, **kwargs):# 调用Seq2SeqDecoder_2super(Seq2SeqDecoder_2, self).__init__(**kwargs)self.embedding = nn.Embedding(vocab_size, embed_size)# 区分编码器和解码器的不同的层数或者隐藏单元数self.rnn = nn.GRU(embed_size + num_hiddens_dec, num_hiddens_dec, num_layers_dec,dropout=dropout)# 添加一个线性层来进行转换, 将编码器的隐状态转换为解码器的隐状态维度self.fc = nn.Linear(num_hiddens_enc, num_hiddens_dec)self.dense = nn.Linear(num_hiddens_dec, vocab_size)def init_state(self, enc_outputs, *args):# enc_outputs[1] 的形状是 (num_layers_enc, batch_size, num_hiddens_enc)enc_last_hidden = enc_outputs[1]# 如果编码器层数大于解码器层数,取最后 num_layers_dec 层if enc_last_hidden.shape[0] > self.rnn.num_layers:enc_last_hidden = enc_last_hidden[-self.rnn.num_layers:]# 如果编码器层数小于解码器层数,复制编码器的隐藏状态以匹配解码器层数elif enc_last_hidden.shape[0] < self.rnn.num_layers:padding_hidden = enc_last_hidden.new_zeros(self.rnn.num_layers - enc_last_hidden.shape[0],enc_last_hidden.shape[1],enc_last_hidden.shape[2])enc_last_hidden = torch.cat([enc_last_hidden, padding_hidden], 0)# 对编码器的最后隐状态进行线性变换以匹配解码器的隐状态维度# (num_layers_dec, batch_size, num_hiddens_dec)enc_last_hidden = self.fc(enc_last_hidden)  return enc_last_hiddendef forward(self, X, state):X = self.embedding(X).permute(1, 0, 2)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)return output, state
encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape

输出结果:
torch.Size([7, 4, 16])

decoder = Seq2SeqDecoder_2(vocab_size=10, embed_size=8, num_hiddens_enc=16, num_hiddens_dec=32, num_layers_dec=3)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape

输出结果:
(torch.Size([4, 7, 10]), torch.Size([3, 4, 32]))

embed_size, num_hiddens_enc, num_hiddens_dec, num_layers_enc, num_layers_dec, dropout = 32, 32, 64, 2, 3, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens_enc, num_layers_enc,dropout)
decoder = Seq2SeqDecoder_2(len(tgt_vocab), embed_size, num_hiddens_enc, num_hiddens_dec, num_layers_dec,dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.022, 6700.5 tokens/sec on cpu

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est riche ., bleu 0.658
i'm home . => je suis chez moi moi moi <unk> ., bleu 0.640

4. 在训练中,如果用前一时间步的预测输入到解码器来代替强制教学,对性能有何影响?

解:
用前一时间步的预测输入到解码器来代替强制教学,称为自回归生成(autoregressive generation)。单独使用这一方法会逐步生成输出序列,导致训练速度急剧下降,并且由于完全依赖模型前一步的预测,会不断累积误差,影响训练性能,导致预测准确性极低。
代码如下:

def train_seq2seq_2(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)# 用 <bos> 作为初始输入dec_input = bosoutputs = []state = None# 开始逐步预测,长度为目标序列的长度for _ in range(Y.shape[1]):# 第一步将<bos>输入,获得预测结果Y_hat, state = net(X, dec_input, X_valid_len, state)   # 使用当前时间步模型预测的结果作为下一个时间步的输入 dec_input = Y_hat.argmax(dim=-1)  outputs.append(Y_hat)# 拼接所有预测结果Y_hat = torch.cat(outputs, dim=1)       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)}')
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.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.EncoderDecoder(encoder, decoder)
train_seq2seq_2(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.046, 2157.4 tokens/sec on cpu

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va allez allez allez allez allez allez allez allez allez, bleu 0.000
i lost . => j'ai venez elles venez bonne bonne bonne bonne suis-je bonne, bleu 0.000
he's calm . => venez sois bonne bonne bonne suis-je bonne suis-je suis-je bonne, bleu 0.000
i'm home . => je le un au suis bonne bonne bonne puis-je bonne, bleu 0.000

5. 用长短期记忆网络替换门控循环单元重新运行实验。

解:
将编码器和解码器的rnn改为nn.LSTM,由于LSTM的state是(H,C),故需将解码器forward中的最后时刻隐状态改为state[0][-1]。
改为LSTM后,超参数不变的情况下,训练速度和预测准确率都有较大提升。 代码如下:

class Seq2SeqEncoder_gru(d2l.Encoder):"""用于序列到序列学习的循环神经网络编码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):super(Seq2SeqEncoder_gru, self).__init__(**kwargs)# 嵌入层self.embedding = nn.Embedding(vocab_size, embed_size)self.rnn = nn.LSTM(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)# 如果未提及状态,则默认为0output, state = self.rnn(X)# output的形状:(num_steps,batch_size,num_hiddens)# LSTM的state为(H, C)return output, state
class Seq2SeqDecoder_gru(d2l.Decoder):"""用于序列到序列学习的循环神经网络解码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):super(Seq2SeqDecoder_gru, self).__init__(**kwargs)self.embedding = nn.Embedding(vocab_size, embed_size)self.rnn = nn.LSTM(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)# H是state[0],state[0][-1]取隐状态最后一层context = state[0][-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
def train_seq2seq_gru(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.LSTM: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)}')
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder_gru(len(src_vocab), embed_size, num_hiddens, num_layers,dropout)
decoder = Seq2SeqDecoder_gru(len(tgt_vocab), embed_size, num_hiddens, num_layers,dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq_gru(net, train_iter, lr, num_epochs, tgt_vocab, device)

输出结果:
loss 0.019, 11335.1 tokens/sec on cpu

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

输出结果:
go . => va !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est riche ., bleu 0.658
i'm home . => je suis chez moi ., bleu 1.000

6. 有没有其他方法来设计解码器的输出层?

解:
可以使用注意力机制,通过计算每个隐状态与编码器输出的相关性得分来选择与输入序列中相关的部分进行输出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/455051.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pdf编辑软件有哪些?方便好用的pdf编辑软件分享

PDF文件因其跨平台、格式固定的特性&#xff0c;成为了工作、学习和生活中不可或缺的一部分。然而&#xff0c;随着需求的不断增加&#xff0c;仅仅阅读PDF文件已难以满足我们的需求&#xff0c;编辑、转换PDF文件成为了新的焦点&#xff0c;下面给大家分享几款方便好用的PDF编…

《Linux从小白到高手》综合应用篇:深入理解Linux常用关键内核参数及其调优

1. 题记 有关Linux关键内核参数的调整&#xff0c;我前面的调优文章其实就有涉及到&#xff0c;只是比较零散&#xff0c;本篇集中深入介绍Linux常用关键内核参数及其调优&#xff0c;Linux调优80%以上都涉及到内核的这些参数的调整。 2. 文件系统相关参数 fs.file-max 参数…

SpringBoot3 + MyBatisPlus 快速整合

一、前言 MyBatis 最佳搭档&#xff0c;只做增强不做改变&#xff0c;为简化开发、提高效率而生。 这个发展到目前阶段已经很成熟了&#xff0c;社区也比较活跃&#xff0c;可以放心使用。官网地址&#xff1a;https://baomidou.com 二、快速开始 引入依赖 这里我引入了核心…

stm32单片机个人学习笔记11(ADC模数转换器)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

Linux系列-Linux的常见指令(三)

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” mv 1.剪切文件&#xff0c;目录 2.重命名 首先&#xff0c;我们先来看第一个作用 假如说&#xff0c;我们原先存在一个hello.txt&#xff0c;我们如果想要将这个文件移动到其他的…

上拉电阻和下拉电阻在电路中的作用(一)

上拉电阻和下拉电阻在电路中的作用&#xff08;一&#xff09; 1.什么是上下拉电阻2.上下拉电阻的作用&#xff1a;2.1.维持输入引脚处于稳定状态。2.2.配合三极管和MOS进行电平转换电路设计2.3.OC、OD电路&#xff08;Open Collector集电极开路、Open Drain漏电极开路&#xf…

什么是分库分表?为什么要分库分表?什么时候需要分库分表?怎么样拆分?(数据库分库分表详解)

文章目录 1、什么是分库分表&#xff1f;1.1、分库分表的概念1.2、分库分表的方式1.2.1、垂直分库1.2.2、垂直分表1.2.3、水平分库1.2.4、水平分表 2、为什么要分库分表&#xff1f;3、什么时候需要分库分表&#xff1f;4、分库分表的数据路由4.1、数据路由的目的4.2、数据路由…

class 9: vue.js 3 组件化基础(2)父子组件间通信

目录 父子组件之间的相互通信父组件传递数据给子组件Prop为字符串类型的数组Prop为对象类型 子组件传递数据给父组件 父子组件之间的相互通信 开发过程中&#xff0c;我们通常会将一个页面拆分成多个组件&#xff0c;然后将这些组件通过组合或者嵌套的方式构建页面。组件的嵌套…

2024开放原子开源生态大会 | 麒麟信安携手openEuler共建开源生态,共塑产业未来

9月25日-27日&#xff0c;由开放原子开源基金会主办的2024开放原子开源生态大会在北京开幕&#xff0c;大会以“开源赋能产业&#xff0c;生态共筑未来”为主题。工业和信息化部党组书记、部长金壮龙&#xff0c;北京市委副书记、市长殷勇&#xff0c;工业和信息化部总经济师、…

汇川机器人与PLC通信-ModbusTCP超详细案例

#SCARA机器人与H5UPLC通过ModbusTCP通信,HMI界面手动操作# 应用背景: 本项目案例部分软件界面已被更新,如机器人示教软件旧版本S01.19R03。但通信的原理基本一致,废话少说,我们直接上图。 一、PLC端配置 1.添加ROB通讯表(自定义),变量表内容包括ROB系统变量,IN区和…

Cadence元件A属性和B属性相互覆盖

最近在使用第三方插件集成到Cadence,协助导出BOM到平台上&#xff0c;方便对BOM进行管理和修改&#xff0c;结果因为属性A和属性B不相同&#xff0c;导致导出的BOM错误。如下图&#xff1a; ​​ 本来我们需要导出Q12&#xff0c;结果给我们导出了Q13&#xff0c;或者反之&…

基于opencv的人脸闭眼识别疲劳监测

1. 项目简介 本项目旨在实现基于眼部特征的眨眼检测&#xff0c;通过监测眼睛开闭状态来计算眨眼次数&#xff0c;从而应用于疲劳监测、注意力检测等场景。使用了面部特征点检测算法&#xff0c;以及眼部特征比率&#xff08;EAR, Eye Aspect Ratio&#xff09;来判断眼睛的闭…

Python 实现 excel 数据过滤

一、场景分析 假设有如下一份 excel 数据 shop.xlsx, 写一段 python 程序&#xff0c;实现对于车牌的分组数据过滤。 并以车牌为文件名&#xff0c;把店名输出到 车牌.txt 文件中。 比如 闽A.txt 文件内容为&#xff1a; 小林书店福州店1 小林书店福州店2 二、依赖安装 程序依…

【C++】拆分详解 - 模板

文章目录 一、泛型编程二、函数模板1. 概念2. 语法3. 函数模板的原理4. 函数模板的实例化5. 模板参数的匹配原则 三、类模板1. 语法2. 实例化 四、模板的特化1. 概念2. 函数模板特化3. 类模板特化3.1 全特化3.2 偏特化 / 半特化3.3 应用示例 4. 小结 五、模板的分离编译1. 分离…

Java:抽象类和接口

一.抽象类 1.抽象类概念和语法 ⨀概念&#xff1a; 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是并不是所有的类都是用来描绘对象的&#xff0c;如果一个类中没有包含足够的信息来描绘一个具体的对象&#xff0c;这样的类就是抽象类。 ⨀语…

JMeter使用不同方式传递接口参数

1、使用 HTTP 请求中的参数&#xff1a; 在 JMeter 的测试计划中&#xff0c;添加一个 "HTTP 请求" 元件。 在 "HTTP 请求" 元件的参数化选项中&#xff0c;可以添加参数的名称和值。可以手动输入参数&#xff0c;也可以使用变量来传递参数值。 如果要使…

Golang | Leetcode Golang题解之第497题非重叠矩形中的随机点

题目&#xff1a; 题解&#xff1a; type Solution struct {rects [][]intsum []int }func Constructor(rects [][]int) Solution {sum : make([]int, len(rects)1)for i, r : range rects {a, b, x, y : r[0], r[1], r[2], r[3]sum[i1] sum[i] (x-a1)*(y-b1)}return Sol…

自定义多级联动选择器指南(uni-app)

多端支持&#xff1a;可以运行在H5、APP、微信小程序还是支付宝小程序&#xff0c;都可以轻松使用改组件。自定义配置&#xff1a;您可以根据需要配置选择器的级数&#xff0c;使其适应不同的数据结构和用例。无限级联&#xff1a;此组件支持无限级联选择&#xff0c;使您能够创…

最好的ppt模板网站是哪个?做PPT不可错过的18个网站!

现在有很多PPT模板网站&#xff0c;但真正免费且高质量的不多&#xff0c;今天我就分享主流的国内外PPT模板下载网站&#xff0c;并且会详细分析这些网站的优缺点&#xff0c;这些网站都是基于个人实际使用经验的&#xff0c;免费站点会特别标注&#xff0c;让你可以放心下载&a…

信息安全工程师(64)其他恶意代码分析与防护

前言 恶意代码是指那些能够损害系统用户和系统所有者利益的软件&#xff0c;是故意在计算机系统上执行恶意任务的恶意代码的集合。 一、恶意代码分析 病毒&#xff08;Virus&#xff09; 定义&#xff1a;病毒是一种人为制造的、能够进行自我复制的、具有对计算机资源的破坏作用…