"人脑有大约 860 亿个神经元,而现代神经网络可能只有几百万个参数。
但我们已经能用它识别图像、翻译语言、生成文本------
这不是因为网络像大脑,而是因为我们找到了一种通用的函数逼近方式。"
------深度学习的工程哲学
一、为什么需要神经网络?
在前六章中,我们系统学习了线性模型 、决策树 、集成方法(随机森林、XGBoost) 等经典机器学习算法。它们在结构化数据(表格数据)上表现卓越,具备良好的可解释性和工程稳定性。
然而,当我们面对以下任务时,传统方法开始力不从心:
- 图像识别:像素之间存在复杂的局部与全局空间关系;
- 语音识别:音频信号是高维、时序、非平稳的;
- 自然语言处理:词语组合具有高度上下文依赖性;
- 高维非线性模式:特征交互呈指数级增长(如"用户点击 A 且未购买 B 且浏览 C")。
这些任务的共同特点是:输入维度极高、特征间存在深层非线性交互、且难以通过人工特征工程有效表达。
🎯 本章目标:
- 理解神经网络如何通过"层叠非线性变换"实现通用函数逼近;
- 掌握前向传播、反向传播、梯度下降的核心机制;
- 从零实现一个全连接神经网络(MLP);
- 深入探讨激活函数、损失函数、优化器的选择逻辑;
- 辩证看待神经网络的"黑箱"属性,探索可解释性工具;
- 对比神经网络与传统模型的适用边界。
二、神经网络的直觉:从感知机到多层网络
2.1 感知机:神经网络的起点
1957 年,Frank Rosenblatt 提出感知机(Perceptron),这是最早的神经网络模型。
对于输入 x = [ x 1 , x 2 , . . . , x n ] ⊤ \mathbf{x} = [x_1, x_2, ..., x_n]^\top x=[x1,x2,...,xn]⊤,感知机计算:
z = w ⊤ x + b = ∑ i = 1 n w i x i + b z = \mathbf{w}^\top \mathbf{x} + b = \sum_{i=1}^{n} w_i x_i + b z=w⊤x+b=i=1∑nwixi+b
然后通过阶跃函数输出:
y ^ = { 1 , if z ≥ 0 0 , otherwise \hat{y} = \begin{cases} 1, & \text{if } z \geq 0 \\ 0, & \text{otherwise} \end{cases} y^={1,0,if z≥0otherwise
这本质上是一个线性分类器------它只能解决线性可分问题(如 AND、OR),但无法解决 XOR(异或)问题。
❌ 局限性:单层感知机无法表示非线性决策边界。
2.2 多层感知机(MLP):引入隐藏层
为突破线性限制,我们在输入和输出之间加入隐藏层(Hidden Layer):
输入层 → 隐藏层 → 输出层
每一层由多个神经元(Neuron)组成,每个神经元执行:
a j ( l ) = σ ( ∑ k w j k ( l ) a k ( l − 1 ) + b j ( l ) ) a^{(l)}j = \sigma \left( \sum{k} w^{(l)}_{jk} a^{(l-1)}_k + b^{(l)}_j \right) aj(l)=σ(k∑wjk(l)ak(l−1)+bj(l))
其中:
- a j ( l ) a^{(l)}_j aj(l):第 l l l 层第 j j j 个神经元的激活值(activation);
- w j k ( l ) w^{(l)}_{jk} wjk(l):从第 l − 1 l-1 l−1 层第 k k k 个神经元到第 l l l 层第 j j j 个神经元的权重;
- b j ( l ) b^{(l)}_j bj(l):第 l l l 层第 j j j 个神经元的偏置;
- σ ( ⋅ ) \sigma(\cdot) σ(⋅):激活函数(非线性函数)。
🔑 关键洞见 :
只要激活函数是非线性的,多层网络就能逼近任意连续函数(Universal Approximation Theorem)。
2.3 通用逼近定理(Universal Approximation Theorem)
1989 年,George Cybenko 证明:
对于任意连续函数 f : R n → R m f: \mathbb{R}^n \to \mathbb{R}^m f:Rn→Rm,在紧集上,存在一个单隐藏层的前馈神经网络,使用 Sigmoid 激活函数,可以以任意精度逼近 f f f。
这意味着:神经网络不是某种神秘的"智能",而是一种极其灵活的函数拟合器。
✅ 它的强大不在于"像人脑",而在于通过组合简单非线性单元,构建复杂映射。
三、数学基础:前向传播与损失函数
3.1 前向传播(Forward Propagation)
设网络有 L L L 层(含输入和输出),第 l l l 层有 n l n_l nl 个神经元。
定义:
- A ( 0 ) = X ∈ R m × n 0 \mathbf{A}^{(0)} = \mathbf{X} \in \mathbb{R}^{m \times n_0} A(0)=X∈Rm×n0:输入矩阵( m m m 个样本, n 0 n_0 n0 个特征);
- W ( l ) ∈ R n l − 1 × n l \mathbf{W}^{(l)} \in \mathbb{R}^{n_{l-1} \times n_l} W(l)∈Rnl−1×nl:第 l l l 层权重矩阵;
- b ( l ) ∈ R 1 × n l \mathbf{b}^{(l)} \in \mathbb{R}^{1 \times n_l} b(l)∈R1×nl:偏置向量(广播);
- Z ( l ) = A ( l − 1 ) W ( l ) + b ( l ) \mathbf{Z}^{(l)} = \mathbf{A}^{(l-1)} \mathbf{W}^{(l)} + \mathbf{b}^{(l)} Z(l)=A(l−1)W(l)+b(l):线性变换;
- A ( l ) = σ ( l ) ( Z ( l ) ) \mathbf{A}^{(l)} = \sigma^{(l)}(\mathbf{Z}^{(l)}) A(l)=σ(l)(Z(l)):激活输出。
最终输出为 A ( L ) \mathbf{A}^{(L)} A(L)。
💡 向量化计算使得一次前向传播可处理整个批量(batch),极大提升效率。
3.2 激活函数:引入非线性
若没有激活函数,多层网络等价于单层线性变换(因为矩阵乘法满足结合律)。
常用激活函数:
| 函数 | 公式 | 特点 |
|---|---|---|
| Sigmoid | σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{-z}} σ(z)=1+e−z1 | 输出 ( 0 , 1 ) (0,1) (0,1),适合概率;但易饱和、梯度消失 |
| Tanh | tanh ( z ) = e z − e − z e z + e − z \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} tanh(z)=ez+e−zez−e−z | 输出 ( − 1 , 1 ) (-1,1) (−1,1),零中心;仍会饱和 |
| ReLU | ReLU ( z ) = max ( 0 , z ) \text{ReLU}(z) = \max(0, z) ReLU(z)=max(0,z) | 最常用;计算快、缓解梯度消失;但有"死神经元"问题 |
| Leaky ReLU | LReLU ( z ) = max ( 0.01 z , z ) \text{LReLU}(z) = \max(0.01z, z) LReLU(z)=max(0.01z,z) | 解决死神经元 |
| 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 | 多分类输出层专用,保证概率和为1 |
✅ 现代实践 :隐藏层用 ReLU 或其变体,输出层根据任务选择(Sigmoid/Softmax)。
3.3 损失函数:衡量预测误差
损失函数需与任务匹配:
回归任务:均方误差(MSE)
L MSE = 1 m ∑ i = 1 m ( y ^ i − y i ) 2 \mathcal{L}{\text{MSE}} = \frac{1}{m} \sum{i=1}^{m} (\hat{y}_i - y_i)^2 LMSE=m1i=1∑m(y^i−yi)2
二分类:二元交叉熵(Binary Cross-Entropy)
L BCE = − 1 m ∑ i = 1 m [ y i log ( y ^ i ) + ( 1 − y i ) 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)]
其中 y ^ i = σ ( z i ) ∈ ( 0 , 1 ) \hat{y}_i = \sigma(z_i) \in (0,1) y^i=σ(zi)∈(0,1)。
多分类:分类交叉熵(Categorical Cross-Entropy)
L CE = − 1 m ∑ i = 1 m ∑ k = 1 K y i k log ( y ^ i k ) \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 i \mathbf{y}_i yi 是 one-hot 标签, y ^ i = softmax ( z i ) \hat{\mathbf{y}}_i = \text{softmax}(\mathbf{z}_i) y^i=softmax(zi)。
⚠️ 注意:不要对 Softmax 输出再加 Sigmoid!Softmax 已确保输出为概率分布。
四、核心算法:反向传播(Backpropagation)
反向传播是神经网络训练的基石,它高效地计算损失函数对所有参数的梯度。
4.1 链式法则(Chain Rule)回顾
设 L = f ( g ( h ( x ) ) ) L = f(g(h(x))) L=f(g(h(x))),则:
d L d x = d L d g ⋅ d g d h ⋅ d h d x \frac{dL}{dx} = \frac{dL}{dg} \cdot \frac{dg}{dh} \cdot \frac{dh}{dx} dxdL=dgdL⋅dhdg⋅dxdh
神经网络的损失是参数的复合函数,链式法则是计算梯度的自然工具。
4.2 反向传播的推导(以两层网络为例)
考虑一个简单网络:
- 输入: x ∈ R n \mathbf{x} \in \mathbb{R}^n x∈Rn
- 隐藏层: h = ReLU ( W 1 x + b 1 ) \mathbf{h} = \text{ReLU}(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) h=ReLU(W1x+b1)
- 输出: y ^ = σ ( w 2 ⊤ h + b 2 ) \hat{y} = \sigma(\mathbf{w}_2^\top \mathbf{h} + b_2) y^=σ(w2⊤h+b2)
- 损失: L = BCE ( y , y ^ ) \mathcal{L} = \text{BCE}(y, \hat{y}) L=BCE(y,y^)
我们希望计算 ∂ L ∂ W 1 \frac{\partial \mathcal{L}}{\partial \mathbf{W}_1} ∂W1∂L, ∂ L ∂ W 2 \frac{\partial \mathcal{L}}{\partial \mathbf{W}_2} ∂W2∂L 等。
步骤 1:计算输出层梯度
令 z 2 = w 2 ⊤ h + b 2 z_2 = \mathbf{w}_2^\top \mathbf{h} + b_2 z2=w2⊤h+b2,则:
∂ L ∂ z 2 = y ^ − y (这是 BCE + Sigmoid 的优雅性质!) \frac{\partial \mathcal{L}}{\partial z_2} = \hat{y} - y \quad \text{(这是 BCE + Sigmoid 的优雅性质!)} ∂z2∂L=y^−y(这是 BCE + Sigmoid 的优雅性质!)
于是:
∂ L ∂ w 2 = ∂ L ∂ z 2 ⋅ ∂ z 2 ∂ w 2 = ( y ^ − y ) ⋅ h \frac{\partial \mathcal{L}}{\partial \mathbf{w}_2} = \frac{\partial \mathcal{L}}{\partial z_2} \cdot \frac{\partial z_2}{\partial \mathbf{w}_2} = (\hat{y} - y) \cdot \mathbf{h} ∂w2∂L=∂z2∂L⋅∂w2∂z2=(y^−y)⋅h
∂ L ∂ b 2 = y ^ − y \frac{\partial \mathcal{L}}{\partial b_2} = \hat{y} - y ∂b2∂L=y^−y
步骤 2:传播到隐藏层
令 z 1 = W 1 x + b 1 \mathbf{z}_1 = \mathbf{W}_1 \mathbf{x} + \mathbf{b}_1 z1=W1x+b1, h = ReLU ( z 1 ) \mathbf{h} = \text{ReLU}(\mathbf{z}_1) h=ReLU(z1)。
首先计算 ∂ L ∂ h \frac{\partial \mathcal{L}}{\partial \mathbf{h}} ∂h∂L:
∂ L ∂ h = ∂ L ∂ z 2 ⋅ ∂ z 2 ∂ h = ( y ^ − y ) ⋅ w 2 ⊤ \frac{\partial \mathcal{L}}{\partial \mathbf{h}} = \frac{\partial \mathcal{L}}{\partial z_2} \cdot \frac{\partial z_2}{\partial \mathbf{h}} = (\hat{y} - y) \cdot \mathbf{w}_2^\top ∂h∂L=∂z2∂L⋅∂h∂z2=(y^−y)⋅w2⊤
然后,由于 ReLU 的导数为:
d d z ReLU ( z ) = { 1 , z > 0 0 , z ≤ 0 = I ( z > 0 ) \frac{d}{dz} \text{ReLU}(z) = \begin{cases} 1, & z > 0 \\ 0, & z \leq 0 \end{cases} = \mathbb{I}(z > 0) dzdReLU(z)={1,0,z>0z≤0=I(z>0)
所以:
∂ L ∂ z 1 = ∂ L ∂ h ⊙ I ( z 1 > 0 ) \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{h}} \odot \mathbb{I}(\mathbf{z}_1 > 0) ∂z1∂L=∂h∂L⊙I(z1>0)
其中 ⊙ \odot ⊙ 表示逐元素相乘(Hadamard product)。
最后:
∂ L ∂ W 1 = ∂ L ∂ z 1 ⋅ x ⊤ \frac{\partial \mathcal{L}}{\partial \mathbf{W}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} \cdot \mathbf{x}^\top ∂W1∂L=∂z1∂L⋅x⊤
∂ L ∂ b 1 = ∂ L ∂ z 1 \frac{\partial \mathcal{L}}{\partial \mathbf{b}_1} = \frac{\partial \mathcal{L}}{\partial \mathbf{z}_1} ∂b1∂L=∂z1∂L
✅ 反向传播的本质 :
从输出层开始,逐层反向计算"误差信号" (即 ∂ L ∂ z ( l ) \frac{\partial \mathcal{L}}{\partial \mathbf{z}^{(l)}} ∂z(l)∂L),再用它计算当前层参数的梯度。
4.3 一般形式:向量化反向传播
对第 l l l 层,定义误差项(error term):
δ ( l ) = ∂ L ∂ Z ( l ) \boldsymbol{\delta}^{(l)} = \frac{\partial \mathcal{L}}{\partial \mathbf{Z}^{(l)}} δ(l)=∂Z(l)∂L
则:
-
输出层(假设用 Softmax + CE):
δ ( L ) = A ( L ) − Y \boldsymbol{\delta}^{(L)} = \mathbf{A}^{(L)} - \mathbf{Y} δ(L)=A(L)−Y -
隐藏层( l < L l < L l<L):
δ ( l ) = ( W ( l + 1 ) δ ( l + 1 ) ) ⊙ σ ′ ( l ) ( Z ( l ) ) \boldsymbol{\delta}^{(l)} = \left( \mathbf{W}^{(l+1)} \boldsymbol{\delta}^{(l+1)} \right) \odot \sigma'^{(l)}(\mathbf{Z}^{(l)}) δ(l)=(W(l+1)δ(l+1))⊙σ′(l)(Z(l)) -
参数梯度:
∂ L ∂ W ( l ) = A ( l − 1 ) ⊤ δ ( l ) \frac{\partial \mathcal{L}}{\partial \mathbf{W}^{(l)}} = \mathbf{A}^{(l-1)\top} \boldsymbol{\delta}^{(l)} ∂W(l)∂L=A(l−1)⊤δ(l)
∂ L ∂ b ( l ) = ∑ i = 1 m δ i ( l ) (按样本求和) \frac{\partial \mathcal{L}}{\partial \mathbf{b}^{(l)}} = \sum_{i=1}^{m} \boldsymbol{\delta}^{(l)}_i \quad \text{(按样本求和)} ∂b(l)∂L=i=1∑mδi(l)(按样本求和)
🔑 反向传播的计算复杂度与前向传播同阶,这是它被广泛采用的关键原因。
五、动手实现:从零构建一个全连接神经网络
我们将用 NumPy 实现一个支持任意层数的 MLP,并用于手写数字分类(MNIST 简化版)。
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class MLP:
def __init__(self, layer_sizes, learning_rate=0.01, epochs=1000):
"""
layer_sizes: list, e.g., [64, 32, 10] for input=64, hidden=32, output=10
"""
self.layer_sizes = layer_sizes
self.lr = learning_rate
self.epochs = epochs
self.weights = []
self.biases = []
self.losses = []
# 初始化参数(Xavier 初始化)
for i in range(len(layer_sizes) - 1):
w = np.random.randn(layer_sizes[i], layer_sizes[i+1]) * np.sqrt(2.0 / layer_sizes[i])
b = np.zeros((1, layer_sizes[i+1]))
self.weights.append(w)
self.biases.append(b)
def relu(self, z):
return np.maximum(0, z)
def relu_derivative(self, z):
return (z > 0).astype(float)
def softmax(self, z):
exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) # 防止溢出
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
def cross_entropy(self, y_true, y_pred):
m = y_true.shape[0]
log_likelihood = -np.log(y_pred[range(m), y_true.argmax(axis=1)] + 1e-8)
return np.sum(log_likelihood) / m
def forward(self, X):
activations = [X]
z_values = []
for i in range(len(self.weights)):
z = np.dot(activations[-1], self.weights[i]) + self.biases[i]
z_values.append(z)
if i == len(self.weights) - 1: # 输出层
a = self.softmax(z)
else: # 隐藏层
a = self.relu(z)
activations.append(a)
return activations, z_values
def backward(self, X, y, activations, z_values):
m = X.shape[0]
grads_w = [None] * len(self.weights)
grads_b = [None] * len(self.biases)
# 输出层误差
dz = activations[-1] - y # Softmax + CE 的梯度
grads_w[-1] = np.dot(activations[-2].T, dz) / m
grads_b[-1] = np.sum(dz, axis=0, keepdims=True) / m
# 反向传播隐藏层
for l in range(len(self.weights) - 2, -1, -1):
dz = np.dot(dz, self.weights[l+1].T) * self.relu_derivative(z_values[l])
grads_w[l] = np.dot(activations[l].T, dz) / m
grads_b[l] = np.sum(dz, axis=0, keepdims=True) / m
# 更新参数
for i in range(len(self.weights)):
self.weights[i] -= self.lr * grads_w[i]
self.biases[i] -= self.lr * grads_b[i]
def fit(self, X, y):
for epoch in range(self.epochs):
activations, z_values = self.forward(X)
loss = self.cross_entropy(y, activations[-1])
self.losses.append(loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
self.backward(X, y, activations, z_values)
def predict(self, X):
activations, _ = self.forward(X)
return np.argmax(activations[-1], axis=1)
# 加载数据:sklearn digits(1797 张 8x8 手写数字)
digits = load_digits()
X, y = digits.data, digits.target
# 标准化(对神经网络很重要!)
scaler = StandardScaler()
X = scaler.fit_transform(X)
# 转换标签为 one-hot
y_onehot = np.eye(10)[y]
# 划分数据
X_train, X_test, y_train, y_test, y_train_oh, y_test_oh = train_test_split(
X, y, y_onehot, test_size=0.2, random_state=42
)
# 训练模型
mlp = MLP(layer_sizes=[64, 32, 16, 10], learning_rate=0.1, epochs=1000)
mlp.fit(X_train, y_train_oh)
# 评估
y_pred = mlp.predict(X_test)
accuracy = np.mean(y_pred == y_test)
print(f"\nTest Accuracy: {accuracy:.4f}")
# 可视化损失
plt.plot(mlp.losses)
plt.title("Training Loss")
plt.xlabel("Epoch")
plt.ylabel("Cross-Entropy Loss")
plt.show()
✅ 关键实现细节:
- 使用 Xavier 初始化 (
np.sqrt(2.0 / fan_in))缓解梯度消失;- 标准化输入(StandardScaler)加速收敛;
- Softmax + Cross-Entropy 的梯度简化为
pred - true;- 向量化实现高效批量计算。
六、训练神经网络的挑战与解决方案
尽管原理清晰,但训练神经网络常面临以下挑战:
6.1 梯度消失与爆炸(Vanishing/Exploding Gradients)
在深层网络中,反向传播的梯度是多个矩阵乘积:
∂ L ∂ W ( 1 ) ∝ W ( 2 ) W ( 3 ) ⋯ W ( L ) \frac{\partial \mathcal{L}}{\partial \mathbf{W}^{(1)}} \propto \mathbf{W}^{(2)} \mathbf{W}^{(3)} \cdots \mathbf{W}^{(L)} ∂W(1)∂L∝W(2)W(3)⋯W(L)
若权重初始化不当:
- ∣ W ∣ < 1 |\mathbf{W}| < 1 ∣W∣<1 → 梯度指数级衰减(消失);
- ∣ W ∣ > 1 |\mathbf{W}| > 1 ∣W∣>1 → 梯度指数级增长(爆炸)。
解决方案:
- 合理初始化:Xavier(Sigmoid/Tanh)、He(ReLU);
- 使用 ReLU:避免 Sigmoid/Tanh 的饱和区;
- Batch Normalization:对每层输入标准化,稳定分布;
- 残差连接(ResNet):允许梯度直接跨层流动。
6.2 过拟合(Overfitting)
神经网络参数量大,极易过拟合小数据集。
正则化技术:
- L2 正则化 (Weight Decay):
L total = L data + λ ∑ l ∥ W ( l ) ∥ F 2 \mathcal{L}{\text{total}} = \mathcal{L}{\text{data}} + \lambda \sum_{l} \|\mathbf{W}^{(l)}\|_F^2 Ltotal=Ldata+λl∑∥W(l)∥F2 - Dropout:训练时随机"关闭"一部分神经元(如 50%),强制网络不依赖特定路径;
- 早停(Early Stopping):监控验证集损失,不再下降时停止;
- 数据增强:对图像旋转、裁剪、加噪等。
6.3 优化困难:局部极小与鞍点
高维损失 landscape 存在大量鞍点(saddle points),而非局部极小。
现代优化器:
-
SGD with Momentum :
v t = β v t − 1 + ( 1 − β ) ∇ L t θ t = θ t − 1 − η v t \mathbf{v}t = \beta \mathbf{v}{t-1} + (1 - \beta) \nabla \mathcal{L}t \\ \theta_t = \theta{t-1} - \eta \mathbf{v}_t vt=βvt−1+(1−β)∇Ltθt=θt−1−ηvt动量帮助穿越平坦区域。
-
Adam (Adaptive Moment Estimation):
m t = β 1 m t − 1 + ( 1 − β 1 ) ∇ L t v t = β 2 v t − 1 + ( 1 − β 2 ) ( ∇ L t ) 2 m ^ t = m t 1 − β 1 t , v ^ t = v t 1 − β 2 t θ t = θ t − 1 − η m ^ t v ^ t + ϵ \mathbf{m}t = \beta_1 \mathbf{m}{t-1} + (1 - \beta_1) \nabla \mathcal{L}_t \\ \mathbf{v}t = \beta_2 \mathbf{v}{t-1} + (1 - \beta_2) (\nabla \mathcal{L}_t)^2 \\ \hat{\mathbf{m}}_t = \frac{\mathbf{m}_t}{1 - \beta_1^t}, \quad \hat{\mathbf{v}}_t = \frac{\mathbf{v}t}{1 - \beta_2^t} \\ \theta_t = \theta{t-1} - \eta \frac{\hat{\mathbf{m}}_t}{\sqrt{\hat{\mathbf{v}}_t} + \epsilon} mt=β1mt−1+(1−β1)∇Ltvt=β2vt−1+(1−β2)(∇Lt)2m^t=1−β1tmt,v^t=1−β2tvtθt=θt−1−ηv^t +ϵm^tAdam 自适应调整每个参数的学习率,是目前最常用的优化器。
七、使用 PyTorch 构建现代神经网络
在实践中,我们使用深度学习框架(如 PyTorch)而非手动实现。
python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 转换数据为 PyTorch 张量
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.long)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.long)
train_loader = DataLoader(TensorDataset(X_train_t, y_train_t), batch_size=32, shuffle=True)
# 定义模型
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(64, 32)
self.fc2 = nn.Linear(32, 16)
self.fc3 = nn.Linear(16, 10)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.dropout(x)
x = self.relu(self.fc2(x))
x = self.dropout(x)
x = self.fc3(x) # 无需 Softmax,CrossEntropyLoss 内部处理
return x
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练循环
for epoch in range(100):
model.train()
for batch_x, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
if epoch % 20 == 0:
model.eval()
with torch.no_grad():
test_outputs = model(X_test_t)
pred = test_outputs.argmax(dim=1)
acc = (pred == y_test_t).float().mean().item()
print(f"Epoch {epoch}, Test Acc: {acc:.4f}")
✅ PyTorch 优势:
- 自动微分(
loss.backward());- GPU 加速(
.to('cuda'));- 丰富的预定义层(Linear, Conv2d, LSTM 等);
- 动态计算图,调试友好。
八、可解释性:揭开神经网络的黑箱
神经网络常被诟病为"黑箱",但近年来出现了多种解释方法。
8.1 全局可解释性
- 特征重要性:通过扰动输入特征,观察输出变化;
- 激活可视化:对隐藏层神经元,找出使其激活最大的输入(如 DeepDream)。
8.2 局部可解释性:LIME 与 SHAP
-
LIME(Local Interpretable Model-agnostic Explanations):
- 在预测点附近生成扰动样本;
- 用简单模型(如线性回归)拟合局部行为;
- 解释该简单模型的系数。
-
SHAP(SHapley Additive exPlanations):
- 基于博弈论,计算每个特征对预测的边际贡献;
- 对神经网络,可用 DeepSHAP 或 Integrated Gradients 近似。
python
# 使用 SHAP 解释 PyTorch 模型
import shap
explainer = shap.DeepExplainer(model, X_train_t[:100]) # 背景数据
shap_values = explainer.shap_values(X_test_t[:1])
shap.image_plot(shap_values, -X_test_t[:1].reshape(1, 8, 8)) # 对图像 reshape
8.3 注意力机制(Attention)
在 NLP 和 CV 中,注意力权重本身提供了解释性:
"模型关注了句子中的'not',因此将情感判断为负面。"
九、神经网络 vs 传统模型:何时选择谁?
| 维度 | 神经网络 | 传统模型(XGBoost 等) |
|---|---|---|
| 数据类型 | 图像、文本、语音、图、序列 | 结构化表格数据 |
| 数据规模 | 需大量数据(>10k 样本) | 小数据也能 work |
| 特征工程 | 自动学习特征表示 | 依赖人工特征工程 |
| 可解释性 | 黑箱(需额外工具) | 相对透明(树路径、系数) |
| 训练资源 | 需 GPU、调参复杂 | CPU 即可,调参简单 |
| 默认性能 | 在非结构化数据上碾压 | 在表格数据上 often 更优 |
✅ 经验法则:
- 结构化数据:先试 XGBoost/LightGBM;
- 图像/文本/语音:直接上 CNN/Transformer;
- 混合数据:用神经网络处理非结构化部分,传统模型处理表格部分,再融合。
十、进阶方向:从 MLP 到现代架构
全连接网络(MLP)只是起点,现代深度学习包含:
- 卷积神经网络(CNN):利用局部性和平移不变性处理图像;
- 循环神经网络(RNN/LSTM):处理序列数据;
- Transformer:通过自注意力机制统一处理各类序列;
- 图神经网络(GNN):处理图结构数据;
- 自监督学习:在无标签数据上预训练(如 BERT、SimCLR)。
这些架构的核心思想仍是:堆叠可微模块 + 端到端训练。
十一、结语:函数逼近的艺术
神经网络不是魔法,而是一种工程化的函数逼近技术。它的力量源于:
- 通用逼近能力:理论上可拟合任意函数;
- 端到端学习:从原始输入到最终输出,无需中间表示;
- 可扩展性:增加层数/宽度即可提升容量。
但也要清醒认识到:
- 它需要大量数据和算力;
- 它缺乏因果推理能力;
- 它的决策过程难以审计。
最好的实践,是在理解其原理的基础上,审慎选择工具,负责任地应用。
下一篇文章,我们将进入无监督学习的世界------那里没有标签,只有数据自身的结构等待发现。
但在那之前,请记住:
模型的价值,不在于它有多深,而在于它解决了什么问题。
行动建议
- 在 MNIST 或 Fashion-MNIST 上复现 MLP,并尝试不同激活函数、初始化方法;
- 对比 MLP 与 XGBoost 在 digits 数据集上的性能与训练时间;
- 使用 SHAP 或 LIME 解释一个神经网络的预测结果;
- 尝试添加 BatchNorm、Dropout,观察对过拟合的影响;
- 阅读《Deep Learning》(Goodfellow et al.)第 6 章"深度前馈网络"。
真正的理解,始于亲手实现,成于反复实验。