PyTorch推出0.2版本:加入分散式機器學習功能

機器之心發表於2017-08-07

正值 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 風格的高階索引的一個子集,這允許使用者使用相同的

[]風格的操作在張量的每一個維度上選擇任意的索引,包括非相鄰索引和重複索引;這也使得無需呼叫 PyTorchIndex[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_sampleF.affine_grid的空間變換網路


論文《自正則化神經網路(Self-Normalizing Neural Networks)》提出nn.SeLUnn.AlphaDropout。論文《從卷積序列到序列學習(Convolutional Sequence to Sequence Learning)》提出nn.GLU(線性門控單元)。


通過torch.utils.weight_norm實現權值歸一化(Weight Normalization)。


計算cross_entropy_lossnll_loss時,可以使用ignore_index引數來

忽略特定的目標索引(target indice)。這是實現掩碼的一種廉價、有用的方式,你可以獲取計算損失時所忽略的mask指數。


F.normalize實現各維度的重歸一化。

F.upsamplenn.Upsample將多個上取樣層合併成一個函式。該函式可實現第二次和第三次雙線性/三線性/最近上取樣(bilinear/trilinear/nearest upsampling)。

nn.EmbeddingBag:在構建詞袋模型(bag-of-words model)時,在SumMean之後執行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、rshiftinverse、gesv、cumprod、atan2的autograd支援


通過關鍵字引數選項(keyword argument option)可獲取無偏varstd。

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]


下載


相關文章