梯度

梯度是一个在微积分中使用的重要概念,它用于衡量函数在给定点上的方向导数沿各个方向最大时的最大值。对于一个标量函数,梯度的方向是函数增长最快的方向,而梯度的反方向则是函数减小最快的方向。

定义

对于在点$x \in \mathbb{R}^n$可微的函数$f: \mathbb{R}^n \rightarrow \mathbb{R}$,其梯度被定义为一个向量,其各个分量为函数在该点上的偏导数。对于函数$f(x_1, x_2, …, x_n)$,它的梯度可以表示为:

$$
\nabla f(x) = \left[ \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, …, \frac{\partial f}{\partial x_n} \right]^T
$$

这里,$\nabla f(x)$表示$f(x)$的梯度,$\frac{\partial f}{\partial x_i}$表示$f$关于$x_i$的偏导数,$T$表示矩阵转置。

物理含义

梯度有一个重要的物理含义。在二维空间中,可以把函数$f(x, y)$看作地形的高度,那么梯度就是指向最陡峭上升方向的向量。而梯度的大小,则对应了最陡峭上升方向的斜率。因此,在优化问题中,我们通常沿着梯度的反方向更新参数,以最快地降低函数值。

梯度在机器学习中的应用

在机器学习中,我们的目标通常是找到一组参数,使得损失函数达到最小。为了实现这个目标,我们可以使用梯度下降算法,不断地沿着损失函数梯度的反方向更新参数。

在深度学习中,由于模型通常有大量的参数,我们需要使用反向传播算法来高效地计算梯度。这种算法基于链式法则,可以在计算图中从输出端到输入端,一层一层地计算各个参数的梯度。

计算梯度

在实践中,我们通常使用自动微分(如PyTorch和TensorFlow提供的自动微分功能)来计算梯度。这使得我们无需手动推导和实现复杂的梯度公式,大大提高了编程的效率。以下是一个PyTorch中计算梯度的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch

# 创建一个张量,并设置requires_grad=True使得我们可以计算关于它的梯度
x = torch.tensor([1.0], requires_grad=True)

# 定义函数
y = 2 * x * x

# 通过反向传播计算梯度
y.backward()

# 输出梯度
print(x.grad) # 输出:tensor([4.]),因为y = 2 * x^2, dy/dx = 4 * x = 4 * 1 = 4

在上述例子中,y.backward()表示计算关于y的梯度,然后将这个梯度反向传播回其输入x。因此,y本身的梯度被认为是1(因为对于任何变量xdx/dx都等于1),然后这个梯度被传递到x,得到的是y关于x的梯度,即dy/dx

注意,y本身没有.grad属性,因为它不是通过requires_grad=True创建的。只有那些通过requires_grad=True创建,并且参与了运算的张量才有.grad属性,这个属性存储了梯度信息。

在PyTorch中,backward()函数的作用是计算梯度,并将梯度信息存储在.grad属性中。backward()函数的调用者(即y)自身的梯度被认为是1,然后这个梯度会被反向传播回所有参与了运算并需要计算梯度的张量。

梯度下降算法

原理

梯度下降算法是一种用于优化目标函数的迭代方法。具体来说,它是一个寻找函数最小值的算法。对于最大化问题,我们可以通过最小化目标函数的相反数来求解。

在梯度下降中,我们首先选择一个初始点(即初始参数值),然后我们迭代地将参数向负梯度方向移动,这样在每一步,我们都能够减小目标函数的值,直到找到函数的局部最小值。

在这个过程中,梯度(函数的一阶导数)给出了函数值下降最快的方向。我们使用一个叫做学习率的参数来控制每一步移动的大小。学习率决定了每次迭代时参数更新的步长,过大的学习率可能会使算法在最小值处震荡,过小则可能会导致算法收敛速度过慢。

公式

基本的梯度下降更新公式为:

$$
\theta_{new} = \theta_{old} - \alpha \nabla J(\theta_{old})
$$

其中,$\theta$ 表示我们试图优化的参数,$J(\theta)$ 是我们试图最小化的目标函数,$\nabla J(\theta_{old})$ 是在当前参数值 $\theta_{old}$ 处的目标函数的梯度,$\alpha$ 是学习率。

示例

考虑一个简单的线性回归问题。我们有一个目标函数(损失函数)为均方误差:

$$
J(\theta) = \frac{1}{2m} \sum_{i=1}^{m} (h_{\theta}(x^{(i)}) - y^{(i)})^2
$$

其中,$h_{\theta}(x^{(i)}) = \theta^T x^{(i)}$ 是预测函数,$m$ 是训练样本数量。

对于这个问题,我们可以使用梯度下降算法来找到最小化损失函数的参数 $\theta$。在每次迭代中,我们首先计算损失函数的梯度,然后根据前面的公式更新参数。

代码实现

下面是使用 Python 实现梯度下降的示例代码:

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

def gradient_descent(X, y, theta, alpha, num_iters):
m = y.size
J_history = np.zeros(num_iters)

for i in range(num_iters):
theta = theta - alpha * (1/m) * (X.T @ (X @ theta - y))
J_history[i] = compute_cost(X, y, theta)

return theta, J_history

def compute_cost(X, y, theta):
m = y.size
J = 1/(2*m) * np.sum(np.square(X @ theta - y))

return J

在这个代码中,gradient_descent 函数实现了梯度下降算法,compute_cost 函数用于计算目标函数(损失函数)的值。

梯度下降算法的分类

梯度下降算法是一种优化算法,用于寻找损失函数的最小值。在机器学习和深度学习中,我们通常使用梯度下降算法来优化我们的模型,即调整模型参数以最小化损失函数。以下将介绍梯度下降的几种主要变体。

批量梯度下降(Batch Gradient Descent)

批量梯度下降是最基本的形式,它在每一次迭代中都使用全量的训练数据来计算损失函数的梯度。因此,批量梯度下降的每一步都朝着全局最优的方向。然而,这也使得批量梯度下降在大规模数据集上非常慢,且无法在线(实时)更新模型。

更新公式:

$$
\theta = \theta - \alpha \nabla J(\theta)
$$

其中,$\theta$ 是参数,$\alpha$ 是学习率,$\nabla J(\theta)$ 是损失函数 $J$ 关于参数 $\theta$ 的梯度。

随机梯度下降(Stochastic Gradient Descent,SGD)

随机梯度下降在每一次迭代中随机选择一个样本来计算梯度。因此,每一步的更新方向并不一定是全局最优的方向,结果会有一些噪音。然而,这使得随机梯度下降在大规模数据集上比批量梯度下降快很多,且能够在线更新模型。

更新公式与批量梯度下降一致,只是每次只对一个随机样本进行计算。

小批量梯度下降(Mini-Batch Gradient Descent)

小批量梯度下降是批量梯度下降和随机梯度下降的折中,它在每一次迭代中使用一部分(小批量)样本来计算梯度。小批量梯度下降比随机梯度下降更稳定,同时仍然具有相对较高的计算速度。

更新公式与前面两者一致,只是每次对一个小批量的样本进行计算。

梯度下降算法的进阶变体

在梯度下降的基础上,研究者们引入了一些额外的概念以改善算法的性能。以下是一些广为使用的梯度下降算法的变体。

Momentum

动量(Momentum)是一种帮助优化器在相关方向上保持速度,从而抑制振荡并加快收敛的策略。其核心思想是引入一个新的变量(通常称为速度),它在每一步中都会增加当前梯度,然后参数更新会按照这个速度进行。

Momentum的更新公式如下:

$$
v = \beta v - \alpha \nabla J(\theta)
$$
$$
\theta = \theta + v
$$

其中,$\theta$ 是参数,$\nabla J(\theta)$ 是损失函数 $J$ 关于参数 $\theta$ 的梯度,$\alpha$ 是学习率,$v$ 是速度,$\beta$ 是动量因子,通常设为0.9。

AdaGrad

AdaGrad(Adaptive Gradient Algorithm)的主要思想是为每个参数分配一个自适应的学习率,这对于稀疏数据和处理非平稳目标函数非常有用。具体来说,对于经常出现且有大梯度的参数,其学习率会被降低;反之,对于稀疏或小梯度的参数,其学习率会被提高。

AdaGrad的更新公式如下:

$$
G_{t} = G_{t-1} + (\nabla J(\theta))^2
$$
$$
\theta = \theta - \frac{\alpha}{\sqrt{G_{t} + \epsilon}} \cdot \nabla J(\theta)
$$

其中,$G_{t}$ 是到目前为止所有梯度的平方和,$\epsilon$ 是一个很小的数,通常设为1e-8,用于防止除零错误。

RMSProp

RMSProp(Root Mean Square Propagation)是AdaGrad的一个改进版本,主要解决了AdaGrad在非凸设置下学习率快速下降的问题。与AdaGrad一样,RMSProp也是为每个参数分配一个自适应的学习率,但是它使用了一个滑动平均的梯度平方来更新 $G_{t}$。

RMSProp的更新公式如下:

$$
G_{t} = \beta G_{t-1} + (1 - \beta) (\nabla J(\theta))^2
$$
$$
\theta = \theta - \frac{\alpha}{\sqrt{G_{t} + \epsilon}} \cdot \nabla J(\theta)
$$

其中,$\beta$ 是平方梯度的滑动平均因子,通常设为0.9。

Adam

Adam(Adaptive Moment Estimation)结合了Momentum和RMSProp的思想。它计算了梯度的指数滑动平均(第一矩)和平方梯度的指数滑动平均(第二矩),并使用这两个量来更新参数。

Adam的更新公式如下:

$$
m = \beta_{1} m + (1 - \beta_{1}) \nabla J(\theta)
$$
$$
v = \beta_{2} v + (1 - \beta_{2}) (\nabla J(\theta))^2
$$
$$
\hat{m} = \frac{m}{1 - \beta_{1}^{t}}
$$
$$
\hat{v} = \frac{v}{1 - \beta_{2}^{t}}
$$
$$
\theta = \theta - \frac{\alpha \hat{m}}{\sqrt{\hat{v}} + \epsilon}
$$

其中,$m$ 和 $v$ 分别是第一矩和第二矩的估计,$\beta_{1}$ 和 $\beta_{2}$ 分别是第一矩和第二矩的滑动平均因子,通常设为0.9和0.999,$t$ 是当前的迭代步数。

PyTorch优化器详细介绍

PyTorch提供了一些已经实现的优化器,这些优化器都在torch.optim模块中。优化器的主要作用是更新模型的参数以最小化目标函数(通常为损失函数)。

常见优化器介绍及使用

  1. 随机梯度下降(SGD)

SGD是最基本的优化器,它对每一个参数使用相同的学习率进行更新。这是其更新公式:

$$
\theta_{new} = \theta_{old} - \alpha \nabla J(\theta_{old})
$$

在PyTorch中,可以这样使用SGD优化器:

1
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
  1. 带动量的随机梯度下降(Momentum SGD)

Momentum SGD是SGD的一种改进,它在更新参数时会考虑过去的梯度,从而达到平滑更新的效果。这是其更新公式:

$$
v = \beta v - \alpha \nabla J(\theta)
$$
$$
\theta = \theta + v
$$

其中,$v$是动量,$\beta$是动量衰减因子。在PyTorch中,可以这样使用Momentum SGD优化器:

1
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
  1. 自适应梯度算法(Adagrad)

Adagrad是一种自适应学习率的优化器,它会对每一个参数使用不同的学习率进行更新。这使得它在处理稀疏数据时有很好的表现。这是其更新公式:

$$
\theta_{new} = \theta_{old} - \frac{\alpha}{\sqrt{G_{t} + \epsilon}} \cdot \nabla J(\theta_{old})
$$

其中,$G_{t}$是到目前为止所有梯度的平方和,$\epsilon$是一个很小的数(如1e-8)用于防止除零错误。在PyTorch中,可以这样使用Adagrad优化器:

1
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.1)
  1. 自适应动量估计(Adam)

Adam结合了Momentum SGD和Adagrad的思想,它对每个参数都有一个自适应的学习率,并且会考虑过去的梯度。这使得它在许多任务上都有很好的表现。这是其更新公式:

$$
m = \beta_{1} m + (1 - \beta_{1}) \nabla J(\theta)
$$
$$
v = \beta_{2} v + (1 - \beta_{2}) (\nabla J(\theta))^2
$$
$$
\hat{m} = \frac{m}{1 - \beta_{1}^{t}}
$$
$$
\hat{v} = \frac{v}{1 - \beta_{2}^{t}}
$$
$$
\theta = \theta - \frac{\alpha \hat{m}}{\sqrt{\hat{v}} + \epsilon}
$$

其中,$m$和$v$分别是第一和第二矩估计,$\beta_{1}$和$\beta_{2}$是衰减因子(一般设为0.9和0.999),$t$是迭代次数。在PyTorch中,可以这样使用Adam优化器:

1
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

优化器的使用方法

优化器的基本使用流程如下:

  1. 定义模型(model)。
  2. 定义损失函数(loss function)。
  3. 选择优化器(optimizer),并将模型的参数和学习率传入优化器。
  4. 在训练循环中,首先清空优化器的梯度(optimizer.zero_grad()),然后计算损失(loss.backward()),最后更新模型的参数(optimizer.step())。

优化器的选择场景

  1. SGD:适用于大规模线性模型,或者在训练初期快速降低损失。
  2. Momentum SGD:适用于深度学习任务,比SGD有更快的收敛速度。
  3. Adagrad:适用于处理稀疏数据的任务,如自然语言处理中的词向量训练。
  4. Adam:适用于大多数深度学习任务,是一个比较通用的优化器。

选择哪种优化器并没有固定的规则,具体要根据任务的特点和数据的特性来决定。一般来说,可以先试用Adam,如果效果不佳,再考虑其他优化器。

反向传播算法

反向传播(Backpropagation)是神经网络中用于训练模型的主要算法之一,它是许多现代深度学习框架(如TensorFlow和PyTorch)的核心部分。以下将详细介绍反向传播的工作原理。它的主要任务是通过计算损失函数(一个衡量模型预测与真实值差异的函数)对模型参数的梯度来有效地更新网络的权重和偏置,以最小化损失函数。

原理

反向传播的目标是计算损失函数关于神经网络参数的梯度,以便使用梯度下降或其他优化算法来更新参数。为此,反向传播从输出层开始,沿着神经网络反向传递梯度。

反向传播的关键思想是链式法则(chain rule),这是微积分的一个基本定理,用于计算复合函数的导数。在神经网络的上下文中,链式法则用于计算损失函数关于参数的梯度,通过将这个复合函数拆分成一系列更简单的函数,并将它们的导数相乘。

算法过程

以下是反向传播算法的一般步骤:

  1. 前向传播:从输入层开始,通过网络向前传播数据,计算每一层的输出,直到得到最终的预测结果。

  2. 计算损失:使用损失函数计算预测结果和实际目标之间的误差。

  3. 反向传播损失:从输出层开始,计算损失函数关于每一层输出的梯度,并反向传播这些梯度。具体来说,对于每一层,我们首先计算损失函数关于这一层输出的梯度,然后使用链式法则,计算这个梯度关于这一层输入的梯度,以及关于这一层参数的梯度。

  4. 更新参数:使用梯度下降或其他优化算法,利用在步骤3中计算出的梯度来更新网络参数。

这个过程在每个训练迭代中重复,直到网络参数收敛,或者达到预设的最大迭代次数。

计算方法

假设我们有一个损失函数 $L$,我们想要知道它关于权重 $w_{ij}$ (表示从第 $i$ 个神经元到第 $j$ 个神经元的权重)的梯度,我们可以使用以下的公式:

$$
\frac{\partial L}{\partial w_{ij}} = \frac{\partial L}{\partial z_j} \cdot \frac{\partial z_j}{\partial w_{ij}}
$$

其中,$z_j$ 是 $j$ 个神经元的输入,它等于 $\sum_i w_{ij} x_i + b_j$。

现在我们需要找到每个部分的导数。首先,$\frac{\partial L}{\partial z_j}$ 通常直接由网络的后面部分提供。这是因为我们一般会按照从后向前的顺序计算导数,也就是说,我们首先计算出损失函数对于最后一层的输出的导数,然后用这个导数来计算损失函数对于倒数第二层的输出的导数,以此类推。

然后,我们需要找到 $\frac{\partial z_j}{\partial w_{ij}}$。由于 $z_j = \sum_i w_{ij} x_i + b_j$,我们可以看到,如果我们改变 $w_{ij}$,$z_j$ 就会按照 $x_i$ 的大小改变。所以,$\frac{\partial z_j}{\partial w_{ij}} = x_i$。

这样,我们就得到了:

$$
\frac{\partial L}{\partial w_{ij}} = \frac{\partial L}{\partial z_j} \cdot x_i
$$

让我们来看一个具体的例子。假设我们有一个简单的神经网络,只有输入层和输出层,没有隐藏层。输入层有一个神经元,其值为 $x$,输出层也有一个神经元,其值为 $y$。他们之间的权重是 $w$,偏置为 $b$。那么,$y$ 的计算公式就是 $y = wx + b$。我们用均方误差作为损失函数,也就是说,如果真实值是 $t$,那么损失函数就是 $L = (t - y)^2$。

现在,我们想要计算 $\frac{\partial L}{\partial w}$。首先,我们需要找到 $\frac{\partial L}{\partial y}$。由于 $L = (t - y)^2$,我们可以得到 $\frac{\partial L}{\partial y} = -2(t - y)$。

然后,我们需要找到 $\frac{\partial y}{\partial w}$。由于 $y = wx + b$,我们可以得到 $\frac{\partial y}{\partial w} = x$。

所以,最后我们得到:

$$
\frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial w} = -2(t - y) \cdot x
$$

这就是我们要找的梯度,我们可以用它来更新权重 $w$,以减小损失函数。

代码实现

PyyTorch中的反向传播

以下是一个简单的反向传播算法PyTorch代码示例进行详细分步解释:

  1. 创建一个简单的神经网络
1
2
3
4
5
model = nn.Sequential(
nn.Linear(10, 20),
nn.ReLU(),
nn.Linear(20, 1),
)

首先,我们使用PyTorch的nn.Sequential来定义一个简单的神经网络。这个网络由两个线性层(nn.Linear)和一个ReLU激活函数(nn.ReLU)组成。第一个线性层将输入的大小从10变为20,ReLU激活函数增加了模型的非线性,第二个线性层将大小为20的隐藏状态映射为大小为1的输出。

  1. 选择损失函数和优化器
1
2
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

接着,我们选择一个损失函数和一个优化器。损失函数用于度量模型的预测与实际目标之间的差距,优化器用于根据损失的梯度来更新模型的参数。在这个例子中,我们选择均方误差损失(nn.MSELoss)作为损失函数,选择随机梯度下降(torch.optim.SGD)作为优化器。

  1. 模拟输入和目标数据
1
2
inputs = torch.randn(5, 10)
targets = torch.randn(5, 1)

然后,我们生成一些模拟的输入和目标数据。在这个例子中,我们生成一个形状为[5, 10]的输入张量和一个形状为[5, 1]的目标张量。

  1. 前向传播
1
outputs = model(inputs)

在前向传播阶段,我们将输入数据传入模型,得到模型的预测输出。

  1. 计算损失
1
loss = criterion(outputs, targets)

接着,我们使用损失函数来计算模型的预测输出与实际目标之间的差距。

  1. 反向传播
1
loss.backward()

在反向传播阶段,我们调用损失张量的backward方法,计算损失关于模型参数的梯度。这些梯度将存储在对应参数的.grad属性中。

  1. 更新参数
1
optimizer.step()

然后,我们调用优化器的step方法,根据存储在模型参数的.grad属性中的梯度来更新参数。

  1. 清零梯度
1
optimizer.zero_grad()

最后,我们调用优化器的zero_grad方法,将模型参数的.grad属性中的梯度清零。这一步是必要的,因为PyTorch默认会累加梯度,即每次调用.backward方法时,梯度都会累加到.grad属性中,而不是替换。如果不清零梯度,下一次迭代时计算的梯度将会与这一次迭代的梯度叠加,导致错误。

注意事项:以上的代码是一个完整的训练步骤,实际使用时通常需要将这个过程放入一个循环中,对整个训练数据集进行多次迭代,直到模型性能满足要求或达到预设的最大迭代次数。在每次迭代中,还可以添加一些代码来记录训练进度,如打印当前的损失值,或者在验证数据集上评估模型的性能。

完整代码如下:

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
import torch
import torch.nn as nn

# 创建一个简单的神经网络
model = nn.Sequential(
nn.Linear(10, 20),
nn.ReLU(),
nn.Linear(20, 1),
)

# 选择损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 模拟输入和目标数据
inputs = torch.randn(5, 10)
targets = torch.randn(5, 1)

# 前向传播
outputs = model(inputs)

# 计算损失
loss = criterion(outputs, targets)

# 反向传播
loss.backward()

# 更新参数
optimizer.step()

# 清零梯度,为下一次迭代做准备
optimizer.zero_grad()

以上就是反向传播算法的基本介绍,其在深度学习训练中扮演着非常重要的角色,是理解和实现神经网络的基础知识。

Python手写反向传播

以下是一个简单的反向传播算法Python代码示例进行详细分步解释:

  1. 设定随机数种子和数据

    1
    2
    3
    np.random.seed(0)
    X = np.array([[0,0,1],[0,1,1],[1,0,1],[1,1,1]])
    y = np.array([[0,1,1,0]]).T

    这段代码首先设定了随机数种子,以确保每次运行程序时,初始化的权重值都是一样的。接着,我们设定了输入数据X和输出数据y

  2. 定义Sigmoid函数

    1
    2
    3
    4
    def sigmoid(x, deriv=False):
    if deriv:
    return x*(1-x)
    return 1/(1+np.exp(-x))

    这里我们定义了Sigmoid函数,该函数用于激活神经元的输出。在参数deriv为True时,该函数返回Sigmoid函数的导数,这对于反向传播计算梯度非常重要。

  3. 初始化权重

    1
    2
    w0 = 2*np.random.random((3,4)) - 1
    w1 = 2*np.random.random((4,1)) - 1

    在这里,我们初始化了权重w0w1。我们的网络有两层,所以需要两组权重。这些权重的初始化值是在-1到1之间随机选择的。

  4. 迭代训练

    1
    for j in range(60000):

    这是我们的训练循环,我们训练网络60000次。

  5. 前向传播

    1
    2
    3
    l0 = X
    l1 = sigmoid(np.dot(l0, w0))
    l2 = sigmoid(np.dot(l1, w1))

    这是我们的前向传播步骤。首先,我们的输入l0就是数据X。然后,我们计算l1层,这是l0w0的点积通过Sigmoid函数的结果。同样,我们计算l2,这是l1w1的点积通过Sigmoid函数的结果。

  6. 计算误差

    1
    l2_loss = y - l2

    在这里,我们计算了网络预测的误差,这就是实际值y减去预测值l2

  7. 打印误差

    1
    2
    if j % 10000 == 0:
    print(f'Loss: {np.mean(np.abs(l2_loss))}')

    每10000次迭代,我们计算并打印一次平均误差。

  8. 反向传播

    1
    2
    3
    l2_delta = l2_loss * sigmoid(l2, deriv=True)
    l1_loss = l2_delta.dot(w1.T)
    l1_delta = l1_loss * sigmoid(l1, deriv=True)

    这是反向传播的步骤。我们首先计算l2_delta,这是l2_lossl2通过Sigmoid导数函数的结果。然后,我们计算l1_loss,这是l2_deltaw1的转置的点积。最后,我们计算l1_delta,这是l1_lossl1通过Sigmoid导数函数的结果。

  9. 更新权重

    1
    2
    w1 += l1.T.dot(l2_delta)
    w0 += l0.T.dot(l1_delta)

    在这里,我们根据反向传播的结果更新权重。w1增加的是l1的转置和l2_delta的点积,w0增加的是l0的转置和l1_delta的点积。

这就是整个网络的训练过程,包括前向传播、计算误差、反向传播和更新权重。