5模式识别与机器学习

神经网络

非线性模型

前馈网络反向传播激活函数

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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)$ 是输出层激活函数。

python
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}$
python
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$$

python
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:标准化每层的输入
  • 残差连接:提供梯度的"高速公路"
python
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)

将数据分为训练集和验证集。训练过程中监控验证集误差,当验证集误差开始增大时停止训练。

早停法隐式地起到了正则化的效果:限制训练步数等价于限制参数空间的有效大小。

python
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在训练时随机将一部分神经元的输出置为零,迫使网络学习更鲁棒的特征。

python
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)。

python
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 非线性分类问题

python
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 回归任务

python
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 隐藏层可视化

python
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个隐藏层)的神经网络,观察训练损失和测试损失的变化,分析梯度消失现象。


5/10神经网络
0%