这部分知识主要包括了深度学习的基本流程介绍、基本配置、数据读入、模型构建、模型初始化、损失函数、优化器、训练和评估几个部分,让我们开始学习。
回顾我们在完成一项机器学习任务时的步骤,首先需要对数据进行预处理,其中重要的步骤包括数据格式的统一和必要的数据变换,同时划分训练集和测试集。接下来选择模型,并设定损失函数和优化函数,以及对应的超参数(当然可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。最后用模型去拟合训练集数据,并在验证集/测试集上计算模型表现。
深度学习和机器学习在流程上类似,但在代码实现上有较大的差异。首先,由于深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现;同时还有批(batch)训练等提高模型表现的策略,需要每次训练读取固定数量的样本送入模型中训练,因此深度学习在数据加载上需要有专门的设计。
在模型实现上,深度学习和机器学习也有很大差异。由于深度神经网络层数往往较多,同时会有一些用于实现特定功能的层(如卷积层、池化层、批正则化层、LSTM层等),因此深度神经网络往往需要**“逐层”搭建**,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。这种“定制化”的模型构建方式能够充分保证模型的灵活性,也对代码实现提出了新的要求。
接下来是损失函数和优化器的设定。这部分和经典机器学习的实现是类似的。但由于模型设定的灵活性,因此损失函数和优化器要能够保证反向传播能够在用户自行定义的模型结构上实现。
上述步骤完成后就可以开始训练了。我们前面介绍了GPU的概念和GPU用于并行计算加速的功能,不过程序默认是在CPU上运行的,因此在代码实现中,需要把模型和数据“放到”GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。如果使用多张GPU进行训练,还需要考虑模型和数据分配、整合的问题。此外,后续计算一些指标还需要把数据“放回”CPU。这里涉及到了一系列有关于GPU的配置和操作。
深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现。
经过以上步骤,一个深度学习任务就完成了。我们在详细讲解每个部分之前,先梳理了完成各个部分所需的功能,下面我们就去进一步了解一下PyTorch是如何实现各个部分的,以及PyTorch作为一个深度学习框架拥有的模块化特点。
首先导入必须的包。对于一个PyTorch项目,我们需要导入一些Python常用的包来帮助我们快速实现功能。常见的包有os、numpy等,此外还需要调用PyTorch自身一些模块便于灵活使用,比如torch、torch.nn等等。
#这只是列举除了一部分包
import os
import numpy as np
import pandas as pd
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
关于gpu的配置
#配置GPU(我没有)
# 方案一:使用os.environ,这种情况如果使用GPU不需要设置
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'
##这个后面的0,1指的是第1块和第2块显卡,根据自己实际情况填写。
##方案一后续使用就是XXX.cuda()
# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
##这个就是把所有对与gpu的操作赋值到device上,如果没有gpu那么torch.cuda.is_available()为False就会执行else的cpu
##方案二后续使用就是XXX.to_device(device)
##XXX可以是一个tensor类型的变量。
导入一些超参数:
batch_size:一批的数据的量
lr:初始学习率(初始),每次参数更新的步长
num_workers:表示有多少个线程用来供你读取数据
max_epochs:训练次数(轮数)
batch_size = 256
num_workers = 0 #我这里线程数设置成0是因为我没有gpu使用的时cpu,所以线程数太多会报错,于是只定义了一个主线程
lr = 1e-4# 10^-4
epochs = 20#由于我是cpu所以后续发现很慢,所以这个地方如果时cpu的话少执行几轮也可以
PyTorch数据读入是通过Dataset+DataLoader的方式完成的,Dataset定义好数据的格式和数据变换形式,DataLoader用iterative的方式不断读入批次数据。
我们可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承PyTorch自身的Dataset类。
我们输入的数据有时是来自使用torchvision自带的数据集,也有时是自己导入的数据集,当我们使用自己导入的数据集时就不免对数据进行变换,使得数据格式规范符合我们的需要,比如将数据都转成tensor类型。
#首先设置数据变换(一般对于自己像录入的数据需要自己来写录入方式,比如图片同意大小,或者数据格式全部转换为tensor等等)
from torchvision import transforms
image_size = 28
data_transform= transforms.Compose([
transforms.ToPILImage(), #这一步取决于后续的数据读取方式,如果使用内置数据集则不需要。
transforms.Resize(image_size), #统一大小
transforms.ToTensor() #数据变成tensor类型
])
数据读取有两种方式:一种是使用torchvision自带的数据集,也有时是自己导入的数据集,接下来我们将根据深度学习进行图像分类的经典例子FashionMNIST进行后续的代码展示,两种方式分别是:
方式一:
## 读取方式一:使用torchvision自带的数据集,下载可能需要一段时间
#这个方式比较小众也不常使用
from torchvision import datasets
train_data = datasets.FashionMNIST(root='./', train=True, transform=data_transform, download=True)
test_data = datasets.FashionMNIST(root='./', train=False, transform=data_transform, download=True)
方式二:
#读取方式二:读入csv格式的数据,自行构建Dataset类
#csv数据下载连接:https://www.kaggle.com/zalando-research/fashionmnist
class FMDataset(Dataset):
def __init__(self, df,transform=None):
self.df = df
self.transform = transform
self.images = df.iloc[:, 1 :].values.astype(np.uint8)#unit8是一个专门的图像的格式
self.labels = df.iloc[:, 0].values
def __getitem__(self, index): #最重要的部分,直接决定函数的构建
#通过index来控制我们要读取哪一行数据
image = self.images[index].reshape(28,28,1)#reshap把它变成28*28*1
label = int(self.labels[index])#label是我们的预测target
if self.transform is not None:
image = self.transform(image)
else:
image = torch.tensor(image/255., dtype=torch.float)
label = torch.tensor(label, dtype=torch.long)
return image, label
def __len__(self):
return len(self.images)
#实例化FMDataset类
train_df = pd.read_csv("./FashionMNIST/fashion-mnist_train.csv")
test_df = pd.read_csv("./FashionMNIST/fashion-mnist_test.csv")
train_data = FMDataset(train_df, data_transform)
test_data = FMDataset(test_df, data_transform)
注意:上面自己定义的FMDataset类有三个函数含义分别是:
查看读入的数据:
这个实验需要的数据集我放在这里如果需要可以自行下载
链接:https://pan.baidu.com/s/1zDsOybKJumkixhB1lqH8Yg
提取码:5688
–来自百度网盘超级会员V3的分享
在构建训练和测试集完成后,需定义DataLoader类,以便在训练和测试时加载数据
参数:
train_data:表示来自哪个Dataset,也就是我们上面定义好的类
batch_size:表示每一批加载多少数据
shuffle:是说上面类里面的getitem就不是从0开始,可能是打乱的来取数据
num_workers:用多少个线程来读
drop_last:是否对应到最后一个数据,因为一般导入数据最后可能是导不满的
pin_memory:增加内存占用但是会让程序运行快(用空间换时间)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=num_workers, drop_last=True)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=num_workers)
读入数据之后,我们可以自己做一些数据可视化操作,只要用于验证训练和测试的时候加载的数据
import matplotlib.pyplot as plt
image, label = next(iter(train_loader))# iter是一种迭代类似for循环,但是iter只循环一次,next是让iter往下再执行一步
print(image.shape, label.size())# 打印一下形状看看
plt.imshow(image[0][0], cmap="gray")#image[0][0] 表示我们读取数据的第一个,也可以自己改成其他的。
这个图片因为每次都是随机生成的训练集所以每次执行都会发生变化
由于任务比较简单,这里我们手搭一个CNN卷积神经网络,而不考虑当下各种模型的复杂结构。模型构建完成后,将模型放到GPU上训练(我没有GPU所以是在cpu上训练的)
在Jupyter notebook中可以使用?+要查看的内容输出帮助文档进行学习。
#使用?弹出帮助文档查看具体信息
?nn.Conv2d
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
#初始化自己定义层,self.conv是自己定义的一个卷积层
#Sequential是序贯模型,表示内部其实已经有顺序了,任何数据进入都需要按照下面的顺序
self.conv = nn.Sequential(
#1, 32, 5与nn.Conv2d的参数有关可以点开帮助文档查看
#比如:第一个位置是输入的频道,因为现在是单通道灰度图片 所以是1,如果是RGB三通道就是3
nn.Conv2d(1, 32, 5),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
nn.Dropout(0.3),
nn.Conv2d(32,64, 5),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
nn.Dropout(0.3)
)
#这是自己定义的全连接层
self.fc = nn.Sequential(
nn.Linear(64*4*4, 512),#从64*4*4这么多神经元变成512大小
nn.ReLU(),
nn.Linear(512, 10)
#再从512变成10,512是一个中间层设成什么其实都可以就是表现可能不同,但是10很关键因为当前有10类,所以是10
)
#顺序排列,x是输入的数据
def forward(self, x):
#先用.conv给他放到卷积层卷一遍
x = self.conv(x)
#再使用view()给他的tensor改一下尺寸,前面是-1表示自动根据后面的64*4*4配合生成行,主要是为了把数据拉平了为了后续全连接层操作
x = x.view(-1, 64*4*4)
#全连接层把刚刚64*4*4维的数据变成10维的用于输出
x = self.fc(x)
# x = nn.functional.normalize(x)
return x
model = Net()#类实例化
#model = model.cuda()#gpu操作
torch.nn提供了很多预定义的损失函数,也可以自己定义。
常用操作–backward()
设定损失函数
例如、使用torch.nn模块自带的CrossEntropy损失
Pytorch会自动把整数的label转为one-hot型,用于计算CE loss,这个也是仅限于CrossEntropy loss
这里需要确保label是从0开始的,同时模型不加softmax层(使用logits计算),这也说明pytorch训练中各个部分不是独立的,需要通盘考虑。
criterion = nn.CrossEntropyLoss()
# 修改权重 criterion = nn.CrossEntropyLoss(weight={1,1,1,1,3,1,1,1,1,1})
#比如这里就是假设我们对于四个参数效果不满意就给他权重加大,别人惩罚一次,4就惩罚三次。
查看相关帮助文档:
?nn.CrossEntropyLoss
这里我们使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)#lr是学习率,也就是每次学习的步长
各自封装成函数,方便后续调用
关注两者的主要区别:
此外,对于测验或验证过程,可以计算分类准确率(acc)。
def train(epoch):
model.train()#进入训练模式
train_loss = 0#初始化成0
for data, label in train_loader:#每次得到一个data, label
#data, label = data.cuda(), label.cuda() #因为之前model已经放到cuda了,所以data和label也要放到cuda,但我没有gpu...
optimizer.zero_grad()#防止梯度累加,要清零
output = model(data)#前向传播,得到输出output
loss = criterion(output, label)#损失函数的计算,criterion在上面定义了,是一个求损失函数的
loss.backward()#反向传播回去
optimizer.step()#用优化器更新一下权重
train_loss += loss.item()*data.size(0)#训练损失叠加
train_loss = train_loss/len(train_loader.dataset)#train_loss = train_loss / dataset的数据长度
print('Epoch:{} \tTraining Loss: {:.6f}'.format(epoch, train_loss))#输出第几个epoch,损失是多少
与训练模型的四点不同:
1.模型定义的不同
2.优化器要不要做0初始化-- optimizer.zero_grad()
3.损失函数要不要回传-- loss.backward()
4.优化器要不要做每步的权重更新-- optimizer.step()
def val(epoch):
model.eval()#模型模式与上面不同
val_loss = 0
gt_labels = []#真实label
pred_labels = []#预测label
with torch.no_grad():#表示不做梯度计算,如果不加这个会报memory的错误
for data, label in test_loader:
#data, label = data.cuda(),label.cuda() #因为之前model已经放到cuda了,所以data和label也要放到cuda,但我没有gpu...
output = model(data)
preds = torch.argmax(output, 1)#对输出的output求最大,找到他是哪一类
gt_labels.append(label.cpu().data.numpy())#把所有真实的结果拼起来
pred_labels.append(preds.cpu().data.numpy())#把所有预测的结果拼起来
loss = criterion(output, label)
val_loss += loss.item()*data.size(0)
val_loss = val_loss/len(test_loader.dataset)
gt_labels, pred_labels = np.concatenate(gt_labels),np.concatenate(pred_labels)
acc = np.sum(gt_labels==pred_labels)/len(pred_labels)#准确率 = 真实=预测的数量 / 预测总数量
print('Epoch:{} \tValidation Loss: {:.6}'.format(epoch, val_loss, acc))
for epoch in range(1, epochs+1):
train(epoch)
val(epoch)
我这里用的cpu对于FashionMNIST数据集训练集给出的六万条数据,训练太慢,平均一分钟一条,所以我没有等到最初epochs设定的20轮就中止了。
很有用:比如报显存错误可以查看是不是数据集一批传送的太大了或者说显卡被占满了。
gpu_info = !nvidia-smi -i 0
gpu_info = '\n'.join(gpu_info)
print(gpu_info)
训练完成后,可以使用torch.save保存模型参数或者整个模型,也可以再训练中保存模型
这部分会在后面的pytorch学习笔记里展示
save_path = "./FahionModel.pkl"#我估计这是学长自己的路径
torch.save(model, save_path)
原因:使用torchvision自带的数据集,下载的时候没下载全只下载了一部分所以报错。
解决:重新下一遍就好了。
原因:我使用的是cpu带不起来太多的线程数。
解决:将num_works改为0,表示只有一个主线程。
原因:敲错了,导致两个地方的大小没有统一。
解决:将上面的改为6444大小即可。
以上就是我第二部分的学习总结,此外还有一些优化器的知识我后续会整理进来。