
简介这份资源面向计算机、人工智能及相关专业的学生与开发者提供一套基于FedAvg联邦学习算法与SMOTE过采样优化的联邦信用卡欺诈交易检测完整项目源码。项目通过构建Server与Clients对象模拟真实场景下服务器与节点间的双向参数传递在保护各银行数据隐私、避免数据集共享的前提下提升欺诈检测效果适合作为毕业设计、课程设计或算法进阶练习。压缩包共8个文件约43.14MB包含5个Python源码文件、1个Markdown说明文档、1张流程示意图和1份信用卡交易数据集分别对应模型定义、服务端与客户端逻辑、数据处理及项目说明等模块。目前已有173人学习下载。代码经过完整测试运行读者可据此理解联邦学习参数聚合流程、SMOTE类别不平衡处理思路以及整体工程结构并可在现有基础上修改扩展用于毕设、课设或项目初期立项演示。1. 联邦信用卡欺诈检测为什么FedAvg加SMOTE值得你花时间信用卡欺诈检测是典型的高不平衡二分类任务正常交易占比动辄 99.5% 以上欺诈样本可能只有千分之几。更棘手的是真实场景下交易数据分散在多家银行或支付机构手里出于合规和商业竞争谁都不愿意把原始流水汇总到一处训练。联邦学习正好切中这个痛点各方只交换模型参数不交换数据。但联邦场景下非独立同分布Non-IID和极端类别不平衡会同时放大——有的节点欺诈样本多有的几乎全是正常交易全局模型很容易被多数类带偏。FedAvg 负责聚合各节点梯度SMOTE 负责在本地把少数类过采样到合理比例两者组合是目前工业界落地联邦风控最常见的一条路径。这篇笔记面向有 Python 基础、想把这套方案跑通并调稳的工程师从数据划分、本地过采样、聚合策略一路讲到踩坑排查代码可直接复现。2. FedAvg 与 SMOTE 的协作机制先搞清楚谁在什么时候起作用2.1 FedAvg 的聚合逻辑与信用卡场景的适配点FedAvgFederated Averaging的核心思想很朴素每个参与方在本地用自己数据跑若干轮 SGD把更新后的模型参数上传到中心服务器服务器按样本量加权平均再把全局模型下发。公式上就是 $w_{t1} \sum_{k1}^{K} \frac{n_k}{n} w_{t1}^k$其中 $n_k$ 是第 k 个节点的样本数。放到信用卡欺诈检测里这个加权方式有个隐含假设样本多的节点贡献大。但欺诈检测中样本多的节点往往是正常交易多欺诈占比反而低直接按总量加权会让全局模型更偏向多数类。常见做法是改成按欺诈样本量加权或者对每个节点的损失做类别加权后再聚合。我一般会在聚合前先统计各节点的正负样本比如果差异超过一个数量级就手动调整权重。另一个适配点是通信轮次。信用卡交易数据量大但欺诈模式变化快联邦训练不能像图像分类那样跑几百轮。实测下来20 到 50 轮全局聚合通常就能收敛再多了收益递减还容易过拟合。每轮本地 epoch 控制在 1 到 3 之间太多会导致本地模型偏离全局太远聚合时震荡。2.2 SMOTE 在联邦节点上的正确打开方式SMOTESynthetic Minority Over-sampling Technique通过在少数类样本之间做线性插值生成合成样本。标准流程是对每个少数类样本找它的 k 个最近邻少数类样本随机选一个在两者连线上随机取一点作为新样本。放到联邦场景关键问题是SMOTE 只能在本地做不能跨节点生成合成样本否则就泄露了其他节点的数据分布。本地执行时我一般把欺诈样本过采样到占总样本的 10% 到 30% 之间。太低起不到平衡作用太高会引入大量合成噪声模型在真实欺诈上的召回反而下降。k 近邻数默认 5如果本地欺诈样本少于 50 条建议降到 3 甚至 2否则近邻本身就不具代表性。还有一个细节SMOTE 之前一定要先做特征标准化因为它是基于欧氏距离找近邻的量纲不统一会让距离计算完全失真。from imblearn.over_sampling import SMOTE from sklearn.preprocessing import StandardScaler import numpy as np def local_smote(X, y, target_ratio0.2, k_neighbors5): 本地节点执行SMOTE过采样 X: 特征矩阵 (n_samples, n_features) y: 标签 (n_samples,) target_ratio: 少数类目标占比 k_neighbors: SMOTE近邻数 scaler StandardScaler() X_scaled scaler.fit_transform(X) n_majority np.sum(y 0) n_minority_target int(n_majority * target_ratio / (1 - target_ratio)) n_minority_current np.sum(y 1) if n_minority_current n_minority_target: return X_scaled, y, scaler # 动态调整k值防止少数类样本过少时报错 actual_k min(k_neighbors, n_minority_current - 1) if actual_k 1: return X_scaled, y, scaler smote SMOTE( sampling_strategy{1: n_minority_target}, k_neighborsactual_k, random_state42 ) X_res, y_res smote.fit_resample(X_scaled, y) return X_res, y_res, scaler这段代码做了三件事先标准化保证距离计算合理再根据多数类数量反推少数类目标数量最后动态调整 k 值防止样本过少崩溃。注意sampling_strategy用的是字典形式只对标签 1 过采样不动多数类。返回的 scaler 要保存下来推理时对新数据做同样的变换。2.3 联邦训练主循环的代码骨架把 FedAvg 和本地 SMOTE 串起来主循环大致长这样import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class FraudNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1), nn.Sigmoid() ) def forward(self, x): return self.net(x) def federated_train(clients, global_rounds30, local_epochs2, lr0.001): clients: 各节点的 (X, y) 列表 input_dim clients[0][0].shape[1] global_model FraudNet(input_dim) global_weights global_model.state_dict() for round_idx in range(global_rounds): local_weights_list [] local_sizes [] for X_local, y_local in clients: # 本地SMOTE X_res, y_res, _ local_smote(X_local, y_local) # 本地训练 local_model FraudNet(input_dim) local_model.load_state_dict(global_weights) optimizer torch.optim.Adam(local_model.parameters(), lrlr) criterion nn.BCELoss() dataset TensorDataset( torch.FloatTensor(X_res), torch.FloatTensor(y_res).unsqueeze(1) ) loader DataLoader(dataset, batch_size64, shuffleTrue) local_model.train() for epoch in range(local_epochs): for batch_x, batch_y in loader: optimizer.zero_grad() pred local_model(batch_x) loss criterion(pred, batch_y) loss.backward() optimizer.step() local_weights_list.append(local_model.state_dict()) local_sizes.append(len(X_res)) # FedAvg聚合按本地过采样后的样本量加权 total sum(local_sizes) new_weights {} for key in global_weights.keys(): new_weights[key] sum( local_weights_list[i][key] * (local_sizes[i] / total) for i in range(len(local_weights_list)) ) global_weights new_weights global_model.load_state_dict(global_weights) return global_model聚合时用的是过采样后的样本量做权重这样欺诈样本多的节点话语权更大缓解了 Non-IID 下的偏置。local_epochs设 2 是折中设 1 收敛慢设 5 以上本地模型容易跑偏。学习率 0.001 配 Adam 是比较稳的起点如果 loss 震荡就降到 0.0005。3. 从原始交易流水到联邦节点数据预处理与划分实操3.1 特征工程里最容易被忽略的四个字段信用卡欺诈检测的公开数据集比如 Kaggle 上的 creditcard.csv通常有 30 个左右特征其中 V1 到 V28 是 PCA 降维后的匿名特征Time 是交易时间戳Amount 是金额。实际落地时我一般会额外构造几个字段交易时间的小时数欺诈在凌晨 2 到 5 点明显高发、金额的对数值原始金额长尾太严重、以及该卡在过去 1 小时内的交易频次。这三个衍生特征在联邦场景下也能本地计算不涉及跨节点数据。Time 字段要转成小时和星期几两个维度直接丢原始秒数没有意义。Amount 做 log1p 变换后再标准化。V 系列特征本身已经做过 PCA不需要再处理但要注意不同节点的 V 特征分布可能有偏移联邦训练前最好各节点独立做一次标准化而不是用全局统计量。3.2 非独立同分布划分模拟真实银行间的数据差异做实验时不能把数据随机切几份就当联邦节点那样是 IID 的体现不出 FedAvg 的真实难度。常见做法是按时间切分每个节点取不同时间段的交易这样欺诈模式随时间漂移节点间分布自然不同。另一种是按金额分桶有的节点专做小额高频有的做大额低频。我一般两种都试时间切分更接近真实场景。import pandas as pd import numpy as np def split_non_iid(df, n_clients5, strategytime): 非独立同分布划分 strategy: time 按时间分段, amount 按金额分桶 df df.sort_values(Time).reset_index(dropTrue) if strategy time: chunk_size len(df) // n_clients clients [] for i in range(n_clients): start i * chunk_size end (i 1) * chunk_size if i n_clients - 1 else len(df) clients.append(df.iloc[start:end]) return clients elif strategy amount: df[amount_bin] pd.qcut(df[Amount], n_clients, labelsFalse) clients [df[df[amount_bin] i].drop(amount_bin, axis1) for i in range(n_clients)] return clients时间切分后每个节点的欺诈率可能从 0.1% 到 1% 不等这种差异正是 FedAvg 需要扛住的。划分完要检查每个节点的正负样本比如果某个节点欺诈样本少于 20 条SMOTE 的 k 值要相应调小或者考虑在该节点上不做过采样、只参与聚合。3.3 本地验证集不能省联邦场景下的评估陷阱很多联邦学习代码只留训练集靠全局模型在中心服务器上的测试集评估。但中心服务器没有数据真实场景下你只能在各节点本地评估。我一般每个节点留 20% 做本地验证训练时用 SMOTE 后的数据验证时用原始未过采样的数据这样评估指标才反映真实分布。评估指标别只看准确率不平衡场景下准确率没有意义。重点看 AUC-ROC、召回率Recall和 F1。欺诈检测里召回率通常比精确率重要漏掉一笔欺诈的代价远大于误拦一笔正常交易。如果业务上误拦成本也高就调阈值让精确率和召回率平衡一般阈值设在 0.3 到 0.5 之间不是默认的 0.5。4. 避坑与排查联邦信用卡欺诈检测的五个血泪教训4.1 现象全局模型 AUC 比单节点本地训练还低原因FedAvg 按总样本量加权正常交易多的节点主导了聚合方向欺诈特征被稀释。解决改成按欺诈样本量加权或者对每个节点的损失函数加类别权重pos_weight让少数类梯度更大。实测按欺诈样本量加权后 AUC 能回升 3 到 5 个百分点。4.2 现象SMOTE 之后模型在验证集上召回率暴跌原因合成样本在特征空间里侵入了多数类区域模型学到了假的决策边界。解决SMOTE 之前先做特征选择把区分度低的 V 特征去掉过采样比例从 0.2 降到 0.1 试试或者改用 Borderline-SMOTE只在边界附近的少数类样本上生成。我一般会画一下过采样前后少数类样本的 t-SNE 分布肉眼确认合成样本没有跑到多数类堆里。4.3 现象训练到第 10 轮左右 loss 突然变成 NaN原因各节点标准化方式不一致聚合后的权重对应了不同尺度的特征前向传播数值爆炸。解决统一标准化流程所有节点用相同的StandardScaler逻辑各自 fit 但变换方式一致或者在聚合后对权重做裁剪torch.nn.utils.clip_grad_norm_。学习率也要检查联邦场景下 0.001 以上容易炸。4.4 现象某些节点训练几轮后直接掉线全局模型性能波动大原因节点本地欺诈样本太少SMOTE 生成的合成样本质量差本地模型输出噪声大聚合时拖累全局。解决设一个参与门槛本地欺诈样本少于 30 条的节点只做推理不参与聚合或者对这些节点降低聚合权重。另外可以加一个异常检测某节点上传的权重与全局权重余弦相似度低于 0.5 就丢弃该轮更新。4.5 现象本地验证 AUC 很高但上线后实际欺诈召回率很低原因验证集和训练集来自同一时间段没有体现时间漂移或者 SMOTE 只在训练时做了验证集没做导致评估偏乐观。解决验证集按时间往后切用未来数据评估当前模型评估时用原始分布不要对验证集过采样上线后持续监控召回率低于阈值就触发重新训练。这个坑我在实际项目里踩过离线 AUC 0.95上线召回只有 0.6后来加了时间切分验证才暴露出来。5. 进阶技巧让 FedAvg 加 SMOTE 在真实风控里跑得更稳5.1 用焦点损失替代交叉熵缓解过采样后的噪声敏感SMOTE 生成的合成样本不可避免带有噪声标准 BCE Loss 对噪声敏感。焦点损失Focal Loss通过调制因子 $(1-p_t)^\gamma$ 降低易分类样本的权重让模型聚焦在难样本上。在联邦场景下每个节点本地用 Focal Loss聚合时依然按样本量加权。实测 $\gamma2$ 时合成噪声带来的假阳性明显减少。class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): bce nn.functional.binary_cross_entropy(pred, target, reductionnone) pt torch.exp(-bce) focal self.alpha * (1 - pt) ** self.gamma * bce return focal.mean()alpha控制正负样本权重欺诈检测里设 0.25 到 0.5 之间gamma控制难易样本的聚焦程度2.0 是常用值。替换掉主循环里的BCELoss即可其他流程不变。5.2 动态调整过采样比例根据每轮全局召回率反馈固定过采样比例在训练初期有效但后期可能过拟合。我一般会加一个反馈机制每轮聚合后在各节点本地验证集上算召回率如果召回率连续 3 轮不升就把下一轮的 SMOTE 目标比例降 0.05最低降到 0.05。这样训练后期合成样本减少模型更多依赖真实欺诈样本泛化更好。5.3 通信压缩让联邦训练在真实网络下可行每轮上传完整模型参数在节点多、模型大时通信开销很高。常见做法是只上传梯度变化最大的 top-k 参数或者做量化float32 转 int8。信用卡欺诈检测模型通常不大几万参数压缩收益有限但如果节点数超过 50 个建议至少做一轮 top-50% 稀疏化。注意压缩后聚合要对应还原否则权重对不上。技巧适用场景预期收益Focal Loss合成样本噪声大假阳性降 10%-20%动态过采样比例训练后期过拟合召回率稳中有升top-k 稀疏化节点数 50通信量降 50%按欺诈样本量加权节点间不平衡严重AUC 升 3-5 个点这些技巧不用全上按你的节点规模和数据分布选两三个就行。我自己的习惯是先把基础 FedAvg 加 SMOTE 跑通看到 baseline 指标后再逐个加每加一个记录一次验证集 AUC 和召回涨了就留没涨就撤。联邦学习调参比单机训练玄学得多同样的配置换个随机种子结果可能差两个点所以每次实验至少跑三遍取平均。希望帮到你。本文还有配套的精品资源点击获取