☰
线性回归:一文看懂深度学习的最小训练闭环
2026/10/5 7:14:58 网站建设 项目流程

打开李沐《动手学深度学习》的时候,我的第一感觉是:这书怎么这么敢写。开头几十页就敢直接把多层感知机、卷积神经网络、循环神经网络全部串一遍,而且每个模型都用“从零实现 + 简洁实现”两条路线去推。但真正让我卡住然后想通的地方,反而是最不起眼的那一章——线性回归(Linear Regression)。

说出来有点丢人,当时我已经把后面几个复杂模型跑通了,回头再看线性回归,才意识到一个问题:我之前跑那些复杂模型,很多代码都是“照着抄、能跑就行”,对于模型怎么更新参数、损失到底在算什么、一个完整的训练过程由哪些环节组成,其实并没有形成体系。而线性回归这一章,恰恰把这个东西讲透了。

它给出的就是全深度学习里最小的训练闭环。什么是闭环?就是给定数据和初始参数,模型通过一次前向计算得到预测,通过损失函数算出误差,通过反向传播算出每个参数的梯度,再用优化算法更新参数,然后进入下一轮。这个过程在任何一个深度学习模型里都一样,只是线性回归没有卷积、没有注意力、没有各种花哨模块,所以你能清清楚楚看到每一环在干什么。如果说后面那些复杂模型是精装房,线性回归就是清水房——水管怎么走、电线怎么埋,全都能看见。

这篇文章我整理了四块内容:线性回归到底在解决什么问题、最小训练闭环由哪几部分组成、如何手动实现一条完整训练循环、以及我实操中踩过的一些坑和排查技巧。李沐在课程里常说“我们动手学”,所以这篇文章里能动手的地方我也全部给到了可运行的代码,你照着敲一遍,再回去看那些复杂模型,会有一种豁然开朗的感觉。

1. 线性回归到底在解决什么问题

1.1 从房价预测场景看懂模型的输入输出

线性回归最经典的切入场景是房价预测。假设我们现在要评估一套房子能卖多少钱,手头有一些历史成交记录,里面有房屋面积、房龄、楼层、周边配套等信息。这些就是特征,也就是模型的输入,通常记为 ( x )。目标值就是成交价,记为 ( y )。线性回归做的事情,就是找到一组权重和偏置,让 ( y ) 和 ( x ) 之间建立一个线性关系:( \hat{y} = w_1 x_1 + w_2 x_2 + ... + w_d x_d + b )。

这句话听起来简单,但里面藏着深度学习的核心语义。权重 ( w ) 表示“每个特征对结果的影响方向和强度”,偏置 ( b ) 表示“当所有特征都为 0 时的一个基准值”。训练过程本质上就是不断调整这两个东西,让预测值 ( \hat{y} ) 尽可能接近真实值 ( y )。

为了更直观,李沐在课程里构造了一个人造数据集,并给出了具体的生成代码。这个设计非常聪明——用真实数据做实验,你无法确认模型是否真的学对了,因为真实世界的映射关系你不知道。但人工数据集的生成规则是明确的,比如设定真实权重是 ( w = [2, -3.4] ),真实偏置是 ( b = 4.2 ),再叠加一些随机噪声。这样一来,你训练完模型后可以直接对比学到的参数和真实参数,误差在哪个数量级,一眼就能看出来。这也是判断一个训练闭环是否正确的最快方式。

我在实际跟代码的时候,发现这个“人造数据集”的做法非常值得记住。后来我做任何新模型验证,都习惯先在一个自己可控的合成数据上跑通流程,再换真实数据。这能帮你把“模型实现写错了”和“数据本身的问题”快速隔离出来。

1.2 线性假设意味着什么,又在哪里失效

线性回归的“线性”二字,既是它的最大优势,也是它的最大限制。所谓线性假设,就是假设目标值和特征之间是直线关系。比如面积每增加 1 平米,房价固定增加某个金额。但真实世界很少有这种严格的直线关系。面积大的房子,单价可能反而低;房龄高的房子,折旧速度可能放缓。这些问题用纯线性模型是拟合不了的。

所以你需要理解,线性回归在深度学习里的定位不是“万能模型”,而是一个“基线模型”。它的意义在于:如果连线性关系都没学好,那问题可能出在数据处理或训练流程上;如果线性关系学好了但效果不够,说明数据里存在非线性关系,这时候再去考虑更复杂的模型。

实际上,神经网络可以看成是线性回归的堆叠和扩展。每一层做的事情先是一个线性变换(就是线性回归的加权求和),再套一个非线性激活函数。卷积神经网络里的卷积核操作,本质也是一种带共享参数的线性变换。理解了线性回归,你就理解了一切神经网络最基础的数学单元。

1.3 为什么线性回归是深度学习的“最小训练闭环”

我们说线性回归是“最小训练闭环”,因为它的完整训练过程包含了一个模型训练所需要的全部环节,但每一个环节都不需要额外的前置知识。你不需要理解感受野、不需要理解注意力机制、不需要理解反向传播的链式法则(虽然梯度下降本身也需要推导,但它的式子足够简单,可以直接手算验证)。

具体来说,一个线性回归训练闭环包含四个环节:

  1. 模型结构:定义输入怎么映射到输出。
  2. 损失函数:定义预测值和真实值之间的差距怎么量化。
  3. 优化算法:定义如何根据损失调整参数。
  4. 数据迭代:定义每次训练更新参数时用多少数据、按什么顺序喂给模型。

这四个环节,翻译成任何复杂模型的训练,都是同一套骨架。你把“线性回归”换成“卷积神经网络”,模型结构变了;把“均方误差”换成“交叉熵”,损失函数变了;把“随机梯度下降”换成“Adam”,优化算法变了。但流程的骨架没有任何变化。所以当你把线性回归的代码一行一行写明白,就等于给后续所有模型的学习搭好了脚手架。

我还想多说一句:李沐这本书选取的代码风格,刻意把“从零实现”放在前面,而不是直接甩出一个nn.Linear了事。原因就是,如果你直接用 PyTorch 的封装,你根本不知道autograd背后发生了什么,也不知道optim.SGD到底更新了什么。从零实现,你被迫面对每一条公式、每一个矩阵形状,这比任何讲解都有效。

2. 最小训练闭环的四个基本单元

如果把一个训练闭环比作一条流水线,那这四个基本单元就是流水线上的四个工位:模型负责把输入变成输出,损失负责给输出打分,优化器根据分数调整产线参数,数据迭代负责把原料源源不断送上产线。缺了任何一个工位,生产线都转不起来。

2.1 模型结构:从输入到输出的映射

线性回归的模型结构可以写成一行代码:

def linreg(X, w, b): return torch.matmul(X, w) + b

这里X的形状是(batch_size, num_features),w的形状是(num_features, 1),b是一个标量。torch.matmul(X, w)做的是矩阵乘法,结果形状是(batch_size, 1),加上偏置后就是最终的预测值。

这里的batch_size可以理解为一次喂给模型的样本数。一个函数搞定模型,这大概是全深度学习里最简单的模型结构了。但它的形状转换逻辑和后面任何模型完全一致:输入进模型,经过一层或若干层计算,输出预测结果。你只需要注意输入张量和权重张量在最后一个维度上要对齐。

另外我提一个细节:为什么偏置b不直接并进权重w?其实可以,只要在特征矩阵里加一列全 1 就行了,这样模型就变成 ( \hat{y} = [X, 1] \cdot [w, b] )。很多框架在底层就是这么干的,只是对使用者来说,单独把偏置拎出来更直观。所以代码里保留w和b两个参数。

2.2 损失函数:三步看懂均方误差的设计逻辑

线性回归用的损失函数是均方误差,公式长得这样:

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

第一次看到这个公式的人通常会问三个问题:为什么要平方?为什么要除以 2?为什么要加一个 ( \frac{1}{n} )?

先回答平方。如果我们用 ( |\hat{y} - y| ) 这种绝对值形式,它在 0 点不可导,梯度下降算起来麻烦,而且绝对值函数对误差的惩罚是线性的,两个相差 10 和相差 1 的错误,会得到不成比例的梯度。平方则放大了大误差的影响,让模型更倾向于优先纠正那些错得离谱的样本。同时平方函数处处可导,求导之后是一个线性表达式,做梯度下降非常顺畅。顺便说一下,这就是 L2 损失的来源,它对异常值非常敏感,训练时如果数据里混入了极端离群点,你会看到损失被个别样本带着走。

再回答除以 2。这个操作纯粹是为了求导方便。对 ( (\hat{y} - y)^2 ) 求偏导会得到一个 2,除以 2 之后梯度就变成 ( (\hat{y} - y) ),清爽不少。这是数学上的一个小技巧,没有物理含义。很多教材里的损失函数都会这么干,比如某些正则化项里也会出现 ( \frac{\lambda}{2} |w|^2 ),都是为了求导方便。

最后是 ( \frac{1}{n} )。这个叫平均,是为了让损失值跟样本数量解耦。如果不做平均,1000 个样本的损失和 1 个样本的损失在数值上完全没有可比性,学习率也不好调。平均之后,不管 batch size 是 32 还是 128,损失的数值尺度大致稳定在同一范围。这一点在调参时非常重要——你会发现损失曲线和 batch size 的相关性会小很多。

代码实现如下:

def squared_loss(y_hat, y): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

注意这里没有做均值,因为后面的优化器是按 batch 汇总的,我们在训练循环里会对一个 batch 的所有样本损失求均值再反向传播,这样可以防止单个样本的噪声主导梯度。

2.3 优化算法:梯度下降的原理和一种“管中窥豹”的方式

线性回归用的优化算法是随机梯度下降(SGD,Stochastic Gradient Descent)。它的核心思想非常朴素:既然损失函数告诉我们当前预测有多差,那沿着损失下降最快的方向更新参数,就能让预测更好。

“沿着损失下降最快的方向”在数学上就是负梯度方向。对权重 ( w ) 求偏导,得到:

[ \frac{\partial L}{\partial w} = \frac{1}{n} \sum_{i=1}^{n} x_i (\hat{y}_i - y_i) ]

这个式子的含义是:每个样本都会产生一个“预测误差 ( \hat{y}_i - y_i )”,然后这个误差乘以对应的特征值 ( x_i ),就是该样本对权重梯度的贡献。如果预测值偏大,梯度就是正的,更新时权重就会减小;如果预测值偏小,梯度是负的,权重就会增大。信息论里有个类似的说法:梯度就是“误差反传的信号”。

更新参数的公式:

[ w \leftarrow w - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_w l^{(i)} ]

其中 ( \eta ) 是学习率,表示每次走多大一步;( |\mathcal{B}| ) 是 batch 大小。从零实现 SGD 只有十行代码:

def sgd(params, lr, batch_size): with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size param.grad.zero_()

这里有个重要细节:为什么更新参数时要放在torch.no_grad()里面?因为更新参数是纯粹的数值操作,不应该被 PyTorch 计入计算图。如果不加这个,你的计算图会越滚越大,运行一段时间后内存就会爆掉。这也是我最初踩过的坑之一,等到后面聊排查技巧时再展开。

另外注意param.grad.zero_(),也就是把梯度清零。PyTorch 的梯度是累积的,不清零的话下一次反向传播会把两次梯度加在一起。对于某些特殊场景(比如梯度累积模拟大 batch)你会利用这个特性,但常规训练里每一次迭代结束都必须清零。

关于学习率 ( \eta ) 的选择,我说一个直观经验:它可能是整个训练闭环里最难调的超参数。太大,损失会震荡甚至发散;太小,训练半天损失几乎不动。李沐在课程中也多次强调学习率的重要性,建议大家在 0.01、0.05、0.1 这一档多试几个值,观察损失曲线的变化。

2.4 数据迭代:batch、epoch、learning rate 怎么配合

训练闭环里数据的喂入方式同样重要。线性回归的数据迭代在书里用一个data_iter函数实现,它的职责是:从全部数据里随机抽取batch_size个样本,形成一个 batch,然后交给模型。

这里有两个概念需要区分:epoch(迭代轮数)和 batch size(批量大小)。epoch 表示完整遍历一遍整个训练数据集;batch size 表示每次更新参数用的样本数量。举个例子,如果有 1000 个样本,batch size 设为 10,那一个 epoch 就有 100 个 batch,也就是说一个 epoch 里参数会被更新 100 次。

代码实现是这样:

def data_iter(batch_size, features, labels): num_examples = len(features) indices = list(range(num_examples)) random.shuffle(indices) for i in range(0, num_examples, batch_size): batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)]) yield features[batch_indices], labels[batch_indices]

每次迭代之前先随机打乱索引顺序,再按顺序切出 batch。为什么要打乱?如果数据本身有某种顺序结构,不打乱会导致每个 batch 都是相同类型的样本,梯度更新会产生系统性偏差。比如房价数据如果按面积从小到大排列,不打乱的话前几个 batch 全是小面积房子,模型会被带着先偏向某个方向,后面再突然见到大面积房子,训练过程就会震荡。

batch size 的取值,在后续所有深度学习任务里都是必须面对的超参数。它在训练闭环里起着微妙的平衡作用:batch 越大,梯度的估计越准,但每次更新前等待计算的时间越长,而且占用的内存越大;batch 越小,更新越快,但梯度噪声大,训练曲线会比较抖。

李沐在书里习惯把超参数集中放在代码开头,比如:

lr = 0.03 num_epochs = 3 net = linreg loss = squared_loss

这看起来只是一个组织代码的习惯,但实际工程里好的习惯是这样的:所有可调的超参数都应该收拢到一个地方,方便统一修改,而不是散落在代码各处。你后面切换到真实项目时,会感受到这种安排对于实验管理和复现有多重要。

3. 实操:一步步跑通最小训练闭环

下面这部分我把整个训练闭环用代码完整走一遍。建议你新建一个 Python 文件,一行一行敲,不要复制粘贴。敲代码的过程,其实就是你的手指在帮你记笔记——这个观点是我学完这本书后体会最深的一句话。

3.1 准备人造数据集:给训练闭环提供足够“体验”

先导入必要的库,然后生成人工数据集:

import torch import random def synthetic_data(w, b, num_examples): X = torch.normal(0, 1, (num_examples, len(w))) y = torch.matmul(X, w) + b y += torch.normal(0, 0.01, y.shape) return X, y.reshape((-1, 1)) true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 1000)

这里torch.normal(0, 1, (1000, 2))生成了 1000 个样本,每个样本有 2 个特征,特征值服从标准正态分布。真实权重是[2, -3.4],真实偏置是 4.2。然后我们加上一些均值为 0、标准差为 0.01 的噪声,模拟现实中的测量误差。

这个数据集的精巧之处在于:噪声很小,所以线性关系非常明显,模型只要结构正确、训练闭环正常运行,学到的参数就会非常接近真实值。如果训练完之后w学出来是[1.2, -2.0],那基本可以确定你的闭环某个环节写错了。这在调试阶段是极其宝贵的反馈信号。

我把真实数据和预测结果放在一起对比了一下,第一行数据长这样(每个人跑出来的随机值不同):第一个特征大约 0.8 左右,第二个特征大约 -0.5 左右,标签大约 4.2 + 20.8 - 3.4(-0.5) ≈ 7.5。当你在训练后重新算一遍预测值,会发现和标签的差距基本在 0.01 数量级,这时候你就知道训练闭环确实跑通了。

3.2 手动实现模型、损失函数和优化器

模型、损失和优化器三个组件的代码在上一节已经分别写过了,这里合在一起再说明一下它们怎么衔接。

模型的输入形状是(batch_size, num_features)。以 batch size 为 10 为例,X的形状是(10, 2),权重w是(2, 1),torch.matmul(X, w)的结果是(10, 1),加上偏置b之后输出预测形状(10, 1)。标签y在生成数据时已经做了reshape((-1, 1)),所以也是(10, 1)。两者相减得到误差,再求平方,得到每个样本的损失。这些损失后续在训练循环里会求均值。

这里有一个关于张量形状的提醒:在squared_loss里我写了y.reshape(y_hat.shape),这是因为在某些情况下y的形状可能和y_hat不一致。比如在d2l库的某些版本里,标签的形状是(batch_size, 1),但预测的形状是(batch_size, 1)或者(batch_size,),保险起见做一个 reshape 能避免很多“形状对不上”的报错。这个习惯我一直保留到之后写复杂模型。

优化器代码:

def sgd(params, lr, batch_size): with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size param.grad.zero_()

为什么里面要除以batch_size?因为我们的损失函数没做均值,每个样本贡献了一个独立的损失。反向传播后,param.grad是整个 batch 所有样本梯度的总和。如果不除以 batch size,梯度大小就会随 batch 增大而增大,学习率会变得极其难调。这个细节是李沐从零实现里“刻意”保留的,一定要看懂。

3.3 核心训练循环:建立最小训练闭环的地方

万事俱备,现在写主训练循环:

lr = 0.03 num_epochs = 3 net = linreg loss = squared_loss w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True) b = torch.zeros(1, requires_grad=True) for epoch in range(num_epochs): for X, y in data_iter(batch_size, features, labels): l = loss(net(X, w, b), y) l.sum().backward() sgd([w, b], lr, batch_size) with torch.no_grad(): train_l = loss(net(features, w, b), labels) print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')

这一段代码我建议你仔细品,因为它浓缩了整个深度学习的训练范式。外层循环是 epoch,内层循环是 batch 迭代。每个 batch 做三件事:前向计算损失、反向传播计算梯度、优化器更新参数。

第一件,前向计算损失。net(X, w, b)得到预测值,loss(...)算出每个样本的平方误差,l是一个形状为(batch_size, 1)的张量。第二件,l.sum().backward()对总损失做反向传播。为什么是 sum 而不是 mean?因为我们的squared_loss没有做均值,直接对所有样本的损失求和即可,求和不影响梯度的方向,只影响大小,后面sgd里会按 batch size 归一。第三件,sgd([w, b], lr, batch_size)用算好的梯度更新参数,并清空梯度。

每完成一个 epoch,我们用一个torch.no_grad()环境在全量数据上计算平均损失并打印。这个操作叫“评估”,目的是观察训练集整体的损失水平。为什么要包no_grad()?因为评估阶段不需要计算梯度,省内存也能避免不小心把评估的损失也接进计算图里。

如果你严格按照书里的参数跑,lr=0.03、num_epochs=3,最终打印出来的损失会一路下降,大概到 0.0001 这个量级。而此时w和b的值也会非常接近真实值。我在一次运行中得到了w = [1.9996, -3.3997],b = 4.2001。这就是整个最小训练闭环给出的结果——误差大概在千分位。你可以通过改动学习率、epoch 数来看训练曲线的不同表现,这是第一个值得做的实验。

3.4 手动实现 vs 简洁实现,两条路线怎么选

李沐这套书每个模型都给了两条路线:一种从零实现,把模型、损失、优化器都手动写出来;一种用 PyTorch 高层 API,比如nn.Linear、nn.MSELoss、optim.SGD,几行代码搞定同一个任务。我强烈建议初学者两条都要跑,但顺序上务必先手动实现,再跑简洁实现。

手动实现的好处前面已经说过:你能看到训练的全部细节。但它的缺点也很明显,就是代码冗长、容易出边界 bug,而且不能扩展到真实规模。简洁实现的好处是开发效率高、代码稳定,但如果你没有先跑过手动实现,你无法理解nn.Linear内部到底初始化了什么参数,MSELoss默认的 reduction 参数是 mean 还是 sum,SGD的 momentum 参数到底影响什么。

简洁实现的关键代码长这样:

net = nn.Sequential(nn.Linear(2, 1)) net[0].weight.data.normal_(0, 0.01) net[0].bias.data.fill_(0) loss = nn.MSELoss() trainer = torch.optim.SGD(net.parameters(), lr=0.03) for epoch in range(num_epochs): for X, y in data_iter(batch_size, features, labels): trainer.zero_grad() l = loss(net(X), y) l.backward() trainer.step()

注意这里loss(net(X), y)传入的y形状是(batch_size, 1),正好符合MSELoss的输入要求。trainer.zero_grad()等价于手动实现里sgd末尾的param.grad.zero_()。trainer.step()等价于param -= lr * param.grad / batch_size(实际上 PyTorch 里是param -= lr * param.grad,但 PyTorch 的MSELoss默认 reduction='mean',所以效果上已经做了均值)。

对比着看这两段代码,你会发现它们做的是完全同一件事。区别只是“把细节藏在了哪里”。对我来说,手动实现是“知其所以然”,简洁实现是“高效生产”,两者缺一不可。李沐在书里反复强调“不是重复造轮子,而是理解轮子怎么转”,这句话值得记下来。

4. 常见问题与排查技巧实录

线性回归虽然简单,但我自己以及身边朋友在学习时踩过的坑还真不少。有些坑在跑复杂模型时会以更隐蔽的方式再次出现,所以值得专门拉一个清单出来。

4.1 学习率怎么选:一个直接影响训练成败的参数

学习率这个参数可以说是训练闭环里最容易出问题、也最需要反复实验的。我在跑线性回归时试过几组不同的学习率,记录一下现象。

先试lr = 0.1。损失曲线会下降得非常快,第一个 epoch 结束就已经接近最优值了,但继续训练时你会发现损失在某个数值附近来回震荡,无法进一步下降。这其实就是学习率偏大、参数在最优解附近来回“跨步”导致的。典型特征是损失曲线不再平滑下降,而是在一个区间里反复横跳。

再试lr = 0.5。这一般会直接让训练发散。损失不但不降,反而越滚越大,甚至打印出来是nan。因为学习率过大,每一步的梯度更新都会跨过最优解,甚至跳到损失更大的区域,下一个梯度反而更大,产生正反馈爆炸。

试lr = 0.0001。损失下降非常缓慢。跑了几个 epoch 之后,损失可能还有 3 左右,而正常情况应该在 0.0001 量级。这不是模型错了,是学习率太小、走得太慢。你需要增加训练轮数(epoch)来弥补,但在真正调参时没有人愿意等那么久。

我的经验是,从一个中等量级开始,观察损失曲线再调整方向。比如从 0.03 开始,如果损失下降平滑,说明学习率可以往大了试探;如果损失震荡,就往小了调。每次调整幅度建议乘以或除以 3 到 10,不要小步挪。这个“指数级试探”的方法在后续调 Adam、调 warmup 时同样适用。

4.2 归一化对训练的影响

我在用真实数据跑线性回归时发现一个问题:如果某个特征的数值范围远大于其他特征,比如房价数据里“面积”可能是 50 到 200,而“房间数”只有 1 到 10,那么梯度更新时占主导的永远是面积这个特征。结果就是模型对大面积特征的权重学得很快,对小范围特征不敏感。这里就涉及特征归一化(Feature Scaling)的必要性。

标准做法是把每个特征缩放到均值为 0、方差为 1。代码很简单:

X_norm = (X - X.mean(dim=0)) / X.std(dim=0)

归一化之后,所有特征的尺度一致,梯度更新时会平等对待每个特征,训练速度和稳定性都会大幅提升。这个操作在后续所有深度学习任务里几乎都是标配。李沐的人工数据集里所有特征都已经服从标准正态分布,所以教材没有刻意强调这一步,但到了真实项目里,这是绕不开的一步。

另外,标签是否要进行归一化,取决于任务。对于回归任务,如果标签的绝对值很大,损失会很大,梯度也会偏大,可能导致数值不稳定。我一般会先看标签的分布,如果跨度超过几个数量级,就做一次标准化,等到预测输出时再反标准化回来。

4.3 一个更隐蔽的坑:忘记清空梯度

手动实现里,sgd函数最后一行写了param.grad.zero_(),简洁实现里也调了trainer.zero_grad()。如果你把这一行去掉,会发生什么?每个 batch 反向传播时计算的梯度会和前一个 batch 的梯度累加,导致参数更新方向严重偏离真实的当前梯度方向。损失曲线表现为下降一会儿又反弹,完全没有规律。

这个坑在你以后用 PyTorch 写自定义训练循环时非常容易踩到。PyTorch 设计成梯度累积模式是为了服务某些特殊方法(比如梯度积累模拟大 batch、GAN 里先更新判别器再更新生成器等),但默认情况下,你必须记得在每个 batch 开始前清空梯度。我的习惯是统一在训练循环的第一步写optimizer.zero_grad(),这样就算后续代码逻辑变了,也不容易漏。

4.4 从线性回归到真实项目的迁移清单

如果看完这篇文章,你决定用同样的思路去跑一个真实回归项目,我建议你先做这几件事。

第一,数据准备阶段先画图或者做分布统计,看看特征和标签之间大致是什么关系。如果关系明显不是线性的,不要指望线性回归出很好效果,试试加多项式特征或者直接换更复杂模型。第二,按 7:2:1 或者 8:1:1 划分训练集、验证集、测试集。训练集用于更新参数,验证集用于评估和调参,测试集只在最终报告结果时用一次。很多初学者把测试集反复用来调参,这会导致“模型对测试集过拟合”而不自知。第三,保存模型参数和训练曲线。就算是最简单的线性回归,有了训练过程的详细记录,你才能复现实验结果,也才能对比不同超参数的影响。李沐在课程里也推荐使用torch.save(net.state_dict(), 'model.weights')保存模型参数。

我还想分享一个小技巧:从线性回归开始,我养成了在训练循环里打印损失时同时打印w和b当前值的习惯。对于合成数据,你可以实时对照真实权重,直观感受训练过程。这个“看着参数逼近真值”的体验非常上瘾,也是你对训练闭环建立直觉的最快方式。

另外,李沐在每一章的配套视频里都有 Jupyter Notebook 代码,建议你阅读文章时把 notebook 打开对照。这本书最有价值的其实是“代码旁白”——就是那些写在 Markdown 单元格里看似随意的注释和提问,它们指向的全是新手最容易犯糊涂的地方。

最后再讲一个很多人没注意到的点:为什么全书第一件事是跑d2l库的安装,而不是直接写模型?因为深度学习项目里“环境”本身就是一个巨大的门槛。如果你跟着这本书走,建议先花半天时间把 PyTorch 环境配好,再用 CPU 跑通前几个模型,完全没问题。等到后面要跑大规模数据或者复杂网络,再考虑 GPU 也不迟。一个最小训练闭环,最先要闭环的其实是“从零到能跑起代码”这个过程本身。这一步迈过去了,后面的路会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询