Python虚假新闻检测:BERT向量融合LightGBM与CatBoost的混合模型实战
简介基于Python搭建的多模态虚假新闻检测项目融合文本与图像特征对新闻真实性进行自动识别面向计算机、人工智能、通信工程、自动化等专业的高校学生和开发者。资源可在毕设答辩、课程设计、项目初期演示中直接使用也适合作为NLP与多模态学习的练习素材。压缩包内共39个文件包含16个Python源码文件、4份Markdown说明文档、4个TXT文本文件、3个Shell脚本、3个TSV数据文件、2个JSON配置、2个tfevents训练日志、1个Jupyter Notebook、1个checkpoint模型检查点及许可证文件整体约350KB目录涵盖模型、代码、文档等模块便于按需查看。已有296人学习下载。源码经测试运行成功包含基于BERT的多模态识别、LightGBM与CatBoost融合、预测脚本及环境依赖说明下载后阅读README可快速了解结构并运行适合在此基础上扩展新闻检测功能。代码结构清晰关键步骤有注释方便定位与修改。1. 用 Python 做虚假新闻多模态检测BERT 向量 树模型为什么比单模型更稳用 Python 做虚假新闻检测最容易翻车的地方其实不是模型选型而是把 BERT 当成一个黑匣子数据丢进去准确率出不来也不知道该调哪。这个资源走的是「多模态识别」路线但这里的多模态不是图文拼接而是把文本语义当一个模态、把统计和元信息特征当另一个模态最后用 BERT 向量 LightGBM/CatBoost 混合模型去融合判断。工程里包含 bert-final.py、lgb_cat_blend_lb9546.py、predict_test.py 和一套 TensorFlow 格式的 BERT 预训练权重训练、推理、依赖安装脚本都齐。适合计科、人工智能方向拿来做毕设或课程设计也适合想复现一整套文本检测管线的入门者照着跑。2. 项目骨架与数据流把源码包拆成配置、训练、推理三条线解压 Multi-domain-fake-news-detection-master.zip 之后不要急着跑训练先花十分钟把文件按职责分清楚。这个工程的调用关系其实很清晰先装依赖再训练 BERT 抽取语义向量再训练树模型做融合分类最后用 predict_test.py 出结果。顺序错了后面每一步都会报让你摸不着头脑的错。2.1 文件清单与职责划分从 req.sh 到 predict_test.py每份文件的角色拿到源码包后我习惯先列一遍文件把「配置类、训练类、推理类」分到三条线上。下面这个表是我对照本项目目录整理的角色划分你解压后可以对着勾一遍文件/目录类型在管线里的角色req.sh/packages.txt环境配置安装 Python 依赖声明了项目所需的包列表bert-final.py训练脚本BERT 微调主脚本负责加载预训练权重、微调并抽取语义向量lgb_cat_blend_lb9546.py训练脚本读取 BERT 向量与统计特征训练 LightGBM 和 CatBoost 并混合predict_test.py推理脚本对测试集做预测输出概率或类别结果train.sh/test.sh一键脚本把训练和测试命令串起来按顺序执行model/tf_bert_model模型目录放 TensorFlow 格式的 BERT 预训练权重code子模块目录一般放数据加载、特征构造、工具函数等catboost_info运行产物CatBoost 训练日志和临时文件目录README.md说明文档作者写的运行说明、环境要求和参数备注作者的 README 往往会把踩过的坑写在里面下载后先打开 README.md比在网上搜别人转述的第二手经验可靠得多。我习惯先看 req.sh 里锁了哪些库再去结合 README 确认 Python 版本因为 BERT 这类老工程的网络结构改动大依赖版本差一个小版本都可能起不来。接下来在项目根目录执行一次目录确认把路径和环境看清楚cd Multi-domain-fake-news-detection-master tree -L 2 python --version这段命令的作用是确认你当前所在目录就是项目根目录同时看出一级目录结构是否完整。tree 不存在时可以用find . -maxdepth 2 -type d代替python --version用于确认解释器版本BERT 相关代码用 Python 3.6 或 3.7 最常见如果你本机默认是 3.11后面依赖安装很容易出现不兼容。2.2 训练与推理的调用链train.sh 和 test.sh 到底谁先执行整个工程不是单文件孤军奋战主流程走的是「req.sh → bert-final.py → lgb_cat_blend_lb9546.py → predict_test.py」这条链。这里我按这类项目最常见的脚本结构还原一下 train.sh你解压后看到的行数可能更多但主线就是这三步#!/bin/bash # 1. 先按 packages.txt 安装依赖 bash req.sh # 2. 用 BERT 微调文本编码器并保存抽取好的向量文件 python bert-final.py \ --data_dir data/fake_news.csv \ --bert_dir model/tf_bert_model \ --max_seq_length 128 \ --batch_size 16 \ --num_epochs 3 \ --output_dir output/bert_ckpt # 3. 训练 LightGBM CatBoost 混合模型 python lgb_cat_blend_lb9546.py --mode train这里每一条命令都有明确目的第 1 条是装依赖第 2 条做语义编码第 3 条做分类器融合。注意第 2 条的输出目录output/bert_ckpt它和第 3 条脚本读取的特征向量目录是同一处这个衔接关系断掉混合模型就会拿到空文件或者直接报错。test.sh 相对简单一般就是把推理脚本指向测试集和模型产物#!/bin/bash python predict_test.py \ --test_file data/test.csv \ --model_dir output \ --output result.csv它的输入有两个关键来源--test_file指向待检测的新闻数据--model_dir指向第 2 步保存的 BERT 和第 3 步保存的树模型。推理脚本会按特征列名对齐数据列名顺序错一位lightgbm 就直接抛特征数不匹配。2.3 model/tf_bert_model 目录TensorFlow 预训练权重的组织方式这个项目用的是 TensorFlow 格式的 BERT不是 PyTorch 的 bin 文件。目录里通常包含三类文件bert_config.json定义模型结构vocab.txt是词表bert_model.ckpt.data-*、bert_model.ckpt.index、bert_model.ckpt.meta是预训练权重。拿到权重后先确认文件完整性ls -lh model/tf_bert_model正常情况下列表里能同时看到bert_config.json、vocab.txt、bert_model.ckpt.index和bert_model.ckpt.data-00000-of-00001。如果只有配置文件没有权重文件说明权重下载不完整加载时会在 restore 阶段报 checkpoint 找不到。这套权重在模型加载阶段的作用是提供初始参数微调之后覆盖的只是分类层和高层语义底层的通用语言知识是从这份权重继承来的。文件名里的tf_bert_model直接说明了它是 TensorFlow 1.x checkpoint 格式这决定了项目里bert-final.py的加载方式用的是tf.train.Checkpoint或tf.train.Saver那一套。后面复现时选择虚拟环境版本也要跟着这个格式走。3. 微调 BERT 文本语义编码器bert-final.py 的加载、训练与特征抽取BERT 在这个工程里的角色不是最终分类器而是把文本变成 768 维语义向量的编码器。所以我拿到 bert-final.py 后最关心三件事预训练权重怎么加载、微调参数怎么设置、向量怎么抽取和保存。这三件事做完后面的树模型才有像样的输入。3.1 加载预训练权重BertConfig、vocab.txt 和 ckpt 的配合方式加载 TF 版 BERT 的标配步骤是用BertConfig.from_json_file读结构配置用FullTokenizer读词表做分词再用 checkpoint 恢复权重。按项目的目录结构加载部分的核心逻辑可以整理成下面这样跑之前对照一下bert-final.py里的函数名避免接口版本不一致# load_bert_encoder.py import tensorflow as tf from bert.tokenization import FullTokenizer from bert.modeling import BertConfig, BertModel BERT_DIR model/tf_bert_model bert_config BertConfig.from_json_file(BERT_DIR /bert_config.json) tokenizer FullTokenizer(vocab_fileBERT_DIR /vocab.txt) init_checkpoint BERT_DIR /bert_model.ckpt这里三个输入的职责不同bert_config.json告诉模型有多少层、多少头、隐层维度是多少vocab.txt是分词用的词表中英文混合文本的 tokenizer 全靠它切出词片bert_model.ckpt是初始权重微调时tf.train.Checkpoint.restore会把预训练参数恢复进来。三者缺任何一个模型都起不来。3.2 训练参数实测max_seq_length、batch_size、learning_rate 的推荐配置BERT 微调没有太多玄学参数组合基本固定但每个参数改错方向损失值都会给你颜色看。这个项目在本地复现时我建议先按下面这组参数起步参数推荐值说明max_seq_length128新闻标题和短文本 128 足够长正文案例可以上调到 256batch_size16显存 6G 以下降到 8 或 4learning_rate2e-5BERT 微调标准量级1e-5 到 5e-5 之间调num_epochs3数据集小的情况下 2 ~ 4 轮收敛warmup_proportion0.1前 10% 步数线性预热防止前期震荡这些参数为什么这样配BERT 微调时学习率如果给到普通网络的 1e-3第一轮就会 loss 爆炸max_seq_length也不是越大越好所有 token 一次性进显存长度翻倍显存占用接近翻倍文本本身只有几十个字时设 512 纯属浪费。首次复现先用小参数把流程跑通再改大值提精度这是正确的调参顺序。3.3 把 BERT 输出转成向量CLS 池化与文本向量化函数BERT 的骨干网络跑完每个 token 位置都有一组隐层向量但树模型需要的是一条文本一个向量而不是二维矩阵。常见做法有两种取[CLS]位的输出或者对所有 token 向量做平均池化。工程里普遍用 CLS 池化处理起来更直接# text_to_vec.py def bert_encode_and_pool(model, tokenizer, texts, max_seq_length128): input_ids_all, mask_all, segment_ids_all [], [], [] for text in texts: tokens tokenizer.tokenize(text)[: max_seq_length - 2] tokens [[CLS]] tokens [[SEP]] input_ids tokenizer.convert_tokens_to_ids(tokens) padding_len max_seq_length - len(input_ids) input_ids [0] * padding_len mask [1] * len(tokens) [0] * padding_len segment_ids [0] * max_seq_length input_ids_all.append(input_ids) mask_all.append(mask) segment_ids_all.append(segment_ids) input_ids_t tf.constant(input_ids_all, dtypetf.int32) mask_t tf.constant(mask_all, dtypetf.int32) segment_t tf.constant(segment_ids_all, dtypetf.int32) pooled model.get_pooled_output() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) return sess.run(pooled, feed_dict{ input_ids:0: input_ids_t, input_mask:0: mask_t, token_type_ids:0: segment_t, })这段代码的关键点有三个一是[CLS]和[SEP]占掉两个位置所以原始 token 截断长度是max_seq_length - 2二是mask里真实 token 记 1、padding 记 0让模型忽略补零位三是get_pooled_output()拿到的就是 CLS 位的池化向量形状是(batch_size, 768)。如果发现向量维度不是 768先检查bert_config.json里的hidden_size。3.4 训练日志与中断恢复看什么指标、存哪些 checkpoint训练时损失值只是参考更重要的是确认 loss 在 epoch 交界处有没有下降趋势。BERT 微调数据集往往只有几万条三个 epoch 的损失曲线如果从一开始就乱跳优先怀疑学习率过大或数据没随机洗牌。启动训练时直接用命令行参数跑python bert-final.py \ --data_dir data/fake_news.csv \ --bert_dir model/tf_bert_model \ --max_seq_length 128 \ --batch_size 16 \ --learning_rate 2e-5 \ --num_epochs 3 \ --output_dir output/bert_ckpt训练中断很常见显存不够、断电、手动 CtrlC 都可能让过程停在中间。所以跑之前确保output/bert_ckpt目录是新建的里面不要残留上一次的旧 checkpoint否则恢复训练时会把旧权重当成初始化权重白跑一轮。我的习惯是每 500 步打印一次 loss同时把模型按 step 号存成ckpt-500、ckpt-1000这种命名断点续跑时用--init_checkpoint指到最近一个 checkpoint。4. 特征融合与 LGB CatBoost 混合lgb_cat_blend_lb9546.py 的集成细节模型名lgb_cat_blend_lb9546.py里的 lb 我理解是 leaderboard 的缩写blend 表示混合这种命名在有竞赛经验的工程里很常见。它要解决的问题是BERT 向量擅长语义但虚假新闻里大量线索藏在标题长度、感叹号数量、URL 出现与否这些统计特征里树模型恰好擅长这些。两者融合准确率通常比单一 BERT 微调分类高出两个点以上。4.1 为什么混合树模型BERT 向量自带语义但弱在统计特征单纯用 BERT 最后一层向量接 softmax 做二分类能抓住「这段话像不像谣言」但抓不住「标题全是感叹号」这种强信号。虚假新闻数据集里标题带多个感叹号、正文带陌生 URL、全是数字百分比这些特征在统计层面有区分度BERT 却把它们稀释在语义编码里了。树模型对小数据集和稀疏特征非常友好而且 LGB 和 CatBoost 两个模型虽然都是梯度提升树内部处理类别特征和缺失值的方式不同预测偏差也不一样。把它们对同一批样本的输出做加权平均能抵消一部分各自的过拟合。这也是这个工程把 BERT 语义向量维度降到 768 之后还要拼接统计特征再进树模型的原因。4.2 特征工程768 维 BERT 向量之外还能拼哪些手写特征特征构造是这套多模态方案里投入产出比最高的一步。我按典型虚假新闻样本的特性整理了几个百搭的统计特征# features.py import re def build_stat_features(title: str) - dict: if not isinstance(title, str): title features {} features[title_len] len(title) features[num_exclamation] title.count(!) features[num_question] title.count(?) features[has_url] 1 if re.search(rhttps?://, title) else 0 features[digit_ratio] sum(c.isdigit() for c in title) / (len(title) 1) features[word_count] len(title.split()) return features每个特征的设计理由都对应一类假新闻套路num_exclamation抓标题党情绪化的感叹号轰炸has_url抓诱导跳转链接digit_ratio抓那种「99% 的人不知道」的伪科学数字化表达title_len是对过短标题和超长标题做区分。这些特征拼接到 BERT 向量的尾部组成最终输入维度从 768 变成768 len(stat_features)。4.3 五折交叉验证与早停blend 逻辑与权重选择混合模型的重点不是分别训练两个模型而是让两个模型在同一个验证集上打分再按权重融合。标准做法是五折交叉验证每一折分别训两个模型各自对验证折出概率。核心逻辑整理如下# blend_core.py import numpy as np from sklearn.model_selection import StratifiedKFold import lightgbm as lgb from catboost import CatBoostClassifier def oof_blend(X, y, lgb_weight0.6, seed42): skf StratifiedKFold(n_splits5, shuffleTrue, random_stateseed) oof_lgb np.zeros(len(y)) oof_cat np.zeros(len(y)) for train_idx, valid_idx in skf.split(X, y): X_tr, X_va X.iloc[train_idx], X.iloc[valid_idx] y_tr, y_va y.iloc[train_idx], y.iloc[valid_idx] lgb_model lgb.LGBMClassifier( n_estimators800, learning_rate0.05, num_leaves31 ) lgb_model.fit( X_tr, y_tr, eval_set(X_va, y_va), early_stopping_rounds50, verbose0 ) cat_model CatBoostClassifier( iterations800, learning_rate0.05, depth7, eval_metricAUC, verbose0 ) cat_model.fit( X_tr, y_tr, eval_set(X_va, y_va), early_stopping_rounds50 ) oof_lgb[valid_idx] lgb_model.predict_proba(X_va)[:, 1] oof_cat[valid_idx] cat_model.predict_proba(X_va)[:, 1] blend lgb_weight * oof_lgb (1 - lgb_weight) * oof_cat return blend, oof_lgb, oof_cat这段代码里有三个参数值得单独说。lgb_weight0.6表示 LGB 结果占 60%、CatBoost 占 40%这个比例不是拍脑袋定的而是看两者在验证集上的 AUC 谁高谁低高的那个给更高权重。early_stopping_rounds50的作用是验证集指标连续 50 轮不提升就停止迭代防止训练到后期过拟合。StratifiedKFold保证每折训练集和验证集里真实新闻、虚假新闻比例跟全量数据一致类别不平衡时这个分层极其重要。4.4 模型产物与推理输入把 lgb 和 catboost 模型 save_model 出来两个模型训练完成后需要把模型权重和特征列名都存到磁盘供 predict_test.py 加载。只存模型不存特征列顺序是后面最容易翻车的地方# save_models.py import lightgbm as lgb # lgb_model 是上面训练好的 LGBMClassifier lgb_model.booster_.save_model(output/lgb_model.txt) # cat_model 是 CatBoostClassifier cat_model.save_model(output/cat_model.bin) # 把特征列名的顺序固定下来推理时按这个顺序拼数据 with open(output/feature_cols.txt, w, encodingutf-8) as f: f.write(\n.join(X.columns))feature_cols.txt是推理阶段的对账工具。训练时 X 的列顺序是 「768 维 BERT 向量 统计特征」推理时必须用完全一样的顺序重新拼接。单独把列名存成文件就是为了避免每次靠记忆排序一记错推理阶段就会报特征数不匹配。以后每训练一版模型这三个文件要同时更新只更新其中一个线上推理用的还是旧特征顺序预测结果会失真。5. 复现避坑版本、路径、显存与 catboost_info 的五个翻车现场这套工程整体不复杂但复现时最容易出问题的往往不是算法而是环境。下面的问题我基本都亲手踩过一遍每条按「现象 → 原因 → 解决」写清楚你可以直接对照排查。5.1 TensorFlow 版本过高AttributeError 和 contrib 找不到现象运行bert-final.py还没开始训练直接报AttributeError: module tensorflow has no attribute gfile或者ModuleNotFoundError: No module named tensorflow.contrib。原因源码基于 TensorFlow 1.x 编写调用的是tf.gfile.GFile、tf.contrib这些 1.x 旧接口。TensorFlow 2.x 把tf.contrib整体移除tf.gfile挪到了tf.io.gfile老代码自然起不来。解决按照req.sh或packages.txt里的说明安装 TF 1.15 系列优先考虑 GPU 版pip install tensorflow-gpu1.15.0如果本机没有 GPU就装 CPU 版tensorflow1.15.0BERT 微调慢一些但流程能走通。我建议建独立虚拟环境再装不要动系统里的 Python否则影响其他项目。5.2 项目放在中文路径vocab.txt 加载失败与乱码现象训练脚本能启动但加载vocab.txt时报NotFoundError路径打印出来是一串看不懂的转义字符。原因tf.gfile在 Windows 下对中文路径和特殊字符支持不稳定路径里的中文被错误编码文件定位失败。解决把整个项目目录复制到纯英文路径下比如C:\projects\fake_news_detection确保从根目录到项目目录中间没有中文和空格。用 PyCharm 的话右键项目根目录执行 Mark Directory as Sources Root让相对路径model/tf_bert_model能稳定解析。5.3 显存溢出CUDA_OUT_OF_MEMORY 不一定只能换显卡现象训练跑到几百步报ResourceExhaustedError: OOM when allocating tensor程序直接中断。原因BERT 是显存大户max_seq_length设为 256、batch_size设为 32 时一张 6G 显存的卡根本扛不住。12 层 Transformer 同时驻留显存前向传播和反向传播都要保存中间激活值。解决优先降batch_size到 8 或 4配合把max_seq_length降到 128。每条新闻标题通常不超过几十个字128 的长度限制足够。显存还是不够时再限制当前进程只看见一张卡CUDA_VISIBLE_DEVICES0 python bert-final.py \ --max_seq_length 128 --batch_size 4CUDA_VISIBLE_DEVICES0的作用是让程序只使用索引为 0 的显卡避免 TensorFlow 把显存均匀吃满所有卡造成其他显存被白白占用。5.4 catboost_info 目录写不进训练中断在第 0 步现象跑lgb_cat_blend_lb9546.pyCatBoost 刚开始训练就退出报无法写入catboost_info或learn目录相关错误。原因项目压缩包里自带了一个catboost_info目录解压后如果被设成只读或者当前工作目录权限受限CatBoost 默认的日志目录写不进去模型直接拒绝训练。解决在脚本开头显式创建目录或者指定一个可写目录。常见做法是在训练前加上import os os.makedirs(catboost_info, exist_okTrue)exist_okTrue表示目录已存在时不报错不存在时自动创建。这样就把目录写权限问题挡在训练启动之前后面 CatBoost 才能正常输出日志和中间模型。5.5 特征维度对不上lightgbm 报 feature mismatch现象predict_test.py推理时lightgbm 报错提示数据特征数是 812但模型期望 813 个特征。原因训练阶段特征列是「768 维 BERT 向量 统计特征」共 813 列推理时 BERT 向量正常但统计特征少拼了一列。最常见的是某条测试数据标题为空特征构造函数返回的字典缺了title_len或word_count导致拼接后矩阵少一列。解决推理前用训练阶段保存的feature_cols.txt强制对齐列顺序和列数量import pandas as pd feature_cols open(output/feature_cols.txt, encodingutf-8).read().splitlines() X_test X_test.reindex(columnsfeature_cols, fill_value0)reindex会把缺失的列补 0多出来的列自动丢弃保证进入树模型的特征数量永远和训练时一致。我的习惯是把这行代码贴在推理脚本的数据准备部分每次训练完模型先跑一遍 test.sh 验证维度再交付到线上。6. 把 predict_test.py 改造成单个新闻标题的实时检测入口predict_test.py 默认是批处理模式读一个 CSV 输出一个 CSV。但实际使用时更多场景是拿到一条新闻标题马上判断真假。把它改造成单条文本的检测函数是最实用的二次开发。6.1 predict_test.py 原本的输入输出格式原脚本做的事是读取测试集 → 拼接 BERT 向量和统计特征 → 加载output目录里的树模型 → 输出预测概率 CSV。改造方向是把「读整个测试集」换成「接收一条字符串」内部流程保持不变。6.2 封装 detect() 函数从文本清洗到概率输出改造后的入口函数可以这样组织# detect_news.py import pandas as pd import lightgbm as lgb from load_bert_encoder import load_bert_encoder, text_to_vec from features import build_stat_features bert_encoder load_bert_encoder(model/tf_bert_model) lgb_model lgb.Booster(model_fileoutput/lgb_model.txt) feature_cols open(output/feature_cols.txt, encodingutf-8).read().splitlines() def detect(news_text: str): clean_text news_text.strip().replace(\n, ) bert_vec_df text_to_vec([clean_text], bert_encoder, max_seq_length128) stat_feats pd.DataFrame([build_stat_features(clean_text)]) X pd.concat([bert_vec_df, stat_feats], axis1)[feature_cols] prob lgb_model.predict(X)[0] return (虚假新闻 if prob 0.5 else 真实新闻), float(prob)这个函数里去掉了 CSV 读取把手工特征构造和 BERT 向量化封装成两个可复用函数输出是(类别, 概率)元组。改造完毕后再包装成 FastAPI 或 Flask 接口就能对外服务。Booster直接加载output/lgb_model.txt比重新训练快得多适合部署时用。6.3 部署前的检查清单上线前至少验证三件事拿一条真实新闻和一条典型假新闻各跑一次确认输出类别符合直觉检查概率值是否落在 0 到 1 之间超出就说明模型输出没做 sigmoid再确认feature_cols.txt是最新一版不要和旧模型交叉使用。我自己就吃过这个亏训练完新模型忘记同步特征列文件线上推理用旧列顺序拼数据静默地出了好几天错结果才被发现。从那以后我每次训练完都强制走一遍「保存模型 → 保存特征列 → 跑单条检测函数 → 核对输出」再交付给别人用。希望这条流程能帮到你也祝你复现顺利。本文还有配套的精品资源点击获取