梯度下降拟合直线:参数爆炸及收敛异常问题求助
首先,我们来一步步拆解你遇到的两个核心问题:初始代码的参数爆炸,以及修改后代码的收敛缓慢且结果偏离最小二乘解。
一、初始代码的参数爆炸原因
你的初始update函数犯了梯度下降的根本性错误:梯度下降的参数更新公式是当前参数 = 当前参数 - 学习率 × 梯度,但你直接把descent_rate * 梯度赋值给了m和c,完全没有保留原有参数的基础值:
# 错误的更新逻辑 def update(n,A,b,m,c,descent_rate): return descent_rate * der_wrt_m(n,A,b,m,c)), descent_rate * der_wrt_c(n,A,b,m,c))
这导致参数每一步都直接被梯度的倍数覆盖,而非在原有基础上微调,必然会出现参数迅速发散到无穷大、NaN的情况。
二、修改后代码的收敛问题分析
你修复了更新逻辑,但还有几个关键错误导致收敛异常:
1. 损失函数与梯度计算的特征参数混用
在调用mean_squared_error时,你传入了A(包含x和常数项的矩阵),但函数内部用m*A[i] + c计算预测值——A[i]是二维数组(比如[0,1]),这会触发数组运算错误,损失函数计算完全偏离真实值,进而干扰梯度更新的方向。
2. 偏置项c的梯度符号错误
对c的梯度计算有误:
def der_wrt_c(n,A,b,m,c): d = 0 for i in range(n): d += (2 * (b[i] - (m*A[i] + c))) return d/n
正确的均方误差对c的梯度应该是:
$$\frac{\partial MSE}{\partial c} = \frac{2}{n}\sum_{i=1}^n (m x_i + c - y_i)$$
等价于:
$$\frac{\partial MSE}{\partial c} = -\frac{2}{n}\sum_{i=1}^n (y_i - (m x_i + c))$$
你的代码中d是2*(y_i - ...)的和,返回d/n等于负的真实梯度。在update函数中,c - descent_rate * der_wrt_c相当于c + 学习率 × 真实梯度,完全搞反了更新方向,这是收敛缓慢且结果偏离的核心原因。
3. 学习率设置过小
你设置的descent_rate=0.0001太小,即使方向正确,10000次迭代也不足以让参数收敛到最优值。结合你的数据范围(x∈[0,3]),可以适当调大学习率。
三、修复后的完整代码
下面是修正了所有问题的代码,同时添加了损失值跟踪,方便观察收敛过程:
import numpy as np import matplotlib.pyplot as plt def mean_squared_error(n, x, y, m, c): e = 0 for i in range(n): # 使用x[i]而非A[i],确保是标量运算 e += ((y[i] - (m * x[i] + c)) ** 2) return e / n def der_wrt_m(n, x, y, m, c): d = 0 for i in range(n): d += x[i] * (y[i] - (m * x[i] + c)) # 正确的梯度:2/n * sum( (mx+c - y)*x ) = -2/n * sum( x*(y - mx -c) ) return (-2 * d) / n def der_wrt_c(n, x, y, m, c): d = 0 for i in range(n): d += (y[i] - (m * x[i] + c)) # 正确的梯度:2/n * sum( mx+c - y ) = -2/n * sum( y - mx -c ) return (-2 * d) / n def update(n, x, y, m, c, descent_rate): # 参数更新:当前值 - 学习率 × 梯度 new_m = m - descent_rate * der_wrt_m(n, x, y, m, c) new_c = c - descent_rate * der_wrt_c(n, x, y, m, c) return new_m, new_c # 数据准备 x = np.array([0, 1, 2, 3]) y = np.array([1, 2, 0, 3]) def descent(x, y): m = 0 c = 0 # 调大学习率,适配数据范围 descent_rate = 0.1 iterations = 1000 n = len(x) plt.scatter(x, y, label='Raw Data') # 跟踪损失变化 loss_history = [] for itr in range(iterations): m, c = update(n, x, y, m, c, descent_rate) current_loss = mean_squared_error(n, x, y, m, c) loss_history.append(current_loss) # 每100次迭代打印状态 if itr % 100 == 0: print(f"Iteration {itr}: m={m:.4f}, c={c:.4f}, Loss={current_loss:.4f}") # 绘制拟合直线 u = np.linspace(0, 3, 100) plt.plot(u, u * m + c, 'r-', label=f'Fitted Line: y={m:.2f}x + {c:.2f}') plt.xlabel('x') plt.ylabel('y') plt.legend() plt.show() # 绘制损失收敛曲线 plt.plot(range(iterations), loss_history) plt.xlabel('Iterations') plt.ylabel('MSE Loss') plt.title('Loss Convergence') plt.show() # 输出最终结果与最小二乘解对比 print(f"\nFinal Result: m={m:.4f}, c={c:.4f}") print(f"Minimum Squares Solution: m=0.4, c=0.9") descent(x, y)
四、修复后的效果
运行这段代码后,你会看到:
- 损失值快速下降,几百次迭代后就收敛到稳定值(约1.05,和最小二乘的MSE一致)
- 最终参数
m≈0.4,c≈0.9,完全匹配最小二乘的最优解 - 参数不会出现爆炸或NaN的情况
额外建议
- 特征缩放:如果数据范围很大(比如x∈[0,1000]),建议先对x做标准化(
(x - x.mean())/x.std()),这样可以使用更大的学习率,加速收敛。 - 自适应学习率:可以尝试使用Adam、Adagrad等自适应学习率优化器,避免手动调参的麻烦。
- 批量/随机梯度下降:你的代码是批量梯度下降(每次用所有数据计算梯度),如果数据量很大,可以改为随机梯度下降(每次用单个样本)或小批量梯度下降,提升效率。
内容的提问来源于stack exchange,提问作者Ahmad Moussa

