Pytorch调研笔记

Pytorch训练调研

首先我们简单说明一下,这么多深度学习框架中,为什么选择PyTorrch呢?

因为PyTorch是当前难得的简洁优雅且高效快速的框架。在笔者眼里,PyTorch达到目前深度学习框架的最高水平。当前开源的框架中,没有哪一个框架能够在灵活性、易用性、速度这三个方面有两个能同时超过PyTorch。下面是许多研究人员选择PyTorch的原因。

1、简洁:PyTorch的设计追求最少的封装,尽量避免重复造轮子。不像TensorFlow中充斥着session、graph、operation、name_scope、variable、tensor、layer等全新的概念,PyTorch的设计遵循tensor→variable(autograd)→nn.Module 三个由低到高的抽象层次,分别代表高维数组(张量)、自动求导(变量)和神经网络(层/模块),而且这三个抽象之间联系紧密,可以同时进行修改和操作。

简洁的设计带来的另外一个好处就是代码易于理解。PyTorch的源码只有TensorFlow的十分之一左右,更少的抽象、更直观的设计使得PyTorch的源码十分易于阅读。

       2、速度:PyTorch的灵活性不以速度为代价,在许多评测中,PyTorch的速度表现胜过TensorFlow和Keras等框架 。框架的运行速度和程序员的编码水平有极大关系,但同样的算法,使用PyTorch实现的那个更有可能快过用其他框架实现的。

       3、易用:PyTorch是所有的框架中面向对象设计的最优雅的一个。PyTorch的面向对象的接口设计来源于Torch,而Torch的接口设计以灵活易用而著称,Keras作者最初就是受Torch的启发才开发了Keras。PyTorch继承了Torch的衣钵,尤其是API的设计和模块的接口都与Torch高度一致。PyTorch的设计最符合人们的思维,它让用户尽可能地专注于实现自己的想法,即所思即所得,不需要考虑太多关于框架本身的束缚。

       4、活跃的社区:PyTorch提供了完整的文档,循序渐进的指南,作者亲自维护的论坛 供用户交流和求教问题。Facebook 人工智能研究院对PyTorch提供了强力支持,作为当今排名前三的深度学习研究机构,FAIR的支持足以确保PyTorch获得持续的开发更新,不至于像许多由个人开发的框架那样昙花一现。

1、   训练需要参数及具体含义

Pytorch的基础部分主要分为以下三个方面:

1.1、Tensor张量

       Tensorflow中数据的核心单元就是Tensor。张量包含了一个数据集合,这个数据集合就是原始值变形而来的,它可以是一个任何维度的数据。tensor的rank就是其维度。

首先,我们需要学会使用PyTorch中的Tensor。Tensor在PyTorch中负责存储基本数据,PyTorch针对Tensor也提供了相对丰富的函数和方法,所以PyTorch中的Tensor与NumPy的数组具有极高的相似性。Tensor是一种高层次架构,也不要明白什么是深度学习,什么是后向传播,如何对模型进行优化,什么是计算图等技术细节。更重要的是,在PyTorch中定义的Tensor数据类型可以在GPU上进行运算,而且只需要对变量做一些简单的类型转换就能轻易实现。

1.2、变量自动求导

       AUTOGRAD自动微分

       PyTorch中所有神经网络的核心是autograd包裹,让我们简单介绍一下。这个autograd软件包为张量上的所有操作提供自动区分。它是一个按运行定义的框架,这意味着您的后端由您的代码运行方式来定义,并且每个迭代都可能是不同的。

自动微分包:torch.autograd

       torch.autograd提供实现任意标量值函数自动微分的类和函数。它需要对现有代码进行最少的更改-您只需声明Tensors,则应使用requires_grad=True关键词。

计算给定张量w,r,t的梯度之和:

torch.autograd.backward(tensorsgrad_tensors=Noneretain_graph=Nonecreate_graph=Falsegrad_variables=None)

计算并返回输出w.r.t的梯度之和:

torch.autograd.grad(outputsinputsgrad_outputs=Noneretain_graph=Nonecreate_graph=Falseonly_inputs=Trueallow_unused=False)

局部禁用梯度计算

上下文管理器,禁用梯度计算:CLASS  torch.autograd.no_grad

启用梯度计算的上下文管理器:CLASS  torch.autograd.enable_grad

上下文管理器,它将梯度计算设置为ON或OFF:

      CLASS  torch.autograd.set_grad_enabled(mode)

张量自梯度函数:CLASS  torch.Tensor

 

Function功能:CLASS  torch.autograd.Function

数值梯度检验:

torch.autograd.gradcheck(funcinputseps=1e-06atol=1e-05rtol=0.001raise_exception=Truecheck_sparse_nnz=False)

等等自动微分函数,详情间官网:https://pytorch.org/docs/stable/autograd.html

tensor对象通过一系列的运算可以组成动态图,对于每个tensor对象,有下面几个变量控制求导的属性。

 

在0.3.0版本中,自动求导还需要借助于Variable类来完成,在0.4.0版本中,Variable已经被废除了,tensor自身即可完成这一过程。

import torch

x = torch.randn((4,4), requires_grad=True)

y = 2*x

z = y.sum()

print z.requires_grad  # True

z.backward()

print x.grad

'''tensor([[ 2.,  2.,  2.,  2.],

        [ 2.,  2.,  2.,  2.],

        [ 2.,  2.,  2.,  2.],

        [ 2.,  2.,  2.,  2.]])

'''

1.3、神经网络层与损失函数优化等高层封装。网络层的封装存在于torch.nn模块,损失函数由torch.nn.functional模块提供,优化函数由torch.optim模块提供

PyTorch与Caffe不同,Caffe中使用Slover创建网络和测试网络,采用Prototxt文件来存储网络结构和训练参数,并将网络参数存储在Caffemodel文件中。在PyTorch中,主要使用torch.nn这个包,这边通过官方手册的一个简单的例子在说明:

import torch.nn as nn

import torch.nn.functional as F

 

class Model(nn.Module):

    def __init__(self):

        super(Model, self).__init__()

        self.conv1 = nn.Conv2d(1, 20, 5)

        self.conv2 = nn.Conv2d(20, 20, 5)

 

    def forward(self, x):

       x = F.relu(self.conv1(x))

       return F.relu(self.conv2(x))

       这个例子定义了一个只有两层的网络Model。其中两个函数:

- 初始化函数 __init__(self)定义了具体网络有什么层,这里实际上没有决定网络的结构,也就是说将上面的例子中的self.conv1和self.conv2定义的前后顺序调换是完全没有影响的。

- forward函数定义了网络的前向传播的顺序。

pytorch中具体支持的不同的层请参考官方手册

下面表格中列出了比较重要的神经网络层组件。对应的在nn.functional模块中,提供这些层对应的函数实现。通常对于可训练参数的层使用module,而对于不需要训练参数的层如softmax这些,可以使用functional中的函数。

 

损失函数与优化方法

torch.nn模块中提供了许多损失函数类,这里列出几种相对常见的。


优化方法

由torch.optim模块提供支持

 

在神经网络的性能调优中,常见的作法是对不对层的网络设置不同的学习率。

class model(nn.Module):

    def __init__():

        super(model,self).__init__()

        self.base = Sequencial()

        # code for base sub module

        self.classifier = Sequencial()

        # code for classifier sub module

 

optim.SGD([

            {'params': model.base.parameters()},

            {'params': model.classifier.parameters(), 'lr': 1e-3}

            ], lr=1e-2, momentum=0.9)

训练需要参数:

在Caffe中,使用solver创建训练网络和测试网络,并采用prototxt文件来存储网络结构。PyTorch和Caffe有所不同,PyTorch中主要使用torch.nn模块来创建网络模型。

 

PyTorch训练时,将一个不可训练的类型Tensor转换成可以训练的类型parameter并将这个parameter绑定到这个module里面(net.parameter()中就有这个绑定的parameter,所以在参数优化的时候可以进行优化的),所以经过类型转换这个self.v变成了模型的一部分,成为了模型中根据训练可以改动的参数了。使用这个函数的目的也是想让某些变量在学习的过程中不断的修改其值以达到最优化。

 

2、   训练需要数据格式

2.1、DataSet与DataLoader

torch.util.data模块提供了DataSet类用于描述一个数据集。定义自己的数据集需要继承自DataSet类,且实现__getitem__()与__len__()方法。__getitem__方法返回指定索引处的tensor与其对应的label。

为了支持数据的批量及随机化操作,可以使用data模块下的DataLoader类型来返回一个加载器:

DataLoader(dataset,batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0)

2.2、torchvision简介

torchvision是配合pytorch的独立计算机视觉数据集的工具库,下面介绍其中常用的数据集类型。

torchvision.datasets.ImageFolder(dir, transform, label_map,loader)

提供了从一个目录初始化出来一个图片数据集的便捷方法。

要求目录下的图片分类存放,每一类的图片存储在以类名为目录名的目录下,方法会将每个类名映射到唯一的数字上,如果你对数字有要求,可以用label_map来定义目录名到数字的映射。

torchvision.datasets.DatasetFolder(dir,transform, label_map, loader, extensions)

提供了从一个目录初始化一般数据集的便捷方法。目录下的数据分类存放,每类数据存储在class_xxx命名的目录下。

此外torchvision.datasets下实现了常用的数据集,如CIFAR-10/100, ImageNet, COCO, MNIST, LSUN等。

除了数据集,torchvision的model模块提供了常见的模型实现,如Alex-Net, VGG,Inception, Resnet等。

2.3、torchvision提供的图像变换工具

torchvision的transforms模块提供了对PIL.Image对象和Tensor对象的常见操作。如果需要连续应用多个变换,可以使用Compose对象组装多个变换。

PyTorch训练需要数据格式:

       在Caffe中,生成lmda格式的文件创建数据集。PyTorch和Caffe有所不同,PyTorch中主要使用Dataset和DataLoaderd预处理和加载数据集。

Dataset是PyTorch中图像数据集中最为重要的一个类,也是PyTorch中所有数据集加载类中应该继承的父类。其中父类中的两个私有成员函数必须被重载,否则会出发错误提示:

def getitem(self, index):

def len(self):

其中_len_应该返回数据集的大小,而_getitem_应该编写支持数据集索引的函数,例如通过dataset[i]可以得到数据集中的第i+1个数据。

现在我们开始定义一个自己的数据集类:

# 假设下面这个类是读取船只的数据类

class ShipDataset(Dataset):

       # root:图像存放的地址根路径

       # agument:是否需要图像增强

       def  _init_ (self, root, augment=None):

              # 这个list存放所有图像的地址

              self.image_files = np.array([x.path for x in os.scandir(root) if

                     x.name.endswith(“.jpg”) or x.name.endswith(“.png”)])

              self.augment = augment # 是否需要图像增强

       def _getitem_ (self, index):

              # 读取图像数据并返回

              # 这里的open_image是读取图像函数,可用PIL,opencv等库读取

              return open_image(self.image_files[index])

       def _len_ (self):

              # 返回图像的数量

              return len(self.image_files)

如果我们需要在读取数据的同时对图像进行增强的话,可以在__getitem__(self, index)函数中设置图像增强的代码,例如

              def _getitem_(self, index):

                     if self.augment:

                            image = open_image(self.image_files[index])

                            image = self.augment(image) # 这里对图像进行了增强

                            return image

                     else:

                            # 如果不进行增强,直接读取图像数据并返回

                            # 这里的open_image是读取图像函数,可用PIL等库读取

                            return open_image(self.image_files[index])

当然,图像增强的方法可以使用Pytorch内置的图像增强方式,也可以使用自定义或者其他的图像增强库。这个很灵活,当然要记住一点,在Pytorch中得到的图像必须是tensor,也就是说我们还需要再修改一下__getitem__(self, index):

              def _getitem_ (self, index):

                     if self.augment:

                            image = open_image(self.image_files[index])

                            image = self.augment(image) # 这里对图像进行了增强

                            return to_tensor(image)    # 将读取到的图像变成tensor再传出

                     else:

                            # 如果不进行增强,直接读取图像数据并返回

                            # 这里的open_image是读取图像函数,可用PIL等库读取

                            return to_tensor(open_image(self.image_files[index]))

这样,一个基本的数据类就设计好了

DataLoader

       之前所说的Dataset类是读入数据集数据并且对读入的数据进行了索引。但是光有这个功能是不够用的,在实际的加载数据集的过程中,我们的数据量往往都很大,对此我们还需要一下几个功能:

l  可以分批次读取:batch-size

l  可以对数据进行随机读取,可以对数据进行洗牌操作(shuffling),打乱数据集内数据分布的顺序

l  可以并行加载数据(利用多核处理器加快载入数据的效率)

这时候就需要Dataloader类了,Dataloader这个类并不需要我们自己设计代码,我们只需要利用DataLoader类读取我们设计好的ShipDataset即可:

# 利用之前创建好的ShipDataset类去创建数据对象

ship_train_dataset = ShipDataset(data_path, augment=transform)

# 利用dataloader读取我们的数据对象,并设定batch_size和工作现场

Ship_train_loader = DataLoader(ship_train_dataset, batch_size=16, num_workers=4, shuffle=Fals, **kwargs)

这时候通过ship_train_loader返回的数据就是按照batch-size来返回特定数量的训练数据的tensor,而且此时利用了多线程,读取数据的速度相比单线程快很多。

我们这样读取:

For image in train_loader:

       Image = image.to(device) # 将tensor数据移动到device中

       Optimizer.zero_grad()

       Output = model(image) # model模型处理(n,c,h,w)格式的数据,n为batch_size

读取数据的基本模式就是这样,当然在实际中不可能这么简单,我们除了图像数据可能还有json、csv等文件需要我们去读取配合图像完成任务。但是原理基本都是一样的,具体复杂点的例子可以查看官方的例程介绍

3、   参数初始化方式

参数的初始化其实就是对参数赋值。而我们需要学的的参数都是Variable,它其实是对Tensor的封装,同时提供了data,grad等接口,这就意味着我们可以直接对这些参数进行操作赋值了。这就是PyTorch简洁高效所在。

PyTorch提供了多种参数初始化函数:

torch.nn.init.constant(tensor, val)

torch.nn.init.normal(tensor, mean=0, std=1)

torch.nn.init.xavier_uniform(tensor, gain=1)

等等。详细请参考:http://pytorch.org/docs/nn.html#torch-nn-init

 

注意上面的初始化函数的参数tensor,虽然写的是tensor,但是也可以是Variable类型的。而神经网络的参数类型Parameter是Variable类的子类,所以初始化函数可以直接作用于神经网络参数。

示例:

self.conv1=nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3)

init.xavier_uniform(self.conv1.weight)

init.constant(self.conv1.bias, 0.1)

 

上面的语句是对网络的某一层参数进行初始化。如何对整个网络的参数进行初始化定制呢?

def weights_init(m):

    classname=m.__class__.__name__

    if classname.find('Conv') != -1:

        xavier(m.weight.data)

        xavier(m.bias.data)

net = Net()

net.apply(weights_init) 

#apply函数会递归地搜索网络内的所有module

并把参数表示的函数应用到所有的module上。

不建议访问以下划线为前缀的成员,他们是内部的,如果有改变不会通知用户。更推荐的一种方法是检查某个module是否是某种类型:

def weights_init(m):

    if isinstance(m, nn.Conv2d):

        xavier(m.weight.data)

        xavier(m.bias.data) 

4、   训练的完整流程

 

这是一个简单的前馈网络,它接受输入,一个接一个地通过几个层输入,然后最后给出输出。

神经网络的典型训练过程如下:

l  定义具有一些可学习参数(或权重)的神经网络

l  迭代输入数据集

l  通过网络处理输入

l  计算损失(输出离正确有多远)

l  将梯度传播回网络参数

l  更新网络的权重,通常使用简单的更新规则:

weight=weight-learning_rate*gradient

 

 

定义网络

 

您只需要定义forward函数,以及backward函数(在其中计算梯度)将自动为您定义autograd。您可以在forward功能模型的可学习参数由net.parameters()

params = list(net.parameters())

print(len(params))

print(params[0].size())  # conv1's .weight

我们还可以尝试一个随机的32*32输入。

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

所有参数和具有随机梯度的后端的梯度缓冲区为零:

net.zero_grad()
out.backward(torch.randn(1, 10))

计算损失

损失函数接受(输出,目标)对输入,并计算一个值,该值估计输出离目标有多远。有几种不同的损失函数在NN包裹下。一个简单的损失是:nn.MESLoss计算输入和目标之间的均方误差。

例如:

output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()
 
loss = criterion(output, target)
print(loss)

现在,如果你跟着losss在向后的方向上,使用它的.grad_fn属性,您将看到如下所示的计算图标:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> view -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

所以,当我们打电话loss.backword(),整个图被区分为w,r,t。的损失,以及图中的所有张量requires_grad=True会有他们的.grad张量随梯度累计。

Backprop

要反向传播错误,我们索要做的就是loss.backward()。您需要清楚现有的梯度,否则梯度将累计到现有的梯度。现在我们要使用loss.backward(),并看一看前后的偏压梯度。

net.zero_grad()     # zeroes the gradient buffers of all parameters
 
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
 
loss.backward()
 
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

至此,我们可以了解如何使用损失函数计算误差。

更新权重

我们可以使用如下代码实现这个功能:

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

然而,在使用神经网络时,您需要使用各种不同的更新规则,如SGD、Nesterov-SGD、ADAM、RMSProp等。为此,我们构建了一个小包:torch.optim实现了所有这些方法。

import torch.optim as optim
 
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
 
# in your training loop:
optimizer.zero_grad()   # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Does the update

注:

       观察如何手动将渐变缓冲区设置为零。Optimizer.zero_grad()。这是梯度按照backprop部分的。

5、   是否支持分布式训练

5.1、什么是分布式计算

      分布式计算

       指的是一种编写程序的方式,它利用网络中多个连接的不同组件。通常,大规模计算通过以这种方式布置计算机来实现,这些计算机能够并行地处理高密度的数值运算。在分布式计算的术语中,这些计算机通常被称为节点(node),这些节点的集合就是集群。这些节点一般是通过以太网连接的,但是其他的高带宽网络也可以利用分布式架构的优势。

5.2、分布式计算框架

       随着大数据时代的来临,现有计算方式已不能满足工作需求,并且CPU近几年往多核方面发展,单台电脑性能有限不足以完成复杂计算任务。分布式计算框架能很好的解决此类需要巨大计算量问题,分布式计算框架允许使用商用服务器组成一个计算集群并提供一个并行计算软件框架,服务器之间的通信、负载均衡、任务计算与处理、任务储存等复杂的操作都交由系统自动处理,减少了软件开发人员的负担。

系统是一个基本C++平台的分布式计算框架,使用了Digia公司开发工具QT5.10;任务储存使用了开源数据库MYSQL。该分布式框架实现了动态链接库的上传、任务文件上传、任务文件解析、计算任务的分发、计算任务的计算、计算结果的储存、计算结果的汇总等功能。在分配任务过程中,本框架优先分配给最先执行完任务的计算节点,高效利用计算资源。当计算节点断开连接时,本框架会自动将其任务重置,并分发给其他计算节点。使用了任务队列与任务报错等容错手段保证了分布式计算框架的稳定运行。

用户编写模板对应函数Read、Handle、Sum,生成动态链接库后上传至服务器中,随后将任务文件上传即可完成整个分布式计算过程。服务器调用Read函数对任务文件进行任务读取与分解,随后将分解任务分配至空闲计算节点,计算节点调用Handle函数进行任务计算,计算完毕后回传任务结果至服务器,当整个计算任务完成时服务器调用Sum函数进行任务的汇总,最后完成分布式任务的计算。

系统结构大致如下:

 

5.3、PyTorch分布式训练

如果需要在短时间内完成大量数据的训练,处理较大的Batch_size时,使用传统的模型训练时间就会很慢,但是PyTorch满足分布式计算的需求。在PyTorch中分布式分为两种,一种是一机多卡,另一种是多机多卡,即指用多个GPU跑PyTorch,可能是一个机器上的多个GPU,也可能是多个机器上,每个机器上有若干个GPU。

分布式PyTorch,主要是Pytorch在v0.2中发布的一个分布式通信包torch.distributed,我们可以使用import torch.distributed as dist导入使用,分布式Pyrorch允许您在多台机器之间交换Tensors。使用此软件包,您可以通过多台机器和更大的小批量扩展网络训练。例如,您将获得实现精准大型小型SGD:1小时训练ImageNet的模型。此外,python在默认情况下只使用一个GPU,在多个GPU的情况下就需要使用PyTorch提供的DataPaeallel包。

分布式训练主要分为以下几个步骤:

  1. 1.       初始化及相关参数解释

torch.distributed.init_process_group(backendinit_method=Nonetimeout=datetime.timedelta(01800)world_size=-1rank=-1store=Nonegroup_name='')

参数说明:

backend(str): 要使用的后端。根据构建时设置,有效值包括MPI,gloo和NCCL。此字段应作为小写字符串(例如,“gloo”),也可通过Backend属性(例如,Backend. GLOO)。如果每台机器上使用多个线程NCCL后端,每个进程必须对它使用的每个GPU具有独占访问权,因为每台进程之间共享GPU可能导致死锁。

init_method(str,optional): 指定如何初始化进程组的URL。默认值为“env:/”,如果没有init_method或store被指定。互斥store。

world_size(int, optional):参与这个工作的进程数目。如store被指定

rank(int,optional): 当前进程的优先级。如store被指定。

Store(store,optional):所有工作人员均可访问的密钥/值存储,用于交换连接/地址信息。互斥init_method。

group_name(str,optional): 用来标记这组进程名的

:PyTorch目前只支持Linux,其中torch.distributed只支持三个后端,GLOO、MPI、NCCL。那么如何选择用哪个后端呢,默认情况下,GLOO和NCCL后端构建并包含在PyTorch分布式中(NCCL仅在使用CUDA构建时使用)。MPI是一个可选的后端,只有在从源代码构建PyTorch时才能包含它。(例如,在安装了MPI的主机上构建PyTorch。)

注:MPL:分布式计算标准。是消息传递接口。

经验法则:

  • 使用NCCL后端进行分布式GPU培训
  • 使用GLOO侯丹进行分布式CPU训练

目前支持三种初始化方法:

  • file:// 共享文件系统(要求所有进程可以访问单个文件系统)有共享文件系统可以选择
  • tcp:// 这两种方法都需要可以从所有进程访问的网络地址和所需的网络地址。world_size。第一种方法要求指定属于秩0进程的地址,不过需要手动设置rank
  • env:// 环境变量(需要您手动分配等级并知道所有进程可访问节点的地址)这是默认的方法。
  1. 2.       分布式数据并行

并行策略主要两种类型:模型并行和数据并行

数据并行

数据并行指的是,通过位于不同硬件/设备上的同一个网络的多个副本来处理数据的不同批(batch)。不同于模型并行,每个副本可能是整个网络,而不仅仅是一部分。这种并行策略随着数据的增长可以很好地扩展,但是由于整个网络必须部署再一个设备上,因此可能无法帮助到具有高内存占用的模型。

 

实际上,在大组织里,为了执行生产质量的深度学习训练算法,数据并行更加流行也更加常用。

PyTorch提供了一个非常优雅并且易于使用的API,就是torch.distributed。作为用 C 语言写的底层 MPI 库的接口。PyTorch 需要从源码编译,并且必须与安装在系统中的 Intel MPI 进行链接。

分布式数据并行(DDP)在模块级实现数据并行。它使用torch.distributed包来同步渐变、参数和缓冲区。并行性在进程内部和跨进程都是可用的。在进程中,ddp将输入模块复制到device_ids,相应地沿批处理维度分散输入,并将输出收集到output_device,类似于数据并行。在跨进程中,DDP在前送中插入必要的参数同步,在后送中插入渐变同步。只要进程不共享GPU设备,就由用户将进程映射到可用资源。推荐的(通常是最快的)方法是为每个模块副本创建一个流程,即在进程中不进行模块复制。

设置Setup:

包含在PyTorch中的分布式包(即,torch.distributed)使研究人员和实践者能够轻松地在进程和机器集群中并行化他们的计算。为此,它利用消息传递语义,允许每个进程将数据通信到任何其他进程。而不是多处理(torch.multiprocessing)包,进程可以使用不同的通信后端,而不限于在同一台机器上执行。

为了开始,我们需要同时运行多个进程的能力。如果您可以访问计算集群,则应该与本地sysadmin检查或使用您最喜欢的协调工具。(例如,pdsh,clustershell或者others)为了本教程的目的,我们将使用以下模板使用单个机器和分叉多个进程。

 

这个脚本生成两个进程,每个进程将设置分布式环境,初始化进程组(dist.init_process_group),并最终执行给定的run功能。

点对点通信:

 

从一个进程到另一个进程的数据传输称为点对点通信.这些都是通过send和recv职能或其立即反零件,isend和irecv. 当我们希望对流程的通信进行细粒度控制时,点对点通信非常有用。

集体交流:

 

与点对点通信不同,集体允许跨所有进程的通信模式。因为我们想要群中所有张量的和,所以我们使用dist.reduce_op.SUM作为精简操作符。一般说来,任何可交换的数学运算都可以用作算子。

分布式训练:

简单地说,我们想要实现一个随机梯度下降的分布式版本。我们的脚本将允许所有进程在其批数据上计算其模型的梯度,然后平均它们的梯度。为了确保在更改进程数时类似的收敛结果,我们首先必须对数据集进行分区。

 

假设我们有两个副本,那么每个进程将有一个train_set60000/2=30000个样品。我们还将批处理大小除以副本的数量,以维护总体批次大小为128。我们现在可以编写我们通常的前、后向优化训练代码,并添加一个函数调用来平均我们模型的梯度。

 

至此,仍然需要实现average_gradients(model)函数,它简单地接受一个模型并平均它在整个世界的梯度

 

现在我们就成功地实现了分布式同步SGD,可以在大型计算机集群上对任何模型进行训练。

  1. 3.       GPU并行方式

通过对模型进行并行GPU处理(这里一般指单机多卡),可以相对提高处理速度,但是处理方法大致有两种

将Module放在GPU上运行也十分简单,只需两步:

model = model.cuda():将模型的所有参数转存到GPU

input.cuda():将输入数据也放置到GPU上

至于如何在多个GPU上并行计算,PyTorch也提供了两个函数,可实现简单高效的并行GPU计算。

①nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)

②class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

可见二者的参数十分相似,通过device_ids参数可以指定在哪些GPU上进行优化,output_device指定输出到哪个GPU上。唯一的不同就在于前者直接利用多GPU并行计算得出结果,而后者则返回一个新的module,能够自动在多GPU上进行并行加速。

DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,各个GPU得到的梯度累加。与Module相关的所有数据也都会以浅复制的方式复制多份,在此需要注意,在module中属性应该是只读的。

并行处理机制是,首先将模型加载到主 GPU 上,然后再将模型复制到各个指定的从 GPU 中,然后将输入数据按 batch 维度进行划分,具体来说就是每个 GPU 分配到的数据 batch 数量是总输入数据的 batch 除以指定 GPU 个数。每个 GPU 将针对各自的输入数据独立进行 forward 计算,最后将各个 GPU 的 loss 进行求和,再用反向传播更新单个 GPU 上的模型参数,再将更新后的模型参数复制到剩余指定的 GPU 中,这样就完成了一次迭代计算。所以该接口还要求输入数据的 batch 数量要不小于所指定的 GPU 数量。

 

注意:

  • 在一个或多个 GPU 上训练大批量模型: 梯度累积
  • 充分利用多 GPU 机器:torch.nn.DataParallel
  • 多 GPU 机器上的均衡负载 : PyTorch-Encoding 的 PyTorch 包,包括两个模块:DataParallelModel 和 DataParallelCriterion
  • 分布式训练:在多台机器上训练: PyTorch 的 DistributedDataParallel
  • Pytorch 的多 GPU 处理接口是 torch.nn.DataParallel(module, device_ids),其中 module 参数是所要执行的模型,而 device_ids 则是指定并行的 GPU id 列表。

l  Pytorch 的多 GPU 处理接口是 torch.nn.DataParallel(module, device_ids),其中 module 参数是所要执行的模型,而 device_ids 则是指定并行的 GPU id 列表。

6、   backward时具体如何更新参数

6.1、loss计算和反向传播

import torch.nn as nn

 

criterion = nn.MSELoss().cuda()

output = model(input)

loss = criterion(output, target)

loss.backward()

通过定义损失函数:criterion,然后通过计算网络真是输出和真是标签之间的误差,得到网络的损失值:loss;

最后通过loss.backward()完成误差的反向传播,通过pytorch的内在机制完成自动求导得到每个参数的梯度。

需要注意,在机器学习或者深度学习中,我们需要通过修改参数使得损失函数最小化或最大化,一般是通过梯度进行网络模型的参数更新,通过loss的计算和误差反向传播,我们得到网络中,每个参数的梯度值,后面我们再通过优化算法进行网络参数优化更新。

6.2、网络参数更新

在更新网络参数时,我们需要选择一种调整模型参数更新的策略,即优化算法。

优化算法中,简单的有一阶优化算法:

其中就是通常说的学习率,是函数的梯度;

自己的理解是,对于复杂的优化算法,基本原理也是这样的,不过计算更加复杂。

在pytorch中,torch.optim是一个实现各种优化算法的包,可以直接通过这个包进行调用。

optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

注意:

1)在前面部分1中,已经通过loss的反向传播得到了每个参数的梯度,然后再本部分通过定义优化器(优化算法),确定了网络更新的方式,在上述代码中,我们将模型的需要更新的参数传入优化器。

 2)注意优化器,即optimizer中,传入的模型更新的参数,对于网络中有多个模型的网络,我们可以选择需要更新的网络参数进行输入即可,上述代码,只会更新model中的模型参数。对于需要更新多个模型的参数的情况,可以参考以下代码:

optimizer=torch.optim.Adam([{'params':model.parameters()},{'params': gru.parameters()}], lr=0.01)

 3) 在优化前需要先将梯度归零,即optimizer.zeros()。

6.3、loss计算和参数更新

import torch.nn as nn

import torch

criterion = nn.MSELoss().cuda()

optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

output = model(input)

loss = criterion(output, target)

optimizer.zero_grad()  # 将所有参数的梯度都置零

loss.backward()        # 误差反向传播计算参数梯度

optimizer.step()       # 通过梯度做一步参数更新

posted @ 2020-01-21 15:15  GangTaoWang  阅读(950)  评论(0编辑  收藏  举报