/
sdp_solvers.py
465 lines (371 loc) · 18.8 KB
/
sdp_solvers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
import numpy as np
import scipy as sp
import picos as pic
import itertools
import time
import clustering_utils as cu
import itertools
import sklearn.metrics as skl
# BRUTE FORCE METHODS (TODO) ===========================================================================================
def maxcut_brute_force_solver(C):
N = C.shape[0]
lst = list(itertools.product([0, 1], repeat=N))
all_partitions = list(itertools.product([-1, 1], repeat=N))
max_ene = 0
best = all_partitions[0]
for lb in all_partitions:
ene = cu.energy_clustering_pairwise(C, lb)
if ene > max_ene:
best = lb
max_ene = ene
return best
# INTERIOR POINT METHODS & UTILS =======================================================================================
def maxcut_ipm_solver(C):
"""
Solve the Max-Cut SDP problem with Interior Point Method
:param C: (2d array[float], NxN) - Weight Matrix representd the graph to be partitioned
:return: (2d array[float], NxN) - SDP solution,
(1d array[integer]) - Final objective value,
(float) - Final elapsed time
"""
N = C.shape[0]
# SDP Creation =====================================================================================================
max_cut = pic.Problem()
C = pic.new_param('C', C)
X = max_cut.add_variable('X', (N, N), 'symmetric')
max_cut.add_constraint(pic.tools.diag_vect(X) == 1)
max_cut.add_constraint(X >> 0)
max_cut.set_objective('max', C | (1 - X))
# Solve SDP ========================================================================================================
start_time = time.time()
max_cut.solve(verbose=0) # Solve SDP
elapsed_time = time.time() - start_time # Calculate execution time
return np.array(X.value), 0.5 * max_cut.obj_value(), elapsed_time
def maxkhypercut_ipm_solver(E, w, K, N, l):
"""
Solve Max-K-Hypercut SDP problem with Interior Point Method
:param E: (List of list of Integers) - Hypergraph vertices
:param w: (1d array[float], |E|) - Hypergraph weights
:param K: (integer) - Number of clusters
:param N: (integer) - Number of data points represented by the hypergraph in E
:param l: (integer) - Subspace dimension
:return: (1d array[integer]) - Partition,
(1d array[integer]) - Final objective value,
(float) - Final elapsed time
"""
# # Old code =======================================================================================================
# C = np.array(list(itertools.combinations(range(N), l))) # All possible combinations of size set_size for N p
# w = np.array([cu.compute_volume(P, s, squared_dist) for s in C]) # Vector of weights given by each subset
#
# if use_clique_expansion:
# E, w = clique_expansion(E, w, N)
# l = 2
#
# if delta != 0:
# E, w = graph_sampling(E, w, delta)
# SDP Creation =====================================================================================================
cluster_problem = pic.Problem()
X = cluster_problem.add_variable('X', (N, N), 'symmetric') # Matrix of inner products as a parameter
z = cluster_problem.add_variable('z', len(w)) # Factor indicators as a parameter
w = pic.new_param('w', w) # Weights as a parameter
C1 = pic.new_param('1/(|S_j| - 1) * (K-1)/K',
1 / (float(l) - 1) * (float(K) - 1) / float(K)) # Constant for the z's constraints
C2 = pic.new_param('-1/(K - 1)', -1 / (float(K) - 1)) # Constant for the X_ij constraints
# Constraint on z
cluster_problem.add_list_of_constraints(
[C1 * pic.tools.sum([1 - X[s[0], s[1]] for s in list(itertools.combinations(E[j], 2))]) > z[j]
for j in range(len(E))],
['i', 'k'],
'|S_j|, i < k, for all j'
)
# Constraint on X_ii
cluster_problem.add_constraint(pic.tools.diag_vect(X) == 1)
# Constraints on X_ij, i != j
cluster_problem.add_list_of_constraints(
[X[i, j] > C2 for i, j in itertools.product(range(N), range(N)) if i > j],
['i', 'j'],
'non-diagonal entries of X'
)
# Constraints on the semipositiveness of X and the positiveness of z
cluster_problem.add_constraint(X >> 0)
cluster_problem.add_constraint(z > 0)
cluster_problem.add_constraint(z <= 1)
cluster_problem.set_objective('max', w | z) # Set objective
# Solve SDP ========================================================================================================
start_time = time.time()
cluster_problem.solve(verbose=0) # Solve SDP
elapsed_time = time.time() - start_time # Calculate execution time
return np.array(X.value), cluster_problem.obj_value(), elapsed_time
def clique_expansion(E, w, N):
mu = 0.5
pairs = np.array(list(itertools.combinations(range(N), 2)))
new_w = [np.sum([w[index]/mu for c, index in zip(E, range(len(E))) if set(p).issubset(c)]) for p in pairs]
return pairs, np.array(new_w)
def graph_sampling(E, w, delta):
"""
Sample the hypergraph represented the hyperedges in E with weights w
:param E: (List of list of Integers) - Hypergraph vertices
:param w: (1d array[float], |E|) - Hypergraph weights
:param delta: sampling parameter
:return:
"""
r = int((delta ** (-2)) * 30)
chosen_idx = np.random.choice(len(w), r, p=w / np.sum(w), replace=True)
hist = np.histogram(chosen_idx, bins=range(len(E)))[0]
C = E[np.squeeze(np.argwhere(hist != 0))]
w = hist[np.squeeze(np.argwhere(hist != 0))]
return C, w
# ADMM METHODS & UTILS =================================================================================================
def maxkcut_admm_solver(C, K, num_max_it=5000, epsilon=1e-8, alpha=0):
"""
Solve Max-K-Hypercut SDP problem with Alternate Direction Multipliers Method
:param C: (2d array[float], NxN) - Weight Matrix represents the graph to be partitioned
:param K: (integer) - Number of partitions
:param num_max_it: (integer) - Maximum Number of ADMM iterations
:param epsilon: (float) - Desired final error
:param alpha: (float) - Dimensionality of the low-rank eigenvalue problem (alpha=0 for full rank)
:return: (2d array[float], NxN) - SDP solution,
(float) - Final error,
(float) - Final elapsed time
(integer) - Total number of iterations
"""
def finish_iteration(X_f, X_i, y, nu, C, b, d, it, params):
"""
Function used to monitor the error in each ADMM iteration
"""
# Primal Infeasibility -----------------------------------------------------------------------------------------
pinf = (np.linalg.norm(np.diag(X_f) - b) + np.linalg.norm(np.minimum(X_f - d, 0))) / (1 + np.linalg.norm(b))
# Dual Infeasibility -------------------------------------------------------------------------------------------
dinf = np.linalg.norm(params['mu'] * (X_f - X_i)) / (1 + np.linalg.norm(C, ord=1))
# Gap ----------------------------------------------------------------------------------------------------------
if np.remainder(it, params['check_finish_rate']) == 0:
CX = np.trace(C.dot(X_f))
y_nu = np.vdot(b, y) + np.vdot(d, nu)
gap = np.abs(CX - y_nu) / (1 + CX + y_nu)
params['prev_gap'] = gap
else:
gap = params['prev_gap']
# print("> pinf: %.4e" % pinf)
# print("> dinf: %.4e" % dinf)
# print("> gap: %.4e" % gap)
# print("> mu: %.4e" % params['mu'])
# Total error assessment ---------------------------------------------------------------------------------------
error = np.max(np.abs([pinf, dinf, gap]))
stop = error < params['epsilon']
# Update mu ---------------------------------------------------------------------------------------------------
if pinf / dinf <= 1:
params['it_pinf'] += 1
params['it_dinf'] = 0
if params['it_pinf'] >= params['h']:
params['mu'] = max(params['gamma'] * params['mu'], params['mu_min'])
params['it_pinf'] = 0
else:
params['it_dinf'] += 1
params['it_pinf'] = 0
if params['it_dinf'] >= params['h']:
params['mu'] = min((1. / params['gamma']) * params['mu'], params['mu_max'])
params['it_dinf'] = 0
return stop, params, error
params = {'mu': 5,
'pho': 1.6,
'mu_max': 1e4,
'mu_min': 1e-4,
'gamma': .5,
'epsilon': epsilon, # Desired error
'it_pinf': 0,
'it_dinf': 0,
'h': 50,
'num_max_it': num_max_it,
'check_finish_rate': 5,
'prev_gap': 0,
'r': alpha * K} # Dimension of the low rank apporximation of X
# SDP Variables ----------------------------------------------------------------------------------------------------
# Objective function's term
N = C.shape[0]
X = np.zeros_like(C)
S = np.zeros_like(C)
b = np.ones(N) # RHS of equality constraints
d = (-1.0 / (K - 1)) * np.ones((N, N)) # RHS of inequality constraints
np.fill_diagonal(d, 0)
# ADMM iterations (According to [1]) ===============================================================================
stop = False
elap_time_step = np.zeros(params['num_max_it'])
err = np.zeros(params['num_max_it'])
t_start = time.time()
it = 0
error = np.inf
while not stop and it < params['num_max_it']:
t_start_it = time.time()
# Update Y -----------------------------------------------------------------------------------------------------
y = -(params['mu'] * (np.diag(X) - b) + np.diag(S)).T
# Update nu ----------------------------------------------------------------------------------------------------
nu = np.maximum(-(params['mu'] * (X - d) + (S - C)), 0)
np.fill_diagonal(nu, 0)
# Update Y (Following the ideas on [2]) ------------------------------------------------------------------------
W = C - np.diag(y) - nu
if alpha == 0:
sigma, U = np.linalg.eigh(X - W / params['mu'])
else:
sigma, U = sp.sparse.linalg.eigsh(X - W / params['mu'], params['r'], which='LM')
X_f = np.linalg.multi_dot([U, np.diag(np.maximum(sigma, 0)), U.T])
S = W + params['mu'] * (X_f - X)
# Assess iteration ---------------------------------------------------------------------------------------------
stop, params, error = finish_iteration(X_f, X, y, nu, C, b, d, it, params)
elap_time_step[it] = time.time() - t_start_it
err[it] = error
# print("> Error: %.6e " % error)
# print("> Iteration elapsed time: %.3f\n" % elap_time_step[it])
# print("> ADMM Iteration: %d" % (it + 1))
X = params['pho'] * X_f + (1 - params['pho']) * X
it += 1
elapsed_time = time.time() - t_start
elapsed_time_step_mean = np.mean(elap_time_step[0:it])
return X, error, elapsed_time, it
# ROUNDING UTILS =======================================================================================================
def solve_round_sdp(C, use_IPM=False):
"""
Solve the Max-Cut SDP problem represented by C
:param C: (2d array[float], NxN) - Max-Cut weight matrix
:param use_IPM: (boolean) - Use Interior Point Method to solve the SDP problem
:return: (1d array[integer]) - Approximate optimal partition of the graph represented by C
"""
# Solve SDP using the Interior Point Method (Picos) or ADMM --------------------------------------------------------
if use_IPM:
X, _, _ = maxcut_ipm_solver(C)
else:
X, _, _, _ = maxkcut_admm_solver(C, 2)
# dv.plot_matrix(X)
# Embedding --------------------------------------------------------------------------------------------------------
V = np.linalg.cholesky(X + 1e-5 * np.trace(X) * np.eye(X.shape[0]))
# Select the best cutting plane (the one that maximizes the objective) ---------------------------------------------
params = {"is_a_hypergraph_problem": False, "C": C}
lb = max_cut_rounding(V, params)
return lb
def solve_round_hypergraph_sdp(E, w, K, N, l):
"""
Solve the Max-Hypergraph Cut SDP problem represented by C
:param E: (List of list of Integers) - Hypergraph vertices
:param w: (1d array[float], |E|) - Hypergraph weights
:param K: (integer) - Number of clusters
:param N: (integer) - Number of data points represented by the hypergraph in E
:param l: (integer) - Subspace dimension
:return: (1d array[integer]) - Approximate optimal partition of the graph represented by C
"""
# Solve SDP using the Interior Point Method (Picos) or ADMM --------------------------------------------------------
X, _, _ = maxkhypercut_ipm_solver(E, w, K, N, l)
# Embedding --------------------------------------------------------------------------------------------------------
V = np.linalg.cholesky(X + 1e-5 * np.trace(X) * np.eye(X.shape[0]))
# Select the best cutting plane (the one that maximizes the objective) ---------------------------------------------
params = {"is_a_hypergraph_problem": True, "E": E, "w": w, "K": K}
lb = max_k_cut_rounding(V, params)
return lb
def max_k_cut_rounding(V, params):
"""
Hyper plane rounding algorithm used in Max-Cut SDP problems (choosing the best out of some trials)
:param V: (2d array[float], NxN) - Embedded vectors (rows) in a R^{N-1} unit sphere'
:param params: (Dictionary) - {'is_a_hypergraph_problem' (boolean): check if the problem that originated V is
a hypergraph problem,
'E' (List of list of Integers): Hypergraph vertices,
'w' (1d array[float], |E|): Hypergraph weights,
'K' (integer): Number of clusters,
'C' (2d array[float], NxN): Weight Matrix represents the graph to be partitioned}
:return: (1d array[integer]) - Rounded partition
"""
assert 'is_a_hypergraph_problem' in params, "Missing the variable 'is_a_hypergraph_problem'!"
assert 'post_processing' in params, "Missing the variable 'post_processing'!"
if params["is_a_hypergraph_problem"]:
assert 'E' in params, "Missing the variable 'E'!"
assert 'w' in params, "Missing the variable 'w'!"
assert 'K' in params, "Missing the variable 'K'!"
else:
assert 'C' in params, "Missing the variable 'C'!"
if params["calculate_purities"]:
assert 'gt' in params, "Missing the variable 'gt'!"
N = V.shape[0]
max_ene = 0
num_rounding_trials = max(1000, np.floor_divide(V.shape[0], 2))
lb = np.zeros(N, dtype=int)
if params["calculate_purities"]:
pur = np.zeros(num_rounding_trials)
for i in range(num_rounding_trials):
new_lb = nearest_neighbours(V, params["K"], post_processing=params['post_processing'])
if params["calculate_purities"]:
pur[i] += cu.purity(new_lb, params["gt"])
if params["is_a_hypergraph_problem"]:
ene = cu.energy_clustering_high_order(params["E"], params["w"], new_lb)
else:
ene = cu.energy_clustering_pairwise(params["C"], new_lb)
if ene > max_ene:
lb = new_lb
max_ene = ene
if params["calculate_purities"]:
return lb, pur
else:
return lb
def max_cut_rounding(V, params):
"""
Hyper plane rounding algorithm used in Max-Cut SDP problems (choosing the best out of some trials)
:param V: (2d array[float], NxN) - Embedded vectors (rows) in a R^{N-1} unit sphere
:param params: (Dictionary) - {'is_a_hypergraph_problem' (boolean): check if the problem that originated V is
a hypergraph problem,
'E' (List of list of Integers): Hypergraph vertices,
'w' (1d array[float], |E|): Hypergraph weights,
'K' (integer): Number of clusters,
'C' (2d array[float], NxN): Weight Matrix represents the graph to be partitioned}
:return: (1d array[integer]) - Rounded partition
"""
assert 'is_a_hypergraph_problem' in params, "Missing the variable 'is_a_hypergraph_problem'!"
if params["is_a_hypergraph_problem"]:
assert 'E' in params, "Missing the variable 'E'!"
assert 'w' in params, "Missing the variable 'w'!"
assert 'K' in params, "Missing the variable 'K'!"
else:
assert 'C' in params, "Missing the variable 'C'!"
N = V.shape[0]
max_ene = 0
num_rounding_trials = max(1000, np.floor_divide(V.shape[0], 2))
lb = np.zeros(N, dtype=int)
for i in range(num_rounding_trials):
# Selects a hyperplane that cuts the unit ball
w = np.random.randn(N)
v = V.dot(w)
new_lb = (v > 0).astype(int) - (v < 0).astype(int)
if params["is_a_hypergraph_problem"]:
ene = cu.energy_clustering_high_order(params["E"], params["w"], new_lb)
else:
ene = cu.energy_clustering_pairwise(params["C"], new_lb)
if ene > max_ene:
lb = new_lb
max_ene = ene
return lb
# OTHER METHODS ========================================================================================================
def nearest_neighbours(V, K, post_processing=True, max_tol=100):
"""
Compute the K nearest neighbours rounding procedure to Max-K-Cut SDP problem
:param V: (2d array[float], NxN) - Embedding created by the SDP solver
:param K: (integer) - Number of partitions to be found
:param post_processing: (boolean) - Do post processing in order to find neigbours to all K vectors
representing the partitions
:param max_tol: (integer) - Maximum number of nearest neighbours retrials in the post-processing step
:return: (1d array[integer]) - Final partitioning
"""
N = V.shape[0]
P = np.random.randn(N, K)
P /= np.linalg.norm(P, axis=0)
lb = np.argmin(skl.pairwise.pairwise_distances(P.T, V), axis=0)
if post_processing:
num_attempts = 0
prev_len_ind = 0
while len(np.unique(lb)) != K and num_attempts < max_tol:
ind_non_used = list(set(range(K)) - set(lb))
new_P = np.random.randn(N, len(ind_non_used))
new_P /= np.linalg.norm(new_P, axis=0)
P[:, ind_non_used] = new_P
lb = np.argmin(skl.pairwise.pairwise_distances(P.T, V), axis=0)
if prev_len_ind <= len(ind_non_used):
num_attempts += 1
prev_len_ind = len(ind_non_used)
else:
num_attempts = 0
prev_len_ind = 0
return lb