多项式回归引入
在线性回归中我们可以用一条直线来拟合具有线性关系的数据,但是并不是所有数据又具有线性关系。
而对于这些非线性关系我们就可以尝试使用多项式回归来进行拟合。
比如有一个呈现出如下关系的数据集。
我们可以发现,如果我们使用一个二次曲线来进行拟合会有更好的效果,所以此时我们就需要进行多项式回归。
回忆对于线性回归,我们是设出了一条直线
y
^
=
θ
0
+
x
1
θ
1
+
x
2
θ
2
+
.
.
.
x
n
θ
n
\hat y=\theta_0+x_1\theta_1 + x_2\theta_2 + ...x_n\theta_n
y^=θ0+x1θ1+x2θ2+...xnθn
同样的对于上面的数据我们也可以射出一条曲线
y
^
=
θ
0
+
x
1
θ
1
+
x
2
θ
2
\hat y=\theta_0 + x_1\theta_1 + x^2\theta_2
y^=θ0+x1θ1+x2θ2
对于这个
x
2
x^2
x2平方,我们就可以把样本的唯一的一个特征进行平方作为
x
2
x^2
x2使用。
然后再看整个式子就会发现其实还是
θ
\theta
θ是变量此时就变得和求解线性回归的思路一样。
多项式回归
以上面的数据举例,我们把它的唯一一个特征进行平方,并且加入训练数据集中。
X = np.hstack([X, X * X])
然后我们使用线性回归模型,对其进行训练,并且绘图。
liner = LinearRegression()
liner.fit(X, y)
plt.scatter(X[:, 0], y)
plt.plot(X[:, 0], liner.predict(X), color='r')
plt.show()
可以发现,这样就很轻松地训练出了一条二次曲线来训练拟合这个数据。
假设训练数据的特征有多个呢?
假设有两个
x
1
,
x
2
x_1, x_2
x1,x2
那么我们设出的函数应该是
y
^
=
θ
0
+
θ
1
x
1
+
θ
2
x
2
+
θ
3
x
1
x
2
+
θ
4
x
1
2
+
θ
5
x
2
2
\hat y = \theta_0 + \theta_1 x_1 + \theta_2 x_2 + \theta_3x_1x_2 + \theta_4x_1^2 + \theta_5x_2^2
y^=θ0+θ1x1+θ2x2+θ3x1x2+θ4x12+θ5x22
可以发现,首先把所有阶数为0的组合列举了一遍,然后把阶数为1的列举了一遍,最后把所有阶数为2的列举了一遍。
由此我们可以推广到n个特征,m阶的多项式。
sklearn的多项式回归
sklearn没有直接封装多项式回归,但是它给出了求出任意阶(度)的的数据集的方法。
from sklearn.preprocessing import PolynomialFeatures
我们创建一个PolynomialFeature的实例,初始化的参数就是上面说所的阶数,也是度,我们设为2。
然后用fit去拟合,再transForm
poly = PolynomialFeatures(degree=2)
poly.fit(X)
X = poly.transform(X)
print(X)
"""
output:
[[ 1.00000000e+00 -5.00000000e+00 2.50000000e+01]
[ 1.00000000e+00 -4.80000000e+00 2.30400000e+01]
[ 1.00000000e+00 -4.60000000e+00 2.11600000e+01]
[ 1.00000000e+00 -4.40000000e+00 1.93600000e+01]
...
"""
看输出,第一列是常数的列和线性回归一致,第三列就是
X
2
X^2
X2。
在sklearn我们还可以使用pipline这种方式来创建一个多项式回归。
pipline = Pipeline(
# 一个列表,里面是元组,元组里是名字+实例的格式
[("poly", PolynomialFeatures(degree=2)), # 数据的处理
("scaler", StandardScaler()), # 归一化方式
("liner", LinearRegression())] # 回归方式
)
然后这个pipline就可以当做是一个多项式回归来使用了,可以使用fit和predict等等。
过拟合与欠拟合
欠拟合
欠拟合简单的来说就是训练出的模型不能很好的拟合数据。
比如我们使用一根直线来拟合上面的数据
这样做很明显与我们的数据偏差很大,通常这种情况是使用的模型太过简单。
过拟合
如果我们把阶数调整到50会发什什么事情呢?
可以看到,把阶数调整到50时曲线变成了十分扭曲的样子,这样的曲线显然不能反映出数据的一般规律。
但是这样的数据在训练数据集上的误差很小,原理很简单,当阶数升高后,就多了很多的参数可以调节,总会能找到一些参数让预测的曲线经过样本的每一个点。
(我们知道多项式知道可以多项式来拟合某个小的范围内的任意函数)
这样的事情在机器学习中是很长遇到的,处理过拟合的情况是机器学习的一个常见的事。
过拟合通常偏差很小,方差却比较大
验证数据集与学习曲线
验证数据集
为了检查是否有过拟合的情况发生,我们可以使用测试训练数据集分离的手段。
我们可以把数据集分成训练数据集与测试数据集,然后使用训练数据集进行训练,使用测试数据集进行测试。
如果我们的模型能够在训练数据集上拟合地很好,但是却不能在测试数据集上拟合地很好,那么我们就可以说我们的模型的泛化能力不行,可能发生过拟合,此时需要对模型进行参数调整。
但是如果我们只分成测试数据集和训练数据集,我们进行调参的过程有可能会对测试数据集发生过拟合,此时无论是测试数据集还是训练数据集,模型都能拟合地很好。
于是我们需要再分出来一个数据集叫做验证数据集,这个数据集用来调节我们的模型。
- 训练数据集(训练用)
- 验证数据集(调参用)
- 测试数据集(用来衡量模型的性能)
学习曲线
描绘学习曲线,可以更直观的看出模型是否发生过拟合。
学习曲线是指在随着训练数据的逐渐增多,算法所训练出的模型的表现能力。
我们把数据集分成测试数据集和训练数据集,然后从1开始逐渐增加训练的数据,并且用一个列表记录下随着训练数据增多算法训练出的模型在训练数据集和测试数据集的score。
- 建立多项式回归并且分离数据
poly = Pipeline(
[("poly", PolynomialFeatures(degree=2)),
("scaler", StandardScaler()),
("liner", LinearRegression())]
)
X_train, X_test, y_train, y_test = trainTestSplit(data, target, train_ratio=train_ratio)
- 循环训练并记录
train_score = [] # 用于记录train
test_score = [] # 用于记录score
for i in range(1, len(X_train) + 1):
poly.fit(X_train[:i], y_train[: i])
train_score.append(poly.score(X_train[: i], y_train[: i]))
test_score.append(poly.score(X_test, y_test))
- 可视化
plt.plot([i + 1 for i in range(len(train_score))], train_score, label='train')
plt.plot([i + 1 for i in range(len(train_score))], test_score, label='test')
plt.legend()
plt.show()
最终得到的结果如上,可以看到随着训练数据的增多,测试数据的准确率逐渐上升,训练数据和测试数据的准确率逐渐接近。
假设把阶数设成20,再来描绘一下学习曲线,就会是另一种样子。
可以发现,train和test之间的准去率的差距会变大,这种情况的泛化能力显然不如第一种,所以发生了过拟合。
交叉验证法
交叉验证法是一种比较靠谱的评估模型的方法。
交叉验证法的做法是把数据集分成大小相近的k份,每次都取出k-1份作为训练集,然后剩下的一份作为测试集,这样重复k次得出k个结果,最后取平均得出最终的得分。
交叉验证法的这种做法可以比较充分的利用数据集,同时经过多次训练取平均也可以避免某次因为随机出的特殊数据比较集中而导致的评分结果偏差很大。
通常把数据集分成k份就叫k折交叉验证法,特别的把大小为m数据集分成m份就叫留一法,留一法可以充分的利用所有的数据,但是代价是高额的计算量。
我们可以使用sklearn的KFold来把数据切分成k份。
from sklearn.model_selection import KFold
kf = KFold(n_splits=6, shuffle=True, random_state=114514)
# 返回一个迭代器
"""
n_splits: 切分成的份数
shuffle: 是否打乱
random_state: 如果要打乱,随机数的种子是多少
'""
我们对其进行迭代,每次迭代产生两个数据,一个是训练数据的下标,一个是测试数据的下标,我们可以直接拿它来训练,最后把训练的得分取平均。
同时我们也可以使用sklearn直接封装的交叉验证法。
from sklearn.model_selection import cross_val_score
print(cross_val_score(liner, X, y, cv=3))
# cv参数表示分组
"""
[-9.67838052 -1.40508806 -2.67292825]
"""
模型正则化
模型正则化
模型正则化是对算法进行修改,对参数的大小加以限制,从而提高训练出的模型的泛化能力的一种方法。
方差与偏差
模型误差 = 偏差 + 方差 + 噪声
这里就要去研究一下什么是偏差什么是方差了。
首先假设几个变量。
测试样本
x
x
x,
x
x
x在数据中的标记为
y
d
y_d
yd,
y
y
y为
x
x
x的真真实标记,
f
(
x
;
D
)
f(x; D)
f(x;D)为在训练数据集D上学到的模型在
x
x
x测试样本中的预测值。
简单来说:
y
y
y->实际值
y
d
y_d
yd->样本所给出的值
f
(
x
;
D
)
f(x; D)
f(x;D)模型所预测出来的值
那就可以定义出学习算法所预测出的期望了。
f
ˉ
(
x
)
=
E
D
[
f
(
x
;
D
)
]
\bar f(x) =E_D[f(x; D)]
fˉ(x)=ED[f(x;D)]
有了期望就可以定义出方差
v
a
r
(
x
)
=
E
D
[
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
2
]
var(x)=E_D[(f(x;D) - \bar f(x))^2]
var(x)=ED[(f(x;D)−fˉ(x))2]
此时也可以设出噪音,即真实的标记与样本数据中的标记的差的的平方和。
w
2
=
E
D
[
(
y
d
−
y
)
2
]
w^2=E_D[(y_d - y)^2]
w2=ED[(yd−y)2]
偏差就定义为,真实的标记与期望的输出的差的平方和。
b
i
a
s
2
(
x
)
=
(
f
ˉ
(
x
)
−
y
)
2
bias^2(x)=(\bar f(x) - y)^2
bias2(x)=(fˉ(x)−y)2
而我们模型的误差实际上模型的预测值与样本的标记值之间的误差,即
E
(
f
;
D
)
=
E
D
[
(
f
(
x
;
D
)
−
y
d
)
2
]
=
E
D
[
(
f
(
x
;
D
)
+
f
ˉ
(
x
)
−
f
ˉ
(
x
)
+
y
d
)
2
]
=
E
D
[
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
2
]
+
E
D
[
(
y
d
−
f
ˉ
(
x
)
)
2
]
+
E
D
[
2
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
(
f
ˉ
(
x
)
−
y
d
)
]
E(f;D)=E_D[(f(x;D) - y_d)^2]\\ =E_D[(f(x; D) + \bar f(x) - \bar f(x) + y_d)^2]\\ =E_D[(f(x;D) - \bar f(x))^2] + E_D[(y_d - \bar f(x))^2] + E_D[2(f(x;D) - \bar f(x))(\bar f(x) - y_d)]
E(f;D)=ED[(f(x;D)−yd)2]=ED[(f(x;D)+fˉ(x)−fˉ(x)+yd)2]=ED[(f(x;D)−fˉ(x))2]+ED[(yd−fˉ(x))2]+ED[2(f(x;D)−fˉ(x))(fˉ(x)−yd)]
由于
E
D
[
2
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
(
f
ˉ
(
x
)
−
y
d
)
]
=
2
{
E
d
[
f
(
x
;
D
)
f
ˉ
(
x
)
]
−
E
D
[
y
d
f
(
x
;
D
)
]
−
E
D
[
f
ˉ
(
x
)
2
]
+
E
D
[
y
d
f
ˉ
(
x
)
]
}
=
2
[
f
ˉ
(
x
)
2
−
y
d
f
ˉ
(
x
)
−
f
ˉ
(
x
)
2
+
y
d
f
ˉ
(
x
)
]
=
0
E_D[2(f(x;D) - \bar f(x))(\bar f(x) - y_d)]\\ = 2\{E_d[f(x;D)\bar f(x)] - E_D[y_df(x;D)] -E_D[\bar f(x)^2] + E_D[y_d\bar f(x)]\}\\ =2[\bar f(x)^2 - y_d\bar f(x) -\bar f(x)^2 + y_d\bar f(x)]\\ =0
ED[2(f(x;D)−fˉ(x))(fˉ(x)−yd)]=2{Ed[f(x;D)fˉ(x)]−ED[ydf(x;D)]−ED[fˉ(x)2]+ED[ydfˉ(x)]}=2[fˉ(x)2−ydfˉ(x)−fˉ(x)2+ydfˉ(x)]=0
所以原式等于
E
D
[
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
2
]
+
E
D
[
(
y
d
−
f
ˉ
(
x
)
)
2
]
=
v
a
r
(
x
)
+
E
D
[
(
y
d
−
y
+
y
−
f
ˉ
(
x
)
)
2
]
=
v
a
r
(
x
)
+
E
D
[
(
y
d
−
y
)
2
]
+
E
D
[
(
y
−
f
ˉ
(
x
)
)
2
]
+
2
E
D
[
(
f
ˉ
(
x
)
−
y
)
(
y
−
y
d
)
]
E_D[(f(x;D) - \bar f(x))^2] + E_D[(y_d - \bar f(x))^2]\\ =var(x) + E_D[(y_d - y + y - \bar f(x))^2]\\ =var(x) + E_D[(y_d - y)^2] + E_D[(y - \bar f(x))^2] + 2E_D[(\bar f(x) - y)(y - y_d)]
ED[(f(x;D)−fˉ(x))2]+ED[(yd−fˉ(x))2]=var(x)+ED[(yd−y+y−fˉ(x))2]=var(x)+ED[(yd−y)2]+ED[(y−fˉ(x))2]+2ED[(fˉ(x)−y)(y−yd)]
假设噪音数据的均值为0则
2
E
D
[
(
f
ˉ
(
x
)
−
y
)
(
y
−
y
d
)
]
=
0
2E_D[(\bar f(x) - y)(y - y_d)]\\ =0
2ED[(fˉ(x)−y)(y−yd)]=0
所以得到原式为
E
(
f
;
D
)
=
E
D
[
(
f
(
x
;
D
)
−
f
ˉ
(
x
)
)
2
]
+
E
D
[
(
y
d
−
y
)
2
]
+
E
D
[
(
y
−
f
ˉ
(
x
)
)
2
]
E(f;D)=E_D[(f(x;D) - \bar f(x))^2] + E_D[(y_d - y)^2] + E_D[(y - \bar f(x))^2]
E(f;D)=ED[(f(x;D)−fˉ(x))2]+ED[(yd−y)2]+ED[(y−fˉ(x))2]
跟进一步的他们就是
E
(
f
;
D
)
=
v
a
r
(
x
)
+
b
i
a
s
(
x
)
2
+
w
2
E(f;D)=var(x) + bias(x)^2 + w^2
E(f;D)=var(x)+bias(x)2+w2
分析各个表达式,我们可以看出,如果偏差越大则说明模型与真实的规律之间有比较大的差距,欠拟合就是这样的例子。
如果方差越大则说明数据与测试数据之间的差距越大,欠拟合就是这样的例子。
通常情况下,方差与偏差是相互冲突的,即一个大另一个就会小,所以就会陷入偏差方差窘境。
用一张图就能更清楚地了解偏差与方差
一个是偏离目标,一个是在目标附近,但是比较松散。
LASSO回归
为了提高模型的泛化能力,我们必须要对算法进行一定的改进。观察过拟合的情况,在过拟合的过程中数据噪音也被模型所学到,导致学习出来的模型存在一些极端情况,为了削弱这种极端的情况我们就可以在训练模型的过程加入一个惩罚的机制,使模型的参数不会过分的大。
线性回归是让
J
(
θ
)
=
1
m
∑
i
=
1
m
(
y
^
(
i
)
−
y
(
i
)
)
2
J(\theta)=\frac{1}{m}\sum\limits_{i = 1}^m(\hat y^{(i)} - y^{(i)})^2
J(θ)=m1i=1∑m(y^(i)−y(i))2最小。
为了防止
θ
\theta
θ的过大,所以可以加上一项
α
∑
i
=
1
n
∣
θ
i
∣
\alpha\sum\limits_{i = 1}^n|\theta_i|
αi=1∑n∣θi∣,即我们把线性回归的损失函数变成
J
(
θ
)
=
1
m
∑
i
=
1
m
(
y
^
(
i
)
−
y
(
i
)
)
2
+
α
∑
i
=
1
n
∣
θ
i
∣
J(\theta)=\frac{1}{m}\sum\limits_{i = 1}^m(\hat y^{(i)} - y^{(i)})^2+\alpha\sum\limits_{i = 1}^n|\theta_i|
J(θ)=m1i=1∑m(y^(i)−y(i))2+αi=1∑n∣θi∣
其中
α
\alpha
α是一个超参数,它可以用来控制惩罚的程度。
可以看出,加了最后一项后,要想使损失函数最小,也就同时需要顾及最小化
θ
\theta
θ的数量级最小。
这就是LASSO回归,通过添加一个惩罚
α
∑
i
=
1
n
∣
θ
i
∣
\alpha\sum\limits_{i = 1}^n|\theta_i|
αi=1∑n∣θi∣来缓和过拟合的情况。
sklearn封装了LASSO回归,我们可以直接调用。
from sklearn.linear_model import Lasso
lasso = Pipeline(
[('poly', PolynomialFeatures(degree=50)),
('scaler', StandardScaler()),
('lasso', Lasso(alpha=0.1))]
)
当阶数为50时得到的图像是这样的。
我们使用LASSO设置alpha=0.01进行绘制
明显缓和了许多,我们继续提高惩罚的强度,设置alpha=5
可以发现此时已经接近是一条二次曲线了,我们继续提高惩罚强度,把alpha=100
可以发现由于惩罚强度太高,导致
α
∑
i
=
1
n
∣
θ
i
∣
\alpha\sum\limits_{i = 1}^n|\theta_i|
αi=1∑n∣θi∣占据主导地位,所以最终使
α
∑
i
=
1
n
∣
θ
i
∣
\alpha\sum\limits_{i = 1}^n|\theta_i|
αi=1∑n∣θi∣趋近于0,也就使其模型变成了一条直线。
LASSO回归还可以用于提取特征,即把一些不太需要的特征的
θ
\theta
θ变为0。
我们画出一张二维参数空间的等高线。
然后画出
∣
θ
1
∣
+
∣
θ
2
∣
|\theta_1| + |\theta_2|
∣θ1∣+∣θ2∣的形状。
我们可以看到对于
∣
θ
1
∣
+
∣
θ
2
∣
|\theta_1| + |\theta_2|
∣θ1∣+∣θ2∣他是有棱角的,也就是在坐标轴位置处离远点的距离最长。
我们在求解线性回归最优解时又想让
∣
θ
1
∣
+
∣
θ
2
∣
|\theta_1| + |\theta_2|
∣θ1∣+∣θ2∣尽可能的小,又想让结果尽可能的接近中心点,所以就要尽可能的往坐标轴处去接近,也就导致某些
θ
\theta
θ的值会变成0.
岭回归
岭回归与LASSO回归很相似,不过它的惩罚措施有所变化,加上岭回归的惩罚措施的线性回归惩罚函数为
J
(
θ
)
=
1
m
∑
i
=
1
m
(
y
^
(
i
)
−
y
(
i
)
)
2
+
1
2
α
∑
i
=
1
n
θ
i
2
J(\theta)=\frac{1}{m}\sum\limits_{i = 1}^m(\hat y^{(i)} - y^{(i)})^2+\frac{1}{2}\alpha\sum\limits_{i = 1}^n\theta_i^2
J(θ)=m1i=1∑m(y^(i)−y(i))2+21αi=1∑nθi2
没错就是把绝对值变成了平方,那它与LASSO回归有什么不同呢?
我们可以绘制图像来辨别
from sklearn.linear_model import Ridge
lasso = Pipeline(
[('poly', PolynomialFeatures(degree=50)),
('scaler', StandardScaler()),
('lasso', Ridge(1))]
)
设置alpha=100
设置alpha=500
设置alpha=10000
我们可以发现,岭回归和LASSO回归一样,都可以缓解过拟合现象,但是与LASSO不同,当惩罚措施非常大时岭回归也不会变成一条直线。
这里是两者的一个区别,岭回归得到的解相对于LASSO比较平滑。
弹性网
弹性网结合了岭回归与LASSO回归,它结合了它们的惩罚方式。
J
(
θ
)
=
1
m
∑
i
=
1
m
(
y
^
(
i
)
−
y
(
i
)
)
2
+
α
(
1
−
λ
)
2
∑
i
=
1
n
θ
i
2
+
α
λ
∑
i
=
1
n
∣
θ
i
∣
J(\theta)=\frac{1}{m}\sum\limits_{i = 1}^m(\hat y^{(i)} - y^{(i)})^2+\alpha\frac{(1-\lambda)}{2}\sum\limits_{i = 1}^n\theta_i^2 +\alpha\lambda\sum\limits_{i = 1}^n|\theta_i|
J(θ)=m1i=1∑m(y^(i)−y(i))2+α2(1−λ)i=1∑nθi2+αλi=1∑n∣θi∣
其中
(
0
≤
λ
≤
1
)
(0\leq\lambda\leq1)
(0≤λ≤1)可以看到,这里是使用
λ
\lambda
λ来调控LASSO和Ridge的比例。
其弹性网的惩罚措施是两者的线性组合。
sklearn中实现了弹性网,可以直接使用。
其中,alpha参数就是上式的
α
\alpha
α,l1_ratio参数就是上式的
λ
\lambda
λ
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import ElasticNet
from sklearn.preprocessing import PolynomialFeatures
elasticnet = Pipeline(
from sklearn.linear_model import ElasticNet
[('poly', PolynomialFeatures(degree=50)),
('scaler', StandardScaler()),
('lasso', ElasticNet(alpha=0.5, l1_ratio=0.8))]
)
kford = KFold(n_splits=20, shuffle=True)
print(np.mean(cross_val_score(elasticnet, X, y, cv=kford)))
"""
0.811233852838664
"""
可以发现,弹性网也很好的缓解了过拟合问题。