线性回归
目录
- 线性回归
- 导包
- 生成数据集
- 观察散点图
- 读取数据集
- 初始化模型参数
- 定义模型
- 定义损失函数
- 定义优化算法
- 训练
- 简易实现(pytorch)
- 生成数据集
- 读取数据集
- 定义模型
- 初始化模型参数
- 定义损失函数
- 定义优化算法
- 训练
导包
import random
import torch
from d2l import torch as d2l
生成数据集
def synthetic_data(w,b,num):
#x通过正态分布生成
x=torch.normal(0,1,(num,len(w)))
y=torch.matmul(x,w)+b
#数据集中加入噪声
y+=torch.normal(0,0.01,y.shape)
return x,y.reshape(-1,1)
w = torch.tensor([2,-3,4],dtype=torch.float32)
b=4.2
#生成1000个数据
features,labels=synthetic_data(w,b,1000)
观察散点图
d2l.set_figsize()
d2l.plt.scatter(features[:,1].detach().numpy(),labels.detach().numpy(),1)
结果:
、
读取数据集
#读取数据集
def data_iter(batch_size,features,labels):
num=len(features)
indices=list(range(num))
random.shuffle(indices)
for i in range(0,num,batch_size):
batch_indices=torch.tensor(indices[i:min(i+batch_size,num)])
yield features[batch_indices],labels[batch_indices]
这段代码实现了一个数据集的迭代器,用于分批次读取数据,每次输出10个随机数据。
参数说明:
batch_size:每个批次的数据量大小。
features:所有数据的特征,例如所有图像的像素点。
labels:所有数据对应的标签,例如所有图像的真实类别。
具体实现步骤:
计算数据集总共的样本数。
创建一个包含所有数据集索引的列表。
随机打乱索引列表。
每次取出 batch_size 个索引,根据这些索引从特征和标签中取出对应的数据,返回这个 batch 的数据作为迭代器的一个元素。
具体来说,代码中用了 Python 的生成器函数 yield,每次迭代时返回一个批次的数据。在循环过程中,用 range 函数以步长为 batch_size 遍历整个数据集。对于每个 i,取出 indices 中从 i 开始的 batch_size 个索引,将它们作为一个 torch.tensor 对象 batch_indices。最后,通过 features[batch_indices] 和 labels[batch_indices] 从数据集中取出相应的数据,返回一个批次的数据作为迭代器的一个元素。
初始化模型参数
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
这段代码使用 PyTorch 创建了两个张量 w 和 b,用于实现一个简单的神经网络中的参数。
具体来说,w 是一个形状为 (2,1) 的张量,其中每个元素都是从均值为 0、标准差为 0.01 的正态分布中随机抽样得到的。这些值将作为神经网络的权重。requires_grad=True 意味着我们希望在反向传播时计算该张量的梯度,以便对其进行优化。
b 是一个形状为 (1,) 的张量,其中所有元素都是 0。这些值将作为神经网络的偏置。同样地,requires_grad=True 意味着我们希望在反向传播时计算该张量的梯度。
这两个张量是神经网络中的参数,可以通过反向传播算法来调整它们以最小化损失函数。
定义模型
def linereg(X,w,b):
return torch.matmul(X,w)+b
定义损失函数
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
定义优化算法
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
这段代码实现了小批量随机梯度下降(SGD)优化算法的函数。它接受三个参数:
params:一个包含需要更新的张量(即模型参数)的列表。
lr:学习率,控制每次迭代参数更新的步长。
batch_size:每个小批量样本的大小,用于归一化梯度。
该函数使用了 PyTorch 的上下文管理器 torch.no_grad(),用于确保在更新参数时不会计算梯度。在函数中,对于每个张量 param,根据梯度下降的公式进行更新:
param -= lr * param.grad / batch_size
其中,param.grad 是张量 param 的梯度,lr 是学习率,batch_size 是小批量样本的大小。这个更新式子是梯度下降的一般形式,用于在每个迭代步骤中更新参数的值。
更新完参数后,param.grad 需要清零,以便进行下一次迭代:、
这个地方可能会有人疑惑:sgd函数没有返回值,里面的变量都是临时变量,上面这行代码只是在函数里做更新,更新的也只是临时变量,而且sgd函数也没有返回值,是怎么更新到w和b的呢?
造成你困惑的最主要原因的核心是“可变对象与不可变对象”。
int类型为一个不可变对象,torch类型为一个可变对象
一般对于可变对象来说“-=”操作符会直接改变self自身,地址不变。不可变对象则会构造一个新地址
举个例子:
import torch
x1 = 1
x2 = 2
params = [x1, x2]
for p in params:
print(id(p), id(x1), id(x2))
p -= 4
print(id(p), id(x1), id(x2))
print(params)
x1 = torch.Tensor([1])
x2 = torch.Tensor([2])
params = [x1, x2]
for p in params:
print(id(p), id(x1), id(x2))
p -= 4
print(id(p), id(x1), id(x2))
print(params)
结果为:
9784896 9784896 9784928
9784768 9784896 9784928
9784928 9784896 9784928
9784800 9784896 9784928
[1, 2]
139752445458112 139752445458112 139752445458176
139752445458112 139752445458112 139752445458176
139752445458176 139752445458112 139752445458176
139752445458176 139752445458112 139752445458176
[tensor([-3.]), tensor([-2.])]
可以看到对于int类型,地址变换了,而torch类型,地址却没有变化。
而若写成p = p - 4则会调用构造函数,并返回一个新的变量,也就不可能作用到原先的“可变对象”。
int类没有发生就地变化是因为它是一个不可变对象。
param.grad.zero_()
这个操作是必要的,因为 PyTorch 会累积梯度,即每次反向传播得到的梯度会被加到之前的梯度上。在每次迭代后清零梯度可以避免这种累积。
训练
超参数的定义
lr = 0.03 #学习率
num_epochs = 3 #训练轮次
net = linreg #网络结构
loss = squared_loss #损失函数
batch_size=10
训练过程
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
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}')
这段代码实现了一个简单的线性回归模型的训练过程,使用小批量随机梯度下降(SGD)优化模型参数。具体来说,代码的功能如下:
定义了超参数 lr(学习率)、num_epochs(迭代周期数)、batch_size(批量大小);
定义了模型 net(线性回归模型)和损失函数 loss(均方误差损失函数);
循环 num_epochs 次,每次循环遍历数据集,使用小批量随机梯度下降更新模型参数;
在每次迭代结束后,计算当前模型在整个数据集上的损失,输出训练结果。
具体来说,代码中的循环遍历数据集的部分使用了一个函数 data_iter(batch_size, features, labels),用于按批次遍历数据集。这个函数的实现可以根据具体的数据集结构进行定制。
每次遍历数据集时,对于每个小批量数据,模型首先根据当前的模型参数(权重w和偏差b)计算模型输出,并根据模型输出和标签计算小批量数据的损失。然后,使用 PyTorch 自动求导功能,计算模型参数的梯度,并使用小批量随机梯度下降更新模型参数。
最后,在每个迭代周期结束后,使用 torch.no_grad() 上下文管理器,关闭自动求导功能,计算当前模型在整个数据集上的损失,并输出结果。
关注l.sum().backward():
当我们使用 PyTorch 自动求导功能进行反向传播时,每次计算完一次损失函数之后,我们需要调用 backward() 方法计算相应的梯度。在这段代码中,l 是一个形状为 (batch_size,1) 的 Tensor,表示当前小批量数据的损失值,它并不是一个标量。
在对一个非标量的 Tensor 求导时,需要指定求导方向,即梯度在哪些维度上进行计算。在这里,我们对损失函数关于 l 所有元素的梯度进行求导,并将其累加到 w 和 b 的梯度上。
为了将所有元素的梯度累加到 w 和 b 的梯度上,我们需要先将 l 中所有元素加起来,得到一个标量,然后再调用 backward() 方法。这就是代码中 l.sum().backward() 的作用。它将 l 中所有元素加起来,得到一个标量,然后对该标量进行求导,将求得的梯度累加到 w 和 b 的梯度上。
需要注意的是,在每次迭代周期开始时,我们需要将 w 和 b 的梯度清零,即调用 param.grad.zero_() 方法。这是因为 PyTorch 默认会将每次计算得到的梯度累加到原有的梯度上,而我们希望每次计算得到的梯度都是新的,所以需要在每次迭代周期开始时将梯度清零。
backward的使用:
在 PyTorch 中,要计算某个节点的梯度,我们需要调用该节点上的 backward() 方法。一般来说,我们需要在计算损失函数后,对损失函数调用 backward() 方法以计算模型参数的梯度。以下是使用 backward() 方法的一个示例:
import torch
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x**2
z = 2*y + 3
# 计算 z 对 x 的梯度
z.backward(torch.ones_like(x))
print(x.grad) # 输出 [4.0, 8.0, 12.0]
在上面的代码中,我们首先创建了一个张量 x,并将 requires_grad 参数设置为 True,以便对 x 求导。接下来,我们定义了两个计算图节点 y 和 z,其中 y 是 x 的平方,而 z 是 2*y + 3。最后,我们对 z 调用 backward() 方法,以计算 z 对 x 的梯度。由于 z 是标量,我们需要在 backward() 方法中传入一个和 x 形状相同的张量作为梯度权重。在这个例子中,我们使用 torch.ones_like(x) 表示所有梯度权重都为1。最后,我们可以通过 x.grad 属性获取 x 的梯度,输出结果为 [4.0, 8.0, 12.0]。
简易实现(pytorch)
生成数据集
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
读取数据集
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
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_iter,用于分批次加载数据集,每个批次的大小为 batch_size。数据迭代器是一个重要的组件,用于在训练过程中批量读取数据,以提高数据加载效率。
在代码中,我们调用了 load_array 函数,该函数将数据集 features 和 labels 打包成一个 TensorDataset 对象,并通过 DataLoader 函数将其转换成数据迭代器。其中,is_train 参数指定是否需要对数据集进行随机洗牌,以增加数据的随机性和多样性。
这里使用 TensorDataset 和 DataLoader 这两个类,可以使数据集按批次读入,方便后续使用 PyTorch 中的各种优化算法进行训练。
定义模型
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
我们将两个参数传递到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}')
首先,num_epochs 变量定义了训练的总轮数。每一轮训练,数据集 data_iter 将会分成若干个大小为 batch_size 的批次,每个批次将会输入到模型中进行前向计算,然后计算损失函数 l。损失函数的值将用于后续的反向传播和优化器的更新。
在每个批次的前向计算结束后,我们调用 trainer.zero_grad() 方法清空梯度信息,然后调用 l.backward() 方法进行反向传播,计算出梯度信息并更新到对应的参数上。最后,我们通过调用 trainer.step() 方法对参数进行更新,完成了一次迭代训练。
在每轮训练结束后,我们对整个数据集进行一次前向计算,计算出该轮训练的平均损失,并打印输出。这样可以帮助我们监控模型的训练情况,以便进行后续调整和优化。