优化算法

坐标轮转法(Coordinate Rotation)

一句话概括:把"同时调 8 个旋钮"的难题,拆成"一次只调 1 个旋钮,调完换下一个,一轮一轮来"。

学术上叫 Cyclic Coordinate Descent 或 Coordinate Rotation。核心思想是:把高维优化问题分解成一维子问题的序列。

直观理解:

想象你站在一个漆黑的 8 维房间里,想找到最低洼的位置。你看不到整个地形,但可以用脚试探。坐标轮转法就是:

  1. 沿着第 1 个方向(转速轴)走几步,找到这个方向上的最低点
  2. 停下来,转而沿着第 2 个方向(负荷轴)走,找到这个方向上的最低点
  3. ……一直走到第 8 个方向
  4. 8 个方向走完算"一轮",下一轮从头再来
  5. 直到所有方向都走不动了(位置不再明显变化),停止

算法流程图:

初始化 x₀(边界中点)dim = 0(一维搜索)dim = 1(一维搜索)···dim = 7(一维搜索)收敛?max|Δx| < tol✓是 → 输出最优 x→ 否 → 回到 dim=0 开始下一轮

一维搜索(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 名

算法流程图

image

适应度函数

跟坐标轮转法用同一个约束得分函数,但 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

posted @ 2026-07-23 07:43  黄艺龙  阅读(4)  评论(0)    收藏  举报