1 神经网络
1.1 人工神经网络
生物里,人脑是复杂神经网络,靠神经元传递电信号:树突接收信号→细胞体处理→轴突输出;
工作原理:当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号;
人工神经网络(Artificial Neural Network, 简写为ANN,也简称为神经网络,即NN)是一种模仿生物神经网络结构和功能的计算模型,设计“人工神经元”,把生物信号传递抽象成数学计算;
单个人工神经元的计算逻辑:
输入:多个输入信号(像 $ x_1、x_2 $),类比生物树突接收的信号;
加权求和:给每个输入分配权重($ w_1、w_2 $),再加上偏置 $ b $,计算 $ b + \sum_{i = 1}^{n} x_iw_i $ ,类比生物神经元“收集、整合信号”;
激活输出:用激活函数 $ f $ 处理加权和,得到输出 $ f(x) $ ,类比生物神经元“达到电位阈值后输出信号”;
简单说,就是输入加权求和→激活函数处理→输出结果 ,模拟生物神经元“接收 - 处理 - 输出”的过程;
把大量人工神经元分层连接,就构成神经网络:
输入层:接收原始数据(如 $ x_1、x_2 $),类比生物神经元的“树突接收端”;
隐藏层:输入层和输出层之间的中间层,负责逐步处理信号,层数、神经元数量按需设计;
输出层:输出最终结果(如 $ y_1、y_2 $),类比生物神经元的“轴突输出端”;
神经网络的连接特点(以全连接网络为例)
同一层神经元互不连接 ,专注“层内独立处理 + 层间传递信号”;
相邻层(如第 $ N $ 层和 $ N - 1 $ 层)全连接 :第 $ N $ 层每个神经元,都会接收第 $ N - 1 $ 层所有神经元的输出(带着权重 $ w $),充分传递、整合信息;
连接有权重($ w )和偏置( )和偏置( )和偏置( b $):调节信号传递的“强度”和“基准”,训练时会不断优化这些参数,让网络学会“正确处理、输出信息”;
神经网络演示:A Neural Network Playground。
1.2 激活函数
1.2.1 概述
激活函数用于对每层的输出数据进行变换,给神经网络注入非线性因素 ,让网络从“只能拟合简单线性关系”,升级为“可以拟合复杂曲线、逼近任意函数”,解决复杂问题(比如图像识别里的复杂特征、语言翻译里的语义关联);
如果没有激活函数
- 如果不用激活函数,哪怕网络层数多、结构复杂,本质还是线性模型;
- 通过给输出增加激活函数,实现引入非线性因素,使得网络模型可以逼近任意函数,提升网络对复杂问题的拟合能力;
像下图中的多层网络,层层计算后,最终输出能化简成 $ f_{\text{out}} = k_0x_0 + k_1x_1 + c $ (线性表达式),只能处理简单线性关系,无法应对复杂、非线性的真实场景(比如图像里的边缘、语义里的多义性);
1.2.2 sigmoid 激活函数
激活函数公式:
f ( x ) = 1 1 + e − x f(x) = \frac{1}{1 + e^{-x}} f(x)=1+e−x1- 作用:把任意实数输入(比如神经网络里的加权和结果),压缩映射到 (0, 1) 区间,输出可理解为“概率”或“归一化后的信号强度”;
激活函数求导公式:
f ′ ( x ) = ( 1 1 + e − x ) ′ = 1 1 + e − x ( 1 − 1 1 + e − x ) = f ( x ) ⋅ ( 1 − f ( x ) ) f'(x) = \left( \frac{1}{1 + e^{-x}} \right)' = \frac{1}{1 + e^{-x}} \left( 1 - \frac{1}{1 + e^{-x}} \right) = f(x) \cdot (1 - f(x)) f′(x)=(1+e−x1)′=1+e−x1(1−1+e−x1)=f(x)⋅(1−f(x))推导逻辑:对 $ f(x) $ 用复合函数求导法则(外层是 $ \frac{1}{u} $ ,内层 $ u = 1 + e^{-x} $);
意义:在神经网络训练中,导数用于反向传播更新参数(比如权重 $ w $ 、偏置 $ b $),决定参数调整的“快慢”和“方向”;
函数图像与特性:
函数图像(左图):横轴是输入 $ x $ ,纵轴是输出 $ f(x) $。特点:
当 $ x \to +\infty (输入极大), (输入极大), (输入极大), e^{-x} \to 0 $ ,$ f(x) \to \frac{1}{1 + 0} = 1 $ ;
当 $ x \to -\infty (输入极小), (输入极小), (输入极小), e^{-x} \to +\infty $ ,$ f(x) \to \frac{1}{1 + \infty} = 0 $ ;
中间($ x \approx -3 $ 到 $ 3 $ )曲线“陡峭变化”,输入小范围变动,输出会明显改变;超出这个区间(比如 $ x > 6 $ 或 $ x < -6 $ ),输出趋近 1 或 0 ,几乎不再变化;
导数图像(右图):横轴是输入 $ x $ ,纵轴是导数 $ f’(x) $。特点:
- 导数最大值约 0.25(当 $ f(x) = 0.5 $ 时,$ f’(x) = 0.5 \times 0.5 = 0.25 $ );
- 当 $ x \to \pm\infty $(输入极大/极小),导数趋近 0 ;
- 中间区间($ x \approx -3 $ 到 $ 3 $ )导数较大,参数更新“有力”;超出区间,导数接近 0 ,参数更新“停滞”;
sigmoid 函数的特性,决定了它在神经网络里的适用场景和局限性:
输出“概率化”:因为输出在 (0, 1) ,适合二分类任务的输出层(比如“是/否”“垃圾邮件/正常邮件”),直接用输出表示“属于某一类的概率”;
信息丢失问题:当输入 $ |x| > 6 $ 时,输出趋近 1 或 0 。比如输入 $ x = 100 $ 和 $ x = 10000 $ ,输出几乎都是 1 ,但“输入相差 100 倍”的信息,会被 sigmoid “压缩”成“无差异的 1” ,丢失细节;
梯度消失问题:当输入 $ |x| > 6 $ 时,导数 $ f’(x) \to 0 $ 。神经网络训练靠反向传播梯度更新参数,如果梯度接近 0 ,参数就“很难更新”(比如多层网络里,前面层的梯度经过 sigmoid 后几乎消失,网络学不到深层特征)。通常,sigmoid 网络超过 5 层,就容易出现“梯度消失”,无法训练深层模型;
非零中心输出:sigmoid 输出始终为正(因为映射到 (0, 1)),会导致神经网络中梯度方向单一(比如所有层的梯度都是正的,参数更新容易“绕弯路”),训练效率变低;
正因这些缺点,sigmoid 函数现在用得很少,仅在两种场景偶尔出现:
二分类任务的输出层(利用“概率化输出”直接判断类别);
早期简单的浅层神经网络(比如几十年前的小模型);
代码:
import torch import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 # 创建一个 1 行 2 列的子图画布,返回的 axes 是子图列表 _, axes = plt.subplots(1, 2) x = torch.linspace(-20, 20, 1000) # 在区间 [-20, 20] 内生成 1000 个等间距的点(用于横坐标) y = torch.sigmoid(x) # 对每个 x 值应用 sigmoid 函数,得到对应的 y 值 axes[0].plot(x, y) # 在第一个子图上绘制 sigmoid 曲线 axes[0].grid() axes[0].set_title('Sigmoid 函数图像') x = torch.linspace(-20, 20, 1000, requires_grad=True) # 重新定义 x,并开启梯度追踪以便求导 torch.sigmoid(x).sum().backward() # 对 sigmoid(x) 的总和求导,结果保存在 x.grad 中 axes[1].plot(x.detach(), x.grad) # 在第二个子图上绘制 sigmoid 函数的导数图像 axes[1].grid() axes[1].set_title('Sigmoid 导数图像') plt.show()
1.2.3 tanh 激活函数
激活函数公式:
f ( x ) = 1 − e − 2 x 1 + e − 2 x f(x) = \frac{1 - e^{-2x}}{1 + e^{-2x}} f(x)=1+e−2x1−e−2x- 作用:把任意实数输入(比如神经网络的加权和结果),压缩映射到 (-1, 1) 区间,输出可理解为 “带符号的归一化信号”(正负表示方向,大小表示强度);
激活函数求导公式:
f ′ ( x ) = ( 1 − e − 2 x 1 + e − 2 x ) ′ = 1 − f ( x ) 2 = 1 − tanh 2 ( x ) f'(x) = \left( \frac{1 - e^{-2x}}{1 + e^{-2x}} \right)' = 1 - f(x)^2= 1 - \tanh^2(x) f′(x)=(1+e−2x1−e−2x)′=1−f(x)2=1−tanh2(x)- 推导逻辑:对 $ f(x) $ 用复合函数求导(外层是分式,内层是 $ e^{-2x} $ ),化简后发现导数可直接用 $ f(x) $ 表示,计算方便;
- 意义:反向传播时,导数决定参数(如权重 $ w $、偏置 $ b $)更新的“快慢”和“方向”,是神经网络训练的关键;
函数图像与特性:
- 函数图像(左图):横轴是输入 $ x $ ,纵轴是输出 $ f(x) $。特点:
- 图像以 0 为中心对称(输入 $ x $ 和 $ -x $ ,输出 $ f(x) $ 和 $ f(-x) $ 互为相反数 ),解决了 sigmoid “输出全为正”的问题;
- 当 $ x \to +\infty (输入极大), (输入极大), (输入极大), e^{-2x} \to 0 $ ,$ f(x) \to \frac{1 - 0}{1 + 0} = 1 $ ;
- 当 $ x \to -\infty (输入极小), (输入极小), (输入极小), e^{-2x} \to +\infty $ ,$ f(x) \to \frac{1 - \infty}{1 + \infty} \approx -1 $ ;
- 中间区间($ x \approx -3 $ 到 $ 3 $)曲线“陡峭变化”,输入小范围变动,输出会明显改变;超出这个区间(比如 $ |x| > 3 $),输出趋近 1 或 -1 ,几乎不再变化;
- 导数图像(右图):横轴是输入 $ x $ ,纵轴是导数 $ f’(x) $。特点:
- 导数最大值为 1(当 $ f(x) = 0 $ 时,$ f’(x) = 1 - 0 = 1 $ );
- 当 $ |x| > 3 $ 时,导数 $ f’(x) \to 0 $ ;
- 中间区间($ |x| < 3 $ )导数较大,参数更新“有力”;超出区间,导数接近 0 ,参数更新“停滞”;
- 函数图像(左图):横轴是输入 $ x $ ,纵轴是输出 $ f(x) $。特点:
tanh vs sigmoid:
- 优点(对比 sigmoid 的改进)
- 零中心输出:tanh 输出在 (-1, 1) ,以 0 为中心对称。这会让神经网络中梯度方向更丰富(有正有负),参数更新更“高效”,训练时收敛速度比 sigmoid 更快(少走弯路);
- 梯度更大:tanh 导数范围是 (0, 1) ,而 sigmoid 导数范围是 (0, 0.25) 。在中间区间($ |x| < 3 $ ),tanh 梯度比 sigmoid 大,参数更新更“有力”,训练效率更高;
- 缺点(和 sigmoid 一样的痛)
- 信息丢失问题:当 $ |x| > 3 $ 时,tanh 输出趋近 1 或 -1 。比如输入 $ x = 10 $ 和 $ x = 1000 $ ,输出几乎都是 1 ,但“输入相差 100 倍”的信息,会被压缩成“无差异的 1” ,丢失细节;
- 梯度消失问题:当 $ |x| > 3 $ 时,导数 $ f’(x) \to 0 $ 。多层神经网络中,前面层的梯度经过 tanh 后几乎消失,导致“深层网络学不到特征”,无法训练深层模型(比如超过 5 层就容易梯度消失);
- 优点(对比 sigmoid 的改进)
正因这些特点,tanh 比 sigmoid 更常用,但也有局限:
隐藏层:适合用在隐藏层(利用零中心输出和更大梯度,提升训练效率),比如早期的 RNN 网络,常把 tanh 当隐藏层激活函数;
输出层:一般不用(输出需要“概率化”时,还是得用 sigmoid 或 softmax);
如果做神经网络开发,常见的“搭配”是:
隐藏层用 tanh:利用零中心输出和大梯度,让网络更快收敛、更高效学习特征;
输出层用 sigmoid:需要“概率化输出”(比如二分类任务)时,用 sigmoid 把输出映射到 (0, 1) ,直接表示“属于某一类的概率”;
代码:
# 创建画布和坐标轴 _, 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()
1.2.4 ReLU 激活函数
ReLU(Rectified Linear Unit,修正线性单元)激活函数公式:
f ( x ) = max ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)激活函数求导公式:
f ′ ( x ) = { 0 , x < 0 1 , x > 0 f'(x) = \begin{cases} 0, & x < 0 \\ 1, & x > 0 \end{cases} f′(x)={0,1,x<0x>0- 注: x = 0 x = 0 x=0 处导数通常可定义为 0 或 1,实际训练中不影响,因为输入恰好为 0 的概率极低;
函数图像与特性:
- 函数图像(左图):横轴是输入 $ x $ ,纵轴是输出 $ f(x) $。特点:
- 当 x ≤ 0 x \leq 0 x≤0 时,输出“硬踩刹车”直接为 0;
- 当 x > 0 x > 0 x>0 时,输出“笔直通过”等于 x x x ,呈现 “单侧抑制,右侧激活” 的特性;
- 导数图像(右图):横轴是输入 $ x $ ,纵轴是导数 $ f’(x) $。特点:
- 当 x < 0 x < 0 x<0 时,导数“一刀切”为 0;
- 当 x > 0 x > 0 x>0 时,导数恒为 1 ,简单到极致;
- 函数图像(左图):横轴是输入 $ x $ ,纵轴是输出 $ f(x) $。特点:
ReLU 是当前深度学习最常用的激活函数之一,核心优势和缺陷都源于其“简单直接”的特性(解决传统激活函数痛点):
- 计算高效:没有指数、分式等复杂运算(对比 Sigmoid 的 1 1 + e − x \frac{1}{1 + e^{-x}} 1+e−x1 、Tanh 的 1 − e − 2 x 1 + e − 2 x \frac{1 - e^{-2x}}{1 + e^{-2x}} 1+e−2x1−e−2x ),只有“比较+选择”( max ( 0 , x ) \max(0, x) max(0,x) ),训练和推理速度大幅提升,尤其适合深层、大规模网络;
- 缓解梯度消失:当 x > 0 x > 0 x>0 时,导数恒为 1 ,梯度不会像 Sigmoid/Tanh 那样“随着层数加深衰减到 0” 。比如在深层网络中,ReLU 能让梯度“稳定传递”,支持训练几十层、上百层的模型(如 ResNet),突破了传统激活函数无法训练深层网络的限制;
- 稀疏性与抗过拟合:当 x ≤ 0 x \leq 0 x≤0 时,输出直接为 0 ,相当于“关闭”部分神经元,让网络变得 “稀疏” 。稀疏性减少了神经元间的“相互依赖”,降低了过拟合风险(参数更少、模型更简单 ),也契合生物神经网络的“节能”特性;
关键缺点(需 trade-off)
- 神经元死亡问题:如果训练过程中,某些神经元的输入长期 ≤ 0 \leq 0 ≤0 (比如初始化不当、学习率过高),它们的导数会恒为 0 ,参数(权重 w w w 、偏置 b b b)无法更新,相当于“永久失效”,称为 “神经元死亡” 。一旦大量神经元死亡,模型表达能力会严重下降;
- 非零中心输出:ReLU 输出在 [ 0 , + ∞ ) [0, +\infty) [0,+∞) ,不像 Tanh 那样以 0 为中心。虽然影响不如 Sigmoid 显著,但仍可能导致 梯度方向单一(比如某层梯度全为正),影响训练稳定性(不过实际中,因“稀疏性”和“梯度稳定”,这个问题常被弱化);
ReLU 的特性让它几乎成为 “默认首选” 激活函数,尤其适合:
深层网络(如 ResNet、VGG):靠“ x > 0 x > 0 x>0 时梯度恒为 1”,支持训练几十层、上百层的模型,突破梯度消失瓶颈;
大规模数据与模型:计算高效,能加速训练,适配 ImageNet 分类、大语言模型等对算力要求高的任务;
需要稀疏表达的场景:比如图像识别中,ReLU 能自动“关闭”无关神经元,聚焦关键特征,提升模型鲁棒性;
对比 Sigmoid/Tanh:ReLU 如何改变深度学习
激活函数 核心公式 输出范围 梯度特性 计算复杂度 适用场景 ReLU max ( 0 , x ) \max(0, x) max(0,x) [ 0 , + ∞ ) [0, +\infty) [0,+∞) x > 0 x > 0 x>0 时梯度=1, x ≤ 0 x \leq 0 x≤0 时梯度=0 极低(无复杂运算) 深层网络、大规模模型、通用场景 Sigmoid 1 1 + e − x \frac{1}{1 + e^{-x}} 1+e−x1 ( 0 , 1 ) (0, 1) (0,1) 梯度范围 ( 0 , 0.25 ) (0, 0.25) (0,0.25),易消失 中(指数运算) 二分类输出层(需概率化) Tanh 1 − e − 2 x 1 + e − 2 x \frac{1 - e^{-2x}}{1 + e^{-2x}} 1+e−2x1−e−2x ( − 1 , 1 ) (-1, 1) (−1,1) 梯度范围 ( 0 , 1 ) (0, 1) (0,1),仍易消失($ x $ 大时) 代码:
# 创建画布和坐标轴 _, 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()
1.2.5 SoftMax 激活函数
SoftMax 是为多分类任务设计的激活函数,公式:
softmax ( z i ) = e z i ∑ j e z j \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j} e^{z_j}} softmax(zi)=∑jezjezi输入: z i z_i zi 是神经网络输出的“原始得分”(logits),比如多分类中,每个类别对应一个 z i z_i zi ;
输出:把每个 z i z_i zi 映射到 (0, 1) 区间,且所有类别的输出之和为 1 ,可直接理解为“该样本属于某一类的概率”;
计算过程:
比如网络输出 10 个类别的原始得分(logits):
scores = [0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75]
;SoftMax 计算分两步:
- 分子:对每个得分 z i z_i zi ,计算 e z i e^{z_i} ezi(比如最后一个类别 z = 3.75 z=3.75 z=3.75 , e 3.75 ≈ 42.5 e^{3.75} \approx 42.5 e3.75≈42.5 )
- 分母:把所有类别“ e z j e^{z_j} ezj”相加(比如把 10 个 e z j e^{z_j} ezj 求和)
- 概率:每个类别的分子÷分母,得到“属于该类的概率”
对应代码(PyTorch 示例 ):
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]) probabilities = torch.softmax(scores, dim=0) # dim=0 表示对这一维(类别维度)计算 print(probabilities)
输出结果(近似值):
[0.0212, 0.0177, 0.0202, 0.0202, 0.0638, 0.0287, 0.0185, 0.0522, 0.0183, 0.7392]
;- 最后一个类别概率最高(≈73.92% ),所以模型会预测样本属于这个类别;
- 所有概率相加≈1(满足概率和为 1 的性质);
特点:
多分类专属:是 Sigmoid 在多分类任务的“升级版”。Sigmoid 适合二分类(输出一个概率),SoftMax 适合多分类(输出多个类别概率,和为 1);
概率化输出:直接把原始得分转化为“概率分布”,方便理解预测结果(比如“猫”的概率 80%,“狗”的概率 15% … );
放大差异:指数函数 e z i e^{z_i} ezi 会放大原始得分的差异。比如原始得分高的类别(如 3.75 ),经过 e 3.75 e^{3.75} e3.75 后会远远超过其他类别,让概率“向高得分类别集中”,突出模型的判断倾向;
应用场景:只要是多分类任务(比如图像识别、文本分类),几乎都会在输出层用 SoftMax ,把网络输出的 logits 转化为“类别概率”,方便:
预测:选概率最大的类别作为结果;
训练:用交叉熵损失(Cross-Entropy Loss ),对比“预测概率分布”和“真实标签分布”,指导网络学习。
1.2.6 其它常见的激活函数
1.2.7 激活函数的选择方法
隐藏层激活函数选择
- 首选 ReLU:计算高效、缓解梯度消失,是当前深度学习隐藏层“默认选项”;
- ReLU 效果差时:尝试变体(如 Leaky ReLU),解决“神经元死亡”问题;
- 用 ReLU 需注意:避免大梯度导致过多神经元死亡(输入长期≤0 ,参数无法更新);
- sigmoid/tanh 慎选:sigmoid 计算慢、易梯度消失;tanh 比 sigmoid 好,但仍不如 ReLU ,隐藏层少用;
输出层激活函数选择:根据任务类型选
二分类:用 sigmoid ,输出 (0,1) 概率(如“是/否”“垃圾邮件/正常邮件”);
多分类:用 softmax ,输出多类别概率分布(和为 1 ,如“猫/狗/鸟”分类);
回归任务:用 identity(恒等函数,输出直接等于输入),预测连续值(如房价、温度)。
1.3 参数初始化
import torch.nn as nn
基础初始化方法(简单直接,但有缺陷)
均匀分布初始化:权重从区间 $ (-\frac{1}{\sqrt{d}}, \frac{1}{\sqrt{d}}) $ 均匀取值($ d $ 是神经元输入数量)。让初始权重有一定范围,避免过大/过小;
# 1. 均匀分布初始化 def test01(): linear = nn.Linear(5, 3) # 从0-1均匀分布产生参数 nn.init.uniform_(linear.weight) print(linear.weight.data) test01()
正态分布初始化:权重从均值 0、标准差 1 的高斯分布取样,用很小的值初始化,让权重“有差异但不过激”;
# 2. 正态分布初始化 def test02(): linear = nn.Linear(5, 3) nn.init.normal_(linear.weight, mean=0, std=1) print(linear.weight.data) test02()
全0/全1初始化:所有权重设为 0 或 1 。但严重缺陷:全0会导致“所有神经元更新一致”(梯度相同,网络学不到差异);全1会让初始权重过大,可能导致梯度爆炸,实际很少用;
# 3. 全1初始化 def test03(): linear = nn.Linear(5, 3) nn.init.ones_(linear.weight) print(linear.weight.data) # 4. 全0初始化 def test04(): linear = nn.Linear(5, 3) nn.init.zeros_(linear.weight) print(linear.weight.data) test03() test04()
固定值初始化:所有权重设为某个固定值(比如 0.5)。和全0/全1类似,易让神经元更新同质化,不推荐复杂网络用;
# 5.固定初始化 def test05(): linear = nn.Linear(5, 3) nn.init.constant_(linear.weight, 5) print(linear.weight.data) test05()
进阶优化方法(解决“梯度消失/爆炸”,常用!)
Kaiming 初始化(HE 初始化),专为ReLU 激活函数设计,分两种:
正态分布 HE:标准差 $ stddev = \sqrt{\frac{2}{fan_in}} ( ( ( fan_in $ 是输入神经元数量);
均匀分布 HE:从 $ [-\text{limit}, \text{limit}] $ 均匀取样,$ limit = \sqrt{\frac{6}{fan_in}} $;
作用:让 ReLU 网络初始梯度更稳定,缓解“神经元死亡”和梯度消失;
# 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) test06()
Xavier 初始化(Glorot 初始化)。适合sigmoid/tanh 激活函数,分两种:
正态分布 Xavier:标准差 $ stddev = \sqrt{\frac{2}{{fan_in} + fan_out}} ( ( (fan_out$ 是输出神经元数量);
均匀分布 Xavier:从 $ [-\text{limit}, \text{limit}] $ 均匀取样,$ limit = \sqrt{\frac{6}{{fan_in} + {fan_out}}} $;
作用:让输入输出的方差更稳定,避免深层网络梯度消失/爆炸;
# 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) test07()
怎么选?
简单网络/快速验证:用均匀分布或正态分布(避免全0/全1);
深层网络(尤其用 ReLU ):优先Kaiming 初始化,适配 ReLU 特性,缓解梯度问题;
用 sigmoid/tanh 的网络:试试Xavier 初始化,让方差更稳定。
1.4 神经网络搭建和参数计算
1.4.1 思路
在 PyTorch 里,定义神经网络要继承
nn.Module
类,并实现两个核心方法:__init__
方法(初始化网络结构)作用:定义网络的层结构(比如全连接层、卷积层),并初始化这些层的参数;
通常会在这里用
nn.Linear
定义全连接层(比如输入维度→隐藏层维度),同时可以指定参数初始化方式(如 Xavier、Kaiming);
forward
方法(定义前向传播)- 作用:描述数据在网络中的流动过程。当你把数据输入模型(如
model(input)
),PyTorch 会自动调用forward
,依次经过定义的层,输出结果; - 注意:学习率不是在
forward
里定义(内容里描述有误),学习率是优化器(如optimizer = Adam(model.parameters(), lr=0.001)
)的参数。forward
主要负责“层的调用 + 数据传递 + 激活函数应用”;
- 作用:描述数据在网络中的流动过程。当你把数据输入模型(如
接下来构建如下图所示的神经网络模型:
输入层:接收原始数据(比如
x1, x2, x3
+ 偏置项+1
,对应实际代码里的输入维度);隐藏层1:输入层→隐藏层1,需定义全连接层(输入维度→隐藏层1维度);
隐藏层2:隐藏层1→隐藏层2,同理定义全连接层;
输出层:隐藏层2→输出层,定义全连接层(隐藏层2维度→输出维度);
各层的“编码设计”(参数、初始化、激活函数)
隐藏层1:
权重初始化:用 Xavier 初始化(标准化的,适配 sigmoid 激活函数);
激活函数:用 sigmoid ,让输出映射到 (0, 1) ,增加非线性;
隐藏层2:
- 权重初始化:用 Kaiming(He)初始化(标准化的,适配 ReLU 激活函数);
- 激活函数:用 ReLU ,计算高效,缓解梯度消失;
输出层:
- 任务:二分类(比如判断“是/否”);
- 处理:用 SoftMax 激活函数(内容里写“线性层 + SoftMax 归一化”),把输出转化为“各类别的概率分布”(和为 1),选概率最大的类别作为预测结果;
流程总结:
PyTorch 网络搭建流程:继承
nn.Module
→__init__
定义层结构 →forward
定义数据流动 + 激活函数;层与初始化的对应:不同隐藏层选不同初始化方法(Xavier 适配 sigmoid,Kaiming 适配 ReLU),让训练更稳定;
激活函数的作用:为网络引入非线性,让模型能拟合复杂规律(比如 sigmoid 做非线性变换,ReLU 提升效率);
输出层适配任务:二分类用 SoftMax 输出概率,多分类同理,回归任务则用线性输出。
1.4.2 代码实现
导包:
import torch import torch.nn as nn # 导入torchsummary库,用于打印网络结构和参数信息 from torchsummary import summary # pip install torchsummary
创建神经网络模型类:
# 创建神经网络模型类 class Model(nn.Module): # 初始化方法,定义网络层和参数 def __init__(self): # 调用父类nn.Module的初始化方法,确保模型能正常工作 super(Model, self).__init__() # 定义第一个全连接层(线性层):接收3个输入特征,输出3个特征 # 全连接层会自动创建权重矩阵(3x3)和偏置向量(3,) self.linear1 = nn.Linear(3, 3) # 使用Xavier正态分布初始化第一个线性层的权重 # 适用于sigmoid/tanh等激活函数,帮助维持梯度稳定性 nn.init.xavier_normal_(self.linear1.weight) # 定义第二个全连接层:接收3个输入特征(与上一层输出匹配),输出2个特征 # 自动创建权重矩阵(2x3)和偏置向量(2,) self.linear2 = nn.Linear(3, 2) # 使用Kaiming(He)正态分布初始化第二个线性层的权重 # 适用于ReLU及其变种激活函数,缓解梯度消失问题 nn.init.kaiming_normal_(self.linear2.weight) # 定义输出层:接收2个输入特征,输出2个特征(对应二分类任务) # 自动创建权重矩阵(2x2)和偏置向量(2,) self.out = nn.Linear(2, 2) # 前向传播方法,定义数据在网络中的流动路径 # 当调用模型实例时(如model(input)),会自动执行此方法 def forward(self, x): # 数据通过第一个线性层进行线性变换 x = self.linear1(x) # 对第一个线性层的输出应用sigmoid激活函数,引入非线性 x = torch.sigmoid(x) # 数据通过第二个线性层进行线性变换 x = self.linear2(x) # 对第二个线性层的输出应用ReLU激活函数,引入非线性并缓解梯度消失 x = torch.relu(x) # 数据通过输出层进行线性变换 x = self.out(x) # 对输出层结果应用softmax激活函数 x = torch.softmax(x, dim=-1) # dim=-1表示在最后一个维度上进行归一化,使每个样本的输出值之和为1(概率分布) return x
主程序入口,当脚本直接运行时执行以下代码:
# 主程序入口,当脚本直接运行时执行以下代码 if __name__ == "__main__": # 自动选择计算设备:有GPU用GPU(cuda),否则用CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 实例化模型并将其移动到选定的设备上 my_model = Model().to(device) # 生成随机输入数据:5个样本,每个样本3个特征,并移动到同一设备 my_data = torch.randn(5, 3).to(device) print("mydata shape", my_data.shape) # 打印输入数据的形状:torch.Size([5, 3]) # 将数据输入模型,得到输出结果(前向传播过程) output = my_model(my_data) print("output shape-->", output.shape) # 打印输出结果的形状:torch.Size([5, 2])(5个样本,每个样本2个概率值) # 使用summary打印网络结构和参数统计信息 # input_size=(3,)表示输入特征数为3,batch_size=5表示批次大小为5 summary(my_model, input_size=(3,), batch_size=5) # 打印模型的所有参数(权重和偏置) print("======查看模型参数w和b======") # named_parameters()返回参数名称和对应的参数张量 for name, parameter in my_model.named_parameters(): print(name, parameter) # 打印参数名称(如linear1.weight)和参数值
输出结果:
mydata shape torch.Size([5, 3]) output shape--> torch.Size([5, 2]) ---------------------------------------------------------------- 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 ---------------------------------------------------------------- ======查看模型参数w和b====== linear1.weight Parameter containing: tensor([[-0.4504, 0.4981, -0.3432], [-0.4571, 1.2531, -0.0866], [ 0.6783, 0.6335, -0.3201]], device='cuda:0', requires_grad=True) linear1.bias Parameter containing: tensor([ 0.2968, -0.5274, -0.0010], device='cuda:0', requires_grad=True) linear2.weight Parameter containing: tensor([[-0.1666, 0.6741, 0.8177], [ 1.3470, 1.2420, 0.4636]], device='cuda:0', requires_grad=True) linear2.bias Parameter containing: tensor([-0.3471, 0.5757], device='cuda:0', requires_grad=True) out.weight Parameter containing: tensor([[-0.3468, -0.0467], [ 0.6420, 0.5581]], device='cuda:0', requires_grad=True) out.bias Parameter containing: tensor([-0.4003, 0.3164], device='cuda:0', requires_grad=True)
1.4.3 输出结果分析
神经网络处理数据时,输入和输出都是 [batch_size, features] 形状的张量:
输入张量:
[batch_size, in_features]
→ 代码里my_data
是[5, 3]
,表示 5 个样本,每个样本 3 个特征;输出张量:
[batch_size, out_features]
→ 代码里output
是[5, 2]
,表示 5 个样本,每个样本输出 2 个特征(二分类概率);前向传播中,每一层的计算会改变
features
维度(如linear1
把 3 维→3 维,linear2
把 3 维→2 维),但batch_size
保持不变(始终是 5 个样本一起算);
神经网络的“参数”指 权重(weight)和偏置(bias) ,决定网络如何“加工数据”。以第一个隐藏层(
linear1
)为例:计算逻辑(对应图示和代码)
上图中有“3 个神经元,每个神经元 4 个参数(w1, w2, w3, b1)” → 3×4=12 个参数;
代码里
linear1
是nn.Linear(3, 3)
→ 输入 3 维、输出 3 维;- 权重参数:形状是
[3, 3]
(3 个输出神经元 × 3 个输入特征)→ 9 个权重 - 偏置参数:形状是
[3]
(3 个输出神经元,每个 1 个偏置)→ 3 个偏置 - 总参数:9 + 3 = 12 个 → 和图示“3×4=12”完全对应(每个神经元 3 个权重 + 1 个偏置 = 4 个参数)
- 权重参数:形状是
各层参数统计(对应
summary
输出)linear1(第一个隐藏层):12 个参数(3×3 权重 + 3 偏置)
linear2(第二个隐藏层):
nn.Linear(3, 2)
→ 权重[2, 3]
(6 个) + 偏置[2]
(2 个)→ 共 8 个参数out(输出层):
nn.Linear(2, 2)
→ 权重[2, 2]
(4 个) + 偏置[2]
(2 个)→ 共 6 个参数总参数:12 + 8 + 6 = 26 个 → 和
summary
里的Total params: 26
一致
输入数据 vs 网络权重
输入数据:是网络要“处理的内容”(如
my_data
,形状[5, 3]
)→ 每次训练/推理的输入会变;网络权重:是网络的“固定配置”(如
linear1.weight
,形状[3, 3]
)→ 训练时靠反向传播更新,推理时固定不变;
小结:神经网络的数据流动(输入输出张量形状) 和参数构成(权重+偏置的计算逻辑):
前向传播时,输入
[batch_size, in_features]
张量,经过各层线性变换+激活函数,输出[batch_size, out_features]
张量;每层参数数量 = (输出神经元数 × 输入特征数) + 输出神经元数(权重 + 偏置);
1.4.4 补充:前向传播
前向传播(Forward Propagation)是神经网络计算过程中的核心步骤之一,通俗来讲,就是数据从输入层开始,按顺序依次经过网络的各层(隐藏层、输出层),逐层进行计算并传递,最终得到输出结果的过程;
以代码中的
Model
为例,输入数据my_data
(形状[5, 3]
,5 个样本、每个样本 3 个特征 ),前向传播的流程是:输入层→第一个隐藏层(线性层 + 激活函数)
x = self.linear1(x) # 第一步:线性变换 x = torch.sigmoid(x) # 第二步:激活函数引入非线性
线性变换:
self.linear1
是全连接层(输入 3 维→输出 3 维 ),会对输入x
做“加权求和 + 偏置”计算($ \text{output} = x \cdot w + b $ ,w
是权重、b
是偏置 ),相当于“用权重整合输入特征”;激活函数:
torch.sigmoid(x)
把线性变换的结果,映射到 (0, 1) 区间,让输出有非线性(否则多层线性层等价于一层,网络学不到复杂规律);
第一个隐藏层→第二个隐藏层(线性层 + 激活函数)
x = self.linear2(x) # 第一步:线性变换(3维→2维 ) x = torch.relu(x) # 第二步:ReLU激活,保留正数、抑制负数
线性变换:
self.linear2
把上一层输出(3 维)映射到 2 维,继续用权重整合特征;激活函数:
torch.relu(x)
让输出“非负”,进一步引入非线性,同时缓解梯度消失问题(相比 sigmoid 更适合深层网络);
第二个隐藏层→输出层(线性层 + 激活函数)
x = self.out(x) # 线性变换(2维→2维 ,为输出做准备 ) x = torch.softmax(x, dim=-1) # 激活函数:输出概率分布
线性变换:
self.out
把上一层输出(2 维)映射到 2 维,为最终分类结果做“数值准备”;激活函数:
torch.softmax
把输出映射到 (0, 1) ,且所有维度和为 1 ,相当于“输出每个类别的概率”(适合二分类任务,选概率大的类别作为预测结果);
前向传播的核心作用
特征逐层提炼:每一层的线性变换 + 激活函数,会让输入数据的“特征”被逐步整合、提炼。比如第一层可能提取“基础特征”(如图像边缘),第二层基于基础特征提取“复杂特征”(如图像轮廓),最终输出层基于这些特征做分类判断;
从输入到输出的“传递链”:数据像“接力棒”一样,从输入层开始,经过隐藏层的层层加工,最终在输出层得到“可解释”的结果(比如分类概率)。整个过程严格按照网络定义的层顺序执行,输入→隐藏层 1→隐藏层 2→输出层,一步接一步;
前向传播和“反向传播”的关系:前向传播是“算结果”的过程,而训练神经网络时,还需要“反向传播”(Backward Propagation)
反向传播:用损失函数计算“输出结果和真实标签的误差”,然后从输出层往输入层反向计算梯度,更新各层的权重和偏置,让网络“学会正确预测”;
前向传播是反向传播的基础:只有前向传播算出了输出,才能计算误差,进而反向更新参数。
1.5 神经网络的优缺点
优点
精度碾压:在图像识别(如 ResNet 识别图片)、自然语言处理(如 Transformer 翻译文本)等领域,能做到“超越传统机器学习、甚至逼近/超过人类表现”,靠的是深度网络对复杂模式的拟合能力;
万能拟合器:理论上,只要网络足够深、参数足够多,能近似任意非线性函数(比如拟合复杂曲线、学习语义关联)。加上硬件(GPU)助力,曾经理论上的“可能性”,现在能轻松落地;
生态完善:从 TensorFlow、PyTorch 到各类开源模型库(如 Hugging Face),“框架、工具、预训练模型”应有尽有。开发者不用从头造轮子,调参、训练、部署都有成熟方案,降低了使用门槛;
缺点
可解释性差(黑箱问题):网络能“高精度预测”,但很难说清“为什么输出这个结果”。比如医疗影像诊断模型,给不出“判断肿瘤的具体依据”,很难让医生/用户完全信任;
训练成本高:
- 算力需求大:深度网络参数动辄百万、上亿,训练时“矩阵运算、梯度回传”需要 GPU 集群甚至专用芯片(如 Tensor Core),普通电脑根本跑不动;
- 时间成本高:从几天到几周都有可能,尤其在“调参 + 重新训练”循环中,等待时间会非常漫长;
超参数敏感:网络结构(层数、每层神经元数 )、训练参数(学习率、batch_size)等超参数,对模型效果影响极大。调参像“开盲盒”,需要经验 + 试错,甚至要靠自动化工具(如 Optuna)辅助;
小数据易过拟合:神经网络“参数多、拟合能力强”,如果数据集小(比如只有几百张图片),模型会“死记硬背”数据规律,而非“学习通用特征”。一旦遇到新数据,预测效果会暴跌(过拟合)。