中文谣言检测实战:从jieba分词到TF-IDF与融合模型
简介面向计算机、人工智能及相关专业的本科毕业设计资料主题为中文谣言检测目标是通过自然语言处理与机器学习技术构建识别和追踪虚假信息的完整系统适合需要完成同类课题的高年级本科生与入门研究者使用。整套资料覆盖数据采集、清洗、分词、特征提取、模型训练、效果评估及谣言演化分析等关键环节能够帮助读者从零搭建谣言检测实验框架。压缩包内共有二十六个文件包含十二个Python脚本、九个文本文件、四个JSON数据集以及一个Markdown说明文档。脚本阶段式完成jieba分词、TF-IDF特征计算、朴素贝叶斯与逻辑回归建模、聚类及最终模型等步骤配套的文本与JSON文件则提供停用词表、标签列表和多轮整理后的训练数据。全部资料约4.93MB按模块编号排列便于对照论文思路逐步复现和二次开发目前已有125人学习借助这些代码与数据可完整走通中文谣言识别实验也可基于已有管线快速迁移到类似文本分类任务。1. 中文谣言检测这套毕设资源能跑通还是要拆开看中文谣言检测这件事最近被问得越来越多。内容治理、舆情应对、社交平台风控都会在某个时间点把“这条帖子是不是谣言”抛给技术侧。这套中文谣言检测本科毕业设计恰好是一条从原始文本走到分类结果的最小闭环合并语料、打标签、jieba 分词、TF-IDF 特征、停用词过滤、朴素贝叶斯与聚类、逻辑回归每一步都对应一个脚本序号从 1 排到 8。适合两类人一是刚接触 NLP 的本科生想找一份能跑通的中文文本分类骨架二是需要快速验证“规则之外还有多少自动分类空间”的从业者。它解决的是从数据到指标的最小路径不解决上线工程想直接部署得自己补一截。2. 数据管道从原始语料到训练集四步脚本怎么搭这套资源真正值钱的不是最后那个分类器而是前几号脚本把“原始 JSON - 干净语料 - 数值特征”的过程固定了下来。压缩包里同时出现了 data.json、alldata.json、data_1.json、data_2.json看命名顺序就知道跑过不止一轮。我建议按 1、2、3、4 的顺序重新跑别直接拿中间文件当输入。读 readme 之前先看脚本名里的数字前缀这套命名本身就是完整流程1_wholedataProcess 是合并2_addtag 是打标签3_1_rumorProcess 是样本筛选3_2_featureProcess 是特征化前置。2.1 全量合并1_wholedataProcess.py 先把多目录数据并到一张表合并脚本做的事情很朴素把不同目录下的 JSON 全部读出来塞进一个大的 Python 列表。常见结构是“谣言/非谣言”两个目录每个目录下再按事件拆子目录文件可能是 JSONL 一行一条也可能是整包 JSON。脚本需要兼容这两种情况。import json import glob import os def merge_all(data_dirChinese_Rumor_Dataset): merged [] # 语料按目录分堆这里把两级目录下的 json 全部拉出来 for path in glob.glob(os.path.join(data_dir, *, *.json)): with open(path, r, encodingutf-8) as f: content f.read().strip() if content.startswith([): merged.extend(json.loads(content)) else: for line in content.splitlines(): line line.strip() if line: merged.append(json.loads(line)) with open(alldata.json, w, encodingutf-8) as f: json.dump(merged, f, ensure_asciiFalse, indent2) return merged这段逻辑里最值得关注的是startswith([)的判断。整包数组和 JSONL 流式格式的处理方式完全不同如果不做分支很多行解析会在第二层 JSON 解析处直接抛异常。ensure_asciiFalse保证中文以明文写进 alldata.json而不是一长串 \uXXXX。indent2是给人工抽查用的正式流程里可以去掉以省磁盘。2.2 打标签2_addtag.py 用目录名生成 0/1标签工程是整个谣言检测里最容易被糊弄过去的一步。很多目录结构本身就暴露了答案比如某个事件目录叫 rumor另一个叫 non_rumor。此时打标签的逻辑就是拿目录名映射到 0/1。import os def add_tag_by_source(obj, source_to_label): # 从 url 字段反推上一级目录名再映射标签 source os.path.basename(os.path.dirname(obj.get(url, ))) label source_to_label.get(source, 0) text (obj.get(text) or ).strip() if len(text) 5: return None obj[label] label return obj这里有个隐蔽的坑url字段可能缺失也可能目录结构在清洗时已经变了。我一般会先跑一段检查脚本把全局出现过的 source 名称打印出来再手工维护映射表。标签一旦打错后续所有模型指标都是虚假繁荣。空文本和超短文本直接过滤掉是因为它们进入特征工程后只会制造噪声对区分谣言没有帮助。2.3 谣言文本筛选3_1_rumorProcess.py 拼接标题正文与回复原始字段的文本往往是碎片化的标题归标题正文归正文评论又是另一个列表。如果只取其中一个字段模型大概率学到的是字段长度差异而不是内容差异。因此常见做法是把标题、正文和回复拼接成一条完整文本再交给分词器。import re def clean_text(s): if not s: return s re.sub(r[^], , s) # 去掉 html 标签 s re.sub(r\s, , s) # 连续空白压缩成单空格 return s.strip() def build_text(item, max_replies5): parts [ clean_text(item.get(title)), clean_text(item.get(content)), ] for r in (item.get(replies) or [])[:max_replies]: parts.append(clean_text(r.get(text))) return .join(p for p in parts if p)max_replies5是一个需要按语料调的参数。回复太少上下文补充不够回复太多单条样本长度会失控TF-IDF 矩阵里会出现大量低频噪音。从效果看取前 5 条回复已经能覆盖大部分转发场景里的补充信息。清洗时优先去掉 html 标签是因为爬下来文本经常夹带a、br这类标签直接保留会让分词结果变成一串乱码。2.4 特征化前置3_2_featureProcess.py 写样本清单与划分数据处理阶段的最后一步是把文本和标签落成按行对齐的文本文件。textList.txt 和 classList.txt 之所以存在就是让后续脚本不需要重新加载 JSON直接读两行文件就能训练。这样的设计虽然简单但在实验迭代时非常省事。import random def dump_lists(pairs, text_path, label_path): with open(text_path, w, encodingutf-8) as ft, open(label_path, w, encodingutf-8) as fl: for text, label in pairs: ft.write(text \n) fl.write(str(label) \n) random.seed(42) random.shuffle(pairs) split int(len(pairs) * 0.8) dump_lists(pairs[:split], textList.txt, classList.txt) dump_lists(pairs[split:], textList_test.txt, classList_test.txt)random.seed(42)必须固定否则每次跑完的样本划分都不同模型对比就没有意义。80/20 划分是文本分类里最常用的起点如果你的语料量超过几十万条可以把验证集比例下调到 10%但千万不要用 90/10 以下的训练占比否则小语料里谣言类别的代表样本会非常少。3. 特征工程与统计模型jieba 分词后TF-IDF 和朴素贝叶斯才成立中文文本不能像英文那样直接按空格切词所以整套资源里分词脚本的位置很靠前。压缩包里出现了 jieba.txt、stopword.txt、stop_word.txt 三个文件它们分别对应分词结果缓存、大停用词表和小停用词表。这里有一个容易被忽略的细节资源方同时保留了两份停用词表说明调参过程里发现过单份表不够用的情况。3.1 停用词表stopword.txt 和 stop_word.txt 为什么两份都要看停用词过滤做得好不好直接决定 TF-IDF 特征里剩下的词是不是有效。常见公开停用词表在新闻场景下表现不错但放到社交平台谣言文本里像“转发”“扩散”“求证”“辟谣”这类词出现频率极高却不应该被删干净因为它们本身就是谣言文本的信号词。def load_stopwords(*paths): words set() for p in paths: with open(p, r, encodingutf-8) as f: for line in f: w line.strip() if w: words.add(w) return words stop_large load_stopwords(stopword.txt, stop_word.txt) print(len(stop_large))把两份表合并的原因是单份表覆盖不全A 表有标点符号和虚词B 表有“转发”“链接”这类社交平台词。合并后按全量过滤能明显减少分词结果里的空转词。我一般会额外打印被删除次数最多的前 100 个词逐批检查里面有没有被误删的情绪词或否定词这一步能拦住很多后续模型里的“正道偏差”。3.2 分词5_3_jiebaTM.py 的 cut 模式选择jieba 分词有两个常用模式cut_allTrue是全模式输出词更多更碎cut_allFalse是精确模式更适合文本分类。谣言检测场景里不需要把句子切到最细保留有语义完整性的词更重要。import jieba def tokenize(text): words jieba.lcut(text, cut_allFalse) return [w for w in words if w not in stop_large and len(w) 1]jieba.lcut返回 list省掉cut之后自己转 list 的步骤。过滤条件里len(w) 1会去掉单字词这在大多数情况下有效因为单字词大多是“的、了、是、在”这类虚词或分词碎片。但要注意当文本里频繁出现“不”“没”“别”这种单字否定词时这个过滤条件是会帮倒忙的。标准做法是单独保留一份否定词白名单在过滤之后重新加回 token 序列。3.3 5_1_preTM.py 和 5_4_tfidfTM.py 的分工从脚本命名看5_1_preTM.py 负责的是传统模型的前置处理也就是把分好词的语料转换成模型可以读取的中间文件5_4_tfidfTM.py 则是真正训练 TF-IDF 特征加统计模型的那一步。这套拆法很实用因为中文语料分词成本高如果每次调模型参数都要重新跑一遍 jieba迭代效率会非常低。import pickle corpus [ .join(tokenize(t)) for t in texts] with open(jieba.txt, w, encodingutf-8) as f: f.write(\n.join(corpus)) with open(preprocessed_corpus.pkl, wb) as f: pickle.dump(corpus, f)把分词结果落成 jieba.txt 或者 pickle 文件之后所有模型脚本都从这份缓存读取。这样改动 TF-IDF 参数、换分类器、调 ngram_range 都不需要重新分词。这里的命名规则值得借鉴pre 开头的方法是准备数据真正的模型文件名用的是 fit 或 train 结尾。3.4 TF-IDF 加朴素贝叶斯的完整基线传统中文文本分类里TF-IDF 加朴素贝叶斯是一个足够稳定且不挑机器的基线。它不一定打得过 BERT但一定能在十分钟内给出可复现的数字。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import make_pipeline pipe make_pipeline( TfidfVectorizer( tokenizerlambda s: s.split(), # 文本已被 jieba 切好并用空格连接 ngram_range(1, 2), max_features50000, sublinear_tfTrue, min_df2 ), MultinomialNB(alpha0.5) ) pipe.fit(train_texts, train_labels)这里让 TfidfVectorizer 的 tokenizer 只做s.split()是因为预处理阶段已经用 jieba 分好词了。ngram_range(1, 2)表示同时使用单词与双词特征双词能捕捉“不是谣言”“尚未证实”这类否定结构。max_features50000是个保守上限中文社交语料的词表通常很大不设上限很容易在 8G 内存机器上直接触顶。sublinear_tfTrue会对词频做 log 平滑削弱长文本中反复出现的语气词冲击。4. 聚类与监督模型7 号脚本的 KMeans 和 8 号脚本的融合整套资源的模型设计不是单一的监督学习而是先跑无监督聚类再跑逻辑回归最后用融合模型收口。我一开始也疑惑为什么要多此一举后来复现才知道聚类在这里不是拿来直接分类而是给最终模型提供“样本离哪个话题簇更近”这类补充特征。4.1 KMeans 在谣言检测里的用途先看文本抱团谣言文本在传播过程中会产生明显的改写和演化同一事件的谣言往往共享一批关键词。KMeans 把这些词向量空间里的点聚成若干话题簇训练完成后每条样本到各个簇中心的距离可以作为额外特征。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score X tfidf_vec.fit_transform(corpus) for k in range(4, 13): km KMeans(n_clustersk, random_state42, n_init10).fit(X) score silhouette_score(X, km.labels_, sample_size5000) print(k, score)KMeans 的k不建议拍脑袋定 8而是扫一遍轮廓系数。silhouette_score越接近 1 说明簇内越紧凑但文本特征下分数普遍偏低更多是看相对变化。n_init10表示用 10 组随机中心初始化后取最优避免 KMeans 陷入局部解。random_state42必须固定否则每次跑出的簇标签不同后面融合模型的特征也会跟着跳。4.2 逻辑回归6_2_logisticFM.py 把特征压成概率逻辑回归在稀疏 TF-IDF 特征上表现非常稳定而且输出的是概率而不是硬分类这为后面的融合提供了方便。from sklearn.linear_model import LogisticRegression clf LogisticRegression( C1.0, class_weightbalanced, solverliblinear, max_iter500 ) clf.fit(X_train, y_train) prob clf.predict_proba(X_val)[:, 1]C1.0是正则强度的倒数C 越小正则越强。谣言语料里负样本往往远多于正样本class_weightbalanced会按类别频率自动调整权重避免模型把所有样本都判成非谣言。solverliblinear对稀疏矩阵支持好且训练快换成lbfgs在特征维度很高时经常不收敛。max_iter500是为了防止默认 100 次迭代在复杂特征下报“未收敛”警告。4.3 8_finalModel.py 的融合策略概率当特征最终模型不是把 KMeans 的簇标签直接当输出而是把前面几个模型的概率或距离拼成新的特征矩阵再训练一个逻辑回归。这个思路本质上是轻量级 Stacking。import numpy as np from sklearn.linear_model import LogisticRegression features np.column_stack([ nb_prob, # 朴素贝叶斯输出的谣言概率 lr_prob, # 逻辑回归输出的谣言概率 cluster_dist # 样本到各聚类中心的距离 ]) meta LogisticRegression(C0.5, max_iter500) meta.fit(features, y_fit) final_prob meta.predict_proba(features_val)[:, 1]融合模型最怕的是数据泄漏如果nb_prob和lr_prob是在同一份训练集上直接算出来的再拿这组概率训练 Meta 模型融合阶段几乎必过拟合。常见做法是用cross_val_predict生成样本外概率再把样本外概率喂给融合模型。cluster_dist我一般取“到最近三个簇中心的距离”而不是只有一个最近簇这样能保留样本处在话题边缘时的模糊信息。5. 中文谣言检测避坑指南五个高频翻车现场前面把主流程讲完了接下来是复现这套资源最容易踩的五个坑。每个都是我实际跑的时候见过的现象原因和解法写在一起方便对照排查。5.1 文件读进来全是乱码或直接抛 UnicodeDecodeError现象脚本跑到一半报UnicodeDecodeError: utf-8 codec cant decode byte或者中文打出来全是乱码。原因原始 JSON 是不同编码混合存储的有的带 BOM有的是 GBK统一按 UTF-8 读必然炸。解决写一个自动尝试多编码的加载函数。def load_jsonl_auto(path): for enc in (utf-8-sig, utf-8, gbk): try: with open(path, r, encodingenc) as f: return [json.loads(line) for line in f if line.strip()] except UnicodeDecodeError: continue raise ValueError(fdecode failed: {path})utf-8-sig必须放在utf-8前面因为它能自动剥离 BOM 头。GBK 放在最后兜底是因为它在纯英文数据上不会报错但遇到生僻字可能解出错误字符。这个函数只能保证不崩无法保证语义无损所以跑完以后还要抽查几条中文是否正常。5.2 标签分布全是 0模型“学了个寂寞”现象训练完准确率看起来很高但召回率几乎为 0打印标签分布发现正样本一条都没有。原因目录名映射的 label_map 键值和实际目录名对不上比如目录叫 rumer映射表里写的是 rumor所有样本都落进默认的 0 类。解决先打印所有 source 去重结果。from collections import Counter sources [os.path.basename(os.path.dirname(item.get(url, ))) for item in alldata] print(Counter(sources))看到真实目录名后再维护映射表而不是猜。这一步很玄学因为 JSON 里的 url 字段可能是空的也可能是相对路径。稳妥做法是同时打印 url 的前两段和上级目录名人工确认两次再写映射逻辑。5.3 把“不、没、别”请进停用词表模型丢掉关键否定现象加了停用词过滤后准确率小幅提升但谣言样本的召回率掉到 0.3 以下。原因停用词表里包含了否定副词模型看不到“不是”“未经证实”这类关键信号。解决过滤前先检查停用词表命中情况。negatives [w for w in stop_large if w in (不, 没, 别, 无, 尚未)] print(negatives)如果发现这些词在停用词表里直接删掉。更好的做法是把停用词过滤的时机放在分词之后、特征化之前单独保存一份否定词白名单遇到“不”字开头的 token 时强制保留。千万别图省事把整个通用停用词表全量套上去。5.4 TF-IDF 矩阵直接把内存撑爆现象程序运行到fit_transform时被系统杀死或者卡到像是在死循环。原因没有设max_features又把ngram_range开到了 (1, 3)词汇表上千万维稀疏矩阵也顶不住。解决先压 feature 维度再考虑模型参数。TfidfVectorizer( ngram_range(1, 2), max_features50000, sublinear_tfTrue, min_df2, max_df0.8 )min_df2表示至少在 2 条样本中出现过的词才保留能滤掉爬虫噪声和乱码碎片。max_df0.8表示在 80% 以上样本中都出现的词会被删掉这类词基本是通用连接词。设完这四个参数后矩阵维度通常能压到百万级以下普通笔记本能跑。5.5 KMeans 每次跑出来的标签都不一样现象两次运行之间聚类结果和后续模型指标明显波动。原因KMeans 初始化中心是随机的没有固定随机种子。解决固定random_state并增加n_init。km KMeans(n_clusters8, random_state42, n_init50) km.fit(X)n_init50会从 50 组不同中心里选目标函数最优的那一次虽然速度慢一些但稳定性比默认值强得多。聚类脚本在整套资源里属于前置特征生成如果这个环节不稳定后面所有模型对比都不可信。固定随机种子这件事在数据处理阶段就该做而不是拖到模型训练才想起来。6. 把训练好的模型接到新语料上验证集与阈值调优整套流程跑通之后真正能拿去应对新语料的是训练好的向量器和融合模型。但有一个地方很容易被忽略model.predict()输出的硬分类标签是拿 0.5 当默认阈值切的而谣言检测场景里正负样本往往不平衡0.5 几乎不可能是最优切割点。我接新语料时习惯把整个评估流程拆成“加载模型 - 输出概率 - 扫阈值”三步。先用 joblib 把向量器和模型加载回来然后只预测概率不急着转成 0/1 标签。import joblib vec joblib.load(tfidf_vec.pkl) model joblib.load(final_model.pkl) X_new vec.transform(new_texts) prob model.predict_proba(X_new)[:, 1] print(prob)下一步拿一份带人工标注的验证集扫一遍不同阈值下的 F1而不是只看准确率。谣言检测更看重的是“别漏掉真谣言”所以我会优先看召回率在召回率不掉太狠的前提下尽量拉高精确率。from sklearn import metrics for th in [0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65]: y_pred (prob th).astype(int) print(th, precision, metrics.precision_score(y_val, y_pred), recall, metrics.recall_score(y_val, y_pred), f1, metrics.f1_score(y_val, y_pred))阈值调好后还要注意两个边界问题。第一新语料如果来自不同平台或不同时间段词汇分布会变最好先算一下新语料在特征空间里和训练集的分布距离差距太大就直接重训分词缓存。第二模型概率输出是相对分数不是绝对置信度线上要做的不是改阈值而是持续回流预测错例。有一次我拿到一批新事件语料直接沿用 0.5 阈值上线结果把一批“疑似谣言但已被官方辟谣”的文本全部误报成了谣言。后来才想明白这类文本在句式和关键词上比谣言还像谣言唯独缺少了“多个独立信源交叉验证”这个特征。从那以后我每次接新语料都强制走一遍阈值扫描同时把误报样本按关键词聚类回填进训练集里做增量更新。这套资源把数据预处理、分词、特征、聚类、分类、融合的完整链路都铺好了很适合当作中文文本分类的工程骨架。前 80% 的路它替你铺平了剩下 20% 的工程化验证得自己在真实语料上补齐。希望帮到你。本文还有配套的精品资源点击获取