Skip to content

啥是神经网络

人工神经网络(Artificial Neural Network, 简写为ANN)也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。它由多个互相连接的人工神经元(也称为节点)构成,可以用于处理和学习复杂的数据模式,尤其适合解决非线性问题。人工神经网络是机器学习中的一个重要模型,尤其在深度学习领域中得到了广泛应用。

如何构建神经网络

神经网络是由多个神经元组成,下图为一个神经元

image-20260420154814225

每个神经元就俩计算,上一层神经元特征的线性求和,以及通过激活函数的计算

同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。

接下来,使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:

image-20260420154933819

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:

  1. 输入层(Input Layer): 即输入x的那一层(如图像、文本、声音等)。==每个输入特征对应一个神经元==。输入层将数据传递给下一层的神经元。
  2. 输出层(Output Layer): 即输出y的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
  3. 隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的“深度”通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。

==特点是:==

  • 同一层的神经元之间没有连接
  • 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义),这就是全连接神经网络(FCNN)
  • 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
  • 第N-1层神经元的输出就是第N层神经元的输入
  • 每个连接都有一个权重值(w系数和b系数)

image-20260420155020814

每一个神经元工作时,前向传播会产生两个值,内部状态值(加权求和值)激活值反向传播时会产生激活值梯度内部状态值梯度

  • 内部状态值

    • 神经元或隐藏单元的内部存储值,它反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。
    • 每个输入$$x_i$$都有一个与之相乘的权重$$w_i$$,表示每个输入信号的重要性。
    • z=w⋅x+b
      • w:权重矩阵
      • x:输入值
      • b:偏置
  • 激活值

    • 通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果。激活值决定了当前神经元的输出。
    • a=f(z)
      • f:激活函数
      • z:内部状态值

通过控制每个神经元的内部状态值、激活值的大小;每一层的内部状态值的方差、每一层的激活值的方差可让整个神经网络工作的更好。

激活函数

网络非线性因素理解

  • 没有引入非线性因素的网络等价于使用一个线性模型来拟合

  • 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力

激活函数用于对每层的输出数据进行变换, 进而为整个网络注入了非线性因素。此时, 神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型

激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。网络参数在更新时,使用反向传播算法(BP),这就要求激活函数必须可微。

Sigmoid

image-20260420155426130

  1. 从sigmoid函数图像可以得到,sigmoid 函数可以将任意的输入映射到 (0, 1) 之间,当输入的值大致在**<-6或者>6**时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入100和输入10000经过 sigmoid的激活值几乎都是等于1的,但是输入的数据之间相差100倍的信息就丢失了。
  2. 对于sigmoid函数而言,输入值在**[-6, 6]之间输出值才会有明显差异**,输入值在**[-3, 3]之间才会有比较好的效果**
  3. 通过上述导数图像,发现导数数值范围是 (0, 0.25),当输入的值**<-6或者>6时,sigmoid激活函数图像的导数接近为 0**,此时网络参数将更新极其缓慢,或者无法更新。
  4. 一般来说,sigmoid网络在5层之内就会产生梯度消失现象。而且,该激活函数的激活值并不是以0为中心的,激活值总是偏向正数,导致梯度更新时,只会对某些特征产生相同方向的影响,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层
python
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']    # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 用来正常显示负号
"""
    OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
    原因:torch和matplotlib工具中都有libiomp5md.dll,只能保留一个
    解决:删除torch下的libiomp5md.dll
"""
def sigmoid_demo():
    fig, axes = plt.subplots(1,2)

    # 创建x轴的刻度
    x = torch.linspace(-20,20,1000)

    # sigmoid函数_原始图像
    y = torch.sigmoid(x)

    # 绘制图形
    axes[0].plot(x,y)
    axes[0].grid()
    axes[0].set_title("sigmoid函数_原始图像")

    # sigmoid函数_导数图像
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.sigmoid(x).sum().backward()

    # 绘制图形
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    axes[1].set_title("sigmoid函数_导数图像")
    plt.show()


if __name__ == '__main__':
    # sigmoid激活函数
    sigmoid_demo()

Tanh

image-20260420155900784

  • 由上面的函数图像可以看到,Tanh函数将输入映射到(-1, 1)之间,图像以0为中心,激活值在0点对称,当输入的值大概**<-3或者>3** 时将被映射为-1或者1。其导数值范围 (0, 1),当输入的值大概**<-3或者>3**时,其导数近似0。

  • 与Sigmoid相比,它是以0为中心的,使得其收敛速度要比Sigmoid快,减少迭代次数。然而,从图中可以看出,Tanh两侧的导数也为0,同样会造成梯度消失。

  • 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数

python
# 导包
import torch
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']    # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 用来正常显示负号


# 遇到的问题: OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
# 解决方案:  去 你的Anaconda软件安装路径下的 anaconda3\Lib\site-packages\torch\lib 删除 libiomp5md.dll 这个文件.
# 例如: 我的路径是 C:\Software\DevelopSofware\anaconda3\Lib\site-packages\torch\lib


# 需求1: 绘制Tanh函数图像

# 1. 创建1个 1行2列的画布.
fig, axes = plt.subplots(1, 2)
# 2. 准备x轴的值 -> [-20, 20]的等差数列, 1000个元素.
x = torch.linspace(-20, 20, 1000)
# 3. 计算Tanh函数的值
y = torch.tanh(x)
# 4. 绘制折线图.
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')
# plt.show()

# 需求2: 绘制Tanh函数的导数图像.
# 1. 准备x轴的值 -> [-20, 20]的等差数列, 1000个元素.
x = torch.linspace(-20, 20, 1000, requires_grad=True)
# 2. 计算Tanh函数的导数
torch.tanh(x).sum().backward()   # 表示 标量张量 才能求导.
# 3. 绘制折线图
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 函数导数图像')
plt.show()

ReLu

image-20260420160032016

  • ReLU 激活函数将小于0的值映射为0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
  • 当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”。
  • ReLU是目前==最常用的激活函数==。与sigmoid相比,RELU的优势是:
    • 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大;而采用Relu激活函数,整个过程的计算量节省很多
    • sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练;而采用relu激活函数,当输入的值>0时,梯度为1,不会出现梯度消失的情况
    • Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生
python
# 导包
import torch
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']    # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 用来正常显示负号


# 遇到的问题: OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
# 解决方案:  去 你的Anaconda软件安装路径下的 anaconda3\Lib\site-packages\torch\lib 删除 libiomp5md.dll 这个文件.
# 例如: 我的路径是 C:\Software\DevelopSofware\anaconda3\Lib\site-packages\torch\lib


# 需求1: 绘制Relu函数图像

# 1. 创建1个 1行2列的画布.
fig, axes = plt.subplots(1, 2)
# 2. 准备x轴的值 -> [-20, 20]的等差数列, 1000个元素.
x = torch.linspace(-20, 20, 1000)
# 3. 计算Relu函数的值
y = torch.relu(x)
# 4. 绘制折线图.
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Relu 函数图像')
# plt.show()

# 需求2: 绘制Relu函数的导数图像.
# 1. 准备x轴的值 -> [-20, 20]的等差数列, 1000个元素.
x = torch.linspace(-20, 20, 1000, requires_grad=True)
# 2. 计算Relu函数的导数
torch.relu(x).sum().backward()   # 表示 标量张量 才能求导.
# 3. 绘制折线图
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Relu 函数导数图像')
plt.show()

SoftMax

image-20260420160247581

SoftMax就是将网络输出的logits通过softmax函数,映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为预测目标类别。

python
# 导包
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])
scores = torch.tensor([[0.3, 0.24, 5, -3.1], [-0.1, 0.56, 2.4, 0.35]])

# 计算得到概率值
"""
        dim=0,表示按列计算概率值
        dim=1,表示按行计算概率值
    """
# prob = torch.softmax(scores,dim=0)
prob = torch.softmax(scores,dim=1)
print(prob)

如何选择激活函数

image-20260420160418906

对于隐藏层:

  1. 优先选择ReLU激活函数
  2. 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
  3. 如果使用了ReLU, 需要注意一下Dead ReLU问题,避免出现0梯度从而导致过多的神经元死亡。
  4. 少用sigmoid激活函数,可以尝试使用tanh激活函数

对于输出层:

  1. 二分类问题选择sigmoid激活函数
  2. 多分类问题选择softmax激活函数
  3. 回归问题选择identity激活函数

参数初始化

在构建网络之后,网络中的参数是需要初始化的。一般需要初始化的参数主要有权重偏置偏置一般初始化为0即可,而对权重的初始化则会更加重要。

参数初始化的作用:

  • 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
  • 提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
  • 破除对称性:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。
    • 简单说就是不能有一摸一样的神经元

常见参数初始化方法

  • 随机初始化

    • 均匀分布初始化:权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置为在(-$$1\over\sqrt{d}$$,$$1\over\sqrt{d}$$)均匀分布中生成当前神经元的权重,其中d为神经元的输入数量。

    • 正态分布初始化:随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化

    • 优点:能有效打破对称性

    • 缺点:随机选择范围不当可能导致梯度问题

    • 适用场景:浅层网络或低复杂度模型。隐藏层1-3层,总层数不超过5层。

  • ==全0初始化==:将神经网络中的所有权重参数初始化为0

    • 优点:实现简单
    • 缺点:无法打破对称性,所有神经元更新方向相同,无法有效训练
    • 适用场景:几乎不使用,==仅用于b偏置项的初始化==
  • 全1初始化:将神经网络中的所有权重参数初始化为1

    • 优点:实现简单
    • 缺点
      • 无法打破对称性,所有神经元更新方向相同,无法有效训练
      • 会导致激活值在网络中呈指数增长,容易出现梯度爆炸
    • 适用场景
      • 测试或调试:比如验证神经网络是否能正常前向传播和反向传播
      • 特殊模型结构:某些稀疏网络或特定的自定义网络中可能需要手动设置部分参数为1
      • 偏置初始化:偶尔可以将偏置初始化为小的正值(如 0.1),但很少用1作为偏置的初始值
  • 固定值初始化:将神经网络中的所有权重参数初始化为某个固定值

    • 优点:实现简单
    • 缺点
      • 无法打破对称性,所有神经元更新方向相同,无法有效训练
      • 初始权重过大或过小可能导致梯度爆炸或梯度消失
    • 适用场景
      • 测试或调试
  • ==kaiming初始化==,也叫做HE初始化:专为ReLU和其变体设计,考虑到ReLU激活函数的特性,对输入维度进行缩放

    • HE初始化分为正态分布的HE初始化、均匀分布的HE初始化
      • 正态分布的he初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / fan_in)
        • std值越大,w权重值离均值0分布相对较广,计算得到的内部状态值有较大的正值或负值
      • 均匀分布的he初始化
        • 它从[-limit,limit] 中的均匀分布中抽取样本, limitsqrt(6 / fan_in)
      • fan_in 输入神经元的个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
    • 优点:适合 ReLU,能保持梯度稳定
    • 缺点:对非 ReLU 激活函数效果一般
    • 适用场景:深度网络(10层及以上),使用 ReLU、Leaky ReLU 激活函数
  • ==xavier初始化==,也叫做Glorot初始化:根据网络输入和输出的维度自动选择权重范围,使输入和输出的方差相同

    • xavier初始化分为正态分布的xavier初始化、均匀分布的xavier初始化

      • 正态化的Xavier初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / (fan_in + fan_out))
        • std值越小,w权重值离均值0分布相对集中,计算得到的内部状态值有较小的正值或负值
      • 均匀分布的Xavier初始化
        • [-limit,limit] 中的均匀分布中抽取样本, limit 是 sqrt(6 / (fan_in + fan_out))
      • fan_in 是输入神经元个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
      • fan_out 是输出神经元个数,当前层输出的神经元的数量,也就是当前层会传递给下一层的神经元的数量。简单来说,就是当前层会产生多少个输出。
    • 优点:适用于Sigmoid、Tanh 等激活函数,解决梯度消失问题

    • 缺点:对 ReLU 等激活函数表现欠佳

    • 适用场景:深度网络(10层及以上),使用 Sigmoid 或 Tanh 激活函数

python
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) # 创建一个全连接层:输入维度:5,输出维度:3
    nn.init.xavier_uniform_(linear.weight) # 初始化参数
    print(linear.weight.data)

image-20260420161007294

如何选择初始化参数的方法

==注意:xavier 或 kaiming 只能对权重进行初始化==

  • 激活函数的选择:根据激活函数的类型选择对应的初始化方法

    • Sigmoid/Tanh:xavier 初始化
    • ReLU/Leaky ReLU:kaiming 初始化
  • 神经网络模型的深度

    • 浅层网络:随机初始化即可
    • 深层网络:需要考虑方差平衡,如 xavier 或 kaiming 初始化

神经网络搭建和参数计算‼️

构建神经网络

在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:

  • __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
  • forward方法,在调用神经网络模型对象的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等。

接下来构建如下图所示的神经网络模型:

image-20260422142624571

python
"""
    搭建神经网络的步骤:
        1- 定义一个类,继承自torch.nn.Module
        2- 实现两个方法:
            2.1- __init__:初始化方法。负责的工作内容如下:
                a、初始化父类
                b、设置属性值
                c、定义神经网络结构:隐藏层有几层、输出层是怎样的等
                d、参数初始化:w、b如何进行初始化

            2.2- forward:前向传播。将数据送入到搭建好的网络模型中,对模型进行训练,也就是前向传播的过程
"""
import torch
import torch.nn as nn
from torchinfo import summary

class MyModel(nn.Module):
    def __init__(self):
        # a、初始化父类:也就是调用父类中的初始化方法
        super().__init__()

        # b、设置属性值【目前不需要】

        # c、定义神经网络结构:隐藏层有几层、输出层是怎样的等
        # 1.1- 隐藏层
        # 第一层隐藏层
        """
            参数解释:
                in_features:输入的特征个数。也就是上游神经元的个数
                out_features:输出的特征个数。也就是本层神经元的个数
        """
        self.linear1 = nn.Linear(in_features=3,out_features=3)

        # 第二层隐藏层
        self.linear2 = nn.Linear(in_features=3,out_features=2)

        # 1.2- 输出层
        self.output = nn.Linear(in_features=2,out_features=2)

        # d、参数初始化:w、b如何进行初始化
        # 隐藏层1的初始化
        nn.init.xavier_normal_(self.linear1.weight)  # w初始化
        nn.init.zeros_(self.linear1.bias)   # b初始化

        # 隐藏层2的初始化
        nn.init.kaiming_normal_(self.linear2.weight)  # w初始化
        nn.init.zeros_(self.linear2.bias)  # b初始化

    def forward(self,data):
        """
        前向传播。对模型进行训练,也就是得到预测结果
        :param data: 前向传播送入到网络结构中的样本数据。类型是张量tensor
        :return:
        """

        # 1- 数据经过第一层隐藏层
        # 1.1- 分开版
        # 先进行线性求和
        # linear1_result = self.linear1(data)
        # 再将线性求和结果给到激活函数,得到激活值
        # data = torch.sigmoid(linear1_result)

        # 1.2- 合并版【推荐】
        data = torch.sigmoid(self.linear1(data))

        # 2- 数据经过第二层隐藏层
        data = torch.relu(self.linear2(data))

        # 3- 数据经过输出层得到输出结果
        """
            dim=-1:不管列表嵌套多少层,全部只对最里层(最后一个维度)的数据进行统计
        """
        return torch.softmax(self.output(data),dim=-1)
python
# 测试网络模型

# 1- 准备训练数据:目前的需求中,每条样本必须有3个特征
# 10:多少条样本;3:每条样本多少个特征
data = torch.randn(10, 3)

# 2- 创建神经网络模型对象
my_model = MyModel()

# 3- 模型训练:将数据输入到神经网络中,进行前向传播
# 内部自动调用forward方法
output = my_model(data)
print(f"预测结果是:{output}")
print(f"预测结果形状:{output.shape}")

print("-" * 30)

# 4- 查看神经网络的概要信息【理解】
"""
    summary(参数1,参数2,参数3):查看神经网络各层参数的信息
        参数1:神经网络算法实例对象
        参数2:输入的特征个数,类型必须是元组
        参数3:输入的每个批次样本条数,任意
"""
display(summary(my_model,input_size=(5,3)))

image-20260422142937987

模型参数计算

  • 模型参数的计算

    • 以第一个隐层为例:该隐层有3个神经元,每个神经元的参数为:4个(w1,w2,w3,b1),所以一共用3x4=12个参数。

    • 输入数据和网络权重是两个不同的事儿!

image-20260422143107024

python
----------------------------------------------------------------
        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
----------------------------------------------------------------

损失函数

在深度学习中, 损失函数是用来==衡量模型参数质量的函数==, 衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。

模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。

image-20260422144027035

损失函数作用:

  • 评估性能:反映模型预测结果与目标值的匹配程度。
  • 指导优化:通过梯度下降等算法最小化损失函数,优化模型参数。

分类任务损失函数

在深度学习的分类任务中使用最多的是交叉熵损失函数

多分类任务损失函数

在多分类任务通常使用softmax将logits转换为概率的形式,所以多分类的交叉熵损失也叫做softmax损失,它的计算方法是:

image-20260422144538924

例子:

image-20260422144604077

上图中的交叉熵损失为:

image-20260422144616264

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

image-20260422144635235

python
import torch
from torch import nn


# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。nn.CrossEntropyLoss()=softmax+损失计算
if __name__ == '__main__':
	# 设置真实值: 可以是热编码后的结果也可以不进行热编码
	# 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)

二分类任务损失函数

在处理二分类任务时,不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数:

image-20260422144656728

其中:

  • y是样本x属于某一个类别的真实概率

  • 而$$\hat{y}$$是样本属于某一类别的预测概率

  • L用来衡量真实值y与预测值$$\hat{y}$$之间差异性的损失结果。

image-20260422144711784

在PyTorch中实现时使用nn.BCELoss() 实现,如下所示:

python
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)

回归任务损失函数

回归的损失函数总结:
    1- L1损失
        特点:也称之为MAE损失。曲线不光滑,容易错误极值点
        使用:很少使用,一般作为正则化下添加到其他损失函数中
    2- L2损失
        特点:也称之为MSE损失。因为对损失值求和平方,容易出现梯度爆炸的情况
        使用:较少使用,相对比MAE用的多些。一般作为正则化下添加到其他损失函数中
    3- SmoothL1损失
        特点:集成了L1损失和L2损失,而且避免了它们的确定
        使用:经常使用

MAE损失函数

**mean absolute loss(MAE)**也被称为L1 Loss,是以绝对误差作为距离

损失函数公式:

image-20260422144832919

曲线如下图所示:

image-20260422144843452

特点是:

  • 由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。(0点不可导, 产生稀疏矩阵)
  • L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值
  • 适用于回归问题中存在异常值或噪声数据时,可以减少对离群点的敏感性

在PyTorch中使用nn.L1Loss()实现,如下所示:

python
import torch
from torch import nn


# 计算inputs与target之差的绝对值
def test03():
    # 1 设置真实值和预测值
    y_pred = torch.tensor([1.0, 1.0, 1.9])
    y_true = torch.tensor([2.0, 2.0, 2.0])
    # 2 实例MAE损失对象
    loss = nn.L1Loss()
    # 3 计算损失
    my_loss = loss(y_pred, y_true)
    print('loss:', my_loss)

MSE损失函数

**Mean Squared Loss/ Quadratic Loss(MSE loss)**也被称为L2 loss,或欧氏距离,它以误差的平方和的均值作为距离

损失函数公式:

image-20260422144926408

曲线如下图所示:

image-20260422144937165

特点是:

  • L2 loss也常常作为正则项,对于离群点(outliers)敏感,因为平方项会放大大误差

  • 当预测值与目标值相差很大时, 梯度容易爆炸

    • 梯度爆炸:网络层之间的梯度(值大于1.0)重复相乘导致的指数级增长会产生梯度爆炸
  • 适用于大多数标准回归问题,如房价预测、温度预测等

在PyTorch中通过nn.MSELoss()实现:

python
import torch
from torch import nn

def test04():
    # 1 设置真实值和预测值
    y_pred = torch.tensor([1.0, 1.0, 1.9])
    y_true = torch.tensor([2.0, 2.0, 2.0])
    # 2 实例MSE损失对象
    loss = nn.MSELoss()
    # 3 计算损失
    my_loss = loss(y_pred, y_true)
    print('myloss:', my_loss)

Smooth L1损失函数

smooth L1说的是光滑之后的L1,是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE,在误差较大时则更像 MAE。

image-20260422145044805

其中:𝑥=f(x)−y 为真实值和预测值的差值。最终再除以样本

image-20260622133118118

从上图中可以看出,该函数实际上就是一个分段函数

  • 在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题
  • 在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题

特点是:

  • 对离群点更加鲁棒:当误差较大时,损失函数会线性增加(而不是像MSE那样平方增加),因此它对离群点的惩罚更小,避免了MSE对离群点过度敏感的问题

  • 计算梯度时更加平滑:与MAE相比,Smooth L1在小误差时表现得像MSE,避免了在训练过程中因使用绝对误差而导致的梯度不连续问题

在PyTorch中使用nn.SmoothL1Loss()计算该损失,如下所示:

python
import torch
from torch import nn


def test05():
    # 1 设置真实值和预测值
    y_true = torch.tensor([0, 3])
    y_pred = torch.tensor([0.6, 0.4])
    # 2 实例smmothL1损失对象
    loss = nn.SmoothL1Loss()
    # 3 计算损失
    my_loss = loss(y_pred, y_true)
    print('loss:', my_loss)

神经网络优化方法

多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练,值得指出的是 BP 算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说 BP 网络时,一般是指用 BP 算法训练的多层前馈神经网络。

这就需要了解两个概念:

  1. 正向传播:指的是数据通过网络从输入层到输出层的传递过程。这个过程的目的是计算网络的输出值(预测值),从而与目标值(真实值)比较以计算误差。
  2. 反向传播:指的是计算损失函数相对于网络中各参数(权重和偏置)的梯度,指导优化器更新参数,从而使神经网络的预测更接近目标值。

反向传播(BP算法)

利用反向传播算法对神经网络进行训练。该方法与梯度下降算法相结合,对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。

前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。

反向传播(Back Propagation):利用损失函数ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。

在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数 ERROR。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)

反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新

如下图是一个简单的神经网络用来举例:激活函数为sigmoid

image-20260622133213046

前向传播运算

image-20260422145345602

接下来是反向传播(求网络误差对各个权重参数的梯度):

先来求最简单的,求误差E对w5的导数。首先明确这是一个链式法则的求导过程,要求误差E对w5的导数,需要先求误差E对$$out_{o1}$$的导数,再求$$out_{o1}$$对$$net_{o1}$$的导数,最后再求$$net_{o1}$$对$$w_5$$的导数,经过这个链式法则,就可以求出误差E对$$w_5$$的导数(偏导),如下图所示:

image-20260422145401213

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

image-20260422145415862

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

image-20260422145434048

梯度下降优化方法‼️

image-20260422145505823

梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到 “鞍点” ,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优

对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:MomentumAdaGradRMSpropAdam

image-20260422145519471

动量算法Momentum

image-20260422145607266

当梯度下降碰到 “峡谷” 、”平缓”、”鞍点” 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。

梯度计算公式

​ $$s_t=βs_{t−1}+(1−β)g_t$$

参数更新公式

​ $$w_t=w_{t−1}−ηs_t$$

st是当前时刻指数加权平均梯度值

st1是历史指数加权平均梯度值

gt是当前时刻的梯度值

β 是调节权重系数,通常取 0.9 或 0.99,β越大越重视历史信息

η是学习率

wt是当前时刻模型权重参数

image-20260422145626538

python
咱们举个例子,假设:权重 β 为 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:当前时刻指数加权平均梯度值
η:学习率

image-20260422145643117

  • 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  • 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 “峡谷” 问题的影响。

在pytorch中动量梯度优化法编程实践如下:

python
import torch
import torch.nn

def momentum_demo():
    # 初始化权重值
    w = torch.tensor([1.0],requires_grad=True,dtype=torch.float32)

    # 自定义损失函数
    criterion = (w**2)/2

    # 创建优化器对象,作用:1- 计算梯度;2- 自动更新权重
    optimizer = torch.optim.SGD([w],lr=0.1,momentum=0.9)

    # 反向传播计算梯度
    # 梯度清零
    optimizer.zero_grad()
    # 反向传播
    criterion.sum().backward()
    # 更新权重
    optimizer.step()
    print(f"第1次,梯度的结果{w.grad}")

    # 第二次
    criterion = (w ** 2) / 2
    # 反向传播计算梯度
    # 梯度清零
    optimizer.zero_grad()
    # 反向传播
    criterion.sum().backward()
    # 更新权重
    optimizer.step()
    print(f"第2次,梯度的结果{w.grad}")

if __name__ == '__main__':
    # 动量法
    momentum_demo()

AdaGrad

AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为 AdaGrad 认为:在起初时,由于距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。

其计算步骤如下:

  1. 初始化学习率 η、初始化参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度gt

  4. 累积平方梯度: st = st1 + gtgt,⊙ 表示各个分量相乘

  5. 学习率 η 的计算公式如下:

    ​ η = $$η\over\sqrt{s_t}+σ$$

  6. 权重参数更新公式如下:

    wt = $$w_{t-1}$$ - $$η\over\sqrt{s_t}+σ$$ * gt

  7. 重复 3-7 步骤

AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。

在PyTorch中AdaGrad优化法编程实践如下:

python
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()))

RMSProp

RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。

其计算过程如下:

  1. 初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度 gt

  4. 使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:

    st = βst1 + (1-β)gtgt

  5. 学习率 η 的计算公式如下:

    ​ η = ηst+σ

  6. 权重参数更新公式如下:

    wt = wt1 - ηst+σ * gt

  7. 重复 3-7 步骤

RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。

RMSProp 通过引入衰减系数β,控制历史梯度对历史梯度信息获取的多少. 被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。

需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。

在PyTorch中RMSprop梯度优化法,编程实践如下:

python
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

Adam

  • Momentum 使用指数加权平均计算当前的梯度值

  • AdaGrad、RMSProp 使用自适应的学习率

  • Adam优化算法(Adaptive Moment Estimation,自适应矩估计)将 Momentum 和 RMSProp 算法结合在一起

    • 修正梯度: 使⽤梯度的指数加权平均
    • 修正学习率: 使⽤梯度平⽅的指数加权平均
  • 原理:Adam 是结合了 MomentumRMSProp 优化算法的优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。

  • 梯度计算公式

    ​ $$m_t=β_1m_{t−1}+(1−β_1)g_t$$

    ​ $$s_t=β_2s_{t−1}+(1−β_2)gt^2$$

    ​ $$\hat{m_t}$$ = $$m_t\over1−β_1^t$$, $$\hat{s_t}$$=$$s_t\over1−β_2^t$$

  • 权重参数更新公式:

    ​ $$w_t$$ = $$w_{t−1}$$ − $$η\over\sqrt{\hat{s_t}}+ϵ$$mt^

其中,$$m_t$$ 是梯度的一阶矩估计,$$s_t$$ 是梯度的二阶矩估计,$$ \hat{m_t}$$和 $$\hat{s_t}$$ 是偏差校正后的估计。

在PyTroch中,Adam梯度优化法编程实践如下:

python
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

小结

优化算法优点缺点适用场景
SGD简单、容易实现。收敛速度较慢,容易震荡,特别是在复杂问题中。用于==简单任务==,或者当数据特征分布相对稳定时。
Momentum可以加速收敛,减少震荡,特别是在高曲率区域。需要手动调整动量超参数,可能会在小步长训练中过度更新。用于非平稳优化问题,尤其是深度学习中的应用。
AdaGrad自适应调整学习率,适用于稀疏数据。学习率会在训练过程中逐渐衰减,可能导致早期停滞。适合==稀疏数据==,如 NLP 或推荐系统中的特征。
RMSProp解决了 AdaGrad 学习率过早衰减的问题,适应性强。需要选择合适的超参数,更新可能会过于激进。适用于动态问题、非平稳目标函数,如深度学习训练。
Adam结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。需要调节更多的超参数,训练过程中可能会产生较大波动。广泛适用于各种深度学习任务,特别是非平稳和复杂问题。
  • 简单任务和较小的模型:SGD 或 Momentum
  • 复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀
  • 需要处理稀疏数据或文本数据:Adagrad 或 RMSProp

学习率衰减优化方法

在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。

运行下面代码,观察学习率设置不同对网络训练的影响:

python
"""
案例:
    通过代码 观察不同的学习率 对 参数更新 的影响.
"""

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(lr=0.3):

    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.3      # x从2.0一下子跨过0点,到了左侧负数区域
    # lr = 0.3      # 梯度越来越大 梯度爆炸
    max_iteration = 40
    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())  # 损失值 列表,这里将y改为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()

if __name__ == '__main__':
    # dm01(0.01) # 学习率过小,梯度下降缓慢,需要迭代计算多次
    # dm01(0.1)
    dm01(0.2) # 学习率设置的不合理,会出现梯度震荡
    # dm01(0.3) # 学习率过大,会出现梯度爆炸

image-20260422150906089

等间隔学习率衰减

image-20260422150932421

python
#   step_size:调整间隔数=50
#   gamma:调整系数=0.5
#   调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.5)
python
import matplotlib.pyplot as plt
import torch
import torch.nn

def demo():
    # 定义真实值、特征值、权重值
    y_true = torch.tensor([0])
    x = torch.tensor([1.0],dtype=torch.float32)
    w = torch.tensor([1.0],requires_grad=True,dtype=torch.float32)

    # 梯度下降优化器
    optimizer = torch.optim.SGD([w],lr=0.1,momentum=0.9)

    # 学习率等间隔下降策略
    """
        step_size:每隔多少个轮次更新一次学习率
        gamma:学习率的更新系数
    """
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size=50,gamma=0.5)

    # 训练的总轮次
    epochs = 200

    iteration = 10

    epoch_list = [] # 记录循环到哪个轮次了
    lr_list = [] # 记录对应轮次的学习率
    for epoch in range(epochs):
        for i in range(iteration):
            # 获得预测值
            y_pred = w*x

            # 计算损失值
            loss_value = (y_pred-y_true)**2

            # 梯度清零、反向传播、更新权重
            optimizer.zero_grad()
            loss_value.sum().backward()
            optimizer.step()

        # 记录对应轮次和对应的学习率
        epoch_list.append(epoch)
        lr_list.append(scheduler.get_last_lr())
        print(f"第{epoch}轮次,学习率是多少{scheduler.get_last_lr()}")
        # 按照等间隔(epoch的等间隔)更新学习率
        scheduler.step()

    # 绘制折线图
    plt.plot(epoch_list,lr_list)
    plt.xlabel("Epoch")
    plt.ylabel("lr")
    plt.show()


if __name__ == '__main__':
    demo()

##按指数学习率衰减

image-20260422151130495

在PyTorch中实现时使用:

python
# gamma:指数的底
# 调整方式
# lr= lr∗gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)

具体使用方式如下所示:

python
"""
案例:
    演示 学习率衰减优化方式.

背景:
    之前学的 AdaGrad, RMSProp, Adam这些梯度下降优化方式, 都可以修改 学习率, 但是底层无法人为精准控制, 例如: *轮 更新一次, 制定轮更新等...
    针对于此, PyTorch中加入了 学习率衰减优化方式, 可以人为的控制学习率的变化.

方式:
    1. 等间隔学习率优化.
        间隔制定的轮数, 优化(更新)学习率.
        lr新 = lr旧 * gamma
    2. 指定间隔学习率优化.
        设置指定的轮数, 当达到指定的轮数后, 即可自动优化 学习率.
        lr新 = lr旧 * gamma
    3. 指数学习率优化.
        lr新 = lr旧 * gamma ^ epoch
"""

# 导包
import torch
import torch.optim as optim
import matplotlib.pyplot as plt

# 3. 定义函数, 演示: 如何实现 指数学习率优化.
def dm03():
    # 1. 定义变量, 记录: 训练的总轮数, 初始学习率, 每轮的迭代器个数.
    epochs, lr, iteration = 200, 0.1, 10

    # 2. 定义变量, 记录: 真实值.
    y_true = torch.tensor([0], dtype=torch.float32)

    # 3. 定义变量, 记录: w(权重), x(特征)
    x = torch.tensor([1.0], dtype=torch.float32)
    w = torch.tensor([1.0], dtype=torch.float32, requires_grad= True)

    # 4. 创建优化器对象, 采用: 动量法.
    optimizer = optim.SGD([w], lr=lr, momentum=0.9)
    # 5. 创建 学习率衰减优化对象.
    # 思路1: 等间隔学习率衰减优化对象, 设置 50轮/次, 更新学习率.
    # 参1: 优化器对象.   参2: 衰减的轮数间隔.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)   # [0.1, 0.05, 0.0125...]

    # 思路2: 指定间隔学习率衰减优化对象, 设置第50, 125, 160轮 更新学习率.
    # milestones = [24, 50, 60, 90, 125, 130, 160]
    # milestones = [50, 125, 160]
    # 参1: 优化器对象.   参2: 衰减的(指定)轮数.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    # scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)

    # 思路3: 指数衰减学习率衰减优化对象.  计算公式: lr新 = lr旧 * gamma ^ epoch
    scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)

    # 6. 定义列表, 分别记录: 训练的轮数, 该轮的学习率.
    epoch_list, lr_list = [], []

    # 7. 具体的 每轮 训练过程
    # 7.1 逐轮训练.
    for epoch in range(epochs):     # epoch: 0 ~ 200, 包左不包右
        # 7.2 添加轮数, 以及本轮的 学习率
        epoch_list.append(epoch)
        lr_list.append(scheduler.get_last_lr())

        # 7.3 每轮的训练过程, 即: 每轮有 10个 迭代器.
        for i in range(iteration):
            # 7.4 计算预测值.
            y_pred = w * x
            # 7.5 计算损失值.
            loss = (y_pred - y_true) ** 2
            # 7.6 反向传播.
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 7.7 走这里,说明: 本轮(一轮)训练完成. 更新本轮的学习率.
        scheduler.step()

    # 8. 打印下: 每轮的 学习率
    print(f'epoch_list: {epoch_list}')
    print(f'lr_list: {lr_list}')

    # 9. 画图, 绘制: 训练的轮数 以及 每轮的 学习率
    plt.plot(epoch_list, lr_list)
    plt.xlabel('Epoch')
    plt.ylabel('Learning Rate')
    plt.show()


# 测试
if __name__ == '__main__':
    dm03()

小结

==手动调整学习率的策略相对前面梯度下降优化算法自动调整的方式,比较少用。原因是实际工作神经网络的结构是比较复杂,手动很难调整到比较好的状态==

方法等间隔学习率衰减 (Step Decay)指定间隔学习率衰减 (Exponential Decay)指数学习率衰减 (Exponential Moving Average Decay)
衰减方式固定步长衰减指定步长衰减平滑指数衰减,历史平均考虑
实现难度简单易实现相对简单,容易调整需要额外历史计算,较复杂
适用场景大型数据集、较为简单的任务对训练平稳性要求较高的任务高精度训练,避免过快收敛
优点直观,易于调试,适用于大批量数据易于调试,稳定训练过程平滑且考虑历史更新,收敛稳定性较强
缺点学习率变化较大,可能跳过最优点在某些情况下可能衰减过快,导致优化提前停滞超参数调节较为复杂,可能需要更多的计算资源

正则化方法

image-20260422151224660

  • 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化
  • 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略
  • 目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍

Dropout正则化‼️

在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。

image-20260422151252240

  • 在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
  • 在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
    • 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
    • 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
    • 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
  • 在测试过程中,随机失活不起作用:==未知数据预测过程中,不允许出现神经元失活==
    • 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
    • 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[x_test] = x
    • 测试/推理模式:==model.eval(),作用是不允许神经元失活==
  • 缩放的必要性
    • 在训练阶段,将参与计算的神经元的输出除以(1-p)
    • 经过Dropout后的期望输出变为 E[x_dropout] = [(1-p) * x] / (1-p) = x,与测试阶段的期望输出一致
    • 训练模型:==model.train(),作用是允许神经元失活==
python
import torch

if __name__ == '__main__':
    # 神经网络层的数据输入数据
    input_data = torch.randint(1,10,size=(1,4),dtype=torch.float32)

    # 构建一层网络
    linear1 = torch.nn.Linear(4,5)

    # 创建Dropout随机失活层
    dropout = torch.nn.Dropout(p=0.4)

    # 线性回归加权求和计算
    x = linear1(input_data)

    # 激活函数计算
    x = torch.tanh(x)

    # Dropout随机失活:经过Dropout随机失活层以后,输出结果值变成0
    print("随机失活前的结果",x)
    x = dropout(x)
    print("随机失活后的结果",x)

批量归一正则化(Batch Normalization)

在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,==这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据==,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力。

image-20260422151422435

properties
深度学习中的批量归一化BN与机器学习中的标准化处理的区别是啥?
	机器学习:对所有的业务数据在输入给到算法之前,统一进行一次性的标准化处理
	深度学习:对输入进神经网络的每个批次的数据进行单独的标准化处理。例如:总共有100条样本,16条/批次,7批次,
			这7个批次各自计算均值和标准差,然后独立进行标准化处理

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

image-20260422151500220

λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换λ为系数,β为偏置;

eps 通常指为 1e-5,避免分母为 0;

E(x) 表示变量的均值;

Var(x) 表示变量的方差;

批量归一化的步骤如下:

  1. 计算均值和方差:对于每个神经元(即每一层的输入特征),计算该特征在一个小批量(batch)上的均值 $$μ_B$$ 和方差 $$\sigma_B^2$$,它们的计算公式如下:

    ​ $$μ_B=\frac{1}{m} \sum_{i=1}^{m} x_i$$

    ​ $$σ_B^2=\frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2$$

    其中 $$x_i$$ 表示小批量中的第 $$i$$ 个样本,$$m$$ 是小批量的样本数量。

  2. 标准化:然后,对每个样本的输入进行标准化,得到归一化的输出:

    ​ $$\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}$$

    其中,$$ϵ$$ 是一个小常数,用来避免除以零的情况。

  3. 缩放和平移:为了让网络能够恢复其学习能力,BN 层引入了两个可训练的参数 $$γ$$ 和 $$β$$,分别用于缩放平移

    ​ $$y_i = \gamma \hat{x}_i + β$$

    其中,$$γ$$ 和 $$β$$ 是可学习的参数,通过 γ 和 β,BN 层不再是简单的将每一层输入强行变为标准正态分布,而是允许网络学习更适合于该层的输入分布;规范化操作会丢失原始输入的一些信息,==而 $$γ$$ 和 $$β$$ 可以弥补这种信息损失==。

**批量归一化的作用:**输入到神经网络的输入层数据,我们可以进行量纲的处理,例如:标准化、归一化

  • **减少内部协方差偏移:**通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。

  • 加速训练:

    • 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
    • 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
  • **起到正则化作用:**批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。

  • **提升泛化能力:**由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。

==批量归一化层在计算机视觉领域使用较多==

Batch Normalization 的使用步骤:

  1. 在网络层后添加 BN 层:

    • 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前
    • 例如:Conv2d -> BN -> ReLU 或者 Linear -> BN -> ReLU。
  2. 训练时:==model.train()==

    • BN 层会计算当前批次的均值 $$μ$$ 和方差 $$σ²$$。
    • 然后,利用这两个统计量对当前批次的数据进行规范化。
    • 规范化后的数据会被缩放 $$γ$$ 和平移 $$β$$。
    • 同时,BN 层还会维护一个全局均值全局方差的移动平均值,用于推理阶段。
  3. 推理时:==model.eval()==

    • 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值全局方差
    • 同样,规范化后的数据会被缩放 $$γ$$ 和平移 $$β$$。
    python
    import torch
    
    if __name__ == '__main__':
        # 准备输入数据
        """
            样本数据结构是4维的,每个位置上的参数含义解释:
                1:样本中有1张图片
                2:图片的颜色通道个数C
                3:图片的高度H
                4:图片的宽度W
        """
        input_2d = torch.randn(size=(1,2,3,4))
    
        # 批量归一化BN层
        """
            BatchNorm2d中参数解释:
                num_features:在处理图片的时候,输入图片的通道数有几个
                eps:是一个小常数,为了防止分母为0
                affine:该值通常设置为True。表示神经网络内部自动的去学习得到公式中的γ和β系数
                
            
            类的作用解释:
                BatchNorm1d:主要用来处理文本内容。输入数据的形状是 N,num_features
                    N:样本数据条数
                    num_features:输入样本的特征个数
            
                BatchNorm2d:主要用来处理图片。输入数据的形状是 N,C,H,W。
                    N:图片的张数
                    C:图片的通道数。常见图片的通道数为3 RGB
                    H:图片的高度
                    W:图片的宽度
                    
                BatchNorm3d:主要用来处理视频(视频是有多张图片组成的)。输入数据的形状是 N,C,D,H,W。
                    N:图片的张数
                    C:图片的通道数。常见图片的通道数为3 RGB
                    D:是维度数
                    H:图片的高度
                    W:图片的宽度
        """
        bn2d = torch.nn.BatchNorm2d(num_features=2,eps=1e-5,momentum=0.1,affine=True)
    
        output = bn2d(input_2d)
        print("公式中的γ",bn2d.weight)
        print("公式中的β",bn2d.bias)

Released under the MIT License.