线性回归的从零开始实现(详解部分疑问)
- 前向传播和反向传播的不同?
前向传播 | 反向传播 |
---|---|
计算预测值 | 计算梯度 |
从输入到输出 | 从输出到输入 |
使用当前参数值 | 更新参数值 |
基于数据和模型结构 | 基于链式法则 |
- param.grad
取参数的梯度(由 loss.backward() 计算得到表示参数的梯度(Gradient),即损失函数关于该参数的偏导数。PyTorch 自动计算复杂函数的梯度,无需手动推导公式,理解 param.grad 是掌握深度学习优化的基础,它是连接前向传播和反向传播的桥梁。通过梯度,模型能够自动调整参数以最小化损失函数。 - l 的直接作用?
l 本身不需要在其他地方被使用,因为它的使命是触发梯度计算一旦 backward() 被调用,l 的历史计算路径就被用于计算梯度,之后 l 可以被丢弃 - 参数更新依赖的是梯度,而非 l?
SGD 优化器直接使用 w.grad 和 b.grad 来更新参数
l 的角色已经完成,不再需要 - 如果没有 l 会怎样?
如果没有 l,就无法计算损失,进而无法触发反向传播。没有反向传播,就无法得到梯度 w.grad 和 b.grad,没有梯度,参数 w 和 b 就无法更新,模型无法学习 - 什么需要 l.sum()?
PyTorch 的 backward() 要求输入是标量(scalar),通过 sum() 将批量损失(形状:(batch_size, 1))转换为标量。等价于计算批次内所有样本的平均损失:l.mean().backward(),但需调整学习率 - 可视化 l 的作用流程
输入数据 X 和标签 y↓
前向传播:net(X, w, b) → 预测值 ŷ↓
计算损失:l = loss(ŷ, y) ← l 在此被创建↓
反向传播:l.sum().backward() → 计算梯度 dw, db↓
参数更新:w ← w - lr·dw, b ← b - lr·db ← 使用梯度,而非 l
完整代码:
"""
文件名: 3.2 线性回归的从零开始实现
作者: 墨尘
日期: 2025/7/11
项目名: dl_env
"""import random
import matplotlib.pyplot as plt
import numpy as np
import torch
from d2l import torch as d2l
import platform# 根据操作系统自动选择字体
system = platform.system()
if system == 'Windows':plt.rcParams["font.family"] = ["SimHei", "Microsoft YaHei"]
elif system == 'Linux':plt.rcParams["font.family"] = ["WenQuanYi Micro Hei", "Heiti TC"]
elif system == 'Darwin': # macOSplt.rcParams["font.family"] = ["Heiti TC", "SimHei"]
else:plt.rcParams["font.family"] = ["SimHei"] # 默认plt.rcParams["axes.unicode_minus"] = False # 使用ASCII减号# 设置随机种子以确保结果可重复
# torch.manual_seed(42)
# random.seed(42)"""生成y=Xw+b+噪声"""# 参数:
# w: 真实权重向量(如[2, -3.4])
# b: 真实偏置(如4.2)
# num_examples: 样本数量(如1000)
# 功能:生成符合线性关系y = Xw + b的数据集,并添加随机噪声。
def synthetic_data(w, b, num_examples): # @save# 使用torch.normal生成服从标准正态分布(均值0,标准差1)的随机张量# 形状为(num_examples, len(w)),例如1000个样本,每个样本有n个特征# 对应线性回归中的输入矩阵XX = torch.normal(0, 1, (num_examples, len(w)))# torch.matmul(X, w): 矩阵乘法,计算Xw# + b: 添加偏置项# 得到理论上的真实标签y = Xw + by = torch.matmul(X, w) + b# 再次使用torch.normal生成噪声(均值0,标准差0.01)# 噪声形状与y相同,逐元素相加# 模拟真实数据中的观测误差y += torch.normal(0, 0.01, y.shape)# X: 特征矩阵(形状:(num_examples, len(w)))# y.reshape((-1, 1)): 将标签重塑为列向量(形状:(num_examples, 1))return X, y.reshape((-1, 1))# 定义一个data_iter函数,
# 该函数接收批量大小、特征矩阵和标签向量作为输入,
# 生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):# 生成大小为batch_size的小批量数据,用于随机梯度下降训练。# 参数:# - batch_size: 每个小批量的样本数量# - features: 特征矩阵(形状:[样本数, 特征数])# - labels: 标签向量(形状:[样本数, 1])# 返回:# - 每次迭代返回一个元组(features_batch, labels_batch)# num_examples:获取总样本数(如 1000)num_examples = len(features)# indices:生成从 0 到num_examples-1的索引列表# 例如:[0, 1, 2, ..., 999]indices = list(range(num_examples))# 这些样本是随机读取的,没有特定的顺序# 实现随机梯度下降(SGD),避免模型学习数据顺序的偏见random.shuffle(indices)# 按批次遍历索引# 循环逻辑:# 从 0 开始,每次递增batch_size,直到覆盖所有样本# 例如:batch_size=2时,i取值为0, 2, 4, ...for i in range(0, num_examples, batch_size):# 截取当前批次的索引并转换为张量# 切片逻辑:# indices[i: i+batch_size]:截取当前批次的索引# min(i + batch_size, num_examples):防止最后一批越界# 转换为张量:# 将 Python 列表转换为 PyTorch 张量,用于高效索引batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])# 关键操作:# 使用 PyTorch 的高级索引功能,通过索引张量一次性获取批量数据# 返回值:# 每次迭代返回一个元组(特征批次, 标签批次)# 生成器(yield):# 避免一次性加载所有数据,节省内存(每次只生成一个批次)yield features[batch_indices], labels[batch_indices]# 定义模型
def linreg(X, w, b): # @save"""线性回归模型"""return torch.matmul(X, w) + b# 定义损失函数
def squared_loss(y_hat, y): # @save"""均方损失"""# 输入参数:# y_hat:模型预测值(形状:[批量大小, 1])# y:真实标签(形状:[批量大小, 1])# 核心操作:# y.reshape(y_hat.shape):确保 y 与 y_hat 形状一致(处理可能的维度不匹配)# (y_hat - y)**2:计算预测值与真实值的平方差# / 2:除以 2 简化后续梯度计算return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2# 定义优化算法
# 深度学习中最基础的优化算法 ——小批量随机梯度下降(Mini-batch Stochastic Gradient Descent, SGD)
def sgd(params, lr, batch_size): # @save"""小批量随机梯度下降"""# with torch.no_grad()# 作用:临时禁用梯度计算,节省内存并加速计算# 原因:参数更新过程不需要反向传播with torch.no_grad(): # 不计算梯度,提高效率for param in params: # 遍历所有参数(如 w 和 b)# 核心更新逻辑:# param.grad:获取参数的梯度(由 loss.backward() 计算得到) 表示参数的梯度(Gradient),即损失函数关于该参数的偏导数。# PyTorch 自动计算复杂函数的梯度,无需手动推导公式# 理解 param.grad 是掌握深度学习优化的基础,它是连接前向传播和反向传播的桥梁。# 通过梯度,模型能够自动调整参数以最小化损失函数。# / batch_size:平均梯度(PyTorch 的 backward() 默认返回梯度总和)# lr * ...:乘以学习率,控制步长# param -= ...:原地更新参数param -= lr * param.grad / batch_size # 参数更新param.grad.zero_() # 梯度清零,防止累积if __name__ == '__main__':# true_w: 真实权重向量,表示每个特征的系数# true_b: 真实偏置项# features: 生成的1000个样本的特征矩阵(形状:(1000, 2))# labels: 对应的标签向量(形状:(1000, 1))true_w = torch.tensor([2, -3.4]) # 两个特征的权重true_b = 4.2features, labels = synthetic_data(true_w, true_b, 1000)# 打印第一个样本的特征和标签print('features:', features[0], '\nlabel:', labels[0])# 可视化第二个特征(索引为1)与标签的关系# 通过生成第二个特征features[:, 1]和labels的散点图,# 可以直观观察到两者之间的线性关系d2l.set_figsize()plt = d2l.pltplt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1)plt.title('特征与标签的关系')plt.xlabel('第二个特征 (x1)')plt.ylabel('标签 (y)')# 理论直线(用于参考)x_line = np.linspace(-3, 3, 100)y_line = true_w[1].item() * x_line + true_bplt.plot(x_line, y_line, 'r-', label=f'理论直线: y = {-3.4:.1f}x + {4.2:.1f}')plt.legend()plt.show()batch_size = 10for X, y in data_iter(batch_size, features, labels):print(X, '\n', y)break# 均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)b = torch.zeros(1, requires_grad=True)lr = 0.03num_epochs = 3net = linregloss = squared_loss# 外层循环:控制训练轮数for epoch in range(num_epochs):# 内层循环:遍历批次数据for X, y in data_iter(batch_size, features, labels):"""l 的直接作用l 本身不需要在其他地方被使用,因为它的使命是触发梯度计算
一旦 backward() 被调用,l 的历史计算路径就被用于计算梯度,之后 l 可以被丢弃""""""如果没有 l 会怎样?如果没有 l,就无法计算损失,进而无法触发反向传播
没有反向传播,就无法得到梯度 w.grad 和 b.grad
没有梯度,参数 w 和 b 就无法更新,模型无法学习"""# 前向传播:计算预测值和损失l = loss(net(X, w, b), y) # X和y的小批量损失# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,# 并以此计算关于[w,b]的梯度# 反向传播:计算梯度"""什么需要 l.sum()?PyTorch 的 backward() 要求输入是标量(scalar)通过 sum() 将批量损失(形状:(batch_size, 1))转换为标量等价于计算批次内所有样本的平均损失:l.mean().backward(),但需调整学习率"""l.sum().backward()# 参数更新:使用优化器更新参数sgd([w, b], lr, batch_size)# 每个 epoch 结束后打印损失(缩进正确)with torch.no_grad():train_l = loss(net(features, w, b), labels)print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')# 循环结束后只打印参数误差print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')print(f'b的估计误差: {true_b - b}')