☰
联邦学习实战:FedAvg+SMOTE破解信用卡欺诈检测中的非独立同分布与数据不平衡
2026/10/7 6:37:56 网站建设 项目流程

简介:这份资源面向计算机、人工智能等专业的在校学生与联邦学习入门者,提供一套基于FedAvg联邦学习算法与SMOTE过采样优化的信用卡欺诈交易检测完整项目源码。项目通过构建Server与Clients对象模拟真实场景下服务器与节点间的双向参数传递,在保护各银行数据隐私、避免数据集共享的前提下提升欺诈识别效果,适合作为毕设、课程设计或算法进阶练习。压缩包共8个文件,约43.14MB,包含5个Python源码文件、1个说明文档、1张流程示意图和1份信用卡交易数据集,分别对应模型定义、客户端与服务端逻辑、数据处理及运行入口,结构清晰便于按模块阅读。目前已有173人学习。代码均经测试运行成功,答辩评审平均分达96分,读者可据此掌握联邦学习与不平衡数据处理的完整实现思路,并在此基础上修改扩展功能。

1. 联邦信用卡欺诈检测里,FedAvg 加 SMOTE 到底解决了什么

信用卡欺诈检测是典型的小样本极端不平衡二分类任务,正常交易占比常年 99.5% 以上,欺诈样本可能只有千分之几。更麻烦的是,真实业务里交易数据分散在各家银行、各个支付机构手里,谁都不愿意把原始流水交出去——这既是合规红线,也是商业机密。联邦学习(Federated Learning)就是冲着这个矛盾来的:数据不动,模型动。而 FedAvg(Federated Averaging)是目前最主流、最容易复现的联邦聚合算法,没有之一。

但把 FedAvg 直接套到欺诈检测上,会撞上两堵墙。第一堵墙是本地数据本身就不平衡,每个客户端手里的欺诈样本都少得可怜,本地训练几轮模型就退化成「全预测为正常」的摆烂状态。第二堵墙是聚合之后全局模型被多数正常样本主导,少数类的梯度信号在加权平均里被稀释掉。SMOTE(Synthetic Minority Over-sampling Technique)过采样就是用来在本地把少数类「造」出来的,让每个客户端在训练前先补足欺诈样本,再参与 FedAvg 聚合。

这套组合适合谁?适合手里有分散数据、又必须做欺诈识别的团队——银行风控、第三方支付、消费金融,以及做联邦学习课程设计或高分项目的同学。它不解决「数据能不能共享」的合规问题,但能在不共享原始数据的前提下,把不平衡检测的召回率往上抬一截。下面我按「原理选型 → 本地跑通 → 参数调优 → 踩坑排查 → 进阶验证」的顺序,把这条链路讲透。

2. FedAvg 与 SMOTE 的耦合逻辑:为什么不能简单叠加

2.1 FedAvg 的聚合公式与欺诈场景的错位

FedAvg 的核心就一行公式:全局模型参数等于各客户端参数按样本量加权平均。标准写法是 w_global = Σ(n_k / n) · w_k,其中 n_k 是第 k 个客户端的样本数,n 是总样本数。这个加权逻辑在图像分类里没问题,因为各客户端类别分布大致均衡。但放到欺诈检测里,n_k 里 99% 以上是正常交易,加权权重几乎完全由正常样本决定,少数类的更新方向被淹没。

更隐蔽的问题是客户端漂移(client drift)。每个客户端的欺诈模式不一样——有的盗刷集中在境外大额,有的集中在凌晨小额试探。FedAvg 把这些异构的本地模型强行平均,得到的全局模型在任何一个客户端上都不是最优。这就是联邦学习里常说的「非独立同分布(Non-IID)」难题,欺诈场景是 Non-IID 的极端形态。

所以正确的做法不是「先 FedAvg 再 SMOTE」,而是「先 SMOTE 再本地训练,再 FedAvg」。顺序反了,过采样就白做了。

2.2 SMOTE 在本地客户端里的插入位置

SMOTE 的原理是对每个少数类样本,找它的 k 个最近邻少数类样本,在两者连线上随机插值生成新样本。公式是 x_new = x_i + λ · (x_nn - x_i),λ 是 [0,1] 的随机数。它比简单复制样本好在能扩展少数类的决策边界,而不是让模型死记硬背几个固定点。

在联邦场景里,SMOTE 必须放在本地训练之前、数据划分之后。关键约束是:只能在训练集上做 SMOTE,验证集和测试集绝对不能碰。我见过太多人为了「让指标好看」,把 SMOTE 应用到全量数据再划分,结果测试集里混入了合成样本,指标虚高十几个点,上线直接翻车。这是血泪经验,别踩。

还有一个细节:SMOTE 的 k 近邻参数 k_neighbors 不能大于本地少数类样本数减一。如果某个客户端只有 3 个欺诈样本,k_neighbors 设 5 会直接报错。所以要么设一个自适应的 k,要么在客户端样本太少时退化成随机过采样。

2.3 一个可复现的本地 SMOTE 实现

下面这段代码是单个客户端的本地过采样逻辑,用 imbalanced-learn 库实现。注意它只处理训练集,并且对少数类样本过少的客户端做了降级保护。

import numpy as np from imblearn.over_sampling import SMOTE from collections import Counter def local_smote(X_train, y_train, target_ratio=0.3, min_samples=6): """ 本地客户端 SMOTE 过采样 target_ratio: 少数类占多数类的目标比例 min_samples: 少数类样本数低于此值时降级为随机过采样 """ counter = Counter(y_train) minority_count = counter[1] majority_count = counter[0] # 少数类太少,SMOTE 无法找近邻,降级处理 if minority_count < min_samples: # 随机复制到目标数量 need = int(majority_count * target_ratio) - minority_count idx = np.where(y_train == 1)[0] extra_idx = np.random.choice(idx, size=max(need, 0), replace=True) X_train = np.vstack([X_train, X_train[extra_idx]]) y_train = np.concatenate([y_train, np.ones(len(extra_idx))]) return X_train, y_train # k_neighbors 不能超过少数类样本数 - 1 k = min(5, minority_count - 1) smote = SMOTE( sampling_strategy=target_ratio, k_neighbors=k, random_state=42 ) X_res, y_res = smote.fit_resample(X_train, y_train) return X_res, y_res

逻辑说明:函数先统计类别分布,判断少数类是否够 SMOTE 用。target_ratio=0.3表示把欺诈样本补到正常样本的 30%,这个值不是越高越好——补到 1:1 会引入大量合成噪声,反而拉低精确率。min_samples=6是经验阈值,低于 6 个样本时 k 近邻空间太小,合成样本几乎和原样本重合,没有意义。random_state=42保证复现性,联邦实验里每个客户端的随机种子最好固定,否则聚合结果每次都不一样,没法对比。

参数怎么调:target_ratio建议从 0.1 开始试,逐步加到 0.3,观察验证集召回率和精确率的平衡点。k_neighbors默认 5,样本多的时候可以加到 7 或 9,但别超过 10,否则合成样本会过度平滑,丢失局部特征。

3. 从零搭一个 FedAvg + SMOTE 的最小可跑系统

3.1 环境准备与依赖安装

先把环境搭起来。Python 版本建议 3.8 到 3.10,太新的版本有些联邦学习库还没适配。核心依赖就四个:numpy 做数值计算,pandas 读数据,scikit-learn 做模型和评估,imbalanced-learn 做 SMOTE。

# 创建虚拟环境,避免污染全局 python -m venv fed_env source fed_env/bin/activate # Windows 用 fed_env\Scripts\activate # 安装核心依赖 pip install numpy pandas scikit-learn imbalanced-learn

如果你在 Windows 上遇到 imbalanced-learn 编译报错,通常是缺少 C++ 构建工具,直接装预编译 wheel 即可:pip install imbalanced-learn --only-binary :all:。这一步卡住的人不少,别在这浪费时间。

3.2 模拟联邦客户端的数据切分

真实联邦场景拿不到多机构数据,做实验一般用公开的信用卡欺诈数据集(比如 Kaggle 上那个 284807 条记录的 creditcard.csv),然后按 Non-IID 方式切给多个客户端。切分策略直接决定实验难度,我一般用「按时间切 + 按类别倾斜」的组合。

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split def split_federated_clients(df, n_clients=5, fraud_ratio_range=(0.1, 0.5)): """ 把数据切成 n_clients 份,每份欺诈比例不同,模拟 Non-IID fraud_ratio_range: 每个客户端保留的欺诈样本比例范围 """ fraud = df[df['Class'] == 1].reset_index(drop=True) normal = df[df['Class'] == 0].reset_index(drop=True) clients = [] fraud_per_client = len(fraud) // n_clients normal_per_client = len(normal) // n_clients for i in range(n_clients): # 每个客户端拿到的欺诈样本比例不同 ratio = np.random.uniform(*fraud_ratio_range) f_part = fraud.iloc[i*fraud_per_client:(i+1)*fraud_per_client] f_part = f_part.sample(frac=ratio, random_state=i) n_part = normal.iloc[i*normal_per_client:(i+1)*normal_per_client] client_df = pd.concat([f_part, n_part]).sample(frac=1, random_state=i) clients.append(client_df.reset_index(drop=True)) return clients

逻辑说明:先把欺诈和正常样本分开,再按客户端数量均分。fraud_ratio_range控制每个客户端保留多少欺诈样本,模拟不同机构欺诈发生率不同的现实。random_state=i保证每个客户端切分可复现。切完之后每个客户端内部再做 train/test 划分,测试集只用于本地评估,不参与聚合。

参数说明:n_clients=5是实验常用值,太少体现不出联邦的异构性,太多单客户端样本不够。fraud_ratio_range我一般设 (0.1, 0.5),让客户端之间的不平衡程度拉开差距,这样能测出算法对 Non-IID 的鲁棒性。

3.3 FedAvg 聚合主循环

这是整个系统的骨架。每一轮,服务端把全局模型下发给选中的客户端,客户端用本地 SMOTE 后的数据训练若干 epoch,回传参数,服务端加权平均。

import copy import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def fedavg_train(global_model, clients_data, rounds=20, local_epochs=3, lr=0.01): """ FedAvg 主循环 rounds: 联邦通信轮数 local_epochs: 每轮本地训练轮数 """ criterion = nn.BCELoss() history = [] for r in range(rounds): local_weights = [] local_sizes = [] for client_df in clients_data: # 本地数据准备 X = client_df.drop('Class', axis=1).values.astype(np.float32) y = client_df['Class'].values.astype(np.float32) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) # 本地 SMOTE X_tr, y_tr = local_smote(X_tr, y_tr, target_ratio=0.3) # 本地模型从全局模型复制 local_model = copy.deepcopy(global_model) optimizer = torch.optim.SGD(local_model.parameters(), lr=lr) loader = DataLoader( TensorDataset(torch.tensor(X_tr), torch.tensor(y_tr)), batch_size=256, shuffle=True) local_model.train() for _ in range(local_epochs): for xb, yb in loader: optimizer.zero_grad() pred = local_model(xb).squeeze() loss = criterion(pred, yb) loss.backward() optimizer.step() local_weights.append(copy.deepcopy(local_model.state_dict())) local_sizes.append(len(X_tr)) # 加权平均聚合 total = sum(local_sizes) global_dict = global_model.state_dict() for key in global_dict.keys(): global_dict[key] = sum( local_weights[i][key] * (local_sizes[i] / total) for i in range(len(local_weights)) ) global_model.load_state_dict(global_dict) history.append(copy.deepcopy(global_dict)) return global_model, history

逻辑说明:外层rounds是通信轮数,内层local_epochs是本地训练轮数。每个客户端先做 SMOTE 再训练,训练完把state_dict存下来。聚合时按local_sizes(SMOTE 后的样本数)加权,这是 FedAvg 的标准做法。注意copy.deepcopy不能省,否则本地模型和全局模型共享内存,聚合结果会错乱。

参数说明:rounds=20是起步值,欺诈检测通常 15 到 30 轮收敛。local_epochs=3是 FedAvg 原论文推荐的折中值,太大客户端会过拟合本地数据,太小全局收敛慢。lr=0.01配合 SGD,如果用 Adam 可以降到 0.001。batch_size=256在几万条样本量级比较稳。

3.4 模型结构与评估指标

模型不用太复杂,欺诈检测的特征维度通常几十维,三层全连接足够。关键是评估指标不能只看准确率——不平衡数据下准确率 99.8% 的模型可能一个欺诈都没抓到。

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)

评估必须看召回率(Recall)、精确率(Precision)和 AUC-ROC。欺诈检测里召回率优先,漏掉一个欺诈的代价远大于误判一个正常交易。我一般要求召回率不低于 0.85,同时精确率不低于 0.5,否则人工复核成本扛不住。Dropout(0.3)是防过拟合的,联邦场景下客户端数据少,这个不能省。

4. 参数调优与避坑:那些让实验白跑的细节

4.1 数据泄漏与 SMOTE 顺序的三个坑

现象:测试集 AUC 高达 0.99,上线后召回率不到 0.3。原因:SMOTE 应用在了全量数据上再划分训练测试集,合成样本和原样本高度相似,测试集被「污染」。解决:严格先划分再 SMOTE,测试集永远不碰过采样。这是最常见的翻车点,没有之一。

现象:某个客户端训练直接报错ValueError: Expected n_neighbors <= n_samples。原因:该客户端欺诈样本数少于 k_neighbors + 1。解决:用 2.3 节的自适应 k 逻辑,或者对样本过少的客户端直接跳过 SMOTE 改用类别权重。

现象:全局模型在部分客户端上表现极差。原因:SMOTE 后各客户端样本量差异被放大,样本量大的客户端在加权平均里话语权过高。解决:聚合权重不要只用样本量,可以引入客户端可信度或损失倒数做加权,或者对样本量做对数平滑。

4.2 联邦聚合的数值稳定性问题

现象:训练到第 8 轮左右 loss 突然变成 NaN。原因:各客户端回传的梯度量级差异太大,加权平均后数值溢出。解决:本地训练加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),学习率调小,或者改用 FedProx 加一个近端项约束本地模型不要偏离全局太远。

现象:每轮聚合后模型性能不升反降。原因:客户端漂移严重,平均操作把各自学到的特征互相抵消了。解决:减少 local_epochs,增加通信轮数,让全局模型更频繁地「拉回」本地模型。这是联邦学习里灾难性遗忘的一种表现,本地训练太久会忘掉全局知识。

4.3 类别不平衡评估的指标陷阱

现象:准确率 99.9%,但混淆矩阵里欺诈类全是 0。原因:模型学会了「全预测正常」这个偷懒策略,因为这样损失最小。解决:损失函数换成带类别权重的 BCE,或者用 Focal Loss。SMOTE 只是从数据层面缓解,损失层面也要配合。

现象:验证集召回率波动极大,不同随机种子差 20 个点。原因:欺诈样本太少,验证集本身统计不稳定。解决:用分层 K 折交叉验证,报告均值和标准差,别拿单次结果下结论。联邦场景下还要固定所有客户端的随机种子。

提示:SMOTE 的sampling_strategy参数在 imbalanced-learn 不同版本里行为有差异,0.7 版本之前只接受 float,之后支持 dict。跑之前先pip show imbalanced-learn确认版本,别照搬网上的老代码。

5. 进阶:用 FedProx 和动态过采样率把召回率再抬一档

基础版跑通之后,想再往上走,有两个方向值得试。第一个是把 FedAvg 换成 FedProx,在本地损失里加一项 μ/2 · ||w - w_global||²,约束本地模型不要偏离全局太远。μ 一般设 0.01 到 0.1,太大本地学不动,太小等于没加。这个改动对 Non-IID 场景的提升很明显,尤其是客户端欺诈模式差异大的时候。

第二个方向是动态过采样率。固定 target_ratio 的问题是:训练前期模型欠拟合,需要多补样本;训练后期模型已经学到边界,再补反而引入噪声。我的做法是按轮次线性衰减,前 10 轮用 0.4,中间 10 轮降到 0.2,最后 5 轮降到 0.1。实测召回率能再涨 3 到 5 个点,精确率不掉。

def dynamic_ratio(round_idx, total_rounds): """过采样率随轮次衰减""" progress = round_idx / total_rounds if progress < 0.5: return 0.4 elif progress < 0.8: return 0.2 else: return 0.1

验证方法上,别只看最终指标。我习惯每轮记录全局模型在每个客户端测试集上的召回率和 AUC,画成曲线看收敛趋势。如果某个客户端的曲线一直往下掉,说明它被其他客户端带偏了,需要单独排查它的数据分布。另外留一个「从未参与训练的客户端」做泛化测试,这是联邦学习里检验全局模型是否真正学到通用特征的关键手段,比在训练客户端上刷分有意义得多。

最后说个习惯:每次改参数之前先 git commit 当前配置,跑完对比。联邦学习实验的随机性比单机大得多,不记录配置,两周后你根本想不起来哪个结果对应哪组参数。这套东西我踩过的坑基本都写在这了,从数据切分到聚合稳定性,每一步都有翻车的可能。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询