4. 梯度下降法基础:梯度概念、学习率、批量梯度下降与随机梯度下降
好,咱们进入优化算法的核心环节。前面聊了那么多曲面参数化、能量函数,最终目的都是为了找到一个「最优解」。怎么找?最经典、最基础的方法就是梯度下降法。
说白了,梯度下降就像你站在一座山的某个位置,想要摸黑走到山脚。你没法一眼看到全局,只能靠脚下的坡度来判断往哪个方向走是下坡。每一步都沿着最陡的下坡方向迈出,最终就能到达谷底。这个「最陡方向」就是梯度,而「迈出的步子大小」就是学习率。
4.1 梯度到底是什么?
从数学上讲,梯度是一个向量,指向函数值增长最快的方向。那对我们来说,要找最小值,就得往梯度的反方向走。
对于一个多元函数 f(x₁, x₂, ..., xₙ),它的梯度就是每个变量的偏导数组成的向量:
∇f = [∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂xₙ]ᵀ
我在做自由曲面光顺优化时,经常把曲面能量函数(比如弯曲能、拉伸能)的梯度算出来。嗯,这里要注意:梯度只告诉你方向,不告诉你该走多远。走多远,那是学习率的事。
核心理解:梯度是函数在当前位置的「局部最陡上升方向」。我们做优化,就是沿着梯度的反方向迭代更新参数。
4.2 学习率:步子大了容易扯着蛋
学习率(Learning Rate)是梯度下降里最敏感的超参数。它决定了你每一步迈多大。
- 学习率太大:可能直接跳过最优点,在谷底附近来回震荡,甚至发散。我曾经调一个曲面拟合的参数,学习率设了0.1,结果损失函数直接飞到了NaN,整个模型崩了。
- 学习率太小:收敛极慢,像蜗牛爬。你可能迭代了几千步,参数还没挪到地方。
你想想看,这就像调焦距——拧多了模糊,拧少了还是模糊。怎么选?我个人习惯先用一个对数尺度试:0.1、0.01、0.001、0.0001,看损失曲线的下降趋势再微调。
| 学习率 | 收敛速度 | 稳定性 | 典型场景 |
|---|---|---|---|
| 过大(>0.1) | 快但易发散 | 差 | 几乎不用,除非你确定曲面很平滑 |
| 适中(0.01~0.001) | 较快 | 较好 | 大多数自由曲面优化任务的起点 |
| 较小(0.0001~1e-5) | 慢 | 好 | 精细调优、接近最优解时使用 |
我的经验:在自由曲面参数调试中,我通常先用0.001跑100步,观察损失曲线。如果下降太慢,就跳到0.01;如果震荡,就降到0.0001。别一上来就追求完美,先跑起来再说。
4.3 批量梯度下降(BGD)
批量梯度下降,每次更新参数时,都用全部样本计算梯度。说白了,就是你看清了整座山的地形,再决定往哪走。
更新公式:
θ = θ - η · (1/m) · Σᵢ₌₁ᵐ ∇Jᵢ(θ)
其中m是样本总数,η是学习率,J是损失函数。
优点:梯度方向准确,收敛稳定。
缺点:计算量大。如果你的曲面有上万个控制点,每次迭代都要算一遍所有点的梯度,那速度……我当年做汽车A面优化时,一个BGD迭代要跑3分钟,等得我怀疑人生。
4.4 随机梯度下降(SGD)
SGD就激进多了——每次只随机选一个样本算梯度。相当于你蒙上眼睛,只靠脚下那一点感觉就迈步。
更新公式:
θ = θ - η · ∇Jᵢ(θ) // 随机选一个i
优点:快,非常快。迭代一次的成本只有BGD的1/m。
缺点:梯度噪声大,收敛路径像喝醉了酒,摇摇晃晃。但有意思的是,这种噪声有时候反而能帮你跳出局部最优。
避坑指南:我曾经在调试一个NURBS曲面拟合时,用了SGD,结果因为噪声太大,参数一直在最优解附近震荡,始终无法收敛到足够精度。后来我改用小批量梯度下降(Mini-batch GD),才解决了这个问题。
4.5 三种方法的对比
| 方法 | 每次使用样本数 | 梯度准确性 | 收敛速度 | 内存占用 | 适用场景 |
|---|---|---|---|---|---|
| BGD | 全部 | 高 | 慢 | 高 | 小数据集、凸优化 |
| SGD | 1个 | 低(噪声大) | 快(单步) | 低 | 大数据集、需要跳出局部最优 |
| Mini-batch GD | 小批量(如32/64) | 中等 | 较快 | 中等 | 最常用,兼顾速度与稳定性 |
我个人最常用的是Mini-batch GD。为什么?因为它既不像BGD那么慢,也不像SGD那么疯。在自由曲面优化中,我一般取batch size为32或64,效果比较稳。
4.6 本章知识结构图
下面这张图帮你理清梯度下降法的核心脉络:
4.7 代码实战:梯度下降实现
光说不练假把式。下面我用Python实现一个简单的梯度下降,用来优化一个二次曲面(说白了就是一个碗状曲面)的最低点。
import numpy as np
# 目标函数:f(x, y) = x² + 2y² (一个椭圆抛物面)
def f(x, y):
return x**2 + 2*y**2
# 梯度计算
def gradient(x, y):
df_dx = 2*x
df_dy = 4*y
return np.array([df_dx, df_dy])
# 梯度下降
def gradient_descent(start_x, start_y, learning_rate, iterations):
x, y = start_x, start_y
history = []
for i in range(iterations):
grad = gradient(x, y)
x -= learning_rate * grad[0]
y -= learning_rate * grad[1]
loss = f(x, y)
history.append((x, y, loss))
if i % 10 == 0:
print(f"Step {i}: x={x:.4f}, y={y:.4f}, loss={loss:.6f}")
return x, y, history
# 试试不同学习率
print("=== 学习率 0.1 ===")
gradient_descent(2.0, 2.0, 0.1, 50)
print("\n=== 学习率 0.01 ===")
gradient_descent(2.0, 2.0, 0.01, 50)
print("\n=== 学习率 0.5(太大,会发散)===")
gradient_descent(2.0, 2.0, 0.5, 10)
试试看:把上面的代码复制到你的环境里跑一下。你会发现学习率0.1时,50步就接近(0,0)了;0.01时50步还没到;0.5时直接飞了。这就是学习率的魔力。
4.8 本章小结
梯度下降法,说白了就是「跟着感觉走,但步子要稳」。梯度给你方向,学习率控制步长,批量大小决定效率。这三者配合好了,你的自由曲面优化就能又快又准地找到最优解。
嗯,这一章的内容就到这里。记住:别迷信某一种方法,BGD、SGD、Mini-batch各有各的脾气。多试试,找到适合你曲面问题的组合,才是王道。