承接感知机及其向多层感知机(MLP)的演进,本课将深入探讨神经网络的训练、优化与正则化这一核心模块,这是将理论模型转化为实用系统的关键。
课程内容将严格遵循“完整知识点介绍”的要求,聚焦于核心概念、数学原理、关键算法和实战代码 。
一、 神经网络训练的核心:梯度下降及其变体
梯度下降是优化神经网络参数的基石算法。其目标是找到一组参数 $\theta$(包含所有权重 $W$ 和偏置 $b$),以最小化损失函数 $J(\theta)$。
1. 批量梯度下降(Batch Gradient Descent)
- 公式:$\theta \leftarrow \theta - \eta \cdot
abla_{\theta} J(\theta)$ - 特点:每次更新使用整个训练集计算梯度。梯度方向准确,但计算开销大,内存要求高,且难以处理在线数据 。
- 代码示意:
# 假设 X_all, y_all 为整个训练集 for epoch in range(num_epochs): gradients = compute_gradients(X_all, y_all, theta) # 计算全量梯度 theta -= learning_rate * gradients # 更新参数
2. 随机梯度下降(Stochastic Gradient Descent, SGD)
- 公式:$\theta \leftarrow \theta - \eta \cdot
abla_{\theta} J(\theta; x^{(i)}, y^{(i)})$ - 特点:每次更新随机使用一个样本计算梯度。更新频繁,收敛速度快,可以跳出局部极小点,但梯度估计噪声大,导致损失函数剧烈震荡 。
- 代码示意:
for epoch in range(num_epochs): np.random.shuffle(training_data) # 打乱数据 for x_i, y_i in training_data: gradient = compute_gradients(x_i, y_i, theta) # 计算单个样本梯度 theta -= learning_rate * gradient
3. 小批量梯度下降(Mini-batch Gradient Descent)
- 公式:$\theta \leftarrow \theta - \eta \cdot
abla_{\theta} J(\theta; X^{(i:i+n)}, y^{(i:i+n)})$ - 特点:实践中最常用。每次更新使用一个小的随机样本子集(Mini-batch)。在梯度估计的准确性和更新速度之间取得了平衡,且能利用GPU的并行计算优势 。
- 代码示意:
batch_size = 32 for epoch in range(num_epochs): for i in range(0, len(X), batch_size): X_batch = X[i:i+batch_size] y_batch = y[i:i+batch_size] gradients = compute_gradients(X_batch, y_batch, theta) theta -= learning_rate * gradients
二、 高级优化算法
基础的SGD存在收敛慢、易陷于鞍点等问题。以下优化器通过引入动量、自适应学习率等机制进行改进。
| 优化器 | 核心思想与更新规则(向量形式) | 超参数 | 优点与适用场景 |
|---|---|---|---|
| SGD with Momentum | 引入动量变量 $v$ 累积历史梯度方向,加速在稳定方向的移动并抑制震荡。 $v_t \leftarrow \beta v_{t-1} + \eta | ||
| abla_{\theta} J(\theta)$ $\theta \leftarrow \theta - v_t$ | $\eta$: 学习率 $\beta$: 动量系数 (常取0.9) | 加速收敛,减少震荡,有助于穿越平缓区和局部极小点。 | |
| AdaGrad | 为每个参数自适应地调整学习率,累积历史梯度平方和,对频繁更新的参数减小步长。 $s_t \leftarrow s_{t-1} + ( | ||
| abla_{\theta} J(\theta))^2$ $\theta \leftarrow \theta - \frac{\eta}{\sqrt{s_t + \epsilon}} \odot | |||
| abla_{\theta} J(\theta)$ | $\eta$, $\epsilon$ (小常数,防除零) | 适合稀疏数据,但学习率可能过早衰减至零。 | |
| RMSProp | 改进AdaGrad,使用指数移动平均衰减历史梯度平方,解决学习率急剧下降问题。 $s_t \leftarrow \beta s_{t-1} + (1-\beta)( | ||
| abla_{\theta} J(\theta))^2$ $\theta \leftarrow \theta - \frac{\eta}{\sqrt{s_t + \epsilon}} \odot | |||
| abla_{\theta} J(\theta)$ | $\eta$, $\beta$, $\epsilon$ | 缓解AdaGrad的学习率衰减问题,是深度学习中常用的自适应方法之一。 | |
| Adam (最常用) | 结合了Momentum(一阶矩估计)和RMSProp(二阶矩估计),并进行偏差校正。 $m_t \leftarrow \beta_1 m_{t-1} + (1-\beta_1) | ||
| abla_{\theta} J(\theta)$ $v_t \leftarrow \beta_2 v_{t-1} + (1-\beta_2)( | |||
| abla_{\theta} J(\theta))^2$ $\hat{m}_t \leftarrow m_t / (1-\beta_1^t)$, $\hat{v}_t \leftarrow v_t / (1-\beta_2^t)$ $\theta \leftarrow \theta - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$ | $\eta$, $\beta_1$, $\beta_2$, $\epsilon$ | 默认推荐。通常收敛快,对超参数选择相对鲁棒,能处理稀疏梯度和非平稳目标。 |
三、 正则化:防止过拟合的关键技术
当模型在训练集上表现很好但在未见数据上表现糟糕时,即发生了过拟合。正则化技术旨在约束模型复杂度,提升泛化能力。
1. L1 与 L2 正则化(权重衰减)
- 原理:在原始损失函数 $J(\theta)$ 上添加一个与权重大小相关的惩罚项。
- L2正则化(岭回归):
$$
J_{\text{reg}}(\theta) = J(\theta) + \frac{\lambda}{2} |\theta|2^2
$$
其中 $\lambda$ 是正则化强度。其梯度为 $
abla{\theta} J_{\text{reg}} =
abla_{\theta} J + \lambda \theta$。更新时相当于在标准SGD更新前先将权重乘以 $(1 - \eta \lambda)$,因此也称权重衰减。它倾向于使权重平滑地趋向于零,但不一定为零 。 - L1正则化(Lasso):
$$
J_{\text{reg}}(\theta) = J(\theta) + \lambda |\theta|_1
$$
其梯度(次梯度)包含 $\text{sign}(\theta)$。L1正则化倾向于产生稀疏解,即让一部分权重精确为零,实现特征选择 。 - 代码实现(在损失计算和梯度更新中):
# 在前向传播计算损失时加入L2惩罚项 def compute_loss_with_l2(self, y_pred, y_true, lambda_reg=0.001): cross_entropy_loss = -np.mean(y_true * np.log(y_pred) + (1-y_true)*np.log(1-y_pred)) l2_penalty = 0.5 * lambda_reg * (np.sum(self.W1**2) + np.sum(self.W2**2)) total_loss = cross_entropy_loss + l2_penalty return total_loss # 在反向传播更新权重时,梯度需加上正则化项的导数 # dW2 = ... (原始梯度) + lambda_reg * self.W2 # self.W2 -= lr * dW2
2. Dropout
- 原理:在训练阶段,以前向传播的每一步,以概率 $p$(如0.5)随机将网络中隐藏层的神经元输出置零(“丢弃”)。在测试阶段,使用所有神经元,但权重需要乘以 $1-p$ 以保持期望输出一致 。
- 作用:防止神经元之间产生复杂的共适应关系,强制网络学习更鲁棒的特征,是一种高效的模型平均方法。
- 代码实现:
class DropoutLayer: def __init__(self, dropout_rate): self.dropout_rate = dropout_rate self.mask = None def forward(self, X, is_training=True): if is_training: # 生成与X同形的二进制掩码 self.mask = np.random.rand(*X.shape) > self.dropout_rate return X * self.mask / (1.0 - self.dropout_rate) # 缩放保持期望 else: return X # 测试时直接通过 def backward(self, dout): return dout * self.mask / (1.0 - self.dropout_rate) # 反向传播时同样屏蔽梯度
3. 早停法(Early Stopping)
- 原理:在训练过程中,持续监控模型在验证集上的性能(如损失或准确率)。当验证集性能在连续多个周期(耐心值)内不再提升时,停止训练,并回滚到验证集性能最佳时的模型参数 。
- 实现:这是最简单有效的正则化方法之一,无需修改损失函数或网络结构。
四、 权重初始化策略
不恰当的初始化(如全零初始化)会导致对称性破坏失败,所有神经元学习到相同的特征。良好的初始化能加速收敛,缓解梯度消失/爆炸。
| 初始化方法 | 公式(对于层 $l$,$n_{in}$ 为输入维度,$n_{out}$ 为输出维度) | 适用场景 |
|---|---|---|
| Xavier/Glorot初始化 | $W \sim \mathcal{N}(0, \sqrt{\frac{2}{n_{in} + n_{out}}})$ 或均匀分布 $U(-\sqrt{\frac{6}{n_{in}+n_{out}}}, \sqrt{\frac{6}{n_{in}+n_{out}}})$ | 适用于Sigmoid、Tanh等S型激活函数,旨在保持各层激活值的方差稳定。 |
| He初始化 | $W \sim \mathcal{N}(0, \sqrt{\frac{2}{n_{in}}})$ 或均匀分布 $U(-\sqrt{\frac{6}{n_{in}}}, \sqrt{\frac{6}{n_{in}}})$ | 专为ReLU及其变体设计,是当前使用ReLU网络的标准初始化方法 。 |
五、 批量归一化(Batch Normalization)
批量归一化通过对每一层的输入进行归一化处理,解决了内部协变量偏移问题,带来了诸多好处 。
1. 算法步骤(训练阶段)
对于一个小批量数据 $\mathcal{B} = {x_1...m}$:
- 计算小批量均值:$\mu_\mathcal{B} \leftarrow \frac{1}{m} \sum_{i=1}^{m} x_i$
- 计算小批量方差:$\sigma_\mathcal{B}^2 \leftarrow \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_\mathcal{B})^2$
- 归一化:$\hat{x}i \leftarrow \frac{x_i - \mu\mathcal{B}}{\sqrt{\sigma_\mathcal{B}^2 + \epsilon}}$
- 缩放与偏移:$y_i \leftarrow \gamma \hat{x}i + \beta \equiv \text{BN}{\gamma,\beta}(x_i)$
其中 $\gamma$ 和 $\beta$ 是可学习的参数,用于恢复网络的表示能力。
2. 测试阶段
使用训练阶段估算的全局移动平均值$\mathbb{E}[x]$ 和 $\text{Var}[x]$ 代替小批量统计量。
3. 优点
- 允许使用更高的学习率。
- 降低对权重初始化的敏感度。
- 起到轻微的正则化作用(与小批量统计的噪声有关)。
六、 综合实战:一个带有优化与正则化的MLP
以下代码整合了上述部分关键技术。
import numpy as np class EnhancedMLP: def __init__(self, layer_dims, learning_rate=0.01, lambda_l2=0.0, dropout_rate=0.0, initialization='he'): """ 初始化一个多层的MLP。 参数: layer_dims: 列表,如 [input_size, hidden1, hidden2, ..., output_size] learning_rate: 学习率 lambda_l2: L2正则化系数 dropout_rate: Dropout概率 (0表示不使用) initialization: 初始化方法 ('he' 或 'xavier') """ self.parameters = {} self.L = len(layer_dims) - 1 self.lambda_l2 = lambda_l2 self.dropout_rate = dropout_rate self.lr = learning_rate # 初始化权重和偏置 for l in range(1, self.L + 1): n_in, n_out = layer_dims[l-1], layer_dims[l] if initialization == 'he': scale = np.sqrt(2.0 / n_in) elif initialization == 'xavier': scale = np.sqrt(2.0 / (n_in + n_out)) else: scale = 0.01 self.parameters['W' + str(l)] = np.random.randn(n_in, n_out) * scale self.parameters['b' + str(l)] = np.zeros((1, n_out)) def relu(self, Z): return np.maximum(0, Z) def relu_backward(self, dA, Z): dZ = np.array(dA, copy=True) dZ[Z <= 0] = 0 return dZ def sigmoid(self, Z): return 1 / (1 + np.exp(-Z)) def forward_with_dropout(self, X, is_training=True): """ 带Dropout的前向传播。 """ caches = [] A = X L = self.L for l in range(1, L): # 隐藏层 W = self.parameters['W' + str(l)] b = self.parameters['b' + str(l)] Z = np.dot(A, W) + b A = self.relu(Z) if is_training and self.dropout_rate > 0: # Dropout 实现 D = (np.random.rand(*A.shape) > self.dropout_rate).astype(float) A = A * D A = A / (1.0 - self.dropout_rate) # 缩放激活值 caches.append((Z, D)) else: caches.append((Z, None)) # 输出层 (无Dropout) W = self.parameters['W' + str(L)] b = self.parameters['b' + str(L)] Z = np.dot(A, W) + b AL = self.sigmoid(Z) # 假设二分类 caches.append((Z, None)) return AL, caches def compute_cost(self, AL, Y): """ 计算带L2正则化的损失。 """ m = Y.shape[0] cross_entropy_cost = -np.mean(Y * np.log(AL + 1e-8) + (1-Y) * np.log(1-AL + 1e-8)) # 计算L2正则化项 l2_cost = 0 for l in range(1, self.L+1): W = self.parameters['W' + str(l)] l2_cost += np.sum(np.square(W)) l2_cost = (self.lambda_l2 / (2 * m)) * l2_cost total_cost = cross_entropy_cost + l2_cost return total_cost def backward_with_dropout(self, AL, Y, caches): """ 结合Dropout和L2正则化的反向传播。 """ grads = {} m = Y.shape[0] L = self.L # 初始化反向传播 dAL = - (np.divide(Y, AL + 1e-8) - np.divide(1 - Y, 1 - AL + 1e-8)) # BCE损失的导数 # 输出层梯度 (第L层) current_cache = caches[L-1] Z, _ = current_cache s = self.sigmoid(Z) dZ = dAL * s * (1-s) # 对于Sigmoid, dZ = dA * s*(1-s) A_prev = caches[L-2][0] if L>1 else self.caches_input # 需要保存输入A dW = np.dot(A_prev.T, dZ) / m + (self.lambda_l2 / m) * self.parameters['W' + str(L)] # L2正则化项 db = np.sum(dZ, axis=0, keepdims=True) / m grads['dW' + str(L)] = dW grads['db' + str(L)] = db dA_prev = np.dot(dZ, self.parameters['W' + str(L)].T) # 隐藏层梯度 (第L-1层到第1层) for l in reversed(range(1, L)): current_cache = caches[l-1] Z, D = current_cache dA = dA_prev # 应用Dropout掩码 if D is not None: dA = dA * D dA = dA / (1.0 - self.dropout_rate) dZ = self.relu_backward(dA, Z) if l == 1: A_prev = self.caches_input else: A_prev = caches[l-2][0] dW = np.dot(A_prev.T, dZ) / m + (self.lambda_l2 / m) * self.parameters['W' + str(l)] db = np.sum(dZ, axis=0, keepdims=True) / m grads['dW' + str(l)] = dW grads['db' + str(l)] = db dA_prev = np.dot(dZ, self.parameters['W' + str(l)].T) return grads def update_parameters(self, grads, optimizer='sgd', beta1=0.9, beta2=0.999, epsilon=1e-8): """ 使用不同的优化器更新参数。 """ if not hasattr(self, 'v'): self.v = {} self.s = {} for l in range(1, self.L+1): self.v['dW' + str(l)] = np.zeros_like(self.parameters['W' + str(l)]) self.v['db' + str(l)] = np.zeros_like(self.parameters['b' + str(l)]) self.s['dW' + str(l)] = np.zeros_like(self.parameters['W' + str(l)]) self.s['db' + str(l)] = np.zeros_like(self.parameters['b' + str(l)]) self.t = 0 self.t += 1 L = self.L for l in range(1, L+1): if optimizer == 'momentum': # Momentum update self.v['dW' + str(l)] = beta1 * self.v['dW' + str(l)] + (1-beta1) * grads['dW' + str(l)] self.v['db' + str(l)] = beta1 * self.v['db' + str(l)] + (1-beta1) * grads['db' + str(l)] self.parameters['W' + str(l)] -= self.lr * self.v['dW' + str(l)] self.parameters['b' + str(l)] -= self.lr * self.v['db' + str(l)] elif optimizer == 'adam': # Adam update # 动量 self.v['dW' + str(l)] = beta1 * self.v['dW' + str(l)] + (1-beta1) * grads['dW' + str(l)] self.v['db' + str(l)] = beta1 * self.v['db' + str(l)] + (1-beta1) * grads['db' + str(l)] # RMSProp self.s['dW' + str(l)] = beta2 * self.s['dW' + str(l)] + (1-beta2) * np.square(grads['dW' + str(l)]) self.s['db' + str(l)] = beta2 * self.s['db' + str(l)] + (1-beta2) * np.square(grads['db' + str(l)]) # 偏差校正 v_corrected_dW = self.v['dW' + str(l)] / (1 - np.power(beta1, self.t)) v_corrected_db = self.v['db' + str(l)] / (1 - np.power(beta1, self.t)) s_corrected_dW = self.s['dW' + str(l)] / (1 - np.power(beta2, self.t)) s_corrected_db = self.s['db' + str(l)] / (1 - np.power(beta2, self.t)) # 参数更新 self.parameters['W' + str(l)] -= self.lr * v_corrected_dW / (np.sqrt(s_corrected_dW) + epsilon) self.parameters['b' + str(l)] -= self.lr * v_corrected_db / (np.sqrt(s_corrected_db) + epsilon) else: # Vanilla SGD self.parameters['W' + str(l)] -= self.lr * grads['dW' + str(l)] self.parameters['b' + str(l)] -= self.lr * grads['db' + str(l)] def train(self, X, Y, epochs=1000, optimizer='adam', verbose=True): self.caches_input = X costs = [] for i in range(epochs): # 前向传播 AL, caches = self.forward_with_dropout(X, is_training=True) # 计算成本 cost = self.compute_cost(AL, Y) costs.append(cost) # 反向传播 grads = self.backward_with_dropout(AL, Y, caches) # 更新参数 self.update_parameters(grads, optimizer=optimizer) if verbose and i % 100 == 0: print(f"Epoch {i}, Cost: {cost:.4f}") return costs # 使用示例 # model = EnhancedMLP(layer_dims=[2, 10, 5, 1], learning_rate=0.01, lambda_l2=0.001, dropout_rate=0.2, initialization='he') # costs = model.train(X_train, Y_train, epochs=2000, optimizer='adam')七、 关键概念与难点解析
- 梯度消失与爆炸:在深层网络中,反向传播时梯度会连乘各层权重。如果权重矩阵的特征值持续大于1,梯度会指数爆炸;如果持续小于1,梯度会指数消失。使用ReLU、恰当的初始化(如He初始化)、批量归一化、梯度裁剪(针对爆炸)和残差连接是主要的解决手段。
- 超参数调优:学习率、批次大小、正则化系数、网络深度与宽度、优化器参数等都需要调整。常用方法包括网格搜索、随机搜索和贝叶斯优化。学习率调度(如指数衰减、余弦退火)也很重要。
- 训练、验证与测试集:必须将数据分为三部分。训练集用于更新参数;验证集用于模型选择、超参数调优和早停;测试集仅用于最终评估模型泛化性能,在整个训练过程中应绝对不可见。
- 归一化与标准化:输入特征通常需要进行预处理,如标准化(减均值除标准差)或归一化(缩放到[0,1]区间),这可以加速训练并提高模型稳定性。
八、 习题
- 推导题:请推导出带有L2正则化的交叉熵损失函数 $J_{\text{reg}}(\theta)$ 对权重参数 $w_{ij}^{(l)}$ 的梯度 $\frac{\partial J_{\text{reg}}}{\partial w_{ij}^{(l)}}$,并解释权重衰减项如何影响更新过程。
- 分析题:比较SGD、Momentum、AdaGrad、RMSProp和Adam优化器在收敛速度、内存占用、超参数敏感性方面的优缺点。在什么情况下你会选择SGD而不是Adam?
- 编程题:实现一个简单的学习率调度器(例如,每过一定轮数将学习率减半),并将其集成到上面的
EnhancedMLP类的update_parameters方法中。 - 综合题:设计一个实验,使用合成数据集,对比研究以下因素对MLP最终测试准确率的影响:(a) 不使用任何正则化;(b) 仅使用L2正则化;(c) 仅使用Dropout;(d) 同时使用L2和Dropout。分析并解释你的实验结果。
参考来源
- 感知机(Perceptron)-----最详细记录感知机
- 深度学习笔记(一)——感知机模型(Perceptron Model)
- 两层感知机解决异或(XOR)问题
- 深度学习中的感知机
- 感知机介绍及MATLAB实现
- 深度学习-感知机