你有没有遇到过这种情况跟一个完全不懂中文的人解释“缘分”这个词你翻词典越查越糊涂看翻译更是莫名其妙。真正有效的办法其实是让他长期泡在中文环境里听足够多次“我们有缘分”“缘分到了”“跟你有缘无分”他再笨也会慢慢生成一个说不清道不明但特别准确的直觉。深度学习里的 Embedding 走的正是这条路。它不靠词典定义不靠人工规则靠的是“一个词出现过的所有上下文”来理解语义最终把每个词变成一串可计算的数字向量。这串数字就是 AI 的“语义地图”——地图上意思相近的词彼此挨着有类似关系的词沿着同一个方向排列。你问一个词是什么意思AI 不看定义打开地图看它周围是谁就知道了答案。这篇文章我们来聊聊 Embedding 那点事儿。我会从最朴素的词表示方法讲起解释为什么它不够用然后拆解 Word2Vec 这类经典算法背后的设计取舍再带着你用 PyTorch 从零搭一个最小可用的 CBOW 训练流程最后分享我在真实项目里踩过的几个坑。适合刚接触自然语言处理的初学者也适合用过词向量但没把原理捋顺的读者。这是上篇重点是“怎么把词画进空间”。下篇我再接着聊怎么把这组向量真正用起来比如向量检索、相似度召回以及和生成模型配合时的组织方式。1. 从“独热编码”说起计算机到底怎么理解“苹果”1.1 老办法是一根“断掉的电话线”假设你面前坐着一个人他只能接收 01 串信息。你要告诉他“苹果”这个词最简单粗暴的方式是——把世界上所有词排成一张表“苹果”排在第三位就传给他一个0001000...只有第三位是 1。这就是 one-hot encoding也叫独热编码。这个方法逻辑上没错在简单任务里也能用但问题很快暴露。首先是维度灾难。假设词汇表里有一万个词每个词是一个一万维的向量但其中只有一维是 1其他全是 0。存储效率极低而且向量维度和词表规模直接挂钩——今天业务里新增了几个品牌名词表就要扩之前的向量又不好随便动牵一发而动全身。其次是语义真空。独热向量里 1 的位置只是词的顺序编号不携带任何意义。“苹果”编码成第 3 位“香蕉”编码成第 78 位计算机没法从第 3 位和第 78 位之间推出任何关系。数学上算两个向量距离交集永远是零相似度恒为 0。明明苹果和香蕉都是水果词向量告诉计算机的是“这俩词毫无关系”。我早期做一个文本分类实验时用的就是词袋加独热编码。模型确实能收敛但语义稍微绕一点的样本就崩。比如“这家店性价比高但服务一般”——“性价比高”和“服务一般”在独热视角下是两个独立维度模型学不到“评价倾向”这种抽象特征因为它根本看不到词和词之间的关联。独热编码更像一本电话簿能查到词但查不到词义。那我们要的不只是词表而是一张地图。地图上每个词是一个点点和点之间有距离、有方向近的意义相近远的八竿子打不着。问题变成这张地图怎么建1.2 分布式表示用“邻居”解释词义关键思路来自语言学的分布假说一个词的意义可以从它反复出现的上下文里提取。与其给每个词分配一个互不相干的编号不如给每个词生成一个“稠密向量”——比如 300 维的浮点数。词和词的关系就体现在这些数值分布的模式里。比如“苹果”和“香蕉”如果它们都是从同一批语料里训出来的你大概率会发现这两个向量在空间里距离很近因为它们的用法太像了经常出现在同类型的位置“我买了几斤苹果”“我买了几斤香蕉”。而“苹果”和“路由器”就离得比较远。这里有个反直觉的地方我们没有显式告诉计算机“苹果是一种水果”是它自己从语料里统计出来的。计算机读到足够多包含“苹果”的句子后自然会把“经常出现在水果类词的上下文里”这个特征编码进向量。语义不是被写进去的是被统计出来的。生活里也有类似经历。你在一个陌生城市住上三个月没人专门给你画过地图但每天走哪条路、经过什么地标你会慢慢形成一张“附近感知图”。你可能说不清整个城市的全貌却能准确描述“从住处往东走两个路口那个卖早点的旁边”。分布式表示就是这个意思不在全局坐标里定义词而在与周围词的关系网络里定义词。1.3 给“语义地图”一个空间直觉现在我们可以建立一个直观模型。假设用一个二维平面代表语义空间把几个词画上去“苹果”在坐标 (0.05, 0.72)“香蕉”在坐标 (0.10, 0.68)“水果”在坐标 (0.03, 0.76)“手机”在坐标 (-0.21, 0.31)“苹果”和“香蕉”距离近“苹果”和“手机”距离远。如果你把几千个词画到同一张图上会发现更有意思的模式所有水果聚在一片区域所有电子产品在另一片动词们各有各的聚集区而且“吃”“喝”“嚼”这类动作离得很近。这就是语义地图的雏形。更有意思的是向量运算。经典例子“国王”向量减去“男人”向量再加上“女人”向量结果非常接近“王后”——语义空间中“国王 - 男人”这个方向代表了性别特征拿这个方向去变换“女人”就把对应角色的词算出来了。说白了相似关系和类比关系都被编码成了空间里的几何关系。这种空间直觉对后续工作非常关键。做相似词搜索本质上是在向量空间里找最近邻做聚类是把空间里挨得近的点圈成一组做文本相似度是先算出两个文本的向量再量距离。所有下游任务的起点都是“把词放到一个有意义的空间里”。2. Word2Vec 是怎么把词“画”到空间里的2.1 核心假说看词不看定义聊 Embedding 绕不开 Word2Vec。它虽然是十几年前提出的思路但直到今天很多现代模型的设计里依然能看到它的影子。Word2Vec 能立住全靠那句著名的分布假说相似的词拥有相似的上下文。翻译成大白话你想搞懂一个人就去观察他经常跟谁混在一起。词也一样想知道“深度学习”是什么意思不用看教科书定义去看看语料里“深度学习”前后经常出现什么词。如果总是“神经网络”“模型训练”“大规模数据”这些词你基本就能猜到它的语义轮廓。基于这个假说Word2Vec 的训练目标被设计成让模型学会预测邻居。它从一个句子中取出中心词以及周围一个窗口内的上下文词构造一个预测任务。模型一开始对语义一无所知只能通过大量样本反复调整词向量让“在类似上下文里出现的词”被映射到接近的位置。训练完成后我们真正关心的不是模型预测得准不准而是模型 Embedding 层里那组参数——那才是地图的坐标。打个比方就像我们让一个人天天猜谜猜到最后他脑子里形成了一套对词语关系的直觉我们要的是这套直觉谜底本身无所谓。2.2 CBOW 和 Skip-gram一个猜词的两种玩法Word2Vec 提供了两种训练架构本质上是同一个猜谜游戏的两个方向。CBOW连续词袋模型给你上下文猜中心词。比如给“人工智能 是 [?] 科学 的 分支”模型要从周围的“人工智能”“科学”“分支”去推断中间缺的词正确输出大概率是“计算机”。Skip-gram 则反过来给你中心词猜上下文。给“苹果”让它预测前后可能出现什么词它要猜“水果”“好吃”“买”“一斤”这些可能出现的邻居。生活类比一下CBOW 像你听到一群人聊天让你猜他们中间缺的那个词Skip-gram 像给你一个词让你想象周围人会怎么聊它。选哪个我的经验是语料规模大、稀有词多选 Skip-gram 效果更好它对低频词更友好能把少见词的语义刻画得更细训练时间紧张、语料体量不大CBOW 更稳收敛快对高频词的表示也更平滑。我做垂直领域语料时经常两个都训一版在业务测试集上对比。大多数情况下 Skip-gram 的语义丰富度会胜出但如果你只有一晚上的训练预算CBOW 是足够稳妥的兜底方案。2.3 训练细节里不能省的设计负采样、窗口和维度把猜词游戏真正跑起来还需要处理几个工程问题。第一词表很大时softmax 会算到你怀疑人生。假设词表有 10 万个词每次预测都要拿中心词的 logits 和全部词计算一遍概率计算量太大。负采样是解决办法训练时不去更新所有词的参数只更新真实目标词和随机抽出的几个“负样本”词。比如猜“计算机”时随机抽“沙发”“煎饼”当反面教材让模型学会区分。这样一来训练开销降到极小大规模语料训练才成为可能。第二窗口大小直接决定语义的分辨率。窗口太小比如 1你看到的都是紧挨着的搭配容易把“吃苹果”和“吃香蕉”拉近却看不到更远的语义关系窗口太大比如 10上下文混进太多噪声词向量会变得浑浊模糊。我常用的经验值是主题分类任务窗口取 5 左右语法相似任务窗口取 2 到 3。这背后是有逻辑的——小窗口强调句法搭配因为相邻词更受语法约束大窗口才能抓住更大范围的语义主题。第三向量维度不是越大越好。32 维在小词表上可能够用常用项目里 300 维属于常规配置。维度太高不仅训练慢还会把噪声当成特征记住词向量反而“过拟合”。这三个参数决定了你最终拿到的是“概览图”还是“精细图”。不少初学朋友问我“为什么我训出来的向量一团乱麻”问题十有八九出在这几个旋钮上而不是模型代码写错了。3. 从零搭建一个 Embedding 系统实操前两章聊原理现在直接动手。下面的代码不追求生产级性能目标是让“训练词向量”这件事原原本本跑通每一步都能看到在做什么。3.1 语料准备与样本构造先准备一份很小的中文语料每行一句话词之间已经用空格切好。真实项目里需要你自己接分词工具我这里是内置分词结果方便演示。corpus [ 人工智能 是 计算机 科学 的 分支, 机器学习 是 人工智能 的 核心 方向, 自然语言 处理 是 人工智能 的 应用, 深度学习 是 机器学习 的 重要 方法, 语言 模型 研究 自然语言 处理, 向量 表示 是 语言 模型 的 基础, 嵌入 把 词语 变成 向量, 词语 的 上下文 决定 语义, ]接着构建词表。词表是语料里所有不重复词的集合再加一个用于兜底的 UNK 占位符。from collections import Counter tokens [s.split() for s in corpus] vocab_counter Counter(w for sent in tokens for w in sent) word2idx {UNK: 0} for w, _ in vocab_counter.items(): word2idx[w] len(word2idx) idx2word {i: w for w, i in word2idx.items()} V len(word2idx) print(词汇表大小:, V)有了词表就能构造 CBOW 训练样本。CBOW 样本的格式是“一组上下文词 → 中心词”。窗口为 1 时句子“人工智能 是 计算机”中“人工智能”和“计算机”共同作为上下文目标词是“是”。def make_cbow_samples(tokens, window1): samples [] for sent in tokens: for i, target in enumerate(sent): context [] for offset in range(-window, window 1): if offset 0: continue j i offset if 0 j len(sent): context.append(word2idx[sent[j]]) if len(context) 2 * window: samples.append((context, word2idx[target])) return samples samples make_cbow_samples(tokens, window1) print(样本数量:, len(samples))窗口越大一个中心词能捞到的上下文越多样本也越多但每个样本的上下文噪声也越大。3.2 用 PyTorch 实现一个最简 CBOW模型结构非常简单一个 Embedding 层负责把词 ID 映射成稠密向量再接一个线性层把向量映射回词表大小输出每个候选词的概率。训练用交叉熵做损失函数。import random import numpy as np import torch import torch.nn as nn import torch.optim as optim EMBED_DIM 16 WINDOW 1 EPOCHS 200 class CBOW(nn.Module): def __init__(self, vocab_size, embed_dim): super().__init__() self.embeddings nn.Embedding(vocab_size, embed_dim) self.out_proj nn.Linear(embed_dim, vocab_size) def forward(self, context): # context: [batch, 2 * window] embeds self.embeddings(context) # [batch, 2*window, embed_dim] pooled embeds.mean(dim1) # [batch, embed_dim] logits self.out_proj(pooled) # [batch, vocab_size] return logits model CBOW(V, EMBED_DIM) loss_fn nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.01)训练循环里我把全量样本随机打乱后切成小批量。这样每一步更新都能看到损失在往下降。batch max(1, len(samples) // 16) for epoch in range(EPOCHS): total_loss 0.0 shuffled samples[:] random.shuffle(shuffled) for i in range(0, len(shuffled), batch): batch_samples shuffled[i:ibatch] ctx torch.tensor([s[0] for s in batch_samples]) tgt torch.tensor([s[1] for s in batch_samples]) logits model(ctx) loss loss_fn(logits, tgt) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if epoch % 20 0: print(fepoch {epoch}, loss {total_loss / max(1, len(shuffled)):.4f})代码里有个细节值得注意embeddings(context)得到的是三维张量我用mean(dim1)把窗口里几个词的向量做了平均作为上下文的整体表示。这是 CBOW 的原始做法它假设上下文信息可以无损叠加。训练结束取出 Embedding 层的参数就是我们要的词向量。word_vectors model.embeddings.weight.detach().numpy() print(词向量形状:, word_vectors.shape)3.3 训练结果怎么验证一份可重复的检查方法模型训完先别急着接下流任务。花几分钟做三个“体检”。第一项查指定词的最相近 Top N 词。用余弦相似度衡量向量距离def cosine_sim(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def top_neighbors(word, k5): if word not in word2idx: print(这个词不在词表里) return [] idx word2idx[word] vec word_vectors[idx] scores [(cosine_sim(vec, word_vectors[i]), idx2word[i]) for i in range(V)] return sorted(scores, reverseTrue)[1:k1] for w in [人工智能, 机器学习, 向量]: print(w, top_neighbors(w))在这份小语料上你大概率会看到“人工智能”和“机器学习”靠得很近因为它们在句子里经常充当主语或修饰语上下文模式相似。这种结果没什么稀奇但它就是语义地图形成的直接证据。第二项把向量降维画图。Embedding 是 16 维可以用 PCA 降到二维后画散点图观察同主题词是不是聚成了一团。这一步特别适合做项目汇报视觉直观非技术同事也能一眼看懂。第三项词类比测试。经典范式是“A 之于 B相当于 C 之于 D”。如果向量训练得足够好计算vec(A) - vec(B) vec(C)结果向量应该和 D 很接近。小语料上这个测试不一定漂亮但方向对了说明几何语义已经在显形。这里提个醒小语料训出来的 Embedding 往往只对语料内部有效换个领域、换种文本风格原来学到的“词义”可能完全不适用。验证时一定要用业务场景里的真实数据来测别拿通用词表的漂亮结果安慰自己。4. 实操中遇到的坑和排查方法这一段是实打实踩坑踩出来的比前面任何理论都值钱。4.1 OOV 词汇用户问了一个语料里没有的词不管你训练语料做得多充分线上总会出现没见过的词。我第一次把词表上线就遇到用户搜“螺蛳粉”训练语料是某财经板块的新闻词表里根本没有它。如果不做兜底查询时直接报错或者全部映射成 UNK整个检索链路就废了。常见兜底方案有三种。最简单的把所有未见词映射到 UNK省事但语义尽失所有稀有词变成同一个向量。更实用的是在预处理阶段保留字符级信息中文里可以把词拆成单字向量再取平均也可以收集语料中的高频词根片段。再进阶一点直接用子词切分方式把任何文本切成通用子词单元即使没见过的长词也能被拆成训练过的片段。我的建议是业务词表单独维护一份白名单专门放产品名、实体名这类词未见词走子词兜底两条路并行OOV 场景基本能盖住。另外别把所有低频词都塞进词表词频低于阈值的词直接丢掉或归入 UNK否则它们会作为噪声把向量空间拉乱。4.2 相似度计算的三个误区第一非归一化向量直接算余弦会受向量长度干扰。不同词出现的频率不同高频词的向量模长通常偏大。拿着未归一化的 Embedding 算相似度热门词会被系统性高估。建议先对向量做 L2 归一化再算距离。第二余弦相似度高不等于语义可互换。两个向量夹角小只说明词的“上下文分布”像。我做某分类系统质检时就遇到“亏损”和“利润”经常出现在同一篇财报文本里余弦相似度不低但语义上它们是反义词。这种场景单独靠向量相似度做业务判断一定会翻车需要叠加规则或者更强的语义模型。第三高维空间里所有欧氏距离都会趋向平均这是高维诅咒。直接拿几千个词的原始向量做距离排序局部邻居的区分度往往不理想。先降维再算距离能看到更清晰的邻域结构。4.3 小语料、垂直领域的正确姿势小语料从头训练 Embedding结果几乎可以预料词向量要么往一个方向飘要么输出一批和直觉完全不符的邻居。原因是数据量太少模型还没来得及把语义差异编码进去就已经把少量高频共现关系记死了。遇到这种情况我强烈建议你换个思路先加载一个通用领域训练的预训练词向量作为初始值再在自己垂直语料上继续训练。这相当于让一个已经认识很多常用词的人去读你们行业的黑话和技术文档他原本有语义底子在学新内容会快得多。学习率要调小否则已有向量会被新数据瞬间冲掉。如果没有预训练向量可用训练参数往“更保守”的方向调窗口调小、Embedding 维度调小、迭代轮数减少。宁可让模型欠拟合也别让它把少量样本的偶然共现当成真理记下。我在一个只有几万条评论的项目里把维度从 300 降到 64 之后词向量的邻居反而更像样了这就是过拟合的典型表现。5. Embedding 已经不只是“词向量”了5.1 从词到句子、图片和跨模态如果你以为 Embedding 只能处理文本里的词那就把格局想小了。现在的 AI 系统里句子、段落、文档、图片甚至用户行为序列都可以被映射成向量。句子向量能用于文本聚类、语义相似度、问答检索图片向量可以支撑以图搜图、相似商品推荐更前沿的做法是把图片和文本映射到同一个语义空间让“猫”这个文本向量和一张猫的照片向量靠得很近。这些年各种热门大模型真正在做的事其实可以概括成一句话把文本、语音、图像都翻译成向量。当它们变成同一套坐标系里的点AI 就能跨模态建立关联——你说一句“戴帽子的狗”它去图片库里找到对应的图。这套思路的根基依然是分布式表示。5.2 向量化应用如何反哺业务把 Embedding 用到业务上通常从相似度计算和向量检索起步。经典场景是语义搜索用户输入“性价比高的餐厅”先把这句话转成向量再去索引里找夹角最小的候选文本。相比纯关键词匹配它能跨词序、跨同义词召回“价格实惠的馆子”也能被搜出来因为在语义空间里它俩挨得近。推荐系统里的向量召回也是同理——把用户最近的行为序列和商品池里的候选品统统映射成向量用最近邻搜索去找“用户向量附近”的商品。这类方法不需要人工打标签有数据就能训练已经成了很多推荐系统的标配。一个落地时的提醒别把向量检索和关键词搜索对立起来。关键词精确、可控、可解释向量检索泛化强但语义边界模糊。我习惯的做法是向量召回打底再用关键词规则过滤最后排序输出。两者配合效果比单独用一个稳得多。5.3 在项目里用 Embedding 的两条实用建议最后说两条自己摸索出来的建议。第一条先小后大。别一上来就收集几百万条语料先把几十句话跑通全流程看词表构建、样本生成、模型训练、向量可视化这一整条链路能不能顺下来再逐步加大数据量。链路里的每一步都可能出问题早期把问题暴露在小规模上省时省力。第二条把向量当作中间表示而不是终点。Embedding 给出的是“语义地图”但地图本身不是答案怎么用地图才是。你可以做相似词分析、文本聚类、向量召回、模型输入特征每种用法对向量的质量要求都不一样。明确你的下游任务再回头调整窗口、维度、训练语料这是迭代效率最高的方式。我自己在做一个知识库检索项目的那段时间最深的感触是——Embedding 从来不只是“把词变成数字”的技术动作它更像一种组织信息的方式。你用什么语料、什么窗口、什么相似度指标最后画出来的语义地图就长什么样。花时间把这张地图琢磨明白后面所有下游任务都会轻松不少。等你有了一份自己训出来的向量建议直接跑一遍第 3 节的代码再换上自己领域的语料亲眼看一次向量从散乱到聚类的过程这比看多少理论都管用。