在之前介绍神经网络如何学习的文章中,我们重点讨论了训练如何调整权重和偏置:反向传播计算梯度,梯度下降利用梯度更新参数。本文将探讨前向传播:网络如何用当前参数把输入转换成输出。训练和推理都会执行这些计算。
向量存储表示,权重矩阵则变换这些表示。我们将用一个小到可以手算的分类器来跟踪这些操作:先看能用直线分开的点,再看位于交叉对角线上的四个 XOR 点,此时任何直线都无法完成分类。要让线性输出层分开 XOR 的两类,必须对其表示进行非线性变换。通过一个使用手工选择权重的隐藏层,我们可以逐步看清这个过程。
在更大的已训练网络中,知道如何计算每个输出,并不自动意味着我们知道内部表示编码了什么,或网络如何使用它们。
向量是一种通用的编码方式,可以表示对象、对象的属性以及对象之间的关系。同样的数学工具既能表示用于判断成熟度的苹果测量值,也能表示用于识别物体的图像像素,或用于语言理解与生成的概念和关系。
表示分析研究内部向量编码了哪些信息,以及这些信息如何组织。机制可解释性研究网络组件如何使用这些信息来产生行为。两者有所交叉,但在表示中找到某种信息,并不能单独证明网络使用了它。
本文假定你对以下线性代数概念有一定了解。重点是展示神经网络如何应用它们,而不是从零讲授。通过点分类的例子,我们会看到具体用途:
- **向量与矩阵:**一个向量如何表示一个点,矩阵如何存储一组点。
- **点积:**每个神经元如何结合输入与权重,得到一个标量加权和,然后再添加偏置和应用激活函数。
- **线性变换:**权重矩阵如何产生新坐标,以及它的行和列分别说明了什么。
- **矩阵乘法与复合:**连续的线性变换如何合并,以及为什么非线性激活通常让我们无法把整个网络合并成一个矩阵。
只有一个神经元的分类器
传统机器学习提供了多种分类方法,包括逻辑回归、决策树和支持向量机。这里我们用神经网络实现一个分类器。神经网络是深度学习的基础;这个小例子会展示相同的基本组成部分,同时让每一步计算都易于掌握。
先看一个可以用直线分开的数据集。假设有四个同品种的苹果,分别叫 A、B、C、D。下表中的每个点代表一个苹果,由两个连续测量值描述:颜色指标(x1,从更绿到更红)和柔软度指标(x2,从更硬到更软)。标签表示苹果未成熟(0)或已成熟(1)。
| 苹果 | 颜色(x1) | 柔软度(x2) | 类别 |
|---|
| A | −1 | −0.5 | 未成熟(0) |
| B | −0.5 | +0.5 | 未成熟(0) |
| C | +0.5 | +0.5 | 已成熟(1) |
| D | +1 | 0 | 已成熟(1) |
这些颜色和柔软度数值仅用于演示,并经过中心化和缩放。因此,零表示各特征的参考水平:相对于参考水平,负值意味着更绿或更硬,正值意味着更红或更软。
第一个网络有两个输入和一个输出神经元,没有隐藏层。
神经元先为每个苹果计算一个分数,再通过阈值预测类别。我们把神经元的两个权重组成单行矩阵 W,把苹果的两个输入组成列向量 x:
W=[w1w2],x=[x1x2]为了计算一个苹果的分数,我们将权重矩阵 W 乘以输入向量 x,再加上偏置 b。结果写作 s(x),其中 s 表示分数(score),x 包含苹果的颜色和柔软度测量值:
s(x)=Wx+b=[w1w2]×[x1x2]+b=w1x1+w2x2+b这里,Wx 计算的是两个向量的点积:一个是存储在 W 的行中的神经元权重向量 (w1,w2),另一个是苹果的输入向量 (x1,x2)。每个权重乘以对应输入,再把乘积相加,最后加上偏置,就得到输出分数。
乘法 Wx 本身已经是从 R2 到 R 的线性变换,也就是从平面到直线。变换可以把向量变成标量,不必保留坐标数量。构建隐藏层时,我们会通过基向量再来理解其几何意义。
这个模型是感知机,即单神经元线性分类器。它的阶跃激活函数把分数转换成预测类别标签 y^:
y^=step(s(x))={10若 s(x)>0(已成熟),若 s(x)≤0(未成熟)给数据集标注时,我们用 1 表示已成熟、0 表示未成熟。一个输出神经元就够了:正分数输出标签 1,零或负分数输出标签 0。
现在来看看神经元如何分类这四个苹果。在默认的二维视图中,颜色和柔软度决定苹果在平面上的位置。背景颜色显示每一对可能输入的预测类别,一条直线把未成熟和已成熟区域分开。标记的颜色表示已知标签,粉色圆环表示预测错误。
调整权重和偏置会改变决策区域,但苹果的位置保持不变。我们选取的测量值让两类线性可分,因此适当的参数能把四个苹果都放在正确区域中。
切换到斜视图(3D),可以看到这些预测背后的分数。输入平面承载颜色和柔软度坐标,蓝色平面则把计算出的分数表示为高于或低于输入平面的高度。两平面的交线处,分数为零;这正是二维视图中的边界线。
关于这些视图如何构建,详见如何阅读神经网络图形。
s(x) = 1.00x₁ + 1.00x₂ − 0.50
4 / 4 分类正确
● 未成熟(0)■ 已成熟(1)
图上方的公式是代入当前参数后的神经元分数计算式。滑块控制网络图中的权重 w1、w2 和偏置 b。这些正是训练时需要学习的参数。
在之前拟合直线的例子中,一个神经元学习一个权重和一个偏置。这里有两个输入测量值,因此有两个权重和一个偏置。梯度下降的思路相同:基于分数和已知标签的可微损失函数提供梯度,用于更新参数;硬阈值用于输出预测类别。
我们来跟踪图中直线背后的计算。初始设置为 w1=1、w2=1 和 b=−0.5,所以分数是 s(x)=x1+x2−0.5。决策边界由分数为零的点组成:
x1+x2−0.5=0这是一条直线。例如,对于坐标为 (0.5,0.5) 的点 C,将 x1=0.5 和 x2=0.5 代入分数公式,得到 s(xC)=0.5+0.5−0.5=0.5。分数为正,所以预测为已成熟(1)。对于坐标为 (−0.5,0.5) 的点 B,代入 x1=−0.5 和 x2=0.5,得到 s(xB)=−0.5+0.5−0.5=−0.5。分数为负,所以预测为未成熟(0)。这条直线将整个平面分成两种答案对应的区域。
我们也可以用一次矩阵乘法计算四个苹果的全部分数。这就是批处理:把输入向量收集到矩阵 X 中,每行一个苹果,两列分别是颜色和柔软度:
X=−1−0.50.51−0.50.50.50参数仍然是同样的两个权重和一个偏置。如果像图中一样从 w1=1、w2=1 和 b=−0.5 开始,权重矩阵及其转置为:
W=[11],W⊤=[11]偏置 b=−0.5 独立于 W,会加到每个苹果的加权和上。
对于写成列向量的单个苹果,我们使用 Wx。当苹果按行存储时,使用 XW⊤:转置将神经元的权重行变为列,让每个苹果的行都与同一组权重做点积。再给每个分数加上相同偏置,得到:
S=XW⊤+b1=−1−0.50.51−0.50.50.50×[11]+−0.5−0.5−0.5−0.5=−2−0.50.50.5这里 1 是由四个 1 组成的列向量,因此 b1 为四个苹果重复同一个偏置。S 的各行按 A–D 顺序保存分数。对苹果 A,第一行计算 1(−1)+1(−0.5)−0.5=−2,与单独处理时完全相同。对各分数应用阶跃函数,得到预测 (0,0,1,1),全部正确。批处理不会改变单个苹果的分数计算方式。 我们的批次包含全部四个样本;更大的数据集通常拆成小批次,参见批次大小如何影响训练。
一个神经元无法分开的四个苹果
继续使用四个苹果,特征仍是颜色(x1)和柔软度(x2)。我们手工改变输入值 (x1,x2),保留标签:A、B 仍为未成熟(0),C、D 仍为已成熟(1)。新坐标让两类分别位于正方形的两条对角线上:
| 苹果 | 颜色(x1) | 柔软度(x2) | 类别 |
|---|
| A | −0.5 | −0.5 | 未成熟(0) |
| B | +0.5 | +0.5 | 未成熟(0) |
| C | −0.5 | +0.5 | 已成熟(1) |
| D | +0.5 | −0.5 | 已成熟(1) |
这就是熟悉的 XOR 模式。我们把常见的二进制坐标 0 和 1 平移为 −0.5 和 +0.5,使它们位于每条坐标轴上零点的两侧。四个苹果构成以原点为中心的正方形:两个输入值符号相反的苹果标为已成熟(1),符号相同的标为未成熟(0)。
每一类占据一条不同的对角线。对角线相交,因此任何直线都无法把所有未成熟苹果放在一侧、所有已成熟苹果放在另一侧。这是感知机局限性的经典例子,马文·明斯基和西摩·派普特在 1969 年出版的 Perceptrons 中对此类局限进行了分析。
把更新后的数据送入原来的分类器时,同样的参数 w1=1、w2=1 和 b=−0.5 现在只能正确分类四个苹果中的一个。展开参数设置,尝试把 w2 改为 −1:这时能正确分类三个,但 C 仍然错误。
调整权重与偏置
s(x) = 1.00x₁ + 1.00x₂ − 0.50
为了解决 XOR,我们保留两个输入和一个输出神经元,再加入一个由两个使用非线性 ReLU 激活的神经元组成的隐藏层。它把每个苹果的测量值变换为新坐标 (h1,h2),输出神经元便能在这些坐标中用直线分开类别:
图中是一个多层感知机(MLP):由全连接层构成的网络,每个神经元都接收上一层的所有输出。它也是前馈网络(FFN),即信息从输入流向输出,没有反馈回路。其他架构使用不同类型的层。卷积神经网络(CNN)通过卷积层把共享滤波器应用于局部区域,也可以包含全连接层,例如执行最终分类。**循环神经网络(RNN)**使用在序列各步之间传递状态的循环层。
接下来我们分析这套网络背后的线性代数,并逐步用 NumPy 实现前向传播。先以单个苹果展开计算,再把每项操作批量应用于全部四个苹果。完整的 NumPy 示例会把这些操作串起来。
用向量和矩阵表示苹果
每个苹果由两个数构成的向量表示:颜色指标和柔软度指标。小写 x 表示一个苹果的输入向量,包含颜色 x1 和柔软度 x2。对于苹果 C,我们将向量写成一列:
xC=[−0.50.5]∈R2R2 是所有实数对构成的空间。它是二维的,因为定位任意点只需要两个独立的坐标方向:先沿第一条轴,再沿第二条轴移动。
可以把向量画成从原点出发的箭头,也可以用点标记其终点。两种画法编码相同的坐标。这里我们要分类样本,所以点很方便。
从苹果 C 的这个表示开始编写 NumPy 示例:
import numpy as np
# 一个苹果的表示:依次为颜色、柔软度。
x = np.array([-0.5, 0.5]) # 形状 (2,)
数学公式中的 xC 写成一列。在 NumPy 中,这个一维数组在与 W 相乘时充当输入向量。
表示空间是整个平面,数据集只包含其中四个点。四个样本不会让空间变成四维;每个样本依然只有两个坐标。
我们可以把四个向量收集到一个表示矩阵中,每行一个样本。大写 X 表示存储全部四个苹果的矩阵,每行一个输入向量。
X=−0.50.5−0.50.5−0.50.50.5−0.5∈R4×2行对应样本,即四个苹果;列对应特征,即颜色和柔软度。成熟度标签单独存储。
这些就是 XOR 图中的四个输入位置。箭头明确表明,每个点都代表一个从原点出发的向量。
表示矩阵既可以像这里的 X 一样存储原始输入向量,也可以存储上一层产生的向量。两种情况下,每行都代表一个样本;层与层之间改变的是坐标编码的信息,有时也包括坐标数量。我们把这种用途与变换矩阵区分开:X 存储表示,而权重矩阵 W 定义应用于它们的线性变换。
苹果已经有两个数值测量,所以很容易构成输入向量。对于其他数据,需要决定如何把每个样本表示为向量。一张 28×28 灰度图像可以变成按固定顺序排列的 784 个像素值组成的向量,一个词元可以表示为嵌入向量。向量的坐标数量决定了全连接层接收的输入数量。
即使再收集一百万个样本,这一层仍然只需要两个输入。
基向量如何定义线性变换
在研究层执行的变换之前,先看看矩阵如何通过输入基向量的去向描述线性变换。知道这些基向量落在哪里,线性性就决定了其他所有输入向量的去向。
在二维空间中,需要两个线性无关的向量才能构成一组基。我们使用通常的单位向量,分别沿两条坐标轴:
e1=[10],e2=[01]它们构成输入平面的标准基。把两个向量作为列,就得到单位矩阵:
I=[e1e2]=[1001]基矩阵让我们能够根据向量在该基下的坐标重建任意向量。 假设一个向量定义为
x=[x1x2]第一坐标 x1 缩放第一列 e1,第二坐标 x2 缩放第二列 e2。我们以 x1 和 x2 为系数,对基向量作线性组合,重建原始向量:
Ix=基向量的线性组合x1e1+x2e2=x1[10]+x2[01]=[x1x2]=x因此,要定义一个线性变换,可以为基向量 e1 和 e2 的落点选择新坐标,并把这些落点写入矩阵的列。然后继续使用同样的线性组合方式,保留输入向量原来的坐标作为系数,构造变换后的向量,确定它在输出空间中的位置。
假设线性变换把 e1 送到 [a,b]⊤,把 e2 送到 [c,d]⊤。变换矩阵将这些新落点记录为列:
M=[abcd],Me1=[ab],Me2=[cd]与前面一样,我们可以用原坐标作为系数,对变换后的方向作线性组合,重建变换后的输入向量。用 x1 缩放 M 的第一列,用 x2 缩放第二列,再将结果相加:
M[x1x2]=x1[ab]+x2[cd]=[ax1+cx2bx1+dx2]输入坐标 (x1,x2) 变成了输出坐标 (ax1+cx2,bx1+dx2)。
有些线性变换会把平面压缩到一条直线或一个点。这些仍是有效的变换,神经网络可以借此丢弃信息,同时保留对任务有用的区别。
同一规则适用于网格中的每个点,也包括数据集之外的点。接下来把它应用到隐藏层的矩阵。
层的矩阵定义了一个变换
先看隐藏层如何为任意输入向量生成新表示,再选择具体权重,跟踪苹果 A 的矩阵乘法,算出它的向量落点。
隐藏层的权重矩阵 W 就是我们的变换矩阵:它定义输入数据或上一层向量的线性变换。其形状取决于输入和神经元的数量:若有 d 个输入、m 个神经元,则 W 有 m 行、d 列。
每个神经元定义一行权重,用于计算一个输出坐标。正如基向量部分所示,每一列给出一个输入基向量在所有输出坐标上的落点。因此,增加神经元会增加一个输出坐标,而不是一个输入基向量。可以把该坐标对应到输出空间的一条轴,但神经元的权重向量本身并不是那个输出基向量。
我们的层有两个神经元和两个输入,因此 W 有两行两列。每行包含一个神经元针对颜色(x1)和柔软度(x2)的权重。
W=[1−0.8−0.81]这些权重本来是网络在训练中学习的参数。这里我们手工选择它们,让每一步都精确而易于检查。
为了应用变换,我们将输入数据向量 x 与 W 按 Wx 的顺序相乘。这会为每个神经元计算一个加权和。
除了权重矩阵,隐藏层还有偏置向量 b,两个神经元各有一个偏置。加上 b 后得到 z=Wx+b,即激活前向量。乘法与偏置相加共同构成仿射变换:先作线性变换,再平移。每个结果都加上相同的偏置向量。线性变换总把原点映射到原点;添加偏置后,原点移到 b。下例中 b=0,没有平移,所以这个映射也是线性的。
b=[00]随后,ReLU把 z 中每个负坐标替换为零,得到隐藏层输出 h=(h1,h2)。这是苹果的新表示,输出神经元将用它计算分数。完整过程为:
xWWx+bzReLUh在 NumPy 中定义相同的变换矩阵和偏置:
# 两个输入坐标 -> 两个激活前坐标。
# 行存储神经元权重;列记录输入基向量的像。
W = np.array([[1.0, -0.8],
[-0.8, 1.0]]) # 形状 (2, 2)
b = np.array([0.0, 0.0]) # 偏置向量;此处没有平移
我们以苹果 A 为例,但下方图形可以跟踪任意一个苹果经过同一矩阵 W 的过程。A 的原始向量为
xA=[−0.5−0.5]根据列的线性组合规则,用原坐标和 W 的列,就能逐步计算 WxA。
-
把 W 的列理解为基向量 e1 和 e2 的落点。 第一列告诉我们 e1=(1,0) 落在哪里,第二列告诉我们 e2=(0,1) 落在哪里:
W=[1−0.8−0.81],We1=[1−0.8],We2=[−0.81].
这些就是原始基向量经过 W 变换后的结果。
-
以 A 的原始坐标为系数。 如前所述,对变换后的基向量作线性组合,可以在变换后的空间中构造 A 的向量:
WxA=−0.5(We1)+(−0.5)(We2)
系数保持不变;与它们相乘的向量现在是变换后的方向。 第一坐标 −0.5 缩放 W 的第一列,即变换后的第一基向量 We1。第二坐标同样是 −0.5,它缩放第二列,即变换后的第二基向量 We2。
完整计算如下:
WxA=−0.5[1−0.8]+(−0.5)[−0.81]=[(−0.5)×1(−0.5)×(−0.8)]+[(−0.5)×(−0.8)(−0.5)×1]=[−0.50.4]+[0.4−0.5]=[−0.5+0.40.4+(−0.5)]=[−0.1−0.1]
两个贡献部分抵消,留下 (−0.1,−0.1)。我们用 A 的原始坐标组合变换后的基方向。同样的规则也适用于 B,它的两个系数都为 +0.5:
WxB=0.5[1−0.8]+0.5[−0.81]=[0.10.1]A 和 B 向原点靠近,但仍是不同的点。矩阵改变了它们的表示,没有将它们合并。
下方动画同时展示这两步:基向量箭头移到 W 记录的落点,选中的苹果则跟随以原坐标为系数的线性组合移动。蓝色网格执行同样的变换,原始网格保持不动,作为参照。
选择 A、B、C 或 D,再点击变换。初始选中 A,其向量从 (−0.5,−0.5) 移到 (−0.1,−0.1)。
原始值: (−0.50, −0.50)→当前: (−0.50, −0.50)
观察两个对角线方向,就能看出变换的作用:
W[11]=0.2[11],W[1−1]=1.8[1−1]A–B 方向缩短到原长的五分之一,C–D 方向则伸长为原来的 1.8 倍。两个方向都没有消失。这改变了四点正方形的形状,却保留了类别对角线的交叉。单靠线性变换,XOR 还不可分。为此需要通过 ReLU 加入非线性,后面会继续讨论。
下面用已定义的矩阵 W 在 NumPy 中实现这个变换。@ 运算符执行矩阵乘法:
x_A = np.array([-0.5, -0.5]) # A 的原始坐标
# 对整个向量应用 W。
transformed_A = W @ x_A
print(transformed_A) # [-0.1 -0.1]
在矩阵与向量相乘时,W @ x_A 等价于用 x_A 的对应坐标缩放 W 的各列,再把得到的向量相加。我们可以明确写出这个线性组合:
# W[:, 0] 是第一列;W[:, 1] 是第二列。
transformed_A = x_A[0] * W[:, 0] + x_A[1] * W[:, 1]
print(transformed_A) # [-0.1 -0.1]
神经元如何共同变换一个向量
上面,我们用层的变换矩阵变换了苹果 A 的输入向量:
W=[1−0.8−0.81],xA=[−0.5−0.5]⟼WxA=[−0.1−0.1]现在看看矩阵如何由层中的神经元和输入向量的维度构成,以及每个神经元的计算具体体现在哪里。
在全连接层中,每个神经元都接收同一个完整输入向量。每个苹果有两个输入坐标,所以每个神经元需要两个权重:一个对应 x1,另一个对应 x2。我们有两个神经元,各提供一行权重:
| 神经元 | x1 的权重 | x2 的权重 | W 的行 |
|---|
| 1 | 1 | −0.8 | (1,−0.8) |
| 2 | −0.8 | 1 | (−0.8,1) |
每行保存一个神经元的权重,每列记录一个输入基向量的落点。例如,第一列包含两个神经元针对 x1 的权重。对于输入 e1=(1,0),这些权重给出两个输出坐标 We1=(1,−0.8)。所以一列汇集了两个神经元的贡献。
将这些行堆叠起来,就得到 2×2 的矩阵 W。更一般地,若全连接层有 d 个输入坐标和 m 个神经元,则有
W∈Rm×d,x∈Rd⟼Wx∈Rm输入维度决定列数;神经元数量决定行数和输出坐标数。这些数量决定矩阵的形状。 权重值决定它执行什么线性变换。例如,两个神经元接收三维输入,就会得到一个 2×3 矩阵,把三个输入坐标映射为两个输出坐标。
有趣的是,以输入坐标为系数对矩阵的列作线性组合,与让矩阵每一行和输入向量作点积,会得到完全相同的变换后向量。
上一节中,我们用 A 的原始坐标缩放各列并相加,得到了 WxA=(−0.1,−0.1)。现在从神经元的计算中得到同一结果:每个神经元将自己的权重行与完整输入向量作点积,产生一个输出坐标。
对固定权重,这个点积定义了一个接收输入向量、返回单个标量数值的函数:fi(x)=wi⋅x。第一个神经元的这个标量值线性函数为 f1(x)=x1−0.8x2,它将 R2 映射到 R。
因此,变换矩阵以及它描述的层的线性部分,可以看作这样一组函数,每个神经元一个。它们的结果组成 T(x)=(f1(x),f2(x))⊤=Wx,即从 R2 到 R2 的映射。
因为偏置为零,这两个函数的结果也就是激活前坐标。展开点积得到
z=Wx=[w1⋅xw2⋅x]=[1⋅x1+(−0.8)⋅x2(−0.8)⋅x1+1⋅x2]对于同一个苹果 A,两个神经元都收到 (−0.5,−0.5):
| 神经元 | 计算 | 产生的坐标 |
|---|
| 1 | 1(−0.5)+(−0.8)(−0.5)=−0.1 | z1=−0.1 |
| 2 | (−0.8)(−0.5)+1(−0.5)=−0.1 | z2=−0.1 |
把这两个数合起来,得到 zA=(−0.1,−0.1),正是列组合构造出的那个向量。神经元的加权和共同完成层的矩阵乘法。 每个神经元贡献结果中的一个坐标,而不是独自应用整个矩阵。每个苹果都使用相同的权重。
两种视角只是把同一组算术运算分组的方式不同。一列汇集一个输入坐标对所有神经元的贡献;一行汇集一个神经元计算其输出坐标所用的全部权重。没有哪一列只属于某个神经元。我们选择的 W 是对称矩阵,所以行和列恰好包含相同的数,但它们的作用不同。
列的线性组合x1[1−0.8]+x2[−0.81]=[x1−0.8x2−0.8x1+x2]=行的点积[(1,−0.8)⋅(x1,x2)(−0.8,1)⋅(x1,x2)]我们可以在 NumPy 中明确写出各个神经元的计算:
# 每行是一个神经元的权重向量。
z1_A = np.dot(W[0], x_A)
z2_A = np.dot(W[1], x_A)
z_A = np.array([z1_A, z2_A])
print(z_A) # [-0.1 -0.1],与 W @ x_A 相同
加上偏置后,每个神经元的函数成为仿射函数;再应用 ReLU,则成为非线性函数。它们的输出共同构成隐藏表示 h。下文再研究 ReLU 的作用。为了继续苹果 C 的前向传播代码,我们使用先前定义的输入向量 x,一次计算两个神经元的加权和并加上偏置:
z = W @ x + b
print(z) # [-0.9 0.9],对应苹果 C
把所有输入向量组成表示矩阵后,就能用一次矩阵乘法将层的变换应用于四个苹果。 将 A、B、C、D 放在 X 的各行,两个输入坐标放在各列。
对一个列向量,我们使用 Wx。由于批次按行存储向量,我们使用 XW⊤:转置 W 将每个神经元的权重放入一列,让每个苹果的行与之作点积。
转置来自我们的存储约定:样本是 X 的行,神经元权重是 W 的行。如果权重按每列一个神经元存储,就直接使用 XW。转置只是匹配维度,不改变我们要执行的变换。
偏置为零时,计算为
Z=XW⊤=−0.50.5−0.50.5−0.50.50.5−0.5×[1−0.8−0.81]=−0.10.1−0.90.9−0.10.10.9−0.9Z 每行保存一个苹果的变换后向量,每列保存一个神经元在整批样本上的加权和。A 对应的第一行仍然是 (−0.1,−0.1),与单独处理完全一样。W 恰好对称,所以 W⊤ 和 W 的元素相同,但转置表达了样本按行存储时的正确运算。
在 NumPy 中,沿用先前定义的 W 和 b:
# 每行一个输入向量,依次为 A、B、C、D。
X = np.array([
[-0.5, -0.5],
[ 0.5, 0.5],
[-0.5, 0.5],
[ 0.5, -0.5],
])
# (4 个苹果, 2 个输入) @ (2 个输入, 2 个神经元) -> (4, 2)
# 每个苹果所在的行与每个神经元的权重计算点积。
Z = X @ W.T + b # 给每行加上同一个偏置向量
print(Z)
# [[-0.1 -0.1] A
# [ 0.1 0.1] B
# [-0.9 0.9] C
# [ 0.9 -0.9]] D
批处理把计算组织到一起,不改变任何单个向量的变换。每个苹果都以同样的权重和偏置经过两个神经元;一个苹果的测量值不会与另一个苹果混合。
激活让新的表示变得可分
到目前为止,我们计算了隐藏层的线性部分:每个神经元产生 z=Wx 的一个坐标。由于偏置为零,没有平移。激活前坐标如下:
| 点 | 类别 | z=Wx |
|---|
| A | 0 | (−0.1,−0.1) |
| B | 0 | (0.1,0.1) |
| C | 1 | (−0.9,0.9) |
| D | 1 | (0.9,−0.9) |
A–B 线段变短,C–D 线段变长,但仍然相交。直线无法分开这两个类别。
W 前W 后两幅图的比例相同。 线段连接同类点,仍在原点相交。
隐藏层还有一步操作。每个神经元对自己的加权和应用非线性的 ReLU 激活。这些结果共同组成隐藏层输出向量 h:
ReLU(z)=max(0,z),h=ReLU(z)它保留正值,将负值替换为零。两个隐藏坐标变为
h1=max(0,x1−0.8x2),h2=max(0,x2−0.8x1)h1 衡量 x1−0.8x2 的正值部分,h2 衡量 x2−0.8x1 的正值部分。这就是隐藏层计算的两个特征。
要观察 ReLU 对每个苹果表示的影响,选择 A、B、C 或 D,然后点击应用 ReLU。图形从 W 产生的坐标开始。初始选中 A,其向量从 (−0.1,−0.1) 变为 (0,0)。选择其他苹果时,动画会重置到该苹果激活前的坐标。
ReLU 前: (−0.10, −0.10)→当前: (−0.10, −0.10)
以下是四个苹果在 ReLU 前后的坐标,与图中的结果一致:
| 点 | 类别 | ReLU 前:z | ReLU 后:h |
|---|
| A | 0 | (−0.1,−0.1) | (0,0) |
| B | 0 | (0.1,0.1) | (0.1,0.1) |
| C | 1 | (−0.9,0.9) | (0,0.9) |
| D | 1 | (0.9,−0.9) | (0.9,0) |
A 的两个坐标都为负,因此移到原点。B 的两个坐标都为正,所以保持在 (0.1,0.1)。C、D 各失去一个负坐标,落到不同的正半轴上。两类的线段不再相交。
在 NumPy 中,将 ReLU 应用于前面计算的批次矩阵 Z。maximum 将每个坐标与零比较,保留较大值。H 每行保存一个苹果的隐藏表示:
# 保留正坐标,将负坐标替换为零。
H = np.maximum(0, Z)
print(H)
# [[0. 0. ] A
# [0.1 0.1] B
# [0. 0.9] C
# [0.9 0. ]] D
ReLU 并不把输出限制为 0 或 1。 它在这里保留了 0.1 和 0.9 等数值。只有最后的分类阈值操作才只返回 0 或 1。
输出层对隐藏表示进行分类
隐藏层为每个苹果产生了一个新向量。输出神经元接收其两个坐标 h1 和 h2。
输出神经元有自己的权重 V 和偏置 c。训练时,这些参数会与隐藏层的权重和偏置一同学习。这里为了演示,我们手工选取:
V=[11],c=−0.5它的分数又是一次点积,然后加上偏置:
s(h)=Vh+c=[11]×[h1h2]+(−0.5)=1⋅h1+1⋅h2−0.5=h1+h2−0.5和之前一样,正分数预测已成熟(1),零或负分数预测未成熟(0)。决策边界包含分数为零的表示:
h1+h2−0.5=0⟹h1+h2=0.5这个阈值位于两类的坐标和之间:
A、B 的坐标和小于 0.5,C、D 则大于它,因此直线 h1+h2=0.5 能分开两类。我们选择的阈值不是唯一答案:任何严格介于 0.2 和 0.9 之间的值都能分开这四个表示。
现在看看完整网络如何在原始输入平面上分类苹果。图形默认打开俯视图(2D),显示决策区域;在我们选择的参数下,四个苹果都分类正确。切换到斜视图(3D)可以看到 (x1,x2,s(x)):每个苹果的原坐标,以及用高度表示的最终分数。第三条轴用来可视化标量输出;隐藏表示仍然只有两个坐标。蓝色分数曲面在隐藏神经元从零输出切换到正输出的位置形成折痕,与输入平面的交线就是决策边界。调整权重和偏置滑块可以探索曲面及预测如何变化,点击重置可恢复本文参数。
调整权重与偏置
隐藏神经元 1: h1 = ReLU(1.00x₁ + (-0.80)x₂ + (0.00)) 隐藏神经元 2: h2 = ReLU(-0.80x₁ + (1.00)x₂ + (0.00)) 输出: s = 1.00h₁ + (1.00)h₂ + (-0.50) 调整隐藏层权重可改变折痕,调整隐藏层偏置可移动折痕,调整输出权重可改变它们对分数的贡献。输出偏置会升高或降低整个曲面。重置可恢复文章中的数值。
s(x) = Σⱼ vⱼ ReLU(wⱼ₁x₁ + wⱼ₂x₂ + bⱼ) + c
在隐藏空间中,边界是直线;在原始输入空间中,它会转折。A、B 位于未成熟区域,C、D 则位于它两侧的已成熟区域。对于这组权重,两侧的已成熟区域会在正对角线更远处、四个样本之外连接起来。正确分类四个样本,并不能规定网络在其余所有位置应该预测什么。
在 NumPy 中实现输出层时,我们将它的权重和偏置应用于四个隐藏表示。H 每行一个苹果,计算 S=HV⊤+c,并给每个分数加上相同偏置:
# 输出变换:两个隐藏坐标 -> 一个分数。
V = np.array([[1.0, 1.0]]) # 形状 (1, 2)
c = -0.5 # 输出偏置
# (4 个苹果, 2 个隐藏坐标) @ (2, 1) -> (4, 1)
# 每行与相同的输出权重计算点积。
S = H @ V.T + c
predictions = (S > 0).astype(int) # 1 = 已成熟,0 = 未成熟
for apple, score, prediction in zip("ABCD", S[:, 0], predictions[:, 0]):
print(f"{apple}: score={score:.1f}, prediction={prediction}")
# A: score=-0.5, prediction=0
# B: score=-0.3, prediction=0
# C: score=0.4, prediction=1
# D: score=0.4, prediction=1
为什么只用 ReLU 无法解决这个数据集
如果隐藏层的权重使用单位矩阵 I,偏置为零,会怎样?它会计算 h=ReLU(Ix)=ReLU(x),等同于直接对原始输入应用 ReLU。结果依然是 XOR 布局:负坐标变成零,使 A 落在 (0,0),B 落在 (0.5,0.5),C 落在 (0,0.5),D 落在 (0.5,0)。这是一个更小正方形的四个顶点,两类仍各占一条对角线。任何直线都无法分开它们。
切换原始输入、ReLU(x) 和 ReLU(Wx),比较三种布局。第二种使用单位矩阵权重,第三种使用本文的 W。两种隐藏层方案都从原始输入向量出发,偏置均为零。
原始输入:两个类别的对角线相交。
每个网格方格为 0.1 × 0.1。三种视图的坐标轴比例相同。
A: (−0.5, −0.5)B: (0.5, 0.5)C: (−0.5, 0.5)D: (0.5, −0.5)
没有一条直线能分开这两个类别。
矩阵 W 先改变坐标,使 ReLU 作用在更有用的布局上。它将 B 放在 (0.1,0.1),C、D 分别放在 (−0.9,0.9) 和 (0.9,−0.9)。随后 ReLU 让 B 留在原点附近,将 C、D 放到更远的正半轴上,并把 A 移到原点。这些表示可以用直线分开。
单独使用 W,或者直接对输入使用 ReLU,都无法解决这个例子;两者结合才让类别线性可分。 如前所示,输出层可以分开得到的表示。
层是函数的复合
有些书将神经网络描述为把向量从一个向量空间映射到另一个向量空间的函数的复合。隐藏层把 R2 映射到 R2,产生隐藏表示;输出层把 R2 映射到 R,产生一个标量分数。每个函数接收前一个函数的结果。这些函数可以是线性的,也可以是非线性的,正如权重矩阵和 ReLU 所展示的那样。
对于我们的网络,将隐藏层函数记为 F,输出层函数记为 G:
F(x)=ReLU(Wx+b),G(h)=Vh+c因此,我们在图中跟踪的前向传播就是
xFhGs(x)把隐藏层输出代入输出层函数,得到
s(x)=G(F(x))=V×ReLU(Wx+b)+c这就是这里的函数复合。常用记号 G∘F 表示先应用 F,再应用 G。更深的网络遵循同样的模式 FL∘⋯∘F2∘F1:每层变换接收到的表示,再把结果交给下一层。
没有非线性激活,各层可合并为一个仿射映射
你可能读到过:没有非线性激活,所有层都能合并成一层。用我们的两个权重矩阵很容易看清这一点。暂时去掉 ReLU。因为隐藏层偏置为零,输出神经元直接收到 Wx:
s无 ReLU(x)=V(Wx)+c=(VW)x+c可以先将 V 与 W 相乘,把乘积用作一个权重矩阵:
VW=[11]×[1−0.8−0.81]=[1⋅1+1⋅(−0.8)1⋅(−0.8)+1⋅1]=[0.20.2]于是网络计算
s无 ReLU(x)=0.2x1+0.2x2−0.5我们又回到原来单个神经元的分数形式:两个加权输入加一个偏置,决策边界是一条直线。具体到这组权重,A、B、C、D 的分数分别为 −0.7、−0.3、−0.5 和 −0.5,全部预测为未成熟。修改权重可能改善预测,但任何单一的仿射分数函数都无法正确分类整个 XOR 布局。
所谓“合并”,指的是网络计算的函数:一个仿射层可以产生与整组层完全相同的输出。非零偏置也不会改变结论;它们会合并成一个偏置:
V(Wx+b)+c=(VW)x+(Vb+c)重复这个计算,就能将任意一串仿射层合并为一个仿射映射。最后应用阶跃阈值,决策边界仍是直线;阈值本身无法产生非线性隐藏层的效果。
矩阵乘法记录复合后的变换
这与我们把列看作输入基向量落点的视角直接相关。W 的列告诉我们 e1、e2 经过第一次变换后落在哪里。对这些落点应用 V,就知道它们经过两次变换后的结果:
VW=[V(We1)V(We2)]=[0.20.2]每个输入基向量最终都落在输出数轴的 0.2 处。对于任意输入,同样的列组合规则因此给出 (VW)x=0.2x1+0.2x2。
更一般地,两个变换矩阵相乘,就是将左矩阵应用于右矩阵的每一列。如果先应用 M1,再应用 M2,它们的乘积就记录输入基向量的最终落点:
xM1M1xM2M2(M1x)=(M2M1)x最右侧的矩阵先作用。乘积把两个线性变换表示成一个。
ReLU 改变下一层接收的内容
现在将 ReLU 放回 W 和 V 之间。输出神经元接收的是 ReLU(Wx),不再直接相加 W 产生的两个坐标,而是相加它们的非负部分:
s(x)=max(0,x1−0.8x2)+max(0,x2−0.8x1)−0.5例如,C 经过 W 后的坐标为 (−0.9,0.9)。没有 ReLU 时,它们抵消,得到分数 −0.5。有了 ReLU,输出神经元收到 (0,0.9),得到分数 0.4。相同的输出权重现在能给出正确预测,因为它们接收到不同的表示。
VReLU(Wx)=(VW)x一般情况下各层仍然是函数的复合,但我们不能用单个仿射层替代这套网络。 ReLU 根据符号保留或清零每个坐标,使完整分数成为原始输入的非线性函数。
这也把分类器图形中的两种视角联系起来。在隐藏空间中,输出边界是直线 h1+h2=0.5。代入隐藏层函数后,就得到原始输入坐标中的同一条边界:
max(0,x1−0.8x2)+max(0,x2−0.8x1)=0.5只有第一个神经元激活时,方程化为 x1−0.8x2=0.5;只有第二个激活时,化为 x2−0.8x1=0.5;两者都激活时,化为 0.2(x1+x2)=0.5。这些直线片段在拐角处连接起来,形成输入平面中看到的边界。
每个新苹果都会经过同样的函数复合:测量值变为隐藏表示,再变为分数和预测标签。因此,隐藏空间中的直线边界可以对应原始空间中的非线性边界。
用 NumPy 同时处理四个苹果
现在把这些操作合并成一个可运行的 NumPy 示例。在整个前向传播中,每行始终代表一个苹果:
4×2X⟶4×2ZReLU4×2H⟶4×1S沿用每行一个样本的约定:Z=XW⊤+b、H=ReLU(Z) 和 S=HV⊤+c,每个偏置都加到每一行。**批次大小与表示大小是不同的维度:**四个样本保持不变,每个苹果的两个隐藏坐标变成一个输出分数。
import numpy as np
# 表示矩阵 X,形状为 (4, 2):
# 行 = 苹果 A、B、C、D;列 = 颜色和柔软度。
X = np.array([
[-0.5, -0.5],
[ 0.5, 0.5],
[-0.5, 0.5],
[ 0.5, -0.5],
])
labels = np.array([0, 0, 1, 1]) # 目标标签,与 X 分开存储
# 变换矩阵 W,形状 (2, 2),所有苹果共用。
# 行存储神经元权重;列是变换后的基向量。
W = np.array([
[ 1.0, -0.8],
[-0.8, 1.0],
])
b = np.array([0.0, 0.0])
# 输出变换 V,形状 (1, 2):两个坐标 -> 一个分数。
V = np.array([[1.0, 1.0]])
c = -0.5
# 样本按行存储,因此转置 W,以便计算每个
# 苹果与每个神经元权重的点积:(4, 2) @ (2, 2)。
# NumPy 给每行加上同一个偏置向量。
Z = X @ W.T + b # 形状 (4, 2)
# ReLU 将负坐标替换为零。
H = np.maximum(0, Z) # 形状 (4, 2):新的表示
# 逐行复合隐藏层与输出层的映射:
# (4, 2) @ (2, 1) -> (4, 1),然后加上同一个标量偏置。
scores = H @ V.T + c # 每个苹果一个分数
predictions = (scores[:, 0] > 0).astype(int)
print(H)
# [[0. 0. ] A
# [0.1 0.1] B
# [0. 0.9] C
# [0.9 0. ]] D
print(scores[:, 0]) # [-0.5 -0.3 0.4 0.4]
print(predictions) # [0 0 1 1]
X 和 H 描述不同空间中的同一组样本。W 和 V 定义空间之间的变换。它们之间的激活让整个网络能够表达非线性规则。
为什么改变表示会有帮助
我们跟踪了四个苹果经过网络的过程,但目的从来不只是移动点,而是构造一套坐标,让我们关心的区别更容易表达。
XOR 让这个道理变得具体。在原始颜色–柔软度平面上,没有直线能分开两类。权重矩阵改变布局的形状,ReLU 再作非线性变换。在得到的表示中,一个加权和加一个阈值就足够了。分类问题没有改变;改变的是用来解决它的表示。
这是网络通过几何和计算表达规则的一个小例子。隐藏向量编码输入的派生属性,输出神经元结合这些属性作出判断。规则由完整的操作序列实现。
更大的网络可以学习坐标更多的表示,编码对任务有用的属性和关系。架构确定可用的维度,训练调整输入在其中的表示方式。概念不必对应单个坐标:信息可以分布在多个坐标中,再由后续层使用。网络还可以把特征编码在相互重叠的方向上,表示比维度更多的特征;这一现象称为叠加(superposition)。
手工选定参数的网络让我们能够观察有用特征如何被计算和使用。线性代数提供变换,非线性激活扩展这些变换的能力,训练则在权重和偏置构成的空间中寻找使损失最小的组合。每个组合都定义一个从输入到输出的候选函数。