简介这份资源是面向人工智能与自然语言处理方向学习者、课程设计或期末大作业需求者的虚假评论检测项目源码基于半监督学习思路在Yelp数据集上完成建模与评估适合希望理解文本分类、半监督策略与模型对比的初中级开发者参考。压缩包共14个文件约7.16MB以9个png图表、1个jpeg混淆矩阵图、1个py主程序、1个sh运行脚本、1个csv数据文件及1个md说明文档为主图表覆盖评论长度分布、类别分布、欠采样前后对比与随机森林、朴素贝叶斯等模型评估结果便于直观查看实验过程。项目已有250人学习代码含注释部署门槛较低。读者可从中获得完整的数据处理、模型训练与结果可视化流程理解半监督学习在虚假评论识别中的落地方式并借助现成脚本快速复现实验为课程设计或进一步调参改进提供可用的起点。1. 半监督虚假评论检测标注只有几百条时Yelp 数据集怎么跑出能用的模型Yelp 上一条五星好评可能来自一个从没到过店的账号一条一星差评也可能是竞对批量刷出来的。虚假评论检测要做的就是从文本、行为、时间等信号里把这类评论挑出来。但真正做过的人都知道难点不在模型结构而在标注数据人工标一万条评论成本极高公开数据集里带标签的往往只有几百到几千条剩下全是无标注文本。半监督学习正是冲着这个缺口来的——用少量标注样本定方向用大量无标注样本学分布。这个方向适合两类人一是做人工智能大作业、课程设计的学生需要一套能跑通、能写进报告的完整方案二是刚转推荐/风控方向的工程师想找一个文本二分类的落地练手项目。Yelp 数据集文本短、噪声大、类别不平衡恰好是检验半监督方法成色的试金石。2. 为什么选半监督而不是直接上 BERT 微调Yelp 场景下的选型账2.1 标注成本与数据分布的现实约束先把账算清楚。Yelp 虚假评论数据集常见版本是 YelpChi / YelpNYC / YelpZip 系列也有从 Yelp 开放数据里筛出的评论子集通常包含几十万条评论但真正带fake/real标签的比例很低。假设你手里只有 500 条标注样本直接微调 BERT 会发生什么模型在训练集上很快降到 0.1 的损失验证集 F1 却卡在 0.6 上下震荡——这就是典型的过拟合到小样本的噪声上。更麻烦的是类别不平衡虚假评论往往只占 10%20%模型学会全部预测“真实”就能拿到 80% 准确率但这个数字毫无意义。半监督的核心逻辑是标注数据负责定义“什么是假”无标注数据负责定义“正常评论长什么样”。当模型见过足够多正常文本的分布后那些偏离分布的样本即使没有标签也会被推向“假”的一侧。常见做法有三种自训练Self-Training、协同训练Co-Training、一致性正则如 FixMatch。在 Yelp 这种纯文本场景自训练 伪标签是最容易复现、也最容易讲清楚的一条路。提示不要一上来就堆半监督的复杂变体。先把“有监督基线 伪标签”跑通再考虑一致性正则否则调参空间太大出了问题根本定位不到。2.2 从零搭建Yelp 数据清洗与标注/无标注划分第一步永远是数据。Yelp 原始数据通常是 JSON 行格式每条评论包含review_id、text、stars、date、user_id、business_id等字段。虚假评论检测不能只看文本行为特征往往更有效但半监督阶段我们先以文本为主行为特征留到进阶章。import json import pandas as pd from sklearn.model_selection import train_test_split def load_yelp(path): records [] with open(path, r, encodingutf-8) as f: for line in f: obj json.loads(line) records.append({ review_id: obj.get(review_id), text: obj.get(text, ), stars: obj.get(stars, 0), date: obj.get(date, ), user_id: obj.get(user_id, ), business_id: obj.get(business_id, ), # 若数据集自带标签则读取否则后续用规则生成弱标签 label: obj.get(label, -1) }) return pd.DataFrame(records) df load_yelp(yelp_reviews.json) # 去掉空文本和过短评论少于 5 个词的评论信息量太低 df df[df[text].str.split().str.len() 5].reset_index(dropTrue) print(清洗后样本数:, len(df))这段代码做了三件事逐行解析 JSON、抽取关键字段、过滤短文本。label字段如果数据集没有提供就置为 -1表示无标注。参数上str.split().str.len() 5这个阈值可以调Yelp 上不少虚假评论就是“Great place!”这种三词短句阈值设太高会漏掉正样本设太低会引入噪声我一般先用 5 跑一版看分布。接下来划分标注集和无标注集。假设你通过人工或弱规则拿到了 800 条标注数据labeled df[df[label] ! -1].reset_index(dropTrue) unlabeled df[df[label] -1].reset_index(dropTrue) # 标注集再切训练/验证 stratify 保证类别比例一致 train_lab, val_lab train_test_split( labeled, test_size0.2, stratifylabeled[label], random_state42 ) print(f标注训练集 {len(train_lab)}验证集 {len(val_lab)}无标注池 {len(unlabeled)})stratify是关键参数虚假评论占比低的时候不加分层验证集可能一条假评论都没有F1 直接算不出来。random_state固定住保证每次实验可比。2.3 有监督基线TF-IDF 逻辑回归先跑一个数在引入半监督之前必须有一个基线。别小看 TF-IDF 逻辑回归在 Yelp 短文本上它经常能到 0.750.82 的 F1而且训练只要几秒。这个数字是你后面判断半监督有没有用的标尺。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import f1_score, classification_report vectorizer TfidfVectorizer( max_features20000, # 控制维度Yelp 短文本 2 万足够 ngram_range(1, 2), # 加入 bigram 捕捉 highly recommend 这类刷评套话 min_df3, # 出现少于 3 次的词直接丢降噪 sublinear_tfTrue # 对长评论做 tf 平滑 ) X_train vectorizer.fit_transform(train_lab[text]) X_val vectorizer.transform(val_lab[text]) clf LogisticRegression(max_iter1000, class_weightbalanced) clf.fit(X_train, train_lab[label]) pred clf.predict(X_val) print(基线 F1:, f1_score(val_lab[label], pred, averagebinary)) print(classification_report(val_lab[label], pred))class_weightbalanced是必须的它让损失函数按类别频率反比加权否则模型会偏向多数类。ngram_range(1,2)对虚假评论特别有用因为刷评文本常有固定搭配。跑完这一步记下 F1后面所有半监督实验都跟它比。3. 自训练半监督落地伪标签怎么打、阈值怎么设、什么时候停3.1 伪标签生成与置信度阈值的选择自训练的流程很直白用标注数据训练一个初始模型拿它去预测无标注数据把置信度高的样本连同预测标签加入训练集重新训练循环。听起来简单但阈值设多少直接决定成败。import numpy as np from sklearn.linear_model import LogisticRegression def self_training(X_lab, y_lab, X_unlab, threshold0.9, rounds5): model LogisticRegression(max_iter1000, class_weightbalanced) model.fit(X_lab, y_lab) X_cur, y_cur X_lab, y_lab for r in range(rounds): # 预测无标注样本的概率 probs model.predict_proba(X_unlab) # 取两类中较大的概率作为置信度 confidence probs.max(axis1) pseudo_label probs.argmax(axis1) # 只保留高置信度样本 mask confidence threshold if mask.sum() 0: print(f第 {r1} 轮无样本达到阈值提前停止) break X_new X_unlab[mask] y_new pseudo_label[mask] # 合并后重新训练 X_cur np.vstack([X_cur.toarray(), X_new.toarray()]) if hasattr(X_cur, toarray) else np.vstack([X_cur, X_new.toarray()]) y_cur np.concatenate([y_cur, y_new]) model.fit(X_cur, y_cur) print(f第 {r1} 轮加入 {mask.sum()} 条伪标签样本) # 每轮从未标注池移除已用样本 X_unlab X_unlab[~mask] return model逻辑说明每轮用当前模型给无标注样本打分predict_proba返回两类概率max(axis1)拿到置信度。阈值 0.9 意味着只有模型非常确定的样本才被采纳。rounds5是经验值通常 35 轮后收益就饱和了。注意每轮要把已采纳的样本从无标注池移除否则同一批样本反复加入会导致过拟合。参数怎么调阈值从 0.95 降到 0.8加入的样本变多但噪声也变大。我的经验是先在验证集上试 0.95、0.9、0.85 三档看哪档验证 F1 最高。如果无标注池很大几十万条阈值可以稍低因为大数定律会稀释噪声如果只有几千条阈值必须高。3.2 迭代停止条件与验证集监控自训练最大的坑是“自信地犯错”。模型一旦在某一轮把一批噪声样本以高置信度标错下一轮它会更加确信错误被放大。所以必须每轮在验证集上评估而不是只看加入样本数。from sklearn.metrics import f1_score best_f1 0 patience 2 no_improve 0 for r in range(10): # ... 执行一轮伪标签训练 ... val_pred model.predict(X_val) f1 f1_score(val_lab[label], val_pred, averagebinary) print(f第 {r1} 轮验证 F1: {f1:.4f}) if f1 best_f1: best_f1 f1 no_improve 0 else: no_improve 1 if no_improve patience: print(验证集连续无提升停止迭代) breakpatience2表示连续两轮没提升就停。这个早停机制比固定轮数靠谱得多。验证 F1 的波动如果超过 0.02说明伪标签噪声太大应该提高阈值或减少每轮加入的样本量。注意验证集必须始终只包含真实标注样本绝不能把伪标签样本混进验证集否则你看到的 F1 是虚高的。3.3 类别不平衡下的伪标签偏置修正Yelp 虚假评论通常只占少数自训练会加剧这个问题模型对多数类真实评论更自信伪标签里真实评论占比会越来越高少数类被淹没。解决办法有两个一是在伪标签采样时按类别配额二是对少数类降低置信度阈值。def balanced_pseudo_select(probs, threshold0.9, max_ratio3.0): confidence probs.max(axis1) pseudo probs.argmax(axis1) mask confidence threshold # 统计当前伪标签中两类数量 n_pos ((pseudo 1) mask).sum() # 假设 1 是虚假评论 n_neg ((pseudo 0) mask).sum() # 如果多数类超过少数类 max_ratio 倍对多数类按置信度排序截断 if n_neg n_pos * max_ratio: neg_idx np.where((pseudo 0) mask)[0] neg_conf confidence[neg_idx] keep neg_idx[np.argsort(-neg_conf)[:int(n_pos * max_ratio)]] new_mask np.zeros_like(mask) new_mask[keep] True new_mask[(pseudo 1) mask] True mask new_mask return maskmax_ratio3.0控制多数类最多是少数类的 3 倍。这个值根据你的类别比例调如果原始数据 1:4设 3 能逐步把比例拉回来。这段逻辑不复杂但不加的话几轮之后伪标签里几乎全是真实评论模型对虚假评论的召回会掉得很难看。4. 避坑与排查半监督虚假评论检测翻车实录4.1 验证集 F1 很高但测试集崩了现象本地验证 F1 0.88换一批新数据掉到 0.6。原因通常有两个一是验证集和训练集同分布但测试集来自不同时间段或不同类目Yelp 上餐厅和酒店的刷评模式差异很大二是伪标签泄漏验证集样本不小心进了无标注池。解决按时间或business_id做分组划分确保验证集和测试集不共享同一批商家检查无标注池是否包含验证集review_id。4.2 伪标签置信度普遍偏低现象阈值设 0.9第一轮只有几十条样本达标迭代几乎不前进。原因初始模型太弱或者 TF-IDF 特征区分度不够。解决先加强有监督基线比如换用字符级 n-gram、加入评论长度和标点密度特征或者把阈值降到 0.8 先跑起来但必须配合早停和验证监控。4.3 训练损失下降但 F1 不动现象每轮加入样本后训练准确率上升验证 F1 原地踏步。原因模型在拟合伪标签的噪声而不是学到新信号。解决减少每轮加入的样本量比如只取置信度 top 10%或者引入样本权重给伪标签样本一个小于 1 的权重。4.4 中文评论混入导致特征稀疏现象Yelp 以英文为主但如果你的数据里混了中文或其他语言TF-IDF 词表会被撑大英文特征被稀释。原因没有做语言过滤。解决用简单规则ASCII 字符占比过滤非英文评论或者分别建词表。4.5 随机种子导致结果不可复现现象同样的代码跑两次 F1 差 0.05。原因逻辑回归的random_state没固定train_test_split没固定甚至TfidfVectorizer的哈希也可能有影响。解决所有涉及随机的环节都设random_state42并在报告里写清楚。5. 把半监督做扎实一致性正则与行为特征的融合技巧自训练跑通之后如果你想把 F1 再往上推 35 个点可以试两条路。第一条是一致性正则代表方法是 FixMatch对同一条无标注样本做两次增强要求两次预测一致。文本增强可以用同义词替换、随机删除、回译。实现上用 PyTorch 搭一个简单文本分类器对无标注样本做两次 dropout 前向计算 KL 散度作为一致性损失和标注样本的交叉熵加权求和。权重通常设 0.51.0无标注 batch 大小是标注的 23 倍。第二条是融合行为特征。Yelp 数据里user_id、business_id、date能构造出强信号同一用户短时间内给同一商家多次评论、评论时间集中在深夜、用户历史评论星级方差极小这些都是刷评的典型模式。做法是把这些统计特征和 TF-IDF 拼接def build_behavior_features(df): df[text_len] df[text].str.len() df[exclaim] df[text].str.count(!) df[upper_ratio] df[text].apply( lambda x: sum(1 for c in x if c.isupper()) / max(len(x), 1) ) # 用户评论频率同一 user 在数据中出现的次数 user_counts df[user_id].value_counts() df[user_freq] df[user_id].map(user_counts) return df[[text_len, exclaim, upper_ratio, user_freq]]这些特征用StandardScaler归一化后和 TF-IDF 做hstack。注意行为特征容易泄漏user_freq如果是在全量数据上算的测试集信息就漏进了训练。正确做法是只在训练集上统计再映射到验证和测试集没见过的用户填 0。最后一个技巧关于阈值不要全局用一个阈值。虚假评论检测里不同商家的评论分布不同按business_id分组做自适应阈值通常比全局阈值稳。我自己的习惯是每做完一轮实验把验证集上的预测概率分布画出来看两类是否真的可分如果分布大量重叠说明特征还不够加特征比调阈值有用。这套方案从基线到半监督再到特征融合代码量不大但每一步都有明确的验证点适合作为人工智能大作业或风控方向的入门项目。希望帮到你。本文还有配套的精品资源点击获取