在深度学习的世界里,线性回归常被视为“Hello World”级别的入门算法。虽然它看起来很简单,但它包含了深度学习的核心思想:参数化模型、损失函数、优化算法

今天,我将带领大家使用 PyTorch,不依赖高级 API,从零开始手动实现线性回归。

1. 生成人造数据

为了验证我们的代码是否正确,我们需要一个已知答案的数据集。我们将根据公式 $y = Xw + b + \text{noise}$ 生成数据。

我们设定真实的参数为:

  • 权重 (w): [2, -3.4]
  • 偏置 (b): 4.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
import torch
from d2l import torch as d2l

def synthetic_data(w, b, num_examples):
"""生成 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))

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

运行代码后,我们可以看到数据集的形状为 (1000, 2),标签形状为 (1000, 1)。通过绘制散点图,我们可以直观地看到特征与标签之间存在的线性关系(此处放你的图)。

2. 数据迭代器

在训练时,我们不会一次性把所有数据喂给模型,而是分批(Batch)进行。这不仅能节省内存,还能让模型收敛得更好。

1
2
3
4
5
6
7
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
random.shuffle(indices) # 打乱数据
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(indices[i:i+batch_size])
yield features[batch_indices], labels[batch_indices]

这里我们使用了 Python 的生成器 (yield),它可以在需要时才生成数据,非常高效。

3. 初始化参数

接下来,我们需要初始化模型的参数。我们将权重 w 初始化为均值为 0、标准差为 0.01 的正态分布随机数,偏置 b 初始化为 0。

注意:因为我们希望 PyTorch 记录这些参数的梯度以便后续更新,所以必须设置 requires_grad=True

1
2
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

4. 定义模型与损失

模型的定义非常简单,就是矩阵乘法:

1
2
def linreg(X, w, b):
return torch.matmul(X, w) + b

损失函数我们选择均方损失 (MSE)

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

5. 定义优化算法

我们实现最基础的小批量随机梯度下降 (SGD)。对于每个参数,我们将其沿着梯度的反方向移动一小步(学习率)。

1
2
3
4
5
6
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad(): # 更新参数时不需要计算梯度
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_() # 清零梯度,防止累积

6. 训练过程

现在,我们将所有组件组装起来,开始训练!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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=10, features=features, labels=labels):
l = loss(net(X, w, b), y) # 1. 计算损失
l.sum().backward() # 2. 自动求导
sgd([w, b], lr, 10) # 3. 更新参数
# 计算整个数据集上的损失
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'Epoch {epoch + 1}, Loss: {float(train_l.mean()):f}')

训练结果:
经过 3 轮训练,我们的损失已经降到了一个很小的值。让我们来看看学到的参数与真实参数的对比:

1
2
print(f'真实 w: {true_w}, 学习到的 w: {w.reshape(true_w.shape)}')
print(f'真实 b: {true_b}, 学习到的 b: {b}')

输出结果应该类似于:

w的估计误差: tensor([ 0.0003, -0.0008])
b的估计误差: tensor([0.0011])

可以看到,学习到的参数与真实参数非常接近!这证明我们的模型训练成功了。

结语

通过这篇文章,我们手动实现了线性回归的所有核心组件。虽然 PyTorch 之后提供了更高级的 nn.Moduletorch.optim,但理解底层原理对于成为一名优秀的算法工程师至关重要。

本文部分内容参考动手学深度学习,遵循 Apache 2.0 协议。