目录
- 前言:为什么文本表示是 NLP 的基石?
- 早期探索:基于计数的表示
- 词袋模型 (Bag of Words - BoW):简单粗暴的力量
- TF-IDF:重要的词,值得更高的权重
- N-gram 模型:捕捉局部语序的尝试
- 向量革命:分布式表示的兴起
- Word2Vec:让词语在向量空间中“活”起来
- GloVe:融合全局统计与局部上下文
- FastText:拥抱形态,告别 OOV 困境
- 深度语境时代:上下文感知的动态表示
- ELMo:一词多“义”,语境决定向量
- 比较与选择:为你的任务挑选合适的“词汇表”
- 总结与展望:表示方法的持续进化
在人工智能(AI)的广阔天地里,自然语言处理(NLP)无疑是最璀璨的明珠之一。它赋予机器理解、生成、并与人类语言交互的能力。但这一切魔法的起点,都离不开一个基础却至关重要的环节——文本表示(Text Representation)。如何将人类能理解的、非结构化的文本,转化为机器能够处理的、结构化的数值形式?这就像是为机器打造一套理解人类语言的“词汇表”和“语法规则”。
在这篇博客中,我们将踏上一段从简单到复杂的旅程,探索 NLP 领域中那些里程碑式的文本表示方法,从经典的词袋模型,到风靡一时的词向量,再到如今引领潮流的上下文感知嵌入。无论你是 AI 新手还是资深玩家,理解这些方法的原理、优劣和适用场景,都将是你提升 NLP 内功的关键一步。
前言:为什么文本表示是 NLP 的基石?
想象一下,你要教计算机区分垃圾邮件和正常邮件。计算机无法直接“阅读”邮件内容。你需要找到一种方法,把邮件中的文字信息转换成计算机可以理解的数字信号。这个转换过程,就是文本表示。
一个好的文本表示方法,应该能够:
- 捕捉关键信息:有效反映文本的核心内容和语义。
- 区分不同文本:相似的文本应该有相似的表示,不同的文本则应易于区分。
- 计算友好:生成的表示(通常是向量)便于后续机器学习模型的计算和处理。
没有合适的文本表示,再强大的算法也如同无米之炊。因此,选择和设计有效的文本表示方法,始终是 NLP 项目成功的关键因素之一。
早期探索:基于计数的表示
在 NLP 发展的早期,研究者们尝试用最直观的方式来表示文本——统计词语出现的频率。
词袋模型 (Bag of Words - BoW):简单粗暴的力量
BoW 是最基础的文本表示方法之一。它简单粗暴地假设:一篇文档的含义,主要由其中出现的词语决定,而与词语的顺序无关。
核心思想:
- 构建词汇表 (Vocabulary):收集语料库中所有出现的不重复词语。
- 向量化 (Vectorization):对于每篇文档,创建一个向量,其维度等于词汇表的大小。向量的每个元素对应词汇表中的一个词,其值表示该词在文档中出现的次数(或其他统计量,如是否出现)。
例子:
文档1: “the cat sat on the mat”
文档2: “the dog ate my homework”
词汇表: {“the”, “cat”, “sat”, “on”, “mat”, “dog”, “ate”, “my”, “homework”} (9个词)
文档1的 BoW 向量 (计数): [2, 1, 1, 1, 1, 0, 0, 0, 0]
文档2的 BoW 向量 (计数): [1, 0, 0, 0, 0, 1, 1, 1, 1]
流程图 (Mermaid):
说明:
- 输入文本示例:“the cat sat on the mat”
- 分词结果:[“the”, “cat”, “sat”, “on”, “the”, “mat”]
- 词汇表示例:{“the”, “cat”, “sat”, “on”, “mat”, “dog”, “ate”, “my”, “homework”}
- 输出BoW向量:[2, 1, 1, 1, 1, 0, 0, 0, 0]
Python 代码示例 (使用 Scikit-learn):
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
'the cat sat on the mat',
'the dog ate my homework'
]
# 初始化 CountVectorizer
vectorizer = CountVectorizer()
# 学习词汇表并转换文本
X = vectorizer.fit_transform(corpus)
# 查看词汇表
print("词汇表:", vectorizer.get_feature_names_out())
# 查看 BoW 矩阵 (稀疏矩阵表示)
print("BoW 矩阵:\n", X.toarray())
代码解释:
CountVectorizer
是 scikit-learn 中实现 BoW 的工具。fit_transform
同时学习词汇表(fit)并将文本转换为 BoW 矩阵(transform)。get_feature_names_out()
显示学习到的词汇表。X.toarray()
将稀疏矩阵转换为密集数组,方便查看。
优点: 简单、快速、易于理解和实现。在某些任务(如文本分类)上效果尚可。
缺点:
- 忽略语序: “man bites dog” 和 “dog bites man” 的 BoW 表示可能非常相似,但含义完全不同。
- 无法捕捉语义: 无法理解 “cat” 和 “feline” 是近义词。
- 维度灾难与稀疏性: 词汇表可能非常庞大,导致向量维度极高且大部分元素为零。
TF-IDF:重要的词,值得更高的权重
BoW 只考虑了词频 (Term Frequency, TF),但有些词(如 “the”, “a”, “is”)在所有文档中都频繁出现,它们对区分文档的重要性可能不高。TF-IDF (Term Frequency-Inverse Document Frequency) 旨在解决这个问题。
核心思想:一个词的重要性与其在**当前文档中出现的频率 (TF)成正比,与其在整个语料库中出现的文档频率 (IDF)**成反比。
- TF (Term Frequency): 词 t 在文档 d 中出现的频率。
TF(t, d) = (词 t 在文档 d 中出现的次数) / (文档 d 的总词数)
(有多种计算方式)
- IDF (Inverse Document Frequency): 衡量词 t 的普遍性。
IDF(t, D) = log(语料库 D 的总文档数 / (包含词 t 的文档数 + 1))
(加 1 防止分母为零,log 平滑处理)
- TF-IDF:
TF-IDF(t, d, D) = TF(t, d) * IDF(t, D)
流程图 (Mermaid):
Python 代码示例 (使用 Scikit-learn):
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
'this is the first document',
'this document is the second document',
'and this is the third one',
'is this the first document' # 与第一篇相似
]
# 初始化 TfidfVectorizer
vectorizer = TfidfVectorizer()
# 学习词汇表和 IDF,并转换文本
X = vectorizer.fit_transform(corpus)
# 查看词汇表
print("词汇表:", vectorizer.get_feature_names_out())
# 查看 TF-IDF 矩阵
print("TF-IDF 矩阵:\n", X.toarray())
代码解释:
TfidfVectorizer
结合了CountVectorizer
和TfidfTransformer
的功能。- 它计算 TF-IDF 值而不是原始计数。
- 输出的矩阵中,值越高表示该词在该文档中越重要(既在该文档中常出现,又在其他文档中不常出现)。
优点: 相较于 BoW,考虑了词语的重要性,能更好地区分文档。
缺点: 仍然忽略语序和语义信息。维度和稀疏性问题依然存在。
N-gram 模型:捕捉局部语序的尝试
为了解决 BoW 和 TF-IDF 忽略语序的问题,N-gram 模型应运而生。它将连续的 N 个词语(或字符)视为一个单元(token)。
- Unigram (1-gram): 单个词语,等同于 BoW。(“the”, “cat”, “sat”)
- Bigram (2-gram): 连续两个词语。(“the cat”, “cat sat”, “sat on”, “on the”, “the mat”)
- Trigram (3-gram): 连续三个词语。(“the cat sat”, “cat sat on”, “sat on the”, “on the mat”)
核心思想:通过考虑相邻词语的组合,捕捉局部的语序信息。可以将 N-gram 视为扩展的词汇表,然后应用 BoW 或 TF-IDF 进行向量化。
Python 代码示例 (生成 Bigram):
from sklearn.feature_extraction.text import CountVectorizer
corpus = ['the cat sat on the mat']
# 使用 ngram_range=(2, 2) 来指定只生成 bigram
bigram_vectorizer = CountVectorizer(ngram_range=(2, 2))
X_bigram = bigram_vectorizer.fit_transform(corpus)
print("Bigram 词汇表:", bigram_vectorizer.get_feature_names_out())
print("Bigram BoW 向量:", X_bigram.toarray())
# 也可以同时生成 unigram 和 bigram
unibi_vectorizer = CountVectorizer(ngram_range=(1, 2))
X_unibi = unibi_vectorizer.fit_transform(corpus)
print("\nUnigram+Bigram 词汇表:", unibi_vectorizer.get_feature_names_out())
print("Unigram+Bigram BoW 向量:", X_unibi.toarray())
代码解释:
CountVectorizer
的ngram_range
参数控制 N-gram 的范围。(1, 1)
是 unigram,(2, 2)
是 bigram,(1, 2)
是 unigram 和 bigram。- 生成的向量维度会随着 N 的增大和范围的扩大而急剧增加。
优点: 捕捉了局部语序信息,比 BoW/TF-IDF 更能反映一些短语结构。
缺点:
- 维度爆炸: N 越大,可能的 N-gram 组合越多,向量维度急剧膨胀。
- 数据稀疏: 很多 N-gram 可能只在语料库中出现一次或几次,甚至从未出现。
- 上下文有限: 仍然只能捕捉 N 个词范围内的局部依赖关系。
向量革命:分布式表示的兴起
基于计数的模型虽然直观,但它们的向量维度高、稀疏,且无法捕捉词语之间的语义关系(例如,“国王”和“女王”在语义上比“国王”和“苹果”更接近)。为了克服这些限制,分布式表示 (Distributed Representation),特别是词嵌入 (Word Embedding),应运而生。
核心思想:将每个词语映射到一个低维、稠密的实数向量。在这个向量空间中,语义或语法相似的词语,其对应的向量在空间中的距离也更近。
Word2Vec:让词语在向量空间中“活”起来
Word2Vec 是 Google 在 2013 年提出的里程碑式工作,它并非单一算法,而是包含两种主要的训练模型:
- CBOW (Continuous Bag-of-Words): 根据上下文词语来预测中心词语。
- Skip-gram: 根据中心词语来预测上下文词语。
核心思想:基于分布式假设 (Distributional Hypothesis) —— 出现在相似上下文中的词语,其含义也相似。通过训练神经网络(通常是浅层网络)在大量文本上执行预测任务,网络权重(或特定的隐藏层输出)就学习到了词语的向量表示(即词嵌入)。
Skip-gram 简化架构 (Mermaid):
Python 代码示例 (使用 Gensim):
from gensim.models import Word2Vec
from nltk.tokenize import word_tokenize
import nltk
# 确保已下载 nltk tokenizer 数据
try:
nltk.data.find('tokenizers/punkt')
except nltk.downloader.DownloadError:
nltk.download('punkt')
# 示例语料库 (实际应用需要大得多)
corpus_sentences = [
"the cat sat on the mat",
"the dog ate my homework",
"a quick brown fox jumps over the lazy dog"
]
# 分词
tokenized_corpus = [word_tokenize(sentence.lower()) for sentence in corpus_sentences]
print("分词结果:", tokenized_corpus)
# 训练 Word2Vec 模型 (Skip-gram 默认 sg=0 是 CBOW, sg=1 是 Skip-gram)
# vector_size: 嵌入向量维度
# window: 上下文窗口大小
# min_count: 忽略词频低于此值的词
# workers: 训练使用的线程数
model = Word2Vec(sentences=tokenized_corpus, vector_size=10, window=2, min_count=1, sg=1, workers=1)
# 获取词向量
vector_cat = model.wv['cat']
print("\n'cat' 的词向量 (前5维):", vector_cat[:5])
# 查找最相似的词
try:
similar_words = model.wv.most_similar('dog', topn=2)
print("\n与 'dog' 最相似的词:", similar_words)
except KeyError as e:
print(f"\n词 '{e}' 不在词汇表中。")
# 注意:由于语料库太小,相似性结果可能不具代表性
代码解释:
gensim
是一个强大的 NLP 库,提供了 Word2Vec 的实现。- 需要先将文本分词成句子列表,每个句子是词语列表。
Word2Vec
类用于训练模型,可以设置向量维度、上下文窗口大小、训练算法 (sg=1 为 Skip-gram) 等参数。model.wv['word']
获取词语的向量。model.wv.most_similar('word')
查找与给定词最相似的词。
优点:
- 捕捉语义: 能够学习到词语间的语义相似性(如 “king” - “man” + “woman” ≈ “queen”)。
- 稠密低维: 相较于 BoW/TF-IDF,向量维度大大降低且稠密,计算效率更高。
- 无监督学习: 可在大量无标签文本上进行训练。
缺点: - 无法处理 OOV (Out-of-Vocabulary): 对于训练语料中未出现的词,无法生成向量。
- 静态向量: 每个词只有一个固定的向量,无法区分多义词在不同语境下的含义(如 “bank” 可以指银行或河岸)。
- 依赖局部上下文: 主要基于局部窗口内的共现信息。
GloVe:融合全局统计与局部上下文
GloVe (Global Vectors for Word Representation) 由斯坦福大学提出,旨在结合基于计数的方法(如 N-gram)和基于预测的方法(如 Word2Vec)的优点。
核心思想:模型的训练目标是使两个词向量的点积,等于它们在语料库中共现频率的对数。它直接利用了全局的词-词共现矩阵 (Word-Word Co-occurrence Matrix)。
与 Word2Vec 的主要区别: Word2Vec 通过预测任务 间接 学习向量,而 GloVe 直接优化目标函数,使其拟合全局的共现统计信息。
优点:
- 利用全局统计: 训练速度相对较快,且在某些任务上表现优于 Word2Vec。
- 捕捉语义能力强。
缺点: - 与 Word2Vec 类似,无法处理 OOV 问题,且是静态向量。
- 构建全局共现矩阵需要较大内存。
(注:GloVe 通常使用预训练好的向量,其训练过程相对复杂,这里不展示代码,但使用方式与 Word2Vec 类似,加载预训练文件即可。)
FastText:拥抱形态,告别 OOV 困境
FastText 由 Facebook AI Research (FAIR) 提出,是对 Word2Vec (特别是 CBOW) 的扩展。
核心思想:将每个词视为字符 N-gram (Character N-grams) 的集合。例如,对于词 “apple” 和 N=3,其字符 N-gram 包括 “<ap”, “app”, “ppl”, “ple”, “le>” (通常会加上特殊边界符号 < 和 >)。一个词的向量最终是其所有字符 N-gram 向量的和。
例子:
词: “eating”
字符 3-grams: “<ea”, “eat”, “ati”, “tin”, “ing”, “ng>”
“eating” 的向量 = Vec(“<ea”) + Vec(“eat”) + … + Vec(“ng>”) + Vec(“eating”) (通常也会包含整个词本身)
优点:
- 处理 OOV 词: 对于未登录词,可以通过组合其字符 N-gram 的向量来“构造”出一个向量。
- 捕捉形态信息: 由于基于字符 N-gram,能更好地理解词根、前缀、后缀等形态结构,对形态丰富的语言(如德语、土耳其语)效果更好。例如,“eat”, “eats”, “eating” 的向量会比较相似。
缺点: - 模型更大: 需要存储所有字符 N-gram 的向量,模型文件通常比 Word2Vec/GloVe 大。
- 计算稍慢: 计算词向量时需要求和字符 N-gram 向量。
Python 代码示例 (使用 Gensim):
from gensim.models import FastText
from nltk.tokenize import word_tokenize
import nltk
# 确保 NLTK 数据已下载
try:
nltk.data.find('tokenizers/punkt')
except nltk.downloader.DownloadError:
nltk.download('punkt')
corpus_sentences = [
"the cat sat on the mat",
"the dog ate my homework",
"a quick brown fox jumps over the lazy dog"
]
tokenized_corpus = [word_tokenize(sentence.lower()) for sentence in corpus_sentences]
# 训练 FastText 模型
# 参数与 Word2Vec 类似,增加了 min_n, max_n 控制字符 n-gram 的长度范围
model_ft = FastText(sentences=tokenized_corpus, vector_size=10, window=2, min_count=1,
min_n=3, max_n=6, # 使用 3 到 6 个字符的 n-gram
workers=1)
# 获取已知词向量
vector_dog = model_ft.wv['dog']
print("'dog' 的向量 (前5维):", vector_dog[:5])
# 获取 OOV 词向量 (即使 'fluffycat' 不在训练集中)
vector_oov = model_ft.wv['fluffycat']
print("\nOOV 词 'fluffycat' 的向量 (前5维):", vector_oov[:5])
# 查找相似词
try:
similar_words_ft = model_ft.wv.most_similar('cat', topn=2)
print("\n与 'cat' 最相似的词 (FastText):", similar_words_ft)
except KeyError as e:
print(f"\n词 '{e}' 不在词汇表中。")
代码解释:
gensim
也提供了FastText
实现。- 增加了
min_n
和max_n
参数来控制字符 N-gram 的最小和最大长度。 - 即使 ‘fluffycat’ 不在训练语料中,
model_ft.wv['fluffycat']
也能成功返回一个向量,因为它是由 ‘flu’, ‘luf’, ‘uff’, ‘ffy’, ‘fyc’, ‘yca’, ‘cat’, '> ’ 等字符 N-gram 向量组合而成的。
深度语境时代:上下文感知的动态表示
Word2Vec、GloVe 和 FastText 生成的都是静态词向量,即一个词只有一个固定的向量表示,无法体现其在不同句子、不同语境下的细微差别。例如,“bank” 在 “river bank”(河岸)和 “investment bank”(投资银行)中的含义截然不同。
为了解决这个问题,研究者们转向了更强大的深度学习模型,特别是基于循环神经网络 (RNN) 和 Transformer 的模型,以生成上下文感知 (Context-aware) 的词嵌入。
ELMo:一词多“义”,语境决定向量
ELMo (Embeddings from Language Models) 是这一方向的早期重要突破。
核心思想:一个词的表示应该是整个输入语句的函数。ELMo 使用双向 LSTM (Bi-LSTM) 训练语言模型(预测句子中下一个或上一个词)。对于一个特定的词,其最终的嵌入向量是 LSTM 不同层的内部状态的加权和。
关键特点:
- 深度 (Deep): 向量来自于深度神经网络(多层 Bi-LSTM)的内部表示。
- 上下文感知 (Contextual): 同一个词在不同句子中,由于其上下文不同,通过 Bi-LSTM 计算得到的内部状态也不同,因此最终的嵌入向量也不同。
- 基于语言模型 (Language Model based): 通过在大规模语料上预训练语言模型来学习这些表示。
ELMo 高层架构 (Mermaid):
优点:
- 解决了多义词问题: 能根据上下文生成不同的词向量。
- 捕捉复杂句法和语义: Bi-LSTM 能捕捉长距离依赖关系。
- 预训练+微调范式: 在大规模数据上预训练,然后在下游任务上微调,效果显著。
缺点: - 计算量大: 相比静态词向量,生成 ELMo 向量需要运行庞大的 Bi-LSTM 模型。
- LSTM 局限: LSTM 对极长距离依赖的捕捉能力仍有限,且难以并行计算。
- 单向预训练: ELMo 的两个 LSTM 分别是独立训练的(一个前向,一个后向),并非真正的“联合”双向。
(注:ELMo 之后,基于 Transformer 架构的模型如 BERT、GPT、RoBERTa、ALBERT 等进一步发展了上下文感知嵌入,它们通常采用更强大的自注意力机制 (Self-Attention) 来捕捉上下文信息,并在各种 NLP 任务上取得了 SOTA (State-of-the-Art) 效果。这些模型是当前 NLP 领域的主流,但其复杂性超出了本篇入门介绍的范围。)
比较与选择:为你的任务挑选合适的“词汇表”
面对如此多的文本表示方法,如何为你的具体任务选择最合适的呢?以下是一些关键考虑因素和选择建议:
方法 | 核心思想 | 上下文处理 | OOV 处理 | 主要优点 | 主要缺点 | 典型场景 |
---|---|---|---|---|---|---|
BoW | 词频统计 | 忽略 | 差 | 简单、快速 | 忽略语序、语义,稀疏,维度高 | 简单文本分类、信息检索 baseline |
TF-IDF | 词频 + 逆文档频率 | 忽略 | 差 | 考虑词重要性,优于 BoW | 忽略语序、语义,稀疏,维度高 | 文本分类、信息检索、关键词提取 |
N-gram | 连续 N 个词的统计 | 局部语序 | 差 | 捕捉局部语序 | 维度爆炸,稀疏性更严重,上下文有限 | 语言模型(早期)、特征工程 |
Word2Vec | 基于上下文预测,分布式假设 | 局部窗口 (训练时) | 差 (无法处理) | 捕捉语义,稠密低维,无监督学习 | 静态向量,无法处理 OOV,忽略全局统计 | 词语相似度计算、下游任务的特征输入 |
GloVe | 拟合全局共现统计 | 局部窗口 (训练时) | 差 (无法处理) | 捕捉语义,利用全局统计,训练可能更快 | 静态向量,无法处理 OOV,需构建共现矩阵 | 词语相似度计算、下游任务的特征输入 |
FastText | 基于字符 N-gram | 局部窗口 (训练时) | 好 (通过子词) | 捕捉语义,处理 OOV,捕捉形态信息 | 静态向量,模型较大,计算稍慢 | OOV 问题严重、形态丰富语言、文本分类 |
ELMo | 基于 Bi-LSTM 的语言模型 | 动态全局上下文 | 好 (字符卷积) | 上下文感知,解决多义词,捕捉复杂依赖 | 计算量大,LSTM 局限,非真正联合双向 | 需要深度理解上下文的 NLP 任务(如问答、情感分析) |
BERT/GPT 等 | 基于 Transformer 的语言模型 | 动态全局上下文 | 好 (子词/BPE) | 强大的上下文感知,并行计算,SOTA 性能 | 计算量巨大,需要大量数据和算力进行预训练 | 几乎所有现代 NLP 任务 |
选择流程图 (Mermaid):
实践建议:
- 从简单的开始: 对于新任务,可以先尝试 TF-IDF 作为基线。
- 利用预训练向量: Word2Vec, GloVe, FastText 有很多高质量的预训练向量可用,可以直接加载使用,省去训练时间,通常效果不错。
- 考虑 OOV: 如果你的应用场景中可能出现很多新词,FastText 是个好选择。
- 追求最佳性能: 对于需要深度语义理解和上下文信息的复杂任务(如机器翻译、问答系统、复杂情感分析),ELMo 或更新的 Transformer 模型(BERT, RoBERTa, GPT 系列等)通常是首选,但需要相应的计算资源。
- 混合使用: 有时也可以将不同类型的表示(如 TF-IDF 和词嵌入)结合起来作为模型的输入特征。
总结与展望:表示方法的持续进化
我们回顾了从简单的词袋模型到复杂的上下文感知嵌入的文本表示演进之路。每种方法都在特定的历史时期解决了关键问题,并为后续的研究奠定了基础。
- BoW/TF-IDF/N-gram: 基于计数的经典方法,简单直观,但在语义和语序捕捉上存在明显短板。
- Word2Vec/GloVe/FastText: 分布式表示的革命,将词语映射到低维稠密向量空间,有效捕捉语义,FastText 还解决了 OOV 问题。
- ELMo 及后续模型: 开启了上下文感知的新时代,让词语的表示能够根据语境动态变化,极大地提升了 NLP 任务的性能上限。
文本表示方法的发展并未停止。当前,大规模预训练语言模型 (Large Language Models, LLMs) 如 GPT-4、LLaMA 等,不仅提供了更强大的上下文表示能力,甚至可以直接用于执行各种 NLP 任务(零样本或少样本学习)。此外,多模态表示(结合文本、图像、声音等信息)也成为新的研究热点。
理解这些文本表示方法的演进脉络和核心思想,不仅能帮助我们更好地选择和应用它们,更能为我们理解当前 AI 技术的浪潮提供坚实的基础。AI 高手之路,从理解“词汇表”开始,未完待续…