sklearn-morpho 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sklearn_morpho/__init__.py +5 -0
- sklearn_morpho/classifiers/__init__.py +3 -0
- sklearn_morpho/classifiers/ldep.py +203 -0
- sklearn_morpho/classifiers/rdep.py +197 -0
- sklearn_morpho/classifiers/simple_perceptron.py +187 -0
- sklearn_morpho/stopping/__init__.py +7 -0
- sklearn_morpho/stopping/stopping_base.py +27 -0
- sklearn_morpho/stopping/stopping_cost.py +24 -0
- sklearn_morpho/stopping/stopping_early.py +40 -0
- sklearn_morpho/stopping/stopping_epoch.py +24 -0
- sklearn_morpho/stopping/stopping_train_stop.py +17 -0
- sklearn_morpho/training/__init__.py +6 -0
- sklearn_morpho/training/dccp_ldep.py +270 -0
- sklearn_morpho/training/dccp_rdep.py +285 -0
- sklearn_morpho/training/dccp_simple_perceptron.py +180 -0
- sklearn_morpho/training/dccp_wrapper.py +343 -0
- sklearn_morpho/weighting/__init__.py +3 -0
- sklearn_morpho/weighting/weighting_base.py +53 -0
- sklearn_morpho/weighting/weighting_dist.py +34 -0
- sklearn_morpho/weighting/weighting_none.py +17 -0
- sklearn_morpho-0.2.3.dist-info/METADATA +30 -0
- sklearn_morpho-0.2.3.dist-info/RECORD +24 -0
- sklearn_morpho-0.2.3.dist-info/WHEEL +4 -0
- sklearn_morpho-0.2.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Literal, cast
|
|
5
|
+
|
|
6
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
7
|
+
from sklearn.preprocessing import StandardScaler
|
|
8
|
+
from sklearn.utils import check_random_state
|
|
9
|
+
from sklearn.utils.validation import validate_data, check_is_fitted
|
|
10
|
+
from sklearn.utils.multiclass import unique_labels
|
|
11
|
+
|
|
12
|
+
from ..training.dccp_ldep import LDEPDccpTrainer
|
|
13
|
+
from ..stopping import (
|
|
14
|
+
StoppingMethod,
|
|
15
|
+
CostStoppingMethod,
|
|
16
|
+
EarlyStoppingMethod,
|
|
17
|
+
EpochStoppingMethod,
|
|
18
|
+
TrainStopStoppingMethod,
|
|
19
|
+
)
|
|
20
|
+
from ..weighting import SampleWeighting, NoneSampleWeighting
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LDEP(ClassifierMixin, BaseEstimator):
|
|
24
|
+
"""
|
|
25
|
+
Scikit-learn estimator wrapper around a l-DEP (linear Dilation-Erosion
|
|
26
|
+
morphological Perceptron) for binary data classification.
|
|
27
|
+
|
|
28
|
+
The l-DEP's activation function is defined as:
|
|
29
|
+
|
|
30
|
+
\\[ y = f(\\lambda \\tau_(R_1(x)) + (1 - \\lambda) \\tau'_(R_2(x))) \\]
|
|
31
|
+
|
|
32
|
+
Where $\\tau$ refers to the activation of a (max, +) morphological
|
|
33
|
+
perceptron and $\\tau'$ to a (min, +) one.
|
|
34
|
+
$R_1, R_2$ are linear transformations to apply to the training data.
|
|
35
|
+
They convert it into a latent space with possibly different dimensions,
|
|
36
|
+
also allowing classification of arbitrarily distributed data.
|
|
37
|
+
A higher dimension for the latent space will result in slower training
|
|
38
|
+
times, but will allow the decision boundary to be more complex.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
latent_dims: tuple[int, int] = (10, 10),
|
|
44
|
+
margin=1.0,
|
|
45
|
+
penalty=0.0,
|
|
46
|
+
validation_ratio=0.3,
|
|
47
|
+
weighting_method: SampleWeighting | None = None,
|
|
48
|
+
stopping_methods: list[StoppingMethod] | None = None,
|
|
49
|
+
use_dccp_library: bool = False,
|
|
50
|
+
verbose: Literal[0, 1, 2] = 0,
|
|
51
|
+
random_state: np.random.RandomState | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Initialize the classifier, see class help for more.
|
|
55
|
+
|
|
56
|
+
param latent_dims: The dimensions of the latent spaces used for the
|
|
57
|
+
linear transformations output
|
|
58
|
+
param margin: Enforce a margin between the decision boundary
|
|
59
|
+
and the data. May help with linearly separable
|
|
60
|
+
datasets, but generally lower is more accurate.
|
|
61
|
+
param penalty: A penalty to add to the weights squared and
|
|
62
|
+
avoid them exploding.
|
|
63
|
+
Must be a small positive number like 1e-6, or
|
|
64
|
+
zero to disable penalty calculation altogether.
|
|
65
|
+
param validation_radio: How much of the training set to dedicate to use
|
|
66
|
+
as validation during fitting.
|
|
67
|
+
Must be between 0 and 1 (inclusive, exclusive),
|
|
68
|
+
if set to exactly 0 then incompatible stopping
|
|
69
|
+
methods cannot be used (e.g. early stopping).
|
|
70
|
+
Ignored when using the dccp library solver.
|
|
71
|
+
param weighting_method: The weighting method to use: apply weights to
|
|
72
|
+
the cost contribution of each data point to help
|
|
73
|
+
avoid outliers.
|
|
74
|
+
If left to None, will use NoneWeightingMethod().
|
|
75
|
+
param stopping_methods: A list of stopping methods, must not be empty.
|
|
76
|
+
At each epoch, these methods will be
|
|
77
|
+
sequentially asked whether the training should
|
|
78
|
+
stop. In this case, epoch ends by rolling back
|
|
79
|
+
to the epoch with the best validation cost.
|
|
80
|
+
If left to None, will use:
|
|
81
|
+
[
|
|
82
|
+
CostStoppingMethod(),
|
|
83
|
+
EarlyStoppingMethod(),
|
|
84
|
+
EpochStoppingMethod(),
|
|
85
|
+
TrainStopStoppingMethod(),
|
|
86
|
+
]
|
|
87
|
+
Ignored when using the dccp library solver.
|
|
88
|
+
param use_dccp_library: Whether to use the dccp library as a solver or
|
|
89
|
+
a manual constraints linearization in fit.
|
|
90
|
+
param verbose: Whether to log extra information. 0: no logging,
|
|
91
|
+
1: basic logging / timing, 2: cvxpy solve() set
|
|
92
|
+
to verbose mode.
|
|
93
|
+
param random_state: A RandomState object or None to allow for seeded
|
|
94
|
+
randomness.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
self.latent_dims = latent_dims
|
|
98
|
+
self.margin = margin
|
|
99
|
+
self.penalty = penalty
|
|
100
|
+
self.validation_ratio = validation_ratio
|
|
101
|
+
self.weighting_method = weighting_method
|
|
102
|
+
self.stopping_methods = stopping_methods
|
|
103
|
+
self.use_dccp_library = use_dccp_library
|
|
104
|
+
self.verbose: Literal[0, 1, 2] = verbose
|
|
105
|
+
self.random_state = random_state
|
|
106
|
+
|
|
107
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> LDEP:
|
|
108
|
+
"""
|
|
109
|
+
Fit the classifier, create attributes:
|
|
110
|
+
- self.max_perceptron_
|
|
111
|
+
- self.min_perceptron_
|
|
112
|
+
- self.lambda_
|
|
113
|
+
- self.max_matrix_
|
|
114
|
+
- self.min_matrix_
|
|
115
|
+
- self.classes_: Unique labels generated from y
|
|
116
|
+
|
|
117
|
+
X and y must represent binary classifiable data.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# input data validation
|
|
121
|
+
random_state = check_random_state(self.random_state)
|
|
122
|
+
X, y = validate_data(self, X, y) # type: ignore
|
|
123
|
+
self.scaler_ = StandardScaler()
|
|
124
|
+
X_scaled = self.scaler_.fit_transform(X)
|
|
125
|
+
|
|
126
|
+
# set unset parameters to their default values
|
|
127
|
+
if self.weighting_method is None:
|
|
128
|
+
weighting_method = NoneSampleWeighting()
|
|
129
|
+
else:
|
|
130
|
+
weighting_method = self.weighting_method
|
|
131
|
+
if self.stopping_methods is None:
|
|
132
|
+
stopping_methods = [
|
|
133
|
+
CostStoppingMethod(),
|
|
134
|
+
EarlyStoppingMethod(),
|
|
135
|
+
EpochStoppingMethod(),
|
|
136
|
+
TrainStopStoppingMethod(),
|
|
137
|
+
]
|
|
138
|
+
else:
|
|
139
|
+
stopping_methods = self.stopping_methods
|
|
140
|
+
|
|
141
|
+
# create classes and convert them to distinct integers for fitting
|
|
142
|
+
# the classes are persisted inside the object for use in predict
|
|
143
|
+
self.classes_ = unique_labels(y)
|
|
144
|
+
classes_list = list(self.classes_)
|
|
145
|
+
y_integers = np.array(
|
|
146
|
+
[classes_list.index(c) for c in y], dtype=np.int32
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if len(classes_list) != 2:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
'Only binary classification is supported but '
|
|
152
|
+
f'got {len(classes_list)} class(es).'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# create and train perceptrons
|
|
156
|
+
trainer = LDEPDccpTrainer(
|
|
157
|
+
self.latent_dims,
|
|
158
|
+
self.margin,
|
|
159
|
+
self.penalty,
|
|
160
|
+
self.validation_ratio,
|
|
161
|
+
weighting_method,
|
|
162
|
+
stopping_methods,
|
|
163
|
+
self.use_dccp_library,
|
|
164
|
+
self.verbose,
|
|
165
|
+
random_state,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
trainer.train(X_scaled, y_integers)
|
|
169
|
+
self.max_perceptron_ = trainer.max_perceptron
|
|
170
|
+
self.min_perceptron_ = trainer.min_perceptron
|
|
171
|
+
self.lambda_ = trainer.lambda_
|
|
172
|
+
self.max_matrix_ = trainer.max_matrix
|
|
173
|
+
self.min_matrix_ = trainer.min_matrix
|
|
174
|
+
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def decision_function(self, X: np.ndarray) -> np.ndarray:
|
|
178
|
+
check_is_fitted(self)
|
|
179
|
+
X = validate_data(self, X, reset=False) # type: ignore
|
|
180
|
+
X_scaled = cast(np.ndarray, self.scaler_.transform(X))
|
|
181
|
+
|
|
182
|
+
expr_max = np.max(
|
|
183
|
+
self.max_perceptron_ + X_scaled @ self.max_matrix_.T, axis=1
|
|
184
|
+
)
|
|
185
|
+
expr_min = np.min(
|
|
186
|
+
self.min_perceptron_ + X_scaled @ self.min_matrix_.T, axis=1
|
|
187
|
+
)
|
|
188
|
+
return expr_max * self.lambda_ + expr_min * (1 - self.lambda_)
|
|
189
|
+
|
|
190
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
191
|
+
check_is_fitted(self)
|
|
192
|
+
return self.classes_[(self.decision_function(X) >= 0).astype(int)]
|
|
193
|
+
|
|
194
|
+
def __sklearn_tags__(self):
|
|
195
|
+
"""
|
|
196
|
+
Overriden method to allow check_estimator to not run accuracy tests.
|
|
197
|
+
These are designed for perceptrons with a linear decision boundary,
|
|
198
|
+
which is not the case for a morphological perceptron.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
tags = super().__sklearn_tags__()
|
|
202
|
+
tags.classifier_tags.multi_class = False
|
|
203
|
+
return tags
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Literal, cast
|
|
5
|
+
|
|
6
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
7
|
+
from sklearn.preprocessing import StandardScaler
|
|
8
|
+
from sklearn.utils import check_random_state
|
|
9
|
+
from sklearn.utils.validation import validate_data, check_is_fitted
|
|
10
|
+
from sklearn.utils.multiclass import unique_labels
|
|
11
|
+
|
|
12
|
+
from ..training.dccp_rdep import RDEPDccpTrainer
|
|
13
|
+
from ..stopping import (
|
|
14
|
+
StoppingMethod,
|
|
15
|
+
CostStoppingMethod,
|
|
16
|
+
EarlyStoppingMethod,
|
|
17
|
+
EpochStoppingMethod,
|
|
18
|
+
TrainStopStoppingMethod,
|
|
19
|
+
)
|
|
20
|
+
from ..weighting import SampleWeighting, NoneSampleWeighting
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RDEP(ClassifierMixin, BaseEstimator):
|
|
24
|
+
"""
|
|
25
|
+
Scikit-learn estimator wrapper around a r-DEP (reduced Dilation-Erosion
|
|
26
|
+
morphological Perceptron) for binary data classification.
|
|
27
|
+
|
|
28
|
+
The r-DEP's activation function is defined as:
|
|
29
|
+
|
|
30
|
+
\\[ y = f(\\lambda \\tau_(x) + (1 - \\lambda) \\tau'_(x)) \\]
|
|
31
|
+
|
|
32
|
+
Where $\\tau$ refers to the activation of a (max, +) morphological
|
|
33
|
+
perceptron and $\\tau'$ to a (min, +) one.
|
|
34
|
+
$\\lambda$ is a real number between 0 and 1 to guarantee correct convexity,
|
|
35
|
+
but in practice a smaller interval can be enforced to avoid imprecisions.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
lambda_bounds=(1e-3, 1 - 1e-3),
|
|
41
|
+
margin=0.0,
|
|
42
|
+
penalty=0.0,
|
|
43
|
+
validation_ratio=0.3,
|
|
44
|
+
weighting_method: SampleWeighting | None = None,
|
|
45
|
+
stopping_methods: list[StoppingMethod] | None = None,
|
|
46
|
+
use_dccp_library: bool = False,
|
|
47
|
+
verbose: Literal[0, 1, 2] = 0,
|
|
48
|
+
random_state: np.random.RandomState | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Initialize the classifier, see class help for more.
|
|
52
|
+
|
|
53
|
+
param lambda_bounds: A pair of min and max values for lambda, to
|
|
54
|
+
avoid solvers (especially dccp) from failing to
|
|
55
|
+
optimize.
|
|
56
|
+
To keep the constraints at the right convexity,
|
|
57
|
+
the bounds must be inside [0, 1].
|
|
58
|
+
param margin: Enforce a margin between the decision boundary
|
|
59
|
+
and the data. May help with linearly separable
|
|
60
|
+
datasets, but generally lower is more accurate.
|
|
61
|
+
param penalty: A penalty to add to the weights squared and
|
|
62
|
+
avoid them exploding.
|
|
63
|
+
Must be a small positive number like 1e-6, or
|
|
64
|
+
zero to disable penalty calculation altogether.
|
|
65
|
+
param validation_radio: How much of the training set to dedicate to use
|
|
66
|
+
as validation during fitting.
|
|
67
|
+
Must be between 0 and 1 (inclusive, exclusive),
|
|
68
|
+
if set to exactly 0 then incompatible stopping
|
|
69
|
+
methods cannot be used (e.g. early stopping).
|
|
70
|
+
Ignored when using the dccp library solver.
|
|
71
|
+
param weighting_method: The weighting method to use: apply weights to
|
|
72
|
+
the cost contribution of each data point to help
|
|
73
|
+
avoid outliers.
|
|
74
|
+
If left to None, will use NoneWeightingMethod().
|
|
75
|
+
param stopping_methods: A list of stopping methods, must not be empty.
|
|
76
|
+
At each epoch, these methods will be
|
|
77
|
+
sequentially asked whether the training should
|
|
78
|
+
stop. In this case, epoch ends by rolling back
|
|
79
|
+
to the epoch with the best validation cost.
|
|
80
|
+
If left to None, will use:
|
|
81
|
+
[
|
|
82
|
+
CostStoppingMethod(),
|
|
83
|
+
EarlyStoppingMethod(),
|
|
84
|
+
EpochStoppingMethod(),
|
|
85
|
+
TrainStopStoppingMethod(),
|
|
86
|
+
]
|
|
87
|
+
Ignored when using the dccp library solver.
|
|
88
|
+
param use_dccp_library: Whether to use the dccp library as a solver or
|
|
89
|
+
a manual constraints linearization in fit.
|
|
90
|
+
param verbose: Whether to log extra information. 0: no logging,
|
|
91
|
+
1: basic logging / timing, 2: cvxpy solve() set
|
|
92
|
+
to verbose mode.
|
|
93
|
+
param random_state: A RandomState object or None to allow for seeded
|
|
94
|
+
randomness.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
self.lambda_bounds = lambda_bounds
|
|
98
|
+
self.margin = margin
|
|
99
|
+
self.penalty = penalty
|
|
100
|
+
self.validation_ratio = validation_ratio
|
|
101
|
+
self.weighting_method = weighting_method
|
|
102
|
+
self.stopping_methods = stopping_methods
|
|
103
|
+
self.use_dccp_library = use_dccp_library
|
|
104
|
+
self.verbose: Literal[0, 1, 2] = verbose
|
|
105
|
+
self.random_state = random_state
|
|
106
|
+
|
|
107
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> RDEP:
|
|
108
|
+
"""
|
|
109
|
+
Fit the classifier, create attributes:
|
|
110
|
+
- self.max_perceptron_
|
|
111
|
+
- self.min_perceptron_
|
|
112
|
+
- self.classes_: Unique labels generated from y
|
|
113
|
+
|
|
114
|
+
X and y must represent binary classifiable data.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# input data validation
|
|
118
|
+
random_state = check_random_state(self.random_state)
|
|
119
|
+
X, y = validate_data(self, X, y) # type: ignore
|
|
120
|
+
self.scaler_ = StandardScaler()
|
|
121
|
+
X_scaled = self.scaler_.fit_transform(X)
|
|
122
|
+
|
|
123
|
+
# set unset parameters to their default values
|
|
124
|
+
if self.weighting_method is None:
|
|
125
|
+
weighting_method = NoneSampleWeighting()
|
|
126
|
+
else:
|
|
127
|
+
weighting_method = self.weighting_method
|
|
128
|
+
if self.stopping_methods is None:
|
|
129
|
+
stopping_methods = [
|
|
130
|
+
CostStoppingMethod(),
|
|
131
|
+
EarlyStoppingMethod(),
|
|
132
|
+
EpochStoppingMethod(),
|
|
133
|
+
TrainStopStoppingMethod(),
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
stopping_methods = self.stopping_methods
|
|
137
|
+
|
|
138
|
+
# create classes and convert them to distinct integers for fitting
|
|
139
|
+
# the classes are persisted inside the object for use in predict
|
|
140
|
+
self.classes_ = unique_labels(y)
|
|
141
|
+
classes_list = list(self.classes_)
|
|
142
|
+
y_integers = np.array(
|
|
143
|
+
[classes_list.index(c) for c in y], dtype=np.int32
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if len(classes_list) != 2:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
'Only binary classification is supported but '
|
|
149
|
+
f'got {len(classes_list)} class(es).'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# create and train perceptrons
|
|
153
|
+
trainer = RDEPDccpTrainer(
|
|
154
|
+
self.lambda_bounds,
|
|
155
|
+
self.margin,
|
|
156
|
+
self.penalty,
|
|
157
|
+
self.validation_ratio,
|
|
158
|
+
weighting_method,
|
|
159
|
+
stopping_methods,
|
|
160
|
+
self.use_dccp_library,
|
|
161
|
+
self.verbose,
|
|
162
|
+
random_state,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
trainer.train(X_scaled, y_integers)
|
|
166
|
+
self.max_perceptron_ = trainer.max_perceptron
|
|
167
|
+
self.min_perceptron_ = trainer.min_perceptron
|
|
168
|
+
self.lambda_ = trainer.lambda_
|
|
169
|
+
self.invert_res_ = trainer.invert_res
|
|
170
|
+
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def decision_function(self, X: np.ndarray) -> np.ndarray:
|
|
174
|
+
check_is_fitted(self)
|
|
175
|
+
X = validate_data(self, X, reset=False) # type: ignore
|
|
176
|
+
X_scaled = cast(np.ndarray, self.scaler_.transform(X))
|
|
177
|
+
|
|
178
|
+
expr_max = np.max(self.max_perceptron_ + X_scaled, axis=1)
|
|
179
|
+
expr_min = np.min(self.min_perceptron_ + X_scaled, axis=1)
|
|
180
|
+
activation = expr_max * self.lambda_ + expr_min * (1 - self.lambda_)
|
|
181
|
+
|
|
182
|
+
return activation * (1 - 2 * self.invert_res_)
|
|
183
|
+
|
|
184
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
185
|
+
check_is_fitted(self)
|
|
186
|
+
return self.classes_[(self.decision_function(X) >= 0).astype(int)]
|
|
187
|
+
|
|
188
|
+
def __sklearn_tags__(self):
|
|
189
|
+
"""
|
|
190
|
+
Overriden method to allow check_estimator to not run accuracy tests.
|
|
191
|
+
These are designed for perceptrons with a linear decision boundary,
|
|
192
|
+
which is not the case for a morphological perceptron.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
tags = super().__sklearn_tags__()
|
|
196
|
+
tags.classifier_tags.multi_class = False
|
|
197
|
+
return tags
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Literal, cast
|
|
5
|
+
|
|
6
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
7
|
+
from sklearn.preprocessing import StandardScaler
|
|
8
|
+
from sklearn.utils import check_random_state
|
|
9
|
+
from sklearn.utils.validation import validate_data, check_is_fitted
|
|
10
|
+
from sklearn.utils.multiclass import unique_labels
|
|
11
|
+
|
|
12
|
+
from ..training.dccp_simple_perceptron import SimplePerceptronDccpTrainer
|
|
13
|
+
from ..stopping import (
|
|
14
|
+
StoppingMethod,
|
|
15
|
+
CostStoppingMethod,
|
|
16
|
+
EarlyStoppingMethod,
|
|
17
|
+
EpochStoppingMethod,
|
|
18
|
+
TrainStopStoppingMethod,
|
|
19
|
+
)
|
|
20
|
+
from ..weighting import SampleWeighting, NoneSampleWeighting
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MorphoPerceptron(ClassifierMixin, BaseEstimator):
|
|
24
|
+
"""
|
|
25
|
+
Scikit-learn estimator wrapper around a simple morphological perceptron.
|
|
26
|
+
|
|
27
|
+
The morphological perceptron's activation function is defined as:
|
|
28
|
+
|
|
29
|
+
\\[ y = \\tau(w + x) \\]
|
|
30
|
+
|
|
31
|
+
Where $\\tau$ refers to max (resp. min), for a dilation (resp. erosion)
|
|
32
|
+
perceptron and $w$ the perceptron's weights.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
kind: Literal['max', 'min'],
|
|
38
|
+
margin=0.0,
|
|
39
|
+
penalty=0.0,
|
|
40
|
+
validation_ratio=0.3,
|
|
41
|
+
weighting_method: SampleWeighting | None = None,
|
|
42
|
+
stopping_methods: list[StoppingMethod] | None = None,
|
|
43
|
+
use_dccp_library: bool = False,
|
|
44
|
+
verbose: Literal[0, 1, 2] = 0,
|
|
45
|
+
random_state: np.random.RandomState | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the classifier, see class help for more.
|
|
49
|
+
|
|
50
|
+
param kind: Whether the perceptron is dilation or erosion.
|
|
51
|
+
param margin: Enforce a margin between the decision boundary
|
|
52
|
+
and the data. May help with linearly separable
|
|
53
|
+
datasets, but generally lower is more accurate.
|
|
54
|
+
param penalty: A penalty to add to the weights squared and
|
|
55
|
+
avoid them exploding.
|
|
56
|
+
Must be a small positive number like 1e-6, or
|
|
57
|
+
zero to disable penalty calculation altogether.
|
|
58
|
+
param validation_radio: How much of the training set to dedicate to use
|
|
59
|
+
as validation during fitting.
|
|
60
|
+
Must be between 0 and 1 (inclusive, exclusive),
|
|
61
|
+
if set to exactly 0 then incompatible stopping
|
|
62
|
+
methods cannot be used (e.g. early stopping).
|
|
63
|
+
Ignored when using the dccp library solver.
|
|
64
|
+
param weighting_method: The weighting method to use: apply weights to
|
|
65
|
+
the cost contribution of each data point to help
|
|
66
|
+
avoid outliers.
|
|
67
|
+
If left to None, will use NoneWeightingMethod().
|
|
68
|
+
param stopping_methods: A list of stopping methods, must not be empty.
|
|
69
|
+
At each epoch, these methods will be
|
|
70
|
+
sequentially asked whether the training should
|
|
71
|
+
stop. In this case, epoch ends by rolling back
|
|
72
|
+
to the epoch with the best validation cost.
|
|
73
|
+
If left to None, will use:
|
|
74
|
+
[
|
|
75
|
+
CostStoppingMethod(),
|
|
76
|
+
EarlyStoppingMethod(),
|
|
77
|
+
EpochStoppingMethod(),
|
|
78
|
+
TrainStopStoppingMethod(),
|
|
79
|
+
]
|
|
80
|
+
Ignored when using the dccp library solver.
|
|
81
|
+
param use_dccp_library: Whether to use the dccp library as a solver or
|
|
82
|
+
a manual constraints linearization in fit.
|
|
83
|
+
param verbose: Whether to log extra information. 0: no logging,
|
|
84
|
+
1: basic logging / timing, 2: cvxpy solve() set
|
|
85
|
+
to verbose mode.
|
|
86
|
+
param random_state: A RandomState object or None to allow for seeded
|
|
87
|
+
randomness.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
self.kind: Literal['max', 'min'] = kind
|
|
91
|
+
self.margin = margin
|
|
92
|
+
self.penalty = penalty
|
|
93
|
+
self.validation_ratio = validation_ratio
|
|
94
|
+
self.weighting_method = weighting_method
|
|
95
|
+
self.stopping_methods = stopping_methods
|
|
96
|
+
self.use_dccp_library = use_dccp_library
|
|
97
|
+
self.verbose: Literal[0, 1, 2] = verbose
|
|
98
|
+
self.random_state = random_state
|
|
99
|
+
|
|
100
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> MorphoPerceptron:
|
|
101
|
+
"""
|
|
102
|
+
Fit the classifier, create attributes:
|
|
103
|
+
- self.weights
|
|
104
|
+
- self.classes_: Unique labels generated from y
|
|
105
|
+
|
|
106
|
+
X and y must represent binary classifiable data.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
# input data validation
|
|
110
|
+
random_state = check_random_state(self.random_state)
|
|
111
|
+
X, y = validate_data(self, X, y) # type: ignore
|
|
112
|
+
self.scaler_ = StandardScaler()
|
|
113
|
+
X_scaled = self.scaler_.fit_transform(X)
|
|
114
|
+
|
|
115
|
+
# set unset parameters to their default values
|
|
116
|
+
if self.weighting_method is None:
|
|
117
|
+
weighting_method = NoneSampleWeighting()
|
|
118
|
+
else:
|
|
119
|
+
weighting_method = self.weighting_method
|
|
120
|
+
if self.stopping_methods is None:
|
|
121
|
+
stopping_methods = [
|
|
122
|
+
CostStoppingMethod(),
|
|
123
|
+
EarlyStoppingMethod(),
|
|
124
|
+
EpochStoppingMethod(),
|
|
125
|
+
TrainStopStoppingMethod(),
|
|
126
|
+
]
|
|
127
|
+
else:
|
|
128
|
+
stopping_methods = self.stopping_methods
|
|
129
|
+
|
|
130
|
+
# create classes and convert them to distinct integers for fitting
|
|
131
|
+
# the classes are persisted inside the object for use in predict
|
|
132
|
+
self.classes_ = unique_labels(y)
|
|
133
|
+
classes_list = list(self.classes_)
|
|
134
|
+
y_integers = np.array(
|
|
135
|
+
[classes_list.index(c) for c in y], dtype=np.int32
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if len(classes_list) != 2:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
'Only binary classification is supported but '
|
|
141
|
+
f'got {len(classes_list)} class(es).'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# create and train perceptrons
|
|
145
|
+
trainer = SimplePerceptronDccpTrainer(
|
|
146
|
+
self.kind,
|
|
147
|
+
self.margin,
|
|
148
|
+
self.penalty,
|
|
149
|
+
self.validation_ratio,
|
|
150
|
+
weighting_method,
|
|
151
|
+
stopping_methods,
|
|
152
|
+
self.use_dccp_library,
|
|
153
|
+
self.verbose,
|
|
154
|
+
random_state,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
trainer.train(X_scaled, y_integers)
|
|
158
|
+
self.weights_ = trainer.weights
|
|
159
|
+
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def decision_function(self, X: np.ndarray) -> np.ndarray:
|
|
163
|
+
check_is_fitted(self)
|
|
164
|
+
X = validate_data(self, X, reset=False) # type: ignore
|
|
165
|
+
X_scaled = cast(np.ndarray, self.scaler_.transform(X))
|
|
166
|
+
|
|
167
|
+
expr = self.weights_ + X_scaled
|
|
168
|
+
|
|
169
|
+
if self.kind == 'max':
|
|
170
|
+
return expr.max(axis=1)
|
|
171
|
+
return expr.min(axis=1)
|
|
172
|
+
|
|
173
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
174
|
+
check_is_fitted(self)
|
|
175
|
+
return self.classes_[(self.decision_function(X) >= 0).astype(int)]
|
|
176
|
+
|
|
177
|
+
def __sklearn_tags__(self):
|
|
178
|
+
"""
|
|
179
|
+
Overriden method to allow check_estimator to not run accuracy tests.
|
|
180
|
+
These are designed for perceptrons with a linear decision boundary,
|
|
181
|
+
which is not the case for a morphological perceptron.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
tags = super().__sklearn_tags__()
|
|
185
|
+
tags.classifier_tags.multi_class = False
|
|
186
|
+
tags.classifier_tags.poor_score = True
|
|
187
|
+
return tags
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .stopping_base import StoppingMethod as StoppingMethod
|
|
2
|
+
from .stopping_cost import CostStoppingMethod as CostStoppingMethod
|
|
3
|
+
from .stopping_early import EarlyStoppingMethod as EarlyStoppingMethod
|
|
4
|
+
from .stopping_epoch import EpochStoppingMethod as EpochStoppingMethod
|
|
5
|
+
from .stopping_train_stop import (
|
|
6
|
+
TrainStopStoppingMethod as TrainStopStoppingMethod,
|
|
7
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StoppingMethod(ABC):
|
|
5
|
+
"""
|
|
6
|
+
Abstract class for a fitting stopping method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def requires_validation(self) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
Whether the stopping method requires the training to be split in
|
|
13
|
+
train/validation sets.
|
|
14
|
+
|
|
15
|
+
Can still be False even if using validation_cost.
|
|
16
|
+
In fact, the should_stop method should use the validation cost as the
|
|
17
|
+
main and most reliable cost, in case of no validation it will just be
|
|
18
|
+
the same as the training cost.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def should_stop(
|
|
23
|
+
self, n_epochs: int, train_cost: float, validation_cost: float
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Given some information, return whether the fitting process should stop.
|
|
27
|
+
"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from . import StoppingMethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CostStoppingMethod(StoppingMethod):
|
|
5
|
+
"""
|
|
6
|
+
Stopping method that triggers whenever the cost gets lower or equal to the
|
|
7
|
+
parameter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, cost_threshold=1e-6):
|
|
11
|
+
if cost_threshold <= 0:
|
|
12
|
+
raise ValueError(
|
|
13
|
+
f'invalid done_threshold, expected > 0 but got {cost_threshold}'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
self.cost_threshold = cost_threshold
|
|
17
|
+
|
|
18
|
+
def requires_validation(self) -> bool:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
def should_stop(
|
|
22
|
+
self, n_epochs: int, train_cost: float, validation_cost: float
|
|
23
|
+
) -> bool:
|
|
24
|
+
return validation_cost <= self.cost_threshold
|