decompress / engine /bocpd.py
squaredcuber's picture
Deploy Decompress MiniCPM demo
bcbb8c5 verified
Raw
History Blame Contribute Delete
4.54 kB
from __future__ import annotations
import math
import sys
from dataclasses import dataclass
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DUMP_PATH = ROOT / "eval" / "probe_dump.npz"
@dataclass(frozen=True)
class BocpdResult:
step: int
value: float
cp_prob: float
map_run_length: int
change_point: bool
def logsumexp(values: np.ndarray) -> float:
max_value = float(np.max(values))
if not math.isfinite(max_value):
return max_value
return max_value + math.log(float(np.sum(np.exp(values - max_value))))
def student_t_logpdf(x: float, mu: np.ndarray, kappa: np.ndarray, alpha: np.ndarray, beta: np.ndarray) -> np.ndarray:
nu = 2.0 * alpha
scale = np.sqrt(beta * (kappa + 1.0) / (alpha * kappa))
z = (x - mu) / scale
return (
np.vectorize(math.lgamma)((nu + 1.0) / 2.0)
- np.vectorize(math.lgamma)(nu / 2.0)
- 0.5 * np.log(nu * math.pi)
- np.log(scale)
- ((nu + 1.0) / 2.0) * np.log1p((z * z) / nu)
)
def update_nig(
x: float,
mu: np.ndarray,
kappa: np.ndarray,
alpha: np.ndarray,
beta: np.ndarray,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
next_kappa = kappa + 1.0
next_mu = (kappa * mu + x) / next_kappa
next_alpha = alpha + 0.5
next_beta = beta + 0.5 * kappa * (x - mu) ** 2 / next_kappa
return next_mu, next_kappa, next_alpha, next_beta
def run_bocpd(
values: np.ndarray,
hazard: float = 1.0 / 50.0,
prior_mu: float = 0.0,
prior_kappa: float = 1.0e-3,
prior_alpha: float = 1.0,
prior_beta: float = 1.0,
) -> list[BocpdResult]:
log_hazard = math.log(hazard)
log_growth_factor = math.log1p(-hazard)
log_run_probs = np.asarray([0.0], dtype=np.float64)
mu = np.asarray([prior_mu], dtype=np.float64)
kappa = np.asarray([prior_kappa], dtype=np.float64)
alpha = np.asarray([prior_alpha], dtype=np.float64)
beta = np.asarray([prior_beta], dtype=np.float64)
results: list[BocpdResult] = []
previous_map_run_length: int | None = None
for step, x_value in enumerate(values, start=1):
x = float(x_value)
predictive = student_t_logpdf(x, mu, kappa, alpha, beta)
growth_probs = log_run_probs + predictive + log_growth_factor
cp_prob = logsumexp(log_run_probs + predictive + log_hazard)
new_log_run_probs = np.concatenate(([cp_prob], growth_probs))
normalizer = logsumexp(new_log_run_probs)
new_log_run_probs -= normalizer
grown_mu, grown_kappa, grown_alpha, grown_beta = update_nig(x, mu, kappa, alpha, beta)
mu = np.concatenate(([prior_mu], grown_mu))
kappa = np.concatenate(([prior_kappa], grown_kappa))
alpha = np.concatenate(([prior_alpha], grown_alpha))
beta = np.concatenate(([prior_beta], grown_beta))
log_run_probs = new_log_run_probs
map_run_length = int(np.argmax(log_run_probs))
cp_probability = float(np.exp(log_run_probs[0]))
change_point = bool(
map_run_length == 0
or (previous_map_run_length is not None and map_run_length < previous_map_run_length)
)
results.append(
BocpdResult(
step=step,
value=x,
cp_prob=cp_probability,
map_run_length=map_run_length,
change_point=change_point,
)
)
previous_map_run_length = map_run_length
return results
def main() -> None:
dump_path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_DUMP_PATH
if not dump_path.is_absolute():
dump_path = ROOT / dump_path
if not dump_path.exists():
print(f"No probe dump found at {dump_path}")
return
dump = np.load(dump_path, allow_pickle=True)
failure = str(dump.get("failure", ""))
values = np.asarray(dump["nll_series"], dtype=np.float64)
if values.size == 0:
print("No NLL samples found; skipping BOCPD.")
if failure:
print(f"Probe failure: {failure}")
return
print("step | nll | cp_prob | map_run_length | change_point")
print("-----|-----|---------|----------------|-------------")
for result in run_bocpd(values):
flag = "YES" if result.change_point else "no"
print(
f"{result.step:>4} | {result.value:.4f} | {result.cp_prob:.4f} | "
f"{result.map_run_length:>14} | {flag}"
)
if __name__ == "__main__":
main()