第5章:神经网络(Neural Networks)
一、导读
5.1 本章学习目标
第5章是全书最具实践影响力的章节,系统介绍了前馈神经网络——深度学习的基石。从感知机到多层网络,从激活函数到反向传播,本章为理解现代深度学习提供了完整的理论基础。Bishop 以概率模型的视角审视神经网络,将其理解为从输入到输出的非线性参数化映射,这种视角使得神经网络与本书其他章节的模型形成了有机的统一。
本章的核心学习目标包括:
- 理解前馈网络的基本结构:输入层、隐藏层、输出层各自扮演什么角色?网络如何表示复杂函数?
- 掌握激活函数的设计原理:为什么非线性激活函数是必要的?常见的激活函数有哪些?各自的特点是什么?
- 深入理解反向传播算法:链式法则如何高效计算梯度?计算图在反向传播中的作用是什么?
- 掌握正则化技术:权重衰减、早停法、数据增强如何防止神经网络过拟合?
- 理解误差函数的选择:回归和分类问题分别应该使用什么误差函数?
- 了解贝叶斯神经网络:如何将贝叶斯方法应用于神经网络?
5.2 为什么本章如此重要
神经网络是现代人工智能的核心技术。从图像识别到自然语言处理,从游戏AI到蛋白质结构预测,神经网络在几乎所有AI领域都取得了突破性进展。理解神经网络的原理,是理解现代AI的基础。
更重要的是,本章引入的技术——反向传播、梯度下降、正则化——不仅适用于神经网络,也是整个机器学习领域的基本工具。即使你最终使用更复杂的深度学习架构,本章的基础知识也是不可或缺的。
5.3 核心问题引导
在阅读本章时,请带着以下问题思考:
为什么单层感知机不能解决XOR问题?多层网络如何克服这个限制?
反向传播算法的本质是什么?它与链式法则有什么关系?
为什么ReLU激活函数在实践中比sigmoid更常用?
神经网络中的"隐藏层"学到了什么?如何可视化隐藏层的表示?
为什么训练深层网络比训练浅层网络困难得多?梯度消失和梯度爆炸的原因是什么?
贝叶斯神经网络如何量化预测的不确定性?
二、核心概念详解
5.1 前馈神经网络
5.1.1 从感知机到多层网络
感知机是最简单的神经网络,由一个线性组合和一个阶跃激活函数组成:
$$y = \begin{cases} 1 & \text{if } \mathbf{w}^T \mathbf{x} + b > 0 \\ 0 & \text{otherwise} \end{cases}$$
感知机只能表示线性可分的函数,无法解决XOR问题。
多层前馈网络通过引入隐藏层和非线性激活函数,可以表示任意复杂的函数(通用逼近定理)。
一个典型的两层网络:
$$y_k(\mathbf{x}) = \sigma\left(\sum_{j=0}^{M} w_{kj}^{(2)} h\left(\sum_{i=0}^{D} w_{ji}^{(1)} x_i\right)\right)$$
其中 $h(\cdot)$ 是隐藏层激活函数,$\sigma(\cdot)$ 是输出层激活函数。
import numpy as np
import matplotlib.pyplot as plt
class SimpleNeuralNetwork:
"""两层前馈神经网络"""
def __init__(self, input_dim, hidden_dim, output_dim, activation='relu'):
self.W1 = np.random.randn(input_dim, hidden_dim) * 0.01
self.b1 = np.zeros(hidden_dim)
self.W2 = np.random.randn(hidden_dim, output_dim) * 0.01
self.b2 = np.zeros(output_dim)
self.activation = activation
def _activate(self, z):
if self.activation == 'relu':
return np.maximum(0, z)
elif self.activation == 'sigmoid':
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
elif self.activation == 'tanh':
return np.tanh(z)
return z
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = self._activate(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
return self.z2
def predict(self, X):
return self.forward(X)
# XOR问题
X_xor = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_xor = np.array([[0], [1], [1], [0]])
# 单层感知机无法解决XOR
# 但两层网络可以
nn = SimpleNeuralNetwork(2, 4, 1, activation='sigmoid')
# 手动设置权重来展示XOR的解决
# 隐藏层:两个神经元分别检测 OR(x1,x2) 和 AND(x1,x2)
nn.W1 = np.array([[10, 10], [10, 10]])
nn.b1 = np.array([-5, -15])
nn.W2 = np.array([[10], [-10]])
nn.b2 = np.array([-5])
predictions = nn.predict(X_xor)
print("XOR问题:")
for i in range(4):
print(f" 输入: {X_xor[i]}, 输出: {predictions[i][0]:.4f}, 期望: {y_xor[i][0]}")5.1.2 激活函数
激活函数为网络引入非线性,是神经网络能够表示复杂函数的关键。
常见激活函数:
- Sigmoid:$\sigma(a) = \frac{1}{1+e^{-a}}$,输出范围 $(0,1)$,适合二分类输出层
- Tanh:$\tanh(a) = \frac{e^a - e^{-a}}{e^a + e^{-a}}$,输出范围 $(-1,1)$,零均值
- ReLU:$\text{ReLU}(a) = \max(0, a)$,计算简单,缓解梯度消失
- Leaky ReLU:$\text{LReLU}(a) = \max(\alpha a, a)$,$\alpha$ 通常取 0.01
- ELU:$\text{ELU}(a) = \begin{cases} a & a > 0 \\ \alpha(e^a - 1) & a \leq 0 \end{cases}$
def plot_activation_functions():
"""可视化常见激活函数"""
a = np.linspace(-4, 4, 200)
functions = {
'Sigmoid': 1 / (1 + np.exp(-a)),
'Tanh': np.tanh(a),
'ReLU': np.maximum(0, a),
'Leaky ReLU': np.where(a > 0, a, 0.1 * a),
'ELU': np.where(a > 0, a, np.exp(a) - 1),
}
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 激活函数
for name, y in functions.items():
axes[0].plot(a, y, linewidth=2, label=name)
axes[0].set_xlabel('a')
axes[0].set_ylabel('f(a)')
axes[0].set_title('Activation Functions')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(-1.5, 4)
# 导数
derivatives = {
'Sigmoid': functions['Sigmoid'] * (1 - functions['Sigmoid']),
'Tanh': 1 - functions['Tanh']**2,
'ReLU': (a > 0).astype(float),
'Leaky ReLU': np.where(a > 0, 1, 0.1),
'ELU': np.where(a > 0, 1, np.exp(a)),
}
for name, dy in derivatives.items():
axes[1].plot(a, dy, linewidth=2, label=name)
axes[1].set_xlabel('a')
axes[1].set_ylabel("f'(a)")
axes[1].set_title('Derivatives of Activation Functions')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].set_ylim(-0.2, 1.2)
plt.tight_layout()
plt.savefig('activation_functions.png', dpi=150)
plt.close()
plot_activation_functions()5.1.3 通用逼近定理
通用逼近定理(Universal Approximation Theorem):一个具有单个隐藏层的前馈网络,只要隐藏层有足够多的神经元,就可以以任意精度逼近任意连续函数。
但这个定理有两个重要的限制:
它不告诉你需要多少神经元(可能指数级多)
它不告诉你如何找到正确的参数
在实践中,使用多个隐藏层(深层网络)通常比使用单个宽隐藏层更有效。深层网络可以学习层次化的特征表示。
5.2 反向传播算法
5.2.1 链式法则与计算图
反向传播算法的核心是链式法则。对于复合函数 $f(g(x))$:
$$\frac{\partial f}{\partial x} = \frac{\partial f}{\partial g} \cdot \frac{\partial g}{\partial x}$$
计算图将复杂的网络计算分解为一系列简单操作的组合,每个操作对应一个局部梯度。反向传播从输出层开始,逐层向后传播梯度。
5.2.2 反向传播的推导
对于两层网络,使用交叉熵损失和sigmoid输出:
$$E = -\sum_n \{t_n \ln y_n + (1-t_n) \ln(1-y_n)\}$$
输出层误差信号:
$$\delta_k = y_k - t_k$$
隐藏层误差信号:
$$\delta_j = h_j'(a_j) \sum_k w_{kj}^{(2)} \delta_k$$
权重梯度:
$$\frac{\partial E}{\partial w_{ji}^{(1)}} = \delta_j x_i, \quad \frac{\partial E}{\partial w_{kj}^{(2)}} = \delta_k h_j$$
class NeuralNetworkBackprop:
"""实现反向传播的两层神经网络"""
def __init__(self, input_dim, hidden_dim, output_dim):
self.W1 = np.random.randn(input_dim, hidden_dim) * np.sqrt(2.0 / input_dim)
self.b1 = np.zeros(hidden_dim)
self.W2 = np.random.randn(hidden_dim, output_dim) * np.sqrt(2.0 / hidden_dim)
self.b2 = np.zeros(output_dim)
def sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def relu(self, z):
return np.maximum(0, z)
def forward(self, X):
self.X = X
self.z1 = X @ self.W1 + self.b1
self.a1 = self.relu(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
self.a2 = self.sigmoid(self.z2)
return self.a2
def backward(self, t, lr=0.01):
N = len(t)
# 输出层误差
delta2 = self.a2 - t # (N, output_dim)
# 隐藏层误差
delta1 = (delta2 @ self.W2.T) * (self.z1 > 0).astype(float) # (N, hidden_dim)
# 梯度
dW2 = self.a1.T @ delta2 / N
db2 = delta2.mean(axis=0)
dW1 = self.X.T @ delta1 / N
db1 = delta1.mean(axis=0)
# 更新
self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
def train(self, X, t, n_epochs=1000, lr=0.1):
losses = []
for epoch in range(n_epochs):
y = self.forward(X)
loss = -np.mean(t * np.log(y + 1e-10) + (1-t) * np.log(1-y + 1e-10))
losses.append(loss)
self.backward(t, lr)
return losses
# 示例:二分类
np.random.seed(42)
N = 200
X_data = np.vstack([
np.random.randn(N//2, 2) + [1, 1],
np.random.randn(N//2, 2) + [-1, -1]
])
t_data = np.array([[1]] * (N//2) + [[0]] * (N//2))
nn_bp = NeuralNetworkBackprop(2, 8, 1)
losses = nn_bp.train(X_data, t_data, n_epochs=500, lr=0.5)
predictions = nn_bp.forward(X_data)
accuracy = np.mean((predictions > 0.5) == t_data)
print(f"最终损失: {losses[-1]:.4f}")
print(f"训练准确率: {accuracy:.4f}")5.2.3 梯度消失与梯度爆炸
梯度消失:在深层网络中,梯度在反向传播过程中逐层缩小,导致浅层参数几乎不更新。原因:sigmoid/tanh的导数小于1,多层连乘后梯度指数级缩小。
梯度爆炸:相反的情况,梯度在反向传播过程中逐层放大。原因:权重过大,多层连乘后梯度指数级增长。
解决方案:
- ReLU激活函数:正区间导数为1,缓解梯度消失
- He初始化:$W \sim \mathcal{N}(0, 2/n_{in})$,配合ReLU使用
- Xavier初始化:$W \sim \mathcal{N}(0, 2/(n_{in}+n_{out}))$,配合sigmoid/tanh使用
- Batch Normalization:标准化每层的输入
- 残差连接:提供梯度的"高速公路"
def compare_initializations():
"""比较不同初始化方法的效果"""
np.random.seed(42)
# 深层网络
layer_sizes = [10, 50, 50, 50, 50, 1]
n_layers = len(layer_sizes) - 1
initializations = {
'Small': lambda n_in, n_out: np.random.randn(n_in, n_out) * 0.01,
'Xavier': lambda n_in, n_out: np.random.randn(n_in, n_out) * np.sqrt(2.0 / (n_in + n_out)),
'He': lambda n_in, n_out: np.random.randn(n_in, n_out) * np.sqrt(2.0 / n_in),
}
X = np.random.randn(100, 10)
for init_name, init_func in initializations.items():
weights = []
for i in range(n_layers):
W = init_func(layer_sizes[i], layer_sizes[i+1])
weights.append(W)
# 前向传播,观察激活值的方差
a = X
variances = []
for i, W in enumerate(weights):
z = a @ W
if i < n_layers - 1:
a = np.maximum(0, z) # ReLU
else:
a = z
variances.append(np.var(a))
print(f"{init_name}初始化: 各层激活值方差 = {[f'{v:.4f}' for v in variances]}")
compare_initializations()5.3 误差函数
5.3.1 回归问题
对于回归问题,最常用的误差函数是平方误差和(Sum of Squares):
$$E = \frac{1}{2} \sum_{n=1}^{N} \|y(\mathbf{x}_n) - t_n\|^2$$
等价于假设目标变量服从高斯分布 $p(t|\mathbf{x}) = \mathcal{N}(t|y(\mathbf{x}), \beta^{-1})$ 的最大似然估计。
5.3.2 分类问题
对于二分类,使用交叉熵损失:
$$E = -\sum_{n=1}^{N} \{t_n \ln y_n + (1-t_n) \ln(1-y_n)\}$$
对于多分类($K$ 类),使用多分类交叉熵:
$$E = -\sum_{n=1}^{N} \sum_{k=1}^{K} t_{nk} \ln y_{nk}$$
5.3.3 误差函数的概率解释
选择误差函数应该基于目标变量的条件分布:
| 目标分布 | 误差函数 | 输出层激活 |
|---|---|---|
| 高斯 | 平方误差 | 线性 |
| 伯努利 | 交叉熵 | Sigmoid |
| 多项式 | 多分类交叉熵 | Softmax |
| 泊松 | 泊松偏差 | Exponential |
5.4 正则化技术
5.4.1 权重衰减(L2正则化)
在误差函数中添加权重平方和的惩罚:
$$\tilde{E} = E + \frac{\lambda}{2} \|\mathbf{w}\|^2$$
等价于给权重添加高斯先验 $p(\mathbf{w}) = \mathcal{N}(\mathbf{w}|\mathbf{0}, \alpha^{-1}\mathbf{I})$,其中 $\lambda = \alpha$。
5.4.2 早停法(Early Stopping)
将数据分为训练集和验证集。训练过程中监控验证集误差,当验证集误差开始增大时停止训练。
早停法隐式地起到了正则化的效果:限制训练步数等价于限制参数空间的有效大小。
def early_stopping_training(X_train, t_train, X_val, t_val,
hidden_dim=16, max_epochs=1000, patience=50):
"""带早停法的神经网络训练"""
nn = NeuralNetworkBackprop(X_train.shape[1], hidden_dim, 1)
train_losses = []
val_losses = []
best_val_loss = float('inf')
best_epoch = 0
best_weights = None
for epoch in range(max_epochs):
# 训练
nn.forward(X_train)
nn.backward(t_train, lr=0.1)
# 计算损失
y_train = nn.forward(X_train)
train_loss = -np.mean(t_train * np.log(y_train + 1e-10) +
(1-t_train) * np.log(1-y_train + 1e-10))
train_losses.append(train_loss)
y_val = nn.forward(X_val)
val_loss = -np.mean(t_val * np.log(y_val + 1e-10) +
(1-t_val) * np.log(1-y_val + 1e-10))
val_losses.append(val_loss)
# 检查是否改善
if val_loss < best_val_loss:
best_val_loss = val_loss
best_epoch = epoch
best_weights = (nn.W1.copy(), nn.b1.copy(), nn.W2.copy(), nn.b2.copy())
# 早停检查
if epoch - best_epoch > patience:
print(f"早停于第 {epoch} 轮 (最佳轮次: {best_epoch})")
break
# 恢复最佳权重
nn.W1, nn.b1, nn.W2, nn.b2 = best_weights
# 可视化
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(train_losses, label='Training Loss', alpha=0.7)
ax.plot(val_losses, label='Validation Loss', alpha=0.7)
ax.axvline(x=best_epoch, color='r', linestyle='--', label=f'Best epoch ({best_epoch})')
ax.set_xlabel('Epoch')
ax.set_ylabel('Loss')
ax.set_title('Early Stopping')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('early_stopping.png', dpi=150)
plt.close()
return nn, train_losses, val_losses
# 示例
np.random.seed(42)
N = 300
X_all = np.vstack([
np.random.randn(N//2, 2) + [1, 1],
np.random.randn(N//2, 2) + [-1, -1]
])
t_all = np.array([[1]] * (N//2) + [[0]] * (N//2))
# 划分训练集和验证集
indices = np.random.permutation(N)
split = int(0.7 * N)
X_train_es = X_all[indices[:split]]
t_train_es = t_all[indices[:split]]
X_val_es = X_all[indices[split:]]
t_val_es = t_all[indices[split:]]
nn_es, _, _ = early_stopping_training(X_train_es, t_train_es, X_val_es, t_val_es)5.4.3 Dropout
Dropout在训练时随机将一部分神经元的输出置为零,迫使网络学习更鲁棒的特征。
def dropout_forward(X, W1, b1, W2, b2, p_drop=0.5, training=True):
"""带Dropout的前向传播"""
z1 = X @ W1 + b1
a1 = np.maximum(0, z1) # ReLU
if training:
mask = (np.random.rand(*a1.shape) > p_drop).astype(float)
a1 *= mask / (1 - p_drop) # Inverted dropout
else:
mask = None
z2 = a1 @ W2 + b2
return z2, mask
# Dropout的效果示意
print("Dropout: 训练时随机失活神经元,等效于训练指数级多个子网络")
print("推理时使用所有神经元(乘以keep_prob进行缩放)")5.5 贝叶斯神经网络
5.5.1 动机
标准神经网络给出点估计,无法量化预测的不确定性。贝叶斯神经网络将权重视为随机变量,通过贝叶斯推断获得权重的后验分布,从而提供预测的置信度。
5.5.2 贝叶斯框架
- 先验:$p(\mathbf{w}) = \mathcal{N}(\mathbf{w}|\mathbf{0}, \alpha^{-1}\mathbf{I})$
- 似然:$p(\mathcal{D}|\mathbf{w}) = \prod_n p(t_n|\mathbf{x}_n, \mathbf{w})$
- 后验:$p(\mathbf{w}|\mathcal{D}) = \frac{p(\mathcal{D}|\mathbf{w}) p(\mathbf{w})}{p(\mathcal{D})}$
由于后验分布通常没有解析解,需要使用近似方法(如变分推断、MCMC)。
def bayesian_nn_mc_dropout(X_train, t_train, X_test, n_models=20, p_drop=0.1):
"""使用MC Dropout近似贝叶斯推断"""
np.random.seed(42)
predictions = []
for m in range(n_models):
# 训练一个带dropout的网络
nn = NeuralNetworkBackprop(X_train.shape[1], 16, 1)
nn.train(X_train, t_train, n_epochs=100, lr=0.1)
# MC Dropout: 推理时也使用dropout
mc_preds = []
for _ in range(50):
pred, _ = dropout_forward(
X_train, nn.W1, nn.b1, nn.W2, nn.b2,
p_drop=p_drop, training=True
)
mc_preds.append(1 / (1 + np.exp(-pred)))
pred_mean = np.mean(mc_preds, axis=0)
predictions.append(pred_mean)
predictions = np.array(predictions)
mean_pred = predictions.mean(axis=0)
std_pred = predictions.std(axis=0)
return mean_pred, std_pred
print("MC Dropout: 在推理时保持dropout开启,多次前向传播取平均")
print("预测均值: 模型的期望预测")
print("预测标准差: 模型不确定性的估计")三、实战案例
5.1 非线性分类问题
def nonlinear_classification():
"""使用神经网络解决非线性分类问题"""
np.random.seed(42)
# 生成环形数据
N = 400
theta = np.random.uniform(0, 2*np.pi, N)
r_inner = np.random.uniform(0, 0.5, N//2)
r_outer = np.random.uniform(1.0, 1.5, N//2)
X_inner = np.column_stack([r_inner * np.cos(theta[:N//2]),
r_inner * np.sin(theta[:N//2])])
X_outer = np.column_stack([r_outer * np.cos(theta[N//2:]),
r_outer * np.sin(theta[N//2:])])
X_nc = np.vstack([X_inner, X_outer])
t_nc = np.array([[0]] * (N//2) + [[1]] * (N//2))
# 训练神经网络
nn = NeuralNetworkBackprop(2, 16, 1)
losses = nn.train(X_nc, t_nc, n_epochs=1000, lr=0.1)
# 可视化决策边界
h = 0.02
x_min, x_max = X_nc[:, 0].min() - 0.5, X_nc[:, 0].max() + 0.5
y_min, y_max = X_nc[:, 1].min() - 0.5, X_nc[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
X_grid = np.c_[xx.ravel(), yy.ravel()]
Z = nn.forward(X_grid)
Z = (Z > 0.5).astype(int).reshape(xx.shape)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].contourf(xx, yy, Z, alpha=0.3, cmap=plt.cm.RdBu)
axes[0].scatter(X_nc[:, 0], X_nc[:, 1], c=t_nc.ravel(), cmap=plt.cm.RdBu,
edgecolors='k', s=20)
axes[0].set_title('Neural Network Decision Boundary')
axes[0].set_xlabel('x1')
axes[0].set_ylabel('x2')
axes[0].grid(True, alpha=0.3)
axes[1].plot(losses)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].set_title('Training Loss')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('nonlinear_classification.png', dpi=150)
plt.close()
accuracy = np.mean((nn.forward(X_nc) > 0.5) == t_nc)
print(f"训练准确率: {accuracy:.4f}")
nonlinear_classification()5.2 回归任务
def neural_network_regression():
"""使用神经网络进行回归"""
np.random.seed(42)
# 生成非线性回归数据
N = 200
X_reg = np.sort(np.random.uniform(-3, 3, N)).reshape(-1, 1)
t_reg = np.sin(X_reg) + 0.1 * np.random.randn(N, 1)
# 训练回归网络
class RegressionNN:
def __init__(self, input_dim, hidden_dim):
self.W1 = np.random.randn(input_dim, hidden_dim) * np.sqrt(2.0/input_dim)
self.b1 = np.zeros(hidden_dim)
self.W2 = np.random.randn(hidden_dim, 1) * np.sqrt(2.0/hidden_dim)
self.b2 = np.zeros(1)
def forward(self, X):
self.X = X
self.z1 = X @ self.W1 + self.b1
self.a1 = np.tanh(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
return self.z2
def train(self, X, t, n_epochs=2000, lr=0.01):
losses = []
for _ in range(n_epochs):
y = self.forward(X)
loss = np.mean((y - t) ** 2)
losses.append(loss)
# 反向传播
delta2 = 2 * (y - t) / len(t)
delta1 = (delta2 @ self.W2.T) * (1 - self.a1**2)
self.W2 -= lr * self.a1.T @ delta2
self.b2 -= lr * delta2.sum(axis=0)
self.W1 -= lr * self.X.T @ delta1
self.b1 -= lr * delta1.sum(axis=0)
return losses
reg_nn = RegressionNN(1, 32)
losses = reg_nn.train(X_reg, t_reg, n_epochs=2000, lr=0.05)
# 可视化
X_plot = np.linspace(-3, 3, 200).reshape(-1, 1)
y_plot = reg_nn.forward(X_plot)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].scatter(X_reg, t_reg, alpha=0.3, label='Data')
axes[0].plot(X_plot, y_plot, 'r-', linewidth=2, label='NN Prediction')
axes[0].plot(X_plot, np.sin(X_plot), 'g--', linewidth=2, label='True function')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Neural Network Regression')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(losses)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('MSE Loss')
axes[1].set_title('Training Loss')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('nn_regression.png', dpi=150)
plt.close()
print(f"最终MSE: {losses[-1]:.6f}")
neural_network_regression()5.3 隐藏层可视化
def visualize_hidden_layer():
"""可视化隐藏层学到的表示"""
np.random.seed(42)
# 生成螺旋数据
N = 300
theta = np.linspace(0, 2*np.pi, N//2)
r = np.linspace(0.5, 2, N//2)
X_spiral = np.vstack([
np.column_stack([r*np.cos(theta), r*np.sin(theta)]),
np.column_stack([r*np.cos(theta+np.pi), r*np.sin(theta+np.pi)])
])
t_spiral = np.array([[0]]*(N//2) + [[1]]*(N//2))
# 训练网络
nn = NeuralNetworkBackprop(2, 8, 1)
nn.train(X_spiral, t_spiral, n_epochs=2000, lr=0.1)
# 获取隐藏层表示
nn.forward(X_spiral)
hidden_repr = nn.a1
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 原始空间
axes[0].scatter(X_spiral[:, 0], X_spiral[:, 1], c=t_spiral.ravel(),
cmap=plt.cm.RdBu, s=20, alpha=0.7)
axes[0].set_title('Original Space')
axes[0].set_xlabel('x1')
axes[0].set_ylabel('x2')
axes[0].grid(True, alpha=0.3)
# 隐藏层空间(取前两个神经元)
axes[1].scatter(hidden_repr[:, 0], hidden_repr[:, 1], c=t_spiral.ravel(),
cmap=plt.cm.RdBu, s=20, alpha=0.7)
axes[1].set_title('Hidden Layer Representation (Neurons 1 & 2)')
axes[1].set_xlabel('h1')
axes[1].set_ylabel('h2')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('hidden_layer_viz.png', dpi=150)
plt.close()
print("隐藏层将非线性可分的数据映射为线性可分的数据")
visualize_hidden_layer()四、本章小结
第5章系统介绍了前馈神经网络的理论基础:
网络结构:前馈网络通过多层非线性变换将输入映射到输出。隐藏层的数量(深度)和宽度决定了网络的表达能力。
激活函数:非线性激活函数是神经网络表示复杂函数的关键。ReLU因其计算简单和缓解梯度消失的优势,成为最常用的隐藏层激活函数。
反向传播:基于链式法则的高效梯度计算方法。通过计算图将复杂网络分解为简单操作的组合,逐层向后传播梯度。
正则化:权重衰减(L2)、早停法、Dropout等技术防止神经网络过拟合。正则化对于泛化能力至关重要。
贝叶斯神经网络:将贝叶斯方法应用于神经网络,提供预测的不确定性量化。MC Dropout是一种实用的近似方法。
神经网络是现代深度学习的基石。后续的第6章核方法可以理解为无穷宽神经网络的极限情况,第8章图模型提供了更灵活的网络结构,第10章的近似推断方法为训练大型神经网络提供了理论基础。
五、思考与练习
思考题
通用逼近定理保证了神经网络可以逼近任意函数,但为什么在实践中我们仍然需要仔细设计网络结构?
比较ReLU和sigmoid激活函数的优缺点。为什么在深层网络中ReLU更受欢迎?
反向传播算法的时间复杂度是多少?与数值梯度计算相比有什么优势?
Dropout为什么能防止过拟合?它与集成学习有什么关系?
贝叶斯神经网络的后验分布为什么难以精确计算?有哪些近似方法?
编程练习
批量归一化:在两层神经网络中实现Batch Normalization,比较加入BN前后的训练速度和最终精度。
学习率调度:实现学习率衰减策略(如指数衰减、余弦退火),观察其对训练过程的影响。
网络深度实验:训练不同深度(1-5个隐藏层)的神经网络,观察训练损失和测试损失的变化,分析梯度消失现象。