线性回归适用于数据呈线性分布的回归问题。如果数据样本呈明显非线性分布,线性回归模型就不再适用(下图左),而采用多项式回归可能更好(下图右)。

多项式模型定义

与线性模型相比,多项式模型引入了高次项,自变量的指数大于1,例如一元二次方程:
$$
y = w_0 + w_1x + w_2x^2
$$
一元三次方程:
$$
y = w_0 + w_1x + w_2x^2 + w_3x ^ 3
$$
推广到一元n次方程:
$$
y = w_0 + w_1x + w_2x^2 + w_3x ^ 3 + … + w_nx^n
$$
上述表达式可以简化为:
$$
y = \sum_{i=1}^N w_ix^i
$$

与线性回归的关系

多项式回归可以理解为线性回归的扩展,在线性回归模型中添加了新的特征值.例如,要预测一栋房屋的价格,有$x_1, x_2, x_3$三个特征值,分别表示房子长、宽、高,则房屋价格可表示为以下线性模型:
$$
y = w_1 x_1 + w_2 x_2 + w_3 x_3 + b
$$
对于房屋价格,也可以用房屋的体积,而不直接使用$x_1, x_2, x_3$三个特征:
$$
y = w_0 + w_1x + w_2x^2 + w_3x ^ 3
$$
相当于创造了新的特征$x, x$ = 长 * 宽 * 高. 以上两个模型可以解释为:

  • 房屋价格是关于长、宽、高三个特征的线性模型
  • 房屋价格是关于体积的多项式模型

因此,可以将一元n次多项式变换成n元一次线性模型.

多项式回归实现

对于一元n次多项式,同样可以利用梯度下降对损失值最小化的方法,寻找最优的模型参数$w_0, w_1, w_2, …, w_n$。可以将一元n次多项式,变换成n元一次线性模型,求线性回归。多项式回归的实现:

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
# 多项式回归示例
import numpy as np
# 线性模型
import sklearn.linear_model as lm
# 模型性能评价模块
import sklearn.metrics as sm
import matplotlib.pyplot as mp
# 管线模块
import sklearn.pipeline as pl
import sklearn.preprocessing as sp

train_x, train_y = [], [] # 输入、输出样本
with open("poly_sample.txt", "rt") as f:
for line in f.readlines():
data = [float(substr) for substr in line.split(",")]
train_x.append(data[:-1])
train_y.append(data[-1])

train_x = np.array(train_x) # 二维数据形式的输入矩阵,一行一样本,一列一特征
train_y = np.array(train_y) # 一维数组形式的输出序列,每个元素对应一个输入样本
# print(train_x)
# print(train_y)

# 将多项式特征扩展预处理,和一个线性回归器串联为一个管线
# 多项式特征扩展:对现有数据进行的一种转换,通过将数据映射到更高维度的空间中
# 进行多项式扩展后,我们就可以认为,模型由以前的直线变成了曲线
# 从而可以更灵活的去拟合数据
# pipeline连接两个模型
model = pl.make_pipeline(sp.PolynomialFeatures(3), # 多项式特征扩展,扩展最高次项为3
lm.LinearRegression())

# 用已知输入、输出数据集训练回归器
model.fit(train_x, train_y)
# print(model[1].coef_)
# print(model[1].intercept_)

# 根据训练模型预测输出
pred_train_y = model.predict(train_x)

# 评估指标
err4 = sm.r2_score(train_y, pred_train_y) # R2得分, 范围[0, 1], 分值越大越好
print(err4)

# 在训练集之外构建测试集
test_x = np.linspace(train_x.min(), train_x.max(), 1000)
pre_test_y = model.predict(test_x.reshape(-1, 1)) # 对新样本进行预测

# 可视化回归曲线
mp.figure('Polynomial Regression', facecolor='lightgray')
mp.title('Polynomial Regression', fontsize=20)
mp.xlabel('x', fontsize=14)
mp.ylabel('y', fontsize=14)
mp.tick_params(labelsize=10)
mp.grid(linestyle=':')
mp.scatter(train_x, train_y, c='dodgerblue', alpha=0.8, s=60, label='Sample')

mp.plot(test_x, pre_test_y, c='orangered', label='Regression')

mp.legend()
mp.show()

打印输出:

1
0.9224401504764776

执行结果:

多项式回归实现

过拟合与欠拟合

什么是欠拟合、过拟合

在上一小节多项式回归示例中,多项特征扩展器PolynomialFeatures()进行多项式扩展时,指定了最高次数为3,该参数为多项式扩展的重要参数,如果选取不当,则可能导致不同的拟合效果。下图显示了该参数分别设为1、20时模型的拟合图像:

线性模型拟合

这两种其实都不是好的模型。前者没有学习到数据分布规律,模型拟合程度不够,预测准确度过低,这种现象称为“欠拟合”;后者过于拟合更多样本,以致模型泛化能力(新样本的适应性)变差,这种现象称为“过拟合”。欠拟合模型一般表现为训练集、测试集下准确度都比较低;过拟合模型一般表现为训练集下准确度较高、测试集下准确度较低。一个好的模型,不论是对于训练数据还是测试数据,都有接近的预测精度,而且精度不能太低。

过拟合、欠拟合比较

第一个模型欠拟合;第三个模型过拟合;第二个模型拟合较好.

如何处理欠拟合、过拟合

  • 欠拟合:提高模型复杂度,如增加特征、增加模型最高次幂等等;
  • 过拟合:降低模型复杂度,如减少特征、降低模型最高次幂等等.

正则化

过拟合还有一个常见的原因,就是模型参数值太大,所以可以通过抑制参数的方式来解决过拟合问题。如上图所示,右图产生了一定程度过拟合,可以通过弱化高次项的系数(但不删除)来降低过拟合。

例如,可以通过在$\theta_3, \theta_4$的系数上添加一定的系数,来压制这两个高次项的系数,这种方法称为正则化。但在实际问题中,可能有更多的系数,且并不知道应该压制哪些系数,所以,可以通过收缩所有系数来避免过拟合。

正则化的定义

正则化是指,在目标函数后面添加一个范数,来防止过拟合的手段,这个范数定义为:

$$
||x||_{p}=(\sum _{i=1}^{N}|x|^{p})^{\frac{1}{p}}
$$

当p=1时,称为L1范数(即所有系数绝对值之和):

$$
||x||_{1}=(\sum _{i=1}^N |x|)
$$

当p=2是,称为L2范数(即所有系数平方之和再开方):

$$
||x||_{2}=(\sum _{i=1}^N |x|^2)^{\frac{1}{2}}
$$

通过对目标函数添加正则项,整体上压缩了参数的大小,从而防止过拟合。

Lasso回归与岭回归

Lasso回归和岭回归(Ridge Regression)都是在标准线性回归的基础上修改了损失函数的回归算法。Lasso回归全称为 Least absolute shrinkage and selection operator,又译“最小绝对值收敛和选择算子”、“套索算法”,其损失函数如下所示:

$$
E = \frac{1}{n}(\sum _{i=1}^N y_i - y_i’)^2 + \lambda ||w||_1
$$

岭回归损失函数为:
$$
E = \frac{1}{n}(\sum _{i=1}^N y_i - y_i’)^2 + \lambda ||w||_2
$$

从逻辑上说,Lasso回归和岭回归都可以理解为通过调整损失函数,减小函数的系数,从而避免过于拟合于样本,降低偏差较大的样本的权重和对模型的影响程度。

线性模型变种模型:

  • 损失函数后面 + 正则项

  • Lasso回归:损失函数 + L1范数

  • 岭回归:损失函数 + L2范数

以下关于Lasso回归于岭回归的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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# Lasso回归和岭回归示例
import numpy as np
# 线性模型
import sklearn.linear_model as lm
# 模型性能评价模块
import sklearn.metrics as sm
import matplotlib.pyplot as mp

x, y = [], [] # 输入、输出样本
with open("abnormal.txt", "rt") as f:
for line in f.readlines():
data = [float(substr) for substr in line.split(",")]
x.append(data[:-1])
y.append(data[-1])

x = np.array(x) # 二维数据形式的输入矩阵,一行一样本,一列一特征
y = np.array(y) # 一维数组形式的输出序列,每个元素对应一个输入样本
# print(x)
# print(y)

# 创建线性回归器
model = lm.LinearRegression()
# 用已知输入、输出数据集训练回归器
model.fit(x, y)
# 根据训练模型预测输出
pred_y = model.predict(x)

# 创建岭回归器并进行训练
# Ridge: 第一个参数为正则强度,该值越大,异常样本权重就越小
model_2 = lm.Ridge(alpha=200, max_iter=1000) # 创建对象, max_iter为最大迭代次数
model_2.fit(x, y) # 训练
pred_y2 = model_2.predict(x) # 预测

# lasso回归
model_3 = lm.Lasso(alpha=0.5, # L1范数相乘的系数
max_iter=1000) # 最大迭代次数
model_3.fit(x, y) # 训练
pred_y3 = model_3.predict(x) # 预测

# 可视化回归曲线
mp.figure('Linear & Ridge & Lasso', facecolor='lightgray')
mp.title('Linear & Ridge & Lasso', fontsize=20)
mp.xlabel('x', fontsize=14)
mp.ylabel('y', fontsize=14)
mp.tick_params(labelsize=10)
mp.grid(linestyle=':')
mp.scatter(x, y, c='dodgerblue', alpha=0.8, s=60, label='Sample')
sorted_idx = x.T[0].argsort()

mp.plot(x[sorted_idx], pred_y[sorted_idx], c='orangered', label='Linear') # 线性回归
mp.plot(x[sorted_idx], pred_y2[sorted_idx], c='limegreen', label='Ridge') # 岭回归
mp.plot(x[sorted_idx], pred_y3[sorted_idx], c='blue', label='Lasso') # Lasso回归

mp.legend()
mp.show()

执行结果:
线性回归、Lasso回归、岭回归

模型保存与加载

可使用Python提供的功能对模型对象进行保存。

1
2
3
4
5
import pickle
# 保存模型
pickle.dump(模型对象, 文件对象)
# 加载模型
model_obj = pickle.load(文件对象)

保存训练模型应该在训练完成或评估完成之后,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 模型保存示例
import numpy as np
import sklearn.linear_model as lm # 线性模型
import pickle

x = np.array([[0.5], [0.6], [0.8], [1.1], [1.4]]) # 输入集
y = np.array([5.0, 5.5, 6.0, 6.8, 7.0]) # 输出集

# 创建线性回归器
model = lm.LinearRegression()
# 用已知输入、输出数据集训练回归器
model.fit(x, y)

print("训练完成.")

# 保存训练后的模型
with open('linear_model.pkl', 'wb') as f:
pickle.dump(model, f)
print("保存模型完成.")

执行完成后,可以看到与源码相同目录下多了一个名称为linear_model.pkl的文件,这就是保存的训练模型。使用该模型代码:

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
# 模型加载示例
import numpy as np
import sklearn.linear_model as lm # 线性模型
import sklearn.metrics as sm # 模型性能评价模块
import matplotlib.pyplot as mp
import pickle

x = np.array([[0.5], [0.6], [0.8], [1.1], [1.4]]) # 输入集
y = np.array([5.0, 5.5, 6.0, 6.8, 7.0]) # 输出集

# 加载模型
with open('linear_model.pkl', 'rb') as f:
model = pickle.load(f)
print("加载模型完成.")

# 根据加载的模型预测输出
pred_y = model.predict(x)

# 可视化回归曲线
mp.figure('Linear Regression', facecolor='lightgray')
mp.title('Linear Regression', fontsize=20)
mp.xlabel('x', fontsize=14)
mp.ylabel('y', fontsize=14)
mp.tick_params(labelsize=10)
mp.grid(linestyle=':')
mp.scatter(x, y, c='blue', alpha=0.8, s=60, label='Sample')

mp.plot(x, pred_y, c='orangered', label='Regression')

mp.legend()
mp.show()

执行结果和训练模型预测结果一样.