用NumPy手写一个神经网络:从矩阵乘法到反向传播的保姆级实现
用NumPy手写一个神经网络从矩阵乘法到反向传播的保姆级实现神经网络常被视为黑箱但真正理解其内部运作机制的最佳方式莫过于亲手实现一个。本文将用Python的NumPy库从零构建一个全连接神经网络通过代码逐层揭开矩阵运算与梯度传播的神秘面纱。不同于框架封装好的现成组件我们将用最基础的数组操作实现前向传播、损失计算和反向传播的全流程让每个数学公式都转化为可执行的代码。1. 环境准备与基础概念在开始编码前我们需要明确几个核心概念。神经网络本质上是由多层神经元组成的计算图每层通过权重矩阵与下一层相连。数据从输入层流向输出层的过程称为前向传播Forward Propagation而误差从输出层反向传递调整权重的过程则是反向传播Backward Propagation。首先安装必要的库pip install numpy matplotlib基础神经网络组件包括权重矩阵连接两层神经元的参数偏置向量为每个神经元添加的偏移量激活函数引入非线性变换如ReLU、Sigmoid损失函数衡量预测与真实值的差距如均方误差提示本文使用Python 3.8和NumPy 1.20版本建议在Jupyter Notebook中逐步运行代码以便观察中间结果。2. 网络架构初始化我们实现一个具有单隐藏层的网络结构为输入层(2节点) → 隐藏层(4节点) → 输出层(1节点)。首先初始化权重和偏置import numpy as np def initialize_parameters(input_dim, hidden_dim, output_dim): np.random.seed(42) # 固定随机种子便于复现 W1 np.random.randn(hidden_dim, input_dim) * 0.01 b1 np.zeros((hidden_dim, 1)) W2 np.random.randn(output_dim, hidden_dim) * 0.01 b2 np.zeros((output_dim, 1)) parameters {W1: W1, b1: b1, W2: W2, b2: b2} return parameters参数初始化要点权重使用小随机数打破对称性偏置初始化为零乘0.01防止初始激活值过大导致梯度消失矩阵维度关系参数维度说明W1(hidden_dim, input_dim)连接输入层到隐藏层b1(hidden_dim, 1)隐藏层偏置W2(output_dim, hidden_dim)连接隐藏层到输出层b2(output_dim, 1)输出层偏置3. 前向传播实现前向传播包含线性变换和激活函数两个步骤。我们使用ReLU作为隐藏层激活函数输出层使用Sigmoid适合二分类问题。def relu(Z): return np.maximum(0, Z) def sigmoid(Z): return 1 / (1 np.exp(-Z)) def forward_propagation(X, parameters): W1, b1, W2, b2 parameters[W1], parameters[b1], parameters[W2], parameters[b2] # 第一层计算 Z1 np.dot(W1, X) b1 A1 relu(Z1) # 第二层计算 Z2 np.dot(W2, A1) b2 A2 sigmoid(Z2) cache {Z1: Z1, A1: A1, Z2: Z2, A2: A2} return A2, cache数据流动示例单个样本输入X: (2,1) → W1: (4,2) → Z1: (4,1) → A1: (4,1) → W2: (1,4) → Z2: (1,1) → A2: (1,1) (预测输出)批量处理时输入X的维度变为(2,m)其中m是样本数量。矩阵乘法实现了并行计算这是神经网络高效处理批量数据的关键。4. 损失计算与反向传播定义二元交叉熵损失函数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) # 去掉不必要的维度反向传播需要计算各参数的梯度这是通过链式法则实现的。我们预先推导出关键公式输出层梯度dZ2 A2 - Y dW2 (1/m) * np.dot(dZ2, A1.T) db2 (1/m) * np.sum(dZ2, axis1, keepdimsTrue)隐藏层梯度dA1 np.dot(W2.T, dZ2) dZ1 np.multiply(dA1, np.int64(A1 0)) dW1 (1/m) * np.dot(dZ1, X.T) db1 (1/m) * np.sum(dZ1, axis1, keepdimsTrue)实现代码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, axis1, keepdimsTrue) / m # 隐藏层梯度 dA1 np.dot(W2.T, dZ2) dZ1 np.multiply(dA1, (A1 0)) dW1 np.dot(dZ1, X.T) / m db1 np.sum(dZ1, axis1, keepdimsTrue) / m grads {dW1: dW1, db1: db1, dW2: dW2, db2: db2} return grads梯度检查是验证实现正确性的重要步骤def gradient_check(parameters, grads, X, Y, epsilon1e-7): parameters_values parameters.flatten() grad grads.flatten() num_parameters parameters_values.shape[0] J_plus np.zeros((num_parameters, 1)) J_minus np.zeros((num_parameters, 1)) gradapprox np.zeros((num_parameters, 1)) for i in range(num_parameters): theta_plus np.copy(parameters_values) theta_plus[i] epsilon J_plus[i] compute_cost(forward_propagation(X, theta_plus)[0], Y) theta_minus np.copy(parameters_values) theta_minus[i] - epsilon J_minus[i] compute_cost(forward_propagation(X, theta_minus)[0], Y) gradapprox[i] (J_plus[i] - J_minus[i]) / (2*epsilon) numerator np.linalg.norm(grad - gradapprox) denominator np.linalg.norm(grad) np.linalg.norm(gradapprox) difference numerator / denominator if difference 1e-7: print(f梯度检查失败差异度: {difference}) else: print(f梯度检查通过差异度: {difference})5. 参数更新与训练循环使用梯度下降更新参数def update_parameters(parameters, grads, learning_rate0.01): 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 model(X, Y, hidden_dim, num_iterations10000, print_costFalse): input_dim X.shape[0] output_dim Y.shape[0] parameters initialize_parameters(input_dim, hidden_dim, output_dim) for i in range(num_iterations): # 前向传播 A2, cache forward_propagation(X, parameters) # 损失计算 cost compute_cost(A2, Y) # 反向传播 grads backward_propagation(parameters, cache, X, Y) # 参数更新 parameters update_parameters(parameters, grads) # 每1000次打印损失 if print_cost and i % 1000 0: print(f迭代次数 {i}: 损失 {cost}) return parameters可视化训练过程import matplotlib.pyplot as plt def plot_decision_boundary(model, X, y): x_min, x_max X[0, :].min() - 1, X[0, :].max() 1 y_min, y_max X[1, :].min() - 1, X[1, :].max() 1 h 0.01 xx, yy np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z model(np.c_[xx.ravel(), yy.ravel()].T) Z Z.reshape(xx.shape) plt.contourf(xx, yy, Z, cmapplt.cm.Spectral) plt.ylabel(x2) plt.xlabel(x1) plt.scatter(X[0, :], X[1, :], cy, cmapplt.cm.Spectral)6. 实战非线性分类问题我们使用一个螺旋数据集测试网络性能def load_spiral_dataset(): np.random.seed(1) m 400 # 样本数量 N int(m/2) # 每类点数 D 2 # 维度 X np.zeros((m, D)) Y np.zeros((m, 1)) for j in range(2): ix range(N*j, N*(j1)) r np.linspace(0.0, 1, N) t np.linspace(j*4, (j1)*4, N) np.random.randn(N)*0.2 X[ix] np.c_[r*np.sin(t), r*np.cos(t)] Y[ix] j X X.T Y Y.T return X, Y X, Y load_spiral_dataset() parameters model(X, Y, hidden_dim4, num_iterations10000, print_costTrue)训练完成后可以观察到损失曲线稳定下降决策边界能够有效区分两类螺旋数据。通过调整隐藏层维度和学习率等超参数可以进一步提升模型性能。在实际项目中这种基础实现会面临梯度消失、过拟合等问题解决方案包括使用更复杂的网络结构如残差连接添加正则化项L2正则、Dropout采用自适应优化器Adam、RMSprop但核心的矩阵运算和梯度传播机制始终保持不变这正是理解神经网络工作原理的价值所在。