news 2026/9/14 2:45:43

BP神经网络实现个人信贷评分:MATLAB实战与German Credit数据复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BP神经网络实现个人信贷评分:MATLAB实战与German Credit数据复现

简介:基于BP神经网络的个人信贷信用评估项目,主要面向金融风控领域初学者及机器学习入门者,提供一套可直接运行的Matlab分类方案,涵盖数据读取、网络训练与正确率评估环节。压缩包共含3个文件,包括核心的.m脚本、data-numeric及data格式的数据文件,整体仅28KB,结构精简,便于快速理解代码逻辑并作二次修改。脚本基于德国信贷数据样本完成信用评估建模,作者连续运行20次所得平均正确率为74.97%,最低正确率73.4%,且迭代次数稳定在3次,体现出较好的收敛稳定性,可作为后续改进的基线参照。利用该资源可快速搭建完整的BP信用评分流程,便于进一步探索网络结构、学习率等参数对分类结果的影响。资源已有334人浏览学习,适合希望掌握BP神经网络实际应用、复现信用评估实验并在此基础上调优的读者。

1. 为什么个人信贷评分会用BP神经网络

信贷审批这个场景很有意思,很多人第一反应是逻辑回归,因为它可解释性强。但真实征信数据里,收入、负债比、历史逾期次数这些特征之间,常常存在非线性交叉作用,比如“低收入高负债”和“高收入中负债”的风险差异并不线性。我拿到这个项目时,zip里核心文件是三个:german.data、german.data-numeric、credit_class.m,主脚本main.m把整套流程串起来。用MATLAB里最经典的BP神经网络对德国信用数据集做二分类,跑20次平均正确率74.97%,最低也在73.4%,迭代只需要3次就收敛。这个结果在学术上不算惊艳,但在工程复现角度非常稳定,尤其适合想做机器学习课设、金融风控入门或者需要快速跑通一个分类基线的人。

2. BP神经网络与信用评估:网络结构和参数选型的依据

2.1 为什么不用决策边界而是用BP

个人信贷数据里,特征类型很杂:有连续值(如贷款金额、年龄、信用历史时长),也有离散编码(如目的、储蓄账户状态)。逻辑回归或线性SVM假设特征与输出之间是线性加权关系,但真实风险往往出现在特征交互项上。比如“贷款目的是新购汽车”单独看风险中等,但如果结合“应付账户余额为负”和“近3个月查询次数多”,坏账概率会显著上升。BP神经网络的优势在于,隐含层神经元可以自动构造这些高阶交互特征,不需要手工做特征工程。

对于1000条样本的German Credit数据集,BP网络规模不需要很大。输入层对应20个特征,输出层1个节点,隐含层神经元数量过多会过拟合,过少欠拟合。常见做法是用经验公式预估一个范围,比如取输入层和输出层节点数之和的平方根附近,或者用(输入+输出)/2做起点,然后在10到25之间扫描。这个项目跑出来的稳定性和75%左右的正确率,说明网络容量和数据量是匹配的。

2.2 网络拓扑:从输入到输出的每一层如何确定

German Credit原始数据集中,每条样本有20个属性,所以输入层节点数定为20。输出层处理的是二分类问题,标签是“好客户”(1)和“坏客户”(2),用单节点输出更简洁,训练时把标签映射成0和1,输出值经过竞争函数或阈值判断最终类别。

隐含层节点数没有严格公式,以MATLAB的newff为例,常见参数表如下:

参数推荐取值说明
隐含层节点数10 ~ 25节点太多会记住噪声,太少学不到交互特征
输入层激活函数tansig双曲正切,输出范围[-1,1],收敛比logsig快
输出层激活函数purelin线性输出,适合二分类回归输出
训练函数trainlmLevenberg-Marquardt,小数据集收敛极快
学习率0.01 ~ 0.1过大会振荡,过小会卡在局部最优
目标误差1e-3迭代3次就能达到,说明数据可分性好

在MATLAB中创建网络的典型代码如下:

% 创建BP网络:20个输入特征,隐含层15个节点,输出层1个节点 net = newff(minmax(P), [15 1], {'tansig', 'purelin'}, 'trainlm'); % 训练参数设置 net.trainParam.epochs = 1000; % 最大迭代次数 net.trainParam.goal = 1e-3; % 误差目标 net.trainParam.lr = 0.01; % 学习率 net.trainParam.showWindow = false; % 后台运行,不弹窗口

这里minmax(P)用于获取输入数据的范围,newff会基于这个范围初始化权重和偏置。隐含层用tansig,输出层用purelin,这是MATLAB里做二分类回归的经典组合。trainlm是默认推荐,它结合了梯度下降和高斯-牛顿法的优点,对小规模数据集尤其有效。

2.3 训练参数的坑:迭代次数和正确率的关系

迭代3次收敛这个现象,第一次看到可能觉得网络没训练充分,但这实际是trainlm的特性。德国信用数据集只有1000条样本,特征维度20,LM算法在矩阵求逆时能一步走到误差极小值附近。需要注意,迭代次数少并不代表模型简单,相反,如果设置goal太小比如1e-6,反而可能陷入过拟合,把训练集噪声也学进去,测试集正确率不升反降。

我一般会保存训练过程中的误差曲线和验证集正确率,如果误差在第1次迭代就掉到1e-3以下,说明模型有足够容量去拟合数据,这时候更该关注的是测试集的稳定性,而不是继续压榨训练误差。这也是为什么该项目会测试20次再取平均,单次运行的正确率会受随机权重初始化影响,20次平均更能反映模型的真实泛化能力。

3. German Credit数据集:从文本到数值矩阵的预处理

3.1 german.data和german.data-numeric到底有什么区别

german.data是原始的文本格式,每一行代表一个客户,包含20个属性。其中既有纯数值属性,比如持续时间(月)、贷款金额、年龄,也有分类属性,比如个人状态和性别、目的、房产情况。分类属性的值用A11A12这样的代码表示,不能直接喂给神经网络。

german.data-numeric则是经过数值化转换后的版本,每列已经是实数,方便直接加载。这个项目里credit_class.m的主要职责,就是完成从文本代码到数值索引的映射,并生成可直接用于训练和测试的数值矩阵。

3.2 文本格式的编码映射:credit_class.m在做什么

在原始german.data中,属性编码规则是固定的。比如“Status of existing checking account”有四种取值:A11代表小于0 DM,A12代表0到200 DM,A13代表大于200 DM,A14代表没有账户。这些取值没有大小含义,不能直接当连续值用。credit_class.m里常见的做法是用switchcontainers.Map把每个Axx代码映射到一个整数序号,同时保证同一个属性的不同取值映射到连续区间,避免类别间产生错误距离。

简化后的逻辑大致是这样的:

% 假设读取german.data后得到cell数组raw_data % 对第1列进行编码映射 mapping = containers.Map({'A11','A12','A13','A14'}, {1, 2, 3, 4}); encoded_col = zeros(size(raw_data, 1), 1); for i = 1:length(encoded_col) if mapping.isKey(raw_data{i, 1}) encoded_col(i) = mapping(raw_data{i, 1}); else encoded_col(i) = str2double(raw_data{i, 1}); % 数值属性直接转换 end end

这里containers.Map负责把类目标签映射成整数,比if-elseif更易维护。如果某个属性的分类取值很多,比如purpose有11种,可以按频率编码或直接用序号编码。german.data-numeric文件本身就是按这种逻辑生成的,所以用load加载后可以直接得到特征矩阵,省去手写解析的时间。

3.3 归一化和数据集划分:不能跳过的两步

BP神经网络的权重初始化通常在0附近,输入特征的尺度如果不一致,量级大的特征会主导梯度更新,导致训练不稳定。常见做法是用mapminmax把特征缩放到[-1,1]或[0,1]区间,缩放参数从训练集上计算,然后应用到测试集,避免数据泄露。

% 加载german.data-numeric data = load('german.data-numeric'); % 分离特征和标签 X = data(:, 1:20); % 前20列是特征 Y = data(:, 21); % 第21列是标签,1表示好客户,2表示坏客户 % 把特征归一化到[-1,1] [X_norm, ps] = mapminmax(X', -1, 1); X_norm = X_norm'; % 标签转换:将2变成0,使网络输出与标签一致 Y_binary = (Y == 1); % 好客户为1,坏客户为0 % 随机划分训练集和测试集,80%训练,20%测试 rng(42); idx = randperm(size(X_norm, 1)); train_idx = idx(1:round(0.8 * length(idx))); test_idx = idx(round(0.8 * length(idx)) + 1:end); X_train = X_norm(train_idx, :); Y_train = Y_binary(train_idx, :); X_test = X_norm(test_idx, :); Y_test = Y_binary(test_idx, :);

需要注意,mapminmax默认按行处理,所以输入必须转置,得到的结果再转置回来。rng(42)固定随机种子,保证每次划分一致,这是复现结果的关键一步。如果不固定种子,20次测试的平均正确率会有一两个百分点的波动,但整体趋势不变。

4. 核心实现:main.m和credit_class.m如何完成一次完整训练

4.1 main.m主流程:加载数据、建网络、训练、20次重复测试

main.m是整个项目的主控脚本,它会调用credit_class.m完成数据解析,然后执行多轮训练和测试。多轮测试的价值在于,BP网络每次初始化的随机权重不同,单轮正确率可能偏高或偏低,循环20次取平均,才能真实反映模型稳定性。

可以用下面的伪代码描述主流程:

clear; clc; close all; % ---- 第1步:通过credit_class.m加载并处理数据 ---- [X_norm, Y_binary] = credit_class(); % 返回归一化后的特征和二值标签 % ---- 第2步:准备记录正确率 ---- all_acc = zeros(20, 1); % ---- 第3步:重复训练与测试20次 ---- for i = 1:20 % 随机划分训练集与测试集 rng(i); % 每次使用不同但可复现的随机种子 idx = randperm(size(X_norm, 1)); train_idx = idx(1:800); test_idx = idx(801:end); X_train = X_norm(train_idx, :); Y_train = Y_binary(train_idx, :); X_test = X_norm(test_idx, :); Y_test = Y_binary(test_idx, :); % 创建并配置BP网络 net = newff(minmax(X_train'), [15 1], {'tansig', 'purelin'}, 'trainlm'); net.trainParam.epochs = 1000; net.trainParam.goal = 1e-3; net.trainParam.showWindow = false; % 训练网络 [net, tr] = train(net, X_train', Y_train'); % 测试 Y_pred = sim(net, X_test'); Y_pred_binary = Y_pred > 0.5; % 概率大于0.5判定为好客户 % 计算正确率 acc = 1 - mean(abs(Y_pred_binary - Y_test')); all_acc(i) = acc * 100; end % ---- 第4步:输出平均正确率和最低正确率 ---- fprintf('平均正确率: %.2f%%\n', mean(all_acc)); fprintf('最低正确率: %.2f%%\n', min(all_acc));

这里credit_class()返回的Y_binary是列向量,train要求输入输出为矩阵格式,行对应样本,列对应变量,所以需要转置。输出层使用purelin线性激活函数,得到的是一个连续预测值,判断时以0.5为阈值转成0/1。平均正确率74.97%就是这么统计出来的。

4.2 credit_class.m是如何工作的

credit_class.m不是网络训练函数,而是数据准备函数。它负责读取german.data文本文件,完成类别编码和数值化,最后输出特征矩阵和标签向量。如果只提供了german.data-numeric,它也可以直接加载这个数值化矩阵,省去重复编码操作。

在实现上,我更倾向于让credit_class支持两种输入:如果检测到german.data-numeric存在,直接load;否则读取german.data做在线编码。这种做法可以避免每次运行都重新解析文本,节省时间。

function [X, Y] = credit_class() % 优先加载已转换好的数值数据 if exist('german.data-numeric', 'file') data = load('german.data-numeric'); X = data(:, 1:20); Y = data(:, 21); else % 否则从原始german.data解析 lines = readlines('german.data'); num_samples = length(lines); X = zeros(num_samples, 20); Y = zeros(num_samples, 1); for i = 1:num_samples tokens = strsplit(strtrim(lines(i)), ' '); for j = 1:20 token = tokens{j}; if isstrprop(token, 'digit') X(i, j) = str2double(token); else % 调用编码映射函数 X(i, j) = map_code(token); end end Y(i) = str2double(tokens{21}); % 最后一个是标签 end end % 归一化特征 [X, ~] = mapminmax(X', -1, 1); X = X'; % 标签转换为0/1 Y = (Y == 1); end

在解析文本时,需要判断某个token是纯数字还是Axx格式,这里用isstrprop(token, 'digit')检查是否全为数字。需要注意的是,readlines是较新版本的MATLAB函数,如果使用旧版本,可以用textscanfgetl逐行读取。

4.3 迭代3次收敛背后的训练机制

该项目报告里提到迭代次数均为3次,这个现象和trainlm的训练特性直接相关。Levenberg-Marquardt算法在每一步迭代时计算雅可比矩阵的近似二阶导,对1000条样本这种规模的数据,一步就能确定一个较好的搜索方向。加上特征已经归一化到[-1,1],权重更新非常平稳,3次迭代就达到了1e-3的误差阈值。

但这不代表网络没有继续学习的空间。如果把goal改为1e-5,迭代次数会增加到十几甚至几十次,训练集误差继续下降,但测试集正确率可能卡在75%附近不再提升。这说明75%左右就是该模型复杂度下能到达的性能上限,继续压训练误差只会让模型记忆更多噪声,对未知客户的预测没有帮助。

5. 复现74.97%正确率并把它做到80%以上的方向

5.1 复现时的两个前置检查

解压zip后,第一步确认当前目录下存在所有文件,尤其注意german.data-numericcredit_class.m不能改名。第二步检查MATLAB当前路径,最好把主目录添加到路径中,避免load找不到文件。运行main.m后,如果看到输出平均正确率74.97%,最低73.4%,说明复现成功。

如果你的环境是MATLAB R2020a以下,readlines函数不可用,需要把credit_class.m里的读取方式替换为textscan

fid = fopen('german.data', 'r'); C = textscan(fid, '%s', 'Delimiter', '\n'); fclose(fid); lines = C{1};

替换后逻辑完全一致,不影响最终正确率。

5.2 从75%往上走的三个可操作方向

第一个方向是隐含层节点数的网格搜索。把15改成10、12、18、25分别测试,观察平均正确率变化。在某些随机划分下,隐含层节点取12时,正确率可能会跳到76%以上。虽然涨幅有限,但能让你直观理解模型容量对结果的影响。

第二个方向是交叉验证。固定一个80/20划分,受限于数据集中正负样本比例,测试集结果波动很大。改成5折交叉验证后,每条样本都有机会参与测试,得到的平均正确率会更稳定,也更容易复现。

第三个方向是特征选择。German Credit的20个特征中,有几个属性比如“电话”和“外籍工人”对区分好客户和坏客户贡献很小,可以尝试用reliefF或卡方检验筛选前15个特征再训练。这个动作通常能把正确率抬高1到3个百分点,并且降低过拟合风险。

5.3 一个实用的快速调参脚本片段

下面的代码可以在不修改主流程的情况下,自动扫描不同隐含层节点数和目标误差组合,并把结果记录到表格中:

results = []; hidden_sizes = [10 12 15 18 20]; goals = [1e-2 1e-3 1e-4]; for h = hidden_sizes for g = goals accs = zeros(5, 1); for k = 1:5 rng(k); idx = randperm(size(X, 1)); train_idx = idx(1:800); test_idx = idx(801:end); net = newff(minmax(X_train'), [h 1], {'tansig','purelin'}, 'trainlm'); net.trainParam.goal = g; net.trainParam.epochs = 500; net.trainParam.showWindow = false; [net, ~] = train(net, X_train', Y_train'); Y_pred = sim(net, X_test') > 0.5; accs(k) = 1 - mean(abs(Y_pred - Y_test')); end results = [results; h, g, mean(accs)*100]; end end disp(array2table(results, 'VariableNames', {'Hidden', 'Goal', 'Acc'}));

这段代码会把每种组合跑5次取平均,运行时间大约一两分钟。你会看到,goal=1e-2时迭代次数更少,但正确率通常略低;goal=1e-4时训练时间翻倍,正确率也不一定更高。最终选择哪个组合,取决于你是要快速出结果,还是要更好的测试集表现。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:42:59

Traefik 插件 localPlugins 加载失败?TaoToken 这样给 Codex 改 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 2:40:58

告别dSPACE与VeriStand,SimuRTS微秒级实时仿真平台迁移实战

做实时仿真和HIL测试这行,说起来都是泪。我在这个领域折腾了快十年,从dSPACE到VeriStand,哪个平台的脾气都摸得差不多。最近两三年,测试团队里开始正经讨论国产平台,我才第一次认真接触凯云SimuRTS。说实话&#xff0c…

作者头像 李华
网站建设 2026/9/14 2:38:10

Bert+TextCNN融合模型:文本分类中的高效落地实践

简介:一份基于BertTextCNN的中文文本分类项目完整源码包,面向NLP初学者、算法工程师及需要快速落地文本分类任务的开发者。该项目将BERT语义表示与TextCNN局部特征提取相结合,适用于情感分析、短文本分类等多种场景,代码结构清晰&…

作者头像 李华
网站建设 2026/9/14 2:37:33

YOLOv8钢材表面缺陷检测:从数据标注到PyQt界面部署全流程

简介:YOLOv8钢材缺陷检测资源包,面向工业质检与计算机视觉开发者,提供从模型训练到可视化检测的完整闭环。内含已训练好的YOLOv8检测权重,附带PR曲线、loss曲线等训练评估文件,并配有使用LabelImg标注的钢材缺陷数据集…

作者头像 李华