回归任务代码详解
前言
Homework 1: COVID-19 Cases Prediction (Regression)
本章节主要是李老师作业中代码架构进行梳理学习,便于读者在深度学习代码部分知识有一个系统的认识,具体的数据集详细代码可点击这里。作为学习笔记供各位入门。有微小出入不影响阅读。如有问题欢迎讨论。
一、数据集介绍
本章节阐述了由两个CSV格式文件构成的数据集的处理方法。在这里,使用import csv
来加载数据,值得注意的是测试文件中的列数比训练文件少了一列,恰好缺少的是模型所需预测的目标列。考虑到本章节聚焦于回归任务,因此模型仅需要基于输入数据预测一个数值即可。整个模型的训练集和验证集是通过划分covid.train.csv
文件来构建的,且数据集中的前40个特征都是由0或1构成的二元特征,具体情况可以打开文件查看。
二、模型整体架构
在本章节中,将对模型代码的整体结构进行分块讲解,并根据需要针对特定细节进行展开举例说明。
1. 导入模型训练使用的使用的包
import torch
import torch.nn as nn
import torch.utils.data as Dataset,DataLoader
import numpy as np
‘’‘
可以看到np主要的作用就是记性它为
大量的维度数组与矩阵运算提供了支持,
同时也提供了很多数学函数库来处理这
些数组。由于其强大的数值计算能力,N
umPy 在深度学习、科学计算、数据分
析等领域有着广泛的应用。
’‘’
import csv
import os
将工具箱找到,应对接下来干活,工具只能多不能少,要不然后面用找不到啊。
2. 设置文件存储路径
现阶段的目的就是拿着人家给你的地址的去找干活用的材料
#设置存放文件路径
tr_path = '/Users/wangwang/desktop/文件名.csv' # 一个路径是训练tr_path train_path
tt_path = '/Users/wangwang/desktop/文件名.csv' # 一个路径是训练tt_path test_path
3.设置参数确保模型可复现
简单的把地面打扫打扫每次都差不多减少差异环境带来的不稳定性
myseed = 15926 # 随机数种子的固定设置
np.random.seed(myseed) # np作为运算的包,其大量运算都要依靠他,其中必然采用随机数所以,每次随机的数字都要固定的便于复现
torch.manual_seed(myseed) #设置相同的随机数种子确保实验的可重复性,就是说每次随机的随机权重是相同的
虽然,上文代码中np.random.seed(myseed)和 torch.manual_seed(myseed)的随机数种子是一样的,但是随机生成的参数可能不同,最终目的不是让np和torch生成的随机数一样,而是设置一个固定的随机数确保代码的的效果是可复现的。也就是说np每次的随机是固定的而torch的随机也是固定的,使用不同的随机数值设置也可以,为了方便实用了相同的随机数而已。
也就是说即使不同的库相同的随机数种子可能有不同的随机数生成机制,但是在这里,提供给每个库相同的种子值 “myseed” 主要是为了简化实验设置——通过这种方式可以确保,无论何时何地重复实验,只要在同一个软件环境下,都能得到相同的结果。
torch.backends.cudnn.deterministic = True #设置cuDNN算法为确定性模式,单词仅使用一种固定的卷积算法
torch.backends.cudnn.benchmark = False #就是不需要模型选择性能更好卷积算法来优化性能
'''
使用PyTorch进行深度学习模型的训练时,图形处理单元(GPU)上的CUDA深度神经网络库(cuDNN)可以提
供针对多种操作的优化。在训练过程中,由于在神经网络中存在大量的并行和非确定性操作,每次运行相同代码
可能会得到略微不同的结果。这是因为cuDNN库会尝试寻找最适合当前配置(如层的大小和形状)的最优算法。
为了获得完全可复现的结果,你需要确保每次运行都使用相同的算法。这就是以下设置的用途:
torch.backends.cudnn.deterministic = True # 设置cuDNN算法为确定性模式
这一行代码会将cuDNN设置为确定性模式,以保证在每次运行时都使用相同的卷积算法,这有助于获得可复现的
结果。
torch.backends.cudnn.benchmark = False # 关闭cuDNN性能自动优化
而这一行代码会禁用cuDNN的性能基准测试,也就是说它不会在运行时自动寻找最优的算法来处理你的数据。虽
然这可能牺牲一些性能,它是实现可复现性的必要条件。
综上所述,这两行代码通常在需要精确复现以前实验结果时使用,例如在进行论文实验和比赛时,确保其他研究
者可以复现你的实验结果。但如果你更关注性能而不是复现性,你可能会选择不使用这些设置。
'''
将这两个设置正确组合使用可以帮助你根据特定的需求,平衡模型的计算效率和结果的一致性:
-
torch.backends.cudnn.benchmark = False
:这个设置确保在模型的生命周期中不会因为输入数据的变化而尝试去寻找最快的卷积算法。这种设置适用于输入尺寸经常变化的场景,防止因为算法的重新选择而引入额外的延迟。 -
torch.backends.cudnn.deterministic = True
:这个设置确保CuDNN使用的卷积算法是确定性的,也就是说,给定相同的输入和模型权重,无论运行多少次,都会产生完全相同的结果。这对于需要确保模型结果可复现的研究和开发非常重要。
当你同时设置torch.backends.cudnn.benchmark = False
和torch.backends.cudnn.deterministic = True
时,你告诉PyTorch在运行模型时不要去寻找可能会变化的最快的算法,而且确保每次运算使用的算法保持一致,从而确保结果的确定性。这有助于实验结果的可重复性,尤其是在科学研究和敏感应用中非常关键,但可能会在某些情况下牺牲一部分计算性能。
if torch.cuda.is_available(): # torch.cuda.is_available() 获取是否有有效的GPU可用返回真假
torch.cuda.manual_seed_all(myseed) #这个和之前上文中存在差异的部分就是有一个all这是便于多个GPU机器上在使用过程中使用相同的随机数种子
多数人不会使用多个GPU训练同样的模型,因此对所有GPU采用相同的随机数和上文中的np和torch采用一样的目的都是为了固定随机出事权重而已,确保在每一个GPU上的结果都是可复现性。
4. 数据集处理
开始利用工具把开始建造原材料。通过工具包中的铁锨将材料进行处理
打个比方就是通过工具将水和泥造成砖,这个dataset就是将砖按照位置摆好了,这个数据加载器就是吐砖机器人,可以按照你的需求自动把砖递给你。这样方便你构建一个盖房子机器人进行协同,更好的工作。
import torch.utils.data as Dataset,DataLoader
通常数据的处理包含两个部分一个是生成数据集,另一个是数据加载器。
Dataset类 索引和数据的映射关系 怎么样才能你要哪个砖机器人就给你什么砖呢,这个映射需要你人为的构建,而这个dataset类就是提供了这样的一个基本写法,让你参考。
数据集的定义就是索引和数据构成的,知道索引可以拿到数据,知道数据可以知道索引。因此存在这样的一类东西,知道数据可以拿到索引知道索引可以拿到数据,将这一行为抽象成类,就是数据集类。但是由于关联的方式不同,具体索引和数据如何关联需要自己定义。定义好这样的符合要求的类后,实例化当前类,得到的对象就是符合要求的数据集,可以按照需求输入索引得到数据。而Dataset就是数据集类,想使用自定义数据集就要继承这个类自己设置索引和数据之间的关系,但是固定的一定要有一个魔法方法,让对象可以通过该方法返回数据,这就是getitem,在实例化过程中计算机需要知道占用多少空间索引需要判断下给定存储的空间即使len魔法方法,这样就可以按照要求继承这个类的按照自己的需求构建索引关系,并且设置返回值
上述内容过于口语化如果没看懂可以看下面这部分。
数据集的核心定义是索引(index)和数据(data)的映射关系,这意味着给定索引就可以得到相应的数据,反之亦然。为了抽象这种关系,我们定义了一个数据集类(Dataset)。在这个类中,索引和数据之间的具体关联方式需要开发者自行定义。一旦这个数据集类被正确定义和实例化,就可以根据索引检索数据了。
在PyTorch中,自定义的Dataset
类需要实现两个核心方法:__getitem__
和__len__
。这两个方法使得Dataset
类与DataLoader
紧密配合,实现了数据的灵活加载和处理。
-
__getitem__(self, index)
:这个方法接收一个索引(index
)作为参数,并返回数据集中对应索引的样本和标签。这样,数据集中的每个样本可以通过索引直接访问,从而使得DataLoader
能够按需加载数据。这对于实现批量加载、随机抽样等操作至关重要。在许多情况下,__getitem__
方法内会包含将原始数据转换为模型能直接处理的格式的代码,例如图像的转换和归一化、文本的编码等。 -
__len__(self)
:这个方法返回数据集中样本的总数。通过实现这个方法,DataLoader
可以了解数据集的大小,进而在进行批量加载、划分数据等操作时做出相应的安排。例如,它可以用来确定每个epoch遍历数据集的次数以及何时结束一个epoch。
结合使用这两个方法,DataLoader
能够实现如下功能:
- 批量处理:根据指定的批大小(batch size),加载一批一批的数据。
- 数据打乱:在每个epoch开始时打乱数据的顺序,这对于训练模型时避免过拟合非常有帮助。
- 并行加载:利用多进程来同时加载数据,显著提高数据加载的效率,尤其是在处理大规模数据集时。
通过为Dataset
提供这两个方法的定义,你可以为几乎任何类型的数据集创建一个与PyTorchDataLoader
兼容的加载器,从而使得数据加载既灵活又高效。
DataLoader
通常在使用了Dataset实例化对象后能够调用数据集的数据了,当时如何按照自己的需求进行调用呢,比如想一次使用30个数据输入模型,想打乱数据,这些行为也被抽象成了装载器类中,只需要输入数据集对象就可以按照需求类会帮你生成一个你期望的这样一个迭代器对象,这样就可以直接送入模型训练了。这就是DataLoader的作用
在机器学习和深度学习任务中,数据的批处理、洗牌(打乱数据顺序)、并行加载等是至关重要的步骤,旨在优化模型的训练效率并有助于提高模型的泛化能力。PyTorch通过DataLoader
类来实现这些功能,简化了数据处理流程。
DataLoader
是一个迭代器,它接受Dataset
类的一个实例化对象作为输入,并支持批量处理、数据洗牌等操作,从而为模型训练提供了强大的数据加载支持。具体而言,DataLoader
通过以下特性帮助我们更灵活、更高效地处理数据:
-
批量加载:
DataLoader
允许我们指定batch_size
,即每次提供给模型的数据项数目。例如,设定batch_size=30
意味着每次从数据集中取出30个数据项作为一组数据送入模型进行训练。 -
数据洗牌:通过设置
shuffle=True
,DataLoader
在每个训练轮次开始时会随机打乱数据的顺序。这有助于模型更好地学习数据的内在规律,避免由于数据顺序带来的偏见。 -
并行加载:
DataLoader
支持多线程数据加载,可通过num_workers
参数指定加载数据时使用的子进程数,这样可以加速数据加载过程,特别是在处理大规模数据集时。
与Dataset
类相结合,DataLoader
为模型训练提供了一种高效、灵活的数据处理方式,大大简化了机器学习和深度学习任务中的数据加载和预处理工作。
通过这种方式,DataLoader
成为了连接数据集和模型的桥梁,确保数据以最适合训练的方式被提交给模型。
4.1 Dataset类
开始处理原材料了
#首先学习Dataset类
class COVID19Dataset(Dataset):
def __init__(self, path, mode = 'train', target_only = False):
按照上文的对Dataset的理解,模型会将输入的数据和索引联系起来,希望训练验证测试都使用这一个类方法(代码的重用),具体这个类是处理哪些数据和索引连接呢,那就需要这个类会进行判断具体使用哪种方法去得到这些数据,不同的mode就会得到不同的数据。
target_only = False 主要作用是特征选择,在代码中理解下具体的作用。
class COVID19Dataset(Dataset):
def __init__(self, path, mode = 'train', target_only = False):
self.mode = mode # 判断使用哪种方式处理数据和索引之间的关系,由于代码的重用,所以采用这种方式。
indices= [] # 创建索引好将数据进行分离,结合后面的代码看这部分的作用
with open(path,'r') as fp: # 读取文件
data = list(csv.reader(fp))
# 仅仅是csv,reader将全部内容看成了一行一行的呗,其本质就是一个按行存的大列表,然后使用list进行强制转换呗
data = np.array(data[1:])[:,1:].astype(float) #[:,1:] 逗号左侧控制行右侧控制列,
# np.array(data[1:])[:,1:]这个代码就是第一行是列表的索引不取,第一列也是索引都不要。中间部分才真正的数据。
if not target_only: # 判断是不是使用全部数据去预测最后一列
feats = list(range(93)) # 模型是回归算法,数据最后一列是数值,因此0-93是特征94是target,也就是只需要通过前面全部信息预测最后一列的结果。
else:
pass #具体的特征选择部分代码需要自己调整
# 在上面的代码中数据已经得到了,具体的要处理索引和哪些数据联系起来呢。这就涉及到具体使用哪种方式,哪些数据去和索引连接,本质上就是数据集的划分,下面就是数据集的划分。
if mode == 'test':
data = data[:,feats] # 测试时候使用测试文件的全部数据0-93
self.data = torch.FloatTensor(data) # 这部分是是将np数据转换成tensor送入神经网络
else:
data = data[:,feats] # 测试时候使用测试文件的全部数据0-93 作为特征
target = data[:,-1] # 测试时候使用测试文件的全部数据94列 作为目标
if mode == 'train': # 训练时候使用的数据先将索引存入
for i in range(len(data)):
if i % 10!= 0:
indices.append(i)
else : # 验证时候使用的数据先将索引存入
for i in range(len(data)):
if i % 10 == 0:
indices.append(i)
self.data = torch.FloatTensor(data[indices])# 这部分是是将np数据转换成tensor送入神经网络
self.target = torch.FloatTensor(target[indices])
# 归一化操作,加速模型收敛速度,具体的可以参考Lee老师的课程。可以看下数据前面40列都是0或1因此不需要归一化
self.data[:,40:] = (self.data[:,40:] - self.data[:,40:].mean(dim = 0, keepdim = True))\
/ self.data[:,40:].std(dim = 0,keepdim = True) #实施Z-score标准化
self.dim = self.data.shape[1] # data.shape 会返回一个数组,就是这个data的形状,而[1]则是取第一个维度的数据。这个数据的目的是为了设置模型的输入维度
'''
举例子
data = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(data.shape) # 输出: torch.Size([2, 3])
dim = data.shape[1]
print(dim) # 输出: 3
'''
print(self.mode,len(self.data),self.dim)
def __getitem__(self, index): #方法便于对象返回值,通过索引返回数值
if self.mode == 'train' or 'dev'
return self.data[index], self.target[index]
else :
return self.data[index] #测试集没答案是上传的代码近kaggle测试使用的,不必纠结如果要是自己训练的数据可以给target
def __len__(self):
return len(self.data)
4.2 Dataloader
数据处理完成后可以使用首先按要求实例化这个数据类创建自己想要的数据和索引关系。文中代码是为了方便所以写成了一个函数,直接用于调用。
def prep_dataloader(path, mode, batch_size, n_jobs = 0 ,target_only = False):
dataset = COVID19Dataset(path, mode,target_only =target_only )
dataloader = DataLoader(dataset ,batch_size,
shuffle = (mode=='train'),
drop_last = False, num_workers = n_jobs,
pin_memory = True)
'''
drop_last = False 最后一个batch丢不丢,num_workers = n_jobs线程问题。
pin_memory = True就是说cpu通过PCIe个gpu进行数num_workers = n_jobs据的传输,
但是cpu的数据有时候会被存在固态中,那么就会多了一个存取的过程呗cpu取出来给pcle然后
传到gpu,这个pin_memory为真就是不让模型cpu将多余的信息存到磁盘中直接通过PCIe传给gpu
换言之当cpu内存不够也不使用固态的内存,因为cpu的内存直接参与计算更快,如果放到固体就会导致cpu处理完了再去固态取再次处理很麻烦
'''
return dataloader
5.神经网络
class NeuralNet(nn.Module):
'''
在PyTorch中,所有的神经网络模块都应该继承"nn.Module"类,这是所有神经网络类的基类。
nn.Module提供了神经网络训练和实施所需的很多功能,如神经网络层的管理和前向传播函数的定义。“nn.Modual是创建神经网络的超类类似于创建其他类的object”
'''
def __init__(self, input_dim ):
super().__init__() #nn.Sequential这个方法提供了直接把神经网络写在属性里面,不需要写在forward中,减少了代码的重复性
self.net = nn.Sequential(
nn.Linear(input_dim, 64)
nn.Relu()
nn.Linear(64,1)
)
self.criterion = nn.MSELoss(redution = 'mean') #定义损失函数
def forward(self, x):
return self.net(x).squeeze(1) # 在处理数据集的时候数据是分批送入模型的,那就会造成torch.Size([batch_size, 1])使用squeeze(1)会对指定的位置维度是1就是删除当前维度,所以变成了1维度的张量。
def cal_loss(self, pred, target): # 计算损失
return self.criterion(pred,target)
6. 训练函数
现阶段有了地砖机器人,并且盖房子的流程也设定好了,可以开始协助训练验证这个盖房子的流程了
数据类有了,网络类有了,现在就差如何协同这几个类了,当然不能是类操作了,应该对象间的协同。所以设置方法处理。
def train(tr_set, dv_set, model, config, device) # 数据,模型,超参数协同构成的训练方法。
n_epochs = cofig['n_epochs'] #本身就是训练方法所以超参数送进去,要知道模型最终需要数据集迭代几次。
# 最主要的是设计优化器,数据送入模型只定义了损失函数
optimizer = getattr(torch.optim,config['optimizer']) \
(model.parameters(),**config['optim_hparas'])
optimizer = getattr(torch.optim,config['optimizer'])
首先解读下这个代码,其实这个getattr就是按照config[‘optimizer’]这个参数去torch.optim里面找符合条件的类,并且返回这个类,如果是属性的话就返回属性值。
实际上,config['optimizer']
这个字典条目将存储一个字符串,它指定了你想要使用的优化器的名称,比如 'Adam'
或 'SGD'
。这个名称将作为 getattr
函数的第二个参数。
这里的 torch.optim
是 PyTorch 中负责优化算法的模块,它提供了许多内置的优化器类别,例如 Adam
、SGD
等。
代码的执行流程如下:
config['optimizer']
从配置字典中读取你希望使用的优化器的名称。getattr(torch.optim, config['optimizer'])
调用会根据这个名称,在torch.optim
模块中查找具有该名称的属性或类。- 如果找到了对应的优化器类,==
getattr
会返回这个类本身。==如果是属性就会返回属性值
所以,如果 config['optimizer']
的值为 'SGD'
,则 getattr(torch.optim,config['optimizer'])
相当于 torch.optim.SGD
。这样你就可以动态地根据配置创建不同的优化器对象了。
那么后面的这个(model.parameters(),**config[‘optim_hparas’])参数就用来实例化这个类的,优化器需要修改的参数,以及优化器所使用的超参数。
def train(tr_set, dv_set, model, config, device) # 数据,模型,超参数协同构成的训练方法。
n_epochs = cofig['n_epochs']
optimizer = getattr(torch.optim,config['optimizer']) \
(model.parameters(),**config['optim_hparas'])
min_mse = 1000 # 设置的就是早停的机制的阈值
loss_rocard = {'train':[],'dev':[]} # 记录一下训练和测试的损失情
early_stop_cnt = 0 #早停机制计数器
epoch = 0 #迭代次数
#准备工作做好开始训练
model.train()
while epoch < n_epochs: # 判断是否超过预期迭代次数
model.train() #模型开始训练
'''
model.train()是PyTorch框架中定义神经网络模型后常用的方法。
在PyTorch框架中,一旦你定义了一个模型的类,它继承自nn.Module,
这个模型类的实例可以调用.train()方法。这个方法的作用是将模型设置为训练模式。
'''
for x,y in tr_set #迭代数据集
optimizer.zero_grad() #梯度清0,里面会存梯度用于指导优化方向
x,y = x.to(device), y.to(device) #数据放在设备上
pred = model(x) # 计算结果
mse_loss = model.cal_loss(pred,y) #计算损失
mse_loss.backward() #计算梯度
optimizer.step() #走一步就是修改了一次参数,学习率上迈出了一步。
loss_record['train'].append(mse.detach().cpu().item()) # 保存损失
dev_mse =dev(de_set, model, device) #将训练完的模型进行验证得到损失
if dev_mse < min_mse:
min_mse = dev_mse # 对比大小,比阈值小则启动早停机制,判断是否会一个很低损失上迭代超过早停数
print('Saving model (epoch = {:4d}, loss = {:.4f})'
.format(epoch + 1,min_mse))
torch.save(model.state_dict(),config['save_path']) # 将各个层的参数通过字典形式存放起来保存,后面指定了存放的路径
early_stop_cnt = 0 # 如果是模型新的训练损失比之前的小早停机制清0
else :
early_stop_cnt += 1 #没损失小那就+= 1
epoch+=1
loss_record['dev'].append(dev_mse)
if early_stop_cnt > config['early_stop']:
break
print('完成训练{} epochs'.format(epoch))
return min_mse, loss_rocard
loss_record[‘train’].append(mse.detach().cpu().item()) # 保存损失
训练函数已经设置好了但是还有一个部分没写呢,在训练中用到了验证函数。注意验证函数不优化,即不更新参数。也可以直接写在训练中代码库啊下面。
def dev(dv_set, model,device):
model.eval() #模型开始测试模式
total_loss = 0 # 损失变量
for x,y in dev:
x,y = x.to(device),y.to(device)
with torch.nograd(): #当前模块下的tensor不会求梯度
pred = model(x)
total_loss += model.cal_loss(pred, y).detach().cpu().item() * len(x)
total_loss = total_loss /len(dv_set.dataset)
'''
在计算损失时total_loss,由于每个批次可能包含不同数量的样本,所以为了确保损失是可比较的,
您可能先将每个批次的损失乘以该批次的样本数量(作为权重),这样做是为了把损失
"标准化"到单个样本的水平,这样可以无视批次的大小差异,得到一个公平的损失度量。
在处理完所有批次之后,若要计算整个数据集的平均损失,需要将这个加权后的总损失
除以整个数据集的样本总数,即 len(dv_set.dataset)。这样会得到无论批次大小
如何,每个样本平均贡献的损失,这是一个常用的做法来评估模型在整个数据集上的表现。
'''
return total_loss
上述代码也可以写在训练内部
def test(tt_set, model,device):
model.eval() #模型开始测试模式
preds = []
for x in dev:
x = x.to(device),y.to(device)
with torch.nograd(): #当前模块下的tensor不会求梯度
pred = model(x)
preds.append(pred.detach().cpu())
preds = torch.cat(preds,dim= 0).numpy()
return pred
7.超参数设置
现阶段全部的类函数都是设计好了,还差超参数需要设计:
def get_device():
if torch.cuda.is_avilble():
return 'cuda'
else:
return 'cpu'
device = get_device()
os.makedirs('models', exist_ok=True)
target_only = False
confit= {
'n_epochs' : 3000,
'batch_size' : 270,
'optimizer': 'SGD',
'optim_hparas' : {
'lr' :0.001,
'momentum':0.9 }
'early_stop':200,
'save_path':'models/model.pth'
}
os.makedirs('models', exist_ok=True) 'save_path':'models/model.pth'
这两个代码是联合使用的,os.makedirs(‘models’, exist_ok=True) 会返回当前执行代码的路径并在这个路径下创建一个models文件夹,直接调用models则是将这个文件的路径复制进去,接上’/model.pth’这部分内容进行村文件
全部代码都写差不多了,开始实例化对象。
8.实例化操作训练模型
数据集实例化,模型实例化,调用训练函数开始训练模型
tr_set = prep_dataloader(tr_path, 'train', config['batch_size'], target_only=target_only)
dv_set = prep_dataloader(tr_path, 'dev', config['batch_size'], target_only=target_only)
tt_set = prep_dataloader(tt_path, 'test', config['batch_size'], target_only=target_only)
模型实例化
model = NeuralNet(tr_set.dataset.dim).to(device) # Construct model and move to device
调用训练函数开始训练模型
model_loss, model_loss_record = train(tr_set, dv_set, model, config, device)
本文中未使用训练函数,是需要代码上传进kaggle测试的故此没有测试集的target。在实际的测试中测试集做的和验证集差不多,具体差异请看lee的课程。
总结
在本篇文章中,详细解析了利用PyTorch进行COVID-19病例预测的回归任务的代码结构和实现过程。文章旨在为初学者提供一个深度学习项目的全面指南,包括从数据处理到模型训练再到预测的每个关键步骤,下一小节将会讲解第二个作业分类任务,便于读者对这部分的内容进一步的深化,从而能够独立的编写自己需要的网络架构。