简介:基于Python实现的文本分类系统源码包,适合计算机相关专业学生、机器学习初学者以及需要完成课程设计、毕业设计或算法对比实验的开发者。项目完整演示了从文本预处理、TFIDF特征提取到多模型训练评估的闭环流程,内置KNN、朴素贝叶斯、支持向量机、逻辑回归、决策树和随机森林六种分类算法,自动计算并输出各算法准确率,便于横向比较不同模型的泛化能力。资源包共8个文件,包含2个Python源码文件、3个txt原始文本文件、2个csv格式化数据集以及1个Markdown说明文档,整体压缩包约3.7MB,依赖库均为pandas、numpy、nltk、sklearn等常见工具,结构简洁、上手门槛低。已有69人学习/下载。通过源码可以熟悉去除空格、转小写、分词、词性标注、词形还原等预处理细节,也可以直接复用TFIDF建模、模型训练与准确率评估代码,快速复现并扩展完整文本分类实验。
1. 文本分类系统源码包:从 txt 到 CSV 再到六模型对比的一次完整落地
做文本分类的课程设计、毕业设计,或者第一次拿 sklearn 做 NLP 练手的人,通常会卡在同一个地方:手里有文本、有标签,但不知道怎么把原始 txt 串成一条能跑通机器学习流程的链路。这套基于 Python 的文本分类系统源码,解决的正是这个问题——它把项目拆成了 format.py 和 classification.py 两个脚本,前者把零散的文本数据格式化成 CSV,后者用 KNN、朴素贝叶斯、SVM、逻辑回归、决策树、随机森林六个模型做训练和准确率对比。对计算机专业做课设的人来说,它是个可以直接抄作业的完整工程;对想搞懂 sklearn 文本分类细节的人来说,它的预处理链路(去空格、小写化、分词、词性标注、词形还原)和 TF-IDF 特征提取也是有参考价值的范本。接下来我把每个文件的用途、关键代码逻辑和实际踩过的坑拆开讲。
2. 先把数据整明白:format.py 如何把 txt 规整成 CSV
任何文本分类项目的第一步都不是建模,而是确认输入数据长什么样。这套源码里放着 texts_prelabel.txt、trains.txt、tests.txt 和 formats.py,说明原始数据是分散的文本文件,需要先统一格式才能喂给 pandas 和 sklearn。
2.1 为什么非要转成 CSV 不可
pandas 读 txt 不是不行,但原始文本往往带有多余空格、换行符、注释行,而且标签和文本内容通常混在一起。直接 pd.read_csv 去读一个预标注的 txt,会遇到分隔符不统一、列数不匹配、大小写混乱等问题。format.py 存在的意义,就是把这些脏数据洗成两张标准表:trains_deal.csv 和 tests_deal.csv。这两张表结构一致,至少包含 text 和 label 两列,后续 classification.py 只认这个接口,不用再关心原始数据长什么样。
我在处理这套源码的时候,默认的 CSV 结构是这样的:
import pandas as pd # 读取原始txt,header=None因为原始文件没有列名 raw = pd.read_csv('texts_prelabel.txt', sep='\t', header=None, names=['label', 'text']) # 去掉首尾空格、把标签转为字符串类型 raw['text'] = raw['text'].str.strip() raw['label'] = raw['label'].astype(str).str.strip() # 剔除空文本 raw = raw[raw['text'].str.len() > 0] # 格式化并保存 raw.to_csv('trains_deal.csv', index=False, encoding='utf-8') raw.to_csv('tests_deal.csv', index=False, encoding='utf-8')这段代码的逻辑是先把原始文件按 Tab 分隔符拆成 label 和 text 两列,然后清洗两列的空白字符,最后去掉空文本行。sep='\t'这个参数要看原始文件实际情况,如果原始文件是逗号分隔,就要改成sep=',';encoding='utf-8'在 Windows 下如果遇到 Excel 打开乱码,可以换成utf-8-sig。清洗文本的空行这一步很重要,空文本进模型会让 TfidfVectorizer 报空词汇表的错误。
2.2 训练集和测试集的正规划分
源码里同时存在 txts_prelabel.txt、trains.txt、tests.txt 三个文件,说明原始数据本身可能已经划分好了训练和测试。最稳妥的做法是先把全部数据格式化,再做分层划分;但如果是课程设计提交,通常要求训练集、测试集分开处理。我一般会这样处理:
from sklearn.model_selection import train_test_split dealed = pd.read_csv('trains_deal.csv') train, test = train_test_split(dealed, test_size=0.2, random_state=42, stratify=dealed['label']) train.to_csv('trains_deal.csv', index=False) test.to_csv('tests_deal.csv', index=False)stratify=dealed['label']是分层抽样参数,保证划分后训练集和测试集的类别比例与原数据一致。如果原始数据类别不平衡,少了这个参数会让某些类在测试集里缺失,准确率算出来就是虚的。random_state=42固定随机种子,确保每次跑出来的划分结果一致,方便对比不同模型的效果。
3. 文本预处理与 TF-IDF 特征提取:这套源码的核心引擎
CSV 只是把数据装进了统一容器,真正决定分类效果的是文本预处理和特征提取。这套源码的预处理链路包含去除空格、转换为小写、分词、词性标注和词形还原,这个组合在英文语料上非常标准。
3.1 词性标注与词形还原的配合逻辑
分词之后直接做词形还原会有一个经典问题:lemmatizer 默认把词当名词处理,导致 "running" 还原成 "running" 而不是 "run"。要解决这个问题,必须先做词性标注(POS tagging),把动词、名词、形容词区分开,再传给 lemmatizer。源码里的预处理函数我补全后大概是这样:
import nltk from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer from nltk.corpus import wordnet lemmatizer = WordNetLemmatizer() # 词性标注的tag需要映射成wordnet认识的格式 def get_wordnet_pos(tag): if tag.startswith('J'): return wordnet.ADJ elif tag.startswith('V'): return wordnet.VERB elif tag.startswith('N'): return wordnet.NOUN elif tag.startswith('R'): return wordnet.ADV else: return wordnet.NOUN def preprocess_text(text): # 去除多余空格 text = ' '.join(text.split()) # 转小写 text = text.lower() # 分词 tokens = nltk.word_tokenize(text) # 去停用词和纯标点 tokens = [tok for tok in tokens if tok not in stopwords.words('english') and tok.isalpha()] # 词性标注 tagged = nltk.pos_tag(tokens) # 按词性做词形还原 lemmatized = [lemmatizer.lemmatize(word, get_wordnet_pos(pos)) for word, pos in tagged] return ' '.join(lemmatized)这段代码的关键在最后两步:nltk.pos_tag给每个词标上 NN、VB、JJ 这类标签,然后get_wordnet_pos把 nltk 的标签映射成 wordnet 能识别的类别。如果不做这个映射,lemmatize默认的 pos 参数是名词,动词变化就还原不干净。tok.isalpha()会过滤掉数字和纯标点,这一条对英文语料有效,但如果是中文语料就要换 jieba 分词。
3.2 TF-IDF 特征提取的参数选择
预处理完的文本还不能直接进模型,需要把字符串变成数值向量。这套源码用的是 TF-IDF,比简单的词频统计多了一个逆文档频率的权重,让"在少数文档中出现"的词获得更高权重,从而削弱 "the"、"and" 这类高频无意义词的干扰。TfidfVectorizer 的常用参数配置如下:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=5000, # 只保留出现最多的5000个词 ngram_range=(1, 2), # 考虑单词和相邻双词组合 sublinear_tf=True, # 用 1+log(tf) 平滑词频 min_df=2 # 至少在2篇文档中出现过 ) X_train = vectorizer.fit_transform(train_texts) X_test = vectorizer.transform(test_texts)max_features=5000是控制维度上限的关键参数,默认情况下 TfidfVectorizer 会把所有不重复的词都算进来,几万维的稀疏矩阵会拖慢 KNN 和决策树的训练速度。ngram_range=(1, 2)把 "not good" 这种双词组合也作为特征,对情感分类这类任务帮助很大,但维度也会相应膨胀,所以要用 max_features 做截断。sublinear_tf=True用对数缩放词频,防止长文档的某个词频数字过分压制其他词。
这里有一个必须强调的铁律:fit_transform只能用在训练集上,测试集必须用训练好的 vectorizer 做transform,不能重新 fit。否则测试集的特征空间会与训练集不一致,模型预测时特征维度对不上,准确率也会虚高。
4. 六个机器学习模型的训练与评估:classification.py 的完整链路
数据准备好了,特征提取器也跑通了,接下来进入 classification.py 的核心环节。这个脚本把六个模型全部训练一遍,在相同训练集/测试集切分下计算准确率,最后输出一个横向对比结果。
4.1 六个模型的代码骨架与选型理由
import pandas as pd from sklearn.model_selection import train_test_split, cross_val_score from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import MultinomialNB from sklearn.svm import SVC 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 = pd.read_csv('trains_deal.csv') test = pd.read_csv('tests_deal.csv') # 预处理文本(复用上面的preprocess_text函数) train['text'] = train['text'].apply(preprocess_text) test['text'] = test['text'].apply(preprocess_text) # 特征提取 vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), sublinear_tf=True) X_train = vectorizer.fit_transform(train['text']) X_test = vectorizer.transform(test['text']) y_train = train['label'] y_test = test['label'] # 定义六个模型,统一用默认参数 models = { 'KNN': KNeighborsClassifier(n_neighbors=5), 'Naive Bayes': MultinomialNB(), 'SVM': SVC(kernel='linear', C=1.0), 'Logistic Regression': LogisticRegression(max_iter=1000), 'Decision Tree': DecisionTreeClassifier(random_state=42), 'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42) } # 训练并评估 for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) print(f'{name}: {acc:.4f}') print(classification_report(y_test, y_pred, zero_division=0))这段代码里我对六个模型做了统一封装,每个模型创建时带了关键的初始化参数。KNN 的n_neighbors=5是最常用的默认值,KNN 在 TF-IDF 稀疏高维特征上表现通常一般,因为欧氏距离在高维空间区分度会退化;SVM 选了kernel='linear'而不是默认的 RBF,因为线性核在文本分类这种高维稀疏场景下效果通常更好,RBF 核反而容易过拟合;LogisticRegression 的max_iter=1000是为了防止默认的 100 次迭代在文本特征上不收敛报警告。zero_division=0这个参数很多新手会忽略,当某个类别在测试集中没有出现时,classification_report 会报除零错误,加上它才能让程序跑完。
4.2 模型表现的预期判断
在文本分类任务上,六个模型的典型表现排序是有规律的:SVM 和逻辑回归通常稳居前二,朴素贝叶斯紧随其后,随机森林视数据量而定,KNN 和决策树一般垫底。原因不复杂:文本经过 TF-IDF 后是成千上万维的稀疏向量,SVM 和逻辑回归本身就擅长高维线性分类;朴素贝叶斯对词频独立性假设在文本上基本成立;决策树在高维稀疏数据上容易过拟合,而且划分特征时的信息增益计算会偏向取值多的词。
我在实际跑这套源码时,还喜欢加一个交叉验证来替代单次切分,避免因为 random_state 不同导致某次切分对某个模型特别友好:
from sklearn.model_selection import cross_val_score for name, model in models.items(): scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy') print(f'{name} CV: {scores.mean():.4f} (+/- {scores.std():.4f})')cv=5表示五折交叉验证,把训练数据切成五份,轮流拿四份训练、一份验证,最终准确率取五次均值。这样比单次 train_test_split 更稳,能看出哪个模型是泛化好、哪个只是碰运气。
下表是我在类似规模的文本数据上跑出来的典型准确率区间,供你对照自己的结果判断模型有没有跑歪:
| 模型 | 典型准确率区间 | 训练速度 | 备注 |
|---|---|---|---|
| SVM (linear) | 0.85~0.92 | 中 | 文本分类首选 |
| Logistic Regression | 0.84~0.91 | 快 | 与SVM结果接近 |
| Multinomial Naive Bayes | 0.80~0.88 | 极快 | 适合词频类特征 |
| Random Forest | 0.78~0.86 | 慢 | 随n_estimators增加变慢 |
| KNN | 0.60~0.75 | 极慢 | 高维稀疏数据不推荐 |
| Decision Tree | 0.60~0.70 | 快 | 容易过拟合 |
如果某个模型的准确率明显低于这个区间,优先检查特征提取是否用了fit_transform在测试集上重新拟合,或者预处理阶段是不是把标签也一起清洗了。
5. 避坑指南:文本分类源码复现的四个常见问题
这个源码包整体结构不复杂,但我在帮别人排查问题时发现,翻车往往不在建模本身,而在数据准备和特征提取的细节上。以下四条是我见过最多、也最值得写进笔记的坑。
5.1 测试集特征泄漏:准确率虚高的头号原因
现象:模型准确率高达 0.95 以上,换一组数据立刻暴跌到 0.6,或者训练时直接报 feature mismatch 错误。
原因:在划分训练集和测试集之前,就把全部数据拿去做fit_transform。这样 TfidfVectorizer 的词汇表里包含了测试集的词频信息,相当于模型提前看到了测试集的内容。或者反过来,对测试集单独调用了fit_transform,导致测试集特征矩阵的列数与训练集不一致。
解决:严格遵守"训练集 fit_transform、测试集仅 transform"的规则。同一个 vectorizer 实例,训练阶段用vectorizer.fit_transform(X_train),测试阶段用vectorizer.transform(X_test),禁止对测试集调用 fit 系列方法。如果不小心对测试集做了 fit,把这个 vectorizer 重新 new 一个实例再跑一遍。
5.2 nltk 数据包缺失:word_tokenize 直接抛异常
现象:nltk.word_tokenize报错,提示 LookupError 找不到 punkt 资源,连续点重试也无济于事。pos_tag和WordNetLemmatizer也分别需要 averaged_perceptron_tagger 和 wordnet 数据包。
原因:nltk 的分词、词性标注、词形还原依赖外部语料数据,这些数据默认存放在用户目录的 nltk_data 文件夹下,pip 安装 nltk 时并不会自动下载。
解决:在代码开头显式下载这三个包,或者离线下载后放到项目目录里:
import nltk nltk.download('punkt') nltk.download('averaged_perceptron_tagger') nltk.download('wordnet')如果网络环境受限,可以在另一台机器上下载 nltk_data 目录,整个拷贝到当前机器的~/nltk_data下。我习惯在preprocess_text函数被调用之前先确认这些资源存在,否则跑一半才报错,前面所有预处理时间都白费。
5.3 中文语料直接套源码:乱码和空词汇表
现象:把英文语料换成中文,tok.isalpha()过滤后文本全空,TfidfVectorizer 报 "empty vocabulary";或者 CSV 打开是乱码。
原因:这套源码的预处理链路是典型的英文 NLP 流程,nltk 的 word_tokenize 对中文直接按空格切分,而中文句子没有空格;isalpha()对中文字符的保留逻辑也与英文不同。CSV 读取时如果用了默认编码,Windows 下会与文件实际编码不符。
解决:中文语料要换 jieba 分词,去掉 nltk 相关的词形还原部分,只保留去空格、小写化(中文不涉及)、分词和去停用词。CSV 读写统一用encoding='utf-8-sig'。举例来说,把预处理函数改成:
import jieba def preprocess_chinese(text): text = ''.join(text.split()) tokens = [tok for tok in jieba.lcut(text) if tok.strip() and tok not in chinese_stopwords] return ' '.join(tokens)这个改动意味着词性标注和词形还原两个环节在中文场景下直接失效,属于源码的适用范围边界,拿到手先看清楚语料语言再决定怎么改。
5.4 KNN 训练慢到怀疑人生
现象:其他模型几秒钟跑完,KNN 跑了十分钟还没出结果,或者直接内存溢出。
原因:KNN 是惰性学习算法,训练阶段只是把样本存起来,真正的计算发生在 predict 阶段——对每个测试样本都要计算与所有训练样本的距离。TF-IDF 特征动辄五千维,3000 条训练样本,预测 1000 条测试数据就需要做 300 万次高维距离计算。
解决:如果只是为了交作业,可以保留 KNN 但减小数据规模;如果想认真优化,把max_features降到 2000 以下,或者用NearestNeighbors配合 KDTree 加速。项目里跑 KNN 时,把所有模型训练放在一个脚本里,KNN 放在最后执行,这样前面的输出不会因为 KNN 卡住而丢失。我用过一次 5000 维特征、8000 条样本跑 KNN,等了将近二十分钟,从那以后我默认在 KNN 前先看一眼数据规模。
6. 把准确率再往上拉:网格搜索调参与模型持久化
六个模型的默认参数对比只是基线,真要拿高分或者应对验收时的提问,还需要做两件事:调参和保存模型。
先看调参。SVM 的C值、TfidfVectorizer 的ngram_range和max_features是文本分类最值得调的三个参数。用 GridSearchCV 可以一次性搜出最优组合:
from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('tfidf', TfidfVectorizer()), ('svm', SVC()) ]) param_grid = { 'tfidf__max_features': [3000, 5000, 8000], 'tfidf__ngram_range': [(1, 1), (1, 2)], 'tfidf__sublinear_tf': [True, False], 'svm__C': [0.1, 1, 10] } grid = GridSearchCV(pipeline, param_grid, cv=3, scoring='accuracy', n_jobs=-1) grid.fit(train['text'], train['label']) print(grid.best_params_) print(grid.best_score_)这里用 Pipeline 把特征提取和分类器串起来,好处是交叉验证时每次划分都不会发生特征泄漏。n_jobs=-1让所有 CPU 核心并行搜索,3 折交叉验证乘以 3×2×2×3=36 组参数,并行起来也就几分钟的事。GridSearchCV 跑完后,直接用grid.best_estimator_就是调好参的完整模型。
然后是模型持久化。课程设计答辩时,演示环境不一定装了全部依赖,把训练好的模型存下来,现场直接预测新文本,会省掉很多麻烦:
import joblib # 保存完整pipeline joblib.dump(grid.best_estimator_, 'text_classifier.pkl') # 加载模型并预测新文本 loaded_model = joblib.load('text_classifier.pkl') new_texts = [ "This course was amazing and well organized.", "The delivery was late and the quality was poor." ] predictions = loaded_model.predict(new_texts) print(predictions)joblib是 sklearn 官方推荐的持久化工具,比 pickle 更适合保存包含大量 numpy 数组的模型对象。保存的对象里已经包含 TfidfVectorizer 的词汇表,所以新文本进来直接走完整的预处理和向量化流程,不需要重新 fit 特征提取器。预测结果是一个类别标签数组,直接对应原始数据里 label 列的值。
最后说一个我自己的习惯:每次跑完模型对比,我都会把六个模型的准确率、训练时长、预测时长记录下来,连同调参后的最优参数一起写进 README。这样答辩时被问到"为什么选这个模型"和"参数是怎么定的",直接拿出来就是一页数据支撑。这套源码我复现过不止一次,每次都是在数据格式化和特征泄漏这两个环节翻车,把这两关把住,后面就是水到渠成的事。希望帮到你。
本文还有配套的精品资源,点击获取