机器学习的方法是基于数据产生的"模型"(model)的算法,也称"学习算法"(learning algorithm)。
包括:
指对数据的若干特征与若干标签(类型)之间的关联性进行建模的过程; 只要模型被确定,就可以应用到新的未知数据上。
这类学习过程可以进一步分为「分类」(classification)任务和「回归」(regression)任务。
在分类任务中,标签都是离散值;
而在回归任务中,标签都是连续值。
指对不带任何标签的数据特征进行建模,通常被看成是一种“让数据自己介绍自己” 的过程。
这类模型包括「聚类」(clustering)任务和「降维」(dimensionality reduction)任务。
聚类算法可以将数据分成不同的组别,而降维算法追求用更简洁的方式表现数据。
另外,还有一种半监督学习(semi-supervised learning)方法,介于有监督学习和无监督学习之间。通常可以在数据不完整时使用。
强化学习不同于监督学习,它将学习看作是试探评价过程,以"试错" 的方式进行学习,并与环境进行交互已获得奖惩指导行为,以其作为评价。
此时系统靠自身的状态和动作进行学习,从而改进行动方案以适应环境。
在上面的场景中,每一杯酒称作一个「样本」,十杯酒组成一个样本集。
酒精浓度、颜色深度等信息称作「特征」。这十杯酒分布在一个「多维特征空间」中。
进入当前程序的“学习系统”的所有样本称作「输入」,并组成「输入空间」。
在学习过程中,所产生的随机变量的取值,称作「输出」,并组成「输出空间」。
在有监督学习过程中,当输出变量均为连续变量时,预测问题称为回归问题;当输出变量为有限个离散变量时,预测问题称为分类问题。
当假设空间中含有不同复杂度的模型时,就要面临模型选择(model selection)的问题。
我们希望获得的是在新样本上能表现得很好的学习器。为了达到这个目的,我们应该从训练样本中尽可能学到适用于所有潜在样本的"普遍规律",我们认为假设空间存在这种"真"模型,那么所选择的模型应该逼近真模型。
拟合度可简单理解为模型对于数据集背后客观规律的掌握程度,模型对于给定数据集如果拟合度较差,则对规律的捕捉不完全,用作分类和预测时可能准确率不高。
换句话说,当模型把训练样本学得太好了的时候,很可能已经把训练样本自身的一些特点当作了所有潜在样本的普遍性质,这时候所选的模型的复杂度往往会比真模型更高,这样就会导致泛化性能下降。这种现象称为过拟合(overfitting)。可以说,模型选择旨在避免过拟合并提高模型的预测能力。
与过拟合相对的是欠拟合(underfitting),是指模型学习能力低下,导致对训练样本的一般性质尚未学好。
虚线:针对训练数据集计算出来的分数,即针对训练数据集拟合的准确性。
实线:针对交叉验证数据集计算出来的分数,即针对交叉验证数据集预测的准确性。
从图中我们可以看出,对于复杂数据,低阶多项式往往是欠拟合的状态,而高阶多项式则过分捕捉噪声数据的分布规律,而噪声之所以称为噪声,是因为其分布毫无规律可言,或者其分布毫无价值,因此就算高阶多项式在当前训练集上拟合度很高,但其捕捉到的无用规律无法推广到新的数据集上。因此该模型在测试数据集上执行过程将会有很大误差,即模型训练误差很小,但泛化误差很大。
它的本质是通过距离判断两个样本是否相似,如果距离够近就认为他们足够相似属于同一类别。
当然只对比一个样本是不够的,误差会很大,我们需要找到离其最近的 k 个样本,并将这些样本称之为「近邻」(nearest neighbor)。对这 k 个近邻,查看它们的都属于何种类别(这些类别我们称作「标签」 (labels))。
然后根据“少数服从多数,一点算一票”原则进行判断,数量最多的的标签类别就是新样本的标签类别。
其中涉及到的原理是“越相近越相似”,这也是KNN的基本假设。
我们常用欧拉公式,即“欧氏距离”。回忆一下,一个平面直角坐标系上,如何计算两点之间的距离?一个立体直角坐标系上,又如何计算两点之间的距离?
当特征数量有很多个形成多维空间时,再用上述的写法就不方便了,我们换一个写法,用 X 加下角标的方式表示特征维度。则在 n 维空间中,有两个点 A 和 B,它们的坐标分别为:
A ( x 1 A , x 2 A , x 3 A , . . . . . . x n A ) ; B ( x 1 B , x 2 B , x 3 B , . . . . . . x n B ) A(x_{1A},x_{2A},x_{3A},......x_{nA});B(x_{1B},x_{2B},x_{3B},......x_{nB}) A(x1A,x2A,x3A,......xnA);B(x1B,x2B,x3B,......xnB)
则 A 和 B 两点之间的欧氏距离的基本计算公式如下:
而在我们的机器学习中,坐标轴上的值x1,x2 , x3,…… xn正是我们样本数据上的 n 个特征。
算法参数是 k,k 可以理解为标记数据周围几个数作为参考对象,参数选择需要根据数据来决定。
变种一:默认情况下,在计算距离时,权重都是相同的,但实际上我们可以针对不同的邻居指定不同的距离权重,比如距离越近权重越高。
变种二:使用一定半径内的点取代距离最近的 k 个点
#全部行都能输出
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 解决坐标轴刻度负号乱码
plt.rcParams['axes.unicode_minus'] = False
# 解决中文乱码问题
plt.rcParams['font.sans-serif'] = ['Simhei']
plt.style.use('ggplot')
# plt.figure(figsize=(2,3),dpi=720)
rowdata = {'颜色深度':
[14.13,13.2,13.16,14.27,13.24,12.07,12.43,11.79,12.37,12.04],
'酒精浓度': [5.64,4.28,5.68,4.80,4.22,2.76,3.94,3.1,2.12,2.6],
'品种': [0,0,0,0,0,1,1,1,1,1]}
# 0 代表 “黑皮诺”,1 代表 “赤霞珠”
wine_data = pd.DataFrame(rowdata)
wine_data
X = np.array(wine_data.iloc[:,0:2]) #我们把特征(酒的属性)放在X
y = np.array(wine_data.iloc[:,-1]) #把标签(酒的类别)放在Y
#探索数据,假如我们给出新数据[12.03,4.1] ,你能猜出这杯红酒是什么类别么?
new_data = np.array([12.03,4.1])
plt.scatter(X[y==1,0], X[y==1,1], color='red', label='赤霞珠') #画出标签y为1的、关于“赤霞珠”的散点
plt.scatter(X[y==0,0], X[y==0,1], color='purple', label='黑皮诺') #画出标签y为0的、关于“黑皮诺”的散点
plt.scatter(new_data[0],new_data[1], color='yellow') # 新数据点
new_data
plt.xlabel('酒精浓度')
plt.ylabel('颜色深度')
plt.legend(loc='lower right')
plt.savefig('葡萄酒样本.png')
from math import sqrt
distance = [sqrt(np.sum((x-new_data)**2)) for x in X ]
distance
sort = np.argsort(distance)
sort
6、7、4为最近的3个“数据点”的索引值,那么这些索引值对应的原数据的标签是什么?
K3 = sort[:3] #索引值
topk = [y[i] for i in K3]
topk
[1, 1, 0]
pd.Series(topk).value_counts().idxmax()
1
def KNN(new_data,dataset,k):
'''
函数功能:KNN分类器
参数说明:
new_data:需要预测分类的新数据
dataset:已有已知分类标签的数据集
k:k-紧邻算法参数,选择距离最小的k个点
return:分类结果
'''
from math import sqrt
import numpy as np
import pandas as pd
X = np.array(dataset.iloc[:,:2])
Y = np.array(dataset.iloc[:,-1])
distance = [sqrt(np.sum((x-new_data)**2)) for x in X]
K_k = np.argsort(distance)[:k]
topk = [Y[i] for i in K_k]
catgory = pd.Series(topk).value_counts().idxmax()
return catgory
# 测试函数的运行结果
new_data=np.array([12.03,4.1])
k = 3
KNN(new_data,wine_data,k)
# 输出1
scikit-learn 自 2007 年发布以来,scikit-learn已经成为 Python 中重要的机器学习库了。
scikit-learn,简称 sklearn, 支持了包括分类、回归、降维和聚类四大机器学习算法,以及特征提取、数据预处理和模型评估三大模块。
在工程应用中,用 Python 手写代码来从头实现一个算法的可能性非常低,这样不仅耗时耗力,还不一 定能够写出构架清晰,稳定性强的模型。更多情况下,是分析采集到的数据,根据数据特征选择适合的算法, 在工具包中调用算法,调整算法的参数,获取需要的信息,从而实现算法效率和效果之间的平衡。而 sklearn, 正是这样一个可以帮助我们高效实现算法应用的工具包。
scikit-learn官网
一致性
所有对象共享一个简单一致的界面(接口)。
监控
检查所有参数,所有估算器的超参数可以通过公共实例变量访问,所有估算器的学习参数都可以通过有下划线后缀的公共实例变量访问。
防止类扩散
对象类型固定,数据集被表示为 Numpy 数组或 Scipy 稀疏矩阵,超参是普通的 Python 字符或数字。
合成
现有的构件尽可能重用,可以轻松创建一个流水线 Pipeline。
合理默认值
大多数参数提供合理默认值,可以轻松搭建一个基本的工作系统
from sklearn.neighbors import KNeighborsClassifier
# 0 代表 “黑皮诺”,1 代表 “赤霞珠”
clf = KNeighborsClassifier(n_neighbors = 3)
clf = clf.fit(wine_data.iloc[:,0:2], wine_data.iloc[:,-1])
a = np.random.normal(11,2,(10,1))
b = np.random.normal(4,2,(10,1))
new_data = np.concatenate((a,b),axis=1)
result = clf.predict(new_data) # 返回预测的标签
result
array([0, 1, 0, 0, 1, 1, 1, 1, 0, 1], dtype=int64)
# 对模型进行一个评估,接口score返回预测的准确率
y_new = np.array([0, 1, 0, 0, 1, 1, 1, 1, 0, 0]) # 实际结果,最后一个1改成0
clf.score(new_data,y_new)
返回0.9,代表90%准确率,即10个结果预测对了9个
clf.predict_proba([[12.8,4.1]])
#输出数据[12.8,4.1]为标签0的概率(0.666...),以及标签为1的概率(0.333...)
array([[0.66666667, 0.33333333]])
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import pandas as pd
import numpy as np
#读取数据集
data = load_breast_cancer()
#DateFrame格式显示
X = data.data
y = data.target
name = data.feature_names
names = np.append(names,'患病否')
data=np.concatenate((X,y.reshape(-1,1)),axis=1)
table=pd.DataFrame(data=data,columns=names)
table.head()
# 划分训练集和测试集 #30%数据作为训练集
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.2,random_state=420)
# 建立模型&评估模型
clf = KNeighborsClassifier(n_neighbors=4)
# 建立分类器
clf = clf.fit(Xtrain,Ytrain)
score = clf.score(Xtest,Ytest)
score
0.9210526315789473
如何用上面分类器拟合结果找出离 Xtest 中第 20 行和第 30 行最近的 4 个“点”?
#查找点的K邻居。返回每个点的邻居的与之的距离和索引值。
clf.kneighbors(Xtest[[20,30],:],return_distance=True)
(array([[35.70015941, 42.02374599, 81.82147557, 83.06271326],
[11.81126721, 14.5871725 , 17.4734004 , 18.94892695]]),
array([[112, 221, 303, 263],[268, 162, 42, 134]], dtype=int64))
KNN 中的 k 是一个超参数,所谓“超参数”,就是需要人为输入,算法不能通过直接计算得出的参数。
KNN 中的 k 代表的是距离需要分类的测试点 x 最近的 k 个样本点,如果不输入这个值,那么算法中重要部分 “选出 k 个最近邻” 就无法实现。
从 KNN 的原理中可见,是否能够确认合适的 k 值对算法有极大的影响。
如果选择的 k 值较小,就相当于较小的邻域中的训练实例进行预测,这时候只有与输入实例较近的(相似的)训练实例才会对预测结果起作用,但缺点是预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰好是噪声,预测就会出错。
相反地,如果选择的 k 值较大,就相当于较大的邻域中的训练实例进行预测。这时与输入实例较远的 (不相似的)训练实例也会对预测起作用,使预测发生错误。因此,超参数 k 的选定是 KNN 的头号问题。
那我们怎样选择一个最佳的 k 呢?在这里我们要使用机器学习中的神器:参数学习曲线。参数学习曲线是一条以不同的参数取值为横坐标,不同参数取值下的模型结果为纵坐标的曲线,我们往往选择模型表现最佳点的参数取值作为这个参数的取值。
# 更换不同的n_neighbors参数的取值,观察结果的变化
clf = KNeighborsClassifier(n_neighbors=7)
clf = clf.fit(Xtrain,Ytrain)
score = clf.score(Xtest,Ytest)
score
0.9385964912280702
绘制学习曲线:
score = [ ]
krange = range(1,20)
for i in krange:
clf = KNeighborsClassifier(n_neighbors=i)
clf = clf.fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
plt.plot(krange,score)
plt.show()
究竟上图中k为多少的时候分数越高?
score.index(max(score))+1
8
但是这个时候会有个问题,如果随机划分的数据集变化的的话,得分最高的k值也会发生变化:
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.2,random_state=421)
score = []
krange = range(1,20)
for i in krange:
clf = KNeighborsClassifier(n_neighbors=i)
clf = clf.fit(Xtrain,Ytrain)
score.append(clf.score(Xtest,Ytest))
plt.plot(krange,score)
plt.show()
score.index(max(score))+1
3
这样就无法确定最佳的k值了,就无法进行下面的建模工作,怎么办?
这就要用到交叉验证了
确定了 k 之后,我们还能够发现一件事:每次运行的时候学习曲线都在变化,模型的效果时好时坏, 这是为什么呢?
实际上,这是由于「训练集」和「测试集」的划分不同造成的。模型每次都使用不同的训练集进行训练, 不同的测试集进行测试,自然也就会有不同的模型结果。
在业务当中,我们的训练数据往往是已有的历史数据,但我们的测试数据却是新进入系统的一系列还没有标签的未知数据。我们的确追求模型的效果,但我们追求的是模型在未知数据集上的效果,在陌生数据集上表现优秀的能力被称为泛化能力,即我们追求的是模型的泛化能力。
我们在进行学习算法前, 通常会将一个样本集分成训练集(training set)和测试集(testing set),其中训练集用于模型的学习或训练,而后测试集通常用于评估训练好的模型对于数据的预测性能评估。
训练误差(training error)代表模型在训练集上的错分样本比率。
测试误差(empirical error)是模型在测试集上的错分样本比率。
训练误差的大小,用来判断给定问题是不是一个容易学习的问题。 测试误差则反映了模型对未知数据的预测能力,测试误差小的学习方法具有很好的预测能力,如果得到的训练集和测试集的数据没有交集,通常将此预测能力称为泛化能力(generalization ability)。 我们认为,如果模型在一套训练集和数据集上表现优秀,那说明不了问题,只有在众多不同的训练集和 测试集上都表现优秀,模型才是一个稳定的模型,模型才具有真正意义上的泛化能力。为此,机器学习领域有发挥神作用的技能:「交叉验证」,来帮助我们认识模型。
最常用的交叉验证是 k 折交叉验证。我们知道训练集和测试集的划分会干扰模型的结果,因此用交叉验证 n 次的结果求出的均值,是对模型效果的一个更好的度量。
对于带交叉验证的学习曲线,我们需要观察的就不仅仅是最高的准确率了,而是准确率高且方差还相对较小的点,这样的点泛化能力才是最强的。在交叉验证+学习曲线的作用下,我们选出的超参数能够保证更好的泛化能力。
from sklearn.model_selection import cross_val_score as CVS
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.2,random_state=420)
clf = KNeighborsClassifier(n_neighbors=8)
cvresult = CVS(clf,Xtrain,Ytrain,cv=6) #训练集对折6次,一共6个预测率输出
cvresult #每次交叉验证运行时估算器得分的数组
array([0.92207792, 0.90789474, 0.97368421, 0.94736842, 0.93333333, 0.92 ])
# 均值:查看模型的平均效果
cvresult.mean()
# 方差:查看模型是否稳定
cvresult.var()
0.934059770638718
0.0004622658270548926
score = []
var = []
krange=range(1,20) #设置不同的k值,从1到19都看看
for i in krange:
clf = KNeighborsClassifier(n_neighbors=i)
cvresult = CVS(clf,Xtrain,Ytrain,cv=5)
score.append(cvresult.mean()) # 每次交叉验证返回的得分数组,再求数组均值
var.append(cvresult.var())
plt.plot(krange,score,color='k')
plt.plot(krange,np.array(score)+np.array(var)*2,c='red',linestyle='--')
plt.plot(krange,np.array(score)-np.array(var)*2,c='red',linestyle='--')
最标准,最严谨的交叉验证应该有三组数据:训练集、验证集和测试集。 当我们获取一组数据后:
通常来说,我们认为经过验证集找出最终参数后的模型的泛化能力是增强了的,因此模型在未知数据(测试集)上的效果会更好,但尴尬的是,模型经过交叉验证在验证集上的调参之后,在测试集上的结果没有变好的情况时有发生。
原因其实是:
如果我们相信交叉验证的调整结果是增强了模型的泛化能力的,那即便测试集上的测试结果并没有变好(甚至变坏了),我们也认为模型是成功的。 如果我们不相信交叉验证的调整结果能够增强模型的泛化能力,而一定要依赖测试集来进行判断,我们完全没有进行交叉验证的必要,直接用测试集上的结果来跑学习曲线就好了。 所以,究竟是否需要验证集,其实是存在争议的,在严谨的情况下,大家还是使用有验证集的方式。
交叉验证的方法不止“k 折” 一种,分割训练集和测试集的方法也不止一种,分门别类的交叉验证占据了sklearn 中非常长的一章。
所有的交叉验证都是在分割训练集和测试集,只不过侧重的方向不同。
各类交叉验证的原理繁琐,大家在机器学习道路上一定会逐渐遇到更难的交叉验证,但是万变不离其宗:本质上交叉验证是为了解决训练集和测试集的划分对模型带来的影响,同时检测模型的泛化能力的。
交叉验证的折数不可太大,因为折数越大抽出来的数据集越小,训练数据所带的信息量会越小,模型会越来越不稳定。
如果你发现不使用交叉验证的时候模型表现很好,一使用交叉验证模型的效果就骤降
折数过大:
什么是归一化?我们把 X 放到数据框中来看一眼,你是否观察到,每个特征的均值差异很大?有的特征数值很大,有的特征数值很小,这种现象在机器学习中被称为"量纲不统一"。KNN 是距离类模型,欧氏距离的计算公式中存在着特征上的平方和:
如果某个特征的取值非常大,其他特征的取值和它比起来就不算什么,那么距离的大小很大程度都会由这个来决定,其他的特征之间的距离可能就无法对d(A,B)的大小产生什么影响,这种现象会让KNN这样的距离类模型的效果大打折扣。
然而在实际分析情景当中,绝大多数数据集都会存在各特征值量纲不同的情况,此时若要使用 KNN 分类器,则需要先对数据集进行归一化处理,即是将所有的数据压缩都同一个范围内。
当数据(x)按照最小值中心化后,再按极差(最大值-最小值)缩放,数据移动了最小值个单位,并且会被收敛到[0,1]之间,而这个过程,就称作数据归一化(Normalization,又称 Min-Max Scaling)。
直接在全数据集 X 上进行了归一化,然后放入交叉验证绘制学习曲线,这种做法是错误的。
真正正确的方式是,先分训练集和测试集,再归一化!
为什么?想想看归一化的处理手段,我们是使用数据中的最小值和极差在对数据进行压缩处理,如果我们在全数据集上进行归一化,那最小值和极差的选取是会参考测试集中的数据的状况的。因此,当我们归一化后,无论我们如何分割数据,都会由一部分测试集的信息被“泄露”给训练集,这会使得我们的模型效果被高估。
在现实业务中,我们只知道训练集的数据,不了解测试集究竟会长什么样,所以我们要利用训练集上的最小值和极差来归一化测试集。
data = [[-1,2],[-0.5,6],[0,10],[1,18]]
data=pd.DataFrame(data)
(data-np.min(data,axis=0))/(np.max(data,axis=0)-np.min(data,axis=0))
from sklearn.preprocessing import MinMaxScaler as mms
Xtrain,Xtest,Ytrain,Ytest=train_test_split(X,y,test_size=0.2,random_state=420)
#归一化
MMS_01=mms().fit(Xtrain) #求训练集最大/小值
MMS_02=mms().fit(Xtest) #求测试集最大/小值
#转换
X_train=MMS_01.transform(Xtrain)
X_test =MMS_02.transform(Xtest)
score=[]
var=[]
for i in range(1,20):
clf=KNeighborsClassifier(n_neighbors=i)
cvresult=CVS(clf,X_train,Ytrain,cv=5) # 交叉验证的每次得分
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color="k")
plt.plot(krange,np.array(score)+np.array(var)*2,c="red",linestyle="--")
plt.plot(krange,np.array(score)-np.array(var)*2,c="red",linestyle="--")
plt.show()
score.index(max(score))+1
8
最终的到 k 最优值为 8,无论 random_state 取什么值,最优 k 值不会相差太多。
把经过交叉验证、归一化处理之后,我们得到最优 k 为 8,放在归一化后的训练集重新建模,然后在归一化后的测试集中查看结果分数:
clf=KNeighborsClassifier(n_neighbors=8,weights='distance').fit(X_train,Ytrain)
score=clf.score(X_test,Ytest)
score
0.956140350877193
最近邻点距离远近修正在对未知分类过程中, “一点一票” 的规则是 KNN 模型优化的一个重要步骤。 也就是说,对于原始分类模型而言,在选取最近的 k 个元素之后,将参考这些点的所属类别,并对其进行简单计数,而在计数的过程中这些点 “一点一票” ,这些点每个点对分类目标点的分类过程中影响效力相同。
但这实际上是不公平的,就算是最近邻的 k 个点,每个点的分类目标点的距离仍然有远近之别,而近的点往往和目标分类点有更大的可能性属于同一类别( 该假设也是 KNN 分类模型的基本假设) 。
关于惩罚因子的选取有很多种方法, 最常用的就是根据每个最近邻 = 距离的不同对其作加权, 加权方法为设置wi权重,该权重计算公式为
w i = 1 d ( x ′ , x i ) w_{i} = \frac{1}{d(x',x_{i})} wi=d(x′,xi)1
这里需要注意的是,关于模型的优化方法只是在理论上而言进行优化会提升模型判别效力,但实际应用过程中最终能否发挥作用,本质上还是取决于优化方法和实际数据情况的契合程度,如果数据本身存在大 量异常值点,则采用距离远近作为惩罚因子则会有较好的效果,反之则不然。
因此在实际我们进行模型优化的过程当中,是否起到优化效果还是要以最终模型运行结果为准。在sklearn中,我们可以通过参数 weights 来控制是否适用距离作为惩罚因子。
for i in range(1,20):
clf=KNeighborsClassifier(n_neighbors=i,weights='distance')
cvresult=CVS(clf,X_train,Ytrain,cv=5) # 交叉验证的每次得分
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color="k")
plt.plot(krange,np.array(score)+np.array(var)*2,c="red",linestyle="--")
plt.plot(krange,np.array(score)-np.array(var)*2,c="red",linestyle="--")
plt.show()
树模型是有监督学习类算法中应用广泛的一类模型,同时可应用于分类问题和回归问题,其中用于解决分类问题的树模型常被称为分类树,而用于解决回归类问题的树模型被称作回归树。
树模型通过递归式切割的方法来寻找最佳分类标准,进而最终形成规则。
其算法原理虽然简单,但模型本身适用面极广,且在分类问题和回归问题上均有良好的表现,外加使用简单,无需人为进行过多变量调整和数据预处理,同时生成规则清晰,模型本身可解释性非常强,因此在各个行业均有广泛应用。
决策树(Decision Tree)是一种实现分治策略的层次数据结构。它是一种有效的非参数学习方法,并可以用于分类和回归。我们主要讨论分类的决策树。
分类决策树模型表示一种基于特征对实例进行分类的树形结构(包括二叉树和多叉树)。
决策树由节点(node)和有向边(directed edge)组成,树中包含三种节点:
从根节点到每个叶子节点的路径对应了一个判定测试序列,其基本流程遵循简单且直观的 “分而治之” 策略。由此,局部区域通过少数几步递归分裂确定,每个决策节点实现一个具有离散输出的属性测试函数,标记分支。
假设给定训练数据集输入:
在每个节点应用一个测试,并根据测试的输出确定一个分支。这一过程从根节点开始,并递归地重复,直至到达一个叶子节点,这时,该leaf的值形成输出。
决策树可以表示为给定决策节点下类的条件概率分布,这一条件概率分布定义在特征空间的一个划分上。每个将空间划分成较小区域,在从根节点沿一条路径向下时,这些较小的区域被进一步划分,并在每个区域定义一个类的概率分布就构成了一个条件概率分布。
假设 X 是表示特征的随机变量,Y 是表示类的随机变量,则条件概率分布可表示为 P(Y|X)。 X 取值于给定划分条件下的区域的集合,Y 取值于类的集合。各叶节点(区域)上的条件概率往往会偏向某一个类,即属于某一类的概率较大。决策树在分类时会将该节点的实例强行分到条件概率大的那一类去。
左图表示了特征空间的一个划分,假定现在只有 w10和w20两个决策节点,特征空间被决策节点沿轴划分,并且相继划分相互正交。 每个小矩形表示一个区域,特征空间划分上的区域构成了一个集合,X 取值为区域的集合。我们在这里 假设只有两类,即 Y 的取值为 ”□“ 和”○”。 当某个区域 c 的条件概率分布满足 P(Y=○|X=c) > 0.5 时,则 认为这个区域属于○类,即落在这个区域的实例都将被视为该类。右图为对应于条件概率分布的决策树。 如果输入维是xN 是离散的,取 n 个可能的值之一,则该决策节点检查xN的值,并取相应分支,实现一 个 n 路划分。因此,如果决策节点具有离散分支,数值输入应当离散化。
如果xn是连续型数值,则测试比较:
其中 wm是适当选择的阈值。该决策节点将输入空间一分为二:
Lm = {Y| xN ≥ wm}和Rm = {Y| xN < wm}称作一个二元划分(binary split)。从根节点到叶节点的路径上的相继决策节点使用其他属性进一步把它们一分为二,产生相互正交的划分。
从图中可以看出,第一次划分后,{Y| x1≤ w10}已经是纯的,所以不需要再划分。
决策树学习本质上是从训练数据集中归纳出一组分类规则,也称为 “树归纳”。
对于给定的训练数据集,存在许多对它无错编码的树。而为了简单起见,我们感兴趣的是从中选出 “最小” 的树,这里的树的大小用树的节点数和决策节点的复杂性度量。
从另一个角度看,决策树学习是由训练数据集估计条件概率模型。基于特征空间划分的类的条件概率模型有无数个,我们选择的模型应该是不仅能对训练数据有很好的拟合,而且对未知数据也有很好的预测。
树的学习算法从包含全部训练数据的根开始,每一步都选择最佳划分。依赖于所选择的属性是数值属性还是离散属性,每次将数据划分为两个或 n 个子集,然后使用对应的子集递归地进行划分,直到所有训练数据子集被基本正确分类,或者没有合适的特征为止,此时,创建一个树叶节点并标记它,这就生成了一颗决策树。
综上,决策树学习算法包含特征选择、决策树的生成与决策树的剪枝。
由于决策树表示一个条件概率分布,所以深浅不同的决策树对应着不同复杂度的概率模型。其中决策树的生成只考虑局部最优,相对地,决策树的剪枝则考虑全局最优。
在信息论与概率统计中,熵是表示随机变量不确定性的度量。这里我们使用的熵,也叫作香农熵,这个名字来源于信息论之父克劳德·香农。
熵:一种度量不纯性的函数是熵函数(entropy)
类分布为(0,1)的节点具有零不纯性,而均衡分布的(0.5,0.5)的节点具有最高的不纯性
分类误差(Classification error)
这里我们给出三种不纯性度量方法的计算实例:
从上面的例子及图中可以看出,不同的不纯性度量是一致的。根据计算,节点N1具有最低的不纯性度量值,然后依次是N2 ,N3 。虽然结果是一致的,但是作为测试条件的属性选择仍然因不纯性度量的选择而异。
row_data = {'是否陪伴' :[0,0,0,1,1],
'是否玩游戏':[1,1,0,1,1],
'渣男' :['是','是','不是','不是','不是']}
dataSet = pd.DataFrame(row_data)
dataSet
def calEnt(dataSet):
n = dataSet.shape[0] # 数据集总行数
iset = dataSet.iloc[:,-1].value_counts() # 标签的所有类别
p = iset/n # 每一类标签所占比
ent = (-p*np.log2(p)).sum() # 计算信息熵
return ent
calEnt(dataSet)
熵越高,信息的不纯度就越高,则混合的数据就越多。
也就是说,单从判断的结果来看,如果你从这 5 人中瞎猜,要准确判断其中一个人是不是“bad boy”,是不容易的。
决策树最终的优化目标使得叶节点的总不纯度最低,即对应衡量不纯度的指标最低。
同时我们知道,全局最优树没有办法简单高效的获得,因此此处我们仍然要以局部最优化方法来指导建模过程,并通过优化条件的设置,最终在每一步都是局部最优的条件下逐步至尽可能全局最优的结果。
而在信息熵指数的指导下,决策树生成过程的局部最优条件也非常好理解:即在选取属性测试条件(attribute test condition)对某节点(数据集)进行切分的时候,尽可能选取使得该节点对应的子节点信息熵最小的特征进行切分。换而言之,就是要求父节点信息熵和子节点总信息熵之差要最大。
为了确定测试条件的效果,我们需要比较父节点(划分前)的不纯程度和分支节点(划分后)的不纯程度,它们的差越大,则意味着使用属性 a 来进行划分所获得的 “不纯度提升” 越大,测试条件效果越好。
其中D是父节点上的记录总数,V 表示属性的个数,Dv是与第 v 个分支节点相关联的记录个数。
对此,我们可以用增益来确定划分效果的标准。
决策树归纳算法通常选择最大化增益的测试条件,因为对所有的测试条件来说,Ent(D) 是一个不变的值,所以最大化增益等价于最小化分支节点的不纯性度量的加权平均值。最后,当选择熵作为公式的不纯性度量时,熵的差就是所谓的 “信息增益” ,即资讯获利(information gain)。 我们手动计算一下,bad boy 数据集中第 0 列的信息增益:
分类算法除了需要测量信息熵,还需要划分数据集。
在知道如何得到熵之后,我们就可以按照获取最大信息增益的方法来判断是否正确地划分了数据集。 我们将对每个特征划分数据集的结果计算一次信息熵,以便判断按照哪个特征划分数据集是最好的划分方式。
划分数据集的最大准则是选择最大信息增益,也就是信息下降最快的方向。
通过手动计算,我们知道:
用 Python 可以通过以下代码来输出每一列的信息增益(了解即可):
# 定义信息熵
def calEnt(dataSet):
n = dataSet.shape[0] # 数据集总行数
iset = dataSet.iloc[:,-1].value_counts() # 统计标签的所有类别
p = iset/n # 统计每一类标签所占比
ent = (-p*np.log2(p)).sum() # 计算信息熵
return ent
# 选择最优的列进行切分
def bestSplit(dataSet):
baseEnt = calEnt(dataSet) # 计算原始熵
bestGain = 0 # 初始化信息增益
axis = -1 # 初始化最佳切分列,标签列
for i in range(dataSet.shape[1]-1): # 对特征的每一列进行循环
levels= dataSet.iloc[:,i].value_counts().index # 提取出当前列的所有取值
ents = 0 # 初始化子节点的信息熵
for j in levels: # 对当前列的每一个取值进行循环
childSet = dataSet[dataSet.iloc[:,i]==j] # 某一个子节点的dataframe
ent = calEnt(childSet) # 计算某一个子节点的信息熵
ents += (childSet.shape[0]/dataSet.shape[0])*ent # 计算当前列的信息熵
print('第{}列的信息熵为{}'.format(i,ents))
infoGain = baseEnt-ents # 计算当前列的信息增益
print('第{}列的信息增益为{}\n'.format(i,infoGain))
if (infoGain > bestGain):
bestGain = infoGain # 选择最大信息增益
axis = i # 最大信息增益所在列的索引
print("第{}列为最优切分列".format(axis))
return axis
接下来,我们来验证我们构造的数据集最佳切分函数返回的结果与手动计算的结果是否一致。
bestSplit(dataSet)
第 0 列的信息熵为 0.5509775004326937
第 0 列的信息增益为 0.4199730940219749
第 1 列的信息熵为 0.8
第 1 列的信息增益为 0.17095059445466854
第 0 列为最优切分列
也就是说,最大信息熵的所选的特征是分类后熵值最小的特征。
分类后熵值最小的特征恰恰是分类结果一致的特征,而分类结果一致的特征必须是两类样本差异最大的特征。
通过最佳切分函数返回最佳切分列的索引,我们就可以根据这个索引,构建一个按照给定列切分数据集的函数。
"""
函数功能:按照给定的列划分数据集
参数说明:
dataSet:原始数据集
axis:指定的列索引
value:指定的属性值
return:redataSet:按照指定列索引和属性值切分后的数据集
"""
def mySplit(dataSet,axis,value):
col = dataSet.columns[axis]
redataSet = dataSet.loc[dataSet[col]==value,:].drop(col,axis=1)
return redataSet
#验证函数:以axis=0,value=1为例
mySplit(dataSet,0,1)
目前我们已经学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:
递归约束的条件是:
在第 2 种情形下,我们把当前节点标记为叶节点,并将其类别设定为该节点所含样本最多的类别,任何到达叶节点的数据必然属于叶节点的分类;
在第 3 种情形下,同样把当前节点标记为叶节点,但将其类别设定为其父节点所含样本最多的类别。
ID3 算法原型见于 J.R Quinlan 的博士论文,是基础理论较为完善,使用较为广泛的决策树模型,在此基础上 J.R Quinlan 进行优化后,陆续推出了 C4.5 和 C5.0 决策树算法,我们先从 ID3 开始,再讨论如何从 ID3 逐渐优化至 C4.5。
ID3 算法的核心是在决策树各个节点应用信息增益准则选择特征,递归地构建决策树。具体方法是:
python代码实现(了解即可)
"""
函数功能:基于最大信息增益切分数据集,递归构建决策树
参数说明:
dataSet:原始数据集(最有一列是标签)
return:myTree:字典形式的树
"""
def createTree(dataSet):
featlist = list(dataSet.columns) # 提取出数据集所有的列
classlist = dataSet.iloc[:,-1].value_counts() # 获取最后一列类标签
# 判断最多标签数目是否等于数据集行数,或者数据集是否只有一列
if classlist[0]==dataSet.shape[0] or dataSet.shape[1] == 1:
return classlist.index[0] # 如果是,返回类标签
axis = bestSplit(dataSet) # 确定出当前最佳切分列的索引
bestfeat = featlist[axis] # 获取该索引对应的特征
myTree = {bestfeat:{}} # 采用字典嵌套的方式存储树信息
del featlist[axis] # 删除当前特征
valuelist = set(dataSet.iloc[:,axis]) # 提取最佳切分列所有属性值
for value in valuelist: # 对每一个属性值递归建树
myTree[bestfeat][value] = createTree(mySplit(dataSet,axis,value))
return myTree
#查看运行结果
myTree = createTree(dataSet)
myTree
{‘是否陪伴’: {0: {‘是否玩游戏’: {0: ‘不是’, 1: ‘是’}}, 1: ‘不是’}}
ID3算法局限性
ID3 算法局限主要源于局部最优化条件,即信息增益的计算方法,其局限性主要有以下几点:
对于 ID3 的诸多优化措施,最终也构成了 C4.5 算法的核心内容。
C4.5 算法与 ID3 算法相似,C4.5 算法对 ID3 算法进行了改进,C4.5 在生成的过程中,用信息增益比准则来选择特征。
信息增益比定义为其信息增益与训练数据集关于某一特征的值的熵之比:
称为属性 a 的 "固有值“(intrinsic value)。
属性 a 的可能取值越多(即 V 越大),则 IV(a) 的值通常会越大。
IV 值会随着叶节点上样本量的变小而逐渐变大,也就是说一个特征属性中如果标签分类太多,每个叶子上的 IV 值就会非常大。
值得注意的是,增益率准则对可取值数目较少的属性有所偏好,因此,C4.5 算法并不是直接选择增益率最大的候选划分属性,而是使用了一种启发式:先从候选划分属性中找出信息增益高于平均水平的属性, 再从中选择增益率最高的。
我们可利用 Gain_ratio 代替 Gain 重新计算数据集中第 0 列的 Gain_ratio ,由于根据 accompany 字段切分后,2 个分支分别有 3 个和 2 个的样例数据,因此其 IV 指标计算过程如下:
然后可进一步计算其他各字段的 Gain_ratio ,并选取 Gain_ratio 最大的字段进行切分。
在 C4.5 中,同样还增加了针对连续变量的处理手段。如果输入特征字段是连续型变量,则算法首先会对这一列数进行从小到大的排序,然后选取相邻的两个数的中间数作为切分数据集的备选点,若一个连续 变量有 N 个值,则在 C4.5 的处理过程中将产生 N-1 个备选切分点,并且每个切分点都代表着一种二叉树的切分方案,例如:
这里需要注意的是,此时针对连续变量的处理并非是将其转化为一个拥有 N-1 个分类水平的分类变量,而是将其转化成了 N-1 个二分方案,而在进行下一次的切分过程中,这 N-1 个方案都要单独带入考虑,其中每一个切分方案和一个离散变量的地位均相同(一个离散变量就是一个单独的多路切分方案)。 例如有如下数据集,数据集中只有两个字段,第一行代表年龄,是特征变量,第二行代表性别,是目标字段,则对年龄这一连续变量的切分方案如图所示:
从上述论述能够看出,在对于包含连续变量的数据集进行树模型构建的过程中要消耗更多的运算资源。但与此同时,我们也会发现,当连续变量的某中间点参与到决策树的二分过程中,往往代表该点对于最终分类结果有较大影响,这也为我们连续变量的分箱压缩提供了指导性意见。 例如上述案例,若要对Age 列进行压缩,则可考虑使用 36.5 对其进行分箱,则分箱结果对于性别这一目标字段仍然具有较好的分类效果,这也是决策树的最常见用途之一,也是最重要的模型指导分箱的方法。
在实际操作过程中,我们判断模型是否过拟合往往是从模型训练误差和泛化误差的比较中得出,而采用我们之前介绍的交叉验证可得到较为准确的训练误差和泛化误差,二者结合使用就能判断模型是否存在过拟合现象。虽然我们之前举例时并没有对数据集进行切分,但任何有监督学习算法建模过程中都需要进行训练集和测试集的划分,决策树也不例外,进而我们可用交叉验证计算训练误差和泛化误差,进而判断决策树是否存在过拟合。这是一套通用地判断有监督学习算法是否过拟合的方法,同时通用的方法中还有更高级的方法,我们将在后续进行逐步介绍。但对于决策树而言,有一套决策树独有的防止过拟合的解决方案— —剪枝。
决策树剪枝
所谓剪枝是指在决策树中去除部分叶节点。
剪枝(Pruning)主要是用来防止过拟合,对于一般的数据集如果总是追求 ”纯的“ 叶节点,或者观测数较小的叶节点,很容易使得树过于庞杂,尤其是存在可以反复使用的连续变量的时候,此时就需要主动去掉一些分支来降低过拟合的风险。
常见的剪枝策略有 ”预剪枝“(Pre-Pruning)和 ”后剪枝“(Post-Pruning)。
CART:分类回归树(Classification and Regression Tree)
对于属性不同的被预测变量 y 分裂准则不同:
而除了训练集和测试集之外,我们还常常会划分一个验证集,验证集数据不参与建模也不参与模型修改和优化,只用于测试最终优化后模型效力。
而训练集、测试集和验证集的划分通常遵照 6:2:2 的比例进行划分,当然也可根据实际需求适当调整划分比例,但无论如何,测试集和验证集数据数量都不宜过多也不宜过少,该二者数据集数据均不参与建模, 若占比太多,则会对模型构建过程造成较大影响(欠拟合),而若划分数据过少,训练集数据量较大, 则又有可能造成过拟合,数据集的划分也是影响拟合度的重要因素。
criterion 这个参数是用来决定不纯度的计算方法。sklearn 提供了两种选择:
比起基尼系数,信息熵对不纯度更加敏感,对不纯度的惩罚最强。但是在实际使用中,信息熵和基尼系数的效果基本相同。
信息熵的计算比基尼系数缓慢一些,因为基尼系数的计算不涉及对数。
另外,因为信息熵对不纯度更加敏感,所以信息熵作为指标时,决策树的生长会更加 ”精细”,因此对于高纬数据或者噪声很多的数据,信息熵很容易过拟合,基尼系数在这种情况下效果往往比较好。
当模型拟合程度不足时,即当模型在训练集和测试集上都表现不太好的时候,使用信息熵。当然,这些不是绝对的。
# 导入需要的算法库和模块
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn import tree
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
plt.rcParams['font.sans-serif']=['Simhei']
plt.rcParams['axes.unicode_minus']=False
wine = load_wine()
wine.data.shape
wine.target
如果 wine 是一张表,应该长这样:
wine_pd = pd.concat([pd.DataFrame(wine.data),pd.DataFrame(wine.target)],axis=1)
wine.feature_names.append("result")
wine_pd.columns = wine.feature_names
wine_pd
Xtrain, Xtest, Ytrain, ytest =
train_test_split(wine.data,wine.target,test_size=0.3,random_state=420)
Xtrain.shape
Xtest.shape
(124, 13)
(54, 13)
建立模型
clf = tree.DecisionTreeClassifier(criterion="gini")
clf = clf.fit(Xtrain, Ytrain)
clf.score(Xtest, ytest) #返回预测的准确度
0.9444444444444444
同时,我们可以利用 Graphviz 模块导出决策树模型,第一次使用 Graphviz 之前需要对其进行安装, 若是使用从 pip 进行的 Python 包管理,则可直接在命令⾏界⾯中利用下述指令进行安装:
pip install graphviz
import matplotlib.pyplot as plt
feature_name = ['酒精','苹果酸','灰','灰的碱性','镁','总酚','类黄酮','非黄烷类酚类','花青素','颜色强度','色调','od280/od315 稀释葡萄酒','脯氨酸']
import graphviz
dot_data = tree.export_graphviz(clf,
out_file = None,
feature_names= feature_name,
class_names=["琴酒","雪莉","贝尔摩德"],
filled=True,
rounded=True)
graph = graphviz.Source(dot_data,filename="决策树.pdf")
graph
export_graphviz生成一个DOT格式的决策树:
clf.feature_importances_
[*zip(feature_name,clf.feature_importances_)]
clf.apply(Xtrain)
3. clf.tree_.node_count——树的节点个数
clf.tree_.node_count
clf.tree_.feature
array([ 9, 12, -2, -2, 6, 10, -2, -2, 12, 0, -2, -2, -2], dtype=int64)
在不加限制的情况下,一棵决策树会生长到衡量不纯度的指标最优,或者没有更多的特征可用为止。这样的决策树往往会过拟合,这就是说,它会在训练集上表现很好,在测试集上却表现糟糕。我们收集的样本数据不可能和整体的状况完全一致,因此当一棵决策树对训练数据有了过于优秀的解释性,它找出的规则必然包含了训练样本中的噪声,并使它对未知数据的拟合程度不足。
#我们的树对训练集的拟合程度如何?
score_train = clf.score(Xtrain, Ytrain)
score_train
为了让决策树有更好的泛化性,我们要对决策树进行剪枝。剪枝策略对决策树的影响巨大,正确的剪枝策略是优化决策树算法的核心。
random_state
如果我们改动 random_state,画出来的每一棵树都不一样。它为什么会不稳定呢?如果使用其他数据集,它还会不稳定吗?
我们之前提到过,无论决策树模型如何进化,在分支上的本质都还是追求某个不纯度相关的指标的优
化,而正如我们提到的,不纯度是基于节点来计算的,也就是说,决策树在建树时,是靠优化节点来追求一棵优化的树,但最优的节点能够保证最优的树吗?
集成算法被用来解决这个问题:sklearn 表示,既然一棵树不能保证最优,那就建更多的不同的树,然后从中取最好的。怎样从一组数据集中建不同的树?在每次分支时,不使用全部特征,而是随机选取一部分特征,从中选取不纯度相关指标最优的作为分支用的节点。这样,每次生成的树也就不同了。
random_state 用来设置分支中的随机模式的参数,默认 None,在高维度时随机性会表现更明显,低维度的数据(比如鸢尾花数据集),随机性几乎不会显现。输入任意整数,会一直长出同一棵树,让模型稳定下来。
splitter
splitter 也是用来控制决策树中的随机选项的,有两种输入值:
当你预测到你的模型会过拟合,用这两个参数来帮助你降低树建成之后过拟合的可能性。当然,树一旦建成,我们依然是使用剪枝参数来防止过拟合。
clf = tree.DecisionTreeClassifier(criterion="entropy"
,random_state=30
,splitter="random"
)
clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, ytest)
score
plt.rcParams['font.sans-serif']=['Simhei']
plt.rcParams['axes.unicode_minus']=False
1) max_depth
max_depth 限制树的最大深度,超过设定深度的树枝全部剪掉,一般用作树的”精修“
这是用得最广泛的剪枝参数,在高维度低样本量时非常有效。决策树多生长一层,对样本量的需求会增加一倍,所以限制树深度能够有效地限制过拟合。在集成算法中也非常实用。
实际使用时,建议从=3 开始尝试,看看拟合的效果再决定是否增加设定深度。
2) min_samples_leaf
一个节点在分支后的每个子节点都必须包含至少 min_samples_leaf 个训练样本,否则分支就不会发生, 或者,分支会朝着满足每个子节点都包min_samples_leaf 个样本的方向去发生。
一般搭配 max_depth 使用,在回归树中有神奇的效果,可以让模型变得更加平滑。这个参数的数量设置得太小会引起过拟合,设置得太大就会阻止模型学习数据。
一般来说,建议从=5 开始使用。
如果叶节点中含有的样本量变化很大,建议输入浮点数作为样本量的百分比来使用。同时,这个参数可以保证每个叶子的最小尺寸,避免低方差,过拟合的叶子节点出现。
3) min_samples_split
一个节点必须要包含至少 min_samples_split 个训练样本,这个节点才允许被分支,否则分支就不会发生。
4) max_features
max_features 限制分支时考虑的特征个数,超过限制个数的特征都会被舍弃。 和 max_depth 异曲同工, max_features 是用来限制高维度数据的过拟合的剪枝参数,但其方法比较暴力,是直接限制可以使用的特征数量而强行使决策树停下的参数,在不知道决策树中的各个特征的重要性的情况下,强行设定这个参数可能会导致模型学习不足。
如果希望通过降维的方式防止过拟合,建议使用 PCA,ICA 或者特征选择模块中的降维算法。
5) min_impurity_decrease
min_impurity_decrease 限制信息增益的大小,信息增益小于设定数值的分支不会发生。这是在 0.19 版本中更新的功能,在 0.19 版本之前时使用 min_impurity_split。
6) 确认最优的剪枝参数
那具体怎么来确定每个参数填写什么值呢?这时候,我们就要使用确定超参数的曲线来进行判断了,继续使用我们已经训练好的决策树模型 clf。
超参数的学习曲线,是一条以超参数的取值为横坐标,模型的度量指标为纵坐标的曲线,它是用来衡量不同超参数取值下模型的表现的线。在我们建好的决策树里,我们的模型度量指标就是 score。
test = []
for i in range(10):
clf = tree.DecisionTreeClassifier(max_depth=i+1,
criterion="entropy",
random_state=30,
splitter="random")
clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, Ytest) # 记录下不同 max_depth 下模型在测试集分数
test.append(score)
plt.plot(range(1,11),test,color="red",label="学习曲线")
plt.ylabel("socre")
plt.xlabel("max_depth")
plt.legend()
plt.show();
无论如何,剪枝参数的默认值会让树无尽地生长,这些树在某些数据集上可能非常巨大,对内存的消耗也非常巨大。
所以如果你手中的数据集非常巨大,你已经预测到无论如何你都是要剪枝的,那提前设定这些参数来控制树的复杂性和大小会比较好。
属性是在模型训练之后,能够调用查看的模型的各种性质 。对决策树来说,最重要的是feature_importances_,能够查看各个特征对模型的重要性。
sklearn 中许多算法的接口都是相似的,比如说我们之前已经用到的 fit 和 score,几乎对每个算法都可以使用。除了这两个接口之外,决策树最常用的接口还有 apply 和 predict。
apply 中输入测试集返回每个测试样本所在的叶子节点的索引。
predict 输入测试集返回每个测试样本的标签。返回的内容一目了然并且非常容易,大家感兴趣可以自己下去试试看。
在这里不得不提的是,所有接口中要求输入 Xtrain 和 Xtest 的部分,输入的特征矩阵必须至少是一个二维矩阵。 sklearn 不接受任何一维矩阵作为特征矩阵被输入。如果你的数据的确只有一个特征,那必须用 reshape(-1,1)来给矩阵增维。
我们已经学完了分类树 DecisionTreeClassifier 和用决策树绘图(export_graphviz)的所有基础。我
们讲解了决策树的基本流程,分类树的八个参数,一个属性,四个接口,以及绘图所用的代码:
有了这些知识,基本上分类树的使用大家都能够掌握了,接下来再到实例中去磨练就好。
对于分类问题,永远都逃不过的一个痛点就是样本不均衡问题。
样本不均衡是指在一组数据集中,标签的一类天生占有很大的比例,但我们有着捕捉出某种特定的分类的需求的状况。比如,我们现在要对潜在犯罪者和普通人进行分类,潜在犯罪者占总人口的比例是相当低的,也许只有2%左右,98%的人都是普通人,而我们的目标是要捕获出潜在犯罪者。这样的标签分布会带来许多问题。
首先,分类模型天生会倾向于多数的类,让多数类更容易被判断正确,少数类被牺牲掉。因为对于模型而言,样本量越大的标签可以学习的信息越多,算法就会更加依赖于从多数类中学到的信息来进行判断。如果我们希望捕获少数类,模型就会失败。
其次,模型评估指标会失去意义。这种分类状况下,即便模型什么也不做,全把所有人都当成不会犯罪的人,准确率也能非常高,这使得模型评估指标 accuracy 变得毫无意义,根本无法达到我们的“要识别出会犯罪的人”的建模目的。 所以现在,我们首先要让算法意识到数据的标签是不均衡的,通过施加一些惩罚或者改变样本本身,来让模型向着捕获少数类的方向建模。
我们可以使用上采样和下采样来达成这个目的,所用的方法叫做 SMOTE,这种方法通过将少数类的特征重新组合,创造出更多的少数类样本。但这些采样方法会增加样本的总数,对于决策树这个样本总是对计算速度影响巨大的算法来说,我们完全不想轻易地增加样本数量,所以我们要寻求另一条路:改进我们的模型评估指标,使用更加针对于少数类的指标来优化模型。
class_weight
在决策树中,存在着调节样本均衡的参数:class_weight 和接口 fit 中可以设定的
sample_weight。
在决策树中,参数 class_weight 默认 None,此模式表示假设数据集中的所有标签是均衡的,即自动认为标签的比例是 1:1。所以当样本不均衡的时候,我们可以使用形如 {“标签的值 1”:权重 ,“标签的值 2”:权重 2} 的字典来输入真实的样本标签比例,来让算法意识到样本是不平衡的。或者使用”balanced“模式,
有了权重之后,样本量就不再是单纯地记录数目,而是受输入的权重影响了,因此这时候剪枝,就需要搭配 min_ weight_fraction_leaf 这个基于权重的剪枝参数来使用。
另请注意,基于权重的剪枝参数(例如 min_weight_ fraction_leaf)将比不知道样本权重的标准(比如min_samples_leaf)更少偏向主导类。如果样本是加权的,则使用基于权重的预修剪标准来更容易优化树结构,这确保叶节点至少包含样本权重的总和的一小部分。
从上一节的例子中可以看出,如果我们的目标是希望尽量捕获少数类,那准确率这个模型评估逐渐失效,所以我们需要新的模型评估指标来帮助我们。如果简单来看,其实我们只需要查看模型在少数类上的准确率就好了,只要能够将少数类尽量捕捉出来,就能够达到我们的目的。
但此时,新问题又出现了,我们对多数类判断错误后,会需要人工甄别或者更多的业务上的措施来一一排除我们判断错误的多数类,这种行为往往伴随着很高的成本。比如银行在判断”一个申请信用卡的客户是否会出现违约行为“的时候,如果一个客户被判断为”会违约“,这个客户的信用卡申请就会被驳回,如果为了捕捉出”会违约“的人,大量地将”不会违约“的客户判断为”会违约“的客户,就会有许多无辜的客户的申请被驳回。信用卡对银行来说意味着利息收入,而拒绝了许多本来不会违约的客户,对银行来说就是巨大的损失。同理,大众在召回不符合欧盟标准的汽车时,如果为了找到所有不符合标准的汽车,而将一堆本来符合标准了的汽车召回,这个成本是不可估量的。
也就是说,单纯地追求捕捉出少数类,就会成本太高,而不顾及少数类,又会无法达成模型的效果。所以在现实中,我们往往在**寻找捕获少数类的能力和将多数类判错后需要付出的成本的平衡。如果一个模型在能够尽量捕获少数类的情况下,还能够尽量对多数类判断正确,则这个模型就非常优秀了。**为了评估这样的能力,我们将引入新的模型评估指标:混淆矩阵来帮助我们。
混淆矩阵是二分类问题的多维衡量指标体系,在样本不平衡时极其有用。
在混淆矩阵中,我们将少数类认为是正例,多数类认为是负例。
在决策树,随机森林这些分类算法里,即是说少数类是 1,多数类是 0。
在 SVM 里,就是说少数类是 1,多数类是-1。
普通的混淆矩阵,一般使用{0,1}来表示。混淆矩阵阵如其名,十分容易让人混淆,在许多教材中,混淆矩阵中各种各样的名称和定义让大家难以理解难以记忆。我为大家找出了一种简化的方式来显示标准二分类的混淆矩阵,如图所示:
其中:
因此矩阵中四个元素分别表示:
◼ TP(True Positive): 真实为 1,预测作 1
◼ FN(False Negative): 真实为 1,预测作 0
◼ FP(False Positive): 真实为 0,预测作 1
◼ TN(True Negative): 真实为 0,预测作 0
基于混淆矩阵,我们有一系列不同的模型评估指标,这些评估指标的范围都在[0,1]之间,所有以 11 和00 为分子的指标都是越接近 1 越好,所以以 01 和 10 为分子的指标都是越接近 0 越好。对于所有的指标, 我们用橙色表示分母,用绿色表示分子,则我们有:
(1) 模型整体效果:准确率
准确率 Accuracy 就是所有预测正确的所有样本除以总样本,通常来说越接近 1 越好。 记住少数类是1,多数类是 0
(2) 精确度 Precision
精确度 Precision,又叫查准率,表示在所有预测结果为 1 的样例数中,实际为 1 的样例数所占比重。精确度越低,意味着 01 比重很大,则代表你的模型对多数类 0 误判率越高,误伤了过多的多数类。 为了避免对多数类的误伤,需要追求高精确度。
精确度是” 将多数类判错后所需付出成本的衡量’’。
#所有判断正确并确实为 1 的样本 / 所有被判断为 1 的样本
#对于没有 class_weight 的决策树来说:
Precision_1 = (Ytest[Ytest == clf_01.predict(Xtest)] == 1).sum()/(clf_01.predict(Xtest) == 1).sum()
Precision_1
#对于有 class_weight 的决策树来说:
Precision_2 = (Ytest[Ytest == clf_02.predict(Xtest)] == 1).sum()/(clf_02.predict(Xtest) == 1).sum()
Precision_2
0.6363636363636364
0.68
在现实的样本不平衡例子中,当每一次将多数类判断错误的成本非常高昂的时候(比如大众召回车辆的例子),我们会追求高精确度。
当然了,如果我们的目标是不计一切代价捕获少数类,那我们并不在意精确度,而在意召回率。
(3) 召回率 Recall
召回率 Recall,又被称为敏感度(sensitivity),真正率,查全率,表示所有真实为 1 的样本中,被我们预测正确的样本所占的比例。
召回率越高,代表我们尽量捕捉出了越多的少数类。
召回率越低,代表我们没有捕捉出足够的少数类。
#所有 predict 为 1 并且正确的点 / 全部为 1 的点的比例
#对于没有 class_weight 的决策树来说:
(ytest[ytest == clf.predict(Xtest)] == 1).sum()/(ytest == 1).sum()
#对于有 class_weight 的决策树来说:
(ytest[ytest == wclf.predict(Xtest)] == 1).sum()/(ytest == 1).sum()
0.4827586206896552
0.5862068965517241
可以看出,做样本平衡之前,我们只成功捕获了 48%左右的少数类点,而做了样本平衡之后的模型,捕捉出了 58%的少数类点。召回率可以帮助我们判断,我们是否捕捉除了全部的少数类,所以又叫做查全率。
如果我们希望不计一切代价,找出少数类(比如找出潜在犯罪者的例子),那我们就会追求高召回率,相反如果我们的目标不是尽量捕获少数类,那我们就不需要在意召回率。
注意召回率和精确度的分子是相同的(都是 11),只是分母不同。
而召回率和精确度是此消彼长的,两者之间的平衡代表了捕捉少数类的需求和尽量不要误伤多数类的需求的平衡。
究竟要偏向于哪一方,取决于我们的业务需求:究竟是误伤多数类的成本更高,还是无法捕捉少数类的代价更高。
(4) F1 measure
为了同时兼顾精确度和召回率,我们创造了两者的调和平均数作为考量两者平衡的综合性指标,称之为F1 measure。
两个数之间的调和平均倾向于靠近两个数中比较小的那一个数,因此我们追求尽量高的 F1 measure,能够保证我们的精确度和召回率都比较高。
F1 measure 在[0,1]之间分布,越接近 1 越好。
(5) 假负率
从 Recall 延申出来的另一个评估指标叫做假负率(False Negative Rate),它等于 1 - Recall,用于衡量所有真实为 1 的样本中,被我们错误判断为 0 的,通常用得不多。
(6) ROC 曲线
ROC 的全称是 Receiver Operating Characteristic Curve,其主要的分析方法就是画这条特征曲线。
该曲线的横坐标为假正率(False Positive Rate, FPR), N 是真实负样本的个数, FP 是 N 个负样本中被分类器器预测为正样本的个数。
纵坐标为召回率,真正率(True Positive Rate, TPR):
P 是真实正样本的个数,TP 是 P 个正样本中被分类器器预测为正样本的个数。
(7) sklearn 中的混淆矩阵
metrics.confusion_matrix(Ytest,clf_01.predict(Xtest))
metrics.confusion_matrix(Ytest,clf_02.predict(Xtest))
array([[183, 8], [ 15, 14]], dtype=int64)
array([[183, 8], [ 12, 17]], dtype=int64)
# Precision
metrics.precision_score(Ytest,clf_01.predict(Xtest))
metrics.precision_score(Ytest,clf_02.predict(Xtest))
0.6363636363636364
0.68
# Recall
metrics.recall_score(Ytest,clf_01.predict(Xtest))
metrics.recall_score(Ytest,clf_02.predict(Xtest))
0.4827586206896552
0.5862068965517241
# F-measure
metrics.f1_score(Ytest,clf_01.predict(Xtest))
metrics.f1_score(Ytest,clf_02.predict(Xtest))
0.5490196078431373
0.6296296296296295
在正式进入到回归分析的相关算法讨论之前,我们需要对有监督学习算法中的回归问题进行进一步的分析和理解。虽然回归问题和分类问题同属于有监督学习范畴,但实际上,回归问题要远比分类问题更加复杂。
首先是关于输出结果的对比,分类模型最终输出结果为离散变量,而离散变量本身包含信息量较少,其本身并不具备代数运算性质,因此其评价指标体系也较为简单,最常用的就是混淆矩阵以及ROC曲线。而回归问题最终输出的是连续变量,其本身不仅能够代数运算,且还具有更"精致"的方法,希望对事物运行的更底层原理进行挖掘。即回归问题的模型更加全面、完善的描绘了事物客观规律,从而能够得到更加细粒度的结论。 因此,回归问题的模型往往更加复杂,建模所需要数据所提供的信息量也越多,进而在建模过程中可能遇到的问题也越多。
线性回归是解决回归类问题最常使用的算法模型,其算法思想和基本原理都是由多元统计分析发展而
来,但在数据挖掘和机器学习领域中,也是不可多得的行之有效的算法模型。一方面,线性回归蕴藏的机器学习思想非常值得借鉴和学习,并且随着时间发展,在线性回归的基础上还诞生了许多功能强大的非线性模型。因此,我们在进行机器学习算法学习过程中,仍然需要对线性回归这个统计分析算法进行系统深入的学习。但这里需要说明的是,线性回归的建模思想有很多理解的角度,此处我们并不需要从统计学的角度来理解、掌握和应用线性回归算法,很多时候,利用机器学习的思维来理解线性回归,会是一种更好的理解方法,这也将是我们这部分内容讲解线性回归的切入角度。
任何机器学习算法首先都有一个最底层的核心逻辑,当我们在利用机器学习思维理解线性回归的时候,首先也是要探究其底层逻辑。值得庆辛的是,虽然线性回归源于统计分析,但其算法底层逻辑和机器学习算法高度契合。
现在问题转换成了求解让SSE最小化的参数向量w ,这种通过最小化真实值和预测值之间的SSE来求解参数的方法叫做最小二乘法。
多元线性回归的执行函数编写并不复杂,主要涉及大量的矩阵运算,需要借助Numpy中的矩阵数据格式来完成。
回顾可能用到的矩阵运算相关知识:
a = np.array([[1,2],[3,4]])
m = np.mat(a) # NumPy中创建矩阵需要使⽤mat函数,该函数需要输⼊⼆维数组
m
在Numpy中,矩阵和数组有非常多的相似之处,例如矩阵的索引、切片等常用方法和数组均保持一致。
# 矩阵转置
m.T
# 矩阵乘法
m * m a * a
# 矩阵行列式
np.linalg.det(m)
# 求逆
m.I
然后编写线性回归函数,同理,我们假设输入数据集为DataFrame,且最后一列为标签值。
def standRegres(dataSet):
xMat = np.mat(dataSet.iloc[:,:-1].values) # 提取特征
yMat = np.mat(dataSet.iloc[:,-1].values).T # 提取标签
xTx = xMat.T*xMat
if np.linalg.det(xTx) ==0:
print('This matrix is singular,cannot do inverse') #行列式为0,则该矩阵为奇异矩阵,无法求解逆矩阵
return
ws = xTx.I * (xMat.T*yMat)
return ws
这里需要提前判断xTx是否是满秩矩阵。若不满秩,则无法对其进行求逆矩阵的操作。定义完函数后,即可测试运行效果,此处我们建立线性随机数进行多元线性回归方程拟合。这里需要注意的是,当使用矩阵分解来求解多元线性回归时,必须添加一列全为1的列,用于表征线性方程截距w0。
rng = np.random.RandomState(1) # 设置随机种子
x = 5*rng.rand(100) # 100个[0,5)的随机数
y = 2*x-5+rng.randn(100) # 真实规律的标签取值
X = pd.DataFrame(x)
Y = pd.DataFrame(y)
ex = pd.DataFrame(np.ones([100,1])) #添加一列权威1的列,表示截距
data = pd.concat([ex,X,Y],axis=1)
数据满足基本的建模要求,然后执行函数运算:
ws = standRegres(data)
ws
返回结果即为各列特征权重,其中数据集第一列值均为1,因此返回结果的第一个分量表示截距。然后可用可视化图形展示模型拟合效果∶
yhat = data.iloc[:,:-1].values*ws # 预测标签值
plt.plot(data.iloc[:,1],data.iloc[:,2],'o') #原始数据点
plt.plot(data.iloc[:,1],yhat) # 拟合直线
y = data.iloc[:,-1].values
yhat = yhat.flatten()
SSE = np.power(yhat-y,2).sum()
SSE
当然也可以编写成一个完整的函数,为提高复用性,设置输入参数为数据集和回归方法:
def sseCal(dataSet,regres):
n = dataSet.shape[0] y = dataSet.iloc[:,-1].values
ws = regres(dataSet)
yhat = dataSet.iloc[:,:-1].values*ws
yhat = yhat.flatten()
SSE = np.power(yhat-y,2).sum()
return SSE
测试运行:
sseCal(data,standRegres)
sse = sseCal(data,standRegres)
y = data.iloc[:,-1].values
sst = np.power(y-y.mean(),2).sum()
1-sse/sst
结果为0.91。能够看出最终拟合效果非常好。当然,我们也可编写函数封装计算判断系数的相关操作,同样留⼀个调整回归函数的接口。
def rSquare(dataSet,regres):
sse = sseCal(dataSet,regres)
y = dataSet.iloc[:,-1].values
sst = np.power(y-y.mean(),2).sum()
return 1-sse/sst
然后进行测试
rSquare(data,standRegres)
使用scikit-learn算法库实现线性回归算法,并计算相应评价指标。回顾前文介绍的相关知识进行下述计算。
from sklearn.linear_model import LinearRegression
reg = LinearRegression()
reg.fit(data.iloc[:,:-1].values,data.iloc[:,-1].values)
reg.coef_ # 查看方程系数
reg.intercept_ # 查看系数
对比手动计算的ws,其结果高度一致。
然后计算模型MSE和判别系数R方:
from sklearn.metrics import mean_squared_error,r2_score
yhat = reg.predict(data.iloc[:,:-1])
mean_squared_error(y,yhat)
r2_score(y,yhat)
和上述手动编写函数结果一致。
方差是来衡量数据集包含了多少数据量
R方越趋近于1,拟合效果就愈好,趋于0拟合效果越差
解决共线性的问题的方法主要有以下三种:
通常来说,能够利用⼀个算法解决的问题尽量不用多个算的组合来解决,因此此处我们主要考虑后两个解决方案,其中逐步回归我们将放在线性回归的最后一部分进行讲解,而第三个解决方案,则是我们接下来需要详细讨论的岭回归算法和Lasso算法。
接下来,尝试在Python中手动编写岭回归函数,当然在借助矩阵运算的情况下岭回归函数的编写也不会太复杂,这里唯⼀需要注意的是对于单位矩阵的⽣成,需要借助Numpy中的eye函数,该函数需要输入对角矩阵规模参数。
虽说是生成单位矩阵,但实际上返回的仍然是array对象,若要真正意义上执行矩阵运算,则还需要进行矩阵转化,接下来编写岭回归函数,此处默认设置岭回归系数为0.2,当该系数不为0的时候不会存在不可逆的情况,因此可免去 if 语句判别是否满秩的设置。
def ridgeRegres(dataSet,lam=0.2):
xMat = np.mat(dataSet.iloc[:,:-1].values)
yMat = np.mat(dataSet.iloc[:,-1].values).T
xTx = xMat.T * xMat
denom = xTx + np.eye(xMat.shape[1])*lam
ws = denom.I * (xMat.T * yMat)
return ws
然后读取 abalone 数据集进行岭回归建模,该数据集源于UCI,记录了鲍⻥的生物属性,目标字段是鲍⻥的年龄:
aba = pd.read_csv('abalone.csv',header=None)
aba.head()
其中⼀列为标签列,同时,由于数据集第⼀列是分类变量,且没有用于承载截距系数的列。为了简化教学流程,此处直接将第⼀列的值全部修改为1,然后在进行建模:
aba.iloc[:,0]=1
aba.head()
接着带入模型进行计算并返回各列系数:
rws = ridgeRegres(aba)
rws
由于数据集本身性质满足最小二乘法求解条件,因此可对比最小⼆乘法求解结果:
standRegres(aba)
能够发现由于惩罚因子存在,最终输出结果存在细微差别。同时我们可比较二者模型的评价指标:
rSquare(aba,ridgeRegres)
rSquare(aba,standRegres)
这里由于数据集本身性质原因,岭回归和线性回归返回结果并没有太大区别。接下来,调用scikit-learn中岭回归算法验证手动建模有效性。
from sklearn.linear_model import Ridge
ridge = Ridge(alpha = 0.2)
ridge.fit(aba.iloc[:,:-1],aba.iloc[:,-1])
查看模型相关参数:
ridge.coef_ #查看系数
ridge.intercept_ # 查看截距
对比手动建模结果,能够看出结果基本保持⼀致。
接着我们使用交叉验证来选择最佳的正则化系数。在scikit-learn中,我们有带交叉验证的岭回归可以使用,我们来看⼀看:
class sklearn.linear_model.RidgeCV (alphas=(0.1, 1.0, 10.0), fifit_intercept=True,
normalize=False,scoring=None, cv=None, gcv_mode=None, store_cv_values=False)
可以看到,这个类与普通的岭回归类Ridge非常相似,不过在输入正则化系数 的时候我们可以传入元组作为正则化系数的备选,非常类似于我们在画学习曲线前设定的for i in 的列表对象。来看RidgeCV的重要参数,属性和接口:
这个类的使也用非常容易,依然使用我们之前建立的鲍鱼年龄数据集:
from sklearn.linear_model import RidgeCV
Ridge_ = RidgeCV(alphas=np.arange(1,1001,100)
,scoring="r2"
,store_cv_values=True
#,cv=5
).fit(aba.iloc[:,:-1],aba.iloc[:,-1])
#⽆关交叉验证的岭回归结果
Ridge_.score(aba.iloc[:,:-1],aba.iloc[:,-1])
#调⽤所有交叉验证的结果
Ridge_.cv_values_.shape
#进⾏平均后可以查看每个正则化系数取值下的交叉验证结果
Ridge_.cv_values_.mean(axis=0)
#查看被选择出来的最佳正则化系数
Ridge_.alpha_
接下来,尝试在scikit-learn中执行Lasso算法,仍然是采用abalone数据集,Lasso算法模型也同样是保存在linear_model模块中。
from sklearn.linear_model import Lasso
las = Lasso(alpha=0.01)
las.fit(aba.iloc[:,:-1],aba.iloc[:,-1])
查看执行结果
las.coef_
las.intercept_
不同于岭回归,Lasso算法的处理结果中自变量系数会更加倾向于迅速递减为0,因此Lasso算法在自变量选择方面要优于岭回归。 能够看出Lasso回归自变量衰减速度非常快,当入取0.01的时候就已经出现部分自变量系数为0的情况,从中也能看出自变量的相对重要性。
多元线性回归,岭回归,Lasso三个算法,它们都是围绕着原始的线性回归进行的拓展和改进。其中岭回归和Lasso是为了解决多元线性回归中使用最小二乘法的各种限制,主要用途是消除多重共线性带来的影响并且做特征选择。除此之外,本章还定义了多重共线性和各种线性相关的概念,并为大家补充了一些线性代数知识。回归算法属于原理简单,但操作困难的机器学习算法,在实践和理论上都还有很长的路可以走。
分类技术是机器学习和数据挖掘应用中的重要组成部分。在数据科学中, 约70%的问题属于分类问题。
解决分类的算法也有很多种。 如:KNN,使距离计算来实现分类;决策树,通过构建直观易懂的树来实现分类。这里我们要展开的是Logistic回归,它是一种很常见的用来解决二元分类问题的回归方法,它主要是通过寻找最优参数来正确地分类原始数据。
逻辑回归(Logistic Regression,简称LR),其实是一个很有误导性的概念,虽然它的名字中带有"回归"两个字,但是它最擅长处理的却是分类问题。LR分类器适用于各项广义上的分类任务,例如:评论信息的正负情感分析(二分类)、用户点击率(二分类)、用户违约信息预测(二分类)、垃圾邮件预测(二分类)、疾病预测(二分类)、用户等级分类(多分类)等场景。我们这里主要讨论的是二分类问题。
在求解机器学习算法的模型参数,即无约束优化问题时,梯度下降(Gradient Descent)是最常用的方法之一,接下来就对梯度下降进行介绍。
首先来看看梯度下降的一个直观的解释。比如我们在一座大山上的某处位置,由于我们不知道怎么下山,于是决定走一步算一步,也就是在每走到一个位置的时候,求解当前位置的梯度,沿着梯度的负方向,也就是当前最陡峭的位置向下走一步,然后继续求解当前位置梯度,向这一步所在位置沿着最陡峭最易下山的位置走一步。这样一步步的走下去,一直走到觉得我们已经到了山脚。当然这样走下去,有可能我们不能走到山脚,而是到了某一个局部的山峰低处。
从上面的解释可以看出,梯度下降不一定能够找到全局的最优解,有可能是一个局部最优解。当然,如果损失函数是凸函数,梯度下降法得到的解就一定是全局最优解。
class sklearn.linear_model.LogisticRegression (penalty=’l2’, dual=False, tol=0.0001,
C=1.0,fifit_intercept=True, intercept_scaling=1, class_weight=None, random_state=None, solver=’warn’,
max_iter=100*, multi_class=’warn’, verbose=0, warm_start=False, n_jobs=None)
LogisticRegression默认就带了正则化项。penalty参数可选择的值为"1"和"2",分别对应L1的正则化和L2的正则化,默认是L2的正则化。
在调参时如果我们主要的目的只是为了解决过拟合,一般penalty选择L2正则化就够了。但是如果选择L2正则化发现还是过拟合,即预测效果差的时候,就可以考虑L1正则化。另外,如果模型的特征非常多,我们希望一些不重要的特征系数归零,从而让模型系数稀疏化的话,也可以使用L1正则化。
penalty参数的选择会影响我们损失函数优化算法的选择。即参数solver的选择,如果是L2正则化,那么4种可选的算法 (‘newton-cg’,‘Ibfgs’,‘liblinear’,‘sag’} 都可以选择。但是如果penalty是L1正则化的话,就只能选择 ‘liblinear’了。这是因为L1正则化的损失函数不是连续可导的,而 {‘newton-cg’,‘Ibfgs,‘sag’}这三种优化算法时都需要损失函数的一阶或者二阶连续导数。而 ’liblinear’ 并没有这个依赖。
而两种正则化下C的取值,都可以通过学习曲线来进行调整。
建立两个逻辑回归,L1正则化和L2正则化的差别一目了然∶
from sklearn.linear_model import LogisticRegression as LR
from sklearn.datasets import load_breast_cancer
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
data = load_breast_cancer()
X = data.data
y = data.target
data.shape
lrl1 = LR(penalty="l1",solver="liblinear",C=0.5,max_iter=1000)
#逻辑回归的重要属性coef_,查看每个特征所对应的参数
lrl2 = LR(penalty="l2",solver="liblinear",C=0.5,max_iter=1000)
lrl1 = lrl1.fit(X,y)
lrl1.coef_
(lrl1.coef_ != 0).sum(axis=1)
lrl2 = lrl2.fit(X,y)
lrl2.coef_
可以看见,当我们选择L1正则化的时候,许多特征的参数都被设置为了0,这些特征在真正建模的时候,就不会出现在我们的模型当中了,而L2正则化则是对所有的特征都给出了参数。
究竟哪个正则化的效果更好呢?还是都差不多?
l1 = []
l2 = []
l1test = []
l2test = []
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,y,test_size=0.3,random_state=420)
for i in np.linspace(0.05,1,19):
lrl1 = LR(penalty="l1",solver="liblinear",C=i,max_iter=1000)
lrl2 = LR(penalty="l2",solver="liblinear",C=i,max_iter=1000)
lrl1 = lrl1.fit(Xtrain,Ytrain)
l1.append(accuracy_score(lrl1.predict(Xtrain),Ytrain))
l1test.append(accuracy_score(lrl1.predict(Xtest),Ytest))
lrl2 = lrl2.fit(Xtrain,Ytrain)
l2.append(accuracy_score(lrl2.predict(Xtrain),Ytrain))
l2test.append(accuracy_score(lrl2.predict(Xtest),Ytest))
graph = [l1,l2,l1test,l2test]
color = ["green","black","lightgreen","gray"]
label = ["L1","L2","L1test","L2test"]
plt.figure(figsize=(6,6))
for i in range(len(graph)):
plt.plot(np.linspace(0.05,1,19),graph[i],color[i],label=label[i])
plt.legend(loc=4) #图例的位置在哪⾥?4表示,右下⻆
plt.show()
可见,至少在我们的乳腺癌数据集下,两种正则化的结果区别不大。但随着C的逐渐变大,正则化的强度越来越小,模型在训练集和测试集上的表现都呈上升趋势,直到C=0.8左右,训练集上的表现依然在走高,但模型在未知数据集上的表现开始下跌,这时候就是出现了过拟合。我们可以认为,C设定为0.8会比较好。在实际使用时,基本就默认使用L2正则化,如果感觉到模型的效果不好,那就换L1试试看。
solver参数决定了我们对逻辑回归损失函数的优化方法,有4种算法可以选择,分别是∶
从上面面的描述可以看出,newton-cg,Ibfgs和sag这三种优化算法时都需要损失函数的一阶或者二阶连续导数,因此不能用于没有连续导数的L1正则化,只能用于L2正则化。而liblinear通吃L1正则化和L2正则化。 同时,sag每次仅使用了部分样本进行梯度迭代,所以当样本量少的时候不要选择它,而如果样本量非常大,比如大于10万,sag是第一选择。但是sag不能用于L1正则化,所以当你有大量的样本,又需要L1正则化的话就要自己做取舍了。要么通过对样本采样来降低样本量量,要么回到L2正则化。
从上面的描述,大家可能觉得,既然newton-cg,Ibfgs和sag这么多限制,如果不是大样本,我们选择liblinear不就行了吗?因为liblinear也有自己的弱点!我们知道,逻辑回归有二元逻辑回归和多元逻辑回归。对于多元逻辑回归常见的有one-vs-rest(OvR)和many-vs-many(MvM)两种,而MvM一般比OvR分类相对准确一些。liblinear只支持OVR,不支持MVM,这样如果我们需要相对精确的多元逻辑回归时就不的先择liblinear了。也意味着如果我们需要相对精确的多元逻辑回归不能位用L1正则化了。
l2 = []
l2test = []
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X,y,test_size=0.3,random_state=420)
for i in np.arange(1,201,10):
lrl2 = LR(penalty="l2",solver="liblinear",C=0.9,max_iter=i)
lrl2 = lrl2.fit(Xtrain,Ytrain)
l2.append(accuracy_score(lrl2.predict(Xtrain),Ytrain))
l2test.append(accuracy_score(lrl2.predict(Xtest),Ytest))
graph = [l2,l2test]
color = ["black","gray"]
label = ["L2","L2test"]
plt.figure(figsize=(20,5))
for i in range(len(graph)):
plt.plot(np.arange(1,201,10),graph[i],color[i],label=label[i])
plt.legend(loc=4)
plt.xticks(np.arange(1,201,10))
plt.show()
#我们可以使⽤属性.n_iter_来调⽤本次求解中真正实现的迭代次数
lr = LR(penalty="l2",solver="liblinear",C=0.9,max_iter=300).fit(Xtrain,Ytrain)
lr.n_iter_
当max_iter中限制的步数已经走完了,逻辑回归却还没有找到损失函数的最小值,参数w的值还没有被收敛,sklearn就会弹出下面的红色警告
当参数solver="liblinear"∶
当参数solver="sag"∶
虽然写法看起来略有不同,但其实都是一个含义,这是在提醒我们:参数没有收敛,请增大max_iter中输入的数字。但我们不一定要听sklearn的。max_iter很大,意味着步长小,模型运行得会更加缓慢。然我们在梯度下降中追求的是损失函数的最小值,但这也可能意味着我们的模型会过拟合(在训练集上表现得太好,在测试集上却不一定),因此,如果在max iter报红条的情况下,模型的训练和预测效果都已经不错了,那我们就不需要再增大max_iter中的数目了,毕竟一切都以模型的预测效果为基准 一只要最终的预测效果好,运行又快,那就一切都好,无所谓是否报红色警告了。
multi_class参数决定了我们分类方式的选择,有ovr和multinomial两个值可以选择,默认是ovr。
ovr即前面提到的one-vs-rest(OvR),而multinomial即前面提到的many-vs-many(MvM)。如果是二元逻辑回归,ovr和multinomial并没有任何区别,区别主要在多元逻辑回归上。 OvR的思想很简单,无论你是多少元逻辑回归,我们都可以看做二元逻辑回归。具体做法是,对于第K 类的分类决策,我们把所有第K类的样本作为正例,除了第K类样本以外的所有样本都作为负例,然后在上面做二元逻辑回归,得到第K类的分类模型。其他类的分类模型获得以此类推。
from sklearn.linear_model import LogisticRegression as LR
from sklearn.datasets import load_iris
iris = load_iris()
for multi_class in ('multinomial', 'ovr'):
clf = LR(solver='sag', max_iter=100, random_state=42,
multi_class=multi_class).fit(iris.data,iris.target)
#打印两种multi_class模式下的训练分数
#%的⽤法,⽤%来代替打印的字符串中,想由变量替换的部分。%.3f表示,保留三位⼩数的浮点数。%s表示,字符串。
#字符串后的%后使⽤元祖来容纳变量,字符串中有⼏个%,元组中就需要有⼏个变量
print("training score : %.3f (%s)" % (clf.score(iris.data,
iris.target),multi_class))