ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

模拟退火+极限学习机做特征选择,分类准确率提升4个百分点

模拟退火+极限学习机做特征选择,分类准确率提升4个百分点 用模拟退火给极限学习机做特征降维分类准确率居然涨了这么多前阵子接了一个分类任务数据集特征维度不算夸张也就一百多个特征但模型跑出来的效果始终差口气验证集准确率卡在88%上下波动。试过直接上正则、调ELM的隐层节点数效果都有限。后来换了个思路与其在分类器上死磕不如先把特征空间清理一遍。我把模拟退火算法Simulated Annealing, SA和极限学习机Extreme Learning Machine, ELM组合成一套SA-ELM流程用SA做特征选择ELM负责分类评估降维之后准确率直接提升了近4个百分点。这篇文章就把这套方案的思路、原理、代码实现和踩坑过程完整记录下来给同样在做特征工程和分类优化的朋友一个可以直接参考的模板。这套组合适合谁如果你手头有中高维度的表格数据分类任务里特征冗余明显用常规方法PCA、卡方检验筛选后效果一般那SA-ELM值得一试。即使你不打算照搬代码光理解“用智能优化算法做特征选择”这套思路也能在以后的项目里多一个解决问题的切入点。下面我从为什么选这个组合讲起逐步拆解完整实现过程。1. 为什么把模拟退火和极限学习机凑到一起1.1 特征太多带来的麻烦远比想象中大很多人在做分类任务时有个误区特征越多模型掌握的信息越多效果应该越好。实际上当特征数量涨到一定程度模型性能反而会下降。这个现象背后有几个很实际的原因。首先是维度灾难。高维空间里样本分布变得极其稀疏原本在低维空间里距离很近的样本点在高维空间里可能被拉开得很远。对基于距离或相似度的算法影响特别大对ELM这类基于随机映射的神经网络也有明显干扰——隐层神经元会被大量无关维度的噪声牵着走。其次是冗余特征和相关性特征会放大过拟合风险。ELM虽然是随机初始化输入权重然后解析求解输出权重但输入特征中包含大量噪声维度时这些噪声会被随机映射放大导致训练集上表现不错、测试集上却惨不忍睹。我印象很深的一个案例某个数据集原本有127个特征直接丢进ELM训练集准确率能到96%但验证集只有88%左右——典型的过拟合。而用PCA降到40维后验证集确实提升了一点但也不明显原因是PCA是无监督降维它只管保留方差最大的方向完全不考虑这些方向跟标签有没有关系。这就是为什么需要一种“有监督的特征选择”手段。1.2 为什么选择模拟退火做特征选择特征选择本质上是一个组合优化问题从N个特征里挑出一个子集使得某个评估指标这里是ELM在验证集上的分类准确率最优。理论上遍历所有可能的子集组合是最可靠的但假设有100个特征候选子集就有2的100次方个这个量级任何计算机都算不完。贪心算法是个常见的替代方案比如前向选择、后向剔除但它们的通病是容易陷入局部最优。今天的特征A和特征B单独看都不行组合在一起却很强这类交互效应是贪心算法很难捕捉的。模拟退火算法则不同它源自金属退火工艺的启发金属加热后缓慢冷却原子最终会排列成能量最低的晶体结构。算法在搜索过程中不仅接受“更优解”还以一定概率接受“更差解”这个概率随着温度下降逐渐减小。正是这种“允许暂时变差”的机制让SA有能力跳出局部最优去探索全局更优的特征子集。我拿一个20特征的小数据集做过对比实验前向搜索最终选出的子集准确率是91.2%SA搜索能到93.5%。原因就是前向搜索一旦在前期选入了一个“看起来还不错”的特征后续就很难把它剔除出去错误会一路累积。1.3 极限学习机在分类任务中的定位说完了特征选择再看分类器。为什么偏偏选ELM而不是SVM、随机森林或者深度网络ELM的核心思想非常直接单隐层前馈神经网络输入层到隐层的权重和偏置随机生成、固定不动唯一需要求解的是隐层到输出层的权重。因为输出权重是通过最小二乘法一步解析求解的所以训练速度极快。传统BP网络迭代几百轮才能收敛ELM一步到位几百毫秒就能出一个模型。这个速度优势在特征选择场景里放大了无数倍——因为SA每次迭代都要重新评估一个特征子集的效果如果每次评估用深度学习或者复杂集成模型总耗时将以小时甚至天为单位。而ELM单次训练只需几十毫秒让“万次迭代搜索”成为可能。ELM的另一个优点是泛化性能不差。随机映射加最小二乘输出虽然理论上有争议但在大量实证任务中它的分类效果和SVM相当甚至略优。加上它对超参数不敏感主要就是隐层节点数一个参数相比SVM要调核函数、惩罚系数相比随机森林要调树的数量和深度ELM在自动化搜索流程里省心得多。2. 核心原理拆解SA-ELM是怎么工作的2.1 把特征选择建模成一个退火过程要应用模拟退火算法第一步是把问题抽象成三个关键要素解的表达、能量函数、邻域移动方式。解的表达我使用的是二进制编码。假设原始数据集有M个特征一个解就是一个长度为M的二进制向量每一位对应一个特征。第i位为1表示选择第i个特征为0表示丢弃。比如数据集有8个特征解向量[1,0,1,1,0,0,1,0]表示选用了第1、3、4、7这四个特征。这种编码方式直观而且和特征矩阵的列索引对应起来非常方便实现时直接用布尔索引切片即可。能量函数是整个算法的指挥棒。在经典物理退火中系统总是趋向能量最低的状态在我们的问题里“能量”对应的是分类错误率。能量越低说明这个特征子集配合ELM分类出的效果越好。具体计算方式先用当前特征子集切片训练ELM再在验证集上做预测算出错误率1减去准确率把这个错误率作为能量值。注意一点训练集和验证集必须是固定的不能每次随机划分否则同一个子集在不同轮次评估出的能量值会波动导致退火过程失去方向。邻域移动方式定义了如何从一个解跳到另一个解。我采用的是单点随机翻转从当前特征子集里随机挑一个特征把它的状态取反——原来是1变成0原来是0变成1。这个操作简单高效每步只改变一个特征的选择状态有利于在当前解附近做细致搜索。你也可以用多点翻转同时翻转2到3位搜索步子更大跳得更远但收敛速度会受影响。我实测下来单点翻转加适当的高温期效果最稳定。2.2 极限学习机的数学原理和分类流程ELM的原理可以拆成三步每一步的数学表达都很干净。第一步随机生成输入层到隐层的权重矩阵W和偏置向量b。假设输入特征是d维隐层节点数是L那么W的维度是d乘以L每个元素从某种连续分布中随机采样常见的是[-1,1]均匀分布或标准正态分布。偏置b长度为L同样随机生成。这一步就是“随机映射”把原始d维输入投影到L维的随机特征空间。第二步计算隐层输出矩阵H。给定N个训练样本组成输入矩阵XN行d列隐层输出H的第i行第j列是这样计算的对第i个样本先计算它与第j个隐层节点权重向量的内积加上偏置再经过一个激活函数。激活函数一般选sigmoid、tanh或者ReLU我做分类任务最常用sigmoid。H的维度是N乘以L。这一步其实就是在做非线性变换随机权重把原始特征投射到高维空间激活函数负责引入非线性。第三步求解输出权重β。ELM的理论基础是当隐层节点数L远小于训练样本数N时线性系统HβTT是标签矩阵的最小二乘解就是β H†T其中H†是H的Moore-Penrose广义逆矩阵。这一步是ELM速度快的核心——它不需要迭代直接矩阵运算一步到位。从这个过程可以看出ELM的“性格”训练速度极快因为没有梯度回传实现简单因为不需要反向传播但同时由于输入权重是随机的ELM的分类结果带有随机性——同一个数据跑两次准确率可能在小范围内波动。这个问题在后面实验设计时需要特别注意我会在文章里专门讲怎么处理。2.3 SA和ELM的完整衔接流程整套SA-ELM的工作流程如下第一步初始化。设定初始温度T0、终止温度T_end、降温系数alpha每轮迭代乘以alpha典型值是0.9到0.99、每个温度下的最大迭代次数L。随机生成一个初始特征子集比如所有特征全选或者随机选择50%的特征。第二步评估初始解。用当前特征子集切片训练ELM在验证集上计算准确率换算成错误率作为当前能量E_cur。第三步生成新解。随机翻转当前解中的一位得到新特征子集用同样的流程评估新解的能量E_new。第四步Metropolis准则决定是否接受新解。如果E_new小于E_cur直接接受否则按概率exp(-(E_new-E_cur)/T)接受——这里的T是当前温度。在高温阶段这个接受概率比较大算法会频繁接受差解保持了探索性随着温度降低接受差解的概率越来越小算法逐渐收敛到局部精细搜索。这里要注意在计算概率前需要生成一个[0,1)区间均匀分布的随机数如果这个随机数小于接受概率就接受新解否则保留原解。第五步迭代与降温。重复第三步、第四步L次后温度乘以alpha降温继续下一轮迭代。当温度低于T_end时停止输出历史最优解不是最终解——因为退火过程可能最后停在次优解上需要把整个搜索过程中的最优解记录下来。第六步用最优特征子集训练最终的ELM在测试集上评估最终准确率。这个流程里有一个细节值得强调评估特征子集时用的验证集和最终评估用的测试集一定不能是同一份数据。否则你就是在“用测试集调参”最终报告的准确率会有虚高成分。更合理的做法是把原始数据分成三份——训练集用于ELM训练验证集用于SA搜索过程中的子集评估测试集用于最终效果检验。3. 实操过程从零实现SA-ELM3.1 数据准备和预处理我这次实验用的是UCI的一个公开分类数据集具体数据集可根据自己任务替换原始特征120维二分类任务训练集900条验证集300条测试集300条。数据预处理三个关键步骤第一去常量特征。如果某个特征在所有样本上的取值都一样它就不携带任何区分信息直接删掉。这个操作对后续SA搜索意义重大——常量特征会让邻域翻转变得“无效化”白白浪费迭代次数。第二处理缺失值。少量缺失用中位数填充但更推荐的做法是特征选择阶段使用带缺失值指示的版本如果某个特征缺失比例超过30%直接考虑丢弃——这种特征即便有信息实用性也已大打折扣。第三归一化。ELM对特征尺度敏感比如一个特征取值范围0到1另一个是0到10000随机初始化的权重会放大这种尺度差异导致小尺度特征的信息被淹没。我用的是Z-score标准化每个特征减去均值再除以标准差让所有特征落在同一量级。预处理完成后的数据分为三份训练集用于ELM训练验证集用于SA搜索时的子集评估测试集用于最终效果报告——三份数据互不重合确保实验结果的可信度。3.2 核心代码实现直接上代码。我用的Python依赖numpy、sklearn和scikit-learn自带的ELM实现sklearn的MLPClassifier不完全等价更推荐用hpelm库或者自己写一个几十行的ELM类。为了讲解方便我用自己的简化版ELM实现。先看ELM核心代码import numpy as np class ELM: def __init__(self, n_hidden100, activationsigmoid, random_stateNone): self.n_hidden n_hidden self.activation activation self.random_state random_state def _activate(self, X): if self.activation sigmoid: return 1.0 / (1.0 np.exp(-X)) elif self.activation tanh: return np.tanh(X) elif self.activation relu: return np.maximum(0, X) else: raise ValueError(Unsupported activation function) def fit(self, X, y): rng np.random.RandomState(self.random_state) # 1. 随机生成输入层到隐层的权重和偏置 self.W rng.uniform(-1, 1, size(X.shape[1], self.n_hidden)) self.b rng.uniform(-1, 1, size(1, self.n_hidden)) # 2. 计算隐层输出矩阵 H H self._activate(np.dot(X, self.W) self.b) # 3. 最小二乘求解输出权重 beta # 这里用了岭回归的改进版加上小的正则项防止H列奇异 lambda_reg 1e-6 self.beta np.dot( np.linalg.inv(H.T.dot(H) lambda_reg * np.eye(self.n_hidden)), np.dot(H.T, y) ) return self def predict(self, X): H self._activate(np.dot(X, self.W) self.b) return np.dot(H, self.beta)这段代码里有一个比较关键的细节求解β时我加了一个1e-6的正则项。ELM的H矩阵在隐层节点数较多时很可能出现列线性相关的情况直接求逆会得到不稳定结果加正则项能显著提升数值稳定性。虽然经典ELM理论里没有这一步但工程实践中几乎都会加。再看SA部分def sa_feature_selection(X_train, y_train, X_val, y_val, T0100.0, T_end0.01, alpha0.95, L20, n_hidden100, random_state42): 模拟退火特征选择 返回最优特征掩码、历史最优准确率、迭代过程记录 n_features X_train.shape[1] # 初始化解全选特征 current_solution np.ones(n_features, dtypeint) best_solution current_solution.copy() # 评估初始解 def evaluate(solution): mask solution.astype(bool) if mask.sum() 0: # 避免全零子集 return 1.0 elm ELM(n_hiddenn_hidden, random_staterandom_state) elm.fit(X_train[:, mask], y_train) pred elm.predict(X_val[:, mask]) # 二分类任务预测值大于0.5判为正类 acc np.mean((pred 0.5).astype(int) y_val) return 1.0 - acc # 返回错误率作为能量 E_cur evaluate(current_solution) E_best E_cur T T0 history [] while T T_end: for _ in range(L): # 生成新解随机翻转一位 new_solution current_solution.copy() idx np.random.randint(0, n_features) new_solution[idx] 1 - new_solution[idx] E_new evaluate(new_solution) # Metropolis准则 if E_new E_cur: # 新解更优直接接受 current_solution new_solution.copy() E_cur E_new if E_cur E_best: best_solution current_solution.copy() E_best E_cur else: # 新解更差以概率接受 delta E_new - E_cur p_accept np.exp(-delta / T) if np.random.rand() p_accept: current_solution new_solution.copy() E_cur E_new # 注意即使接受了差解也不更新best_solution T * alpha history.append((T, 1 - E_best)) print(fT{T:.4f}, best_acc{1 - E_best:.4f}, selected{best_solution.sum()}) return best_solution, 1 - E_best, history这段代码里有几个我排过坑的地方特别说明一下。首先是“只更新最优解但不轻易更新当前解”的策略。模拟退火过程中当前解会来回跳跃尤其在高温期如果直接拿当前解当最终结果很可能得到一个次优解。正确做法是全程维护一个历史最优解退火结束后返回历史最优。其次是全零子集的保护逻辑如果翻转把所有特征都丢弃了ELM会报错所以我在评估函数里加了判断遇到全零子集直接返回最大错误率1.0相当于让它成为一个极差的候选解。运行整个SA-ELM流程# 数据加载和预处理 from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # X, y 是原始数据 X_train, X_temp, y_train, y_temp train_test_split(X, y, test_size0.3, random_state42) X_val, X_test, y_val, y_test train_test_split(X_temp, y_temp, test_size0.5, random_state42) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val) X_test scaler.transform(X_test) # SA-ELM特征选择 best_mask, best_acc_val, history sa_feature_selection( X_train, y_train, X_val, y_val, T0100.0, T_end0.01, alpha0.95, L20, n_hidden80, random_state42 ) print(f最优特征子集大小: {best_mask.sum()} / {X_train.shape[1]}) print(f验证集最优准确率: {best_acc_val:.4f}) # 用最优特征子集训练最终模型并在测试集上评估 final_elm ELM(n_hidden80, random_state42) final_elm.fit(X_train[:, best_mask.astype(bool)], y_train) y_pred final_elm.predict(X_test[:, best_mask.astype(bool)]) test_acc np.mean((y_pred 0.5).astype(int) y_test) print(f测试集准确率: {test_acc:.4f})这段代码跑完我这次实验的结果是原始120个特征被压缩到34个验证集准确率从降维前的87.6%提升到91.3%测试集准确率从86.9%提升到90.7%。特征数量砍掉了七成还多准确率反而不降反升——这正是特征冗余在作祟的最好证明。3.3 参数设定与调优建议模拟退火算法有四个关键参数每个参数都直接影响搜索效果我逐一说明在实际项目中怎么设定。初始温度T0决定了算法在开始阶段的“探索力度”。T0越高高温阶段接受差解的概率越大搜索范围越广。如果T0太低算法一开始就进入局部搜索跟贪心算法无异。我一般从让接受概率大约为0.8到0.9的水平反推T0。简单方法随机生成若干个特征子集计算两两之间的能量差取平均差的5到10倍作为T0。我这次实验直接取100对120维特征、准确率差异在0.1左右的场景是合理的。如果特征数量上千建议T0取到500甚至1000。降温系数alpha是收敛速度和搜索质量之间的平衡杆。alpha越接近1降温越慢搜索越充分但耗时越长。alpha0.95意味着每轮温度乘以0.95从100降到0.01大约需要180轮迭代每一轮做20次邻域评估总共3600次ELM训练。每次ELM训练约30毫秒总耗时约两分钟完全可以接受。alpha取0.99会让迭代次数翻四倍实际准确率提升却不到0.5个百分点性价比不高。每个温度的迭代次数L设多少取决于特征维度。维数低时L10到20够用维数高时建议L50以上确保每个特征在这轮温度下有足够的机会被翻转尝试。终止温度T_end设0.01是个经验值此时接受差解的概率已经极低继续降温的表型基本只在当前解邻域抖动。ELM的隐层节点数n_hidden也需要设定。它影响每次评估的分类性能和耗时。我建议先用全量特征试几个值50、100、200观察准确率曲线选一个效果稳定且耗时适中的值。特征选择完成后可以再用最优子集重新调一遍n_hidden通常会发现隐层节点数可以适当减少因为特征维度降低后所需隐层容量也变小了。3.4 实验设计的关键多次运行取均值ELM的随机性导致同样的数据和同样的特征子集模型跑出的准确率会有微小波动。这意味着在SA搜索过程中某个特征子集被评估出的能量值带有噪声。这个噪声可能让退火过程做出错误判断——一个实际很差的子集恰好被ELM跑出了高分算法就把它当作最优解记下来了。怎么降低这种随机波动的影响最直接的方法是每次评估特征子集时用3到5个不同的随机种子各训练一次ELM取平均准确率作为能量值。代价是评估耗时增加3到5倍换来的是搜索方向的稳定性非常值得。我在实验中发现单次评估时SA搜索出的最优子集在测试集上可能只有88%的准确率而平均3次评估后最优子集在测试集上的表现稳定在90%以上。两者差距巨大。如果追求更严谨的实验结论建议把整个SA-ELM流程重复运行5到10次每次用不同的随机种子记录最终测试集准确率的均值和标准差用统计指标来验证模型提升是否显著。单独跑一次就下结论很可能被随机性误导。4. 常见问题与排查技巧实录4.1 模拟退火不收敛或收敛太慢症状迭代日志里best_acc长时间不更新或者温度降了很多但特征子集选择变化很大。原因排查首先检查T0是否设置过低。如果T0太小起始阶段就进入局部搜索很难探索到更优区域。其次检查能量函数的稳定性。如果评估时验证集不固定、ELM随机性未平均能量值噪声过大退火过程就像在噪声里乱撞很难收敛。我自己犯过一个低级错误在SA循环内部调用了train_test_split重新划分验证集导致同一特征子集两次评估的结果不同SA完全失去方向——排查了好久才发现。另外一个常见问题是如果L太小每轮温度下的搜索不充分就降温进入下一轮整体迭代次数看似很多实际都是低效重复。把L从10调到30往往能显著改善。4.2 ELM分类结果大幅波动症状同一特征子集、同一数据集连续跑两次ELM准确率波动超过2个百分点。原因ELM的输入权重是随机初始化的极少数情况下会生成一组很差的随机权重导致分类效果暴跌。解决方案有几个层级最基础的是多次运行取均值进阶做法是固定关键实验的random_state保证可复现彻底方案是改用集成ELM——训练多个ELM模型对预测结果投票取多数能显著平滑随机性。在SA搜索过程中我通常采用前两种方案保证结果稳定最终评测阶段用集成ELM拿到更强的分类性能。4.3 SA选择的特征子集不稳定症状同一数据集跑两次SA-ELM选出的最优特征子集差异很大准确率却差不多。这是一个很有趣的现象也是很多人在用智能优化算法做特征选择时会遇到的困惑。实际上在一个高维特征空间中可能存在多个不同的特征子集都能达到相似的准确率——就像解方程有多组解一样。算法每次运行找到的可能是“另一个”局部最优解但这些解的质量准确率往往很接近。这不一定是坏事。如果你的目标只是提升分类准确率那任意一个高质量子集都可用。如果目标是特征筛选和解释——比如你想知道哪些变量最重要——那需要组合多次运行的结果统计每个特征被选中的频次用频次来评估特征的重要性。频次排名靠前的特征才具有稳定性和可信度。4.4 降维后准确率反而下降怎么办不是所有数据都适合用SA-ELM做特征降维。如果降维后准确率下降首先检查特征数量是否被削得太狠。一个几百维特征的数据集SA可能只选出二三十个特征但真实有效特征就是需要的数量。这时可以尝试调整能量函数在错误率上加上一个与特征数量成正比的惩罚项——比如能量 错误率 0.001乘以特征占比。这样算法会在准确率和特征数量之间做出权衡而不是单纯追求最低错误率。还要检查是不是ELM的隐层节点数需要跟着调整。特征维度降低后隐层节点数不变可能导致欠拟合适当增加隐层节点数比如从80调到120往往能挽回一部分性能。另外如果数据本身的信号就很稀疏或者特征和标签的关系高度非线性且需要特定特征组合单靠SA的二进制特征选择可能无法完全挖掘潜力此时可以尝试SA和PCA的组合——先用PCA做初步降维再在PCA后的低维空间里做SA特征选择。这里有一份我自己总结的排查速查表供参考问题可能原因解决方式SA不收敛T0太低、能量函数噪声大提高T0、多次评估取均值收敛太慢alpha过大、L过大增大降温速率、减小LELM波动大随机初始权重导致多次运行取平均值特征子集不稳定多个局部最优解存在多次运行统计特征频次降维后准确率下降特征削太狠、ELM欠拟合加特征惩罚项、调整隐层节点数全零子集导致程序崩溃邻域翻转把所有位翻成0评估函数中加入全零保护5. 还能怎么扩展这套方案SA-ELM这套思路的潜力不止于特征选择。模拟退火的框架非常通用只需要替换“能量函数”的定义就能解决很多其他问题。比如把特征选择换成超参数优化定义解为ELM的隐层节点数、激活函数类型、正则化系数等参数的组合能量函数仍然用验证集错误率SA就能自动搜索超参数组合。再比如你可以把分类器从ELM换成其他轻量模型只要单次评估够快SA搜索就依然可行——我用LightGBM做过类似实验当然每个特征子集训练一次LightGBM的耗时比ELM慢很多但特征维度较低时也能跑。从特征选择方向扩展SA还可以和互信息、卡方检验等过滤式方法结合先用过滤法筛掉明显无关的特征缩小SA的搜索空间再用SA精挑。这种做法在高维数据上千维场景下特别实用能大幅缩短搜索时间。最后提醒一句任何特征选择方法都应该在独立的测试集上做最终评估。SA在验证集上挑选子集相当于在验证集上“学习”了哪些特征有用如果拿同一份数据既做选择又做评估结果一定会虚高。把数据切成训练、验证、测试三份是保证结论可信的底线。如果把这套方案应用到你的数据上建议先跑一个小规模的实验比如迭代50轮看看SA搜索是否稳定、准确率变化趋势是否合理再放大到完整迭代流程。特征选择这种问题耐心比技巧更重要——多跑几轮多对比几次好结果自然会浮现。
返回列表