Bootstrap

pytorch项目实战-回归模型李宏毅 21 机器学习第一次作业代码详解

请添加图片描述

前言

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
import csv
import os

2. 设置文件存储路径


#设置存放文件路径
tr_path = '/Users/wangwang/desktop/文件名.csv'
tt_path = '/Users/wangwang/desktop/文件名.csv'

3.设置参数确保模型可复现

myseed = 15926
np.random.seed(myseed) # np.random.seed()设置np下的随机数,要和torch使用的一致
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的性能基准测试,也就是说它不会在运行时自动寻找最优的算法来处理你的数据。虽
然这可能牺牲一些性能,它是实现可复现性的必要条件。

综上所述,这两行代码通常在需要精确复现以前实验结果时使用,例如在进行论文实验和比赛时,确保其他研究
者可以复现你的实验结果。但如果你更关注性能而不是复现性,你可能会选择不使用这些设置。

'''
if torch.cuda.is_available(): # torch.cuda.is_available() 获取是否有有效的GPU可用返回真假
	torch.cuda.manual_seed_all(myseed) #这个和之前上文中存在差异的部分就是有一个all这是便于多个GPU机器上在使用过程中使用相同的随机数种子

多数人不会使用多个GPU训练同样的模型,因此对所有GPU采用相同的随机数和上文中的np和torch采用一样的目的都是为了固定随机出事权重而已,确保模型结果的可复现性。

4. 数据集处理

  import torch.utils.data as Dataset,DataLoader

通常数据的处理包含两个部分一个是生成数据集,另一个是数据加载器。

Dataset
数据集的定义就是索引和数据构成的,知道索引可以拿到数据,知道数据可以知道索引。因此存在这样的一类东西,知道数据可以拿到索引知道索引可以拿到数据,将这一行为抽象成类,就是数据集类。但是由于关联的方式不同,具体索引和数据如何关联需要自己定义。定义好这样的符合要求的类后,实例化当前类,得到的对象就是符合要求的数据集,可以按照需求输入索引得到数据。而Dataset就是数据集类,想使用自定义数据集就要继承这个类自己设置索引和数据之间的关系,但是固定的一定要有一个魔法方法,让对象可以通过该方法返回数据,这就是getitem,在实例化过程中计算机需要知道占用多少空间索引需要判断下给定存储的空间即使len魔法方法,这样就可以按照要求继承这个类的按照自己的需求构建索引关系,并且设置返回值

上述内容过于口语化如果没看懂可以看下面这部分。


数据集的核心定义是索引(index)和数据(data)的映射关系,这意味着给定索引就可以得到相应的数据,反之亦然。为了抽象这种关系,我们定义了一个数据集类(Dataset)。在这个类中,索引和数据之间的具体关联方式需要开发者自行定义。一旦这个数据集类被正确定义和实例化,就可以根据索引检索数据了。

在PyTorch中,自定义数据集通常涉及继承Dataset基类。要成功继承这个类,必须实现以下两个关键方法:

  1. __getitem__(self, index):这个方法使得实例可以使用索引操作符[]直接访问数据。当数据集的某个项目被请求时,这个方法会被调用,并返回与给定索引相对应的数据项。

  2. __len__(self):这个方法应当返回数据集中的数据项总数,使得可以使用len()函数获取数据集的大小。

通过正确实现这两个方法,我们定义的数据集类就能与PyTorch的其他组件(例如数据加载器DataLoader)无缝协作,从而支持复杂的数据操作和模型训练流程。


DataLoader
通常在使用了Dataset实例化对象后能够调用数据集的数据了,当时如何按照自己的需求进行调用呢,比如想一次使用30个数据输入模型,想打乱数据,这些行为也被抽象成了装载器类中,只需要输入数据集对象就可以按照需求类会帮你生成一个你期望的这样一个迭代器对象,这样就可以直接送入模型训练了。这就是DataLoader的作用


在机器学习和深度学习任务中,数据的批处理、洗牌(打乱数据顺序)、并行加载等是至关重要的步骤,旨在优化模型的训练效率并有助于提高模型的泛化能力。PyTorch通过DataLoader类来实现这些功能,简化了数据处理流程。

DataLoader是一个迭代器,它接受Dataset类的一个实例化对象作为输入,并支持批量处理、数据洗牌等操作,从而为模型训练提供了强大的数据加载支持。具体而言,DataLoader通过以下特性帮助我们更灵活、更高效地处理数据:

  1. 批量加载DataLoader允许我们指定batch_size,即每次提供给模型的数据项数目。例如,设定batch_size=30意味着每次从数据集中取出30个数据项作为一组数据送入模型进行训练。

  2. 数据洗牌:通过设置shuffle=TrueDataLoader在每个训练轮次开始时会随机打乱数据的顺序。这有助于模型更好地学习数据的内在规律,避免由于数据顺序带来的偏见。

  3. 并行加载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(fp)返回每一行文件。这个对象将数据按行看待,强制转换这个对象也是按行变成的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
'''
	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 中负责优化算法的模块,它提供了许多内置的优化器类别,例如 AdamSGD 等。

代码的执行流程如下:

  • 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病例预测的回归任务的代码结构和实现过程。文章旨在为初学者提供一个深度学习项目的全面指南,包括从数据处理到模型训练再到预测的每个关键步骤,下一小节将会讲解第二个作业分类任务,便于读者对这部分的内容进一步的深化,从而能够独立的编写自己需要的网络架构。

;