11from ..backend import get_backend
22import numpy as np
3- from .solver_tree import topological_sort
43from ..utils import proj_simplex
54from ..utils import list_to_array
65
98# IMPORTANT : ON PREND COMME CONVENTION QUE LES FEUILLES SONT LES PREMIERS SOMMETS DE L'ARBRE
109
1110
12- def wgm (values , weights ):
13- # Returns the weighted geometric median
14-
15- nx = get_backend (values , weights )
16-
17- sorted_indices = np .argsort (values , kind = "stable" )
18-
19- values_sorted = values [sorted_indices ]
20- weights_sorted = weights [sorted_indices ]
21-
22- cum_weights = nx .cumsum (weights_sorted )
23-
24- id = nx .searchsorted (cum_weights , 0.5 - 1e9 )
25-
26- return values_sorted [id ]
27-
28-
29- def get_measure (z , tree , length ):
30- # Retrieves the measure from a vector after the wgm
31-
32- n = z .shape [0 ]
33-
34- nx = get_backend (length )
35-
36- measure = nx .zeros (n )
37-
38- for i in range (n ):
39- p = tree [i ]
40-
41- if i == p :
42- measure [i ] += 1
43- else :
44- measure [i ] += z [i ] / length [i ]
45- measure [p ] -= z [i ] / length [i ]
46-
47- return measure
48-
49-
50- def tree_barycenter (tree , length , measure , weights , topo_order = None ):
51- r"""
52- Computes the tree wasserstein barycenter for a given tree between multiplie empirical distributions
53-
54- Parameters
55- ----------
56- tree : array_like, shape(n)
57- ancestor of each node in the tree (ancestor of root is root)
58- length : array_like, shape(n)
59- length of the arc above each node (length of root is 0)
60- measure : array_like, shape(m, n)
61- distributions in the tree
62- weights : array_like, shape(m)
63- weight of each distribution
64-
65- Returns
66- -------
67- barycenter : array_like, shape(n)
68- distribution of the barycenter
69-
70- Reference
71- ---------
72- The code is a direct implementation of the algorithm described in
73- Tree-Wasserstein Barycenter for Large-Scale Multilevel Clustering and Scalable Bayes
74-
75- """
76- n_measure = measure .shape [0 ]
77- n_node = tree .shape [0 ]
78-
79- assert n_measure == weights .shape [0 ], "dimension error"
80-
81- nx = get_backend (measure , weights , length )
82-
83- z_measure = nx .zeros ((n_measure , n_node ))
84-
85- if topo_order is None :
86- topo_order = topological_sort (tree )
87-
88- for cur_node in topo_order :
89- p = tree [cur_node ]
90-
91- for id_mes in range (n_measure ):
92- z_measure [id_mes ][cur_node ] += measure [id_mes ][cur_node ]
93-
94- if cur_node != p :
95- z_measure [id_mes ][p ] += z_measure [id_mes ][cur_node ]
96-
97- z = nx .zeros (n_node )
98-
99- for cur_node in range (n_node ):
100- z_measure [:, cur_node ] *= length [cur_node ]
101-
102- z [cur_node ] = wgm (z_measure [:, cur_node ], weights )
103-
104- return get_measure (z , tree , length )
105-
106-
10711def get_B_matrix (tree , length , nb_leafs ):
10812 nx = get_backend (length )
10913
@@ -141,38 +45,14 @@ def get_gradient(cur_B, B_mes_sorted, B, nb_mes, nb_nodes):
14145 return g
14246
14347
144- def fixed_support_tree_barycenter (tree , length , measures , nb_itr = 100 , step = 0.1 ):
145- nx = get_backend (length , measures )
146- nb_leafs = measures .shape [1 ]
147-
148- B = get_B_matrix (tree , length , nb_leafs )
149-
150- nb_mes = measures .shape [0 ]
151- nb_nodes = tree .shape [0 ]
152-
153- cur_mes = nx .ones (nb_leafs ) / nb_leafs
154-
155- B_mes = list_to_array ([B .dot (measures [i ]) for i in range (nb_mes )])
156-
157- sigma = nx .argsort (B_mes , axis = 0 )
158-
159- B_mes_sorted = nx .take_along_axis (B_mes , sigma , axis = 0 )
160-
161- for itr in range (nb_itr ):
162- cur_B = B .dot (cur_mes )
163-
164- g = get_gradient (cur_B , B_mes_sorted , B , nb_mes , nb_nodes )
165-
166- cur_mes -= step * g
167-
168- cur_mes = proj_simplex (cur_mes )
169-
170- return cur_mes
171-
172-
17348def pre_process_trees (tree_list , length_list , measures ):
17449 nx = get_backend (length_list , measures )
17550
51+ if tree_list .ndim == 1 :
52+ tree_list = nx .reshape (tree_list , (1 , * tree_list .shape ))
53+ length_list = nx .reshape (length_list , (1 , * length_list .shape ))
54+ measures = nx .reshape (measures , (1 , * measures .shape ))
55+
17656 nb_leafs = measures .shape [2 ]
17757
17858 prepared_trees = []
@@ -197,22 +77,69 @@ def pre_process_trees(tree_list, length_list, measures):
19777 return prepared_trees
19878
19979
200- def sliced_fixed_support_tree_barycenter (
201- tree_list , length_list , measures , nb_itr = 100 , step = 0.01 , tol = 1e-5
80+ def fixed_support_tree_barycenter (
81+ tree_list , length_list , measures , nb_itr = 100 , step = 0.01 , tol = 1e-5 , init_measure = None
20282):
20383 """
84+ Computes the Tree-Wasserstein (or Tree-Sliced) barycenter for one or multiple trees,
85+ with the constraint that the support of the barycenter is fixed at the leaves.
86+ It is assumed that the leaves correspond to the first nodes of the tree (indices 0 to k-1).
87+ While the number of leaves (k) must be strictly identical across all structures to ensure
88+ consistent alignment, the total number of nodes (leaves + internal nodes) can
89+ freely vary from one tree to another.
90+
91+ If a single tree structure is provided (e.g., 1D arrays for tree_list/length_list
92+ and 2D for measures), the function automatically expands their dimensions to 3D
93+ internal structures to handle them uniformly as a multi-tree setting with t=1.
94+
20495 Parameters
20596 -----------
206- tree_list : array_like, shape (t, n)
207- length_list : array_like, shape (t, n)
208- measures : array_like, shape (t, m, k)
97+ tree_list : array_like
98+ A single tree of shape (n_t,) or a list of t trees where each tree has shape (n_t,).
99+ n_t is the total number of nodes in that specific tree. tree[i] contains the index
100+ of the parent of node i (with tree[root] == root).
101+ length_list : array_like
102+ The edge weights corresponding to tree_list. A single array of shape (n_t,) or a list
103+ of t arrays, where the t-th array has shape (n_t,) and contains the length of the edge
104+ connecting node i to its parent.
105+ measures : array_like, shape (t, m, k) or (m,k)
106+ The input probability distributions mapped to the leaves.
107+ k is the fixed number of leaves shared by all trees, and m is the number
108+ of measures. Accepts a 2D array of shape (m, k) for a single tree, or a 3D array
109+ of shape (t, m, k) in a multi-tree setting.
110+ nb_tr : int, optional
111+ the maximal number of iterations for the subgradient descent
112+ step : float, optional
113+ the step size of the descent
114+ tol : float, optional
115+ Convergence tolerance. The descent stops if the L2 norm of the difference
116+ between two consecutive iterations is smaller than tol.
117+ init_measure : array_like, shape (k), optional
118+ The starting point of the descent, default is None
119+
120+ Returns
121+ -------
122+ cur_mes : array_like, shape (k)
123+ The computed fixed-support barycenter supported on the k leaves.
124+
125+ References
126+ ----------
127+ "Fixed Support Tree-Sliced Wasserstein Barycenter"
209128 """
210129
211130 nx = get_backend (length_list , measures )
212131
132+ assert (
133+ tree_list .shape [0 ] == length_list .shape [0 ] == measures .shape [0 ]
134+ and tree_list .shape [1 ] == length_list .shape [1 ]
135+ ), "dimension error in the input"
136+
213137 nb_leafs = measures .shape [2 ]
214138
215- cur_mes = nx .ones (nb_leafs ) / nb_leafs
139+ if init_measure is None :
140+ cur_mes = nx .ones (nb_leafs ) / nb_leafs
141+ else :
142+ cur_mes = init_measure
216143
217144 prepared_trees = pre_process_trees (tree_list , length_list , measures )
218145
0 commit comments