根据菜菜的课程进行整理,方便记忆理解
代码位置如下:
非线性问题:多项式回归
重塑我们心中的“线性”概念
在机器学习和统计学中,甚至在我们之前的课程中,我们无数次提到”线性“这个名词。首先我们算法就叫做”线性回归“,而在支持向量机中,我们也曾经提到最初的支持向量机只能够分割线性可分的数据,然后引入了”核函数“来帮助我们分类那些非线性可分的数据。我们也曾经说起过,比如说决策树,支持向量机是”非线性“模型。所有的这些概念,让我们对”线性“这个词非常熟悉,却又非常陌生——因为我们并不知道它的真实含义。在这一小节,我将来为大家重塑线性的概念,并且为大家解决线性回归模型改进的核心之一:帮助线性回归解决非线性问题。
变量之间的线性关系
首先,”线性“这个词用于描述不同事物时有着不同的含义。我们最常使用的线性是指“变量之间的线性关系(linear relationship)”,它表示两个变量之间的关系可以展示为一条直线,即可以使用方程来进行拟合。要探索两个变量之间的关系是否是线性的,最简单的方式就是绘制散点图,如果散点图能够相对均匀地分布在一条直线的两端,则说明这两个变量之间的关系是线性的。因此,三角函数(如),高次函数( ),指数函数()等等图像不为直线的函数所对应的自变量和因变量之间是非线性关系(non-linear relationship)。也因此被称为线性方程或线性函数(linear function),三角函数,高次函数等也因此被称为非线性函数(non-linear function)。
数据的线性与非线性
从线性关系这个概念出发,我们有了一种说法叫做“线性数据”。通常来说,一组数据由多个特征和标签组成。当这些特征分别与标签存在线性关系的时候,我们就说这一组数据是线性数据。当特征矩阵中任意一个特征与标签之间的关系需要使用三角函数,指数函数等函数来定义,则我们就说这种数据叫做“非线性数据”。对于线性和非线性数据,最简单的判别方法就是利用模型来帮助我们——如果是做分类则使用逻辑回归,如果做回归则使用线性回归,如果效果好那数据是线性的,效果不好则数据不是线性的。当然,也可以降维后进行绘图,绘制出的图像分布接近一条直线,则数据就是线性的。
不难发现,都这里为止我们为大家展示的都是或多或少能够连成线的数据分布,他们之间只不过是直线与曲线的分别罢了。然而考虑一下,当我们在进行分类的时候,我们的决策函数往往是一个分段函数,比如二分类下的决策函数可以是符号函数,符号函数的图像可以表示为取值为1和-1的两条直线。这个函数明显不符合我们所说的可以使用一条直线来进行表示的属性,因此分类问题中特征与标签[0,1]或者[-1,1]之间关系明显是非线性的关系。除非我们在拟合分类的概率,否则不存在例外。
同时我们还注意到,当我们在进行分类的时候,我们的数据分布往往是这样的:
可以看得出,这些数据都不能由一条直线来进行拟合,他们也没有均匀分布在某一条线的周围,那我们怎么判断,这些数据是线性数据还是非线性数据呢?在这里就要注意了,当我们在回归中绘制图像时,绘制的是特征与标签的关系图,横坐标是特征,纵坐标是标签,我们的标签是连续型的,所以我们可以通过是否能够使用一条直线来拟合图像判断数据究竟属于线性还是非线性。然而在分类中,我们绘制的是数据分布图,横坐标是其中一个特征,纵坐标是另一个特征,标签则是数据点的颜色。因此在分类数据中,我们使用“是否线性可分”(linearly separable)这个概念来划分分类数据集。当分类数据的分布上可以使用一条直线来将两类数据分开时,我们就说数据是线性可分的。反之,数据不是线性可分的。
总结一下,对于回归问题,数据若能分布为一条直线,则是线性的,否则是非线性。对于分类问题,数据分布若能使用一条直线来划分类别,则是线性可分的,否则数据则是线性不可分的。
线性模型与非线性模型
在回归中,线性数据可以使用如下的方程来进行拟合:
也就是我们的线性回归的方程。根据线性回归的方程,我们可以拟合出一组参数,在这一组固定的参数下我们可以建立一个模型,而这个模型就被我们称之为是线性回归模型。所以建模的过程就是寻找参数的过程。此时此刻我们建立的线性回归模型,是一个用于拟合线性数据的线性模型。作为线性模型的典型代表,我们可以从线性回归的方程中总结出线性模型的特点:其自变量都是一次项。
那线性回归在非线性数据上的表现如何呢?我们来建立一个明显是非线性的数据集,并观察线性回归和决策树的而回归在拟合非线性数据集时的表现:
- 导入所需要的库
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
- 创建需要拟合的数据集
rnd = np.random.RandomState(42) #设置随机数种子
X = rnd.uniform(-3, 3, size=100) #random.uniform,从输入的任意两个整数中取出size个随机数
X #作为我的横坐标
"""
array([-0.75275929, 2.70428584, 1.39196365, 0.59195091, -2.06388816,
-2.06403288, -2.65149833, 2.19705687, 0.60669007, 1.24843547,
-2.87649303, 2.81945911, 1.99465584, -1.72596534, -1.9090502 ,
-1.89957294, -1.17454654, 0.14853859, -0.40832989, -1.25262516,
0.67111737, -2.16303684, -1.24713211, -0.80182894, -0.26358009,
1.71105577, -1.80195731, 0.08540663, 0.55448741, -2.72129752,
0.64526911, -1.97685526, -2.60969044, 2.69331322, 2.7937922 ,
1.85038409, -1.17231738, -2.41396732, 1.10539816, -0.35908504,
-2.26777059, -0.02893854, -2.79366887, 2.45592241, -1.44732011,
0.97513371, -1.12973354, 0.12040813, 0.28026168, -1.89087327,
2.81750777, 1.65079694, 2.63699365, 2.3689641 , 0.58739987,
2.53124541, -2.46904499, -1.82410283, -2.72863627, -1.04801802,
-0.66793626, -1.37190581, 1.97242505, -0.85948004, -1.31439294,
0.2561765 , -2.15445465, 1.81318188, -2.55269614, 2.92132162,
1.63346862, -1.80770591, -2.9668673 , 1.89276857, 1.24114406,
1.37404301, 1.62762208, -2.55573209, -0.84920563, -2.30478564,
2.17862056, 0.73978876, -1.01461185, -2.6186499 , -1.13410607,
-1.04890007, 1.37763707, 0.82534483, 2.32327646, -0.16671045,
-2.28243452, 1.27946872, 1.56471029, 0.36766319, 1.62580308,
-0.03722642, 0.13639698, -0.43475389, -2.84748524, -2.35265144])
"""
#生成y的思路:先使用NumPy中的函数生成一个sin函数图像,然后再人为添加噪音
y = np.sin(X) + rnd.normal(size=len(X)) / 3 #random.normal,生成size个服从正态分布的随机数
#使用散点图观察建立的数据集是什么样子
plt.scatter(X, y,marker='o',c='k',s=20)
plt.show()
- 使用原始数据进行建模
#使用原始数据进行建模
LinearR = LinearRegression().fit(X, y)
TreeR = DecisionTreeRegressor(random_state=0).fit(X, y)
#放置画布
fig, ax1 = plt.subplots(1)
#创建测试数据:一系列分布在横坐标上的点
line = np.linspace(-3, 3, 1000, endpoint=False).reshape(-1, 1)
#将测试数据带入predict接口,获得模型的拟合效果并进行绘制
ax1.plot(line, LinearR.predict(line), linewidth=2, color='green',
label="linear regression")
ax1.plot(line, TreeR.predict(line), linewidth=2, color='red',
label="decision tree")
#将原数据上的拟合绘制在图像上
ax1.plot(X[:, 0], y, 'o', c='k')
#其他图形选项
ax1.legend(loc="best")
ax1.set_ylabel("Regression output")
ax1.set_xlabel("Input feature")
ax1.set_title("Result before discretization")
plt.tight_layout()
plt.show()
从图像上可以看出,线性回归无法拟合出这条带噪音的正弦曲线的真实面貌,只能够模拟出大概的趋势,而决策树却通过建立复杂的模型将几乎每个点都拟合出来了。可见,使用线性回归模型来拟合非线性数据的效果并不好,而决策树这样的模型却拟合得太细致,但是相比之下,还是决策树的拟合效果更好一些。
决策树无法写作一个方程(我们在XGBoost章节中会详细讲解如何将决策树定义成一个方程,但它绝对不是一个形似的方程),它是一个典型的非线性模型,当它被用于拟合非线性数据,可以发挥奇效。其他典型的非线性模型还包括使用高斯核的支持向量机,树的集成算法,以及一切通过三角函数,指数函数等非线性方程来建立的模型。
根据这个思路,我们也许可以这样推断:线性模型用于拟合线性数据,非线性模型用于拟合非线性数据。但事实上机器学习远远比我们想象的灵活得多,线性模型可以用来拟合非线性数据,而非线性模型也可以用来拟合线性数据,更神奇的是,有的算法没有模型也可以处理各类数据,而有的模型可以既可以是线性,也可以是非线性模型!接下来,我们就来一一讨论这些问题。
非线性模型拟合线性数据
非线性模型能够拟合或处理线性数据的例子非常多,我们在之前的课程中多次为大家展示了非线性模型诸如决策树,随机森林等算法在分类中处理线性可分的数据的效果。无一例外的,非线性模型们几乎都可以在线性可分数据上有不逊于线性模型的表现。同样的,如果我们使用随机森林来拟合一条直线,那随机森林毫无疑问会过拟合,因为线性数据对于非线性模型来说太过简单,很容易就把训练集上的训练得很高,MSE训练的很低。
线性模型拟合非线性数据
但是相反的,线性模型若用来拟合非线性数据或者对非线性可分的数据进行分类,那通常都会表现糟糕。通常如果我们已经发现数据属于非线性数据,或者数据非线性可分的数据,则我们不会选择使用线性模型来进行建模。改善线性模型在非线性数据上的效果的方法之一时进行分箱,并且从下图来看分箱的效果不是一般的好,甚至高过一些非线性模型。在下一节中我们会详细来讲解分箱的效果,但很容易注意到,在没有其他算法或者预处理帮忙的情况下,线性模型在非线性数据上的表现时很糟糕的。
从上面的图中,我们可以观察出一个特性:线性模型们的决策边界都是一条条平行的直线,而非线性模型们的决策边界是交互的直线(格子),曲线,环形等等。对于分类模型来说,这是我们判断模型是线性还是非线性的重要评判因素:线性模型的决策边界是平行的直线,非线性模型的决策边界是曲线或者交叉的直线。之前我们提到,模型上如果自变量上的最高次方为1,则模型是线性的,但这种方式只适用于回归问题。分类模型中,我们很少讨论模型是否线性,因为我们很少使用线性模型来执行分类任务(逻辑回归是一个特例)。但从上面我们总结出的结果来看,我们可以认为对分类问题而言,如果一个分类模型的决策边界上自变量的最高次方为1,则我们称这个模型是线性模型。
既是线性,也是非线性的模型
对于有一些模型来说,他们既可以处理线性模型又可以处理非线性模型,比如说强大的支持向量机。支持向量机的前身是感知机模型,朴实的感知机模型是实打实的线性模型(其决策边界是直线),在线性可分数据上表现优秀,但在非线性可分的数据上基本属于无法使用状态。
但支持向量机就不一样了。支持向量机本身也是处理线性可分数据的,但却可以通过对数据进行升维(将数据转移到高维空间中),将非线性可分数据变成高维空间中的线性可分数据,然后使用相应的“核函数”来求解。当我们选用线性核函数"linear"的时候,数据没有进行变换,支持向量机中就是线性模型,此时它的决策边界是直线。而当我们选用非线性核函数比如高斯径向基核函数的时候,数据进行了升维变化,此时支持向量机就是非线性模型,此时它的决策边界在二维空间中是曲线。所以这个模型可以在线性和非线性之间自由切换,一切取决于它的核函数。
还有更加特殊的,没有模型的算法,比如最近邻算法KNN,这些都是不建模,但是能够直接预测出标签或做出判断的算法。而这些算法,并没有线性非线性之分,单纯的是不建模的算法们。
讨论到这里,相信大家对于线性和非线性模型的概念就比较清楚了。来看看下面这张表的总结:
| 线性模型 | 非线性模型 | |
|---|---|---|
| 代表模型 | 线性回归,逻辑回归,弹性网,感知机 | 决策树,树的集成模型,使用高斯核的SVM |
| 模型特点 | 模型简单,运行速度快 | 模型复杂,效果好,但速度慢 |
| 数学特征:回归 | 自变量是一次项 | 自变量不都是一次项 |
| 分类 | 决策边界上的自变量都是一次项 | 决策边界上的自变量不都是一次项 |
| 可视化: 回归 | 拟合出的图像是一条直线 | 拟合出的图像不是一条直线 |
| 分类 | 决策边界在二维平面是一条直线 | 决策边界在二维平面不是一条直线 |
| 擅长数据类型 | 主要是线性数据,线性可分数据 | 所有数据 |
模型在线性和非线性数据集上的表现为我们选择模型提供了一个思路:当我们获取数据时,我们往往希望使用线性模型来对数据进行最初的拟合(线性回归用于回归,逻辑回归用于分类),如果线性模型表现良好,则说明数据本身很可能是线性的或者线性可分的,如果线性模型表现糟糕,那毫无疑问我们会投入决策树,随机森林这些模型的怀抱,就不必浪费时间在线性模型上了。
不过这并不代表着我们就完全不能使用线性模型来处理非线性数据了。在现实中,线性模型有着不可替代的优势:计算速度异常快速,所以也还是存在着我们无论如何也希望使用线性回归的情况。因此,我们有多种手段来处理线性回归无法拟合非线性问题的问题,接下来我们就来看一看。
使用分箱处理非线性问题
让线性回归在非线性数据上表现提升的核心方法之一是对数据进行分箱,也就是离散化。与线性回归相比,我们常用的一种回归是决策树的回归。我们之前拟合过一条带有噪音的正弦曲线以展示多元线性回归与决策树的效用差异,我们来分析一下这张图,然后再使用采取措施帮助我们的线性回归。
- 导入所需要的库
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
- 创建需要拟合的数据集
rnd = np.random.RandomState(42) # 设置随机数种子
X = rnd.uniform(-3, 3, size=100) # random.uniform,从输入的任意两个整数中取出size个随机数
# 生成y的思路:先使用NumPy中的函数生成一个sin函数图像,然后再人为添加噪音
y = np.sin(X) + rnd.normal(size=len(X)) / 3 #random.normal,生成size个服从正态分布的随机数
# 使用散点图观察建立的数据集是什么样子
plt.scatter(X, y,marker='o',c='k',s=20)
plt.show()
#为后续建模做准备:sklearn只接受二维以上数组作为特征矩阵的输入
X.shape
# (100,)
X = X.reshape(-1, 1)
# (100, 1)
- 使用原始数据进行建模
#使用原始数据进行建模
LinearR = LinearRegression().fit(X, y)
TreeR = DecisionTreeRegressor(random_state=0).fit(X, y)
#放置画布
fig, ax1 = plt.subplots(1)
#创建测试数据:一系列分布在横坐标上的点
line = np.linspace(-3, 3, 1000, endpoint=False).reshape(-1, 1)
#将测试数据带入predict接口,获得模型的拟合效果并进行绘制
ax1.plot(line, LinearR.predict(line), linewidth=2, color='green',
label="linear regression")
ax1.plot(line, TreeR.predict(line), linewidth=2, color='red',
label="decision tree")
#将原数据上的拟合绘制在图像上
ax1.plot(X[:, 0], y, 'o', c='k')
#其他图形选项
ax1.legend(loc="best")
ax1.set_ylabel("Regression output")
ax1.set_xlabel("Input feature")
ax1.set_title("Result before discretization")
plt.tight_layout()
plt.show()
从图像上可以看出,线性回归无法拟合出这条带噪音的正弦曲线的真实面貌,只能够模拟出大概的趋势,而决策树却通过建立复杂的模型将几乎每个点都拟合出来了。此时此刻,决策树正处于过拟合的状态,对数据的学习过于细致,而线性回归处于拟合不足的状态,这是由于模型本身只能够在线性关系间进行拟合的性质决定的。为了让线性回归在类似的数据上变得更加强大,我们可以使用分箱,也就是离散化连续型变量的方法来处理原始数据,以此来提升线性回归的表现。来看看我们如何实现:
- 分箱及分箱的相关问题
from sklearn.preprocessing import KBinsDiscretizer
#将数据分箱
enc = KBinsDiscretizer(n_bins=10 #分几类?
,encode="onehot") #ordinal
X_binned = enc.fit_transform(X)
#encode模式"onehot":使用做哑变量方式做离散化
#之后返回一个稀疏矩阵(m,n_bins),每一列是一个分好的类别
#对每一个样本而言,它包含的分类(箱子)中它表示为1,其余分类中它表示为0
X.shape
# (100, 1)
X_binned
"""
<100x10 sparse matrix of type '<class 'numpy.float64'>'
with 100 stored elements in Compressed Sparse Row format>
"""
#使用pandas打开稀疏矩阵
import pandas as pd
pd.DataFrame(X_binned.toarray()).head()
# 我们将使用分箱后的数据来训练模型,在sklearn中,测试集和训练集的结构必须保持一致,否则报错
LinearR_ = LinearRegression().fit(X_binned, y)
LinearR_.predict(line) # line作为测试集
line.shape # 测试
X_binned.shape # 训练
# (100, 10)
# 因此我们需要创建分箱后的测试集:按照已经建好的分箱模型将line分箱
line_binned = enc.transform(line)
line_binned.shape # 分箱后的数据是无法进行绘图的
# (1000, 10)
line_binned
"""
<1000x10 sparse matrix of type '<class 'numpy.float64'>'
with 1000 stored elements in Compressed Sparse Row format>
"""
LinearR_.predict(line_binned).shape
# (1000,)
enc.bin_edges_[0] # 分出的箱子的上限和下限
"""
array([-2.9668673 , -2.55299973, -2.0639171 , -1.3945301 , -1.02797432,
-0.21514527, 0.44239288, 1.14612193, 1.63693428, 2.32784522,
2.92132162])
"""
- 使用分箱数据进行建模和绘图
# 准备数据
enc = KBinsDiscretizer(n_bins=10,encode="onehot")
X_binned = enc.fit_transform(X)
line_binned = enc.transform(line)
#将两张图像绘制在一起,布置画布
fig, (ax1, ax2) = plt.subplots(ncols=2
, sharey=True #让两张图共享y轴上的刻度
, figsize=(10, 4))
#在图1中布置在原始数据上建模的结果
ax1.plot(line, LinearR.predict(line), linewidth=2, color='green',
label="linear regression")
ax1.plot(line, TreeR.predict(line), linewidth=2, color='red',
label="decision tree")
ax1.plot(X[:, 0], y, 'o', c='k')
ax1.legend(loc="best")
ax1.set_ylabel("Regression output")
ax1.set_xlabel("Input feature")
ax1.set_title("Result before discretization")
#使用分箱数据进行建模
LinearR_ = LinearRegression().fit(X_binned, y)
TreeR_ = DecisionTreeRegressor(random_state=0).fit(X_binned, y)
#进行预测,在图2中布置在分箱数据上进行预测的结果
ax2.plot(line #横坐标
, LinearR_.predict(line_binned) #分箱后的特征矩阵的结果
, linewidth=2
, color='green'
, linestyle='-'
, label='linear regression')
ax2.plot(line, TreeR_.predict(line_binned), linewidth=2, color='red',
linestyle=':', label='decision tree')
#绘制和箱宽一致的竖线
ax2.vlines(enc.bin_edges_[0] #x轴
, *plt.gca().get_ylim() #y轴的上限和下限
, linewidth=1
, alpha=.2)
#将原始数据分布放置在图像上
ax2.plot(X[:, 0], y, 'o', c='k')
#其他绘图设定
ax2.legend(loc="best")
ax2.set_xlabel("Input feature")
ax2.set_title("Result after discretization")
plt.tight_layout()
plt.show()
从图像上可以看出,离散化后线性回归和决策树上的预测结果完全相同了——线性回归比较成功地拟合了数据的分布,而决策树的过拟合效应也减轻了。由于特征矩阵被分箱,因此特征矩阵在每个区域内获得的值是恒定的,因此所有模型对同一个箱中所有的样本都会获得相同的预测值。与分箱前的结果相比,线性回归明显变得更加灵活,而决策树的过拟合问题也得到了改善。但注意,一般来说我们是不使用分箱来改善决策树的过拟合问题的,因为树模型带有丰富而有效的剪枝功能来防止过拟合。
在这个例子中,我们设置的分箱箱数为10,不难想到这个箱数的设定肯定会影响模型最后的预测结果,我们来看看不同的箱数会如何影响回归的结果:
- 箱子数如何影响模型的结果
enc = KBinsDiscretizer(n_bins=15,encode="onehot")
X_binned = enc.fit_transform(X)
line_binned = enc.transform(line)
fig, ax2 = plt.subplots(1,figsize=(5,4))
LinearR_ = LinearRegression().fit(X_binned, y)
print(LinearR_.score(line_binned,np.sin(line)))
TreeR_ = DecisionTreeRegressor(random_state=0).fit(X_binned, y)
ax2.plot(line #横坐标
, LinearR_.predict(line_binned) #分箱后的特征矩阵的结果
, linewidth=2
, color='green'
, linestyle='-'
, label='linear regression')
ax2.plot(line, TreeR_.predict(line_binned), linewidth=2, color='red',
linestyle=':', label='decision tree')
ax2.vlines(enc.bin_edges_[0], *plt.gca().get_ylim(), linewidth=1, alpha=.2)
ax2.plot(X[:, 0], y, 'o', c='k')
ax2.legend(loc="best")
ax2.set_xlabel("Input feature")
ax2.set_title("Result after discretization")
plt.tight_layout()
plt.show()
# 0.9590978882491229
- 如何选取最优的箱数
#怎样选取最优的箱子?
from sklearn.model_selection import cross_val_score as CVS
import numpy as np
pred,score,var = [], [], []
binsrange = [2,5,10,15,20,30]
for i in binsrange:
#实例化分箱类
enc = KBinsDiscretizer(n_bins=i,encode="onehot")
#转换数据
X_binned = enc.fit_transform(X)
line_binned = enc.transform(line)
#建立模型
LinearR_ = LinearRegression()
#全数据集上的交叉验证
cvresult = CVS(LinearR_,X_binned,y,cv=5)
score.append(cvresult.mean())
var.append(cvresult.var())
#测试数据集上的打分结果
pred.append(LinearR_.fit(X_binned,y).score(line_binned,np.sin(line)))
#绘制图像
plt.figure(figsize=(6,5))
plt.plot(binsrange,pred,c="orange",label="test")
plt.plot(binsrange,score,c="k",label="full data")
plt.plot(binsrange,score+np.array(var)*0.5,c="red",linestyle="--",label = "var")
plt.plot(binsrange,score-np.array(var)*0.5,c="red",linestyle="--")
plt.legend()
plt.show()
在工业中,大量离散化变量与线性模型连用的实例很多,在深度学习出现之前,这种模式甚至一度统治一些工业中的机器学习应用场景,可见效果优秀,应用广泛。对于现在的很多工业场景而言,大量离散化特征的情况可能已经不是那么多了,不过大家依然需要对“分箱能够解决线性模型无法处理非线性数据的问题”有所了解