算法可视化与交互学习平台

线性模型中的梯度下降Gradient Descent for a Linear Model

用最小线性回归例子理解 loss、gradient、参数更新,并通过实时调参观察训练曲线与拟合效果。

OptimizationBeginnerFree
PyodideKernel
1

目标理解

这个模块聚焦三个关键词:lossgradient参数更新。我们从最小线性模型 y = wx + b 出发,用梯度下降训练参数,并观察损失如何一步步下降。

2

交互直线:y = wx + b

单独调节斜率 w 和截距 b,观察直线如何围绕坐标轴旋转和上下平移。这里的 w 是数据空间里的直线斜率:x 每增加 1,y 改变多少;它不是梯度下降里的 ∇L。

Parameter Panel
2 Params
3

模型与损失

线性模型用参数 w、b 生成预测值,再用均方误差衡量预测与真实值之间的差异。

y^=wx+b,L=1ni=1n(yiy^i)2\hat{y} = wx + b,\quad L = \frac{1}{n}\sum_{i=1}^n (y_i - \hat{y}_i)^2
ww
bb
LL
ww
权重 / 斜率
weight / slope
bb
偏置 / 截距
bias / intercept
LL
均方误差损失
mean squared error loss
4

梯度下降更新

梯度告诉我们 loss 在参数空间中增长最快的方向,因此更新时要沿负梯度方向移动。学习率 η 控制每一步走多远。

wt+1=wtηLw,bt+1=btηLbw_{t+1}=w_t-\eta\frac{\partial L}{\partial w},\qquad b_{t+1}=b_t-\eta\frac{\partial L}{\partial b}
η\eta
Lw\frac{\partial L}{\partial w}
Lb\frac{\partial L}{\partial b}
η\eta
学习率
learning rate
Lw\frac{\partial L}{\partial w}
loss 对参数 w 的变化率,不是直线对 x 的斜率
gradient of loss with respect to w
Lb\frac{\partial L}{\partial b}
loss 对参数 b 的变化率
gradient of loss with respect to b
5

公式 + 代码 解释

1. 我们到底在做什么?

目标很简单:用一条直线去拟合一组数据。模型写成 y = wx + b,其中 w 是斜率,b 是截距。

在代码里,这一步就是:

preds = w * xs + b

这里的 wb 是要学习的参数。一开始它们通常只是随便给一个初值,所以预测往往并不准确。

2. 怎样衡量预测得好不好?

我们用损失函数 loss 衡量“模型错了多少”。这个例子使用的是均方误差:

L = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2

对应代码:

errors = preds - ys
loss = np.mean(errors * errors)

含义是:先计算每个样本的误差,再把误差平方后取平均。

  • loss 大,说明模型还差得远。
  • loss 小,说明预测已经更接近真实数据。

3. 怎样知道参数该往哪边改?

核心问题不是“要不要改参数”,而是“wb 应该往哪个方向改,才能让 loss 变小”。答案就是看梯度。

先换一个空间看问题

y = wx + b 描述的是数据空间:横轴是 x,纵轴是 y。训练时真正优化的是 L(w,b),这属于参数空间:横轴可以是 w,纵轴可以是 b,高度是 loss。所以 w 是直线的斜率,而 dwdb 是损失函数对参数的斜率。

w 求导,可得到:

\frac{\partial L}{\partial w} = \frac{1}{n}\sum_{i=1}^{n} 2(wx_i + b - y_i)x_i

代码对应:

dw = np.mean(2.0 * errors * xs)

b 求导,可得到:

\frac{\partial L}{\partial b} = \frac{1}{n}\sum_{i=1}^{n} 2(wx_i + b - y_i)

代码对应:

db = np.mean(2.0 * errors)

可以把它们理解成:

  • dw:如果改变 wloss 会怎么变。
  • db:如果改变 bloss 会怎么变。

误差越大,梯度通常也会越大;而 x 的取值越大,对 w 的影响也会更明显。

4. 梯度下降到底在“下降”什么?

梯度指向的是 loss 增长最快的方向,所以如果我们想让 loss 下降,就要沿着梯度的反方向更新参数:

w_{t+1} = w_t - \eta \frac{\partial L}{\partial w}, \qquad b_{t+1} = b_t - \eta \frac{\partial L}{\partial b}

代码就是:

w = w - lr * dw
b = b - lr * db
  • lr 就是学习率,也就是公式里的 \eta
  • 减号表示我们要往“下坡方向”走,而不是往上坡走。

5. 整个训练过程其实就是重复同一件事

训练并不神秘,本质上就是在循环里不断重复四步:

  1. 先预测:preds = w * xs + b
  2. 再计算误差和损失:errorsloss
  3. 然后计算梯度:dwdb
  4. 最后更新参数:wb

对应代码中的训练循环:

for epoch in range(num_epochs):
    preds = w * xs + b
    errors = preds - ys
    loss = np.mean(errors * errors)
    dw = np.mean(2.0 * errors * xs)
    db = np.mean(2.0 * errors)
    w = w - lr * dw
    b = b - lr * db

同时程序会把每一轮的损失记录下来:

loss_history.append(...)

6. 图表里应该重点看什么?

实验台里最值得观察的是 loss_history 对应的曲线。它展示的是:随着训练一轮轮进行,损失是如何变化的。

理想情况下,你会看到这样的趋势:

  • 开始时 loss 较大。
  • 前几轮下降较快。
  • 后面逐渐趋于稳定。

这说明参数正在逐步逼近更合适的值,拟合直线也在不断贴近数据点。

7. 一句话串起来

我们先用 loss 衡量“错多少”,再用梯度判断“往哪改”,然后按学习率控制步长,一步步更新参数,让误差持续减小,最后得到更合适的直线。

6

为什么“碗形”的是损失,不是模型

1. 先纠正一个常见误区

在线性模型里,\hat{y} = wx + b 本身是一条直线,不是碗形。

真正呈现“碗形”的,是损失函数。例如单个样本下:

L = (wx + b - y)^2

这里的 L 是关于参数的函数。只要把某个样本的 xy 和当前的 b 固定下来,它就会变成一个关于 w 的二次函数。

2. 为什么它一定像碗?

因为它本质上是“线性函数再平方”:

线性 + 平方 = 二次函数

比如固定 x = 2b = 1y = 5,那么:

L(w) = (2w + 1 - 5)^2 = (2w - 4)^2 = 4w^2 - 16w + 16

展开后可以直接看到最高次项是 4w^2,系数为正,所以曲线一定开口向上,也就是碗形。

3. 梯度和“往哪边走”有什么关系?

对上面的损失函数求导:

\frac{dL}{dw} = 8w - 16
  • w < 2 时,梯度为负,说明继续增大 w 会让损失下降。
  • w > 2 时,梯度为正,说明应该减小 w
  • w = 2 时,梯度为 0,正好落在最低点。

同理,直线的斜率是 w;而梯度下降里的梯度,是这个“碗形损失”对 w,b 的斜率。

这就是梯度下降能工作的原因:不是模型本身是碗,而是平方误差把优化目标变成了碗,梯度就能稳定地把参数往最低点推过去。

4. 和更一般的线性回归怎么对应?

上面的例子只是把问题简化成“只看一个样本、只看一个参数”。回到完整线性回归时,均方误差仍然是关于参数的凸函数,所以整体思路不变:

  • 先看当前参数下损失有多大。
  • 再看梯度的方向和大小。
  • 沿负梯度方向更新参数,让损失继续下降。

所以要记住的不是“y = wx + b 是碗”,而是“误差平方把损失函数变成了碗”。

7

固定样本下的损失曲线

固定一个样本,只看参数 w 的变化时,平方误差会形成开口向上的二次曲线。最低点对应梯度为 0,左右两侧梯度符号相反,因此梯度下降总能知道该往哪边走。
当前未自动执行后端 Python。请手动运行实验,或使用下方轻量运行查看代码输出。
Chart Python
import numpy as np

x = 2.0
b = 1.0
y = 5.0
ws = np.linspace(-1, 5, 121)
losses = (x * ws + b - y) ** 2
gradients = 2 * (x * ws + b - y) * x
w_star = 2.0
loss_star = 0.0

plot_data = {
    'data': [
        {
            'x': ws.tolist(),
            'y': losses.tolist(),
            'type': 'scatter',
            'mode': 'lines',
            'name': 'L(w) = (2w - 4)^2',
            'line': {'color': '#0f766e', 'width': 3}
        },
        {
            'x': [w_star],
            'y': [loss_star],
            'type': 'scatter',
            'mode': 'markers+text',
            'name': 'minimum',
            'text': ['grad = 0'],
            'textposition': 'top center',
            'marker': {'size': 12, 'color': '#f59e0b'}
        },
        {
            'x': ws[::20].tolist(),
            'y': losses[::20].tolist(),
            'type': 'scatter',
            'mode': 'markers',
            'name': 'sample points',
            'marker': {
                'size': [max(8, min(18, abs(g) * 0.6)) for g in gradients[::20]],
                'color': gradients[::20].tolist(),
                'colorscale': 'RdBu',
                'showscale': True,
                'colorbar': {'title': {'text': 'gradient'}}
            }
        }
    ],
    'layout': {
        'title': {'text': 'Loss over w for one sample: x=2, b=1, y=5'},
        'xaxis': {'title': {'text': 'w'}},
        'yaxis': {'title': {'text': 'L(w)'}},
        'annotations': [
            {
                'x': 0.5,
                'y': 9,
                'text': 'gradient < 0, increase w',
                'showarrow': True,
                'ax': -60,
                'ay': -30
            },
            {
                'x': 3.5,
                'y': 9,
                'text': 'gradient > 0, decrease w',
                'showarrow': True,
                'ax': 60,
                'ay': -30
            }
        ]
    }
}

print('For x=2, b=1, y=5:')
print('L(w) = (2w - 4)^2')
print('dL/dw = 8w - 16')
print('minimum at w = 2')
后端测试运行仅对具备实验权限的登录账号开放。
8

两种“斜率”:直线斜率与损失斜率

1. 先保留直觉:w 确实是直线的斜率

在数据空间里,模型 y = wx + b 画出的是一条直线。这里的横轴是 x,纵轴是 y

所以当你说“x 单位长度下,y 的变化率”时,描述的是:

\frac{dy}{dx} = w

也就是 x 每增加 1,y 会增加多少。这是模型在 x-y 空间里的几何关系。

2. 训练时换到了参数空间

梯度下降真正优化的不是 xy,而是参数 wb。训练目标可以写成:

L = L(w,b)

这时我们关心的是:沿着 w 方向挪一点,loss 怎么变;沿着 b 方向挪一点,loss 又怎么变。

于是就会得到两个偏导数:

\frac{\partial L}{\partial w},\qquad \frac{\partial L}{\partial b}

它们描述的是 loss 对参数变化的敏感程度,而不是直线本身对 x 的斜率。

3. 梯度是 loss 曲面的坡度向量

把这些方向上的变化率放在一起,就得到梯度:

\nabla L = \begin{bmatrix} \frac{\partial L}{\partial w} \\ \frac{\partial L}{\partial b} \end{bmatrix}

这时它不再是一个单独的数,而是一个向量。每个分量告诉你某个参数方向有多陡,整个向量告诉你当前 loss 往哪里上升最快。

所以梯度长度更像“loss 曲面的坡度大小”,不是“拟合直线的斜率大小”。

4. 两个空间不要混在一起

空间对象斜率/梯度在说什么
数据空间y = wx + bw 表示 x 变时 y 怎么变
参数空间L(w,b)∇L 表示参数变时 loss 怎么变

可以把前者想成纸上的一条直线,后者想成一座以 loss 为高度的山。训练不是在直线上走,而是在 loss 山面上移动参数。

5. 为什么梯度下降要减去梯度

梯度指向的是 loss 上升最快的方向。但训练目标是让 loss 下降,所以更新时要走反方向:

\theta_{t+1} = \theta_t - \eta \nabla L

这里的减号就是关键:梯度指向最陡上坡,而我们要走最陡下坡。

6. 一句话记住

w 描述的是直线在 x-y 数据空间里有多斜;∇L 描述的是 loss 在 w-b 参数空间里有多陡。

9

梯度的方向与大小

1. 梯度是一个有方向和大小的向量

在线性模型训练里,我们同时更新两个参数:wb。因此梯度不是一个单独的数,而是一个向量:

\nabla L = \begin{bmatrix} \frac{\partial L}{\partial w} \\ \frac{\partial L}{\partial b} \end{bmatrix}

它像参数空间里的一支箭头:方向告诉我们 loss 往哪里上升最快,长度告诉我们当前这片 loss 曲面有多陡。

2. 方向决定往哪走

梯度 \nabla L 指向 loss 增长最快的方向。训练要让 loss 下降,所以更新时走负梯度方向:

w = w - lr * dw
b = b - lr * db

这里的减号表示:不沿着上坡方向走,而是沿着下坡方向改参数。

3. 大小决定坡有多陡

梯度大小也叫梯度模长,可以写成:

|\nabla L| = \sqrt{\left(\frac{\partial L}{\partial w}\right)^2 + \left(\frac{\partial L}{\partial b}\right)^2}
  • 梯度大,说明当前位置坡很陡,参数更新量容易变大。
  • 梯度小,说明曲面变平,参数接近最低点时更新会自然慢下来。

4. learning rate 只是缩放器

学习率 \eta 不是用来代替梯度大小的。真正的更新量由梯度和学习率共同决定:

\Delta w = -\eta \frac{\partial L}{\partial w},\qquad \Delta b = -\eta \frac{\partial L}{\partial b}

移动量 = 梯度大小 × learning rate。 梯度大小像坡度,learning rate 像每一步允许迈出的比例。

5. 太大和太小都会出问题

如果 |\nabla L| 极大,即使学习率不大,也可能一步更新太多,导致 loss 发散、震荡甚至 NaN,这就是梯度爆炸。

如果 |\nabla L| 接近 0,参数几乎不动,模型学得很慢甚至学不动,这就是梯度消失。

Adam 这类优化器会根据历史梯度自动调节不同方向的步长,所以可以看成更聪明的梯度下降变体。

10

梯度不是背公式,是看变化如何传播

1. 梯度公式不是背出来的

求梯度时,真正的问题不是“公式是什么”,而是:如果参数 wb 动一点点,loss 会变多少。

所以所有梯度公式,本质上都在描述一件事:微小变化如何传播。

2. 求导公式来自微小变化

先看最简单的变化规律。如果 y = 5,无论 w 怎么变,y 都不变,所以导数是 0。

如果 y = aww 增加一点,y 会按 a 倍变化,所以导数是 a

也就是说,求导公式不是凭空来的,而是常见微小变化规律的总结。

3. 推一次平方求导

平方函数最能说明“公式如何长出来”。设:

y = w^2

现在让 w 增加一个很小的量 \Delta w

(w + \Delta w)^2 = w^2 + 2w\Delta w + (\Delta w)^2

所以变化量是:

\Delta y = 2w\Delta w + (\Delta w)^2

两边除以 \Delta w

\frac{\Delta y}{\Delta w} = 2w + \Delta w

\Delta w \to 0 时,最后只剩下主要变化项:

\frac{dy}{dw} = 2w

这就是求导的核心直觉:展开微小变化,保留最主要的变化项。

4. 链式法则:变化一层层传递

训练线性模型时,w 并不是直接变成 loss。它会先影响预测,再影响误差,最后影响损失:

w → prediction → error → loss

如果写成函数嵌套,就是:

y=f(u),\qquad u=g(w)

总变化率等于每一层变化倍率相乘:

\frac{dy}{dw}=\frac{dy}{du}\cdot\frac{du}{dw}

可以把它想成齿轮传动:第一层放大 3 倍,第二层放大 5 倍,最终就是 15 倍。

5. 回到本模块:dw 和 db 怎么来的

单个样本的平方误差可以写成:

L=(wx+b-y)^2

它可以拆成三层:

prediction = w * x + b
error = prediction - y
loss = error * error

因为平方的变化率是 2 * error,而 w 对 prediction 的影响倍率是 x,所以:

\frac{\partial L}{\partial w}=2(wx+b-y)x

b 对 prediction 的影响倍率是 1,所以:

\frac{\partial L}{\partial b}=2(wx+b-y)

多样本时,把每个样本的梯度取平均,就得到实验台里的代码:

dw = np.mean(2.0 * errors * xs)
db = np.mean(2.0 * errors)

6. 神经网络和自动求导也是同一件事

神经网络只是把这种变化传播链条拉长了:

x → Linear → ReLU → Linear → Loss

PyTorch 和 TensorFlow 的自动求导不是魔法。它们会记录计算图,然后从 loss 往回不断应用链式法则,这就是反向传播。

梯度公式不是“记忆公式”,而是“描述微小变化如何传递”的数学规律。

实验

实验台

调整学习率、训练轮数、初始参数和观察 epoch,查看参数点在 w-b-loss 曲面上的下降轨迹、梯度大小变化,以及对应拟合直线和数值变化。

Parameter Panel
5 Params
Initial w
w0 · [-5, 5], step 0.1
-1.5
Initial b
b0 · [-5, 5], step 0.1
2
先运行 Tiny Transformer。运行完成后,View Epoch 和 Edge Threshold 会启用;拖动它们只切换已保存的训练帧和边阈值,不会重新训练模型。
拓展
延伸提问