event2vector 0.3.0__tar.gz → 0.3.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.
- event2vector-0.3.1/CHANGELOG.md +39 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/MANIFEST.in +1 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/PKG-INFO +6 -5
- {event2vector-0.3.0 → event2vector-0.3.1}/README.md +1 -1
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/__init__.py +1 -1
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/geometries.py +4 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/models.py +12 -1
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector.egg-info/PKG-INFO +6 -5
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector.egg-info/SOURCES.txt +2 -2
- {event2vector-0.3.0 → event2vector-0.3.1}/pyproject.toml +5 -4
- event2vector-0.3.1/tests/test_device.py +75 -0
- event2vector-0.3.0/tests/test_datasets.py +0 -82
- event2vector-0.3.0/tests/test_models.py +0 -360
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/baselines.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/data.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/__init__.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/arithmetic.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/brown.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/decay.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/groups.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/lifepath.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/datasets/realdata.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/eval/__init__.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/eval/figures.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/eval/harness.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector/metrics.py +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector.egg-info/dependency_links.txt +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector.egg-info/requires.txt +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/event2vector.egg-info/top_level.txt +0 -0
- {event2vector-0.3.0 → event2vector-0.3.1}/setup.cfg +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `event2vector` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.3.1] - 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `Event2Vec(geometry="rotation")` no longer crashes on Apple Silicon with
|
|
12
|
+
`NotImplementedError: The operator 'aten::linalg_matrix_exp' is not currently
|
|
13
|
+
implemented for the MPS device`. `RotationModel` now computes the matrix
|
|
14
|
+
exponential on the CPU when its input is on MPS and moves the result back, so
|
|
15
|
+
`device="mps"`, a bare `RotationModel` and a `ProductModel` with a rotation
|
|
16
|
+
factor all work.
|
|
17
|
+
- The project links on PyPI pointed to `github.com/sulcantonin/event2vector`,
|
|
18
|
+
which does not exist; they now point to
|
|
19
|
+
[`github.com/sulcantonin/event2vec_public`](https://github.com/sulcantonin/event2vec_public).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- When the device is chosen automatically and MPS would be picked, the
|
|
24
|
+
`rotation` geometry runs on the CPU instead and emits a `RuntimeWarning`. On
|
|
25
|
+
MPS every step round-trips through the CPU, which made training roughly 8–14×
|
|
26
|
+
slower than on the CPU alone for this model. Pass `device="mps"` to use MPS
|
|
27
|
+
anyway. CUDA and the other geometries are unaffected.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Tests for the automatic device selection (`tests/test_device.py`).
|
|
32
|
+
- This changelog, linked from the PyPI project page.
|
|
33
|
+
|
|
34
|
+
## [0.3.0] - 2026-07-21
|
|
35
|
+
|
|
36
|
+
Earlier releases are listed on [PyPI](https://pypi.org/project/event2vector/#history).
|
|
37
|
+
|
|
38
|
+
[0.3.1]: https://pypi.org/project/event2vector/0.3.1/
|
|
39
|
+
[0.3.0]: https://pypi.org/project/event2vector/0.3.0/
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: event2vector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Scikit-learn-style geometric embeddings for event sequences.
|
|
5
5
|
Author: Antonin Sulc
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/sulcantonin/
|
|
8
|
-
Project-URL: Repository, https://github.com/sulcantonin/
|
|
9
|
-
Project-URL: Issues, https://github.com/sulcantonin/
|
|
7
|
+
Project-URL: Homepage, https://github.com/sulcantonin/event2vec_public
|
|
8
|
+
Project-URL: Repository, https://github.com/sulcantonin/event2vec_public
|
|
9
|
+
Project-URL: Issues, https://github.com/sulcantonin/event2vec_public/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md
|
|
10
11
|
Keywords: sequence-embedding,event-sequences,representation-learning,word2vec,scikit-learn,time-series
|
|
11
12
|
Classifier: Programming Language :: Python :: 3
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -132,7 +133,7 @@ Key methods:
|
|
|
132
133
|
- `most_similar`: gensim-style nearest-neighbor lookup over learned event embeddings using tokens or full sequences as queries.
|
|
133
134
|
- `pad_sequences=True`: enables fully vectorized batches with masking for substantial throughput gains on large corpora.
|
|
134
135
|
|
|
135
|
-
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`).
|
|
136
|
+
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`). On Apple Silicon the `rotation` geometry defaults to the CPU, because MPS has no `matrix_exp` kernel and the CPU is faster for it; pass `device="mps"` to override.
|
|
136
137
|
|
|
137
138
|
## Geometries (v0.2)
|
|
138
139
|
|
|
@@ -98,7 +98,7 @@ Key methods:
|
|
|
98
98
|
- `most_similar`: gensim-style nearest-neighbor lookup over learned event embeddings using tokens or full sequences as queries.
|
|
99
99
|
- `pad_sequences=True`: enables fully vectorized batches with masking for substantial throughput gains on large corpora.
|
|
100
100
|
|
|
101
|
-
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`).
|
|
101
|
+
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`). On Apple Silicon the `rotation` geometry defaults to the CPU, because MPS has no `matrix_exp` kernel and the CPU is faster for it; pass `device="mps"` to override.
|
|
102
102
|
|
|
103
103
|
## Geometries (v0.2)
|
|
104
104
|
|
|
@@ -216,6 +216,10 @@ class RotationModel(nn.Module):
|
|
|
216
216
|
a = torch.zeros(b, self.n, self.n, device=gen.device, dtype=gen.dtype)
|
|
217
217
|
a[:, self.triu_i, self.triu_j] = gen
|
|
218
218
|
a = a - a.transpose(1, 2)
|
|
219
|
+
if a.device.type == "mps":
|
|
220
|
+
# MPS has no matrix_exp kernel (pytorch/pytorch#141287); compute it on
|
|
221
|
+
# the CPU and move it back. Both transfers are differentiable.
|
|
222
|
+
return torch.matrix_exp(a.cpu()).to(a.device)
|
|
219
223
|
return torch.matrix_exp(a)
|
|
220
224
|
|
|
221
225
|
def zero_state(self, batch, device):
|
|
@@ -249,7 +249,18 @@ class Event2Vec:
|
|
|
249
249
|
if use_gpu and torch.cuda.is_available():
|
|
250
250
|
device = torch.device("cuda")
|
|
251
251
|
elif use_gpu and hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
252
|
-
|
|
252
|
+
if self.geometry == "rotation":
|
|
253
|
+
# MPS has no matrix_exp, so every rotation step would round-trip
|
|
254
|
+
# through the CPU; for a model this small the CPU is much faster.
|
|
255
|
+
warnings.warn(
|
|
256
|
+
"geometry='rotation' needs torch.matrix_exp, which MPS does not "
|
|
257
|
+
"implement; using device='cpu'. Pass device='mps' to run on MPS "
|
|
258
|
+
"anyway (matrix_exp is then computed on the CPU).",
|
|
259
|
+
RuntimeWarning,
|
|
260
|
+
)
|
|
261
|
+
device = torch.device("cpu")
|
|
262
|
+
else:
|
|
263
|
+
device = torch.device("mps")
|
|
253
264
|
else:
|
|
254
265
|
device = torch.device("cpu")
|
|
255
266
|
else:
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: event2vector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Scikit-learn-style geometric embeddings for event sequences.
|
|
5
5
|
Author: Antonin Sulc
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/sulcantonin/
|
|
8
|
-
Project-URL: Repository, https://github.com/sulcantonin/
|
|
9
|
-
Project-URL: Issues, https://github.com/sulcantonin/
|
|
7
|
+
Project-URL: Homepage, https://github.com/sulcantonin/event2vec_public
|
|
8
|
+
Project-URL: Repository, https://github.com/sulcantonin/event2vec_public
|
|
9
|
+
Project-URL: Issues, https://github.com/sulcantonin/event2vec_public/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md
|
|
10
11
|
Keywords: sequence-embedding,event-sequences,representation-learning,word2vec,scikit-learn,time-series
|
|
11
12
|
Classifier: Programming Language :: Python :: 3
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -132,7 +133,7 @@ Key methods:
|
|
|
132
133
|
- `most_similar`: gensim-style nearest-neighbor lookup over learned event embeddings using tokens or full sequences as queries.
|
|
133
134
|
- `pad_sequences=True`: enables fully vectorized batches with masking for substantial throughput gains on large corpora.
|
|
134
135
|
|
|
135
|
-
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`).
|
|
136
|
+
Device control: set `use_gpu=False` to force CPU even if CUDA/MPS is present, or pass an explicit `device` (e.g., `"cuda:0"` or `"cpu"`). On Apple Silicon the `rotation` geometry defaults to the CPU, because MPS has no `matrix_exp` kernel and the CPU is faster for it; pass `device="mps"` to override.
|
|
136
137
|
|
|
137
138
|
## Geometries (v0.2)
|
|
138
139
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
1
2
|
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
@@ -22,5 +23,4 @@ event2vector/datasets/realdata.py
|
|
|
22
23
|
event2vector/eval/__init__.py
|
|
23
24
|
event2vector/eval/figures.py
|
|
24
25
|
event2vector/eval/harness.py
|
|
25
|
-
tests/
|
|
26
|
-
tests/test_models.py
|
|
26
|
+
tests/test_device.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "event2vector"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Scikit-learn-style geometric embeddings for event sequences."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -63,9 +63,10 @@ dev = [
|
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
[project.urls]
|
|
66
|
-
Homepage = "https://github.com/sulcantonin/
|
|
67
|
-
Repository = "https://github.com/sulcantonin/
|
|
68
|
-
Issues = "https://github.com/sulcantonin/
|
|
66
|
+
Homepage = "https://github.com/sulcantonin/event2vec_public"
|
|
67
|
+
Repository = "https://github.com/sulcantonin/event2vec_public"
|
|
68
|
+
Issues = "https://github.com/sulcantonin/event2vec_public/issues"
|
|
69
|
+
Changelog = "https://github.com/sulcantonin/event2vec_public/blob/main/CHANGELOG.md"
|
|
69
70
|
|
|
70
71
|
[tool.setuptools.packages.find]
|
|
71
72
|
include = ["event2vector*"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Device-selection tests: the default constructor must work on every backend.
|
|
2
|
+
|
|
3
|
+
The rest of the suite pins ``use_gpu=False``, so these are the tests that exercise
|
|
4
|
+
the estimator's automatic CUDA / MPS / CPU choice. Run with `pytest tests/ -q`.
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from event2vector import Event2Vec, ProductModel, RotationModel, StereographicModel
|
|
11
|
+
|
|
12
|
+
MPS_AVAILABLE = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
|
13
|
+
|
|
14
|
+
TINY_SEQS = [[0, 1, 2], [2, 1, 0, 1], [1, 2], [0, 2, 2, 1]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.filterwarnings("ignore:geometry='rotation' needs torch.matrix_exp")
|
|
18
|
+
def test_rotation_fits_with_default_device():
|
|
19
|
+
"""Event2Vec(geometry="rotation") with no device arguments must fit on any machine
|
|
20
|
+
(it used to raise NotImplementedError for aten::linalg_matrix_exp on Apple Silicon)."""
|
|
21
|
+
est = Event2Vec(num_event_types=3, geometry="rotation", rotation_n=3,
|
|
22
|
+
num_epochs=2, batch_size=2, random_state=0)
|
|
23
|
+
assert est.device.type != "mps"
|
|
24
|
+
est.fit(TINY_SEQS)
|
|
25
|
+
|
|
26
|
+
assert est.is_fitted_
|
|
27
|
+
assert np.isfinite(est.training_loss_).all()
|
|
28
|
+
emb = est.transform(TINY_SEQS)
|
|
29
|
+
assert emb.shape == (len(TINY_SEQS), 9) # flattened 3x3 rotation
|
|
30
|
+
assert np.isfinite(emb).all()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_auto_device_skips_mps_for_rotation(monkeypatch):
|
|
34
|
+
"""When MPS would be auto-selected, rotation falls back to CPU with a warning.
|
|
35
|
+
Simulated so it runs on any platform; the model never touches MPS."""
|
|
36
|
+
monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
|
|
37
|
+
monkeypatch.setattr(torch.backends.mps, "is_available", lambda: True)
|
|
38
|
+
with pytest.warns(RuntimeWarning, match="matrix_exp"):
|
|
39
|
+
est = Event2Vec(num_event_types=3, geometry="rotation")
|
|
40
|
+
assert est.device.type == "cpu"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.skipif(not MPS_AVAILABLE, reason="needs an MPS device")
|
|
44
|
+
def test_rotation_runs_on_explicit_mps():
|
|
45
|
+
"""device="mps" is honoured: matrix_exp runs on the CPU and the result (and its
|
|
46
|
+
gradient) matches a pure-CPU computation."""
|
|
47
|
+
torch.manual_seed(0)
|
|
48
|
+
gen = torch.randn(5, 3)
|
|
49
|
+
cpu_model = RotationModel(3, n=3)
|
|
50
|
+
mps_model = RotationModel(3, n=3).to("mps")
|
|
51
|
+
|
|
52
|
+
g_cpu = gen.clone().requires_grad_()
|
|
53
|
+
r_cpu = cpu_model._rot(g_cpu)
|
|
54
|
+
(r_cpu ** 3).sum().backward()
|
|
55
|
+
g_mps = gen.clone().to("mps").requires_grad_()
|
|
56
|
+
r_mps = mps_model._rot(g_mps)
|
|
57
|
+
(r_mps ** 3).sum().backward()
|
|
58
|
+
|
|
59
|
+
assert r_mps.device.type == "mps"
|
|
60
|
+
assert torch.allclose(r_cpu, r_mps.cpu(), atol=1e-5)
|
|
61
|
+
assert torch.allclose(g_cpu.grad, g_mps.grad.cpu(), atol=1e-5)
|
|
62
|
+
|
|
63
|
+
# a product manifold with a rotation factor goes through the same path
|
|
64
|
+
product = ProductModel([
|
|
65
|
+
RotationModel(3, n=3),
|
|
66
|
+
StereographicModel(3, embedding_dim=4, kappa=-1.0),
|
|
67
|
+
]).to("mps")
|
|
68
|
+
logits, _, _ = product(torch.tensor([0, 1, 2], device="mps"))
|
|
69
|
+
assert torch.isfinite(logits).all()
|
|
70
|
+
|
|
71
|
+
est = Event2Vec(num_event_types=3, geometry="rotation", device="mps",
|
|
72
|
+
num_epochs=2, batch_size=2, random_state=0)
|
|
73
|
+
est.fit(TINY_SEQS)
|
|
74
|
+
assert np.isfinite(est.training_loss_).all()
|
|
75
|
+
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Ground-truth tests for the centerpiece datasets.
|
|
2
|
-
|
|
3
|
-
These lock the exact structure the paper's claims rely on: decay operators that
|
|
4
|
-
sum to the known transmutation, and group abelianizations that match the known
|
|
5
|
-
algebra. No model training required.
|
|
6
|
-
"""
|
|
7
|
-
import numpy as np
|
|
8
|
-
|
|
9
|
-
from event2vector.datasets import make_decay, make_group
|
|
10
|
-
from event2vector.datasets.decay import GROUND_TRUTH
|
|
11
|
-
from event2vector.metrics import _multiset_ceiling
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# --------------------------------------------------------------------------- #
|
|
15
|
-
# Physics: nuclear decay
|
|
16
|
-
# --------------------------------------------------------------------------- #
|
|
17
|
-
def test_decay_ground_truth_matches_physics():
|
|
18
|
-
d = make_decay(num_seq=200, seed=0)
|
|
19
|
-
gt = d["ground_truth"]
|
|
20
|
-
for mode_id, dzdn in gt.items():
|
|
21
|
-
name = d["inv_vocab"][mode_id]
|
|
22
|
-
assert tuple(dzdn) == GROUND_TRUTH[name]
|
|
23
|
-
# the canonical decays present must include alpha and beta_minus
|
|
24
|
-
assert "alpha" in d["vocab"] and "beta_minus" in d["vocab"]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_canonical_series_sum_to_known_transmutation():
|
|
28
|
-
d = make_decay(num_seq=50, seed=0)
|
|
29
|
-
gt = d["ground_truth"]
|
|
30
|
-
expected = {"thorium_4n": (-8, -16), "uranium_4n+2": (-10, -22)}
|
|
31
|
-
for name, c in d["canonical"].items():
|
|
32
|
-
net = tuple(np.sum([gt[t] for t in c["tokens"]], axis=0))
|
|
33
|
-
assert net == c["net_dZdN"] == expected[name]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_extended_decay_modes_present_and_exact():
|
|
37
|
-
d = make_decay(num_seq=6000, seed=0)
|
|
38
|
-
vocab = d["vocab"]
|
|
39
|
-
# the richer particle set is exercised by the generator
|
|
40
|
-
for m in ["electron_capture", "double_beta", "cluster_c14",
|
|
41
|
-
"proton_emission", "neutron_emission"]:
|
|
42
|
-
assert m in vocab, f"{m} not generated"
|
|
43
|
-
assert GROUND_TRUTH["cluster_c14"] == (-6, -8)
|
|
44
|
-
# electron capture and beta+ are physically distinct but share the operator
|
|
45
|
-
assert GROUND_TRUTH["electron_capture"] == GROUND_TRUTH["beta_plus"] == (-1, 1)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# --------------------------------------------------------------------------- #
|
|
49
|
-
# Math: group sequences
|
|
50
|
-
# --------------------------------------------------------------------------- #
|
|
51
|
-
def test_symmetric_group_abelianization_is_z2():
|
|
52
|
-
for name, n_el in [("s3", 6), ("s4", 24)]:
|
|
53
|
-
r = make_group(name, num_seq=300, max_len=8, seed=0)
|
|
54
|
-
assert r["is_abelian"] is False
|
|
55
|
-
assert r["n_elements"] == n_el
|
|
56
|
-
assert r["n_abelian"] == 2 # S_n abelianization = Z_2 (sign)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_abelian_groups_element_equals_abelianization():
|
|
60
|
-
for name, n_el in [("z12", 12), ("z6xz4", 24)]:
|
|
61
|
-
r = make_group(name, num_seq=300, max_len=8, seed=0)
|
|
62
|
-
assert r["is_abelian"] is True
|
|
63
|
-
assert r["n_elements"] == n_el
|
|
64
|
-
assert r["n_abelian"] == n_el # abelianization is the whole group
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_multiset_ceiling_abelian_vs_nonabelian():
|
|
68
|
-
n_gen = 3
|
|
69
|
-
# Abelian (Z6xZ4): the multiset determines the element exactly -> ceiling 1.0
|
|
70
|
-
z = make_group("z6xz4", num_seq=4000, max_len=10, seed=0)
|
|
71
|
-
seqs_z = z["processed_sequences"]
|
|
72
|
-
ng_z = len(z["vocab"])
|
|
73
|
-
assert _multiset_ceiling(seqs_z, z["sequence_labels_element"], ng_z) > 0.999
|
|
74
|
-
# Non-abelian (S4): abelianization is multiset-determined (1.0) but the full
|
|
75
|
-
# element is not (well below 1).
|
|
76
|
-
s = make_group("s4", num_seq=4000, max_len=10, seed=0)
|
|
77
|
-
seqs_s = s["processed_sequences"]
|
|
78
|
-
ng_s = len(s["vocab"])
|
|
79
|
-
abel_ceil = _multiset_ceiling(seqs_s, s["sequence_labels_abelian"], ng_s)
|
|
80
|
-
elem_ceil = _multiset_ceiling(seqs_s, s["sequence_labels_element"], ng_s)
|
|
81
|
-
assert abel_ceil > 0.999
|
|
82
|
-
assert elem_ceil < 0.8
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
"""Journal-grade correctness tests for the Event2Vec estimator and models.
|
|
2
|
-
|
|
3
|
-
Each test locks one property asserted by the paper or fixed during the TMLR
|
|
4
|
-
revision. Run with `pytest tests/ -q`.
|
|
5
|
-
"""
|
|
6
|
-
import math
|
|
7
|
-
|
|
8
|
-
import numpy as np
|
|
9
|
-
import pytest
|
|
10
|
-
import torch
|
|
11
|
-
|
|
12
|
-
from event2vector import Event2Vec, EuclideanModel, HyperbolicModel, HyperbolicUtils
|
|
13
|
-
from event2vector import (
|
|
14
|
-
HeisenbergModel,
|
|
15
|
-
ProductModel,
|
|
16
|
-
RotationModel,
|
|
17
|
-
StereographicModel,
|
|
18
|
-
StereographicUtils,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# --------------------------------------------------------------------------- #
|
|
23
|
-
# Fixtures / helpers
|
|
24
|
-
# --------------------------------------------------------------------------- #
|
|
25
|
-
def structured_sequences():
|
|
26
|
-
"""A small, strongly-patterned dataset (learnable, order-bearing)."""
|
|
27
|
-
base = [
|
|
28
|
-
[0, 1, 2, 3],
|
|
29
|
-
[0, 1, 2, 4],
|
|
30
|
-
[0, 2, 3, 4],
|
|
31
|
-
[0, 1, 3, 4],
|
|
32
|
-
[0, 2, 1, 4],
|
|
33
|
-
]
|
|
34
|
-
return [torch.tensor(s, dtype=torch.long) for s in base for _ in range(8)]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def euclid(**kw):
|
|
38
|
-
defaults = dict(
|
|
39
|
-
num_event_types=5,
|
|
40
|
-
embedding_dim=16,
|
|
41
|
-
dropout_p=0.0,
|
|
42
|
-
num_epochs=4,
|
|
43
|
-
batch_size=8,
|
|
44
|
-
random_state=0,
|
|
45
|
-
use_gpu=False,
|
|
46
|
-
)
|
|
47
|
-
defaults.update(kw)
|
|
48
|
-
return Event2Vec(**defaults)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def hyper(**kw):
|
|
52
|
-
defaults = dict(
|
|
53
|
-
num_event_types=5,
|
|
54
|
-
embedding_dim=16,
|
|
55
|
-
geometry="hyperbolic",
|
|
56
|
-
curvature=1.0,
|
|
57
|
-
dropout_p=0.0,
|
|
58
|
-
num_epochs=4,
|
|
59
|
-
batch_size=8,
|
|
60
|
-
random_state=0,
|
|
61
|
-
use_gpu=False,
|
|
62
|
-
)
|
|
63
|
-
defaults.update(kw)
|
|
64
|
-
return Event2Vec(**defaults)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# --------------------------------------------------------------------------- #
|
|
68
|
-
# Shape / dtype / API
|
|
69
|
-
# --------------------------------------------------------------------------- #
|
|
70
|
-
def test_transform_shape_and_dtype():
|
|
71
|
-
m = euclid().fit(structured_sequences())
|
|
72
|
-
out = m.transform([[0, 1, 2], [0, 2, 3, 4]])
|
|
73
|
-
assert out.shape == (2, 16)
|
|
74
|
-
assert out.dtype == np.float32
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_accepts_list_and_tensor_inputs():
|
|
78
|
-
seqs_t = structured_sequences()
|
|
79
|
-
seqs_l = [s.tolist() for s in seqs_t]
|
|
80
|
-
euclid().fit(seqs_t)
|
|
81
|
-
euclid().fit(seqs_l) # should not raise
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# --------------------------------------------------------------------------- #
|
|
85
|
-
# Linear additive hypothesis
|
|
86
|
-
# --------------------------------------------------------------------------- #
|
|
87
|
-
def test_additivity_exact_when_clip_off():
|
|
88
|
-
"""With clipping off and dropout off, the encoded state is *exactly* the
|
|
89
|
-
sum of constituent embeddings (the linear additive hypothesis, proved by
|
|
90
|
-
measurement)."""
|
|
91
|
-
m = euclid(max_norm=1e12).fit(structured_sequences())
|
|
92
|
-
emb = m.model.embedding.weight.detach()
|
|
93
|
-
seq = [0, 1, 2, 3]
|
|
94
|
-
h = m.transform([seq], as_numpy=False)[0]
|
|
95
|
-
ideal = emb[seq].sum(dim=0)
|
|
96
|
-
assert torch.allclose(h, ideal, atol=1e-5)
|
|
97
|
-
cos = torch.nn.functional.cosine_similarity(h[None], ideal[None]).item()
|
|
98
|
-
assert cos > 1 - 1e-5
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_additivity_is_order_invariant_euclidean():
|
|
102
|
-
"""New Result 1: the ideal Euclidean endpoint is permutation invariant."""
|
|
103
|
-
m = euclid(max_norm=1e12).fit(structured_sequences())
|
|
104
|
-
a = m.transform([[0, 1, 2, 3]], as_numpy=False)[0]
|
|
105
|
-
b = m.transform([[3, 1, 0, 2]], as_numpy=False)[0]
|
|
106
|
-
assert torch.allclose(a, b, atol=1e-5)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# --------------------------------------------------------------------------- #
|
|
110
|
-
# Reconstruction loss reaches zero at the ideal update (both geometries)
|
|
111
|
-
# --------------------------------------------------------------------------- #
|
|
112
|
-
def _one_step_losses(m, h_old):
|
|
113
|
-
m.model.eval() # disable dropout: increment == raw embedding
|
|
114
|
-
x = torch.tensor([1])
|
|
115
|
-
target = torch.tensor([2])
|
|
116
|
-
y1, h1, e1 = m.model(x, h_old)
|
|
117
|
-
_, h2, _ = m.model(x, h_old)
|
|
118
|
-
return m._compute_losses(y1, h1, e1, h_old, target, h2)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def test_reconstruction_zero_at_ideal_euclidean():
|
|
122
|
-
m = euclid(max_norm=1e12)
|
|
123
|
-
_, recon, consist = _one_step_losses(m, torch.zeros(1, 16))
|
|
124
|
-
assert recon.item() < 1e-10
|
|
125
|
-
assert consist.item() < 1e-10 # eval mode => identical passes
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_reconstruction_zero_at_ideal_hyperbolic():
|
|
129
|
-
"""The corrected left-cancellation target makes L_recon reach ~0 at the
|
|
130
|
-
ideal Möbius update (the old right-cancellation form could not)."""
|
|
131
|
-
m = hyper()
|
|
132
|
-
_, recon, _ = _one_step_losses(m, torch.zeros(1, 16))
|
|
133
|
-
assert recon.item() < 1e-6
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# --------------------------------------------------------------------------- #
|
|
137
|
-
# Hyperbolic / Möbius algebra
|
|
138
|
-
# --------------------------------------------------------------------------- #
|
|
139
|
-
def test_mobius_identity():
|
|
140
|
-
x = torch.randn(4, 8) * 0.1
|
|
141
|
-
z = torch.zeros(4, 8)
|
|
142
|
-
assert torch.allclose(HyperbolicUtils.mobius_add(x, z, 1.0), x, atol=1e-5)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_mobius_left_cancellation():
|
|
146
|
-
"""(-x) ⊕ (x ⊕ y) == y — the law the corrected recon loss relies on."""
|
|
147
|
-
x = torch.randn(4, 8) * 0.1
|
|
148
|
-
y = torch.randn(4, 8) * 0.1
|
|
149
|
-
lhs = HyperbolicUtils.mobius_add(-x, HyperbolicUtils.mobius_add(x, y, 1.0), 1.0)
|
|
150
|
-
assert torch.allclose(lhs, y, atol=1e-4)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def test_project_to_ball_respects_radius():
|
|
154
|
-
c = 1.0
|
|
155
|
-
big = torch.randn(16, 8) * 100.0
|
|
156
|
-
proj = HyperbolicUtils.project_to_ball(big, c)
|
|
157
|
-
norms = proj.norm(dim=-1)
|
|
158
|
-
assert torch.all(norms < 1.0 / math.sqrt(c))
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# --------------------------------------------------------------------------- #
|
|
162
|
-
# Hyperbolic init is NOT clobbered to std=0.02 (P0-1)
|
|
163
|
-
# --------------------------------------------------------------------------- #
|
|
164
|
-
def test_hyperbolic_init_near_origin():
|
|
165
|
-
model = HyperbolicModel(num_event_types=5, embedding_dim=64, dropout_p=0.0, c=1.0)
|
|
166
|
-
norms = model.embedding.weight.detach().norm(dim=-1)
|
|
167
|
-
# tiny uniform(-1e-3,1e-3) over d=64 => norm ~ 0.008, far below a std=0.02
|
|
168
|
-
# normal (~0.16) and well inside the ball.
|
|
169
|
-
assert norms.max().item() < 0.05
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def test_estimator_reset_keeps_hyperbolic_init_tiny():
|
|
173
|
-
m = hyper(embedding_dim=64)
|
|
174
|
-
m._reset_parameters()
|
|
175
|
-
norms = m.model.embedding.weight.detach().norm(dim=-1)
|
|
176
|
-
assert norms.max().item() < 0.05
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# --------------------------------------------------------------------------- #
|
|
180
|
-
# Consistency loss is active iff dropout > 0 (P0-3)
|
|
181
|
-
# --------------------------------------------------------------------------- #
|
|
182
|
-
def test_consistency_loss_active_with_dropout():
|
|
183
|
-
m = euclid(dropout_p=0.3)
|
|
184
|
-
m.model.train()
|
|
185
|
-
x = torch.tensor([1])
|
|
186
|
-
h_old = torch.zeros(1, 16)
|
|
187
|
-
_, h1, _ = m.model(x, h_old)
|
|
188
|
-
_, h2, _ = m.model(x, h_old)
|
|
189
|
-
assert m.mse_loss(h1, h2).item() > 0.0
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def test_consistency_loss_zero_without_dropout():
|
|
193
|
-
m = euclid(dropout_p=0.0)
|
|
194
|
-
m.model.train()
|
|
195
|
-
x = torch.tensor([1])
|
|
196
|
-
h_old = torch.zeros(1, 16)
|
|
197
|
-
_, h1, _ = m.model(x, h_old)
|
|
198
|
-
_, h2, _ = m.model(x, h_old)
|
|
199
|
-
assert m.mse_loss(h1, h2).item() == 0.0
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# --------------------------------------------------------------------------- #
|
|
203
|
-
# Training actually reduces the loss
|
|
204
|
-
# --------------------------------------------------------------------------- #
|
|
205
|
-
@pytest.mark.parametrize("geom", ["euclidean", "hyperbolic"])
|
|
206
|
-
def test_fit_improves_loss(geom):
|
|
207
|
-
builder = euclid if geom == "euclidean" else hyper
|
|
208
|
-
m = builder(num_epochs=40, learning_rate=1e-2).fit(structured_sequences())
|
|
209
|
-
assert m.training_loss_[-1] < m.training_loss_[0]
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# --------------------------------------------------------------------------- #
|
|
213
|
-
# Determinism given a seed (CPU)
|
|
214
|
-
# --------------------------------------------------------------------------- #
|
|
215
|
-
def test_determinism_given_seed_cpu():
|
|
216
|
-
seqs = structured_sequences()
|
|
217
|
-
a = euclid(dropout_p=0.2, num_epochs=6, random_state=123).fit(seqs)
|
|
218
|
-
b = euclid(dropout_p=0.2, num_epochs=6, random_state=123).fit(seqs)
|
|
219
|
-
assert np.allclose(a.training_loss_, b.training_loss_)
|
|
220
|
-
assert np.allclose(
|
|
221
|
-
a.model.embedding.weight.detach().numpy(),
|
|
222
|
-
b.model.embedding.weight.detach().numpy(),
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# --------------------------------------------------------------------------- #
|
|
227
|
-
# padded vs unpadded must agree at inference (P2-8: only the loss differed)
|
|
228
|
-
# --------------------------------------------------------------------------- #
|
|
229
|
-
def test_padded_equals_unpadded_embeddings():
|
|
230
|
-
m = euclid(num_epochs=3).fit(structured_sequences())
|
|
231
|
-
seq = [0, 1, 2, 3, 4]
|
|
232
|
-
a = m.transform([seq], pad_sequences=True, as_numpy=False)[0]
|
|
233
|
-
b = m.transform([seq], pad_sequences=False, as_numpy=False)[0]
|
|
234
|
-
assert torch.allclose(a, b, atol=1e-5)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# --------------------------------------------------------------------------- #
|
|
238
|
-
# Curvature validation + optional Riemannian optimization
|
|
239
|
-
# --------------------------------------------------------------------------- #
|
|
240
|
-
def test_curvature_must_be_positive():
|
|
241
|
-
with pytest.raises(ValueError):
|
|
242
|
-
Event2Vec(num_event_types=5, geometry="hyperbolic", curvature=-1.0)
|
|
243
|
-
with pytest.raises(ValueError):
|
|
244
|
-
Event2Vec(num_event_types=5, geometry="hyperbolic", curvature=0.0)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def test_riemannian_uses_manifold_parameter():
|
|
248
|
-
geoopt = pytest.importorskip("geoopt")
|
|
249
|
-
m = hyper(riemannian=True).fit(structured_sequences())
|
|
250
|
-
assert isinstance(m.model.embedding.weight, geoopt.ManifoldParameter)
|
|
251
|
-
norms = m.model.embedding.weight.detach().norm(dim=-1)
|
|
252
|
-
assert torch.all(norms < 1.0) # stays inside the ball
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def test_most_similar_returns_topn():
|
|
256
|
-
m = euclid().fit(structured_sequences())
|
|
257
|
-
res = m.most_similar(positive=1, topn=3)
|
|
258
|
-
assert len(res) == 3
|
|
259
|
-
assert all(isinstance(i, int) and isinstance(v, float) for i, v in res)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# --------------------------------------------------------------------------- #
|
|
263
|
-
# v0.2 geometries: stereographic (kappa), rotation, Heisenberg, product
|
|
264
|
-
# --------------------------------------------------------------------------- #
|
|
265
|
-
def _geo(geometry, **kw):
|
|
266
|
-
defaults = dict(num_event_types=5, embedding_dim=8, dropout_p=0.0, num_epochs=4,
|
|
267
|
-
batch_size=8, pad_sequences=True, random_state=0, use_gpu=False)
|
|
268
|
-
defaults.update(kw)
|
|
269
|
-
return Event2Vec(geometry=geometry, **defaults)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
ALL_NEW_GEOMS = [
|
|
273
|
-
dict(geometry="spherical", kappa=1.0),
|
|
274
|
-
dict(geometry="stereographic", kappa=-0.5),
|
|
275
|
-
dict(geometry="rotation", rotation_n=3),
|
|
276
|
-
dict(geometry="heisenberg", heisenberg_m=1),
|
|
277
|
-
dict(geometry="product", product_kappas=(-1.0, 1.0)),
|
|
278
|
-
]
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@pytest.mark.parametrize("cfg", ALL_NEW_GEOMS)
|
|
282
|
-
def test_new_geometry_fits_and_transforms(cfg):
|
|
283
|
-
m = _geo(**cfg).fit(structured_sequences())
|
|
284
|
-
emb = m.transform(structured_sequences())
|
|
285
|
-
assert emb.shape[1] == m.model.state_dim
|
|
286
|
-
assert np.isfinite(emb).all()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
@pytest.mark.parametrize("cfg", ALL_NEW_GEOMS)
|
|
290
|
-
def test_new_geometry_fit_improves_loss(cfg):
|
|
291
|
-
# Curved-geometry optimization can overshoot at a high LR (the endpoint may
|
|
292
|
-
# bounce), so we lock the honest property: training reduces the loss.
|
|
293
|
-
m = _geo(num_epochs=40, learning_rate=5e-3, **cfg).fit(structured_sequences())
|
|
294
|
-
assert min(m.training_loss_) < m.training_loss_[0] * 0.98
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def test_stereographic_kappa0_is_euclidean_addition():
|
|
298
|
-
x, y = torch.randn(4, 6), torch.randn(4, 6)
|
|
299
|
-
assert torch.allclose(StereographicUtils.add(x, y, 0.0), x + y, atol=1e-6)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def test_stereographic_negative_kappa_matches_mobius():
|
|
303
|
-
"""kappa<0 stereographic addition equals Mobius addition with c=-kappa."""
|
|
304
|
-
x, y = torch.randn(4, 6) * 0.1, torch.randn(4, 6) * 0.1
|
|
305
|
-
kappa = -1.0
|
|
306
|
-
assert torch.allclose(
|
|
307
|
-
StereographicUtils.add(x, y, kappa),
|
|
308
|
-
HyperbolicUtils.mobius_add(x, y, -kappa),
|
|
309
|
-
atol=1e-5,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
@pytest.mark.parametrize("kappa", [-1.0, -0.3, 0.5, 2.0])
|
|
314
|
-
def test_stereographic_left_cancellation(kappa):
|
|
315
|
-
"""(-x) (+)_k (x (+)_k y) == y for every curvature (the recon-loss law)."""
|
|
316
|
-
x, y = torch.randn(4, 6) * 0.1, torch.randn(4, 6) * 0.1
|
|
317
|
-
add = StereographicUtils.add
|
|
318
|
-
assert torch.allclose(add(-x, add(x, y, kappa), kappa), y, atol=1e-4)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def test_rotation_state_stays_orthogonal():
|
|
322
|
-
m = _geo("rotation", rotation_n=3, num_epochs=3).fit(structured_sequences())
|
|
323
|
-
h = m.transform([[0, 1, 2, 3, 4]], as_numpy=False)[0].reshape(3, 3)
|
|
324
|
-
assert torch.allclose(h @ h.t(), torch.eye(3), atol=1e-4)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
@pytest.mark.parametrize("cfg", ALL_NEW_GEOMS)
|
|
328
|
-
def test_new_geometry_reconstruction_zero_at_ideal(cfg):
|
|
329
|
-
"""Each geometry's left-cancellation makes L_recon ~ 0 at the ideal update."""
|
|
330
|
-
m = _geo(**cfg)
|
|
331
|
-
h0 = m.model.zero_state(1, torch.device("cpu"))
|
|
332
|
-
_, recon, _ = _one_step_losses(m, h0)
|
|
333
|
-
assert recon.item() < 1e-6
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def test_heisenberg_carries_order_information():
|
|
337
|
-
"""The Heisenberg central coordinate distinguishes AB from BA; the additive
|
|
338
|
-
(Euclidean) endpoint does not."""
|
|
339
|
-
hm = HeisenbergModel(num_event_types=5, m=1)
|
|
340
|
-
hm.eval()
|
|
341
|
-
ab = hm.forward(torch.tensor([1]), hm.forward(torch.tensor([2]),
|
|
342
|
-
hm.zero_state(1, torch.device("cpu")))[1])[1]
|
|
343
|
-
ba = hm.forward(torch.tensor([2]), hm.forward(torch.tensor([1]),
|
|
344
|
-
hm.zero_state(1, torch.device("cpu")))[1])[1]
|
|
345
|
-
# same v (abelian part) but different central z (swept area) => order-sensitive
|
|
346
|
-
assert torch.allclose(ab[:, :2], ba[:, :2], atol=1e-5)
|
|
347
|
-
assert (ab[:, 2] - ba[:, 2]).abs().item() > 1e-4
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def test_stereographic_requires_kappa():
|
|
351
|
-
with pytest.raises(ValueError):
|
|
352
|
-
Event2Vec(num_event_types=5, geometry="stereographic") # kappa missing
|
|
353
|
-
with pytest.raises(ValueError):
|
|
354
|
-
Event2Vec(num_event_types=5, geometry="spherical", kappa=-1.0) # needs kappa>0
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def test_product_has_no_single_embedding():
|
|
358
|
-
m = _geo("product", product_kappas=(-1.0, 1.0)).fit(structured_sequences())
|
|
359
|
-
with pytest.raises(NotImplementedError):
|
|
360
|
-
m.most_similar(positive=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|