从 One-hot 到 Embedding:词的分布式表示

本节要解决什么问题

在后端系统中,你可能写过这样的代码:用枚举类型标记请求类型——GET = 0POST = 1PUT = 2……每个请求只有一个"身份",不同枚举值之间毫无关联,GETPOST 的距离和 GETDELETE 完全一样。这种表示方式简单直接,但当你想"找相似的请求"或"推荐类似接口"时,枚举类型就无能为力了。

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 的"拉近正样本、推远负样本"训练目标从语料中自动涌现,而相似度则通过点积或余弦相似度来计算。

延伸阅读

results matching ""

    No results matching ""