1、线性回归(多种方法训练)
Setup:
# 让这份笔记同步支持 python 2 和 python 3
from __future__ import division, print_function, unicode_literals
# Common imports
import numpy as np
import os
# 让笔记全程输入稳定
np.random.seed(42)
# 导入绘图工具
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
plt.rcParams['axes.labelsize'] = 14
plt.rcParams['xtick.labelsize'] = 12
plt.rcParams['ytick.labelsize'] = 12
# 设定图片保存路径并创建相应文件,这里写了一个函数,后面直接调用即可
PROJECT_ROOT_DIR = "C:\Hands-on"
CHAPTER_ID = "Linear_Regression"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH)
#保存图片
def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
print("Saving figure", fig_id)
if tight_layout:
#tight_layout()会自动调整子图参数,使之填充整个图像区域
plt.tight_layout()
#把图片以fig_extension格式存入path,分辨率为resolution
plt.savefig(path, format=fig_extension, dpi=resolution)
# 忽略无用警告
import warnings
warnings.filterwarnings(action="ignore", message="^internal gelsd")
生成数据:
import numpy as np
#np.random.rand()可以返回一个或一组服从“0~1”均匀分布的随机样本值。随机样本取值范围是[0,1)
#这里np.random.rand(100,1)返回一个100*1的由一串0~1随机值组成的列向量
X=2*np.random.rand(100,1)
#y为4+3*X再加上随机扰动,np.random.randn(100,1)中生成的随机数服从标准正态分布
y=4+3*X+np.random.randn(100,1)
做出生成数据的散点图:
#plt.plot(x,y,format_string,**kwargs)
#x轴数据,y轴数据,format_string控制曲线的格式字串
plt.plot(X, y, "b.")
# 设置x轴的文本,用于描述x轴代表的是什么
plt.xlabel("$x_1$", fontsize=18)
# 设置y轴的文本,用于描述x轴代表的是什么
plt.ylabel("$y$", rotation=0, fontsize=18)
#plt.axis([xmin, xmax, ymin, ymax]),axis()命令给定了坐标范围。
plt.axis([0, 2, 0, 15])
#把当前图像命名为generated_data_plot并存入指定路径
save_fig("generated_data_plot")
#展示当前图像
plt.show()
根据正态方程求解线性回归模型的闭式解:
#np.c_是按行连接两个矩阵,就是把两矩阵左右拼接,要求行数相等
#这里拼接后得到100*2的矩阵,第一列全为1
X_b=np.c_[np.ones((100,1)),X]
#np.linalg.inv()求解矩阵的逆,A.T表示矩阵A的转置
#A.dot(B)得到的是两数组的內积(若A,B为一维)或两矩阵乘积(若A,B为多维)
theta_best=np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)
#这里的theta_best就是我们上面的闭式解
theta_best
out:
array([[4.10292404],
[2.95619775]])
用得到的参数theta做出两个预测点:
#找两个点,x坐标分别为0和2,然后计算出其相应的y_predict
X_new=np.array([[0],[2]])
X_new_b=np.c_[np.ones((2,1)),X_new] # add x0=1 to each instance
y_predict=X_new_b.dot(theta_best)
y_predict
out:
array([[ 4.10292404],
[10.01531954]])
连接两个点得到线性回归方程对应的图像:
#利用上面计算的两个点就可以画出回归方程的图像
plt.plot(X_new,y_predict,"r-")
plt.plot(X,y,"b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([0,2,0,15])
plt.show()
上述过程用Scikit-Learn来写十分简洁:
from sklearn.linear_model import LinearRegression
#实例化
lin_reg = LinearRegression()
#拟合线性模型
lin_reg.fit(X, y)
#coef_存放回归系数,intercept_则存放截距,因此要查看方程,就是查看这两个变量的取值
lin_reg.intercept_, lin_reg.coef_
out:
(array([4.10292404]), array([[2.95619775]]))
此外,直接使用最小二乘法也是可以的:
#元组中四个元素,第一元素表示所求的最小二乘解,第二个元素表示残差总和
#第三个元素表示X_b矩阵秩,第四个元素表示X_b的奇异值
theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
theta_best_svd
out:
array([[4.10292404],
[2.95619775]])
根据正态方程求解线性回归模型很方便,这个方程在训练集上对于每一个实例来说是线性的,其复杂度为(m为样本数),因此只要有能放得下它的内存空间,它就可以对大规模数据进行训练。同时,一旦你得到了线性回归模型(通过解正态方程或者其他的算法),进行预测是非常快的。
但正态方程求解线性模型的复杂度较高,正态方程计算矩阵求逆的运算,复杂度大约在到之间(n是特征数)。具体取决于计算方式。换句话说,如果你将你的特征个数翻倍的话,其计算时间大概会变为原来的 5.3到 8倍。
梯度下降法适合在特征个数非常多,训练实例非常多,内存无法满足要求的时候使用,接下来我们使用批量梯度下降法来训练模型:
可以看到,在这个方程中每一步计算时都包含了整个训练集,这也是为什么这个算法称为批量梯度下降:每一次训练过程都使用所有的的训练数据。因此,在大数据集上,其会变得相当的慢(但是我们接下来将会介绍更快的梯度下降算法)。然而,梯度下降的运算规模和特征的数量成正比(因为这里直涉及的计算,而没有求逆)。训练一个数千数量特征的线性回归模型使用梯度下降要比使用正态方程快的多。
按上述过程计算梯度后,我们只需要迭代地更新我们的参数,这里要设置一个合适的学习率,使得算法尽快收敛。代码如下:
#设置学习率为0.1
eta=0.1
#设置迭代次数为1000
n_iterations=1000
#m代表样本数量
m=100
#参数theta随机初始化
theta=np.random.randn(2,1)
#迭代地计算梯度并更新参数
for interation in range(n_iterations):
gradients=2/m*X_b.T.dot(X_b.dot(theta)-y)
theta = theta-eta*gradients
theta
out:
array([[4.10292404],
[2.95619775]])
可以看到,批量梯度下降法的效果非常好,和我们的正态方程得到的解完全相同。
那么不同学习率的选择会导致怎样的差别呢?
theta_path_bgd = []
#画图显示训练过程中模型的变化
def plot_gradient_descent(theta, eta, theta_path=None):
#统计样本数量
m = len(X_b)
#画出样本散点图
plt.plot(X, y, "b.")
#设置迭代次数
n_iterations = 1000
#记录前十次迭代得到的参数theta
for iteration in range(n_iterations):
if iteration < 10:
y_predict = X_new_b.dot(theta)
#初始theta对应的直线用红色虚线表示,训练后的用蓝色实线表示
style = "b-" if iteration > 0 else "r--"
plt.plot(X_new, y_predict, style)
gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
theta = theta - eta * gradients
if theta_path is not None:
theta_path.append(theta)
#fontsize设置字体大小
plt.xlabel("$x_1$", fontsize=18)
plt.axis([0, 2, 0, 15])
plt.title("$\eta = {}$".format(eta), fontsize=16)
theta = np.random.randn(2,1)
#figure创建自定义图像,figsize指定figure的宽和高
plt.figure(figsize=(10,4))
#subplot创建单个子图,括号中前两个数字代表子图分布的长和宽,此处13意味着三个子图排成一行
#第三个数字表示当前子图处于哪第几个位置
plt.subplot(131); plot_gradient_descent(theta, eta=0.02)
plt.ylabel("$y$", rotation=0, fontsize=18)
##这里我们只记录eta=0.1时的参数变化情况,因此只在这一行加入theta_path设置
plt.subplot(132); plot_gradient_descent(theta, eta=0.1, theta_path=theta_path_bgd)
plt.subplot(133); plot_gradient_descent(theta, eta=0.5)
save_fig("gradient_descent_plot")
plt.show()
以上是不同学习率对应的前十次迭代的训练结果,我们发现:
在左侧,学习率太低:算法最终会达到解决方案,但需要很长时间。
在中间,学习率看起来非常好:在几次迭代中,它已经融合到解决方案中。
在右边,学习率太高:算法发散,跳到各处,实际上每一步都越来越远离解决方案。
有什么好的解决方法呢?一个简单的方法是:设置一个非常大的迭代次数,但是当梯度向量变得非常小的时候,结束迭代。非常小指的是:梯度向量小于一个值 (容差)。这时候可以认为梯度下降几乎已经达到了最小值。
批量梯度下降法的问题是,每次计算梯度都要用到所有样本,计算开销大,如果我们一次只使用一个样本呢?这就是随机梯度下降法,这种算法比批量梯度下降更不规则:成本函数不会逐渐下降直到达到最小值,而是会反复上下跳动,随着时间的推移,它将最终接近最小值。
#随机梯度下降法
theta_path_sgd = []
m = len(X_b)
np.random.seed(42)
#1个epoch代表使用训练集中的全部样本训练一次,epochs的值就是整个数据集被轮几次。
n_epochs=50
#learning_schedule的超参数
t0,t1=5,50
#t代表迭代的次数,我们看到学习率随t增加渐渐下降
def learning_schedule(t):
return t0/(t+t1)
theta=np.random.randn(2,1)
for epoch in range(n_epochs):
for i in range(m):
#对第一轮的前20次迭代画图观察
if epoch == 0 and i < 20:
y_predict = X_new_b.dot(theta)
style = "b-" if i > 0 else "r--"
plt.plot(X_new, y_predict, style)
#随机从样本中选取一个
random_index=np.random.randint(m)
xi=X_b[random_index:random_index+1]
yi=y[random_index:random_index+1]
#对选定的样本计算梯度并更新theta值
gradients=2*xi.T.dot(xi.dot(theta)-yi)
eta=learning_schedule(epoch*m+i)
theta=theta-eta*gradients
theta_path_sgd.append(theta)
plt.plot(X, y, "b.")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([0, 2, 0, 15])
save_fig("sgd_plot")
plt.show()
我们可以看到前20次迭代效果已经不错了。
theta
out:
array([[4.13288968],
[2.98203809]])
从上述结果可以看到,批量梯度下降法(Batch Gradient Descent)迭代了1,000次训练集。而我们使用随机梯度下降法(Stochastic Gradient Descent)仅迭代训练集50次,就达到了一个相当不错的解决方案。
注意,由于实例是随机挑选的,因此某些实例可能会在每个epoch被挑选几次,而其他实例可能根本不会被挑选。
我们可以使用SGD和Scikit-Learn执行线性回归,可以使用SGDRegressor类,默认为优化平方误差成本函数。以下代码运行50个epochs,从学习率0.1(eta0 = 0.1)开始,使用默认学习计划(与前一个learning_schedule不同),且它不使用任何正则化。
from sklearn.linear_model import SGDRegressor
#罚项为None即无正则项,eta0表示初始学习率
sgd_reg=SGDRegressor(n_iter=50,penalty=None,eta0=0.1)
#y.ravel()表示将y扁平化处理
sgd_reg.fit(X,y.ravel())
sgd_reg.intercept_,sgd_reg.coef_
out:
(array([4.09367284]), array([2.93689965]))
可以看到,得到的解与正态方程的解也很接近。
既然批量梯度下降效果好,计算开销大,随机梯度下降收敛慢,计算开销小,那么我们能不能找到一种折衷的方法呢?每次用一小部分样本做训练就可以达到要求,这就是所谓的Mini-batch Gradient Descent:
#mini-batch梯度下降法
theta_path_mgd = []
#迭代轮次
n_iterations = 50
#每次迭代的batch大小为20,即一次对20个样本做梯度下降
minibatch_size = 20
np.random.seed(42)
theta = np.random.randn(2,1)
t0, t1 = 200, 1000
def learning_schedule(t):
return t0 / (t + t1)
t = 0
for epoch in range(n_iterations):
#permutation返回0~m-1的一个排列构成的array
shuffled_indices = np.random.permutation(m)
#相当于将原数组X_b中的元素打乱重排
X_b_shuffled = X_b[shuffled_indices]
y_shuffled = y[shuffled_indices]
#minibatch_size在这里是步长,即每次循环i都增加minibatch_size
for i in range(0, m, minibatch_size):
t += 1
xi = X_b_shuffled[i:i+minibatch_size]
yi = y_shuffled[i:i+minibatch_size]
gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi)
eta = learning_schedule(t)
theta = theta - eta * gradients
theta_path_mgd.append(theta)
theta
out:
array([[4.13288968],
[2.98203809]])
得到的解与正态方程的解也很接近。
接下来,我们来看一下三种梯度下降方法的训练路径图:
#把记录三种方法参数变化路径的列表转换成array格式
theta_path_bgd = np.array(theta_path_bgd)
theta_path_sgd = np.array(theta_path_sgd)
theta_path_mgd = np.array(theta_path_mgd)
#三种方法参数变化路径可视化
plt.figure(figsize=(7,4))
plt.plot(theta_path_sgd[:, 0], theta_path_sgd[:, 1], "r-s", linewidth=1, label="Stochastic")
plt.plot(theta_path_mgd[:, 0], theta_path_mgd[:, 1], "g-+", linewidth=1, label="Mini-batch")
plt.plot(theta_path_bgd[:, 0], theta_path_bgd[:, 1], "b-o", linewidth=1, label="Batch")
plt.legend(loc="upper left", fontsize=10)
plt.xlabel(r"$\theta_0$", fontsize=20)
plt.ylabel(r"$\theta_1$ ", fontsize=20, rotation=0)
plt.axis([2.5, 4.5, 2.3, 3.9])
save_fig("gradient_descent_paths_plot")
plt.show()
观察可知,它们都接近最小值,但Batch GD的路径实际上停止在最小值,而SGD和Mini-batch GD继续走动。
但是,不要忘记Batch GD需要花费大量时间来完成每一步,如果我们使用良好的学习计划,Stochastic GD和Mini-batch GD也将得到很好的结果。
2、多项式回归
和线性回归类似,第一步先生成数据:
m=100
X=6*np.random.rand(m,1)-3
y=0.5*X**2+X+2+np.random.randn(m,1)
画出数据散点图:
plt.plot(X, y, "b.")
plt.title("Polynomial Regression")
plt.xlabel("$X_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
save_fig("quadratic_data_plot")
plt.show()
为了拟合目标函数(二次多项式),我们应添加二次的特征,步骤如下:
from sklearn.preprocessing import PolynomialFeatures
#将特征的平方添加为新特征
poly_features=PolynomialFeatures(degree=2,include_bias=False)
X_poly=poly_features.fit_transform(X)
X[0]
out:
array([-0.75275929])
X_poly[0]
out:
array([-0.75275929, 0.56664654])
可以看到,我们新的特征相比原来的特征多了一列平方项。用线性回归模型拟合并画出图像:
lin_reg=LinearRegression()
lin_reg.fit(X_poly,y)
lin_reg.intercept_,lin_reg.coef_
out:
(array([1.78134581]), array([[0.93366893, 0.56456263]]))
结果还不错(目标结果是2,1,0.5)。
#因为这里要画的图像是二次的,所以要计算出多个点再连线,以使得图像准确
#这里计算了100个点
X_new=np.linspace(-3, 3, 100).reshape(100, 1)
X_new_poly = poly_features.transform(X_new)
y_new = lin_reg.predict(X_new_poly)
plt.plot(X, y, "b.")
plt.title('Figure 4-13. Polynomial Regression model predictions')
plt.plot(X_new, y_new, "r-", linewidth=2, label="Predictions")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.legend(loc="upper left", fontsize=14)
plt.axis([-3, 3, 0, 10])
save_fig("quadratic_predictions_plot")
plt.show()
然而面对实际问题我们并不知道目标函数是几次的,也难以准确的添加平方项作为新特征。针对二次模型,如果我们选用的特征次数太高或者太低会怎样呢?我们考察新特征为1次,2次,300次三种情形:
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
for style, width, degree in (("g-", 1, 300), ("b--", 1, 2), ("r-+", 1, 1)):
#将特征的degree次方添加为新特征
polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
#std_scaler用于参数标准化
std_scaler = StandardScaler()
lin_reg = LinearRegression()
#Pipeline构造器接受(name, transform) tuple的列表作为参数。按顺序执行列表中的transform,完成数据预处理
#这里进行的预处理过程为对数据先添加新特征,然后标准化,最后用线性回归模型拟合求解
polynomial_regression = Pipeline([
("poly_features", polybig_features),
("std_scaler", std_scaler),
("lin_reg", lin_reg),
])
#对原数据X,y进行上述操作求出结果
polynomial_regression.fit(X, y)
y_newbig = polynomial_regression.predict(X_new)
plt.plot(X_new, y_newbig, style, label=str(degree), linewidth=width)
plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
save_fig("high_degree_polynomials_plot")
plt.show()
结论很显然,1次的模型会欠拟合,300次的模型则会过拟合。
如何判断所用的模型是简单了还是复杂了呢?使用交叉验证来估计模型的泛化性能:
如果模型在训练数据上表现良好,但根据交叉验证指标泛化不佳,那么模型就是过拟合了。如果两者都表现不佳,那么它就是欠拟合。
还有一种直观的方法是查看学习曲线,学习曲线反映的是模型在训练集上和验证集上的表现关于训练集大小的函数。
接下来我们绘制学习曲线:
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
def plot_learning_curves(model, X, y):
#train_test_split用于将数据集分成两部分,test_size即为测试集(此处为验证集)占比
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=10)
train_errors, val_errors = [], []
#记录不同个数的样本训练得到的模型对应的训练误差和验证误差
for m in range(1, len(X_train)):
model.fit(X_train[:m], y_train[:m])
y_train_predict = model.predict(X_train[:m])
y_val_predict = model.predict(X_val)
train_errors.append(mean_squared_error(y_train[:m], y_train_predict))
val_errors.append(mean_squared_error(y_val, y_val_predict))
#这里sqrt运算后由MSE得到RMSE,即均方根误差
plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
plt.plot(np.sqrt(val_errors), "b-", linewidth=2, label="val")
plt.legend(loc="upper right", fontsize=14)
plt.xlabel("Training set size", fontsize=14)
plt.ylabel("RMSE", fontsize=14)
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3])
save_fig("underfitting_learning_curves_plot")
plt.show()
首先,让我们看一下训练数据的表现:当训练集中只有一个或两个实例时,模型可以完美地拟合它们,这就是曲线从零开始的原因。但是随着新实例被添加到训练集中,模型不可能完美地拟合训练数据,因为数据是嘈杂的,并且因为它根本不是线性的。因此,训练数据的误差一直持续到达到稳定状态,此时向训练集添加新实例不会使平均误差更好或更差。
其次让我们看一下模型在验证数据上的性能。当模型在极少数训练实例上训练时,它无法正确推广泛化,这就是验证错误最初非常大的原因。 然后,当模型显示更多训练样例时,它会学习,因此验证错误会慢慢下降。然而,直线无法很好地对数据进行建模,因此误差最终处于平稳状态,非常接近另一条曲线。
这些学习曲线是典型的欠拟合模型。两条曲线都达到了稳定水平,它们很接近而且相当高。如果你的模型不适合训练数据,添加更多训练数据将无济于事。你需要使用更复杂的模型或提出更好的特征。
接下来考察一个复杂模型的学习曲线:
from sklearn.pipeline import Pipeline
polynomial_regression = Pipeline([
("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
("lin_reg", LinearRegression()),
])
plot_learning_curves(polynomial_regression, X, y)
plt.axis([0, 80, 0, 3])
plt.title('Figure.4-16')
save_fig("learning_curves_plot")
plt.show()
这个10次多项式模型的学习曲线看起来有点像之前的欠拟合的学习曲线,但有两个非常重要的差异:
(1)训练数据的误差远低于线性回归模型。
(2)曲线之间存在间隙。这意味着模型在训练数据上的表现明显优于验证数据,这是过拟合模型的标志。 但是,如果使用更大的训练集,则两条曲线将继续靠近。
机器学习的一个重要理论结果是模型的泛化错误事实上可以表示为三个非常不同的错误的总和:
(1)Bias-偏差
这部分泛化错误是由于错误的假设,例如假设数据实际上是二次数时是线性的。 高偏差模型最有可能欠拟合训练数据。
(2)Variance-方差
这部分是由于模型对训练数据中的小变化过度敏感。 具有更高次数的模型(例如高次多项式模型)可能具有高方差,因此过拟合训练数据。这就造成了从训练集中选取不同的数据进行训练会得到差别很大的模型。
(3)Irreducible error-不可避免的错误
这部分是由于数据本身的噪音。 减少这部分错误的唯一方法是清洗数据(例如,修复数据源,例如损坏的传感器,或检测并删除异常值)。
增加模型的复杂性通常会增加其方差并减少其偏差。相反,降低模型的复杂性会增加其偏差并减少其方差。 这就是二者被称为具有权衡关系(trade-off)的原因。
3、正则化线性模型
那么问题来了,在不知道目标函数形式的时候,我们如何选择复杂度适当的模型呢?正则化是常用的方法。正则化一个多项式模型,一个简单的方法就是减少多项式的阶数。对于一个线性模型,正则化的典型实现就是约束模型中参数的权重。 接下来介绍三种方法:Ridge 回归(岭回归),Lasso 回归(索套回归)和 Elastic Net(弹性网)。
(1)Ringe Regression(岭回归)
注意正则项是从开始计算的,偏差并不算在内。
因此上式的是除了左上角元素为0对角线上其它元素为1的对角矩阵。
接下来我们可以选择使用闭式解或梯度下降来求解模型:
np.random.seed(42)
m = 20
X = 3 * np.random.rand(m, 1)
y = 1 + 0.5 * X + np.random.randn(m, 1) / 1.5
X_new = np.linspace(0, 3, 100).reshape(100, 1)
#参数**model_kargs实质就是将函数的参数和值,存储在字典类型的kargs变量中
#eg.传入参数(a=1,b=2,c=3),实质上会得到{'a': 1, 'b': 2, 'c': 3}
def plot_model(model_class, polynomial, alphas, **model_kargs):
#zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
#eg.a = [1,2,3],b = [4,5,6],zipped = zip(a,b)=[(1, 4), (2, 5), (3, 6)]
for alpha, style in zip(alphas, ("b-", "g--", "r:")):
model = model_class(alpha, **model_kargs) if alpha > 0 else LinearRegression()
if polynomial:
model = Pipeline([
("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
("std_scaler", StandardScaler()),
("regul_reg", model),
])
model.fit(X, y)
y_new_regul = model.predict(X_new)
plt.plot(X_new, y_new_regul, style, linewidth=1, label=r"$\alpha = {}$".format(alpha))
plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left", fontsize=15)
plt.xlabel("$x_1$", fontsize=18)
plt.axis([0, 3, 0, 4])
plt.figure(figsize=(8,4))
plt.subplot(121)
plt.title('Figure 4-17. Ridge Regression')
#使用普通的Ridge模型,得到线性预测。
plot_model(Ridge, polynomial=False, alphas=(0, 10, 100), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(122)
plt.title('Figure 4-17. Ridge Regression')
#首先使用PolynomialFeatures(degree = 10)扩展数据,然后使用StandardScaler对其进行缩放,
#最后将Ridge模型应用于结果特征:这是具有岭正则化的多项式回归。
plot_model(Ridge, polynomial=True, alphas=(0, 10**-5, 1), random_state=42)
save_fig("ridge_regression_plot")
plt.show()
注意到增加正则项系数将导致更平坦的预测。这减少了模型的方差,但增加了偏差。
我们还可以用Cholesky法计算岭回归的闭式解:
from sklearn.linear_model import Ridge
ridge_reg=Ridge(alpha=1,solver="cholesky")
ridge_reg.fit(X,y)
ridge_reg.predict([[1.5]])
out:
array([[1.55071465]])
随机梯度下降法也可以用来计算岭回归,只需设置罚项为L2正则项即可:
sgd_reg=SGDRegressor(penalty="l2")
sgd_reg.fit(X,y.ravel())
sgd_reg.predict([[1.5]])
out:
array([1.12795912])
可以看到,和之前的闭式解相比相差较大,我们也可以考虑将Ridge类与“sag”解算器(Stochastic Average GD是SGD的变体)一起使用。
ridge_reg = Ridge(alpha=1, solver="sag", random_state=42)
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
out:
array([[1.5507201]])
这样我们就得到了和闭式解很接近的解。
(2)Lasso Regression(索套回归)
Lasso也在损失函数上添加了一个正则化项,但是它使用权重向量的范数而不是权重向量的范数平方的一半。
Lasso 回归的一个重要特征是它倾向于完全消除最不重要的特征的权重(即将它们设置为零),关于这一点,我之前上过的课程中有过一个直观解释:
实现如下:
from sklearn.linear_model import Lasso
plt.figure(figsize=(8,4))
plt.subplot(121)
plt.title('Figure 4-18. Lasso Regression')
plot_model(Lasso, polynomial=False, alphas=(0, 0.1, 1), random_state=42)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.subplot(122)
plt.title('Figure 4-18. Lasso Regression')
plot_model(Lasso, polynomial=True, alphas=(0, 10**-7, 1), tol=1, random_state=42)
save_fig("lasso_regression_plot")
plt.show()
右图中的绿色虚线看起来像一条二次曲线,而且几乎是线性的,这是因为所有的高阶特征都被设置为零。换句话说,Lasso回归自动的进行特征选择同时输出一个稀疏模型。
直接使用Scikit-Learn的Lasso类做Lasso回归的代码如下:
from sklearn.linear_model import Lasso
lasso_reg=Lasso(alpha=0.1)
lasso_reg.fit(X,y)
lasso_reg.predict([[1.5]])
out:
array([1.53788174])
(3)Elastic Net(弹性网)
弹性网络介于 Ridge 回归和 Lasso 回归之间。它的正则项是 Ridge 回归和 Lasso 回归正则项的简单混合。
使用Scikit-Learn的ElasticNet的简短示例如下:
from sklearn.linear_model import ElasticNet
elastic_net =ElasticNet(alpha=0.1,l1_ratio=0.5)
elastic_net.fit(X,y)
elastic_net.predict([[1.5]])
out:
array([1.54333232])
我们如何在这三种方法中做出选择呢?岭回归是一个很好的首选项,但是如果你的特征仅有少数是真正有用的,你应该选择 Lasso 和弹性网络。就像我们讨论的那样,它两能够将无用特征的权重降为零。一般来说,弹性网络的表现要比 Lasso 好,因为当特征数量比样本的数量大的时候,或者特征之间有很强的相关性时,Lasso 可能会表现的不规律。
(4)Early Stopping(早期停止法)
随着训练的进行,算法一直学习,它在训练集上的预测误差(RMSE)自然而然的下降。然而一段时间后,验证误差停止下降,并开始上升。这意味着模型在训练集上开始出现过拟合。一旦验证错误达到最小值,便提早停止训练。这种简单有效的正则化方法被 Geoffrey Hinton 称为“beautiful free lunch”。
我们看一个示例:
np.random.seed(42)
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 2 + X + 0.5 * X**2 + np.random.randn(m, 1)
X_train, X_val, y_train, y_val = train_test_split(X[:50], y[:50].ravel(), test_size=0.5, random_state=10)
#添加高阶特征并正规化
poly_scaler = Pipeline([
("poly_features", PolynomialFeatures(degree=90, include_bias=False)),
("std_scaler", StandardScaler()),
])
X_train_poly_scaled = poly_scaler.fit_transform(X_train)
X_val_poly_scaled = poly_scaler.transform(X_val)
#max_iter表示迭代训练集的最大轮次(epoch)
#当warm_start设置为True时,调用fit()方法后,训练会从停下来的地方继续,而不是从头重新开始
sgd_reg = SGDRegressor(max_iter=1,
penalty=None,
eta0=0.0005,
warm_start=True,
learning_rate="constant",
random_state=42)
n_epochs = 500
train_errors, val_errors = [], []
for epoch in range(n_epochs):
sgd_reg.fit(X_train_poly_scaled, y_train)
y_train_predict = sgd_reg.predict(X_train_poly_scaled)
y_val_predict = sgd_reg.predict(X_val_poly_scaled)
train_errors.append(mean_squared_error(y_train, y_train_predict))
val_errors.append(mean_squared_error(y_val, y_val_predict))
best_epoch = np.argmin(val_errors)
best_val_rmse = np.sqrt(val_errors[best_epoch])
#plt.annotate在图形中添加注释,主要是起到提示作用
# 第一个参数是注释的内容
# xy设置箭头尖的坐标
# xytext设置注释内容显示的起始位置
# arrowprops 用来设置箭头
plt.annotate('Best model',
xy=(best_epoch, best_val_rmse),
xytext=(best_epoch, best_val_rmse + 1),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.05),
fontsize=16,
)
# 这里-0.03只是为了使图形更好看
best_val_rmse -= 0.03
plt.plot([0, n_epochs], [best_val_rmse, best_val_rmse], "k:", linewidth=2)
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="Validation set")
plt.plot(np.sqrt(train_errors), "r--", linewidth=2, label="Training set")
plt.legend(loc="upper right", fontsize=14)
plt.xlabel("Epoch", fontsize=14)
plt.ylabel("RMSE", fontsize=14)
plt.title('Figure 4-20. Early stopping regularization')
save_fig("early_stopping_plot")
plt.show()
对于Stochastic and Mini-batch GD,曲线不是那么平滑,可能很难知道是否达到了最小值。 一种解决方案是仅在验证错误超过最小值一段时间后停止(当确信模型不会做得更好时),然后将模型参数回滚到验证错误最小的点。
对Early Stoping 的一个基本实现如下:
from sklearn.base import clone
sgd_reg=SGDRegressor(max_iter=1,
warm_start=True,
penalty=None,
learning_rate="constant",
eta0=0.0005)
minimum_val_error=float("inf")
best_epoch=None
best_model=None
for epoch in range(1000):
#训练会从停下来的地方继续
sgd_reg.fit(X_train_poly_scaled,y_train)
y_val_predict=sgd_reg.predict(X_val_poly_scaled)
val_error=mean_squared_error(y_val_predict,y_val)
if val_error
best_epoch,best_model
out:
(227,
SGDRegressor(alpha=0.0001, average=False, early_stopping=False, epsilon=0.1,
eta0=0.0005, fit_intercept=True, l1_ratio=0.15,
learning_rate='constant', loss='squared_loss', max_iter=1,
n_iter=None, n_iter_no_change=5, penalty=None, power_t=0.25,
random_state=None, shuffle=True, tol=None, validation_fraction=0.1,
verbose=0, warm_start=True))