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_features=5000到50000之间,具体看数据量。数据量小就设小一点,防止过拟合;数据量大可以设大一些,保留更多信息。min_df和max_df控制词频阈值。min_df=2表示只保留至少在2个文档中出现的词,这能过滤掉那些只出现一次的罕见词。max_df=0.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(r'http\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_all=False))如果领域术语多,加载自定义词典: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_features=10000, min_df=2, max_df=0.95, ngram_range=(1, 2), sublinear_tf=True, norm='l2' ) 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训练Word2Vec:from gensim.models import Word2Vec # 假设corpus是分词后的列表,每个元素是一个词列表 model = Word2Vec( sentences=corpus, vector_size=100, window=5, min_count=2, workers=4, epochs=10 ) # 获取词向量 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, axis=0)取平均会丢失词序信息,但简单有效。也可以用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, axis=0, weights=weights.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_tensors='pt', truncation=True, max_length=512, padding=True) 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, k=5000) 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加逻辑回归看看效果,然后再逐步优化。不要一上来就追求最复杂的方案,很多时候简单方法就能解决问题。遇到效果不好的情况,先检查数据质量和预处理流程,再考虑换模型或调参数。大部分问题其实出在数据上,而不是模型上。
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_features=5000到50000之间,具体看数据量。数据量小就设小一点,防止过拟合;数据量大可以设大一些,保留更多信息。min_df和max_df控制词频阈值。min_df=2表示只保留至少在2个文档中出现的词,这能过滤掉那些只出现一次的罕见词。max_df=0.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(r'http\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_all=False))如果领域术语多,加载自定义词典: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_features=10000, min_df=2, max_df=0.95, ngram_range=(1, 2), sublinear_tf=True, norm='l2' ) 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训练Word2Vec:from gensim.models import Word2Vec # 假设corpus是分词后的列表,每个元素是一个词列表 model = Word2Vec( sentences=corpus, vector_size=100, window=5, min_count=2, workers=4, epochs=10 ) # 获取词向量 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, axis=0)取平均会丢失词序信息,但简单有效。也可以用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, axis=0, weights=weights.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_tensors='pt', truncation=True, max_length=512, padding=True) 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, k=5000) 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加逻辑回归看看效果,然后再逐步优化。不要一上来就追求最复杂的方案,很多时候简单方法就能解决问题。遇到效果不好的情况,先检查数据质量和预处理流程,再考虑换模型或调参数。大部分问题其实出在数据上,而不是模型上。