
简介这份资源面向机器学习初学者与图像分类方向的研究者提供宽度学习BLS在Matlab环境下的完整实现并配套经典Mnist手写数字数据集用于理解这一宽度优先于深度的建模思路。压缩包共12个文件以11个m脚本和1个mat数据文件为主整体约14.04MB脚本覆盖数据预处理、模型构建、训练与评估等环节mat文件则承载Mnist样本数据便于直接运行验证。目前已有1624人学习下载说明其在入门实践中具有一定参考价值。读者可借助代码走通从归一化、稀疏权重初始化到训练测试的完整流程观察BLS在图像分类上的收敛表现并对比其他机器学习方法的差异为后续调参与扩展实验提供可复用的脚本基础。1. 宽度学习BLS跑Mnist为什么它敢在CPU上跟深度学习叫板如果你手头只有一台普通笔记本没有独立显卡却想跑一遍手写数字识别深度学习那套 CNN 动辄几十轮 epoch 的训练节奏大概率会让你等到怀疑人生。宽度学习系统Broad Learning SystemBLS就是冲着这个痛点来的它把网络横着长用随机映射生成特征节点和增强节点然后只对最后一层输出权重做一次岭回归求解。整个过程没有反向传播、没有梯度下降Mnist 数据集上几分钟就能出结果CPU 单核都够用。这套 BLS 的 matlab 代码配 Mnist 数据集适合三类人想入门机器学习但被深度学习环境劝退的学生、需要快速验证分类想法又不想调参的工程师、以及做课程设计或图像处理大作业需要一份能跑通、能改、能写进报告的完整方案的人。它不追求 SOTA 精度追求的是我今晚就能看到结果。2. BLS 到底怎么宽起来的从随机映射到岭回归的完整链路2.1 宽度学习的结构直觉为什么不需要反向传播传统深度网络是深——一层叠一层靠链式法则把误差往回传每多一层就多一份梯度消失的风险和计算开销。BLS 换了个思路输入数据先经过一组随机权重映射成特征节点feature nodes这些特征节点再经过另一组随机权重映射成增强节点enhancement nodes最后把所有特征节点和增强节点拼在一起用一个线性层输出。关键在于前面那些随机权重一旦生成就固定不动不参与训练唯一需要求解的就是最后那个输出权重矩阵。这就把问题从非线性优化变成了线性最小二乘。给定输入矩阵 A特征节点和增强节点的拼接目标输出 Y求 W 使得 AW ≈ Y。标准解法是岭回归W (AᵀA λI)⁻¹AᵀY。λ 是正则化系数防止 AᵀA 奇异或过拟合。整个训练过程就是一次矩阵求逆或者用伪逆没有迭代没有学习率没有 epoch 概念。这个设计的代价是什么随机映射的质量全靠运气和稀疏编码的预处理。如果随机权重太离谱特征节点可能根本没抓住数据的结构精度就上不去。所以 BLS 通常会在特征映射前做一步稀疏自编码式的微调或者至少对输入做归一化。这也是为什么同样的代码有人跑出 98%有人只有 90%——随机种子和预处理差异很大。2.2 Mnist 数据在 Matlab 里的加载与预处理Mnist 原始格式是 IDX 二进制文件Matlab 不能直接读。常见做法是先用 Python 或现成脚本转成 .mat 文件或者用 Matlab 的 fopen/fread 按字节解析。我一般推荐转成 .mat省得每次跑都解析一遍。假设你已经有了 mnist.mat里面包含 train_images60000×784、train_labels60000×1、test_images10000×784、test_labels10000×1。% 加载 Mnist 数据假设已转为 mat 格式 load(mnist.mat); % 包含 train_images, train_labels, test_images, test_labels % 归一化到 [0,1]BLS 对输入尺度敏感 train_x double(train_images) / 255; test_x double(test_images) / 255; % 标签转为 one-hot 编码输出层需要向量形式 num_classes 10; train_y zeros(size(train_x,1), num_classes); for i 1:size(train_x,1) train_y(i, train_labels(i)1) 1; % Mnist 标签从 0 开始Matlab 索引从 1 开始 end test_y zeros(size(test_x,1), num_classes); for i 1:size(test_x,1) test_y(i, test_labels(i)1) 1; end fprintf(训练集: %d 样本, 测试集: %d 样本\n, size(train_x,1), size(test_x,1));这段代码做了三件事归一化、one-hot 编码、维度确认。归一化必须做因为 BLS 的随机权重映射对输入范围很敏感像素值 0-255 直接进去会导致特征节点输出爆炸。one-hot 编码是因为输出层是线性回归标签必须是向量形式才能算最小二乘。注意 Mnist 标签是 0-9Matlab 索引从 1 开始所以加 1。提示如果你的 mnist.mat 里标签已经是 one-hot 格式跳过编码步骤。用whos命令确认变量名和维度别凭记忆写。2.3 特征节点与增强节点的生成参数怎么设BLS 的核心参数有三个特征节点窗口数N1、每个窗口的特征节点数N2、增强节点总数N3。常见配置是 N110、N210、N3100也就是 10 个窗口各生成 10 个特征节点共 100 个特征节点再映射出 100 个增强节点。最终拼接矩阵 A 的列数是 N1×N2 N3 200。% BLS 参数设置 N1 10; % 特征节点窗口数 N2 10; % 每个窗口的特征节点数 N3 100; % 增强节点数 lambda 1e-3; % 岭回归正则化系数 % 生成特征节点 train_x_aug [train_x, ones(size(train_x,1),1)]; % 加偏置列 test_x_aug [test_x, ones(size(test_x,1),1)]; % 随机权重初始化固定种子保证可复现 rng(42); feat_weights cell(N1,1); feat_bias cell(N1,1); for i 1:N1 feat_weights{i} randn(size(train_x_aug,2), N2) * 0.1; feat_bias{i} randn(1, N2) * 0.1; end % 计算特征节点输出 Z_train []; Z_test []; for i 1:N1 z_tr train_x_aug * feat_weights{i} feat_bias{i}; z_te test_x_aug * feat_weights{i} feat_bias{i}; % 稀疏化用 soft threshold 或简单的 ReLU z_tr max(z_tr, 0); z_te max(z_te, 0); Z_train [Z_train, z_tr]; Z_test [Z_test, z_te]; end fprintf(特征节点矩阵维度: 训练 %dx%d, 测试 %dx%d\n, size(Z_train), size(Z_test));这里有几个细节值得说。第一随机权重乘了 0.1这是经验值不乘的话特征节点输出方差太大ReLU 之后大部分激活值要么全零要么巨大。第二加了偏置列相当于给线性映射一个截距。第三用 ReLU 做稀疏化比原始 BLS 论文里的稀疏自编码简单但效果差不太多适合快速上手。如果你追求更高精度可以把 ReLU 换成论文里的稀疏编码优化但那就需要额外的迭代了。增强节点的生成类似只是输入变成特征节点的输出% 生成增强节点 enh_weights randn(size(Z_train,2), N3) * 0.1; enh_bias randn(1, N3) * 0.1; H_train max(Z_train * enh_weights enh_bias, 0); H_test max(Z_test * enh_weights enh_bias, 0); % 拼接最终输入矩阵 A_train [Z_train, H_train]; A_test [Z_test, H_test]; fprintf(最终输入矩阵维度: 训练 %dx%d\n, size(A_train));到这里A_train 就是 60000×200 的矩阵。接下来就是一次岭回归求解输出权重。2.4 输出权重的岭回归求解与预测% 岭回归求解输出权重 % W (A*A lambda*I)^(-1) * A * Y AAt A_train * A_train lambda * eye(size(A_train,2)); W AAt \ (A_train * train_y); % 用左除比直接求逆数值更稳定 % 预测 train_pred A_train * W; test_pred A_test * W; % 取最大值作为类别 [~, train_cls] max(train_pred, [], 2); [~, test_cls] max(test_pred, [], 2); % 计算准确率注意标签索引偏移 train_acc mean(train_cls train_labels 1); test_acc mean(test_cls test_labels 1); fprintf(训练准确率: %.2f%%\n, train_acc * 100); fprintf(测试准确率: %.2f%%\n, test_acc * 100);用左除\而不是inv()是 Matlab 数值计算的基本素养前者用 LU 分解后者显式求逆精度和速度都差一截。lambda 取 1e-3 是常见起点太小会过拟合太大会欠拟合。测试准确率通常在 97%-98.5% 之间取决于随机种子和参数。如果低于 96%先检查归一化和 one-hot 编码有没有写错。3. 把 BLS 跑出 98% 的调参路径从默认配置到逐项优化3.1 特征节点数和增强节点数怎么配默认的 N110、N210、N3100 在 Mnist 上大概能到 97.5% 左右。想往上走最直接的办法是加宽。但加宽不是无脑加特征节点和增强节点的作用不同特征节点负责捕捉输入的局部模式增强节点负责组合这些模式形成更复杂的决策边界。我的经验是先把 N3 加到 200-300因为增强节点是随机映射的多一些可以覆盖更多组合然后再考虑加 N1 或 N2。配置特征节点总数增强节点数测试准确率参考训练耗时CPUN110,N210,N310010010097.5%~15sN110,N210,N330010030098.1%~25sN115,N210,N330015030098.3%~35sN120,N210,N350020050098.4%~60s注意加宽到一定程度后收益递减而且 AᵀA 的维度变大求逆开销是 O(n³)N3500 时矩阵是 700×700还能接受再大就明显慢了。Mnist 这个任务本身不难98.5% 左右基本是 BLS 的天花板再往上需要改稀疏编码或集成多个 BLS。3.2 正则化系数 lambda 的搜索策略lambda 控制岭回归的惩罚强度。太小1e-6时 AᵀA λI 接近奇异解不稳定太大1e-1时权重被压得太狠欠拟合。我一般用对数网格搜索lambda_list [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1]; best_acc 0; best_lambda 1e-3; for lam lambda_list AAt A_train * A_train lam * eye(size(A_train,2)); W_tmp AAt \ (A_train * train_y); pred_tmp A_test * W_tmp; [~, cls_tmp] max(pred_tmp, [], 2); acc_tmp mean(cls_tmp test_labels 1); fprintf(lambda%.0e, 测试准确率%.2f%%\n, lam, acc_tmp*100); if acc_tmp best_acc best_acc acc_tmp; best_lambda lam; end end fprintf(最佳 lambda: %.0e, 准确率: %.2f%%\n, best_lambda, best_acc*100);这段代码在已经算好 A_train 和 A_test 的基础上跑每次只重算输出权重很快。通常 1e-3 到 1e-2 之间会出最佳值。如果最佳值落在边界上说明网格范围不够往外扩。3.3 随机种子对结果的影响有多大BLS 的随机权重是固定的但初始化的随机种子不同结果会波动。我实测过 10 个不同种子测试准确率的标准差大约 0.3%。这意味着你看到 97.8% 和 98.1% 的差异可能只是种子不同不是参数优劣。所以调参时一定要固定种子比较不同参数时用同一个种子。% 固定种子保证可复现 rng(42); % 如果想看种子影响可以循环 seeds [1, 42, 123, 2024, 999]; for s seeds rng(s); % ... 重新生成随机权重并训练 ... % 记录准确率 end如果多个种子下准确率波动超过 1%说明模型不稳定可能是 lambda 太小或者特征节点太少。稳定后波动应该在 0.5% 以内。3.4 用混淆矩阵看 BLS 到底错在哪准确率只是一个数字想知道模型哪里弱看混淆矩阵。Mnist 里最容易混的是 4 和 9、3 和 8、7 和 1。% 混淆矩阵 cm confusionmat(test_labels, test_cls - 1); % 注意索引偏移 figure; imagesc(cm); colorbar; xlabel(预测类别); ylabel(真实类别); title(BLS 在 Mnist 上的混淆矩阵); axis square; % 打印每个类别的准确率 for i 0:9 acc_i cm(i1,i1) / sum(cm(i1,:)); fprintf(类别 %d 准确率: %.2f%%\n, i, acc_i*100); end如果某个类别准确率明显低可以考虑对该类别做数据增强或者增加特征节点让模型学到更细的笔画差异。不过 Mnist 本身很干净BLS 的瓶颈不在数据质量而在随机映射的表达能力。4. 避坑与排查BLS 跑 Mnist 最常见的 5 个翻车现场4.1 准确率卡在 10% 不动现象训练和测试准确率都在 10% 左右相当于随机猜。原因标签索引没对齐。Mnist 标签是 0-9Matlab 的 one-hot 编码如果直接train_y(i, train_labels(i)) 1当标签为 0 时会报错或写到错误位置。另外预测时max返回的索引是 1-10跟原始标签 0-9 比较时忘了减 1。解决编码时用train_labels(i)1比较时用test_cls - 1 test_labels。跑之前先打印前 5 个标签和对应的 one-hot 行确认。4.2 训练准确率 99% 但测试只有 90%现象训练集几乎完美测试集差一大截。原因lambda 太小或者根本没加正则化。AᵀA 在特征节点多于样本数时容易奇异解出来的权重对训练数据过拟合。另一个可能是特征节点太多而增强节点太少模型记住了训练样本的噪声。解决把 lambda 从 1e-6 提到 1e-3 或 1e-2观察测试准确率变化。同时检查 A_train 的列数是否远小于行数如果列数接近或超过行数必须加正则化。4.3 内存爆了AᵀA 矩阵太大现象N3 设到 1000 以上时Matlab 报 Out of memory。原因A_train 是 60000×1100A_train * A_train 是 1100×1100虽然不大但中间计算 A_train 会复制一份加上其他变量内存吃紧。如果样本数更多比如换成 Fashion-Mnist 的 60000 样本、更多特征问题更明显。解决用single精度代替double存储 A_train内存减半。或者分块计算 AᵀA但 Matlab 里直接算通常够用。最根本的是控制 N3 不超过 500Mnist 上没必要。4.4 每次跑结果都不一样现象同样的代码每次准确率差 1%-2%。原因没有固定随机种子。Matlab 默认的随机种子基于时间每次启动都不一样。解决在生成随机权重之前加rng(42)或任何固定整数。如果用了并行工具箱还要注意每个 worker 的种子。4.5 训练时间远超预期现象说好的几分钟跑了半小时还没完。原因可能是用了inv()而不是\或者循环里反复拼接矩阵导致内存碎片。另一个常见原因是把 Mnist 原始 IDX 文件放在循环里反复读取。解决用\求解预分配矩阵空间Z_train zeros(n, N1*N2)而不是Z_train []然后[Z_train, z_tr]数据只加载一次。5. 让 BLS 再快一截增量学习与批量预测的实操技巧BLS 有一个深度学习没有的优势增量学习。当你有了新数据不需要重新训练整个网络只需要更新输出权重。这在在线学习场景下非常实用。假设你已经训练好了 W现在来了新样本 A_new 和 Y_new更新公式是% 增量学习新数据到来时更新输出权重 % 假设已有 A_train, W, 以及新数据 A_new, Y_new A_combined [A_train; A_new]; Y_combined [train_y; Y_new]; % 重新求解小规模数据下直接重算比增量公式更简单 AAt A_combined * A_combined lambda * eye(size(A_combined,2)); W_updated AAt \ (A_combined * Y_combined); % 如果数据量很大用 Sherman-Morrison 公式增量更新 % 这里不展开核心思想是避免每次重算整个矩阵对于 Mnist 这种规模直接重算也就几十秒增量公式的收益不明显。但如果你的数据是流式的每次只来几百个样本增量更新可以把单次更新时间压到毫秒级。另一个实用技巧是批量预测。如果你有大量测试样本不要一个一个跑直接矩阵乘法一次算完% 批量预测一次处理所有测试样本 test_pred A_test * W; % 10000x10 矩阵一次算完 [~, test_cls] max(test_pred, [], 2);这比循环快几百倍因为 Matlab 的矩阵乘法是底层 BLAS 优化的。我见过有人写 for 循环逐个样本预测10000 个样本跑了好几分钟改成矩阵乘法后不到一秒。最后一个技巧是关于稀疏化的。ReLU 是最简单的稀疏化但如果你想要更高精度可以试试 soft threshold% soft threshold 稀疏化比 ReLU 更平滑 function y soft_threshold(x, lambda) y sign(x) .* max(abs(x) - lambda, 0); end把特征节点生成里的max(z_tr, 0)换成soft_threshold(z_tr, 0.01)在某些数据集上能提升 0.2%-0.5%。代价是多了一个参数要调而且计算量略大。Mnist 上 ReLU 够用了不用折腾。我自己跑 BLS 的习惯是先用默认参数跑通确认准确率在 97% 以上然后固定种子只调 lambda 和 N3其他不动。这样能在半小时内找到接近最优的配置剩下的时间用来分析混淆矩阵和写报告。BLS 不是精度最高的方法但它是从想跑个分类到看到结果之间路径最短的方法之一。希望帮到你。本文还有配套的精品资源点击获取