Skip to content

Commit 73da345

Browse files
author
domosedy
committed
[feat] added new class structure and manual testing for conjugate prior
1 parent 896638a commit 73da345

3 files changed

Lines changed: 247 additions & 104 deletions

File tree

src/pysatl_core/families/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
parametrization,
2323
)
2424
from .exponential_family import (
25-
ExponentialFamily,
26-
ExponentialClassParametrization,
2725
ExponentialConjugateHyperparameters,
26+
ExponentialFamily,
27+
ExponentialFamilyParametrization,
28+
NaturalExponentialFamily,
29+
SpacePredicate,
30+
SpacePredicateArray,
2831
)
2932
from .registry import ParametricFamilyRegister
3033

@@ -40,8 +43,11 @@
4043
# builtins
4144
*_builtins_all,
4245
"ExponentialFamily",
43-
"ExponentialClassParametrization",
46+
"ExponentialFamilyParametrization",
4447
"ExponentialConjugateHyperparameters",
48+
"SpacePredicate",
49+
"SpacePredicateArray",
50+
"NaturalExponentialFamily",
4551
]
4652

4753
del _builtins_all
Lines changed: 155 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
from __future__ import annotations
22
from collections.abc import Callable
3-
from dataclasses import dataclass
4-
import math
5-
from typing import Any, cast
3+
from typing import Any, cast, TYPE_CHECKING
64
from scipy.integrate import nquad, quad
75
import numpy as np
86

97
from pysatl_core.distributions.fitters import _ppf_brentq_from_cdf
10-
from pysatl_core.families.parametric_family import (
11-
ParametricFamily,
12-
)
8+
from pysatl_core.families.parametric_family import ParametricFamily
139
from pysatl_core.families.parametrizations import Parametrization, parametrization
1410
from pysatl_core.types import (
1511
DistributionType,
@@ -19,6 +15,13 @@
1915
SamplingStrategy,
2016
)
2117

18+
if TYPE_CHECKING:
19+
from pysatl_core.distributions.support import Support
20+
21+
type ParametrizedFunction = Callable[[Parametrization, Any], Any]
22+
type SupportArg = Callable[[Parametrization], Support | None] | None
23+
24+
2225
PDF = "pdf"
2326
CDF = "cdf"
2427
PPF = "ppf"
@@ -29,7 +32,7 @@
2932
KURT = "kurtosis"
3033

3134

32-
class ExponentialClassParametrization(Parametrization):
35+
class ExponentialFamilyParametrization(Parametrization):
3336
"""
3437
Standard parametrization of Exponential Family.
3538
"""
@@ -46,45 +49,56 @@ def __str__(self):
4649
return f"alpha={self.alpha}, beta={self.beta}"
4750

4851

49-
def accepts(x, support):
52+
def doesAccept(x, support):
5053
if not hasattr(x, "__len__"):
5154
x = [x]
5255

5356
def accept_1D(x, borders):
5457
left, right = borders
58+
if abs(x) == 0 and (abs(left) == 0 or abs(right) == 0):
59+
return False
5560
return left <= x <= right
5661

5762
return all(accept_1D(x_i, border) for x_i, border in zip(x, support))
5863

5964

60-
class ExponentialFamily(ParametricFamily):
65+
class SpacePredicate:
66+
def __init__(self, predicate: Callable[[Any], bool]):
67+
self._predicate = predicate
68+
69+
def accepts(self, x: Any) -> bool:
70+
return self._predicate(x)
71+
72+
73+
class SpacePredicateArray(SpacePredicate):
74+
def __init__(self, space: list[tuple[float, float]]):
75+
SpacePredicate.__init__(self, lambda x: doesAccept(x, space))
76+
self._space = space
77+
78+
79+
class NaturalExponentialFamily(ParametricFamily):
6180
def __init__(
6281
self,
6382
*,
64-
A: Callable[[ExponentialClassParametrization], float],
65-
T: Callable[[Any], Any],
66-
h: Callable[[Any], float],
67-
eta: Callable[[Any], Any],
68-
support: list[tuple[float, float]],
69-
param_space: list[tuple[float, float]],
70-
natural_param_space: list[tuple[float, float]],
71-
name: str = "ExponentialFamily",
72-
theta_from_eta: Callable[[Any], Any] = None,
83+
log_partition: Callable[[ExponentialFamilyParametrization], float],
84+
sufficient_statistics: Callable[[Any], Any],
85+
normalization_constant: Callable[[Any], Any],
86+
support: SpacePredicate,
87+
parameter_space: SpacePredicate,
88+
sufficient_statistics_values: SpacePredicate,
89+
name: str = "NaturalExponentialFamily",
7390
distr_type: DistributionType | Callable[[Parametrization], DistributionType],
7491
distr_parametrizations: list[ParametrizationName],
7592
sampling_strategy: SamplingStrategy,
7693
support_by_parametrization: SupportArg = None,
7794
):
95+
self._sufficient = sufficient_statistics
96+
self._log_partition = log_partition
97+
self._normalization = normalization_constant
7898

79-
self._A = A
80-
self._T = T
81-
self._h = h
82-
83-
self._eta = eta if eta is not None else (lambda th: th)
84-
self._theta_from_eta = theta_from_eta
85-
self._natural_param_space = natural_param_space
86-
self._param_space = param_space
8799
self._support = support
100+
self._parameter_space = parameter_space
101+
self._sufficient_statistics_values = sufficient_statistics_values
88102

89103
distr_characteristics = {
90104
PDF: self.density,
@@ -101,23 +115,25 @@ def __init__(
101115
sampling_strategy=sampling_strategy,
102116
support_by_parametrization=support_by_parametrization,
103117
)
104-
parametrization(family=self, name="theta")((ExponentialClassParametrization))
118+
parametrization(family=self, name="theta")((ExponentialFamilyParametrization))
105119

106120
@property
107121
def log_density(self) -> ParametrizedFunction:
108122
def log_density_func(
109-
parametrization: ExponentialClassParametrization, x: Any
123+
parametrization: ExponentialFamilyParametrization, x: Any
110124
) -> Any:
111-
if not accepts(x, self._support):
125+
if not self._support.accepts(x):
112126
return float("-inf")
113127

114-
params = cast(ExponentialClassParametrization, parametrization)
128+
params = cast(ExponentialFamilyParametrization, parametrization)
115129
theta = params.parameters.get("theta")
116-
eta = self._eta(theta)
117-
sufficient = self._T(x)
118-
dot = np.dot(eta, sufficient)
119-
120-
result = float(np.log(self._h(x)) + dot + self._A(parametrization))
130+
sufficient = self._sufficient(x)
131+
dot = np.dot(theta, sufficient)
132+
result = float(
133+
np.log(self._normalization(x))
134+
+ dot
135+
+ self._log_partition(parametrization)
136+
)
121137
return result
122138

123139
return log_density_func
@@ -128,41 +144,59 @@ def density(self) -> ParametrizedFunction:
128144

129145
@property
130146
def conjugate_prior_family(self):
131-
def conjugate_sufficient(eta: Any):
132-
theta = [self._theta_from_eta(eta)]
133-
if not accepts(theta, self._param_space):
147+
def conjugate_sufficient(theta: Any):
148+
if not self._parameter_space.accepts(theta):
134149
return [float("-inf"), float("-inf")]
135150

136-
return [eta, self._A(ExponentialClassParametrization(theta=theta))]
151+
return [
152+
theta,
153+
self._log_partition(ExponentialFamilyParametrization(theta=[theta])),
154+
]
137155

138-
def conjugate_log_partition(parametrization: ExponentialClassParametrization):
156+
def conjugate_log_partition(parametrization: ExponentialFamilyParametrization):
139157
alpha = parametrization.theta[0]
140158
beta = parametrization.theta[1]
141159

142-
def pdf(eta: Any):
143-
theta = self._theta_from_eta(eta)
160+
def pdf(theta: Any):
144161
if not hasattr(theta, "__len__"):
145162
theta = [theta]
146-
parametrization = ExponentialClassParametrization(
163+
parametrization = ExponentialFamilyParametrization(
147164
theta=theta,
148165
)
149-
return np.exp(np.dot(eta, alpha) + beta * self._A(parametrization))
166+
return np.exp(
167+
np.dot(theta, alpha) + beta * self._log_partition(parametrization)
168+
)[0]
150169

151-
all_value = nquad(pdf, self._natural_param_space)[0]
170+
all_value = nquad(
171+
lambda x: pdf(x) if self._parameter_space.accepts(x) else 0,
172+
[(float("-inf"), float("+inf"))],
173+
)[0]
152174
return -np.log(all_value)
153175

154-
if self._theta_from_eta is None:
155-
raise RuntimeError("Theta from eta wasn't specified")
156-
157-
return ExponentialFamily(
158-
A=conjugate_log_partition,
159-
T=conjugate_sufficient,
160-
h=lambda _: 1,
161-
eta=lambda x: x,
162-
theta_from_eta=lambda eta: eta,
163-
support=self._natural_param_space,
164-
natural_param_space=[(float("-inf"), float("inf"))] * 2,
165-
param_space=[(float("-inf"), float("inf"))] * 2,
176+
# TODO: remove hardcoding - Done, all hardcoding is only on user's hands
177+
# 1. pr with prototype/draft - in progress
178+
# 2. write instruction about to add distributions as member of exponential family - not started
179+
# 3. parametrization's spaces (передавать в конструктор) - maybe impossible, discuss this with desiment on meeting
180+
181+
def conjugate_sufficient_accepts(
182+
parametrization: ExponentialFamilyParametrization,
183+
):
184+
parametrization = cast(parametrization, ExponentialFamilyParametrization)
185+
theta = parametrization.parameters.get("theta")
186+
xi = theta[:-1]
187+
nu = theta[-1]
188+
189+
return self._sufficient_statistics_values(xi) and SpacePredicateArray(
190+
[(0, float("+inf"))]
191+
).accepts(nu)
192+
193+
return NaturalExponentialFamily(
194+
log_partition=conjugate_log_partition,
195+
sufficient_statistics=conjugate_sufficient,
196+
normalization_constant=lambda _: 1,
197+
support=self._parameter_space,
198+
sufficient_statistics_values=self._parameter_space, # TODO: write convex hull for this
199+
parameter_space=SpacePredicate(conjugate_sufficient_accepts),
166200
sampling_strategy=self.sampling_strategy,
167201
distr_type=self._distr_type,
168202
distr_parametrizations=self.parametrization_names,
@@ -172,13 +206,15 @@ def pdf(eta: Any):
172206
@property
173207
def _mean(self) -> ParametrizedFunction:
174208
def mean_func(parametrization: Parametrization, x: Any) -> Any:
209+
dimension_size = 1
175210
if hasattr(x, "__len__"):
176211
dimension_size = len(x)
177-
else:
178-
dimension_size = 1
179-
print(dimension_size)
180212
return nquad(
181-
lambda x: np.dot(x, self.density(parametrization, x)),
213+
lambda x: (
214+
np.dot(x, self.density(parametrization, x))
215+
if self._support.accepts(x)
216+
else 0
217+
),
182218
[(float("-inf"), float("inf"))] * dimension_size,
183219
)[0]
184220

@@ -187,12 +223,15 @@ def mean_func(parametrization: Parametrization, x: Any) -> Any:
187223
@property
188224
def _second_moment(self) -> ParametrizedFunction:
189225
def func(parametrization: Parametrization, x: Any) -> Any:
226+
dimension_size = 1
190227
if hasattr(x, "__len__"):
191228
dimension_size = len(x)
192-
else:
193-
dimension_size = 1
194229
return nquad(
195-
lambda x: x**2 * self.density(parametrization, x),
230+
lambda x: (
231+
x**2 * self.density(parametrization, x)
232+
if self._support.accepts(x)
233+
else 0
234+
),
196235
[(float("-inf"), float("inf"))] * dimension_size,
197236
)[0]
198237

@@ -217,12 +256,62 @@ def posterior_hyperparameters(
217256
alpha_post = None
218257
beta_post = None
219258
if hasattr(sample, "__iter__") and not isinstance(sample, str):
220-
alpha_post = np.sum([self._T(x) for x in sample], axis=0)
259+
alpha_post = np.sum([self._sufficient(x) for x in sample], axis=0)
221260
beta_post = len(sample)
222261
else:
223-
alpha_post = self.T(sample)
262+
alpha_post = self._sufficient(sample)
224263
beta_post = 1
225264

226265
return ExponentialConjugateHyperparameters(
227266
alpha=alpha + alpha_post, beta=beta + beta_post
228267
)
268+
269+
270+
class ExponentialFamily(NaturalExponentialFamily):
271+
def __init__(
272+
self,
273+
*,
274+
log_partition: Callable[[ExponentialFamilyParametrization], float],
275+
sufficient_statistics: Callable[[Any], Any],
276+
normalization_constant: Callable[[Any], Any],
277+
parameter_from_natural_parameter: Callable[[Any], Any],
278+
support: SpacePredicate,
279+
parameter_space: SpacePredicate,
280+
sufficient_statistics_values: SpacePredicate,
281+
distr_type: DistributionType | Callable[[Parametrization], DistributionType],
282+
distr_parametrizations: list[ParametrizationName],
283+
sampling_strategy: SamplingStrategy,
284+
name: str = "ExponentialFamily",
285+
support_by_parametrization: SupportArg = None,
286+
):
287+
def natural_log_partition(eta_parametrizaion: ExponentialFamilyParametrization):
288+
eta_parametrizaion = cast(
289+
ExponentialFamilyParametrization, eta_parametrizaion
290+
)
291+
eta = eta_parametrizaion.parameters.get("theta")
292+
theta = parameter_from_natural_parameter(eta)
293+
return log_partition(ExponentialFamilyParametrization(theta=[theta]))
294+
295+
natural_sufficient_statistics_values = SpacePredicate(
296+
lambda eta: sufficient_statistics_values.accepts(
297+
parameter_from_natural_parameter(eta)
298+
)
299+
)
300+
natural_parameter_space = SpacePredicate(
301+
lambda eta: parameter_space.accepts(parameter_from_natural_parameter(eta)),
302+
)
303+
304+
NaturalExponentialFamily.__init__(
305+
self,
306+
log_partition=natural_log_partition,
307+
sufficient_statistics=sufficient_statistics,
308+
normalization_constant=normalization_constant,
309+
support=support,
310+
parameter_space=natural_parameter_space,
311+
sufficient_statistics_values=natural_sufficient_statistics_values,
312+
name=name,
313+
distr_parametrizations=distr_parametrizations,
314+
distr_type=distr_type,
315+
sampling_strategy=sampling_strategy,
316+
support_by_parametrization=support_by_parametrization,
317+
)

0 commit comments

Comments
 (0)