news 2026/4/24 5:50:36

别再死磕梯度下降了!用Python手写BFGS算法,5分钟搞定二次函数优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死磕梯度下降了!用Python手写BFGS算法,5分钟搞定二次函数优化

别再死磕梯度下降!用Python手写BFGS算法,5分钟搞定二次函数优化

优化算法是机器学习和数据科学中的核心工具,而梯度下降可能是大多数人接触到的第一个优化方法。但当你开始处理更复杂的模型或更大规模的数据时,梯度下降的局限性就会显现——收敛速度慢、需要手动调整学习率、对初始值敏感等问题让人头疼。今天,我们就来探索一种更高效的替代方案:BFGS算法。

BFGS(Broyden-Fletcher-Goldfarb-Shanno)算法属于拟牛顿法家族,它通过近似Hessian矩阵的逆来智能调整搜索方向和步长,无需手动设置学习率,收敛速度通常比梯度下降快一个数量级。我们将通过一个简单的二次函数优化问题,直观对比两种方法的差异,并手把手教你用Python实现BFGS算法。

1. 为什么需要BFGS?梯度下降的痛点解析

梯度下降法虽然简单直观,但在实际应用中存在几个明显的缺点:

  • 学习率选择困难:过大导致震荡,过小收敛缓慢
  • 收敛速度慢:特别是当条件数较大时(即函数在不同方向上的曲率差异大)
  • 需要手动调参:不同问题需要不同的学习率和迭代次数

让我们用一个简单的二次函数f(x) = x² + y²来演示梯度下降的问题。下面是梯度下降的实现:

import numpy as np def gradient_descent(f, df, x0, lr=0.1, max_iter=100, tol=1e-6): x = x0.copy() history = [x.copy()] for i in range(max_iter): grad = df(x) if np.linalg.norm(grad) < tol: break x -= lr * grad history.append(x.copy()) return x, np.array(history)

测试这个函数:

def f(x): return x[0]**2 + x[1]**2 def df(x): return np.array([2*x[0], 2*x[1]]) x0 = np.array([1.5, 1.5]) x_gd, hist_gd = gradient_descent(f, df, x0, lr=0.1)

即使对于这样一个简单的凸函数,梯度下降也需要约15次迭代才能收敛到最小值点[0,0]。如果我们把学习率设置得稍大(比如0.2),算法就会在最小值附近震荡;如果设置得太小(比如0.01),则需要上百次迭代。

2. BFGS算法原理揭秘:智能调整搜索方向

BFGS算法的核心思想是通过迭代构建Hessian矩阵逆的近似,从而更智能地确定搜索方向。与梯度下降总是沿着负梯度方向搜索不同,BFGS会根据函数局部曲率信息调整方向。

算法的主要步骤如下:

  1. 初始化:选择初始点x₀和初始Hessian逆近似H₀(通常设为单位矩阵)
  2. 迭代直到收敛:
    • 计算当前梯度∇f(xₖ)
    • 确定搜索方向pₖ = -Hₖ∇f(xₖ)
    • 通过线搜索确定步长αₖ
    • 更新参数xₖ₊₁ = xₖ + αₖpₖ
    • 计算梯度变化yₖ = ∇f(xₖ₊₁) - ∇f(xₖ)和参数变化sₖ = xₖ₊₁ - xₖ
    • 更新Hessian逆近似Hₖ₊₁

关键的Hessian逆更新公式为:

Hₖ₊₁ = (I - ρₖsₖyₖᵀ)Hₖ(I - ρₖyₖsₖᵀ) + ρₖsₖsₖᵀ

其中ρₖ = 1/(yₖᵀsₖ)。这个更新保证了Hₖ₊₁保持正定(对于凸函数),从而保证搜索方向是下降方向。

3. Python实现BFGS:从零开始手写代码

现在让我们实现完整的BFGS算法。我们将分几个关键部分来实现:

3.1 回溯线搜索实现

首先实现一个辅助函数,用于确定合适的步长:

def backtracking(f, df, x, p, alpha=1, rho=0.5, c=1e-4): """回溯线搜索满足Armijo条件的步长""" fx = f(x) grad = df(x) slope = np.dot(grad, p) while f(x + alpha * p) > fx + c * alpha * slope: alpha *= rho if alpha < 1e-10: # 防止步长过小 break return alpha

3.2 核心BFGS算法实现

def bfgs(f, df, x0, max_iter=100, tol=1e-6): x = x0.copy() n = len(x0) H = np.eye(n) # 初始Hessian逆近似 history = [x.copy()] for k in range(max_iter): grad = df(x) if np.linalg.norm(grad) < tol: break # 计算搜索方向 p = -H.dot(grad) # 线搜索确定步长 alpha = backtracking(f, df, x, p) # 更新参数 x_new = x + alpha * p # 计算变化量 s = x_new - x y = df(x_new) - grad # 更新Hessian逆近似 rho = 1.0 / (y.dot(s) + 1e-10) # 防止除以零 I = np.eye(n) H = (I - rho * np.outer(s, y)).dot(H).dot(I - rho * np.outer(y, s)) + rho * np.outer(s, s) x = x_new history.append(x.copy()) return x, np.array(history)

3.3 测试BFGS实现

让我们用同样的二次函数测试BFGS:

x_bfgs, hist_bfgs = bfgs(f, df, x0=np.array([1.5, 1.5])) print(f"BFGS找到的最优解: {x_bfgs}") print(f"迭代次数: {len(hist_bfgs)}")

对于这个简单问题,BFGS通常只需要3-5次迭代就能收敛到机器精度,远少于梯度下降的15次左右。

4. 性能对比:BFGS vs 梯度下降

为了直观展示两种算法的差异,我们来系统性地比较它们的表现:

4.1 迭代次数对比

算法平均迭代次数 (tol=1e-6)收敛标准
梯度下降 (lr=0.1)15‖∇f(x)‖ < 1e-6
BFGS4‖∇f(x)‖ < 1e-6

4.2 收敛路径可视化

我们可以绘制两种算法的优化路径:

import matplotlib.pyplot as plt # 绘制等高线 x = np.linspace(-1.6, 1.6, 100) y = np.linspace(-1.6, 1.6, 100) X, Y = np.meshgrid(x, y) Z = f([X, Y]) plt.figure(figsize=(10, 6)) plt.contour(X, Y, Z, levels=20) plt.plot(hist_gd[:,0], hist_gd[:,1], 'o-', label='梯度下降') plt.plot(hist_bfgs[:,0], hist_bfgs[:,1], 's-', label='BFGS') plt.legend() plt.title('优化路径对比') plt.xlabel('x') plt.ylabel('y') plt.show()

从图中可以明显看出,BFGS的路径更加直接,几乎沿着最速下降方向直达最小值点,而梯度下降则呈现典型的"之字形"路径。

4.3 函数值下降曲线

plt.figure(figsize=(10, 6)) plt.semilogy([f(x) for x in hist_gd], label='梯度下降') plt.semilogy([f(x) for x in hist_bfgs], label='BFGS') plt.xlabel('迭代次数') plt.ylabel('函数值 (log scale)') plt.title('函数值下降曲线') plt.legend() plt.grid(True) plt.show()

在半对数坐标下,BFGS显示出超线性收敛的特性,而梯度下降只是线性收敛。

5. 进阶话题:BFGS的实际应用技巧

虽然我们的例子使用了简单的二次函数,但BFGS的真正价值在于处理更复杂的非线性优化问题。以下是一些实际应用中的技巧:

5.1 处理非凸函数

对于非凸函数,标准的BFGS实现可能会遇到以下问题:

  • Hessian逆近似可能失去正定性
  • 可能收敛到鞍点或局部极小值

解决方案:

# 在BFGS实现中添加保护措施 if y.dot(s) <= 1e-10: # 曲率条件不满足 H = np.eye(n) # 重置Hessian逆近似 continue

5.2 内存受限的L-BFGS

当参数维度很高时,存储完整的Hessian逆近似矩阵可能不现实。这时可以使用L-BFGS(Limited-memory BFGS),它只保存最近的m个{s,y}对来近似Hessian逆。

5.3 结合自动微分

对于复杂的函数,手动计算梯度容易出错。可以结合自动微分工具如JAX或PyTorch:

import jax.numpy as jnp from jax import grad def f(x): return jnp.sum(x**2) + jnp.prod(x) df = grad(f) # 自动计算梯度 # 然后可以直接使用我们的BFGS实现 x_opt, _ = bfgs(f, df, x0=jnp.array([1.0, 1.0]))

6. 常见问题与调试技巧

在实际使用BFGS时,可能会遇到以下问题:

6.1 算法不收敛的可能原因

  1. 梯度计算错误:这是最常见的问题。可以通过有限差分法验证梯度:

    def check_gradient(f, df, x, eps=1e-5): grad_analytic = df(x) grad_numeric = np.zeros_like(x) for i in range(len(x)): x_plus = x.copy() x_plus[i] += eps x_minus = x.copy() x_minus[i] -= eps grad_numeric[i] = (f(x_plus) - f(x_minus)) / (2*eps) return grad_analytic, grad_numeric
  2. 初始Hessian逆近似不合适:对于不同尺度的问题,可以尝试调整初始Hessian逆:

    H = np.eye(n) * scale_factor
  3. 线搜索不精确:可以尝试调整回溯线搜索的参数:

    alpha = backtracking(f, df, x, p, alpha=1, rho=0.9, c=0.1)

6.2 性能优化技巧

  • 向量化计算:确保所有操作都使用NumPy的向量化操作
  • 避免不必要的计算:缓存重复使用的值
  • 预热启动:对于类似问题,可以使用前一次的Hessian逆近似作为初始值

7. 超越二次函数:BFGS在机器学习中的应用

虽然我们使用二次函数作为示例,但BFGS的真正威力在于处理更复杂的机器学习模型优化问题。以下是一些典型应用场景:

7.1 逻辑回归

def logistic_loss(w, X, y): z = X.dot(w) return np.mean(np.log1p(np.exp(-y * z))) def logistic_grad(w, X, y): z = X.dot(w) s = 1 / (1 + np.exp(y * z)) return -X.T.dot(y * s) / len(y) # 使用BFGS优化 w0 = np.zeros(X.shape[1]) w_opt, _ = bfgs(lambda w: logistic_loss(w, X, y), lambda w: logistic_grad(w, X, y), w0)

7.2 神经网络参数优化

虽然深度学习通常使用随机梯度下降及其变种,但对于小型网络或全批量训练,BFGS也是一个不错的选择:

def neural_net_loss(params, X, y): # 前向传播计算损失 ... return loss def neural_net_grad(params, X, y): # 反向传播计算梯度 ... return grad # 展平参数并优化 params_flat, unflatten = flatten_params(initial_params) params_opt_flat, _ = bfgs(lambda p: neural_net_loss(unflatten(p), X, y), lambda p: flatten_grad(neural_net_grad(unflatten(p), X, y)), params_flat)

7.3 超参数优化

BFGS也可以用于优化模型的超参数,虽然这通常需要计算二阶导数或使用基于梯度的超参数优化方法。

8. 算法变种与扩展阅读

BFGS算法有多种变体和改进,值得进一步探索:

  1. L-BFGS:内存受限版本,适合高维问题
  2. BFGS-B:支持边界约束的版本
  3. DFP:另一种拟牛顿法,与BFGS类似但更新公式不同
  4. 自适应BFGS:自动调整参数的版本

对于想深入了解的读者,推荐以下资源:

  • Nocedal & Wright的《Numerical Optimization》
  • Boyd & Vandenberghe的《Convex Optimization》
  • SciPy的scipy.optimize.minimize实现,其中包含BFGS和L-BFGS选项
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/24 5:46:32

卡内基梅隆大学:人形机器人实现类人触觉抓握力道感知能力提升

这项由卡内基梅隆大学联合德克萨斯大学阿灵顿分校、博世人工智能中心共同完成的研究&#xff0c;以预印本形式发布于2026年4月&#xff0c;论文编号为arXiv:2604.13015&#xff0c;有兴趣深入了解的读者可通过该编号查询完整原文。人类在叠毛巾、往书架上放书、端着两杯热茶走路…

作者头像 李华
网站建设 2026/4/24 5:44:17

Phi-3-mini-4k-instruct-gguf实战案例:用Chainlit构建个人AI知识助理

Phi-3-mini-4k-instruct-gguf实战案例&#xff1a;用Chainlit构建个人AI知识助理 1. 项目概述 Phi-3-Mini-4K-Instruct是一个38亿参数的轻量级开源模型&#xff0c;采用GGUF格式提供。这个模型在参数少于130亿的模型中表现出色&#xff0c;特别擅长常识推理、语言理解、数学计…

作者头像 李华
网站建设 2026/4/24 5:33:17

LaTeX数学公式字体控制:从斜体到正体的实用指南

1. 为什么需要控制数学公式的字体样式&#xff1f; 在科研论文写作中&#xff0c;数学公式的字体样式不仅仅是美观问题&#xff0c;更是学术规范的一部分。默认情况下&#xff0c;LaTeX的数学环境会将字母自动转换为斜体&#xff0c;这是数学排版领域的传统惯例——斜体用于区分…

作者头像 李华
网站建设 2026/4/24 5:30:05

R语言实战:oncoPredict包在肿瘤药敏预测与生物标志物挖掘中的应用

1. oncoPredict包简介与安装指南 oncoPredict是R语言中一个专门用于肿瘤药物敏感性预测和生物标志物挖掘的工具包。作为pRRophetic包的升级版本&#xff0c;它不仅继承了前者的核心算法优势&#xff0c;还整合了更全面的训练数据集和新增功能模块。这个包特别适合肿瘤研究人员分…

作者头像 李华