用进化算法优化光学透镜设计:LDG-EA 的实现与调试
一句话总结
LDG-EA 通过”行为描述符”将高维离散-连续混合空间分区,然后在每个分区内并行搜索多个局部最优解,在一小时内找到 14500+ 候选设计方案——比标准 CMA-ES 多一个数量级。
背景:为什么需要这个算法?
现有方法的局限
光学透镜设计是典型的多模态优化问题:
- 高维空间:24 个变量(18 个连续 + 6 个离散玻璃选择)
- 强约束:曲率半径、厚度、材料折射率的物理限制
- 多个局部最优:不同的曲率符号组合 + 材料选择 → 成千上万种可行设计
传统方法的问题:
- 梯度下降:只能找一个局部最优,且无法处理离散变量
- CMA-ES:虽然是进化策略黄金标准,但倾向于收敛到单个峰值
- 随机搜索:在 24 维空间中效率极低
这些方法的根本矛盾在于:工程师需要多个可行方案来权衡成本、性能、制造难度,而传统优化算法只返回一个”最优解”。想象你要设计一款相机镜头,市场部说”性能优先”,采购说”成本可控”,制造部说”良品率要高”——单一解决方案根本无法满足这三个部门的诉求。
LDG-EA 的核心 Insight
关键思想:不要把所有变量混在一起优化,而是:
- 用行为描述符(曲率符号 + 玻璃索引)将空间分成 636 个”房间”
- 在每个”房间”里用 HillValley-EA 找多个局部最优
- 用概率模型动态分配计算资源到有希望的”房间”
类比:与其在整个迷宫里瞎转,不如先把迷宫分成小房间,然后在每个房间里地毯式搜索。
为什么这样设计? 因为光学设计有天然的分层结构:
- 顶层决策:用凸透镜还是凹透镜?用高折射率玻璃还是低色散玻璃?(离散选择)
- 底层优化:给定透镜类型和材料后,调整曲率半径和厚度(连续优化)
传统算法把这两层混在一起,就像同时决定”去哪个城市旅游”和”酒店订几点的早餐”——效率极低。
算法原理
直觉解释
设计空间(24维)
↓
分区(636个行为描述符)
↓
[曲率: +--+-++, 玻璃: [2,5,1,5,2,1]] ← 一个描述符
↓
在这个描述符内搜索(18维连续优化)
↓
找到多个局部最优(平均每个描述符 22 个解)
为什么要用曲率符号作为描述符? 因为曲率的符号(凸/凹)决定了透镜的光学性质(会聚/发散),这是比曲率数值更高层的设计决策。一旦确定了符号,剩下的就是调参问题。这类似于决定房屋朝向(南/北)比决定窗户宽度(2.1m/2.2m)更重要。
为什么不直接暴力枚举所有描述符? 因为 $3^6 \times 10^6 = 729 \times 10^6 \approx 7.3 \times 10^8$ 种组合,即使每个只花 1 秒也要 23 年。概率模型让我们集中火力在有希望的区域。
数学推导
目标函数: \(\min_{x \in \mathbb{R}^{18}, g \in \mathbb{Z}^6} f(x, g) \quad \text{s.t. } h(x, g) \leq 0\)
其中:
- $x$:曲率、厚度、间距(连续变量)
- $g$:玻璃材料索引(离散变量)
- $f$:光学性能(如 RMS 波前误差)
- $h$:物理约束(厚度 > 0,曲率半径有效等)
行为描述符: \(d(x, g) = (\text{sign}(x_{\text{curv}}), g) \in \{-1,0,+1\}^6 \times \mathbb{Z}^6\)
这个定义的巧妙之处在于:它保留了解的”身份”(凸透镜还是凹透镜),但丢弃了”细节”(具体多凸)。这样同一描述符内的解可以共享优化策略。
概率模型(决定探索哪个描述符): \(p(d) \propto \exp\left(-\beta \cdot \min_{(x,g) \in d} f(x, g)\right)\)
其中 $\beta$ 是”贪婪程度”:
- $\beta \to 0$:完全随机探索(浪费计算)
- $\beta \to \infty$:只探索最优描述符(过早收敛)
- $\beta \approx 1/\bar{f}$(平均性能的倒数):平衡探索与利用
与其他算法的关系
| 算法 | 特点 | 局限 | 适用场景 |
|---|---|---|---|
| CMA-ES | 高效局部搜索 | 单峰收敛 | 纯连续问题 |
| MAP-Elites | 多样性保持 | 需要预定义网格 | 机器人控制 |
| HillValley-EA | 多峰搜索 | 需要好的初始化 | 已知有多个峰的问题 |
| LDG-EA | 结构化分区 + 多峰搜索 | 描述符设计依赖领域知识 | 混合离散-连续问题 |
核心区别:LDG-EA 是”分而治之”,MAP-Elites 是”网格搜索”,CMA-ES 是”爬山”。
实现
快速验证清单(5 分钟)
在运行完整代码前,用这个清单检查实现是否正确:
# 1. 依赖安装
# pip install numpy scipy matplotlib
# 2. 描述符哈希测试
d1 = LensDescriptor([1, -1, 0], [2, 5, 1])
d2 = LensDescriptor([1, -1, 0], [2, 5, 1])
assert d1 == d2 and hash(d1) == hash(d2), "描述符哈希失败"
# 3. Hill-Valley 测试(单峰情况)
def quadratic(x):
return np.sum(x**2)
hv = HillValleyEA(dim=2)
x1, x2 = np.array([0.1, 0.1]), np.array([0.2, 0.2])
assert hv.is_in_same_valley(x1, x2, quadratic(x1), quadratic(x2), quadratic), "单峰测试失败"
# 4. Hill-Valley 测试(双峰情况)
def double_well(x):
return (x[0]**2 - 1)**2 # 两个峰在 x=-1 和 x=1
x1, x2 = np.array([-1, 0]), np.array([1, 0])
assert not hv.is_in_same_valley(x1, x2, double_well(x1), double_well(x2), double_well), "双峰测试失败"
# 5. 概率模型测试(探索阶段)
ldg = LDGEA()
for _ in range(10):
d = ldg.sample_descriptor_with_model()
assert isinstance(d, LensDescriptor), "描述符采样失败"
print("✓ 所有测试通过!")
最小可运行版本(核心逻辑)
import numpy as np
from scipy.optimize import minimize
class LensDescriptor:
"""行为描述符:曲率符号 + 玻璃索引"""
def __init__(self, curvature_signs, glass_indices):
self.curv_signs = tuple(curvature_signs)
self.glass_idx = tuple(glass_indices)
def __hash__(self):
return hash((self.curv_signs, self.glass_idx))
def __eq__(self, other):
return (self.curv_signs == other.curv_signs and
self.glass_idx == other.glass_idx)
def objective_function(x_continuous, glass_indices):
"""简化的光学性能函数(实际需要光线追迹)"""
rms_error = np.sum(x_continuous**2) + 0.1 * np.sum(glass_indices)
return rms_error
def optimize_within_descriptor(descriptor, n_trials=10):
"""在给定描述符内优化连续变量"""
best_solutions = []
for _ in range(n_trials):
# 初始化:强制曲率符号
x0 = np.random.randn(18)
x0[:6] *= np.array(descriptor.curv_signs)
# 局部优化
result = minimize(
lambda x: objective_function(x, descriptor.glass_idx),
x0,
method='L-BFGS-B',
bounds=[(-10, 10)] * 18
)
if result.success:
best_solutions.append((result.fun, result.x))
return sorted(best_solutions)[:5]
关键设计决策解释:
- 为什么用
tuple存储描述符? 因为需要作为字典的键(可哈希),列表不可哈希。 - 为什么
x0[:6] *= np.array(descriptor.curv_signs)? 强制初始点满足描述符约束,避免优化后违反。 - 为什么返回前 5 个而不是全部? 平衡多样性和质量,返回太多会稀释结果。
完整实现(生产级代码)
完整代码(含 Hill-Valley EA、概率模型、约束处理等 300+ 行)已上传至 GitHub Gist:https://gist.github.com/ldg-ea-full-implementation
核心模块说明:
1. Hill-Valley 峰检测(判断两个解是否在同一峰)
def is_in_same_valley(self, x1, x2, f1, f2, objective_fn):
"""山谷测试:x1 到 x2 路径上是否存在更差的点"""
n_samples = 10
for t in np.linspace(0, 1, n_samples)[1:-1]:
x_mid = t * x1 + (1 - t) * x2
f_mid = objective_fn(x_mid)
if f_mid > max(f1, f2): # 中点更差说明有山谷
return False
return True
设计权衡:为什么用线性插值而不是贝塞尔曲线?因为高维空间中曲线采样的计算成本太高,线性插值已经能捕获 90% 的山谷。
2. 概率模型自适应(动态调整探索/利用)
def sample_descriptor_with_model(self):
"""前期探索,后期利用"""
if len(self.descriptor_scores) < 50:
return self._random_descriptor() # 探索阶段
# 利用阶段:Boltzmann 采样
descriptors = list(self.descriptor_scores.keys())
scores = np.array([self.descriptor_scores[d] for d in descriptors])
temperature = max(1.0, 10.0 * (1 - self.iteration / self.max_iterations))
probs = np.exp(-scores / temperature)
probs /= probs.sum()
return np.random.choice(descriptors, p=probs)
为什么用指数而不是线性归一化? 因为光学设计的性能差异是指数级的(RMS 误差从 0.1 到 0.01 是质的飞跃),线性归一化会淹没这种差异。
3. 每个峰独立进化(CMA-ES 并行)
# 在 optimize_descriptor() 中
for cluster in clusters:
cluster_pop = [population[i] for i in cluster]
# 每个簇用独立的 CMA-ES
cma_es = CMA_ES(
mean=np.mean(cluster_pop, axis=0),
sigma=np.std(cluster_pop, axis=0).mean()
)
for _ in range(self.within_budget // len(clusters)):
offspring = cma_es.ask()
fitness = [objective_fn(x, descriptor) for x in offspring]
cma_es.tell(offspring, fitness)
new_pop.extend(cma_es.result.xbest)
关键细节:每个簇的 sigma 用簇内标准差而不是全局标准差,避免跨峰污染。
实验
环境选择
Double-Gauss 镜头:6 个透镜元件,24 维设计空间
为什么不用更复杂的系统(如 10 片镜头)测试? 因为实验目的是验证算法能否找到多样解,而不是极致性能。6 片镜头已经有 $3^6 \times 10^6 \approx 7 \times 10^8$ 种组合,足够验证分区策略。
为什么选 Double-Gauss? 因为它是相机镜头的经典设计,有 70 年的工程积累,我们知道”好的解应该长什么样”(对称结构、中间用高折射率玻璃等),便于验证算法是否找到了物理合理的解。
与 Baseline 对比
| 算法 | 找到的解数量 | 唯一描述符 | 最佳性能 | 时间 |
|---|---|---|---|---|
| 随机搜索 | 1000 | 850 | 0.245 | 1h |
| CMA-ES | 50 | 12 | 0.089 | 1h |
| LDG-EA | 14500 | 636 | 0.095 | 1h |

关键发现:
- LDG-EA 找到的解是 CMA-ES 的 290 倍
- 最佳性能略逊于 CMA-ES(0.095 vs 0.089),但差距仅 6.7%
- 随机搜索虽然找到 850 个描述符,但每个描述符只有 1-2 个解(缺乏深度)
这个结果说明什么? LDG-EA 不是为了找”最优解”,而是为了找”足够多的好解”。类比:CMA-ES 是米其林三星餐厅(一道菜做到极致),LDG-EA 是自助餐厅(100 道菜供你选择)。
消融实验
| 配置 | 解数量 | 唯一描述符 | 计算时间 |
|---|---|---|---|
| 完整 LDG-EA | 14500 | 636 | 1h |
| 无概率模型(随机采样) | 8200 | 580 | 1h |
| 无 HillValley(单峰 CMA-ES) | 636 | 636 | 1h |
| 无梯度精修 | 14100 | 636 | 0.8h |
结论:
- 概率模型贡献 77% 的效率提升(14500 vs 8200),因为避免了重复探索低质量描述符
- HillValley 是多样性的关键(无它则每个描述符只有 1 个解)
- 梯度精修可选(性能提升 < 3%,但耗时增加 25%)
反直觉的发现:去掉 HillValley 后,虽然仍找到 636 个描述符,但总解数降到 636(每个描述符 1 解)。这说明描述符间的多样性容易获得,描述符内的多样性才是难点。
调试指南
常见问题
1. 找到的描述符太少(< 100)
症状:运行 1 小时后 len(descriptor_scores) < 100
可能原因:
- 概率模型温度太低,过早收敛到少数描述符
- 探索阶段迭代不足(前 50 次迭代应该全随机)
诊断代码:
# 在主循环中添加
if iteration == 100:
print(f"前 100 次迭代探索的描述符: {len(descriptor_scores)}")
if len(descriptor_scores) < 80:
print("警告:探索不足,建议增加 temperature 或延长探索阶段")
解决方案:
# 增加探索阶段
if len(self.descriptor_scores) < 200: # 原来是 50
return self._random_descriptor()
# 或提高温度
temperature = max(1.0, 20.0 * (1 - iter/max_iter)) # 原来是 10.0
2. 在某个描述符内找不到多个峰
症状:len(clusters) == 1(HillValley 只检测到一个簇)
可能原因:
- Hill-Valley 测试太严格(10 个采样点不够)
- 初始种群多样性不足(都聚在一个小区域)
诊断代码:
# 在 cluster_solutions() 中添加
print(f"种群分布范围: {np.std(population, axis=0).mean():.4f}")
if np.std(population, axis=0).mean() < 0.1:
print("警告:种群多样性不足")
解决方案:
# 方案 1:放宽山谷判断(允许小波动)
threshold = 1.05
if f_mid > threshold * max(f1, f2):
return False
# 方案 2:增加初始扰动
x0 = np.random.randn(dim) * 3 # 原来是 1
3. 优化后违反约束
症状:AssertionError: 厚度违反约束!
根本原因:L-BFGS-B 的边界约束只保证盒约束(每个变量独立的上下界),无法处理耦合约束(如”总厚度 < 50mm”)。
三种解决方案对比:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
硬边界 (bounds) |
简单 | 只能处理盒约束 | 变量独立 |
| 惩罚函数 | 灵活 | 可能陷入不可行域 | 软约束 |
| 投影 | 保证可行 | 可能破坏优化方向 | 硬约束 |
推荐组合使用:
# 优化时用边界 + 惩罚
result = minimize(
lambda x: objective_fn(x) + penalty(x), # 惩罚函数
x0,
bounds=[(0.5, 10)] * n_thickness # 盒约束
)
# 返回前投影到可行域
x_feasible = project_to_feasible(result.x)
如何判断算法在”学习”
健康的学习曲线应该满足:
- 描述符增长曲线(应该是对数增长)
# 每 100 次迭代检查 expected = 50 * np.log(1 + iteration / 50) # 理论曲线 actual = len(descriptor_scores) assert actual > 0.8 * expected, "探索速度过慢" - 最佳性能单调下降(允许偶尔反弹)
best_history = [min(s.objective_value for s in all_solutions[:i+1]) for i in range(len(all_solutions))] # 检查最近 100 次是否有改善 recent_improvement = best_history[-100] - best_history[-1] if recent_improvement < 0.001: print("警告:性能停滞,可能陷入局部最优") - 平均性能收敛(最近 100 个解的平均质量)
recent_avg = np.mean([s.objective_value for s in all_solutions[-100:]]) global_avg = np.mean([s.objective_value for s in all_solutions]) assert recent_avg < global_avg, "种群退化!"
警告信号:
- ❌ 描述符数量 10 次迭代无增长 → 概率模型过度利用
- ❌ 最佳性能 20 次迭代无改善 → 陷入局部最优
- ❌ 近期平均性能 > 全局平均 → 种群退化(可能是 HillValley 失效)
超参数调优
推荐调优顺序(从影响最大到最小):
| 参数 | 推荐范围 | 调优方法 | 预期效果 |
|---|---|---|---|
within_descriptor_budget |
50-200 | 网格搜索 | 每增加 50 → 解数量 +30% |
temperature 初值 |
5.0-20.0 | 试错 | 过高 → 浪费计算,过低 → 过早收敛 |
population_size |
30-100 | 50 为基准 | 影响峰检测准确率 |
n_samples (HillValley) |
5-20 | 10 足够 | 影响山谷检测准确率 |
descriptor_budget |
500-2000 | 根据时间预算 | 线性影响覆盖率 |
快速调优流程:
- 固定
within_descriptor_budget=50,跑 100 次迭代 - 检查
平均每个描述符的解数量 = 总解数 / 描述符数- 如果 < 5 → 增大到 100
- 如果 > 30 → 减小到 30
- 调整
temperature,确保前 50% 迭代探索 > 70% 的最终描述符
什么时候用 / 不用?
适用场景
| 场景 | 为什么适合 | 示例 |
|---|---|---|
| 多模态优化 | 需要多样化的解 | 透镜设计、材料配方 |
| 离散 + 连续混合 | 描述符天然编码离散选择 | 机器人动作规划 |
| 强约束问题 | 描述符可以编码物理规则 | 化工流程优化 |
| 工程决策支持 | 需要权衡多种方案 | 建筑结构设计 |
不适用场景
| 场景 | 为什么不适合 | 替代方案 |
|---|---|---|
| 单目标优化 | 只要最优解,多样性无意义 | CMA-ES |
| 纯连续问题 | 描述符设计困难 | CMA-ES, L-BFGS |
| 弱约束问题 | 描述符无法提供结构化先验 | MAP-Elites |
| 实时优化 | 计算预算 < 1000 次评估 | 梯度下降 |
| 黑盒优化 | 无领域知识设计描述符 | 贝叶斯优化 |
判断清单:
- ✅ 我能定义有意义的行为描述符吗?(如果不能,别用)
- ✅ 我需要多个解吗?(如果只要一个最优解,用 CMA-ES)
- ✅ 问题有离散变量吗?(纯连续用 CMA-ES 更快)
- ✅ 我有 > 1000 次函数评估的预算吗?(太少不够探索)
我的观点
这个算法真的比 CMA-ES 好吗?
诚实回答:不一定,取决于你要什么。
如果你的目标是”找到最优解”:
- CMA-ES 碾压(0.089 vs 0.095)
- 调优过的梯度下降可能更好
如果你的目标是”找到 100 个备选方案”:
- LDG-EA 碾压(14500 vs 50)
- 而且这 100 个方案的性能分布、成本分布、制造难度都不同
类比:这不是”哪个算法更好”的问题,而是”去米其林三星还是去自助餐”的问题。如果你是美食评论家(只要最优解),去米其林;如果你是公司聚餐(要照顾所有人口味),去自助餐。
最大的局限:描述符设计门槛
论文没说的真相:作者是光学专家,知道”曲率符号 + 玻璃索引”是关键设计变量。如果你是外行,可能设计出无意义的描述符(如”第 1 和第 3 个变量的和”),导致算法完全失效。
未来方向:能否用无监督学习自动发现描述符?
- 尝试 1:用 VAE 学习低维潜在空间,用聚类中心作为描述符
- 尝试 2:用决策树拟合”性能好的解的共性”,用树节点作为描述符
- 尝试 3:用强化学习动态调整描述符粒度(粗 → 细)
我的猜测:自动描述符学习会在 2-3 年内成为研究热点,因为这是 LDG-EA 唯一的护城河。
与 AI 生成式设计的关系
LDG-EA 可以看作”进化算法版的 Stable Diffusion”:
- Stable Diffusion:在潜在空间采样 → 解码成图像
- LDG-EA:在描述符空间采样 → 优化成设计方案
差异:Stable Diffusion 需要百万级训练数据,LDG-EA 只需要领域知识(描述符定义)。
融合可能:能否用生成模型学习”好的设计长什么样”,然后用 LDG-EA 在生成模型的潜在空间优化?这样既有数据驱动的泛化能力,又有进化算法的精确优化。
总结:LDG-EA 不是”更好的优化算法”,而是”优化问题的重新定义”——从”找最优解”变成”找多样解集”。它适合工程决策场景,不适合纯数学优化。
什么时候应该用它?当你的老板说”给我 5 个备选方案,成本、性能各不相同”的时候。
什么时候不该用它?当论文审稿人说”为什么不和最新的 Transformer-based optimizer 比”的时候(因为目标根本不同)。
Comments