Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python-envs.defaultEnvManager": "ms-python.python:system"
}
21 changes: 21 additions & 0 deletions quantum_acoustic_qsvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Quantum Acoustic QSVM & Multi-Class QCNN Pipeline

This repository contains a hybrid Quantum Machine Learning (QML) pipeline for acoustic signal processing, statistical feature filtering, and classification using PennyLane and Scikit-Learn.

## 🔑 Key Features
* **DSP & Spectral Extraction**: Fast Mel-Spectrogram extraction and feature aggregation using `librosa`.
* **Statistical Feature Selection**: Vectorized $t$-test and ANOVA $F$-test filtering ($p < 0.01$) to compress frequency feature space.
* **PennyLane QSVM Engine**: Vectorized state-vector embeddings (`lightning.qubit`) using an entangling feature map ansatz to precompute quantum Gram matrices for SVM classification.
* **Hierarchical QCNN Architecture**: 8-qubit Quantum Convolutional and Pooling Neural Network built with custom variational ansatzes for 2D time-frequency patches and multi-class expectation decoding.

---

## 📁 Repository Structure

```text
quantum_acoustic_qsvm/
├── README.md # Project overview & usage instructions
├── requirements.txt # Dependencies
├── main_pipeline.py # Quantum Kernel + SVM pipeline script
├── train_qcnn.py # Multi-Class QCNN training script
└── demo.ipynb # Walkthrough demo notebook
Empty file.
94 changes: 94 additions & 0 deletions quantum_acoustic_qsvm/main_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import time
import librosa
import numpy as np
import pennylane as qml
from sklearn.model_selection import StratifiedKFold
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score

# ==========================================
# 1. GENERATE BASE DATASET (DSP FUNNEL)
# ==========================================
files = [librosa.example('nutcracker'), librosa.example('choice')]

def extract_dsp_features(file_path):
y, sr = librosa.load(file_path, sr=None, mono=True)
mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=32)
mel_db = librosa.power_to_db(mel_spec, ref=np.max)
return np.mean(mel_db, axis=1)

base_0 = extract_dsp_features(files[0])
base_1 = extract_dsp_features(files[1])

def generate_dataset(size):
half = size // 2
X = np.vstack([
base_0 + np.random.normal(0, 0.5, size=(half, 32)),
base_1 + np.random.normal(0, 0.5, size=(half, 32))
])
y = np.array([0] * half + [1] * half)
X_f = SelectKBest(score_func=f_classif, k=4).fit_transform(X, y)
X_q = MinMaxScaler(feature_range=(0, np.pi)).fit_transform(X_f)
return X_q, y

# Standard operational dataset size
X_data, y_data = generate_dataset(60)

# ==========================================
# 2. DEFINE QUANTUM KERNEL (WITH ENTANGLEMENT)
# ==========================================
n_qubits = 4
dev = qml.device("lightning.qubit", wires=n_qubits)

def ansatz(x, wires):
"""Expressive quantum feature map with single-qubit rotations and entanglement."""
for i in range(len(wires)):
qml.RX(x[i], wires=wires[i])
qml.RY(x[i], wires=wires[i])
# Entangling layer
for i in range(len(wires)):
qml.CNOT(wires=[wires[i], wires[(i + 1) % len(wires)]])

@qml.qnode(dev)
def quantum_kernel_circuit(x1, x2):
ansatz(x1, wires=range(n_qubits))
qml.adjoint(ansatz)(x2, wires=range(n_qubits))
return qml.probs(wires=range(n_qubits))

kernel_func = lambda x1, x2: quantum_kernel_circuit(x1, x2)[0]

# ==========================================
# 3. STRATIFIED CROSS-VALIDATION & HYPERPARAMETER TUNING
# ==========================================
print("📦 Computing Global Training Gram Matrix...")
global_kernel_matrix = qml.kernels.square_kernel_matrix(X_data, kernel_func)

param_grid = [0.1, 1.0, 10.0, 100.0]
cv_strategy = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

best_score = 0.0
best_C = None

print("⚡ Running Hyperparameter Optimization across Quantum Features...")
for C in param_grid:
fold_scores = []
for train_idx, val_idx in cv_strategy.split(X_data, y_data):
# Slice Gram matrix correctly for precomputed SVM kernel
K_train = global_kernel_matrix[np.ix_(train_idx, train_idx)]
K_val = global_kernel_matrix[np.ix_(val_idx, train_idx)]

clf = SVC(kernel="precomputed", C=C)
clf.fit(K_train, y_data[train_idx])
preds = clf.predict(K_val)
fold_scores.append(accuracy_score(y_data[val_idx], preds))

mean_score = np.mean(fold_scores)
if mean_score > best_score:
best_score = mean_score
best_C = C

print(f"\n🏆 Grid Search Optimization Complete!")
print(f"Best Regularization Parameter ('C'): {best_C}")
print(f"Mean Validation Cross-Validation Accuracy Score: {best_score * 100:.2f}%\n")
7 changes: 7 additions & 0 deletions quantum_acoustic_qsvm/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pennylane>=0.35.0
pennylane-lightning>=0.35.0
librosa>=0.10.1
scikit-learn>=1.3.0
scipy>=1.10.0
numpy>=1.24.0
matplotlib>=3.7.0
192 changes: 192 additions & 0 deletions quantum_acoustic_qsvm/train_qcnn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import time
import librosa
from pennylane import numpy as np
import pennylane as qml
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import accuracy_score, classification_report

# ========================================================
# 1. ENHANCED MULTI-CLASS 2D ACOUSTIC PATCH INGESTION
# ========================================================
files = [
librosa.example('nutcracker'),
librosa.example('choice'),
librosa.example('trumpet'),
librosa.example('vibeace')
]

def extract_2d_spectrogram_patches(file_path):
"""Extracts a static 4x4 frequency-time block preserving structural layout."""
y, sr = librosa.load(file_path, sr=None, mono=True)
mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=4, n_fft=2048, hop_length=512)
mel_db = librosa.power_to_db(mel_spec, ref=np.max)

mid_frame = mel_db.shape[1] // 2
patch_2d = mel_db[:, mid_frame:mid_frame + 4]

if patch_2d.shape[1] < 4:
patch_2d = np.pad(patch_2d, ((0, 0), (0, 4 - patch_2d.shape[1])), mode='constant')

return patch_2d

base_patches = [extract_2d_spectrogram_patches(f) for f in files]
num_classes = 4

def generate_multi_class_2d_dataset(size):
"""Generates a dataset split evenly across 4 distinct audio classes."""
samples_per_class = size // num_classes
X_list, y_list = [], []

for class_idx in range(num_classes):
for _ in range(samples_per_class):
noisy_patch = base_patches[class_idx] + np.random.normal(0, 0.2, size=(4, 4))
X_list.append(noisy_patch)
y_list.append(class_idx)

X_raw = np.array(X_list)
y = np.array(y_list)

X_flat = X_raw.reshape(size, 16)
X_scaled = MinMaxScaler(feature_range=(0, np.pi)).fit_transform(np.array(X_flat))
return X_scaled.reshape(size, 4, 4), y

dataset_size = 40
X_data, y_data = generate_multi_class_2d_dataset(dataset_size)

target_map = {
0: np.array([0.8, 0.8]),
1: np.array([0.8, -0.8]),
2: np.array([-0.8, 0.8]),
3: np.array([-0.8, -0.8])
}

y_transformed = np.array([target_map[int(val)] for val in y_data])

# ========================================================
# 2. HIERARCHICAL QCNN WITH INDEPENDENT PAIR WEIGHTS
# ========================================================
n_qubits = 8
dev = qml.device("lightning.qubit", wires=n_qubits)

def quantum_conv_layer_unshared(params, wires):
"""Applies independent parameters to each qubit pair to break spatial symmetry."""
param_idx = 0
# Even pairs
for w1, w2 in zip(wires[0::2], wires[1::2]):
qml.CRX(params[param_idx], wires=[w1, w2])
qml.CRY(params[param_idx + 1], wires=[w2, w1])
param_idx += 2
# Odd pairs
for w1, w2 in zip(wires[1::2], wires[2::2]):
qml.CRX(params[param_idx], wires=[w1, w2])
param_idx += 1

def quantum_pooling_layer(wires_sink, wires_source):
for si, so in zip(wires_sink, wires_source):
qml.CRZ(np.pi / 2, wires=[so, si])

@qml.qnode(dev)
def qcnn_circuit(patch_2d, weights):
features = patch_2d.flatten()
for i in range(n_qubits):
qml.RX(features[i], wires=i)
qml.RY(features[i + 8], wires=i)

# Layer 1: 8 wires -> 11 parameters
quantum_conv_layer_unshared(weights[:11], wires=list(range(8)))
quantum_pooling_layer(wires_sink=[0, 2, 4, 6], wires_source=[1, 3, 5, 7])

# Layer 2: 4 active wires -> 5 parameters
quantum_conv_layer_unshared(weights[11:16], wires=[0, 2, 4, 6])
quantum_pooling_layer(wires_sink=[0, 4], wires_source=[2, 6])

return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(4))]

# ========================================================
# 3. OPTIMIZATION & EVALUATION
# ========================================================
def cost_function(weights, X_batch, y_batch):
loss = 0.0
for sample, target in zip(X_batch, y_batch):
expectations = qcnn_circuit(sample, weights)
loss += (expectations[0] - target[0]) ** 2 + (expectations[1] - target[1]) ** 2
return loss / len(X_batch)

initial_lr = 0.15
decay_rate = 0.85
decay_patience = 3
patience = 8

np.random.seed(42)
# Total of 16 independent parameters across conv blocks
weights = np.random.randn(16, requires_grad=True)

best_weights = weights.copy()
best_loss = float('inf')
patience_counter = 0

print("🏋️ Starting Asymmetric QCNN Parametric Training Loop...")
print("---------------------------------------------------------")

epochs = 35
batch_size = 8
current_lr = initial_lr

for epoch in range(epochs):
if epoch > 0 and epoch % decay_patience == 0:
current_lr *= decay_rate

opt = qml.AdamOptimizer(stepsize=current_lr)

indices = np.random.permutation(len(X_data))
X_shuffled = X_data[indices]
y_shuffled = y_transformed[indices]

X_batch = X_shuffled[:batch_size]
y_batch = y_shuffled[:batch_size]

weights, current_loss = opt.step_and_cost(lambda w: cost_function(w, X_batch, y_batch), weights)

if current_loss < best_loss:
best_loss = current_loss
best_weights = weights.copy()
patience_counter = 0
status_msg = "⭐ (New Best Saved)"
else:
patience_counter += 1
status_msg = ""

print(f"Epoch {epoch+1:02d} | LR: {current_lr:.4f} | Loss: {current_loss:.4f} {status_msg}")

if patience_counter >= patience:
print(f"\n🛑 Early stopping triggered at Epoch {epoch+1}!")
break

print("---------------------------------------------------------")
print("🔮 Evaluation: Distance Decoding on Best Weights...")

def decode_expectations_euclidean(exp_vector):
exp_arr = np.array(exp_vector)
best_class = 0
min_dist = float('inf')

for class_idx, target_vec in target_map.items():
dist = np.sum((exp_arr - target_vec) ** 2)
if dist < min_dist:
min_dist = dist
best_class = class_idx

return best_class

final_predictions = []
for sample in X_data:
raw_scores = qcnn_circuit(sample, best_weights)
predicted_class = decode_expectations_euclidean(raw_scores)
final_predictions.append(predicted_class)

y_true = [int(val) for val in y_data]
final_acc = accuracy_score(y_true, final_predictions)

print(f"\n🏆 Post-Training Classification Accuracy: {final_acc * 100:.2f}%\n")
print("📊 Detailed Classification Report:")
print(classification_report(y_true, final_predictions, target_names=['Nutcracker', 'Choice', 'Trumpet', 'Vibe Ace'], zero_division=0))