神经网络 | ④ 前向传播:数据如何流经网络

引述

​ 前面我们详细拆解了激活函数------从隐藏层的 S i g m o i d Sigmoid Sigmoid、 T a n h Tanh Tanh、 R e L U ReLU ReLU,到输出层的恒等、 S i g m o i d Sigmoid Sigmoid 或 S o f t m a x Softmax Softmax。但激活函数本身只是一个零件:它定义了单个神经元如何对输入做非线性变换,却还没有解释整个网络是如何"运转"起来。

​ 本文介绍神经网络的正向传播 ------ 数据从输入层流入,经过各隐藏层的线性变换和激活函数,最终从输出层流出预测结果。这是神经网络最基本的运转方式,也是后面理解反向传播和训练过程的基础。

​ 我们将从单个神经元的具体计算出发,逐步推导到矩阵形式的批量运算,并用 NumPy 手动实现一个能识别手写数字的前向传播网络。


前向传播

神经网络的计算/传播方式主要有两种:前向传播foward propagation, FP)与反向传播backward propagation, BP

  • 概念 ------ 所谓前向传播,顾名思义,即数据流从输入流向输出 (在视觉上是正向),通过 线性加权 → 加偏置 → 激活函数 线性加权 → 加偏置 → 激活函数 线性加权→加偏置→激活函数,把信息重构为更高阶的特征传递给下一层。通过逐层抽象的机制,让神经网络具备自动学习特征的能力。下面考虑一个有输入层、两层隐藏层、输出层的神经网络,看它如果把输入信息计算得到输出信息。

  • 具体计算过程

    • 现在考虑输入层有3个神经元、隐藏层有4个神经元的情况,每个神经元都有自己的权重和偏置

    • 输入层 ------ 对输入层的结点,其输出值就是输入值,即 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3

    • 隐藏层 ------ 输入层的结点 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3 与下一层的结点 a_1 有连接,则其输出值计算分为两步

      1. 线性变换 ------ 上一层的 x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3 结点与对应连接权值 w 11 , w 12 , w 13 w_{11},w_{12},w_{13} w11,w12,w13 进行加权和运算,结果加偏置项 b 1 b_1 b1:

        z 1 = w 11 x 1 + w 12 x 2 + w 13 x 3 + b 1 z_1 = w_{11}x_1+w_{12}x_2+w_{13}x_3+b_1 z1=w11x1+w12x2+w13x3+b1

      2. 激活函数 ------ 通过非线性函数 h ( ) h() h()(即激活函数),如 R e L u ReLu ReLu, s i g m o i d sigmoid sigmoid 等函数激活:

        a 1 = h ( z 1 ) a_1 = h(z_1) a1=h(z1)

      3. 得到的结果就是本层结点 a_1 的输出,对其他节点也是同样的操作:

        • 第1个神经元的计算: a 1 = h ( z 1 ) = σ ( b 1 + w 11 x 1 + w 12 x 2 + w 13 x 3 ) a_1 = h(z_1) = σ(b_1 + w_{11}x_1 + w_{12}x_2 + w_{13}x_3) a1=h(z1)=σ(b1+w11x1+w12x2+w13x3)

        • 第2个神经元的计算: a 2 = h ( z 2 ) = σ ( b 2 + w 21 x 1 + w 22 x 2 + w 23 x 3 ) a_2=h(z_2) = σ(b_2 + w_{21}x_1 + w_{22}x_2 + w_{23}x_3) a2=h(z2)=σ(b2+w21x1+w22x2+w23x3)

        • 第3个神经元的计算: a 3 = h ( z 3 ) = σ ( b 3 + w 31 x 1 + w 32 x 2 + w 33 x 3 ) a_3=h(z_3) = σ(b_3 + w_{31}x_1 + w_{32}x_2 + w_{33}x_3) a3=h(z3)=σ(b3+w31x1+w32x2+w33x3)

        • 第4个神经元的计算: a 4 = h ( z 4 ) = σ ( b 4 + w 41 x 1 + w 42 x 2 + w 43 x 3 ) a_4=h(z_4) = σ(b_4 + w_{41}x_1 + w_{42}x_2 + w_{43}x_3) a4=h(z4)=σ(b4+w41x1+w42x2+w43x3)

      4. 下一层也是同样的操作,不断通过 加权和+激活 的方法一层层的运算,最终到达输出层

    • 输出层 ------ 经过前面所有隐藏层的计算,数据到达输出层。输出层同样遵循"线性变换 + 激活函数"的模式,但激活函数的选择取决于任务类型的不同。



隐藏层的前向传播实现

在深度学习框架中,隐藏层被拆解为两个独立组件:

组件 英文 数学表达 作用
仿射变换层 Affine Z = X W + B Z=XW+B Z=XW+B 线性变换:缩放、旋转、平移
激活函数层 ReLU层、Sigmoid层等 A = σ ( Z ) A=σ(Z) A=σ(Z) 非线性映射:打破线性,赋予表达能力
全连接层 --- A = σ ( X W + B ) A=σ(XW+B) A=σ(XW+B) 完整的隐藏层运算

仿射变换层、激活函数层的组合就是一层完整的全连接层。通过这样的组合,一步步把神经网络搭建出来:


仿射变换层

仿射变换层又称 Affine 层,affine transformation仿射变换 是数学术语,意思是"线性变换 + 平移"。Affine 层实现的是 Z = X W + B Z=XW+B Z=XW+B 的功能

  • 参考前面的神经网络,把四个隐藏层的神经元关于 Z = X W + B Z=XW+B Z=XW+B 的操作提取出来,有:

    • 第1个神经元的操作: z 1 = b 1 + w 11 x 1 + w 12 x 2 + w 13 x 3 z_1 = b_1 + w_{11}x_1 + w_{12}x_2 + w_{13}x_3 z1=b1+w11x1+w12x2+w13x3

    • 第2个神经元的操作: z 2 = b 2 + w 21 x 1 + w 22 x 2 + w 23 x 3 z_2 = b_2 + w_{21}x_1 + w_{22}x_2 + w_{23}x_3 z2=b2+w21x1+w22x2+w23x3

    • 第3个神经元的操作: z 3 = σ ( b 3 + w 31 x 1 + w 32 x 2 + w 33 x 3 z_3 = σ(b_3 + w_{31}x_1 + w_{32}x_2 + w_{33}x_3 z3=σ(b3+w31x1+w32x2+w33x3

    • 第4个神经元的操作: z 4 = σ ( b 4 + w 41 x 1 + w 42 x 2 + w 43 x 3 z_4 = σ(b_4 + w_{41}x_1 + w_{42}x_2 + w_{43}x_3 z4=σ(b4+w41x1+w42x2+w43x3

  • 方程组化 ------ 把四个神经元写在一起,写成方程组

    { z 1 = b 1 + w 11 x 1 + w 12 x 2 + w 13 x 3 z 2 = b 2 + w 21 x 1 + w 22 x 2 + w 23 x 3 z 3 = b 3 + w 31 x 1 + w 32 x 2 + w 33 x 3 z 4 = b 4 + w 41 x 1 + w 42 x 2 + w 43 x 3 \begin{cases} z_1 = b_1 + w_{11}x_1 + w_{12}x_2 + w_{13}x_3 \\ z_2 = b_2 + w_{21}x_1 + w_{22}x_2 + w_{23}x_3 \\ z_3 = b_3 + w_{31}x_1 + w_{32}x_2 + w_{33}x_3 \\ z_4 = b_4 + w_{41}x_1 + w_{42}x_2 + w_{43}x_3 \end{cases} ⎩ ⎨ ⎧z1=b1+w11x1+w12x2+w13x3z2=b2+w21x1+w22x2+w23x3z3=b3+w31x1+w32x2+w33x3z4=b4+w41x1+w42x2+w43x3

  • 矩阵化 ------ 观察方程组,可以看到:每个 z i z_i zi 都是 x 1 , x 2 , x 3 x_1, x_2, x_3 x1,x2,x3 的线性组合。具体来说:

    w 11 x 1 + w 12 x 2 + w 13 x 3 w 21 x 1 + w 22 x 2 + w 23 x 3 w 31 x 1 + w 32 x 2 + w 33 x 3 w 41 x 1 + w 42 x 2 + w 43 x 3 = w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 w 41 w 42 w 43 x 1 x 2 x 3 \begin{bmatrix} w_{11}x_1 + w_{12}x_2 + w_{13}x_3 \\ w_{21}x_1 + w_{22}x_2 + w_{23}x_3 \\ w_{31}x_1 + w_{32}x_2 + w_{33}x_3 \\ w_{41}x_1 + w_{42}x_2 + w_{43}x_3 \end{bmatrix}= \begin{bmatrix} w_{11}&w_{12}&w_{13} \\ w_{21}&w_{22}&w_{23} \\ w_{31}&w_{32}&w_{33} \\ w_{41}&w_{42}&w_{43} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} w11x1+w12x2+w13x3w21x1+w22x2+w23x3w31x1+w32x2+w33x3w41x1+w42x2+w43x3 = w11w21w31w41w12w22w32w42w13w23w33w43 x1x2x3

    进一步的,加上偏置值:

    z 1 z 2 z 3 z 4 = w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 w 41 w 42 w 43 x 1 x 2 x 3 + b 1 b 2 b 3 b 4 \begin{bmatrix} z_1 \\ z_2\\ z_3 \\ z_4 \end{bmatrix} = \begin{bmatrix} w_{11}&w_{12}&w_{13} \\ w_{21}&w_{22}&w_{23} \\ w_{31}&w_{32}&w_{33} \\ w_{41}&w_{42}&w_{43} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ b_3 \\ b_4 \end{bmatrix} z1z2z3z4 = w11w21w31w41w12w22w32w42w13w23w33w43 x1x2x3 + b1b2b3b4

    写成简写,即有: A = W X + B A=WX+B A=WX+B

    这是为什么神经网络计算天然适合 G P U GPU GPU,因为它本质就是一连串大型矩阵运算,通过一次矩阵即可算完四个神经元

  • 代码实现 ------ 采用 numpy 库的 np.dot 矩阵乘法实现

    python 复制代码
    a = np.dot(x, W) + b
    • np.dot(x, W) 采用 numpy 的矩阵乘法

      • 输入 x x x 形状 ( N , n ) (N, n) (N,n),权重 W W W 形状 ( n , m ) (n, m) (n,m),有: ( N , n ) ⋅ ( n , m ) = ( N , m ) (N, n)\cdot(n, m)=(N, m) (N,n)⋅(n,m)=(N,m)
      • 结果矩阵的每个元素 = 某个样本与某个神经元权重的点积
    • + b 采用 numpy 的广播机制

      • 偏置 b b b 形状 ( m , ) (m,) (m,),有: ( N , m ) + ( m , ) = ( N , m ) (N, m) + (m,)=(N, m) (N,m)+(m,)=(N,m)
    • 整合为类的形式,有:

      python 复制代码
      class Affine:
          def __init__(self, W, b):
              self.W = W
              self.b = b
              self.x = None
                
          def forward(self, x):
              self.x = x
              return np.dot(x, self.W) + self.b

激活函数层

!IMPORTANT

激活函数层是紧跟在仿射变换之后的组件:

  • Affine 层完成了 Z Z Z = X W XW XW + B B B 的线性变换
  • 激活函数层则对 Z Z Z 中的每个元素独立应用非线性函数,得到本层的最终输出 A A A = σ σ σ( Z Z Z)
逐元素性质
  • 激活函数(如 SigmoidReLU)是逐元素应用的,这意味着:无论这层有多少个神经元,激活函数都会独立地处理每个神经元的输出

    python 复制代码
    # 输入一个数组
    a = np.array([-2, -1, 0, 1, 2])
    z = sigmoid(a)
    python 复制代码
    # 等价于对每个元素分别应用
    z[0] = sigmoid(a[0])
    z[1] = sigmoid(a[1]) ...
ReLU 函数
  • 数学表达 ------ R e L U ( x ) = max ⁡ ( 0 , x ) ReLU(x) = \max(0, x) ReLU(x)=max(0,x)

  • 代码实现

    python 复制代码
    def relu(x):
        return np.maximum(0, x)
Sigmoid 函数
  • 数学表达 ------ s i g m o i d ( x ) = 1 1 + e − x sigmoid(x) = \frac{1}{1 + e^{-x}} sigmoid(x)=1+e−x1

  • 代码实现

    python 复制代码
    def sigmoid(x):
        return 1 / (1 + np.exp(-x))
Tanh 函数
  • 数学表达 ------ T a n h ( 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

  • 代码实现

    python 复制代码
    def tanh(x):
        return np.tanh(x)  # NumPy 内置


输出层的前向传播实现

由于输出层与隐藏层的目标不同,进而他们的激活函数的任务也有所不同:

  • 隐藏层追求的是 "信号长距离传输""训练稳定性" ------ 激活函数负责引入非线性、保证梯度流动
  • 输出层追求的是 "匹配任务格式" ------ 激活函数负责把内部特征翻译成符合任务需求的结果

输出层任务分为两大类:分类回归,根据问题类型选择不同的激活函数:

任务类型 激活函数 输出范围 举例
回归问题 恒等函数 任意实数 房价 200
二分类问题 S i g m o i d Sigmoid Sigmoid 函数 一个 ( 0 , 1 ) (0, 1) (0,1) 的概率 垃圾邮件 0.92 → 是垃圾
多分类问题 S o f t m a x Softmax Softmax 函数 多个 ( 0 , 1 ) (0, 1) (0,1) 的概率,且总和为 1 1 1 0.1、狗 0.7、鸟 0.2 → 是狗
多标签问题 N N N 个 S i g m o i d Sigmoid Sigmoid 函数 多个独立的 ( 0 , 1 ) (0, 1) (0,1) 的概率 有猫 0.9、有狗 0.8、有鸟 0.6
恒等函数
  • 任务 ------ 把任意实数原样输出,用于回归任务

  • 数学表达 ------ σ ( x ) = x σ(x) = x σ(x)=x

  • 代码实现

    python 复制代码
    def identity(x):
        return x
Sigmoid 函数
  • 任务 ------ 把任意实数映射到 ( 0 , 1 ) (0,1) (0,1) 区间,输出可解释为"正类概率"

  • 数学表达 ------ s i g m o i d ( x ) = 1 1 + e − x sigmoid(x) = \frac{1}{1 + e^{-x}} sigmoid(x)=1+e−x1

  • 代码实现

    • 基本实现

      python 复制代码
      def sigmoid(x):
          return 1 / (1 + np.exp(-x))
    • 二分类任务 ------ 判断邮件是否为垃圾邮件

      python 复制代码
      logit = 2.0                    # 网络原始输出
      prob = sigmoid(logit)
      print(f"垃圾邮件的概率: {prob:.4f}")  		# 0.8808
      print(f"正常邮件的概率: {1 - prob:.4f}")  	# 0.1192
    • 多标签分类 ------ 判断图片里是否有猫、狗、兔、鸟

      python 复制代码
      labels = ['猫', '狗', '兔', '鸟']				# 4个标签
      logits = np.array([3.0, -1.5, 0.8, -2.0])       # 4个标签的原始输出
      probs = sigmoid(logits)
      
      for label, prob in zip(labels, probs):
          print(f"标签 [{label}]: {prob:.4f} ({'✓' if prob > 0.5 else '✗'})")
      python 复制代码
      # 标签 [猫]: 0.9526 (✓)
      # 标签 [狗]: 0.1824 (✗)
      # 标签 [兔]: 0.6900 (✓)
      # 标签 [鸟]: 0.1192 (✗)
      # 一张图片可以同时有猫、狗、兔、鸟,各标签独立判断
Softmax 函数
  • 任务 ------ 把多个实数同时挤压到 (0, 1) 且强制总和为 1,形成一个概率分布

  • 数学表达 ------ y k = e a k ∑ i = 1 n e a i y_k = \frac{e^{a_k}}{\sum_{i=1}^{n} e^{a_i}} yk=∑i=1neaieak,其中 a k a_k ak 是第 k k k 个输出神经元的原始值( l o g i t s logits logits), n n n 是类别总数

  • 代码实现

    • 基本实现

      python 复制代码
      def softmax(a):
          exp_a = np.exp(a)             # 分子:取指数
          sum_exp_a = np.sum(exp_a)     # 分母:指数求和
          y = exp_a / sum_exp_a
          return y
    • 多分类任务 ------ 判断图片的动物是否为猫(判断选项有猫、狗、兔)

      python 复制代码
      a = np.array([0.3, 2.9, 4.0])		# 3个选项的原始输出
      y = softmax(a)
      
      print(y)                         # [0.018 0.245 0.737]
      print(np.sum(y))                 # 1.0(概率之和为 1)
Softmax 的实现陷阱:数值溢出
  • 问题 ------ 上面的基础实现虽然正确,但存在一个严重问题:数值溢出,考虑一个极端情况:

    python 复制代码
    a = np.array([1010, 1000, 990])
    result = np.exp(a) / np.sum(np.exp(a))		# 输出: array([nan, nan, nan])

    这里输出错误数据 n a n nan nan,这是因为 e 1010 e^{1010} e1010 是一个天文数字,超出了计算机能表示的范围

  • 解决原理 ------ 常数不变性

    • Softmax 有一个重要的数学性质:同时加减一个常数,结果不变

    • 故可以给所有输入减去一个数(最大值),结果不变,且避免数值溢出问题

      证明过程:

      y k = e a k ∑ i = 1 n e a i = C ⋅ e a k C ⋅ ∑ i = 1 n e a i = e a k + log ⁡ C ∑ i = 1 n e a i + log ⁡ C = e a k + C ′ ∑ i = 1 n e a i + C ′ y_k = \frac{e^{a_k}}{\sum_{i=1}^{n} e^{a_i}} = \frac{C \cdot e^{a_k}}{C \cdot \sum_{i=1}^{n} e^{a_i}} = \frac{e^{a_k + \log C}}{\sum_{i=1}^{n} e^{a_i + \log C}} = \frac{e^{a_k + C'}}{\sum_{i=1}^{n} e^{a_i + C'}} yk=∑i=1neaieak=C⋅∑i=1neaiC⋅eak=∑i=1neai+logCeak+logC=∑i=1neai+C′eak+C′

  • 代码改进

    • 在输入时,先减去最大值,再取指数

      python 复制代码
      def softmax_safe(a):
          exp_a = np.exp(a - np.max(a))	# 减去最大值后取指数
          sum_exp_a = np.sum(exp_a)
          return exp_a / sum_exp_a
    • 补全处理 2 D 2D 2D 输入和处理 1 D 1D 1D​ 输入的不同情况,得到最终实现

      python 复制代码
      def softmax(x):
          # 处理2D输入(批处理)
          if x.ndim == 2:
              x = x.T
              x = x - np.max(x, axis=0)
              y = np.exp(x) / np.sum(np.exp(x), axis=0)
              return y.T
          
          # 处理1D输入
          x = x - np.max(x)
          return np.exp(x) / np.sum(np.exp(x))
神经元数量的设计原则

基本原则:输出层神经元数 = 任务输出维度

任务类型 神经元数量 激活函数 示例 示例神经元数
二分类 1 S i g m o i d Sigmoid Sigmoid 垃圾邮件判断 1 个神经元,输出 P ( 是垃圾邮件 ) P(\text{是垃圾邮件}) P(是垃圾邮件)
多分类( k k k 个种类) k k k S o f t m a x Softmax Softmax 手写数字识别 10 个神经元,依次对应数字 0~9
多标签分类( k k k 个标签) k k k S i g m o i d Sigmoid Sigmoid 动物标签 4 个神经元,分别判断图片的猫、狗、鸟、兔
回归(单值) 1 恒等函数 房价预测 1 个神经元,输出价格
回归(多值) 预测值个数 恒等函数 预测天气 3 个神经元,输出温度 + 湿度 + 气压

参考文献

1 斋藤康毅. 深度学习入门:基于Python的理论与实现M. 陆宇杰, 译. 北京: 人民邮电出版社, 2018.

2 谦行AIing. "从MLP到神经网络,理解反向传播的基础." 小红书 , 2026.5.10, http://xhslink.com/o/39HpvVA52Di