第17章:为什么可微光学重要?
上一章我们了解了经典的光学设计循环。
我们有一个处方。我们追迹光线。我们计算了点列图、波前图、PSF、MTF曲线,以及一个评价函数。然后优化器改变变量,试图降低那个评价函数。
这个循环已经很强大了:
镜头处方
→ 分析
→ 评价函数
→ 优化器
→ 更新处方
但“优化器”这个词背后隐藏着一个问题。
优化器是如何知道该往哪个方向移动的?
在一个简单的单变量聚焦问题中,它可以尝试附近的焦点位置,看看评价是否变好。而在一个有着许多半径、厚度、非球面系数、材料和像质指标的真实镜头中,盲目的试错就变得昂贵了。
这就是梯度变得重要的地方。
梯度告诉我们,当一个参数变化时,结果会怎样变化。
例如:
如果我稍微增加这个面的曲率,
RMS光斑是变好还是变差?
如果我将这个元件移动0.01毫米,
40线对/毫米处的MTF是上升还是下降?
如果我改变这个相位掩模参数,
最终重建图像的损失是减小还是增大?
可微光学就是要让这些问题变得可计算。
它不会取代光学。它不会使光线追迹变得多余。它也不会消除对物理判断的需求。
它增加了一项强大的能力:
计算机不仅能计算光学结果,
还能计算这个结果如何随设计参数变化。
这就是自动微分、可微光线追迹,以及现代端到端计算成像的入口。
1. 这里“可微”的含义
如果一个函数输入的微小变化能在输出中产生可由导数描述的变化,那么这个函数就是可微的。
在光学设计中,我们通常有如下的链条:
设计参数
→ 光线追迹
→ 波前
→ PSF
→ 像质指标
→ 损失
这个链条的可微版本允许我们计算:
当每个设计参数变化时,损失如何变化
在数学上,如果我们将设计参数收集到一个向量中:
并且评价函数或损失为:
那么梯度就是:
每个分量告诉我们损失对一个变量的局部敏感度。
如果:
那么增加 倾向于会使损失局部增大。
如果:
那么增加 倾向于会使损失局部减小。
一个基于梯度的优化器可以利用这些信息,比盲目搜索更聪明地进行移动。
这是第一个关键思想:
可微光学为优化提供了方向信息。
优化器不再只需要评价函数的值了。它还能知道哪些变量在使评价函数变大,哪些在使其变小。
2. 自动微分并非数值猜测
估计导数有几种方法。
一种简单的方法是有限差分。
假设我们有一个函数:
我们可以通过两次计算函数值来估计导数:
这很容易理解,但它存在问题:
- 对很多变量来说,速度可能很慢;
- 依赖于选择一个好的 ;
- 可能是带噪声的;
- 需要很多次重复计算;
- 在大型计算图中变得笨拙。
自动微分(automatic differentiation,常简称为 autodiff)的工作方式则不同。
它记录用于计算输出的操作,然后通过这些操作运用链式法则。
如果计算是:
那么 autodiff 会通过链式法则计算导数:
这在传统意义上并不是符号代数。它也不是一个粗糙的有限差分估计。它是通过实际执行的操作来对导数进行计算。
现代机器学习库,例如 PyTorch 和 JAX,就是围绕这个思想建立的。
对于光学计算来说,这之所以重要,是因为我们的计算是成链状的:
半径
→ 面交点
→ 折射
→ 光线位置
→ 光斑尺寸
→ 损失
如果每一步都是用可微操作实现的,那么 autodiff 就能计算损失如何随着半径变化。
这就是它的基本承诺。
3. 一个小巧的 PyTorch 示例
在接触光学量之前,我们先看一个标量示例。
我们希望 变成 3,因此我们定义:
其导数为:
在 处,导数为:
那意味着增大 将减小损失。
在 PyTorch 中:
import torch
x = torch.tensor([0.0], requires_grad=True)
loss = (x - 3.0) ** 2
loss.backward()
print("x:", x.item())
print("loss:", loss.item())
print("d loss / d x:", x.grad.item())
输出的梯度应该接近于:
-6
现在我们可以手动进行梯度下降:
import torch
x = torch.tensor([0.0], requires_grad=True)
learning_rate = 0.1
for step in range(30):
loss = (x - 3.0) ** 2
loss.backward()
with torch.no_grad():
x -= learning_rate * x.grad
x.grad.zero_()
print("optimized x:", x.item())
变量应该会朝着 3 移动。
这个例子虽然小,但它包含了整个模式:
定义变量
→ 计算损失
→ 计算梯度
→ 更新变量
→ 重复
可微光学使用相同的模式,但损失来自于光学计算。
4. 一个可微的薄透镜示例
现在,让我们把这个例子变成光学的。
我们将使用空气中的薄透镜模型:
其中:
- 是光焦度;
- 是折射率;
- 和 是表面曲率。
焦距为:
假设我们想要一个 50 毫米的焦距。
我们可以让 和 成为可训练的变量。
import torch
# 可训练的曲率,单位 1/mm
c1 = torch.tensor([0.02], requires_grad=True)
c2 = torch.tensor([-0.02], requires_grad=True)
n = 1.5
target_focal_length = 50.0
optimizer = torch.optim.SGD([c1, c2], lr=0.001)
for step in range(200):
optimizer.zero_grad()
power = (n - 1.0) * (c1 - c2)
focal_length = 1.0 / power
relative_error = (focal_length - target_focal_length) / target_focal_length
curvature_penalty = 0.01 * (c1**2 + c2**2)
loss = relative_error**2 + curvature_penalty
loss.backward()
optimizer.step()
print("c1:", c1.item())
print("c2:", c2.item())
print("focal length:", focal_length.item())
print("loss:", loss.item())
这是一个玩具模型,但它展示了机制。
PyTorch 计算了损失如何依赖于 和 。然后优化器更新了那些曲率。
没有手动编写有限差分循环。
计算图是:
c1, c2
→ 光焦度
→ 焦距
→ 焦距误差
→ 损失
Autodiff 在这个图中计算梯度。
这就是最简版本的可微光学设计。
5. 一个可微的傍轴光线束
前面的例子只优化了焦距。让我们朝成像再靠近一步。
在傍轴光学中,薄透镜会大致按如下方式改变光线斜率:
其中:
- 是透镜处的光线高度;
- 是光线角度或斜率;
- 是焦距。
传播距离 后,光线高度变为:
我们可以用 PyTorch 构建一个可微的光线束。
import torch
dtype = torch.float64
# 透镜处的光线高度,单位 mm
y = torch.linspace(-5.0, 5.0, 41, dtype=dtype)
# 入射的准直光线
u_in = torch.zeros_like(y)
# 可训练的透镜曲率和像面位置
c1 = torch.tensor([0.02], dtype=dtype, requires_grad=True)
c2 = torch.tensor([-0.02], dtype=dtype, requires_grad=True)
z_image = torch.tensor([50.0], dtype=dtype, requires_grad=True)
n = 1.5
optimizer = torch.optim.Adam([c1, c2, z_image], lr=0.01)
for step in range(1000):
optimizer.zero_grad()
power = (n - 1.0) * (c1 - c2)
focal_length = 1.0 / power
u_out = u_in - y / focal_length
y_image = y + z_image * u_out
rms_spot = torch.sqrt(torch.mean(y_image**2))
target_focal_length = 50.0
focal_error = (focal_length - target_focal_length) / target_focal_length
curvature_penalty = 0.001 * (c1**2 + c2**2)
loss = rms_spot**2 + 10.0 * focal_error**2 + curvature_penalty
loss.backward()
optimizer.step()
print("c1:", c1.item())
print("c2:", c2.item())
print("z_image:", z_image.item())
print("focal length:", focal_length.item())
print("rms spot:", rms_spot.item())
print("loss:", loss.item())
这仍然是傍轴且简化的。但它具有可微光线优化的结构:
可训练的光学参数
→ 光线计算
→ 像面截距
→ RMS光斑损失
→ 梯度
→ 参数更新
梯度告诉优化器如何改变曲率和像面位置以减少损失。
这就是核心思想。
6. 这与第15章有什么不同
在第15章中,我们对评价函数使用了 scipy.optimize.minimize。那已经是优化了。
那么这里有什么新东西呢?
区别不在于评价函数的存在。两种方法都有评价函数。
区别在于导数的信息是如何获得和使用的。
一个无导数的优化器可以通过多次计算评价函数的值来推断该在哪里搜索。
而一个基于梯度的可微工作流是通过光学计算来计算梯度的。
这意味着我们可以直接询问:
对于大型系统,这可能非常有价值。
区别是:
传统评价函数优化:
评估评价函数,搜索更低的值。
可微优化:
评估评价函数,并通过计算过程计算梯度。
两者都可以有用。
可微光学并没有让传统优化过时。它给了我们另一套工具,尤其是在光学系统是更大可微流水线的一部分的时候。
7. 可微光线追迹
一个可微光线追迹器就是一个以某种方式编写的、能让导数流过其操作的光线追迹器。
一个普通的光线追迹器会计算:
表面参数 + 入射光线
→ 交点
→ 表面法线
→ 折射方向
→ 出射光线
一个可微的光线追迹器还允许计算诸如以下的导数:
如果表面半径改变,出射光线会怎样变化
如果厚度改变,像面截距会怎样变化
如果某个非球面系数改变,损失会怎样变化
光线追迹的操作必须是可微的,或者需要小心处理:
- 光线与面的交点;
- 表面法线的计算;
- 折射;
- 传播;
- 光程;
- 光线截距误差;
- 波前误差。
对于球面,交点涉及求解一个二次方程。只要我们避开诸如未命中交点或分支改变之类的不连续性,这个解就可以是可微的。
对于折射光线,矢量斯涅尔公式只要避免发生全反射或小心处理,就可以是可微的。
因此,可微光线追迹链可能看起来像:
表面半径
→ 交点距离
→ 交点
→ 法向量
→ 折射方向
→ 像面截距
→ 损失
→ 相对于半径的梯度
这在概念上和我们之前构建的光线追迹没有不同。
它只是让同样的物理规律变得与梯度计算兼容了。
8. 一个最小化的可微折射示意
我们来勾勒一下矢量归一化和折射的可微版本。
这不是一个完整、稳健的生产实现。这只是一个教学片段。
import torch
def normalize(v, eps=1e-12):
return v / torch.sqrt(torch.sum(v * v, dim=-1, keepdim=True) + eps)
def refract_torch(direction, normal, n1, n2):
"""
可微的矢量折射示意。
direction 和 normal 是形状为 (..., 3) 的张量。
两者都应已归一化。
此函数假定没有发生全反射。
"""
direction = normalize(direction)
normal = normalize(normal)
eta = n1 / n2
cos_i = -torch.sum(normal * direction, dim=-1, keepdim=True)
sin_t2 = eta**2 * (1.0 - cos_i**2)
cos_t = torch.sqrt(1.0 - sin_t2)
refracted = eta * direction + (eta * cos_i - cos_t) * normal
return normalize(refracted)
这里的操作是 PyTorch 的张量操作。如果 direction、normal,或者用于计算它们的表面参数需要梯度,PyTorch 就能让导数流经这个函数。
但请注意这个警告:
此函数假定没有发生全反射。
如果:
那么平方根对于折射就变成无效的了。
一个生产级的可微光线追迹器必须处理那种情形。它可以使用掩膜、惩罚项、替代分支,或者小心限定域。
这是一个反复出现的主题:
让光学可微并不会消除光学的边界情况。
它会让那些边界情况变得更加重要。
9. 可微性变得困难的地方
上面这些清晰的例子是平滑的。
从优化器的视角看,真实的光学系统并不总是平滑的。
有几件事会造成困难。
光线未命中与孔径裁切
如果一条光线在某个参数值下能通过孔径,而在另一个值下被阻挡,那么损失就可能发生不连续的改变。
这条光线要么存在,要么不存在。
那不是平滑的改变。
全反射
当入射角穿过临界角时,折射可能突然变得不可能。
这在计算中形成了一个分支。
表面交点分支改变
一个光线-球面的交点可能有两个数学根。在普通光线追迹中选择物理上正确的那个通常是直截了当的,但分支的选择可能会带来导数方面的复杂问题。
渐晕
与视场相关的光线裁切可能引入硬边界。
微小的参数改变可能导致光线的出现或消失。
玻璃选择
从玻璃目录中选择一种玻璃是离散的。除非引入一个连续的代理模型,否则你无法平滑地从一种玻璃类型微分到另一种。
带有硬阈值的评价函数
像这样的一个损失并非处处平滑:
penalty = max(0.0, value - limit)
它虽然可以使用,但导数在阈值处会有一个转折点。
FFT 幅度和相位问题
PSF 和 MTF 的计算可以是可微的,但幅度、归一化、采样和相位行为仍然需要注意。
结论并不是“可微光学太脆弱了”。
结论是:
当计算路径是本着梯度思想来设计时,可微光学才能发挥最佳作用。
那意味着平滑的损失函数、谨慎的掩膜、稳定的参数化,以及良好的物理约束。
10. 平滑的损失函数通常优于硬性规则
在第15章中,我们讨论了硬约束和软惩罚。
可微优化通常更偏好平滑的惩罚项。
例如,假设一个厚度应该保持在 1 毫米以上。
一个硬性失败判定可以是:
if thickness < 1.0:
loss = huge_number
这对于剔除不可能的设计可能有用,但它给出的梯度信息很差。
一个更平滑的惩罚项是:
penalty = torch.relu(1.0 - thickness) ** 2
这会惩罚低于限值的厚度,同时在违规发生时给出梯度。
对于一个更平滑的版本,可以使用类似softplus的函数:
def soft_lower_bound_penalty(value, lower, sharpness=20.0):
violation = lower - value
return torch.nn.functional.softplus(sharpness * violation) / sharpness
这并不能代替硬性的工程限制。但它在搜索时给了优化器一个更平滑的路径。
一个良好的实践模式是:
用平滑的惩罚项来引导优化。
用硬性检查来剔除最终无效的设计。
这在优化具有许多变量的光学系统时尤其有帮助。
11. 可微的 PSF 和 MTF
前面几章的 PSF 和 MTF 计算也可以成为可微流水线的一部分。
一个简化的 PyTorch PSF 计算如下所示:
import torch
def pupil_function_torch(opd, mask, wavelength):
phase = 2.0 * torch.pi * opd / wavelength
pupil = mask * torch.exp(1j * phase)
return pupil
def psf_from_pupil_torch(pupil):
field = torch.fft.fftshift(
torch.fft.fft2(
torch.fft.ifftshift(pupil)
)
)
psf = torch.abs(field) ** 2
psf = psf / torch.sum(psf)
return psf
def mtf_from_psf_torch(psf):
otf = torch.fft.fftshift(
torch.fft.fft2(
torch.fft.ifftshift(psf)
)
)
center = (otf.shape[0] // 2, otf.shape[1] // 2)
otf = otf / otf[center]
mtf = torch.abs(otf)
return mtf
如果 opd 依赖于可训练的参数,那么由 mtf 计算出的损失就能将梯度反向传播经过:
MTF
→ OTF
→ PSF
→ 光瞳函数
→ OPD
→ 光学参数
这非常强大。
例如,一个损失可以将目标设定为在一个选定的频率上获取高 MTF:
def mtf_loss_at_sample(mtf, fy, fx, target=0.5):
value = mtf[fy, fx]
error = torch.relu(target - value)
return error**2
这只是一个采样的教学示例。一个真实的实现需要正确的频率坐标、视场处理、波长处理、采样和归一化。
但思路是明确的:
如果 PSF 和 MTF 是用可微的张量运算计算出来的,
它们就可以在基于梯度的优化中使用。
这直接把第13章和第14章的内容与现代优化联系了起来。
12. 一个微小的可微相位掩模示例
我们再构建一个更接近计算成像而非经典透镜弯曲的示例。
假设我们有一个光瞳,带有一个可训练的、类似离焦的相位系数。我们希望 PSF 的能量集中在中心。
这不是一个完整的透镜设计。这是一个小型的可微傅里叶光学示例。
import torch
import matplotlib.pyplot as plt
dtype = torch.float64
device = "cpu"
n = 128
coord = torch.linspace(-1.0, 1.0, n, dtype=dtype, device=device)
yy, xx = torch.meshgrid(coord, coord, indexing="ij")
rho = torch.sqrt(xx**2 + yy**2)
mask_real = (rho <= 1.0).to(dtype)
mask = mask_real.to(torch.complex128)
# 可训练的离焦系数,以波长为单位
defocus_waves = torch.tensor([0.5], dtype=dtype, requires_grad=True)
optimizer = torch.optim.Adam([defocus_waves], lr=0.05)
for step in range(200):
optimizer.zero_grad()
opd_waves = defocus_waves * (2.0 * rho**2 - 1.0)
phase = 2.0 * torch.pi * opd_waves
pupil = mask * torch.exp(1j * phase.to(torch.complex128))
field = torch.fft.fftshift(
torch.fft.fft2(
torch.fft.ifftshift(pupil)
)
)
psf = torch.abs(field) ** 2
psf = psf / torch.sum(psf)
center = (n // 2, n // 2)
# 我们想最大化中心强度,所以最小化负的峰值。
loss = -psf[center]
loss.backward()
optimizer.step()
print("optimized defocus coefficient [waves]:", defocus_waves.item())
print("center intensity:", psf[center].item())
优化器应该将离焦系数推向一个能增加中心 PSF 能量的值,通常是朝着减少离焦的方向。
这是一个微小的例子,但它很重要,因为整个路径都是可微的:
相位系数
→ 光瞳相位
→ FFT 场
→ PSF
→ 中心强度损失
→ 梯度
→ 相位系数更新
这种计算风格正是让现代可微光学具有吸引力的原因。
我们不再局限于只优化光线截距或简单的标量操作数。我们可以通过像差计算过程来优化。
13. 端到端计算成像
经典的透镜设计常常将镜头视为主要对象。
现代计算成像常常将整个成像系统视为对象:
场景
→ 光学
→ 传感器
→ 噪声
→ 图像处理
→ 最终图像或任务损失
在一个端到端的可微流水线中,这些阶段中的一部分或全部可能是可微的。
这允许了一种非常不同的优化方式。
我们不再只问:
这个镜头有高的 MTF 吗?
而是可以问:
整个成像系统,在考虑了光学、传感器采样、噪声和重建之后,
是否产生了最佳的最终结果?
例如,一个计算相机可能故意使用一个按经典标准看起来很奇怪的 PSF 的光学元件,因为一个重建算法能很好地解码它。
一个波前编码系统可能刻意让 PSF 对离焦不那么敏感。
一个超表面或衍射光学元件可能和一个神经网络重建网络一起被设计。
一个镜头和图像处理模型可能一起为一个特定任务进行训练。
可微光学在这里变得尤为重要。
如果整个流水线是可微的,我们就可以计算从最终损失一路返回到光学参数的梯度。
链条就变成了:
最终图像损失
→ 重建算法
→ 传感器模型
→ PSF 或波动光学模型
→ 光学参数
这就是端到端的优化。
但这种力量带有一个警告。
如果模拟是不切实际的,那么优化出的系统可能在模拟中表现良好,而在现实中表现很差。
这有时被称为模拟到现实的差距。
对于光学来说,这种差距可能来自:
- 不精确的材料模型;
- 制造误差;
- 对准公差;
- 温度效应;
- 传感器噪声假设;
- 衍射模型的简化;
- 偏振效应;
- 杂散光;
- 镀膜行为;
- 不完美的标定;
- 重建模型的假设。
可微性并不能消除这些问题。它甚至可能把它们隐藏在漂亮的训练曲线后面。
一个谨慎的说法是:
端到端的可微优化只和它所优化的物理模型及损失函数一样可信。
每当这种方法显得神奇时,这句话都应该被放在近旁。
14. 经典指标仍然重要
可微光学可以直接优化图像损失、任务损失或是学习到的感知指标。
但这并没有让经典光学指标过时。
点列图、波前图、PSF、MTF、畸变、场曲和公差分析依然重要,因为它们揭示了物理行为。
一个神经重建指标可能会说最终图像看起来不错,而光学系统却存在:
- 严重的视场相关模糊;
- 对离焦高度敏感;
- 易碎的对准;
- 糟糕的颜色表现;
- 不可接受的畸变;
- 低光效率;
- 在训练分布之外的强烈伪影。
经典指标不是该被扔掉的旧家具。
它们是诊断仪器。
一个好的现代工作流可能同时包含两者:
经典光学指标
+
可微的端到端损失
经典指标告诉我们光学在做什么。
端到端的损失告诉我们,整个成像系统在某个特定任务上的表现如何。
两者相关但并不等同。
这是一种思考可微光学的成熟方式:
它扩展了设计空间。
它并未取消理解光学的必要性。
15. 可微并不意味着物理上无约束
基于梯度的优化能够发现出人意料的设计。
这可能是好事。
它也可能创造出难以制造、难以对准或难以解释的设计。
例如,如果一个相位掩模是用自由像素来表示的,优化器可能会创造出一个高度振荡的相位图案。它在模拟中可能表现良好,但却无法制造,或对波长过于敏感。
如果一个非球面有太多自由系数,优化器可能会创造出一个斜率变化很大或具有难测要求的面形。
如果一个材料参数被允许连续变化,优化器可能会选择一个不对应于任何真实玻璃的折射率。
因此,参数化很重要。
参数化就是我们描述设计变量的方式。
例如:
表面半径
圆锥常数
非球面系数
泽尼克相位系数
自由曲面系数
衍射区高度
像素化的相位掩模值
玻璃插值变量
机械间隔
一个好的参数化在自由有用时给优化器自由,在必要时给优化器限制。
这是设计判断的另一种形式。
优化器只跟随梯度。除非可制造性出现在模型或约束中,否则它并不知道哪些形状是可制造的。
16. 稳定的参数化:一个例子
假设我们想优化一个相位掩模。
一种诱人的参数化是:
每个像素一个自由的相位值
这给了极大的自由度。
它也允许产生带噪声、快速变化的图案。
一个更平滑的替代方法是将相位表示为低阶多项式或类似泽尼克项的和:
现在,可训练的变量就是系数:
这减少了自由度,但提高了稳定性和可解释性。
在代码中,一个简单的离焦加像散相位模型可能是:
def phase_model(rho, theta, coeffs):
"""
简单的可微相位模型。
coeffs[0]:类似离焦的项
coeffs[1]:类似像散的项
coeffs[2]:用于教学的类似彗差的项
"""
defocus = coeffs[0] * (2.0 * rho**2 - 1.0)
astig = coeffs[1] * rho**2 * torch.cos(2.0 * theta)
coma = coeffs[2] * (3.0 * rho**3 - 2.0 * rho) * torch.cos(theta)
return defocus + astig + coma
这不是一个完整的泽尼克实现,但它展示了这个思路。
优化器改变的是少数有意义的系数,而不是成千上万个互不相关的像素。
这常常让问题变得更容易,也使结果更容易理解。
一条好的法则是:
给优化器提供那些能描述合理光学变化的变量。
而不仅仅是那些容易被放进张量里的变量。
17. 可微损失是一种设计表述
在第15章中,我们说过评价函数就是一种设计论证。
这里也是如此。
一个可微的损失函数会告诉优化器什么才是重要的。
如果损失函数最大化 PSF 中心强度,优化器就会试着将能量集中到中心。
如果损失函数以选定的频率上的 MTF 为目标,它就会试着改善那些频率。
如果损失函数将重建图像与真值图像进行比较,它就会试着改善那个重建。
如果损失函数是一个神经网络任务损失,它就可能为分类或检测性能而非人眼视觉质量进行优化。
这些中没有哪个是普遍正确的。
损失必须与成像任务相匹配。
例如:
一个条形码扫描仪,与一个肖像镜头,对光学的优先级需求就不一样。
一个显微镜,与一个手机摄像头,对光学的优先级需求就不一样。
一个星敏感器,与一个内窥镜,对光学的优先级需求也不一样。
可微光学并没有消除定义任务的必要性。
它只是让任务的定义更直接,也更具影响力。
18. 梯度下降也可能被愚弄
梯度告诉我们一个局部的方向。
它并不能保证一个全局最佳的设计。
梯度下降仍然可能受以下问题之苦:
- 局部极小值;
- 平坦的区域;
- 糟糕的缩放;
- 不稳定的学习率;
- 不佳的参数化;
- 不切实际的损失函数;
- 未在模型中表示的约束;
- 对初始化的敏感性。
例如,如果学习率太高,优化可能会震荡或发散。
如果学习率太低,它可能几乎不移动。
如果初始设计物理上很差,光线可能错过表面或击中无效区域。
如果损失函数的地形有很多谷地,优化器可能在一个局部折中方案中停驻。
因此,老的经验依然成立:
优化只是工具,不是保证。
可微性给出了更好的信息。它并没有给出智慧。
19. 缩放仍然重要
梯度依赖于尺度。
如果一个参数是用毫米度量的,而另一个是极小的、以每毫米为倒数的曲率,它们的梯度大小可能相差极大。
如果一个损失项的数值在 1000 左右,而另一个大约为 0.001,那么大的那一项就可能占据主导。
因此,可微光学仍然需要:
- 归一化的变量;
- 归一化的损失;
- 合理的权重;
- 稳定的单位;
- 有界的参数;
- 良好的初始化。
这直接与第14章联系起来。
采样和归一化并没有因为我们迁移到了 PyTorch 中就不再重要。
它们变得更重要了,因为梯度依赖于它们。
一个 MTF 损失中错误的频率轴意味着优化器会改善错误的频率。
一个错误的 PSF 归一化意味着梯度可能奖励的是能量缩放,而不是图像集中度。
一个错误的光瞳掩膜意味着优化器为一个错误的物理系统做设计。
安全的链条是:
正确的计算
→ 正确的损失
→ 有意义的梯度
→ 有用的优化
不要跳过前两步。
20. 可微光学如何与 Optiland 连接
一个现代的开放光学设计框架可以以两种方式提供价值。
首先,它可以提供经典的分析:
光线追迹
点列图
波前
PSF
MTF
优化
其次,如果它支持可微的或面向机器学习的工作流,它就能帮助将光学模型与张量库以及基于梯度的优化连接起来。
一个可微的工作流轮廓大致如下:
# 版本校验的工作流轮廓。
# 请检查已安装的 Optiland 版本和配套代码仓库,
# 以获取准确的 API 名称。
# 1. 构建或加载一个光学系统。
# optic = make_system(...)
# 2. 如果支持,选择可微后端或张量模式。
# optic.enable_differentiable_backend("torch")
# 3. 将参数标记为可训练。
# optic.surface(1).radius.requires_grad = True
# optic.surface(2).radius.requires_grad = True
# optic.image_plane.position.requires_grad = True
# 4. 追迹光线或计算波动光学结果。
# result = optic.trace(...)
# 5. 计算损失。
# loss = result.rms_spot() + mtf_penalty(...)
# 6. 反向传播。
# loss.backward()
# 7. 优化器更新参数。
# optimizer.step()
具体的 API 可能不同。结构才是重要的部分。
重要的是,光学参数要参与到一个可微的计算图中。
本书前面提到的链条依然适用:
表面
→ 光线
→ 波前
→ PSF
→ MTF
可微的附加部分是:
损失
→ 沿着链条返回梯度
→ 参数更新
因此,可微光学并不是一个分离的宇宙。它就是附着上梯度的同一个计算链条。
21. 为什么开放工具在这里很重要
开放工具的教育价值在可微光学中变得尤为清晰。
如果一个工具只给出最终结果,你可以使用这个结果,但无法轻易检查计算图。
如果一个工具将系统以代码的形式暴露出来,你就可以问:
- 哪些参数需要梯度?
- 哪些操作是可微的?
- 在哪里使用了掩膜或硬分支?
- 正在优化哪个损失函数?
- 光线是如何被采样的?
- 无效的光线是如何被处理的?
- PSF 是如何被归一化的?
- MTF 的频率轴是如何定义的?
- 哪些参数实际上改变了?
这不是在崇拜开源。
它关乎的是可检查性。
对该主张更尖锐的表述是:
对于学习可微光学来说,可检查性不是奢侈品。
它是理解该方法的一部分。
如果一个人看到的只是一个训练损失曲线,那么一个可微的流水线甚至可以比一个经典流水线更加不透明。
开放的、可检查的代码有助于防止这种情况。
22. 一个小型的梯度检查习惯
在进行可微光学工作时,要检查梯度。
不是每一步都检查,也不是永远都检查,但要足够频繁以便能发现问题。
例如:
loss.backward()
print("c1 grad:", c1.grad)
print("c2 grad:", c2.grad)
print("z_image grad:", z_image.grad)
问以下问题:
梯度是有限的吗?
它们是否极大?
它们是否恰好为零?
当预期它们应改变符号时,它们改变了吗?
有些变量是否没有收到梯度?
如果一个变量没有梯度,那么它可能没有与损失相连。
如果梯度是 nan,可能是一个平方根、除法、无效光线或是归一化出了问题。
如果梯度巨大,缩放可能不佳。
如果梯度极小,问题可能是在平坦区域、饱和了,或是缩放不好。
这就是检查中间光学图的可微版本。
正如我们检查光瞳振幅和 PSF 居中情况一样,我们也要检查梯度的健康状况。
一个实用的调试检查清单:
[ ] 损失是有限的吗?
[ ] 梯度是有限的吗?
[ ] 可训练的变量是否实际影响着损失?
[ ] 单位和缩放合理吗?
[ ] 掩膜是否阻挡了梯度?
[ ] 硬阈值是否制造了死区?
[ ] 一个小的手动参数改变是否按预期影响了损失?
这能够节省时间。
23. 可微光学与 MTF:一个精细的例子
假设我们想改善在某个选定频率上的 MTF。
一个天真的损失函数可能是:
loss = -mtf[fy, fx]
这最大化了一个单一的 MTF 采样点。
作为玩具示例,这可能可行,但在真正的设计中,它太狭窄了。
它可能鼓励系统改善一个点,而忽略附近的频率或其他视场。
一个更好的教学级损失可能会在一个小的频带上求平均值:
def band_mtf_loss(mtf, fy_slice, fx_slice, target=0.4):
band = mtf[fy_slice, fx_slice]
mean_mtf = torch.mean(band)
error = torch.relu(target - mean_mtf)
return error**2
一个更好的设计损失还可能包括:
多个视场
多个波长
多个频带
几何惩罚
畸变惩罚
制造约束
可微方法给了我们梯度。它并不会自动为我们选择一个聪明的损失函数。
设计问题依然是:
对于这个系统来说,哪些像质是重要的?
这个问题不能被委托给 autodiff。
24. 可微渲染与学习重建
在计算成像中,光学系统可能会与重建软件一起被优化。
一个简化的训练循环可能看起来像:
真值图像
→ 光学仿真
→ 传感器仿真
→ 重建模型
→ 重建图像
→ 相对于真值的损失
→ 梯度更新光学和重建参数
在代码结构上:
# 仅为概念结构。
for image in training_images:
optimizer.zero_grad()
psf = optical_model(trainable_optical_parameters)
sensor_image = simulate_sensor(image, psf, noise_model)
reconstructed = reconstruction_model(sensor_image)
loss = image_loss(reconstructed, image)
loss.backward()
optimizer.step()
这比经典的镜头优化要广泛得多。
损失可能不是 RMS 光斑或 MTF。它可能是:
- 均方图像误差;
- 感知损失;
- 分类损失;
- 检测损失;
- 深度估计损失;
- 任务特定的重建损失。
这就是为什么可微光学对于现代成像很重要。
光学设计可以为了图像的最终用途而优化,而不仅仅是为了经典的中间指标。
但同样,这既强大又充满风险。
如果训练图像不具有代表性,光学系统可能会过拟合。
如果传感器模型过于干净,设计可能在真实噪声下失效。
如果制造差异被忽略,设计可能是脆弱的。
如果重建网络太过强大,它可能在模拟中隐藏光学的弱点,而在现实中产生伪影。
优化越趋向端到端,物理上的验证就必须越仔细。
25. 验证并未消失
一个可微的设计仍然需要在训练循环之外进行验证。
对于一个镜头或计算相机来说,验证可能包括:
经典的光学分析
独立的光线追迹
PSF 和 MTF 检查
公差分析
制造约束
传感器采样检查
噪声鲁棒性
视场和波长扫描
训练集之外的测试图像
物理原型测量
如果一个设计只在其被训练时所使用的精确可微仿真中有效,那它还不是一个鲁棒的光学设计。
这是一种看待可微光学的健康方式:
用可微性来搜索。
用光学分析来理解。
用验证来信任。
三者缺一不可。
26. 为什么这对本书的读者很重要
本书花了许多章节来剖析经典计算。
那并非弯路。
那是理解可微光学所需的基础。
如果有人说:
我们通过一个可微成像流水线端到端地优化镜头。
你现在可以问:
- 光学模型是什么?
- 它追迹的是真实光线,还是使用傍轴近似?
- 它是否包含了衍射?
- 光瞳函数是如何表示的?
- PSF 是如何计算的?
- 传感器模型是否被包含在内?
- 优化的损失是什么?
- 哪些光学参数是可训练的?
- 材料是离散的还是连续的?
- 孔径和渐晕是如何处理的?
- 梯度稳定吗?
- 之后检查了哪些经典指标?
这些问题本身并不是为了怀疑而怀疑。
它们是理解该主张所必须问的问题。
可微光学之所以令人兴奋,恰恰是因为它扩展了我们已经构建起来的链条。没有那条链条,它就只是另一个黑盒。
27. 一个小小的并排对比
对三种优化风格进行比较,可能会有帮助。
| 风格 | 它需要什么 | 优势 | 风险 |
|---|---|---|---|
| 无导数搜索 | 评价函数值 | 使用简单;能处理别扭的函数 | 可能较慢;可能需要很多次计算 |
| 经典梯度 / 最小二乘优化 | 评价函数值和类似导数的信息 | 对平滑的光学设计问题有效 | 依赖于缩放、局部行为、操作数设计 |
| Autodiff 可微光学 | 可微的计算图 | 能通过光线、波动、传感器和重建进行优化 | 需要仔细的可微建模和验证 |
没有哪种是普遍最好的。
一个小的镜头设计问题用传统的光学优化可能就很好。
一个波前编码的计算相机可能从端到端的可微优化中获益。
一个坚固的工程设计可能仍然需要经典公差分析和人工评审。
实用的教训是:
选择与问题相匹配的优化方法,
而不是因为某个方法听起来更现代。
28. 主要的概念升级
本章的主要升级,并不是“使用 PyTorch”。
主要的升级是:
光学计算可以成为一个可微程序的一部分。
一旦做到这一点,光学变量就可以与其他可微组件一起被优化。
镜头可以连接到:
- 相位掩模;
- 衍射元件;
- 传感器模型;
- 重建算法;
- 神经网络;
- 任务特定的损失。
这就是为什么可微光学很重要。
它改变了设计问题的边界。
经典设计常问:
什么样的镜头能给出好的光学指标?
可微计算成像可能会问:
什么样的光学系统和计算重建一同能给出最好的最终结果?
那个问题更大。
如果你漫不经心地提问,它也是一个更危险的问题。
优化循环越大,理解链条中的每一环节就越重要。
29. 哪些不应该相信
因为可微光学与机器学习相关,所以很容易出现夸大的说法。
对如下的说法要保持谨慎:
网络会学到光学。
优化器会发现最好的镜头。
物理约束不再必要了。
MTF 已经过时了。
经典光学设计已死。
这些说法都太松散了。
更好的看法是:
可微优化可以搜索那些手动很难探索的设计空间。
但结果仍然依赖于物理建模、约束、损失、数据和验证。
这个看法保留了兴奋感,却又不失判断力。
本书的立场不是反现代。它是反神秘化。
可微光学值得学习,因为它很强大。
也正因为它强大,它才值得被解密。
30. 为最后一章做准备
我们现在准备回到读者的实际任务。
在本书的开头,问题是:
一条 MTF 曲线是从哪儿来的?
我们已经回答了这个问题。
现在一个新问题出现了:
当光学软件给我任何一个输出时,
我该如何阅读它?
这包括经典输出:
- 布局图;
- 处方;
- 点列图;
- 光线扇形图;
- OPD;
- PSF;
- MTF;
- 评价函数值。
它也包括现代的输出:
- 梯度;
- 训练损失;
- 优化后的相位掩模;
- 可微光线追迹结果;
- 端到端的重建分数。
同样的习惯依然适用:
不要停留在输出上。
要追溯产生它的计算过程。
这个习惯是本书一直在教的真正技艺。
最后一章将把这个技艺收拢成一个实用的阅读检查清单。
本章总结
可微光学意味着构建光学计算的过程,使得梯度能够关于设计参数被计算出来。
自动微分通过执行过的操作运用链式法则。它与通过有限差分手动估计导数的做法不同。
一个可微的光学链可能看起来像:
光学参数
→ 光线追迹或波动光学
→ PSF / MTF / 图像仿真
→ 损失
→ 梯度
→ 参数更新
PyTorch 示例展示了基本模式:
定义可训练的变量
计算光学结果
计算损失
调用 backward()
更新变量
可微光线追迹通过允许导数流过交点、法线、折射、传播和像面误差,扩展了普通光线追迹。
可微的 PSF 和 MTF 计算可以用张量 FFT 操作构建,从而允许基于像质或频率响应的损失。
这种方法对现代计算成像来说是很强大的,因为光学、传感器、重建算法和任务损失可以被一起优化。
但是,可微光学并不是魔法。它仍然需要正确的物理、稳定的采样、有意义的损失、良好的参数化、约束、可制造性检查,以及训练循环之外的验证。
核心思想是:
可微光学并不会取代经典的计算链条。
它是在那个链条上附加上梯度。
这使得那座链条更强大,但也使得理解它变得更加重要。
生成的验证图表

来源与验证说明
本章是一篇定位性章节,而非对整个可微光学文献的综述。其所保留的主张是有限的:当操作是以可微数值框架实现时,自动微分能够将梯度附加到光学计算链条上。实际设计仍然需要物理建模、约束、采样检查,以及训练循环之外的验证。