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.
@@ -0,0 +1,40 @@
1
+ from . import StoppingMethod
2
+
3
+
4
+ class EarlyStoppingMethod(StoppingMethod):
5
+ """
6
+ Stopping method that triggers whenever the validation cost starts increasing
7
+ while the training cost keeps decreasing, this for a set number of
8
+ consecutive epochs.
9
+
10
+ It is assumed that the training cost keeps decreasing as should be the case
11
+ during functional training.
12
+ """
13
+
14
+ def __init__(self, delay=5):
15
+ if delay <= 0:
16
+ raise ValueError(f'invalid delay, expected > 0 but got {delay}')
17
+
18
+ self.delay = delay
19
+ self.count = 0
20
+
21
+ self.best_validation_cost: float | None = None
22
+
23
+ def requires_validation(self) -> bool:
24
+ return True
25
+
26
+ def should_stop(
27
+ self, n_epochs: int, train_cost: float, validation_cost: float
28
+ ) -> bool:
29
+ if self.best_validation_cost is None:
30
+ self.best_validation_cost = validation_cost
31
+ else:
32
+ if validation_cost >= self.best_validation_cost:
33
+ self.count += 1
34
+ if self.count >= self.delay:
35
+ return True
36
+ else:
37
+ self.count = 0
38
+ self.best_validation_cost = validation_cost
39
+
40
+ return False
@@ -0,0 +1,24 @@
1
+ from . import StoppingMethod
2
+
3
+
4
+ class EpochStoppingMethod(StoppingMethod):
5
+ """
6
+ Stopping method that triggers whenever the number of epochs gets greater or
7
+ equal to the parameter.
8
+ """
9
+
10
+ def __init__(self, max_epochs=20):
11
+ if max_epochs <= 0:
12
+ raise ValueError(
13
+ f'invalid max_epochs, expected > 0 but got {max_epochs}'
14
+ )
15
+
16
+ self.max_epochs = max_epochs
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 n_epochs >= self.max_epochs
@@ -0,0 +1,17 @@
1
+ from . import StoppingMethod
2
+
3
+
4
+ class TrainStopStoppingMethod(StoppingMethod):
5
+ """
6
+ Stopping method that triggers whenever the training cost gets to 0, that is
7
+ the perceptron found a global minimum and will no longer try to improve,
8
+ rendering validation-related stopping methods unusable.
9
+ """
10
+
11
+ def requires_validation(self) -> bool:
12
+ return False
13
+
14
+ def should_stop(
15
+ self, n_epochs: int, train_cost: float, validation_cost: float
16
+ ) -> bool:
17
+ return train_cost <= 0
@@ -0,0 +1,6 @@
1
+ from .dccp_ldep import LDEPDccpTrainer as LDEPDccpTrainer
2
+ from .dccp_rdep import RDEPDccpTrainer as RDEPDccpTrainer
3
+ from .dccp_simple_perceptron import (
4
+ SimplePerceptronDccpTrainer as SimplePerceptronDccpTrainer,
5
+ )
6
+ from .dccp_wrapper import DccpTrainer as DccpTrainer
@@ -0,0 +1,270 @@
1
+ from typing import Literal, cast
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
+
10
+ class LDEPDccpTrainer(DccpTrainer):
11
+ """
12
+ l-DEP trainer.
13
+
14
+ During training, the final parameters are implicitly embedded into one
15
+ another.
16
+ Some of them (like lambda) are only extracted once the training ends.
17
+ This is not necessary for performance, but is there for readability and
18
+ interpretability reasons once the estimator is trained.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ latent_dims: tuple[int, int],
24
+ margin: float,
25
+ penalty: float,
26
+ validation_ratio: float,
27
+ weighting_method: SampleWeighting,
28
+ stopping_methods: list[StoppingMethod],
29
+ use_dccp_library: bool,
30
+ verbose: Literal[0, 1, 2],
31
+ random_state: np.random.RandomState,
32
+ ) -> None:
33
+ """
34
+ Initialize the l-DEP trainer.
35
+
36
+ param latent_dims: the latent dimensions for the max and min perceptrons
37
+ param [others]: see base class
38
+ """
39
+
40
+ self.latent_dims = latent_dims
41
+
42
+ super().__init__(
43
+ margin,
44
+ penalty,
45
+ validation_ratio,
46
+ weighting_method,
47
+ stopping_methods,
48
+ use_dccp_library,
49
+ verbose,
50
+ random_state,
51
+ )
52
+
53
+ def at_training_start(self, data_dim: int) -> None:
54
+ N_max, N_min = self.latent_dims
55
+
56
+ self._objective = None
57
+
58
+ # Extracted parameters that will be populated during training but need
59
+ # initial values for linearization
60
+ self.max_perceptron = self.random_state.randn(N_max)
61
+ self.min_perceptron = self.random_state.randn(N_min)
62
+ self.max_matrix = self.random_state.randn(N_max, data_dim)
63
+ self.min_matrix = self.random_state.randn(N_min, data_dim)
64
+
65
+ # Create constraints for linearization derived from real parameters,
66
+ # used to absorbe non-convex parameters that are restored at the end.
67
+ # Method inspired by arXiv:2011.06512v1.
68
+ self._max_training_weights = cp.Variable(N_max)
69
+ self._min_training_weights = cp.Variable(N_min)
70
+ self._max_training_matrix = cp.Variable((N_max, data_dim))
71
+ self._min_training_matrix = cp.Variable((N_min, data_dim))
72
+
73
+ def set_objective(self, K: int, cost_weights: np.ndarray) -> None:
74
+ # the objective and the slack variables do not change, cache them
75
+ if self._objective is not None:
76
+ return
77
+
78
+ self._slack = cp.Variable(K)
79
+
80
+ value = cp.sum(cp.multiply(cp.pos(self._slack), cost_weights))
81
+ if self.penalty > 0:
82
+ value += self.penalty * (
83
+ cp.sum_squares(self._max_training_weights)
84
+ + cp.sum_squares(self._min_training_weights)
85
+ + cp.sum_squares(self._max_training_matrix)
86
+ + cp.sum_squares(self._min_training_matrix)
87
+ )
88
+
89
+ self._objective = cp.Minimize(value)
90
+
91
+ def get_problem_linearized(
92
+ self, X: np.ndarray, y: np.ndarray, cost_weights: np.ndarray
93
+ ) -> cp.Problem:
94
+ K = X.shape[0]
95
+ self.set_objective(K, cost_weights)
96
+
97
+ # Constraints: convex constraints are for data points in the first
98
+ # class, while concave ones are for points in the second class.
99
+ # Therefore, linearize things when needed to make the problem convex,
100
+ # sometimes using values from the previous epoch.
101
+
102
+ idx_max = np.argmax(self.max_perceptron + X @ self.max_matrix.T, axis=1)
103
+ idx_min = np.argmin(self.min_perceptron + X @ self.min_matrix.T, axis=1)
104
+
105
+ # create arrays to regroup the active indices using numpy, to then apply
106
+ # constraints all at once and use AST optimizations inside cvxpy
107
+ M_max = np.zeros((K, self.max_perceptron.size))
108
+ M_min = np.zeros((K, self.min_perceptron.size))
109
+ M_max[np.arange(K), idx_max] = 1
110
+ M_min[np.arange(K), idx_min] = 1
111
+
112
+ constraints = []
113
+ for label in [0, 1]:
114
+ mask = y == label
115
+ if not np.any(mask):
116
+ continue
117
+
118
+ X_ = X[mask]
119
+ K_ = X_.shape[0]
120
+
121
+ # start building the perceptron's outputs before the min/max
122
+ expr_max = X_ @ self._max_training_matrix.T
123
+ expr_min = X_ @ self._min_training_matrix.T
124
+
125
+ # add weights to every row of (X @ matrix.T) using np.ones to create
126
+ # a matrix safely for cvxpy's cpp backend
127
+ ones = np.ones((K_, 1))
128
+ expr_max += ones @ cp.reshape(
129
+ self._max_training_weights,
130
+ (1, self.max_perceptron.size),
131
+ order='C',
132
+ )
133
+ expr_min += ones @ cp.reshape(
134
+ self._min_training_weights,
135
+ (1, self.min_perceptron.size),
136
+ order='C',
137
+ )
138
+
139
+ active_max = cp.sum(cp.multiply(M_max[mask], expr_max), axis=1)
140
+ active_min = cp.sum(cp.multiply(M_min[mask], expr_min), axis=1)
141
+
142
+ if label == 0:
143
+ constraints.append(
144
+ self._slack[mask] - active_min
145
+ >= self.margin + cp.max(expr_max, axis=1)
146
+ )
147
+ else:
148
+ constraints.append(
149
+ self._slack[mask] + active_max
150
+ >= self.margin - cp.min(expr_min, axis=1)
151
+ )
152
+
153
+ return cp.Problem(cast(cp.Minimize, self._objective), constraints)
154
+
155
+ def get_problem_dccp(
156
+ self, X: np.ndarray, y: np.ndarray, cost_weights: np.ndarray
157
+ ) -> cp.Problem:
158
+ K = X.shape[0]
159
+ self.set_objective(K, cost_weights)
160
+
161
+ # Constraints: convex constraints are for data points in the first
162
+ # class, while concave ones are for points in the second class.
163
+
164
+ constraints = []
165
+ for label in [0, 1]:
166
+ mask = y == label
167
+ if not np.any(mask):
168
+ continue
169
+
170
+ X_ = X[mask]
171
+ K_ = X_.shape[0]
172
+
173
+ # start building the perceptron's outputs before the min/max
174
+ expr_max = X_ @ self._max_training_matrix.T
175
+ expr_min = X_ @ self._min_training_matrix.T
176
+
177
+ # add weights to every row of (X @ matrix.T) using np.ones to create
178
+ # a matrix safely for cvxpy's cpp backend
179
+ ones = np.ones((K_, 1))
180
+ expr_max += ones @ cp.reshape(
181
+ self._max_training_weights,
182
+ (1, self.max_perceptron.size),
183
+ order='C',
184
+ )
185
+ expr_min += ones @ cp.reshape(
186
+ self._min_training_weights,
187
+ (1, self.min_perceptron.size),
188
+ order='C',
189
+ )
190
+
191
+ if label == 0:
192
+ constraints.append(
193
+ self.margin + cp.max(expr_max, axis=1)
194
+ <= self._slack[mask] - cp.min(expr_min, axis=1)
195
+ )
196
+ else:
197
+ constraints.append(
198
+ self.margin - cp.min(expr_min, axis=1)
199
+ <= self._slack[mask] + cp.max(expr_max, axis=1)
200
+ )
201
+
202
+ return cp.Problem(cast(cp.Minimize, self._objective), constraints)
203
+
204
+ def get_cost(self, X: np.ndarray, y: np.ndarray) -> float:
205
+ expr_max = np.max(self.max_perceptron + X @ self.max_matrix.T, axis=1)
206
+ expr_min = np.min(self.min_perceptron + X @ self.min_matrix.T, axis=1)
207
+
208
+ # can use the expressions directly without needing to multiply by lambda
209
+ # since still in the training phase
210
+ cost = (expr_max + expr_min) * (1 - 2 * y)
211
+ return np.maximum(0, cost).sum()
212
+
213
+ def after_epoch(self) -> None:
214
+ # update the perceptrons weights from this epoch's results
215
+ if (
216
+ self._max_training_weights.value is None
217
+ or self._min_training_weights.value is None
218
+ ):
219
+ raise ValueError('CvxPy could not optimize a perceptron')
220
+
221
+ self.max_perceptron = self._max_training_weights.value
222
+ self.min_perceptron = self._min_training_weights.value
223
+
224
+ # extract the matrices
225
+ if (
226
+ self._max_training_matrix.value is None
227
+ or self._min_training_matrix.value is None
228
+ ):
229
+ raise ValueError('CvxPy could not optimize transformation matrices')
230
+ self.max_matrix = self._max_training_matrix.value
231
+ self.min_matrix = self._min_training_matrix.value
232
+
233
+ def at_training_end(self) -> None:
234
+ max_matrix_norm = np.linalg.norm(self.max_matrix)
235
+ min_matrix_norm = np.linalg.norm(self.min_matrix)
236
+
237
+ div = max_matrix_norm + min_matrix_norm
238
+ if np.isclose(div, 0):
239
+ raise ValueError('Transformation matrices are zero, cannot solve')
240
+
241
+ self.lambda_ = max_matrix_norm / div
242
+
243
+ # if lambda is close to a number that creates divisions by zero, it is
244
+ # safe to nullify the affected elements, that will not contribute anyway
245
+ if np.isclose(self.lambda_, 0):
246
+ self.max_matrix = np.zeros_like(self.max_matrix)
247
+ self.max_perceptron = np.zeros_like(self.max_perceptron)
248
+ else:
249
+ self.max_matrix /= self.lambda_
250
+ self.max_perceptron /= self.lambda_
251
+ if np.isclose(self.lambda_, 1):
252
+ self.min_matrix = np.zeros_like(self.max_matrix)
253
+ self.min_perceptron = np.zeros_like(self.min_perceptron)
254
+ else:
255
+ self.min_matrix /= 1 - self.lambda_
256
+ self.min_perceptron /= 1 - self.lambda_
257
+
258
+ def save_best(self) -> None:
259
+ self.saved = {
260
+ 'max_w': np.copy(self.max_perceptron),
261
+ 'min_w': np.copy(self.min_perceptron),
262
+ 'max_m': np.copy(self.max_matrix),
263
+ 'min_m': np.copy(self.min_matrix),
264
+ }
265
+
266
+ def rollback_to_best(self) -> None:
267
+ self.max_perceptron = self.saved['max_w']
268
+ self.min_perceptron = self.saved['min_w']
269
+ self.max_matrix = self.saved['max_m']
270
+ self.min_matrix = self.saved['min_m']
@@ -0,0 +1,285 @@
1
+ import numpy as np
2
+ import cvxpy as cp
3
+ from typing import Literal, cast
4
+ from warnings import warn
5
+
6
+ from .dccp_wrapper import DccpTrainer
7
+ from ..weighting import SampleWeighting
8
+ from ..stopping import StoppingMethod
9
+
10
+
11
+ class RDEPDccpTrainer(DccpTrainer):
12
+ """
13
+ r-DEP trainer.
14
+
15
+ During training, the final parameters are implicitly embedded into one
16
+ another.
17
+ Some of them (like lambda) are only extracted once the training ends.
18
+ This is not necessary for performance, but is there for readability and
19
+ interpretability reasons once the estimator is trained.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ lambda_bounds: tuple[float, float],
25
+ margin: float,
26
+ penalty: float,
27
+ validation_ratio: float,
28
+ weighting_method: SampleWeighting,
29
+ stopping_methods: list[StoppingMethod],
30
+ use_dccp_library: bool,
31
+ verbose: Literal[0, 1, 2],
32
+ random_state: np.random.RandomState,
33
+ ) -> None:
34
+ """
35
+ Initialize the r-DEP trainer.
36
+
37
+ param lambda_bounds: A pair of min and max values for lambda, to avoid
38
+ solvers (especially dccp) from failing to optimize.
39
+ To keep the constraints at the right convexity,
40
+ the bounds must be inside [0, 1].
41
+ param [others]: See base class.
42
+ """
43
+
44
+ super().__init__(
45
+ margin,
46
+ penalty,
47
+ validation_ratio,
48
+ weighting_method,
49
+ stopping_methods,
50
+ use_dccp_library,
51
+ verbose,
52
+ random_state,
53
+ )
54
+
55
+ if lambda_bounds[0] < 0 or lambda_bounds[1] > 1:
56
+ raise ValueError(
57
+ 'Invalid lambda_bounds, expected within [0, 1] '
58
+ f'got {list(lambda_bounds)}'
59
+ )
60
+ if (
61
+ lambda_bounds[0] == 0 or lambda_bounds[1] == 1
62
+ ) and use_dccp_library:
63
+ warn(
64
+ 'Warning: lambda_bounds may be inappropriate for dccp solver: '
65
+ f'{list(lambda_bounds)}'
66
+ )
67
+
68
+ self.lambda_bounds = lambda_bounds
69
+
70
+ def at_training_start(self, data_dim: int) -> None:
71
+ # similar to l-DEP, see corresponding files for implementation comments
72
+ self._objective = None
73
+
74
+ # Extracted parameters that will be populated during training but need
75
+ # initial values for linearization
76
+ self.max_perceptron = self.random_state.randn(data_dim)
77
+ self.min_perceptron = self.random_state.randn(data_dim)
78
+ self.lambda_ = 0.5
79
+
80
+ # to make the problem DCP, the training weights absorb the
81
+ # multiplications involving lambda_, they are explicited at the end of
82
+ # epochs
83
+ self._max_training_weights = cp.Variable(data_dim)
84
+ self._min_training_weights = cp.Variable(data_dim)
85
+ self._training_lambda = cp.Variable()
86
+
87
+ def set_objective(
88
+ self, X: np.ndarray, y: np.ndarray, cost_weights: np.ndarray
89
+ ) -> None:
90
+ # the objective and the slack variables do not change, cache them
91
+ if self._objective is not None:
92
+ return
93
+
94
+ n_features = len(np.unique(y))
95
+ if n_features != 2:
96
+ raise ValueError(
97
+ 'Detected degenerate dataset, perhaps after '
98
+ 'train/validation split. Expected 2 features, '
99
+ f'found only {n_features} feature(s)'
100
+ )
101
+
102
+ # figure out whether we should invert the perceptron's output, since
103
+ # the lower class should be lower in coordinates
104
+ labels, inv, counts = np.unique(
105
+ y, return_inverse=True, return_counts=True
106
+ )
107
+ sums = np.zeros((len(labels), X.shape[1]))
108
+ np.add.at(sums, inv, X)
109
+ centroids = sums / counts[:, np.newaxis]
110
+
111
+ # set the objective
112
+ self.invert_res = centroids[0].sum() > centroids[1].sum()
113
+
114
+ self._slack = cp.Variable(len(X))
115
+
116
+ value = cp.sum(cp.multiply(cp.pos(self._slack), cost_weights))
117
+ if self.penalty > 0:
118
+ value += self.penalty * (
119
+ cp.sum_squares(self._max_training_weights)
120
+ + cp.sum_squares(self._min_training_weights)
121
+ )
122
+
123
+ self._objective = cp.Minimize(value)
124
+
125
+ def get_problem_linearized(
126
+ self, X: np.ndarray, y: np.ndarray, cost_weights: np.ndarray
127
+ ) -> cp.Problem:
128
+ K = X.shape[0]
129
+ self.set_objective(X, y, cost_weights)
130
+
131
+ # Constraints: convex constraints are for data points in the first
132
+ # class, while concave ones are for points in the second class.
133
+ # Therefore, linearize things when needed to make the problem convex,
134
+ # sometimes using values from the previous epoch.
135
+
136
+ idx_max = np.argmax(self.lambda_ * X + self.max_perceptron, axis=1)
137
+ idx_min = np.argmin(
138
+ (1 - self.lambda_) * X + self.min_perceptron, axis=1
139
+ )
140
+
141
+ # create arrays to regroup the active indices using numpy, to then apply
142
+ # constraints all at once and use AST optimizations inside cvxpy
143
+ M_max = np.zeros((K, self.max_perceptron.size))
144
+ M_min = np.zeros((K, self.min_perceptron.size))
145
+ M_max[np.arange(K), idx_max] = 1
146
+ M_min[np.arange(K), idx_min] = 1
147
+
148
+ constraints = []
149
+ for label in [0, 1]:
150
+ mask = y == label
151
+ if not np.any(mask):
152
+ continue
153
+
154
+ X_ = X[mask]
155
+ K_ = X_.shape[0]
156
+
157
+ # add weights to every row of (X @ matrix.T) using np.ones to create
158
+ # a matrix safely for cvxpy's cpp backend
159
+ ones = np.ones((K_, 1))
160
+ expr_max = self._training_lambda * X_
161
+ expr_min = (1 - self._training_lambda) * X_
162
+ expr_max += ones @ cp.reshape(
163
+ self._max_training_weights,
164
+ (1, self.max_perceptron.size),
165
+ order='C',
166
+ )
167
+ expr_min += ones @ cp.reshape(
168
+ self._min_training_weights,
169
+ (1, self.min_perceptron.size),
170
+ order='C',
171
+ )
172
+
173
+ active_max = cp.sum(cp.multiply(M_max[mask], expr_max), axis=1)
174
+ active_min = cp.sum(cp.multiply(M_min[mask], expr_min), axis=1)
175
+
176
+ if (label == 0) ^ self.invert_res:
177
+ constraints.append(
178
+ self._slack[mask] - active_min
179
+ >= self.margin + cp.max(expr_max, axis=1)
180
+ )
181
+ else:
182
+ constraints.append(
183
+ self._slack[mask] + active_max
184
+ >= self.margin - cp.min(expr_min, axis=1)
185
+ )
186
+
187
+ # avoid lambda making some constraints convex when they should be
188
+ # concave and vice versa
189
+ constraints += [
190
+ self.lambda_bounds[0] <= self._training_lambda,
191
+ self._training_lambda <= self.lambda_bounds[1],
192
+ ]
193
+ return cp.Problem(cast(cp.Minimize, self._objective), constraints)
194
+
195
+ def get_problem_dccp(
196
+ self, X: np.ndarray, y: np.ndarray, cost_weights: np.ndarray
197
+ ) -> cp.Problem:
198
+ self.set_objective(X, y, cost_weights)
199
+
200
+ constraints = []
201
+ for label in [0, 1]:
202
+ mask = y == label
203
+ if not np.any(mask):
204
+ continue
205
+
206
+ X_ = X[mask]
207
+ K_ = X_.shape[0]
208
+
209
+ ones = np.ones((K_, 1))
210
+ expr_max = self._training_lambda * X_
211
+ expr_min = (1 - self._training_lambda) * X_
212
+ expr_max += ones @ cp.reshape(
213
+ self._max_training_weights,
214
+ (1, self.max_perceptron.size),
215
+ order='C',
216
+ )
217
+ expr_min += ones @ cp.reshape(
218
+ self._min_training_weights,
219
+ (1, self.min_perceptron.size),
220
+ order='C',
221
+ )
222
+
223
+ expr_max = cp.max(expr_max, axis=1)
224
+ expr_min = cp.min(expr_min, axis=1)
225
+
226
+ if (label == 0) ^ self.invert_res:
227
+ constraints.append(
228
+ self.margin + expr_max <= self._slack[mask] - expr_min
229
+ )
230
+ else:
231
+ constraints.append(
232
+ self.margin - expr_min <= self._slack[mask] + expr_max
233
+ )
234
+
235
+ constraints += [
236
+ self.lambda_bounds[0] <= self._training_lambda,
237
+ self._training_lambda <= self.lambda_bounds[1],
238
+ ]
239
+ return cp.Problem(cast(cp.Minimize, self._objective), constraints)
240
+
241
+ def get_cost(self, X: np.ndarray, y: np.ndarray) -> float:
242
+ expr_max = np.max(self.lambda_ * X + self.max_perceptron, axis=1)
243
+ expr_min = np.min((1 - self.lambda_) * X + self.min_perceptron, axis=1)
244
+
245
+ cost = (expr_max + expr_min) * (1 - 2 * y) * (1 - 2 * self.invert_res)
246
+
247
+ return np.maximum(0, cost).sum()
248
+
249
+ def after_epoch(self) -> None:
250
+ # update the perceptrons weights from this epoch's results
251
+ if (
252
+ self._max_training_weights.value is None
253
+ or self._min_training_weights.value is None
254
+ ):
255
+ raise ValueError('CvxPy could not optimize a perceptron')
256
+ if self._training_lambda.value is None:
257
+ raise ValueError('CvxPy could not optimize lambda')
258
+
259
+ self.max_perceptron = self._max_training_weights.value
260
+ self.min_perceptron = self._min_training_weights.value
261
+ self.lambda_ = self._training_lambda.value
262
+
263
+ # if lambda is close to a number that creates divisions by zero, it is
264
+ # safe to nullify the affected elements, that will not contribute anyway
265
+ if np.isclose(self.lambda_, 0):
266
+ self.max_perceptron = np.zeros_like(self.max_perceptron)
267
+ else:
268
+ self.max_perceptron /= self.lambda_
269
+
270
+ if np.isclose(self.lambda_, 1):
271
+ self.min_perceptron = np.zeros_like(self.min_perceptron)
272
+ else:
273
+ self.min_perceptron /= 1 - self.lambda_
274
+
275
+ def save_best(self) -> None:
276
+ self.saved = {
277
+ 'max_w': np.copy(self.max_perceptron),
278
+ 'min_w': np.copy(self.min_perceptron),
279
+ 'lambda': self.lambda_,
280
+ }
281
+
282
+ def rollback_to_best(self) -> None:
283
+ self.max_perceptron = self.saved['max_w']
284
+ self.min_perceptron = self.saved['min_w']
285
+ self.lambda_ = self.saved['lambda']