☰
从梯度到大模型(008):从手写 w、b 到神经网络,讲清 nn.Module、Loss 与 Optimizer
2026/9/28 18:44:41 网站建设 项目流程

从梯度到大模型(008):从手写 w、b 到神经网络,讲清 nn.Module、Loss 与 Optimizer

课程:《从梯度到大模型:LLM 系统实践课》
本课预计用时:90~120 分钟,可分两次完成
运行环境:Python 3、PyTorch,CPU 即可
前置课程:第 6 课自动求导、第 7 课 Dataset 与 DataLoader

在上一课,我们已经能把数据分成小批次,再一批一批地训练:

# 摘出一个批次的关键步骤;参数已在循环外创建。w.grad=Noneb.grad=Noneprediction=xb @ w+b loss=((prediction-yb)**2).mean()loss.backward()withtorch.no_grad():w-=learning_rate*w.grad b-=learning_rate*b.grad

这段代码里,预测、求导、更新参数的过程都很清楚。

但当模型从两个参数变成几十个、几万个参数时,我们会遇到新的问题:参数放在哪里?怎样找到需要更新的参数?每一层的计算又怎样组织?

本课就从我们熟悉的工时预测开始,把这段代码逐步改写成 PyTorch 常见的训练方式。

学习时先记住三个分工:

组件本课中负责什么对应的问题
nn.Module组织模型的参数和前向计算怎样得到预测值?
Loss 函数比较预测与目标模型整体错了多少?
Optimizer使用已有梯度更新参数下一步怎样调整?

它们共同完成训练,但各自只负责其中一部分。后面我们会用同一组数字,把每个步骤算出来。

这篇文章怎样学?

不必一口气记住所有新名字,可以分两次完成:

  • 第一次读第二至十节:把手写的线性模型改成 PyTorch 写法,并手算一次更新。读完应能回答“梯度是谁算的,参数又是谁改的”。
  • 第二次读第十一至十五节,再阅读第十六节完整代码,按第十七节运行:看清两层网络的形状、参数数量和训练过程。
  • 第十九节是分类预习,完成回归实验后再读;最后独立完成三道课后作业,答案留到下一课。

第一节保留上节作业解析,方便连载读者核对。只想学习本课新内容,可以从第二节开始。

正文中的短代码用于解释局部步骤,不能从第一段到最后一段直接拼接运行。需要执行时,请使用第十六节的完整demo.py。文中X表示整组输入,xb表示一批输入;y、target都表示目标值,yb是一批目标值。

一、上节作业答案与讲解

如果你还没完成第 7 课作业,可以先跳到第二节,做完后再回来核对。

作业 1:11 条样本,batch_size=4,会分成几批?

先不丢弃最后一批:

第 1 批:4 条 第 2 批:4 条 第 3 批:3 条 合计:11 条

因此:

drop_last=False# 共3批,本课每批更新一次,所以每轮更新3次。

如果设置:

drop_last=True

最后不足 4 条的那批会被丢弃:

第 1 批:4 条 第 2 批:4 条 合计:8 条

每轮只更新 2 次,另外 3 条不参加这一轮的训练。

注意,“一个 epoch”表示遍历一轮训练数据;是否丢弃尾批会影响实际参与计算的样本数。

作业 2:单样本和批次的形状

已知:

X.shape = [11, 2] y.shape = [11, 1]

Dataset 返回:

returnX[index],y[index]

整数索引会去掉最前面的样本维度:

一条样本: X[index].shape = [2] y[index].shape = [1]

DataLoader 把 4 条样本放在一起后:

第一批: xb.shape = [4, 2] yb.shape = [4, 1]

最后只有 3 条:

最后一批: xb.shape = [3, 2] yb.shape = [3, 1]

第二个维度没有变:每条任务仍有两个特征,仍对应一个标签。变化的是这一批有多少条任务。

作业 3:自己创建两特征数据集

一种可用的数据是:

importtorch X=torch.tensor([[1.0,0.0],[0.0,2.0],[2.0,2.0],[3.0,1.0],[1.0,4.0],[4.0,0.0],])y=2*X[:,0:1]+0.5*X[:,1:2]+1

这里特意使用0:1和1:2,让取出的特征列保持[6,1]。计算后的 y 也是[6,1]。

各条标签为:

[3.0, 2.0, 6.0, 7.5, 5.0, 9.0]

索引 2 对应第三条数据[2,2]:

y = 2×2 + 0.5×2 + 1 = 6

接着前面的 X、y,实现题目要求的 Dataset,并打印全部批次:

fromtorch.utils.dataimportDataLoader,DatasetclassTaskDataset(Dataset):def__init__(self,features,labels):self.features=features self.labels=labelsdef__len__(self):returnlen(self.features)def__getitem__(self,index):returnself.features[index],self.labels[index]dataset=TaskDataset(X,y)print("样本数:",len(dataset))print("索引2:",dataset[2])loader=DataLoader(dataset,batch_size=2,shuffle=False)forbatch_index,(xb,yb)inenumerate(loader,start=1):print(batch_index,xb.tolist(),yb.tolist())

__len__告诉加载器共有多少条数据,__getitem__保证特征和标签按同一个索引配对。这里没有打乱,三批依次来自索引 0、1;2、3;4、5。每批特征[2,2],标签[2,1]。

作业 4:为什么固定种子,每一轮顺序仍可能不同?

种子确定的是随机数生成器的起点。

假设第一次运行从种子 123 开始:

起点123 → 第1轮排列 → 第2轮排列

第二次从同一种子开始,并执行相同流程:

起点123 → 对应的第1轮排列 → 对应的第2轮排列

我们比较的是“两次运行中的对应轮次”,而不是要求“同一次运行的两轮完全一样”。

正确做法是创建一次 generator 和 loader,在多轮之间复用。不要为了“固定随机数”,在每个 epoch 开头重新初始化成相同种子。

用作业 3 的 dataset 核对,下面每次调用函数只创建一次 loader,然后连续遍历两轮:

defcollect_two_epochs(seed):generator=torch.Generator().manual_seed(seed)loader=DataLoader(dataset,batch_size=2,shuffle=True,generator=generator,num_workers=0,)orders=[]for_inrange(2):order=[]forxb,_inloader:order.extend(xb.tolist())orders.append(order)returnorders first_run=collect_two_epochs(123)second_run=collect_two_epochs(123)forepochinrange(2):print("轮次:",epoch+1)print("第一次:",first_run[epoch])print("第二次:",second_run[epoch])print("对应轮次一致:",first_run[epoch]==second_run[epoch])assertfirst_run==second_run

这里每条特征都不同,可以用特征行辨认顺序。在同一环境、相同代码路径下,两次的对应轮次应一致;不要再加“两轮必须不同”的断言,因为不同随机抽取也可能偶然得到相同排列。具体排列以运行结果为准。

作业 5:整体 MSE 应该怎样算?

三批分别有 4、4、1 条样本,批次 MSE 分别为 2、4、10。

先还原每批的平方误差总和:

第1批:4×2 = 8 第2批:4×4 = 16 第3批:1×10 = 10

再除以全部样本数:

整体 MSE = (8+16+10)/(4+4+1) = 34/9 ≈ 3.7778

如果直接算:

(2+4+10)/3 ≈ 5.3333

就把只有一条样本的最后一批,与有四条样本的批次赋予了相同权重。

这里每条样本只有一个输出,所以按样本数加权即可。若要称为“某一组模型参数的整体 MSE”,这三批必须由同一组固定参数计算;如果批次之间更新了参数,这只是训练过程中的误差汇总。第十七节会用程序中的两个指标说明这一区别。

作业 6:参数初始化和清梯度应该放在哪里?

上一课错误代码有两个问题:

第一,把参数放在 epoch 内创建,会每轮重新开始:

forepochinrange(5):w=torch.zeros((2,1),requires_grad=True)

第二,只在 epoch 开头清梯度,会让不同批次的梯度累积到一起。

正确结构为:

# 只初始化一次w=torch.zeros((2,1),requires_grad=True)b=torch.zeros(1,requires_grad=True)forepochinrange(5):forxb,ybintrain_loader:# 每批重新计算自己的梯度w.grad=Noneb.grad=

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

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

立即咨询