数学重学 - 26 优化与梯度下降

这是 数学重学路线图 阶段五的子页面

优化与梯度下降:让机器”学习”的核心

优化 = 找到最好的那个值

机器学习本质上就是一个优化问题

这一节是 25-微积分直觉 中梯度下降的深入展开

一、什么是优化问题

直觉比喻

你被蒙上眼睛丢到一座山上

任务:找到最低的山谷

你唯一能做的:感受脚下的坡度(梯度),然后往下坡方向走

这就是梯度下降

数学表述

给定目标函数 f(θ),找到参数 θ* 使得 f(θ*) 最小

θ* = argmin f(θ)

优化无处不在

| 场景 | 目标函数 | 要优化的参数 |
| 机器学习 | 损失函数 | 模型权重 |
| 后端调优 | 响应时间 | 连接池大小、缓存大小 |
| 安全模型 | 检测误差 | 分类阈值 |
| 资源分配 | 成本 | 资源配额 |

二、损失函数:衡量”错得多离谱”

MSE(均方误差)

MSE = Σ(预测值 - 真实值)² / n

直觉:每个预测的误差平方后取平均

为什么要平方?

消除正负抵消

放大大误差(惩罚离谱的预测)

交叉熵损失

Loss = -Σ y·log(ŷ)

用于分类问题

当预测概率偏离真实标签时,损失急剧增大

和信息论有关 → 27-信息论基础

Python 计算损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np

# MSE 损失
def mse_loss(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)

y_true = np.array([3.0, 5.0, 2.5, 7.0])
y_pred = np.array([2.8, 5.2, 2.0, 6.5])
print(f"MSE = {mse_loss(y_true, y_pred):.4f}")

# 交叉熵损失(二分类)
def binary_cross_entropy(y_true, y_pred):
epsilon = 1e-15 # 防止 log(0)
y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
return -np.mean(y_true * np.log(y_pred) +
(1 - y_true) * np.log(1 - y_pred))

y_true = np.array([1, 0, 1, 1])
y_pred = np.array([0.9, 0.1, 0.8, 0.7])
print(f"交叉熵 = {binary_cross_entropy(y_true, y_pred):.4f}")

# 预测越离谱,交叉熵越大
y_pred_bad = np.array([0.5, 0.5, 0.5, 0.5])
print(f"差预测的交叉熵 = {binary_cross_entropy(y_true, y_pred_bad):.4f}")

三、梯度:多元函数的”导数”

从导数到梯度

一元函数的导数 f'(x) → 告诉你往哪走函数增长最快

多元函数的梯度 ∇f(x₁, x₂, ...) → 一个向量,每个分量是对应变量的偏导数

梯度的几何含义

梯度方向 = 函数增长最快的方向

梯度大小 = 增长的速率

负梯度方向 = 函数下降最快的方向

偏导数

多元函数对某一个变量求导,其他变量当常数

例:f(x, y) = x² + 2xy + y³

∂f/∂x = 2x + 2y(把 y 当常数)

∂f/∂y = 2x + 3y²(把 x 当常数)

∇f = (2x + 2y, 2x + 3y²)

Python 计算梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np

def f(params):
x, y = params
return x**2 + 2*x*y + y**3

def numerical_gradient(f, params, h=1e-7):
"""数值梯度:对每个参数微扰"""
grad = np.zeros_like(params, dtype=float)
for i in range(len(params)):
params_plus = params.copy()
params_minus = params.copy()
params_plus[i] += h
params_minus[i] -= h
grad[i] = (f(params_plus) - f(params_minus)) / (2 * h)
return grad

point = np.array([1.0, 2.0])
grad = numerical_gradient(f, point)
print(f"在 (1, 2) 处的梯度: {grad}")
# 解析梯度: (2*1+2*2, 2*1+3*4) = (6, 14)

四、梯度下降算法

核心公式

θ_new = θ_old - α · ∇f(θ_old)

α学习率(步长)

每一步都朝着下降最快的方向走一小步

直觉理解

蒙眼人在山上的策略:

  1. 感受脚下坡度(计算梯度)
  2. 朝下坡方向迈一步(更新参数)
  3. 重复,直到脚下变平(梯度接近零)

手动实现梯度下降(线性回归)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import numpy as np
import matplotlib.pyplot as plt

# 生成模拟数据: y = 3x + 7 + 噪声
np.random.seed(42)
X = np.random.uniform(0, 10, 100)
y_true = 3 * X + 7 + np.random.normal(0, 2, 100)

# 参数初始化
w = 0.0 # 权重(斜率)
b = 0.0 # 偏置(截距)
lr = 0.001 # 学习率
epochs = 200

loss_history = []

for epoch in range(epochs):
# 前向传播:预测
y_pred = w * X + b

# 计算损失 (MSE)
loss = np.mean((y_pred - y_true) ** 2)
loss_history.append(loss)

# 计算梯度(MSE对w和b的偏导数)
dw = 2 * np.mean((y_pred - y_true) * X)
db = 2 * np.mean(y_pred - y_true)

# 更新参数(梯度下降核心步骤)
w = w - lr * dw
b = b - lr * db

if epoch % 50 == 0:
print(f"Epoch {epoch:3d}: loss={loss:.4f}, w={w:.4f}, b={b:.4f}")

print(f"\n最终结果: w={w:.4f} (真实=3), b={b:.4f} (真实=7)")

# 可视化损失下降
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(loss_history)
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('损失函数下降过程')
axes[0].grid(True)

axes[1].scatter(X, y_true, alpha=0.5, label='数据点')
axes[1].plot([0, 10], [b, w*10+b], 'r-', linewidth=2, label=f'拟合线: y={w:.2f}x+{b:.2f}')
axes[1].legend()
axes[1].set_title('线性回归结果')
axes[1].grid(True)

plt.tight_layout()
plt.show()

五、学习率:最关键的超参数

学习率太大

每步迈太远,跳过最优点

在最优点附近来回震荡,甚至发散

比喻:跳远运动员在山谷里蹦,越蹦越高

学习率太小

每步迈太短,收敛极慢

可能需要上万步才到最优点

比喻:蜗牛下山

学习率对比实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import numpy as np
import matplotlib.pyplot as plt

def f(x):
return (x - 3) ** 2 + 1

def df(x):
return 2 * (x - 3)

learning_rates = [0.01, 0.1, 0.5, 0.95]

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
axes = axes.flatten()

for idx, lr in enumerate(learning_rates):
x = 0.0
history = [x]
for _ in range(30):
x = x - lr * df(x)
history.append(x)

x_plot = np.linspace(-2, 8, 200)
axes[idx].plot(x_plot, f(x_plot), 'b-')
axes[idx].plot(history, [f(h) for h in history], 'ro-', markersize=4)
axes[idx].set_title(f'学习率 = {lr}')
axes[idx].set_ylim(0, 15)
axes[idx].grid(True)

plt.suptitle('不同学习率的梯度下降表现', fontsize=14)
plt.tight_layout()
plt.show()

六、局部最优 vs 全局最优

问题

现实中的损失函数不是简单的碗状

可能有很多”坑”(局部最小值)

梯度下降可能掉进一个不是最深的坑里

解决策略

随机初始化:多次随机初始点,取最好的结果

动量 Momentum:给下降过程加”惯性”,冲过小坑

v = β·v + ∇f(θ)

θ = θ - α·v

比喻:球从山上滚下来,有惯性不会卡在小坑里

学习率衰减:开始大步走快速接近,后面小步走精确定位

模拟退火:偶尔接受”上坡”的步骤,跳出局部最优

鞍点问题

高维空间中,鞍点比局部最小值更常见

鞍点:某些方向是最小,某些方向是最大

梯度为零但不是极值点

七、常见优化算法直觉

SGD(随机梯度下降)

不用全部数据算梯度,随机取一小批(mini-batch)

优点:快,有随机性可以跳出局部最优

缺点:震荡大,需要手动调学习率

Momentum(带动量的 SGD)

在 SGD 基础上加了”惯性”

加速收敛,减少震荡

像球在碗里滚动,越滚越快

RMSProp

自适应学习率:对每个参数用不同的学习率

梯度大的参数用小学习率(防震荡)

梯度小的参数用大学习率(加速)

Adam(最常用)

Momentum + RMSProp 的结合

既有动量加速,又有自适应学习率

大多数情况下的默认选择

一张表对比

| 算法 | 核心思想 | 优点 | 缺点 |
| SGD | 随机采样梯度 | 简单 | 需调学习率 |
| Momentum | 加惯性 | 收敛快 | 多一个超参数 |
| RMSProp | 自适应学习率 | 自动调节 | 可能不够稳 |
| Adam | 动量+自适应 | 通用 | 偶尔不如SGD |

八、凸优化简介

什么是凸函数

直觉:像一个,只有一个最低点

数学:任意两点连线在函数上方

对凸函数做梯度下降,局部最优 = 全局最优

常见凸函数

|x|e^x、MSE 损失(对线性模型)

非凸函数

深度学习的损失函数几乎都是非凸的

所以深度学习训练是个”尽力而为”的过程

好消息:实践中找到的局部最优通常也足够好

九、大数据中的优化

模型训练 = 优化问题

数据量 → 百万/亿条

参数量 → 百万/十亿

全量梯度计算不现实 → SGD 和 mini-batch

分布式优化

数据并行:不同机器算不同 batch 的梯度,然后聚合

模型并行:不同机器存储模型的不同部分

超参数调优

学习率、batch size、正则化系数都需要优化

网格搜索、随机搜索、贝叶斯优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import Ridge
import numpy as np

# 网格搜索最优正则化参数
X = np.random.randn(200, 5)
y = X @ np.array([1, 2, 0, -1, 0.5]) + np.random.randn(200) * 0.5

param_grid = {'alpha': [0.001, 0.01, 0.1, 1.0, 10.0]}
grid_search = GridSearchCV(Ridge(), param_grid, cv=5,
scoring='neg_mean_squared_error')
grid_search.fit(X, y)

print(f"最优 alpha: {grid_search.best_params_}")
print(f"最优得分: {-grid_search.best_score_:.4f}")

十、安全中的优化

对抗样本(梯度攻击)

攻击者利用模型梯度来制造对抗样本

FGSM 攻击原理:

  1. 计算损失函数对输入的梯度
  2. 沿梯度方向微调输入
  3. 人眼看不出区别,但模型判断完全错误

Python FGSM 攻击演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np

# 简化演示:一维输入的对抗攻击
# 假设模型是 y = sigmoid(w*x + b)
def sigmoid(z):
return 1 / (1 + np.exp(-z))

w, b = 2.0, -3.0 # 模型参数
x_original = 2.0 # 原始输入
y_true = 1 # 真实标签

# 原始预测
y_pred = sigmoid(w * x_original + b)
print(f"原始预测: {y_pred:.4f} (正确分类为1)")

# 计算损失对输入的梯度
# Loss = -y*log(ŷ), dLoss/dx = -(y - ŷ) * w
grad_x = -(y_true - y_pred) * w

# FGSM 攻击
epsilon = 1.5
x_adversarial = x_original + epsilon * np.sign(grad_x)

y_pred_adv = sigmoid(w * x_adversarial + b)
print(f"对抗样本预测: {y_pred_adv:.4f} (被攻击后可能误分类)")
print(f"输入扰动: {x_original}{x_adversarial}")

安全模型训练

对抗训练:训练时加入对抗样本,提高模型鲁棒性

梯度裁剪:限制梯度大小,防止梯度爆炸攻击

差分隐私:给梯度加噪声,防止通过梯度泄露训练数据

十一、后端中的优化思维

连接池大小优化

太小 → 请求排队,响应慢

太大 → 内存消耗大,数据库压力大

最优值 = 使总响应时间最小的连接数

缓存大小优化

缓存太小 → 命中率低,频繁回源

缓存太大 → 内存浪费

最优值 = 使 cost(命中率, 内存消耗) 最小的缓存大小

简单的后端参数优化模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import numpy as np
import matplotlib.pyplot as plt

# 模拟:连接池大小 vs 平均响应时间
def response_time(pool_size):
"""
太小: 排队时间长
太大: 数据库压力导致慢查询
最优点在中间
"""
queue_delay = 500 / pool_size # 排队延迟
db_pressure = 0.01 * pool_size ** 1.5 # 数据库压力
base_time = 50 # 基础处理时间
return base_time + queue_delay + db_pressure

# 梯度下降找最优连接池大小
pool = 5.0 # 初始值
lr = 0.5
h = 0.01

for i in range(100):
grad = (response_time(pool + h) - response_time(pool - h)) / (2 * h)
pool = pool - lr * grad
pool = max(1, pool) # 至少1个连接

print(f"最优连接池大小: {pool:.1f}")
print(f"最小响应时间: {response_time(pool):.2f}ms")

# 可视化
sizes = np.linspace(1, 100, 200)
times = [response_time(s) for s in sizes]
plt.plot(sizes, times, 'b-')
plt.axvline(x=pool, color='r', linestyle='--', label=f'最优={pool:.1f}')
plt.xlabel('连接池大小')
plt.ylabel('平均响应时间 (ms)')
plt.title('连接池大小优化')
plt.legend()
plt.grid(True)
plt.show()

十二、练习题

题目1:概念理解

为什么梯度下降要沿梯度的反方向走?如果沿梯度正方向走会怎样?

答案

梯度方向是函数增长最快的方向

我们要最小化目标函数,所以要往减小最快的方向走 = 梯度反方向

如果沿梯度正方向走 → 函数值会越来越大(梯度上升,可用于最大化问题)

题目2:学习率实验

用 Python 对 f(x) = x⁴ - 3x³ + 2 做梯度下降,分别尝试学习率 0.01, 0.1, 0.3,观察哪个收敛最快,哪个可能发散

答案

1
2
3
4
5
6
7
8
def f(x): return x**4 - 3*x**3 + 2
def df(x): return 4*x**3 - 9*x**2

for lr in [0.01, 0.1, 0.3]:
x = 4.0
for i in range(50):
x = x - lr * df(x)
print(f"lr={lr}: x={x:.4f}, f(x)={f(x):.4f}")

0.01 收敛慢但稳,0.1 适中,0.3 可能震荡

题目3:手算梯度

函数 f(x, y) = x² + xy + 2y²,求 ∇f 并计算在点 (1, -1) 处的梯度方向

答案

∂f/∂x = 2x + y,在 (1,-1) 处 = 1

∂f/∂y = x + 4y,在 (1,-1) 处 = -3

∇f(1, -1) = (1, -3)

梯度下降方向 = (-1, 3)

题目4:安全应用

解释 FGSM 攻击为什么要用 sign(梯度) 而不是直接用梯度值?

答案

sign() 保证每个维度的扰动大小一致(都是 ±ε)

直接用梯度值会导致某些维度扰动很大、某些很小

sign 版本在 L∞ 范数下是最优攻击(最小扰动最大效果)

且计算简单高效

题目5:综合实践

实现一个带 Momentum 的梯度下降,与普通梯度下降对比在 f(x) = x² + 10*sin(x) 上的表现(这个函数有多个局部最小值)

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np

def f(x): return x**2 + 10*np.sin(x)
def df(x): return 2*x + 10*np.cos(x)

# 普通梯度下降
x_gd = 5.0
for _ in range(100):
x_gd -= 0.05 * df(x_gd)

# 带 Momentum
x_mom = 5.0
v = 0
for _ in range(100):
v = 0.9 * v + df(x_mom)
x_mom -= 0.05 * v

print(f"普通GD: x={x_gd:.4f}, f={f(x_gd):.4f}")
print(f"Momentum: x={x_mom:.4f}, f={f(x_mom):.4f}")

十三、总结与关联

梯度下降是机器学习最基础的优化算法

关键要素:损失函数 + 梯度 + 学习率

Adam 是最常用的优化器,但理解 SGD 是基础

关联笔记

25-微积分直觉 导数和梯度的基础

27-信息论基础 交叉熵损失的信息论解释

→ 数学重学/22-线性代数直觉 高维梯度与矩阵运算


上一章 目录 下一章
25-微积分直觉 数学重学路线图 27-信息论基础