ggml-ot 0.9.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.
- ggml_ot-0.9.1/LICENSE +21 -0
- ggml_ot-0.9.1/PKG-INFO +82 -0
- ggml_ot-0.9.1/README.md +49 -0
- ggml_ot-0.9.1/pyproject.toml +78 -0
- ggml_ot-0.9.1/src/ggml_ot/__init__.py +9 -0
- ggml_ot-0.9.1/src/ggml_ot/benchmark/__init__.py +2 -0
- ggml_ot-0.9.1/src/ggml_ot/benchmark/knn.py +258 -0
- ggml_ot-0.9.1/src/ggml_ot/benchmark/pivoted_chol.py +67 -0
- ggml_ot-0.9.1/src/ggml_ot/data/__init__.py +3 -0
- ggml_ot-0.9.1/src/ggml_ot/data/scRNA.py +234 -0
- ggml_ot-0.9.1/src/ggml_ot/data/synth.py +275 -0
- ggml_ot-0.9.1/src/ggml_ot/data/util.py +32 -0
- ggml_ot-0.9.1/src/ggml_ot/distances/__init__.py +1 -0
- ggml_ot-0.9.1/src/ggml_ot/distances/dists.py +246 -0
- ggml_ot-0.9.1/src/ggml_ot/ggml.py +653 -0
- ggml_ot-0.9.1/src/ggml_ot/plot/__init__.py +1 -0
- ggml_ot-0.9.1/src/ggml_ot/plot/plotting.py +548 -0
ggml_ot-0.9.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Damin Kühn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ggml_ot-0.9.1/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ggml-ot
|
|
3
|
+
Version: 0.9.1
|
|
4
|
+
Summary: Global Ground Metric Learning
|
|
5
|
+
Author: Damin Kuehn
|
|
6
|
+
Author-email: kuehn@cs.rwth-aachen.de
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Framework :: Sphinx
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
16
|
+
Requires-Dist: anndata (>=0.11.4,<0.12.0)
|
|
17
|
+
Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
|
|
18
|
+
Requires-Dist: numpy (>=2.2.4,<3.0.0)
|
|
19
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
20
|
+
Requires-Dist: pot (>=0.9.5,<0.10.0)
|
|
21
|
+
Requires-Dist: pydiffmap (>=0.2.0.1,<0.3.0.0)
|
|
22
|
+
Requires-Dist: scanpy (>=1.11.1,<2.0.0)
|
|
23
|
+
Requires-Dist: scikit-learn (>=1.1,<1.6)
|
|
24
|
+
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
25
|
+
Requires-Dist: seaborn (>=0.13.2,<0.14.0)
|
|
26
|
+
Requires-Dist: torch (>=2.6.0,<3.0.0)
|
|
27
|
+
Requires-Dist: tqdm (>=4.67.1,<5.0.0)
|
|
28
|
+
Requires-Dist: umap-learn (>=0.5.7,<0.6.0)
|
|
29
|
+
Project-URL: Homepage, https://github.com/DaminK/ggml-ot
|
|
30
|
+
Project-URL: Issues, https://github.com/DaminK/ggml-ot/issues
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# GGML-OT
|
|
34
|
+
|
|
35
|
+
<img src="https://github.com/user-attachments/assets/5c522e9b-8eb3-4b0a-8ae5-bb8671eba3d6" width="300" />
|
|
36
|
+
|
|
37
|
+
## Abstract
|
|
38
|
+
|
|
39
|
+
Optimal transport (OT) provides a robust framework for comparing probability distributions.
|
|
40
|
+
Its effectiveness is significantly influenced by the choice of the underlying ground metric.
|
|
41
|
+
Traditionally, the ground metric has either been (i) predefined, e.g. as a Euclidean metric, or (ii) learned in a supervised way, by utilizing labeled data to learn a suitable ground metric for enhanced task-specific performance.
|
|
42
|
+
While predefined metrics often do not account for the inherent structure and varying significance of different features in the data, existing supervised ground metric learning methods often fail to generalize across multiple classes or are limited to distributions with shared supports.
|
|
43
|
+
To address this issue, this paper introduces a novel approach for learning metrics for arbitrary distributions over a shared metric space.
|
|
44
|
+
Our method differentiates elements like a global metric, but requires only class labels on a distribution-level for training akin a ground metric.
|
|
45
|
+
The resulting learned global ground metric enables more accurate OT distances, which can significantly improve clustering and classification tasks. It can create task-specific shared embeddings across elements of different distributions including unseen data.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### Via pip
|
|
50
|
+
|
|
51
|
+
```terminal
|
|
52
|
+
pip install ggml-ot
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Manual
|
|
56
|
+
|
|
57
|
+
```terminal
|
|
58
|
+
git clone https://github.com/DaminK/ggml-ot
|
|
59
|
+
cd ggml-ot
|
|
60
|
+
pip install poetry
|
|
61
|
+
poetry lock && poetry install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Development installation
|
|
65
|
+
|
|
66
|
+
```terminal
|
|
67
|
+
git clone https://github.com/DaminK/ggml-ot
|
|
68
|
+
cd ggml-ot
|
|
69
|
+
pip install poetry
|
|
70
|
+
peotry lock && poetry install --with dev
|
|
71
|
+
pre-commit install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Citation
|
|
75
|
+
|
|
76
|
+
If you use this code in your research, please cite the following paper:
|
|
77
|
+
> Global Ground Metric Learning with Applications to scRNA data
|
|
78
|
+
>
|
|
79
|
+
> Damin Kuehn and Michael T. Schaub, Department of Computer Science RWTH Aachen
|
|
80
|
+
>
|
|
81
|
+
> Published at AISTATS2025 (DOI will follow)
|
|
82
|
+
|
ggml_ot-0.9.1/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# GGML-OT
|
|
2
|
+
|
|
3
|
+
<img src="https://github.com/user-attachments/assets/5c522e9b-8eb3-4b0a-8ae5-bb8671eba3d6" width="300" />
|
|
4
|
+
|
|
5
|
+
## Abstract
|
|
6
|
+
|
|
7
|
+
Optimal transport (OT) provides a robust framework for comparing probability distributions.
|
|
8
|
+
Its effectiveness is significantly influenced by the choice of the underlying ground metric.
|
|
9
|
+
Traditionally, the ground metric has either been (i) predefined, e.g. as a Euclidean metric, or (ii) learned in a supervised way, by utilizing labeled data to learn a suitable ground metric for enhanced task-specific performance.
|
|
10
|
+
While predefined metrics often do not account for the inherent structure and varying significance of different features in the data, existing supervised ground metric learning methods often fail to generalize across multiple classes or are limited to distributions with shared supports.
|
|
11
|
+
To address this issue, this paper introduces a novel approach for learning metrics for arbitrary distributions over a shared metric space.
|
|
12
|
+
Our method differentiates elements like a global metric, but requires only class labels on a distribution-level for training akin a ground metric.
|
|
13
|
+
The resulting learned global ground metric enables more accurate OT distances, which can significantly improve clustering and classification tasks. It can create task-specific shared embeddings across elements of different distributions including unseen data.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Via pip
|
|
18
|
+
|
|
19
|
+
```terminal
|
|
20
|
+
pip install ggml-ot
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Manual
|
|
24
|
+
|
|
25
|
+
```terminal
|
|
26
|
+
git clone https://github.com/DaminK/ggml-ot
|
|
27
|
+
cd ggml-ot
|
|
28
|
+
pip install poetry
|
|
29
|
+
poetry lock && poetry install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Development installation
|
|
33
|
+
|
|
34
|
+
```terminal
|
|
35
|
+
git clone https://github.com/DaminK/ggml-ot
|
|
36
|
+
cd ggml-ot
|
|
37
|
+
pip install poetry
|
|
38
|
+
peotry lock && poetry install --with dev
|
|
39
|
+
pre-commit install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Citation
|
|
43
|
+
|
|
44
|
+
If you use this code in your research, please cite the following paper:
|
|
45
|
+
> Global Ground Metric Learning with Applications to scRNA data
|
|
46
|
+
>
|
|
47
|
+
> Damin Kuehn and Michael T. Schaub, Department of Computer Science RWTH Aachen
|
|
48
|
+
>
|
|
49
|
+
> Published at AISTATS2025 (DOI will follow)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ggml-ot"
|
|
3
|
+
authors = [{ name = "Damin Kuehn", email = "kuehn@cs.rwth-aachen.de" }]
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
classifiers = [
|
|
7
|
+
"Programming Language :: Python :: 3",
|
|
8
|
+
"Development Status :: 5 - Production/Stable",
|
|
9
|
+
"Framework :: Sphinx",
|
|
10
|
+
"Intended Audience :: Science/Research",
|
|
11
|
+
"Intended Audience :: Healthcare Industry",
|
|
12
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
13
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
14
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"tqdm (>=4.67.1,<5.0.0)",
|
|
18
|
+
"scipy (>=1.15.2,<2.0.0)",
|
|
19
|
+
"matplotlib (>=3.10.1,<4.0.0)",
|
|
20
|
+
"seaborn (>=0.13.2,<0.14.0)",
|
|
21
|
+
"umap-learn (>=0.5.7,<0.6.0)",
|
|
22
|
+
"numpy (>=2.2.4,<3.0.0)",
|
|
23
|
+
"pandas (>=2.2.3,<3.0.0)",
|
|
24
|
+
"pot (>=0.9.5,<0.10.0)",
|
|
25
|
+
"pydiffmap (>=0.2.0.1,<0.3.0.0)",
|
|
26
|
+
"anndata (>=0.11.4,<0.12.0)",
|
|
27
|
+
"torch (>=2.6.0,<3.0.0)",
|
|
28
|
+
"scanpy (>=1.11.1,<2.0.0)",
|
|
29
|
+
"scikit-learn (>=1.1,<1.6)",
|
|
30
|
+
]
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/DaminK/ggml-ot"
|
|
33
|
+
Issues = "https://github.com/DaminK/ggml-ot/issues"
|
|
34
|
+
|
|
35
|
+
[tool.poetry]
|
|
36
|
+
name = "ggml-ot"
|
|
37
|
+
version = "0.9.1"
|
|
38
|
+
description = "Global Ground Metric Learning"
|
|
39
|
+
authors = ["Damin Kuehn <damin.kuehn@rwth-aachen.de>"]
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.dev]
|
|
42
|
+
optional = true
|
|
43
|
+
|
|
44
|
+
[tool.poetry.group.dev.dependencies]
|
|
45
|
+
pre-commit = "^3.2.1"
|
|
46
|
+
ipykernel = "^6.29.5"
|
|
47
|
+
|
|
48
|
+
[tool.poetry.group.docs]
|
|
49
|
+
optional = true
|
|
50
|
+
|
|
51
|
+
[tool.poetry.group.docs.dependencies]
|
|
52
|
+
sphinx = "^8.2.3"
|
|
53
|
+
sphinx-autorun = "^2.0.0"
|
|
54
|
+
sphinx-rtd-theme = "^3.0.2"
|
|
55
|
+
myst-parser = "^4.0.1"
|
|
56
|
+
|
|
57
|
+
[tool.poetry.group.test]
|
|
58
|
+
optional = true
|
|
59
|
+
|
|
60
|
+
[tool.poetry.group.test.dependencies]
|
|
61
|
+
pytest = "^8.3.5"
|
|
62
|
+
pytest-cov = "^6.1.1"
|
|
63
|
+
ruff = "^0.11.6"
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
select = ["E4", "E7", "E9", "F"]
|
|
67
|
+
task-tags = ["TODO", "FIXME"]
|
|
68
|
+
output-format = "github"
|
|
69
|
+
respect-gitignore = true
|
|
70
|
+
|
|
71
|
+
[tool.ruff.format]
|
|
72
|
+
quote-style = "double"
|
|
73
|
+
docstring-code-format = true
|
|
74
|
+
|
|
75
|
+
[build-system]
|
|
76
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
77
|
+
build-backend = "poetry.core.masonry.api"
|
|
78
|
+
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from sklearn.metrics import confusion_matrix
|
|
2
|
+
from sklearn.model_selection import StratifiedShuffleSplit, GroupShuffleSplit
|
|
3
|
+
from sklearn.neighbors import KNeighborsClassifier
|
|
4
|
+
from sklearn.metrics import silhouette_score
|
|
5
|
+
from sklearn.metrics.cluster import adjusted_rand_score
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
from IPython.display import display
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ShuffleSplit(
|
|
16
|
+
labels,
|
|
17
|
+
n_splits=10,
|
|
18
|
+
train_size=0.4,
|
|
19
|
+
test_size=0.4,
|
|
20
|
+
validation_size=0.2,
|
|
21
|
+
distribution_labels=None,
|
|
22
|
+
):
|
|
23
|
+
if validation_size > 0:
|
|
24
|
+
# draw validation inds in test split and later split into two test sets
|
|
25
|
+
test_size = test_size + validation_size
|
|
26
|
+
if n_splits > 0:
|
|
27
|
+
if distribution_labels is None:
|
|
28
|
+
sss = StratifiedShuffleSplit(
|
|
29
|
+
n_splits=n_splits,
|
|
30
|
+
test_size=test_size,
|
|
31
|
+
train_size=train_size,
|
|
32
|
+
random_state=0,
|
|
33
|
+
)
|
|
34
|
+
train_test_inds = sss.split(np.zeros(len(labels)), labels)
|
|
35
|
+
else:
|
|
36
|
+
# split patients into test and train,should be stratified
|
|
37
|
+
# distribution_labels
|
|
38
|
+
|
|
39
|
+
# unique_distribution_labels = np.unique(distribution_labels)
|
|
40
|
+
# distribution_train_test_inds = sss.split(np.zeros(len(unique_distribution_labels)), unique_distribution_labels)
|
|
41
|
+
|
|
42
|
+
gss = GroupShuffleSplit(
|
|
43
|
+
n_splits=n_splits,
|
|
44
|
+
test_size=test_size,
|
|
45
|
+
train_size=train_size,
|
|
46
|
+
random_state=0,
|
|
47
|
+
)
|
|
48
|
+
print('shapes')
|
|
49
|
+
print(np.zeros(len(labels)).shape)
|
|
50
|
+
print(labels.shape)
|
|
51
|
+
print(distribution_labels.shape)
|
|
52
|
+
train_test_inds = gss.split(
|
|
53
|
+
np.zeros(len(labels)), labels, distribution_labels
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# train_test_inds = []
|
|
57
|
+
# for i, (train_distribution_index, test_distribution_index) in enumerate(distribution_train_test_inds):
|
|
58
|
+
# train_label_ind = [distribution_labels == unique_distribution_labels[train_distribution_index]]
|
|
59
|
+
# test_label_ind = [distribution_labels == unique_distribution_labels[test_distribution_index]]
|
|
60
|
+
# #for each patient split, do test and train splits between cells from both splits
|
|
61
|
+
# train = sss.split(np.zeros(len(labels)), labels)
|
|
62
|
+
# test =
|
|
63
|
+
else:
|
|
64
|
+
# Train = Test
|
|
65
|
+
train_test_inds = [(np.arange(len(labels)), np.arange(len(labels)))]
|
|
66
|
+
|
|
67
|
+
if validation_size > 0:
|
|
68
|
+
test_size = test_size - validation_size
|
|
69
|
+
|
|
70
|
+
return train_test_inds
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def knn_from_dists(
|
|
74
|
+
dists,
|
|
75
|
+
labels,
|
|
76
|
+
n_splits=20,
|
|
77
|
+
distribution_labels=None,
|
|
78
|
+
n_neighbors=5,
|
|
79
|
+
weights=None,
|
|
80
|
+
test_size=0.2,
|
|
81
|
+
train_size=None,
|
|
82
|
+
):
|
|
83
|
+
predicted_labels, true_labels, test_indices, scores = [], [], [], []
|
|
84
|
+
|
|
85
|
+
train_test_inds = ShuffleSplit(
|
|
86
|
+
labels,
|
|
87
|
+
n_splits,
|
|
88
|
+
train_size,
|
|
89
|
+
test_size,
|
|
90
|
+
validation_size=0,
|
|
91
|
+
distribution_labels=distribution_labels,
|
|
92
|
+
)
|
|
93
|
+
# , np.arange(len(labels)))
|
|
94
|
+
|
|
95
|
+
for i, (train_index, test_index) in enumerate(train_test_inds):
|
|
96
|
+
print(train_index)
|
|
97
|
+
print(test_index)
|
|
98
|
+
train_dists = dists[np.ix_(train_index, train_index)]
|
|
99
|
+
test_to_train_dists = dists[np.ix_(test_index, train_index)]
|
|
100
|
+
|
|
101
|
+
neigh = KNeighborsClassifier(
|
|
102
|
+
n_neighbors=n_neighbors, metric='precomputed', weights=weights
|
|
103
|
+
)
|
|
104
|
+
neigh.fit(train_dists, [labels[t] for t in train_index])
|
|
105
|
+
|
|
106
|
+
predicted_labels.append(neigh.predict(test_to_train_dists))
|
|
107
|
+
true_labels.append(np.asarray([labels[t] for t in test_index]))
|
|
108
|
+
scores.append(neigh.score(test_to_train_dists, true_labels[-1]))
|
|
109
|
+
test_indices.append(test_index)
|
|
110
|
+
# ari.append(adjusted_rand_score(predicted_labels[-1],true_labels[-1]))
|
|
111
|
+
|
|
112
|
+
ari = adjusted_rand_score(
|
|
113
|
+
np.concatenate(true_labels), np.concatenate(predicted_labels)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return predicted_labels, true_labels, scores, ari, test_indices
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def silhouette_score_wrapper(dists, labels):
|
|
120
|
+
# wrapper function as fill diagonal is only available as inplace operation. It is needed to catch cases where due to numerical errors the distance of a graph to itself may be very close to zero, but not zero which is required by sklearn silhoute score method
|
|
121
|
+
zero_dia_dists = np.copy(dists)
|
|
122
|
+
np.fill_diagonal(zero_dia_dists, 0)
|
|
123
|
+
return silhouette_score(zero_dia_dists, labels, metric='precomputed')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def compute_dists(Graphs, Graphs2=None, method='TiedOT'):
|
|
127
|
+
dist, plan = methods[method](Graphs, Graphs2)
|
|
128
|
+
dist[dist < 0] = 0
|
|
129
|
+
return dist
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_dist_precomputed(precomputed_dists, ind1, ind2):
|
|
133
|
+
return precomputed_dists[ind1, :][:, ind2]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def plot_1split(predicted, true, title=None, ax=None):
|
|
137
|
+
annot_labels_ind = np.unique(true, return_index=True)[1]
|
|
138
|
+
annot_labels = true[annot_labels_ind]
|
|
139
|
+
# ind
|
|
140
|
+
cf_matrix = confusion_matrix(true, predicted, labels=annot_labels)
|
|
141
|
+
if ax is None:
|
|
142
|
+
plt.figure()
|
|
143
|
+
ax = sns.heatmap(
|
|
144
|
+
cf_matrix,
|
|
145
|
+
annot=True, # fmt='.0',
|
|
146
|
+
cmap='Blues',
|
|
147
|
+
xticklabels=annot_labels,
|
|
148
|
+
yticklabels=annot_labels,
|
|
149
|
+
ax=ax,
|
|
150
|
+
fmt='g',
|
|
151
|
+
)
|
|
152
|
+
ax.set(xlabel='Predicted Label', ylabel='True Label')
|
|
153
|
+
ax.set_title(title)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def plot_table(df, tranpose=False):
|
|
157
|
+
format_df = df
|
|
158
|
+
format_df.set_index('method', inplace=True)
|
|
159
|
+
if tranpose:
|
|
160
|
+
format_df = format_df.transpose()
|
|
161
|
+
display(format_df)
|
|
162
|
+
print(
|
|
163
|
+
format_df.to_latex(
|
|
164
|
+
index=True,
|
|
165
|
+
# formatters={"name": str.upper},
|
|
166
|
+
float_format='{:.2f}'.format,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def VI(
|
|
172
|
+
labels1: npt.NDArray[np.int32],
|
|
173
|
+
labels2: npt.NDArray[np.int32],
|
|
174
|
+
torch: bool = True,
|
|
175
|
+
device: str = 'cpu',
|
|
176
|
+
return_split_merge: bool = False,
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
Calculates the Variation of Information between two clusterings.
|
|
180
|
+
|
|
181
|
+
Arguments:
|
|
182
|
+
labels1: flat int32 array of labels for the first clustering
|
|
183
|
+
labels2: flat int32 array of labels for the second clustering
|
|
184
|
+
torch: whether to use torch, default:True
|
|
185
|
+
device: device to use for torch, default:"cpu"
|
|
186
|
+
return_split_merge: whether to return split and merge terms, default:False
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
vi: variation of information
|
|
190
|
+
vi_split: split term of variation of information
|
|
191
|
+
vi_merge: merge term of variation of information
|
|
192
|
+
splitters(optional): labels of labels2 which are split by labels1. splitters[i,0] is the contribution of the i-th splitter to the VI and splitters[i,1] is the corresponding label of the splitter
|
|
193
|
+
mergers(optional): labels of labels1 which are merging labels from labels2. mergers[i,0] is the contribution of the i-th merger to the VI and mergers[i,1] is the corresponding label of the merger
|
|
194
|
+
"""
|
|
195
|
+
if labels1.ndim > 1 or labels2.ndim > 1:
|
|
196
|
+
warnings.warn(
|
|
197
|
+
f"Inputs of shape {labels1.shape}, {labels2.shape} are not one-dimensional -- inputs will be flattened."
|
|
198
|
+
)
|
|
199
|
+
labels1 = labels1.flatten()
|
|
200
|
+
labels2 = labels2.flatten()
|
|
201
|
+
|
|
202
|
+
if torch:
|
|
203
|
+
return VI_torch(
|
|
204
|
+
labels1, labels2, device=device, return_split_merge=return_split_merge
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
return VI_np(labels1, labels2, return_split_merge=return_split_merge)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def VI_np(labels1, labels2, return_split_merge=False):
|
|
211
|
+
assert len(labels2) == len(labels1)
|
|
212
|
+
size = len(labels2)
|
|
213
|
+
|
|
214
|
+
mutual_labels = (labels1.astype(np.uint64) << 32) + labels2.astype(np.uint64)
|
|
215
|
+
|
|
216
|
+
sm_unique, sm_inverse, sm_counts = np.unique(
|
|
217
|
+
labels2, return_inverse=True, return_counts=True
|
|
218
|
+
)
|
|
219
|
+
fm_unique, fm_inverse, fm_counts = np.unique(
|
|
220
|
+
labels1, return_inverse=True, return_counts=True
|
|
221
|
+
)
|
|
222
|
+
_, mutual_inverse, mutual_counts = np.unique(
|
|
223
|
+
mutual_labels, return_inverse=True, return_counts=True
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
terms_mutual = -np.log(mutual_counts / size) * mutual_counts / size
|
|
227
|
+
terms_mutual_per_count = (
|
|
228
|
+
terms_mutual[mutual_inverse] / mutual_counts[mutual_inverse]
|
|
229
|
+
)
|
|
230
|
+
terms_sm = -np.log(sm_counts / size) * sm_counts / size
|
|
231
|
+
terms_fm = -np.log(fm_counts / size) * fm_counts / size
|
|
232
|
+
if not return_split_merge:
|
|
233
|
+
terms_mutual_sum = np.sum(terms_mutual_per_count)
|
|
234
|
+
vi_split = terms_mutual_sum - terms_sm.sum()
|
|
235
|
+
vi_merge = terms_mutual_sum - terms_fm.sum()
|
|
236
|
+
vi = vi_split + vi_merge
|
|
237
|
+
return vi, vi_split, vi_merge
|
|
238
|
+
|
|
239
|
+
vi_split_each = np.zeros(len(sm_unique))
|
|
240
|
+
np.add.at(vi_split_each, sm_inverse, terms_mutual_per_count)
|
|
241
|
+
vi_split_each -= terms_sm
|
|
242
|
+
vi_merge_each = np.zeros(len(fm_unique))
|
|
243
|
+
np.add.at(vi_merge_each, fm_inverse, terms_mutual_per_count)
|
|
244
|
+
vi_merge_each -= terms_fm
|
|
245
|
+
|
|
246
|
+
vi_split = np.sum(vi_split_each)
|
|
247
|
+
vi_merge = np.sum(vi_merge_each)
|
|
248
|
+
vi = vi_split + vi_merge
|
|
249
|
+
|
|
250
|
+
i_splitters = np.argsort(vi_split_each)[::-1]
|
|
251
|
+
i_mergers = np.argsort(vi_merge_each)[::-1]
|
|
252
|
+
|
|
253
|
+
vi_split_sorted = vi_split_each[i_splitters]
|
|
254
|
+
vi_merge_sorted = vi_merge_each[i_mergers]
|
|
255
|
+
|
|
256
|
+
splitters = np.stack([vi_split_sorted, sm_unique[i_splitters]], axis=1)
|
|
257
|
+
mergers = np.stack([vi_merge_sorted, fm_unique[i_mergers]], axis=1)
|
|
258
|
+
return vi, vi_split, vi_merge, splitters, mergers
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# pivoted_chol.py Author "Nathan Wycoff <nathanbrwycoff@gmail.com>" Date 01.14.2020
|
|
4
|
+
|
|
5
|
+
## A pivoted cholesky function for kernel functions
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def pivoted_chol(get_diag, get_row, M, err_tol=1e-6):
|
|
10
|
+
"""
|
|
11
|
+
A simple python function which computes the Pivoted Cholesky decomposition/approximation of positive semi-definite operator. Only diagonal elements and select rows of that operator's matrix represenation are required.
|
|
12
|
+
|
|
13
|
+
get_diag - A function which takes no arguments and returns the diagonal of the matrix when called.
|
|
14
|
+
get_row - A function which takes 1 integer argument and returns the desired row (zero indexed).
|
|
15
|
+
M - The maximum rank of the approximate decomposition; an integer.
|
|
16
|
+
err_tol - The maximum error tolerance, that is difference between the approximate decomposition and true matrix, allowed. Note that this is in the Trace norm, not the spectral or frobenius norm.
|
|
17
|
+
|
|
18
|
+
Returns: R, an upper triangular matrix of column dimension equal to the target matrix. It's row dimension will be at most M, but may be less if the termination condition was acceptably low error rather than max iters reached.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
d = np.copy(get_diag())
|
|
22
|
+
N = len(d)
|
|
23
|
+
|
|
24
|
+
pi = list(range(N))
|
|
25
|
+
|
|
26
|
+
R = np.zeros([M, N])
|
|
27
|
+
|
|
28
|
+
err = np.sum(np.abs(d))
|
|
29
|
+
|
|
30
|
+
m = 0
|
|
31
|
+
while (m < M) and (err > err_tol):
|
|
32
|
+
i = m + np.argmax([d[pi[j]] for j in range(m, N)])
|
|
33
|
+
|
|
34
|
+
tmp = pi[m]
|
|
35
|
+
pi[m] = pi[i]
|
|
36
|
+
pi[i] = tmp
|
|
37
|
+
|
|
38
|
+
R[m, pi[m]] = np.sqrt(d[pi[m]])
|
|
39
|
+
Apim = get_row(pi[m])
|
|
40
|
+
for i in range(m + 1, N):
|
|
41
|
+
if m > 0:
|
|
42
|
+
ip = np.inner(R[:m, pi[m]], R[:m, pi[i]])
|
|
43
|
+
else:
|
|
44
|
+
ip = 0
|
|
45
|
+
R[m, pi[i]] = (Apim[pi[i]] - ip) / R[m, pi[m]]
|
|
46
|
+
d[pi[i]] -= pow(R[m, pi[i]], 2)
|
|
47
|
+
|
|
48
|
+
err = np.sum([d[pi[i]] for i in range(m + 1, N)])
|
|
49
|
+
m += 1
|
|
50
|
+
|
|
51
|
+
R = R[:m, :]
|
|
52
|
+
|
|
53
|
+
return R
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Example:
|
|
57
|
+
N = 10
|
|
58
|
+
S = np.random.normal(size=[N, N])
|
|
59
|
+
A = np.matmul(S.T, S)
|
|
60
|
+
|
|
61
|
+
# Important to copy as diag may not be writable depending on np version
|
|
62
|
+
get_diag = lambda: np.diag(A).copy()
|
|
63
|
+
get_row = lambda i: A[i, :]
|
|
64
|
+
R = pivoted_chol(get_diag, get_row, M=N)
|
|
65
|
+
|
|
66
|
+
# R.T @ R is numerically equal to A.
|
|
67
|
+
print(np.linalg.norm(np.matmul(R.T, R) - A))
|