第 2 章 · 打地基
一个神经元
深度学习这台庞大的机器,是由一种极其简单的小零件重复堆叠出来的,这个零件就是 神经元(neuron)。把这一个零件看透,你就拿到了理解后面一切的钥匙—— 因为哪怕是上万亿参数的大模型,它最小的计算单元,和我们这一章拆开的这个,几乎没有区别。
读完这一章,你会明白
- 一个神经元内部到底在算什么(其实就两步:加权求和、过激活函数);
- “权重”和“偏置”这两个旋钮分别在调什么;
- 为什么非要有激活函数不可,没有它会发生什么;
- Sigmoid / ReLU / GELU 这些激活函数长什么样、各有什么脾气,并对照真实 C++ 实现。
1. 一个神经元,就是一个“打分员”
想象你是面试官,要给一个候选人打分。你手上有几条线索: 工作年限、项目经验。这两条线索对你来说重要程度不一样: 也许你觉得项目经验更关键。于是你心里其实在做这样一件事:
- 给“工作年限”一个重要程度,比如 ×1.2;
- 给“项目经验”一个重要程度,比如 ×2.0;
- 再加上一个“底分 / 心情分”,比如今天面试官心情好,所有人 +0.5;
- 把它们加起来,得到一个总分,再根据总分决定“要不要”。
恭喜,你刚刚已经在脑子里跑了一遍神经元。那几个“重要程度”就是 权重(weight),那个“底分”就是 偏置(bias),“加起来”就是 加权求和,“根据总分做决定”就是 激活函数(activation)。
2. 神经元内部的两步运算
把上面的故事写成式子。设输入是 x1、x2,神经元先做第一步:加权求和 + 偏置,得到一个中间值 z:
然后做第二步:把 z 丢进一个激活函数 f,得到神经元的最终输出 a:
一个神经元:输入各自乘上权重 → 求和再加偏置得到 z → 过激活函数 f → 输出 a。
3. 别光看,动手拧一拧
下面是一个真正能玩的神经元。拖动滑块改变输入 x、权重 w、偏置 b, 再切换不同的激活函数,观察输出怎么变。建议你试试这几件事:
- 把某个权重拖成负数,看看那条输入是怎么“反着起作用”的;
- 只调偏置 b,感受它整体抬高 / 压低输出的效果(它就是个“底分”);
- 切到 ReLU,把 z 调到负数,看输出怎么被“一刀切”成 0。
神经元实验台:中间那行式子,就是上面 z = wx + b 再过激活函数的实时计算。
4. 为什么非得有激活函数?
这是初学者最容易忽略、但极其关键的一点。假如去掉激活函数, 神经元就只剩“加权求和”这一步,也就是一个线性变换。 这时候,你把很多层这样的神经元叠起来,会发生一件令人沮丧的事:
换句话说,没有激活函数,叠再多层都白搭,整个网络的表达能力和单层一模一样, 只能学“按比例缩放”这种最简单的关系,学不会任何“拐弯”的、复杂的模式。
线性变换就像“只会把图片整体放大缩小、平移”的工具; 激活函数则给了网络“折叠、弯曲”的能力。 正是这点非线性,让深层网络能拟合出任意复杂的形状。
5. 几种常见的激活函数
它们的差别,就在于“怎么根据总分 z 做决定”这条曲线长什么样:
- Sigmoid 把任何数压到 0 和 1 之间,像一个平滑的开关。早期很常用,但两端容易“饱和”(梯度趋近 0,学不动)。
- Tanh 和 Sigmoid 形状像,但压到 −1 到 1 之间,以 0 为中心。
- ReLU 大于 0 原样输出,小于 0 直接归零。又快又好用,是现代网络的默认选择。
- LeakyReLU 给负半轴留一条很小的斜坡(比如 0.01),缓解 ReLU 把神经元“彻底关死”的问题。
- GELU 比 ReLU 更平滑的“软开关”,是 BERT / GPT 这类 Transformer 的常客。
把输入 z(横轴)喂给不同激活函数,得到输出(纵轴)。注意看:Sigmoid/Tanh 两端压平饱和, ReLU/GELU 在正半轴一路向上不封顶。(LeakyReLU 的负半轴斜率为看清画成了 0.1,实际常用 0.01。)
最早的激活函数是图里那条虚线——阶跃函数(step):输入过 0 就输出 1,否则 0, 完美对应“神经元要么点亮、要么不亮”。但它有个致命伤:在 0 处是断的、不可导, 没法做第 6 章要讲的反向传播(要求处处能求导)。于是人们换成了平滑的 Sigmoid——既非线性,又处处可导。 这也是为什么后来的激活函数几乎都是“平滑曲线”:能求导,才能学习。
(上面的实验台里这些都能直接切换,建议挨个切一遍,对照这张图找找手感。)
6. 对照真实代码:激活函数是怎么实现的
在本书配套的 C++ 项目里,每个激活函数都实现两个方法:Activate(前向,把 z 变成输出)
和 DerivActivate(它的导数,留着第 6 章反向传播时用)。先看最经典的 Sigmoid:
double SigmoidActivate::Activate(const double &input) {
return 1 / (1 + exp(-input)); 1
}
double SigmoidActivate::DerivActivate(const double &output) {
return output * (1 - output); 2
}
- 前向:这一行就是 Sigmoid 的全部。输入 z 很大时
exp(-z)趋近 0,输出趋近 1;z 很小(很负)时输出趋近 0;z = 0 时正好是 0.5。所以它把任意实数“压”进了 (0, 1)。 - 导数:Sigmoid 有个非常漂亮的性质——它的导数可以直接用输出 a 表示,即 a(1 − a),完全不用再碰输入。这让反向传播算起来特别省事(细节见第 6 章)。
再看现在最流行的 ReLU,简单到几乎像在开玩笑:
double ReluActivate::Activate(const double &x) {
return x > 0 ? x : 0; 1
}
double ReluActivate::DerivActivate(const double &output) {
return output > 0 ? 1 : 0; 2
}
- 前向:正数原样放过,负数一律压成 0。就这么简单。它的好处是计算极快,而且在正区间梯度恒为 1,不会像 Sigmoid 那样“饱和”到学不动。
- 导数:像一个开关——输出为正时斜率是 1,否则是 0。负区间梯度为 0,正是它偶尔会把神经元“关死”的原因(于是有了 LeakyReLU 这种改良版)。
最后看 Transformer 时代的宠儿 GELU。它不像 ReLU 那样“一刀切”,而是按“x 有多大概率为正”来平滑地决定保留多少,所以训练更稳:
// 精确 GELU = x * 0.5 * (1 + erf(x / sqrt(2)))
double GeluActivate::Activate(const double &input) {
return 0.5 * input * (1.0 + std::erf(input * kInvSqrt2)); 1
}
double GeluActivate::DerivActivate(const double &input,
const double &output) {
double cdf = 0.5 * (1.0 + std::erf(input * kInvSqrt2)); 2
double pdf = std::exp(-0.5 * input * input) * kInvSqrt2Pi;
return cdf + input * pdf; 3
}
- 前向:
std::erf是“误差函数”,这里用它算出 Φ(x)——也就是“标准正态分布里,取值小于 x 的概率”。直觉上,x 越大越该保留,GELU 就用这个概率去平滑地缩放 x。 cdf就是上面说的 Φ(x)。- 导数:GELU 的导数是 Φ(x) + x·φ(x)(φ 是正态分布的“钟形曲线”)。它处处平滑,没有 ReLU 在 0 处的那个硬拐角,这也是它训练大模型时表现更稳的原因之一。
你可能发现 GELU 有两个 DerivActivate:一个只接收 output,一个同时接收 input 和 output。
原因是:像 Sigmoid 那样能“只用输出就算出导数”的激活很省事,但 GELU 做不到,它必须知道原始的 z(也就是 input)才能算准导数。
所以项目里前向时顺手把每一层的 z 都存了下来——这个伏笔到第 6 章会用上。
小结
- 一个神经元只做两步:① 加权求和加偏置得到 z = Σ w·x + b;② 过激活函数 a = f(z)。
- 权重决定每条输入“有多重要”(可正可负),偏置是一个可调的“底分 / 门槛”。
- 没有激活函数,叠多少层都等价于一层线性变换——非线性是深度网络的命根子。
- 不同激活函数差别只在那条曲线的形状:Sigmoid 平滑开关、ReLU 一刀切、GELU 软开关。
- 代码里每个激活都配一个前向
Activate和一个导数DerivActivate,后者留给反向传播。
动手与思考
问题 1:在实验台里把 w1 调成负数,输出发生了什么?为什么?
对应输入越大,反而把总分 z 拉得越低。因为负权重表示“这条线索是反向证据”——它出现得越多,神经元越倾向于给出更低的激活值。
问题 2:如果把所有神经元的激活函数都拿掉,一个 100 层的网络还能学复杂模式吗?
不能。100 层线性变换叠加仍然等价于一层线性变换(W′x + b′),表达能力和单层一样,学不会任何非线性的、需要“拐弯”的关系。
问题 3:为什么 Sigmoid 的导数写成 output * (1 - output) 这么简洁,而 GELU 却需要 input?
因为 Sigmoid 的导数恰好能用它自己的输出 a 表示为 a(1−a);而 GELU 的导数公式 Φ(x)+x·φ(x) 里离不开原始的 x(input),光有输出反推不出来,所以必须把 z 存下来。
一个神经元只能做很有限的判断。下一章,我们把成百上千个神经元堆成“层”、再叠成“网络”, 看数据怎么一层层向前流动,最终算出预测。