尽管张量在深度学习中无处不在,但它已经崩溃了。它迫使人们养成暴露私有维度、基于绝对位置广播、将类型信息保存在文档中等坏习惯。这篇文章提供了另一种方法的概念证明,称为张量,具有命名的维度。此更改消除了对索引、模糊参数、einsum样式解包和基于文档的编码的需要。这篇博客文章附带的原型PyTorchlibrary名为namedtensor。
Jon Malmaud指出,xarray项目的目标与本文非常相似,增加了扩展的熊猫和科学计算支持。
斯蒂芬·霍耶(Stephan Hoyer)和埃里克·克里斯汀森(Eric Christian)有一个标记为Tensorflow的张量库,它与这个Appraoch相同。标号张量。
#@标题安装#!rm-fr命名张量/;git克隆-q https://github.com/harvardnlp/NamedTensor.git#!cd命名张量;pip安装-q;pip install-q手电筒编号opt_einsum。
这篇文章是关于张量类的,这是一个多维数组对象,是深度学习框架(如Torch、TensorFlow和Chainer)以及Numpy的中心对象。张量携带一个存储斑点,并向用户公开大量的维度信息。
这里有4个维度,分别对应于BATCH_SIZE、高度、宽度和通道。大多数情况下,您可以通过代码中的一些注释来解决这个问题,代码如下所示:
这种方法简明扼要,而且是伪数学的。然而,从编程的角度来看,这不是构建复杂软件的好方法。
操作张量的代码通过元组中的维标识符执行此操作。如果您想旋转您阅读评论的图像,确定需要更改的尺寸并更改它们。
定义旋转(IMS):#Batch_Size x高度x宽度x旋转的通道=IMS。转置(1,2)#Batch_Size x宽度x高度x通道返回旋转旋转(IMS)[0]。
这段代码很简单,理论上有很好的文档记录。但是,它不反映目标函数的语义。旋转属性与批次或通道无关。在确定要更改的维度时,该函数不应考虑这些维度。
这导致了两个问题。首先,非常令人担忧的是,如果我们传入一个单例图像,则此函数运行良好,但无法正常工作。
然而,更令人担忧的是,该函数实际上可能会错误地使用BatchDimension,并将不同图像的属性混合在一起。这可能会导致令人讨厌的错误,如果对代码隐藏此维度,则很容易避免这些错误。
张量最有用的方面是它们可以快速地进行数组运算,而不直接需要循环。要做到这一点,维度需要直接对齐,这样它们才能成为广播。同样,这是通过约定和代码文档来完成的,这使得排列维度变得“容易”。例如,假设我们想要对上面的图像应用蒙版。
这是失败的,因为即使我们知道我们正在建造一个高度和宽度形状的面具,广播规则也没有正确的语义。要实现这一点,我们鼓励您使用视图或压缩我最不喜欢的函数。
#任一掩码=掩码。解压(-1)#或MASK=MASK。视图(96,96,1)#高度x宽度x通道IMS。MASTED_FILL(MASK,1)[0]。
注意,我们不需要对最左边的维度执行此操作,因此这里有一点抽象。然而,通过实际代码阅读时,数十个右侧视图和压缩内容会变得完全不可读。
您可能会看到前两个问题,认为只要小心,这些问题就会被运行时错误捕获。但是,即使很好地使用广播和索引的组合,也会导致很难捕获的问题。
A=IMS[1]。Mean(2,Keepdim=True)#高x宽x 1#(中间有很多代码)#...。#解释应该发生什么的代码注释。Dim=1b=a+ims。Mean(Dim,Keepdim=True)[0]#(或者可能应该是2?或0?)。索引=2b=a+ims。Mean(Dim,Keep Dim=True)[0]b。
在这里,我们假设编码器正在尝试使用缩减操作和维度索引来组合两个张量。(老实说,在这一点上,我已经忘记了维度代表什么)。
不过,主要的一点是,无论给定dim的值是什么,此代码都可以正常运行。这里的注释可能描述了正在发生的事情,但是代码本身不会抛出运行时错误。
基于这些问题,我认为深度学习代码应该转向更好的中心对象。其中有几个建议。在这里,为了好玩,我将开发一个新的原型。我有以下目标。
为了试验这些想法,我构建了一个名为NamedTensor的库,目前它是特定于PyTorch的,但理论上类似的想法可以在其他框架中使用。代码可以在github.com/atherardnlp/namedtensor上找到。
库的核心是一个对象,它包装张量并为每个维度提供名称。在这里,我们简单地用维度名称包装给定的火炬张量。
名称的第一个好处是能够完全取代对Dimand轴样式参数的需要。例如,假设我们想要对每列进行排序。
所提供的名称也为广播操作提供了基础,当两个命名张量之间存在二进制操作时,它们首先确保所有维度在名称上匹配,然后应用标准广播。为了演示,让我们返回到上面的掩码示例。在这里,我们只需声明我们的面具尺寸的名称,并要求图书馆计算出广播。
IM=命名张量(IMS[0],(";高度";,";宽度";,";通道";)IM2=命名张量(IMS[1],(";高度";,";宽度";,";通道";))掩码=命名张量(火炬。Randint(0,2,[96,96])。Byte(),(";高度";,";宽度";))im。MASTED_FILL(掩码,1)。
一个更普遍的特征是名称传感器之间张量收缩的点法。张量压缩是Einsum背后的机制,它是考虑点积、矩阵向量乘积、矩阵矩阵乘积等的推广的一种优雅的方法。
类似的符号可以用于稀疏索引(灵感来自einindex库)。这是有用的前嵌入查找和其他稀疏操作。
在幕后,所有命名的张量都充当张量对象。正如诸如此类的事情一样,维度的秩序和步幅确实很重要。TRANSPOSE和VIEW等操作对于维护这一点至关重要,但不幸的是它们很容易出错。
取而代之的是,考虑一种领域特定的语言转换,它大量借鉴了Alex Rogozhnikov优秀的einops包。
张量=命名张量(IMS,(';b&39;,';h';,';w';,';c';))张量。Split(h=(';h1';,';h2';),h2=2)。Split(w=(';w1';,';w2';),w2=2)\。平均值(';h2';,';w2';))。堆栈(bw=(';b';,';w1';))。
张量=命名张量(IMS,(';b&39;,';h';,';w';,';c';))张量。Split(b=(';b1';,';b2';),b1=2)。平均(#39;c&39;)\。堆栈(bw=(";b1";,";w";),bh=(';b2';,';h';))。转置(';bh';,';bw';)
从Torch中拉出了一些有用的命名替代函数。例如,解除绑定会将一个维分离为一个元组。
最后,窄索引可以用来代替花哨的索引。但是,您必须给出一个新的暗淡的名称(因为它不能再广播了)。
最后,命名张量试图让您直接隐藏不应该被内部函数访问的维度。函数MASK_TO将保持在保护任何较早维度不受函数操作的左侧掩码周围。最简单的示例使用掩码删除批次维度。
Def BAD_Function(x,y):#访问私有批处理维度返回x。平均值(#34;批次#34;)x=ntorch。随机n(dict(批次=10,高度=100,宽度=100))y=ntorch.。RANDN(dict(Batch=10,Height=100,Width=100))try:BAD_Function(x.。MASK_TO(";批";),y)除运行错误外错误为e:error=";收到错误:";+str(E)错误。
这是弱动态检查,可以通过内部功能关闭。在将来的版本中,也许我们可以添加函数注释来提升未命名的函数,以尊重这些属性。
为了说明为什么这些选择会带来更好的封装属性,让我们考虑一个真实世界的深度学习示例。
这个例子是我的同事Tim Rocktashel在博客PostDescription Einsum(https://rockt.github.io/2018/04/30/einsum).)中提出的。Tim的代码被建议作为原始PyTorch的更好替代方案。虽然我同意Einsum是向前迈进了一步,但它仍然落入了上述许多陷阱。
Def Random_ntensor(名称,Num=1,Requires_grad=false):张量=[ntorch.。如果num==1,则返回张量[0]如果num==1,则返回张量类参数:def__init__(self,in_hid,out_hid):Torch。MANUAL_SEED(0)SELF。怀伊,赛尔夫。什,赛尔夫。WR,赛尔夫。Wt=\Random_ntenors(dict(inhid=in_hid,outhid=out_hid),num=4,requires_grad=True)self。BM,赛尔夫。布莱尔,赛尔夫。W=\Random_n张量(dict(outhid=out_hid),Num=3,Requires_grad=True)
#Einsum实现import torch.nn.function as F def einsum_attn(params,Y,ht,rt1):#--[BATCH_SIZE x HIDDEN_DIMENSION]tmp=torch。Einsum(";ik,kl->;il&34;,[ht,param.。WH。值])+\手电筒。Einsum(";ik,kl->;il&34;,[rt1,param.。WR.。值])mt=手电筒。Tanh(手电筒。Einsum(";ijk,kl->;ijl&34;,[Y,param.。怀俄明州。值])+\tmp。解压(1)。展开_AS(Y)+PARAMS。宾夕法尼亚州立大学。值)#--[BATCH_SIZE x SEQUENCE_LENGTH]at=F。Softmax(手电筒。Einsum(";ijk,k->;ij&34;,[mt,param.。W.。值]),DIM=-1)#--[BATCH_SIZE x HIDDED_DIMENSION]RT=手电筒。Einsum(";ijk,ij->;ik&34;,[Y,at])+\torch。Tanh(手电筒。Einsum(";ij,jk->;ik&34;,[rt1,param.。WT。值])+参数。Br.。值)#--[Batch_Size x Hidden_Dimension],[Batch_Size x Sequence_Dimension]返回RT,位于。
此实现是对朴素的PyTorch实现的改进。它移动了许多视图和转置,这将是实现这一工作所必需的。但是,它仍然使用挤压,引用私有批处理DIM,并使用未强制执行的注释。
Def namedtensor_attn(参数,Y,ht,rt1):tmp=ht。点(";隐藏";,参数。WH)+RT1。点(";隐藏";,参数。Wr)at=ntorch。Tanh(Y.。点(";隐藏";,参数。WY)+tmp+params。Bm)\。点(";outhid";,param.。W)\。Softmax(#34;seqlen&34;)rt=Y。点(";序列";,at)。堆栈(inhid=(';outhid';,))+\n手电筒。Tanh(RT1.。点(";隐藏";,参数。WT)+参数。Br)返回RT,位于。
(陷阱3)跨DIM的操作是明确的。例如,Softmax明显超出了序列。
#运行Einsum in_hid=7;out_hid=7 Y=手电筒。Randn(3,5,in_HID)ht,rt1=手电筒。Randn(3,in_HID),火炬。Rann(3,in_hid)params=param(in_hid,out_hid)r,a=einsum_attn(params,Y,ht,rt1)。
#运行命名张量(隐藏批次)Y=命名张量(Y,(";Batch&34;,";seqlen";,";inHid";),MASK=1)ht=Named张量(ht,(";Batch";,";inHID";),MASK=1)rt1=Named张量(RT1,(";Batch";,),掩码=1)nr,na=namedtensor_attn(params,Y,ht,rt1)。
深度学习工具帮助研究人员实现标准模型,但它们也会影响研究人员的尝试。目前的模型可以用我们拥有的工具很好地构建,但是编程实践不会扩展到新的模型。
(例如,我们最近一直在研究的一个空间是离散潜变量模型,它通常有许多特定于问题的变量,每个变量都有各自的行变量维数。这一设置几乎立即打破了当前的张量范式。)
这篇博客文章只是这种方法可能走向何方的一个原型。如果你感兴趣,我希望能为这个图书馆的建设做出贡献。如果你想把公关寄给Namedtensor,我会给你一些建议。一些想法:
1)扩展到PyTorch之外:我们能以一种支持NumPy和TensorFlow的方式来推广这种方法吗?
2)与PyTorch模块交互:我们是否可以“提升”带有类型注释的PyTorch模块,以便我们知道它们是如何更改输入的?
3)错误检查:是否可以在函数中添加注释,给出前置条件和后置条件,以便自动检查尺寸。
如果下面有俗气的广告,很抱歉:Disqus似乎是自动做到这一点的。