注意
點選此處下載完整示例程式碼
(原型) MaskedTensor 概述¶
建立日期:2022 年 10 月 28 日 | 最後更新:2022 年 10 月 28 日 | 最後驗證:未驗證
本教程旨在作為使用 MaskedTensor 並討論其掩碼語義的起點。
MaskedTensor 作為 torch.Tensor 的擴充套件,提供了使用者以下能力:
使用任何掩碼語義(例如,變長張量、nan* 運算子等)
區分 0 梯度和 NaN 梯度
各種稀疏應用(參見下面的教程)
有關 MaskedTensor 是什麼的更詳細介紹,請查閱 torch.masked 文件。
使用 MaskedTensor¶
在本節中,我們將討論如何使用 MaskedTensor,包括如何構造、訪問資料和掩碼,以及索引和切片。
準備工作¶
我們將首先進行本教程所需的設定
import torch
from torch.masked import masked_tensor, as_masked_tensor
import warnings
# Disable prototype warnings and such
warnings.filterwarnings(action='ignore', category=UserWarning)
構造¶
有幾種不同的方法來構造 MaskedTensor
第一種方法是直接呼叫 MaskedTensor 類
第二種(也是我們推薦的方法)是使用
masked.masked_tensor()和masked.as_masked_tensor()工廠函式,它們類似於torch.tensor()和torch.as_tensor()
在本教程中,我們將假定匯入行是:from torch.masked import masked_tensor。
訪問資料和掩碼¶
MaskedTensor 中的底層欄位可以透過以下方式訪問:
MaskedTensor.get_data()函式MaskedTensor.get_mask()函式。回想一下,True表示“指定”或“有效”,而False表示“未指定”或“無效”。
通常,返回的底層資料在未指定的條目中可能無效,因此我們建議使用者在需要不包含任何掩碼條目的 Tensor 時,使用 MaskedTensor.to_tensor()(如上所示)來返回一個填充值的 Tensor。
索引和切片¶
MaskedTensor 是 Tensor 的子類,這意味著它繼承了與 torch.Tensor 相同的索引和切片語義。下面是一些常見的索引和切片模式示例:
data:
tensor([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
mask:
tensor([[[ True, False, True, False],
[ True, False, True, False],
[ True, False, True, False]],
[[ True, False, True, False],
[ True, False, True, False],
[ True, False, True, False]]])
mt[0]:
MaskedTensor(
[
[ 0.0000, --, 2.0000, --],
[ 4.0000, --, 6.0000, --],
[ 8.0000, --, 10.0000, --]
]
)
mt[:, :, 2:4]:
MaskedTensor(
[
[
[ 2.0000, --],
[ 6.0000, --],
[ 10.0000, --]
],
[
[ 14.0000, --],
[ 18.0000, --],
[ 22.0000, --]
]
]
)
為什麼 MaskedTensor 有用?¶
由於 MaskedTensor 將指定值和未指定值作為一等公民處理,而不是作為事後填充(使用填充值、nan 等),它能夠解決普通 Tensor 無法解決的幾個缺點;事實上,MaskedTensor 在很大程度上正是因此類反覆出現的問題而誕生的。
下面,我們將討論當前 PyTorch 中一些尚未解決的最常見問題,並說明 MaskedTensor 如何解決這些問題。
區分 0 梯度和 NaN 梯度¶
torch.Tensor 遇到的一個問題是無法區分未定義的梯度 (NaN) 和實際為 0 的梯度。由於 PyTorch 沒有一種方法來標記值是指定/有效還是未指定/無效,因此它被迫依賴 NaN 或 0(取決於用例),這導致語義不可靠,因為許多操作無法正確處理 NaN 值。更令人困惑的是,有時根據操作順序的不同,梯度可能會發生變化(例如,取決於 NaN 值在操作鏈中出現得有多早)。
MaskedTensor 是完美的解決方案!
torch.where¶
在 Issue 10729 中,我們注意到在使用 torch.where() 時,操作順序可能會產生影響,因為我們難以區分 0 是真實的 0 還是來自未定義梯度的 0。因此,我們保持一致並掩蓋結果:
當前結果
x = torch.tensor([-10., -5, 0, 5, 10, 50, 60, 70, 80, 90, 100], requires_grad=True, dtype=torch.float)
y = torch.where(x < 0, torch.exp(x), torch.ones_like(x))
y.sum().backward()
x.grad
tensor([4.5400e-05, 6.7379e-03, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
0.0000e+00, 0.0000e+00, 0.0000e+00, nan, nan])
MaskedTensor 結果
x = torch.tensor([-10., -5, 0, 5, 10, 50, 60, 70, 80, 90, 100])
mask = x < 0
mx = masked_tensor(x, mask, requires_grad=True)
my = masked_tensor(torch.ones_like(x), ~mask, requires_grad=True)
y = torch.where(mask, torch.exp(mx), my)
y.sum().backward()
mx.grad
MaskedTensor(
[ 0.0000, 0.0067, --, --, --, --, --, --, --, --, --]
)
這裡的梯度僅提供給選定的子集。實際上,這將 where 的梯度更改為掩蓋元素,而不是將它們設定為零。
另一個 torch.where¶
Issue 52248 是另一個示例。
當前結果
a = torch.randn((), requires_grad=True)
b = torch.tensor(False)
c = torch.ones(())
print("torch.where(b, a/0, c):\n", torch.where(b, a/0, c))
print("torch.autograd.grad(torch.where(b, a/0, c), a):\n", torch.autograd.grad(torch.where(b, a/0, c), a))
torch.where(b, a/0, c):
tensor(1., grad_fn=<WhereBackward0>)
torch.autograd.grad(torch.where(b, a/0, c), a):
(tensor(nan),)
MaskedTensor 結果
a = masked_tensor(torch.randn(()), torch.tensor(True), requires_grad=True)
b = torch.tensor(False)
c = torch.ones(())
print("torch.where(b, a/0, c):\n", torch.where(b, a/0, c))
print("torch.autograd.grad(torch.where(b, a/0, c), a):\n", torch.autograd.grad(torch.where(b, a/0, c), a))
torch.where(b, a/0, c):
MaskedTensor( 1.0000, True)
torch.autograd.grad(torch.where(b, a/0, c), a):
(MaskedTensor(--, False),)
這個問題類似(甚至連結到下面的下一個問題),它表達了對意外行為的沮喪,因為無法區分“無梯度”和“零梯度”,這反過來又使得使用其他操作時難以推斷。
使用掩碼時,x/0 產生 NaN 梯度¶
在 Issue 4132 中,使用者提出 x.grad 應該是 [0, 1] 而不是 [nan, 1],而 MaskedTensor 透過完全掩蓋梯度,使其變得非常清晰。
當前結果
tensor([nan, 1.])
MaskedTensor 結果
MaskedTensor(
[ --, 1.0000]
)
torch.nansum() 和 torch.nanmean()¶
在 Issue 67180 中,梯度計算不正確(一個長期存在的問題),而 MaskedTensor 則可以正確處理它。
當前結果
a = torch.tensor([1., 2., float('nan')])
b = torch.tensor(1.0, requires_grad=True)
c = a * b
c1 = torch.nansum(c)
bgrad1, = torch.autograd.grad(c1, b, retain_graph=True)
bgrad1
tensor(nan)
MaskedTensor 結果
a = torch.tensor([1., 2., float('nan')])
b = torch.tensor(1.0, requires_grad=True)
mt = masked_tensor(a, ~torch.isnan(a))
c = mt * b
c1 = torch.sum(c)
bgrad1, = torch.autograd.grad(c1, b, retain_graph=True)
bgrad1
MaskedTensor( 3.0000, True)
安全的 Softmax¶
安全 softmax 是 一個經常出現的問題的又一個極佳示例。簡而言之,如果有一個完整的批次被“掩蓋掉”或完全由填充組成(在 softmax 的情況下,這意味著被設定為 -inf),那麼這將導致 NaNs,從而可能導致訓練發散。
幸運的是,MaskedTensor 解決了這個問題。考慮以下設定:
data = torch.randn(3, 3)
mask = torch.tensor([[True, False, False], [True, False, True], [False, False, False]])
x = data.masked_fill(~mask, float('-inf'))
mt = masked_tensor(data, mask)
print("x:\n", x)
print("mt:\n", mt)
x:
tensor([[ 0.2345, -inf, -inf],
[-0.1863, -inf, -0.6380],
[ -inf, -inf, -inf]])
mt:
MaskedTensor(
[
[ 0.2345, --, --],
[ -0.1863, --, -0.6380],
[ --, --, --]
]
)
例如,我們想要沿 dim=0 計算 softmax。請注意,第二列是“不安全”的(即完全被掩蓋掉),因此當計算 softmax 時,結果將是 0/0 = nan,因為 exp(-inf) = 0。然而,我們真正想要的是梯度被掩蓋掉,因為它們是未指定的,對訓練無效。
PyTorch 結果
x.softmax(0)
tensor([[0.6037, nan, 0.0000],
[0.3963, nan, 1.0000],
[0.0000, nan, 0.0000]])
MaskedTensor 結果
mt.softmax(0)
MaskedTensor(
[
[ 0.6037, --, --],
[ 0.3963, --, 1.0000],
[ --, --, --]
]
)
實現缺失的 torch.nan* 運算子¶
在 Issue 61474 中,有一個請求是新增額外的運算子來涵蓋各種 torch.nan* 應用,例如 torch.nanmax、torch.nanmin 等。
總的來說,這些問題更自然地適用於掩碼語義,因此我們建議使用 MaskedTensor 來代替引入額外的運算子。由於 nanmean 已經落地,我們可以將其作為比較點:
y:
tensor([ 0., 1., 4., 9., 0., 5., 12., 21., 0., 9., 20., 33., 0., 13.,
28., 45.])
z:
tensor([nan, 1., 4., 9., nan, 5., 12., 21., nan, 9., 20., 33., nan, 13.,
28., 45.])
print("y.mean():\n", y.mean())
print("z.nanmean():\n", z.nanmean())
# MaskedTensor successfully ignores the 0's
print("torch.mean(masked_tensor(y, y != 0)):\n", torch.mean(masked_tensor(y, y != 0)))
y.mean():
tensor(12.5000)
z.nanmean():
tensor(16.6667)
torch.mean(masked_tensor(y, y != 0)):
MaskedTensor( 16.6667, True)
在上面的示例中,我們構造了一個 y,並希望在忽略零值的情況下計算該系列的均值。torch.nanmean 可以用來實現這一點,但我們沒有其餘 torch.nan* 操作的實現。MaskedTensor 透過能夠使用基本操作來解決這個問題,而且我們已經支援該問題中列出的其他操作。例如:
torch.argmin(masked_tensor(y, y != 0))
MaskedTensor( 1.0000, True)
確實,忽略 0 時最小引數的索引是索引 1 中的 1。
MaskedTensor 在資料完全被掩蓋時也支援 reduction 操作,這等同於上面資料 Tensor 完全是 nan 的情況。nanmean 會返回 nan(一個模糊的返回值),而 MaskedTensor 會更準確地指示一個被掩蓋的結果。
x = torch.empty(16).fill_(float('nan'))
print("x:\n", x)
print("torch.nanmean(x):\n", torch.nanmean(x))
print("torch.nanmean via maskedtensor:\n", torch.mean(masked_tensor(x, ~torch.isnan(x))))
x:
tensor([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])
torch.nanmean(x):
tensor(nan)
torch.nanmean via maskedtensor:
MaskedTensor(--, False)
這與安全 softmax 問題類似,即 0/0 = nan,而我們真正想要的是一個未定義的值。
結論¶
在本教程中,我們介紹了 MaskedTensor 是什麼,演示瞭如何使用它們,並透過它們幫助解決的一系列示例和問題來闡明它們的價值。
進一步閱讀¶
要繼續瞭解更多,您可以查閱我們的 MaskedTensor 稀疏性教程,瞭解 MaskedTensor 如何實現稀疏性以及我們目前支援的不同儲存格式。
指令碼總執行時間: ( 0 分鐘 0.029 秒)