简介:这份资源是本科毕业设计级别的深度学习LSTM虚假新闻检测项目,包含可直接运行的Python源码、训练/测试数据集和说明文档,主要面向计算机、通信、人工智能、自动化等专业学生及从业者,可用于课程设计、大作业或毕业设计参考,也适合入门者理解文本分类与序列建模流程。压缩包共6个文件,涵盖CSV数据文件(train/news/test)、Python主程序、停用词表及README说明文档,整体约5.86MB,结构紧凑清晰。项目答辩评审分达到98分,代码经过调试测试,具备较高的完整性与可借鉴价值;基础较好的读者可直接在其上扩展功能或调整模型。目前已有273人学习下载,适合作为深度学习/NLP方向实战项目的对照样例与二次开发起点。
1. LSTM虚假新闻检测毕设:这份源码把文本分类链路走通到什么程度
当我第一次打开这份基于深度学习LSTM的虚假新闻检测源码包时,最直观的感受是它的工程完成度:train.news.csv训练集、test.news.csv测试集、submit.csv预测结果,加上一个main.py主脚本和chinesestopwords.txt停用词表,整个Python实现没有多余包装。项目用深度学习LSTM把中文新闻文本从原始字符串一路转化为真假二分类概率,文档注明答辩评审98分,代码经过调试可运行。对正在为毕业设计、课程大作业找方向的计算机、人工智能、自动化专业学生来说,它最大的价值在于把文本分类最容易被卡住的数据预处理和序列对齐环节全部摊开在源码里。有基础的人可以直接改模型结构做创新,初学者也能照着main.py一步步跑通整个流程。
2. 数据预处理:从CSV到LSTM能吃的定长序列,三个关键步骤
2.1 先看数据:train.news.csv的字段结构与标签分布
拿到train.news.csv,第一步不是急着写模型,而是把数据长什么样搞清楚。常见做法是直接用pandas读进来,打印头部信息和标签分布:
import pandas as pd df = pd.read_csv('train.news.csv', encoding='utf-8') print(df.columns.tolist()) print(df.head()) print(df['label'].value_counts()) print(f'总样本数: {len(df)}')这段代码做了三件事:columns.tolist()列出全部字段名,确认是否有标题、正文、标签等列;head()看前五行样例,判断文本是否完整、有没有爬虫留下的脏字符;value_counts()输出正负样本各自数量,判断类别是否均衡。对虚假新闻检测这种二分类任务来说,标签含义需要确认清楚——是0代表真实、1代表虚假,还是反过来,直接决定评估时正类怎么取。
需要格外注意两步:用df.isnull().sum()检查每列的空值数量,新闻爬虫数据里正文缺失是家常便饭,缺失行要么删除要么统一填充为空字符串,不能留着NaN进后续分词流程;再看一下text列的长度分布,用df['text'].apply(len).describe()能看到最小值、最大值、中位数。这个分布直接决定后面序列填充的maxlen怎么设,新闻文本短的只有几十个字符,长的可能上万字符,差距非常大。
这部分的核心结论是:标签不均衡是虚假新闻数据集的常态,大多数时候真实样本远多于虚假样本。如果分布极端,比如虚假样本只有5%,那后续训练必须处理类别不均衡问题,否则模型会学会“全部预测为真实新闻”这种偷懒策略,准确率虚高但没有任何实用价值。我一般会在这一步就把train_test_split做好,固定随机种子切出验证集,防止后面用validation_split时数据顺序影响切分质量。
2.2 中文分词与停用词过滤:为什么说这是效果下限的保证
中文文本和英文本质区别在于没有天然空格分词,必须先切成词序列才能做Embedding。这个项目里最常用的分词工具是jieba。一个典型的中文新闻预处理函数长这样:
import jieba stopwords = set() with open('chinesestopwords.txt', 'r', encoding='utf-8') as f: for line in f: stopwords.add(line.strip()) def clean_text(text): text = str(text).replace('\n', ' ').replace('\r', ' ') # 去除URL和@提及,新闻数据里这类噪音很常见 text = re.sub(r'http\S+|www\.\S+', '', text) words = jieba.lcut(text) # 过滤停用词、单字符和纯空白 words = [w.strip() for w in words if w.strip() and w not in stopwords and len(w.strip()) > 1] return ' '.join(words)这段代码逻辑清晰:先用replace把换行符统一成空格,避免文本被错误截断;再用正则去掉URL,新闻数据里链接通常和新闻内容无关,留着只会增加噪音;jieba.lcut做精确模式分词,返回词列表;最后过滤时同时去掉停用词和长度为一的字符。单字词在中文新闻里大多是“啊、呢、这、那、又、也”这类功能词,过滤掉能让序列更紧凑。
参数说明:chinesestopwords.txt这个文件就是为此准备的,里面通常是“的、了、吗、在、是、和”这类高频功能词。但要非常小心停用词表不能过度清洗——如果表里收录了“不”“没”“无”“别”这些否定词,清洗后“不真实”会变成“真实”,语义完全反转,模型再训练也学不到正确规律。我在复现类似项目时踩过这个坑,后来干脆在清洗逻辑里加了一个否定词白名单,强制不过滤这些词。
分词质量直接影响后面所有环节。建议在写完clean_text后,抽出五条正样本和五条负样本打印分词结果,人眼扫一遍确认没有严重问题再往下走。这一步花五分钟,能省下后面调试模型的一天时间。
2.3 序列填充与词表构建:maxlen和vocab_size的取值逻辑
LSTM要求同一个batch内的所有样本长度一致,这是张量运算的硬约束。新闻文本长短差异极大,所以必须做两件事:构建词表、定长填充。Keras的Tokenizer加pad_sequences是最经典的做法:
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences VOCAB_SIZE = 5000 MAX_LEN = 200 tokenizer = Tokenizer(num_words=VOCAB_SIZE, oov_token='<OOV>') tokenizer.fit_on_texts(train_texts_clean) train_seq = tokenizer.texts_to_sequences(train_texts_clean) train_pad = pad_sequences(train_seq, maxlen=MAX_LEN, padding='post', truncating='post')逻辑说明:Tokenizer遍历全部语料,按词频从高到低排序取前5000个词构成词表,词频太低的词统一映射到 。pad_sequences把每条序列补0或截断到200个词。padding='post'表示在序列尾部补0,truncating='post'表示超长部分从尾部截掉。
参数说明:VOCAB_SIZE取5000是文本分类任务的经验起步值,新闻语料完整词表通常一两万,5000能覆盖大部分有效信息,同时控制Embedding矩阵规模。MAX_LEN设200对中短新闻够用,但具体值应该根据第2.1节统计的长度分布来定——我一般取训练集文本长度的90分位数,这样大部分样本不需要被截断。如果你发现长新闻预测总是出错,优先看是不是MAX_LEN太小,把尾部结论性内容截掉了。
这里有一个非常隐蔽但后果严重的坑:Tokenizer的fit_on_texts只能传训练集文本,绝对不能把测试集文本一起传进去。一旦词表构建时看到测试集,模型就等于提前接触了测试集分布,评估结果会虚高。这是典型的数据泄漏,答辩时被问到会被直接扣分。正确做法是tokenizer只用训练集fit,测试集只调用texts_to_sequences。
3. LSTM训练:Embedding维度、hidden_size与batch_size怎么设
3.1 网络结构:Embedding、LSTM、Dense三层之间的维度匹配
这个项目main.py里的模型是典型的“Embedding + LSTM + Dense”文本分类骨架。用Keras序列模型写出来大致是这样:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout model = Sequential([ Embedding(input_dim=VOCAB_SIZE + 1, output_dim=128, input_length=MAX_LEN), LSTM(units=128, dropout=0.3, recurrent_dropout=0.3), Dense(units=64, activation='relu'), Dropout(0.3), Dense(units=1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])逻辑说明:Embedding层把每个词的整数编号映射成128维稠密向量,input_dim写成VOCAB_SIZE+1是因为要留出padding符号0的位置。LSTM层设置128个隐藏单元,dropout=0.3作用于输入到隐层的连接,recurrent_dropout=0.3作用于循环连接。LSTM的默认输出是最后一个时间步的隐状态,形状为(batch_size, 128),直接接全连接层。最后用sigmoid输出一个0到1之间的概率。
参数说明:hidden_size=128是多数场景的稳妥起步值。数据量大可以升到256,数据量小用64反而更稳,因为大模型在小数据上学到的全是噪声。Embedding维度128和hidden_size 128的搭配在工程上很常见,如果改了Embedding维度,LSTM的units最好跟着一起调,避免维度瓶颈。Dense中间层64维足够表达特征组合,再加一层Dropout防止全连接层过拟合。
关于随机种子:为了让训练结果可复现,建议在import之后固定numpy和tensorflow的随机种子。没有固定种子的话,每次跑出来的指标会上下浮动几个点,答辩时如果被要求复现实验,结果对不上会很尴尬。
3.2 损失函数与优化器:为什么是binary_crossentropy配Adam
虚假新闻检测本质是二分类,标签为0或1,所以损失函数用binary_crossentropy。它计算的是模型预测概率分布与真实标签之间的交叉熵,预测越不确定,损失越大。sigmoid激活函数输出的值天然就是概率,两者配套使用是Keras二分类的标准组合。
优化器选Adam而不是SGD,核心原因是Adam对学习率不敏感、收敛快。Adam融合了Momentum和RMSProp的思路,默认学习率0.001在绝大多数LSTM文本任务上都能正常收敛。SGD需要手动调学习率和动量,训练周期长,对初学者不友好。如果后续想追求极致精度,可以先让Adam跑出基线,再换成SGD做最后一轮微调,这是比赛里常见的调参路径。
训练时还有个容易被忽略的配置:类别权重。如果第2.1节发现标签不均衡,要在model.fit里传入class_weight:
class_weight = {0: 1.0, 1: 3.0} # 正样本权重放大3倍 model.fit(train_pad, train_labels, validation_split=0.2, epochs=10, batch_size=64, class_weight=class_weight, verbose=1)这是处理类别不均衡最直接的手段。给少数类更高的loss权重,模型就会更努力地学习少数类的特征。权重具体设多少,我一般看正负样本比例,比如负样本是正样本的3倍,就把正样本权重设为3。权重太大会导致模型过度偏向预测正类,把一堆真实新闻误判成假新闻,需要观察验证集F1来折中。
3.3 训练循环与早停:用EarlyStopping对抗过拟合
LSTM训练里最常见的现象是:训练loss一路下降,验证集准确率涨到某个点就开始回落。这是过拟合的标志——模型在背训练集的答案,而不是学习泛化规律。解决方式主要有两个:增加Dropout强度,以及使用EarlyStopping在验证集指标不再改善时自动停止。
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping( monitor='val_loss', patience=3, restore_best_weights=True ) history = model.fit( train_pad, train_labels, validation_split=0.2, epochs=20, batch_size=64, class_weight=class_weight, callbacks=[early_stop], verbose=1 )逻辑说明:EarlyStopping监控验证集损失,连续3轮没有下降就终止训练。restore_best_weights=True保证停止时模型权重自动恢复到验证集表现最好的那一轮,不需要手动比较和加载历史权重。这样就避免了“设了20个epoch但第7轮已经过拟合”的尴尬。
实践参考:我在跑LSTM虚假新闻检测时,模型一般在5到8轮达到验证集峰值。超过10轮后,训练loss会继续降到很低,但验证loss开始反弹,这就是在背答案的信号。如果你用的是自己的数据集,观察两条loss曲线交叉点,交叉点附近就是最佳停止位置。还有一个经验是batch_size不要设太小,小于16时梯度更新太频繁,训练抖动明显,收敛反而不稳定。
4. 预测与评估:准确率之外,召回率和F1才是答辩护城河
4.1 混淆矩阵与多指标评估:准确率可能有欺骗性
虚假新闻检测最大的评估陷阱是类别不均衡带来的准确率虚高。比如数据集中90%是真实新闻,模型什么都不学,全部预测为真实新闻,准确率就有90%,但它一条假新闻都检测不出来。所以评估必须看更细的指标:精确率、召回率、F1。
from sklearn.metrics import precision_recall_fscore_support, confusion_matrix import numpy as np y_pred_prob = model.predict(val_pad) y_pred = (y_pred_prob > 0.5).astype(int) tn, fp, fn, tp = confusion_matrix(val_labels, y_pred).ravel() accuracy = (tp + tn) / (tp + tn + fp + fn) precision = tp / (tp + fp) if (tp + fp) > 0 else 0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 print(f'准确率: {accuracy:.4f}') print(f'精确率: {precision:.4f}') print(f'召回率: {recall:.4f}') print(f'F1: {f1:.4f}')逻辑说明:混淆矩阵的四个值分别是:tn真负、fp假正、fn假负、tp真正。精确率衡量模型预测为假新闻的样本里有多少是真的假新闻——精确率低说明误报多。召回率衡量所有真实假新闻里模型找回了多少——召回率低说明漏报多。F1是两者的调和平均,综合反映模型在“少误报”和“少漏报”之间的平衡。
参数说明:阈值0.5是默认决策边界,但实际应用中可以根据需求调整。如果业务上更担心漏报假新闻造成误导,可以调低阈值到0.3,模型会预测出更多假新闻候选,召回率上升但精确率下降。这个trade-off需要根据场景决策,没有对错。答辩时能主动讲清阈值选择逻辑,反而是加分项。
4.2 生成submit.csv:推理流程必须与训练完全对齐
测试集预测是整个流程最后的临门一脚,这里翻车会让前面的所有努力白费。核心原则只有一条:测试集预处理和训练集完全一致,包括同一个tokenizer实例、同一个clean_text函数、同一个MAX_LEN。
def build_submit(test_texts, tokenizer, model, output_path='submit.csv'): test_clean = [clean_text(t) for t in test_texts] test_seq = tokenizer.texts_to_sequences(test_clean) test_pad = pad_sequences(test_seq, maxlen=MAX_LEN, padding='post', truncating='post') probs = model.predict(test_pad, batch_size=64) submit = pd.DataFrame({ 'id': range(len(test_pad)), 'label': (probs > 0.5).astype(int).reshape(-1) }) submit.to_csv(output_path, index=False, encoding='utf-8')逻辑说明:预测流程复制训练流程,不能重新初始化tokenizer,不能用不同的清洗规则,否则词表编号错位,模型预测退化成随机猜测。输出格式方面,多数评测任务要求label是整数0或1,保存成True/False或者浮点概率都会被判错。
参数说明:batch_size在推理时可以和训练时不同,显存占用比训练小,内存够的话调大能加快速度。输出文件用utf-8编码保存,但不要带BOM头,某些评测系统的CSV解析器对BOM兼容性差,会读乱第一个字段名。如果评测要求submit.csv必须包含特定的列名,先提交一次看反馈,按反馈修正格式。
4.3 训练曲线解读:用val_loss提前发现模型异常
训练结束后,把history里的损失曲线画出来看趋势,比只看最后一个epoch的准确率信息量大多了:
import matplotlib.pyplot as plt plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.show()判断标准非常直接:训练loss和验证loss同时下降,模型在正常学习;训练loss下降但验证loss停滞或回升,过拟合已经开始。第二个情况出现时,EarlyStopping会自动截停,但你仍然需要观察回升的幅度来判断模型是否调过头。如果验证loss一直震荡不下降,可能是学习率太大,把Adam的学习率从0.001降到0.0003再试。
还有一个技巧:打印模型结构确认参数数量。model.summary()输出每层参数量和总参数量,如果总参数量远大于训练样本数,模型几乎必然过拟合。此时应该缩小Embedding维度或LSTM隐层单元数,而不是盲目加训练轮次。
5. 避坑指南:复现虚假新闻检测时的五个典型翻车与修复
5.1 中文CSV乱码:读出来全是锟斤拷
现象:pd.read_csv后打印head(),中文新闻正文显示为System乱码,或者报UnicodeDecodeError,程序直接崩溃。
原因:CSV实际编码不是utf-8。中文数据集的CSV文件大量使用gbk或gb18030编码,尤其是从旧系统导出的数据,用utf-8解码当然错位。
解决:读取时带encoding参数逐个试。我的习惯顺序是utf-8、gb18030、gbk、latin1,哪个不报错且文本正常显示用哪个。gb18030是gbk的超集,兼容性最好,绝大多数中文CSV都能用它正确打开。实在不放心就先以二进制模式读文件头几个字节,用chardet库检测编码再回填。
5.2 停用词表把否定词过滤掉,语义反转
现象:数据清洗后“不真实”变成“真实”,“无法确认”变成“确认”,模型训练后验证集F1异常低,预测结果几乎不可用。
原因:停用词表里收录了“不”“没”“无”“别”等否定词。清洗逻辑一刀切把整个词表里的词全部过滤,否定语义彻底丢失,句子意思完全反转。
解决:清洗逻辑里加否定词保护白名单,强制保留“不、没、无、别、莫、休”等词。更推荐的做法是人工检查停用词表内容,只保留“的、了、吗、呢、在、是、和”这类语气词和连接词,不要图省事直接用网上下的全量停用词表。这个坑最坑人的地方在于它不会让程序报错,只会默默降低模型上限,隐蔽性极强。
5.3 标签不均衡:模型学会了无脑预测“真实新闻”
现象:训练完成后验证集准确率90%以上,但F1不到0.2,预测结果几乎全是真实新闻,一条假新闻都抓不出来。
原因:数据集中真实新闻占比远大于虚假新闻,模型发现直接预测多数类就能把loss压得很低,根本不需要学习深层语义特征。
解决:三个手段叠加使用。第一,class_weight给少数类更大权重,改动最小效果最快;第二,过采样少数类样本,让正负样本比例接近1:1;第三,评估指标换成F1而不是准确率,让模型的目标和评测目标对齐。我一般先试class_weight,观察F1的变化,不够再加过采样。
5.4 验证集表现虚高:词表构建泄漏了测试集信息
现象:训练时验证集准确率95%,测试集准确率掉到80%,差距大得离谱。换了随机种子再跑,测试集结果依然明显低于验证集。
原因:Tokenizer在fit_on_texts时把训练集和测试集全传进去了。测试集的高频词进了词表,模型在训练时虽然没直接见过测试集内容,但Embedding层对测试集词汇的映射已经经过了调优,属于软性的数据泄漏。
解决:严格分离。tokenizer只fit训练集文本,测试集只做texts_to_sequences。同理,任何归一化操作比如StandardScaler也只能fit训练数据,用训练集的均值和方差去变换验证集和测试集。答辩时被问到数据泄漏问题,能说清这个细节非常加分。
5.5 MAX_LEN设置不当:长新闻的正确率崩了
现象:短新闻预测基本正确,超过300字的长新闻反复误判,特别是结论出现在文本后半段的新闻。
原因:MAX_LEN设太小,比如只有150或200,配合truncating='post'策略,把长文本尾部信息全部截掉了。很多新闻的标题结论和事件定性恰恰出现在最后几句话里。
解决:先统计训练集文本长度分布,把90或95分位数作为MAX_LEN参考值。如果任务对长新闻敏感,改用truncating='pre'只截开头、保留结尾,或者分段截取关键信息拼接。还有一种方案是输入直接用“标题+正文前200字+正文最后200字”,把最多信息压缩进固定长度。
6. 进阶技巧:用注意力机制给LSTM模型再提一个百分点
基础流程跑通后,我建议的下一步不是加深网络,而是在LSTM上方加一层注意力机制。原因是LSTM默认输出序列最后一个时间步的隐状态,这相当于让模型“用最后一句话代表全篇文章”。但虚假新闻的判别特征往往散落在文本不同位置——开头可能有耸动标题,中段可能是不可靠信源,结尾可能是诱导转发。注意力机制让模型在每个时间步的隐状态上计算权重,自己决定哪些位置更重要。
用Keras实现一个轻量注意力层,代码大概长这样:
from tensorflow.keras.layers import Layer import tensorflow as tf class Attention(Layer): def __init__(self, units): super().__init__() self.units = units def build(self, input_shape): self.W = self.add_weight(shape=(input_shape[-1], self.units), initializer='glorot_uniform') self.b = self.add_weight(shape=(self.units,), initializer='zeros') self.u = self.add_weight(shape=(self.units,), initializer='glorot_uniform') super().build(input_shape) def call(self, inputs): # 每个时间步计算注意力打分 score = tf.nn.relu(tf.tensordot(inputs, self.W, axes=1) + self.b) attn_weights = tf.nn.softmax(tf.tensordot(score, self.u, axes=1), axis=1) # 按注意力权重对隐状态做加权和 context = tf.reduce_sum(inputs * tf.expand_dims(attn_weights, -1), axis=1) return context接入时需要把LSTM的输出模式改成return_sequences=True,让模型输出全部时间步的隐状态,而不是只给最后一个:
model = Sequential([ Embedding(VOCAB_SIZE + 1, 128, input_length=MAX_LEN), LSTM(128, dropout=0.3, recurrent_dropout=0.3, return_sequences=True), Attention(64), Dropout(0.3), Dense(1, activation='sigmoid') ])我自己在实验里的体验是:加注意力后F1通常能提升一到三个百分点,模型对“权威信源是否被援引”这类关键线索的敏感度明显变高。缺点是训练时间增加约20%,而且如果注意力层的units和LSTM的hidden_size相差太大,会出现收敛变慢或验证loss震荡。从那以后我每次在这个项目上迭代,都会先跑一个不加注意力的baseline,再逐项往上加模块,每次只改一个变量,对比曲线的变化。没有基线的调参全是撞运气,这个习惯帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取