摘要:本章深入讲解神经网络的基础理论,从生物神经元启发到感知机模型,揭示单层感知机的局限性,完整推导多层感知机的结构与反向传播算法,详解激活函数演进历程与权重初始化策略,并通过NumPy从零实现神经网络完成手写数字分类实战。
环境声明
- Python版本:Python 3.12+
- 核心依赖:NumPy 1.24+, Matplotlib 3.7+, Scikit-learn 1.3+
- 开发工具:PyCharm / VS Code / Jupyter Notebook
- 操作系统:Windows / macOS / Linux
pip install numpy matplotlib scikit-learn
学习目标
完成本章学习后,你将能够:
1. 感知机原理与局限性
1.1 生物神经元的启发
神经网络的设计灵感来源于生物神经系统。人脑约包含860亿个神经元,每个神经元通过突触与其他神经元连接,形成复杂的网络结构。
生物神经元的工作机制:
| 树突 | 接收来自其他神经元的信号输入 |
| 细胞体 | 整合输入信号,进行信息处理 |
| 轴突 | 输出信号至其他神经元 |
| 突触 | 连接不同神经元的信号传递节点 |
信息传递过程:
关键特性:
- 整合性:神经元整合多个输入信号
- 阈值特性:只有超过阈值才触发输出
- 可塑性:突触连接强度可随学习改变
1.2 感知机模型
1943年,McCulloch和Pitts提出了第一个人工神经元模型(M-P模型)。1957年,Frank Rosenblatt在此基础上发明了感知机(Perceptron),成为神经网络发展的里程碑。
感知机的数学模型:
y=f(∑i=1nwixi+b)=f(wTx+b)y = f\\left(\\sum_{i=1}^{n} w_i x_i + b\\right) = f(w^T x + b)y=f(i=1∑nwixi+b)=f(wTx+b)
其中:
- x=[x1,x2,…,xn]Tx = [x_1, x_2, …, x_n]^Tx=[x1,x2,…,xn]T 为输入特征向量
- w=[w1,w2,…,wn]Tw = [w_1, w_2, …, w_n]^Tw=[w1,w2,…,wn]T 为权重向量
- bbb 为偏置项
- f(⋅)f(\\cdot)f(⋅) 为激活函数(通常使用阶跃函数)
阶跃函数定义:
f(z)={1if z≥00if z<0f(z) = \\begin{cases} 1 & \\text{if } z \\geq 0 \\\\ 0 & \\text{if } z < 0 \\end{cases}f(z)={10if z≥0if z<0
感知机的几何解释:
感知机在特征空间中定义了一个超平面 wTx+b=0w^T x + b = 0wTx+b=0,将空间划分为两个区域:
- 超平面一侧的样本被分类为正类(输出1)
- 另一侧的样本被分类为负类(输出0)
1.3 感知机学习算法
感知机使用迭代算法更新权重,核心思想是:当分类错误时,调整权重使超平面向误分类样本移动。
算法步骤:
输入: 训练集 {(x_i, y_i)},学习率 η
初始化: w = 0, b = 0
重复:
对于每个样本 (x_i, y_i):
计算预测值: ŷ = sign(w^T x_i + b)
如果 ŷ ≠ y_i:
w ← w + η * y_i * x_i
b ← b + η * y_i
直到所有样本被正确分类或达到最大迭代次数
权重更新规则的推导:
当样本 (xi,yi)(x_i, y_i)(xi,yi) 被误分类时,满足 yi(wTxi+b)≤0y_i(w^T x_i + b) \\leq 0yi(wTxi+b)≤0。
更新后的权重应满足:
yi(wnewTxi+bnew)=yi[(w+ηyixi)Txi+(b+ηyi)]y_i(w_{new}^T x_i + b_{new}) = y_i[(w + \\eta y_i x_i)^T x_i + (b + \\eta y_i)]yi(wnewTxi+bnew)=yi[(w+ηyixi)Txi+(b+ηyi)]
=yi(wTxi+b)+ηyi2xiTxi+ηyi2= y_i(w^T x_i + b) + \\eta y_i^2 x_i^T x_i + \\eta y_i^2=yi(wTxi+b)+ηyi2xiTxi+ηyi2
=yi(wTxi+b)+η(∥xi∥2+1)>yi(wTxi+b)= y_i(w^T x_i + b) + \\eta (\\|x_i\\|^2 + 1) > y_i(w^T x_i + b)=yi(wTxi+b)+η(∥xi∥2+1)>yi(wTxi+b)
这表明更新后,样本被正确分类的可能性增加。
收敛性定理(Novikoff, 1962):
如果训练数据线性可分,感知机算法在有限步内收敛。设:
- R=maxi∥xi∥R = \\max_i \\|x_i\\|R=maxi∥xi∥ 为样本的最大范数
- γ\\gammaγ 为分离超平面的几何间隔
则收敛所需的更新次数不超过 (R/γ)2(R/\\gamma)^2(R/γ)2。
1.4 感知机的局限性:异或问题
1969年,Minsky和Papert在《Perceptrons》一书中证明了单层感知机的致命局限:无法解决非线性可分问题。
异或问题(XOR Problem):
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
几何分析:
在二维平面上,异或问题的四个样本点分布如下:
- 负类(输出0):(0,0) 和 (1,1)
- 正类(输出1):(0,1) 和 (1,0)
不存在一条直线能将两类样本完全分开。正类样本位于对角位置,任何线性超平面都无法正确分类。
深层原因:
感知机只能表示线性函数,而异或问题是非线性可分的。这一发现导致神经网络研究进入第一次寒冬期。
补充:Minsky的批评虽然指出了单层感知机的局限,但也为多层网络的发展指明了方向。1986年,Rumelhart等人提出反向传播算法,使训练多层网络成为可能,开启了神经网络的复兴。
2. 多层感知机结构
2.1 从单层到多层
多层感知机(Multi-Layer Perceptron, MLP)通过引入隐藏层,解决了单层感知机的非线性问题。MLP也被称为前馈神经网络(Feedforward Neural Network)或深度神经网络(DNN)。
MLP的核心创新:
万能近似定理(Universal Approximation Theorem):
1989年,Cybenko和Hornik等人证明:具有至少一个隐藏层的前馈神经网络,只要隐藏层神经元数量足够,配合非线性激活函数,可以以任意精度近似任何连续函数。
数学表述:设 σ(⋅)\\sigma(\\cdot)σ(⋅) 为非线性激活函数,K⊂RnK \\subset \\mathbb{R}^nK⊂Rn 为紧致集,f:K→Rf: K \\to \\mathbb{R}f:K→R 为连续函数。对于任意 ϵ>0\\epsilon > 0ϵ>0,存在整数 NNN、权重 wiw_iwi、偏置 bib_ibi 和 viv_ivi,使得:
∣f(x)−∑i=1Nviσ(wiTx+bi)∣<ϵ\\left|f(x) – \\sum_{i=1}^{N} v_i \\sigma(w_i^T x + b_i)\\right| < \\epsilonf(x)−i=1∑Nviσ(wiTx+bi)<ϵ
对所有 x∈Kx \\in Kx∈K 成立。
2.2 网络层结构详解
一个典型的三层MLP包含:输入层、一个隐藏层、输出层。
输入层:
- 接收原始特征数据
- 神经元数量等于特征维度
- 不对数据进行任何变换
隐藏层:
- 进行非线性特征变换
- 神经元数量和层数是超参数
- 每个神经元执行:hj=σ(∑iwjixi+bj)h_j = \\sigma(\\sum_i w_{ji} x_i + b_j)hj=σ(∑iwjixi+bj)
输出层:
- 产生最终预测结果
- 神经元数量取决于任务类型
- 回归任务:1个神经元(线性输出)
- 二分类:1个神经元(Sigmoid输出)
- 多分类:KKK 个神经元(Softmax输出)
层间连接:
- 全连接(Dense):每层神经元与下一层所有神经元相连
- 权重矩阵 W[l]∈Rnl×nl−1W^{[l]} \\in \\mathbb{R}^{n_l \\times n_{l-1}}W[l]∈Rnl×nl−1
- 偏置向量 b[l]∈Rnlb^{[l]} \\in \\mathbb{R}^{n_l}b[l]∈Rnl
2.3 前向传播算法
前向传播(Forward Propagation)计算网络输出,是神经网络推理和训练的基础。
符号定义:
| LLL | 网络总层数(含输入输出层) | 标量 |
| n[l]n^{[l]}n[l] | 第 lll 层的神经元数量 | 标量 |
| W[l]W^{[l]}W[l] | 第 lll 层的权重矩阵 | (n[l],n[l−1])(n^{[l]}, n^{[l-1]})(n[l],n[l−1]) |
| b[l]b^{[l]}b[l] | 第 lll 层的偏置向量 | (n[l],1)(n^{[l]}, 1)(n[l],1) |
| z[l]z^{[l]}z[l] | 第 lll 层的线性输出 | (n[l],1)(n^{[l]}, 1)(n[l],1) |
| a[l]a^{[l]}a[l] | 第 lll 层的激活输出 | (n[l],1)(n^{[l]}, 1)(n[l],1) |
| σ[l]\\sigma^{[l]}σ[l] | 第 lll 层的激活函数 | – |
前向传播公式:
对于第 lll 层(l=1,2,…,Ll = 1, 2, …, Ll=1,2,…,L):
z[l]=W[l]a[l−1]+b[l]z^{[l]} = W^{[l]} a^{[l-1]} + b^{[l]}z[l]=W[l]a[l−1]+b[l]
a[l]=σ[l](z[l])a^{[l]} = \\sigma^{[l]}(z^{[l]})a[l]=σ[l](z[l])
其中 a[0]=xa^{[0]} = xa[0]=x 为输入。
向量化的批量计算:
对于 mmm 个样本的批量数据 X∈Rn[0]×mX \\in \\mathbb{R}^{n^{[0]} \\times m}X∈Rn[0]×m:
Z[l]=W[l]A[l−1]+b[l]Z^{[l]} = W^{[l]} A^{[l-1]} + b^{[l]}Z[l]=W[l]A[l−1]+b[l]
A[l]=σ[l](Z[l])A^{[l]} = \\sigma^{[l]}(Z^{[l]})A[l]=σ[l](Z[l])
其中 A[l]∈Rn[l]×mA^{[l]} \\in \\mathbb{R}^{n^{[l]} \\times m}A[l]∈Rn[l]×m,偏置 b[l]b^{[l]}b[l] 通过广播机制自动扩展。
计算图表示:
输入 X → [线性: Z = WX + b] → [激活: A = σ(Z)] → 输出
↑ ↑
权重W 激活函数
偏置b
3. 反向传播算法详解
3.1 问题定义与目标
神经网络的训练目标是最小化损失函数:
minW,b1m∑i=1mL(yi,y^i)\\min_{W, b} \\frac{1}{m} \\sum_{i=1}^{m} \\mathcal{L}(y_i, \\hat{y}_i)W,bminm1i=1∑mL(yi,y^i)
其中 L\\mathcal{L}L 为损失函数,y^i\\hat{y}_iy^i 为网络预测输出。
核心问题:如何高效计算损失函数对所有参数的梯度?
解决方案:反向传播算法(Backpropagation),由Rumelhart、Hinton和Williams于1986年提出。
3.2 链式法则基础
反向传播的核心是微积分中的链式法则(Chain Rule)。
单变量链式法则:
若 y=f(u)y = f(u)y=f(u),u=g(x)u = g(x)u=g(x),则:
dydx=dydu⋅dudx\\frac{dy}{dx} = \\frac{dy}{du} \\cdot \\frac{du}{dx}dxdy=dudy⋅dxdu
多变量链式法则:
若 y=f(u1,u2,…,un)y = f(u_1, u_2, …, u_n)y=f(u1,u2,…,un),ui=gi(x)u_i = g_i(x)ui=gi(x),则:
∂ypartialx=∑i=1n∂y∂ui⋅∂ui∂x\\frac{\\partial y}{partial x} = \\sum_{i=1}^{n} \\frac{\\partial y}{\\partial u_i} \\cdot \\frac{\\partial u_i}{\\partial x}partialx∂y=i=1∑n∂ui∂y⋅∂x∂ui
3.3 反向传播的数学推导
定义第 lll 层的误差项(Error Term):
δ[l]=∂L∂z[l]\\delta^{[l]} = \\frac{\\partial \\mathcal{L}}{\\partial z^{[l]}}δ[l]=∂z[l]∂L
表示损失函数对第 lll 层线性输出的梯度。
输出层的误差计算:
对于输出层 LLL:
δ[L]=∂L∂z[L]=∂L∂a[L]⊙σ′[L](z[L])\\delta^{[L]} = \\frac{\\partial \\mathcal{L}}{\\partial z^{[L]}} = \\frac{\\partial \\mathcal{L}}{\\partial a^{[L]}} \\odot \\sigma'^{[L]}(z^{[L]})δ[L]=∂z[L]∂L=∂a[L]∂L⊙σ′[L](z[L])
其中 ⊙\\odot⊙ 表示逐元素乘法(Hadamard积)。
隐藏层的误差反向传播:
第 lll 层的误差可由第 l+1l+1l+1 层计算得到:
δ[l]=∂L∂z[l]=∂L∂z[l+1]⋅∂z[l+1]∂a[l]⋅∂a[l]∂z[l]\\delta^{[l]} = \\frac{\\partial \\mathcal{L}}{\\partial z^{[l]}} = \\frac{\\partial \\mathcal{L}}{\\partial z^{[l+1]}} \\cdot \\frac{\\partial z^{[l+1]}}{\\partial a^{[l]}} \\cdot \\frac{\\partial a^{[l]}}{\\partial z^{[l]}}δ[l]=∂z[l]∂L=∂z[l+1]∂L⋅∂a[l]∂z[l+1]⋅∂z[l]∂a[l]
=(W[l+1]Tδ[l+1])⊙σ′[l](z[l])= (W^{[l+1]T} \\delta^{[l+1]}) \\odot \\sigma'^{[l]}(z^{[l]})=(W[l+1]Tδ[l+1])⊙σ′[l](z[l])
参数梯度的计算:
得到误差项后,权重和偏置的梯度为:
∂L∂W[l]=δ[l]a[l−1]T\\frac{\\partial \\mathcal{L}}{\\partial W^{[l]}} = \\delta^{[l]} a^{[l-1]T}∂W[l]∂L=δ[l]a[l−1]T
∂L∂b[l]=δ[l]\\frac{\\partial \\mathcal{L}}{\\partial b^{[l]}} = \\delta^{[l]}∂b[l]∂L=δ[l]
完整的反向传播流程:
| 1 | 前向传播 | 计算并保存所有 z[l]z^{[l]}z[l] 和 a[l]a^{[l]}a[l] |
| 2 | 输出层误差 | δ[L]=∇a[L]L⊙σ′[L](z[L])\\delta^{[L]} = \\nabla_{a^{[L]}}\\mathcal{L} \\odot \\sigma'^{[L]}(z^{[L]})δ[L]=∇a[L]L⊙σ′[L](z[L]) |
| 3 | 反向传播误差 | δ[l]=(W[l+1]Tδ[l+1])⊙σ′[l](z[l])\\delta^{[l]} = (W^{[l+1]T} \\delta^{[l+1]}) \\odot \\sigma'^{[l]}(z^{[l]})δ[l]=(W[l+1]Tδ[l+1])⊙σ′[l](z[l]) |
| 4 | 计算梯度 | ∂L∂W[l]=δ[l]a[l−1]T\\frac{\\partial \\mathcal{L}}{\\partial W^{[l]}} = \\delta^{[l]} a^{[l-1]T}∂W[l]∂L=δ[l]a[l−1]T,∂L∂b[l]=δ[l]\\frac{\\partial \\mathcal{L}}{\\partial b^{[l]}} = \\delta^{[l]}∂b[l]∂L=δ[l] |
| 5 | 参数更新 | W[l]←W[l]−η∂L∂W[l]W^{[l]} \\leftarrow W^{[l]} – \\eta \\frac{\\partial \\mathcal{L}}{\\partial W^{[l]}}W[l]←W[l]−η∂W[l]∂L |
3.4 梯度下降与参数更新
批量梯度下降(Batch GD):
使用全部训练数据计算梯度:
W[l]:=W[l]−ηm∑i=1m∂Li∂W[l]W^{[l]} := W^{[l]} – \\frac{\\eta}{m} \\sum_{i=1}^{m} \\frac{\\partial \\mathcal{L}_i}{\\partial W^{[l]}}W[l]:=W[l]−mηi=1∑m∂W[l]∂Li
随机梯度下降(SGD):
每次使用单个样本:
W[l]:=W[l]−η∂Li∂W[l]W^{[l]} := W^{[l]} – \\eta \\frac{\\partial \\mathcal{L}_i}{\\partial W^{[l]}}W[l]:=W[l]−η∂W[l]∂Li
小批量梯度下降(Mini-batch GD):
使用 bbb 个样本的批量:
W[l]:=W[l]−ηb∑i=1b∂Li∂W[l]W^{[l]} := W^{[l]} – \\frac{\\eta}{b} \\sum_{i=1}^{b} \\frac{\\partial \\mathcal{L}_i}{\\partial W^{[l]}}W[l]:=W[l]−bηi=1∑b∂W[l]∂Li
这是实践中最常用的方法,兼顾计算效率和收敛稳定性。
4. 激活函数演进
激活函数引入非线性,是神经网络能够学习复杂模式的关键。不同激活函数有不同的特性,选择合适的激活函数对网络性能至关重要。
4.1 Sigmoid函数
定义:
σ(x)=11+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),可解释为概率 | 两端饱和,梯度趋近于0 |
| 平滑可导 | 输出非零中心化 |
| – | 计算涉及指数,开销较大 |
梯度消失问题:
当 ∣x∣|x|∣x∣ 较大时,σ′(x)≈0\\sigma'(x) \\approx 0σ′(x)≈0,导致深层网络梯度消失,参数无法更新。
4.2 Tanh函数
定义:
tanh(x)=ex−e−xex+e−x=2σ(2x)−1\\tanh(x) = \\frac{e^x – e^{-x}}{e^x + e^{-x}} = 2\\sigma(2x) – 1tanh(x)=ex+e−xex−e−x=2σ(2x)−1
导数:
tanh′(x)=1−tanh2(x)\\tanh'(x) = 1 – \\tanh^2(x)tanh′(x)=1−tanh2(x)
特点:
- 输出范围(-1,1),零中心化
- 相比Sigmoid,梯度更强(最大值为1)
- 仍存在饱和问题
4.3 ReLU函数
定义:
ReLU(x)=max(0,x)\\text{ReLU}(x) = \\max(0, x)ReLU(x)=max(0,x)
导数:
ReLU′(x)={1x>00x<0\\text{ReLU}'(x) = \\begin{cases} 1 & x > 0 \\\\ 0 & x < 0 \\end{cases}ReLU′(x)={10x>0x<0
特点:
| 计算简单,无指数运算 | 负值区域神经元"死亡" |
| 缓解梯度消失问题 | 输出非零中心化 |
| 收敛速度快 | – |
神经元死亡问题:
当输入为负时,梯度为0,神经元不再更新。如果大量神经元死亡,网络表达能力下降。
4.4 Leaky ReLU与PReLU
Leaky ReLU:
LeakyReLU(x)=max(αx,x)\\text{LeakyReLU}(x) = \\max(\\alpha x, x)LeakyReLU(x)=max(αx,x)
其中 α\\alphaα 为小的正数(通常0.01),允许负值区域有小的梯度。
PReLU(Parametric ReLU):
将 α\\alphaα 设为可学习参数,让网络自适应调整。
4.5 Swish函数
2017年,Google Brain通过自动搜索发现Swish激活函数。
定义:
Swish(x)=x⋅σ(x)=x1+e−x\\text{Swish}(x) = x \\cdot \\sigma(x) = \\frac{x}{1 + e^{-x}}Swish(x)=x⋅σ(x)=1+e−xx
特点:
- 自门控机制:输入自身作为门控信号
- 在负值区域有非单调性
- 在深层网络中表现优于ReLU
- 计算开销略高于ReLU
4.6 GELU函数
GELU(Gaussian Error Linear Unit)是BERT、GPT等Transformer模型的默认激活函数。
定义:
GELU(x)=x⋅Φ(x)=x⋅12[1+erf(x2)]\\text{GELU}(x) = x \\cdot \\Phi(x) = x \\cdot \\frac{1}{2}\\left[1 + \\text{erf}\\left(\\frac{x}{\\sqrt{2}}\\right)\\right]GELU(x)=x⋅Φ(x)=x⋅21[1+erf(2x)]
其中 Φ(x)\\Phi(x)Φ(x) 为标准正态分布的累积分布函数。
近似计算:
GELU(x)≈0.5x(1+tanh[2π(x+0.044715×3)])\\text{GELU}(x) \\approx 0.5x\\left(1 + \\tanh\\left[\\sqrt{\\frac{2}{\\pi}}\\left(x + 0.044715x^3\\right)\\right]\\right)GELU(x)≈0.5x(1+tanh[π2(x+0.044715x3)])
特点:
- 平滑非单调,处处可导
- 在Transformer架构中表现优异
- 计算复杂度较高
4.7 激活函数选择指南
| 隐藏层(通用) | ReLU / Leaky ReLU | 计算高效,缓解梯度消失 |
| 深层CNN | ReLU / Swish | 收敛快,性能好 |
| Transformer | GELU | 与自注意力机制配合好 |
| 输出层(二分类) | Sigmoid | 输出概率解释 |
| 输出层(多分类) | Softmax | 多类概率归一化 |
| 输出层(回归) | 线性(无激活) | 无界输出 |
5. 权重初始化策略
权重初始化对神经网络的训练至关重要。不良的初始化可能导致梯度消失、梯度爆炸或对称性问题。
5.1 零初始化与随机初始化
零初始化:
所有权重设为0。导致所有神经元计算相同输出,梯度相同,无法学习不同特征。
随机初始化:
从均匀分布或正态分布采样:
Wij∼U(−ϵ,ϵ)或Wij∼N(0,σ2)W_{ij} \\sim \\mathcal{U}(-\\epsilon, \\epsilon) \\quad \\text{或} \\quad W_{ij} \\sim \\mathcal{N}(0, \\sigma^2)Wij∼U(−ϵ,ϵ)或Wij∼N(0,σ2)
问题:当网络较深时,方差可能逐层累积或衰减。
5.2 Xavier初始化
2010年,Xavier Glorot和Yoshua Bengio提出Xavier初始化(也称Glorot初始化)。
核心思想:
保持每层输入和输出的方差一致,防止信号在前向传播中爆炸或消失。
数学推导:
假设:
- 权重独立同分布,均值为0
- 输入与权重独立
- 激活函数在0附近近似线性
对于第 lll 层:
z[l]=∑j=1n[l−1]wj[l]aj[l−1]z^{[l]} = \\sum_{j=1}^{n^{[l-1]}} w_j^{[l]} a_j^{[l-1]}z[l]=j=1∑n[l−1]wj[l]aj[l−1]
方差:
Var(z[l])=n[l−1]⋅Var(w[l])⋅Var(a[l−1])\\text{Var}(z^{[l]}) = n^{[l-1]} \\cdot \\text{Var}(w^{[l]}) \\cdot \\text{Var}(a^{[l-1]})Var(z[l])=n[l−1]⋅Var(w[l])⋅Var(a[l−1])
为使 Var(z[l])=Var(a[l−1])\\text{Var}(z^{[l]}) = \\text{Var}(a^{[l-1]})Var(z[l])=Var(a[l−1]),需要:
Var(w[l])=1n[l−1]\\text{Var}(w^{[l]}) = \\frac{1}{n^{[l-1]}}Var(w[l])=n[l−1]1
同时考虑反向传播,取平均:
Var(w[l])=2n[l−1]+n[l]\\text{Var}(w^{[l]}) = \\frac{2}{n^{[l-1]} + n^{[l]}}Var(w[l])=n[l−1]+n[l]2
Xavier初始化公式:
W[l]∼U(−6n[l−1]+n[l],6n[l−1]+n[l])W^{[l]} \\sim \\mathcal{U}\\left(-\\sqrt{\\frac{6}{n^{[l-1]} + n^{[l]}}}, \\sqrt{\\frac{6}{n^{[l-1]} + n^{[l]}}}\\right)W[l]∼U(−n[l−1]+n[l]6,n[l−1]+n[l]6)
或正态分布:
W[l]∼N(0,2n[l−1]+n[l])W^{[l]} \\sim \\mathcal{N}\\left(0, \\frac{2}{n^{[l-1]} + n^{[l]}}\\right)W[l]∼N(0,n[l−1]+n[l]2)
适用场景:
- 配合Sigmoid或Tanh激活函数
- 全连接层和卷积层
5.3 He初始化
2015年,Kaiming He等人针对ReLU激活函数提出He初始化。
核心思想:
ReLU将负值置为0,导致输出方差减半。需要调整初始化方差进行补偿。
数学推导:
对于ReLU,约一半的神经元输出为0,因此:
Var(a[l])=12n[l−1]Var(w[l])Var(a[l−1])\\text{Var}(a^{[l]}) = \\frac{1}{2} n^{[l-1]} \\text{Var}(w^{[l]}) \\text{Var}(a^{[l-1]})Var(a[l])=21n[l−1]Var(w[l])Var(a[l−1])
为使方差保持一致:
Var(w[l])=2n[l−1]\\text{Var}(w^{[l]}) = \\frac{2}{n^{[l-1]}}Var(w[l])=n[l−1]2
He初始化公式:
W[l]∼N(0,2n[l−1])W^{[l]} \\sim \\mathcal{N}\\left(0, \\frac{2}{n^{[l-1]}}\\right)W[l]∼N(0,n[l−1]2)
或均匀分布:
W[l]∼U(−6n[l−1],6n[l−1])W^{[l]} \\sim \\mathcal{U}\\left(-\\sqrt{\\frac{6}{n^{[l-1]}}}, \\sqrt{\\frac{6}{n^{[l-1]}}}\\right)W[l]∼U(−n[l−1]6,n[l−1]6)
适用场景:
- 配合ReLU及其变体(Leaky ReLU、PReLU)
- 现代深度网络的标准初始化方法
5.4 正交初始化
核心思想:
使用正交矩阵初始化权重,保持范数在传播过程中不变。
方法:
优势:
- 防止梯度消失和爆炸
- 适用于RNN等循环网络
5.5 初始化方法选择
| Sigmoid / Tanh | Xavier初始化 |
| ReLU / Leaky ReLU | He初始化 |
| 线性激活 | Xavier初始化 |
| 深层网络 | He初始化或正交初始化 |
6. 损失函数设计
损失函数衡量预测值与真实值的差异,指导网络参数更新。
6.1 均方误差(MSE)
定义:
LMSE=1m∑i=1m(yi−y^i)2\\mathcal{L}_{\\text{MSE}} = \\frac{1}{m} \\sum_{i=1}^{m} (y_i – \\hat{y}_i)^2LMSE=m1i=1∑m(yi−y^i)2
适用场景:
- 回归任务
- 输出为连续值
梯度:
∂L∂y^i=−2m(yi−y^i)\\frac{\\partial \\mathcal{L}}{\\partial \\hat{y}_i} = -\\frac{2}{m}(y_i – \\hat{y}_i)∂y^i∂L=−m2(yi−y^i)
6.2 交叉熵损失
二分类交叉熵:
LBCE=−1m∑i=1m[yilog(y^i)+(1−yi)log(1−y^i)]\\mathcal{L}_{\\text{BCE}} = -\\frac{1}{m} \\sum_{i=1}^{m} \\left[y_i \\log(\\hat{y}_i) + (1-y_i)\\log(1-\\hat{y}_i)\\right]LBCE=−m1i=1∑m[yilog(y^i)+(1−yi)log(1−y^i)]
多分类交叉熵(Softmax Loss):
LCE=−1m∑i=1m∑k=1Kyiklog(y^ik)\\mathcal{L}_{\\text{CE}} = -\\frac{1}{m} \\sum_{i=1}^{m} \\sum_{k=1}^{K} y_{ik} \\log(\\hat{y}_{ik})LCE=−m1i=1∑mk=1∑Kyiklog(y^ik)
其中 y^ik=ezik∑jezij\\hat{y}_{ik} = \\frac{e^{z_{ik}}}{\\sum_{j}e^{z_{ij}}}y^ik=∑jezijezik 为Softmax输出。
适用场景:
- 分类任务
- 与Sigmoid/Softmax输出层配合
优势:
- 梯度形式简洁,收敛快
- 对错误预测惩罚更大
6.3 Hinge Loss
定义:
Lhinge=1m∑i=1mmax(0,1−yi⋅y^i)\\mathcal{L}_{\\text{hinge}} = \\frac{1}{m} \\sum_{i=1}^{m} \\max(0, 1 – y_i \\cdot \\hat{y}_i)Lhinge=m1i=1∑mmax(0,1−yi⋅y^i)
其中 yi∈{−1,+1}y_i \\in \\{-1, +1\\}yi∈{−1,+1}。
适用场景:
- 支持向量机
- 最大间隔分类
6.4 损失函数选择指南
| 回归 | 线性 | MSE / MAE |
| 二分类 | Sigmoid | 二分类交叉熵 |
| 多分类 | Softmax | 多分类交叉熵 |
| 多标签分类 | Sigmoid | 二分类交叉熵(每个标签独立) |
7. 实战案例:使用NumPy从零实现神经网络进行手写数字分类
7.1 项目概述
本实战案例使用纯NumPy实现一个多层感知机,完成MNIST手写数字分类任务。通过从零实现,深入理解神经网络的每个细节。
7.2 完整代码实现
"""
使用NumPy从零实现神经网络
任务:MNIST手写数字分类
"""
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import accuracy_score, confusion_matrix
import warnings
warnings.filterwarnings('ignore')
# 设置随机种子保证可复现
np.random.seed(42)
class NeuralNetwork:
"""
多层感知机神经网络
支持任意层数和每层的神经元数量
"""
def __init__(self, layer_dims, activation='relu', initialization='he'):
"""
初始化神经网络
参数:
layer_dims: 列表,包含每层神经元数量 [输入层, 隐藏层1, …, 输出层]
activation: 隐藏层激活函数,可选 'relu', 'tanh', 'sigmoid'
initialization: 权重初始化方法,可选 'he', 'xavier', 'random'
"""
self.layer_dims = layer_dims
self.num_layers = len(layer_dims) – 1 # 不包含输入层
self.activation = activation
self.initialization = initialization
self.parameters = {}
self.caches = {}
# 初始化权重和偏置
self._initialize_parameters()
def _initialize_parameters(self):
"""
初始化网络参数(权重和偏置)
"""
for l in range(1, self.num_layers + 1):
n_prev = self.layer_dims[l–1] # 前一层神经元数量
n_curr = self.layer_dims[l] # 当前层神经元数量
if self.initialization == 'he':
# He初始化:适用于ReLU
std = np.sqrt(2.0 / n_prev)
self.parameters[f'W{l}'] = np.random.randn(n_curr, n_prev) * std
elif self.initialization == 'xavier':
# Xavier初始化:适用于Sigmoid/Tanh
limit = np.sqrt(6.0 / (n_prev + n_curr))
self.parameters[f'W{l}'] = np.random.uniform(–limit, limit,
(n_curr, n_prev))
else: # random
self.parameters[f'W{l}'] = np.random.randn(n_curr, n_prev) * 0.01
# 偏置初始化为0
self.parameters[f'b{l}'] = np.zeros((n_curr, 1))
def _relu(self, Z):
"""ReLU激活函数"""
return np.maximum(0, Z)
def _relu_derivative(self, Z):
"""ReLU导数"""
return (Z > 0).astype(float)
def _sigmoid(self, Z):
"""Sigmoid激活函数"""
return 1 / (1 + np.exp(–Z))
def _sigmoid_derivative(self, Z):
"""Sigmoid导数"""
s = self._sigmoid(Z)
return s * (1 – s)
def _tanh(self, Z):
"""Tanh激活函数"""
return np.tanh(Z)
def _tanh_derivative(self, Z):
"""Tanh导数"""
return 1 – np.tanh(Z) ** 2
def _softmax(self, Z):
"""
Softmax激活函数(用于多分类输出层)
使用数值稳定性技巧
"""
exp_Z = np.exp(Z – np.max(Z, axis=0, keepdims=True))
return exp_Z / np.sum(exp_Z, axis=0, keepdims=True)
def _activation_forward(self, Z, activation_type):
"""
前向传播激活函数
"""
if activation_type == 'relu':
return self._relu(Z)
elif activation_type == 'sigmoid':
return self._sigmoid(Z)
elif activation_type == 'tanh':
return self._tanh(Z)
elif activation_type == 'softmax':
return self._softmax(Z)
else:
raise ValueError(f"未知的激活函数: {activation_type}")
def _activation_backward(self, dA, Z, activation_type):
"""
反向传播激活函数导数
"""
if activation_type == 'relu':
return dA * self._relu_derivative(Z)
elif activation_type == 'sigmoid':
return dA * self._sigmoid_derivative(Z)
elif activation_type == 'tanh':
return dA * self._tanh_derivative(Z)
elif activation_type == 'softmax':
# Softmax + CrossEntropy 的梯度简化为 (A – Y)
return dA
else:
raise ValueError(f"未知的激活函数: {activation_type}")
def forward_propagation(self, X):
"""
前向传播
参数:
X: 输入数据,形状为 (n_features, n_samples)
返回:
AL: 输出层激活值
caches: 缓存中间结果用于反向传播
"""
caches = {}
A = X
caches['A0'] = X
# 隐藏层前向传播
for l in range(1, self.num_layers):
W = self.parameters[f'W{l}']
b = self.parameters[f'b{l}']
# 线性部分: Z = W * A + b
Z = np.dot(W, A) + b
caches[f'Z{l}'] = Z
# 激活部分: A = activation(Z)
A = self._activation_forward(Z, self.activation)
caches[f'A{l}'] = A
# 输出层(使用Softmax)
W = self.parameters[f'W{self.num_layers}']
b = self.parameters[f'b{self.num_layers}']
Z = np.dot(W, A) + b
caches[f'Z{self.num_layers}'] = Z
AL = self._activation_forward(Z, 'softmax')
caches[f'A{self.num_layers}'] = AL
return AL, caches
def compute_cost(self, AL, Y):
"""
计算交叉熵损失
参数:
AL: 模型输出,形状为 (n_classes, n_samples)
Y: 真实标签(one-hot编码),形状为 (n_classes, n_samples)
返回:
cost: 标量损失值
"""
m = Y.shape[1]
# 添加小常数防止log(0)
epsilon = 1e-8
AL = np.clip(AL, epsilon, 1 – epsilon)
# 交叉熵损失
cost = –np.sum(Y * np.log(AL)) / m
return cost
def backward_propagation(self, AL, Y, caches):
"""
反向传播
参数:
AL: 输出层激活值
Y: 真实标签
caches: 前向传播的缓存
返回:
grads: 各层参数的梯度
"""
grads = {}
m = Y.shape[1]
L = self.num_layers
# 输出层误差(Softmax + CrossEntropy的组合梯度)
dZL = AL – Y
# 输出层梯度
A_prev = caches[f'A{L–1}']
grads[f'dW{L}'] = np.dot(dZL, A_prev.T) / m
grads[f'db{L}'] = np.sum(dZL, axis=1, keepdims=True) / m
# 反向传播到隐藏层
dA = np.dot(self.parameters[f'W{L}'].T, dZL)
for l in range(L–1, 0, –1):
Z = caches[f'Z{l}']
# 通过激活函数反向传播
dZ = self._activation_backward(dA, Z, self.activation)
# 计算梯度
A_prev = caches[f'A{l–1}']
grads[f'dW{l}'] = np.dot(dZ, A_prev.T) / m
grads[f'db{l}'] = np.sum(dZ, axis=1, keepdims=True) / m
# 传播到前一层
if l > 1:
dA = np.dot(self.parameters[f'W{l}'].T, dZ)
return grads
def update_parameters(self, grads, learning_rate):
"""
使用梯度下降更新参数
参数:
grads: 参数梯度
learning_rate: 学习率
"""
for l in range(1, self.num_layers + 1):
self.parameters[f'W{l}'] -= learning_rate * grads[f'dW{l}']
self.parameters[f'b{l}'] -= learning_rate * grads[f'db{l}']
def train(self, X, Y, epochs=100, learning_rate=0.1, batch_size=64,
X_val=None, Y_val=None, verbose=True):
"""
训练神经网络
参数:
X: 训练数据
Y: 训练标签(one-hot编码)
epochs: 训练轮数
learning_rate: 学习率
batch_size: 批量大小
X_val: 验证数据(可选)
Y_val: 验证标签(可选)
verbose: 是否打印训练进度
返回:
history: 训练历史记录
"""
m = X.shape[1]
num_batches = m // batch_size
history = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
for epoch in range(epochs):
# 随机打乱数据
permutation = np.random.permutation(m)
X_shuffled = X[:, permutation]
Y_shuffled = Y[:, permutation]
epoch_loss = 0
for i in range(num_batches):
# 获取批量数据
start = i * batch_size
end = start + batch_size
X_batch = X_shuffled[:, start:end]
Y_batch = Y_shuffled[:, start:end]
# 前向传播
AL, caches = self.forward_propagation(X_batch)
# 计算损失
batch_loss = self.compute_cost(AL, Y_batch)
epoch_loss += batch_loss
# 反向传播
grads = self.backward_propagation(AL, Y_batch, caches)
# 更新参数
self.update_parameters(grads, learning_rate)
# 计算训练集指标
AL_train, _ = self.forward_propagation(X)
train_loss = self.compute_cost(AL_train, Y)
train_acc = self.compute_accuracy(X, Y)
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
# 计算验证集指标
if X_val is not None and Y_val is not None:
AL_val, _ = self.forward_propagation(X_val)
val_loss = self.compute_cost(AL_val, Y_val)
val_acc = self.compute_accuracy(X_val, Y_val)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# 打印进度
if verbose and (epoch % 10 == 0 or epoch == epochs – 1):
if X_val is not None:
print(f"Epoch {epoch:3d}/{epochs} – "
f"loss: {train_loss:.4f} – acc: {train_acc:.4f} – "
f"val_loss: {val_loss:.4f} – val_acc: {val_acc:.4f}")
else:
print(f"Epoch {epoch:3d}/{epochs} – "
f"loss: {train_loss:.4f} – acc: {train_acc:.4f}")
return history
def predict(self, X):
"""
预测类别
参数:
X: 输入数据
返回:
predictions: 预测的类别索引
"""
AL, _ = self.forward_propagation(X)
predictions = np.argmax(AL, axis=0)
return predictions
def compute_accuracy(self, X, Y):
"""
计算准确率
参数:
X: 输入数据
Y: 真实标签(one-hot编码)
返回:
accuracy: 准确率
"""
predictions = self.predict(X)
true_labels = np.argmax(Y, axis=0)
accuracy = np.mean(predictions == true_labels)
return accuracy
def load_and_preprocess_data(n_samples=10000):
"""
加载并预处理MNIST数据集
参数:
n_samples: 使用的样本数量(用于快速测试)
返回:
预处理后的训练和测试数据
"""
print("正在加载MNIST数据集…")
# 加载数据
mnist = fetch_openml('mnist_784', version=1, parser='auto')
X = mnist.data.values if hasattr(mnist.data, 'values') else mnist.data
y = mnist.target.values if hasattr(mnist.target, 'values') else mnist.target
# 限制样本数量(可选,用于快速测试)
if n_samples < len(X):
indices = np.random.choice(len(X), n_samples, replace=False)
X = X[indices]
y = y[indices]
# 数据归一化到[0, 1]
X = X.astype('float32') / 255.0
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# One-hot编码标签
encoder = OneHotEncoder(sparse_output=False)
y_train_onehot = encoder.fit_transform(y_train.reshape(–1, 1)).T
y_test_onehot = encoder.transform(y_test.reshape(–1, 1)).T
# 转置数据以适应神经网络输入 (n_features, n_samples)
X_train = X_train.T
X_test = X_test.T
print(f"训练集大小: {X_train.shape[1]}")
print(f"测试集大小: {X_test.shape[1]}")
print(f"特征维度: {X_train.shape[0]}")
print(f"类别数量: {y_train_onehot.shape[0]}")
return X_train, X_test, y_train_onehot, y_test_onehot, y_test
def plot_training_history(history):
"""
绘制训练历史曲线
"""
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 损失曲线
axes[0].plot(history['train_loss'], label='Train Loss')
if history['val_loss']:
axes[0].plot(history['val_loss'], label='Val Loss')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('Training Loss')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 准确率曲线
axes[1].plot(history['train_acc'], label='Train Acc')
if history['val_acc']:
axes[1].plot(history['val_acc'], label='Val Acc')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy')
axes[1].set_title('Training Accuracy')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('training_history.png', dpi=150)
plt.show()
def visualize_predictions(X, y_true, y_pred, n_samples=10):
"""
可视化预测结果
"""
fig, axes = plt.subplots(2, 5, figsize=(12, 6))
axes = axes.flatten()
# 随机选择样本
indices = np.random.choice(X.shape[1], n_samples, replace=False)
for i, idx in enumerate(indices):
# 重塑图像 (28×28)
img = X[:, idx].reshape(28, 28)
axes[i].imshow(img, cmap='gray')
color = 'green' if y_pred[idx] == y_true[idx] else 'red'
axes[i].set_title(f'True: {y_true[idx]}, Pred: {y_pred[idx]}', color=color)
axes[i].axis('off')
plt.tight_layout()
plt.savefig('predictions.png', dpi=150)
plt.show()
# 主程序
if __name__ == "__main__":
# 加载数据
X_train, X_test, y_train, y_test_onehot, y_test = load_and_preprocess_data(
n_samples=10000
)
# 进一步划分验证集
val_size = int(0.1 * X_train.shape[1])
X_val = X_train[:, –val_size:]
y_val = y_train[:, –val_size:]
X_train = X_train[:, :–val_size]
y_train = y_train[:, :–val_size]
print(f"最终训练集: {X_train.shape[1]} 样本")
print(f"验证集: {X_val.shape[1]} 样本")
# 创建神经网络
# 输入层: 784 (28×28像素)
# 隐藏层1: 256
# 隐藏层2: 128
# 输出层: 10 (0-9数字)
layer_dims = [784, 256, 128, 10]
print(f"\\n网络结构: {layer_dims}")
print(f"总参数数量: {sum(layer_dims[i] * layer_dims[i+1] + layer_dims[i+1] for i in range(len(layer_dims)–1))}")
nn = NeuralNetwork(
layer_dims=layer_dims,
activation='relu',
initialization='he'
)
# 训练网络
print("\\n开始训练…")
history = nn.train(
X_train, y_train,
epochs=50,
learning_rate=0.1,
batch_size=128,
X_val=X_val,
Y_val=y_val,
verbose=True
)
# 绘制训练曲线
plot_training_history(history)
# 测试集评估
print("\\n测试集评估:")
test_acc = nn.compute_accuracy(X_test, y_test_onehot)
print(f"测试准确率: {test_acc:.4f}")
# 可视化预测
y_pred = nn.predict(X_test)
y_true = np.argmax(y_test_onehot, axis=0)
visualize_predictions(X_test, y_true, y_pred)
# 混淆矩阵
cm = confusion_matrix(y_true, y_pred)
print("\\n混淆矩阵:")
print(cm)
7.3 代码解析
网络架构设计:
| 输入层 | 784 | – | 28×28图像展平 |
| 隐藏层1 | 256 | ReLU | 特征提取 |
| 隐藏层2 | 128 | ReLU | 特征提取 |
| 输出层 | 10 | Softmax | 10类分类 |
关键实现细节:
训练超参数:
| 学习率 | 0.1 | 较大学习率加速收敛 |
| 批量大小 | 128 | 平衡计算效率和稳定性 |
| 训练轮数 | 50 | 足够收敛 |
| 优化器 | SGD | 基础梯度下降 |
7.4 运行结果预期
在MNIST数据集上(10000样本),该网络预期达到:
- 训练准确率:约95-98%
- 验证准确率:约92-95%
- 测试准确率:约92-95%
8. 避坑小贴士
8.1 梯度消失与梯度爆炸
问题表现:
- 梯度消失:深层网络参数几乎不更新,损失停滞
- 梯度爆炸:损失突然变为NaN或急剧增大
解决方案:
8.2 学习率设置
学习率过大:损失震荡不收敛
学习率过小:收敛极慢,陷入局部最优
建议:
- 从0.1或0.01开始尝试
- 使用学习率衰减策略
- 监控训练和验证损失曲线
8.3 过拟合问题
表现:训练准确率高,验证准确率低
解决方案:
8.4 数据预处理
必须进行的预处理:
8.5 调试技巧
9. 本章小结
本章从感知机出发,逐步深入到多层感知机的核心原理,构建了完整的神经网络理论基础。
核心知识点回顾:
| 感知机 | 生物启发、数学模型、学习算法、异或局限 |
| MLP结构 | 输入层、隐藏层、输出层、前向传播 |
| 反向传播 | 链式法则、误差反向传播、参数更新 |
| 激活函数 | Sigmoid、Tanh、ReLU、Swish、GELU演进 |
| 权重初始化 | Xavier、He初始化的数学原理 |
| 损失函数 | MSE、交叉熵、Hinge Loss的适用场景 |
一句话总结:
神经网络通过多层非线性变换提取特征,反向传播算法高效计算梯度,合理的激活函数和权重初始化是训练成功的关键。
下一步学习建议:
如果本文对你有帮助,欢迎点赞、收藏、评论交流!持续关注我,获取更多机器学习深度教程。
标签:python 机器学习 深度学习 神经网络 反向传播

