参考资料:
- 《Pytorch深度学习》(人民邮电出版社)第三章 深入了解神经网络 《Pytorch深度学习》(人民邮电出版社)第四章 机器学习基础
- Pytorch官方文档
- 其他有参考的资料都在文章中以超链接的形式给出啦
目录
-
- 0 写在前面
- 1 神经网络的组成部分
- 1.1 层
- 1.2 非线性激活函数
- 2 利用Pytorch构建深度学习框架
- 2.1 数据预处理与特征工程
- 2.2 如何决定要使用的层?
- 2.3 损失函数
- 2.4 优化器的选择
- 2.5 评估机器学习模型
- 2.6 模型的选择
- 3 案例实践——猫狗图像分类
- 3.1 数据集的建立(训练集+验证集)
- 3.2 数据预处理(图片数据转换成PyTorch张量)
- 3.3 批量加载PyTorch张量
- 3.4 构建网络架构
- 3.5 训练模型
0 写在前面
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()
beforeoptimizer.step()
.”这样的错误。可以参考这篇文章解决。 pytorch安装相关 conda安装Pytorch下载过慢解决办法
1 神经网络的组成部分
训练深度学习算法需要的几个步骤:
- 构建数据管道
- 构建网络架构
- 使用损失函数评估架构
- 使用优化算法优化网络架构的权重
当运用神经网络去处理一些比较复杂的问题时,神经网络的架构就会变得特别复杂。为此,诸如pytorch、tensorflow这样的深度学习框架都对一些复杂的高级功能进行了抽象,方便使用者的使用。 抽象出底层的运算并训练深度学习算法的过程如下图所示
1.1 层
层(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
1.2 非线性激活函数
我们知道,神经网络每一层的的输出应该是 z=g(Wx+b),其中g(*)为非线性的激活函数。Pytorch里面也提供了一些非线性的激活函数可以使用。
- sigmoid函数. sigmoid函数应该是大家最熟悉的非线性函数之一。它的数学定义很简单:
σ(x)=1/(1+e^-x^)
- tanh函数
- ReLU ReLU最近变得很受欢迎,它的数学公式也很简单:
f(x)=max(0, x)
线性整流函数(Rectified Linear Unit, ReLU),又称修正线性单元,是一种人工神经网络中常用的激活函数(activation function),通常指代以斜坡函数及其变种为代表的非线性函数。
ReLU函数有助于优化器更快地找到正确的权重集合,可以使随机梯度下降收敛得更快;而且计算成本更低,因为只需要设置一个阈值。但是当一个很大的梯度进行反向传播时,会出现一些无效神经元。对于这些无效神经元,可以通过调整学习率来控制。
- Leaky ReLU
Pytorch里面的非线性激活函数的使用:
import torch
import torch.nn as nn
m = nn.ReLU()
inp = torch.randn(2)
output = m(inp)
print(output)
2 利用Pytorch构建深度学习框架
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方法中实现。
2.1 数据预处理与特征工程
数据预处理的目的是减小数据尺度等对深度学习算法训练的影响,主要包括以下数据处理步骤:
- 向量化 向量化主要是将各种格式(文本、声音、图片、视频等)的数据转换成PyTorch张量。
- 归一化 归一化是指将特定特征的数据表示成均值为0、标准差为1的数据的过程,有助于更快地训练算法并达到更高的性能。主要有两种归一化方法:
(1)min-max标准化(Min-Max Normalization) 也称为离差标准化,是对原始数据的线性变换,使结果值映射到[0 – 1]之间。 (2)Z-score标准化方法 这种方法给予原始数据的均值(mean)和标准差(standard deviation)进行数据的标准化。经过处理的数据符合标准正态分布,即均值为0,标准差为1。
- 特征提取 深度学习算法可以使用大量的数据自己学习出特征,不再使用手动的特征工程。
2.2 如何决定要使用的层?
要使用哪些层通常由具体的机器学习来决定。机器学习问题中,通常要解决的问题有三类,无论前面的层如何设计,最后一层通常是确定的:
- 对于回归问题,最后使用的是有一个输出的线性层,输出值是连续的;
- 对于二分类问题,通常最后一层会使用sigmoid激活函数。
- 对于多分类问题,网络最后会使用softmax层。softmax的数学意义在于将概率和分类进行挂钩,输入概率值最大的分类。
2.3 损失函数
定义好了网络架构,还剩下了最重要的两步——评估和优化。 评估神经网络通常会利用损失函数,一般来说,损失函数越小,模型越好。损失函数的梯度可以对模型的参数进行优化。 PyTorch提供了一些可用的损失函数:
在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()所需参数含义
2.4 优化器的选择
计算出网络的损失函数之后,需要对网络的参数进行优化以减少损失。PyTorch提供了很多优化器,这些优化器可以看成是一个黑盒子,他们接受损失函数和所有的学习参数,并微量调整来改善网络性能。这些可以在PyTorch官方文章中的torch.optim里面找到。
2.5 评估机器学习模型
为了评估机器学习模型,通常会把数据集分成3个不同的部分——训练集、验证集、测试集。训练集和验证集用于训练算法并调优所有超参数;测试集用于评估模型的泛化能力。 训练集和测试集的划分通常由3种策略:简单保留验证、K折验证和迭代K这验证。在拆分数据时,需要考虑数据代表性、时间敏感性和数据冗余,这三种特性本质上都是体现样本的分布。
2.6 模型的选择
要使模型能够工作,有三个选择至关重要:
- 最后一层的选择(激活函数的选择)和损失函数的选择 对于不同的机器学习问题,激活函数的选择和损失函数的选择可以概括成下表:
- 优化器的选择
- 学习率的选择 如何找到适合模型的学习率目前还是一个正在研究的课题,不过PyTorch提供了一些工具来动态选择学习率(torch.optim.lr_scheduler): StepLR:StepLR有两个重要的参数,第一个参数step_size是步长,它表示学习率多少轮改变一次;第二个参数是gamma,他决定学习率必须改变多少,例如这一次的学习率是0.001,那么经过30轮后,学习率变成0.001*gamma=0.0001。
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)
3 案例实践——猫狗图像分类
光说不练假把式,我们下面就通过一个案例来巩固一下学习到的东西吧。 数据集:Dogs vs. Cats。数据集有个文件夹,一个是train(训练数据集),一个是test(测试集)。如果官网下载很慢的话,这里指路一个百度网盘的链接:【猫狗数据集】使用预训练的resnet18模型。在训练集中,有猫和狗的照片各12500张,每一张都通过文件名打标签:
3.1 数据集的建立(训练集+验证集)
首先是数据的分类,将数据集分为训练集(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)
3.2 数据预处理(图片数据转换成PyTorch张量)
数据预处理的目的是将图片加载成PyTorch张量。PyTorch的torchvision.datasets包提供了一个名为ImageFolder的工具类,可以用于加载图片以及相应的标签。
参数解释: root:指定图片存储的路径 transform: 一个函数,原始图片作为输入,返回一个转换后的图片。 target_transform – 一个函数,输入为target,输出对其的转换。例子,输入的是图片标注的string,输出为word的索引。
预处理通常会经过以下三个步骤:
- 把所有的图片转换成同等大小,希望图片具有相同的尺寸;
- 用数据集的均值和标准差把数据集归一化;
- 把图片数据集转换成PyTorch张量
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])
输出图片:
3.3 批量加载PyTorch张量
在深度学习或者机器学习中把图片进行批量取样是一个常用的方法,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深入剖析 文章中也给了一些参数的介绍:
- dataset(Dataset):传入的数据集
- batch_size(int, optional):每个batch有多少个样本
- shuffle(bool, optional):在每个epoch开始的时候,对数据进行重新排序
- sampler(Sampler, optional):自定义从数据集中取样本的策略,如果指定这个参数,那么shuffle必须为False
- batch_sampler(Sampler,optional):与sampler类似,但是一次只返回一个batch的indices(索引),需要注意的是,一旦指定了这个参数,那么batch_size,shuffle,sampler,drop_last就不能再制定了(互斥——Mutually exclusive)
- num_workers (int, optional):这个参数决定了有几个进程来处理data loading。0意味着所有的数据都会被load进主进程。(默认为0)
- collate_fn (callable, optional):将一个list的sample组成一个mini-batch的函数
- pin_memory (bool, optional):如果设置为True,那么data loader将会在返回它们之前,将tensors拷贝到CUDA中的固定内存(CUDA pinned memory)中.
- drop_last (bool, optional): 如果设置为True,这个是对最后的未完成的batch来说的,比如你的batch_size设置为64,而一个epoch只有100个样本,那么训练的时候后面的36个就被扔掉了…如果为False(默认),那么会继续正常执行,只是最后的batch_size会小一点。
- timeout(numeric, optional):如果是正数,表明等待从worker进程中收集一个batch等待的时间,若超出设定的时间还没有收集到,那就不收集这个内容了。这个numeric应总是大于等于0。默认为0。
- worker_init_fn (callable, optional): 每个worker初始化函数 If not None, this will be called on each worker subprocess with the worker id (an int in [0, num_workers – 1]) as input, after seeding and before data loading. (default: None)
3.4 构建网络架构
对于计算机视觉中的大多数案例,我们可以使用已有的不同架构来解决实际的问题。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次,对应的学习率就会按照策略调整一次。
3.5 训练模型
我们首先来看一下训练模型的整体代码,再详细进行解读。下面的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:构建好的神经网络框架
- criterion:损失函数
- optimizer:构建的优化器
- scheduler:学习率的修改
- num_epochs = 25:循环次数
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=2)
代码要点:
- optimizer.step()和scheduler.step()的区别 optimizer.step()通常用在每个mini-batch之中,而scheduler.step()通常用在epoch里面,但是不绝对,可以根据具体的需求来做。只有用了optimizer.step(),模型才会更新,而scheduler.step()是对lr进行调整。 在scheduler的step_size表示scheduler.step()每调用step_size次,对应的学习率就会按照策略调整一次。所以如果scheduler.step()是放在mini-batch里面,那么step_size指的是经过这么多次迭代,学习率改变一次。
- pytorch 状态字典:state_dict pytorch 中的 state_dict 是一个简单的python的字典对象,将每一层与它的对应参数建立映射关系.(如model的每一层的weights及偏置等等)。注意,只有那些参数可以训练的layer才会被保存到模型的state_dict中,如卷积层,线性层等等。优化器对象Optimizer也有一个state_dict,它包含了优化器的状态以及被使用的超参数(如lr, momentum,weight_decay等)