基于PyTorch的线性回归的简洁实现

前言

这篇文章用来记录本人在学习《动手学深度学习》这本书时对章节( 线性回归的简洁实现)的一些困惑、理解和解答。

1、生成数据集

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)

2、读取数据集

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)

next(iter(data_iter))

PyTorch中Dataset,TensorDataset和DataLoader用法

  • 详见以下博客

PyTorch中Dataset,TensorDataset和DataLoader用法

  • 用法介绍

PyTorch中常用 类torch.utils.data.Datase t和 类torch.utils.data.TensorDataset 对数据进行封装;
常用 类torch.utils.data.DataLoader 对数据进行加载。

  • torch.utils.data.Dataset的用法
class Dataset(object):
	def  __getitem__(self, index):
		raise NotImplementError
	def __len__(self):
		raise NotImplementError
	def __add__(self, other):
		return ConcatDataset([self, other])

注:torch.utils.data.Dataset表示一个数据集的抽象类,所有的其它数据集都要以它为父类进行数据封装。Dataset的类函数__getitem__和__len__必须要被进行重写

  • torch.utils.data.TensorDataset的用法

classtorch.utils.data.TensorDataset(data_tensor, target_tensor)

  • data_tensor : 需要被封装的数据样本
  • target_tensor : 需要被封装的数据标签
class TensorDataset(Dataset):
    # TensorDataset继承Dataset, 重载了__init__, __getitem__, __len__
    def __init__(self, data_tensor, target_tensor):
        self.data_tensor = data_tensor
        self.target_tensor = target_tensor
    def __getitem__(self, index):
        return self.data_tensor[index], self.target_tensor[index]
    def __len__(self):
        return self.data_tensor.size(0)

注:torch.utils.data.TensorDataset继承父类torch.utils.data.Dataset,不需要对类TensorDataset的函数进行重写

  • torch.utils.data.DataLoader的用法
class torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, 
								  sampler=None, batch_sampler=None, num_workers=0, 
								  collate_fn=None, pin_memory=False, drop_last=False, 
								  timeout=0, worker_init_fn=None, multiprocessing_context=None)
  • dataset (Dataset): 封装后的数据集
  • batch_size (python:int,optional)): 每一批加载的样本量,默认值为1
  • shuffle (bool,optional): 设置为True时,每一个epoch重新打乱数据顺序
  • sampler (Sampler,optional): 定义在数据集中进行采样的策略,如果被指定,则False必须为shuffle。
  • batch_sampler (Sampler,optional):类似sampler,但是一次返回一批索引。互斥有batch_size,shuffle,sampler和drop_last。
  • num_workers (python:int,optional): 多少个子进程用于数据加载。0表示将在主进程中加载数据,默认值为0。
  • collate_fn(callable,optional): 合并样本列表以形成张量的小批量。在从地图样式数据集中使用批量加载时使用。
  • pin_memory (bool,optional): 如果为True,则数据加载器在将张量返回之前将其复制到CUDA固定的内存中。
  • drop_last (bool,optional): 设置为True,如果数据集大小不能被该批次大小整除则删除最后一个不完整的批次。如果False,数据集的大小不能被批量大小整除,那么最后一个批量将更小,默认值为False。
  • timeout (numeric,optional): 如果为正,则为从worker收集批次的超时值。应始终为非负数,默认值为0。
  • worker_init_fn (callable,optional): 如果不是None,则在种子工作之后和数据加载之前,将在每个工作程序子进程上调用此程序,并以工作程序ID作为输入,取值为[0, num_workers - 1]或None。

注:torch.utils.data.DataLoader结合了数据集和取样器,并且可以提供多个线程处理数据集。在训练模型时该类可以将数据进行切分,每次抛出一组数据,直至把所有的数据都抛出。

  • 代码示例:

数据封装利用的是TensorDataset,数据加载利用的是DataLoader。具体代码如下所示:

import torch  # 导入PyTorch库
import torch.utils.data as Data  # 导入PyTorch的数据工具模块

# 设置批量大小为5
BATCH_SIZE = 5 

# 生成一组从1到10的线性空间数据,共10个元素
x = torch.linspace(1, 10, 10)
# 生成一组从10到1的线性空间数据,共10个元素
y = torch.linspace(10, 1, 10)

# 使用x和y创建一个TensorDataset对象
torch_dataset = Data.TensorDataset(x, y)

# 创建一个DataLoader对象,用于按批次加载数据
loader = Data.DataLoader(
    dataset=torch_dataset,  # 设置数据集
    batch_size=BATCH_SIZE,  # 设置批量大小
    shuffle=True,  # 设置为True,表示在每个epoch开始时打乱数据
    num_workers=0,  # 设置使用的子进程数量,0表示不使用子进程
)

# 打印DataLoader对象的信息
print(loader)

# 定义一个函数,用于显示每个批次的数据和标签
def show_batch():
    for epoch in range(3):  # 遍历3个epoch
        for step, (batch_x, batch_y) in enumerate(loader):  # 按批次迭代数据
            # 在这里可以添加模型训练的代码
            print("Step:{}, Batch x:{}, Batch y:{}".format(step, batch_x, batch_y))  # 打印当前步骤、批次数据和标签

# 程序的主入口
if __name__ == '__main__':
    show_batch()  # 调用函数显示批次数据

代码运行结果:

在这里插入图片描述

定义模型

# nn是神经网络的缩写
from torch import nn

# 创建一个单个线性层的简单神经网络模型
net = nn.Sequential(nn.Linear(2,1))

nn.Linear

  • 详见以下博客

PyTorch nn.Linear的基本用法与原理详解

  • nn.Linear的基本定义

nn.Linear定义了一个神经网络的线性层

torch.nn.Linear(in_features, # 输入的神经元个数
           out_features, # 输出神经元个数
           bias=True # 是否包含偏置
           )
  • 代码示例
from torch import nn
import torch

model = nn.Linear(2, 1) # 输入特征数为2,输出特征数为1

input = torch.Tensor([1, 2]) # 给一个样本,该样本有2个特征(这两个特征的值分别为1和2)
output = model(input)
print(output) 

# 我们的输入为[1,2],输出了[-0.6220]。可以查看模型参数验证一下上述的式子:
# 查看模型参数
for param in model.parameters():
    print(param)

代码运行结果如下:

tensor([-0.6220], grad_fn=) Parameter containing:
tensor([[ 0.3339, -0.1969]], requires_grad=True) Parameter containing:
tensor([-0.5621], requires_grad=True)

可以看到,模型有3个参数,分别为两个权重和一个偏置。计算可得:
y = [ 1 , 2 ] ⋅ [ 0.3339 , − 0.1969 ] T − 0.5621 = − 0.6220 y = \begin{bmatrix}1 , 2\end{bmatrix} \cdot \begin{bmatrix}0.3339 ,-0.1969\end{bmatrix}^T - 0.5621 = -0.6220 y=[12][0.33390.1969]T0.5621=0.6220

初始化模型参数

# 使用weight.data和bias.data方法访问参数
# 使用替换方法normal_和fill_来重写参数值
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

定义损失函数

# 计算均方误差使用的是MSELoss类,也称为平方L2范数。默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()

nn.MSELoss损失函数

  • 均方损失函数:
    loss ⁡ ( x i , y i ) = ( x i − y i ) 2 \operatorname{loss}\left(\mathbf{x}_{i}, \mathbf{y}_{i}\right) = \left(\mathbf{x}_{i} - \mathbf{y}_{i}\right)^{2} loss(xi,yi)=(xiyi)2

这里的loss,x,y 的维度是一样的,可以是向量或者矩阵,i 是下标。

  • 参数
    很多的 loss 函数都有 size_averagereduce 两个布尔类型的参数。

(1) 如果 reduce = False,那么 size_average 参数失效直接返回向量形式的 loss

(2)如果 reduce = True,那么 loss 返回的是标量

(3)如果 size_average = True,返回 loss.mean()

(4)如果 size_average = False,返回 loss.sum()

注意:默认情况下, reduce = True,size_average = True(即返回的是 loss.mean() )

  • 代码示例
import torch
import torch.nn as nn

loss=nn.MSELoss() # 均方损失函数
target = torch.FloatTensor([[1, 2, 3], [4, 5, 6]])
pred   = torch.FloatTensor([[3, 2, 1], [6, 5, 4]])
# 将pred,target逐个元素求差,然后求平方,再求和,最后求均值
cost=loss(pred,target) 
print(cost) # tensor(2.6667)

sum=0
for i in range (0,2): # 遍历行i
    for j in range(0,3): # 遍历列
        sum+=(target[i][j]-pred[i][j])*(target[i][j]-pred[i][j])#对应元素做差,然后平方
print(sum/6) # tensor(2.6667)

定义优化算法

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

小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch在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
print('w的估计误差:', true_w- w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b- b)

代码解读

在每个迭代周期里,我们将完整遍历一次数据集(train_data),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤:

  • 通过调用net(X)生成预测并计算损失l(前向传播)。
  • 通过进行反向传播来计算梯度。
  • 通过调用优化器来更新模型参数。

为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。

对是否使用torch.no_grad()的理解

torch.optim.SGD的step函数

  • SGD源码中SGD类中的step函数
 @torch.no_grad()
    def step(self, closure=None):
        #网络模型参数和优化器的参数都保存在列表 self.param_groups 的元素中
        loss = None
        if closure is not None:
            with torch.enable_grad():
                loss = closure()
 
        for group in self.param_groups:
            params_with_grad = []
            d_p_list = []
            momentum_buffer_list = []
            weight_decay = group['weight_decay']
            momentum = group['momentum']
            dampening = group['dampening']
            nesterov = group['nesterov']
            lr = group['lr']
            #可以通过两层循环访问网络模型的每一个参数 p
            for p in group['params']:
                if p.grad is not None:
                    params_with_grad.append(p)
                    #获取到梯度d_p
                    d_p_list.append(p.grad)
 
                    state = self.state[p]
                    if 'momentum_buffer' not in state:
                        momentum_buffer_list.append(None)
                    else:
                        momentum_buffer_list.append(state['momentum_buffer'])
            #封装sgd函数
            F.sgd(params_with_grad,
                  d_p_list,
                  momentum_buffer_list,
                  weight_decay=weight_decay,
                  momentum=momentum,
                  lr=lr,
                  dampening=dampening,
                  nesterov=nesterov)
 
            # update momentum_buffers in state
            for p, momentum_buffer in zip(params_with_grad, momentum_buffer_list):
                state = self.state[p]
                state['momentum_buffer'] = momentum_buffer
 
        return loss

从上述代码中可以看到,调用 .step() 是有进行 with torch.no_grad(): 的
这就解释了为什么使用以下代码:

trainer.step()

来更新模型参数时无需另外的使用torch.no_grad()来关闭梯度计算,即无需多此一举成以下代码:

with torch.no_grad():
	trainer.step()

对梯度的计算不影响求损失

  • 训练过程
    在每一次的训练过程中,
    step 1:我们首先利用模型生成预测值,
    step 2:再把预测值同真实的标签值通过损失函数进行损失计算,
    step 3:然后把计算出来的损失值通过反向传播进行梯度计算,
    step 4:最后根据计算出来的梯度通过优化算法来更新模型参数。

需要注意的是,在每一次执行 step 3 之前,需要把上一次反向传播时生成的梯度进行清空,防止梯度在新的一次反向传播过程中累加。

也就是说,只需要确保在每一轮训练时,l.backward() 前需先进行梯度清空 trainer.zero_grad(),而不需要关心他是在 利用模型生成预测值 前或者后 进行的梯度清空。

换句话说,由于对梯度的计算不影响求损失,所以 .zero_grad() 放在 l = loss(net(X) ,y) 前面或者后面都无所谓。

作用域

在Python中,如果你在for循环内部定义了一个变量,那么这个变量的作用域仅限于for循环内部。这意味着,你在for循环内部对这个变量的任何操作都不会影响到for循环外部的同名变量。

代码示例:

a = 10  # 在for循环外部定义变量a

for i in range(5):
    a = i  # 在for循环内部重新定义变量a
    print("In loop:", a)  # 打印for循环内部的a

print("Out of loop:", a)  # 打印for循环外部的a

代码运行结果如下:

In loop: 0
In loop: 1
In loop: 2
In loop: 3
In loop: 4
Out of loop: 4

可以看到,尽管我们在for循环内部改变了变量a的值,但是这并没有影响到for循环外部的变量a。这是因为在Python中,变量的作用域是由其定义的位置决定的。在for循环内部定义的变量只在该for循环内部可见,而不会影响其他作用域中的同名变量。

所以对于以下代码:

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}')

来说,这里的 l = loss(net(X) ,y) 中的 ll = loss(net(features), labels) 中的 l 不是同一个变量

一般来说,为了避免混淆,我们把 l = loss(net(features), labels) 改为等价的 train_l = loss(net(features),labels)。

对于l = loss(net(features), labels)前为什么不进行with torch.no_grad(): ?

即为什么是

    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

而不是

with torch.no_grad():
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')	

原因如下:
通过前面对作用域的探讨可知,这里的 l = loss(net(X) ,y) 中的 ll = loss(net(features), labels) 中的 l 不是同一个变量

也就是说,假设 执行 l = loss(net(features), labels)前 没有进行梯度计算的关闭,而for循环里面的反向传播l.backward() 只受同属于一个for循环里的 l = loss(net(X) ,y) 操作的影响并不会受到来自for循环外的 l = loss(net(features), labels)操作的影响

所以对于 l = loss(net(features), labels) 前进行 with torch.no_grad(): 不进行无所谓,只不过进行 with torch.no_grad(): 可以确保为了在计算整个数据集上的损失时关闭梯度计算,这样可以节省内存并提高计算速度。同时,这也确保了在评估模型性能时不会意外地进行梯度计算和参数更新。

  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值