词向量与分布式语义
文档概述
本文档系统阐述词向量的理论基础、实现原理及认知科学联系。重点介绍Word2Vec、GloVe、FastText等经典模型,以及ELMo、BERT等上下文词向量技术,并探讨Embedding空间的几何性质及其认知意义。本文档还将深入讨论词向量在不同NLP任务中的应用、现代预训练语言模型的发展脉络,以及词向量技术的最新研究前沿。
关键词速览
| 术语 | 英文 | 核心定义 |
|---|---|---|
| 分布式假说 | Distributional Hypothesis | 词语由其上下文定义 |
| 词向量 | Word Vector/Embedding | 词语的稠密向量表示 |
| Word2Vec | Word2Vec | 经典的词嵌入训练模型 |
| GloVe | Global Vectors | 全局共现统计词嵌入 |
| FastText | FastText | 子词嵌入模型 |
| 上下文词向量 | Contextualized Embedding | 随上下文变化的词表示 |
| ELMo | Embeddings from Language Models | 双向LSTM词向量 |
| BERT | Bidirectional Encoder Representations | Transformer编码器 |
| 语义空间 | Semantic Space | 词向量张成的空间 |
| 认知科学 | Cognitive Science | 研究人类认知的学科 |
| Transformer | Transformer | 基于自注意力的序列建模架构 |
| 表征学习 | Representation Learning | 自动学习数据有效表示的技术 |
| 预训练 | Pre-training | 在大规模数据上预先训练模型 |
| 微调 | Fine-tuning | 在特定任务上调整预训练模型 |
| 注意力机制 | Attention Mechanism | 根据相关性加权聚合信息的技术 |
一、分布式假说:为什么”你身边的词决定了你的含义”?
1.1 理论起源与核心思想
分布式假说是现代计算语言学和词向量研究的理论基石,由英国语言学家约翰·鲁伯特·费斯(John Rupert Firth)于1957年在丹麦哥本哈根举行的第九届国际语言学大会上首次系统性地提出。费斯被公认为现代语言学的奠基人之一,他在伦敦大学亚非学院(SOAS)任教期间,发展了一套独特的语言学理论体系,强调语言的实际使用而非抽象的语法规则。
费斯的经典论述至今仍是NLP领域最重要的指导思想之一,其原文如下:
“You shall know a word by the company it keeps” (观其伴,知其义)
这一假说的核心洞见在于:语义相似的词语倾向于出现在相似的上下文中。因此,我们可以通过分析一个词的上下文分布来推断其语义。这一思想与认知科学中的分布表征假说(Distributed Representation Hypothesis)高度一致,后者认为知识不是存储在单一神经元或节点中,而是分布式地存储在多个神经元的激活模式中。
1.2 为什么分布式假说是对的?
你身边的朋友决定了你是谁——这句话虽然听起来像心灵鸡汤,但在语言学里它是个硬道理。让我用一个具体例子来解释。
假设你从没学过”獭祭”这个词,但你看到以下两句话:
- “他打开了一瓶獭祭,清酒很入口”
- “獭祭是日本著名的清酒品牌”
即使”獭祭”这两个字你完全不认识,你也大概能猜到它是一种酒。这就是分布式假说的精髓:我们不需要知道一个词本身的定义,只需要看它经常和什么词一起出现,就能推断出它的含义。
语言学上把这个现象叫做”分布相似性”(distributional similarity)。语义相近的词会出现在相似的上下文中——它们前面后面往往跟着类似的词。比如”猫”和”狗”都经常出现在”宠物”、“可爱”、“动物”、“喂食”等词语附近,所以它们在向量空间中距离很近。而”猫”和”汽车”虽然都可能出现在”买了一辆/一只”后面,但整体上下文差异很大,所以距离较远。
这个原理看似简单,但它奠定了整个现代NLP的基础。你现在用的翻译软件、搜索引擎、智能客服,背后都在用这个原理。
1.3 历史发展脉络
分布式假说的思想渊源可以追溯到更早的语言学研究:
早期探索阶段(1950-1970年代)
在这一阶段,语言学家开始意识到词语的意义与其使用环境密切相关。费斯的学生和追随者,如韩礼德(M.A.K. Halliday),进一步发展了系统功能语言学,强调语言的社会功能和使用语境。然而,由于计算能力的限制,这一时期的分布式语义研究主要停留在理论层面。
统计语言学阶段(1980-1990年代)
随着计算机技术的发展,研究者开始尝试使用统计方法实现分布式语义表示。这一时期的代表性工作包括:
- 潜在语义分析(LSA):由Scott Deerwester等人于1990年提出,使用奇异值分解(SVD)对词-文档共现矩阵进行降维,生成词的潜在语义表示。
- 随机索引(Random Indexing):由Peter Gärdenfors提出,通过随机投影高效地生成分布式语义表示。
- 贝叶斯潜在语义分析(PLSA):由Thomas Hofmann于1999年提出,使用概率模型对潜在语义进行建模。
这些方法虽然计算效率较低(需要显式构建和分解大型矩阵),但为后来的词向量研究奠定了重要的理论基础。
神经词向量阶段(2003-2013年)
这一阶段的标志性事件是Yoshua Bengio等人在2003年提出的神经语言模型(Neural Language Model),首次使用神经网络学习词的低维密集表示。该模型使用一个三层的神经网络(输入层、隐藏层、输出层)来预测下一个词,同时学习得到词的分布式表示。然而,由于计算复杂度较高(O(V×N)的输出层计算,其中V是词表大小,N是隐藏层维度),该模型在当时并未得到广泛应用。
Word2Vec时代(2013年至今)
2013年,Tomas Mikolov等人提出了Word2Vec模型,包括Skip-gram和CBOW两种架构,通过一系列技术优化(如负采样、分层Softmax)大幅提高了训练效率,使得在大规模语料上训练词向量成为可能。这一突破引发了词向量研究的热潮,催生了GloVe、FastText、ELMo、BERT等一系列重要模型。
1.4 形式化定义与数学基础
从数学角度,分布式假说可以形式化为:
其中上下文相似度可以通过以下方式度量:
这里 表示词语 的上下文向量,可以使用多种方式定义:
词袋模型(Bag-of-Words Context)
最简单的上下文表示方法是将目标词周围的词视为一个集合,不考虑顺序:
其中 是上下文词的词向量, 是以 为中心的上下文窗口。
加权词袋模型(Weighted BOW)
考虑到距离越近的词对目标词语义影响越大,可以使用距离加权:
其中 是衰减因子, 是词 到目标词 的距离。
依存关系上下文(Dependency-based Context)
基于依存句法分析,只考虑与目标词有句法关系的词:
其中 是依存关系类型, 表示Hadamard积(或逐元素乘法)。
1.5 上下文的定义与选择
上下文的定义方式直接影响词向量的质量,不同的上下文定义编码不同类型的语言信息。
窗口上下文(Window-based)
最常用的上下文定义是基于词语周围的固定窗口:
原始句子: "The quick brown fox jumps over the lazy dog"
窗口大小=2时的上下文:
- "quick" 的上下文: [The, brown, fox]
- "fox" 的上下文: [quick, brown, jumps, over]
窗口大小=1时的上下文:
- "quick" 的上下文: [The, brown]
- "fox" 的上下文: [brown, jumps]
窗口大小的选择是一个重要的超参数:
- 较小的窗口(1-2):倾向于捕捉词汇的句法功能,生成的向量在语法关系上相似(如动词的过去式、名词的复数形式)
- 较大的窗口(5-10):倾向于捕捉更广泛的语义主题,生成的向量在主题相关性上相似
更广义的上下文定义
| 上下文类型 | 定义 | 应用场景 | 优势 | 劣势 |
|---|---|---|---|---|
| 词汇窗口 | 固定窗口内的词 | 句法/语义分析 | 简单高效 | 忽略长距离依赖 |
| 文档级 | 同一文档中的词 | 主题建模 | 捕捉主题相似性 | 忽略词序 |
| 依赖结构 | 依存语法关系中的词 | 句法语义 | 编码语法关系 | 需要依存分析器 |
| 语义框架 | 同一框架中的词 | 框架语义学 | 深层语义 | 需要框架知识库 |
| 句法上下文 | 句法树中的相关节点 | 句法功能 | 精确语法关系 | 计算复杂度高 |
| 话语级 | 整个语篇中的词 | 篇章分析 | 宏观语义 | 需要长文本处理 |
1.6 分布式表示的理论基础
稀疏表示 vs 分布式表示
传统的one-hot表示是一种稀疏表示,每个词由一个维度为V(词表大小)的向量表示,其中只有一个维度为1,其余全为0。这种表示存在两个主要问题:
- 维度灾难:词表大小通常在数万到数百万,导致高维稀疏向量难以处理
- 语义鸿沟:任意两个词的表示都是正交的,无法捕捉语义相似性
相比之下,分布式表示将每个词映射到一个低维稠密空间(如100-1000维),每个维度都不是binary的,而是连续值。这种表示的优势在于:
- 维度压缩:从V维稀疏向量压缩到d维稠密向量(d << V)
- 语义编码:语义相似的词在向量空间中距离较近
- 知识迁移:相似的语义特征可以在不同词之间共享
1.7 分布式假说的认知科学对应
分布式假说与认知科学中的多个重要理论存在深刻联系:
分布式记忆理论(Distributed Memory)
心理学的分布式记忆理论认为,记忆不是存储在单一神经元中,而是分布式地存储在多个神经元的连接权重中。这与词向量的分布式表示高度一致:词的语义不是存储在单个维度中,而是编码在整个向量空间中。
原型理论(Prototype Theory)
Eleanor Rosch提出的原型理论认为,概念以原型(典型成员)为中心向外辐射,边界模糊。词向量空间中的语义聚类也呈现类似结构:典型成员(如”水果”类别中的”苹果”)距离类别中心更近,而非典型成员(如”牛油果”)距离较远。
二、经典词向量模型
2.1 Word2Vec系列模型
Word2Vec由Tomas Mikolov等人于2013年在谷歌提出,是词向量研究的里程碑式工作。该模型通过浅层神经网络学习词的高维稠密表示,具有训练速度快、效果好的优点,迅速成为NLP领域的基础工具。
2.1.1 Skip-gram模型详解
Skip-gram模型的核心思想是:用中心词预测上下文词。与传统的基于上下文的语言模型不同,Skip-gram完全忽略了词的顺序,将上下文视为一个词袋。
模型架构
输入层: 中心词 one-hot 向量 [V × 1]
↓ W [V × d] (输入嵌入矩阵)
隐藏层: 词嵌入 [d × 1]
↓ W' [d × V] (输出嵌入矩阵)
输出层: Softmax → 上下文词概率分布 [V × 1]
目标函数
给定训练语料中的词序列 ,Skip-gram的目标是最大化所有位置的上下文条件概率之和:
其中 是上下文窗口大小,条件概率定义为:
这里 是输入词嵌入(input embedding), 是输出词嵌入(output embedding)。注意Word2Vec维护两套嵌入:输入嵌入和输出嵌入,这在后续的相似度计算中通常会综合使用。
2.1.2 CBOW模型详解
CBOW(Continuous Bag-of-Words)与Skip-gram互补,用上下文预测中心词。模型假设上下文中所有词的贡献是等价的(词袋假设)。
CBOW模型架构
上下文词: [w_{t-c}, ..., w_{t-1}, w_{t+1}, ..., w_{t+c}]
↓ 各自的嵌入
嵌入层: [d × 2c] 个 d维向量
↓ 平均池化
隐藏层: [d × 1] 平均向量
↓ W' [d × V]
输出层: Softmax → 中心词概率分布 [V × 1]
CBOW与Skip-gram的对比
| 特性 | Skip-gram | CBOW |
|---|---|---|
| 学习方向 | 中心→上下文 | 上下文→中心 |
| 训练数据量 | 较少(每对词一个样本) | 较多(每个中心词一个样本) |
| 稀有词处理 | 更好(从中心词学习) | 较差(被常用词稀释) |
| 训练速度 | 较慢(更多样本) | 较快 |
| 大规模语料 | 表现优秀 | 表现一般 |
| 小规模语料 | 表现较差 | 表现尚可 |
2.2 负采样与层次Softmax:训练Word2Vec的高效技巧
训练Word2Vec最大的挑战是计算softmax的概率分母,这需要遍历整个词表(可能数十万个词)。负采样和层次Softmax是两种主流的加速技巧。
2.2.1 负采样(Negative Sampling)
负采样的核心思想很朴素:不要求模型给所有词都打出正确的概率,只需要让它区分正样本和负样本就行。
对于每个训练样本(中心词,上下文词),我们采样几个”负样本”——这些词不是真正的上下文。比如你看到”狗”出现在”宠物”附近,那”宠物-狗”就是正样本对,而”宠物-汽车”、“宠物-电脑”就是负样本对。模型的任务变成:判断这个词对是不是真的上下文关系。
这就把多分类问题转化成了二分类问题。损失函数变成了:
其中 是sigmoid函数, 是负样本数量(通常取5-20)。
负采样分布的选择也很有讲究。简单用均匀分布效果不好——因为高频词(如”的”、“了”)会被采样的太多,但它们本身信息量不大。研究发现用unigram频率的3/4次方效果更好:
这个公式给低频词稍微提升了一点被采样的概率,让模型能更好地学习稀有词的表示。
2.2.2 层次Softmax(Hierarchical Softmax)
层次Softmax用一棵霍夫曼树来组织词表,把预测词的问题变成从根节点走到叶子节点的问题。每个内部节点都是一个二分类器,告诉你应该走左还是走右。
因为霍夫曼树的特性,高频词离根节点更近,路径更短,平均只需要 O(log V) 次计算就能到达,而不需要遍历整个词表。
两种方法各有优劣:负采样实现简单、效果好,是目前的主流选择;层次Softmax在超大规模词表上效率更高,但实现稍复杂。
2.3 GloVe模型
GloVe(Global Vectors)由Jeffrey Pennington、Richard Socher和Christopher Manning于2014年在斯坦福大学提出,融合了全局矩阵分解和局部上下文窗口两种方法的优点。
2.3.1 GloVe的核心思想
Word2Vec是”在线”学习——它一个窗口一个窗口地看语料,效率高但只利用局部信息。GloVe则先统计整个语料库中词与词共现的次数,然后用数学方法从这个共现矩阵中学习词向量。
你可以把GloVe的损失函数理解为:我预测两个词共现次数的对数,应该接近它们词向量的点积加上两个偏置。
这个公式里 就是词 和词 共现的次数。 是一个加权函数,给太频繁的共现(比如”的”和”了”)降权,给稀有但有意义的共现加权。
GloVe的优势在于它能更好地捕捉全局统计信息,在一些任务上(特别是类比推理)表现更好。
2.3.2 GloVe与Word2Vec的深入对比
| 特性 | Word2Vec | GloVe |
|---|---|---|
| 训练目标 | 预测概率(条件) | 重构共现概率 |
| 语料利用 | 局部上下文 | 全局共现矩阵 |
| 训练速度 | 快(在线) | 较慢(需要矩阵分解) |
| 语义任务表现 | 相似词效果好 | 类比推理更优 |
| 语法任务表现 | 一般 | 优秀 |
| 内存需求 | 低(流式处理) | 高(需存储共现矩阵) |
何时选择哪个模型
- 选择Word2Vec的场景:超大规模语料、资源受限、需要流式训练、增量更新
- 选择GloVe的场景:中等规模语料、需要稳定的收敛性、语法相关任务、类比推理任务
2.4 FastText模型
FastText由Piotr Bojanowski等人于2016年在Facebook AI Research提出,核心创新是引入子词(subword)信息。
2.4.1 子词嵌入原理
FastText把每个词拆成更小的块——字符n-gram。比如”walking”会拆成”<wa, wal, alk, lki, kin, ing, ng>“(这里<>是边界标记,防止子词跨词)。
这样做的直接好处是能处理未登录词(OOV)。如果测试时遇到一个训练时没见过的词,比如”runned”(虽然这是个错误拼写),传统Word2Vec完全无法处理。但FastText可以通过子词的平均嵌入来估计这个词的向量——“runned”的子词和”run”的子词大部分重叠,所以能得到一个合理的表示。
这个特性对形态丰富的语言(德语、土耳其语、芬兰语等)特别有价值。在这些语言里,同一个词根可以组合出几十种不同的形式,如果每个形式都单独学一个向量,信息会很分散。FastText通过共享子词信息,能更好地处理这种情况。
2.4.2 FastText的代码实现
from gensim.models import FastText
from gensim.models import Word2Vec
# 用gensim训练FastText
sentences = [
["我", "爱", "自然语言处理"],
["词向量", "是", "NLP", "的", "基础"],
# ... 更多句子
]
# 训练FastText模型
# min_n和max_n控制子词的长度范围
fasttext_model = FastText(
sentences=sentences,
vector_size=100, # 词向量维度
window=5, # 上下文窗口大小
min_count=2, # 最小词频
min_n=3, # 最小子词长度
max_n=6 # 最大子词长度
)
# 获取词向量
vector = fasttext_model.wv['词向量']
# 即使词不在词表里,也可以通过子词估计(OOV处理)
# fasttext_model.wv['不存在的词'] # 会报错
# 但在gensim中可以通过相似度搜索找到类似词三、上下文词向量:为什么”bank”既可以是银行也可以是河岸?
3.1 传统词向量的致命缺陷
Word2Vec、GloVe、FastText有一个共同的问题:每个词只有一个固定的向量。这在处理多义词时会出大问题。
拿英文单词”bank”举例:
- “I deposited money at the bank” —— 银行
- “We picnicked on the bank of the river” —— 河岸
传统词向量会把这两种完全不同的含义平均成一个向量。结果就是这个词向量既不太像”银行”,也不太像”河岸”,变成了一个模糊的中间状态。
更糟糕的是,一些中性词如果经常出现在不同语义类别的上下文中,它们的向量也会被”污染”。比如”芯片”可能同时指”电脑芯片”和”土豆芯片(薯片)“,如果语料中土豆相关的文本更多,“芯片”的向量就会偏向他食物一边。
3.2 ELMo:第一个上下文敏感的词向量
ELMo(Embeddings from Language Models)由Matthew Peters等人于2018年提出,首次实现了真正的上下文相关词表示。
ELMo的思路是:让词的表示取决于它的整个上下文。它用双向LSTM来处理文本,得到每个词在特定上下文中的向量。
具体来说,ELMo用的是语言模型——给定前文,预测下一个词是什么。这是自然语言处理中历史最悠久的任务之一,有大量的理论基础。ELMo同时训练前向语言模型(从左往右看)和后向语言模型(从右往左看),然后把两边的表示拼接起来。
最终每个词的ELMo表示是各层表示的加权组合。底层的LSTM层更多编码语法信息(词性、句法结构),高层的LSTM层更多编码语义信息。通过学习权重,ELMo可以自动决定在什么任务上应该更依赖哪一层。
3.3 BERT:上下文词向量的集大成者
BERT(Bidirectional Encoder Representations from Transformers)于2018年由谷歌的Jacob Devlin等人提出,是NLP领域的里程碑式模型。
BERT的核心创新有两个:
第一是双向Transformer架构。之前的方法要么从左往右看,要么从右往左看,都是单向的。BERT用Transformer的注意力机制,同时看到上下文的所有位置。
第二是掩码语言模型(MLM)。训练时随机把一些词替换成”[MASK]“标记,然后让模型预测被遮盖的是什么词。这种方式让模型必须同时利用上下文信息来推断被遮盖的词。
BERT还加入了下一句预测(NSP)任务:给定两个句子,判断它们是否是原文中的连续句子。这帮助模型学习句子级别的语义关系,对问答、自然语言推理等任务很有帮助。
3.3.1 BERT的输入处理
BERT有特殊的输入格式:
[CLS] The man went to the store [SEP] He bought some milk [SEP]
[CLS]是一个特殊标记,它的输出向量可以作为整个句子的表示,用于分类任务。[SEP]用来分隔不同的句子。BERT还会学习段落嵌入(segment embeddings)来区分句子A和句子B。
3.3.2 BERT的变体
BERT之后涌现了大量变体:
- RoBERTa:Facebook的改进版本,删掉了NSP任务,用了更多数据和更大的batch
- ALBERT:参数共享的轻量版本,减少了内存占用
- DistilBERT:知识蒸馏的压缩版本,推理速度快60%
- ERNIE、XLNet、ELECTRA:各有特色的改进
四、词向量的评估:内在评估 vs 外在评估
训练完词向量后,怎么知道它们好不好?这就涉及到词向量的评估方法。
4.1 内在评估(Intrinsic Evaluation)
内在评估直接测试词向量空间本身的性质,不涉及具体应用任务。常见的方法有:
词语相似度任务(Word Similarity)
给模型一些人工标注的相似词对,比如(狗, 猫, 0.9),(狗, 汽车, 0.1),然后计算模型预测的相似度与人工标注的相关性(Spearman相关系数)。相关性越高越好。
from scipy.stats import spearmanr
def evaluate_word_similarity(model, word_pairs_with_scores):
"""
评估词语相似度
word_pairs_with_scores: [(word1, word2, human_score), ...]
human_score通常是0-10或0-1之间的相似度评分
"""
model_scores = []
human_scores = []
for word1, word2, human_score in word_pairs_with_scores:
if word1 in model and word2 in model:
model_sim = model.similarity(word1, word2)
model_scores.append(model_sim)
human_scores.append(human_score)
# 计算Spearman相关系数
correlation, p_value = spearmanr(model_scores, human_scores)
return correlation词语类比任务(Word Analogy)
这是Mikolov等人提出的经典测试。测试”king - man + woman ≈ queen”这类向量运算。常用数据集包括Google的类比数据集,涵盖语法类比(复数形式、时态等)和语义类比(首都关系、家族关系等)。
def evaluate_word_analogy(model, analogy_questions):
"""
评估词语类比能力
analogy_questions格式: [(a, b, c, d), ...]
测试: a - b ≈ c - d
"""
correct = 0
total = 0
for a, b, c, expected_d in analogy_questions:
# 跳过模型中没有的词
if any(w not in model for w in [a, b, c]):
continue
# 计算类比结果
result = model.most_similar(positive=[a, c], negative=[b])
# 检查是否命中正确答案
if result and result[0][0] == expected_d:
correct += 1
total += 1
accuracy = correct / total if total > 0 else 0
return accuracy4.2 外在评估(Extrinsic Evaluation)
外在评估把词向量作为特征或初始化,用到下游任务中,看最终任务的表现。常见任务包括:
| 任务 | 说明 | 典型指标 |
|---|---|---|
| 文本分类 | 判断文本属于哪个类别 | 准确率、F1 |
| 命名实体识别 | 识别文本中的人名地名等 | F1 |
| 情感分析 | 判断情感正负面 | 准确率 |
| 机器翻译 | 源语言到目标语言 | BLEU |
| 问答系统 | 回答用户问题 | EM、F1 |
外在评估更能反映词向量的实用价值——就算内在评估分数一般,如果下游任务效果好,那也是好向量。
4.3 评估的局限性
内在评估和外在评估有时会不一致。有些词向量在相似度任务上表现很好,但用到下游任务反而拖后腿。这提示我们:没有完美的评估方法,综合考虑多种指标才能全面评价词向量。
五、词向量实战:用代码训练你的第一个词向量
5.1 使用gensim训练Word2Vec
gensim是最流行的词向量训练库之一,API简洁易用。
from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
import re
# 方法1:直接传入句子列表
sentences = [
"我喜欢学习自然语言处理".split(),
"词向量是NLP的核心技术".split(),
"深度学习让机器理解语言".split(),
"Word2Vec可以训练词向量".split(),
"GloVe是另一种词向量方法".split(),
"BERT是上下文相关的表示".split(),
# ... 更多语料
]
# 训练Word2Vec
model = Word2Vec(
sentences=sentences,
vector_size=100, # 向量维度,一般100-300
window=5, # 上下文窗口
min_count=1, # 最小词频,低于此的词被忽略
workers=4, # 并行训练线程数
sg=1, # 1=Skip-gram, 0=CBOW
epochs=100, # 训练轮数
negative=5 # 负采样数量
)
# 保存模型
model.save("my_word2vec.model")
# 加载模型
# model = Word2Vec.load("my_word2vec.model")
# 常用功能
print("词表大小:", len(model.wv))
print("向量维度:", model.wv.vector_size)
# 获取词向量
vector = model.wv['自然语言']
print("'自然语言'的向量:", vector[:5], "...")
# 找最相似的词
similar = model.wv.most_similar('词向量', topn=5)
print("与'词向量'最相似的词:")
for word, sim in similar:
print(f" {word}: {sim:.4f}")
# 词类比: 国王 - 男人 + 女人 ≈ ?
analogy = model.wv.most_similar(positive=['国王', '女人'], negative=['男人'])
print("国王 - 男人 + 女人 ≈", analogy)5.2 使用PyTorch训练Word2Vec
如果想更深入理解词向量的原理,可以自己实现一个:
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import random
class Word2Vec(nn.Module):
"""简化版Word2Vec"""
def __init__(self, vocab_size, embedding_dim):
super().__init__()
self.target_embeddings = nn.Embedding(vocab_size, embedding_dim)
self.context_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 初始化
nn.init.uniform_(self.target_embeddings.weight, -0.5, 0.5)
nn.init.zeros_(self.context_embeddings.weight)
def forward(self, target, context):
"""
target: 中心词ID [batch]
context: 上下文词ID [batch]
"""
# 获取嵌入
target_emb = self.target_embeddings(target) # [batch, dim]
context_emb = self.context_embeddings(context) # [batch, dim]
# 计算点积(相似度)
score = torch.sum(target_emb * context_emb, dim=1)
return torch.sigmoid(score)
def generate_training_data(corpus, window_size):
"""生成Skip-gram训练数据"""
pairs = []
for sentence in corpus:
for i, center in enumerate(sentence):
# 窗口内的上下文词
start = max(0, i - window_size)
end = min(len(sentence), i + window_size + 1)
for j in range(start, end):
if i != j:
pairs.append((center, sentence[j]))
return pairs
# 使用示例
vocab = ["我", "爱", "自然语言", "处理", "是", "有趣", "的"]
vocab_size = len(vocab)
word_to_idx = {w: i for i, w in enumerate(vocab)}
corpus = [["我", "爱", "自然语言", "处理"],
["自然语言", "处理", "是", "有趣", "的"]]
# 生成数据
training_pairs = generate_training_data(corpus, window_size=2)
# 初始化
model = Word2Vec(vocab_size, embedding_dim=10)
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 训练
for epoch in range(100):
total_loss = 0
for center_word, context_word in training_pairs:
center_idx = torch.tensor([word_to_idx[center_word]])
context_idx = torch.tensor([word_to_idx[context_word]])
# 正样本损失
pos_score = model(center_idx, context_idx)
pos_loss = -torch.log(pos_score + 1e-10).mean()
# 简单负采样(随机词)
neg_word = random.choice([i for i in range(vocab_size) if i != word_to_idx[center_word]])
neg_idx = torch.tensor([neg_word])
neg_score = model(center_idx, neg_idx)
neg_loss = -torch.log(1 - neg_score + 1e-10).mean()
loss = pos_loss + neg_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}, Loss: {total_loss/len(training_pairs):.4f}")
# 获取最终词向量
final_embeddings = model.target_embeddings.weight.detach().numpy()
print("\n词向量结果:")
for word, idx in word_to_idx.items():
print(f"{word}: {final_embeddings[idx][:5]}...")5.3 加载预训练词向量
如果不想自己训练,可以直接使用预训练模型:
# 使用gensim加载预训练中文词向量
# 下载地址:https://github.com/Embedding/Chinese-Word-Vectors
from gensim.models import KeyedVectors
# 加载预训练词向量(几百MB,需要先下载)
# model = KeyedVectors.load_word2vec_format('sgns.context.word')
# 使用中文预训练模型(需要pip install gensim)
# 或者使用更小的模型
import gensim.downloader as api
# 加载英文预训练向量(GloVe)
# glove_vectors = api.load('glove-wiki-gigaword-100')
# 加载Facebook的FastText预训练向量
fasttext_vectors = api.load('fasttext-wiki-news-subwords-300')
# 使用示例
print(fasttext_vectors.most_similar('computer'))
print(fasttext_vectors.similarity('king', 'queen'))六、词向量可视化:用t-SNE/UMAP看词向量空间
词向量通常有100-300维,人类无法直接理解。高维可视化技术可以帮助我们理解词向量空间的结构。
6.1 t-SNE可视化
t-SNE(t-distributed Stochastic Neighbor Embedding)是最常用的降维可视化方法之一。它能保持高维空间中相似点在低维空间中也相近的特性。
import numpy as np
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
# 假设你已经有了一些词向量
words = ['king', 'queen', 'man', 'woman', 'boy', 'girl',
'dog', 'cat', 'lion', 'tiger',
'apple', 'banana', 'orange', 'grape',
'run', 'walk', 'eat', 'sleep', 'think', 'love']
# 假设有对应的300维向量
embeddings = fasttext_vectors[words]
# t-SNE降维到2D
tsne = TSNE(n_components=2, random_state=42, perplexity=5)
embeddings_2d = tsne.fit_transform(embeddings)
# 可视化
plt.figure(figsize=(12, 8))
for i, word in enumerate(words):
plt.scatter(embeddings_2d[i, 0], embeddings_2d[i, 1])
plt.annotate(word, (embeddings_2d[i, 0], embeddings_2d[i, 1]))
plt.title('Word Embeddings Visualization with t-SNE')
plt.show()6.2 UMAP可视化
UMAP(Uniform Manifold Approximation and Projection)是近年来更流行的选择,比t-SNE更快,而且能更好地保留全局结构。
import umap
# UMAP降维
reducer = umap.UMAP(n_components=2, random_state=42)
embeddings_2d = reducer.fit_transform(embeddings)
# 可视化
plt.figure(figsize=(12, 8))
plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1])
for i, word in enumerate(words):
plt.annotate(word, (embeddings_2d[i, 0], embeddings_2d[i, 1]))
plt.title('Word Embeddings Visualization with UMAP')
plt.show()6.3 词类比可视化
词类比揭示了词向量空间的线性结构。我们可以可视化这种关系:
def visualize_analogy(word1, word2, word3, model):
"""
可视化词类比 a - b ≈ c - d
"""
# 获取词向量
v1 = model[word1]
v2 = model[word2]
v3 = model[word3]
# 计算目标向量
v_d = v1 - v2 + v3
# 投影到2D(使用PCA)
from sklearn.decomposition import PCA
vectors = np.array([v1, v2, v3, v_d])
pca = PCA(n_components=2)
coords = pca.fit_transform(vectors)
# 绘图
plt.figure(figsize=(8, 6))
labels = [word1, word2, word3, '? (≈' + word1 + '-' + word2 + '+' + word3 + ')']
colors = ['blue', 'blue', 'green', 'red']
for i, (x, y) in enumerate(coords):
plt.scatter(x, y, c=colors[i], s=100)
plt.annotate(labels[i], (x, y), fontsize=12)
# 画箭头
plt.arrow(coords[1][0], coords[1][1],
coords[0][0]-coords[1][0], coords[0][1]-coords[1][1],
head_width=0.05, head_length=0.1, fc='blue', ec='blue', alpha=0.5)
plt.arrow(coords[2][0], coords[2][1],
coords[3][0]-coords[2][0], coords[3][1]-coords[2][1],
head_width=0.05, head_length=0.1, fc='green', ec='green', alpha=0.5)
plt.title(f'Word Analogy: {word1} - {word2} ≈ {word3} - ?')
plt.show()
# 示例:king - man + woman ≈ queen
visualize_analogy('king', 'man', 'woman', fasttext_vectors)七、词向量偏见:模型学到了什么不该学的?
7.1 性别偏见问题
词向量会从训练语料中学习到人类社会的偏见,其中最研究最多的是性别偏见。
比如,如果你问”man is to computer programmer as woman is to ___“,用Word2Vec做类比,答案很可能是”homemaker”(家庭主妇)。这不是因为AI歧视女性,而是因为训练语料中”computer programmer”确实更多地和”he/him”出现在一起。
更直接的偏见可以通过向量运算暴露出来:
man - woman的方向向量编码了性别doctor和nurse在性别方向上的投影不同
7.2 种族、年龄等偏见
除了性别,词向量还可能编码:
- 种族偏见:某些名字和职业的关联
- 年龄偏见:年龄与能力的刻板印象
- 文化偏见:以西方为中心的语义框架
7.3 如何检测偏见
def detect_gender_bias(embeddings, definitional_pairs):
"""
检测性别偏见
definitional_pairs: 定义性词对,如 [('man', 'woman'), ('king', 'queen')]
"""
gender_direction = []
for word1, word2 in definitional_pairs:
direction = embeddings[word1] - embeddings[word2]
gender_direction.append(direction)
# 取平均作为性别方向
gender_direction = np.mean(gender_direction, axis=0)
# 标准化
gender_direction = gender_direction / np.linalg.norm(gender_direction)
return gender_direction
def measure_bias_for_words(embeddings, words, gender_direction):
"""
测量一组词在性别方向上的投影
正值偏向男性方向,负值偏向女性方向
"""
projections = {}
for word in words:
if word in embeddings:
projection = np.dot(embeddings[word], gender_direction)
projections[word] = projection
return projections
# 示例
gender_direction = detect_gender_bias(
embeddings,
[('man', 'woman'), ('king', 'queen'), ('boy', 'girl'), ('he', 'she')]
)
occupations = ['doctor', 'nurse', 'engineer', 'teacher', 'secretary', 'ceo', 'assistant']
bias_scores = measure_bias_for_words(embeddings, occupations, gender_direction)
print("职业性别偏见分数(正=偏男性化,负=偏女性化):")
for occ, score in sorted(bias_scores.items(), key=lambda x: x[1], reverse=True):
print(f" {occ}: {score:.4f}")7.4 如何消除偏见
主要有两种方法:
硬去偏(Hard Debiasing):把性别词对投影到与性别方向正交的空间,使它们在性别维度上距离相等。比如”doctor”和”nurse”,它们的性别偏见被强制消除。
软去偏(Soft Debiasing):不是完全消除,而是减小偏见方向的强度。这保留了词的某些语义,同时减少了歧视性的关联。
八、跨语言词向量:让不同语言”说好中国话”
8.1 跨语言词向量的挑战
不同语言的人说的词,即使意思相同,在向量空间中也可能位置完全不同。“狗”和”dog”可能一个在空间的这头,一个在那头。
跨语言词向量(Cross-lingual Word Embeddings)要解决的问题是:让不同语言的词向量共享同一个语义空间,这样就能做跨语言任务,比如翻译、跨语言检索。
8.2 对齐方法
方法一:字典学习(Dictionary Learning)
如果有少量双语词典(几千个词对就够了),可以学习一个线性变换矩阵,把源语言向量映射到目标语言空间:
这个优化问题可以通过奇异值分解(SVD)求解。
方法二:MUSE方法
Facebook的MUSE方法使用对抗训练,让一个判别器无法区分向量是来自源语言还是目标语言,同时学习对齐。
方法三:多语言预训练
现代的多语言模型(如mBERT、XLM-R)通过在大规模多语言语料上联合训练,自然而然地学习到跨语言的对应关系。它们不需要显式的对齐监督。
# 使用sentence-transformers做跨语言相似度
from sentence_transformers import SentenceTransformer
# 多语言模型
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# 中文和英文句子
chinese_sentence = "我喜欢学习人工智能"
english_sentence = "I love studying artificial intelligence"
# 编码
vec1 = model.encode(chinese_sentence)
vec2 = model.encode(english_sentence)
# 计算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity([vec1], [vec2])[0][0]
print(f"相似度: {similarity:.4f}") # 应该很高8.3 跨语言词向量的应用
- 机器翻译:用双语向量做翻译初始化
- 跨语言检索:用中文Query搜索英文文档
- 多语言NLP:在一个语言上训练,迁移到其他语言
- 语言学研究:比较不同语言的语义结构
九、动手实验:词语类比实战
词语类比(Word Analogies)是词向量最神奇的特性之一。让我们做一个完整的实验。
9.1 实验代码
from gensim.models import Word2Vec
import numpy as np
# 准备一个较大的语料(这里用简化的示例)
texts = [
# 国家和首都
"北京是中国的首都 上海是中国的大城市".split(),
"东京是日本的首都 大阪是日本的城市".split(),
"巴黎是法国的首都 里昂是法国的城市".split(),
"伦敦是英国的首都 曼彻斯特是英国的城市".split(),
"柏林是德国的首都 慕尼黑是德国的城市".split(),
"罗马是意大利的首都 米兰是意大利的城市".split(),
# 男人-女人关系
"国王是男人 王后是女人".split(),
"男人跑步 女人跑步".split(),
"男孩玩耍 女孩玩耍".split(),
"先生来访 女士来访".split(),
# 动词形态
"他跑 他跑步".split(),
"他吃 他吃东西".split(),
"他工作 他工作者".split(),
]
# 训练词向量
model = Word2Vec(texts, vector_size=50, window=3, min_count=1, epochs=100)
print("=" * 50)
print("词向量类比实验")
print("=" * 50)
# 测试1:国家-首都关系
print("\n1. 测试国家-首都关系")
print("-" * 30)
test_cases = [
('中国', '北京', '日本'), # 应该得到 东京
('法国', '巴黎', '德国'), # 应该得到 柏林
('英国', '伦敦', '意大利'), # 应该得到 罗马
]
for country1, capital1, country2 in test_cases:
try:
result = model.wv.most_similar(positive=[country2, capital1], negative=[country1])
predicted = result[0][0]
print(f" {country1} : {capital1} ≈ {country2} : {predicted}")
except KeyError as e:
print(f" 词 '{e}' 不在词表中")
# 测试2:性别关系
print("\n2. 测试男人-女人关系")
print("-" * 30)
test_cases = [
('国王', '王后', '男人'), # 应该得到 女人
('先生', '女士', '男孩'), # 应该得到 女孩
]
for word1, word2, word3 in test_cases:
try:
result = model.wv.most_similar(positive=[word3, word2], negative=[word1])
predicted = result[0][0]
print(f" {word1} : {word2} ≈ {word3} : {predicted}")
except KeyError as e:
print(f" 词 '{e}' 不在词表中")
# 测试3:直接查看向量
print("\n3. 向量运算展示")
print("-" * 30)
def show_vector(word):
v = model.wv[word]
print(f" {word} 的向量: [{v[0]:.3f}, {v[1]:.3f}, ..., {v[-1]:.3f}]")
for word in ['中国', '日本', '北京', '东京']:
show_vector(word)
# 计算向量差
v_china = model.wv['中国']
v_beijing = model.wv['北京']
v_japan = model.wv['日本']
difference = v_beijing - v_china
predicted_tokyo = v_japan + difference
print(f"\n 北京 - 中国 ≈ {predicted_tokyo[:3]}...")
print(f" 实际东京向量 ≈ {model.wv['东京'][:3]}...")9.2 运行结果解读
这个实验展示了词向量空间的线性结构。虽然我们用的是很小的示例语料,但已经能看出一些规律。如果用真实的Wikipedia或新闻语料训练,类比准确率可以达到80-90%。
十、大语言模型时代的词向量:GPT的Token Embedding
10.1 从词向量到Token Embedding
现代大语言模型(如GPT系列)使用的是Token Embedding,这与传统词向量有本质区别。
Token vs 词
传统NLP把”词”作为基本单位,但LLM用Token。Token可能是:
- 一个完整的词(如”机器”)
- 一个子词(如”学习”可能分成”学”和”习”)
- 一个字符
- 特殊符号
分词由Tokenizer完成,常见的Tokenizer包括BPE(Byte-Pair Encoding)、WordPiece、SentencePiece等。
# 查看GPT的Tokenizer
from transformers import GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
text = "人工智能让机器学习"
# 分词
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.encode(text)
print("原文:", text)
print("Token:", tokens)
print("Token IDs:", token_ids)
# 看看tokenizer是如何切分的
for i, (token, token_id) in enumerate(zip(tokens, token_ids)):
print(f" {i}: '{token}' -> {token_id}")10.2 GPT的Embedding结构
GPT模型的Embedding层包含三部分:
- Token Embedding:每个Token ID映射到一个向量
- 位置 Embedding(Positional Encoding):给每个位置一个向量,表示词在序列中的位置
- 段落 Embedding(Token Type Embedding):BERT有,GPT-2没有
最终的输入嵌入 = Token Embedding + 位置 Embedding
import torch
from transformers import GPT2Model, GPT2Tokenizer
model = GPT2Model.from_pretrained('gpt2')
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
text = "机器学习很有趣"
inputs = tokenizer(text, return_tensors='pt')
# 模型处理
with torch.no_grad():
outputs = model(**inputs)
# outputs.last_hidden_state包含每层的隐藏状态
# 最后一层: [batch_size, seq_len, hidden_dim]
hidden_states = outputs.last_hidden_state
print("输入形状:", inputs['input_ids'].shape)
print("隐藏状态形状:", hidden_states.shape)
# 获取Token Embedding
token_embeddings = model.wte # Token Embedding Table
position_embeddings = model.wpe # Position Embedding Table
print("Token嵌入表形状:", token_embeddings.weight.shape)
print("位置嵌入表形状:", position_embeddings.weight.shape)10.3 上下文词向量 vs 静态词向量
这是大语言模型和传统词向量的核心区别:
静态词向量(Word2Vec、GloVe、FastText):
- 同一个词,不管在什么句子中,向量都一样
- “银行”在”去银行存钱”和”坐在河岸边”里的向量完全相同
上下文词向量(ELMo、BERT、GPT):
- 同一个词,在不同上下文中,向量不同
- “银行”在”去银行存钱”中的向量偏向金融机构
- “银行”在”坐在河岸边”中的向量偏向地理概念
BERT通过双向注意力做到这一点,GPT通过单向(从左到右)注意力做到这一点。
10.4 如何从LLM提取词向量
from transformers import AutoModel, AutoTokenizer
import torch
# 加载BERT
model_name = 'bert-base-chinese'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# 输入句子
sentence = "银行的工作很有意思" # "银行"指金融机构
sentence2 = "河边的银行" # "银行"指河岸
# Tokenize
inputs1 = tokenizer(sentence, return_tensors='pt')
inputs2 = tokenizer(sentence2, return_tensors='pt')
# 获取最后一层隐藏状态
with torch.no_grad():
outputs1 = model(**inputs1)
outputs2 = model(**inputs2)
# 找到"银"字的位置
tokens1 = tokenizer.convert_ids_to_tokens(inputs1['input_ids'][0])
tokens2 = tokenizer.convert_ids_to_tokens(inputs2['input_ids'][0])
print("句子1 tokens:", tokens1)
print("句子2 tokens:", tokens2)
# 获取"银"字的向量
yin_idx1 = tokens1.index('银')
yin_idx2 = tokens2.index('银')
yin_vec1 = outputs1.last_hidden_state[0, yin_idx1]
yin_vec2 = outputs2.last_hidden_state[0, yin_idx2]
# 计算两个"银"向量的相似度
similarity = torch.cosine_similarity(yin_vec1.unsqueeze(0), yin_vec2.unsqueeze(0))
print(f"\n两个'银'向量的余弦相似度: {similarity.item():.4f}")
print("(相似度越高,说明模型区分不同含义的能力越强)")十一、词向量进阶话题
11.1 句子和文档嵌入
词向量只能表示单个词,怎么表示整个句子或文档?
简单方法
- 平均池化:把所有词的向量加起来求平均
- 最大池化:每个维度取所有词中的最大值
更好方法
- Sentence-BERT:专门训练句子向量的BERT变体
- Universal Sentence Encoder:Google的多语言句子编码器
- Doc2Vec:Word2Vec的扩展,同时学习词向量和文档向量
11.2 知识图谱增强
把词向量和知识图谱结合,可以获得结构化知识。例如,用知识图谱中的实体关系来增强实体词的表示。
11.3 动态词向量
传统词向量是静态的,但有些词的意思会随时间变化(如”酷”这个词在80年代和现在的含义完全不同)。动态词向量研究如何捕捉词义的时间演变。
十二、实践建议与常见问题
12.1 词向量选择指南
| 场景 | 推荐选择 | 理由 |
|---|---|---|
| 快速实验 | Word2Vec (gensim) | API简单,效果不错 |
| 多语言/子词 | FastText | 支持OOV,处理形态丰富语言 |
| 形态丰富语言 | FastText | 德语、土耳其语等 |
| 下游任务效果 | BERT | 上下文敏感,性能最强 |
| 资源受限 | DistilBERT | 小模型,效果还行 |
| 语义相似度 | Sentence-BERT | 专门优化过 |
12.2 超参数设置建议
embedding_dim(向量维度)
- 小语料:50-100
- 中等语料:100-200
- 大语料:200-300
- 更高维度不一定更好,可能过拟合
window_size(上下文窗口)
- 捕捉语法:2-3
- 捕捉语义:5-10
- 需要权衡:太大可能引入噪音
min_count(最小词频)
- 大语料:可以设高(5-10)
- 小语料:设低(1-2)
negative_samples(负采样数)
- 通常5-20
- 负样本越多,训练越慢,但效果可能更稳定
12.3 常见问题
Q: 词向量训练很慢怎么办? A: 1) 减少词表大小(提高min_count);2) 用负采样代替完整softmax;3) 用更大的batch size;4) 用GPU训练
Q: 相似词效果不好怎么办? A: 1) 检查语料质量;2) 增加训练数据;3) 调整window_size;4) 用更大的embedding_dim
Q: OOV词怎么办? A: 用FastText(子词)或者用字符级编码
Q: 如何处理中文? A: 需要先分词(可以用jieba),或者用字向量(不需分词),或者直接用预训练的中文模型
参考文献与推荐阅读
-
经典论文
- Mikolov, T., et al. (2013). Distributed representations of words and phrases and their compositionality. NeurIPS.
- Mikolov, T., et al. (2013). Efficient estimation of word representations in vector space. ICLR Workshop.
- Pennington, J., Socher, R., & Manning, C. D. (2014). GloVe: Global vectors for word representation. EMNLP.
- Bojanowski, P., et al. (2017). Enriching word vectors with subword information. TACL, 5, 135-146.
- Peters, M. E., et al. (2018). Deep contextualized word representations. NAACL-HLT.
- Devlin, J., et al. (2019). BERT: Pre-training of deep bidirectional transformers for language understanding. NAACL-HLT.
- Rogers, A., et al. (2020). A primer in BERTology: What we know about how BERT works. TACL, 8, 842-866.
-
进阶阅读
- Levy, O., & Goldberg, Y. (2014). Neural Word Embedding as Implicit Matrix Factorization. NeurIPS.
- Schnabel, T., et al. (2015). Evaluation methods for unsupervised word embeddings. EMNLP.
- Bakarov, A. (2018). A Survey of Word Embeddings Evaluation Methods. arXiv.
- Ethayarajh, K. (2019). How Contextual are Contextualized Word Representations? EMNLP.
-
认知科学相关
- Firth, J. R. (1957). Papers in Linguistics 1934-1951. Oxford University Press.
- Rosch, E. (1975). Cognitive representations of semantic categories. Journal of Experimental Psychology.
- Rogers, T. T., & McClelland, J. L. (2004). Semantic Cognition: A Parallel Distributed Processing Approach. MIT Press.
关联文档
实践示例
- 使用gensim训练Word2Vec:参考gensim官方文档
- 使用HuggingFace加载BERT:transformers库文档
- 词向量可视化:使用TensorBoard或Plotly
- 偏见检测与修正:参考Tensorflow的what-if工具