线性回归练习代码解读

part.1 函数定义 create_data()

1
2
3
4
5
6
7
8
def create_data(w, b, data_num):
x = torch.normal(0, 1, (data_num, len(w)))
y = torch.matmul(x, w) + b

noise = torch.normal(0, 0.01, y.shape)
y += noise

return x, y

定义了一个create_data函数,用于生成数据。

torch.normal: docs

torch.matmul: Matrix product of two tensors.

x : 以0为均值,1为标准差,【(data_num)*(w长度)】的tensor数据
y : x*w + b 并在此基础上附加了noise扰动

part.2 变量赋值

1
2
3
4
5
6
num = 500  

true_w = torch.tensor([10.0, 7.0, 5.0, 2.0])
true_b = torch.tensor(1.1)

X, Y = create_data(true_w, true_b, num)

定义了本实验的真实值和实验规模,并调用create_data函数生成了实验数据。
true_w: 4*1 true_b: 1
X: 500*4 Y:500*1

part.3 main函数

跳过函数定义看代码主体部分:

1
2
3
4
5
6
lr = 0.05  

w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)
b_0 = torch.tensor(0.01, requires_grad=True)

epochs = 50

lr: 超参数,学习率
w_0, b_0:初始值
requires_grad = True:A tensor can be created with requires_grad=True so that torch.autograd records operations on them for automatic differentiation.
Each tensor has an associated torch.Storage, which holds its data.
epochs: 定义了执行梯度下降算法的轮数

1
2
3
4
5
6
7
8
9
10
11
12
13
for epoch in range(epochs):  
data_loss = 0
for batch_x, batch_y in data_provider(X, Y, batchsize):
pred_y = fun(batch_x, w_0, b_0)
loss = maeLoss(pred_y, batch_y)
loss.backward()
sgd([w_0, b_0], lr)
data_loss += loss

print("epoch %03d: loss: %.6f"%(epoch, data_loss))

print("真实的函数值是", true_w, true_b)
print("深度学习得到的函数值是", w_0, b_0)

from 66 to 78
data_loss变量:统计每一轮深度学习的效果
torch.backward():深度学习python库
当调用 loss.backward() 时:

  • 系统会计算损失值对 w_0每个元素 的偏导数
  • 最终 w_0.grad 也会是一个形状为 (4,) 的张量

part.4 函数定义

data_provider()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def data_provider(data, label, batchsize):  
length = len(label)
indices = list(range(length))
random.shuffle(indices)

for each in range(0, length, batchsize):
get_indices = indices[each: each + batchsize]
get_data = data[get_indices]
get_label = label[get_indices]

yield get_data, get_label


batchsize = 16

from 26 to 36
以下是 data_provider() 函数的逐行代码解读:


函数定义

1
def data_provider(data, label, batchsize):
  • 输入参数
    • data: 特征数据张量(形状通常为 [样本数, 特征维度]
    • label: 标签数据张量(形状为 [样本数]
    • batchsize: 每个批次的样本数量
  • 功能:生成随机小批量(mini-batch)数据

步骤分解

1. 获取数据集长度

1
length = len(label)  # 获取总样本数(假设数据与标签一一对应)
  • 关键作用:确定需要处理的总样本数量
  • 潜在风险:如果 datalabel 长度不一致会引发错误,但代码未做检查

2. 创建索引列表

1
indices = list(range(length))  # 生成顺序索引 [0, 1, 2, ..., length-1]
  • 示例:当 length=500 时,生成 [0,1,2,...,499]
  • 目的:为后续随机采样做准备

3. 随机打乱索引

1
random.shuffle(indices)  # 原地打乱索引顺序
  • 重要性:
    • 破坏数据原始顺序,防止模型学习到顺序特征
    • 每个epoch会生成不同的批次组合
  • 示例:可能变为 [253, 12, 487, ..., 76]

4. 批次循环生成

1
for each in range(0, length, batchsize):
  • 循环机制
    • each 从0开始,以 batchsize 为步长递增
    • 最后一个批次可能小于 batchsize(例如总样本500,batchsize=16时,最后一批次有4个样本)
  • 可视化
    1
    2
    3
    4
    batch1: 0-15
    batch2: 16-31
    ...
    batch31: 496-500

5. 获取当前批次索引

1
get_indices = indices[each: each + batchsize]  # 切片获取当前批次索引
  • 示例:当 each=32, batchsize=16 时,获取索引 indices[32:48]
  • 注意:对列表进行切片时,超出范围不会报错(自动取到列表末尾)

6. 提取批次数据

1
2
get_data = data[get_indices]    # 按索引提取特征数据
get_label = label[get_indices] # 按索引提取对应标签
  • 张量索引特性
    • 支持通过索引列表进行高级索引(advanced indexing)
    • 要求 datalabel 的第一个维度必须与 length 一致
  • 输出形状
    • get_data: [当前批次大小, 特征维度]
    • get_label: [当前批次大小]

7. 生成数据批次

1
yield get_data, get_label  # 返回生成器对象
  • 生成器优势
    • 惰性加载:不会一次性将所有批次加载到内存
    • 内存效率:适合处理大型数据集
  • 使用场景
    1
    2
    3
    # 在训练循环中使用
    for batch_data, batch_label in data_provider(X, Y, 16):
    # 执行训练步骤

关键特性总结

特性 说明 重要性
随机打乱 每个epoch重新洗牌数据顺序 防止模型记住样本顺序
动态批次生成 使用生成器逐批产生数据 节省内存,支持大数据集
不完整批次处理 自动处理末尾不完整批次 保证数据利用率100%
通用索引机制 适用于任何支持高级索引的数据结构 兼容NumPy/PyTorch等张量

执行流程示意图

1
2
3
4
5
6
7
8
9
10
原始数据

├─ 创建顺序索引 [0,1,2,...]

└─ 随机洗牌 → [253,12,487,...]

├─ 切片[0:16] → batch1
├─ 切片[16:32] → batch2

└─ ... → 直到遍历所有数据

该函数实现了深度学习训练中最基础且重要的 随机小批量采样 功能,是确保模型有效训练的关键组件。

fun()函数

1
2
3
def fun(x, w, b):  
pred_y = torch.matmul(x, w) + b
return pred_y

根据给定的参数w, b对数据x生成预测值y并返回预测值pred_y

maeLoss()函数

1
2
def maeLoss(pred_y, y):  
return torch.sum(abs(pred_y-y))/len(y)

sgd()函数

1
2
3
4
5
def sgd(paras, lr):  
with torch.no_grad():
for para in paras:
para -= para.grad * lr
para.grad.zero_()

以下是 sgd 函数的详细执行过程分析:


sgd 函数执行步骤详解

1. 进入无梯度计算模式

1
with torch.no_grad():
  • 作用:禁用梯度跟踪,确保参数更新操作不会记录到计算图中。
  • 必要性:参数更新是纯粹的数值操作,不需要梯度信息。禁用梯度跟踪可以:
    • 避免不必要的内存占用(计算图不会被扩展)
    • 防止参数更新操作被错误地加入反向传播流程

2. 遍历所有参数

1
for para in paras:  # paras = [w_0, b_0]
  • 参数类型pararequires_grad=True 的叶子张量(如 w_0b_0
  • 关键属性:每个 para 的梯度存储在 para.grad 中,由之前的 loss.backward() 计算得到

3. 参数值更新

1
para -= para.grad * lr  # 等价于 para = para - lr * para.grad
  • 数学意义:执行梯度下降更新
    $$ \theta_{\text{new}} = \theta_{\text{old}} - \eta \cdot \nabla_{\theta}\mathcal{L} $$
  • 实现细节
    • 原地修改张量的值 (para 是直接操作对象)
    • 由于在 no_grad() 上下文中,此操作不会影响后续反向传播的计算图

4. 梯度清零

1
para.grad.zero_()
  • 必要性
    • PyTorch 默认会累积梯度(梯度 += 新梯度)
    • 必须显式清零,否则下一个 batch 的梯度会与当前梯度错误叠加
  • 方法:调用 zero_() 原地清零梯度张量

执行时序示例

假设当前 batch 的梯度已计算完成(loss.backward() 后):

步骤 参数值 w_0 梯度 w_0.grad 学习率 lr
初始 0.5 2.0 0.05
更新 0.5 - 0.05*2.0 = 0.4 2.0 → 0.0 (清零后) -

与标准实现的差异

  1. 手动更新 vs 优化器

    1
    2
    3
    4
    # 标准 PyTorch 方式(等效实现)
    optimizer = torch.optim.SGD([w_0, b_0], lr=lr)
    optimizer.step()
    optimizer.zero_grad()
    • 用户代码手动实现了优化器的核心逻辑
  2. 梯度清零时机

    • 用户代码在每个 batch 更新后立即清零梯度(正确)
    • 错误做法:在 epoch 结束后才清零(会导致梯度跨 batch 累积)

潜在问题与改进

  1. 梯度爆炸风险

    • 如果学习率 (lr) 过大,可能导致参数更新幅度过大
    • 改进方案:添加梯度裁剪 (torch.nn.utils.clip_grad_norm_)
  2. 更复杂的优化器

    1
    2
    3
    4
    5
    # 添加动量(需修改 sgd 函数)
    velocity = 0
    def sgd_momentum(para, lr, momentum=0.9):
    velocity = momentum * velocity - lr * para.grad
    para += velocity

关键总结

操作 作用 必要性等级
torch.no_grad() 防止参数更新污染计算图 必要
para -= grad*lr 执行梯度下降参数更新 核心操作
grad.zero_() 防止梯度跨 batch 累积 必要

通过这种手动实现的 SGD,开发者可以更直观地理解优化器底层的工作原理,但在实际项目中建议使用 PyTorch 内置优化器以获得更好的性能和稳定性。

图示


深度学习的训练过程