神经网络基础
一、神经网络概述
1.1 什么是神经网络
深度学习神经网络本质上是对大脑的一种仿生。数据从输入到输出,经过一层又一层的神经元产生预测值的过程,称为前向传播(也叫正向传播)。
前向传播涉及人工神经元如何工作(包括神经元的初始化、激活函数),神经网络如何搭建,权重参数如何计算,以及数据形状如何变化。
人工神经网络(Artificial Neural Network,简称 ANN),也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。它由多个互相连接的人工神经元(也称为节点)构成,可以用于处理和学习复杂的数据模式,尤其适合解决非线性问题。人工神经网络是机器学习中的重要模型,在深度学习领域得到了广泛应用。
人脑可以看作是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号:树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。下图是生物神经元示意图:

当电信号通过树突进入细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。
1.2 如何构建神经网络
神经网络由多个神经元组成,构建神经网络就是在构建神经元。以下是神经网络中神经元的构建说明:

这个流程就像来自不同树突的信息(树突都有不同的权重)进行加权计算,输入到细胞中做加和,再通过激活函数输出细胞值。
同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。
接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本组成部分是:
全连接神经网络的特点:
- 同一层的神经元之间没有连接
- 第 N 层的每个神经元与第 N-1 层的所有神经元相连(这就是 Fully Connected 的含义),即全连接神经网络(FCNN)
- 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
- 第 N-1 层神经元的输出就是第 N 层神经元的输入
- 每个连接都有一个权重值(w 系数和 b 系数)
1.3 神经网络内部状态值和激活值

每一个神经元工作时,前向传播会产生两个值——内部状态值(加权求和值)和激活值;反向传播时会产生激活值梯度和内部状态值梯度。
内部状态值
- 神经元或隐藏单元的内部存储值,反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。
- 每个输入 xix_ixi 都有一个与之相乘的权重 wiw_iwi,表示每个输入信号的重要性。
- z=w⋅x+bz = w \\cdot x + bz=w⋅x+b
- www:权重矩阵
- xxx:输入值
- bbb:偏置
激活值
- 通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果,决定了当前神经元的输出。
- a=f(z)a = f(z)a=f(z)
- fff:激活函数
- zzz:内部状态值
通过控制每个神经元的内部状态值、激活值的大小,以及每一层的内部状态值的方差、每一层的激活值的方差,可以让整个神经网络更好地工作。
接下来,我们将学习神经元的激活函数和权重初始化。
二、激活函数
2.1 网络非线性因素理解
- 没有引入非线性因素的网络等价于使用一个线性模型来拟合
- 通过给网络输出增加激活函数,实现引入非线性因素,使得网络模型可以逼近任意函数,提升网络对复杂问题的拟合能力
激活函数用于对每层的输出数据进行变换,进而为整个网络注入非线性因素。此时,神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:


另外通过图像可视化的形式理解:
[神经网络可视化工具]

可以发现,增加激活函数之后,对于线性不可分的场景,神经网络的拟合能力更强。
2.2 常见激活函数
激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。网络参数在更新时使用反向传播算法(BP),这就要求激活函数必须可微。
2.2.1 Sigmoid 激活函数
激活函数公式:

激活函数求导公式:

Sigmoid 函数图像和导数图像:

Sigmoid 的特点与问题:
- 从函数图像可以看出,Sigmoid 函数可以将任意的输入映射到 (0, 1) 之间。当输入的值大致 < -6 或者 > 6 时,意味着输入任何值得到的激活值都差不多,这样会丢失部分信息。例如:输入 100 和输入 10000 经过 Sigmoid 的激活值几乎都等于 1,但输入数据之间相差 100 倍的信息就丢失了。
- 对于 Sigmoid 函数而言,输入值在 [-6, 6] 之间输出值才会有明显差异,输入值在 [-3, 3] 之间才会有比较好的效果。
- 通过导数图像可以发现,导数数值范围是 (0, 0.25),当输入的值 < -6 或者 > 6 时,Sigmoid 导数接近为 0,此时网络参数将更新极其缓慢,或者无法更新。
- 一般来说,Sigmoid 网络在 5 层之内就会产生梯度消失现象。而且,该激活函数的激活值并不是以 0 为中心的,激活值总是偏向正数,导致梯度更新时只会对某些特征产生相同方向的影响。因此,在实践中这种激活函数使用得很少。Sigmoid 函数一般只用于二分类的输出层。
PyTorch 实现 Sigmoid 的示例代码:
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
"""
绘制激活函数图像时出现以下提示,需要将 anaconda3/Lib/site-packages/torch/lib 目录下的 libiomp5md.dll 文件删除
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
"""
# 创建画布和坐标轴
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(–20, 20, 1000)
# 输入值 x 通过 sigmoid 函数转换成激活值 y
y = torch.sigmoid(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Sigmoid 函数图像')
# 导数图像
x = torch.linspace(–20, 20, 1000, requires_grad=True)
torch.sigmoid(x).sum().backward()
# x.detach(): 输入值 x 的 ndarray 数组
# x.grad: 计算梯度,求导
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Sigmoid 导数图像')
plt.show()
2.2.2 Tanh 激活函数
Tanh 称为双曲正切函数,其公式如下:

求导公式:

Tanh 函数图像和导数图像:

Tanh 的特点:
- 由函数图像可以看到,Tanh 函数将输入映射到 (-1, 1) 之间,图像以 0 为中心,激活值在 0 点对称。当输入的值大概 < -3 或者 > 3 时将被映射为 -1 或 1。其导数值范围 (0, 1),当输入的值大概 < -3 或者 > 3 时,其导数近似为 0。
- 与 Sigmoid 相比,它是以 0 为中心的,使得其收敛速度要比 Sigmoid 快,减少迭代次数。然而,从图中可以看出,Tanh 两侧的导数也为 0,同样会造成梯度消失。
- 若使用,可在隐藏层使用 tanh 函数,在输出层使用 sigmoid 函数。
PyTorch 实现 Tanh 的示例代码:
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(–20, 20, 1000)
y = torch.tanh(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')
# 导数图像
x = torch.linspace(–20, 20, 1000, requires_grad=True)
torch.tanh(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')
plt.show()
2.2.3 ReLU 激活函数
ReLU 激活函数公式:

求导公式:

ReLU 函数图像和导数图像:

ReLU 的特点:
- ReLU 激活函数将小于 0 的值映射为 0,而大于 0 的值则保持不变。它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
- 当 x<0x < 0x<0 时,ReLU 导数为 0;而当 x>0x > 0x>0 时,则不存在饱和问题。所以,ReLU 能够在 x>0x > 0x>0 时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于 0 的区域,导致对应权重无法更新,这种现象被称为"神经元死亡"。
- ReLU 是目前最常用的激活函数。与 Sigmoid 相比,ReLU 的优势是:
- Sigmoid 函数计算量大(指数运算),反向传播求误差梯度时计算量也大;而 ReLU 激活函数整个过程的计算量节省很多。
- Sigmoid 函数反向传播时很容易出现梯度消失,从而无法完成深层网络的训练;而 ReLU 激活函数当输入 x>0x > 0x>0 时,梯度为 1,不会出现梯度消失的情况。
- ReLU 会使一部分神经元的输出为 0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。
PyTorch 实现 ReLU 的示例代码:
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(–20, 20, 1000)
y = torch.relu(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('ReLU 函数图像')
# 导数图像
x = torch.linspace(–20, 20, 1000, requires_grad=True)
torch.relu(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('ReLU 导数图像')
plt.show()
2.2.4 SoftMax 激活函数
SoftMax 用于多分类过程中,它是二分类函数 Sigmoid 在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。
计算方法如下图所示:


SoftMax 就是将网络输出的 logits 通过 softmax 函数,映射成为 (0, 1) 的值,而这些值的累和为 1(满足概率的性质)。我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。
PyTorch 实现 SoftMax 的示例代码:
import torch
scores = torch.tensor([0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75])
# dim=0, 按行计算
probabilities = torch.softmax(scores, dim=0)
print(probabilities)
程序输出结果:
tensor([0.0212, 0.0177, 0.0202, 0.0202, 0.0638, 0.0287, 0.0185, 0.0522, 0.0183,
0.7392])
2.3 如何选择激活函数
除了上述的激活函数,还存在很多其他的激活函数,如下图所示:

对于隐藏层:
对于输出层:
三、参数初始化
构建网络之后,网络中的参数需要进行初始化。需要初始化的参数主要有权重和偏置,偏置一般初始化为 0 即可,而对权重的初始化则更加重要。
参数初始化的作用:
- 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
- 提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
- 保持对称性破除:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。
3.1 常见参数初始化方法
随机初始化
-
均匀分布初始化:权重参数从区间均匀随机取值,默认区间为 (0,1)(0, 1)(0,1)。可以设置为在 (−1d,1d)(-\\frac{1}{\\sqrt{d}}, \\frac{1}{\\sqrt{d}})(−d1,d1) 均匀分布中生成当前神经元的权重,其中 ddd 为神经元的输入数量。

-
正态分布初始化:从均值为 0、标准差为 1 的高斯分布中取样,使用一些很小的值对参数 W 进行初始化。
-
优点:能有效打破对称性
-
缺点:随机选择范围不当可能导致梯度问题
-
适用场景:浅层网络或低复杂度模型(隐藏层 1-3 层,总层数不超过 5 层)
全 0 初始化
将神经网络中的所有权重参数初始化为 0。
- 优点:实现简单
- 缺点:无法打破对称性,所有神经元更新方向相同,无法有效训练
- 适用场景:几乎不使用,仅用于偏置项的初始化
全 1 初始化
将神经网络中的所有权重参数初始化为 1。
- 优点:实现简单
- 缺点:
- 无法打破对称性,所有神经元更新方向相同,无法有效训练
- 会导致激活值在网络中呈指数增长,容易出现梯度爆炸
- 适用场景:
- 测试或调试:比如验证神经网络是否能正常前向传播和反向传播
- 特殊模型结构:某些稀疏网络或特定的自定义网络中可能需要手动设置部分参数为 1
- 偏置初始化:偶尔可以将偏置初始化为小的正值(如 0.1),但很少用 1 作为偏置的初始值
固定值初始化
将神经网络中的所有权重参数初始化为某个固定值。
- 优点:实现简单
- 缺点:
- 无法打破对称性,所有神经元更新方向相同,无法有效训练
- 初始权重过大或过小可能导致梯度爆炸或梯度消失
- 适用场景:测试或调试
Kaiming 初始化(HE 初始化)
专为 ReLU 和其变体设计,考虑到 ReLU 激活函数的特性,对输入维度进行缩放。
-
HE 初始化分为正态分布的 HE 初始化和均匀分布的 HE 初始化:
- 正态分布的 HE 初始化:www 权重值从均值为 0、标准差为 std 中随机采样,std=2/fan_in\\text{std} = \\sqrt{2 / \\text{fan\\_in}}std=2/fan_in。std 值越大,www 权重值离均值 0 分布相对越广,计算得到的内部状态值有较大的正值或负值。
- 均匀分布的 HE 初始化:从 [−limit,limit][-\\text{limit}, \\text{limit}][−limit,limit] 中的均匀分布中抽取样本,limit=6/fan_in\\text{limit} = \\sqrt{6 / \\text{fan\\_in}}limit=6/fan_in。
- fan_in\\text{fan\\_in}fan_in 是输入神经元的个数,即当前层接受的来自上一层的神经元的数量。
-
优点:适合 ReLU,能保持梯度稳定
-
缺点:对非 ReLU 激活函数效果一般
-
适用场景:深度网络(10 层及以上),使用 ReLU、Leaky ReLU 激活函数
Xavier 初始化(Glorot 初始化)
根据网络输入和输出的维度自动选择权重范围,使输入和输出的方差相同。
-
Xavier 初始化分为正态分布的 Xavier 初始化和均匀分布的 Xavier 初始化:
- 正态分布的 Xavier 初始化:www 权重值从均值为 0、标准差为 std 中随机采样,std=2/(fan_in+fan_out)\\text{std} = \\sqrt{2 / (\\text{fan\\_in} + \\text{fan\\_out})}std=2/(fan_in+fan_out)。std 值越小,www 权重值离均值 0 分布相对越集中,计算得到的内部状态值有较小的正值或负值。
- 均匀分布的 Xavier 初始化:从 [−limit,limit][-\\text{limit}, \\text{limit}][−limit,limit] 中的均匀分布中抽取样本,limit=6/(fan_in+fan_out)\\text{limit} = \\sqrt{6 / (\\text{fan\\_in} + \\text{fan\\_out})}limit=6/(fan_in+fan_out)。
- fan_in\\text{fan\\_in}fan_in 是输入神经元个数,即当前层接受的来自上一层的神经元的数量。
- fan_out\\text{fan\\_out}fan_out 是输出神经元个数,即当前层输出的神经元的数量,也就是当前层会传递给下一层的神经元的数量。
-
优点:适用于 Sigmoid、Tanh 等激活函数,解决梯度消失问题
-
缺点:对 ReLU 等激活函数表现欠佳
-
适用场景:深度网络(10 层及以上),使用 Sigmoid 或 Tanh 激活函数
PyTorch 实现各种初始化的代码示例:
import torch.nn as nn
# 1. 均匀分布随机初始化
def test01():
linear = nn.Linear(5, 3)
# 从 0-1 均匀分布产生参数
nn.init.uniform_(linear.weight)
nn.init.uniform_(linear.bias)
print(linear.weight.data)
# 2. 固定初始化
def test02():
linear = nn.Linear(5, 3)
nn.init.constant_(linear.weight, 5)
print(linear.weight.data)
# 3. 全 0 初始化
def test03():
linear = nn.Linear(5, 3)
nn.init.zeros_(linear.weight)
print(linear.weight.data)
# 4. 全 1 初始化
def test04():
linear = nn.Linear(5, 3)
nn.init.ones_(linear.weight)
print(linear.weight.data)
# 5. 正态分布随机初始化
def test05():
linear = nn.Linear(5, 3)
nn.init.normal_(linear.weight, mean=0, std=1)
print(linear.weight.data)
# 6. Kaiming 初始化
def test06():
# kaiming 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_normal_(linear.weight, nonlinearity='relu')
print(linear.weight.data)
# kaiming 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')
print(linear.weight.data)
# 7. Xavier 初始化
def test07():
# xavier 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_normal_(linear.weight)
print(linear.weight.data)
# xavier 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight.data)
3.2 如何选择参数初始化
-
根据激活函数选择:
- Sigmoid/Tanh:Xavier 初始化
- ReLU/Leaky ReLU:Kaiming 初始化
-
根据网络深度:
- 浅层网络:随机初始化即可
- 深层网络:需要考虑方差平衡,如 Xavier 或 Kaiming 初始化
四、神经网络搭建和参数计算
4.1 构建神经网络
在 PyTorch 中定义深度神经网络其实就是层堆叠的过程,继承自 nn.Module,实现两个方法:
- __init__ 方法中定义网络中的层结构,主要是全连接层,并进行初始化
- forward 方法:在调用神经网络模型对象时,底层会自动调用该函数,为初始化定义的 layer 传入数据,进行前向传播等
接下来我们来构建如下图所示的神经网络模型:

编码设计如下:
- 第 1 个隐藏层:权重初始化采用标准化的 Xavier 初始化,激活函数使用 Sigmoid
- 第 2 个隐藏层:权重初始化采用标准化的 HE 初始化,激活函数采用 ReLU
- 输出层:线性层,若为多分类,采用 SoftMax 做数据归一化
构造神经网络模型代码:
import torch
import torch.nn as nn
from torchsummary import summary # 计算模型参数,查看模型结构
# 创建神经网络模型类
class Model(nn.Module):
# 初始化属性值
def __init__(self):
# 调用父类的初始化属性值,确保 nn.Module 的初始化代码能够正确执行
super(Model, self).__init__()
# 创建第一个隐藏层模型,3 个输入特征,3 个输出特征
self.linear1 = nn.Linear(3, 3)
# 初始化权重
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)
# 创建第二个隐藏层模型,3 个输入特征(上一层的输出特征),2 个输出特征
self.linear2 = nn.Linear(3, 2)
# 初始化权重
nn.init.kaiming_normal_(self.linear2.weight, nonlinearity='relu')
nn.init.zeros_(self.linear2.bias)
# 创建输出层模型
self.out = nn.Linear(2, 2)
# 创建前向传播方法,调用神经网络模型对象时自动执行 forward() 方法
def forward(self, x):
# 数据经过第一个线性层
x = self.linear1(x)
# 使用 sigmoid 激活函数
x = torch.sigmoid(x)
# 数据经过第二个线性层
x = self.linear2(x)
# 使用 relu 激活函数
x = torch.relu(x)
# 数据经过输出层
x = self.out(x)
# 使用 softmax 激活函数
# dim=-1: 每一维度行数据相加为 1
x = torch.softmax(x, dim=–1)
return x
训练神经网络模型代码:
# 创建构造模型函数
def train():
# 实例化 model 对象
my_model = Model()
# 随机产生数据
my_data = torch.randn(5, 3)
print("my_data–>", my_data)
print("my_data shape", my_data.shape)
# 数据经过神经网络模型训练
output = my_model(my_data)
print("output–>", output)
print("output shape–>", output.shape)
# 计算模型参数
# 计算每层每个神经元的 w 和 b 个数总和
print("======计算模型参数======")
summary(my_model, input_size=(3,), batch_size=5)
# 查看模型参数
print("======查看模型参数 w 和 b======")
for name, parameter in my_model.named_parameters():
print(name, parameter)
if __name__ == '__main__':
train()
4.2 观察数据形状变化
- 输入 5 行数据,输出也是 5 行数据
- 输入 5 行数据 3 个特征,经过第一个隐藏层是 3 个特征,经过第二个隐藏层是 2 个特征,经过输出层是 2 个特征
- 模型最终预测结果是:5 行 2 列数据
mydata.shape—> torch.Size([5, 3])
output.shape—> torch.Size([5, 2])
mydata—>
tensor([[-0.3714, -0.8578, -1.6988],
[ 0.3149, 0.0142, -1.0432],
[ 0.5374, -0.1479, -2.0006],
[ 0.4327, -0.3214, 1.0928],
[ 2.2156, -1.1640, 1.0289]])
output—>
tensor([[0.5095, 0.4905],
[0.5218, 0.4782],
[0.5419, 0.4581],
[0.5163, 0.4837],
[0.6030, 0.3970]], grad_fn=<SoftmaxBackward>)
4.3 模型参数计算
以第一个隐层为例:该隐层有 3 个神经元,每个神经元的参数为 4 个(w1, w2, w3, b1),所以一共有 3×4=123 \\times 4 = 123×4=12 个参数。
输入数据和网络权重是两个不同的事情!对于初学者来说,理解这一点十分重要。

—————————————————————-
Layer (type) Output Shape Param #
================================================================
Linear-1 [5, 3] 12
Linear-2 [5, 2] 8
Linear-3 [5, 2] 6
================================================================
Total params: 26
Trainable params: 26
Non-trainable params: 0
—————————————————————-
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
—————————————————————-
4.4 查看模型参数
通常继承 nn.Module 撰写自己的网络层,其强大的封装不需要我们定义可学习的参数(比如卷积核的权重和偏置参数)。那么,如何查看封装好的可学习网络参数呢?
通过 模块实例名.named_parameters() 方法,会分别返回 name 和 parameter。
# 实例化 model 对象
mymodel = Model()
# 查看网络参数
for name, parameter in mymodel.named_parameters():
# print('name—>', name)
# print('parameter—>', parameter)
print(name, parameter)
结果显示:
linear1.weight Parameter containing:
tensor([[ 0.1715, -0.3711, 0.1692],
[-0.2497, -0.6156, -0.4235],
[-0.7090, -0.0380, 0.4790]], requires_grad=True)
linear1.bias Parameter containing:
tensor([-0.2320, 0.3431, 0.2771], requires_grad=True)
linear2.weight Parameter containing:
tensor([[-0.5044, -0.7435, -0.6736],
[ 0.6908, -0.1466, -0.0019]], requires_grad=True)
linear2.bias Parameter containing:
tensor([0.2340, 0.4730], requires_grad=True)
out.weight Parameter containing:
tensor([[ 0.5185, 0.4019],
[-0.4313, -0.3438]], requires_grad=True)
out.bias Parameter containing:
tensor([ 0.4521, -0.6339], requires_grad=True)
五、损失函数
5.1 损失函数概念
在深度学习中,损失函数是用来衡量模型参数质量的函数,衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。
模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。

损失函数在不同的文献中名称有所不同,主要有以下几种命名方式:

损失函数的作用:
- 评估性能:反映模型预测结果与目标值的匹配程度
- 指导优化:通过梯度下降等算法最小化损失函数,优化模型参数
5.2 分类任务损失函数
在深度学习的分类任务中使用最多的是交叉熵损失函数,这里我们着重介绍这种损失函数。
5.2.1 多分类任务损失函数
在多分类任务中,通常使用 SoftMax 将 logits 转换为概率的形式,所以多分类的交叉熵损失也叫做 SoftMax 损失,其计算方法如下:

其中:
- yiy_iyi 是样本 xxx 属于某一个类别的真实概率
- f(x)f(x)f(x) 是样本属于某一类别的预测分数
- SSS 是 SoftMax 激活函数,将属于某一类别的预测分数转换成概率
- LLL 用来衡量真实值 yyy 和预测值 f(x)f(x)f(x) 之间差异性的损失结果
例子:

上图中的交叉熵损失为:

从概率角度理解,我们的目的是最小化正确类别所对应的预测概率的对数的负值(损失值最小),如下图所示:

PyTorch 实现多分类交叉熵损失:
import torch
from torch import nn
# 分类损失函数:交叉熵损失使用 nn.CrossEntropyLoss() 实现
# nn.CrossEntropyLoss() = softmax + 损失计算
def test01():
# 设置真实值:可以是 one-hot 编码后的结果,也可以不进行 one-hot 编码
# y_true = torch.tensor([[0, 1, 0], [0, 0, 1]], dtype=torch.float32)
# 注意:类型必须是 64 位整型数据
y_true = torch.tensor([1, 2], dtype=torch.int64)
y_pred = torch.tensor([[0.2, 0.6, 0.2], [0.1, 0.8, 0.1]],
requires_grad=True, dtype=torch.float32)
# 实例化交叉熵损失,默认求平均损失
# reduction='sum':总损失
loss = nn.CrossEntropyLoss()
# 计算损失结果
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
5.2.2 二分类任务损失函数
在处理二分类任务时,不再使用 SoftMax 激活函数,而是使用 Sigmoid 激活函数,损失函数也相应调整,使用二分类的交叉熵损失函数:

其中:
- yyy 是样本 xxx 属于某一个类别的真实概率
- y^\\hat{y}y^ 是样本属于某一类别的预测概率
- LLL 用来衡量真实值 yyy 与预测值 y^\\hat{y}y^ 之间差异性的损失结果

PyTorch 实现二分类交叉熵损失:
import torch
from torch import nn
def test02():
# 1. 设置真实值和预测值
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
# 预测值是 sigmoid 输出的结果
y_pred = torch.tensor([0.6901, 0.5459, 0.2469], requires_grad=True)
# 2. 实例化二分类交叉熵损失
loss = nn.BCELoss()
# 3. 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
5.3 回归任务损失函数
5.3.1 MAE 损失函数
Mean Absolute Loss(MAE) 也被称为 L1 Loss,以绝对误差作为距离。
损失函数公式:

曲线图:

特点:
- 由于 L1 loss 具有稀疏性,为了惩罚较大的值,常常将其作为正则项添加到其他 loss 中作为约束(0 点不可导,产生稀疏矩阵)。
- L1 loss 的最大问题是梯度在零点不平滑,导致会跳过极小值。
- 适用于回归问题中存在异常值或噪声数据时,可以减少对离群点的敏感性。
PyTorch 实现 MAE 损失:
import torch
from torch import nn
# 计算 inputs 与 target 之差的绝对值
def test03():
# 1. 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2. 实例化 MAE 损失对象
loss = nn.L1Loss()
# 3. 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
5.3.2 MSE 损失函数
Mean Squared Loss / Quadratic Loss(MSE loss) 也被称为 L2 loss 或欧氏距离,它以误差的平方和的均值作为距离。
损失函数公式:

曲线图:

特点:
- L2 loss 也常常作为正则项,对于离群点(outliers)敏感,因为平方项会放大大误差。
- 当预测值与目标值相差很大时,梯度容易爆炸(梯度爆炸:网络层之间的梯度值大于 1.0 重复相乘导致的指数级增长)。
- 适用于大多数标准回归问题,如房价预测、温度预测等。
PyTorch 实现 MSE 损失:
import torch
from torch import nn
def test04():
# 1. 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2. 实例化 MSE 损失对象
loss = nn.MSELoss()
# 3. 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('myloss:', my_loss)
5.3.3 Smooth L1 损失函数
Smooth L1 指的是光滑之后的 L1,是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE,在误差较大时则更像 MAE。
Smooth L1 损失函数公式:

其中:x=f(x)−yx = f(x) – yx=f(x)−y 为真实值和预测值的差值。

从上图可以看出,该函数实际上就是一个分段函数:
- 在 [−1,1][-1, 1][−1,1] 之间实际上就是 L2 损失,这样解决了 L1 的不光滑问题
- 在 [−1,1][-1, 1][−1,1] 区间外,实际上就是 L1 损失,这样就解决了离群点梯度爆炸的问题
特点:
- 对离群点更加鲁棒:当误差较大时,损失函数会线性增加(而不是像 MSE 那样平方增加),因此它对离群点的惩罚更小,避免了 MSE 对离群点过度敏感的问题。
- 计算梯度时更加平滑:与 MAE 相比,Smooth L1 在小误差时表现得像 MSE,避免了在训练过程中因使用绝对误差而导致的梯度不连续问题。
PyTorch 实现 Smooth L1 损失:
import torch
from torch import nn
def test05():
# 1. 设置真实值和预测值
y_true = torch.tensor([0, 3])
y_pred = torch.tensor([0.6, 0.4], requires_grad=True)
# 2. 实例化 Smooth L1 损失对象
loss = nn.SmoothL1Loss()
# 3. 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
六、神经网络优化方法
多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation,BP) 是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练。值得指出的是,BP 算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说 BP 网络时,一般是指用 BP 算法训练的多层前馈神经网络。
这里需要了解两个概念:
6.1 梯度下降算法回顾
梯度下降法简单来说就是一种寻找使损失函数最小化的方法。
从数学角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向,所以有:

其中,η\\etaη 是学习率。如果学习率太小,那么每次训练之后得到的效果都太小,增加训练的时间成本。如果学习率太大,那就有可能直接跳过最优解,进入无限的训练中。解决的方法是,学习率也需要随着训练的进行而变化。

在进行模型训练时,有三个基础的概念:
假设数据集有 50000 个训练样本,现在选择 Batch Size = 256 对模型进行训练:
- 每个 Epoch 要训练的图片数量:50000
- 训练集具有的 Batch 个数:50000 / 256 + 1 = 196
- 每个 Epoch 具有的 Iteration 个数:196
- 10 个 Epoch 具有的 Iteration 个数:1960
6.2 反向传播(BP 算法)
利用反向传播算法对神经网络进行训练。该方法与梯度下降算法相结合,对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。
6.2.1 反向传播概念
前向传播:数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。
反向传播(Back Propagation):利用损失函数 ERROR 值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。

在网络的训练过程中,经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数 ERROR。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)。
6.2.2 反向传播详解
反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新。
【举个栗子🌰】
如下图是一个简单的神经网络,激活函数为 Sigmoid:

前向传播运算:

反向传播(求网络误差对各个权重参数的梯度):
我们先来求最简单的——求误差 EEE 对 w5w_5w5 的导数。首先明确这是一个链式法则的求导过程,要求误差 EEE 对 w5w_5w5 的导数,需要先求误差 EEE 对 outo1out_{o1}outo1 的导数,再求 outo1out_{o1}outo1 对 neto1net_{o1}neto1 的导数,最后再求 neto1net_{o1}neto1 对 w5w_5w5 的导数。经过这个链式法则,我们就可以求出误差 EEE 对 w5w_5w5 的导数(偏导),如下图所示:

导数(梯度)已经计算出来,下面就是反向传播与参数更新过程:

如果要求误差 EEE 对 w1w_1w1 的导数,由于误差 EEE 对 w1w_1w1 的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:

至此,反向传播算法的过程就讲完啦!
6.3 梯度下降优化方法
梯度下降优化算法中,可能会碰到以下情况:
- 碰到平缓区域,梯度值较小,参数优化变慢
- 碰到"鞍点",梯度为 0,参数无法优化
- 碰到局部最小值,参数不是最优
对于这些问题,出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSProp、Adam 等。

6.3.1 指数加权平均
最常见的算术平均指的是将所有数加起来除以数的个数,每个数的权重相同。指数加权平均指的是给每个数赋予不同的权重求得平均数。移动平均数指的是计算最近邻的 N 个数来获得平均数。
指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。
比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。
计算公式:

- StS_tSt 表示指数加权平均值
- YtY_tYt 表示 ttt 时刻的值
- β\\betaβ 为调节权重系数,该值越大平均数越平缓
第 100 天的指数加权平均值为:

下面通过代码来看结果,随机产生 30 天的气温数据:
import torch
import matplotlib.pyplot as plt
ELEMENT_NUMBER = 30
# 1. 实际平均温度
def test01():
# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
# 绘制平均温度
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, temperature, color='r')
plt.scatter(days, temperature)
plt.show()
# 2. 指数加权平均温度
def test02(beta=0.9):
# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
exp_weight_avg = []
# idx从1开始
for idx, temp in enumerate(temperature, 1):
# 第一个元素的 EWA 值等于自身
if idx == 1:
exp_weight_avg.append(temp)
continue
# 第二个元素的 EWA 值等于上一个 EWA 乘以 β + 当前气温乘以 (1-β)
# idx-2:2-2=0,exp_weight_avg 列表中第一个值的下标值
new_temp = exp_weight_avg[idx – 2] * beta + (1 – beta) * temp
exp_weight_avg.append(new_temp)
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, exp_weight_avg, color='r')
plt.scatter(days, temperature)
plt.show()
if __name__ == '__main__':
test01()
test02(0.5)
test02(0.9)

从程序运行结果可以看到:
- 指数加权平均绘制出的气温变化曲线更加平缓
- β\\betaβ 的值越大,则绘制出的折线越加平缓,波动越小(1−β1-\\beta1−β 越小,ttt 时刻的 StS_tSt 越不依赖 YtY_tYt 的值)
- β\\betaβ 值一般默认都是 0.9
6.3.2 动量算法 Momentum
当梯度下降碰到"峡谷"、“平缓”、"鞍点"区域时,参数更新速度变慢。Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。
梯度计算公式:
st=βst−1+(1−β)gts_t = \\beta s_{t-1} + (1-\\beta)g_tst=βst−1+(1−β)gt
参数更新公式:
wt=wt−1−ηstw_t = w_{t-1} – \\eta s_twt=wt−1−ηst
其中:
- sts_tst 是当前时刻指数加权平均梯度值
- st−1s_{t-1}st−1 是历史指数加权平均梯度值
- gtg_tgt 是当前时刻的梯度值
- β\\betaβ 是调节权重系数,通常取 0.9 或 0.99
- η\\etaη 是学习率
- wtw_twt 是当前时刻模型权重参数
咱们举个例子,假设:权重 β 为 0.9,例如:
第一次梯度值:s1 = g1 = w1
第二次梯度值:s2 = 0.9 * s1 + g2 * 0.1
第三次梯度值:s3 = 0.9 * s2 + g3 * 0.1
第四次梯度值:s4 = 0.9 * s3 + g4 * 0.1
1. w 表示初始梯度
2. g 表示当前轮数计算出的梯度值
3. s 表示历史梯度移动加权平均值
梯度下降公式中梯度的计算,就不再是当前时刻 t 的梯度值,而是历史梯度值的指数移动加权平均值。
公式修改为:
Wt = Wt-1 – η * St
Wt:当前时刻模型权重参数
St:当前时刻指数加权平均梯度值
η:学习率
Momentum 优化方法是如何克服"平缓"、“鞍点”、"峡谷"问题的?

- 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
- 由于 mini-batch 普通的梯度下降算法每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程,一定程度上有利于降低"峡谷"问题的影响。
PyTorch 实现 Momentum 梯度优化:
def test01():
# 1. 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2. 实例化优化方法:SGD 指定参数 beta=0.9
optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)
# 3. 第1次更新,计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4. 第2次更新,计算梯度,并对参数进行更新
# 使用更新后的参数计算输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.971100
6.3.3 AdaGrad
AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小。这是因为 AdaGrad 认为:在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。
计算步骤如下:
AdaGrad 的缺点: 可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。
PyTorch 实现 AdaGrad 优化:
def test02():
# 1. 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2. 实例化优化方法:adagrad 优化方法
optimizer = torch.optim.Adagrad([w], lr=0.01)
# 3. 第1次更新,计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4. 第2次更新,计算梯度,并对参数进行更新
# 使用更新后的参数计算输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.982965
6.3.4 RMSProp
RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。
计算过程如下:
RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。
RMSProp 通过引入衰减系数 β\\betaβ,控制历史梯度对历史梯度信息获取的多少,被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。
需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。
PyTorch 实现 RMSProp 优化:
def test03():
# 1. 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2. 实例化优化方法:RMSprop 算法,其中 alpha 对应 beta
optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)
# 3. 第1次更新,计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4. 第2次更新,计算梯度,并对参数进行更新
# 使用更新后的参数计算输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.968377
第2次: 梯度w.grad: 0.968377, 更新后的权重:0.945788
6.3.5 Adam
- Momentum 使用指数加权平均计算当前的梯度值
- AdaGrad、RMSProp 使用自适应的学习率
- Adam 优化算法(Adaptive Moment Estimation,自适应矩估计) 将 Momentum 和 RMSProp 算法结合在一起
- 修正梯度:使用梯度的指数加权平均
- 修正学习率:使用梯度平方的指数加权平均
原理:Adam 是结合了 Momentum 和 RMSProp 优化算法优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。
梯度计算公式:
mt=β1mt−1+(1−β1)gtm_t = \\beta_1 m_{t-1} + (1-\\beta_1)g_tmt=β1mt−1+(1−β1)gt
st=β2st−1+(1−β2)gt2s_t = \\beta_2 s_{t-1} + (1-\\beta_2)g_t^2st=β2st−1+(1−β2)gt2
mt^=mt1−β1t,st^=st1−β2t\\hat{m_t} = \\frac{m_t}{1-\\beta_1^t}, \\quad \\hat{s_t} = \\frac{s_t}{1-\\beta_2^t}mt^=1−β1tmt,st^=1−β2tst
权重参数更新公式:
wt=wt−1−ηst^+ϵmt^w_t = w_{t-1} – \\frac{\\eta}{\\sqrt{\\hat{s_t}} + \\epsilon} \\hat{m_t}wt=wt−1−st^+ϵηmt^
其中,mtm_tmt 是梯度的一阶矩估计,sts_tst 是梯度的二阶矩估计,mt^\\hat{m_t}mt^ 和 st^\\hat{s_t}st^ 是偏差校正后的估计。
PyTorch 实现 Adam 优化:
def test04():
# 1. 初始化权重参数
w = torch.tensor([1.0], requires_grad=True)
loss = ((w ** 2) / 2.0).sum()
# 2. 实例化优化方法:Adam 算法,其中 betas 是指数加权的系数
optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])
# 3. 第1次更新,计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4. 第2次更新,计算梯度,并对参数进行更新
# 使用更新后的参数计算输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
结果显示:
第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.980003
6.3.6 优化方法小结
| SGD | 简单、容易实现 | 收敛速度较慢,容易震荡,特别是在复杂问题中 | 用于简单任务,或者当数据特征分布相对稳定时 |
| Momentum | 可以加速收敛,减少震荡,特别是在高曲率区域 | 需要手动调整动量超参数,可能会在小步长训练中过度更新 | 用于非平稳优化问题,尤其是深度学习中的应用 |
| AdaGrad | 自适应调整学习率,适用于稀疏数据 | 学习率会在训练过程中逐渐衰减,可能导致早期停滞 | 适合稀疏数据,如 NLP 或推荐系统中的特征 |
| RMSProp | 解决了 AdaGrad 学习率过早衰减的问题,适应性强 | 需要选择合适的超参数,更新可能会过于激进 | 适用于动态问题、非平稳目标函数,如深度学习训练 |
| Adam | 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定 | 需要调节更多的超参数,训练过程中可能会产生较大波动 | 广泛适用于各种深度学习任务,特别是非平稳和复杂问题 |
选择建议:
- 简单任务和较小的模型:SGD 或 Momentum
- 复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀
- 需要处理稀疏数据或文本数据:Adagrad 或 RMSProp
七、学习率衰减优化方法
7.1 为什么要进行学习率优化
在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成 loss 的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。
运行下面代码,观察学习率设置不同对网络训练的影响:
import torch
import matplotlib.pyplot as plt
# x 看成是权重,y 看成是 loss,下面通过代码来理解学习率的作用
def func(x_t):
return torch.pow(2*x_t, 2) # y = 4 * x^2
# 采用较小的学习率,梯度下降的速度慢
# 采用较大的学习率,梯度下降太快越过了最小值点,导致不收敛,甚至震荡
def dm01():
x = torch.tensor([2.], requires_grad=True)
# 记录 loss 迭代次数,画曲线
iter_rec, loss_rec, x_rec = list(), list(), list()
# 实验学习率:0.01 0.02 0.03 0.1 0.2 0.3 0.4
# lr = 0.1 # 正常的梯度下降
# lr = 0.125 # 当学习率设置0.125,一下子求出一个最优解
# # x=0 y=0,在x=0处梯度等于0,x的值 x=x-lr*x.grad 就不用更新了
# # 后续再多少次迭代,都固定在最优点
lr = 0.2 # x从2.0一下子跨过0点,到了左侧负数区域
# lr = 0.3 # 梯度越来越大,梯度爆炸
max_iteration = 4
for i in range(max_iteration):
y = func(x) # 得出 loss 值
y.backward() # 计算 x 的梯度
print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))
x_rec.append(x.item()) # 梯度下降点列表
# 更新参数
x.data.sub_(lr * x.grad) # x = x – x.grad
x.grad.zero_()
iter_rec.append(i) # 迭代次数列表
loss_rec.append(y.item()) # 损失值列表
# 迭代次数-损失值关系图
plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.grid()
plt.xlabel("Iteration X")
plt.ylabel("Loss value Y")
# 函数曲线-下降轨迹显示图
x_t = torch.linspace(–3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.detach().numpy(), y.detach().numpy(), label="y = 4*x^2")
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
print('x_rec—>', x_rec)
print('y_rec—>', y_rec)
# 指定线的颜色和样式(-ro:红色圆圈,b-:蓝色实线等)
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.grid()
plt.legend()
plt.show()
dm01()
运行效果图如下:
可以看出:采用较小的学习率,梯度下降的速度慢;采用较大的学习率,梯度下降太快越过了最小值点,导致震荡,甚至不收敛(梯度爆炸)。

7.2 等间隔学习率衰减
等间隔学习率衰减方式如下所示:

PyTorch 实现:
# step_size:调整间隔数=50
# gamma:调整系数=0.5
# 调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)
具体使用方式:
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_StepLR():
# 0. 参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1. 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2. 优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3. 设置学习率下降策略
scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
# 4. 获取学习率的值和当前的epoch
lr_list, epoch_list = [], []
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr()) # 获取当前lr
epoch_list.append(epoch) # 获取当前的epoch
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x–y_true)**2 # 目标函数
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5. 绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
7.3 指定间隔学习率衰减
指定间隔学习率衰减的效果如下:

PyTorch 实现:
# milestones:设定调整轮次 [50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=–1)
具体使用方式:
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_MultiStepLR():
torch.manual_seed(1)
LR = 0.1
iteration = 10
max_epoch = 200
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 设定调整时刻数
milestones = [50, 125, 160]
# 设置学习率下降策略
scheduler_lr = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration):
loss = (w*x–y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\\nmilestones:{}".format(milestones))
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
7.4 按指数学习率衰减
按指数衰减调整学习率的效果如下:

PyTorch 实现:
# gamma:指数的底
# 调整方式:lr = lr * gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)
具体使用方式:
import torch
from torch import optim
import matplotlib.pyplot as plt
def test_ExponentialLR():
# 0. 参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1. 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2. 优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3. 设置学习率下降策略
gamma = 0.95
scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=gamma)
# 4. 获取学习率的值和当前的epoch
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x–y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5. 绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()
7.5 学习率衰减方法小结
| 衰减方式 | 固定步长衰减 | 指定步长衰减 | 平滑指数衰减,历史平均考虑 |
| 实现难度 | 简单易实现 | 相对简单,容易调整 | 需要额外历史计算,较复杂 |
| 适用场景 | 大型数据集、较为简单的任务 | 对训练平稳性要求较高的任务 | 高精度训练,避免过快收敛 |
| 优点 | 直观,易于调试,适用于大批量数据 | 易于调试,稳定训练过程 | 平滑且考虑历史更新,收敛稳定性较强 |
| 缺点 | 学习率变化较大,可能跳过最优点 | 在某些情况下可能衰减过快,导致优化提前停滞 | 超参数调节较为复杂,可能需要更多的计算资源 |
八、正则化方法
8.1 什么是正则化

- 在设计机器学习算法时,希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化。
- 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略。
- 目前在深度学习中使用较多的策略有范数惩罚、Dropout、特殊的网络层等,接下来我们对其进行详细介绍。
8.2 Dropout 正则化
在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译为随机失活)是一个简单有效的正则化方法。

- 在训练过程中,Dropout 的实现是让神经元以超参数 ppp(丢弃概率)的概率停止工作或者激活被置为 0,未被置为 0 的进行缩放,缩放比例为 1/(1−p)1/(1-p)1/(1−p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数。
- 在实际应用中,Dropout 参数 ppp 的概率通常取值在 0.2 到 0.5 之间:
- 对于较小的模型或较复杂的任务,丢弃率可以选择 0.3 或更小
- 对于非常深的网络,较大的丢弃率(如 0.5 或 0.6)可能会有效防止过拟合
- 实际应用中,通常会在全连接层(激活函数后)之后添加 Dropout 层
- 在测试过程中,随机失活不起作用:
- 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
- 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[xtest]=xE[x_{test}] = xE[xtest]=x
- 测试/推理模式:model.eval()
- 缩放的必要性:
- 在训练阶段,将参与计算的神经元的输出除以 (1−p)(1-p)(1−p)
- 经过 Dropout 后的期望输出变为 E[xdropout]=[(1−p)⋅x]/(1−p)=xE[x_{dropout}] = [(1-p) \\cdot x] / (1-p) = xE[xdropout]=[(1−p)⋅x]/(1−p)=x,与测试阶段的期望输出一致
- 训练模式:model.train()
观察 Dropout 效果的代码:
import torch
import torch.nn as nn
def test():
# 初始化随机失活层
dropout = nn.Dropout(p=0.4)
# 初始化输入数据:表示某一层的 weight 信息
inputs = torch.randint(0, 10, size=[1, 4]).float()
layer = nn.Linear(4, 5)
y = layer(inputs)
y = torch.relu(y)
print("未失活FC层的输出结果:\\n", y)
y = dropout(y)
print("失活后FC层的输出结果:\\n", y)
输出结果:
未失活FC层的输出结果:
tensor([[0.0000, 1.8033, 1.4608, 4.5189, 6.9116]], grad_fn=<ReluBackward0>)
失活后FC层的输出结果:
tensor([[0.0000, 3.0055, 2.4346, 7.5315, 11.5193]], grad_fn=<MulBackward0>)
上述代码将 Dropout 层的丢弃概率 ppp 设置为 0.4,此时经过 Dropout 层计算的张量中出现了很多 0,未变为 0 的按照 1/(1−0.4)1/(1-0.4)1/(1−0.4) 进行了缩放处理。
8.3 批量归一化(Batch Normalization)
在神经网络的训练过程中,流经网络的数据都是一个 batch,每个 batch 之间的数据分布变化非常剧烈,这就使得网络参数频繁地进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个 batch 的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。
通过标准化每一层的输入,使其均值接近 0,方差接近 1,从而加速训练并提高泛化能力。

先对数据标准化,再对数据重构(缩放 + 平移),写成公式如下所示:

其中:
- λ\\lambdaλ 和 β\\betaβ 是可学习的参数,相当于对标准化后的值做了一个线性变换,λ\\lambdaλ 为系数,β\\betaβ 为偏置
- ϵ\\epsilonϵ 通常取 1e−51e-51e−5,避免分母为 0
- E(x)E(x)E(x) 表示变量的均值
- Var(x)Var(x)Var(x) 表示变量的方差
批量归一化的步骤如下:

批量归一化的作用:
- 减少内部协方差偏移:通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。
- 加速训练:
- 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
- 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
- 起到正则化作用:批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如 Dropout)的需求。
- 提升泛化能力:由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。
批量归一化层在计算机视觉领域使用较多
Batch Normalization 的使用步骤:
- 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前。
- 例如:Conv2d → BN → ReLU 或者 Linear → BN → ReLU。
- BN 层会计算当前批次的均值 μ\\muμ 和方差 σ2\\sigma^2σ2。
- 然后,利用这两个统计量对当前批次的数据进行规范化。
- 规范化后的数据会被缩放 γ\\gammaγ 和平移 β\\betaβ。
- 同时,BN 层还会维护一个全局均值和全局方差的移动平均值,用于推理阶段。
- 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值和全局方差。
- 同样,规范化后的数据会被缩放 γ\\gammaγ 和平移 β\\betaβ。
PyTorch 实现批量归一化的代码示例:
import torch
import torch.nn as nn
"""
BatchNorm1d:主要应用于全连接层或处理一维数据的网络,例如文本处理。它接收形状为 (N, num_features) 的张量作为输入。
BatchNorm2d:主要应用于卷积神经网络,处理二维图像数据或特征图。它接收形状为 (N, C, H, W) 的张量作为输入。
BatchNorm3d:主要用于三维卷积神经网络 (3D CNN),处理三维数据,例如视频或医学图像。它接收形状为 (N, C, D, H, W) 的张量作为输入。
"""
def test01():
# 创建测试样本,假设是经过卷积层 (Conv2d) 处理后的特征图
# (N, C, H, W): 一张图,两个通道,每个通道3行4列
# 可以创建1个样本,图像的BN是对每个通道的特征图(行列数据)进行标准化
input_2d = torch.randn(size=(1, 2, 3, 4))
print("input–>", input_2d)
# num_features:输入特征数
# eps:非常小的浮点数,防止除以0的错误
# momentum:动量系数
# affine:默认为True,γ和β被使用,让BN层更加灵活
bn2d = nn.BatchNorm2d(num_features=2, eps=1e-05, momentum=0.1, affine=True)
output = bn2d(input_2d)
print("output–>", output)
print(output.size())
print(bn2d.weight)
print(bn2d.bias)
def test02():
# 创建测试样本
# 2个样本,1个特征
# 不能创建1个样本,无法统计均值和方差
input_1d = torch.randn(size=(2, 2))
# 创建线性层对象
linear1 = nn.Linear(in_features=2, out_features=3)
# 创建BN层对象
# num_features:输入特征数
bn1d = nn.BatchNorm1d(num_features=3)
output_1d = linear1(input_1d)
# 进行批量归一化
output = bn1d(output_1d)
print("output–>", output)
print(output.size())
输出结果:
test01:
input_2d–> tensor([[[[-0.2751, -1.2183, -0.5106, -0.1540],
[-0.4585, -0.5989, -0.6063, 0.5986],
[-0.4745, 0.1496, -1.1266, -1.2377]],
[[ 0.2580, 1.2065, 1.4598, 0.8387],
[-0.4586, 0.8938, -0.3328, 0.1192],
[-0.3265, -0.6263, 0.0419, -1.2231]]]])
output–> tensor([[[[ 0.4164, -1.3889, -0.0343, 0.6484],
[ 0.0655, -0.2032, -0.2175, 2.0889],
[ 0.0349, 1.2294, -1.2134, -1.4262]],
[[ 0.1340, 1.3582, 1.6853, 0.8835],
[-0.7910, 0.9546, -0.6287, -0.0452],
[-0.6205, -1.0075, -0.1449, -1.7779]]]],
grad_fn=<NativeBatchNormBackward0>)
torch.Size([1, 2, 3, 4])
Parameter containing:
tensor([1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0.], requires_grad=True)
test02:
output–> tensor([[-0.9998, 1.0000, 1.0000],
[ 0.9998, -1.0000, -1.0000]], grad_fn=<NativeBatchNormBackward0>)
torch.Size([2, 3])
九、手机价格分类案例
9.1 案例需求分析
小明创办了一家手机公司,他不知道如何估算手机产品的价格。为了解决这个问题,他收集了多家公司的手机销售数据。该数据为二手手机的各个性能的数据,最后根据这些性能得到 4 个价格区间,作为这些二手手机售出的价格区间。主要包括:

我们需要帮助小明找出手机的功能(例如:RAM 等)与其售价之间的某种关系。我们可以使用机器学习的方法来解决这个问题,也可以构建一个全连接的网络。
需要注意的是:在这个问题中,我们不需要预测实际价格,而是一个价格范围,它的范围使用 0、1、2、3 来表示,所以该问题也是一个分类问题。接下来我们还是按照四个步骤来完成这个任务:
9.2 构建数据集
数据共有 2000 条,其中 1600 条数据作为训练集,400 条数据用作测试集。我们使用 sklearn 的数据集划分工作来完成,并使用 PyTorch 的 TensorDataset 来将数据集构建为 Dataset 对象,方便构造数据集加载对象。
# 导入相关模块
import torch
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.nn as nn
from torchsummary import summary
import torch.optim as optim
from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd
import time
# 构建数据集
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv('data/手机价格预测.csv')
# 特征值和目标值
x, y = data.iloc[:, :–1], data.iloc[:, –1]
# 类型转换:特征值
x = x.astype(np.float32)
# 数据集划分
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, random_state=88)
# 构建数据集,转换为 pytorch 的形式
train_dataset = TensorDataset(torch.from_numpy(x_train.values), torch.tensor(y_train.values))
valid_dataset = TensorDataset(torch.from_numpy(x_valid.values), torch.tensor(y_valid.values))
# 返回结果
# x_train.shape[1]: 特征数
# len(np.unique(y)): 类别数
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))
if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
print("输入特征数:", input_dim)
print("分类个数:", class_num)
输出结果:
输入特征数: 20
分类个数: 4
9.3 构建分类网络模型
构建全连接神经网络来进行手机价格分类,该网络主要由三个线性层来构建,使用 ReLU 激活函数。
网络共有 3 个全连接层,具体信息如下:
- 第一层:输入维度为 20,输出维度为 128
- 第二层:输入维度为 128,输出维度为 256
- 第三层:输入维度为 256,输出维度为 4
# 构建网络模型
class PhonePriceModel(nn.Module):
def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 1. 第一层:输入维度为 20,输出维度为 128
self.linear1 = nn.Linear(input_dim, 128)
# 2. 第二层:输入维度为 128,输出维度为 256
self.linear2 = nn.Linear(128, 256)
# 3. 第三层:输入维度为 256,输出维度为 4
self.linear3 = nn.Linear(256, output_dim)
def forward(self, x):
# 前向传播过程
x = torch.relu(self.linear1(x))
x = torch.relu(self.linear2(x))
# 后续 CrossEntropyLoss 损失函数中包含 softmax 过程,所以当前步骤不进行 softmax 操作
output = self.linear3(x)
# 获取数据结果
return output
if __name__ == '__main__':
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型实例化
model = PhonePriceModel(input_dim, class_num)
summary(model, input_size=(input_dim,), batch_size=16)

9.4 模型训练
网络编写完成之后,我们需要编写训练函数。所谓的训练函数,指的是输入数据读取、送入网络、计算损失、更新参数的流程,该流程较为固定。我们使用多分类交叉熵损失函数、使用 SGD 优化方法。最终,将训练好的模型持久化到磁盘中。
# 模型训练过程
def train(train_dataset, input_dim, class_num):
# 固定随机数种子
torch.manual_seed(0)
# 初始化数据加载器
dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)
# 初始化模型
model = PhonePriceModel(input_dim, class_num)
# 损失函数 CrossEntropyLoss = softmax + 损失计算
criterion = nn.CrossEntropyLoss()
# 优化方法
optimizer = optim.SGD(model.parameters(), lr=1e-3)
# 训练轮数
num_epoch = 50
# 遍历每个轮次的数据
for epoch_idx in range(num_epoch):
# 训练时间
start = time.time()
# 计算损失
total_loss = 0.0
total_num = 0
# 遍历每个 batch 数据进行处理
for x, y in dataloader:
# 将数据送入网络中进行预测
model.train() # 使用训练模式
output = model(x)
# 计算损失
loss = criterion(output, y)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 损失计算
total_num += 1
total_loss += loss.item()
# 打印损失变换结果
print('epoch: %4s loss: %.2f, time: %.2fs' % (epoch_idx + 1, total_loss / total_num, time.time() – start))
# 模型保存
# state_dict(): 将模型的参数保存到字典中
torch.save(model.state_dict(), 'model/phone-price-model.pth')
if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型训练过程
train(train_dataset, input_dim, class_num)

9.5 模型评估
使用训练好的模型,对未知的样本进行预测。我们这里使用前面单独划分出来的验证集来进行评估。
def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
# load_state_dict: 将加载的参数字典应用到模型上
# load: 加载用来保存模型参数的文件
model.load_state_dict(torch.load('model/phone-price-model.pth'))
# 构建加载器
dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)
# 评估测试集
correct = 0
# 遍历测试集中的数据
for x, y in dataloader:
# 将其送入网络中
model.eval() # 使用推理模式
output = model(x)
# 获取类别结果
# argmax: 最大值对应的下标,即类别编码
y_pred = torch.argmax(output, dim=1)
# 获取预测正确的个数
correct += (y_pred == y).sum()
# 求预测精度
print('Acc: %.5f' % (correct.item() / len(valid_dataset)))
if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型预测结果
test(valid_dataset, input_dim, class_num)
输出结果:
Acc: 0.64250
9.6 网络性能优化
我们前面的网络模型在测试集的准确率为 0.64250,我们可以通过以下方面进行调优:
进行如下调整:
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.optim as optim
import numpy as np
import time
from sklearn.preprocessing import StandardScaler
# 构建数据集
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv('./data/手机价格预测.csv')
# 特征值和目标值
x, y = data.iloc[:, :–1], data.iloc[:, –1]
# 类型转换:特征值,目标值
x = x.astype(np.float32)
y = y.astype(np.int64)
# 数据集划分
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, random_state=88, stratify=y)
# 优化①: 数据标准化
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_valid = transfer.transform(x_valid)
# 构建数据集,转换为 pytorch 的形式
train_dataset = TensorDataset(torch.from_numpy(x_train), torch.tensor(y_train.values))
valid_dataset = TensorDataset(torch.from_numpy(x_valid), torch.tensor(y_valid.values))
# 返回结果
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))
# 构建网络模型
class PhonePriceModel(nn.Module):
def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 优化②: 增加网络深度
# 1. 第一层:输入维度为 20,输出维度为 128
self.linear1 = nn.Linear(input_dim, 128)
# 2. 第二层:输入维度为 128,输出维度为 256
self.linear2 = nn.Linear(128, 256)
# 3. 第三层:输入维度为 256,输出维度为 512
self.linear3 = nn.Linear(256, 512)
# 4. 第四层:输入维度为 512,输出维度为 128
self.linear4 = nn.Linear(512, 128)
# 5. 输出层:输入维度为 128,输出维度为 4
self.linear5 = nn.Linear(128, output_dim)
def forward(self, x):
# 前向传播过程
x = torch.relu(self.linear1(x))
x = torch.relu(self.linear2(x))
x = torch.relu(self.linear3(x))
x = torch.relu(self.linear4(x))
# 后续 CrossEntropyLoss 损失函数中包含 softmax 过程,所以当前步骤不进行 softmax 操作
output = self.linear5(x)
# 获取数据结果
return output
# 编写训练函数
def train(train_dataset, input_dim, class_num):
# 固定随机数种子
torch.manual_seed(0)
# 初始化数据加载器
dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)
# 初始化模型
model = PhonePriceModel(input_dim, class_num)
# 损失函数 CrossEntropyLoss = softmax + 损失计算
criterion = nn.CrossEntropyLoss()
# 优化③: 使用Adam优化方法,优化④: 学习率变为1e-4
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# 遍历每个轮次的数据
num_epoch = 50
for epoch_idx in range(num_epoch):
# 训练时间
start = time.time()
# 计算损失
total_loss = 0.0
total_num = 0
# 遍历每个 batch 数据进行处理
for x, y in dataloader:
model.train()
output = model(x)
# 计算损失
loss = criterion(output, y)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 损失计算
total_num += len(y)
total_loss += loss.item() * len(y)
# 打印损失变换结果
print('epoch: %4s loss: %.2f, time: %.2fs' %
(epoch_idx + 1, total_loss / total_num, time.time() – start))
# 模型保存
torch.save(model.state_dict(), './model/phone-price-model2.pth')
def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
# load_state_dict: 将加载的参数字典应用到模型上
model.load_state_dict(torch.load('./model/phone-price-model2.pth'))
# 构建加载器
dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)
# 评估测试集
correct = 0
# 遍历测试集中的数据
for x, y in dataloader:
# 将其送入网络中
# model.eval()
output = model(x)
# 获取预测类别结果
y_pred = torch.argmax(output, dim=1)
# 获取预测正确的个数
correct += (y_pred == y).sum()
# 求预测精度
print('Acc: %.5f' % (correct / len(valid_dataset)))
if __name__ == '__main__':
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
train(train_dataset, input_dim, class_num)
test(valid_dataset, input_dim, class_num)
📌 温馨提示:本文档中的图片引用了本地 assets/ 目录下的文件,上传 CSDN 时,请将对应的图片一并上传,并更新图片链接为 CSDN 图床的链接地址,以确保图片正常显示。
网硕互联帮助中心


评论前必须登录!
注册