Skip to content

Commit 6aa51ff

Browse files
vloisontomMoral
andauthored
ENH add UNHaP experiments and documentation (#21)
Co-authored-by: Thomas Moreau <thomas.moreau.2010@gmail.com>
1 parent 9397c2d commit 6aa51ff

25 files changed

Lines changed: 5067 additions & 344 deletions

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
# FaDIn: Fast Discretized Inference For Hawkes Processes with General Parametric Kernels
1+
# FaDIn: a tool box for fast and robust inference for parametric point processes
22

33
![build](https://img.shields.io/github/actions/workflow/status/GuillaumeStaermanML/FaDIn/unit_tests.yml?event=push&style=for-the-badge)
44
![python version](https://img.shields.io/badge/python-3.7_|_3.8_|_3.9_|_3.10_|_3.11-blue?style=for-the-badge)
55
![license](https://img.shields.io/github/license/GuillaumeStaermanML/FaDIn?style=for-the-badge)
66
![code style](https://img.shields.io/badge/code_style-black-black?style=for-the-badge)
77

8-
This Package implements FaDIn.
8+
This package implements FaDIn and UNHaP. FaDIn and UNHaP are inference methods for parametric Hawkes Processes (HP) with finite-support kernels, with the following features:
9+
- *Fast:* computation time is low compared to other methods.
10+
- Compatible in univariate and multivariate settings.
11+
- *Flexible:* various kernel choices are implemented, with classical ones (exponential, truncated Gaussian, raised cosine) and an API to add custom kernels for inference.
12+
- *Masking:* if some parameters can be fixed, the user can mask them easily.
13+
- Smart initialization of parameters before optimization: the user can choose between `random` (purely random), `moment_matching_max` (moment matching with maximum mode) and `moment_matching_mean` (moment matching with mean mode). The moment matching options are implemented for UNHaP.
914

10-
## Installation
1115

12-
**To install this package, make sure you have an up-to-date version of** `pip`.
16+
[FaDIn](https://proceedings.mlr.press/v202/staerman23a/staerman23a.pdf) does classical Hawkes inference with gradient descent.
17+
[UNHaP](https://raw.githubusercontent.com/mlresearch/v258/main/assets/loison25a/loison25a.pdf) does Hawkes inference where the Hawkes Process is marked and mixed with a noisy Poisson process.
1318

1419

20+
## Installation
21+
22+
**To install this package, make sure you have an up-to-date version of** `pip`.
23+
```bash
24+
python3 -m pip install --upgrade pip
25+
```
1526
### From PyPI (coming soon)
1627

1728
In a dedicated Python env, run:
@@ -41,6 +52,18 @@ pip install -e ".[dev]"
4152
pre-commit install
4253
```
4354

55+
Before running the experiments of the FaDIn and UNHaP papers located in the `experiments` directory, please install the corresponding dependencies beforehand.
56+
57+
```bash
58+
pip install -e ".[experiments]"
59+
```
60+
61+
## Short examples
62+
A few illustrative examples are provided in the `examples` folder of this repository, in particular:
63+
- `plot_univariate_fadin`: simulate an univariate unmarked Hawkes process, infer Hawkes Process parameters using FaDIn, and plot inferred kernel.
64+
- `plot_multivariate_fadin`: same as `plot_univariate_fadin` but in the multivariate case.
65+
- `plot_unhap`: simulate an univariate marked Hawkes process and a marked Poisson process, infer Hawkes Process parameters using UNHaP, ald plot inferred kernels.
66+
4467
## Citing this work
4568

4669
If this package was useful to you, please cite it in your work:
@@ -54,4 +77,12 @@ If this package was useful to you, please cite it in your work:
5477
year={2023},
5578
organization={PMLR}
5679
}
80+
81+
@inproceedings{loison2025unhap,
82+
title={UNHaP: Unmixing Noise from Hawkes Process},
83+
author={Loison, Virginie and Staerman, Guillaume and Moreau, Thomas},
84+
booktitle={International Conference on Artificial Intelligence and Statistics},
85+
pages={1342--1350},
86+
year={2025}
87+
}
5788
```

examples/plot_multivariate_fadin.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
events = simu_hawkes_cluster(T, baseline, alpha, kernel)
5050

5151
###############################################################################
52-
# Here, we apply FaDIn.
53-
52+
# Here, we initiate FaDIn and fit it to the simulated data.
53+
print("Fitting FaDIn solver...")
5454
solver = FaDIn(
5555
n_dim=n_dim,
5656
kernel="truncated_exponential",
@@ -60,25 +60,20 @@
6060
max_iter=10000
6161
)
6262
solver.fit(events, T)
63+
print("FaDIn solver fitted.")
64+
# We can now access the estimated parameters of the model.
6365

64-
# We average on the 10 last values of the optimization.
65-
66-
estimated_baseline = solver.param_baseline[-10:].mean(0)
67-
estimated_alpha = solver.param_alpha[-10:].mean(0)
68-
param_kernel = [solver.param_kernel[0][-10:].mean(0)]
69-
70-
print('Estimated baseline is:', estimated_baseline)
71-
print('Estimated alpha is:', estimated_alpha)
66+
print('Estimated baseline is:', solver.baseline_)
67+
print('Estimated alpha is:', solver.alpha_)
7268
print('Estimated parameters of the truncated Exponential kernel is:',
73-
param_kernel[0])
69+
solver.kernel_)
7470

7571
###############################################################################
7672
# Here, we plot the values of the estimated kernels with FaDIn.
7773

7874
kernel = DiscreteKernelFiniteSupport(dt, n_dim, kernel='truncated_exponential',
7975
kernel_length=kernel_length)
80-
kernel_values = kernel.kernel_eval(param_kernel,
81-
discretization)
76+
kernel_values = kernel.kernel_eval(solver.kernel_, discretization)
8277

8378
plt.subplots(figsize=(12, 8))
8479
for i in range(n_dim):

examples/plot_unhap.py

Lines changed: 45 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@
2020
import numpy as np
2121
import matplotlib.pyplot as plt
2222

23-
from fadin.utils.utils_simu import simu_marked_hawkes_cluster, custom_density
24-
from fadin.utils.utils_simu import simu_multi_poisson
23+
from fadin.utils.utils_simu import simulate_marked_data
2524
from fadin.solver import UNHaP
26-
from fadin.utils.functions import identity, linear_zero_one
27-
from fadin.utils.functions import reverse_linear_zero_one, truncated_gaussian
2825
from fadin.utils.vis import plot
2926

3027

31-
# %% Fixing the parameter of the simulation setting
28+
# %% Fix the simulation and solver parameters
3229

3330
baseline = np.array([0.3])
3431
baseline_noise = np.array([0.05])
@@ -37,120 +34,74 @@
3734
sigma = np.array([[0.1]])
3835

3936
delta = 0.01
40-
end_time = 10000
37+
end_time = 1000
4138
seed = 0
42-
max_iter = 20000
39+
max_iter = 2000
4340
batch_rho = 200
4441

45-
# %% Create the simulating function
46-
47-
48-
def simulate_data(baseline, baseline_noise, alpha, end_time, seed=0):
49-
n_dim = len(baseline)
50-
51-
marks_kernel = identity
52-
marks_density = linear_zero_one
53-
time_kernel = truncated_gaussian
54-
55-
params_marks_density = dict()
56-
# params_marks_density = dict(scale=1)
57-
params_marks_kernel = dict(slope=1.2)
58-
params_time_kernel = dict(mu=mu, sigma=sigma)
59-
60-
marked_events, _ = simu_marked_hawkes_cluster(
61-
end_time,
62-
baseline,
63-
alpha,
64-
time_kernel,
65-
marks_kernel,
66-
marks_density,
67-
params_marks_kernel=params_marks_kernel,
68-
params_marks_density=params_marks_density,
69-
time_kernel_length=None,
70-
marks_kernel_length=None,
71-
params_time_kernel=params_time_kernel,
72-
random_state=seed,
73-
)
74-
75-
noisy_events_ = simu_multi_poisson(end_time, [baseline_noise])
76-
77-
random_marks = [np.random.rand(noisy_events_[i].shape[0]) for i in range(n_dim)]
78-
noisy_marks = [
79-
custom_density(
80-
reverse_linear_zero_one,
81-
dict(),
82-
size=noisy_events_[i].shape[0],
83-
kernel_length=1.0,
84-
)
85-
for i in range(n_dim)
86-
]
87-
noisy_events = [
88-
np.concatenate(
89-
(noisy_events_[i].reshape(-1, 1), random_marks[i].reshape(-1, 1)), axis=1
90-
)
91-
for i in range(n_dim)
92-
]
93-
94-
events = [
95-
np.concatenate((noisy_events[i], marked_events[i]), axis=0)
96-
for i in range(n_dim)
97-
]
98-
99-
events_cat = [events[i][events[i][:, 0].argsort()] for i in range(n_dim)]
100-
101-
labels = [
102-
np.zeros(marked_events[i].shape[0] + noisy_events_[i].shape[0])
103-
for i in range(n_dim)
104-
]
105-
labels[0][-marked_events[0].shape[0] :] = 1.0
106-
true_rho = [labels[i][events[i][:, 0].argsort()] for i in range(n_dim)]
107-
# put the mark to one to test the impact of the marks
108-
# events_cat[0][:, 1] = 1.
109-
110-
return events_cat, noisy_marks, true_rho
111-
112-
113-
ev, noisy_marks, true_rho = simulate_data(
114-
baseline, baseline_noise.item(), alpha, end_time, seed=0
42+
# %% Simulate Hawkes Process with truncated Gaussian kernel and Poisson noise
43+
44+
ev, noisy_marks, true_rho = simulate_marked_data(
45+
baseline, baseline_noise.item(), alpha, end_time, mu, sigma, seed=0
11546
)
116-
# %% Apply UNHAP
47+
48+
# %% Let's take a closer look at the events
49+
print('Type of ev object', type(ev))
50+
# ev is a list of numpy arrays, one for each dimension
51+
print('Number of events', len(ev[0]))
52+
print('Shape of first event array', ev[0].shape)
53+
# Each dimension is stored as a numpy array of shape (n_events, 2).
54+
print('First 10 events timestamps and marks', ev[0][:10])
55+
# Each event is stored as [timestamp, mark].
56+
# This is the expected data format for UNHaP.
57+
print('First event timestamp', ev[0][0][0])
58+
print('First event mark', ev[0][0][1])
59+
print('Second event timestamp', ev[0][1][0])
60+
print('Second event mark', ev[0][1][1])
61+
62+
# %% Initiate and fit UNHAP to the simulated events
11763

11864
solver = UNHaP(
11965
n_dim=1,
12066
kernel="truncated_gaussian",
12167
kernel_length=1.0,
68+
init='moment_matching_mean',
12269
delta=delta,
12370
optim="RMSprop",
12471
params_optim={"lr": 1e-3},
12572
max_iter=max_iter,
12673
batch_rho=batch_rho,
12774
density_hawkes="linear",
12875
density_noise="uniform",
129-
moment_matching=True,
13076
)
13177
solver.fit(ev, end_time)
13278

13379
# %% Print estimated parameters
13480

135-
print("Estimated baseline is: ", solver.param_baseline[-10:].mean().item())
136-
print("Estimated alpha is: ", solver.param_alpha[-10:].mean().item())
137-
print("Estimated kernel mean is: ", (solver.param_kernel[0][-10:].mean().item()))
138-
print("Estimated kernel sd is: ", solver.param_kernel[1][-10:].mean().item())
139-
print("Estimated noise baseline is: ", solver.param_baseline_noise[-10:].mean().item())
81+
print("Estimated baseline is: ", solver.baseline_.item())
82+
print("Estimated alpha is: ", solver.alpha_.item())
83+
print("Estimated kernel mean is: ", solver.kernel_[0].item())
84+
print("Estimated kernel sd is: ", solver.kernel_[1].item())
85+
print("Estimated noise baseline is: ", solver.baseline_noise_.item())
14086
# error on params
141-
error_baseline = (solver.param_baseline[-10:].mean().item() - baseline.item()) ** 2
142-
error_baseline_noise = (
143-
solver.param_baseline_noise[-10:].mean().item() - baseline_noise.item()
87+
error_bl = (solver.baseline_.item() - baseline.item()) ** 2
88+
error_bl_noise = (
89+
solver.baseline_noise_.item() - baseline_noise.item()
14490
) ** 2
145-
error_alpha = (solver.param_alpha[-10:].mean().item() - alpha.item()) ** 2
146-
error_mu = (solver.param_kernel[0][-10:].mean().item() - 0.5) ** 2
147-
error_sigma = (solver.param_kernel[1][-10:].mean().item() - 0.1) ** 2
148-
sum_error = error_baseline + error_baseline_noise + error_alpha + error_mu + error_sigma
91+
error_alpha = (solver.alpha_.item() - alpha.item()) ** 2
92+
error_mu = (solver.kernel_[0].item() - mu.item()) ** 2
93+
error_sigma = (solver.kernel_[1].item() - sigma.item()) ** 2
94+
sum_error = error_bl + error_bl_noise + error_alpha + error_mu + error_sigma
14995
error_params = np.sqrt(sum_error)
15096

151-
print("L2 square errors of the vector of parameters is:", error_params)
97+
print("L2 square error of the vector of parameters is:", error_params)
15298

15399
# %% Plot estimated parameters
154-
fig, axs = plot(solver, plotfig=False, bl_noise=True, title="UNHaP fit", savefig=None)
100+
fig, axs = plot(
101+
solver,
102+
plotfig=False,
103+
bl_noise=True,
104+
title="UNHaP fit",
105+
savefig=None
106+
)
155107
plt.show(block=True)
156-
# %%

examples/plot_univariate_fadin.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
# Here, we set the parameters of a Hawkes process with an Exponential(1) distribution.
3838

3939
baseline = np.array([.4])
40-
alpha = np.array([[0.8]])
41-
beta = 2.
40+
alpha = np.array([[0.9]])
41+
beta = 0.8
4242
###############################################################################
4343
# Here, we simulate the data.
4444

@@ -48,6 +48,17 @@
4848
events = simu_hawkes_cluster(T, baseline, alpha, kernel,
4949
params_kernel={'scale': 1 / beta})
5050

51+
###############################################################################
52+
# Let's take a closer look at the events
53+
print('Type of events object', type(events))
54+
# events is a list of numpy arrays, one for each dimension
55+
print('Number of events', len(events[0]))
56+
print('First 10 events timestamps', events[0][:10])
57+
# For each event, its occurence time (timestamp) is stored in the numpy array.
58+
59+
# `events`` is a list. Its elements are numpy arrays containing the timestamps
60+
# of the events. This is the expected data format for FaDIn.
61+
5162
###############################################################################
5263
# Here, we apply FaDIn.
5364

@@ -60,24 +71,20 @@
6071
)
6172
solver.fit(events, T)
6273

63-
# We average on the 10 last values of the optimization.
64-
65-
estimated_baseline = solver.param_baseline[-10:].mean().item()
66-
estimated_alpha = solver.param_alpha[-10:].mean().item()
67-
param_kernel = [solver.param_kernel[0][-10:].mean().item()]
74+
###############################################################################
75+
# Here, we print the estimated parameters of the Hawkes process.
6876

69-
print('Estimated baseline is:', estimated_baseline)
70-
print('Estimated alpha is:', estimated_alpha)
71-
print('Estimated beta parameter of the exponential kernel is:', param_kernel[0])
77+
print('Estimated baseline is:', solver.baseline_.item())
78+
print('Estimated alpha is:', solver.alpha_.item())
79+
print('Estimated beta parameter of the exponential kernel is:', solver.kernel_)
7280

7381

7482
###############################################################################
7583
# Here, we plot the values of the estimated kernel with FaDIn.
7684

7785
kernel = DiscreteKernelFiniteSupport(dt, n_dim, kernel='truncated_exponential',
7886
kernel_length=kernel_length)
79-
kernel_values = kernel.kernel_eval([torch.Tensor([param_kernel])],
80-
discretization)
87+
kernel_values = kernel.kernel_eval(solver.kernel_, discretization)
8188

8289
plt.plot(discretization[1:], kernel_values.squeeze()[1:]/kernel_length,
8390
label='FaDIn\' estimated kernel')

0 commit comments

Comments
 (0)