Train the model with gradient descent
Gradient Descent for Linear Regression
线性回归的梯度下降法
- 使用梯度下降法自动优化 w 和 b 的过程。
import math, copy
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle')
from lab_utils_uni import plt_house_x, plt_contour_wgrad, plt_divergence, plt_gradients
问题描述 (Problem Statement)
我们将使用之前的两个数据点:一栋面积为 1000 平方英尺的房屋售价为 30 万美元,另一栋面积为 2000 平方英尺的房屋售价为 50 万美元。
面积(1000 平方英尺) | 价格(千美元) |
---|---|
1 | 300 |
2 | 500 |
x_train = np.array([1.0, 2.0]) # 特征 (面积,单位为 1000 平方英尺)
y_train = np.array([300.0, 500.0]) # 目标值 (价格,单位为千美元)
计算成本函数 (Compute_Cost)
这是在上一个实验中开发的函数。在本实验中我们仍然需要它。
J(w,b) = \frac{1}{2m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})^2\tag{2}
# 成本损失函数 (Function to calculate the cost)
def compute_cost(x, y, w, b):
m = x.shape[0]
cost = 0
for i in range(m):
f_wb = w * x[i] + b
cost = cost + (f_wb - y[i])**2
total_cost = 1 / (2 * m) * cost
return total_cost
梯度下降算法推导过程 (Gradient Descent Algorithm)
- Goal 目标: Repeat the update process until convergence. 重复更新过程直到收敛。
Update Equations 更新方程:
- w = w - \alpha \frac{\partial}{\partial w} J(w, b)
- b = b - \alpha \frac{\partial}{\partial b} J(w, b)
- \alpha: Learning rate 学习率
- \frac{\partial}{\partial w} J(w, b) 和 \frac{\partial}{\partial b} J(w, b): Derivatives with respect to w and b 分别为对 w 和 b 的偏导数
Key Points 关键点:
- Simultaneous Update of w and b 同时更新 w 和 b is essential. 同时更新是关键。
Correct Method (Simultaneous Update) 正确方法(同时更新):
\text{tmp\_w} = w - \alpha \frac{\partial}{\partial w} J(w, b)
\text{tmp\_b} = b - \alpha \frac{\partial}{\partial b} J(w, b)
Update w and b simultaneously: 同时更新 w 和 b:
w = \text{tmp\_w}
b = \text{tmp\_b}
Incorrect Method (Sequential Update) 错误方法(顺序更新):
\text{tmp\_w} = w - \alpha \frac{\partial}{\partial w} J(w, b)
w = \text{tmp\_w}
(updates w immediately 立即更新 w)
\text{tmp\_b} = b - \alpha \frac{\partial}{\partial b} J(w, b)
b = \text{tmp\_b}
Notes on Assignments vs. Assertions 赋值与断言的说明:
- Assignment 赋值 (e.g., a = C, a = a + 1) is code-based 是基于代码的操作。
- Assertion 断言 (e.g., a = c) is math-based (truth assertion) 是基于数学的真值断言, not to be confused with assignment in programming. 不要与编程中的赋值混淆。
This method ensures a proper gradient descent update by preventing partial updates that could lead to incorrect convergence.
这种方法确保了正确的梯度下降更新,防止部分更新导致不正确的收敛。
梯度下降 (Gradient descent summary)
在本课程中,您已经开发了一个可以预测 f_{w,b}(x^{(i)}) 的线性模型:
f_{w,b}(x^{(i)}) = wx^{(i)} + b \tag{1}
在线性回归中,您通过最小化预测值 f_{w,b}(x^{(i)}) 与实际数据 y^{(i)} 之间的误差来利用输入训练数据拟合参数 w 和 b。这种误差度量称为 成本/误差函数 J(w, b)。在训练过程中,您计算所有训练样本 x^{(i)}, y^{(i)} 上的成本:
(2) \quad J(w,b) = \frac{1}{2m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})^2\tag{2}
- J(w, b): 成本函数,衡量模型预测与实际数据之间的差异。
在课程中,梯度下降 被描述为:
(4) \quad \begin{align*} \text{repeat}&\text{ until convergence:} \; \lbrace \newline \; w &= w - \alpha \frac{\partial J(w,b)}{\partial w} \tag{3} \; \newline b &= b - \alpha \frac{\partial J(w,b)}{\partial b} \newline \rbrace \end{align*}
其中,参数 w 和 b 被同时更新。
梯度定义为:
(5) \quad \begin{align} \frac{\partial J(w,b)}{\partial w} &= \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})x^{(i)} \tag{4}\\ \frac{\partial J(w,b)}{\partial b} &= \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)}) \tag{5}\\ \end{align}
在这里,同时更新意味着您在更新任何参数之前先计算所有参数的偏导数。
实现梯度下降法 (Implement Gradient Descent)
您将为一个特征实现梯度下降算法。您需要以下三个函数:
计算梯度 (compute_gradient)
实现上述的方程 (4) 和 (5)计算成本函数 (compute_cost)
实现上述的方程 (2)(来自上一次实验的代码)梯度下降 (gradient_descent)
,调用compute_gradient
和compute_cost
约定 (Conventions) :
- 在 Python 变量命名中,包含偏导数的变量遵循以下模式,例如 \frac{\partial J(w,b)}{\partial b} 将命名为
dj_db
。 - w.r.t 是 With Respect To 的缩写,表示“关于”的意思,例如 \frac{\partial J(w,b)}{\partial b} 是 “关于 b 的偏导数”。
函数compute_gradient
compute_gradient
实现了上面的公式 (4) 和 (5),并返回 \frac{\partial J(w,b)}{\partial w} 和 \frac{\partial J(w,b)}{\partial b}。嵌入的注释详细描述了各个操作的具体功能。
def compute_gradient(x, y, w, b):
"""
计算线性回归的梯度
参数:
x (ndarray (m,)): 数据,包含 m 个样本
y (ndarray (m,)): 目标值
w, b (scalar): 模型参数
返回:
dj_dw (scalar): 成本函数对参数 w 的梯度
dj_db (scalar): 成本函数对参数 b 的梯度
"""
# 训练样本数量
m = x.shape[0]
dj_dw = 0
dj_db = 0
# 遍历每个样本,累积梯度
for i in range(m):
f_wb = w * x[i] + b # 预测值
dj_dw_i = (f_wb - y[i]) * x[i] # 对 w 的偏导数项
dj_db_i = f_wb - y[i] # 对 b 的偏导数项
dj_db += dj_db_i # 累积对 b 的偏导数
dj_dw += dj_dw_i # 累积对 w 的偏导数
# 计算平均梯度
dj_dw = dj_dw / m
dj_db = dj_db / m
return dj_dw, dj_db
上图描述了梯度下降如何利用成本函数对某个参数在某一点的偏导数来更新该参数。
让我们使用 compute_gradient
函数,计算并绘制成本函数相对于某个参数 w_0 的偏导数。
plt_gradients(x_train,y_train, compute_cost, compute_gradient)
plt.show()
上图中,左侧图显示了 \frac{\partial J(w,b)}{\partial w},即成本函数相对于 w 的曲线斜率在三个点的值。在图的右侧,偏导数为正;而在左侧,偏导数为负。由于成本函数的“碗形”特性,偏导数将始终引导梯度下降朝向最低点(梯度为零的地方)。
左侧图中固定 b=100。梯度下降将同时利用 \frac{\partial J(w,b)}{\partial w} 和 \frac{\partial J(w,b)}{\partial b} 来更新参数。右侧的“箭头图”(quiver plot)提供了一种同时查看两个参数梯度的方法。箭头的大小反映了梯度在该点的大小,而箭头的方向和斜率反映了 \frac{\partial J(w,b)}{\partial w} 和 \frac{\partial J(w,b)}{\partial b} 之间的比值。
需要注意的是,梯度的方向是指向远离最小值的方向。回顾上方的公式 (3),缩放后的梯度被从当前的 w 或 b 值中减去,这会使参数朝着降低成本的方向移动。
函数 compute_cost
# 成本损失函数 (Function to calculate the cost)
def compute_cost(x, y, w, b):
m = x.shape[0]
cost = 0
for i in range(m):
f_wb = w * x[i] + b
cost = cost + (f_wb - y[i])**2
total_cost = 1 / (2 * m) * cost
return total_cost
梯度下降 Gradient Descent
现在可以计算梯度了,接下来我们可以实现梯度下降算法。以下代码中的 gradient_descent
函数实现了这一算法,其实现细节在注释中进行了描述。随后,您将使用该函数在训练数据上找到 w 和 b 的最优值。
def gradient_descent(x, y, w_in, b_in, alpha, num_iters, cost_function, gradient_function):
"""
执行梯度下降以拟合 w 和 b。通过学习率 alpha 进行 num_iters 次梯度下降更新 w 和 b。
参数:
x (ndarray (m,)): 数据,包含 m 个样本
y (ndarray (m,)): 目标值
w_in, b_in (scalar): 模型参数的初始值
alpha (float): 学习率
num_iters (int): 运行梯度下降的迭代次数
cost_function: 用于计算成本的函数
gradient_function: 用于计算梯度的函数
返回:
w (scalar): 运行梯度下降后的参数 w 的更新值
b (scalar): 运行梯度下降后的参数 b 的更新值
J_history (List): 成本值的历史记录
p_history (list): 参数 [w, b] 的历史记录
"""
w = copy.deepcopy(w_in) # 避免修改全局变量 w_in
# 创建数组用于存储每次迭代的成本 J 和参数值(主要用于后续绘图)
J_history = []
p_history = []
b = b_in
w = w_in
for i in range(num_iters):
# 使用 gradient_function 计算梯度并更新参数
dj_dw, dj_db = gradient_function(x, y, w , b)
# 使用公式 (3) 更新参数
b = b - alpha * dj_db
w = w - alpha * dj_dw
# 在每次迭代中保存成本 J
if i < 100000: # 防止资源耗尽
J_history.append( cost_function(x, y, w , b))
p_history.append([w, b])
# 每运行 1/10 总迭代次数的间隔打印一次成本
if i % math.ceil(num_iters / 10) == 0:
print(f"Iteration {i:4}: Cost {J_history[-1]:0.2e} ",
f"dj_dw: {dj_dw: 0.3e}, dj_db: {dj_db: 0.3e} ",
f"w: {w: 0.3e}, b:{b: 0.5e}")
return w, b, J_history, p_history # 返回 w、b 和历史记录以用于绘图
# 初始化参数 (Initialize parameters)
w_init = 0
b_init = 0
# 梯度下降的一些设置 (Gradient descent settings)
iterations = 10000 # 迭代次数 (Number of iterations)
tmp_alpha = 1.0e-2 # 学习率 (Learning rate)
# 运行梯度下降 (Run gradient descent)
w_final, b_final, J_hist, p_hist = gradient_descent(x_train, y_train, w_init, b_init, tmp_alpha,
iterations, compute_cost, compute_gradient)
print(f"通过梯度下降找到的 (w, b) 值: ({w_final:8.4f},{b_final:8.4f})") # (w,b) found by gradient descent
Iteration 0: Cost 7.93e+04 dj_dw: -6.500e+02, dj_db: -4.000e+02 w: 6.500e+00, b: 4.00000e+00
Iteration 1000: Cost 3.41e+00 dj_dw: -3.712e-01, dj_db: 6.007e-01 w: 1.949e+02, b: 1.08228e+02
Iteration 2000: Cost 7.93e-01 dj_dw: -1.789e-01, dj_db: 2.895e-01 w: 1.975e+02, b: 1.03966e+02
Iteration 3000: Cost 1.84e-01 dj_dw: -8.625e-02, dj_db: 1.396e-01 w: 1.988e+02, b: 1.01912e+02
Iteration 4000: Cost 4.28e-02 dj_dw: -4.158e-02, dj_db: 6.727e-02 w: 1.994e+02, b: 1.00922e+02
Iteration 5000: Cost 9.95e-03 dj_dw: -2.004e-02, dj_db: 3.243e-02 w: 1.997e+02, b: 1.00444e+02
Iteration 6000: Cost 2.31e-03 dj_dw: -9.660e-03, dj_db: 1.563e-02 w: 1.999e+02, b: 1.00214e+02
Iteration 7000: Cost 5.37e-04 dj_dw: -4.657e-03, dj_db: 7.535e-03 w: 1.999e+02, b: 1.00103e+02
Iteration 8000: Cost 1.25e-04 dj_dw: -2.245e-03, dj_db: 3.632e-03 w: 2.000e+02, b: 1.00050e+02
Iteration 9000: Cost 2.90e-05 dj_dw: -1.082e-03, dj_db: 1.751e-03 w: 2.000e+02, b: 1.00024e+02
通过梯度下降找到的 (w, b) 值: (199.9929,100.0116)
请花一些时间观察上面打印的梯度下降过程的特征:
- 成本值(Cost)开始时很大,并且快速下降,这与讲座幻灯片中的描述一致。
- 偏导数
dj_dw
和dj_db
也迅速变小,一开始下降得很快,然后逐渐变慢。正如讲座中的图示,当过程接近“碗底”时,由于此时导数值较小,进展会变得更缓慢。 - 尽管学习率(alpha)保持不变,优化进程仍然减缓。
梯度下降中成本与迭代次数的关系 Cost versus iterations of gradient descent
绘制成本(Cost)与迭代次数的关系图是衡量梯度下降进展的有用方法。在一次成功的运行中,成本值应始终减少。由于初始阶段成本的变化非常迅速,因此将初始下降和最终下降绘制在不同的尺度上会更有帮助。在下方的图中,请注意坐标轴上成本的尺度以及迭代步长的设置。
# 绘制成本与迭代次数的关系图 (Plot cost versus iteration)
fig, (ax1, ax2) = plt.subplots(1, 2, constrained_layout=True, figsize=(12,4))
ax1.plot(J_hist[:100]) # 绘制前 100 次迭代的成本变化 (Plot cost for the first 100 iterations)
ax2.plot(1000 + np.arange(len(J_hist[1000:])), J_hist[1000:]) # 绘制从第 1000 次迭代开始的成本变化 (Plot cost starting from iteration 1000)
ax1.set_title("Cost vs. iteration (start)") # 图标题: 成本与迭代次数 (起始) (Cost vs. iteration (start))
ax2.set_title("Cost vs. iteration (end)") # 图标题: 成本与迭代次数 (末尾) (Cost vs. iteration (end))
ax1.set_ylabel('Cost') # y轴标签: 成本 (Cost)
ax2.set_ylabel('Cost') # y轴标签: 成本 (Cost)
ax1.set_xlabel('teration step') # x轴标签: 迭代步长 (Iteration step)
ax2.set_xlabel('Iteration step') # x轴标签: 迭代步长 (Iteration step)
plt.show()
预测 Predictions
现在您已经找到了参数 w 和 b 的最优值,您可以使用模型基于学习到的参数预测房屋的价值。正如预期的那样,对于相同的房屋,预测值几乎与训练值相同。此外,对于不在训练集中的值,预测结果也与预期值一致。
print(f"1000 sqft house prediction {w_final*1.0 + b_final:0.1f} Thousand dollars")
print(f"1200 sqft house prediction {w_final*1.2 + b_final:0.1f} Thousand dollars")
print(f"2000 sqft house prediction {w_final*2.0 + b_final:0.1f} Thousand dollars")
1000 sqft house prediction 300.0 Thousand dollars
1200 sqft house prediction 340.0 Thousand dollars
2000 sqft house prediction 500.0 Thousand dollars
观察梯度下降中的细节
您可以通过在成本函数 J(w, b) 的等高线图上绘制迭代过程中成本随迭代次数变化的轨迹,来展示梯度下降执行过程中的进展情况。
fig, ax = plt.subplots(1,1, figsize=(12, 6)) # 创建一个大小为 (12, 6) 的绘图区域
plt_contour_wgrad(x_train, y_train, p_hist, ax) # 绘制等高线图,显示梯度下降过程中 (w, b) 的轨迹
上图的等高线图展示了 cost(w, b) 在一系列 w 和 b 值范围内的变化。成本的不同水平由环形曲线表示。红色箭头表示梯度下降的路径。以下是一些需要注意的点:
- 路径稳步(单调地)向目标推进。
- 初始的步长比接近目标时的步长要大得多。
放大观察,我们可以看到梯度下降的最后几步。请注意,随着梯度接近零,步与步之间的距离逐渐缩小。
fig, ax = plt.subplots(1,1, figsize=(12, 4)) # 创建一个大小为 (12, 4) 的绘图区域
plt_contour_wgrad(
x_train, y_train, p_hist, ax,
w_range=[180, 220, 0.5], # w 的取值范围:从 180 到 220,步长为 0.5
b_range=[80, 120, 0.5], # b 的取值范围:从 80 到 120,步长为 0.5
contours=[1, 5, 10, 20], # 指定等高线的值
resolution=0.5 # 图像分辨率,步长为 0.5
)
学习率\alpha Increased Learning Rate
在上图中,讨论了学习率 \alpha取值的适当性问题。
随着 \alpha 增大,梯度下降收敛到解的速度会加快,但如果 \alpha 过大,梯度下降可能会发散。
上面的例子展示了一个良好收敛的解。现在,让我们尝试增大 \alpha 的值,看看会发生什么:
-
公式:
w = w - \alpha \frac{d}{dw} J(w) -
如果学习率 \alpha 太小:
- 梯度下降过程可能会非常缓慢。
- (右图上半部分展示了小步长的梯度下降路径,沿曲线逐步下降)
-
如果学习率 \alpha 太大:
- 梯度下降可能会:
- 超出最小值,无法达到最低点。
- 无法收敛,甚至出现发散。
- (右图下半部分展示了过大步长导致的发散情况,梯度下降路径偏离曲线并产生震荡)
- 梯度下降可能会:
# initialize parameters
w_init = 0
b_init = 0
# set alpha to a large value
iterations = 10
tmp_alpha = 8.0e-1
# run gradient descent
w_final, b_final, J_hist, p_hist = gradient_descent(x_train ,y_train, w_init, b_init, tmp_alpha,
iterations, compute_cost, compute_gradient)
Iteration 0: Cost 2.58e+05 dj_dw: -6.500e+02, dj_db: -4.000e+02 w: 5.200e+02, b: 3.20000e+02
Iteration 1: Cost 7.82e+05 dj_dw: 1.130e+03, dj_db: 7.000e+02 w: -3.840e+02, b:-2.40000e+02
Iteration 2: Cost 2.37e+06 dj_dw: -1.970e+03, dj_db: -1.216e+03 w: 1.192e+03, b: 7.32800e+02
Iteration 3: Cost 7.19e+06 dj_dw: 3.429e+03, dj_db: 2.121e+03 w: -1.551e+03, b:-9.63840e+02
Iteration 4: Cost 2.18e+07 dj_dw: -5.974e+03, dj_db: -3.691e+03 w: 3.228e+03, b: 1.98886e+03
Iteration 5: Cost 6.62e+07 dj_dw: 1.040e+04, dj_db: 6.431e+03 w: -5.095e+03, b:-3.15579e+03
Iteration 6: Cost 2.01e+08 dj_dw: -1.812e+04, dj_db: -1.120e+04 w: 9.402e+03, b: 5.80237e+03
Iteration 7: Cost 6.09e+08 dj_dw: 3.156e+04, dj_db: 1.950e+04 w: -1.584e+04, b:-9.80139e+03
Iteration 8: Cost 1.85e+09 dj_dw: -5.496e+04, dj_db: -3.397e+04 w: 2.813e+04, b: 1.73730e+04
Iteration 9: Cost 5.60e+09 dj_dw: 9.572e+04, dj_db: 5.916e+04 w: -4.845e+04, b:-2.99567e+04
上图中,w 和 b 在正负之间来回跳动,并且每次迭代的绝对值都在增加。此外,每次迭代中 \frac{\partial J(w,b)}{\partial w} 的符号都会改变,且成本值在增加而不是减少。这明显表明学习率过大,导致解在发散。
- 数学描述
假设当前参数为 w 和 b,且它们对应的偏导数为 \frac{\partial J(w, b)}{\partial w} 和 \frac{\partial J(w, b)}{\partial b}。如果学习率 \alpha 过大,则更新后的参数为:
w_{\text{new}} = w - \alpha \frac{\partial J(w, b)}{\partial w}
b_{\text{new}} = b - \alpha \frac{\partial J(w, b)}{\partial b}
由于 \alpha 很大,更新后的 w_{\text{new}} 和 b_{\text{new}} 可能在极小值的另一侧,导致梯度方向反转,并且偏离了极小值点。随着迭代的进行,这种偏离会越来越严重,表现为参数在正负之间跳动,且成本值不断增加。
让我们通过绘图来可视化这一现象。
plt_divergence(p_hist, J_hist,x_train, y_train)
plt.show()
上图中,左侧图展示了梯度下降前几步中 w 的变化过程。w 在正负之间振荡,且成本迅速增加。梯度下降同时作用于 w 和 b,因此需要右侧的 3D 图来完整地观察整个过程。