神经网络分类任务代码详解
前言
本章节主要是对代码架构进行梳理学习,便于读者在深度学习代码部分有一个系统的认识,作为学习笔记供各位入门。具体的代码参考了李宏毅老师机器学习第二次作业的baseline,有微小出入不影响阅读。如有问题欢迎讨论。
一、深度学习代码框架
在上一节中,通过对回归模型代码的初步学习,对深度学习编程的基本结构有了一个粗略的认识。尽管如此,对于初学者而言,独立编写一套完整的深度学习代码仍然是一项挑战,分模块的教学方式有时也会导致知识点之间产生割裂感。为解决这一问题,本章将代码分块进行讲解,并结合脑图的形式,使学习过程更加系统化。这种结构化的安排便于读者更好地掌握和运用知识,也更容易独立编写深度学习代码,从而对整体的编程逻辑有一个清晰的把控。
上图是编写神经网络模型需要的各个模块的构成,在编写的代码过程中,可以按照上文的顺序进行编写,故此本章使用上述模块顺序讲解代码。
二、数据集介绍
模型所使用的数据是通过对声音信息进行预处理得到的,这些数据被保存为npy文件格式,因此在加载这些数据时我们主要依赖NumPy包。每一行数据代表一个唯一的音频特征,整个数据集的形状为(1229932, 429),表示有1229932条数据样本,每个样本都有429个特征。有1229932条数据,标签数据的维度也应为(1229932, )。数据集涵盖了39个不同的类别,这可以理解为对声音数据进行了一种标注或分类。本章节只使用到了训练文件的数据,通过划分训练和验证两部分作为模型使用,因为并没有测试集合的标签故此不使用测试集文件。
三、 分类代码详解
3.1 导入需要使用的包
在本节中,将逐步深入探究深度学习代码的构建。首先,构建任何代码项目的前提是准备好必要的工具——也就是导入相应的编程包。就像建筑工人在施工前要准备好各种工具一样,合适的编程包能助力我们高效地完成代码建设。现在,让我们回顾一下常用的编程包,并重点介绍本章新引入的tqdm
包。
相比于上一章,这个新增加的tqdm
包,其实是一个非常方便的工具,能够以进度条的形式反映循环操作的进度,给予即时的反馈。它就像是施工中的进度标尺,告诉我们距离任务完成还有多远,从而让整个训练过程更加清晰和可控。
import os
import torch
import numpy as np
import torch.nn as nn
from tqdm import tqdm
from torch.utils.data import Dataset, DataLoader
下面对tqdm进行举例子讲解:
import time
from tqdm import tqdm, trange
#trange(i)是tqdm(range(i))的一种简单写法
for i in trange(100):
time.sleep(0.05)
for i in tqdm(range(100), desc='Processing'):
time.sleep(0.05)
dic = ['a', 'b', 'c', 'd', 'e']
pbar = tqdm(dic)
for i in pbar:
pbar.set_description('Processing '+ i)
time.sleep(0.2)
预期的结果如下
100%|█████████████████████████████████████████| 100/100 [00:05<00:00, 18.12it/s]
Processing: 100%|█████████████████████████████| 100/100 [00:05<00:00, 18.18it/s]
Processing e: 100%|███████████████████████████████| 5/5 [00:01<00:00, 4.80it/s]
三种情况,trange本质上就是tqdm(range()的缩写,只要给tqdm类一个可以迭代的对象,就会生成一个进度条对象并且是可迭代的,通过循环遍历的方式就可以实现进度条的效果,本质上就是判断数据长度然后记录当下的遍历进度。而后面的desc 是tqdm类的参数可以设置进度条前展示的数据。
以第三个例子来讲解前面描述的信息。首先pbar = tqdm(dic)实例化一个进度条类,pbar可以进行遍历从而使用for进行遍历,i获取到的就是dic的值,pbar.set_description('Processing '+ i) 这行代码的作用是在进度条前显示“Processing ”加上当前正在迭代的元素 i。这样,随着循环的进行,进度条前的描述会在每次迭代时更新,显示当前正在处理的元素。比如,当循环到列表的第一个元素 a 时,进度条前会显示“Processing a”,当迭代到下一个元素 b 时,描述会更新为 “Processing b”,以此类推。这个方法提供了一种直观的方式来观察进度条当前状态以及正在进行的操作是针对哪个元素的。
3.2设置随机数种子
导入过包后,主要做的就是设置随机数种子从而保证模型的可复现性。
def same_seed(myseed):
np.random.seed(myseed)
torch.manual_seed(myseed)
if torch.cuda.is_available():
torch.cuda.manual_seed(myseed)
torch.cuda.manual_seed_all(myseed)
torch.backends.cudnn.benchmark = False
# 简单的说就是需要大量的并行化重复的运算,所以需要性能上的提升会采用一些优化算法
# 使用这些优化算法会出现一些不确定性,会让每次的效果存在一定的差异。这种情况会导致模型的不可复现性
# 而cudnn就是库的目的就是为了找到这些能够增加计算速度的算法, 因此要关闭这个设置torch.backends.cudnn.deterministic = True # 设置cuDNN算法为确定性模式
# 这一行代码会将cuDNN设置为确定性模式,以保证在每次运行时都使用相同的卷积算法,这有助于获得可复现的结果
# torch.backends.cudnn.benchmark = False 这个代码的目的就是为去找最优秀的算法处理,每次可能使用的代码不同,因此关闭这个设置会牺牲掉一部分的性能但是增加了模型的可复现性
torch.bachends.cudnn.deterministic = True
再次解释下,之所以对cpu,np,gpu设置相同的种子的目的不是保证他们生成的随机数都是一样的,只是保证每次模型训练使用到的随机数都是一样的。有点拗口,细细想想。
3.3构建数据集
在对环境搭建好的情况下开始着手构建所需要的数据集。分为四步。首先要导入数据,然后构建数据集类,实例化数据集对象,最后实例化数据加载器对象。
3.3.1 导入数据
path_root = '作业2/'
tr_set = np.load(path_root + 'train_11.npy')
tr_labels = np.load(path_root + 'train_label_11.npy')
tt_set = np.load(path_root + 'test_11.npy')
当前模型使用的数据全部是由lee老师的带教学生处理生成numpy格式,故此导入所采用的也是np.load(),由于模型所需数据全部在相同的跟目录下,所以采用的导入路径形式如上所示。具备数据后,希望按照自己的要求得到对应的数据集。因此下一步就是构建数据集类。
3.3.2 构建数据类
在上一节中展示,torch中所使用的构建数据集需要使用要两种类,一个是Dataset,一个就是Dataloader。Dataset就是数据集类,而数据集的定义就是使用索引可以取到数据,使用数据可以找到索引。因此,在对Dataset进行继承,主要是对数据和索引之间关系进行重构,按照自己的预期方式处理索引和数据之间的关系。为了满足通过索引拿到数据的操作,这个类方法中需要有一个索引才能够访问数据,故此这个类设计了__getitem__。数据的条数过多的情况下模型还需要使用分批处理,则这个类还需要让外部知道数据项的个数,因此有__len__方法。
class TIMTDataset(Dataset):
def __init__(self, x, y = None ):
super().__init__()
self.data = torch.from_numpy(x).float() #需要注意的是读取得到的np文件为了能够在torch使用需要转换格式。
if y is not None:
y = y.astype(np.int64)
self.label = torch.LongTensor(y)
else:
self.label = None
def __getitem__(self, index):
if self.label is None:
return self.data[index]
else:
return self.data[index], self.label[index]
def __len__(self):
return len(self.data)
self.data = torch.from_numpy(x).float()
在读取数据部分中说明数据都是被处理成npy文件。故此,数据格式都是np的,那么要将Numpy中的ndarray处理称为Tensor才能在神经网络中使用。这是由于在PyTorch中,无论是输入数据、模型的参数还是输出结果,都是以Tensor的形式存在的。在进行前向传播、反向传播和优化等一系列神经网络操作时,Tensor允许高效地在CPU或GPU上执行计算,并且能够自动跟踪梯度,这对于实现复杂的深度学习模型尤为关键。因此,需要进行数据转换。
对于x都转换了那么y标签同理也要转换。
y = y.astype(np.int64) self.label = torch.LongTensor(y)。
y = y.astype(np.int64) 这个操作首先将y数组的数据类型转换为Numpy的64位整型。这通常是为了确保后续操作的数据类型一致性,特别是在处理标签或索引时。
self.label = torch.LongTensor(y) 这一步将已经是64位整型的Numpy数组y转换为PyTorch中的长整型Tensor。LongTensor在PyTorch中是64位整型的别名,适用于存储整数类标签或者索引。
剩下的就是书写过程中对__getitem__ 和__len__的返回值认真对待。
3.3.3 实例化数据集
由于DataLoader中需要使用到Dataset数据集对象,因此在先对Dataset进行实例化。
val_ratio = 0.2 # 对训练集合进行分配,分成训练集和验证集合, 训练集合占比百分之80验证集百分之20
percent = int(tr_set.shape[0] * (1 - val_ratio))
train_data = tr_set[:percent]
train_labels = tr_labels[:percent]
val_data = tr_set[percent:]
val_labels = tr_labels[percent:]
data_tr= TIMTDataset(train_data,train_labels)
data_val = TIMTDataset(val_data,val_labels)
3.3.4 实例化数据加载器
得到数据集的对象,能够方便的拿取文件进行训练,如何批量话的,能够满足对数据打乱或者是各种操作,应对模型的训练呢?将这一系列行为封装成了一个类,是DataLoader从而满足对这部分操作的处理。
开始设计数据加载器,是否分批进行训练,是否打乱。注意验证集不打乱
train = DataLoader(data_tr, batch_size = 64, shuffle = True)
val = DataLoader(data_val, batch_size= 64, shuffle = False)
3.4 构建神经网络类
环境构建完成,数据加载器有了,现在开始搭建网络。一个简单的分类网络通过多个线性层和激活成构成,值得注意的是模型的接口是输入特征维度,而输出的大小是类别个数。
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(429,1024)
self.layer2 = nn.Linear(1024,512)
self.layer3 = nn.Linear(512,128)
self.out = nn.Linear(128, 39)
self.act_fn = nn.Sigmoid()
def forward(self, x):
x = self.layer1(x)
x = self.act_fn(x)
x = self.layer2(x)
x = self.act_fn(x)
x = self.layer3(x)
x = self.act_fn(x)
x = self.out(x)
return x
3.5 设置超参数为训练作准备
在训练神经网络时,除了需要数据和模型之外,还需要以下几个核心组件:
-
计算设备:这通常意味着选择在CPU或GPU上进行运算。GPU能够提供更快的计算速度,尤其是当涉及到大型网络和大规模数据集时。
-
模型保存路径:训练过程中需要定期保存你的模型,以便后续可以恢复或者部署。
-
超参数:超参数是在训练开始前设置的参数,它们会影响训练过程和最终模型的性能。
-
优化器:这决定了模型参数的更新方式。常见的优化器包括SGD、Adam等。
-
损失函数:在训练中用于衡量模型预测和真实标签之间的差异。
本文将优化器在训练代码中进行实例化,在当前部分也可以。因此在本部分将实现损失函数,模型的实例化以及超参数的设置。
下面代码值得注意的是 os.makedirs(‘models’, exist_ok = True),获取当前环境并且在当前环境下创建文件夹models,和’save_path’:'models/model.pth’一起使用作为保存训练模型的地址。
def get_device():
return 'cuda' if torch.cuda.is_available() else 'cpu'
device = get_device()
os.makedirs('models', exist_ok = True)
config = {
'myseed': 3000,
'n_epochs': 20,
'optim': 'Adam',
'lr':0.0001,
'save_path':'models/model.pth'
}
model = Classifier().to(device) # 如果有GPU可以送到GPU上训练更快
criterion = nn.CrossEntropyLoss()
现阶段,为模型训练的所有准备工作已经就绪:数据集、模型结构、超参数、损失函数等全部搭配完成。现在,只需将这些要素如同烹饪食材一样协同整合,送入训练框架中,就能顺利完成模型的训练和测试工作。
3.6 训练模块
训练模块大体上包含两部分内容,一部分是模块的训练一部分是模块的测试,本文没有测试集,因此验证模块作为测试的作用。
def Train(train_set,dev_set,model, config, device, criterion): # 模型需要接受训练集,验证集,模型,计算设备,损失函数
n_epochs = config['n_epochs'] #设置模型训练周期
optimizer = getattr(torch.optim, config['optim'] #实例化优化器,实现梯度下降更新参数
)(model.parameters(),config['lr']) # 后面这两个参数第一个是告诉模型需要修改的参数,第二个是优化器的超参数设置。
best_acc = 0.0 #这里存储模型数据最好的数值,用于控制什么时候存储模型保留可复现
for epoch in range(n_epochs): # 按周期遍历开始训练
train_acc = 0.0 # 为训练准确率变量分配空间
train_loss = 0.0 # 为训练损失变量分配空间
val_acc = 0.0 # 为验证准确率变量分配空间
val_loss = 0.0 # 验证损失变量分配空间
model.train() # 模型训练模式
for i, data in enumerate(tqdm(train_set)): #遍历数据集
'''
解释下 enumerate()
lt=['a','b','c','d','e','f','g'] # 数组
for i,item in enumerate(lt):
print(i, item) # 对数列的每个元素进行编码实现了类似于索引和值的组合,而这个索引值是由enumerate()自动生成的,从0开始,到迭代对象的长度减1。
下面是输出结果
0 a
1 b
2 c
3 d
4 e
5 f
6 g
'''
x, y = data # data部分才是train_set数据迭代器的返回值,
# 也就是说这个才是数据集__getitem__ 的返回值。
# x 是数据而y是标签
x, y = x.to(device), y.to(device) # 将数据迁移到设备上,如果有GPU加速运算
optimizer.zero_grad() # 将上一次计算的梯度清0
preds = model(x) # 接受模型的输出
batch_loss = criterion(preds,y) # 通过损失函数计算损失
_, train_preds= torch.max(preds,1) # 使用max函数,返回每行的最大值索引,即类别信息
batch_loss.backward() # 通过损失函数计算梯度
optimizer.step() # 通过梯度更新模型的参数,走了一步
train_acc += (train_preds.cpu() == y.cpu()).sum().item() # 将两部分信息比对,计算算对的数值。
train_loss += batch_loss.item() #计算损失
model.eval() # 模型设置测试模式
for i, data in enumerate(tqdm(dev_set)):
x, y = data
with torch.no_grad(): # 模型后续不会计算梯度
x, y = x.to(device),y.to(device)
preds = model(x) # 测试数据模型预测结果
batch_loss = criterion(preds,y) # 记录损失
_, val_preds = torch.max(preds,1) # 看下输出的类别,便于计算准确率
val_acc += (val_preds.cpu() == y.cpu()).sum().item()
val_loss +=batch_loss.item()
print('[{:03d}/{:03d}] Train Acc: {:3.6f} Loss: {:3.6f} | Val Acc: {:3.6f} loss: {:3.6f}'.format(
epoch + 1, n_epochs, train_acc/len(train_data), train_loss/len(train_set), val_acc/len(val_data), val_loss/len(dev_set)
))
if val_acc > best_acc: # 判断下模型在迭代过程中是否有进步有进步则保存下模型信息
best_acc = val_acc
print('saving model(val_acc{:.3f})'.format(best_acc/len(val_data)))
torch.save(model.state_dict(),config['save_path'])
print('finsh %s'% epoch)
接下来只需要进行训练整个模型就可以了,调用一下这个函数。
Train(train,val,model,config,device,criterion)
小结
本章节系统地讲解了使用PyTorch构建和训练深度学习模型的全过程,包括数据预处理、模型设计、训练策略设置和模型验证等关键步骤。通过这些内容,读者应该能够更加清楚地理解深度学习模型的开发过程,为独立编写代码打下了良好的基础。接下来,本专栏将继续更新,内容不仅限于作业解析,还包括深度学习模型的详细讲解。对于一些较为复杂的模型,将提供逐行代码解析和对模型整体架构的深入理解。如果在理解某些内容时遇到难点,欢迎大家参与讨论和提出修正意见。