event2vector 0.1.2.1__tar.gz → 0.3.0__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.
Files changed (30) hide show
  1. {event2vector-0.1.2.1 → event2vector-0.3.0}/PKG-INFO +70 -9
  2. {event2vector-0.1.2.1 → event2vector-0.3.0}/README.md +62 -3
  3. event2vector-0.3.0/event2vector/__init__.py +28 -0
  4. event2vector-0.3.0/event2vector/baselines.py +362 -0
  5. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector/data.py +13 -0
  6. event2vector-0.3.0/event2vector/datasets/__init__.py +52 -0
  7. event2vector-0.3.0/event2vector/datasets/arithmetic.py +68 -0
  8. event2vector-0.3.0/event2vector/datasets/brown.py +91 -0
  9. event2vector-0.3.0/event2vector/datasets/decay.py +146 -0
  10. event2vector-0.3.0/event2vector/datasets/groups.py +146 -0
  11. event2vector-0.3.0/event2vector/datasets/lifepath.py +199 -0
  12. event2vector-0.3.0/event2vector/datasets/realdata.py +458 -0
  13. event2vector-0.3.0/event2vector/eval/__init__.py +5 -0
  14. event2vector-0.3.0/event2vector/eval/figures.py +87 -0
  15. event2vector-0.3.0/event2vector/eval/harness.py +155 -0
  16. event2vector-0.3.0/event2vector/geometries.py +412 -0
  17. event2vector-0.3.0/event2vector/metrics.py +461 -0
  18. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector/models.py +250 -68
  19. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector.egg-info/PKG-INFO +70 -9
  20. event2vector-0.3.0/event2vector.egg-info/SOURCES.txt +26 -0
  21. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector.egg-info/requires.txt +7 -4
  22. {event2vector-0.1.2.1 → event2vector-0.3.0}/pyproject.toml +11 -6
  23. event2vector-0.3.0/tests/test_datasets.py +82 -0
  24. event2vector-0.3.0/tests/test_models.py +360 -0
  25. event2vector-0.1.2.1/event2vector/__init__.py +0 -4
  26. event2vector-0.1.2.1/event2vector.egg-info/SOURCES.txt +0 -11
  27. {event2vector-0.1.2.1 → event2vector-0.3.0}/MANIFEST.in +0 -0
  28. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector.egg-info/dependency_links.txt +0 -0
  29. {event2vector-0.1.2.1 → event2vector-0.3.0}/event2vector.egg-info/top_level.txt +0 -0
  30. {event2vector-0.1.2.1 → event2vector-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: event2vector
3
- Version: 0.1.2.1
3
+ Version: 0.3.0
4
4
  Summary: Scikit-learn-style geometric embeddings for event sequences.
5
5
  Author: Antonin Sulc
6
6
  License-Expression: MIT
@@ -12,12 +12,12 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
14
  Classifier: Intended Audience :: Science/Research
15
- Requires-Python: >=3.6
15
+ Requires-Python: >=3.9
16
16
  Description-Content-Type: text/markdown
17
- Requires-Dist: numpy
18
- Requires-Dist: torch
19
- Requires-Dist: scikit-learn
20
- Requires-Dist: tqdm
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: torch>=1.13
19
+ Requires-Dist: scikit-learn>=1.0
20
+ Requires-Dist: tqdm>=4.0
21
21
  Provides-Extra: viz
22
22
  Requires-Dist: matplotlib; extra == "viz"
23
23
  Requires-Dist: seaborn; extra == "viz"
@@ -25,6 +25,8 @@ Provides-Extra: manifold
25
25
  Requires-Dist: openTSNE; extra == "manifold"
26
26
  Provides-Extra: nlp
27
27
  Requires-Dist: gensim; extra == "nlp"
28
+ Provides-Extra: hyperbolic
29
+ Requires-Dist: geoopt>=0.5; extra == "hyperbolic"
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: pytest; extra == "dev"
30
32
  Requires-Dist: black; extra == "dev"
@@ -132,15 +134,46 @@ Key methods:
132
134
 
133
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"`).
134
136
 
137
+ ## Geometries (v0.2)
135
138
 
139
+ Beyond `euclidean` and `hyperbolic`, the estimator exposes the paper's pilot geometries as first-class `geometry=` backbones, each the natural home of a structure the additive endpoint cannot carry. They share the same estimator and a generic gyrogroup/Lie-group left-cancellation reconstruction loss.
140
+
141
+ | `geometry` | update | best for |
142
+ |---|---|---|
143
+ | `euclidean` | vector addition | order-invariant bag of events, exact decomposition |
144
+ | `hyperbolic` | Möbius addition (curvature `c>0`) | hierarchical / tree-like structure |
145
+ | `stereographic` | κ-gyroaddition (signed `kappa`) | unified curvature: `kappa<0` hyperbolic, `0` Euclidean, `>0` spherical |
146
+ | `spherical` | κ-gyroaddition (`kappa>0`) | cyclic / modular (periodic) structure |
147
+ | `rotation` | left `SO(n)` product | non-commutative orientation (order matters) |
148
+ | `heisenberg` | Heisenberg group product | order-dependent signed (Lévy) area |
149
+ | `product` | per-factor update | two structure types at once (e.g. hyperbolic × spherical) |
150
+
151
+ ```python
152
+ from event2vector import Event2Vec
153
+
154
+ # one unified curvature dial: kappa<0 hyperbolic, 0 Euclidean, >0 spherical
155
+ model = Event2Vec(num_event_types=len(vocab), geometry="stereographic", kappa=-1.0)
156
+
157
+ # non-commutative orientation (SO(3))
158
+ model = Event2Vec(num_event_types=len(vocab), geometry="rotation", rotation_n=3)
159
+
160
+ # product of a hyperbolic and a spherical factor
161
+ model = Event2Vec(num_event_types=len(vocab), geometry="product", product_kappas=(-1.0, 1.0))
162
+ ```
163
+
164
+ Reproduce the curvature sweep on a real knowledge graph (auto-downloads WN18RR):
165
+
166
+ ```bash
167
+ python3 -m scripts.run_kg_curvature # next-relation accuracy vs kappa
168
+ ```
136
169
 
137
170
  ## Brown Corpus POS tagging example
138
171
  After installation, you can try to run Brown Part-of-Speech tagging example from the paper.
139
172
 
140
173
  ```bash
141
- python3 -m experiments.prepare_brown_data.py
142
- python3 -m experiments.train_brown_data.py
143
- python3 -m experiments.visualize_brown_corpus.py
174
+ python3 -m examples.prepare_brown_data
175
+ python3 -m examples.train_brown_data
176
+ python3 -m examples.visualize_brown_corpus
144
177
  ```
145
178
 
146
179
 
@@ -154,6 +187,34 @@ python3 examples/minimal_example.py
154
187
 
155
188
  To try a hyperbolic run, open `examples/minimal_example.py` and set `geometry="hyperbolic"` in the `Event2Vec` constructor, then rerun the script.
156
189
 
190
+ ## Reproducing the paper experiments
191
+
192
+ The journal experiments are built around domains with *exact ground truth* for the
193
+ additive structure, so interpretability is measured rather than visualized:
194
+
195
+ ```bash
196
+ # Physics (nuclear decay) + Math (group sequences): exact-ground-truth centerpiece
197
+ python3 -m scripts.run_centerpiece --seeds 5 --epochs 20 --out results/centerpiece
198
+
199
+ # Synthetic life-path workbench (order-sensitivity-vs-curvature, additivity decay)
200
+ python3 -m scripts.run_lifepath --seeds 5 --out results/lifepath
201
+ ```
202
+
203
+ Datasets live in `event2vector/datasets/` and all return a uniform record
204
+ (`sequences`, `processed_sequences`, `vocab`, `labels`, `graph`):
205
+
206
+ - `make_decay()`: radioactive decay chains; each event is an exact `(ΔZ, ΔN)` so
207
+ additivity coincides with conservation laws (metric: `operator_recovery`).
208
+ - `make_group(name)`: Cayley-graph walks on abelian/non-abelian groups; the
209
+ additive endpoint provably equals the abelianization (metric: `abelianization_gap`).
210
+ - `make_lifepath()`, `load_brown()`: controlled synthetic + real (POS) workbench.
211
+ - `load_gdelt()`, `load_sec_8k()`, `load_synthea()`, ...: real-world event streams
212
+ (require a public data download; see each loader's docstring).
213
+
214
+ Metrics (`event2vector.metrics`) and the seeded harness (`event2vector.eval`) are
215
+ reusable across datasets; baselines (GRU/LSTM/Transformer/Word2Vec) are in
216
+ `event2vector.baselines`. Run the test suite with `pytest tests/ -q`.
217
+
157
218
  ## References
158
219
  For citations please use following Bibtex.
159
220
  ```bibtex
@@ -100,15 +100,46 @@ Key methods:
100
100
 
101
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"`).
102
102
 
103
+ ## Geometries (v0.2)
103
104
 
105
+ Beyond `euclidean` and `hyperbolic`, the estimator exposes the paper's pilot geometries as first-class `geometry=` backbones, each the natural home of a structure the additive endpoint cannot carry. They share the same estimator and a generic gyrogroup/Lie-group left-cancellation reconstruction loss.
106
+
107
+ | `geometry` | update | best for |
108
+ |---|---|---|
109
+ | `euclidean` | vector addition | order-invariant bag of events, exact decomposition |
110
+ | `hyperbolic` | Möbius addition (curvature `c>0`) | hierarchical / tree-like structure |
111
+ | `stereographic` | κ-gyroaddition (signed `kappa`) | unified curvature: `kappa<0` hyperbolic, `0` Euclidean, `>0` spherical |
112
+ | `spherical` | κ-gyroaddition (`kappa>0`) | cyclic / modular (periodic) structure |
113
+ | `rotation` | left `SO(n)` product | non-commutative orientation (order matters) |
114
+ | `heisenberg` | Heisenberg group product | order-dependent signed (Lévy) area |
115
+ | `product` | per-factor update | two structure types at once (e.g. hyperbolic × spherical) |
116
+
117
+ ```python
118
+ from event2vector import Event2Vec
119
+
120
+ # one unified curvature dial: kappa<0 hyperbolic, 0 Euclidean, >0 spherical
121
+ model = Event2Vec(num_event_types=len(vocab), geometry="stereographic", kappa=-1.0)
122
+
123
+ # non-commutative orientation (SO(3))
124
+ model = Event2Vec(num_event_types=len(vocab), geometry="rotation", rotation_n=3)
125
+
126
+ # product of a hyperbolic and a spherical factor
127
+ model = Event2Vec(num_event_types=len(vocab), geometry="product", product_kappas=(-1.0, 1.0))
128
+ ```
129
+
130
+ Reproduce the curvature sweep on a real knowledge graph (auto-downloads WN18RR):
131
+
132
+ ```bash
133
+ python3 -m scripts.run_kg_curvature # next-relation accuracy vs kappa
134
+ ```
104
135
 
105
136
  ## Brown Corpus POS tagging example
106
137
  After installation, you can try to run Brown Part-of-Speech tagging example from the paper.
107
138
 
108
139
  ```bash
109
- python3 -m experiments.prepare_brown_data.py
110
- python3 -m experiments.train_brown_data.py
111
- python3 -m experiments.visualize_brown_corpus.py
140
+ python3 -m examples.prepare_brown_data
141
+ python3 -m examples.train_brown_data
142
+ python3 -m examples.visualize_brown_corpus
112
143
  ```
113
144
 
114
145
 
@@ -122,6 +153,34 @@ python3 examples/minimal_example.py
122
153
 
123
154
  To try a hyperbolic run, open `examples/minimal_example.py` and set `geometry="hyperbolic"` in the `Event2Vec` constructor, then rerun the script.
124
155
 
156
+ ## Reproducing the paper experiments
157
+
158
+ The journal experiments are built around domains with *exact ground truth* for the
159
+ additive structure, so interpretability is measured rather than visualized:
160
+
161
+ ```bash
162
+ # Physics (nuclear decay) + Math (group sequences): exact-ground-truth centerpiece
163
+ python3 -m scripts.run_centerpiece --seeds 5 --epochs 20 --out results/centerpiece
164
+
165
+ # Synthetic life-path workbench (order-sensitivity-vs-curvature, additivity decay)
166
+ python3 -m scripts.run_lifepath --seeds 5 --out results/lifepath
167
+ ```
168
+
169
+ Datasets live in `event2vector/datasets/` and all return a uniform record
170
+ (`sequences`, `processed_sequences`, `vocab`, `labels`, `graph`):
171
+
172
+ - `make_decay()`: radioactive decay chains; each event is an exact `(ΔZ, ΔN)` so
173
+ additivity coincides with conservation laws (metric: `operator_recovery`).
174
+ - `make_group(name)`: Cayley-graph walks on abelian/non-abelian groups; the
175
+ additive endpoint provably equals the abelianization (metric: `abelianization_gap`).
176
+ - `make_lifepath()`, `load_brown()`: controlled synthetic + real (POS) workbench.
177
+ - `load_gdelt()`, `load_sec_8k()`, `load_synthea()`, ...: real-world event streams
178
+ (require a public data download; see each loader's docstring).
179
+
180
+ Metrics (`event2vector.metrics`) and the seeded harness (`event2vector.eval`) are
181
+ reusable across datasets; baselines (GRU/LSTM/Transformer/Word2Vec) are in
182
+ `event2vector.baselines`. Run the test suite with `pytest tests/ -q`.
183
+
125
184
  ## References
126
185
  For citations please use following Bibtex.
127
186
  ```bibtex
@@ -0,0 +1,28 @@
1
+ __version__ = "0.3.0"
2
+
3
+ from .models import EuclideanModel, HyperbolicModel, HyperbolicUtils, Event2Vec
4
+ from .geometries import (
5
+ StereographicUtils,
6
+ StereographicModel,
7
+ RotationModel,
8
+ HeisenbergModel,
9
+ ProductModel,
10
+ )
11
+ from .data import generate_sequences, get_sequences
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "Event2Vec",
16
+ "EuclideanModel",
17
+ "HyperbolicModel",
18
+ "HyperbolicUtils",
19
+ "StereographicUtils",
20
+ "StereographicModel",
21
+ "RotationModel",
22
+ "HeisenbergModel",
23
+ "ProductModel",
24
+ "generate_sequences",
25
+ "get_sequences",
26
+ ]
27
+
28
+
@@ -0,0 +1,362 @@
1
+ """Baseline sequence encoders with the same fit/transform interface as the
2
+ ``Event2Vec`` estimator, so the experiment metrics can treat every model
3
+ uniformly.
4
+
5
+ - ``RNNEncoder`` (GRU/LSTM) and ``TransformerEncoder``: order-sensitive encoders
6
+ trained as next-event predictors; ``transform`` returns the pooled hidden
7
+ state. These are the order-sensitive reference points for A2 / E1.
8
+ - ``Word2VecBagEncoder``: an order-blind bag-of-tokens baseline (mean of learned
9
+ token vectors), the Word2Vec comparison used throughout the paper.
10
+
11
+ All encoders expose ``n_params`` so comparisons can be framed per-parameter
12
+ rather than as a leaderboard.
13
+ """
14
+ import random
15
+ from typing import List, Optional, Sequence
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn as nn
20
+ from torch.nn.utils.rnn import pad_sequence
21
+
22
+
23
+ def _seed(seed):
24
+ if seed is not None:
25
+ random.seed(seed)
26
+ np.random.seed(seed)
27
+ torch.manual_seed(seed)
28
+
29
+
30
+ def _device(use_gpu):
31
+ if use_gpu and torch.cuda.is_available():
32
+ return torch.device("cuda")
33
+ if use_gpu and hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
34
+ return torch.device("mps")
35
+ return torch.device("cpu")
36
+
37
+
38
+ def _standardize(sequences) -> List[torch.Tensor]:
39
+ out = []
40
+ for s in sequences:
41
+ t = s.detach().long().cpu() if isinstance(s, torch.Tensor) else torch.tensor(s, dtype=torch.long)
42
+ out.append(t)
43
+ return out
44
+
45
+
46
+ class _NextEventEncoder(nn.Module):
47
+ """Shared next-event-prediction training loop for neural baselines."""
48
+
49
+ def fit(self, sequences, epochs=None, verbose=False):
50
+ seqs = [s for s in _standardize(sequences) if s.numel() >= 2]
51
+ opt = torch.optim.AdamW(self.parameters(), lr=self.learning_rate)
52
+ ce = nn.CrossEntropyLoss(ignore_index=self.pad_value)
53
+ self.train()
54
+ total_epochs = epochs or self.num_epochs
55
+ rng = random.Random(self.random_state)
56
+ for ep in range(total_epochs):
57
+ rng.shuffle(seqs)
58
+ running = 0.0
59
+ nb = 0
60
+ for i in range(0, len(seqs), self.batch_size):
61
+ batch = seqs[i : i + self.batch_size]
62
+ tens = [b.to(self.device) for b in batch]
63
+ padded = pad_sequence(tens, batch_first=True, padding_value=self.pad_value)
64
+ inp, tgt = padded[:, :-1], padded[:, 1:]
65
+ logits = self._logits(inp)
66
+ loss = ce(logits.reshape(-1, self.num_event_types), tgt.reshape(-1))
67
+ opt.zero_grad()
68
+ loss.backward()
69
+ opt.step()
70
+ running += float(loss.item())
71
+ nb += 1
72
+ if verbose:
73
+ print(f"epoch {ep + 1}/{total_epochs} loss {running / max(1, nb):.4f}")
74
+ self.eval()
75
+ return self
76
+
77
+ @torch.no_grad()
78
+ def transform(self, sequences, as_numpy=True, pad_sequences=None):
79
+ self.eval()
80
+ seqs = _standardize(sequences)
81
+ outs = []
82
+ for i in range(0, len(seqs), self.batch_size):
83
+ batch = seqs[i : i + self.batch_size]
84
+ tens = [b.to(self.device) for b in batch]
85
+ lengths = torch.tensor([t.size(0) for t in tens], device=self.device)
86
+ padded = pad_sequence(tens, batch_first=True, padding_value=self.pad_value)
87
+ pooled = self._pool(padded, lengths)
88
+ outs.append(pooled.cpu())
89
+ emb = torch.cat(outs, dim=0)
90
+ return emb.numpy() if as_numpy else emb
91
+
92
+ @property
93
+ def n_params(self):
94
+ return sum(p.numel() for p in self.parameters())
95
+
96
+
97
+ class RNNEncoder(_NextEventEncoder):
98
+ def __init__(self, num_event_types, embedding_dim=32, kind="gru", num_layers=1,
99
+ learning_rate=1e-3, batch_size=64, num_epochs=12, pad_value=None,
100
+ random_state=None, use_gpu=True):
101
+ super().__init__()
102
+ self.num_event_types = num_event_types
103
+ self.embedding_dim = embedding_dim
104
+ self.learning_rate = learning_rate
105
+ self.batch_size = batch_size
106
+ self.num_epochs = num_epochs
107
+ # A dedicated pad id OUTSIDE the real vocabulary. Using 0 collides with a genuine
108
+ # token (e.g. alpha-decay is 18% of the decay corpus, the first group generator is
109
+ # 33% of S4 words); that token would then be frozen at zero, excluded from the loss,
110
+ # and -- for the Transformer -- dropped from attention and pooling.
111
+ self.pad_value = num_event_types if pad_value is None else pad_value
112
+ self.random_state = random_state
113
+ self.geometry = "euclidean" # for distance_for() in metrics
114
+ self.curvature = 1.0
115
+ _seed(random_state)
116
+ self.device = _device(use_gpu)
117
+ self.embedding = nn.Embedding(num_event_types + 1, embedding_dim,
118
+ padding_idx=self.pad_value)
119
+ rnn_cls = nn.LSTM if kind.lower() == "lstm" else nn.GRU
120
+ self.rnn = rnn_cls(embedding_dim, embedding_dim, num_layers=num_layers, batch_first=True)
121
+ self.dec = nn.Linear(embedding_dim, num_event_types)
122
+ self.kind = kind.lower()
123
+ self.to(self.device)
124
+
125
+ def _logits(self, inp):
126
+ out, _ = self.rnn(self.embedding(inp))
127
+ return self.dec(out)
128
+
129
+ def _pool(self, padded, lengths):
130
+ out, _ = self.rnn(self.embedding(padded))
131
+ last = (lengths - 1).clamp_min(0)
132
+ return out[torch.arange(out.size(0), device=out.device), last]
133
+
134
+
135
+ class TransformerEncoder(_NextEventEncoder):
136
+ def __init__(self, num_event_types, embedding_dim=32, nhead=4, num_layers=2,
137
+ dim_feedforward=64, learning_rate=1e-3, batch_size=64, num_epochs=12,
138
+ pad_value=None, max_len=512, random_state=None, use_gpu=True):
139
+ super().__init__()
140
+ self.num_event_types = num_event_types
141
+ self.embedding_dim = embedding_dim
142
+ self.learning_rate = learning_rate
143
+ self.batch_size = batch_size
144
+ self.num_epochs = num_epochs
145
+ self.pad_value = num_event_types if pad_value is None else pad_value
146
+ self.random_state = random_state
147
+ self.max_len = max_len
148
+ self.geometry = "euclidean"
149
+ self.curvature = 1.0
150
+ _seed(random_state)
151
+ self.device = _device(use_gpu)
152
+ self.embedding = nn.Embedding(num_event_types + 1, embedding_dim,
153
+ padding_idx=self.pad_value)
154
+ self.pos = nn.Embedding(max_len, embedding_dim)
155
+ layer = nn.TransformerEncoderLayer(
156
+ embedding_dim, nhead, dim_feedforward, batch_first=True, dropout=0.1
157
+ )
158
+ self.encoder = nn.TransformerEncoder(layer, num_layers)
159
+ self.dec = nn.Linear(embedding_dim, num_event_types)
160
+ self.to(self.device)
161
+
162
+ def _embed(self, x):
163
+ L = x.size(1)
164
+ pos = torch.arange(L, device=x.device).clamp_max(self.max_len - 1)
165
+ return self.embedding(x) + self.pos(pos)[None]
166
+
167
+ def _logits(self, inp):
168
+ L = inp.size(1)
169
+ mask = torch.triu(torch.ones(L, L, device=inp.device), diagonal=1).bool()
170
+ key_pad = inp == self.pad_value
171
+ h = self.encoder(self._embed(inp), mask=mask, src_key_padding_mask=key_pad)
172
+ # fully-masked (all-padding) query rows can emit NaN; targets there are
173
+ # ignored in the loss, but NaN would still poison the backward pass.
174
+ return torch.nan_to_num(self.dec(h))
175
+
176
+ def _pool(self, padded, lengths):
177
+ key_pad = padded == self.pad_value
178
+ h = self.encoder(self._embed(padded), src_key_padding_mask=key_pad)
179
+ h = torch.nan_to_num(h) # padded rows may be NaN; they are zeroed by the mask below
180
+ mask = (~key_pad).float()[..., None]
181
+ return (h * mask).sum(1) / mask.sum(1).clamp_min(1.0)
182
+
183
+
184
+ class Word2VecBagEncoder:
185
+ """Order-blind baseline: mean of gensim Word2Vec token vectors."""
186
+
187
+ def __init__(self, num_event_types, embedding_dim=32, window=5, min_count=1,
188
+ num_epochs=12, random_state=0, pooling="mean", **_):
189
+ self.num_event_types = num_event_types
190
+ self.embedding_dim = embedding_dim
191
+ self.window = window
192
+ self.min_count = min_count
193
+ self.num_epochs = num_epochs
194
+ self.random_state = random_state
195
+ self.pooling = pooling
196
+ self.geometry = "euclidean"
197
+ self.curvature = 1.0
198
+ self._vectors = None
199
+
200
+ def fit(self, sequences, **_):
201
+ from gensim.models import Word2Vec
202
+
203
+ toks = [[str(int(t)) for t in (s.tolist() if isinstance(s, torch.Tensor) else s)]
204
+ for s in sequences]
205
+ model = Word2Vec(
206
+ sentences=toks, vector_size=self.embedding_dim, window=self.window,
207
+ min_count=self.min_count, workers=1, seed=self.random_state, epochs=self.num_epochs,
208
+ )
209
+ vecs = np.zeros((self.num_event_types, self.embedding_dim), dtype=np.float32)
210
+ for i in range(self.num_event_types):
211
+ key = str(i)
212
+ if key in model.wv:
213
+ vecs[i] = model.wv[key]
214
+ self._vectors = torch.tensor(vecs)
215
+ return self
216
+
217
+ def transform(self, sequences, as_numpy=True, **_):
218
+ out = []
219
+ for s in sequences:
220
+ toks = [int(t) for t in (s.tolist() if isinstance(s, torch.Tensor) else s)]
221
+ if not toks:
222
+ out.append(torch.zeros(self.embedding_dim))
223
+ continue
224
+ v = self._vectors[toks]
225
+ out.append(v.sum(0) if self.pooling == "sum" else v.mean(0))
226
+ emb = torch.stack(out)
227
+ return emb.numpy() if as_numpy else emb
228
+
229
+ @property
230
+ def n_params(self):
231
+ # Skip-Gram trains TWO V x d matrices (input `wv` and output `syn1neg`); gensim
232
+ # discards the output one after fitting. Reporting only the retained matrix would
233
+ # make this look half the size of Event2Vec at equal capacity, so count both and
234
+ # note that only V x d is kept at inference time.
235
+ return 2 * self.num_event_types * self.embedding_dim
236
+
237
+
238
+ class GraphWalkEncoder:
239
+ """Order-blind graph baselines: DeepWalk and Node2Vec.
240
+
241
+ These methods do not embed sequences directly; they embed the *nodes* of a
242
+ graph learned from random walks, exactly the ``walks as unordered context''
243
+ setting discussed in the Related Work. We build the observed event-transition
244
+ graph (nodes = event types, edge weight = count of ``i \\to j`` transitions in
245
+ the training sequences), learn node vectors by DeepWalk (uniform walks +
246
+ Skip-Gram) or Node2Vec (biased $p,q$ walks), and pool the node vectors of a
247
+ sequence's tokens into a sequence embedding. Pooling is a *sum* by default so
248
+ the comparison to Event2Vec's additive endpoint isolates the learned
249
+ representation rather than the pooling rule (``mean`` is also available).
250
+ """
251
+
252
+ def __init__(self, num_event_types, embedding_dim=32, method="deepwalk",
253
+ walk_length=20, num_walks=10, window=5, p=1.0, q=1.0,
254
+ pooling="sum", num_epochs=5, random_state=0, **_):
255
+ self.num_event_types = num_event_types
256
+ self.embedding_dim = embedding_dim
257
+ self.method = method.lower()
258
+ self.walk_length = walk_length
259
+ self.num_walks = num_walks
260
+ self.window = window
261
+ self.p = p
262
+ self.q = q
263
+ self.pooling = pooling
264
+ self.num_epochs = num_epochs
265
+ self.random_state = random_state
266
+ self.geometry = "euclidean"
267
+ self.curvature = 1.0
268
+ self._vectors = None
269
+
270
+ def _graph(self, sequences):
271
+ import networkx as nx
272
+
273
+ g = nx.DiGraph()
274
+ g.add_nodes_from(range(self.num_event_types))
275
+ for s in sequences:
276
+ toks = [int(t) for t in (s.tolist() if isinstance(s, torch.Tensor) else s)]
277
+ for a, b in zip(toks[:-1], toks[1:]):
278
+ w = g[a][b]["weight"] + 1.0 if g.has_edge(a, b) else 1.0
279
+ g.add_edge(a, b, weight=w)
280
+ return g
281
+
282
+ def _deepwalk_walks(self, g):
283
+ rng = random.Random(self.random_state)
284
+ nodes = [n for n in g.nodes() if g.out_degree(n) > 0]
285
+ walks = []
286
+ for _ in range(self.num_walks):
287
+ rng.shuffle(nodes)
288
+ for start in nodes:
289
+ walk = [start]
290
+ while len(walk) < self.walk_length:
291
+ cur = walk[-1]
292
+ nbrs = list(g.successors(cur))
293
+ if not nbrs:
294
+ break
295
+ wts = np.array([g[cur][n]["weight"] for n in nbrs], dtype=np.float64)
296
+ walk.append(nbrs[int(rng.choices(range(len(nbrs)), weights=wts)[0])])
297
+ walks.append([str(n) for n in walk])
298
+ return walks
299
+
300
+ def fit(self, sequences, **_):
301
+ g = self._graph(sequences)
302
+ if self.method == "node2vec":
303
+ from node2vec import Node2Vec
304
+
305
+ n2v = Node2Vec(g, dimensions=self.embedding_dim, walk_length=self.walk_length,
306
+ num_walks=self.num_walks, p=self.p, q=self.q, weight_key="weight",
307
+ workers=1, quiet=True, seed=self.random_state)
308
+ wv = n2v.fit(window=self.window, min_count=0, sg=1, seed=self.random_state,
309
+ epochs=self.num_epochs).wv
310
+ else: # deepwalk = uniform random walks + Skip-Gram
311
+ from gensim.models import Word2Vec
312
+
313
+ walks = self._deepwalk_walks(g)
314
+ wv = Word2Vec(walks, vector_size=self.embedding_dim, window=self.window,
315
+ min_count=0, sg=1, workers=1, seed=self.random_state,
316
+ epochs=self.num_epochs).wv
317
+ vecs = np.zeros((self.num_event_types, self.embedding_dim), dtype=np.float32)
318
+ for i in range(self.num_event_types):
319
+ if str(i) in wv:
320
+ vecs[i] = wv[str(i)]
321
+ self._vectors = torch.tensor(vecs)
322
+ return self
323
+
324
+ def transform(self, sequences, as_numpy=True, **_):
325
+ out = []
326
+ for s in sequences:
327
+ toks = [int(t) for t in (s.tolist() if isinstance(s, torch.Tensor) else s)]
328
+ if not toks:
329
+ out.append(torch.zeros(self.embedding_dim))
330
+ continue
331
+ v = self._vectors[toks]
332
+ out.append(v.sum(0) if self.pooling == "sum" else v.mean(0))
333
+ emb = torch.stack(out)
334
+ return emb.numpy() if as_numpy else emb
335
+
336
+ @property
337
+ def n_params(self):
338
+ return 2 * self.num_event_types * self.embedding_dim # Skip-Gram: input + output matrices
339
+
340
+
341
+ def build_baseline(name: str, num_event_types: int, **kw):
342
+ """Construct a baseline by name.
343
+
344
+ Note on pooling: ``Word2VecBagEncoder``/``GraphWalkEncoder`` default to *sum*
345
+ pooling here, matching \methodtitle{}'s endpoint (which is a sum, not a mean).
346
+ Mean pooling rescales every sequence by 1/L and so measures something different;
347
+ it stays available via ``pooling="mean"`` but is no longer what a caller gets by
348
+ forgetting to pass the argument -- the paper's tables are all sum-pooled.
349
+ """
350
+ name = name.lower()
351
+ if name in ("word2vec", "word2vec_bag", "bag", "deepwalk", "node2vec"):
352
+ kw.setdefault("pooling", "sum")
353
+ if name in ("gru", "lstm"):
354
+ return RNNEncoder(num_event_types, kind=name, **kw)
355
+ if name == "transformer":
356
+ return TransformerEncoder(num_event_types, **kw)
357
+ if name in ("word2vec", "word2vec_bag", "bag"):
358
+ return Word2VecBagEncoder(num_event_types, **kw)
359
+ if name in ("deepwalk", "node2vec"):
360
+ method = "node2vec" if name == "node2vec" else "deepwalk"
361
+ return GraphWalkEncoder(num_event_types, method=method, **kw)
362
+ raise ValueError(f"unknown baseline: {name}")
@@ -7,6 +7,19 @@ def generate_sequences(event_types, event_transitions, initial, terminal, num_se
7
7
  Generates synthetic sequences of events based on a state transition graph.
8
8
  This function simulates realistic life trajectories by performing a guided
9
9
  random walk on the graph.
10
+
11
+ Note the walk is only 90% faithful to ``event_transitions``: with probability
12
+ 0.1 (and always from a node with no outgoing edges) it teleports to a uniformly
13
+ chosen event type. That noise is deliberate, but it has two consequences worth
14
+ stating, because the record returned by ``make_lifepath`` advertises the graph
15
+ as ground truth:
16
+
17
+ * roughly 10% of observed transitions are NOT edges of the graph, so the
18
+ graph explains ~90% of the data, not all of it;
19
+ * event types unreachable from ``initial`` still occur in the data. In the
20
+ lifepath graph 5 of the 64 types (adoption, inheritance, job_loss,
21
+ military_service, widowhood) are unreachable from ``birth`` and appear
22
+ only via this teleport.
10
23
  """
11
24
  sequences = []
12
25
  for _ in range(num_seq):
@@ -0,0 +1,52 @@
1
+ """Dataset loaders returning a uniform record.
2
+
3
+ Each loader returns a dict with at least ``sequences`` (raw token sequences),
4
+ ``processed_sequences`` (1-D ``LongTensor``s), ``vocab``/``inv_vocab``, and,
5
+ where available, ``labels`` (per-event-id ground-truth label) and ``graph``
6
+ (a transition graph for ground-truth structural evaluation).
7
+ """
8
+ from .arithmetic import make_arithmetic
9
+ from .brown import load_brown
10
+ from .decay import make_decay
11
+ from .groups import make_group
12
+ from .lifepath import (
13
+ EVENT_TYPES,
14
+ INITIAL,
15
+ LIFEPATH_DOMAINS,
16
+ LIFEPATH_TRANSITIONS,
17
+ TERMINAL,
18
+ make_lifepath,
19
+ )
20
+ from .realdata import (
21
+ load_event_log,
22
+ load_gdelt,
23
+ load_moleculenet,
24
+ load_retailrocket,
25
+ load_sec_8k,
26
+ load_stackoverflow,
27
+ load_synthea,
28
+ load_wn18rr,
29
+ tokenize_smiles,
30
+ )
31
+
32
+ __all__ = [
33
+ "make_arithmetic",
34
+ "make_lifepath",
35
+ "LIFEPATH_TRANSITIONS",
36
+ "LIFEPATH_DOMAINS",
37
+ "EVENT_TYPES",
38
+ "INITIAL",
39
+ "TERMINAL",
40
+ "make_decay",
41
+ "make_group",
42
+ "load_brown",
43
+ "load_event_log",
44
+ "load_gdelt",
45
+ "load_moleculenet",
46
+ "tokenize_smiles",
47
+ "load_retailrocket",
48
+ "load_sec_8k",
49
+ "load_synthea",
50
+ "load_stackoverflow",
51
+ "load_wn18rr",
52
+ ]