一、实验目的
理解 BP 神经网络的结构和原理,掌握反向传播算法对神经元的训练过程,了解反向传播公式。通过构建 BP 网络模式识别实例,熟悉 BP 网络的原理及结构。
二、实验原理
1. BP 神经网络概述
BP (Back Propagation) 神经网络是一种按照误差逆向传播算法训练的多层前馈神经网络。它是目前应用最广泛的神经网络模型之一。其核心思想是通过梯度下降法来自动调整网络中神经元之间的连接权值和偏置,从而使得网络的实际输出与期望输出之间的误差最小化。
2. 神经元与激活函数
BP 网络的基本单元是神经元。每个神经元接收上一层神经元的输出作为输入,经过加权求和后,通过一个非线性激活函数变换,输出到下一层。
在本实验中,我们使用Sigmoid 函数作为激活函数。
作用:它像一个平滑的开关,能够将任意大小的输入数值压缩映射到0 到 1的区间内。
意义:这种特性非常适合二分类问题(如本实验中的良性/恶性判断),输出值可以直接被理解为患病的“概率”。
3. BP 算法的核心流程
BP 算法的学习过程就像是“老师改作业”,主要分为两个阶段:前向传播(做题)和反向传播(纠错)。
(1) 前向传播
这是网络“思考”的过程。信号从输入层出发,经过隐藏层的层层处理(加权求和与激活),最终传递到输出层,得出一个预测结果。在本实验中,30 个特征数据输入网络,经过两层隐藏层的计算,最终输出一个 0 到 1 之间的数值。
(2) 误差计算
网络得出的预测值通常与真实标签(标准答案)存在差异。我们需要计算这个差距的大小,通常使用均方误差来衡量。误差越小,说明模型越准。
(3) 反向传播
这是 BP 算法最精髓的“纠错”阶段。因为我们无法直接知道中间隐藏层到底错在哪,所以我们需要将输出层的总误差反向传回给网络。
利用链式法则,从后往前逐层推导。
计算出每一个神经元的连接权重对总误差“贡献”了多少(即计算梯度)。如果某个权重的贡献大,说明它错得厉害,需要大幅调整。
(4) 权重更新 (梯度下降)
有了“纠错指南”(梯度)之后,网络就会沿着误差减小的方向更新所有的权重和偏置。这个过程重复进行(训练多轮),直到误差小到我们满意为止。
4. 算法流程总结
初始化:随机设定网络权重和偏置。
前向传播:输入样本,计算各层输出。
计算误差:对比预测值与真实标签。
反向传播:利用链式法则计算各层误差梯度。
修正权重:根据梯度和学习率更新网络参数。
迭代:重复步骤 2-5,直到误差满足要求或达到最大迭代次数。
三、实验数据集与预处理
数据集:威斯康星乳腺癌诊断数据集 (WDBC)
输入:30 个特征属性
输出:1 个类别 (M-恶性, B-良性)
划分:训练集 80%,测试集 20%
四、实验代码实现
1. 数据转换代码 (DataProcess.m)
原始数据为.data文本格式,需转换为 MATLAB 可用的.mat格式。
%% 数据预处理:将文本数据转换为 .mat 格式 clc; clear; close all; % 1. 读取数据 (请修改为你电脑上的实际路径) filePath = 'E:\matlab\bin\实验\BP\数据集\wdbc.data'; if exist(filePath, 'file') ~= 2, error('找不到文件'); end % 使用 readtable 读取混合格式 raw_data = readtable(filePath, 'FileType', 'text', 'ReadVariableNames', false); % 2. 提取特征 (第3列到第32列 -> 1-30属性) X = table2array(raw_data(:, 3:end)); % 3. 提取标签 (第2列) 并转为数字 % 'M'(恶性)=1, 'B'(良性)=0 Y_labels = raw_data.Var2; Y = double(strcmp(Y_labels, 'M')); % 4. 保存为 .mat 文件 save('wdbc_data.mat', 'X', 'Y'); disp('数据转换完成!已保存为 wdbc_data.mat'); disp(['特征矩阵维度: ', num2str(size(X))]); % 应该是 569x30 disp(['标签矩阵维度: ', num2str(size(Y))]); % 应该是 569x12. 纯手写 BP 神经网络
代码亮点:
满足 4 层网络要求:实现了输入层 -> 隐藏层1 -> 隐藏层2 -> 输出层的双隐层结构。
学习率对比分析:自动循环
0.01, 0.1, 0.5, 1.0四种学习率,并在同一张图上绘制误差曲线,直观展示学习率对收敛速度的影响。
clc; clear; close all; % 1. 数据准备 dataFile = 'E:\matlab\bin\实验\BP\数据集\wdbc.mat'; if ~exist(dataFile, 'file'), error('找不到 wdbc.mat,请检查路径'); end load(dataFile); X = X'; Y = Y'; [X, ps] = mapminmax(X, 0, 1); % 归一化 % 划分 4:1 numSamples = size(X, 2); numTrain = floor(0.8 * numSamples); numTest = numSamples - numTrain; rng(42); % 固定随机种子 randIndex = randperm(numSamples); X_train = X(:, randIndex(1:numTrain)); Y_train = Y(:, randIndex(1:numTrain)); X_test = X(:, randIndex(numTrain+1:end)); Y_test = Y(:, randIndex(numTrain+1:end)); % 2. 网络参数 input_size = 30; hidden_size = 4; % 隐藏层节点数 output_size = 1; lr = 0.1; % 学习率 max_epoch = 20000; % 训练轮数 W1 = rand(hidden_size, input_size) - 0.5; b1 = rand(hidden_size, 1) - 0.5; W2 = rand(output_size, hidden_size) - 0.5; b2 = rand(output_size, 1) - 0.5; % --- 初始化历史记录 (用于画全套曲线) --- loss_history = zeros(1, max_epoch); acc_train_his = zeros(1, max_epoch); acc_test_his = zeros(1, max_epoch); prec_his = zeros(1, max_epoch); % 精确率曲线 recall_his = zeros(1, max_epoch); % 召回率曲线 f1_his = zeros(1, max_epoch); % F1分数曲线 % 3. 训练循环 disp(['开始训练 (隐藏层: ' num2str(hidden_size) ')...']); for epoch = 1:max_epoch % --- 训练集前向传播 --- Z1 = W1 * X_train + b1; A1 = 1 ./ (1 + exp(-Z1)); Z2 = W2 * A1 + b2; A2 = 1 ./ (1 + exp(-Z2)); % 计算 Loss E = Y_train - A2; loss = sum(E.^2) / numTrain; loss_history(epoch) = loss; % 记录训练集准确率 acc_train_his(epoch) = sum((A2>0.5) == Y_train) / numTrain; % --- 反向传播 --- delta2 = E .* (A2 .* (1 - A2)); delta1 = (W2' * delta2) .* (A1 .* (1 - A1)); W2 = W2 + lr * (delta2 * A1') / numTrain; b2 = b2 + lr * sum(delta2, 2) / numTrain; W1 = W1 + lr * (delta1 * X_train') / numTrain; b1 = b1 + lr * sum(delta1, 2) / numTrain; % --- 测试集评估 (计算所有指标曲线) --- % 只做前向,不更新权重 A1_t = 1 ./ (1 + exp(-(W1 * X_test + b1))); A2_t = 1 ./ (1 + exp(-(W2 * A1_t + b2))); pred_t = double(A2_t > 0.5); % 计算 TP, TN, FP, FN TP = sum(pred_t == 1 & Y_test == 1); FP = sum(pred_t == 1 & Y_test == 0); FN = sum(pred_t == 0 & Y_test == 1); TN = sum(pred_t == 0 & Y_test == 0); % 计算指标 (防止分母为0) acc = (TP+TN) / numTest; prec = TP / (TP + FP + 1e-8); % 加极小值防报错 rec = TP / (TP + FN + 1e-8); f1 = 2 * prec * rec / (prec + rec + 1e-8); % 存入历史数组 acc_test_his(epoch) = acc; prec_his(epoch) = prec; recall_his(epoch) = rec; f1_his(epoch) = f1; if mod(epoch, 1000) == 0 fprintf('Epoch %d: Loss=%.4f, Test Acc=%.2f%%\n', epoch, loss, acc*100); end end % 最终预测 A1_t = 1 ./ (1 + exp(-(W1 * X_test + b1))); A2_t = 1 ./ (1 + exp(-(W2 * A1_t + b2))); final_pred = double(A2_t > 0.5); TP = sum(final_pred == 1 & Y_test == 1); TN = sum(final_pred == 0 & Y_test == 0); FP = sum(final_pred == 1 & Y_test == 0); FN = sum(final_pred == 0 & Y_test == 1); disp('1. 混淆矩阵详情 :'); disp([' - [预测正确] 真阳性 (TP - 恶性测出恶性): ' num2str(TP)]); disp([' - [预测正确] 真阴性 (TN - 良性测出良性): ' num2str(TN)]); disp([' - [预测错误] 假阳性 (FP - 误判为恶性) : ' num2str(FP)]); disp([' - [预测错误] 假阴性 (FN - 漏判为良性) : ' num2str(FN)]); disp('2. 核心性能指标 (Performance Metrics):'); disp([' - 准确率 (Accuracy) : ' num2str(acc_test_his(end)*100, '%.2f') '%']); disp([' - 精确率 (Precision): ' num2str(prec_his(end)*100, '%.2f') '%']); disp([' - 召回率 (Recall) : ' num2str(recall_his(end)*100, '%.2f') '%']); disp([' - F1 分数 (F1-Score): ' num2str(f1_his(end)*100, '%.2f') '%']); disp('==========================================='); % 5. 全指标可视化绘图 figure('Color', 'w', 'Position', [100, 100, 1000, 600]); % 子图1: Loss 曲线 subplot(2, 2, 1); plot(loss_history, 'b-', 'LineWidth', 1.5); title('1. 训练误差曲线 (Loss)'); xlabel('Epoch'); ylabel('MSE Loss'); grid on; % 子图2: Accuracy 对比 subplot(2, 2, 2); plot(acc_train_his*100, 'r-', 'LineWidth', 1.5); hold on; plot(acc_test_his*100, 'g--', 'LineWidth', 1.5); legend('训练集', '测试集', 'Location', 'SouthEast'); title('2. 准确率曲线 (Accuracy)'); xlabel('Epoch'); ylabel('Accuracy (%)'); grid on; ylim([50 100]); % 子图3: 核心指标 (Precision / Recall / F1) subplot(2, 2, [3 4]); % 占满下面两格 plot(prec_his*100, 'c-', 'LineWidth', 1.5); hold on; plot(recall_his*100, 'm-', 'LineWidth', 1.5); plot(f1_his*100, 'k--', 'LineWidth', 1.5); legend('Precision (精确率)', 'Recall (召回率)', 'F1-Score', 'Location', 'SouthEast'); title('3. 详细性能指标变化曲线'); xlabel('Epoch'); ylabel('Value (%)'); grid on; ylim([50 100]);3. MATLAB 工具箱代码 (BP_Toolbox.m)
%% BP 神经网络 (工具箱版本 - 4层结构) clc; clear; close all; load('wdbc_data.mat'); inputs = X'; targets = Y'; % 构建网络: 两个隐藏层 [10, 10] % 结构: Input(30) -> H1(10) -> H2(10) -> Output(1) hiddenLayerSize = [10, 10]; net = patternnet(hiddenLayerSize); % 设置参数 net.trainParam.lr = 0.1; % 学习率 net.trainParam.epochs = 1000; net.divideParam.trainRatio = 0.8; net.divideParam.valRatio = 0.0; % 不使用验证集,为了与手写保持一致 net.divideParam.testRatio = 0.2; % 训练 [net, tr] = train(net, inputs, targets); % 测试 outputs = net(inputs); testInd = tr.testInd; testOutputs = outputs(:, testInd); testTargets = targets(:, testInd); % 计算准确率 predictions = (testOutputs > 0.5); acc = sum(predictions == testTargets) / length(testTargets) * 100; disp(['工具箱模型准确率: ', num2str(acc), '%']); % 绘图 figure; plotperform(tr);五、实验结果分析
1.手写工具库
2. 纯手写 BP 神经网络
3. 手写代码与工具箱的对比
原理验证:手写代码完全复现了 BP 算法的前向传播和反向传播(链式法则)过程。其实验结果(准确率、误差曲线趋势)与 MATLAB 官方工具箱 (
patternnet) 高度一致,从而验证了我们对手写算法推导的正确性。灵活性:虽然工具箱训练速度更快、优化更好,但手写代码让我们能够直观地监控每一层的梯度变化和权重更新过程,这对于深入理解“梯度下降”和“反向传播”的数学本质至关重要。
4. 实验心得
通过本次实验,我深入理解了 BP 神经网络的“黑盒”内部机制。特别是通过亲手推导 $\delta$ 误差项的传递公式,不仅掌握了神经网络的训练流程,也深刻体会到了数据归一化(防止梯度消失)和参数调优(如学习率选择)在实际工程应用中的重要性。