Skip to content

Commit dd665bf

Browse files
ENH: Refactor NNOP and NNPOM for scikit-learn compatibility (#34)
* ENH: Remove get/set methods in NNOP and NNPOM * REF: Remove redundant attributes in NNOP and NNPOM * ENH: Add sklearn standard attributes to NNOP and NNPOM * BUG: Raise ValueError on invalid hyperparameters in fit * REF: Make nn_params a local variable in fit (drop attribute) * BUG: Fix mixin inheritance order (ClassifierMixin before BaseEstimator) * DOC: Update docstrings for NNOP and NNPOM to match sklearn * ENH: Enforce fitted-attr check in predict * TST: Add NNOP and NNPOM unit tests
1 parent cdf41b9 commit dd665bf

File tree

4 files changed

+206
-475
lines changed

4 files changed

+206
-475
lines changed

orca_python/classifiers/NNOP.py

Lines changed: 58 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y
1212

1313

14-
class NNOP(BaseEstimator, ClassifierMixin):
14+
class NNOP(ClassifierMixin, BaseEstimator):
1515
"""Neural Network with Ordered Partitions (NNOP).
1616
1717
This model considers the OrderedPartitions coding scheme for the labels and a rule
1818
for decisions based on the first node whose output is higher than a predefined
19-
threshold (T=0.5, in our experiments). The model has one hidden layer with hiddenN
20-
neurons and one output layer with as many neurons as the number of classes minus
21-
one.
19+
threshold (T=0.5, in our experiments). The model has one hidden layer with
20+
"n_hidden" neurons and one output layer with as many neurons as the number of
21+
classes minus one.
2222
2323
The learning is based on iRProp+ algorithm and the implementation provided by
2424
Roberto Calandra in his toolbox Rprop Toolbox for MATLAB:
@@ -37,21 +37,34 @@ class NNOP(BaseEstimator, ClassifierMixin):
3737
Number of hidden neurons of the model.
3838
3939
max_iter : int, default=500
40-
Number of iterations for fmin_l_bfgs_b algorithm.
40+
Maximum number of iterations. The solver iterates until convergence or this
41+
number of iterations.
4142
4243
lambda_value : float, default=0.01
4344
Regularization parameter.
4445
4546
Attributes
4647
----------
4748
classes_ : ndarray of shape (n_classes,)
48-
Array that contains all different class labels found in the original dataset.
49+
Class labels for each output.
4950
50-
n_classes_ : int
51-
Number of labels in the problem.
51+
loss_ : float
52+
The current loss computed with the loss function.
5253
53-
n_samples_ : int
54-
Number of samples of X (train patterns array).
54+
n_features_in_ : int
55+
Number of features seen during fit.
56+
57+
n_iter_ : int
58+
The number of iterations the solver has run.
59+
60+
n_layers_ : int
61+
Number of layers.
62+
63+
n_outputs_ : int
64+
Number of outputs.
65+
66+
out_activation_ : str
67+
Name of the output activation function.
5568
5669
theta1_ : ndarray of shape (n_hidden, n_features + 1)
5770
Hidden layer weights (with bias).
@@ -106,16 +119,15 @@ def __init__(self, epsilon_init=0.5, n_hidden=50, max_iter=500, lambda_value=0.0
106119

107120
@_fit_context(prefer_skip_nested_validation=True)
108121
def fit(self, X, y):
109-
"""Fit the model with the training data.
122+
"""Fit the model to data matrix X and target(s) y.
110123
111124
Parameters
112125
----------
113-
X : {array-like, sparse matrix} of shape (n_samples, n_features)
114-
Training patterns array, where n_samples is the number of samples
115-
and n_features is the number of features.
126+
X : ndarray or sparse matrix of shape (n_samples, n_features)
127+
The input data.
116128
117-
y : array-like of shape (n_samples,)
118-
Target vector relative to X.
129+
y : ndarray of shape (n_samples,)
130+
The target values.
119131
120132
Returns
121133
-------
@@ -128,24 +140,16 @@ def fit(self, X, y):
128140
If parameters are invalid or data has wrong format.
129141
130142
"""
131-
if (
132-
self.epsilon_init < 0
133-
or self.n_hidden < 1
134-
or self.max_iter < 1
135-
or self.lambda_value < 0
136-
):
137-
return None
138-
139143
# Check that X and y have correct shape
140144
X, y = check_X_y(X, y)
141145
# Store the classes seen during fit
142146
self.classes_ = unique_labels(y)
143147

144148
# Aux variables
145149
y = y[:, np.newaxis]
146-
n_features = X.shape[1]
147-
n_classes = np.size(np.unique(y))
150+
n_classes = len(self.classes_)
148151
n_samples = X.shape[0]
152+
self.n_features_in_ = X.shape[1]
149153

150154
# Recode y to Y using ordinalPartitions coding
151155
Y = 1 * (
@@ -154,7 +158,9 @@ def fit(self, X, y):
154158
)
155159

156160
# Hidden layer weights (with bias)
157-
initial_theta1 = self._rand_initialize_weights(n_features + 1, self.n_hidden)
161+
initial_theta1 = self._rand_initialize_weights(
162+
self.n_features_in_ + 1, self.n_hidden
163+
)
158164
# Output layer weights
159165
initial_theta2 = self._rand_initialize_weights(self.n_hidden + 1, n_classes - 1)
160166

@@ -167,22 +173,34 @@ def fit(self, X, y):
167173
results_optimization = scipy.optimize.fmin_l_bfgs_b(
168174
func=self._nnop_cost_function,
169175
x0=initial_nn_params.ravel(),
170-
args=(n_features, self.n_hidden, n_classes, X, Y, self.lambda_value),
176+
args=(
177+
self.n_features_in_,
178+
self.n_hidden,
179+
n_classes,
180+
X,
181+
Y,
182+
self.lambda_value,
183+
),
171184
fprime=None,
172185
factr=1e3,
173186
maxiter=self.max_iter,
174-
iprint=-1,
175187
)
176188

177-
self.nn_params = results_optimization[0]
189+
nn_params = results_optimization[0]
190+
self.loss_ = float(results_optimization[1])
191+
self.n_iter_ = int(results_optimization[2].get("nit", 0))
192+
178193
# Unpack the parameters
179194
theta1, theta2 = self._unpack_parameters(
180-
self.nn_params, n_features, self.n_hidden, n_classes
195+
nn_params, self.n_features_in_, self.n_hidden, n_classes
181196
)
182197
self.theta1_ = theta1
183198
self.theta2_ = theta2
184-
self.n_classes_ = n_classes
185-
self.n_samples_ = n_samples
199+
200+
# Scikit-learn compatibility
201+
self.n_layers_ = 3
202+
self.n_outputs_ = n_classes - 1
203+
self.out_activation_ = "logistic"
186204

187205
return self
188206

@@ -192,13 +210,12 @@ def predict(self, X):
192210
Parameters
193211
----------
194212
X : {array-like, sparse matrix} of shape (n_samples, n_features)
195-
Test patterns array, where n_samples is the number of samples and n_features
196-
is the number of features.
213+
The input data.
197214
198215
Returns
199216
-------
200217
y_pred : ndarray of shape (n_samples,)
201-
Class labels for samples in X.
218+
The predicted classes.
202219
203220
Raises
204221
------
@@ -210,11 +227,12 @@ def predict(self, X):
210227
211228
"""
212229
# Check is fit had been called
213-
check_is_fitted(self)
230+
check_is_fitted(self, attributes=["theta1_", "theta2_", "classes_"])
214231

215232
# Input validation
216233
X = check_array(X)
217234
n_samples = X.shape[0]
235+
n_classes = len(self.classes_)
218236

219237
a1 = np.append(np.ones((n_samples, 1)), X, axis=1)
220238
z2 = np.append(np.ones((n_samples, 1)), np.matmul(a1, self.theta1_.T), axis=1)
@@ -225,189 +243,13 @@ def predict(self, X):
225243

226244
a3 = np.multiply(
227245
np.where(np.append(projected, np.ones((n_samples, 1)), axis=1) > 0.5, 1, 0),
228-
np.tile(np.arange(1, self.n_classes_ + 1), (n_samples, 1)),
246+
np.tile(np.arange(1, n_classes + 1), (n_samples, 1)),
229247
)
230-
a3[np.where(a3 == 0)] = self.n_classes_ + 1
248+
a3[np.where(a3 == 0)] = n_classes + 1
231249
y_pred = a3.min(axis=1)
232250

233251
return y_pred
234252

235-
def get_epsilon_init(self):
236-
"""Return the value of the variable self.epsilon_init.
237-
238-
Returns
239-
-------
240-
epsilon_init : float
241-
The initialization range of the weights.
242-
243-
"""
244-
return self.epsilon_init
245-
246-
def set_epsilon_init(self, epsilon_init):
247-
"""Modify the value of the variable self.epsilon_init.
248-
249-
Parameters
250-
----------
251-
epsilon_init : float
252-
The initialization range of the weights.
253-
254-
"""
255-
self.epsilon_init = epsilon_init
256-
257-
def get_n_hidden(self):
258-
"""Return the value of the variable self.n_hidden.
259-
260-
Returns
261-
-------
262-
n_hidden : int
263-
Number of nodes/neurons in the hidden layer.
264-
265-
"""
266-
return self.n_hidden
267-
268-
def set_n_hidden(self, n_hidden):
269-
"""Modify the value of the variable self.n_hidden.
270-
271-
Parameters
272-
----------
273-
n_hidden : int
274-
Number of nodes/neurons in the hidden layer.
275-
276-
"""
277-
self.n_hidden = n_hidden
278-
279-
def get_max_iter(self):
280-
"""Return the value of the variable self.max_iter.
281-
282-
Returns
283-
-------
284-
max_iter : int
285-
Number of iterations.
286-
287-
"""
288-
return self.max_iter
289-
290-
def set_max_iter(self, max_iter):
291-
"""Modify the value of the variable self.max_iter.
292-
293-
Parameters
294-
----------
295-
max_iter : int
296-
Number of iterations.
297-
298-
"""
299-
self.max_iter = max_iter
300-
301-
def get_lambda_value(self):
302-
"""Return the value of the variable self.lambda_value.
303-
304-
Returns
305-
-------
306-
lambda_value : float
307-
Lambda parameter used in regularization.
308-
309-
"""
310-
return self.lambda_value
311-
312-
def set_lambda_value(self, lambda_value):
313-
"""Modify the value of the variable self.lambda_value.
314-
315-
Parameters
316-
----------
317-
lambda_value : float
318-
Lambda parameter used in regularization.
319-
320-
"""
321-
self.lambda_value = lambda_value
322-
323-
def get_theta1(self):
324-
"""Return the value of the variable self.theta1_.
325-
326-
Returns
327-
-------
328-
theta1_ : ndarray of shape (n_hidden, n_features + 1)
329-
Array with the weights of the hidden layer (with biases included).
330-
331-
"""
332-
return self.theta1_
333-
334-
def set_theta1(self, theta1):
335-
"""Modify the value of the variable self.theta1_.
336-
337-
Parameters
338-
----------
339-
theta1 : ndarray of shape (n_hidden, n_features + 1)
340-
Array with the weights of the hidden layer (with biases included).
341-
342-
"""
343-
self.theta1_ = theta1
344-
345-
def get_theta2(self):
346-
"""Return the value of the variable self.theta2_.
347-
348-
Returns
349-
-------
350-
theta2_ : ndarray of shape (n_classes - 1, n_hidden + 1)
351-
Array with the weights of the output layer.
352-
353-
"""
354-
return self.theta2_
355-
356-
def set_theta2(self, theta2):
357-
"""Modify the value of the variable self.theta2_.
358-
359-
Parameters
360-
----------
361-
theta2 : ndarray of shape (n_classes - 1, n_hidden + 1)
362-
Array with the weights of the output layer.
363-
364-
"""
365-
self.theta2_ = theta2
366-
367-
def get_n_classes(self):
368-
"""Return the value of the variable self.n_classes_.
369-
370-
Returns
371-
-------
372-
n_classes_ : int
373-
Number of labels in the problem.
374-
375-
"""
376-
return self.n_classes_
377-
378-
def set_n_classes(self, n_classes):
379-
"""Modify the value of the variable self.n_classes_.
380-
381-
Parameters
382-
----------
383-
n_classes : int
384-
Number of labels in the problem.
385-
386-
"""
387-
self.n_classes_ = n_classes
388-
389-
def get_n_samples(self):
390-
"""Return the value of the variable self.n_samples_.
391-
392-
Returns
393-
-------
394-
n_samples_ : int
395-
Number of samples of X (train patterns array).
396-
397-
"""
398-
return self.n_samples_
399-
400-
def set_n_samples(self, n_samples):
401-
"""Modify the value of the variable self.n_samples_.
402-
403-
Parameters
404-
----------
405-
n_samples : int
406-
Number of samples of X (train patterns array).
407-
408-
"""
409-
self.n_samples_ = n_samples
410-
411253
def _unpack_parameters(self, nn_params, n_features, n_hidden, n_classes):
412254
"""Get theta1 and theta2 back from nn_params.
413255
@@ -468,10 +310,7 @@ def _rand_initialize_weights(self, L_in, L_out):
468310
Array with the weights of each synaptic relationship between nodes.
469311
470312
"""
471-
W = (
472-
np.random.rand(L_out, L_in) * 2 * self.get_epsilon_init()
473-
- self.get_epsilon_init()
474-
)
313+
W = np.random.rand(L_out, L_in) * 2 * self.epsilon_init - self.epsilon_init
475314

476315
return W
477316

0 commit comments

Comments
 (0)