深入浅出梯度下降与反向传播
文章目录
- 1. 前言
- 2. 基本概念
- 2.1 一元函数的导数
- 2.2 偏导数
- 2.3 方向导数
- 2.4 梯度
- 2.5 均方误差
- 3. 梯度下降
- 3.1 梯度下降的公式
- 3.2 梯度下降的类型(优化器)
- 4. 反向传播
- 4.1 反向传播的基本步骤
- 4.2 反向传播的数学推导
- 5. 实战
- 5.1 手动求导
- 5.2 自动求导
- 5.3 过程可视化
1. 前言
在深度学习中,梯度下降和反向传播是两个至关重要的概念。它们共同作用于优化神经网络,使其能够从数据中学习到有用的模式。尽管它们是深度学习的核心,但很多人对这两个概念仍然感到困惑。本博客将深入讲解梯度下降和反向传播的基本原理,并通过一个简单的示例来加以说明。
2. 基本概念
2.1 一元函数的导数
如果有一个函数
f
(
x
)
f(x)
f(x),它的导数
f
′
(
x
)
f^′(x)
f′(x) 给出了函数在点
x
0
x_0
x0 处沿着
x
x
x 轴的变化速率,即:
f
′
(
x
0
)
=
lim
Δ
x
→
0
f
(
x
0
+
Δ
x
)
−
f
(
x
0
)
Δ
x
f ^\prime (x_0) = \lim_{\Delta x \to 0} \frac {f(x_0 + \Delta x) - f(x_0)} {{\Delta x}}
f′(x0)=Δx→0limΔxf(x0+Δx)−f(x0) 这个导数告诉我们,沿着
x
x
x轴方向,函数的变化快慢情况。
2.2 偏导数
对于多变量函数
f
(
x
1
,
x
2
,
…
,
x
n
)
f(x_1, x_2, \dots, x_n)
f(x1,x2,…,xn),偏导数描述了函数在某一维度(即某个变量)上变化的速率。例如,函数
f
(
x
,
y
)
=
x
2
+
y
2
f(x, y) = x^2 + y^2
f(x,y)=x2+y2 的偏导数为:
∂
f
∂
x
=
2
x
,
∂
f
∂
y
=
2
y
\frac{\partial f}{\partial x} = 2x, \quad \frac{\partial f}{\partial y} = 2y
∂x∂f=2x,∂y∂f=2y 这些偏导数分别表示函数在
x
x
x轴方向和
y
y
y轴方向的变化速率。
2.3 方向导数
描述了一个多变量函数在某一点沿任意给定方向的变化率。对于一个函数
f
(
x
1
,
x
2
,
…
,
x
n
)
f(x_1, x_2, \dots, x_n)
f(x1,x2,…,xn),在点
x
0
\mathbf{x_0}
x0 处沿着单位向量
v
\mathbf{v}
v 方向的方向导数表示为:
D
v
f
(
x
0
)
=
∇
f
(
x
0
)
⋅
v
D_{\mathbf{v}} f(\mathbf{x_0}) = \nabla f(\mathbf{x_0}) \cdot \mathbf{v}
Dvf(x0)=∇f(x0)⋅v其中,
∇
f
(
x
0
)
\nabla f(\mathbf{x_0})
∇f(x0) 是函数在点
x
0
\mathbf{x_0}
x0 的梯度。
方向导数的几何意义:方向导数给出了函数在某一点沿某个方向的变化速率。它可以看作是沿着某个方向的切线变化率,而不仅仅是沿坐标轴的变化率。
即在某一点
x
0
\mathbf{x_0}
x0,如果我们沿着某个方向
v
\mathbf{v}
v 走,方向导数告诉我们,沿着这个方向走时,函数值变化的快慢。如果方向导数为正,说明函数值在增加;如果为负,说明函数值在减少;如果为零,说明函数值在这个方向上没有变化。
2.4 梯度
在一个多变量的标量函数
f
(
x
1
,
x
2
,
…
,
x
n
)
f(x_1, x_2, \dots, x_n)
f(x1,x2,…,xn) 中,梯度是一个向量,表示函数在某一点的最速上升方向。梯度不仅告诉我们函数的变化率,还告诉我们该变化率最大的方向。具体来说,梯度是函数的所有偏导数组成的向量:
∇
f
(
x
1
,
x
2
,
…
,
x
n
)
=
(
∂
f
∂
x
1
,
∂
f
∂
x
2
,
…
,
∂
f
∂
x
n
)
\nabla f(x_1, x_2, \dots, x_n) = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \dots, \frac{\partial f}{\partial x_n} \right)
∇f(x1,x2,…,xn)=(∂x1∂f,∂x2∂f,…,∂xn∂f) 梯度的方向:梯度的方向指向函数值增加最快的方向。
梯度的大小:梯度的模长(即向量的长度)表示沿着这个方向,函数变化的速率。
可以把梯度看作是一个“指示器”,它告诉我们在某一点,函数增长最快的方向。换句话说,梯度指向了“上坡”的方向。
2.5 均方误差
均方误差(Mean Squared Error, MSE)
,是一种常用的衡量模型预测值与实际值之间差异的指标,尤其是回归任务中,MSE
的计算公式如下:
M
S
E
=
1
n
∑
i
=
1
n
(
y
i
−
y
^
i
)
2
MSE = \frac {1} {n} \sum _{i=1} ^ n (y_i - \hat y_i) ^ 2
MSE=n1i=1∑n(yi−y^i)2其中,
n
n
n是样本数量,
y
i
y_i
yi是第
i
i
i个样本的真实值,
y
^
i
\hat y_i
y^i是模型对第
i
i
i个样本的预测值。
均方误差也叫做最小二乘法。
3. 梯度下降
梯度下降(Gradient Descent, GD)
是一种优化算法,用于最小化(或最大化)一个函数,通常用来训练机器学习模型。在神经网络中,我们的目标是通过调整模型的参数(例如权重和偏置)来最小化损失函数。损失函数衡量了模型的预测与实际标签之间的差异,目标是让这个损失函数的值尽可能小。
梯度下降的核心思想是,沿着损失函数的梯度(偏导数)下降,不断更新参数,直到达到最小值。
3.1 梯度下降的公式
梯度下降的更新规则如下:
θ
=
θ
−
η
⋅
∇
θ
L
(
θ
)
\theta = \theta - \eta \cdot \nabla_\theta L(\theta)
θ=θ−η⋅∇θL(θ)其中:
θ
\theta
θ 是模型的参数(例如权重和偏置)。
η
\eta
η 是学习率(learning rate),控制每次更新的步长。
∇
θ
L
(
θ
)
\nabla_\theta L(\theta)
∇θL(θ) 是损失函数
L
L
L 关于参数
θ
\theta
θ 的梯度。
梯度
∇
θ
L
(
θ
)
\nabla_\theta L(\theta)
∇θL(θ) 告诉我们,沿着哪个方向更新参数能使损失函数下降得更快。
3.2 梯度下降的类型(优化器)
1.
批量梯度下降(Batch Gradient Descent)
:每次使用整个数据集来计算梯度和更新参数。虽然准确性高,但计算开销大,尤其是在数据量很大的时候。
2.
随机梯度下降(Stochastic Gradient Descent, SGD)
:每次只用一个样本来计算梯度和更新参数。虽然更新速度快,但可能会导致参数更新不稳定。
3.
小批量梯度下降(Mini-Batch Gradient Descent)
:每次使用数据集中的一个小批量样本来计算梯度和更新参数。这种方法结合了批量和随机梯度下降的优点。
当然,还有很多类型的优化,比如Adam
、RMSprop
、AdaW
等,这里不再细述。
4. 反向传播
反向传播(Back Propagation, BP)
是用于计算神经网络中每一层的梯度的算法。它是梯度下降的一部分,特别用于计算和传播每个参数对损失函数的贡献。
反向传播算法依赖于链式法则(Chain Rule)
,通过链式法则可以将损失函数对模型参数的梯度逐层传播回去,从而更新每一层的参数。
即从输出层一步步传播到输入层。
4.1 反向传播的基本步骤
假设我们有一个包含多个层的神经网络,每一层都有权重
W
W
W 和偏置
b
b
b。反向传播的步骤如下:
1.
前向传播:从输入层开始,将输入数据传递到输出层,并计算出预测值。
2.
计算损失:根据模型的输出与真实标签计算损失函数。
3.
反向传播:
(1)
计算输出层的梯度,即损失函数关于输出的偏导数。
(2)
逐层计算隐藏层的梯度,即损失函数关于每一层的输入、权重和偏置的偏导数。
4.
更新权重和偏置:根据梯度下降规则更新每一层的权重和偏置。
4.2 反向传播的数学推导
假设神经网络有两层,每层的输出分别为
a
1
a_1
a1 和
a
2
a_2
a2,损失函数为
L
L
L。反向传播的目标是计算
∂
L
∂
W
1
\frac{\partial L}{\partial W_1}
∂W1∂L 和
∂
L
∂
W
2
\frac{\partial L}{\partial W_2}
∂W2∂L,即每一层权重的梯度。
1.
输出层梯度计算:
∂
L
∂
a
2
=
∂
L
∂
y
⋅
∂
y
∂
a
2
\frac{\partial L}{\partial a_2} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial a_2}
∂a2∂L=∂y∂L⋅∂a2∂y其中
y
y
y是输出层的预测值。
2.
隐藏层梯度计算:
∂
L
∂
a
1
=
∂
L
∂
a
2
⋅
∂
a
2
∂
a
1
\frac{\partial L}{\partial a_1} = \frac{\partial L}{\partial a_2} \cdot \frac{\partial a_2}{\partial a_1}
∂a1∂L=∂a2∂L⋅∂a1∂a2然后,我们使用链式法则计算每一层的权重梯度。
3.
参数更新:根据计算出来的梯度使用梯度下降更新权重和偏置。
5. 实战
5.1 手动求导
我们来构建一个简单的神经网络,做一个线性回归:
模型的输入为2维数据,输出为1维数据,该模型的函数表达式可以表示为:
y
∗
=
w
5
⋅
(
w
1
⋅
x
1
+
w
3
⋅
x
2
)
+
w
6
⋅
(
w
2
⋅
x
1
+
w
4
⋅
x
2
)
y^* = w_5 \cdot (w_1 \cdot x_1 + w_3 \cdot x_2) + w_6 \cdot (w_2 \cdot x_1 + w_4 \cdot x_2)
y∗=w5⋅(w1⋅x1+w3⋅x2)+w6⋅(w2⋅x1+w4⋅x2) 我们使用MSE
来作为损失函数,来优化我们的网络,即
L
o
s
s
=
1
n
∑
i
=
1
n
(
y
i
−
y
i
∗
)
2
Loss = \frac {1} {n} \sum _{i=1} ^ n (y_i - y^*_i) ^ 2
Loss=n1i=1∑n(yi−yi∗)2
假设初始时模型的权重为:
w
1
=
1.0
,
w
2
=
0.5
,
w
3
=
0.5
,
w
4
=
0.7
w_1=1.0,w_2=0.5,w_3=0.5,w_4=0.7
w1=1.0,w2=0.5,w3=0.5,w4=0.7
w
5
=
1.0
,
w
6
=
2.0
w_5=1.0,w_6=2.0
w5=1.0,w6=2.0 已知一个样本数据:
x
1
=
0.5
,
x
2
=
1.0
,
y
=
0.8
x_1=0.5,x_2=1.0,y=0.8
x1=0.5,x2=1.0,y=0.8 按照上述公式,计算一次前向传播,得到
y
∗
=
2.9
y^*=2.9
y∗=2.9,与真实值
y
y
y 的偏差可以通过
l
o
s
s
loss
loss 计算得到,
l
o
s
s
=
(
y
−
y
∗
)
2
=
4.41
loss = (y - y^*) ^ 2 = 4.41
loss=(y−y∗)2=4.41。然后我们通过梯度下降的方式来更新
w
5
w_5
w5,需要先计算出在点
(
x
1
,
x
2
)
(x_1,x_2)
(x1,x2)的梯度:
∇
=
∂
L
∂
w
1
=
∂
L
∂
y
∗
⋅
∂
y
∗
∂
w
5
\nabla = \frac {\partial L} {\partial w_1} = \frac {\partial L} {\partial y^*} \cdot \frac {\partial y^*} {\partial w_5}
∇=∂w1∂L=∂y∗∂L⋅∂w5∂y∗
∂
L
∂
y
∗
=
−
2
(
y
−
y
∗
)
=
4.2
\frac {\partial L} {\partial y^*} = -2 (y- y^*) = 4.2
∂y∗∂L=−2(y−y∗)=4.2
∂
y
∗
∂
w
5
=
w
1
⋅
x
1
+
w
3
⋅
x
2
=
1.0
\frac {\partial y^*} {\partial w_5} = w_1 \cdot x_1 + w_3 \cdot x_2 = 1.0
∂w5∂y∗=w1⋅x1+w3⋅x2=1.0
所以梯度为:
∂
L
∂
w
1
=
4.2
\frac {\partial L} {\partial w_1} = 4.2
∂w1∂L=4.2 假设,学习率为
0.01
0.01
0.01,根据梯度下降算法
θ
=
θ
−
η
⋅
∇
θ
L
(
θ
)
\theta = \theta - \eta \cdot \nabla_{\theta} L(\theta)
θ=θ−η⋅∇θL(θ) 更新
w
5
w5
w5:
w
5
=
w
5
−
η
∂
L
∂
w
5
=
0.958
w_5 = w_5 - \eta \frac {\partial L} {\partial w_5} = 0.958
w5=w5−η∂w5∂L=0.958 同理,可以更新
w
1
w_1
w1:
∂
y
∗
∂
w
1
=
w
5
x
1
=
0.5
\frac {\partial y^*} {\partial w_1} = w_5x_1 = 0.5
∂w1∂y∗=w5x1=0.5
∂
L
∂
w
1
=
∂
L
∂
y
∗
⋅
∂
y
∗
∂
w
1
=
2.1
\frac {\partial L} {\partial w_1} = \frac {\partial L} {\partial y^*} \cdot \frac {\partial y^*} {\partial w_1} = 2.1
∂w1∂L=∂y∗∂L⋅∂w1∂y∗=2.1
w
1
=
w
1
−
η
∂
L
∂
w
1
=
0.979
w_1 = w_1 - \eta \frac {\partial L} {\partial w_1} = 0.979
w1=w1−η∂w1∂L=0.979
5.2 自动求导
将上面模型的表达式形式化,即
y
∗
=
(
x
T
⋅
W
h
)
⋅
W
o
y* = (x^T \cdot W_h) \cdot W_o
y∗=(xT⋅Wh)⋅Wo其中,
x
=
[
x
1
x
2
]
,
W
h
=
[
w
1
w
2
w
3
w
4
]
,
W
o
=
[
w
5
w
6
]
x = \begin{bmatrix} x_1 \\ x_2\end{bmatrix},W_h = \begin{bmatrix} w_{1} & w_{2} \\ w_{3} & w_{4} \end{bmatrix},W_o = \begin{bmatrix} w_{5} & w_{6} \end{bmatrix}
x=[x1x2],Wh=[w1w3w2w4],Wo=[w5w6] 这样,我们就可以使用pytorch
来构建上述神经网络了,具体如下:
# -*- coding: utf-8 -*-
# Author : liyanpeng
# Email : yanpeng.li@cumt.edu.cn
# Datetime: 2024/11/29 17:38
# Filename: chain_rule.py
import torch
import torch.nn as nn
import torch.optim as optim
def func_y():
x1 = torch.tensor(0.5, dtype=torch.float32)
x2 = torch.tensor(1.0, dtype=torch.float32)
w1 = torch.tensor(1.0, dtype=torch.float32)
w2 = torch.tensor(0.5, dtype=torch.float32)
w3 = torch.tensor(0.5, dtype=torch.float32)
w4 = torch.tensor(0.7, dtype=torch.float32)
w5 = torch.tensor(1.0, dtype=torch.float32)
w6 = torch.tensor(2.0, dtype=torch.float32)
y = w5 * (w1 * x1 + w2 * x2) + w6 * (w3 * x1 + w4 * x2)
print(y)
class SimpleNeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(in_features=2, out_features=2, bias=False)
self.linear2 = nn.Linear(in_features=2, out_features=1, bias=False)
self.linear1.weight.data = torch.tensor([[1.0, 0.5], [0.5, 0.7]], dtype=torch.float32)
self.linear2.weight.data = torch.tensor([[1.0, 2.0]], dtype=torch.float32)
def forward(self, x):
x = self.linear1(x)
y = self.linear2(x)
return y
def grad_hook(grad):
print('grad:', grad[0][0])
if __name__ == '__main__':
x = torch.tensor([0.5, 1.0], dtype=torch.float32)
y = torch.tensor(0.8, dtype=torch.float32)
model = SimpleNeuralNetwork()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
hook_list = []
for name, param in model.named_parameters():
if param.requires_grad:
hook = param.register_hook(grad_hook)
hook_list.append(hook)
y_pred = model(x)
loss = criterion(y_pred, y)
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 更新权重
for hook in hook_list:
hook.remove()
print('更新后的w1:', model.linear1.weight.data[0, 0])
print('更新后的w5:', model.linear2.weight.data[0, 0])
打印结果如下,与手动计算的结果一致:
grad: tensor(4.2000)
grad: tensor(2.1000)
更新后的w1: tensor(0.9790)
更新后的w5: tensor(0.9580)
5.3 过程可视化
再加入一些可视化代码,看一下权重更新的轨迹:
# -*- coding: utf-8 -*-
# Author : liyanpeng
# Email : yanpeng.li@cumt.edu.cn
# Datetime: 2024/11/29 17:38
# Filename: chain_rule.py
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
if __name__ == '__main__':
x = torch.tensor([0.5, 1.0], dtype=torch.float32)
y = torch.tensor(0.8, dtype=torch.float32)
model = SimpleNeuralNetwork()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_list = []
w1_list = []
w2_list = []
for epoch in range(5):
y_pred = model(x)
loss = criterion(y_pred, y)
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 更新权重
loss_list.append(loss.item())
w1_list.append(model.linear1.weight.data[0, 0].item())
w2_list.append(model.linear2.weight.data[0, 0].item())
fig = plt.figure(figsize=(8, 6))
# 3D曲面图和等高线图
ax1 = fig.add_subplot(111, projection='3d')
ax1.set_title("3D Loss Surface with Contours and Trajectory")
# 将损失值、权重映射到X, Y, Z轴
ax1.plot_trisurf(w1_list, w2_list, loss_list, cmap='viridis', alpha=0.7)
# 生成网格数据,用于绘制等高线图
w1_range = np.linspace(min(w1_list), max(w1_list), 100)
w2_range = np.linspace(min(w2_list), max(w2_list), 100)
W1, W2 = np.meshgrid(w1_range, w2_range)
np.random.seed(42)
x_train = np.random.rand(100, 2)
noise = np.random.randn(100, 1)
# f(x, y) = 2x^2 + y^2 - 0.7 + noise
y_train = 2 * x_train[:, 0] * x_train[:, 0] + x_train[:, 1] * x_train[:, 1] - 0.7 + noise
# 计算每个网格点的损失值
loss_grid = np.zeros(W1.shape)
for i in range(W1.shape[0]):
for j in range(W1.shape[1]):
model.linear1.weight.data[0, 0] = W1[i, j]
model.linear2.weight.data[0, 0] = W2[i, j]
y_pred = model(torch.tensor(x_train, dtype=torch.float32))
loss_grid[i, j] = criterion(y_pred, torch.tensor(y_train, dtype=torch.float32)).item()
# 绘制等高线图
ax1.contour(W1, W2, loss_grid, levels=20, cmap='viridis', offset=min(loss_list))
# 绘制权重更新轨迹
ax1.plot(w1_list, w2_list, loss_list, 'k-', marker='o', markersize=5, label="Weight Update Trajectory")
ax1.plot(w1_list, w2_list, [min(loss_list)] * len(w1_list), 'r-', marker='o', markersize=5, label="Trajectory on Contours")
# 在等高线图上标记权重更新的坐标
for i in range(len(w1_list)):
ax1.text(w1_list[i], w2_list[i], min(loss_list),
f'({w1_list[i]:.4f}, {w2_list[i]:.4f})',
color='red', fontsize=8, ha='center', va='center')
ax1.set_xlabel('Linear1 Weight (First Element)')
ax1.set_ylabel('Linear2 Weight (First Element)')
ax1.set_zlabel('Loss')
ax1.legend()
plt.tight_layout()
plt.show()
绘制的效果图如下:
损失函数从右侧至左侧逐渐收敛,最右侧的点是我们第一次更新时
w
1
w_1
w1和
w
2
w_2
w2的权重。