线性模型利用输入特征的线性函数(linear function)进行预测。
对于回归问题,线性模型预测的一般公式如下:
y ^ = w [ 0 ] ∗ x [ 0 ] + w [ 1 ] ∗ x [ 1 ] + . . . + w [ p ] ∗ x [ p ] + b \hat{y}=w[0]*x[0]+w[1]*x[1]+...+w[p]*x[p]+b y^=w[0]∗x[0]+w[1]∗x[1]+...+w[p]∗x[p]+b
这里 x[0] 到 x[p] 表示单个数据点的特征(本例中特征数为 p+1),w 和 b 是学习模型的参数,y 是模型的预测结果。对于单一特征的数据集,公式如下:
y ^ = w [ 0 ] ∗ x [ 0 ] + b \hat{y}=w[0]*x[0]+b y^=w[0]∗x[0]+b
你可能还记得,这就是高中数学里的直线方程。这里 w[0] 是斜率,b 是 y 轴偏移。对于有更多特征的数据集,w 包含沿每个特征坐标轴的斜率。或者,你也可以将预测的响应值看作输入特征的加权求和,权重由 w 的元素给出(可以取负值):
下列代码可以在一维 wave 数据集上学习参数 w[0] 和 b:
mglearn.plots.plot_linear_regression_wave()
# w[0]: 0.393906 b: -0.031804
用于回归的线性模型可以表示为这样的回归模型:对单一特征的预测结果是一条直线,两个特征时是一个平面,或者在更高维度(即更多特征)时是一个超平面。
如果将直线预测结果与 KNeighborsRegressor 的预测结果进行比较,你会发现直线的预测能力非常受限。似乎数据的所有细节都丢失了。从某种意义上说,这种说法是正确的。假设目标 y 是特征的线性组合,这是一个非常强(也有点不现实)的假设。但观察一维数据得出的观点有些片面。对于有多个特征的数据集而言,线性模型可以非常强大。特别的,如果特征数量大于训练数据点的数量,任何目标 y 都可以(在训练集上)用线性函数完美拟合。
有许多不同的线性回归模型。这些模型之间的区别在于如何从训练数据中学习参数 w 和 b,以及如何控制模型复杂度。
线性回归,或者普通最小二乘法(ordinary least squares,OLS),是回归问题最简单也最经典的线性方法。线性回归寻找参数 w 和 b,使得对训练集与真实的回归目标值 y 之间的均方误差最小。均方误差(mean squared error)是预测值与真实值之差的平方和除以样本数。线性回归没有参数,这是一个优点,但也因此无法控制模型的复杂度。
下列代码可以生成上图的模型:
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_))
# lr.coef_ : [0.39390555]
print("lr.intercept_ : {}".format(lr.intercept_))
# 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)))
# Training set score: 0.67
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
# Test set score: 0.66
R2 约为 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)
比较一下训练集和测试集的分数就可以发现,我们在训练集上的预测非常准确,但测试集上的 R2 要低得多:
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
# Training set score: 0.95
print("Test set score: {:.2f}".format(lr.score(X_test, y_test)))
# 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)))
# Training set score: 0.89
print("Test set score: {:.2f}".format(ridge.score(X_test, y_test)))
# Test set score: 0.75
可以看出,Ridge 在训练集上的分数要低于 LinearRegression,但在测试集上的分数更高。线性回归对数据存在过拟合。Ridge 是一种约束更强的模型,所以更不容易过拟合。复杂性更小的模型意味着在训练集上的性能更差,但泛化性能更好。
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)))
# Training set score: 0.79
print("Test set score: {:.2f}".format(ridge10.score(X_test, y_test)))
# 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)))
# Training set score: 0.93
print("Test set score: {:.2f}".format(ridge01.score(X_test, y_test)))
# Test set score: 0.77
这里 alpha=0.1 似乎效果不错。我们可以尝试进一步减小 alpha 以提高泛化能力。
我们还可以查看 alpha 取不同值时模型的 coef_ 属性,从而更加定性地理解 alpha 参数是如何改变模型的。更大的 alpha 表示约束更强的模型,所以我们预计大 alpha 对应的 coef_ 元素比小 alpha 对应的 coef_ 元素更小。
plt.plot(ridge.coef_, 's', label="Ridge alpha=1")
plt.plot(ridge10.coef_, "^", label='Rifge alpha=10')
plt.plot(ridge01.coef_, "v", label="Ridge alpha=0.1")
plt.plot(lr.coef_, 'o', label="LinearRegression")
plt.xlabel("Cofficient index")
plt.ylabel("Cofficient magnitude")
plt.hlines(0, 0, len(lr.coef_))
plt.ylim(-25, 25)
plt.legend()
这里 x 轴对应 coef_ 的元素:x = 0 对应第一个特征的系数,x = 1 对应第二个特征的系数,以此类推,一直到 x = 100。y 轴表示该系数的具体数值。这里需要记住的是,对于 alpha = 10,系数大约在 -3 和 3 之间。对于 alpha = 1 的 Ridge 模型,系数要稍大一些。对于 alpha = 0.1,点的范围更大。对于没有做正则化的线性回归(即 alpha = 0),点的范围很大,许多点都超出了图像的范围。
还有一种方法可以用来理解正则化影响,就要固定 alpha 值,但改变训练数据量。对于下图,我们对波士顿房价数据集做二次抽样,并在数据量逐渐增加的子数据集上分别对 LinearRegression 和 Ridge(alpha = 1)两个模型进行评估(将模型性能作为数据集大小的函数进行绘图,这样的图像叫做学习曲线):
mglearn.plots.plot_ridge_n_samples()
正如所预测的那样,无论是岭回归还是线性回归,所有数据集大小对应的训练分数都要高于测试分数。由于岭回归是正则化的,因此它的训练分数要整体低于线性回归的训练分数。但岭回归的测试分数要更高,特别是对较小的子数据集。如果少于 400 个数据点,线性回归学不到任何内容。随着模型可用的数据越来越多,两个模型的性能都在提升,最终线性回归能追上了岭回归。这里需要注意的是,如果有足够多的训练数据,正则化变得不那么重要,并且岭回归和线性回归将具有相同的性能。上图还有一个有趣之处,就是线性回归的训练性能在下降。如果添加更多的数据,模型将更加难以过拟合或记住所有的数据。
除了 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)))
# Training set score: 0.29
print("Test set score: {:.2f}".format(lasso.score(X_test, y_test)))
# Test set score: 0.21
print("Number of feature used: {}".format(np.sum(lasso.coef_ != 0)))
# Number of feature used: 4
Lasso 在训练集和测试集上的表现都很差。这表示存在欠拟合,我们发现模型只用到了 105 个特征中的 4 个。与 Ridge 类似,Lasso 也有一个正则化参数 alpha,可以控制系数趋向于 0 的强度。在上一个例子中,我们用的是默认值 alhpa = 1.0。为了降低欠拟合,我们尝试减少 alpha。这么做的同时,我们还需要增加 max_iter 的值(运行迭代的最大次数):
# 我们增大 max_iter 的值,否则模型会警告我们,说应该增大 max_iter
lasso001 = Lasso(alpha=0.01, max_iter=10000).fit(X_train, y_train)
print("Training set score: {:.2f}".format(lasso001.score(X_train, y_train)))
# Training set score: 0.90
print("Test set score: {:.2f}".format(lasso001.score(X_test, y_test)))
# Test set score: 0.77
print("Number of feature used: {}".format(np.sum(lasso001.coef_ != 0)))
# Number of feature used: 33
apha 值变小,我们可以拟合一个更复杂的模型,在训练集上测试集上的表现也更好。模型性能比使用 Ridge 时略好一点,而且我们只用到了 105 个特征中的 33 个。这样的模型可能更容易理解一些。
但如果把 alpha 设得太小,那么就会消除正则化的效果,并出现过拟合,得到与 LinearRegression 类似的结果。
lasso00001 = Lasso(alpha=0.0001, max_iter=10000).fit(X_train, y_train)
print("Training set score: {:.2f}".format(lasso00001.score(X_train, y_train)))
# Training set score: 0.95
print("Test set score: {:.2f}".format(lasso00001.score(X_test, y_test)))
# Test set score: 0.65
print("Number of feature used: {}".format(np.sum(lasso00001.coef_ != 0)))
# Number of feature used: 97
再次像之前那样对不同模型的系数进行作图,如下:
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=1')
plt.xlabel("Cofficient index")
plt.ylabel("Cofficient magnitude")
plt.ylim(-25, 25)
plt.legend(ncol=2, loc=(0, 1.05))
在 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 正则化。
线性模型也广泛应用于分类问题。我们先来看看二分类。这时可以利用下面的公式进行预测:
y ^ = w [ 0 ] ∗ x [ 0 ] + w [ 1 ] ∗ x [ 1 ] + . . . + w [ p ] ∗ x [ p ] > 0 \hat{y}=w[0]*x[0]+w[1]*x[1]+...+w[p]*x[p]>0 y^=w[0]∗x[0]+w[1]∗x[1]+...+w[p]∗x[p]>0
这个公式看起来与线性回归的公式非常类似,但我们没有返回特征的加权求和,而是为预测设置了阈值(0)。如果函数值小于 0,我们就预测类别 -1;如果函数值大于 0,我们就预测类别 +1。对于所有用于分类的线性模型,这个预测规则都是通用的。同样,有很多种不同的方法来找出系数(w)和截距(b)。
对于用于回归的线性模型,输出 y 是特征的线性函数,是直线、平面或超平面(对于更高维的数据集)。对于用于分类的线性模型,决策边界是输入的线性函数。换句话说,(二元)线性分类器是利用直线、平面或超平面来分开两个类别的分类器。
学习线性模型有很多种算法。这些算法的区别在于以下两点:
最常见的两种分类算法是 Logistic 回归(logistic regression)和线性支持向量机(线性 SVM),前者在 linear_model.LogisticRegression 中实现,后者在 svm.LinearSVC(SVC 代表支持向量分类器)中实现。
我们将 Logistic 回归和线性支持向量机模型应用到 forge 数据集上,并将线性模型找到的决策边界可视化:
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
X, 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("Featue 1")
axes[0].legend()
两个模型都默认使用 L2 正则化。对于 LogisticRegression 和 LinearSVC,决定正则化强度的权衡参数叫做 C。C 值越大,对应的正则化越弱。换句话说**,如果参数 C 值较大,那么 LogisticRegression 和 LinearSVC 将尽可能将训练集拟合到最好**,而如果 C 值较小,那么模型更强调使系数向量(w)接近于 0。
参数 C 的作用还有另一个有趣之处。较小的 C 值可以让算法尽可能适应 “大多数” 数据点,而较大的 C 值更强调每个数据点都分类正确的重要性。如下图:
mglearn.plots.plot_linear_svc_regularization()
在左侧的图中,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)))
# Training set score: 0.941
print("Test set score: {.3f}".format(logreg.score(X_test, y_test)))
# Test set score: 0.965
C=1 的默认值给出了相当好的性能,在训练集和测试集上都达到了 94% 的精度。但由于训练集和测试集的性能非常接近,所以模型很可能欠拟合。我们尝试增大 C 来拟合一个更灵活的模型:
logreg100 = LogisticRegression(C=100).fit(X_train, y_train)
print("Training set score: {:.3f}".format(logreg100.score(X_train, y_train)))
# Training set score: 0.951
print("Test set score: {:.3f}".format(logreg100.score(X_test, y_test)))
# Test set score: 0.958
我们还可以研究一下正则化更强的模型时会发生什么。设置 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)))
# Training set score: 0.937
print("Test set score: {:.3f}".format(logreg001.score(X_test, y_test)))
# Test set score: 0.930
最后,来看一下正则化参数 C 取三个不同的值时模型学到的系数:
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.01")
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()
如果你想要一个可解释性更强的模型,使用 L1 正则化可能更好,因为它约束模型只能使用很少数几个特征。下面使用 L1 正则化的系数图像和分类精度:
for C, marker in zip([0.001, 1, 100], ['o', '^', 'v']):
lr_l1 = LogisticRegression(C=C, penalty="l2").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)
'''
Training accuracy of l1 logreg with C=0.001: 0.95
Test accuracy of l1 logreg with C=0.001: 0.94
Training accuracy of l1 logreg with C=1.000: 0.95
Test accuracy of l1 logreg with C=1.000: 0.97
Training accuracy of l1 logreg with C=100.000: 0.96
Test accuracy of l1 logreg with C=100.000: 0.95
'''
用于二分类的线性模型与用于回归的线性模型有许多相似之处。与用于回归的线性模型一样,模型的主要差别在于 penalty 参数,这个参数会影响正则化,也会影响模型是使用所有可用特征还是只选择特征的一个子集。
参数
线性模型的主要参数就是正则化参数,在回归模型中叫做 alpha,在 LinearSVC 和 LogisticRegression 中叫做 C。alpha 值较大或 C 值较小,说明模型比较简单。特别对于回归而言,调节这些参数非常重要。
你还需要确定的是用 L1 正则化还是 L2 正则化。如果你假定只有几个特征是真正重要的,那么你应该用 L1 正则化,否则应默认使用 L2 正则化。如果模型的可解释性很重要的话,使用 L1 也会有帮助。由于 L1 只用到几个特征,所以更容易解释哪些特征对模型是重要的,以及这些特征的作用。
优点
线性模型的训练速度非常快,预测速度也很快。这种模型可以推广到非常大的数据集,对稀疏数据也很有效。如果你的数据包含数十万甚至百万个样本,你可能需要研究如何使用 LogisticRegression 和 Ridge 模型中的 solver=‘sag’ 选项,在处理大数据时,这一选型可能比默认值更快。
另一个优点是用于回归或分类的公式,很容易理解。
如果特征数量大于样本数量,线性模型的表现通常都很好。它通常用于非常大的数据集。
缺点
承上。但是,往往并不完全清楚系数为什么是这样的。如果你的数据中有高度相关的特征,这一问题尤其突出