第一次看到神经元的结构图时,它很容易显得比逻辑回归复杂:左边有许多输入,中间汇聚到一个圆圈,右边再输出一个数。图里还会出现权重、偏置、激活函数,仿佛我们已经跨进了一个全新的模型世界。
这一篇先不搭建多层网络。我们只拆开一个最小的计算单元,跟着一条样本走完预测、计算误差和更新参数的过程,看看它与逻辑回归究竟是什么关系。
第一步:模型先给出一个分数
假设我们要判断一封邮件是否为垃圾邮件,并从每封邮件中取出两个特征:
- :某类关键词出现的频率;
- :邮件中链接的数量。
一封邮件的这两个值组成输入 :
和 来自样本。模型还需要判断每个特征对结果有多大影响,因此为它们分别准备一个可以调整的数: 对应 , 对应 。这两个数叫作权重,合在一起记为 :
权重不是样本提供的,而是模型自己的参数。训练开始前,我们先给它们一个初始值;在后面的训练过程中,再根据预测误差反复调整它们。本文的代码会从 开始。
模型还有一个不与特定输入相乘的参数 ,叫作偏置。它控制没有任何特征贡献时的基础分数,也会和权重一起在训练中被调整。
现在,模型把每个特征乘上对应的权重,再加上偏置,得到一个分数:
这里的两个特征是 和 ;、 是分别控制它们影响力的两个参数。得到的 只是一个分数,它可以是 、 或 ,不能直接解释成概率。我们还需要一个“压缩器”,把任意大小的分数压到 和 之间。
Sigmoid:把分数压成概率
这个压缩器就是 Sigmoid 函数。它的图像像一条平滑的 S 形曲线:
σ(z) = 1 / (1 + e⁻ᶻ)这里的 是一个约等于 的数学常数,就像圆周率 一样。实际写代码时,NumPy 会替我们完成指数运算,不需要手算。
也不必先记住公式的每个符号。先观察它的输入和输出:
| 输入分数 | Sigmoid 输出 | 直观含义 |
|---|---|---|
| 很接近类别 | ||
| 更偏向类别 | ||
| 两个类别之间的分界点 | ||
| 更偏向类别 | ||
| 很接近类别 |
Sigmoid 有三个重要性质:
- 无论输入多大或多小,输出始终在 和 之间。
- 输入越大,输出也越大,不会打乱模型原来的排序。
- 它是平滑、可导的,因此可以通过梯度来调整前面的权重。
如果任务是判断一封邮件是否为垃圾邮件,我们可以把 解释为“属于类别 的概率”。例如 表示模型非常倾向于类别 , 表示它更倾向于类别 。
不过,一个数落在 和 之间,并不会自动成为可靠的概率。 为了训练模型,我们还需要一个评分规则:答对时给出较小的惩罚,答错时给出较大的惩罚。
这条链在两种语境中有两套名字:
| 逻辑回归中的说法 | 神经元中的说法 | 实际计算 |
|---|---|---|
| 特征 | 输入 | |
| 系数 | 权重 | |
| 截距 | 偏置 | |
| 线性预测值 / logit | 预激活值 | |
| Logistic 逆链接函数 / Sigmoid | 激活函数 | |
| 预测概率 | 神经元输出 |
因此,就前向计算而言,一个采用 Sigmoid 激活的单个神经元与逻辑回归相同。 “神经元”这个词本身并不保证它一定使用 Sigmoid,也不意味着所有神经元都等于逻辑回归。
一次前向传播到底发生了什么
取一个具体例子:
第一步是加权求和:
第二步是激活:
到这里,前向传播已经结束。神经元没有在内部进行某种难以观察的“思考”,它只完成了两个明确的变换:
输入 x
↓
线性组合 z = wᵀx + b
↓
Sigmoid p = σ(z)
↓
概率 p
如果我们规定 时预测为类别 ,这条样本会被分到类别 。但要注意,阈值判断通常是推理阶段的决策规则,不是训练过程本身。训练需要一个连续、可微的量来衡量预测到底错了多少。
二元交叉熵:给概率预测打分
这个评分规则就是二元交叉熵。先拆开这个名字。
- 二元表示答案只有两种: 或 。
- 交叉熵在这里可以先理解为:比较“真实答案”和“模型给出的概率分布”有多不一致。
假设真实标签是 。模型给类别 的概率是 ,说明它既答对了,也很有把握;如果只给出 ,说明它非常自信地答错了,理应受到更大的惩罚。
如果真实标签是 ,判断方向就反过来。此时 是一个好预测,因为留给类别 的概率是 。
我们可以先只看模型分给“正确类别”的概率 :
| 真实标签 | 类别 1 概率 | 正确类别概率 | 损失 | 判断 |
|---|---|---|---|---|
| 答对且比较自信 | ||||
| 自信地答错 | ||||
| 答对且比较自信 | ||||
| 自信地答错 |
是自然对数。这里暂时不需要掌握对数运算,只需要记住它制造的评分规则:,而 越接近 , 就越大。
的作用很直观:正确类别的概率越接近 ,损失越接近 ;正确类别的概率越接近 ,损失就会急剧增大。它不仅关心模型有没有跨过 的分类线,也关心模型有多确定。
现在再看完整公式:
它看起来有两项,但每条样本实际上只会使用一项:
- 当 时,,公式只剩 。
- 当 时,,公式只剩 。
这就是代码里把 y 和 1 - y 同时写进去的原因。它只是在用一个公式同时处理两种标签。
损失函数本身并不负责修改参数。它只是把“预测概率有多不好”变成一个标量。真正让模型学习的是下一步:计算这个标量相对于每个参数会怎样变化。
最关键的梯度:预测减去标签
把 Sigmoid 与二元交叉熵放在一起,会得到一个非常简洁的结果:
可以沿着计算链验证它:
将两项相乘并整理:
由于 ,继续使用链式法则:
可以看作从输出端返回的误差信号:
- 若真实标签是 ,预测只有 ,则 。梯度下降会推动参数让下一次的 变大。
- 若真实标签是 ,预测却是 ,则 。更新会推动下一次的 变小。
在一批 条样本上,我们通常取平均梯度:
然后执行一次梯度下降:
其中 是学习率。至此,“学习”不再是一个抽象动词:它就是根据误差计算梯度,再对 与 做一次很小的修改。
用 NumPy 训练一个神经元
下面的实验只使用 NumPy 完成前向传播、损失计算和梯度更新。两团二维点分别代表类别 和类别 ;橙色直线是神经元最终学到的 决策边界。
点击“运行”后,Python 会在浏览器的 Web Worker 中执行,数据和代码不会发送到服务器。第一次需要下载 Pyodide、NumPy 与 Matplotlib,因此会比后续运行慢。
运行代码后,文本与图表会显示在这里。
代码中最值得盯住的不是画图部分,而是训练循环里的四行:
z = x @ w + b
probability = sigmoid(z)
error = probability - y
dw = x.T @ error / len(x)
它们正好对应刚才的推导:线性组合、Sigmoid、输出端误差和权重梯度。框架以后会替我们自动完成求导,但自动微分并没有改变这条计算链。
运行结果里还有两件事值得观察。
第一,交叉熵随着更新逐渐下降。这说明当前参数产生的预测越来越符合训练标签。第二,二维平面上出现了一条直线。令 等价于令 ,所以决策边界满足:
无论训练多久,这个神经元都只能移动或旋转这条直线,不能把它弯曲。
既然计算相同,为什么还要叫神经元
因为接下来的关注点不再是“完成一次二分类”,而是“把计算单元连接起来”。
单独看时:
x → Linear → Sigmoid → p
它是一套完整的逻辑回归模型。放入网络以后,一个单元的输出可以成为下一层许多单元的输入:
x₁ ─┐ ┌→ h₁ ─┐
├→ weighted sum ─→│ ├→ next layer
x₂ ─┘ └→ h₂ ─┘
此时,权重不再只是在原始特征上画一条最终的分类边界。前面的单元可以先把输入变换成新的表示,后面的单元再基于这些表示做判断。“神经元”这个名称强调的正是它作为网络组成部分的身份。
不过,复制许多个线性计算还不够。如果层与层之间没有非线性激活,无论叠多少层,整个网络仍然可以合并成一次线性变换。这也是下一篇必须讨论激活函数的原因。
这一步还没有解决什么
理解“逻辑回归就是一个 Sigmoid 神经元”并不等于已经理解了神经网络。至少还有三条边界需要保留:
- 单个神经元只能学习线性决策边界。 两类数据如果无法被一条直线或一个超平面分开,它就无能为力。
- 神经元不一定使用 Sigmoid。 ReLU、Tanh、GELU 都可能出现在网络中,它们有不同的输出形状与梯度性质。
- “神经元”只是计算抽象。 生物学类比有助于命名输入、连接和激活,但人工神经元远不是生物神经元的忠实模型。
第一条边界会把我们带到 XOR:为什么一个线性模型永远无法解决某些看起来很小的问题?第二条会把我们带到激活函数:非线性究竟为表示能力增加了什么?
留下一个可以复述的模型
这一篇真正需要带走的不是某一段 NumPy 代码,而是下面这条完整链条:
输入 x
↓
z = wᵀx + b
↓
p = sigmoid(z)
↓
L = binary_cross_entropy(p, y)
↓
error = p - y
↓
计算 dw、db
↓
更新 w、b
当它被当作一个完整模型时,我们叫它逻辑回归;当它被当作可连接的计算单元时,我们叫它一个 Sigmoid 神经元。
神经元并没有凭空引入一种新的学习方法。它继承了已经熟悉的线性模型、概率、损失、梯度和更新。真正的新问题从这里才刚刚出现:当一条线不够时,我们怎样把许多这样的单元组合成非线性的表示?