原文:https://www.bookbookmark.ds100.org/ch/11/gradient_descence_define.html
# HIDDEN
# Clear previously defined variables
%reset -f
# Set directory for data loading to work properly
import os
os.chdir(os.path.expanduser('~/notebooks/11'))
# HIDDEN
import warnings
# Ignore numpy dtype warnings. These warnings are caused by an interaction
# between numpy and Cython and can be safely ignored.
# Reference: https://stackoverflow.com/a/40846742
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
%matplotlib inline
import ipywidgets as widgets
from ipywidgets import interact, interactive, fixed, interact_manual
import nbinteract as nbi
sns.set()
sns.set_context('talk')
np.set_printoptions(threshold=20, precision=2, suppress=True)
pd.options.display.max_rows = 7
pd.options.display.max_columns = 8
pd.set_option('precision', 2)
# This option stops scientific notation for pandas
# pd.set_option('display.float_format', '{:.2f}'.format)
# HIDDEN
tips = sns.load_dataset('tips')
tips['pcttip'] = tips['tip'] / tips['total_bill'] * 100
# HIDDEN
def mse(theta, y_vals):
return np.mean((y_vals - theta) ** 2)
def grad_mse(theta, y_vals):
return -2 * np.mean(y_vals - theta)
def plot_loss(y_vals, xlim, loss_fn):
thetas = np.arange(xlim[0], xlim[1] + 0.01, 0.05)
losses = [loss_fn(theta, y_vals) for theta in thetas]
plt.figure(figsize=(5, 3))
plt.plot(thetas, losses, zorder=1)
plt.xlim(*xlim)
plt.title(loss_fn.__name__)
plt.xlabel(r'$ \theta $')
plt.ylabel('Loss')
def plot_theta_on_loss(y_vals, theta, loss_fn, **kwargs):
loss = loss_fn(theta, y_vals)
default_args = dict(label=r'$ \theta $', zorder=2,
s=200, c=sns.xkcd_rgb['green'])
plt.scatter([theta], [loss], **{**default_args, **kwargs})
def plot_tangent_on_loss(y_vals, theta, loss_fn, eps=1e-6):
slope = ((loss_fn(theta + eps, y_vals) - loss_fn(theta - eps, y_vals))
/ (2 * eps))
xs = np.arange(theta - 1, theta + 1, 0.05)
ys = loss_fn(theta, y_vals) + slope * (xs - theta)
plt.plot(xs, ys, zorder=3, c=sns.xkcd_rgb['green'], linestyle='--')
我们有兴趣创建一个函数,它可以最小化损失函数,而不必强制用户预先确定要尝试的值是\theta$。换句话说,虽然simple_minimize
函数具有以下签名:
simple_minimize(loss_fn, dataset, thetas)
我们想要一个具有以下签名的函数:
minimize(loss_fn, dataset)
此函数需要自动查找最小化的$\theta$值,无论其大小。我们将使用一种称为梯度下降的技术来实现这个新的minimize
函数。
与损失函数一样,我们将首先讨论梯度下降的直觉,然后用数学形式化我们的理解。
由于minimize
函数没有给定要尝试的$\theta$的值,因此我们从选择需要的任何位置的$\theta$开始。然后,我们可以迭代地改进对$\theta$的估计。为了改进对$\theta$的估计,我们研究了在选择$\theta$时损失函数的斜率。
例如,假设我们对简单数据集$\textbf y=[12.1、12.8、14.9、16.3、17.2]$使用 mse,而当前选择的$\theta$是 12。
# HIDDEN
pts = np.array([12.1, 12.8, 14.9, 16.3, 17.2])
plot_loss(pts, (11, 18), mse)
plot_theta_on_loss(pts, 12, mse)
我们想为.\theta$选择一个减少损失的新值。要做到这一点,我们看损失函数在$\theta=12$时的斜率:
# HIDDEN
pts = np.array([12.1, 12.8, 14.9, 16.3, 17.2])
plot_loss(pts, (11, 18), mse)
plot_tangent_on_loss(pts, 12, mse)
坡度为负,这意味着增加$\theta$将减少损失。
另一方面,如果$\theta=16.5 美元,则损失函数的斜率为正:
# HIDDEN
pts = np.array([12.1, 12.8, 14.9, 16.3, 17.2])
plot_loss(pts, (11, 18), mse)
plot_tangent_on_loss(pts, 16.5, mse)
当坡度为正时,降低$\theta$将减少损失。
切线的斜率告诉我们移动$\theta$的方向,以减少损失。如果坡度为负,我们希望$\theta$朝正方向移动。如果坡度为正,则$\theta$应朝负方向移动。在数学上,我们写道:
其中,$\theta^(t)$是当前估计数,$\theta^(t+1)$是下一个估计数。
对于 MSE,我们有:
当$\theta^(t)=12$时,我们可以计算$-\frac 2 n sum i=1 n(y i-\theta)=-5.32$。因此,$\theta^(t+1)=12-(-5.32)=17.32 美元。
我们将旧的$theta$值绘制为绿色轮廓圆圈,新的值绘制为下面损失曲线上的填充圆圈。
# HIDDEN
pts = np.array([12.1, 12.8, 14.9, 16.3, 17.2])
plot_loss(pts, (11, 18), mse)
plot_theta_on_loss(pts, 12, mse, c='none',
edgecolor=sns.xkcd_rgb['green'], linewidth=2)
plot_theta_on_loss(pts, 17.32, mse)
虽然$\theta$朝着正确的方向发展,但最终却远远超出了最低值。我们可以通过将斜率乘以一个小常数,然后从$\theta$中减去它来解决这个问题。我们的最终更新公式是:
其中,$\alpha$是一个小常量。例如,如果我们设置$\alpha=0.3$,这是新的$\theta^(t+1)$:
# HIDDEN
def plot_one_gd_iter(y_vals, theta, loss_fn, grad_loss, alpha=0.3):
new_theta = theta - alpha * grad_loss(theta, y_vals)
plot_loss(pts, (11, 18), loss_fn)
plot_theta_on_loss(pts, theta, loss_fn, c='none',
edgecolor=sns.xkcd_rgb['green'], linewidth=2)
plot_theta_on_loss(pts, new_theta, loss_fn)
print(f'old theta: {theta}')
print(f'new theta: {new_theta}')
# HIDDEN
plot_one_gd_iter(pts, 12, mse, grad_mse)
old theta: 12
new theta: 13.596
以下是此过程连续迭代的$\theta$值。请注意,$\theta$随着接近最小损失而变化得更慢,因为坡度也更小。
# HIDDEN
plot_one_gd_iter(pts, 13.60, mse, grad_mse)
old theta: 13.6
new theta: 14.236
# HIDDEN
plot_one_gd_iter(pts, 14.24, mse, grad_mse)
old theta: 14.24
new theta: 14.492
# HIDDEN
plot_one_gd_iter(pts, 14.49, mse, grad_mse)
old theta: 14.49
new theta: 14.592
现在我们有了完整的梯度下降算法:
- 选择一个起始值$\theta$(0 是一个常见的选择)。
- 计算$\theta-\alpha\cdot\frac \partial \partial\theta l(\theta、\textbf y)$并将其存储为新值$\theta$。
- 重复直到$\theta$在迭代之间不改变。
您将更常见地看到梯度$\nabla_uta$代替部分导数$\frac \部分\部分\theta$。这两个符号本质上是等效的,但是由于梯度符号更为常见,从现在起我们将在梯度更新公式中使用它:
要查看符号:
-
$\theta^(t)$ 是第$t$次迭代时的当前估计值$\theta^*。 -
$\theta^(t+1)$ 是$\theta$的下一个选择。 -
$\alpha$ 称为学习率,通常设置为一个小常量。有时,从一个更大的$\alpha$开始并随着时间的推移减少它是有用的。如果在迭代之间$\alpha$发生变化,我们使用变量$\alpha^t$来标记$\alpha$随时间变化$t$。 -
$\nabla_ \theta l(\theta^(t),\textbf y)$ 是损失函数相对于时间$t$的偏导数/梯度。
现在您可以看到选择一个可微分损失函数的重要性:$\nabla_theta l(\theta、\textbf y)$是梯度下降算法的关键部分。(虽然可以通过计算两个稍有不同的$theta$值的损失差异并除以$theta$值之间的距离来估计梯度,但这通常会显著增加梯度下降的运行时间,因此使用它变得不切实际。)
梯度算法简单而强大,因为我们可以将它用于许多类型的模型和许多类型的损失函数。它是拟合许多重要模型的计算工具,包括大数据集和神经网络上的线性回归。
定义minimize
函数¶
现在,我们回到原来的任务:定义minimize
函数。我们将不得不稍微改变我们的函数签名,因为我们现在需要计算损失函数的梯度。
def minimize(loss_fn, grad_loss_fn, dataset, alpha=0.2, progress=True):
'''
Uses gradient descent to minimize loss_fn. Returns the minimizing value of
theta_hat once theta_hat changes less than 0.001 between iterations.
'''
theta = 0
while True:
if progress:
print(f'theta: {theta:.2f} | loss: {loss_fn(theta, dataset):.2f}')
gradient = grad_loss_fn(theta, dataset)
new_theta = theta - alpha * gradient
if abs(new_theta - theta) < 0.001:
return new_theta
theta = new_theta
然后我们可以定义函数来计算 mse 及其梯度:
def mse(theta, y_vals):
return np.mean((y_vals - theta) ** 2)
def grad_mse(theta, y_vals):
return -2 * np.mean(y_vals - theta)
最后,我们可以使用minimize
函数计算$\textbf y=[12.1,12.8,14.9,16.3,17.2]$的最小化值。
%%time
theta = minimize(mse, grad_mse, np.array([12.1, 12.8, 14.9, 16.3, 17.2]))
print(f'Minimizing theta: {theta}')
print()
theta: 0.00 | loss: 218.76
theta: 5.86 | loss: 81.21
theta: 9.38 | loss: 31.70
theta: 11.49 | loss: 13.87
theta: 12.76 | loss: 7.45
theta: 13.52 | loss: 5.14
theta: 13.98 | loss: 4.31
theta: 14.25 | loss: 4.01
theta: 14.41 | loss: 3.90
theta: 14.51 | loss: 3.86
theta: 14.57 | loss: 3.85
theta: 14.61 | loss: 3.85
theta: 14.63 | loss: 3.84
theta: 14.64 | loss: 3.84
theta: 14.65 | loss: 3.84
theta: 14.65 | loss: 3.84
theta: 14.66 | loss: 3.84
theta: 14.66 | loss: 3.84
Minimizing theta: 14.658511131035242
CPU times: user 7.88 ms, sys: 3.58 ms, total: 11.5 ms
Wall time: 8.54 ms
我们可以看到,梯度下降很快找到了与解析法相同的解:
np.mean([12.1, 12.8, 14.9, 16.3, 17.2])
14.66
现在,我们可以应用梯度下降来最小化提示百分比数据集上的 Huber 损失。
Huber 损失为:
L 123; 123; 1 2 \ delta)&;\text 否则\结束案例$$
Huber 损失的梯度为:
$$\nabla_\theta l_\delta(\theta,\textbf y)=\frac 1 n \sum i=1 n\ begin cases-(y \theta)&;y i-\theta \le\delta\
- \delta \cdot \text{sign} (y_i - \theta) & \text{otherwise}
\结束案例$$
(注意,在之前的 Huber 损失定义中,我们使用变量$\alpha$来表示转换点。为了避免与梯度下降中使用的$\alpha$混淆,我们将 Huber 损失的过渡点参数替换为$\delta$。)
def huber_loss(theta, dataset, delta = 1):
d = np.abs(theta - dataset)
return np.mean(
np.where(d <= delta,
(theta - dataset)**2 / 2.0,
delta * (d - delta / 2.0))
)
def grad_huber_loss(theta, dataset, delta = 1):
d = np.abs(theta - dataset)
return np.mean(
np.where(d <= delta,
-(dataset - theta),
-delta * np.sign(dataset - theta))
)
让我们最小化 Tips 数据集上的 Huber 损失:
%%time
theta = minimize(huber_loss, grad_huber_loss, tips['pcttip'], progress=False)
print(f'Minimizing theta: {theta}')
print()
Minimizing theta: 15.506849531471964
CPU times: user 194 ms, sys: 4.13 ms, total: 198 ms
Wall time: 208 ms
摘要¶
梯度下降给了我们一种一般的方法来最小化损失函数,当我们无法通过分析来求解$\theta$的最小值时。随着我们的模型和损失函数的复杂性增加,我们将转向梯度下降作为我们选择适合模型的工具。