Source code for mixle.models._neural_serial
"""Serialization + input-validation helpers shared by the neural-leaf families (``mixle.models``).
The neural leaves wrap a live ``torch.nn.Module``, which the generic JSON encoder in
:mod:`mixle.utils.serialization` cannot walk (a module is not a registered mixle class). These helpers give
every neural leaf a working ``to_dict``/``from_dict`` and the recursive ``__pysp_getstate__``/``__pysp_setstate__``
hooks (so a leaf inside a ``MixtureDistribution`` serializes too) by persisting the module as portable bytes.
The module round-trips through ``torch.save``/``torch.load`` of a ``pickle`` byte buffer -- which requires the
wrapped nn.Module class to be reachable at module level (that is why the ``build_*`` helpers were hoisted). The
bytes are base64-encoded so the whole payload is plain JSON.
"""
from __future__ import annotations
import base64
import io
import pickle
from typing import Any
import numpy as np
[docs]
def module_to_bytes(module: Any) -> bytes:
"""Serialize a torch nn.Module (architecture + weights) to portable bytes via ``torch.save``."""
import torch
buf = io.BytesIO()
torch.save(module, buf, pickle_protocol=pickle.HIGHEST_PROTOCOL)
return buf.getvalue()
[docs]
def module_from_bytes(data: bytes) -> Any:
"""Reconstruct a torch nn.Module previously encoded by :func:`module_to_bytes`."""
import torch
buf = io.BytesIO(bytes(data))
try:
return torch.load(buf, weights_only=False) # full module (arch + weights); trusted local artifact
except TypeError: # torch < 2.0 has no weights_only kwarg
buf.seek(0)
return torch.load(buf)
[docs]
def encode_module(module: Any) -> dict[str, str]:
"""A JSON-safe tagged dict for a torch module (base64 of :func:`module_to_bytes`)."""
return {"__neural_module__": base64.b64encode(module_to_bytes(module)).decode("ascii")}
[docs]
def decode_module(payload: Any) -> Any:
"""Inverse of :func:`encode_module`."""
return module_from_bytes(base64.b64decode(payload["__neural_module__"].encode("ascii")))
[docs]
def check_finite(x: np.ndarray, where: str) -> np.ndarray:
"""Raise a clear error if ``x`` has any non-finite entry, so a NaN cannot silently poison a mixture E-step.
A neural leaf that returned NaN log-density would corrupt every responsibility in the E-step without a
diagnosable failure; validating at the density boundary turns that into an immediate, named error instead.
"""
arr = np.asarray(x, dtype=float)
if not np.all(np.isfinite(arr)):
raise ValueError(
"%s received non-finite input (NaN or inf); a neural leaf cannot score it and it would poison a "
"mixture E-step. Clean the data before fitting/scoring." % where
)
return arr