InfluenceDiffusion 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- InfluenceDiffusion/Graph.py +478 -0
- InfluenceDiffusion/Trace.py +431 -0
- InfluenceDiffusion/__init__.py +4 -0
- InfluenceDiffusion/estimation_models/BaseWeightEstimator.py +436 -0
- InfluenceDiffusion/estimation_models/EMEstimation.py +236 -0
- InfluenceDiffusion/estimation_models/OptimEstimation.py +291 -0
- InfluenceDiffusion/estimation_models/__init__.py +3 -0
- InfluenceDiffusion/influence_models.py +614 -0
- InfluenceDiffusion/utils.py +29 -0
- InfluenceDiffusion-0.0.3.dist-info/METADATA +21 -0
- InfluenceDiffusion-0.0.3.dist-info/RECORD +13 -0
- InfluenceDiffusion-0.0.3.dist-info/WHEEL +5 -0
- InfluenceDiffusion-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from typing import List, Union, Dict
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
import numpy as np
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
from joblib import Parallel, delayed
|
|
7
|
+
from scipy.stats import uniform
|
|
8
|
+
from scipy.stats._distn_infrastructure import rv_frozen
|
|
9
|
+
|
|
10
|
+
from ..Trace import Traces, PseudoTraces
|
|
11
|
+
from ..Graph import Graph
|
|
12
|
+
from ..utils import multiple_union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseWeightEstimator:
|
|
16
|
+
"""Base class for weight estimation in influence models.
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
graph : Graph
|
|
21
|
+
The graph representing the network.
|
|
22
|
+
n_jobs : Optional[int]
|
|
23
|
+
Number of jobs for parallel processing.
|
|
24
|
+
informative_vertices : set
|
|
25
|
+
Set of informative vertices in the graph.
|
|
26
|
+
weights_ : np.ndarray
|
|
27
|
+
Estimated weights for edges.
|
|
28
|
+
_vertex_2_num_traces_was_informative : Dict[int, int]
|
|
29
|
+
Mapping of vertices to the number of informative traces they participated in.
|
|
30
|
+
_failed_vertices_masks : Dict[int, np.ndarray]
|
|
31
|
+
Masks for failed vertices.
|
|
32
|
+
_vertex_2_active_parent_mask_tm1 : Dict[int, np.ndarray]
|
|
33
|
+
Masks for active parents at time t-1.
|
|
34
|
+
_vertex_2_active_parent_mask_t : Dict[int, np.ndarray]
|
|
35
|
+
Masks for active parents at time t.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, graph: Graph, n_jobs: int = None):
|
|
39
|
+
self.graph = graph
|
|
40
|
+
self.n_jobs = n_jobs
|
|
41
|
+
self.informative_vertices = set()
|
|
42
|
+
self.weights_ = np.array([])
|
|
43
|
+
|
|
44
|
+
def _precompute_num_traces_vertices_participated(self) -> None:
|
|
45
|
+
"""Precompute the number of traces each vertex participated in."""
|
|
46
|
+
self._vertex_2_num_traces_was_informative = {}
|
|
47
|
+
for vertex in self.informative_vertices:
|
|
48
|
+
num_failed_traces = 0 if vertex not in self._failed_vertices_masks \
|
|
49
|
+
else self._failed_vertices_masks[vertex].shape[0]
|
|
50
|
+
num_activated_traces = 0 if vertex not in self._vertex_2_active_parent_mask_tm1 \
|
|
51
|
+
else self._vertex_2_active_parent_mask_tm1[vertex].shape[0]
|
|
52
|
+
self._vertex_2_num_traces_was_informative[vertex] = num_activated_traces + num_failed_traces
|
|
53
|
+
|
|
54
|
+
def _precompute_failed_vertices_parents_masks_from_traces(self, traces: Traces) -> None:
|
|
55
|
+
"""Compute masks for failed vertices based on traces.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
traces : Traces
|
|
60
|
+
The traces to analyze.
|
|
61
|
+
"""
|
|
62
|
+
failed_vertices_masks = {vertex: [] for vertex in self.informative_vertices}
|
|
63
|
+
for trace in traces:
|
|
64
|
+
active_vertices = np.array(list(trace.get_all_activated_vertices()))
|
|
65
|
+
failed_vertices = np.array(list(trace.get_all_failed_vertices()))
|
|
66
|
+
for vertex in failed_vertices:
|
|
67
|
+
parents = self.graph.get_parents(vertex, out_type=np.array)
|
|
68
|
+
active_parents_mask = np.in1d(parents, active_vertices)
|
|
69
|
+
failed_vertices_masks[vertex].append(active_parents_mask)
|
|
70
|
+
|
|
71
|
+
self._failed_vertices_masks = {vertex: np.vstack(masks) for vertex, masks in failed_vertices_masks.items()
|
|
72
|
+
if len(masks) > 0}
|
|
73
|
+
|
|
74
|
+
def _precompute_active_vertices_parents_masks_from_traces(self, traces: Traces) -> None:
|
|
75
|
+
"""Compute active vertices' parents masks from traces.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
traces : Traces
|
|
80
|
+
The traces to analyze.
|
|
81
|
+
"""
|
|
82
|
+
vertex_2_active_parent_mask_tm1 = {vertex: [] for vertex in self.informative_vertices}
|
|
83
|
+
vertex_2_active_parent_mask_t = {vertex: [] for vertex in self.informative_vertices}
|
|
84
|
+
|
|
85
|
+
for trace in traces:
|
|
86
|
+
cum_trace_list = [set()] + list(trace.cum_union())
|
|
87
|
+
for new_vertices_tp1, vertices_t, vertices_tm1 in zip(trace[1:], cum_trace_list[1:-1], cum_trace_list[:-2]):
|
|
88
|
+
for vertex in new_vertices_tp1:
|
|
89
|
+
parents = self.graph.get_parents(vertex, out_type=np.array)
|
|
90
|
+
active_parents_mask_t = np.in1d(parents, np.array(list(vertices_t)))
|
|
91
|
+
active_parents_mask_tm1 = np.in1d(parents, np.array(list(vertices_tm1)))
|
|
92
|
+
vertex_2_active_parent_mask_tm1[vertex].append(active_parents_mask_tm1)
|
|
93
|
+
vertex_2_active_parent_mask_t[vertex].append(active_parents_mask_t)
|
|
94
|
+
|
|
95
|
+
self._vertex_2_active_parent_mask_tm1 = {vertex: np.vstack(masks)
|
|
96
|
+
for vertex, masks in vertex_2_active_parent_mask_tm1.items()
|
|
97
|
+
if len(masks) > 0}
|
|
98
|
+
self._vertex_2_active_parent_mask_t = {vertex: np.vstack(masks)
|
|
99
|
+
for vertex, masks in vertex_2_active_parent_mask_t.items()
|
|
100
|
+
if len(masks) > 0}
|
|
101
|
+
|
|
102
|
+
def _load_masks(self, masks_path: str) -> None:
|
|
103
|
+
"""Load masks from specified file paths.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
masks_path : str
|
|
108
|
+
Path to the directory containing mask files.
|
|
109
|
+
"""
|
|
110
|
+
with open(os.path.join(masks_path, "activated_masks_tm1.pkl"), "rb") as f:
|
|
111
|
+
self._vertex_2_active_parent_mask_tm1 = pickle.load(f)
|
|
112
|
+
with open(os.path.join(masks_path, "activated_masks_t.pkl"), "rb") as f:
|
|
113
|
+
self._vertex_2_active_parent_mask_t = pickle.load(f)
|
|
114
|
+
with open(os.path.join(masks_path, "failed_masks.pkl"), "rb") as f:
|
|
115
|
+
self._failed_vertices_masks = pickle.load(f)
|
|
116
|
+
|
|
117
|
+
def _precompute_all_informative_vertices(self, traces: Union[Traces, PseudoTraces]) -> None:
|
|
118
|
+
"""Precompute all informative vertices from traces.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
traces : Union[Traces, PseudoTraces]
|
|
123
|
+
The traces to analyze.
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(traces, Traces):
|
|
126
|
+
self.informative_vertices = multiple_union(
|
|
127
|
+
[trace.get_all_failed_and_activated_vertices_no_seed() for trace in traces])
|
|
128
|
+
elif isinstance(traces, PseudoTraces):
|
|
129
|
+
self.informative_vertices = set(traces.keys())
|
|
130
|
+
else:
|
|
131
|
+
raise NotImplementedError("traces should be of either Traces or PseudoTraces types")
|
|
132
|
+
|
|
133
|
+
def _precompute_vertices_parents_masks_from_pseudo_traces(self, traces: PseudoTraces) -> None:
|
|
134
|
+
"""Compute parents masks from pseudo traces.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
traces : PseudoTraces
|
|
139
|
+
The pseudo traces to analyze.
|
|
140
|
+
"""
|
|
141
|
+
vertex_2_active_parent_mask_tm1 = {vertex: [] for vertex in self.informative_vertices}
|
|
142
|
+
vertex_2_active_parent_mask_t = {vertex: [] for vertex in self.informative_vertices}
|
|
143
|
+
failed_vertices_masks = {vertex: [] for vertex in self.informative_vertices}
|
|
144
|
+
|
|
145
|
+
for vertex, vertex_pseudo_traces in tqdm(traces.items()):
|
|
146
|
+
for vertices_tm1, new_vertices_t in vertex_pseudo_traces:
|
|
147
|
+
parents = self.graph.get_parents(vertex, out_type=np.array)
|
|
148
|
+
active_parents_mask_tm1 = np.in1d(parents, np.array(list(vertices_tm1)))
|
|
149
|
+
if new_vertices_t:
|
|
150
|
+
vertices_t = vertices_tm1 | new_vertices_t
|
|
151
|
+
active_parents_mask_t = np.in1d(parents, np.array(list(vertices_t)))
|
|
152
|
+
vertex_2_active_parent_mask_tm1[vertex].append(active_parents_mask_tm1)
|
|
153
|
+
vertex_2_active_parent_mask_t[vertex].append(active_parents_mask_t)
|
|
154
|
+
else:
|
|
155
|
+
failed_vertices_masks[vertex].append(active_parents_mask_tm1)
|
|
156
|
+
|
|
157
|
+
self._failed_vertices_masks = {vertex: np.vstack(masks)
|
|
158
|
+
for vertex, masks in failed_vertices_masks.items() if len(masks) > 0}
|
|
159
|
+
self._vertex_2_active_parent_mask_tm1 = {vertex: np.vstack(masks)
|
|
160
|
+
for vertex, masks in vertex_2_active_parent_mask_tm1.items()
|
|
161
|
+
if len(masks) > 0}
|
|
162
|
+
self._vertex_2_active_parent_mask_t = {vertex: np.vstack(masks)
|
|
163
|
+
for vertex, masks in vertex_2_active_parent_mask_t.items()
|
|
164
|
+
if len(masks) > 0}
|
|
165
|
+
|
|
166
|
+
def _precompute_vertices_parents_masks(self, traces: Union[Traces, PseudoTraces]) -> None:
|
|
167
|
+
"""Precompute vertices' parents masks based on traces.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
traces : Union[Traces, PseudoTraces]
|
|
172
|
+
The traces to analyze.
|
|
173
|
+
"""
|
|
174
|
+
if isinstance(traces, Traces):
|
|
175
|
+
self._precompute_failed_vertices_parents_masks_from_traces(traces)
|
|
176
|
+
self._precompute_active_vertices_parents_masks_from_traces(traces)
|
|
177
|
+
elif isinstance(traces, PseudoTraces):
|
|
178
|
+
self._precompute_vertices_parents_masks_from_pseudo_traces(traces)
|
|
179
|
+
else:
|
|
180
|
+
raise NotImplementedError("traces can only be of type Traces or PseudoTraces")
|
|
181
|
+
|
|
182
|
+
def _preprocess_traces(self, traces: Union[Traces, PseudoTraces], masks_path: str = None) -> None:
|
|
183
|
+
"""Preprocess traces by validating and computing masks.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
traces : Union[Traces, PseudoTraces]
|
|
188
|
+
The traces to preprocess.
|
|
189
|
+
masks_path : Optional[str]
|
|
190
|
+
Path to load masks from, if applicable.
|
|
191
|
+
"""
|
|
192
|
+
traces = self._validate_traces(traces)
|
|
193
|
+
self._precompute_all_informative_vertices(traces)
|
|
194
|
+
if masks_path is None:
|
|
195
|
+
self._precompute_vertices_parents_masks(traces)
|
|
196
|
+
else:
|
|
197
|
+
self._load_masks(masks_path)
|
|
198
|
+
self._precompute_num_traces_vertices_participated()
|
|
199
|
+
|
|
200
|
+
def _validate_traces(self, traces) -> Union[Traces, PseudoTraces]:
|
|
201
|
+
"""Validate and convert traces to Traces or PseudoTraces.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
traces : any
|
|
206
|
+
The traces to validate.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
Union[Traces, PseudoTraces]
|
|
211
|
+
The validated traces.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
return Traces(self.graph, traces)
|
|
215
|
+
except:
|
|
216
|
+
return PseudoTraces(traces)
|
|
217
|
+
|
|
218
|
+
def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray]) -> None:
|
|
219
|
+
"""Check the correctness of initial weights.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
init_weights : Union[List, np.ndarray]
|
|
224
|
+
Initial weights to validate.
|
|
225
|
+
|
|
226
|
+
Raises
|
|
227
|
+
------
|
|
228
|
+
AssertionError
|
|
229
|
+
If the length of the weights does not match the number of edges.
|
|
230
|
+
"""
|
|
231
|
+
assert len(init_weights) == self.graph.count_edges()
|
|
232
|
+
|
|
233
|
+
def _init_weights(self, init_weights: Union[List, np.ndarray]) -> None:
|
|
234
|
+
"""Initialize weights based on provided initial weights or generate random weights.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
init_weights : Optional[Union[List, np.ndarray]]
|
|
239
|
+
Initial weights to set. If None, random weights are generated.
|
|
240
|
+
"""
|
|
241
|
+
if init_weights is not None:
|
|
242
|
+
self._check_init_weight_correctness(init_weights)
|
|
243
|
+
self.weights_ = np.array(init_weights)
|
|
244
|
+
else:
|
|
245
|
+
self.weights_ = self._generate_random_weights()
|
|
246
|
+
|
|
247
|
+
def _generate_random_weights(self) -> np.ndarray:
|
|
248
|
+
"""Generate random weights for edges.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
np.ndarray
|
|
253
|
+
Randomly generated weights.
|
|
254
|
+
"""
|
|
255
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
256
|
+
|
|
257
|
+
def _optimize_vertex_parent_params(self, vertex: int, **optim_kwargs) -> np.ndarray:
|
|
258
|
+
"""Optimize parameters for a specific vertex.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
vertex : int
|
|
263
|
+
The vertex to optimize parameters for.
|
|
264
|
+
optim_kwargs : dict
|
|
265
|
+
Additional optimization parameters.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
np.ndarray
|
|
270
|
+
Optimized parameters for the vertex.
|
|
271
|
+
"""
|
|
272
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
273
|
+
|
|
274
|
+
def _optimize_informative_vertices_parent_params(self, verbose: bool = False, **optim_kwargs) -> List[np.ndarray]:
|
|
275
|
+
"""Optimize parameters for all informative vertices in parallel.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
verbose : bool, optional
|
|
280
|
+
If True, progress messages are printed.
|
|
281
|
+
optim_kwargs : dict
|
|
282
|
+
Additional optimization parameters.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
List[np.ndarray]
|
|
287
|
+
List of optimized parameters for each informative vertex.
|
|
288
|
+
"""
|
|
289
|
+
informative_vertices_params = Parallel(n_jobs=self.n_jobs, verbose=verbose)(
|
|
290
|
+
delayed(self._optimize_vertex_parent_params)(vertex, **optim_kwargs)
|
|
291
|
+
for vertex in self.informative_vertices)
|
|
292
|
+
return informative_vertices_params
|
|
293
|
+
|
|
294
|
+
def _set_informative_vertices_parent_params(self, informative_vertices_params: List[np.ndarray]) -> None:
|
|
295
|
+
"""Set the optimized parameters for informative vertices.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
informative_vertices_params : List[np.ndarray]
|
|
300
|
+
List of optimized parameters for each informative vertex.
|
|
301
|
+
"""
|
|
302
|
+
for vertex, parent_weights in zip(self.informative_vertices, informative_vertices_params):
|
|
303
|
+
self.weights_[self.graph.get_parents_mask(vertex)] = parent_weights
|
|
304
|
+
|
|
305
|
+
def _pre_fit(self, traces: Union[Traces, PseudoTraces],
|
|
306
|
+
init_weights: Union[List, np.ndarray] = None,
|
|
307
|
+
masks_path: str = None) -> None:
|
|
308
|
+
"""Prepare for fitting by preprocessing traces and initializing weights.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
traces : Union[Traces, PseudoTraces]
|
|
313
|
+
The traces to analyze.
|
|
314
|
+
init_weights : Optional[Union[List, np.ndarray]]
|
|
315
|
+
Initial weights to set.
|
|
316
|
+
masks_path : Optional[str]
|
|
317
|
+
Path to load masks from.
|
|
318
|
+
"""
|
|
319
|
+
self._preprocess_traces(traces, masks_path=masks_path)
|
|
320
|
+
self._init_weights(init_weights)
|
|
321
|
+
|
|
322
|
+
def fit(self, traces: Union[Traces, PseudoTraces],
|
|
323
|
+
init_weights: Union[List, np.ndarray] = None,
|
|
324
|
+
verbose: bool = False,
|
|
325
|
+
masks_path: str = None,
|
|
326
|
+
**optim_kwargs) -> np.ndarray:
|
|
327
|
+
"""Fit the model to the given traces.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
traces : Union[Traces, PseudoTraces]
|
|
332
|
+
The traces to analyze.
|
|
333
|
+
init_weights : Optional[Union[List, np.ndarray]]
|
|
334
|
+
Initial weights to set.
|
|
335
|
+
verbose : bool, optional
|
|
336
|
+
If True, progress messages are printed.
|
|
337
|
+
masks_path : Optional[str]
|
|
338
|
+
Path to load masks from.
|
|
339
|
+
optim_kwargs : dict
|
|
340
|
+
Additional optimization parameters.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
np.ndarray
|
|
345
|
+
The final estimated weights.
|
|
346
|
+
"""
|
|
347
|
+
self._pre_fit(traces, init_weights=init_weights, masks_path=masks_path)
|
|
348
|
+
informative_vertices_params = self._optimize_informative_vertices_parent_params(verbose=verbose, **optim_kwargs)
|
|
349
|
+
self._set_informative_vertices_parent_params(informative_vertices_params)
|
|
350
|
+
return self.weights_
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class BaseGLTWEightEstimator(BaseWeightEstimator):
|
|
354
|
+
"""Base class for GLTW weight estimation.
|
|
355
|
+
|
|
356
|
+
Attributes
|
|
357
|
+
----------
|
|
358
|
+
vertex_2_distrib : Dict[int, scipy.stats.rv_frozen]
|
|
359
|
+
Distribution mapping for vertices.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(self, graph: Graph, n_jobs: int = None,
|
|
363
|
+
vertex_2_distrib: Dict[int, rv_frozen] = None):
|
|
364
|
+
super().__init__(graph, n_jobs)
|
|
365
|
+
if vertex_2_distrib is None:
|
|
366
|
+
vertex_2_distrib = {v: uniform(0, 1) for v in graph.get_vertices()}
|
|
367
|
+
self.vertex_2_distrib = vertex_2_distrib
|
|
368
|
+
|
|
369
|
+
def _generate_random_weights(self) -> np.ndarray:
|
|
370
|
+
"""Generate random weights based on vertex distributions.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
np.ndarray
|
|
375
|
+
Randomly generated weights for edges.
|
|
376
|
+
"""
|
|
377
|
+
weights = np.zeros(self.graph.count_edges())
|
|
378
|
+
for vertex in self.informative_vertices:
|
|
379
|
+
parent_mask = self.graph.get_parents_mask(vertex)
|
|
380
|
+
num_parents = self.graph.get_indegree(vertex, weighted=False)
|
|
381
|
+
support_ub = self.vertex_2_distrib[vertex].support()[1]
|
|
382
|
+
if support_ub == np.inf:
|
|
383
|
+
weights[parent_mask] = np.random.exponential(num_parents)
|
|
384
|
+
else:
|
|
385
|
+
weights[parent_mask] = np.random.rand(num_parents) * support_ub / num_parents
|
|
386
|
+
return weights
|
|
387
|
+
|
|
388
|
+
def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray], eps: float = 1e-6) -> None:
|
|
389
|
+
"""Check the correctness of initial weights for GLTM.
|
|
390
|
+
|
|
391
|
+
Parameters
|
|
392
|
+
----------
|
|
393
|
+
init_weights : Union[List, np.ndarray]
|
|
394
|
+
Initial weights to validate.
|
|
395
|
+
eps : float, optional
|
|
396
|
+
Tolerance for weight validation.
|
|
397
|
+
|
|
398
|
+
Raises
|
|
399
|
+
------
|
|
400
|
+
AssertionError
|
|
401
|
+
If initial weights do not meet the criteria.
|
|
402
|
+
"""
|
|
403
|
+
super()._check_init_weight_correctness(init_weights)
|
|
404
|
+
indegrees = np.array([init_weights[self.graph.get_parents_mask(vertex)].sum()
|
|
405
|
+
for vertex in self.vertex_2_distrib])
|
|
406
|
+
vertex_supports = np.vstack([distrib.support() for vertex, distrib in self.vertex_2_distrib.items()])
|
|
407
|
+
assert np.all((indegrees >= vertex_supports[:, 0] - eps) & (indegrees <= vertex_supports[:, 1] + eps))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class BaseICWEightEstimator(BaseWeightEstimator):
|
|
411
|
+
"""Base class for Independent Cascade Model weight estimation."""
|
|
412
|
+
|
|
413
|
+
def _generate_random_weights(self) -> np.ndarray:
|
|
414
|
+
"""Generate random weights for edges uniformly distributed between 0 and 1.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
np.ndarray
|
|
419
|
+
Randomly generated weights for edges.
|
|
420
|
+
"""
|
|
421
|
+
return np.random.rand(self.graph.count_edges())
|
|
422
|
+
|
|
423
|
+
def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray]) -> None:
|
|
424
|
+
"""Check the correctness of initial weights for the Independent Cascade Model.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
init_weights : Union[List, np.ndarray]
|
|
429
|
+
Initial weights to validate.
|
|
430
|
+
|
|
431
|
+
Raises
|
|
432
|
+
------
|
|
433
|
+
AssertionError
|
|
434
|
+
If initial weights are not within the range [0, 1].
|
|
435
|
+
"""
|
|
436
|
+
assert np.all((init_weights >= 0) & (init_weights <= 1))
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from BaseWeightEstimator import BaseWeightEstimator, BaseGLTWEightEstimator, BaseICWEightEstimator
|
|
5
|
+
from ..Trace import Traces, PseudoTraces
|
|
6
|
+
from ..utils import invert_non_zeros
|
|
7
|
+
from ..Graph import Graph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseWeightEstimatorEM(BaseWeightEstimator):
|
|
11
|
+
"""Base class for EM weight estimators.
|
|
12
|
+
|
|
13
|
+
This class implements the EM algorithm for optimizing parent weights of vertices in a graph.
|
|
14
|
+
|
|
15
|
+
Methods
|
|
16
|
+
-------
|
|
17
|
+
_em_step(vertex, vertex_parent_weights)
|
|
18
|
+
Performs a single EM step for the specified vertex.
|
|
19
|
+
_optimize_vertex_parent_params(vertex, max_iter=50, tol=1e-3)
|
|
20
|
+
Optimizes the parent weights for the specified vertex using the EM algorithm.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
|
|
24
|
+
"""Perform a single EM step for the specified vertex.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
vertex : int
|
|
29
|
+
The vertex for which to perform the EM step.
|
|
30
|
+
vertex_parent_weights : np.array
|
|
31
|
+
The current weights of the parent vertices.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
np.array
|
|
36
|
+
The updated weights for the parent vertices.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
def _optimize_vertex_parent_params(self, vertex: int, max_iter: int = 50, tol: float = 1e-3) -> np.array:
|
|
41
|
+
"""Optimize the parent weights for the specified vertex using the EM algorithm.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
vertex : int
|
|
46
|
+
The vertex for which to optimize parent weights.
|
|
47
|
+
max_iter : int, optional
|
|
48
|
+
The maximum number of iterations (default is 50).
|
|
49
|
+
tol : float, optional
|
|
50
|
+
The tolerance for convergence (default is 1e-3).
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
np.array
|
|
55
|
+
The optimized weights for the parent vertices.
|
|
56
|
+
"""
|
|
57
|
+
parent_weights = self.weights_[self.graph.get_parents_mask(vertex)]
|
|
58
|
+
|
|
59
|
+
for _ in range(max_iter):
|
|
60
|
+
new_parent_weights = self._em_step(vertex=vertex, vertex_parent_weights=parent_weights)
|
|
61
|
+
parent_weight_change = np.linalg.norm(new_parent_weights - parent_weights)
|
|
62
|
+
parent_weights = new_parent_weights
|
|
63
|
+
if parent_weight_change < tol:
|
|
64
|
+
break
|
|
65
|
+
return parent_weights
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ICWeightEstimatorEM(BaseICWEightEstimator, BaseWeightEstimatorEM):
|
|
69
|
+
"""Independent Cascade Weight Estimator using EM algorithm.
|
|
70
|
+
|
|
71
|
+
This class extends the base EM weight estimator for the Independent Cascade model.
|
|
72
|
+
|
|
73
|
+
Methods
|
|
74
|
+
-------
|
|
75
|
+
_precompute_num_traces_parents_activated()
|
|
76
|
+
Precomputes the number of traces where parents were activated for each vertex.
|
|
77
|
+
_em_step(vertex, vertex_parent_weights)
|
|
78
|
+
Performs a single EM step for the specified vertex.
|
|
79
|
+
_preprocess_traces(traces: Union[Traces, PseudoTraces], masks_path: str = None)
|
|
80
|
+
Preprocesses the input traces and prepares necessary data for EM steps.
|
|
81
|
+
_generate_random_weights()
|
|
82
|
+
Generates random initial weights for the parent vertices.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def _precompute_num_traces_parents_activated(self) -> None:
|
|
86
|
+
"""Precompute the number of traces where parents were activated for each vertex.
|
|
87
|
+
|
|
88
|
+
This method updates the internal state to keep track of how many traces
|
|
89
|
+
activated the parent vertices of the informative vertices.
|
|
90
|
+
"""
|
|
91
|
+
self._vertex_2_num_traces_parents_activated = {}
|
|
92
|
+
for vertex in self.informative_vertices:
|
|
93
|
+
n_traces_parents_activated = np.zeros(self.graph.get_indegree(vertex))
|
|
94
|
+
if vertex in self._vertex_2_active_parent_mask_t:
|
|
95
|
+
n_traces_parents_activated += self._vertex_2_active_parent_mask_t[vertex].sum(0)
|
|
96
|
+
if vertex in self._failed_vertices_masks:
|
|
97
|
+
n_traces_parents_activated += self._failed_vertices_masks[vertex].sum(0)
|
|
98
|
+
self._vertex_2_num_traces_parents_activated[vertex] = n_traces_parents_activated
|
|
99
|
+
|
|
100
|
+
def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
|
|
101
|
+
"""Perform a single EM step for the specified vertex.
|
|
102
|
+
|
|
103
|
+
This method computes the updated weights based on the current parent weights.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
vertex : int
|
|
108
|
+
The vertex for which to perform the EM step.
|
|
109
|
+
vertex_parent_weights : np.array
|
|
110
|
+
The current weights of the parent vertices.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
np.array
|
|
115
|
+
The updated weights for the parent vertices.
|
|
116
|
+
"""
|
|
117
|
+
if vertex not in self._vertex_2_active_parent_mask_t:
|
|
118
|
+
return np.zeros_like(vertex_parent_weights)
|
|
119
|
+
|
|
120
|
+
active_parents_mask_t = self._vertex_2_active_parent_mask_t[vertex]
|
|
121
|
+
active_parents_mask_tm1 = self._vertex_2_active_parent_mask_tm1[vertex]
|
|
122
|
+
new_active_parents_mask = active_parents_mask_t & (~active_parents_mask_tm1)
|
|
123
|
+
|
|
124
|
+
no_activation_probs = new_active_parents_mask * (1. - vertex_parent_weights)
|
|
125
|
+
probs_vertex_activated = 1. - np.prod(no_activation_probs, axis=1, where=no_activation_probs > 0, keepdims=True)
|
|
126
|
+
inv_probs_vertex_activated = invert_non_zeros(probs_vertex_activated)
|
|
127
|
+
upd_weights = vertex_parent_weights * (new_active_parents_mask * inv_probs_vertex_activated).sum(0)
|
|
128
|
+
|
|
129
|
+
n_traces_parents_activated = self._vertex_2_num_traces_parents_activated[vertex]
|
|
130
|
+
seen_parents_mask = n_traces_parents_activated != 0
|
|
131
|
+
upd_weights[seen_parents_mask] /= n_traces_parents_activated[seen_parents_mask]
|
|
132
|
+
return np.clip(upd_weights, 0, 1)
|
|
133
|
+
|
|
134
|
+
def _preprocess_traces(self, traces: Union[Traces, PseudoTraces], masks_path: str = None) -> None:
|
|
135
|
+
"""Preprocesses the input traces and prepares necessary data for EM steps.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
traces : Union[Traces, PseudoTraces]
|
|
140
|
+
The input traces or pseudotraces to analyze.
|
|
141
|
+
masks_path : str, optional
|
|
142
|
+
The path to load masks from (default is None).
|
|
143
|
+
"""
|
|
144
|
+
super()._preprocess_traces(traces=traces, masks_path=masks_path)
|
|
145
|
+
self._precompute_num_traces_parents_activated()
|
|
146
|
+
|
|
147
|
+
def _generate_random_weights(self) -> np.array:
|
|
148
|
+
"""Generates random initial weights for the parent vertices.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
np.array
|
|
153
|
+
An array of random weights for the parent vertices.
|
|
154
|
+
"""
|
|
155
|
+
weights = np.zeros(self.graph.count_edges())
|
|
156
|
+
informative_edge_mask = np.isin(self.graph.edge_array[:, 1], list(self.informative_vertices))
|
|
157
|
+
weights[informative_edge_mask] = np.random.rand(informative_edge_mask.sum())
|
|
158
|
+
return weights
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class LTWeightEstimatorEM(BaseGLTWEightEstimator, BaseWeightEstimatorEM):
|
|
162
|
+
"""Linear Threshold Weight Estimator using EM algorithm.
|
|
163
|
+
|
|
164
|
+
This class extends the base EM weight estimator for the Linear Threshold model.
|
|
165
|
+
|
|
166
|
+
Methods
|
|
167
|
+
-------
|
|
168
|
+
_em_step(vertex, vertex_parent_weights)
|
|
169
|
+
Performs a single EM step for the specified vertex.
|
|
170
|
+
_generate_random_weights()
|
|
171
|
+
Generates random initial weights for the parent vertices.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, graph: Graph, n_jobs: int = None):
|
|
175
|
+
"""Initialize the Linear Threshold Weight Estimator.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
graph : Graph
|
|
180
|
+
The graph representing the relationships between vertices.
|
|
181
|
+
n_jobs : int, optional
|
|
182
|
+
The number of jobs for parallel processing (default is None).
|
|
183
|
+
"""
|
|
184
|
+
super().__init__(graph, n_jobs=n_jobs)
|
|
185
|
+
|
|
186
|
+
def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
|
|
187
|
+
"""Perform a single EM step for the specified vertex.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
vertex : int
|
|
192
|
+
The vertex for which to perform the EM step.
|
|
193
|
+
vertex_parent_weights : np.array
|
|
194
|
+
The current weights of the parent vertices.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
np.array
|
|
199
|
+
The updated weights for the parent vertices.
|
|
200
|
+
"""
|
|
201
|
+
if vertex in self._vertex_2_active_parent_mask_t:
|
|
202
|
+
active_parents_mask_t = self._vertex_2_active_parent_mask_t[vertex]
|
|
203
|
+
active_parents_mask_tm1 = self._vertex_2_active_parent_mask_tm1[vertex]
|
|
204
|
+
new_active_parents_mask = active_parents_mask_t & (~active_parents_mask_tm1)
|
|
205
|
+
probs_vertex_activated = (new_active_parents_mask * vertex_parent_weights).sum(1).reshape(-1, 1)
|
|
206
|
+
inv_probs_vertex_activated = invert_non_zeros(probs_vertex_activated)
|
|
207
|
+
H_uvs = vertex_parent_weights * (new_active_parents_mask * inv_probs_vertex_activated).sum(0)
|
|
208
|
+
else:
|
|
209
|
+
H_uvs = np.zeros_like(vertex_parent_weights)
|
|
210
|
+
|
|
211
|
+
if vertex in self._failed_vertices_masks:
|
|
212
|
+
active_parents_T = self._failed_vertices_masks[vertex]
|
|
213
|
+
probs_vertex_failed = 1. - (vertex_parent_weights * active_parents_T).sum(1).reshape(-1, 1)
|
|
214
|
+
inv_probs_vertex_failed = invert_non_zeros(probs_vertex_failed)
|
|
215
|
+
H_empty_v = (1. - vertex_parent_weights.sum()) * inv_probs_vertex_failed.sum()
|
|
216
|
+
H_uvs += vertex_parent_weights * ((~active_parents_T) * inv_probs_vertex_failed).sum(0)
|
|
217
|
+
else:
|
|
218
|
+
H_empty_v = 0.
|
|
219
|
+
|
|
220
|
+
upd_weights = H_uvs / (H_empty_v + H_uvs.sum())
|
|
221
|
+
return upd_weights
|
|
222
|
+
|
|
223
|
+
def _generate_random_weights(self) -> np.array:
|
|
224
|
+
"""Generates random initial weights for the parent vertices.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
np.array
|
|
229
|
+
An array of random weights for the parent vertices.
|
|
230
|
+
"""
|
|
231
|
+
weights = np.zeros(self.graph.count_edges())
|
|
232
|
+
for vertex in self.informative_vertices:
|
|
233
|
+
parent_mask = self.graph.get_parents_mask(vertex)
|
|
234
|
+
num_parents = self.graph.get_indegree(vertex, weighted=False)
|
|
235
|
+
weights[parent_mask] = np.random.rand(num_parents) / num_parents
|
|
236
|
+
return weights
|