improving docs and new example for image datasets
This commit is contained in:
parent
4916919833
commit
f6c822ca8f
|
|
@ -13,7 +13,7 @@ for facilitating the analysis and interpretation of the experimental results.
|
||||||
|
|
||||||
### Last updates:
|
### Last updates:
|
||||||
|
|
||||||
* Version 0.2.0 is released! major changes can be consulted [here](CHANGE_LOG.txt).
|
* Version 0.2.1 is released! major changes can be consulted [here](CHANGE_LOG.txt).
|
||||||
* The developer API documentation is available [here](https://hlt-isti.github.io/QuaPy/index.html)
|
* The developer API documentation is available [here](https://hlt-isti.github.io/QuaPy/index.html)
|
||||||
* Manuals are available [here](https://hlt-isti.github.io/QuaPy/manuals.html)
|
* Manuals are available [here](https://hlt-isti.github.io/QuaPy/manuals.html)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
.navbar-brand.logo .title.logo__title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
max-height: 2.2rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand.logo .title.logo__title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.bd-header .navbar-header-items {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-header .navbar-header-items__center {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-header .bd-navbar-elements {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-copy {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 56rem;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-grid {
|
||||||
|
margin: 1.2rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-card {
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--pst-color-border, #d0d7de);
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-card .sd-card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .landing-card {
|
||||||
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,68 @@
|
||||||
|
import numpy as np
|
||||||
|
import quapy as qp
|
||||||
|
from quapy.method.confidence import ConfidenceIntervals
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
A minimal example showing how to visualise ternary prevalences on the simplex.
|
||||||
|
The plot combines a cloud of posterior triplets, a few reference prevalences, a
|
||||||
|
confidence ellipse induced by the cloud, and a smooth density centred around the
|
||||||
|
true prevalence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
true_prev = np.array([0.20, 0.35, 0.45])
|
||||||
|
train_prev = np.array([0.50, 0.30, 0.20])
|
||||||
|
pred_prev = np.array([0.18, 0.39, 0.43])
|
||||||
|
posterior_cloud = rng.dirichlet(alpha=45 * true_prev, size=250)
|
||||||
|
|
||||||
|
point_layers = [
|
||||||
|
{
|
||||||
|
'points': posterior_cloud,
|
||||||
|
'label': 'posterior cloud',
|
||||||
|
'style': {'s': 12, 'alpha': 0.25, 'color': 'steelblue', 'edgecolors': 'none'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'points': true_prev,
|
||||||
|
'label': 'true prevalence',
|
||||||
|
'style': {'s': 70, 'color': 'black'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'points': pred_prev,
|
||||||
|
'label': 'predicted prevalence',
|
||||||
|
'style': {'s': 70, 'color': 'crimson'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'points': train_prev,
|
||||||
|
'label': 'training prevalence',
|
||||||
|
'style': {'s': 70, 'color': 'darkorange'},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
confidence_region = ConfidenceIntervals(posterior_cloud, confidence_level=0.95, bonferroni_correction=True)
|
||||||
|
|
||||||
|
region_layers = [
|
||||||
|
{
|
||||||
|
'fn': lambda p: float(p in confidence_region),
|
||||||
|
'label': '95% confidence intervals',
|
||||||
|
'color': 'seagreen',
|
||||||
|
'alpha': 0.15,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
density = lambda p: np.exp(-45 * np.sum((p - true_prev) ** 2, axis=1))
|
||||||
|
|
||||||
|
qp.plot.plot_simplex(
|
||||||
|
point_layers=point_layers,
|
||||||
|
region_layers=region_layers,
|
||||||
|
density_function=density,
|
||||||
|
density_color='royalblue',
|
||||||
|
class_names=['class A', 'class B', 'class C'],
|
||||||
|
title='Ternary prevalence visualisation',
|
||||||
|
legend_ncol=3,
|
||||||
|
figsize=(7.2, 5.8),
|
||||||
|
class_name_fontsize=9,
|
||||||
|
title_fontsize=10,
|
||||||
|
legend_fontsize=8,
|
||||||
|
savepath='./plots/simplex_visualization.png',
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import quapy as qp
|
||||||
|
from quapy.data.datasets import fetch_image_embeddings
|
||||||
|
from quapy.method.aggregative import EMQ, RLLS
|
||||||
|
from quapy.classification.calibration import TemperatureScalingFromLogits
|
||||||
|
from quapy.protocol import UPP
|
||||||
|
from sklearn.linear_model import LogisticRegression
|
||||||
|
|
||||||
|
|
||||||
|
# This example illustrates how to run experiments with image datasets, in this case with CIFAR10
|
||||||
|
# The datasets available in quapy do not consist of raw image files, but are instead pre-generated
|
||||||
|
# embeddings (see the manuals for further information).
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
# Let us begin with a typical case in which the embeddings come from the penultimate layer of a neural
|
||||||
|
# model (in this case, a resnet18). We get these representations by specifying embedding='features'
|
||||||
|
|
||||||
|
print('fetching cifar10 embeddings')
|
||||||
|
train, test = fetch_image_embeddings(dataset_name='cifar10', embedding='features').train_test
|
||||||
|
|
||||||
|
print('training:', train)
|
||||||
|
print('test:', test)
|
||||||
|
|
||||||
|
Xtr, ytr = train.Xy
|
||||||
|
|
||||||
|
# let us train an Expectation Maximazion Quantifier (EMQ), aka Maximum Likelihood for Label Shift (MLLS)
|
||||||
|
# using a logistic regressor as the underlying classifier, with Bias Corrected Temperature Scaling (BCTS)
|
||||||
|
|
||||||
|
bcts_emq = EMQ(classifier=LogisticRegression(), calib='bcts', val_split=5)
|
||||||
|
|
||||||
|
print(f'fitting quantifier {bcts_emq}')
|
||||||
|
bcts_emq.fit(Xtr, ytr)
|
||||||
|
|
||||||
|
# we generate many samples exhibiting prior probability shift with the artificial prevalence protocol
|
||||||
|
# (we use the multiclass variant UPP instead of the grid-based APP)
|
||||||
|
|
||||||
|
qp.environ["SAMPLE_SIZE"] = 500 # when the sample size is common to all experiments, it is conveniet to set it once and for all
|
||||||
|
artificial_prev_prot = UPP(test, repeats=200)
|
||||||
|
print('generating 200 test bags of 500 instances each')
|
||||||
|
|
||||||
|
bctsemq_report = qp.evaluation.evaluation_report(bcts_emq, protocol=artificial_prev_prot, error_metrics=['mae', 'mrae'])
|
||||||
|
print(bctsemq_report.mean(numeric_only=True))
|
||||||
|
|
||||||
|
# we could instead use the pre-generated logits of the resnet18
|
||||||
|
print('fetching cifar10 logits')
|
||||||
|
train, test = fetch_image_embeddings(dataset_name='cifar10', embedding='logits').train_test
|
||||||
|
Xtr, ytr = train.Xy
|
||||||
|
|
||||||
|
# in this case, the representations are already classification-related outputs;
|
||||||
|
# we can convert them into (hopefully well-) calibrated outputs via TemperatureScaling
|
||||||
|
|
||||||
|
print('generating posterior probabilities out of logits via temperature scaling')
|
||||||
|
emq = EMQ(classifier=TemperatureScalingFromLogits(bias_corrected=True))
|
||||||
|
emq.fit(Xtr, ytr)
|
||||||
|
|
||||||
|
print('generating 200 test bags of 500 instances each')
|
||||||
|
artificial_prev_prot = UPP(test, repeats=200)
|
||||||
|
emq_report = qp.evaluation.evaluation_report(emq, protocol=artificial_prev_prot, error_metrics=['mae', 'mrae'])
|
||||||
|
print(emq_report.mean(numeric_only=True))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1113,8 +1113,6 @@ def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tu
|
||||||
val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes)
|
val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes)
|
||||||
test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes)
|
test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes)
|
||||||
|
|
||||||
print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}')
|
|
||||||
|
|
||||||
return train, val, test
|
return train, val, test
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""
|
||||||
|
Internal helper utilities shared by quantification methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.metrics import confusion_matrix
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
|
||||||
|
|
||||||
|
def _get_abstention_calibrators():
|
||||||
|
try:
|
||||||
|
from abstention.calibration import NoBiasVectorScaling, TempScaling, VectorScaling
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Posterior calibration for EMQ requires the optional 'abstention' package."
|
||||||
|
) from exc
|
||||||
|
return {
|
||||||
|
'nbvs': NoBiasVectorScaling(),
|
||||||
|
'bcts': TempScaling(bias_positions='all'),
|
||||||
|
'ts': TempScaling(),
|
||||||
|
'vs': VectorScaling(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cvxpy():
|
||||||
|
try:
|
||||||
|
import cvxpy as cp
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"RLLS requires the optional 'cvxpy' package."
|
||||||
|
) from exc
|
||||||
|
return cp
|
||||||
|
|
||||||
|
|
||||||
|
def _labels_to_indices(labels, classes):
|
||||||
|
encoder = LabelEncoder().fit(classes)
|
||||||
|
return encoder.transform(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def _rlls_check_mode(mode):
|
||||||
|
valid = {'soft', 'hard'}
|
||||||
|
if mode not in valid:
|
||||||
|
raise ValueError(f'unknown mode {mode!r}; valid ones are {valid}')
|
||||||
|
return mode
|
||||||
|
|
||||||
|
|
||||||
|
def _rlls_joint_distribution(posteriors, labels, classes, mode='soft'):
|
||||||
|
mode = _rlls_check_mode(mode)
|
||||||
|
posteriors = np.asarray(posteriors, dtype=float)
|
||||||
|
labels = np.asarray(labels)
|
||||||
|
n_samples, n_classes = posteriors.shape
|
||||||
|
assert n_classes == len(classes), 'wrong number of posterior columns'
|
||||||
|
|
||||||
|
if mode == 'hard':
|
||||||
|
pred = np.argmax(posteriors, axis=1)
|
||||||
|
encoded_labels = _labels_to_indices(labels, classes)
|
||||||
|
joint = confusion_matrix(encoded_labels, pred, labels=np.arange(n_classes)).T.astype(float)
|
||||||
|
return joint / n_samples
|
||||||
|
|
||||||
|
joint = np.zeros((n_classes, n_classes), dtype=float)
|
||||||
|
for class_index, class_ in enumerate(classes):
|
||||||
|
idx = labels == class_
|
||||||
|
if idx.any():
|
||||||
|
joint[:, class_index] = posteriors[idx].sum(axis=0)
|
||||||
|
return joint / n_samples
|
||||||
|
|
||||||
|
|
||||||
|
def _rlls_predicted_marginal(posteriors, mode='soft'):
|
||||||
|
mode = _rlls_check_mode(mode)
|
||||||
|
posteriors = np.asarray(posteriors, dtype=float)
|
||||||
|
if mode == 'soft':
|
||||||
|
return posteriors.mean(axis=0)
|
||||||
|
|
||||||
|
pred = np.argmax(posteriors, axis=1)
|
||||||
|
counts = np.bincount(pred, minlength=posteriors.shape[1]).astype(float)
|
||||||
|
return counts / counts.sum()
|
||||||
|
|
||||||
|
|
||||||
|
def _rlls_compute_3deltaC(n_classes, n_train, delta):
|
||||||
|
return 3 * (
|
||||||
|
2 * np.log(2 * n_classes / delta) / (3 * n_train)
|
||||||
|
+ np.sqrt(2 * np.log(2 * n_classes / delta) / n_train)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rlls_compute_weights(C_zy, qz, pz, rho, clip=False):
|
||||||
|
cp = _get_cvxpy()
|
||||||
|
|
||||||
|
n_classes = C_zy.shape[0]
|
||||||
|
theta = cp.Variable(n_classes)
|
||||||
|
b = qz - pz
|
||||||
|
objective = cp.Minimize(cp.pnorm(C_zy @ theta - b) + rho * cp.pnorm(theta))
|
||||||
|
constraints = [-1 <= theta]
|
||||||
|
problem = cp.Problem(objective, constraints)
|
||||||
|
|
||||||
|
try:
|
||||||
|
problem.solve(verbose=False, solver=cp.SCS)
|
||||||
|
except cp.error.SolverError:
|
||||||
|
problem.solve(verbose=False, solver=cp.SCS, use_indirect=True)
|
||||||
|
|
||||||
|
if theta.value is None:
|
||||||
|
raise RuntimeError('RLLS optimization failed to produce a solution')
|
||||||
|
|
||||||
|
w = 1 + np.asarray(theta.value, dtype=float)
|
||||||
|
if clip and np.any(w < 0):
|
||||||
|
w = np.clip(w, 0, None)
|
||||||
|
return w
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import quapy.functional as F
|
||||||
|
|
||||||
|
|
||||||
|
class TestFunctional(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_ternary_search_binary(self):
|
||||||
|
def loss(prev):
|
||||||
|
return (prev[1] - 0.37) ** 2
|
||||||
|
|
||||||
|
result = F.argmin_prevalence(loss, n_classes=2, method='ternary_search')
|
||||||
|
self.assertTrue(np.allclose(result.sum(), 1.0))
|
||||||
|
self.assertAlmostEqual(result[1], 0.37, places=3)
|
||||||
|
|
||||||
|
def test_ternary_search_multiclass_not_supported(self):
|
||||||
|
def loss(prev):
|
||||||
|
return np.sum((prev - np.array([0.2, 0.3, 0.5])) ** 2)
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
F.argmin_prevalence(loss, n_classes=3, method='ternary_search')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
HAS_MATPLOTLIB = True
|
||||||
|
except ImportError:
|
||||||
|
plt = None
|
||||||
|
HAS_MATPLOTLIB = False
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import quapy as qp
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_MATPLOTLIB and qp.plot is not None, 'matplotlib is not available')
|
||||||
|
class TestPlot(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_plot_simplex_smoke(self):
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
true_prev = np.array([0.2, 0.3, 0.5])
|
||||||
|
cloud = rng.dirichlet(alpha=30 * true_prev, size=50)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(5, 5))
|
||||||
|
fig, ax = qp.plot.plot_simplex(
|
||||||
|
point_layers=[
|
||||||
|
{'points': cloud, 'label': 'cloud', 'style': {'s': 8, 'alpha': 0.2}},
|
||||||
|
{'points': true_prev, 'label': 'target', 'style': {'s': 50, 'color': 'black'}},
|
||||||
|
],
|
||||||
|
region_layers=[
|
||||||
|
{'fn': lambda p: p[:, 2] >= 0.4, 'label': 'high class-3', 'color': 'green', 'alpha': 0.2},
|
||||||
|
],
|
||||||
|
density_function=lambda p: np.exp(-25 * np.sum((p - true_prev) ** 2, axis=1)),
|
||||||
|
class_names=['A', 'B', 'C'],
|
||||||
|
ax=ax,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(fig, ax.figure)
|
||||||
|
self.assertGreaterEqual(len(ax.collections), 2)
|
||||||
|
plt.close(fig)
|
||||||
Loading…
Reference in New Issue