Source code for mulearn

__version__ = '1.0.6'


import copy
import logging
import warnings

import numpy as np
from scipy.optimize import OptimizeWarning
from sklearn.base import BaseEstimator, RegressorMixin
from sklearn.exceptions import NotFittedError, FitFailedWarning
from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
from sklearn.utils import check_random_state

import mulearn.fuzzifier as fuzzifier
import mulearn.kernel as kernel
from mulearn.optimization import GurobiSolver

logger = logging.getLogger(__name__)

warnings.filterwarnings("ignore", category=OptimizeWarning)
warnings.filterwarnings("ignore", category=FitFailedWarning)


[docs]class FuzzyInductor(BaseEstimator, RegressorMixin): """FuzzyInductor class."""
[docs] def __init__(self, c=1, k=kernel.GaussianKernel(), fuzzifier=fuzzifier.ExponentialFuzzifier(), solver=GurobiSolver(), random_state=None): r"""Create an instance of :class:`FuzzyInductor`. :param c: Trade-off constant, defaults to 1. :type c: `float` :param k: Kernel function, defaults to :class:`GaussianKernel()`. :type k: :class:`mulearn.kernel.Kernel` :param fuzzifier: fuzzifier mapping distance values to membership degrees, defaults to `ExponentialFuzzifier()`. :type fuzzifier: :class:`mulearn.fuzzifier.Fuzzifier` :param solver: Solver to be used to obtain the optimization problem solution, defaults to `GurobiSolver()`. :type solver: :class:`mulearn.optimization.Solver` :param random_state: Seed of the pseudorandom generator. :type random_state: `int` """ self.c = c self.k = k self.fuzzifier = fuzzifier self.solver = solver self.random_state = random_state self.estimated_membership_ = None self.chis_ = None self.gram_ = None self.fixed_term_ = None
def __repr__(self, **kwargs): return f"FuzzyInductor(c={self.c}, k={self.k}, f={self.fuzzifier}, " \ f"solver={self.solver})" def __eq__(self, other): """Check equality w.r.t. other objects.""" equal = (type(self) is type(other) and \ self.c == other.c and self.k == other.k and \ self.fuzzifier == other.fuzzifier) if 'chis_' in self.__dict__: if 'chis_' not in other.__dict__: return False else: return equal and (self.chis_ == other.chis_) def x_to_sq_dist(self, X_new): X_new = np.array(X_new) t1 = self.k.compute(X_new, X_new) t2 = np.array([self.k.compute(x_i, X_new) for x_i in self.X_]).transpose().dot(self.chis_) ret = t1 -2 * t2 + self.fixed_term_ return ret
[docs] def fit(self, X, y, warm_start=False): r"""Induce the membership function starting from a labeled sample. :param X: Vectors in data space. :type X: iterable of `float` vectors having the same length :param y: Membership for the vectors in `X`. :type y: iterable of `float` having the same length of `X` :param warm_start: flag triggering the non reinitialization of independent variables of the optimization problem, defaults to None. :type warm_start: `bool` :raises: ValueError if the values in `y` are not between 0 and 1, if `X` and have different lengths, or if `X` contains elements of different lengths. :returns: self -- the trained model. """ X = check_array(X) X, y = check_X_y(X, y) for e in y: if e < 0 or e > 1: raise ValueError("`y` values should belong to [0, 1]") self.random_state = check_random_state(self.random_state) self.X_ = X if warm_start: check_is_fitted(self, ["chis_"]) if self.chis_ is None: raise NotFittedError("chis variable are set to None") self.solver.initial_values = self.chis_ if type(self.k) is kernel.PrecomputedKernel: idx = X.flatten() self.gram_ = self.k.kernel_computations[idx][:, idx] else: self.gram_ = np.array([self.k.compute(x1, X) for x1 in X]) self.chis_ = self.solver.solve(X, y, self.c, self.gram_) chi_SV_index = [i for i, (chi, mu) in enumerate(zip(self.chis_, y)) if -self.c * (1 - mu) < chi < self.c * mu] self.fixed_term_ = np.array(self.chis_).dot(self.gram_.dot(self.chis_)) chi_sq_radius = list(self.x_to_sq_dist(X[chi_SV_index])) if len(chi_sq_radius) == 0: self.estimated_membership_ = None self.chis_ = None logger.warning('No support vectors found') return self self.fuzzifier.sq_radius_05 = np.mean(chi_sq_radius) self.fuzzifier.fit(self.x_to_sq_dist(X), y, np.mean(chi_sq_radius)) return self
[docs] def decision_function(self, X): r"""Compute predictions for the membership function. :param X: Vectors in data space. :type X: iterable of `float` vectors having the same length :returns: array of float -- the predictions for each value in `X`. """ check_is_fitted(self, ['chis_', 'estimated_membership_']) X = check_array(X) return self.fuzzifier.get_membership(self.x_to_sq_dist(X))
[docs] def predict(self, X, alpha=None): r"""Compute predictions for membership to the set. Predictions are either computed through the membership function (when `alpha` is set to a float in [0, 1]) or obtained via an $\alpha$-cut on the same function (when `alpha` is set to `None`). :param X: Vectors in data space. :type X: iterable of `float` vectors having the same length :param alpha: $\alpha$-cut value, defaults to `None`. :type alpha: float :raises: ValueError if `alpha` is set to a value different from `None` and not included in $[0, 1]$. :returns: array of int -- the predictions for each value in `X`. """ check_is_fitted(self, ['chis_', 'estimated_membership_']) X = check_array(X) mus = self.decision_function(X) if alpha is None: return mus else: if alpha < 0 or alpha > 1: raise ValueError("alpha cut value should belong to [0, 1]" f" (provided {alpha})") return np.array([1 if mu >= alpha else 0 for mu in mus])
[docs] def score(self, X, y, **kwargs): r"""Compute the fuzzifier score. Score is obtained as the opposite of MSE between predicted membership values and labels. :param X: Vectors in data space. :type X: iterable of `float` vectors having the same length :param y: Labels containing the *gold standard* membership values for the vectors in `X`. :type y: iterable of `float` having the same length of `X` :returns: `float` -- opposite of MSE between the predictions for the elements in `X` w.r.t. the labels in `y`. """ check_X_y(X, y) if self.estimated_membership_ is None: return -np.inf else: return -np.mean((self.decision_function(X) - y) ** 2)
def get_profile(self): check_is_fitted(self, ['chis_', 'estimated_membership_']) return self.fuzzifier.get_profile(self.x_to_sq_dist(self.X_))