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条样本特征维度20LM算法在矩阵求逆时能一步走到误差极小值附近。需要注意迭代次数少并不代表模型简单相反如果设置goal太小比如1e-6反而可能陷入过拟合把训练集噪声也学进去测试集正确率不升反降。我一般会保存训练过程中的误差曲线和验证集正确率如果误差在第1次迭代就掉到1e-3以下说明模型有足够容量去拟合数据这时候更该关注的是测试集的稳定性而不是继续压榨训练误差。这也是为什么该项目会测试20次再取平均单次运行的正确率会受随机权重初始化影响20次平均更能反映模型的真实泛化能力。3. German Credit数据集从文本到数值矩阵的预处理3.1 german.data和german.data-numeric到底有什么区别german.data是原始的文本格式每一行代表一个客户包含20个属性。其中既有纯数值属性比如持续时间月、贷款金额、年龄也有分类属性比如个人状态和性别、目的、房产情况。分类属性的值用A11、A12这样的代码表示不能直接喂给神经网络。german.data-numeric则是经过数值化转换后的版本每列已经是实数方便直接加载。这个项目里credit_class.m的主要职责就是完成从文本代码到数值索引的映射并生成可直接用于训练和测试的数值矩阵。3.2 文本格式的编码映射credit_class.m在做什么在原始german.data中属性编码规则是固定的。比如“Status of existing checking account”有四种取值A11代表小于0 DMA12代表0到200 DMA13代表大于200 DMA14代表没有账户。这些取值没有大小含义不能直接当连续值用。credit_class.m里常见的做法是用switch或containers.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函数如果使用旧版本可以用textscan或fgetl逐行读取。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-numeric和credit_class.m不能改名。第二步检查MATLAB当前路径最好把主目录添加到路径中避免load找不到文件。运行main.m后如果看到输出平均正确率74.97%最低73.4%说明复现成功。如果你的环境是MATLAB R2020a以下readlines函数不可用需要把credit_class.m里的读取方式替换为textscanfid 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次取平均运行时间大约一两分钟。你会看到goal1e-2时迭代次数更少但正确率通常略低goal1e-4时训练时间翻倍正确率也不一定更高。最终选择哪个组合取决于你是要快速出结果还是要更好的测试集表现。本文还有配套的精品资源点击获取