Source code for dlordinal.soft_labelling.triangular_distribution

import math

import numpy as np

from .utils import get_intervals, triangular_cdf


[docs] def get_triangular_soft_labels(J: int, alpha2: float = 0.01, verbose: int = 0): """ Get soft labels using triangular distributions for ``J`` classes or splits using the approach described in :footcite:t:`vargas2023softlabelling`. The :math:`[0,1]` interval is split into ``J`` intervals and the probability for each interval is computed as the difference between the value of the triangular distribution function for the interval boundaries. The probability for the first interval is computed as the value of the triangular distribution function for the first interval boundary. The triangular distribution function is denoted as :math:`\\text{p}(x, a, b, c)` where :math:`a`, :math:`b` and :math:`c` are the parameters of the distribution, and are determined by the number of classes :math:`J` and the value of the :math:`\\alpha_2` parameter. The value of :math:`\\alpha_2` represents the probability that is assigned to the adjacent classes of the target class. The parameters :math:`a`, :math:`b` and :math:`c` for class :math:`j` are computed as follows: .. math:: a_j = \\begin{cases} 0 & \\text{if } j = 1 \\\\ \\frac{2j - 2 - 4j\\alpha_2 + 2\\alpha_2 \\pm \\sqrt{2\\alpha_2}}{2n(1 - 2\\alpha_2)} & \\text{if } 1 < j < J\\\\ 1 + \\frac{1}{\\pm J (\\sqrt{\\alpha_3} - 1)} & \\text{if } j = J \\end{cases} .. math:: b_j = \\begin{cases} \\frac{1}{(1 - \\sqrt{\\alpha_1})n} & \\text{if } j = 1 \\\\ \\frac{2j - 4j\\alpha_2 + 2\\alpha_2 \\pm \\sqrt{2\\alpha_2}}{2n(1 - 2\\alpha_2)} & \\text{if } 1 < j < J\\\\ 1 & \\text{if } j = J \\end{cases} .. math:: c_j = \\begin{cases} 0 & \\text{if } j = 1 \\\\ \\frac{a + b}{2} & \\text{if } 1 < j < J\\\\ 1 & \\text{if } j = J \\end{cases} The value of :math:`\\alpha_1`, that represents the error for the first class, is computed as follows: .. math:: \\alpha_1 = \\left(\\frac{1 - \\sqrt{1 - 4(1 - 2\\alpha_2)(2\\alpha_2 - \\sqrt{2\\alpha_2})}}{2}\\right)^2 The value of :math:`\\alpha_3`, that represents the error for the last class, is computed as follows: .. math:: \\alpha_3 = \\left(\\frac{1 - \\sqrt{1 - 4\\left(\\frac{J - 1}{J}\\right)^2(1 - 2\\alpha_2)(\\sqrt{2\\alpha_2} (-1 + \\sqrt{2\\alpha_2}))}}{2}\\right)^2 The value of :math:`\\alpha_2` is given by the user. Parameters ---------- J : int Number of classes or splits (:math:`J`). alpha2 : float, optional, default=0.01 Value of the :math:`\\alpha_2` parameter. verbose : int, optional, default=0 Verbosity level. Raises ------ ValueError If ``J`` is not a positive integer greater than 1. If ``alpha2`` is not a float between 0 and 1. Returns ------- probs : 2d array-like of shape (J, J) Matrix of probabilities where each row represents the true class and each column the probability for class j. Example ------- >>> from dlordinal.soft_labelling import get_triangular_soft_labels >>> get_triangular_soft_labels(5) array([[0.98845494, 0.01154505, 0. , 0. , 0. ], [0.01 , 0.98 , 0.01 , 0. , 0. ], [0. , 0.01 , 0.98 , 0.01 , 0. ], [0. , 0. , 0.01 , 0.98 , 0.01 ], [0. , 0. , 0. , 0.00505524, 0.99494475]]) """ if J < 2 or not isinstance(J, int): raise ValueError(f"{J=} must be a positive integer greater than 1") if alpha2 < 0 or alpha2 > 1: raise ValueError(f"{alpha2=} must be a float between 0 and 1") if verbose >= 1: print(f"Computing triangular probabilities for {J=} and {alpha2=}...") def compute_alpha1(alpha2): c_minus = (1 - 2 * alpha2) * (2 * alpha2 - math.sqrt(2 * alpha2)) return pow((1 - math.sqrt(1 - 4 * c_minus)) / 2, 2) def compute_alpha3(alpha2): c1 = ( pow((J - 1) / J, 2) * (1 - 2 * alpha2) * (math.sqrt(2 * alpha2) * (-1 + math.sqrt(2 * alpha2))) ) return pow((1 - math.sqrt(1 - 4 * c1)) / 2, 2) alpha1 = compute_alpha1(alpha2) alpha3 = compute_alpha3(alpha2) if verbose >= 1: print(f"{alpha1=}, {alpha2=}, {alpha3=}") def b1(J): return 1.0 / ((1.0 - math.sqrt(alpha1)) * J) def aj(J, j): num1 = 2.0 * j - 2 - 4 * j * alpha2 + 2 * alpha2 num2 = math.sqrt(2 * alpha2) den = 2.0 * J * (1 - 2 * alpha2) max_value = (j - 1.0) / J # +- return ( (num1 + num2) / den if (num1 + num2) / den < max_value else (num1 - num2) / den ) def bj(J, j): num1 = 2.0 * j - 4 * j * alpha2 + 2 * alpha2 num2 = math.sqrt(2 * alpha2) den = 2.0 * J * (1 - 2 * alpha2) min_value = j / J # +- return ( (num1 + num2) / den if (num1 + num2) / den > min_value else (num1 - num2) / den ) def aJ(J): aJ_plus = 1.0 + 1.0 / (J * (math.sqrt(alpha3) - 1.0)) aJ_minus = 1.0 + 1.0 / (-J * (math.sqrt(alpha3) - 1.0)) return aJ_plus if aJ_plus > 0.0 else aJ_minus if verbose >= 3: print(f"{b1(J)=}, {aJ(J)=}, {aj(J, 1)=}, {bj(J,1)=}") for i in range(1, J + 1): print(f"{i=} {aj(J, i)=}, {bj(J,i)=}") intervals = get_intervals(J) probs = [] # Compute probability for each interval (class) using the distribution function. for j in range(1, J + 1): j_probs = [] if j == 1: a = 0.0 b = b1(J) c = 0.0 elif j == J: a = aJ(J) b = 1.0 c = 1.0 else: a = aj(J, j) b = bj(J, j) c = (a + b) / 2.0 if verbose >= 1: print(f"Class: {j}, {a=}, {b=}, {c=}, (j-1)/J={(j-1)/J}, (j/J)={j/J}") for interval in intervals: j_probs.append( triangular_cdf(interval[1], a, b, c) - triangular_cdf(interval[0], a, b, c) ) if verbose >= 2: print(f"\tinterval: {interval}, prob={j_probs[-1]}") probs.append(j_probs) return np.array(probs)