2026/10/20 12:10:43

中文文本特征提取实战:从分词、TF-IDF到词嵌入与BERT

中文文本特征提取实战:从分词、TF-IDF到词嵌入与BERT 1. 文本特征提取的整体设计思路1.1 为什么文本不能直接喂给模型做机器学习项目绕不开特征工程这一步。图像可以直接把像素拉成向量数值型数据本身就能进模型但文本不行。模型只认数字你给它一段中文、一段英文它完全不知道你在说什么。所以文本特征提取要解决的核心问题就一个把人类能看懂的文字转换成机器能计算的数值向量。这件事听起来简单做起来坑特别多。我见过不少新手直接把一整段话当成一个特征丢进去结果模型跑出来准确率惨不忍睹。原因很简单文本是有结构的词序、词频、上下文关系都携带信息粗暴处理等于把这些信息全扔了。文本特征提取的演进路线大致是这样的最早是词袋模型把文本当成一堆词的集合只看词出现没出现、出现了几次完全不管顺序后来有了TF-IDF在词频基础上引入逆文档频率降低那些到处都出现的常见词的权重再往后是词嵌入把每个词映射成一个稠密向量语义相近的词在向量空间里距离也近现在最火的是预训练语言模型直接输出带上下文信息的动态向量。对于大多数实际项目来说TF-IDF配合线性模型仍然是性价比最高的方案尤其是在数据量不大、算力有限的情况下。词嵌入和预训练模型虽然效果好但计算成本高调参也更复杂。选哪个方案取决于你的数据规模、任务难度和可用资源。1.2 中文文本特征提取的特殊难点英文文本天然有空格分隔单词中文没有。这是中文文本处理最根本的差异。你拿到一句“今天天气真好”模型不知道应该切成“今天/天气/真好”还是“今/天/气/真/好”。分词的质量直接决定了后续特征提取的效果。中文分词的难点主要集中在三个方面。第一是歧义切分比如“南京市长江大桥”可以切成“南京市/长江大桥”也可以切成“南京/市长/江大桥”不同切分方式意思完全不同。第二是未登录词识别网络新词、人名、地名、专业术语层出不穷分词工具的词表往往跟不上。第三是领域适应性通用分词工具在特定领域比如医疗、法律、金融的表现可能很差因为领域术语它根本不认识。除了分词中文还有编码问题。早期项目里GBK和UTF-8混用导致乱码的情况非常普遍现在虽然好一些但处理网络爬取的数据时仍然要小心。另外中文的标点符号、全角半角、繁简体转换也都是需要预处理的内容。1.3 方案选型的核心考量因素选文本特征提取方案我一般从这几个维度去评估数据规模。几千条数据用TF-IDF就够了几十万条以上可以考虑词嵌入上百万条且有充足算力再上预训练模型。数据量小的时候复杂模型很容易过拟合反而不如简单的TF-IDF稳定。任务类型。文本分类任务TF-IDF加朴素贝叶斯或线性SVM通常就能拿到不错的效果。情感分析、意图识别这类任务词嵌入能捕捉更细粒度的语义差异。如果是问答、摘要生成这类生成式任务那必须用预训练模型。可解释性要求。TF-IDF的每个维度对应一个词权重高低一目了然出了问题容易排查。词嵌入和预训练模型是黑盒调试起来麻烦得多。如果项目对可解释性有硬性要求比如金融风控场景那TF-IDF几乎是唯一选择。工程复杂度。TF-IDF用scikit-learn几行代码就能搞定词嵌入需要训练Word2Vec或加载预训练词向量预训练模型还要考虑GPU部署、推理延迟等问题。团队技术栈和运维能力也是选型时必须考虑的现实因素。2. 核心细节解析与实操要点2.1 中文分词工具的选择与对比中文分词工具市面上主流的有好几种我逐个说一下实际使用感受。jieba是用的最多的安装简单pip install jieba就能用。它支持三种分词模式精确模式、全模式和搜索引擎模式。精确模式适合文本分析全模式会把所有可能的词都切出来有冗余搜索引擎模式在精确模式基础上对长词再切分适合做搜索索引。jieba还支持自定义词典你可以把领域术语加进去提高分词准确率。HanLP功能更全面除了分词还有词性标注、命名实体识别、依存句法分析等。它分轻量级和标准版轻量级版不需要额外下载模型文件标准版效果更好但配置稍麻烦。HanLP对未登录词的识别能力比jieba强一些但速度慢一点。SnowNLP偏向情感分析场景分词只是它的一部分功能。它的分词效果中规中矩优点是自带情感分析、拼音转换等功能适合做快速原型。THULAC和LTP是学术圈常用的工具分词准确率很高但部署相对复杂文档也没有jieba那么友好。选哪个我的建议是通用场景用jieba需要更全面的NLP功能用HanLP做学术研究可以试试LTP。实际项目中分词工具往往不是瓶颈预处理流程和特征设计才是关键。2.2 停用词处理的正确姿势停用词就是那些对文本语义贡献很小的词比如“的”、“了”、“是”、“在”这些。去掉它们能降低特征维度减少噪声。但停用词处理有几个坑要注意。第一个坑是停用词表不能照搬。网上能找到很多中文停用词表但不同领域的停用词不一样。比如在医疗文本里“患者”、“症状”这些词出现频率极高但它们携带重要信息不应该被去掉。在情感分析任务里“不”、“没”这些否定词如果被当成停用词去掉情感极性可能完全反转。第二个坑是停用词处理要在分词之后。有些人直接在原始文本上做字符串替换这会导致误删。比如“的确”这个词如果你直接把“的”替换掉就变成了“确”意思完全变了。正确做法是先分词然后在词级别过滤停用词。第三个坑是停用词表要持续维护。项目跑一段时间后你会发现某些词在所有文档里都出现但对分类没有帮助这些词应该加入停用词表。反过来如果发现某些被过滤掉的词其实很重要就要从停用词表里移除。我一般会准备两个停用词表一个通用停用词表包含“的”、“了”、“是”这类一个领域停用词表根据具体任务动态调整。两个表合并使用效果比较稳。2.3 TF-IDF参数调优的实战经验TF-IDF是文本特征提取的经典方法但很多人直接用默认参数效果不理想。这里说几个关键参数怎么调。max_features控制特征维度上限。默认是None意味着保留所有词。实际项目中词表可能有几万甚至几十万个词但很多词只出现一两次对模型没有帮助还增加计算量。我一般会设置max_features5000到50000之间具体看数据量。数据量小就设小一点防止过拟合数据量大可以设大一些保留更多信息。min_df和max_df控制词频阈值。min_df2表示只保留至少在2个文档中出现的词这能过滤掉那些只出现一次的罕见词。max_df0.95表示过滤掉在95%以上文档中都出现的词这些词通常是领域停用词。这两个参数配合使用能有效降低噪声。ngram_range控制是否提取词组特征。默认是(1,1)只提取单个词。设置成(1,2)会同时提取单个词和相邻两个词的组合。比如“机器学习”如果被分成“机器”和“学习”bigram能捕捉到“机器 学习”这个组合语义更完整。但ngram_range设大了会导致特征维度爆炸一般(1,2)就够了最多(1,3)。sublinear_tf是个布尔参数设为True时会对词频取对数防止某些词频过高的词权重过大。文本长度差异大的时候这个参数很有用。norm控制归一化方式默认是l2一般不用改。如果做余弦相似度计算l2归一化是必须的。2.4 词嵌入与预训练模型的取舍词嵌入Word2Vec、GloVe、FastText把词映射到低维稠密空间能捕捉语义关系。比如“国王”和“王后”的向量差跟“男人”和“女人”的向量差很接近。这是TF-IDF做不到的。但词嵌入也有问题。每个词只有一个向量无法处理一词多义。“苹果”在“吃苹果”和“苹果手机”里意思完全不同但Word2Vec给出的向量是一样的。ELMo和BERT这类预训练模型通过上下文动态生成词向量解决了这个问题但计算成本高很多。实际项目中怎么选我的经验是文本分类任务TF-IDF加线性模型先跑baseline如果效果不够再上词嵌入词嵌入还不够再考虑预训练模型。不要一上来就用BERT杀鸡用牛刀不说调试成本也高。很多任务TF-IDF就能做到90%的准确率上BERT可能只提升到92%但推理时间增加几十倍不划算。如果决定用预训练模型中文场景推荐BERT-base-chinese或RoBERTa-wwm-ext。前者是谷歌官方中文模型后者是哈工大讯飞联合发布的在中文任务上通常表现更好。用HuggingFace的transformers库加载很方便但要注意模型大小和推理速度的平衡。3. 实操过程与核心环节实现3.1 环境准备与依赖安装先说一下环境。Python 3.7以上都行我习惯用3.8或3.9兼容性好。主要依赖库pip install jieba scikit-learn pandas numpy如果要玩词嵌入和预训练模型再加pip install gensim transformers torchjieba第一次运行会构建前缀词典稍微慢一点之后就快了。scikit-learn的TfidfVectorizer是核心工具CountVectorizer用于词频统计。pandas用来读数据numpy做数值计算。注意如果数据量很大建议用稀疏矩阵存储特征scikit-learn默认返回的就是稀疏矩阵不要用toarray()转成稠密矩阵内存会爆。3.2 中文文本预处理完整流程预处理是文本特征提取最耗时的环节但也是最关键的。我一般按这个流程走第一步读取数据。用pandas读CSV或Excel检查有没有缺失值。文本列如果有NaN直接填充空字符串或者删掉对应行。第二步文本清洗。去掉HTML标签、URL、邮箱、电话号码这些无关内容。用正则表达式处理import re def clean_text(text): text re.sub(r[^], , text) # 去HTML标签 text re.sub(rhttp\S|www\.\S, , text) # 去URL text re.sub(r\S\S, , text) # 去邮箱 text re.sub(r\d{11}, , text) # 去手机号 text re.sub(r\s, , text) # 合并空白字符 return text.strip()第三步全角转半角。中文文本里经常混着全角字符比如全角逗号“”和半角逗号“,”。统一转成半角避免同一个标点被当成两个不同字符。def full_to_half(text): result [] for char in text: code ord(char) if code 12288: # 全角空格 code 32 elif 65281 code 65374: # 全角字符范围 code - 65248 result.append(chr(code)) return .join(result)第四步分词。用jieba的精确模式import jieba def segment(text): return list(jieba.cut(text, cut_allFalse))如果领域术语多加载自定义词典jieba.load_userdict(user_dict.txt)词典格式是每行一个词可以带词频和词性比如“机器学习 100 n”。第五步去停用词。加载停用词表过滤掉停用词和单字单字通常信息量低但也不是绝对看任务def remove_stopwords(words, stopwords): return [w for w in words if w not in stopwords and len(w) 1]第六步重新拼接。把分词结果用空格拼起来因为TfidfVectorizer默认按空格分词def join_words(words): return .join(words)整个预处理流程走完原始文本就变成了空格分隔的词序列可以直接喂给TfidfVectorizer了。3.3 TF-IDF特征提取代码实现预处理完成后特征提取就简单了。核心代码from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features10000, min_df2, max_df0.95, ngram_range(1, 2), sublinear_tfTrue, norml2 ) X vectorizer.fit_transform(corpus)fit_transform返回的是稀疏矩阵shape是(样本数, 特征数)。vectorizer.get_feature_names_out()可以拿到所有特征词vectorizer.idf_可以拿到每个词的IDF值。如果后面还有测试集用vectorizer.transform(test_corpus)不要再fit一次否则特征空间不一致。提示fit_transform只能在训练集上做测试集和验证集必须用transform。这是新手最容易犯的错误之一导致数据泄露模型评估结果虚高。3.4 词嵌入特征提取代码实现用gensim训练Word2Vecfrom gensim.models import Word2Vec # 假设corpus是分词后的列表每个元素是一个词列表 model Word2Vec( sentencescorpus, vector_size100, window5, min_count2, workers4, epochs10 ) # 获取词向量 word_vectors model.wv训练完成后每个词对应一个100维向量。但文本分类需要的是整个文档的向量不是单个词的向量。常见做法是对文档中所有词的向量取平均import numpy as np def document_vector(words, model): vectors [model.wv[w] for w in words if w in model.wv] if len(vectors) 0: return np.zeros(model.vector_size) return np.mean(vectors, axis0)取平均会丢失词序信息但简单有效。也可以用TF-IDF加权平均效果通常更好def tfidf_weighted_vector(words, model, tfidf_dict): vectors [] weights [] for w in words: if w in model.wv and w in tfidf_dict: vectors.append(model.wv[w]) weights.append(tfidf_dict[w]) if len(vectors) 0: return np.zeros(model.vector_size) weights np.array(weights).reshape(-1, 1) return np.average(vectors, axis0, weightsweights.flatten())3.5 预训练模型特征提取代码实现用HuggingFace的transformers提取BERT特征from transformers import BertTokenizer, BertModel import torch tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertModel.from_pretrained(bert-base-chinese) model.eval() def get_bert_embedding(text): inputs tokenizer(text, return_tensorspt, truncationTrue, max_length512, paddingTrue) with torch.no_grad(): outputs model(**inputs) # 取[CLS]位置的向量作为句子表示 return outputs.last_hidden_state[:, 0, :].numpy()BERT的[CLS]向量通常用来做句子级别的分类任务。也可以对所有token的向量取平均效果差不多。注意max_length不要超过512超长文本要截断或分段处理。注意BERT推理很慢CPU上一条文本可能要几百毫秒。如果数据量大建议用GPU或者用蒸馏版模型如DistilBERT加速。4. 常见问题与排查技巧实录4.1 分词效果差的排查思路分词效果差是最常见的问题。表现是明明是人能看懂的话分出来的词莫名其妙。排查步骤先看是不是词典问题。领域术语没被识别比如“卷积神经网络”被切成“卷积”、“神经”、“网络”。解决办法是加自定义词典。把领域术语整理成一个txt文件每行一个词用jieba.load_userdict()加载。再看是不是歧义切分。比如“研究生命起源”可能被切成“研究”、“生命”、“起源”也可能被切成“研究生”、“命”、“起源”。这种只能靠上下文判断jieba的精确模式大部分时候能处理对但不可能100%准确。如果某个歧义对任务影响很大可以考虑用HanLP或LTP它们的分词模型更强。最后看是不是新词问题。网络新词、缩写、谐音梗分词工具的词表里没有自然切不对。解决办法是维护一个动态新词表定期更新。也可以考虑用基于深度学习的分词工具它们对未登录词的识别能力更强。4.2 特征维度爆炸的处理方法特征维度太高会导致训练慢、内存占用大、模型容易过拟合。处理方法设置max_features。这是最直接的办法限制特征总数。具体设多少看数据量我一般从5000开始试逐步增加观察验证集效果。提高min_df。把min_df从1调到2或3能过滤掉大量只出现一两次的罕见词。这些词对模型几乎没有贡献但会显著增加维度。降低max_df。把max_df从1.0调到0.95或0.9过滤掉那些在几乎所有文档中都出现的词。这些词通常是领域停用词区分度低。用卡方检验或互信息做特征选择。scikit-learn的SelectKBest可以基于统计检验选择top K个特征from sklearn.feature_selection import SelectKBest, chi2 selector SelectKBest(chi2, k5000) X_selected selector.fit_transform(X, y)4.3 中文编码问题的排查与解决编码问题在爬取的数据里特别常见。表现是读出来的文本是乱码或者报UnicodeDecodeError。先确认文件编码。用chardet库检测import chardet with open(data.txt, rb) as f: raw f.read() result chardet.detect(raw) print(result)读取时指定编码。pandas的read_csv有encoding参数常见的有utf-8、gbk、gb18030。gb18030兼容gbk和gb2312不确定的时候可以先用gb18030试。统一转成UTF-8。处理完的数据统一存成UTF-8格式避免后续环节再出问题。注意BOM头。Windows下用记事本保存的UTF-8文件可能带BOM头读取时用utf-8-sig编码可以自动去掉。4.4 常见问题速查表问题现象可能原因排查方法解决方案分词结果不合理领域术语未识别检查分词结果中是否有被切碎的术语加载自定义词典特征维度太高罕见词太多查看feature_names数量提高min_df设置max_features模型准确率低停用词未处理检查高频特征词完善停用词表内存溢出稠密矩阵太大检查矩阵类型保持稀疏矩阵不要toarray测试集效果差数据泄露检查是否在测试集上fit训练集fit测试集transform编码报错文件编码不一致用chardet检测统一转UTF-8BERT推理慢模型太大检查模型参数量用蒸馏版或GPU加速词向量效果差训练语料太少检查语料规模用预训练词向量或增加数据4.5 实操避坑经验汇总最后分享几个我踩过的坑都是文档里不会写的。坑一jieba多线程问题。jieba在第一次加载词典时会构建前缀树如果多线程同时初始化可能出问题。解决办法是在程序启动时先调用一次jieba.initialize()之后再并发使用。坑二TfidfVectorizer的token_pattern。默认的token_pattern是(?u)\b\w\w\b会过滤掉单字符。中文分词后如果保留单字会被这个正则过滤掉。如果确实需要单字特征要修改token_pattern或设置tokenizer为自定义函数。坑三Word2Vec的min_count。min_count设得太低罕见词也会被训练但向量质量很差设得太高词表太小很多词查不到向量。一般设2到5之间比较合适。坑四BERT的max_length。中文BERT的max_length是512个token但中文一个字通常对应一个token所以实际能处理的文本长度大概500字左右。超长文本要截断但截断会丢失信息。可以考虑分段处理再合并或者用Longformer这类支持长文本的模型。坑五特征缓存。文本预处理和特征提取很耗时调试阶段反复跑很浪费时间。建议把中间结果缓存到磁盘用pickle或joblib保存。下次直接加载能省很多时间。import joblib # 保存 joblib.dump(vectorizer, tfidf_vectorizer.pkl) joblib.dump(X, features.pkl) # 加载 vectorizer joblib.load(tfidf_vectorizer.pkl) X joblib.load(features.pkl)坑六版本兼容性。scikit-learn不同版本的TfidfVectorizer行为可能有细微差异保存的模型在新版本加载可能报错。建议在requirements.txt里锁定版本号保证环境一致。坑七中文标点处理。中文的句号、逗号、问号等标点在分词时通常会被单独切出来。这些标点对语义贡献很小但会增加特征维度。建议在预处理阶段去掉所有标点或者把标点加入停用词表。坑八数字和英文的处理。文本里的数字和英文单词分词工具的处理方式可能不符合预期。比如“2023年”可能被切成“2023”和“年”英文单词可能被切成字母。建议根据任务需求决定是否保留数字和英文或者做归一化处理比如把所有数字替换成特殊标记。文本特征提取这个环节说难不难说简单也不简单。工具就那些但参数怎么调、流程怎么设计直接决定了最终效果。我的建议是先把baseline跑通用TF-IDF加逻辑回归看看效果然后再逐步优化。不要一上来就追求最复杂的方案很多时候简单方法就能解决问题。遇到效果不好的情况先检查数据质量和预处理流程再考虑换模型或调参数。大部分问题其实出在数据上而不是模型上。