qinghuan
← 机器学习与统计推断 / 全部笔记

单元 08 / 机器学习与统计推断

P0 · 必会主线L1 · 基础

线性回归与梯度下降:第一次拟合数据

用 y=w*x+b 预测小数据,再从平方误差算梯度,检查参数该往哪调、学习率多大,以及形状是否正确。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能手推一维回归梯度,在固定小数据上拟合已知斜率和截距,并说明这个小实验能验证什么、不能证明什么。

学习理由、准备要求与知识点

为什么学它

线性回归把预测、损失、求梯度和改参数串成一个能手算的小例子,方便以后理解更复杂的训练循环。

先读这些内容

知识点清单

  • 特征目标与参数
  • 线性预测和截距
  • 残差与半均方误差
  • 批量梯度推导
  • 学习率与同步更新
  • 每轮重置梯度累加器
  • 损失归约和形状
  • 拟合与泛化的区别

运行前先确认Java 17;Python 3.14 标准库 CPU 教学实验。手工计算梯度与更新,无需安装训练框架;框架源码作为后续对照阅读。无 GPU、多机或真实任务泛化结论。

01先弄懂它是什么

如果手里有输入和实际结果,就可以试着找到一条预测规则,让预测尽量接近这些记录,这叫拟合。对于样本 (x_i,y_i),本篇用 yhat_i=w*x_i+b 预测结果:w 控制直线有多陡,b 控制整体上下位置,x 是输入特征,y 是实际目标。训练时调整的是 w 和 b;本例的样本数据固定,不是把每个样本的 x 也拿去随意修改。

判断预测好不好,可以先看预测值减去实际值,也就是残差 e_i=yhat_i-y_i。正负误差直接相加可能相互抵消,所以这里先平方再平均,使用 L=(1/(2n))*sum(e_i^2) 作为损失。前面的二分之一是为了让求导更方便。实验用 Python 数值和列表实现;后续框架的默认 MSE 通常没有这个二分之一,比较结果时要注意损失和梯度的数值尺度也会不同。

梯度给出损失局部增加最快的方向,因此梯度下降使用相反方向:w_new=w-lr*dw,b_new=b-lr*db。lr 为正学习率,控制一步走多远。局部下降方向不保证任意大步都降低损失,曲率较大或特征尺度较大时,过高学习率可能造成振荡甚至数值发散。

文中这几个词,先认一认

参数
训练中需要调整的模型内部数值。本篇只调整直线的 w 和 b。
残差
预测值减去真实目标,表示预测比目标高了或低了多少。
梯度下降
先算出损失在当前位置增长最快的方向,再朝反方向小步调整参数,反复尝试减小损失。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

由链式法则,单项半平方误差对预测的导数为 e_i,对 w 的导数再乘 x_i,对 b 的导数再乘一。累加并平均后,dw=sum(e_i*x_i)/n,db=sum(e_i)/n。这两项来自同一批旧参数下的预测,应先完整计算,再同时更新参数,不能更新 w 后又用新 w 计算同一步的 b 梯度。

采用 x=[-2,-1,0,1,2]、y=2x+1,从 w=b=0 开始。初始损失为四点五,dw=-4、db=-1,学习率零点一使第一步到 w=0.4、b=0.1。这个可手算的起点很重要:它能检查符号、平均分母和半因子,而不只观察最后损失似乎变小。

训练循环包含前向预测、计算损失、求导和参数更新。纯 Python 实验每次调用 gradients 都把两个累加器初始化为零,因此不会混入上一轮结果。后续框架也需要显式管理梯度清零或有意累积;这是相同数学约定的不同实现。本篇每一步都使用完整五个样本,只演示全批梯度下降,不要求提前掌握张量与自动微分。

这组数据也能用公式直接求出最小二乘解(闭式解),作为小例子的参考,但并非所有问题都适合直接求逆。特征矩阵可能秩不足,使多个参数组合在这组数据上产生同样预测;显式求逆还可能放大数值误差。实际线性求解通常考虑 QR、SVD 或正则化,梯度法则更容易扩展到大数据和后续非线性模型。

拟合到 w≈2、b≈1,只证明在这组人工线性数据及运行配置上,计算链条能恢复已知关系。真实数据包含噪声、遗漏变量、分布变化和数据泄漏风险,需要独立划分与评估。训练损失下降不能单独证明因果关系,也不能说明部署后的预测一定准确。

先跑通一个最小例子

只需 Python 3.14 标准库,无需 NumPy 或 PyTorch。将代码保存为 experiment.py,在文件所在终端执行 python experiment.py;程序用列表保存五个样本,用数值变量保存两个参数。

experiment.py · python
import math
x=[-2.,-1.,0.,1.,2.]
y=[2*v+1 for v in x]
def gradients(w,b):
    gw=gb=0.0  # 每次调用从零开始,不混入前一轮梯度。
    for xi,yi in zip(x,y):
        error=w*xi+b-yi
        gw+=error*xi
        gb+=error
    return gw/len(x),gb/len(x)
def loss(w,b):
    return math.fsum((w*xi+b-yi)**2 for xi,yi in zip(x,y))/(2*len(x))
w=b=0.0
assert loss(w,b)==4.5
gw,gb=gradients(w,b)
assert gw==-4.0
assert gb==-1.0
for _ in range(100):
    gw,gb=gradients(w,b)
    w,b=w-0.1*gw,b-0.1*gb  # 右侧先计算完,再同时写回。
assert loss(w,b)<1e-7
assert abs(w-2)<1e-3
assert abs(b-1)<1e-3
assert len(x)==len(y)  # 严格一一对应,列表不会自动做张量广播。
print("PASS 7; w =",round(w,6),"b =",round(b,6),"loss =",loss(w,b))

核对运行结果打印 PASS 7,w 接近 2、b 接近 1,损失小于 1e-7;这是固定五点 CPU 拟合结果。

正在加载流程图…

每次更新的两个梯度来自同一组旧参数,清零策略与损失尺度都属于训练契约。
查看流程图源码(Mermaid)
flowchart LR
 A["固定样本与当前参数"] --> B["预测 yhat"]
 B --> C["半均方误差"]
 C --> D["求两个参数梯度"]
 D --> E["同时更新 w 与 b"]
 E --> A

按顺序看,每一步做了什么

  1. 明确样本形状与预测公式。
  2. 定义损失及归约分母。
  3. 手算初始梯度作为参考。
  4. 每轮重置累加器,完成手工梯度与同步更新。
  5. 核对参数和损失,再另做新数据评估。

03对照实现代码

PyTorch v2.9.1:functional.mse_loss 的归约与广播 (新标签页)↗

PyTorch v2.9.1;torch/nn/functional.py · mse_loss

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
def half_mse(prediction,target):
    if len(prediction)!=len(target) or not prediction:
        raise ValueError("same nonempty shape")
    errors=[a-b for a,b in zip(prediction,target)]
    # 原创一维模型明确拒绝广播,且主动加入二分之一。
    return sum(e*e for e in errors)/(2*len(errors))
    # PyTorch 默认 mse_loss 没有二分之一,并支持 Tensor 归约路径。

PyTorch v2.9.1 的 mse_loss 默认 reduction 为 mean。它检查输入与目标形状,不一致时发出警告,然后仍通过 broadcast_tensors 扩展张量。由此可见,出现警告不一定阻止错误形状继续计算;调用方最好在任务边界明确断言预测与目标对应。

不带权重时,函数将广播后的输入、目标和归约枚举传给底层 mse_loss。带权重时,源码显式形成平方误差和加权误差,并根据 none、sum、mean 返回不同结果。加权平均的分母也有具体规则,不能默认所有 mean 都仅除以样本行数。

原创模型只有一维同形列表,并使用本文定义的半均方误差,不是原样框架实现。省略广播有利于初学者清楚看到每个预测只对应一个目标;真实多维输出需要进一步说明是按样本还是按全部元素平均,这会影响梯度尺度。

源码模块解释损失如何构造,优化器更新则是另一条链。调用 mse_loss 不会自动清零梯度、执行 backward 或调整参数。将前向目标、自动微分和优化器三个职责分开,有助于定位损失正常但参数不变、或梯度不断累积的问题。

代码先调用谁,再调用谁

  1. 预测与目标进入 mse_loss
  2. 检查形状并执行广播
  3. 选择权重与 reduction 分支
  4. 底层计算平方误差归约
  5. 调用者再执行 backward 与优化器 step

04动手写一个例子

同步梯度更新的一维线性拟合

有限非空样本数组使用半均方误差,从 w=b=0 开始全批梯度下降。学习率为正有限数,迭代次数非负;无外部训练库。

输入 1
x=[-2,-1,0,1,2], y=2x+1, lr=.1, steps=100
输出 1
w≈2,b≈1
输入 2
同样数据 steps=0
输出 2
w=b=0, loss=4.5

每一轮扫描全部样本,依据旧 w、b 计算残差和两个梯度和,再除以 n。只有完成所有样本后才一起更新参数,确保这确实是同一个损失位置的梯度。若边扫边更新,就变成另一种在线优化过程,不能继续使用相同的手算期望。

方法对样本、学习率和步数进行入口验证,并检查更新后的数值是否有限。它没有承诺所有正学习率都收敛;过大步长可能使有限输入逐步产生非有限参数,因此错误信息指向数值发散而不是伪装成成功拟合。

返回参数与最终损失,测试同时检查两者。仅损失接近零在欠定问题中未必唯一确定参数;本例数据的特征有足够变化,斜率和截距可以辨认。另用零步、全零目标、空输入和非法学习率检查实现是否尊重契约。

为什么这样写是对的一轮内所有残差来自同一组参数;轮末参数按该组参数的完整批量梯度同时更新。

时间 O((T+1)*n),T 为更新次数;T>=1 时可简写为 O(T*n)。额外空间 O(1) 额外状态,输入存储不计。

每轮一次样本扫描,最终再扫描计算损失;对固定两个参数,无需保存每个样本的计算图。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
public class Main {
  record Fit(double w,double b,double loss){}
  static Fit fit(double[] x,double[] y,double rate,int steps){
    if(x==null||y==null||x.length==0||x.length!=y.length||!Double.isFinite(rate)||rate<=0||steps<0)throw new IllegalArgumentException("input");
    for(int i=0;i<x.length;i++)if(!Double.isFinite(x[i])||!Double.isFinite(y[i]))throw new IllegalArgumentException("finite");
    double w=0,b=0;
    for(int t=0;t<steps;t++){
      double gw=0,gb=0;
      for(int i=0;i<x.length;i++){double e=w*x[i]+b-y[i];gw+=e*x[i];gb+=e;} // 全批使用旧参数。
      double nw=w-rate*gw/x.length,nb=b-rate*gb/x.length;
      if(!Double.isFinite(nw)||!Double.isFinite(nb))throw new ArithmeticException("diverged");
      w=nw;b=nb; // 两个参数在梯度完整计算后同步更新。
    }
    double sum=0;for(int i=0;i<x.length;i++){double e=w*x[i]+b-y[i];sum+=e*e;}
    double loss=sum/(2*x.length); // 半因子与梯度推导一致。
    if(!Double.isFinite(loss))throw new ArithmeticException("loss");
    return new Fit(w,b,loss);
  }
  static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-3)throw new AssertionError(a+" != "+b);}
  static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
  public static void main(String[] args){
    double[] x={-2,-1,0,1,2},y={-3,-1,1,3,5};
    Fit f=fit(x,y,.1,100);near(f.w,2);near(f.b,1);if(f.loss>1e-7)throw new AssertionError();
    Fit zero=fit(x,y,.1,0);near(zero.w,0);near(zero.b,0);near(zero.loss,4.5);
    near(fit(x,new double[5],.1,10).loss,0);
    bad(()->fit(new double[0],new double[0],.1,10));
    bad(()->fit(x,y,0,10));
    bad(()->fit(new double[]{1},new double[]{1,2},.1,10));
    System.out.println("PASS 6");
  }
}

这些特殊情况也要试一试

  • 空数据无法定义平均损失。
  • 零步返回初始模型而非偷偷训练一次。
  • 正学习率不保证收敛。
  • 预测与目标形状必须一致。
进阶选读 · 多特征与病态线性问题

场景特征尺度差异大或高度相关,梯度下降缓慢。

做法分析 X^T X 的条件性质,使用训练集标准化、适当正则化或稳定线性求解,比较优化误差与统计误差。

复杂度全批梯度通常每步 O(n*d),直接求解成本依算法与维度而异。

好处与代价缩放改善优化不等于增加信息,秩不足仍可能需要约束或更多数据。

05项目里怎么用

训练前保存数据定义和损失约定,比盲目增加迭代次数更重要。确认目标是否使用相同单位、缺失值怎样处理,以及样本权重是否正确。训练曲线异常时,先检查首批数据和可手算梯度,再逐步排查学习率、尺度与代码路径。

参数可辨识性影响结果解释。如果所有 x 都相同,多个 w、b 组合可能给出同样预测,优化器选到某组参数不代表发现唯一真实规律。现实回归中的相关关系也不自动构成因果关系,需要研究设计和额外假设;本篇人工线性关系只是用来检验计算闭环。

实际评估需要独立验证数据,并避免用测试集反复选择学习率或训练步数。训练误差可能继续下降而新数据误差开始上升,这是泛化问题而非优化器一定失效。后续文章会展开数据划分与正则化,当前实验结论严格限于固定小数据和 CPU 执行。

容易踩的坑

  • 忘记平方损失是否带二分之一。
  • 先更新一个参数再计算同一步另一个梯度。
  • 梯度未清零导致非预期累积。
  • 训练误差低就宣称对新数据有效。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
全批梯度下降更新确定,推导直观每步扫描全部数据教学和小型数据
小批随机梯度便于大数据迭代存在抽样噪声与更多状态实际训练
稳定最小二乘求解小中型线性问题可直接求解受维度和数值条件影响线性参考基线

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01为什么梯度前要减号?

展开参考答案与推导

梯度指向局部增加损失最快的方向,负梯度在足够小步长下给出下降方向。减号并不保证任意大步都会下降,步长仍需要控制。

02半均方误差与默认 MSE 梯度相同吗?

展开参考答案与推导

相差固定二倍尺度。本文乘二分之一使梯度公式更简洁;使用库时应显式对齐定义,不要在推导与代码之间忽略这个常数。

03为什么每轮重置梯度累加器?

展开参考答案与推导

每一步需要当前参数位置的批量梯度;混入旧参数位置的梯度会改变算法。实验在函数入口初始化两个和为零,后续框架也要明确清零或有意累积的约定。

换个情况,还会处理吗

01参数不变但 loss 能计算,先检查什么?

展开参考答案与推导

先确认求得的梯度不是意外恒零,再检查学习率、更新语句是否执行以及新参数是否写回。计算损失只评估当前模型,不会自动调整参数;后续框架同样区分求导与更新职责。

02学习率很大时 loss 上升怎么办?

展开参考答案与推导

先核对梯度符号和尺度,再降低学习率或改善特征缩放。正方向设置正确仍可能因步长跨越局部下降区域而发散。

03所有样本 x 都相同还能解释斜率吗?

展开参考答案与推导

通常无法唯一辨认斜率和截距,因为只约束一个组合。即便预测误差很小,也不能声称恢复了唯一参数,需要更多变化信息或明确约束。

写代码试一试

01手算首步更新。

输入 / 条件
dw=-4,db=-1,w=b=0,lr=.1
输出 / 预期
w=.4,b=.1

提示 减去梯度。

展开参考答案与推导

负梯度使两个参数增加,分别增加零点四和零点一;先算好两个梯度再写回参数,保持同一步定义一致。

02计算初始损失。

输入 / 条件
预测全零,y=[-3,-1,1,3,5]
输出 / 预期
4.5

提示 平方和除以2n。

展开参考答案与推导

平方和为四十五,n 为五,除以十得到四点五。若使用没有半因子的默认 MSE,则结果为九。

03检查错误广播。

输入 / 条件
预测形状[5,1],目标形状[5]
输出 / 预期
应先修正为对应形状

提示 库可能广播为二维。

展开参考答案与推导

显式确认每个预测只对应一个标签,再按任务选择 squeeze 或调整目标形状;不能因为损失返回标量就认为配对正确。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能手推一维回归梯度,在固定小数据上拟合已知斜率和截距,并说明这个小实验能验证什么、不能证明什么。

本篇重点加练

梯度大小受损失归约与数据表示影响,加练与样本复制、权重变化有关的等价性。

  • 批量梯度推导
  • 损失归约和形状

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    数据 x=[-1,0,2]、y=[-1,1,4],初始斜率 0.5、截距 -0.5。写出预测、残差、半均方误差和两个参数的批量梯度。

    提交内容
    完整手算记录
    怎样算完成
    • 损失定义一致
    • 截距梯度包含所有样本

    练到的知识点特征目标与参数 · 线性预测和截距 · 残差与半均方误差 · 批量梯度推导

  2. 作业 02原理问答

    比较在同一轮内依次修改参数和用同一份旧参数统一更新的含义;写出两种算法明确的状态转移定义。

    提交内容
    两份更新契约
    怎样算完成
    • 新旧状态标注完整
    • 累加器生命周期明确

    练到的知识点学习率与同步更新 · 每轮重置梯度累加器

  3. 作业 03原理问答

    把每条训练记录复制 k 次,分别研究 sum 与 mean 损失的参数梯度;写出哪些训练设置才能让一次更新保持一致。

    提交内容
    带条件的推导
    怎样算完成
    • 损失分母明确
    • 复制数据与新增信息区分

    练到的知识点批量梯度推导 · 损失归约和形状

综合应用

  1. 作业 04编程实践

    用 NumPy 实现双特征线性回归训练器,固定随机种子生成 40 条数据;保存每轮损失并对至少两个参数做独立梯度检查。

    提交内容
    CPU 程序与训练记录
    怎样算完成
    • 不依赖自动求导
    • 梯度校验与拟合分开

    练到的知识点批量梯度推导 · 学习率与同步更新 · 每轮重置梯度累加器 · 损失归约和形状

  2. 作业 05排错实验

    构造目标形状 [4,1]、预测形状 [4] 的训练案例,比较接口期望和实际参与损失的元素;加入能够捕获这一类问题的检查。

    提交内容
    复现代码与修订测试
    怎样算完成
    • 记录实际张量形状
    • 测试防止错误广播

    练到的知识点每轮重置梯度累加器 · 损失归约和形状

  3. 作业 06面试场景

    训练误差持续降低,而保留数据误差上升。设计仅改变训练时长的对照实验,写出模型选择与最终报告规则。

    提交内容
    实验协议与曲线记录
    怎样算完成
    • 只用验证集选型
    • 最终评价不参与调参

    练到的知识点特征目标与参数 · 拟合与泛化的区别

  4. 作业 07编程实践

    实现含非负样本权重的双参数线性回归,自己声明样本权重总和为零时的处理规则;用三条记录比较权重表示与整数次数复制表示的损失和梯度。

    提交内容
    CPU 程序与等价性测试
    怎样算完成
    • 权重归约明确
    • 两种表示使用同一目标

    练到的知识点批量梯度推导 · 损失归约和形状

  5. 作业 08排错实验

    给线性回归输入增加一个始终为零的特征列,在同一初值上比较各参数梯度;故意将残差与错误特征相乘,检查测试能否定位错列。

    提交内容
    故障复现与特征梯度表
    怎样算完成
    • 特征身份可检查
    • 正常对照与故障分开

    练到的知识点批量梯度推导

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    对同一数据将一个特征放大 1000 倍,比较三组学习率的收敛行为;解释稳定性报告需要保留哪些条件。

    提交内容
    尺度实验与分析报告
    怎样算完成
    • 其他因素控制一致
    • 失败运行也被记录

    练到的知识点残差与半均方误差 · 学习率与同步更新 · 拟合与泛化的区别

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    将一个目标值的单位从米改成厘米,“沿用学习率就能等价训练”是否成立?明确参数和损失单位,提出可核验的等价条件或反例。

    提交内容
    单位变更论证与小实验
    怎样算完成
    • 参数与目标同步约定
    • 相同预测与相同更新分开

    练到的知识点批量梯度推导 · 损失归约和形状

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗