
做文本分类如果让我只推荐一个入门项目那肯定是THUCNews新闻文本分类。这个任务用TF-IDF加sklearn就能跑出非常漂亮的结果而且整套链路下来特征工程、模型选择、评估调优这些NLP基础能力全都能练到。我自己当初就是靠这个项目把sklearn从“装上了但不会用”练到“闭着眼能写分类pipeline”的今天把整个实操过程、参数细节和踩过的坑都整理出来给想入坑文本分类的朋友一份能直接照着跑的参考。THUCNews是清华大学自然语言处理实验室开放的新浪新闻数据集社区里常用的是抽出来的子集10个类别、每类6500篇文本按训练集5000、验证集500、测试集1000划分总共65000条数据。文本分类的任务就是给定一段新闻正文让模型判断它属于哪个类别。用TF-IDF把中文新闻转成向量再喂给sklearn里的分类器就是一个非常经典而且特别适合练手的方案。1. 项目到底是做什么的THUCNews与TF-IDF的适配性1.1 数据集背景与规模THUCNews这个数据集原始版本从新浪新闻的历史数据里清洗而来样本量非常大。不过日常做实验时大家常用的是抽出的子集按类别均匀抽样去掉长尾和重复内容整理成规整的文本文件。这个子集的好处在于类别均衡不需要做额外的样本均衡处理拿到手就能直接做有监督分类实验。10个类别分别是体育、娱乐、家居、房产、教育、时尚、时政、游戏、科技、财经覆盖面够广词表也足够丰富。每个类别6000多条样本对TF-IDF这种统计方法来说已经能学到足够有区分度的特征了。我自己实测下来用这套数据训练一个线性分类器准确率能做到95%以上训练时间也就几分钟性价比非常离谱。1.2 为什么选TF-IDF加sklearn而不是直接上深度学习很多人一上来就想着用BERT或TextCNN但对一个入门项目来说这是把简单问题复杂化。TF-IDF加sklearn有几个不可替代的优势首先是可解释性。TF-IDF是统计方法每一个特征都能对应到具体的词模型预测某条新闻属于“体育”我们能直接看哪些词起了关键作用比如“比赛”“进球”“球员”这些词权重高。深度学习学出来的是稠密向量可解释性就差很多。其次是资源友好。BERT要GPU要显存要漫长的训练时间。TF-IDF加逻辑回归用CPU就能在几分钟内完成训练调参迭代的速度快得多对刚开始理解机器学习流程的人来说非常友好。第三它训练出了一个很好的基准线。后续不管换什么模型都可以拿这个baseline做对比。我见过不少项目里深度学习模型跑出来的效果还不如调好参的TF-IDF加SVM不是深度模型不行而是数据量和调参没跟上。2. 数据准备与预处理从原始文本到干净语料2.1 数据集的目录结构与加载方式THUCNews子集常见的组织方式有两种。一种是每个类别一个txt文件比如cnews.train.txt这类单文件格式每行是一个样本标签和文本用制表符分隔。另一种是按目录组织根目录下每个类别的文件夹里存了该类别的多篇txt文件。我推荐用第一种因为加载代码更简洁内存和IO也都更可控。单文件格式长这样体育 中国女排今天在世界杯比赛中以3比0战胜对手... 娱乐 某电影发布会现场导演携主创团队亮相...加载方式很直接Python里按行读用split(\t)切出标签和内容。这里有个细节要提醒读文件时一定要指定编码THUCNews这个数据集在很多地方流传的副本是UTF-8编码但也有转成GBK的版本如果不指定encodingutf-8或encodinggbk很容易在跑数据时直接抛UnicodeDecodeError。2.2 中文分词TF-IDF绕不开的预处理步骤中文文本和英文最大的区别是词之间没有天然空格所以分词是绕不开的。TF-IDF这个模型本身以“词”为基本单位不分词直接用“字”做特征信息粒度太细效果会差很多。所以需要给文本加一个分词层。我用的jieba分词简单直接性能也够。加载数据时对每一条新闻做分词然后用空格把词拼接起来。这一步看起来平平无奇但有几个小细节一是建议把自定义词典加进去。THUCNews里面有大量专业术语比如“科创板”“区块链”“新能源汽车”这些词默认分词器可能会把它们切碎。jieba.load_userdict()可以加载自定义词典词表里每行一个词分词器会优先按整词切分对分类准确率的提升非常明显。二是要不要去停用词。停用词是“的了在是”这类没有实际含义的词在TF-IDF里它们的IDF值会很低对分类没有正向作用反而会稀释真正有区分度的特征。实际操作中我会准备一个常规的中文停用词表在分词后过滤一遍。三是分词后统一用空格拼接格式喂给TfidfVectorizer刚刚好。这个过程如果有需要也可以把英文、数字等统一处理不过THUCNews里这类内容占比不高不处理影响也不大。import jieba import jieba.analyse def cut_texts(texts, stopwords): 对文本列表做jieba分词并过滤停用词 cutted [] for text in texts: words jieba.lcut(text) words [w for w in words if w.strip() and w not in stopwords] cutted.append( .join(words)) return cutted2.3 训练集、验证集、测试集的正确打开方式THUCNews子集已经划分好了训练集、验证集和测试集这个划分直接沿用就好。但有一个很多人会忽略的点在切分数据或者做交叉验证时不能把验证集和测试集混用。验证集是用来调参的比如确定max_features该取多少、分类器的C该取多少这些决定都依据验证集的效果。测试集只能最后用一次用来评估模型的泛化能力。如果反复用测试集调参测试集就变成了隐形的验证集最终评估结果会虚高。这个思维习惯在做任何机器学习项目时都很重要。我在实验里的做法是训练集负责训练模型验证集负责调参测试集只做最终评估。因为THUCNews子集本身已经做了随机采样和类别均衡所以不需要我再做分层切分直接加载使用就行。3. TF-IDF特征构建与参数调优3.1 TF-IDF的数学原理与直觉理解TF-IDF全称是Term Frequency-Inverse Document Frequency也就是词频-逆文档频率。它衡量的是一个词对某篇文档的重要程度核心思想很直观如果一个词在当前的这篇新闻里出现得多但在整个语料的其他文档里出现得少那这个词就很有可能是这篇新闻的主题词。数学公式不复杂TF词频 某词在文档中出现的次数 / 文档的总词数表示词在当前文档中的出现频率IDF逆文档频率 log(语料库文档总数 / 包含该词的文档数 1)加1是为了平滑防止除数为0TF-IDF TF × IDF直接用这个乘积作为特征值这个公式的精髓在IDF这一项。像“的”“是”“了”这种词几乎每篇文档都有IDF值趋近于0乘出来的TF-IDF权重就很低。而像“进球”“上市”“房价”这类词只在特定类别的新闻里高频出现IDF值高权重也就拉上来了。TfidfVectorizer在实现时还做了平滑和归一化处理比如默认用L2范数对每一行的向量做归一化来消除文档长度差异对TF值的影响。这些细节反映到参数上就对应着smooth_idf和norm这些配置项理解原理之后看参数才不会懵。3.2 TfidfVectorizer核心参数配置实测用sklearn做TF-IDF核心就是TfidfVectorizer这个类。参数一看一大把但真正影响效果的其实就那几个我把实际测试的感觉逐一说出来tokenizer参数非常关键。sklearn默认按空格切词如果我们把jieba分词后的文本用空格拼好那默认切分就够了。但还有一种更优雅的写法自定义一个tokenizer函数内部调用jieba这样TfidfVectorizer就能直接接收原始文本内部自动分词。我自己更习惯后者代码可读性更高。需要注意自定义tokenizer返回的是分词后的词列表而不是拼接字符串。ngram_range控制特征粒度。默认是(1,1)只看单个词。把范围扩到(1,2)也就是把相邻两个词也作为特征比如“中国经济”这种组合词确实能提升一点准确率但特征的维度会成倍上涨内存和训练时间都跟着涨。THUCNews这个数据我就用(1,1)加bigram收益不大复杂度倒是涨了不少。max_features限制特征总数。把词表规模控制在5000到10000个训练速度明显提升准确率下降很小。原因很简单TF-IDF排序后排在后面的词都是低频词区分度很弱忽略掉反而是好事。min_df和max_df用来过滤极端词。min_df2表示只在1篇文档里出现的词直接不要这能过滤掉很多噪声max_df0.8表示在80%以上文档里都出现的词也不要这类词基本是停用词性质的通用词留着也是干扰。sublinear_tf这个参数值得试一下设为True时TF值用1 log(TF)代替原始数值可以压低高频词的影响。我在THUCNews上实测开启后准确率有小幅提升。from sklearn.feature_extraction.text import TfidfVectorizer import jieba def jieba_tokenizer(text): return jieba.lcut(text) vectorizer TfidfVectorizer( tokenizerjieba_tokenizer, ngram_range(1, 1), max_features10000, min_df2, max_df0.8, sublinear_tfTrue, norml2 ) X_train vectorizer.fit_transform(train_texts) X_val vectorizer.transform(val_texts) X_test vectorizer.transform(test_texts)3.3 特征维度与内存的现实约束做TF-IDF有一个无法回避的问题特征矩阵非常大。THUCNews训练集5万条样本按单字词粒度去构建词表词表规模轻松超过10万特征矩阵就是5万乘10万的规模。如果全部用float64存储内存占用就是50000 × 100000 × 8字节算下来要40GB直接爆内存。解决办法一是调max_features控制词表规模二是靠sklearn内部的稀疏存储。TfidfVectorizer默认返回的是稀疏矩阵只存非零元素的位置和值所以5万乘10万的矩阵实际占用的内存可能只有几百MB这是它能跑起来的关键。实践中有两个建议一是训练完特征提取器后可以查看vectorizer.vocabulary_的大小确认词表规模合理。二是如果数据量翻好几倍max_features也要跟着调大否则高频特征覆盖不了新语料信息损失会明显。THUCNews这个量级max_features10000是一个不错的默认值想精细调优可以在验证集上用网格搜索来定位最优值。4. 模型训练与效果评估朴素贝叶斯、逻辑回归与线性SVM4.1 三个适合TF-IDF特征的模型TF-IDF特征矩阵是高维稀疏向量大部分元素是0。决策树、K近邻这类模型在稀疏高维数据上表现很差随机森林这类bagging集成虽然能用但速度慢且效果不突出。真正适合TF-IDF稀疏特征的是线性模型和朴素贝叶斯我分别测试了三个朴素贝叶斯具体说是MultinomialNB。它基于词频做概率建模和TF-IDF这种词频类特征契合度很高训练速度极快几十秒就能完成是我在这个任务上第一个试的模型。缺点是精度上限略低对特征间的相关性假设太强真实文本里词和词之间是有明显关联的。逻辑回归也就是LogisticRegression。它是一个线性分类模型对稀疏特征非常友好训练快效果通常好于朴素贝叶斯而且可以通过调节正则化强度控制过拟合。THUCNews上逻辑回归能跑到95%以上的准确率。线性SVM也就是LinearSVC。在文本分类这个任务上LinearSVC一直有“好得离谱”的名声本质是线性分类器加合页损失配合L2正则化对TF-IDF特征有很强的适应力。它在THUCNews上的准确率通常比逻辑回归略高代价是训练时间略长。我最终选它作为主力模型但仅以微弱优势胜出。4.2 训练与评估完整流程训练部分的代码其实很简洁核心就三步加载数据、抽取特征、训练模型。我用一个统一的评估函数来对比三个模型输出准确率和分类报告这样效果差异一目了然。from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC from sklearn.metrics import accuracy_score, classification_report def train_evaluate(model, X_train, y_train, X_val, y_val, model_name): model.fit(X_train, y_train) pred model.predict(X_val) acc accuracy_score(y_val, pred) print(f[{model_name}] validation accuracy: {acc:.4f}) print(classification_report(y_val, pred, digits4)) models [ (MultinomialNB(), MultinomialNB), (LogisticRegression(max_iter1000), LogisticRegression), (LinearSVC(max_iter2000), LinearSVC) ] for model, name in models: train_evaluate(model, X_train, y_train, X_val, y_val, name)评估指标上因为THUCNews类别均衡准确率是很可靠的指标。但更全面的还是看分类报告里的精确率、召回率和F1值。有时候总体准确率看着不错某个类别的召回率却掉下来了比如“时尚”和“娱乐”类目容易互相混淆分类报告能把这类问题暴露得很清楚。4.3 调优路径与实际结果解读我在THUCNews验证集上的实测结果大致是这样的MultinomialNB准确率92%左右LogisticRegression 95%以上LinearSVC能到96%左右。这个成绩放在入门项目里已经相当能打而且考虑到特征提取只花了不到一分钟训练也就一两分钟性价比极高。继续往上提准确率的方法有几条路。一是把ngram_range扩到(1,2)通过词组特征捕捉局部语义准确率通常有一两个百分点的提升但特征维度成倍涨。二是调节模型自身的正则化参数比如逻辑回归的C值LinearSVC的C值用网格搜索在[0.1, 0.5, 1.0, 2.0]里找最优。三是针对混淆明显的类别做错误分析看是不是停用词过滤太狠把一些类别相关的功能词也误删了。我个人更推荐先把LinearSVC的C值调一遍这是成本最低的提分手段然后再考虑要不要上bigram。5. 常见问题排查与工程化落地建议5.1 高频报错与排查方法这个项目虽然简单但实操中能踩的坑不少我按出现频率列一下内存不足是常见问题特征矩阵规模太大导致的。处理办法是调低max_features或者检查sublinear_tf、norm这些参数是否正常工作。另一个常见问题是编码错误加载txt时报UnicodeDecodeError用encodinggbk重试通常能解决。分词不一致也是隐患用自定义tokenizer时训练和预测阶段必须走同一个函数否则特征维度对不上直接报错。预测阶段最容易出的问题就是特征维度不一致报错信息通常长这样意思是训练时词表里没有这些词无法完成映射。解决办法是预测时只调用vectorizer.transform()绝对不能再调用fit_transform()否则就会用新数据重新拟合词表导致特征空间完全对不上。# 正确复用训练好的向量化器 new_X vectorizer.transform(new_texts) # 错误重新fit导致特征维度不一致 new_X vectorizer.fit_transform(new_texts)5.2 模型保存与单条预测的完整链路验证集调参完成后要把保存模型和预测流程固化下来。这一步对工程化落地非常关键我通常把训练好的TfidfVectorizer和分类器放到同一个dict里用joblib一起保存避免分开管理导致版本不一致。import joblib # 保存完整pipeline pipeline { vectorizer: vectorizer, classifier: model } joblib.dump(pipeline, thucnews_tfidf_svc.pkl) # 单条新闻预测 def predict_news(texts): loaded joblib.load(thucnews_tfidf_svc.pkl) vec loaded[vectorizer].transform(texts) pred loaded[classifier].predict(vec) return pred加载模型预测单条新闻时有一个容易忽略的细节测试时传入的文本和训练时必须是同一种预处理方式。训练时每家新闻文本经过了jieba分词预测时如果不分词直接丢给模型向量化器会把整段文本当成一个词特征维度对不上预测结果毫无意义。所以预测函数里必须用同一个分词函数先处理输入文本。def preprocess(text): words [w for w in jieba.lcut(text) if w.strip() and w not in stopwords] return .join(words)5.3 从实验到工程化的一些想法这个项目跑通之后想让它更有工程价值可以往这几个方向扩展。一个是用Pipeline把向量化和分类器串成一条链这样整个流程更内聚调参和部署都更方便。另一个是在特征层面做文章比如在TF-IDF基础上叠加一些统计特征文本长度、标点密度、类别关键词命中数量等等。这些特征在新闻分类里也能贡献不少信息量。我在实际使用中还有一个感受模型上线后要定期监控预测结果的类别分布如果实际新闻内容分布和训练分布偏差太大要及时用新数据增量训练TF-IDF词表。词表是统计特征的根基语料变了词表不变效果一定会衰减。这也是TF-IDF方案相比深度学习模型的一个天然短板遇到剧烈分布变化时需要付出额外的维护成本。最后分享一个小技巧因为LinearSVC的决策函数是基于线性权重的我们可以把每个类别最重要的特征词打印出来看看模型到底学到了什么。这一步对排查模型问题非常有用它能直观看出来“房产”类的强特征是不是“房价”“贷款”“楼盘”如果强特征词和业务认知有明显偏差那数据和特征这一层一定有问题得回头检查。