词袋模型(Bag of Words)—— 文本数值化的第一步
刚接触自然语言处理的朋友,第一个绕不开的概念大概率就是词袋模型(Bag of Words,简称 BoW)。不管你是要做垃圾短信识别、舆情分析,还是给搜索系统写个关键词匹配模块,第一件事永远是同一件——把文本变成机器能算的数值。而词袋模型,就是这门功课的入门第一课。
它做的事情其实非常朴素:把一段文本拆成一个个词,然后统计每个词出现的次数,用一个向量表示整段文本。听起来简单,但这是几乎所有高深NLP方法的地基。你后边学到的TF-IDF、LDA主题模型、甚至词向量模型,底层逻辑都在回应词袋模型留下的一些问题。这篇文章我会从模型原理、数学表达、代码实现一路拆到工程里的常见坑,争取让你看完之后不仅会用,还能理解它为什么这么设计。
这篇文章适合谁看?适合刚入门NLP的学生、做数据分析想给文本特征建模的开发者,以及任何想把非结构化文本变成结构化表格的从业者。不需要太多前置知识,会一点Python就行,核心概念我会从头讲透。
1. 词袋模型到底在做什么:先建立直觉
1.1 为什么叫“词袋”
先回到名字本身。“词袋”这个词翻译自Bag of Words,它强调一个核心设计思想:我们只关心文本里有哪些词、各自出现了多少次,完全不关心词的先后顺序。
打个比方。你把一篇文章里所有词一个个拆下来,扔进一个口袋里摇匀,口袋里有什么词、每个词有几个,这就是词袋模型看到的全部信息。原本句子结构里的主谓宾、修饰关系,它一概不管。“我喜欢你”和“你喜欢我”,在词袋模型看来是完全一样的,因为两个句子的词频统计结果完全相同。
这听起来是个很明显的缺陷,对吧?确实,它丢失了语序,也就丢了句法逻辑。但有意思的是,大量文本任务的基线效果,用这个看起来“很笨”的方法就已经不错了。比如垃圾邮件识别、情感倾向判断,很多时候核心词汇的出现频率就足够说明问题。“免费”“点击”“中奖”这些词频繁出现,这封邮件是垃圾邮件的概率就很高,语序几乎不影响这个判断。
所以词袋模型的核心价值在于:它用最少的计算代价,把“文本相似度”这个感知层面的概念,转化成了“向量距离”这个可以精确计算的数学概念。这个转化是后续所有文本处理的前提。
1.2 文本数值化的完整链路
你可能会问,为什么非得数值化?因为机器只会做数学运算。我们人类阅读时能自然理解“今天天气不错”这句话的含义,但计算机看到的是字符编码,它们不理解“天气”和“不错”之间有语义上的关联。要让计算机处理文本,必须先把文本映射到某个数学空间里。
整个文本数值化的链路大致是这样的:
- 原始文本获取:从数据库、日志、网页等来源拿到的未处理字符串。
- 文本清洗:去掉HTML标签、特殊符号、多余空白,统一大小写等。
- 分词:把连续的字符串切成一个个独立的词。英文按空格切就行,中文需要专门的分词工具。
- 构建词汇表:把所有文档中出现的不重复词收集起来,形成一个词表。
- 文本向量化:根据词汇表,把每一篇文档映射成一个数值向量。
词袋模型就是第4步和第5步的执行者。前几步的产物是“一堆词”,词袋模型负责把“一堆词”变成“一个向量”。后面所有分类、聚类、检索的操作,全都在这个向量上展开。
这个链路里有个很关键的思维转换:文档集合(语料库)决定词汇表,词汇表决定向量的含义。同一句话,放在不同语料库里,得到的向量是不同的。比如单独看“苹果”这个词,在没有上下文时你无法判断它是水果还是手机品牌,但如果你把包含“苹果”的所有句子放进语料库,模型会在统计意义上帮你抓住这个词在不同语境中的分布特征——当然,这是后话了,基础版词袋模型不处理一词多义,先有这个印象就行。
2. 手动算一遍词袋:原理、数学表达与代码实现
2.1 词袋模型的三个基本步骤
先别急着跑代码,我们用手算一遍,彻底搞懂里面发生了什么事。假设我有三句短文本作为语料库:
- 文档1:“我喜欢猫”
- 文档2:“我喜欢狗”
- 文档3:“猫和狗我都很喜欢”
第一步:分词。中文分词这里先用最简单的方式演示,假设我们已经把每句话拆成了词:
- 文档1:[“我”, “喜欢”, “猫”]
- 文档2:[“我”, “喜欢”, “狗”]
- 文档3:[“猫”, “和”, “狗”, “我”, “都”, “很”, “喜欢”]
第二步:构建词汇表。把语料库里所有出现过的不同词收集起来,排序后编号:
| 编号 | 词 | 编号 | 词 |
|---|---|---|---|
| 0 | 我 | 4 | 和 |
| 1 | 喜欢 | 5 | 都 |
| 2 | 猫 | 6 | 很 |
| 3 | 狗 |
词汇表大小 V = 7,这就是每个文档向量的维度。这一步是整个词袋模型的核心,因为一旦词汇表确定下来,整个语料库的向量空间也就确定了。
第三步:统计词频。对每一篇文档,统计词汇表中每个词在文档里出现多少次,按词汇表的顺序填进一个向量里。向量的第 i 个位置对应的就是词汇表里第 i 个词的词频。
文档1里:我出现1次、喜欢1次、猫1次,其余词0次,所以向量是[1, 1, 1, 0, 0, 0, 0]。
文档2:[1, 1, 0, 1, 0, 0, 0]。
文档3:我1次、喜欢1次、猫1次、狗1次、和1次、都1次、很1次,向量是[1, 1, 1, 1, 1, 1, 1]。
现在三句话变成了三个7维向量。你看,这个过程里完全不关心词的位置,“我”在句首还是句尾,“喜欢”后面跟的是“猫”还是“狗”,统统不影响向量结果。这就是前面说的“词袋”的含义。
有了向量之后,下游任务就简单了。比如要算文档1和文档2的相似度,直接算这两个向量的余弦相似度就行,数值越接近1代表越相似。文档1和文档2除了中间一位,其他位置都是相同的,所以它们相似度很高,这符合直觉——都是“我喜欢XX”句式。而文档1和文档3的相似度相对低一些,因为文档3包含了更多其他词。一个分类器接在这组向量后面,就能根据向量的几何位置判断文本类别了。
2.2 代码实现:用Python跑一个最小样例
理论清楚了,代码其实非常简单。这里我直接用Python手写一个最简版,不做任何封装,让你看清每一步。
import jieba corpus = [ "我喜欢猫", "我喜欢狗", "猫和狗我都很喜欢" ] # 1. 分词 tokenized_docs = [list(jieba.cut(doc)) for doc in corpus] print(tokenized_docs) # 输出:[['我', '喜欢', '猫'], ['我', '喜欢', '狗'], ['猫', '和', '狗', '我', '都', '很', '喜欢']] # 2. 构建词汇表 vocab = {} vocab_list = [] for doc in tokenized_docs: for word in doc: if word not in vocab: vocab[word] = len(vocab_list) vocab_list.append(word) print(vocab) # 输出:{'我': 0, '喜欢': 1, '猫': 2, '狗': 3, '和': 4, '都': 5, '很': 6} # 3. 词频向量化 import numpy as np def bow_vectorize(tokens, vocab): vec = np.zeros(len(vocab), dtype=np.int8) for token in tokens: if token in vocab: vec[vocab[token]] += 1 return vec vectors = [bow_vectorize(doc, vocab) for doc in tokenized_docs] for vec in vectors: print(vec) # 输出: # [1 1 1 0 0 0 0] # [1 1 0 1 0 0 0] # [1 1 1 1 1 1 1]整个过程不到20行代码。实际工程项目里一般不会自己造轮子,直接用scikit-learn的CountVectorizer就够了,底层实现加了并行化和稀疏矩阵优化,效率高得多:
from sklearn.feature_extraction.text import CountVectorizer corpus = [ "我喜欢猫", "我喜欢狗", "猫和狗我都很喜欢" ] vectorizer = CountVectorizer(tokenizer=lambda s: jieba.lcut(s)) X = vectorizer.fit_transform(corpus) # 词汇表:vectorizer.vocabulary_ print(vectorizer.vocabulary_) # 输出:{'我': 4, '喜欢': 3, '猫': 1, '狗': 2, '和': 0, '都': 5, '很': 6} print(X.toarray()) # 输出: # [[1 1 0 1 1 0 0] # [1 0 1 1 1 0 0] # [1 1 1 1 1 1 1]]注意CountVectorizer默认词汇表顺序是按字母排序的,而不是按出现顺序,所以和排在了第0位,这不要紧,顺序只是索引含义,不影响模型使用。
用CountVectorizer时有一个关键点:fit_transform在训练集上调用,之后对测试集只需要调用transform,不要再次fit。因为词汇表必须保持一致,如果测试集单独再建一个词汇表,向量维度就对不上了,模型也就没法用了。
2.3 词袋模型输出的真实形态:稀疏矩阵
上面演示用的是toarray()转换后的稠密矩阵,看起来直观。但当语料库变大,比如你有1万篇新闻稿,词汇表5万词,那么矩阵规模就是 10000 × 50000 = 5亿个位置。即使每个位置只存一个整数,也需要大量内存。
好在实际情况是,一篇1000字的文章可能只用到几百个不同词,也就是说矩阵里绝大多数位置是0。CountVectorizer返回的实际上是一个稀疏矩阵(scipy.sparse),它只存储非零位置的值,内存占用能降低几个数量级。这个我之前在实际项目里实测过:一个100万条评论的语料库,如果用稠密矩阵存,直接几十个GB内存不够用,但用稀疏矩阵只要几百MB。所以务必记住:拿到CountVectorizer的输出后,不要轻易调toarray(),尤其是大数据场景。后续模型训练(比如朴素贝叶斯、线性SVM)都直接支持稀疏矩阵输入。
3. 词袋模型的进阶优化:TF-IDF与n-gram
3.1 为什么纯词频不够用
基础版词袋模型有个问题:高频词会主导向量,但不一定代表文本特征。
比如做新闻分类时,“今天”“我们”“时间”这类词几乎每篇文章都有,它们词频高,但完全没法区分文章主题。真正有用的往往是那些出现次数适中、只在特定类别里出现的“特征词”。
为了压制通用高频词、凸显代表性词汇,常用的改进方式就是 TF-IDF(Term Frequency - Inverse Document Frequency,词频-逆文档频率)。
TF-IDF的直觉是:一个词在当前文档里出现次数多(TF高),但在整个语料库的很多文档中都出现(DF高),那它对当前文档的区分能力就弱,要给个惩罚;反之,如果它在当前文档中频繁出现,但其他文档很少出现,说明它是这篇文章的重要特征,要给个奖励。
数学表达式是:
TF-IDF(t, d) = TF(t, d) × IDF(t)
IDF(t) = log((N + 1) / (DF(t) + 1)) + 1
其中 N 是语料库文档总数,DF(t) 是包含词 t 的文档数。
注意这里有个 +1 的平滑项。传统公式是 log(N / DF),但直接用的话,如果某个词出现在所有文档里,IDF会变成 log(1) = 0,整个权重就归零了。加上平滑项后,这类词至少有一个基础权重,不会完全消失,这在工程上是更稳妥的处理方式。
代码实现很简单:
from sklearn.feature_extraction.text import TfidfVectorizer import jieba corpus = [ "我喜欢猫,猫很可爱", "我喜欢狗,狗很忠诚", "我喜欢花,花很香" ] vectorizer = TfidfVectorizer(tokenizer=lambda s: jieba.lcut(s)) X_tfidf = vectorizer.fit_transform(corpus) # 查看核心特征词的权重 feature_names = vectorizer.get_feature_names_out() for i in range(X_tfidf.shape[0]): print(f"文档{i+1}:") for j in range(X_tfidf.shape[1]): if X_tfidf[i, j] > 0: print(f" {feature_names[j]}: {X_tfidf[i, j]:.3f}")像“我”“喜欢”这类在每篇文档都出现的词,TF-IDF权重会被压得很低,而“猫”“狗”“花”这类只在特定文档出现的词,权重会明显凸起。做文本聚类或分类时,TF-IDF的特征选择性明显好于纯词频。
3.2 n-gram:给词袋模型补上“上下文”
前面说词袋模型忽略顺序,那有没有办法在保留词袋思路的前提下,捕捉一点局部语序信息?
有,这就是 n-gram 扩展。
n-gram 的做法是把相邻的n个词打包成一个整体,当作一个“词”来统计。比如 2-gram(bigram),“我喜欢猫”会被拆成“我喜欢”和“喜欢猫”两个组合。这样一来,模型的词汇表里既有单个词(unigram),也有相邻词组合。
在CountVectorizer里通过ngram_range参数控制:
vectorizer = CountVectorizer(ngram_range=(1, 2), tokenizer=lambda s: jieba.lcut(s)) X = vectorizer.fit_transform(["我喜欢猫", "我喜欢狗"]) print(vectorizer.vocabulary_) # 输出里会同时包含 '我', '喜欢', '猫', '狗', '我喜欢', '喜欢猫', '喜欢狗' 等词项加了bigram之后,模型能区分“我喜欢猫”和“猫喜欢我”——前者的bigram是“我喜欢/喜欢猫”,后者是“猫喜欢/喜欢我”,特征完全不同。这对情感分析这类任务帮助很大,比如“不好”和“不 好”拆成单个词时顺序信息丢失,但bigram“不好”能作为一个整体特征参与计算。
代价也很直接:词汇表规模暴涨。n-gram的组合数量是近似指数级的,词表从几千涨到几万甚至几十万很正常。工程上通常设置ngram_range=(1, 2)或(1, 3),很少超过3,因为超过3之后稀疏性太大,统计意义也不够了,还会拖垮后续模型训练速度。
3.3 参数调优心得:min_df、max_df和stop_words怎么设
实际项目里很少有人直接拿默认参数跑。我自己的习惯是先拿一套小规模数据跑一遍,观察词汇表的构成,再做这几件事:
第一,设置min_df和max_df。min_df指的是词至少在多少篇文档里出现过才保留,默认是1,也就是说只要在1篇文档里出现就进词汇表。这在语料大时会导致大量只在某一篇文档里出现的“噪声词”进入词表,让维度爆炸。经验值一般设2~5。max_df指的是词在超过多少比例的文档里出现就舍弃,默认1.0表示不限制。对于通用文本,我常把max_df设在0.8~0.95之间,去掉那些几乎每篇都出现的高频虚词。这两个参数是对词汇表规模的第一道闸门。
第二,停用词表要想清楚再加。英文有成熟的停用词表如nltk自带的stopwords,但中文的停用词质量参差不齐。我会在项目初期尽量少用停用词,而是靠max_df做高频词过滤。因为有些词比如“不”“但是”,在情感分析里是有实际意义的转折信号,一刀切掉会伤特征。停用词表更适合在明确知道场景的前提下手工维护一份针对性的列表。
第三,sublinear_tf开关值得一试。TfidfVectorizer里有个参数sublinear_tf=True,它计算时用的是1 + log(TF)而不是原始TF,本质是对词频做了一次压缩,降低“一个词反复出现30次”和“出现3次”之间的绝对差异。这个参数在长文档场景下非常有效,因为长文档的词频天然偏高,不做压缩的话频率高的词会过度主导向量。
4. 工程落地:文本分类项目的完整流程
4.1 从原始文本到模型输入
前面的原理和代码都是切片式的,接下来我串一个完整的实战流程——用词袋模型做一个简单的文本分类器,把“体育”“科技”“娱乐”三类新闻区分开。这个流程你在很多项目里都能复用。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import jieba # 假设已经拿到了文本和标签 texts = [...] # 新闻文本列表 labels = [...] # 对应标签列表 # 定义预处理函数 def tokenize(text): return jieba.lcut(text) # 创建流水线:TF-IDF向量化 -> 朴素贝叶斯分类 pipeline = Pipeline([ ('vect', TfidfVectorizer( tokenizer=tokenize, ngram_range=(1, 2), min_df=2, max_df=0.9, sublinear_tf=True )), ('clf', MultinomialNB()) ]) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( texts, labels, test_size=0.2, random_state=42 ) # 训练与评估 pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(classification_report(y_test, y_pred))这里我选择的分类器是朴素贝叶斯。为什么?因为MultinomialNB对稀疏高维特征非常友好,训练极快,而且它假设特征之间条件独立——这个假设在词袋模型下虽然不真实(“人工智能”和“算法”明显相关),但实践中它依然能取得不错的基线效果。如果你换用SVM或逻辑回归,也能得到不错的结果,但要先做特征缩放或选择适合稀疏矩阵的求解器。
完整流程里有一个容易被忽略的细节:向量化器只能先把分词后的原始文本传进去。我见过不少新手在这个环节犯迷糊,以为要先手工构造词频矩阵再丢给分类器,其实Pipeline已经把向量化和训练打包了,fit时它会自动完成fit_transform变换,predict时自动transform测试数据,不用自己手动做。
4.2 评估模型时除了准确率还要看什么
文本分类的评估经常陷入只盯着准确率(Accuracy)的误区。在类别不平衡时,准确率会骗人。比如100条样本里95条是体育新闻、5条是科技新闻,模型全预测成体育也能拿到95%的准确率,但这显然不是你想要的模型。
这时候应该看precision(精确率)、recall(召回率)和F1-score。精确率关心“预测为A类的样本里有多少真的是A类”,召回率关心“所有A类样本里模型抓回来了多少”。一个只把“足球”“篮球”相关文本判为体育类的模型,精确率可能很高但召回率很低,因为大量没有这些关键词的体育新闻被漏掉了。
在词袋模型场景下,我发现一个挺实用的诊断方法:把模型分错和分对的样本拉出来对比,看它们的特征词分布。比如有个体育类新闻被错判成了科技类,把它的预测概率最高的特征词打出来,看看是不是混入了大量“计算机”“芯片”这类词汇。如果确实如此,大概率是训练数据里这两类的界限不够清晰,或者停用词过滤把真正重要的区分词给滤掉了。通过这种方式能很快定位是特征工程问题还是数据标注问题。
4.3 中文场景的分词注意点
中文文本处理绕不开分词这一步。词袋模型的粒度完全取决于分词器的输出:如果分词器把“机器学习”切成一个整词,模型就会把它当作一个特征;如果切成“机器”和“学习”两个词,模型就只知道这两个单独词的出现频率,丢失了“机器学习”这个整体概念。
目前主流的开源分词工具有几个选择:jieba简单易用,但词表相对固定,新词发现能力一般;HanLP和LTP提供更丰富的词法分析能力;BERT等预训练模型也自带分词器,不过那是另一个技术路线了。
我的经验是,刚起步的项目直接用jieba就够了,但要注意两件事。第一,自定义词典:对于特定领域(比如医疗、法律、游戏),先准备一份该领域的术语表导入词典,能显著提升分词质量。第二,分词一致性:训练集和预测集必须用同一套分词配置,不能训练用jieba预测换了个工具,否则特征空间直接对不上。
5. 词袋模型的局限性:你要知道它的边界在哪里
5.1 高维稀疏与计算压力
词袋模型向量的维度等于词汇表大小。语料稍微一多,几万维是家常便饭。虽然稀疏矩阵缓解了内存问题,但几万维的特征依然会拖慢下游模型的训练。尤其是深度模型(图神经网络等)对输入维度非常敏感,直接接几万维的词袋特征,第一批参数就已经是一个巨大的矩阵了。
处理方式有几类。一是做特征筛选,上面提到的min_df、max_df就是最简单的维度控制;二是用HashingVectorizer做哈希映射,把词直接哈希到固定维度的向量,比如2^18维,彻底避免维护词汇表;三是降维,比如TruncatedSVD(LSA)可以把词袋向量压到几百维,同时在一定程度上缓解稀疏性问题。
5.2 语义鸿沟:同义词和多义词
词袋模型把每个词当成独立符号,词与词之间没有任何关联。这意味着“汽车”和“轿车”虽然语义相近,但在向量里它们是两个完全正交的维度,模型无法“理解”这两个词之间存在相似性,因为它们在特征空间里没有任何天然的相似关系。反过来,“苹果”作为水果名和作为公司名,在词袋模型里被看成同一个词,完全无法区分语境差异。
这就是常说的语义鸿沟问题。词袋模型只能捕捉“文档用了什么词”,但捕捉不到“词在说什么意思”。要解决这个问题,就需要词嵌入(Word Embedding)这类思路了,比如Word2Vec通过上下文学习词的分布式表示,让语义相近的词在向量空间里距离较近。不过那是词袋模型的下一站,不是本文的重点。
5.3 什么时候该用词袋,什么时候该换方案
讲了这么多局限,不代表词袋模型就该被丢弃。它是典型的“简单有效”方案:如果你手头是几万到几十万量级的数据,任务本身对语义理解要求不高(比如垃圾内容过滤、关键词匹配、文本检索),词袋模型完全可以作为核心方案,而且推理速度极快、可解释性强。你可以直观地告诉业务方:“我们这个模型把‘发票’‘代开发票’识别为高危词,所以这条消息被拦下了。”这种可解释性在金融风控、内容审核等场景里很重要。
反过来,如果你的任务要求理解反讽、理解长距离依赖、回答自由文本问题,那词袋模型就明显不够了,需要上BERT类预训练模型或者大语言模型路线。
我个人的建议是:任何文本项目,先做一版词袋模型当baseline,再考虑要不要上更复杂的方案。这不仅仅是图省事,它给你提供了一把刻度尺——如果词袋模型都能跑到0.8的F1,而你上一套BERT只跑到0.81,那说明当前瓶颈不在特征能力上,可能数据标注质量才是主要问题。这个判断会帮你节约大量时间和成本。
6. 词袋模型实践中的常见错误与排查技巧
6.1 训练集和测试集特征不对齐
这是词袋模型项目里最隐蔽也最致命的错误。
场景是这样的:你拿训练集fit了CountVectorizer,然后在测试集上跑的时候顺手又fit了一次,或者说测试集里出现了一个训练集词汇表里没有的新词,然后你手动把向量对齐到训练词汇表时漏掉了一部分。结果就是训练集向量是5000维,测试集向量是4800维,模型predict时报维度不匹配。更隐蔽的情况是维度没报错,但实际上两个向量各说各话——比如训练集词汇表第100位是“猫”,测试集第100位却是“狗”,模型算出来的结果全是错的。
排查方法其实很简单:做完向量化之后,手动打印一下训练集和测试集的特征维度,确保完全一致;另外CountVectorizer和TfidfVectorizer都提供了vocabulary_和get_feature_names_out()方法,你可以把两个集合的特征名拉出来比对一下,看是否对得上。
6.2 新词和未登录词怎么处理
测试集里出现训练集没见过的词非常正常。默认情况下,CountVectorizer会直接忽略这些词,也就是说它们对向量没有任何贡献。这在词袋场景下其实问题不大,因为新词往往出现频率很低,对分类决策影响微弱。
但有一种情况要小心:如果训练语料太小,大量测试集常用词都没在训练中出现过,导致测试向量极度稀疏,模型的判别能力会大幅下降。这时候可以考虑补充训练语料,或者用HashingVectorizer绕开词汇表限制。HashingVectorizer不需要维护词汇表,任何词都可以哈希映射到特征空间的某个位置,不存在“未登录词”的概念。缺点是它不支持逆向映射(无法告诉你哪个特征对应哪个词),这对于需要解释模型结果的应用是个硬伤。
6.3 词频权重的极端情况
我踩过一个具体的坑:做舆情分析时,有个类别是事故通报类,文本里“死亡”这个词出现了几百次,因为每篇通报都在重复伤亡数字。词袋向量里这个词的频率高到把其他特征全部淹没,模型的预测结果几乎就变成了“看到‘死亡’就分到事故类”。这其实是数据本身分布不均衡导致的,单靠词袋模型没法根治。
解决思路有两个:一是用TF-IDF替代纯词频,因为“死亡”在很多文档里都有出现,IDF会压低它的权重;二是对文本做段落截断或长度限制,让每篇文档的词频分布更均匀。如果你的任务里存在某些词出现频率呈幂律分布的情况,这两种方法都值得优先尝试。
7. 词袋模型之外:下一步往哪儿走
7.1 从词袋到词嵌入的演进
词袋模型和词嵌入都做“把文本变成向量”这件事,但底层逻辑完全不同。词袋模型是稀疏的、基于统计的,词与词相互独立;词嵌入是稠密的、基于学习的,语义相近的词在向量空间里距离更近。从词袋到Word2Vec、GloVe再到Transformer的演进,本质上就是把“统计”升级为“语义理解”。
如果你已经吃透了词袋模型,再往上学这些会轻松得多。因为你会发现它们解决的都是词袋模型留下的问题:词袋忽略语序,所以有了n-gram和LSTM;词袋无法表达语义相似度,所以有了Word2Vec;n-gram只能捕捉局部窗口,所以有了Transformer。每个新方法都是对前一个方法某个缺点的定向修正。
7.2 词袋思想在现代系统中的影响
有意思的是,词袋模型没有被完全淘汰。在大语言模型时代,你依然能看到它的影子:文本检索领域经典的BM25算法,本质上就是带权重的词袋打分;推荐系统的召回阶段,有些方案用的还是TF-IDF计算文本相似度;传统反作弊规则系统里,“某个词在一段时间内出现频率暴涨”这种统计特征,用的还是词袋思想。
我自己的体会是,这个模型行业地位有点像“矩阵中的初等变换”——所有人都在学,日常计算中却不一定直接用它。但你越熟悉它,越能在遇到新问题时,敏锐判断它属于“统计匹配型”还是“语义理解型”问题,并顺手给出一个预期合理的方案。
这篇文章讲到这里,词袋模型从原理、代码、调参到项目落地和边界问题就都覆盖了。如果你正在做文本相关的项目,不妨从今天起先搭一个词袋baseline跑通全流程,把上面的参数一个个调着试试。动手改一遍比读十遍文章都管用。