第4章:GloVe与FastText — 全局矩阵分解 vs 局部窗口、子词信息对OOV问题的处理

好,咱们继续往下走。

上一章聊了Word2Vec,那个基于局部窗口的玩法。说实话,当年我刚开始接触词向量时,觉得Word2Vec已经够神奇了。但做着做着就发现,有些场景它不太够用。比如,你训练好的模型,遇到一个没见过的词,直接傻眼——这就是OOV(Out-of-Vocabulary)问题。

另外还有一个让我纠结的点:Word2Vec只看局部上下文,它会不会漏掉一些全局的统计信息?

嗯,今天咱们就来解决这两个问题。主角就是GloVe和FastText。

4.1 GloVe:把全局统计也拉进来

GloVe的全称是Global Vectors for Word Representation。名字里就带着“全局”二字。

它的核心思路其实很朴素:词向量不仅要能预测上下文,还要能拟合整个语料库的共现统计信息

4.1.1 共现矩阵是什么?

你想想看,Word2Vec每次只用一个窗口内的词来训练。但整个语料库里,“苹果”和“水果”一起出现的次数,其实是一个很有价值的全局信号。

GloVe的做法是:先统计一个词-词共现矩阵。矩阵的每个元素 Xij 表示词 j 出现在词 i 上下文中的次数。

举个例子,假设我们有这么几句话:

我喜欢吃苹果
苹果是一种水果
水果很健康

统计“苹果”和“水果”的共现次数,你会发现它们经常一起出现。这个全局信息,Word2Vec其实没有直接利用。

核心思想:GloVe的目标是让词向量的点积,尽可能接近共现次数的对数。

公式长这样:wi · wj + bi + bj ≈ log(Xij)

说白了,就是让两个词的向量越相似,它们的点积越大,对应的共现次数也应该越大。

4.1.2 损失函数的设计

GloVe的损失函数我当年第一次看时觉得挺巧妙:

J = Σ f(Xij) * (wi · wj + bi + bj - log(Xij))²

这里有个权重函数 f(Xij),它的作用是:

  • 高频共现的词对,权重不能太大(防止被“的”、“是”这类词带偏)
  • 低频共现的词对,权重也不能太小(否则信息浪费)

我在项目中遇到过一个问题:直接用原始共现次数训练,结果“的”和“了”这种高频词的向量几乎主导了整个空间。后来加了权重函数,效果才正常。

个人经验:GloVe在中等规模语料(几亿到几十亿词)上表现很好。如果语料太小,共现矩阵会非常稀疏,效果反而不如Word2Vec。

4.1.3 GloVe vs Word2Vec:怎么选?

对比维度 Word2Vec GloVe
训练方式 局部窗口预测 全局共现统计
统计信息 只利用局部 利用全局
训练速度 快(尤其是负采样) 中等(需构建共现矩阵)
OOV处理 无法处理 无法处理
适用场景 大规模语料、实时训练 中等规模、需要全局语义

我个人习惯是:如果语料在10亿词以内,我会优先试试GloVe。超过这个量级,Word2Vec的负采样版本更省时间。

4.2 FastText:子词信息来救场

好,现在来说OOV问题。

你想想看,Word2Vec和GloVe都有一个共同的硬伤:词表是固定的。训练时没见过的词,推理时直接返回零向量或者报错。

我在做中文分词项目时就吃过这个亏。模型训练时用的是新闻语料,结果上线后用户输入了一个网络新词“绝绝子”,模型直接懵了。

4.2.1 子词(Subword)是什么?

FastText的思路其实很简单:别再拿整个词当最小单位了,把词拆成更小的片段

比如“apple”这个词,FastText会把它拆成:

"<ap", "app", "ppl", "ple", "le>"

其中 < 和 > 是词边界标记。每个子词(n-gram)都会学到一个向量。

最终“apple”的向量,就是所有这些子词向量的加和平均

关键点:即使“apples”这个词没在训练集中出现过,只要它的子词“app”、“ppl”、“ple”、“les”等在训练集中出现过,就能拼出它的向量。

4.2.2 为什么能解决OOV?

原因很简单:子词的组合是无限的,但子词本身是有限的

举个例子:

  • 训练集里有“running”、“runner”、“ran”
  • 它们共享子词“run”、“nn”、“ning”等
  • 新词“runaway”出现时,子词“run”和“naway”的部分片段已经在训练集中学过了
  • 于是“runaway”的向量可以被合理估计出来

我曾经用FastText处理过生物医学领域的术语。那些专业名词又长又怪,但很多都共享词根。FastText在这种场景下,OOV覆盖率从不到60%直接提升到了90%以上。

4.2.3 FastText的损失函数

FastText的训练方式和Word2Vec的CBOW或Skip-gram几乎一样。唯一的区别是:

  • Word2Vec:输入是一个词的向量
  • FastText:输入是一个词的所有子词向量的平均

代码实现上,其实就是多了一步子词提取:

def get_subwords(word, min_n=3, max_n=6):
    """提取一个词的所有n-gram子词"""
    word = "<" + word + ">"
    subwords = []
    for n in range(min_n, max_n + 1):
        for i in range(len(word) - n + 1):
            subwords.append(word[i:i+n])
    return subwords

# 示例
print(get_subwords("apple"))
# 输出: ['<ap', 'app', 'ppl', 'ple', 'le>',
#        '<app', 'appl', 'pple', 'ple>',
#        '<appl', 'apple', 'pple>',
#        '<apple', 'apple>']

注意:子词长度(min_n和max_n)的选择很关键。太短(比如2-gram)会产生大量无意义的子词,太长(比如6-gram以上)又失去了泛化能力。我一般默认用3-6。

4.3 实战对比:GloVe vs FastText

咱们用一个简单的例子来感受一下区别。

假设训练语料里有“苹果”、“香蕉”、“水果”,但没有“榴莲”。

  • GloVe:遇到“榴莲”,直接返回零向量。没辙。
  • FastText:“榴莲”包含子词“榴”、“莲”、“榴莲”等。如果“榴”和“莲”在其他词中出现过(比如“石榴”、“莲花”),就能拼出一个合理的向量。

我做过一个实验:在中文电商评论数据上,用FastText处理商品名。那些生僻品牌名、错别字、新造词,FastText都能给出有意义的向量。而GloVe和Word2Vec直接投降。

3.3.1 什么时候用哪个?

场景 推荐方法 原因
语料干净、词表固定 GloVe 或 Word2Vec 训练快,效果稳定
语料中有大量新词、拼写变体 FastText 子词机制天然抗OOV
需要全局语义信息 GloVe 共现矩阵捕捉全局统计
资源有限、需要快速迭代 FastText(负采样版) 训练速度快,且自带OOV处理

我的建议:如果你不确定选哪个,直接上FastText。它几乎在所有场景下都不会太差,而且OOV处理是刚需。GloVe更像是一个“锦上添花”的选择——当你需要更精细的全局语义时再考虑。

4.4 小结

今天咱们聊了两个重要的词向量方法:

  • GloVe:把全局共现统计拉进来,用矩阵分解的思路学词向量。适合中等规模、需要全局语义的场景。
  • FastText:引入子词信息,让模型能处理没见过的词。适合有大量新词、拼写变体的场景。

说实话,这两个方法现在看起来有点“古典”了。但在很多实际项目中,它们依然是性价比很高的选择。尤其是FastText,我到现在还在用——简单、稳定、不挑数据。

下一章,咱们会聊聊ELMo和BERT这些预训练模型。你会发现,它们其实都借鉴了今天讲的这些思路:全局上下文、子词信息。嗯,到时候你就明白了。