深度学习——03 神经网络(1)-神经网络

发布于:2025-08-13 ⋅ 阅读:(28) ⋅ 点赞:(0)

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+ex1

    • 作用:把任意实数输入(比如神经网络里的加权和结果),压缩映射到 (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+ex1)=1+ex1(11+ex1)=f(x)(1f(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+e2x1e2x

    • 作用:把任意实数输入(比如神经网络的加权和结果),压缩映射到 (-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+e2x1e2x)=1f(x)2=1tanh2(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 ,参数更新“停滞”;

    在这里插入图片描述

  • 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 层就容易梯度消失);
  • 正因这些特点,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 x0 时,输出“硬踩刹车”直接为 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 ,简单到极致;

    在这里插入图片描述

  • ReLU 是当前深度学习最常用的激活函数之一,核心优势和缺陷都源于其“简单直接”的特性(解决传统激活函数痛点):

    • 计算高效:没有指数、分式等复杂运算(对比 Sigmoid 的 1 1 + e − x \frac{1}{1 + e^{-x}} 1+ex1 、Tanh 的 1 − e − 2 x 1 + e − 2 x \frac{1 - e^{-2x}}{1 + e^{-2x}} 1+e2x1e2x ),只有“比较+选择”( 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 x0 时,输出直接为 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 x0 时梯度=0 极低(无复杂运算) 深层网络、大规模模型、通用场景
    Sigmoid 1 1 + e − x \frac{1}{1 + e^{-x}} 1+ex1 ( 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+e2x1e2x ( − 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 计算分两步:

      1. 分子:对每个得分 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.7542.5
      2. 分母:把所有类别“ e z j e^{z_j} ezj”相加(比如把 10 个 e z j e^{z_j} ezj 求和)
      3. 概率:每个类别的分子÷分母,得到“属于该类的概率”
    • 对应代码(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),选概率最大的类别作为预测结果;
  • 流程总结:

    1. PyTorch 网络搭建流程:继承 nn.Module__init__ 定义层结构 → forward 定义数据流动 + 激活函数;

    2. 层与初始化的对应:不同隐藏层选不同初始化方法(Xavier 适配 sigmoid,Kaiming 适配 ReLU),让训练更稳定;

    3. 激活函数的作用:为网络引入非线性,让模型能拟合复杂规律(比如 sigmoid 做非线性变换,ReLU 提升效率);

    4. 输出层适配任务:二分类用 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 )为例:

    在这里插入图片描述

    1. 计算逻辑(对应图示和代码)

      • 上图中有“3 个神经元,每个神经元 4 个参数(w1, w2, w3, b1)” → 3×4=12 个参数;

      • 代码里 linear1nn.Linear(3, 3) → 输入 3 维、输出 3 维;

        • 权重参数:形状是 [3, 3](3 个输出神经元 × 3 个输入特征)→ 9 个权重
        • 偏置参数:形状是 [3](3 个输出神经元,每个 1 个偏置)→ 3 个偏置
        • 总参数:9 + 3 = 12 个 → 和图示“3×4=12”完全对应(每个神经元 3 个权重 + 1 个偏置 = 4 个参数)
    2. 各层参数统计(对应 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 神经网络的优缺点

  • 优点

    1. 精度碾压:在图像识别(如 ResNet 识别图片)、自然语言处理(如 Transformer 翻译文本)等领域,能做到“超越传统机器学习、甚至逼近/超过人类表现”,靠的是深度网络对复杂模式的拟合能力

    2. 万能拟合器:理论上,只要网络足够深、参数足够多,能近似任意非线性函数(比如拟合复杂曲线、学习语义关联)。加上硬件(GPU)助力,曾经理论上的“可能性”,现在能轻松落地;

    3. 生态完善:从 TensorFlow、PyTorch 到各类开源模型库(如 Hugging Face),“框架、工具、预训练模型”应有尽有。开发者不用从头造轮子,调参、训练、部署都有成熟方案,降低了使用门槛;

  • 缺点

    • 可解释性差(黑箱问题):网络能“高精度预测”,但很难说清“为什么输出这个结果”。比如医疗影像诊断模型,给不出“判断肿瘤的具体依据”,很难让医生/用户完全信任;

    • 训练成本高

      • 算力需求大:深度网络参数动辄百万、上亿,训练时“矩阵运算、梯度回传”需要 GPU 集群甚至专用芯片(如 Tensor Core),普通电脑根本跑不动;
      • 时间成本高:从几天到几周都有可能,尤其在“调参 + 重新训练”循环中,等待时间会非常漫长;
    • 超参数敏感:网络结构(层数、每层神经元数 )、训练参数(学习率、batch_size)等超参数,对模型效果影响极大。调参像“开盲盒”,需要经验 + 试错,甚至要靠自动化工具(如 Optuna)辅助;

    • 小数据易过拟合:神经网络“参数多、拟合能力强”,如果数据集小(比如只有几百张图片),模型会“死记硬背”数据规律,而非“学习通用特征”。一旦遇到新数据,预测效果会暴跌(过拟合)。


网站公告

今日签到

点亮在社区的每一天
去签到