DAY 13 启发式算法

超参数调整专题3

1.  三种启发式算法的思想:遗传算法、粒子群算法、退火算法

2.  列表推导式

这张图展示的是二元函数 z=f(x,y) 的三维曲面,用来直观解释函数的极值(极大值、极小值)与最值(最大值、最小值) 的概念,核心是区分 “局部” 和 “全局” 的高低 / 深浅。

1. 坐标系与函数形态

  • 水平面的 x 轴、y 轴是自变量的取值范围,垂直的 z 轴是函数值(可以理解为 “高度” 或 “深度”)。
  • 整个曲面 z=f(x,y) 可以想象成 “起伏的山地”—— 有山峰、山谷,也有全局的最高山和最低谷。

2. 极大值 vs 最大值

  • 极大值:曲面上的 “局部山峰”(如图中两处标注 “极大值” 的区域)。它们在自己周围的小范围内是最高的,但不一定是整个曲面的最高点。
  • 最大值:曲面上的 “全局最高峰”(如图中标注 “最大值、极大值” 的区域)。它既是一个局部极大值,也是整个函数定义域内的最高值

3. 极小值 vs 最小值

  • 极小值:曲面上的 “局部山谷”(如图中两处标注 “极小值” 的区域)。它们在自己周围的小范围内是最低的,但不一定是整个曲面的最低点。
  • 最小值:曲面上的 “全局最深谷”(如图中标注 “最小值、极小值” 的区域)。它既是一个局部极小值,也是整个函数定义域内的最低值

总结

这张图用 “山地模型” 把抽象的数学概念可视化:

  • 局部的 “山峰 / 山谷” 对应极大值 / 极小值(只在小范围里最优);
  • 全局的 “最高峰 / 最深谷” 对应最大值 / 最小值(在整个范围内最优)。这种可视化能帮助理解 “为什么有些算法会陷入局部最优(比如困在一个小山峰里,以为是最高点),而我们需要找全局最优(真正的最高峰)”。

启发式算法

一、遗传算法:模仿生物进化的 “优胜劣汰”

遗传算法思想

灵感来源:达尔文的生物进化论(物竞天择,适者生存)。核心思想:把 “问题的每个可能解” 当成一个 “生物个体”,通过 “繁殖、变异” 让好的解留下,差的解淘汰,一代代进化出更好的解。

可以想象成 “养一群会解题的小生物”:

  1. 每个小生物的 “基因” 就是它的解题方案(比如解一个 “怎么安排路线最短” 的问题,基因就是具体的路线顺序);
  2. 先随机生成一群 “初始生物”(随机的路线方案),然后给它们打分(比如路线越短,分数越高,也就是 “适应度” 越高);
  3. 让分数高的生物更容易 “生孩子”(选择优质个体繁殖),孩子的基因会混合父母的基因(比如爸爸的前半段路线 + 妈妈的后半段路线,这叫 “交叉”);
  4. 偶尔孩子的基因会随机变一下(比如突然换了一个路口,这叫 “变异”),防止大家都困在同一个方案里;
  5. 重复上面的过程,过几代之后,剩下的生物基本都是 “高分选手”,它们的基因就是近似最优解了。

用 “学生考试” 的例子来一步步解释

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)及其对应的模型准确率(适应度): 

在这一代中,个体 I1I2的准确率最高,是当前表现最好的优秀解。 

2.  选择:根据准确率(适应度)进行选择,准确率越高的个体被选中作为“父代”进行繁殖的概率越大。

在 I1, I2, I3, I4, 中,I1 (0.88) 和 I2 (0.85) 更有可能被选中。

3.  交叉:选择表现好的来繁殖,交叉他们的参数

我们选取I1和I2作为来繁衍,交叉操作成功生成了两个新的、从未被评估过的参数组合 (450,25,4)和(200,12,8)

4.  变异(探索):随机改变新组合的部分参数,得到变异后的参数组合

5.  替换:用新参数组合替换掉之前表现差的个体新的种群重新评估

6.  循环这一操作

二、粒子群算法:模仿鸟群 / 鱼群的 “群体学习”

灵感来源:鸟群找食物、鱼群游动的规律(个体跟着群体里的 “高手” 和自己的 “经验” 走)。核心思想:把 “问题的每个可能解” 当成一个 “会飞的粒子”,粒子们通过互相学习(看别人的好位置)和总结自己的经验,慢慢飞向最优解的位置。

可以想象成 “一群鸟找最好吃的果子”:

  1. 每只鸟(粒子)都在一个 “可能有果子的区域” 里飞,它的位置就是一个解题方案(比如果子最多的地方就是最优解);
  2. 每只鸟都记着两个信息:自己飞过的 “最好位置”(比如之前找到过 5 个果子的地方),以及整个鸟群目前找到的 “最好位置”(比如群里有只鸟找到过 10 个果子的地方);
  3. 接下来,每只鸟会调整自己的飞行方向和速度:既往自己的最好位置飞一点,也往群里的最好位置飞一点(相当于 “跟着自己的经验” 和 “跟着高手学”);
  4. 飞着飞着,整个鸟群会慢慢聚集到 “果子最多的地方”—— 也就是问题的最优解附近。

粒子群优化算法

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× 距离)。作用:群体协作,跟着高手学。

算法流程:鸟群找食物的步骤

就像一群鸟每天出门找食物,重复以下动作,直到找到食物最多的地方:

  1. 初始化:第一天,5 只鸟随机站在森林的不同地方(随机初始位置),一开始都慢慢飞(初始速度设为 0,相当于站着不动)。
  2. 评估:每只鸟看看自己脚下有多少食物(计算适应度,比如鸟 A 脚下有 80 颗,鸟 B 有 88 颗)。
  3. 记下来最好的地方
    • 每只鸟把自己当前位置当成 “自己历史最佳”(pbest),因为是第一天,还没去过别的地方。
    • 整个群里,食物最多的鸟 B 的位置(88 颗),就是 “群体最佳”(gbest)。
  4. 调整飞行,飞到新地方:每只鸟用上面的 3 个规则算新速度(比如鸟 A 综合惯性、自己的 pbest、群体的 gbest,决定往东北飞,速度 6 米 / 秒),然后按新速度飞到新位置。
  5. 重复:第二天,再评估新位置的食物量,更新自己的 pbest(如果新地方食物更多)和群体的 gbest(如果有鸟找到更多食物),再调整飞行…… 循环几十天,最后大家基本都会聚集到食物最多的地方(找到最优解)。

PSO 和 GA 的区别(一句话总结)

  • GA(遗传算法):像 “生物进化”,好的个体生宝宝(交叉),宝宝偶尔突变,差的个体被淘汰,是 “换一批新的”。
  • PSO(粒子群):像 “鸟群协作”,每只鸟都记得自己和大家的好地方,不断调整飞行靠近,是 “同一批鸟慢慢挪”,互相学习,不淘汰谁。

是不是很简单?PSO 的核心就是 “一群个体带着记忆,跟着自己和群体的经验慢慢调整,最后一起找到最好的答案”~

三、模拟退火算法:模仿金属 “慢慢降温” 的稳定过程

灵感来源:物理上的 “退火”(金属加热后慢慢降温,原子会排列得更稳定,能量更低)。核心思想:先 “大胆试错”(接受暂时变差的解),再 “逐渐收敛”(越来越少接受差解),最后稳定在一个好解上。

可以想象成 “在山里找最高峰”:

  1. 一开始你在山里随机走(相当于高温状态,原子运动剧烈),哪怕走到一个比现在低的地方(暂时变差的解),你也愿意试试(因为可能绕个弯能找到更高的山);
  2. 随着时间推移(温度降低),你越来越 “保守”:如果下一步是上坡(更好的解),你肯定走;但如果是下坡(更差的解),你几乎不怎么走了(偶尔走一下,防止困在小土坡上以为是山顶);
  3. 最后温度降到很低,你基本只在附近的高点转悠,最终停在的位置就是近似最高峰(最优解)。

模拟退火算法里的 “温度参数”,可以理解成 “人的冒险意愿”—— 温度越高,人越敢冒险;温度越低,人越保守。它的核心作用是让算法在 “大胆探索新方向” 和 “专注优化好结果” 之间找到平衡 。

举个生活例子:假设你在山里找最高峰(目标是找到最优解),温度就像你 “敢不敢走下坡路” 的心态:

  • 高温时:你刚进山,对地形一无所知。这时候你 “胆子很大”—— 哪怕眼前是下坡(暂时找到的高度变低,也就是 “更差的解”),你也愿意走过去试试。为什么?因为你不知道下坡后面是不是藏着更高的山(全局最优解)。比如你站在一个小土坡上(局部最优),看起来很高,但旁边有个下坡,坡底后面其实有座珠穆朗玛峰。如果一开始就不敢下坡,你永远找不到珠峰。

  • 温度慢慢降低:随着你在山里走了一段时间,对地形有了些了解,“胆子” 开始变小。这时候如果遇到下坡,你会犹豫:只有特别短的下坡(稍微差一点的解),你才可能试试;太长的下坡(差很多的解),你基本不碰了。

  • 低温时:你已经离最高峰很近了,这时候 “胆子极小”—— 只愿意往上走(接受更好的解),绝对不往下走(拒绝更差的解)。最终你会在最高峰附近徘徊,直到停在山顶。

所以温度参数的作用很关键:

  • 高温阶段:保证算法 “不固执”,敢于跳出暂时的 “小甜头”(局部最优解),去探索更广阔的区域,避免错过真正的 “大赢家”(全局最优解)。
  • 低温阶段:让算法 “收心”,不再瞎折腾,专注在已找到的好区域里打磨细节,最终稳定在最优解附近。

就像烧金属时,必须 “先高温加热让原子自由运动,再慢慢降温让原子稳定排列”—— 温度的变化节奏,直接决定了最终能不能找到最好的结果。

列表推导式

今天的代码用到了这个知识点,这是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 循环:

  1. # 普通for循环可以加打印
    squares = []
    for x in range(1,6):
        square = x**2
        print(f"正在处理:{x},平方是:{square}")  # 列表推导式无法加这行
        squares.append(square)

调试不方便

列表推导式是一行代码,如果运行出错(比如表达式写错),很难定位具体哪里出问题。而普通 for 循环可以逐行调试,比如在循环中加断点,查看每一步变量的值,更容易找到错误。

三、普通 for 循环的优点

灵活性高,能做更多事

除了生成列表,还能在循环中加入任意操作:打印日志、条件判断后跳出循环(break)、跳过当前元素(continue)、修改其他变量等。比如生成列表时,遇到某个值就停止:

  1. 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

避坑提醒(小白必记)

  1. 分类任务用分类模型 + 分类指标:模型:逻辑回归、决策树、随机森林;指标:准确率(accuracy)、混淆矩阵。
  2. 回归任务用回归模型 + 回归指标:模型:线性回归、Ridge 回归;指标:均方误差(MSE)、R² 分数。
  3. 永远先明确任务类型:拿到数据先看标签 —— 是 “类别”(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)}个)")

六、小白避坑指南

  1. 标签列名错误:若第一步报错 “KeyError: ['target']”,打开第一步的 “数据列名” 输出,把drop('target')['target']换成真实标签列名(如'label')。
  2. 模型类型错误:代码只针对 “分类模型”,不要加线性回归(回归模型不适合此任务)。
  3. 运行速度慢:若觉得慢,可减小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()

四、代码运行后能看到什么?

  1. 输出结果:会显示 “最优解 x≈2.00,函数最小值≈-1.00”,和理论结果一致;
  2. 图表:蓝色曲线是函数本身,红色小点是算法 “探索的路径”(先乱走,后聚焦),绿色大圆点是最终找到的最优解。

总结:退火算法的核心优势

  • 不怕 “局部最优陷阱”:高温阶段的试错能跳出 “小山峰”,找到 “全局最高峰”;
  • 逻辑简单:不用复杂的交叉、变异(对比遗传算法),核心就是 “降温 + 概率接受”;
  • 适用场景广:比如找最优路径、调模型超参数、设计最优结构等。

作为小白,先记住 “先乱闯再聚焦” 的思想,再结合这个代码案例跑一遍,就能真正理解退火算法啦~

一、核心思想速记(3 句话搞懂)

  1. 模仿自然:像金属 “高温加热→慢慢降温→原子稳定排列” 一样,算法从 “大胆试错” 到 “专注最优”;
  2. 核心逻辑:温度越高,越敢接受 “变差的解”(避免困在局部最优);温度越低,越只接受 “更好的解”(稳定到全局最优);
  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:初始化(找个 “起点” 开始)

  • 核心动作:随机选一个 “初始解”,把它当成 “当前最好的解”;
  • 举例(找函数最小值)
    1. 随机选 x 的初始值(比如从 - 5 到 10 里随机挑一个,比如 x=3);
    2. 计算初始解的目标函数值(f (3)=3²-4×3+3=0);
    3. 设 “当前最好的解”(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 个常用场景,够用了:

  1. 找最优值:比如找函数最小值 / 最大值、找城市里最短的路线;
  2. 调超参数:比如用退火算法优化随机森林的 “树数量”“树深度”,让模型更准;
  3. 组合优化:比如安排工厂生产计划(哪种产品生产多少,利润最高)。

六、关键口诀(记不住就看这个)

  1. 参数设:初始温度 100,最低 0.1,降温 0.95;
  2. 循环跑:温度没到 0.1,就一直找新解;
  3. 接不接:新解好就直接接,新解差看概率;
  4. 验结果:画图看路径,多跑几次取最优。

浙大疏锦行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值