Python机器学习基础教程学习笔记 笔记学习来源:Python机器学习基础教程
学习时间:2021年4月15日开始学习
第一章 引言 1.1 概念
监督学习
从输入输出对中进行学习的算法。
示例:识别信封上手写的邮政编码,基于医学影像判断肿瘤是否为良性,检测信用卡交易中的诈骗行为。
无监督学习
只有输入数据是已知的,没有为算法提供输出数据。
示例:确定一系列博客文章的主题,将客户分成具有相似偏好的群组,检测网站的异常访问模式。
1.2 环境搭建和工具介绍
scikit-learn
Scikit-learn(以前称为scikits.learn,也称为sklearn)是针对Python 编程语言的免费软件机器学习库。它具有各种分类,回归和聚类算法,包括支持向量机,随机森林,梯度提升,k均值和DBSCAN,并且旨在与Python数值科学图书馆NumPy和SciPy。
1 pip install numpy scipy matplotlib ipython scikit-learn pandas
必要的库和工具
NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。NumPy 是一个运行速度非常快的数学库,主要用于数组计算,核心功能是ndarray
类,即n维数组,数组中的所有元素必须是同一类型。
1 2 3 4 import numpy as npx = np.array([[1 ,2 ,3 ],[4 ,5 ,6 ]]) print ("x:\n{}" .format (x))
Scipy是一个用于数学、科学、工程领域的常用软件包,可以处理插值、积分、优化、图像处理、常微分方程数值解的求解、信号处理等问题。它用于有效计算Numpy矩阵,使Numpy和Scipy协同工作,高效解决问题。最重要的是scipy.sparse
1 2 3 4 5 from scipy import sparseeye = np.eye(4 ) print ("NumPy array:\n{}" .format (eye))
Matplotlib 是 Python 的绘图库。 它可与 NumPy 一起使用,提供了一种有效的 MatLab 开源替代方案。 它也可以和图形工具包一起使用,如 PyQt 和 wxPython。
1 2 3 4 5 6 7 8 import matplotlib.pyplot as pltx = np.linspace(-10 ,10 ,100 ) y = np.sin(x) plt.plot(x,y,marker="x" ) plt.show()
pandas,python+data+analysis的组合缩写,是python中基于numpy和matplotlib的第三方数据分析库,与后两者共同构成了python数据分析的基础工具包,享有数分三剑客之名。正因为pandas是在numpy基础上实现,其核心数据结构与numpy的ndarray十分相似,但pandas与numpy的关系不是替代,而是互为补充。
默认导入环境
1 2 3 4 5 import numpy as npfrom scipy import sparseimport matplotlib.pyplot as pltimport pandas as pdimport mglearn
1.3 第一个应用:鸢尾花分类
目标:构建一个机器学习模型,可以从这些已知品种的鸢尾花测量数据中进行学习,从而能够预测新鸢尾花的品种,这一模型为监督学习。
核心术语:单个数据点
(一朵鸢尾花)的预期输出是这朵花的品种,对于一个数据点来说,他的品种叫做标签
。
1.3.1 初识数据
iris数据集
1 2 3 from sklearn.datasets import load_irisiris_dataset = load_iris()
iris_dataset对象是一个Bunch对象,里面包括键和值。
1 2 3 print ("Keys of iris_dataset:\n{}" .format (iris_dataset.keys()))
DESCR键
DESCR键对应的值是数据集的简要说明。
1 print (iris_dataset['DESCR' ][:193 ] + "\n..." )
target_names键
target_names键对应的值是一个字符串数组,里面包含要预测的花的品种。
1 print ("Target names: {}" .format (iris_dataset['target_names' ]))
feature_names键
feature_names键对应的值是一个字符串列表,对每一个特征进行了说明。
1 print ("Feature names: \n{}" .format (iris_dataset['feature_names' ]))
data键
data键对应的值是二维NumPy数组,每一行对应一朵花,列代表每朵花的四个测量数据。
1 print ("Shape of data: {}" .format (iris_dataset['data' ].shape))
可以看出,数组中包含150朵不同的花的测量数据。data数组的形状(shape)是样本数乘以特征数,这是scikit-learn的约定。
1 print ("First five rows of data:\n{}" .format (iris_dataset['data' ][:5 ]))
以上为data数组的前五行数据。
target键
target键对应的值是一个一维NumPy数组,包含的是测量过的每朵花的品种(标签)。品种被转换为0到2的整数。
1 print ("Target:\n{}" .format (iris_dataset['target' ]))
1.3.2 训练数据和测试数据 我们将数据集分成训练集(training set)和测试集(test set),前者用于构建模型,后者用于评估模型对于前所未见的新数据的泛化能力。
训练数据:用于构建
机器学习模型,又叫做训练集。
测试数据:用于评估
机器学习模型,又叫做测试集,留出集。
train_test_split
函数
train_test_split
函数可以将数据集打乱并进行拆分,这个函数将75%的行数据及对应标签作为训练集,剩下的25%作为测试集。通常,数据用大写的X
表示,而标签用小写的y
表示,X
为输入的二维数组,y
则为输出的一维数组。
1 2 3 4 5 6 7 8 9 from sklearn.datasets import load_irisfrom sklearn.model_selection import train_test_splitiris_dataset = load_iris() X_train,X_test,y_train,y_test = train_test_split(iris_dataset['data' ],iris_dataset['target' ],random_state=0 ) print ("X_train shape: {}" .format (X_train.shape))print ("y_train shape: {}" .format (y_train.shape))print ("X_test shape: {}" .format (X_test.shape))print ("y_test shape: {}" .format (y_test.shape))
1.3.3 观察数据 暂略
1.3.4 构建第一个模型:k近邻算法
k近邻算法含义
scikit-learn中有许多可用的分类算法。要对一个新的数据点做出预测,k近邻算法会在训练集中寻找与这个新数据点距离最近的数据点,然后将找到的数据点的标签赋给这个新数据点。
k近邻算法中的k的含义就是考虑训练集中与新数据点最近的任意k个邻居。
k近邻算法实在neighbors模块的KNeighborsClassifier
类中实现的,最重要的参数就是这个k,即邻居的数量。
代码实现
1 2 3 4 5 6 from sklearn.neighbors import KNeighborsClassifierknn = KNeighborsClassifier(n_neighbors=1 ) knn.fit(X_train,y_train)
1.3.5 做出预测
新的数据点
1 2 X_new = np.array([[5 ,2.9 ,1 ,0.2 ]])
做出预测——predict
1 2 3 4 prediction = knn.predict(X_new) print ("Prediction: {}" .format (prediction))print ("Predicted target name :{}" .format (iris_dataset['target_names' ][prediction]))
1.3.6 评估模型 可以对测试数据中的每朵鸢尾花进行预测,并将预测结果与标签进行对比。
代码实现
1 2 3 4 5 6 7 iris_dataset = load_iris() X_train,X_test,y_train,y_test = train_test_split(iris_dataset['data' ],iris_dataset['target' ],random_state=0 ) knn = KNeighborsClassifier(n_neighbors=1 ) knn.fit(X_train,y_train) print ("Test set score: {:.2f}" .format (knn.score(X_test,y_test)))
可知该模型的准确率为97%。
1.3.7 小结 下面汇总了整个训练和评估过程所必需的代码:
1 2 3 4 5 6 7 8 X_train,X_test,y_train,y_test = train_test_split(iris_dataset['data' ],iris_dataset['target' ],random_state=0 ) knn = KNeighborsClassifier(n_neighbors=1 ) knn.fit(X_train,y_train) print ("Test set score: {:.2f}" .format (knn.score(X_test,y_test)))
这个代码片段包含了应用scikit-learn中任何机器学习算法的核心代码。fit
,predict
和score
方法是scikit-learn监督学习模型中最常用的接口。
第二章 监督学习 2.1 分类与回归 监督机器学习问题主要有两种:分类(classification)和回归(regression)
分类
分类问题的目标是预测类别标签,可分为二分类和多分类问题。
回归
回归问题的目标是预测一个连续值。
区分分类和回归,就是问一个问题,输出是否具有某种连续性。
2.2 泛化,过拟合和欠拟合
泛化 generalization
如果一个模型能够对没见过的数据做出准确预测,我们就说它能够从训练集泛化到测试集。
过拟合 overfitting
如果在拟合模型时过分关注训练集的细节,得到一个在训练集上表现很好,但不能泛化到新数据上的模型,那么就存在过拟合。这种情况通常是构建了一个对现有信息(训练集)过于复杂
的模型。越靠近训练集,越过拟合。或者,训练集和测试集的效率差异太大,也是过拟合。
欠拟合 underfitting
和过拟合相反,构建了过于简单的模型,没有充分考虑到训练集中的各种有效信息。或者,训练集和测试集的效率相似且很差,就是欠拟合。
总结
我们的模型越复杂,在训练数据上的预测结果就越好,但是,如果我们的模型过于复杂,我们开始过多关注训练集中每个单独的数据点,模型就不能很好的泛化到新数据上去。
经验:收集更多的数据,适当
构建更复杂的模型,对监督学习任务往往特别有用,在现实世界中,你往往能够决定收集多少数据,这可能比模型调参更为有效,永远不要低估更多数据的力量。
2.3 监督学习算法 许多算法都有分类和回归两种模式。
2.3.1 一些样本数据集
二分类数据集——forge
1 2 3 4 5 6 7 8 9 10 11 import mglearnimport matplotlib.pyplot as pltX, y = mglearn.datasets.make_forge() mglearn.discrete_scatter(X[:,0 ], X[:,1 ], y) plt.legend(["Class 0" , "Class 1" ], loc=4 ) plt.xlabel("First feature" ) plt.ylabel("Second feature" ) plt.show() print ("X.shape: {}" .format (X.shape))
图像以第一个特征为x轴,第二个特征为y轴,每个数据点对应图像中的一点。
还可知,X数据集总共26个数据,2个特征。
回归用数据集——wave
wave数据集只有一个输入特征和一个连续的目标变量(或==响应==),后者是模型想要预测的对象(一个连续值)。
1 2 3 4 5 6 X, y = mglearn.datasets.make_wave(n_samples=40 ) plt.plot(X, y, 'o' ) plt.ylim(-3 ,3 ) plt.xlabel("Feature" ) plt.ylabel("Target" ) plt.show()
图中x轴表示特征,y轴表示回归目标(对应的响应)。
现实中的二分类数据集——cancer
1 2 3 from sklearn.datasets import load_breast_cancercancer = load_breast_cancer() print ("keys of cancer: \n{}" .format (cancer.keys()))
该数据集包含569个数据,每个数据有30个特征。212个被标记为恶性(malignant),357个被标记为良性(benign)。
现实中的回归数据集——boston
与这个数据集相关的任务是,利用犯罪率,是否临近查尔斯河,公路可达性等信息,预测20世纪70年代波士顿地区房屋价格的中位数,这个数据集包含506个数据点和13个特征。
1 2 3 from sklearn.datasets import load_bostonboston = load_boston() print ("Data shape: {}" .format (boston.data.shape))
2.3.2 k近邻算法
k近邻分类算法
分为单近邻和多近邻。可用第一章的知识分析两者的效率区别。
分析KNeighborsClassifier
查看决策边界,即算法对类别0和类别1的分界线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport mglearnfrom sklearn.neighbors import KNeighborsClassifierX, y = mglearn.datasets.make_forge() fig, axes = plt.subplots(1 ,3 , figsize=(10 ,3 )) for n_neighbors, ax in zip ([1 ,3 ,9 ], axes): clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X,y) mglearn.plots.plot_2d_separator(clf, X, fill=True , eps=0.5 , ax=ax, alpha=.4 ) mglearn.discrete_scatter(X[:,0 ], X[:,1 ], y, ax=ax) ax.set_title("{} neighbor(s)" .format (n_neighbors)) ax.set_xlabel("feature 0" ) ax.set_ylabel("feature 1" ) axes[0 ].legend(loc=3 ) plt.show()
从图中可以看出,使用单一邻居绘制的决策边界紧跟着训练数据,模型越复杂,随着邻居的个数越来越多,决策边界也越来越平滑,更平滑的边界对应着更简单的模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitcancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=66 ) training_accuracy=[] test_accuracy=[] neighbors_settings=range (1 ,11 ) for n_neighbors in neighbors_settings: clf = KNeighborsClassifier(n_neighbors=n_neighbors) clf.fit(X_train, y_train) training_accuracy.append(clf.score(X_train, y_train)) test_accuracy.append(clf.score(X_test, y_test)) plt.plot(neighbors_settings, training_accuracy, label="training accuracy" ) plt.plot(neighbors_settings, test_accuracy, label="test accuracy" ) plt.ylabel("Accuracy" ) plt.xlabel("n_neighbors" ) plt.legend() plt.show()
从图中可知,最佳点在邻居为6的附近,6之前的越靠近训练集,则过拟合,导致泛化能力下降,6以后的越远离训练集,则欠拟合,泛化能力也不好。
k近邻回归算法
单一近邻时,利用单一邻居的预测结果就是最近邻居的目标值。
使用多个近邻时,预测结果为这些邻居的平均值。
利用k近邻回归算法在scikit-learn的KNeighborsRegressor类中实现。
1 2 3 4 5 6 7 8 9 10 from sklearn.neighbors import KNeighborsRegressorX, y=mglearn.datasets.make_wave(n_samples=40 ) X_train, X_test, y_train, y_test=train_test_split(X,y, random_state=0 ) reg=KNeighborsRegressor(n_neighbors=3 ) reg.fit(X_train, y_train) print ("Test set predictions:\n{}" .format (reg.predict(X_test)))print ("Test set R^2: {:.2f}" .format (reg.score(X_test, y_test)))
可知准确率为0.83。
分析KNeighborsRegressor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from sklearn.neighbors import KNeighborsRegressorX, y=mglearn.datasets.make_wave(n_samples=40 ) X_train, X_test, y_train, y_test=train_test_split(X,y, random_state=0 ) fig, axes = plt.subplots(1 ,3 , figsize=(15 ,4 )) line=np.linspace(-3 ,3 ,1000 ).reshape(-1 ,1 ) for n_neighbors,ax in zip ([1 ,3 ,9 ], axes): reg=KNeighborsRegressor(n_neighbors=n_neighbors) reg.fit(X_train, y_train) ax.plot(line, reg.predict(line)) ax.plot(X_train, y_train, '^' , c=mglearn.cm2(0 ), markersize=8 ) ax.plot(X_test, y_test, 'v' , c=mglearn.cm2(1 ), markersize=8 ) ax.set_title( "{} neighbor(s)\n train score: {:.2f} test score: {:.2f}" .format ( n_neighbors, reg.score(X_train, y_train), reg.score(X_test, y_test))) ax.set_xlabel("Feature" ) ax.set_ylabel("Target" ) axes[0 ].legend(["Model predictions" , "Training data/target" , "Test data/target" ], loc="best" ) plt.show()
从图中可知,仅使用单一邻居,训练集中的每个点对预测结果都有显著影响,预测结果的图像经过所有数据点,这导致测试结果非常不稳定,考虑更多邻居后,预测结果变得更加平滑,但对训练数据的拟合也不太好。
k近邻算法优缺点分析
优点:容易理解,模型参数调节简单
缺点:训练集很大时,预测速度会变慢,这一算法对有很多特征的数据集往往效果不好。
综上,k近邻算法由于预测速度慢且不能处理多特征的数据集,所以实践中往往不会用到,但可作为基准测试方法使用。
2.3.3 线性模型 线性模型利用输入特征的线性函数进行预测。
用于回归的线性模型
对于回归问题,线性模型预测的一般公示如下:
1 y = w[0]*x[0] + w[1]*x[1] + ... + w[p]*x[p] +b
x表示单个数据点的特征,w和b是学习模型的参数,y是模型的预测结果。
下面线性模型在wave数据集上学习的参数w和b:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import numpy as npimport matplotlib.pyplot as pltfrom sklearn.linear_model import LinearRegressionfrom sklearn.model_selection import train_test_splitfrom mglearn.datasets import make_wavefrom mglearn.plot_helpers import cm2def plot_linear_regression_wave (): X, y = make_wave(n_samples=60 ) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42 ) line = np.linspace(-3 , 3 , 100 ).reshape(-1 , 1 ) lr = LinearRegression().fit(X_train, y_train) print ("w[0]: %f b: %f" % (lr.coef_[0 ], lr.intercept_)) plt.figure(figsize=(8 , 8 )) plt.plot(line, lr.predict(line)) plt.plot(X, y, 'o' , c=cm2(0 )) ax = plt.gca() ax.spines['left' ].set_position('center' ) ax.spines['right' ].set_color('none' ) ax.spines['bottom' ].set_position('center' ) ax.spines['top' ].set_color('none' ) ax.set_ylim(-3 , 3 ) ax.legend(["model" , "training data" ], loc="best" ) ax.grid(True ) ax.set_aspect('equal' ) if __name__ == "__main__" : plot_linear_regression_wave() plt.show()
有许多线性回归模型,区别在于如何从训练集中学习到参数w和b,以及如何控制模型的复杂度。
标准线性回归(普通最小二乘法)
标准线性回归寻找参数w和b,使得对训练集的预测值与真实的回归目标值y之间的均方误差最小,均方误差是指预测值和真实值之差的平方和除以样本数。线性回归没有额外的控制参数,因此无法控制模型的复杂度。
1 2 3 4 5 6 7 8 from sklearn.linear_model import LinearRegressionimport mglearnfrom sklearn.model_selection import train_test_splitX, y = mglearn.datasets.make_wave(n_samples=60 ) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42 ) lr = LinearRegression().fit(X_train, y_train) print ("lr.coef_: {}" .format (lr.coef_))print ("lr.intercept_: {}" .format (lr.intercept_))
因为wave数据集的数据只有一个特征,则coef_的元素个数为1。
在boston数据集上的准确率测试:
1 2 3 4 5 X, y = mglearn.datasets.load_extended_boston() X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0 ) lr = LinearRegression().fit(X_train, y_train) print ("Traning set score: {:.2f}" .format (lr.score(X_train, y_train)))print ("Test set score: {:.2f}" .format (lr.score(X_test, y_test)))
可以看出,训练集和测试集的效率差异过大,是过拟合
的标志。
岭回归——ridge regression
岭回归特点:
对系数w的拟合附加约束
L2正则化:希望w尽可能小,都接近于0,这样意味着每个特征对输出的影响应尽可能小,模型越简单,避免过拟合。用数学术语来讲,Ridge惩罚
了系数w的L2范数或者w的欧式长度。惩罚系数(penalty)为alpha
。
岭回归在linear_model.Ridge中实现。
在boston数据集上的准确率测试:
1 2 3 4 5 6 from sklearn.linear_model import RidgeX, y = mglearn.datasets.load_extended_boston() X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0 ) ridge = Ridge().fit(X_train, y_train) print ("Traning set score: {:.2f}" .format (ridge.score(X_train, y_train)))print ("Test set score: {:.2f}" .format (ridge.score(X_test, y_test)))
可以看出,训练集上的准确率不及线性回归,但是测试集准确率高,表示泛化能力强。
岭回归模型通过用户设置alpha惩罚参数来在模型的简单性和训练集性能之间做出权衡。
==增大惩罚系数alpha,会使得系数coef_(或w)越趋近于0,正则化约束越强,模型越简单,从而降低了训练集性能,但是泛化能力可能会增强==。
1 ridge = Ridge(alpha = 10 ).fit(X_train, y_train)
lasso回归
lasso回归模型特点:
对系数w的拟合附加约束
L1正则化:使用lasso时,某些系数刚好为0,这说明某些特征被模型完全忽略。lasso惩罚了系数向量的L1范数,即系数的绝对值之和。惩罚系数记也为alpha。
岭回归在linear_model.Lasso中实现。
lasso在boston数据集上的表现:
1 2 3 4 5 6 7 8 from sklearn.linear_model import Lassoimport numpy as npX, y = mglearn.datasets.load_extended_boston() X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0 ) lasso = Lasso().fit(X_train, y_train) print ("Traning set score: {:.2f}" .format (lasso.score(X_train, y_train)))print ("Test set score: {:.2f}" .format (lasso.score(X_test, y_test)))print ("Number of features userd: {}" .format (np.sum (lasso.coef_ != 0 )))
可以看出,lasso在训练集和测试集上的表现都很差,说明存在欠拟合,并且只使用了4个特征,而单个数据点具有105个特征。
lasso默认alpha的值为1.0,为了提高两者表现,需要减小alpha的值,使其拟合一个更复杂的模型。
1 2 lasso = Lasso(alpha=0.01 ,max_iter=100000 ).fit(X_train, y_train)
如果把alpha设置的太小,那么就会消除正则化的约束,使其变得和标准线性回归的结果一样。
在实践中,一般选择岭回归模型,如果考虑到模型的可解释性,即存在某几个特征的影响相比于其它的更显著,那么选择lasso回归模型。
用于二分类的线性模型
线性模型也可用于分类问题。有二分类公式:
1 y = w[0 ]*x[0 ] + w[1 ]*x[1 ] + ... + w[p]*x[p] + b > 0
上式为预测设置了阈值(0),当函数值小于0时,则贴上标签-1,否则为+1。
最常见的两种线性分类算法为Logistic回归和线性支持向量机(linear support vector machine,简称线性SVM),注意前者虽然叫做“回归”,其实是用于分类算法。
两者特点:
使用L2正则化。权衡系数记为C
,与惩罚系数alpha的效果==相反==。
前者在linear_model.LogisticRegression
中实现,后者在svm.LinearSVC
中实现。
两者在forge数据集上的决策边界可视化结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from sklearn.linear_model import LogisticRegressionfrom sklearn.svm import LinearSVCimport matplotlib.pyplot as pltX, y = mglearn.datasets.make_forge() fig, axes = plt.subplots(1 , 2 , figsize=(10 , 3 )) for model, ax in zip ([LinearSVC(),LogisticRegression()], axes): clf = model.fit(X,y) mglearn.plots.plot_2d_separator(clf, X, fill=False , eps=0.5 , ax=ax, alpha=.7 ) mglearn.discrete_scatter(X[:,0 ], X[:,1 ], y, ax=ax) ax.set_title("{}" .format (clf.__class__.__name__)) ax.set_xlabel("Feature 0" ) ax.set_ylabel("Feature 1" ) axes[0 ].legend() plt.show()
注意:两个模型中都有两个点的分类是错误的。
参数C对线性SVM的决策边界影响结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def plot_linear_svc_regularization (): X, y = make_blobs(centers=2 , random_state=4 , n_samples=30 ) fig, axes = plt.subplots(1 , 3 , figsize=(12 , 4 )) y[7 ] = 0 y[27 ] = 0 x_min, x_max = X[:, 0 ].min () - .5 , X[:, 0 ].max () + .5 y_min, y_max = X[:, 1 ].min () - .5 , X[:, 1 ].max () + .5 for ax, C in zip (axes, [1e-2 , 10 , 1e3 ]): discrete_scatter(X[:, 0 ], X[:, 1 ], y, ax=ax) svm = LinearSVC(C=C, tol=0.00001 , dual=False ).fit(X, y) w = svm.coef_[0 ] a = -w[0 ] / w[1 ] xx = np.linspace(6 , 13 ) yy = a * xx - (svm.intercept_[0 ]) / w[1 ] ax.plot(xx, yy, c='k' ) ax.set_xlim(x_min, x_max) ax.set_ylim(y_min, y_max) ax.set_xticks(()) ax.set_yticks(()) ax.set_title("C = %f" % C) axes[0 ].legend(loc="best" ) if __name__ == "__main__" : plot_linear_svc_regularization() plt.show()
Logistic回归在cancer数据集上的测试:
1 2 3 4 5 6 7 8 from sklearn.datasets import load_breast_cancerfrom sklearn.linear_model import LogisticRegressioncancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42 ) logreg = LogisticRegression(C=100 ).fit(X_train, y_train) print ("Traning set score: {:.3f}" .format (logreg.score(X_train, y_train)))print ("Test set score: {:.3f}" .format (logreg.score(X_test, y_test)))
用于多分类的线性模型
许多线性分类模型只适用于二分类问题,将其推广到多分类算法的一种常见方法是“一对其余”。对每个类别都学习一个二分类模型,将这个类别与其所有其他类别尽量区分开,这样就生成了与类别个数相同的二分类模型。在测试点上运行所有二分类器来进行预测,在对应类别上分数最高的分类器胜出,将这个类别标签返回作为预测结果。
以下是一个三分类的数据集:
1 2 3 4 5 6 7 8 from sklearn.datasets import make_blobsimport matplotlib.pyplot as pltX, y = make_blobs(random_state=42 ) mglearn.discrete_scatter(X[:,0 ],X[:,1 ],y) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.legend(["Class 0" , "Class 1" , "Class 2" ]) plt.show()
在这个数据集上训练一个线性SVC分类器
1 2 3 4 5 6 from sklearn.datasets import make_blobsfrom sklearn.svm import LinearSVCX, y = make_blobs(random_state=42 ) linear_svm = LinearSVC().fit(X, y) print ("Coeffient shape: " ,linear_svm.coef_.shape)print ("Intercept shape: " ,linear_svm.intercept_.shape)
coef_的形状为(3,2),说明每行包含三个之一的系数向量,每列包含2个特征的系数值。可视化结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from sklearn.datasets import make_blobsfrom sklearn.svm import LinearSVCimport matplotlib.pyplot as pltimport numpy as npX, y = make_blobs(random_state=42 ) linear_svm = LinearSVC().fit(X, y) mglearn.discrete_scatter(X[:,0 ],X[:,1 ],y) line = np.linspace(-15 ,15 ) for coef, intercept, color in zip (linear_svm.coef_, linear_svm.intercept_, ['b' ,'r' ,'g' ]): plt.plot(line, -(line * coef[0 ] + intercept) / coef[1 ], c=color) plt.ylim(-10 ,15 ) plt.xlim(-10 ,8 ) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.legend(["Class 0" , "Class 1" , "Class 2" ,"line class 0" , "line class 1" ,"line class 2" ],loc=(1.01 ,0.3 )) plt.show()
总结
线性模型的主要参数时正则化参数,或称为惩罚参数,权衡参数,在回归模型中为alpha,分类模型中为C。
补充:方法链
sklearn中的所有模型的fit
方法返回的都是self
,因此可以编写下面的代码:
1 2 logreg = LogisticRegression().fit(X_train, y_train)
这种方式被称为方法链
。
1 2 3 4 logreg = LogisticRegression() y_pred = logreg.fit(X_train, y_train).predict(X_test)
甚至可以在一行代码中实现模型初始化,拟合和预测,但不推荐,因为代码可读性差。此外,拟合后的模型也没有保存在任何变量中,形成了“浪费”。
1 y_pred = LogisticRegression().fit(X_train, y_train).predict(X_test)
2.3.4 朴素贝叶斯分类器
特点
与线性模型分类器相似,但训练速度更快,因此泛化能力相比较弱。
三类分类器
GaussianNB
:可应用于任意连续数据,高维数据,文本数据分类。
BernoulliNB
:它假定输入数据为二分类数据。
MultinomiaNB
:它假定输入数据为计数数据,例如统计单词出现次数,可用于文本数据分类。
2.3.5 决策树 决策树是广泛应用于分类和回归任务的模型。本质上,它从一层层的if/else问题中进行学习,并得出结论。
决策树的目标就是通过提出尽可能少
的if/else问题来得到正确答案。
上图,用机器语言来讲,就是为了区分四种动物,利用三个特征来构建一个模型。
构造决策树
数据通常并不是上例中的那样具有二元性(是或否)的形式,而是表示为连续特征。用于连续数据的测试形式为:==特征i的值是否大于a?==
对数据进行反复递归划分,直到划分后的每个区域(决策树的每个叶节点)只包含单一目标值(单一类别或单一回归值)。如果树中某个叶节点所包含数据点的目标值都相同,那么这个叶节点就是纯的。
决策树也可用于回归任务,预测的方法是:基于每个节点的测试对树进行遍历,最终找到新数据点所属的叶节点。这一数据点的输出就是此叶节点中所有训练点的平均目标值。
控制决策树的复杂度——防止过拟合
通常来说,构造决策树直到所有叶节点都是纯的叶节点,会导致模型非常复杂(对训练集数据的预测率将会是100%),泛化能力会很低。
防止过拟合的策略有两种:
预剪枝(pre-pruning):及时停止树的生长,可以限制树的最大深度,限制叶节点的最大数目,或者规定一个节点中数据点的最小数目来防止继续划分。
后剪枝(post-pruning):先构造树,然后删除或折叠信息量很少的节点。
sklearn的决策树在DecisionTreeRegressor
和DecisionTreeClassifier
类中实现,但只实现了预剪枝。
决策树在cancer数据集上的表现:
1 2 3 4 5 6 7 8 9 10 from sklearn.tree import DecisionTreeClassifierfrom sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitcancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42 ) tree = DecisionTreeClassifier(random_state=0 ) tree.fit(X_train, y_train) print ("Accuracy on training set: {:.3f}" .format (tree.score(X_train, y_train)))print ("Accuracy on test set: {:.3f}" .format (tree.score(X_test, y_test)))
可以看出,训练集准确率为100%,出现了过拟合。如果不限制决策树的深度,它的深度和复杂度会非常大,因此未剪枝的树容易出现过拟合
,对新数据的泛化能力不佳。
可以设置参数max_depth的值,来限制树的最大深度:
1 tree = DecisionTreeClassifier(max_depth=4 ,random_state=0 )
可以看出,训练集下降了,但是测试集的准确率上升,泛化能力得到了增强。
分析决策树——树的可视化
1 2 3 4 5 6 rom sklearn.tree import export_graphviz export_graphviz(tree,out_file="tree.dot" ,class_names=["magligant" ,"benign" ],feature_names=cancer.feature_names,impurity=False ,filled=True ) import graphvizwith open ("tree.dot" ) as f: dot_graph=f.read() graphviz.Source(dot_graph)
树的特征重要性——fearture importance
特征重要性:决策树为每个特征对树的决策的重要性进行排序,是一个0到1之间的数字,其中0表示根本没有用到,1表示完美预测目标值,特征重要性之和始终为1。
1 print ("Feature importance: \n {}" .format (tree.feature_importances_))
柱状图可视化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from sklearn.tree import DecisionTreeClassifierfrom sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitimport matplotlib.pyplot as pltimport numpy as npcancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42 ) tree = DecisionTreeClassifier(max_depth=4 ,random_state=0 ) tree.fit(X_train, y_train) def plot_feature_importances_cancer (model ): n_features = cancer.data.shape[1 ] plt.barh(range (n_features), model.feature_importances_, align='center' ) plt.yticks(np.arange(n_features), cancer.feature_names) plt.xlabel("Feature importance" ) plt.ylabel("Feature" ) plot_feature_importances_cancer(tree) plt.show()
可以看出,顶部划分用到的特征(worst radius)是最重要的特征,第一层划分已经将两个类别区分得很好。
特征重要性告诉我们worst radius特征最重要,但是并没有告诉我们半径大表示样本是良性还是恶性,在特征和类别之间没有这样简单的正比反比关系。
用于回归任务的决策树
注意:==基于树的回归模型不能外推,也不能在训练数据范围之外进行预测。==
示例:以下为ram历史价格的数据集:
1 2 3 4 5 6 import pandas as pdram_prices = pd.read_csv("data/ram_price.csv" ) plt.semilogy(ram_prices.date, ram_prices.price) plt.xlabel("Year" ) plt.ylabel("Price in $/Mbyte" ) plt.show()
我们将利用线性回归模型和决策树模型和2000年以前的历史价格来预测2000年以后的价格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import pandas as pdfrom sklearn.tree import DecisionTreeRegressorfrom sklearn.linear_model import LinearRegressionram_prices = pd.read_csv("data/ram_price.csv" ) data_train = ram_prices[ram_prices.date < 2000 ] data_test = ram_prices[ram_prices.date >= 2000 ] X_train = data_train.date[:,np.newaxis] y_train = np.log(data_train.price) tree = DecisionTreeRegressor().fit(X_train, y_train) linear_reg = LinearRegression().fit(X_train, y_train) X_all = ram_prices.date[:,np.newaxis] pred_tree = tree.predict(X_all) pred_lr = linear_reg.predict(X_all) price_tree = np.exp(pred_tree) price_lr = np.exp(pred_lr) plt.semilogy(data_train.date, data_train.price, label="Training data" ) plt.semilogy(data_test.date, data_test.price, label="Test data" ) plt.semilogy(ram_prices.date, price_tree, label="Tree prediction" ) plt.semilogy(ram_prices.date, price_lr, label="linear prediction" ) plt.legend() plt.show()
可以看出,两个模型的差异非常明显。对于线性模型,它丢失了训练集的某些信息,但是泛化能力很强,对2000年以后的价格预测较好。对于决策树模型,在训练集的预测上,与原始数据完全一致(图中蓝线和绿线重合在了一起),但是一旦超出训练数据的范围,即对2000年以后的价格预测上,则只能持续预测最后一个已知数据点。树不能在训练数据的范围之外生成新的响应,是所有基于树的回归模型的缺点。
决策树的优缺点
优点:得到的模型很容易可视化;算法完全不受数据缩放的影响,不需要数据预处理,例如归一化或者标准化。
缺点:即使做了预剪枝,也经常会过拟合,泛化能力很差。实践中常常使用下一节介绍的决策树集成来代替单一的决策树模型。
重要参数:预剪枝参数,例如max_depth
,max_leaf_nodes
,max_samples_leaf
等。
2.3.6 决策树集成 集成(ensemble)是合并多个机器学习模型来构建更强大模型的方法,已证明有两种集成模型对大量分类和回归的数据集都是有效的,二者都以决策树为基础,分别是随机森林和梯度提升决策树。
随机森林——random forest
思想:每棵树的预测都可能相对较好,但可能对部分数据存在过拟合,所以可以构造许多棵树,对这些树的结果取平均值来降低过拟合。
在sklearn中,随机森林在RandomForestRegressor
和RandomForestClassifier
中实现,首先要确定用于构造的树的个数,取决于参数n_estimators
通过选择用于构造树的数据点:首先要对数据进行自助采样(bootstrap sample),从n_samples个数据点中有放回地重复随机抽取样本。共抽取n_samples次,这样会创建一个与原数据集想用大小的数据集,但是有些数据点(大约三分之一)会被丢失,另一些则会重复。例如:创建数据集[a,b,c,d]的自助采样,一种可能的是采样是[a,a,b,b],或者[d,c,,d,a]等等。
通过选择每次划分测试的特征:在每个决策节点处,算法随机选择特征的一个子集(特征子集),并对其中一个特征寻找最佳测试。选择特征的个数(特征子集的大小)由参数max_features
决定。
由以上两种方法,随机森林中构造每棵树的数据集都是略有不同的,由于每个节点的特征选择,每棵树中的每次划分都是基于特征的不同子集,这两种随机化方法保证随机森林中的每棵树都是不同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from sklearn.datasets import make_moonsfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.model_selection import train_test_splitimport matplotlib.pyplot as pltimport mglearnX, y = make_moons(n_samples=100 , noise=0.25 , random_state=3 ) X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42 ) forest = RandomForestClassifier(n_estimators=5 , random_state=2 ) forest.fit(X_train, y_train) fig, axes = plt.subplots(2 ,3 ,figsize=(20 ,10 )) for i,(ax,tree) in enumerate (zip (axes.ravel(), forest.estimators_)): ax.set_title("Tree {}" .format (i)) mglearn.plots.plot_tree_partition(X_train, y_train, tree, ax=ax) mglearn.plots.plot_2d_separator(forest, X_train, fill=True , ax=axes[-1 ,-1 ], alpha=.4 ) axes[-1 ,-1 ].set_title("Random Forest" ) mglearn.discrete_scatter(X_train[:,0 ], X_train[:,1 ], y_train) plt.show()
可以看出,每棵树都犯了一些错误,因为这里画出的一些训练点实际上并没有包含在这些树的训练集中,原因在于自助采样。
在cancer数据集上的表现(n_estimators=100):
1 2 3 4 5 6 cancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0 ) forest = RandomForestClassifier(n_estimators=100 , random_state=2 ) forest.fit(X_train, y_train) print ("Accuracy on training set: {:.3f}" .format (forest.score(X_train, y_train)))print ("Accuracy on test set: {:.3f}" .format (forest.score(X_test, y_test)))
与决策树相似,随机森林也给出了特征重要性,一般来说,后者的可靠性要比单棵树的要好。
此处,随机森林认为worst perimeter是信息量最大的特征。
优缺点:对于维度非常高的稀疏数据(例如文本数据),随机森林表现不是很好,这种情况通常采用线性模型处理。优点是鲁棒性很好,不需要反复调节参数,也不需要数据预处理就可以给出很好的结果。
重要参数:
n_estimators
:总是越大越好,鲁棒性越强,不过收益是递减的,对计算机硬件的要求就越高,训练时间也越长。
max_features
:决定每棵树的随机性大小,最好使用默认值,为sqrt(n_features)
;对于回归,则为n_features
;有时调整该参数可能提高性能。
预剪枝参数:预剪枝参数可能影响随机森林的性能。
梯度提升决策树(梯度提升机)——gradient boosted decision tree
梯度提升采用连续的方式构造树,每棵树都视图纠正前一棵树的错误。默认情况下,梯度提升回归树没有随机化,而是用到了预强剪枝。梯度提升树通常采用深度很小(1到5之间)的树。
1 2 3 4 5 6 7 from sklearn.ensemble import GradientBoostingClassifiercancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0 ) gbrt = GradientBoostingClassifier(random_state=0 ) gbrt.fit(X_train, y_train) print ("Accuracy on training set: {:.3f}" .format (gbrt.score(X_train, y_train)))print ("Accuracy on test set: {:.3f}" .format (gbrt.score(X_test, y_test)))
出现了过拟合,可以限制最大深度,或者降低学习率:
1 gbrt = GradientBoostingClassifier(random_state=0 , max_depth=1 )
1 gbrt = GradientBoostingClassifier(random_state=0 , learning_rate=0.01 )
优点:鲁棒性强;监督学习中最强大最常用的模型之一
缺点:需要仔细调参,训练时间也可能比较长
2.3.7 核支持向量机 核支持向量机:kernelized support vector machine
可用于分类和回归,分类在SVC中实现,回归在SVR中实现。
线性模型和非线性特征
线性模型在低维空间中可能非常受限,因为线和平面的灵活性有限。
对二分类blobs数据集:
1 2 3 4 5 6 X, y = make_blobs(centers=4 , random_state=8 ) y = y % 2 mglearn.discrete_scatter(X[:,0 ],X[:,1 ],y) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
这样的类别并不是线性可分的,如果采用一条直线的线性模型来划分数据点,将无法给出较好的结果:
1 2 3 4 5 6 7 8 9 10 from sklearn.svm import LinearSVCX, y = make_blobs(centers=4 , random_state=8 ) y = y % 2 linear_svm = LinearSVC().fit(X, y) mglearn.plots.plot_2d_separator(linear_svm, X) mglearn.discrete_scatter(X[:,0 ],X[:,1 ],y) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
现在对输入特征进行扩展,例如添加第二个特征的平方作为一个新特征,将每个数据点表示为三维点。
2.3.8 神经网络(深度学习) 第三章 无监督学习 无监督学习包括没有已知输出,没有老师指导学习算法的各种机器学习,在无监督学习中,学习算法只有输入数据,并需要从这些数据中提取知识。
3.1 无监督学习的类型 主要研究两种类型的无监督学习:
常见应用:降维
:接受包括许多特征的数据的高维表示,并找到表示该数据的一种新方法,用较少的特征就可以概括其重要特性;聚类
:将数据划分成不同的组,每组包括相似的物项。作为监督算法的预处理步骤
3.2 无监督学习的挑战 无监督学习算法常用于不包含任何标签信息的数据,评估无监督算法结果的唯一方法就是人工检查。
3.3 预处理和缩放 通常来说,这是对数据的一种简单的按特征
的缩放和移动。
3.3.1 不同类型的预处理
左图显示的是一个有两个特征的二分类数据集,第一个特征x轴位于10-15之间,第二个特征y轴大约位于1-9之间。
StandardScaler确保每个特征的平均值为0,方差为1,使所有特征都位于同一量级。
RobustScaler与上者类似,但是利用中位数和四分位数。
MinMaxScaler移动数据,使所有特征都刚好位于0-1之间。
Normalizer对每个数据点进行缩放,使得特征向量的欧式长度为1,换言之,它将一个数据点投射到半径为1的圆上(或者球面),这意味着每个数据点的缩放比例都不同。
3.3.2 应用数据变换 将SVM应用在cancer数据集上,并使用MinMaxScaler来预处理数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import MinMaxScalercancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=1 ) scaler = MinMaxScaler() scaler.fit(X_train) X_train_scaled = scaler.transform(X_train) print ("transformed shape: {}" .format (X_train_scaled.shape))print ("per-feature minimum before scaling:\n {}" .format (X_train.min (axis=0 )))print ("per-feature maximum before scaling:\n {}" .format (X_train.max (axis=0 )))print ("per-feature mininum after scaling:\n {}" .format (X_train_scaled.min (axis=0 )))print ("per-feature mininum after scaling:\n {}" .format (X_train_scaled.max (axis=0 )))
可以看出没有改变数据集的形状,但是所有特征的值都位于0-1之间。
然后对测试集进行缩放:
1 2 3 4 X_test_scaled = scaler.transform(X_test) print ("per-feature mininum after scaling:\n {}" .format (X_test_scaled.min (axis=0 )))print ("per-feature mininum after scaling:\n {}" .format (X_test_scaled.max (axis=0 )))
可以看出对测试集进行缩放后其最大最小值并不是0和1,这是因为,所有的缩放器总是对训练集和测试集进行相同的变换,也就是说,transform方法总是减去训练集的最小值 ,然后除以训练集的范围,这两个值可能与测试集中的不同。
3.3.3 对训练数据和测试数据进行相同的缩放 略
快捷方式与高效的替代方法
通常来说,你需要在某个数据集上拟合(fit)一个模型,然后再将其transform。所有具有transform方法的模型都具有fit_transform方法:
1 2 3 4 5 6 from sklearn.preprocessing import StandardScalerscaler = StandardScaler() X_scaled = scaler.fit(X).transform(X) X_scaled = scaler.fit_transform(X)
3.3.4 预处理对监督学习的作用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import MinMaxScalerfrom sklearn.svm import SVCcancer = load_breast_cancer() X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0 ) svm = SVC(C=100 ) svm.fit(X_train, y_train) print ("Test set accuracy: {:.2f}" .format (svm.score(X_test, y_test)))scaler = MinMaxScaler() scaler.fit(X_train) X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) svm.fit(X_train_scaled, y_train) print ("Scaled test set accuracy: {:.2f}" .format (svm.score(X_test_scaled, y_test)))
可见数据经过预处理后,准确率提高了。
3.4 降维,特征提取与流形学习 利用无监督学习进行数据变换可能有多种目的,最常见的目的就是可视化,压缩数据,以及寻找信息量更大的数据表示以用于进一步的处理。常见算法有主成分分析,非负矩阵分解和t-SNE。
3.4.1主成分分析PCA
简介
PCA是一种基本的数据降维技术。 PCA是一种著名的数据降维算法,它应用的条件是数据/特征之间具有明显的线性相关性,它的两个主要的指导思想是抓主要矛盾和方差即信息,它的基本应用是数据降维 ,以此为基础还有数据可视化 、数据压缩存储 、异常检测 、特征匹配与距离计算等。 从数学上理解,它是一种矩阵分解 算法;从物理意义上理解,它是线性空间上的线性变换 。
主成分分析:principal component analysis。是一种旋转数据集的方法,旋转后的特征在统计上不相关。在做完这种旋转后,通常是根据新特征对解释数据的重要性来选择它的一个子集。
左上图显示的是原始数据点。算法首先找到方差最大的方向,将其标记为“成分1”(component 1)。这是数据中包含信息最多的方向(或向量),沿着这个方向的特征之间最为相关 。然后算法找到与第一个方向正交且包含信息最多的方向,在二维空间中只有一个,但在高维空间中则有许多个。这些方向被称为主成分 ,一般来说,主成分个数与原始特征相同。
右上图将其左上图旋转得来,使得成分1与x轴平行,成分2与y轴平行。在旋转之前,从数据中减去平均值,使得变换后的数据以0为中心。
可以通过仅保留一部分主成分来使用pca进行降维 ,对于左下图,保留成分1,使得数据变成1维数据集,但要注意,并不是保留了原始特征中的某一个,而是保留了主成分的某一方向。
右下图由左下图反向旋转并重新加回平均值得来,这种变换有时用于去除数据中的噪声影响,或将主成分中保留的那部分信息可视化。
原理概述
见打印资料
将PCA应用于cancer数据集并可视化
PCA最常见的应用之一是将高维数据集可视化 。
在应用PCA之前,利用StandardScaler缩放数据。
1 2 3 4 5 6 from sklearn.datasets import load_breast_cancerfrom sklearn.preprocessing import StandardScalercancer = load_breast_cancer() scaler = StandardScaler() X_scaled = scaler.fit_transform(cancer.data)
首先将PCA对象实例化,然后调用fit找到主成分,然后调用transform来旋转并降维,在这个过程中默认保留所有主成分,可以指定保留的个数。
1 2 3 4 5 6 7 8 9 10 from sklearn.decomposition import PCApca = PCA(n_components=2 ) pca.fit(X_scaled) X_pca = pca.transform(X_scaled) print ("Original shape: {}" .format (str (X_scaled.shape)))print ("Reduced shape: {}" .format (str (X_pca.shape)))
然后利用前两个主成分绘制cancer数据集的二维散点图:
1 2 3 4 5 6 7 plt.figure(figsize=(8 ,8 )) mglearn.discrete_scatter(X_pca[:,0 ], X_pca[:,1 ],cancer.target) plt.legend(cancer.target_names,loc="best" ) plt.gca().set_aspect("equal" ) plt.xlabel("First principal component" ) plt.ylabel("Second principal component" ) plt.show()
注意,pca是无监督算法,在寻找旋转方向时没有用到任何类别信息,但是可以看出两个类别被很好地区分开来了。
pca的缺点是解释性不好,主成分对应于原始数据中的方向,是原始特征的组合 ,这些组合往往非常复杂。
在拟合过程中,主成分被保存在PCA对象的components_属性中:
1 2 print ("PCA component shape: {}" .format (pca.components_.shape))print ("PCA components:\n{}" .format (pca.components_))
components_中的每一行对应于一个主成分,他们按照重要性排序(第一主成分排在第一位,以此类推),列对应于PCA的原始特征属性,共30个,例如mean radius,mean texture等。
用热图 进行可视化:
1 2 3 4 5 6 7 plt.matshow(pca.components_, cmap='viridis' ) plt.yticks([0 ,1 ],["First component" ,"Second component" ]) plt.colorbar() plt.xticks(range (len (cancer.feature_names)), cancer.feature_names, rotation=60 , ha='left' ) plt.xlabel("Feature" ) plt.ylabel("Principal components" ) plt.show()
这些所有特征的混合使得解释坐标轴含义变得十分困难。
PCA用于人脸识别
PCA的另一个应用是特征提取 。
Wild数据集一共有3023张图像,每张大小为87*65像素,分别属于62个不同的人。但这个数据集有数据偏斜,例如乔治布什的有近530张图像,为降低数据偏斜,我们对每个人最多只取50张图像。
1 2 3 4 5 6 7 8 9 10 from sklearn.datasets import fetch_lfw_peopleimport matplotlib.pyplot as pltimport numpy as nppeople = fetch_lfw_people(min_faces_per_person=20 , resize=0.7 ) image_shape = people.images[0 ].shape fix, axes = plt.subplots(2 , 5 , figsize=(15 ,8 ), subplot_kw={'xticks' : (), 'yticks' : ()}) for target, image, ax in zip (people.target, people.images, axes.ravel()): ax.imshow(image) ax.set_title(people.target_names[target]) plt.show()
1 2 3 4 5 6 7 8 9 mask = np.zeros(people.target.shape, dtype=np.bool_) for target in np.unique(people.target): mask[np.where(people.target == target)[0 ][:50 ]] = 1 X_people = people.data[mask] y_people = people.target[mask] X_people = X_people / 255.
利用单一近邻算法对人脸进行预测:
1 2 3 4 5 6 7 from sklearn.neighbors import KNeighborsClassifierfrom sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X_people, y_people, stratify=y_people,random_state=0 ) knn = KNeighborsClassifier(n_neighbors=1 ) knn.fit(X_train, y_train) print ("Test set score of 1-nn: {:.2f}" .format (knn.score(X_test, y_test)))
利用PCA进行数据预处理:这里可以使用PCA的白化 选项
上图是经过PCA白化(数据预处理)后的图像。
现在对训练数据拟合PCA对象,并提取前100个主成分,然后对训练数据和测试数据进行变换:
1 2 3 4 5 6 7 from sklearn.decomposition import PCApca = PCA(n_components=100 , whiten=True ,random_state=0 ).fit(X_train) X_train_pca = pca.transform(X_train) X_test_pca = pca.transform(X_test) print ("X_train_pca.shape: {}" .format (X_train_pca.shape))
可以看出新数据集有100个新特征,即前100个主成分。现在利用k近邻算法进行分类:
1 2 3 knn = KNeighborsClassifier(n_neighbors=1 ) knn.fit(X_train_pca,y_train) print ("Test set score of 1-nn: {:.2f}" .format (knn.score(X_test_pca, y_test)))
可见准确率得到了提升。
现在查看前几个主成分:
1 2 3 4 5 fix, axes = plt.subplots(3 , 5 , figsize=(15 ,12 ), subplot_kw={'xticks' : (), 'yticks' : ()}) for i, (component, ax) in enumerate (zip (pca.components_, axes.ravel())): ax.imshow(component.reshape(image_shape), cmap='viridis' ) ax.set_title("{}. component" .format ((i+1 ))) plt.show()
理解主成分很难,但是可以猜测,例如左上图似乎主要编码的是人脸与背景的对比,第二主成分编码的可能是脸左半部分和右半部分的明暗程度差异。
3.4.2 非负矩阵分解NMF 非负矩阵分解:non-negative matrix factorization。目的在于提取有用特征
,这种方法只能应用于每个特征都是非负的数据,于PCA相比,NMF得到的分量更容易解释。
NMF原理简介
对于任意给定的一个非负矩阵V,其能够寻找到一个非负矩阵W和一个非负矩阵H,满足条件V=WH,从而将一个非负的矩阵分解为左右两个非负矩阵的乘积。*其中,V矩阵中每一列代表一个观测(observation),每一行代表一个特征(feature);W矩阵称为基矩阵,H矩阵称为系数矩阵或权重矩阵。这时用系数矩阵H代替原始矩阵,就可以实现对原始矩阵进行降维,得到数据特征的降维矩阵,从而减少存储空间。 过程如下图所示:
其物理意义为每个基向量W(如眼睛、鼻子等等)乘以相应权重H最终组合而成V(脸)。虽然NMF是一个很厉害的算法,但其实质是加权和,我们可以在原理上等效为基本的线性方程。
sklearn中封装的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from sklearn.decomposition import NMFfrom sklearn.datasets import load_irisX, _ = load_iris(True ) nmf = NMF(n_components=2 , init=None , solver='cd' , beta_loss='frobenius' , tol=1e-4 , max_iter=200 , random_state=None , alpha=0. , l1_ratio=0. , verbose=0 , shuffle=False ) print ('params:' , nmf.get_params()) nmf.fit(X) W = nmf.fit_transform(X) W = nmf.transform(X) nmf.inverse_transform(W) H = nmf.components_ print ('reconstruction_err_' , nmf.reconstruction_err_) print ('n_iter_' , nmf.n_iter_)
将NMF应用于模拟数据
以上的图给出了NMF在二维模拟数据上的结果。对于两个分量的NMF,显然所有的数据点都可以写成这两个分量的正数组合,如果有足够的分量能够完美地重建数据(分量个数等于特征个数),那么算法会指向数据极值的方向。
NMF的主要参数是我们想要提取的分量个数。
将NMF应用于信号源区分
假设有一个信号由三个不同的信号源合成的。
1 2 3 4 5 6 7 8 9 import mglearnimport matplotlib.pyplot as pltS = mglearn.datasets.make_signals() plt.figure(figsize=(6 ,1 )) plt.plot(S, '-' ) plt.xlabel("Time" ) plt.ylabel("Signal" ) plt.show()
我们想要将混合信号分解为原始分量,假设有100种方法来观测混合信号,每种方法都为我们提供了一系列的观测结果。
1 2 3 4 A = np.random.RandomState(0 ).uniform(size=(100 ,3 )) X = np.dot(S, A.T) print ("Shape of measurements: {}" .format (X.shape))
我们可以用NMF来还原这三个信号:
1 2 3 nmf = NMF(n_components=3 , random_state=42 ) S_ = nmf.fit_transform(X) print ("Recovered signal shape: {}" .format (S_.shape))
添加了PCA方法用于比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 A = np.random.RandomState(0 ).uniform(size=(100 ,3 )) X = np.dot(S, A.T) nmf = NMF(n_components=3 , random_state=42 ) S_ = nmf.fit_transform(X) pca = PCA(n_components=3 ) H = pca.fit_transform(X) models = [X, S, S_, H] names = ['Observations (first three measurements)' , 'True sources' , 'NMF recovered signals' , 'PCA recovered signals' ] fig, axes = plt.subplots(4 , figsize=(8 ,4 ), gridspec_kw={'hspace' : .5 }, subplot_kw={'xticks' :(), 'yticks' :()}) for model, name, ax in zip (models, names, axes): ax.set_title(name) ax.plot(model[:, :3 ],'-' ) plt.show()
可以看出NMF在发现原始信号源时得到了不错的结果,而PCA则失败了,仅使用第一个成分来解释数据中的大部分变化。
3.4.3 用t-SNE进行流形学习
简介
有一类用于可视化的算法叫做流形学习算法,其中特别有用的一个是t-SNE算法。
t-SNE全称为t-distributed Stochastic Neighbor Embedding,翻译为t-随机邻近嵌入,它是一种嵌入模型,能够将高维空间中的数据映射到低维空间中,并保留数据集的局部特性,该算法在论文中非常常见,主要用于高维数据的降维和可视化 。
t-SNE可以算是目前效果最好的数据降维和可视化方法之一,当我们想对高维数据集进行分类,但又不清楚这个数据集有没有很好的可分性(同类之间间隔小、异类之间间隔大)时,可以通过t-SNE将数据投影到2维或3维空间中观察一下:如果在低维空间中具有可分性,则数据是可分的;如果在低维空间中不可分,则可能是因为数据集本身不可分,或者数据集中的数据不适合投影到低维空间。
t-SNE将数据点之间的相似度转化为条件概率,原始空间中数据点的相似度由高斯联合分布表示,嵌入空间中数据点的相似度由学生t分布表示。通过原始空间和嵌入空间的联合概率分布的KL散度(用于评估两个分布的相似度的指标,经常用于评估机器学习模型的好坏)来评估嵌入效果的好坏,即将有关KL散度的函数作为损失函数(loss function),通过梯度下降算法最小化损失函数,最终获得收敛结果。要注意t-SNE的缺点很明显:占用内存较多、运行时间长。
此外 :流形学习算法主要用于可视化,对t-SNE算法,不允许变换新数据,这意味着不能用于测试集,只能变换用于训练的数据。
应用1——降维
输入4个5维的数据,通过t-SNE将其降维成2维的数据,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 import numpy as npfrom sklearn.manifold import TSNEx = np.array([[0 , 0 , 0 , 1 , 2 ], [0 , 1 , 1 , 3 , 5 ], [1 , 0 , 1 , 7 , 2 ], [1 , 1 , 1 , 10 , 22 ]]) ts = TSNE(n_components=2 ) ts.fit_transform(x) print (ts.embedding_)
应用2——S型曲线的降维与可视化
S型曲线中的数据是高维的数据,不同的颜色表示不同的数据点。当我们通过t-SNE将数据嵌入到2维空间中后,可以看到数据点之间的类别信息被完整地保留了下来 。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import matplotlib.pyplot as pltfrom sklearn import manifold, datasetsx, color = datasets._samples_generator.make_s_curve(n_samples=1000 , random_state=0 ) n_neighbors = 10 n_components = 2 fig = plt.figure(figsize=(8 , 8 )) plt.suptitle("Dimensionality Reduction and Visualization of S-Curve Data " , fontsize=14 ) ax = fig.add_subplot(211 , projection='3d' ) ax.scatter(x[:, 0 ], x[:, 1 ], x[:, 2 ], c=color, cmap=plt.cm.Spectral) ax.set_title('Original S-Curve' , fontsize=14 ) ax.view_init(4 , -72 ) ts = manifold.TSNE(n_components=n_components, init='pca' , random_state=0 ) y = ts.fit_transform(x) ax1 = fig.add_subplot(2 , 1 , 2 ) plt.scatter(y[:, 0 ], y[:, 1 ], c=color, cmap=plt.cm.Spectral) ax1.set_title('t-SNE Curve' , fontsize=14 ) plt.show()
应用3——手写数字数据集的降维与可视化
手写数字数据集是一个经典的图片分类数据集,数据集中包含0-9这10个数字的灰度图片,每张图片以8*8共64个像素点表示。
首先利用PCA降维到二维可视化。对前两个主成分作图,并按类别对数据点着色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from logging import disableimport matplotlib.pyplot as pltfrom sklearn.decomposition import PCAfrom sklearn.datasets import load_digitsdigits = load_digits() pca = PCA(n_components=2 ) pca.fit(digits.data) digits_pca = pca.transform(digits.data) colors = ["#476A2A" ,"#7851B8" ,"#BD3430" ,"#4A2D4E" ,"#875525" ,"#A83683" ,"#4E655E" ,"#853541" ,"#3A3120" ,"#535D8E" ] plt.figure(figsize=(10 ,10 )) plt.xlim(digits_pca[:,0 ].min (), digits_pca[:,0 ].max ()) plt.ylim(digits_pca[:,1 ].min (), digits_pca[:,1 ].max ()) for i in range (len (digits.data)): plt.text(digits_pca[i,0 ], digits_pca[i,1 ], str (digits.target[i]), color=colors[digits.target[i]], fontdict={'weight' : 'bold' , 'size' :9 }) plt.xlabel("First principal component" ) plt.ylabel("Second principal component" ) plt.show()
可以看出各个类别的界限不太好。现在利用t-sne算法进行降维并可视化,由于其算法不支持变换新数据,即没有transform方法,但可以调用fit_transform方法,其会构建模型并立即返回变换后的数据。
1 2 3 4 5 6 7 8 9 10 11 from sklearn.manifold import TSNEtsne = TSNE(random_state=42 ) digits_tsne = tsne.fit_transform(digits.data) plt.figure(figsize=(10 ,10 )) plt.xlim(digits_tsne[:,0 ].min (), digits_tsne[:,0 ].max () + 1 ) plt.ylim(digits_tsne[:,1 ].min (), digits_tsne[:,1 ].max () + 1 ) for i in range (len (digits.data)): plt.text(digits_tsne[i,0 ], digits_tsne[i,1 ], str (digits.target[i]), color=colors[digits.target[i]], fontdict={'weight' : 'bold' , 'size' :9 }) plt.xlabel("t-SNE feature 0" ) plt.ylabel("t-SNE feature 1" ) plt.show()
所有类别都被明确分开,要记住 ,这种方法并不知道类别标签,它完全是无监督的,但它能够找到数据的一种二维表示,仅根据原始空间中数据点之间的靠近程度就能够将各个类别明确分开。
3.5 聚类 聚类(clustering)是将数据集划分成组的任务,这些组称为簇 。
分类和聚类
分类: 分类其实是从特定的数据中挖掘模式,作出判断的过程。比如Gmail邮箱里有垃圾邮件分类器,一开始的时候可能什么都不过滤,在日常使用过程中,我人工对于每一封邮件点选“垃圾”或“不是垃圾”,过一段时间,Gmail就体现出一定的智能,能够自动过滤掉一些垃圾邮件了。这是因为在点选的过程中,其实是给每一条邮件打了一个“标签”,这个标签只有两个值,要么是“垃圾”,要么“不是垃圾”,Gmail就会不断研究哪些特点的邮件是垃圾,哪些特点的不是垃圾,形成一些判别的模式,这样当一封信的邮件到来,就可以自动把邮件分到“垃圾”和“不是垃圾”这两个我们人工设定的分类的其中一个。
聚类: 聚类的目的也是把数据分类,但是事先我是不知道如何去分的,完全是算法自己来判断各条数据之间的相似性,相似的就放在一起。在聚类的结论出来之前,我完全不知道每一类有什么特点,一定要根据聚类的结果通过人的经验来分析,看看聚成的这一类大概有什么特点。
聚类和分类最大的不同在于:分类的目标是事先已知的,而聚类则不一样,聚类事先不知道目标变量是什么,类别没有像分类那样被预先定义出来。
3.5.1 k均值聚类 聚类算法有很多种(几十种),K-Means是聚类算法中的最常用的一种,算法最大的特点是简单,好理解,运算速度快,但是只能应用于连续型 的数据,并且一定要在聚类前需要手工指定要分成几类。
算法步骤
下面,我们描述一下K-means算法的过程,为了尽量不用数学符号,所以描述的不是很严谨,大概就是这个意思,“物以类聚、人以群分”:
首先输入k的值,即我们希望将数据集经过聚类得到k个分组。
从数据集中随机选择k个数据点作为初始大哥(质心,Centroid)
对集合中每一个小弟,计算与每一个大哥的距离(距离的含义后面会讲),离哪个大哥距离近,就跟定哪个大哥。
这时每一个大哥手下都聚集了一票小弟,这时候召开人民代表大会,每一群选出新的大哥(其实是通过算法选出新的质心)。
如果新大哥和老大哥之间的距离小于某一个设置的阈值(表示重新计算的质心的位置变化不大,趋于稳定,或者说收敛),可以认为我们进行的聚类已经达到期望的结果,算法终止。
如果新大哥和老大哥距离变化很大,需要迭代3~5步骤。
上图中,三角形表示簇中心,圆形表示数据点。我们指定了要寻找三个簇。下图为算法的决策边界:
算法的简单实现
下面给出它的Python实现,其中中心点的选取是手动选择的。在代码中随机产生了一个样本,用于测试K-means算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 import numpy as npimport matplotlib.pyplot as pltimport random def get_distance (p1, p2 ): diff = [x-y for x, y in zip (p1, p2)] distance = np.sqrt(sum (map (lambda x: x**2 , diff))) return distance def calc_center_point (cluster ): N = len (cluster) m = np.matrix(cluster).transpose().tolist() center_point = [sum (x)/N for x in m] return center_point def check_center_diff (center, new_center ): n = len (center) for c, nc in zip (center, new_center): if c != nc: return False return True def K_means (points, center_points ): N = len (points) n = len (points[0 ]) k = len (center_points) tot = 0 while True : temp_center_points = [] clusters = [] for c in range (0 , k): clusters.append([]) for i, data in enumerate (points): distances = [] for center_point in center_points: distances.append(get_distance(data, center_point)) index = distances.index(min (distances)) clusters[index].append(data) tot += 1 print (tot, '次迭代 ' , clusters) k = len (clusters) colors = ['r.' , 'g.' , 'b.' , 'k.' , 'y.' ] for i, cluster in enumerate (clusters): data = np.array(cluster) data_x = [x[0 ] for x in data] data_y = [x[1 ] for x in data] plt.subplot(2 , 3 , tot) plt.plot(data_x, data_y, colors[i]) plt.axis([0 , 1000 , 0 , 1000 ]) for cluster in clusters: temp_center_points.append(calc_center_point(cluster)) for j in range (0 , k): if len (clusters[j]) == 0 : temp_center_points[j] = center_points[j] for c, nc in zip (center_points, temp_center_points): if not check_center_diff(c, nc): center_points = temp_center_points[:] break else : break plt.show() return clusters def get_test_data (): N = 1000 area_1 = [0 , N / 4 , N / 4 , N / 2 ] area_2 = [N / 2 , 3 * N / 4 , 0 , N / 4 ] area_3 = [N / 4 , N / 2 , N / 2 , 3 * N / 4 ] area_4 = [3 * N / 4 , N, 3 * N / 4 , N] area_5 = [3 * N / 4 , N, N / 4 , N / 2 ] areas = [area_1, area_2, area_3, area_4, area_5] k = len (areas) points = [] for area in areas: rnd_num_of_points = random.randint(50 , 200 ) for r in range (0 , rnd_num_of_points): rnd_add = random.randint(0 , 100 ) rnd_x = random.randint(area[0 ] + rnd_add, area[1 ] - rnd_add) rnd_y = random.randint(area[2 ], area[3 ] - rnd_add) points.append([rnd_x, rnd_y]) center_points = [[0 , 250 ], [500 , 500 ], [500 , 250 ], [500 , 250 ], [500 , 750 ]] return points, center_points if __name__ == '__main__' : points, center_points = get_test_data() clusters = K_means(points, center_points) print ('#######最终结果##########' ) for i, cluster in enumerate (clusters): print ('cluster ' , i, ' ' , cluster)
结果:
sklearn中的算法使用
1 2 3 4 5 6 7 8 from sklearn.datasets import make_blobsfrom sklearn.cluster import KMeansX, y = make_blobs() kmeans = KMeans(n_clusters=3 ) kmeans.fit(X)
算法会为X中的每个数据点分配一个簇标签
1 print ("Cluster memberships:\n{}" .format (kmeans.labels_))
调用predict方法会为新数据点分配簇标签,返回同上的结果。
1 print (kmeans.predict(X))
可以看出,每个数据点都有标签,但这些标签本身没有任何含义 ,算法唯一 给出的信息就是同一个标签下的数据点非常相似 而已。
当然我们也可以指定其他的簇的数量。
k均值聚类算法失败的案例
即使知道给定数据集中簇的正确个数,k均值可能也不是总能找到他们。k均值只能找到相对简单的形状,此外他还假设所有的簇在某种程度上具有相同的“直径”,所以他总是将簇之间的边界刚好画在簇的中心的中间位置。这会产生令人惊讶的结果:
1 2 3 4 5 6 7 X_varied, y_varied = make_blobs(n_samples=200 , cluster_std=[1.0 ,2.5 ,0.5 ],random_state=170 ) y_pred = KMeans(n_clusters=3 , random_state=0 ).fit_predict(X_varied) mglearn.discrete_scatter(X_varied[:,0 ], X_varied[:,1 ], y_pred) plt.legend(["cluster 0" , "cluster 1" , "cluster 2" ], loc='best' ) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
可以看出,“应该在”中间零散的数据点组成的簇中的一些数据点,分布在了左下角和右上角密集簇中。
又如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 X, y = make_blobs(n_samples=600 , random_state=170 ) rng = np.random.RandomState(74 ) transformation = rng.normal(size=(2 ,2 )) X = np.dot(X, transformation) kmeans = KMeans(n_clusters=3 ) kmeans.fit(X) y_pred = kmeans.predict(X) plt.scatter(X[:,0 ], X[:,1 ], c=y_pred, cmap=mglearn.cm3) plt.scatter(kmeans.cluster_centers_[:,0 ], kmeans.cluster_centers_[:,1 ], marker='^' , c=[0 ,1 ,2 ], s=100 , linewidths=2 , cmap=mglearn.cm3) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
可以看出,数据中包含明确分开的三部分,但这三部分被沿着对角线方向拉长,由于k均值仅考虑到最近簇中心的距离,所以他无法处理这种类型的数据。
如果簇的形状更复杂,那么k均值的表现会更差。
矢量量化,或者将k均值看作分解
k均值尝试利用簇中心来表示每个数据点,可以将其看成仅用一个分量来表示每个数据点,该分量由簇中心给出,这种观点将k均值看作是一种分解方法,其中每个点用单一分量来表示,这种观点称为矢量量化 。
对于k均值,重建就是在训练集中找到的最近的簇中心。
总结
k均值是非常流行的聚类算法,缺点在于它依赖于随机初始化,此外它对簇形状的假设的约束性较强,而且还要求指定所要寻找的簇的个数(现实应用中可能并不知道这个数字)。
3.5.2 凝聚聚类
简介
层次聚类:是一种很直观的算法。顾名思义就是要一层一层地进行聚类,可以从下而上地把小的cluster合并聚集,也可以从上而下地将大的cluster进行分割。自下而上地进行聚类称为凝聚式层次聚类 ,自上而下地进行聚类称为分裂式层次聚类 。似乎一般用得比较多的是从下而上地聚集。
凝聚式:从点作为个体簇开始,每一步合并两个最接近的簇。
分裂式:从包含所有点的某个簇开始,每一步分裂一个簇,直到仅剩下单点簇。
凝聚聚类(agglomerative clustering)指的是许多基于相同原则构建的聚类算法,这一原则是:算法首先声明每个点是自己的簇,然后合并两个最相似的簇,直到满足某种停止准则为止。scikit-learn中实现的停止准则是簇的个数,因此相似的簇被合并,直到仅剩下指定个数的簇。还有一些链接准则,规定如何度量“最相似的簇”。这种度量总是定义在两个现有的簇之间。
scikit-learn中提供三种选项:
ward:默认选项,ward挑选两个簇来合并,使得所有簇中的方差增加最小 。这通常会得到大小差不多相等的簇。
average:将簇中所有点之间平均距离最小的两个簇合并。
complete:也称为最大链接,将簇中点之间最大距离最小的两个簇合并。
ward适用于大多数数据集,如果簇中的成员个数非常不同,那么可以使用后两者效果可能更好。
算法步骤
如果需要,计算邻近度矩阵
repeat
合并最接近的两个簇
更新邻近度矩阵,以反映新的簇与原来的簇之间的邻近性
until 仅剩下一个簇/达到某个终止条件
其中的概念有:
邻近度有许多种定义方式,比如欧氏距离,曼哈顿距离,马氏距离,余弦相似度,Jaccard系数,Bregman散度等等。种类丰富,样品奇多,根据不同的需求来选择最适合的邻近度,计算得到相应的邻近度矩阵。
每个簇中的点数不一定相等,如何计算两个不同簇之间的邻近度呢?
常用的有三种方法:单链(MIN准则 ),全链(MAX准则) ,组平均技术 。
单链:单链(MIN)定义簇的邻近度为不同簇的两个最近的点之间的邻近度。在图中,即不同结点子集中两个节点之间的最短边。下图中的虚线,就是左右两个簇的邻近度。
全链(MAX)定义簇的邻近度为不同簇中两个最远的点之间的邻近度作为簇的邻近度。在图中,即不同结点子集中两个结点之间的最长边。下图中的虚线,就是左右两个簇的邻近度。
组平均是一种基于图的方法。它定义簇邻近度为取自不同簇的所有点对邻近度的平均值(平均长度)。
下图给出了在一个二维数据及上的凝聚聚类过程,指定要寻找三个簇:
最开始,每个点自成一簇,然后在每一步中,相距最近的两个簇被合并。
凝聚算发不能对新数据点做出预测,因此没有predict方法,但是有fit_predict方法。
1 2 3 4 5 6 7 8 9 10 11 12 from sklearn.cluster import AgglomerativeClusteringfrom sklearn.datasets import make_blobsimport matplotlib.pyplot as pltimport mglearnX, y = make_blobs(random_state=1 ) agg = AgglomerativeClustering(n_clusters=3 ) assignment = agg.fit_predict(X) mglearn.discrete_scatter(X[:,0 ], X[:,1 ], assignment) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
可以看出,算法完美地完成了聚类。
树状图可视化
层次化的簇分配图如下:
也可采用树状图显示这一过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from scipy.cluster.hierarchy import dendrogram, wardX, y = make_blobs(random_state=0 , n_samples=12 ) linkage_array = ward(X) dendrogram(linkage_array) ax = plt.gca() bounds = ax.get_xbound() ax.plot(bounds, [7.25 ,7.25 ], '--' , c='k' ) ax.plot(bounds, [4 ,4 ], '--' , c='k' ) ax.text(bounds[1 ], 7.25 , ' two clusters' , va='center' , fontdict={'size' : 15 }) ax.text(bounds[1 ], 4 , ' three clusters' , va='center' , fontdict={'size' : 15 }) plt.xlabel("Sample index" ) plt.ylabel("Cluster distance" ) plt.show()
树状图在底部显示数据点,然后以这些点(表示单点簇)作为叶节点绘制一棵树,每合并两个簇就添加一个新的父节点。
主要问题
缺乏全局目标函数
凝聚层次聚类不能视为全局优化目标函数,因为在每一步合并时仅仅局部地确定哪些簇应当合并(或分裂,对于分裂式)。但是避开了解决困难的组合优化问题,并且这样的方法没有局部极小问题或难选择初始点的问题。
处理不同大小簇的能力
关于如何处理待合并的簇对的相对大小这个问题,解决的方法有两种:一是加权 ,就是不同簇中的点具有不同的权值;二是非加权 ,需要考虑每个簇的点数。
比如组平均技术,上面说的组平均实际上是组平均技术的非加权版,全称为“使用算术平均的非加权的对组方法”(UPGMA),Lance-Williams公式的系数也涉及每个被合并簇的大小。而对于加权版本(WPGMA),它的系数是常数: =1/2, =1/2,β=0,γ=0。通常非加权方法更可取,除非出于某种原因个体点具有不同的权值,如对象类非均匀的抽样。
合并决策是最终的
对于合并两个簇,凝聚层次聚类可以使用所有点的逐对相似度信息趋向于作出好的局部决策。然而,一旦做出合并两个簇的决策,以后就不能撤销,这阻碍了局部最优标准变成全局最优标准。一些方法试图克服“合并是最终的”这一限制,如移动树的分支以改善全局目标;使用划分聚类方法(K均值)来创建许多小簇,然后从这些小簇出发进行层次聚类。
3.5.3 DBSCAN
简介
DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)是一种基于密度的空间聚类算法。 该算法将具有足够密度的区域划分为簇,并在具有噪声的空间数据库中发现任意形状的簇,它将簇定义为密度相连的点的最大集合。
主要优点在于不需要用户先验地设置簇的个数,可以划分具有复杂形状的簇,还可以找出不属于任何簇的点。
算法过程
DBSCAN算法有两个参数:eps和min_samples。
eps:epsilon,为以某个点为圆心的圆的半径,默认为0.5
min_samples:在这个圆中至少含有数据点的个数
DBSCAN算法将数据点类型分为三类:
核心点 :在半径Eps内含有超过MinPts数目的点。
边界点 :在半径Eps内点的数量小于MinPts,但是落在核心点的邻域内的点。
噪音点 :既不是核心点也不是边界点的点。
算法首先任意选取一个点,然后找到到这个点的距离小于等于eps的所有的点。如果所有点的个数小于min_samples,那么这个点被标记为噪声,不属于任何一个簇;否则这个点标记为核心点,并被分配一个新的簇标签。然后访问该点的所有邻居(在eps范围内),如果他们还没有簇标签,则将核心点的标签赋给他们。如果他们是核心点,那么就依次访问邻居,以此类推。直到在簇的eps距离内没有更多的核心点为止,然后选取另一个尚未被访问的点,并重复相同的过程。
以下展示了两个参数设置对聚类效果的影响:
上图中,较大点为核心点,较小的为边界点,没有颜色的为噪声。
虽然DBSCAN不需要显式地设置簇的个数,但是设置eps可以隐式 地控制找到的簇的个数。
在moon数据集上的表现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 X, y = make_moons(n_samples=200 , noise=0.05 , random_state=0 ) scaler = StandardScaler() scaler.fit(X) X_scaled = scaler.transform(X) dbscan = DBSCAN() clusters = dbscan.fit_predict(X_scaled) plt.scatter(X_scaled[:,0 ], X_scaled[:,1 ], c=clusters, cmap=mglearn.cm2, s=60 ) plt.xlabel("Feature 0" ) plt.ylabel("Feature 1" ) plt.show()
采用默认eps可以达到理想的效果,如果减小eps,可能会出现更多的簇,以下是去eps=0.2时的情况:
3.5.4 聚类算法的对比与评估
用真实值评估聚类
有一些指标可以用于评估聚类算法相对于真是聚类的结果。其中重要的有:
调整兰德指数(adjusted rand index,ARI): 取值在[-1,1]之间,负数代表结果不好,越接近于1越好;对任意数量的聚类中心和样本数,随机聚类的ARI都非常接近于0;可用于聚类算法之间的比较。缺点在于ARI需要真实的标签信息对聚类结果进行评价。
归一化互信息(normalized mutual information,NMI):将互信息放在[0,1]之间,容易评价算法的好坏。
下面是利用ARI来比较三种聚类算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from numpy.core.fromnumeric import sizefrom numpy.random import seedfrom sklearn import clusterfrom kmeans_proc import K_meansfrom sklearn.metrics.cluster import adjusted_rand_scorefrom sklearn.preprocessing import StandardScalerfrom sklearn.datasets import make_moonsfrom sklearn.cluster import KMeans, AgglomerativeClustering, DBSCANimport matplotlib.pyplot as pltimport numpy as npimport mglearnX, y = make_moons(n_samples=200 , noise=0.05 , random_state=0 ) scaler = StandardScaler() scaler.fit(X) X_scaled = scaler.transform(X) fix, axes = plt.subplots(1 , 4 , figsize=(15 ,3 ), subplot_kw={'xticks' : (), 'yticks' : ()}) algorithms = [KMeans(n_clusters=2 ), AgglomerativeClustering(n_clusters=2 ), DBSCAN()] random_state = np.random.RandomState(seed=0 ) random_clusters = random_state.randint(low=0 , high=2 , size=len (X)) axes[0 ].scatter(X_scaled[:,0 ], X_scaled[:,1 ], c=random_clusters, cmap=mglearn.cm3, s=60 ) axes[0 ].set_title("Random assignment - ARI: {:.2f}" .format (adjusted_rand_score(y, random_clusters))) for ax, algorithm in zip (axes[1 :], algorithms): clusters = algorithm.fit_predict(X_scaled) ax.scatter(X_scaled[:,0 ], X_scaled[:,1 ], c=clusters,cmap=mglearn.cm3, s=60 ) ax.set_title("{} - ARI: {:.2f}" .format (algorithm.__class__.__name__,adjusted_rand_score(y, clusters))) plt.show()
从上图可以看出,调整兰德指数给出了符合直觉的结果。
注意:用这种真实值方式评估聚类时,一个常见的错误就是使用accuracy_score而不是ARI或NMI或其他聚类指标来评估聚类算法,使用精度的问题 在于,他要求分配的簇标签与真实值完全匹配,但簇标签本身毫无意义——唯一重要的是哪些点位于同一个簇中。
1 2 3 4 5 6 7 8 from sklearn.metrics import accuracy_scoreclusters1 = [0 ,0 ,1 ,1 ,0 ] clusters2 = [1 ,1 ,0 ,0 ,1 ] print ("Accuracy: {:.2f}" .format (accuracy_score(clusters1, clusters2)))print ("ARI: {:.2f}" .format (adjusted_rand_score(clusters1, clusters2)))
在没有真实值的情况下评估聚类
轮廓系数:轮廓分数计算一个簇的紧致度,其值越大越好,最高为1,但是对于复杂形状效果不好。
算法
:假设我们已经通过一定算法,将待分类数据进行了聚类。常用的比如使用K-means ,将待分类数据分为了 k 个簇 。对于簇中的每个向量。分别计算它们的轮廓系数。
对于其中的一个点 i 来说:
计算 a(i) = average(i向量到所有它属于的簇中其它点的距离)
计算 b(i) = min (i向量到与它相邻最近的一簇内的所有点的平均距离)
那么 i 向量轮廓系数就为:
可见轮廓系数的值是介于 [-1,1] ,越趋近于1代表内聚度和分离度都相对较优。将所有点的轮廓系数求平均,就是该聚类结果总的轮廓系数 。a(i) :i向量到同一簇内其他点不相似程度 的平均值;b(i) :i向量到其他簇的平均不相似程度 的最小值。
以下是利用轮廓系数对三种算法的评估:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from sklearn.metrics.cluster import silhouette_scoreX, y = make_moons(n_samples=200 , noise=0.05 , random_state=0 ) scaler = StandardScaler() scaler.fit(X) X_scaled = scaler.transform(X) fix, axes = plt.subplots(1 , 4 , figsize=(15 ,3 ), subplot_kw={'xticks' : (), 'yticks' : ()}) algorithms = [KMeans(n_clusters=2 ), AgglomerativeClustering(n_clusters=2 ), DBSCAN()] random_state = np.random.RandomState(seed=0 ) random_clusters = random_state.randint(low=0 , high=2 , size=len (X)) axes[0 ].scatter(X_scaled[:,0 ], X_scaled[:,1 ], c=random_clusters, cmap=mglearn.cm3, s=60 ) axes[0 ].set_title("Random assignment: {:.2f}" .format (silhouette_score(X_scaled, random_clusters))) for ax, algorithm in zip (axes[1 :], algorithms): clusters = algorithm.fit_predict(X_scaled) ax.scatter(X_scaled[:,0 ], X_scaled[:,1 ], c=clusters,cmap=mglearn.cm3, s=60 ) ax.set_title("{} : {:.2f}" .format (algorithm.__class__.__name__,silhouette_score(X_scaled, clusters))) plt.show()
可以看出,k均值分数最高,但实际聚类效果不如分数较低的DBSCAN算法。
3.5.5 聚类方法小结 聚类的应用于评估是一个非常定性的过程,通常在数据分析的探索阶段很有帮助。
k均值可以用簇的平均值来表示簇,他还可以看作一种分解方法,每个数据点都有其簇中心表示。
DBSCAN可以检测到没有分配任何簇的噪声点,还可以帮助自动判断簇的数量,它允许簇有复杂的形状。
凝聚聚类可以提供数据的可能划分的整个层次结构,可以通过树状图轻松查看。
第四章 数据表示与特征工程
数据表示方式都会对机器学习模型的性能产生巨大影响。对于某个特定应用,如何找到最佳数据表示,这个问题被称为特征工程 。
4.1 分类变量 adult数据集:任务是预测一名工人的收入是高于5w,还是低于5w,属于分类任务,在这个数据集中age,hours-per-week属于连续特征,但是workclass,education,gender等属于离散特征,他们都来自于一系列固定的可能取值(而不是一个范围),表示的是定性属性(而不是数量)。
我们需要换一种方式来表示数据。
4.1.1 One-Hot编码(虚拟变量) 表示分类变量最常用的方法就是使用one-hot编码(或N取1编码),也叫虚拟变量。虚拟变量背后的思想是将一个分类变量替换为一个或多个新特征 ,新特征取值为0或1。
将数据转换为分类变量的one-hot编码常常使用pandas。
1 2 3 4 5 6 7 8 9 10 11 import pandas as pdfrom IPython.display import displaydata = pd.read_csv("data/adult.data" , header=None , index_col=False , names=[ 'age' ,'workclass' ,'fnlwgt' ,'education' ,'education-num' ,'martial-status' ,'occupation' , 'relationship' ,'race' ,'gender' ,'capital-gain' ,'capital-loss' ,'hours-per-week' ,'native-country' ,'income' ]) data = data[['age' ,'workclass' ,'education' ,'gender' ,'hours-per-week' ,'occupation' ,'income' ]] display(data.head())
用pandas编码数据可以使用get_dummies函数,它将自动变换所有具有对象类型(例如字符串)的列或所有分类的列。
1 2 3 print ("Original features:\n" , list (data.columns), "\n" )data_dummies = pd.get_dummies(data) print ("Features after get_dummies:\n" , list (data_dummies.columns))
可以看出,连续特征age和hours-per-week没有发生变化,而分类特征的每个可能取值都被扩展为一个新特征:
训练模型
下面使用values属性将data_dummies数据框转换为NumPy数组,然后在其上训练一个机器学习模型。在训练模型之前,要把两个目标变量(现在被编码为两个income列)从数据中分离出来。
常见错误 :将输出变量或输出变量的一些导出属性包含在特征表示中,是构建监督学习模型的一个常见错误。
1 2 3 4 5 features = data_dummies.loc[:,'age' :'occupation_ Transport-moving' ] X = features.values y = data_dummies['income_ >50K' ].values print ("X.shape: {} y.shape: {}" .format (X.shape, y.shape))
现在的数据表示就可以被scikit-learn处理:
1 2 3 4 5 6 from sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_splitX_train ,X_test, y_train, y_test = train_test_split(X, y, random_state=0 ) logreg = LogisticRegression() logreg.fit(X_train, y_train) print ("Test score: {:.2f}" .format (logreg.score(X_test, y_test)))
4.1.2 数字可以编码分类变量 分类特征通常用整数进行编码,它们是数字并不意味着它们必须被视为连续特征。一个整数特征被视为连续的还是离散的,有时并不明确。
pandas的get_dummies函数将所有数字看作是连续特征,不会为其创建虚拟变量。可以将数据框中的数值列转换成字符串,再进行处理:
1 2 demo_df = pd.DataFrame({'Integer Feature' : [0 ,1 ,2 ,1 ], 'Categorical Feature' : ['socks' ,'fox' ,'socks' ,'bix' ]}) display(demo_df)
以上是包含分类字符串和整数特征的数据框。
使用get_dummies函数只会编码字符串特征,不会改变整数特征:
1 display(pd.get_dummies(demo_df))
如果想为Interger Feature这一列创建虚拟变量,可以使用columns参数显式地给出想要编码的列,于是两个特征都会被当做分类处理特征处理:
1 2 3 demo_df['Integer Feature' ] = demo_df['Integer Feature' ].astype(str ) demo_hot = pd.get_dummies(demo_df, columns=['Integer Feature' , 'Categorical Feature' ]) display(demo_hot)
4.2 分箱,离散化,线性模型与树 数据表示的最佳方法不仅取决于数据的语义,还取决于所使用的的模型种类。以下是在wave数据集上比较线性回归和决策树的图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import numpy as npfrom sklearn.linear_model import LinearRegressionfrom sklearn.tree import DecisionTreeRegressorfrom mglearn.datasets import make_waveimport matplotlib.pyplot as pltimport numpy as npX ,y = make_wave(n_samples=100 ) line = np.linspace(-3 , 3 , 1000 , endpoint=False ).reshape(-1 , 1 ) reg = DecisionTreeRegressor(min_samples_split=3 ).fit(X, y) plt.plot(line, reg.predict(line), label="decision tree" ) reg = LinearRegression().fit(X, y) plt.plot(line, reg.predict(line), label="linear regression" ) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.show()
有一种方法可以让线性模型 在连续数据 上变得更加强大,就是利用特征分箱 (或者离散化)将其划分为多个特征。
将特征的输入范围划分成固定个数的箱子(bin),比如10个,那么数据点就可以利用它们所在的箱子来表示。
1 2 3 bins = np.linspace(-3 , 3 , 11 ) print ("bins: {}" .format (bins))
接下来记录每个数据点所属的箱子。
1 2 3 which_bin = np.digitize(X, bins=bins) print ("\nData points:\n" , X[:5 ])print ("\nBin membership for data points:\n" , which_bin[:5 ])
这里做的就是将wave数据集中单个连续输入特征变换为一个分类特征(离散特征)。
下面将这个离散特征用one-hot编码表示:
1 2 3 4 5 6 7 8 from sklearn.preprocessing import OneHotEncoderencoder = OneHotEncoder(sparse=False ) encoder.fit(which_bin) X_binned = encoder.transform(which_bin) print (X_binned[:5 ])
由于我们指定了10个箱子,因此变换后的X_binned数据集现在包含10个特征。
现在在X_binned数据集上进行构建模型:
1 2 3 4 5 6 7 8 9 10 11 12 line_binned = encoder.transform(np.digitize(line, bins=bins)) reg = DecisionTreeRegressor(min_samples_split=3 ).fit(X_binned, y) plt.plot(line, reg.predict(line_binned), label="decision tree" ) reg = LinearRegression().fit(X_binned, y) plt.plot(line, reg.predict(line_binned), label="linear regression" ) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.show()
线性模型和决策树的线段完全重合,说明两者做出了相同的预测。对于每个箱子,二者都预测了同一个常数值。实际上每个线段的拐点就是两个箱子的边界。可以看出分箱,让线性模型的表现力得到了一定的提高。
适用范围
对于特定的数据集,如果有理由选择使用线性模型,例如数据集很大,维度很高,但有些特征与输出的关系是非线性的 ,那么分箱是提高建模能力的好方法。
4.3 交互特征与多项式特征 想要丰富特征表示,特别是对于线性模型而言,另一种方式是添加原始特征的交互特征和多项式特征 。
交互特征
在上面的例子中,加入原始特征(即x轴),这样会得到11维的数据集:
1 2 3 X_binned = encoder.transform(which_bin) X_combined = np.hstack([X, X_binned]) print (X_combined.shape)
现在利用X_combined数据集构建模型:
1 2 3 4 5 6 7 8 9 10 reg = LinearRegression().fit(X_combined, y) line_combined = np.hstack([line, line_binned]) plt.plot(line, reg.predict(line_combined), label="linear regression combined" ) for bin in bins: plt.plot([bin ,bin ], [-3 ,3 ], ':' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.show()
可以看出,模型学习到了一个向下的斜率,每个箱子都是相同的,现在我们要让每个箱子学习到不同的斜率,我们可以添加一个交互特征 ,这个特征是箱子指示符与原始特征的乘积。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 X_product = np.hstack([X_binned, X * X_binned]) reg = LinearRegression().fit(X_product, y) line_product = np.hstack([line_binned, line * line_binned]) plt.plot(line, reg.predict(line_product), label="linear regression product" ) for bin in bins: plt.plot([bin ,bin ], [-3 ,3 ], ':' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.show()
现在模型中每个箱子都有自己的偏移和斜率。
多项式特征
另一种方法是使用原始特征的多项式特征。对于给定特征x,可以考虑x乘2,乘3,平方等等。
1 2 3 4 5 6 from sklearn.preprocessing import PolynomialFeaturespoly = PolynomialFeatures(degree=10 , include_bias=False ) poly.fit(X) X_poly = poly.transform(X) print ("X_poly.shape: {}" .format (X_poly.shape))
此时,经过变换后的X_poly数据集的特征为x,一直乘到10,共有10个特征。
将多项式特征与线性回归模型一起使用,可以得到经典的多项式回归模型
1 2 3 4 5 6 7 8 9 reg = LinearRegression().fit(X_poly, y) line_poly = poly.transform(line) plt.plot(line, reg.predict(line_poly), label="polynomial linear regression" ) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.show()
多项式特征在这个一维数据上得到了非常平滑的拟合,但高次多项式在边界上或数据很少的区域上可能会有极端的表现。
作为对比,下面是在原始数据上学到的核SVM模型,没有做任何变换:
1 2 3 4 5 6 7 8 9 from sklearn.svm import SVRfor gamma in [1 ,10 ]: svr = SVR(gamma=gamma).fit(X, y) plt.plot(line, svr.predict(line), label='SVR gamma={}' .format (gamma)) plt.plot(X[:,0 ], y, 'o' , c='k' ) plt.ylabel("Regression output" ) plt.xlabel("Input feature" ) plt.legend(loc="best" ) plt.show()
多项式特征的构造方式和作用
先使用boston数据集进行说明:首先加载数据,然后利用MinMaxScaler将其缩放到0到1之间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from numpy.core.fromnumeric import transposefrom sklearn.datasets import load_bostonfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import MinMaxScalerfrom sklearn.preprocessing import PolynomialFeaturesboston = load_boston() X_train, X_test, y_train, y_test = train_test_split(boston.data, boston.target, random_state=0 ) scaler = MinMaxScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.fit_transform(X_test) poly = PolynomialFeatures(degree=2 ).fit(X_train_scaled) X_trian_poly = poly.transform(X_train_scaled) X_test_poly = poly.transform(X_test_scaled) print ("X_train.shape: {}" .format (X_train.shape))print ("X_train_poly.shape: {}" .format (X_trian_poly.shape))print ("Polynomial feature names:\n{}" .format (poly.get_feature_names()))
原始数据有13个特征,现在被扩展到105个特征,具体如上面所示。第一个特征为常数特征,为‘1’,接下来13个特征为原始特征,然后是为第一个特征的平方,以及它与其他特征的组合。
下面利用岭回归Ridge在有交互特征的数据上和没有交互特征的数据上进行性能对比:
1 2 3 4 5 6 from sklearn.linear_model import Ridgeridge = Ridge().fit(X_train_scaled, y_train) print ("Score without interactions: {:.3f}" .format (ridge.score(X_test_scaled, y_test)))ridge = Ridge().fit(X_trian_poly, y_train) print ("Score without interactions: {:.3f}" .format (ridge.score(X_test_poly, y_test)))
显然,在使用岭回归时,交互特征和多项式特征对性能提升有很大的帮助,但如果使用更加复杂的模型(比如随机森林),情况会稍有不同:
1 2 3 4 5 from sklearn.ensemble import RandomForestRegressorrf = RandomForestRegressor(n_estimators=100 ).fit(X_train_scaled, y_train) print ("Score without interactions: {:.3f}" .format (rf.score(X_test_scaled, y_test)))rf = RandomForestRegressor(n_estimators=100 ).fit(X_trian_poly, y_train) print ("Score without interactions: {:.3f}" .format (rf.score(X_test_poly, y_test)))
没有额外的特征,随机森林的性能也要优于岭回归,但是可以看出,添加了交互特征和多项式特征后,实际上会略微降低随机森林的性能。
4.4 单变量非线性变换 其它变换通常对某些特征也有作用,特别是应用数学函数,例如log,exp或sin。大部分模型都在每个特征大致遵循高斯分布时表现最好,下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 rnd = np.random.RandomState(0 ) X_org = rnd.normal(size=(1000 ,3 )) w = rnd.normal(size=3 ) X = rnd.poisson(10 * np.exp(X_org)) y = np.dot(X_org, w) bins = np.bincount(X[:,0 ]) plt.bar(range (len (bins)), bins, color='r' ) plt.ylabel("Number of appearnces" ) plt.xlabel("Value" ) plt.show()
利用这个数据集拟合一个岭回归模型:
1 2 3 X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0 ) score = Ridge().fit(X_train, y_train).score(X_test, y_test) print ("Test score: {:.3f}" .format (score))
现在对特征应用log变换:变换之后,数据分布的不对称性变小,也没有非常大的异常值。
1 2 3 4 5 6 X_train_log = np.log(X_train + 1 ) X_test_log = np.log(X_test + 1 ) plt.hist(X_train_log[:,0 ], bins=25 , color='gray' ) plt.ylabel("Number of appearnces" ) plt.xlabel("Value" ) plt.show()
然后拟合一个岭回归模型:
1 2 score = Ridge().fit(X_train_log, y_train).score(X_test_log, y_test) print ("Test score: {:.3f}" .format (score))
在所举的例子中,所有特征都具有相同的性质。通常来说,只有一部分特征应该进行变换,有时每个特征的变换方式也各不相同。
4.5 自动化特征选择 在添加新特征或处理一般的高维数据集时,最好将特征的数量减少到只包含最有用的那些特征,并且删除其余特征,这样会得到泛化能力更好,更简单的模型。如何判断每个特征的作用,方法主要有三种:
单变量统计(univariate statistics)
基于模型的选择(model-based selection)
迭代选择(iterative selection)
4.5.1 单变量统计 在单变量统计中,我们计算每个特征和目标值之间的关系是否存在统计显著性,然后选择具有最高置信度的特征。这个方法只单独考虑每个特征,如果一个特征只有在与另一个特征合并时才具有信息量,那么这个特征将被舍弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 rom sklearn.datasets import load_breast_cancer from sklearn.feature_selection import SelectPercentilefrom sklearn.model_selection import train_test_splitimport matplotlib.pyplot as pltimport numpy as npcancer = load_breast_cancer() rng = np.random.RandomState(42 ) noise = rng.normal(size=(len (cancer.data), 50 )) X_w_noise = np.hstack([cancer.data, noise]) X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=0 , test_size=.5 ) select = SelectPercentile(percentile=50 ) select.fit(X_train, y_train) X_train_selected = select.transform(X_train) print ("X_train.shape: {}" .format (X_train.shape))print ("X_train_selected.shape: {}" .format (X_train_selected.shape))
特征的数量从80减少到40个。布尔遮罩如下:
1 2 3 4 5 6 mask = select.get_support() print (mask)plt.matshow(mask.reshape(1 ,-1 ), cmap='gray_r' ) plt.xlabel("Sample index" ) plt.show()
可以看出大多数所选择的特征都是原始特征,并且大多数噪声特征都已被删除。下面是Logistic回归的拟合情况:
1 2 3 4 5 6 7 8 9 10 from sklearn.linear_model import LogisticRegressionX_test_selected = select.transform(X_test) lr = LogisticRegression() lr.fit(X_train, y_train) print ("Score with all features: {:.3f}" .format (lr.score(X_test, y_test)))lr.fit(X_train_selected, y_train) print ("Score with only selected features: {:.3f}" .format (lr.score(X_train_selected, y_test)))
4.5.2 基于模型的特征选择 基于模型的特征选择使用一个监督机器学习模型来判断每个特征的重要性,并且仅保留最重要的特征。用于特征选择的模型不需要与用于最终监督建模的模型相同。与单变量选择不同,基于模型的选择同时考虑所有特征。
下面的例子使用包含100棵树的随机森林分类器来计算特征重要性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from sklearn.feature_selection import SelectFromModelfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_splitimport matplotlib.pyplot as pltimport numpy as npcancer = load_breast_cancer() rng = np.random.RandomState(42 ) noise = rng.normal(size=(len (cancer.data), 50 )) X_w_noise = np.hstack([cancer.data, noise]) X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=0 , test_size=.5 ) select = SelectFromModel(RandomForestClassifier(n_estimators=100 , random_state=42 ),threshold="median" ) select.fit(X_train, y_train) X_train_l1 = select.transform(X_train) print ("X_train.shape: {}" .format (X_train.shape))print ("X_train_l1.shape: {}" .format (X_train_l1.shape))mask = select.get_support() plt.matshow(mask.reshape(1 ,-1 ), cmap='gray_r' ) plt.xlabel("Sample index" ) plt.show()
1 2 3 X_test_l1 = select.transform(X_test) score = LogisticRegression(max_iter=5000 ).fit(X_train_l1, y_train).score(X_test_l1, y_test) print ("Test score: {:.3f}" .format (score))
4.5.3 迭代特征选择 在迭代特征选择中,将会构建一系列模型,每个模型都使用不同数量的特征。有两种基本方法:
开始时没有特征,然后逐个添加特征,直到满足某个终止条件。
从所有特征开始,然后逐个删除特征,直到满足某个终止条件。
其中一种特殊方法是递归特征消除(RFE),它从所有特征开始构建模型,并根据模型舍弃最不重要的特征,然后使用除被舍弃特征之外的所有特征来构建一个新的模型,如此继续,直到剩下预设数量的特征。
1 2 3 4 5 6 7 from sklearn.feature_selection import RFEselect = RFE(RandomForestClassifier(n_estimators=100 , random_state=42 ),n_features_to_select=40 ) select.fit(X_train, y_train) mask = select.get_support() plt.matshow(mask.reshape(1 ,-1 ), cmap='gray_r' ) plt.xlabel("Sample index" ) plt.show()
对一个随机森林训练了40次,每训练每一次就删除一个特征。
1 2 3 4 5 X_train_rfe = select.transform(X_train) X_test_rfe = select.transform(X_test) score = LogisticRegression(max_iter=1000 ).fit(X_train_rfe, y_train).score(X_test_rfe, y_test) print ("Test score: {:.3f}" .format (score))
4.6 小结 对于线性模型,可能会从分箱,添加多项式和交互项而生成的新特征中大大受益,对于更加复杂的非线性模型(例如随机森林和SVM),在无需显示扩展特征空间的前提下就可以学习更加复杂的任务。在实践中,所使用的特征(以及特征与方法之间的匹配)通常是使机器学习方法表现良好的重要因素。