eqc-models 0.9.8__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.
- eqc_models-0.9.8.data/platlib/compile_extensions.py +23 -0
- eqc_models-0.9.8.data/platlib/eqc_models/__init__.py +15 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/__init__.py +4 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/base.py +10 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/penaltymultiplier.py +169 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/__init__.py +6 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/allocation.py +367 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/portbase.py +128 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/portmomentum.py +137 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/qap.py +82 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/setpartition.py +170 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/__init__.py +72 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/base.py +150 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/constraints.py +276 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/operators.py +201 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.c +11363 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.pyx +72 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polynomial.py +274 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/quadratic.py +250 -0
- eqc_models-0.9.8.data/platlib/eqc_models/decoding.py +20 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/base.py +63 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/hypergraph.py +307 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/maxcut.py +155 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/maxkcut.py +184 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/__init__.py +15 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierbase.py +99 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqboost.py +423 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqsvm.py +237 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/clustering.py +323 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/clusteringbase.py +112 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/decomposition.py +363 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/forecast.py +255 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/forecastbase.py +139 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/regressor.py +220 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/regressorbase.py +97 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/reservoir.py +106 -0
- eqc_models-0.9.8.data/platlib/eqc_models/sequence/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/sequence/tsp.py +217 -0
- eqc_models-0.9.8.data/platlib/eqc_models/solvers/__init__.py +12 -0
- eqc_models-0.9.8.data/platlib/eqc_models/solvers/qciclient.py +707 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/__init__.py +6 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/fileio.py +38 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/polynomial.py +137 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/qplib.py +375 -0
- eqc_models-0.9.8.dist-info/LICENSE.txt +202 -0
- eqc_models-0.9.8.dist-info/METADATA +139 -0
- eqc_models-0.9.8.dist-info/RECORD +52 -0
- eqc_models-0.9.8.dist-info/WHEEL +5 -0
- eqc_models-0.9.8.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
# Import libs
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import warnings
|
|
9
|
+
from functools import wraps
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from eqc_models.ml.classifierbase import ClassifierBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QSVMClassifier(ClassifierBase):
|
|
16
|
+
"""An implementation of QSVM classifier that uses QCi's Dirac-3.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
|
|
21
|
+
relaxation_schedule: Relaxation schedule used by Dirac-3; default:
|
|
22
|
+
2.
|
|
23
|
+
|
|
24
|
+
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
25
|
+
|
|
26
|
+
upper_limit: Coefficient upper limit; a regularization parameter;
|
|
27
|
+
default: 1.0.
|
|
28
|
+
|
|
29
|
+
gamma: Gaussian kernel parameter; default: 1.0.
|
|
30
|
+
|
|
31
|
+
eta: A penalty multiplier; default: 1.0.
|
|
32
|
+
|
|
33
|
+
zeta: A penalty multiplier; default: 1.0.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
-----------
|
|
37
|
+
|
|
38
|
+
>>> from sklearn import datasets
|
|
39
|
+
>>> from sklearn.preprocessing import MinMaxScaler
|
|
40
|
+
>>> from sklearn.model_selection import train_test_split
|
|
41
|
+
>>> iris = datasets.load_iris()
|
|
42
|
+
>>> X = iris.data
|
|
43
|
+
>>> y = iris.target
|
|
44
|
+
>>> scaler = MinMaxScaler()
|
|
45
|
+
>>> X = scaler.fit_transform(X)
|
|
46
|
+
>>> for i in range(len(y)):
|
|
47
|
+
... if y[i] == 0:
|
|
48
|
+
... y[i] = -1
|
|
49
|
+
... elif y[i] == 2:
|
|
50
|
+
... y[i] = 1
|
|
51
|
+
>>> X_train, X_test, y_train, y_test = train_test_split(
|
|
52
|
+
... X,
|
|
53
|
+
... y,
|
|
54
|
+
... test_size=0.2,
|
|
55
|
+
... random_state=42,
|
|
56
|
+
... )
|
|
57
|
+
>>> from eqc_models.ml.classifierqsvm import QSVMClassifier
|
|
58
|
+
>>> obj = QSVMClassifier(
|
|
59
|
+
... relaxation_schedule=2,
|
|
60
|
+
... num_samples=1,
|
|
61
|
+
... upper_limit=1.0,
|
|
62
|
+
... gamma=1.0,
|
|
63
|
+
... eta=1.0,
|
|
64
|
+
... zeta=1.0,
|
|
65
|
+
... )
|
|
66
|
+
>>> from contextlib import redirect_stdout
|
|
67
|
+
>>> import io
|
|
68
|
+
>>> f = io.StringIO()
|
|
69
|
+
>>> with redirect_stdout(f):
|
|
70
|
+
... obj = obj.fit(X_train, y_train)
|
|
71
|
+
... y_train_prd = obj.predict(X_train)
|
|
72
|
+
... y_test_prd = obj.predict(X_test)
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
relaxation_schedule=2,
|
|
79
|
+
num_samples=1,
|
|
80
|
+
upper_limit=1.0,
|
|
81
|
+
gamma=1.0,
|
|
82
|
+
eta=1.0,
|
|
83
|
+
zeta=1.0,
|
|
84
|
+
):
|
|
85
|
+
super(QSVMClassifier).__init__()
|
|
86
|
+
|
|
87
|
+
self.relaxation_schedule = relaxation_schedule
|
|
88
|
+
self.num_samples = num_samples
|
|
89
|
+
self.upper_limit = upper_limit
|
|
90
|
+
self.gamma = gamma
|
|
91
|
+
self.eta = eta
|
|
92
|
+
self.zeta = zeta
|
|
93
|
+
|
|
94
|
+
def kernel(self, vec1, vec2):
|
|
95
|
+
return np.exp(-self.gamma * np.linalg.norm(vec1 - vec2) ** 2)
|
|
96
|
+
|
|
97
|
+
def fit(self, X, y):
|
|
98
|
+
"""
|
|
99
|
+
Build a QSVM classifier from the training set (X, y).
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
X : {array-like, sparse matrix} of shape (n_samples, n_features)
|
|
104
|
+
The training input samples.
|
|
105
|
+
|
|
106
|
+
y : array-like of shape (n_samples,)
|
|
107
|
+
The target values.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Response of Dirac-3 in JSON format.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
assert X.shape[0] == y.shape[0], "Inconsistent sizes!"
|
|
115
|
+
|
|
116
|
+
assert set(y) == {-1, 1}, "Target values should be in {-1, 1}"
|
|
117
|
+
|
|
118
|
+
J, C, sum_constraint = self.get_hamiltonian(X, y)
|
|
119
|
+
|
|
120
|
+
assert J.shape[0] == J.shape[1], "Inconsistent hamiltonian size!"
|
|
121
|
+
assert J.shape[0] == C.shape[0], "Inconsistent hamiltonian size!"
|
|
122
|
+
|
|
123
|
+
self.set_model(J, C, sum_constraint)
|
|
124
|
+
|
|
125
|
+
sol, response = self.solve()
|
|
126
|
+
|
|
127
|
+
assert len(sol) == C.shape[0], "Inconsistent solution size!"
|
|
128
|
+
|
|
129
|
+
self.params = self.convert_sol_to_params(sol)
|
|
130
|
+
|
|
131
|
+
self.X_train = X
|
|
132
|
+
self.y_train = y
|
|
133
|
+
|
|
134
|
+
n_records = X.shape[0]
|
|
135
|
+
self.kernel_mat_train = np.zeros(
|
|
136
|
+
shape=(n_records, n_records), dtype=np.float32
|
|
137
|
+
)
|
|
138
|
+
for m in range(n_records):
|
|
139
|
+
for n in range(n_records):
|
|
140
|
+
self.kernel_mat_train[m][n] = self.kernel(X[m], X[n])
|
|
141
|
+
|
|
142
|
+
return response
|
|
143
|
+
|
|
144
|
+
def predict(self, X: np.array):
|
|
145
|
+
"""
|
|
146
|
+
Predict classes for X.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
X : {array-like, sparse matrix} of shape (n_samples, n_features)
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
y : ndarray of shape (n_samples,)
|
|
155
|
+
The predicted classes.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
assert self.X_train is not None, "Model not trained yet!"
|
|
159
|
+
assert self.y_train is not None, "Model not trained yet!"
|
|
160
|
+
|
|
161
|
+
assert (
|
|
162
|
+
X.shape[1] == self.X_train.shape[1]
|
|
163
|
+
), "Inconsistent dimensions!"
|
|
164
|
+
|
|
165
|
+
n_records = X.shape[0]
|
|
166
|
+
n_records_train = self.X_train.shape[0]
|
|
167
|
+
kernel_mat = np.zeros(
|
|
168
|
+
shape=(n_records, n_records_train), dtype=np.float32
|
|
169
|
+
)
|
|
170
|
+
for m in range(n_records):
|
|
171
|
+
for n in range(n_records_train):
|
|
172
|
+
kernel_mat[m][n] = self.kernel(X[m], self.X_train[n])
|
|
173
|
+
|
|
174
|
+
intercept = 0
|
|
175
|
+
tmp_vec1 = np.tensordot(
|
|
176
|
+
self.params * self.y_train, self.kernel_mat_train, axes=(0, 0)
|
|
177
|
+
)
|
|
178
|
+
assert tmp_vec1.shape[0] == n_records_train, "Inconsistent size!"
|
|
179
|
+
|
|
180
|
+
tmp1 = np.sum(
|
|
181
|
+
self.params
|
|
182
|
+
* (self.upper_limit - self.params)
|
|
183
|
+
* (self.y_train - tmp_vec1)
|
|
184
|
+
)
|
|
185
|
+
tmp2 = np.sum(self.params * (self.upper_limit - self.params))
|
|
186
|
+
|
|
187
|
+
assert tmp2 != 0, "Something went wrong!"
|
|
188
|
+
|
|
189
|
+
intercept = tmp1 / tmp2
|
|
190
|
+
|
|
191
|
+
y = np.zeros(shape=(n_records), dtype=np.float32)
|
|
192
|
+
y += np.tensordot(
|
|
193
|
+
self.params * self.y_train, kernel_mat, axes=(0, 1)
|
|
194
|
+
)
|
|
195
|
+
y += intercept
|
|
196
|
+
y = np.sign(y)
|
|
197
|
+
|
|
198
|
+
return y
|
|
199
|
+
|
|
200
|
+
def get_hamiltonian(
|
|
201
|
+
self,
|
|
202
|
+
X: np.array,
|
|
203
|
+
y: np.array,
|
|
204
|
+
):
|
|
205
|
+
n_records = X.shape[0]
|
|
206
|
+
n_dims = X.shape[1]
|
|
207
|
+
|
|
208
|
+
J = np.zeros(
|
|
209
|
+
shape=(2 * n_records, 2 * n_records), dtype=np.float32
|
|
210
|
+
)
|
|
211
|
+
C = np.zeros(shape=(2 * n_records,), dtype=np.float32)
|
|
212
|
+
|
|
213
|
+
for n in range(n_records):
|
|
214
|
+
for m in range(n_records):
|
|
215
|
+
J[n][m] = (
|
|
216
|
+
0.5 * y[n] * y[m] * self.kernel(X[n], X[m])
|
|
217
|
+
+ self.zeta * y[n] * y[m]
|
|
218
|
+
)
|
|
219
|
+
J[n][n] += self.eta
|
|
220
|
+
J[n][n + n_records] = self.eta
|
|
221
|
+
J[n + n_records][n] = self.eta
|
|
222
|
+
J[n + n_records][n + n_records] = self.eta
|
|
223
|
+
|
|
224
|
+
C[n] = -1.0 - 2.0 * self.eta * self.upper_limit
|
|
225
|
+
C[n + n_records] = -2.0 * self.eta * self.upper_limit
|
|
226
|
+
|
|
227
|
+
C = C.reshape((2 * n_records, 1))
|
|
228
|
+
J = 0.5 * (J + J.transpose())
|
|
229
|
+
|
|
230
|
+
return J, C, n_records * self.upper_limit
|
|
231
|
+
|
|
232
|
+
def convert_sol_to_params(self, sol):
|
|
233
|
+
assert len(sol) % 2 == 0, "Expected an even solution size!"
|
|
234
|
+
|
|
235
|
+
sol = sol[: int(len(sol) / 2)]
|
|
236
|
+
|
|
237
|
+
return np.array(sol)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import numpy as np
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
from eqc_models.ml.clusteringbase import ClusteringBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphClustering(ClusteringBase):
|
|
9
|
+
"""
|
|
10
|
+
A clustering approach on a graph based on maximizing modularity.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
|
|
15
|
+
graph: A NetwrokX graph object representing the data.
|
|
16
|
+
num_clusters: Number of clusters.
|
|
17
|
+
|
|
18
|
+
alpha: A penalty term multilplier; default: 1.0.
|
|
19
|
+
|
|
20
|
+
relaxation_schedule: Relaxation schedule used by Dirac-3; default:
|
|
21
|
+
2.
|
|
22
|
+
|
|
23
|
+
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
24
|
+
|
|
25
|
+
device: The device used, dirac-1 or dirac-3; default: dirac-3.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
---------
|
|
29
|
+
|
|
30
|
+
>>> import networkx as nx
|
|
31
|
+
>>> G = nx.Graph()
|
|
32
|
+
>>> G.add_edge(1, 2, weight=5)
|
|
33
|
+
>>> G.add_edge(2, 3, weight=3)
|
|
34
|
+
>>> G.add_edge(1, 3, weight=4)
|
|
35
|
+
>>> G.add_edge(4, 5, weight=6)
|
|
36
|
+
>>> G.add_edge(5, 6, weight=2)
|
|
37
|
+
>>> G.add_edge(4, 6, weight=7)
|
|
38
|
+
>>> G.add_edge(7, 8, weight=8)
|
|
39
|
+
>>> G.add_edge(8, 9, weight=1)
|
|
40
|
+
>>> G.add_edge(7, 9, weight=5)
|
|
41
|
+
>>> G.add_edge(3, 4, weight=0.5)
|
|
42
|
+
>>> G.add_edge(6, 7, weight=0.5)
|
|
43
|
+
>>> from eqc_models.ml.clustering import GraphClustering
|
|
44
|
+
>>> from contextlib import redirect_stdout
|
|
45
|
+
>>> import io
|
|
46
|
+
>>> f = io.StringIO()
|
|
47
|
+
>>> with redirect_stdout(f):
|
|
48
|
+
... obj = GraphClustering(
|
|
49
|
+
... relaxation_schedule=2,
|
|
50
|
+
... num_samples=1,
|
|
51
|
+
... graph=G,
|
|
52
|
+
... num_clusters=3,
|
|
53
|
+
... alpha=10.0,
|
|
54
|
+
... device="dirac-3",
|
|
55
|
+
... )
|
|
56
|
+
... labels = obj.fit_predict()
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
graph: nx.Graph,
|
|
62
|
+
num_clusters: int,
|
|
63
|
+
alpha: float = 1.0,
|
|
64
|
+
relaxation_schedule=2,
|
|
65
|
+
num_samples=1,
|
|
66
|
+
device="dirac-3",
|
|
67
|
+
):
|
|
68
|
+
super(GraphClustering).__init__()
|
|
69
|
+
|
|
70
|
+
assert device in ["dirac-1", "dirac-3"]
|
|
71
|
+
|
|
72
|
+
self.graph = graph
|
|
73
|
+
self.num_nodes = graph.number_of_nodes()
|
|
74
|
+
self.num_edges = graph.number_of_edges()
|
|
75
|
+
self.num_clusters = num_clusters
|
|
76
|
+
self.alpha = alpha
|
|
77
|
+
self.relaxation_schedule = relaxation_schedule
|
|
78
|
+
self.num_samples = num_samples
|
|
79
|
+
self.device = device
|
|
80
|
+
self.labels = None
|
|
81
|
+
|
|
82
|
+
def get_hamiltonian(self):
|
|
83
|
+
adj_mat_sparse = nx.adjacency_matrix(self.graph)
|
|
84
|
+
A = adj_mat_sparse.toarray()
|
|
85
|
+
|
|
86
|
+
assert A.shape[0] == A.shape[1], "Inconsistent size!"
|
|
87
|
+
assert A.shape[0] == self.num_nodes, "Inconsistent size!"
|
|
88
|
+
|
|
89
|
+
num_clusters = self.num_clusters
|
|
90
|
+
num_nodes = self.num_nodes
|
|
91
|
+
|
|
92
|
+
J = np.zeros(
|
|
93
|
+
shape=(num_nodes * num_clusters, num_nodes * num_clusters),
|
|
94
|
+
dtype=np.float64,
|
|
95
|
+
)
|
|
96
|
+
C = np.zeros(
|
|
97
|
+
shape=(num_nodes * num_clusters),
|
|
98
|
+
dtype=np.float64,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for i in range(num_nodes):
|
|
102
|
+
for j in range(num_nodes):
|
|
103
|
+
for c in range(num_clusters):
|
|
104
|
+
for d in range(num_clusters):
|
|
105
|
+
if c == d:
|
|
106
|
+
J[i * num_clusters + c][
|
|
107
|
+
j * num_clusters + d
|
|
108
|
+
] += -(
|
|
109
|
+
A[i][j]
|
|
110
|
+
- np.sum(A[i]) * np.sum(A[j]) / np.sum(A)
|
|
111
|
+
)
|
|
112
|
+
if i == j:
|
|
113
|
+
J[i * num_clusters + c][
|
|
114
|
+
j * num_clusters + d
|
|
115
|
+
] += self.alpha
|
|
116
|
+
|
|
117
|
+
for i in range(num_nodes):
|
|
118
|
+
for c in range(num_clusters):
|
|
119
|
+
C[i * num_clusters + c] += -2.0 * self.alpha
|
|
120
|
+
|
|
121
|
+
return J, C, num_nodes
|
|
122
|
+
|
|
123
|
+
def get_labels(self, sol: np.array):
|
|
124
|
+
labels = np.empty(shape=(self.num_nodes), dtype=np.int32)
|
|
125
|
+
|
|
126
|
+
for i in range(self.num_nodes):
|
|
127
|
+
vec = sol[i * self.num_clusters : (i + 1) * self.num_clusters]
|
|
128
|
+
labels[i] = np.argmax(vec) + 1
|
|
129
|
+
|
|
130
|
+
return labels
|
|
131
|
+
|
|
132
|
+
def fit(self):
|
|
133
|
+
"""
|
|
134
|
+
Fit clustering.
|
|
135
|
+
"""
|
|
136
|
+
J, C, sum_constraint = self.get_hamiltonian()
|
|
137
|
+
|
|
138
|
+
assert J.shape[0] == J.shape[1], "Inconsistent hamiltonian size!"
|
|
139
|
+
assert J.shape[0] == C.shape[0], "Inconsistent hamiltonian size!"
|
|
140
|
+
|
|
141
|
+
self.set_model(J, C, sum_constraint)
|
|
142
|
+
|
|
143
|
+
sol = self.solve()
|
|
144
|
+
|
|
145
|
+
assert len(sol) == C.shape[0], "Inconsistent solution size!"
|
|
146
|
+
assert len(sol) == self.num_clusters * self.num_nodes
|
|
147
|
+
|
|
148
|
+
self.labels = self.get_labels(sol)
|
|
149
|
+
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def fit_predict(self):
|
|
153
|
+
"""
|
|
154
|
+
Fit clustering and return cluster labels for all nodes.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
self.fit()
|
|
158
|
+
|
|
159
|
+
return self.labels
|
|
160
|
+
|
|
161
|
+
def get_modularity(self):
|
|
162
|
+
if self.labels is None:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
clusters = [set() for i in range(self.num_clusters)]
|
|
166
|
+
nodes = list(self.graph.nodes())
|
|
167
|
+
for i in range(self.num_nodes):
|
|
168
|
+
clusters[self.labels[i] - 1].add(nodes[i])
|
|
169
|
+
|
|
170
|
+
return nx.community.modularity(self.graph, clusters)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Clustering(ClusteringBase):
|
|
174
|
+
"""A clustering approach based on QCi's Dirac machines.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
|
|
179
|
+
num_clusters: Number of clusters.
|
|
180
|
+
|
|
181
|
+
alpha: A penalty term multilplier; default: 1.0.
|
|
182
|
+
|
|
183
|
+
relaxation_schedule: Relaxation schedule used by Dirac-3; default:
|
|
184
|
+
2.
|
|
185
|
+
|
|
186
|
+
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
187
|
+
|
|
188
|
+
distance_func: Distance function used; default: squared_l2_norm.
|
|
189
|
+
|
|
190
|
+
device: The device used, dirac-1 or dirac-3; default: dirac-3.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
---------
|
|
194
|
+
|
|
195
|
+
>>> np.random.seed(42)
|
|
196
|
+
>>> cluster1 = np.random.randn(15, 2) * 0.5 + np.array([2, 2])
|
|
197
|
+
>>> cluster2 = np.random.randn(15, 2) * 0.5 + np.array([8, 3])
|
|
198
|
+
>>> cluster3 = np.random.randn(15, 2) * 0.5 + np.array([5, 8])
|
|
199
|
+
>>> X = np.vstack((cluster1, cluster2, cluster3))
|
|
200
|
+
>>> from eqc_models.ml.clustering import Clustering
|
|
201
|
+
>>> from contextlib import redirect_stdout
|
|
202
|
+
>>> import io
|
|
203
|
+
>>> f = io.StringIO()
|
|
204
|
+
>>> with redirect_stdout(f):
|
|
205
|
+
... obj = Clustering(
|
|
206
|
+
... num_clusters=3,
|
|
207
|
+
... relaxation_schedule=1,
|
|
208
|
+
... num_samples=1,
|
|
209
|
+
... alpha=500.0,
|
|
210
|
+
... distance_func="squared_l2_norm",
|
|
211
|
+
... device="dirac-3",
|
|
212
|
+
... )
|
|
213
|
+
... labels = obj.fit_predict(X)
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
num_clusters: int,
|
|
220
|
+
alpha: float = 1.0,
|
|
221
|
+
relaxation_schedule: int = 2,
|
|
222
|
+
num_samples: int = 1,
|
|
223
|
+
distance_func: str = "squared_l2_norm",
|
|
224
|
+
device: str = "dirac-3",
|
|
225
|
+
):
|
|
226
|
+
super(Clustering).__init__()
|
|
227
|
+
|
|
228
|
+
assert device in ["dirac-1", "dirac-3"]
|
|
229
|
+
|
|
230
|
+
self.num_clusters = num_clusters
|
|
231
|
+
self.alpha = alpha
|
|
232
|
+
self.relaxation_schedule = relaxation_schedule
|
|
233
|
+
self.num_samples = num_samples
|
|
234
|
+
self.distance_func = distance_func
|
|
235
|
+
self.device = device
|
|
236
|
+
self.labels = None
|
|
237
|
+
|
|
238
|
+
assert distance_func in ["squared_l2_norm"], (
|
|
239
|
+
"Unknown distance function <%s>!" % distance_func
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def get_hamiltonian(self, X: np.array):
|
|
243
|
+
num_items = X.shape[0]
|
|
244
|
+
num_clusters = self.num_clusters
|
|
245
|
+
|
|
246
|
+
if self.distance_func == "squared_l2_norm":
|
|
247
|
+
dist = lambda u, v: np.linalg.norm(u - v) ** 2
|
|
248
|
+
|
|
249
|
+
J = np.zeros(
|
|
250
|
+
shape=(num_items * num_clusters, num_items * num_clusters),
|
|
251
|
+
dtype=np.float64,
|
|
252
|
+
)
|
|
253
|
+
C = np.zeros(
|
|
254
|
+
shape=(num_items * num_clusters),
|
|
255
|
+
dtype=np.float64,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
for i in range(num_items):
|
|
259
|
+
for j in range(num_items):
|
|
260
|
+
for c in range(num_clusters):
|
|
261
|
+
for d in range(num_clusters):
|
|
262
|
+
if c == d:
|
|
263
|
+
J[i * num_clusters + c][
|
|
264
|
+
j * num_clusters + d
|
|
265
|
+
] += dist(X[i], X[j])
|
|
266
|
+
|
|
267
|
+
if i == j:
|
|
268
|
+
J[i * num_clusters + c][
|
|
269
|
+
j * num_clusters + d
|
|
270
|
+
] += self.alpha
|
|
271
|
+
|
|
272
|
+
for i in range(num_items):
|
|
273
|
+
for c in range(num_clusters):
|
|
274
|
+
C[i * num_clusters + c] += -2.0 * self.alpha
|
|
275
|
+
|
|
276
|
+
return J, C, num_items
|
|
277
|
+
|
|
278
|
+
def get_labels(self, sol: np.array, num_items: int):
|
|
279
|
+
labels = np.empty(shape=(num_items), dtype=np.int32)
|
|
280
|
+
|
|
281
|
+
for i in range(num_items):
|
|
282
|
+
vec = sol[i * self.num_clusters : (i + 1) * self.num_clusters]
|
|
283
|
+
labels[i] = np.argmax(vec) + 1
|
|
284
|
+
|
|
285
|
+
return labels
|
|
286
|
+
|
|
287
|
+
def fit(self, X: np.array):
|
|
288
|
+
"""
|
|
289
|
+
Fit clustering.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
X: Dataset; an array of shape (num_items, num_dims).
|
|
294
|
+
"""
|
|
295
|
+
num_items = X.shape[0]
|
|
296
|
+
J, C, sum_constraint = self.get_hamiltonian(X)
|
|
297
|
+
|
|
298
|
+
assert J.shape[0] == J.shape[1], "Inconsistent hamiltonian size!"
|
|
299
|
+
assert J.shape[0] == C.shape[0], "Inconsistent hamiltonian size!"
|
|
300
|
+
|
|
301
|
+
self.set_model(J, C, sum_constraint)
|
|
302
|
+
|
|
303
|
+
sol = self.solve()
|
|
304
|
+
|
|
305
|
+
assert len(sol) == C.shape[0], "Inconsistent solution size!"
|
|
306
|
+
assert len(sol) == self.num_clusters * num_items
|
|
307
|
+
|
|
308
|
+
self.labels = self.get_labels(sol, num_items)
|
|
309
|
+
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
def fit_predict(self, X: np.array):
|
|
313
|
+
"""
|
|
314
|
+
Fit clustering and return cluster labels for all records.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
X: Dataset; an array of shape (num_items, num_dims).
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
self.fit(X)
|
|
322
|
+
|
|
323
|
+
return self.labels
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
import sys
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from eqc_models import QuadraticModel
|
|
6
|
+
from eqc_models.solvers.qciclient import (
|
|
7
|
+
Dirac1CloudSolver,
|
|
8
|
+
Dirac3CloudSolver,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClusteringBase(QuadraticModel):
|
|
13
|
+
"""
|
|
14
|
+
A base class for clustering algorithms
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
relaxation_schedule=2,
|
|
20
|
+
num_samples=1,
|
|
21
|
+
device="dirac-3",
|
|
22
|
+
):
|
|
23
|
+
super(self).__init__(None, None, None)
|
|
24
|
+
|
|
25
|
+
assert device in ["dirac-1", "dirac-3"]
|
|
26
|
+
|
|
27
|
+
self.relaxation_schedule = relaxation_schedule
|
|
28
|
+
self.num_samples = num_samples
|
|
29
|
+
self.device = device
|
|
30
|
+
|
|
31
|
+
def fit(self, X: np.array):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def predict(self, X: np.array):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def get_hamiltonian(
|
|
38
|
+
self,
|
|
39
|
+
X: np.array,
|
|
40
|
+
):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def set_model(self, J, C, sum_constraint):
|
|
44
|
+
# Set hamiltonians
|
|
45
|
+
self._C = C
|
|
46
|
+
self._J = J
|
|
47
|
+
self._H = C, J
|
|
48
|
+
self._sum_constraint = sum_constraint
|
|
49
|
+
num_variables = C.shape[0]
|
|
50
|
+
|
|
51
|
+
if self.device == "dirac-1":
|
|
52
|
+
self.upper_bound = np.ones((num_variables,))
|
|
53
|
+
elif self.device == "dirac-3":
|
|
54
|
+
self.upper_bound = sum_constraint * np.ones((num_variables,))
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
def solve(self):
|
|
59
|
+
if self.device == "dirac-1":
|
|
60
|
+
solver = Dirac1CloudSolver()
|
|
61
|
+
response = solver.solve(
|
|
62
|
+
self,
|
|
63
|
+
num_samples=self.num_samples,
|
|
64
|
+
)
|
|
65
|
+
elif self.device == "dirac-3":
|
|
66
|
+
solver = Dirac3CloudSolver()
|
|
67
|
+
response = solver.solve(
|
|
68
|
+
self,
|
|
69
|
+
sum_constraint=self._sum_constraint,
|
|
70
|
+
relaxation_schedule=self.relaxation_schedule,
|
|
71
|
+
solution_precision=1,
|
|
72
|
+
num_samples=self.num_samples,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
min_id = np.argmin(response["results"]["energies"])
|
|
76
|
+
|
|
77
|
+
sol = response["results"]["solutions"][min_id]
|
|
78
|
+
|
|
79
|
+
print(response)
|
|
80
|
+
|
|
81
|
+
return sol
|
|
82
|
+
|
|
83
|
+
def get_labels(self, sol):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def get_energy(self, sol: np.array):
|
|
87
|
+
C = self._C
|
|
88
|
+
J = self._J
|
|
89
|
+
|
|
90
|
+
return sol.transpose() @ J @ sol + sol.transpose @ C
|
|
91
|
+
|
|
92
|
+
def get_dynamic_range(self):
|
|
93
|
+
C = self._C
|
|
94
|
+
J = self._J
|
|
95
|
+
|
|
96
|
+
if C is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if J is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
absc = np.abs(C)
|
|
103
|
+
absj = np.abs(J)
|
|
104
|
+
minc = np.min(absc[absc > 0])
|
|
105
|
+
maxc = np.max(absc)
|
|
106
|
+
minj = np.min(absj[absj > 0])
|
|
107
|
+
maxj = np.max(absj)
|
|
108
|
+
|
|
109
|
+
minval = min(minc, minj)
|
|
110
|
+
maxval = max(maxc, maxj)
|
|
111
|
+
|
|
112
|
+
return 10 * np.log10(maxval / minval)
|