☰
雅可比与海森矩阵:从一阶到二阶的工程实战指南
2026/10/17 11:09:36 网站建设 项目流程

看到标题里的 Jacobin,我第一反应是拼错了——大概率是 Jacobian。这个笔误几乎每周都能在技术群里见到,但问题不大,真正要搞懂的是 Jacobian 矩阵和 Hessian 矩阵这一对组合到底在干什么。搞数值优化、机器人运动学、参数辨识、三维重建和机器学习的人,基本天天和它们打交道;但很多新手要么停留在“雅可比是个矩阵,海森是二阶导”的背诵层面,要么写代码时把维度搞反,让优化器原地爆炸。这篇文章我想换个讲法:不按教科书顺序罗列定义,而是从工程视角把两个矩阵讲透。

1. 雅可比矩阵的核心:它把局部变化翻译成了矩阵

1.1 先来一个可以直接算的例子

很多人一看到 Jacobian 的定义就头疼,因为它总是用一大串符号:

J[i][j] = ∂f_i / ∂x_j

翻译成人话就是:我的第i个输出,对第j个输入求偏导,然后把所有偏导排成一张表。举个最简单的二维映射:

f1(x, y) = x^2 + sin(y) f2(x, y) = x*y + e^y

这个f有两个输入x, y,也有两个输出f1, f2,所以 Jacobian 是 2×2 矩阵:

J = [[2x, cos(y)], [y, x + e^y]]

拿点(x=1, y=0)代入,具体得到:

J = [[2, 1], [0, 2]]

现在做一个小实验:让x增加 0.01,y不变。真实输出的变化约等于J第一列乘以 0.01,也就是[0.02, 0.00]。让y增加 0.01,真实输出的变化约等于J第二列乘以 0.01,也就是[0.01, 0.02]。

这个理解非常重要:Jacobian 的每一列,就是“某个输入分量变化时,输出向量会朝哪个方向移动”。输入变一点,输出就沿着对应的列方向滑出去;输入几个分量同时变,输出变化就是这些列的线性组合。哪怕函数再非线性,只要扰动足够小,这种线性翻译都是成立的。

1.2 工程里真正关心的,往往是 Jacobian 的列空间

在参数辨识、机器人运动学、优化这些场景里,我们很少只关心 Jacobian 的某一项,而是关心整张矩阵里“列和列之间的关系”。

简单说,如果 Jacobian 的两列方向差不多,意味着两个不同参数对输出的影响方式非常相似,这时候数据很难把这两个参数区分开。比如某个模型里两个系数都以几乎相同的方式出现在输出里,那么从观测数据反推参数时,你会得到一个非常病态的问题:参数组合有无穷多个,拟合曲线却几乎一样。

我在实际项目里碰到过类似情况。一开始用梯度下降怎么调都不收敛,后来把 Jacobian 的列拿出来一看,两列相关性高得离谱,本质上是模型本身的可辨识性不足,不是优化器不行。这时候要做的不是硬调迭代步长,而是改模型结构、加约束,或者换一组信息量更大的输入数据。

如果处理的是方阵,det(J) = 0的点叫奇异位形。机器人的例子最直观:末端速度v和关节速度q_dot的关系是v = J(q) * q_dot。当机器人某个关节角度组合让 Jacobian 行列式变成零时,某些方向的末端速度根本无法用关节运动产生,这就是机械臂“卡死”或失去可控性的位置。很多控制器在这些位形附近会突然输出特别大的关节力矩,原因就是 Jacobian 的逆接近无穷大,微小误差被放大。

1.3 从雅可比到海森:一阶到二阶只差一次求导

Jacobian 和 Hessian 不是两个孤立的概念。如果函数输出只有一个分量,也就是说f是一个标量函数S(x),那它的 Jacobian 退化成一行偏导,这就是我们常说的梯度。

现在把梯度看作一个新的向量函数,再对它求一次 Jacobian,得到的就是 Hessian 矩阵:

H[i][j] = ∂²S / ∂x_i ∂x_j

我习惯这样记忆:Hessian 是梯度的 Jacobian。例如标量函数:

S(x, y) = 3x²y + e^y

先算梯度:

∇S = [6xy, 3x² + e^y]

再对梯度求偏导:

H = [[6y, 6x], [6x, e^y]]

这个矩阵一定对称。原因是混合偏导和求导顺序无关,∂²S/∂x∂y = ∂²S/∂y∂x。工程上看到不对称的 Hessian,第一反应应该是公式或代码写错了,而不是“数学本来就如此”。

2. Hessian 矩阵:曲率不是玄学,它直接决定优化方向

2.1 光看梯度为零,根本判断不了极值

很多刚入门的朋友以为“梯度为零就是极小值”,这在多元函数里完全不够。梯度为零只说明这是一个驻点,它可能是极小值、极大值,也可能是鞍点。

最经典的例子是:

S(x, y) = x² - y²

在(0,0)处,梯度是[0,0]。但沿x方向,函数是下凸的,像是谷底;沿y方向,函数是上凸的,像是山顶。这个点既不是极小也不是极大,它是一个鞍点。

Hessian 能帮你区分:在驻点处,如果 Hessian 正定,也就是所有特征值都大于零,那这是局部极小值点;如果 Hessian 负定,所有特征值都小于零,就是局部极大值点;如果特征值有正有负,那就是鞍点。

对应上面例子,Hessian 是diag(2, -2),特征值一正一负,所以一眼就看出是鞍点。这也是为什么只靠梯度下降容易卡在鞍点附近,尤其在高维神经网络损失函数里,鞍点远比极小值点常见。

2.2 牛顿步为什么比梯度下降更“聪明”

把标量函数S在某个点x附近做二阶泰勒展开:

S(x + δ) ≈ S(x) + ∇S^T δ + 0.5 * δ^T H δ

如果 Hessian 正定,极小点对应的δ满足:

H δ = -∇S

所以:

δ = -H^{-1} ∇S

这就是牛顿法。梯度下降的更新是:

δ = -η ∇S

区别在哪?梯度下降用一个固定的标量学习率η给所有方向统一缩放。牛顿法则用H^{-1}对梯度做了一次线性变换,相当于给每个方向配了不同的步长:曲率大的方向走小步,曲率小的地方走大步。

我常用一个生活类比:梯度告诉你在房间里往哪边走能下坡,Hessian 告诉你这个坡是越来越陡还是越来越缓,甚至前面是不是有个拐弯。只看梯度,你可能会在一个长而平的“峡谷”里来回震荡;带上曲率信息,就能沿着谷底更快地滑下去。

2.3 为什么现实里几乎没人显式存一个完整 Hessian

理论很漂亮,但工程里 Hessian 常常存不下来。原因很简单:参数数量n一旦上来,n × n的双精度浮点矩阵就吃不消了。

参数数量 nHessian 元素个数约占用内存
1001e480 KB
1 万1e8800 MB
100 万1e128 TB

一百万个参数在机器学习里很常见,8 TB 的矩阵基本告别常规服务器。所以现实工程中很少直接构造和存储完整 Hessian,而会改用以下三类替代思路:

  • 拟牛顿法,比如 L-BFGS,用最近几步的梯度差分信息隐式逼近H^{-1},只需要存一小段历史向量。
  • Gauss-Newton 近似,在最小二乘问题里用J^T J近似 Hessian,后面会详细展开。
  • Hessian-vector product,利用自动微分算H v,不需要把H形成矩阵。

这些手段本质上都是在和“Hessian 太贵”这件事博弈。你能理解 Hessian 是什么,再看这些方法,就会知道它们到底在近似什么、牺牲了什么。

3. 一个完整的拟合案例:Jacobian 和 Hessian 如何联手干活

3.1 建模:先写出残差和解析 Jacobian

理论知识说再多,不如把一个具体问题跑通。假设我有若干组观测数据(x_i, y_i),希望用衰减指数模型去拟合:

y = a * e^{-b x} + c

要估计的参数是p = [a, b, c]。定义每个数据点的残差:

r_i(p) = y_i - (a * e^{-b x_i} + c)

目标函数是:

S(p) = 0.5 * Σ r_i^2

这里0.5纯粹是为了让后面的梯度表达式少个系数,完全不影响最优解。

如果直接对r_i求偏导,可以得到每一行残差的解析 Jacobian:

∂r_i / ∂a = -e^{-b x_i} ∂r_i / ∂b = a * x_i * e^{-b x_i} ∂r_i / ∂c = -1

如果有一千个观测点,J就是一千行乘三列的矩阵。每一行对应一个残差,每一列对应一个参数。

很多初学者会习惯用有限差分去算 Jacobian,省事,但更好的是写出解析形式。解析求导速度快,精度也高,尤其是接近最优值时,数值差分的误差会明显干扰收敛。

3.2 高斯牛顿和 LM:J 怎么变出近似 Hessian

最小二乘目标函数的海森矩阵其实可以写成两部分:

H = Σ (J_i^T J_i + r_i * ∇²r_i)

第一项是J^T J,第二项是残差对参数的二阶导乘以残差值本身。高斯牛顿法的核心近似就是把第二项丢掉,直接令:

H ≈ J^T J

为什么可以丢?接近最优解时,残差r_i本身已经很小,乘以二阶导后就更小,所以第二项相对于第一项不关键。但在迭代刚开始、残差还很大时,这个近似就不太靠谱,步子容易迈得太大导致发散。

于是工程上几乎都用列文伯格-马夸尔特方法,也就是加一个阻尼项:

A = J^T J + λ I
  • λ很大时,A ≈ λ I,更新步长约等于梯度下降的一小步,稳定但慢。
  • λ很小时,A ≈ J^T J,退化为高斯牛顿,接近最优解时收敛很快。

你只需要提供一次 Jacobian,就能在迭代中通过调节λ自动在“稳妥的梯度下降”和“快速的高斯牛顿”之间切换。这就是为什么很多非线性最小二乘求解器让你返回 Jacobian,而不要求你返回 Hessian。

3.3 一个 10 行左右的 LM 迭代骨架

下面是一个极简实现,矩阵运算用常规线性代数库就能完成,重点看迭代结构:

# Python 风格伪代码:矩阵库负责 transpose、solve、norm 等操作 def residual(p, x, y): a, b, c = p return y - (a * exp(-b * x) + c) def jac_res(p, x): a, b, c = p J = empty((len(x), 3)) J[:, 0] = -exp(-b * x) J[:, 1] = a * x * exp(-b * x) J[:, 2] = -1.0 return J def lm_fit(x, y, p0, lam=1e-3, max_iter=100): p = p0[:] for _ in range(max_iter): r = residual(p, x, y) J = jac_res(p, x) A = transpose(J) @ J + lam * identity(3) g = transpose(J) @ r step = solve_linear(A, -g) new_p = p + step new_r = residual(new_p, x, y) if norm(new_r) < norm(r): p = new_p lam *= 0.7 else: lam *= 2.0 if norm(step) < 1e-12: break return p

这里A就是近似的 Hessian,g是目标函数的梯度。每次迭代如果残差下降,就接受步长并减小λ,否则增大λ让更新更保守。

实际经验里有几个点值得留意:

  • 初始λ可以从1e-3或1e-2开始,太小容易一开始就发散,太大则前几步很像梯度下降,收敛慢。
  • 如果数据量级很大,建议先做归一化,否则J^T J的条件数会很难看。
  • 步长范数的停机阈值不要拍脑袋定,最好看你参数的物理量级,参数本身在1e-6量级时,阈值设在1e-12会永远不触发。

4. 工程里的四道坎:从拼写到稀疏结构,都有人栽过

4.1 行和列方向搞反,是最常见的“公式致命伤”

把 Jacobian 拼写成 Jacobin 只是小尴尬,真正致命的是维度方向写反。绝大多数优化器期望的 Jacobian 是“观测数 × 参数数”,也就是每一行对应一个残差,每一列对应一个参数。

我见过很多人的代码里把J返回到参数数乘观测数,之后J^T J的尺寸完全不对。最诡异的是,一部分矩阵库并不会立刻报维度错误,而是算出一个“莫名其妙”的结果,让你绕很久才发现问题。

这里给一个快速检查办法:看J^T J的维度。如果参数数量是m,那么J^T J必须是m × m。算出来的矩阵不是这个尺寸,就说明 Jacobian 的维度反了。另一个更直接的检查是:只让某一个参数变化一点点,看残差向量的变化方向,是不是和 Jacobian 里对应的那一列一致。这个检查不需要任何高级工具,几行代码就能做。

4.2 用有限差分验证解析导数,别盲信手推公式

手推 Jacobian 和 Hessian 很容易出错,尤其是链式法则套来套去的时候。我的习惯是:凡是写进代码里的导数公式,先拿有限差分做一次交叉验证,再放进优化器。

中心差分的公式很简单:

df/dx ≈ (f(x + h) - f(x - h)) / (2h)

更粗糙的单侧差分(f(x+h)-f(x))/h精度差一截,能不用就不用。

h并不是越小越好。受浮点舍入误差限制,取h为sqrt(eps) * max(1, |x|)通常量级在1e-8左右,太大会有截断误差,太小会全是舍入噪声。

验证 Hessian 的时候,我一般先验证梯度,再对梯度做中心差分,得到近似 Hessian 和解析值对比。误差在1e-5这个量级很正常,毕竟是差分近似;但如果误差大到1e-2,基本可以确定公式写错了。

还有一个隐藏问题:有些优化算法内部会用分解算法,要求对称矩阵。哪怕你解析 Hessian 在数学上对称,浮点舍入也可能让它不对称。拿到 Hessian 后顺手做一次(H + H^T) / 2强制对称,能省掉许多莫名其妙的线性代数报错。

4.3 大规模稀疏结构:J 千万别硬生生转成稠密矩阵

大规模优化里,最典型的例子是三维重建中的集束调整问题。每个重投影残差只与“当前相机的位姿”和“被观测的那个三维点”有关,和场景里其他相机、其他点没有关系。于是整个 Jacobian 非常稀疏,每一行只有少数几个非零块。

但如果用最无脑的方式,把完整J存成稠密矩阵,再算J^T J,内存和计算量都会爆炸。正确做法是保留稀疏结构,用稀疏线性代数求解;集束调整还会用分块消元,利用J^T J的箭头形稀疏结构高效求解。

很多新人第一次接触这种感觉是在跑大型非线性优化时,节点一多,机器突然内存爆掉。卡了很久才发现是稠密矩阵的问题。遇到这类问题,第一反应不要是“优化器不够好”,而是检查有没有过度稠密化。

4.4 参数多到 Hessian 放不进内存时怎么办

当模型参数到了百万级,完整 Hessian 根本没法显式构造。这时候常见的选择有三类:

  • L-BFGS:只保存最近几步的梯度差和参数差,隐式构造逼近H^{-1}的矩阵,内存开销是O(n),不是O(n²)。
  • Hessian-vector product:借助自动微分,直接算H v,不需要形成完整H。很多二阶优化算法真正需要的往往只是H v,不是每个元素。
  • 分块近似:把 Hessian 按网络结构分块,每块用低秩或 Kronecker 近似,这类方法是很多神经网络二阶优化器的理论基础。

在这些方法里,你依然需要通过 Jacobian 或梯度去描述一阶信息,再通过一些操作去逼近二阶信息。所以把 Jacobian 和 Hessian 的基本概念吃透,再去看这些高阶工具,思路会清晰很多。

5. 最后聊一点学习路径,和实操心态

每次讲完矩阵,总有人问我“该从哪里开始刷题”。我的建议一直很直接:不要先去背性质,先找一个最小的二维例子,比如本文里那个f1=x²+sin(y), f2=xy+e^y,自己动手写出 Jacobian,再挑一个标量函数写出 Hessian。能用手算清楚两个小例子,后面再大的矩阵也只是维度变大而已。

等你能手算了,就去做一个最简单的指数拟合。这个项目几乎是最合适的练功房:模型不复杂,解析 Jacobian 好写,迭代过程又完整覆盖了“一阶信息→近似二阶信息→步长控制”这条主线。跑通之后,再去挑战机器人运动学或三维重建里的稀疏 Jacobian,会顺畅许多。

最后补一个我自己的经验:写完任何导数代码,先跑五分钟有限差分验证,再丢进优化器。这个习惯帮我省下的调试时间,远超验证本身花掉的时间。遇到优化不收敛,也不要急着怀疑求解器,先看J^T J的条件数、再看 Hessian 的特征值符号,多数问题在这一步就暴露了。想清楚每一步在干什么,比记住一堆矩阵公式重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询