神经网络初识

特点

一、优点

  1. 精度高,性能优于其他的机器学习方法,甚至在某些领域超过了人类
  2. 可以近似任意的非线性函数
  3. 近年来在学界和业界受到了热捧,有大量的框架和库可供调用

二、缺点

  1. 属于黑箱模型,很难解释模型内部的工作逻辑
  2. 训练耗时久,对算力要求高
  3. 网络结构复杂,需要手动调试大量超参数
  4. 在小规模数据集上效果差,容易出现过拟合问题

大致演示

同层不同神经元之间拿不到彼此的数据

image-20260708213628849

image-20260708215423816

神经网络的演示地址

激活函数

激活函数用于对每层的输出数据进行变换,进而为整个网络注入了非线性因素。此时,神经网络就可以拟合各种曲线。如果没有激活函数,不管你堆多少层全连接,整个网络在数学上仍然只是一个线性变换——因为线性变换的复合还是线性变换,W2(W1x + b1) + b2 永远可以化简成 W'x + b'。所以多层和单层没区别,都只能画直线(或超平面)。

激活函数的作用就是在每一层输出后做一次非线性”扭折”,把那个可化简的线性链打断。一旦打断,网络就有了表达弯曲、拐折、复杂边界的能力,这就是万能逼近定理说的 ——足够宽的单隐层网络能逼近任意连续函数。

Sigmoid函数

image-20260708221936864

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
x = torch.linspace(-10, 10, 1000, requires_grad=True)  # requires_grad=True 需梯度以便求导
fig, ax = plt.subplots(1, 2) # 1 行 2 列子图
fig.set_size_inches(12, 4)
ax[0].plot(x.data, torch.sigmoid(x).data, "purple") # 左图:sigmoid 函数值
ax[0].set_title("sigmoid(x)")
ax[0].spines["top"].set_visible(False)
ax[0].spines["right"].set_visible(False)
ax[0].spines["left"].set_position("zero")
ax[0].spines["bottom"].set_position("zero")
ax[0].axhline(0.5, color="gray", alpha=0.7, linewidth=1)
ax[0].axhline(1, color="gray", alpha=0.7, linewidth=1)
torch.sigmoid(x).sum().backward() # 反向传播,使 x.grad 为 sigmoid 的导数
ax[1].plot(x.data, x.grad, "purple") # 右图:导数
ax[1].set_title("sigmoid\'(x)")
ax[1].spines["top"].set_visible(False)
ax[1].spines["right"].set_visible(False)
ax[1].spines["left"].set_position("zero")
ax[1].spines["bottom"].set_position("zero")
ax[1].set_ylim(0, 0.3)
plt.show()
  1. torch.linspace(start, end, steps) 的作用是在指定区间内均匀地取若干个点

    plt.subplots(1, 2) 为创建一张画布和一组子图,两个数字是”行数、列数”:1, 2 就是 1 行 2 列,并排放两个子图。它返回两个东西:

    • fig – 整张画布(容器)
    • ax – 子图数组,这里是长度 2 的+数组,ax[0] 是左图,ax[1] 是右图

    所以代码里能写 ax[0].plot(...) 画左边 sigmoid,ax[1].plot(...) 画右边导数。如果写 plt.subplots(2, 2) 就是 2 行 2 列四个子图,ax 变成二维数组,用 ax[0][1] 这种方式访问。

    • ax[i][j] 就是”第 i 行第 j 列”那个子图,跟二维数组的习惯完全一致:ax[0][0] 左上、ax[0][1] 右上、ax[1][0] 左下、ax[1][1] 右下。
    • 不过注意 matplotlib 会自动”压扁”只有一行或一列的情况。 subplots(1, 2) 返回的 ax一维的(长度 2),所以用 ax[0]ax[1],写 ax[0][0] 会报错。只有行列都大于 1(比如 subplots(2, 2))时,ax 才是真正的二维数组,才能用 ax[0][1] 两个下标访问
  2. fig.set_size_inches(12, 4)

    设置整张画布的物理尺寸,宽 12 英寸、高 4 英寸。因为两个子图是并排的,宽给足、高压低,这样左右两个图各自看起来扁长,适合画函数曲线(横轴跨度大、纵轴变化小)。不设的话 matplotlib 用默认尺寸(约 6.4×4.8),两个子图挤在一起会很小。

    ax[0].axhline(0.5, color="gray", alpha=0.7, linewidth=1)

    • 0.5 — 在 y=0.5 处画这条线
    • color="gray" — 灰色
    • alpha=0.7 — 透明度,0 全透明、1 不透明,0.7 是淡淡的灰
    • linewidth=1 — 线宽 1 磅

    放在 sigmoid 的场景里,这条线是有意义的:sigmoid 在 x=0 时正好等于 0.5,而且它是个 S 形从 0 涨到 1 的函数。所以画 y=0.5 这条参考线,能帮你一眼看清”曲线在哪儿穿过中点”;代码里紧接着的 axhline(1) 则标出 sigmoid 的上限。

    对应的还有 axvline(vertical),画竖直参考线,用法一样。

Tanh函数

.image-20260710213809369

RULU函数

image-20260709212520505

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fig, ax = plt.subplots(1, 2)
fig.set_size_inches(12, 4)
ax[0].plot(x.data, torch.relu(x).data, "purple")
ax[0].set_title("relu(x)")
ax[0].spines["top"].set_visible(False)
ax[0].spines["right"].set_visible(False)
ax[0].spines["left"].set_position("zero")
ax[0].spines["bottom"].set_position("zero")
torch.relu(x).sum().backward() # 反向传播求 ReLU 梯度
ax[1].plot(x.data, x.grad, "purple")
ax[1].set_title("relu\'(x)")
ax[1].spines["top"].set_visible(False)
ax[1].spines["right"].set_visible(False)
ax[1].spines["left"].set_position("zero")
ax[1].spines["bottom"].set_position("zero")
plt.show()

Softmax

image-20260710213432249

可以直接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
#自定义环境 library/python3-be38bd9cb461ab775f44082e06df14e1
# 整体流程:构造一维输入 → softmax 将原始值转为概率分布(和为 1)→ 打印对比
x = torch.tensor([2.0, 1.0, 0.1])
output = torch.softmax(x, dim=0) # dim=0:沿第 0 维(唯一维)做 softmax
print("输入:", x)
print("Softmax输出:", output) # 各元素 ∈ (0,1),总和 = 1
print("输出之和:", output.sum())
# 二维示例:按行做 softmax(每行和为 1)
x2 = torch.tensor([[1.0, 2.0, 3.0],
[1.0, 1.0, 1.0]])
output2 = torch.softmax(x2, dim=1) # dim=1:沿列方向(每行内部)做 softmax
print("二维Softmax输出:\n", output2)
print("每行之和:", output2.sum(dim=1)) # 均为 1
  1. softmax 的公式是对每个元素先取指数、再除以指数之和: output[i] = exp(x[i]) / Σ exp(x[j])。比如 [2.0, 1.0, 0.1]

    x exp(x) 除以总和(11.21) output
    2.0 7.389 7.389/11.21 0.659
    1.0 2.718 2.718/11.21 0.242
    0.1 1.105 1.105/11.21 0.099

    可获得以下信息:

    • 每个输出都落在 (0, 1),可以当”概率”看
    • 三个加起来 = 1,这就是 output.sum() 打印的东西

    还值得注意一点:原始值 2.01.0 的两倍,但 softmax 之后 0.6590.242 的约 2.7 倍 – 指数运算放大了差距,大的值优势更明显。这就是 softmax 用来挑”最可能的类别”的原因。

  2. 二维dim 的含义

    1
    2
    3
    x2 = torch.tensor([[1.0, 2.0, 3.0],
    [1.0, 1.0, 1.0]])
    output2 = torch.softmax(x2, dim=1)

    dim 决定沿哪个轴做归一化,也就是让那个轴方向上的元素之和为 1

    • dim=1 – 沿列方向,在每一行内部做 softmax,所以每行和为 1
    • dim=0 – 沿行方向,在每一列内部做,每列和为 1

    dim=1,所以分别看每一行。第一行 [1.0, 2.0, 3.0]

    1
    2
    exp = [2.72, 7.39, 20.09],总和 30.19
    output = [0.090, 0.245, 0.665] # 3 最大,拿到 0.665

    第二行 [1.0, 1.0, 1.0] 三个相等

    1
    output = [0.333, 0.333, 0.333]   # 完全均分

    output2.sum(dim=1) 打印的就是每行之和,两个都是 1。这个例子展示了 softmax 的两种行为:值有差距时”放大优势”,值相等时”平均分配”。

参数初始化

在训练开始前,给 W 和 b 设一个初始值。我们选择哪个激活函数以及如何初始化参数,可以决定优化算法收敛的速度有多快;糟糕的选择可能会导致我们在训练时遇到梯度爆炸或梯度消失

常数初始化

所有权重参数初始化为一个常数,即

image-20260712102632372

这里 J 为全1矩阵,*k 为初始化的常数。

注意:将权重初始值设为 0 将无法正确进行学习。严格地说,不能将权重初始值设成一样的值。因为这意味着反向传播时权重全部都会进行相同的更新,被更新为相同的值(对称的值)。这使得神经网络拥有许多不同的权重的意义丧失了。为了防止”权重均一化”(瓦解权重的对称结构),必须随机生成初始值。

秩初始化

权重参数初始化为单位矩阵,即

image-20260712102903862

这里 I 为单位矩阵,即主对角线上元素为 1,其它元素为 0。秩初始化多用于 RNN 等需要保持恒等映射的场景,全连接层中较少使用。

正态分布初始化

权重参数按指定均值μ与标准差σ正态分布初始化。因为不能直接将权重初始化为相同的常数,所以需要对参数进行随机初始化。最常见的随机分布就是
正态分布(也叫 高斯分布),记作 X ~ N(μ, σ2)。

其概率密度函数为:

image-20260712103447941

均匀分布初始化

权重参数在指定区间内均匀分布初始化。均匀分布一般记作 X ~ U(a,b)。

其概率密度函数为:

image-20260712103513348

Xavier 初始化(Glorot 初始化)

Xavier初始化根据输入和输出的神经元数量调整权重的初始范围,确保每一层的输出方差与输入方差相近。

image-20260712103604198

Xavier 初始化参数适用于 Sigmoid 和 Tanh 等激活函数,能有效缓解梯度消失或爆炸问题。其推导假设激活函数在 0 附近近似线性且对称,因此不适用于 ReLU(输出恒非负,破坏了对称性)。

He 初始化(Kaiming 初始化)

He初始化根据输入的神经元数量调整权重的初始范围。其方差为 Xavier 的 2 倍,以补偿 ReLU 将一半神经元置零导致的方差减半。

image-20260712103953768

He 初始化参数主要适用于 ReLU 及其变体(如 Leaky ReLU)激活函数。

搭建神经网络

image-20260712113502295

自定义模型

接口:继承 nn.Module,实现 __init__forward(input)

功能:所有神经网络模块的基类;自定义模型需继承此类并实现前向传播。

参数:在 __init__ 中定义子模块与参数,在 forward 中接收输入并返回输出。

在神经网络框架中,由多个层组成的组件称之为 模块(Module)。在 PyTorch 中模型和各网络层都是 Module。在定义时需主要实现两个方法:

  • __init__:定义网络各层的结构,并初始化参数。
  • forward:根据输入进行前向传播,并返回输出。计算其输出关于输入的梯度,可通过其反向传播函数进行访问(通常自动发生)。forward方法是每次调用的具体实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ModelDemo(nn.Module):
# todo: 1. 在init魔法方法中,完成初始化:父类成员及神经网络搭建。
def __init__(self):
# 1.1初始化父类成员
super().__init__()
# 1.2 搭建神经网络 → 隐藏层 + 输出层
# 隐藏层1: 输入特征数 3, 输出特征数 3
self.linear1 = nn.Linear(3, 3)
# 隐藏层2: 输入特征数 3, 输出特征数 2
self.linear2 = nn.Linear(3, 2)
# 输出层: 输入特征数 2, 输出特征数 2
self.output = nn.Linear(2, 2)
# 1.3 对隐藏层进行参数初始化.
# 隐藏层1
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)

# 隐藏层2
nn.init.kaiming_normal_(self.linear2.weight)
nn.init.zeros_(self.linear2.bias)

# todo: 1.2 前向传播:输入层 -> 隐藏层 -> 输出层 (forward名字不可更改)
def forward(self, x):
# 1.1 第一层 隐藏层计算:加权求和 + 激活函数.
# 分解版写法.
# x = self.linear1(x) # 加权求和
# x = torch.sigmoid(x) # 激活函数
# 合并版写法.
x = torch.sigmoid(self.linear1(x))

# 1.2 第2层 隐藏层计算:加权求和 + 激活函数(ReLU)
x = torch.relu(self.linear2(x))

# 1.3 第3层 输出层计算:加权求和 + 激活函数(Softmax)
# dim=-1, 表示按行计算,一条样本一条样本的处理。
x = torch.softmax(self.output(x), dim=-1)
return x

模型训练

查看模型结构和参数数量

接口:torchsummary.summary(model, input_size, batch_size=None, device='cuda')

功能:打印模型结构和各层参数数量。

参数:

  • model:待查看的模型
  • input_size:输入形状(如 (3,) 表示 3 个特征)
  • batch_size:可选,批大小
  • device:运行设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def train():
# 1. 创建模型对象.
my_model = ModelDemo()
# print(f'my_model: {my_model}')

# 2. 创建数据集样本, 随机生成.
data = torch.randn(size=(5, 3))
print(f'data: {data}')
print(f'data.shape: {data.shape}') # (5行, 3列)
print(f'data.requires_grad: {data.requires_grad}') # False

# 3. 调用神经网络模型 → 进行模型训练.
output = my_model(data) # 底层自动调用了 forward()方法, 进行 前向传播.
print(f'output: {output}')
print(f'output.shape: {output.shape}') # (5行, 2列)
print(f'output.requires_grad: {output.requires_grad}') # True
print('-' * 30)
# 4. 计算 和 查看模型参数.
print('==================== 计算模型参数 ====================')
# 参1: (神经网络)模型对象, 参2: 输入数据维度(5行3列)
summary(my_model, input_size=(3,))

对于这里的summary输出值为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
               (type)                Output Shape          Param #
================================================================
Linear-1 [-1, 3] 12
Linear-2 [-1, 2] 8
Linear-3 [-1, 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
----------------------------------------------------------------

进程已结束,退出代码为 0

其中:

  • Layer (type) – 层的类型和序号。模型有三个 nn.Linear(全连接层),按前向传播顺序编号 Linear-1/2/3,对应代码里的 linear1linear2output
  • Output Shape – 这一层的输出张量形状。[-1, 2]-1 是 batch 维(动态,不固定),,2 是特征数。
  • Param # – 这一层的可训练参数数量(权重 + 偏置)。

可训练的参数如何计算出来的:nn.Linear(in, out) 的参数 = in × out(权重矩阵) + out(偏置)。

nn.Linear(in, out) 的参数运算逻辑

Linear-2 举例,它是 nn.Linear(3, 2),意思是输入 3 个特征,输出 2 个特征。

权重矩阵:in × out 个

把输入想象成 3 个数 [x1, x2, x3],输出是 2 个数 [y1, y2]每个输出都是所有输入加权求和算出来的:

1
2
y1 = w11·x1 + w12·x2 + w13·x3 + b1
y2 = w21·x1 + w22·x2 + w23·x3 + b2

看那些 w,一共需要 2 × 3 = 6 个权重(每个输出 3 个,共 2 个输出)。排成矩阵就是 2 行 3 列,形状 (out, in) = (2, 3),所以权重数量 = out × in = in × out

偏置:out 个

再看 b1b2,每个输出配一个偏置,共 2 个 = out 个。偏置不乘输入,直接加上去,作用是让直线能上下平移(不固定过原点)。

加起来

in × out(权重)+ out(偏置)= 3 × 2 + 2 = 6 + 2 = 8 个参数,正好是表格里 Linear-2 那行的 Param #

image-20260715231158202

总结

  1. 5行3列,代表5个数据,每个数据有三个特征。此为输入层

  2. 隐藏层1接收上一层所有输出(第一个数据的三个特征),然后把这三个数据整合起来,整合的方法就是用本层的权重(反向传播可不断更新这个权重)和一个偏置值进行训练整合。单层可训练参数 = 神经元个数 × 上一层的输出数(=每个神经元的权重数) + 神经元个数(=每个神经元1个偏置)

    1. 样本1: [x1, x2, x3]  ──┐
      样本2: [x1, x2, x3]  ──┤
      样本3: [x1, x2, x3]  ──┼──> 同一个神经元(3个权重+1偏置),每条各算一次
      样本4: [x1, x2, x3]  ──┤
      样本5: [x1, x2, x3]  ──┘
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97

      神经元看到"3 个特征" , 这个 "3 个特征的加权求和" 操作根据 <span style="color:#FF00FF">GPU/向量化计算(不是写个 for 循环逐条算,而是把 5 条样本摞成一个矩阵,一次矩阵乘法全算完。)</span>被**同时重复执行了 5 次**,每次喂一条样本。



      3. 隐藏层2重复隐藏层1

      4. 输出层重复隐藏层2

      5. 取下一个 batch,重复前向传播、算损失、反向传播、更新参数的过程 , 直到所有数据过了一遍(一个 epoch),再开始下一轮 epoch。

      # 损失函数

      神经网络中,需要以某个指标为线索来寻找最优权重参数;这个指标就是**损失函数(loss function)**。

      ## 分类任务

      ### 二分类任务损失函数

      ![image-20260720193339409](<PyTorch 进阶—人工神经网络/image-20260720193339409.png>)

      ### 多分类损失函数自带`Softmax`,其具体公式如下

      ![image-20260718210801468](<PyTorch 进阶—人工神经网络/image-20260718210801468.png>)

      **具体执行流程如下**

      ![image-20260718211404551](<PyTorch 进阶—人工神经网络/image-20260718211404551.png>)

      ## 回归任务

      ### MAE

      描述预测值与真实值之间差值绝对值的平均

      ![image-20260720194155017](<PyTorch 进阶—人工神经网络/image-20260720194155017.png>)

      ### MSE

      ![image-20260720203508451](<PyTorch 进阶—人工神经网络/image-20260720203508451.png>)

      ### Smooth L1

      可以理解为平滑L1函数:

      ![image-20260720204206276](<PyTorch 进阶—人工神经网络/image-20260720204206276.png>)

      当误差较小时(预测值与真实值之差的绝对值小于1),使用L2 Loss,使得损失函数平滑可导。

      当误差较大时时(预测值与真实值之差的绝对值大于1),用L1 Loss降低异常值的影响。

      # 神经网络优化方法

      ## 梯度下降算法回顾

      ### 基本概念

      梯度下降法(Gradient Descent)是一种用于最小化目标函数的迭代优化算法。核心是沿着目标函数(如损失函数)的负梯度方向逐步调整参数,从而逼近函数的最小值。梯度方向指示了函数增长最快的方向,因此负梯度方向是函数下降最快的方向。所以有:

      ![image-20260720211512214](<PyTorch 进阶—人工神经网络/image-20260720211512214.png>)

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

      **举个具体例子:**

      - 模型只有一个参数 `w`
      - 预测公式是 `y_pred = w * x`
      - 真实值是 `y = 3`。
      - 数据就一个样本:`x = 1, y = 3`。
      - 损失用平方误差:`L = (y_pred − y)² = (w − 3)²`

      这个损失函数在 `w = 3` 时最小(损失为 0)。现在从 `w = 0.5` 开始,学习率 `η = 0.1`,看看每一步怎么更新。

      1. 第0步:

      - 当前 `w = 0.5`,预测 `y_pred = 0.5`
      - 损失 `L = (0.5 − 3)² = 6.25`
      - 对 `w` 求导:`∂L/∂w = 2(w − 3) = 2(0.5 − 3) = −5`

      梯度是 `−5`,说明“往 w 增大的方向走,损失会下降”。负梯度方向就是 `+5`,所以我们要把 w 往大调。

      <span style="color:#DEB887">Q:既然往`w`增大的方向走,损失会下降。直接不就能得出把`w`增大吗,为什么还要专门说负梯度方向就是 `+5`?</span>

      <span style="color:#DEB887">A:两者完全等价。但“负梯度方向”这个说法,本质是在描述一条不变的规则:</span>

      > <span style="color:#DEB887">不管梯度是什么,永远朝着梯度的反方向更新参数。</span>

      <span style="color:#DEB887">当梯度是 `−5` 时:w ← w − η × (−5) = w + η×5 ← 这就是把 w 往大调</span>

      <span style="color:#DEB887">当梯度是 `+5` 时:w ← w − η × (+5) = w − η×5 ← 把 w 往小调 </span>

      <span style="color:#DEB887">公式不需要你每轮手动判断“这次该增大还是减小”,减号会自动处理。“沿负梯度方向更新”可以不用思考地套用到所有情况。</span>

      <span style="color:#DEB887">更重要的是,真实网络有成千上万个参数,梯度是一个向量,每个参数分量可能有正有负,不可能每个都说“把参数增大”——到底增大哪个、增大多少?但统一用 `参数 ← 参数 − η × 梯度` 这条规则,每个参数会自动按自己梯度的反方向调整。</span>

      更新新的值

      新 w = 旧 w − 学习率 × 梯度 = 0.5 − 0.1 × (−5) = 0.5 + 0.5 = 1.0 //现在w更接近真实值w=3了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58

    2. 第1步

    `w = 1.0`,梯度变成 `2(1.0 − 3) = −4`

    更新:`w = 1.0 − 0.1 × (−4) = 1.4`

    3. 继续下去:

    | 轮次 | 当前 w | 梯度 2(w−3) | 新 w = w − 0.1×梯度 | 损失 |
    | ---- | ------------ | ------------ | ------------------- | ---- |
    | 0 | 0.5 | −5 | 1.0 | 6.25 |
    | 1 | 1.0 | −4 | 1.4 | 4.00 |
    | 2 | 1.4 | −3.2 | 1.72 | 2.56 |
    | 3 | 1.72 | −2.56 | 1.976 | 1.64 |
    | ... | 越来越接近 3 | 越来越接近 0 | 3 | 0 |

    **在深度学习中,梯度下降的几种方式的根本区别就在于 Batch Size 不同,如下表所示:**

    | 梯度下降方式 | Training Set Size | Batch Size | Number of Batches |
    | ------------ | ----------------- | ---------- | ----------------- |
    | BGD | N | N | 1 |
    | SGD | N | 1 | N |
    | Mini‑Batch | N | B | \(N/B+1\) |

    ### 训练过程

    在进行模型训练时,有三个基础的概念,从大到小是 Epoch → Iteration → Batch。

    1. Batch size:一次喂多少样本

    如果把50000 张图一次性塞进模型算梯度则会显存爆掉、太慢。所以每次只取一小批,比如 256 张,算一次损失、做一次反向传播、更新一次权重。这个 256 就是 batch size。

    2. *teration:更新一次权重叫一次迭代

    拿一个 batch(256 张)跑完前向 + 反向 + 权重更新,就是一次 iteration。所以一次 iteration 处理 256 个样本。

    3. Epoch:把全部数据看一遍叫一轮

    如果有50000张图片,50000 张图全部过一遍模型,就是一个 epoch。因为每次只处理 256 张,所以要分多批才能看完一遍。

    假设数据集有50000个训练样本,现在选择Batch Size=256对模型进行训练。

    - 每个Epoch要训练的图片数量:50000
    - 训练集具有的Batch个数:50000/256+1=196
    - 每个Epoch具有的Iteration 个数:196
    - 10个Epoch具有的Iteration个数:1960

    ## 反向传播算法

    **知识回顾**:`backward()`

    比如损失函数为`loss = ((w ** 2) / 2.0).sum()`,`loss.backward()`

    正向计算时,数值是这么流动的(假设`w`初始值为1.0):

    ```python
    w → w² → w²/2 → loss
  • w = 1.0w² = 1.0
  • 再算 w²/2 = 0.5
  • 最后这个 0.5 就是 loss。

backward() 要做的事情正好反过来:梯度从 loss 往回流,所以图写成:

1
loss ← w²/2 ← w

箭头往左表示梯度传播的方向。它把每一步的导数相乘(链式法则),最后得到 loss 对 w 的梯度:

1
2
3
loss 对 w²/2 的导数 = 1
w²/2 对 w 的导数 = w
两者相乘 = 1 × w = w

Q:为什么不直接正向求导,还非要加一个反向传播?

A:正向模式求导的话,如果想得到 loss 对每个参数的导数,就得把整个计算图“带导数”完整走一遍,如果每个参数都走一遍,那么100 万参数 = 100 万遍,时间复杂度很高。反向的话,从 loss 出发往回走,每过一个节点就把梯度传给它下面的所有参数,走一遍就能同时拿到全部参数的梯度。成本大约是一遍前向 + 一遍反向,和参数个数基本无关。

所以 w = 1.0 时梯度就是 1.0,这个结果自动存进 w.grad

w.grad中存的值会在w=w-学习率 * 梯度中拿出来。

更新完,格子里的 1.0 还在(step 不清空)。到下一轮。就要进行梯度清空:.zero_grad()

梯度下降优化方法

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

对于这些问题,出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam等

![image-20260823193941796](PyTorch 进阶—人工神经网络/image-20260823193941796.png)

优化前置方法:指数加权平均

我们最常见的算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的。指数加权平均指的是给每个数赋
予不同的权重求得平均数。移动平均数,指的是计算最近邻的N个数来获得平均数。

指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较
小),距离越近则对平均数的计算贡献就越大(权重越大)。

![image-20260823195623158](PyTorch 进阶—人工神经网络/image-20260823195623158.png)

β越小,1-β越大,也就越参考本次的值(Yt)。所以浮动越大。

梯度下降优化方法—动量算法Momenttum

算法流程

(指数加权)梯度计算公式:st=β * st-1 + (1-β) * gt

参数更新公式:wt=wt-1 - n * st

  • st是当前时刻指数加权平均梯度值。St-1是历史指数加权平均梯度值
  • gt是当前时刻的梯度值
  • β是调节权重系数,通常取0.9或0.99
  • n是学习率
  • wt是当前时刻模型权重参数

举个例子,假设:权重β为0.9

  1. 第一次梯度值:s1=g1=w1
  2. 第二次梯度值:s2= 0.9 * s1 + g2 * 0.1
  3. 第三次梯度值:s3= 0.9 * s2 + g3 * 0.1
  4. 第四次梯度值:s4=0.9 * s3 + g4 * 0.1
  • w表示初始梯度
  • g表示当前轮数计算出的梯度值
  • s表示历史梯度移动加权平均值

梯度下降公式中梯度的计算,就不再是当前时刻 t 的梯度值,而是历史梯度值的指数移动加权平均值。公式修改为

  • Wt = Wt-1 - η * St
  • Wt:当前时刻模型权重参数
  • St:当前时刻指数加权平均梯度值
  • η:学习率
作用
  • 当处于鞍点位置时,由于当前的梯度为0,参数无法更新。但是Momentum动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  • 由于mini-batch普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def Momenttum():
# 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()))