简介:这份资源面向具备一定Python与机器学习基础、希望掌握超参数调优实战的学习者,聚焦用遗传算法(GA)自动搜索支持向量机(SVM)的最优参数组合,解决人工调参效率低、易陷入局部最优的问题。压缩包共5个文件,含3个py脚本、1个xlsx数据集与1份pdf说明文档,整体约1.45MB,脚本分别承担主流程调度、问题建模与数据预处理等职责,数据集可直接用于复现实验。内容按需求分析、数据采集、数据预处理、探索性数据分析、特征工程、机器建模、模型评估到实际应用八个环节完整展开,读者可据此理解GA的编码、适应度设计与选择交叉变异流程,并对照SVM分类结果评估优化效果。目前已有4334人学习下载,适合作为课程设计、毕业项目或算法调参入门的参考范例。
1. 遗传算法调 SVM 参数:为什么网格搜索该让位了
跑过 SVM 分类的人多半有过这种体验:GridSearchCV一把梭,参数网格稍微铺大一点,笔记本风扇就开始狂转,等半小时出来一组C=100, gamma=0.001,换份数据又得重来。问题不在于网格搜索错,而在于它把每个参数组合都当成独立事件去穷举,维度一高就指数爆炸。遗传算法(GA)换了个思路——它不遍历,它进化。把 SVM 的惩罚系数C、核函数系数gamma编码成一条“染色体”,让一群候选解自己交叉、变异、优胜劣汰,几十代之后往往能逼近甚至超过网格搜索的最优解,而评估次数可能只有后者的几分之一。
这篇笔记讲的就是怎么用 Python 把 GA 和 SVM 接起来,做一套能直接跑在自己数据上的参数优化流程。适合两类人:一类是已经会用sklearn跑 SVM、但被调参拖慢节奏的;另一类是听说过遗传算法原理、却不知道怎么把它落到真实分类任务上的。核心链路是sklearn的 SVM 做分类器、deap或手写 GA 做外层搜索、交叉验证做适应度评估。下面从编码方式讲到能复现的代码,再讲我踩过的坑。
2. GA 优化 SVM 的编码与适应度设计
2.1 为什么选 GA 而不是网格或贝叶斯
先把选型理由说清楚,不然代码写了也不知道边界在哪。网格搜索的复杂度是各参数取值个数的乘积,C取 10 个值、gamma取 10 个值就是 100 次训练;如果再加上核函数类型、degree,轻松上千次。贝叶斯优化(比如optuna)在低维连续空间里很省评估次数,但它对参数空间的先验假设较强,遇到C和gamma这种跨几个数量级、且对核函数类型敏感的场景,代理模型的拟合未必稳。
GA 的优势在于它对目标函数几乎不做假设,只要求你能算出一个适应度分数。它天然支持混合编码——C、gamma用实数,核函数类型用整数或类别,degree用整数,一条染色体里可以塞不同类型。代价是 GA 本身有随机性,需要控制种群规模和代数,否则评估次数也会上去。我的经验是:参数维度在 2 到 6 个、单次训练在秒级、总评估预算在几百到两千次之间时,GA 的性价比最高。
2.2 染色体怎么编码:实数、整数与类别混合
SVM 常用参数里,C和gamma是正实数,跨度大,通常取对数后再编码;kernel是类别(rbf、linear、poly、sigmoid);degree只在poly核下有意义,是 2 到 5 的整数;coef0在poly和sigmoid下有用。如果全用二进制编码,精度和长度很难平衡;全用实数编码,又不好表达类别。
我一般用混合编码:C和gamma用实数基因,但实际传给 SVM 前做10**x变换,这样搜索空间在对数尺度上均匀;kernel用整数基因,取值 0 到 3 映射到四个核;degree用整数基因。这样一条染色体就是[logC, logGamma, kernelIdx, degree],长度固定,交叉变异都好写。
import numpy as np from sklearn.svm import SVC from sklearn.model_selection import cross_val_score # 基因边界:log10(C) 在 [-3, 3],log10(gamma) 在 [-5, 1] # kernelIdx 0~3 对应 rbf/linear/poly/sigmoid,degree 2~5 BOUNDS = [(-3.0, 3.0), (-5.0, 1.0), (0, 3), (2, 5)] KERNELS = ['rbf', 'linear', 'poly', 'sigmoid'] def decode(individual): logC, logGamma, k_idx, degree = individual C = 10 ** logC gamma = 10 ** logGamma kernel = KERNELS[int(round(k_idx)) % 4] degree = int(round(degree)) return C, gamma, kernel, degree def fitness(individual, X, y, cv=5): C, gamma, kernel, degree = decode(individual) # linear 核不用 gamma,poly 核才用 degree if kernel == 'linear': model = SVC(C=C, kernel=kernel) elif kernel == 'poly': model = SVC(C=C, kernel=kernel, gamma=gamma, degree=degree) else: model = SVC(C=C, kernel=kernel, gamma=gamma) scores = cross_val_score(model, X, y, cv=cv, scoring='f1_weighted') return scores.mean(),这段代码里decode负责把基因翻译成 SVM 能吃的参数,fitness用 5 折交叉验证的加权 F1 作为适应度。注意fitness返回的是元组,这是deap的约定,手写 GA 时可以直接返回浮点数。scoring选什么很关键:类别不平衡就用f1_weighted或roc_auc,均衡数据用accuracy也行,但别默认 accuracy,否则少数类会被牺牲掉。
2.3 适应度函数里交叉验证的折数与评分选择
交叉验证折数cv直接决定单次适应度评估的成本。cv=5是常见起点,数据量小于 500 时可以用cv=10或留一法,但评估次数会翻倍。如果数据有分组结构(比如同一患者多条记录),必须用GroupKFold,否则交叉验证会泄漏,GA 会朝着过拟合的方向进化,最后给你一组在训练集上漂亮、上线就崩的参数。
评分指标要和业务目标对齐。二分类且正类重要,用f1或average_precision;多分类用f1_macro或f1_weighted;如果只关心排序,用roc_auc。我见过有人用accuracy调 SVM,结果在不平衡数据上 GA 收敛到“全预测多数类”的参数,交叉验证分数还挺高,这就是指标选错的典型翻车。
提示:适应度函数里不要做任何数据泄漏的操作,比如先对全量数据做标准化再交叉验证。标准化必须放进
Pipeline,让每一折自己拟合。
3. 用 deap 搭一套能跑的 GA-SVM 流程
3.1 安装依赖与最小可运行骨架
先把环境弄干净。deap是遗传算法常用库,sklearn负责 SVM 和交叉验证,numpy做数值。安装命令如下:
pip install deap scikit-learn numpy pandas如果用的是conda,把pip换成conda install -c conda-forge deap scikit-learn也行。装完先跑一个最小骨架,确认 GA 能驱动 SVM 训练,再往里面加复杂度。
import random import numpy as np from deap import base, creator, tools, algorithms from sklearn.datasets import load_breast_cancer from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.model_selection import cross_val_score # 1. 准备数据 data = load_breast_cancer() X, y = data.data, data.target # 2. 定义适应度:内部用 Pipeline 防止标准化泄漏 def eval_svm(individual): logC, logGamma, k_idx, degree = individual C = 10 ** logC gamma = 10 ** logGamma kernel = ['rbf', 'linear', 'poly', 'sigmoid'][int(round(k_idx)) % 4] degree = int(round(degree)) if kernel == 'linear': clf = SVC(C=C, kernel=kernel) elif kernel == 'poly': clf = SVC(C=C, kernel=kernel, gamma=gamma, degree=degree) else: clf = SVC(C=C, kernel=kernel, gamma=gamma) pipe = Pipeline([('scaler', StandardScaler()), ('svm', clf)]) score = cross_val_score(pipe, X, y, cv=5, scoring='f1').mean() return score, # 3. 定义个体和种群 creator.create('FitnessMax', base.Fitness, weights=(1.0,)) creator.create('Individual', list, fitness=creator.FitnessMax) BOUNDS = [(-3.0, 3.0), (-5.0, 1.0), (0, 3), (2, 5)] def init_individual(): return creator.Individual([ random.uniform(*BOUNDS[0]), random.uniform(*BOUNDS[1]), random.randint(*BOUNDS[2]), random.randint(*BOUNDS[3]), ]) toolbox = base.Toolbox() toolbox.register('individual', init_individual) toolbox.register('population', tools.initRepeat, list, toolbox.individual) toolbox.register('evaluate', eval_svm) toolbox.register('mate', tools.cxBlend, alpha=0.5) toolbox.register('mutate', tools.mutGaussian, mu=0, sigma=0.5, indpb=0.2) toolbox.register('select', tools.selTournament, tournsize=3) # 4. 跑 GA pop = toolbox.population(n=30) hof = tools.HallOfFame(1) stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register('max', np.max) stats.register('avg', np.mean) pop, log = algorithms.eaSimple( pop, toolbox, cxpb=0.6, mutpb=0.3, ngen=20, stats=stats, halloffame=hof, verbose=True ) print('最优个体:', hof[0]) print('最优适应度:', hof[0].fitness.values[0])这段骨架里几个关键点:creator.create定义了最大化适应度;init_individual按边界随机初始化;cxBlend是混合交叉,适合实数基因;mutGaussian做高斯变异;selTournament是锦标赛选择。跑完eaSimple会打印每代的max和avg,如果avg长期不涨、max早早停滞,说明种群多样性不够或者变异率太低。
3.2 种群、交叉率、变异率的取值经验
参数没有万能值,但有几条经验线。种群规模n=30到50是常见起点,太小容易早熟,太大评估次数浪费。代数ngen=20到50,配合早停(连续几代max不提升就停)更省时间。交叉率cxpb=0.6到0.9,变异率mutpb=0.1到0.3。变异率太高会退化成随机搜索,太低则跳不出局部最优。
mutGaussian的sigma控制变异步长。对logC、logGamma这种对数尺度基因,sigma=0.5意味着每次变异大约在 10 的 0.5 次方即 3 倍左右浮动,比较合理;sigma=2就太猛,等于乱跳。indpb是每个基因独立变异的概率,0.2 表示平均每条染色体有 0.8 个基因变异,对 4 基因的个体来说节奏刚好。
如果发现 GA 收敛太快,先加种群规模,再加变异率,最后考虑换选择策略。锦标赛的tournsize越大,选择压力越大,收敛越快但多样性掉得也快,tournsize=3是平衡点。
3.3 早停、日志与最优个体保存
eaSimple没有内置早停,生产里我一般自己写循环,记录每代最优,连续patience=5代不提升就break。日志用tools.Statistics注册max、avg、std,std能反映种群多样性,掉到接近 0 就该停了。
最优个体保存用HallOfFame,它会在每代结束后保留适应度最高的个体。注意HallOfFame保存的是个体对象,如果后续还要用,最好把解码后的参数和适应度一起写进文件,避免进程结束丢失。
import json best = hof[0] C, gamma, kernel, degree = decode(best) result = { 'C': C, 'gamma': gamma, 'kernel': kernel, 'degree': degree, 'f1_cv': best.fitness.values[0] } with open('best_svm_params.json', 'w') as f: json.dump(result, f, indent=2) print(result)把结果落盘是个好习惯,GA 跑一次可能几分钟到几十分钟,别让结果只活在内存里。decode函数复用第 2 章的版本,保证编码解码一致。
4. 避坑与排查:GA-SVM 最常见的五类翻车
4.1 适应度不升反降或剧烈震荡
现象:每代max忽高忽低,avg没有上升趋势。原因通常是交叉验证折数太少(比如cv=2)导致评估噪声大,或者数据没打乱、存在顺序偏差。解决:把cv提到 5 或 10,交叉验证前用KFold(shuffle=True, random_state=42)固定随机种子,保证同一代内不同个体的评估条件一致。
4.2 最优参数落在边界上
现象:跑完发现logC正好等于 3.0 或logGamma等于 -5.0,贴着边界。原因是你给的搜索边界太窄,真正的最优在边界外。解决:先看边界值对应的适应度是否还在上升,如果是,把边界往外扩一到两个数量级再跑。但别无限扩,C大到 1e6 以上基本就是过拟合信号,要回头检查特征和核函数选择。
4.3 训练集分数高、测试集崩
现象:GA 报出的交叉验证 F1 有 0.95,换测试集只有 0.6。原因多半是数据泄漏——标准化、特征选择在交叉验证外做了,或者同一实体的样本被分到了训练和验证两边。解决:所有预处理塞进Pipeline,分组数据用GroupKFold,时间序列用TimeSeriesSplit。这一步不做,GA 越努力,过拟合越严重。
4.4 运行时间失控
现象:跑了一晚上还没结束。原因:种群大、代数多、cv高、数据量大,四者相乘。解决:先用小种群(n=20)、少代数(ngen=10)、cv=3做一次快速探索,看适应度范围;再逐步加预算。另外SVC在样本量超过几万时本身很慢,考虑换LinearSVC或先降维。
4.5 核函数选择被 GA 带偏
现象:GA 总是选linear核,但数据明显非线性。原因:linear核训练快,在早期代数里适应度略高就被大量繁殖,挤掉了rbf的探索机会。解决:给不同核函数加一个小的适应度惩罚或奖励,或者在初始化时强制每个核至少出现几次。更简单的办法是分两阶段:先固定rbf调C和gamma,再单独比较核函数。
5. 把 GA-SVM 用稳的三个进阶习惯
第一个习惯是固定随机种子并做多次重复。GA 是随机算法,单次结果有波动。我一般用不同seed跑 3 到 5 次,取适应度中位数对应的参数,而不是盲目取最高分。最高分那次可能是运气好,中位数更稳。
from sklearn.model_selection import StratifiedKFold def eval_svm_robust(individual, X, y, seeds=(42, 7, 2024)): scores = [] for s in seeds: cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=s) # ... 同 eval_svm 内部逻辑,把 cross_val_score 的 cv 换成这个 cv return np.mean(scores),第二个习惯是把 GA 的搜索结果和网格搜索做一次小范围对照。不是全量网格,而是在 GA 最优解附近取一个小网格,确认 GA 没有漏掉明显更好的点。这一步能帮你判断 GA 是真的找到了好区域,还是只是随机撞上了一个局部峰。
第三个习惯是记录每次实验的配置。种群、代数、交叉率、变异率、cv、评分指标、随机种子,全部写进日志文件。GA 调参本身也是个调参过程,没有记录就没法复现,更没法迭代。
| 配置项 | 快速探索 | 正式运行 | 说明 |
|---|---|---|---|
| 种群规模 | 20 | 40~50 | 越大越稳,评估次数线性增长 |
| 代数 | 10 | 30~50 | 配合早停,别硬跑满 |
| 交叉率 | 0.6 | 0.7~0.9 | 太低进化慢 |
| 变异率 | 0.2 | 0.1~0.2 | 太高退化成随机搜索 |
| 交叉验证折数 | 3 | 5~10 | 折数越高噪声越小 |
| 重复种子数 | 1 | 3~5 | 取中位数更稳 |
最后说个我自己的教训:早期我图省事,把标准化放在 GA 外面,全量数据先fit_transform再交叉验证,结果 GA 报出的 F1 虚高,上线后掉了一大截。后来所有预处理都塞进Pipeline,分数虽然降了一点,但和线上表现对得上了。GA 也好,贝叶斯也好,搜索算法只能在你给的评估标准里找最优,评估标准本身有泄漏,它就会朝着错误方向拼命进化。把评估做干净,比换更花哨的优化器重要得多。希望帮到你。
本文还有配套的精品资源,点击获取