☰
北大凸优化编程作业全解析:从自动微分到ADMM的代码链
2026/10/3 20:48:15 网站建设 项目流程

简介:北京大学信息科学技术学院智能科学与技术系“凸分析与优化方法”课程编程作业集合,面向正在学习凸优化、自动微分和神经网络训练的学生,用于实践并比较多种经典优化算法。资源共63个文件,以23个Python脚本和32张结果图为主,另有3个JSON配置、1个TXT、1个DOCX及Markdown说明等,压缩包整体仅1.03MB,结构按HW模块清晰组织。已有92人学习浏览,适合课程复习、算法对照实验或项目参考。作业覆盖自动微分与梯度计算、回溯线搜索、最陡下降、阻尼牛顿法、高斯牛顿法、共轭梯度法、DFP/BFGS及L-BFGS拟牛顿法,并包含ADMM、随机优化方法、块坐标下降、惩罚函数与增广拉格朗日等进阶实现;每个作业配有可运行的Python代码、收敛过程可视化图像以及说明文档,帮助使用者直观理解不同优化器的收敛行为和适用场景。附赠说明文件与资源文档还补充了作业背景和实现要点,能够支持读者在掌握理论的同时快速上手调试与复现。

1. 这包作业的真实价值:把凸优化课程从公式变成可运行的代码链

拿到这份北大的《凸分析与优化方法》编程作业集合时,我第一反应不是「又是一堆交差代码」,而是「这条代码链太完整了」。从自动微分的计算图实现,到梯度下降、最陡下降、阻尼牛顿法、高斯牛顿法、共轭梯度法、DFP 拟牛顿、BFGS、L-BFGS,再到罚函数法、块坐标下降和 ADMM 家族,十二次编程作业几乎把凸优化主线算法全走了一遍,而且每个作业都带收敛曲线图和可运行的 Python 源码。对正在学凸优化、数值优化或机器学习但苦于「公式看得懂、代码写不出」的从业者来说,这是一份可以直接对照教材、逐行复现实验的资源,不是笔记,不是截图,是能跑出图的代码。

2. 先盘清作业地图:HW3 到 HW21 的算法谱系与两条阅读主线

2.1 作业编号与算法对照:知道每个文件在干什么

这份资源最容易被忽视的一点,是它内部有一套清晰的作业编号体系。HW3 是自动微分与神经网络参数优化,HW11 是梯度下降,HW12 是最陡下降和阻尼牛顿法,HW13 是共轭梯度法和 DFP,HW14 是 BFGS、L-BFGS 和 majorization-minimization,HW17 是算法对比实验,HW18 是罚函数法和 Frank-Wolfe,HW19 是块坐标下降(逻辑回归和字典学习)以及 LADMAP、LADMPSAP,HW20 是随机梯度下降,HW21 是加速梯度下降和增广罚函数。这个顺序本身就在暗示一门课的教学节奏:先解决「怎么求梯度」,再解决「怎么用梯度走直线」,再升级到「用二阶信息走曲线」,最后进入约束优化和分布式场景。

作业编号核心算法对应文件
HW3自动微分、梯度计算、神经网络参数优化autoDiff.py, grad4Net.py, grad4NetTest.py
HW11梯度下降、回溯线搜索gradDescent.py
HW12最陡下降、阻尼牛顿法steepDescent.py, Newton.py
HW13共轭梯度法、DFP 拟牛顿conjGrad.py, DFP.py
HW14BFGS、L-BFGS、majorization-minimizationBFGS.py, L-BFGS.py, majMin.py
HW17多种算法收敛对比compare.py
HW18罚函数法、Frank-Wolfe 算法penalty.py, FW.py
HW19块坐标下降、LADMAP、LADMPSAPBCD4LR.py, BCD4Dict.py, LADMAP.py, LADMPSAP.py
HW20随机梯度下降Stochastic.py
HW21加速梯度下降、增广罚函数AGD.py, APenalty.py

对照这个表去翻文件,你很快会发现:HW12 的steepDescent.py和 HW11 的gradDescent.py长得非常像,但前者固定步长、后者带回溯线搜索;HW13 的DFP.py和 HW14 的BFGS.py是同一套拟牛顿框架换了两组更新公式。这些「同框架不同算法」的对比,恰恰是这门课最有价值的地方,因为它让你看到算法之间的差异不是玄学,而是几行公式的区别。

2.2 文件组织与运行环境:先解决能不能跑的问题

资源的根目录下有几个容易被忽略的辅助文件:说明文件.txt、附赠资源.docx、README.md、.gitignore以及.vscode/settings.json。按我处理开源作业包的习惯,先看README.md和附赠资源.docx,前者通常是作业要求和代码结构说明,后者往往夹带了课程讲义、实验指引,甚至是老师给的伪代码。.vscode/settings.json里配置的是 Python 解释器路径和 lint 规则,说明这些作业原始开发环境就是 VS Code,你用 PyCharm 也没问题,只要保证 Python 版本能跑 numpy 和 matplotlib。

运行环境比我预想的简单:Python 3.8 以上就够,依赖只有 numpy 和 matplotlib。所有作业都是单文件脚本,没有复杂的包结构,直接python HW11/gradDescent.py就能跑出对应的converge.png收敛曲线。如果你想一次性验证全部作业,按目录逐个执行即可。唯一要注意的是 HW19 和 HW21 里的 ADMM 变体会稍微慢一些,建议先跑小规模数据确认逻辑无误,再放开迭代轮数。

2.3 两条阅读主线:从哪个作业开始读最省力

我建议你按「一阶方法 → 二阶方法 → 约束优化」的顺序读,而不是按作业编号顺序。第一条主线是一阶优化:HW3(自动微分)→ HW11(梯度下降)→ HW20(随机梯度下降)→ HW21(加速梯度下降)。这条线解决的是「梯度从哪来、往哪走、走多远」的问题,逻辑最连贯,入门成本最低。第二条主线是二阶与拟牛顿:HW12(阻尼牛顿)→ HW13(共轭梯度、DFP)→ HW14(BFGS、L-BFGS)→ HW17(对比实验)。这条线解决的是「如何用曲率信息加速收敛」,需要你熟悉 Hessian 矩阵和线搜索的概念后再上。

我的实际建议是:第一遍按主线一跑通 HW3 和 HW11,第二遍再进入主线二,因为 HW3 的自动微分是整个资源的技术地基,后面所有作业里的梯度都靠它或者 numpy 手写梯度来提供。如果你一上来就啃 L-BFGS,大概率会被 two-loop recursion 绕晕,反而失了信心。

3. 自动微分与一阶优化:HW3/HW11 复现路径与线搜索参数

3.1 自动微分骨架:从计算图到反向传播

HW3 的autoDiff.py是这包资源的第一个技术亮点。它实现了类似 PyTorch 早期版本的计算图自动微分机制,核心是三个组件:变量节点(Var)、算子节点(Add、Mul 等)和反向传播入口。变量节点不仅要保存数值,还要保存梯度,并且用一个_grad_fn指向反向传播时如何把上游梯度传给下游节点。下面的代码是这类作业最常见的骨架写法:

class Var: """计算图节点:保存数值、梯度和反向传播闭包""" def __init__(self, value, grad_fn=None, children=()): self.value = float(value) self.grad = 0.0 self._grad_fn = grad_fn # 接收上游梯度,传给子节点 def backward(self, g=1.0): self.grad += g if self._grad_fn: self._grad_fn(g) def __add__(self, other): other = other if isinstance(other, Var) else Var(other) left, right = self, other def _grad_fn(g): left.backward(g) right.backward(g) return Var(left.value + right.value, _grad_fn, (left, right)) def __mul__(self, other): other = other if isinstance(other, Var) else Var(other) left, right = self, other def _grad_fn(g): left.backward(g * right.value) # d(left*right)/d(left) = right right.backward(g * left.value) # d(left*right)/d(right) = left return Var(left.value * right.value, _grad_fn, (left, right))

逻辑说明:backward(g)接收上游梯度g,先累加到当前节点的grad上,再通过_grad_fn把梯度传给子节点。加法的梯度直接透传,乘法的梯度则按乘法法则乘上另一节点的数值。这段代码虽然简略,但你已经能看到自动微分的本质:它不是在求导,而是在记录计算过程,然后沿着计算图反向传播局部梯度。

参数说明:grad初始为 0.0,每次backward累加而不是赋值,这对应的是批量梯度计算——多个损失分量同时对同一个参数求梯度时,梯度必须求和。_grad_fn是闭包,捕获了算子左右两端的节点引用,所以即使计算图被保存到变量out里,反向传播时依然能沿着闭包链走回去。这也是为什么作业里强调不要 break 计算图——一旦你用 numpy 原生函数打断 Var 节点的连接,梯度链就断了。

3.2 回溯线搜索:梯度下降里最容易翻车的环节

HW11 的gradDescent.py表面上是梯度下降,实际考察点是回溯线搜索(backtracking line search)。很多初学者直接固定步长如 0.01,结果要么收敛慢得像蜗牛,要么干脆震荡。回溯线搜索的思路是:先给一个较大的初始步长,然后按比例缩小,直到满足 Armijo 充分下降条件。作业里通常会有两个关键参数:c(下降量阈值系数)和rho(步长缩减因子)。

import numpy as np def backtracking_line_search(f, grad, x, d, c=1e-4, rho=0.8, max_iter=50): """ 回溯线搜索:沿方向 d 找满足 Armijo 条件的步长 t f: 目标函数, grad: 梯度函数 x: 当前点, d: 搜索方向 c: Armijo 条件系数, rho: 步长缩减因子 """ t = 1.0 g = grad(x) fx = f(x) descent = g.dot(d) # 方向导数,应小于 0 for _ in range(max_iter): if f(x + t * d) <= fx + c * t * descent: break t *= rho return t

逻辑说明:descent是当前梯度与搜索方向的内积,对于梯度下降d = -g,它恒为负值。Armijo 条件要求新点的函数值不高于fx + c * t * descent,这保证步长不会大到把函数值抬升。如果条件不满足,就t *= rho把步长缩小 20%,继续尝试。循环上限max_iter是兜底,防止死循环。

参数说明:c通常取 1e-4,这是极小值,基本只要求函数值有下降趋势;rho取 0.8 左右,太小会让步长缩得过快,一次迭代要多算好几轮函数值。我见过不少人把rho设成 0.5,结果每一步都要回退七八次才找到合适步长,总迭代次数没变但函数求值次数翻倍。HW11 的converge.png如果出现「锯齿状」下降,先怀疑步长策略,再怀疑梯度算错。

3.3 从梯度下降到最陡下降:同一条路、不同步长策略

HW11 和 HW12 放在一起看很有意思。gradDescent.py用的是回溯线搜索算步长,而steepDescent.py更贴近「最陡下降」的原始定义:每一步都沿着当前梯度的反方向走,但步长直接取固定值,或者用精确线搜索求解min_t f(x - t * g)。在作业实现里,后者往往被简化成一次二次函数拟合,因为精确线搜索的解析解只在特定函数上存在。

实操中的差别在收敛速度上非常明显:最陡下降在椭圆型二次函数上会走「之」字形,因为相邻两步方向正交;带回溯线搜索的梯度下降则因为步长比较保守,反而路径更平滑。HW12 里配的converge.png和converge_damped.png就是用来对比这两种行为的——如果你跑出来的图里,最陡下降的损失下降速度明显慢于阻尼牛顿,说明你的梯度方向计算正确,而问题本身的条件数比较大,需要二阶方法出场。

3.4 一个顺手实验:硬间隔 SVM 的梯度下降

读 HW11 的时候我顺手做了一个小验证:把目标函数换成了硬间隔 SVM 的合页损失加正则项,用同一套回溯线搜索跑梯度下降。做法是把f(x)改成sum(max(0, 1 - y * (w @ x))) + 0.5 * lambda * w @ w,梯度里多一项正则化导数。这个改动只用十分钟,但能让你立刻感受到梯度下降算法本身的通用性——它不关心你的目标函数来自什么模型,只要提供梯度和函数值就能跑。热搜里提到的「svm 的梯度下降」「硬间隔 svm 的梯度下降」本质就是这个操作。想验证的话,直接在gradDescent.py里替换目标函数,收敛曲线一样能画出来。

4. 二阶与近似二阶方法:阻尼牛顿、共轭梯度、DFP/BFGS 的代码骨架

4.1 阻尼牛顿法:修正 Hessian 的不正定

HW12 的Newton.py实现了阻尼牛顿法,它的经典迭代格式是:

d_k = -(H_k + lambda_k * I)^(-1) * g_k x_{k+1} = x_k + t_k * d_k

其中lambda_k就是阻尼项,t_k是线搜索步长。H_k是 Hessian 矩阵,g_k是梯度。为什么要加lambda_k * I?因为牛顿法的原版假设 Hessian 正定,但很多非凸函数在某一点的 Hessian 并不正定,直接求逆会得到一个上升方向,导致发散。加上lambda_k * I后,只要lambda_k足够大,矩阵就强制变成正定。

作业代码里一般会这样组织:

def damped_newton_step(f, gfun, hfun, x, lam=1e-3): g = gfun(x) H = hfun(x) n = H.shape[0] # 强制对称正定:加阻尼项后尝试 Cholesky,失败则加大阻尼 A = H + lam * np.eye(n) try: d = -np.linalg.solve(A, g) except np.linalg.LinAlgError: lam *= 10.0 A = H + lam * np.eye(n) d = -np.linalg.solve(A, g) return d, lam

逻辑说明:先用np.linalg.solve解线性方程组而不是直接求逆矩阵,数值上更稳定。Cholesky 分解失败说明矩阵仍不正定,就把lam放大十倍重新来。这样既保证了搜索方向是下降方向,又避免了作业里「牛顿法发散」的常见翻车。

参数说明:lam的初始值选取很敏感。1e-3 适合目标函数尺度在 1 附近的场景;如果你的损失函数动辄上千,lam初始值可以给到 0.1 甚至 1。HW12 里converge_damped.png与converge.png的对比,目的就是让你看到阻尼项对稳定性的影响——不加阻尼的牛顿法在 Rosenbrock 函数上几步就会冲出边界。

4.2 共轭梯度法:如何避开矩阵求逆

HW13 的conjGrad.py实现的是线性共轭梯度法,用来解大型对称正定线性方程组。它最大的价值是不需要显式存储 Hessian,只需要 Hessian 和向量的乘积H @ v。在代码里你会看到经典的 Fletcher-Reeves 版本:

def conjugate_gradient(Afun, b, x0, tol=1e-8, max_iter=200): x = x0.copy() r = b - Afun(x) # 残差 p = r.copy() rho = r.dot(r) for i in range(max_iter): Ap = Afun(p) alpha = rho / p.dot(Ap) x = x + alpha * p r_new = r - alpha * Ap rho_new = r_new.dot(r_new) if np.sqrt(rho_new) < tol: break beta = rho_new / rho # Fletcher-Reeves 系数 p = r_new + beta * p r, rho = r_new, rho_new return x

逻辑说明:Afun是一个返回矩阵向量乘积的函数,这是共轭梯度法的灵魂。算法维护残差r和搜索方向p,alpha是沿当前方向的精确步长,beta是新残差与旧残差模长比的平方,用来保证新搜索方向与之前所有方向共轭。整个过程只需要 O(n) 存储,这是它在大规模稀疏问题里受欢迎的原因。

参数说明:tol=1e-8控制停机精度,作业里如果收敛曲线尾部不下降,可以放宽到 1e-6 观察趋势。max_iter理论上不超过变量维度 n,超过这个数还没收敛基本就是代码写错了。你可以在 HW13 里用Afun = lambda v: H @ v传入 Hessian 矩阵,就能把它当非线性优化的子问题求解器用。

4.3 DFP 与 BFGS 的更新公式与代码要点

HW13 的DFP.py与 HW14 的BFGS.py是同一个拟牛顿框架的两组更新公式。它们做的事完全一样:用梯度差值y = g_{k+1} - g_k和位移s = x_{k+1} - x_k去逼近 Hessian 逆矩阵,省去二阶导计算。BFGS 的更新公式是:

rho_k = 1 / (y^T s) H_{k+1} = (I - rho_k * s * y^T) * H_k * (I - rho_k * y * s^T) + rho_k * s * s^T

DFP 则是把 s 和 y 的角色对调,更新的是 Hessian 本身而不是逆矩阵。作业里的代码一般会先用 BFGS 算出搜索方向,再搭配线搜索,最后更新矩阵:

def bfgs_update(H, s, y): """BFGS 更新拟牛顿矩阵 H""" ys = y.dot(s) if ys < 1e-12: return H # 防止除零 rho = 1.0 / ys I = np.eye(H.shape[0]) A = I - rho * np.outer(s, y) B = I - rho * np.outer(y, s) return A @ H @ B + rho * np.outer(s, s)

逻辑说明:ys = y.dot(s)必须大于 0,这对应目标函数的凸性条件。如果ys接近 0,说明步长太小或梯度噪声太大,更新矩阵会退化,此时直接返回原矩阵更安全。np.outer构造的是外积矩阵,BFGS 要的就是这种秩一更新的叠加效果。

参数说明:BFGS 的收敛速度比梯度下降高一个量级,但内存占用是 O(n^2)。如果你的参数维度超过几千,BFGS 会直接吃掉几百兆内存,这时就得换 L-BFGS。

4.4 L-BFGS 与高斯牛顿:内存受限时的两个走向

HW14 的L-BFGS.py是 BFGS 的低内存变体,它不显式存储矩阵,而是保存最近 m 组(s, y)对,用 two-loop recursion 隐式计算搜索方向。参数 m 一般取 10 到 20,代表保留多少步历史信息。实现核心是两层循环:第一层从最新到最旧计算 rho 和 alpha,第二层从最旧到最新累加方向。这段代码不难,但非常容易在索引上犯错,我的调试经验是把s_list和y_list打印出来逐行对。

高斯牛顿法则出现在非线性最小二乘场景,比如 HW17 的compare.py里做曲线拟合时。它把目标函数写成残差平方和,用雅可比矩阵近似 Hessian:H ≈ J^T J,梯度是J^T r,这样迭代步长公式变成delta = -(J^T J + lambda I)^{-1} J^T r。注意这里的J^T J天然半正定,加了lambda I后就是完整的阻尼高斯牛顿——这也是 Levenberg-Marquardt 算法的雏形。作业里如果要求你对一个非线性模型做参数拟合,优先用高斯牛顿而不是 BFGS,因为残差结构能被充分利用,收敛更快。

5. 罚函数、块坐标下降与 ADMM:约束优化作业的避坑记录

5.1 罚函数法与 Frank-Wolfe:怎么处理约束

HW18 的penalty.py实现的是外罚函数法:把约束c(x) <= 0变成惩罚项加到目标函数上,然后增大惩罚系数mu迭代求解无约束问题。这个思路直白,但有个经典陷阱:mu太小约束不满足,mu太大目标函数变得病态,收敛极慢。作业代码里一般会写成:

def penalty_method(f, con, x0, mu0=1.0, max_iter=20): x = x0.copy() mu = mu0 for k in range(max_iter): # 定义增广目标函数 def f_pen(x): return f(x) + mu * max(0, con(x)) ** 2 # 用无约束优化方法(如 BFGS)求解 f_pen 的极小点 x = solve_unconstrained(f_pen, x) mu *= 5.0 # 增大惩罚系数 return x

逻辑说明:mu每次放大 5 到 10 倍,让惩罚项逐渐主导目标函数,最终把解逼近可行域边界。solve_unconstrained可以用前面任意一个无约束方法替代,HW18 里配的是FW.py,即 Frank-Wolfe 算法——它把约束优化问题分解成一系列线性规划子问题,适合约束集是单纯形或球集的场景。

参数说明:mu0的初值决定了第一轮迭代对约束的重视程度。初值太小会导致刚开头几步完全无视约束,后面要花大量代价拉回来。我的习惯是先用无约束解算一次,看约束违反量级,再定mu0。

5.2 块坐标下降:BCD4LR 与 BCD4Dict 的分块逻辑

HW19 的BCD4LR.py和BCD4Dict.py是块坐标下降(Block Coordinate Descent)的两个应用实例。BCD4LR.py针对逻辑回归:把权重向量按维度分块,每一轮只优化一个维度,固定其他维度;BCD4Dict.py针对字典学习:把变量分成字典矩阵和系数矩阵两组,交替更新一组、固定另一组。核心代码骨架是:

def bcd_lr(X, y, w0, lr=0.1, block_size=1, max_epoch=100): """ 块坐标下降训练逻辑回归 X: (n_samples, n_features), y: 标签 block_size: 每轮更新的特征块大小 """ w = w0.copy() n_features = X.shape[1] for epoch in range(max_epoch): for start in range(0, n_features, block_size): idx = slice(start, start + block_size) # 只对当前块的权重计算梯度 grad_block = compute_grad(X[:, idx], y, w[idx]) w[idx] -= lr * grad_block return w

逻辑说明:块坐标下降的核心是「分而治之」。当block_size=1时它就是坐标下降,每一轮只更新一个坐标,计算开销小但迭代次数多;当block_size=n_features时退化成全批量梯度下降。作业里要求你观察不同块大小对收敛速度的影响,这比直接调学习率更直观。

参数说明:block_size的选择是内存与速度的权衡。字典学习的问题里,系数矩阵维度可能上千,但每个样本的稀疏编码子问题独立,可以并行处理,这才是块坐标下降在稀疏场景胜出的原因。

5.3 LADMAP / LADMPSAP:ADMM 的调参与收敛

HW19 里的LADMAP.py、LADMPSAP.py以及 HW21 的pLADMPSAP.py是 ADMM(交替乘子法)的改进版本。ADMM 的标准迭代格式是:交替更新原始变量x、辅助变量z和对偶变量u,其中最关键的是惩罚参数rho。固定rho的 ADMM 在简单问题上够用,但遇到病态问题收敛慢得让人怀疑人生,所以 LADMPSAP 加了自适应调整:

def admm_step(A, b, x, z, u, rho): # 更新 x:求解近端梯度子问题 x = prox_f(x - (A.T @ (A @ x - b) + u) / rho, rho) # 更新 z:求解约束投影子问题 z_new = project_g(x + u / rho) # 更新对偶变量 u(尺度化形式) u = u + rho * (x - z_new) return x, z_new, u

逻辑说明:ADMM 把大问题拆成分别以x和z为变量的两个近端算子(prox)计算,u是对偶变量,记录原始变量与辅助变量之间的不一致性。prox_f是近端算子,在作业里通常是对l2范数或l1范数的软阈值函数;project_g是对可行域的投影,比如非负约束就是max(0, ...)。

参数说明:rho的更新策略是每迭代若干轮比较原始残差和对偶残差的大小,哪个大就调整哪个方向的权重。pLADMPSAP.py里加了并行前缀,说明它支持多个子问题并行求解,这是大数据场景下的标配。HW19 配了三张收敛图19_1_adm_fi.png、19_1_grad_fi.png、19_3_fi.png,分别对应 ADMM、梯度法和第三个变体——如果你跑出来的 ADMM 收敛速度还不如梯度法,先检查rho是否被固定在了极不合理的值上。

5.4 四个高频雷区:现象、原因与解决

我跑完这一整套作业,踩过的坑基本都能归成下面四类,都是在HW12、HW14、HW19里反复出现的。

雷区一:阻尼牛顿法损失曲线发散。现象是前几步损失下降正常,到第十步左右突然NaN。原因是步长过大,Hessian 在远离极小点的地方接近奇异,阻尼项lambda初始值太小。解决方法是把lambda从 1e-3 调到 0.1,并在每次迭代检查更新向量的范数,超过阈值就退回上一步缩小步长。从那以后我每次跑牛顿法都强制加一层「方向范数守卫」。

雷区二:BFGS 在参数维度 5000 时内存直接打满。现象是程序运行到一半系统开始疯狂 swap。原因很简单,BFGS 要维护一个 n×n 的矩阵,5000 维就是 200MB 浮点存储。解决方法是换L-BFGS.py,设置m=10只保留 10 组历史增量,内存占用降到几 MB,收敛速度下降不到 20%。

雷区三:ADMM 的收敛曲线有一个长长的平台期。现象是对偶残差下降很慢,主残差却已经很小。原因是rho固定不变,早期压力值不适合后期。解决方法是每 20 轮按残差比值更新rho,比值大于 10 就放大,小于 0.1 就缩小。HW19 的LADMPSAP.py里应该有这个逻辑,跑不通时多看一眼rho的打印值。

雷区四:共轭梯度法迭代超过 n 次还不收敛。现象是max_iter跑满但残差停在 1e-4 附近。原因是目标矩阵的条件数太大,或者Afun里用了浮点误差较大的实现。解决方法是先做一步对角预处理,把A缩放成对角线为 1 的矩阵,再跑共轭梯度。这个预处理能直接把迭代次数从几千降到几十。

6. 跑通之后:梯度验证与线搜索参数的习惯

6.1 梯度检查:用数值梯度给解析梯度兜底

整个资源里我最后悔没早点做的事,是在改代码前先跑一遍梯度检查。HW3 里带了grad4NetTest.py,它做的事情就是对比自动微分求出的梯度与数值梯度,判断差异是否在容差范围内。通用做法是用中心差分公式:

def numerical_grad(f, x, eps=1e-6): """中心差分数值梯度:O(eps^2) 精度""" g = np.zeros_like(x) for i in range(x.size): x_plus = x.copy() x_minus = x.copy() x_plus.ravel()[i] += eps x_minus.ravel()[i] -= eps g.ravel()[i] = (f(x_plus) - f(x_minus)) / (2 * eps) return g

逻辑说明:中心差分同时取正负两个扰动点,误差比单侧差分小一个量级。eps取 1e-6 到 1e-7 是经验区间,太大梯度被截断误差污染,太小被浮点精度污染。检查时比较解析梯度与数值梯度的相对误差,小于 1e-5 就说明实现正确。我对 HW11 到 HW21 的每个目标函数都跑过一次这个检查,凡是收敛曲线异常的,最后查出来九成是梯度符号或维度索引写反。

参数说明:这个函数在参数维度高时很慢,但它是「后悔药」,只看小规模随机数据就能拦住大部分低级错误。

6.2 收敛图解读与线搜索参数复用习惯

作业里那些converge.png不是摆设。读它们时我看三个信息:纵轴损失曲线的下降斜率、是否出现平台期、末端是平稳还是锯齿。下降斜率对应线搜索参数rho和c的选择——斜率太陡但曲线震荡,说明步长大需要调小rho;斜率平缓说明步长保守,可以加大初始步长。平台期通常意味着算法进入了 Hessian 病态区域,该换二阶方法。末端锯齿往往是固定步长导致的。

我的个人习惯是维护一套「线搜索参数基线」:目标函数尺度未知时,先用c=1e-4, rho=0.8, t0=1.0跑一遍,观察前三次迭代的函数值;如果第一步就不满足 Armijo 条件且连续回退超过五次,把t0改为 0.5;如果曲线震荡,把rho降到 0.5。这套基线在我处理作业和后续自用项目里一直有效,比每次从头调参数省太多时间。跑完这份作业集合后我再写优化代码,都会先写梯度检查函数再写优化器本身,先验证梯度再调参,最后画收敛曲线,这套顺序已经变成雷打不动的习惯,希望也能帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询