condensite 0.0.1__tar.gz

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,12 @@
1
+ Copyright (c) 2025-, condensité Developers.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ * Neither the name of condensité nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: condensite
3
+ Version: 0.0.1
4
+ Summary: Transforming conditional density estimation into a single nonparametric regression task.
5
+ Author-email: Alexander Reisach <alexander.reisach@math.cnrs.fr>
6
+ License: BSD 3-Clause License
7
+ Keywords: conditional density estimation,machine learning,statistics
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Intended Audience :: Science/Research
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch
17
+ Requires-Dist: numpy
18
+ Requires-Dist: scikit-learn
19
+ Requires-Dist: lightgbm
20
+ Dynamic: license-file
21
+
22
+ # condensité
23
+
24
+ The approach behind `condensité` enables transforming conditional density estimation into a single nonparametric regression task. Our package implements this approach and provides a way of turning regressors from common libraries such as `sklearn` or `torch` into conditional density estimators. See below for a complete example. See `example.py` for further examples using the reference implementations in `src/example_models`.
25
+
26
+ ```python
27
+ import torch
28
+ import condensite as cde
29
+ from sklearn.ensemble import HistGradientBoostingRegressor
30
+
31
+ # create a condensité predictor based on sklearn's HistGradientBoostingRegressor
32
+ class TreePredictor(cde.CondensitePredictor):
33
+ def __init__(self, params):
34
+ self.tree = HistGradientBoostingRegressor(**params)
35
+
36
+ def device(self):
37
+ return "cpu"
38
+
39
+ def to(self, device):
40
+ return self
41
+
42
+ def forward(self, x):
43
+ return self.tree.predict(x)
44
+
45
+ def predict(self, x):
46
+ return torch.tensor(self.forward(x), dtype=torch.float32)
47
+
48
+ def fit(self, dataset: cde.RepeatDataset):
49
+ # create data loader (c.f. cde.RepeatDataset and cde.concat_collate)
50
+ train_DL = torch.utils.data.DataLoader(dataset=dataset,
51
+ batch_size=dataset.n*dataset.M,
52
+ shuffle=True,
53
+ collate_fn=cde.concat_collate)
54
+ xtorch, ytorch = next(iter(train_DL)) # single batch of all data
55
+ self.tree.fit(xtorch.numpy(), ytorch.numpy())
56
+
57
+ # instantiate and wrap using Condensite
58
+ tree_predictor = TreePredictor(params={
59
+ 'max_bins': 40,
60
+ 'max_leaf_nodes': 100,
61
+ 'l2_regularization': 0.1,
62
+ })
63
+ sklearn_model = cde.Condensite(tree_predictor, h=0.01, M=100, name='sklearn_model')
64
+
65
+ # generate some data and fit
66
+ def sample_data(n_obs=1000, n_features=10):
67
+ x = torch.empty((n_obs, n_features)).normal_(0,1)
68
+ x1 = x[:, 0]
69
+ std = torch.sqrt(0.25 + x1**2)
70
+ noise = torch.distributions.Normal(torch.zeros_like(x1), std).sample()
71
+ y = x1 + noise
72
+ return x, y
73
+
74
+ # fit conditional density estimator
75
+ x_train, y_train = sample_data()
76
+ sklearn_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100)
77
+
78
+ # evaluate fit out-of-sample
79
+ x_test, y_test = sample_data()
80
+ test_ISE = cde.utils.ISE(sklearn_model, x_test, y_test, n_grid=100).item()
81
+ print(f'{sklearn_model.name}:\t {test_ISE=:.4f}')
82
+ ```
83
+
84
+ If you find our algorithms useful please consider citing
85
+ ```
86
+ ```
@@ -0,0 +1,65 @@
1
+ # condensité
2
+
3
+ The approach behind `condensité` enables transforming conditional density estimation into a single nonparametric regression task. Our package implements this approach and provides a way of turning regressors from common libraries such as `sklearn` or `torch` into conditional density estimators. See below for a complete example. See `example.py` for further examples using the reference implementations in `src/example_models`.
4
+
5
+ ```python
6
+ import torch
7
+ import condensite as cde
8
+ from sklearn.ensemble import HistGradientBoostingRegressor
9
+
10
+ # create a condensité predictor based on sklearn's HistGradientBoostingRegressor
11
+ class TreePredictor(cde.CondensitePredictor):
12
+ def __init__(self, params):
13
+ self.tree = HistGradientBoostingRegressor(**params)
14
+
15
+ def device(self):
16
+ return "cpu"
17
+
18
+ def to(self, device):
19
+ return self
20
+
21
+ def forward(self, x):
22
+ return self.tree.predict(x)
23
+
24
+ def predict(self, x):
25
+ return torch.tensor(self.forward(x), dtype=torch.float32)
26
+
27
+ def fit(self, dataset: cde.RepeatDataset):
28
+ # create data loader (c.f. cde.RepeatDataset and cde.concat_collate)
29
+ train_DL = torch.utils.data.DataLoader(dataset=dataset,
30
+ batch_size=dataset.n*dataset.M,
31
+ shuffle=True,
32
+ collate_fn=cde.concat_collate)
33
+ xtorch, ytorch = next(iter(train_DL)) # single batch of all data
34
+ self.tree.fit(xtorch.numpy(), ytorch.numpy())
35
+
36
+ # instantiate and wrap using Condensite
37
+ tree_predictor = TreePredictor(params={
38
+ 'max_bins': 40,
39
+ 'max_leaf_nodes': 100,
40
+ 'l2_regularization': 0.1,
41
+ })
42
+ sklearn_model = cde.Condensite(tree_predictor, h=0.01, M=100, name='sklearn_model')
43
+
44
+ # generate some data and fit
45
+ def sample_data(n_obs=1000, n_features=10):
46
+ x = torch.empty((n_obs, n_features)).normal_(0,1)
47
+ x1 = x[:, 0]
48
+ std = torch.sqrt(0.25 + x1**2)
49
+ noise = torch.distributions.Normal(torch.zeros_like(x1), std).sample()
50
+ y = x1 + noise
51
+ return x, y
52
+
53
+ # fit conditional density estimator
54
+ x_train, y_train = sample_data()
55
+ sklearn_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100)
56
+
57
+ # evaluate fit out-of-sample
58
+ x_test, y_test = sample_data()
59
+ test_ISE = cde.utils.ISE(sklearn_model, x_test, y_test, n_grid=100).item()
60
+ print(f'{sklearn_model.name}:\t {test_ISE=:.4f}')
61
+ ```
62
+
63
+ If you find our algorithms useful please consider citing
64
+ ```
65
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools.packages.find]
6
+ where = ["src"]
7
+
8
+ [project]
9
+ name = "condensite"
10
+ description = "Transforming conditional density estimation into a single nonparametric regression task."
11
+ authors = [
12
+ {name = "Alexander Reisach", email = "alexander.reisach@math.cnrs.fr"}
13
+ ]
14
+ license = {text = "BSD 3-Clause License"}
15
+ keywords = [
16
+ "conditional density estimation",
17
+ "machine learning",
18
+ "statistics",
19
+ ]
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Scientific/Engineering",
23
+ "Topic :: Scientific/Engineering :: Mathematics",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "License :: OSI Approved :: BSD License",
26
+ "Intended Audience :: Science/Research",
27
+ ]
28
+ dependencies = [
29
+ "torch",
30
+ "numpy",
31
+ "scikit-learn",
32
+ "lightgbm",
33
+ ]
34
+ readme = "README.md"
35
+ version = "0.0.1"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .core import Condensite, CondensitePredictor, RepeatDataset, concat_collate
2
+ __all__ = ["Condensite",
3
+ "CondensitePredictor",
4
+ "RepeatDataset",
5
+ "concat_collate"]
@@ -0,0 +1,344 @@
1
+ import os
2
+ import pickle
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.distributions as dist
6
+ from copy import deepcopy
7
+ from abc import ABC, abstractmethod
8
+ import torch
9
+ from torch.utils.data import DataLoader
10
+ from . import utils as ut
11
+ import warnings
12
+ import logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class RepeatDataset(torch.utils.data.Dataset):
17
+ """
18
+ Custom dataset for the auxiliary condensite samples (keeps RAM usage in check).
19
+ """
20
+ def __init__(self, x, y_aux, targets, M):
21
+ """
22
+ Args:
23
+ x (torch.Tensor): the features to be repeated
24
+ y_aux (torch.Tensor): the auxiliary labels
25
+ targets (torch.Tensor): target values
26
+ M (int): number of repetitions
27
+ """
28
+ self.n = len(x)
29
+ # assert dimensions
30
+ assert len(y_aux) == self.n*M
31
+ assert len(targets) == self.n*M
32
+ # parameters
33
+ self.x = x # (Nxd)
34
+ self.y_aux = y_aux # (NM)
35
+ self.targets = targets # (NM)
36
+ self.M = M # scalar
37
+ # precompute indices
38
+ self.j_map = torch.arange(self.n * self.M) // self.M
39
+
40
+ def __len__(self):
41
+ return self.n * self.M
42
+
43
+ def __getitem__(self, i):
44
+ return self.x[self.j_map[i]], self.y_aux[i], self.targets[i]
45
+
46
+
47
+ def concat_collate(batch):
48
+ """
49
+ To be used in a DataLoader, as collate_fn for RepeatDataset.
50
+ Args:
51
+ batch (torch.Tensor): batch of features, auxiliary labels, and targets.
52
+ """
53
+ x, y_aux, targets = zip(*batch) # tuples of tensors
54
+ x = torch.stack(x) # shape: (B, d)
55
+ y_aux = torch.stack(y_aux).reshape(-1, 1) # shape: (B, 1)
56
+ features = torch.cat([x, y_aux], dim=1) # shape: (B, d+1)
57
+ targets = torch.stack(targets)
58
+ return features, targets
59
+
60
+
61
+ class CondensitePredictor(ABC):
62
+ """ Defines the interface predictors have to implement. """
63
+ def __init__(self, device):
64
+ pass
65
+
66
+ @abstractmethod
67
+ def device(self):
68
+ """
69
+ Return device (would typically be "cpu" or "cuda").
70
+ """
71
+ return self._device
72
+
73
+ @abstractmethod
74
+ def to(self, device):
75
+ """
76
+ Port to device if applicable. Otherwise, simply return self.
77
+ Args:
78
+ device (str): output of self.device()
79
+ """
80
+ raise NotImplementedError
81
+
82
+ @abstractmethod
83
+ def forward(self, features: torch.Tensor):
84
+ """
85
+ Transform features into targets.
86
+ Args:
87
+ features (torch.Tensor): feature values
88
+ """
89
+ raise NotImplementedError
90
+
91
+ @abstractmethod
92
+ def predict(self, features: torch.Tensor) -> torch.Tensor:
93
+ """
94
+ Predict from given features.
95
+ Args:
96
+ features (torch.Tensor): feature values
97
+ """
98
+ raise NotImplementedError
99
+
100
+ @abstractmethod
101
+ def fit(self, dataset: RepeatDataset) -> torch.Tensor:
102
+ """
103
+ Fit the predictor to data.
104
+ Args:
105
+ dataset (RepeatDataset): virtual dataset of features and targets
106
+ Returns:
107
+ loss (torch.Tensor)
108
+ """
109
+ raise NotImplementedError
110
+
111
+
112
+ class Condensite:
113
+ """
114
+ Fits CondensitePredictor objects to training data from a conditional density and provides a prediction function for the fitted object.
115
+ """
116
+
117
+ def __init__(self, predictor, h, M,
118
+ name='condensite',
119
+ prediction_batch_size=None):
120
+ """
121
+ Args:
122
+ predictor (CondensitePredictor): conditional density estimator.
123
+ h (int): sharpness; for transformation into regression task.
124
+ M (int): number of auxiliary samples.
125
+ name (str): model name
126
+ prediction_batch_size (int): batch size when predicting (to limit RAM usage)
127
+ """
128
+ self.name = name
129
+ self.device = predictor.device()
130
+ self.predictor = predictor.to(self.device)
131
+ self.h = h
132
+ self.M = M
133
+ # parameters for standardization; crucial for training stability
134
+ self.x_mean = None
135
+ self.x_scale = None
136
+ self.y_min = None
137
+ self.y_max = None
138
+ self.y_aux_mean = None
139
+ self.y_aux_scale = None
140
+ self.t_mean = None
141
+ self.t_scale = None
142
+ self.prediction_batch_size=prediction_batch_size
143
+
144
+ def fit(self, x, y, train_frac, n_grid_ISE, verbose=0, epochs=None, patience=None):
145
+ """
146
+ Train the predictor on the data.
147
+ Args:
148
+ x (tensor): (n x d)
149
+ y (tensor): (n x 1)
150
+ train_frac (float): fraction fo data to use for training, rest is used for cross-validation.
151
+ n_grid_ISE: number of grid points for ISE in cross-validation.
152
+ verbose (int): verbosity of training.
153
+ epochs (int or None): train epochs, None if irrelevant
154
+ patience (int or None): early stopping count, None if irrelevant
155
+ """
156
+ # check if epochs relevant
157
+ if epochs is None:
158
+ epochs = 1
159
+ # train-CV split index
160
+ x_train, x_val, y_train, y_val = ut.train_test_split(x, y, train_frac)
161
+ # sample auxiliary features
162
+ y_aux_train = self.sample_y_aux(y_train)
163
+ # feature-scaling parameters
164
+ self.set_sample_scaling_params(x_train, y_train, y_aux_train)
165
+ # min-max scale y
166
+ y_01 = (y_train-self.y_min)/(self.y_max-self.y_min)
167
+ # training targets
168
+ targets = self.compute_targets(y_01, y_aux_train)
169
+ # label-scaling parameters
170
+ self.set_target_scaling_params(targets)
171
+ # standardize
172
+ x_train = self.standardize(x_train, self.x_mean, self.x_scale)
173
+ y_aux_train = self.standardize(y_aux_train, self.y_aux_mean, self.y_aux_scale)
174
+ targets = self.standardize(targets, self.t_mean, self.t_scale)
175
+
176
+ # create the condensite RepeatDataset for training
177
+ train_dataset = RepeatDataset(x=x_train,
178
+ y_aux=y_aux_train,
179
+ targets=targets,
180
+ M=self.M)
181
+
182
+ # training
183
+ best_val_ise = float('inf')
184
+ best_epoch_predictor = None
185
+ for epoch in range(epochs):
186
+ # call the predictor's fit method
187
+ mse = self.predictor.fit(train_dataset)
188
+ # compute ISE on validation set
189
+ val_ise = ut.ISE(self, x_val, y_val, n_grid_ISE)
190
+ if verbose > 0:
191
+ if epochs > 1:
192
+ logger.info(f"Epoch {epoch+1}/{epochs}, Train mse: {mse:.4f}, Validation ISE: {val_ise:.4f}")
193
+ else:
194
+ logger.info(f"Train mse: {mse:.4f}, Validation ISE: {val_ise:.4f}")
195
+ # Early stopping
196
+ if val_ise < best_val_ise:
197
+ best_val_ise = val_ise
198
+ best_epoch_predictor = deepcopy(self.predictor)
199
+ epochs_without_improvement = 0
200
+ else:
201
+ epochs_without_improvement += 1
202
+ if epochs_without_improvement >= patience:
203
+ if verbose>0:
204
+ logger.info(f'Early stopping at epoch {epoch + 1}')
205
+ break
206
+
207
+ # select best network
208
+ self.predictor = best_epoch_predictor
209
+ self.ise = best_val_ise
210
+
211
+ # return run summary
212
+ return {'h': self.h,
213
+ 'M': self.M,
214
+ 'epochs': epoch,
215
+ 'ISE': best_val_ise}
216
+
217
+ def set_sample_scaling_params(self, x_train, y_train, y_aux_train):
218
+ """
219
+ Store values for scaling the observations and auxiliary samples.
220
+ Args:
221
+ x_train (torch.Tensor): training set feature values
222
+ y_train (torch.Tensor): training set labels
223
+ y_aux_train (torch.Tensor): auxiliary labels
224
+ """
225
+ # standard scaling for x
226
+ self.x_mean = x_train.mean(dim=0).to(self.device)
227
+ self.x_scale = x_train.std(dim=0).to(self.device)
228
+ # for min-max scaling of y
229
+ self.y_min = y_train.min()
230
+ self.y_max = y_train.max()
231
+ # standard scaling for auxiliary samples y_aux
232
+ self.y_aux_mean = y_aux_train.mean(dim=0).to(self.device)
233
+ self.y_aux_scale = y_aux_train.std(dim=0).to(self.device)
234
+
235
+ def set_target_scaling_params(self, t_train):
236
+ """
237
+ Store mean and standard deviation of targets for scaling.
238
+ Args:
239
+ t_train (torch.Tensor): targets derived from auxiliary samples
240
+ """
241
+ self.t_mean = t_train.mean(dim=0).to(self.device)
242
+ self.t_scale = t_train.std(dim=0).to(self.device)
243
+
244
+ def standardize(self, x, mean, scale):
245
+ """
246
+ Standardize features.
247
+ Args:
248
+ x: features,
249
+ mean (torch.Tensor): mean values for standardization
250
+ scale (torch.Tensor): standard deviations for standardization
251
+ """
252
+ if not x.is_cuda:
253
+ mean, scale = mean.cpu(), scale.cpu()
254
+ return (x - mean) / (scale + 1e-8)
255
+
256
+ def predict(self, x, n_grid):
257
+ """
258
+ Predict cdes for x at a grid across the range of y seen at training; batching to limit RAM usage.
259
+ Args:
260
+ x (torch.Tensor): features
261
+ n_grid (int): number of grid points for prediction
262
+ """
263
+ # create grid on [0,1] scale using RepeatDataset
264
+ x = self.standardize(x, self.x_mean, self.x_scale)
265
+ y_points_01 = torch.linspace(0, 1, n_grid).view(-1,1)
266
+ grid_y = y_points_01.repeat(len(x), 1)
267
+ grid_y = self.standardize(grid_y, self.y_aux_mean, self.y_aux_scale)
268
+ dataset = RepeatDataset(
269
+ x=x,
270
+ y_aux=grid_y,
271
+ targets=torch.zeros_like(grid_y), # not actually needed here
272
+ M=n_grid
273
+ )
274
+
275
+ # let predictor predict and scale up
276
+ if self.prediction_batch_size is None:
277
+ batch_size = len(grid_y) # single batch
278
+ else:
279
+ batch_size = self.prediction_batch_size
280
+ predict_DL = DataLoader(
281
+ dataset,
282
+ batch_size,
283
+ shuffle=False,
284
+ collate_fn=concat_collate
285
+ )
286
+ cdes = torch.empty(len(x)*n_grid, device="cpu")
287
+ start = 0
288
+ for feature_batch, _ in predict_DL:
289
+ feature_batch = feature_batch.to(self.device)
290
+ batch_preds = self.predictor.predict(feature_batch)
291
+ batch_preds = batch_preds*self.t_scale+self.t_mean
292
+ cdes[start:(start+len(batch_preds))] = batch_preds.flatten().cpu()
293
+ start += len(batch_preds)
294
+ cdes = cdes.view(len(x), n_grid)
295
+
296
+ # min-max upscaling; we want a density on the original scale
297
+ y_points = y_points_01 * (self.y_max - self.y_min) + self.y_min
298
+ cdes = self.make_density(cdes, y_points)
299
+ return cdes, y_points
300
+
301
+ def make_density(self, cdes, y):
302
+ """
303
+ Make sure estimated densities are positive and add up to 1 under numerical integration.
304
+ Args:
305
+ cdes (torch.Tensor): (n x n_grid) density estimates
306
+ """
307
+ # force positive
308
+ cdes = torch.where(cdes<0, 0, cdes)
309
+ area = torch.trapz(cdes, y.flatten(), dim=1).reshape(-1, 1)
310
+ # if we predict zero density everywhere, assume a uniform distribution
311
+ empty_pred = torch.argwhere(area.ravel() == 0)
312
+ cdes[empty_pred, :] = 1/cdes.shape[1]
313
+ area[empty_pred] = 1
314
+ # normalize density
315
+ cdes = cdes / area
316
+ return cdes
317
+
318
+ def sample_y_aux(self, y):
319
+ """
320
+ Sample auxiliary labels.
321
+ Args:
322
+ y (torch.Tensor): the (n) y values
323
+ """
324
+ return torch.empty(len(y)*self.M).uniform_(0, 1).view(-1,1)
325
+
326
+ def compute_targets(self, y_01, y_aux):
327
+ """
328
+ Compute targets via $K_h(Y_i-Y_{im})$.
329
+ Args:
330
+ y_01 (torch.Tensor): observed y rescaled to [0,1]
331
+ y_aux (torch.Tensor): auxiliary labels for target construction
332
+ """
333
+ # prepare for grid
334
+ y_bar = y_01.repeat_interleave(self.M, dim=0).view(-1,1)
335
+ # compute auxiliary densities
336
+ Kh = dist.MultivariateNormal(loc=torch.zeros(1),
337
+ covariance_matrix=torch.eye(1) * self.h**2)
338
+ targets = torch.exp(Kh.log_prob(y_bar - y_aux))
339
+ return targets
340
+
341
+ def save(self, path):
342
+ """ Save the model. """
343
+ with open(os.path.join(path, f"{self.name}.pkl"), "wb") as f:
344
+ pickle.dump(self, f)
@@ -0,0 +1,49 @@
1
+ import torch
2
+
3
+
4
+ def ISE_from_pred(cdes_grid, y_points, y):
5
+ """
6
+ Separate this from the predict part since sometimes we'd like to load saved predictions.
7
+ Args:
8
+ cdes_grid (torch.Tensor): conditional density estimates over a grid
9
+ y_points (torch.Tensor): the grid points
10
+ y (torch.Tensor): the labels
11
+ """
12
+ ## ISE term 1: volume under squared curve for each X point
13
+ a2 = torch.mean(torch.trapz(cdes_grid**2, y_points.flatten(), dim=1))
14
+ ## ISE term 2: density height at the actual data points.
15
+ # choose the grid value closest to a real value as nearest neighbor
16
+ nn = torch.argmin(torch.abs(y.view(-1,1)-y_points.view(1,-1)), axis=1)
17
+ cdes = cdes_grid[range(len(y)), nn]
18
+ ab = torch.mean(cdes)
19
+ # combine first two terms
20
+ return a2 - 2*ab
21
+
22
+
23
+ def ISE(model, x, y, n_grid):
24
+ """
25
+ ISE approximation up to a constant; on a grid of length n_grid.
26
+ Args:
27
+ model (object): one of our CDE models
28
+ x (torch.Tensor): (n x d)
29
+ y (torch.Tensor): (n x 1)
30
+ n_grid (int): number of grid points for evaluation
31
+ """
32
+ cdes_grid, y_points = model.predict(x, n_grid)
33
+ return ISE_from_pred(cdes_grid, y_points, y)
34
+
35
+
36
+ def train_test_split(x, y, train_frac):
37
+ """
38
+ Akin to sklearn.model_selection.train_test_split.
39
+ Args:
40
+ x (torch.Tensor): features
41
+ y (torch.Tensor): labels
42
+ train_frac (float): fraction to be used for training
43
+ """
44
+ split_idx = int(train_frac*len(y))
45
+ mask = torch.randperm(len(y))
46
+ train_mask, test_mask = mask[:split_idx], mask[split_idx:]
47
+ x_train, x_test = x[train_mask], x[test_mask]
48
+ y_train, y_test = y[train_mask], y[test_mask]
49
+ return x_train, x_test, y_train, y_test
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: condensite
3
+ Version: 0.0.1
4
+ Summary: Transforming conditional density estimation into a single nonparametric regression task.
5
+ Author-email: Alexander Reisach <alexander.reisach@math.cnrs.fr>
6
+ License: BSD 3-Clause License
7
+ Keywords: conditional density estimation,machine learning,statistics
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Intended Audience :: Science/Research
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch
17
+ Requires-Dist: numpy
18
+ Requires-Dist: scikit-learn
19
+ Requires-Dist: lightgbm
20
+ Dynamic: license-file
21
+
22
+ # condensité
23
+
24
+ The approach behind `condensité` enables transforming conditional density estimation into a single nonparametric regression task. Our package implements this approach and provides a way of turning regressors from common libraries such as `sklearn` or `torch` into conditional density estimators. See below for a complete example. See `example.py` for further examples using the reference implementations in `src/example_models`.
25
+
26
+ ```python
27
+ import torch
28
+ import condensite as cde
29
+ from sklearn.ensemble import HistGradientBoostingRegressor
30
+
31
+ # create a condensité predictor based on sklearn's HistGradientBoostingRegressor
32
+ class TreePredictor(cde.CondensitePredictor):
33
+ def __init__(self, params):
34
+ self.tree = HistGradientBoostingRegressor(**params)
35
+
36
+ def device(self):
37
+ return "cpu"
38
+
39
+ def to(self, device):
40
+ return self
41
+
42
+ def forward(self, x):
43
+ return self.tree.predict(x)
44
+
45
+ def predict(self, x):
46
+ return torch.tensor(self.forward(x), dtype=torch.float32)
47
+
48
+ def fit(self, dataset: cde.RepeatDataset):
49
+ # create data loader (c.f. cde.RepeatDataset and cde.concat_collate)
50
+ train_DL = torch.utils.data.DataLoader(dataset=dataset,
51
+ batch_size=dataset.n*dataset.M,
52
+ shuffle=True,
53
+ collate_fn=cde.concat_collate)
54
+ xtorch, ytorch = next(iter(train_DL)) # single batch of all data
55
+ self.tree.fit(xtorch.numpy(), ytorch.numpy())
56
+
57
+ # instantiate and wrap using Condensite
58
+ tree_predictor = TreePredictor(params={
59
+ 'max_bins': 40,
60
+ 'max_leaf_nodes': 100,
61
+ 'l2_regularization': 0.1,
62
+ })
63
+ sklearn_model = cde.Condensite(tree_predictor, h=0.01, M=100, name='sklearn_model')
64
+
65
+ # generate some data and fit
66
+ def sample_data(n_obs=1000, n_features=10):
67
+ x = torch.empty((n_obs, n_features)).normal_(0,1)
68
+ x1 = x[:, 0]
69
+ std = torch.sqrt(0.25 + x1**2)
70
+ noise = torch.distributions.Normal(torch.zeros_like(x1), std).sample()
71
+ y = x1 + noise
72
+ return x, y
73
+
74
+ # fit conditional density estimator
75
+ x_train, y_train = sample_data()
76
+ sklearn_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100)
77
+
78
+ # evaluate fit out-of-sample
79
+ x_test, y_test = sample_data()
80
+ test_ISE = cde.utils.ISE(sklearn_model, x_test, y_test, n_grid=100).item()
81
+ print(f'{sklearn_model.name}:\t {test_ISE=:.4f}')
82
+ ```
83
+
84
+ If you find our algorithms useful please consider citing
85
+ ```
86
+ ```
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/condensite/__init__.py
5
+ src/condensite/core.py
6
+ src/condensite/utils.py
7
+ src/condensite.egg-info/PKG-INFO
8
+ src/condensite.egg-info/SOURCES.txt
9
+ src/condensite.egg-info/dependency_links.txt
10
+ src/condensite.egg-info/requires.txt
11
+ src/condensite.egg-info/top_level.txt
12
+ src/examples/example.py
13
+ src/examples/example_models/LGBMPredictor.py
14
+ src/examples/example_models/sklearnPredictor.py
15
+ src/examples/example_models/torchPredictor.py
16
+ tests/test_core.py
@@ -0,0 +1,4 @@
1
+ torch
2
+ numpy
3
+ scikit-learn
4
+ lightgbm
@@ -0,0 +1,2 @@
1
+ condensite
2
+ examples
@@ -0,0 +1,61 @@
1
+
2
+ import condensite as cde
3
+ import torch
4
+ import torch.distributions as dist
5
+ from example_models.sklearnPredictor import TreePredictor
6
+ from example_models.LGBMPredictor import LGBMPredictor
7
+ from example_models.torchPredictor import NNPredictor
8
+ import logging
9
+ logging.basicConfig(level=logging.INFO)
10
+
11
+ # create some example data
12
+ def sample_data(n_obs=1000, n_features=10):
13
+ x = torch.empty((n_obs, n_features)).normal_(0,1)
14
+ x1 = x[:, 0]
15
+ std = torch.sqrt(0.25 + x1**2)
16
+ noise = torch.distributions.Normal(torch.zeros_like(x1), std).sample()
17
+ y = x1 + noise
18
+ return x, y
19
+
20
+ x_train, y_train = sample_data()
21
+
22
+ # ------------------------------------------------------------------------------
23
+
24
+ # model 1) condensité with a sklearn predictor
25
+ tree_predictor = TreePredictor(params={
26
+ 'max_bins': 40,
27
+ 'max_leaf_nodes': 100,
28
+ 'l2_regularization': 0.1,
29
+ })
30
+ sklearn_model = cde.Condensite(tree_predictor, h=0.01, M=100, name='sklearn_model')
31
+ sklearn_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100)
32
+
33
+ # model 2) condensité with a lgbm predictor
34
+ lgbm_predictor = LGBMPredictor(params={
35
+ 'objective': 'regression',
36
+ 'metric': 'mse',
37
+ 'min_data_in_leaf': 50,
38
+ 'max_bin': 40,
39
+ 'num_leaves': 40,
40
+ })
41
+ lgbm_model = cde.Condensite(lgbm_predictor, h=0.01, M=100, name='lgbm_model')
42
+ lgbm_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100)
43
+
44
+ # model 3) condensité with a torch predictor
45
+ torch_predictor = NNPredictor(
46
+ n_in=x_train.shape[1]+1, # note extra feature (all methods, here explicit)
47
+ n_hidden=20,
48
+ batch_size=512,
49
+ lr=1e-3,
50
+ weight_decay=1e-4
51
+ )
52
+ torch_model = cde.Condensite(torch_predictor, h=0.01, M=100, name='torch_model')
53
+ torch_model.fit(x_train, y_train, train_frac=0.8, n_grid_ISE=100, epochs=20, patience=5, verbose=1)
54
+
55
+ # ------------------------------------------------------------------------------
56
+
57
+ # compare their out-of-sample ISE
58
+ x_test, y_test = sample_data()
59
+ for model in [sklearn_model, lgbm_model, torch_model]:
60
+ test_ISE = cde.utils.ISE(model, x_test, y_test, n_grid=100).item()
61
+ print(f'{model.name}:\t {test_ISE=:.4f}')
@@ -0,0 +1,35 @@
1
+ import torch
2
+ from torch.utils.data import DataLoader
3
+ import condensite as cde
4
+ import lightgbm as lgb
5
+ import warnings
6
+ warnings.filterwarnings("ignore", message="Converting data to scipy sparse matrix.")
7
+
8
+
9
+ class LGBMPredictor(cde.CondensitePredictor):
10
+ def __init__(self, params):
11
+ self.tree = None
12
+ self.params = params
13
+
14
+ def device(self):
15
+ return "cpu"
16
+
17
+ def to(self, device):
18
+ return self
19
+
20
+ def forward(self, x):
21
+ return self.tree.predict(x)
22
+
23
+ def predict(self, x):
24
+ return torch.tensor(self.forward(x), dtype=torch.float32)
25
+
26
+ def fit(self, dataset):
27
+ # create dataset (c.f. RepeatDataset and concat_collate)
28
+ train_DL = torch.utils.data.DataLoader(dataset=dataset,
29
+ batch_size=dataset.n*dataset.M,
30
+ shuffle=True,
31
+ collate_fn=cde.concat_collate)
32
+ xtorch, ytorch = next(iter(train_DL)) # single batch of all data
33
+ x, y = xtorch.numpy(), ytorch.numpy()
34
+ train_data = lgb.Dataset(x, label=y)
35
+ self.tree = lgb.train(self.params, train_data)
@@ -0,0 +1,29 @@
1
+ import torch
2
+ import condensite as cde
3
+ from sklearn.ensemble import HistGradientBoostingRegressor
4
+
5
+
6
+ class TreePredictor(cde.CondensitePredictor):
7
+ def __init__(self, params):
8
+ self.tree = HistGradientBoostingRegressor(**params)
9
+
10
+ def device(self):
11
+ return "cpu"
12
+
13
+ def to(self, device):
14
+ return self
15
+
16
+ def forward(self, x):
17
+ return self.tree.predict(x)
18
+
19
+ def predict(self, x):
20
+ return torch.tensor(self.forward(x), dtype=torch.float32)
21
+
22
+ def fit(self, dataset: cde.RepeatDataset):
23
+ # create data loader (c.f. cde.RepeatDataset and cde.concat_collate)
24
+ train_DL = torch.utils.data.DataLoader(dataset=dataset,
25
+ batch_size=dataset.n*dataset.M,
26
+ shuffle=True,
27
+ collate_fn=cde.concat_collate)
28
+ xtorch, ytorch = next(iter(train_DL)) # single batch of all data
29
+ self.tree.fit(xtorch.numpy(), ytorch.numpy())
@@ -0,0 +1,96 @@
1
+ """ Use a neural network (NN) as predictor in Condensité. """
2
+ import torch
3
+ import torch.nn as nn
4
+ from torch.optim import AdamW
5
+ from torch.utils.data import TensorDataset, DataLoader
6
+ from condensite import CondensitePredictor, concat_collate
7
+
8
+
9
+ class NNPredictor(nn.Module, CondensitePredictor):
10
+ def __init__(self,
11
+ n_in, n_hidden, # structure params
12
+ batch_size, lr, weight_decay # training params
13
+ ):
14
+ super().__init__()
15
+ # define device
16
+ self._device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
+ # network architecture
18
+ self.network = nn.Sequential(
19
+ nn.Linear(n_in, n_hidden),
20
+ nn.BatchNorm1d(n_hidden),
21
+ nn.SiLU(),
22
+ nn.Linear(n_hidden, n_hidden),
23
+ nn.BatchNorm1d(n_hidden),
24
+ nn.SiLU(),
25
+ nn.Linear(n_hidden, n_hidden),
26
+ nn.BatchNorm1d(n_hidden),
27
+ nn.SiLU(),
28
+ nn.Linear(n_hidden, n_hidden),
29
+ nn.BatchNorm1d(n_hidden),
30
+ nn.SiLU(),
31
+ nn.Linear(n_hidden, n_hidden),
32
+ nn.BatchNorm1d(n_hidden),
33
+ nn.SiLU(),
34
+ nn.Linear(n_hidden, 1)
35
+ )
36
+ self.network.apply(self.init_weights)
37
+ # set training params
38
+ self.batch_size = batch_size
39
+ self.lr = lr
40
+ self.weight_decay = weight_decay
41
+ # define optimizer
42
+ self.optimizer = AdamW(params=self.network.parameters(),
43
+ lr=lr, weight_decay=weight_decay)
44
+
45
+ def device(self):
46
+ return self._device
47
+
48
+ def init_weights(self, m):
49
+ # initialization to match ReLU activations
50
+ if isinstance(m, nn.Linear):
51
+ nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
52
+ nn.init.zeros_(m.bias)
53
+
54
+ def forward(self, features):
55
+ return self.network(features).ravel()
56
+
57
+ def predict(self, features):
58
+ """ Predict for given features. """
59
+ # labels don't matter here, set to zeros
60
+ self.eval()
61
+ dataset = TensorDataset(features, torch.zeros(len(features)))
62
+ loader = DataLoader(dataset, self.batch_size, shuffle=False)
63
+ predictions = torch.empty(len(features), device=self.device())
64
+ start = 0
65
+ with torch.no_grad():
66
+ for feature_batch, _ in loader:
67
+ feature_batch = feature_batch.to(self.device())
68
+ batch_preds = self.forward(feature_batch).detach()
69
+ predictions[start:(start+len(batch_preds))] = batch_preds.flatten()
70
+ start += len(batch_preds)
71
+ return predictions
72
+
73
+ def fit(self, dataset):
74
+ """
75
+ Perform a fit on the features and targets in the dataset passed.
76
+ """
77
+ self.train()
78
+ ## batching
79
+ train_DL = DataLoader(dataset, self.batch_size, shuffle=True, collate_fn=concat_collate)
80
+ # training
81
+ epoch_losses = []
82
+ loss_fn = nn.MSELoss()
83
+ # loss_fn = nn.BCEWithLogitsLoss()
84
+ for feature_batch, target_batch in train_DL:
85
+ # move to device
86
+ feature_batch = feature_batch.to(self.device())
87
+ target_batch = target_batch.to(self.device())
88
+ # compute MSE and update weights
89
+ self.optimizer.zero_grad()
90
+ pred = self.forward(feature_batch)
91
+ loss = loss_fn(target_batch, pred)
92
+ loss.backward()
93
+ self.optimizer.step()
94
+ epoch_losses.append(loss.item())
95
+ # results of epoch
96
+ return sum(epoch_losses) / len(epoch_losses)
@@ -0,0 +1,3 @@
1
+ def test_import():
2
+ import condensite
3
+ assert condensite is not None