-
Notifications
You must be signed in to change notification settings - Fork 303
Expand file tree
/
Copy pathensemble_selection.py
More file actions
305 lines (260 loc) · 11.3 KB
/
Copy pathensemble_selection.py
File metadata and controls
305 lines (260 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
"""The title of the module description # noqa
* Describe at the beginning of the source code.
* Describe before the package imports
TODO:
* Add the following
References:
Title: Ensemble Selection from Libraries of Models
Authors: Rich Caruana et. al.
URL: https://www.cs.cornell.edu/~alexn/papers/shotgun.icml04.revised.rev2.pdf
* `A copy of self` --> check if it is really true
* Change `<variable>_` to `_<variable>`
* get_models_with_weights --> looks sort by descending of weights
* soft voting ==> explanation
References:
Title: Consensus Based Ensembles of Soft Clusterings
Authors: Kunal Punera and Joydeep Ghosh
URL: https://www.researchgate.net/profile/Joydeep-Ghosh-8/publication/221188694_Consensus_Based_Ensembles_of_Soft_Clusterings/links/02e7e521fe367e06c3000000/Consensus-Based-Ensembles-of-Soft-Clusterings.pdf
* _calculate_weights ==> what about np.sum(weights) > 1??
* Refactor _fit() and add the shape of predictions
"""
from collections import Counter
from typing import Any, Dict, List, Tuple, Union
import numpy as np
from autoPyTorch.ensemble.abstract_ensemble import AbstractEnsemble
from autoPyTorch.pipeline.base_pipeline import BasePipeline
from autoPyTorch.pipeline.components.training.metrics.base import autoPyTorchMetric
from autoPyTorch.pipeline.components.training.metrics.utils import calculate_loss
class EnsembleSelection(AbstractEnsemble):
def __init__(
self,
ensemble_size: int,
metric: autoPyTorchMetric,
task_type: int,
random_state: np.random.RandomState,
) -> None:
self.ensemble_size = ensemble_size
self.metric = metric
self.random_state = random_state
self.task_type = task_type
def __getstate__(self) -> Dict[str, Any]:
# Cannot serialize a metric if
# it is user defined.
# That is, if doing pickle dump
# the metric won't be the same as the
# one in __main__. we don't use the metric
# in the EnsembleSelection so this should
# be fine
self.metric = None # type: ignore
return self.__dict__
def fit(
self,
predictions: List[np.ndarray],
labels: np.ndarray,
identifiers: List[Tuple[int, int, float]],
) -> AbstractEnsemble:
"""
Builds a ensemble given the individual models out of fold predictions.
Fundamentally, defines a set of weights on how to perform a soft-voting
aggregation of the models in the given identifiers.
Args:
predictions (List[np.array]):
A list of individual model predictions of shape (n_datapoints, n_targets)
corresponding to the OutOfFold estimate of the ground truth
labels (np.ndarray):
The ground truth targets of shape (n_datapoints, n_targets)
identifiers: List[Tuple[int, int, float]]
A list of model identifiers, each with the form
(seed, number of run, budget)
Returns:
A copy of self
"""
self.ensemble_size = int(self.ensemble_size)
if self.ensemble_size < 1:
raise ValueError('Ensemble size cannot be less than one!')
self._fit(predictions, labels)
self._calculate_weights()
self.identifiers_ = identifiers
return self
def _fit(
self,
predictions: List[np.ndarray],
labels: np.ndarray,
) -> None:
"""
Fast version of Rich Caruana's ensemble selection method.
For more details, please check the paper
"Ensemble Selection from Library of Models" by R Caruana (2004)
Args:
predictions (List[np.array]):
A list of individual model predictions of shape (n_datapoints, n_targets)
corresponding to the OutOfFold estimate of the ground truth
identifiers (List[Tuple[int, int, float]]):
A list of model identifiers, each with the form
(seed, number of run, budget)
"""
self.num_input_models_ = len(predictions)
ensemble = [] # type: List[np.ndarray]
trajectory = []
order = []
ensemble_size = self.ensemble_size
weighted_ensemble_prediction = np.zeros(
predictions[0].shape,
dtype=np.float64,
)
fant_ensemble_prediction = np.zeros(
weighted_ensemble_prediction.shape,
dtype=np.float64,
)
for i in range(ensemble_size):
losses = np.zeros(
(len(predictions)),
dtype=np.float64,
)
s = len(ensemble)
if s > 0:
np.add(
weighted_ensemble_prediction,
ensemble[-1],
out=weighted_ensemble_prediction,
)
# Memory-efficient averaging!
for j, pred in enumerate(predictions):
# fant_ensemble_prediction is the prediction of the current ensemble
# and should be ([predictions[selected_prev_iterations] + predictions[j])/(s+1)
# We overwrite the contents of fant_ensemble_prediction
# directly with weighted_ensemble_prediction + new_prediction and then scale for avg
np.add(
weighted_ensemble_prediction,
pred,
out=fant_ensemble_prediction
)
np.multiply(
fant_ensemble_prediction,
(1. / float(s + 1)),
out=fant_ensemble_prediction
)
# Calculate loss is versatile and can return a dict of slosses
losses[j] = calculate_loss(
metrics=[self.metric],
target=labels,
prediction=fant_ensemble_prediction,
task_type=self.task_type,
)[self.metric.name]
all_best = np.argwhere(losses == np.nanmin(losses)).flatten()
best = self.random_state.choice(all_best)
ensemble.append(predictions[best])
trajectory.append(losses[best])
order.append(best)
# Handle special case
if len(predictions) == 1:
break
self.indices_: List[int] = order
self.trajectory_: List[float] = trajectory
self.train_loss_: float = trajectory[-1]
def _calculate_weights(self) -> None:
"""
Calculates the contribution each of the individual models
should have, in the final ensemble soft voting. It does so by
a frequency counting scheme. In particular, how many times a model
was used during hill climbing optimization.
"""
ensemble_members = Counter(self.indices_).most_common()
weights = np.zeros(
(self.num_input_models_,),
dtype=np.float64,
)
for ensemble_member in ensemble_members:
weight = float(ensemble_member[1]) / self.ensemble_size
weights[ensemble_member[0]] = weight
if np.sum(weights) < 1:
weights = weights / np.sum(weights)
self.weights_ = weights
def predict(self, predictions: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray:
"""
Given a list of predictions from the individual model, this method
aggregates the predictions using a soft voting scheme with the weights
found during training.
Args:
predictions (List[np.ndarray]):
A list of predictions from the individual base models.
Returns:
average (np.array): Soft voting predictions of ensemble models, using
the weights found during ensemble selection (self._weights)
"""
average = np.zeros_like(predictions[0], dtype=np.float64)
tmp_predictions = np.empty_like(predictions[0], dtype=np.float64)
# if predictions.shape[0] == len(self.weights_),
# predictions include those of zero-weight models.
if len(predictions) == len(self.weights_):
for pred, weight in zip(predictions, self.weights_):
np.multiply(pred, weight, out=tmp_predictions)
np.add(average, tmp_predictions, out=average)
# if prediction model.shape[0] == len(non_null_weights),
# predictions do not include those of zero-weight models.
elif len(predictions) == np.count_nonzero(self.weights_):
non_null_weights = [w for w in self.weights_ if w > 0]
for pred, weight in zip(predictions, non_null_weights):
np.multiply(pred, weight, out=tmp_predictions)
np.add(average, tmp_predictions, out=average)
# If none of the above applies, then something must have gone wrong.
else:
raise ValueError("The dimensions of ensemble predictions"
" and ensemble weights do not match!")
del tmp_predictions
return average
def __str__(self) -> str:
return 'Ensemble Selection:\n\tTrajectory: %s\n\tMembers: %s' \
'\n\tWeights: %s\n\tIdentifiers: %s' % \
(' '.join(['%d: %5f' % (idx, performance)
for idx, performance in enumerate(self.trajectory_)]),
self.indices_, self.weights_,
' '.join([str(identifier) for idx, identifier in
enumerate(self.identifiers_)
if self.weights_[idx] > 0]))
def get_models_with_weights(
self,
models: Dict[Any, BasePipeline]
) -> List[Tuple[float, BasePipeline]]:
"""
Handy function to tag the provided input models with a given weight.
Args:
models (List[Tuple[float, BasePipeline]]):
A dictionary that maps a model's name to it's actual python object.
Returns:
output (List[Tuple[float, BasePipeline]]):
each model with the related weight, sorted by ascending
performance. Notice that ensemble selection solves a minimization
problem.
"""
output = []
for i, weight in enumerate(self.weights_):
if weight > 0.0:
identifier = self.identifiers_[i]
model = models[identifier]
output.append((weight, model))
output.sort(reverse=True, key=lambda t: t[0])
return output
def get_selected_model_identifiers(self) -> List[Tuple[int, int, float]]:
"""
After training of ensemble selection, not all models will be used.
Some of them will have zero weight. This procedure filters this models
out.
Returns:
output (List[Tuple[int, int, float]]):
The models actually used by ensemble selection
"""
output = []
for i, weight in enumerate(self.weights_):
identifier = self.identifiers_[i]
if weight > 0.0:
output.append(identifier)
return output
def get_validation_performance(self) -> float:
"""
Returns the best optimization performance seen during hill climbing
Returns:
(float):
best ensemble training performance
"""
return self.trajectory_[-1]