在前面一部分,我们构造了几种分类算法,而在这一部分,我们将介绍利用scikit-learn
提供的便于使用的接口进行机器学习
将分成以下几个部分进行介绍:
- 对具有良好稳定性的常用分类算法的介绍,例如
logistic
回归算法,支持向量机,决策树 - 一些使用
scikit-learn
进行一些常见的应用 - 对于线性与非线性决策边界的优势与劣势的讨论
选取分类算法
俗话说:天下没有免费的午餐,不同的分类算法具有相应的优势与劣势,在选择的时候需要充分考虑数据的特征以及目的,一般来说,训练一个机器学习模型可以分为以下五步
- 选取特征收集训练数据
- 选取一个表示矩阵
- 选取一个分类与优化算法
- 计算这个算法的效果
- 改良这个算法
使用scikit-learn
训练一个算法
为了快速使用scikit-learn
库,我们将延续前面的例子,使用那个花的分类模型进行练习
我们这次将从sklearn
的datasets
库中加载数据集
1 | from sklearn import datasets |
Class labels: [0 1 2]
在这里,我们可以发现,数据被分成了三类(为了妥善处理内存问题并且加快速度,一般只会拿整数进行标号)
为了更好的判断一个数据集训练的有多好,我们将数据分割成训练集和测试集(这一部分在未来会进一步讨论)
1 | from sklearn.model_selection import train_test_split |
stratify
的指定表明在分割的训练集与测试集中,y
的三种不同值被均等分割
相似的,为了更好的进行机器学习,我们需要对数据进行标准化处理,在这里,我们使用正态分布进行标准化
1 | from sklearn.preprocessing import StandardScaler |
需要注意,我们要对训练集和测试集采用相同的归一化手段,这样可以才能实现统一
接下来我们就可以进行训练了.我们在这里使用OvR
方法对多分类问题进行处理,代码如下
1 | from sklearn.linear_model import Perceptron |
Perceptron(eta0=0.001, n_iter_no_change=100, random_state=1)
就像前面所说的一样,我们需要正确的选择学习率,如果学习率过高,那么模型会跳过全局最低点,而如果学习率过低,学习速率又会太低.
我们可以使用predict
对训练后的模型进行预测,例如以下代码
1 | y_pred=ppn.predict(X_test_std) |
Miss classfied samples:1
当然,scikit-learn
库里面也内置了处理学习准确率的函数
1 | from sklearn.metrics import accuracy_score |
Accuracy: 0.97
接下来,我们仿照上一章的例子,编写决策区间的绘图函数
在其中,我们添加了一些修改来展现出数据是来自于测试集的
1 | import matplotlib.pyplot as plt |
我们可以将刚才训练的模型的效果展示出来
1 | plot_decision_regions(X_train_std,ytrain,ppn) |
使用logistic回归构建类模型
虽然我们之前构建的分类算法在机器学习分类中非常好用,但是这中间有个非常严重的问题,那就是当几个类不能被线性分割时,将永远不会收敛
为了节约我们的时间,我们将介绍另外一种强而有力的线性分类模型:logistic回归模型
需要注意,这里虽然用了”回归”这个名字,但是算法是用来分类的
logistic回归的介绍和条件概率
为了理解logistic回归,我们首先介绍比值比,比值比可以被写作$\frac{p}{1-p}$(其中$p$是我们所想预测的概率),接下来我们就可以定义比值比的对数
$$logit(p)=\log\frac{p}{1-p}$$
需要注意到,我们可以将发生预测事件分类为$y=1$,认为某一特征与$logit(p(y=1|x))$之间呈现线性关系
而我们的工作是预测某一个样本属于某一类的可能性,即从比值比的对数求$p$,那么我们可以使用一个s型生长曲线
$$\phi(z)=\frac{1}{1+e^{-z}}$$
其中$z$是输入,为$z=w^Tx$
为了更加直观的看出这个的效果,我们首先绘制一幅s型生长曲线的图像
1 | def sigmoid(x): |
我们可以仿照上一章的例子,就是将ADaline
中的线性函数变成了生长曲线,而对于生长曲线的输出则是以0.5为阈值大于0.5为1,小于0.5为0
学习logistic
误差函数
接下来我们讨论如何拟合模型中的参数,我们之前定义过平方和误差函数
$$J(\omega)=\sum_i\frac{1}{2}(\phi(z^{(i)}-y^{(i)})^2$$
我们的目的就是让这个函数值最小,首先我们需要定义在已知$x$下,$y$的后验概率分布
$$L(\omega)=P(y|x;\omega)=\Pi_{i=1}^nP(y^{(i)}|x^{(i)};\omega)=\Pi_{i=1}^n(\phi(z^{(i)}))^{y^{(i)}}(1-\phi(z^{(i)}))^{1-y^{(i)}}$$
对于实际应用之中,往往取对数可以让问题更加方便,因此最后误差函数在定义时就可以被写作:
若$y=1$则为$-\log(\phi(z))$,若$y=0$则为$-\log(1-\phi(z))$
这么做的好处在于,我们对于错误的估计,误差函数会逐渐加大,从下面这张图可以看出来
1 | def cost_0(z): |
将Adaline
内置logistic回归
我们在之前的Adaline
算法中,可以做出以下修改:
- 将线性激发函数修改为生长激发函数
- 修改分类阈值从-1到1修改为0到1
1 | #TODO 这些内容还需要修改,未来改一下 |
使用scikit-learn
训练一个回归模型
我们刚才的讨论是基于数学计算上的区别,现在我们来介绍一下如何使用scikit-learn
来训练一个回归模型
1 | from sklearn.linear_model import LogisticRegression |
我们在看这个问题的时候,会很好奇这个训练参数C
是什么东西,我们将在下一小节介绍这个问题
这个问题主要涉及到关于过拟合和欠拟合
我们还可以计算出每一个元素属于某一类的概率,需要使用predict_proba
方法
1 | lr.predict_proba(X_test_std[:3,:])*100#这里乘100为了方便用百分数 |
array([[5.83527138e-11, 4.26528227e-03, 9.99957347e+01],
[9.99623181e+01, 3.76819349e-02, 3.51225598e-17],
[2.32430493e+00, 9.76756905e+01, 4.61949531e-06]])
相应的,我们可以用这个来进行预测
1 | lr.predict_proba(X_test_std[:3,:]).argmax(axis=1) |
array([2, 0, 1], dtype=int64)
一个需要注意的问题
在使用predict
时,如果预测是一个单一样本,那么就需要进行reshape
1 | lr.predict(X_test_std[0,:].reshape(1,-1)) |
array([2])
通过正则化处理过拟合
过拟合在机器学习中是一个常见的问题,过拟合主要来自于过于复杂的模型,模型非常容易受到一噪音的影响,而欠拟合则是相反的情况,主要来自于过于简单的模型约束过多
虽然我们现在讨论的是线性模型,但是很快我们就会遇到一些更加复杂的决策区间
这个时候我们就需要使用正则化来调节模型的自由度,这种手段可以有效的处理特征之间的相关性,消除数据误差和防止过拟合有重要作用
正则化的原理是通过添加约束来消除极端变量的值,最为常用的是L2正则化
,可以被写成下式
$$\frac{\lambda}{2}||\omega||^2=\frac{\lambda}{2}\sum_{j=1}^m\omega_j^2$$
那么我们之前所使用的损失函数就可以被加上这一项$\frac{\lambda}{2}||\omega||^2$
这个有点像拉格朗日乘子法,添加了一个约束,但是这个$\lambda$是一个预先设定好的量
而如果$\lambda$越大,正则化强度越高,之前的参数$C$就是和这个密切相关,是它的倒数
我们用绘制两个参数随着$C$变化的变化展示L2正则化
强度对机器学习结果的影响
1 | weights,params=[],[] |
可以注意到,$C$越小,相应的正则化强度越高,导致参数越大
使用支持向量机处理最大边界问题
另外一个强力而广泛应用的算法是支持向量机算法Support Vector Machine(SVM)
,这可以被看做是感知机的升级版.
在感知机算法中,我们的目标是让误分类误差尽量小,但是在支持向量机算法中,我们的目标是让边界最大
边界被定义为决策边界和离决策边界最近的点的距离,又被称作支持向量support vectors
,下图可以展现出来
最大边界
大的边界在于这种手段可以得到尽量小的整体误差,但是小的边界更加容易发生过拟合.为了更好地理解最大边界,我们来考量一下正负决策边界,这些边界与决策边界平行,可以用下式来表述
$$\omega_0+\omega^Tx_{pos}=1$$
(到标号为1的点的分割面)
$$\omega_0+\omega^Tx_{neg}=-1$$
(到标号为-1的点的分割面)
假如我们两式相减,我们可以得到
$$\omega^T(x_{pos}-x_{neg})=2$$
我们可以借助$\omega$的长度对这个式子进行归一化
$$\frac{\omega^T(x_{pos}-x_{neg})}{||\omega||}=\frac{2}{||\omega||}$$
左侧的式子可以被看做正负决策边界的距离,这就是我们希望最大化的东西
现在,问题就转变为让$\frac{2}{||\omega||}$最大(前提是可以使用两个面的分开,满足对于所有的i有 $y^{(i)}(\omega_0+\omega^Tx^{(i)})\ge 1$ 这样的约束(线性约束))
在实际问题中,往往使用二次规划来解决这个问题,但这实在是有点复杂,我们不再涉及
通过添加松弛变量应对无法线性完全分类问题
我们来简单介绍松弛变量$\xi$,这种被称作软边界分类问题.引入松弛变量的目的是线性约束需要被松弛来解决无法线性完全分类问题来实现对存在误分类的优化
正值松弛变量就是简单的减在线性约束上
$$y^{(i)}(\omega_0+\omega^Tx^{(i)})\ge 1-\xi^{(i)}$$
因此新的最优化问题可以被看做让以下式子最小
$$\frac{1}{2}||\omega||^2+C(\sum_i \xi^{(i)})$$
通过修改$C$,我们可以控制错误分类的惩罚,下面这张图展示了不同的$C$的效果
这一观念与正则化有关,就像我们所讨论的$C$一样,减少$C$的值会增加约束并减少自由度
现在,我们来训练一个支持向量机的模型来给花分类
1 | from sklearn.svm import SVC |
在scikit-learn
中作为代替的实现
在一些特殊的情况下(比如数据集奇大无比),可以去参考SGDClassifier
实现
使用一个SVM核来解决非线性问题
支持向量机方法如此流行还有一个原因在于支持向量机可以非常轻易的被内核化(kernelized
),在我们深入讨论SVM核的数学机理之前,我们首先来看一个例子
对无法线性区分的数据的核方法
在接下来的数据集中,我们将创建一个简单的X状数据使用logical_xor
函数
1 | np.random.seed(1) |
可以看出来,这两种数据有着显著的区别,但是又明显不能线性区分开来,这个时候就要使用核
来解决问题
对于核方法最为简单的解释就是将数据通过指定的非线性组合,扩展到高维,然后在高维就可以实现区分,即
$$\phi(x_1,x_2)=(z_1,z_2,z_3)=(x_1,x_2,x_1^2+x_2^2)$$
而这样做的效果用下面一张图可以很好的体现出来
原本区分不开的两簇点通过扩展到三维很轻易地区分开来
使用核方法在高维空间中找到决策边界
为了处理上述问题,我们首先需要利用一个投影函数$\phi$将训练数据投影到高维空间,然后再训练一个支持向量机模型,最后再将原本的投影函数取反来进行预测.
然而,这种方法说起来简单,但是实际运作起来(尤其是面对高维数据)非常困难,因此我们需要使用到核技巧(kernel trick
),核技巧的数学原理再次不再过多涉及
核函数可以被看做给向量空间定义了一个全新的点乘
粗略地说,核这个词可以被理解为一对样本之间的关联函数,例如说最为常用的高斯核函数Gaussian kernel
就是如下定义:
$$\mathcal{K}(x^{(i)},x^{(j)})=exp\left(-\frac{||x^{(i)}-x^{(j)}||^2}{2\sigma^2}\right)$$
(其中的$\sigma$是一个拟合自由度)其得到样本中两个样本差距的模长,并且取$e$的负指数来实现定义两个样本之间的相近程度
现在我们来看刚才的问题,我们来试试高斯核能不呢解决这个问题
1 | svm=SVC(kernel='rbf',random_state=1,gamma=0.1,C=10) |
而我们使用的$\gamma$参数,可以被理解为切断参数,$\gamma$越大,我们会得到一个越紧凑的决策边界,我们可以拿之前那个花分类的问题来进行讨论
1 | svm=SVC(kernel='rbf',random_state=1,gamma=0.2,C=1.0) |
因为我们的$\gamma$值比较小,所以看起来还不错,接下来,我们把$\gamma$放大,看看会怎么样
1 | svm=SVC(kernel='rbf',random_state=1,gamma=100,C=1.0) |
虽然这种拟合在训练集上很有用,但是无法用在测试集上
这样的东西,能不能做聚类
决策树学习
如果我们非常在乎可解释性的话,决策树(Decision tree
)分类器是非常吸引人的模型,正如名字所预示的那样,我们可以考虑将数据通过做出一些决策而进行分解,以下面一个决定某一天是否要做某件事的决策树为例:
相似的,对于连续变化的数据,我们可以定义一个阈值来进行决策
在实际应用中,我们从树根开始,对于可以导致最大学习增加(largest IG
)进行分类(这在之后会详细介绍),然后对每一个分支重复这个过程,直到树变得整洁
然而这样做在实际情况下往往会导致过拟合,因此我们需要通过设定最大深度对树进行修剪
最大化信息增加
为了准确地分隔节点,我们需要定义一个最优化函数来处理决策树学习算法.在这里,我们决策树的目的是尽可能增加信息,信息量定义如下:
$$IG(D_p,f)=I(D_p)-\sum_{j=1}^m\frac{N_j}{N_p}I(D_j)$$
式中$f$是实行分割的特征,$D_p$和$D_j$是父节点对应数据集和第$j$个子节点数据集,而$I$就是我们的不纯度,我们可以看出,所谓信息的增加就是父节点和子节点不纯度和之差,子节点不纯度越小,信息量增加越大
不过在实际使用过程中,为了简明起见,编译器往往会使用二分法进行分类
现在我们来介绍经常被使用的三种不纯度的度量($t$为节点)
- 基尼不纯度$I_G=1-\sum p(i|t)^2$
- 信息熵$I_H=-\sum p(i|t)\log_2p(i|t)$
- 分类误差$I_E=1-\max{p(i|t)}$
1,2往往能够获得相似的结果,而3往往被用在修建上而非生长上
下面这张图可以展现出三种度量方式的特性(对于二分类样本)
1 | def gini(p): |
构建一颗决策树
我们可以借助scikit-learn
来构建一棵决策树,我们在此训练一个最大深度为3的决策树,使用信息熵作为度量.
需要注意到,虽然在可视化的时候进行数据预处理是好的,但是对于决策树而言,不需要进行数据缩放
1 | from sklearn.tree import DecisionTreeClassifier |
C:\Users\h\AppData\Local\Temp\ipykernel_43220\485392308.py:21: UserWarning: You passed a edgecolor/edgecolors ('black') for an unfilled marker ('x'). Matplotlib is ignoring the edgecolor in favor of the facecolor. This behavior may change in the future.
plt.scatter(x=X[y==cl,0],
C:\Users\h\AppData\Local\Temp\ipykernel_43220\485392308.py:21: UserWarning: You passed a edgecolor/edgecolors ('black') for an unfilled marker ('x'). Matplotlib is ignoring the edgecolor in favor of the facecolor. This behavior may change in the future.
plt.scatter(x=X[y==cl,0],
使用scikit-learn
有一个很好的功能在于你可以将训练好的树保存为.dot
文件,然后我们可以使用pydotplus
库进行查看
需要注意,我们需要安装
GraphViz
这样的程序
1 | #! 添加GraphViz环境变量 |
True
我们可以得到一个如下所示的结果
我们可以看到这中间的各种决策过程,这个决策树在区分花种类中可以做得很好
遗憾的是,scikit-learn
库中并没有内置进行修剪的函数,我们需要修改之前的源代码
使用随机森林法合并不同的决策树
决策森林(Random forests
)在机器学习中变得非常流行,随机森林可以被看作是一组决策树的集合,随机森林可以被总结为以下四步:
- 从数据集中随机抽取$n$个样本(放回抽样)
- 从这$n$个样本中生长出一棵决策树,而在每一个样本中,有:
- 随机选取$d$个特征
- 选取使用最能分割特征的节点进行分割
- 重复这个过程$k$次
- 使用绝对多数投票合并这些树
我们尤其需要注意在步骤2中是对部分特征进行生长
虽然随机森林的结果不像决策树一样易于解读,但是相应的,其可以过滤掉很大一部分噪声,鲁棒性很高,我们只需要关心我们需要训练多少个树,而往往树越多,结果越为理想.
当然,像$n$和$d$这样的的值也可以优化,但是在这里不加以过多赘述
我们可以使用库来构建随机森林
1 | from sklearn.ensemble import RandomForestClassifier |
虽然我们长的树很少,数据集也很小,但是我们修改了n_jobs
来实现多线程运算
第K近邻居–一种懒惰的学习算法
我们最后要介绍的算法是KNN算法,这种算法非常有趣因为其采用了一种不同的方式进行学习
所谓懒惰不是因为其结构简单,而是因为其不从训练数据中学习区分函数,而是通过记住训练集的方式
KNN属于一个典型的非参数模型
KNN算法自身是相当直截了当的,可以用以下几步来总结
- 选择数量$k$和距离矩阵
- 找到我们想要分类的$k$个邻居
- 使用绝对多数投票决定类标签
下图展示了一个新的数据点是如何拿KNN算法分类的
这种方法的好处在于一旦我们添加新的数据,那么模型可以立即适应,然而,当训练数据非常大时,这一算法会变得非常慢似乎不准确的抽样可能会对KNN带来权重
下面我们用KNN算法实践一下
1 | from sklearn.neighbors import KNeighborsClassifier |
在KNN中,选取合适的$k$尤为重要,同时也要选取一个合理的距离矩阵,例如我们刚才选用的minkowski
矩阵,其形式如下:
$$d(x^{(i)},x^{(j)})=\sqrt[p]{\sum_k |x^{(i)}_k-x^{(j)}_k|}$$
当$p=2$,为欧几里得距离,当$p=1$,为曼哈顿距离
可以参考这个网页
需要注意:当数据维度非常高时,KNN往往会给出过拟合的结果 :
总结
这一章所涉及的机器学习算法的特点列举如下:
- Logistic回归相当于
Adaline
的一种改进,最为基本 - 支持向量机可以支持许多类型的核来处理非线性问题,但是其具有非常多的参数需要调节
- 决策树的结果易于解读,但是鲁棒性不如随机森林
- KNN算法不依赖参数模型,但是当数据维度大时综合效果会显著下降
有待讨论的问题
- 关于logistic回归函数的意义
- 核的意义
- 修改logistic回归代码