ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

D2L 实战:从零预训练 word2vec 的 skip-gram 模型(负采样 + PTB 数据集)

D2L 实战:从零预训练 word2vec 的 skip-gram 模型(负采样 + PTB 数据集) 文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载本篇文章以 D2LDive into Deep Learning仓库中chapter_natural-language-processing-pretraining/word2vec-pretraining.md为骨架完整演示如何基于 PTBPenn Tree Bank语料用 PyTorch / MXNet 双框架从零实现并预训练 word2vec 的 skip-gram 模型包括数据加载、嵌入层与前向传播、带掩码的二元交叉熵损失、训练循环以及训练完成后用余弦相似度检索语义相近词。读完本文你将掌握一套可直接复制运行的 word2vec 预训练流水线并理解其中 mask、label、负采样等关键设计的底层原理。1. 整体路线从数据迭代器到语义相似的词预训练 word2vec 的完整链路可以分为四步通过d2l.load_data_ptb获得 PTB 数据集的数据迭代器data_iter与词表vocab用嵌入层Embedding Layer与批量矩阵乘法实现 skip-gram 模型的前向传播使用负采样negative sampling对应的二元交叉熵损失训练模型两个嵌入层分别对应中心词向量与上下文词向量训练完成后取中心词嵌入层的权重用余弦相似度找出与查询词最相似的词。其中模型本身的数学定义中心词预测上下文词的条件概率、softmax 形式与训练损失来自 word2vec.md数据集处理下采样、中心词/上下文词抽取、负采样、minibatch 构造来自 word-embedding-dataset.md负采样与近似训练的理论来自 approx-training.md。本文聚焦“实现与预训练”这一环节并穿插源码级讲解。2. 准备数据调用d2l.load_data_ptb训练开始前先获取数据迭代器和词表。本文的预训练实验使用以下超参数批量大小 512、最大上下文窗口 5、每个中心词上下文词对采样的噪声词数 5。#tab mxnet from d2l import mxnet as d2l import math from mxnet import autograd, gluon, np, npx from mxnet.gluon import nn npx.set_np() batch_size, max_window_size, num_noise_words 512, 5, 5 data_iter, vocab d2l.load_data_ptb(batch_size, max_window_size, num_noise_words)#tab pytorch from d2l import torch as d2l import math import torch from torch import nn batch_size, max_window_size, num_noise_words 512, 5, 5 data_iter, vocab d2l.load_data_ptb(batch_size, max_window_size, num_noise_words)2.1 数据流水线的底层实现load_data_ptb是仓库中真实可用的工具函数PyTorch 版本定义在 d2l/torch.pyMXNet 版本定义在 d2l/mxnet.py。其内部按顺序完成如下环节读取语料read_ptb()从d2l.DATA_HUB[ptb]PTB 压缩包校验和为319d85e578af0cdc590547f26231e4e31cdf1e42下载并解压数据按行读取ptb.train.txt每行按空格切分成一个句子。PTB 语料采样自《华尔街日报》文章原始格式就是“每行一句、词与词以空格分隔”。构建词表d2l.Vocab(sentences, min_freq10)将出现次数低于 10 次的词统一替换为unk未知词元原始数据中本就包含unk。高频词下采样subsample()以概率 $P(w_i)\max(1-\sqrt{t/f(w_i)},0)$ 丢弃高频词其中 $f(w_i)$ 是该词的相对频率阈值 $t10^{-4}$见 d2l/torch.py。例如“the”的保留率不到 1/20而低频词如 “join” 会被完整保留。下采样既能减少“the、a、in”这类与大量词共现、信息量低的高频词也能显著加速训练。抽取中心词与上下文词get_centers_and_contexts(corpus, max_window_size)对每个句子、每个位置i随机采样一个 1 到max_window_size之间的整数作为窗口大小窗口内除中心词外的词即为上下文词见 d2l/torch.py。随机窗口大小等效于对窗口尺寸做了数据增强能有效利用全部上下文信息。负采样get_negatives(all_contexts, vocab, counter, K)为每个上下文词对采样 K 个噪声词本文 K5噪声词按词频的 0.75 次幂加权分布抽样且不能是当前的上下文词见 d2l/torch.py。抽样由RandomGenerator实现它一次性缓存 10000 个抽样结果避免频繁调用随机数生成器拖慢数据加载。构造 minibatchbatchify()将每个样本的上下文词与噪声词拼接为contexts_negatives并对齐到批内最长长度max_len同时生成掩码masks1 表示真实词元0 表示填充位和标签labels1 表示正例上下文词0 表示噪声词与填充位见 d2l/torch.py。返回的四个张量形状为中心词(batch_size, 1)、contexts_negatives (batch_size, max_len)、masks (batch_size, max_len)、labels (batch_size, max_len)。训练时每个 minibatch 的max_len由当前批内各样本的实际长度决定max(len(c)len(n) for _, c, n in data)因此不同批次间长度可以不同。PyTorch 端通过DataLoader(..., collate_fnbatchify, num_workersd2l.get_dataloader_workers())注入该批处理函数MXNet 端则使用gluon.data.DataLoader(..., batchify_fnbatchify)两者行为一致。3. 实现 Skip-Gram 模型skip-gram 模型的实现只用嵌入层与批量矩阵乘法两样东西即可完成不需要任何复杂的神经网络结构。3.1 回顾嵌入层Embedding Layer嵌入层把词元的索引映射为特征向量其权重是一个矩阵行数等于字典大小input_dim/num_embeddings列数等于每个词元的向量维度output_dim/embedding_dim。词嵌入模型训练完成后这个权重矩阵就是我们最终需要的词向量。#tab mxnet embed nn.Embedding(input_dim20, output_dim4) embed.initialize() embed.weight#tab pytorch embed nn.Embedding(num_embeddings20, embedding_dim4) print(fParameter embedding_weight ({embed.weight.shape}, fdtype{embed.weight.dtype}))嵌入层的输入是词元单词的索引对任意索引 $i$其向量表示就是权重矩阵的第 $i$ 行。向量维度设为 4 时对形状为(2, 3)的 token 索引小批量嵌入层返回形状(2, 3, 4)的向量#tab all x d2l.tensor([[1, 2, 3], [4, 5, 6]]) embed(x)3.2 定义前向传播skip_gram前向传播的输入包括两部分中心词索引center形状为(batch size, 1)拼接后的上下文词与噪声词索引contexts_and_negatives形状为(batch size, max_len)其中max_len由小批量加载阶段确定见 word-embedding-dataset.md 的 minibatch 部分。这两个变量先分别通过嵌入层embed_v中心词向量与embed_u上下文/噪声词向量转成向量再做批量矩阵乘法输出形状为(batch size, 1, max_len)。输出中的每个元素是一个中心词向量与一个上下文或噪声词向量的点积#tab mxnet def skip_gram(center, contexts_and_negatives, embed_v, embed_u): v embed_v(center) u embed_u(contexts_and_negatives) pred npx.batch_dot(v, u.swapaxes(1, 2)) return pred#tab pytorch def skip_gram(center, contexts_and_negatives, embed_v, embed_u): v embed_v(center) u embed_u(contexts_and_negatives) pred torch.bmm(v, u.permute(0, 2, 1)) return pred这里批量矩阵乘法等价于把形状(batch, 1, embed_size)的v与转置后形状(batch, embed_size, max_len)的u相乘得到(batch, 1, max_len)的相似度得分。打印几个示例输入下的输出形状#tab mxnet skip_gram(np.ones((2, 1)), np.ones((2, 4)), embed, embed).shape#tab pytorch skip_gram(torch.ones((2, 1), dtypetorch.long), torch.ones((2, 4), dtypetorch.long), embed, embed).shape4. 训练负采样 二元交叉熵损失4.1 从负采样目标到二元交叉熵损失在 approx-training.md 中负采样把“上下文词 $w_o$ 出现在中心词 $w_c$ 的上下文窗口内”建模为概率事件 $P(D1\mid w_c, w_o)\sigma(\mathbf{u}_o^\top \mathbf{v}_c)$。如果只最大化所有正例事件的联合概率目标会在所有词向量趋于无穷时取到最大值 1毫无意义因此负采样还要加入从预定义分布中采样的负例把目标函数变成区分正例与负例的二分类问题——这正是二元交叉熵损失binary cross-entropy loss的用武之地。#tab mxnet loss gluon.loss.SigmoidBCELoss()#tab pytorch class SigmoidBCELoss(nn.Module): # Binary cross-entropy loss with masking def __init__(self): super().__init__() def forward(self, inputs, target, maskNone): out nn.functional.binary_cross_entropy_with_logits( inputs, target, weightmask, reductionnone) return out.mean(dim1) loss SigmoidBCELoss()PyTorch 端之所以要自定义SigmoidBCELoss是因为批量数据中含有大量填充位padding必须在损失计算时通过mask把它们排除在外。实现把 mask 作为weight传入binary_cross_entropy_with_logits每个样本的损失为reductionnone后按行求平均。下面用给定变量验证损失的计算第一行pred是模型的预测 logitslabel标记正负例第一行第 1 位为正例第二行第 2 位为正例mask表示哪些位置是有效词元第二行只有前两位有效。#tab all pred d2l.tensor([[1.1, -2.2, 3.3, -4.4]] * 2) label d2l.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]]) mask d2l.tensor([[1, 1, 1, 1], [1, 1, 0, 0]]) loss(pred, label, mask) * mask.shape[1] / mask.sum(axis1)第一行输出约为2.336第二行约为1.654。上面的除法* mask.shape[1] / mask.sum(axis1)把“按行平均”重新归一化为“在非掩码预测上平均”相当于对每个样本去掉填充位的平均损失。下面的手动计算效率较低但能看清原理用 sigmoid 函数逐项验证了上述结果两个输出可理解为在非掩码预测上平均的两个归一化损失。#tab all def sigmd(x): return -math.log(1 / (1 math.exp(-x))) print(f{(sigmd(1.1) sigmd(2.2) sigmd(-3.3) sigmd(4.4)) / 4:.4f}) print(f{(sigmd(-1.1) sigmd(-2.2)) / 2:.4f})输出为2.3362与1.6540与loss(...)的结果一致验证了带掩码的二元交叉熵损失实现是正确的。4.2 初始化模型参数为词表中所有词分别定义两个嵌入层一个用于词作为中心词的情形一个用于词作为上下文词的情形。词向量维度embed_size设为 100#tab mxnet embed_size 100 net nn.Sequential() net.add(nn.Embedding(input_dimlen(vocab), output_dimembed_size), nn.Embedding(input_dimlen(vocab), output_dimembed_size))#tab pytorch embed_size 100 net nn.Sequential(nn.Embedding(num_embeddingslen(vocab), embedding_dimembed_size), nn.Embedding(num_embeddingslen(vocab), embedding_dimembed_size))两个嵌入层分别对应前向传播中的embed_vnet[0]与embed_unet[1]。按词嵌入的惯例训练完成后中心词向量net[0]用作最终词表示。4.3 定义训练循环由于数据中存在填充位训练循环中的损失计算与一般分类任务略有不同需要对pred按label的形状做 reshape并在归一化损失时除以每个样本的有效词元数mask.sum(axis1)。#tab mxnet def train(net, data_iter, lr, num_epochs, deviced2l.try_gpu()): net.initialize(ctxdevice, force_reinitTrue) trainer gluon.Trainer(net.collect_params(), adam, {learning_rate: lr}) animator d2l.Animator(xlabelepoch, ylabelloss, xlim[1, num_epochs]) # Sum of normalized losses, no. of normalized losses metric d2l.Accumulator(2) for epoch in range(num_epochs): timer, num_batches d2l.Timer(), len(data_iter) for i, batch in enumerate(data_iter): center, context_negative, mask, label [ data.as_in_ctx(device) for data in batch] with autograd.record(): pred skip_gram(center, context_negative, net[0], net[1]) l (loss(pred.reshape(label.shape), label, mask) * mask.shape[1] / mask.sum(axis1)) l.backward() trainer.step(batch_size) metric.add(l.sum(), l.size) if (i 1) % (num_batches // 5) 0 or i num_batches - 1: animator.add(epoch (i 1) / num_batches, (metric[0] / metric[1],)) print(floss {metric[0] / metric[1]:.3f}, f{metric[1] / timer.stop():.1f} tokens/sec on {str(device)})#tab pytorch def train(net, data_iter, lr, num_epochs, deviced2l.try_gpu()): def init_weights(module): if type(module) nn.Embedding: nn.init.xavier_uniform_(module.weight) net.apply(init_weights) net net.to(device) optimizer torch.optim.Adam(net.parameters(), lrlr) animator d2l.Animator(xlabelepoch, ylabelloss, xlim[1, num_epochs]) # Sum of normalized losses, no. of normalized losses metric d2l.Accumulator(2) for epoch in range(num_epochs): timer, num_batches d2l.Timer(), len(data_iter) for i, batch in enumerate(data_iter): optimizer.zero_grad() center, context_negative, mask, label [ data.to(device) for data in batch] pred skip_gram(center, context_negative, net[0], net[1]) l (loss(pred.reshape(label.shape).float(), label.float(), mask) / mask.sum(axis1) * mask.shape[1]) l.sum().backward() optimizer.step() metric.add(l.sum(), l.numel()) if (i 1) % (num_batches // 5) 0 or i num_batches - 1: animator.add(epoch (i 1) / num_batches, (metric[0] / metric[1],)) print(floss {metric[0] / metric[1]:.3f}, f{metric[1] / timer.stop():.1f} tokens/sec on {str(device)})几个实现要点优化器统一使用Adam学习率lr0.002设备默认通过d2l.try_gpu()自动选择 GPU若无 GPU 则回退到 CPUtry_gpu定义于 d2l/torch.py训练指标d2l.Accumulator(2)累计“归一化损失之和”与“归一化损失个数”最终损失为两者之比每轮训练中animator在每 1/5 个 epoch 处记录一次平均损失便于实时观察收敛。在 PTB 数据集上执行训练学习率 0.0025 轮#tab all lr, num_epochs 0.002, 5 train(net, data_iter, lr, num_epochs)训练 5 个 epoch 后损失会明显下降同时打印每个 epoch 的处理速度tokens/sec与运行设备。注意这是教学规模的预训练词向量质量与大规模语料上的完整 word2vec 仍有差距但它验证了整条训练流水线是正确的。5. 应用词嵌入用余弦相似度检索语义相近词预训练完成后就可以用训练好的词向量做最经典的语义检索任务找出与给定词余弦相似度最高的若干词。余弦相似度定义为向量夹角余弦$$\frac{\mathbf{x}^\top \mathbf{y}}{|\mathbf{x}| |\mathbf{y}|} \in [-1, 1].$$由于词向量来自 skip-gram 的训练目标中心词与其上下文词共现语义相近的词往往具有相近的上下文分布其向量夹角较小、余弦相似度较高。#tab mxnet def get_similar_tokens(query_token, k, embed): W embed.weight.data() x W[vocab[query_token]] # Compute the cosine similarity. Add 1e-9 for numerical stability cos np.dot(W, x) / np.sqrt(np.sum(W * W, axis1) * np.sum(x * x) 1e-9) topk npx.topk(cos, kk1, ret_typindices).asnumpy().astype(int32) for i in topk[1:]: # Remove the input words print(fcosine sim{float(cos[i]):.3f}: {vocab.to_tokens(i)}) get_similar_tokens(chip, 3, net[0])#tab pytorch def get_similar_tokens(query_token, k, embed): W embed.weight.data x W[vocab[query_token]] # Compute the cosine similarity. Add 1e-9 for numerical stability cos torch.mv(W, x) / torch.sqrt(torch.sum(W * W, dim1) * torch.sum(x * x) 1e-9) topk torch.topk(cos, kk1)[1].cpu().numpy().astype(int32) for i in topk[1:]: # Remove the input words print(fcosine sim{float(cos[i]):.3f}: {vocab.to_tokens(i)}) get_similar_tokens(chip, 3, net[0])实现细节取中心词嵌入层权重W形状为(词表大小, embed_size)再取查询词chip对应的向量x余弦相似度通过批量点积除以范数乘积计算分母加上1e-9防止数值不稳定导致的除零topk(cos, k1)取前 k1 个最相似词topk[1:]跳过第 0 位即查询词本身因为它与自己的相似度恒为 1。vocab.to_tokens(i)把索引还原为词元字符串。以chip为查询词、k3 时输出形如cosine sim0.539: intel cosine sim0.512: microprocessor cosine sim0.491: chips具体数值会因训练随机性而略有差异。可以看到模型捕捉到了 “chip” 与半导体行业相关词汇intel、microprocessor、chips的语义关联——这正是词嵌入训练的目标体现。你也可以把查询词换成其他词如stock、bank、president来观察效果。6. 核心要点小结skip-gram 模型可以只用嵌入层 批量矩阵乘法实现中心词向量与上下文含噪声词向量做批量点积得到形状(batch, 1, max_len)的相似度得分。训练损失采用带掩码的二元交叉熵损失masks排除填充位labels区分正例上下文词与负例噪声词归一化时要除以每个样本的有效词元数mask.sum(axis1)。模型参数是两个独立的嵌入层一个用于中心词一个用于上下文词embed_size100优化器为 Adamlr0.002训练 5 个 epoch。训练完成后中心词向量net[0]作为最终词表示。word2vec 的典型应用是语义相似词检索用余弦相似度对词向量排序返回与查询词最相近的 k 个词需剔除查询词自身。7. 延伸练习使用训练好的模型对其他输入词查找语义相近词。试着调整超参数如batch_size、max_window_size、num_noise_words、embed_size、lr、num_epochs观察结果能否改善。当训练语料很大时常采用“更新参数时再采样”的做法即当前 minibatch 内为每个中心词即时采样上下文词与噪声词而不是一次性全部抽取。这样同一个中心词在不同训练轮次中会面对不同的上下文词或噪声词。想想这样做的好处相当于更多的数据增广、更充分的负例多样性同时降低内存占用并尝试实现这种训练方式。具体来说需要把数据集的中心词/上下文词/噪声词抽取逻辑从预处理阶段迁移到每次迭代内部。深入阅读相关章节可以继续skip-gram 与 CBOW 的数学模型见 word2vec.md数据预处理与 minibatch 装载细节见 word-embedding-dataset.md负采样与分层 softmax 的理论推导见 approx-training.md模型源码实现见 d2l/torch.py 与 d2l/mxnet.py。赞分享文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载相关推荐d2l-en 词嵌入预训练数据集构建实战从 PTB 原始语料到 skip-gram 负采样小批量d2l en 词嵌入预训练数据集构建实战从 PTB 原始语料到 skip gram 负采样小批量 本文围绕《Dive into Deep Learning》文档教程人工智能深度学习NLP计算机视觉强化学习d2l-en 深度解析word2vec 词嵌入原理、Skip-Gram 与 CBOW 模型及负采样训练实战d2l en 深度解析word2vec 词嵌入原理、Skip Gram 与 CBOW 模型及负采样训练实战 导读 本文以《Dive into Deep Lea文档教程人工智能深度学习NLP计算机视觉强化学习动手学深度学习d2l-zh实战在 PTB 语料上使用负采样预训练 word2vec 跳元模型动手学深度学习d2l zh实战在 PTB 语料上使用负采样预训练 word2vec 跳元模型 本文基于《动手学深度学习》中文版d2l zh仓库中的 c人工智能深度学习机器学习教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表