Module sortedness.embedding.surrogate
Expand source code
#  Copyright (c) 2023. Davi Pereira dos Santos
#  This file is part of the sortedness project.
#  Please respect the license - more about this in the section (*) below.
#
#  sortedness is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  sortedness is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with sortedness.  If not, see <http://www.gnu.org/licenses/>.
#
#  (*) Removing authorship by any means, e.g. by distribution of derived
#  works or verbatim, obfuscated, compiled or rewritten versions of any
#  part of this work is illegal and it is unethical regarding the effort and
#  time spent here.
#
import math
from math import pi
import torch
from scipy.stats import weightedtau, kendalltau
from torch import tanh, sum, topk, sqrt, abs
cau = lambda r, gamma=1: 1 / pi * gamma / (gamma ** 2 + r ** 2)
har = lambda r: 1 / (r + 1)
def pdiffs(x):
    dis = x.unsqueeze(1) - x
    indices = torch.triu_indices(*dis.shape, offset=1)
    return dis[indices[0], indices[1]]
def psums(x):
    dis = x.unsqueeze(1) + x
    indices = torch.triu_indices(*dis.shape, offset=1)
    return dis[indices[0], indices[1]]
def surrogate_tau(a, b, smooothness):
    """
    >>> from torch import tensor
    >>> from scipy.stats import kendalltau
    >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), .01)
    tensor(1.)
    >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]), .01)
    tensor(-1.)
    >>> round(float(surrogate_tau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]), .01)), 6)
    0.948683
    >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6)
    0.948683
    """
    da, db = pdiffs(a), pdiffs(b)
    ta, tb = tanh(da / smooothness), tanh(db / smooothness)
    r = ta * tb
    return sum(r) / sqrt(sum(abs(ta)) * sum(abs(tb)))
def surrogate_wtau(a, b, w, smooothness):
    """
    >>> from torch import tensor
    >>> from scipy.stats import kendalltau
    >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]),  tensor([1,2,3,4,5]), .01)
    tensor(1.)
    >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]),  tensor([1,2,3,4,5]), .01)
    tensor(-1.)
    >>> round(float(surrogate_wtau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]),  tensor([1,1,1,1,1]), .000001)), 6)
    0.948683
    >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6)
    0.948683
    """
    da, db, sw = pdiffs(a), pdiffs(b), psums(w)
    ta, tb = tanh(da / smooothness), tanh(db / smooothness)
    r = ta * tb * sw
    return sum(r) / sqrt(sum(abs(ta * sw)) * sum(abs(tb * sw)))
def geomean(lo, gl, beta=0.5):
    """
    >>> from torch import tensor
    >>> geomean(tensor([0.6]), tensor([0.64]))
    tensor([0.6199])
    """
    l = (lo + 1) / 2
    g = (gl + 1) / 2
    return torch.exp((1 - beta) * torch.log(l) + beta * torch.log(g)) * 2 - 1
def geomean_np(lo, gl, beta=0.5):
    """
    >>> round(geomean_np(0.6, 0.64), 4)
    0.6199
    """
    l = (lo + 1) / 2
    g = (gl + 1) / 2
    return math.exp((1 - beta) * math.log(l) + beta * math.log(g)) * 2 - 1
def loss_function(predicted_D, expected_D, k, global_k, w, beta=0.5, smooothness_tau=1, min_global_k=100, max_global_k=1000, ref=False):
    n, v = predicted_D.shape  # REMINDER: n can be the size of the batch
    if global_k == "sqrt":
        global_k = max(min_global_k, min(max_global_k, int(math.sqrt(v))))
    if global_k > v:
        global_k = v
    if k + 1 > v:
        k = v - 1
    mu = mu_local = mu_global = tau_local = tau_global = 0
    rnd_idxs = torch.randperm(v)
    start = 0
    for pred_d, target_d in zip(predicted_D, expected_D):
        # REMINDER: o próprio ponto vai ser usado/amostrado 1 vez como sendo vizinho de si mesmo, não vale o custo de remover:
        # Impacta significado da medida em datasets muito pequenos, deixando ela marginalmente mais otimista
        # Pra global não impacta otimização, pra local fiz k+1 e slicing abaixo pra não perder o primeiro peso de w.
        pass
        # local
        a, idxs = topk(pred_d, k + 1, largest=False)
        a, idxs = a[1:], idxs[1:]
        b = target_d[idxs]
        mu_local += (mu_local0 := surrogate_wtau(a, b, w[:k], smooothness_tau))
        # global
        end = start + global_k
        if end > v:
            start = 0
            end = global_k
            rnd_idxs = torch.randperm(v)
        gidxs = rnd_idxs[start:end]
        ga = pred_d[gidxs]
        gb = target_d[gidxs]
        # ga = pred_d
        # gb = target_d
        mu_global += (mu_global0 := surrogate_tau(ga, gb, smooothness_tau))
        start += global_k
        mu += geomean(mu_local0, mu_global0, beta)
        if ref:
            tau_local += weightedtau(a.cpu().detach().numpy(), b.cpu().detach().numpy(), weigher=lambda r: w[r], rank=False)[0]
            p, t = ga.cpu().detach().numpy(), gb.cpu().detach().numpy()
            tau_global += kendalltau(p, t)[0]
    return mu / n, mu_local / n, mu_global / n, tau_local / n, tau_global / n
Functions
def cau(r, gamma=1)- 
Expand source code
cau = lambda r, gamma=1: 1 / pi * gamma / (gamma ** 2 + r ** 2) def geomean(lo, gl, beta=0.5)- 
>>> from torch import tensor >>> geomean(tensor([0.6]), tensor([0.64])) tensor([0.6199])Expand source code
def geomean(lo, gl, beta=0.5): """ >>> from torch import tensor >>> geomean(tensor([0.6]), tensor([0.64])) tensor([0.6199]) """ l = (lo + 1) / 2 g = (gl + 1) / 2 return torch.exp((1 - beta) * torch.log(l) + beta * torch.log(g)) * 2 - 1 def geomean_np(lo, gl, beta=0.5)- 
>>> round(geomean_np(0.6, 0.64), 4) 0.6199Expand source code
def geomean_np(lo, gl, beta=0.5): """ >>> round(geomean_np(0.6, 0.64), 4) 0.6199 """ l = (lo + 1) / 2 g = (gl + 1) / 2 return math.exp((1 - beta) * math.log(l) + beta * math.log(g)) * 2 - 1 def har(r)- 
Expand source code
har = lambda r: 1 / (r + 1) def loss_function(predicted_D, expected_D, k, global_k, w, beta=0.5, smooothness_tau=1, min_global_k=100, max_global_k=1000, ref=False)- 
Expand source code
def loss_function(predicted_D, expected_D, k, global_k, w, beta=0.5, smooothness_tau=1, min_global_k=100, max_global_k=1000, ref=False): n, v = predicted_D.shape # REMINDER: n can be the size of the batch if global_k == "sqrt": global_k = max(min_global_k, min(max_global_k, int(math.sqrt(v)))) if global_k > v: global_k = v if k + 1 > v: k = v - 1 mu = mu_local = mu_global = tau_local = tau_global = 0 rnd_idxs = torch.randperm(v) start = 0 for pred_d, target_d in zip(predicted_D, expected_D): # REMINDER: o próprio ponto vai ser usado/amostrado 1 vez como sendo vizinho de si mesmo, não vale o custo de remover: # Impacta significado da medida em datasets muito pequenos, deixando ela marginalmente mais otimista # Pra global não impacta otimização, pra local fiz k+1 e slicing abaixo pra não perder o primeiro peso de w. pass # local a, idxs = topk(pred_d, k + 1, largest=False) a, idxs = a[1:], idxs[1:] b = target_d[idxs] mu_local += (mu_local0 := surrogate_wtau(a, b, w[:k], smooothness_tau)) # global end = start + global_k if end > v: start = 0 end = global_k rnd_idxs = torch.randperm(v) gidxs = rnd_idxs[start:end] ga = pred_d[gidxs] gb = target_d[gidxs] # ga = pred_d # gb = target_d mu_global += (mu_global0 := surrogate_tau(ga, gb, smooothness_tau)) start += global_k mu += geomean(mu_local0, mu_global0, beta) if ref: tau_local += weightedtau(a.cpu().detach().numpy(), b.cpu().detach().numpy(), weigher=lambda r: w[r], rank=False)[0] p, t = ga.cpu().detach().numpy(), gb.cpu().detach().numpy() tau_global += kendalltau(p, t)[0] return mu / n, mu_local / n, mu_global / n, tau_local / n, tau_global / n def pdiffs(x)- 
Expand source code
def pdiffs(x): dis = x.unsqueeze(1) - x indices = torch.triu_indices(*dis.shape, offset=1) return dis[indices[0], indices[1]] def psums(x)- 
Expand source code
def psums(x): dis = x.unsqueeze(1) + x indices = torch.triu_indices(*dis.shape, offset=1) return dis[indices[0], indices[1]] def surrogate_tau(a, b, smooothness)- 
>>> from torch import tensor >>> from scipy.stats import kendalltau >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), .01) tensor(1.) >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]), .01) tensor(-1.) >>> round(float(surrogate_tau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]), .01)), 6) 0.948683 >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6) 0.948683Expand source code
def surrogate_tau(a, b, smooothness): """ >>> from torch import tensor >>> from scipy.stats import kendalltau >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), .01) tensor(1.) >>> surrogate_tau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]), .01) tensor(-1.) >>> round(float(surrogate_tau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]), .01)), 6) 0.948683 >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6) 0.948683 """ da, db = pdiffs(a), pdiffs(b) ta, tb = tanh(da / smooothness), tanh(db / smooothness) r = ta * tb return sum(r) / sqrt(sum(abs(ta)) * sum(abs(tb))) def surrogate_wtau(a, b, w, smooothness)- 
>>> from torch import tensor >>> from scipy.stats import kendalltau >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), .01) tensor(1.) >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]), tensor([1,2,3,4,5]), .01) tensor(-1.) >>> round(float(surrogate_wtau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]), tensor([1,1,1,1,1]), .000001)), 6) 0.948683 >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6) 0.948683Expand source code
def surrogate_wtau(a, b, w, smooothness): """ >>> from torch import tensor >>> from scipy.stats import kendalltau >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), tensor([1,2,3,4,5]), .01) tensor(1.) >>> surrogate_wtau(tensor([1,2,3,4,5]), tensor([5,4,3,2,1]), tensor([1,2,3,4,5]), .01) tensor(-1.) >>> round(float(surrogate_wtau(tensor([1,2,2,4,5]), tensor([1,2,3,4,5]), tensor([1,1,1,1,1]), .000001)), 6) 0.948683 >>> round(kendalltau([1,2,2,4,5], [1,2,3,4,5])[0], 6) 0.948683 """ da, db, sw = pdiffs(a), pdiffs(b), psums(w) ta, tb = tanh(da / smooothness), tanh(db / smooothness) r = ta * tb * sw return sum(r) / sqrt(sum(abs(ta * sw)) * sum(abs(tb * sw)))