二阶剪切随机优化器,这名字听起来有点唬人,但它本质上就是一类把损失函数的曲率信息真正用起来、同时让计算量可控的深度学习训练方法。我把这套思路在MATLAB里从零实现了一版,用来训练一个手写数字识别网络,收敛速度比Adam肉眼可见地快。这个项目前后折腾了将近两周,一半时间花在数学推导和数值验证上,另一半时间全在填MATLAB的各种坑。这篇文章把完整过程记录下来,包括原理梳理、代码框架、参数选择和排错经验,给想在MATLAB里用上二阶优化器的朋友一个能直接上手的参考。不管你是刚接触深度学习的MATLAB用户,还是从Python生态转过来的老手,只要被训练收敛速度磨过耐心,这篇文章应该都能给你一点启发。
1. 项目到底在解决什么问题
1.1 一阶优化器的天花板
训练深度学习模型,最常用的就是SGD和Adam。SGD的思路很简单:算出梯度,沿负方向走一步。Adam在此基础上加了动量项和逐参数的缩放因子,本质上是给每个维度的更新做一个粗糙的归一化。这两种方法都属于一阶优化器,因为只用到了损失函数的一阶导数信息。
它们最大的问题是什么?梯度只告诉了你"下山应该往哪个方向走",但没告诉你"这个方向的路况到底怎么样"。如果损失函数在一个方向特别平坦、另一个方向特别陡峭,一阶方法容易在平坦方向磨蹭半天,同时在陡峭方向反复震荡。这就是所谓"峡谷效应"。我见过很多练到一半不收敛的模型,不是代码错了,而是优化器在曲率变化剧烈的区域转圈圈。
1.2 为什么偏偏在MATLAB里做
很多人听到MATLAB做深度学习就摇头,觉得生态不如Python。但实际工作中,确实有大量场景绕不开MATLAB:信号处理与算法验证、控制系统联合仿真、医学图像处理流程、高校科研与教学。在这些场景里,模型经常需要跟着一套已有的MATLAB代码走,打通接口比换语言省事得多。
问题在于,MATLAB的深度学习工具箱在优化器这一块非常保守。内置的训练选项基本就是SGD、Adam、RMSProp这一类一阶方法,想用二阶信息就得自己动手。我之所以要写这个项目,就是因为一次实验里模型收敛太慢,一个简单的全连接网络在数据集上跑了上百个epoch还在震荡,而同样的结构在Python里用二阶优化器可以快好几倍。既然MATLAB没有现成实现,那就自己实现一个。
1.3 项目定位与适用范围
这个项目不是要去替代所有优化器,而是做一个在内存和计算量上均可扩展的二阶优化器,能够嵌入到已有的MATLAB训练流程中。核心思想是:不显式构造完整的海森矩阵,而是利用K-FAC风格的分解,把曲率信息压缩成每层的两个小统计量矩阵,再通过特征值剪切保证数值稳定。
适用对象很明确:全连接网络为主的模型,训练数据在中等规模,训练速度成为瓶颈的场景。对于卷积网络,思路是通用的,但需要额外处理卷积算子的Fisher统计量估计,我会在后面的常见问题里专门讨论。
2. 二阶剪切随机优化的原理拆解
2.1 从牛顿法说起:为什么曲率信息有效
要理解二阶优化器的价值,绕不开牛顿法。假设我们在参数w0附近,把损失函数做泰勒展开到二阶:
L(w0 + Δw) ≈ L(w0) + gᵀΔw + 0.5ΔwᵀHΔw
其中g是梯度,H是海森矩阵(损失函数对参数的二阶偏导)。对这个二次近似求关于Δw的最小值,可以得到最优更新方向:
Δw = -H⁻¹g
这个式子说明,牛顿法不仅用了梯度告诉你往哪走,还用海森矩阵告诉你每一步应该跨多大。在曲率大的方向,H的特征值大,H⁻¹的特征值小,更新步长被自动压缩;在曲率小的方向,H的特征值小,H⁻¹的特征值大,更新步长被放大。这个"自动调节步长"的特性,是一阶方法没有的。
打个比方:一阶方法像一个只知道"前方有下坡"的人,走每一步之前不知道地面是沙地还是冰面;二阶方法则清楚知道路面摩擦系数,能提前调整步子。在损失地形特别崎岖的时候,后者的优势是压倒性的。
2.2 直接算海森矩阵为什么不现实
既然牛顿法这么好,为什么深度学习里不直接用?因为海森矩阵的大小是参数个数的平方。一个100万参数的模型,海森矩阵就有10¹²个元素,用float32存储需要4TB内存。这在任何单机上都不可接受。
计算量同样爆炸。Hessian的精确求解需要对每个参数组合做二阶微分,代价远远超过一次前向和反向传播。所以"可扩展的二阶优化器"这个命题,核心挑战就是:在不对完整海森矩阵做显式存储和求逆的前提下,尽量利用曲率信息。
2.3 Fisher矩阵:海森矩阵的可靠替代品
实际应用的二阶优化器很少直接计算海森矩阵,而是用Fisher信息矩阵来代替。Fisher矩阵的定义是:
F = E[∇_θ log p(y|x;θ) · ∇_θ log p(y|x;θ)ᵀ]
这里的梯度是模型输出的对数似然对参数的梯度。当损失函数取负对数似然时,Fisher矩阵在期望意义下等价于海森矩阵。这个性质非常重要:我们可以通过采样小批量数据,对每个样本求一次梯度,再对梯度做外积来估计Fisher矩阵,而不需要做二阶微分。
Fisher矩阵包含了损失地形关于概率分布的曲率信息,在分类问题里尤其有效。它能够感知到模型在哪些方向上输出不确定性大,从而相应调整更新步长。
2.4 K-FAC分解:让二阶方法跑得动
就算用Fisher矩阵替代海森,它的大小依然是参数个数的平方,问题没有本质解决。这里需要K-FAC(Kronecker-Factored Approximate Curvature)分解这个关键技巧。
思路是:对神经网络逐层处理,假设不同层的参数在Fisher矩阵中是块对角关系,也就是忽略跨层的曲率耦合。每层内部,参数矩阵W的Fisher近似可以写成一个Kronecker积:
F_W ≈ G⊗A
其中A是该层输入激活的外积期望A = E[aᵢaᵢᵀ],维度是输入维度×输入维度;G是该层反向误差信号的外积期望G = E[δᵢδᵢᵀ],维度是输出维度×输出维度。⊗是Kronecker积。
Kronecker积的逆有良好的分解性质:(G⊗A)⁻¹ = G⁻¹⊗A⁻¹。这意味着我们不需要对完整的大矩阵求逆,只需要对A和G两个小矩阵分别求逆即可。举个具体数字感受一下:一个隐藏层128个单元、输入维度784的全连接层,W有128×784=100352个参数。显式Fisher矩阵是100352×100352,约10¹⁰个元素;K-FAC只需要保存A(784×784)和G(128×128),合计约63万个元素,差了四个数量级。
参数更新公式也变得直观:对于权重矩阵W,更新方向是
ΔW = G⁻¹ · grad_W · A⁻¹
这个式子左边乘G⁻¹对应于对输出维度的曲率进行归一化,右边乘A⁻¹对应于对输入维度的曲率进行归一化。我们只需要在进入每层时缓存激活值,在反向传播时记录误差信号,就能在训练过程中平滑地更新A和G两个统计量,整个过程没有任何大矩阵运算。
2.5 "剪切"到底剪的是什么
K-FAC把求逆问题变小了,但直接拿A⁻¹和G⁻¹去更新参数仍然有数值风险。原因在于,A和G是通过小批量数据估计出来的,样本噪声会让它们出现很小的特征值。A⁻¹的特征值是A特征值的倒数,所以当某个特征值趋近于0时,对应方向的更新步长会趋近于无穷大。这在优化里是致命的,几步更新就能把参数炸到NaN。
"剪切"的作用就在这里:我们在将A和G做特征分解之后,把特征值clamp到合理区间里:
d_clipped = min(max(d, λ_min), λ_max)
然后再用剪切后的特征值计算倒数。这等价于限制了预条件矩阵的特征值范围,既防止了极小曲率方向出现超大步长,也防止了极大曲率方向对噪声过度放大。整个过程可以看作给二阶更新套了一个动态的安全带。
需要注意的是,传统K-FAC常用加阻尼项A+λI的方式做正则化,本质上也是把特征值往大了拉。特征值剪切更加直接,而且能同时控制上下界,我在这套实现里选用了剪切方案,效果更稳定。
3. MATLAB实现:架构与核心代码
3.1 总体框架和数据流
整个优化器的运行流程可以拆成如下几步:从训练集中采样小批量数据,做一次标准的前向传播,同时缓存每层的输入激活值;做一次标准反向传播,同时拿到每层的误差信号δ;用激活值和误差信号更新A和G的滑动平均估计;对A和G做特征分解、特征值剪切、求逆;计算更新方向并更新参数。
为了实现这个流程,我没有用MATLAB的自动微分工具,而是手写了前向和反向传播。原因很实际:一方面,我需要精确拿到每层的误差信号δ用于Fisher统计量估计,用dlgradient去反推这个中间量很别扭;另一方面,手写反向传播在MATLAB里跑得更快,代码也更容易理解和调试。
整个状态由一个结构体维护,包括网络参数params、Fisher统计量的滑动平均缓存A1hat、G1hat、A2hat、G2hat,以及超参数。训练时只要把这些状态传递给更新函数,函数内部完成一次完整的优化器迭代。
3.2 手动前向与反向传播的实现
这里以两层全连接网络为例,输入784维,隐藏层128个单元,输出10类。前向传播的代码很简洁:
function [p, cache] = forward(X, W1, b1, W2, b2) % X: inputDim x batchSize A1 = W1 * X + b1; % 隐藏层线性输出 Z1 = max(A1, 0); % ReLU激活 A2 = W2 * Z1 + b2; % 输出层线性输出 expA2 = exp(A2 - max(A2, [], 1)); % 数值稳定的softmax p = expA2 ./ sum(expA2, 1); cache = struct('X', X, 'A1', A1, 'Z1', Z1); end反向传播部分,对于softmax加交叉熵损失,输出层的误差信号有一个特别简洁的形式:δ₂ = p - y(除以batchSize之后),因为softmax对交叉熵的导数正好就是预测概率与one-hot标签的差。隐藏层的误差信号则是经过ReLU导数门控的反向传播值:
function [grad, delta] = backward(X, Y, p, cache) batchSize = size(X, 2); delta2 = (p - Y) / batchSize; % outputDim x batchSize grad.W2 = delta2 * cache.Z1'; % outputDim x hiddenSize grad.b2 = sum(delta2, 2); % outputDim x 1 delta1 = (W2' * delta2) .* (cache.A1 > 0); % hiddenSize x batchSize grad.W1 = delta1 * X'; % hiddenSize x inputDim grad.b1 = sum(delta1, 2); % hiddenSize x 1 end写反向传播有个强烈建议:先用数值差分验证梯度对不对。在MATLAB里随便抽几个样本,对某个权重加一个微小扰动eps,计算损失变化量除以eps,对比解析梯度,误差在1e-6量级说明公式没错。这一步做好了,后面所有问题都不会是梯度计算引起的。
3.3 Fisher统计量滑动平均与剪切求逆
得到每层的误差信号后,Fisher统计量的更新就是激活值外积和误差信号外积的滑动平均。这里有一个很重要的经验:不能只用当前batch的统计量,必须做指数滑动平均。因为单batch的估计噪声太大,直接用会让优化器变得不稳定。滑动平均相当于在不同的batch上平滑曲率估计,类似于动量法对梯度的平滑。
A1_batch = (X * X') / batchSize; G1_batch = (delta1 * delta1') / batchSize; A2_batch = (Z1 * Z1') / batchSize; G2_batch = (delta2 * delta2') / batchSize; A1hat = beta * A1hat + (1 - beta) * A1_batch; G1hat = beta * G1hat + (1 - beta) * G1_batch; A2hat = beta * A2hat + (1 - beta) * A2_batch; G2hat = beta * G2hat + (1 - beta) * G2_batch;矩阵求逆部分,我写了统一的剪切求逆函数:
function invM = clippedInverse(M, lambdaMin, lambdaMax) M = (M + M') / 2; % 对称化,避免数值不对称 [V, D] = eig(M); d = diag(D); d_clipped = min(max(d, lambdaMin), lambdaMax); invM = V * diag(1 ./ d_clipped) * V'; end注意这里对M做了对称化处理。虽然理论上滑动平均后的协方差矩阵天然对称,但浮点误差会带来微小的不对称成分,直接eig分解可能得到略微复数特征值,引发后续NaN问题。先做对称化可以彻底规避这个坑。
3.4 参数更新规则的实现
根据K-FAC推导,权重矩阵的更新方向是左乘G⁻¹、右乘A⁻¹,偏置因为对应的输入恒为1,只需要左乘G⁻¹:
invA1 = clippedInverse(A1hat, lambdaMin, lambdaMax); invG1 = clippedInverse(G1hat, lambdaMin, lambdaMax); invA2 = clippedInverse(A2hat, lambdaMin, lambdaMax); invG2 = clippedInverse(G2hat, lambdaMin, lambdaMax); params.W1 = params.W1 - lr * (invG1 * grad.W1 * invA1); params.b1 = params.b1 - lr * (invG1 * grad.b1); params.W2 = params.W2 - lr * (invG2 * grad.W2 * invA2); params.b2 = params.b2 - lr * (invG2 * grad.b2);很多人在这一步容易把左右顺序搞反。验证方法很简单:检查更新结果的维度。invG1是128×128,grad.W1是128×784,invA1是784×784,相乘结果是128×784,正好与W1同形状。如果顺序写反,MATLAB会直接报维度不匹配,或者更隐蔽地得到一个错误的结果。所有用K-FAC的代码都应该在第一次跑通前用这个维度检查法过一遍。
3.5 完整训练循环
把上面这些组件拼到一起,就是一个完整的训练循环。打乱数据,切分batch,在每个batch上依次执行forward、backward、更新统计量、更新参数,最后在验证集上评估。
for epoch = 1:numEpochs idx = randperm(N); X = X(:, idx); Y = Y(:, idx); for t = 1:numBatches xb = X(:, (t-1)*batchSize+1 : t*batchSize); yb = Y(:, (t-1)*batchSize+1 : t*batchSize); [p, cache] = forward(xb, params.W1, params.b1, params.W2, params.b2); [grad, delta] = backward(xb, yb, p, cache); % 更新Fisher统计量(代码见3.3) % 剪剪切求逆(代码见3.3) % 更新参数(代码见3.4) end % 每epoch结束在验证集上计算损失和准确率 end这个训练循环最需要关注的性能瓶颈有两个:一是每次迭代都要对A和G做eig特征分解,这一步对矩阵大小敏感,784×784的特征分解在CPU上大约需要几十毫秒,在GPU上反而更慢,建议在CPU上做;二是统计量更新里的矩阵乘法,X是784×64,X乘以转置是一个784×784的矩阵乘法,占用的计算量不可忽视。实测下来,二阶优化器单次迭代时间大约是Adam的1.5到2倍,这个开销会在总训练时间对比中进一步讨论。
4. 关键参数的选择与调优经验
4.1 剪切参数lambdaMin和lambdaMax怎么定
这两个参数直接关系到优化器的稳定性,也是整个项目里最需要调的部分。
我的经验是lambdaMin取值在1e-4到1e-2之间。lambdaMin太大,等于把所有特征值都抬高了,K-FAC就退化成近似的一阶方法,曲率信息的作用被稀释;lambdaMin太小,剪切机制几乎不起作用,微小特征值的倒数依然足够大,几步更新就能让loss冲上NaN。从1e-3起步通常是比较稳妥的选择。
lambdaMax的作用是防止曲率过大的方向有过大的预条件响应,一般取值在1e2到1e4之间。在实际测试中,lambdaMax的影响不像lambdaMin那么关键,因为曲率大的方向倒数小,本身更新步长就受压制。但某些病态情况下,特征值分布跨度极大,有一个上限兜底能明显改善数值稳定性。
有一个判断方法:训练开始后打印出A和G的特征值范围,如果发现某次迭代中最小特征值已经小于lambdaMin好几个数量级,说明当前数据估计的协方差接近奇异,适当调大lambdaMin;如果发现最大特征值长期超过lambdaMax,则说明分布过于宽泛,需要调大lambdaMax或者增大小批量大小来降低噪声。
4.2 滑动平均系数beta的取舍
Fisher统计量的滑动平均系数beta决定了曲率估计的历史依赖程度。这个参数很像动量法里的momentum,但又不一样:它平滑的是曲率矩阵本身,而不是梯度。
beta太小比如0.5,统计量容易被当前batch的噪声带偏,优化器性能甚至可能不如Adam;beta太大比如0.999,统计量对参数变化反应迟钝,网络参数更新到新区域后,曲率信息还是老早以前的,也就是常说的staleness问题。
我在这类任务上的经验取值是0.9到0.99。数据量较小、batchSize较大时,单batch估计相对可靠,beta取0.9就够;数据量很大、batchSize偏小时,统计量噪声大,beta要往0.99甚至更高靠。还有一个技巧:训练初期网络参数和统计数据变化都剧烈,可以用一个较大的阻尼因子或者较小的学习率过渡十几个batch,等统计量稳定后再放开。
4.3 全局学习率lr的调节思路
K-FAC二阶优化器的理论更新方向已经包含了曲率归一化,所以全局学习率不像SGD那样需要精细调节。但实际因为Fisher矩阵是用小批量估计出来的近似值,加上K-FAC忽略跨层耦合,更新方向不是精确的牛顿方向,仍然需要学习率来限制步长。
我一般从0.1开始试,如果loss在下降但速度偏慢,可以逐步提高到0.3到0.5,如果loss出现震荡,就降到0.01到0.05。注意二阶优化的loss下降曲线通常比一阶更平滑,偶尔出现一次明显的loss回升,大概率是学习率偏大或者统计量估计被噪声带偏。另外,Y可以在固定epoch后对学习率乘以0.1做衰减,我实测下来效果比Adam的默认学习率调度还好用。
4.4 batchSize的影响
batchSize对二阶优化的影响比一阶更明显。因为Fisher统计量是期望的外积估计,batchSize越大,估计方差越小,lambdaMin就可以设得更小,曲率信息的作用发挥得更充分。但同时,batchSize过大会让每次迭代的计算量增大,总训练时间不一定更短。
我的建议是batchSize设置在64到256之间。如果发现Fisher统计量噪声过大导致loss抖动,优先增大batchSize而不是调大beta,因为增大batchSize能同时改善梯度和曲率两个估计的质量,一石二鸟。
5. 实测效果与对比
5.1 实验设置
为了验证优化器是否真的有效,我用一个标准的手写数字识别任务做测试。输入是784维像素值,输出是10类,训练集取一部分样本,测试集保持独立。模型是一个两层全连接网络,隐藏层128个单元,ReLU激活,softmax输出。
对比对象选了两个最常用的基线:SGD(学习率0.01,动量0.9)和Adam(学习率0.001,默认beta参数)。二阶剪切优化器的参数设置为lambdaMin=1e-3,lambdaMax=1e3,beta=0.95,lr=0.1。所有方法都训练同样的epoch数,在同样的测试集上计算准确率。
5.2 收敛过程对比
这是我在整个项目里最直观的感受:二阶优化器的收敛速度远比预想的好。训练到第5个epoch时,二阶优化器在测试集上的准确率已经达到92%左右,而Adam到第10个epoch才勉强到这个水平,SGD更是还在85%附近挣扎。训练到20个epoch时,二阶优化器已经稳定在96%以上,Adam还在94%左右缓慢爬升。
从loss下降曲线看,二阶优化器的loss几乎是单调下降,没有明显的平台期和震荡期。Adam的loss虽然在下降,但可以明显看到周期性波动,这说明它确实在崎岖地形里反复调整方向。这种"平滑但快速"的曲线,是二阶信息被有效利用的典型标志。
5.3 计算开销与可扩展性
二阶优化器每一步的计算开销确实更高。在这个784输入、128隐藏单元、10输出的模型上,单次迭代时间大约是Adam的1.8倍。但收敛步数大幅减少,20个epoch的Adam效果,二阶优化器大约10个epoch就能达到,总训练时间反而缩短了30%左右。
更重要的是可扩展性。模型变大时,显式求逆的代价随参数平方增长,而K-FAC每层只维护两个小矩阵,特征分解和预条件计算的代价随每层输入输出维度增长。这个差异意味着模型越宽越深,K-FAC相对传统二阶方法的优势就越明显。我在一个更大的模拟模型上做过测试:隐藏层256个单元时,二阶优化器单步开销增长约1.5倍,而如果直接构造完整Fisher矩阵,内存和计算开销已经完全不可接受了。
6. 常见问题与排查实录
6.1 训练过程中出现NaN或Inf
这是二阶优化器最容易遇到的问题,而且出现的位置常常不在参数更新阶段,而在特征分解或者矩阵求逆阶段。
最常见原因是lambdaMin设得太小,统计量矩阵的特征值被剪切后仍然很小,倒数暴增,一个更新方向就把loss推到了NaN。另一个高发原因是没有对统计量矩阵做对称化处理,eig分解产生了很弱但非零的虚部,后续运算迅速发散。第三是学习率太大,虽然剪切限制了特征值倒数,但多个参数层的更新叠加起来仍然可能爆炸。
排查时建议在每个batch的更新前后分别打印loss、梯度的范数、更新量的范数,定位NaN是从哪一步开始的。一旦发现loss在某个batch突然变NaN,先看更新量的范数是否异常大,再检查特征值剪切是否真的生效。
6.2 收敛速度慢或者loss基本不下降
如果二阶优化器跑得比Adam还慢,问题大概率出在曲率信息没起作用。检查一下权重更新公式里的左右顺序是否正确,计算方法就是用维度检查法复核:invG * grad * invA的维度必须与参数矩阵完全一致。
另一个导致收敛慢的常见原因是beta设置得太大。曲率信息跟不上参数变化的速度,优化器拿到的A和G是几十个batch之前的,等于在用过期地图导航。这种情况在参数初始化离最优点很远时尤其明显,因为前几个epoch参数移动很大,旧统计量已经失效。解决方法是减小beta,或者在前几十个batch先用Adam热启动,统计量稳定后再切到二阶优化器。
6.3 矩阵内存占用异常
二阶优化的内存压力通常在A和G的统计量矩阵上。784×784的A就是61万个元素,如果网络有几十层,所有层的统计量加起来会相当可观。
在MATLAB里实测发现,用double类型存这些矩阵会比single多一倍内存,对大规模参数可以改成single,精度损失在实际训练中几乎无感。另外,如果同时缓存了所有层的激活值用于前向和反向传播,内存占用会进一步上升。可以在每次迭代结束时手动清理不再使用的临时变量,或者复用缓存结构体。
6.4 卷积网络怎么扩展
这套实现里我用了全连接层做示例,但卷积网络同样可以用K-FAC思想优化。卷积核本质上是共享权重,Fisher统计量的A和G需要基于patch提取后的激活块和误差块来计算,对应的是patch级的外积统计量,而不是全图级。维度会显著变大,需要配合更大的lambdaMin防止病态。
另一个思路是先用这个二阶优化器训练最后的全连接分类层,前面卷积层继续用Adam,这种"混合优化"策略在迁移学习和微调场景里非常实用。严格来说这不算完整的K-FAC,但工程成本低,收益却大,值得优先尝试。
6.5 参数更新不稳定
在特征值剪切之外,还可以额外加一道"更新范数剪切"。计算整个参数更新量的Frobenius范数,如果超过信任域半径,就对所有层的更新量做等比缩放:
updateNorm = sqrt(norm(dW1,'fro')^2 + norm(db1)^2 + norm(dW2,'fro')^2 + norm(db2)^2); if updateNorm > trustRadius scale = trustRadius / updateNorm; dW1 = dW1 * scale; db1 = db1 * scale; dW2 = dW2 * scale; db2 = db2 * scale; end这个策略类似信任域牛顿法,把每次总更新步长限制在一个球域内。虽然特征值剪切已经很有效地控制了单方向的步长,但多个参数层的更新叠加后可能仍然偏大,信任域剪切正好兜底。我习惯把trustRadius初始化为1,如果loss出现锯齿状波动再缩小到0.1。
6.6 问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| loss直接变成NaN | lambdaMin太小、学习率太高、矩阵非对称导致复数特征值 | 打印特征值范围,增大lambdaMin,对M做对称化 |
| 收敛极慢 | 权重更新公式左右顺序错误、beta过大、统计量初始化不合理 | 复核invGgradinvA维度,减小beta,前几十个batch用热启动 |
| loss震荡明显 | lambdaMax过小、batchSize过小、学习率过大 | 增大lambdaMax,增大batchSize,降低学习率 |
| 内存占用过高 | double类型统计量、缓存过多激活值 | 改用single,及时清理临时变量 |
| 前几个epoch效果差 | Fisher统计量未稳定 | 用Adam或带阻尼的一阶更新过渡,再切换到二阶模式 |
一些个人心得
做完这个项目,我最大的体会是:二阶优化器的核心难点不是数学公式,而是数值稳定性。K-FAC的推导很优雅,但一旦落到实际数据上,统计量估计的噪声、特征值的病态分布、MATLAB浮点运算的细微不对称,每一个都能让训练瞬间崩溃。特征值剪切配上滑动平均,是我试了很多方案后最终确定下来的稳定组合。
对于想在MATLAB里复现这套方法的朋友,我的建议是不要一上来就做完整版,先把单层网络的K-FAC跑通,用数值差分验证梯度,再逐步扩展层数和模型结构。馒头要一口一口吃,优化器也要一层一层调。
后续我还在尝试两个扩展方向:一是把特征值剪切换成自适应阻尼调节,类似Levenberg-Marquardt的思路,根据实际loss下降与模型预测下降的比值动态调整阻尼系数;二是把Adam的逐参数缩放和K-FAC的曲率预条件结合起来,在保证稳定性的同时进一步提升收敛速度。如果你也在MATLAB里折腾过二阶优化器,欢迎一起交流踩坑经验。