概念
之前,我们介绍过拟合的概念。拟合指的是构建的模型能够符合样本数据的特征。与拟合相关的两个概念是欠拟合与过拟合。
- 欠拟合:模型过于简单,未能充分捕获样本数据的特征。表现为模型在训练集上的效果不好。
- 过拟合:模型过于复杂,过分捕获样本数据的特征,从而将样本数据中一些特殊特征当成了共性特征。表现为模型在训练集上的效果非常好,但是在未知数据上的表现效果不好。
解决方案
如果产生欠拟合,可以采用如下方式,来达到更好的拟合效果。
- 增加迭代次数
- 增加模型复杂度(例如,引入新的特征)
- 通过多项式扩展
- 使用更复杂的模型(例如非线性模型)
如果产生过拟合,可以采用如下方式,来降低过拟合的程度。
- 收集更多的数据
- 正则化
- 降低模型的复杂度
- 减少迭代次数
- 选择简单的模型
现在,我们来介绍下多项式扩展与正则化。
多项式扩展
我们可以使用线性回归模型来拟合数据,然而,在现实中,数据未必总是线性(或接近线性)的。当数据并非线性时,直接使用LinearRegression的效果可能会较差,产生欠拟合。
# x = np.linspace(0, 10, 50)
# print(x.shape)
# print(x[:, np.newaxis].shape)
(50,) (50, 1)
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
x = np.linspace(0, 10, 50)
# 真实的数据分布。该分布是非线性的。
y = x * np.sin(x)
# np.newaxis 表示进行维度的扩展,可以认为是增加一个维度,该维度为1。
# 此种方式也可以通过reshape方法来实现。
X = x[:, np.newaxis]
lr = LinearRegression()
lr.fit(X, y)
# 输出在训练集上的分值。查看线性回归LinearRegression在非线性数据集上的拟合效果。
print(lr.score(X, y))
# 将样本数据以散点图进行绘制。
plt.scatter(x, y, c="g", label="样本数据")
# 绘制预测值(模型的回归线)
plt.plot(X, lr.predict(X), "r-", label="拟合线")
plt.legend()
plt.show()
# 结果:R ^ 2值为0.05908132146396671,模型在训练集上表现非常不好,产生欠拟合。
0.05908132146396671
# 多项式扩展的规则:
# 每个输入特征分别带有一个指数(指数值为非负整数),然后让指数之间进行任意可能的组合,
# 但要保证所有的指数之和不能大于扩展的阶数。
# 类似PolynomialFeatures这样功能的类(数据预处理),所有的转换方法都叫做transform。
# 拟合与转换可以同时进行,方法名称都叫做fit_transform。
import numpy as np
# sklearn.preprocessing 该模块提供数据预处理的相关功能。
# PolynomialFeatures多项式扩展类,可以对模型进行n阶扩展。从而可以解决欠拟合问题。
from sklearn.preprocessing import PolynomialFeatures
X = np.array([[1, 2], [3, 4]])
# X = np.array([[1, 2, 3], [3, 4, 5]])
# 定义多项式扩展类,参数指定要扩展的阶数。
poly = PolynomialFeatures(2)
# 拟合模型,计算power_的值。
# poly.fit(X)
# 对数据集X进行多项式扩展,即进行多项式转换。
# r = poly.transform(X)
# 我们可以令fit与transofrm两步一起完成。
r = poly.fit_transform(X)
print("转换之后的结果:")
print(r)
print("指数矩阵:")
# 指数矩阵,形状为(输出特征数,输入特征数)。
print(poly.powers_)
print(f"输入的特征数量:{poly.n_input_features_}")
print(f"输出的特征数量:{poly.n_output_features_}")
# 根据power_矩阵,自行计算转换结果。
# 循环获取X中的每一个样本。
for x1, x2 in X:
for e1, e2 in poly.powers_:
print(x1 ** e1 * x2 ** e2, end="\t")
print()
转换之后的结果:
[[ 1. 1. 2. 1. 2. 4.]
[ 1. 3. 4. 9. 12. 16.]]
指数矩阵:
[[0 0]
[1 0]
[0 1]
[2 0]
[1 1]
[0 2]]
输入的特征数量:2
输出的特征数量:6
1 1 2 1 2 4
1 3 4 9 12 16
多项式扩展解决欠拟合
现在,就让我们对之前的程序来进行多项式扩展,尝试解决欠拟合问题。
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
x = np.linspace(0, 10, 50)
y = x * np.sin(x)
X = x[:, np.newaxis]
figure, ax = plt.subplots(2, 3)
figure.set_size_inches(18, 10)
ax = ax.ravel()
# n为要进行多项式扩展的阶数。
for n in range(1, 7):
# 注意:多项式1阶扩展,相当于没有扩展(没有变化)。
poly = PolynomialFeatures(degree=n)
X_transform = poly.fit_transform(X)
# 注意:多项式扩展之后,我们依然可以将数据视为线性的,因此,我们还是可以通过之前的
# LinearRegression来求解问题。
lr = LinearRegression()
# 使用多项式扩展之后的数据集来训练模型。
lr.fit(X_transform, y)
ax[n - 1].set_title(f"{n}阶,拟合度:{lr.score(X_transform, y):.3f}")
ax[n - 1].scatter(x, y, c="g", label="样本数据")
ax[n - 1].plot(x, lr.predict(X_transform), "r-", label="拟合线")
ax[n - 1].legend()
流水线
在上例中,我们使用多项式对训练数据进行了转换(扩展),然后使用线性回归类(LinearRegression)在转换后的数据上进行拟合。可以说,这是两个步骤。我们虽然可以分别去执行这两个步骤,然而,当数据预处理的工作较多时,可能会涉及更多的步骤(例如标准化,编码等),此时再去一一执行会显得过于繁琐。
流水线(Pipeline类)可以将每个评估器视为一个步骤,然后将多个步骤作为一个整体而依次执行,这样,我们就无需分别执行每个步骤。流水线中的所有评估器(除了最后一个评估器外)都必须具有转换功能(具有transform方法)。
流水线具有最后一个评估器的所有方法。当调用某个方法f时,会首先对前n - 1个(假设流水线具有n个评估器)评估器执行transform方法(如果调用的f是fit方法,则n-1个评估器会执行fit_transform方法),对数据进行转换,然后在最后一个评估器上调用f方法。
例如,当在流水线上调用fit方法时,将会依次在每个评估器上调用fit方法,然后再调用transform方法,接下来将转换之后的结果传递给下一个评估器,直到最后一个评估器调用fit方法为止(最后一个评估器不会调用transform方法)。而当在流水线上调用predict方法时,则会依次在每个评估器上调用transform方法,最后在最后一个评估器上调用predict方法。
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
# sklearn库提供的流水线类,作用就是将多个评估器打包成一个整体,这样,当我们对流水线进行某些操作时,
# 流水线内的所有评估器都会执行相关的操作。这样,就可以作为一个整体而执行,无需我们分别对每个评估器
# 单独进行执行。
from sklearn.pipeline import Pipeline
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
x = np.linspace(0, 10, 50)
y = x * np.sin(x)
X = x[:, np.newaxis]
# 定义流水线中的每一个评估器。
# 格式为一个含有元组的列表。每个元组定义流水线中的一个步骤。
# 元组中含有两个元素。前面的元素为流水线步骤的名称,后面的
# 元素为该流水线步骤处理的对象。
estimators = [("poly", PolynomialFeatures()), ("lr", LinearRegression())]
# 创建流水线对象,将评估器数组传递给流水线类。
pipeline = Pipeline(estimators)
# 流水线的steps属性,可以返回流水线所有的步骤。包括步骤名与该步骤的处理对象。
# pipeline.steps
# 流水线的named_steps属性,与steps属性相似,只是类型不同(字典类型)。
# pipeline.named_steps
# 设置流水线对象的参数信息。
# 如果需要为流水线的某个步骤处理对象设置相关的参数,则参数名为:步骤名__处理对象参数。
pipeline.set_params(poly__degree=8)
# 获取流水线支持设置的所有参数。
# print(pipeline.get_params())
# 在流水线对象上调用fit方法,相当于对除了最后一个评估器之外的所有评估器调用fit_transform方法,
# 然后最后一个评估器调用fit方法。
pipeline.fit(X, y)
# 流水线对象具有最后一个评估器的所有方法。
# 当通过流水线对象,调用最后一个评估器的方法时,会首先调用之前所有评估器的transform方法。
score = pipeline.score(X, y)
plt.title(f"8阶,拟合度:{score:.3f}")
plt.scatter(X, y, c="g", label="样本数据")
# 当调用流水线对象的predict方法时,除最后一个评估器外,其余评估器会调用transform方法,然后,最后
# 一个评估器调用predict方法。
plt.plot(X, pipeline.predict(X), "r-", label="拟合线")
[<matplotlib.lines.Line2D at 0x95f3f98>]
多项式产生过拟合
通过之前的程序,我们发现,使用多项式扩展完美的解决了欠拟合问题。如果我们使用更多阶的多项式扩展,甚至可以将拟合度提高为1。但是,问题来了,多项式扩展时,是否阶数越多越好呢?
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
def true_fun(X):
return np.cos(1.5 * np.pi * X)
np.random.seed(0)
n_samples = 30
degrees = [1, 4, 10, 15]
x_train = np.sort(np.random.rand(n_samples))
y_train = true_fun(x_train) + np.random.randn(n_samples) * 0.1
X_train = x_train[:, np.newaxis]
plt.figure(figsize=(18, 10))
for i, n in enumerate(degrees):
plt.subplot(2, 2, i + 1)
pipeline = Pipeline([("poly", PolynomialFeatures(degree=n)),
("lr", LinearRegression())])
pipeline.fit(X_train, y_train)
train_score = pipeline.score(X_train, y_train)
x_test = np.linspace(0, 1, 100)
y_test = true_fun(x_test)
X_test = x_test[:, np.newaxis]
test_score = pipeline.score(X_test, y_test)
plt.plot(X_test, pipeline.predict(X_test), label="预测线")
plt.plot(X_test, true_fun(X_test), label="真实线")
plt.scatter(X_train, y_train, c='b', s=20, label="样本数据")
plt.xlabel("x")
plt.ylabel("y")
plt.xlim((0, 1))
plt.ylim((-2, 2))
plt.legend(loc="best")
plt.title(f"训练集:{train_score:.3f} 测试集:{test_score:.3f}")
# print(pipeline.named_steps["lr"].coef_)
plt.show()
说明:
以上假设样本数量为m,特征数量为n。
α>0并且0<=p<=1
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.linear_model import Ridge
mpl.rcParams["font.family"] = "serif"
mpl.rcParams["axes.unicode_minus"] = False
X, y, w = make_regression(n_samples=10, n_features=10, coef=True,
random_state=1, bias=3.5)
alphas = np.logspace(-4, 4, 200)
coefs = []
ridge = Ridge()
for a in alphas:
ridge.set_params(alpha=a)
ridge.fit(X, y)
coefs.append(ridge.coef_)
ax = plt.gca()
ax.plot(alphas, coefs)
ax.set_xscale('log')
ax.set_xlabel('alpha')
ax.set_ylabel('weights')
正则化之间的比较
- L2正则化不会产生稀疏解,L1正则化会产生稀疏解,这也使得LASSO成为一种监督特征选择技术。
- 如果数据的维度中存在噪音和冗余,稀疏的解可以找到有用的维度并且减少冗余,提高回归预测的准确性和鲁棒性。
- Ridge模型具有较高的准确性、鲁棒性以及稳定性,而LASSO模型具有较高的求解速度。
- 如果既要考虑稳定性也考虑求解的速度,可考虑使用Elasitc Net。
-
import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt from sklearn.pipeline import Pipeline from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression, Lasso, Ridge, ElasticNet mpl.rcParams["font.family"] = "SimHei" mpl.rcParams["axes.unicode_minus"] = False def true_fun(X): return np.cos(1.5 * np.pi * X) np.random.seed(0) n_samples = 30 x_train = np.sort(np.random.rand(n_samples)) y_train = true_fun(x_train) + np.random.randn(n_samples) * 0.1 X_train = x_train[:, np.newaxis] models = [("线性回归(无正则化)", LinearRegression()), ("L1正则化:", Lasso(alpha=0.01)), ("L2正则化", Ridge(alpha=0.01)), ("弹性网络", ElasticNet(alpha=0.01, l1_ratio=0.5))] plt.figure(figsize=(18, 10)) for i, (name, model) in enumerate(models): plt.subplot(2, 2, i + 1) pipeline = Pipeline([("poly", PolynomialFeatures(degree=15)), ("model", model)]) pipeline.fit(X_train, y_train) train_score = pipeline.score(X_train, y_train) x_test = np.linspace(0, 1, 100) y_test = true_fun(x_test) X_test = x_test[:, np.newaxis] test_score = pipeline.score(X_test, y_test) plt.plot(X_test, pipeline.predict(X_test), label="预测线") plt.plot(X_test, true_fun(X_test), label="真实线") plt.scatter(X_train, y_train, c='b', s=20, label="样本数据") plt.xlabel("x") plt.ylabel("y") plt.xlim((0, 1)) plt.ylim((-2, 2)) plt.legend(loc="best") plt.title(f"{name} 训练集:{train_score:.3f} 测试集:{test_score:.3f}") print(pipeline.named_steps["model"].coef_) plt.show()
训练集,验证集与测试集
当模型建立后,我们需要评估下模型的效果,例如,是否存在欠拟合,过拟合等。但是,在我们建立模型时,我们不能使用全部数据用于训练(考试的示例)。因此,我们可以将数据集分为训练集与测试集。然而,模型并不是绝对单一化的,其可能含有很多种不同的配置方案(参数),这种参数不同于我们之前接触过的权重(w)与偏置(b),这是因为,权重与偏置是通过数据学习来的,而这种参数我们需要在训练前事先指定(例如,正则化的参数alpha等),而这些参数取值不同,也可能会导致训练出来模型的结果也不同。我们将这种参数称为超参数。可以说,超参数不是通过训练得出(事先指定),但是超参数的取值可能会对模型性能造成较大的影响。
因此,我们需要不断去调整超参数的值,进而选择一个合适的超参数,使得模型的表现最优(或接近最优)。我们可以使用测试集去验证这一点。然而,这会导致选择的合适超参数后,无法去检验模型最终的效果。此时,我们可将数据进一步划分,即将原来的训练集再次切割,分为两部分:训练集与验证集。训练集用来建立模型(与之前相同),验证集用来选择合适的超参数,而测试集用于最终模型效果的评估(测试集应该总是作为最终结果的评估,而不是作为中间结果的评估)。交叉验证
将数据分为训练集,验证集与测试集后,可以解决上述的问题,不过,这样划分依然具有缺陷:
- 划分验证集后,会将模型的训练数据进一步减少,不利于模型的训练。
- 不同的划分方式,模型的结果也可能不尽相同。
-
我们可以使用交叉验证来解决以上问题。在训练集中,我们分成k个部分,然后使用其中的k - 1个部分作为训练集,剩下的一部分作为验证集来对模型进行评估。如此重复的进行k次,最终取k次评估的平均值。这种交叉验证方式我们称为“k折交叉验证”。
k折交叉验证的优缺点如下: - 优点:不需要额外的验证集数据,因此不会令训练集数据较少。
- 缺点:需要重复计算k次,大大增加的程序的运行时间。
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LassoCV, RidgeCV, ElasticNetCV
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
def true_fun(X):
return np.cos(1.5 * np.pi * X)
np.random.seed(0)
n_samples = 30
x_train = np.sort(np.random.rand(n_samples))
y_train = true_fun(x_train) + np.random.randn(n_samples) * 0.1
X_train = x_train[:, np.newaxis]
alphas = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5]
models = [("L1正则化:", LassoCV(alphas=alphas, max_iter=5000)), ("L2正则化", RidgeCV(alphas=alphas)),
("弹性网络", ElasticNetCV(l1_ratio=0.5, alphas=alphas))]
plt.figure(figsize=(18, 5))
for i, (name, model) in enumerate(models):
plt.subplot(1, 3, i + 1)
pipeline = Pipeline([("poly", PolynomialFeatures(degree=15)), ("model", model)])
pipeline.set_params(model__cv=10)
pipeline.fit(X_train, y_train)
train_score = pipeline.score(X_train, y_train)
x_test = np.linspace(0, 1, 100)
y_test = true_fun(x_test)
X_test = x_test[:, np.newaxis]
test_score = pipeline.score(X_test, y_test)
plt.plot(X_test, pipeline.predict(X_test), label="预测线")
plt.plot(X_test, true_fun(X_test), label="真实线")
plt.scatter(X_train, y_train, c='b', s=20, label="样本数据")
plt.xlabel("x")
plt.ylabel("y")
plt.xlim((0, 1))
plt.ylim((-2, 2))
plt.legend(loc="best")
plt.title(f"{name} 训练集:{train_score:.3f} 测试集:{test_score:.3f}")
print(pipeline.named_steps["model"].alpha_)
plt.show()
模型持久化
当我们训练好模型后,就可以使用模型进行预测。然而,这毕竟不像打印一个Hello World那样简单,当我们需要的时候,重新运行一次就可以了。在实际生产环境中,数据集可能非常庞大,如果在我们每次需要使用该模型时,都去重新运行程序,势必会耗费大量的时间。
为了方便以后能够复用,我们可以将模型保存,在需要的时候,直接加载之前保存的模型,就可以直接进行预测。其实,保存模型,就是保存模型的参数(结构),在载入模型的时候,将参数(结构)恢复成模型保存时的参数(结构)而已。
保存模型
注意:保存模型时,保存位置的目录必须事先存在,否则会出现错误。
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.externals import joblib
X, y = load_diabetes(return_X_y=True)
train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.25, random_state=0)
lr = LinearRegression()
lr.fit(train_X, train_y)
joblib.dump(lr, "lr.model")
载入模型
我们可以载入之前保存的模型,进行预测。
model = joblib.load("lr.model")
print(model.predict(test_X))