跳到主要內容

深度學習框架中的「張量」不好用?也許我們需要重新定義Tensor了

機器之心 3天前
選自harvardnlp
作者:Alexander Rush
機器之心編譯
參與:李詩萌、路雪

本文介紹了張量的陷阱和一種可以閃避陷阱的替代方法named tensor,並進行了概念驗證。

儘管張量在深度學習的世界中無處不在,但它是有破綻的。它催生出了一些壞習慣,比如公開專用維度、基於絕對位置進行廣播,以及在文檔中保存類型信息。這篇文章介紹了一種具有命名維度的替代方法named tensor,並對其進行了概念驗證。這一改變消除了對索引、維度參數、einsum 式解壓縮以及基於文檔的編碼的需求。這篇文章附帶的原型PyTorch 庫可以作為namedtensor 使用。

PyTorch 庫參見:https://github.com/harvardnlp/NamedTensor

實現:

  • Jon Malmaud 指出xarray 項目(http://xarray.pydata.org/en/stable/)的目標與namedtensor 非常相似,xarray 項目還增加了大量Pandas 和科學計算的支持。
  • Tongfei Chen 的Nexus 項目在Scala 中提出了靜態類型安全的張量。
  • Stephan Hoyer 和Eric Christiansen 為TensorFlow 建立了標註張量庫,Labed Tensor,和本文的方法是一樣的。
  • Nishant Sinha 有TSA 庫,它使用類型註釋來定義維度名稱。

#@title Setup
#!rm -fr NamedTensor/; git clone -q https://github.com/harvardnlp/NamedTensor.git
#!cd NamedTensor; pip install -q .; pip install -q torch numpy opt_einsum
import numpy
import torch
from namedtensor import NamedTensor, ntorch
from namedtensor import _im_init
_im_init()

張量陷阱

這篇文章是關於張量類的。張量類是多維數組對象,是Torch、TensorFlow、Chainer 以及NumPy 等深度學習框架的核心對象。張量具備大量存儲空間,還可以向用戶公開維度信息。

ims = torch.tensor(numpy.load('test_images.npy'))
ims.shape

torch.Size([696963])

該示例中有4 個維度,對應的是batch_size、height、width 和channels。大多數情況下,你可以通過代碼註釋弄明白維度的信息,如下所示:

# batch_size x height x width x channels
ims[0]


這種方法簡明扼要,但從編程角度看來,這不是構建複雜軟件的好方法。

陷阱1:按慣例對待專用維度

代碼通過元組中的維度標識符操縱張量。如果要旋轉圖像,閱讀註釋,確定並更改需要改變的維度。

def rotate(ims):
    # batch_size x height x width x channels
    rotated = ims.transpose(12)

    # batch_size x width x height x channels
    return rotated
rotate(ims)[0]


這段代碼很簡單,而且從理論上講記錄詳盡。但它並沒有反映目標函數的語義。旋轉的性質與batch 或channel 都無關。在確定要改變的維度時,函數不需要考慮這些維度。

這就產生了兩個問題。首先,令人非常擔心的是如果我們傳入單例圖像,函數可以正常運行但是卻不起作用。

rotate(ims[0]).shape

torch.Size([96396])

但更令人擔憂的是,這個函數實際上可能會錯誤地用到batch 維度,還會把不同圖像的屬性混到一起。如果在代碼中隱藏了這個維度,可能會產生一些本來很容易避免的、討厭的bug。

陷阱2:通過對齊進行廣播

張量最有用的地方是它們可以在不直接需要for 循環的情況下快速執行數組運算。為此,要直接對齊維度,以便廣播張量。同樣,這是按照慣例和代碼文檔實現的,這使排列維度變得「容易」。例如,假設我們想對上圖應用掩碼。

# height x width
mask = torch.randint(02, [9696]).byte()
mask


try:
    ims.masked_fill(mask, 0)
except RuntimeError:
    error = "Broadcasting fail %s %s"%(mask.shape, ims.shape)
error

'Broadcasting fail torch.Size([96, 96]) torch.Size([6, 96, 96, 3])'

這裡的失敗的原因是:即便我們知道要建立掩碼的形狀,廣播的規則也沒有正確的語義。為了讓它起作用,你需要使用view 或squeeze 這些我最不喜歡的函數。

# either
mask = mask.unsqueeze(-1)
# or
mask = mask.view(96961)

# height x width x channels
ims.masked_fill(mask, 1)[0]

注意,最左邊的維度不需要進行這樣的運算,所以這裡有些抽象。但閱讀真正的代碼後會發現,右邊大量的view 和squeeze 變得完全不可讀。

陷阱3:通過註釋訪問

看過上面兩個問題後,你可能會認為只要足夠小心,運行時就會捕捉到這些問題。但是即使很好地使用了廣播和索引的組合,也可能會造成很難捕捉的問題。

a = ims[1].mean(2, keepdim=True)
# height x width x 1

# (Lots of code in between)
#  .......................

# Code comment explaining what should be happening.
dim = 1
b = a + ims.mean(dim, keepdim=True)[0]


# (Or maybe should be a 2? or a 0?)
index = 2
b = a + ims.mean(dim, keepdim=True)[0]
b

我們在此假設編碼器試著用歸約運算和維度索引將兩個張量結合在一起。(說實話這會兒我已經忘了維度代表什麼。)

重點在於無論給定的維度值是多少,代碼都會正常運行。這裡的註釋描述的是在發生什麼,但是代碼本身在運行時不會報錯。

Named Tensor:原型

根據這些問題,我認為深度學習代碼應該轉向更好的核心對象。為了好玩,我會開發一個新的原型。目標如下:

  1. 維度應該有人類可讀的名字。
  2. 函數中不應該有維度參數。
  3. 廣播應該通過名稱匹配。
  4. 轉換應該是顯式的。
  5. 禁止基於維度的索引。
  6. 應該保護專用維度。

為了試驗這些想法,我建立了一個叫做NamedTensor 的庫。目前它只用於PyTorch,但從理論上講類似的想法也適用於其他框架。

建議1:分配名稱

庫的核心是封裝了張量的對象,並給每個維度提供了名稱。我們在此用維度名稱簡單地包裝了給定的torch 張量。

named_ims = NamedTensor(ims, ("batch""height""width""channels"))
named_ims.shape

OrderedDict([('batch'6), ('height'96), ('width'96), ('channels'3)])

此外,該庫有針對PyTorch 構造函數的封裝器,可以將它們轉換為命名張量。

ex = ntorch.randn(dict(height=96, width=96, channels=3))
ex
大多數簡單的運算只是簡單地保留了命名張量的屬性。

ex.log()

# or

ntorch.log(ex)

None
建議2:訪問器和歸約

名字的第一個好處是可以完全替換掉維度參數和軸樣式參數。例如,假設我們要對每列進行排序。

sortex, _ = ex.sort("width")
sortex


另一個常見的操作是在匯集了一個或多個維度的地方進行歸約。

named_ims.mean("batch")


named_ims.mean(("batch""channels"))


建議3:廣播和縮並

提供的張量名稱也為廣播操作提供了基礎。當兩個命名張量間存在二進制運算時,它們首先要保證所有維度都和名稱匹配,然後再應用標準的廣播。為了演示,我們回到上面的掩碼示例。在此我們簡單地聲明了一下掩碼維度的名稱,然後讓庫進行廣播。

im = NamedTensor(ims[0], ("height""width""channels"))
im2 = NamedTensor(ims[1], ("height""width""channels"))

mask = NamedTensor(torch.randint(02, [9696]).byte(), ("height""width"))
im.masked_fill(mask, 1)


加和乘等簡單運算可用於標準矩陣。

im * mask.double()


在命名向量間進行張量縮併的更普遍的特徵是dot 方法。張量縮並是einsum 背後的機制,是一種思考點積、矩陣-向量乘積、矩陣-矩陣乘積等泛化的優雅方式。

# Runs torch.einsum(ijk,ijk->jk, tensor1, tensor2)
im.dot("height", im2).shape

OrderedDict([('width'96), ('channels'3)])

# Runs torch.einsum(ijk,ijk->il, tensor1, tensor2)
im.dot("width", im2).shape

OrderedDict([('height'96), ('channels'3)])

# Runs torch.einsum(ijk,ijk->l, tensor1, tensor2)
im.dot(("height""width"), im2).shape

OrderedDict([('channels'3)])

類似的註釋也可用於稀疏索引(受einindex 庫的啟發)。這在嵌入查找和其他稀疏運算中很有用。

pick, _ = NamedTensor(torch.randint(096, [50]).long(), ("lookups",)) \
             .sort("lookups")

# Select 50 random rows.
im.index_select("height", pick)


建議4:維度轉換

在後台計算中,所有命名張量都是張量對象,因此維度順序和步幅這樣的事情就尤為重要。transpose 和view 等運算對於保持維度的順序和步幅至關重要,但不幸的是它們很容易出錯。

那麼,我們來考慮領域特定語言shift,它大量借鑒了Alex Rogozhnikov 優秀的einops 包(https://github.com/arogozhnikov/einops)。

tensor = NamedTensor(ims[0], ("h""w""c"))
tensor


維度轉換的標準調用。

tensor.transpose("w""h""c")


拆分和疊加維度。

tensor = NamedTensor(ims[0], ("h""w""c"))
tensor.split(h=("height""q"), height=8).shape
OrderedDict([('height'8), ('q'12), ('w'96), ('c'3)])
tensor = NamedTensor(ims, ('b''h''w''c'))
tensor.stack(bh = ('b''h')).shape
OrderedDict([('bh'576), ('w'96), ('c'3)])

鏈接Ops。

tensor.stack(bw=('b''w')).transpose('h''bw''c')


這裡還有一些einops 包中有趣的例子。

tensor.split(b=('b1''b2'), b1=2).stack(a=('b2''h'), d=('b1''w'))\
      .transpose('a''d''c')


建議5:禁止索引

一般在命名張量範式中不建議用索引,而是用上面的index_select 這樣的函數。

在torch 中還有一些有用的命名替代函數。例如unbind 將維度分解為元組。

tensor = NamedTensor(ims, ('b''h''w''c'))

# Returns a tuple
images = tensor.unbind("b")
images[3]


get 函數直接從命名維度中選擇了一個切片。

# Returns a tuple
images = tensor.get("b"0).unbind("c")
images[1]


最後,可以用narrow 代替花哨的索引。但是你一定要提供一個新的維度名稱(因為它不能再廣播了)。

tensor.narrow( 3050, h='narowedheight').get("b"0)


建議6:專用維度

最後,命名張量嘗試直接隱藏不應該被內部函數訪問的維度。mask_to 函數會保留左邊的掩碼,它可以使任何早期的維度不受函數運算的影響。最簡單的使用掩碼的例子是用來刪除batch 維度的。

def bad_function(x, y):
    # Accesses the private batch dimension
    return x.mean("batch")

x = ntorch.randn(dict(batch=10, height=100, width=100))
y = ntorch.randn(dict(batch=10, height=100, width=100))

try:
    bad_function(x.mask_to("batch"), y)
except RuntimeError as e:
    error = "Error received: " + str(e)
error

'Error received: Dimension batch is masked'
這是弱動態檢查,可以通過內部函數關閉。在將來的版本中,也許我們會添加函數註釋來lift 未命名函數,來保留這些屬性。

示例:神經註意力

為了說明為什麼這些選擇會帶來更好的封裝屬性,我們來思考一個真實世界中的深度學習例子。這個例子是我的同事Tim Rocktashel 在一篇介紹einsum 的博客文章中提出來的。和原始的PyTorch 相比,Tim 的代碼是更好的替代品。雖然我同意enisum 是一個進步,但它還是存在很多上述陷阱。

下面來看神經註意力的問題,它需要計算,

首先我們要配置參數。

def random_ntensors(names, num=1, requires_grad=False):
    tensors = [ntorch.randn(names, requires_grad=requires_grad)
               for i in range(0, num)]
    return tensors[0if num == 1 else tensors

class Param:
    def __init__(self, in_hid, out_hid):
        torch.manual_seed(0)
        self.WY, self.Wh, self.Wr, self.Wt = \
            random_ntensors(dict(inhid=in_hid, outhid=out_hid),
                            num=4, requires_grad=True)
        self.bM, self.br, self.w = \
            random_ntensors(dict(outhid=out_hid),
                            num=3,
                            requires_grad=True)

現在考慮這個函數基於張量的enisum 實現。

# Einsum Implementation
import torch.nn.functional as F
def einsum_attn(params, Y, ht, rt1):
    # -- [batch_size x hidden_dimension]
    tmp = torch.einsum("ik,kl->il", [ht, params.Wh.values]) + \
          torch.einsum("ik,kl->il", [rt1, params.Wr.values])

    Mt = torch.tanh(torch.einsum("ijk,kl->ijl", [Y, params.WY.values]) + \
                tmp.unsqueeze(1).expand_as(Y) + params.bM.values)
    # -- [batch_size x sequence_length]
    at = F.softmax(torch.einsum("ijk,k->ij", [Mt, params.w.values]), dim=-1)

    # -- [batch_size x hidden_dimension]
    rt = torch.einsum("ijk,ij->ik", [Y, at]) + \
         torch.tanh(torch.einsum("ij,jk->ik", [rt1, params.Wt.values]) +
                    params.br.values)

    # -- [batch_size x hidden_dimension], [batch_size x sequence_dimension]
    return rt, at

該實現是對原版PyTorch 實現的改進。它刪除了這項工作必需的一些view 和transpose。但它仍用了squeeze,引用了private batch dim,使用了非強制的註釋。

接下來來看namedtensor 版本:

def namedtensor_attn(params, Y, ht, rt1):
    tmp = ht.dot("inhid", params.Wh) + rt1.dot("inhid", params.Wr)
    at = ntorch.tanh(Y.dot("inhid", params.WY) + tmp + params.bM) \
         .dot("outhid", params.w) \
         .softmax("seqlen")

    rt = Y.dot("seqlen", at).stack(inhid=('outhid',)) + \
         ntorch.tanh(rt1.dot("inhid", params.Wt) + params.br)
    return rt, at

該代碼避免了三個陷阱:

  • (陷阱1)該代碼從未提及batch 維度。
  • (陷阱2)所有廣播都是直接用縮並完成的,沒有views。
  • (陷阱3)跨維度的運算是顯式的。例如,softmax 明顯超過了seqlen。

# Run Einsum
in_hid = 7; out_hid = 7
Y = torch.randn(35, in_hid)
ht, rt1 = torch.randn(3, in_hid), torch.randn(3, in_hid)
params = Param(in_hid, out_hid)
r, a = einsum_attn(params, Y, ht, rt1)

# Run Named Tensor (hiding batch)
Y = NamedTensor(Y, ("batch""seqlen""inhid"), mask=1)
ht = NamedTensor(ht, ("batch""inhid"), mask=1)
rt1 = NamedTensor(rt1, ("batch""inhid"), mask=1)
nr, na = namedtensor_attn(params, Y, ht, rt1)

結論/請求幫助

深度學習工具可以幫助研究人員實現標準模型,但它們也影響了研究人員的嘗試。我們可以用現有工具很好地構建模型,但編程實踐無法擴展到新模型。(例如,我們最近研究的是離散隱變量模型,它通常有許多針對特定問題的變量,每個變量都有自己的變量維度。這個設置幾乎可以立即打破當前的張量範式。)

這篇博文只是這種方法的原型。如果你感興趣,我很願意為構建這個庫作出貢獻。還有一些想法:

擴展到PyTorch 之外:我們是否可以擴展這種方法,使它支持NumPy 和TensorFlow?

與PyTorch 模塊交互:我們是否可以通過類型註釋「lift」PyTorch 模塊,從而了解它們是如何改變輸入的?

錯誤檢查:我們是否可以給提供前置條件和後置條件的函數添加註釋,從而自動檢查維度?

原文鏈接:http://nlp.seas.harvard.edu/NamedTensor?fbclid=IwAR2FusFxf-c24whTSiF8B3R2EKz_-zRfF32jpU8D-F5G7rreEn9JiCfMl48

本文為機器之心編譯,轉載請聯繫本公眾號獲得授權
✄------------------------------------------------
加入機器之心(全職記者/ 實習生):hr@jiqizhixin.com
投稿或尋求報導:content @jiqizhixin.com
廣告& 商務合作:bd@jiqizhixin.com

留言

這個網誌中的熱門文章

2017通訊大賽「聯發科技物聯網開發競賽」決賽團隊29強出爐!作品都在11月24日頒獎典禮進行展示

2017通訊大賽「聯發科技物聯網開發競賽」決賽團隊29強出爐!作品都在11月24日頒獎典禮進行展示 LIS   發表於 2017年11月16日 10:31   收藏此文 2017通訊大賽「聯發科技物聯網開發競賽」決賽於11月4日在台北文創大樓舉行,共有29個隊伍進入決賽,角逐最後的大獎,並於11月24日進行頒獎,現場會有全部進入決賽團隊的展示攤位,總計約為100個,各種創意作品琳琅滿目,非常值得一看,這次錯過就要等一年。 「聯發科技物聯網開發競賽」決賽持續一整天,每個團隊都有15分鐘面對評審團做簡報與展示,並接受評審們的詢問。在所有團隊完成簡報與展示後,主辦單位便統計所有評審的分數,並由評審們進行審慎的討論,決定冠亞季軍及其他各獎項得主,結果將於11月24日的「2017通訊大賽頒獎典禮暨成果展」現場公佈並頒獎。 在「2017通訊大賽頒獎典禮暨成果展」現場,所有入圍決賽的團隊會設置攤位,總計約為100個,展示他們辛苦研發並實作的作品,無論是想觀摩別人的成品、了解物聯網應用有那些新的創意、尋找投資標的、尋找人才、尋求合作機會或是單純有興趣,都很適合花點時間到現場看看。 頒獎典禮暨成果展資訊如下: 日期:2017年11月24日(星期五) 地點:中油大樓國光廳(台北市信義區松仁路3號) 我要報名參加「2017通訊大賽頒獎典禮暨成果展」>>> 在參加「2017通訊大賽頒獎典禮暨成果展」之前,可以先在本文觀看各團隊的作品介紹。 決賽29強團隊如下: 長者安全救星 可隨意描繪或書寫之電子筆記系統 微觀天下 體適能訓練管理裝置 肌少症之行走速率檢測系統 Sugar Robot 賽亞人的飛機維修輔助器 iTemp你的溫度個人化管家 語音行動冰箱 MR模擬飛行 智慧防盜自行車 跨平台X-Y視覺馬達控制 Ironmet 菸消雲散 無人小艇 (Mini-USV) 救OK-緊急救援小幫手 穿戴式長照輔助系統 應用於教育之模組機器人教具 這味兒很台味 Aquarium Hub 發展遲緩兒童之擴增實境學習系統 蚊房四寶 車輛相控陣列聲納環境偵測系統 戶外團隊運動管理裝置 懷舊治療數位桌曆 SeeM智能眼罩 觸...
opencv4nodejs Asynchronous OpenCV 3.x Binding for node.js   122     2715     414   0   0 Author Contributors Repository https://github.com/justadudewhohacks/opencv4nodejs Wiki Page https://github.com/justadudewhohacks/opencv4nodejs/wiki Last Commit Mar. 8, 2019 Created Aug. 20, 2017 opencv4nodejs           By its nature, JavaScript lacks the performance to implement Computer Vision tasks efficiently. Therefore this package brings the performance of the native OpenCV library to your Node.js application. This project targets OpenCV 3 and provides an asynchronous as well as an synchronous API. The ultimate goal of this project is to provide a comprehensive collection of Node.js bindings to the API of OpenCV and the OpenCV-contrib modules. An overview of available bindings can be found in the  API Documentation . Furthermore, contribution is highly appreciated....

完形心理學!?讓我們了解“介面設計師”為什麼這樣設計

完形心理學!?讓我們了解“介面設計師”為什麼這樣設計 — 說服客戶與老闆、跟工程師溝通、強化設計概念的有感心理學 — 情況 1 : 為何要留那麼多空白? 害我還要滾動滑鼠(掀桌) 情況 2 : 為什麼不能直接用一頁展現? 把客戶的需求塞滿不就完工啦! (無言) 情況 3: 這種設計好像不錯,但是為什麼要這樣做? (直覺大神告訴我這樣設計,但我說不出來為什麼..) 雖然世界上有許多 GUI 已經走得又長又遠又厲害,但別以為這種古代人對話不會出現,一直以來我們只是習慣這些 GUI 被如此呈現,但為何要這樣設計我們卻不一定知道。 由於 完形心理學 歸納出人類大腦認知之普遍性的規則,因此無論是不是 UI/UX 設計師都很適合閱讀本篇文章。但還是想特別強調,若任職於傳統科技公司,需要對上說服老闆,需要平行說服(資深)工程師,那請把它收進最愛;而習慣套用設計好的 UI 套件,但不知道為何這樣設計的 IT 工程師,也可以透過本文來強化自己的產品說服力。 那就開始吧~(擊掌) 完形心理學,又稱作格式塔(Gestalt)心理學,於二十世紀初由德國心理學家提出 — 用以說明人類大腦如何解釋肉眼所觀察到的事物,並轉化為我們所認知的物件。它可說是現代認知心理學的基礎,其貫徹的概念就是「整體大於個體的總合 “The whole is other than the sum of the parts.” —  Kurt Koffka」。 若深究完整的理論將會使本文變得非常的艱澀,因此筆者直接抽取個人認為與 UI 設計較為相關的 7 個原則(如下),並搭配實際案例做說明。有興趣了解全部理論的話可以另外 Google。 1. 相似性 (Similarity)  — 我們的大腦會把相似的事物看成一體 如果數個元素具有類似的尺寸、體積、顏色,使用者會自動為它們建立起關聯。這是因為我們的眼睛和大腦較容易將相似的事物組織在一起。如下圖所示,當一連串方塊和一連串的圓形並排時,我們會看成(a)一列方塊和兩列圓形(b)一排圓形和兩排三角形。 對應用到介面設計上,FB 每則文章下方的按鈕圖標(按讚 Like / 留言Comment / 分享 Share)雖然功能各不相同,但由於它們在視覺上顏色、大小、排列上的相似性,用戶會將它們視認為...