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.
- condensite-0.0.1/LICENSE +12 -0
- condensite-0.0.1/PKG-INFO +86 -0
- condensite-0.0.1/README.md +65 -0
- condensite-0.0.1/pyproject.toml +35 -0
- condensite-0.0.1/setup.cfg +4 -0
- condensite-0.0.1/src/condensite/__init__.py +5 -0
- condensite-0.0.1/src/condensite/core.py +344 -0
- condensite-0.0.1/src/condensite/utils.py +49 -0
- condensite-0.0.1/src/condensite.egg-info/PKG-INFO +86 -0
- condensite-0.0.1/src/condensite.egg-info/SOURCES.txt +16 -0
- condensite-0.0.1/src/condensite.egg-info/dependency_links.txt +1 -0
- condensite-0.0.1/src/condensite.egg-info/requires.txt +4 -0
- condensite-0.0.1/src/condensite.egg-info/top_level.txt +2 -0
- condensite-0.0.1/src/examples/example.py +61 -0
- condensite-0.0.1/src/examples/example_models/LGBMPredictor.py +35 -0
- condensite-0.0.1/src/examples/example_models/sklearnPredictor.py +29 -0
- condensite-0.0.1/src/examples/example_models/torchPredictor.py +96 -0
- condensite-0.0.1/tests/test_core.py +3 -0
condensite-0.0.1/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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)
|