ludwig-ai--ludwig
593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
299 行
9.7 KiB
Python
299 行
9.7 KiB
Python
"""Bisection implementation of alpha-entmax (Peters et al., 2019).
|
|
|
|
Backward pass wrt alpha per (Correia et al., 2019). See https://arxiv.org/pdf/1905.05702 for detailed description.
|
|
"""
|
|
|
|
# Author: Goncalo M Correia
|
|
# Author: Ben Peters
|
|
# Author: Vlad Niculae <vlad@vene.ro>
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
from torch.autograd import Function
|
|
|
|
|
|
class EntmaxBisectFunction(Function):
|
|
@classmethod
|
|
def _gp(cls, x, alpha):
|
|
return x ** (alpha - 1)
|
|
|
|
@classmethod
|
|
def _gp_inv(cls, y, alpha):
|
|
return y ** (1 / (alpha - 1))
|
|
|
|
@classmethod
|
|
def _p(cls, X, alpha):
|
|
return cls._gp_inv(torch.clamp(X, min=0), alpha)
|
|
|
|
@classmethod
|
|
def forward(cls, ctx, X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True):
|
|
p_m, backward_kwargs = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls)
|
|
|
|
ctx.alpha = backward_kwargs["alpha"]
|
|
ctx.dim = backward_kwargs["dim"]
|
|
ctx.save_for_backward(p_m)
|
|
return p_m
|
|
|
|
@classmethod
|
|
def backward(cls, ctx, dY):
|
|
(Y,) = ctx.saved_tensors
|
|
|
|
gppr = torch.where(Y > 0, Y ** (2 - ctx.alpha), Y.new_zeros(1))
|
|
|
|
dX = dY * gppr
|
|
q = dX.sum(ctx.dim) / gppr.sum(ctx.dim)
|
|
q = q.unsqueeze(ctx.dim)
|
|
dX -= q * gppr
|
|
|
|
d_alpha = None
|
|
if ctx.needs_input_grad[1]:
|
|
# alpha gradient computation
|
|
# d_alpha = (partial_y / partial_alpha) * dY
|
|
# NOTE: ensure alpha is not close to 1
|
|
# since there is an indetermination
|
|
# batch_size, _ = dY.shape
|
|
|
|
# shannon terms
|
|
S = torch.where(Y > 0, Y * torch.log(Y), Y.new_zeros(1))
|
|
# shannon entropy
|
|
ent = S.sum(ctx.dim).unsqueeze(ctx.dim)
|
|
Y_skewed = gppr / gppr.sum(ctx.dim).unsqueeze(ctx.dim)
|
|
|
|
d_alpha = dY * (Y - Y_skewed) / ((ctx.alpha - 1) ** 2)
|
|
d_alpha -= dY * (S - Y_skewed * ent) / (ctx.alpha - 1)
|
|
d_alpha = d_alpha.sum(ctx.dim).unsqueeze(ctx.dim)
|
|
|
|
return dX, d_alpha, None, None, None
|
|
|
|
|
|
def _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls=EntmaxBisectFunction):
|
|
if not isinstance(alpha, torch.Tensor):
|
|
alpha = torch.tensor(alpha, dtype=X.dtype, device=X.device)
|
|
|
|
alpha_shape = list(X.shape)
|
|
alpha_shape[dim] = 1
|
|
alpha = alpha.expand(*alpha_shape)
|
|
|
|
d = X.shape[dim]
|
|
|
|
max_val, _ = X.max(dim=dim, keepdim=True)
|
|
X = X * (alpha - 1)
|
|
max_val = max_val * (alpha - 1)
|
|
|
|
# Note: when alpha < 1, tau_lo > tau_hi. This still works since dm < 0.
|
|
tau_lo = max_val - cls._gp(1, alpha)
|
|
tau_hi = max_val - cls._gp(1 / d, alpha)
|
|
|
|
f_lo = cls._p(X - tau_lo, alpha).sum(dim) - 1
|
|
|
|
dm = tau_hi - tau_lo
|
|
|
|
for _it in range(n_iter):
|
|
dm /= 2
|
|
tau_m = tau_lo + dm
|
|
p_m = cls._p(X - tau_m, alpha)
|
|
f_m = p_m.sum(dim) - 1
|
|
|
|
mask = (f_m * f_lo >= 0).unsqueeze(dim)
|
|
tau_lo = torch.where(mask, tau_m, tau_lo)
|
|
|
|
if ensure_sum_one:
|
|
p_m /= p_m.sum(dim=dim).unsqueeze(dim=dim)
|
|
|
|
return p_m, {"alpha": alpha, "dim": dim}
|
|
|
|
|
|
# slightly more efficient special case for sparsemax
|
|
class SparsemaxBisectFunction(EntmaxBisectFunction):
|
|
@classmethod
|
|
def _gp(cls, x, alpha):
|
|
return x
|
|
|
|
@classmethod
|
|
def _gp_inv(cls, y, alpha):
|
|
return y
|
|
|
|
@classmethod
|
|
def _p(cls, x, alpha):
|
|
return torch.clamp(x, min=0)
|
|
|
|
@classmethod
|
|
def forward(cls, ctx, X, dim=-1, n_iter=50, ensure_sum_one=True):
|
|
p_m, backward_kwargs = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one)
|
|
|
|
ctx.alpha = backward_kwargs["alpha"]
|
|
ctx.dim = backward_kwargs["dim"]
|
|
ctx.save_for_backward(p_m)
|
|
return p_m
|
|
|
|
@classmethod
|
|
def backward(cls, ctx, dY):
|
|
(Y,) = ctx.saved_tensors
|
|
gppr = (Y > 0).to(dtype=dY.dtype)
|
|
dX = dY * gppr
|
|
q = dX.sum(ctx.dim) / gppr.sum(ctx.dim)
|
|
q = q.unsqueeze(ctx.dim)
|
|
dX -= q * gppr
|
|
return dX, None, None, None
|
|
|
|
|
|
def _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one):
|
|
return _entmax_bisect_forward(X, alpha=2, dim=dim, n_iter=50, ensure_sum_one=True, cls=SparsemaxBisectFunction)
|
|
|
|
|
|
def entmax_bisect(X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True, training=True):
|
|
"""alpha-entmax: normalizing sparse transform (a la softmax).
|
|
|
|
Solves the optimization problem:
|
|
|
|
max_p <x, p> - H_a(p) s.t. p >= 0, sum(p) == 1.
|
|
|
|
where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1,
|
|
using a bisection (root finding, binary search) algorithm.
|
|
|
|
This function is differentiable with respect to both X and alpha.
|
|
|
|
Parameters
|
|
----------
|
|
X : torch.Tensor
|
|
The input tensor.
|
|
|
|
alpha : float or torch.Tensor
|
|
Tensor of alpha parameters (> 1) to use. If scalar
|
|
or python float, the same value is used for all rows, otherwise,
|
|
it must have shape (or be expandable to)
|
|
alpha.shape[j] == (X.shape[j] if j != dim else 1)
|
|
A value of alpha=2 corresponds to sparsemax, and alpha=1 would in theory recover
|
|
softmax. For numeric reasons, this algorithm does not work with `alpha=1`: if you
|
|
want softmax, we recommend `torch.nn.softmax`.
|
|
|
|
dim : int
|
|
The dimension along which to apply alpha-entmax.
|
|
|
|
n_iter : int
|
|
Number of bisection iterations. For float32, 24 iterations should
|
|
suffice for machine precision.
|
|
|
|
ensure_sum_one : bool,
|
|
Whether to divide the result by its sum. If false, the result might
|
|
sum to close but not exactly 1, which might cause downstream problems.
|
|
|
|
Returns
|
|
-------
|
|
P : torch tensor, same shape as X
|
|
The projection result, such that P.sum(dim=dim) == 1 elementwise.
|
|
"""
|
|
# Avoids call to custom autograd.Function during eval to ensure torchscript compatibility
|
|
# custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053
|
|
if not training:
|
|
output, _ = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one)
|
|
return output
|
|
return EntmaxBisectFunction.apply(X, alpha, dim, n_iter, ensure_sum_one)
|
|
|
|
|
|
def sparsemax_bisect(X, dim=-1, n_iter=50, ensure_sum_one=True, training=True):
|
|
"""sparsemax: normalizing sparse transform (a la softmax), via bisection.
|
|
|
|
Solves the projection:
|
|
|
|
min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1.
|
|
|
|
Parameters
|
|
----------
|
|
X : torch.Tensor
|
|
The input tensor.
|
|
|
|
dim : int
|
|
The dimension along which to apply sparsemax.
|
|
|
|
n_iter : int
|
|
Number of bisection iterations. For float32, 24 iterations should
|
|
suffice for machine precision.
|
|
|
|
ensure_sum_one : bool,
|
|
Whether to divide the result by its sum. If false, the result might
|
|
sum to close but not exactly 1, which might cause downstream problems.
|
|
|
|
Note: This function does not yet support normalizing along anything except
|
|
the last dimension. Please use transposing and views to achieve more
|
|
general behavior.
|
|
|
|
Returns
|
|
-------
|
|
P : torch tensor, same shape as X
|
|
The projection result, such that P.sum(dim=dim) == 1 elementwise.
|
|
"""
|
|
# Avoids call to custom autograd.Function during eval to ensure torchscript compatibility
|
|
# custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053
|
|
if not training:
|
|
output, _ = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one)
|
|
return output
|
|
return SparsemaxBisectFunction.apply(X, dim, n_iter, ensure_sum_one)
|
|
|
|
|
|
class SparsemaxBisect(nn.Module):
|
|
def __init__(self, dim=-1, n_iter=None):
|
|
"""sparsemax: normalizing sparse transform (a la softmax) via bisection
|
|
|
|
Solves the projection:
|
|
|
|
min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1.
|
|
|
|
Parameters
|
|
----------
|
|
dim : int
|
|
The dimension along which to apply sparsemax.
|
|
|
|
n_iter : int
|
|
Number of bisection iterations. For float32, 24 iterations should
|
|
suffice for machine precision.
|
|
"""
|
|
self.dim = dim
|
|
self.n_iter = n_iter
|
|
super().__init__()
|
|
|
|
def forward(self, X):
|
|
return sparsemax_bisect(X, dim=self.dim, n_iter=self.n_iter, training=self.training)
|
|
|
|
|
|
class EntmaxBisect(nn.Module):
|
|
def __init__(self, alpha=1.5, dim=-1, n_iter=50):
|
|
"""alpha-entmax: normalizing sparse map (a la softmax) via bisection.
|
|
|
|
Solves the optimization problem:
|
|
|
|
max_p <x, p> - H_a(p) s.t. p >= 0, sum(p) == 1.
|
|
|
|
where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1,
|
|
using a bisection (root finding, binary search) algorithm.
|
|
|
|
Parameters
|
|
----------
|
|
alpha : float or torch.Tensor
|
|
Tensor of alpha parameters (> 1) to use. If scalar
|
|
or python float, the same value is used for all rows, otherwise,
|
|
it must have shape (or be expandable to)
|
|
alpha.shape[j] == (X.shape[j] if j != dim else 1)
|
|
A value of alpha=2 corresponds to sparsemax; and alpha=1 would in theory recover
|
|
softmax. For numeric reasons, this algorithm does not work with `alpha=1`; if you
|
|
want softmax, we recommend `torch.nn.softmax`.
|
|
|
|
dim : int
|
|
The dimension along which to apply alpha-entmax.
|
|
|
|
n_iter : int
|
|
Number of bisection iterations. For float32, 24 iterations should
|
|
suffice for machine precision.
|
|
|
|
"""
|
|
super().__init__()
|
|
self.dim = dim
|
|
self.n_iter = n_iter
|
|
if isinstance(alpha, torch.Tensor):
|
|
self.register_buffer("alpha", alpha)
|
|
else:
|
|
self.alpha = alpha
|
|
|
|
def forward(self, X):
|
|
return entmax_bisect(X, alpha=self.alpha, dim=self.dim, n_iter=self.n_iter, training=self.training)
|