正值 ICML 2017 期间,我们发行了下一代主版本 PyTorch V0.2.0,现在你可从官网 http://pytorch.org 安装它,该版本的软件包文档可从 http://pytorch.org/docs/0.2.0/ 获取 。PyTorch V0.2 新增了期待已久的功能,比如广播、高级索引、高阶梯度以及最重要的分布式 PyTorch。
由于引入了广播功能,特定可广播情景的代码行为不同于 V0.1.12 中的行为。这可能导致你的现有代码中存在不易发现的误差。在「重要的破损量与工作区」这一章节中,我们将提供识别这些模糊代码的简单方法。
- 张量广播(numpy 风格)
- 张量和变量的高级索引
- 高阶梯度(Higher-order gradients)
- 分布式 PyTorch(多结点训练等)
- 神经网络层与特征: SpatialTransformers、WeightNorm、EmbeddingBag 等
- torch 和 autograd 中的新东西:矩阵乘法、矩阵的逆等
- 更简单的调试,更好的错误信息
- 漏洞修复
- 重要的破损量和工作区
张量广播(numpy 风格)
简单来说,如果一个 PyTorch 操作支持广播,那么其张量参数可自动扩展为相同大小(无需拷贝数据)。 PyTorch 广播的语义严格遵守 numpy 风格的广播;如果你很熟悉 numpy 广播,事情就会按照你的预期发展。
通用语义
如果以下规则成立,则两个张量是「可广播的」:
- 每个张量至少有一个维度。
- 当开始迭代维度大小时,从后面的维度开始,维度大小必须相同,它们中的一个是 1,或者其中一个不存在
例如:
>>> x=torch.FloatTensor(5,7,3)
>>> y=torch.FloatTensor(5,7,3)
# same shapes are always broadcastable (i.e. the above rules always hold)
# can line up trailing dimensions
>>> x=torch.FloatTensor(5,3,4,1)
>>> y=torch.FloatTensor( 3,1,1)
# x and y are broadcastable.
# 1st trailing dimension: both have size 1
# 2nd trailing dimension: y has size 1
# 3rd trailing dimension: x size == y size
# 4th trailing dimension: y dimension doesn't exist
# but:>>> x=torch.FloatTensor(5,2,4,1)
>>> y=torch.FloatTensor( 3,1,1)
# x and y are not broadcastable, because in the 3rd trailing dimension 2 != 3
如果两个张量 x、y 是可广播的,得到的张量大小计算如下:
如果维度 x 和 y 的数量不相同,则把带有更少维度的张量的维度设为 1 以使它们的长度相等。
接着,对于每一个维度大小,所得到的维度大小是该维度上的 x 和 y 大小的最大值。
例如:
# can line up trailing dimensions to make reading easier>>> x=torch.FloatTensor(5,1,4,1)>>> y=torch.FloatTensor( 3,1,1)>>> (x+y).size()
torch.Size([5, 3, 4, 1])
# error case>>> x=torch.FloatTensor(5,2,4,1)>>> y=torch.FloatTensor( 3,1,1)>>> (x+y).size()RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1
更多细节请参见 PyTorch 文档网站 http://pytorch.org/docs/0.2.0/notes/broadcasting.html。同样,每个 torch 函数列举了其在文档中的广播语义。
张量和变量的高级索引
PyTorch 现在支持 NumPy 风格的高级索引的一个子集,这允许用户使用相同的
[]风格的操作在张量的每一个维度上选择任意的索引,包括非相邻索引和重复索引;这也使得无需调用 PyTorch
Index[Select, Add, ...]函数即可获得一个更加灵活的索引策略。
让我们看一些实例:
x = torch.Tensor(5, 5, 5)
纯整数组索引 - 在每个维度上指定任意的索引
x[[1, 2], [3, 2], [1, 0]]--> yields a 2-element Tensor (x[1][3][1], x[2][2][0])
同样支持广播、复制
x[[2, 3, 2], [0], [1]]--> yields a 3-element Tensor (x[2][0][1], x[3][0][1], x[2][0][1])
允许任意的分度器(indexer)形状
x[[[1, 0], [0, 1]], [0], [1]].shape--> yields a 2x2 Tensor [[x[1][0][1], x[0][0][1]],
[x[0][0][1], x[1][0][1]]]
可以使用冒号、椭圆
x[[0, 3], :, :]
x[[0, 3], ...]--> both yield a 2x5x5 Tensor [x[0], x[3]]
也可以使用张量来索引!
y = torch.LongTensor([0, 2, 4])
x[y, :, :]--> yields a 3x5x5 Tensor [x[0], x[2], x[4]]
选择小于 n维,请注意逗号的使用。
x[[1, 3], ]--> yields a 2x5x5 Tensor [x[1], x[3]]
高阶梯度
现在你可以评估 PyTorch 中的高阶微分。例如,你可以计算 Hessian-Vector积,以模型的梯度的范数为罚项,实现展开的 GAN 和提升的 WGAN 等。在0.2版本中,我们使得所有torch.XXX
函数和最流行的n层具备了计算高阶梯度的能力。其余的将会在下一个版本中介绍。
下面是一个简短的实例,它以 Resnet-18 模型权重梯度的范数为罚项,因此权重数量的变化比较缓慢。
import torchfrom torchvision.models import resnet18from torch.autograd import Variable
model = resnet18().cuda()
# dummy inputs for the exampleinput = Variable(torch.randn(2,3,224,224).cuda(), requires_grad=True)
target = Variable(torch.zeros(2).long().cuda())
# as usual
output = model(input)
loss = torch.nn.functional.nll_loss(output, target)
grad_params = torch.autograd.grad(loss, model.parameters(), create_graph=True)# torch.autograd.grad does not accumuate the gradients into the .grad attributes# It instead returns the gradients as Variable tuples.# now compute the 2-norm of the grad_params
grad_norm = 0for grad in grad_params:
grad_norm += grad.pow(2).sum()
grad_norm = grad_norm.sqrt()
# take the gradients wrt grad_norm. backward() will accumulate# the gradients into the .grad attributes
grad_norm.backward()
# do an optimization step
optimizer.step()
这里我们看两个新的概念:
- torch.autograd.grad 是一个函数,它包含 [输出、输入列表(为了它你需要梯度)],并且返回梯度 wrt。这些输入作为元组,而不是将梯度累加到 .grad 属性中。如果你想要进一步在梯度上操作,这很有帮助。
- 你可以在梯度上操作,并在其上调用backward()。
支持高阶梯度的n层列表是:
- AvgPool*d、 BatchNorm*d、 Conv*d、MaxPool1d,2d、Linear、 Bilinear
- pad、ConstantPad2d、ZeroPad2d、LPPool2d、PixelShuffle
- ReLU6、LeakyReLU、PReLU、Tanh、Tanhshrink、Threshold、Sigmoid、HardTanh、ELU、Softsign、SeLU
- L1Loss、NLLLoss、 PoissonNLLLoss、LogSoftmax、Softmax2d
其余的将在下一个版本中启用。
为了高阶梯度,我们引入了一种编写autograd.Function的新风格。更多函数新风格的信息请参阅http://pytorch.org/docs/0.2.0/notes/extending.html。你们中的大多数并不亲自写
autograd.Functions ,因为它们是把新操作引入到 autograd 引擎的低级基元,其中你指定了前向与后向调用。
分布式PyTorch
我们还介绍了torch.distributed包,它允许我们在多机器间交换Tensors。使用该软件包,我们能够将神经网络放在多机器和使用较大批量进行训练。例如,给定一些基元,我们就能实现《精确的、大批量随机梯度下降:在1小时内训练ImageNet》。
该distributed包遵循MPI风格的程序设计模型。这意味着该软件包会提供像send、recv、all_reduce那样的函数以允许在结点(机器)之间交换Tensors。
对于每个机器最开始识别彼此并分配唯一的编码(排级),我们提供了简单的初始方法:
- 共享文件系统(要求所有进程都能访问一个单一的文件系统)
- IP 组播传输(要求所有的进程都在一个网络中)
- 环境变量(要求我们手动配置等级,并且需要知道从所有进程中可获得结点的地址)
我们的软件包文档包含更多初始化和可用后端的信息,现在我们了解一下使用组播地址初始化的案例:
import torch.distributed as dist
dist.init_process_group(backend='tcp',
init_method='tcp://[ff15:1e18:5d4c:4cf0:d02d:b659:53ba:b0a7]:23456',
world_size=4)
print('Hello from process {} (out of {})!'.format(
dist.get_rank(), dist.get_world_size()))
该代码片段将从第三个机器的process 2中打印“Hello”。
World size就是将参与整个工作的进程数量。每一个进程都将分配一个等级,即从0到world_size^( - 1)的数字,这些代号在整个工作中都是唯一的。它将成为处理识别符,并用于替代地址,例如指定哪一个进程将发送张量。
下面的代码片段展示了点到点的传输是如何简单地实现的:
# All processes (receiving ones too!) need to have tensors of appropriate# size preallocated.
x = torch.Tensor(10)if dist.get_rank() == 0:
x.normal_()
# Send x to process with rank 1
dist.send(x, dst=1)else: # rank == 1# Receive data from process with rank 0 and save result in x
dist.recv(x, src=0)
异步p2p函数(isend、irecv)同样是可用的。
然而,一些通信模式经常出现,并且已经开发了更高效的集合调用。它们一般占用整个进程组,并且比使用send/recv的朴素算法要更快一些。例如all_reduce:
x = torch.Tensor([dist.get_rank()])# Add tensors from all processes such that they all receive the result.# x is an input and output to this operation.
dist.all_reduce(x)
该分布式包是相当低级的,所以它允许实现更高级的算法和剪切代码以适应特定的任务目标,但是数据并行训练更为通用,所以我们为它构建了高级助手。因此,我们将介绍DistributedDataParallel,该函数基本上是nn.DataParallel的普适性替代。
下面的代码展示了添加它到已存代码的必要修改:
# Wrap model in DistributedDataParallel (CUDA only for the moment)
model = torch.nn.parallel.DistributedDataParallel(model.cuda())
# Use a DistributedSampler to restrict each process to a distinct subset# of the dataset.
train_dataset = ...
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=args.batch_size, num_workers=args.workers,
pin_memory=True, sampler=train_sampler)
for epoch in range(args.num_epochs):
# Use .set_epoch() method to reshuffle the dataset partition at every iteration
train_sampler.set_epoch(epoch)
# training loop...
我们可以从以下查看全部的ImageNet训练案例:
https://github.com/pytorch/examples/tree/master/imagenet
新型神经网络层:SpatialTransformers、WeightNorm、EmbeddingBag等
新特征
- 引入forward_pre_hook,以在前向函数(forward function)被调用之前执行用户指定的闭包。
- 易于获得非叶梯度(non-leaf gradient):目前,我们必须使用hooks获取和检查中间值的梯度。这种做法对于简单的检查并不方便。因此,我们引入了retain_grad。以下示例可充分解释该方法:
input = Variable(torch.rand(1, 3), requires_grad=True)
h1 = input * 3
out = (h1 * h1).sum()
h1.retain_grad()
out.backward()
print(h1.grad)# without calling retain_grad(), h1.grad is None
- DataParallel now supports dicts as inputs
- 现在,DataParallel作为输入支持dicts。
新型层
使用F.grid_sample和
F.affine_grid
的空间变换网络
论文《自正则化神经网络(Self-Normalizing Neural Networks)》提出nn.SeLU和nn.AlphaDropout。
论文《从卷积序列到序列学习(Convolutional Sequence to Sequence Learning)》提出
nn.GLU(线性门控单元)。
通过torch.utils.weight_norm实现权值归一化(Weight Normalization)。
计算cross_entropy_loss
和nll_loss时,可以使用ignore_index参数来
忽略特定的目标索引(target indice)。这是实现掩码的一种廉价、有用的方式,你可以获取计算损失时所忽略的mask指数。
F.normalize实现各维度的重归一化。
F.upsample和nn.Upsample将多个上采样层合并成一个函数。该函数可实现第二次和第三次双线性/三线性/最近上采样(bilinear/trilinear/nearest upsampling)。
nn.EmbeddingBag:在构建词袋模型(bag-of-words model)时,在Sum或Mean之后执行Embedding是一种常见做法。对于不同长度的序列,计算词袋嵌入涉及到掩码。我们提供一个单独的nn.EmbeddingBag,它能够更高效、快捷地计算词袋嵌入,尤其是不同长度的序列。
使用bce_with_logits的数值稳定的二元交叉熵参数(Binary Cross-Entropy loss)。
使用PoissonNLLLoss的带有目标泊松分布的负对数似然损失。
cosine_similarity
:沿维度计算并返回x1和x2之间的余弦相似度(cosine similarity)。训练工具
学习率调度器:torch.optim.lr_scheduler提供多个简单或聪明的方法来调整当前的学习率。在实验过程中,这些方法都很方便,i为用户可能想要做的事提供代理。
提供多种策略,可根据具体情况使用,详见
http://pytorch.org/docs/master/optim.html#how-to-adjust-learning-rate:ReduceLROnPlateau, LambdaLR, StepLR, MultiStepLR, ExponentialLR
ConcatDataset是一种可以合并和连接两个单独的数据集的数据集元类。
torch 和 autograd 中的新特性
现在,所有reduce函数如sum和mean默认为挤压降维。例如,
torch.sum(torch.randn(10, 20))返回一个1D 张量。
x.shape,与numpy类似。一种等价于x.size()的便捷属性。
torch.matmul,与np.matmul类似。
bitwise and、or、xor、lshift、rshift对inverse、
gesv、
cumprod、
atan2的autograd支持
通过关键字参数选项(keyword argument option)可获取无偏var和
std。
torch.scatter_add - torch.scatter,除了重复指数的情况,这些值都可以汇总。
无给定参数时,torch.median与torch.sum类似,即它减少所有维度,并返回扁平张量(flattened Tensor)的单个median值。
masked_copy_被重命名为masked_scatter_(因为对masked_copy_有反对声)。
torch.manual_seed现在也对所有的CUDA设备播种。
你现在可以通过关键字参数torch.rand(1000, generator=gen)
指定随机数生成器对象(random number generator object)。
修正和小提升
- 现在,当变量转为布尔类型时,会出现一个错误,例如:
b = Variable(torch.zeros(1))
if b[0]: # errors now
- 修正了CUDA中qr分解的正确性问题。
- 现已支持 IBM PowerPC64 平台。
- 现在在运行时会检查 CuDNN 版本是否相同。
- 改进了CUDA子进程中的错误消息。
- 现在,Pytorch在CPU上可以更快地转置。
- 改进了InstanceNorm上的错误信息。
- 为各种例程添加了更多的参数检查,特别是BatchNorm和Convolution例程。
- 在CPU后端时报告可以提出更好的错误信息。
- 支持每台机器超过 8 块 GPU (仍有 CUDA p2p 限制)
- 当访问不存在的属性时,错误消息获得了改进。
- 变量的T()与Tensor一致。
- 防止被零除时dropout p=1
- 修复在非当前设备上共享CUDA张量的问题。
- 当 BNε < 允许 CuDNN 值时,回退到THNN
- 修正了当使用不同线程数量的MKL和OMP时线程破坏问题。
- 在使用CuDNN RNN时提升了内存使用效率。
- 使用负填充修正了ZeroPad2d后向的问题。
- 加入了虚拟tensor.data属性,为用户提供可解释的错误消息。
- 修正了Python3原位分配。
- 在0-dim数组上调用from_numpy时生成错误。
- 现在,空张量在多处理器共享时不会发生错误了。
- 修复扩展张量的baddbmm
- 现在,parallel_apply 可以接受任意输入了。
- Tensor 和 Variable 中的关键字参数现在是一致的。
- 修正了Magma不可用时的fix torch.inverse
- 为ByteTensor添加逻辑非运算符。
- 在分散/集中核心里加入设备声明。
重要问题和解决方法
值得注意的是,有两个重要的变化是无法向下兼容的:
- Numpy风格的广播
- 缩减函数,如sum(1)现在默认为keepdim=False
我们提供不同级别的Python警告,它们可以在代码变更、使用错误操作时提醒用户。
例子
以下是一个代码片段,你可以将其添加到脚本的顶部。
添加此代码会高亮不合适的代码,并生成警告。
修复代码,警告就会消除。
# insert this to the top of your scripts (usually main.py)import sys, warnings, traceback, torchdef warn_with_traceback(message, category, filename, lineno, file=None, line=None):
sys.stderr.write(warnings.formatwarning(message, category, filename, lineno, line))
traceback.print_stack(sys._getframe(2))
warnings.showwarning = warn_with_traceback; warnings.simplefilter('always', UserWarning);
torch.utils.backcompat.broadcast_warning.enabled = True
torch.utils.backcompat.keepdim_warning.enabled = True
在所有警告消失后,你就可以删除此代码片段了。
细节
下面将介绍三种不兼容的变化和例子。
使用(现已弃用)一维视图点态函数
早期版本的PyTorch允许点态函数在不同形状的张量上执行,只要每个张量中的元素数量和相等即可。旧的框架可以将每个张量视为一维来执行点操作。新版PyTorch支持广播。“一维”点操作被认为是不推荐的,并且在张量不可广播但具有相同数量元素的情况下会产生Python警告。
>>> torch.add(torch.ones(4), torch.ones(2,2))
__main__:1: UserWarning: self and other not broadcastable, but have the same
number of elements. Falling back to deprecated pointwise behavior.2222
[torch.FloatTensor of size 4]
在实现没有出现过的代码中进行广播
在两个张量尺寸不同的情况下,广播的引入可能导致向后不兼容的变化,但是可以广播并具有相同数量的元素,例如:
>>> torch.add(torch.ones(4,1), torch.randn(4))
可以预先生成一个特定尺寸的张量:torch.Size([4,1])
现在则生成张量尺寸:torch.Size([4,4])
为了帮助你识别代码中广播可能造成的后向不兼容情况,你可能需要将
torch.utils.backcompat.broadcast_warning.enabled
设置为True,这样就会在相应的问题发生时生成python警告。例如:
>>> torch.utils.backcompat.broadcast_warning.enabled=True>>> torch.add(torch.ones(4,1), torch.ones(4))
__main__:1: UserWarning: self and other do not have the same shape, but are broadcastable, and have the same number of elements.
注意:此设置会触发广播有效性(包含库代码)的警告,所以你或许会希望在迁移代码后关闭这个警告。
减少函数:使用Keepdim=False
如需在默认Keepdim参数时使用维度缩减获函数得警告,请将
torch.utils.backcompat.keepdim_warning.enabled
设置为True。例如:
>>> torch.sum(torch.ones(2,3), 1)
__main__:1: UserWarning: backwards compatibility: call to "sum" uses default value for keepdim which has changed default to False. Consider passing as kwarg.33
[torch.FloatTensor of size 2]
可能会出现torch.utils.backcompat.broadcast_warning.enabled
,这一警告会被有效代码出发,所以你肯定希望在代码迁移后屏蔽它。
同时需注意:用keepdim=False可以使你的已有代码和广播“可以工作”。例如:
# behavior with (old) keepdim=True, causes accidental broadcast>>> torch.add(torch.ones(4), torch.ones(4,4).sum(dim=1, keepdim=True))5 5 5 55 5 5 55 5 5 55 5 5 5
[torch.FloatTensor of size 4x4]
# new behavior with keepdim=False is equivalent to non-broadcasted result>>> torch.add(torch.ones(4), torch.ones(4,4).sum(dim=1, keepdim=False))5555
[torch.FloatTensor of size 4]