多项式回归
多项式回归使用线性回归的基本思路
非线性曲线如图:
假设曲线表达式为:$y=ax^2+bx+c$,如果将 $x^2$ 看作为 $x_1$,即 $y_1=ax_1+bx+c$,此时就有了两个特征,则可以看作是线性曲线表达式。
首先生成一组样本数据:
import numpy as np
import matplotlib.pyplot as plt
x = np.random.uniform(-3, 3, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x**2 + x + 2 + np.random.normal(0, 1, size=100)
(x, y) 如图所示(横轴为 x,纵轴为 y):
接着在 $X$ 的基础上增加一个新的特征 $x1(x^2)$ 形成一个新的 $X2$:
X2 = np.hstack([X, X**2])
再使用线性回归算法:
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X2, y)
y_predict = lin_reg.predict(X2)
此时对 $X2$ 的预测值反映到图中就是第一张图里的曲线。
PolynomialFeatures 和 Pipeline
对于增加新的特征(如:$x^2$),Scikit Learn 提供了 PolynomialFeatures,使用方式如下:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2)
poly.fit(X)
X2 = poly.transform(X)
参数 degree
表示最高次幂;得到的新的 $X2$ 前5行数据如下:
# X2[:5,:]
array([[ 1. , 1.16207716, 1.35042333],
[ 1. , -2.62969804, 6.91531181],
[ 1. , 0.99966958, 0.99933928],
[ 1. , 0.35525362, 0.12620514],
[ 1. , -2.48933626, 6.19679503]])
第一列为 $x^0$,第二列为 $x$,第三列为 $x^2$。
Scikit Learn 还提供了 Pipeline,将多项式特征、数据归一化和线性回归组合在了一起,大大方便的编程的过程。使用方式如下:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
poly_regression = Pipeline([
("poly", PolynomialFeatures(degree=2)),
("std_scaler", StandardScaler()),
("lin_reg", LinearRegression())
])
Pipeline()
传入的是一个列表,包含了执行每一个步骤的实例,每一个步骤又是一个元组类型,其中第二个表示实例,第一个表示给实例取的名称。接着就可以进行 fit()
、predict()
等内容了:
poly_regression.fit(X, y)
y_predict = poly_regression.predict(X)
欠拟合和过拟合
欠拟合和过拟合的理解
在使用多项式回归的过程中需要考虑一个问题,即欠拟合和过拟合。
如果对前面的样本单单使用线性回归,得到的模型如图(这里省略的代码实现):
训练出来的模型很简单,但它并不能完整的表述数据之间的关系,这就是欠拟合(underfitting)。
如果使用多项式回归,代码如下:
def PolynomialRegression(degree):
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("lin_reg", LinearRegression())
])
poly100_reg = PolynomialRegression(degree=100)
poly100_reg.fit(X, y)
y_predict100 = poly100_reg.predict(X)
将 degree
设置为100,即最高次幂为 100,训练出来的模型对训练数据 X 的预测结果如图:
可见该模型对训练数据解释的很好,但是如果用测试数据来预测一下:
X_plot = np.linspace(-3, 3, 100).reshape(-1, 1)
y_plot = poly100_reg.predict(X_plot)
可以看出对预测数据预测的非常糟糕,这是因为训练的模型过多的表达了训练数据中的噪音,从而造成了对预测数据的结果也含有了寻多噪音,这就是过拟合(overfitting)。
模型复杂度曲线
上面的过拟合出来的模型对训练数据解释的很好,但对测试数据(新的数据)解释的非常差,也就是模型的泛化能力差。
所以为了防止模型过拟合,通常将数据分为训练数据和测试数据,通过测试数据来检验是否过拟合。一般模型准确率与训练数据和测试数据的关系为:
图形中左边属于欠拟合,右边属于过拟合,而中间对于测试数据模型准确率高的地方就是模型泛化能力好的地方。
学习曲线
除了模型复杂度曲线,还可以使用学习曲线来可视化欠拟合和过拟合。
所谓学习曲线,就是随着训练样本的逐渐增多,训练出的模型的能力的变化。
首先将数据分为训练数据和测试数据:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y)
learning_curve()
函数中使用均方误差来表示模型的优劣,并且训练数据(75个)从1个慢慢增大为75个,记录对训练数据和测试数据预测值的均方误差,用于画学习曲线。
from sklearn.metrics import mean_squared_error # 均方误差
def learning_curve(algo, X_train, X_test, y_train, y_test):
train_score = []
test_score = []
for i in range(1, 76):
algo.fit(X_train[:i], y_train[:i])
y_train_predict = algo.predict(X_train[:i])
train_score.append(mean_squared_error(y_train[:i], y_train_predict))
y_test_predict = algo.predict(X_test)
test_score.append(mean_squared_error(y_test, y_test_predict))
先来看看使用线性回归的情况(根据前文已经知道是欠拟合):
learning_curve(LinearRegression(), X_train, X_test, y_train, y_test)
作出的学习曲线如图:
可以看出均方误差最后趋于稳定。
接着来看看使用多项式回归并且设定最高次幂为2的情况:
poly2_reg = PolynomialRegression(degree=2)
learning_curve(poly2_reg, X_train, X_test, y_train, y_test)
作出的学习曲线如图:
可以看出均方误差最后也趋于稳定,但是比过拟合情况下均方误差的值要小,表示模型更好。
接着来看看使用多项式回归并且设定最高次幂为20的情况:
poly20_reg = PolynomialRegression(degree=20)
learning_curve(poly20_reg, X_train, X_test, y_train, y_test)
作出的学习曲线如图:
可以看出对训练数据的军方误差最后会趋于稳定,但对测试数据则不然,这种情况就是过拟合的表现。
验证数据集和交叉验证
虽然将数据集划分为训练数据集和测试数据集能够为判断模型是否过拟合提供参考,但这样的划分方式并不严谨,因为模型可能是针对测试数据集过拟合的。
更好的方式是将数据集划分为训练数据集、验证数据集和测试数据集。验证数据集用于验证模型的效果,方便调整超参数来改善模型;测试数据集用于衡量最终的模型性能。
但是这种方式也有可能对验证数据集过拟合,此时可以使用交叉验证(Cross Validation)。
k-folds 交叉验证
交叉验证即将数据集划分为训练数据集和测试数据集,并将训练数据集分成 k 份,每次将其中一份作为验证数据集,剩下的(k-1)份作为训练数据集,如此可以得到 k 个模型,再将这 k 个模型的均值作为结果调参。以 kNN 手写数字识别算法举例说明:
首先准备数据:
from sklearn import datasets
from sklearn.model_selection import train_test_split
digits = datasets.load_digits()
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
Scikit Learn 中提供了用于交叉验证的 cross_val_score
,模型将数据分成三份用于交叉验证,返回三个模型的估算分数:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
knn_clf = KNeighborsClassifier()
cross_val_score(knn_clf, X_train, y_train)
# 返回 array([0.9640884 , 0.97506925, 0.96901408])
接着进行调参:
best_score, best_p, best_k = 0, 0, 0
for k in range(2, 10):
for p in range(1, 5):
knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=k, p=p)
scores = cross_val_score(knn_clf, X_train, y_train)
score = np.mean(scores)
if score > best_score:
best_score, best_p, best_k = score, p, k
print("best score is:", best_score)
print("best p is:", best_p)
print("best k is:", best_k)
# best score is: 0.9795997710242826
# best p is: 3
# best k is: 2
循环中每次比较 cross_val_score
返回数组的均值,最大的对应的 k 和 p 就是最优的超参数。
拿到了想要的超参数就可以进行模型训练了:
best_knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=2, p=3)
best_knn_clf.fit(X_train, y_train)
best_knn_clf.score(X_test, y_test)
# 结果0.9902642559109874
网格搜索
Scikit Learn 提供了网格搜索(GridSearchCV)整合了上面交叉验证和调参的全过程:
from sklearn.model_selection import GridSearchCV
param_grid = [
{
'weights': ['distance'],
'n_neighbors': [i for i in range(1, 11)],
'p': [i for i in range(1, 6)]
}
]
knn_clf = KNeighborsClassifier()
grid_search = GridSearchCV(knn_clf, param_grid=param_grid, verbose=1)
grid_search.fit(X_train, y_train)
grid_search.best_params_
返回得到的最优超参数;best_knn_clf = grid_search.best_estimator_
返回最优的模型。
在cross_val_score
和GridSearchCV
中可以指定参数cv
来设置将训练数据集分为几份(默认为3)。
方差处理
偏差和方差
对于一个模型而言,模型误差=偏差(Bias)+方差(Variance)+不可避免的误差。偏差和方差表示如图
导致偏差的主要原因是对问题本身的假设不正确,导致方差的主要原因是使用的模型太复杂。对于欠拟合,就属于高偏差;而过拟合,就属于高方差。
在机器学习算法中,主要的挑战来自方差,解决的方法主要有:
- 降低模型复杂度;
- 降维;
- 增加样本数;
- 使用验证集;
- 模型正则化。
接下来主要看看模型正则化。
模型正则化
对于高方差的模型可以用模型正则化(Regularization)处理,限制参数的大小。
以使用梯度下降法的线性回归为例,其目标函数为:使 $J(\theta) = MSE(y, \hat{y};\theta)$ 尽可能小;如果模型过拟合,得到的 $\theta$ 就可能非常大,因此需要对 $J(\theta)$ 加入限制是的 $\theta$ 尽可能小。
有两种主要方式:岭回归和 LASSO 回归。
岭回归
岭回归(Ridge Regression)就是在目标函数中加入了 $\alpha\frac{1}{2}\sum_{i=1}^n\theta_i^2$,即使 $J(\theta) = MSE(y, \hat{y};\theta)+\alpha\frac{1}{2}\sum_{i=1}^n\theta_i^2$ 尽可能小。
Scikit Learn 中提供了 Ridge
类表示岭回归,参数为 $\alpha$。使用过程如下:
from sklearn.linear_model import Ridge
x = np.random.uniform(-3, 3, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x + 3 + np.random.normal(0, 1, size=100)
def PolynomialRegression(degree, alpha):
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("ridge_reg", Ridge(alpha=alpha))
])
设置 degree
为20,alpha
为0.0001 训练模型:
ridge_reg = PolynomialRegression(20, 0.0001)
ridge_reg.fit(X_train, y_train)
y_predict = ridge_reg.predict(X_test)
画出来的图形为:
设置 degree
为20,alpha
为1 训练模型:
ridge_reg = PolynomialRegression(20, 1)
ridge_reg.fit(X_train, y_train)
y_predict = ridge_reg.predict(X_test)
画出来的图形为:
相比之下模型好上了不少。
LASSO 回归
LASSO 回归(Least Absolute Shrinkage and Selection Operator Regression)与岭回归不同的是使用 $\alpha\sum_{i=1}^n|\theta_i|$ 对 $\theta$ 进行限制,即使 $J(\theta) = MSE(y, \hat{y};\theta)+\alpha\sum_{i=1}^n|\theta_i|$ 尽可能小。
Scikit Learn 中提供了 Lasso
类表示岭回归,参数为 $\alpha$。使用过程如下:
from sklearn.linear_model import Lasso
def LassoRegression(degree, alpha):
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("lasso_reg", Lasso(alpha=alpha))
])
设置 degree
为20,alpha
为0.01 训练模型:
lasso_reg = PolynomialRegression(20, 0.01)
lasso_reg.fit(X_train, y_train)
y_predict = lasso_reg.predict(X_test)
画出来的图形为:
设置 degree
为20,alpha
为0.1 训练模型:
lasso_reg = PolynomialRegression(20, 0.1)
lasso_reg.fit(X_train, y_train)
y_predict = lasso_reg.predict(X_test)
画出来的图形为:
相比之下模型也好上了不少。
LASSO 回归相比岭回归趋向于使得一部分 $\theta$ 等于0,因此画出来的曲线也相对更直一些。
弹性网
弹性网(Elastic Net)则同时使用了岭回归和LASSO 回归,即使用了 $\alpha\frac{1-r}{2}\sum_{i=1}^n\theta_i^2 +r \alpha\sum_{i=1}^n|\theta_i|$ 对 $\theta$ 进行限制。
使用上面的例子就是使 $J(\theta) = MSE(y, \hat{y};\theta)+\alpha\frac{1-r}{2}\sum_{i=1}^n\theta_i^2 +r \alpha\sum_{i=1}^n|\theta_i|$ 尽可能小。
源码地址
Github | ML-Algorithms-Action