Angshul's picture
Upload 22 files
ce3f6c1 verified
Raw History Blame Contribute Delete
4.32 kB
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Generic, Sequence, TypeVar
import math
import random
S = TypeVar("S")
A = TypeVar("A")
def normalize_logweights(logw: Sequence[float]) -> list[float]:
if not logw:
return []
finite = [w for w in logw if math.isfinite(w)]
if not finite:
return [1.0 / len(logw)] * len(logw)
m = max(finite)
ws = [math.exp(w - m) if math.isfinite(w) else 0.0 for w in logw]
z = sum(ws)
if not math.isfinite(z) or z <= 0.0:
return [1.0 / len(logw)] * len(logw)
return [w / z for w in ws]
def effective_sample_size(probs: Sequence[float]) -> float:
s2 = sum(float(p) ** 2 for p in probs)
return 0.0 if s2 <= 0.0 else 1.0 / s2
def systematic_resample(probs: Sequence[float], rng: random.Random) -> list[int]:
n = len(probs)
if n == 0:
return []
cdf: list[float] = []
total = 0.0
for p in probs:
total += float(p)
cdf.append(total)
if total <= 0.0 or not math.isfinite(total):
return list(range(n))
cdf = [x / total for x in cdf]
cdf[-1] = 1.0
u0 = rng.random() / n
out: list[int] = []
j = 0
for i in range(n):
u = u0 + i / n
while j < n - 1 and u > cdf[j]:
j += 1
out.append(j)
return out
@dataclass(frozen=True)
class Proposal(Generic[A]):
action: A
probability: float
@dataclass
class Particle(Generic[S]):
state: S
logw: float = 0.0
history: list[Any] = field(default_factory=list)
@dataclass(frozen=True)
class SMCConfig:
n_particles: int = 32
n_steps: int = 3
ess_resample_fraction: float = 0.5
seed: int = 0
class ParticleFilter(Generic[S, A]):
"""Importance-corrected SMC runtime.
`propose(state, t)` returns a discrete proposal distribution q(a|state).
`transition(state, action, t)` produces the next state.
`log_target_increment(prev, action, nxt, t)` supplies the incremental
log target density. The importance update is log w += log target - log q.
Resampling is never performed after the final weighting step, so the final
posterior weights are preserved.
"""
def __init__(self, config: SMCConfig | None = None):
self.config = config or SMCConfig()
def run(
self,
initial_state: S,
propose: Callable[[S, int], Sequence[Proposal[A]]],
transition: Callable[[S, A, int], S],
log_target_increment: Callable[[S, A, S, int], float],
) -> list[Particle[S]]:
cfg = self.config
rng = random.Random(cfg.seed)
particles = [Particle(state=initial_state) for _ in range(cfg.n_particles)]
for t in range(cfg.n_steps):
nxt: list[Particle[S]] = []
for p in particles:
proposals = [x for x in propose(p.state, t) if x.probability > 0.0]
if not proposals:
raise ValueError("proposal distribution is empty")
z = sum(x.probability for x in proposals)
probs = [x.probability / z for x in proposals]
r = rng.random()
acc = 0.0
j = len(proposals) - 1
for i, pr in enumerate(probs):
acc += pr
if r <= acc:
j = i
break
selected = proposals[j]
q = probs[j]
new_state = transition(p.state, selected.action, t)
inc = float(log_target_increment(p.state, selected.action, new_state, t))
nxt.append(Particle(new_state, p.logw + inc - math.log(q), p.history + [selected.action]))
particles = nxt
probs_now = normalize_logweights([p.logw for p in particles])
if t < cfg.n_steps - 1 and effective_sample_size(probs_now) < cfg.ess_resample_fraction * len(particles):
ids = systematic_resample(probs_now, rng)
particles = [Particle(particles[i].state, 0.0, list(particles[i].history)) for i in ids]
final_probs = normalize_logweights([p.logw for p in particles])
for p, pr in zip(particles, final_probs):
setattr(p, "posterior", pr)
return particles