参考资料:
- 《Pytorch深度学习》(人民邮电出版社)第三章 深入了解神经网络
《Pytorch深度学习》(人民邮电出版社)第四章 机器学习基础- Pytorch官方文档
- 其他有参考的资料都在文章中以超链接的形式给出啦
0.1. 利用GPU加速深度学习
疫情期间没有办法用实验室的电脑来跑模型,只好用自己的笔记本来弄。发现如果没有GPU来跑的话真的是太慢了,非常推荐利用GPU加速深度学习的训练速度。
可以参考这篇文章:深度学习pytorch GPU windows 环境搭建
如果采用GPU的话,训练函数train_model(*)中数据的输入要改变一下,也就是需要将数据放在GPU上
inputs, labels = Variable(inputs).cuda(), Variable(labels).cuda()
另外,使用GPU训练可能会导致GPU内存不足的情况(CUDA out of memory),有一个办法就是将batch_size的值调小(其中一个原因是GPU没有办法一下子处理打包过来的那么多图片)。batch_size调小之后面临的问题自然就是训练的速度变慢。
0.2. 监控你的显存占用情况
在训练的过程中可以随时监控自己的显存占用情况,输入下面这个命令就可以:
C:\Program Files\NVIDIA Corporation\NVSMI>nvidia-smi
得到的结果如下:
0.3. optimal.step()和scheduler.step()
如果有更新到PyTorch 1.1.0之后的版本,就会出现“Detected call of lr_scheduler.step()
before optimizer.step()
.”这样的错误。可以参考这篇文章解决。
训练深度学习算法需要的几个步骤:
当运用神经网络去处理一些比较复杂的问题时,神经网络的架构就会变得特别复杂。为此,诸如pytorch、tensorflow这样的深度学习框架都对一些复杂的高级功能进行了抽象,方便使用者的使用。
层(Layer)是神经网络的基本组成,线性层是其中最重要的一种。在pytorch里面,线性层只需要一行代码就可以实现:
from torch.nn import Linear
myLayer = Linear(in_features = 10, out_features = 5, bias = True)
上面这行代码的作用在于对输入数据进行一个线性变换 y=Wx+b
其中,in_features是输入数据的维度,out_features是输出数据的维度,bias是“b”的值,默认为True;如果bias=False,则b=0。
例子:
import torch
from torch.nn import Linear
m = Linear(20, 30)
inp = torch.randn(128, 20)
out = m(inp)
print(out.size())
# output:torch.Size([128, 30])
线性层Linear可以查询两个训练参数:W和b
# 查询weight
w = m.weight
# 查询bias
b = m.bias
我们知道,神经网络每一层的的输出应该是 z=g(Wx+b),其中g(*)为非线性的激活函数。Pytorch里面也提供了一些非线性的激活函数可以使用。
sigmoid函数以实数作为输入,并以一个0-1之间的数值作为输出。
sigmoid一度被用于不同的机器学习框架,但它存在一个主要的弊端:当sigmoid函数的输出值接近0或1时,sigmoid函数前一层的梯度接近于0,由于前一层的学习参数的梯度接近于0,使得权重不能经常调整,从而产生了无效神经元。
线性整流函数(Rectified Linear Unit, ReLU),又称修正线性单元,是一种人工神经网络中常用的激活函数(activation function),通常指代以斜坡函数及其变种为代表的非线性函数。
ReLU函数有助于优化器更快地找到正确的权重集合,可以使随机梯度下降收敛得更快;而且计算成本更低,因为只需要设置一个阈值。但是当一个很大的梯度进行反向传播时,会出现一些无效神经元。对于这些无效神经元,可以通过调整学习率来控制。
Pytorch里面的非线性激活函数的使用:
import torch
import torch.nn as nn
m = nn.ReLU()
inp = torch.randn(2)
output = m(inp)
print(output)
import torch.nn as nn
class MyFirstNetwork(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(MyFirstNetwork, self).__init__()
self.layer1 = nn.Linear(input_size, hidden_size)
self.layer2 = nn.Linear(hidden_size, output_size)
def __forward__(self, input):
out = self.layer1(input)
out = nn.ReLU(out)
out = self.layer2(out)
return out
在PyTorch中,所有的网络都实现为类,因此,神经网络MyFirstNetwork被创建为PyTorch类nn.Module的子类(从nn.Module继承),并实现init和forward方法。super方法用于将子类的参数传给父类。
在init方法中,初始化层,这里是构建了两个线性层。
在forward方法中,把数据传入init方法中初始化的层,并返回最终的输出。非线性层经常被forward函数直接调用,有些时候也可以在init方法中实现。
数据预处理的目的是减小数据尺度等对深度学习算法训练的影响,主要包括以下数据处理步骤:
要使用哪些层通常由具体的机器学习来决定。机器学习问题中,通常要解决的问题有三类,无论前面的层如何设计,最后一层通常是确定的:
定义好了网络架构,还剩下了最重要的两步——评估和优化。
评估神经网络通常会利用损失函数,一般来说,损失函数越小,模型越好。损失函数的梯度可以对模型的参数进行优化。
PyTorch提供了一些可用的损失函数:
损失函数 | 用法/作用 |
---|---|
L1 loss | 通常作为正则化器使用 |
MSE loss | 均方误差损失,用于回归问题的损失函数 |
Cross-entropy loss | 交叉熵损失,用于二分类和多类别问题 |
NLL Loss | 用于分类问题,允许用户使用特定的权重处理不平衡数据集 |
NLL Loss2d | 用于像素级分类,通常和图像分割问题有关 |
在PyTorch里面使用这些损失函数,只要调用相应的函数就可以了:
import torch
import torch.nn as nn
loss = nn.CrossEntropyLoss()
inp = torch.randn(3,5,requires_grad = True)
target = torch.empty(3, dtype=torch.long).random_(5)
output = loss(inp, target)
output.backward()
这里以交叉熵Cross-entropy loss为例。
对于backward()函数的理解,参考Pytorch中的自动求导函数backward()所需参数含义
计算出网络的损失函数之后,需要对网络的参数进行优化以减少损失。PyTorch提供了很多优化器,这些优化器可以看成是一个黑盒子,他们接受损失函数和所有的学习参数,并微量调整来改善网络性能。这些可以在PyTorch官方文章中的torch.optim里面找到。
为了评估机器学习模型,通常会把数据集分成3个不同的部分——训练集、验证集、测试集。训练集和验证集用于训练算法并调优所有超参数;测试集用于评估模型的泛化能力。
训练集和测试集的划分通常由3种策略:简单保留验证、K折验证和迭代K这验证。在拆分数据时,需要考虑数据代表性、时间敏感性和数据冗余,这三种特性本质上都是体现样本的分布。
要使模型能够工作,有三个选择至关重要:
机器学习问题 | 激活函数 | 损失函数 |
---|---|---|
二分类 | sigmoid | nn.CrossEntropyLoss() |
多类别分类 | softmax | nn.CrossEntropyLoss() |
多标签分类 | sigmoid | nn.CrossEntropyLoss() |
回归 | 无(生成标量值作为输出的线性层) | MSE(均方误差) |
向量回归 | 无 | MSE(均方误差) |
torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
MultiStepLR:与StepLR类似,只不过步长是以列表的形式给出。
torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[30,80], gamma=0.1)
ExponentialLR:每一轮都将学习率乘上gamma值。
torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.1)
ReduceLROnPlateau:这是最常用的学习率改变策略之一。当特定的度量指标,如训练损失、验证损失或者准确率不再变化时,学习率就会改变。通常会将学习率的原始值降低为原来的1/2~1/10。
>>> ptimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
>>> scheduler = ReduceLROnPlateau(optimizer, 'min')
>>> for epoch in range(10):
>>> train(...)
>>> val_loss = validate(...)
>>> # Note that step should be called after validate()
>>> scheduler.step(val_loss)
光说不练假把式,我们下面就通过一个案例来巩固一下学习到的东西吧。
数据集:Dogs vs. Cats。数据集有个文件夹,一个是train(训练数据集),一个是test(测试集)。如果官网下载很慢的话,这里指路一个百度网盘的链接:【猫狗数据集】使用预训练的resnet18模型。在训练集中,有猫和狗的照片各12500张,每一张都通过文件名打标签:
首先是数据的分类,将数据集分为训练集(training set)和验证集(validation set)。
import os
import numpy as np
'''
数据集的分配
'''
path = 'train'
# 读取文件夹内的所有文件
files= os.listdir(path) #得到文件夹下的所有文件名称
print(f'Total no of images {len(files)}')
no_of_images = len(files)
# 创建验证集的随机文件索引
shuffle = np.random.permutation(no_of_images)
# 将训练数据集进行划分,2000个样本归入验证集,剩下的样本归入训练集
validation_index = shuffle[:2000]
train_index = shuffle[2000:]
# 创建验证集和训练集文件夹
os.mkdir(os.path.join(path, 'validation'))
os.mkdir(os.path.join(path, 'training'))
for t in ['training', 'validation']:
for folder in ['dog', 'cat']:
os.mkdir(os.path.join(path, t, folder))
# 将图片的一小部分复制到validation文件夹
for i in validation_index:
folder = files[i].split('/')[-1].split('.')[0]
image = files[i].split('/')[-1]
os.rename(os.path.join(path, files[i]), os.path.join(path, 'validation', folder, image))
# 将剩下的图片复制到training文件夹
for i in train_index:
folder = files[i].split('/')[-1].split('.')[0]
image = files[i].split('/')[-1]
os.rename(os.path.join(path, files[i]), os.path.join(path, 'training', folder, image))
上面的代码将数据集中的样本图片分成了训练集(23000个样本)和验证集(2000个样本),并在相应的目录底下创建了对应的类别文件夹(cat和dog)
数据预处理的目的是将图片加载成PyTorch张量。PyTorch的torchvision.datasets包提供了一个名为ImageFolder的工具类,可以用于加载图片以及相应的标签。
参数解释:
root:指定图片存储的路径
transform: 一个函数,原始图片作为输入,返回一个转换后的图片。
target_transform - 一个函数,输入为target,输出对其的转换。例子,输入的是图片标注的string,输出为word的索引。
预处理通常会经过以下三个步骤:
PyTorch在transforms模块中提供了很多工具函数,可以用于完成这些预处理的步骤。
'''
数据输入及加载
'''
import torchvision.datasets as dset
import torchvision.transforms as ts
import numpy as np
import matplotlib.pyplot as plt
simple_transform = ts.Compose([ts.Resize((224, 224)),
ts.ToTensor(),
ts.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225])
])
train = dset.ImageFolder('train/training/', simple_transform)
valid = dset.ImageFolder('train/validation/', simple_transform)
torchvision.transforms是pytorch中的图像预处理包,包含了很多种对图像数据进行变换的函数。
注意这里的transforms是torchvision里面的transforms,不是torch.distributions里面的transforms
transforms.Compose()用于将多种转换合并在一起。
transforms.Resize()将图片调整成制定的大小。
transforms.ToTensor()将PILImage转变为torch.FloatTensor的数据形式;
transforms.Normalize()进行归一化数据
多种组合变换有一定的先后顺序,处理PILImage的变换方法(大多数方法)都需要放在ToTensor方法之前,而处理tensor的方法(比如Normalize方法)就要放在ToTensor方法之后。
更多的图片转换方法可以参考pytorch中transform常用的几个方法
在train对象中,保留了所有图片以及相应的标签:
print(train.class_to_idx)
# out: {'cat': 0, 'dog': 1}
print(train.classes)
# out: ['cat', 'dog']
同样,可以对得到的张量进行再次变形并将值反归一化,就可以得到相应的图片:
# 对张量进行可视化
def imshow(inp):
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
imshow(train[50][0])
在深度学习或者机器学习中把图片进行批量取样是一个常用的方法,PyTorch提供了DataLoader类,它是PyTorch中数据读取的一个重要接口,该接口定义在dataloader.py中,只要是用PyTorch来训练模型基本都会用到该接口。该接口的目的:将自定义的Dataset根据batch size大小、是否shuffle等封装成一个Batch Size大小的Tensor,用于后面的训练。(可以参考PyTorch源码解读(一)torch.utils.data.DataLoader)
下面的代码将前面的train数据集和valid数据集转换到数据加载器(data loader)中:
import torch
# 按批加载Pytorch张量
train_data_gen = torch.utils.data.DataLoader(train, batch_size = 64, num_workers = 3)
valid_data_gen = torch.utils.data.DataLoader(valid, batch_size = 64, num_workers = 3)
dataset_sizes = {'train':len(train_data_gen.dataset),'valid':len(valid_data_gen.dataset)}
dataloaders = {'train':train_data_gen,'valid':valid_data_gen}
这里的num_workers用于多线程处理的设置。如果在运行代码的时候碰到下面这个错误代码:
[Errno 32] Broken pipe
那么可以参考这个原因:BrokenPipeError: [Errno 32] Broken pipe
把上面的train_data_gen和valid_data_gen改成下面的形式就可以了:
train_data_gen = torch.utils.data.DataLoader(train, batch_size = 64)
valid_data_gen = torch.utils.data.DataLoader(valid, batch_size = 64)
关于DataLoader的介绍可以看这篇文章:pytorch之dataloader深入剖析
文章中也给了一些参数的介绍:
对于计算机视觉中的大多数案例,我们可以使用已有的不同架构来解决实际的问题。torchvision.models模块里面提供了很多现成的应用:
import torchvision.models as models
resnet18 = models.resnet18()
alexnet = models.alexnet()
vgg16 = models.vgg16()
squeezenet = models.squeezenet1_0()
densenet = models.densenet161()
inception = models.inception_v3()
googlenet = models.googlenet()
shufflenet = models.shufflenet_v2_x1_0()
mobilenet = models.mobilenet_v2()
resnext50_32x4d = models.resnext50_32x4d()
wide_resnet50_2 = models.wide_resnet50_2()
mnasnet = models.mnasnet1_0()
在这里我们使用ResNet架构来解决。
# 构建网络架构
import torchvision.models as models
import torch.nn as nn
import torch.optim as op
model_ft = models.resnet18(pretrained = True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)
在这里,model_fit = models.resnet18(pretrained = True)创建了算法的实例,实例是PyTorch层的集合。ResNet架构是一个层的集合,具体可以参考 “Deep Residual Learning for Image Recognition”。同时,可以在这里预下载好ResNet-18模型,模型放在“C:\Users\Administrator.torch\models”文件夹下面。
pretrained (bool) – If True, returns a model pre-trained on ImageNet
ImageNet使用广泛的WordNet架构的变体来对对象进行分类,其预测的类别有1000种。使用预训练的权重会比随机分配权重得到的模型准确率更高一些。
然而,再我们这个案例中,我们最终预测的类别只有猫/狗两类,因此需要将ResNet模型的最后一层的输出特征改为2。即 model_ft.fc = nn.Linear(num_ftrs, 2)
如果在基于GPU的机器上面运行算法,需要在模型上调用cuda方法,让算法在GPU上运行:
# 检查是否可以在GPU上运行
if torch.cuda.is_available():
model_ft = model_ft.cuda()
接下来建立损失函数和基于SGD的优化器
# 损失函数和优化器
learning_rate = 0.001
criterion = nn.CrossEntropyLoss()
optimizer_ft = op.SGD(model_ft.parameters(), lr = learning_rate, momentum = 0.9)
exp_lr_scheduler = op.lr_scheduler.StepLR(optimizer_ft, step_size = 7, gamma = 0.1)
StepLR函数帮助动态修改学习率。在scheduler的step_size表示scheduler.step()每调用step_size次,对应的学习率就会按照策略调整一次。
我们首先来看一下训练模型的整体代码,再详细进行解读。下面的train_model函数获取模型输入,并通过多轮训练调优算法的权重,降低损失函数:
import time
from torch.autograd import Variable
# 训练模型
def train_model(model, criterion, optimizer, scheduler, num_epochs = 25):
since = time.time()
best_model_wts = model.state_dict()
best_acc = 0.0
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs-1))
print('-'*10)
# 每轮都有训练和验证的阶段
for phase in ['train', 'valid']:
if phase == 'train':
scheduler.step()
model.train(True) # 模型设置为训练模式
else:
model.train(False) # 模型设置为评估模式
running_loss = 0.0
running_correct = 0
# 在数据上迭代
for data in dataloaders[phase]:
# 获取输入
inputs, labels = data
# 封装成变量
inputs, labels = Variable(inputs), Variable(labels)
# 梯度参数清零
optimizer.zero_grad()
# 前向
outputs = model(inputs)
_, preds = torch.max(outputs.data, 1)
loss = criterion(outputs, labels)
# 只在训练阶段反向优化
if phase == 'train':
loss.backward()
optimizer.step()
# 统计
running_loss += loss.item()
running_correct += torch.sum(preds == labels.data)
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_correct / dataset_sizes[phase]
print('{} Loss: (:.4f) Acc: (:.4f)'.format(phase, epoch_loss, epoch_acc))
# 深度复刻模型
if phase == 'valid' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = model.state_dict()
print()
time_elapsed = time.time() - since
print('Training Complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed%60))
#加载最优权重
model.load_state_dict(best_model_wts)
return model
上述函数主要实现以下四个功能:
输入参数:
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=2)
代码要点: