GloVe、FastText 与子词嵌入


文档摘要

GloVe、FastText 与子词嵌入 本节摘要:Word2Vec 给每个词训练一个向量。GloVe 分解了共现矩阵。FastText 嵌入了「零件」。BPE 则架起了通往 Transformer 的桥。Word2Vec 留下了两个未解的问题:矩阵分解那条平行线是否本质上更优?对从未见过的词该怎么办?GloVe 用精心选择的损失做矩阵分解,匹配甚至超越 Word2Vec 且训练更省。FastText 通过嵌入字符 n-gram 修掉了 OOV——一个词是它各部分之和,即便词表外的词也能得到合理向量。Transformer 到来后,字节对编码(BPE)学一套覆盖一切的高频子词词表,成了所有现代 LLM 分词器的根基。本节从零走完这三条路,再说清何时伸手拿哪一个。

GloVe、FastText 与子词嵌入

本节摘要:Word2Vec 给每个词训练一个向量。GloVe 分解了共现矩阵。FastText 嵌入了「零件」。BPE 则架起了通往 Transformer 的桥。Word2Vec 留下了两个未解的问题:矩阵分解那条平行线是否本质上更优?对从未见过的词该怎么办?GloVe 用精心选择的损失做矩阵分解,匹配甚至超越 Word2Vec 且训练更省。FastText 通过嵌入字符 n-gram 修掉了 OOV——一个词是它各部分之和,即便词表外的词也能得到合理向量。Transformer 到来后,字节对编码(BPE)学一套覆盖一切的高频子词词表,成了所有现代 LLM 分词器的根基。本节从零走完这三条路,再说清何时伸手拿哪一个。

对应原课程:Phase 5 · Lesson 04 · glove-fasttext-subword(原英文 phases/05-nlp-foundations-to-advanced/04-glove-fasttext-subword/docs/en.md)。前置依赖:第 03 节(Word2Vec 从零实现)。

学习目标

阅读完本节,你应当能够:

  1. 说清 GloVe、FastText、BPE 各自要解决的问题与核心机制。
  2. 从零实现共现矩阵分解(GloVe)、字符 n-gram 嵌入(FastText)、迭代合并学词表(BPE)。
  3. 解释为何 FastText 能处理 OOV、BPE 为何是所有现代分词器的根基。
  4. 根据任务正确选型:何时用 GloVe、何时用 FastText、何时直接用模型自带的分词器。

一、问题与直觉

Word2Vec 留下了三个未解的问题。

第一,有一条平行的研究脉络——直接分解共现矩阵(LSA、HAL),而非做在线 skip-gram 更新。Word2Vec 的迭代法本质上更优,还是差异只是两者处理计数方式不同造成的假象?GloVe 回答了:用精心选择的损失做矩阵分解,匹配甚至超越 Word2Vec,而且训练更省。

第二,两种方法对没见过的词都无话可说。Zoomer-approveddogecoin、上周才造的任何专有名词、罕见词根的每一种屈折形式。FastText 修掉了这个,通过嵌入字符 n-gram——一个词是它各部分之和,包括语素,所以即便词表外的词也能得到一个合理向量。

第三,Transformer 到来后,问题再次转移。词级词表上限约百万条,真实语言比这开放得多。字节对编码(BPE) 及其同族通过学一套覆盖一切的高频子词词表解决了这个。每一个现代 LLM 的每一个现代分词器,都是子词分词器。

三种思路,各有职责:

  • GloVe(全局向量):构建词-词共现矩阵 X,X[i][j] 是词 j 出现在词 i 上下文里的频次。训练向量,使 v_i · v_j + b_i + b_j ≈ log(X[i][j])。给损失加权,免得高频对一家独大。完工。
  • FastText:一个词是它字符 n-gram 之和,再加它本身。where 变成 <wh, whe, her, ere, re>, <where>。词向量是这些分量向量之和。像 Word2Vec 那样训练。好处:未见词(whereupon)能从已知 n-gram 组合出来。
  • BPE(字节对编码):从单个字节(或字符)词表开始,数语料里每个相邻对的频次,把最频繁的对合并成一个新 token,重复 k 次。结果是一套 k + 256 个 token 的词表,高频序列(ingtionthe)是单 token,罕见词拆成熟悉的碎片。任何句子都能被分词成「某种东西」。

二、从零实现

GloVe:分解共现矩阵

import numpy as np from collections import Counter def build_cooccurrence(docs, window=5): pair_counts = Counter() vocab = {} for doc in docs: for token in doc: if token not in vocab: vocab[token] = len(vocab) for doc in docs: indexed = [vocab[t] for t in doc] for i, center in enumerate(indexed): for j in range(max(0, i - window), min(len(indexed), i + window + 1)): if i != j: distance = abs(i - j) pair_counts[(center, indexed[j])] += 1.0 / distance return vocab, pair_counts def glove_train(vocab, pair_counts, dim=16, epochs=100, lr=0.05, x_max=100, alpha=0.75, seed=0): n = len(vocab) rng = np.random.default_rng(seed) W = rng.normal(0, 0.1, size=(n, dim)) W_tilde = rng.normal(0, 0.1, size=(n, dim)) b = np.zeros(n) b_tilde = np.zeros(n) for epoch in range(epochs): for (i, j), x_ij in pair_counts.items(): weight = (x_ij / x_max) ** alpha if x_ij < x_max else 1.0 diff = W[i] @ W_tilde[j] + b[i] + b_tilde[j] - np.log(x_ij) coef = weight * diff grad_W_i = coef * W_tilde[j] grad_W_tilde_j = coef * W[i] W[i] -= lr * grad_W_i W_tilde[j] -= lr * grad_W_tilde_j b[i] -= lr * coef b_tilde[j] -= lr * coef return W + W_tilde

两个要点值得一说。加权函数 f(x) = (x/x_max)^alpha 压低极频繁对(如 (the, and)),免得它们在损失里一家独大。最终嵌入是 W(中心)与 W_tilde(上下文)两表之和——求和是已发表的小技巧,通常比单用一表更好。

FastText:子词感知嵌入

def char_ngrams(word, n_min=3, n_max=6): wrapped = f"<{word}>" grams = {wrapped} for n in range(n_min, n_max + 1): for i in range(len(wrapped) - n + 1): grams.add(wrapped[i:i + n]) return grams
>>> char_ngrams("where") {'<where>', '<wh', 'whe', 'her', 'ere', 're>', '<whe', 'wher', 'here', 'ere>', '<wher', 'where', 'here>'}

每个词用它 n-gram 的集合表示(通常 3 到 6 个字符)。词嵌入是其 n-gram 嵌入之和。做 skip-gram 训练时,把这一套插到 Word2Vec 原本用单一向量的位置。

def fasttext_vector(word, ngram_table): grams = char_ngrams(word) vecs = [ngram_table[g] for g in grams if g in ngram_table] if not vecs: return None return np.sum(vecs, axis=0)

对未见词,只要它的部分 n-gram 是已知的,你仍能得到一个向量。whereuponwhere 共享 <whherere<where,所以两者落在附近。

BPE:学到的子词词表

def learn_bpe(corpus, k_merges): vocab = Counter() for word, freq in corpus.items(): tokens = tuple(word) + ("</w>",) vocab[tokens] = freq merges = [] for _ in range(k_merges): pair_freq = Counter() for tokens, freq in vocab.items(): for a, b in zip(tokens, tokens[1:]): pair_freq[(a, b)] += freq if not pair_freq: break best = pair_freq.most_common(1)[0][0] merges.append(best) new_vocab = Counter() for tokens, freq in vocab.items(): new_tokens = [] i = 0 while i < len(tokens): if i + 1 < len(tokens) and (tokens[i], tokens[i + 1]) == best: new_tokens.append(tokens[i] + tokens[i + 1]) i += 2 else: new_tokens.append(tokens[i]) i += 1 new_vocab[tuple(new_tokens)] = freq vocab = new_vocab return merges def apply_bpe(word, merges): tokens = list(word) + ["</w>"] for a, b in merges: new_tokens = [] i = 0 while i < len(tokens): if i + 1 < len(tokens) and tokens[i] == a and tokens[i + 1] == b: new_tokens.append(a + b) i += 2 else: new_tokens.append(tokens[i]) i += 1 tokens = new_tokens return tokens
>>> corpus = Counter({"low": 5, "lower": 2, "newest": 6, "widest": 3}) >>> merges = learn_bpe(corpus, k_merges=10) >>> apply_bpe("lowest", merges) ['low', 'est</w>']

第一轮合并最频繁的相邻对。足够多轮后,高频子串(lowesttion)成了单 token,罕见词则干净地拆开。真正的 GPT / BERT / T5 分词器学 3 万到 10 万次合并。结果:任何文本都能分词成一段长度有界的已知 ID 序列,永远没有 OOV。

三、框架对比

实践中你很少自己训任何一个,而是加载预训练检查点。

import fasttext.util fasttext.util.download_model("en", if_exists="ignore") ft = fasttext.load_model("cc.en.300.bin") print(ft.get_word_vector("whereupon").shape) print(ft.get_word_vector("zoomerapproved").shape)

Transformer 时代的 BPE 式子词分词:

from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained("gpt2") print(tok.tokenize("unbelievably tokenized"))
['un', 'bel', 'iev', 'ably', 'Ġtoken', 'ized']

Ġ 前缀标记词边界(GPT-2 的约定)。每个现代分词器都是 BPE 变体、WordPiece(BERT)或 SentencePiece(T5、LLaMA)。

如何取舍

场景 选什么
预训练通用词向量,不容忍 OOV GloVe 300 维
预训练通用词向量,要处理拼写错误/新词/形态丰富语言 FastText
任何要进 Transformer 的(训练或推理) 模型自带的分词器,永不替换
从零训自己的语言模型 先在语料上训一个 BPE 或 SentencePiece 分词器
生产文本分类配线性模型 仍用 TF-IDF(第 02 节)

四、可复用产物

保存为 outputs/skill-embeddings-picker.md:

--- name: tokenizer-picker description: Pick a tokenization approach for a new language model or text pipeline. version: 1.0.0 phase: 5 lesson: 04 tags: [nlp, tokenization, embeddings] --- Given a task and dataset description, you output: 1. Tokenization strategy (word-level, BPE, WordPiece, SentencePiece, byte-level). One-sentence reason. 2. Vocabulary size target (e.g., 32k for an English-only LM, 64k-100k for multilingual). 3. Library call with the exact training command. Name the library. Quote the arguments. 4. One reproducibility pitfall. Tokenizer-model mismatch is the single most common silent production bug; call out which pair must be used together. Refuse to recommend training a custom tokenizer when the user is fine-tuning a pretrained LLM. Refuse to recommend word-level tokenization for any model targeting production inference. Flag non-English / multi-script corpora as needing SentencePiece with byte fallback.

五、练习

  1. 基础:跑 char_ngrams("playing")char_ngrams("played"),算两个 n-gram 集合的 Jaccard 重叠。应能看到大量共享碎片(plalayplay),这正是 FastText 跨形态变体迁移良好的原因。
  2. 进阶:扩展 learn_bpe 追踪词表增长。画出「每语料字符的 token 数」随合并次数的变化曲线,应看到起初快速压缩,渐近到约每 token 2~3 字符。
  3. 挑战:在莎士比亚全集上训一个 1 千次合并的 BPE。对比常见词与罕见专有名词的分词,测量前后的平均每词 token 数,写下让你意外的发现。

本节要点回顾

  1. Word2Vec 留三问:矩阵分解是否本质更优、OOV 怎么办、Transformer 时代词表怎么办。
  2. GloVe 分解共现矩阵:v_i·v_j + b_i + b_j ≈ log(X[i][j]),加权函数压低高频对,两表求和作最终嵌入。
  3. FastText 嵌入字符 n-gram:词向量是 n-gram 向量之和,未见词也能从已知碎片组合。
  4. BPE 迭代合并:从字节/字符起,反复合并最频繁相邻对,高频串成单 token、罕见词干净拆开。
  5. BPE 是所有现代分词器根基:GPT 用字节级 BPE,BERT 用 WordPiece,T5/LLaMA 用 SentencePiece。
  6. 加权函数 (x/x_max)^alpha(the, and) 这类极频繁对不主导损失。
  7. FastText 跨形态迁移好:共享 n-gram 让 playing/played 落得近。
  8. 永不替换模型自带分词器:分词器-模型不匹配是最常见的静默生产 bug。
  9. 选型:GloVe(通用、无 OOV 容忍)、FastText(拼写错/新词/形态丰富)、模型自带(进 Transformer)、TF-IDF(线性分类)。
  10. Transformer 用子词,词级永不进生产:词表上限百万,真实语言更开放。

下一节,我们把向量喂给分类器,进入「情感分析」——看正负情绪如何从词袋一路演进到 Transformer。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U