资讯动态

别再死记硬背公式了!用NumPy手搓一个全连接层,5分钟搞懂它的计算过程

发布时间:2026/8/14 17:00:30 来源:尧图企业网站定制
用NumPy手搓全连接层从矩阵运算到深度学习本质记得第一次翻开神经网络教材时那些密密麻麻的矩阵公式让我头皮发麻——W·xb看起来简单但为什么权重矩阵要转置偏置向量怎么广播直到我用NumPy从零实现了一个全连接层所有抽象概念突然变得清晰可见。今天我们就用代码代替公式让矩阵运算自己说话。1. 全连接层的计算本质全连接层的核心其实就是一次线性变换加非线性激活。假设输入是一个3维向量[1.2, 0.5, -0.7]要输出2维结果整个过程可以分解为import numpy as np # 输入向量 (3维) x np.array([1.2, 0.5, -0.7]) # 权重矩阵 (2行3列) W np.array([[0.1, -0.2, 0.3], [0.4, 0.5, -0.6]]) # 偏置向量 (2维) b np.array([0.1, -0.1]) # 线性变换 z np.dot(W, x) b # 等价于 W x b这个简单的W x b包含了三个关键操作矩阵乘法权重矩阵W的每一行都与输入x做点积相当于在提取不同的特征组合向量加法每个神经元加上自己的偏置项调整输出的基准线维度变换把3维输入映射到2维空间为什么权重矩阵是output×input的形状因为NumPy的dot运算要求第一个矩阵的列数等于第二个矩阵的行数。当我们把2×3的W与3×1的x相乘自然得到2×1的结果。2. 实现可训练的全连接层类让我们用面向对象的方式封装这个逻辑class FullyConnectedLayer: def __init__(self, input_size, output_size): # 初始化权重和偏置 self.weights np.random.randn(output_size, input_size) * 0.1 self.bias np.zeros((output_size, 1)) def forward(self, x): # 确保输入是列向量 if x.ndim 1: x x.reshape(-1, 1) self.input x # 保存输入用于反向传播 return self.weights x self.bias关键设计点权重初始化使用小随机数打破对称性乘0.1避免初始输出过大偏置初始化通常从零开始输入处理自动将一维数组转为列向量n×1矩阵测试我们的实现fc FullyConnectedLayer(3, 2) x_test np.array([1.0, -1.0, 0.5]) print(fc.forward(x_test)) # 示例输出[[ 0.175] # [-0.452]]3. 维度广播的魔法当处理批量数据时多个样本一起计算全连接层展现出了NumPy广播机制的强大# 批量输入 (3个样本每个3维) X_batch np.array([[1.0, -1.0, 0.5], [0.3, 0.2, -0.1], [0.8, 0.0, -0.4]]).T # 转置为3×3 output fc.forward(X_batch) print(output.shape) # (2, 3)这里发生了两件事矩阵乘法W X2×3的W与3×3的X相乘得到2×3的结果偏置加法2×1的bias自动广播到2×3与每列相加维度对照表操作输入形状权重形状输出形状单样本(3,)(2,3)(2,)批量处理(3,n)(2,3)(2,n)4. 添加激活函数没有非线性激活函数多层网络就退化为单层网络。让我们扩展实现class FullyConnectedLayer: def __init__(self, input_size, output_size, activationrelu): self.weights np.random.randn(output_size, input_size) * 0.1 self.bias np.zeros((output_size, 1)) self.activation activation def relu(self, x): return np.maximum(0, x) def forward(self, x): if x.ndim 1: x x.reshape(-1, 1) self.input x z self.weights x self.bias if self.activation relu: return self.relu(z) elif self.activation sigmoid: return 1 / (1 np.exp(-z)) else: # 无激活 return z常见激活函数对比函数类型公式特点适用场景ReLUmax(0,x)计算快缓解梯度消失隐藏层首选Sigmoid1/(1e^-x)输出0-1易饱和二分类输出层Tanh(e^x-e^-x)/(e^xe^-x)输出-1到1RNN等特殊场景5. 反向传播实现训练神经网络的核心是反向传播算法。让我们实现一个简化版本class FullyConnectedLayer: # ... 前面的代码 ... def backward(self, dout, learning_rate0.01): # dout是损失函数对输出的梯度 if self.activation relu: dout dout * (self.output 0) # ReLU的导数 # 计算梯度 dW dout self.input.T db np.sum(dout, axis1, keepdimsTrue) dx self.weights.T dout # 参数更新 self.weights - learning_rate * dW self.bias - learning_rate * db return dx # 传递给前一层的梯度反向传播的数学本质是链式法则的应用。以输入维度3、输出维度2为例权重梯度∂L/∂W ∂L/∂z · ∂z/∂W dout · xᵀ偏置梯度∂L/∂b Σ(∂L/∂z) sum(dout, axis1)输入梯度∂L/∂x Wᵀ · ∂L/∂z实际训练时的注意事项通常会在大批量数据上计算平均梯度需要实现参数初始化、正则化等机制学习率需要适当调整6. 从零训练一个分类器现在我们把全连接层用起来在简单数据集上测试# 生成螺旋数据集 def spiral_data(points, classes): X np.zeros((points*classes, 2)) y np.zeros(points*classes, dtypeuint8) for class_number in range(classes): ix range(points*class_number, points*(class_number1)) r np.linspace(0.0, 1, points) t np.linspace(class_number*4, (class_number1)*4, points) np.random.randn(points)*0.2 X[ix] np.c_[r*np.sin(t*2.5), r*np.cos(t*2.5)] y[ix] class_number return X, y # 构建网络 class SimpleNN: def __init__(self): self.fc1 FullyConnectedLayer(2, 16, relu) self.fc2 FullyConnectedLayer(16, 3) # 3类分类 def forward(self, x): x self.fc1.forward(x) x self.fc2.forward(x) return x def backward(self, dout, lr): dout self.fc2.backward(dout, lr) dout self.fc1.backward(dout, lr)训练循环的关键步骤# 初始化 nn SimpleNN() X, y spiral_data(100, 3) # 300个点3类 lr 0.1 for epoch in range(1000): # 前向传播 logits nn.forward(X.T) # 计算softmax交叉熵损失 exp_logits np.exp(logits - np.max(logits, axis0, keepdimsTrue)) probs exp_logits / np.sum(exp_logits, axis0, keepdimsTrue) loss -np.mean(np.log(probs[y, range(len(y))])) # 反向传播 dout probs dout[y, range(len(y))] - 1 dout / len(y) nn.backward(dout, lr) if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.4f})这个简单网络在螺旋数据集上可以达到约95%的准确率。虽然比不上现代深度学习框架的性能但完整展示了全连接层的核心机制。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价