做文本处理或者机器学习相关项目时,只要涉及中文、英文的自然语言数据,十有八九绕不开“文本向量化”这一步。模型不认字,只认数字,怎么把一句话、一篇文章变成一串能塞进算法的数字,独热编码(One-Hot Encoding)和词袋模型(Bag of Words,简称 BoW)就是最基础、也最常被拿出来对比的两套方案。这篇文章我不打算写教科书式的定义,而是结合我实际做过的几个小项目,把这些概念掰开揉碎讲清楚:它们各自怎么用、边界在哪、参数怎么调、以及哪些坑是文档里不会写的。不管你是刚入门 NLP 的新手,还是已经写过不少分类模型但一直对“特征表示”这件事没深究过的开发者,这篇内容都值得你花十分钟过一遍。
1. 先从“计算机怎么读文本”这件事说起
1.1 模型不认字,只认向量
很多初学者一开始会困惑:为什么不能直接把“我喜欢吃苹果”这句话扔给逻辑回归或者随机森林?原因很简单——绝大多数传统机器学习算法(以及早期神经网络的大部分实现)的输入都必须是固定长度的数值向量。你给它一个字符串,它连“大小怎么比较”都不知道,更别说做梯度计算、矩阵运算这些基础操作了。
所以文本进入模型之前,必须经过一个“翻译”过程:把自然语言里一个个词汇、短语,映射成高维空间中的数值坐标。这个映射方式的好坏,直接决定了后续分类、聚类、检索等任务的上限。很多跑出来的模型效果差,不是算法选错了,而是文本向量化这第一步就没做扎实。
1.2 向量化的三个基本流派
我习惯把文本向量化方案粗分成三代:
- 第一代:基于统计的离散表示,代表作就是独热编码和词袋模型(BoW),还包括后续的 TF-IDF。它们不考虑词序,只统计“出现与否”或“出现频率”。
- 第二代:基于降维的稠密表示,比如 LSA(潜在语义分析)、Word2Vec、GloVe。它们把每个词压缩成几百维的稠密向量,能捕捉一定的语义相似度。
- 第三代:基于上下文的动态表示,比如 BERT、GPT 这类预训练语言模型。同一个词在不同句子里会有不同的向量。
这篇文章聚焦第一代,因为它是理解后面所有方案的地基。如果你能彻底搞懂独热编码和词袋模型的局限在哪里,你就能明白为什么后来的人非要折腾出 Word2Vec 和 BERT——所有“为什么”都源于前者的“做不到”。
1.3 两个概念别混淆:独热编码是“词”的表示,词袋是“文本”的表示
这是很多文章没说透的一点。独热编码的对象是单个词,它是一个词对应一个向量;而词袋模型的对象是一整段文本(句子、段落、文章),它把文本表示成一个向量。词袋模型内部包含了一个“词汇表构建”步骤,而这一步通常就用独热编码的思路实现——每个词先在词汇表里找到自己的独热位置,然后文本向量根据这些位置来填值。
换句话说,独热编码是“零件”,词袋模型是“组装好的成品”。理解这个层次关系,后面看代码就不会晕。
2. 独热编码:最朴素也最“奢侈”的词汇表示
2.1 原理就一句话:一个词,一个位置,只有自己点亮
假设我们有一个迷你词汇表,包含 5 个词:["我", "喜欢", "吃", "苹果", "香蕉"]。要为“苹果”这个词做独热编码,就构造一个长度为 5 的向量,只在“苹果”对应的位置写 1,其余位置写 0。
我 -> [1, 0, 0, 0, 0] 喜欢 -> [0, 1, 0, 0, 0] 吃 -> [0, 0, 1, 0, 0] 苹果 -> [0, 0, 0, 1, 0] 香蕉 -> [0, 0, 0, 0, 1]就这么简单。每个词都是一个高维稀疏向量,维度等于词汇表大小。词汇表有多大,每个词就有多长。
2.2 动手实现:手写独热编码
用 Python 写这个逻辑很简单,我从项目里抽一段简化版给你看:
from collections import Counter def build_vocab(corpus): """从语料中构建词汇表,按出现频率排序""" counter = Counter() for text in corpus: # 这里用最简单的空白切分,中文场景需要换分词器 tokens = text.lower().split() counter.update(tokens) vocab = [word for word, _ in counter.most_common()] word2idx = {word: idx for idx, word in enumerate(vocab)} return vocab, word2idx def one_hot(word, word2idx): """返回单个词的独热向量""" vec = [0] * len(word2idx) if word in word2idx: vec[word2idx[word]] = 1 return vec corpus = ["i like apples", "i like bananas", "she likes apples"] vocab, word2idx = build_vocab(corpus) print("词汇表:", vocab) print("'apples' 的独热向量:", one_hot("apples", word2idx))输出结果大致是:
词汇表: ['i', 'like', 'apples', 'bananas', 'she', 'likes'] 'apples' 的独热向量: [0, 0, 1, 0, 0, 0]注意我在预处理里做了两件事:统一转小写、按空格切分。英文这么做没问题,中文文本这一步要换成分词工具,否则“我喜欢吃苹果”会被切成一整个词,词汇表里一个句子一个词,毫无泛化能力。
2.3 独热编码的致命伤与适用场景
它的缺点非常明显,我列几个实际项目里最痛的点:
- 维度爆炸:中文词汇表轻松上万,每个词变成一个上万维的向量,内存直接告急。我在一个电商评论分类项目里试过,全量词汇 12 万,光存词表映射就吃掉了几百 MB 内存,更别说后续喂给模型的矩阵了。
- 毫无语义信息:“苹果”和“香蕉”都是水果,但在独热空间里,这两个向量的距离和“苹果”与“冰箱”的距离完全一样——全是根号 2。它区分不了近义词,也区分不了无关词。
- 无法表达词序:独热编码只表达“这个位置是哪个词”,不表达“这个词前面是谁、后面是谁”。
它适合用的场景其实很窄:类别特征很少(比如性别、星期几、颜色这类 ≤ 几十种的离散特征)、并且你明确知道每一个取值都相互独立时,用它很合适。但放在文本词汇这种量级的场景里,它基本上只作为词袋模型的中间步骤存在,很少单独作为最终特征。
3. 词袋模型:把整段文本压成一个向量
3.1 核心思想:不管顺序,只管“有哪些词、出现几次”
词袋模型(Bag of Words)的命名很形象:把一句话里的所有词扔进一个袋子里,搅一搅,顺序全乱了也无所谓,你只知道袋子里有哪些词、每个词有几个。它彻底放弃了词序信息,只保留“词频”这一维度的信息。
对于一个文本,它的词袋向量长度还是词汇表大小,第 i 个位置的值,就是词汇表第 i 个词在这个文本里出现的次数(或者是否出现,取决于你用哪种变体)。
还是刚才那个例子,词汇表是 ["我", "喜欢", "吃", "苹果", "香蕉"],文本“我喜欢吃苹果”的向量就是:
[1, 1, 1, 1, 0]文本“我最喜欢吃香蕉”则是:
[1, 1, 1, 0, 1]注意“我”出现了两次,如果你用“计数”而非“是否出现”,那么对应位置写 2 而不是 1。这两种变体分别叫 Count Vector 和 Binary Vector。
3.2 用 sklearn 三行代码搞定
实际项目里没人手写这套逻辑,直接用sklearn.feature_extraction.text.CountVectorizer就够了:
from sklearn.feature_extraction.text import CountVectorizer corpus = [ "i like apples and i like bananas", "she likes apples very much", "he hates bananas" ] vectorizer = CountVectorizer(stop_words="english") X = vectorizer.fit_transform(corpus) print("词汇表:", vectorizer.get_feature_names_out()) print("向量矩阵形状:", X.shape) print("第一句话的向量:", X.toarray()[0])它的输出是一个稀疏矩阵。为什么是稀疏?因为语料里每一句话通常只包含词汇表的一小部分词,大部分位置都是 0。如果转成普通稠密数组,上万维的花销很大;保持稀疏格式,存储和计算都能省不少事。
3.3 词袋向量到底怎么算出来的
fit_transform这一行背后做了四件事:
- 分词(tokenize):默认按空格和标点切分,中文需要传自定义 tokenizer。
- 构建词汇表:统计所有文本的词频,可以设置
max_features只保留高频词。 - 统计词频:遍历每个文本,统计词汇表中每个词出现的次数。
- 输出稀疏矩阵:行是文本,列是词汇,值是计数。
理解这个流程很重要,因为你后续调参——比如要不要去停用词、要不要限制词汇表大小、要不要用 n-gram——本质都是在干预这四个环节的某一处。
3.4 常用参数与选择逻辑
我实际用过最有价值的几个参数,整理成一张表:
| 参数 | 作用 | 我的使用建议 |
|---|---|---|
stop_words | 去掉“the、a、的、了”这类高频无意义词 | 英文直接用"english";中文最好传入自定义停用词表,因为默认停用词表是英文的 |
max_features | 只保留频次最高的前 N 个词 | 我一般设 5000~20000,视语料规模而定。太小丢信息,太大引入噪声 |
min_df/max_df | 过滤掉出现次数过少/过多的词 | min_df=2通常能去掉只出现一次的生僻词;max_df=0.8能去掉几乎每篇都出现的高频废词 |
ngram_range | 是否把相邻词组合作为特征 | 文本分类里(1,2)很常用,能缓解“语序丢失”的问题 |
binary | 是否把计数改成 0/1 | 某些短文本任务用 binary 效果反而更好,因为长尾词频干扰少 |
这些参数不是摆设,选错了会直接影响模型效果。
3.5 一个容易被忽略的陷阱:训练集和测试集的词汇表必须一致
这是词袋模型实操里最大的坑。很多人图省事,先对整个数据集(包括测试集)做fit_transform,然后才划分训练测试集。这样做会导致数据泄漏:词汇表里包含了测试集的信息,模型在训练时就已经“见过”未来的数据分布,评估指标会虚高,上线后直接崩。
正确做法是:先在训练集上fit,拿到词汇表,再对训练集transform、对测试集transform。测试集里出现的新词,直接忽略不计;测试集向量里没有对应位置,就用 0 填充。sklearn 里写起来是这样:
vectorizer = CountVectorizer(max_features=5000) # 只在训练集上拟合 X_train = vectorizer.fit_transform(train_texts) # 测试集只做转换,不重新拟合 X_test = vectorizer.transform(test_texts)代码写起来只差一个fit的位置,但语义差得很远。
4. 独热编码与词袋模型的核心差异
4.1 一张表看懂区别
| 对比维度 | 独热编码 | 词袋模型 |
|---|---|---|
| 作用对象 | 单个词 / 单个类别 | 整段文本 |
| 向量长度 | 词汇表大小 | 词汇表大小(同) |
| 向量含义 | 这个词在词汇表中处于哪个位置 | 每个词在这段文本中出现的次数 |
| 是否表达频次 | 否,只有 0/1 | 是,可计数可二值化 |
| 能否单独作为文本特征 | 通常不能 | 能 |
| 内存占用 | 高(每词一个长向量) | 相对可控(稀疏矩阵) |
| 语义表达能力 | 无 | 极弱,但有基本的“主题词”区分能力 |
4.2 为什么说词袋模型是独热编码的“升级版”
词袋模型并没有抛弃独热编码,而是站在它的肩膀上做了一层聚合。它对文本里的每个词,查词表的独热位置,然后把所有词的位置“叠”起来,同一位置多次出现就累加。你可以理解为:独热编码是“一个词一张图”,词袋模型是“一篇文章里所有词的图叠在一起”。
这就是为什么我说要先彻底理解独热编码,否则你看到词袋输出的那一排数字,会觉得莫名其妙——为啥这个位置是 2,那个位置是 0?其实它背后就是独热向量按文本内容做了一次加法。
4.3 什么时候用哪个:我的选择经验
我自己的选择原则很简单:
- 特征维度少、类是离散且互斥的(比如商品类目、省份、星期几),直接用独热编码,
pd.get_dummies或者OneHotEncoder都行。 - 目标是做整段文本的分类、聚类、相似度计算,直接上词袋模型(或者它后边的升级版 TF-IDF)。
- 如果两者都要用(比如文本特征 + 商品类目特征),就把词袋向量和独热向量拼在一起,
scipy.sparse.hstack可以搞定。我做广告点击率预测时,就常这么干:文本用词袋,离散 ID 用独热,拼接后喂给 LR 模型。
5. 词袋模型的三大硬伤与补丁方案
5.1 硬伤一:高频无意义词淹没关键词
词袋模型统计的是词频。问题在于,语料里最高的频次往往来自“的”“了”“在”“and”“the”这类虚词。它们对分类毫无贡献,却会在距离计算、相似度计算时占据主导地位。比如两篇文章都在频繁使用“的”,它们的向量距离就会异常接近,哪怕内容风马牛不相及。
标准补丁是 TF-IDF(词频-逆文档频率)。它把“这个词在多少篇文档里出现过”加进惩罚项——越是常见于多篇文档的词,权重压得越低;越是只在少数文档中出现的词,权重拉得越高。在 sklearn 里,把CountVectorizer换成TfidfVectorizer就能获得这种加权效果,代码几乎不用改。
我实测过一个新闻分类场景:同样的逻辑回归,用 CountVectorizer 准确率约 82%,换成 TF-IDF 后到 87%。提升主要来自“的”这类词不再霸榜。
5.2 硬伤二:语序信息彻底丢失
“猫追狗”和“狗追猫”在词袋模型里是完全相同的向量,因为它们的词频分布一模一样。这是词袋模型的固有缺陷,几乎无解,只能缓解。缓解办法:
- 用 n-gram:把相邻词组合成特征,比如
ngram_range=(1,2)会产生“猫追”“追狗”这样的二元特征,能部分捕捉局部语序。 - 换用能建模词序的模型:这是后话了,Word2Vec、LSTM、BERT 这条路线才是根本解。
5.3 硬伤三:词汇表外的新词直接哑火
训练时词汇表固定了,上线后遇到新词、错别字、网络新词,词袋模型直接当作没看见——向量对应位置是 0,等于该词信息完全丢失。这个问题没有完美的补丁,我的经验是:
- 用
min_df过滤低频词,反而能在一定程度上“免疫”拼写错误带来的噪声。 - 预处理时尽量做拼写纠正、简繁转换、同义替换,减少词表外的概率。
- 如果你发现新词问题很严重(比如新闻流里不断出现新概念),也许该考虑换更灵活的表示方案,比如字符级别的 n-gram,它至少能兜住“词不认识但字认得”的情况。
5.4 维度爆炸的另一条路:哈希技巧
词袋模型要维护一个词汇表,词汇表越大,向量越长,内存越紧张。一个轻量级替代方案是HashingVectorizer,它不存词表,直接用哈希函数把词映射到固定维度的位置,有冲突也没关系,反正哈希碰撞的损失通常远小于词表维护的收益。适合超大语料、在线学习这类场景。
不过它有个缺点:你已经无法从向量反推回具体是哪些词——特征解释性基本归零。
6. 中文文本的特殊处理:分词是关键中的关键
6.1 中文不能按空格切分
英文文本按空格切的思路,在中文这里完全走不通。“我最喜欢吃苹果”用空格切只会得到一整串字符。中文必须靠分词工具,最常用的有 jieba、HanLP、LTP 这些。切得好不好,直接影响词表构建和向量质量。
我踩过的一个典型教训:某次用默认的 jieba 全模式切分,把“北京大学生”切成了“北京大学 / 学生 / 北京 / 大学生”这种交叉片段,导致词表里冒出很多奇奇怪怪的组合词,模型效果反而比不切更差。后来换成精确模式并添加自定义词典,才算稳定下来。
6.2 jieba + CountVectorizer 的标准组合
CountVectorizer支持自定义 tokenizer,和 jieba 配合很容易:
import jieba from sklearn.feature_extraction.text import CountVectorizer def jieba_tokenize(text): return list(jieba.cut(text)) vectorizer = CountVectorizer(tokenizer=jieba_tokenize, max_features=8000) X = vectorizer.fit_transform(corpus_chinese)注意,tokenizer参数传入的函数,sklearn 会在内部调用它来切分每一篇文档。jieba 默认输出包含空格和标号,建议自己再过滤一遍无用符号,否则词表里会出现“,”“。”这类脏 token。正规做法是先做数据清洗:去标点、去数字、去停用词,再做分词。
6.3 中文停用词不能偷懒
英文有 sklearn 自带的stop_words="english",中文没有现成的。我的做法是维护一个本地停用词文件,把常见虚词、标点、无意义高频词都放进去,然后自定义一个 tokenizer,分词后直接过滤:
stop_words = set(open("chinese_stopwords.txt", encoding="utf-8").read().split()) def clean_tokenize(text): words = jieba.cut(text) return [w for w in words if w.strip() and w not in stop_words and not w.isdigit()]这个文件我一般会包含“的、了、是、在、和、我、你、他、它、这、那”等几百个高频词,根据任务补充。宁可多过滤,也别让废词污染特征空间。
7. 实操记录:从原始文本到可训练特征
7.1 一个完整的最小流程
我把一个完整的最小流程写在下面,你照着跑就能得到可喂给模型的特征矩阵。场景是“电商评论情感分类”,正负样本各 5000 条。
import jieba import pandas as pd from sklearn.feature_extraction.text import CountVectorizer from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score # 1. 加载数据,假设 df 有 "comment" 和 "label" 两列 df = pd.read_csv("review.csv") X_text = df["comment"].tolist() y = df["label"].tolist() # 2. 切分训练测试集,必须先切分再 fit train_texts, test_texts, y_train, y_test = train_test_split( X_text, y, test_size=0.2, random_state=42 ) # 3. 构建向量器,带停用词过滤和中英文字符保留 def tokenizer(text): words = jieba.lcut(text) return [w for w in words if w.strip() and w not in stop_words] vectorizer = CountVectorizer( tokenizer=tokenizer, max_features=10000, min_df=2, max_df=0.9, ngram_range=(1, 2) ) # 4. 只对训练集 fit X_train = vectorizer.fit_transform(train_texts) # 5. 测试集只 transform X_test = vectorizer.transform(test_texts) print("训练集特征矩阵形状:", X_train.shape) # 6. 训练一个简单的逻辑回归 model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred))这套流程我在好几个项目里复用,基本是零改动的模板。唯一需要调整的是max_features和ngram_range。短评数据我一般用(1, 1)就够了,因为短文本里语序信息本来就不多;长文档则必须上(1, 2),否则语义丢失严重。
7.2 关于 max_features 的取值经验
max_features不是越大越好。我做过一轮网格搜索,样本量 1 万左右时,5000 和 20000 的效果差异不超过 1 个百分点,但内存和时间成本差了不少。维度太多反而容易引入稀疏噪声,让线性模型过拟合。我的默认值写死 8000~10000,除非语料规模特别大,才会往上调。
7.3 关于数据泄漏的再强调
我在文章里反复提到训练测试要分开 fit,是因为这个问题真的很容易犯,而且犯错的后果非常隐蔽——训练集准确率异常高,测试集也没有明显异常,因为测试集的“信息”已经被训练过程偷看过了,但你完全察觉不到。只有上线后面对真实新数据,效果才会断崖式下跌。我本人就吃过一次这样的亏,后来凡是写文本预处理代码,第一行就先切分数据,第二行才轮到向量化,顺序都不敢变。
8. 常见问题与排查技巧实录
8.1 问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 训练集效果好、测试集差很多 | 可能是数据泄漏——词汇表用全量数据拟合了;也可能是 min_df 设置过低导致过拟合 | 检查 fit/transform 顺序;调高 min_df;检查测试集与训练集分布是否差异过大 |
| 向量矩阵全是 0 | 分词后所有词都被停用词过滤了 | 打印 tokenizer 输出,看看 token 列表是否为空;放宽停用词表 |
| 词汇表里出现频率极高的无意义词 | 没做停用词处理,或停用词表太薄 | 补充停用词,尤其是中文虚词 |
| 特征维度巨大、内存溢出 | 没设置 max_features,低频长尾词太多 | 设max_features=10000;用 HashingVectorizer 替代 |
| 新词、错别字导致信息丢失 | 词表固定,词表外词无法表示 | 预处理纠错;使用字符级 n-gram;必要时换用预训练词向量 |
| 同样的文本,向量结果对不上 | 词典排序不一致 | 检查 vectorizer.vocabulary_ 的键值映射;保存 vectorizer 模型供推理复用 |
| 词表里出现标点、数字 | 预处理清洗不足 | 在 tokenizer 里过滤非中英文字符;用正则提前清洗文本 |
8.2 排查工具推荐:从向量反看原始词
调试词袋模型时,我最常做的一件事是“反向验证”——把某个文本的向量还原成可读的词列表,看特征到底是怎么生成的。方法很简单:
import numpy as np def show_features(vectorizer, text, tokenizer): tokens = tokenizer(text) vec = vectorizer.transform([text]).toarray()[0] idxs = np.argsort(vec)[::-1] vocab = list(vectorizer.get_feature_names_out()) features = [(vocab[i], vec[i]) for i in idxs if vec[i] > 0] return tokens, features # 打印分词结果和实际特征这一步能帮你快速定位三类问题:分词分错了、停用词滤多了、特征权重和预期不符。
8.3 我第一次做文本分类踩的坑
第一次做文本分类时,我把整个语料拼起来后用CountVectorizer一次性 fit_transform,然后才划分训练集。当时的逻辑是“反正整个数据我都拥有,为何要分前后”,结果线下测试集准确率 93%,上线后对新数据只有 71%。排查了半天,最终锁定在词汇表泄漏。那次之后我养成了习惯:预处理流水线里,fit_transform和transform之间的分界永远跟着数据划分走,绝不混淆。
8.4 中文 UI 乱码与编码问题补充
做中文语料处理时,还有一个跟向量化无关但很常见的问题:文件编码。读取 CSV 时漏了encoding="utf-8",或者用 Windows 环境默认的 gbk 读 utf-8 文件,都会导致分词结果全是乱码,词汇表变成一堆诡异字符。我的建议是统一约定:数据文件一律 UTF-8,读取时显式指定encoding="utf-8",写入也显式指定,不要依赖系统默认编码。
9. 独热编码在非文本场景下的价值
9.1 类别特征编码可别忘
虽然这篇文章的重头戏是文本向量化,但独热编码在表格数据里的应用同样广泛。性别、城市、产品类目这类离散特征,如果不加处理直接喂进模型,数值大的类别会被赋予更高的权重,这对大多数模型是灾难性的。解决办法就是独热编码。
我见过一个真实场景:某商品推荐项目把“商品 ID”直接当成数值特征放进模型,ID 大的商品天然获得更高预测分数,推荐结果完全被带偏。换成独热编码后,每个商品都是平等的一个维度,问题立刻消失。
9.2 什么时候不该用独热
独热编码也不是万能。对于取值特别多的高基类特征(比如用户 ID、IP 地址、商品 ID),独热编码会让特征维度爆炸。这类场景通常改用 embedding 或目标编码。这也是为什么我在前面反复强调:独热适合低基离散特征,高基特征要另行斟酌。
理解这一点,你就能在面对“这个特征到底要不要独热”的问题时,快速做出判断,而不是生搬硬套。
10. 后续扩展:从词袋走向 TF-IDF 和词向量
10.1 TF-IDF:词袋模型最常用的补丁
词袋模型的问题在于“高频词通吃”。TF-IDF 是解决这个问题最经典的方案。它的计算逻辑是词频(TF)乘以逆文档频率(IDF)——某个词在文档里出现得多,加分;但同时它也大量出现在很多文档里,就减分。这样像“的”“了”这种到处都是的词,权重会接近零;而“显卡”“售后”这种有区分度的词,权重会被拉到很高。
sklearn 里TfidfVectorizer和CountVectorizer的参数几乎一样,你只需要换类名。它输出的矩阵也可以直接喂给同样的模型。我个人的建议是:除非你有明确的理由用纯词频,否则文本分类任务直接上 TF-IDF,省去后面返工。
10.2 词向量:真正理解语义的下一步
词袋和 TF-IDF 有一个共同的本质局限——它们把每个词当独立的符号,词和词之间没有关联。而 Word2Vec、GloVe 这类词向量模型,能让“苹果”和“香蕉”的向量在空间里比较接近,这来自于它们在大语料上的共现统计。我前几年在搜索场景里,把词袋特征替换成词向量均值后,召回质量有了阶段性的提升,但这需要更多的训练数据和计算资源。
10.3 如何根据场景做技术选型
我总结了一个简单的选型流程,供你参考:
- 数据量小(几千条以内)、任务简单:直接用 TF-IDF + 线性模型,又快又稳。
- 数据量中等(几万条)、任务较复杂:TF-IDF + ngram,必要时加一点特征拼接。
- 数据量大、有 GPU、追求最好效果:预训练词向量或 BERT 类模型,但成本和复杂度要高不少。
这套流程对应的场景很多,我的经验是,大部分业务问题其实落在第一档——用 TF-IDF 就能解决 80% 的需求,不必一上来就跑到大模型赛道卷。
11. 最后分享几个实用的小技巧
独热编码和词袋模型,说到底只是特征工程的两个小环节。但它们背后反映的核心思维,是“如何把非结构化数据变成结构化的、可计算的形式”,这个思维在 NLP 之后的各种任务里都适用。
我个人在实际操作中的体会是:文本向量化再怎么花哨,都不如先把数据处理干净来得实在。分词、清洗、停用词这几步如果做得糙,后面换什么高级模型都难以彻底补救。很多工程问题不是模型不行,而是数据从源头就没喂对。
最后再分享一个小技巧:CountVectorizer生成的特征矩阵,可以用scipy.sparse直接和数值特征拼接起来,喂给同一个模型。这一步操作在很多场景里是提升效果的有效手段——文本描述和数值属性共同决定了最终结果,而不是只靠其中一类。
如果这篇文章让你对独热编码和词袋模型有了更深的理解,你自己找个公开的中文语料跑一遍,感受会完全不同。文本向量化的这几个基础方法,用熟了以后,你会发现自己已经迈过了文本处理领域最关键的“第一道门槛”——从那以后,再去看 TF-IDF、词向量甚至 Transformer,都会觉得循着一条清晰的脉络在发展,而不再是零散的名词堆砌。