本系列教程基本就是摘抄《Python机器学习基础教程》中的例子内容。html
为了便于跟踪和学习,本系列教程在Github上提供了jupyter notebook 版本:python
Github仓库:https://github.com/Holy-Shine/Introduciton-2-ML-with-Python-notebookgit
系列教程总目录 Python机器学习基础教程github
先导入必要的包算法
import numpy as np import matplotlib.pyplot as plt import pandas as pd import mglearn %matplotlib inline
线性模型是在实践中普遍使用的一类模型,几十年来被普遍研究,它能够追溯到一百多年前。线性模型利用输入特征的线性函数(linear function)进行预测,稍后会对此进行解释。数组
对于回归问题,线性模型预测的通常公式以下:dom
$$ŷ = w[0] * x[0] + w[1] * x[1] + … + w[p] * x[p] + b$$机器学习
这里 $x[0]$ 到 $x[p]$ 表示单个数据点的特征(本例中特征个数为 $p+1$),$w$ 和 $b$ 是学习模型的 参数,$ŷ$ 是模型的预测结果。对于单一特征的数据集,公式以下:函数
$$ŷ = w[0] * x[0] + b$$性能
你可能还记得,这就是高中数学里的直线方程。这里 $w[0]$ 是斜率,$b$ 是 $y$ 轴偏移。对于有 更多特征的数据集,$w$ 包含沿每一个特征坐标轴的斜率。或者,你也能够将预测的响应值看 做输入特征的加权求和,权重由 $w$ 的元素给出(能够取负值)。 下列代码能够在一维 wave 数据集上学习参数 $w[0]$ 和 $b$:
mglearn.plots.plot_linear_regression_wave()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b0668c23025.jpg" width=60%></center> <center><b>图 2-11:线性模型对 wave 数据集的预测结果</b></center>
咱们在图中添加了坐标网格,便于理解直线的含义。从 w[0] 能够看出,斜率应该在 0.4 左右,在图像中也能够直观地确认这一点。截距是指预测直线与 y 轴的交点:比 0 略小,也能够在图像中确认。
用于回归的线性模型能够表示为这样的回归模型:对单一特征的预测结果是一条直线,两个特征时是一个平面,或者在更高维度(即更多特征)时是一个超平面。
若是将直线的预测结果与上一章图 2-10 中 KNeighborsRegressor 的预测结果进行比较,你会发现直线的预测能力很是受限。彷佛数据的全部细节都丢失了。从某种意义上来讲,这种说法是正确的。假设目标 y 是特征的线性组合,这是一个很是强的(也有点不现实的)假设。但观察一维数据得出的观点有些片面。对于有多个特征的数据集而言,线性模型能够很是强大。特别地,若是特征数量大于训练数据点的数量,任何目标 y 均可以(在训练集上)用线性函数完美拟合。
有许多不一样的线性回归模型。这些模型之间的区别在于如何从训练数据中学习参数 w 和 b,以及如何控制模型复杂度。下面介绍最多见的线性回归模型。
线性回归,或者普通最小二乘法(ordinary least squares,OLS),是回归问题最简单也最经典的线性方法。线性回归寻找参数 w 和 b,使得对训练集的预测值与真实的回归目标值 y 之间的均方偏差最小。均方偏差(mean squared error)是预测值与真实值之差的平方和除以样本数。线性回归没有参数,这是一个优势,但也所以没法控制模型的复杂度。
下列代码能够生成图 2-11 中的模型:
from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split X, 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)
“斜率”参数(w,也叫做权重或系数)被保存在 coef_ 属性中,而偏移或截距(b)被保存在 intercept_ 属性中:
print("lr.coef_: {}".format(lr.coef_)) print("lr.intercept_: {}".format(lr.intercept_))
[out]:
lr.coef_: [0.39390555] lr.intercept_: -0.031804343026759746
你可能注意到了 coef_ 和 intercept_ 结尾处奇怪的下划线。scikit-learn 老是将从训练数据中得出的值保存在如下划线结尾的属性中。这是为了将其与用户设置的参数区分开。
intercept_ 属性是一个浮点数,而 coef_ 属性是一个 NumPy 数组,每一个元素对应一个输入特征。因为 wave 数据集中只有一个输入特征,因此 lr.coef_ 中只有一个元素。咱们来看一下训练集和测试集的性能:
print("Training set score: {:.2f}".format(lr.score(X_train, y_train))) print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
[out]:
Training set score: 0.67 Test set score: 0.66
$R^2$ 约为 0.66,这个结果不是很好,但咱们能够看到,训练集和测试集上的分数很是接近。这说明可能存在欠拟合,而不是过拟合。对于这个一维数据集来讲,过拟合的风险很小,由于模型很是简单(或受限)。然而,对于更高维的数据集(即有大量特征的数据集),线性模型将变得更增强大,过拟合的可能性也会变大。咱们来看一下 LinearRegression 在更复杂的数据集上的表现,好比波士顿房价数据集。记住,这个数据集有 506 个样本和 105个导出特征。首先,加载数据集并将其分为训练集和测试集。而后像前面同样构建线性回归模型:
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)
比较一下训练集和测试集的分数就能够发现,咱们在训练集上的预测很是准确,但测试集上的 $R^2$ 要低不少:
print("Training set score: {:.2f}".format(lr.score(X_train, y_train))) print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
[out]:
Training set score: 0.95 Test set score: 0.61
训练集和测试集之间的性能差别是过拟合的明显标志,所以咱们应该试图找到一个能够控制复杂度的模型。标准线性回归最经常使用的替代方法之一就是岭回归(ridge regression),下面来看一下。
岭回归也是一种用于回归的线性模型,所以它的预测公式与普通最小二乘法相同。但在岭回归中,对系数(w)的选择不只要在训练数据上获得好的预测结果,并且还要拟合附加约束。咱们还但愿系数尽可能小。换句话说,w 的全部元素都应接近于 0。直观上来看,这意味着每一个特征对输出的影响应尽量小(即斜率很小),同时仍给出很好的预测结果。这种约束是所谓正则化(regularization)的一个例子。正则化是指对模型作显式约束,以免过拟合。岭回归用到的这种被称为 L2 正则化。
岭回归在 linear_model.Ridge 中实现。来看一下它对扩展的波士顿房价数据集的效果如何
from sklearn.linear_model import Ridge ridge = Ridge().fit(X_train, y_train) print("Training set score: {:.2f}".format(ridge.score(X_train, y_train))) print("Test set score: {:.2f}".format(ridge.score(X_test, y_test)))
[out] :
Training set score: 0.89 Test set score: 0.75
能够看出, Ridge 在训练集上的分数要低于 LinearRegression ,但在测试集上的分数更高。这和咱们的预期一致。线性回归对数据存在过拟合。 Ridge 是一种约束更强的模型,因此更不容易过拟合。复杂度更小的模型意味着在训练集上的性能更差,但泛化性能更好。因为咱们只对泛化性能感兴趣,因此应该选择 Ridge 模型而不是 LinearRegression 模型。
Ridge 模型在模型的简单性(系数都接近于 0)与训练集性能之间作出权衡。简单性和训练集性能两者对于模型的重要程度能够由用户经过设置 alpha 参数来指定。在前面的例子中,咱们用的是默认参数 alpha=1.0 。但没有理由认为这会给出最佳权衡。 alpha 的最佳设定值取决于用到的具体数据集。增大 alpha 会使得系数更加趋向于 0,从而下降训练集性能,但可能会提升泛化性能。例如:
ridge10=Ridge(alpha=10).fit(X_train, y_train) print("Training set score: {:.2f}".format(ridge10.score(X_train, y_train))) print("Test set score: {:.2f}".format(ridge10.score(X_test, y_test)))
[out]:
Training set score: 0.79 Test set score: 0.64
减少 alpha 可让系数受到的限制更小。对于很是小的 alpha 值,系数几乎没有受到限制,咱们获得一个与 LinearRegression 相似的模型:
ridge01 = Ridge(alpha=0.1).fit(X_train, y_train) print("Training set score: {:.2f}".format(ridge01.score(X_train, y_train))) print("Test set score: {:.2f}".format(ridge01.score(X_test, y_test)))
[out]:
Training set score: 0.93 Test set score: 0.77
这里 alpha=0.1 彷佛效果不错。咱们能够尝试进一步减少 alpha 以提升泛化性能。第 5 章将会讨论选择参数的正确方法。
咱们还能够查看 alpha 取不一样值时模型的 coef_ 属性,从而更加定性地理解 alpha 参数是如何改变模型的。更大的 alpha 表示约束更强的模型,因此咱们预计大 alpha 对应的 coef_ 元素比小 alpha 对应的 coef_ 元素要小。这一点能够在图 2-12 中获得证明:
plt.plot(ridge.coef_, 's', label="Ridge alpha=1") plt.plot(ridge10.coef_, '^', label="Ridge alpha=10") plt.plot(ridge01.coef_, 'v', label="Ridge alpha=0.1") plt.plot(lr.coef_, 'o', label="LinearRegression") plt.xlabel("Coefficient index") plt.ylabel("Coefficient magnitude") plt.hlines(0, 0, len(lr.coef_)) plt.ylim(-25, 25) plt.legend()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b2feaf87934.jpg" width=60%></center> <center><b>图 2-12:不一样 alpha 值的岭回归与线性回归的系数比较</b></center>
这里 x 轴对应 coef_ 的元素: x=0 对应第一个特征的系数, x=1 对应第二个特征的系数,以此类推,一直到 x=100 。y 轴表示该系数的具体数值。这里须要记住的是,对于 alpha=10 ,系数大多在 -3 和 3 之间。对于 alpha=1 的 Ridge 模型,系数要稍大一点。对于 alpha=0.1 ,点的范围更大。对于没有作正则化的线性回归(即 alpha=0 ),点的范围很大,许多点都超出了图像的范围。
还有一种方法能够用来理解正则化的影响,就是固定 alpha 值,但改变训练数据量。对于图 2-13 来讲,咱们对波士顿房价数据集作二次抽样,并在数据量逐渐增长的子数据集上分别对 LinearRegression 和 Ridge(alpha=1) 两个模型进行评估(将模型性能做为数据集大小的函数进行绘图,这样的图像叫做学习曲线):
mglearn.plots.plot_ridge_n_samples()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b1730438899.jpg" width=60%></center> <center><b>图 2-13:岭回归和线性回归在波士顿房价数据集上的学习曲线</b></center>
正如所预计的那样,不管是岭回归仍是线性回归,全部数据集大小对应的训练分数都要高于测试分数。因为岭回归是正则化的,所以它的训练分数要总体低于线性回归的训练分数。但岭回归的测试分数要更高,特别是对较小的子数据集。若是少于 400 个数据点,线性回归学不到任何内容。随着模型可用的数据愈来愈多,两个模型的性能都在提高,最终线性回归的性能追上了岭回归。这里要记住的是,若是有足够多的训练数据,正则化变得不那么重要,而且岭回归和线性回归将具备相同的性能(在这个例子中,两者相同刚好发生在整个数据集的状况下,这只是一个巧合)。图 2-13 中还有一个有趣之处,就是线性回归的训练性能在降低。若是添加更多数据,模型将更加难以过拟合或记住全部的数据。
除了 Ridge ,还有一种正则化的线性回归是 Lasso 。与岭回归相同,使用 lasso 也是约束系数使其接近于 0,但用到的方法不一样,叫做 L1 正则化。L1 正则化的结果是,使用 lasso 时某些系数恰好为 0。这说明某些特征被模型彻底忽略。这能够看做是一种自动化的特征选择。某些系数恰好为 0,这样模型更容易解释,也能够呈现模型最重要的特征。
咱们将 lasso 应用在扩展的波士顿房价数据集上:
from sklearn.linear_model import Lasso lasso = Lasso().fit(X_train, y_train) print("Training 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 used: {}".format(np.sum(lasso.coef_ != 0))) print("Number of all feature: {}".format(lasso.coef_.shape[0]))
[out]:
Training set score: 0.29 Test set score: 0.21 Number of features used: 4 Number of all feature: 104
如你所见, Lasso 在训练集与测试集上的表现都不好。这表示存在欠拟合,咱们发现模型只用到了 105 个特征中的 4 个。与 Ridge 相似, Lasso 也有一个正则化参数 alpha ,能够控制系数趋向于 0 的强度。在上一个例子中,咱们用的是默认值 alpha=1.0 。为了下降欠拟合,咱们尝试减少 alpha 。这么作的同时,咱们还须要增长 max_iter 的值(运行迭代的最大次数):
# 咱们增大max_iter的值,不然模型会警告咱们,说应该增大max_iter lasso001 = Lasso(alpha=0.01, max_iter=100000).fit(X_train, y_train) print("Training set score: {:.2f}".format(lasso001.score(X_train, y_train))) print("Test set score: {:.2f}".format(lasso001.score(X_test, y_test))) print("Number of features used: {}".format(np.sum(lasso001.coef_ != 0)))
[out]:
Training set score: 0.90 Test set score: 0.77 Number of features used: 33
alpha 值变小,咱们能够拟合一个更复杂的模型,在训练集和测试集上的表现也更好。模型性能比使用 Ridge 时略好一点,并且咱们只用到了 105 个特征中的 33 个。这样模型可能更容易理解。
但若是把 alpha 设得过小,那么就会消除正则化的效果,并出现过拟合,获得与LinearRegression 相似的结果:
lasso00001 = Lasso(alpha=0.0001, max_iter=100000).fit(X_train, y_train) print("Training set score: {:.2f}".format(lasso00001.score(X_train, y_train))) print("Test set score: {:.2f}".format(lasso00001.score(X_test, y_test))) print("Number of features used: {}".format(np.sum(lasso00001.coef_ != 0)))
[out]:
Training set score: 0.95 Test set score: 0.64 Number of features used: 96
再次像图 2-12 那样对不一样模型的系数进行做图,见图 2-14:
plt.plot(lasso.coef_, 's', label="Lasso alpha=1") plt.plot(lasso001.coef_, '^', label="Lasso alpha=0.01") plt.plot(lasso00001.coef_, 'v', label="Lasso alpha=0.0001") plt.plot(ridge01.coef_, 'o', label="Ridge alpha=0.1") plt.legend(ncol=2, loc=(0, 1.05)) plt.ylim(-25, 25) plt.xlabel("Coefficient index") plt.ylabel("Coefficient magnitude")
<center><img src="https://i.loli.net/2019/06/13/5d01e56b28f7022674.jpg" width=60%></center> <center><b>图 2-14:不一样 alpha 值的 lasso 回归与岭回归的系数比较</b></center>
在 alpha=1 时,咱们发现不只大部分系数都是 0(咱们已经知道这一点),并且其余系数也都很小。将 alpha 减少至 0.01 ,咱们获得图中向上的三角形,大部分特征等于 0。alpha=0.0001 时,咱们获得正则化很弱的模型,大部分系数都不为 0,而且还很大。为了便于比较,图中用圆形表示 Ridge 的最佳结果。 alpha=0.1 的 Ridge 模型的预测性能与alpha=0.01 的 Lasso 模型相似,但 Ridge 模型的全部系数都不为 0。
在实践中,在两个模型中通常首选岭回归。但若是特征不少,你认为只有其中几个是重要的,那么选择 Lasso 可能更好。一样,若是你想要一个容易解释的模型, Lasso 能够给出更容易理解的模型,由于它只选择了一部分输入特征。 scikit-learn 还提供了 ElasticNet类,结合了 Lasso 和 Ridge 的惩罚项。在实践中,这种结合的效果最好,不过代价是要调节两个参数:一个用于 L1 正则化,一个用于 L2 正则化。
线性模型也普遍应用于分类问题。咱们首先来看二分类。这时能够利用下面的公式进行 预测:
$$ŷ = w[0] * x[0] + w[1] * x[1] + …+ w[p] * x[p] + b > 0$$
这个公式看起来与线性回归的公式很是类似,但咱们没有返回特征的加权求和,而是为预测设置了阈值(0)。若是函数值小于 0,咱们就预测类别 -1;若是函数值大于 0,咱们就预测类别 +1。对于全部用于分类的线性模型,这个预测规则都是通用的。一样,有不少种不一样的方法来找出系数(w)和截距(b)。
对于用于回归的线性模型,输出 ŷ 是特征的线性函数,是直线、平面或超平面(对于更高维的数据集)。对于用于分类的线性模型,决策边界是输入的线性函数。换句话说,(二元)线性分类器是利用直线、平面或超平面来分开两个类别的分类器。本节咱们将看到这方面的例子。
学习线性模型有不少种算法。这些算法的区别在于如下两点:
不一样的算法使用不一样的方法来度量“对训练集拟合好坏”。因为数学上的技术缘由,不可能调节 w 和 b 使得算法产生的误分类数量最少。对于咱们的目的,以及对于许多应用而言,上面第一点(称为损失函数)的选择并不重要。
最多见的两种线性分类算法是 Logistic 回归(logistic regression)和线性支持向量机(linear support vector machine,线性 SVM),前者在 linear_model.LogisticRegression 中实现,后者在 svm.LinearSVC (SVC 表明支持向量分类器)中实现。虽然 LogisticRegression的名字中含有回归(regression),但它是一种分类算法,并非回归算法,不该与LinearRegression 混淆。
咱们能够将 LogisticRegression 和 LinearSVC 模型应用到 forge 数据集上,并将线性模型找到的决策边界可视化(图 2-15):
from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC X, y = mglearn.datasets.make_forge() figs, 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()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b1a84b68821.jpg" width=80%></center> <center><b>图 2-15:线性 SVM 和 Logistic 回归在 forge 数据集上的决策边界(均为默认参数)</b></center>
在这张图中, forge 数据集的第一个特征位于 x 轴,第二个特征位于 y 轴,与前面相同。图中分别展现了 LinearSVC 和 LogisticRegression 获得的决策边界,都是直线,将顶部归为类别 1 的区域和底部归为类别 0 的区域分开了。换句话说,对于每一个分类器而言,位于黑线上方的新数据点都会被划为类别 1,而在黑线下方的点都会被划为类别 0。
两个模型获得了类似的决策边界。注意,两个模型中都有两个点的分类是错误的。两个模型都默认使用 L2 正则化,就像 Ridge 对回归所作的那样。
对于 LogisticRegression 和 LinearSVC ,决定正则化强度的权衡参数叫做 C 。 C 值越大,对应的正则化越弱。换句话说,若是参数 C 值较大,那么 LogisticRegression 和LinearSVC 将尽量将训练集拟合到最好,而若是 C 值较小,那么模型更强调使系数向量(w)接近于 0。参数 C 的做用还有另外一个有趣之处。较小的 C 值可让算法尽可能适应“大多数”数据点,而较大的 C 值更强调每一个数据点都分类正确的重要性。下面是使用 LinearSVC 的图示(图 2-16):
mglearn.plots.plot_linear_svc_regularization()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b2765b66381.jpg" width=80%></center> <center><b>图 2-16:不一样 C 值的线性 SVM 在 forge 数据集上的决策边界</b></center>
在左侧的图中, C 值很小,对应强正则化。大部分属于类别 0 的点都位于底部,大部分属于类别 1 的点都位于顶部。强正则化的模型会选择一条相对水平的线,有两个点分类错误。在中间的图中, C 值稍大,模型更关注两个分类错误的样本,使决策边界的斜率变大。最后,在右侧的图中,模型的 C 值很是大,使得决策边界的斜率也很大,如今模型对类别 0 中全部点的分类都是正确的。类别 1 中仍有一个点分类错误,这是由于对这个数据集来讲,不可能用一条直线将全部点都分类正确。右侧图中的模型尽可能使全部点的分类都正确,但可能没法掌握类别的总体分布。换句话说,这个模型极可能过拟合。
与回归的状况相似,用于分类的线性模型在低维空间中看起来可能很是受限,决策边界只能是直线或平面。一样,在高维空间中,用于分类的线性模型变得很是强大,当考虑更多特征时,避免过拟合变得愈来愈重要。
咱们在乳腺癌数据集上详细分析 LogisticRegression :
from sklearn.datasets import load_breast_cancer cancer = 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().fit(X_train, y_train) print("Training set score: {:.3f}".format(logreg.score(X_train, y_train))) print("Test set score: {:.3f}".format(logreg.score(X_test, y_test)))
[out]:
Training set score: 0.955 Test set score: 0.958
C=1 的默认值给出了至关好的性能,在训练集和测试集上都达到 95% 的精度。但因为训练集和测试集的性能很是接近,因此模型极可能是欠拟合的。咱们尝试增大 C 来拟合一个更灵活的模型:
logreg100 = LogisticRegression(C=100).fit(X_train, y_train) print("Training set score: {:.3f}".format(logreg100.score(X_train, y_train))) print("Test set score: {:.3f}".format(logreg100.score(X_test, y_test)))
[out]:
Training set score: 0.972 Test set score: 0.965
使用 C=100 能够获得更高的训练集精度,也获得了稍高的测试集精度,这也证明了咱们的直觉,即更复杂的模型应该性能更好。 咱们还能够研究使用正则化更强的模型时会发生什么。设置 C=0.01 :
logreg001 = LogisticRegression(C=0.01).fit(X_train, y_train) print("Training set score: {:.3f}".format(logreg001.score(X_train, y_train))) print("Test set score: {:.3f}".format(logreg001.score(X_test, y_test)))
[out]:
Training set score: 0.934 Test set score: 0.930
正如咱们所料,在图 2-1 中将已经欠拟合的模型继续向左移动,训练集和测试集的精度都比采用默认参数时更小。
最后,来看一下正则化参数 C 取三个不一样的值时模型学到的系数(图 2-17):
plt.plot(logreg.coef_.T, 'o', label="C=1") plt.plot(logreg100.coef_.T, '^', label="C=100") plt.plot(logreg001.coef_.T, 'v', label="C=0.001") plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90) plt.hlines(0, 0, cancer.data.shape[1]) plt.ylim(-5, 5) plt.xlabel("Coefficient index") plt.ylabel("Coefficient magnitude") plt.legend()
<center><img src="https://i.loli.net/2019/06/13/5d01e56b2ab2b85354.jpg" width=60%></center> <center><b>图 2-17:不一样 C 值的 Logistic 回归在乳腺癌数据集上学到的系数</b></center>
因为 LogisticRegression 默认应用 L2 正则化,因此其结果与图 2-12 中 Ridge 的结果相似。更强的正则化使得系数更趋向于 0,但系数永远不会正好等于 0。进一步观察图像,还能够在第 3 个系数那里发现有趣之处,这个系数是“平均周长”(mean perimeter)。C=100 和 C=1 时,这个系数为负,而C=0.001 时这个系数为正,其绝对值比 C=1 时还要大。在解释这样的模型时,人们可能会认为,系数能够告诉咱们某个特征与哪一个类别有关。例如,人们可能会认为高“纹理错误”(texture error)特征与“恶性”样本有关。但“平均周长”系数的正负号发生变化,说明较大的“平均周长”能够被看成“良性”的指标或“恶性”的指标,具体取决于咱们考虑的是哪一个模型。这也说明,对线性模型系数的解释应该始终持保留态度。
若是想要一个可解释性更强的模型,使用 L1 正则化可能更好,由于它约束模型只使用少 数几个特征。下面是使用 L1 正则化的系数图像和分类精度(图 2-18)。
<center><img src="https://i.loli.net/2019/06/13/5d01e56b2e14546796.jpg" width=60%></center> <center><b>图 2-18:对于不一样的 C 值,L1 惩罚的 Logistic 回归在乳腺癌数据集上学到的系数</b></center>
for C, marker in zip([0.001, 1, 100], ['o', '^', 'v']): lr_l1 = LogisticRegression(C=C, penalty="l1").fit(X_train, y_train) print("Training accuracy of l1 logreg with C={:.3f}: {:.2f}".format( C, lr_l1.score(X_train, y_train))) print("Test accuracy of l1 logreg with C={:.3f}: {:.2f}".format( C, lr_l1.score(X_test, y_test))) plt.plot(lr_l1.coef_.T, marker, label="C={:.3f}".format(C)) plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90) plt.hlines(0, 0, cancer.data.shape[1]) plt.xlabel("Coefficient index") plt.ylabel("Coefficient magnitude") plt.ylim(-5, 5) plt.legend(loc=3)
如你所见,用于二分类的线性模型与用于回归的线性模型有许多类似之处。与用于回归的线性模型同样,模型的主要差异在于 penalty 参数,这个参数会影响正则化,也会影响模型是使用全部可用特征仍是只选择特征的一个子集。
许多线性分类模型只适用于二分类问题,不能轻易推广到多类别问题(除了 Logistic 回归)。将二分类算法推广到多分类算法的一种常见方法是“一对其他”(one-vs.-rest)方法。在“一对其他”方法中,对每一个类别都学习一个二分类模型,将这个类别与全部其余类别尽可能分开,这样就生成了与类别个数同样多的二分类模型。在测试点上运行全部二类分类器来进行预测。在对应类别上分数最高的分类器“胜出”,将这个类别标签返回做为预测结果。
每一个类别都对应一个二类分类器,这样每一个类别也都有一个系数(w)向量和一个截距(b)。下面给出的是分类置信方程,其结果中最大值对应的类别即为预测的类别标签:
$$ w[0] * x[0] + w[1] * x[1] + … + w[p] * x[p] + b$$
多分类 Logistic 回归背后的数学与“一对其他”方法稍有不一样,但它也是对每一个类别都有一个系数向量和一个截距,也使用了相同的预测方法。
咱们将“一对其他”方法应用在一个简单的三分类数据集上。咱们用到了一个二维数据集,每一个类别的数据都是从一个高斯分布中采样得出的(见图 2-19):
from sklearn.datasets import make_blobs X, 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"])
<center><img src="https://i.loli.net/2019/06/13/5d01e56b18d7321180.jpg" width=60%></center> <center><b>图 2-19:包含 3 个类别的二维玩具数据集</b></center>
如今,在这个数据集上训练一个 LinearSVC 分类器:
linear_svm = LinearSVC().fit(X, y) print("Coefficient shape: ", linear_svm.coef_.shape) print("Intercept shape: ", linear_svm.intercept_.shape)
[out]:
Coefficient shape: (3, 2) Intercept shape: (3,)
们看到, coef_ 的形状是 (3, 2) ,说明 coef_ 每行包含三个类别之一的系数向量,每列包含某个特征(这个数据集有 2 个特征)对应的系数值。如今 intercept_ 是一维数组,保存每一个类别的截距。
咱们将这 3 个二类分类器给出的直线可视化(图 2-20):
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))
<center><img src="https://i.loli.net/2019/06/13/5d01e56b2c5eb15132.jpg" width=80%></center> <center><b>图 2-20:三个“一对其他”分类器学到的决策边界</b></center>
你能够看到,训练集中全部属于类别 0 的点都在与类别 0 对应的直线上方,这说明它们位于这个二类分类器属于“类别 0”的那一侧。属于类别 0 的点位于与类别 2 对应的直线上方,这说明它们被类别 2 的二类分类器划为“其他”。属于类别 0 的点位于与类别 1 对应的直线左侧,这说明类别 1 的二元分类器将它们划为“其他”。所以,这一区域的全部点都会被最终分类器划为类别 0(类别 0 的分类器的分类置信方程的结果大于 0,其余两个类别对应的结果都小于 0)。
但图像中间的三角形区域属于哪个类别呢,3 个二类分类器都将这一区域内的点划为“其他”。这里的点应该划归到哪个类别呢?答案是分类方程结果最大的那个类别,即最接近的那条线对应的类别。下面的例子(图 2-21)给出了二维空间中全部区域的预测结果:
mglearn.plots.plot_2d_classification(linear_svm, X, fill=True, alpha=.7) 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.legend(['Class 0', 'Class 1', 'Class 2', 'Line class 0', 'Line class 1', 'Line class 2'], loc=(1.01, 0.3)) plt.xlabel("Feature 0") plt.ylabel("Feature 1")
<center><img src="https://i.loli.net/2019/06/13/5d01e8313540f37143.jpg" width=80%></center> <center><b>图 2-21:三个“一对其他”分类器获得的多分类决策边界</b></center>
线性模型的主要参数是正则化参数,在回归模型中叫做 alpha ,在 LinearSVC 和 Logistic-Regression 中叫做 C 。 alpha 值较大或 C 值较小,说明模型比较简单。特别是对于回归模型而言,调节这些参数很是重要。一般在对数尺度上对 C 和 alpha 进行搜索。你还须要肯定的是用 L1 正则化仍是 L2 正则化。若是你假定只有几个特征是真正重要的,那么你应该用L1 正则化,不然应默认使用 L2 正则化。若是模型的可解释性很重要的话,使用 L1 也会有帮助。因为 L1 只用到几个特征,因此更容易解释哪些特征对模型是重要的,以及这些特征的做用。
线性模型的训练速度很是快,预测速度也很快。这种模型能够推广到很是大的数据集,对稀疏数据也颇有效。若是你的数据包含数十万甚至上百万个样本,你可能须要研究如何使用 LogisticRegression 和 Ridge 模型的 solver='sag' 选项,在处理大型数据时,这一选项比默认值要更快。其余选项还有 SGDClassifier 类和 SGDRegressor 类,它们对本节介绍的线性模型实现了可扩展性更强的版本。
线性模型的另外一个优势在于,利用咱们之间见过的用于回归和分类的公式,理解如何进行预测是相对比较容易的。不幸的是,每每并不彻底清楚系数为何是这样的。若是你的数据集中包含高度相关的特征,这一问题尤其突出。在这种状况下,可能很难对系数作出解释。
若是特征数量大于样本数量,线性模型的表现一般都很好。它也经常使用于很是大的数据集,只是由于训练其余模型并不可行。但在更低维的空间中,其余模型的泛化性能可能更好。2.3.7 节会介绍几个线性模型不适用的例子。