共计 2868 个字符,预计需要花费 8 分钟才能阅读完成。
神经网络训练的核心流程
神经网络的训练本质是通过前向传播计算预测值,反向传播计算梯度,最后用优化器更新参数的三步循环。数学上依赖链式法则求导和梯度下降算法,核心目标是让损失函数对参数的偏导数指导权重调整方向。整个过程需要处理矩阵运算的维度匹配、非线性激活的导数计算以及数值稳定性控制。

新手常见痛点分析
梯度消失 / 爆炸问题
当网络层数较深时,梯度在反向传播过程中连续乘以小于 1 的权重会导致梯度指数级减小(消失),而大于 1 的权重则会导致梯度指数级增大(爆炸)。典型场景如使用 Sigmoid 激活函数时,其导数值最大仅 0.25,多层连乘后梯度极易消失。
学习率设置误区
- 过大学习率会导致参数在最优解附近震荡甚至发散
- 过小学习率使得训练速度缓慢且可能陷入局部最优
- 固定学习率难以适应不同参数层的更新需求
激活函数选择陷阱
- Sigmoid 在极端值区梯度接近零(饱和区)
- Tanh 虽然零中心化但仍有梯度消失问题
- ReLU 的死亡神经元现象(负区间梯度为零)
5.1 版本神经网络实现
类结构定义
import numpy as np
class NeuralNetwork_5_1:
"""
输入层维度: input_size
隐藏层维度: hidden_size
输出层维度: output_size
"""
def __init__(self, input_size, hidden_size, output_size):
# 参数初始化(后续会讲 Xavier 方法)self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros((1, output_size))
前向传播实现
def forward(self, X):
"""
输入: X (batch_size, input_size)
输出: A2 (batch_size, output_size)
"""
# 第一层计算
self.Z1 = np.dot(X, self.W1) + self.b1 # (batch_size, hidden_size)
self.A1 = np.tanh(self.Z1) # 使用 tanh 激活
# 输出层计算
self.Z2 = np.dot(self.A1, self.W2) + self.b2 # (batch_size, output_size)
self.A2 = self._sigmoid(self.Z2) # 二分类输出用 sigmoid
return self.A2
反向传播关键步骤
def backward(self, X, y, learning_rate):
"""
输入:
X (batch_size, input_size)
y (batch_size, 1)
"""
m = X.shape[0] # 样本数
# 输出层梯度
dZ2 = self.A2 - y # (batch_size, output_size)
dW2 = np.dot(self.A1.T, dZ2) / m # (hidden_size, output_size)
db2 = np.sum(dZ2, axis=0, keepdims=True) / m
# 隐藏层梯度
dA1 = np.dot(dZ2, self.W2.T) # (batch_size, hidden_size)
dZ1 = dA1 * (1 - np.power(self.A1, 2)) # tanh 导数
dW1 = np.dot(X.T, dZ1) / m # (input_size, hidden_size)
db1 = np.sum(dZ1, axis=0, keepdims=True) / m
# 参数更新
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
SGD 优化器实现
def train(self, X, y, epochs, lr):
"""
输入:
X (n_samples, input_size)
y (n_samples, 1)
"""
losses = []
for epoch in range(epochs):
# 前向传播
output = self.forward(X)
# 计算损失(交叉熵)loss = -np.mean(y*np.log(output) + (1-y)*np.log(1-output))
losses.append(loss)
# 反向传播
self.backward(X, y, lr)
if epoch % 100 == 0:
print(f'Epoch {epoch}, Loss: {loss:.4f}')
return losses
避坑指南
梯度检查实现
def gradient_check(self, X, y, epsilon=1e-7):
# 计算解析梯度
self.forward(X)
self.backward(X, y, lr=0.01) # 不实际更新参数
# 对 W1 做数值梯度近似
grad_approx = np.zeros_like(self.W1)
for i in range(self.W1.shape[0]):
for j in range(self.W1.shape[1]):
# 正向扰动
self.W1[i,j] += epsilon
loss_plus = self._compute_loss(X, y)
# 负向扰动
self.W1[i,j] -= 2*epsilon
loss_minus = self._compute_loss(X, y)
# 恢复原值
self.W1[i,j] += epsilon
# 中心差分
grad_approx[i,j] = (loss_plus - loss_minus) / (2*epsilon)
# 比较差异
difference = np.linalg.norm(grad_approx - self.dW1) / \
(np.linalg.norm(grad_approx) + np.linalg.norm(self.dW1))
print(f"Gradient check: {difference}")
Loss 震荡调试
- 优先检查学习率:尝试将当前学习率除以 3 或 10
- 检查数据是否未做归一化(特别是输入值范围差异大时)
- 考虑添加动量(Momentum)项平滑更新方向
参数初始化经验
- 使用 Xavier 初始化:
W = np.random.randn(fan_in, fan_out) * np.sqrt(1/fan_in) - ReLU 系列激活建议用 He 初始化:
sqrt(2/fan_in) - 避免全零初始化(会导致对称性问题)
思考题
-
如何改进代码支持 mini-batch 训练?
需要增加数据 shuffle 逻辑,并在迭代时按 batch_size 切片数据 -
如果遇到 NaN 损失值应该检查哪些点?
- 检查是否存在 log(0) 情况(可加 epsilon 平滑)
- 验证梯度爆炸(添加梯度裁剪)
-
确认输入数据是否含非法值
-
为什么 ReLU 比 Sigmoid 更适合深层网络?
- 单边抑制特性缓解梯度消失
- 计算效率高(无需指数运算)
- 稀疏激活特性符合生物学认知
正文完
