一、整体项目流程总览

整套线性回归分为 6 大模块,对应代码分段:

  1. 生成仿真数据集(带高斯噪声的标准线性数据 y=Xw+b+ϵ)
  2. 自定义随机批量数据迭代器(替代torch.utils.data.DataLoader
  3. 初始化待训练权重 w、偏置 b,开启梯度追踪
  4. 手写前向传播线性模型、均方损失函数
  5. 手写 SGD 小批量随机梯度下降优化器(参数更新 + 梯度清零)
  6. 完整训练循环,多轮迭代更新参数,输出损失与拟合误差

二、完整可运行代码

import random
import torch
from d2l import torch as d2l

#生成数据集

def synthetic_data(w, b, num_examples):  #@save
    """生成y=Xw+b+噪声"""
    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))#返回x为1000个样本,每个样本有2个特征(1000×2),满足正态分布。y的话就是要reshape成1000×1的列向量

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
print('features:', features[0],'\nlabel:', labels[0])
#d2L画图
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1)
d2l.plt.show()


def data_iter(batch_size, features, labels):
    """
    自定义数据集批量迭代器,用于分批次随机读取训练数据
    :batch_size: 每一批次的样本数量(批量大小)
    :features: 全部样本的特征张量,shape=[样本总数, 特征维度]
    :labels: 全部样本的标签张量,shape=[样本总数, 输出维度] 或 [样本总数,]
    :yield: 每次循环返回一批次 (批次特征, 批次标签),生成器惰性加载,节省内存
    """
    # 获取整个数据集的样本总数量为1000
    num_examples = len(features)
    
    # 生成 0~999  的索引列表,每个数字代表一个样本的下标
    indices = list(range(num_examples))
    
    # 随机打乱索引顺序,实现样本随机采样(打乱训练集,避免模型学到样本顺序规律)
    random.shuffle(indices)
    
    # 从0开始,以batch_size为步长遍历所有索引,切割出每一批的索引区间
    # range(start, end, step):i = 0, batch_size, 2*batch_size ...
    for i in range(0, num_examples, batch_size):
        # 截取当前批次对应的索引:indices[i : i+batch_size]
        # min(i + batch_size, num_examples) 防止最后一批不足batch_size时下标越界
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)]
        )
        # 根据批次索引切片取出对应特征和标签,通过yield返回(生成器)
        # 每次调用迭代器只加载一批数据,不会一次性加载全部批次,内存友好 
        yield features[batch_indices], labels[batch_indices]

batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break
#初始化参数
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
#定义模型
def linreg(X, w, b):  #@save
    """线性回归模型"""
    return torch.matmul(X, w) + b
#定义损失函数
def squared_loss(y_hat, y):  #@save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
#定义优化算法:只做参数更新+梯度清零
def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():#临时关闭张量自动求导计算图
        for param in params:
            param -= lr * param.grad / batch_size#更新w的公式
            param.grad.zero_()#梯度清0

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

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        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}')

print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')

三、分模块深度解析

3.1 仿真数据集生成模块

现实中我们很难拿到完全干净的线性数据,因此手动构造带噪声数据模拟真实场景:

  1. torch.normal(0,1)生成标准正态分布特征,保证特征取值均匀;
  2. 基础线性公式 y=Xw+b 构建理想真值;
  3. 叠加微小正态噪声torch.normal(0,0.01),模拟采集数据的测量误差;
  4. 标签y统一 reshape 为列向量,避免后续矩阵运算维度不匹配报错。

可视化部分:我们取第二个特征和标签绘制散点图,能直观看到清晰的线性正 / 负相关趋势,验证数据集构造无误。

3.2 手动批量迭代器(核心面试考点)

PyTorch 高层 APIDataLoader内部逻辑和这里完全一致,核心逻辑 3 步:

  1. 生成全部样本索引,用random.shuffle打乱顺序 ——避免模型记住样本排列顺序,造成过拟合
  2. batch_size分段切分索引,通过索引切片取出对应数据;
  3. 使用yield生成器惰性输出,不会一次性把全部数据存入内存,大数据集下内存占用极低。

小细节:min(i + batch_size, num_examples)用来兜底最后一批不足batch_size的样本,防止索引越界。

3.3 参数初始化与梯度追踪

  1. 权重w使用极小正态分布初始化:全部置 0 会导致梯度对称、收敛变慢,小随机值打破对称性;
  2. requires_grad=True:开启张量的自动求导记录,PyTorch 会跟踪该张量所有运算构建计算图;
  3. 偏置b初始化为 0 是线性回归通用做法。

3.4 前向模型 & 均方损失

  1. 线性模型linreg仅做矩阵乘法 + 偏置加法,是最简单的单层神经网络;
  2. 均方损失除以 2 是数学技巧:求导后平方项的 2 会抵消,简化梯度计算,不影响最优解。

3.5 手写 SGD 优化器(深度学习底层核心)

这是本文最关键的部分,全程不调用torch.optim.SGD,手动实现参数更新:

  1. with torch.no_grad():参数更新属于权重赋值,不需要记录梯度,关闭计算图节省显存;
  2. 更新公式:w=w−η⋅batch_size∇L​,梯度除以批次大小得到平均梯度;
  3. param.grad.zero_():梯度会默认累加,每轮更新后必须清零,否则梯度累积导致更新错误。

3.6 完整训练循环逻辑

  1. 外层epoch:一轮 epoch 代表完整遍历一次全部 1000 条样本;
  2. 内层循环:遍历所有小批量,前向计算损失 → backward()反向传播求梯度 → SGD 更新参数;
  3. 每轮 epoch 结束后,在全量数据上计算平均损失,直观观察损失持续下降;
  4. 训练结束对比预测w、b与预设真实值,误差极小代表模型拟合成功。

四、运行结果解读

  1. 损失输出:3 轮训练损失持续下降,说明 SGD 梯度下降有效收敛;
  2. 参数误差:输出true_w - wtrue_b - b数值非常接近 0,模型学习到了我们预设的真实线性关系 y=2x1​−3.4x2​+4.2;
  3. 散点图:特征与标签呈明显线性分布,验证数据集构造合理。

五、常见易错点总结(踩坑指南)

  1. 维度不匹配:标签 y 必须 reshape 为列向量,否则(y_hat - y)会触发广播错误;
  2. 梯度不清零:忘记param.grad.zero_()会导致梯度叠加,参数永远无法收敛;
  3. no_grad 缺失:参数更新时不关闭自动求导,会持续构建冗余计算图,显存爆炸;
  4. 梯度不除以 batch_size:使用批次总梯度更新,步长过大,训练震荡不收敛;
  5. 不打乱数据索引:样本顺序固定,模型会学习顺序特征,泛化能力变差。

完整训练流程:人工构造带噪声的训练数据集 → 手动实现随机批量迭代器 → 初始化权重偏置 → 手写线性前向模型、均方损失、SGD 优化器 → 完整训练循环,最后对比预测参数与真实参数。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐