简介:本资源是一套面向中文社交媒体虚假新闻识别任务的完整项目源码,适合自然语言处理初学者、机器学习课程设计者及毕业设计开发者参考。项目以微信文章标题为输入,区分真实与虚假新闻,覆盖传统机器学习、深度学习与BERT预训练模型三条技术路线,并给出准确率、召回率、F1与AUC等评估指标。压缩包共92个文件,约173KB,以45个Python脚本为核心,辅以zbak备份、xml配置、iml工程文件及ipynb笔记,涵盖jieba分词、词袋与TF-IDF特征工程、随机森林、支持向量机、朴素贝叶斯、逻辑回归以及BERT微调等模块。其中逻辑回归结合词袋模型取得90.48%的准确率,支持向量机配合TF-IDF在精确率上达到89%。已有66人学习,读者可据此复现完整实验流程,理解文本清洗、特征提取与模型集成的优化思路,并借助网盘数据文件快速搭建Anaconda与PyTorch实验环境。
1. 虚假新闻检测系统:从标题到可运行方案的拆解
虚假新闻检测系统,说白了就是让机器帮你判断一条新闻是真是假。这件事在舆情监控、内容审核、事实核查场景里需求非常硬。我做过几个类似项目,从最朴素的 TF-IDF 加逻辑回归,到后来上 BERT 微调,中间踩的坑比想象中多得多。这个标题覆盖了三个层次:机器学习做基线、深度学习提效果、BERT 模型冲上限。适合谁看?有 Python 基础、想做一个完整 NLP 落地项目的同学,或者已经跑过 sklearn 但不知道怎么把 BERT 接进业务流程的工程师。整条链路我会按「数据怎么来、特征怎么建、模型怎么选、参数怎么调、上线怎么排错」讲清楚,每一步都给可复现的命令和代码。你照着走,能拿到一个可用的检测系统,而不是一个只跑在 notebook 里的 demo。
2. 数据准备与基线机器学习模型:先把能跑通的版本做出来
2.1 虚假新闻数据集从哪来、怎么清洗
做检测系统,数据是第一个卡点。公开数据集常见的有 Kaggle 上的 Fake and Real News Dataset、LIAR 数据集,中文场景可以用微博辟谣数据或自建爬取。我一般会先确认三件事:标签是否可靠、正负样本是否均衡、文本长度分布如何。很多数据集标签噪声很大,比如把「讽刺性真实新闻」标成假新闻,这种脏数据不处理,后面模型再强也白搭。
清洗流程我固定用这几步:去 HTML 标签、去 URL、去特殊符号、统一编码、去重。下面是一个可复用的清洗脚本:
import re import pandas as pd def clean_text(text): if not isinstance(text, str): return "" # 去 HTML 标签 text = re.sub(r'<[^>]+>', '', text) # 去 URL text = re.sub(r'http[s]?://\S+', '', text) # 去特殊符号,保留中英文和基本标点 text = re.sub(r'[^\w\s\u4e00-\u9fff.,!?;:]', '', text) # 压缩连续空白 text = re.sub(r'\s+', ' ', text).strip() return text df = pd.read_csv('news.csv') df['content'] = df['content'].apply(clean_text) df = df.drop_duplicates(subset=['content']) df = df[df['content'].str.len() > 20] # 过滤过短文本 print(df['label'].value_counts())逻辑说明:clean_text函数按顺序处理 HTML、URL、特殊符号和空白,顺序不能乱,先去标签再去 URL 是因为有些 URL 藏在标签属性里。drop_duplicates去重很关键,虚假新闻数据集里重复文本特别多,不去重会导致训练集和测试集泄漏。len > 20这个阈值是经验值,太短的文本没有足够信息,留着只会引入噪声。
参数说明:subset=['content']指定按内容列去重;value_counts()用来检查标签均衡性,如果正负比超过 3:1,后面训练时要加class_weight='balanced'。
2.2 用 TF-IDF 加逻辑回归跑通第一个基线
别一上来就上 BERT。先用机器学习跑一个基线,目的是确认数据管线没问题、评估指标有参考值。TF-IDF 加逻辑回归是我最常用的基线组合,训练快、可解释、调参少。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( df['content'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) vectorizer = TfidfVectorizer(max_features=50000, ngram_range=(1, 2), min_df=3) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) clf = LogisticRegression(C=1.0, max_iter=1000, class_weight='balanced') clf.fit(X_train_vec, y_train) y_pred = clf.predict(X_test_vec) print(classification_report(y_test, y_pred))逻辑说明:TfidfVectorizer把文本转成稀疏向量,ngram_range=(1,2)同时考虑单词和双词组合,对虚假新闻检测有帮助,因为「据可靠消息」「内部人士透露」这类短语是强信号。stratify保证训练测试集标签比例一致。逻辑回归的class_weight='balanced'自动处理样本不均衡。
参数说明:max_features=50000控制词表大小,太大容易过拟合;min_df=3过滤低频词;C=1.0是正则化强度的倒数,值越小正则越强,基线先用默认值。
跑完这个基线,你大概能拿到 0.85 到 0.92 的 F1,具体看数据集质量。如果 F1 低于 0.8,先回去查数据,别急着换模型。
2.3 机器学习基线的三个必调参数与评估陷阱
基线跑通后,有三个参数值得花时间调:max_features、ngram_range、C。我一般用网格搜索快速扫一遍:
from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline pipe = Pipeline([ ('tfidf', TfidfVectorizer()), ('clf', LogisticRegression(max_iter=1000, class_weight='balanced')) ]) params = { 'tfidf__max_features': [30000, 50000, 80000], 'tfidf__ngram_range': [(1,1), (1,2)], 'clf__C': [0.1, 1.0, 10.0] } grid = GridSearchCV(pipe, params, cv=3, scoring='f1', n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_)逻辑说明:用 Pipeline 把向量化和分类器串起来,避免在交叉验证时数据泄漏。scoring='f1'而不是 accuracy,因为虚假新闻检测里漏判和误判代价不同,F1 更均衡。
评估陷阱要特别注意:第一,别用 accuracy 当唯一指标,样本不均衡时 accuracy 会骗人;第二,测试集一定要和训练集同分布,我见过有人用不同来源的数据做测试,F1 直接掉 20 个点;第三,保存好 vectorizer,线上推理时必须用同一个,否则特征对不上。
3. 深度学习模型:用 CNN 和 LSTM 把文本特征吃得更透
3.1 为什么机器学习基线不够用
TF-IDF 加逻辑回归的瓶颈很明显:它只看词频,不理解语序和上下文。「狗咬人」和「人咬狗」在 TF-IDF 里几乎一样,但语义完全相反。虚假新闻里大量存在这种语序敏感的表达,比如「专家否认了谣言」和「专家否认了,谣言」,一个标点位置就改变意思。深度学习模型通过词嵌入和序列建模,能捕捉这种上下文关系。
另一个问题是特征工程成本。机器学习方案需要人工设计特征,比如情感得分、标点密度、大写字母比例。深度学习端到端学习,省去大量手工特征。但代价是训练数据需求更大、调参更玄学、解释性更差。我的建议是:数据量低于 1 万条,先用机器学习;超过 1 万条,再考虑深度学习。
3.2 用 Keras 搭一个 CNN 文本分类器
CNN 做文本分类的思路是用卷积核在词向量序列上滑动,提取局部 n-gram 特征。相比 LSTM,CNN 训练更快、并行度更好,适合作为深度学习的第一个模型。
import numpy as np from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Dense, Dropout MAX_WORDS = 50000 MAX_LEN = 300 EMBED_DIM = 128 tokenizer = Tokenizer(num_words=MAX_WORDS, oov_token='<OOV>') tokenizer.fit_on_texts(X_train) X_train_seq = pad_sequences(tokenizer.texts_to_sequences(X_train), maxlen=MAX_LEN, padding='post') X_test_seq = pad_sequences(tokenizer.texts_to_sequences(X_test), maxlen=MAX_LEN, padding='post') model = Sequential([ Embedding(MAX_WORDS, EMBED_DIM, input_length=MAX_LEN), Conv1D(128, 5, activation='relu'), GlobalMaxPooling1D(), Dropout(0.5), Dense(64, activation='relu'), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) model.summary() history = model.fit(X_train_seq, y_train, epochs=5, batch_size=64, validation_split=0.1, class_weight={0:1, 1:2})逻辑说明:Tokenizer把词映射成整数 ID,pad_sequences统一长度,padding='post'在末尾补零。Conv1D(128, 5)表示 128 个卷积核、窗口大小 5,能捕捉 5 个词以内的局部模式。GlobalMaxPooling1D取每个特征图的最大值,把变长序列压成固定向量。Dropout(0.5)防过拟合。class_weight={0:1, 1:2}给假新闻类别更高权重,因为漏判假新闻代价更大。
参数说明:MAX_LEN=300覆盖大部分新闻长度,太短截断信息,太长增加计算量;EMBED_DIM=128是常用起点,数据量大可以加到 256;batch_size=64在显存和训练稳定性之间取平衡。
3.3 LSTM 与 CNN 的选型对比和调参要点
LSTM 适合捕捉长距离依赖,比如新闻开头和结尾的矛盾表述。但 LSTM 训练慢、容易梯度消失。我的经验是:文本长度超过 500 词、需要建模长距离关系时用 LSTM;短文本分类 CNN 足够。
from tensorflow.keras.layers import LSTM, Bidirectional model_lstm = Sequential([ Embedding(MAX_WORDS, EMBED_DIM, input_length=MAX_LEN), Bidirectional(LSTM(64, return_sequences=False)), Dropout(0.5), Dense(64, activation='relu'), Dense(1, activation='sigmoid') ]) model_lstm.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])逻辑说明:Bidirectional同时从前向和后向读序列,能捕捉双向上下文。return_sequences=False只取最后一个时间步的输出。
调参要点:LSTM 层数不要超过 2 层,否则训练极慢;units从 64 开始试;学习率用1e-3,如果 loss 震荡就降到1e-4。CNN 和 LSTM 可以堆叠,先 CNN 提局部特征再 LSTM 提序列特征,但调参复杂度翻倍,新手先单独用。
4. BERT 模型:把检测效果推到上限
4.1 BERT 微调为什么比从头训练更靠谱
BERT 的核心价值是预训练。它在海量语料上已经学到了语法、语义和常识,你只需要用几千条标注数据微调,就能超过从头训练的 CNN/LSTM。虚假新闻检测里很多信号是隐式的,比如「某研究显示」但没给出处、「据说」但没信源,BERT 的注意力机制能捕捉这些模式。
但 BERT 不是银弹。第一,它需要 GPU,显存至少 8G 才能跑 base 版本;第二,推理速度慢,单条 50ms 左右,高并发场景要加缓存或蒸馏;第三,中文场景要用 bert-base-chinese 或 RoBERTa-wwm-ext,别用英文模型硬套。
4.2 用 HuggingFace 微调 BERT 的完整代码
import torch from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizer, BertForSequenceClassification, AdamW from transformers import get_linear_schedule_with_warmup class NewsDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=256): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding = self.tokenizer( self.texts[idx], truncation=True, padding='max_length', max_length=self.max_len, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'].squeeze(), 'attention_mask': encoding['attention_mask'].squeeze(), 'labels': torch.tensor(self.labels[idx], dtype=torch.long) } tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2) model.cuda() train_dataset = NewsDataset(X_train.tolist(), y_train.tolist(), tokenizer) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01) scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=100, num_training_steps=len(train_loader)*3) model.train() for epoch in range(3): for batch in train_loader: optimizer.zero_grad() outputs = model( input_ids=batch['input_ids'].cuda(), attention_mask=batch['attention_mask'].cuda(), labels=batch['labels'].cuda() ) loss = outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() print(f'epoch {epoch} loss {loss.item():.4f}')逻辑说明:NewsDataset封装了 tokenization,truncation=True截断超长文本,padding='max_length'统一长度。BertForSequenceClassification在 BERT 顶部加了一个分类头。AdamW是 BERT 微调的标准优化器,lr=2e-5是关键参数,太大导致灾难性遗忘,太小收敛慢。clip_grad_norm_防止梯度爆炸。warmup让学习率从 0 线性上升到设定值,稳定训练初期。
参数说明:max_len=256平衡信息量和显存;batch_size=16是 8G 显存的极限,显存不够降到 8;epoch=3通常够用,超过 5 容易过拟合;weight_decay=0.01是 BERT 微调的推荐值。
4.3 BERT 推理加速与模型蒸馏的落地选择
BERT 推理慢是上线时的最大障碍。我一般按这个顺序优化:第一,用torch.no_grad()关闭梯度,速度提升 30%;第二,用 ONNX Runtime 或 TensorRT 导出,再提升 2 到 3 倍;第三,如果还不够,用 DistilBERT 或 TinyBERT 蒸馏,精度掉 1 到 2 个点,速度提升 3 到 5 倍。
model.eval() with torch.no_grad(): outputs = model(input_ids=batch['input_ids'].cuda(), attention_mask=batch['attention_mask'].cuda()) preds = torch.argmax(outputs.logits, dim=1)逻辑说明:model.eval()切换 dropout 和 batchnorm 到推理模式,torch.no_grad()不建计算图,省显存和计算。
选择建议:QPS 低于 10,直接用 BERT;QPS 在 10 到 100,用 ONNX 加速;QPS 超过 100,上蒸馏模型或加 Redis 缓存高频文本。
5. 避坑与排查:虚假新闻检测系统最常见的五个翻车点
5.1 数据泄漏导致 F1 虚高
现象:离线 F1 跑到 0.98,上线后掉到 0.7。原因:训练集和测试集有重复文本,或者同一事件的新闻被分到两边。解决:按事件 ID 或时间划分数据集,去重时用drop_duplicates加模糊匹配,比如 SimHash 去重。
5.2 标签噪声让模型学偏
现象:模型把「据新华社报道」判成假新闻。原因:训练集里假新闻大量引用权威媒体,模型学到了错误关联。解决:人工抽检 500 条标签,清洗明显错误的样本;用置信学习(confident learning)自动找噪声标签。
5.3 中文分词和 tokenizer 不匹配
现象:BERT 微调后效果不如 TF-IDF。原因:用了英文 tokenizer 处理中文,或者 jieba 分词后输入 BERT。解决:中文场景统一用bert-base-chinese的 tokenizer,不要自己分词后再喂给 BERT。
5.4 推理时预处理不一致
现象:训练时 F1 0.95,线上单条测试结果乱跳。原因:训练用padding='max_length',线上用padding=True,导致 attention mask 不一致。解决:把预处理逻辑封装成函数,训练和推理共用同一份代码。
5.5 类别不均衡导致漏判
现象:假新闻召回率只有 0.6。原因:正负样本 1:5,模型偏向预测多数类。解决:训练时加class_weight,或者用 focal loss 替代交叉熵,让模型关注难样本。
6. 把 BERT 接进业务流程:一个可上线的推理服务技巧
模型训完只是开始,真正落地要解决「怎么让业务系统调用」。我一般用 FastAPI 包一层 HTTP 服务,配合 Redis 缓存高频文本。下面是一个最小可用的推理服务:
from fastapi import FastAPI from pydantic import BaseModel import torch from transformers import BertTokenizer, BertForSequenceClassification app = FastAPI() tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') model = BertForSequenceClassification.from_pretrained('./finetuned_bert') model.eval() model.cuda() class NewsRequest(BaseModel): text: str @app.post('/predict') def predict(req: NewsRequest): inputs = tokenizer(req.text, truncation=True, padding='max_length', max_length=256, return_tensors='pt') with torch.no_grad(): outputs = model(input_ids=inputs['input_ids'].cuda(), attention_mask=inputs['attention_mask'].cuda()) prob = torch.softmax(outputs.logits, dim=1)[0][1].item() return {'label': 'fake' if prob > 0.5 else 'real', 'confidence': round(prob, 4)}逻辑说明:NewsRequest定义请求体,predict函数做 tokenization 和推理,返回标签和置信度。prob > 0.5是默认阈值,业务上可以调,比如舆情场景宁可误判也要高召回,阈值降到 0.3。
参数说明:max_length=256要和训练时一致;round(prob, 4)保留四位小数,方便前端展示。
一个实用技巧:加一层规则兜底。BERT 对短文本和反讽文本容易翻车,我一般会加几条规则,比如文本长度小于 10 直接返回「无法判断」,包含「纯属虚构」「辟谣」等关键词时降低假新闻概率。规则和模型结合,线上 F1 能再提 2 到 3 个点。
最后说个血泪教训:别在测试集上反复调阈值。我见过团队把阈值调到测试集 F1 最高,上线后完全失效。正确做法是留一个独立的验证集调阈值,测试集只用一次。这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取