尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于Yelp的半监督虚假评论检测:从数据预处理到伪标签实战

发布时间:2026/9/26 8:22:00

资讯中心
01
ARTICLE

基于Yelp的半监督虚假评论检测:从数据预处理到伪标签实战

基于Yelp的半监督虚假评论检测:从数据预处理到伪标签实战
简介一个基于半监督学习的虚假评论检测项目面向人工智能、数据挖掘方向的课程设计与期末大作业场景。项目以Yelp公开评论数据集为对象使用Python语言实现涵盖数据分布分析、欠采样处理、多种分类模型对比评估等完整流程采用Random Forest、Naive Bayes等算法并配置混淆矩阵、评论长度分布等可视化图表帮助理解半监督学习在虚假评论识别中的实际应用。压缩包共14个文件以py脚本为核心配合csv数据文件、sh运行脚本、png/jpeg可视化图片及md说明文档整体包体大小7.16MB结构紧凑便于快速部署。目前已有250人学习下载适合需要高分项目参考、快速搭建检测模型的初学者与进阶者。资源内代码注释详细随附可视化结果与数据文件可直接运行复现也可作为课程设计报告与答辩的支撑材料。1. 半监督虚假评论检测为什么说这是人工智能大作业里最值得认真做的方向这个标题说的是一个很具体的落地场景在Yelp评论数据集上用半监督学习做虚假评论检测并且交付一份跑得通的源码。很多人在人工智能导论或课程设计里拿到这个题目第一反应是直接拿BERT微调一个分类器结果发现标注样本太少——Yelp上千万条评论人工确认过的虚假样本往往只有几百条监督学习根本喂不饱。半监督学习恰恰是为这种场景设计的用少量人工标注锁定决策边界再用海量无标注评论持续修正边界的位置。这篇文章从数据解析、特征构造、伪标签实现到参数调优按一条能复现的路径完整讲清楚目标是让你拿到手后不只是会跑还能答清楚原理和踩过的坑。2. 半监督虚假评论检测的立足点Yelp数据长什么样标签为什么稀缺又带噪2.1 Yelp Open Dataset的字段构成review/user/business三张表怎么串Yelp Open Dataset是官方公开的真实评论数据集解压后的核心是三个JSON文件business.json保存商家信息review.json保存评论内容user.json保存评论者档案。三个文件通过business_id和user_id这两个主键关联起来review.json里每一条记录都带有评论者的ID和对应商家的ID。import json with open(yelp_academic_dataset_review.json, r, encodingutf-8) as f: for i, line in enumerate(f): record json.loads(line.strip()) if i 0: print(list(record.keys())) print(record[text][:200]) break这段代码只读第一行然后退出不会把整个几GB的文件载入内存是拿到数据集后第一件该做的事。打印的键名会告诉你能用的字段有哪些。不同年份发布的Yelp版本字段略有差异老版本里直接携带过滤标记相关的字段新版本的字段可能已经变化先确认键名再做后续处理是常规动作。常见字段大致如下字段含义在特征工程中的作用review_id评论唯一ID只作为主键不进特征user_id评论者ID关联用户历史统计后丢弃business_id商家ID关联商家统计后丢弃stars用户打星1-5星级偏离度是强信号text评论正文喂给文本模型的原料date评论日期计算评论者活跃跨度useful/funny/cool其他用户投票数反映评论被社区接受程度这个表格看起来简单但它是整个数据工程的起点。实际项目里一个很常见的翻车操作是直接把user_id和business_id做成embedding输入模型模型很快会学会按ID记忆真假换一批新商家立刻失灵。2.2 标签构造的三种来源与噪声分析Yelp官方数据集并不直接给每条评论标注“这是虚假评论”这是整个项目最需要想清楚的地方。从业者常用的标签来源有三种各有各的成本和噪声。第一种是官方过滤信号。Yelp有自己的评论过滤系统老版本数据集中被过滤掉的评论常被当作可疑正样本。优势是零人工成本可以一下拿到几万条候选劣势是过滤系统本身有误杀也存在漏网之鱼标签噪声比例估计在10%到30%之间。第二种是启发式规则生成初筛标签。比如将正文高度重复、评论者一次性给多个商家写评论、账号活跃天数极短等规则组合筛出一批疑似样本剩余样本归为疑似正常。规则方案跑得快但规则的边界很难定义得干净通常用来做候选池不适合直接做标签。第三种是人工复核小样本。从上述候选池中随机抽取一批让两三个人独立标注再取一致的部分最终得到几百条高置信度的标注数据。这是半监督训练的“种子”。我一般会把三种结合起来启发式规则生成候选池、人工复核标注一小批种子集、官方信号或Yelp自身过滤结果作为补充参考但绝不直接全部当labels用。原因很简单——半监督训练会把标签噪声成倍放大如果种子标签里混了一堆错的后面伪标签也会错误地自我强化。2.3 半监督学习为什么在这个任务上能成立半监督学习的核心假设是无标注数据里蕴含着关于决策边界位置的信息只是模型需要合适的方法把它提取出来。在虚假评论检测这个任务里这个假设相当成立因为虚假评论生产存在明显的模板化特征和群体行为模式。三种常见半监督路线在这个任务上的表现差异很大。第一种是自训练加伪标签先用种子模型给无标注数据打标签挑高置信度的加入训练集迭代若干轮。优点是实现简单、显存开销小缺点是确认偏差问题突出模型会把初始错误预测当成正确答案喂给自己。第二种是一致性正则化对同一条评论做两次不同的随机增强要求模型输出一致代表方法有MixMatch、FixMatch一类。这能有效抑制确认偏差但对增强方式的要求高评论数据的语义增强并不好设计。第三种是图方法把评论者、评论、商家构造成图用标签传播做半监督学习能利用行为网络结构信息但构造和训练开销都很大。对Yelp这个场景我推荐的组合是伪标签为主、一致性正则辅助。伪标签让项目快速跑起来一致性正则用来稳定训练。后面的代码会围绕这个组合展开。3. Yelp数据预处理从几GB原始JSON到半监督可用的训练集3.1 分块读取与字段裁剪不吃内存的IO写法review.json未压缩时通常是几个GB直接pandas.read_json会吃掉大量内存经常在中途被kill。常规做法是逐行解析先裁字段再转DataFrame最后落盘为parquet后续实验就不需要再碰原始大文件了。import json import random import pandas as pd records [] with open(yelp_academic_dataset_review.json, r, encodingutf-8) as f: for line in f: record json.loads(line.strip()) text record.get(text, ) if len(text) 20: # 过短文本信息量不足直接丢掉 continue records.append({ review_id: record[review_id], user_id: record[user_id], business_id: record[business_id], stars: record[stars], text: text, date: record[date], useful: record.get(useful, 0), funny: record.get(funny, 0), cool: record.get(cool, 0), }) if len(records) 500000: # 降采样到50万条防止内存与训练时间失控 break df pd.DataFrame(records) df.to_parquet(yelp_reviews_sample.parquet, indexFalse) print(df.shape)这段代码里值得注意的参数是50万这个降采样上限。它不是一个标准答案而是一个平衡点数据量再大半监督提升幅度开始收敛训练时长却线性上升。如果你的机器内存充足且需要覆盖更多无标注分布可以把上限放到200万但要记得同时检查正负样本比例有没有因为截断而失真。为什么要落盘成parquet而不是csv因为parquet列式存储读取快、省空间重复实验时直接load就行。实际体验里这一步能把后续启动时间从几分钟压到十几秒属于非常划算的一笔投入。3.2 文本与行为特征工程哪些数字真的暴露了虚假评论特征工程直接影响模型上限。虚假评论在文本层面有几个常见弱点词汇重复率偏高、句式机械化、情绪表达极端且空洞。把这些信号转成数字比让模型从原始文本中自己摸索要直给得多。import re def build_text_features(text): sentences re.split(r[.!?。], text) words re.findall(r\b[a-zA-Z]\b, text.lower()) unique_words set(words) return { text_len: len(text), n_sentences: len([s for s in sentences if s.strip()]), avg_sent_len: len(text) / max(1, len([s for s in sentences if s.strip()])), capital_ratio: sum(1 for c in text if c.isupper()) / max(1, len(text)), exclam_count: text.count(!) text.count(), unique_word_ratio: len(unique_words) / max(1, len(words)), }这些特征不是随机凑的。capital_ratio高通常意味着评论用大量大写强调情绪这是机器生成或文案模板常见痕迹。unique_word_ratio反映词汇丰富度虚假评论大量复用固定句式时这个值明显偏低。评论者行为特征更重要也更隐蔽。需要从user.json或者在同一份数据里按user_id聚合统计评论者历史评论总数、历史平均打星、当前评论星级与历史均值的差、评论者从第一次到最后一次评论的时间跨度。把当前星级和历史均值做差能抓到“大批量给不同商家打低分或打满分”的刷单行为模式。注意这类行为特征只能以聚合值的形式进模型原始user_id等身份字段必须丢弃。否则模型学到的是“ID为xyz的用户都是假的”这属于身份捷径而非行为规律完全经不起跨数据源验证。3.3 有标注/无标注集合的切分分层采样与隔离测试集半监督实验里切分方式对结果的影响甚至超过模型结构。最常见错误是从全量里随机抽几百条作为标注数据这样做标注集里虚假评论占比可能连5%都不到模型连一条像样的决策边界都学不出来。from sklearn.model_selection import train_test_split # 假设已通过规则初筛得到候选正样本和负样本 train_pos, test_pos train_test_split( labeled_pos, test_size200, random_state42) train_neg, test_neg train_test_split( labeled_neg, test_size200, random_state42) # 有标注集合按 25% 正样本、75% 负样本的比例配额 n_pos 125 n_neg 375 labeled_df pd.concat([ train_pos.sample(n_pos, random_state42), train_neg.sample(n_neg, random_state42) ]) # 无标注集合 全部数据 - 有标注集合 - 测试集合 unlabeled_df df_full.loc[ ~df_full.index.isin(labeled_df.index | test_pos.index | test_neg.index) ]这套切分逻辑里的关键点是约束标注集中正样本占比。把有标注正样本配到25%左右模型在冷启动阶段就能区分出两类的大致边界伪标签的质量会明显更高。测试集正负各200条并且从切分那一刻起完全不触达训练流程。test_size200表示测试集数量random_state保证可复现。一个容易被忽视的细节是测试集也要保持和真实场景相似的类别比例。如果测试集人工做成1:1评估指标会偏乐观如果测试集是原始分布里5%的正样本F1会更真实但波动也更大。建议单独留两份测试集一份平衡版本用来调参一份原分布版本用来汇报最终结果。4. 基于PyTorch的半监督检测实现伪标签与自训练完整步骤4.1 模型搭建GRU句向量与统计特征拼接的分类器预训练语言模型在这个任务上确实效果好但会把项目变成重型工程。一份常见的高分项目源码通常从轻量模型起步文本用GRU编码再和统计特征拼接最后过一个两层全连接分类头。这样做的实际收益是训练速度快能在一台普通显卡上迭代几十轮实验把伪标签策略和超参数调明白后再考虑是否升级骨干。import torch.nn as nn class ReviewDetector(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, n_stats_features12): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.gru nn.GRU(embed_dim, hidden_dim, num_layers1, bidirectionalTrue, batch_firstTrue) self.classifier nn.Sequential( nn.Linear(hidden_dim * 2 n_stats_features, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 2) ) def forward(self, text_ids, text_len, stats_features): embedded self.embedding(text_ids) gru_out, _ self.gru(embedded) text_emb gru_out[:, -1, :] combined torch.cat([text_emb, stats_features], dim-1) logits self.classifier(combined) return logits这里有个值得注意的细节代码取gru_out[:, -1, :]作为句向量这对变长序列来说并不严谨正确做法应按照text_len取每个样本最后一个有效位置的隐状态。在文本长度都接近max_len且padding占少数的情况下影响不大但如果是长评论分布差异很大这个简化会带来误差。我会在完整代码里用torch.gather按长度索引来取真实最后一步。embed_dim128、hidden_dim128是面向速度和稳定的参数不是追求精度的配置。如果后续要刷分把这两个数翻倍同时把dropout调到0.5通常会涨两三个点F1代价是训练时间涨一倍。4.2 伪标签生成不是所有无标注数据都值得进训练池半监督训练的第二阶段模型会对无标注数据预测概率然后把置信度超过阈值的样本连同预测标签一起加入训练集。这个阈值是项目里最灵敏的旋钮直接影响最终分数。torch.no_grad() def generate_pseudo_labels(model, unlabeled_loader, threshold0.9, devicecuda): model.eval() pseudo_samples [] for batch in unlabeled_loader: text_ids batch[text_ids].to(device) text_len batch[text_len].to(device) stats batch[stats].to(device) logits model(text_ids, text_len, stats) probs torch.softmax(logits, dim-1) max_prob, pseudo_label torch.max(probs, dim-1) mask max_prob threshold for i in torch.nonzero(mask).flatten().cpu().tolist(): pseudo_samples.append({ text_ids: text_ids[i].cpu(), text_len: text_len[i].cpu(), stats: stats[i].cpu(), pseudo_label: int(pseudo_label[i]), confidence: float(max_prob[i]) }) return pseudo_samples这段代码在model.eval()模式下关闭了dropout保证预测稳定。每次迭代结束后重新生成一次伪标签模型权重更新伪标签集合也更新。confidence字段要保留后面调阈值时会用它排序。threshold0.9只是一个起点。0.95时可能只有几百条伪标签0.85时可能涌进上万条而且置信度分布通常不是线性的。所以项目里我会先跑一次完整预测画出置信度直方图看看样本量在哪个区间出现断崖再把阈值选在断崖前三分之一的位置。这是避免阈值玄学的具体操作。4.3 训练策略与关键超参数阈值、权重、epoch怎么配合完整训练分两阶段。第一阶段只在有标注数据上训练3到5个epoch得到一个种子模型第二阶段让种子模型生成伪标签把有标注数据和高置信度伪标签混合起来联合训练。import torch from torch.utils.data import DataLoader def train_stage2(model, labeled_loader, pseudo_loader, optimizer, epochs5, lambda_u0.5): loss_fn nn.CrossEntropyLoss() for epoch in range(epochs): model.train() total_loss 0.0 for labeled_batch, pseudo_batch in zip(labeled_loader, pseudo_loader): # 有监督部分真实标注的交叉熵 logits_l model(labeled_batch[text_ids], labeled_batch[text_len], labeled_batch[stats]) loss_l loss_fn(logits_l, labeled_batch[label]) # 无监督部分伪标签的交叉熵权重为lambda_u logits_u model(pseudo_batch[text_ids], pseudo_batch[text_len], pseudo_batch[stats]) loss_u loss_fn(logits_u, pseudo_batch[pseudo_label]) loss loss_l lambda_u * loss_u optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fStage2 Epoch {epoch1}, Loss: {total_loss:.4f})这里zip会让两个循环在短的一侧结束所以伪标签loader的batch数量必须不少于有标注loader否则后面几个epoch实际只在训练有标注数据半监督名存实亡。常见做法是给伪标签loader套一个无限循环迭代器或者采样时保证伪标签数据量够多。lambda_u是最值得调的第二个参数。阈值选得高伪标签总体可靠lambda_u可以放到1.0阈值选得低就必须降到0.3以下防止噪声样本主导梯度。我的经验是先在固定阈值0.9下把lambda_u从0.5、1.0、2.0各跑一趟选出最优后再回头调阈值。调参顺序反了会浪费大量时间。参数建议范围对结果的影响置信度阈值0.85 - 0.95决定伪标签数量与噪声比例lambda_u0.3 - 1.0决定无监督损失占比有标注预训练epoch3 - 5决定种子模型质量联合训练epoch5 - 10太长会过拟合伪标签噪声batch_size16 - 32影响BN统计量与训练稳定性5. 避坑半监督虚假评论检测的常见问题与排查路径5.1 现象伪标签噪声越滚越大模型从只犯小错变成全盘崩溃原因伪标签阶段把模型的偏误当成正确答案重新喂给自己模型对某些错误模式越来越自信形成自我强化的恶性循环。这是自训练方法的通用毛病在虚假评论这种类别不均衡数据上尤其明显。解决阈值设高只是缓兵之计。真正有效的做法是每轮只取置信度排序前一半的新伪标签并且在上轮伪标签中剔除当前验证集表现明显变差的样本。更稳的变体是同一份文本用两次不同dropout的预测两次预测类别一致的才进伪标签池。这种双预测策略能拦住相当一部分模型自身的系统性偏误。5.2 现象准确率97%F1却只有0.12模型几乎没抓到虚假评论原因原始分布里真实评论占绝大多数模型全部判真实就能拿到高准确率这是类别不均衡场景下最经典的指标假象。解决训练指标从准确率切到F1并且以PR曲线作为调参依据。损失函数里给正样本加权权重建议从标签分布的反频率开始试比如负正比20:1时给正样本损失乘10左右。评估时只用平衡测试集做内部调参最后再用原分布测试集汇报一次两者都报才是完整结果。5.3 现象验证集F1漂亮换一批商家数据立刻失效原因把user_id、business_id之类的主键直接放进特征模型学会的是记住特定ID的真假而不是识别虚假评论的一般规律。这在数据切分时往往看不出来因为同批数据的主键模式被记住了测试集里又出现了相同商家。解决主键字段一律不进特征。行为特征只保留聚合统计比如用户历史评论数、历史平均星级、评论活跃天数。项目里做一次特征列名审计凡是列名能直接对应到某条评论唯一身份字段的全部去掉。这步听起来简单但很多人因为贪图方便在特征合并阶段就把主键悄悄带进去了。5.4 现象阈值从0.90调到0.95F1直接掉了一半变化毫无规律原因置信度分布不是均匀的0.90到0.95之间的样本量可能突然从几万变成几百伪标签集合整体被替换了模型学到的边界自然完全不同。这不是玄学是样本量断崖带来的正常波动。解决不要盲目试阈值。先跑一次全量无标注预测把置信度保存下来画直方图再在图上确认样本量变化最剧烈的区间。阈值选在“数量还够多、置信度已经明显集中”的位置通常就在这个拐点附近。同时记录每个阈值下伪标签的正样本占比如果0.90时正样本占3%、0.93时占17%这个变化说明类别分布也在随阈值剧变需要谨慎处理。5.5 现象加了半监督反而比纯监督更差伪标签一点忙没帮上原因无标注数据和有标注数据分布差异过大或者种子模型质量太差生成的伪标签大多不可靠。半监督不是万能放大器它在标注质量极低、无标注数据相关性弱的时候会反过来拖累模型。解决先做消融实验办法是生成伪标签后人工抽检50条肉眼看看这些高置信度预测到底靠不靠谱。如果抽检里有一半明显判错先别急着调参回到种子模型训练阶段把epoch加长、或者补充几十条标注。如果伪标签本身看着没问题训练仍不涨点那就检查无标注集合是否整体分布偏移比如包含了大量不同语言的评论这时就需要过滤后再用。半监督适合的是标注少且无标注数据与任务同分布的场景硬上只会得到一个更复杂而且不稳定的模型。6. 收尾技巧用温度缩放与置信度阈值搜索把F1再提一档模型训练结束后直接拿默认的0.5阈值做判决是省事但吃亏的做法。很多高分项目拉开差距的地方其实就是最后这一小步先对验证集做置信度阈值搜索再做温度缩放校准。from sklearn.metrics import precision_recall_curve, f1_score # 验证集预测概率 probs_val model.predict_proba(val_loader)[:, 1] precision, recall, thresholds precision_recall_curve(y_val, probs_val) f1_scores [f1_score(y_val, probs_val t) for t in thresholds] best_idx int(torch.argmax(torch.tensor(f1_scores))) best_threshold thresholds[best_idx] print(fBest threshold: {best_threshold:.3f}, F1: {f1_scores[best_idx]:.4f})这段代码把验证集概率落下来对每一个可能的阈值算一次F1找到最优切分点。实测中这个阈值往往落在0.55到0.75之间而不是默认的0.5。原因很简单模型输出的概率在0.5附近聚集了大量模糊样本把阈值往高调一点丢掉部分真阳但能显著降低误报F1往往会更好。温度缩放是进一步的手段。做法是在验证集上学习一个温度参数T把logits除以T再softmax让输出的概率分布更接近真实置信度。温度缩放不会改变预测类别但会改变置信度排序的分布从而让伪标签阈值和最终判决阈值都更有解释力。校准集和测试集不能重叠否则整体评估会虚高。我自己养成的习惯是每次实验结束都额外抽500条阈值附近样本做人工检查尤其是那些被判错但置信度很高的样本。看多了你会发现机器坚持认为是虚假、人工也说不清的样本往往指向标签噪声而不是模型缺陷。这种对错误样本的体感比任何指标都更能帮你判断半监督策略是收敛还是失控。这条路走通之后再遇到其他类别不均衡任务也能少走很多弯路希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。