1. 神经网络基础概念回顾
在正式进入浅层神经网络的具体内容之前,我们需要先明确几个核心概念。神经网络本质上是一种模仿生物神经网络结构和功能的数学模型,由大量相互连接的节点(或称"神经元")组成。每个神经元接收输入信号,经过加权求和后通过激活函数产生输出。
第三周课程中提到的"浅层神经网络"通常指的是仅包含一个隐藏层的网络结构。这种网络虽然结构简单,但已经能够解决许多非线性分类问题。与后续要学习的深层网络相比,浅层网络的计算复杂度更低,训练速度更快,是初学者理解神经网络工作原理的理想切入点。
提示:虽然称为"浅层",但单隐藏层网络理论上已经能够以任意精度逼近任何连续函数(通用逼近定理),关键在于隐藏层神经元的数量。
2. 浅层神经网络的结构解析
2.1 网络层级组成
一个典型的浅层神经网络包含三层:
- 输入层(Input layer):接收原始数据特征
- 隐藏层(Hidden layer):进行特征变换和非线性映射
- 输出层(Output layer):产生最终预测结果
以二分类问题为例,假设我们有nx个输入特征,隐藏层有n[1]个神经元,那么各层的维度为:
- 输入层:nx
- 隐藏层:n[1]
- 输出层:1(二分类输出一个0-1之间的概率值)
2.2 参数矩阵的维度分析
理解参数矩阵的维度对于正确实现神经网络至关重要。对于上述网络结构:
- W[1]:隐藏层权重矩阵,维度为(n[1], nx)
- b[1]:隐藏层偏置向量,维度为(n[1], 1)
- W[2]:输出层权重矩阵,维度为(1, n[1])
- b[2]:输出层偏置标量,维度为(1, 1)
在实际编程实现时,务必检查这些矩阵的维度是否正确,这是避免许多错误的第一个检查点。
3. 前向传播的数学原理与实现
3.1 单样本前向传播公式
对于单个样本x(维度nx×1),前向传播的计算步骤如下:
隐藏层计算: z[1] = W[1]x + b[1] a[1] = g 1
输出层计算: z[2] = W[2]a[1] + b[2] a[2] = g 2
其中g[1]和g[2]分别是隐藏层和输出层的激活函数。常见的组合是隐藏层使用ReLU或tanh,输出层使用sigmoid(二分类问题)。
3.2 多样本向量化实现
在实际应用中,我们通常同时处理多个样本(m个)以提高计算效率。这时需要将上述计算向量化:
- 将m个样本按列堆叠成矩阵X(维度nx×m)
- 隐藏层计算: Z[1] = W[1]X + b[1] A[1] = g 1
- 输出层计算: Z[2] = W[2]A[1] + b[2] A[2] = g 2
注意:这里的b[1]和b[2]会通过python的广播机制自动扩展到所有样本,这是NumPy的一个便利特性。
4. 激活函数的选择与比较
4.1 常用激活函数及其导数
Sigmoid函数: σ(z) = 1/(1+e^-z) σ'(z) = σ(z)(1-σ(z))
适用于输出层二分类问题,但隐藏层中容易导致梯度消失。
tanh函数: tanh(z) = (e^z - e^-z)/(e^z + e^-z) tanh'(z) = 1 - tanh²(z)
通常比sigmoid表现更好,因为输出以0为中心。
ReLU函数: ReLU(z) = max(0,z) ReLU'(z) = 1 if z>0 else 0
目前最常用的隐藏层激活函数,计算简单且能缓解梯度消失问题。
4.2 激活函数选择建议
输出层:
- 二分类:sigmoid
- 回归问题:线性(无激活)或ReLU(输出非负时)
隐藏层:
- 首选ReLU(计算高效且梯度保持良好)
- 次选tanh(某些特定场景可能表现更好)
- 避免使用sigmoid(梯度消失问题严重)
5. 反向传播算法详解
5.1 损失函数计算
对于二分类问题,常用交叉熵损失函数: L(ŷ,y) = -[ylog(ŷ) + (1-y)log(1-ŷ)]
对于m个样本,成本函数为: J = (1/m)ΣL(ŷ(i),y(i))
5.2 梯度计算步骤
反向传播的核心是计算成本函数对各参数的偏导数:
输出层梯度: dZ[2] = A[2] - Y dW[2] = (1/m)dZ[2]A[1]ᵀ db[2] = (1/m)ΣdZ[2]
隐藏层梯度: dZ[1] = W[2]ᵀdZ[2] * g[1]'(Z[1]) dW[1] = (1/m)dZ[1]Xᵀ db[1] = (1/m)ΣdZ[1]
5.3 参数更新规则
使用梯度下降法更新参数: W[1] = W[1] - αdW[1] b[1] = b[1] - αdb[1] W[2] = W[2] - αdW[2] b[2] = b[2] - αdb[2]
其中α是学习率,控制每次更新的步长。
6. 参数初始化技巧
6.1 随机初始化的必要性
如果将所有权重初始化为0,会导致所有神经元学习相同的特征,失去多层网络的意义。因此必须进行随机初始化。
6.2 推荐的初始化方法
小随机数初始化: W = np.random.randn(n[l],n[l-1]) * 0.01
适用于浅层网络,但深层网络可能需要更复杂的初始化。
Xavier初始化(tanh激活): W = np.random.randn(n[l],n[l-1]) * sqrt(1/n[l-1])
He初始化(ReLU激活): W = np.random.randn(n[l],n[l-1]) * sqrt(2/n[l-1])
提示:偏置项b通常初始化为0,因为不对称性主要由权重随机初始化提供。
7. 超参数调优实践
7.1 学习率选择
学习率α是最关键的超参数之一:
- 太大:可能无法收敛,成本函数震荡甚至发散
- 太小:收敛速度过慢
建议尝试对数尺度上的值:0.001, 0.003, 0.01, 0.03, 0.1等
7.2 隐藏单元数量
隐藏层神经元数量n[1]决定模型的容量:
- 太少:欠拟合,无法捕捉复杂模式
- 太多:过拟合,计算成本增加
可以从较小值(如32、64)开始,根据验证集表现逐步增加。
7.3 迭代次数
训练迭代次数需要平衡:
- 不足:欠拟合
- 过多:浪费时间且可能导致过拟合
使用早停法(early stopping)可以在验证集误差不再下降时终止训练。
8. 常见问题与调试技巧
8.1 梯度检查
在实现反向传播时,建议先进行梯度检查(gradient checking)来验证梯度计算的正确性:
计算数值梯度: grad_approx = (J(θ+ε) - J(θ-ε))/(2ε)
与解析梯度比较: diff = ||grad - grad_approx||₂ / (||grad||₂ + ||grad_approx||₂)
如果diff < 1e-7,通常认为实现正确。
注意:梯度检查仅用于调试,正式训练时应关闭,因为它计算代价很高。
8.2 成本函数不下降的可能原因
- 学习率太小
- 初始化不当导致梯度消失
- 实现中存在bug(如梯度计算错误)
- 正则化过强
8.3 过拟合应对策略
- 增加训练数据
- 应用L2正则化
- 使用dropout(虽然浅层网络通常不需要)
- 减少隐藏单元数量
9. 完整实现示例(Python)
以下是一个浅层神经网络的简化实现框架:
import numpy as np def initialize_parameters(n_x, n_h, n_y): W1 = np.random.randn(n_h, n_x) * 0.01 b1 = np.zeros((n_h, 1)) W2 = np.random.randn(n_y, n_h) * 0.01 b2 = np.zeros((n_y, 1)) return {"W1": W1, "b1": b1, "W2": W2, "b2": b2} def forward_propagation(X, parameters): W1, b1, W2, b2 = parameters["W1"], parameters["b1"], parameters["W2"], parameters["b2"] Z1 = np.dot(W1, X) + b1 A1 = np.tanh(Z1) Z2 = np.dot(W2, A1) + b2 A2 = sigmoid(Z2) cache = {"Z1": Z1, "A1": A1, "Z2": Z2, "A2": A2} return A2, cache def compute_cost(A2, Y): m = Y.shape[1] logprobs = np.multiply(np.log(A2), Y) + np.multiply(np.log(1 - A2), (1 - Y)) cost = -np.sum(logprobs) / m return np.squeeze(cost) def backward_propagation(parameters, cache, X, Y): m = X.shape[1] W1, W2 = parameters["W1"], parameters["W2"] A1, A2 = cache["A1"], cache["A2"] dZ2 = A2 - Y dW2 = np.dot(dZ2, A1.T) / m db2 = np.sum(dZ2, axis=1, keepdims=True) / m dZ1 = np.dot(W2.T, dZ2) * (1 - np.power(A1, 2)) dW1 = np.dot(dZ1, X.T) / m db1 = np.sum(dZ1, axis=1, keepdims=True) / m grads = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2} return grads def update_parameters(parameters, grads, learning_rate): W1 = parameters["W1"] - learning_rate * grads["dW1"] b1 = parameters["b1"] - learning_rate * grads["db1"] W2 = parameters["W2"] - learning_rate * grads["dW2"] b2 = parameters["b2"] - learning_rate * grads["db2"] return {"W1": W1, "b1": b1, "W2": W2, "b2": b2} def sigmoid(z): return 1/(1+np.exp(-z))10. 实际应用中的注意事项
数据预处理:
- 确保输入特征尺度相近(标准化或归一化)
- 分类标签y应为0或1(二分类问题)
训练监控:
- 记录训练集和验证集的成本函数值
- 绘制学习曲线判断是否欠拟合或过拟合
随机性控制:
- 固定随机种子(如np.random.seed(1))以便复现结果
- 多次随机初始化避免局部最优
性能优化:
- 使用向量化操作避免显式循环
- 对于大数据集考虑mini-batch梯度下降
在实际项目中实现浅层神经网络时,我发现最常遇到的问题往往不是算法本身,而是数据预处理和参数初始化的细节。例如,当输入特征尺度差异很大时,如果不进行标准化,某些权重可能需要非常小的学习率才能稳定更新,这会显著拖慢训练速度。另一个常见陷阱是在计算梯度时忘记除以m(样本数量),导致更新步长过大。