简介:这份资源是面向计算机、人工智能、自动化等专业学生与从业者的深度学习实战项目包,以个人贷款违约预测为主题,可用于课程设计、大作业或毕业设计参考。项目代码经过调试测试,注释详尽,并附有运行教程文档,适合零基础学习者上手,也便于进阶者在此基础上修改扩展。压缩包共28个文件,约16.37MB,包含csv数据集、py源码、ipynb笔记本、xml配置、xlsx商业取数逻辑表及md运行说明等,覆盖数据读取、预处理、可视化分析与特征编码等完整流程,其中数据构建脚本与多个练习脚本可帮助理解贷款违约预测的数据处理逻辑。目前已有673人学习下载,资源提供了从数据到模型构建的完整方案,配合详细注释与教程,能有效降低深度学习项目入门门槛,适合需要快速搭建预测系统原型的读者参考借鉴。
1. 个人贷款违约预测系统:从一份 Python 源码到能跑通的模型
拿到「基于深度学习的个人贷款违约预测系统设计与实现python源码」这个标题,多数人第一反应是去搜一份能直接跑的代码,然后卡在环境配置、数据字段对不上、模型跑出来 AUC 只有 0.5 这几步上。这个方向真正要解决的问题很具体:给定一批贷款申请人的结构化信息(收入、负债、历史还款、贷款金额等),预测这个人未来会不会违约,输出一个概率值,再按业务阈值切成「通过/拒绝/人工复核」。它适合两类人:一类是课程设计或毕设需要完整跑通一套流程的学生,另一类是信贷风控方向想用深度学习替代逻辑回归基线的一线从业者。下面这套东西,我按「数据怎么处理、模型怎么搭、参数怎么调、坑在哪」的顺序讲清楚,你照着能复现,也能判断这套方案值不值得往生产环境推。
2. 违约预测的数据底座:字段、清洗与不平衡处理
深度学习在结构化信贷数据上能不能打赢 XGBoost,八成取决于数据这一层,而不是网络结构。个人贷款违约数据集通常是「宽表 + 标签」的形态,一行一个申请人,一列一个特征,最后一列是是否违约(0/1)。常见公开数据集的特征维度在 10 到 30 之间,样本量从几千到几十万不等。真正动手前,先把字段分成四类,分类方式直接决定后面怎么编码。
2.1 字段分四类,编码方式各不同
连续数值型(年龄、月收入、负债收入比、贷款金额、利率)直接标准化即可;有序类别(学历、职级)用序号编码保留大小关系;无序类别(职业类型、贷款用途、担保方式)必须做独热或嵌入;还有一类是「看似数值实则类别」的,比如地区编码、邮编,直接当数值喂进去是典型的翻车点,模型会学出「邮编越大越容易违约」这种毫无业务意义的规律。
我一般会先跑一遍字段画像,把每列的取值个数、缺失率、分布偏度打出来,再决定编码。取值个数小于 15 的当类别处理,大于 15 且业务上连续的当数值处理,介于两者之间的看业务含义。
import pandas as pd import numpy as np def profile_columns(df, target_col): """输出每列的类型建议、缺失率、唯一值数""" rows = [] for col in df.columns: if col == target_col: continue nunique = df[col].nunique(dropna=True) missing = df[col].isna().mean() # 取值少且非浮点,倾向类别;取值多且是数值,倾向连续 if nunique <= 15: suggest = "category" elif pd.api.types.is_numeric_dtype(df[col]): suggest = "numeric" else: suggest = "high_card_category" rows.append({ "column": col, "dtype": str(df[col].dtype), "nunique": nunique, "missing_rate": round(missing, 4), "suggest": suggest }) return pd.DataFrame(rows).sort_values("missing_rate", ascending=False) # 用法:profile = profile_columns(df, target_col="default_flag")这段代码的逻辑是先看唯一值数量,再看数据类型,最后给出编码建议。nunique <= 15这个阈值不是死的,业务上如果某个类别字段有 20 个取值但分布极度集中,也可以当类别处理。missing_rate排序是为了让你优先处理缺失严重的列,缺失率超过 60% 的字段,除非业务上极其关键,否则直接删掉比费劲填补更划算。
2.2 缺失值填补:别用全局均值一刀切
信贷数据的缺失往往不是随机的。收入字段缺失的人,可能是无固定职业者,本身违约率就偏高。用全局均值填补会把这个信号抹平。我的做法是:数值型按目标变量的分组中位数填补,类别型单独设一个「缺失」类别。这样缺失本身成了一个特征。
def fill_missing(df, num_cols, cat_cols, target_col): df = df.copy() for col in num_cols: # 按标签分组的中位数填补,保留缺失与违约的关联 median_map = df.groupby(target_col)[col].median() df[col] = df.apply( lambda r: median_map[r[target_col]] if pd.isna(r[col]) else r[col], axis=1 ) for col in cat_cols: df[col] = df[col].fillna("MISSING").astype(str) return dfgroupby(target_col)[col].median()得到的是「违约人群的收入中位数」和「正常人群的收入中位数」,各自填补对应群体。这里有个数据泄漏的隐患:如果是在训练集上算中位数再应用到验证集,验证集的填补逻辑必须复用训练集的映射,不能重新算。生产环境里这一步要固化成一个fill_params.json,训练时存下来,推理时加载。
2.3 类别不平衡:过采样、欠采样还是改损失函数
违约样本通常只占 5% 到 20%,极端情况低于 2%。这时候模型全预测「不违约」也能有 95% 以上的准确率,但业务上毫无价值。处理方式有三条路:SMOTE 过采样、随机欠采样、以及直接用带类别权重的损失函数。我的经验是,样本量大于 5 万时优先用类别权重,小于 1 万时用 SMOTE,欠采样只在正负样本都极其充足时考虑。
from sklearn.utils.class_weight import compute_class_weight import numpy as np def get_class_weight(y): classes = np.unique(y) weights = compute_class_weight( class_weight="balanced", classes=classes, y=y ) return dict(zip(classes, weights)) # 训练时传给模型的 class_weight 参数 # 例如违约样本占 8%,则违约类权重约为 6.25,正常类约为 0.54compute_class_weight的balanced模式按n_samples / (n_classes * np.bincount(y))计算,样本越少的类权重越高。这个权重直接乘在交叉熵损失上,让模型对少数类的错误更敏感。注意权重不是越大越好,如果违约类权重设到 20 以上,模型会变得极度保守,把大量正常客户也判成违约,业务上同样不可接受。一般控制在 10 以内,配合后面的阈值调优一起用。
3. 深度学习模型怎么搭:从 MLP 到嵌入层的结构化网络
结构化数据的深度学习模型,核心就两件事:类别特征怎么变成低维稠密向量,数值特征怎么和这些向量拼接后送进全连接层。别一上来就上 Transformer,在几千到几十万样本的表格数据上,一个设计合理的 MLP 加嵌入层,效果和调参成本都更可控。
3.1 嵌入层维度怎么定:经验公式与业务约束
类别特征的嵌入维度,常见经验是min(50, (n_categories + 1) // 2),其中n_categories是该字段的取值个数。但这个公式在高基数类别上会给出过大的维度,比如一个 1000 个取值的职业编码,按公式会得到 50 维,参数量爆炸。我一般会加一个上限:嵌入维度不超过 16,且不超过n_categories的平方根。
import torch import torch.nn as nn class LoanDefaultNet(nn.Module): def __init__(self, cat_dims, num_dim, hidden_dims=(256, 128, 64), dropout=0.3): """ cat_dims: list of int, 每个类别特征的取值个数 num_dim: int, 数值特征数量 """ super().__init__() # 为每个类别特征建一个嵌入表 self.embeddings = nn.ModuleList([ nn.Embedding(n_cat, min(16, int(n_cat ** 0.5) + 1)) for n_cat in cat_dims ]) emb_total = sum(min(16, int(n ** 0.5) + 1) for n in cat_dims) input_dim = emb_total + num_dim layers = [] prev = input_dim for h in hidden_dims: layers += [ nn.Linear(prev, h), nn.BatchNorm1d(h), nn.ReLU(), nn.Dropout(dropout) ] prev = h layers.append(nn.Linear(prev, 1)) self.mlp = nn.Sequential(*layers) def forward(self, x_cat, x_num): # x_cat: (batch, n_cat_features) 长整型 embeds = [emb(x_cat[:, i]) for i, emb in enumerate(self.embeddings)] x = torch.cat(embeds + [x_num], dim=1) return self.mlp(x).squeeze(-1)这段网络的定义里,nn.Embedding为每个类别字段维护一张查找表,输入是类别索引,输出是稠密向量。BatchNorm1d放在线性层之后、激活之前,能显著加快收敛,尤其在数值特征量纲差异大的时候。Dropout设 0.3 是结构化数据的常用值,超过 0.5 容易欠拟合。输出层是单个神经元,配合BCEWithLogitsLoss使用,不要在模型里加 Sigmoid,损失函数内部会做。
3.2 训练循环:早停、学习率与验证指标
训练循环里最容易出问题的是验证指标的选择。违约预测是不平衡二分类,准确率没有参考价值,要看 AUC 和 KS。KS 在风控里比 AUC 更常用,它衡量的是好坏样本累计分布的最大差值,直接对应业务上能区分出多少违约客户。
from sklearn.metrics import roc_auc_score, roc_curve import numpy as np def ks_score(y_true, y_prob): fpr, tpr, _ = roc_curve(y_true, y_prob) return max(tpr - fpr) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for x_cat, x_num, y in loader: x_cat, x_num, y = x_cat.to(device), x_num.to(device), y.to(device) optimizer.zero_grad() logits = model(x_cat, x_num) loss = criterion(logits, y) loss.backward() # 梯度裁剪,防止嵌入层梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() total_loss += loss.item() * len(y) return total_loss / len(loader.dataset) # 验证阶段 def evaluate(model, loader, device): model.eval() probs, labels = [], [] with torch.no_grad(): for x_cat, x_num, y in loader: x_cat, x_num = x_cat.to(device), x_num.to(device) logits = model(x_cat, x_num) probs.extend(torch.sigmoid(logits).cpu().numpy()) labels.extend(y.numpy()) auc = roc_auc_score(labels, probs) ks = ks_score(labels, probs) return auc, ksclip_grad_norm_的max_norm=5.0是经验值,嵌入层参数量大,梯度容易在早期爆炸,裁剪后训练稳定很多。早停的耐心值设 5 到 10 个 epoch,监控指标用验证集 KS,KS 连续 5 轮不提升就停。学习率用 1e-3 配 Adam,如果 loss 震荡明显,降到 3e-4。batch size 在 256 到 1024 之间,太小训练慢,太大泛化差。
3.3 数值特征标准化:树模型不用,神经网络必须做
神经网络对输入尺度敏感,数值特征不做标准化,收入这种量级上万的字段会直接主导梯度。标准化要在训练集上算均值和标准差,然后应用到验证集和测试集。注意:如果用了按标签分组填补缺失,标准化要在填补之后做。
from sklearn.preprocessing import StandardScaler import joblib scaler = StandardScaler() # 只在训练集上 fit X_num_train = scaler.fit_transform(X_num_train) X_num_val = scaler.transform(X_num_val) X_num_test = scaler.transform(X_num_test) # 保存 scaler 供推理使用 joblib.dump(scaler, "num_scaler.pkl")fit_transform和transform的区别是血泪教训:如果验证集也调fit_transform,均值和标准差会包含验证集信息,造成数据泄漏,验证指标虚高,上线后直接翻车。生产推理时加载num_scaler.pkl,用transform处理新样本。
4. 避坑与排查:跑不通、指标假高、上线崩的典型场景
这一章列的每一条都是我或身边人真实踩过的,按「现象 → 原因 → 解决」写,你对照自己的情况排查。
4.1 训练 loss 不降,AUC 卡在 0.5
现象:模型训练几十个 epoch,loss 在 0.69 附近震荡(二分类随机水平),验证 AUC 约 0.5。原因通常是标签列没对齐,或者类别特征索引从 1 开始而嵌入层期望从 0 开始。解决:先检查y的取值是不是只有 0 和 1,再检查类别编码后的最小值是不是 0。用LabelEncoder重新编码,确保索引从 0 连续到n_categories - 1。如果标签是「是/否」字符串,先映射成 1/0。
4.2 验证集 AUC 0.95,测试集掉到 0.6
现象:验证指标漂亮得不像话,换一批数据就崩。原因几乎都是数据泄漏:标准化、缺失填补、类别编码在划分训练测试之前就做了,或者用了未来信息。解决:把数据划分放在所有预处理之前,用train_test_split先切分,再在训练集上 fit 所有变换器,验证集和测试集只 transform。时间序列数据要按时间切,不能随机切。
4.3 违约样本召回率极低,阈值 0.5 一刀切
现象:模型 AUC 有 0.75,但按 0.5 阈值分类,违约样本几乎全漏。原因是类别不平衡下,模型输出的概率整体偏低,0.5 不是最优阈值。解决:在验证集上画 KS 曲线或 PR 曲线,找 KS 最大或 F1 最优的阈值。风控场景通常把阈值设在违约概率的 70% 到 90% 分位数附近,宁可错杀不可放过。阈值要固化到配置文件,推理时读取。
4.4 嵌入层维度设太大,显存爆了
现象:模型刚初始化就 OOM,或者 batch size 只能设到 32。原因是高基数类别特征的嵌入表参数量过大,比如 10 万个取值 × 50 维 = 500 万参数,光嵌入层就占几百 MB。解决:高基数类别先做频次截断,出现次数少于 100 的取值统一归为「其他」,把基数降到几千以内。嵌入维度按min(16, sqrt(n_categories))控制,别用固定 50。
4.5 推理时预处理和训练不一致
现象:离线指标 0.78,上线后监控显示 KS 只有 0.3。原因是推理服务的预处理逻辑和训练脚本各写各的,缺失填补的中位数、标准化的均值、类别编码的映射表都对不上。解决:把预处理固化成一个Preprocessor类或sklearn的Pipeline,训练和推理共用同一份代码,参数存成文件。每次模型更新,预处理参数一起版本化。
5. 从跑通到能用:阈值调优、模型解释与部署前检查
模型跑出 AUC 只是起点,真正决定这套系统能不能用的是阈值怎么定、业务方信不信、以及上线前有没有做一致性校验。这一章讲三个具体技巧,都是我实际项目里验证过的。
5.1 用 KS 曲线定阈值,而不是拍脑袋
KS 曲线横轴是阈值,纵轴是 KS 值。找到 KS 最大的点,对应的阈值就是统计上最优的切分点。但业务上还要考虑通过率:如果阈值定得太高,违约拦截率上去了,但通过率可能从 70% 掉到 40%,业务量撑不住。我的做法是画一张双轴图,左轴 KS,右轴通过率,找两者的平衡点。
import numpy as np from sklearn.metrics import roc_curve def find_best_threshold(y_true, y_prob, min_pass_rate=0.5): """在保证最低通过率的前提下,找 KS 最大的阈值""" fpr, tpr, thresholds = roc_curve(y_true, y_prob) ks = tpr - fpr # 通过率 = 预测为正常的比例 pass_rate = 1 - (y_prob[:, None] >= thresholds[None, :]).mean(axis=0) valid = pass_rate >= min_pass_rate if not valid.any(): return thresholds[np.argmax(ks)] ks_valid = np.where(valid, ks, -1) best_idx = np.argmax(ks_valid) return thresholds[best_idx], ks[best_idx], pass_rate[best_idx]min_pass_rate是业务约束,比如要求至少 50% 的申请人能通过。pass_rate的计算用广播比较,得到每个阈值下预测为正常的比例。这个函数返回最优阈值、对应 KS 和通过率,直接写进配置文件。注意阈值是在验证集上定的,测试集只用来最终评估,不能反复调。
5.2 用 SHAP 做单样本解释,让风控方敢用
深度学习模型被诟病最多的是黑匣子,风控审批人员不敢直接信一个说不清理由的分数。SHAP 能给每个样本输出每个特征的贡献值,正贡献推高违约概率,负贡献拉低。对表格数据,KernelExplainer或DeepExplainer都能用,但DeepExplainer对嵌入层模型支持更好。
import shap import torch # 取 100 个背景样本 background = next(iter(train_loader)) explainer = shap.DeepExplainer(model, [background[0], background[1]]) # 解释一个测试样本 test_sample = (x_cat_test[:1], x_num_test[:1]) shap_values = explainer.shap_values(test_sample) # shap_values 的形状对应每个输入特征,可以映射回原始字段名 # 正数表示推高违约概率,负数表示拉低DeepExplainer的背景样本数量影响解释稳定性和速度,100 到 500 之间比较合适。解释结果可以做成审批界面上的「主要影响因素」列表,比如「负债收入比过高(+0.12)、历史逾期次数多(+0.08)、收入稳定(-0.05)」。这一步做了,业务方的接受度会明显不一样。
5.3 上线前的三项一致性检查
模型部署前,我会固定做三件事。第一,用同一批测试数据,分别走离线脚本和推理服务,对比输出的概率值,差异超过 1e-4 就要查预处理。第二,检查推理服务的输入字段顺序和训练时是否一致,字段错位是上线后指标暴跌的常见原因。第三,压测推理延迟,单样本推理超过 50ms 就要考虑模型蒸馏或量化,信贷审批通常要求实时返回。
# 一致性检查示例 def check_consistency(offline_probs, online_probs, tol=1e-4): diff = np.abs(np.array(offline_probs) - np.array(online_probs)) max_diff = diff.max() if max_diff > tol: bad_idx = np.where(diff > tol)[0] print(f"不一致样本数: {len(bad_idx)}, 最大差异: {max_diff:.6f}") print(f"前 5 个不一致索引: {bad_idx[:5]}") return False print("一致性检查通过") return True这套流程走下来,从一份 Python 源码到一个能解释、能监控、能上线的违约预测系统,中间的差距主要就在数据预处理的一致性、阈值调优和解释性这三块。我自己的习惯是,每次模型更新,先把预处理参数和阈值一起版本化,再跑一遍一致性检查,确认无误才推生产。希望帮到你。
本文还有配套的精品资源,点击获取