1.5. 随机梯度下降#

随机梯度下降 (SGD) 是一种简单但非常有效的拟合线性分类器和回归器的方法,这些分类器和回归器在凸损失函数下,例如(线性)支持向量机逻辑回归。尽管 SGD 在机器学习社区中已经存在很长时间了,但它最近在大型学习的背景下获得了相当多的关注。

SGD 已成功应用于文本分类和自然语言处理中经常遇到的大型稀疏机器学习问题。鉴于数据是稀疏的,此模块中的分类器可以轻松扩展到具有超过 10^5 个训练示例和超过 10^5 个特征的问题。

严格来说,SGD 仅仅是一种优化技术,并不对应于特定的机器学习模型家族。它只是一种训练模型的方式。通常,SGDClassifierSGDRegressor 的实例在 scikit-learn API 中会有一个等效的估计器,可能使用不同的优化技术。例如,使用 SGDClassifier(loss='log_loss') 会导致逻辑回归,即与 LogisticRegression 等效的模型,它通过 SGD 拟合而不是通过 LogisticRegression 中的其他求解器之一拟合。类似地,SGDRegressor(loss='squared_error', penalty='l2')Ridge 通过不同的方式解决相同的优化问题。

随机梯度下降的优点包括

  • 效率。

  • 易于实现(大量代码调整的机会)。

随机梯度下降的缺点包括

  • SGD 需要许多超参数,例如正则化参数和迭代次数。

  • SGD 对特征缩放很敏感。

警告

确保在拟合模型之前对训练数据进行置换(洗牌),或者使用 shuffle=True 在每次迭代后进行置换(默认使用)。此外,理想情况下,特征应该使用例如 make_pipeline(StandardScaler(), SGDClassifier()) 进行标准化(参见 管道)。

1.5.1. 分类#

SGDClassifier 实现了一个简单的随机梯度下降学习程序,它支持不同的损失函数和用于分类的惩罚。以下是使用铰链损失训练的 SGDClassifier 的决策边界,等效于线性 SVM。

../_images/sphx_glr_plot_sgd_separating_hyperplane_001.png

与其他分类器一样,SGD 必须使用两个数组进行拟合:一个形状为 (n_samples, n_features) 的数组 X,包含训练样本,以及一个形状为 (n_samples,) 的数组 y,包含训练样本的目标值(类别标签)。

>>> from sklearn.linear_model import SGDClassifier
>>> X = [[0., 0.], [1., 1.]]
>>> y = [0, 1]
>>> clf = SGDClassifier(loss="hinge", penalty="l2", max_iter=5)
>>> clf.fit(X, y)
SGDClassifier(max_iter=5)

拟合后,模型就可以用来预测新的值。

>>> clf.predict([[2., 2.]])
array([1])

SGD 对训练数据拟合一个线性模型。 coef_ 属性保存模型参数。

>>> clf.coef_
array([[9.9..., 9.9...]])

intercept_ 属性保存截距(也称为偏移量或偏差)。

>>> clf.intercept_
array([-9.9...])

模型是否应该使用截距,即有偏超平面,由参数 fit_intercept 控制。

到超平面的有符号距离(计算为系数与输入样本的点积加上截距)由 SGDClassifier.decision_function 给出。

>>> clf.decision_function([[2., 2.]])
array([29.6...])

具体的损失函数可以通过 loss 参数设置。 SGDClassifier 支持以下损失函数。

  • loss="hinge": (软间隔) 线性支持向量机。

  • loss="modified_huber": 平滑铰链损失。

  • loss="log_loss": 逻辑回归。

  • 以及下面所有回归损失。在这种情况下,目标被编码为 -1 或 1,问题被视为回归问题。预测的类别然后对应于预测目标的符号。

请参考 下面的数学部分 获取公式。前两个损失函数是惰性的,它们只在示例违反边际约束时更新模型参数,这使得训练非常高效,并且可能导致更稀疏的模型(即具有更多零系数),即使使用 L2 惩罚。

使用 loss="log_loss"loss="modified_huber" 可以启用 predict_proba 方法,该方法为每个样本 \(x\) 提供一个概率估计向量 \(P(y|x)\)

>>> clf = SGDClassifier(loss="log_loss", max_iter=5).fit(X, y)
>>> clf.predict_proba([[1., 1.]]) 
array([[0.00..., 0.99...]])

具体的惩罚可以通过 penalty 参数设置。SGD 支持以下惩罚。

  • penalty="l2": 对 coef_ 的 L2 范数惩罚。

  • penalty="l1": 对 coef_ 的 L1 范数惩罚。

  • penalty="elasticnet": L2 和 L1 的凸组合; (1 - l1_ratio) * L2 + l1_ratio * L1

默认设置是 penalty="l2"。L1 惩罚会导致稀疏解,将大多数系数驱动为零。弹性网络 [11] 解决了一些在高度相关属性存在的情况下 L1 惩罚的不足之处。参数 l1_ratio 控制 L1 和 L2 惩罚的凸组合。

SGDClassifier 通过在“一对多”(OVA)方案中组合多个二元分类器来支持多类分类。对于每个 \(K\) 类,都会学习一个二元分类器,用于区分该类与所有其他 \(K-1\) 类。在测试时,我们计算每个分类器的置信度得分(即到超平面的有符号距离),并选择置信度最高的类别。下图说明了在鸢尾花数据集上的 OVA 方法。虚线表示三个 OVA 分类器;背景颜色显示由三个分类器引起的决策面。

../_images/sphx_glr_plot_sgd_iris_001.png

在多类分类的情况下, coef_ 是一个形状为 (n_classes, n_features) 的二维数组,而 intercept_ 是一个形状为 (n_classes,) 的一维数组。 coef_ 的第 i 行保存第 i 类 OVA 分类器的权重向量;类别按升序索引(参见属性 classes_)。请注意,原则上,由于它们允许创建概率模型,因此 loss="log_loss"loss="modified_huber" 更适合一对多分类。

SGDClassifier 通过拟合参数 class_weightsample_weight 支持加权类别和加权实例。请参见下面的示例以及 SGDClassifier.fit 的文档字符串以获取更多信息。

SGDClassifier 支持平均 SGD (ASGD) [10]。可以通过设置 average=True 来启用平均。ASGD 执行与常规 SGD 相同的更新(参见 数学公式),但不是使用系数的最后一个值作为 coef_ 属性(即最后一次更新的值),而是将 coef_ 设置为所有更新中系数的 **平均** 值。对 intercept_ 属性也执行相同的操作。当使用 ASGD 时,学习率可以更大,甚至可以保持恒定,这在某些数据集上会导致训练时间的加快。

对于使用逻辑损失的分类,另一种具有平均策略的 SGD 变体可用于随机平均梯度 (SAG) 算法,该算法可作为 LogisticRegression 中的求解器使用。

示例

1.5.2. 回归#

SGDRegressor 实现了一个简单的随机梯度下降学习程序,它支持不同的损失函数和惩罚项来拟合线性回归模型。 SGDRegressor 非常适合训练样本数量较多(> 10.000)的回归问题,对于其他问题,我们推荐使用 RidgeLassoElasticNet

具体的损失函数可以通过 loss 参数设置。 SGDRegressor 支持以下损失函数

  • loss="squared_error": 普通最小二乘法,

  • loss="huber": 用于稳健回归的 Huber 损失,

  • loss="epsilon_insensitive": 线性支持向量回归。

请参考 下面的数学部分 获取公式。Huber 和 epsilon-insensitive 损失函数可用于稳健回归。不敏感区域的宽度需要通过参数 epsilon 指定。此参数取决于目标变量的尺度。

penalty 参数决定要使用的正则化(见上面分类部分的描述)。

SGDRegressor 还支持平均 SGD [10](同样,见上面分类部分的描述)。

对于具有平方损失和 l2 惩罚的回归,另一种具有平均策略的 SGD 变体可用于随机平均梯度 (SAG) 算法,它作为 Ridge 中的求解器提供。

1.5.3. 在线一类 SVM#

sklearn.linear_model.SGDOneClassSVM 使用随机梯度下降实现了在线线性版本的一类 SVM。结合核近似技术,sklearn.linear_model.SGDOneClassSVM 可用于近似 sklearn.svm.OneClassSVM 中实现的核化一类 SVM 的解,其线性复杂度与样本数量成正比。请注意,核化一类 SVM 的复杂度至少与样本数量的平方成正比。因此,sklearn.linear_model.SGDOneClassSVM 非常适合训练样本数量较多(> 10,000)的数据集,对于这些数据集,SGD 变体可以快几个数量级。

数学细节#

它的实现基于随机梯度下降的实现。实际上,一类 SVM 的原始优化问题由下式给出

\[\begin{split}\begin{aligned} \min_{w, \rho, \xi} & \quad \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \xi_i \\ \text{s.t.} & \quad \langle w, x_i \rangle \geq \rho - \xi_i \quad 1 \leq i \leq n \\ & \quad \xi_i \geq 0 \quad 1 \leq i \leq n \end{aligned}\end{split}\]

其中 \(\nu \in (0, 1]\) 是用户指定的参数,用于控制异常值的比例和支持向量的比例。去除松弛变量 \(\xi_i\),这个问题等价于

\[\min_{w, \rho} \frac{1}{2}\Vert w \Vert^2 - \rho + \frac{1}{\nu n} \sum_{i=1}^n \max(0, \rho - \langle w, x_i \rangle) \, .\]

乘以常数 \(\nu\) 并引入截距 \(b = 1 - \rho\),我们得到以下等价优化问题

\[\min_{w, b} \frac{\nu}{2}\Vert w \Vert^2 + b\nu + \frac{1}{n} \sum_{i=1}^n \max(0, 1 - (\langle w, x_i \rangle + b)) \, .\]

这类似于我们在 数学公式 部分研究的优化问题,其中 \(y_i = 1, 1 \leq i \leq n\)\(\alpha = \nu/2\)\(L\) 是铰链损失函数,\(R\) 是 L2 范数。我们只需要在优化循环中添加项 \(b\nu\)

SGDClassifierSGDRegressor 一样,SGDOneClassSVM 支持平均 SGD。通过设置 average=True 可以启用平均。

1.5.4. 用于稀疏数据的随机梯度下降#

注意

稀疏实现产生的结果与密集实现略有不同,这是由于截距的学习率缩小了。见 实现细节

内置支持任何以 scipy.sparse 支持的格式给出的稀疏数据。但是,为了获得最大效率,请使用 scipy.sparse.csr_matrix 中定义的 CSR 矩阵格式。

示例

1.5.5. 复杂度#

SGD 的主要优势是它的效率,它基本上与训练样本的数量成线性关系。如果 X 是大小为 (n, p) 的矩阵,则训练的成本为 \(O(k n \bar p)\),其中 k 是迭代次数(时期),\(\bar p\) 是每个样本的非零属性的平均数量。

然而,最近的理论结果表明,获得某种所需优化精度的运行时间不会随着训练集大小的增加而增加。

1.5.6. 停止准则#

SGDClassifierSGDRegressor 提供了两个准则,当达到给定的收敛水平时,算法将停止。

  • 使用 early_stopping=True,输入数据将被分成训练集和验证集。然后,模型将在训练集上进行拟合,停止准则基于在验证集上计算的预测分数(使用 score 方法)。验证集的大小可以通过参数 validation_fraction 更改。

  • 使用 early_stopping=False,模型将在整个输入数据上进行拟合,停止准则基于在训练数据上计算的目标函数。

在这两种情况下,准则都会在每个时期评估一次,当准则连续 n_iter_no_change 次没有改进时,算法将停止。改进将使用绝对容差 tol 进行评估,算法在任何情况下都会在最大迭代次数 max_iter 后停止。

1.5.7. 实际使用技巧#

  • 随机梯度下降对特征缩放很敏感,因此强烈建议对数据进行缩放。例如,将输入向量 X 上的每个属性缩放到 [0,1] 或 [-1,+1],或将其标准化为均值为 0,方差为 1。请注意,必须对测试向量应用相同的缩放才能获得有意义的结果。这可以使用 StandardScaler 轻松完成。

    from sklearn.preprocessing import StandardScaler
    scaler = StandardScaler()
    scaler.fit(X_train)  # Don't cheat - fit only on training data
    X_train = scaler.transform(X_train)
    X_test = scaler.transform(X_test)  # apply same transformation to test data
    
    # Or better yet: use a pipeline!
    from sklearn.pipeline import make_pipeline
    est = make_pipeline(StandardScaler(), SGDClassifier())
    est.fit(X_train)
    est.predict(X_test)
    

    如果您的属性具有内在尺度(例如词频或指标特征),则不需要缩放。

  • 找到合理的正则化项 \(\alpha\) 最好使用自动超参数搜索,例如 GridSearchCVRandomizedSearchCV,通常在 10.0**-np.arange(1,7) 范围内。

  • 根据经验,我们发现 SGD 在观察大约 10^6 个训练样本后收敛。因此,迭代次数的合理首选是 max_iter = np.ceil(10**6 / n),其中 n 是训练集的大小。

  • 如果您将 SGD 应用于使用 PCA 提取的特征,我们发现通常明智的做法是将特征值按某个常数 c 缩放,使训练数据的平均 L2 范数等于 1。

  • 我们发现平均 SGD 在特征数量更多且 eta0 更高的情况下效果最佳。

参考文献

1.5.8. 数学公式#

这里我们描述了 SGD 过程的数学细节。可以在 [12] 中找到一个包含收敛速度的良好概述。

给定一组训练样本 \((x_1, y_1), \ldots, (x_n, y_n)\),其中 \(x_i \in \mathbf{R}^m\)\(y_i \in \mathcal{R}\) (\(y_i \in {-1, 1}\) 用于分类),我们的目标是学习一个线性评分函数 \(f(x) = w^T x + b\),其模型参数为 \(w \in \mathbf{R}^m\),截距为 \(b \in \mathbf{R}\)。为了对二元分类进行预测,我们只需查看 \(f(x)\) 的符号。为了找到模型参数,我们最小化正则化训练误差,该误差由下式给出

\[E(w,b) = \frac{1}{n}\sum_{i=1}^{n} L(y_i, f(x_i)) + \alpha R(w)\]

其中 \(L\) 是一个损失函数,用于衡量模型的(不)拟合程度,\(R\) 是一个正则化项(也称为惩罚项),用于惩罚模型的复杂性;\(\alpha > 0\) 是一个非负超参数,用于控制正则化强度。

损失函数细节#

\(L\) 的不同选择会导致不同的分类器或回归器

  • 铰链(软边距):等效于支持向量机分类。 \(L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))\)

  • 感知器: \(L(y_i, f(x_i)) = \max(0, - y_i f(x_i))\)

  • 修改后的 Huber: \(L(y_i, f(x_i)) = \max(0, 1 - y_i f(x_i))^2\) 如果 \(y_i f(x_i) > -1\),否则 \(L(y_i, f(x_i)) = -4 y_i f(x_i)\)

  • 对数损失:等效于逻辑回归。 \(L(y_i, f(x_i)) = \log(1 + \exp (-y_i f(x_i)))\)

  • 平方误差:线性回归(取决于 \(R\) 的岭回归或 Lasso 回归)。 \(L(y_i, f(x_i)) = \frac{1}{2}(y_i - f(x_i))^2\)

  • Huber:对异常值的敏感度低于最小二乘法。当 \(|y_i - f(x_i)| \leq \varepsilon\) 时,它等效于最小二乘法,否则 \(L(y_i, f(x_i)) = \varepsilon |y_i - f(x_i)| - \frac{1}{2} \varepsilon^2\)

  • ε-不敏感: (软边距)等效于支持向量机回归。 \(L(y_i, f(x_i)) = \max(0, |y_i - f(x_i)| - \varepsilon)\)

所有上述损失函数都可以被视为误分类误差(零一损失)的上限,如下图所示。

../_images/sphx_glr_plot_sgd_loss_functions_001.png

正则化项 \(R\)penalty 参数)的常用选择包括

  • L2 范数: \(R(w) := \frac{1}{2} \sum_{j=1}^{m} w_j^2 = ||w||_2^2\)

  • L1 范数: \(R(w) := \sum_{j=1}^{m} |w_j|\),这会导致稀疏解。

  • 弹性网络: \(R(w) := \frac{\rho}{2} \sum_{j=1}^{n} w_j^2 + (1-\rho) \sum_{j=1}^{m} |w_j|\),L2 和 L1 的凸组合,其中 \(\rho\)1 - l1_ratio 给出。

下图显示了当 \(R(w) = 1\) 时,二维参数空间 (\(m=2\)) 中不同正则化项的等高线。

../_images/sphx_glr_plot_sgd_penalties_001.png

1.5.8.1. SGD#

随机梯度下降是一种用于无约束优化问题的优化方法。与(批次)梯度下降相比,SGD 通过一次考虑一个训练样本来近似 \(E(w,b)\) 的真实梯度。

SGDClassifier 实现了一阶 SGD 学习例程。该算法迭代训练样本,并针对每个样本根据由下式给出的更新规则更新模型参数

\[w \leftarrow w - \eta \left[\alpha \frac{\partial R(w)}{\partial w} + \frac{\partial L(w^T x_i + b, y_i)}{\partial w}\right]\]

其中 \(\eta\) 是学习率,它控制参数空间中的步长。截距 \(b\) 的更新方式类似,但没有正则化(并且对稀疏矩阵有额外的衰减,如 实现细节 中所述)。

学习率 \(\eta\) 可以是常数,也可以是逐渐衰减的。对于分类,默认学习率计划 (learning_rate='optimal') 由下式给出

\[\eta^{(t)} = \frac {1}{\alpha (t_0 + t)}\]

其中 \(t\) 是时间步长(总共有 n_samples * n_iter 个时间步长),\(t_0\) 是根据 Léon Bottou 提出的启发式方法确定的,这样预期初始更新与预期权重大小相当(假设训练样本的范数约为 1)。确切的定义可以在 BaseSGD 中的 _init_t 中找到。

对于回归,默认学习率计划是反向缩放 (learning_rate='invscaling'),由下式给出

\[\eta^{(t)} = \frac{eta_0}{t^{power\_t}}\]

其中 \(eta_0\)\(power\_t\) 是用户通过 eta0power_t 分别选择的超参数。

对于常数学习率,使用 learning_rate='constant' 并使用 eta0 指定学习率。

对于自适应递减学习率,使用 learning_rate='adaptive' 并使用 eta0 指定起始学习率。当达到停止条件时,学习率除以 5,算法不会停止。当学习率低于 1e-6 时,算法停止。

模型参数可以通过 coef_intercept_ 属性访问:coef_ 保存权重 \(w\)intercept_ 保存 \(b\)

当使用平均 SGD(使用 average 参数)时,coef_ 被设置为所有更新的平均权重:coef_ \(= \frac{1}{T} \sum_{t=0}^{T-1} w^{(t)}\),其中 \(T\) 是更新的总数,可以在 t_ 属性中找到。

1.5.9. 实现细节#

SGD 的实现受 Stochastic Gradient SVM 的影响,来自 [7]。与 SvmSGD 类似,权重向量表示为标量和向量的乘积,这允许在 L2 正则化的情况下进行有效的权重更新。在稀疏输入 X 的情况下,截距以较小的学习率(乘以 0.01)更新,以解释它被更频繁地更新的事实。训练示例按顺序选取,并且在观察到每个示例后降低学习率。我们采用了来自 [8] 的学习率计划。对于多类分类,使用“一对多”方法。我们使用 [9] 中提出的截断梯度算法进行 L1 正则化(以及弹性网络)。代码是用 Cython 编写的。

参考文献