简介面向本科毕业设计与课程大作业的深度学习实践资源基于LSTM的虚假新闻检测项目提供可直接运行的Python源码、新闻数据集与文档说明。项目覆盖文本预处理、特征表示、LSTM模型构建与训练评估等环节适用于计算机、人工智能等相关专业学生用作毕业设计参考、期末项目或入门进阶练习。资源压缩包约5.86MB共6个文件包含Python主程序、训练/测试CSV数据、停用词表与说明文档代码经过调试可直接运行目录划分清晰便于按步骤复现实验。已有273人学习下载适合需要快速搭建完整深度学习流程的读者。项目整体完成度高除源码与数据外还附有答辩说明文档可帮助理解设计思路、参数设置与实验结果在基础之上也便于进行扩展改进。1. 想做基于深度学习LSTM的虚假新闻检测先想清楚你要交付什么做本科毕业设计选“基于深度学习LSTM的虚假新闻检测”这个题真正要交付的不是一段能跑的代码而是一条“数据→模型→评估→结论”的完整证据链。虚假新闻检测本质是文本二分类问题但比普通分类更依赖上下文结构——夸大标题、情绪化叙事、事实引用错位这些信号都藏在词语顺序里。LSTM恰好擅长这种序列建模在CPU上就能完成训练数据量也不需要很大对毕设场景非常友好。这个方案适合想入门深度学习自然语言处理的学生也适合需要快速验证文本分类思路的从业者。下面按数据准备、模型训练、评估调参、避坑的顺序把每条命令和参数讲透。2. 数据与预处理虚假新闻检测的80%工作量藏在这里2.1 数据集怎么选别一上来就自己爬很多同学拿到题目第一反应是自己写爬虫爬新闻再找几个网站手动标注。我不建议这么做标注成本高不说还会引入标注者主观偏差——同一句话在这个人眼里很假换个人就半信半疑。常见做法是去Kaggle找Fake News相关的公开数据集或者用研究机构开源的虚假新闻检测数据。这类数据通常是一个CSV文件每一行有一条新闻的标题、正文和标签标签用0/1表示真实/虚假。你拿到手第一件事不是训练而是先做两个检查。import pandas as pd df pd.read_csv(news.csv) print(df[label].value_counts()) # 先看标签分布太偏就要处理 print(df.isnull().sum()) # 缺失值统计正文为空的行直接删除 df df.dropna(subset[text]) # 去掉没有正文的样本这段代码帮你在建模前排除两类常见问题数据极度不均衡和大量缺正文。如果假新闻样本只有5%模型学到的最优策略就是全部预测为真实准确率95%看起来很好实际一点用没有。遇到这种情况优先用正负样本比例在1:4以内的子集训练或者后续在损失函数里给少数类加权重。数据清理要尽早训练完成后再回头补数据是最懊恼的事。选数据还要看字段完整度。只有标题没有正文的数据集训练出来的模型只能靠标题判断泛化能力很弱因为真实场景里模型看到的是完整新闻。我一般会优先选带title和text两列的数据title作为单独特征拼接进去让模型同时把握标题和内容的风格差异。2.2 文本清洗与分词中文和英文两条路线新闻文本不像评论那样随意但也有大量需要清理的噪声HTML标签、超链接、多余空白、中文全角符号。清洗的目标不是把句子砍成光杆而是去掉明显无意义的字符保留句法和语义结构。英文文本先统一小写再用正则去掉非英文字符最后按空白切分中文文本要分词否则按字切分会让词表扩大到几十万训练和推理都变慢。import re import jieba def clean_text_en(text): text text.lower() text re.sub(r[^], , text) # 去HTML标签 text re.sub(rhttps?://\S, , text) # 去URL text re.sub(r[^a-z\s], , text) # 去标点数字 return text.split() def clean_text_zh(text): text re.sub(r[^], , text) text re.sub(rhttps?://\S, , text) text re.sub(r[^\u4e00-\u9fff], , text) # 只保留中文 return jieba.lcut(text)注意一个反直觉的点停用词表在LSTM这个项目里我建议别去。TF-IDF时代去掉“的、了、是”是因为这些词没有区分度但LSTM要学的是词与词的组合关系“的”和“了”恰恰承载了语法结构。把停用词删掉模型看到的句子支离破碎性能反而下降。分词后的结果存成列表等会儿直接喂给词表构建函数。清洗还有一个细节不要把转换后的词用空格拼回字符串存进CSV那样后续每次读取都要重新分词。直接以列表形式存为pickle或者parquet训练脚本重新加载时省掉一次分词时间。毕设周期里这种小优化能省下大量重复等待。2.3 构造训练样本词表、截断、padding、划分分词完成后下一步是把词转成数字。常见做法是统计全量词频保留出现次数最多的前N个词构建一个词到id的映射表。这里有一个容易被忽略的参数词表大小。N太小生僻词全变成UNK模型学不到冷门但关键的词N太大词表里堆满出现次数不到五次的噪声Embedding层参数爆炸。一般我先把语料的词频分布画出来观察累计覆盖率再定N通常两万是一个甜点值。from collections import Counter def build_vocab(all_words, max_vocab20000, min_freq5): counter Counter(all_words) vocab {PAD: 0, UNK: 1} for word, count in counter.most_common(max_vocab): if count min_freq: break vocab[word] len(vocab) return vocabmin_freq5这个条件会过滤掉低频噪声词但注意不要过滤太狠否则会丢失虚假新闻里特有的生造词。LSTM对序列长度敏感新闻正文动辄几百上千词直接送进去会让训练时间成倍增加。一般按正文长度的分位数截断p75或p85都可以截断后如果长度不足则补PAD占位符。MAX_LEN 300 # 按语料长度分布来定不是拍脑袋 def make_sequence(words, vocab, max_lenMAX_LEN): ids [vocab.get(w, 1) for w in words] # 未登录词用UNK(1) ids ids[:max_len] if len(ids) max_len: ids [0] * (max_len - len(ids)) # PAD补位 return ids这里最容易翻车的是padding方向。PAD放在句子尾部LSTM读完全部有效词后自然进入PAD区域结果受padding影响较小如果放在头部模型先从一堆0开始语义被严重污染。如果你用LSTM做文本分类时发现效果总比CNN差检查的第一件事就是padding方向。词表和序列化结果建议落盘保存不要在每次运行时重建。用pickle把vocab和训练序列存下来后面重新实验时直接load可以省掉每次清洗与构建的时间。这不算什么高级技巧但能让你在迭代模型时少发几次脾气。数据划分是另一个大坑。新闻数据有时间属性正确做法是按时间先后切分前70%做训练后30%做验证不要用random.sample做随机打乱。随机打乱会把时间特征当成泄露信息给模型让模型在验证集上显得优秀但部署时面对新新闻立刻失灵。df df.sort_values(publish_date) train_df df.iloc[:int(len(df) * 0.7)] val_df df.iloc[int(len(df) * 0.7):]逻辑也简单文本分类考的是跨时间的泛化能力而不是记忆某个时期的语料风格。很多论文里F1很高却在真实场景失效根子就在划分策略。反过来如果你只求毕设答辩顺利随机划分也能交出不错的数字但这会经不住追问。3. 模型构建与训练用PyTorch跑通LSTM分类器的最小可行代码3.1 选PyTorch还是TensorFlow先定工具链。TensorFlow 2.x的Keras接口上手快但遇到自定义损失函数、改网络结构时封装太厚调试体验不如PyTorch。现在网上流传的lstm源码、lstm模型代码大多用PyTorch写的照着改也方便。建议先装好Python 3.8以上环境用VSCode配好python环境再写LSTM模型调试时能盯着变量走比Jupyter里黑盒跑完整训练更靠谱。深度学习课本pdf里讲LSTM的那几章数学符号看得人头晕真到实现阶段你会发现核心代码就几十行难的是数据封装和训练循环。3.2 定义LSTM分类器Embedding、LSTM、全连接层网络结构分四层Embedding层把词id映射成稠密向量LSTM层建模序列依赖全连接层把最后一个时刻的隐状态映射成两个类别的logitsdropout做正则。注意这里是二分类输出维度是2配合CrossEntropyLoss不需要手动接Sigmoid。import torch import torch.nn as nn class FakeNewsLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers2, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_dim, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) out, (h_n, c_n) self.lstm(emb) # out: (batch, seq_len, hidden_dim) h_last h_n[-1] # 取最后一层最后时刻的隐状态 h_last self.dropout(h_last) return self.fc(h_last)几个参数不是随手写的。padding_idx0告诉Embedding层id为0的PAD位置不参与梯度更新否则模型会在PAD上学出一套没意义的向量。batch_firstTrue让输入形状是(batch, seq_len)符合我们用make_sequence构造数据的习惯。取隐状态这里有个隐藏细节当num_layers2时out[:, -1]取的是最后一层最后一个时刻的输出而h_n[-1]表达的是“最后一层在所有时刻处理完后留下的隐状态”两者在多数情况下等价但h_n语义更明确也方便以后改成双向LSTM时不用改代码。我一般用h_n[-1]没出过问题。单向还是双向毕设场景我建议先上单向。双向LSTM会把hidden_dim翻倍训练时间变长对虚假新闻检测这种全局结构不算特别复杂的任务提升往往不到2个点但你要付出的调参时间翻倍。3.3 数据封装与训练循环能跑起来比什么都重要模型定义完接着要把数据装进DataLoader。这里新手最容易卡住因为DataLoader默认的batch拼接要求每个样本形状一致而我们前面已经统一截断到MAX_LEN所以直接用torch.stack就可以。from torch.utils.data import Dataset, DataLoader class NewsDataset(Dataset): def __init__(self, sequences, labels): self.sequences sequences self.labels labels def __len__(self): return len(self.labels) def __getitem__(self, idx): return (torch.tensor(self.sequences[idx], dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.long)) train_dataset NewsDataset(X_train, y_train) def collate_fn(batch): seqs, labels zip(*batch) return torch.stack(seqs), torch.tensor(labels) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, collate_fncollate_fn)collate_fn看上去多余但养成写它的习惯很值钱。后面如果改成变长序列想用pack_padded_sequence省显存只需改collate_fn即可训练循环完全不用动。这算是一个小的“后悔药”。训练循环本身不复杂核心是前向传播、算loss、反向传播、更新参数。注意LSTM和普通全连接网络有个区别梯度回传会跨时间步学习率稍微偏大就会出现loss剧烈震荡这也是很多人复现LSTM源码时发现“同样的代码我的loss不降”的原因。import torch.optim as optim model FakeNewsLSTM(vocab_sizelen(vocab)) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) epochs 10 for epoch in range(epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch 1}, loss {total_loss / len(train_loader):.4f})学习率1e-3是Adam的默认值但LSTM时序依赖深梯度尺度大建议从3e-4开始试。loss在前三个epoch内没有稳定下降直接降学习率重来不要在同一个状态下死磕。weight_decay用1e-5做轻量正则够用太大了会让模型欠拟合。如果你用的是CPU训练batch_size记得改小到32hidden_dim降到64后面几章会展开讲。3.4 没有GPU怎么训把CPU训练跑得更快毕设环境经常没有GPU这个其实不致命。CPU训练的全部技巧就是把计算量压到合理范围torch.set_num_threads(4)可以吃满多核序列长度从300降到150新闻数据里的关键线索通常在开头段落batch_size设32hidden_dim设64训练时间能缩短一半以上。我见过不少同学在CPU上硬训hidden_dim256、seq_len500的模型一个epoch要跑四十分钟最后还没收敛。用LSTM做毕设先把问题变小跑通以后再慢慢加复杂度这才是正确的节奏。4. 评估与调参怎么让F1从0.7升到0.854.1 准确率虚高是常态看混淆矩阵和F1很多LSTM虚假新闻检测源码跑完第一件事是打印准确率看到0.93就欢呼。我明确说在文本分类任务里准确率是最不值得信任的指标。新闻数据天然不均衡真实新闻往往占多数模型只需要把测试集全部判为真实准确率就能到70%以上。正确做法是看每一类的精确率、召回率、F1以及混淆矩阵。拿假新闻这一类单独看模型是不是真的能把假新闻揪出来。from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch_x, batch_y in val_loader: logits model(batch_x) preds torch.argmax(logits, dim1) all_preds.extend(preds.tolist()) all_labels.extend(batch_y.tolist()) print(classification_report(all_labels, all_preds, target_names[real, fake])) print(confusion_matrix(all_labels, all_preds))如果real类的F1在0.95fake类的F1只有0.4这个模型就是摆设。你看混淆矩阵会发现模型几乎把所有假新闻都预测成了真实。这时候错误的根源多半是2.1节说的数据不均衡而不是模型结构。先回去看标签分布再决定是采样还是加权重不要急着改网络。4.2 三个最值得调的参数学习率、序列长度、隐藏层维数调参不需要把所有旋钮都转一遍LSTM文本分类影响最大的三个参数是学习率、序列长度和隐藏层维数。我没列batch_size因为batch_size对最终指标影响相对小主要影响训练速度和显存。参数推荐范围影响方向优先调整时机学习率3e-4 ~ 1e-3收敛速度与稳定性loss震荡、不下降序列长度语料长度p80 ~ p90上下文完整度长新闻占比高时隐藏层维数64 ~ 256模型容量与过拟合验证集F1低学习率是最容易见效的旋钮。遇到loss在0.69附近不下降或者两个epoch跳来跳去把学习率减半重来大概率能解决。序列长度要按语料分布定不要抄别人的512或256如果新闻平均长度只有150你设512模型要读大量PAD如果平均长度800你设200关键信息直接被截断。方法很简单先统计len(text)的分位数取p85再往整数凑。隐藏层维数128是甜点64偏少但训练快256只会带来边际收益。小数据集上128和256的F1差距通常在1个点内但训练时间差了四倍。隐藏层维数不是越大越好它和词表大小一样模型容量超过数据量承载能力后就开始过拟合。4.3 过拟合的识别与早停别等训练完才后悔过拟合的信号很有辨识度训练loss持续下降验证集F1在某个epoch后不再上升甚至下降这就是典型的学过头了。处理方式不是降模型复杂度而是先做早停保留验证集F1最高的那一个checkpoint不是最后一个epoch的模型。很多源码里训练完直接保存model.pt最后拿来评估的很可能是一个过拟合状态。best_f1 0.0 patience 3 bad_epochs 0 for epoch in range(epochs): train_loss run_one_epoch(model, train_loader, modetrain) val_f1 run_one_epoch(model, val_loader, modeeval).f1 if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pt) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: print(fearly stop at epoch {epoch 1}) break早停的patience设3到5比较合理太小容易被验证集波动骗到太大又起不到防止过拟合的作用。保存best_model.pt之后最终评估和答辩演示都用这个文件不要拿最后一个epoch的权重。如果早停之后F1还是上不去再考虑降低hidden_dim或增大dropout到0.6。5. 避坑记录LSTM虚假新闻检测翻车现场与止损方案5.1 崩了词表里全是只出现一次的稀有词现象训练正常但打印词表发现两万个词里有一大半只出现过一两次验证集F1始终很低。原因build_vocab只按词频排序取前N个没有过滤低频词。那些只出现一次的词不会被排除反而把真正有区分度的词挤出了词表。解决在build_vocab里加min_freq5出现次数低于5次的词统一映射到UNK。这样词表更干净Embedding层不需要为噪声词维护向量模型还能学到“没见过就是异常”的规律。5.2 翻车训练loss不降反升现象第一个epoch loss在0.69附近后面变成0.70、0.72再也没有回到0.69以下。原因学习率太大LSTM跨时间步的梯度在损失面上震荡走不进局部最优。另一个常见原因是忘记shuffle数据每个batch里全是同一个标签模型被反复带到同一个方向。解决把学习率从1e-3降到3e-4或5e-4。同时检查训练数据加载时shuffleTrue有没有生效。如果降学习率后loss还在涨把词表重建一遍看看是否出现了数据读取时的index错位。5.3 假象验证集准确率95%却是个摆设现象训练集和验证集准确率都超过95%F1也好看但拿几条新写的新闻进去预测结果全是真实。原因数据泄露。最常见的是数据里有一列叫“label来源”或“声明状态”模型学到的是这个字段的规律不是新闻文本本身。另一个更隐蔽的原因是随机划分导致训练集和验证集里存在同一事件的重复报道模型记住了事件而不是学会判断真假。解决逐个字段排查凡是和标签直接或间接相关的列全部删除。划分数据严格按时间先后切分不要偷懒用随机划分。验证时自己构造几条明显是虚假新闻的文本测一遍这一步能暴露90%的数据泄露问题。5.4 黑匣子PAD补位让模型学到的是长度而不是语义现象把模型在验证集上的输出按序列长度分组看发现长度越长的新闻越容易被判为真实F1在不同长度区间差异极大。原因padding把序列补齐到固定长度后长新闻和短新闻的PAD数量不同。LSTM读到最后时刻短新闻已经被PAD稀释得差不多了模型学到的是“句子长短”这个伪特征。尤其当MAX_LEN刻意设得很大时这个现象更严重。解决先用文本长度的p85做MAX_LEN不要盲目取最大值。如果训练数据长度差异悬殊给batch按长度排序让同一个batch内的样本长度接近。最彻底的做法是用pack_padded_sequence但那会显著增加代码复杂度毕设阶段建议先靠长度排序和合理MAX_LEN解决。5.5 玄学换台电脑随机种子不同结果差一大截现象同一个代码在实验室机器上F1是0.82回到宿舍电脑复现变成了0.74谁也没改任何东西。原因LSTM的权重初始化和dropout都依赖于随机数不同机器、不同PyTorch版本、甚至CPU多线程浮点累加顺序都会改变结果。解决在代码开头固定torch.manual_seed(42)、numpy.random.seed(42)并在文档说明里把PyTorch版本、Python版本、seed值写清楚。答辩演示和论文实验用同一个环境千万别自己打自己脸。6. 进阶让检测更实用的三个技巧6.1 用预训练词向量初始化Embedding层随机初始化Embedding意味着模型要从零学词义小数据集上效率很低。常见做法是加载word2vec或fastText预训练向量把命中词表的向量填进model.embedding.weight.data未命中的词保持随机初始化。这样模型一开始就知道“疫苗”和“接种”语义相近训练收敛更快F1通常能涨2到3个点。加载预训练向量的代码在pyTorch里只有几行网上python深度学习教程里都有现成写法注意别把padding_idx对应的行覆盖了。6.2 加一个Self-Attention层让判断更有依据单向LSTM最后一个隐状态承载了全句信息但信息量太大模型说不清自己是看了哪个词下的结论。一个轻量做法是在LSTM输出后加一层token-level self-attention对每个时刻的隐状态加权平均再送给全连接层。这个改动只增加十几行代码却能让模型在可视化时给出“模型主要依据哪几个词判断真假”答辩时这是非常加分的点。权重可视化可以用torch.topk直接取出权重最高的五个词不需要额外装复杂的解释库。6.3 验证你的模型不是“抄答案”最后一个技巧是答辩前必做的一项验证把测试集全部打乱时间顺序重新预测看F1是否大幅下降。如果F1从0.82掉到0.60说明模型依赖了时间特征或数据泄露信息而不是真正的语义判断。这个结果一旦被答辩老师问出来比模型跑崩更尴尬。我第一次做这个题目时就在这个小实验上翻了车后来老老实实按时间划分重新训练才把真正有效的模型跑出来。如果你也在做这个方向建议把数据划分方式、随机种子、三个核心超参数写进文档说明这是最便宜的“后悔药”。希望这些经验能帮到你少走弯路。本文还有配套的精品资源点击获取