superquantx 0.1.0__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.
- superquantx/__init__.py +321 -0
- superquantx/algorithms/__init__.py +55 -0
- superquantx/algorithms/base_algorithm.py +413 -0
- superquantx/algorithms/hybrid_classifier.py +628 -0
- superquantx/algorithms/qaoa.py +406 -0
- superquantx/algorithms/quantum_agents.py +1006 -0
- superquantx/algorithms/quantum_kmeans.py +575 -0
- superquantx/algorithms/quantum_nn.py +544 -0
- superquantx/algorithms/quantum_pca.py +499 -0
- superquantx/algorithms/quantum_svm.py +346 -0
- superquantx/algorithms/vqe.py +553 -0
- superquantx/algorithms.py +863 -0
- superquantx/backends/__init__.py +265 -0
- superquantx/backends/base_backend.py +321 -0
- superquantx/backends/braket_backend.py +420 -0
- superquantx/backends/cirq_backend.py +466 -0
- superquantx/backends/ocean_backend.py +491 -0
- superquantx/backends/pennylane_backend.py +419 -0
- superquantx/backends/qiskit_backend.py +451 -0
- superquantx/backends/simulator_backend.py +455 -0
- superquantx/backends/tket_backend.py +519 -0
- superquantx/circuits.py +447 -0
- superquantx/cli/__init__.py +28 -0
- superquantx/cli/commands.py +528 -0
- superquantx/cli/main.py +254 -0
- superquantx/client.py +298 -0
- superquantx/config.py +326 -0
- superquantx/exceptions.py +287 -0
- superquantx/gates.py +588 -0
- superquantx/logging_config.py +347 -0
- superquantx/measurements.py +702 -0
- superquantx/ml.py +936 -0
- superquantx/noise.py +760 -0
- superquantx/utils/__init__.py +83 -0
- superquantx/utils/benchmarking.py +523 -0
- superquantx/utils/classical_utils.py +575 -0
- superquantx/utils/feature_mapping.py +467 -0
- superquantx/utils/optimization.py +410 -0
- superquantx/utils/quantum_utils.py +456 -0
- superquantx/utils/visualization.py +654 -0
- superquantx/version.py +33 -0
- superquantx-0.1.0.dist-info/METADATA +365 -0
- superquantx-0.1.0.dist-info/RECORD +46 -0
- superquantx-0.1.0.dist-info/WHEEL +4 -0
- superquantx-0.1.0.dist-info/entry_points.txt +2 -0
- superquantx-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,575 @@
|
|
1
|
+
"""Quantum K-Means clustering implementation.
|
2
|
+
|
3
|
+
This module provides quantum algorithms for K-means clustering, including
|
4
|
+
quantum distance calculations and quantum amplitude estimation approaches.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Any, Dict, Optional, Union
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
from sklearn.cluster import KMeans
|
12
|
+
from sklearn.metrics import adjusted_rand_score, silhouette_score
|
13
|
+
from sklearn.preprocessing import StandardScaler
|
14
|
+
|
15
|
+
from .base_algorithm import UnsupervisedQuantumAlgorithm
|
16
|
+
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
class QuantumKMeans(UnsupervisedQuantumAlgorithm):
|
21
|
+
"""Quantum K-Means clustering algorithm.
|
22
|
+
|
23
|
+
This implementation uses quantum algorithms to perform K-means clustering,
|
24
|
+
potentially offering speedup for high-dimensional data through quantum
|
25
|
+
distance calculations and amplitude estimation.
|
26
|
+
|
27
|
+
The algorithm can use different quantum approaches:
|
28
|
+
- Quantum Distance Calculation: Use quantum circuits to compute distances
|
29
|
+
- Quantum Amplitude Estimation: For probabilistic distance measurements
|
30
|
+
- Variational Quantum Clustering: Use VQC for cluster optimization
|
31
|
+
- Quantum Annealing: For global cluster optimization
|
32
|
+
|
33
|
+
Args:
|
34
|
+
backend: Quantum backend for circuit execution
|
35
|
+
n_clusters: Number of clusters (k)
|
36
|
+
method: Quantum method ('distance', 'amplitude', 'variational', 'annealing')
|
37
|
+
distance_metric: Distance metric ('euclidean', 'manhattan', 'quantum')
|
38
|
+
encoding: Data encoding method ('amplitude', 'angle', 'dense')
|
39
|
+
max_iterations: Maximum iterations for clustering
|
40
|
+
tolerance: Convergence tolerance
|
41
|
+
init_method: Centroid initialization ('random', 'k-means++', 'quantum')
|
42
|
+
shots: Number of measurement shots
|
43
|
+
classical_fallback: Use classical K-means if quantum fails
|
44
|
+
**kwargs: Additional parameters
|
45
|
+
|
46
|
+
Example:
|
47
|
+
>>> qkmeans = QuantumKMeans(backend='pennylane', n_clusters=3, method='distance')
|
48
|
+
>>> qkmeans.fit(X_train)
|
49
|
+
>>> labels = qkmeans.predict(X_test)
|
50
|
+
>>> centroids = qkmeans.cluster_centers_
|
51
|
+
|
52
|
+
"""
|
53
|
+
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
backend: Union[str, Any],
|
57
|
+
n_clusters: int = 3,
|
58
|
+
method: str = 'distance',
|
59
|
+
distance_metric: str = 'euclidean',
|
60
|
+
encoding: str = 'amplitude',
|
61
|
+
max_iterations: int = 300,
|
62
|
+
tolerance: float = 1e-4,
|
63
|
+
init_method: str = 'k-means++',
|
64
|
+
shots: int = 1024,
|
65
|
+
classical_fallback: bool = True,
|
66
|
+
normalize_data: bool = True,
|
67
|
+
random_state: Optional[int] = None,
|
68
|
+
**kwargs
|
69
|
+
) -> None:
|
70
|
+
super().__init__(backend=backend, shots=shots, **kwargs)
|
71
|
+
|
72
|
+
self.n_clusters = n_clusters
|
73
|
+
self.method = method
|
74
|
+
self.distance_metric = distance_metric
|
75
|
+
self.encoding = encoding
|
76
|
+
self.max_iterations = max_iterations
|
77
|
+
self.tolerance = tolerance
|
78
|
+
self.init_method = init_method
|
79
|
+
self.classical_fallback = classical_fallback
|
80
|
+
self.normalize_data = normalize_data
|
81
|
+
self.random_state = random_state
|
82
|
+
|
83
|
+
# Clustering results
|
84
|
+
self.cluster_centers_ = None
|
85
|
+
self.labels_ = None
|
86
|
+
self.inertia_ = None
|
87
|
+
self.n_iter_ = None
|
88
|
+
|
89
|
+
# Quantum-specific attributes
|
90
|
+
self.n_qubits = None
|
91
|
+
self.quantum_distances_ = None
|
92
|
+
self.convergence_history = []
|
93
|
+
|
94
|
+
# Classical components
|
95
|
+
self.scaler = StandardScaler() if normalize_data else None
|
96
|
+
self.classical_kmeans = KMeans(
|
97
|
+
n_clusters=n_clusters,
|
98
|
+
max_iter=max_iterations,
|
99
|
+
tol=tolerance,
|
100
|
+
random_state=random_state
|
101
|
+
)
|
102
|
+
|
103
|
+
# Set random seed
|
104
|
+
if random_state is not None:
|
105
|
+
np.random.seed(random_state)
|
106
|
+
|
107
|
+
logger.info(f"Initialized QuantumKMeans with method={method}, n_clusters={n_clusters}")
|
108
|
+
|
109
|
+
def _determine_qubits(self, n_features: int) -> int:
|
110
|
+
"""Determine number of qubits needed for encoding."""
|
111
|
+
if self.encoding == 'amplitude':
|
112
|
+
return max(1, int(np.ceil(np.log2(n_features))))
|
113
|
+
elif self.encoding == 'angle':
|
114
|
+
return n_features
|
115
|
+
elif self.encoding == 'dense':
|
116
|
+
return n_features
|
117
|
+
else:
|
118
|
+
return max(1, int(np.ceil(np.log2(n_features))))
|
119
|
+
|
120
|
+
def _initialize_centroids(self, X: np.ndarray) -> np.ndarray:
|
121
|
+
"""Initialize cluster centroids."""
|
122
|
+
n_samples, n_features = X.shape
|
123
|
+
|
124
|
+
if self.init_method == 'random':
|
125
|
+
# Random initialization
|
126
|
+
centroids = np.random.uniform(
|
127
|
+
X.min(axis=0), X.max(axis=0),
|
128
|
+
size=(self.n_clusters, n_features)
|
129
|
+
)
|
130
|
+
elif self.init_method == 'k-means++':
|
131
|
+
# K-means++ initialization
|
132
|
+
centroids = self._kmeans_plus_plus_init(X)
|
133
|
+
elif self.init_method == 'quantum':
|
134
|
+
# Quantum-inspired initialization
|
135
|
+
centroids = self._quantum_init(X)
|
136
|
+
else:
|
137
|
+
raise ValueError(f"Unknown initialization method: {self.init_method}")
|
138
|
+
|
139
|
+
return centroids
|
140
|
+
|
141
|
+
def _kmeans_plus_plus_init(self, X: np.ndarray) -> np.ndarray:
|
142
|
+
"""K-means++ initialization algorithm."""
|
143
|
+
n_samples, n_features = X.shape
|
144
|
+
centroids = np.zeros((self.n_clusters, n_features))
|
145
|
+
|
146
|
+
# Choose first centroid randomly
|
147
|
+
centroids[0] = X[np.random.randint(n_samples)]
|
148
|
+
|
149
|
+
for i in range(1, self.n_clusters):
|
150
|
+
# Compute distances to nearest centroid
|
151
|
+
distances = np.array([
|
152
|
+
min([np.linalg.norm(x - c) ** 2 for c in centroids[:i]])
|
153
|
+
for x in X
|
154
|
+
])
|
155
|
+
|
156
|
+
# Choose next centroid with probability proportional to distance^2
|
157
|
+
probabilities = distances / distances.sum()
|
158
|
+
cumulative_probs = probabilities.cumsum()
|
159
|
+
r = np.random.random()
|
160
|
+
|
161
|
+
for j, p in enumerate(cumulative_probs):
|
162
|
+
if r < p:
|
163
|
+
centroids[i] = X[j]
|
164
|
+
break
|
165
|
+
|
166
|
+
return centroids
|
167
|
+
|
168
|
+
def _quantum_init(self, X: np.ndarray) -> np.ndarray:
|
169
|
+
"""Quantum-inspired centroid initialization."""
|
170
|
+
try:
|
171
|
+
if hasattr(self.backend, 'quantum_centroid_init'):
|
172
|
+
return self.backend.quantum_centroid_init(
|
173
|
+
X, self.n_clusters, encoding=self.encoding, shots=self.shots
|
174
|
+
)
|
175
|
+
else:
|
176
|
+
logger.warning("Quantum initialization not available, using k-means++")
|
177
|
+
return self._kmeans_plus_plus_init(X)
|
178
|
+
except Exception as e:
|
179
|
+
logger.error(f"Quantum initialization failed: {e}")
|
180
|
+
return self._kmeans_plus_plus_init(X)
|
181
|
+
|
182
|
+
def _encode_data_point(self, x: np.ndarray) -> Any:
|
183
|
+
"""Encode data point into quantum state."""
|
184
|
+
try:
|
185
|
+
if hasattr(self.backend, 'encode_data_point'):
|
186
|
+
return self.backend.encode_data_point(
|
187
|
+
x, encoding=self.encoding, n_qubits=self.n_qubits
|
188
|
+
)
|
189
|
+
else:
|
190
|
+
return self._fallback_encoding(x)
|
191
|
+
except Exception as e:
|
192
|
+
logger.error(f"Data encoding failed: {e}")
|
193
|
+
return self._fallback_encoding(x)
|
194
|
+
|
195
|
+
def _fallback_encoding(self, x: np.ndarray) -> Any:
|
196
|
+
"""Fallback data encoding."""
|
197
|
+
logger.warning("Using fallback data encoding")
|
198
|
+
return None
|
199
|
+
|
200
|
+
def _quantum_distance(self, x: np.ndarray, centroid: np.ndarray) -> float:
|
201
|
+
"""Compute quantum distance between point and centroid."""
|
202
|
+
try:
|
203
|
+
if hasattr(self.backend, 'compute_quantum_distance'):
|
204
|
+
return self.backend.compute_quantum_distance(
|
205
|
+
x, centroid,
|
206
|
+
metric=self.distance_metric,
|
207
|
+
encoding=self.encoding,
|
208
|
+
n_qubits=self.n_qubits,
|
209
|
+
shots=self.shots
|
210
|
+
)
|
211
|
+
else:
|
212
|
+
return self._fallback_distance(x, centroid)
|
213
|
+
except Exception as e:
|
214
|
+
logger.error(f"Quantum distance computation failed: {e}")
|
215
|
+
return self._fallback_distance(x, centroid)
|
216
|
+
|
217
|
+
def _fallback_distance(self, x: np.ndarray, centroid: np.ndarray) -> float:
|
218
|
+
"""Fallback classical distance computation."""
|
219
|
+
if self.distance_metric == 'euclidean':
|
220
|
+
return np.linalg.norm(x - centroid)
|
221
|
+
elif self.distance_metric == 'manhattan':
|
222
|
+
return np.sum(np.abs(x - centroid))
|
223
|
+
else:
|
224
|
+
return np.linalg.norm(x - centroid) # Default to euclidean
|
225
|
+
|
226
|
+
def _compute_distances_batch(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
|
227
|
+
"""Compute distances between all points and all centroids."""
|
228
|
+
n_samples, n_features = X.shape
|
229
|
+
distances = np.zeros((n_samples, self.n_clusters))
|
230
|
+
|
231
|
+
if self.method == 'distance':
|
232
|
+
# Use quantum distance calculations
|
233
|
+
for i in range(n_samples):
|
234
|
+
for j in range(self.n_clusters):
|
235
|
+
distances[i, j] = self._quantum_distance(X[i], centroids[j])
|
236
|
+
|
237
|
+
elif self.method == 'amplitude':
|
238
|
+
# Use quantum amplitude estimation
|
239
|
+
distances = self._amplitude_estimation_distances(X, centroids)
|
240
|
+
|
241
|
+
elif self.method == 'variational':
|
242
|
+
# Use variational quantum circuits
|
243
|
+
distances = self._variational_distances(X, centroids)
|
244
|
+
|
245
|
+
else:
|
246
|
+
# Fallback to classical distances
|
247
|
+
for i in range(n_samples):
|
248
|
+
for j in range(self.n_clusters):
|
249
|
+
distances[i, j] = self._fallback_distance(X[i], centroids[j])
|
250
|
+
|
251
|
+
return distances
|
252
|
+
|
253
|
+
def _amplitude_estimation_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
|
254
|
+
"""Use quantum amplitude estimation for distance computation."""
|
255
|
+
try:
|
256
|
+
if hasattr(self.backend, 'amplitude_estimation_distances'):
|
257
|
+
return self.backend.amplitude_estimation_distances(
|
258
|
+
X, centroids,
|
259
|
+
encoding=self.encoding,
|
260
|
+
shots=self.shots
|
261
|
+
)
|
262
|
+
else:
|
263
|
+
logger.warning("Amplitude estimation not available, using classical")
|
264
|
+
return self._classical_distances(X, centroids)
|
265
|
+
except Exception as e:
|
266
|
+
logger.error(f"Amplitude estimation failed: {e}")
|
267
|
+
return self._classical_distances(X, centroids)
|
268
|
+
|
269
|
+
def _variational_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
|
270
|
+
"""Use variational quantum circuits for distance computation."""
|
271
|
+
try:
|
272
|
+
if hasattr(self.backend, 'variational_distances'):
|
273
|
+
return self.backend.variational_distances(
|
274
|
+
X, centroids,
|
275
|
+
encoding=self.encoding,
|
276
|
+
shots=self.shots
|
277
|
+
)
|
278
|
+
else:
|
279
|
+
logger.warning("Variational distances not available, using classical")
|
280
|
+
return self._classical_distances(X, centroids)
|
281
|
+
except Exception as e:
|
282
|
+
logger.error(f"Variational distance computation failed: {e}")
|
283
|
+
return self._classical_distances(X, centroids)
|
284
|
+
|
285
|
+
def _classical_distances(self, X: np.ndarray, centroids: np.ndarray) -> np.ndarray:
|
286
|
+
"""Classical distance computation."""
|
287
|
+
n_samples = X.shape[0]
|
288
|
+
distances = np.zeros((n_samples, self.n_clusters))
|
289
|
+
|
290
|
+
for i in range(n_samples):
|
291
|
+
for j in range(self.n_clusters):
|
292
|
+
distances[i, j] = self._fallback_distance(X[i], centroids[j])
|
293
|
+
|
294
|
+
return distances
|
295
|
+
|
296
|
+
def _assign_clusters(self, distances: np.ndarray) -> np.ndarray:
|
297
|
+
"""Assign points to clusters based on distances."""
|
298
|
+
return np.argmin(distances, axis=1)
|
299
|
+
|
300
|
+
def _update_centroids(self, X: np.ndarray, labels: np.ndarray) -> np.ndarray:
|
301
|
+
"""Update cluster centroids."""
|
302
|
+
centroids = np.zeros((self.n_clusters, X.shape[1]))
|
303
|
+
|
304
|
+
for k in range(self.n_clusters):
|
305
|
+
cluster_points = X[labels == k]
|
306
|
+
if len(cluster_points) > 0:
|
307
|
+
centroids[k] = np.mean(cluster_points, axis=0)
|
308
|
+
else:
|
309
|
+
# Handle empty clusters - reinitialize randomly
|
310
|
+
centroids[k] = X[np.random.randint(len(X))]
|
311
|
+
|
312
|
+
return centroids
|
313
|
+
|
314
|
+
def _compute_inertia(self, X: np.ndarray, labels: np.ndarray, centroids: np.ndarray) -> float:
|
315
|
+
"""Compute within-cluster sum of squares (inertia)."""
|
316
|
+
inertia = 0.0
|
317
|
+
for k in range(self.n_clusters):
|
318
|
+
cluster_points = X[labels == k]
|
319
|
+
if len(cluster_points) > 0:
|
320
|
+
distances = np.sum((cluster_points - centroids[k]) ** 2, axis=1)
|
321
|
+
inertia += np.sum(distances)
|
322
|
+
return inertia
|
323
|
+
|
324
|
+
def _check_convergence(self, old_centroids: np.ndarray, new_centroids: np.ndarray) -> bool:
|
325
|
+
"""Check if centroids have converged."""
|
326
|
+
centroid_shift = np.max(np.linalg.norm(new_centroids - old_centroids, axis=1))
|
327
|
+
return centroid_shift < self.tolerance
|
328
|
+
|
329
|
+
def fit(self, X: np.ndarray, y: Optional[np.ndarray] = None, **kwargs) -> 'QuantumKMeans':
|
330
|
+
"""Fit quantum K-means to the data.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
X: Training data
|
334
|
+
y: Ignored (unsupervised learning)
|
335
|
+
**kwargs: Additional fitting parameters
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
Self for method chaining
|
339
|
+
|
340
|
+
"""
|
341
|
+
logger.info(f"Fitting QuantumKMeans to data of shape {X.shape}")
|
342
|
+
|
343
|
+
# Validate and preprocess data
|
344
|
+
super().fit(X, y, **kwargs)
|
345
|
+
|
346
|
+
# Normalize data if specified
|
347
|
+
if self.normalize_data:
|
348
|
+
X = self.scaler.fit_transform(X)
|
349
|
+
|
350
|
+
# Determine quantum circuit requirements
|
351
|
+
self.n_qubits = self._determine_qubits(X.shape[1])
|
352
|
+
|
353
|
+
# Initialize centroids
|
354
|
+
centroids = self._initialize_centroids(X)
|
355
|
+
|
356
|
+
# Reset convergence history
|
357
|
+
self.convergence_history = []
|
358
|
+
|
359
|
+
# Main K-means iteration loop
|
360
|
+
for iteration in range(self.max_iterations):
|
361
|
+
# Store old centroids for convergence check
|
362
|
+
old_centroids = centroids.copy()
|
363
|
+
|
364
|
+
# Compute distances and assign clusters
|
365
|
+
distances = self._compute_distances_batch(X, centroids)
|
366
|
+
labels = self._assign_clusters(distances)
|
367
|
+
|
368
|
+
# Update centroids
|
369
|
+
centroids = self._update_centroids(X, labels)
|
370
|
+
|
371
|
+
# Compute inertia
|
372
|
+
inertia = self._compute_inertia(X, labels, centroids)
|
373
|
+
self.convergence_history.append(inertia)
|
374
|
+
|
375
|
+
# Check convergence
|
376
|
+
if self._check_convergence(old_centroids, centroids):
|
377
|
+
logger.info(f"Converged after {iteration + 1} iterations")
|
378
|
+
break
|
379
|
+
|
380
|
+
if iteration % 10 == 0:
|
381
|
+
logger.info(f"Iteration {iteration}: Inertia = {inertia:.6f}")
|
382
|
+
|
383
|
+
# Store final results
|
384
|
+
self.cluster_centers_ = centroids
|
385
|
+
self.labels_ = labels
|
386
|
+
self.inertia_ = inertia
|
387
|
+
self.n_iter_ = iteration + 1
|
388
|
+
|
389
|
+
# Fit classical K-means for comparison
|
390
|
+
if self.classical_fallback:
|
391
|
+
try:
|
392
|
+
self.classical_kmeans.fit(X)
|
393
|
+
except Exception as e:
|
394
|
+
logger.warning(f"Classical K-means fitting failed: {e}")
|
395
|
+
|
396
|
+
self.is_fitted = True
|
397
|
+
|
398
|
+
logger.info(f"Quantum K-means completed. Final inertia: {self.inertia_:.6f}")
|
399
|
+
|
400
|
+
return self
|
401
|
+
|
402
|
+
def predict(self, X: np.ndarray, **kwargs) -> np.ndarray:
|
403
|
+
"""Predict cluster labels for new data.
|
404
|
+
|
405
|
+
Args:
|
406
|
+
X: Data to cluster
|
407
|
+
**kwargs: Additional parameters
|
408
|
+
|
409
|
+
Returns:
|
410
|
+
Cluster labels
|
411
|
+
|
412
|
+
"""
|
413
|
+
if not self.is_fitted:
|
414
|
+
raise ValueError("QuantumKMeans must be fitted before prediction")
|
415
|
+
|
416
|
+
# Normalize data if specified
|
417
|
+
if self.normalize_data:
|
418
|
+
X = self.scaler.transform(X)
|
419
|
+
|
420
|
+
# Compute distances to centroids
|
421
|
+
distances = self._compute_distances_batch(X, self.cluster_centers_)
|
422
|
+
|
423
|
+
# Assign to nearest cluster
|
424
|
+
return self._assign_clusters(distances)
|
425
|
+
|
426
|
+
def fit_predict(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> np.ndarray:
|
427
|
+
"""Fit K-means and return cluster labels."""
|
428
|
+
return self.fit(X, y).labels_
|
429
|
+
|
430
|
+
def transform(self, X: np.ndarray) -> np.ndarray:
|
431
|
+
"""Transform data to cluster-distance space."""
|
432
|
+
if not self.is_fitted:
|
433
|
+
raise ValueError("QuantumKMeans must be fitted before transform")
|
434
|
+
|
435
|
+
# Normalize data if specified
|
436
|
+
if self.normalize_data:
|
437
|
+
X = self.scaler.transform(X)
|
438
|
+
|
439
|
+
# Return distances to all centroids
|
440
|
+
return self._compute_distances_batch(X, self.cluster_centers_)
|
441
|
+
|
442
|
+
def get_quantum_advantage_metrics(self) -> Dict[str, Any]:
|
443
|
+
"""Analyze potential quantum advantage."""
|
444
|
+
if not self.is_fitted:
|
445
|
+
raise ValueError("Must fit model first")
|
446
|
+
|
447
|
+
n_features = self.cluster_centers_.shape[1]
|
448
|
+
n_samples = self.n_samples_
|
449
|
+
|
450
|
+
metrics = {
|
451
|
+
'data_dimension': n_features,
|
452
|
+
'n_samples': n_samples,
|
453
|
+
'n_clusters': self.n_clusters,
|
454
|
+
'quantum_circuit_qubits': self.n_qubits,
|
455
|
+
'encoding_efficiency': n_features / self.n_qubits if self.n_qubits > 0 else 1,
|
456
|
+
}
|
457
|
+
|
458
|
+
# Complexity estimates
|
459
|
+
classical_complexity = n_samples * self.n_clusters * n_features * self.n_iter_
|
460
|
+
quantum_complexity = n_samples * self.n_clusters * self.n_qubits * self.shots * self.n_iter_
|
461
|
+
|
462
|
+
metrics.update({
|
463
|
+
'classical_complexity_estimate': classical_complexity,
|
464
|
+
'quantum_complexity_estimate': quantum_complexity,
|
465
|
+
'theoretical_speedup': classical_complexity / quantum_complexity if quantum_complexity > 0 else 1,
|
466
|
+
})
|
467
|
+
|
468
|
+
return metrics
|
469
|
+
|
470
|
+
def compare_with_classical(self, X: np.ndarray, y_true: Optional[np.ndarray] = None) -> Dict[str, Any]:
|
471
|
+
"""Compare quantum K-means results with classical K-means."""
|
472
|
+
if not self.is_fitted or not hasattr(self.classical_kmeans, 'cluster_centers_'):
|
473
|
+
raise ValueError("Both quantum and classical K-means must be fitted")
|
474
|
+
|
475
|
+
# Get predictions from both methods
|
476
|
+
quantum_labels = self.predict(X)
|
477
|
+
classical_labels = self.classical_kmeans.predict(X if not self.normalize_data
|
478
|
+
else self.scaler.transform(X))
|
479
|
+
|
480
|
+
comparison = {
|
481
|
+
'quantum_inertia': self.inertia_,
|
482
|
+
'classical_inertia': self.classical_kmeans.inertia_,
|
483
|
+
'inertia_ratio': self.inertia_ / self.classical_kmeans.inertia_ if self.classical_kmeans.inertia_ > 0 else float('inf'),
|
484
|
+
'quantum_iterations': self.n_iter_,
|
485
|
+
'classical_iterations': self.classical_kmeans.n_iter_,
|
486
|
+
}
|
487
|
+
|
488
|
+
# Compute silhouette scores
|
489
|
+
try:
|
490
|
+
if len(np.unique(quantum_labels)) > 1:
|
491
|
+
quantum_silhouette = silhouette_score(X, quantum_labels)
|
492
|
+
comparison['quantum_silhouette'] = quantum_silhouette
|
493
|
+
|
494
|
+
if len(np.unique(classical_labels)) > 1:
|
495
|
+
classical_silhouette = silhouette_score(X, classical_labels)
|
496
|
+
comparison['classical_silhouette'] = classical_silhouette
|
497
|
+
|
498
|
+
if 'quantum_silhouette' in comparison and 'classical_silhouette' in comparison:
|
499
|
+
comparison['silhouette_ratio'] = quantum_silhouette / classical_silhouette
|
500
|
+
|
501
|
+
except Exception as e:
|
502
|
+
logger.warning(f"Silhouette score computation failed: {e}")
|
503
|
+
|
504
|
+
# Compare with ground truth if available
|
505
|
+
if y_true is not None:
|
506
|
+
try:
|
507
|
+
quantum_ari = adjusted_rand_score(y_true, quantum_labels)
|
508
|
+
classical_ari = adjusted_rand_score(y_true, classical_labels)
|
509
|
+
|
510
|
+
comparison.update({
|
511
|
+
'quantum_adjusted_rand_score': quantum_ari,
|
512
|
+
'classical_adjusted_rand_score': classical_ari,
|
513
|
+
'ari_ratio': quantum_ari / classical_ari if classical_ari != 0 else float('inf'),
|
514
|
+
})
|
515
|
+
|
516
|
+
except Exception as e:
|
517
|
+
logger.warning(f"Adjusted rand score computation failed: {e}")
|
518
|
+
|
519
|
+
# Compare centroid similarities
|
520
|
+
centroid_distances = []
|
521
|
+
for i in range(min(len(self.cluster_centers_), len(self.classical_kmeans.cluster_centers_))):
|
522
|
+
dist = np.linalg.norm(self.cluster_centers_[i] - self.classical_kmeans.cluster_centers_[i])
|
523
|
+
centroid_distances.append(dist)
|
524
|
+
|
525
|
+
if centroid_distances:
|
526
|
+
comparison.update({
|
527
|
+
'centroid_distances': centroid_distances,
|
528
|
+
'mean_centroid_distance': np.mean(centroid_distances),
|
529
|
+
'max_centroid_distance': np.max(centroid_distances),
|
530
|
+
})
|
531
|
+
|
532
|
+
return comparison
|
533
|
+
|
534
|
+
def analyze_convergence(self) -> Dict[str, Any]:
|
535
|
+
"""Analyze convergence properties."""
|
536
|
+
if not self.convergence_history:
|
537
|
+
return {'message': 'No convergence history available'}
|
538
|
+
|
539
|
+
inertias = np.array(self.convergence_history)
|
540
|
+
|
541
|
+
return {
|
542
|
+
'total_iterations': len(inertias),
|
543
|
+
'final_inertia': inertias[-1],
|
544
|
+
'initial_inertia': inertias[0],
|
545
|
+
'inertia_reduction': inertias[0] - inertias[-1] if len(inertias) > 0 else 0,
|
546
|
+
'convergence_rate': np.mean(np.diff(inertias)) if len(inertias) > 1 else 0,
|
547
|
+
'converged': len(inertias) < self.max_iterations,
|
548
|
+
'inertia_variance': np.var(inertias[-10:]) if len(inertias) >= 10 else np.var(inertias),
|
549
|
+
}
|
550
|
+
|
551
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
552
|
+
"""Get quantum K-means parameters."""
|
553
|
+
params = super().get_params(deep)
|
554
|
+
params.update({
|
555
|
+
'n_clusters': self.n_clusters,
|
556
|
+
'method': self.method,
|
557
|
+
'distance_metric': self.distance_metric,
|
558
|
+
'encoding': self.encoding,
|
559
|
+
'max_iterations': self.max_iterations,
|
560
|
+
'tolerance': self.tolerance,
|
561
|
+
'init_method': self.init_method,
|
562
|
+
'classical_fallback': self.classical_fallback,
|
563
|
+
'normalize_data': self.normalize_data,
|
564
|
+
'random_state': self.random_state,
|
565
|
+
})
|
566
|
+
return params
|
567
|
+
|
568
|
+
def set_params(self, **params) -> 'QuantumKMeans':
|
569
|
+
"""Set quantum K-means parameters."""
|
570
|
+
if self.is_fitted and any(key in params for key in
|
571
|
+
['n_clusters', 'method', 'distance_metric', 'encoding']):
|
572
|
+
logger.warning("Changing core parameters requires refitting the model")
|
573
|
+
self.is_fitted = False
|
574
|
+
|
575
|
+
return super().set_params(**params)
|