PyTorch 进阶—人工神经网络
神经网络初识
特点
一、优点
- 精度高,性能优于其他的机器学习方法,甚至在某些领域超过了人类
- 可以近似任意的非线性函数
- 近年来在学界和业界受到了热捧,有大量的框架和库可供调用
二、缺点
- 属于黑箱模型,很难解释模型内部的工作逻辑
- 训练耗时久,对算力要求高
- 网络结构复杂,需要手动调试大量超参数
- 在小规模数据集上效果差,容易出现过拟合问题
大致演示
同层不同神经元之间拿不到彼此的数据


激活函数
激活函数用于对每层的输出数据进行变换,进而为整个网络注入了非线性因素。此时,神经网络就可以拟合各种曲线。如果没有激活函数,不管你堆多少层全连接,整个网络在数学上仍然只是一个线性变换——因为线性变换的复合还是线性变换,W2(W1x + b1) + b2 永远可以化简成 W'x + b'。所以多层和单层没区别,都只能画直线(或超平面)。
激活函数的作用就是在每一层输出后做一次非线性”扭折”,把那个可化简的线性链打断。一旦打断,网络就有了表达弯曲、拐折、复杂边界的能力,这就是万能逼近定理说的 ——足够宽的单隐层网络能逼近任意连续函数。
Sigmoid函数

1 | x = torch.linspace(-10, 10, 1000, requires_grad=True) # requires_grad=True 需梯度以便求导 |
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]两个下标访问
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函数
.
RULU函数

1 | fig, ax = plt.subplots(1, 2) |
Softmax

可以直接调用
1 | #自定义环境 library/python3-be38bd9cb461ab775f44082e06df14e1 |
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.0是1.0的两倍,但softmax之后0.659是0.242的约 2.7 倍 – 指数运算放大了差距,大的值优势更明显。这就是softmax用来挑”最可能的类别”的原因。二维
dim的含义1
2
3x2 = torch.tensor([[1.0, 2.0, 3.0],
[1.0, 1.0, 1.0]])
output2 = torch.softmax(x2, dim=1)dim决定沿哪个轴做归一化,也就是让那个轴方向上的元素之和为 1dim=1– 沿列方向,在每一行内部做softmax,所以每行和为 1dim=0– 沿行方向,在每一列内部做,每列和为 1
dim=1,所以分别看每一行。第一行[1.0, 2.0, 3.0]1
2exp = [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 设一个初始值。我们选择哪个激活函数以及如何初始化参数,可以决定优化算法收敛的速度有多快;糟糕的选择可能会导致我们在训练时遇到梯度爆炸或梯度消失
常数初始化
所有权重参数初始化为一个常数,即

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

这里 I 为单位矩阵,即主对角线上元素为 1,其它元素为 0。秩初始化多用于 RNN 等需要保持恒等映射的场景,全连接层中较少使用。
正态分布初始化
权重参数按指定均值μ与标准差σ正态分布初始化。因为不能直接将权重初始化为相同的常数,所以需要对参数进行随机初始化。最常见的随机分布就是
正态分布(也叫 高斯分布),记作 X ~ N(μ, σ2)。
其概率密度函数为:

均匀分布初始化
权重参数在指定区间内均匀分布初始化。均匀分布一般记作 X ~ U(a,b)。
其概率密度函数为:

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

Xavier 初始化参数适用于 Sigmoid 和 Tanh 等激活函数,能有效缓解梯度消失或爆炸问题。其推导假设激活函数在 0 附近近似线性且对称,因此不适用于 ReLU(输出恒非负,破坏了对称性)。
He 初始化(Kaiming 初始化)
He初始化根据输入的神经元数量调整权重的初始范围。其方差为 Xavier 的 2 倍,以补偿 ReLU 将一半神经元置零导致的方差减半。

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

自定义模型
接口:继承 nn.Module,实现 __init__ 与 forward(input)。
功能:所有神经网络模块的基类;自定义模型需继承此类并实现前向传播。
参数:在 __init__ 中定义子模块与参数,在 forward 中接收输入并返回输出。
在神经网络框架中,由多个层组成的组件称之为 模块(Module)。在 PyTorch 中模型和各网络层都是 Module。在定义时需主要实现两个方法:
__init__:定义网络各层的结构,并初始化参数。forward:根据输入进行前向传播,并返回输出。计算其输出关于输入的梯度,可通过其反向传播函数进行访问(通常自动发生)。forward方法是每次调用的具体实现。
1 | class ModelDemo(nn.Module): |
模型训练
查看模型结构和参数数量
接口:torchsummary.summary(model, input_size, batch_size=None, device='cuda')
功能:打印模型结构和各层参数数量。
参数:
model:待查看的模型input_size:输入形状(如(3,)表示 3 个特征)batch_size:可选,批大小device:运行设备
1 | def train(): |
对于这里的summary输出值为:
1 | (type) Output Shape Param # |
其中:
Layer (type)– 层的类型和序号。模型有三个nn.Linear(全连接层),按前向传播顺序编号 Linear-1/2/3,对应代码里的linear1、linear2、output。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 | y1 = w11·x1 + w12·x2 + w13·x3 + b1 |
看那些 w,一共需要 2 × 3 = 6 个权重(每个输出 3 个,共 2 个输出)。排成矩阵就是 2 行 3 列,形状 (out, in) = (2, 3),所以权重数量 = out × in = in × out。
偏置:out 个
再看 b1、b2,每个输出配一个偏置,共 2 个 = out 个。偏置不乘输入,直接加上去,作用是让直线能上下平移(不固定过原点)。
加起来
in × out(权重)+ out(偏置)= 3 × 2 + 2 = 6 + 2 = 8 个参数,正好是表格里 Linear-2 那行的 Param #。

总结
5行3列,代表5个数据,每个数据有三个特征。此为输入层
隐藏层1接收上一层所有输出(第一个数据的三个特征),然后把这三个数据整合起来,整合的方法就是用本层的权重(反向传播可不断更新这个权重)和一个偏置值进行训练整合。单层可训练参数 = 神经元个数 × 上一层的输出数(=每个神经元的权重数) + 神经元个数(=每个神经元1个偏置)
样本1: [x1, x2, x3] ──┐ 样本2: [x1, x2, x3] ──┤ 样本3: [x1, x2, x3] ──┼──> 同一个神经元(3个权重+1偏置),每条各算一次 样本4: [x1, x2, x3] ──┤ 样本5: [x1, x2, x3] ──┘新 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
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)**。
## 分类任务
### 二分类任务损失函数

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

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

## 回归任务
### MAE
描述预测值与真实值之间差值绝对值的平均

### MSE

### Smooth L1
可以理解为平滑L1函数:

当误差较小时(预测值与真实值之差的绝对值小于1),使用L2 Loss,使得损失函数平滑可导。
当误差较大时时(预测值与真实值之差的绝对值大于1),用L1 Loss降低异常值的影响。
# 神经网络优化方法
## 梯度下降算法回顾
### 基本概念
梯度下降法(Gradient Descent)是一种用于最小化目标函数的迭代优化算法。核心是沿着目标函数(如损失函数)的负梯度方向逐步调整参数,从而逼近函数的最小值。梯度方向指示了函数增长最快的方向,因此负梯度方向是函数下降最快的方向。所以有:

其中,`η`是学习率,如果学习率太小,那么每次训练之后得到的效果都太小,增大训练的时间成本。如果,学习率太大,那就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化。
**举个具体例子:**
- 模型只有一个参数 `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>
更新新的值
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.0算w² = 1.0; - 再算
w²/2 = 0.5; - 最后这个
0.5就是 loss。
backward() 要做的事情正好反过来:梯度从 loss 往回流,所以图写成:
1 | loss ← w²/2 ← w |
箭头往左表示梯度传播的方向。它把每一步的导数相乘(链式法则),最后得到 loss 对 w 的梯度:
1 | loss 对 w²/2 的导数 = 1 |
Q:为什么不直接正向求导,还非要加一个反向传播?
A:正向模式求导的话,如果想得到 loss 对每个参数的导数,就得把整个计算图“带导数”完整走一遍,如果每个参数都走一遍,那么100 万参数 = 100 万遍,时间复杂度很高。反向的话,从 loss 出发往回走,每过一个节点就把梯度传给它下面的所有参数,走一遍就能同时拿到全部参数的梯度。成本大约是一遍前向 + 一遍反向,和参数个数基本无关。
所以 w = 1.0 时梯度就是 1.0,这个结果自动存进 w.grad。
w.grad中存的值会在w=w-学习率 * 梯度中拿出来。
更新完,格子里的 1.0 还在(step 不清空)。到下一轮。就要进行梯度清空:.zero_grad()。
梯度下降优化方法
- 碰到平缓区域,梯度值较小,参数优化变慢
- 碰到 “鞍点”,梯度为0,参数无法优化
- 碰到局部最小值,参数不是最优
对于这些问题,出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam等

优化前置方法:指数加权平均
我们最常见的算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的。指数加权平均指的是给每个数赋
予不同的权重求得平均数。移动平均数,指的是计算最近邻的N个数来获得平均数。
指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较
小),距离越近则对平均数的计算贡献就越大(权重越大)。

β越小,1-β越大,也就越参考本次的值(Yt)。所以浮动越大。
梯度下降优化方法—动量算法Momenttum
算法流程
(指数加权)梯度计算公式:st=β * st-1 + (1-β) * gt
参数更新公式:wt=wt-1 - n * st
- s
t是当前时刻指数加权平均梯度值。St-1是历史指数加权平均梯度值 - g
t是当前时刻的梯度值 - β是调节权重系数,通常取0.9或0.99
- n是学习率
- w
t是当前时刻模型权重参数
举个例子,假设:权重β为0.9
- 第一次梯度值:s
1=g1=w1 - 第二次梯度值:s
2= 0.9 * s1+ g2* 0.1 - 第三次梯度值:s
3= 0.9 * s2+ g3* 0.1 - 第四次梯度值:s
4=0.9 * s3+ g4* 0.1
- w表示初始梯度
- g表示当前轮数计算出的梯度值
- s表示历史梯度移动加权平均值
梯度下降公式中梯度的计算,就不再是当前时刻 t 的梯度值,而是历史梯度值的指数移动加权平均值。公式修改为
- W
t= Wt-1- η * St - W
t:当前时刻模型权重参数 - S
t:当前时刻指数加权平均梯度值 - η:学习率
作用
- 当处于鞍点位置时,由于当前的梯度为0,参数无法更新。但是Momentum动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
- 由于mini-batch普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。
代码
1 | def Momenttum(): |








/cover.jpg)