pavi-pyro 0.1.0__py3-none-any.whl
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.
- pavi/__init__.py +8 -0
- pavi/core/__init__.py +12 -0
- pavi/core/encoding_manager.py +436 -0
- pavi/core/pavi_autoguide.py +380 -0
- pavi/core/transform_scheme.py +377 -0
- pavi/core/utils.py +27 -0
- pavi/data/__init__.py +0 -0
- pavi/data/generic.py +193 -0
- pavi/distributions/__init__.py +64 -0
- pavi/distributions/_pyro_base.py +27 -0
- pavi/distributions/affine_modules.py +605 -0
- pavi/distributions/householder_modules.py +115 -0
- pavi/distributions/power_spherical.py +272 -0
- pavi/distributions/pyro_factories.py +62 -0
- pavi/distributions/zuco_modules.py +96 -0
- pavi/inference/__init__.py +1 -0
- pavi/inference/callbacks.py +289 -0
- pavi/inference/lightning_module.py +275 -0
- pavi/inference/utils.py +45 -0
- pavi/py.typed +0 -0
- pavi_pyro-0.1.0.dist-info/METADATA +226 -0
- pavi_pyro-0.1.0.dist-info/RECORD +24 -0
- pavi_pyro-0.1.0.dist-info/WHEEL +4 -0
- pavi_pyro-0.1.0.dist-info/licenses/LICENSE +21 -0
pavi/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""PAVI: plate-amortized variational inference for Pyro models."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("pavi-pyro")
|
|
7
|
+
except PackageNotFoundError: # Not installed, e.g. imported from a source checkout
|
|
8
|
+
__version__ = "unknown"
|
pavi/core/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .encoding_manager import EncodingManager, FactorizedEM, JointEM
|
|
2
|
+
from .pavi_autoguide import PAVIGuide
|
|
3
|
+
from .transform_scheme import CascadingScheme, NormalizingFlowScheme
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"PAVIGuide",
|
|
7
|
+
"CascadingScheme",
|
|
8
|
+
"NormalizingFlowScheme",
|
|
9
|
+
"EncodingManager",
|
|
10
|
+
"JointEM",
|
|
11
|
+
"FactorizedEM",
|
|
12
|
+
]
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Encoding managers: the per-instance encodings of PAVI.
|
|
2
|
+
|
|
3
|
+
In PAVI, the posterior of each instance of a site (random variable) inside a
|
|
4
|
+
plate is a normalizing flow whose transforms are shared across the plate and
|
|
5
|
+
conditioned on a small learnable **encoding** of the instance. The encoding
|
|
6
|
+
encodes the statistics of the instance's posterior, which is much cheaper than
|
|
7
|
+
storing these statistics for every instance (as in the mean-field
|
|
8
|
+
approximation). Encoding managers create these encodings and give each site its
|
|
9
|
+
context vector. They differ in how the encoding of an instance in several
|
|
10
|
+
plates (e.g. subject ``s`` and vertex ``n``) is built:
|
|
11
|
+
|
|
12
|
+
- ``JointEM``: every instance has its own encoding ``E^{s,n}``, stored in one
|
|
13
|
+
table per plate combination (size: product of the plate sizes).
|
|
14
|
+
- ``FactorizedEM``: the encoding is built from one encoding per plate,
|
|
15
|
+
concatenated ``[E^s, E^n]`` or summed ``E^s + E^n`` (size: sum of the plate
|
|
16
|
+
sizes).
|
|
17
|
+
|
|
18
|
+
Both managers share the same options:
|
|
19
|
+
|
|
20
|
+
- ``share_across_sites``: whether sites in the same plates reuse the same
|
|
21
|
+
encodings (default) or each site gets its own tables.
|
|
22
|
+
- ``sparse`` and ``device_params``: sparse gradients and storage of the tables
|
|
23
|
+
on another device (e.g. CPU memory) to save GPU memory.
|
|
24
|
+
- ``expose``: restrict the manager to some sites.
|
|
25
|
+
|
|
26
|
+
Encodings are ``torch.nn.Embedding`` tables stored in ``guide.encodings``, with
|
|
27
|
+
stable parameter names: ``encodings.<plates>.weight`` for shared tables and
|
|
28
|
+
``encodings.<site>.<plates>.weight`` for per-site tables, where ``<plates>`` is
|
|
29
|
+
the names of the plates of the table joined by ``|``, from the outermost to the
|
|
30
|
+
innermost plate (e.g. ``encodings.subjects|vertices.weight``).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from collections.abc import Iterable, Mapping
|
|
35
|
+
from typing import Literal, cast
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import torch
|
|
39
|
+
from pyro.infer.autoguide import AutoGuide
|
|
40
|
+
from pyro.nn.module import PyroModule
|
|
41
|
+
|
|
42
|
+
PlateKey = str | Iterable[str]
|
|
43
|
+
"""A plate, or a combination of plates: a name, names joined by ``|``, or an iterable of names."""
|
|
44
|
+
|
|
45
|
+
EncodingSizeType = int | Mapping[PlateKey, int]
|
|
46
|
+
"""Encoding size specification: either a single integer applied to every table,
|
|
47
|
+
or a mapping from plates (or plate combinations) to encoding sizes."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _to_plates(key: PlateKey) -> tuple[str, ...]:
|
|
51
|
+
"""Convert a plate key (``"a"``, ``"a|b"`` or ``("a", "b")``) to a tuple of plate names."""
|
|
52
|
+
if isinstance(key, str):
|
|
53
|
+
return tuple(key.split("|"))
|
|
54
|
+
return tuple(key)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EncodingManager(ABC):
|
|
58
|
+
"""Base class of the encoding managers.
|
|
59
|
+
|
|
60
|
+
An encoding manager stores encoding tables, i.e. ``torch.nn.Embedding``
|
|
61
|
+
modules with one row per instance of a plate (or of a plate combination),
|
|
62
|
+
and builds the context vector of each site from them. Subclasses define
|
|
63
|
+
which tables a site uses (``_site_tables``) and how they are combined
|
|
64
|
+
(``_combine``).
|
|
65
|
+
|
|
66
|
+
The manager is tightly coupled with ``PAVIGuide``: during guide
|
|
67
|
+
initialization, ``init_encodings()`` creates the tables in
|
|
68
|
+
``guide.encodings``, and ``get_encodings()`` returns the encodings of the
|
|
69
|
+
plate instances selected by ``idx``, which condition the transforms.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
encoding_size: The size of the encodings: an ``int`` for every table, or
|
|
73
|
+
a mapping keyed by plates (see the subclasses for the accepted keys).
|
|
74
|
+
A size of 0 disables the corresponding encodings.
|
|
75
|
+
share_across_sites: If ``True`` (default), sites in the same plates
|
|
76
|
+
reuse the same tables. If ``False``, each site gets its own tables.
|
|
77
|
+
expose: Optional list of the sites this manager handles. ``None``
|
|
78
|
+
(default) means all the sites in plates. Other sites get no
|
|
79
|
+
encodings (context of size 0).
|
|
80
|
+
sparse: If ``True``, embedding gradients are sparse: only the encodings
|
|
81
|
+
of the current mini-batch are updated, which saves GPU memory.
|
|
82
|
+
Optimize them with ``torch.optim.SparseAdam`` (``InferenceModule``
|
|
83
|
+
does it automatically).
|
|
84
|
+
device_params: Device of the tables, e.g. ``"cpu"`` to keep large tables
|
|
85
|
+
out of GPU memory while the rest of the guide runs on the GPU.
|
|
86
|
+
``None`` (default) uses the device of the guide.
|
|
87
|
+
init_scale: Standard deviation of the Normal initialization of the
|
|
88
|
+
encodings.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
encoding_size: The encoding size specification, with keys converted to
|
|
92
|
+
tuples of plate names.
|
|
93
|
+
site_tables: Maps each managed site to the list of its tables, as
|
|
94
|
+
``(path, plates)`` pairs: the path of the table in
|
|
95
|
+
``guide.encodings`` and the plates indexing its rows.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
encoding_size: EncodingSizeType,
|
|
101
|
+
*,
|
|
102
|
+
share_across_sites: bool = True,
|
|
103
|
+
expose: list[str] | None = None,
|
|
104
|
+
sparse: bool = False,
|
|
105
|
+
device_params: str | torch.device | None = None,
|
|
106
|
+
init_scale: float = 0.1,
|
|
107
|
+
) -> None:
|
|
108
|
+
if isinstance(encoding_size, Mapping):
|
|
109
|
+
self.encoding_size: int | dict[tuple[str, ...], int] = {_to_plates(k): v for k, v in encoding_size.items()}
|
|
110
|
+
elif isinstance(encoding_size, int):
|
|
111
|
+
self.encoding_size = encoding_size
|
|
112
|
+
else:
|
|
113
|
+
raise TypeError(f"`encoding_size` must be an int or a mapping, got {type(encoding_size).__name__}.")
|
|
114
|
+
|
|
115
|
+
self.share_across_sites = share_across_sites
|
|
116
|
+
self.expose = expose
|
|
117
|
+
self.sparse = sparse
|
|
118
|
+
self.device_params = device_params
|
|
119
|
+
self.init_scale = init_scale
|
|
120
|
+
self.site_tables: dict[str, list[tuple[tuple[str, ...], tuple[str, ...]]]] = {}
|
|
121
|
+
self._table_sizes: dict[tuple[str, ...], int] = {}
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def _site_tables(self, plates: tuple[str, ...]) -> list[tuple[str, ...]]:
|
|
125
|
+
"""Return the plates of each table used by a site in ``plates`` (outermost plate first)."""
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def _table_size(self, plates: tuple[str, ...], name_site: str) -> int:
|
|
130
|
+
"""Return the encoding size of the table indexed by ``plates``."""
|
|
131
|
+
raise NotImplementedError
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def _combine(self, encodings: list[torch.Tensor]) -> torch.Tensor:
|
|
135
|
+
"""Combine the (broadcast) encodings of the tables of a site into its context."""
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def _context_size(self, sizes: list[int], name_site: str) -> int:
|
|
140
|
+
"""Return the context size of a site whose tables have the given sizes.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If the sizes cannot be combined.
|
|
144
|
+
"""
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
def init_encodings(self, guide: AutoGuide, *args, **kwargs) -> None:
|
|
148
|
+
"""Create the encoding tables in ``guide.encodings``.
|
|
149
|
+
|
|
150
|
+
Uses ``guide.sample_plate``, ``guide.plate_size``, ``guide.plate_dim`` and
|
|
151
|
+
``guide.device``. Called once by ``PAVIGuide.__init__``.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
guide: The guide onto which the tables are registered.
|
|
155
|
+
*args: Unused.
|
|
156
|
+
**kwargs: Unused.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
KeyError: If ``encoding_size`` has no entry for a table needed by a site.
|
|
160
|
+
"""
|
|
161
|
+
device = self.device_params if self.device_params is not None else guide.device
|
|
162
|
+
guide.encodings = PyroModule(name="encodings")
|
|
163
|
+
self.site_tables = {}
|
|
164
|
+
self._table_sizes = {}
|
|
165
|
+
|
|
166
|
+
for name_site, site_plates in guide.sample_plate.items():
|
|
167
|
+
if self.expose is not None and name_site not in self.expose:
|
|
168
|
+
continue
|
|
169
|
+
# Order plates from the outermost to the innermost one, so that tables
|
|
170
|
+
# do not depend on the order of the plates in the model.
|
|
171
|
+
plates = tuple(sorted(site_plates, key=lambda p: guide.plate_dim[p]))
|
|
172
|
+
|
|
173
|
+
tables: list[tuple[tuple[str, ...], tuple[str, ...]]] = []
|
|
174
|
+
for table_plates in self._site_tables(plates):
|
|
175
|
+
size = self._table_size(table_plates, name_site)
|
|
176
|
+
if size <= 0:
|
|
177
|
+
continue
|
|
178
|
+
key = "|".join(table_plates)
|
|
179
|
+
path: tuple[str, ...] = (key,) if self.share_across_sites else (name_site, key)
|
|
180
|
+
if path not in self._table_sizes:
|
|
181
|
+
self._add_table(guide, path, int(np.prod([guide.plate_size[p] for p in table_plates])), size, device)
|
|
182
|
+
self._table_sizes[path] = size
|
|
183
|
+
tables.append((path, table_plates))
|
|
184
|
+
|
|
185
|
+
if tables:
|
|
186
|
+
self._context_size([self._table_sizes[path] for path, _ in tables], name_site) # Validate the sizes
|
|
187
|
+
self.site_tables[name_site] = tables
|
|
188
|
+
|
|
189
|
+
def _add_table(self, guide: AutoGuide, path: tuple[str, ...], num_rows: int, size: int, device) -> None:
|
|
190
|
+
"""Create an embedding table and register it at ``path`` under ``guide.encodings``."""
|
|
191
|
+
parent = guide.encodings
|
|
192
|
+
for name in path[:-1]:
|
|
193
|
+
if not hasattr(parent, name):
|
|
194
|
+
setattr(parent, name, PyroModule(name=name))
|
|
195
|
+
parent = getattr(parent, name)
|
|
196
|
+
|
|
197
|
+
# PyroModule[...] builds a subclass of its argument dynamically, which type checkers cannot see.
|
|
198
|
+
embedding_cls = cast(type[torch.nn.Embedding], PyroModule[torch.nn.Embedding])
|
|
199
|
+
table = embedding_cls(num_embeddings=num_rows, embedding_dim=size, sparse=self.sparse, device=device)
|
|
200
|
+
with torch.no_grad():
|
|
201
|
+
table.weight.normal_(0.0, self.init_scale)
|
|
202
|
+
if self.device_params is not None:
|
|
203
|
+
# Read by InferenceModule._apply to keep the table on this device (a plain attribute, not a tensor or module).
|
|
204
|
+
table.device_params = self.device_params # type: ignore[assignment]
|
|
205
|
+
setattr(parent, path[-1], table)
|
|
206
|
+
|
|
207
|
+
def _get_table(self, guide: AutoGuide, path: tuple[str, ...]) -> torch.nn.Embedding:
|
|
208
|
+
module = guide.encodings
|
|
209
|
+
for name in path:
|
|
210
|
+
module = getattr(module, name)
|
|
211
|
+
return module
|
|
212
|
+
|
|
213
|
+
def get_encodings(
|
|
214
|
+
self,
|
|
215
|
+
guide: AutoGuide,
|
|
216
|
+
name_sample: str,
|
|
217
|
+
idx: dict | None = None,
|
|
218
|
+
device: str | torch.device | None = None,
|
|
219
|
+
*args,
|
|
220
|
+
**kwargs,
|
|
221
|
+
) -> torch.Tensor:
|
|
222
|
+
"""Return the context of a site for the plate instances selected by ``idx``.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
guide: The guide holding the encoding tables.
|
|
226
|
+
name_sample: The name of the site.
|
|
227
|
+
idx: Optional mapping from plate names to subsample indices. Plates
|
|
228
|
+
missing from ``idx`` (or mapped to ``None``) use all their instances.
|
|
229
|
+
device: Device of the output. Defaults to ``guide.device``.
|
|
230
|
+
*args: Unused.
|
|
231
|
+
**kwargs: Unused.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
A tensor of shape ``(batch_dims..., context_size)``, broadcastable to
|
|
235
|
+
the batch shape of the site, or an empty tensor if the site has no
|
|
236
|
+
encodings.
|
|
237
|
+
"""
|
|
238
|
+
if device is None:
|
|
239
|
+
device = guide.device
|
|
240
|
+
if idx is not None and not isinstance(idx, dict):
|
|
241
|
+
raise TypeError("`idx` must be a dict mapping plate names to indices.")
|
|
242
|
+
|
|
243
|
+
tables = self.site_tables.get(name_sample)
|
|
244
|
+
if not tables:
|
|
245
|
+
return torch.tensor([], device=device)
|
|
246
|
+
|
|
247
|
+
site_plates = guide.sample_plate[name_sample]
|
|
248
|
+
len_batch_shape = max(-guide.plate_dim[p] for p in site_plates)
|
|
249
|
+
plate2idx = {p: (idx or {}).get(p) for p in site_plates}
|
|
250
|
+
|
|
251
|
+
# Batch shape of the site: plate sizes at their plate dims, 1 elsewhere.
|
|
252
|
+
batch_shape = [1] * len_batch_shape
|
|
253
|
+
for plate in site_plates:
|
|
254
|
+
indices = plate2idx[plate]
|
|
255
|
+
batch_shape[guide.plate_dim[plate]] = guide.plate_size[plate] if indices is None else len(indices)
|
|
256
|
+
|
|
257
|
+
encodings = []
|
|
258
|
+
for path, table_plates in tables:
|
|
259
|
+
table = self._get_table(guide, path)
|
|
260
|
+
encoding = self._lookup(guide, table, table_plates, plate2idx, len_batch_shape)
|
|
261
|
+
encodings.append(encoding.to(device).expand(*batch_shape, table.embedding_dim))
|
|
262
|
+
return self._combine(encodings)
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _lookup(guide: AutoGuide, table: torch.nn.Embedding, plates: tuple[str, ...], plate2idx: dict, len_batch_shape: int) -> torch.Tensor:
|
|
266
|
+
"""Look up the rows of ``table`` and reshape them to the plate dims of ``plates``."""
|
|
267
|
+
table_device = table.weight.device
|
|
268
|
+
indices = []
|
|
269
|
+
for plate in plates:
|
|
270
|
+
if plate2idx[plate] is None:
|
|
271
|
+
indices.append(torch.arange(guide.plate_size[plate], device=table_device))
|
|
272
|
+
elif isinstance(plate2idx[plate], torch.Tensor):
|
|
273
|
+
indices.append(plate2idx[plate].to(device=table_device, dtype=torch.long))
|
|
274
|
+
else:
|
|
275
|
+
indices.append(torch.as_tensor(np.asarray(plate2idx[plate]), device=table_device, dtype=torch.long))
|
|
276
|
+
|
|
277
|
+
# Row-major linearization of the multi-plate indices: plates are ordered
|
|
278
|
+
# from the outermost to the innermost, as the rows of the table.
|
|
279
|
+
flat_idx = torch.zeros((), dtype=torch.long, device=table_device)
|
|
280
|
+
stride = 1
|
|
281
|
+
for i in reversed(range(len(plates))):
|
|
282
|
+
shape = [1] * len(plates)
|
|
283
|
+
shape[i] = -1
|
|
284
|
+
flat_idx = flat_idx + indices[i].reshape(shape) * stride
|
|
285
|
+
stride *= guide.plate_size[plates[i]]
|
|
286
|
+
|
|
287
|
+
# Plates are sorted by plate dim, so the looked-up axes keep their order
|
|
288
|
+
# when the singleton dims of the other plates are inserted.
|
|
289
|
+
shape = [1] * len_batch_shape
|
|
290
|
+
for plate, plate_indices in zip(plates, indices, strict=True):
|
|
291
|
+
shape[guide.plate_dim[plate]] = len(plate_indices)
|
|
292
|
+
return table(flat_idx).reshape(*shape, table.embedding_dim)
|
|
293
|
+
|
|
294
|
+
def get_sample_encodings_size(self, guide: AutoGuide, name_sample: str) -> int:
|
|
295
|
+
"""Return the context size of a site (0 if the site has no encodings).
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
guide: The guide (unused, kept for API consistency).
|
|
299
|
+
name_sample: The name of the site.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
The size of the last dimension of ``get_encodings(guide, name_sample)``.
|
|
303
|
+
"""
|
|
304
|
+
tables = self.site_tables.get(name_sample)
|
|
305
|
+
if not tables:
|
|
306
|
+
return 0
|
|
307
|
+
return self._context_size([self._table_sizes[path] for path, _ in tables], name_sample)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class JointEM(EncodingManager):
|
|
311
|
+
"""One encoding per instance, for every plate combination.
|
|
312
|
+
|
|
313
|
+
Every instance of a site gets its own encoding, even for sites in several
|
|
314
|
+
plates: the table of a plate combination has as many rows as the product of
|
|
315
|
+
the plate sizes (e.g. subjects × vertices). This is the most flexible
|
|
316
|
+
manager, and the natural choice for sites in a single plate; for nested
|
|
317
|
+
plates, the table can become very large (see ``FactorizedEM``).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
encoding_size: An ``int`` (the same for every plate combination) or a
|
|
321
|
+
mapping keyed by plate combinations, with plate names joined by
|
|
322
|
+
``|`` in any order (e.g. ``{"subjects": 8, "subjects|vertices": 4}``).
|
|
323
|
+
A size of 0 disables the encodings of that combination.
|
|
324
|
+
share_across_sites: If ``True`` (default), sites in the same plates share
|
|
325
|
+
one table. If ``False``, each site gets its own table.
|
|
326
|
+
**kwargs: ``expose``, ``sparse``, ``device_params`` and ``init_scale``,
|
|
327
|
+
see ``EncodingManager``.
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
```py
|
|
331
|
+
JointEM({"subjects": 8, "subjects|vertices": 4})
|
|
332
|
+
# sites in `subjects` use `encodings.subjects.weight` (S rows of size 8),
|
|
333
|
+
# sites in both plates use `encodings.subjects|vertices.weight` (S × N rows of size 4)
|
|
334
|
+
```
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def __init__(self, encoding_size: EncodingSizeType, *, share_across_sites: bool = True, **kwargs) -> None:
|
|
338
|
+
super().__init__(encoding_size, share_across_sites=share_across_sites, **kwargs)
|
|
339
|
+
if isinstance(self.encoding_size, dict):
|
|
340
|
+
sizes: dict[frozenset[str], int] = {}
|
|
341
|
+
for plates, size in self.encoding_size.items():
|
|
342
|
+
if frozenset(plates) in sizes:
|
|
343
|
+
raise ValueError(f"Plate combination {'|'.join(plates)} is given twice in `encoding_size`.")
|
|
344
|
+
sizes[frozenset(plates)] = size
|
|
345
|
+
self._sizes = sizes
|
|
346
|
+
|
|
347
|
+
def _site_tables(self, plates: tuple[str, ...]) -> list[tuple[str, ...]]:
|
|
348
|
+
return [plates]
|
|
349
|
+
|
|
350
|
+
def _table_size(self, plates: tuple[str, ...], name_site: str) -> int:
|
|
351
|
+
if isinstance(self.encoding_size, int):
|
|
352
|
+
return self.encoding_size
|
|
353
|
+
try:
|
|
354
|
+
return self._sizes[frozenset(plates)]
|
|
355
|
+
except KeyError:
|
|
356
|
+
raise KeyError(
|
|
357
|
+
f"Plate combination {'|'.join(plates)!r} of site {name_site!r} is missing in `encoding_size`.",
|
|
358
|
+
) from None
|
|
359
|
+
|
|
360
|
+
def _combine(self, encodings: list[torch.Tensor]) -> torch.Tensor:
|
|
361
|
+
return encodings[0]
|
|
362
|
+
|
|
363
|
+
def _context_size(self, sizes: list[int], name_site: str) -> int:
|
|
364
|
+
return sizes[0]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class FactorizedEM(EncodingManager):
|
|
368
|
+
"""Encodings built from one encoding per plate.
|
|
369
|
+
|
|
370
|
+
The encoding of an instance in several plates combines per-plate encodings,
|
|
371
|
+
e.g. ``[E^s, E^n]`` (``agg_type="concat"``) or ``E^s + E^n``
|
|
372
|
+
(``agg_type="sum"``) for subject ``s`` and vertex ``n``. Only the sum of the
|
|
373
|
+
plate sizes is stored, instead of their product with ``JointEM``. With
|
|
374
|
+
``share_across_sites=True`` (default), every site of a plate reuses the same
|
|
375
|
+
plate encodings: this is the configuration of the PAVI brain application,
|
|
376
|
+
whose subject encodings are refitted for new subjects.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
encoding_size: An ``int`` (the same for every plate) or a mapping keyed
|
|
380
|
+
by single plate names (e.g. ``{"subjects": 120, "vertices": 8}``).
|
|
381
|
+
A size of 0 disables the encodings of that plate.
|
|
382
|
+
agg_type: How per-plate encodings are combined: ``"concat"`` (default;
|
|
383
|
+
context size = sum of the plate sizes) or ``"sum"`` (all the plates
|
|
384
|
+
of a site must have the same encoding size).
|
|
385
|
+
share_across_sites: If ``True`` (default), all sites of a plate share its
|
|
386
|
+
table. If ``False``, each site gets its own table per plate.
|
|
387
|
+
**kwargs: ``expose``, ``sparse``, ``device_params`` and ``init_scale``,
|
|
388
|
+
see ``EncodingManager``.
|
|
389
|
+
|
|
390
|
+
Example:
|
|
391
|
+
```py
|
|
392
|
+
FactorizedEM({"subjects": 120, "vertices": 8})
|
|
393
|
+
# tables `encodings.subjects.weight` (S × 120) and `encodings.vertices.weight` (N × 8);
|
|
394
|
+
# a site in both plates gets a context of size 128
|
|
395
|
+
```
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def __init__(
|
|
399
|
+
self,
|
|
400
|
+
encoding_size: EncodingSizeType,
|
|
401
|
+
*,
|
|
402
|
+
agg_type: Literal["concat", "sum"] = "concat",
|
|
403
|
+
share_across_sites: bool = True,
|
|
404
|
+
**kwargs,
|
|
405
|
+
) -> None:
|
|
406
|
+
super().__init__(encoding_size, share_across_sites=share_across_sites, **kwargs)
|
|
407
|
+
if isinstance(self.encoding_size, dict):
|
|
408
|
+
for plates in self.encoding_size:
|
|
409
|
+
if len(plates) != 1:
|
|
410
|
+
raise ValueError(f"FactorizedEM takes one plate per `encoding_size` key, got {'|'.join(plates)!r}.")
|
|
411
|
+
if agg_type not in ("concat", "sum"):
|
|
412
|
+
raise ValueError("`agg_type` must be 'concat' or 'sum'.")
|
|
413
|
+
self.agg_type = agg_type
|
|
414
|
+
|
|
415
|
+
def _site_tables(self, plates: tuple[str, ...]) -> list[tuple[str, ...]]:
|
|
416
|
+
return [(plate,) for plate in plates]
|
|
417
|
+
|
|
418
|
+
def _table_size(self, plates: tuple[str, ...], name_site: str) -> int:
|
|
419
|
+
if isinstance(self.encoding_size, int):
|
|
420
|
+
return self.encoding_size
|
|
421
|
+
try:
|
|
422
|
+
return self.encoding_size[plates]
|
|
423
|
+
except KeyError:
|
|
424
|
+
raise KeyError(f"Plate {plates[0]!r} of site {name_site!r} is missing in `encoding_size`.") from None
|
|
425
|
+
|
|
426
|
+
def _combine(self, encodings: list[torch.Tensor]) -> torch.Tensor:
|
|
427
|
+
if self.agg_type == "concat":
|
|
428
|
+
return torch.cat(encodings, dim=-1)
|
|
429
|
+
return torch.stack(encodings, dim=0).sum(dim=0)
|
|
430
|
+
|
|
431
|
+
def _context_size(self, sizes: list[int], name_site: str) -> int:
|
|
432
|
+
if self.agg_type == "concat":
|
|
433
|
+
return sum(sizes)
|
|
434
|
+
if len(set(sizes)) > 1:
|
|
435
|
+
raise ValueError(f"With agg_type='sum', the plates of site {name_site!r} must have the same encoding size, got {sizes}.")
|
|
436
|
+
return sizes[0]
|