线性回归的从零开始实现(详解部分疑问)

  1. 前向传播和反向传播的不同?
前向传播反向传播
计算预测值计算梯度
从输入到输出从输出到输入
使用当前参数值更新参数值
基于数据和模型结构基于链式法则
  1. param.grad
    取参数的梯度(由 loss.backward() 计算得到表示参数的梯度(Gradient),即损失函数关于该参数的偏导数。PyTorch 自动计算复杂函数的梯度,无需手动推导公式,理解 param.grad 是掌握深度学习优化的基础,它是连接前向传播和反向传播的桥梁。通过梯度,模型能够自动调整参数以最小化损失函数。
  2. l 的直接作用?
    l 本身不需要在其他地方被使用,因为它的使命是触发梯度计算一旦 backward() 被调用,l 的历史计算路径就被用于计算梯度,之后 l 可以被丢弃
  3. 参数更新依赖的是梯度,而非 l?
    SGD 优化器直接使用 w.grad 和 b.grad 来更新参数
    l 的角色已经完成,不再需要
  4. 如果没有 l 会怎样?
    如果没有 l,就无法计算损失,进而无法触发反向传播。没有反向传播,就无法得到梯度 w.grad 和 b.grad,没有梯度,参数 w 和 b 就无法更新,模型无法学习
  5. 什么需要 l.sum()?
    PyTorch 的 backward() 要求输入是标量(scalar),通过 sum() 将批量损失(形状:(batch_size, 1))转换为标量。等价于计算批次内所有样本的平均损失:l.mean().backward(),但需调整学习率
  6. 可视化 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':  # macOS
    plt.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个特征
    # 对应线性回归中的输入矩阵X
    X = torch.normal(0, 1, (num_examples, len(w)))

    # torch.matmul(X, w): 矩阵乘法,计算Xw
    # + b: 添加偏置项
    # 得到理论上的真实标签y = Xw + b
    y = 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.2
    features, labels = synthetic_data(true_w, true_b, 1000)

    # 打印第一个样本的特征和标签
    print('features:', features[0], '\nlabel:', labels[0])

    # 可视化第二个特征(索引为1)与标签的关系
    # 通过生成第二个特征features[:, 1]和labels的散点图,
    # 可以直观观察到两者之间的线性关系
    d2l.set_figsize()
    plt = d2l.plt
    plt.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_b
    plt.plot(x_line, y_line, 'r-', label=f'理论直线: y = {-3.4:.1f}x + {4.2:.1f}')
    plt.legend()

    plt.show()

    batch_size = 10

    for 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.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 的直接作用
                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}')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值