• 教學 >
  • (原型)使用 MaskedTensor 有效率地撰寫 Adagrad 的「稀疏」語義
捷徑

(原型)使用 MaskedTensor 有效率地撰寫 Adagrad 的「稀疏」語義

建立於:2022 年 10 月 28 日 | 最後更新:2022 年 10 月 28 日 | 最後驗證:未驗證

在完成本教學之前,請先檢閱 MaskedTensor 概觀稀疏性 教學。

簡介與動機

Issue 1369 討論了在為 Adagrad 撰寫「稀疏」語義時引入的額外程式碼行,但實際上,該程式碼使用稀疏性作為遮罩語義的代理,而不是稀疏性的預期用途:一種壓縮和最佳化技術。先前,我們透過引入一次性的語義和運算子來解決缺乏正式遮罩語義的問題,同時強迫使用者注意儲存細節,例如索引和值。

現在我們有了遮罩語義,我們可以更好地指出何時將稀疏性用作語義擴展。我們還將使用 MaskedTensor 比較和對比等效程式碼。最後,重複程式碼片段,沒有額外的註解,以顯示簡潔性的差異。

準備

import torch
import warnings

# Disable prototype warnings and such
warnings.filterwarnings(action='ignore', category=UserWarning)

# Some hyperparameters
eps = 1e-10
clr = 0.1

i = torch.tensor([[0, 1, 1], [2, 0, 2]])
v = torch.tensor([3, 4, 5], dtype=torch.float32)
grad = torch.sparse_coo_tensor(i, v, [2, 4])

使用 MaskedTensor 的更簡單程式碼

在我們深入研究細節之前,讓我們先更具體地介紹這個問題。我們將研究 PyTorch 中 Adagrad (functional) 的實作,最終目標是簡化並更忠實地表示遮罩方法。

作為參考,這是沒有遮罩梯度或稀疏性的常規密集程式碼路徑

state_sum.addcmul_(grad, grad, value=1)
std = state_sum.sqrt().add_(eps)
param.addcdiv_(grad, std, value=-clr)

用於稀疏性的 vanilla 張量實作是

def _make_sparse(grad, grad_indices, values):
    size = grad.size()
    if grad_indices.numel() == 0 or values.numel() == 0:
        return torch.empty_like(grad)
    return torch.sparse_coo_tensor(grad_indices, values, size)

grad = grad.coalesce()  # the update is non-linear so indices must be unique
grad_indices = grad._indices()
grad_values = grad._values()

state_sum.add_(_make_sparse(grad, grad_indices, grad_values.pow(2)))   # a different _make_sparse per layout
std = state_sum.sparse_mask(grad)
std_values = std._values().sqrt_().add_(eps)
param.add_(_make_sparse(grad, grad_indices, grad_values / std_values), alpha=-clr)

MaskedTensor 將程式碼最小化到以下片段

state_sum2 = state_sum2 + masked_grad.pow(2).get_data()
std2 = masked_tensor(state_sum2.to_sparse(), mask)
std2 = std2.sqrt().add(eps)
param2 = param2.add((masked_grad / std2).get_data(), alpha=-clr)

在本教學中,我們將逐行瀏覽每個實作,但乍看之下,我們可以注意到 (1) MaskedTensor 實作有多短,以及 (2) 它如何避免在密集和稀疏張量之間進行轉換。

原始稀疏實作

現在,讓我們用一些內聯註解來分解程式碼

def _make_sparse(grad, grad_indices, values):
    size = grad.size()
    if grad_indices.numel() == 0 or values.numel() == 0:
        return torch.empty_like(grad)
    return torch.sparse_coo_tensor(grad_indices, values, size)

# We don't support sparse gradients
param = torch.arange(8).reshape(2, 4).float()
state_sum = torch.full_like(param, 0.5)  # initial value for state sum

grad = grad.coalesce()  # the update is non-linear so indices must be unique
grad_indices = grad._indices()
grad_values = grad._values()
# pow(2) has the same semantics for both sparse and dense memory layouts since 0^2 is zero
state_sum.add_(_make_sparse(grad, grad_indices, grad_values.pow(2)))

# We take care to make std sparse, even though state_sum clearly is not.
# This means that we're only applying the gradient to parts of the state_sum
# for which it is specified. This further drives the point home that the passed gradient is not sparse, but masked.
# We currently dodge all these concerns using the private method `_values`.
std = state_sum.sparse_mask(grad)
std_values = std._values().sqrt_().add_(eps)

# Note here that we currently don't support div for sparse Tensors because zero / zero is not well defined,
# so we're forced to perform `grad_values / std_values` outside the sparse semantic and then convert back to a
# sparse tensor with `make_sparse`.
# We'll later see that MaskedTensor will actually handle these operations for us as well as properly denote
# undefined / undefined = undefined!
param.add_(_make_sparse(grad, grad_indices, grad_values / std_values), alpha=-clr)
tensor([[0.0000, 1.0000, 1.9027, 3.0000],
        [3.9015, 5.0000, 5.9010, 7.0000]])

倒數第三行 – std = state_sum.sparse_mask(grad) – 是我們有一個非常重要的分歧的地方。

技術上來說,eps 的增加應該應用於所有值,但實際上只應用於指定的值。在這裡,我們使用稀疏性作為語義擴展,並強制執行某種已定義和未定義值的模式。如果梯度的部分值為零,即使它們可以被其他稀疏儲存佈局壓縮,它們仍然包含在物化中。從理論上講,這是非常脆弱的!也就是說,有人可能會爭辯說 eps 總是微小的,因此在實踐中可能不太重要。

此外,作為儲存佈局和壓縮方案的稀疏性的實作 add_ 應該導致密集化,但我們為了效能而強制它不這樣做。對於這種一次性的情況來說,這是可以接受的...直到我們想要引入新的壓縮方案,例如 CSCBSRBSC。然後,我們需要為每個引入單獨的 Tensor 類型,並為使用不同儲存格式壓縮的梯度編寫變體,這很不方便,也不太可擴展或乾淨。

MaskedTensor 稀疏實作

我們一直在將稀疏性作為一種最佳化與稀疏性作為 PyTorch 的一種語義擴展混為一談。MaskedTensor 建議將稀疏性最佳化與語義擴展分開;例如,目前我們無法擁有具有稀疏儲存的密集語義,或者具有密集儲存的遮罩語義。MaskedTensor 透過有目的地將儲存與語義分離來實現這些想法。

考慮使用遮罩梯度的上述範例

# Let's now import MaskedTensor!
from torch.masked import masked_tensor

# Create an entirely new set of parameters to avoid errors
param2 = torch.arange(8).reshape(2, 4).float()
state_sum2 = torch.full_like(param, 0.5)  # initial value for state sum

mask = (grad.to_dense() != 0).to_sparse()
masked_grad = masked_tensor(grad, mask)

state_sum2 = state_sum2 + masked_grad.pow(2).get_data()
std2 = masked_tensor(state_sum2.to_sparse(), mask)

# We can add support for in-place operations later. Notice how this doesn't
# need to access any storage internals and is in general a lot shorter
std2 = std2.sqrt().add(eps)

param2 = param2.add((masked_grad / std2).get_data(), alpha=-clr)

請注意,實作看起來非常相似,但 MaskedTensor 實作更短更簡單。特別是,圍繞 _make_sparse 的許多樣板程式碼(以及需要每個佈局都有單獨的實作)都由 MaskedTensor 為使用者處理。

此時,讓我們列印此版本和原始版本,以便更容易比較

print("state_sum:\n", state_sum)
print("state_sum2:\n", state_sum2)
state_sum:
 tensor([[ 0.5000,  0.5000,  9.5000,  0.5000],
        [16.5000,  0.5000, 25.5000,  0.5000]])
state_sum2:
 tensor([[ 0.5000,  0.5000,  9.5000,  0.5000],
        [16.5000,  0.5000, 25.5000,  0.5000]])
print("std:\n", std)
print("std2:\n", std2)
std:
 tensor(indices=tensor([[0, 1, 1],
                       [2, 0, 2]]),
       values=tensor([3.0822, 4.0620, 5.0498]),
       size=(2, 4), nnz=3, layout=torch.sparse_coo)
std2:
 MaskedTensor(
  [
    [      --,       --,   3.0822,       --],
    [  4.0620,       --,   5.0498,       --]
  ]
)
print("param:\n", param)
print("param2:\n", param2)
param:
 tensor([[0.0000, 1.0000, 1.9027, 3.0000],
        [3.9015, 5.0000, 5.9010, 7.0000]])
param2:
 tensor([[0.0000, 1.0000, 1.9027, 3.0000],
        [3.9015, 5.0000, 5.9010, 7.0000]])

結論

在本教學中,我們討論了原生遮罩語意如何使 Adagrad 在 PyTorch 中現有實作的開發者體驗更加簡潔,該實作過去使用稀疏性作為編寫遮罩語意的代理。但更重要的是,允許遮罩語意透過 MaskedTensor 成為一級公民,消除了對稀疏性或不可靠的技巧來模擬遮罩的依賴,從而實現適當的獨立性和開發,同時啟用稀疏語意,例如這個。

延伸閱讀

若要繼續深入了解,您可以參考我們(目前)關於 MaskedTensor 進階語意的最終評論,以了解 MaskedTensor 和 NumPy 的 MaskedArray 之間在設計決策以及歸約語意上的一些差異。

腳本總執行時間: ( 0 分鐘 0.017 秒)

由 Sphinx-Gallery 產生圖庫


評價本教學

© 版權所有 2024,PyTorch。

使用 Sphinx 建構,並採用 theme,由 Read the Docs 提供。

文件

取得 PyTorch 的全面開發者文件

檢視文件

教學

取得為初學者和進階開發者提供的深度教學課程

檢視教學課程

資源

尋找開發資源並取得您問題的解答

檢視資源