Skip to content

Commit 98d895c

Browse files
Merge pull request #93 from stefanradev93/Development
Development
2 parents 26de450 + 18c10fe commit 98d895c

File tree

4 files changed

+137
-2
lines changed

4 files changed

+137
-2
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,10 @@ General Improvements:
6767
1. Bugfix in ``SetTransformer`` affecting saving and loading when using the version with inducing points.
6868
2. Bugfix in ``SetTransformer`` when using ``train_offline`` and batches result in unequal shapes.
6969
3. Improved documentation with examples
70+
71+
1.1.3 Series
72+
----------
73+
74+
1. Bugfix in ``SimulationMemory`` affecting the use of empty folders for initializing a ``Trainer``
75+
2. Bugfix in ``Trainer.train_from_presimulation()`` for model comparison tasks
76+
3. Added a classifier two-sample test function ``c2st`` in ``computational_utilities``

bayesflow/computational_utilities.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import tensorflow as tf
2323
from scipy import stats
2424
from sklearn.calibration import calibration_curve
25+
from sklearn.model_selection import KFold, cross_val_score
26+
from sklearn.neural_network import MLPClassifier
2527

2628
from bayesflow.default_settings import MMD_BANDWIDTH_LIST
2729
from bayesflow.exceptions import ShapeError
@@ -517,3 +519,87 @@ def aggregated_rmse(x_true, x_pred):
517519
return aggregated_error(
518520
x_true=x_true, x_pred=x_pred, inner_error_fun=root_mean_squared_error, outer_aggregation_fun=np.mean
519521
)
522+
523+
524+
def c2st(
525+
source_samples,
526+
target_samples,
527+
n_folds=5,
528+
scoring="accuracy",
529+
normalize=True,
530+
seed=123,
531+
hidden_units_per_dim=16,
532+
aggregate_output=True,
533+
):
534+
"""C2ST metric [1] using an sklearn neural network classifier (i.e., MLP).
535+
Code adapted from https://github.com/sbi-benchmark/sbibm/blob/main/sbibm/metrics/c2st.py
536+
537+
[1] Lopez-Paz, D., & Oquab, M. (2016). Revisiting classifier two-sample tests. arXiv:1610.06545.
538+
539+
Parameters
540+
----------
541+
source_samples : np.ndarray or tf.Tensor
542+
Source samples (e.g., approximate posterior samples)
543+
target_samples : np.ndarray or tf.Tensor
544+
Target samples (e.g., samples from a reference posterior)
545+
n_folds : int, optional, default: 5
546+
Number of folds in k-fold cross-validation for the classifier evaluation
547+
scoring : str, optional, default: "accuracy"
548+
Evaluation score of the sklearn MLP classifier
549+
normalize : bool, optional, default: True
550+
Whether the data shall be z-standardized relative to source_samples
551+
seed : int, optional, default: 123
552+
RNG seed for the MLP and k-fold CV
553+
hidden_units_per_dim : int, optional, default: 16
554+
Number of hidden units in the MLP, relative to the input dimensions.
555+
Example: source samples are 5D, hidden_units_per_dim=16 -> 80 hidden units per layer
556+
aggregate_output : bool, optional, default: True
557+
Whether to return a single value aggregated over all cross-validation runs
558+
or all values from all runs. If left at default, the empirical mean will be returned
559+
560+
Returns
561+
-------
562+
c2st_score : float
563+
The resulting C2ST score
564+
565+
"""
566+
567+
x = np.array(source_samples)
568+
y = np.array(target_samples)
569+
570+
num_dims = x.shape[1]
571+
if not num_dims == y.shape[1]:
572+
raise ShapeError(
573+
f"source_samples and target_samples can have different number of observations (1st dim)"
574+
f"but must have the same dimensionality (2nd dim)"
575+
f"found: source_samples {source_samples.shape[1]}, target_samples {target_samples.shape[1]}"
576+
)
577+
578+
if normalize:
579+
x_mean = np.mean(x, axis=0)
580+
x_std = np.std(x, axis=0)
581+
x = (x - x_mean) / x_std
582+
y = (y - x_mean) / x_std
583+
584+
clf = MLPClassifier(
585+
activation="relu",
586+
hidden_layer_sizes=(hidden_units_per_dim * num_dims, hidden_units_per_dim * num_dims),
587+
max_iter=10000,
588+
solver="adam",
589+
random_state=seed,
590+
)
591+
592+
data = np.concatenate((x, y))
593+
target = np.concatenate(
594+
(
595+
np.zeros((x.shape[0],)),
596+
np.ones((y.shape[0],)),
597+
)
598+
)
599+
600+
shuffle = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
601+
scores = cross_val_score(clf, data, target, cv=shuffle, scoring=scoring)
602+
603+
if aggregate_output:
604+
c2st_score = np.asarray(np.mean(scores)).astype(np.float32)
605+
return c2st_score

bayesflow/helper_classes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ def load_from_file(self, file_path):
744744
memory_path = os.path.join(file_path, f"{SimulationMemory.file_name}.pkl")
745745

746746
# Case memory file exists
747-
if os.path.exists(file_path):
747+
if os.path.exists(memory_path):
748748
# Load pickle and fill in attributes
749749
with open(memory_path, "rb") as f:
750750
full_memory_dict = pickle.load(f)

tests/test_computational_utilities.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import pytest
44
import numpy as np
55
from bayesflow import computational_utilities
6-
from bayesflow.exceptions import ArgumentError
6+
from bayesflow.exceptions import ArgumentError, ShapeError
77
from bayesflow.trainers import Trainer
8+
import tensorflow as tf
89

910

1011
@pytest.mark.parametrize("x_true, x_pred, output",
@@ -93,3 +94,44 @@ def test_aggregated_error(x_true, x_pred, inner_error_fun, outer_aggregation_fun
9394
outer_aggregation_fun=outer_aggregation_fun
9495
)
9596
assert aggregated_error_result == pytest.approx(output)
97+
98+
99+
def test_c2st_shape_error():
100+
source_samples = np.random.random(size=(5, 2))
101+
target_samples = np.random.random(size=(5, 3))
102+
with pytest.raises(ShapeError):
103+
computational_utilities.c2st(source_samples, target_samples)
104+
105+
106+
@pytest.mark.parametrize(
107+
"source_samples, target_samples",
108+
[
109+
(np.random.random((5, 2)), np.random.random((5, 2))),
110+
(np.random.random((10, 2)), np.random.random((5, 2))),
111+
(tf.constant(np.random.random((5, 2))), tf.constant(np.random.random((5, 2))))
112+
]
113+
)
114+
def test_c2st(source_samples, target_samples):
115+
c2st_score = computational_utilities.c2st(source_samples, target_samples)
116+
assert 0.0 <= c2st_score <= 1.0
117+
118+
119+
@pytest.mark.parametrize(
120+
"n_folds, scoring, normalize, seed, hidden_units_per_dim",
121+
[
122+
(3, "accuracy", False, 42, 5),
123+
(7, "f1", True, 12, 10)
124+
]
125+
)
126+
def test_c2st_params(n_folds, scoring, normalize, seed, hidden_units_per_dim):
127+
source_samples = np.random.random((5, 2))
128+
target_samples = np.random.random((10, 2))
129+
_ = computational_utilities.c2st(
130+
source_samples=source_samples,
131+
target_samples=target_samples,
132+
n_folds=n_folds,
133+
scoring=scoring,
134+
normalize=normalize,
135+
seed=seed,
136+
hidden_units_per_dim=hidden_units_per_dim
137+
)

0 commit comments

Comments
 (0)