【翻译】Sklearn 与 TensorFlow 机器学习实用指南 —— 第4章( 中) 训练模型

浏览: 1633

作者:ApacheCN   Python机器学习爱好者

Python爱好者社区专栏作者

GitHub:https://github.com/apachecn/hands_on_Ml_with_Sklearn_and_TF



前文传送门:



随机梯度下降

批量梯度下降的最要问题是计算每一步的梯度时都需要使用整个训练集,这导致在规模较大的数据集上,其会变得非常的慢。与其完全相反的随机梯度下降,在每一步的梯度计算上只随机选取训练集中的一个样本。很明显,由于每一次的操作都使用了非常少的数据,这样使得算法变得非常快。由于每一次迭代,只需要在内存中有一个实例,这使随机梯度算法可以在大规模训练集上使用。

另一方面,由于它的随机性,与批量梯度下降相比,其呈现出更多的不规律性:它到达最小值不是平缓的下降,损失函数会忽高忽低,只是在大体上呈下降趋势。随着时间的推移,它会非常的靠近最小值,但是它不会停止在一个值上,它会一直在这个值附近摆动(如图 4-9)。因此,当算法停止的时候,最后的参数还不错,但不是最优值。

图4-9:随机梯度下降

当损失函数很不规则时(如图 4-6),随机梯度下降算法能够跳过局部最小值。因此,随机梯度下降在寻找全局最小值上比批量梯度下降表现要好。

虽然随机性可以很好的跳过局部最优值,但同时它却不能达到最小值。解决这个难题的一个办法是逐渐降低学习率。 开始时,走的每一步较大(这有助于快速前进同时跳过局部最小值),然后变得越来越小,从而使算法到达全局最小值。 这个过程被称为模拟退火,因为它类似于熔融金属慢慢冷却的冶金学退火过程。 决定每次迭代的学习率的函数称为learning schedule。 如果学习速度降低得过快,你可能会陷入局部最小值,甚至在到达最小值的半路就停止了。 如果学习速度降低得太慢,你可能在最小值的附近长时间摆动,同时如果过早停止训练,最终只会出现次优解。

下面的代码使用一个简单的learning schedule来实现随机梯度下降:

n_epochs = 50 
t0, t1 = 5, 50  #learning_schedule的超参数
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):
       random_index = np.random.randint(m)
       xi = X_b[random_index:random_index+1]
       yi = y[random_index:random_index+1]
       gradients = 2 * xi.T.dot(xi,dot(theta)-yi)
       eta = learning_schedule(epoch * m + i)
       theta = theta - eta * gradiens

按习惯来讲,我们进行 $m$ 轮的迭代,每一轮迭代被称为一代。在整个训练集上,随机梯度下降迭代了 1000 次时,一般在第 50 次的时候就可以达到一个比较好的结果。

>>> theta
array([[4.21076011],[2.748560791]])

图 4-10 展示了前 10 次的训练过程(注意每一步的不规则程度)。

图 4-10:随机梯度下降的前10次迭代

由于每个实例的选择是随机的,有的实例可能在每一代中都被选到,这样其他的实例也可能一直不被选到。如果你想保证每一代迭代过程,算法可以遍历所有实例,一种方法是将训练集打乱重排,然后选择一个实例,之后再继续打乱重排,以此类推一直进行下去。但是这样收敛速度会非常的慢。

通过使用 Scikit-Learn 完成线性回归的随机梯度下降,你需要使用SGDRegressor类,这个类默认优化的是均方差损失函数。下面的代码迭代了 50 代,其学习率 $\eta$ 为0.1(eta0=0.1),使用默认的learning schedule(与前面的不一样),同时也没有添加任何正则项(penalty = None):

from sklearn.linear_model import SGDRegressor
sgd_reg + SGDRregressor(n_iter=50, penalty=None, eta0=0.1)
sgd_reg.fit(X,y.ravel())

你可以再一次发现,这个结果非常的接近正态方程的解:

>>> sgd_reg.intercept_, sgd_reg.coef_
(array([4.18380366]),array([2.74205299]))

小批量梯度下降

最后一个梯度下降算法,我们将介绍小批量梯度下降算法。一旦你理解了批量梯度下降和随机梯度下降,再去理解小批量梯度下降是非常简单的。在迭代的每一步,批量梯度使用整个训练集,随机梯度时候用仅仅一个实例,在小批量梯度下降中,它则使用一个随机的小型实例集。它比随机梯度的主要优点在于你可以通过矩阵运算的硬件优化得到一个较好的训练表现,尤其当你使用 GPU 进行运算的时候。

小批量梯度下降在参数空间上的表现比随机梯度下降要好的多,尤其在有大量的小型实例集时。作为结果,小批量梯度下降会比随机梯度更靠近最小值。但是,另一方面,它有可能陷在局部最小值中(在遇到局部最小值问题的情况下,和我们之前看到的线性回归不一样)。 图4-11显示了训练期间三种梯度下降算法在参数空间中所采用的路径。 他们都接近最小值,但批量梯度的路径最后停在了最小值,而随机梯度和小批量梯度最后都在最小值附近摆动。 但是,不要忘记,批次梯度需要花费大量时间来完成每一步,但是,如果你使用了一个较好的learning schedule,随机梯度和小批量梯度也可以得到最小值。

图 4-11:参数空间的梯度下降路径

让我比较一下目前我们已经探讨过的对线性回归的梯度下降算法。如表 4-1 所示,其中 $m$ 表示训练样本的个数,$n$ 表示特征的个数。

表 4-1:比较线性回归的不同梯度下降算法

提示上述算法在完成训练后,得到的参数基本没什么不同,它们会得到非常相似的模型,最后会以一样的方式去进行预测。

多项式回归

如果你的数据实际上比简单的直线更复杂呢? 令人惊讶的是,你依然可以使用线性模型来拟合非线性数据。 一个简单的方法是对每个特征进行加权后作为新的特征,然后训练一个线性模型在这个扩展的特征集。 这种方法称为多项式回归。

让我们看一个例子。 首先,我们根据一个简单的二次方程(并加上一些噪声,如图 4-12)来生成一些非线性数据:

m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)

图 4-12:生产加入噪声的非线性数据

很清楚的看出,直线不能恰当的拟合这些数据。于是,我们使用 Scikit-Learning 的PolynomialFeatures类进行训练数据集的转换,让训练集中每个特征的平方(2 次多项式)作为新特征(在这种情况下,仅存在一个特征):

>>> from sklearn.preprocessing import PolynomialFeatures
>>> poly_features = PolynomialFeatures(degree=2,include_bias=False)
>>> X_poly = poly_features.fit_transform(X)
>>> X[0]
array([-0.75275929])
>>> X_poly[0]
array([-0.75275929, 0.56664654])

X_poly现在包含原始特征$X$并加上了这个特征的平方 $X^2$。现在你可以在这个扩展训练集上使用LinearRegression模型进行拟合,如图 4-13:

>>> lin_reg = LinearRegression()
>>> lin_reg.fit(X_poly, y)
>>> lin_reg.intercept_, lin_reg.coef_
(array([ 1.78134581]), array([[ 0.93366893, 0.56456263]]))

图 4-13:多项式回归模型预测

还是不错的,模型预测函数 $\hat{y}=0.56x_1^2+0.93x_1+1.78$,事实上原始函数为 $y=0.5x_1^2+1.0x_1+2.0$ 再加上一些高斯噪声。

请注意,当存在多个特征时,多项式回归能够找出特征之间的关系(这是普通线性回归模型无法做到的)。 这是因为LinearRegression会自动添加当前阶数下特征的所有组合。例如,如果有两个特征 $a,b$,使用 3 阶(degree=3)的LinearRegression时,不仅有 $a^2,a^3,b^2$ 以及 $b^3$,同时也会有它们的其他组合项 $ab,a^2b,ab^2$。

提示PolynomialFeatures(degree=d)把一个包含 $n$ 个特征的数组转换为一个包含 $\frac{(n+d)!}{d!n!}$ 特征的数组,$n!$ 表示 $n$ 的阶乘,等于 $1 * 2 * 3 \cdots * n$。小心大量特征的组合爆炸!

学习曲线

如果你使用一个高阶的多项式回归,你可能发现它的拟合程度要比普通的线性回归要好的多。例如,图 4-14 使用一个 300 阶的多项式模型去拟合之前的数据集,并同简单线性回归、2 阶的多项式回归进行比较。注意 300 阶的多项式模型如何摆动以尽可能接近训练实例。

图 4-14:高阶多项式回归

当然,这种高阶多项式回归模型在这个训练集上严重过拟合了,线性模型则欠拟合。在这个训练集上,二次模型有着较好的泛化能力。那是因为在生成数据时使用了二次模型,但是一般我们不知道这个数据生成函数是什么,那我们该如何决定我们模型的复杂度呢?你如何告诉我你的模型是过拟合还是欠拟合?

在第二章,你可以使用交叉验证来估计一个模型的泛化能力。如果一个模型在训练集上表现良好,通过交叉验证指标却得出其泛化能力很差,那么你的模型就是过拟合了。如果在这两方面都表现不好,那么它就是欠拟合了。这种方法可以告诉我们,你的模型是太复杂还是太简单了。

另一种方法是观察学习曲线:画出模型在训练集上的表现,同时画出以训练集规模为自变量的训练集函数。为了得到图像,需要在训练集的不同规模子集上进行多次训练。下面的代码定义了一个函数,用来画出给定训练集后的模型学习曲线:

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
def plot_learning_curves(model, X, y):
   X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
   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_predict, y_train[:m]))
       val_errors.append(mean_squared_error(y_val_predict, y_val))
plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
plt.plot(np.sqrt(val_errors), "", linewidth=3, label="val")

我们一起看一下简单线性回归模型的学习曲线(图 4-15):

lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)

图 4-15:学习曲线

这幅图值得我们深究。首先,我们观察训练集的表现:当训练集只有一两个样本的时候,模型能够非常好的拟合它们,这也是为什么曲线是从零开始的原因。但是当加入了一些新的样本的时候,训练集上的拟合程度变得难以接受,出现这种情况有两个原因,一是因为数据中含有噪声,另一个是数据根本不是线性的。因此随着数据规模的增大,误差也会一直增大,直到达到高原地带并趋于稳定,在之后,继续加入新的样本,模型的平均误差不会变得更好或者更差。我们继续来看模型在验证集上的表现,当以非常少的样本去训练时,模型不能恰当的泛化,也就是为什么验证误差一开始是非常大的。当训练样本变多的到时候,模型学习的东西变多,验证误差开始缓慢的下降。但是一条直线不可能很好的拟合这些数据,因此最后误差会到达在一个高原地带并趋于稳定,最后和训练集的曲线非常接近。

上面的曲线表现了一个典型的欠拟合模型,两条曲线都到达高原地带并趋于稳定,并且最后两条曲线非常接近,同时误差值非常大。

提示如果你的模型在训练集上是欠拟合的,添加更多的样本是没用的。你需要使用一个更复杂的模型或者找到更好的特征。

现在让我们看一个在相同数据上10阶多项式模型拟合的学习曲线(图 4-16):

from sklearn.pipeline import Pipeline
polynomial_regression = Pipeline((
   ("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
   ("sgd_reg", LinearRegression()),
))
plot_learning_curves(polynomial_regression, X, y)

这幅图像和之前的有一点点像,但是其有两个非常重要的不同点:

  • 在训练集上,误差要比线性回归模型低的多。
  • 图中的两条曲线之间有间隔,这意味模型在训练集上的表现要比验证集上好的多,这也是模型过拟合的显著特点。当然,如果你使用了更大的训练数据,这两条曲线最后会非常的接近。

图4-16:多项式模型的学习曲线

提示改善模型过拟合的一种方法是提供更多的训练数据,直到训练误差和验证误差相等。偏差和方差的权衡在统计和机器学习领域有个重要的理论:一个模型的泛化误差由三个不同误差的和决定:
  • 偏差:泛化误差的这部分误差是由于错误的假设决定的。例如实际是一个二次模型,你却假设了一个线性模型。一个高偏差的模型最容易出现欠拟合。
  • 方差:这部分误差是由于模型对训练数据的微小变化较为敏感,一个多自由度的模型更容易有高的方差(例如一个高阶多项式模型),因此会导致模型过拟合。
  • 不可约误差:这部分误差是由于数据本身的噪声决定的。降低这部分误差的唯一方法就是进行数据清洗(例如:修复数据源,修复坏的传感器,识别和剔除异常值)。

线性模型的正则化

正如我们在第一和第二章看到的那样,降低模型的过拟合的好方法是正则化这个模型(即限制它):模型有越少的自由度,就越难以拟合数据。例如,正则化一个多项式模型,一个简单的方法就是减少多项式的阶数。

对于一个线性模型,正则化的典型实现就是约束模型中参数的权重。 接下来我们将介绍三种不同约束权重的方法:Ridge 回归,Lasso 回归和 Elastic Net。

岭(Ridge)回归

岭回归(也称为 Tikhonov 正则化)是线性回归的正则化版:在损失函数上直接加上一个正则项 $\alpha\sum_{i=1}^n\theta_i^2$。这使得学习算法不仅能够拟合数据,而且能够使模型的参数权重尽量的小。注意到这个正则项只有在训练过程中才会被加到损失函数。当得到完成训练的模型后,我们应该使用没有正则化的测量方法去评价模型的表现。

提示一般情况下,训练过程使用的损失函数和测试过程使用的评价函数是不一样的。除了正则化,还有一个不同:训练时的损失函数应该在优化过程中易于求导,而在测试过程中,评价函数更应该接近最后的客观表现。一个好的例子:在分类训练中我们使用对数损失(马上我们会讨论它)作为损失函数,但是我们却使用精确率/召回率来作为它的评价函数。

超参数 $\alpha$ 决定了你想正则化这个模型的强度。如果 $\alpha=0$ 那此时的岭回归便变为了线性回归。如果 $\alpha$ 非常的大,所有的权重最后都接近于零,最后结果将是一条穿过数据平均值的水平直线。公式 4-8 是岭回归的损失函数:

公式 4-8:岭回归损失函数

$$ J(\theta)=MSE(\theta)+\alpha\frac{1}{2}\sum\limits_{i=1}^n\theta_i^2 $$

值得注意的是偏差 $\theta_0$ 是没有被正则化的(累加运算的开始是 $i=1$ 而不是 $i=0$)。如我定义 $\mathbf{w}$ 作为特征的权重向量($\theta_1$ 到 $\theta_n$),那么正则项可以简写成 $\frac{1}{2}{({\parallel \mathbf{w}\parallel_2})}^2$,其中 $\parallel \cdot \parallel_2 $ 表示权重向量的 $\ell_2$ 范数。对于梯度下降来说仅仅在均方差梯度向量(公式 4-6)加上一项 $\alpha\mathbf{w}$。

提示在使用岭回归前,对数据进行放缩(可以使用StandardScaler)是非常重要的,算法对于输入特征的数值尺度(scale)非常敏感。大多数的正则化模型都是这样的。

图 4-17 展示了在相同线性数据上使用不同 $\alpha$ 值的岭回归模型最后的表现。左图中,使用简单的岭回归模型,最后得到了线性的预测。右图中的数据首先使用 10 阶的PolynomialFearures进行扩展,然后使用StandardScaler进行缩放,最后将岭模型应用在处理过后的特征上。这就是带有岭正则项的多项式回归。注意当$\alpha$增大的时候,导致预测曲线变得扁平(即少了极端值,多了一般值),这样减少了模型的方差,却增加了模型的偏差。

对线性回归来说,对于岭回归,我们可以使用封闭方程去计算,也可以使用梯度下降去处理。它们的缺点和优点是一样的。公式 4-9 表示封闭方程的解(矩阵 $\mathbf{A}$ 是一个除了左上角有一个 $0$ 的 $n \times n$ 的单位矩,这个 $0$ 代表偏差项。译者注:偏差 $\theta_0$ 不被正则化的)。

图 4-17:岭回归

公式 4-9:岭回归的封闭方程的解

$$ \hat{\theta} = ({\mathbf{X}}^T\cdot\mathbf{X}+\alpha\mathbf{A})^{-1}\cdot{\mathbf{X}}^T\cdot\mathbf{y} $$

下面是如何使用 Scikit-Learn 来进行封闭方程的求解(使用 Cholesky 法进行矩阵分解对公式 4-9 进行变形):

>>> from sklearn.linear_model import Ridge
>>> ridge_reg = Ridge(alpha=1, solver="cholesky")
>>> ridge_reg.fit(X, y)
>>> ridge_reg.predict([[1.5]])
array([[ 1.55071465]]

使用随机梯度法进行求解:

>>> sgd_reg = SGDRegressor(penalty="l2")
>>> sgd_reg.fit(X, y.ravel())
>>> sgd_reg.predict([[1.5]])
array([[ 1.13500145]])

penalty参数指的是正则项的惩罚类型。指定“l2”表明你要在损失函数上添加一项:权重向量 $\ell_2$ 范数平方的一半,这就是简单的岭回归。

Lasso 回归

Lasso 回归(也称 Least Absolute Shrinkage,或者 Selection Operator Regression)是另一种正则化版的线性回归:就像岭回归那样,它也在损失函数上添加了一个正则化项,但是它使用权重向量的 $\ell_1$ 范数而不是权重向量 $\ell_2$ 范数平方的一半。(如公式 4-10)

公式 4-10:Lasso 回归的损失函数

$$ J(\theta)=MSE(\theta)+\alpha\sum\limits_{i=1}^n\left|\theta_i \right| $$

图 4-18 展示了和图 4-17 相同的事情,仅仅是用 Lasso 模型代替了 Ridge 模型,同时调小了 $\alpha$ 的值。

图 4-18:Lasso回归

Lasso 回归的一个重要特征是它倾向于完全消除最不重要的特征的权重(即将它们设置为零)。例如,右图中的虚线所示($\alpha=10^{-7}$),曲线看起来像一条二次曲线,而且几乎是线性的,这是因为所有的高阶多项特征都被设置为零。换句话说,Lasso回归自动的进行特征选择同时输出一个稀疏模型(即,具有很少的非零权重)。

你可以从图 4-19 知道为什么会出现这种情况:在左上角图中,后背景的等高线(椭圆)表示了没有正则化的均方差损失函数($\alpha=0$),白色的小圆圈表示在当前损失函数上批量梯度下降的路径。前背景的等高线(菱形)表示$\ell_1$惩罚,黄色的三角形表示了仅在这个惩罚下批量梯度下降的路径($\alpha\rightarrow\infty$)。注意路径第一次是如何到达 $\theta_1=0$,然后向下滚动直到它到达 $\theta_2=0$。在右上角图中,等高线表示的是相同损失函数再加上一个 $\alpha=0.5$ 的 $\ell_1$ 惩罚。这幅图中,它的全局最小值在 $\theta_2=0$ 这根轴上。批量梯度下降首先到达 $\theta_2=0$,然后向下滚动直到达到全局最小值。 两个底部图显示了相同的情况,只是使用了 $\ell_2$ 惩罚。 规则化的最小值比非规范化的最小值更接近于 $\theta=0$,但权重不能完全消除。

图 4-19:Ridge 回归和 Lasso 回归对比

提示在 Lasso 损失函数中,批量梯度下降的路径趋向与在低谷有一个反弹。这是因为在 $\theta_2=0$ 时斜率会有一个突变。为了最后真正收敛到全局最小值,你需要逐渐的降低学习率。

Lasso 损失函数在 $\theta_i=0(i=1,2,\cdots,n) $ 处无法进行微分运算,但是梯度下降如果你使用子梯度向量 $\mathbf{g}$ 后它可以在任何 $\theta_i=0$ 的情况下进行计算。公式 4-11 是在 Lasso 损失函数上进行梯度下降的子梯度向量公式。

公式 4-11:Lasso 回归子梯度向量

$$ g(\theta,J)=\nabla_{\theta}MSE(\theta)+ \alpha{\left(\begin{matrix} sign(\theta_1)\ sign(\theta_2)\ \vdots \ sign(\theta_n)\ \end{matrix}\right)}

where\ sign(\theta_i)= \begin{cases} -1, &\theta_i<0 \ 0, &\theta_i=0 \ +1,&\theta_i>0 \ \end{cases} $$

下面是一个使用 Scikit-Learn 的Lasso类的小例子。你也可以使用SGDRegressor(penalty="l1")来代替它。

>>> from sklearn.linear_model import Lasso
>>> lasso_reg = Lasso(alpha=0.1)
>>> lasso_reg.fit(X, y)
>>> lasso_reg.predict([[1.5]])
array([ 1.53788174]

Python爱好者社区历史文章大合集:

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。

推荐 0
本文由 Python爱好者社区 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册