# 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)技术支持
然后,定义模型函数:
defmodel(m, x, b):"""Simple linear model"""return m * x + b技术支持
defstochastic_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 inrange(epochs):# Shuffle the data
indices = np.random.permutation(n)
x = x[indices]
y = y[indices]技术支持
随后,在另一个 for 循环中开始实际的梯度计算。
for j inrange(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技术支持
截至目前的完整代码如下:
defstochastic_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 inrange(epochs):# Shuffle the data
indices = np.random.permutation(n)
x = x[indices]
y = y[indices]for j inrange(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)技术支持
# 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.51595.3955619759456技术支持
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)技术支持
想象一下,您被蒙住眼睛要在群山之间找到最低点。由于只能依靠触觉,您只能感知脚下附近的地面,从而判断哪个方向更低。这本质上就是机器学习算法在寻找问题最佳解时所做的事情。
它们把问题表述为一个数学函数,输入和输出共同构成起伏的“地形”。找到该函数的最小值,就意味着您已经找到了问题的最佳解。最常用的算法之一就叫作随机梯度下降(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)。
实际值(真实值)与模型预测之间的差称为误差或损失。因此,将所有这些误差或损失综合起来的函数称为误差函数、损失函数或代价函数。不同资料可能交替使用这些术语;从现在起,本教程将使用“损失函数”这一称呼。
均方误差
在回归问题中,常见如下图:横纵轴分别为真实值与模型预测值。
点越接近这条直线,说明模型预测越好。因此,大多数回归算法都试图最小化各点到这条“完美预测线”的平均距离。正如前文所述,这种最小化是通过均方误差(MSE)等代价函数实现的。
MSE 以实际值和预测值为输入,输出的是到“完美直线”的平方平均距离。
您可能会问“为何要平方差值,而不是取绝对值?”第一,平方函数的导数非常容易求;第二,平方会放大较大的误差,从而对模型最差的预测施加更重的惩罚。
这也是 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 + bm和 b 是函数变量x是钻石的克拉数n是数据点数量y是钻石的实际价格因此,其梯度就是 MSE 对
m和b的偏导:不必过分纠结这些公式。关键在于:它们告诉我们,当我们对
m或b做微小调整时,均方误差会如何变化。梯度下降:向解迈进的一小步
学习率:控制步长
现在我们已经具备执行梯度下降的一切条件,换句话说,准备以尽可能高效的方式到达山脚。那么我们是“一跃而下”还是“迈小步”?这不就取决于梯度吗?梯度的大小告诉我们每一步应走多远。
但假如梯度很大,导致我们一步跨过山谷到达另一座山顶。这种情况在梯度下降中并不罕见,因此我们引入一个名为步长的参数,用于控制梯度的影响。
在迈出一步之前,我们将梯度乘以步长,从而降低其幅度。例如,如果梯度指示我们向下走 3.56 个单位,步长为 0.1 会把距离变为 0.356。
虽然会更慢到达谷底,但不至于直接越过它。
在机器学习中,步长通常称为学习率,通常取值在 0.001 到 0.3 之间。
下面看看这一过程的数学表达。
以数学方式迈小步
首先,我们给模型参数 m 和 b 赋予随机值:
使用它们,我们在整个数据集上计算初始 MSE:
现在,为了减小该误差,我们需要沿梯度的反方向迈出一小步,换言之,我们需要利用梯度和学习率来更新 m 和 b:
大多数资料使用
alpha或α(希腊字母)来表示学习率。得到新的
m和b后,我们再次计算 MSE:接着再次计算梯度,并如此迭代,直到满足停止条件为止。
通过阅读 这篇梯度下降文章,直观了解学习率如何影响训练。
何时停止
由于被蒙住眼睛,我们看不见自己是否到达了谷底,尤其是在迈小步时。
因此需要一些停止规则。通常会使用如下规则:
这些规则由您(ML 工程师)在执行梯度下降时设定。Python 中的实现通常提供相应参数,稍后我们会看到一些示例。
梯度下降的优点与挑战
优点:
挑战:
下一节我们将看到,随机梯度下降如何在大数据集场景下缓解其中的一些挑战。
随机梯度下降
理解了常规梯度下降后,SGD 就非常容易上手了。
常规梯度下降的问题
尽管梯度下降是一种非常优雅的优化算法,但在处理大数据集时,它有一个显著劣势。回忆一下:在每一步中,我们都要使用数据集中的所有数据点来计算梯度:
这里,
x和y是包含所有钻石克拉数与实际价格的数组。这意味着每一步更新参数时,我们都要使用每一颗钻石的信息。可以想见,当我们有数百万数据点时,这会变得计算开销巨大且耗时。在大数据时代,这一限制会让常规梯度下降在许多真实世界的机器学习问题上变得不切实际。
随机化思路:引入随机性
“Stochastic”意为“随机决定的”,这也正是 SGD 的做法:它在优化过程中引入了随机性。
SGD 不再使用所有数据点计算梯度,而是在每一步随机选择一个数据点,利用这单个样本来估计梯度并更新参数。
由于 SGD 一次只看一个数据点,它能更频繁地更新参数。这让它能更快地在误差曲面(“山丘”)上移动,尤其是在大数据集上。
不过,这种速度是有代价的。因为我们用单个样本来估计整个数据集的梯度,估计值会更嘈杂。这意味着我们沿误差曲面的下降路径会更不平滑、更不稳定。
随机梯度下降的数学表达
执行 SGD 时,我们首先定义一个随机代价函数。
我们不再对所有数据点计算均方误差,而是仅针对一个随机选择的样本,计算它的平方误差:
其中,y 是随机选定的那颗钻石的实际价格,
f(x) = mx + b是我们对该钻石的预测价格。然后,我们计算该代价函数关于参数
m和b的随机梯度:使用该随机梯度,我们以与常规梯度下降相同的方式更新参数:
SGD 的核心思想是:每一步都只使用一个随机选择的数据点。停止下降的规则与常规梯度下降相同。
SGD 的变体与使用场景
在实践中,很少使用“纯”SGD(每个训练样本都更新参数)。原因在于这种更新的方差过大,会导致损失函数波动,使其更难收敛到精确的极小值。
常规梯度下降能力强且结果更稳定,但如前所述,它每一步都使用全部数据点,在当下的数据规模下往往不切实际。
为在稳定性与速度之间取得平衡,小批量梯度下降(mini-batch GD)被广泛使用,尤其是在神经网络中。小批量 GD 中,在进行参数更新时使用的是一个批次样本,而不是单一样本。常见批大小包括 16、32、64 等。
因此,当批大小为 1 时就是 SGD;当批大小等于数据集大小时就是常规梯度下降;介于两者之间的任何批大小,都是小批量梯度下降。
下面的表总结了它们的差异及各自的适用场景:
梯度下降算法中的 Epoch
任何优化算法中一个重要概念是epoch(轮次)。它指对整个训练数据集的一次完整遍历。
在一个 epoch 中,算法会恰好处理训练数据集中的每个样本一次。对于每个样本(或一个小批量样本),模型会进行预测、计算损失,并依据损失的梯度更新权重。一个 epoch 标志着模型的一次改进周期。每个 epoch 结束后,您通常可以观察到模型表现的变化。
训练通常包含多个 epoch(建议至少 10 个)。epoch 数是一个可调的超参数。为防止模型学习到训练样本的顺序,通常会在每个 epoch 之前对数据进行洗牌。
在我们“蒙眼下山”的类比中,一个 epoch 就是对整个地形的一次完整探索。在每个 epoch 中,您会:
每次完整探索后,您会从新的位置重新开始。多个 epoch 让模型反复探索损失地形,从而更有机会找到一个较好的极小值。
然而,多次 epoch 并不保证一定能找到全局最小值,整个过程也会受到起点选择或损失地形复杂度等因素的影响。
SGD 实战:走查示例
本节我们将仅使用 Numpy,实现一个支持批大小(小批量 GD)的 SGD。
首先,导入必要的库:
我们将使用 Seaborn 中的 Diamonds 数据集的 10,000 个数据点。只取 carat 特征和 price 列作为目标值:
现在将数据划分为训练集和测试集,训练集占 80%。
然后,定义模型函数:
这里我们做的是简单线性回归,只有两个自变量
m和b。我们还需要定义损失函数 MSE:
现在定义一个名为
stochastic_gradient_descent的函数,它接收六个参数:x和y分别代表我们问题中的单个特征与目标epochs表示我们希望执行多少次下降(后文详述)learning_rate为步长batch_size用于控制参数更新的频率stopping_threshold设定每步损失应当至少降低的最小值开始时,我们随机初始化待优化的参数,并将损失设为无穷大:
然后,使用
epochs参数开启一个for循环。在循环内,我们打乱数据,以提升计算鲁棒性:随后,在另一个
for循环中开始实际的梯度计算。从
x与y中切出一批样本,并计算梯度:我们使用前文定义的
model函数,在当前的m与b下进行预测。然后按前文给出的公式,求构成梯度的偏导。利用偏导与学习率,更新参数:
截至目前的完整代码如下:
在内层循环之外继续该函数,计算该 epoch 的损失:
如果
current_loss小于stopping_threshold之差阈值,我们就停止整个过程:否则,将
previous_loss设为current_loss。最后返回优化后的m与b参数:我已将完整代码粘贴到 这个 GitHub gist,方便您整体查看。
让我们在测试集上运行模型,检验所求参数的效果:
MSE 的平方根约为 $1,600,意味着我们的模型平均偏差约 $1,600。要进一步降低误差,可以增加 epoch 数或使用更大的数据集。
在真实问题中使用 SGD
上一节中 SGD 的实现较为粗糙低效,主要用于通过代码走查来巩固前文建立的直观认识。
在实践中,几乎不会手写 SGD,而是使用主流框架中现成的实现。
例如,Scikit-learn 提供了
SGDRegressor和SGDClassifier等估计器,用于训练如下算法:并以 SGD 作为优化算法。
在 PyTorch 中,它作为
SGD类位于optim模块中:要学习如何在 PyTorch 中训练与优化模型,请参考 DataCamp 的 Introduction to PyTorch 课程。
在 Keras 中,SGD 也以同名提供:
要学习如何在 TensorFlow 中训练与优化模型,请参考 DataCamp 的 Introduction to TensorFlow 课程。
使用 SGD 的实用技巧
在使用 SGD 或类似算法时,有很多最佳实践与技巧。下面列出部分要点:
1. 打乱数据
2. 使用小批量
3. 归一化输入
4. 选择合适的学习率
5. 使用学习率调度
6. 使用动量
7. 考虑自适应学习率方法
8. 应用梯度裁剪
9. 监控验证集表现
10. 使用正则化
总结
今天我们学习了机器学习中的关键优化算法之一——随机梯度下降。
我们先通过常规梯度下降算法建立直觉与基本概念,并大量借助“蒙眼找谷底”的类比来帮助理解。
我们了解到,SGD 与常规 GD 的区别在于每次参数更新所使用的数据点数量。为巩固直觉并厘清数学细节,我们用 Numpy 实现了 SGD。
请记住,SGD 只是整个机器学习训练流程的一部分。要进一步了解其角色,请参阅以下资源: