简介:这份源码资源面向具备一定Python基础、希望入门中文短文本分类的开发者与学习者,围绕新闻文本分类任务提供了一套可运行的实践框架,用于对比传统机器学习与深度学习方法在短文本场景下的表现差异。资源包共9个文件,以txt数据文件、py源码脚本和md说明文档为主,压缩包约1.99MB,其中数据文件承载训练与测试语料,脚本负责模型构建与流程调度,说明文档则帮助读者快速理解项目结构。项目采用搜狗实验室新闻数据集,深度学习部分基于keras,传统机器学习依托sklearn,并引入word2vec预训练词向量,参与比较的方法涵盖SVM、SVM+word2vec、LSTM、LSTM+word2vec、MLP、KNN与朴素贝叶斯等。已有58人学习,读者可借此获得一套完整的中文短文本分类实验方案,理解不同算法在相同语料上的效果差异,并在此基础上迁移到情感分析、标题分类等相近任务中。
1. 拆开这份 Python 新闻文本分类源码:从数据到模型的完整链路
搜「Python 新闻文本分类系统 源码」的人,多半不是想从零学 NLP,而是手里已经有一批中文新闻语料,想找个能跑通的框架,把 SVM、LSTM、朴素贝叶斯这些方法横向比一遍,看看哪种在自己场景下更靠谱。这份基于 Python 的新闻文本分类系统源码,正好切中这个需求:它用搜狗实验室的新闻数据集做训练和测试,深度学习侧走 Keras,传统机器学习侧走 sklearn,预训练词向量用 word2vec,参与对比的方法覆盖 SVM、SVM+word2vec、LSTM、LSTM+word2vec、MLP、KNN 和朴素贝叶斯。换句话说,它不是一个「调包跑个 demo」的玩具,而是一套把中文短文本分类从数据预处理到多模型对比串起来的工程骨架。适合谁?适合已经会 Python、装过 numpy 和 sklearn、想拿一份能改能扩的源码当起点的从业者。下面我按自己拆包的顺序,把这份资源从结构、跑通、参数到踩坑讲透。
2. 源码结构与数据格式:先看清每个文件在干什么
2.1 目录树与文件职责
拿到压缩包解压后,根目录下大致是这么几块:main.py是入口,utils.py放公共函数,src目录装核心逻辑,data目录放数据,根目录还有requirements.txt和README.md。数据文件是四个 txt:train_contents.txt、train_labels.txt、test_contents.txt、test_labels.txt。这个「内容与标签分离成两个文件、按行对齐」的设计,是中文文本分类里很常见的做法,好处是标签可以单独替换、内容可以单独清洗,坏处是行数一旦对不上就会静默错位,后面会专门讲这个坑。
先看requirements.txt,它决定了你要装哪些依赖。常见内容是 Keras、TensorFlow、scikit-learn、gensim、numpy、jieba 这几类。我一般不会直接pip install -r,而是先看一眼版本约束,因为 Keras 和 TensorFlow 的版本耦合是出了名的玄学,2.x 和 1.x 的 API 差异能把main.py直接跑挂。
# 先看依赖清单,别急着装 cat requirements.txt # 建议在虚拟环境里操作,避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 如果 requirements 没锁版本,手动指定一组能跑的 pip install numpy scikit-learn gensim jieba pip install tensorflow==2.10.0 keras==2.10.0这里的关键参数是 TensorFlow 与 Keras 的版本对应关系。TF 2.10 自带 Keras 2.10,是最后一个「Keras 作为 TF 子模块且 API 相对稳定」的版本区间,再往上 Keras 3 会改掉不少层调用方式。如果你装完 import 就报cannot import name 'xxx' from 'keras',八成是版本错配,退回 2.10 附近通常能解决。
2.2 数据文件的格式约定
四个 txt 的格式必须理解清楚,否则后面所有模型都白跑。train_contents.txt每行是一条新闻正文(可能已经分好词,也可能是原始句子),train_labels.txt每行是对应类别标签,两个文件行数必须严格相等,第 N 行内容对应第 N 行标签。test_*同理。常见做法是标签用整数或类别名,比如「体育」「财经」「科技」。
# 读取并校验数据对齐,这是跑任何模型前的第一步 def load_data(content_path, label_path): with open(content_path, encoding='utf-8') as f: contents = [line.strip() for line in f if line.strip()] with open(label_path, encoding='utf-8') as f: labels = [line.strip() for line in f if line.strip()] # 行数不一致直接报错,别让它静默错位 assert len(contents) == len(labels), \ f"内容{len(contents)}行 与 标签{len(labels)}行 不匹配" return contents, labels train_x, train_y = load_data('data/train_contents.txt', 'data/train_labels.txt') test_x, test_y = load_data('data/test_contents.txt', 'data/test_labels.txt') print(f"训练集 {len(train_x)} 条,测试集 {len(test_x)} 条")这段代码的逻辑很直白:分别读两个文件,去掉空行,然后断言行数相等。参数上,encoding='utf-8'是中文语料的底线,用默认编码在 Windows 上大概率乱码;if line.strip()过滤空行是为了避免空样本干扰词向量训练。逻辑说明:一旦断言失败,说明你的数据在下载或解压环节被截断了,这时候不要硬跑,先回去核对原始数据。
提示:搜狗新闻数据集原始格式和这里拆好的 txt 不一定完全一致,如果你用的是自己爬的语料,务必先转成「一行一条、内容标签分文件」的格式再喂进来。
3. 从分词到词向量:中文短文本预处理的实操细节
3.1 分词与停用词处理
中文和英文最大的区别是没有天然空格,所以第一步必须分词。这份源码里 word2vec 相关的方法依赖分词结果,常见做法是用 jieba。分词质量直接决定词向量的质量,进而决定 SVM+word2vec、LSTM+word2vec 这些方法的上限。
import jieba # 加载停用词表,没有就自己攒一份 def load_stopwords(path='data/stopwords.txt'): try: with open(path, encoding='utf-8') as f: return set(line.strip() for line in f) except FileNotFoundError: # 兜底:至少过滤掉高频无意义词 return set(['的', '了', '在', '是', '我', '有', '和', '就']) stopwords = load_stopwords() def cut_text(text): # 精确模式分词,过滤停用词和单字 words = jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) > 1] # 对全量语料分词 train_cut = [cut_text(t) for t in train_x] test_cut = [cut_text(t) for t in test_x] print(train_cut[0][:20]) # 看一眼分词结果逻辑说明:jieba.lcut返回列表,精确模式适合新闻这种规范文本;过滤停用词和长度为 1 的字,是因为单字在短文本里噪声大、区分度低。参数上,len(w) > 1这个阈值可以调,如果你的语料里有很多有意义的单字(比如「涨」「跌」),可以放宽到>= 1。这一步做完,建议先打印几条看看,分词切错了后面全白搭。
3.2 word2vec 词向量训练与句向量构造
源码里 SVM+word2vec、LSTM+word2vec 的核心,是先在大规模语料上训练词向量,再把每条新闻的词向量聚合成句向量(或直接喂给 LSTM 序列)。gensim 的 Word2Vec 是标准选择。
from gensim.models import Word2Vec import numpy as np # 训练词向量,size 是维度,window 是上下文窗口 w2v_model = Word2Vec( sentences=train_cut + test_cut, # 训练+测试一起训,扩大词表 vector_size=100, # 常见 100/200/300,越大越吃内存 window=5, # 新闻语境,5 左右够用 min_count=2, # 出现少于2次的词丢弃 workers=4, epochs=10 ) w2v_model.save('data/w2v.model') def sentence_vector(words, model, dim=100): # 把词向量求平均得到句向量,没有词就返回零向量 vecs = [model.wv[w] for w in words if w in model.wv] if not vecs: return np.zeros(dim) return np.mean(vecs, axis=0) train_vec = np.array([sentence_vector(w, w2v_model) for w in train_cut]) test_vec = np.array([sentence_vector(w, w2v_model) for w in test_cut]) print(train_vec.shape) # 应为 (样本数, 100)逻辑说明:vector_size=100是维度和效果的折中,短文本分类 100 维通常够用;min_count=2过滤低频词,避免噪声词污染向量;句向量用「词向量求平均」是最简单也最常用的 baseline,源码里如果用了更复杂的加权,可以在此基础上替换。参数上,window越大越偏向主题、越小越偏向语法,新闻分类偏主题,5 到 8 都合理。注意train_cut + test_cut一起训词向量在严格评测里算数据泄漏,但作为工程实践、尤其是词表覆盖优先的场景,很多人会这么做,你要做严谨对比就只用训练集训。
注意:词向量训练是随机的,
workers和epochs会影响结果,做多模型对比时最好固定随机种子,否则 SVM+word2vec 和 LSTM+word2vec 的差异里混进了随机性,结论就不可信了。
4. 多模型对比:SVM、LSTM、朴素贝叶斯怎么跑和怎么调
4.1 传统机器学习:SVM、KNN、朴素贝叶斯
传统方法吃的是句向量或 TF-IDF 特征,sklearn 一把梭。源码里 SVM、KNN、朴素贝叶斯都属于这一类,区别主要在特征和核函数。
from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import MultinomialNB from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics import classification_report # 方案A:TF-IDF + 朴素贝叶斯(快,适合做baseline) tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_train_tfidf = tfidf.fit_transform([' '.join(w) for w in train_cut]) X_test_tfidf = tfidf.transform([' '.join(w) for w in test_cut]) nb = MultinomialNB(alpha=1.0) nb.fit(X_train_tfidf, train_y) print("朴素贝叶斯:\n", classification_report(test_y, nb.predict(X_test_tfidf))) # 方案B:word2vec句向量 + SVM svm = SVC(kernel='rbf', C=1.0, gamma='scale') svm.fit(train_vec, train_y) print("SVM+word2vec:\n", classification_report(test_y, svm.predict(test_vec))) # 方案C:word2vec句向量 + KNN knn = KNeighborsClassifier(n_neighbors=5) knn.fit(train_vec, train_y) print("KNN:\n", classification_report(test_y, knn.predict(test_vec)))逻辑说明:TF-IDF 方案用ngram_range=(1,2)捕捉二元词组,max_features=5000控制维度防止稀疏爆炸;朴素贝叶斯的alpha是平滑系数,语料大可以调小。SVM 的C控制惩罚力度,C越大越容易过拟合,gamma影响 RBF 核的作用范围。KNN 的n_neighbors是核心参数,5 是常见起点,类别多可以调大。参数怎么改:先用默认跑一遍拿到 baseline,再单独调一个参数看指标变化,别一次改一堆。
4.2 深度学习:LSTM 与 MLP
深度学习侧用 Keras,LSTM 吃的是词索引序列,需要先做 tokenizer 和 padding。MLP 则可以直接吃句向量。
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout # 构建词表,只保留最常见的 10000 个词 tokenizer = Tokenizer(num_words=10000) tokenizer.fit_on_texts([' '.join(w) for w in train_cut]) X_train_seq = tokenizer.texts_to_sequences([' '.join(w) for w in train_cut]) X_test_seq = tokenizer.texts_to_sequences([' '.join(w) for w in test_cut]) # 统一长度,短补长截 maxlen = 100 X_train_pad = pad_sequences(X_train_seq, maxlen=maxlen, padding='post', truncating='post') X_test_pad = pad_sequences(X_test_seq, maxlen=maxlen, padding='post', truncating='post') # 标签转 one-hot(类别数按你的数据改) from tensorflow.keras.utils import to_categorical num_classes = len(set(train_y)) y_train_cat = to_categorical([int(y) for y in train_y], num_classes) y_test_cat = to_categorical([int(y) for y in test_y], num_classes) model = Sequential([ Embedding(input_dim=10000, output_dim=100, input_length=maxlen), LSTM(64, dropout=0.2, recurrent_dropout=0.2), Dense(64, activation='relu'), Dropout(0.3), Dense(num_classes, activation='softmax') ]) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.summary() history = model.fit(X_train_pad, y_train_cat, validation_split=0.1, epochs=10, batch_size=64)逻辑说明:num_words=10000限制词表规模,太大反而稀疏;maxlen=100是序列截断长度,新闻短文本一般 100 到 300 够用,太长 LSTM 训练慢且容易梯度问题;Embedding的output_dim=100和词向量维度对齐,如果你想用预训练 word2vec 权重初始化 Embedding,可以把w2v_model.wv的向量按词表顺序填进去,这是 LSTM+word2vec 相对 LSTM 的关键差异。dropout和recurrent_dropout是防过拟合的,短文本数据量不大时尤其重要。参数上,batch_size=64是显存和收敛速度的折中,epochs配合validation_split看验证集何时不再下降。
提示:LSTM+word2vec 和纯 LSTM 的对比,本质是「随机初始化 Embedding」和「预训练 Embedding」的对比。要公平,除了 Embedding 初始化方式,其他超参必须完全一致,否则你比的是超参不是词向量。
5. 避坑与排查:跑这份源码最容易翻车的五个地方
5.1 内容与标签行数错位
现象:模型准确率异常低,甚至接近随机猜,但代码不报错。原因:train_contents.txt和train_labels.txt行数不一致,或者中间有空行被过滤后错位。解决:在load_data里加断言,读完后立刻assert len(contents) == len(labels),并且打印前几行人工核对内容和标签是否对应。我一般还会把(内容, 标签)配对后 shuffle 前先存一份,方便回溯。
5.2 中文编码乱码
现象:读文件报UnicodeDecodeError,或者分词结果全是乱码。原因:文件不是 UTF-8,可能是 GBK 或 GB18030。解决:先探测编码,open(path, encoding='gb18030')试一下,或者用chardet检测。统一转成 UTF-8 再处理,别在流程里混用编码。
5.3 Keras/TensorFlow 版本冲突
现象:import keras报错,或者Embedding、LSTM参数不认。原因:Keras 3 与 TF 2.x 的 API 不兼容,或者 TF 版本太老不支持某些层参数。解决:锁定tensorflow==2.10.0和keras==2.10.0,在虚拟环境里重装。如果必须用新版本,把input_length这类旧参数去掉,改用Input层显式声明。
5.4 词向量维度与模型输入不匹配
现象:ValueError: expected shape (None, 100) but got (None, 200)。原因:word2vec 训练时vector_size和后续模型里写死的维度不一致。解决:把维度抽成变量,训练词向量和构造句向量、Embedding 都用同一个dim,别在两处硬编码不同数字。
5.5 测试集泄漏进训练
现象:测试集指标高得离谱,换一批真实数据就崩。原因:词向量在train_cut + test_cut上训练,或者 TF-IDF 在测试集上fit。解决:词向量只用训练集训,TF-IDF 只fit训练集、transform测试集。做严谨对比时,这条是底线,工程上想省事也要心里有数。
6. 进阶技巧:把多模型对比做成可复现的实验
跑通单个模型只是起点,这份源码真正的价值在于「对比」。但对比最容易变成「跑一遍、记个数、拍脑袋下结论」。我的习惯是把每次实验的配置和结果落成结构化记录,这样换参数、换特征时才有据可查。下面这个表格是我一般会维护的实验记录格式,你可以直接套:
| 实验编号 | 特征方案 | 模型 | 关键参数 | 测试准确率 | 备注 |
|---|---|---|---|---|---|
| E01 | TF-IDF | 朴素贝叶斯 | alpha=1.0 | 待填 | baseline |
| E02 | TF-IDF | SVM | C=1.0, rbf | 待填 | 对比核函数 |
| E03 | word2vec句向量 | SVM | C=1.0 | 待填 | 对比特征 |
| E04 | 随机Embedding | LSTM | hidden=64 | 待填 | 深度学习baseline |
| E05 | word2vec初始化 | LSTM | hidden=64 | 待填 | 对比预训练 |
固定随机种子是让对比可信的前提。numpy、TensorFlow、sklearn 各有各的种子入口,我一般写一个set_seed函数在入口调用:
import numpy as np import random import tensorflow as tf def set_seed(seed=42): random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) # sklearn 的 random_state 要在各模型里单独传 print(f"seed fixed: {seed}") set_seed(42)逻辑说明:random管 Python 内置随机,np.random管 numpy,tf.random.set_seed管 TensorFlow,三者缺一不可。sklearn 的模型要在实例化时传random_state=42,比如SVC(random_state=42)(部分模型支持)。参数上,种子选多少无所谓,关键是全程一致。做完这步,E04 和 E05 的差异才能归因到「Embedding 是否预训练」,而不是随机波动。
还有一个容易被忽略的点:类别不平衡。新闻数据集里各类别数量往往不均,准确率会被大类主导。我一般会额外看classification_report里的 macro-F1,而不是只盯 accuracy。如果某类样本极少,考虑在训练时用class_weight或对少数类过采样。这些在源码里不一定有,但作为从业者,这是把「能跑」变成「能用」的关键一步。
从那以后我每次拿到一份文本分类源码,都强制先跑一遍数据对齐校验和种子固定,再谈模型对比——这两步省下来的返工时间,比调任何超参都值。希望帮到你。
本文还有配套的精品资源,点击获取