超参数调整专题3
1. 三种启发式算法的思想:遗传算法、粒子群算法、退火算法
2. 列表推导式

这张图展示的是二元函数 z=f(x,y) 的三维曲面,用来直观解释函数的极值(极大值、极小值)与最值(最大值、最小值) 的概念,核心是区分 “局部” 和 “全局” 的高低 / 深浅。
1. 坐标系与函数形态
- 水平面的 x 轴、y 轴是自变量的取值范围,垂直的 z 轴是函数值(可以理解为 “高度” 或 “深度”)。
- 整个曲面 z=f(x,y) 可以想象成 “起伏的山地”—— 有山峰、山谷,也有全局的最高山和最低谷。
2. 极大值 vs 最大值
- 极大值:曲面上的 “局部山峰”(如图中两处标注 “极大值” 的区域)。它们在自己周围的小范围内是最高的,但不一定是整个曲面的最高点。
- 最大值:曲面上的 “全局最高峰”(如图中标注 “最大值、极大值” 的区域)。它既是一个局部极大值,也是整个函数定义域内的最高值。
3. 极小值 vs 最小值
- 极小值:曲面上的 “局部山谷”(如图中两处标注 “极小值” 的区域)。它们在自己周围的小范围内是最低的,但不一定是整个曲面的最低点。
- 最小值:曲面上的 “全局最深谷”(如图中标注 “最小值、极小值” 的区域)。它既是一个局部极小值,也是整个函数定义域内的最低值。
总结
这张图用 “山地模型” 把抽象的数学概念可视化:
- 局部的 “山峰 / 山谷” 对应极大值 / 极小值(只在小范围里最优);
- 全局的 “最高峰 / 最深谷” 对应最大值 / 最小值(在整个范围内最优)。这种可视化能帮助理解 “为什么有些算法会陷入局部最优(比如困在一个小山峰里,以为是最高点),而我们需要找全局最优(真正的最高峰)”。
启发式算法
一、遗传算法:模仿生物进化的 “优胜劣汰”
遗传算法思想
灵感来源:达尔文的生物进化论(物竞天择,适者生存)。核心思想:把 “问题的每个可能解” 当成一个 “生物个体”,通过 “繁殖、变异” 让好的解留下,差的解淘汰,一代代进化出更好的解。
可以想象成 “养一群会解题的小生物”:
- 每个小生物的 “基因” 就是它的解题方案(比如解一个 “怎么安排路线最短” 的问题,基因就是具体的路线顺序);
- 先随机生成一群 “初始生物”(随机的路线方案),然后给它们打分(比如路线越短,分数越高,也就是 “适应度” 越高);
- 让分数高的生物更容易 “生孩子”(选择优质个体繁殖),孩子的基因会混合父母的基因(比如爸爸的前半段路线 + 妈妈的后半段路线,这叫 “交叉”);
- 偶尔孩子的基因会随机变一下(比如突然换了一个路口,这叫 “变异”),防止大家都困在同一个方案里;
- 重复上面的过程,过几代之后,剩下的生物基本都是 “高分选手”,它们的基因就是近似最优解了。
用 “学生考试” 的例子来一步步解释
1. 随机初始化 n 组参数 ——“召集第一批考生”
假设我们要解决一个问题:比如 “调整 3 个参数(比如学习时间、做题数量、休息时长),让考试分数最高”。“随机初始化 n 组参数” 就是先随便找 5 个学生(n=5),每个学生都有自己的 3 个参数组合(比如 I1: 学习 200 分钟、做 12 道题、休息 8 分钟;I2: 学习 450 分钟、做 25 道题、休息 4 分钟……)。然后让他们去考试,算出每个人的分数(就是 “适应度”,这里是准确率,比如 I1 考了 88 分,I2 考了 85 分,I3 考了 60 分……)。
2. 选择 ——“选优秀的学生当‘老师’”
我们希望下一代学生更厉害,所以优先让分数高的学生当 “父母”(就像优秀老师更可能教出好学生)。比如 I1(88 分)和 I2(85 分)分数最高,所以他们被选中当父母的概率最大(比如 80% 概率选他们,而分数低的 I3、I4 可能只有 20% 概率)。
3. 交叉 ——“父母的‘学习方法’结合,生出新学生”
选 I1 和 I2 当父母后,他们的参数要 “交叉”(相当于结合两人的优点)。比如 I1 的参数是(200, 12, 8),I2 的是(450, 25, 4):
- 可以让前两个参数取 I2 的,最后一个取 I1 的,得到新学生 A:(450, 25, 8)?
- 或者第一个取 I1 的,后两个取 I2 的,得到新学生 B:(200, 25, 4)?你例子里是(450,25,4)和(200,12,8),本质就是父母的参数 “拆开来重新拼”,这样新学生可能继承父母的优点(比如 I1 的休息时长、I2 的学习时间)。
4. 变异 ——“新学生偶尔‘突发奇想’换个方法”
如果只交叉,新学生的方法可能和父母太像,万一父母的方法有隐藏缺陷呢?所以要 “变异”:随机改一点参数。比如新学生 A 是(450,25,4),突然把 “做题数量” 从 25 改成 27,变成(450,27,4);新学生 B 是(200,12,8),把 “学习时间” 改成 210,变成(210,12,8)。变异就像 “冒险尝试新方法”,可能变差,但也可能意外找到更好的组合(比如多做 2 道题反而分数更高)。
5. 替换 ——“淘汰差生,加入新学生”
现在有了变异后的新学生(比如上面两个),原来的学生里有分数低的(比如 I3 考了 60 分,I4 考了 55 分),就把这两个差生 “淘汰”,换成新学生。这样新一代的学生就是:原来的优秀生(I1、I2 可能保留,也可能替换,看规则)+ 新生成的变异学生,整体分数水平比上一代高了。
6. 循环 ——“一代一代重复,越来越强”
让新一代学生再去考试(算适应度),然后再选优秀的当父母,交叉、变异、替换…… 重复几十上百次。每一代学生的平均分数都会越来越高,最后可能出现一个超级学生,参数组合能让考试分数接近 100 分 —— 这就是我们要找的 “最优解”。
简单说,遗传算法就是:先随便找一批 “方案”,让好方案多生孩子,孩子结合父母优点再加点随机变化,淘汰差方案,一代代优化,直到找到最好的方案。是不是很像 “优胜劣汰” 的进化过程?
遗传算法是受到生物遗传和进化,遵循优胜劣汰原则。

1. 随机初始化n组参数
假设初始种群中评估出以下几个参数组合(个体 I)及其对应的模型准确率(适应度):

在这一代中,个体 I1和I2的准确率最高,是当前表现最好的优秀解。
2. 选择:根据准确率(适应度)进行选择,准确率越高的个体被选中作为“父代”进行繁殖的概率越大。
在 I1, I2, I3, I4, 中,I1 (0.88) 和 I2 (0.85) 更有可能被选中。
3. 交叉:选择表现好的来繁殖,交叉他们的参数
我们选取I1和I2作为来繁衍,交叉操作成功生成了两个新的、从未被评估过的参数组合 (450,25,4)和(200,12,8)
4. 变异(探索):随机改变新组合的部分参数,得到变异后的参数组合
5. 替换:用新参数组合替换掉之前表现差的个体新的种群重新评估
6. 循环这一操作
二、粒子群算法:模仿鸟群 / 鱼群的 “群体学习”
灵感来源:鸟群找食物、鱼群游动的规律(个体跟着群体里的 “高手” 和自己的 “经验” 走)。核心思想:把 “问题的每个可能解” 当成一个 “会飞的粒子”,粒子们通过互相学习(看别人的好位置)和总结自己的经验,慢慢飞向最优解的位置。
可以想象成 “一群鸟找最好吃的果子”:
- 每只鸟(粒子)都在一个 “可能有果子的区域” 里飞,它的位置就是一个解题方案(比如果子最多的地方就是最优解);
- 每只鸟都记着两个信息:自己飞过的 “最好位置”(比如之前找到过 5 个果子的地方),以及整个鸟群目前找到的 “最好位置”(比如群里有只鸟找到过 10 个果子的地方);
- 接下来,每只鸟会调整自己的飞行方向和速度:既往自己的最好位置飞一点,也往群里的最好位置飞一点(相当于 “跟着自己的经验” 和 “跟着高手学”);
- 飞着飞着,整个鸟群会慢慢聚集到 “果子最多的地方”—— 也就是问题的最优解附近。
粒子群优化算法
PSO 算法是受鸟群捕食行为或鱼群游动启发的优化技术。与遗传算法(GA)基于“优胜劣汰”的演化机制不同,PSO 基于群体协作和信息共享来寻找最优解。
这里我们每一个个体(一组参数)都被称之为粒子,粒子中的每个具体参数就是他当前的位置分量
这里我们精确一下变异的幅度,在这里变异意为速度
自身历史最佳位置(pbest): 粒子自己找到的最好的位置(即适应度最高的参数组合)。
群体历史最佳位置(gbest): 整个粒子群至今找到的最好的位置(即所有粒子中适应度最高的参数组合)。
每个粒子i的位置在迭代,他在t+1代的位置记作xt+1,速度记作vt+1

下一时刻的速度 = 惯性权重*当前时刻的速度+加速因子1*随机数1*(自身历史最佳位置-目前位置)+加速因子2*随机数2(群体最好位置-当前位置)

下一时刻位置 = 当前时刻位置+下一时刻速度
这个公式由三个主要的组成部分。

1. 惯性部分:保持粒子当前的运动趋势,进行全局探索(w是惯性权重)
2. 认知部分:粒子向自己历史最好位置学习和靠近,体现个体经验(c1 是加速因子,r1是随机数)
3. 社会部分:粒子向群体历史最好位置学习和靠近,体现群体协作。(c2 是加速因子,r2 是随机数)
算法迭代流程
先对这个粒子群算法定义他的超参数:
● 惯性权重 w = 0.8
● 加速因子 c1 = 1.5 (认知部分权重)
● 加速因子 c2 = 1.5 (社会部分权重)
● 随机数 r1 和 r2(在 [0, 1] 之间均匀分布)。
1. 初始化: 随机生成N个粒子的初始位置(参数组合)和初始速度。

PSO和GA的主要区别体现在粒子群是群体协作、信息共享,而且每个个体连续且有记忆的移动,也都会学习最好的粒子;反观遗传算法是跳跃式的产生新的个体(替换),也并非学习而是交叉参数,且没有个体记忆,只有群体演化。
用“一群鸟找食物” 的场景,把粒子群算法(PSO)讲明白
先理解几个核心概念(对应鸟群场景):
- 粒子:每一只鸟(比如有 5 只鸟,就是 5 个粒子)。
- 位置:鸟当前所在的地方(对应 “一组参数”,比如鸟 A 在(x=20,y=30),就像参数组合(20,30))。
- 速度:鸟飞行的 “方向和快慢”(比如往东北飞,每秒 10 米,对应参数调整的幅度和方向)。
- 适应度:这个地方的食物量(食物越多,适应度越高,就像参数组合的准确率越高)。
- 自身历史最佳(pbest):这只鸟这辈子找到过的 “食物最多的地方”(自己最好的参数组合)。
- 群体历史最佳(gbest):所有鸟里,至今找到的 “食物最多的地方”(整个群体最好的参数组合)。
核心:鸟怎么飞?(速度和位置的更新)
每只鸟每秒都会调整飞行方向和快慢(更新速度),然后飞到新的地方(更新位置)。调整的规则就来自那个公式,拆解成 3 个简单逻辑:
1. 惯性部分:“保持现在的飞法”
比如鸟 A 现在正往南飞,速度是 5 米 / 秒。惯性就像 “懒得变”,会让它继续往南飞,速度保留一部分(比如 80%,对应惯性权重 w=0.8)。作用:不让鸟突然乱拐弯,保持对全局的探索(万一南边藏着更多食物呢)。
2. 认知部分:“往自己之前的好地方飞一点”
鸟 A 记得自己上周在(x=10,y=20)找到过很多食物(pbest),现在它在(x=15,y=25)。这时候它会想:“往之前的好地方靠一靠”,于是会调整方向,往(10,20)飞一点(具体飞多远,由加速因子 c1=1.5 和随机数 r1 决定,比如随机选 0.6,就飞 1.5×0.6× 距离)。作用:尊重自己的经验,别忘本。
3. 社会部分:“往大家公认的好地方飞一点”
整个鸟群最近发现,鸟 B 在(x=5,y=10)找到的食物最多(gbest)。鸟 A 会想:“大家都觉得那儿好,我也往那儿飞一点”,于是再调整方向,往(5,10)飞一点(由 c2=1.5 和 r2 决定,比如 r2=0.7,就飞 1.5×0.7× 距离)。作用:群体协作,跟着高手学。
算法流程:鸟群找食物的步骤
就像一群鸟每天出门找食物,重复以下动作,直到找到食物最多的地方:
- 初始化:第一天,5 只鸟随机站在森林的不同地方(随机初始位置),一开始都慢慢飞(初始速度设为 0,相当于站着不动)。
- 评估:每只鸟看看自己脚下有多少食物(计算适应度,比如鸟 A 脚下有 80 颗,鸟 B 有 88 颗)。
- 记下来最好的地方:
- 每只鸟把自己当前位置当成 “自己历史最佳”(pbest),因为是第一天,还没去过别的地方。
- 整个群里,食物最多的鸟 B 的位置(88 颗),就是 “群体最佳”(gbest)。
- 调整飞行,飞到新地方:每只鸟用上面的 3 个规则算新速度(比如鸟 A 综合惯性、自己的 pbest、群体的 gbest,决定往东北飞,速度 6 米 / 秒),然后按新速度飞到新位置。
- 重复:第二天,再评估新位置的食物量,更新自己的 pbest(如果新地方食物更多)和群体的 gbest(如果有鸟找到更多食物),再调整飞行…… 循环几十天,最后大家基本都会聚集到食物最多的地方(找到最优解)。
PSO 和 GA 的区别(一句话总结)
- GA(遗传算法):像 “生物进化”,好的个体生宝宝(交叉),宝宝偶尔突变,差的个体被淘汰,是 “换一批新的”。
- PSO(粒子群):像 “鸟群协作”,每只鸟都记得自己和大家的好地方,不断调整飞行靠近,是 “同一批鸟慢慢挪”,互相学习,不淘汰谁。
是不是很简单?PSO 的核心就是 “一群个体带着记忆,跟着自己和群体的经验慢慢调整,最后一起找到最好的答案”~
三、模拟退火算法:模仿金属 “慢慢降温” 的稳定过程
灵感来源:物理上的 “退火”(金属加热后慢慢降温,原子会排列得更稳定,能量更低)。核心思想:先 “大胆试错”(接受暂时变差的解),再 “逐渐收敛”(越来越少接受差解),最后稳定在一个好解上。
可以想象成 “在山里找最高峰”:
- 一开始你在山里随机走(相当于高温状态,原子运动剧烈),哪怕走到一个比现在低的地方(暂时变差的解),你也愿意试试(因为可能绕个弯能找到更高的山);
- 随着时间推移(温度降低),你越来越 “保守”:如果下一步是上坡(更好的解),你肯定走;但如果是下坡(更差的解),你几乎不怎么走了(偶尔走一下,防止困在小土坡上以为是山顶);
- 最后温度降到很低,你基本只在附近的高点转悠,最终停在的位置就是近似最高峰(最优解)。
模拟退火算法里的 “温度参数”,可以理解成 “人的冒险意愿”—— 温度越高,人越敢冒险;温度越低,人越保守。它的核心作用是让算法在 “大胆探索新方向” 和 “专注优化好结果” 之间找到平衡 。
举个生活例子:假设你在山里找最高峰(目标是找到最优解),温度就像你 “敢不敢走下坡路” 的心态:
-
高温时:你刚进山,对地形一无所知。这时候你 “胆子很大”—— 哪怕眼前是下坡(暂时找到的高度变低,也就是 “更差的解”),你也愿意走过去试试。为什么?因为你不知道下坡后面是不是藏着更高的山(全局最优解)。比如你站在一个小土坡上(局部最优),看起来很高,但旁边有个下坡,坡底后面其实有座珠穆朗玛峰。如果一开始就不敢下坡,你永远找不到珠峰。
-
温度慢慢降低:随着你在山里走了一段时间,对地形有了些了解,“胆子” 开始变小。这时候如果遇到下坡,你会犹豫:只有特别短的下坡(稍微差一点的解),你才可能试试;太长的下坡(差很多的解),你基本不碰了。
-
低温时:你已经离最高峰很近了,这时候 “胆子极小”—— 只愿意往上走(接受更好的解),绝对不往下走(拒绝更差的解)。最终你会在最高峰附近徘徊,直到停在山顶。
所以温度参数的作用很关键:
- 高温阶段:保证算法 “不固执”,敢于跳出暂时的 “小甜头”(局部最优解),去探索更广阔的区域,避免错过真正的 “大赢家”(全局最优解)。
- 低温阶段:让算法 “收心”,不再瞎折腾,专注在已找到的好区域里打磨细节,最终稳定在最优解附近。
就像烧金属时,必须 “先高温加热让原子自由运动,再慢慢降温让原子稳定排列”—— 温度的变化节奏,直接决定了最终能不能找到最好的结果。
列表推导式
今天的代码用到了这个知识点,这是python里面的一个语法糖,我们一起学习一下。
列表推导式是 Python 中用于快速生成列表的语法结构,它以简洁的方式替代了 “创建空列表 + for 循环 + 条件判断(可选)” 的繁琐流程,让代码更紧凑、可读性更强。
语法糖是指 “对功能没有本质改变,但让代码更简洁、易读的语法形式”。列表推导式本质上可以被等价的 for 循环替代,但写法更优雅。
1.1 简单的列表推导式
比如,你想生成一个包含 1~5 每个数字平方的列表(也就是 [1,4,9,16,25]),用普通 for 循环会这样写:。
squares = []
for x in range(1, 6):
squares.append(x**2)
print(squares)
# 空列表准备装结果
squares = []
# 循环1到5的数字
for num in range(1, 6):
# 计算平方,添加到列表
squares.append(num **2)
print(squares) # 输出 [1, 4, 9, 16, 25]
用列表推导式,一行搞定:前者VS后者(后者极大的减轻了代码量)
同样的需求,列表推导式可以写成:
squares = [x**2 for x in range(1, 6)]
print(squares) #
1.2 带条件过滤的列表推导式
生成 1 到 10 中所有偶数的列表。
evens = []
for x in range(1, 11):
if x % 2 == 0:
evens.append(x)
print(evens)
evens = [x for x in range(1, 11) if x % 2 == 0]
print(evens) # 输出:[2, 4, 6, 8, 10]
1.3 带嵌套循环的列表推导式
生成两个列表 [1,2] 和 [3,4] 的所有元素组合(笛卡尔积)。
笛卡尔积(Cartesian Product)是数学和计算机科学中一个基础概念,简单来说,它是两个或多个集合中所有可能的元素组合。
设有集合 A = {1, 2},集合 B = {3, 4},它们的笛卡尔积就是所有以 A 中元素为第一个元素、B 中元素为第二个元素的有序对,即:
A × B = {(1, 3), (1, 4), (2, 3), (2, 4)}
在编程中,笛卡尔积常用来生成 “所有可能的组合”。比如:
- 两个列表 [a, b] 和 [x, y] 的笛卡尔积是 [(a,x), (a,y), (b,x), (b,y)]
- 三个集合的笛卡尔积则是所有三元组的组合(如 A×B×C 的元素是 (a,b,c),其中 a∈A、b∈B、c∈C)
combinations = []
for x in [1,2]:
for y in [3,4]:
combinations.append((x, y))
print(combinations)
combinations = [(x, y) for x in [1,2] for y in [3,4]]
print(combinations) # 输出:[(1, 3), (1, 4), (2, 3), (2, 4)]
1.4 结合函数调用
对一个字符串列表,每个元素都调用 upper() 方法转为大写。
words = ["apple", "banana", "cherry"]
upper_words = []
for word in words:
upper_words.append(word.upper())
print(upper_words)
words = ["apple", "banana", "cherry"]
upper_words = [word.upper() for word in words]
print(upper_words) # 输出:['APPLE', 'BANANA', 'CHERRY']
列表推导式和普通for循环创建列表的优缺点对比
列表推导式和普通 for 循环都是创建列表的常用方式,但它们各有优缺点,适合不同的场景。我们可以从简洁性、可读性、灵活性、效率、调试难度这几个角度对比,用大白话讲清楚:
一、列表推导式的优点
代码更简洁,写起来快
同样的功能,列表推导式能把多行 for 循环浓缩成一行,省代码量。比如生成 1~5 的平方列表:
- 列表推导式:
[x**2 for x in range(1,6)](一行搞定) - 普通 for 循环:需要先定义空列表,再循环 append(至少 3 行)对于简单逻辑,少写代码就意味着少出错,也更符合 Python “简洁优雅” 的风格。
执行效率更高
Python 对列表推导式有专门的底层优化,运行速度通常比普通 for 循环快一点(尤其是处理大量数据时)。原因是:普通 for 循环每次调用append()方法都会有额外的开销,而列表推导式是直接在底层一次性构建列表,减少了中间步骤。
意图更明确
熟悉列表推导式的人一眼就能看出来:“哦,这行代码是在生成一个列表”,不用费劲读多行循环逻辑。
二、列表推导式的缺点
逻辑复杂时,可读性会变差
如果列表推导式里嵌套了多层循环或多个条件,会变得非常冗长,甚至 “一行代码写到底”,反而让人看不懂。比如这样的推导式(虽然能运行,但读起来费劲):[x*y for x in range(3) if x>0 for y in range(5) if y%2==0]换成普通 for 循环,分步写反而更清晰。
功能单一,只能生成列表
列表推导式的核心作用就是 “生成列表”,没办法在循环过程中插入其他操作(比如打印中间结果、修改其他变量、跳出循环等)。比如你想在生成列表时,顺便打印每个元素的值,列表推导式做不到,必须用普通 for 循环:
-
# 普通for循环可以加打印 squares = [] for x in range(1,6): square = x**2 print(f"正在处理:{x},平方是:{square}") # 列表推导式无法加这行 squares.append(square)
调试不方便
列表推导式是一行代码,如果运行出错(比如表达式写错),很难定位具体哪里出问题。而普通 for 循环可以逐行调试,比如在循环中加断点,查看每一步变量的值,更容易找到错误。
三、普通 for 循环的优点
灵活性高,能做更多事
除了生成列表,还能在循环中加入任意操作:打印日志、条件判断后跳出循环(break)、跳过当前元素(continue)、修改其他变量等。比如生成列表时,遇到某个值就停止:
-
squares = [] for x in range(1,100): if x == 6: break # 遇到6就停止,列表推导式无法用break squares.append(x**2)
逻辑复杂时,可读性更好
当循环里有多层嵌套、多个条件判断,或者需要分步处理数据时,普通 for 循环按步骤写,条理更清晰,新手也更容易理解。
调试简单
可以逐行执行,查看每一步的变量状态,轻松定位错误(比如某个元素计算错了,直接看循环到那一步时的变量值就行)。
四、普通 for 循环的缺点
代码冗长,写起来麻烦
简单的列表生成也要写好几行(定义空列表、循环、append),不如列表推导式简洁。
效率稍低
相比列表推导式,普通 for 循环因为每次调用append()有额外开销,处理大量数据时会慢一点(虽然大多数场景下差异不明显)。
总结:什么时候用哪个?
- 用列表推导式:当逻辑简单(单层循环 + 简单条件),只需要生成列表,不需要中间操作时(比如快速生成筛选、转换后的列表)。
- 用普通 for 循环:当逻辑复杂(多层循环、多条件),需要中间操作(打印、break/continue),或者需要方便调试时。
简单说:简单场景选推导式(简洁高效),复杂场景选 for 循环(灵活清晰)。作为新手,先掌握基本用法,再根据实际需求选择就好~
作业:
1. 对其他模型采用这几种算法尝试优化超参数
2. 尝试写出退火算法的背后思想和案例(可选)
错误点

这个错误的核心原因是:用了 “回归模型”(比如线性回归)去做 “分类任务”,导致模型输出的是 “连续数值”(比如 0.3、0.8),但标签y_test是 “二进制分类值”(0 或 1),两者类型不匹配,所以计算准确率时报错。
先理解问题本质(小白版)
- 分类任务(比如 “判断是否患病”):标签是 离散的类别(0 = 健康,1 = 患病),模型要输出 “类别”,准确率是 “预测对的类别数 / 总样本数”。
- 回归任务(比如 “预测房价”):标签是 连续的数值(比如 100 万、200 万),模型输出 “数值”,不能用 “准确率” 衡量(要用均方误差 MSE)。
你报错时,大概率是model_name选了'linear'(线性回归,回归模型),但却用了分类任务的 “准确率” 来评估 —— 就像用 “尺子量体重”,工具和目标不匹配。
解决方法:分 2 种情况处理
根据你想优化的模型类型,修改代码即可,二选一:
情况 1:想做 “分类任务”(目标是判断是否患病,推荐)
如果你的核心需求是 “预测是否患病”(分类),就排除线性回归,只优化 3 个分类模型(逻辑回归、决策树、随机森林)。修改 “验证最优模型效果” 的代码,确保model_name是分类模型(如'random_forest'、'logistic'、'decision_tree'),代码不变:
# 关键:确保model_name是分类模型(不是'linear')
model_name = 'random_forest' # 换成'logistic'或'decision_tree'都可以
# 用分类模型的最优超参数训练(比如遗传算法的结果)
best_params = best_params_ga # 或best_params_pso/best_params_sa
best_model = get_model(model_name, best_params)
best_model.fit(X_train_scaled, y_train)
# 测试效果(分类模型输出类别,和y_test(0/1)匹配,准确率可正常计算)
y_pred = best_model.predict(X_test_scaled) # 输出0或1
accuracy = accuracy_score(y_test, y_pred)
print("\n=== 最优模型最终效果 ===")
print("模型:", model_name)
print("最优超参数:", best_params)
print("测试集准确率:", accuracy)
情况 2:想演示 “线性回归”(仅学习流程,不推荐用于此数据)
如果只是想学习线性回归的优化流程(虽然它不适合分类任务),就不能用准确率评估,要换成回归任务的指标(比如均方误差 MSE),修改代码如下:
# 仅演示线性回归:model_name设为'linear'
model_name = 'linear'
# 用线性回归的最优超参数训练
best_params = best_params_ga # 或其他算法的结果
best_model = get_model(model_name, best_params)
best_model.fit(X_train_scaled, y_train)
# 测试效果:回归模型输出连续数值,用MSE评估(越小越好)
y_pred = best_model.predict(X_test_scaled) # 输出连续值(比如0.2、0.9)
mse = mean_squared_error(y_test, y_pred) # 回归任务的评估指标
print("\n=== 线性回归最优模型效果 ===")
print("模型:线性回归(Ridge)")
print("最优超参数:", best_params)
print("测试集均方误差(MSE):", mse) # 不用accuracy,改用MSE
情况 2:想演示 “线性回归”(仅学习流程,不推荐用于此数据)
如果只是想学习线性回归的优化流程(虽然它不适合分类任务),就不能用准确率评估,要换成回归任务的指标(比如均方误差 MSE),修改代码如下:
# 仅演示线性回归:model_name设为'linear'
model_name = 'linear'
# 用线性回归的最优超参数训练
best_params = best_params_ga # 或其他算法的结果
best_model = get_model(model_name, best_params)
best_model.fit(X_train_scaled, y_train)
# 测试效果:回归模型输出连续数值,用MSE评估(越小越好)
y_pred = best_model.predict(X_test_scaled) # 输出连续值(比如0.2、0.9)
mse = mean_squared_error(y_test, y_pred) # 回归任务的评估指标
print("\n=== 线性回归最优模型效果 ===")
print("模型:线性回归(Ridge)")
print("最优超参数:", best_params)
print("测试集均方误差(MSE):", mse) # 不用accuracy,改用MSE
避坑提醒(小白必记)
- 分类任务用分类模型 + 分类指标:模型:逻辑回归、决策树、随机森林;指标:准确率(accuracy)、混淆矩阵。
- 回归任务用回归模型 + 回归指标:模型:线性回归、Ridge 回归;指标:均方误差(MSE)、R² 分数。
- 永远先明确任务类型:拿到数据先看标签 —— 是 “类别”(0/1、A/B/C)就是分类,是 “数值”(价格、温度)就是回归,再选对应的模型和指标。
快速验证:检查当前任务类型
运行这段代码,确认你的标签是 “分类值”(0 和 1),所以优先选情况 1 的分类模型:
# 检查标签类型(确认是分类任务)
print("标签的唯一值:", y_test.unique()) # 输出[0 1],说明是二分类任务
print("标签的数据类型:", y_test.dtype) # 输出int64,是离散类别
如果输出[0 1],就按情况 1 修改代码,用分类模型和准确率,就能解决报错啦!
分类模型的完整验证代码
按 “数据预处理→模型定义→启发式算法优化→最优模型验证” 的标准化流程整理,复制即可直接运行:
完整流程:启发式算法优化心脏病分类模型(小白友好版)
一、核心说明
- 任务:用心脏病数据集(
heart.csv)做 “二分类”(预测是否患病,标签列默认target,值为 0/1)。 - 优化对象:3 种分类模型(逻辑回归、决策树、随机森林)。
- 优化算法:遗传算法(GA)、粒子群算法(PSO)、模拟退火算法(SA)。
- 评估指标:分类任务专用的 “准确率”(越高模型效果越好)。
二、第一步:环境准备与数据预处理(地基操作)
# 1. 导入所有必备工具(不用改)
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
# 2. 安装启发式算法库(第一次运行需执行,后续注释掉)
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "scikit-opt"])
from sko.GA import GA # 遗传算法
from sko.PSO import PSO # 粒子群算法
from sko.SA import SA # 模拟退火算法
# 3. 加载数据+查看列名(确认标签列,避免KeyError)
data = pd.read_csv('/mnt/heart.csv')
print("=== 数据列名(确认标签列)===")
print(data.columns.tolist()) # 通常输出含'target'(标签列),若不是则替换后续'target'
print("\n=== 数据前5行 ===")
print(data.head())
# 4. 数据预处理(清洗+拆分+标准化)
# 4.1 处理缺失值(若有缺失值则填充,无则跳过不影响)
data = data.fillna(data.mean()) # 用平均值填充缺失值
# 4.2 拆分特征(X:输入数据,如年龄、血压)和标签(y:是否患病)
# !关键:若标签列不是'target',把这里的'target'换成你数据里的标签列名(如'label')
X = data.drop('target', axis=1)
y = data['target']
# 4.3 划分训练集(教模型)和测试集(测效果)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42 # 20%数据当测试集,结果可重复
)
# 4.4 特征标准化(让数据范围一致,模型不偏心)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 训练集学规则
X_test_scaled = scaler.transform(X_test) # 测试集用同样规则
print("\n=== 数据预处理完成 ===")
print(f"训练集特征形状:{X_train_scaled.shape},测试集特征形状:{X_test_scaled.shape}")
三、第二步:定义 “模型 + 超参数范围 + 评价函数”(核心配置)
# 1. 模型工厂:输入模型名+超参数,输出对应模型(不用改)
def get_model(model_name, params):
if model_name == 'logistic': # 逻辑回归(分类)
return LogisticRegression(
C=params['C'], # 要优化的超参数:正则化强度
random_state=42,
max_iter=1000 # 确保模型能收敛
)
elif model_name == 'decision_tree': # 决策树(分类)
return DecisionTreeClassifier(
max_depth=params['max_depth'], # 要优化的超参数1:树最大深度
min_samples_split=params['min_samples_split'], # 要优化的超参数2:分裂最小样本数
random_state=42
)
elif model_name == 'random_forest': # 随机森林(分类)
return RandomForestClassifier(
n_estimators=params['n_estimators'], # 要优化的超参数1:树的数量
max_depth=params['max_depth'], # 要优化的超参数2:树最大深度
random_state=42
)
# 2. 超参数范围:每种模型的参数取值边界(启发式算法在这个范围内搜最优)
# !若想调整范围(如树深度1-20),直接改括号里的数值即可
param_ranges = {
'logistic': {
'C': (0.01, 10) # 逻辑回归:C越大,正则化越弱(范围0.01-10)
},
'decision_tree': {
'max_depth': (1, 10), # 决策树:深度1-10(太深易过拟合)
'min_samples_split': (2, 20) # 决策树:分裂需2-20个样本(太少易过拟合)
},
'random_forest': {
'n_estimators': (10, 100), # 随机森林:树数量10-100(越多越稳但越慢)
'max_depth': (1, 10) # 随机森林:树深度1-10
}
}
# 3. 评价函数:输入模型名+超参数,输出模型在测试集的准确率(越高越好)
def evaluate_model(model_name, params):
model = get_model(model_name, params)
model.fit(X_train_scaled, y_train) # 训练模型
y_pred = model.predict(X_test_scaled) # 测试模型
return accuracy_score(y_test, y_pred) # 输出准确率(分类任务专用指标)
# 4. 超参数与向量转换:启发式算法只认数字向量,需转换(不用改)
# 4.1 超参数→向量(算法输入)
def params_to_vec(model_name, params):
ranges = param_ranges[model_name]
return [params[key] for key in sorted(ranges.keys())] # 按固定顺序转向量
# 4.2 向量→超参数(算法输出转模型参数)
def vec_to_params(model_name, vec):
ranges = param_ranges[model_name]
params = {}
for i, key in enumerate(sorted(ranges.keys())):
param_val = vec[i]
# 整数参数(如树深度)需四舍五入,浮点数(如C)直接用
if key in ['max_depth', 'min_samples_split', 'n_estimators']:
params[key] = int(round(param_val))
else:
params[key] = param_val
return params
# 5. 算法目标函数:连接算法与模型(不用改)
def objective_func(vec, model_name):
params = vec_to_params(model_name, vec)
return evaluate_model(model_name, params) # 算法要最大化这个准确率
四、第三步:用 3 种启发式算法优化超参数(核心执行)
选择要优化的模型(三选一,新手推荐先试random_forest)
# !关键:选择要优化的模型,可替换为'logistic'(逻辑回归)、'decision_tree'(决策树)
model_name = 'random_forest'
# 获取当前模型的超参数配置(不用改)
ranges = param_ranges[model_name]
param_keys = sorted(ranges.keys()) # 参数名(如['max_depth', 'n_estimators'])
dim = len(param_keys) # 参数维度(如随机森林是2维)
lb = [ranges[key][0] for key in param_keys] # 参数下限(如[1, 10])
ub = [ranges[key][1] for key in param_keys] # 参数上限(如[10, 100])
print(f"\n=== 开始用3种算法优化 {model_name} 超参数 ===")
print(f"要优化的参数:{param_keys},参数范围:{lb}~{ub}")
1. 遗传算法(GA)优化
print("\n--- 1. 遗传算法优化 ---")
# 初始化遗传算法(size_pop=种群数,max_iter=迭代次数,数值越大搜得越细但越慢)
ga = GA(
func=lambda vec: objective_func(vec, model_name), # 目标函数(最大化准确率)
n_dim=dim, # 参数维度
size_pop=50, # 每次选50组参数(种群大小)
max_iter=20, # 进化20代
lb=lb, # 参数下限
ub=ub, # 参数上限
# 整数参数精度1,浮点数精度0.001(确保参数类型正确)
precision=[1 if key in ['max_depth', 'min_samples_split', 'n_estimators'] else 1e-3
for key in param_keys]
)
# 运行算法
ga.run()
# 提取结果
best_vec_ga = ga.best_x
best_score_ga = ga.best_y[0]
best_params_ga = vec_to_params(model_name, best_vec_ga)
print(f"遗传算法最优超参数:{best_params_ga}")
print(f"遗传算法最优测试集准确率:{best_score_ga:.4f}")
2. 粒子群算法(PSO)优化
print("\n--- 2. 粒子群算法优化 ---")
# 初始化粒子群算法(pop=粒子数,max_iter=迭代次数)
pso = PSO(
func=lambda vec: objective_func(vec, model_name),
n_dim=dim,
pop=50, # 50个粒子(对应50组参数)
max_iter=20, # 迭代20次
lb=lb,
ub=ub,
w=0.8, # 惯性权重(之前讲的“保持当前飞法”)
c1=1.5, # 认知因子(“向自己经验学”)
c2=1.5 # 社会因子(“向群体高手学”)
)
# 运行算法
pso.run()
# 提取结果
best_vec_pso = pso.best_x
best_score_pso = pso.best_y[0]
best_params_pso = vec_to_params(model_name, best_vec_pso)
print(f"粒子群算法最优超参数:{best_params_pso}")
print(f"粒子群算法最优测试集准确率:{best_score_pso:.4f}")
3. 模拟退火算法(SA)优化
print("\n--- 3. 模拟退火算法优化 ---")
# 初始化模拟退火算法(T_max=初始温度,T_min=最低温度)
sa = SA(
# SA默认“最小化目标”,所以加负号(把“最大化准确率”转“最小化负准确率”)
func=lambda vec: -objective_func(vec, model_name),
x0=np.random.uniform(lb, ub, dim), # 初始参数(随机)
T_max=100, # 初始高温(敢试错)
T_min=1e-3, # 最低温度(不试错)
L=100, # 每个温度下试100次
lb=lb,
ub=ub
)
# 运行算法
sa.run()
# 提取结果(负号转回来,恢复原准确率)
best_vec_sa = sa.best_x
best_score_sa = -sa.best_y
best_params_sa = vec_to_params(model_name, best_vec_sa)
print(f"模拟退火算法最优超参数:{best_params_sa}")
print(f"模拟退火算法最优测试集准确率:{best_score_sa:.4f}")
五、第四步:验证最优模型效果(最终结论)
# 1. 选择最优算法的结果(选准确率最高的,这里默认选遗传算法,可替换)
best_algorithms = {
'遗传算法': (best_params_ga, best_score_ga),
'粒子群算法': (best_params_pso, best_score_pso),
'模拟退火算法': (best_params_sa, best_score_sa)
}
# 找出准确率最高的算法
best_alg_name = max(best_algorithms.keys(), key=lambda k: best_algorithms[k][1])
best_params, best_score = best_algorithms[best_alg_name]
# 2. 用最优超参数训练最终模型
final_model = get_model(model_name, best_params)
final_model.fit(X_train_scaled, y_train)
# 3. 测试最终模型效果
y_pred_final = final_model.predict(X_test_scaled)
final_accuracy = accuracy_score(y_test, y_pred_final)
# 4. 输出最终结论
print("\n=== 最终优化结果 ===")
print(f"优化模型:{model_name}")
print(f"最优超参数来源:{best_alg_name}")
print(f"最优超参数:{best_params}")
print(f"最终测试集准确率:{final_accuracy:.4f}")
print(f"(解释:模型在测试集上,每预测100个样本,约对{int(final_accuracy*100)}个)")
六、小白避坑指南
- 标签列名错误:若第一步报错 “KeyError: ['target']”,打开第一步的 “数据列名” 输出,把
drop('target')和['target']换成真实标签列名(如'label')。 - 模型类型错误:代码只针对 “分类模型”,不要加线性回归(回归模型不适合此任务)。
- 运行速度慢:若觉得慢,可减小
size_pop(如 30)或max_iter(如 10),但搜得会粗一点。
# ============================== 1. 导入所有必备工具(修改:通用中文字体设置) ==============================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, mean_squared_error
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
try:
from sko.GA import GA
from sko.PSO import PSO
from sko.SA import SA
except ImportError:
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "scikit-opt"])
from sko.GA import GA
from sko.PSO import PSO
from sko.SA import SA
# ---------------------- 关键修改:通用中文字体配置(优先加载系统已有的中文字体) ----------------------
def set_chinese_font():
"""自动检测并设置系统中的中文字体,避免乱码"""
try:
# 1. 优先尝试Windows系统常见中文字体
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 验证字体是否生效(画一个测试文本,无报错则成功)
plt.figure(figsize=(1,1))
plt.text(0.5, 0.5, '测试中文', fontsize=12)
plt.close()
except:
try:
# 2. 尝试Mac/Linux系统常见中文字体
plt.rcParams['font.sans-serif'] = ['PingFang SC', 'WenQuanYi Zen Hei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
# 验证字体
plt.figure(figsize=(1,1))
plt.text(0.5, 0.5, '测试中文', fontsize=12)
plt.close()
except:
# 3. 兜底方案:用英文显示(避免乱码)
print("⚠️ 未检测到可用中文字体,图表将用英文显示标签")
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 执行字体设置(运行一次即可生效)
set_chinese_font()
# ============================== 2. 数据预处理(不变) ==============================
def data_preprocessing(data_path):
data = pd.read_csv(data_path)
print("=== 数据基本信息 ===")
print(f"数据形状:{data.shape}")
print(f"标签列名:'target'(0=健康,1=患病)")
print(f"缺失值情况:\n{data.isnull().sum()}")
if data.isnull().sum().sum() > 0:
data = data.fillna(data.mean())
print("已用平均值填充缺失值")
X = data.drop('target', axis=1)
y = data['target']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"\n=== 预处理完成 ===")
print(f"训练集:特征{X_train_scaled.shape},标签{y_train.shape}")
print(f"测试集:特征{X_test_scaled.shape},标签{y_test.shape}")
return X_train_scaled, X_test_scaled, y_train, y_test, scaler
# ============================== 3. 模型与超参数配置(不变) ==============================
def get_model(model_name, params):
if model_name == 'linear':
alpha = max(0.0, params.get('alpha', 1.0))
return Ridge(alpha=alpha, random_state=42)
elif model_name == 'logistic':
C = max(1e-6, params.get('C', 1.0))
return LogisticRegression(
C=C, max_iter=1000, random_state=42
)
elif model_name == 'decision_tree':
max_depth = max(1, int(round(params.get('max_depth', 5))))
min_samples_split = max(2, int(round(params.get('min_samples_split', 10))))
return DecisionTreeClassifier(
max_depth=max_depth,
min_samples_split=min_samples_split,
random_state=42
)
elif model_name == 'random_forest':
n_estimators = max(10, int(round(params.get('n_estimators', 50))))
max_depth = max(1, int(round(params.get('max_depth', 5))))
return RandomForestClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
random_state=42
)
else:
raise ValueError(f"不支持的模型:{model_name},可选:linear/logistic/decision_tree/random_forest")
param_ranges = {
'linear': {'alpha': (0.01, 10)},
'logistic': {'C': (0.01, 10)},
'decision_tree': {'max_depth': (1, 10), 'min_samples_split': (2, 20)},
'random_forest': {'n_estimators': (10, 100), 'max_depth': (1, 10)}
}
# ============================== 4. 评价函数(不变) ==============================
def evaluate_model(model_name, params, X_train, X_test, y_train, y_test):
model = get_model(model_name, params)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
if model_name in ['logistic', 'decision_tree', 'random_forest']:
return accuracy_score(y_test, y_pred)
elif model_name == 'linear':
mse = mean_squared_error(y_test, y_pred)
return -mse
# ============================== 5. 超参数与向量转换(不变) ==============================
def vec_to_params(model_name, vec):
ranges = param_ranges[model_name]
params = {}
for i, key in enumerate(sorted(ranges.keys())):
param_val = vec[i]
if model_name == 'logistic' and key == 'C':
param_val = max(1e-6, param_val)
params[key] = round(param_val, 2)
elif model_name == 'linear' and key == 'alpha':
param_val = max(0.0, param_val)
params[key] = round(param_val, 2)
elif key == 'max_depth':
param_val = int(round(param_val))
param_val = max(1, param_val)
params[key] = param_val
elif key == 'min_samples_split':
param_val = int(round(param_val))
param_val = max(2, param_val)
params[key] = param_val
elif key == 'n_estimators':
param_val = int(round(param_val))
param_val = max(10, param_val)
params[key] = param_val
else:
params[key] = round(param_val, 2)
return params
def objective_func(vec, model_name, X_train, X_test, y_train, y_test):
params = vec_to_params(model_name, vec)
return evaluate_model(model_name, params, X_train, X_test, y_train, y_test)
# ============================== 6. 启发式算法优化(不变) ==============================
def optimize_hyperparameters(model_name, X_train, X_test, y_train, y_test, algo_name='GA'):
ranges = param_ranges[model_name]
param_keys = sorted(ranges.keys())
dim = len(param_keys)
lb = [ranges[key][0] for key in param_keys]
ub = [ranges[key][1] for key in param_keys]
print(f"\n=== 用{algo_name}优化{model_name}超参数 ===")
print(f"超参数:{param_keys},范围:{lb}~{ub}")
if algo_name == 'GA':
algo = GA(
func=lambda vec: objective_func(vec, model_name, X_train, X_test, y_train, y_test),
n_dim=dim, size_pop=50, max_iter=20, lb=lb, ub=ub,
precision=[1 if key in ['max_depth', 'min_samples_split', 'n_estimators'] else 1e-3
for key in param_keys]
)
algo.run()
best_vec = algo.best_x
best_score = algo.best_y[0]
iter_scores = algo.generation_best_Y
elif algo_name == 'PSO':
algo = PSO(
func=lambda vec: objective_func(vec, model_name, X_train, X_test, y_train, y_test),
n_dim=dim, pop=50, max_iter=20, lb=lb, ub=ub, w=0.8, c1=1.5, c2=1.5
)
algo.run()
best_vec = algo.best_x
best_score = algo.best_y[0]
iter_scores = algo.gbest_y_history if hasattr(algo, 'gbest_y_history') else [algo.best_y[0]]*20
elif algo_name == 'SA':
algo = SA(
func=lambda vec: -objective_func(vec, model_name, X_train, X_test, y_train, y_test),
x0=np.random.uniform(lb, ub, dim), T_max=100, T_min=1e-3, L=100
)
algo.run()
best_vec = algo.best_x
best_score = -algo.best_y
iter_scores = [-s for s in algo.best_y_history[:20]]
else:
raise ValueError(f"不支持的算法:{algo_name},可选:GA/PSO/SA")
best_params = vec_to_params(model_name, best_vec)
score_name = "准确率" if model_name in ['logistic', 'decision_tree', 'random_forest'] else "负MSE"
print(f"最优超参数:{best_params}")
print(f"最优{score_name}:{best_score:.4f}")
return best_params, best_score, iter_scores
# ============================== 7. 结果汇总与可视化(修改:加固无网格+字体适配) ==============================
def save_results(all_optim_results, save_folder='model_optimization_results'):
if not os.path.exists(save_folder):
os.makedirs(save_folder)
print(f"\n=== 结果保存至:{os.path.abspath(save_folder)} ===")
models = list(all_optim_results.keys())
algos = ['GA', 'PSO', 'SA']
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
axes = axes.flatten()
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
# 适配中英文标签(根据字体设置自动切换)
model_labels = {
'linear': '线性回归' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Linear Regression',
'logistic': '逻辑回归' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Logistic Regression',
'decision_tree': '决策树' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Decision Tree',
'random_forest': '随机森林' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Random Forest'
}
x_label = '迭代次数' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Iteration'
y_label_acc = '准确率' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Accuracy'
y_label_mse = '负MSE' if 'YaHei' in plt.rcParams['font.sans-serif'][0] or 'PingFang' in plt.rcParams['font.sans-serif'][0] else 'Negative MSE'
for idx, model in enumerate(models):
ax = axes[idx]
# 绘制曲线
for algo_idx, algo in enumerate(algos):
iter_scores = all_optim_results[model][algo]['iter_scores']
ax.plot(range(1, len(iter_scores)+1), iter_scores,
color=colors[algo_idx], label=algo, linewidth=2.5,
marker='o', markersize=5, markerfacecolor='white', markeredgewidth=2)
# 彻底无网格+边框优化
ax.grid(False) # 强制关闭网格
ax.set_axisbelow(False) # 防止网格残留
ax.spines['top'].set_visible(False) # 隐藏上边框
ax.spines['right'].set_visible(False) # 隐藏右边框
# 设置标签(适配中英文)
y_label = y_label_acc if model != 'linear' else y_label_mse
ax.set_title(f'{model_labels[model]} Optimization Process', fontsize=14, fontweight='bold', pad=15)
ax.set_xlabel(x_label, fontsize=12, fontweight='bold')
ax.set_ylabel(y_label, fontsize=12, fontweight='bold')
# 图例与刻度
ax.legend(fontsize=11, frameon=True, fancybox=True, shadow=True)
ax.tick_params(axis='both', labelsize=10, width=1.5)
# 保存时加固字体显示
plt.tight_layout()
save_path = os.path.join(save_folder, 'optimization_process.png')
# 关键:设置dpi=300+facecolor=white,确保字体清晰无模糊
plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white', edgecolor='none')
plt.close()
print("1. 优化过程曲线已保存(中文正常+无网格)")
# ============================== 8. 主函数(不变) ==============================
def main(data_path='heart.csv'):
X_train, X_test, y_train, y_test, _ = data_preprocessing(data_path)
models = ['linear', 'logistic', 'decision_tree', 'random_forest']
algos = ['GA', 'PSO', 'SA']
all_optim_results = {}
for model in models:
all_optim_results[model] = {}
for algo in algos:
best_params, best_score, iter_scores = optimize_hyperparameters(
model_name=model,
X_train=X_train, X_test=X_test,
y_train=y_train, y_test=y_test,
algo_name=algo
)
all_optim_results[model][algo] = {
'best_params': best_params,
'best_score': best_score,
'iter_scores': iter_scores
}
save_results(all_optim_results)
print("\n=== 最终最优结果汇总 ===")
for model in models:
model_cn = {
'linear': '线性回归', 'logistic': '逻辑回归',
'decision_tree': '决策树', 'random_forest': '随机森林'
}[model]
score_cn = "准确率" if model != 'linear' else "负MSE"
best_algo = max(algos, key=lambda a: all_optim_results[model][a]['best_score'])
best_score = all_optim_results[model][best_algo]['best_score']
best_params = all_optim_results[model][best_algo]['best_params']
print(f"{model_cn}:最优算法={best_algo},最优{score_cn}={best_score:.4f},最优参数={best_params}")
# ============================== 9. 运行入口(不变) ==============================
if __name__ == "__main__":
data_path = 'heart.csv' # 按实际路径修改
# data_path = 'C:/Users/你的名字/下载/heart.csv' # Windows示例
# data_path = '/Users/你的名字/Downloads/heart.csv' # Mac示例
if not os.path.exists(data_path):
print("❌ 错误:找不到heart.csv!请修改上面的data_path为正确路径")
else:
print("✅ 找到数据,开始运行...")
main(data_path=data_path)
print("\n🎉 运行完成!结果在 model_optimization_results 文件夹")
尝试写出退火算法的背后思想和案例
要理解模拟退火算法,核心是抓住它 “模仿金属退火” 的自然规律 —— 先 “大胆试错”,再 “逐渐收敛”,最后找到最优解。用生活例子讲透,小白也能秒懂!
一、退火算法的核心思想:模仿金属 “慢慢降温”
先回忆物理现象:金属加热到高温时,原子会剧烈运动(混乱无序);随着温度慢慢降低,原子会逐渐排列成稳定结构(能量最低,最稳定)。
退火算法把这个过程 “翻译” 成解决问题的思路:
- 高温阶段(初期):像原子剧烈运动一样,算法 “大胆试错”—— 哪怕当前解变差(比如从 “小山峰” 走到 “山谷”),也愿意尝试,目的是跳出局部最优解(避免把 “小土坡” 当成 “最高峰”)。
- 降温阶段(中期):温度慢慢下降,算法 “逐渐保守”—— 只偶尔接受轻微变差的解,重点往 “更好的方向” 探索(比如从 “山谷” 往更高的山坡走)。
- 低温阶段(后期):温度接近 0,算法 “完全收敛”—— 只接受更好的解,不再试错,最终稳定在 “全局最优解” 附近(找到真正的最高峰)。
一句话总结:先乱闯,再聚焦,最后定下来,本质是用 “可控的试错” 避免错过全局最优。
二、生活案例:用退火算法 “找城市里的最低房价小区”
假设你要在一个城市找 “房价最低的小区”(最优解),城市里有高低错落的小区(对应不同的解),用退火算法的思路会这么做:
1. 初始化(对应 “金属初始状态”)
- 随机选一个小区作为起点(比如 “A 小区”,房价 3 万 / 平);
- 设定初始温度(比如
T=100,代表 “冒险意愿”,温度越高越敢动); - 设定降温速度(比如每次降温后温度变为原来的 95%,即
降温系数=0.95)。
2. 高温阶段(T=100→T=50):大胆试错
- 从当前小区(A)随机选一个相邻小区(比如 B 小区,房价 3.2 万 / 平,比 A 贵,是 “变差的解”);
- 算法判断:因为温度高(冒险意愿强),即使 B 更贵,也愿意去 B 小区看看(接受变差的解);
- 再随机选相邻小区 C(房价 2.8 万 / 平,比 B 便宜,是 “更好的解”),肯定去 C(接受更好的解)。
- 目的:不局限在 A 附近,多探索城市不同区域,避免错过偏远但更便宜的小区。
3. 降温阶段(T=50→T=10):逐渐保守
- 当前在 C 小区(2.8 万 / 平),随机选相邻小区 D(房价 2.9 万 / 平,略贵);
- 算法判断:温度降低,冒险意愿减弱,用 “概率” 决定是否去 D—— 比如计算一个概率(温度越高,概率越大),这次概率 30%,没中,所以不去 D,继续留在 C;
- 再选相邻小区 E(房价 2.7 万 / 平,更便宜),直接去 E(肯定接受更好的解)。
- 目的:减少无效试错,重点往房价更低的方向靠近。
4. 低温阶段(T=10→T=0.1):完全收敛
- 当前在 E 小区(2.7 万 / 平),随机选相邻小区 F(房价 2.8 万 / 平,更贵);
- 算法判断:温度极低,冒险概率接近 0,直接拒绝去 F;
- 再选相邻小区 G(房价 2.6 万 / 平,更便宜),去 G;
- 继续探索,发现 G 附近没有更便宜的小区,最终确定 G 是 “房价最低的小区”(最优解)。
三、代码案例:用退火算法找 “函数的最小值”
以找函数 f(x) = x² - 4x + 3 的最小值(该函数最小值在 x=2,值为 -1)为例,写一段小白能看懂的代码,每步都标注释:
import numpy as np
import matplotlib.pyplot as plt
# 1. 定义要优化的函数(找它的最小值)
def target_function(x):
return x**2 - 4*x + 3 # 二次函数,图像是开口向上的抛物线,最小值在x=2
# 2. 退火算法核心参数(小白可直接套用)
T = 100.0 # 初始温度(高温)
T_min = 0.1 # 最低温度(接近0时停止)
cooling_rate = 0.95 # 降温系数(每次温度乘以0.95,慢慢降温)
x_current = np.random.uniform(-5, 10) # 随机选一个初始点(x范围:-5到10)
x_best = x_current # 初始时,最好的点就是当前点
history = [x_current] # 记录探索过程,方便画图
# 3. 退火算法主循环(核心逻辑)
while T > T_min:
# 步骤1:随机生成一个“新点”(在当前点附近小范围波动)
x_new = x_current + np.random.normal(0, 1) # 围绕当前点随机动一点(类似“相邻小区”)
# 步骤2:计算“当前点”和“新点”的函数值(对应“当前小区房价”和“新小区房价”)
f_current = target_function(x_current)
f_new = target_function(x_new)
# 步骤3:判断是否接受新点
if f_new < f_current:
# 新点更好(房价更低),直接接受
x_current = x_new
# 更新“最好的点”
if f_new < target_function(x_best):
x_best = x_new
else:
# 新点更差(房价更高),按概率接受(温度越高,概率越大)
probability = np.exp((f_current - f_new) / T) # 核心概率公式
if np.random.random() < probability: # 生成0-1的随机数,小于概率则接受
x_current = x_new
# 步骤4:降低温度(模拟降温过程)
T *= cooling_rate
# 记录过程,方便画图
history.append(x_current)
# 4. 输出结果
print(f"找到的最优解:x = {x_best:.2f}")
print(f"最优解对应的函数最小值:f(x) = {target_function(x_best):.2f}") # 理论上是-1
# 5. 画图看探索过程(直观理解)
x = np.linspace(-5, 10, 1000) # 生成-5到10的1000个点,用于画函数曲线
y = target_function(x)
plt.figure(figsize=(10, 6))
plt.plot(x, y, label='函数 f(x) = x² - 4x + 3', color='blue') # 画函数曲线
plt.scatter(history, [target_function(h) for h in history],
color='red', s=10, label='探索过程') # 画算法探索的点
plt.scatter(x_best, target_function(x_best),
color='green', s=100, label=f'最优解 (x={x_best:.2f})', zorder=5) # 画最优解
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
plt.title('模拟退火算法找函数最小值')
plt.grid(True, alpha=0.3)
plt.show()
四、代码运行后能看到什么?
- 输出结果:会显示 “最优解 x≈2.00,函数最小值≈-1.00”,和理论结果一致;
- 图表:蓝色曲线是函数本身,红色小点是算法 “探索的路径”(先乱走,后聚焦),绿色大圆点是最终找到的最优解。
总结:退火算法的核心优势
- 不怕 “局部最优陷阱”:高温阶段的试错能跳出 “小山峰”,找到 “全局最高峰”;
- 逻辑简单:不用复杂的交叉、变异(对比遗传算法),核心就是 “降温 + 概率接受”;
- 适用场景广:比如找最优路径、调模型超参数、设计最优结构等。
作为小白,先记住 “先乱闯再聚焦” 的思想,再结合这个代码案例跑一遍,就能真正理解退火算法啦~
一、核心思想速记(3 句话搞懂)
- 模仿自然:像金属 “高温加热→慢慢降温→原子稳定排列” 一样,算法从 “大胆试错” 到 “专注最优”;
- 核心逻辑:温度越高,越敢接受 “变差的解”(避免困在局部最优);温度越低,越只接受 “更好的解”(稳定到全局最优);
- 一句话总结:先乱闯、再聚焦、最后定,用 “可控的试错” 找最好的答案。
二、关键概念大白话(不用记术语,记例子)
| 算法术语 | 大白话解释 | 生活例子(找最低房价小区) |
|---|---|---|
| 解(Solution) | 一个可能的答案 | 某个小区的房价(比如 3 万 / 平) |
| 目标函数 | 衡量 “解好不好” 的标准 | 房价高低(数值越小,解越好) |
| 温度(T) | “冒险意愿”(温度高 = 敢试错) | 初期敢去贵的小区,后期只看便宜的 |
| 降温系数 | 每次 “冒险意愿” 降低的幅度 | 每次温度乘以 0.95,慢慢变保守 |
| 收敛 | 不再轻易改变,找到稳定答案 | 确定某个小区是全城最低价 |
三、算法步骤拆解(5 步走,小白也能按步骤做)
步骤 1:明确 “要解决的问题”(先定目标)
核心动作:确定 “什么是解”“怎么判断解好不好”;
- 举例 1(找函数最小值):
- 解:x 的取值(比如 x=1、x=2);
- 目标函数:f (x)=x²-4x+3(计算 x 对应的函数值,值越小解越好);
- 举例 2(调模型超参数):
- 解:模型的超参数组合(比如学习率 = 0.01、树深度 = 5);
- 目标函数:模型的测试集准确率(值越大解越好)。
步骤 2:设置 3 个关键参数(直接套模板)
不用自己瞎琢磨,按以下模板设参数,90% 场景都能用:
| 参数名称 | 小白推荐值 | 作用说明 | 调整技巧 |
|---|---|---|---|
| 初始温度(T) | 100~200 | 初期冒险意愿,太高会浪费时间,太低易困在局部最优 | 问题越复杂,设越高(比如找最优路径设 200) |
| 最低温度(T_min) | 0.1~1 | 停止条件(温度低于这个值,就结束) | 设太小会变慢,设太大可能没找到最优,默认 0.1 |
| 降温系数 | 0.9~0.98 | 每次温度降低的比例(比如 0.95 = 每次降 5%) | 想快就设 0.9(降温快),想准就设 0.98(降温慢) |
步骤 3:初始化(找个 “起点” 开始)
- 核心动作:随机选一个 “初始解”,把它当成 “当前最好的解”;
- 举例(找函数最小值):
- 随机选 x 的初始值(比如从 - 5 到 10 里随机挑一个,比如 x=3);
- 计算初始解的目标函数值(f (3)=3²-4×3+3=0);
- 设 “当前最好的解”(x_best)=3,“当前最好的函数值”=0。
步骤 4:主循环(核心逻辑,按代码模板走)
这是算法的 “心脏”,但不用懂原理,直接套以下代码模板(已标小白注释):
# 模板:模拟退火算法主循环(以找函数最小值为例)
import numpy as np
# 1. 先定义目标函数(根据你的问题改)
def target_function(x):
return x**2 - 4*x + 3 # 要找最小值的函数
# 2. 按步骤2设参数
T = 100.0 # 初始温度
T_min = 0.1 # 最低温度
cooling_rate = 0.95# 降温系数
x_current = np.random.uniform(-5, 10) # 随机初始解(x范围:-5到10)
x_best = x_current # 初始最好的解
# 3. 主循环(温度没降到最低,就一直跑)
while T > T_min:
# ① 生成一个“新解”(在当前解附近小范围波动,类似“看相邻小区”)
x_new = x_current + np.random.normal(0, 1) # 围绕当前x随机动一点(不用改)
# ② 算当前解和新解的“好坏”(目标函数值)
f_current = target_function(x_current) # 当前解的函数值
f_new = target_function(x_new) # 新解的函数值
# ③ 判断要不要接受新解(核心!按规则来)
if f_new < f_current:
# 新解更好(比如新小区房价更低),直接接受
x_current = x_new
# 如果新解比“历史最好解”还好,更新历史最好解
if f_new < target_function(x_best):
x_best = x_new
else:
# 新解更差(比如新小区房价更高),按概率接受(温度越高,概率越大)
probability = np.exp((f_current - f_new) / T) # 概率公式(直接抄)
if np.random.random() < probability: # 随机数小于概率,就接受
x_current = x_new
# ④ 降低温度(冒险意愿减弱)
T *= cooling_rate
# 4. 输出结果
print(f"找到的最优解:x = {x_best:.2f}")
print(f"最优解对应的函数最小值:{target_function(x_best):.2f}")
步骤 5:验证结果(看看对不对)
- 简单验证:比如找函数 f (x)=x²-4x+3 的最小值,理论上 x=2 时最小值 =-1,若算法输出 x≈2、值≈-1,就是对的;
- 画图直观看:跑代码时加一段画图代码(模板如下),看 “探索路径” 是不是 “先乱走,后聚焦到最优解”:
import matplotlib.pyplot as plt
# 画函数曲线
x_all = np.linspace(-5, 10, 1000) # 生成很多x值
y_all = target_function(x_all) # 对应的函数值
plt.plot(x_all, y_all, color='blue', label='函数曲线')
# 画探索过程(需要在主循环里加history记录,见下文)
# 先在主循环前加:history = [x_current]
# 主循环最后加:history.append(x_current)
plt.scatter(history, [target_function(h) for h in history],
color='red', s=10, label='探索路径')
# 画最优解
plt.scatter(x_best, target_function(x_best),
color='green', s=100, label=f'最优解(x={x_best:.2f})', zorder=5)
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
四、常见问题小白解决方案(避坑指南)
| 遇到的问题 | 可能原因 | 解决方案 |
|---|---|---|
| 结果不对(比如没找到最小值) | 初始温度太低 | 把初始温度从 100 改成 200,再跑一次 |
| 运行太慢 | 最低温度设太小(比如 0.001) | 把 T_min 改成 0.1,减少循环次数 |
| 结果每次不一样 | 初始解是随机的 | 多跑几次,取多次结果里最好的那个 |
| 不知道目标函数怎么写 | 没明确 “解好不好” 的标准 | 比如调模型超参数,目标函数就是 “模型准确率” |
五、适用场景清单(知道什么时候用)
不用记复杂场景,记住以下 3 个常用场景,够用了:
- 找最优值:比如找函数最小值 / 最大值、找城市里最短的路线;
- 调超参数:比如用退火算法优化随机森林的 “树数量”“树深度”,让模型更准;
- 组合优化:比如安排工厂生产计划(哪种产品生产多少,利润最高)。
六、关键口诀(记不住就看这个)
- 参数设:初始温度 100,最低 0.1,降温 0.95;
- 循环跑:温度没到 0.1,就一直找新解;
- 接不接:新解好就直接接,新解差看概率;
- 验结果:画图看路径,多跑几次取最优。

804

被折叠的 条评论
为什么被折叠?



