说实话,拿到一份脱敏的反欺诈数据集时,大多数人的第一反应是直接跑二分类模型——XGBoost、逻辑回归、随机森林,一顿操作猛如虎,最后看AUC也能勉强交差。但真做过风控建模的人都知道,这类数据集有一种让人非常头疼的特性:正样本(欺诈交易)稀少得可怜,往往只有总量的千分之几,而且标注质量参差不齐,很多“坏”样本还是事后几个月才被确认的。这时候监督学习就会变得非常被动。我在处理手头这个模拟项目X的反欺诈脱敏数据集时,换了一条思路,用Autoencoder做无监督异常检测,不依赖欺诈标签的完备性,只学习“正常交易长什么样”,然后用重构误差去衡量一条样本偏离正常模式的程度。整个过程走下来,效果比我预想的要好,踩坑也不少。这篇文章就是一次完整的项目复盘,从数据预处理、网络结构选型、训练细节,到阈值划定、评估指标选择,再到几个特别容易让人怀疑人生的细节坑,尽量把能交代的都交代清楚。适合刚接触异常检测、或者正在被极度不平衡的欺诈标签折磨的读者。
1. 欺诈检测为什么天然适合Autoencoder这种无监督方案
1.1 监督学习在欺诈场景的三个死穴
先说结论:在反欺诈领域,监督学习不是不能用,但它有三个绕不开的问题。
第一个问题是标签稀疏且不可信。欺诈标签通常来源于用户投诉、银行拒付、内部稽核,每一环都有滞后性。一份训练数据里标记为“正常”的样本,很可能只是“尚未被发现异常”而已,里面混着未爆雷的欺诈交易。你用这种标签训练监督模型,模型会主动去拟合这些“错误答案”,即使训练集很大,学出来的边界也是错位的。
第二个问题是正负样本极端不平衡。我手里这份数据,欺诈标签占比大约是0.13%,也就是一万条里只有13条左右。监督模型在这种情况下很容易掉进“全预测正常也能获得极高准确率”的陷阱。你可以通过欠采样、过采样、调整class weight勉强压住,但整体上模型对“少数类”的刻画能力依然有限,因为压根没有足够的坏样本让它去学“欺诈是什么”。
第三个问题是概念漂移。欺诈手法是活的,今天用伪卡,明天可能就换成了团伙盗号后的批量试探交易。一套监督模型上线后,面对没见过的新型欺诈模式,泛化能力往往很差。你不可能每出现一种新手法就重新标注一大批样本重新训练,那太慢了。
这三个问题叠加在一起,注定了纯监督路线在反欺诈场景里是“戴着镣铐跳舞”。所以我把目光投向无监督异常检测:不学习“欺诈长什么样”,而是学习“正常长什么样”,一切偏离正常的都视为可疑。这是欺诈检测最务实的切入口。
1.2 Autoencoder的“压缩-还原”逻辑
Autoencoder做的事情,本质上就是把一条样本压到低维空间,再从低维空间还原出来。训练目标是最小化还原误差(重构误差)。你输入一条交易记录,经过编码器压缩成几个数值,再经解码器拉回原始维度,如果这条交易与训练集中的“主流模式”高度一致,还原自然很顺畅,误差就小;如果它是一段从来没见过的行为模式,模型很难用几个隐变量把它表达清楚,还原出来就会走样,误差就大。
这个逻辑用类比非常好懂:想象你要把一张人脸照片压缩成50个字节的摘要,再根据摘要还原照片。假如你只见过亚洲人的面孔,压缩-还原算法就会在“亚洲人脸”这个流形上表现很好;突然递给你一张欧洲人的照片,压缩到同样50字节再还原,出来的效果大概率是“看起来像亚洲人但五官哪儿都不对”——因为它没有足够的维度去记录陌生的结构。异常检测的判断依据,就是还原后“哪儿都不对”的程度。
用数学一点的话说,Autoencoder在训练中学习的是正常数据的低维流形结构。欺诈交易落在流形之外或者流形的稀疏边缘,重构误差自然偏大。这个方案有三个很实在的优势:
- 不需要大量标注,正常样本到处都是,取一部分干净的就行
- 天然适应概念漂移,你只需要定期用新确认的正常样本微调模型
- 检测到的异常是“偏离正常”的样本,不限定于某几类已知欺诈手法
1.3 和PCA这类线性方法的比较
聊到降维重构,很多人会想到PCA。PCA其实是Autoencoder的一个特例——单层、线性激活函数的Autoencoder,学到的潜空间就是主成分空间。但PCA只能捕捉线性相关性,而交易数据里大量的交互模式是非线性的,比如“深夜大额+陌生商户类别+极短时间间隔+频繁交易”这种组合式异常。Autoencoder通过多层非线性映射,能抓住特征之间的高阶交互。
当然,非线性带来的是更高的过拟合风险和更差的可解释性,后面我会讲怎么用结构设计和正则化来压制它。先把定位说清楚:PCA是我的baseline,Autoencoder是主力方案。
2. 动手前的数据观察:三个脏数据问题不处理,后面全是白做
2.1 数据字段构成与缺失值处理
这份脱敏数据集涉及交易的常规字段,我做了一下归类:
| 类别 | 字段示例 | 特征类型 |
|---|---|---|
| 基础属性 | 交易金额、交易时间、商户类型编码 | 连续/离散混合 |
| 账户维度 | 卡号脱敏ID、设备指纹、IP区域代码 | 离散ID类 |
| 行为特征 | 该卡当日累计交易次数、历史平均金额、距上一笔交易间隔 | 连续 |
| 标签 | 是否欺诈(事后确认) | 二值,极度不平衡 |
先看缺失值。原始数据里设备指纹缺失率大约12%,IP区域代码缺失率接近8%。这时候最容易犯的错是全局填充:所有缺失字段统一填0或填均值。我第一版就这么干的,结果模型把“缺失”这个状态本身学成了一种正常模式,或者干脆学成了一种强异常信号,导致后续阈值飘忽不定。
我最后的处理方式分三类:连续字段的缺失,先按卡号ID分组计算该卡均值,再用于填充组内缺失,如果整卡都缺失则标记为-1,并为此专门生成一列“是否缺失”的布尔特征;离散ID字段的缺失单独编码成“未知”类别;金额这种核心字段坚决不做均值填充,宁可保留缺失标志位。
这里要注意的一点是:缺失值处理方式本身会影响模型的业务含义。比如设备指纹缺失,在真实场景里往往意味着高风险(用户拒绝授权设备信息),所以“缺失”这个状态值得模型学习,但要给模型一个明确的信号,而不是混淆成一团。
2.2 标准化方法:异常检测场景下别急着用StandardScaler
标准化是异常检测项目里最容易翻车的一步。常见做法是StandardScaler,把每个特征变成均值0、方差1。这在分类场景没毛病,但在重构误差类模型里有个隐含风险:极端值被压缩得太平了。举个例子,正常交易金额的均值可能是320元,标准差260,一笔欺诈交易金额9000元,标准化后大约是33倍的z-score,看上去很大,但经过映射函数之后,网络只需要很小的权重扰动就能大致还原它;反观在MinMaxScaler下,9000元会直接顶到1.0,形成一个明确的上界信号,模型想还原它必须把该维度的激活值推到极限,重构误差更容易被放大。
所以我在这个项目里使用了RobustScaler,它按中位数和四分位距缩放,对离群点不那么敏感,但又不会像StandardScaler那样把异常压扁。
实操上的硬性纪律:标准化器只允许在训练集(正常样本)上fit,然后同参数transform训练集和测试集。这个问题我在第6章的踩坑清单里还会重点讲,很多公开教程在这里写得不够仔细。
2.3 按实体分组切分数据,让评估结果不作弊
这一点我认为是整个项目里性价比最高的一步:切分训练集和验证集时,必须按卡号ID进行分组切割,严禁随机行级切分。原因不复杂——同一张卡的多笔交易之间高度相关,模式高度相似。如果随机切分,模型在训练时已经见过这张卡的历史交易,验证时遇到同一张卡的新交易,会表现得很“聪明”,重构误差当然小,评估指标被系统性抬高。等模型上线遇到全新卡片时,表现立刻崩塌。
我按照卡号ID做分层分组,确保同一张卡的所有交易只出现在训练集或验证集其中之一,同时尽量保证训练集和验证集中卡号数量比例接近8:2。这一步做完,验证集上实际效果比随机切分下降了不少,但那才是真实水平。
3. 网络结构设计:信息瓶颈的宽度、深度与激活函数怎么配合
3.1 具体网络结构与参数选择
我最终采用的Autoencoder结构如下,输入特征维度是52(原始数值特征+衍生特征+缺失标志位拼接后):
以输入维度52重建:
- 编码器:52 → 32 → 16 → 8 → 6(latent,也就是信息瓶颈)
- 解码器:6 → 8 → 16 → 32 → 52
- 激活函数:编码器和解码器的隐藏层用SELU,输出层不用激活函数
- Dropout:在latent向量之前加0.1的Dropout,其余位置不加
- 损失函数:MSE
这个结构不是拍脑袋定的,有几个设计上的考虑值得展开说。
3.2 信息瓶颈的维度:为什么latent取6而不是取2或取20
latent维度过小,信息瓶颈太窄,正常样本的表达能力都不够,重构误差整体抬高,正常和异常的重构误差分布会粘连在一起,阈值很难切;latent维度过大,模型开始“记忆”而非“泛化”,连欺诈样本也能被较好还原,异常信号彻底消失。
我用了特征总数的1/8到1/16这个经验区间来做初始值。52维特征,取6到7维是合理区间。我测过latent=4和latent=12两个对照组:latent=4时,正常样本MSE中位数从0.0038抬到0.0061,正常异常分布重叠面积明显变大;latent=12时,欺诈样本的MSE中位数从0.012降到了0.007,区分度直接砍半。6是一个相对好的平衡点。
3.3 激活函数与输出层的坑
隐藏层使用SELU而不是ReLU,是因为SELU在自归一化属性下可以缓解内部协变量偏移,在中小型网络里表现稳定;ReLU在全连接小网络里容易出现神经元死亡,尤其是编码器压缩层,一旦死掉,latent维度实际可用的不足一半。
输出层不激活,直接输出线性结果——这是MSE重构的基本前提。有人习惯性在输出层加sigmoid,把输出压到0到1之间,但这个数据集的RobustScaler输出范围并不被限制在0~1,sigmoid会把重构误差的区分度抹掉,模型只能输出0到1之间的值,异常样本的重构误差被系统性压低。我一开始在这个坑上浪费了大半天,后面到第6章统一复盘。
Dropout只加在latent之前,是为了给信息瓶颈再增加一点点随机扰动,促使编码器学到更鲁棒的表征,而不是死记硬背某几条样本。不要在整个网络到处加Dropout,这种几十层的矮胖网络加多了会严重影响重构精度。
4. 训练策略与误差分布:别只盯着loss曲线,要看误差分得开不开
4.1 训练集净化与训练细节
我取训练集时只使用标签为“正常”的样本,砍掉所有欺诈样本和标签模糊样本。训练集大约12万条正常交易。但这里有一个隐患:前面说过,正常标签本身不准确,里面可能混着尚未爆雷的欺诈样本。直接拿去训练,异常样本会污染正常分布。
我的做法是两轮训练:第一轮先用全部“正常”样本训练一个初版模型,对这批样本算重构误差,把MSE最高的0.5%样本剔除(这批样本大概率是潜伏的欺诈),再用净化后的样本重新训练最终模型。两轮训练跑下来,最终验证集的召回率大约提升了3到4个百分点,代价只是多跑一次训练,非常划算。
优化器选择Adam,初始学习率1e-3,batch size 256,epoch上限60,带上early stopping,patience设8,监控验证集上的MSE均值。
4.2 判断训练是否有效的关键:看正常与异常的分布分离度
这是整个项目最重要的一张图:把验证集所有样本过一遍模型,算出每条样本的MSE,然后分开画正常样本和欺诈样本的直方图或核密度图。
在理想情况下,正常样本的MSE集中在一个很小的区间,比如0.003到0.006附近;欺诈样本的MSE呈现长尾,中位数可能是0.01以上,而且两者重叠区域越少越好。
我这次的结果是:正常样本MSE中位数0.0048,95%分位数0.0071;欺诈样本MSE中位数0.0134,但分布很宽,有少量跟正常样本重叠。这说明模型确实学到了正常模式的主体结构,但有一部分欺诈交易行为模式与正常交易高度相似——比如小额、高频、时间分散的薅羊毛型欺诈,它就是个“小坏”,跟正常交易的边界天然模糊。
这时候不要急着调网络,先看分布再做下一步决策。我当初年轻的时候只会盯着loss曲线,看得再平再低也没有用,样本间的相对距离才是异常检测的唯一判据。
4.3 MSE与MAE的取舍
默认用MSE,理由很直接:MSE是平方误差,对偏离较大的异常样本施加了平方惩罚,重构误差更容易拉大,异常信号更强。缺点是对个别极端离群点过于敏感,正常样本里如果混入了极端值,MSE分布会被拉宽。
如果数据里正常样本的极端离群点比较多,可以换成MAE,它对离群的容忍度更高,但换来的代价是异常样本的MSE区别度也会变小。我两个都试了,MAE的ROC-AUC比MSE低了约0.02,所以最后保留了MSE。
5. 阈值划定与评估指标:准确率在这种场景里就是骗自己
5.1 为什么不能看准确率
如果99.87%的数据是正常样本,模型什么都不学,全部判正常,准确率也有99.87%。这个数字会让很多人产生错觉,以为模型已经天下无敌。实际上它对所有欺诈交易毫无识别能力。
哪怕你看了AUC,也要留个心眼:ROC曲线在极度不平衡的数据集上会过度乐观,因为假阳性率的分母太大,随便一点误报在巨大的正常样本基数下都显得微不足道,ROC会被“数量优势”稀释。风控领域更常看的是PR曲线和top-K命中率,因为我们要的是“捞出来的名单里有多少是真正的问题”,而不是“把所有负样本正确排除的能力”。
5.2 三套阈值划定方法
我给三条路径,按业务条件选:
方法A、分位数法:取训练集正常样本MSE的95%分位数或者99%分位数作为阈值,简单快速,适合做冷启动baseline。缺点是完全没有考虑业务端对误报和漏报的真实代价。
方法B、业务成本权衡法:误报(FN)的成本是一次人工核查,假设50元/单;漏报(FP)的成本是这笔欺诈交易的实际损失,假设平均每单损失800元。把验证集所有样本排序,遍历每一个可能的阈值,计算综合成本cost = FN数量 × 50 + FP数量 × 800,取成本最低点作为阈值。这种方法最贴合实际,但需要业务方给出比较靠谱的单损和核查单价。
方法C、F-beta最大点:在验证集上按MSE排序,遍历阈值,画出PR曲线,找到F1或F-beta最大的位置。如果你更在意召回率(宁可多误报也要抓住欺诈),可以设beta=2。
我这次因为业务方给得出核查单价,用的是方法B,最后阈值落在0.0065附近。
5.3 一份简化的验证集混淆矩阵
为了直观,我把验证集数据压到一个简化场景:正常样本9990条,欺诈样本10条。阈值取0.0065时,模型结果如下:
| 指标 | 数值 |
|---|---|
| 验证集正常样本 | 9990 |
| 验证集欺诈样本 | 10 |
| 抓到欺诈 | 8 |
| 漏掉欺诈 | 2 |
| 误报(正常判异常) | 37 |
| 召回率 | 80% |
| 精确率 | 约17.8%(8/45) |
| F1 | 约0.29 |
这个表看着刺眼,精确率只有18%,但风控业务里这个数字完全可以接受——人工核查团队只需要多处理37单,就能把10条欺诈里的8条揪出来。如果看top-K命中率,在分值最高的前50条里命中8条,命中率16%,远高于基准概率0.13%。
这就是为什么反欺诈评估一定要看PR曲线和top-K,而不是盯着准确率或AUC自我感动。
6. 五个特别容易翻车的实操细节
6.1 标准化泄漏:fit只能接触训练数据
StandardScaler或RobustScaler必须在训练集上fit,然后transform训练集、验证集和测试集。不要贪图省事,在合并后的全量数据上fit。否则,未来数据的分布信息会提前“泄漏”给模型,尤其金额类特征,整个数据集的均值可能被少数欺诈大额拉高,导致正常样本被压缩到更窄的区间,训练和评估全部失真。
6.2 没有按实体分组切分,评估虚高
这章前面详细讲过,只补充一个判断方法:如果你发现验证集上欺诈样本的MSE低得离谱,第一反应不要高兴,先查切分逻辑。我见过有人复现项目,验证集AUC高达0.98,狂喜三天,后来发现同一张卡散落在训练集和验证集,改成按卡号分组后AUC掉到0.87。0.87才是真实水平。
6.3 缺失值统一填充的连锁反应
统一填充弊端前面也讲过,这里聊一个更隐蔽的连锁反应:如果某个字段缺失率很高,比如设备指纹12%缺失,统一填充成0,而真实欺诈交易中设备指纹缺失比例远高于正常交易(欺诈分子往往拿不到真实设备信息),那么“设备指纹=0”这个特征就会成为最强的欺诈信号,模型的重构误差基本被这个假特征主导,其他行为特征全部被淹没。模型学到的不是“欺诈行为偏离正常”,而是“有一列特征经常等于0”。这种模型上线后,只要欺诈分子开始伪装设备信息,效果直接归零。
6.4 训练样本净化这步容易被跳过
很多人拿着“正常标签”就直接训练,白白浪费了两轮训练方案。我强烈建议加入净化步骤:训练一轮后,把训练集重构误差最高的0.3%到0.5%样本剔除,重新训练。这比任何数据增强技巧都便宜有效,而且能温和地纠正标签噪声。
6.5 输出层激活函数别乱加
这个坑我再强调一次:Autoencoder的最终输出层必须和损失函数匹配。损失函数是MSE,输出层就不要接激活函数(除非特征做了sigmoid标准化,但一般不会这么做)。我在项目里见过有人给输出层加了tanh,损失函数改成MSE,结果所有重构误差都被压在一段极窄的范围内,正常和异常样本的MSE中位数只差0.002,直接报废。想用有界激活函数,就得把标准化改成对应的取值域,一般不值得。
7. 这套方案的边界与后续可扩展方向
Autoencoder做反欺诈,能解决“正常行为模式的学习与偏离度量”这个问题,但它不是银弹。如果欺诈和正常行为在特征空间里高度重合,比如小额高频刷单,这种和正常用户行为几乎无异的数据,Autoencoder也很难区分。它的价值在于快速缩小可疑范围,而不是一步到位给出判决结果。
如果后续要扩展,可以考虑在latent向量上接一个轻量分类头做半监督;或者用变分自编码器(VAE)把重构误差换成重建概率,这在有更多业务先验时会更稳健;再往后可以把时间维度的序列特征加进来,比如用LSTM-Autoencoder刻画卡号ID的交易时序模式。这些都是在现有框架上的自然升级。
最后分享一点个人心得:这类项目真正难的地方从来不是模型结构,而是数据切分、标准化方式、特征衍生这些看起来平淡无奇的细节。把基础打牢,Autoencoder这个老牌方法在反欺诈场景里的效果,依然够用且稳定。