追踪边界
探讨为什么感知机用直线作为决策边界是不够的。
我们花了本课程的大部分时间来构建分类器:先是感知机,然后在最近几章中构建了一个完整的神经网络。然而,我们可能仍难以直观地理解分类器的工作原理。为什么感知机在某些数据集上表现良好,而在其他数据集上却不行?神经网络具备而感知机不具备的关键特性是什么?之所以难以回答这些问题,是因为我们很难在脑海中描绘出分类器实际运作时的图像。
接下来的几节课将聚焦于建立这种直观的心理图像。一个名为决策边界的新概念,将帮助我们可视化感知机和神经网络是如何“看待”世界的。这一洞察不仅能丰富我们的理论理解,也将使我们在未来更轻松地设计和调优神经网络。
让我们从回顾感知机开始。
绘制线性可分数据的边界
若要直观理解分类过程,我们需要一个易于可视化的数据集。像 MNIST 这样具有上百个维度的高维数据过于复杂,不适合此目的。因此,我们将使用一个更简单、更适合人类大脑理解的数据集:
| 输入_A | 输入_B | 标签 |
|---|---|---|
| -0.470680718301 | -1.905835436960 | 1 |
| 0.9952553595720 | 1.4019246363100 | 0 |
| -0.903484238413 | -1.233058043620 | 1 |
| -1.775876322450 | -0.436802254656 | 1 |
该文件共包含 300 个样本,每个样本有两个输入变量和一个二分类标签。plot_data.py 将两个输入变量作为坐标,在图中标记每个点,并根据其标签用 蓝色方块 表示类别 0,用 绿色三角形 表示类别 1,如下图所示:
这些是两个划分清晰的类别。我们甚至可以在蓝色方块和绿色三角形之间画一条直线将其分开。在之前的【感知机】章节中我们学到,感知机擅长处理线性可分的数据。现在让我们通过重新使用本课程早期的感知机代码来验证这一点。
感知机在其擅长领域的表现
让我们在 linearly_separable.txt 数据集上运行感知机。首先,以下是加载和格式化数据的代码:
1import numpy as np 2x1, x2, y = np.loadtxt('linearly_separable.txt', skiprows=1, unpack=True) 3X_train = X_test = prepend_bias(np.column_stack((x1, x2))) 4Y_train_unencoded = Y_test = y.astype(int).reshape(-1, 1) 5Y_train = one_hot_encode(Y_train_unencoded)
在【训练与测试】部分我们了解到,真实机器学习系统应将训练数据与测试数据分离。但这里并非真实系统,因此训练集和测试集使用了相同的数据。除此之外,上述代码对数据的预处理方式与我们处理 MNIST 时类似:为样本添加偏置列,将标签转换为整数(因为 loadtxt() 返回的是浮点数),并对标签进行独热编码。
现在让我们在数据上运行感知机:
1import perceptron 2import numpy as np 3 4# 独热编码 5def one_hot_encode(Y): 6 n_labels = Y.shape[0] 7 n_classes = 10 8 encoded_Y = np.zeros((n_labels, n_classes)) 9 for i in range(n_labels): 10 label = Y[i] 11 encoded_Y[i][label] = 1 12 return encoded_Y 13 14# 加载数据,添加偏置,并对训练集进行独热编码 15x1, x2, y = np.loadtxt('linearly_separable.txt', skiprows=1, unpack=True) 16X_train = X_test = perceptron.prepend_bias(np.column_stack((x1, x2))) 17Y_train_unencoded = Y_test = y.astype(int).reshape(-1, 1) 18Y_train = one_hot_encode(Y_train_unencoded) 19 20# 调用训练函数 21w = perceptron.train(X_train, Y_train, X_test, Y_test, 22 iterations=10000, lr=0.1)
运行结果如下:
0 - 损失: 6.93147180559945397249, 准确率: 50.00%
1 - 损失: 6.64033931138695887597, 准确率: 100.00%
…
9999 - 损失: 0.01506250845005210372, 准确率: 100.00%
可以看出,10000 次迭代完全是多余的:感知机仅用一次迭代就在该数据集上达到了 100% 的准确率。
现在我们尝试可视化其背后的原因。
深入理解分类机制
我们或许还记得在【模型函数去哪了?】一节中,曾可视化过二分类器的模型函数。现在我们对该具体分类器也做同样的事情。下图展示了训练结束后模型函数的形态:
这个 S 形曲面就是分类器的模型函数。感知机在训练过程中生成了这个曲面,并不断调整模型参数以逼近数据点。稍作观察即可发现,曲面上分布着模型试图拟合的蓝色方块和绿色三角形。
在预测阶段,分类器将此模型应用于任意输入点(即任意的输入 A 和输入 B 值)。对于该点,模型输出一个介于 0 和 1 之间的预测值 ŷ。随后分类器将该值四舍五入到最近的整数:小于 0.5 的值被归为 0,判为 蓝色方块;大于 0.5 的值被归为 1,判为 绿色三角形。
现在考虑一个看不见的边界——即模型输出 ŷ 正好等于 0.5 的所有点构成的集合。这个边界被称为决策边界,因为一个点最终被分类为蓝色方块还是绿色三角形,取决于它落在该边界的哪一侧,如下图所示:
从某种意义上说,分类器的核心工作正是围绕决策边界展开的。在训练过程中,分类器寻找一条能够将蓝色方块与绿色三角形分开的边界;在分类过程中,它利用这条边界判断新数据点属于哪一类。
有些概念在二维平面上更容易理解。下图是前一张图的平面投影,展示了被划分为 蓝色 或 绿色 的区域以及它们之间的边界:
该图显示,决策边界完美地分开了两个类别:所有蓝色方块都在蓝色区域内,所有绿色三角形都在绿色区域内。这意味着如果让感知机对这些点进行分类,它将返回正确的标签。如果感知机准确率低于 100%,那么我们会看到一些 蓝色方块 出现在 绿色 区域内,或反之。
无论我们如何调整感知机的权重,其决策边界始终是一条直线。感知机无法拟合曲线。
需要说明的是,我们这里所说的“直线”是因为示例数据只有两个输入变量。如果输入变量更多,边界将是更高维空间中的“直线”,例如三维空间中的平面,或 785 维空间中的超平面。不过我们可以通俗地说,在所有情况下,感知机的决策边界都是一条“直线”。
我们知道感知机在线性可分数据上表现良好,这正是由其直线型决策边界直接决定的。毕竟,“线性可分”的定义就是“能用一条直线分开的数据”。
现在让我们看看,当数据不是线性可分时,用感知机训练会发生什么。
直线不足以解决问题
下面是另一个数据文件的几行示例:
| 输入_A | 输入_B | 标签 |
|---|---|---|
| 0.185872557346525 | 0.567645715301291 | 0 |
| 0.284770005578376 | 0.458145376240974 | 1 |
| 0.150041107877389 | 0.453290078176759 | 0 |
这个数据集看起来与之前相似,但当我们绘制出来时却发现不同:
该数据集无法用一条直线将蓝色方块和绿色三角形完全分开。如果我们在这个数据集上训练感知机,它的表现会很差:
0 - 损失: 6.93147180559945397249, 准确率: 36.00%
1 - 损失: 6.65950860649818565662, 准确率: 64.00%
…
9999 - 损失: 1.10368339183411090509, 准确率: 73.33%
即使增加迭代次数或尝试不同的学习率,我们也无法使其准确率达到 80% 以上。这种性能不佳的原因在我们画出感知机的决策边界后变得一目了然:
感知机试图切出一块包含蓝色方块的区域,但仍有大量 蓝色方块 被错误分类为 绿色三角形。
到目前为止,本章只是从一个新的角度再次印证了一个我们已知的事实:感知机只适用于线性可分的数据。但我们并不局限于感知机,对吧?接下来,让我们转向目前最先进的分类器:神经网络。