人工神经网络知识点讲解

知识导图

人工神经网络
├── 基础认知
│   ├── 神经网络的核心概念
│   ├── 神经元的工作机制
│   └── 网络的层级结构
├── 激活函数
│   ├── 激活函数的作用
│   ├── 常见激活函数:sigmoid/tanh/ReLU/Softmax
│   └── 激活函数的选择
├── 参数初始化
│   ├── 7种初始化方法
│   └── 初始化方法的选择
├── 损失函数
│   ├── 分类任务损失
│   └── 回归任务损失
├── 网络优化
│   ├── 优化算法
│   └── 学习率衰减
├── 正则化方法
│   ├── 正则化的作用
│   ├── Dropout 随机失活
│   └── 批量归一化
└── 实战案例
    └── 手机价格分类案例

核心名词解释

  • 人工神经网络 (ANN):也简称为神经网络 (NN),是一种模仿生物神经网络结构和功能的计算模型,用来拟合复杂的非线性关系。

  • 神经元:神经网络的基本单元,接收输入后进行加权求和,再通过激活函数输出结果,模拟生物神经元的工作方式。

  • 输入层 (Input Layer):神经网络的输入层,负责接收原始的输入数据,每个输入特征对应一个神经元。

  • 隐藏层 (Hidden Layers):输入层和输出层之间的层,负责对输入数据进行特征提取和变换,神经网络的深度由隐藏层的数量决定。

  • 输出层 (Output Layer):神经网络的输出层,负责生成最终的预测结果,根据任务的不同(分类 / 回归)有不同的设计。

  • 全连接层:相邻层的所有神经元都相互连接的层,也叫稠密层,每个连接都有对应的权重和偏置。

  • 内部状态值:神经元的加权求和结果,公式为 z = W⋅x\+b,是激活函数的输入。

  • 激活值:激活函数对内部状态值进行非线性变换后得到的结果,是神经元的最终输出。

  • 激活函数:为神经网络引入非线性因素的函数,让网络可以拟合复杂的非线性函数。

  • sigmoid:将输入映射到 (0,1) 区间的激活函数,常用于二分类的输出层。

  • tanh:将输入映射到 (-1,1) 区间的激活函数,是 sigmoid 的平移版本,解决了 sigmoid 的输出非零均值的问题。

  • ReLU:修正线性单元,是目前最常用的隐藏层激活函数,计算简单,缓解了梯度消失问题。

  • Softmax:多分类任务的输出激活函数,将输出转换为概率,所有输出的概率和为 1。

  • 参数初始化:对神经网络的权重和偏置进行初始赋值的过程,合适的初始化可以让网络更快收敛,避免梯度消失 / 爆炸。

  • Xavier 初始化:也叫 Glorot 初始化,用来平衡输入和输出的方差,适合 sigmoid、tanh 这类激活函数。

  • Kaiming 初始化:也叫 HE 初始化,针对 ReLU 这类激活函数设计,考虑了 ReLU 的半正则特性,让深层网络更容易训练。

  • 损失函数:用来衡量模型预测值和真实值之间差异的函数,是网络训练的优化目标。

  • 交叉熵损失:分类任务中常用的损失函数,衡量预测概率分布和真实分布的差异。

  • MAE 损失:也叫 L1 损失,回归任务的损失,计算预测和真实的绝对误差。

  • MSE 损失:也叫 L2 损失,回归任务的损失,计算预测和真实的均方误差。

  • 优化器:用来更新神经网络参数,最小化损失函数的算法。

  • 学习率衰减:随着训练进行逐渐降低学习率的策略,让模型训练更稳定。

  • Dropout:随机失活正则化,训练时随机让部分神经元失活,防止过拟合。

  • 批量归一化 (BN):对批量数据进行标准化和重构的层,缓解梯度消失,加速训练。

  • 正则化:用来防止模型过拟合的方法,通过限制模型的复杂度,提升模型的泛化能力。


一、神经网络基础认知

本章节学习目标:

  • 知道什么是人工神经网络

  • 了解神经网络的构成

  • 理解内部状态和激活值的概念

1.1 什么是人工神经网络

人工神经网络( Artificial Neural Network, 简写为 ANN)也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。

人脑可以看做是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号,树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。
在这里插入图片描述

1.2 神经元的工作机制

神经网络是由多个神经元组成,构建神经网络就是在构建神经元。

这个过程就像,来源不同树突 (树突都会有不同的权重) 的信息,进行的加权计算,输入到细胞中做加和,再通过激活函数输出细胞值。

在这里插入图片描述

1.3 神经网络的层级结构

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

  1. 输入层(Input Layer):即输入 x 的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。

  2. 输出层(Output Layer):即输出 y 的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。

  3. 隐藏层(Hidden Layers):输入层和输出层之间的层,负责对输入数据进行特征提取和变换,神经网络的深度由隐藏层的数量决定。

网络的特点:

  • 同一层的神经元之间没有连接

  • 第 N 层的每个神经元和第 N-1 层 的所有神经元相连,这就是全连接神经网络

  • 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递

  • 第 N-1 层神经元的输出就是第 N 层神经元的输入

  • 每个连接都有一个权重值(w 系数和 b 系数)

在这里插入图片描述

1.4 内部状态值和激活值

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

  • 内部状态值: z = W⋅x\+b

    • W:权重矩阵

    • x:输入值

    • b:偏置

  • 激活值: a=f\(z\)

    • f:激活函数

    • z:内部状态值

在这里插入图片描述


二、激活函数

本章节学习目标:

  • 知道常见的激活函数

  • 了解激活函数的作用

  • 掌握激活函数的选择方法

2.1 激活函数的作用

激活函数用于对每层的输出数据进行变换,进而为整个网络注入了非线性因素。此时,神经网络就可以拟合各种曲线。

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

在这里插入图片描述

2.2 常见的激活函数

2.2.1 sigmoid 激活函数

sigmoid 激活函数可以将输入映射到 (0,1) 的区间,它的公式和导数公式如下:
在这里插入图片描述
在这里插入图片描述

对应的绘制代码:

# 解决OMP冲突的代码
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

# 加这行!切换Matplotlib的后端,绕过PyCharm的兼容bug
import matplotlib
matplotlib.use('TkAgg')
# 导包
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 指定用Windows自带的微软雅黑,支持中文
plt.rcParams['axes.unicode_minus'] = False # 顺便解决负号显示成方框的问题
# 准备数据
# 准备 -20,20的等差张量,1000点 --> x
x = torch.linspace(start=-20,end=20,steps=1000,requires_grad=False)
print(x.shape,x.ndim)
y = torch.sigmoid(x)
print(y.shape,y.ndim)
# 绘制图像
# **todo 为了把值域图像和导数图像画到一张画布上,创建一个一行两列的画布**
fig,axs = plt.subplots(1,2)
axs[0].plot(x,y)
axs[0].grid()
axs[0].set_title('sigmoid 值域图像')
# plt.show()
print('=======1=======')
# 准备数据
# # 准备x,y数据 -> **todo 注意此处的x开启自动微分,y的本质是梯度**
x = torch.linspace(start=-20,end=20,steps=1000,requires_grad=True)
print(x.shape,x.ndim)
torch.sigmoid(x).sum().backward() # -> # backward():触发反向传播,PyTorch自动帮你计算Sigmoid对x的导数,结果会自动存在 x.grad 里
y = x.grad
print(y.shape,y.ndim)
# 绘制图像
axs[1].plot(x.detach(),x.grad)
axs[1].grid()
axs[1].set_title('Sigmoid 导数图像')
plt.show()
print('=======2=======')
2.2.2 tanh 激活函数

tanh 激活函数可以将输入映射到 (-1,1) 的区间,解决了 sigmoid 输出非零均值的问题,它的公式和导数公式如下:
在这里插入图片描述
在这里插入图片描述

对应的绘制代码:

# 创建画布和坐标轴
_, 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 是修正线性单元,是目前最常用的隐藏层激活函数,它的公式和导数公式如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对应的绘制代码:

# 创建画布和坐标轴
_, 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(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。

在这里插入图片描述

对应的使用代码:

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=-1)
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 其他激活函数

还有其他的常见激活函数,比如 Leaky ReLU、PReLU、ELU 等:

在这里插入图片描述

2.4 激活函数的选择方法

隐藏层的选择
  • 优先选择 ReLU 激活函数

  • 如果 ReLu 效果不好,那么尝试其他激活,如 Leaky ReLu 等。

  • 如果你使用了 ReLU, 需要注意一下 Dead ReLU 问题, 避免出现 0 梯度从而导致过多的神经元死亡。

  • 少用使用 sigmoid 激活函数,可以尝试使用 tanh 激活函数

输出层的选择
  • 二分类问题选择 sigmoid 激活函数

  • 多分类问题选择 softmax 激活函数

  • 回归问题选择 identity 激活函数

激活函数对比辨析

激活函数 输出区间 特点 适用场景
sigmoid (0, 1) 输出非零均值,容易梯度消失 二分类输出层
tanh (-1, 1) 输出零均值,缓解了 sigmoid 的部分问题 隐藏层(早期)
ReLU [0, +∞) 计算简单,缓解梯度消失,可能出现 Dead ReLU 隐藏层(主流)
Softmax (0, 1),总和为 1 输出概率分布 多分类输出层

三、参数初始化

本章节学习目标:

  • 了解常见的参数初始化方法

  • 掌握初始化方法的选择

3.1 常见的初始化方法

参数初始化是对神经网络的权重和偏置进行初始赋值的过程,常见的 7 种初始化方法如下:

基础初始化方法
  1. 均匀分布初始化:权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置为在 (-1/√d,1/√d) 均匀分布中生成当前神经元的权重,其中 d 为神经元的输入数量

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

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

  4. 全 1 初始化:将神经网络中的所有权重参数初始化为 1

  5. 固定值初始化:将神经网络中的所有权重参数初始化为某个固定值

进阶初始化方法
  1. kaiming 初始化(HE 初始化)
    HE 初始化分为正态分布的 HE 初始化、均匀分布的 HE 初始化.

    • 正态分布的 he 初始化:它是从 [0, std] 中抽取样本的,std = sqrt\(2 / fan\_in\)

    • 均匀分布的 he 初始化:它从 [-limit,limit] 中的均匀分布中抽取样本,limit是 sqrt\(6 / fan\_in\)

    • fan_in:输入层神经元的个数

  2. xavier 初始化(Glorot 初始化)
    该方法也有两种,一种是正态分布的 xavier 初始化、一种是均匀分布的 xavier 初始化.

    • 正态化的 Xavier 初始化:它是从 [0, std] 中抽取样本的,std = sqrt\(2 / \(fan\_in \+ fan\_out\)\)

    • 均匀分布的 Xavier 初始化:[-limit,limit] 中的均匀分布中抽取样本,limit 是 sqrt\(6 / \(fan\_in \+ fan\_out\)\)

    • fan_in:输入层神经元的个数, fan_out:输出层神经元个数

3.2 初始化的代码示例

import torch
import torch.nn.functional as F
import torch.nn as nn

# 1. 均匀分布随机初始化
def test01():
    linear = nn.Linear(5, 3)
    # 从0-1均匀分布产生参数
    nn.init.uniform_(linear.weight)
    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)
    print(linear.weight.data)

    # kaiming 均匀分布初始化
    linear = nn.Linear(5, 3)
    nn.init.kaiming_uniform_(linear.weight)
    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.3 初始化方法的选择

根据激活函数选择
  • Sigmoid/Tanh:选择 Xavier 初始化

  • ReLU/Leaky ReLU:选择 kaiming 初始化

根据网络深度选择
  • 浅层网络:随机初始化即可

  • 深层网络:需要考虑方差平衡,如 Xavier 或 kaiming 初始化

初始化方法对比辨析

初始化方法 核心特点 适用激活函数 适用场景
均匀 / 正态随机 简单随机初始化 浅层网络的激活函数 简单的浅层网络
全 0 / 全 1 / 固定值 固定值初始化 不推荐,容易导致对称问题 仅偏置可以使用,权重不推荐
Xavier 初始化 平衡输入输出方差 Sigmoid、Tanh 早期的深层网络
Kaiming 初始化 适配 ReLU 的半正则特性 ReLU、Leaky ReLU 现代深层网络

四、神经网络搭建实战

我们来构建一个简单的神经网络模型,结合我们学到的激活函数和参数初始化:

在这里插入图片描述

搭建步骤

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

  1. \_\_init\_\_方法中定义网络中的层结构,主要是全连接层,并进行初始化

  2. forward方法,在实例化模型的时候,底层会自动调用该函数。该函数中为初始化定义的 layer 传入数据,进行前向传播等。

代码实现

from torch.nn import Module, Linear
import torch
# TODO 1.自定义模型类继承Module类
class My_Model(Module):
    # TODO 2.重写init魔法方法和forward前向传播方法
    def __init__(self, *args, **kwargs):
        # 1.调用父类的init初始化方法
        super().__init__(*args, **kwargs)
        # TODO 定义神经网络结构
        self.linear1 = Linear(3, 3)
        self.linear2 = Linear(3, 2)
        self.out = Linear(2, 2)
        # 3.参数初始化(生成权重矩阵)
        # 隐藏层初始化权重矩阵
        torch.nn.init.xavier_normal_(self.linear1.weight)
        torch.nn.init.kaiming_normal_(self.linear2.weight)

    def forward(self, x):
        #  TODO 前向传播计算(每层都是加权求和+激活函数)
        x = torch.sigmoid(self.linear1(x))
        x = torch.relu(self.linear2(x))
        # 下面-1代表最后一维, 也就是按照每个样本概率和为1.
        x = torch.softmax(self.out(x), dim=-1)
        return x

# TODO 3.创建模型对象并使用模型对象
# 创建模型对象
model = My_Model()  # 自动调用init魔法方法
# 准备数据集(正态分布数据)
torch.manual_seed(66)
data = torch.randn(5, 3)  # 5个样本,3个特征
# 把数据传入模型对象
output = model(data)  # 自动调用forward方法
print(output)

五、损失函数

本章节学习目标:

  • 知道分类任务的损失函数

  • 知道回归任务的损失函数

5.1 什么是损失函数

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

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

  • 损失函数(Loss Function)

  • 代价函数(Cost Function)

  • 目标函数(Objective Function)

在这里插入图片描述

5.2 分类任务的损失函数

5.2.1 多分类任务:交叉熵损失

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

在这里插入图片描述

其中:

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

  • 而 f (x) 是样本属于某一类别的预测分数

  • S 是 softmax 激活函数,将属于某一类别的预测分数转换成概率

  • L 用来衡量真实值 y 和预测值 f (x) 之间差异性的损失结果

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

在这里插入图片描述

上图中的交叉熵损失为:

在这里插入图片描述

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

在这里插入图片描述

在 pytorch 中使用nn\.CrossEntropyLoss\(\)实现,代码如下:

# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。nn.CrossEntropyLoss()=softmax + 损失计算
def test01():
    # 设置真实值: 可以是热编码后的结果也可以不进行热编码
    # 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)
    # 实例化交叉熵损失
    loss = nn.CrossEntropyLoss()
    # 计算损失结果
    my_loss = loss(y_pred, y_true).detach().numpy()
    print('loss:', my_loss)
5.2.2 二分类任务:二分类交叉熵损失

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

在这里插入图片描述

其中:

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

  • 而 y^ 是样本属于某一类别的预测概率

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

在 pytorch 中实现时使用nn\.BCELoss\(\),代码如下:

def test02():
    # 1 设置真实值和预测值
    # 预测值是sigmoid输出的结果
    y_pred = torch.tensor([0.6901, 0.5459], requires_grad=True)
    y_true = torch.tensor([0, 1], dtype=torch.float32)
    # 2 实例化二分类交叉熵损失
    loss = nn.BCELoss()
    # 3 计算损失
    my_loss = loss(y_pred, y_true).detach().numpy()
    print('loss:', my_loss)

5.3 回归任务的损失函数

5.3.1 MAE 损失函数(L1 Loss)

Mean absolute loss (MAE) 也被称为 L1 Loss,是以绝对误差作为距离。损失函数公式:

在这里插入图片描述

在这里插入图片描述

特点是:

  1. 由于 L1 loss 具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他 loss 中作为约束。

  2. L1 loss 的最大问题是梯度在零点不平滑,导致会跳过极小值。

在 pytorch 中使用nn\.L1Loss\(\)实现,代码如下:

# 计算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 损失函数(L2 Loss)

Mean Squared Loss (MSE) 也被称为 L2 Loss,是以均方误差作为距离。损失函数公式:

在这里插入图片描述
在这里插入图片描述

特点是:

  1. L2 loss 的梯度会随着误差减小而减小,因此更容易收敛到最小值。

  2. 对异常值更敏感,因为它会对大的误差进行平方惩罚。

在 pytorch 中使用nn\.MSELoss\(\)实现,代码如下:

# 计算inputs与target之差的平方
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('loss:', my_loss)

损失函数对比辨析

损失函数 适用任务 特点 适用场景
交叉熵损失(CrossEntropy) 多分类 衡量预测概率分布和真实分布的差异,梯度稳定 多分类任务
二分类交叉熵损失(BCELoss) 二分类 针对二分类的交叉熵,配合 sigmoid 使用 二分类任务
MAE 损失(L1 Loss) 回归 对异常值不敏感,零点梯度不平滑 回归任务,对异常值鲁棒的场景
MSE 损失(L2 Loss) 回归 梯度随误差减小,更容易收敛,对异常值敏感 回归任务,数据质量较好的场景

六、网络优化方法

本章节学习目标:

  • 了解常见的优化算法

  • 掌握学习率的调整方法

6.1 梯度下降优化算法

梯度下降是神经网络优化的基础算法,常见的梯度下降变种有:

  • 批量梯度下降(BGD):使用全部样本计算梯度,更新稳定但速度慢

  • 随机梯度下降(SGD):使用单个样本计算梯度,速度快但波动大

  • 小批量梯度下降(MBGD):使用小批量样本,平衡速度和稳定性,是目前的主流

在这里插入图片描述

6.2 带动量的梯度下降

动量(Momentum)是模拟物理中的动量概念,积累之前的梯度方向,加速收敛,减少震荡:

在这里插入图片描述

6.3 自适应学习率算法

6.3.1 AdaGrad

自适应调整每个参数的学习率,对稀疏数据效果好,但学习率会单调递减:

在这里插入图片描述

6.3.2 RMSProp

解决 AdaGrad 学习率递减的问题,使用指数移动平均来累积梯度:

在这里插入图片描述

6.3.3 Adam

结合了动量和 RMSProp 的优点,是目前最常用的优化算法,自适应调整学习率,收敛速度快:

在这里插入图片描述

6.4 学习率衰减

随着训练的进行,逐渐降低学习率,让模型在训练后期更稳定,避免震荡:

6.4.1 等间隔学习率衰减

每经过固定的轮次,就将学习率乘以一个衰减系数:

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 = list(), 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)/2.0 # 目标函数
            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()
6.4.2 指定间隔学习率衰减

在指定的轮次进行学习率衰减,更灵活:

在这里插入图片描述

def test_MultiStepLR():
    torch.manual_seed(1)
    LR = 0.1
    iteration = 10
    max_epoch = 200
    weights = torch.randn((1), requires_grad=True)
    target = torch.zeros((1))
    print('weights--->', weights, 'target--->', target)
    optimizer = optim.SGD([weights], 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 = torch.pow((weights - target), 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()
6.4.3 按指数学习率衰减

学习率按指数规律衰减:

在这里插入图片描述

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)/2.0
            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()

优化方法对比辨析

优化算法 核心特点 适用场景
SGD 简单的随机梯度下降,收敛慢,容易震荡 简单任务,数据量小
SGD+Momentum 加入动量,加速收敛,减少震荡 大部分任务,稳定训练
AdaGrad 自适应学习率,适合稀疏数据 稀疏数据,NLP 任务
RMSProp 解决 AdaGrad 学习率递减问题 深度学习任务
Adam 结合动量和自适应学习率,收敛快 主流深度学习任务,默认首选

七、正则化方法

本章节学习目标:

  • 知道正则化的作用

  • 掌握随机失活 DropOut 策略

  • 知道 BN 层的作用

7.1 正则化的作用

在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化。

神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略。

在这里插入图片描述

7.2 Dropout 正则化

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

在训练过程中,Dropout 的实现是让神经元以超参数 p 的概率停止工作或者激活被置为 0, 未被置为 0 的进行缩放,缩放比例为 1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数。

在测试过程中,随机失活不起作用。

在这里插入图片描述

代码示例:

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)
    print("未失活FC层的输出结果:\n", y)

    y =  dropout(y)
    print("失活后FC层的输出结果:\n", y)

输出结果:

未失活FC层的输出结果:
 tensor([[-0.8369, -5.5720,  0.2258,  3.4256,  2.1919]],
       grad_fn=<AddmmBackward0>)

失活后FC层的输出结果:
 tensor([[-1.3949, -9.2866,  0.3763,  0.0000,  3.6531]], grad_fn=<MulBackward0>)

7.3 批量归一化(Batch Normalization)

批量归一化是对数据标准化,再对数据重构(缩放 + 平移),如下所示:

在这里插入图片描述

其中:

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

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

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

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

批量归一化层在计算机视觉领域使用较多,它可以:

  1. 缓解梯度消失 / 爆炸问题

  2. 允许使用更高的学习率

  3. 降低对初始化的敏感程度

  4. 有轻微的正则化效果

代码示例:

import torch
import torch.nn as nn

m = nn.BatchNorm2d(2, eps=1e-05, momentum=0.1, affine=True)  # affine参数设为True表示weight和bias将被使用
input = torch.randn(1, 2, 3, 4)
print("input-->", input)

output = m(input)
print("output-->", output)
print(output.size())

print(m.weight)
print(m.bias)

输出结果:

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

八、实战案例:手机价格分类

本章节学习目标:

  • 掌握构建分类模型的完整流程

  • 动手实践整个训练和评估过程

8.1 需求分析

小明创办了一家手机公司,他不知道如何估算手机产品的价格。为了解决这个问题,他收集了多家公司的手机销售数据。该数据为二手手机的各个性能的数据,最后根据这些性能得到 4 个价格区间,作为这些二手手机售出的价格区间。

我们需要帮助小明找出手机的功能(例如:RAM 等)与其售价之间的某种关系。我们可以使用机器学习的方法来解决这个问题,也可以构建一个全连接的网络。

需要注意的是:在这个问题中,我们不需要预测实际价格,而是一个价格范围,它的范围使用 0、1、2、3 来表示,所以该问题也是一个分类问题。

I在这里插入图片描述

8.2 完整流程

我们按照四个步骤来完成这个任务:

  1. 准备训练集数据

  2. 构建要使用的模型

  3. 模型训练

  4. 模型预测评估

8.2.1 导入工具包
# 导入相关模块
import torch
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import time
8.2.2 构建数据集

数据共有 2000 条,其中 1600 条数据作为训练集,400 条数据用作测试集。 我们使用 sklearn 的数据集划分工作来完成。并使用 PyTorch 的 TensorDataset 来将数据集构建为 Dataset 对象,方便构造数据集加载对象。

# 构建数据集
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)
    # 构建数据集,转换为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))
    # 返回结果
    return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))

输出结果:

输入特征数: 20 
分类个数: 4
8.2.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))
        output = self.linear3(x)
        # 获取数据结果
        return output

模型实例化的输出:

在这里插入图片描述

8.2.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)
    # 损失函数
    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:
            # 将数据送入网络中进行预测
            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))
    # 模型保存
    torch.save(model.state_dict(), 'model/phone.pth')

训练的输出结果:

在这里插入图片描述

8.2.5 模型评估

使用训练好的模型,对未知的样本的进行预测的过程。我们这里使用前面单独划分出来的验证集来进行评估。

def test(valid_dataset,input_dim,class_num):

    # 加载模型和训练好的网络参数
    model = PhonePriceModel(input_dim, class_num)
    model.load_state_dict(torch.load('model/phone.pth'))

    # 构建加载器
    dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)

    # 评估测试集
    correct = 0
    # 遍历测试集中的数据
    for x, y in dataloader:
        #  将其送入网络中
        output = model(x)
        # 获取类别结果
        y_pred = torch.argmax(output, dim=1)
        # 获取预测正确的个数
        correct += (y_pred == y).sum()
    # 求预测精度
    print('Acc: %.5f' % (correct.item() / len(valid_dataset)))

输出结果:

Acc: 0.64250

8.3 调优方向

我们前面的网络模型在测试集的准确率为: 0.64250, 我们可以通过以下方面进行调优:

  1. 优化方法由 SGD 调整为 Adam

  2. 学习率由 1e-3 调整为 1e-4

  3. 对数据进行标准化

  4. 增加网络深度,即:增加网络参数量

  5. 调整训练轮次


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐