深度学习线性回归的代码实现

线性回归是深度学习中最简单也是最基础的模型,理解它对后面的学习是非常有帮助的。本文将使用 “小批量随机梯度下降” 的优化方法手动从零开始实现一个基本的线性回归预测,厘清它的工作细节,同时在最后也会再调用 PyTorch 实现一遍。

◇ 从零开始实现

▷ 生成数据集

为了简单起见,我们根据带有噪声的线性模型构造一个人造数据集,然后使用这个有限样本的数据集来恢复这个模型的参数。本次使用的是低维数据,这样可以很容易地将其可视化。

下面的代码生成了一个包含 1000 个样本的数据集,每个样本包含从标准正态分布中采样的 2 个特征,这样合成的数据集是一个矩阵 XR1000×2\boldsymbol X\in\mathbb{R}^{1000\times2}

本次使用线性模型参数 w=[2,3.4]\boldsymbol w=\left[2,-3.4\right]^\topb=4.2b=4.2 和噪声项 ε\varepsilon 生成数据集及其标签:

y=Xw+b+ε\boldsymbol y=\boldsymbol X\boldsymbol w+b+\varepsilon

ε\varepsilon 可以视为模型预测和标签时的潜在观测误差。在这里简单认为标准假设成立,即 ε\varepsilon 服从均值为 0 的正态分布。为了简化问题,我们将标准差设为 0.01。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
import torch

def synthetic_data(w, b, num_examples):
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, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

torch.normal() 是 PyTorch 中用于生成服从正态 (高斯) 分布的随机数的函数。其中前 2 个参数分别指定了分布的均值和标准差,括号中的参数指定要生成张量的形状。

len() 表示张量的长度。

torch.matmul() 是 PyTorch 中用于执行矩阵乘法的函数。

y.reshape(-1, 1) 中,-1 表示自适应行数。上面的代码中 y 的计算结果是一维向量,为了方便起见,把 y 转换成 (num_examples, 1) 的矩阵。

打印前 5 个样本看看什么样:

1
print('feature: ', features[0:5], '\nlabel: ', labels[0:5])

结果:

1
2
3
4
5
6
7
8
9
10
feature:  tensor([[ 0.0644, -1.7546],
[ 1.5372, -0.1418],
[ 0.2341, 0.3238],
[ 0.2704, -1.0086],
[-1.0450, 0.4461]])
label: tensor([[10.3100],
[ 7.7451],
[ 3.5761],
[ 8.1716],
[ 0.5938]])

然后分别生成 2 个特征和 labels 的散点图,可以直观地观察到它们之间的线性关系:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

plt.scatter(features[:, 0].numpy(), labels.numpy(), 1, label='feature_1', color='blue')
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1, label='feature_2', color='red')

plt.xlabel('features')
plt.ylabel('labels')
plt.legend()
plt.show()

plt.scatter() 是 Matplotlib 库中用于创建散点图的函数。上面代码中的 1 这个参数指定散点的大小,即每个点的直径。在这里,它被设置为 1,表示很小的点。

结果:
散点图

从上面散点图中可以看出很有意思的一点:红色点似乎比蓝色点更集中。这是因为我们设置的 w=[2,3.4]\boldsymbol w=\left[2,-3.4\right]^\top,第二个特征 (红色) -3.4 的权重,也就是影响程度,比第一个特征 2 要大。而散点图显示的是单一特征对结果的影响,蓝色点由于相对而言对结果的影响比较有限,所以更 “散” 一点。

▷ 读取数据集

训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。

在下面的代码中,定义了一个 data_iter 函数,该函数接收批量大小的特征矩阵和标签向量作为输入,生成大小为 batch_size 的小批量。每个小批量包含一组特征和标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import random

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):
# torch.tensor() 将切片后的索引列表转换为 PyTorch 张量
# 这里主要是为了确保操作的一致性和兼容性,在本例中不转换也可以运行
batch_indices = torch.tensor(
# min() 来确保不超过索引范围,即最大取到 num_examples
indices[i: min(i+batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]

yield 是 Python 中用于定义生成器的关键字。生成器是一种特殊类型的迭代器,通过使用 yield 语句,可以在每次迭代中产生一个值,而不是一次性返回所有值。这允许生成器函数保持它们的状态,从而支持在迭代之间暂停和继续执行。

也就是说,不同于 return,函数执行到 yield 时不会立即退出函数并返回结果,而是保存函数的进度,然后返回结果。等下次再次调用这个函数时,不会从头运行,而是从上次 yield 的位置继续运行。

可以试着读取第一个小批量数据样本并打印:

1
2
3
4
batch_size = 10
for batch_features, batch_labels in data_iter(batch_size, features, labels):
print(f"batch_features: {batch_features} \n batch_labels: {batch_labels}")
break

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
batch_features: tensor([[-0.1849,  0.7958],
[-0.0286, 0.5583],
[ 1.1953, -0.4185],
[ 0.0078, 0.0185],
[ 0.4892, -0.9079],
[ 0.6524, -0.6818],
[-0.1173, 0.9535],
[ 1.1543, -0.2051],
[-1.6046, -0.1765],
[-0.4376, -0.3649]])
batch_labels: tensor([[1.1180],
[2.2471],
[8.0201],
[4.1516],
[8.2617],
[7.8197],
[0.7257],
[7.2015],
[1.5883],
[4.5670]])

当运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。上面实现的迭代方法其实执行效率很低,可能会在实际问题上陷入麻烦。例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。在深度学习框架中实现的内置迭代器效率要高得多,它可以处理存储在文件中的数据和数据流提供的数据。

▷ 定义模型

模型即为线性回归模型:

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

▷ 初始化模型参数

在开始用小批量随机梯度下降优化模型参数之前,需要先有一些参数。在下面的代码中,通过从均值为 0、标准差为 0.01 的正态分布中采样随机数来初始化权重,并将偏置初始化为 0:

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

在初始化参数之后,需要的任务是更新这些参数,直到这些参数足够拟合先前的数据。每次更新都需要计算损失函数关于模型参数的梯度,有了这个梯度,就可以向减小损失的方向更新每个参数。下面会使用 PyTorch 的自动微分来计算梯度。

▷ 定义损失函数

这里使用平方损失函数:

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

▷ 定义优化算法

在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新参数,下面的函数可以实现小批量随机梯度下降更新。

该函数接受模型参数集合、学习速率和批量大小作为输入。每一步更新的大小由学习速率 lr 决定。因为我们计算的损失是一个批量样本的总和,所以用批量大小 batch_size 来规范化步长,这样 lr 步长大小就不会取决于我们对批量大小的选择:

1
2
3
4
5
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

在 pytorch 中,tensor requires_grad 参数如果设置为 True,则反向传播时,该 tensor 就会自动求导。tensor 的 requires_grad 属性默认为 False,若一个节点 (叶子变量:自己创建的 tensor) requires_grad 被设置为 True,那么所有依赖它的节点 requires_grad 都为 True (即使其他相依赖的 tensor 的 requires_grad = False)。

requires_grad 设置为 False 时,反向传播时就不会自动求导了,因此大大节约了显存或者说内存。

with torch.no_grad() 的作用:在该模块下,PyTorch 不会追踪计算图,所有计算得出的 tensor 的 requires_grad 都自动设置为 False,不会存储梯度信息。

▷ 训练

现在已经准备好了模型训练所有需要的要素,可以进行主要的训练部分了。在每次迭代中,读取一小批量训练样本,并通过模型来获得一组预测。计算完损失后,开始反向传播,存储每个参数的梯度。最后,调用优化算法 sgd 来更新模型参数。

在每个迭代周期 (epoch) 中,使用 data_iter 函数遍历整个数据集,并将训练数据集中所有样本都使用一次 (假设样本数能够被批量大小整除)。这里的迭代周期个数 num_epochs 和学习率 lr 都是超参数,分别设为 3 和 0.03。设置超参数很棘手,需要通过反复试验进行调整。 现在可以先忽略这些细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
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}")

mean() 用来计算平均。

:f 是一个格式说明符,用于格式化浮点数的输出,确保输出的是浮点数格式。

结果:

1
2
3
epoch 1: loss 0.029975
epoch 2: loss 0.000101
epoch 3: loss 0.000048

从上面的结果可以看出效果还是不错的。

因为使用的是自己合成的数据集,我们知道真正的参数是什么,所以也可以通过比较真实参数和训练得到的参数来评估训练的成功程度:

1
2
print(f"w 的误差:{true_w - w.reshape(true_w.shape)}")
print(f"b 的误差:{true_b - b}")

结果:

1
2
w 的误差:tensor([-7.5626e-04, -7.0572e-05], grad_fn=<SubBackward0>)
b 的误差:tensor([0.0008], grad_fn=<RsubBackward1>)

可以看到真实参数和通过训练学到的参数确实非常接近。

注意,我们不应该想当然地认为我们能够完美地求解参数。在机器学习中,我们通常不太关心恢复真正的参数,而更关心如何高度准确预测参数。幸运的是,即使是在复杂的优化问题上,随机梯度下降通常也能找到非常好的解。其中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。

▷ 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import torch
import random

# 定义数据库生成方法
def synthetic_data(w, b, num_examples):
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, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

# 随机小批量读取数据
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):
# torch.tensor() 将切片后的索引列表转换为 PyTorch 张量
# 这里主要是为了确保操作的一致性和兼容性,在本例中不转换也可以运行
batch_indices = torch.tensor(
# min() 来确保不超过索引范围,即最大取到 num_examples
indices[i: min(i+batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]

# 定义模型
def linreg(X, w, b):
return torch.matmul(X, w) + b

# 初始化模型参数
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

# 定义损失函数
def squared_loss(y_hat, y):
return ((y_hat - y.reshape(y_hat.shape)) ** 2) / 2

# 定义优化算法
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

# 指定训练超参数
batch_size = 10
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)
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 : {w.reshape(2).detach().numpy()}")
print(f"训练得到的 b : {b.detach().numpy()}")

改进,去除小批量采样数对训练的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import torch
import random

# 定义数据库生成方法
def synthetic_data(w, b, num_examples):
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, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

# 随机小批量读取数据
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):
# torch.tensor() 将切片后的索引列表转换为 PyTorch 张量
# 这里主要是为了确保操作的一致性和兼容性,在本例中不转换也可以运行
batch_indices = torch.tensor(
# min() 来确保不超过索引范围,即最大取到 num_examples
indices[i: min(i+batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]

# 定义模型
def linreg(X, w, b):
return torch.matmul(X, w) + b

# 初始化模型参数
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

# 定义损失函数
def squared_loss(y_hat, y):
return ((y_hat - y.reshape(y_hat.shape)) ** 2).mean() # 这里换成均值

# 定义优化算法
def sgd(params, lr):
with torch.no_grad():
for param in params:
param -= lr * param.grad # 无需除以批量大小,同时避免了总样本数和采样样本数无法整除导致单次学习率变小的问题
param.grad.zero_()

# 指定训练超参数
batch_size = 10
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)
l.backward() # 损失函数中,均值已经是常量,无需 sum 求和
sgd([w, b], lr)
# 每训练完一轮打印一下训练成果
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 : {w.reshape(2).detach().numpy()}")
print(f"训练得到的 b : {b.detach().numpy()}")

◇ PyTorch 实现

通过上面的步骤我们一步步实现了一个简单的线性回归预测,实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用,现代深度学习库也为我们实现了这些组件。

本节将使用深度学习框架 PyTorch 来简洁地实现上面的线性回归模型。

▷ 读取数据集

我们可以调用框架中现有的 API 来读取数据。可以将 featureslabels 作为 API 的参数传递,并通过数据迭代器指定 batch_size。此外,布尔值 is_train 表示是否希望数据迭代器对象在每个迭代周期内打乱数据:

1
2
3
4
5
6
7
8
9
from torch.utils import data

# 构造一个 PyTorch 数据迭代器
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 = 10
data_iter = load_array((features, labels), batch_size)

* 操作符用来解包,它会将元组中的元素拆分为独立的参数。即上面的代码中 *data_arrays 将会将元组中的两个数组 featureslabels 拆分为两个独立的参数。

data.TensorDataset 是 PyTorch 中的一个类,用于将多个张量组合成一个数据集。在这里,它将特征和标签作为参数传递进去,创建了一个 PyTorch 数据集,其中每个样本由对应的特征和标签组成。

data.DataLoader 是 PyTorch 中用于创建数据加载器 (data loader) 的类。数据加载器用于将数据集划分为小批次,并提供一个迭代器,使得在训练模型时可以方便地遍历这些小批次数据。

使用 data_iter 的方式与上节使用 data_iter 函数的方式相同。为了验证是否正常工作,可以试着读取并打印第一个小批量样本。

这里我们使用 iter 构造 Python 迭代器,并使用 next 从迭代器中获取第一项:

1
next(iter(data_iter))

iter()data_iter 转换为一个迭代器。

next() 是一个内置函数,用于获取迭代器的下一个元素。

在这里,我们将 data_iter 转换为迭代器后,通过 next() 获取了它的下一个元素。

或者使用上一节的方法也可以:

1
2
3
for x, y in data_iter:
print(x,'\n',y)
break

这里 for 循环会在每次迭代时调用迭代器的 __next__ 方法,并捕获 StopIteration 异常以结束循环。

更进一步讲for 循环其实就是通过迭代器协议工作的,就算是一个普通的列表,它也是将列表视为一个迭代器,而不是像我先前想像的那样只是简单从头到尾逐个访问元素而已。比如:

1
2
3
my_list = [1, 2, 3, 4, 5]
for item in my_list:
print(item)

首先 for 循环会调用 my_list 对象的 __iter__() 方法,获得一个迭代器对象。然后,for 循环会反复调用迭代器对象的 __next__() 方法,直到迭代结束。

当迭代器耗尽时,它会引发 StopIteration 异常,这是通知 for 循环迭代结束的信号。for 循环捕获这个异常,并自动结束循环。

那什么是迭代器?

迭代器 (Iterator) 其实是一个对象,它实现了迭代器协议,即包含 __iter__()__next__() 两个方法。迭代器允许对象按照一定顺序逐个访问元素,而不需要提前知道对象的整体结构。

下面是一个简单的迭代器实现的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyIterator:
def __init__(self, data):
self.data = data
self.index = 0

def __iter__(self):
return self # 返回迭代器对象自身

def __next__(self):
if self.index < len(self.data):
result = self.data[self.index]
self.index += 1
return result
else:
raise StopIteration # 没有更多元素时引发 StopIteration 异常

# 使用迭代器
my_list = [1, 2, 3, 4, 5]
my_iter = MyIterator(my_list)

for item in my_iter:
print(item)

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[tensor([[-0.3348, -0.9365],
[-1.0858, -0.5065],
[ 0.6894, -0.0577],
[-0.0191, 1.1570],
[ 0.5926, 0.2058],
[-0.5109, -0.5741],
[-0.4956, 0.0432],
[ 0.7325, -0.9802],
[-0.3977, 0.5215],
[ 0.3869, -0.4802]]),
tensor([[6.7197],
[3.7451],
[5.7863],
[0.2190],
[4.6974],
[5.1376],
[3.0434],
[9.0020],
[1.6255],
[6.6034]])]

▷ 定义模型

我们可以将线性回归模型描述为一个神经网络,如下图所示。需要注意的是,该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。

线性回归是一个单层神经网络

对于线性回归,每个输入都与每个输出 (本例只有一个输出) 相连,这种变换称为全连接层 (fully-connected layer) 或称为稠密层 (dense layer)

对于标准深度学习模型,可以使用框架预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。

首先定义一个模型变量 net,它是一个 Sequential 类的实例,Sequential 类将多个层串联在一起。当给定输入数据时,Sequential 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,以此类推。

在下面的例子中,由于模型只包含一个层,因此实际上不需要 Sequential。但是因为以后几乎所有的模型都是多层的,所以在这里使用 Sequential 来熟悉一下 “标准的流水线”。

在 PyTorch 中,全连接层在 Linear 类中定义。值得注意的是,我们将两个参数传递到 nn.Linear 中。第一个指定输入特征形状,即 2,第二个指定输出特征形状,输出特征形状为单个标量,因此为 1

1
2
3
from torch import nn

net = nn.Sequential(nn.Linear(2, 1))

▷ 初始化模型参数

在使用 net 之前,需要先初始化模型参数,如在线性回归模型中的权重和偏置。深度学习框架通常有预定义的方法来初始化参数。在这里,我们指定每个权重参数从均值为 0、标准差为 0.01 的正态分布中随机采样,将偏置参数初始化为 0。

正如构造 nn.Linear 时指定输入和输出尺寸一样,我们可以直接访问参数以设定它们的初始值。通过 net[0] 选择网络中的第一个图层,然后使用 weight.databias.data 方法访问参数。可以使用替换方法 normal_fill_ 来重写参数值:

1
2
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

weight 表示该层的权重参数。

bias 表示该层的偏置参数。

data 表示张量的数据部分。

▷ 定义损失函数

计算均方误差使用的是 MSELoss 类,也称为平方 L2\boldsymbol L_2 范数。默认情况下,它返回所有样本损失的平均值:

1
loss = nn.MSELoss()

▷ 定义优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch 在 optim 模块中实现了该算法的许多变种。当我们实例化一个 SGD 实例时,要指定优化的参数 (可通过 net.parameters() 从模型中获得) 以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 lr 值,这里设置为 0.03:

1
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

▷ 训练

当有了所有的基本组件,训练过程代码与上节从零开始实现时所做的非常相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 指定迭代周期
num_epochs = 3

for epoch in range(num_epochs):
for X, y in data_iter:
# 计算损失 (前向传播)
l = loss(net(X), y)
# 梯度清零
trainer.zero_grad()
# 反向传播计算梯度
l.backward()
# 调用优化器更新模型参数
trainer.step()
# 计算每个迭代周期后的损失,并打印出来监控训练过程
l = loss(net(features), labels)
print(f"epoch {epoch+1}, loss {l:f}")

结果:

1
2
3
epoch 1, loss 0.000298
epoch 2, loss 0.000091
epoch 3, loss 0.000091

同上节一样,可以比较一下生成数据集的真实参数和通过有限数据训练获得的模型参数。要访问参数,我们首先从 net 访问所需的层,然后读取该层的权重和偏置:

1
2
3
4
5
w = net[0].weight.data
b = net[0].bias.data

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

结果:

1
2
w 的误差:tensor([ 9.2030e-05, -4.8161e-04])
b 的误差:tensor([5.6267e-05])

▷ 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import torch
from torch.utils import data
from torch import nn

# 定义数据库生成方法
def synthetic_data(w, b, num_examples):
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, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

# 构造一个 PyTorch 数据迭代器
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 = 10
data_iter = load_array((features, labels), batch_size)

# 定义模型
net = nn.Sequential(nn.Linear(2, 1))

# 初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

# 定义损失函数
loss = nn.MSELoss()

# 定义优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

# 指定迭代周期
num_epochs = 3

# 训练
for epoch in range(num_epochs):
for X, y in data_iter:
# 计算损失 (前向传播)
l = loss(net(X), y)
# 梯度清零
trainer.zero_grad()
# 反向传播计算梯度
l.backward()
# 调用优化器更新模型参数
trainer.step()
# 计算每个迭代周期后的损失,并打印出来监控训练过程
l = loss(net(features), labels)
print(f"epoch {epoch+1}, loss {l:f}")

# 打印训练结果
w = net[0].weight.data
b = net[0].bias.data
print(f"训练得到的 w : {w.reshape(2).numpy()}")
print(f"训练得到的 b : {b.numpy()}")

◇ 参考内容

  1. 线性回归的从零开始实现. https://zh.d2l.ai/chapter_linear-networks/linear-regression-scratch.html
  2. 【pytorch系列】 with torch.no_grad():用法详解. https://blog.csdn.net/sazass/article/details/116668755
  3. 通俗讲解Pytorch梯度的相关问题:计算图、with torch.no_grad()、zero_grad()和backward();Variable,Parameter和torch.tensor(). https://zhuanlan.zhihu.com/p/416083478
  4. 线性回归的简洁实现. https://zh.d2l.ai/chapter_linear-networks/linear-regression-concise.html