从零实现多层感知机(MLP)前向传播:从单神经元到 Transformer FFN 的完整指南
2026/9/18 5:12:21 网站建设 项目流程

从零实现多层感知机(MLP)前向传播:从单神经元到 Transformer FFN 的完整指南

【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

导读

本文围绕 LeetCode 仓库中 mlp-from-scratch.md 这篇文档展开,系统讲解多层感知机(Multi-Layer Perceptron, MLP)的前向传播:如何在给定权重矩阵与偏置向量的情况下,用 NumPy 的@运算符逐层完成线性变换、偏置相加与 ReLU 激活,并正确跳过输出层的激活函数。读完本文,你将掌握 MLP 的数学定义、矩阵形状规则、线性层塌缩的原理,以及它在 Transformer 前馈网络(FFN)中的真实应用形态,从而为理解 GPT 类模型打下坚实基础。


前置知识

在动手实现 MLP 之前,需要先掌握以下三个基础概念,本仓库的配套文章可以帮你补齐:

  • 单神经元前向传播:MLP 本质上是大量神经元按层组织的集合,每个神经元都在计算"加权和加偏置,再经过激活函数"。单神经元前向传播 一文给出了最基本的 $\sigma(x \cdot w + b)$ 运算,MLP 只是把它向量化、层叠化。
  • 矩阵乘法:每一层通过乘以权重矩阵来变换输入,形状规则 $(B, d_{\text{in}}) \times (d_{\text{in}}, d_{\text{out}}) = (B, d_{\text{out}})$ 贯穿所有层的计算。线性回归前向传播 展示的 $\hat{y} = Xw$ 正是"一层无激活"的特例。
  • ReLU 激活:层与层之间的 ReLU 引入非线性,这是深度网络表达能力的来源。激活函数的取值范围与梯度特性可参考 Sigmoid 与 ReLU。

概念:MLP 是什么

多层感知机是含有一个或多个隐藏层的神经网络。每一层都执行一次线性变换加上一次非线性激活:

$$h^{(l)} = \sigma(h^{(l-1)} W^{(l)} + b^{(l)})$$

其中 $h^{(l-1)}$ 是第 $l$ 层的输入,$W^{(l)}$ 是权重矩阵,$b^{(l)}$ 是偏置向量,$\sigma$ 是激活函数。输入层到第一个隐藏层之间、隐藏层之间、隐藏层到输出层之间,全部遵循这一公式;区别只在于激活函数是否施加。

为什么需要多层:线性层的塌缩

关键在于:为什么不能只用一层?

  • 单个线性层只能学习线性决策边界(超平面),表达能力有限。
  • 两层线性层之间如果没有激活函数,会塌缩成一层,因为:

$$W_2(W_1 x + b_1) + b_2 = W_2 W_1 x + W_2 b_1 + b_2$$

这仍然是一个关于 $x$ 的线性函数——两个线性变换的复合还是线性变换。无论堆多少层,都不增加表达能力。

  • 层与层之间的ReLU 阻止了这种塌缩。每一层都能以新的方式"弯曲"数据的表示,堆叠足够多的层(配合足够宽度)后,网络可以逼近任意连续函数(万能逼近定理的精神)。

输出层不加激活

输出层通常不加激活(回归场景输出原始预测值),或加 softmax(分类场景输出概率分布)。本文实现的场景中,最终层不加激活,直接产生raw logits(原始 logit)。


解决方案

直觉

遍历权重矩阵列表与偏置向量列表:对每一层,用@运算符完成矩阵乘法,加上偏置,然后施加 ReLU。最后一层跳过激活,输出原始 logits。整个过程完全向量化,不需要任何显式 for 循环逐神经元计算。

实现

import numpy as np from numpy.typing import NDArray from typing import List class Solution: def forward(self, x: NDArray[np.float64], weights: List[NDArray[np.float64]], biases: List[NDArray[np.float64]]) -> NDArray[np.float64]: h = x for i in range(len(weights)): h = h @ weights[i] + biases[i] # Linear transformation if i < len(weights) - 1: h = np.maximum(0, h) # ReLU on hidden layers only return np.round(h, 5)

代码要点:

  • h @ weights[i]:NumPy 的@运算符等价于np.matmul,对任意维度的数组执行矩阵乘法。当输入是形状 $(B, d_{\text{in}})$ 的批数据时,一次调用即可同时处理 $B$ 个样本,这正是 Transformer 中批量前向传播的写法。
  • + biases[i]:NumPy 广播(broadcasting)机制会自动把形状为 $(d_{\text{out}},)$ 的偏置向量加到每个样本的每个输出维度上。
  • np.maximum(0, h):向量化的 ReLU,对数组中每个元素取 $\max(0, \cdot)$。注意不能用 Python 内置的max(它只接受标量,且对数组会报错或产生歧义)。
  • np.round(h, 5):将最终输出四舍五入到 5 位小数,保证结果可复现、可比较。
  • len(weights) == 1(单层网络)时,循环体不进入if分支,等价于一个不带激活的线性回归层,与 线性回归前向传播 中的np.matmul(X, weights)语义一致。

逐步演练

给定一个 2 层 MLP:输入 $x = [1, 2]$,$W_1$ 形状 $2 \times 3$,$b_1$ 形状 $3$,$W_2$ 形状 $3 \times 1$,$b_2$ 形状 $1$:

操作示例
1(隐藏层)$h_1 = x @ W_1 + b_1$$[-0.5, 1.2, 0.3]$
ReLU$\max(0, h_1)$$[0, 1.2, 0.3]$
2(输出层)$h_2 = h_1 @ W_2 + b_2$$[0.9]$(不加激活)

注意观察:负值 $-0.5$ 被 ReLU 清零为 $0$。这种稀疏性是 ReLU 的一个特性:对任意给定输入,只有一部分神经元"激活"(输出非零),每个输入拥有自己独特的激活模式,这也使得激活模式本身携带了输入的有用信息。

时间与空间复杂度

  • 时间:$O(\sum_{l} d_{l-1} \cdot d_l)$,其中 $d_l$ 是第 $l$ 层的维度,计算量主要由矩阵乘法主导。
  • 空间:$O(\max_l d_l)$,主要存储最大的隐藏层表示(在不保存全部中间激活的前提下)。

补充:如果考虑批量大小 $B$,时间应记为 $O(B \cdot \sum_l d_{l-1} \cdot d_l)$。若后续要训练(反向传播),还需要额外保存每一层的输入 $h^{(l-1)}$ 与预激活 $z^{(l)}$,这正是 多层反向传播 中 forward pass 阶段缓存中间值的由来——训练阶段的内存开销因此高于推理阶段。


常见陷阱

陷阱一:对输出层施加激活

输出层应当产生原始 logits(分类场景)或原始预测值(回归场景)。若对输出层施加 ReLU,会把负值全部裁剪为 0,导致模型永远无法预测负数——这在回归任务中往往是致命的。

# Wrong: ReLU on every layer including output for i in range(len(weights)): h = h @ weights[i] + biases[i] h = np.maximum(0, h) # don't do this on the last layer! # Correct: skip activation on the last layer for i in range(len(weights)): h = h @ weights[i] + biases[i] if i < len(weights) - 1: h = np.maximum(0, h)

判别标准:只有隐藏层需要激活函数,输出层永远不加(除非是 softmax 之类的输出专用变换)

陷阱二:权重矩阵形状不匹配

每个权重矩阵的输入维度必须等于上一层输出的输出维度。形状 $(3, 4)$ 的权重矩阵期望 3 维输入,产生 4 维输出。

# Wrong: weights[1] has incompatible input dimension weights = [np.zeros((2, 3)), np.zeros((4, 1))] # 3 != 4 # Correct: output dim of layer i matches input dim of layer i+1 weights = [np.zeros((2, 3)), np.zeros((3, 1))] # 3 == 3

自检方法:逐个检查相邻两层——第 $i$ 层权重形状 $(d_{i-1}, d_i)$,第 $i+1$ 层权重形状必须是 $(d_i, d_{i+1})$,链式传播下来整体形状必须闭合:输入维度 $d_0$ 与第一层权重行数一致,最后一层权重列数 $d_L$ 与输出维度一致。


在 GPT 项目中:FFN 就是两层的 MLP

原文档指出,本实现对应 GPT 课程项目中的foundations/mlp.py(该文件属于外部课程仓库,不在当前仓库内)。在当前仓库中,最能印证 MLP 实际形态的是 Transformer 块:每个 Transformer 块内部的前馈网络(FFN)恰好就是一个两层 MLP——先上投影(up-projection)将维度扩大 4 倍,经过 ReLU,再下投影(down-projection)回到原始维度。

该文中VanillaNeuralNetwork的源码给出了精确实现:

self.up_projection = nn.Linear(model_dim, model_dim * 4) self.relu = nn.ReLU() self.down_projection = nn.Linear(model_dim * 4, model_dim) self.dropout = nn.Dropout(0.2) # using p = 0.2
def forward(self, x): return self.dropout(self.down_projection(self.relu(self.up_projection(x))))

对照本文的实现可以清晰看到对应关系:

  • h @ W1 + b1up_projection:维度从 $d_{\text{model}}$ 扩张到 $4 \cdot d_{\text{model}}$(例如 GPT-2 中从 768 到 3072);
  • np.maximum(0, h)ReLU:引入非线性;
  • h @ W2 + b2down_projection:维度从 $4 \cdot d_{\text{model}}$ 压缩回 $d_{\text{model}}$;
  • 额外的Dropout(0.2)是训练期防止过拟合的正则化手段,推理时可关闭。

在这个架构中,注意力机制负责 token 之间的通信,而 FFN(MLP)负责每个 token 自身的独立计算。MLP 的 4 倍扩张为网络提供了额外的参数容量——在 GPT-2 中,FFN 占据了每个 Transformer 块约三分之二的可学习参数。

进一步的延伸:前向传播只是训练的一半。MLP 训练时对每一层求梯度依赖链式法则逐层回传,具体推导(含 ReLU 导数作为 0/1 掩码、权重梯度等于误差信号与层输入的外积)见 多层反向传播;而 PyTorch 中调用loss.backward()时,自动微分引擎做的正是这些手算链式法则的机械化版本。


关键要点

  • MLP 将线性变换与 ReLU 激活链式组合,只要有足够的宽度和深度,就能逼近任意连续函数。
  • @运算符为矩阵乘法提供了简洁的语法,形状规则 $(B, d_{\text{in}}) @ (d_{\text{in}}, d_{\text{out}}) = (B, d_{\text{out}})$ 支配着每一层的计算。
  • 层与层之间若没有非线性激活,无论堆叠多少层,整个网络都会塌缩为单个线性变换,表达能力不增反减。
  • 输出层必须跳过激活函数,直接输出 raw logits 或回归预测值。
  • 在 Transformer 中,FFN 正是这种"扩展 4 倍 → ReLU → 压缩回来"的两层 MLP,与注意力机制分工协作,构成 GPT 类模型的核心计算单元。

【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询