第 3 章 · 打地基
搭成网络:前向传播
一个神经元能做的判断很有限。但只要把许多神经元并排成一层、再把许多层 叠起来,它们就能逐层提炼信息,拼出惊人的表达能力。 这一章我们就来看:数据是怎么从输入,一层层向前流动,最终变成预测的—— 这个过程叫前向传播(forward propagation)。
读完这一章,你会明白
- “层”和“网络(MLP)”是怎么由神经元搭起来的;
- 前向传播到底在传什么、怎么一层层算下去;
- 怎么用一个矩阵公式 z = Wx + b 概括一整层;
- 这些权重、偏置在代码里是怎么存的,并逐行读懂真实的前向传播实现。
1. 从一个神经元,到一层,再到一张网
上一章的神经元只有一个输出。现在我们把好几个神经元并排放在一起, 让它们看同样的输入,但各自拥有不同的权重和偏置—— 于是它们会从不同角度去“看”这份输入,得到好几个不同的输出。这就是一个 层(layer)。
再把一层的输出,当作下一层的输入,层层往下接,就得到了一张网络。 这种“每一层的每个神经元,都和上一层的所有神经元相连”的结构, 叫全连接网络,也叫多层感知机(MLP)。
别被这三个字母吓到。MLP = Multi-Layer Perceptron = 多层感知机,拆开看: “感知机(perceptron)”就是上一章那个神经元的老名字(它能“感知”输入、给个判断); “多层”就是把它们叠成好几层。所以 MLP 就是“一堆神经元分层全连接搭成的网络”, 也就是本章画的这种图,没有任何玄机。它是最基础的网络,本书配套的 C++ 代码、MNIST 手写数字识别用的都是它。
后面章节会蹦出一堆网络名字,这里先一次性认个脸,以后遇到就不慌了——它们只是神经元的不同“连线方式”:
- MLP 多层感知机最朴素:层层全连接。适合固定长度的输入(一张图、一行特征)。本书第 2–9 章的主角。
- CNN 卷积网络擅长图像:用一个小窗口在图片上滑动、找局部花纹(边缘、纹理)。第 13 章的主角。
- RNN 循环网络擅长序列:像接力一样一个词一个词地读,把“记忆”往后传。第 14 章会细讲它的思路和短板。
- Transformer今天大模型的主流:靠“注意力”让每个词直接看全局,又快又强。第 16–18 章的主角。
它们的“零件”(神经元、权重、激活、损失、梯度下降)完全一样,只是搭法不同。学透 MLP,后面都是变体。
一个 2 → 3 → 1 的小网络。每条连线都是一个权重,每个圆圈(除输入层)都有一个偏置。
一层里神经元的个数,叫这一层的宽度;叠了多少层,叫网络的深度。 更宽,意味着每层能并行抓更多种特征;更深,意味着能把简单特征逐层组合成更抽象的特征。 “深度学习”的“深”,就是指这种多层堆叠。
2. 前向传播:数据一层层往前流
前向传播的规则极其简单,而且每一层都在重复同一件事:
- 拿到上一层的输出(对第一层来说,就是原始输入);
- 这一层的每个神经元,各自做“加权求和 + 偏置”,得到 z;
- 每个 z 过激活函数,得到这一层的输出;
- 把这层输出交给下一层,回到第 1 步,直到最后一层吐出预测。
注意第 2、3 步,正是上一章那个神经元做的两件事——只不过现在一层里有很多个神经元同时在做。 所以可以说:前向传播 = 把“单个神经元的两步运算”,按层、按神经元,从左到右重复一遍。
用矩阵把“一整层”一次写完
一层里有很多神经元,每个都有一组权重。把它们的权重摞成一个表格(矩阵)W, 把偏置排成一列 b,那么一整层的加权求和就能浓缩成一行公式:
这就是你以后会反复见到的 z = Wx + b。它没有任何新东西,
只是把“这一层每个神经元各算各的”用矩阵语言打了个包而已。
3. 边看动画边理解
下面这个动画,把一个 2 → 3 → 1 的网络画了出来。请先点 “切到前向传播”,然后反复点 “下一步”, observe 数据是怎么从输入层,逐层算到输出层的。 (反向传播的部分先不用管,那是第 6 章的内容,这里你可以只看前向。)
前向传播:亮起的节点和连线,就是当前正在参与计算的部分;数据从左流到右。
4. 这些参数在代码里是怎么存的
在配套项目里,整张网络的结构用一个数组描述,比如 layer_ = {2, 3, 1}
就表示“输入 2 个、隐藏 3 个、输出 1 个”。参数则存成嵌套数组:
std::vector<int> layer_; 1
// 偏置: neuron_bias_[第几层][这层第几个神经元]
std::vector<std::vector<double>> neuron_bias_; 2
// 权重: neuron_weight_[第几层][这层第几个][上一层第几个]
std::vector<std::vector<std::vector<double>>> neuron_weight_; 3
layer_描述每一层有几个神经元,网络的整体“骨架”。neuron_bias_[l][o]:第 l 层、第 o 个神经元的偏置(就是公式里的 b)。neuron_weight_[l][o][i]:第 l 层第 o 个神经元,连到上一层第 i 个神经元的那条线的权重。三个下标连起来读,就是“哪一层、这层第几个、来自上层第几个”。
5. 逐行读懂前向传播
下面是真实的前向传播代码(为聚焦核心,略去了边界检查,逻辑完全一致)。 你会看到它和上面那四步规则一一对应:
// 第 0 层 = 输入层: 把这一批样本原样放进去当作"第 0 层的输出"
for (int b = 0; b < B; b++)
for (int j = 0; j < layer_[0]; j++)
neuron_output_[0][b][j] = batch_data[b][j]; 1
// 第 1 层到最后一层: 每层都做 "加权求和 + 激活"
for (int l = 1; l < L; l++) {
for (int b = 0; b < B; b++) {
const auto &in_vec = neuron_output_[l - 1][b]; 2
for (int o = 0; o < layer_[l]; o++) {
double z = neuron_bias_[l][o]; 3
const auto &w_row = neuron_weight_[l][o];
for (int i = 0; i < layer_[l - 1]; i++)
z += w_row[i] * in_vec[i]; 4
neuron_preact_[l][b][o] = z; 5
neuron_output_[l][b][o] = activate_function_->Activate(z); 6
}
}
}
- 第 0 层不做任何运算,直接把样本放进
neuron_output_[0]。这样后面每一层都能统一地“读上一层的输出”。 in_vec指向上一层的输出——当前层的输入,就是上一层的输出,前向传播的精髓。- 每个神经元先用自己的偏置 b 作为 z 的起点(对应公式里的
+ b)。 - 把上一层每个输出乘对应权重,逐个累加进 z。这三层循环
l / o / i念作:“第 l 层的第 o 个神经元,收集来自上一层第 i 个输入的贡献”——正是 z = Σ w·x + b。 - 把激活前的 z 存进
neuron_preact_。还记得第 2 章的伏笔吗?反向传播算梯度时要用到它。 - z 过激活函数,得到这个神经元的输出;它马上会成为下一层的
in_vec,继续向前流。
代码里比我们的“四步规则”多了一层 for (int b ...),它表示“一次同时算一批(batch)样本”。
这纯粹是为了效率——与其一个一个样本算,不如一批一起算更快。
原理和算单个样本一模一样,你完全可以先在脑子里把这层循环忽略掉。
顺便提一句:对单个样本做一次预测的 Predict,内部其实就是把这个样本包成一个“只有一条”的 batch,
跑一遍上面的前向传播,然后取最后一层的输出当作结果。所以 Predict 和训练用的前向,是同一套代码。
小结
- 把神经元并排成“层”,把层叠起来成“网络(MLP)”;连线是权重,节点是偏置 + 激活。
- 前向传播 = 从输入开始,逐层重复“加权求和 + 激活”,直到最后一层给出预测。
- 一整层的运算可以打包成矩阵公式 z = Wx + b,本质没变。
- 参数存成
neuron_weight_[层][本层第几个][上层第几个]和neuron_bias_[层][第几个]。 - 前向时顺手保存了每层的净输入 z(
neuron_preact_),为反向传播做准备。
动手与思考
问题 1:layer_ = {784, 128, 64, 10} 描述的是一个什么样的网络?
输入 784 维(比如 28×28 的手写数字图片摊平),两层隐藏层分别是 128 和 64 个神经元,输出 10 维(对应 0–9 十个类别)。这正是本项目 MNIST 例子用的结构。
问题 2:前向传播里,“当前层的输入”到底是什么?
就是“上一层的输出”。代码里的 in_vec = neuron_output_[l-1][b] 表达的正是这件事;数据就这样被一层接一层地往前传递。
问题 3:为什么前向时要特地把每层的 z 存进 neuron_preact_?
因为第 6 章反向传播算梯度时,需要每个神经元“激活前的净输入 z”来计算激活函数的导数(尤其是像 GELU 这种必须用 input 才能求导的激活)。前向时顺手存好,反向时直接取用。
现在网络能“向前算出一个预测”了,但它八成是错的。下一章,我们要给“错”下一个精确的定义—— 损失函数,这是让网络开始“学习”的第一步。