优化算法
坐标轮转法(Coordinate Rotation)
一句话概括:把"同时调 8 个旋钮"的难题,拆成"一次只调 1 个旋钮,调完换下一个,一轮一轮来"。
学术上叫 Cyclic Coordinate Descent 或 Coordinate Rotation。核心思想是:把高维优化问题分解成一维子问题的序列。
直观理解:
想象你站在一个漆黑的 8 维房间里,想找到最低洼的位置。你看不到整个地形,但可以用脚试探。坐标轮转法就是:
- 沿着第 1 个方向(转速轴)走几步,找到这个方向上的最低点
- 停下来,转而沿着第 2 个方向(负荷轴)走,找到这个方向上的最低点
- ……一直走到第 8 个方向
- 8 个方向走完算"一轮",下一轮从头再来
-
直到所有方向都走不动了(位置不再明显变化),停止
算法流程图:
一维搜索(Line Search)
每一轮对每个维度,固定其他 7 个参数不变,只在当前维度上找最优值。搜索策略:网格法
# 伪代码:第 dim 维的一维搜索
def line_search(x, dim):
best_x = x.copy()
best_score = ∞
# 在该维度边界内均匀取 20 个试探点
for val in [low, low+step, ..., high]:
x_try = x.copy()
x_try[dim] = val # 只改这一个维度
y_norm = 模型预测(x_try) # 调用 ResNet/SVR
score = 约束得分(y_norm) # 评估好坏
if score < best_score:
best_x = x_try
best_score = score
return best_x, best_score
约束得分函数
这是连接优化器和实际问题的心脏。不是简单的最小化排放量,而是先确保达标,再追求冗余
设计思路:两级优先级
权重 10000 是对超标个数的惩罚——让你用尽全力先减少超标目标数。
权重 100 是对超标幅度的惩罚——超标数相同的情况下,选超得少的。
全部达标后变成负值,用裕度奖励作为 tie-breaker——达标的解里,选离阈值最远、最稳健的那个。
def constraint_score(y_norm): # 返回值越小越好
violations = max(0, y_norm - threshold) # 数组:13个目标各自的超限量
n_violations = 超标目标个数
total_violation = 超限幅度之和
margin = 达标目标距离阈值的剩余空间之和
if n_violations == 0: # ★ 全部达标 —— 大胜利
return -margin × 10 # 负值,裕度越大越"负"=越好
else: # 有超标
return n_violations × 10000 + total_violation × 100
完整迭代流程
def optimize(x0=None):
x = x0 or 边界中点 # 初始值:每个维度取 [min+max]/2
best = (x, 预测值, 物理量, 达标数, 是否全达标)
for 轮次 in range(100): # 最多 100 轮
x_prev = x.copy()
for dim in range(8): # 0→1→2→3→4→5→6→7
x = line_search(x, dim) # 在当前维度上找最优
# 更新全局最优
if current_score < best.score:
best = (x, ...)
# 收敛判断:所有维度的变化量都小于阈值
if max| x[i] - x_prev[i] | < 1e-6:
break # 走不动了,停止
return best
优缺点
优点:
- 实现极简:核心逻辑不到 40 行,不需要计算梯度
- 每步都保证提升:一维搜索只接受更好的解,不会"走错路"
- 局部精细:在初始点附近能找到让约束得分最低的局部最优
- 收敛保证:只要目标函数在每个维度上有唯一最优,算法一定收敛
缺点:
- 严重依赖初值:从边界中点出发,只能找到"附近"的最优,如果可行解在完全不同的区域,它根本探索不到
- 不能处理变量耦合:如果转速和负荷的最优值互相依赖(调转速的同时也需要调负荷才能达标),轮流调可能永远找不到联合最优
- 容易掉进"伪坑":因为约束得分的阶梯式跳跃,可能在某个维度上"看起来每个点都不如当前点",但实际上换个维度走一步就能发现新天地——这种复杂地形坐标轮转法搞不定
遗传算法(Genetic Algorithm)
一句话概括:模拟自然进化——生成一群候选解(种群),让好的"交配"产生后代,随机"变异"引入新基因,一代一代淘汰差的、保留好的,最后活下来的就是最优个体。
生物类比
| 生物学概念 | 优化算法对应 | 本项目中的实现 |
|---|---|---|
| 种群(Population) | 100 组参数,同时探索不同区域 | 100 个 8 维向量,在边界内随机初始化 |
| 个体(Individual) | 一组具体的控制参数 | [转速, 负荷, VVT进气, VVT排气, 点火角, 喷1相位, 喷2相位, 喷油比] |
| 基因(Gene) | 单个参数的值 | 比如"点火角 = 15.3°"就是一个基因位点 |
| 适应度(Fitness) | 这组参数有多"好" | 约束得分函数(全部达标→正分,有超标→负分) |
| 选择(Selection) | 适应度高的个体更可能被选为父母 | 轮盘赌选择:概率 ∝ 适应度 |
| 交叉(Crossover) | 两个父母的基因混合产生后代 | SBX 模拟二进制交叉 |
| 变异(Mutation) | 基因随机微调,引入新特性 | 高斯变异:加 N(0, σ) |
| 精英保留(Elitism) | 最优秀的个体直接进入下一代 | 每代保留前 2 名 |
算法流程图

适应度函数
跟坐标轮转法用同一个约束得分函数,但 GA 需要正值才能做概率选择,所以套了一层映射:
def fitness(x):
y_norm = 模型预测(x)
violations = max(0, y_norm - threshold)
n_v = 超标个数
total_v = 超标幅度和
margin = 达标裕度和
if n_v == 0: # ★ 完美个体
return 10000.0 + margin × 10 # 正值,越大越好
else: # 有缺陷个体
return -(n_v × 10000 + total_v × 100) # 负值,-20000 比 -30000 好
选择算子 — 轮盘赌
不是"最强的 3 个直接选",而是按适应度比例随机抽——适应度越高,被抽到的概率越大,但低适应度的也有微小机会。
def selection(population, fitnesses):
# 1. 偏移:把所有负值变成非负(加 |min_fitness|)
shift = -min(fitnesses) if min(fitnesses) < 0 else 0
adj_fitness = [f + shift for f in fitnesses]
# 2. 归一化概率
total = sum(adj_fitness)
probs = [f / total for f in adj_fitness]
# 3. 按概率抽 2 个(不放回)
return random.choices(population, weights=probs, k=2)
为什么要随机而不是直接选前两名?
保持基因多样性。如果每代只选前两名交配,种群很快就"近亲繁殖"——所有个体趋同,失去探索能力。偶尔让"不太行但有点意思"的个体参与繁殖,可能产生出人意料的好后代(就像两个普通人也能生出天才)。
极端情况:假设有一个个体在"转速"维度有独特的好基因但整体适应度不算顶尖——确定性选择会直接淘汰它,轮盘赌还有概率保留它参与交配,把这个优点传下去。
交叉算子 — SBX(Simulated Binary Crossover)
这是遗传算法里最精巧的部分。SBX 模拟了二进制编码遗传算法中单点交叉的行为,但直接操作实数。
直觉:两个父母的值分别是 10 和 50,SBX 产生的子代大概率落在 10~50 之间,且有概率超出这个范围(扩展搜索)。概率分布的"宽窄"由分布指数 η 控制。
def SBX(parent1, parent2, eta=15):
child1, child2 = parent1.copy(), parent2.copy()
for i in range(8): # 每个基因独立判断是否交叉
if random() < 0.5: # 50%概率该基因发生交叉
continue # 跳过,保留父母原值
# 确保 y1 ≤ y2
y1, y2 = sorted([parent1[i], parent2[i]])
# 计算扩展因子 β
rand = random()
if rand ≤ 1/α: # 大概率:子代在父母之间(收缩)
βq = (rand × α)^(1/(η+1))
else: # 小概率:子代超出父母范围(扩展)
βq = (1/(2 - rand × α))^(1/(η+1))
# 生成两个子代
child1[i] = 0.5[(y1+y2) - βq(y2-y1)]
child2[i] = 0.5[(y1+y2) + βq(y2-y1)]
return child1, child2
SBX 比简单"取平均"好在哪?
- 取平均:`child = (p1+p2)/2` → 子代永远是父母的中点,搜索空间越缩越小
- 均匀随机:`child = uniform(p1, p2)` → 子代在父母之间均匀分布,但永远不会超越父母
- SBX:子代大概率在父母之间(继承好基因),但小概率超出父母范围(探索新领域)——既有利用又有探索
η=15 意味着分布比较集中在父母之间,扩展的概率较小(约 3%),保证了搜索的稳定性。
变异算子 — 高斯变异
每个基因以 10% 的独立概率发生变异:
def mutate(individual):
for i in range(8):
if random() < 0.1: # 10% 概率变异
σ = (bound_max - bound_min) × 0.1 # 标准差 = 参数范围的 10%
individual[i] += N(0, σ) # 加一个小的随机扰动
individual[i] = clamp(individual[i], bounds[i])
return individual
变异率和步长的权衡:
- 变异率 10% 不算低——对于 8 维个体,期望每代每个个体有 0.8 个基因发生变异
- 步长 σ = 范围×10%,比如转速范围 5400,σ=540,变异会加减约 ±500 rpm——足够大,能跳出局部
- 配合精英保留(top 2 不参与变异),保证"最好基因"不会因为变异走偏
精英保留 — 保底机制
每代适应度最高的 2 个个体原封不动复制到下一代,不参与交叉和变异。
为什么要精英保留?
没有这个机制,可能出现一种悲剧:第 50 代找到了一个全部达标的完美个体,但第 51 代交叉/变异的随机性把它"弄坏了"——后代都不如它。精英保留保证"史上最强"的个体绝不会因为随机操作而丢失,适应度单调不降。
完整的进化循环
def optimize(x0=None):
# === 第一步:初始化种群 ===
种群 = [random(bounds) for _ in range(100)]
if x0: 种群[0] = x0 # 注入用户当前参数
全局最优 = None
全局最优适应度 = -∞
# === 第二步:世代进化 ===
for gen in range(200):
# 评估
适应度 = [fitness(ind) for ind in 种群]
这一代最优 = argmax(适应度)
if 适应度[这一代最优] > 全局最优适应度:
更新全局最优
# 精英保留 (top 2)
精英 = 种群[适应度最高的2个]
# 生成新一代 (填充剩余 98 个位置)
新种群 = []
while len(新种群) < 98:
p1, p2 = 轮盘赌选择(种群, 适应度) # 选父母
c1, c2 = SBX交叉(p1, p2) # 交配生双胞胎
c1 = 高斯变异(c1) # 老大变异
c2 = 高斯变异(c2) # 老二变异
clip(c1), clip(c2) # 裁剪到边界内
新种群.extend([c1, c2])
种群 = 精英 + 新种群[:98] # 组成新一代
# === 第三步:收集所有可行解 ===
可行解 = [ind for ind in 种群 if 全部达标(ind)]
return 全局最优, 历史, 可行解
两种算法的对比
| 维度 | 坐标轮转法 | 遗传算法 |
|---|---|---|
| 搜索方式 | 确定性:逐维网格搜索,每次都取最优 | 随机性:种群竞争 + 交叉变异,概率驱动 |
| 探索 vs 利用 | 纯利用——只在当前点附近精细优化 | 探索为主(交叉+变异) + 利用为辅(精英保留) |
| 全局性 | 局部——从初值出发,走不远 | 全局——100 个个体分布在搜索空间各处 |
| 收敛速度 | 快(每轮 8×20=160 次模型调用) | 慢(每代 100 次模型调用,200 代 = 20000 次) |
| 依赖初值 | 重度依赖——起点决定终点 | 几乎不依赖——随机初始化覆盖全空间 |
| 处理耦合 | 弱——变量耦合时容易卡住 | 强——交叉操作天然处理多变量协同 |
| 计算成本 | 低——通常 5~10 轮收敛 | 高——固定 200 代,不可提前停止 |
| 最优性保证 | 无——只能保证局部最优 | 无——启发式,但有概率逼近全局最优 |
| 可并行性 | 差——串行依赖,必须一个维度一个维度来 | 好——种群内个体可并行评估适应度 |
一句话总结:
坐标轮转法像一个在坡上小心翼翼往下走的人——步子小、走得稳,但视野窄,只能走到最近的洼地。
遗传算法像撒 100 个人到 8 维空间随机探索——有人掉进坑、有人爬上山、有人走到平原,活下来的都是各自区域的最强者,最后比谁最强。
两个算法如何结合
# gui.py _optimize() 方法(简化)
def _optimize(self):
x0 = 当前UI界面的参数值
# 两个算法同时跑,都用当前参数作为初始值注入
result_cr = 坐标轮转法.optimize(x0)
result_ga = 遗传算法.optimize(x0)
# 择优:先比达标数,再比超限幅度
if quality(ga) > quality(cr):
最优 = ga # GA 找到的更优
else:
最优 = cr # 坐标轮转法更优
show(最优) # 展示结果,询问是否应用到参数滑块
互补逻辑
坐标轮转法如果初值已经接近可行域边界,它能精细地"挤"进去——像一个精雕师傅,在已有的好方案上微调。
遗传算法如果初值离可行域很远,它能跨越空间找到全新的区域——像一个探险队,撒出 100 人去找哪里有金矿。
两者取优:GA 找到了新区域、坐标轮转法在 GA 的基础上还能再精调——但因为代码里 GUI 是分别调两个算法的 optimize() 然后择优,没有串行接力。实践中谁的结果好就用谁。
参考项目:https://github.com/2143616/ai_calibration

浙公网安备 33010602011771号