资讯动态

基于LSTM的虚假新闻检测:PyTorch实现与调参避坑指南

发布时间:2026/10/9 3:33:31 来源:尧图企业网站定制
简介这是一套面向计算机、通信、人工智能等专业本科生的毕业设计资源基于深度学习LSTM实现虚假新闻检测涵盖数据预处理、模型训练与预测的完整流程。项目源码经调试可运行相关文档能帮助理解整体设计思路可作为课程设计、大作业或毕业设计的直接参考也适合新手入门LSTM文本分类任务。压缩包共6个文件以csv数据集、Python程序、txt停用词表和md说明文档为主整体体积仅5.86MB轻量易部署。其中CSV文件分别对应训练集、测试集与提交格式Python脚本实现核心检测逻辑停用词表用于文本清洗说明文档则记录项目结构与用法。目前已有273人学习下载项目曾获答辩评审98分代码具备较强完整性与可复用性基础较好的读者还可在此基础上调整模型结构以适配不同场景。1. 这是什么样的毕设LSTM 虚假新闻检测到底在做什么本科毕业设计里基于深度学习 LSTM 的虚假新闻检测通常配着一份 Python 源码、一份数据集和一份文档说明。这套组合看上去是“深度学习 文本分类”的标准配置但真正动手你会发现它把自然语言处理最经典的一整套流程全包了数据清洗、序列建模、训练调参、指标评估最后还要写成一篇能过盲审的论文。用 LSTM 做虚假新闻检测的本质是二分类——判断新闻文本是真是假难点不在“分类”本身而在数据噪声大、正负样本不均、模型容易记住措辞习惯而不是学会识别谎言。这个方向适合想在一个学期内完整走完深度学习全流程的本科生也适合刚入门 NLP、想找不烧算力又能讲清模型结构的人。2. 数据集与预处理想让 LSTM 学明白先得把文本切成序列2.1 选数据集公开可用与领域自建怎么权衡做虚假新闻检测第一步永远是数据。常规做法是先找公开可用的新闻数据集当主战场例如英文场景下的 LIAR来自 Politifact 的短句标注或 FakeNewsNet带社交上下文中文场景下真正带“真假”标注的公开集很少很多本科毕设其实是自己爬新闻、自己标或者退一步用情感分类数据集做迁移验证。我的建议是优先用公开数据集完成主实验再拿一小部分自建数据做领域验证这样论文里既能交代清楚数据来源又能体现增量工作。选数据集时还要注意三个容易被忽略的点。第一文本不能太短三五句话的短新闻会让 LSTM 几乎没有可捕捉的上下文第二真假两类样本的比例不能太悬殊公开数据集的标注分布要提前查第三最好带时间戳或来源字段否则论文里想写“谣言随时间演化的特征”这类分析时没有依据。还有一个现实问题数据规模直接影响时间安排几千条数据半天能清洗完几万条就要规划出两三天专门做预处理别把这一步压缩到答辩前一周。2.2 清洗与序列化从中文分词到 padding 的完整 pipeline数据拿到手之后核心工作是把文本转成 LSTM 能吃的整数序列。以中文为例常规 pipeline 是去噪 → 分词 → 构建词表 → 序列化 → padding。去噪这一步要去掉 HTML 标签、URL、提及和多余标点但要保留句末标点因为它们在语气判断上有作用。分词用 jieba英文则直接用空格分词再加上停用词过滤。下面是我在毕设项目里常用的清洗函数可以直接照搬import re import jieba def clean_text(text: str, is_chinese: bool True) - str: # 去掉 HTML 标签和 URL保留中文、英文、数字和基本标点 text re.sub(r[^], , text) text re.sub(rhttp[s]?://\S, , text) text re.sub(r\S, , text) if is_chinese: text re.sub(r[^\u4e00-\u9fff0-9a-zA-Z。、], , text) else: text re.sub(r[^a-zA-Z0-9,.!? ], , text) return text.strip() def tokenize_text(text: str, is_chinese: bool True) - list: text clean_text(text, is_chinese) if is_chinese: return [w for w in jieba.cut(text) if w.strip()] return [w.lower() for w in text.split() if w.strip()]这段代码的要点是清洗时不要把所有标点都删掉中文的逗号和句号在 LSTM 神经网络眼里是天然的分隔信号分词结果里要过滤空串和纯空格否则词表里会混进大量无意义 token。英文场景记得做小写化不然News和news会被当成两个词词表直接膨胀。接下来是构建词表和序列化。我一般把词表大小限制在 20000 左右出现次数低于 3 的词归入UNK这样既能控制 embedding 矩阵规模又能保证低频词不干扰训练。padding 策略上先统计所有样本的长度分布取 95 分位数作为max_len比盲目设 500 更稳。from collections import Counter import torch from torch.nn.utils.rnn import pad_sequence def build_vocab(tokenized_texts, min_freq3, max_vocab20000): counter Counter() for tokens in tokenized_texts: counter.update(tokens) vocab {PAD: 0, UNK: 1} for word, freq in counter.most_common(max_vocab - 2): if freq min_freq: vocab[word] len(vocab) return vocab def encode_and_pad(tokenized_texts, vocab, max_len128): encoded [] for tokens in tokenized_texts: ids [vocab.get(w, vocab[UNK]) for w in tokens[:max_len]] encoded.append(ids) seqs [torch.tensor(e, dtypetorch.long) for e in encoded] return pad_sequence(seqs, batch_firstTrue, padding_value0)逻辑说明build_vocab先统计全量词频再把高频词按频次编号PAD固定为 0UNK固定为 1这两个 id 在后续 embedding 和 loss 计算里都要用到。encode_and_pad先截断到max_len再用pad_sequence统一成[batch, max_len]的矩阵padding_value0必须和PAD的 id 对应否则 embedding 层会学到无意义的 PAD 向量。最后给一个容易踩的细节序列长度上限不要拍脑袋。我经手的数据集里短新闻的中位长度在 40-60 个 token 之间但 95 分位数可能到 200 以上。你要是把 max_len 设成 500大多数样本只用了前 20%剩下全是 PAD不仅训练慢LSTM 最后一步的隐状态还会被大量 PAD 干扰。正确做法是先跑一句lengths [len(t) for t in tokenized_texts]看一眼分位数再定 max_len这才是真实数据说了算而不是论文模板说了算。2.3 词嵌入随机初始化还是加载预训练向量序列化之后就是词嵌入。很多同学一上来就加载预训练词向量这在大规模数据集上是合理的但在本科毕设的小规模数据下我反而建议先用随机初始化跑通流程再在进阶阶段换成预训练向量做对比。因为预训练词向量比如中文的 Word2Vec 或英文的 GloVe会把 embedding 层直接拉到通用语义空间而虚假新闻检测里很多决定真假的其实是标题里的搭配方式、来源语气这些任务相关信息这些恰恰是通用词向量不敏感的。如果要用预训练向量我的做法是先随机初始化 embedding 层再把预训练权重覆盖进去设置requires_gradTrue让它参与微调。直接冻结 embedding 层会让模型学不到数据里的领域语义在虚假新闻这种文本风格差异明显的任务上很吃亏。另外注意向量维度要和nn.Embedding的embed_dim对齐比如 Word2Vec 是 300 维那embed_dim就设 300别设 128 再硬塞。2.4 数据划分与标签处理别把验证集做成“开卷考试”虚假新闻检测是二分类标签通常映射为 0真实和 1虚假。数据划分上我会用 8:1:1 分出 train / dev / test并且按新闻来源或时间戳分层。为什么要分层因为 LSTM 很容易记住特定来源的措辞风格如果同一个来源的新闻同时出现在训练集和测试集测试准确率会虚高论文里写出来也经不起推敲。分层划分可以用 sklearn 的StratifiedKFold也可以按来源字段 groupby 后抽样。我习惯先把数据按来源聚合成列表再切分并固定随机种子。这里有一个经验值数据集只有几千条时train / dev / test 按 7:2:1 比 8:1:1 更稳因为 dev 集太小早停和调参都会变得很抖动。标签处理上没什么玄学映射成整数即可但要在数据准备阶段就把正负样本数量记录下来后面训练和论文写作都会用到。3. 跑通源码PyTorch 实现 LSTM 虚假新闻检测的核心步骤3.1 数据加载器把 Dataset 和 DataLoader 接好预处理做完下一步是把数据接进 PyTorch。我习惯用一个自定义 Dataset 把“文本张量、标签、真实长度”三样东西打包其中真实长度是给后面pack_padded_sequence用的。from torch.utils.data import Dataset, DataLoader class FakeNewsDataset(Dataset): def __init__(self, texts, labels, vocab, max_len): self.texts [encode_and_pad([t], vocab, max_len)[0] for t in texts] self.labels labels self.lengths [min(len(t), max_len) for t in texts] def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.texts[idx], self.labels[idx], self.lengths[idx]这里encode_and_pad沿用第 2 章的函数返回的texts[idx]已经是[max_len]的张量。注意lengths是截断前的真实 token 数不能直接拿max_len顶上否则pack_padded_sequence等于没做。train_loader DataLoader(train_ds, batch_size64, shuffleTrue) dev_loader DataLoader(dev_ds, batch_size128, shuffleFalse)shuffle训练集打开、验证集关掉。有一个容易被忽略的小问题pack_padded_sequence的lengths需要是 CPU 张量不能放到 GPU 上我在 3.2 的 forward 里会特意处理避免“lengths should be on CPU”的报错。3.2 模型定义embedding LSTM 全连接模型结构很固定三层embedding 把 token id 转成稠密向量LSTM 编码序列全连接层输出二分类 logits。下面是完整定义import torch import torch.nn as nn class LSTMDetector(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers2, num_classes2, dropout0.5, bidirectionalTrue): 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, bidirectionalbidirectional) lstm_out_dim hidden_dim * 2 if bidirectional else hidden_dim self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(lstm_out_dim, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, num_classes) ) def forward(self, x, lengths): emb self.embedding(x) # [B, T, E] packed nn.utils.rnn.pack_padded_sequence( emb, lengths.cpu(), batch_firstTrue, enforce_sortedFalse ) _, (hn, _) self.lstm(packed) # hn: [num_layers * num_directions, B, H]取最后一层的双向状态 last_hidden hn[-2:, :, :].transpose(0, 1) # [B, 2, H] last_hidden last_hidden.reshape(last_hidden.size(0), -1) logits self.classifier(last_hidden) return logits逻辑说明pad_sequence之后的输入x形状是[B, T]embedding 后变[B, T, E]。pack_padded_sequence让 LSTM 只在真实 token 上做循环跳过 PAD 位这样既快又不会被 PAD 干扰。hn的形状是[层数 * 方向数, B, H]双向 LSTM 的最后一层两个方向分别存在倒数第二行和最后一行所以用[-2:, :, :]取出来再拼成[B, 2*H]。如果你改成单向这里必须换回hn[-1]经常有人在切换方向时忘了改这一步loss 不降先查这里。3.3 训练循环与超参设置训练循环本身不复杂但有几个参数是 LSTM 的“默认动作”必须写上def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for x, y, lengths in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x, lengths) loss criterion(logits, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() preds logits.argmax(dim-1) total_loss loss.item() * x.size(0) correct (preds y).sum().item() total y.size(0) return total_loss / total, correct / total参数说明clip_grad_norm_是 LSTM 的保命操作梯度范数裁剪到 5.0能挡住大多数梯度爆炸。优化器用 Adam学习率从1e-3起步batch_size64最多训 20 个 epochdev loss 连续 3 个 epoch 不降就早停。早停实现很简单记录每一轮 dev loss如果当前比历史最优差就计数超过阈值直接 break。我的调参顺序是先固定 embed_dim128、hidden_dim128单层单向跑一个 baseline确认 loss 能稳定下降再改成双向看 dev F1 是否真的提升最后加一层同时把 dropout 提到 0.5。不要一开始就上双向两层否则翻车了你分不清是数据问题还是模型问题。早停加模型保存的完整逻辑长这样best_f1 0.0 patience 0 for epoch in range(20): train_loss, train_acc train_one_epoch( model, train_loader, optimizer, criterion, device ) y_true, y_pred evaluate(model, dev_loader, device) dev_f1 f1_score(y_true, y_pred) if dev_f1 best_f1: best_f1 dev_f1 torch.save(model.state_dict(), best_model.pt) patience 0 else: patience 1 if patience 3: print(fEarly stop at epoch {epoch}) break这里把“保存最优模型”和“早停”绑在一起每次 dev F1 刷新就保存一次权重最终拿best_model.pt做测试集评估。不要等全部 epoch 跑完再手动挑那样既浪费时间又容易在论文里写不清到底哪个模型是最终结果。3.4 评估与结果记录实验表格就是文档说明的骨架训练完成后用 sklearn 的指标函数一次性算清楚from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader, device): model.eval() y_true, y_pred [], [] with torch.no_grad(): for x, y, lengths in loader: x x.to(device) logits model(x, lengths) y_pred.extend(logits.argmax(dim-1).tolist()) y_true.extend(y.tolist()) print(classification_report(y_true, y_pred, target_names[real, fake])) return y_true, y_pred跑完这一节把准确率、精确率、召回率、F1、混淆矩阵都记进实验表。我见过的文档说明最难看的就是“实验结果”一章贴着三行准确率既没有 baseline 也没有控制变量。正确做法是下面这样的表格骨架模型准确率精确率召回率F1单层单向 LSTM0.9120.8910.8560.873双向 LSTM0.9240.9030.8810.892双向 LSTM 注意力0.9310.9160.8940.905每次实验只改一个变量这样论文的“文档说明”部分直接抄这张表再补上训练曲线和混淆矩阵图工作量反而比写一堆描述性文字小。数据、代码、实验表格三样对齐答辩时老师问任何一行数字你都能答出来源。4. 避坑指南LSTM 虚假新闻检测里最容易翻车的 5 个问题4.1 现象训练 loss 降了验证集 F1 却一直不动原因数据不均衡。新闻数据集里真实新闻通常远多于虚假新闻模型学到“全预测为真实”就能拿到很高的准确率但 F1 上不去。解决在 loss 里加类别权重。PyTorch 的CrossEntropyLoss(weighttorch.tensor([w0, w1]))是最快的改法权重按负样本比例算正负比大约 1:4 时设w04, w11。也可以用WeightedRandomSampler在采样时平衡但要注意别把重复样本采太多否则模型会对少数的几条样本过拟合。4.2 现象验证集很好测试集一测就差得离谱原因随机种子没固定或信息泄露。没固定种子时每次结果波动能有 2-3 个点你以为模型“很好”其实是运气好。信息泄露更隐蔽如果用全量数据构建词表再切分数据集测试集的信息已经通过词表渗进训练过程LSTM 容易在 OOV 词的表示上“作弊”。解决任何涉及随机的地方划分、shuffle、初始化先torch.manual_seed(42)词表只用训练集构建验证集和测试集里没见过的词一律映射到UNK。这个习惯要从第一天就养成后面再补很容易漏。4.3 现象loss 变成 NaN原因学习率太大embedding 层没有正确设置padding_idx或者序列里出现极端值。LSTM 对梯度爆炸非常敏感我见过最典型的例子就是忘记给nn.Embedding传padding_idx0PAD 位参与反向传播后梯度直接爆炸。解决先降学习率1e-3改5e-4再检查 embedding 的padding_idx是不是和PAD的 id 一致最后在backward()后加clip_grad_norm_。这三步按顺序做95% 的 NaN 都能解决。排查时先用 CPU 跑一个 batch 看 loss不要直接上 GPU报错信息会清楚得多。4.4 现象训练特别慢一个 epoch 要跑十分钟原因所有序列都 padding 到同样的max_len长尾样本拖慢了整个 batch。如果你的max_len200而大多数样本只有 60将近 70% 的计算都浪费在 PAD 上。解决用pack_padded_sequence或者把max_len再卡紧一点。PyTorch 里只需要在 forward 里对lengths做传入即可不需要手动排序。另外DataLoader 的num_workers2、pin_memoryTrue在小数据上也能省不少时间但别开太多 worker毕设机上 4 个就够。4.5 现象改成双向和加深层数结果反而变差原因模型容量过大过拟合。双向 LSTM 的参数量接近单层单向的 4 倍两层双向就是 8 倍。公开的虚假新闻数据集从几千到几万条不等小数据根本喂不饱大模型。解决先跑单层单向作为 baseline确认能复现合理结果后再逐档加容量。每加一档同步提高 dropout 到 0.5并观察 train 和 dev 的 gap。如果 train 在涨、dev 不涨就是过拟合这时回头减小 hidden_dim 或加 dropout而不是继续加深。如果 dropout 压不住就在优化器里补 L2 正则化torch.optim.Adam(params, lr1e-3, weight_decay1e-5)weight_decay就是 PyTorch 里 L2 正则化的实现方式。LSTM 调参里最玄学的部分就是“层数和方向”我的血泪经验是永远拿 baseline 说话别靠感觉。5. 从及格到优秀让 LSTM 检测模型和论文都加分的三个技巧第一个技巧是双向 LSTM 加注意力。顺着《动手深度学习》里注意力那一章的思路把 LSTM 每个时间步的隐状态做加权求和权重由当前状态和“任务向量”的相似度算出。好处是模型能告诉你“判决依据是哪几个词”论文里放一张注意力热力图比任何文字描述都有说服力。第二个技巧是阈值移动。除了在 loss 里加类别权重还可以在预测阶段不取 0.5 作为阈值而是在 dev 集上搜索让 F1 最大的阈值。虚假新闻检测里漏掉一条假新闻比误伤一条真新闻代价更大阈值向“更愿意判假”的方向移一点往往能让 F1 涨 1-2 个点。import numpy as np from sklearn.metrics import f1_score probas model.predict_proba(val_x)[:, 1] # 取“虚假”那一类的概率 best_thr, best_f1 0.5, 0.0 for thr in np.arange(0.30, 0.70, 0.02): f1 f1_score(val_y, (probas thr).astype(int)) if f1 best_f1: best_thr, best_f1 thr, f1逻辑说明predict_proba输出的是每个类别的概率这里只取索引 1 对应的“虚假”概率。阈值从 0.30 扫到 0.70步长 0.02每次用 dev 集算 F1保留最优阈值。测试时用best_thr代替默认的 0.5这个操作对不均衡数据特别管用。第三个技巧是消融实验。把模型从完整版逐级拆掉注意力、拆掉双向、拆掉预训练词向量三组实验一摆论文的贡献点就立住了。我自己的习惯是每个实验跑三遍取平均标准差不写进论文但自己记录这样答辩被问“你这个结果稳定吗”时心里有底。最后说一个我踩过的坑有一版结果写得特别漂亮但忘记记录数据划分种子回头复现时怎么都跑不回那个准确率最后只能重跑全部实验。从那以后我每个实验文件名的后缀都是模型_数据版本_种子比如lstm_bi_v2_seed42。这个习惯能让你的毕设少走很多弯路希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑