损失函数与优化器

适用版本 · ArcQML 0.1.0

本页目录

10.1 损失函数:输入约束与前向公式

函数 关键约束
mse_loss prediction 与 target 均为非空 F32/F64;底层逐元素运算可广播。
l1_loss prediction 与 target 均为非空 F32/F64。
binary_nll_loss prediction 与 labels 均为一维且长度相同;标签只能取 0 或 1。
BCE with logits logits 与 targets 的 shape 和 dtype 相同;target 取值位于 0 到 1 之间。
cross_entropy_loss logits 为二维 Tensor;labels 长度等于 batch 大小,每个标签都是合法类别索引。

设样本数为 N,prediction 和 target 的第 i 个元素分别记为预测值与目标值。均方误差损失定义为:

LMSE=12Ni=1N(y^iyi)2.L_{\mathrm{MSE}} =\frac{1}{2N}\sum_{i=1}^{N} \left(\hat{y}_i-y_i\right)^2.

L1 损失定义为:

LL1=1Ni=1Ny^iyi.L_{\mathrm{L1}} =\frac{1}{N}\sum_{i=1}^{N} \left\lvert\hat{y}_i-y_i\right\rvert.

binary_nll_loss 的 prediction 表示 Pauli-Z 期望值,而不是普通概率或 logit。框架先对期望值执行截断:

ziclip=clamp ⁣(zi,1+2ε,12ε),ε=1012.z_i^{\mathrm{clip}} =\operatorname{clamp}\!\left( z_i,-1+2\varepsilon,1-2\varepsilon \right), \qquad \varepsilon=10^{-12}.

随后,将截断后的期望值转换为两个类别的概率:

pi(0)=1+ziclip2,pi(1)=1ziclip2.p_i(0)=\frac{1+z_i^{\mathrm{clip}}}{2}, \qquad p_i(1)=\frac{1-z_i^{\mathrm{clip}}}{2}.

对应的二分类负对数似然损失为:

Lbinary NLL=1Ni=1N[(1yi)logpi(0)+yilogpi(1)],yi{0,1}.L_{\mathrm{binary\ NLL}} =-\frac{1}{N}\sum_{i=1}^{N} \left[ (1-y_i)\log p_i(0)+y_i\log p_i(1) \right], \qquad y_i\in\{0,1\}.

直接接收 logits 的二元交叉熵采用数值稳定形式:

LBCE=1Ni=1N[max(xi,0)+log ⁣(1+exi)yixi].L_{\mathrm{BCE}} =\frac{1}{N}\sum_{i=1}^{N} \left[ \max(x_i,0)+\log\!\left(1+e^{-\lvert x_i\rvert}\right)-y_ix_i \right].

对于包含 B 个样本和 C 个类别的多分类 logits,交叉熵损失定义为:

LCE=1Bb=1Blog ⁣[softmax(Xb)yb].L_{\mathrm{CE}} =-\frac{1}{B}\sum_{b=1}^{B} \log\!\left[ \operatorname{softmax}(X_b)_{y_b} \right].

10.2 优化器

SGD:实现是带耦合 L2 项的更新

Sgd::step 对 trainable 且 requires_grad、并且已有 grad 的 Parameter 更新。对于 F32 或 F64,每个元素执行以下更新:

θθη(g+λθ).\theta\leftarrow\theta-\eta\left(g+\lambda\theta\right).
η=学习率,g=当前梯度,λ=权重衰减系数.\eta=\text{学习率}, \qquad g=\text{当前梯度}, \qquad \lambda=\text{权重衰减系数}.

无梯度参数计入 skipped_no_grad,冻结或 requires_grad=false 参数计入 skipped_frozen

Adam:偏置校正与权重衰减位置

Adam 在首次 step 时按参数切片中的位置建立状态槽位,后续调用必须保持参数数量和顺序不变。优化器保存迭代步数、一阶矩和二阶矩。每次更新时,首先把耦合式 L2 项加入当前梯度:

g~t=gt+λθt1.\widetilde{g}_t =g_t+\lambda\theta_{t-1}.

随后更新一阶矩和二阶矩:

mt=β1mt1+(1β1)g~t,vt=β2vt1+(1β2)g~t2.\begin{aligned} m_t&=\beta_1m_{t-1}+(1-\beta_1)\widetilde{g}_t,\\ v_t&=\beta_2v_{t-1}+(1-\beta_2)\widetilde{g}_t^2. \end{aligned}

完成偏置校正后,再更新参数:

m^t=mt1β1t,v^t=vt1β2t,θt=θt1ηm^tv^t+ε.\begin{aligned} \widehat{m}_t&=\frac{m_t}{1-\beta_1^t},\\ \widehat{v}_t&=\frac{v_t}{1-\beta_2^t},\\ \theta_t&=\theta_{t-1} -\eta\frac{\widehat{m}_t}{\sqrt{\widehat{v}_t}+\varepsilon}. \end{aligned}

因此,这里的 weight_decay 会进入矩估计,不等同于 AdamW 的解耦权重衰减。

构造器要求 learning_rate、weight_decay 为有限非负数,beta1 和 beta2 位于 [0,1),epsilon 为有限正数。训练中必须复用同一个 Adam 实例;重新创建会丢失动量与步数。

10.3 推荐训练循环

rust
let mut optimizer = Adam::new(0.01, 0.9, 0.999, 1e-8, 0.0)?;
for _ in 0..steps {
    let prediction = simulator.run(&circuit, &observable)?;
    let loss = mse_loss(&prediction, &target)?;
    loss.backward()?;
    optimizer.step(circuit.parameters())?;
    optimizer.zero_grad(circuit.parameters());
}

建议在每轮优化完成之后调用 zero_grad,清空当前梯度防止下一个训练循环的梯度累积。不要在 backward 之前替换或原地修改参与前向计算的 Parameter Tensor,否则版本检查可能报告前向数据已被修改。参数更新应交给 optimizer.step 完成。