From 85748d81994297f818c4a13d172c96c5ef9eeffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 09:05:50 +0200 Subject: [PATCH 1/9] ENH: Remove get/set methods in NNOP and NNPOM --- orca_python/classifiers/NNOP.py | 181 +------------------------- orca_python/classifiers/NNPOM.py | 211 +------------------------------ 2 files changed, 4 insertions(+), 388 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index 34f56a7..5833b37 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -232,182 +232,6 @@ def predict(self, X): return y_pred - def get_epsilon_init(self): - """Return the value of the variable self.epsilon_init. - - Returns - ------- - epsilon_init : float - The initialization range of the weights. - - """ - return self.epsilon_init - - def set_epsilon_init(self, epsilon_init): - """Modify the value of the variable self.epsilon_init. - - Parameters - ---------- - epsilon_init : float - The initialization range of the weights. - - """ - self.epsilon_init = epsilon_init - - def get_n_hidden(self): - """Return the value of the variable self.n_hidden. - - Returns - ------- - n_hidden : int - Number of nodes/neurons in the hidden layer. - - """ - return self.n_hidden - - def set_n_hidden(self, n_hidden): - """Modify the value of the variable self.n_hidden. - - Parameters - ---------- - n_hidden : int - Number of nodes/neurons in the hidden layer. - - """ - self.n_hidden = n_hidden - - def get_max_iter(self): - """Return the value of the variable self.max_iter. - - Returns - ------- - max_iter : int - Number of iterations. - - """ - return self.max_iter - - def set_max_iter(self, max_iter): - """Modify the value of the variable self.max_iter. - - Parameters - ---------- - max_iter : int - Number of iterations. - - """ - self.max_iter = max_iter - - def get_lambda_value(self): - """Return the value of the variable self.lambda_value. - - Returns - ------- - lambda_value : float - Lambda parameter used in regularization. - - """ - return self.lambda_value - - def set_lambda_value(self, lambda_value): - """Modify the value of the variable self.lambda_value. - - Parameters - ---------- - lambda_value : float - Lambda parameter used in regularization. - - """ - self.lambda_value = lambda_value - - def get_theta1(self): - """Return the value of the variable self.theta1_. - - Returns - ------- - theta1_ : ndarray of shape (n_hidden, n_features + 1) - Array with the weights of the hidden layer (with biases included). - - """ - return self.theta1_ - - def set_theta1(self, theta1): - """Modify the value of the variable self.theta1_. - - Parameters - ---------- - theta1 : ndarray of shape (n_hidden, n_features + 1) - Array with the weights of the hidden layer (with biases included). - - """ - self.theta1_ = theta1 - - def get_theta2(self): - """Return the value of the variable self.theta2_. - - Returns - ------- - theta2_ : ndarray of shape (n_classes - 1, n_hidden + 1) - Array with the weights of the output layer. - - """ - return self.theta2_ - - def set_theta2(self, theta2): - """Modify the value of the variable self.theta2_. - - Parameters - ---------- - theta2 : ndarray of shape (n_classes - 1, n_hidden + 1) - Array with the weights of the output layer. - - """ - self.theta2_ = theta2 - - def get_n_classes(self): - """Return the value of the variable self.n_classes_. - - Returns - ------- - n_classes_ : int - Number of labels in the problem. - - """ - return self.n_classes_ - - def set_n_classes(self, n_classes): - """Modify the value of the variable self.n_classes_. - - Parameters - ---------- - n_classes : int - Number of labels in the problem. - - """ - self.n_classes_ = n_classes - - def get_n_samples(self): - """Return the value of the variable self.n_samples_. - - Returns - ------- - n_samples_ : int - Number of samples of X (train patterns array). - - """ - return self.n_samples_ - - def set_n_samples(self, n_samples): - """Modify the value of the variable self.n_samples_. - - Parameters - ---------- - n_samples : int - Number of samples of X (train patterns array). - - """ - self.n_samples_ = n_samples - def _unpack_parameters(self, nn_params, n_features, n_hidden, n_classes): """Get theta1 and theta2 back from nn_params. @@ -468,10 +292,7 @@ def _rand_initialize_weights(self, L_in, L_out): Array with the weights of each synaptic relationship between nodes. """ - W = ( - np.random.rand(L_out, L_in) * 2 * self.get_epsilon_init() - - self.get_epsilon_init() - ) + W = np.random.rand(L_out, L_in) * 2 * self.epsilon_init - self.epsilon_init return W diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 74c69de..912b87a 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -155,11 +155,9 @@ def fit(self, X, y): ) # Hidden layer weigths (with bias) - initial_theta1 = self._rand_initialize_weights( - n_features + 1, self.get_n_hidden() - ) + initial_theta1 = self._rand_initialize_weights(n_features + 1, self.n_hidden) # Output layer weigths (without bias, the biases will be the thresholds) - initial_theta2 = self._rand_initialize_weights(self.get_n_hidden(), 1) + initial_theta2 = self._rand_initialize_weights(self.n_hidden, 1) # Class thresholds parameters initial_thresholds = self._rand_initialize_weights((n_classes - 1), 1) @@ -244,206 +242,6 @@ def predict(self, X): return y_pred - def get_epsilon_init(self): - """Return the value of the variable self.epsilon_init. - - Returns - ------- - epsilon_init : float - The initialization range of the weights. - - """ - return self.epsilon_init - - def set_epsilon_init(self, epsilon_init): - """Modify the value of the variable self.epsilon_init. - - Parameters - ---------- - epsilon_init : float - The initialization range of the weights. - - """ - self.epsilon_init = epsilon_init - - def get_n_hidden(self): - """Return the value of the variable self.n_hidden. - - Returns - ------- - n_hidden : int - Number of nodes/neurons in the hidden layer. - - """ - return self.n_hidden - - def set_n_hidden(self, n_hidden): - """Modify the value of the variable self.n_hidden. - - Parameters - ---------- - n_hidden : int - Number of nodes/neurons in the hidden layer. - - """ - self.n_hidden = n_hidden - - def get_max_iter(self): - """Return the value of the variable self.max_iter. - - Returns - ------- - max_iter : int - Number of iterations. - - """ - return self.max_iter - - def set_max_iter(self, max_iter): - """Modify the value of the variable self.max_iter. - - Parameters - ---------- - max_iter : int - Number of iterations. - - """ - self.max_iter = max_iter - - def get_lambda_value(self): - """Return the value of the variable self.lambda_value. - - Returns - ------- - lambda_value : float - The regularization parameter. - - """ - return self.lambda_value - - def set_lambda_value(self, lambda_value): - """Modify the value of the variable self.lambda_value. - - Parameters - ---------- - lambda_value : float - The regularization parameter. - - """ - self.lambda_value = lambda_value - - def get_theta1(self): - """Return the value of the variable self.theta1_. - - Returns - ------- - theta1_ : ndarray of shape (n_hidden, n_features + 1) - The weights of the hidden layer (with biases included). - - """ - return self.theta1_ - - def set_theta1(self, theta1): - """Modify the value of the variable self.theta1_. - - Parameters - ---------- - theta1 : ndarray of shape (n_hidden, n_features + 1) - The weights of the hidden layer (with biases included). - - """ - self.theta1_ = theta1 - - def get_theta2(self): - """Return the value of the variable self.theta2_. - - Returns - ------- - theta2_ : ndarray of shape (1, n_hidden) - The weights of the output layer (without bias, the biases will be the - thresholds). - - """ - return self.theta2_ - - def set_theta2(self, theta2): - """Modify the value of the variable self.theta2_. - - Parameters - ---------- - theta2 : ndarray of shape (1, n_hidden) - The weights of the output layer (without bias, the biases will be the - thresholds). - - """ - self.theta2_ = theta2 - - def get_thresholds(self): - """Return the value of the variable self.thresholds_. - - Returns - ------- - thresholds_ : ndarray of shape (n_classes - 1, 1) - The class thresholds parameters. - - """ - return self.thresholds_ - - def set_thresholds(self, thresholds): - """Modify the value of the variable self.thresholds_. - - Parameters - ---------- - thresholds : ndarray of shape (n_classes - 1, 1) - The class thresholds parameters. - - """ - self.thresholds_ = thresholds - - def get_n_classes(self): - """Return the value of the variable self.n_classes_. - - Returns - ------- - n_classes_ : int - Number of labels in the problem. - - """ - return self.n_classes_ - - def set_n_classes(self, n_classes): - """Modify the value of the variable self.n_classes_. - - Parameters - ---------- - n_classes : int - Number of labels in the problem. - - """ - self.n_classes_ = n_classes - - def get_n_samples(self): - """Return the value of the variable self.n_samples_. - - Returns - ------- - n_samples_ : int - Number of samples of X (train patterns array). - - """ - return self.n_samples_ - - def set_n_samples(self, n_samples): - """Modify the value of the variable self.n_samples_. - - Parameters - ---------- - n_samples : int - Number of samples of X (train patterns array). - - """ - self.n_samples_ = n_samples - def _unpack_parameters(self, nn_params, n_features, n_hidden, n_classes): """Get theta1, theta2 and thresholds_param from nn_params. @@ -513,10 +311,7 @@ def _rand_initialize_weights(self, L_in, L_out): Array with the weights of each synaptic relationship between nodes. """ - W = ( - np.random.rand(L_out, L_in) * 2 * self.get_epsilon_init() - - self.get_epsilon_init() - ) + W = np.random.rand(L_out, L_in) * 2 * self.epsilon_init - self.epsilon_init return W From a0165d6c8789efdbb864d9cff063d01cf5c78dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 10:12:24 +0200 Subject: [PATCH 2/9] REF: Remove redundant attributes in NNOP and NNPOM --- orca_python/classifiers/NNOP.py | 15 ++++----------- orca_python/classifiers/NNPOM.py | 13 +++---------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index 5833b37..b7979ad 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -47,12 +47,6 @@ class NNOP(BaseEstimator, ClassifierMixin): classes_ : ndarray of shape (n_classes,) Array that contains all different class labels found in the original dataset. - n_classes_ : int - Number of labels in the problem. - - n_samples_ : int - Number of samples of X (train patterns array). - theta1_ : ndarray of shape (n_hidden, n_features + 1) Hidden layer weights (with bias). @@ -144,7 +138,7 @@ def fit(self, X, y): # Aux variables y = y[:, np.newaxis] n_features = X.shape[1] - n_classes = np.size(np.unique(y)) + n_classes = len(self.classes_) n_samples = X.shape[0] # Recode y to Y using ordinalPartitions coding @@ -181,8 +175,6 @@ def fit(self, X, y): ) self.theta1_ = theta1 self.theta2_ = theta2 - self.n_classes_ = n_classes - self.n_samples_ = n_samples return self @@ -215,6 +207,7 @@ def predict(self, X): # Input validation X = check_array(X) n_samples = X.shape[0] + n_classes = len(self.classes_) a1 = np.append(np.ones((n_samples, 1)), X, axis=1) z2 = np.append(np.ones((n_samples, 1)), np.matmul(a1, self.theta1_.T), axis=1) @@ -225,9 +218,9 @@ def predict(self, X): a3 = np.multiply( np.where(np.append(projected, np.ones((n_samples, 1)), axis=1) > 0.5, 1, 0), - np.tile(np.arange(1, self.n_classes_ + 1), (n_samples, 1)), + np.tile(np.arange(1, n_classes + 1), (n_samples, 1)), ) - a3[np.where(a3 == 0)] = self.n_classes_ + 1 + a3[np.where(a3 == 0)] = n_classes + 1 y_pred = a3.min(axis=1) return y_pred diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 912b87a..9e0527f 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -46,12 +46,6 @@ class NNPOM(BaseEstimator, ClassifierMixin): classes_ : ndarray of shape (n_classes,) Array that contains all different class labels found in the original dataset. - n_classes_ : int - Number of labels in the problem - - n_samples_ : int - Number of samples of X (train patterns array). - theta1_ : ndarray of shape (n_hidden, n_features + 1) Hidden layer weigths (with bias) @@ -145,7 +139,7 @@ def fit(self, X, y): # Aux variables y = y[:, np.newaxis] n_features = X.shape[1] - n_classes = np.size(np.unique(y)) + n_classes = len(self.classes_) n_samples = X.shape[0] # Recode y to Y using nominal coding @@ -191,8 +185,6 @@ def fit(self, X, y): self.theta1_ = theta1 self.theta2_ = theta2 self.thresholds_ = self._convert_thresholds(thresholds_param, n_classes) - self.n_classes_ = n_classes - self.n_samples_ = n_samples return self @@ -226,6 +218,7 @@ def predict(self, X): X = check_array(X) n_samples = X.shape[0] + n_classes = len(self.classes_) a1 = np.append(np.ones((n_samples, 1)), X, axis=1) z2 = np.matmul(a1, self.theta1_.T) @@ -233,7 +226,7 @@ def predict(self, X): projected = np.matmul(a2, self.theta2_.T) z3 = np.tile(self.thresholds_, (n_samples, 1)) - np.tile( - projected, (1, self.n_classes_ - 1) + projected, (1, n_classes - 1) ) a3T = 1.0 / (1.0 + np.exp(-z3)) a3 = np.append(a3T, np.ones((n_samples, 1)), axis=1) From 8b01d1b3becbb12fd847ed70f4431103ce73ff58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 10:43:38 +0200 Subject: [PATCH 3/9] ENH: Add sklearn standard attributes to NNOP and NNPOM --- orca_python/classifiers/NNOP.py | 44 ++++++++++++++++++++++++++++---- orca_python/classifiers/NNPOM.py | 43 +++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index b7979ad..3a84a58 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -47,6 +47,24 @@ class NNOP(BaseEstimator, ClassifierMixin): classes_ : ndarray of shape (n_classes,) Array that contains all different class labels found in the original dataset. + loss_ : float + The current loss computed with the loss function. + + n_features_in_ : int + Number of features seen during fit. + + n_iter_ : int + The number of iterations the solver has run. + + n_layers_ : int + Number of layers. + + n_outputs_ : int + Number of outputs. + + out_activation_ : str + Name of the output activation function. + theta1_ : ndarray of shape (n_hidden, n_features + 1) Hidden layer weights (with bias). @@ -137,9 +155,9 @@ def fit(self, X, y): # Aux variables y = y[:, np.newaxis] - n_features = X.shape[1] n_classes = len(self.classes_) n_samples = X.shape[0] + self.n_features_in_ = X.shape[1] # Recode y to Y using ordinalPartitions coding Y = 1 * ( @@ -148,7 +166,9 @@ def fit(self, X, y): ) # Hidden layer weights (with bias) - initial_theta1 = self._rand_initialize_weights(n_features + 1, self.n_hidden) + initial_theta1 = self._rand_initialize_weights( + self.n_features_in_ + 1, self.n_hidden + ) # Output layer weights initial_theta2 = self._rand_initialize_weights(self.n_hidden + 1, n_classes - 1) @@ -161,21 +181,35 @@ def fit(self, X, y): results_optimization = scipy.optimize.fmin_l_bfgs_b( func=self._nnop_cost_function, x0=initial_nn_params.ravel(), - args=(n_features, self.n_hidden, n_classes, X, Y, self.lambda_value), + args=( + self.n_features_in_, + self.n_hidden, + n_classes, + X, + Y, + self.lambda_value, + ), fprime=None, factr=1e3, maxiter=self.max_iter, - iprint=-1, ) self.nn_params = results_optimization[0] + self.loss_ = float(results_optimization[1]) + self.n_iter_ = int(results_optimization[2].get("nit", 0)) + # Unpack the parameters theta1, theta2 = self._unpack_parameters( - self.nn_params, n_features, self.n_hidden, n_classes + self.nn_params, self.n_features_in_, self.n_hidden, n_classes ) self.theta1_ = theta1 self.theta2_ = theta2 + # Scikit-learn compatibility + self.n_layers_ = 3 + self.n_outputs_ = n_classes - 1 + self.out_activation_ = "logistic" + return self def predict(self, X): diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 9e0527f..0208b4e 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -46,6 +46,24 @@ class NNPOM(BaseEstimator, ClassifierMixin): classes_ : ndarray of shape (n_classes,) Array that contains all different class labels found in the original dataset. + loss_ : float + The current loss computed with the loss function. + + n_features_in_ : int + Number of features seen during fit. + + n_iter_ : int + The number of iterations the solver has run. + + n_layers_ : int + Number of layers. + + n_outputs_ : int + Number of outputs. + + out_activation_ : str + Name of the output activation function. + theta1_ : ndarray of shape (n_hidden, n_features + 1) Hidden layer weigths (with bias) @@ -138,9 +156,9 @@ def fit(self, X, y): # Aux variables y = y[:, np.newaxis] - n_features = X.shape[1] n_classes = len(self.classes_) n_samples = X.shape[0] + self.n_features_in_ = X.shape[1] # Recode y to Y using nominal coding Y = 1 * ( @@ -149,7 +167,9 @@ def fit(self, X, y): ) # Hidden layer weigths (with bias) - initial_theta1 = self._rand_initialize_weights(n_features + 1, self.n_hidden) + initial_theta1 = self._rand_initialize_weights( + self.n_features_in_ + 1, self.n_hidden + ) # Output layer weigths (without bias, the biases will be the thresholds) initial_theta2 = self._rand_initialize_weights(self.n_hidden, 1) # Class thresholds parameters @@ -168,24 +188,37 @@ def fit(self, X, y): results_optimization = scipy.optimize.fmin_l_bfgs_b( func=self._nnpom_cost_function, x0=initial_nn_params.ravel(), - args=(n_features, self.n_hidden, n_classes, X, Y, self.lambda_value), + args=( + self.n_features_in_, + self.n_hidden, + n_classes, + X, + Y, + self.lambda_value, + ), fprime=None, factr=1e3, maxiter=self.max_iter, - iprint=-1, ) self.nn_params = results_optimization[0] + self.loss_ = float(results_optimization[1]) + self.n_iter_ = int(results_optimization[2].get("nit", 0)) # Unpack the parameters theta1, theta2, thresholds_param = self._unpack_parameters( - self.nn_params, n_features, self.n_hidden, n_classes + self.nn_params, self.n_features_in_, self.n_hidden, n_classes ) self.theta1_ = theta1 self.theta2_ = theta2 self.thresholds_ = self._convert_thresholds(thresholds_param, n_classes) + # Scikit-learn compatibility + self.n_layers_ = 3 + self.n_outputs_ = n_classes - 1 + self.out_activation_ = "logistic" + return self def predict(self, X): From a08a3ab9a4a7bc5b63f5a428594860a7c839c106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 10:58:46 +0200 Subject: [PATCH 4/9] BUG: Raise ValueError on invalid hyperparameters in fit --- orca_python/classifiers/NNOP.py | 8 -------- orca_python/classifiers/NNPOM.py | 8 -------- 2 files changed, 16 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index 3a84a58..5484a7d 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -140,14 +140,6 @@ def fit(self, X, y): If parameters are invalid or data has wrong format. """ - if ( - self.epsilon_init < 0 - or self.n_hidden < 1 - or self.max_iter < 1 - or self.lambda_value < 0 - ): - return None - # Check that X and y have correct shape X, y = check_X_y(X, y) # Store the classes seen during fit diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 0208b4e..9e382a7 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -141,14 +141,6 @@ def fit(self, X, y): If parameters are invalid or data has wrong format. """ - if ( - self.epsilon_init < 0 - or self.n_hidden < 1 - or self.max_iter < 1 - or self.lambda_value < 0 - ): - return None - # Check that X and y have correct shape X, y = check_X_y(X, y) # Store the classes seen during fit From c01473d532c980d749c17f757bb0e350f85d221b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 11:09:25 +0200 Subject: [PATCH 5/9] REF: Make nn_params a local variable in fit (drop attribute) --- orca_python/classifiers/NNOP.py | 4 ++-- orca_python/classifiers/NNPOM.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index 5484a7d..ae4eeae 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -186,13 +186,13 @@ def fit(self, X, y): maxiter=self.max_iter, ) - self.nn_params = results_optimization[0] + nn_params = results_optimization[0] self.loss_ = float(results_optimization[1]) self.n_iter_ = int(results_optimization[2].get("nit", 0)) # Unpack the parameters theta1, theta2 = self._unpack_parameters( - self.nn_params, self.n_features_in_, self.n_hidden, n_classes + nn_params, self.n_features_in_, self.n_hidden, n_classes ) self.theta1_ = theta1 self.theta2_ = theta2 diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 9e382a7..1101861 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -193,13 +193,13 @@ def fit(self, X, y): maxiter=self.max_iter, ) - self.nn_params = results_optimization[0] + nn_params = results_optimization[0] self.loss_ = float(results_optimization[1]) self.n_iter_ = int(results_optimization[2].get("nit", 0)) # Unpack the parameters theta1, theta2, thresholds_param = self._unpack_parameters( - self.nn_params, self.n_features_in_, self.n_hidden, n_classes + nn_params, self.n_features_in_, self.n_hidden, n_classes ) self.theta1_ = theta1 From dda42da488ae93172d54fc1a3eba59cbef0642c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 11:20:34 +0200 Subject: [PATCH 6/9] BUG: Fix mixin inheritance order (ClassifierMixin before BaseEstimator) --- orca_python/classifiers/NNOP.py | 2 +- orca_python/classifiers/NNPOM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index ae4eeae..6329156 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -11,7 +11,7 @@ from sklearn.utils.validation import check_array, check_is_fitted, check_X_y -class NNOP(BaseEstimator, ClassifierMixin): +class NNOP(ClassifierMixin, BaseEstimator): """Neural Network with Ordered Partitions (NNOP). This model considers the OrderedPartitions coding scheme for the labels and a rule diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index 1101861..a7015d7 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -11,7 +11,7 @@ from sklearn.utils.validation import check_array, check_is_fitted, check_X_y -class NNPOM(BaseEstimator, ClassifierMixin): +class NNPOM(ClassifierMixin, BaseEstimator): """Neural Network based on Proportional Odd Model (NNPOM). This class implements a neural network model for ordinal regression. The model has From 436a416ac13f847869451c3903d99639e2e2fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 11:29:55 +0200 Subject: [PATCH 7/9] DOC: Update docstrings for NNOP and NNPOM to match sklearn --- orca_python/classifiers/NNOP.py | 27 +++++++++++++-------------- orca_python/classifiers/NNPOM.py | 21 ++++++++++----------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index 6329156..fe897e5 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -16,9 +16,9 @@ class NNOP(ClassifierMixin, BaseEstimator): This model considers the OrderedPartitions coding scheme for the labels and a rule for decisions based on the first node whose output is higher than a predefined - threshold (T=0.5, in our experiments). The model has one hidden layer with hiddenN - neurons and one output layer with as many neurons as the number of classes minus - one. + threshold (T=0.5, in our experiments). The model has one hidden layer with + "n_hidden" neurons and one output layer with as many neurons as the number of + classes minus one. The learning is based on iRProp+ algorithm and the implementation provided by Roberto Calandra in his toolbox Rprop Toolbox for MATLAB: @@ -37,7 +37,8 @@ class NNOP(ClassifierMixin, BaseEstimator): Number of hidden neurons of the model. max_iter : int, default=500 - Number of iterations for fmin_l_bfgs_b algorithm. + Maximum number of iterations. The solver iterates until convergence or this + number of iterations. lambda_value : float, default=0.01 Regularization parameter. @@ -45,7 +46,7 @@ class NNOP(ClassifierMixin, BaseEstimator): Attributes ---------- classes_ : ndarray of shape (n_classes,) - Array that contains all different class labels found in the original dataset. + Class labels for each output. loss_ : float The current loss computed with the loss function. @@ -118,16 +119,15 @@ def __init__(self, epsilon_init=0.5, n_hidden=50, max_iter=500, lambda_value=0.0 @_fit_context(prefer_skip_nested_validation=True) def fit(self, X, y): - """Fit the model with the training data. + """Fit the model to data matrix X and target(s) y. Parameters ---------- - X : {array-like, sparse matrix} of shape (n_samples, n_features) - Training patterns array, where n_samples is the number of samples - and n_features is the number of features. + X : ndarray or sparse matrix of shape (n_samples, n_features) + The input data. - y : array-like of shape (n_samples,) - Target vector relative to X. + y : ndarray of shape (n_samples,) + The target values. Returns ------- @@ -210,13 +210,12 @@ def predict(self, X): Parameters ---------- X : {array-like, sparse matrix} of shape (n_samples, n_features) - Test patterns array, where n_samples is the number of samples and n_features - is the number of features. + The input data. Returns ------- y_pred : ndarray of shape (n_samples,) - Class labels for samples in X. + The predicted classes. Raises ------ diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index a7015d7..d31c873 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -15,7 +15,7 @@ class NNPOM(ClassifierMixin, BaseEstimator): """Neural Network based on Proportional Odd Model (NNPOM). This class implements a neural network model for ordinal regression. The model has - one hidden layer with n_hidden neurons and one output layer with only one neuron + one hidden layer with "n_hidden" neurons and one output layer with only one neuron but as many thresholds as the number of classes minus one. The standard POM model is applied in this neuron to have probabilistic outputs. @@ -36,7 +36,8 @@ class NNPOM(ClassifierMixin, BaseEstimator): Number of hidden neurons of the model. max_iter : int, default=500 - Number of iterations for fmin_l_bfgs_b algorithm. + Maximum number of iterations. The solver iterates until convergence or this + number of iterations. lambda_value : float, default=0.01 Regularization parameter. @@ -44,7 +45,7 @@ class NNPOM(ClassifierMixin, BaseEstimator): Attributes ---------- classes_ : ndarray of shape (n_classes,) - Array that contains all different class labels found in the original dataset. + Class labels for each output. loss_ : float The current loss computed with the loss function. @@ -119,16 +120,15 @@ def __init__(self, epsilon_init=0.5, n_hidden=50, max_iter=500, lambda_value=0.0 @_fit_context(prefer_skip_nested_validation=True) def fit(self, X, y): - """Fit the model with the training data. + """Fit the model to data matrix X and target(s) y. Parameters ---------- - X : {array-like, sparse matrix} of shape (n_samples, n_features) - Training patterns array, where n_samples is the number of samples and - n_features is the number of features. + X : ndarray or sparse matrix of shape (n_samples, n_features) + The input data. y : array-like of shape (n_samples,) - Target vector relative to X. + The target values. Returns ------- @@ -219,13 +219,12 @@ def predict(self, X): Parameters ---------- X : {array-like, sparse matrix} of shape (n_samples, n_features) - Test patterns array, where n_samples is the number of samples and - n_features is the number of features. + The input data. Returns ------- y_pred : ndarray of shape (n_samples,) - Class labels for samples in X. + The predicted classes. Raises ------ From 4b1901dc7243d137f2c2218868577a56c62c0122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 11:49:04 +0200 Subject: [PATCH 8/9] ENH: Enforce fitted-attr check in predict --- orca_python/classifiers/NNOP.py | 2 +- orca_python/classifiers/NNPOM.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/orca_python/classifiers/NNOP.py b/orca_python/classifiers/NNOP.py index fe897e5..57852b6 100644 --- a/orca_python/classifiers/NNOP.py +++ b/orca_python/classifiers/NNOP.py @@ -227,7 +227,7 @@ def predict(self, X): """ # Check is fit had been called - check_is_fitted(self) + check_is_fitted(self, attributes=["theta1_", "theta2_", "classes_"]) # Input validation X = check_array(X) diff --git a/orca_python/classifiers/NNPOM.py b/orca_python/classifiers/NNPOM.py index d31c873..c38d3ba 100644 --- a/orca_python/classifiers/NNPOM.py +++ b/orca_python/classifiers/NNPOM.py @@ -236,7 +236,7 @@ def predict(self, X): """ # Check is fit had been called - check_is_fitted(self) + check_is_fitted(self, attributes=["theta1_", "theta2_", "classes_"]) # Input validation X = check_array(X) From 2cede4e16ee58d00b169ad2031395f27c7fd2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Mon, 1 Sep 2025 12:07:48 +0200 Subject: [PATCH 9/9] TST: Add NNOP and NNPOM unit tests --- orca_python/classifiers/tests/test_nnop.py | 56 ++++++++++++++++++--- orca_python/classifiers/tests/test_nnpom.py | 56 ++++++++++++++++++--- 2 files changed, 96 insertions(+), 16 deletions(-) diff --git a/orca_python/classifiers/tests/test_nnop.py b/orca_python/classifiers/tests/test_nnop.py index 46da287..8e3e563 100644 --- a/orca_python/classifiers/tests/test_nnop.py +++ b/orca_python/classifiers/tests/test_nnop.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from sklearn.exceptions import NotFittedError from orca_python.classifiers.NNOP import NNOP @@ -53,6 +54,13 @@ def test_nnop_hyperparameter_type_validation(X, y, param_name, invalid_value): classifier.fit(X, y) +def test_nnop_fit_returns_self(X, y): + """fit should return self for sklearn compatibility.""" + classifier = NNOP() + model = classifier.fit(X, y) + assert model is classifier + + def test_nnop_fit_input_validation(X, y): """Test that input data is validated.""" X_invalid = X[:-1, :-1] @@ -60,20 +68,45 @@ def test_nnop_fit_input_validation(X, y): classifier = NNOP() with pytest.raises(ValueError): - model = classifier.fit(X, y_invalid) - assert model is None, "The NNOP fit method doesnt return Null on error" + classifier.fit(X, y_invalid) with pytest.raises(ValueError): - model = classifier.fit([], y) - assert model is None, "The NNOP fit method doesnt return Null on error" + classifier.fit([], y) with pytest.raises(ValueError): - model = classifier.fit(X, []) - assert model is None, "The NNOP fit method doesnt return Null on error" + classifier.fit(X, []) with pytest.raises(ValueError): - model = classifier.fit(X_invalid, y) - assert model is None, "The NNOP fit method doesnt return Null on error" + classifier.fit(X_invalid, y) + + +def test_nnop_sets_fitted_attributes_after_fit(X, y): + """Test than NNOP exposes fitted attributes aligned con sklearn-style.""" + clf = NNOP(n_hidden=4, max_iter=5) + clf.fit(X, y) + + for attr in [ + "classes_", + "n_features_in_", + "theta1_", + "theta2_", + "loss_", + "n_iter_", + "n_layers_", + "n_outputs_", + "out_activation_", + ]: + assert hasattr(clf, attr), f"Missing fitted attribute: {attr}" + + assert isinstance(clf.classes_, np.ndarray) and np.array_equal( + clf.classes_, np.unique(y) + ) + assert isinstance(clf.n_features_in_, int) and clf.n_features_in_ == X.shape[1] + assert isinstance(clf.loss_, (float, np.floating)) and clf.loss_ >= 0 + assert isinstance(clf.n_iter_, int) and clf.n_iter_ == 5 + assert isinstance(clf.n_layers_, int) and clf.n_layers_ == 3 + assert isinstance(clf.n_outputs_, int) and clf.n_outputs_ == len(np.unique(y)) - 1 + assert isinstance(clf.out_activation_, str) and clf.out_activation_ == "logistic" def test_nnop_predict_invalid_input_raises_error(X, y): @@ -83,3 +116,10 @@ def test_nnop_predict_invalid_input_raises_error(X, y): with pytest.raises(ValueError): classifier.predict([]) + + +def test_nnop_predict_raises_if_not_fitted(X): + """Test that predict raises NotFittedError if called before fit.""" + classifier = NNOP() + with pytest.raises(NotFittedError): + classifier.predict(X) diff --git a/orca_python/classifiers/tests/test_nnpom.py b/orca_python/classifiers/tests/test_nnpom.py index c992db4..5efbd16 100644 --- a/orca_python/classifiers/tests/test_nnpom.py +++ b/orca_python/classifiers/tests/test_nnpom.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from sklearn.exceptions import NotFittedError from orca_python.classifiers.NNPOM import NNPOM @@ -53,6 +54,13 @@ def test_nnpom_hyperparameter_type_validation(X, y, param_name, invalid_value): classifier.fit(X, y) +def test_nnpom_fit_returns_self(X, y): + """fit should return self for sklearn compatibility.""" + classifier = NNPOM() + model = classifier.fit(X, y) + assert model is classifier + + def test_nnpom_fit_input_validation(X, y): """Test that input data is validated.""" X_invalid = X[:-1, :-1] @@ -60,20 +68,45 @@ def test_nnpom_fit_input_validation(X, y): classifier = NNPOM() with pytest.raises(ValueError): - model = classifier.fit(X, y_invalid) - assert model is None, "The NNPOM fit method doesnt return Null on error" + classifier.fit(X, y_invalid) with pytest.raises(ValueError): - model = classifier.fit([], y) - assert model is None, "The NNPOM fit method doesnt return Null on error" + classifier.fit([], y) with pytest.raises(ValueError): - model = classifier.fit(X, []) - assert model is None, "The NNPOM fit method doesnt return Null on error" + classifier.fit(X, []) with pytest.raises(ValueError): - model = classifier.fit(X_invalid, y) - assert model is None, "The NNPOM fit method doesnt return Null on error" + classifier.fit(X_invalid, y) + + +def test_nnpom_sets_fitted_attributes_after_fit(X, y): + """Test than NNPOM exposes fitted attributes aligned con sklearn-style.""" + clf = NNPOM(n_hidden=4, max_iter=5) + clf.fit(X, y) + + for attr in [ + "classes_", + "n_features_in_", + "theta1_", + "theta2_", + "loss_", + "n_iter_", + "n_layers_", + "n_outputs_", + "out_activation_", + ]: + assert hasattr(clf, attr), f"Missing fitted attribute: {attr}" + + assert isinstance(clf.classes_, np.ndarray) and np.array_equal( + clf.classes_, np.unique(y) + ) + assert isinstance(clf.n_features_in_, int) and clf.n_features_in_ == X.shape[1] + assert isinstance(clf.loss_, (float, np.floating)) and clf.loss_ >= 0 + assert isinstance(clf.n_iter_, int) and 1 <= clf.n_iter_ <= 5 + assert isinstance(clf.n_layers_, int) and clf.n_layers_ == 3 + assert isinstance(clf.n_outputs_, int) and clf.n_outputs_ == len(np.unique(y)) - 1 + assert isinstance(clf.out_activation_, str) and clf.out_activation_ == "logistic" def test_nnpom_predict_invalid_input_raises_error(X, y): @@ -83,3 +116,10 @@ def test_nnpom_predict_invalid_input_raises_error(X, y): with pytest.raises(ValueError): classifier.predict([]) + + +def test_nnpom_predict_raises_if_not_fitted(X): + """Test that predict raises NotFittedError if called before fit.""" + classifier = NNPOM() + with pytest.raises(NotFittedError): + classifier.predict(X)