注意
点击here下载完整的示例代码
(原型) 使用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(功能)实现,最终目标是简化和更忠实地表示掩码方法。
作为参考,这是没有掩码梯度或稀疏性的常规密集代码路径:
稀疏的普通张量实现是:
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_应该会导致密集化,但出于性能考虑,我们强制它不这样做。对于这种一次性情况,这是可以的...直到我们想要引入新的压缩方案,例如CSC、BSR或BSC。然后,我们需要为每种方案引入单独的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, --]
]
)
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]])
结论¶
在本教程中,我们讨论了原生掩码语义如何为PyTorch中现有的Adagrad实现提供更清晰的开发者体验,该实现使用稀疏性作为编写掩码语义的代理。但更重要的是,通过MaskedTensor允许掩码语义成为一等公民,消除了对稀疏性或不可靠技巧的依赖来模拟掩码,从而允许适当的独立性和开发,同时启用稀疏语义,例如这个。
进一步阅读¶
要继续学习更多内容,您可以查看我们目前的最终回顾
MaskedTensor 高级语义
以了解MaskedTensor与NumPy的MaskedArray在设计决策上的一些差异,以及归约语义。
脚本总运行时间: ( 0 分钟 0.018 秒)