Python文本分类系统实战:传统机器学习方法玩转TF-IDF与模型调优
简介这是一份面向Python初学者的文本分类实战源码包覆盖KNN、朴素贝叶斯、支持向量机、逻辑回归、决策树和随机森林六种经典机器学习算法。项目中文本预处理流程完整包含去空格、小写转换、分词、词性标注与词形还原并用TFIDF完成特征提取同时将训练与测试数据整理为CSV格式方便读者直接替换语料后运行classification.py复现结果。压缩包共8个文件以py脚本为核心、txt文本为数据集、csv为处理后数据另含README说明文档整体仅3.7MB轻量易用。已有69人学习下载。对于正在做课程设计或入门自然语言处理的学习者这份代码提供了从预处理、特征工程到多模型评估的完整链路可作为文本分类任务的可运行模板和对比实验基础。项目结构简洁训练与测试文件分离适合迁移到新闻分类、评论情感分析等场景。1. 别急着上深度学习传统文本分类依然能打拿到这个(源码)基于Python的文本分类系统.zip时我第一反应是看它的依赖清单和算法列表。拆开之后发现这套源码包的选型非常克制KNN、朴素贝叶斯、支持向量机、逻辑回归、决策树、随机森林清一色传统机器学习方法配合 TF-IDF 特征提取。在 2025 年这个时间点回看这类项目恰好踩在「业务落地够用」和「教学路线完整」的交界处。深度学习在长文本、大规模语料上的优势无需多言但当你面对几千条标注样本、没有 GPU 资源、业务方只要求一个可解释的基线模型时传统方法依然是性价比最高的起点。这篇拆解会沿着数据预处理、特征工程、模型评估、调参优化这条主线走一遍附带可直接复用的代码片段和参数参考适合想快速搭起文本分类基线、又不想被框架绑死的工程师。2. 从语料到 CSVformat.py 里的预处理管线2.1 源码包里先要理清的数据流展开压缩包文件清单并不复杂texts_prelabel.txt、trains.txt、tests.txt是原始语料trains_deal.csv和tests_deal.csv是预处理后的结构化数据format.py承担转换职责classification.py负责训练和评估。在实际跑classification.py之前先看texts_prelabel.txt的内部格式。常见做法是每一行存储一条样本格式为文本内容\t类别标签也可能直接以逗号分隔。这个脚本里没有采用通用的 DataFrame 接口读入原始文件而是先通过format.py做清洗和持久化再交给分类脚本使用。2.2 format.py 核心逻辑拆解一个合格的文本预处理流程至少要包含去除空格、统一小写、分词、词性标注、词形还原这几个环节。下面这段代码是对format.py核心逻辑的还原保留了原有的处理顺序import re import pandas as pd import nltk from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer from nltk.tag import pos_tag nltk.download(punkt) nltk.download(stopwords) nltk.download(averaged_perceptron_tagger) nltk.download(wordnet) def normalize_text(raw_text: str) - str: # 去除首位空格、折叠连续空白字符 text re.sub(r\s, , raw_text.strip()) # 统一转为小写避免大小写导致特征分裂 text text.lower() # 使用 nltk 的分词器切分单词 tokens nltk.word_tokenize(text) # 过滤停用词保留字母类 token tokens [t for t in tokens if t.isalpha() and t not in stopwords.words(english)] # 词性标注为词形还原提供词性上下文 tagged pos_tag(tokens) lemmatizer WordNetLemmatizer() lemma_tokens [] for word, tag in tagged: # 将 nltk 的 POS 标签映射为 wordnet 兼容的简化标签 pos map_pos_tag(tag) lemma_tokens.append(lemmatizer.lemmatize(word, pospos)) return .join(lemma_tokens) def map_pos_tag(treebank_tag: str) - str: if treebank_tag.startswith(J): return a # 形容词 elif treebank_tag.startswith(V): return v # 动词 elif treebank_tag.startswith(N): return n # 名词 else: return n # 默认按名词处理 def format_csv(input_file: str, output_file: str) - None: rows [] with open(input_file, r, encodingutf-8) as f: for line in f: # 此处假设原始语料用制表符分隔文本与标签 parts line.strip().split(\t) if len(parts) ! 2: continue text, label parts normalized normalize_text(text) rows.append({text: normalized, label: label}) df pd.DataFrame(rows) df.to_csv(output_file, indexFalse, encodingutf-8) if __name__ __main__: format_csv(trains.txt, trains_deal.csv) format_csv(tests.txt, tests_deal.csv)这段代码里最关键的不是正则或者大小写转换而是pos_tag与WordNetLemmatizer的配合。如果不做词性映射lemmatizer.lemmatize(studies)默认返回studies因为默认词性是名词而经过pos_tag识别为动词后lemmatize(studies, posv)才能正确还原为study。map_pos_tag里把动词、名词、形容词分别映射到v、n、a副词可以继续扩展但在这个源码包里没有单独处理默认归到名词。这个细节决定了词形还原的有效率直接复用的时候建议把副词映射补上。2.3 CSV 中间格式为什么要保留trains_deal.csv里只有text和label两列这种中间格式的价值在于预处理脚本只需要执行一次后续调参数、换模型都不需要重新做词形还原。我一般会把预处理和训练阶段彻底解耦尤其是在语料规模达到几十万条的层级重复跑预处理的时间成本不可忽略。分类脚本直接读取 CSV用pandas.read_csv加载之后切分特征和标签。3. TF-IDF 特征工程从词形还原后的文本到稀疏矩阵3.1 为什么选 TF-IDF 而不是词袋模型预处理之后的文本已经是一串干净的词序列表但分类器不能直接吃字符串。词袋模型CountVectorizer只统计词频问题在于高频的冠词、连词会淹没真正有区分度的词。TF-IDF 在词频基础上引入逆文档频率核心公式是tfidf tf * (log((1 n) / (1 df)) 1)其中n是文档总数df是包含该词的文档数。某个词在越少的文档里出现其 IDF 值越高意味着它越能代表当前文档的主题倾向。在classification.py里调用TfidfVectorizer时常见的参数组合如下from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, # 最多保留 5000 个特征防止维度爆炸 ngram_range(1, 2), # 同时使用单词和相邻二元词组 min_df2, # 至少在 2 篇文档中出现过才会被纳入 max_df0.8, # 超过 80% 文档出现的词视为停用词 sublinear_tfTrue # 对词频做 log1p 压缩抑制高频词 ) X_train vectorizer.fit_transform(train_df[text]) X_test vectorizer.transform(test_df[text])每个参数都值得细看。max_features直接决定向量维度文本分类场景里 5000 到 20000 是比较常见的区间太小会丢失长尾特征太大会让矩阵稀疏到几乎无法训练。ngram_range(1, 2)是在向「词汇顺序」妥协模型无法真正理解语序但二元词组可以捕捉部分局部语义比如not good和good在词袋视角下没有区别加入二元词组后这个差异就能体现出来。min_df是下限过滤只出现过一次的词通常是拼写错误或噪音但对小数据集要小心可能把真实的有区分度的词也过滤掉。sublinear_tfTrue是我在这个源码包里看到的一个加分项它把原始词频替换为1 log(tf)削弱单个词在同一文档内反复出现带来的偏置。3.2 fit_transform 与 transform 的使用陷阱注意.fit_transform()只用于训练集测试集必须用已经拟合好的 vectorizer 调用.transform()这样才能保证训练集和测试集的特征空间完全一致。常见误用是在测试集上重新fit_transform()这会导致词表不同、列数不匹配模型在 predict 时会报维度错误。另外向量化输出是稀疏矩阵直接打印会是(行数, 列数)的坐标格式不要试图用肉眼查看内容用vectorizer.get_feature_names_out()可以取出特征词的映射顺序排查特征是否合理。3.3 中文场景下的替代方案这个源码包里的word_tokenize和stopwords.words(english)是英文语料的处理方式。如果是中文文本需要替换为 jieba 分词并把停用词表改为中文停用词表其他流程词性标注、去重依然适用。TF-IDF 不区分语言只关心分词结果。4. 六种分类器的训练与评估从 KNN 到随机森林4.1 classification.py 的主流程classification.py承担了从数据加载到模型对比的完整链路。去掉无关输出后主干逻辑如下import pandas as pd from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import MultinomialNB from sklearn.svm import LinearSVC from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report train_df pd.read_csv(trains_deal.csv, encodingutf-8) X vectorizer.fit_transform(train_df[text]) y train_df[label] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) models { knn: KNeighborsClassifier(n_neighbors5), naive_bayes: MultinomialNB(alpha1.0), svm: LinearSVC(C1.0), logistic_regression: LogisticRegression(max_iter1000, C1.0), decision_tree: DecisionTreeClassifier(max_depth10, random_state42), random_forest: RandomForestClassifier( n_estimators100, max_depth15, random_state42, n_jobs-1 ) } for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_val) acc accuracy_score(y_val, y_pred) print(f{name}: {acc:.4f}) print(classification_report(y_val, y_pred, zero_division0))train_test_split里有两个值得注意的参数。stratifyy保证划分后训练集和验证集的类别分布与原数据集一致避免某类样本全部落到验证集里这在分布不均衡的分类任务里直接决定评估结果可信度。random_state42固定随机种子让每次运行结果可复现这也是源码包里留下的一个good practice。zero_division0是给那些可能在验证集缺失类别的场景兜底。4.2 每种模型在这个场景下的定位KNN 的原理是「物以类聚」文本向量化后的高维稀疏空间里KNN 的距离计算退化得比较厉害但作为基线足够了。n_neighbors5是经验值之后可以通过网格搜索验证。MultinomialNB 是文本分类里最经典的朴素贝叶斯变体alpha1.0是拉普拉斯平滑参数防止某个特征词在所有训练样本中都没有出现为零概率。LinearSVC 的本质是一个带 L2 正则的线性 SVM在文本分类上通常以极低的调参成本拿到接近最优的结果C越小正则越强。LogisticRegression 在文本分类中同样表现稳定但注意max_iter1000这个参数默认值 100 在特征维度较高时经常不收敛会报警告如果遇到收敛问题优先调大它。决策树的max_depth10是控制深度的正则化手段纯决策树在这个场景下很容易过拟合。随机森林通过多棵树的投票降低方差n_estimators100是一个时间成本与效果的平衡点再多到 300 提升有限。4.3 评估指标不能只看准确率分类脚本里输出了准确率和classification_report后者包含了 precision、recall、f1-score。准确率在类别均衡时够用一旦类别分布偏斜某个占比高的类别会主导准确率此时需要用 macro avg 或 weighted avg 的 f1-score 来判断。一个具体的排查场景训练集里0类占 80%模型全预测为0类也能拿到 80% 准确率。如果classification_report里某个类别 f1-score 明显偏低优先确认该类别样本量再考虑要不要做类别重采样或给模型加class_weightbalanced。4.4 六种算法的典型表现区间参考模型准确率大致区间训练速度可解释性主要风险KNN0.70 ~ 0.85极快预测慢高高维稀疏下距离退化朴素贝叶斯0.80 ~ 0.90极快高特征独立性假设过强线性SVM0.85 ~ 0.95快中需要特征标准化逻辑回归0.85 ~ 0.95快高线性边界局限决策树0.75 ~ 0.85快极高易过拟合随机森林0.85 ~ 0.95中等中参数量大需调优这个表不会告诉你这六种模型谁是冠军但会告诉你没必要纠结模型复杂度在 TF-IDF 特征下线性模型和随机森林的差距通常只有几个百分点优先跑通逻辑回归和 SVM 拿到上线基线再花时间调随机森林。5. 用交叉验证与网格搜索压榨分类器性能5.1 交叉验证取代单次划分train_test_split只做一次数据切分评估结果高度依赖划分方式。更稳妥的方案是用cross_val_score做 K 折交叉验证把数据切成 K 份轮流用其中 K-1 份训练、剩余 1 份验证取 K 次结果的平均值from sklearn.model_selection import cross_val_score for name, model in models.items(): scores cross_val_score(model, X, y, cv5, scoringf1_macro, n_jobs-1) print(f{name}: {scores.mean():.4f} ± {scores.std():.4f})cv5在几百到几万条样本的数据集上是比较通用的默认值样本量再大可以降到 3样本量很小可以用 10 或者做留一法但后者计算成本极高。scoringf1_macro比默认的 accuracy 更抗类别不均衡。输出结果里的均值加标准差才是最完整的评估信息比如0.9102 ± 0.0134表示模型不仅平均表现好而且在不同数据子集上表现稳定。5.2 网格搜索定位最优参数交叉验证解决的是「评估可靠」的问题网格搜索解决的是「参数怎么选」的问题。以线性 SVM 和随机森林为例from sklearn.model_selection import GridSearchCV param_grid_svm { C: [0.1, 1.0, 10.0], loss: [hinge, squared_hinge] } param_grid_rf { n_estimators: [100, 200], max_depth: [10, 15, 20], min_samples_split: [2, 5] } grid_svm GridSearchCV(LinearSVC(), param_grid_svm, cv3, scoringf1_macro, n_jobs-1) grid_svm.fit(X_train, y_train) print(grid_svm.best_params_, grid_svm.best_score_) grid_rf GridSearchCV(RandomForestClassifier(), param_grid_rf, cv3, scoringf1_macro, n_jobs-1) grid_rf.fit(X_train, y_train) print(grid_rf.best_params_, grid_rf.best_score_)这里的C列表跨度是十倍的指数区间先粗搜定位量级再在最优量级附近细搜。losshinge对应标准 SVMsquared_hinge则是一个平滑可导的替代收敛更快但分类面略有不同。网格搜索的时间成本是「参数组合数 × 折数 × 单次训练时间」的乘积所以参数网格不要一开始就上全组合先固定其他参数调一个维度找到敏感参数后再联合调。5.3 最后收个尾如果觉得网格搜索还是慢可以用RandomizedSearchCV替代它在参数空间中随机采样指定次数比如n_iter50在超参维度较多时能在同样时间内探索到更广的区间。完成了这一步文本分类系统的另一个关键优化点是特征维度消减比如对TfidfVectorizer的max_features做消融实验看准确率在什么维度收敛记录的曲线可以避免无谓的算力浪费。提示跑classification.py之前先确认 Python 环境已安装 pandas、numpy、scikit-learn 和 nltk缺失依赖时直接pip install即可。整个流程里最值得花时间的不是调参而是观察预处理管线的每一步输出——先看懂trains_deal.csv长什么样再谈模型优化。本文还有配套的精品资源点击获取