词袋模型BoW与TF-IDF:文本向量化的Python实战

发布时间:2026/10/17 12:47:41
词袋模型BoW与TF-IDF:文本向量化的Python实战 搞 NLP 的人绕不开文本向量化而词袋模型Bag of Words简称 BoW和 TF-IDF 就是两条最基础的必经之路。标题里“时序和文本”这几个字可能会让一部分人误解成时间序列那套东西其实这里说的更准确的理解是我们处理文本时天然会考虑词出现的先后顺序但 BoW 和 TF-IDF 恰恰反着来它们把顺序彻底丢掉只保留“词在不在、出现了几次”这一层信息。我见过不少初学者在这里卡壳一边背公式一边困惑为什么这么简单的操作会有这么大的影响力。这篇文章就把 BoW 和 TF-IDF 掰开揉碎讲清楚它们解决的是什么问题、计算过程长什么样、Python 里怎么落地以及实际项目里踩过的坑。适合刚入门 NLP、准备做文本分类或关键词提取的读者也适合复习基础的朋友直接从代码和参数层面找答案。1. 先看清问题文本数据为什么不能直接喂给模型1.1 模型的输入约束与文本的天然形态机器学习模型尤其是传统的线性模型和大部分深度学习模型要求的输入都是一个形状规整的数值矩阵。你给它一个字符串它没法像人一样直接“读懂”语义它只认识数字。文本是一条变长的字符序列短的可能只有几个词长的能到几万字这种天然形态没法直接进模型必须先做一步转换把文本映射成固定长度的数值向量。拿现实生活类比一下。你让一个从来没见过汉字的外国人判断两句话是不是同一个意思他没办法从字形上直接判断你得先给他一个字典把每个词翻译成他能理解的编号或者符号组合。模型也是一样它需要一份“词表”Vocabulary然后按照词表把每句话编码成一个向量。这里有个很关键的设计选择编码时到底要不要保留词序保留词序意味着“我打你”和“你打我”要编码成完全不同的向量不保留就是只要这两个词出现过向量就一样只是顺序颠倒不影响表示。这个选择直接决定了模型能捕捉到什么信息。保留顺序的编码方式已经属于序列模型或者 n-gram 的范畴而 BoW 和 TF-IDF 选择了完全忽略顺序的策略。看起来这损失了信息但恰好换来了两个好处一个是向量长度固定每篇文档都能映射成词表长度的一维向量另一个是计算极其简单底层只需要做统计计数这套逻辑在当年算力有限的年代非常讨巧即便放到现在它对很多任务来说依然是性价比极高的基线方法。1.2 “词袋”这两个字到底意味着什么把文本想象成一颗颗水果扔进一个口袋里然后你晃一晃袋子再倒出来最终只关心“袋子里有苹果、香蕉、橙子各多少个”完全不关心它们进去的先后顺序这就是“词袋”的核心隐喻。每一篇文档就是一个袋子装满了一个个词模型看到的是一个鼓鼓囊囊的袋子里面只有词和词的频次没有排列顺序。所以真相是BoW 和 TF-IDF 本质上都处理的是无序词集合。标题里提到的“时序”在它们面前没有生存空间。如果你在做任务之前反复纠结这句话的顺序很重要比如要做情感分析、判断“我不喜欢”和“喜欢我不”那 BoW 和 TF-IDF 就不该是第一选择你得考虑把局部词序纳入模型的 n-gram 特征或者直接上序列模型。但如果任务本身是“判断这篇新闻属于哪个栏目”“找出文档中的关键词”顺序影响其实没那么致命词袋类的向量就完全够用。这个认知特别重要因为它能帮你在项目启动阶段做对第一道选择题不是所有文本任务都需要时序建模很多任务用 BoW 就足够了盲目上复杂模型反而是浪费资源。2. 词袋模型 BoW从词频计数到向量空间2.1 手工实现一个最简 BoW 流程先把 BoW 的完整流程拆开一共三步构建词表、对每篇文档统计词频、把统计结果拼成向量。词表就是所有文档中出现过的去重词的集合。假设有两篇文档文档一“今天天气不错适合出门跑步。”文档二“跑步回来喝了杯咖啡天气很舒服。”中文本身没有空格分词这里为了讲原理我先按词划分。去掉标点后词表就是“今天、天气、不错、适合、出门、跑步、回来、喝了、杯、咖啡、很、舒服”这 12 个词。然后对每篇文档统计每个词的次数。文档一里“今天”出现 1 次“跑步”出现 1 次没出现过的词记 0。最终它的向量就是 [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]文档二对应是 [0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]。就这样两篇变长的文本被压成了两个同样长度为 12 的数值向量。这个向量的每一个位置都对应词表里的一个词位置上的数字就是该词在这篇文档里的出现次数也就是词频Term Frequency简称 TF。现在你可以把这两行向量丢进分类器、做相似度计算或者聚类因为它们的形态已经符合模型输入要求。这里注意到一个“时序”被抹掉的直观表现把“今天天气不错”和“天气今天不错”写成两篇BoW 得到的向量完全一致。这在某些场景下是缺陷在某些场景下反而是鲁棒性比如垃圾邮件检测里“中奖 恭喜”“恭喜 中奖”这两句话虽然语序不同但表达意图非常接近词袋模型恰好能做到一定程度的泛化。2.2 用 Scikit-learn 实现 CountVectorizer手工统计只能帮你建立直觉真正干活的时候没人手写这个步骤。Scikit-learn 里的 CountVectorizer 是 BoW 的标准实现名字里的 Count 指的就是做词频计数“Vectorizer”表示语料到向量的转换器。用法非常简单几行代码就能跑通。from sklearn.feature_extraction.text import CountVectorizer docs [ 今天天气不错 适合出门跑步, 跑步回来喝了杯咖啡 天气很舒服 ] vectorizer CountVectorizer() bow_matrix vectorizer.fit_transform(docs) print(vectorizer.get_feature_names_out()) print(bow_matrix.toarray())运行结果会打印出词表顺序和两行词频向量。默认情况下CountVectorizer 采用单个词作为特征也就是 unigram词表里的词不排序按内部哈希或者词典序处理。输出的是一个稀疏矩阵用 toarray() 转成密集数组方便查看但真实项目里千万别转成密集矩阵因为词表常常是几万维全转成密集矩阵内存直接爆炸。CountVectorizer 有几个参数直接影响向量效果。token_pattern 默认匹配的是字母数字字符中文场景下如果用默认配置可能把整句切成一个又一个连续片段实际使用中通常需要先分词把分词结果用空格拼起来再喂给它。ngram_range 控制的是特征粒度默认 (1, 1) 表示只取单个词如果设成 (1, 2) 就会额外生成相邻连着的两个词作为特征比如“天气”“天气不错”都会进词表这相当于给无序的 BoW 模型补了一丁点局部顺序信息。min_df 和 max_df 负责过滤低频和高频词这两个参数能大幅影响词表大小后面实操部分我会再展开讲。2.3 词袋模型的硬伤频率不代表重要性BoW 用词频来度量一个词在文档中的分量这在朴素的视角下说得通但放到真实文本里立刻露馅。中文里“的”“了”“是”“在”这类功能词几乎每篇文档都有而且出现频率往往比实词还高。比如“这个方案的问题在于设备成本太高的部分原因在于……”一句话里两个“在于”都占着词频指标但对理解文章主题几乎没有帮助。BoW 给所有词一视同仁地计数结果就是高频的虚词霸占了向量里最大的权重真正的主题词反而被淹没。更麻烦的是词频高不见得就代表这篇文档特有。假设你现在做新闻分类体育新闻里“比赛”出现次数很多财经新闻里“市场”出现很多。可如果语料里大部分文档都出现“记者”“报道”这类词它们的词频一样不低却说不清新闻属于哪个类别。BoW 把这种“到处都在出现”的词和“只在特定类别中出现”的词同等对待导致向量丢失了区分力。这就是 TF-IDF 出来要解决的问题。它本质上是在 BoW 词频统计的基础上给每个词额外乘上一个“权重系数”用来拉低那些四处乱跑的高频词的得分同时抬高那些只在少数文档里出现的词的得分。所以理解 TF-IDF 之前必须先把 BoW 的缺陷想清楚否则你会觉得 TF-IDF 的公式很莫名其妙为什么要对词频取对数为什么还要算整个语料里的文档频率。3. TF-IDF给每个词重新称重3.1 TF 和 IDF 各自的职责划分TF-IDF 是 Term Frequency - Inverse Document Frequency 的缩写翻译过来就是“词频-逆文档频率”。这个名字直接把两个核心组件写清楚了。TF 是词频前面已经出现过它的职责是衡量这个词在本文档里有多重要。这个指标的直觉很朴素一篇文章反复提到“锂电池”说明这篇文章大概率在讲锂电池相关的话题。IDF 是逆文档频率它的职责是衡量这个词在整个语料里的稀缺程度。如果一个词在绝大多数文档里都出现比如“我们”“可以”它的区分度就很低逆文档频率就会小反之如果一个词只在极少数文档里出现比如“析锂”“正极材料”说明它具有很强的主题指向性IDF 就会大。TF 管“这篇文章里谁更显眼”IDF 管“整个语料里谁更稀有”两者相乘就得到一个既考虑词频又考虑稀缺性的综合权重。这里有个非常精妙的地方IDF 是在语料级别计算而不是在单篇文档里计算。这意味着同一个词在不同文档里的 TF 可能不同但它的 IDF 是固定的。TF-IDF 是一个把“局部重要性”和“全局区分度”结合起来的权重方案比单纯的词频多了一个全局视角所以它比 BoW 更能反映文档的主题差异。3.2 TF-IDF 到底怎么计算一步一步手动算一遍公式层面TF 最简单粗暴的版本就是词在文档中的出现次数但也有不少实现会用词频除以文档总词数做归一化。IDF 的常见公式是IDF(t) log( (总文档数 N) / (包含词 t 的文档数 df(t)) )这里 log 的底数通常取自然常数 e不过取对数底数只会让所有词的 IDF 等比例缩放不影响相对大小。实际工程里为了防止 df(t) 等于 0 造成除零错误会在分母上加 1很多时候分子上也会加 1这种平滑操作叫做平滑 IDF。Sklearn 默认走的是一种变体公式是 log( (1 N) / (1 df(t)) ) 1这么做的目的是避免一个极高 IDF 的词被赋予过于夸张的权重同时确保从未出现的词不会直接得到一个 0 分。手动跑一遍计算能帮你彻底理解。假设语料有三篇文档词表和文档频率如表所示词文档1出现次数文档2出现次数文档3出现次数包含该词的文档数 df天气2102跑步1032咖啡0201舒服1001全体文档数 N 3。先算 IDF。“天气”的 IDF 是 log((1 3) / (1 2)) 1 log(4 / 3) 1约等于 0.2877 1 1.2877。“咖啡”的计算则是 log((1 3) / (1 1)) 1 log(2) 1约等于 1.693。可以看到“咖啡”因为只出现在一篇文档里IDF 比“天气”高出一截。再算 TF-IDF。文档1里“天气”出现 2 次TF-IDF 得分就是 2 × 1.2877 ≈ 2.5754。文档1里“咖啡”没出现得分直接是 0。这里“天气”虽然词频高但它同时出现在文档1和文档2里“稀缺性”被拉低所以总权重没有想象中那么夸张。真正能拿到高分的往往是那些在当前文档中反复出现、但整体语料里很少露面的词。Sklearn 的 TfidfVectorizer 在计算之后还会做一步 L2 归一化也就是把每个文档的向量除以它的长度让整个向量的欧几里得范数变成 1。这一步主要是为了后续计算余弦相似度时更方便也消除了文档长度对向量数值的影响。所以你拿到的矩阵不是一个简简单单的 TF × IDF 结果而是归一化之后的版本这个细节很多人不看源码都不知道。3.3 从 CountVectorizer 到 TfidfVectorizer只差这一步理解了公式之后你会发现 TfidfVectorizer 本质上就是 CountVectorizer 的升级版先用词频统计得到稀疏矩阵再对矩阵里的每个数值乘以全局 IDF 权重最后做 L2 归一化。Sklearn 甚至提供了一个参数 use_idf设为 False 之后它输出的就是纯词频加归一化相当于一个带缩放的 BoW。很多刚入门的同学会把 CountVectorizer 和 TfidfVectorizer 当成两个完全不同的工具其实不用器记清楚了会发现它们底层共享同一套“先分词、再计数”的逻辑。TfidfVectorizer 内部有一个 TfidfTransformer 转换器它的输入正是 CountVectorizer 输出的计数矩阵。甚至可以直接这样理解TfidfVectorizer CountVectorizer TfidfTransformer。4. 实操用 Python 快速构建一个文本向量化 Demo4.1 完整代码与参数配置中文场景下你直接调 TfidfVectorizer 默认配置基本没法用因为它内部按空格分词对中文一整段没有空格的文本只能当做一个长字符串处理。所以中文文本向量化的标准流程是多一步分词通常先用 jieba 把句子切成词再用空格重新拼接最后才交给 TfidfVectorizer。我用一个完整的小 demo 演示整个流程。import jieba from sklearn.feature_extraction.text import TfidfVectorizer docs [ 今天天气不错 适合出门跑步, 跑步回来喝了杯咖啡 天气很舒服, 咖啡提神醒脑 我每天都喝 ] def chinese_cut(text): return .join(jieba.cut(text)) cut_docs [chinese_cut(doc) for doc in docs] vectorizer TfidfVectorizer( ngram_range(1, 2), min_df1, max_df0.8, sublinear_tfTrue, use_idfTrue ) tfidf_matrix vectorizer.fit_transform(cut_docs) print(vectorizer.get_feature_names_out()) print(tfidf_matrix.toarray())这里几个参数各有讲究。ngram_range(1,2) 会把单个词和相邻两个词的组合都当成特征比如“天气”“天气不错”“跑步”都会进词表这样能在保持词袋高效率的同时捕捉一些局部短语信息。min_df1 和 max_df0.8 的组合是过滤用的min_df 小于 1 表示最少出现在 1 篇文档里max_df0.8 表示超过 80% 文档都包含的词会被剔除这能自动干掉类似“的”“了”“是”这些可能到处出现的高频词前提是你的语料足够有代表性。sublinear_tfTrue 表示对词频改用 1 log(tf) 的形式目的是减弱词频线性增长带来的影响避免某篇文档里同一个词反复出现导致它的权重无限放大。代码跑完打印出特征词表和每篇文档的 TF-IDF 向量。你会发现“天气”和“咖啡”这些词的权重分布比较符合直觉高频但是常见的词被压下来了。稀疏矩阵同样不要直接 toarray()这里只是为了演示看数值。4.2 向量化之后的常见应用方向拿到 TF-IDF 向量之后最常见的三个应用方向是文本分类、文档相似度和关键词提取。文本分类的套路很标准用 TfidfVectorizer 对训练语料做 fit_transform拿到一个大的稀疏矩阵把这个矩阵直接当作特征输入逻辑回归、SVM 这类分类器。TF-IDF 向量的好处是特征维度虽然高但极其稀疏许多线性模型的训练效率依然很高而且不需要额外做特征筛选就能获得还不错的基线效果。我在实际项目里发现用 TF-IDF 加逻辑回归做新闻分类在中小规模语料上经常能到达一个很强的 baseline甚至能超过一些没有经过精细调参的深度学习模型。文档相似度计算一般配合余弦相似度使用。因为两个向量的长度不影响夹角余弦相似度天然适合高维稀疏向量的比较。写几行代码就能把两两文档之间的相似度算出来from sklearn.metrics.pairwise import cosine_similarity sim_matrix cosine_similarity(tfidf_matrix) print(sim_matrix)输出的是一个 3x3 的相似度矩阵第 i 行第 j 列表示文档 i 和文档 j 的相似度。这个技术在文本去重、问题检索、文章推荐里面都是基础操作。关键词提取的逻辑也很直观一篇文档的向量里每个维度对应一个词维度的数值就是 TF-IDF 权重按权重从大到小排序取前 N 个词作为文档关键词。这种方式不需要额外训练模型跑起来速度非常快在标题生成、标签推荐这类场景里可以做快速预处理。4.3 实操中的三个坑第一个坑是中文没有预先分词。很多人拿着一篇中文文章直接丢进 TfidfVectorizer结果除了标点符号之外什么都没有提取出来。分词这一步不能省而且分词器质量直接影响效果比如“不知道”切错成“不知”“道”整个特征就废掉了。实际项目中还可以结合自定义词典补充领域术语。第二个坑是词表没控制好维度爆炸。默认参数下几千篇文档就能产生几十万维的特征如果再开 ngram_range(1,3)词表轻松上百万。这个维度后续喂给模型会造成训练时间变长、内存挤爆、模型泛化变差。解决办法是从上到下控制三个参数max_df 干掉遍布语料的停用虚词min_df 去掉只出现一两次的噪声词最后把 max_features 设一个上限比如 50000 或者 100000只保留词频最高的部分。这样做不仅降维还能提升模型质量。第三个坑是拿 fit_transform 的结果直接用在测试集上而不是用训练好的 vectorizer 去 transform。正确做法是训练集上 fit_transform测试集上只调用 transform。这样保证词表不依赖测试数据的分布也让评估结果更接近线上表现。很多人因为手滑写错这部分得到的评估分数虚高上线后却表现大幅下跌。5. 真实项目里的经验与建议5.1 什么时候用 BoW什么时候用 TF-IDF如果只做简单的词频统计或者任务本质上就是计数相关的比如某些规则系统判断“这份文档里是否出现某个词”BoW 够用。但绝大部分文本分类和检索任务直接用 TF-IDF 会更稳因为它的全局加权天然具备停用词抑制能力不需要依赖一个额外的停用词表。我发现一个规律短文本场景下 BoW 和 TF-IDF 的差距没那么大。比如判断一句商品评论是好评还是差评文本很短词频就是 0 或 1IDF 的作用有限用词袋特征配上分类器已经能拿到不错的结果。长文本场景下差距就很明显因为文档里词的分布更加极端高频词和低频词的区分度需要 IDF 来校准。例如识别技术文档的主题单纯词频容易把“系统”“问题”“方案”这些词推上高位加上 IDF 才能把“微服务”“容器化”“分布式事务”这些更具体的主题词顶上来。5.2 参数调试的优先级参数调优的顺序我先看 min_df 和 max_df再看是否要开 ngram_range最后才考虑调 max_features。原因是前两个参数直接决定了词表质量虚词和生僻词先去掉特征的基本盘就稳了。ngram_range 是否需要开启取决于任务是否看中短语信息。分类新闻标题这种短文本开 bigram 常常有帮助分类长文章单字词和双字词已经能表达主题开 bigram 反而让维度翻好几倍。max_df 的设置要参考语料本身的分布。语料里全是同一领域的文档时max_df 可以设高一点比如 0.9语料来源很杂比如要处理多个完全不同的话题max_df 设成 0.5 可能更合适。这里没有银子弹得跑几次看效果。5.3 给新手的一点心得我见过很多入门的人一头扎进深度学习的序列模型把 LSTM、Transformer 当成标配结果连 TF-IDF 是什么都讲不清楚。实际上无论是学术论文的 baseline还是工业界的快速原型BoW 和 TF-IDF 都还占据着重要的位置。它们计算快、解释性强而且能跟很多复杂模型混用比如把 TF-IDF 向量和 embedding 特征拼在一起往往能同时提升模型的覆盖率和精准度。我自己的经验是做任何文本项目第一步都先拿 TF-IDF 跑一个最简单的基线把结果记下来。后面不管上多复杂的模型拿这个基线作为参照物来判断复杂模型是否真的带来了增益。这样做的好处是不至于一上来就把周末耗在调参上结果连简单方案能到什么水平都没摸清。最后提一个小技巧当你处理的数据里包含大量专有名词和领域术语时记得先整理一份领域词典喂给分词器再做 TF-IDF。这个动作看起来不起眼却常常能带来比调参还要明显的效果提升。分词切得准词袋里的词才靠谱后面的所有事情才有意义。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询