Day 6: Anatomy of VQC — Its Limitations in Cybersecurity Analytics
We have noted that the Variational Quantum Classifier (VQC) often performs poorly. This is puzzling, especially considering that examples in Qiskit tutorials demonstrate an accuracy of 100%, and PennyLane also shows interesting results. To understand this discrepancy, we delve deeper into the workings of VQC. We aim to ascertain the limitations and potential of this algorithm, especially in the context of cybersecurity analytics.
Let’s code the theory step by step to find the probable issue. Same as our previous anatomy study, we need to minimize the loss; let’s define it:
The loss of an individual sample with label y and predicted probability p (of class 1) is:
loss(y,p)=−y⋅log(p)−(1−y)⋅log(1−p)
Here, y is the true label, and p is the predicted probability of the sample belonging to class 1.
The safe_log function, is an auxiliary function that ensures we don't take the logarithm of zero. We make a cost function, and we minimize it using COBYLA:
# Helper function for the loss
def safe_log(value):
return np.log(max(cutoff, value))
# Loss function
def loss(theta, X, y):
loss_value = 0
for X_i, y_i in zip(X, y):
pr_1 = predict(theta, X_i, return_probabilities=True)
loss_value -= y_i * safe_log(pr_1) + (1 - y_i) * safe_log(1 - pr_1)
return loss_value# Convert labels from [-1, 1] to [0, 1]
one_zero_labels = (1 + np.array(labels)) // 2
cost = lambda theta: loss(theta, points, one_zero_labels.tolist())
# Training the VQC
x0 = np.random.random(model.num_parameters)
cobyla = COBYLA()
result = cobyla.minimize(cost, x0=x0)
COBYLA (Constrained Optimization BY Linear Approximation) is a nonlinear derivative-free optimization algorithm that utilizes linear approximation techniques. The function to be optimized is denoted by F(x). COBYLA evaluates the function at n+1 vertices of a simplex, which is an n-dimensional geometric object. The edges of the simplex are then interpolated using a linear polynomial L(x), which is defined for all real numbers in set R. Each iteration of COBYLA requires a trust region radius(Delta>0) that can be automatically adjusted. The next set of variables is obtained by minimizing the function L(x), subject to the constraint: |x-x0|<Delta. Here, F(x0) represents the minimum value of F(x) found thus far.
Now, we need the quantum part, which is about bringing the data into the quantum state(by a featuremap like ZFeatureMap) and calculating weights(by another circuit like EfficientSU2).
feature_map=ZFeatureMap(num_features,reps=1)
model=EfficientSU2(num_features, reps=2, entanglement="pairwise")
circuit=feature_map.compose(model)circuit.measure_all()
The above code makes a circuit like this:
Inside these boxes are quantum operators, Hadamard and Pauli show ZFeatureMap, and the rest are Rotation around Z-axis and Rotation around Y-axis showing the EfficientSU2:
It’s like using a pipeline same as the below:
When we measure, we will get some results like |0101>, so we want aparity function that determines the parity of a given bit string. In this context, parity refers to whether the number of '1' bits in the bit string is even or odd.
def parity(bitstring):
#return 1 if there's an even number of 1's , otherwise return -1
return 1 if bitstring.count("1") %2 else -1
labels = {parity(bitstring): count /1000 for bitstring, count in counts.items()}
print(labels)
Let’s make the circle test data and put it all together and see the accuracy of this implementation of VQC:
from qiskit import BasicAer, transpile
import numpy as np
from qiskit.circuit.library import ZZFeatureMap, EfficientSU2
from qiskit.algorithms.optimizers import COBYLA
import matplotlib.pyplot as plt
# Data generation function
def circle():
num_points = 120
points = 1 - 2 * np.random.random((num_points, 2))
radius = 0.6
labels = [1 if np.linalg.norm(point) > radius else -1 for point in points]
return points, labels
# Parity function
def parity(bit_string):
return sum([int(bit) for bit in bit_string]) % 2
# Prediction function
def predict(theta, point, return_probabilities=False):
bound = transpiled.assign_parameters(np.concatenate((point, theta)))
job = backend.run(bound, shots=shots)
result = job.result()
counts = result.get_counts()
pr_1 = 0
for bitstr, count in counts.items():
if parity(bitstr) == 1:
pr_1 += count / shots
if return_probabilities:
return pr_1
return 1 if pr_1 > 0.5 else 0
# Helper function for the loss
def safe_log(value):
return np.log(max(cutoff, value))
# Loss function
def loss(theta, X, y):
loss_value = 0
for X_i, y_i in zip(X, y):
pr_1 = predict(theta, X_i, return_probabilities=True)
loss_value -= y_i * safe_log(pr_1) + (1 - y_i) * safe_log(1 - pr_1)
return loss_value
# Generate the data
points, labels = circle()
colors = ["blue" if label == 1 else "red" for label in labels]
# Visualize the original data
plt.figure(figsize=(6, 6))
plt.scatter(points[:, 0], points[:, 1], color=colors)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.title("Original Data")
plt.show()
# Setup for the VQC
num_features = 2
feature_map = ZZ
FeatureMap(num_features, reps=3)
model = EfficientSU2(num_features, reps=2, entanglement="pairwise")
circuit = feature_map.compose(model)
circuit.measure_all()
backend = BasicAer.get_backend("qasm_simulator")
cutoff = 1e-10
shots = 1024
transpiled = transpile(circuit, backend)
# Convert labels from [-1, 1] to [0, 1]
one_zero_labels = (1 + np.array(labels)) // 2
cost = lambda theta: loss(theta, points, one_zero_labels.tolist())
# Training the VQC
x0 = np.random.random(model.num_parameters)
cobyla = COBYLA()result = cobyla.minimize(cost, x0=x0)
# Making predictions
predictions = [predict(result.x, X_i) for X_i in points.tolist()]
# Visualizing the predictions
markers = ["o" if label == predicted_label else "x" for label, predicted_label in zip(one_zero_labels, predictions)]
plt.figure(figsize=(6, 6))
for point, marker, color in zip(points.tolist(), markers, colors):
plt.scatter(point[0], point[1], color=color, marker=marker)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.title("Predictions from VQC")
plt.show()
Our implementation will work faster than standard API as we made a purpose-specific code rather make a generic implementation like Qiskit, and the accuracy for the above toy dataset is 88.4%
def compute_accuracy(predictions, true_labels):
"""Compute accuracy of predictions against true labels."""
correct = sum(p == t for p, t in zip(predictions, true_labels))
accuracy = correct / len(true_labels)
return accuracy
# Calculate accuracy for the VQC predictions
accuracy = compute_accuracy(predictions, one_zero_labels.tolist())
accuracy
What is the bottleneck? Quantum circuits! Due to known issues of stability of architecture, error correction, and complexity of the algorithm.
The last one, the complexity of training, is the very interesting one for cybersecurity data, as researchers have shown training could be the tricky part of VQCs:
Even with 1000 data samples, researchers have not gained a good result for Quantum Cybersecurity Analytics with VQC over simulators:
Considering the cost of VQC and its complexity for training, it is not the best choice for cybersecurity use cases. We will explore the Quatum Neural Network as our next algorithm for Quatum Cybersecurity Analytics.
We’re uncertain if QNN is the solution for Quantum Cybersecurity Analytics. However, what we do know is that we should avoid a hybridization strategy that diminishes the capabilities of quantum computers, as illustrated in the following image. How to make synergy with both computation types is what we are searching for.
Comments