从 One-hot 到 Embedding:词的分布式表示
本节要解决什么问题
在后端系统中,你可能写过这样的代码:用枚举类型标记请求类型——GET = 0、POST = 1、PUT = 2……每个请求只有一个"身份",不同枚举值之间毫无关联,GET 和 POST 的距离和 GET 和 DELETE 完全一样。这种表示方式简单直接,但当你想"找相似的请求"或"推荐类似接口"时,枚举类型就无能为力了。
Embedding 要解决的核心问题正是:如何用一组数字来表示一个词,使得语义相近的词在数字空间中也"相近"。 就像用户画像不再只是 user_type = 1(枚举),而是一个 [0.72, -0.31, 0.55, ...] 这样的特征向量——我们可以计算向量之间的距离,发现"高活跃度 + 低付费意愿"的用户和另一个类似用户其实很相似。Embedding 之于文本,做的是同样的事:把离散的词变成可计算距离的连续向量。
这个工具/机制是怎么工作的
One-hot:最简单的词表示
设词表有 $V$ 个词,每个词对应一个 $V$ 维向量,仅在该词位置为 1,其余为 0:
词表: [我, 爱, 你, 中国, 北京]
"北京" -> [0, 0, 0, 0, 1]
"中国" -> [0, 0, 0, 1, 0]
One-hot 的问题:所有词之间的相似度恒为 0,无法表达任何语义关系。
分布式表示:语义相近的词,向量也相近
分布式表示的核心假设是:一个词的意义由它的上下文决定。相近的词出现在相似的上下文中,因此训练后它们的向量也会在空间中靠近。
Embedding 层本质上是一个查表操作:
embedding 矩阵 E ∈ ℝ^(V×d) # V=词表大小,d=向量维度
词 id=3 -> 取出 E[3] -> [0.12, -0.45, 0.78, ...] # d 维向量
语义空间与相似度计算
Embedding 把所有词映射到一个 $d$ 维连续空间。语义相近的词,其向量在空间中距离更近。衡量相似度最常用的两种方式:
点积(Dot Product):
similarity("北京", "中国") = E["北京"] · E["中国"]
= Σ(v_i × u_i)
点积越大,两个向量越"指向同一方向",语义越相近。
余弦相似度(Cosine Similarity):
cosine(A, B) = (A · B) / (||A|| × ||B||)
余弦相似度只关心方向是否一致,不受向量长度影响,是最常用的语义相似度指标。
Skip-gram + Negative Sampling:如何从语料中学习 Embedding
训练流程可以用一句话概括:让"真实上下文词对"的向量靠近,让"随机词对"的向量远离。
语料: "北京 是 中国 的 首都"
窗口半径 w=1,正样本对:
(北京, 是), (北京, 中国)
(是, 北京), (是, 中国)
(中国, 北京), (中国, 的)
...
负采样:每次对每个正样本,随机采样 k 个"肯定不会出现"的词作为负样本
训练过程中,同一个词有两套向量(输入向量表 + 输出向量表),由同一个损失函数同时驱动更新:
1. 中心词向量 与 真实上下文词向量 -> 拉近(损失减小)
2. 中心词向量 与 随机负样本词向量 -> 推远(损失增大)
经过海量样本的迭代,词的语义结构在向量空间中自然涌现。
ASCII 流程图
语料文本
│
▼
滑动窗口构造正样本对 (中心词, 上下文词)
│
▼
负采样:随机采样 k 个负样本词
│
▼
┌──────────────────────────────────────────┐
│ Skip-gram 损失函数 │
│ │
│ 正样本: -log σ(v_c · u_o) │
│ 负样本: -log σ(-v_c · u_neg_i) │
│ │
│ v_c = 中心词输入向量 │
│ u_o = 真实上下文词输出向量 │
│ u_neg_i = 负样本词输出向量 │
└──────────────────────────────────────────┘
│
▼
梯度下降更新两张向量表
│
▼
词的语义结构在 d 维空间中涌现
形式化(可选,附注)
核心公式
Skip-gram + Negative Sampling 损失函数(自然语言描述版):
正样本损失:-log σ(中心词向量 · 上下文词向量),让正样本对的得分越高越好。
负样本损失:sum_i -log σ(-中心词向量 · 第i个负样本向量),让负样本对的得分越低越好。
总损失 = 正样本损失 + 所有负样本损失之和,最小化这个目标即可。
最小可跑示例(PyTorch)
import torch
import torch.nn.functional as F
V, D = 10000, 256 # 词表 10000,维度 256
class SGNS(torch.nn.Module):
def __init__(self):
super().__init__()
self.in_emb = torch.nn.Embedding(V, D) # 输入向量表
self.out_emb = torch.nn.Embedding(V, D) # 输出向量表
def forward(self, center, pos, negs):
v = self.in_emb(center) # [batch, D]
u_pos = self.out_emb(pos) # [batch, D]
u_neg = self.out_emb(negs) # [batch, k, D]
pos_loss = -F.logsigmoid(torch.sum(v * u_pos, dim=1))
neg_loss = -F.logsigmoid(-torch.sum(v.unsqueeze(1) * u_neg, dim=-1)).sum(dim=1)
return (pos_loss + neg_loss).mean()
训练完成后,通常使用输入向量表(in_emb)作为最终词向量,因为它的语义表达更稳定。
本节小结
Embedding 将离散的词映射为连续的 d 维向量,使得语义相似的词在向量空间中距离相近;这个向量空间通过 Skip-gram 的"拉近正样本、推远负样本"训练目标从语料中自动涌现,而相似度则通过点积或余弦相似度来计算。
延伸阅读
- 点积与余弦相似度 — 深入理解相似度计算的核心工具
- 从 Word2Vec 到 Transformer:Embedding 角色演化 — Embedding 在不同模型架构中的演变