跳至主要内容

Python 中的随机梯度下降:机器学习优化完整指南

通过这份全面的 Python 指南学习随机梯度下降,这是一种机器学习中必不可少的优化技术。适合初学者与专家。
· 12分钟 阅读

使用 AI 探索

想象一下,您被蒙住眼睛要在群山之间找到最低点。由于只能依靠触觉,您只能感知脚下附近的地面,从而判断哪个方向更低。这本质上就是机器学习算法在寻找问题最佳解时所做的事情。 

它们把问题表述为一个数学函数,输入和输出共同构成起伏的“地形”。找到该函数的最小值,就意味着您已经找到了问题的最佳解。最常用的算法之一就叫作随机梯度下降(SGD)。

在本教程中,您将系统学习这个算法:先从不涉及数学的直观理解开始,再到数学细节,最后讲解如何用 Python 实现它。 

什么是随机梯度下降?简要解读

随机梯度下降(SGD)是一种用于机器学习的优化技术,目的是最小化预测模型的误差。与使用整个数据集计算梯度并更新模型参数的常规梯度下降不同,SGD 每次仅使用一个数据点来更新参数。这使得它在大数据集上更快、更高效,但也更噪声、更不稳定。SGD 通过寻找模型参数的最优取值,来提升预测的准确性。

机器学习中的优化是什么?

首先需要明确的是,随机梯度下降(SGD)并不是一种机器学习算法,而是一种可以应用于机器学习算法的优化技术。

那么,什么是优化?为理解它,我们从机器学习的问题定义阶段开始逐步展开。

假设我们要根据克拉数预测钻石价格(1 克拉=0.2 克)。这是一个回归问题,因为模型输出的是数值。

为解决该问题,我们有多种算法可选,这里选择简单线性回归,其公式为f(x) = mx + b,其中:

  • b 是基础钻石价格
  • m 是每克拉的价格增量
  • x 是钻石的克拉数
  • f(x) 是钻石的预测价格

该线性方程就是我们的模型。我们的目标是在整个数据集上的所有钻石样本上,找到能使预测尽可能准确的 m 和 b 的最佳取值。

如果数据集中再加入一个特征,比如钻石体积,公式会变成 f(x1, x2) = m1*x1 + m2*x2 + b,其中:

  • b 是基础钻石价格
  • m1 是每克拉的价格增量
  • m2 是每单位体积的价格增量
  • x1 是钻石的克拉数
  • x2 是钻石的体积
  • f(x) 是钻石的预测价格

现在,我们需要找到 m1、m2 和 b 的最优取值。

一般而言,所有机器学习模型都有类似上述的一元或多元参数方程。因此,在这个语境下,优化的定义是:“给定模型和数据集,求解方程中参数的最优取值”。

能够执行该任务的优化算法有很多,例如随机梯度下降——这也是本教程的重点。但要彻底理解 SGD 的工作原理,我们需要先回顾它背后的一些基本概念。

机器学习中的误差概念

为了理解 SGD,我们需要先了解普通的梯度下降算法(GD),它与随机版本共享许多基本思想。简单梯度下降从机器学习中的误差概念出发。

什么是误差或损失?

机器学习算法通常是在“猜”问题的正确答案。我们称这个答案为预测,而它并不总是准确的。因此,我们引入“误差”或“损失”这一术语,表示真实答案与模型预测之间的差异。我们的目标是构建一个能最小化该误差的模型。

如果我们的模型 f(x) 对实际价格为 $12,000 的钻石预测为 $10,000,误差即为 $2,000。我们应该调整模型来降低该误差。但模型的预测必须对任意一颗钻石都足够好,而不是仅仅对某一颗。因此,我们需要一种方法将数据集中所有钻石的误差综合起来,这正是代价函数发挥作用的地方。

代价函数将所有单个误差合并为一个数,代表模型的整体表现。整体代价越低,模型的预测就越好。

机器学习中的代价函数

代价函数会根据我们要解决的问题类型而变化。

在回归问题中,模型预测数值,例如钻石价格、游一圈所需的时间。而在分类问题中,模型预测所属类别。例如蘑菇是否可食用,或图像中的物体是猫、狗还是马?

还有其他类型的问题,但关键在于每种问题需要不同的代价函数。在本教程中,我们将聚焦于常用于回归的均方误差(MSE)。

实际值(真实值)与模型预测之间的差称为误差或损失。因此,将所有这些误差或损失综合起来的函数称为误差函数、损失函数或代价函数。不同资料可能交替使用这些术语;从现在起,本教程将使用“损失函数”这一称呼。

均方误差

在回归问题中,常见如下图:横纵轴分别为真实值与模型预测值。

A plot that shows actual vs. predicted values of model with a line for perfect predictions.

点越接近这条直线,说明模型预测越好。因此,大多数回归算法都试图最小化各点到这条“完美预测线”的平均距离。正如前文所述,这种最小化是通过均方误差(MSE)等代价函数实现的。

MSE 以实际值和预测值为输入,输出的是到“完美直线”的平方平均距离。

The visual intuition behind Mean Squared Error and its formula for Stochastic Gradient Descent

您可能会问“为何要平方差值,而不是取绝对值?”第一,平方函数的导数非常容易求;第二,平方会放大较大的误差,从而对模型最差的预测施加更重的惩罚。

这也是 MSE 相比看起来更“简单”但更难求导的平均绝对误差(MAE)等替代函数更受青睐的原因。

梯度

理解梯度下降的下一块拼图就是“梯度”。让我们回到“下山”的类比来更好地理解它。

梯度代表最陡路径

我们被蒙住眼睛站在山顶,希望尽快到达山脚。如果在脚边倒些水,它会往哪个方向流?它会沿着最陡的下坡方向流淌。

这正是梯度告诉我们的信息,但方向相反。梯度指向上坡——即最陡上升的方向。当我们试图最小化误差时,我们只需沿着梯度的反方向前进,就能以最快速度下降。

从数学上讲,梯度告诉我们当改变输入时,函数输出如何变化。更具体地说,它指向函数增长最快的方向。 

在机器学习中,我们试图通过损失函数最小化误差,梯度就成为指引我们尽快降低误差的“路标”。

梯度用一个向量表示——函数的每个输入参数对应一个分量。这个向量中每个数值都表示:若对相应参数做微小调整,函数会改变多少。

如何求函数的梯度

那么,如何求一个函数的梯度?

对于单变量函数,如 f(x) = x ** 2,梯度就是导数,以单元素向量给出——[2x]。 

对于多变量函数,如 f(x1, x2) = x1 ** 3 - x2 ** 2,梯度包含对每个输入的偏导数。因此,上述函数的梯度向量为[3x1 ** 2, 2*x2]。

回到 MSE,其公式如下:

MSE = (1/n) * Σ(y - f(x))² 其中:

  • f(x) 为 mx + b
  • m 和 b 是函数变量
  • x 是钻石的克拉数
  • n 是数据点数量
  • y 是钻石的实际价格

因此,其梯度就是 MSE 对 m 和 b 的偏导:

∂/∂m (MSE) = (-2/n) * Σ(x * (y - (mx + b)))
∂/∂b (MSE) = (-2/n) * Σ(y - (mx + b))
The gradient = [∂/∂m (MSE), ∂/∂b (MSE)]

不必过分纠结这些公式。关键在于:它们告诉我们,当我们对 m 或 b 做微小调整时,均方误差会如何变化。

梯度下降:向解迈进的一小步

学习率:控制步长

现在我们已经具备执行梯度下降的一切条件,换句话说,准备以尽可能高效的方式到达山脚。那么我们是“一跃而下”还是“迈小步”?这不就取决于梯度吗?梯度的大小告诉我们每一步应走多远。

但假如梯度很大,导致我们一步跨过山谷到达另一座山顶。这种情况在梯度下降中并不罕见,因此我们引入一个名为步长的参数,用于控制梯度的影响。

在迈出一步之前,我们将梯度乘以步长,从而降低其幅度。例如,如果梯度指示我们向下走 3.56 个单位,步长为 0.1 会把距离变为 0.356。 

虽然会更慢到达谷底,但不至于直接越过它。

在机器学习中,步长通常称为学习率,通常取值在 0.001 到 0.3 之间。

下面看看这一过程的数学表达。

以数学方式迈小步

首先,我们给模型参数 m 和 b 赋予随机值:

m = 0.215  # Generated randomly
b = 0.059

使用它们,我们在整个数据集上计算初始 MSE:

MSE = (1/n) * Σ(y - (0.215 * x + 0.059))² = ...

现在,为了减小该误差,我们需要沿梯度的反方向迈出一小步,换言之,我们需要利用梯度和学习率来更新 m 和 b:

m_new = 0.215 - alpha * (∂/∂m (MSE))
b_new = 0.059 - alpha * (∂/∂b (MSE))

大多数资料使用 alpha 或 α(希腊字母)来表示学习率。

得到新的 m 和 b 后,我们再次计算 MSE:

MSE_new = (1/n) * Σ(y - (m_new * x + b_new))²

接着再次计算梯度,并如此迭代,直到满足停止条件为止。

通过阅读 这篇梯度下降文章,直观了解学习率如何影响训练。

何时停止

由于被蒙住眼睛,我们看不见自己是否到达了谷底,尤其是在迈小步时。

因此需要一些停止规则。通常会使用如下规则:

  • 达到预设的步数(最大步数或迭代次数)
  • 每步后 MSE 的变化非常小(低于预设阈值)
  • 梯度非常接近 0(表明我们已经在或接近谷底)
  • MSE 不降反升

这些规则由您(ML 工程师)在执行梯度下降时设定。Python 中的实现通常提供相应参数,稍后我们会看到一些示例。

梯度下降的优点与挑战

优点:

  • 算法简单直观
  • 适用于广泛的问题
  • 在高维参数问题上计算高效

挑战:

  • 在超大数据集上可能较慢
  • 在非凸问题中可能陷入局部极小值
  • 选择合适的学习率并不容易

下一节我们将看到,随机梯度下降如何在大数据集场景下缓解其中的一些挑战。

随机梯度下降

理解了常规梯度下降后,SGD 就非常容易上手了。

常规梯度下降的问题

尽管梯度下降是一种非常优雅的优化算法,但在处理大数据集时,它有一个显著劣势。回忆一下:在每一步中,我们都要使用数据集中的所有数据点来计算梯度:

∂/∂m (MSE) = (-2/n) * Σ(x * (y - (mx + b)))
∂/∂b (MSE) = (-2/n) * Σ(y - (mx + b))

这里,x 和 y 是包含所有钻石克拉数与实际价格的数组。这意味着每一步更新参数时,我们都要使用每一颗钻石的信息。

可以想见,当我们有数百万数据点时,这会变得计算开销巨大且耗时。在大数据时代,这一限制会让常规梯度下降在许多真实世界的机器学习问题上变得不切实际。

随机化思路:引入随机性

“Stochastic”意为“随机决定的”,这也正是 SGD 的做法:它在优化过程中引入了随机性。

SGD 不再使用所有数据点计算梯度,而是在每一步随机选择一个数据点,利用这单个样本来估计梯度并更新参数。

由于 SGD 一次只看一个数据点,它能更频繁地更新参数。这让它能更快地在误差曲面(“山丘”)上移动,尤其是在大数据集上。

不过,这种速度是有代价的。因为我们用单个样本来估计整个数据集的梯度,估计值会更嘈杂。这意味着我们沿误差曲面的下降路径会更不平滑、更不稳定。

随机梯度下降的数学表达

执行 SGD 时,我们首先定义一个随机代价函数。

我们不再对所有数据点计算均方误差,而是仅针对一个随机选择的样本,计算它的平方误差:

Cost = (y - f(x))²

其中,y 是随机选定的那颗钻石的实际价格,f(x) = mx + b 是我们对该钻石的预测价格。

然后,我们计算该代价函数关于参数 m 和 b 的随机梯度:

∂/∂m (Cost) = -2x(y - (mx + b))
∂/∂b (Cost) = -2(y - (mx + b))
The stochastic gradient = [∂/∂m (Cost), ∂/∂b (Cost)]

使用该随机梯度,我们以与常规梯度下降相同的方式更新参数:

m_new = m_old - learning_rate * ∂/∂m (Cost)
b_new = b_old - learning_rate * ∂/∂b (Cost)

SGD 的核心思想是:每一步都只使用一个随机选择的数据点。停止下降的规则与常规梯度下降相同。

SGD 的变体与使用场景

在实践中,很少使用“纯”SGD(每个训练样本都更新参数)。原因在于这种更新的方差过大,会导致损失函数波动,使其更难收敛到精确的极小值。

常规梯度下降能力强且结果更稳定,但如前所述,它每一步都使用全部数据点,在当下的数据规模下往往不切实际。

为在稳定性与速度之间取得平衡,小批量梯度下降(mini-batch GD)被广泛使用,尤其是在神经网络中。小批量 GD 中,在进行参数更新时使用的是一个批次样本,而不是单一样本。常见批大小包括 16、32、64 等。

因此,当批大小为 1 时就是 SGD;当批大小等于数据集大小时就是常规梯度下降;介于两者之间的任何批大小,都是小批量梯度下降。

下面的表总结了它们的差异及各自的适用场景:

类型更新频率计算效率内存需求收敛稳定性最佳使用场景
梯度下降(GD)每次更新使用整个数据集最慢,因为每次使用完整数据集需要将整个数据集放入内存最稳定,但最慢小数据集、对稳定性要求高的场景
随机梯度下降(SGD)每次更新使用一个样本最快,一次处理一个样本内存需求低方差高,易波动需要快速更新的大规模数据集
小批量梯度下降每次更新使用一个批次在效率与速度间折中,比 GD 更高效,比 SGD 稍慢需要容纳一个批次的内存比 SGD 稳定,比 GD 稍差大型数据集,需在稳定性与效率间平衡

梯度下降算法中的 Epoch

任何优化算法中一个重要概念是epoch(轮次)。它指对整个训练数据集的一次完整遍历。

在一个 epoch 中,算法会恰好处理训练数据集中的每个样本一次。对于每个样本(或一个小批量样本),模型会进行预测、计算损失,并依据损失的梯度更新权重。一个 epoch 标志着模型的一次改进周期。每个 epoch 结束后,您通常可以观察到模型表现的变化。

训练通常包含多个 epoch(建议至少 10 个)。epoch 数是一个可调的超参数。为防止模型学习到训练样本的顺序,通常会在每个 epoch 之前对数据进行洗牌。

在我们“蒙眼下山”的类比中,一个 epoch 就是对整个地形的一次完整探索。在每个 epoch 中,您会:

  • 在多个位置采样地面(处理训练样本)
  • 依据局部坡度迈出小步(更新模型参数)

每次完整探索后,您会从新的位置重新开始。多个 epoch 让模型反复探索损失地形,从而更有机会找到一个较好的极小值。

然而,多次 epoch 并不保证一定能找到全局最小值,整个过程也会受到起点选择或损失地形复杂度等因素的影响。

SGD 实战:走查示例

本节我们将仅使用 Numpy,实现一个支持批大小(小批量 GD)的 SGD。

首先,导入必要的库:

import seaborn as sns
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
np.random.seed(42)

我们将使用 Seaborn 中的 Diamonds 数据集的 10,000 个数据点。只取 carat 特征和 price 列作为目标值:

# Load the data
dataset_size = 10_000
diamonds = sns.load_dataset('diamonds')
# Extract the target and the feature
xy = diamonds[['carat', 'price']].values
np.random.shuffle(xy)  # Shuffle the data
xy = xy[:dataset_size]
xy.shape
(10000, 2)

现在将数据划分为训练集和测试集,训练集占 80%。

# Split the data
np.random.shuffle(xy)
train_size = int(0.8 * dataset_size)
train_xy, test_xy = xy[:train_size], xy[train_size:]
train_xy.shape
(8000, 2)

然后,定义模型函数:

def model(m, x, b):
   """Simple linear model"""
   return m * x + b

这里我们做的是简单线性回归,只有两个自变量 m 和 b。

我们还需要定义损失函数 MSE:

def loss(y_true, y_pred):
   """Mean squared error"""
   return np.mean((y_true - y_pred) ** 2)

现在定义一个名为 stochastic_gradient_descent 的函数,它接收六个参数:

  • x 和 y 分别代表我们问题中的单个特征与目标
  • epochs 表示我们希望执行多少次下降(后文详述)
  • learning_rate 为步长
  • batch_size 用于控制参数更新的频率
  • stopping_threshold 设定每步损失应当至少降低的最小值
def stochastic_gradient_descent(
   x, y, epochs=100, learning_rate=0.01, batch_size=32, stopping_threshold=1e-6
):
   """
   SGD with support for mini-batches.
   """

开始时,我们随机初始化待优化的参数,并将损失设为无穷大:

# Initialize the model parameters randomly
m = np.random.randn()
b = np.random.randn()
n = len(x)  # The number of data points
previous_loss = np.inf

然后,使用 epochs 参数开启一个 for 循环。在循环内,我们打乱数据,以提升计算鲁棒性:

for i in range(epochs):
   # Shuffle the data
   indices = np.random.permutation(n)
   x = x[indices]
   y = y[indices]

随后,在另一个 for 循环中开始实际的梯度计算。

for j in range(0, n, batch_size):
   x_batch = x[j:j + batch_size]
   y_batch = y[j:j + batch_size]

从 x 与 y 中切出一批样本,并计算梯度:

# Compute the gradients
y_pred = model(m, x_batch, b)
m_gradient = -2 * np.mean(x_batch * (y_batch - y_pred))
b_gradient = -2 * np.mean(y_batch - y_pred)

我们使用前文定义的 model 函数,在当前的 m 与 b 下进行预测。然后按前文给出的公式,求构成梯度的偏导。

利用偏导与学习率,更新参数:

# Update the model parameters
m -= learning_rate * m_gradient
b -= learning_rate * b_gradient

截至目前的完整代码如下:

def stochastic_gradient_descent(
       x, y, epochs=100, learning_rate=0.01, batch_size=32, stopping_threshold=1e-6
):
   """
   SGD with support for mini-batches.
   """
   # Initialize the model parameters randomly
   m = np.random.randn()
   b = np.random.randn()
  
   n = len(x)
   previous_loss = np.inf
  
   for i in range(epochs):
       # Shuffle the data
       indices = np.random.permutation(n)
       x = x[indices]
       y = y[indices]
      
       for j in range(0, n, batch_size):
           x_batch = x[j:j + batch_size]
           y_batch = y[j:j + batch_size]
          
           # Compute the gradients
           y_pred = model(m, x_batch, b)
           m_gradient = -2 * np.mean(x_batch * (y_batch - y_pred))
           b_gradient = -2 * np.mean(y_batch - y_pred)
          
           # Update the model parameters
           m -= learning_rate * m_gradient
           b -= learning_rate * b_gradient

在内层循环之外继续该函数,计算该 epoch 的损失:

# Compute the loss
y_pred = model(m, x, b)
current_loss = loss(y, y_pred)

如果 current_loss 小于 stopping_threshold 之差阈值,我们就停止整个过程:

if previous_loss - current_loss < stopping_threshold:
   break
previous_loss = current_loss

否则,将 previous_loss 设为 current_loss。最后返回优化后的 m 与 b 参数:

return m, b

我已将完整代码粘贴到 这个 GitHub gist,方便您整体查看。

A screenshot of the code for implementing Stochastic Gradient Descent (SGD) in Python

让我们在测试集上运行模型,检验所求参数的效果:

# Find the optimal parameters
m, b = stochastic_gradient_descent(train_xy[:, 0], train_xy[:, 1])
# Make predictions
y_preds = model(m, test_xy[:, 0], b)
# Compute and print the loss
mean_squared_error = loss(test_xy[:, 1], y_preds)
mean_squared_error ** 0.5
1595.3955619759456

MSE 的平方根约为 $1,600,意味着我们的模型平均偏差约 $1,600。要进一步降低误差,可以增加 epoch 数或使用更大的数据集。

在真实问题中使用 SGD

上一节中 SGD 的实现较为粗糙低效,主要用于通过代码走查来巩固前文建立的直观认识。

在实践中,几乎不会手写 SGD,而是使用主流框架中现成的实现。

例如,Scikit-learn 提供了 SGDRegressor 和 SGDClassifier 等估计器,用于训练如下算法:

  • 线性回归
  • 逻辑回归
  • 岭回归
  • SVM

并以 SGD 作为优化算法。

from sklearn.linear_model import SGDRegressor, SGDClassifier
# SGD for Linear Regression with 1000 epochs
regressor = SGDRegressor(loss='squared_loss', max_iter=1000)

在 PyTorch 中,它作为 SGD 类位于 optim 模块中:

import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.01)

要学习如何在 PyTorch 中训练与优化模型,请参考 DataCamp 的 Introduction to PyTorch 课程。

在 Keras 中,SGD 也以同名提供:

from tensorflow.keras.optimizers import SGD
optimizer = SGD(learning_rate=0.01)

要学习如何在 TensorFlow 中训练与优化模型,请参考 DataCamp 的 Introduction to TensorFlow 课程。

使用 SGD 的实用技巧

在使用 SGD 或类似算法时,有很多最佳实践与技巧。下面列出部分要点:

1. 打乱数据

  • 在每个 epoch 之前随机化训练样本顺序
  • 避免优化过程产生循环,确保批次多样性

2. 使用小批量

  • 在纯 SGD 与批量梯度下降之间取得平衡
  • 常见批大小:32 到 256

3. 归一化输入

  • 将特征缩放到零均值与单位方差
  • 确保各特征贡献均衡,加快收敛

4. 选择合适的学习率

  • 从较小数值开始(如 0.1、0.01、0.001)
  • 根据表现进行调整

5. 使用学习率调度

  • 随时间衰减学习率
  • 可选方案:阶梯衰减、指数衰减、1/t 衰减

6. 使用动量

  • 在更新中加入动量项
  • 加速 SGD,减弱震荡

7. 考虑自适应学习率方法

  • 尝试 Adam、RMSprop 或 Adagrad
  • 自动对不同参数调整学习率

8. 应用梯度裁剪

  • 用于抑制梯度爆炸(如在 RNN 中)
  • 将梯度范数裁剪到最大值

9. 监控验证集表现

  • 定期检查验证集表现
  • 必要时实施早停

10. 使用正则化

  • 应用 L1/L2 正则化或 dropout
  • 防止过拟合,尤其在大模型中

总结

今天我们学习了机器学习中的关键优化算法之一——随机梯度下降。

我们先通过常规梯度下降算法建立直觉与基本概念,并大量借助“蒙眼找谷底”的类比来帮助理解。

我们了解到,SGD 与常规 GD 的区别在于每次参数更新所使用的数据点数量。为巩固直觉并厘清数学细节,我们用 Numpy 实现了 SGD。

请记住,SGD 只是整个机器学习训练流程的一部分。要进一步了解其角色,请参阅以下资源:

随机梯度下降常见问答

随机梯度下降(SGD)与小批量梯度下降的主要区别是什么?

SGD 每次用一个数据点更新参数,更新更频繁但方差更大。小批量梯度下降使用一小批数据点,在更新频率与稳定性之间取得平衡,对大数据集通常更高效。

如何为 SGD 选择合适的学习率?

选择合适的学习率需要试验与调参。通常从 0.01 或 0.001 等较小值开始,根据模型表现进行调整。使用学习率调度或自适应学习率方法(如 Adam 或 RMSprop)也有助于找到较优学习率。

使用 SGD 时常见问题有哪些,如何缓解?

常见问题包括更新方差较高、收敛困难。可以通过使用小批量、动量、学习率调度以及梯度裁剪等技术来缓解。

随机梯度下降是否适用于所有类型的机器学习问题?

SGD 通用性强,适用于多种问题,尤其适合大规模场景。然而由于更新噪声较大,对于需要高精度的问题可能不是理想之选。

有哪些流行的 Python 机器学习库或框架实现了 SGD?

常用库包括 Scikit-learn(SGDRegressor、SGDClassifier)、TensorFlow(SGD 优化器)和 PyTorch(optim.SGD)。这些库提供了高效、易用的 SGD 实现。

Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题