Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion orca_python/classifiers/NNOP.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Neural Network with Ordered Partitions (NNOP)."""

import math as math
from numbers import Integral, Real

import numpy as np
import scipy
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context
from sklearn.utils._param_validation import Interval
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

Expand Down Expand Up @@ -89,12 +91,20 @@ class NNOP(BaseEstimator, ClassifierMixin):

"""

_parameter_constraints: dict = {
"epsilon_init": [Interval(Real, 0.0, None, closed="neither")],
"n_hidden": [Interval(Integral, 1, None, closed="left")],
"max_iter": [Interval(Integral, 1, None, closed="left")],
"lambda_value": [Interval(Real, 0.0, None, closed="neither")],
}

def __init__(self, epsilon_init=0.5, n_hidden=50, max_iter=500, lambda_value=0.01):
self.epsilon_init = epsilon_init
self.n_hidden = n_hidden
self.max_iter = max_iter
self.lambda_value = lambda_value

@_fit_context(prefer_skip_nested_validation=True)
def fit(self, X, y):
"""Fit the model with the training data.

Expand Down
12 changes: 11 additions & 1 deletion orca_python/classifiers/NNPOM.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Neural Network based on Proportional Odd Model (NNPOM)."""

import math as math
from numbers import Integral, Real

import numpy as np
import scipy
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context
from sklearn.utils._param_validation import Interval
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

Expand Down Expand Up @@ -90,12 +92,20 @@ class NNPOM(BaseEstimator, ClassifierMixin):

"""

_parameter_constraints: dict = {
"epsilon_init": [Interval(Real, 0.0, None, closed="neither")],
"n_hidden": [Interval(Integral, 1, None, closed="left")],
"max_iter": [Interval(Integral, 1, None, closed="left")],
"lambda_value": [Interval(Real, 0.0, None, closed="neither")],
}

def __init__(self, epsilon_init=0.5, n_hidden=50, max_iter=500, lambda_value=0.01):
self.epsilon_init = epsilon_init
self.n_hidden = n_hidden
self.max_iter = max_iter
self.lambda_value = lambda_value

@_fit_context(prefer_skip_nested_validation=True)
def fit(self, X, y):
"""Fit the model with the training data.

Expand Down
27 changes: 23 additions & 4 deletions orca_python/classifiers/OrdinalDecomposition.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""OrdinalDecomposition ensemble."""

import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context
from sklearn.utils._param_validation import StrOptions
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

from orca_python.utilities import load_classifier

# from sys import path
# path.append('..')


class OrdinalDecomposition(BaseEstimator, ClassifierMixin):
"""OrdinalDecomposition ensemble classifier.
Expand Down Expand Up @@ -88,6 +86,26 @@ class OrdinalDecomposition(BaseEstimator, ClassifierMixin):

"""

_parameter_constraints: dict = {
"dtype": [
StrOptions(
{
"ordered_partitions",
"one_vs_next",
"one_vs_followers",
"one_vs_previous",
}
)
],
"decision_method": [
StrOptions(
{"exponential_loss", "hinge_loss", "logarithmic_loss", "frank_hall"}
)
],
"base_classifier": [str],
"parameters": [dict],
}

def __init__(
self,
dtype="ordered_partitions",
Expand All @@ -100,6 +118,7 @@ def __init__(
self.base_classifier = base_classifier
self.parameters = parameters

@_fit_context(prefer_skip_nested_validation=True)
def fit(self, X, y):
"""Fit the model with the training data.

Expand Down
73 changes: 58 additions & 15 deletions orca_python/classifiers/REDSVM.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Reduction from ordinal regression to binary SVM (REDSVM)."""

from numbers import Integral, Real

import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context
from sklearn.utils._param_validation import Interval, StrOptions
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

from orca_python.classifiers.libsvmRank.python import svm

# from .libsvmRank.python import svm


class REDSVM(BaseEstimator, ClassifierMixin):
"""Reduction from ordinal regression to binary SVM classifiers.
Expand Down Expand Up @@ -37,13 +38,16 @@ class REDSVM(BaseEstimator, ClassifierMixin):
degree : int, default=3
Set degree in kernel function.

gamma : float, default=1/n_features
Set gamma in kernel function.
gamma : {'scale', 'auto'} or float, default=1.0
Kernel coefficient determining the influence of individual training samples:
- 'scale': 1 / (n_features * X.var())
- 'auto': 1 / n_features
- float: Must be non-negative.

coef0 : float, default=0
Set coef0 in kernel function.

shrinking : int, default=1
shrinking : bool, default=True
Set whether to use the shrinking heuristics.

tol : float, default=0.001
Expand Down Expand Up @@ -74,14 +78,42 @@ class REDSVM(BaseEstimator, ClassifierMixin):

"""

_parameter_constraints: dict = {
"C": [Interval(Real, 0.0, None, closed="neither")],
"kernel": [
StrOptions(
{
"linear",
"poly",
"rbf",
"sigmoid",
"stump",
"perceptron",
"laplacian",
"exponential",
"precomputed",
}
)
],
"degree": [Interval(Integral, 0, None, closed="left")],
"gamma": [
StrOptions({"scale", "auto"}),
Interval(Real, 0.0, None, closed="neither"),
],
"coef0": [Interval(Real, None, None, closed="neither")],
"shrinking": ["boolean"],
"tol": [Interval(Real, 0.0, None, closed="neither")],
"cache_size": [Interval(Real, 0.0, None, closed="neither")],
}

def __init__(
self,
C=1,
kernel="rbf",
degree=3,
gamma=None,
gamma="auto",
coef0=0,
shrinking=1,
shrinking=True,
tol=0.001,
cache_size=100,
):
Expand All @@ -94,6 +126,7 @@ def __init__(
self.tol = tol
self.cache_size = cache_size

@_fit_context(prefer_skip_nested_validation=True)
def fit(self, X, y):
"""Fit the model with the training data.

Expand All @@ -117,14 +150,24 @@ def fit(self, X, y):
If parameters are invalid or data has wrong format.

"""
# Additional strict validation for boolean parameters
if not isinstance(self.shrinking, bool):
raise ValueError(
f"The 'shrinking' parameter must be of type bool. "
f"Got {type(self.shrinking).__name__} instead."
)

# Check that X and y have correct shape
X, y = check_X_y(X, y)
# Store the classes seen during fit
self.classes_ = unique_labels(y)

# Set the default g value if necessary
if self.gamma is None:
self.gamma = 1 / np.size(X, 1)
# Set default gamma value if not specified
gamma_value = self.gamma
if self.gamma == "auto":
gamma_value = 1.0 / X.shape[1]
elif self.gamma == "scale":
gamma_value = 1.0 / (X.shape[1] * X.var())

# Map kernel type
kernel_type_mapping = {
Expand All @@ -138,18 +181,18 @@ def fit(self, X, y):
"exponential": 7,
"precomputed": 8,
}
kernel_type = kernel_type_mapping.get(self.kernel, -1)
kernel_type = kernel_type_mapping[self.kernel]

# Fit the model
options = "-s 5 -t {} -d {} -g {} -r {} -c {} -m {} -e {} -h {} -q".format(
str(kernel_type),
str(self.degree),
str(self.gamma),
str(gamma_value),
str(self.coef0),
str(self.C),
str(self.cache_size),
str(self.tol),
str(self.shrinking),
str(1 if self.shrinking else 0),
)
self.model_ = svm.fit(y.tolist(), X.tolist(), options)

Expand Down Expand Up @@ -184,6 +227,6 @@ def predict(self, X):
# Input validation
X = check_array(X)

y_pred = svm.predict(X.tolist(), self.model_)
y_pred = np.array(svm.predict(X.tolist(), self.model_))

return y_pred
26 changes: 23 additions & 3 deletions orca_python/classifiers/SVOREX.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Support Vector for Ordinal Regression (Explicit constraints) (SVOREX)."""

from sklearn.base import BaseEstimator, ClassifierMixin
from numbers import Integral, Real

import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context
from sklearn.utils._param_validation import Interval, StrOptions
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

# from .svorex import svorex
from orca_python.classifiers.svorex import svorex


Expand Down Expand Up @@ -56,13 +59,30 @@ class SVOREX(BaseEstimator, ClassifierMixin):

"""

_parameter_constraints: dict = {
"C": [Interval(Real, 0.0, None, closed="neither")],
"kernel": [
StrOptions(
{
"gaussian",
"linear",
"poly",
}
)
],
"degree": [Interval(Integral, 0, None, closed="left")],
"tol": [Interval(Real, 0.0, None, closed="neither")],
"kappa": [Interval(Real, 0.0, None, closed="neither")],
}

def __init__(self, C=1.0, kernel="gaussian", degree=2, tol=0.001, kappa=1):
self.C = C
self.kernel = kernel
self.degree = degree
self.tol = tol
self.kappa = kappa

@_fit_context(prefer_skip_nested_validation=True)
def fit(self, X, y):
"""Fit the model with the training data.

Expand Down Expand Up @@ -135,6 +155,6 @@ def predict(self, X):
# Input validation
X = check_array(X)

y_pred = svorex.predict(X.tolist(), self.model_)
y_pred = np.array(svorex.predict(X.tolist(), self.model_))

return y_pred
28 changes: 24 additions & 4 deletions orca_python/classifiers/tests/test_nnop.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,36 @@ def y():
@pytest.mark.parametrize(
"param_name, invalid_value",
[
("epsilon_init", 0),
("epsilon_init", -1),
("n_hidden", -1),
("max_iter", -1),
("lambda_value", -1e-5),
],
)
def test_nnop_fit_hyperparameters_validation(X, y, param_name, invalid_value):
"""Test that hyperparameters are validated."""
def test_nnop_hyperparameter_value_validation(X, y, param_name, invalid_value):
"""Test that NNOP raises ValueError for invalid of hyperparameters."""
classifier = NNOP(**{param_name: invalid_value})
model = classifier.fit(X, y)

assert model is None, "The NNOP fit method doesnt return Null on error"
with pytest.raises(ValueError, match=rf"The '{param_name}' parameter.*"):
classifier.fit(X, y)


@pytest.mark.parametrize(
"param_name, invalid_value",
[
("epsilon_init", "high"),
("n_hidden", 5.5),
("max_iter", 2.5),
("lambda_value", "tight"),
],
)
def test_nnop_hyperparameter_type_validation(X, y, param_name, invalid_value):
"""Test that NNOP raises ValueError for invalid types of hyperparameters."""
classifier = NNOP(**{param_name: invalid_value})

with pytest.raises(ValueError, match=rf"The '{param_name}' parameter.*"):
classifier.fit(X, y)


def test_nnop_fit_input_validation(X, y):
Expand Down
Loading