简介:这是一份面向计算机相关专业学生与项目实战学习者的课程设计资料,围绕Python与MLP多层感知机实现互联网虚假新闻检测器,适合正在准备期末大作业、课程设计或需要机器学习实战练习的人群,可帮助理解文本预处理、特征提取、模型训练与预测评估的完整流程。压缩包共21个文件,约92.72MB,包含4个py源码文件、4个xml配置、3个model模型文件、2个pkl序列化数据、2个csv数据集、2个txt说明及md、docx文档等,覆盖数据预处理、模型拟合、参数优化与预测推理等模块,并附有项目报告文档。目前已有104人学习下载。资源提供可运行的完整源码与文档说明,读者可据此复现虚假新闻检测实验,参考模型训练与调参思路,并借助报告文档梳理项目结构,适合作为课程设计模板或二次开发的基础。
1. 从一份大作业说起:Python + MLP 虚假新闻检测器到底能跑出什么
期末周前后,总有人拿着一个压缩包来找我,名字大概就是「基于 Python 和 MLP 实现的互联网虚假新闻检测器源码 + 文档说明」。问的问题也高度一致:这东西能不能直接跑?MLP 是不是太浅了,做出来会不会被老师一眼看穿?文档说明里到底该写什么才不算凑字数?
先把结论摆出来:这个题目的价值不在模型有多深,而在于它是一条完整的、可解释的文本二分类流水线。虚假新闻检测本质上是把新闻标题或正文映射成「真 / 假」两个标签,MLP(多层感知机)在这里承担的是分类头角色,前面必须接文本向量化。它适合三类人:刚学完 Python 想找一个能写进简历的完整项目的人、需要交课程大作业但不想只调库的人、以及想搞清楚「文本分类从原始数据到预测结果中间到底发生了什么」的入门者。下面我按自己带学生复现这套东西的顺序,把选型、代码、参数和翻车点讲清楚。
2. 数据与特征:虚假新闻检测器的输入到底长什么样
2.1 先搞清楚任务形态,再决定 MLP 的输入维度
虚假新闻检测在工程上通常被建模成有监督的二分类问题。数据集一般是一个 CSV 或 TSV,至少两列:文本列(title 或 text)和标签列(label,取值 0/1 或 fake/real)。MLP 本身不认识字符串,所以中间必须有一个向量化层,把每条新闻压成一个固定长度的数值向量。
常见做法有两种。第一种是 TF-IDF,把词袋加权后取 top-N 个特征,得到一个稀疏但维度可控的向量,比如 5000 维。第二种是词嵌入平均,用预训练词向量把每个词映射成 100 或 300 维,再对整句取平均。大作业场景下我更推荐 TF-IDF,原因是它不依赖外部词向量文件,离线可跑,而且特征权重可解释——你能直接看出哪些词把样本推向了「假」。
这里有个容易被忽略的点:MLP 的输入维度等于特征维度,不是句子长度。很多人第一次写的时候把 padding 后的序列直接塞进全连接层,结果维度对不上,报错信息还特别绕。记住,全连接层要的是「一条样本一个向量」,不是「一条样本一个矩阵」。
2.2 用 TfidfVectorizer 做特征,参数怎么设
下面这段是数据加载加特征化的最小可跑代码,我一般会先把它单独跑通,确认特征矩阵形状对了再往下接模型。
import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split # 读取数据,假设文件是 utf-8 编码,列名为 text 和 label df = pd.read_csv("news.csv", encoding="utf-8") df = df.dropna(subset=["text", "label"]) # 丢掉空文本和空标签 df["text"] = df["text"].astype(str) # 标签统一成 0/1,兼容 fake/real 这类字符串标签 if df["label"].dtype == object: df["label"] = df["label"].map({"fake": 0, "real": 1}) # TF-IDF:最多 5000 个词,忽略出现在超过 80% 文档里的词 vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), # 同时统计单词和二元词组 min_df=2, # 至少出现在 2 篇文档里才保留 max_df=0.8, # 出现在 80% 以上文档的词视为停用词 sublinear_tf=True # 用 1+log(tf) 抑制高频词 ) X = vectorizer.fit_transform(df["text"]).toarray() y = df["label"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(X_train.shape, X_test.shape)逻辑说明:fit_transform只在训练集上做拟合,测试集必须用同一个 vectorizer 的transform,否则特征空间不一致,这是数据泄漏的经典来源。参数上,max_features=5000是精度和内存的折中,文本量在几万条以内够用;ngram_range=(1,2)能捕捉「not true」这类否定搭配,对虚假新闻识别有实际收益;min_df=2过滤只出现一次的词,降低噪声;sublinear_tf=True让高频词不至于主导权重。
提示:如果数据量超过十万条,
.toarray()会把稀疏矩阵转成稠密矩阵,内存可能直接爆掉。这种情况要么调小max_features,要么改用稀疏输入配合支持稀疏的模型。
2.3 标签分布和文本长度,先看再训
在接模型之前,我习惯先做两件事:看标签比例、看文本长度分布。虚假新闻数据集经常是不平衡的,真假比例可能是 7:3 甚至更极端。如果不处理,模型只要全预测多数类就能拿到 70% 准确率,看起来很美,实际毫无用处。
文本长度也要看。如果大部分样本只有几十个字符,TF-IDF 的 5000 维里会有大量零,MLP 容易过拟合;如果文本很长,截断策略就要提前定好。这两步不写进最终代码,但决定了你后面参数怎么调。
3. 用 PyTorch 搭一个能收敛的 MLP 分类器
3.1 网络结构:两层隐藏层就够了
MLP 在文本分类里不需要堆很深。我的经验是两层隐藏层、每层 256 或 128 个神经元,配合 Dropout,效果和三层差不多,但训练更快、更不容易过拟合。结构大致是:输入 5000 维 → 全连接 256 → ReLU → Dropout 0.3 → 全连接 128 → ReLU → Dropout 0.3 → 输出 2 类。
import torch import torch.nn as nn class FakeNewsMLP(nn.Module): def __init__(self, input_dim, hidden1=256, hidden2=128, num_classes=2, dropout=0.3): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden1), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden1, hidden2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden2, num_classes) ) def forward(self, x): return self.net(x) model = FakeNewsMLP(input_dim=X_train.shape[1]) print(model)逻辑说明:input_dim必须等于 TF-IDF 的特征数,写死会出问题,所以从X_train.shape[1]取。Dropout 放在每个 ReLU 之后,训练时随机丢弃一部分神经元,推理时自动关闭。输出层不加 Softmax,因为 PyTorch 的CrossEntropyLoss内部已经包含了 log-softmax,重复加会导致数值不稳定。
参数上,hidden1和hidden2是主要调节对象。如果训练集只有几千条,建议降到 128 和 64;如果数据量在十万级以上,可以升到 512 和 256。dropout在 0.2 到 0.5 之间调,过拟合严重就往上加。
3.2 训练循环:学习率、batch size 和早停
训练部分我一般用 Adam,学习率 1e-3,batch size 64。这三个值在文本分类里算是比较稳的起点。
from torch.utils.data import TensorDataset, DataLoader # 转成 tensor train_ds = TensorDataset( torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.long) ) test_ds = TensorDataset( torch.tensor(X_test, dtype=torch.float32), torch.tensor(y_test, dtype=torch.long) ) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) test_loader = DataLoader(test_ds, batch_size=64) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) best_acc = 0.0 patience, wait = 3, 0 for epoch in range(30): model.train() for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() # 每个 epoch 结束在测试集上评估 model.eval() correct = total = 0 with torch.no_grad(): for xb, yb in test_loader: xb, yb = xb.to(device), yb.to(device) pred = model(xb).argmax(dim=1) correct += (pred == yb).sum().item() total += yb.size(0) acc = correct / total print(f"epoch {epoch+1}, acc {acc:.4f}") # 早停:连续 3 个 epoch 没提升就停 if acc > best_acc: best_acc = acc wait = 0 torch.save(model.state_dict(), "best_mlp.pt") else: wait += 1 if wait >= patience: print("early stop") break逻辑说明:weight_decay=1e-4是 L2 正则,配合 Dropout 一起压制过拟合。早停的patience=3表示连续三轮测试准确率没刷新就停,避免无效训练。保存的是state_dict而不是整个模型,加载时更灵活。
参数上,学习率如果 loss 震荡明显就降到 5e-4;batch size 受显存限制,CPU 训练可以降到 32。如果发现训练准确率一直上不去,先检查特征矩阵是不是全零或者标签是不是映射错了,这两个是新手最常见的翻车点。
3.3 不平衡数据的处理:加权损失
前面提到标签可能不平衡。最省事的做法是在CrossEntropyLoss里传weight,给少数类更高的权重。
import numpy as np class_counts = np.bincount(y_train) weights = 1.0 / class_counts weights = weights / weights.sum() * len(class_counts) criterion = nn.CrossEntropyLoss( weight=torch.tensor(weights, dtype=torch.float32).to(device) )逻辑说明:np.bincount统计每类样本数,取倒数后归一化,少数类权重自然更大。这样模型在优化时对少数类的错误惩罚更重,召回率会明显改善。注意权重张量要放到和模型同一个 device 上,否则会报设备不匹配。
4. 评估与调参:准确率之外你该看什么
4.1 混淆矩阵和 F1,比单一准确率靠谱
虚假新闻检测里,把假新闻判成真(漏报)和把真新闻判成假(误报)的代价不一样。只看准确率会掩盖问题,必须看混淆矩阵和 F1。
from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds = [] with torch.no_grad(): for xb, yb in test_loader: xb = xb.to(device) pred = model(xb).argmax(dim=1).cpu().numpy() all_preds.extend(pred) print(confusion_matrix(y_test, all_preds)) print(classification_report(y_test, all_preds, digits=4))逻辑说明:classification_report会输出每类的 precision、recall、F1。如果假新闻类的 recall 明显低于真新闻类,说明模型偏向多数类,需要回头调损失权重或者做重采样。混淆矩阵能直观看出错误集中在哪个方向。
4.2 关键超参数的影响顺序
调参不要一把抓,按影响从大到小来。我的经验顺序是:特征维度(max_features)> 学习率 > 隐藏层大小 > Dropout > batch size。特征维度决定了信息上限,5000 和 10000 的差距往往比换优化器更明显。学习率决定能不能收敛,1e-3 不行就试 5e-4 和 2e-3。隐藏层大小和 Dropout 是过拟合的主要调节旋钮,放在后面调。
| 参数 | 常用范围 | 调大后的影响 | 调小后的影响 |
|---|---|---|---|
| max_features | 3000–20000 | 信息多,易过拟合,内存涨 | 信息少,可能欠拟合 |
| 学习率 | 5e-4–2e-3 | 收敛快,可能震荡 | 收敛慢,可能卡住 |
| hidden1 | 128–512 | 表达强,易过拟合 | 表达弱,欠拟合 |
| dropout | 0.2–0.5 | 正则强,可能欠拟合 | 正则弱,易过拟合 |
| batch_size | 32–128 | 训练稳,收敛慢 | 收敛快,噪声大 |
4.3 交叉验证:小数据集别只切一次
如果数据量在几千条级别,单次 train_test_split 的结果波动会很大。用 5 折交叉验证看平均 F1,比单次准确率可信得多。
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (tr_idx, va_idx) in enumerate(skf.split(X, y)): # 每折重新拟合 vectorizer,避免泄漏 vec = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), min_df=2) X_tr = vec.fit_transform(df["text"].iloc[tr_idx]).toarray() X_va = vec.transform(df["text"].iloc[va_idx]).toarray() # 后面接训练循环,记录每折 F1 print(f"fold {fold}, train {X_tr.shape}, val {X_va.shape}")逻辑说明:每折都要重新fitvectorizer,只在训练折上拟合,验证折只做transform。这是交叉验证里最容易出错的地方,很多人图省事在全部数据上拟合一次,结果验证分数虚高。
5. 避坑与排查:这份源码最容易翻车的五个地方
5.1 报错「mat1 and mat2 shapes cannot be multiplied」
现象:模型 forward 时报维度不匹配,提示类似mat1 and mat2 shapes cannot be multiplied (64x300 and 5000x256)。
原因:input_dim和实际特征维度不一致。常见于先定义了模型,后来又改了max_features,或者测试集用了不同的 vectorizer。
解决:在建模前打印X_train.shape[1],用它初始化FakeNewsMLP。测试集必须复用训练集的 vectorizer,不要重新fit。
5.2 准确率一直卡在 50% 左右
现象:训练 loss 不降,准确率在 0.5 附近徘徊。
原因:标签映射错了,比如fake/real没转成 0/1,或者转的时候映射反了导致标签和特征对不上;也可能是特征矩阵全零。
解决:打印y_train[:20]和X_train[:1]确认。检查df["label"].unique(),确保只有两个取值且已转成整数。
5.3 训练集准确率 99%,测试集只有 60%
现象:训练集表现极好,测试集拉胯,典型过拟合。
原因:特征维度太高、模型太大、Dropout 太小,或者数据量本身太少。
解决:先把max_features降到 3000,hidden1降到 128,dropout提到 0.5,加weight_decay。如果还不行,考虑做数据增强或换更简单的模型。
5.4 中文文本没分词,TF-IDF 全是单字
现象:特征里全是单个汉字,模型效果一般。
原因:TfidfVectorizer默认按空格切词,中文没有空格,整句被当成一个 token 或者按字符切。
解决:中文场景要么先分词再传入,要么把token_pattern改成按字符匹配。分词可以用 jieba,处理后再拼成空格分隔的字符串。
import jieba df["text"] = df["text"].apply(lambda s: " ".join(jieba.cut(s)))逻辑说明:jieba 把中文切成词,再用空格连接,TfidfVectorizer就能正常按词统计。这一步对中文虚假新闻检测的效果提升很明显,不做的话特征基本没有语义。
5.5 保存模型后加载预测结果不对
现象:训练时准确率 85%,加载best_mlp.pt后预测全错。
原因:加载时模型结构参数和保存时不一致,比如隐藏层大小改了;或者忘了model.eval(),Dropout 还在起作用。
解决:加载前用相同参数实例化模型,load_state_dict后再eval()。推理时用torch.no_grad()包住,避免不必要的计算图。
6. 把检测器做成能演示的东西:文档说明与推理脚本
大作业和真实项目的区别在于,前者需要「可展示」。文档说明不是凑字数,它要回答三个问题:数据从哪来、模型怎么训、结果怎么复现。我一般会在文档里放一张流程图(文字描述即可)、一份参数表、一段推理示例。
推理脚本要独立于训练脚本,输入一条新闻文本,输出预测标签和置信度。
import torch import jieba from sklearn.feature_extraction.text import TfidfVectorizer def predict(text, model, vectorizer, device): model.eval() # 中文先分词,和训练时保持一致 tokens = " ".join(jieba.cut(text)) vec = vectorizer.transform([tokens]).toarray() tensor = torch.tensor(vec, dtype=torch.float32).to(device) with torch.no_grad(): logits = model(tensor) prob = torch.softmax(logits, dim=1) pred = prob.argmax(dim=1).item() label = "真实" if pred == 1 else "虚假" return label, prob[0][pred].item() # 使用示例 text = "某地发生离奇事件,官方尚未回应" label, conf = predict(text, model, vectorizer, device) print(f"预测:{label},置信度:{conf:.4f}")逻辑说明:推理时的预处理必须和训练时完全一致,分词方式、vectorizer 参数、标签映射都不能变。softmax把 logits 转成概率,置信度低于 0.6 的样本建议人工复核,这在真实场景里比强行给一个标签更负责任。
文档说明里我还会写清楚环境依赖:Python 3.8 以上、PyTorch、scikit-learn、jieba、pandas。版本不写死,但会注明「在 Python 3.8 + PyTorch 1.x 环境下验证通过」。这样别人复现时遇到问题能有个对照。
最后说一个我自己的习惯:每次改完参数,我都会把当次的配置、准确率、F1 记在一个experiments.md里,哪怕只是随手几行。带过几届学生之后我发现,真正拉开差距的不是模型多复杂,而是有没有留下可追溯的记录。这份虚假新闻检测器的大作业,把 MLP 跑通只是起点,能说清楚每个参数为什么这么设、每个错误为什么这么修,才是它值得写进简历的原因。希望帮到你。
本文还有配套的精品资源,点击获取