
简介一份整合机器学习、深度学习与BERT模型的虚假新闻检测项目源码面向自然语言处理文本分类任务适用于计算机、电子信息、数学等专业学生的课程设计、期末大作业或毕业设计参考。项目源自南开大学Python语言程序设计课程以中文微信消息标题为训练数据系统对比了朴素贝叶斯、支持向量机、随机森林、逻辑回归等传统算法与神经网络、BERT微调方案的性能并给出准确率、召回率、F1与AUC等评估指标便于理解不同方法论在文本分类中的差异。压缩包共76个文件包含45个Python脚本、XML配置、Jupyter Notebook及Markdown说明整体仅163KB按机器学习、深度学习、BERT等目录模块组织并配有README与调试记录方便对照学习与二次开发。目前已有1138人学习下载源码可直接运行适合有一定Python基础并希望深入理解自然语言处理分类与模型调优的读者。1. 虚假新闻检测项目源码先建立三级技术栈的全局观拿到这份基于机器学习深度学习bert方法的虚假新闻检测项目源码.zip先别急着解压点 train。这类源码包我接过不止一个名字越全越容易在“能打开”到“能复现”之间翻车数据标签口径不统一、机器学习基线和 BERT 的结果对不上、显存一上来就爆。这篇笔记把整条链路拆开——先用 TF-IDF 加逻辑回归拿到及格线再用 TextCNN、BiLSTM 做深度学习阶段的对比最后微调 BERT 冲上限每一级都配上代码、参数和退出条件。适合三类人拿它做毕业设计的新手、要跑比赛 baseline 的队伍、以及公司内容风控想快速冷启动的工程师。不要指望一个压缩包解压完就能跑出漂亮结果。它给你的是一套可运行的框架真正耗时间的永远是数据清洗、标签审核和踩坑排查。2. 数据与预处理标签口径和文本清洗决定后面所有模型的上限2.1 数据集选型别让模型在脏标签上学“假动作”做虚假新闻检测模型结构再花哨数据不行全都白搭。业界有个不成文的说法数据决定了模型的上限模型只是在逼近这个上限。所以拿到源码后第一件事不是读模型代码而是搞清楚两个问题数据从哪里来标签怎么定义的。常见做法是直接用公开数据集。中文场景下微博谣言相关集合、疫情期间的谣言集都是经常被用的英文场景用 FakeNewsNet、LIAR 这一类。选数据集时我一般卡三个硬指标数据量至少一万条起步太少的话 BERT 微调根本学不出泛化能力标签必须是明确的真假二分类或多分类而不是“可转发”“已辟谣”这种含混口径数据要有时间跨度否则你没法做按时间切割的验证集。这里有个很容易被忽略的坑划分训练集和验证集时不要随机切要按发布时间切。新闻的特点是同一个事件会衍生出一堆改写稿随机切很容易让同一事件的不同报道同时出现在训练集和验证集里验证分数会虚高得离谱。按时间切虽然会让验证集更难模型没见过新事件但那才是真实上线后的情况。2.2 文本清洗与去重一个脚本清掉八成脏数据源码包里一般会有一个 preprocess 或 clean 之类的脚本没有的话自己写一个也就几十行。我会在清洗阶段做三件事统一列名、去空白与特殊符号、按文本相似度去重。第一件事特别现实因为不同的数据集源的列名五花八门有的叫 label有的叫 tag有的叫 is_fake统一成 label 能省掉后面所有模型脚本的麻烦。import pandas as pd import re import hashlib def norm_text(text: str) - str: 归一化去空白、去特殊符号、统一小写 if not isinstance(text, str): return text re.sub(r[\s\u3000], , text) text re.sub(r[^\w\u4e00-\u9fa5], , text) return text.lower() def text_hash(text: str) - str: 基于归一化文本的哈希用于去重 norm norm_text(text) return hashlib.md5(norm.encode(utf-8)).hexdigest() def load_and_deduplicate(csv_path: str): df pd.read_csv(csv_path) # 列名统一源码里可能是 label/tag/is_fake统一下 label_col [c for c in df.columns if c.lower() in (label, tag, is_fake, labels)] if not label_col: raise KeyError(找不到标签列请检查数据集的列名) df[label] df[label_col[0]] # 去掉过短的文本短文本对真假判断没有信息量 df df[df[text].map(lambda x: len(norm_text(x)) 30)].copy() # 哈希去重同一事件改写稿只保留一条 df[_hash] df[text].map(text_hash) df df.drop_duplicates(subset_hash, keepfirst).drop(columns_hash) # 标签统一为 0/10 真1 假 df[label] df[label].map(lambda x: 1 if str(x) in (1, 真, fake, True) else 0) return df[[text, label]]这个脚本的逻辑很简单但参数值得说两句。len(norm_text(x)) 30是过滤短文本这个阈值可以按你的数据调如果新闻源本身就有很多短消息降到 20 也行。哈希去重有个特点它只去“完全一样”的文本改写稿改两个字就对不上了。想抓近似重复可以用 SimHash 或 MinHash但效果往往不值得它引入的复杂度我先用精确 hash 兜底。2.3 跑通预处理后先看分布再动手训练清洗完不要急着进模型阶段。先花两分钟看类别分布和文本长度分布这两个数字几乎决定了后面所有的模型选型。python -c import pandas as pd df pd.read_csv(clean_data.csv) print(df[label].value_counts(normalizeTrue)) print(df[text].str.len().describe()) 类别分布的意义在于如果假新闻只占 5%那么无脑预测“真”就有 95% 准确率后面所有模型的评估指标都得围着少数类转。文本长度分布的意义在于如果大部分文本在 200 字以内TextCNN 这种短文本模型就够用如果动辄上千字BERT 的输入截断策略就要单独设计。这一步跑完你应该已经能预判哪个模型会出什么效果了。我一般会在笔记本上记下这三行输出作为后面每一轮实验的对标基准。3. 机器学习基线TF-IDF 逻辑回归先拿到一个能对比的及格线3.1 为什么先跑基线而不是直接上 BERT很多人拿到这种源码包习惯性地跳过机器学习部分直接冲 BERT理由是“BERT 效果最好”。这是典型的把手段当目标。BERT 是效果好但它是个黑匣子训练慢、显存贵、出了问题很难定位。而 TF-IDF 加逻辑回归跑一遍只要几秒钟你得到的不是一个最终方案而是一条参照系如果后面 TextCNN 和 BERT 连逻辑回归都没跑赢那不是模型的问题是数据或训练流程出了问题。这也是一条完整的机器学习应用流程先定评估口径再选候选模型最后对比筛选。作为一线工程师我几乎不会在没有基线的情况下直接上大模型。逻辑回归给出的概率是校准过的相对而言它还能直接输出每个词对决策的贡献权重这在排查标签噪声时特别有用——如果模型对某个词给了极高权重你去看那些样本十有八九是标注错了。3.2 本地跑通 TF-IDF 逻辑回归的最小代码把这套代码用起来不需要 GPU一台普通笔记本就够了。核心就三段文本向量化、训练、交叉验证。import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report, f1_score df pd.read_csv(clean_data.csv) X, y df[text], df[label] # 文本向量化词频 - TF-IDF vectorizer TfidfVectorizer( max_features100000, # 词表上限防过拟合 ngram_range(1, 2), # 保留相邻词组合能抓住“不实”“谣言”这种短语 sublinear_tfTrue, # 用 1log(tf) 平滑词频削弱高频词 min_df2, # 至少在 2 篇文档出现过的词才保留 stop_words[] # 不单独做停用词表TF-IDF 本身会降权高频词 ) # 逻辑回归C 越小正则越强balanced 自动调类别权重 clf LogisticRegression(C1.0, class_weightbalanced, max_iter1000, n_jobs-1) # 5 折交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) macro_f1s [] for train_idx, val_idx in skf.split(X, y): X_tr, X_val X.iloc[train_idx], X.iloc[val_idx] y_tr, y_val y.iloc[train_idx], y.iloc[val_idx] X_vec vectorizer.fit_transform(X_tr) X_val_vec vectorizer.transform(X_val) clf.fit(X_vec, y_tr) y_pred clf.predict(X_val_vec) macro_f1s.append(f1_score(y_val, y_pred, averagemacro)) print(classification_report(y_val, y_pred, digits3)) print(Macro-F1:, sum(macro_f1s) / len(macro_f1s))这段代码里的参数都不是玄学每个都有明确目的。ngram_range(1, 2)单独用 unigram 抓不到“毫无根据”这种包含否定词的短语加上 bigram 就能把“不实消息”保留成一个特征。sublinear_tfTrue是为了削弱“的”“了”这种高频无意义词的绝对数量优势比硬砍停用词表更稳。class_weightbalanced处理类别不平衡的方式是给少数类更高的错分代价比下采样省事数学上也等价于给损失函数加权。3.3 别只盯着 accuracy混淆矩阵和 F1 才是紧要的用这份源码的人在评估阶段最容易犯的错就是只看 accuracy。假设数据集里真新闻占 95%模型全部预测“真”accuracy 是 95%但这个模型毫无用处。虚假新闻检测的本质是少数类识别所以必须看少数类的 precision 和 recall以及综合两者的 F1。精确率在意的“我抓出来的假新闻里有几成是真的假新闻”召回率在意“真正的假新闻里我抓到了几成”。业务侧通常更看重召回率因为漏掉一条假新闻的代价远大于误伤一条真新闻。这时候分类报告里的 macro-F1 就比 accuracy 可靠得多它把两个类别同等对待不会被大头类别带偏。如果跑完基线发现少数类 F1 很低先别急着换模型。把预测错误的样本捞出来看大概率能看到两类问题一类是标签本身就是错的一类是这条“假新闻”用词和真新闻没有区别、人类都分不出来。这两类问题换任何模型都解决不了必须回数据层修。4. 深度学习与 BERT从 TextCNN、BiLSTM 到微调 BERT 的完整链路4.1 选型原则文本长度和算力约束决定用哪一级机器学习阶段看的是“词有哪些”深度学习阶段看的是“词的顺序和上下文”BERT 阶段看的是“全文语义关联”。这三个层次不是替代关系是递进关系。实际选型时我一般用两条硬约束来卡第一是文本长度第二是算力。如果数据绝大部分在 200 字以内TextCNN 是性价比之王训练速度快、参数少、不容易过拟合。如果句子之间有长距离依赖比如“他先声称 X三天后又说 Y但 Z 证明他在撒谎”BiLSTM 能捕捉这类序列关系。如果这两者都跑完了还想继续提升效果再上 BERT。动手深度学习的第一步不是调参而是先把这三者的边界画清楚省得后面用 BERT 跑短文本既浪费算力又提升有限。4.2 TextCNN 的 PyTorch 实现与关键参数TextCNN 的思路是把文本看成一行像素用不同宽度的卷积核去抓局部 n-gram 特征。三个不同尺寸的卷积核并行相当于同时看 2-gram、3-gram、4-gram 的短语。代码实现很稳定几乎不用改。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim100, num_filters256, num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, kernel_sizeks, paddingks // 2) for ks in (2, 3, 4) # 三种窗口并行 ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * 3, num_classes) def forward(self, x): # x: (batch, seq_len) emb self.embedding(x).transpose(1, 2) # (batch, embed_dim, seq_len) pooled [torch.max(nn.functional.relu(conv(emb)), dim2).values for conv in self.convs] out self.dropout(torch.cat(pooled, dim1)) return self.fc(out)这里paddingks // 2是为了让卷积输出长度不变避免 maxpool 之后信息丢失nn.MaxPool用的是全局最大池化取序列维度上最强的特征dropout0.5是 TextCNN 的标准配置加太低容易过拟合。训练参数我一般给 batch_size64、Adam 学习率 1e-3、epochs 上限 10 轮配合早停 patience3。TextCNN 的局限性在于它只抓局部短语抓不到“开头说 A、结尾说 B”这种跨段落的矛盾。这时候就需要 BiLSTM。4.3 BiLSTM 适合长新闻但训练更慢BiLSTM 的核心思路是让网络从头到尾和从尾到头各读一遍文本然后把两个方向的信息拼起来这样每个位置的表示都同时包含左邻和右邻的上下文。class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim100, hidden_size128, num_layers2, num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM( embed_dim, hidden_size, num_layersnum_layers, bidirectionalTrue, # 双向就是两份隐藏层 batch_firstTrue, dropout0.5 if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size * 2, num_classes) # 双向拼接 def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) out, (h_n, c_n) self.lstm(emb) # out: 所有时刻输出 # 对长新闻mean pooling 比取最后时刻更稳 pooled torch.mean(out, dim1) # (batch, hidden*2) return self.fc(pooled)参数上比较关键的是hidden_size128和num_layers2。单层 LSTM 对新闻这种长文本往往欠拟合三层以上在数据量不够时又容易过拟合。双向拼接意味着最后的特征维度是hidden_size * 2全连接层的输入维度别写错。取最后时刻的输出是很多教材里的写法但对长文本不稳定——最后一步的隐状态可能被尾部几个词主导所以这里用 mean pooling把全序列的信息平均下来。代价就是训练速度明显比 TextCNN 慢尤其在序列长度超过 500 时LSTM 的循环展开会让反向传播非常耗时。这时候先把序列截断到 300 以内是明智的。4.4 微调 BERT模型选型、max_length 与学习率BERT 阶段是这份源码的压轴戏。中文场景下最常见的做法是加载bert-base-chinese如果追求更高上限用哈工大的roberta-wwm-ext它全词掩码对中文更友好但模型文件更大加载更慢。两者在评估指标上的差距通常在 1-2 个点以内数据量少时这个差距都可以忽略。BERT 模型实操的关键不是模型结构而是微调参数。from transformers import AutoTokenizer, AutoModelForSequenceClassification, AdamW model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2) # 训练参数按显存来16G 显卡可以 batch16、max_length256 batch_size 16 max_length 256 lr 2e-5 epochs 4 # tokenize截断到 max_length别舍不得 def encode(texts, labels): enc tokenizer( texts, max_lengthmax_length, paddingmax_length, truncationTrue, return_tensorspt ) enc[labels] torch.tensor(labels) return enc参数上learning_rate2e-5是 BERT 微调的惯例高于 5e-5 很容易把预训练特征冲掉俗称“灾难性遗忘”。weight_decay0.01可以加在 AdamW 里只对非偏置和层归一化参数生效。warmup比例设 0.1前 10% 的 step 让学习率从 0 线性爬到 2e-5避免刚开始训练就大步走崩。显存不足时先降max_length而不是先降batch_size。把 512 截到 256显存占用直接减半而且新闻这种文体的关键信息通常集中在前 200 字内。截断之后如果 F1 反而更低再来检查是不是关键信息真的落在后半段。5. 避坑指南跑这份源码最容易翻车的 5 个现场5.1 数据与源码包自身的三个坑现象解压后直接跑train.py立刻报KeyError: label。原因数据集的列名不统一。源码里写死的列名是label但实际数据可能是labels、tag、is_fake甚至两列都有但含义不同。这是这种打包源码最常见的粗糙点因为作者自己用的数据集和大家下载到的数据集往往不是同一个。解决在数据加载后立刻统一列名打印前五行确认。不要直接改源码里的列引用统一在入口处做一次映射后面所有脚本就都不用动了。现象清洗完之后正负样本比例悬殊到 1:20模型把整片验证集都预测成“真”。原因数据本身类别不平衡而模型没有感知到这种不平衡。解决逻辑回归加class_weightbalanced深度模型在损失函数里给少数类加权或者在采样时做正样本过采样。最彻底的办法是在数据收集阶段补充更多假新闻样本但从源码起步的话先调类别权重最现实。现象BERT 训练到第 3 个 epochloss 突然变成 NaN。原因学习率太高或者有个别样本包含极端长串数字/特殊字符BERT 的 tokenizer 把它切成一堆碎片后数值不稳定。解决先降到 1e-5 试跑如果还在 NaN把清洗脚本里的特殊字符过滤加强。千万不要忽略这条BERT 一旦 NaN前面两个 epoch 的训练成果全部作废。5.2 训练和评估阶段的坑现象验证集 F1 比训练集还高高得离谱比如训练集 0.87、验证集 0.94。原因时间泄漏。同一个新闻事件的多篇改写稿一篇在训练集、一篇在验证集模型其实是记住了事件而不是学到了真假判别。解决回到第 2 章的按时间切分。随机切分看到的高分都是自欺欺人按时间切以后分数会掉 5-10 个点那个数字才接近线上真实水平。现象整体准确率 95%但少数类假新闻的 recall 只有 18%。原因准确率被大类别的 97%正确率撑起来了少数类几乎没被识别出来。解决评估报告只看classification_report里第一行的 precision/recall/f1以及 macro-F1。如果少数类 F1 低于 60%这个模型上线等于摆设。另一个技巧是看模型输出概率的分布假新闻类的平均概率如果低于 0.6说明模型根本没学会把这类样本和真新闻分开。6. 把模型从能跑推到能信坏例复盘与阈值搜索6.1 先看坏例再调阈值每个模型都要过这一关模型训练完第一件事不是看测试集分数而是把预测错误的样本全部捞出来逐条读。这一步能过滤掉一半以上的“虚假高分”。一个阈值搜索脚本就能做这件事# 在验证集上导出错误样本并按模型置信度排序 import pandas as pd val pd.read_csv(val_data.csv) val[pred] preds val[prob] probs[:, 1] # 假新闻类的概率 bad val[val[pred] ! val[label]].sort_values(prob, ascendingFalse) print(bad[[text, label, pred, prob]].head(50)) # 阈值搜索不固定 0.5找 F1 最大的阈值 from sklearn.metrics import f1_score best_th, best_f1 0.5, 0 for th in [i / 100 for i in range(30, 80)]: yp (probs[:, 1] th).astype(int) f1 f1_score(val[label], yp, averagemacro) if f1 best_f1: best_th, best_f1 th, f1 print(best threshold:, best_th, macro-F1:, best_f1)读坏例时有一个经验如果错误样本里大量出现“某明星微博辟谣”这种明显可以靠关键词判别的文本说明模型没用上这些特征回数据层加特征。如果错误样本人类都分不清那就放过模型降低预期。阈值搜索是免费的提升手段但注意要在验证集上调调完再用测试集确认一次否则阈值本身也会过拟合。6.2 再往上走半监督伪标签与多模型集成如果坏例复盘做完还想提点常见做法是用训练好的 BERT 去预测大量无标注新闻挑置信度高于 0.95 的样本作为伪标签补进训练集再微调一轮。这个操作对长尾的“新事件句式”有帮助但也可能把模型自己的偏见放大——只取高置信度就是给模型自己当复读机所以只适合作为最后一搏。另一个是集成把 TF-IDF 逻辑回归、TextCNN 和 BERT 三个模型的结果做投票或概率平均通常能再涨 1-2 个点代价是推理速度变成原来的三倍。我自己现在拿到任何一个虚假新闻检测源码第一件事永远是先跑基线、再存下预测错误的样本逐条读一遍再谈调参。这个习惯帮我挡掉了太多次“分数好看、上线翻车”的尴尬。希望帮到你。本文还有配套的精品资源点击获取