查看全课大纲(第 16 / 69 节)
  1. 1.NLP 的研究领域、应用与学习路径
  2. 2.自然语言的基本术语
  3. 3.信息论基础:信息熵与交叉熵
  4. 4.语言的进化与语义变迁
  5. 5.主观概率、客观概率与条件概率
  6. 6.贝叶斯原理与推理
  7. 7.随机变量:二项式、期望方差与联合概率
  8. 8.常见概率分布
  9. 9.辛普森悖论与贝叶斯解题实例
  10. 10.语言模型:语言概率与词袋模型
  11. 11.n-gram 语言模型(二元 / 三元 / N-Gram)
  12. 12.语言模型评价:交叉熵、困惑度与插值
  13. 13.朴素贝叶斯文本分类:垃圾邮件
  14. 14.拼写检查
  15. 15.中文分词:构词法、最大匹配、最优路径与 Jieba
  16. 16.词表示问题与语义相似度
  17. 17.TF-IDF 权重处理
  18. 18.One-Hot 表达与词表示的局限
  19. 19.Word2Vec 核心思想
  20. 20.Word2Vec 训练过程
  21. 21.Skip-gram 模型详解
  22. 22.词性标注任务与标注方法(规则 + N-Gram)
  23. 23.EM 算法与混合模型
  24. 24.高斯混合模型 GMM
  25. 25.从混合模型到隐马尔可夫模型
  26. 26.隐马尔可夫模型(HMM)推导与维特比解码
  27. 27.线性链条件随机场 CRF
  28. 28.贝叶斯网络与图模型
  29. 29.隐马尔可夫链与 HMM 推导
  30. 30.PLSA 主题模型
  31. 31.LSI 与 SVD 向量表达
  32. 32.LDA 与狄利克雷分布
  33. 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
  34. 34.上下文无关文法(CFG)与句法分析
  35. 35.概率上下文无关文法 PCFG
  36. 36.统计机器翻译:词对齐与翻译模型
  37. 37.统计机器翻译中的对齐模型与参数估计
  38. 38.从统计机器翻译到神经机器翻译
  39. 39.神经元与反向传播速览(指向深度学习课)
  40. 40.CNN 原理及其在 NLP 中的应用
  41. 41.循环神经网络的基本原理与 Elman / Jordan 网络
  42. 42.LSTM:长短期记忆网络的核心思想与分步实现
  43. 43.Encoder-Decoder 框架
  44. 44.Seq2Seq 与早期注意力机制
  45. 45.注意力机制的背景与动机
  46. 46.注意力模型实现原理与匹配函数
  47. 47.记忆网络:组成与计算
  48. 48.注意力机制的应用与历史延展
  49. 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
  50. 50.Multi-Head Attention、FFNN 与位置编码
  51. 51.层归一化、损失函数与 Universal Transformer 延展
  52. 52.预训练范式:从 ELMo 到 BERT
  53. 53.BERT 与掩码语言模型
  54. 54.BERT 下游任务微调与 GLUE 口径
  55. 55.GPT 与自回归语言建模
  56. 56.GPT-1 到 GPT-4 的演进
  57. 57.指令微调与 RLHF
  58. 58.分词器:BPE、WordPiece 与中文词表
  59. 59.上下文窗口与长文本处理
  60. 60.能力边界、成本与适用场景
  61. 61.文本分类实战:从朴素贝叶斯到逻辑回归
  62. 62.命名实体识别实战
  63. 63.文本生成与摘要实战
  64. 64.语义检索与向量库实战
  65. 65.RAG 知识库问答
  66. 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
  67. 67.幻觉与事实性问题
  68. 68.文本与知识结合(知识图谱压缩)
  69. 69.跨媒体信息检索与未来展望

词表示问题与语义相似度

小象实战讲义 · 自然语言处理

在自然语言处理中,如何让计算机“理解”词语的含义是核心挑战。本节我们将探讨词表示(Word Representation)的基本问题,即如何将离散的、符号化的词语转换为计算机可以处理的数学对象。我们将从最直观但存在局限的独热表示(One-Hot)入手,引出基于分布式假设的语义相似度概念,并学习一种基于计数的经典方法,为后续学习更强大的词向量模型(如Word2Vec)奠定基础。本节内容衔接了传统语言模型与神经网络模型,是理解现代NLP模型如何“表示”语言的关键。

💡 核心导读

  • 词表示的动机:将自然语言(离散符号序列)转化为数学向量,以便进行相似度计算、分类等下游任务。
  • 独热表示的局限:向量稀疏、维度高、无法表达词语间的语义关系(正交性)。
  • 分布式语义假设:“一个词的含义由其上下文决定”,这是构建稠密、语义化词向量的理论基础。
  • 基于计数的词表示方法:通过统计一个词在语料库中特定窗口内与其他词共现的频率,构建其向量表示。
  • 相似度度量:使用余弦相似度(Cosine Similarity)来衡量两个词向量在语义空间中的接近程度。

从符号到向量:词表示的动机

自然语言文本本质上是一个离散符号(Discrete Symbols)的序列,例如单词序列。对于计算机而言,像“kitten”(小猫)和“cat”(猫)这样的词,如果不经过特殊处理,它们就是两个完全独立、毫无关系的符号,就像“钱包”和“书包”一样。

为了让机器学习模型能够处理语言,我们必须将这些符号数学化。无论是机器翻译、语音识别还是拼写纠正,都需要将词或语言本身视为数据,并通过数学方式进行计算和推理。这就是词表示的核心目标:为词汇表中的每一个词,找到一个合适的数学表示(通常是向量),从而将语言问题转化为数学问题。

独热表示及其局限

最直观的词表示方法是独热表示(One-Hot Representation)。

定义:假设我们的词汇表(Vocabulary)大小为 ∣V∣|V|。对于词汇表中的第 ii 个词,我们用一个长度为 ∣V∣|V| 的向量来表示它,这个向量在第 ii 维上是1,其余所有维都是0。

例如,假设词汇表 V=[a,hotel,motel,dog,zebra]V = [\text{a}, \text{hotel}, \text{motel}, \text{dog}, \text{zebra}],那么:

  • a 的独热向量为 [1,0,0,0,0][1, 0, 0, 0, 0]
  • hotel 的独热向量为 [0,1,0,0,0][0, 1, 0, 0, 0]
  • zebra 的独热向量为 [0,0,0,0,1][0, 0, 0, 0, 1]
# 示例:独热向量表示
vocabulary = ['a', 'hotel', 'motel', 'dog', 'zebra']
vocab_size = len(vocabulary)

# 创建词到索引的映射
word_to_idx = {word: i for i, word in enumerate(vocabulary)}

def one_hot_encode(word):
    vec = [0] * vocab_size
    vec[word_to_idx[word]] = 1
    return vec

hotel_vec = one_hot_encode('hotel')
motel_vec = one_hot_encode('motel')
dog_vec = one_hot_encode('dog')

print(f"词汇表: {vocabulary}")
print(f"hotel 的独热向量: {hotel_vec}")
print(f"motel 的独热向量: {motel_vec}")
print(f"dog 的独热向量:   {dog_vec}")
词汇表: ['a', 'hotel', 'motel', 'dog', 'zebra']
hotel 的独热向量: [0, 1, 0, 0, 0]
motel 的独热向量: [0, 0, 1, 0, 0]
dog 的独热向量:   [0, 0, 0, 1, 0]

有了向量表示,我们就可以进行数学计算,比如计算两个词向量的相似度(Similarity)。一个简单的相似度度量是向量的点积(内积)。


# —— 重新构建上文用到的微型数据(本节各代码块相互独立,可单独运行)
vocabulary = ['a', 'hotel', 'motel', 'dog', 'zebra']
vocab_size = len(vocabulary)
word_to_idx = {word: i for i, word in enumerate(vocabulary)}
def one_hot_encode(word):
    vec = [0] * vocab_size
    vec[word_to_idx[word]] = 1
    return vec
hotel_vec = one_hot_encode('hotel')
motel_vec = one_hot_encode('motel')
dog_vec = one_hot_encode('dog')

# 计算独热向量之间的点积(内积)
def dot_product(vec_a, vec_b):
    return sum(a * b for a, b in zip(vec_a, vec_b))

print(f"hotel · motel = {dot_product(hotel_vec, motel_vec)}")
print(f"hotel · dog = {dot_product(hotel_vec, dog_vec)}")
print(f"hotel · hotel = {dot_product(hotel_vec, hotel_vec)}")
hotel · motel = 0
hotel · dog = 0
hotel · hotel = 1

独热表示的核心问题:

  1. 稀疏性(Sparse):向量维度极高(等于词汇表大小),且绝大多数元素为0。
  2. 正交性(Orthogonal):任意两个不同词的独热向量点积为0,即它们是正交的。这意味着模型无法从表示中得知 hotel 和 motel 的语义相关性远高于它们与 dog 的相关性。
  3. 语义信息缺失:无法捕获词与词之间的任何语义关系(如同义、近义、上下位关系)。

我们希望得到的词表示,应该能让语义相近的词(如 hotel 和 motel)在向量空间中距离较近,而与语义较远的词(如 dog)距离较远。独热表示无法做到这一点。

分布式语义与基于计数的方法

如何获得能表达语义相似度的词表示呢?语言学家 J.R. Firth 在1957年提出了一个深刻见解:“You shall know a word by the company it keeps.”(观其伴,知其义。)这被称为分布式语义假设(Distributional Hypothesis):一个词的含义可以由其上下文(Context)中的词来定义。

基于这个假设,我们可以通过统计一个词在大量文本中与哪些词共同出现(共现)来构建它的向量表示。这就是基于计数的方法(Count-Based Methods)。

基本步骤:

  1. 定义一个上下文词基础词汇表(Basis Vocabulary) CC,通常由语料库中除停用词(Stop Words)外的重要词构成。
  2. 定义一个窗口大小(Window Size) ww(例如,w=3w=3 表示考虑中心词左右各3个词)。
  3. 在语料库中扫描每个目标词(Target Word) 的出现位置。
  4. 对于每个目标词实例,统计其窗口内出现的、属于基础词汇表 CC 的上下文词。
  5. 汇总所有实例的统计结果,形成一个基于上下文词计数的向量,作为目标词的表示。

举例说明: 假设我们有如下小型语料库(已去除 the, and, on, that, she 等停用词):

  1. cute kitten purred
  2. cute furry cat purred miaowed
  3. small kitten miaowed
  4. loud furry dog ran bit

我们定义基础词汇表 CC = {bit, cute, furry, loud, miaowed, purred, ran, small},窗口大小 w=2w=2(即中心词左右各取两个词;四句话都很短,句内不足两个词的一侧就只取到句界为止)。

  • 对于 kitten:
    • 在句子1中,上下文是 {cute, purred}
    • 在句子3中,上下文是 {small, miaowed}
    • 合并统计:{cute, purred, small, miaowed}
    • 向量表示为(按C顺序):[0, 1, 0, 0, 1, 1, 0, 1]
  • 对于 cat:
    • 在句子2中,上下文是 {cute, furry, purred, miaowed}
    • 向量表示为:[0, 1, 1, 0, 1, 1, 0, 0]
  • 对于 dog:
    • 在句子4中,上下文是 {loud, furry, ran, bit}
    • 向量表示为:[1, 0, 1, 1, 0, 0, 1, 0]
# 基于计数的词表示示例
import math

# 基础上下文词汇表
context_vocab = ['bit', 'cute', 'furry', 'loud', 'miaowed', 'purred', 'ran', 'small']
context_to_idx = {word: i for i, word in enumerate(context_vocab)}

# 定义目标词的上下文计数向量(基于上述手工分析)
kitten_vec = [0, 1, 0, 0, 1, 1, 0, 1]  # cute, miaowed, purred, small
cat_vec    = [0, 1, 1, 0, 1, 1, 0, 0]  # cute, furry, miaowed, purred
dog_vec    = [1, 0, 1, 1, 0, 0, 1, 0]  # bit, furry, loud, ran

def cosine_similarity(vec_a, vec_b):
    """计算两个向量的余弦相似度"""
    dot = sum(a * b for a, b in zip(vec_a, vec_b))
    norm_a = math.sqrt(sum(a * a for a in vec_a))
    norm_b = math.sqrt(sum(b * b for b in vec_b))
    if norm_a == 0 or norm_b == 0:
        return 0
    return dot / (norm_a * norm_b)

print("基于上下文的词向量(基础词汇表顺序):")
print(f"kitten: {kitten_vec}")
print(f"cat:    {cat_vec}")
print(f"dog:    {dog_vec}")
print("\n余弦相似度计算:")
print(f"kitten 与 cat 的余弦相似度: {cosine_similarity(kitten_vec, cat_vec):.4f}")
print(f"cat 与 dog 的余弦相似度:    {cosine_similarity(cat_vec, dog_vec):.4f}")
print(f"kitten 与 dog 的余弦相似度: {cosine_similarity(kitten_vec, dog_vec):.4f}")
基于上下文的词向量(基础词汇表顺序):
kitten: [0, 1, 0, 0, 1, 1, 0, 1]
cat:    [0, 1, 1, 0, 1, 1, 0, 0]
dog:    [1, 0, 1, 1, 0, 0, 1, 0]

余弦相似度计算:
kitten 与 cat 的余弦相似度: 0.7500
cat 与 dog 的余弦相似度:    0.2500
kitten 与 dog 的余弦相似度: 0.0000

结果分析:

  • kitten 与 cat 的余弦相似度为 0.7500:两者各有 4 个非零上下文维度,其中 cute、purred、miaowed 三项共享,kitten 独有 small、cat 独有 furry。共享比例高,所以相似度高,符合它们语义相近的直觉。
  • cat 与 dog 的余弦相似度为 0.2500:8 个维度里只共享 furry 一项(cat 另有 cute、purred、miaowed,dog 另有 loud、ran、bit)。共享少,相似度自然低。
  • kitten 与 dog 的相似度为 0.0000:两者的上下文集合在这个小语料里完全不相交,向量正交。

对比一下独热表示:三个词的独热向量两两点积都是 0,相似度全都一样是 0,无法把“kitten 更像 cat”这件事表达出来。基于计数的表示做到了 0.75 / 0.25 / 0 的区分,这才是它能表达语义相似度的原因。

余弦相似度(Cosine Similarity) 是衡量向量相似度的常用指标,它计算两个向量夹角的余弦值,取值范围为 [−1,1][-1, 1],值越大表示越相似。其定义为: simcos(u,v)=u⋅v∣u∣∣v∣=∑i=1nuivi∑i=1nui2∑i=1nvi2\text{sim}_{\text{cos}}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{|\mathbf{u}| |\mathbf{v}|} = \frac{\sum_{i=1}^n u_i v_i}{\sqrt{\sum_{i=1}^n u_i^2} \sqrt{\sum_{i=1}^n v_i^2}} 它具有归一化的优点,对向量的绝对长度不敏感,更适合比较不同频率词的表示。

方法的改进与局限

上述基于计数的方法虽然简单有效,但仍有改进空间:

  • 高频词影响:像 the, a 这样的高频词,作为上下文时可能提供很少的语义信息。我们需要区分哪些共现是信息丰富的,哪些只是偶然或普遍的。这引出了像 TF-IDF、点互信息(PMI) 等加权或归一化方法。
  • 向量维度:向量维度等于基础词汇表大小,仍然可能很高。我们能否学习到一个更低维、更稠密的表示?这直接引向了基于神经网络的词嵌入模型,如 Word2Vec。

从表示学习的角度看,基于计数的方法可以看作是在学习一个嵌入矩阵(Embedding Matrix) E∈R∣V∣×∣C∣E \in \mathbb{R}^{|V| \times |C|},其中每一行对应一个目标词的向量。而独热向量 ot\mathbf{o}_t 可以用来检索这个矩阵中的某一行:vt=E⊤ot\mathbf{v}_t = E^\top \mathbf{o}_t。

📝 动手练一练

  1. 概念辨析:独热表示和基于上下文的计数表示,在表达词语“语义相似度”的能力上有何本质区别?请结合 hotel, motel, dog 的例子说明。
👉 点击查看参考答案

本质区别在于是否利用了词的分布信息(上下文)。

  • 独热表示:每个词被映射到一个唯一且正交的维度。hotel 和 motel 的向量点积为0,hotel 和 dog 的点积也为0。这种表示法完全无法区分语义相近词对 (hotel, motel) 与语义无关词对 (hotel, dog) 在相似度上的差异,因为它没有编码任何语义关联。
  • 基于上下文的计数表示:一个词的向量由其周围出现的词(上下文)的统计信息构成。hotel 和 motel 很可能拥有相似的上下文(如 book, stay, room),因此它们的向量在非零维度上有大量重叠,余弦相似度高。而 dog 的上下文(如 walk, bark, pet)与 hotel 差异很大,导致向量重叠少,相似度低。这种表示法能够通过共享的上下文来反映语义的相似性。
  1. 手动计算:假设有一个极小的语料库,包含三个句子(已分词):['fast', 'car', 'race'], ['new', 'car', 'buy'], ['fast', 'dog', 'run']。设定上下文基础词汇表为 ['fast', 'new', 'race', 'buy', 'run'],窗口大小 w=1w=1。请手动计算或编写代码验证 car 和 dog 的基于计数的向量表示,并计算它们的余弦相似度。
👉 点击查看参考答案

# —— 重新构建上文用到的微型数据(本节各代码块相互独立,可单独运行)
vocabulary = ['a', 'hotel', 'motel', 'dog', 'zebra']
vocab_size = len(vocabulary)
word_to_idx = {word: i for i, word in enumerate(vocabulary)}
def one_hot_encode(word):
    vec = [0] * vocab_size
    vec[word_to_idx[word]] = 1
    return vec
hotel_vec = one_hot_encode('hotel')
motel_vec = one_hot_encode('motel')
dog_vec = one_hot_encode('dog')
def dot_product(vec_a, vec_b):
    return sum(a * b for a, b in zip(vec_a, vec_b))
import math
context_vocab = ['bit', 'cute', 'furry', 'loud', 'miaowed', 'purred', 'ran', 'small']
context_to_idx = {word: i for i, word in enumerate(context_vocab)}
kitten_vec = [0, 1, 0, 0, 1, 1, 0, 1]  # cute, miaowed, purred, small
cat_vec    = [0, 1, 1, 0, 1, 1, 0, 0]  # cute, furry, miaowed, purred
dog_vec    = [1, 0, 1, 1, 0, 0, 1, 0]  # bit, furry, loud, ran
def cosine_similarity(vec_a, vec_b):
    """计算两个向量的余弦相似度"""
    dot = sum(a * b for a, b in zip(vec_a, vec_b))
    norm_a = math.sqrt(sum(a * a for a in vec_a))
    norm_b = math.sqrt(sum(b * b for b in vec_b))
    if norm_a == 0 or norm_b == 0:
        return 0
    return dot / (norm_a * norm_b)

# 手动推导/代码验证
corpus_small = [['fast', 'car', 'race'], ['new', 'car', 'buy'], ['fast', 'dog', 'run']]
context_vocab_small = ['fast', 'new', 'race', 'buy', 'run']
c2i = {w:i for i,w in enumerate(context_vocab_small)}

def manual_count(word, corpus, window=1):
    vec = [0]*len(context_vocab_small)
    for sent in corpus:
        for i, w in enumerate(sent):
            if w == word:
                start = max(0, i-window)
                end = min(len(sent), i+window+1)
                # 收集窗口内的上下文词
                context_words = sent[start:end]
                # 移除中心词本身
                context_words.remove(word)
                # 统计
                for cw in context_words:
                    if cw in c2i:
                        vec[c2i[cw]] += 1
    return vec

car_vec_small = manual_count('car', corpus_small)
dog_vec_small = manual_count('dog', corpus_small)

print(f"基础词汇表: {context_vocab_small}")
print(f"car 的计数向量: {car_vec_small}")
print(f"dog 的计数向量: {dog_vec_small}")

# 使用前面定义的 cosine_similarity 函数
sim = cosine_similarity(car_vec_small, dog_vec_small)
print(f"car 与 dog 的余弦相似度: {sim:.4f}")
基础词汇表: ['fast', 'new', 'race', 'buy', 'run']
car 的计数向量: [1, 1, 1, 1, 0]
dog 的计数向量: [1, 0, 0, 0, 1]
car 与 dog 的余弦相似度: 0.3536

计算过程:

  • car 的上下文:
    • 在 ['fast', 'car', 'race'] 中,窗口内词为 fast, race。
    • 在 ['new', 'car', 'buy'] 中,窗口内词为 new, buy。
    • 合并统计:fast(1), new(1), race(1), buy(1), run(0)。向量为 [1, 1, 1, 1, 0]。
  • dog 的上下文:
    • 在 ['fast', 'dog', 'run'] 中,窗口内词为 fast, run。
    • 合并统计:fast(1), new(0), race(0), buy(0), run(1)。向量为 [1, 0, 0, 0, 1]。
  • 余弦相似度计算:
    • 点积 = 11 + 10 + 10 + 10 + 0*1 = 1
    • car 的模 = 12+12+12+12+02=4=2\sqrt{1^2+1^2+1^2+1^2+0^2} = \sqrt{4} = 2
    • dog 的模 = 12+02+02+02+12=2≈1.4142\sqrt{1^2+0^2+0^2+0^2+1^2} = \sqrt{2} \approx 1.4142
    • 相似度 = 1/(2∗1.4142)≈0.35361 / (2 * 1.4142) \approx 0.3536,与代码输出一致。
  • 分析:car 和 dog 因为共享了上下文词 fast,所以相似度不为0,但整体相似度不高,符合直觉。

本章小结

本节我们深入探讨了自然语言处理中词表示的根本问题。我们认识到,将词语从离散符号转化为有意义的数学向量是NLP模型工作的基础。独热表示虽然简单,但其稀疏性和正交性使其无法捕获语义信息。Firth的分布式假设为我们指明了方向:通过词的上下文来定义其含义。基于计数的方法实践了这一思想,通过统计共现信息构建词向量,并利用余弦相似度来衡量语义关联,取得了比独热表示合理得多的结果。

然而,基于计数的方法通常会产生高维向量,且对高频常见词的处理不够精细。这为下一阶段基于神经网络的词嵌入模型(如Word2Vec)的兴起埋下了伏笔,后者能够学习到更低维、更稠密、且语义关系更丰富的词向量。

📋 行动清单

  • 理解独热表示的构造方式及其在语义相似度计算上的根本缺陷。
  • 掌握分布式语义假设的核心思想:“观其伴,知其义”。
  • 能够描述基于计数的词表示方法的基本步骤,并理解其如何体现语义相似度。
  • 会计算两个向量的余弦相似度,并能解释其结果在语义上的含义。

—— 小象教研组