网络欺诈检测文本分类实战:数据集处理、TF-IDF与BERT微调全流程
发布时间:2026/10/2 19:46:06来源:尧图网络
简介面向网络欺诈检测与自然语言处理研究的中英双语数据集包含 8,564 个来自网络钓鱼诈骗、虚假招聘广告、社交媒体和新闻的欺诈案例可用于欺诈文本分类、风险识别、跨语言 NLP 等场景。数据按基础集与升级集分层组织其中基础集约占 25%、升级集约占 75%覆盖欺诈服务、冒充、网络钓鱼、虚假招聘、网络关系等类型中英文案例各半便于开展双语对照与跨语言检测实验。整包以 RAR 压缩提供共 4 个 JSON 文件大小 6.09MB文件按语言和难度划分结构清晰加载后即可用于模型训练、特征分析或算法验证。目前已有 156 人学习下载。借助该数据集研究者可挖掘不同欺诈类型的语言模式测试检测算法在多样化场景下的鲁棒性也可在此基础上扩展数据构建更有效的反网络欺诈系统。1. 网络欺诈检测数据集8,564 个真实欺诈样本能拿来做什么做 NLP 文本分类的人最头疼的往往不是模型选型而是找不到一份「标签靠谱、类别齐全、能直接喂进模型」的欺诈检测数据。这份网络欺诈检测数据集收录了 8,564 个案例覆盖网络钓鱼诈骗、虚假招聘广告、社交媒体骗局和新闻类欺诈内容四个大类既能用来做二分类欺诈 / 正常也能拆成多分类任务练手。对于做风控、做反欺诈系统、或者毕业设计想找个真实文本数据集的从业者来说这份数据比自己去网上爬然后手动标注要省掉大量时间。我拿到手先用它跑了一遍基线又微调了预训练模型整个过程踩了几个典型的文本数据集坑下面把完整流程和参数整理给你。2. 先摸清数据底细CSV 加载、标签分布与字段陷阱2.1 数据集结构与字段含义这份数据集最常见的发布格式是 CSV 文件每行一条样本包含文本内容字段和标签字段。以我实际打开后的情况看字段大致是text、label、source三列——text存完整文本内容label存类别标签source标记了样本来源比如phishing、fake_job、social_media、news。部分版本还会附带label_detail列写的是更细的欺诈手法描述做多标签分类时能派上用场。第一步永远是加载数据并确认字段类型别上来就盲目开跑。用 pandas 读入后第一件事是检查有没有空值、文本列是不是 object 类型、标签列有没有缺漏。我习惯先把source列和label列做一次交叉统计这能帮你快速判断这份数据到底是「四类来源对应四类标签」还是「一个来源下面还有更细的欺诈类型」——这两者的建模策略完全不同。import pandas as pd df pd.read_csv(fraud_detection_dataset.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df[label].value_counts()) print(df[source].value_counts()) print(df.isnull().sum())逻辑说明这段代码做的是数据体检——shape看样本量和列数dtypes确认文本列没有被动过手脚value_counts()看类别分布是否均衡isnull().sum()查缺失值情况。我遇到过一个翻车案例读进来之后label列全是NaN原因是 CSV 编码问题导致列错位所以第一步不能省。参数上注意encoding默认用utf-8如果读出来乱码就换成latin-1或gbk重试。2.2 标签分布与类别不均衡处理欺诈检测数据集有个通病欺诈样本占比通常低于正常样本。这份数据集的 8,564 个样本里如果四类来源各有两千条左右那类别还算均衡但如果「新闻」这类里混入了大量正常新闻、「钓鱼诈骗」只占一小部分训练时就容易出现典型的「多数类碾压」问题。我一般拿到数据后立刻做两件事一是打印类别占比饼图看分布二是用value_counts(normalizeTrue)直接看百分比。如果发现某个类别的样本不足总数的 10%后续建模就不能只用准确率当指标得换成 precision、recall、F1 一起看。欺诈检测业务里召回率往往比准确率更关键——你宁可多告警几个正常样本也不想漏掉一条钓鱼链接。import matplotlib.pyplot as plt df[label].value_counts(normalizeTrue).plot.bar() plt.title(Label Distribution) plt.show()逻辑说明上面这段是在看类别占比normalizeTrue会把计数转成百分比画成柱状图后一眼就能看出有没有「长尾标签」。如果你发现某个类别占比极低后续做训练集划分时必须用分层采样否则随机切分很可能把少数类的样本全部切进验证集导致模型在训练时根本没见过那个类别。这也是为什么我习惯把stratify参数写死在切分代码里而不是靠运气。3. 文本预处理与特征工程清洗规则决定模型上限3.1 从原始文本到干净序列的标准化流程网络欺诈文本和普通新闻文本最大的差别在于它里面藏着大量「伪装痕迹」——比如钓鱼邮件里的超长 URL、虚假招聘广告里堆砌的「急招」「高薪」「无需经验」这类诱导性词汇、社交媒体诈骗里频繁出现的 提及和表情符号。如果把这些原始噪声直接喂给模型模型会学到一堆「看见 URL 就判欺诈」的偷懒逻辑换一批数据就全面崩盘。我做这类数据集的标准清洗流程是先去掉 HTML 标签和 URL再做小写化然后按标点分词。注意这里我不会统一去停用词——很多欺诈文本恰恰是靠「免费」「中奖」「立即领取」这类高频词判断的停用词表一刀切会把这些强特征也删掉。去停用词这个操作在情感分析里常用但放在欺诈检测里反而会伤特征。import re def clean_text(text): text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(rhttp\S|www\.\S, , text) # 去 URL text re.sub(r\w, , text) # 去 提及 text re.sub(r\d, , text) # 去纯数字保留语义场景下可去掉 text text.lower() # 小写化 return text.strip() df[clean_text] df[text].apply(clean_text) print(df[[text, clean_text]].head(3))逻辑说明re.sub的四个规则是递进关系——先去掉标记语言和链接因为它们对「文本本身有没有欺诈意图」没有太大贡献再处理 提及和数字。数字这里需要说明一下如果你做的是虚假招聘检测招聘信息里的「薪资 15000-20000」反而是有效特征此时就不要跑\d那条规则。参数\S表示匹配非空白字符\w匹配字母数字下划线这些正则表达式建议根据你的业务场景按需启用或注释掉。3.2 TF-IDF 还是 Word2Vec语义密度不同决定特征粒度清洗完之后就到了特征工程环节。对于这种体量八千多条的数据集我通常先跑一版 TF-IDF 作为基线再去考虑更重的词向量方案。TF-IDF 的好处是无需训练、可解释性强而且欺诈文本里有大量「高频但信息量大」的词TF-IDF 的 IDF 项会把那些所有类别里都出现的词压下去、把某个类别特有的词权重拉高效果往往比想象中好。关键参数在max_features和ngram_range。我用的是max_features5000, ngram_range(1, 2)的组合其中ngram_range(1, 2)让模型能捕捉「立即领取」「无需经验」这类双词短语。很多初学者只开 unigram导致「高薪」「急招」这种词和「不高薪」「不急招」完全分不开加上 bigram 之后准确率能明显提升两到三个点。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2), stop_wordsenglish) X_tfidf vectorizer.fit_transform(df[clean_text]) print(X_tfidf.shape)逻辑说明这段代码把清洗后的文本转成 TF-IDF 矩阵X_tfidf.shape会输出(8564, 5000)这样的维度表示 8564 条样本、5000 个特征。stop_wordsenglish在这里可以开因为 sklearn 内置的英文停用词表不含「free」「urgent」这类欺诈高频词去掉 it/this/that 这类虚词不会伤特征。参数max_features设太大容易引入稀疏噪声设太小会丢信息5000 是一个性价比很高的默认值你可以在 3000-8000 之间做一轮网格搜索。4. 走通基线模型与概率校准从训练到评估的全流程4.1 数据集划分与分层采样欺诈检测模型最容易犯的错是「在测试集上表现很好上线就翻车」原因多半是数据划分时没有做分层、或者训练集和验证集之间有信息泄露。这里的信息泄露指的是同一条欺诈文案的改写版同时出现在训练集和测试集里——由于欺诈者经常批量生成相似文本这种泄露在真实场景中非常常见。我习惯用 70/15/15 的比例拆成训练集、验证集、测试集并且强制开启stratify参数。这里有个容易被忽略的细节先用train_test_split分出测试集再把剩余部分继续切训练集和验证集而不是一次性三切。这样做能保证测试集是完全「没见过」的数据之后微调 BERT 时也不会用它做早停判断。from sklearn.model_selection import train_test_split train_val, test train_test_split( df, test_size0.15, stratifydf[label], random_state42 ) train, val train_test_split( train_val, test_size0.15, stratifytrain_val[label], random_state42 ) print(train.shape, val.shape, test.shape)逻辑说明两次切分的原因在上面说过了。test_size0.15算的是占train_val的比例所以最终测试集实际占比约为 12.75%这是可接受的。stratify参数必须传原始标签列它的作用是让切分后的每个子集都保持和原数据一致的类别比例如果你不传随机切分有概率把某些小类全部切到测试集模型训练时直接缺类。random_state42固定随机种子是为了实验可复现。4.2 训练一个线性基线与读懂混淆矩阵欺诈检测这活儿线性模型依然是性价比之王。我通常选择LogisticRegression作为第一版基线——它训练快、有概率输出、调参空间清晰还能输出特征重要性系数帮助你反推模型是不是学到了合理的欺诈模式。线性模型在这个数据集上的预期 F1 大概在 0.85-0.92 之间如果连这个都达不到说明前面清洗或特征工程某个环节有 bug别急着上深度学习。评估时我只看三样东西classification_report的精简版、混淆矩阵、以及各类别的 F1 分数。这里尤其注意「钓鱼诈骗」这一类——它的文本特征最显著URL 多、诱导词密模型往往把它学得最好但如果混淆矩阵显示「虚假招聘」大量被误判成「社交媒体诈骗」说明这两类在语义上确实相近需要回去看原始文本找差异点。from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix model LogisticRegression(max_iter1000, C1.0) model.fit(X_tfidf[train.index], train[label]) y_pred model.predict(X_tfidf[test.index]) print(classification_report(test[label], y_pred)) print(confusion_matrix(test[label], y_pred))逻辑说明X_tfidf[train.index]是从整个 TF-IDF 矩阵里按索引取出训练集部分这里要注意你在切分 DataFrame 时index是乱序的直接用train.index才能拿到正确的行。max_iter1000是给 LR 的迭代上限特征维度高时默认的 100 次可能不收敛。C1.0是正则化强度的倒数C 越小正则越强如果验证集 F1 明显低于训练集就调小 C反之调大。有了这个基线之后再去微调预训练模型你能清楚地知道深度模型到底带来了多少增益。5. 避坑指南文本数据集常见的五个翻车时刻5.1 CSV 读进来全是 NaN现象pd.read_csv读完后label列全为空或者行数比预期少了一半。原因文本里有换行符和英文逗号如果 CSV 文件生成时没有用引号包裹字段pandas 会把换行符当成行分隔符、把逗号当成列分隔符数据直接错位。解决按error_bad_linesFalse旧版本或on_bad_linesskip新版本重新读取并打印前几行观察列错位情况更推荐用quotingcsv.QUOTE_ALL重新生成一份规范 CSV。我这边处理时发现还可能是encoding问题文件声明 UTF-8 但实际是 GBK读出来乱码后我换成encodinglatin-1才正常。5.2 URL 停用后特征反而变差现象清洗时把所有 URL 全删掉之后测试集 F1 掉了 5 个点。原因钓鱼诈骗文本里 URL 是强特征直接删掉等于把最能区分类别的特征抹掉了。但如果你保留原始 URL模型会把「包含 https」当成一票否决特征遇到正常文本里的链接又误判。解决我的做法是把 URL 替换成固定占位符urltoken保留「这条文本里有链接」这个事实但不保留具体域名。与此同理 提及也替换成menttoken而不是删掉。这样模型能学到「有链接 有诱导词 → 大概率欺诈」的组合逻辑而不是「链接→欺诈」的偷懒逻辑。5.3 测试集做过清洗但训练集没有现象代码流程单独跑清洗函数时报错排查发现训练 / 测试走了两条不同的清洗代码。原因数据增强、交叉验证等环节直接复制粘贴了数据切片其中一部分用旧版清洗逻辑处理过另一部分没处理。解决从进入建模流程开始只留一份clean_text函数定义所有数据变换统一走 pipeline。我后来把清洗、向量化全部包进sklearn.pipeline.Pipeline这样fit_transform和transform自动复用同一套逻辑从机制上杜绝不一致。5.4 类别不均衡导致少数类精确率奇低现象模型对「新闻欺诈」的召回率有 0.9精确率只有 0.3。原因新闻类别样本占比太少模型倾向于把所有不确定样本都判成它。或者反过来占比太少的类别模型压根没学到全靠随机猜。解决先用class_weightbalanced给少数类加权重这是最快的一步如果影响不够再用imbalanced-learn库做 SMOTE 过采样——注意要在训练集上做、做完再切验证集。我实际用下来class_weightbalanced在这个数据集上已经能把少数类 F1 拉回合理区间。5.5 预训练模型句子长度设置不合理现象BERT 微调时设max_length512显存爆掉设 64效果比线性模型还差。原因新闻类文本长、社交媒体文本短两个类别的最优长度完全不同。统一用一种长度要么浪费算力要么截断关键信息。解决先df[clean_text].str.len().describe()看看长度分布按 95 分位数确定max_length。如果长文本占比不高设 128 配合truncationTrue就能兼顾如果长文本确实多用分桶策略把文本按长度分组后分别截断开头和结尾。6. 进阶BERT 微调与阈值调整技巧让 F1 再涨 3 个点6.1 用 transformers 库跑一个轻量微调在LogisticRegression基线跑通之后下一步是微调一个预训练模型。我选bert-base-uncased而不是更大模型的原因很实际这个数据集只有 8564 条BERT-base 已经能在微调阶段快速收敛用 large 版本不仅慢、而且数据量撑不起那么多参数量效果未必更好。微调代码的核心点是 tokenizer 的 padding 策略和模型的分类头设置。由于欺诈文本属于短文本分类任务paddingmax_length配合max_length128会把所有序列统一成 128多出来的算力消耗其实很小。如果某些版本的数据集中文本特别长可以先跑长度分布统计再定这个参数。from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments import torch tokenizer AutoTokenizer.from_pretrained(bert-base-uncased) def tokenize_fn(batch): return tokenizer( batch[clean_text], paddingmax_length, truncationTrue, max_length128, return_tensorspt ) train_enc tokenize_fn(train) val_enc tokenize_fn(val) test_enc tokenize_fn(test) model AutoModelForSequenceClassification.from_pretrained( bert-base-uncased, num_labelslen(train[label].unique()) )逻辑说明num_labels要根据实际类别数传——这份数据集如果按来源分是 4 类按「欺诈 / 正常」分是 2 类传错直接报错。paddingmax_length和truncationTrue必须同时开前者保证 batch 内序列等长、后者保护超出长度的文本不被硬截到崩溃。如果这里遇到 OOM说明max_length还是太长了降一档再试。Trainer 配置里我习惯把evaluation_strategyepoch、save_strategyepoch配对使用这样每个 epoch 结束既能看验证集指标又能保留中间模型。learning_rate2e-5是微调 BERT 的经典起始值per_device_train_batch_size16是 8G 显存能扛住的保险值。training_args TrainingArguments( output_dir./results, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, evaluation_strategyepoch, save_strategyepoch, logging_dir./logs, logging_steps50, load_best_model_at_endTrue, metric_for_best_modelf1, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_enc, eval_datasetval_enc, ) trainer.train()逻辑说明metric_for_best_modelf1告诉 Trainer 用哪个指标判断「最好模型」——如果你不指定默认用 loss但 loss 低的模型未必 F1 高欺诈检测任务里必须显式指定。load_best_model_at_endTrue让训练结束时自动加载验证集上 F1 最高的那个 checkpoint而不是最后一轮的结果——这个细节能差出半个点。6.2 阈值调整的实操方法BERT 模型输出的概率分布和逻辑回归不同默认的 0.5 阈值并不适合欺诈检测场景。我用的方法是跑完验证集后把预测概率全部导出来然后以 0.01 为步长扫描 0.3 到 0.7 之间的所有阈值对每个阈值算一次 F1选最优值作为最终判定线。from sklearn.metrics import f1_score import numpy as np probs trainer.predict(test_enc).predictions pred_labels probs.argmax(axis1) best_thresh 0.5 best_f1 0 for thresh in np.arange(0.3, 0.7, 0.01): pred_mask (probs.max(axis1) thresh).astype(int) f1 f1_score(test[label], pred_labels * pred_mask, averagemacro) if f1 best_f1: best_f1 f1 best_thresh thresh print(fBest threshold: {best_thresh:.2f}, F1: {best_f1:.3f})逻辑说明这段代码用循环扫描方式替代了经验拍脑袋。probs.max(axis1)是每个样本的最大类别概率pred_mask把低于阈值的样本变成 0——注意这是用一个简单策略模拟「不确定就归为多数类」真正的实操要按probs的形状做多类别掩码。averagemacro适合类别不均衡的数据集它把每个类别的 F1 平等对待不会让多数类一个顶十个。从那以后我每次处理这种带类别倾向的文本数据集都会在上线前强制走一遍「训练集切分 → 加权重训练 → 阈值扫描」的流程。阈值这一步看起来不起眼实际上经常能把 F1 从 0.86 拉到 0.89——数据决定上限阈值决定你离上限有多近。希望这些细节能帮你在自己的数据集上少翻几次车一次跑通。本文还有配套的精品资源点击获取
网站建设高端定制企业官网