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,291 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Union, Dict, Tuple
|
|
3
|
+
from scipy.optimize import LinearConstraint, minimize
|
|
4
|
+
from scipy.stats._distn_infrastructure import rv_frozen
|
|
5
|
+
from ..Graph import Graph
|
|
6
|
+
from BaseWeightEstimator import BaseGLTWEightEstimator
|
|
7
|
+
from ..Trace import Traces, PseudoTraces
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GLTWeightEstimator(BaseGLTWEightEstimator):
|
|
11
|
+
"""GLTW Weight Estimator for modeling influence in networks.
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
vertex_2_distrib : Dict[int, rv_frozen]
|
|
16
|
+
Distribution mapping for vertices.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def _compute_failed_vertex_ll(self, weights: np.ndarray, vertex: int) -> float:
|
|
20
|
+
"""Compute the log-likelihood for failed vertices.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
weights : np.ndarray
|
|
25
|
+
The weights for the graph edges.
|
|
26
|
+
vertex : int
|
|
27
|
+
The vertex index.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
float
|
|
32
|
+
The log-likelihood for the failed vertex.
|
|
33
|
+
"""
|
|
34
|
+
if vertex not in self._failed_vertices_masks:
|
|
35
|
+
return 0.0
|
|
36
|
+
cdf = self.vertex_2_distrib[vertex].cdf
|
|
37
|
+
failed_vertices_indeg = self._failed_vertices_masks[vertex] @ weights
|
|
38
|
+
failure_probs = 1.0 - cdf(failed_vertices_indeg)
|
|
39
|
+
ll = np.log(np.clip(failure_probs, 1e-8, None)).sum()
|
|
40
|
+
return ll
|
|
41
|
+
|
|
42
|
+
def _compute_activated_vertex_ll(self, weights: np.ndarray, vertex: int) -> float:
|
|
43
|
+
"""Compute the log-likelihood for activated vertices.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
weights : np.ndarray
|
|
48
|
+
The weights for the graph edges.
|
|
49
|
+
vertex : int
|
|
50
|
+
The vertex index.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
float
|
|
55
|
+
The log-likelihood for the activated vertex.
|
|
56
|
+
"""
|
|
57
|
+
if vertex not in self._vertex_2_active_parent_mask_tm1:
|
|
58
|
+
return 0.0
|
|
59
|
+
cdf = self.vertex_2_distrib[vertex].cdf
|
|
60
|
+
activated_vertices_indeg_tm1 = self._vertex_2_active_parent_mask_tm1[vertex] @ weights
|
|
61
|
+
activated_vertices_indeg_t = self._vertex_2_active_parent_mask_t[vertex] @ weights
|
|
62
|
+
activation_probs = cdf(activated_vertices_indeg_t) - cdf(activated_vertices_indeg_tm1)
|
|
63
|
+
ll = np.log(np.clip(activation_probs, 1e-8, None)).sum()
|
|
64
|
+
return ll
|
|
65
|
+
|
|
66
|
+
def _compute_vertex_nll(self, weights: np.ndarray, vertex: int) -> float:
|
|
67
|
+
"""Compute the negative log-likelihood for a vertex.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
weights : np.ndarray
|
|
72
|
+
The weights for the graph edges.
|
|
73
|
+
vertex : int
|
|
74
|
+
The vertex index.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
float
|
|
79
|
+
The negative log-likelihood for the vertex.
|
|
80
|
+
"""
|
|
81
|
+
ll = self._compute_activated_vertex_ll(weights, vertex) + self._compute_failed_vertex_ll(weights, vertex)
|
|
82
|
+
return -ll
|
|
83
|
+
|
|
84
|
+
def _compute_total_nll(self, weights: np.ndarray) -> float:
|
|
85
|
+
"""Compute the total negative log-likelihood for informative vertices.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
weights : np.ndarray
|
|
90
|
+
The weights for the graph edges.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
float
|
|
95
|
+
The total negative log-likelihood.
|
|
96
|
+
"""
|
|
97
|
+
return np.sum([
|
|
98
|
+
self._compute_vertex_nll(weights[self.graph.get_parents_mask(vertex)], vertex)
|
|
99
|
+
for vertex in self.informative_vertices
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
def _compute_normalized_vertex_nll(self, weights: np.ndarray, vertex: int) -> float:
|
|
103
|
+
"""Compute the normalized negative log-likelihood for a vertex.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
weights : np.ndarray
|
|
108
|
+
The weights for the graph edges.
|
|
109
|
+
vertex : int
|
|
110
|
+
The vertex index.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
float
|
|
115
|
+
The normalized negative log-likelihood for the vertex.
|
|
116
|
+
"""
|
|
117
|
+
return self._compute_vertex_nll(weights, vertex) / self._vertex_2_num_traces_was_informative[vertex]
|
|
118
|
+
|
|
119
|
+
def _make_parent_weight_constraints(self, vertex: int, eps: float = 1e-8) -> LinearConstraint:
|
|
120
|
+
"""Create constraints for parent weights of a vertex.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
vertex : int
|
|
125
|
+
The vertex index.
|
|
126
|
+
eps : float, optional
|
|
127
|
+
Small value for numerical stability.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
LinearConstraint
|
|
132
|
+
The linear constraints for optimization.
|
|
133
|
+
"""
|
|
134
|
+
indeg_max = self.vertex_2_distrib[vertex].support()[1]
|
|
135
|
+
num_parents = self.graph.get_indegree(vertex, weighted=False)
|
|
136
|
+
|
|
137
|
+
mat_constrain = np.vstack([np.eye(num_parents), np.ones(num_parents)])
|
|
138
|
+
lb = eps * np.ones(num_parents + 1)
|
|
139
|
+
ub = (indeg_max - eps) * np.ones(num_parents + 1)
|
|
140
|
+
return LinearConstraint(A=mat_constrain, lb=lb, ub=ub)
|
|
141
|
+
|
|
142
|
+
def _precompute_all_constraints(self) -> None:
|
|
143
|
+
"""Precompute weight constraints for all informative vertices."""
|
|
144
|
+
self._weight_constraints = {
|
|
145
|
+
vertex: self._make_parent_weight_constraints(vertex)
|
|
146
|
+
for vertex in self.informative_vertices
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) -> Tuple[np.ndarray, rv_frozen]:
|
|
150
|
+
"""Optimize parameters for a specific vertex.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
vertex : int
|
|
155
|
+
The vertex to optimize.
|
|
156
|
+
optimization_kwargs : Dict, optional
|
|
157
|
+
Additional optimization parameters.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
Tuple[np.ndarray, rv_frozen]
|
|
162
|
+
Optimized weights and the vertex distribution.
|
|
163
|
+
"""
|
|
164
|
+
optimizer_output = minimize(
|
|
165
|
+
lambda weights: self._compute_normalized_vertex_nll(weights, vertex=vertex),
|
|
166
|
+
x0=self.weights_[self.graph.get_parents_mask(vertex)],
|
|
167
|
+
method='SLSQP',
|
|
168
|
+
constraints=self._weight_constraints[vertex],
|
|
169
|
+
options=optimization_kwargs
|
|
170
|
+
)
|
|
171
|
+
return optimizer_output.x, self.vertex_2_distrib[vertex]
|
|
172
|
+
|
|
173
|
+
def _set_informative_vertices_parent_params(self, informative_vertices_params: List[Tuple[np.ndarray, rv_frozen]]) -> None:
|
|
174
|
+
"""Set the optimized parameters for informative vertices.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
informative_vertices_params : List[Tuple[np.ndarray, rv_frozen]]
|
|
179
|
+
List of optimized parameters for each informative vertex.
|
|
180
|
+
"""
|
|
181
|
+
for vertex, (parent_weights, vertex_distrib) in zip(self.informative_vertices, informative_vertices_params):
|
|
182
|
+
self.weights_[self.graph.get_parents_mask(vertex)] = parent_weights
|
|
183
|
+
self.vertex_2_distrib[vertex] = vertex_distrib
|
|
184
|
+
|
|
185
|
+
def _pre_fit(self, traces: Union[Traces, PseudoTraces],
|
|
186
|
+
init_weights: Union[List[float], np.array] = None,
|
|
187
|
+
masks_path: str = None) -> None:
|
|
188
|
+
"""Prepare for fitting by preprocessing traces and initializing constraints.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
traces : Union[Traces, PseudoTraces]
|
|
193
|
+
The traces to analyze.
|
|
194
|
+
init_weights : Optional[List[float]]
|
|
195
|
+
Initial weights to set.
|
|
196
|
+
masks_path : Optional[str]
|
|
197
|
+
Path to load masks from.
|
|
198
|
+
"""
|
|
199
|
+
self._preprocess_traces(traces, masks_path=masks_path)
|
|
200
|
+
self._precompute_all_constraints()
|
|
201
|
+
self._init_weights(init_weights)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class GLTGridSearchEstimator(GLTWeightEstimator):
|
|
205
|
+
"""GLT Grid Search Estimator for hyperparameter tuning.
|
|
206
|
+
|
|
207
|
+
Attributes
|
|
208
|
+
----------
|
|
209
|
+
vertex_2_distrib_grid : Dict[int, List[rv_frozen]]
|
|
210
|
+
Distribution grid for each vertex.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(self, graph: Graph,
|
|
214
|
+
distribs_grid: Union[Dict[int, List[rv_frozen]], List[rv_frozen]],
|
|
215
|
+
n_jobs: int = 1) -> None:
|
|
216
|
+
"""Initialize the GLT Grid Search Estimator.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
graph : Graph
|
|
221
|
+
The graph structure.
|
|
222
|
+
distribs_grid : Union[Dict[int, List[rv_frozen]], List[rv_frozen]]
|
|
223
|
+
The grid of distributions for each vertex.
|
|
224
|
+
n_jobs : int, optional
|
|
225
|
+
Number of jobs for parallel processing.
|
|
226
|
+
"""
|
|
227
|
+
super().__init__(graph=graph, vertex_2_distrib=None, n_jobs=n_jobs)
|
|
228
|
+
self._validate_grid(distribs_grid)
|
|
229
|
+
|
|
230
|
+
def _validate_grid(self, distribs_grid: Union[Dict[int, List[rv_frozen]], List[rv_frozen]]) -> None:
|
|
231
|
+
"""Validate the distribution grid.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
distribs_grid : Union[Dict[int, List[rv_frozen]], List[rv_frozen]]
|
|
236
|
+
The grid of distributions to validate.
|
|
237
|
+
|
|
238
|
+
Raises
|
|
239
|
+
------
|
|
240
|
+
AssertionError
|
|
241
|
+
If the distribution grid does not match the graph vertices.
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(distribs_grid, (List, Tuple)):
|
|
244
|
+
self.vertex_2_distrib_grid = {vertex: distribs_grid for vertex in self.graph.get_vertices()}
|
|
245
|
+
elif isinstance(distribs_grid, Dict):
|
|
246
|
+
assert set(distribs_grid.keys()) == self.graph.get_vertices(), \
|
|
247
|
+
"If `distribs_grid` is a dict, its keys should be the vertices of the graph."
|
|
248
|
+
self.vertex_2_distrib_grid = distribs_grid
|
|
249
|
+
else:
|
|
250
|
+
raise NotImplementedError("`distribs_grid` should be either a list, tuple, or a dict.")
|
|
251
|
+
|
|
252
|
+
assert all([
|
|
253
|
+
all([isinstance(distrib, rv_frozen) for distrib in vertex_distribs])
|
|
254
|
+
for vertex_distribs in self.vertex_2_distrib_grid.values()
|
|
255
|
+
]), "All elements of the grid should be `rv_frozen`."
|
|
256
|
+
|
|
257
|
+
def _optimize_vertex_parent_params(self, vertex: int, optimization_kwargs: Dict = None) -> Tuple[np.ndarray, rv_frozen]:
|
|
258
|
+
"""Optimize parameters for a specific vertex using grid search.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
vertex : int
|
|
263
|
+
The vertex to optimize.
|
|
264
|
+
optimization_kwargs : Dict, optional
|
|
265
|
+
Additional optimization parameters.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
Tuple[np.ndarray, rv_frozen]
|
|
270
|
+
Best weights and the best distribution for the vertex.
|
|
271
|
+
"""
|
|
272
|
+
best_weights = None
|
|
273
|
+
best_nll = np.inf
|
|
274
|
+
best_distrib = None
|
|
275
|
+
|
|
276
|
+
for distrib in self.vertex_2_distrib_grid[vertex]:
|
|
277
|
+
self.vertex_2_distrib[vertex] = distrib
|
|
278
|
+
optimizer_output = minimize(
|
|
279
|
+
lambda weights: self._compute_normalized_vertex_nll(weights, vertex=vertex),
|
|
280
|
+
x0=self.weights_[self.graph.get_parents_mask(vertex)],
|
|
281
|
+
method='SLSQP',
|
|
282
|
+
constraints=self._weight_constraints[vertex],
|
|
283
|
+
options=optimization_kwargs
|
|
284
|
+
)
|
|
285
|
+
nll = optimizer_output.fun
|
|
286
|
+
if nll < best_nll:
|
|
287
|
+
best_weights = optimizer_output.x
|
|
288
|
+
best_distrib = distrib
|
|
289
|
+
best_nll = nll
|
|
290
|
+
|
|
291
|
+
return best_weights, best_distrib
|