sklearn-morpho 0.1.0__py2.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 +3 -0
- sklearn_morpho/classifiers/ldep.py +172 -0
- sklearn_morpho/dccp/dccp_ldep.py +162 -0
- sklearn_morpho/dccp/dccp_wrapper.py +225 -0
- sklearn_morpho/stopping/__init__.py +5 -0
- sklearn_morpho/stopping/stopping_base.py +20 -0
- sklearn_morpho/stopping/stopping_cost.py +21 -0
- sklearn_morpho/stopping/stopping_early.py +35 -0
- sklearn_morpho/stopping/stopping_epoch.py +21 -0
- sklearn_morpho/stopping/stopping_train_stop.py +15 -0
- sklearn_morpho/tests/moons_test.py +28 -0
- sklearn_morpho/tests/sklearn_test.py +19 -0
- sklearn_morpho/tests/train_test.py +17 -0
- sklearn_morpho/weighting/__init__.py +3 -0
- sklearn_morpho/weighting/weighting_base.py +45 -0
- sklearn_morpho/weighting/weighting_dist.py +33 -0
- sklearn_morpho/weighting/weighting_none.py +14 -0
- sklearn_morpho-0.1.0.dist-info/METADATA +4 -0
- sklearn_morpho-0.1.0.dist-info/RECORD +21 -0
- sklearn_morpho-0.1.0.dist-info/WHEEL +5 -0
- sklearn_morpho-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Literal, cast
|
|
3
|
+
|
|
4
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
5
|
+
from sklearn.preprocessing import StandardScaler
|
|
6
|
+
from sklearn.utils import check_random_state
|
|
7
|
+
from sklearn.utils.validation import validate_data, check_is_fitted
|
|
8
|
+
from sklearn.utils.multiclass import unique_labels
|
|
9
|
+
|
|
10
|
+
from ..dccp.dccp_ldep import LDEPDccpTrainer
|
|
11
|
+
from ..stopping import (
|
|
12
|
+
StoppingMethod,
|
|
13
|
+
CostStoppingMethod,
|
|
14
|
+
EarlyStoppingMethod,
|
|
15
|
+
EpochStoppingMethod,
|
|
16
|
+
TrainStopStoppingMethod,
|
|
17
|
+
)
|
|
18
|
+
from ..weighting import SampleWeighting, NoneSampleWeighting
|
|
19
|
+
|
|
20
|
+
class LDEP(ClassifierMixin, BaseEstimator):
|
|
21
|
+
"""
|
|
22
|
+
Scikit-learn estimator wrapper around a l-DEP (linear Dilation-Erosion
|
|
23
|
+
morphological Perceptron) for binary data classification.
|
|
24
|
+
|
|
25
|
+
l-DEP forward pass equation:
|
|
26
|
+
|
|
27
|
+
\\[ y = f(\\lambda \\tau_(R_1(x)) + (1 - \\lambda) \\tau'_(R_2(x))) \\]
|
|
28
|
+
|
|
29
|
+
Where $\\tau$ refers to the activation of a (max, +) morphological
|
|
30
|
+
perceptron and $\\tau'$ to a (min, +) one.
|
|
31
|
+
$R_1, R_2$ are linear transformations to apply to the training data.
|
|
32
|
+
They convert it into a latent space with possibly different dimensions,
|
|
33
|
+
also allowing classification of arbitrarily distributed data.
|
|
34
|
+
A higher dimension for the latent space will result in slower training
|
|
35
|
+
times, but will allow the decision boundary to be more complex.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, latent_dims: tuple[int, int] = (10, 10), margin = 1.,
|
|
39
|
+
validation_ratio = .3,
|
|
40
|
+
weighting_method: SampleWeighting | None = None,
|
|
41
|
+
stopping_methods: list[StoppingMethod] | None = None,
|
|
42
|
+
verbose: Literal[0, 1, 2] = 0,
|
|
43
|
+
random_state: np.random.RandomState | None = None) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Initialize the classifier, see class help for more.
|
|
46
|
+
|
|
47
|
+
param latent_dims: The dimensions of the latent spaces used for the
|
|
48
|
+
linear transformations output
|
|
49
|
+
param margin: Enforce a margin between the decision boundary
|
|
50
|
+
and the data. May help with linearly separable
|
|
51
|
+
datasets, but generally lower is more accurate.
|
|
52
|
+
param validation_radio: How much of the training set to dedicate to use
|
|
53
|
+
as validation during fitting.
|
|
54
|
+
Must be between 0 and 1 (inclusive, exclusive),
|
|
55
|
+
if set to exactly 0 then incompatible stopping
|
|
56
|
+
methods cannot be used (e.g. early stopping).
|
|
57
|
+
param weighting_method: The weighting method to use: apply weights to
|
|
58
|
+
the cost contribution of each data point to help
|
|
59
|
+
avoid outliers.
|
|
60
|
+
If left to None, will use NoneWeightingMethod()
|
|
61
|
+
param stopping_methods: A list of stopping methods, must not be empty.
|
|
62
|
+
At each epoch, these methods will be
|
|
63
|
+
sequentially asked whether the training should
|
|
64
|
+
stop. In this case, epoch ends by rolling back
|
|
65
|
+
to the epoch with the best validation cost.
|
|
66
|
+
If left to None, will use:
|
|
67
|
+
[
|
|
68
|
+
CostStoppingMethod(1e-6),
|
|
69
|
+
EarlyStoppingMethod(5),
|
|
70
|
+
EpochStoppingMethod(20),
|
|
71
|
+
TrainStopStoppingMethod(),
|
|
72
|
+
]
|
|
73
|
+
param verbose: Whether to log extra information. 0: no logging,
|
|
74
|
+
1: basic logging / timing, 2: cvxpy solve() set
|
|
75
|
+
to verbose mode.
|
|
76
|
+
param random_state: A RandomState object or None to allow for seeded
|
|
77
|
+
randomness.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
self.latent_dims = latent_dims
|
|
81
|
+
self.margin = margin
|
|
82
|
+
self.validation_ratio = validation_ratio
|
|
83
|
+
self.weighting_method = weighting_method
|
|
84
|
+
self.stopping_methods = stopping_methods
|
|
85
|
+
self.verbose: Literal[0, 1, 2] = verbose
|
|
86
|
+
self.random_state = random_state
|
|
87
|
+
|
|
88
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> LDEP:
|
|
89
|
+
"""
|
|
90
|
+
Fit the classifier, create attributes:
|
|
91
|
+
- self.max_perceptron_
|
|
92
|
+
- self.min_perceptron_
|
|
93
|
+
- self.lambda_
|
|
94
|
+
- self.max_matrix_
|
|
95
|
+
- self.min_matrix_
|
|
96
|
+
- self.classes_: Unique labels generated from y
|
|
97
|
+
|
|
98
|
+
X and y must represent binary classifiable data.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# input data validation
|
|
102
|
+
random_state = check_random_state(self.random_state)
|
|
103
|
+
X, y = validate_data(self, X, y)
|
|
104
|
+
self.scaler_ = StandardScaler()
|
|
105
|
+
X_scaled = self.scaler_.fit_transform(X)
|
|
106
|
+
|
|
107
|
+
# set unset parameters to their default values
|
|
108
|
+
if self.weighting_method is None:
|
|
109
|
+
weighting_method = NoneSampleWeighting()
|
|
110
|
+
else:
|
|
111
|
+
weighting_method = self.weighting_method
|
|
112
|
+
if self.stopping_methods is None:
|
|
113
|
+
stopping_methods = [
|
|
114
|
+
CostStoppingMethod(1e-6),
|
|
115
|
+
EarlyStoppingMethod(5),
|
|
116
|
+
EpochStoppingMethod(20),
|
|
117
|
+
TrainStopStoppingMethod(),
|
|
118
|
+
]
|
|
119
|
+
else:
|
|
120
|
+
stopping_methods = self.stopping_methods
|
|
121
|
+
|
|
122
|
+
# create classes and convert them to distinct integers for fitting
|
|
123
|
+
# the classes are persisted inside the object for use in predict
|
|
124
|
+
self.classes_ = unique_labels(y)
|
|
125
|
+
classes_list = list(self.classes_)
|
|
126
|
+
y_integers = np.array([classes_list.index(c) for c in y],
|
|
127
|
+
dtype=np.int32)
|
|
128
|
+
|
|
129
|
+
if len(classes_list) != 2:
|
|
130
|
+
raise ValueError('Only binary classification is supported but '
|
|
131
|
+
f'got {len(classes_list)} class(es).')
|
|
132
|
+
|
|
133
|
+
# create and train perceptrons
|
|
134
|
+
trainer = LDEPDccpTrainer(self.latent_dims, self.margin,
|
|
135
|
+
self.validation_ratio, weighting_method,
|
|
136
|
+
stopping_methods, self.verbose,
|
|
137
|
+
random_state)
|
|
138
|
+
|
|
139
|
+
trainer.train(X_scaled, y_integers)
|
|
140
|
+
self.max_perceptron_ = trainer.max_perceptron
|
|
141
|
+
self.min_perceptron_ = trainer.min_perceptron
|
|
142
|
+
self.lambda_ = trainer.lambda_
|
|
143
|
+
self.max_matrix_ = trainer.max_matrix
|
|
144
|
+
self.min_matrix_ = trainer.min_matrix
|
|
145
|
+
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def decision_function(self, X: np.ndarray) -> np.ndarray:
|
|
149
|
+
check_is_fitted(self)
|
|
150
|
+
X = validate_data(self, X, reset=False)
|
|
151
|
+
X_scaled = cast(np.ndarray, self.scaler_.transform(X))
|
|
152
|
+
|
|
153
|
+
expr_max = np.max(self.max_perceptron_ + X_scaled @ self.max_matrix_.T,
|
|
154
|
+
axis=1)
|
|
155
|
+
expr_min = np.min(self.min_perceptron_ + X_scaled @ self.min_matrix_.T,
|
|
156
|
+
axis=1)
|
|
157
|
+
return expr_max * self.lambda_ + expr_min * (1 - self.lambda_)
|
|
158
|
+
|
|
159
|
+
def predict(self, X: np.ndarray) -> np.ndarray:
|
|
160
|
+
check_is_fitted(self)
|
|
161
|
+
return self.classes_[(self.decision_function(X) >= 0).astype(int)]
|
|
162
|
+
|
|
163
|
+
def __sklearn_tags__(self):
|
|
164
|
+
"""
|
|
165
|
+
Overriden method to allow check_estimator to not run accuracy tests.
|
|
166
|
+
These are designed for perceptrons with a linear decision boundary,
|
|
167
|
+
which is not the case for a morphological perceptron.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
tags = super().__sklearn_tags__()
|
|
171
|
+
tags.classifier_tags.multi_class = False
|
|
172
|
+
return tags
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
import numpy as np
|
|
3
|
+
import cvxpy as cp
|
|
4
|
+
|
|
5
|
+
from .dccp_wrapper import DccpTrainer
|
|
6
|
+
from ..weighting import SampleWeighting
|
|
7
|
+
from ..stopping import StoppingMethod
|
|
8
|
+
|
|
9
|
+
class LDEPDccpTrainer(DccpTrainer):
|
|
10
|
+
def __init__(self, latent_dims: tuple[int, int],
|
|
11
|
+
margin: float, validation_ratio: float,
|
|
12
|
+
weighting_method: SampleWeighting,
|
|
13
|
+
stopping_methods: list[StoppingMethod],
|
|
14
|
+
verbose: Literal[0, 1, 2],
|
|
15
|
+
random_state: np.random.RandomState) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialize the l-DEP trainer.
|
|
18
|
+
|
|
19
|
+
param latent_dims: the latent dimensions for the max and min perceptrons
|
|
20
|
+
param [others]: see base class
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
self.latent_dims = latent_dims
|
|
24
|
+
|
|
25
|
+
super().__init__(margin, validation_ratio, weighting_method,
|
|
26
|
+
stopping_methods, verbose, random_state)
|
|
27
|
+
|
|
28
|
+
def at_training_start(self, data_dim: int) -> None:
|
|
29
|
+
N_max, N_min = self.latent_dims
|
|
30
|
+
|
|
31
|
+
self._objective = None
|
|
32
|
+
|
|
33
|
+
# Extracted parameters that will be populated during training but need
|
|
34
|
+
# initial values for linearization
|
|
35
|
+
self.max_perceptron = self.random_state.randn(N_max)
|
|
36
|
+
self.min_perceptron = self.random_state.randn(N_min)
|
|
37
|
+
self.max_matrix = self.random_state.randn(N_max, data_dim)
|
|
38
|
+
self.min_matrix = self.random_state.randn(N_min, data_dim)
|
|
39
|
+
|
|
40
|
+
# Create constraints for linearization derived from real parameters,
|
|
41
|
+
# used to absorbe non-convex parameters that are restored at the end.
|
|
42
|
+
# Method inspired by arXiv:2011.06512v1.
|
|
43
|
+
# TODO: is this representation an issue since the final lambda parameter
|
|
44
|
+
# is not necessarily in [0, 1]?
|
|
45
|
+
self._max_training_weights = cp.Variable(N_max)
|
|
46
|
+
self._min_training_weights = cp.Variable(N_min)
|
|
47
|
+
self._max_training_matrix = cp.Variable((N_max, data_dim))
|
|
48
|
+
self._min_training_matrix = cp.Variable((N_min, data_dim))
|
|
49
|
+
|
|
50
|
+
def get_problem(self, X: np.ndarray, y: np.ndarray,
|
|
51
|
+
cost_weights: np.ndarray) -> cp.Problem:
|
|
52
|
+
K = X.shape[0]
|
|
53
|
+
|
|
54
|
+
# the objective and the slack variables do not change, cache them
|
|
55
|
+
if self._objective is None:
|
|
56
|
+
self._slack = cp.Variable(K)
|
|
57
|
+
self._objective = cp.Minimize(
|
|
58
|
+
cp.sum(cp.multiply(cp.pos(self._slack), cost_weights)))
|
|
59
|
+
|
|
60
|
+
# Constraints: convex constraints are for data points in the first
|
|
61
|
+
# class, while concave ones are for points in the second class.
|
|
62
|
+
# Therefore, linearize things when needed to make the problem convex,
|
|
63
|
+
# sometimes using values from the previous epoch.
|
|
64
|
+
|
|
65
|
+
idx_max = np.argmax(self.max_perceptron + X @ self.max_matrix.T,
|
|
66
|
+
axis=1)
|
|
67
|
+
idx_min = np.argmin(self.min_perceptron + X @ self.min_matrix.T,
|
|
68
|
+
axis=1)
|
|
69
|
+
|
|
70
|
+
# create arrays to regroup the active indices using numpy, to then apply
|
|
71
|
+
# constraints all at once and use AST optimizations inside cvxpy
|
|
72
|
+
M_max = np.zeros((K, self.max_perceptron.size))
|
|
73
|
+
M_min = np.zeros((K, self.min_perceptron.size))
|
|
74
|
+
M_max[np.arange(K), idx_max] = 1
|
|
75
|
+
M_min[np.arange(K), idx_min] = 1
|
|
76
|
+
|
|
77
|
+
constraints = []
|
|
78
|
+
for label in [0, 1]:
|
|
79
|
+
mask = y == label
|
|
80
|
+
if not np.any(mask):
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
X_ = X[mask]
|
|
84
|
+
K_ = X_.shape[0]
|
|
85
|
+
|
|
86
|
+
# start building the perceptron's outputs before the min/max
|
|
87
|
+
expr_max = X_ @ self._max_training_matrix.T
|
|
88
|
+
expr_min = X_ @ self._min_training_matrix.T
|
|
89
|
+
|
|
90
|
+
# add weights to every row of (X @ matrix.T) using np.ones to create
|
|
91
|
+
# a matrix safely for cvxpy's cpp backend
|
|
92
|
+
ones = np.ones((K_, 1))
|
|
93
|
+
expr_max += ones @ cp.reshape(self._max_training_weights,
|
|
94
|
+
(1, self.max_perceptron.size),
|
|
95
|
+
order='C')
|
|
96
|
+
expr_min += ones @ cp.reshape(self._min_training_weights,
|
|
97
|
+
(1, self.min_perceptron.size),
|
|
98
|
+
order='C')
|
|
99
|
+
|
|
100
|
+
active_max = cp.sum(cp.multiply(M_max[mask], expr_max), axis=1)
|
|
101
|
+
active_min = cp.sum(cp.multiply(M_min[mask], expr_min), axis=1)
|
|
102
|
+
|
|
103
|
+
if label == 0:
|
|
104
|
+
constraints.append(self._slack[mask] >= self.margin
|
|
105
|
+
+ cp.max(expr_max, axis=1) + active_min)
|
|
106
|
+
else:
|
|
107
|
+
constraints.append(self._slack[mask] >= self.margin
|
|
108
|
+
- active_max - cp.min(expr_min, axis=1))
|
|
109
|
+
return cp.Problem(self._objective, constraints)
|
|
110
|
+
|
|
111
|
+
def get_cost(self, X: np.ndarray, y: np.ndarray) -> float:
|
|
112
|
+
expr_max = np.max(self.max_perceptron + X @ self.max_matrix.T, axis=1)
|
|
113
|
+
expr_min = np.min(self.min_perceptron + X @ self.min_matrix.T, axis=1)
|
|
114
|
+
|
|
115
|
+
# can use the expressions directly without needing to multiply by lambda
|
|
116
|
+
# since still in the training phase
|
|
117
|
+
cost = self.margin + (expr_max + expr_min) * (1 - 2 * y)
|
|
118
|
+
return np.maximum(0, cost).sum()
|
|
119
|
+
|
|
120
|
+
def after_epoch(self) -> None:
|
|
121
|
+
# update the perceptrons weights from this epoch's results
|
|
122
|
+
if self._max_training_weights.value is None or \
|
|
123
|
+
self._min_training_weights.value is None:
|
|
124
|
+
raise ValueError('CvxPy could not optimize a perceptron')
|
|
125
|
+
|
|
126
|
+
self.max_perceptron = self._max_training_weights.value
|
|
127
|
+
self.min_perceptron = self._min_training_weights.value
|
|
128
|
+
|
|
129
|
+
# extract the matrices
|
|
130
|
+
if self._max_training_matrix.value is None \
|
|
131
|
+
or self._min_training_matrix.value is None:
|
|
132
|
+
raise ValueError('CvxPy could not optimize transformation matrices')
|
|
133
|
+
self.max_matrix = self._max_training_matrix.value
|
|
134
|
+
self.min_matrix = self._min_training_matrix.value
|
|
135
|
+
|
|
136
|
+
def at_training_end(self) -> None:
|
|
137
|
+
max_matrix_norm = np.linalg.norm(self.max_matrix)
|
|
138
|
+
min_matrix_norm = np.linalg.norm(self.min_matrix)
|
|
139
|
+
|
|
140
|
+
div = max_matrix_norm + min_matrix_norm
|
|
141
|
+
if np.isclose(div, 0):
|
|
142
|
+
raise ValueError('Transformation matrices are zero, cannot solve')
|
|
143
|
+
|
|
144
|
+
self.lambda_ = max_matrix_norm / div
|
|
145
|
+
self.max_matrix /= self.lambda_
|
|
146
|
+
self.min_matrix /= 1 - self.lambda_
|
|
147
|
+
self.max_perceptron /= self.lambda_
|
|
148
|
+
self.min_perceptron /= 1 - self.lambda_
|
|
149
|
+
|
|
150
|
+
def save_best(self) -> None:
|
|
151
|
+
self.saved = {
|
|
152
|
+
'max_w': np.copy(self.max_perceptron),
|
|
153
|
+
'min_w': np.copy(self.min_perceptron),
|
|
154
|
+
'max_m': np.copy(self.max_matrix),
|
|
155
|
+
'min_m': np.copy(self.min_matrix),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def rollback_to_best(self) -> None:
|
|
159
|
+
self.max_perceptron = self.saved['max_w']
|
|
160
|
+
self.min_perceptron = self.saved['min_w']
|
|
161
|
+
self.max_matrix = self.saved['max_m']
|
|
162
|
+
self.min_matrix = self.saved['min_m']
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Literal, cast
|
|
3
|
+
import numpy as np
|
|
4
|
+
import cvxpy as cp
|
|
5
|
+
from time import time
|
|
6
|
+
|
|
7
|
+
from sklearn.model_selection import train_test_split
|
|
8
|
+
from ..weighting import SampleWeighting
|
|
9
|
+
from ..stopping import StoppingMethod
|
|
10
|
+
|
|
11
|
+
class DccpTrainer(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract class for DCCP optimization using cvxpy.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, margin: float, validation_ratio: float,
|
|
17
|
+
weighting_method: SampleWeighting,
|
|
18
|
+
stopping_methods: list[StoppingMethod],
|
|
19
|
+
verbose: Literal[0, 1, 2],
|
|
20
|
+
random_state: np.random.RandomState) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the trainer.
|
|
23
|
+
|
|
24
|
+
param validation_radio: How much of the training set to dedicate to use
|
|
25
|
+
as validation during fitting.
|
|
26
|
+
param weighting_method: The weighting method to use: apply weights to
|
|
27
|
+
the cost contribution of each data point to help
|
|
28
|
+
avoid outliers.
|
|
29
|
+
param stopping_methods: A list of stopping methods, must not be empty.
|
|
30
|
+
At each epoch, these methods will be
|
|
31
|
+
sequentially asked whether the training should
|
|
32
|
+
stop. In this case, training ends by rolling
|
|
33
|
+
back to the epoch with the best validation cost.
|
|
34
|
+
param verbose: Whether to log extra information. 0: no logging,
|
|
35
|
+
1: basic logging / timing, 2: cvxpy solve() set
|
|
36
|
+
to verbose mode.
|
|
37
|
+
param random_state: A RandomState object or None to allow for seeded
|
|
38
|
+
randomness.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
if margin < 0:
|
|
42
|
+
raise ValueError(f'Invalid margin, expected >= 0 but got {margin}')
|
|
43
|
+
if not 0 <= validation_ratio < 1:
|
|
44
|
+
raise ValueError('Invalid validation ratio, expected >= 0 and < 1 '
|
|
45
|
+
f'but got {validation_ratio}')
|
|
46
|
+
if not len(stopping_methods):
|
|
47
|
+
raise ValueError('Empty list of stopping methods, training would '
|
|
48
|
+
'run indefinitely')
|
|
49
|
+
|
|
50
|
+
self.margin = margin
|
|
51
|
+
self.validation_ratio = validation_ratio
|
|
52
|
+
self.weighting_method = weighting_method
|
|
53
|
+
self.stopping_methods = stopping_methods
|
|
54
|
+
self.verbose = verbose
|
|
55
|
+
self.random_state = random_state
|
|
56
|
+
|
|
57
|
+
def at_training_start(self, data_dim: int) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Optional actions to take when the training starts.
|
|
60
|
+
For example, create and persist a list of additional variables to
|
|
61
|
+
optimize.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def after_epoch(self) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Optional additional actions to take after each epoch, that is after
|
|
67
|
+
cvxpy solve() and during wrapping code.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def at_training_end(self) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Optional actions to take when the training is over.
|
|
73
|
+
Ran as the very last function call in train, meaning after
|
|
74
|
+
rollback_to_best is called.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def save_best(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Paired with rollback_to_best.
|
|
81
|
+
|
|
82
|
+
Save the data generated by after_epoch somewhere accessible at the
|
|
83
|
+
end of the training process.
|
|
84
|
+
|
|
85
|
+
Used in the training loop to roll back to the best state of the model at
|
|
86
|
+
the end of the loop.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def rollback_to_best(self) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Paired with save_best.
|
|
93
|
+
|
|
94
|
+
Restore the state saved in save_best, after the training process is
|
|
95
|
+
done, to go back to the best performing model.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def get_problem(self, X: np.ndarray, y: np.ndarray,
|
|
100
|
+
cost_weights: np.ndarray) -> cp.Problem:
|
|
101
|
+
"""
|
|
102
|
+
Compute a cvxpy Objective and a list of Constraints for use in DCCP.
|
|
103
|
+
The prlblem must be DCP, meaning the constraints must all be convex.
|
|
104
|
+
For concave constraints, they must be linearized beforehand, possibly
|
|
105
|
+
from constant values taken from the previous epoch result.
|
|
106
|
+
|
|
107
|
+
param X: The data points
|
|
108
|
+
param y: The data points classes
|
|
109
|
+
param cost_weights: The wdccp weights, all 1 if not using wdccp, for
|
|
110
|
+
use in computing the objective.
|
|
111
|
+
This is an array with one element corresponding to
|
|
112
|
+
an element in X.
|
|
113
|
+
|
|
114
|
+
return: A tuple containing a cvxpy Objective and a list of
|
|
115
|
+
Constraints.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def get_cost(self, X: np.ndarray, y: np.ndarray) -> float:
|
|
120
|
+
"""
|
|
121
|
+
Compute the cost for a set of sample points in the same way as the cost
|
|
122
|
+
defined in get_problem, used in train/validation training.
|
|
123
|
+
|
|
124
|
+
If applicable, should use the weights computed in this epoch's
|
|
125
|
+
after_epoch call, as it will have been made before this method's call.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def train(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Train using DCCP.
|
|
131
|
+
This means this function can only solve problems where the cost function
|
|
132
|
+
can be separable in the given convex and concave parts.
|
|
133
|
+
|
|
134
|
+
param X: The set of data points to use for training.
|
|
135
|
+
param y: The set of labels associated with elements of X.
|
|
136
|
+
|
|
137
|
+
return: The final cost, or -1 if no training happened.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
if self.verbose:
|
|
141
|
+
print('Starting fitting with DCCP')
|
|
142
|
+
|
|
143
|
+
no_validation = self.validation_ratio == 0
|
|
144
|
+
if no_validation:
|
|
145
|
+
for stopping_method in self.stopping_methods:
|
|
146
|
+
if stopping_method.requires_validation():
|
|
147
|
+
raise ValueError('Cannot train, at least one stopping '
|
|
148
|
+
'method requires validation which is '
|
|
149
|
+
'disabled by the user')
|
|
150
|
+
|
|
151
|
+
X_train, X_validation = X, np.empty(X.shape)
|
|
152
|
+
y_train, y_validation = y, np.empty(y.shape)
|
|
153
|
+
else:
|
|
154
|
+
X_train, X_validation, y_train, y_validation = train_test_split(
|
|
155
|
+
X, y, test_size=self.validation_ratio,
|
|
156
|
+
random_state=self.random_state)
|
|
157
|
+
X_train = cast(np.ndarray, X_train)
|
|
158
|
+
X_validation = cast(np.ndarray, X_validation)
|
|
159
|
+
y_train = cast(np.ndarray, y_train)
|
|
160
|
+
y_validation = cast(np.ndarray, y_validation)
|
|
161
|
+
|
|
162
|
+
if not X_train.size or (not X_validation.size and not no_validation):
|
|
163
|
+
raise ValueError('Current validation ratio makes unwanted '
|
|
164
|
+
'degenerate train/validation split: '
|
|
165
|
+
+ str(self.validation_ratio))
|
|
166
|
+
|
|
167
|
+
start = time()
|
|
168
|
+
cost_weights, cost_normalizer = \
|
|
169
|
+
self.weighting_method.fit_transform(X_train, y_train)
|
|
170
|
+
|
|
171
|
+
epoch = 1
|
|
172
|
+
validation_cost: float = np.inf
|
|
173
|
+
best_validation_cost: float = np.inf
|
|
174
|
+
|
|
175
|
+
# formulate the cvxpy problem to solve, common to all epochs
|
|
176
|
+
self.at_training_start(X_train.shape[1])
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
problem = self.get_problem(X_train, y_train, cost_weights)
|
|
180
|
+
|
|
181
|
+
# solve the problem, normalize the cost when using wdccp
|
|
182
|
+
cvxpy_cost = cast(float, problem.solve(verbose=self.verbose == 2)) \
|
|
183
|
+
* cost_normalizer
|
|
184
|
+
|
|
185
|
+
self.after_epoch()
|
|
186
|
+
|
|
187
|
+
# best score and loop logic
|
|
188
|
+
done = False
|
|
189
|
+
if no_validation:
|
|
190
|
+
train_cost = cvxpy_cost
|
|
191
|
+
validation_cost = cvxpy_cost
|
|
192
|
+
else:
|
|
193
|
+
train_cost = self.get_cost(X_train, y_train)
|
|
194
|
+
validation_cost = self.get_cost(X_validation, y_validation)
|
|
195
|
+
if validation_cost < best_validation_cost:
|
|
196
|
+
best_validation_cost = validation_cost
|
|
197
|
+
self.save_best()
|
|
198
|
+
|
|
199
|
+
if self.verbose:
|
|
200
|
+
if no_validation:
|
|
201
|
+
validation_comment = ' [no validation]'
|
|
202
|
+
else:
|
|
203
|
+
validation_comment = f', validation: {validation_cost:.8f}'
|
|
204
|
+
|
|
205
|
+
print(f'Epoch {epoch}, training cost: {cvxpy_cost:.8f}' +
|
|
206
|
+
validation_comment)
|
|
207
|
+
|
|
208
|
+
for stopping_method in self.stopping_methods:
|
|
209
|
+
if stopping_method.should_stop(
|
|
210
|
+
epoch, train_cost, validation_cost):
|
|
211
|
+
done = True
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if done:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
epoch += 1
|
|
218
|
+
|
|
219
|
+
if self.verbose:
|
|
220
|
+
dt = time() - start
|
|
221
|
+
print(f'DCCP done in {epoch} epochs, '
|
|
222
|
+
f'final cost is {validation_cost:.8f} in {dt:.2f}s')
|
|
223
|
+
|
|
224
|
+
self.rollback_to_best()
|
|
225
|
+
self.at_training_end()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class StoppingMethod(ABC):
|
|
4
|
+
"""
|
|
5
|
+
Abstract class for a fitting stopping method.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def requires_validation(self) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Whether the stopping method requires the training to be split in
|
|
12
|
+
train/validation sets.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def should_stop(self, n_epochs: int, train_cost: float,
|
|
17
|
+
validation_cost: float) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Given some information, return whether the fitting process should stop.
|
|
20
|
+
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import StoppingMethod
|
|
2
|
+
|
|
3
|
+
class CostStoppingMethod(StoppingMethod):
|
|
4
|
+
"""
|
|
5
|
+
Stopping method that triggers whenever the cost gets lower or equal to the
|
|
6
|
+
parameter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, cost_threshold: float):
|
|
10
|
+
if cost_threshold <= 0:
|
|
11
|
+
raise ValueError('invalid done_threshold, expected > 0 but got '
|
|
12
|
+
f'{cost_threshold}')
|
|
13
|
+
|
|
14
|
+
self.cost_threshold = cost_threshold
|
|
15
|
+
|
|
16
|
+
def requires_validation(self) -> bool:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
def should_stop(self, n_epochs: int, train_cost: float,
|
|
20
|
+
validation_cost: float) -> bool:
|
|
21
|
+
return validation_cost <= self.cost_threshold
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from . import StoppingMethod
|
|
2
|
+
|
|
3
|
+
class EarlyStoppingMethod(StoppingMethod):
|
|
4
|
+
"""
|
|
5
|
+
Stopping method that triggers whenever the validation cost starts increasing
|
|
6
|
+
while the training cost keeps decreasing, this for a set number of
|
|
7
|
+
consecutive epochs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, delay: int):
|
|
11
|
+
if delay <= 0:
|
|
12
|
+
raise ValueError(f'invalid delay, expected > 0 but got {delay}')
|
|
13
|
+
|
|
14
|
+
self.delay = delay
|
|
15
|
+
self.count = 0
|
|
16
|
+
|
|
17
|
+
self.best_validation_cost: float | None = None
|
|
18
|
+
|
|
19
|
+
def requires_validation(self) -> bool:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
def should_stop(self, n_epochs: int, train_cost: float,
|
|
23
|
+
validation_cost: float) -> bool:
|
|
24
|
+
if self.best_validation_cost is None:
|
|
25
|
+
self.best_validation_cost = validation_cost
|
|
26
|
+
else:
|
|
27
|
+
if validation_cost >= self.best_validation_cost:
|
|
28
|
+
self.count += 1
|
|
29
|
+
if self.count >= self.delay:
|
|
30
|
+
return True
|
|
31
|
+
else:
|
|
32
|
+
self.count = 0
|
|
33
|
+
self.best_validation_cost = validation_cost
|
|
34
|
+
|
|
35
|
+
return False
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import StoppingMethod
|
|
2
|
+
|
|
3
|
+
class EpochStoppingMethod(StoppingMethod):
|
|
4
|
+
"""
|
|
5
|
+
Stopping method that triggers whenever the number of epochs gets greater
|
|
6
|
+
than the parameter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, max_epochs: int):
|
|
10
|
+
if max_epochs <= 0:
|
|
11
|
+
raise ValueError('invalid max_epochs, expected > 0 but got '
|
|
12
|
+
f'{max_epochs}')
|
|
13
|
+
|
|
14
|
+
self.max_epochs = max_epochs
|
|
15
|
+
|
|
16
|
+
def requires_validation(self) -> bool:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
def should_stop(self, n_epochs: int, train_cost: float,
|
|
20
|
+
validation_cost: float) -> bool:
|
|
21
|
+
return n_epochs > self.max_epochs
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from . import StoppingMethod
|
|
2
|
+
|
|
3
|
+
class TrainStopStoppingMethod(StoppingMethod):
|
|
4
|
+
"""
|
|
5
|
+
Stopping method that triggers whenever the training cost gets to 0, that is
|
|
6
|
+
the perceptron found a global minimum and will no longer try to improve,
|
|
7
|
+
rendering validation-related stopping methods unusable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def requires_validation(self) -> bool:
|
|
11
|
+
return False
|
|
12
|
+
|
|
13
|
+
def should_stop(self, n_epochs: int, train_cost: float,
|
|
14
|
+
validation_cost: float) -> bool:
|
|
15
|
+
return train_cost <= 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from sklearn_morpho.classifiers.ldep import LDEP
|
|
4
|
+
from sklearn.datasets import make_moons
|
|
5
|
+
|
|
6
|
+
from sklearn_morpho.stopping import (
|
|
7
|
+
CostStoppingMethod,
|
|
8
|
+
EpochStoppingMethod,
|
|
9
|
+
TrainStopStoppingMethod,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def test_moons(runs=10, n_samples=500):
|
|
13
|
+
dep = LDEP(validation_ratio=0, stopping_methods=[
|
|
14
|
+
CostStoppingMethod(1e-6),
|
|
15
|
+
EpochStoppingMethod(20),
|
|
16
|
+
TrainStopStoppingMethod(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
for _ in range(runs):
|
|
20
|
+
X, y = make_moons(n_samples=n_samples)
|
|
21
|
+
dep.fit(X, y)
|
|
22
|
+
|
|
23
|
+
fails = X.shape[0] - np.sum(dep.predict(X) == y)
|
|
24
|
+
assert fails == 0
|
|
25
|
+
|
|
26
|
+
# TODO will most likely fail the test, will get back to this
|
|
27
|
+
# basically sometimes the matrices get really big, but adding their norm to
|
|
28
|
+
# the cost makes things converge slower and epochs very expensive
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from sklearn.utils.estimator_checks import check_estimator
|
|
2
|
+
|
|
3
|
+
from sklearn_morpho.classifiers.ldep import LDEP
|
|
4
|
+
from sklearn_morpho.stopping import (
|
|
5
|
+
CostStoppingMethod,
|
|
6
|
+
EpochStoppingMethod,
|
|
7
|
+
TrainStopStoppingMethod,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_check_estimator():
|
|
12
|
+
ldep = LDEP(validation_ratio=0,
|
|
13
|
+
stopping_methods=[
|
|
14
|
+
CostStoppingMethod(1e-6),
|
|
15
|
+
EpochStoppingMethod(20),
|
|
16
|
+
TrainStopStoppingMethod(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
assert check_estimator(ldep)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from sklearn_morpho.classifiers.ldep import LDEP
|
|
4
|
+
from sklearn.datasets import make_classification
|
|
5
|
+
|
|
6
|
+
def test_train_separable_dataset(n_samples=500):
|
|
7
|
+
# use a random state to guarantee a separable dataset
|
|
8
|
+
random_state = np.random.RandomState(11)
|
|
9
|
+
X, y = make_classification(n_samples=n_samples, n_features=2, n_redundant=0,
|
|
10
|
+
n_classes=2, n_clusters_per_class=1,
|
|
11
|
+
random_state=random_state)
|
|
12
|
+
|
|
13
|
+
classifier = LDEP(random_state=random_state)
|
|
14
|
+
classifier.fit(X, y)
|
|
15
|
+
|
|
16
|
+
fails = X.shape[0] - np.sum(classifier.predict(X) == y)
|
|
17
|
+
assert fails == 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
from sklearn.base import OneToOneFeatureMixin, TransformerMixin, BaseEstimator
|
|
5
|
+
|
|
6
|
+
WeightingResult = tuple[np.ndarray, float]
|
|
7
|
+
|
|
8
|
+
class SampleWeighting(ABC, OneToOneFeatureMixin, TransformerMixin,
|
|
9
|
+
BaseEstimator):
|
|
10
|
+
"""
|
|
11
|
+
Abstract class for transformers that convert data points to a set of
|
|
12
|
+
respective weights.
|
|
13
|
+
These weights may be used as the cost contribution in a DCCP problem.
|
|
14
|
+
|
|
15
|
+
For example, a sample weighting with functionality `np.full(len(X), 1)`
|
|
16
|
+
corresponds to no weighting at all.
|
|
17
|
+
|
|
18
|
+
The fit() method must create both:
|
|
19
|
+
- self.weights_: np.ndarray
|
|
20
|
+
- self.cost_normalizer_: float
|
|
21
|
+
|
|
22
|
+
If the implementation does not follow these guidelines, the weighting should
|
|
23
|
+
still be able to provide a WeightingResult as the return value of
|
|
24
|
+
transform().
|
|
25
|
+
In this case, you are free to override the transform() and fit_transform()
|
|
26
|
+
methods containing boilerplate code.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# for LSPs
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.weights_: np.ndarray
|
|
32
|
+
self.cost_normalizer_: float
|
|
33
|
+
|
|
34
|
+
def transform(self, X: np.ndarray | None) -> WeightingResult:
|
|
35
|
+
if X is not None:
|
|
36
|
+
raise ValueError(f'{self.__class__.__name__}: only supports '
|
|
37
|
+
'fitting and transforming the same data, do not '
|
|
38
|
+
'specify it in transform()')
|
|
39
|
+
|
|
40
|
+
return self.weights_, self.cost_normalizer_
|
|
41
|
+
|
|
42
|
+
# override this to make fit_transform usable but still prevent wrong usage
|
|
43
|
+
def fit_transform(self, X: np.ndarray, y: np.ndarray) -> WeightingResult:
|
|
44
|
+
self.fit(X, y)
|
|
45
|
+
return self.transform(None)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from . import SampleWeighting
|
|
4
|
+
|
|
5
|
+
class DistSampleWeighting(SampleWeighting):
|
|
6
|
+
"""
|
|
7
|
+
Weighting method that weights its inputs inversely proportionally to the
|
|
8
|
+
distance to their class' centroid.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> DistSampleWeighting:
|
|
12
|
+
# compute centroids, there should be no classes with no elements thanks
|
|
13
|
+
# to sklearn checks if used in the intended way with a compatible
|
|
14
|
+
# estimator
|
|
15
|
+
labels, inv, counts = np.unique(y, return_inverse=True,
|
|
16
|
+
return_counts=True)
|
|
17
|
+
sums = np.zeros((len(labels), X.shape[1]))
|
|
18
|
+
np.add.at(sums, inv, X)
|
|
19
|
+
centroids = sums / counts[:, np.newaxis]
|
|
20
|
+
|
|
21
|
+
# inverse distance from each data point to its respective class centroid
|
|
22
|
+
self.weights_ = 1 / (1e-6 + np.linalg.norm(X - centroids[y], axis=1))
|
|
23
|
+
max_centroid_w = np.array([self.weights_[y == y_].max()
|
|
24
|
+
for y_ in range(2)])
|
|
25
|
+
self.weights_ /= max_centroid_w[y]
|
|
26
|
+
|
|
27
|
+
cost_normalizer = self.weights_.sum()
|
|
28
|
+
if np.isclose(cost_normalizer, 0):
|
|
29
|
+
self.cost_normalizer_ = 1
|
|
30
|
+
else:
|
|
31
|
+
self.cost_normalizer_ = cost_normalizer
|
|
32
|
+
|
|
33
|
+
return self
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from . import SampleWeighting
|
|
4
|
+
|
|
5
|
+
class NoneSampleWeighting(SampleWeighting):
|
|
6
|
+
"""
|
|
7
|
+
Weighting method that does not weight its inputs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> NoneSampleWeighting:
|
|
11
|
+
self.weights_ = np.ones(len(X))
|
|
12
|
+
self.cost_normalizer_ = 1
|
|
13
|
+
|
|
14
|
+
return self
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
sklearn_morpho/__init__.py,sha256=ecP48uQBEYLb6b2ZOU4M3gd66oO_Nes_ayK6BsQ7XJU,84
|
|
2
|
+
sklearn_morpho/classifiers/ldep.py,sha256=N0FRkQAKvmGcxYbAdzJypn8fCEZs9JuFQCpR8gIs554,7613
|
|
3
|
+
sklearn_morpho/dccp/dccp_ldep.py,sha256=XqbF_kNa_jLy6721IiEUHa5QbwmeIXUACO9lE2I5flY,6986
|
|
4
|
+
sklearn_morpho/dccp/dccp_wrapper.py,sha256=i8GwbEH4q0q3N3yqhY2dcCnL1aeLppXTHy1f7iDWqJE,8814
|
|
5
|
+
sklearn_morpho/stopping/__init__.py,sha256=N4B8zlC7Q0-fJgn5O7h4Uk_CYMTP3yeB9-CaziigfHA,241
|
|
6
|
+
sklearn_morpho/stopping/stopping_base.py,sha256=INPU9XvHEnnOqZ4CMSVm5-xlw7UdPQyGsFoEG6_Vzdo,560
|
|
7
|
+
sklearn_morpho/stopping/stopping_cost.py,sha256=pSrp2NAF0d3H_JoMm3BcIFVszB3fqg_rlOZVuXIg24M,668
|
|
8
|
+
sklearn_morpho/stopping/stopping_early.py,sha256=yUE9EaNRFtmkLbJQIvd5F5Sy0PLka1wbuwD_9IaLOtM,1104
|
|
9
|
+
sklearn_morpho/stopping/stopping_epoch.py,sha256=dkJ7nW60KzfQotQwwnHS9Fvg2_EK-l6AdHgoEFs2mW0,638
|
|
10
|
+
sklearn_morpho/stopping/stopping_train_stop.py,sha256=ElPYiRUjoDTvwTqUszo2RrG9N8fOcZwq7YzHWJ9udoo,520
|
|
11
|
+
sklearn_morpho/tests/moons_test.py,sha256=xxKO3vNjTIcEPTmyS6HB3r_DiD82uVdQ8szfrVl5Pig,840
|
|
12
|
+
sklearn_morpho/tests/sklearn_test.py,sha256=pISK-F6wjVCIzeAfeR7J0ObEozi0lTRAkCpaelotEGY,531
|
|
13
|
+
sklearn_morpho/tests/train_test.py,sha256=GgZRo82wD-1ztmsWt_WwO4YAJSlx70T0_-F1sGemZoc,636
|
|
14
|
+
sklearn_morpho/weighting/__init__.py,sha256=AzWBDUf33vC6hnNHfC6TftpqFODTsbeV-D_TW4Ao5Tc,140
|
|
15
|
+
sklearn_morpho/weighting/weighting_base.py,sha256=928tUWDI3-bj3HahvDuOf_XtjeLk21MQ-ymNTcEc37g,1654
|
|
16
|
+
sklearn_morpho/weighting/weighting_dist.py,sha256=PIwEU7gmwjWz3eRkzbDgqiG5zg894CChMBGRc5AHZUI,1268
|
|
17
|
+
sklearn_morpho/weighting/weighting_none.py,sha256=1AgORveFg8EHtSr-GqdI1GjhWadzwLgryyIo0o0OE7U,333
|
|
18
|
+
sklearn_morpho-0.1.0.dist-info/METADATA,sha256=p8hzjB7yxlN6Tu2P1EuIfrcCM6sdIbbBJ70HSAfWa7w,80
|
|
19
|
+
sklearn_morpho-0.1.0.dist-info/WHEEL,sha256=e22IIVjxDyt0lABi4WpktFIGsmO_ebSDXLnPUbPK0E0,105
|
|
20
|
+
sklearn_morpho-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
21
|
+
sklearn_morpho-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|