
简介面向数据分类任务粒子群算法优化极限学习机的完整实现适合正在从事机器学习、模式识别相关课题的学生与工程师用于解决传统极限学习机因随机设置输入权值和阈值而导致分类精度不稳定的问题。压缩包共12个文件以m脚本为主包含主程序、粒子群寻优、极限学习机训练与预测等模块另有效果图、使用说明和mat数据集各部分分工明确便于单独调用或改造整体体积仅133KB。目前已有153人学习程序基于Matlab平台开发可直接运行并替换为自己的样本数据和标签开展实验。借助粒子群算法自动搜索输入权值与隐含层阈值可对比普通极限学习机和优化后的分类性能配套效果图与使用说明能帮助理解收敛过程、参数设置对结果的影响。无论是课程设计、毕业设计还是作为科研预实验中的分类基线这套代码都具备较好的可复用性与参考价值。1. 为什么 ELM 还需要粒子群算法优化第一次拿极限学习机做分类实验的人都会先被它的训练速度吸引单个隐层、随机固定输入权重输出权重用最小二乘一步算出几十毫秒就能出一个模型。但多跑几次随机种子会发现分类准确率跟着“随机情绪”走同一份数据结果可能差出三五个百分点。原因就出在隐层参数随机生成不一定落在合适的区域。粒子群优化极限学习机PSO-ELM的思路很直接把输入权重和偏置当成粒子位置用粒子群算法PSO搜出一组更稳定的隐层参数再交给 ELM 用解析方法计算输出权重。这是一份适合做分类实验、论文对比和课程设计的资源Matlab 2019b 环境下可以直接运行也可以作为进一步改进的基线。2. 极限学习机的前向计算与 elmtrain.m 的源码位置2.1 单隐层网络为什么能把训练问题转成解线性方程ELM 针对的是单隐层前馈神经网络。假设输入矩阵 X 维度是 nSample×nFeature隐层神经元个数是 nHidden随机初始化输入权重 WnHidden×nFeature和偏置 bnHidden×1隐层输出矩阵 H g(XW b) 的维度是 nSample×nHidden。这一步之后网络里还没有学到的只剩输出权重 beta。ELM 的核心观点是当 W 和 b 固定时训练目标就是让 Hbeta 尽量接近标签矩阵 Y于是 beta 可以通过岭回归或 Moore-Penrose 伪逆直接求出即 beta pinv(H)*Y。这个过程不需要梯度回传也不需要迭代这就是极限学习机训练速度快的根本原因。在 PSO-ELM 中这个性质依然保留。粒子群只负责优化 W 和 bbeta 仍然由伪逆一次解出所以 PSO 每次评估一个粒子的代价只是执行一次矩阵乘法和一次伪逆计算量完全可控。这也是为什么压缩包里的 PSO 主循环可以和 ELM 训练函数频繁互相调用而不会出现传统神经网络嵌入 PSO 后那种训练时间爆炸的问题。2.2 elmtrain.m 与 elmpredict.m 的常规实现压缩包里同时存在 elmtrain.m 和 elmtrainNew.m很多第一次打开资源的人会困惑到底该用哪一个我拆过的同类项目里命名习惯通常是elmtrain.m 是标准 ELM 训练函数随机产生 W 和 belmtrainNew.m 接收外部传入的 W 和 b适合 PSO 迭代结束后用最优粒子组装模型。下面这段代码是标准 elmtrain.m 的常见写法和资源中的函数名对应function model elmtrain(X, Y, nHidden, act) % X: nSample*nFeature, Y: nSample*nClassnClass 为类别数 % nHidden: 隐层神经元个数, act: 激活函数类型 [n, d] size(X); W rand(nHidden, d) * 2 - 1; % 输入权重映射到 [-1,1] b rand(nHidden, 1) * 2 - 1; % 隐层偏置映射到 [-1,1] H X * W b; % 线性加权和 switch lower(act) case sig H 1 ./ (1 exp(-H)); % Sigmoid 激活 case sin H sin(H); % 正弦激活 otherwise H max(H, 0); % ReLU 或自定义 end beta pinv(H) * Y; % 最小二乘解 model.W W; model.b b; model.beta beta; model.act act; end注意rand(nHidden, d)生成的是 [0,1] 均匀分布随机数乘 2 减 1 后落在 [-1,1]这是 ELM 常见的初始化范围。pinv是伪逆对 H 列满秩或行满秩都能给出最小范数解比直接inv稳定得多。预测函数 elmpredict.m 的结构与这里对称输入模型和测试矩阵 X先算 H 再乘 beta输出一个 nSample×nClass 的得分矩阵取每行最大值的下标就得到预测类别。标准的 ELM 到这里就结束了但 PSO-ELM 的关键在于 W 和 b 不应当完全随机。粒子群搜索的正是这些“不随机值”因此 elmtrain.m 里如果写入rng(default)或rng(0)这类重置随机数种子的语句会让每次调用生成同样的 W 和 bPSO 的适应度曲线会退化成一条水平直线。看到这个现象时第一件事就是去检查训练函数内部是否重置了随机数生成器。2.3 fun.m 如何把分类错误率转换成粒子群适应度PSO 并不知道“分类模型”是什么它只认一个标量适应度。常见做法是让 fun.m 接收一个粒子向量 p把 p 拆成输入权重和偏置在训练集上计算 H求出 beta再对内部验证集做预测返回预测错误的样本占比。资源中 PSO.m 调用 fun.m 就是这个过程。下面是适应度函数的框架function err fun(p, Xtr, Ytr, Xva, Yva, nHidden, act) nIn size(Xtr, 2); Wp reshape(p(1:nIn * nHidden), nHidden, nIn); % 粒子前段为权重 bp p(nIn * nHidden 1 : end); % 粒子后段为偏置 Htr Xtr * Wp bp; % 训练集隐层输出 switch lower(act) case sig Htr 1 ./ (1 exp(-Htr)); case sin Htr sin(Htr); otherwise Htr max(Htr, 0); end beta pinv(Htr) * Ytr; % 输出权重 Hva Xva * Wp bp; % 验证集隐层输出 Yp Hva * beta; % 预测得分矩阵 [~, labelPred] max(Yp, [], 2); % 取最大分量作为类别 [~, labelTrue] max(Yva, [], 2); err mean(labelPred ~ labelTrue); % 分类错误率 end这里的reshape是按列填充的所以粒子前段权重要以 “第一列权重放完再放第二列” 的顺序排列也就是 W(:) 的方式展开。如果初始化时按行顺序写入粒子reshape 出来的 Wp 需要转置才能参与后面的X * Wp计算。运行中如果报维度不匹配优先检查这一处。适应度选分类错误率而不是 MSE是因为分类任务更关心“分对还是分错”MSE 会被多数样本的微弱差异主导粒子群往往花很多迭代去优化不太重要的数值。文件在 PSO-ELM 中的作用elmtrain.m标准 ELM 训练随机初始化 W、b计算 betaelmpredict.m用训练好的模型预测输出各类别得分elmtrainNew.m用 PSO 得到的最优粒子作为 W、b重新计算 betafun.m粒子适应度函数返回分类错误率PSO.m粒子群主循环更新速度和位置initialization.m在上下界范围内初始化种群Bounds.m把越界的位置拉回边界main.m主流程包含数据读取、划分、训练和测试3. 粒子群优化极限学习机的训练主线与 PSO.m 实现3.1 粒子编码一个粒子就是一组 ELM 隐层参数第 2 章把 ELM 的前向计算拆开了现在看 PSO 如何与它结合。PSO 中每个粒子是一个一维向量维度 dim (nIn 1) * nHidden。前 nIn * nHidden 个分量按列展开对应输入权重 W最后 nHidden 个分量是隐层偏置 b。这样设计的好处是粒子群不需要了解网络结构只需要在一个连续实数空间里搜索。粒子群算法原理上属于群体智能优化它不依赖目标函数梯度所以对 ELM 这种“随机参数敏感但不连续可导”的场景非常合适。initialization.m 和 Bounds.m 是配套的。initialization.m 在 [lb, ub] 范围内产生初始种群lb 和 ub 是长度为 dim 的向量这两个界限要和 W、b 的取值一致。常见设置是 [-1,1]也可以根据激活函数放宽到 [-5,5]。Bounds.m 的作用是在每次位置更新后检查越界最简单的实现是“超过上界就置为上界低于下界就置为下界”。有些改进版 PSO 会把粒子随机反弹回边界内但在这个资源里直接拉回就能满足多数分类任务。3.2 PSO.m 的速度位置更新与参数含义下面给出 PSO 主循环的常见代码结构和压缩包里的 PSO.m 对应。注意这里把适应度函数句柄 fobj 作为参数传入调用方可以自由切换 fun.mfunction [gbest, gbestval, curve] PSO(fobj, dim, lb, ub, N, T, c1, c2, w) % fobj: 适应度函数句柄, 输入粒子向量, 输出分类错误率 % dim: 粒子维度, lb/ub: 上下界向量, N: 种群规模, T: 迭代次数 X initialization(N, dim, ub, lb); % 初始化位置 V zeros(N, dim); % 初始速度设为 0 pbest X; % 个体最优位置 pbestval zeros(N, 1); for i 1:N pbestval(i) fobj(X(i, :)); % 初始个体适应度 end [gbestval, idx] min(pbestval); gbest X(idx, :); % 全局最优 curve zeros(T, 1); for t 1:T for i 1:N r1 rand(1, dim); r2 rand(1, dim); V(i, :) w * V(i, :) c1 * r1 .* (pbest(i, :) - X(i, :)) ... c2 * r2 .* (gbest - X(i, :)); X(i, :) X(i, :) V(i, :); X(i, :) Bounds(X(i, :), lb, ub); % 边界约束 fval fobj(X(i, :)); if fval pbestval(i) pbest(i, :) X(i, :); pbestval(i) fval; end if fval gbestval gbest X(i, :); gbestval fval; end end curve(t) gbestval; % 记录每代最优错误率 end end这是最经典的速度更新公式。w 是惯性权重控制上一代速度对当前速度的影响c1 是自我学习因子c2 是社会学习因子r1、r2 是 [0,1] 均匀分布的随机数给搜索过程引入随机性。注意速度 V 初始化为全零的做法对常规 PSO 是安全的但面对高维问题收敛会变慢可以把 V 初始化为边界范围的 10%加快早期探索。粒子群优化算法真正影响 ELM 效果的是这些参数与隐层节点数之间的配合而不是单独把某一项调大。参数建议起点对 PSO-ELM 的影响N30太小容易早熟太大会明显增加训练时间T100看 curve 是否平稳不平就加大c1, c22.0, 2.0两者都取 2 是经典配置适用于中小规模特征w0.6固定 0.6 简单稳定递减策略更精细lb/ub-1 到 1与 ELM 输入权重初始化范围保持一致nHidden30后续根据分类精度和适应度曲线再调整3.3 elmtrainNew.m 与最优粒子的组装PSO 迭代结束后gbest 是一组使得验证集错误率最低的 W 和 b。elmtrainNew.m 的作用是避免再次生成随机数直接用 gbest 组装模型。它的代码逻辑是把 gbest 拆成 Wp 和 bp再走一遍 H 的计算最后用全部训练数据求解 betafunction model elmtrainNew(X, Y, nHidden, act, gbest) % 用粒子群结果训练 ELMgbest 是最优粒子的完整向量 nIn size(X, 2); Wp reshape(gbest(1 : nIn * nHidden), nHidden, nIn); bp gbest(nIn * nHidden 1 : end); H X * Wp bp; switch lower(act) case sig H 1 ./ (1 exp(-H)); case sin H sin(H); otherwise H max(H, 0); end beta pinv(H) * Y; model.W Wp; model.b bp; model.beta beta; model.act act; end和 elmtrain.m 相比这个函数只少了rand生成 W 和 b 的步骤其余完全一致。用全部训练数据再算一次 beta是因为 PSO 优化阶段主要利用验证集错误率选参数最终模型应该吃掉所有已知样本。如果只拿部分数据训练最终模型测试集准确率会偏低。这一步是 PSO-ELM 能真正落地应用的关键不要省。3.4 main.m 中的数据划分与调用关系main.m 通常会先读取 data.mat得到样本矩阵 X 和标签矩阵 Y然后把标签转换成 one-hot 编码并划分训练集、验证集和测试集。PSO 阶段使用训练集和验证集测试集只用来评估最终模型不能参与粒子群搜索。否则测试集信息会通过适应度泄漏到模型选择里得到的准确率虚高换到真实新样本马上打回原形。整体调用顺序是main.m 定义 fobj - 调用 PSO.m 得到 gbest - 调用 elmtrainNew.m 得到 model - 调用 elmpredict.m 预测 - 与真实标签对比。一个常见训练比例是训练集 60%、验证集 20%、测试集 20%。小数据集可以放宽到 70%、15%、15%。如果 data.mat 中的样本量本身很小建议使用分层划分而不是简单randperm尽量保证每个类别在三个集合中的比例接近。4. Matlab 环境中复现 PSO-ELM 分类的完整过程与参数调整4.1 运行前准备当前文件夹、数据归一化与随机种子把压缩包里的 main.m、PSO.m、fun.m、elmtrain.m、elmtrainNew.m、elmpredict.m、initialization.m、Bounds.m 和 data.mat 放到同一个目录在 Matlab 2019b 中把当前文件夹切到这个目录。然后先不要急着运行用whos -file data.mat看一下变量名。常见数据格式有两种X 和 Y 已经分开或者 data 结构体里还有 feature、target 字段。变量名写错是最低级的报错却也是最常见的问题来源。数据归一化是容易被忽略的一步。因为 ELM 隐层计算是X*W b如果 X 的不同特征量纲差距很大即使 W 在 [-1,1]加权后的 H 也会被大数值特征主导。我一般用mapminmax把 X 按行缩放到 [0,1]但mapminmax默认处理每一行所以要先转置再转回来load(data.mat); % 假设 data.mat 中包含 X 和 YY 是类别标签列向量 X mapminmax(X, 0, 1); % 转置后归一化再转回 nSample*nFeature rng(1); % 固定随机种子便于结果复现加rng(1)是保证randperm、rand等随机序列可复现。很多人跑同一份 main.m 两次结果不同不是代码坏了而是没有统一随机种子。如果要和标准 ELM 做公平对比随机种子必须在数据划分和粒子初始化之前设置好并且两个算法使用同一套划分子集。4.2 关键参数隐层节点数、种群规模与迭代次数PSO-ELM 需要同时调 ELM 结构参数和粒子群参数。以下是我在类似分类任务上的默认起点不是绝对标准但足够让资源里的 main.m 先跑出合理结果参数建议范围说明nHidden20~100隐层神经元数越多拟合能力越强但 PSO 搜索维度线性变大N20~60种群规模小数据集 20 就够T50~300迭代次数主要看适应度曲线是否平稳c1, c21.5~2.0学习因子通常 c1c22.0w0.4~0.9固定 0.6或从 0.9 线性递减到 0.4速度上限边界范围的 10%~20%防止粒子单步飞过远隐层节点数是最值得观察的参数。nHidden 从 20 增加到 100粒子维度从 (nIn1)*20 变成 (nIn1)*100。如果输入特征有 30 个维度会从 620 变成 3100粒子群在高维空间的搜索效率明显下降。因此并不是隐层越多越好。对 data.mat 这类小规模数据建议固定 nHidden30先把 PSO 超参调顺再加大 nHidden用控制变量法看准确率变化。4.3 训练完成后的结果评估与收敛曲线main.m 跑完后通常会输出训练集准确率和测试集准确率同时把 PSO.m 返回的 curve 画成随迭代次数变化的适应度曲线。如果 curve 在 30 代左右就平稳说明已经收敛继续增大迭代次数收益不大如果 curve 还在持续下降说明最优解没找到应该加大 T或者把 w 从固定值改成线性递减。测试集评估建议写成下面的代码避免 main.m 里残留大量调试变量model elmtrainNew(Xtr, Ytr, nHidden, act, gbest); Yscore elmpredict(model, Xte); [~, Ypredict] max(Yscore, [], 2); % 每行取最大值对应的列 accuracy mean(Ypredict YteLabel) * 100; fprintf(PSO-ELM test accuracy: %.2f%%\n, accuracy);这里的 YteLabel 是测试集的列向量标签Ytr 是 one-hot 编码的训练标签。需要确认主脚本中标签转换的类别顺序是一致的比如把原始标签“1”映射到第一列、“2”映射到第二列否则max得到的列索引和真实标签对不上准确率会出现系统性偏差。资源里的 data.mat 若是二分类还要检查类别是不是 0/1若是多分类建议先unique(Y)看一下类别集合再做 one-hot。4.4 常见报错维度不匹配、NaN 和适应度不下降第一个高频问题出现在粒子维度和边界向量长度不一致。dim 由 nIn 和 nHidden 决定lb、ub 如果用ones(1, dim)就没问题如果写死长度在输入特征数变化时立即报错。第二个高频问题是 H 中出现 NaN通常来自数据里的缺失值或归一化后出现 0 除以 0。Sigmoid 对输入值过大很敏感exp(-H)溢出会产生 Inf建议在训练前检查any(isnan(X(:)))和any(isinf(X(:)))。第三个高频问题与第 2 章呼应如果 elmtrain.m 内部调用rng(default)每次 PSO 评估都会用同一组 W 和 bcurve 永远是一条水平线。解决办法不是简单去掉这一行而是把随机种子管理集中在 main.m 开头训练函数内部不要碰全局随机数状态。5. 把 PSO-ELM 用到真实数据集前的三个验证技巧5.1 固定随机种子并记录多次运行的标准差刚跑通 PSO-ELM 时先用同样的数据连续运行 10 次记录每次测试集准确率。固定随机种子只能保证代码可复现模型的随机性依然存在因此真实评估要用“均值±标准差”来表示。下面的循环比较标准 ELM 与 PSO-ELMaccELM zeros(1, 10); accPSO zeros(1, 10); for rep 1:10 rng(rep); % 不同运行使用不同但确定的种子 % 数据划分、归一化与标签转换写在这里 modelELM elmtrain(Xtr, Ytr, nHidden, act); accELM(rep) mean(max(elmpredict(modelELM, Xte), [], 2) YteLabel); modelPSO elmtrainNew(Xtr, Ytr, nHidden, act, gbest); accPSO(rep) mean(max(elmpredict(modelPSO, Xte), [], 2) YteLabel); end fprintf(ELM: %.2f±%.2f\n, mean(accELM)*100, std(accELM)*100); fprintf(PSO-ELM: %.2f±%.2f\n, mean(accPSO)*100, std(accPSO)*100);如果 PSO-ELM 的标准差明显更小说明粒子群优化提升的是稳定性而不只是某一个随机种子下的运气。5.2 用混淆矩阵查看错误集中的类别准确率只能反映整体情况真实数据集的类别往往不平衡。用confusionmat查看混淆矩阵可以知道哪两类容易被混在一起。如果某个类别的召回率特别低可以考虑在 fun.m 中使用 F1 而不是错误率作为适应度或者在数据划分时做分层抽样。具体命令是C confusionmat(trueLabel, predictedLabel)再结合sum(C, 2)计算每个类别的精确率和召回率这一步对论文里的对比实验很有价值。5.3 最小消融实验ELM 与 PSO-ELM 在相同隐层节点下的对比最后一层验证要回答一个问题粒子群优化带来的提升是否来自隐层节点数变化正确做法是让标准 ELM 和 PSO-ELM 使用完全相同的 nHidden、激活函数、归一化方式和数据划分只改变 W 和 b 的产生方式。如果 PSO-ELM 的准确率更高且标准差更小说明搜索隐层参数是有收益的如果两者几乎一样说明数据本身对隐层参数不敏感此时应该把时间花在特征工程或其他分类器上。做实验时把 nHidden 分别取 20、50、80 各跑一轮画一张折线图就能看到 PSO 优化在不同容量下的真实作用这也是这类源码包最值得深挖的角度。本文还有配套的精品资源点击获取