资讯详情

资讯详情

半监督学习实战:Yelp数据集虚假评论检测与伪标签调参

简介这份资源面向人工智能、数据挖掘方向的课程设计与期末大作业需求者提供一套基于半监督学习的虚假评论检测完整源码方案以Yelp数据集为实验对象帮助读者理解在标注样本有限条件下如何结合少量标签与大量未标注评论完成分类建模。压缩包共14个文件约7.16MB以png可视化图表、Python主程序、Shell运行脚本、csv数据集与md说明文档为主涵盖数据分布、混淆矩阵、模型对比等实验产出便于快速部署与结果复现。项目代码附有注释新手也能看懂适合作为课程设计、毕业设计或高分大作业参考。目前已有250人学习下载读者可从中获得半监督分类的完整实现思路、随机森林与朴素贝叶斯等基线对比结果以及数据欠采样处理与模型调优的实践参考具备较高的实际应用与学习价值。1. 虚假评论检测遇上半监督学习Yelp 数据集上为什么标注少反而更接近实战电商和本地生活平台每天要处理海量评论真正被人工标注过的欺诈样本往往不到千分之一。你手上如果只有几百条标注数据却要覆盖几十万条评论的检测需求纯监督模型几乎必然过拟合。虚假评论检测这个方向之所以在人工智能项目里反复出现核心痛点不是模型不够深而是标注成本压不下来。半监督学习恰好卡在这个位置上用少量标注样本定方向用大量无标注样本学分布。Yelp 数据集是公开评论数据里少有的带标注欺诈标签的集合适合拿来跑通整套流程。这篇笔记面向想复现一个能写进简历、能讲清楚技术选型的从业者从数据加载一路写到伪标签迭代和阈值调参中间踩过的坑都会标出来。2. 半监督学习做虚假评论检测一致性正则和伪标签到底选哪个2.1 虚假评论检测的任务定义与 Yelp 数据分布虚假评论检测通常被建模为二分类问题给定一条评论文本及其元信息评分、评论数、用户历史等判断它是否为欺诈评论。Yelp 数据集在学术圈常用的版本包含约 60 万条评论其中标注为欺诈的过滤后大约 13% 左右。注意这个比例在不同子集里波动很大有的子集欺诈占比不到 5%直接按 0.5 阈值切分会导致召回率极低。文本侧的特征工程常见做法是 TF-IDF 加 n-gram配合评论长度、感叹号密度、全大写词比例等手工特征。元信息侧则用用户注册时长、评论频率、评分偏差等。半监督学习在这里的价值是你不需要把 60 万条全标完只需要标注 2000 到 5000 条高质量样本剩下的交给算法去利用无标注数据的结构。提示Yelp 数据集原始文件是 JSON 行格式每行一个 review 对象字段包括 review_id、user_id、business_id、stars、text、date 等。欺诈标签在单独的标注文件里需要按 review_id 做 join。2.2 一致性正则与伪标签的适用边界半监督学习在虚假评论检测上有两条主流路线。第一条是伪标签Pseudo-Labeling先用标注数据训练一个教师模型对无标注数据预测高置信度样本把预测标签当作真实标签加入训练集迭代几轮。第二条是一致性正则Consistency Regularization对同一条无标注样本施加不同扰动如 dropout、数据增强要求模型输出一致典型代表是 FixMatch 和 UDA。选哪条我的经验是标注数据少于 1000 条时一致性正则更稳因为它不依赖教师模型的初始预测质量标注数据在 2000 到 5000 条时伪标签加置信度阈值筛选的性价比更高实现简单且容易调。虚假评论检测的文本扰动不好做——同义词替换可能改变欺诈意图回译又引入噪声所以一致性正则的数据增强策略要谨慎设计。下面是一个伪标签迭代的最小实现框架基于 PyTorch 和 HuggingFace 的文本分类流程import torch import torch.nn as nn from torch.utils.data import DataLoader from transformers import AutoTokenizer, AutoModelForSequenceClassification # 教师模型和学生模型共用同一架构 def build_model(num_labels2): model AutoModelForSequenceClassification.from_pretrained( bert-base-uncased, num_labelsnum_labels ) return model # 伪标签生成只取置信度高于阈值的样本 def generate_pseudo_labels(model, unlabeled_loader, threshold0.95, devicecuda): model.eval() pseudo_samples [] with torch.no_grad(): for batch in unlabeled_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) logits model(input_ids, attention_maskattention_mask).logits probs torch.softmax(logits, dim-1) max_probs, preds probs.max(dim-1) # 只保留高置信度样本 mask max_probs threshold for i in range(len(mask)): if mask[i]: pseudo_samples.append({ input_ids: input_ids[i].cpu(), attention_mask: attention_mask[i].cpu(), label: preds[i].cpu() }) return pseudo_samples这段代码的逻辑是教师模型对无标注数据推理只保留 softmax 最大概率超过阈值的样本作为伪标签样本。threshold是关键参数设太低会引入噪声标签设太高则伪标签数量不够。我一般从 0.95 起步如果伪标签数量少于无标注数据的 10%降到 0.9 再试。device根据你的显卡调整CPU 上跑 BERT 推理会非常慢建议至少用一张 8G 显存的卡。2.3 从零搭建半监督训练循环的四个步骤第一步准备标注数据和无标注数据。标注数据按 8:1:1 切分训练、验证、测试。无标注数据就是去掉标签的那部分注意不要把测试集混进去。第二步用标注数据训练教师模型。学习率设 2e-5batch size 16训练 3 到 5 个 epoch在验证集上早停。这一步不需要太多技巧关键是得到一个比随机猜好得多的初始模型。第三步生成伪标签并合并训练集。把伪标签样本和原始标注样本按 1:1 到 3:1 的比例混合重新训练学生模型。混合比例太高会让模型被噪声带偏太低则半监督效果不明显。第四步迭代。用学生模型重新生成伪标签再训练下一轮。通常 2 到 3 轮后收益递减超过 5 轮容易过拟合到伪标签噪声上。# 合并标注样本和伪标签样本 def combine_datasets(labeled_dataset, pseudo_samples, pseudo_ratio2.0): # pseudo_ratio 控制伪标签样本相对标注样本的倍数 max_pseudo int(len(labeled_dataset) * pseudo_ratio) selected_pseudo pseudo_samples[:max_pseudo] combined labeled_dataset selected_pseudo return combined # 训练循环骨架 def train_semi_supervised(model, combined_loader, val_loader, epochs3, lr2e-5): optimizer torch.optim.AdamW(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() for epoch in range(epochs): model.train() for batch in combined_loader: input_ids batch[input_ids].cuda() attention_mask batch[attention_mask].cuda() labels batch[label].cuda() logits model(input_ids, attention_maskattention_mask).logits loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估 evaluate(model, val_loader)pseudo_ratio这个参数我一般设 2.0也就是伪标签样本是标注样本的两倍。如果你的标注数据只有 500 条伪标签样本控制在 1000 条左右比较安全。lr在第二轮迭代时可以降到 1e-5避免破坏已经学到的特征。3. Yelp 数据集预处理从 JSON 到模型可读的四个转换坑3.1 字段对齐与标签泄漏排查Yelp 的评论 JSON 和欺诈标注文件是分开的join 的时候最容易出两个问题。第一个是 review_id 大小写不一致有的版本里标注文件用了大写评论文件用了小写直接 merge 会丢数据。第二个是重复 review_id同一个用户对同一商家多次评论会产生重复记录需要去重后再 join。import pandas as pd import json # 读取 JSON 行格式的评论数据 def load_reviews(path): records [] with open(path, r, encodingutf-8) as f: for line in f: records.append(json.loads(line)) return pd.DataFrame(records) # 读取欺诈标注 def load_labels(path): return pd.read_csv(path) reviews load_reviews(yelp_reviews.json) labels load_labels(yelp_labels.csv) # 统一 review_id 格式 reviews[review_id] reviews[review_id].str.strip().str.lower() labels[review_id] labels[review_id].str.strip().str.lower() # 去重后合并 reviews reviews.drop_duplicates(subsetreview_id) merged reviews.merge(labels, onreview_id, howinner) print(f合并后样本数: {len(merged)})howinner保证只保留有标签的样本。如果你要做半监督需要把how改成left这样无标签样本的 label 列为 NaN后续单独处理。合并后一定要检查正负样本比例如果欺诈样本少于 5%考虑用加权损失或者过采样。3.2 文本清洗的边界什么时候不该去掉标点虚假评论的一个显著特征是过度使用感叹号和全大写词。如果你在清洗阶段把所有标点都去掉、把所有字母转小写等于把最有区分度的信号扔了。我的做法是保留原始文本作为 BERT 的输入另外构造一份清洗后的文本用于 TF-IDF 特征。清洗时只做三件事——去除 HTML 标签、合并连续空格、截断超长文本到 512 个 token。import re def clean_text(text): # 去除 HTML 标签 text re.sub(r[^], , text) # 合并连续空白字符 text re.sub(r\s, , text).strip() return text def truncate_by_token(tokenizer, text, max_len512): tokens tokenizer.encode(text, truncationTrue, max_lengthmax_len) return tokenizer.decode(tokens, skip_special_tokensTrue) merged[clean_text] merged[text].apply(clean_text)max_len512是 BERT 的硬限制超过会被截断。Yelp 上不少评论超过 1000 词截断会丢失尾部信息。如果欺诈信号集中在评论末尾可以考虑用 Longformer 或者分段池化但那是另一个话题了。3.3 无标注数据的构造与采样策略半监督学习需要无标注数据。Yelp 里没有欺诈标签的评论就是天然的无标注池。但直接全用有两个问题一是数量太大训练时间不可控二是分布可能和标注数据偏差太大。我一般从无标注池里随机采样标注数据 5 到 10 倍的数量保证每个 epoch 不会跑太久。# 分离标注和无标注数据 labeled merged[merged[label].notna()].copy() unlabeled merged[merged[label].isna()].copy() # 采样无标注数据 sample_size min(len(unlabeled), len(labeled) * 8) unlabeled_sampled unlabeled.sample(nsample_size, random_state42) print(f标注样本: {len(labeled)}, 无标注采样: {len(unlabeled_sampled)})random_state42保证每次采样一致方便复现。如果你的标注数据只有几百条采样倍数可以拉到 10 倍但要注意无标注数据里的噪声也会被放大。4. 避坑与排查半监督虚假评论检测翻车的五个血泪教训现象一伪标签准确率越迭代越低。原因置信度阈值设太低教师模型的错误预测被当成真标签错误在迭代中累积放大。解决每轮迭代后在验证集上评估伪标签的准确率如果低于 85%提高阈值或停止迭代。我一般会在第一轮用 0.95第二轮用 0.97。现象二模型在测试集上 F1 很高但实际部署后误报率爆炸。原因Yelp 数据集的欺诈标注标准和你实际业务的标准不一致。学术数据集里的欺诈定义可能只覆盖刷好评不覆盖恶意差评。解决在自己的业务数据上重新标注 200 条做校准不要直接拿 Yelp 上训好的模型上线。现象三训练 loss 正常下降但验证集 F1 卡在 0.6 上不去。原因文本特征被过度清洗标点和大写信号丢失。解决检查清洗流程保留原始文本作为主输入清洗文本只用于辅助特征。另外检查类别权重欺诈样本少的时候 CrossEntropyLoss 要加 weight 参数。现象四无标注数据里混入了测试集样本。原因切分数据时先合并再切分导致测试集泄漏到无标注池。解决先切分标注数据为 train/val/test再从剩余数据里取无标注样本。测试集的 review_id 要单独存一份训练全程不碰。现象五GPU 显存不够batch size 只能设 4。原因BERT 加 512 token 长度对显存压力大。解决用梯度累积模拟大 batch或者换 DistilBERT 把参数量降一半。如果还不行把 max_len 降到 256大部分评论的前 256 个 token 已经包含足够信息。5. 把半监督虚假评论检测推到可交付阈值调优与验证集构造的一个技巧伪标签的置信度阈值是这套方案里最玄学的参数。设 0.95 可能只剩 3% 的无标注样本能用设 0.8 又引入太多噪声。我后来固定用一个做法不设全局阈值而是按类别分别设。欺诈类的阈值设高一点0.97正常类的阈值设低一点0.9。原因是教师模型对正常样本的预测更可靠对欺诈样本容易漏判高阈值可以过滤掉那些模棱两可的欺诈预测。验证集的构造也有讲究。半监督场景下验证集必须完全来自标注数据而且要和训练集的标注样本不重叠。如果标注数据只有 2000 条验证集至少留 200 条否则 F1 的波动会大到无法判断模型好坏。我习惯用分层采样保证验证集里欺诈比例和整体一致。from sklearn.model_selection import train_test_split # 分层切分标注数据 train_texts, val_texts, train_labels, val_labels train_test_split( labeled[clean_text].tolist(), labeled[label].tolist(), test_size0.1, stratifylabeled[label], random_state42 ) print(f训练集: {len(train_texts)}, 验证集: {len(val_texts)}) print(f验证集欺诈比例: {sum(val_labels) / len(val_labels):.3f})stratify参数保证切分后正负样本比例一致。如果你的欺诈样本只有几十条验证集里可能只有几条欺诈样本F1 的置信区间会非常宽。这种情况下建议用交叉验证代替单次切分取 5 折的平均 F1 作为评估指标。最后一轮迭代完成后不要急着导出模型。先在验证集上画一条阈值-F1 曲线找到 F1 最高点对应的阈值把这个阈值固化到推理代码里。推理时输出的概率超过这个阈值才判为欺诈否则判为正常。这个阈值在不同业务场景下要重新调没有万能值。我自己在这个方向上最大的教训是半监督学习不是银弹它只能帮你把标注成本从 10000 条降到 2000 条降不到 200 条。如果标注数据少于 500 条先想办法标数据别急着上复杂算法。另外 Yelp 数据集上的高分不代表实际业务效果跨域迁移时 F1 掉 20 个点是常事做好心理准备。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →