线性回归是深度学习训练的最小闭环:

  • 模型:

  • 损失:平方损失(MSE)

  • 求导:autograd

  • 更新:手写 SGD

这篇目标很明确:不用任何高级封装,你要把“参数怎么动起来”彻底弄明白。


1. 先造一个可控的数据集

我们用已知的真实参数 ((w^*, b^*)) 生成数据,并加一点噪声:

import torch
from torch.utils import data

torch.manual_seed(0)

def synthetic_data(w, b, num_examples):
    """生成 y = Xw + b + noise"""
    X = torch.randn(num_examples, len(w))
    y = X @ w + b
    y += torch.randn(num_examples, 1) * 0.01
    return X, y

true_w = torch.tensor([2.0, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

print(features.shape, labels.shape)  # (1000,2) (1000,1)

2. 读取小批量数据:DataLoader(但训练仍是手写)

def load_array(data_arrays, batch_size, is_train=True):
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 32
data_iter = load_array((features, labels), batch_size)

# 看一眼 batch 的形状
X_batch, y_batch = next(iter(data_iter))
print(X_batch.shape, y_batch.shape)  # (32,2) (32,1)

注意:DataLoader 只是帮你分 batch,不算“高级模型封装”。


3. 初始化参数:w、b 都是要学的

w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
  • w.shape=(d,1),方便写 X @ w

  • requires_grad=True 才能自动求导


4. 定义模型、损失、优化器(手写)

4.1 线性回归模型

def linreg(X, w, b):
    return X @ w + b

4.2 平方损失

这里写成“每个样本的损失”,后面再 mean():

def squared_loss(y_hat, y):
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

4.3 手写 SGD

核心更新公式:

def sgd(params, lr, batch_size):
    with torch.no_grad():  # 更新时不建图
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

5. 训练:把闭环跑起来

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

for epoch in range(num_epochs):
    for X, y in data_iter:
        y_hat = net(X, w, b)
        l = loss(y_hat, y)        # (batch,1)
        l.sum().backward()        # 变标量再反传
        sgd([w, b], lr, batch_size)

    with torch.no_grad():
        train_l = loss(net(features, w, b), labels).mean()
    print(f"epoch {epoch+1}, loss {train_l.item():.6f}")

print("w error:", true_w - w.reshape(true_w.shape))
print("b error:", true_b - b)
print("learned w:", w.reshape(-1).tolist(), " learned b:", b.item())

你会看到:

  • loss 快速下降到很小

  • 学到的 w, b 非常接近 true_w, true_b


6. 这一节最关键的“从零”细节(面试/作业常考)

6.1 为什么要 l.sum().backward()?

因为 l 是一个 batch 的向量损失,backward()更自然的是标量。
sum() 相当于对 batch 里的损失求和,再反向传播。

6.2 为什么更新要写 with torch.no_grad()?

否则“参数更新”也会被记录进计算图,图会越来越大,显存爆炸。

6.3 为什么梯度要 zero_()?

梯度默认累加,不清零会导致“越训越偏”。


7. 小结:你已经具备训练任何神经网络的最小能力

线性回归从零实现,看似简单,但它包含了深度学习训练的核心骨架:

  • 前向(模型)

  • 损失

  • 反向(autograd)

  • 参数更新(SGD)

后面 softmax、MLP、CNN 只是在“前向函数”更复杂而已。

 

更多推荐