从零理解模型训练:参数、损失与梯度下降的最小闭环

举报
柠檬🍋 发表于 2026/10/17 20:12:31 2026/10/17
【摘要】 从零理解模型训练:参数、损失与梯度下降的最小闭环 一、先看一个具体问题假设你手上有一份房屋数据,每条记录包含三个特征:面积、房龄、到地铁的距离,以及一个目标值:成交价。你想让机器学会"给定特征,预测价格"。用最朴素的语言描述这件事,机器需要做三件事:先猜一个规则,比如"价格 = 2 × 面积 − 3 × 房龄 + 0.5 × 距离 + 1.2";拿这个规则去算已有的数据,看算出来的价格和真...

从零理解模型训练:参数、损失与梯度下降的最小闭环

一、先看一个具体问题

假设你手上有一份房屋数据,每条记录包含三个特征:面积、房龄、到地铁的距离,以及一个目标值:成交价。你想让机器学会"给定特征,预测价格"。

用最朴素的语言描述这件事,机器需要做三件事:

  1. 先猜一个规则,比如"价格 = 2 × 面积 − 3 × 房龄 + 0.5 × 距离 + 1.2";
  2. 拿这个规则去算已有的数据,看算出来的价格和真实价格差多少;
  3. 根据差多少,把规则里的数字调一调,让下次算得更准。

然后重复第 2、3 步,直到误差小到可以接受。

这三步就是**模型训练(model training)**的全部骨架。听起来简单得不像"人工智能",但无论你后面用多大的 Transformer、多少张 GPU,底层逻辑仍然是这三步,只是"规则"变复杂了、"调数字"的方法变精细了。

这篇文章的目标,是把这三步拆成可以手推、可以手写、可以运行的完整闭环。读完你应该能回答:

  • 参数(parameter)到底是什么,和超参数(hyperparameter)差在哪;
  • 损失函数(loss function)在衡量什么;
  • 梯度(gradient)为什么能指示"往哪调";
  • 学习率(learning rate)在控制什么;
  • 一次训练迭代里,代码层面究竟发生了什么。

二、把"规则"形式化:参数与假设空间

机器学习里的"规则",标准叫法是模型(model),或者更严谨地说,是假设(hypothesis)。上面那个线性规则可以写成:

[
\hat{y} = w_1 x_1 + w_2 x_2 + w_3 x_3 + b
]

其中:

  • (x_1, x_2, x_3) 是输入特征(feature),来自数据,训练过程中不会被改动;
  • (w_1, w_2, w_3) 和 (b) 是参数,它们才是训练要调整的对象;
  • (\hat{y})(读作 y-hat)是模型的预测值(prediction);
  • (b) 通常叫**偏置(bias)**或截距,作用是把整条直线上下平移,没有它,当所有特征为 0 时预测必然为 0,表达能力受限。

用矩阵写法更紧凑。把 (N) 条样本、每条 (D) 个特征堆成矩阵 (X \in \mathbb{R}^{N \times D}),参数堆成向量 (w \in \mathbb{R}^{D}),那么所有样本的预测就是:

[
\hat{y} = Xw + b\mathbf{1}
]

这里的 (Xw) 是一次矩阵乘法,形状从 ((N, D)) 变成 ((N,))。

**假设空间(hypothesis space)**指的是"参数取遍所有可能值之后,能得到的所有模型的集合"。对线性模型来说,就是所有直线/超平面;对神经网络来说,是所有可能的非线性映射。训练的本质,是在这个空间里找一组参数,使某个"好坏标准"达到最优。

这就引出一个关键区分:

类别 含义 例子
参数 训练过程中由优化算法自动更新 权重 (w)、偏置 (b)、神经网络每层的矩阵
超参数 由人在训练前或训练中设定,不通过梯度更新 学习率、批量大小、层数、正则化系数

这个区分不是学术洁癖,而是工程分工:参数是"算出来的",超参数是"选出来的"。后面所有关于调参的讨论,基本都围绕超参数展开。

三、把"差多少"形式化:损失函数

有了模型,就需要一个量化标准来衡量预测和真实值的差距。这个标准就是损失函数,也叫目标函数(objective function)或代价函数(cost function)。注意:实践中这三个词经常混用,但严格说,"目标函数"是优化器真正去最小化的那个量,损失函数是其中衡量误差的部分,二者在有正则项时会不同(这一点会在后续讨论正则化时展开)。

回归任务最常用的是均方误差(Mean Squared Error, MSE):

[
L(w, b) = \frac{1}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i)^2
]

为什么平方?三个理由:

  1. 消除符号:预测偏高和偏低都算"错",不相互抵消;
  2. 放大严重错误:差 10 的样本,惩罚是差 1 的 100 倍,模型会优先修正离谱的预测;
  3. 数学性质好:平方函数处处可导,且导数是线性的,求梯度非常方便。

但平方也有代价:对离群点(outlier)极其敏感。一条被错误标注成天价的房子,会让模型整体被拉偏。这时候可以考虑**平均绝对误差(Mean Absolute Error, MAE)**或 Huber 损失。选择哪种损失,本质是在回答"我更怕哪种错误"。

关键认知:损失函数是你对"什么是好模型"的定义。损失设计错了,优化过程再完美,得到的也是错误的目标。这个坑在训练大型模型时尤其致命——模型会很忠实地优化你写下的东西,而不是你以为你写下的东西。

四、把"怎么调"形式化:梯度与梯度下降

现在到核心问题:知道当前损失是 3.5,怎么决定参数该往哪个方向调?

答案是梯度(gradient)。梯度是多元函数对所有参数的偏导数组成的向量:

[
\nabla L = \left(\frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}, \frac{\partial L}{\partial w_3}, \frac{\partial L}{\partial b}\right)
]

它有两个几何含义,第二个才是关键:

  • 方向:梯度指向函数值上升最快的方向;
  • 大小:梯度的模长表示这个方向上的变化率有多陡。

既然梯度指向上升最快方向,那么沿着梯度的反方向走一小步,函数值就会下降。这就是梯度下降(Gradient Descent):

[
w \leftarrow w - \eta \nabla_w L
]

其中 (\eta) 是学习率,控制每一步走多远。

对上面的 MSE 和线性模型,偏导数可以手推出来:

[
\frac{\partial L}{\partial w} = \frac{2}{N}X^{\top}(\hat{y} - y), \qquad
\frac{\partial L}{\partial b} = \frac{2}{N}\sum_{i=1}^{N}(\hat{y}_i - y_i)
]

推导并不复杂:令残差 (r_i = \hat{y}i - y_i),则 (L = \frac{1}{N}\sum r_i^2),链式法则给出 (\frac{\partial L}{\partial w_j} = \frac{2}{N}\sum_i r_i \frac{\partial r_i}{\partial w_j}),而 (\frac{\partial r_i}{\partial w_j} = x{ij}),写成矩阵形式就是上面的结果。

这里有个容易被忽略的点:梯度是局部信息。它只告诉你"在当前这个点上,往哪走下降最快",不保证指向全局最优。对于线性回归这类凸问题(convex problem),局部最优即全局最优,梯度下降有理论保证;对神经网络的非凸损失曲面,梯度下降只能保证找到一个不错的驻点。这不是缺陷,而是深度学习能工作的前提之一——我们并不需要全局最优。

五、手写一个最小闭环(NumPy 版)

下面用纯 NumPy 实现完整闭环,不依赖任何深度学习框架。这样你能看清每一步在算什么。

环境与依赖

  • Python 3.9 及以上
  • NumPy 1.22 及以上(仅用到基础线性代数与随机数接口)

运行方式:保存为 gd_numpy.py,执行 python gd_numpy.py。

"""用纯 NumPy 实现线性回归的梯度下降,展示训练的最小闭环。"""
import numpy as np

# ---------- 1. 造一份可控的合成数据 ----------
rng = np.random.default_rng(seed=0)   # 固定随机种子,保证结果可复现
N, D = 200, 3                          # 200 条样本,每条 3 个特征

X = rng.normal(size=(N, D))            # 特征:标准正态分布
w_true = np.array([2.0, -3.0, 0.5])    # 真实参数(只有造数据时才知道)
b_true = 1.2
noise = rng.normal(scale=0.1, size=N)  # 观测噪声
y = X @ w_true + b_true + noise        # 目标值

# ---------- 2. 初始化参数 ----------
w = np.zeros(D)
b = 0.0
lr = 0.1          # 学习率(超参数)
n_steps = 200     # 迭代步数(超参数)

# ---------- 3. 训练循环:预测 -> 算损失 -> 求梯度 -> 更新 ----------
for step in range(n_steps):
    y_hat = X @ w + b                  # 前向:算出预测
    residual = y_hat - y               # 残差
    loss = np.mean(residual ** 2)      # 损失:均方误差

    grad_w = 2.0 * X.T @ residual / N  # 对 w 的梯度
    grad_b = 2.0 * np.mean(residual)   # 对 b 的梯度

    w -= lr * grad_w                   # 梯度下降更新
    b -= lr * grad_b

    if step % 40 == 0 or step == n_steps - 1:
        print(f"step {step:3d}  loss={loss:.5f}  b={b:+.3f}")

# ---------- 4. 检查结果 ----------
print("\n学到的 w:", np.round(w, 3))
print("真实的 w:", w_true)
print(f"学到的 b: {b:.3f}  真实的 b: {b_true}")

一次典型运行的输出大致如下(不同 NumPy 版本的随机数实现一致,结果应当稳定):

step   0  loss=24.93086  b=+0.063
step  40  loss=0.01117  b=+1.184
step  80  loss=0.01001  b=+1.193
step 120  loss=0.00988  b=+1.196
step 160  loss=0.00986  b=+1.198
step 199  loss=0.00986  b=+1.198

学到的 w: [ 1.995 -2.994  0.499]
真实的 w: [ 2.  -3.   0.5]
学到的 b: 1.198  真实的 b: 1.2

注意几点:

  • 学到的参数不会精确等于真实参数,因为数据里有噪声(scale=0.1),模型只能逼近生成数据的规律,无法还原被噪声掩盖的真值;
  • 损失下降到约 0.0099 后基本不动,这个下限大致等于噪声方差 (0.1^2 = 0.01),说明模型已经学到了信号部分,剩下的误差是数据本身无法解释的;
  • 如果我们把 noise 设为 0,损失会一路降到非常接近 0,参数也会非常接近真值。

关于学习率的实验,值得动手改一改:

  • lr = 0.001:收敛极慢,200 步远远不够;
  • lr = 0.5:可能仍然收敛,但曲线更抖;
  • lr = 10:几乎必然发散,损失变成 nan 或天文数字。

这直观说明了学习率是训练中最敏感的超参数之一。

六、换成 PyTorch:同一件事,自动化了梯度

手工推导梯度只对最简单的模型可行。一旦模型变成几百万参数的神经网络,必须靠自动微分(automatic differentiation)。下面用 PyTorch 重写同一段逻辑。

环境与依赖

  • Python 3.9 及以上
  • PyTorch 1.13 / 2.x(torch.optim.SGD、Tensor.backward() 等接口长期稳定,2.x 系列均适用)
"""用 PyTorch 实现同一套训练闭环,梯度由自动微分计算。"""
import torch

torch.manual_seed(0)

# ---------- 数据 ----------
N, D = 200, 3
X = torch.randn(N, D)
w_true = torch.tensor([2.0, -3.0, 0.5])
b_true = 1.2
y = X @ w_true + b_true + 0.1 * torch.randn(N)

# ---------- 参数:requires_grad=True 表示需要计算梯度 ----------
w = torch.zeros(D, requires_grad=True)
b = torch.zeros(1, requires_grad=True)

optimizer = torch.optim.SGD([w, b], lr=0.1)   # 优化器接管"更新"这一步
loss_fn = torch.nn.MSELoss()                   # 损失函数

# ---------- 训练循环 ----------
for step in range(200):
    optimizer.zero_grad()          # 1) 清空上一轮的梯度(重要!)
    y_hat = X @ w + b              # 2) 前向计算
    loss = loss_fn(y_hat, y)       # 3) 计算损失
    loss.backward()                # 4) 反向传播,填充 w.grad / b.grad
    optimizer.step()               # 5) 按梯度更新参数

    if step % 40 == 0 or step == 199:
        print(f"step {step:3d}  loss={loss.item():.5f}")

print("\n学到的 w:", w.detach().numpy().round(3))
print("学到的 b:", b.item().round(3))

对照两版代码,五步流程完全一致:

  1. optimizer.zero_grad():清空历史梯度。PyTorch 的梯度是累加的,不清零会导致梯度越滚越大。这是新手最常见的 bug 之一。
  2. 前向计算:得到预测值。
  3. 计算损失:把预测和真值交给损失函数。
  4. loss.backward():从损失出发,沿计算图反向求出所有 requires_grad=True 张量的梯度。
  5. optimizer.step():用梯度更新参数。

注意 loss_fn(y_hat, y) 的参数顺序:PyTorch 的 MSELoss 计算的是 (input - target)^2 的均值,交换顺序对 MSE 无影响,但对交叉熵等损失函数顺序至关重要,习惯上应写成"预测在前,真值在后"。

七、几个必须澄清的概念

epoch、batch、step 不是一回事

  • 样本(sample):一条数据;
  • 批量(batch):一次前向/反向计算同时处理的一组样本;
  • 迭代步(step / iteration):处理一个 batch 并更新一次参数;
  • 轮次(epoch):把训练集完整过一遍。

若训练集有 10000 条样本,批量大小为 100,则 1 个 epoch = 100 个 step。这三个词在日常交流中被大量混用,写实验记录和复现他人结果时务必区分。

梯度下降是"贪心"的

每一步都只根据当前位置的局部斜率做决策,没有全局规划。这意味着:

  • 损失曲面上如果存在"峡谷"地形,普通梯度下降会沿着陡峭方向来回震荡,收敛缓慢;
  • 初始参数的位置会影响最终结果;
  • 学习率过大时,可能直接跳过谷底,甚至越走越高。

训练不是"教会机器知识",而是"拟合数据分布"

模型学到的是训练数据中体现出来的统计规律。如果训练数据有偏、有噪声、覆盖不全,模型就会忠实地学到这些缺陷。这也是为什么数据工程在真实项目中往往比模型结构更耗时。

八、常见误区

误区一:损失降到 0 就是成功。
损失为 0 通常意味着模型把训练集"背"下来了,包括其中的噪声和标注错误,泛化能力反而更差。这是**过拟合(overfitting)**的典型信号。

误区二:训练集上的表现能代表真实能力。
不能。必须用模型从未见过的数据来评估。如何划分数据、如何避免信息泄漏,是下一篇要处理的问题。

误区三:梯度下降必然找到最优解。
只有凸问题才有这个保证。深度学习中我们追求的是"足够好的解",而非"全局最优解"。

误区四:参数越多越强。
参数数量决定模型的容量(capacity),容量必须与数据量和任务复杂度匹配。容量远大于需求时,正则化和早停等手段就变得必要。

误区五:调参就是调学习率。
学习率极其重要,但批量大小、初始化方式、优化器选择、损失函数设计同样影响结果,而且它们之间存在耦合。

九、适用边界与延伸方向

本文的闭环适用于任何基于梯度的训练,但有几个前提:

  • 可导:损失函数必须对参数可导(或在几乎处处可导),否则无法直接用梯度下降。强化学习中的策略梯度、进化策略属于绕开这一限制的路线。
  • 有监督信号:我们假设每条样本都有对应的目标值 (y)。无监督和自监督学习需要自己构造监督信号(比如用"预测被遮住的词"来替代人工标注)。
  • 能承受计算量:每更新一次都要遍历一遍参与计算的样本。数据规模极大时,需要靠采样、分布式和梯度累积等手段。

从这三点出发,可以自然延伸出后续一系列问题:监督信号从哪来(学习范式)、如何保证评估可靠(数据划分与评估指标)、如何让更新更高效(优化器与学习率调度)、如何在有限显存下训练大模型(分布式与显存优化)。

十、小结

把训练的最小闭环压缩成一句话:

用当前参数做出预测,用损失函数衡量差距,用梯度指示调整方向,用学习率控制调整幅度,然后重复。

这句话在 20 行的 NumPy 脚本里成立,在千亿参数模型的训练框架里同样成立。区别只在于:模型更复杂、梯度靠自动微分、更新由优化器完成、计算被切分到成千上万张卡上。

理解了这个闭环,后面所有内容——损失函数的选择、优化器的演进、正则化的作用、分布式训练的动机——都能挂到这条主线上,而不是零散的知识点。


动手练习:保留上面这份 NumPy 脚本,试着在损失函数里加一项 (\lambda \lVert w \rVert^2)((\lambda) 取 0.01、0.1、1.0),观察学到的参数范数如何变化,以及损失下限如何上升。这一项就是 L2 正则化,它体现了"训练目标"与"损失函数"在加入惩罚项后并不完全等同。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。