import numpy as np# 构造词表vocab = ['apple', 'banana', 'cat', 'dog', 'elephant']# “cat”是词表第2号(索引从0计数)cat_index = vocab.index('cat') # 2# 创建一个全0的长度5向量one_hot = np.zeros(len(vocab))# 将“cat”对应位置置为1one_hot[cat_index] = 1print(one_hot) # [0. 0. 1. 0. 0.]摘要:import numpy as np# 构造词表vocab = ['apple', 'banana', 'cat', 'dog', 'elephant']# “cat”是词表第2号(索引从0计数)cat_index = vocab.index('cat') #
每行代码解释:
• 构造词表,找到“cat”索引• 创建全0向量• 将“cat”所在位置置1• 打印结果One-hot与Embedding对比图
图片来源:lena-voita.github.io/nlp_course/word_embeddings.html
词表:["the", "cat", "sat", "on", "mat", "dog", "log"]
统计窗口为1,计算“cat”左、右各一个词的共现频次:
thecatsatonmatdoglogcat1010000
• “cat”左边出现过1次“the”,右边出现过1次“sat”。from collections import defaultdict# 定义语料库corpus = ["the cat sat on the mat","the dog sat on the log"]# 分词并建立词表tokenized = [sentence.split for sentence in corpus]vocab = sorted(set(word for sent in tokenized for word in sent))word2idx = {word: i for i, word in enumerate(vocab)}# 初始化共现矩阵cooc_mat = np.zeros((len(vocab), len(vocab)), dtype=int)# 遍历每个句子和每个词,统计窗口=1window_size = 1for sent in tokenized:for i, word in enumerate(sent):word_idx = word2idx[word]# 看左边if i - window_size >= 0:left_word = sent[i - window_size]left_idx = word2idx[left_word]cooc_mat[word_idx, left_idx] += 1# 看右边if i + window_size代码逐行解释:
• 用字典映射词到索引• 初始化共现矩阵(行/列都为词表)• 遍历所有句子的每个词,窗口为1时只看前后各一个词• 遇到共现就矩阵+1• 打印结果为DataFrame便于查看• 优点:实现简单,能表达一定语义• 缺点:维度仍然很高,遇到大语料库时会爆炸,且不能有效表达复杂语义与多义性共现矩阵可视化(原文配图)
• 本质:Word2Vec要让词向量空间反映“语境相似性”。它不是单纯统计词与词出现频率,而是让向量学会“猜测上下文”。• Skip-gram模型:用一个“中心词”预测它附近窗口内的所有“上下文词”。• CBOW模型:用窗口内的多个上下文词预测中心词。• 通俗理解:你可以把Skip-gram想象成一个“猜词游戏”:• 输入:现在屏幕上出现一个“中心词”w(比如“cat”)。• 目标:让模型“猜”出它附近会出现哪些词(比如“sat”、“on”、“mat”)。• 训练的过程,就是不断让模型在“猜得越准”时奖励它、猜错了就惩罚它。最终的目标,是让模型学会“见词知上下文”,即给定任何中心词,都能把真正出现过的上下文词概率提到最高。• 概率建模(softmax):用数学语言来说,模型希望“中心词”的向量与它真实的“上下文词”的向量内积越大越好(说明它们关系更亲密)。但我们不仅要让“正确的上下文词”分数高,还得让“不相关的词”分数低。因此,用softmax把所有词的关系都考虑进来:• 分子:中心词和真实上下文词的“亲密度”(向量点积),指数化让更高的亲密度得分增长更快。• 分母:对所有词都算一遍,保证概率和为1,这样模型就要“拉高真词分数、压低其他词分数”。• 全局目标:我们希望对每个样本都让“真实上下文词概率最高”,所以目标是所有中心词与对应上下文词的联合概率最大:• 即所有中心词-上下文词组合出现的概率相乘。• 负对数似然(Negative Log-Likelihood,NLL):在机器学习里,最大化概率通常等价于最小化负对数似然:• 就是让“每一个真实中心-上下文词对的概率”都尽量大(对数后方便求导和优化),全体平均起来损失越小模型就越好。• 你可以理解为:每猜中一次上下文,奖励得分越高;猜错就有惩罚。NLL损失就是让“猜得准的词对”整体分数最大。
Skip-gram模型就是想让每个词的向量都像“磁铁”一样,把它真实的邻居词牢牢吸引在身边,同时把其他词排斥远一点。训练的全部目标,就是让“中心词-上下文词”组合的概率最大。
import torchimport torch.nn as nn# 假设词表大小5,嵌入维度4vocab_size = 5embed_dim = 4embeddings = nn.Embedding(vocab_size, embed_dim)output_layer = nn.Linear(embed_dim, vocab_size)loss_fn = nn.CrossEntropyLosscenter_word_idx = torch.tensor([0]) # 假设中心词索引为0target_context_idx = torch.tensor([2]) # 上下文词索引为2# 得到中心词的词向量embed = embeddings(center_word_idx)# 输出层得到每个词的得分scores = output_layer(embed)# 计算交叉熵损失loss = loss_fn(scores, target_context_idx)print(loss.item)每行解释:
• 随机初始化词向量、输出层• 输入中心词索引• 输出层预测全词表概率• 用目标上下文词算交叉熵损失import torch.optim as optimoptimizer = optim.SGD(list(embeddings.parameters) + list(output_layer.parameters), lr=0.01)for epoch in range(100):optimizer.zero_gradembed = embeddings(center_word_idx)scores = output_layer(embed)loss = loss_fn(scores, target_context_idx)loss.backward # 自动计算所有参数梯度optimizer.step # 更新参数if epoch % 10 == 0:print(f"第{epoch}轮损失:{loss.item:.4f}")每行解释:
• 初始化SGD优化器• 前向传播得到预测• 反向传播+参数更新• 打印每10轮的损失• 训练样本生成:• 遍历文本,每遇到一个词作为“中心词”,就从它左右各取window_size个词作为“正样本”。• 这样训练数据其实就是大量的(中心词, 上下文词)二元组。corpus = ["the cat sat on the mat".split]window_size = 2pairs = for sentence in corpus:for idx, word in enumerate(sentence):center = word# 遍历窗口for i in range(max(0, idx - window_size), min(len(sentence), idx + window_size + 1)):if i != idx:pairs.append((center, sentence[i]))print(pairs)5.5 提速技巧:负采样(Faster Training: Negative Sampling)import torchdef negative_sampling_loss(center_vec, pos_vec, neg_vecs):# center_vec: (embed_dim,)# pos_vec: (embed_dim,)# neg_vecs: (neg_num, embed_dim)pos_score = torch.dot(center_vec, pos_vec)pos_loss = torch.log(torch.sigmoid(pos_score))neg_score = torch.matmul(neg_vecs, center_vec)neg_loss = torch.log(torch.sigmoid(-neg_score)).sumreturn - (pos_loss + neg_loss)# 实际训练时,用batch维度扩展即可每行解释:
• 计算中心词与正样本的点积• 负样本分别做点积,符号取负• 用sigmoid和log得到损失5.6 Word2Vec的两种主要变体(Variants: Skip-Gram 和 CBOW)class CBOWModel(nn.Module):def __init__(self, vocab_size, embed_dim):super(CBOWModel, self).__init__self.embeddings = nn.Embedding(vocab_size, embed_dim)self.output = nn.Linear(embed_dim, vocab_size)def forward(self, context_indices):# context_indices: (batch_size, window*2)# 取平均,作为上下文向量context_vec = self.embeddings(context_indices).mean(dim=1)out = self.output(context_vec)return out每行解释:
• 输入多个上下文词索引• 取平均后作为输入,通过全连接层预测中心词概率其中:
• :词和词在窗口内共现的次数• :词和词的词向量• :两个词的偏置项换句话说,GloVe让“词向量的点积+偏置”近似等于它们在语料中的共现概率对数。
由于对于稀有词对是负无穷,GloVe采用加权平方损失进行优化:
• :权重函数,常取 , 否则为1• 避免极端低频词影响损失GloVe的标准PyTorch实现较复杂,下面是简化版伪代码,着重展示共现矩阵输入与损失计算:
import torchimport torch.nn as nnimport torch.optim as optimclass GloveModel(nn.Module):def __init__(self, vocab_size, embedding_dim):super.__init__# 输入词向量和上下文词向量,均需学习self.wi = nn.Embedding(vocab_size, embedding_dim)self.wj = nn.Embedding(vocab_size, embedding_dim)# 输入词和上下文词偏置self.bi = nn.Embedding(vocab_size, 1)self.bj = nn.Embedding(vocab_size, 1)def forward(self, i_idx, j_idx):# 获取词i和词j的向量与偏置vi = self.wi(i_idx)vj = self.wj(j_idx)bi = self.bi(i_idx).squeezebj = self.bj(j_idx).squeeze# 点积+偏置x = (vi * vj).sum(dim=1) + bi + bjreturn x# 构建共现数据# 假设我们有i_idx, j_idx为词对索引,X_ij为对应共现次数# log_Xij为log(X_ij)# f_Xij为权重# 前向out = model(i_idx, j_idx)# 损失loss = (f_Xij * (out - log_Xij) ** 2).mean代码每行解释:
• 优点:• 利用全局统计信息,能刻画更多全局语义关系• 训练后可直接用于任何下游NLP任务• 局限:• 需要先计算出完整的共现矩阵,遇到大语料库时内存压力很大• 没有上下文动态性(遇到多义词时只给唯一向量)GloVe优化目标与语义几何示意图
常用的三类评估任务:
1. 词相似度(Word Similarity)• 测试向量空间中“man”与“woman”、“cat”与“dog”距离是否接近• 经典数据集:WordSim353、SimLex-9992. 类比推理(Word Analogy)• 检查“king-queen”、“man-woman”等类比关系是否成立• 经典任务:Google Analogy Dataset3. 下游NLP任务效果• 将Embedding作为下游模型输入,看实际分类/序列任务表现提升• 欧氏距离/L2范数• 余弦相似度:最常用,越接近1说明方向越相近from sklearn.metrics.pairwise import cosine_similarityimport numpy as np# 假设我们有两个词向量vec1 = np.array([1.0, 2.0, 3.0])vec2 = np.array([1.1, 2.1, 2.9])# 用sklearn计算余弦相似度sim = cosine_similarity([vec1], [vec2])print("余弦相似度:", sim[0][0]) # 越接近1越相似每行解释:
• 导入余弦相似度工具• 构造两个词向量• 计算并输出相似度from sklearn.manifold import TSNEimport matplotlib.pyplot as plt# 假设embeddings为shape=(num_words, embedding_dim)的词向量矩阵# labels为词语文本# 用t-SNE降到2维tsne = TSNE(n_components=2, random_state=0)embed_2d = tsne.fit_transform(embeddings)# 画散点图plt.figure(figsize=(8, 6))plt.scatter(embed_2d[:, 0], embed_2d[:, 1])# 标出每个点的词for i, label in enumerate(labels):plt.annotate(label, (embed_2d[i, 0], embed_2d[i, 1]))plt.show每行解释:
• 用t-SNE降维• 绘制散点图,每个词向量对应一个点• 用annotate在每个点标上词语Embedding降维可视化原图
# 假设我们有如下词向量embedding_dict = {"king": np.array([1.1, 0.8, 0.7]),"man": np.array([1.0, 0.9, 0.6]),"woman":np.array([1.2, 0.7, 0.8]),"queen":np.array([1.3, 0.6, 0.9])}# King - Man + Womanresult_vec = embedding_dict["king"] - embedding_dict["man"] + embedding_dict["woman"]# 计算result_vec与所有词向量的余弦相似度for word, vec in embedding_dict.items:sim = cosine_similarity([result_vec], [vec])[0][0]print(f"{word} 相似度: {sim:.4f}")每行解释:
• 定义词向量• 做“类比运算”• 对所有词计算相似度,看最接近的词是什么语义线性关系可视化
import torchimport torch.nn as nnvocab_size = 5 # 假设词表5个词embed_dim = 3 # 嵌入维度3embedding = nn.Embedding(vocab_size, embed_dim)# 假设词“cat”索引为2cat_idx = torch.tensor([2])cat_vec = embedding(cat_idx)print("cat 的词向量:", cat_vec.detach.numpy)每行解释:
• 创建词向量矩阵,每个词对应一行• 输入“cat”的索引,查表得到该词的稠密向量• 可以参与神经网络的梯度学习,不断被更新代码简化版:Skip-gram with Negative Samplingimport torchimport torch.nn as nnimport torch.optim as optimclass SkipGramNegSample(nn.Module):def __init__(self, vocab_size, embed_dim):super.__init__self.input_embed = nn.Embedding(vocab_size, embed_dim)self.output_embed = nn.Embedding(vocab_size, embed_dim)def forward(self, center_idx, pos_idx, neg_idx):# center_idx: (batch_size,)# pos_idx: (batch_size, 1)# neg_idx: (batch_size, neg_num)center_vec = self.input_embed(center_idx) # (batch_size, embed_dim)pos_vec = self.output_embed(pos_idx).squeeze(1) # (batch_size, embed_dim)neg_vec = self.output_embed(neg_idx) # (batch_size, neg_num, embed_dim)# 正例log-sigmoidpos_score = torch.mul(center_vec, pos_vec).sum(dim=1)pos_loss = torch.log(torch.sigmoid(pos_score))# 负例log-sigmoidneg_score = torch.bmm(neg_vec, center_vec.unsqueeze(2)).squeeze(2) # (batch_size, neg_num)neg_loss = torch.log(torch.sigmoid(-neg_score)).sum(dim=1)# 总损失(负对数似然,取负号)loss = - (pos_loss + neg_loss).meanreturn loss每行解释:
• 输入Embedding为中心词,输出Embedding为上下文词• 对正例(中心词/真实上下文)做点积• 对负例(中心词/负采样词)做点积,符号取负• 最终损失是所有正例和负例的log-sigmoid总和1. 词向量和one-hot的本质区别是什么?• One-hot无语义信息,Embedding有空间几何和语义距离2. Word2Vec和GloVe有什么区别?• Word2Vec局部预测优化,GloVe全局共现建模3. 怎么选择Embedding维度?• 通常50-300维,任务越复杂可适当增大4. 词向量能表达多义性吗?• 静态Embedding不能,需用上下文动态Embedding来源:SoulJaTech光年