对许多刚入门机器学习的开发者而言,许多参数和定义都显得抽象、难以理解,可能许多人直到开始进入实际的项目研发,都还没能真正搞清楚这些参数和定义的确切含义。为此,我在这里故意避开 scikit-learn 等现成的算法工具,从零开始自己用 Python 实现了一个感知机二元分类器,一方面通过实际代码深入认识了感知机的内部原理和相关参数的具体含义,另一方面也总结了一些自己的研发心得,希望对各位初学者有所帮助。
█ 什么是二元分类器(Binary Classifier)?
分类器是基于一组特征来确定输入元素所在类别的机器学习算法。例如,分类器可以根据一些既定特征,预测一个啤酒的类别。这些特征可以是酒精含量、香气和外观等。更详细一点,例如一个基于机器学习的分类器,根据 8% 的酒精含量、100 IBU (International Bitterness Unit,国际苦味指数)和强烈橙子香味,就能判断一种啤酒是不是 Indian Pale Ale 。
一般来说,机器学习可以分为三个主要的类型:无监督学习,监督学习和强化学习。分类器属于监督学习的范畴。所谓监督学习就是我们提前知道待解问题的答案,即期望的输出是已知的那些场景。例如在上述关于啤酒分类的例子中,我们完全可以想办法得到一组描述啤酒各种特征和类别的数据集,然后基于这组数据对分类器展开训练。
这里我将实现的是一个二元分类器,是所有分类器中最简单的一种,其输出结果只有两种:0 或 1 ,对或错。
█ 怎么搭建机器学习模型?
概括地说,要搭建和使用一个机器学习模型,一般分为如下四个步骤:
1. 预处理 2. 训练 3. 评估 4. 预测
预处理
预处理是构建机器学习模型的第一步,该步骤的主要工作是获取数据,并对数据进行必要的预处理,以备后续使用。包括去掉数据中的冗余、格式整理以及选定与数据相关的特征等。预处理中的常见工作包括:
从原始数据中提取特征 清理并格式化数据 删除多余的特征(或高度相关的特征) 优化特征数 标准化特征数据的范围(也称为特征缩放 Feature Scaling ) 随机拆分数据集:训练数据集和测试数据集
训练
准备好数据之后,下一步是为目标任务选择一个合适的算法。在下面的二元分类器中,我们选择的算法名为感知机(perceptron)。通常各种算法都有各自的优缺点,要根据目标任务灵活选择。
在这个步骤中,你可以先针对几个不同算法展开测试,然后根据测试结果选择性能最佳的算法。评估一个算法性能表现的方法有很多,在分类器场景中,一个最常用的方法是看分类精度(classification accuracy),即在所有输入样例中,正确分类的比例越高,算法就越优秀。在这个步骤中,开发者需要调整选定算法的参数,即所谓的超参数(Hyperparameters)过程。
本文将主要关注二元分类器的训练过程,深入探讨算法的内在工作原理。如果你对机器学习流程中的其他步骤感兴趣,可以通过文末链接阅读更多其他内容。
评估
当模型训练完成之后,就可以通过训练数据集之外的未知数据对模型展开评估。评估中一个非常重要的指标是泛化误差(Generalization Error),即一个算法面对未知数据集的预测精度究竟怎样。一旦你对评估结果满意,就可以通过模型进行真正的预测了。
█ 实现感知机
下面开始搭建我们的分类器。这里我们选用的算法是感知机(perceptron),它是神经网络与支持向量机的基础,是一种最简单的二元分类器模型。Perceptron算法的思路虽然简单,但功能强大,给定一个数据集,算法可以自动学习最佳权重系数,然后乘以输入特征,根据结果决定一个神经元是否启用。
下面我们根据具体代码简述感知机模型的基本实现流程。
首先,初始化一个权重等于零的数组,数组长度等于特征数加1。这里之所以加1,是为了存储“阈值”(threshold)。这里需要注意的是,Perceptron算法要求特征必须是数字值。具体代码如下:
self.w_ = np.zeros(1 X.shape[1])
第二步,开始一个迭代次数为 n_iter 的循环。这是一个由数据科学家定义的超参数。具体代码如下:
for _ in range(self.n_iter):
第三步,针对每个训练数据和结果都开始一个循环,这里的结果是指算法的最终期望输出。由于我们搭建的是一个二元分类器,因此结果是 -1 或 1 两种。
基于数据点的特征,算法将计算出最终结果:-1 或 1 。这里的预测方法具体是指特征与适当权重的矩阵乘积。在乘积的基础上加上此前定义好的阈值,如果结果大于 0 ,则预测为 1 ,否则为 -1.
算法可以根据每次迭代得到的预测结果的准确性灵活调整权重。在迭代的初期,预测结果一般不太可能是准确的,因为权重没有被调整过,也就不会收敛。需要注意的是,调整操作与目标值、预测值之间的差成比例,这个差值需要乘以 eta。这里 eta 是数据科学家定义的另一个超参数,介于 0 和 1 之间,eta 的值越大,权重的校正就越多。最终当预测结果准确时,就会停止调整权重的过程。具体代码如下:
self.w_ = np.zeros(1 X.shape[1]) for _ in range(self.n_iter): for xi, target in zip(X, y): update = self.eta * (target - self.predict(xi)) self.w_[1:] = update * xi self.w_[0] = update def net_input(self, X): """Calculate net input""" return np.dot(X, self.w_[1:]) self.w_[0] def predict(self, X): """Return class label after unit step""" return np.where(self.net_input(X) >= 0.0, 1, -1)
在代码中,只有当两个类别是线性可分时,感知机模型才会收敛。简单说就是:如果你能画一条直线来完全分离两个类,算法才会收敛。否则,算法将一直迭代下去,并将重新调整权重,直到循环达到最大次数 n_iter。
以上感知机的完整代码如下所示:
通过以上实践,我有如下几点收获:
█ 收获1:参数的理解
如果你直接调用 scikit-learn 等工具来实现感知机,那么像学习率和迭代次数这些参数就会显得很抽象,因为你只需要把它们填到 API 接口里,然后就得到了结果,完全不清楚这些参数的实际意义。但是如果你试着自己写代码来实现,例如自己实现一个感知机,那么这些参数的含义就一目了然。
学习率
例如学习率,就是指当预测不准确时权重被校正的比例,该值必须介于 0 和 1 之间。如下代码所示,fit 函数将对每个观察结果进行迭代,调用 predict 函数,然后根据目标和预测值之间的差异调整权重,然后乘以学习率。
更高的学习率意味着算法将更积极地调整权重。每次迭代都会根据预测值是否准确重新调整权重值。
# Partial portion of the "fit" function for xi, target in zip(X, y): update = self.eta * (target - self.predict(xi)) self.w_[1:] = update * xi self.w_[0] = update errors = int(update != 0.0)
迭代次数
迭代次数是指算法在训练集中运行的总次数。如果迭代次数设为 1,则算法就只在数据集上运行一次,针对每个数据点只更新一次权重。这样得到的模型相比较高迭代次数的模型,准确率可能更低。在数据集的体量较大时,高迭代次数可能引起非常高迭代成本。
for _ in range(self.n_iter): errors = 0 for xi, target in zip(X, y): update = self.eta * (target - self.predict(xi)) self.w_[1:] = update * xi self.w_[0] = update errors = int(update != 0.0) self.errors_.append(errors)
学习度和迭代次数通常是相互关联的,需要一起调整。例如,如果你的学习率很小,则意味着算法每次对权重的调整都很微小,那么可能就需要更多的迭代次数。
█ 收获2:线性代数的重要性
其次,特别重要的一点是:不单是Perceptron算法,在整个机器学习领域,线性代数课程中的相关内容都至关重要,因为整个算法都可以通过线性代数的相关公式来描述。而如果你从来没有学过线性代数的相关知识,那么这些公式对你来说就是不可见的,也就不利于算法的理解和实现。因此,学好线性代数对开发机器学习和理解各种算法至关重要,这里推荐一个线性代数的在线教程,并且附带练习。
教程地址:https://www.khanacademy.org/math/linear-algebra
█ 收获3:一种通用的学习方法
最后,我想通过以上 Perceptron 算法推荐一个通用的学习方法,即手动敲入代码,拒绝简单的复制粘贴。
早在2012年,当我在学习编写一个 Web 应用时就体会到了手动敲入代码的好处。当时,我花了比别人多得多的时间跟着教程,一步一步把案例中的代码手动敲入编辑器,而没有选择复制粘贴。这看起来很蠢,但不可否认这种方法真的有用。因为不可避免的,在手动敲入这些代码时你一定会引入错误,因此你敲完的代码可能根本就运行不起来,也可能得到一些意想不到的错误,这时你就必须排查和修改代码中的错误。其实,这个排查和修改的过程就是思考和学习的过程,通过这样的过程,你会对整个代码和教程中的知识点理解的更透彻,当然也记得更清楚。
所以,如果你要学习 Perceptron 算法,请不要直接复制和粘贴。试着将这些代码手动敲入编辑器,然后编译运行。更不要被动地阅读,仅仅对着代码读来读去,永远也成不了数据科学家,你必须参与进去,主动修改和运行这些代码,才能收获的更多。
原文地址:http://www.jeannicholashould.com/what-i-learned-implementing-a-classifier-from-scratch.html
深入阅读:http://www.jeannicholashould.com/learning-machine-learning.html