文章目录
- 神经网络深度学习笔记(上):框架总览、参数初始化与激活函数
- [1. 神经网络概述](#1. 神经网络概述)
-
- [1.1 什么是神经网络?](#1.1 什么是神经网络?)
- [1.2 为什么需要神经网络?(vs传统机器学习)](#1.2 为什么需要神经网络?(vs传统机器学习))
- [1.3 神经网络的结构骨架](#1.3 神经网络的结构骨架)
-
- [1.3.1 神经元:神经网络的最小计算单元](#1.3.1 神经元:神经网络的最小计算单元)
- [1.3.2 用神经元搭建基本网络框架:输入层、隐藏层、输出层](#1.3.2 用神经元搭建基本网络框架:输入层、隐藏层、输出层)
- [1.3.3 理解隐藏层的意义](#1.3.3 理解隐藏层的意义)
- [1.4 训练的两大阶段:前向传播与反向传播](#1.4 训练的两大阶段:前向传播与反向传播)
-
- [1.4.1 前向传播](#1.4.1 前向传播)
- [1.4.2 反向传播](#1.4.2 反向传播)
- [1.5 数据如何流动](#1.5 数据如何流动)
-
- [1.5.1 Batch、Epoch、Iteration](#1.5.1 Batch、Epoch、Iteration)
- [1.5.2 权重矩阵的构造逻辑](#1.5.2 权重矩阵的构造逻辑)
- [1.5.3 前向传播数据流动](#1.5.3 前向传播数据流动)
- [1.5.4 反向传播数据流动](#1.5.4 反向传播数据流动)
- [2. 参数初始化](#2. 参数初始化)
-
- [2.1 是什么与为什么](#2.1 是什么与为什么)
- [2.2 常见初始化方法](#2.2 常见初始化方法)
- [2.3 如何选择初始化方法](#2.3 如何选择初始化方法)
- [2.4 PyTorch API 实现](#2.4 PyTorch API 实现)
- [3. 激活函数](#3. 激活函数)
-
- [3.1 是什么与为什么](#3.1 是什么与为什么)
- [3.2 常见激活函数详解](#3.2 常见激活函数详解)
-
- [3.2.1 Sigmoid](#3.2.1 Sigmoid)
- [3.2.2 Tanh(双曲正切)](#3.2.2 Tanh(双曲正切))
- [3.2.3 ReLU(Rectified Linear Unit,修正线性单元)](#3.2.3 ReLU(Rectified Linear Unit,修正线性单元))
- [3.2.4 Softmax](#3.2.4 Softmax)
- [3.4 激活函数选择指南](#3.4 激活函数选择指南)
- [3.5 PyTorch API 实现](#3.5 PyTorch API 实现)
神经网络深度学习笔记(上):框架总览、参数初始化与激活函数
这篇笔记是在学习神经网络过程中的归纳梳理,目标是把散落的知识点串联成一张清晰的地图:先俯瞰全景框架,弄明白数据在神经网络里到底怎么流动、每一步的维度如何变化、各个组件(初始化、激活函数、损失函数、优化器、正则化)分别在什么环节起作用;然后逐一深入每个组件的细节~
1. 神经网络概述
1.1 什么是神经网络?
人工神经网络(Artificial Neural Network, ANN)是一种模仿生物神经系统的计算模型。
人脑中的生物神经网络由数百亿个神经元通过突触彼此连接而成。单个神经元的工作方式是:树突接收来自其他神经元的电信号,细胞体对这些输入信号进行累积,当膜电位超过一定阈值时,神经元被激活,通过轴突将信号传递给下游神经元。
人工神经网络沿用了这个结构隐喻。它的基本计算单元也叫"神经元",工作逻辑高度类似:
接收上一层所有神经元的输出 → 对输入加权求和 → 通过激活函数决定输出 → 传递给下一层
简言之,神经网络 = 多层神经元组成的有向计算图 ,而深度学习的"深",指的就是隐藏层的层数很多。构建神经网络,本质上就是在定义神经元之间的连接方式和每个连接的权重。 输入数据流经层层神经元的加工和传递,最终在输出层产生预测结果。

1.2 为什么需要神经网络?(vs传统机器学习)
回顾传统机器学习的工作流程:
数据读取 → 数据预处理 → 特征工程 → 模型训练 → 模型评估
在这个流程中,有一句广为流传的话:数据和特征决定了模型性能的上限,模型的选择和调参只是在逼近这个上限。 特征工程依赖领域专家手动设计:什么样的特征组合有效、什么样的变换能更好地表达数据,全靠人的经验和试错。这就带来了一个天然的瓶颈:人工设计特征不仅费时费力,而且很难穷举所有有效的特征表达。
神经网络的核心突破在于:把特征学习也交给模型。 它不需要人工设计特征,而是通过层层的线性变换和非线性激活,自动从原始数据中逐层抽象出越来越高级的特征:
原始数据 → [浅层:低级特征,如边缘、纹理] → [中层:中级特征,如形状、部件] → [深层:高级特征,如语义、类别]
网络的内部表示很难被人理解,所以神经网络常被称为"黑盒模型"。但换个角度看,这恰恰是它的优势,它学到的东西可能超出了人类的经验和直觉。

1.3 神经网络的结构骨架
任何全连接神经网络(也叫多层感知机,MLP)都由三种层组成,每层的职责和计算方式各不相同。
1.3.1 神经元:神经网络的最小计算单元
我们知道神经元的工作逻辑是:
text
接收上一层神经元的输出数值 → 加权求和 → 激活函数判断输出多少 → 传给下一层
所以对于一个神经元内部只做两件事:加权求和(线性变换)+ 激活函数(非线性变换)

示例理解 :
假设这个神经元接收 3 个输入值:
输入: x₁ = 2.0, x₂ = -1.0, x₃ = 0.5 权重: w₁ = 0.8, w₂ = -0.3, w₃ = 1.2 偏置: b = 0.1第一步:加权求和(线性变换)
z = w 1 x 1 + w 2 x 2 + w 3 x 3 + b z = w_1 x_1 + w_2 x_2 + w_3 x_3 + b z=w1x1+w2x2+w3x3+b z = 0.8 × 2.0 + ( − 0.3 ) × ( − 1.0 ) + 1.2 × 0.5 + 0.1 = 1.6 + 0.3 + 0.6 + 0.1 = 2.6 z = 0.8 \times 2.0 + (-0.3) \times (-1.0) + 1.2 \times 0.5 + 0.1 = 1.6 + 0.3 + 0.6 + 0.1 = 2.6 z=0.8×2.0+(−0.3)×(−1.0)+1.2×0.5+0.1=1.6+0.3+0.6+0.1=2.6
z = 2.6 是这个神经元对输入的综合打分。权重 w i w_i wi 代表了"我有多在意第 i i i 个输入", w 1 = 0.8 w_1=0.8 w1=0.8 说明这个神经元比较关注 x 1 x_1 x1; w 2 = − 0.3 w_2=-0.3 w2=−0.3 说明 x 2 x_2 x2 越大反而会让这个神经元的得分越低。偏置 b b b 是一个平移量,即使三个输入全是 0,这个神经元也有 0.1 的基础得分。
第二步:激活函数(非线性变换)
加权求和得到 z = 2.6 之后,激活函数决定"我要输出多少"。以 ReLU 为例,
f(z) = max(0, z):z 是正数就原样输出,z 是负数就输出 0:a = ReLU ( 2.6 ) = 2.6 a = \text{ReLU}(2.6) = 2.6 a=ReLU(2.6)=2.6 a a a 就是这个神经元的最终输出,会作为下一层所有神经元的输入之一。
所以,一个神经元的完整构成就是:
神经元 = 一组权重 w₁...wₙ + 一个偏置 b + 一个激活函数 f
输入 x₁...xₙ → [加权求和 z = Σwᵢxᵢ + b] → [激活 a = f(z)] → 输出 a
-
加权平均 :每个神经元有自己独立的权重和偏置。这意味着不同的神经元可以学习识别数据中不同的模式,有的神经元对特征 A 敏感,有的对特征 B 敏感,组合起来形成对数据丰富且多角度的表达。不同的神经元,权重不同,对同一个输入的反应也就不同,这是整个神经网络能工作的根基。
- 权重 w :可以把权重理解为神经元对每一项输入数据的"重视程度",某个输入的权重越大,它对这个神经元最终决策的影响就越大。
- 偏置 b :可以把偏置理解为给神经元一个"自带的基础激活门槛"。假设三个输入全是 0,如果没有偏置(即 (b=0)),加权求和的结果也是 0,经过 ReLU 后输出依然是 0,这个神经元就彻底沉默了。但有偏置的话,即使输入全为零,神经元仍然可以有一个非零的基础输出值。这赋予了网络更大的灵活性:输入为零时,神经元依然可以自主选择"激活"或"不激活",而不是被输入完全决定。
-
激活函数 :是对加权求和结果的"非线性筛选",它决定了这个神经元要不要把信号传递出去,以及以多强的强度传递。比如 ReLU 直接拦掉所有负信号(输出 0),只放行正信号(原样输出);Sigmoid 则把任意值压缩到 (0,1) 之间,输出一个"激活强度"。正是这种"筛选机制",让堆叠起来的神经元能够逐层抽取和组合特征,最终学会复杂的模式。
1.3.2 用神经元搭建基本网络框架:输入层、隐藏层、输出层

-
输入层 :将原始数据(图像的像素值、文本的词向量、表格数据的各列数值等)接入网络。严格来说,它不进行"加权求和+激活"的运算,仅负责将原始数据传递给下一层。但为了方便理解,我们通常称"输入有多少个特征,就有多少个输入神经元"。
输入层上的每个位置习惯上也叫"神经元",但它们不参与计算,没有权重、没有偏置、不经过激活函数。它们只是数据的"占位符",将数据原样传递给下一层。一个输入样本有几个特征,输入层就有几个位置。
-
隐藏层 :输入层和输出层之间的所有层统称隐藏层(Hidden Layer)。它们是神经网络计算能力的来源。网络的"深度"通常就是指隐藏层的数量。
一层隐藏层到底指什么?
一层隐藏层 = 这一层所有神经元(即全部神经元[加权求和] + [激活函数])一层隐藏层里的每个神经元,都接收上一层全部神经元的输出作为自己的输入,各自用自己的权重做加权求和,然后各自过激活函数,最后各自产出一个值。这一层所有神经元的输出拼在一起,就形成了这层对数据的"表示"。
同一层的所有神经元共享输入,但权重各不相同。正是因为权重不同,不同神经元才会对相同的输入数据产生不同的响应,各自捕捉数据中不同的侧面。
-
输出层:输出层是网络的最后一层,产生最终的预测结果。它的每个神经元同样做加权求和,然后用与任务匹配的激活函数产出最终预测:
任务 输出层激活函数 输出含义 二分类 Sigmoid 一个 0~1 的值,正类概率 多分类 Softmax C 个概率值,和为 1 回归 恒等映射(不加激活) 任意实数预测值
1.3.3 理解隐藏层的意义
每个神经元 = 一个特征检测器
一个神经元接收上一层的全部数据 ,用自己的一套权重做加权求和。这套权重决定了这个神经元 "对什么敏感",权重分布不同的神经元,会对输入数据的不同模式产生强烈响应。
同层的每个神经元被赋予不同的权重,于是这 5 个神经元就会分化出不同的"检测专长":有的对某些特征组合敏感,有的对另一些特征组合敏感。整层合在一起,就构成了一组多样化的特征探测器阵列。
逐层抽象:从低级模式到高级语义
单层神经元只能学到相对简单的模式。但把多层堆叠起来,就形成了一种层次化的特征抽象:
原始输入(3个特征)
↓
[隐藏层1] 每个神经元学习一种基础特征组合
例如:某个神经元对"年龄+身高"的组合敏感,
另一个对"城市+收入"的组合敏感...
→ 输出:5个"基础模式强度"
↓
[隐藏层2] 每个神经元接收的是"基础模式的组合"
检测更抽象的模式:
例如:将"年龄模式"和"收入模式"组合成"消费能力"
→ 输出:5个"抽象模式强度"
↓
[更深层] 将抽象模式组合成语义概念
→ 越来越接近最终决策所需的信息
↓
[输出层] 基于最高层语义特征做出判断
通俗理解 :浅层神经元学到的是"哪些原始特征的组合有意义",深层神经元学到的是"哪些组合的组合更有意义"。每一层都在前一层的基础上进行再组合、再抽象,逐步从原始数据中提炼出越来越高级的语义信息。
重要提醒:以上是为了帮助直观理解的比喻说法。实际上,神经网络内部的表征往往是高度分布式和纠缠的,一个语义概念可能分散在多个神经元的激活模式中,单个神经元很少能对应清晰的人类可理解的概念。这部分科学研究至今仍带有"黑盒"性质,我们看到的只是输入端和输出端,中间层究竟学到了什么,仍是一个活跃的研究课题。
1.4 训练的两大阶段:前向传播与反向传播
有了网络结构,怎么让它从数据中学会东西?答案是反复执行两个阶段:前向传播 算预测,反向传播 调参数。

1.4.1 前向传播
前向传播(Forward Propagation):数据按照网络结构一层一层向前流动,直到产出最终预测,这个过程不涉及任何参数更新。
预测值出来之后,用 损失函数(Loss Function) 衡量它和真实标签之间的差距。损失值是一个标量:数字越大说明预测越差,越小说明越准。损失函数是前向传播和反向传播的分界线,往前是正向计算,往后是反向求导。
1.4.2 反向传播
反向传播(Backward Propagation)要回答的问题是:损失对每个参数的"责任"有多大?参数应该往什么方向改、改多少,才能让损失变小?
核心机制是微积分中的链式求导法则 。损失 L L L 是一个超长的嵌套函数:
L = CrossEntropy(Softmax(W₃·ReLU(W₂·ReLU(W₁·x+b₁)+b₂)+b₃), y)
这个复合函数中,每一个参与前向计算的参数,都会在反向传播时被求偏导数,包括:
- (W_1, b_1)(隐藏层1的权重和偏置)
- (W_2, b_2)(隐藏层2的权重和偏置)
- (W_3, b_3)(输出层的权重和偏置)
每一组参数,都会得到一个和自己形状完全相同的梯度矩阵/向量:
| 参数 | 形状 | 梯度形状 |
|---|---|---|
| (W_1) | (3, 5) | W1.grad → (3, 5) |
| (b_1) | (5,) | b1.grad → (5,) |
| (W_2) | (5, 5) | W2.grad → (5, 5) |
| (b_2) | (5,) | b2.grad → (5,) |
| (W_3) | (5, 2) | W3.grad → (5, 2) |
| (b_3) | (2,) | b3.grad → (2,) |
也就是说,每一层的权重矩阵和偏置向量,反向传播后都会拥有自己专属的 .grad 属性 ,里面存的就是损失对该参数每个分量的偏导数。
梯度在计算图上的传播方向和前向传播正好相反,从末端的损失出发,一路反推到最前面的参数,所以叫"反向"传播。
比如要算 L L L 对 W 1 W_1 W1 的梯度,链式法则要求依次求:loss 对 Softmax 的导数 → Softmax 对 W 3 W_3 W3 的导数 → ... → ReLU 对 W 1 W_1 W1 的导数,共穿透 7 层运算,全部连乘起来。
假设隐藏层1的权重矩阵 (W_1) 形状是 (3, 2)(3个输入,2个神经元):
W₁ = [w₁₁ w₁₂] ← 神经元1的权重(第1列) [w₂₁ w₂₂] ← 神经元2的权重(第2列) [w₃₁ w₃₂]注意在 PyTorch 的
nn.Linear(3, 2)中,权重矩阵形状是 (2, 3) ,每一行是一个神经元的权重。这里按列写是为了和日常坐标习惯一致,不影响理解。损失 (L) 是一个标量,(W_1) 是一个矩阵。标量对矩阵求导,结果是一个和矩阵形状相同的梯度矩阵 :
∂ L ∂ W 1 = ∂ L ∂ w 11 ∂ L ∂ w 12 ∂ L ∂ w 21 ∂ L ∂ w 22 ∂ L ∂ w 31 ∂ L ∂ w 32 \frac{\partial L}{\partial W_1} = \begin{bmatrix} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} \\ \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} \\ \frac{\partial L}{\partial w_{31}} & \frac{\partial L}{\partial w_{32}} \end{bmatrix} ∂W1∂L= ∂w11∂L∂w21∂L∂w31∂L∂w12∂L∂w22∂L∂w32∂L
这里面的 6 个偏导数,每一个都是独立计算的 。以 (w_{11}) 为例,链式法则穿过 7 层运算:
∂ L ∂ w 11 = ∂ L ∂ Softmax ⋅ ∂ Softmax ∂ Z 3 ⋅ ∂ Z 3 ∂ A 2 ⋅ ∂ A 2 ∂ Z 2 ⋅ ∂ Z 2 ∂ A 1 ⋅ ∂ A 1 ∂ Z 1 ⋅ ∂ Z 1 ∂ w 11 \frac{\partial L}{\partial w_{11}} = \frac{\partial L}{\partial \text{Softmax}} \cdot \frac{\partial \text{Softmax}}{\partial Z_3} \cdot \frac{\partial Z_3}{\partial A_2} \cdot \frac{\partial A_2}{\partial Z_2} \cdot \frac{\partial Z_2}{\partial A_1} \cdot \frac{\partial A_1}{\partial Z_1} \cdot \frac{\partial Z_1}{\partial w_{11}} ∂w11∂L=∂Softmax∂L⋅∂Z3∂Softmax⋅∂A2∂Z3⋅∂Z2∂A2⋅∂A1∂Z2⋅∂Z1∂A1⋅∂w11∂Z1
所以对于6 个权重标量,每个都有一条专属的"最后一环"求导路径。
因为数学上,这 6 个偏导数可以打包 写成矩阵形式,用一次矩阵乘法同时算出来。PyTorch 的自动求导引擎会在底层为每个标量参数算出
.grad,然后组装成一个和原参数同形状的张量。
算出了每个参数的梯度 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L 之后,用梯度下降更新参数:
W n e w = W o l d − η ⋅ ∂ L ∂ W W_{new} = W_{old} - \eta \cdot \frac{\partial L}{\partial W} Wnew=Wold−η⋅∂W∂L
- 学习率 η \eta η 控制每一步的更新幅度。
- 梯度 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L 告诉"往哪个方向走损失降得最快"。
训练的本质就是:反复做前向传播(看预测准不准)和反向传播(往准的方向调参数),成千上万次迭代后,所有参数协同配合,最终找到一个能让预测值逼近真实值的参数组合。
1.5 数据如何流动
网络结构回顾:
- 输入层:3个特征
- 隐藏层1:5个神经元
- 隐藏层2:5个神经元
- 输出层:2个神经元

1.5.1 Batch、Epoch、Iteration
- epoch(轮次):把整个训练集完整过一遍,称为1个epoch。
- batch_size(批次大小):每次取多少个样本一起做一次前向+反向传播。
- iteration(迭代次数):完成一个epoch需要多少次batch。比如总样本1000,batch_size=100,则需要10个iteration跑完1个epoch。
1.5.2 权重矩阵的构造逻辑
设隐藏层1:接收 in_features=3 个输入,有 out_features=5 个神经元。
每个神经元要对3个输入做加权求和,所以每个神经元需要 3个w + 1个b 。5个神经元一共需要 3×5=15 个权重和 5 个偏置。
我们输入一个batch的数据 X,形状是 (batch_size, 3):
X = [样本1: x11 x12 x13] ← 第1个样本的3个特征
[样本2: x21 x22 x23] ← 第2个样本的3个特征
[样本3: x31 x32 x33] ← 第3个样本的3个特征
... (共m个样本)
如果W采用方式B的形状 (3, 5):
W1 (3×5)
┌─────────────────────┐
│ w₁₁ w₁₂ w₁₃ w₁₄ w₁₅ │ ← 第1个输入特征分别给5个神经元的权重
│ w₂₁ w₂₂ w₂₃ w₂₄ w₂₅ │ ← 第2个输入特征分别给5个神经元的权重
│ w₃₁ w₃₂ w₃₃ w₃₄ w₃₅ │ ← 第3个输入特征分别给5个神经元的权重
└─────────────────────┘
↑ ↑ ↑ ↑ ↑
神经元1 神经元2 神经元3 神经元4 神经元5
(每列就是一个神经元的全部权重)
然后做矩阵乘法 X @ W1:
Z1 = X @ W1
(m,3) @ (3,5) → (m,5)
结果的形状是 (m, 5)。其中:
- m 行:每一行对应一个样本经过隐藏层1后产出的5个数值
- 5 列 :每一列对应一个特定神经元对所有m个样本的输出值
换句话说,Z1[i][j]就是第i个样本在第j个隐藏层神经元上的加权求和结果。
权重矩阵维度的记忆规律: 对于任意一层,设它接收
in_features个输入,有out_features个神经元:
- W 形状:
(in_features, out_features),每一列就是一个神经元的所有权重。- b 形状:
(out_features,),每个元素就是一个神经元的偏置- 每个神经元拥有的参数量:
in_features个 w + 1 个 b
以隐藏层 1 为例:W₁ 有 3×5=15 个权重参数,b₁ 有 5 个偏置参数,总共 20 个可学习参数分布在这层的 5 个神经元上。
1.5.3 前向传播数据流动
假设一个batch有 m 个样本,它们同时、独立地流经网络:
| 步骤 | 矩阵运算 | 形状变化 | 含义解读 |
|---|---|---|---|
| ① 输入batch | X |
(m, 3) |
m个样本,每个3个特征 |
| ② 隐藏层1线性变换 | Z1 = X @ W1 + b1 |
(m,5) = (m,3) @ (3,5) + (5,) |
每行:该样本在5个神经元上的加权和 |
| ③ 隐藏层1激活 | A1 = ReLU(Z1) |
(m,5) |
负值归零,正值保留 |
| ④ 隐藏层2线性变换 | Z2 = A1 @ W2 + b2 |
(m,5) = (m,5) @ (5,5) + (5,) |
接收5维特征,输出5维新特征 |
| ⑤ 隐藏层2激活 | A2 = ReLU(Z2) |
(m,5) |
再次非线性筛选 |
| ⑥ 输出层线性变换 | Z3 = A2 @ W3 + b3 |
(m,2) = (m,5) @ (5,2) + (2,) |
每行:该样本的2个原始logits |
| ⑦ 输出层激活 | Y_pred = Softmax(Z3) |
(m,2) |
每行:2个类别的预测概率,和为1 |
| ⑧ 损失计算 | loss = CrossEntropy(Y_pred, Y_true) |
标量 | 一个数值,衡量整个batch的平均预测质量 |
三个关键理解:
- batch中的m个样本完全并行计算,矩阵乘法一次性处理全部。结果的每一行都是一个样本在当前层的"特征表示",列数就是这一层的神经元个数。batch内所有样本完全并行处理,网络参数被所有样本共享,每个样本都经过完全相同的网络。矩阵乘法 X @ W1 的本质,就是把m个样本各自做"加权求和"这件事,用一次矩阵运算并行完成。
- 激活函数逐元素操作,作用在Z矩阵的每个元素上,不会跨神经元混合。ReLU就像一个"闸门":正值直接放行,负值拦下归零。
- 输出层的logits (Z3)是一个
(m,2)的矩阵,每行两个数字代表该样本在两类上的"原始得分"。
1.5.4 反向传播数据流动
前向传播走了一条计算路径,反向传播就沿着完全相同的路径逆向走回去 ,用链式法则逐层计算损失对每个参数的梯度。
反向传播的执行流程:
loss.backward() 被调用
│
▼
① 计算 ∂L/∂Z₃ ← 损失对输出层logits的梯度(CrossEntropy+Softmax合起来求导)
│
▼
② 计算 ∂L/∂W₃, ∂L/∂b₃ ← 输出层参数的梯度
│
▼
③ 计算 ∂L/∂A₂ ← 梯度穿过输出层,传到隐藏层2的激活输出
│
▼
④ 计算 ∂L/∂Z₂ ← 梯度穿过ReLU(正值梯度×1,负值梯度×0)
│
▼
⑤ 计算 ∂L/∂W₂, ∂L/∂b₂ ← 隐藏层2参数的梯度
│
▼
⑥ 计算 ∂L/∂A₁ ← 梯度继续前传
│
▼
⑦ 计算 ∂L/∂Z₁ ← 再次穿过ReLU
│
▼
⑧ 计算 ∂L/∂W₁, ∂L/∂b₁ ← 隐藏层1参数的梯度
计算完成后,每个参数张量的 .grad 属性被填充:
| 参数 | 形状 | .grad 形状 |
含义 |
|---|---|---|---|
W1 |
(3, 5) |
(3, 5) |
损失对15个权重的偏导数 |
b1 |
(5,) |
(5,) |
损失对5个偏置的偏导数 |
W2 |
(5, 5) |
(5, 5) |
损失对25个权重的偏导数 |
b2 |
(5,) |
(5,) |
损失对5个偏置的偏导数 |
W3 |
(5, 2) |
(5, 2) |
损失对10个权重的偏导数 |
b3 |
(2,) |
(2,) |
损失对2个偏置的偏导数 |
每一个可学习参数,都拥有了一个专属于自己的梯度值,告诉它:往哪个方向改、改多少,能让损失变小。
参数更新发生在每个iteration,即每处理完一个batch,就立刻用该batch算出的梯度更新一次参数:
python
for X_batch, y_batch in dataloader: # 取一个batch
logits = model(X_batch) # ① 前向传播
loss = criterion(logits, y_batch) # ② 计算损失
optimizer.zero_grad() # ③ 清空上一batch遗留的梯度!
loss.backward() # ④ 反向传播,填充.grad
optimizer.step() # ⑤ 用.grad更新参数
关键细节:
-
optimizer.zero_grad():PyTorch默认累加 梯度(每次.backward()会把新梯度加到.grad上,而非覆盖)。这是为了支持梯度累积等高级用法。不清零的话,当前batch的梯度会混入之前所有batch的梯度,导致更新方向错误。 -
optimizer.step():优化器用每个参数的.grad和自身算法(如Adam的动量、自适应学习率)计算出实际更新量,然后修改参数值。 -
一个epoch = 遍历完整个训练集 = 多次参数更新 。比如1000个样本,batch_size=100,则1个epoch内参数更新10次。

┌─────────────────────────────────────────────────────────────────┐
│ 一次 Iteration(一个 batch)的完整数据流 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 输入 batch_x ──────────────────────────────────────┐ │
│ 形状: (batch_size, 3) │ │
│ ▼ │
│ ┌─ 前向传播 ─────────────────────────────────────────────┐ │
│ │ │ │
│ │ (bs,3)@(3,5)+(5,) → (bs,5) → ReLU → (bs,5) │ │
│ │ W₁,b₁ 第3章:激活函数 │ │
│ │ │ │
│ │ (bs,5)@(5,5)+(5,) → (bs,5) → ReLU → (bs,5) │ │
│ │ W₂,b₂ │ │
│ │ │ │
│ │ (bs,5)@(5,2)+(2,) → (bs,2) │ │
│ │ W₃,b₃ logits │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ 损失计算 ──────────────────────────────────────────────┐ │
│ │ L = CrossEntropyLoss(logits, batch_y) → 标量 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ 反向传播 ─────────────────────────────────────────────┐ │
│ │ loss.backward() │ │
│ │ ∂L/∂W₃, ∂L/∂b₃ → ∂L/∂W₂, ∂L/∂b₂ → ∂L/∂W₁, ∂L/∂b₁ │ │
│ │ 梯度存入: W₁.grad, b₁.grad, W₂.grad, b₂.grad, ... │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ 参数更新 ──────────────────────────────────────────────┐ │
│ │ optimizer.step() │ │
│ │ W_new = W_old - lr × W.grad │ │
│ │ 全部 W 和 b 更新完毕 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 重复 N 次 iteration = 1 个 epoch │
│ 重复 M 个 epoch = 训练完成 │
└─────────────────────────────────────────────────────────────────┘
现在对整个流程有了宏观理解后,我们逐一深入。
2. 参数初始化
2.1 是什么与为什么
神经网络训练的目标,是通过不断调整权重 W 和偏置 b,让损失函数降到最低。但在调整开始之前,需要给这些参数一个初始值,这就是参数初始化。
参数初始化的三大核心作用:
- 打破对称性
想象隐藏层有 5 个神经元,如果它们的初始权重完全相同,那么在前向传播时,对于同一个输入,5 个神经元会产生完全相同的输出;在反向传播时,5 个神经元会收到完全相同的梯度,做出完全相同的更新。结果就是:无论这层有多少个神经元,实际效果都等价于只有一个神经元。 多神经元的表达能力完全被浪费了。
所以权重必须用不同的值来初始化,让每个神经元从一开始就关注不同的数据特征,各自学习不同的模式。这发生在前向传播中,属于特征提取层面的需求。 - 防止梯度消失与梯度爆炸
这个作用和反向传播直接相关。回顾链式求导:梯度从输出层一路反传,每经过一层就要乘以该层激活函数的导数和权重矩阵。如果权重初始值太大,经过多层连乘后梯度会指数级膨胀(梯度爆炸);如果权重初始值太小,经过多层连乘后梯度会指数级衰减到接近 0(梯度消失)。 - 加速收敛
如果把参数初始化在损失函数曲面上一个"好的起点"附近,梯度下降找到最优解需要的步数就少。相反,如果初始位置很差(比如位于一个极其平坦的区域),梯度很小,优化就会非常缓慢。合理的初始化本质上是在给优化器一个更好的起跑位置。
2.2 常见初始化方法
下面是几种主流初始化策略的全面对比:
| 初始化方法 | 核心公式/分布 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全0/全1 /固定值 | W=0 或 W=1 或 W=val | 实现简单 | 无法打破对称性;全1还容易导致梯度爆炸 | 仅偏置可用0,权重绝不建议 |
| 随机初始化 | 正态分布 N(0,1) 或均匀分布 U(-a,a) | 简单,可打破对称性 | 没有考虑层间方差传递,深层时易出现梯度问题 | 浅层网络(≤5层)中可临时使用 |
| Xavier | 正态:std = sqrt(2/(fan_in+fan_out)) |
维持Sigmoid/Tanh的输入输出方差一致,稳定梯度 | 对ReLU等非对称激活函数效果差 | 深层网络 + Sigmoid/Tanh激活 |
| Kaiming | 正态:std = sqrt(2/fan_in) |
专门为ReLU设计,考虑其负半轴置0导致方差减半的特性 | 对Sigmoid/Tanh不如Xavier理想 | 深层网络(≥10层) + ReLU/LeakyReLU |
1.Kaiming 初始化(He 初始化)
专为 ReLU 及其变体(Leaky ReLU、PReLU 等)设计的初始化方法。核心思想是:ReLU 会把一半的激活值置为 0(负值区域),导致输出的方差减半。为了让方差在经过 ReLU 后保持稳定,Kaiming 初始化把权重的方差放大了 2 倍。
- 正态分布版本 :
W ~ N(0, std),其中std = √(2 / fan_in) - 均匀分布版本 :
W ~ U(-limit, limit),其中limit = √(6 / fan_in)
这里的 fan_in(输入扇入)指的是当前层接收的来自上一层的神经元数量 。例如,隐藏层 1 接收 3 个输入特征,那它的 fan_in = 3;隐藏层 2 接收上一层 5 个神经元的输出,fan_in = 5。
std = √(2 / fan_in) 这个公式的含义是:输入维度越大,单个权重对输出的影响需要被适当缩小,从而让方差保持在一个合理的范围。
| 维度 | 说明 |
|---|---|
| 优点 | 适合 ReLU 类激活函数,能有效保持梯度稳定,缓解梯度消失 |
| 缺点 | 对 Sigmoid、Tanh 等非 ReLU 类激活函数效果一般 |
| 适用场景 | 深层网络(10 层及以上),配合 ReLU 或 Leaky ReLU 使用 |
2.Xavier 初始化(Glorot 初始化)
适用于 Sigmoid、Tanh 等 S 型激活函数。与 Kaiming 不同的是,Xavier 同时考虑了输入维度 和输出维度,目标是让前向传播时每一层的输出方差和反向传播时每一层的梯度方差都保持一致。
-
正态分布版本 :
W ~ N(0, std),其中std = √(2 / (fan_in + fan_out)) -
均匀分布版本 :
W ~ U(-limit, limit),其中limit = √(6 / (fan_in + fan_out)) -
fan_in:输入神经元个数,即当前层接收的上一层输出数量 -
fan_out:输出神经元个数,即当前层产生的输出数量(当前层神经元数)
以隐藏层 1(3→5)为例:fan_in = 3,fan_out = 5。
同时考虑 fan_in 和 fan_out 的原因:前向传播中方差缩放由 fan_in 决定,反向传播中方差缩放由 fan_out 决定。取两者的调和平均 (fan_in + fan_out) / 2,是前向和反向的折中,使信息在两个方向上都能平稳流动。
| 维度 | 说明 |
|---|---|
| 优点 | 适合 Sigmoid、Tanh 激活函数,保持前向和反向方差一致 |
| 缺点 | 对 ReLU 类激活函数表现欠佳 |
| 适用场景 | 深层网络(10 层及以上),配合 Sigmoid 或 Tanh 使用 |
2.3 如何选择初始化方法
| 激活函数 | 网络深度 | 推荐初始化 |
|---|---|---|
| ReLU / Leaky ReLU | 浅层(≤5层) | 随机初始化或 Kaiming |
| ReLU / Leaky ReLU | 深层(>5层) | Kaiming 初始化 |
| Sigmoid / Tanh | 浅层(≤5层) | 随机初始化或 Xavier |
| Sigmoid / Tanh | 深层(>5层) | Xavier 初始化 |
简单记忆:ReLU 系列找 Kaiming,Sigmoid/Tanh 系列找 Xavier。
2.4 PyTorch API 实现
所属模块:
torch.nn.init,助记:init = 初始化(initialization)。专门为张量参数提供初始化策略。
核心 API:
| 函数 | 分布 | 适用场景 |
|---|---|---|
kaiming_uniform_ |
均匀 | ReLU / Leaky ReLU |
kaiming_normal_ |
正态 | ReLU / Leaky ReLU |
xavier_uniform_ |
均匀 | Sigmoid / Tanh |
xavier_normal_ |
正态 | Sigmoid / Tanh |
uniform_ |
均匀 | 通用(手动指定范围) |
normal_ |
正态 | 通用(手动指定均值/标准差) |
zeros_ |
全 0 | 偏置初始化 |
constant_ |
固定值 | 调试或特殊场景 |
函数名以下划线 _ 结尾 = 原地操作(in-place),直接修改张量。
在示例网络中的代码
python
import torch
import torch.nn as nn
import torch.nn.init as init # init = 初始化
# --------- 定义一个线性层 ---------
linear = nn.Linear(in_features=3, out_features=5) # 隐藏层1: 3输入→5神经元
# --------- 方式1: 对已有层手动应用初始化 ---------
init.kaiming_normal_(linear.weight, mode='fan_in', nonlinearity='relu') # Kaiming正态
init.kaiming_uniform_(linear.weight, mode='fan_in', nonlinearity='relu') # Kaiming均匀
init.xavier_normal_(linear.weight) # Xavier正态
init.xavier_uniform_(linear.weight) # Xavier均匀
init.normal_(linear.weight, mean=0, std=0.01) # 普通正态
init.uniform_(linear.weight, a=-0.1, b=0.1) # 普通均匀
init.zeros_(linear.bias) # 偏置初始化为0(常用做法)
# --------- 方式2: 创建层时通过参数指定 ---------
# 某些层支持直接传入初始化函数,但更通用的做法是方式1
linear = nn.Linear(3, 5)
nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')
nn.init.zeros_(linear.bias)
参数说明:
| 函数 | 关键参数 | 含义 |
|---|---|---|
kaiming_normal_ |
mode='fan_in' |
使用 fan_in 计算 std(√(2/fan_in)) |
kaiming_normal_ |
nonlinearity='relu' |
指定配套的激活函数类型,影响方差补偿系数 |
xavier_normal_ |
gain=1.0 |
可选的增益系数,Sigmoid/Tanh 一般用默认 1.0 |
在实际项目中,PyTorch 的 nn.Linear 默认使用 Kaiming 均匀初始化,因为现在绝大多数网络都使用 ReLU 类激活函数。
3. 激活函数
3.1 是什么与为什么
先看一个极端情况:如果去掉所有激活函数(或者说激活函数就是 f(x)=x,即恒等映射),那么神经网络会发生什么?
z₁ = x·W₁ + b₁
z₂ = z₁·W₂ + b₂ = (x·W₁ + b₁)·W₂ + b₂ = x·(W₁·W₂) + (b₁·W₂ + b₂)
多层的线性变换复合后,仍然等价于一个单层线性变换。无论堆多少层隐藏层,整个网络都只能表达线性函数,而现实中绝大多数问题的决策边界都是非线性的。
激活函数的作用就是引入非线性 。在每一层的加权求和之后,激活函数对结果做一次非线性变换,通过引入非线性,使网络理论上可以逼近任意复杂函数。
z = Wx + b ← 线性变换
a = f(z) ← 非线性激活
激活函数的两个硬性要求:
理解前面 第一章 的前向传播和反向传播原理介绍,这就对激活函数提出了两个硬性条件:
- 非线性 :否则前向传播时多层等价于单层,失去深度学习的意义
- 可微(至少几乎处处可微) :反向传播 需要对激活函数求导,因此激活函数必须几乎处处可微。
3.2 常见激活函数详解
3.2.1 Sigmoid
公式:
σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+e−x1
导数:
σ ′ ( x ) = σ ( x ) ( 1 − σ ( x ) ) \sigma'(x) = \sigma(x)(1 - \sigma(x)) σ′(x)=σ(x)(1−σ(x))

输出范围和形状:
- 将任意实数输入映射到 (0, 1) 区间,呈 S 形曲线
- 输入在 -6, 6 之间时输出有明显差异,输入在 -3, 3 之间时变化最为显著
- 输入超过 ±6 时输出几乎触底(≈0)或封顶(≈1),信息差异几乎被抹平
导数特性:
导数范围是 (0, 0.25]。最大值 0.25 出现在 x=0 处。当输入在 ±6 以外时,导数趋近于 0,这意味着反向传播时梯度几乎消失,对应层的参数几乎不更新。
最佳位置 :二分类输出层。在隐藏层中几乎已被弃用。
为什么 Sigmoid 不适合隐藏层:
两个原因。第一,梯度最大只有 0.25,经过多层连乘后梯度会指数衰减到接近 0(梯度消失),深层网络几乎训不动。第二,Sigmoid 的输出始终为正(0~1),不以 0 为中心,这会导致下一层所有神经元的梯度始终同号(全正或全负),参数更新的方向受到限制,出现"之字形"优化路径,收敛变慢。
为什么 Sigmoid 适合二分类输出层:对于二分类问题,Sigmoid 输出一个 (0, 1) 之间的值,可以直接解释为样本属于正类的概率。比如输出 0.8,可以理解为"模型认为该样本有 80% 的概率属于正类"。在逻辑上还有一个优雅的性质:Sigmoid 实际上是 Softmax 在类别数为 2 时的特例,如果两个类别的 logits 分别是 z₁ 和 z₂,Softmax 出的第一个概率恰好等于 Sigmoid(z₁-z₂)。
3.2.2 Tanh(双曲正切)
公式:
tanh ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+e−xex−e−x
导数:
tanh ′ ( x ) = 1 − tanh 2 ( x ) \tanh'(x) = 1 - \tanh^2(x) tanh′(x)=1−tanh2(x)

输出范围和形状:
- 将任意实数输入映射到 (-1, 1) 区间,同样呈 S 形
- 输出以 0 为中心对称(这是相对于 Sigmoid 的最大改进)
- 输入在 ±3 以外时输出趋近于 -1 或 1,导数接近 0
导数特性:
导数范围是 (0, 1],比 Sigmoid 的 (0, 0.25] 宽裕不少,梯度衰减相对缓解但仍存在。当 x < -3 或 x > 3 时导数同样接近 0,深层网络仍然面临梯度消失问题。
与 Sigmoid 的关系:
Tanh 本质上是 Sigmoid 的缩放和平移:tanh(x) = 2σ(2x) - 1。由于输出零中心化,实际训练中 Tanh 通常比 Sigmoid 收敛更快,但计算成本略高。
最佳位置:某些需要零中心输出的隐藏层或RNN中,但大多数情况下已被ReLU取代。
3.2.3 ReLU(Rectified Linear Unit,修正线性单元)
公式:
ReLU ( x ) = max ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x)
导数:
ReLU ′ ( x ) = { 1 , x > 0 0 , x ≤ 0 \text{ReLU}'(x) = \begin{cases} 1, & x > 0 \\ 0, & x \leq 0 \end{cases} ReLU′(x)={1,0,x>0x≤0
行为特征:
- 输入大于 0:原样输出,导数为 1
- 输入小于等于 0:输出为 0,导数为 0

为什么 ReLU 是隐藏层的默认首选:
- 缓解梯度消失:正数区域的导数恒为 1,梯度在反向传播时不会衰减,这是对 Sigmoid/Tanh 最大的改进。深层网络终于能有效训练了。
- 计算极其简单:不需要指数运算,只是取 max(0, x),前向和反向都非常快。
- 稀疏激活:负数区域输出为 0,意味着网络中大约一半的神经元不激活。这种稀疏性类似于人脑中同一时刻只有部分神经元活跃的机制,也带来了一定的正则化效果(减少神经元之间的相互依赖)。
- Dead ReLU问题:若某神经元一直输出负值,梯度恒为0,权重永不更新,该神经元"死亡"。通过合理的初始化和较小学习率可缓解。
最佳位置:所有隐藏层的首选激活函数。
3.2.4 Softmax
公式:
Softmax ( z i ) = e z i ∑ j = 1 C e z j \text{Softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{C} e^{z_j}} Softmax(zi)=∑j=1Cezjezi
其中 C 是类别总数。分母是所有类别的指数和,确保输出概率和为 1。
将任意实向量映射为概率分布(每个分量在0~1之间,且和为1)。

计算过程分两步理解:
e^(z_i):把 logits 通过指数函数映射到 (0, +∞),指数保证了单调性,logits 越大,映射后的值越大,且始终为正- 除以总和:在所有类别之间做归一化,使输出成为一个概率分布(所有值之和为 1)
dim 参数的含义:
Softmax 需要指定沿哪个维度做归一化。dim 参数告诉函数"在哪个维度上求和为 1"。对于分类任务:
python
# logits 形状: (batch_size, num_classes)
# 我们希望每个样本的各类别概率和为1 → 沿类别维度(第1维)做Softmax
softmax = nn.Softmax(dim=1) # dim=1 或 dim=-1,沿类别维度归一化
output = softmax(logits) # (batch_size, num_classes),每行和为1
其他激活函数(Sigmoid、Tanh、ReLU)都是逐元素操作,不涉及跨元素归一化,所以不需要 dim 参数。
为什么 Softmax 适合多分类输出层:
Softmax 将原始的 logits 转化为可以直接解释的概率分布,"模型认为样本属于类别 A 的概率是 70%,属于类别 B 的概率是 20%,等等"。配合交叉熵损失,两者在数学上形成了一个优雅的梯度表达式(见下篇第 4 章)。
与 Sigmoid 的关系: 当类别数 C=2 时,设两个类别的 logits 为 z₁ 和 z₂:
Softmax ( z 1 ) = e z 1 e z 1 + e z 2 = 1 1 + e − ( z 1 − z 2 ) = σ ( z 1 − z 2 ) \text{Softmax}(z_1) = \frac{e^{z_1}}{e^{z_1} + e^{z_2}} = \frac{1}{1 + e^{-(z_1 - z_2)}} = \sigma(z_1 - z_2) Softmax(z1)=ez1+ez2ez1=1+e−(z1−z2)1=σ(z1−z2)
也就是说,二分类的 Softmax 等价于对 logits 差值做 Sigmoid。在实际工程中,二分类用 Sigmoid(输出 1 个值),多分类用 Softmax(输出 C 个值)。
最佳位置:多分类输出层。
3.4 激活函数选择指南
隐藏层
| 优先级 | 选择 | 原因 |
|---|---|---|
| 首选 | ReLU | 计算快、缓解梯度消失、实际工程中效果好 |
| 备选 | Leaky ReLU / PReLU | 当 ReLU 出现大量 Dead 神经元时替换 |
| 谨慎使用 | Tanh | 比 Sigmoid 好(零中心化),但仍可能梯度消失 |
| 避免 | Sigmoid | 梯度消失严重,现代深度网络中几乎不用于隐藏层 |
输出层
| 任务类型 | 激活函数 | 原因 |
|---|---|---|
| 二分类 | Sigmoid | 输出 0~1 概率值,正类概率 = 输出值 |
| 多分类 | Softmax | 输出 C 个概率值,和为 1,取最大概率的类别 |
| 回归 | 恒等映射(不加激活) | 输出可以是任意实数值 |

3.5 PyTorch API 实现
所属模块
激活函数位于 torch.nn 模块下,作为单独的层(Layer)来使用,继承自 nn.Module,可以在 forward 中像普通层一样调用。
核心 API
| 激活函数 | 层式 API | 参数 |
|---|---|---|
| ReLU | nn.ReLU(inplace=False) |
inplace:是否原地操作,默认 False |
| Leaky ReLU | nn.LeakyReLU(negative_slope=0.01) |
negative_slope:负区斜率 α,默认 0.01 |
| Sigmoid | nn.Sigmoid() |
无参数 |
| Tanh | nn.Tanh() |
无参数 |
| Softmax | nn.Softmax(dim=None) |
dim:归一化维度,分类任务 =1 或 -1 |
对应示例网络的代码框架(逐步搭建):
python
import torch
import torch.nn as nn
import torch.nn.init as init
class MyNet(nn.Module):
"""示例网络:3输入 → 5隐藏 → 5隐藏 → 2输出(二分类)"""
def __init__(self):
super().__init__()
# ------ 第2章:定义各层的线性变换 ------
self.fc1 = nn.Linear(3, 5) # 隐藏层1:3输入→5神经元
self.fc2 = nn.Linear(5, 5) # 隐藏层2:5→5
self.fc3 = nn.Linear(5, 2) # 输出层:5→2
# ------ 第2章:参数初始化 ------
nn.init.kaiming_uniform_(self.fc1.weight, nonlinearity='relu')
nn.init.kaiming_uniform_(self.fc2.weight, nonlinearity='relu')
nn.init.xavier_uniform_(self.fc3.weight) # 输出层不接ReLU,用Xavier
nn.init.zeros_(self.fc1.bias)
nn.init.zeros_(self.fc2.bias)
nn.init.zeros_(self.fc3.bias)
# ------ 第3章:激活函数(作为层定义) ------
self.relu = nn.ReLU() # 隐藏层统一用ReLU
self.softmax = nn.Softmax(dim=1) # 输出层用Softmax,沿类别维度归一化
def forward(self, x):
# x 形状: (batch_size, 3)
x = self.relu(self.fc1(x)) # → (batch_size, 5)
x = self.relu(self.fc2(x)) # → (batch_size, 5)
x = self.softmax(self.fc3(x)) # → (batch_size, 2)
return x
python
def forward(self, x):
x = self.relu(self.fc1(x)) # 加权求和 → 激活
x = self.relu(self.fc2(x))
x = self.fc3(x) # 只做加权求和,返回logits
return x
在下篇中,我们会继续讲解损失函数、梯度下降的各种优化算法(Momentum、AdaGrad、RMSProp、Adam)、学习率衰减策略,以及 Dropout 和 Batch Normalization 两种正则化技术,最终拼出完整的训练代码~
以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。如果文章对你有帮助,别忘了点个赞、留个言,让更多的小伙伴看到~ 我们下篇再见!