简介:面向数据分类任务,粒子群算法优化极限学习机的完整实现,适合正在从事机器学习、模式识别相关课题的学生与工程师,用于解决传统极限学习机因随机设置输入权值和阈值而导致分类精度不稳定的问题。压缩包共12个文件,以m脚本为主,包含主程序、粒子群寻优、极限学习机训练与预测等模块,另有效果图、使用说明和mat数据集,各部分分工明确,便于单独调用或改造,整体体积仅133KB。目前已有153人学习,程序基于Matlab平台开发,可直接运行并替换为自己的样本数据和标签开展实验。借助粒子群算法自动搜索输入权值与隐含层阈值,可对比普通极限学习机和优化后的分类性能;配套效果图与使用说明能帮助理解收敛过程、参数设置对结果的影响。无论是课程设计、毕业设计,还是作为科研预实验中的分类基线,这套代码都具备较好的可复用性与参考价值。
1. 为什么 ELM 还需要粒子群算法优化
第一次拿极限学习机做分类实验的人,都会先被它的训练速度吸引:单个隐层、随机固定输入权重,输出权重用最小二乘一步算出,几十毫秒就能出一个模型。但多跑几次随机种子会发现,分类准确率跟着“随机情绪”走,同一份数据结果可能差出三五个百分点。原因就出在隐层参数随机生成,不一定落在合适的区域。粒子群优化极限学习机(PSO-ELM)的思路很直接:把输入权重和偏置当成粒子位置,用粒子群算法(PSO)搜出一组更稳定的隐层参数,再交给 ELM 用解析方法计算输出权重。这是一份适合做分类实验、论文对比和课程设计的资源,Matlab 2019b 环境下可以直接运行,也可以作为进一步改进的基线。
2. 极限学习机的前向计算与 elmtrain.m 的源码位置
2.1 单隐层网络为什么能把训练问题转成解线性方程
ELM 针对的是单隐层前馈神经网络。假设输入矩阵 X 维度是 nSample×nFeature,隐层神经元个数是 nHidden,随机初始化输入权重 W(nHidden×nFeature)和偏置 b(nHidden×1),隐层输出矩阵 H = g(XW' + b) 的维度是 nSample×nHidden。这一步之后,网络里还没有学到的只剩输出权重 beta。ELM 的核心观点是:当 W 和 b 固定时,训练目标就是让 Hbeta 尽量接近标签矩阵 Y,于是 beta 可以通过岭回归或 Moore-Penrose 伪逆直接求出,即 beta = pinv(H)*Y。这个过程不需要梯度回传,也不需要迭代,这就是极限学习机训练速度快的根本原因。
在 PSO-ELM 中,这个性质依然保留。粒子群只负责优化 W 和 b,beta 仍然由伪逆一次解出,所以 PSO 每次评估一个粒子的代价,只是执行一次矩阵乘法和一次伪逆,计算量完全可控。这也是为什么压缩包里的 PSO 主循环可以和 ELM 训练函数频繁互相调用,而不会出现传统神经网络嵌入 PSO 后那种训练时间爆炸的问题。
2.2 elmtrain.m 与 elmpredict.m 的常规实现
压缩包里同时存在 elmtrain.m 和 elmtrainNew.m,很多第一次打开资源的人会困惑:到底该用哪一个?我拆过的同类项目里,命名习惯通常是:elmtrain.m 是标准 ELM 训练函数,随机产生 W 和 b;elmtrainNew.m 接收外部传入的 W 和 b,适合 PSO 迭代结束后用最优粒子组装模型。下面这段代码是标准 elmtrain.m 的常见写法,和资源中的函数名对应:
function model = elmtrain(X, Y, nHidden, act) % X: nSample*nFeature, Y: nSample*nClass,nClass 为类别数 % 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 和 b,PSO 的适应度曲线会退化成一条水平直线。看到这个现象时,第一件事就是去检查训练函数内部是否重置了随机数生成器。
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,计算 beta |
| elmpredict.m | 用训练好的模型预测,输出各类别得分 |
| elmtrainNew.m | 用 PSO 得到的最优粒子作为 W、b,重新计算 beta |
| fun.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.m:
function [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 的影响 |
|---|---|---|
| N | 30 | 太小容易早熟,太大会明显增加训练时间 |
| T | 100 | 看 curve 是否平稳,不平就加大 |
| c1, c2 | 2.0, 2.0 | 两者都取 2 是经典配置,适用于中小规模特征 |
| w | 0.6 | 固定 0.6 简单稳定,递减策略更精细 |
| lb/ub | -1 到 1 | 与 ELM 输入权重初始化范围保持一致 |
| nHidden | 30 | 后续根据分类精度和适应度曲线再调整 |
3.3 elmtrainNew.m 与最优粒子的组装
PSO 迭代结束后,gbest 是一组使得验证集错误率最低的 W 和 b。elmtrainNew.m 的作用是避免再次生成随机数,直接用 gbest 组装模型。它的代码逻辑是把 gbest 拆成 Wp 和 bp,再走一遍 H 的计算,最后用全部训练数据求解 beta:
function model = elmtrainNew(X, Y, nHidden, act, gbest) % 用粒子群结果训练 ELM,gbest 是最优粒子的完整向量 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 和 Y,Y 是类别标签列向量 X = mapminmax(X', 0, 1)'; % 转置后归一化,再转回 nSample*nFeature rng(1); % 固定随机种子,便于结果复现加rng(1)是保证randperm、rand等随机序列可复现。很多人跑同一份 main.m 两次结果不同,不是代码坏了,而是没有统一随机种子。如果要和标准 ELM 做公平对比,随机种子必须在数据划分和粒子初始化之前设置好,并且两个算法使用同一套划分子集。
4.2 关键参数:隐层节点数、种群规模与迭代次数
PSO-ELM 需要同时调 ELM 结构参数和粒子群参数。以下是我在类似分类任务上的默认起点,不是绝对标准,但足够让资源里的 main.m 先跑出合理结果:
| 参数 | 建议范围 | 说明 |
|---|---|---|
| nHidden | 20~100 | 隐层神经元数,越多拟合能力越强,但 PSO 搜索维度线性变大 |
| N | 20~60 | 种群规模,小数据集 20 就够 |
| T | 50~300 | 迭代次数,主要看适应度曲线是否平稳 |
| c1, c2 | 1.5~2.0 | 学习因子,通常 c1=c2=2.0 |
| w | 0.4~0.9 | 固定 0.6,或从 0.9 线性递减到 0.4 |
| 速度上限 | 边界范围的 10%~20% | 防止粒子单步飞过远 |
隐层节点数是最值得观察的参数。nHidden 从 20 增加到 100,粒子维度从 (nIn+1)*20 变成 (nIn+1)*100。如果输入特征有 30 个,维度会从 620 变成 3100,粒子群在高维空间的搜索效率明显下降。因此并不是隐层越多越好。对 data.mat 这类小规模数据,建议固定 nHidden=30,先把 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 和 b,curve 永远是一条水平线。解决办法不是简单去掉这一行,而是把随机种子管理集中在 main.m 开头,训练函数内部不要碰全局随机数状态。
5. 把 PSO-ELM 用到真实数据集前的三个验证技巧
5.1 固定随机种子并记录多次运行的标准差
刚跑通 PSO-ELM 时,先用同样的数据连续运行 10 次,记录每次测试集准确率。固定随机种子只能保证代码可复现,模型的随机性依然存在,因此真实评估要用“均值±标准差”来表示。下面的循环比较标准 ELM 与 PSO-ELM:
accELM = 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 优化在不同容量下的真实作用,这也是这类源码包最值得深挖的角度。
本文还有配套的精品资源,点击获取