本章采用面向对象的方法将感知机接口定义为Python中的一个类,这样可以初始化新的Perceptron对象,而且这些对象可以运行fit方法从数据中学习,还可以通过predict方法预测标签。按照惯例,在对象初始化时,变量名后面加一个下划线(_)表示未赋值的属性。可以通过调用对象的其他方法来初始化这些未赋值的属性,例如调用self.w_。
Python科学计算资源
如果你不熟悉或需要重温Python科学计算库,可以参考以下资源:
·NumPy:https://sebastianraschka.com/blog/2020/numpy-intro.html。
·pandas:https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html。
·Matplotlib:https://matplotlib.org/stable/tutorials/introductory/usage.html。
以下为感知机的Python代码实现:
基于上述感知机实现,可以使用学习率eta( η )和学习次数n_iter(训练模型时遍历训练数据集的次数)初始化新的Perceptron对象。
通过fit方法,初始化偏置self.b_为初始值0,初始化权重self.w_为一个属于R m 的向量,其中 m 表示数据集中的维数或特征数。
请注意,初始化权重向量的命令为rgen.normal(loc=0.0,scale=0.01,size=1+X.shape[1]),产生均值为0、标准差为0.01的正态分布随机数。其中,rgen是NumPy的一个随机数生成器,用户可以设定随机种子,这样可以在需要时复现以前运行的结果。
从技术上讲,可以将权重初始化为0(实际上最初的感知机算法就是这么做的)。然而,如果这样做,学习率 η (eta)就失去了对决策边界的影响。当所有权重都初始化为0时,学习率参数eta只影响权重向量的大小,而不影响其方向。如果熟悉三角函数,考虑向量 v 1 =[1 2 3], v 2 =0.5× v 1 ,那么向量 v 1 与向量 v 2 间的夹角为0度,如下述代码所示:
这里np.arccos是反余弦函数,np.linalg.norm是一个计算向量长度的函数。(这里使用标准差为0.01的正态随机数并非故意为之,也可以使用均匀分布的随机数。切记,使用值很小的随机数是为了避免向量中所有元素为0的情况)。
作为阅读本章后的选做练习题,可以将self.w_=rgen.normal(loc=0.0,scale=0.01,size=X.shape[1])替换为self.w_=np.zeros(X.shape[1]),并改变eta的值,运行下一小节介绍的感知机训练代码,会发现决策边界没有变化。
NumPy数组索引
NumPy一维数组索引方式与Python中使用方括号([])的列表一样。对于NumPy二维数组,第一个索引是数组的行,第二个索引是数组的列。例如,使用X[2,3]表示二维数组X中位于第三行第四列的元素。
初始化权重后,调用fit方法遍历训练数据集中的所有样本,并根据上一节讨论的感知机学习规则更新权重。
在训练阶段,predict函数被fit方法调用,获得预测标签以更新权重。也可以在模型拟合后调用predict函数预测新样本的类别标签。此外,在每次迭代中使用fit方法收集分类错误的样本并记入self.errors_列表中,用于后期分析感知机在训练阶段的表现。net_input方法使用np.dot函数计算两个向量的点积。
向量化:使用向量计算替代for循环
为了计算数组a和b的向量点积,在Python中,可以用sum([i * j for i,j in zip(a,b)])实现,如果使用NumPy,则可以用a.dot(b)或np.dot(a,b)实现。然而,与使用Python相比,使用NumPy的好处是可以实现算术运算向量化。向量化意味着自动将算术运算应用于数组的所有元素。算术运算向量化不对每个元素完成一套运算,而是把算术运算形成一连串的数组指令,这样就能更好地使用CPU的单指令多数据(Single Instruction Multiple Data,SIMD)架构。另外,NumPy采用高度优化的C或Fortran语言编写线性代数库,例如基本线性代数子程序(Basic Linear Algebra Subprograms,BLAS)和线性代数包(Linear Algebra Package,LAPACK)。最后,使用NumPy我们可以借助线性代数的基本知识(如向量和矩阵点积)将计算机代码写得更紧凑、更自然。
为了测试前一小节实现的感知机代码,本章剩余部分使用的样本只有两个特征变量(维度)。尽管感知机规则并不限于二维,但仅使用两个特征(萼片长度和花瓣长度)可以在散点图中可视化训练模型的决策区域,以便于我们学习模型。
请注意,由于感知机是二元分类器,这里也只考虑两种类型的花,即山鸢尾和变色鸢尾。然而,也可以推广感知机算法解决多元分类问题,例如,使用一对多(One-versus-All,OvA)方法。
用于多元分类的OvA方法
OvA有时也被称为一对其余(One-versus-Rest,OvR),是一种将二元分类器用于多元分类任务的方法。OvA为每个类别训练一个分类器,此类别被视为正类,所有其他类别都被视为负类。如果要对新的未标记数据样本进行分类,使用 n 个分类器对样本分类,其中 n 为类别标签的数量,并将置信度最高的分类器输出的标签作为最终预测标签。在使用感知机情况下,OvA选择与最大净输入值对应的类别作为分类标签。
首先,使用pandas从UCI机器学习库将鸢尾花数据集加载到DataFrame对象中,并用tail方法打印数据的最后五行,以检查是否正确加载数据:
运行上述代码后,可以看到如图2.5所示的输出,这里显示了鸢尾花数据集的最后五行:
图2.5 鸢尾花数据集的最后五行
加载鸢尾花数据集
如果无法上网或UCI的服务器(https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data)宕机,可以从本书的代码集下载鸢尾花数据集(也包括本书所有其他的数据集)。如果本地计算机存储了鸢尾花数据集,可从本地文件加载鸢尾花数据,使用
替换如下代码:
接下来,提取50朵山鸢尾和50朵变色鸢尾对应的100个类别标签,并将类别标签转换为两个整数,分别为1(山鸢尾)和0(变色鸢尾)。将转换后的类别标签存入向量y中。pandas中DataFrame的values方法生成的NumPy数据可以作为向量y。
类似地,从这100个训练样本中提取第一个特征列(萼片长度)和第三个特征列(花瓣长度),并将它们存入特征矩阵X中,这可以通过二维散点图可视化:
运行上述代码后,可以看到图2.6所示的散点图。
图2.6显示了鸢尾花数据集在“花瓣长度”和“萼片长度”两个特征维度上的样本分布情况。从这个二维特征分布图中可以看出一个线性决策边界足以将山鸢尾与变色鸢尾分开。因此,像感知机这样的线性分类器应该能够完美地对数据集中的花朵进行分类。
现在,在刚刚提取的鸢尾花数据子集上训练感知机算法。此外,绘制每次迭代的分类错误,以检查算法是否收敛,并找到两种鸢尾花数据的决策边界:
图2.6 山鸢尾和变色鸢尾萼片长度与花瓣长度的散点图
理想情况下
,参数更新的次数与样本误分类的次数相同。运行上述代码后,可以得到参数更新次数与epoch次数之间的关系图,如图2.7所示。
图2.7 参数更新次数与epoch次数的关系图
如图2.7所示,感知机在第6个epoch后开始收敛。在此之后,感知机应该能够完美地对训练样本进行分类。下面通过一个短小精干的函数实现二维数据集决策边界的可视化:
首先,定义许多colors和markers,并通过ListedColormap从颜色列表中创建一个colormap。然后,确定两个特征的最小值和最大值,调用NumPy的meshgrid函数创建一对网格数组xx1和xx2。由于在两个特征维度上训练感知机,因此需要展平网格数组,并创建一个与鸢尾花训练数据集具有相同列数的矩阵,以便可以使用predict方法预测相应网格点的类别标签lab。
将预测的类别标签lab重塑为与xx1和xx2尺寸相同的网格后,可以通过Matplotlib中的contourf函数绘制轮廓图,将网格数组中不同预测类的决策区域映射为不同的颜色:
在运行上述代码后,可以看到如图2.8所示的决策区域图。
图2.8 感知机的决策区域图
正如图2.8所示,感知机学习到了一个决策边界,可以完美地对鸢尾花训练数据集进行分类。
感知机收敛问题
虽然感知机可以完美地区分两种类别的鸢尾花,但收敛问题是感知机的最大问题之一。Frank Rosenblatt已经从数学上证明,如果包含两个类别的数据可以被一个线性超平面分离,那么感知机学习规则收敛。然而,如果不存在这样一个能将数据中的两个类别完全分开的线性决策边界,那么除非设定最大epoch次数,否则算法将永远不会停止权重更新。有兴趣的读者可以从下述网站找到关于该问题的证明:https://sebastianraschka.com/pdf/lecture-notes/stat453ss21/L03_perceptron_slides.pdf