共计 3534 个字符,预计需要花费 9 分钟才能阅读完成。
神经网络基础回顾
BP 神经网络是一种多层前馈神经网络,通过误差反向传播算法进行训练。它由输入层、隐藏层和输出层组成,每层包含若干神经元。相邻层的神经元通过权重连接,每个神经元有自己的偏置。神经网络的学习过程就是不断调整这些权重和偏置,使得网络的输出尽可能接近期望的输出。

正向传播的数学推导与实现
正向传播是指输入数据从输入层经过隐藏层最终到达输出层的过程。对于第 l 层的第 j 个神经元,其输入为:
$$z_j^{(l)} = \sum_{i} w_{ji}^{(l)} a_i^{(l-1)} + b_j^{(l)}$$
其中,$a_i^{(l-1)}$ 是第 l - 1 层第 i 个神经元的输出,$w_{ji}^{(l)}$ 是连接第 l - 1 层第 i 个神经元和第 l 层第 j 个神经元的权重,$b_j^{(l)}$ 是第 l 层第 j 个神经元的偏置。
神经元的输出通过激活函数 $\sigma$ 计算:
$$a_j^{(l)} = \sigma(z_j^{(l)})$$
常见的激活函数包括 sigmoid、ReLU 和 tanh 等。
反向传播的梯度计算详解
反向传播是通过链式法则计算损失函数对网络参数的梯度。假设使用均方误差作为损失函数:
$$L = \frac{1}{2}\sum_{k}(y_k – a_k^{(L)})^2$$
其中,$L$ 表示输出层,$y_k$ 是期望输出,$a_k^{(L)}$ 是实际输出。
对于输出层神经元,误差项为:
$$\delta_k^{(L)} = (a_k^{(L)} – y_k) \odot \sigma'(z_k^{(L)})$$
对于隐藏层神经元,误差项通过反向传播计算:
$$\delta_j^{(l)} = (\sum_{k} w_{kj}^{(l+1)} \delta_k^{(l+1)}) \odot \sigma'(z_j^{(l)})$$
最后,权重和偏置的梯度为:
$$\frac{\partial L}{\partial w_{ji}^{(l)}} = a_i^{(l-1)} \delta_j^{(l)}$$
$$\frac{\partial L}{\partial b_j^{(l)}} = \delta_j^{(l)}$$
完整 Python 代码实现
import numpy as np
class NeuralNetwork:
def __init__(self, layers, learning_rate=0.01):
self.layers = layers
self.learning_rate = learning_rate
self.weights = []
self.biases = []
# 初始化权重和偏置
for i in range(len(layers)-1):
# 使用 Xavier 初始化
w = np.random.randn(layers[i+1], layers[i]) / np.sqrt(layers[i])
b = np.zeros((layers[i+1], 1))
self.weights.append(w)
self.biases.append(b)
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(self, x):
return x * (1 - x)
def forward(self, x):
a = x.T if x.ndim > 1 else x.reshape(-1, 1)
activations = [a]
zs = []
for w, b in zip(self.weights, self.biases):
z = np.dot(w, a) + b
a = self.sigmoid(z)
zs.append(z)
activations.append(a)
return activations[-1], activations, zs
def backward(self, x, y, activations, zs):
m = y.shape[0]
y = y.T if y.ndim > 1 else y.reshape(-1, 1)
# 计算输出层误差
delta = (activations[-1] - y) * self.sigmoid_derivative(activations[-1])
# 初始化梯度
dw = [np.zeros(w.shape) for w in self.weights]
db = [np.zeros(b.shape) for b in self.biases]
# 输出层梯度
dw[-1] = np.dot(delta, activations[-2].T) / m
db[-1] = np.sum(delta, axis=1, keepdims=True) / m
# 反向传播误差
for l in range(2, len(self.layers)):
delta = np.dot(self.weights[-l+1].T, delta) * self.sigmoid_derivative(activations[-l])
dw[-l] = np.dot(delta, activations[-l-1].T) / m
db[-l] = np.sum(delta, axis=1, keepdims=True) / m
return dw, db
def update_params(self, dw, db):
for i in range(len(self.weights)):
self.weights[i] -= self.learning_rate * dw[i]
self.biases[i] -= self.learning_rate * db[i]
def train(self, X, y, epochs=1000, batch_size=32):
m = X.shape[0]
for epoch in range(epochs):
# 随机打乱数据
permutation = np.random.permutation(m)
X_shuffled = X[permutation]
y_shuffled = y[permutation]
for i in range(0, m, batch_size):
# 获取当前 batch
X_batch = X_shuffled[i:i+batch_size]
y_batch = y_shuffled[i:i+batch_size]
# 前向传播
_, activations, zs = self.forward(X_batch)
# 反向传播
dw, db = self.backward(X_batch, y_batch, activations, zs)
# 更新参数
self.update_params(dw, db)
# 打印训练进度
if epoch % 100 == 0:
output, _, _ = self.forward(X)
loss = np.mean((output - y.T)**2)
print(f'Epoch {epoch}, Loss: {loss:.4f}')
超参数调优建议
-
学习率:通常设置在 0.001 到 0.1 之间。太大会导致震荡,太小会收敛过慢。可以尝试学习率衰减策略。
-
批量大小:常见的批量大小有 32、64、128 等。较小的批量可以提供更多的梯度更新,但计算效率较低。较大的批量可以提供更稳定的梯度估计,但可能导致收敛到较差的局部最优。
-
网络结构:隐藏层数量和每层神经元数量需要根据任务复杂度调整。通常从较简单的网络开始,逐步增加复杂度。
-
激活函数:ReLU 通常比 sigmoid 和 tanh 有更好的表现,可以缓解梯度消失问题。
-
权重初始化:Xavier 初始化或 He 初始化通常比随机初始化表现更好。
常见错误与解决方案
-
梯度消失:当网络较深时,梯度可能在反向传播过程中变得非常小。解决方案包括使用 ReLU 激活函数、批归一化、残差连接等。
-
过拟合:网络在训练集上表现很好,但在测试集上表现差。解决方案包括增加训练数据、使用正则化(L1/L2)、Dropout 等。
-
学习率设置不当:学习率太大导致震荡,太小导致收敛慢。可以使用学习率衰减或自适应优化器(如 Adam)。
-
数据未归一化:输入数据范围差异大可能导致训练困难。应该对输入数据进行标准化或归一化处理。
-
权重初始化不当:全零初始化会导致所有神经元学习相同的特征。应该使用随机初始化或更高级的初始化方法。
延伸思考题
-
如何修改代码实现不同的激活函数(如 ReLU、Leaky ReLU 等)?不同的激活函数对反向传播有什么影响?
-
除了均方误差,还可以使用哪些损失函数?它们分别适用于什么场景?
-
如何实现动量(Momentum)或 Adam 优化器来加速训练?
-
批归一化(Batch Normalization)是如何工作的?如何在现有代码中实现?
-
如何可视化训练过程中的权重变化和损失函数变化?这对调参有什么帮助?
通过这篇文章的学习,你应该已经掌握了 BP 神经网络的核心原理和实现方法。接下来可以尝试在更复杂的数据集上应用这些知识,或者探索更先进的神经网络结构和训练技巧。
