C2QA / bosonic-qiskit

Extension of Qiskit to support hybrid boson-qubit simulations for the NQI C2QA effort.
BSD 2-Clause "Simplified" License
52 stars 16 forks source link

Machine Learning with bosonic-qiskit #79

Closed ShikBan closed 1 year ago

ShikBan commented 1 year ago

I am trying to work with bosonic qiskit and ML. For example, I tried to convert the qubits in this example to qumodes. But when I try to use the mentioned EstimatorQNN, I get the error “ParameterExpression with unbound parameters ({Parameter(x)}) cannot be cast to a complex.” Do you have any suggestions on how to resolve this? Since we are using CV gates, there will be complex numbers involved.

Is there any other way to do ML using bosonic-qiskit?

kevincsmith commented 1 year ago

Hi -- could you share the code you have written that produces the error (i.e., after translating over to qumodes)? This would be helpful so that we can reproduce the error.

ShikBan commented 1 year ago

Sure, I tried to put it all together. Also, I just used some gates as an example; I know they won't give good results.

importing

import os import sys import c2qa import qiskit from math import pi import matplotlib.pyplot as plt import numpy as np from IPython.display import clear_output from qiskit import QuantumCircuit from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B from qiskit.circuit import Parameter from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit.utils import algorithm_globals

from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier, VQC from qiskit_machine_learning.algorithms.regressors import NeuralNetworkRegressor, VQR from qiskit_machine_learning.neural_networks import SamplerQNN, EstimatorQNN

algorithm_globals.random_seed = 42

Plotting the sine function

numsamples = 20 eps = 0.2 lb, ub = -np.pi, np.pi X = np.linspace(lb, ub, num=50).reshape(50, 1) f = lambda x: np.sin(x)

X = (ub - lb) algorithm_globals.random.random([num_samples, 1]) + lb y = f(X[:, 0]) + eps (2 * algorithm_globals.random.random(num_samples) - 1)

plt.plot(X, f(X), "r--") plt.plot(X, y, "bo") plt.show()

preparing the circuit

qmr = c2qa.QumodeRegister(num_qumodes=2, num_qubits_per_qumode=1) qbr = qiskit.QuantumRegister(1)

construct simple feature map

param_x = Parameter("x") feature_map = c2qa.CVCircuit(qmr, name="fm") feature_map.cv_d(param_x, qmr[0])

construct simple ansatz

param_y = Parameter("y") ansatz = c2qa.CVCircuit(qmr, name="vf") ansatz.cv_d(param_y, qmr[0]) ansatz.cv_r(param_y, qmr[0])

construct a circuit

qmr = c2qa.QumodeRegister(num_qumodes=2, num_qubits_per_qumode=1) qbr = qiskit.QuantumRegister(1)

qc = c2qa.CVCircuit(qmr, qbr) qc.compose(feature_map, inplace=True) qc.compose(ansatz, inplace=True) qc.draw()

construct QNN

regression_estimator_qnn = EstimatorQNN( circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters )

callback function that draws a live plot when the .fit() method is called

def callback_graph(weights, obj_func_eval): clear_output(wait=True) objective_func_vals.append(obj_func_eval) plt.title("Objective function value against iteration") plt.xlabel("Iteration") plt.ylabel("Objective function value") plt.plot(range(len(objective_func_vals)), objective_func_vals) plt.show()

construct the regressor from the neural network

regressor = NeuralNetworkRegressor( neural_network=regression_estimator_qnn, loss="squared_error", optimizer=L_BFGS_B(maxiter=5), callback=callback_graph, )

create empty array for callback to store evaluations of the objective function

objective_func_vals = [] plt.rcParams["figure.figsize"] = (12, 6)

fit to data

regressor.fit(X, y)

return to default figsize

plt.rcParams["figure.figsize"] = (6, 4)

score the result

regressor.score(X, y)

tjstavenger-pnnl commented 1 year ago

I'm not too familiar with qiskit-machine-learning -- I'm guessing at the regressor.fit(X, y) step you're using the ML model to fit values to the X & y parameters? This call into fit() is caling into the Qiskit circuit at a point it is expecting bound parameters (at least for the way we've implemented it in bosonic-qiskit), but the parameters aren't actually bound yet.

For example, a call to bind_parameters() before simulating the quantum circuit as we currently test here: https://github.com/C2QA/bosonic-qiskit/blob/main/tests/test_parameterized.py#L49

As it is implemented now, obviously our parameterized circuit support in bosonic-qiskit doesn't support the ML integration, but I'm interested in helping you out to get it working.

tjstavenger-pnnl commented 1 year ago

In doing my due diligence to better understand what you're trying to achieve, it looks like you're following this tutorial, but with CVCircuit and our bosonic gates instead of RY? https://qiskit.org/documentation/machine-learning/tutorials/02_neural_network_classifier_and_regressor.html#Regression

EDIT: I see now that you told me this in your original post ... I missed reading that initially.

tjstavenger-pnnl commented 1 year ago

I was able to get that tutorial working (at least it didn't raise any errors). I'll look more into how bosonic-qiskit handles parameterized circuits vs how Qiskit does by default.

tjstavenger-pnnl commented 1 year ago

See https://github.com/C2QA/bosonic-qiskit/blob/parameterized-ml/tests/test_ml.py for the code I've been using to test.

tjstavenger-pnnl commented 1 year ago

Quick update on what I've found:

  1. bosonic-qiskit for Parameterized circuits casts bound paramters to complex(). This isn't too dissimilar from Qiskit itself that casts bound parameters to float. E.g., in the R gate https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/library/standard_gates/r.py#L85. Though Qiskit isn't consistent with this as the RY gate doesn't cast... https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/library/standard_gates/ry.py#L102-L106
  2. The ParameterExpression class Qiskit uses for Parameterized circuits (both bound and unbound) has math operators overloaded (e.g., +,-,*,/) to let you do math on ParameterExpression. See https://github.com/Qiskit/qiskit-terra/blob/925f9f3abfdee4faeae67503082d7877f080c2ed/qiskit/circuit/parameterexpression.py#L242. I believe this is what lets the RY gate not cast in # 1. So in theory bosonic-qiskit wouldn't have to cast, but it turns out it doesn't help much, as I've tried not casting in bosonic-qiskit and I still run into # 3.
  3. bosonic-qiskit uses scipy sparse matrices as much as it can to reduce its memory footprint. Unfortunately scipy sparse matrices don't understand what to do with math operators between ParameterExpression, so we have to cast as in # 2. I tried temporarily using standard numpy arrays, but I still ran into # 4 anyway
  4. Ultimately the problem traces back to the NeuralNetworkRegressor.fit() function not binding the parameter. In my testing some of the fit iterations work and all parameters are bound. Eventually the fit() doesn't bind the parameter, and when bosonic-qiskit tries to cast it fails. Even without the cast, you can't perform math on an unbound parameter, so it fails. I haven't been able to get the fit() function to not bind a parameter when using base Qiskit (i.e., using the tutorial as-is, without bosonic-qiskit). So presumably there's still something bosonic-qiskit is doing that is confusing the fit() function.
ShikBan commented 1 year ago

Thank you so much for your help. I want to share another code of hybrid NN where I saw that if I use the CV gates with fixed parameters, it works fine (training and testing), but when I train on the gate parameters, I get the "complex number" error. This information may be helpful in resolving the issue. Here is the code: https://colab.research.google.com/drive/1CpAAc4C18j63kM8fTQDssgoTWx5jRKUk?usp=sharing It is a long code because I start with qubits, then qumodes with fixed parameters, and finally qumodes with training parameters

kevincsmith commented 1 year ago

Could you try skipping the transpile step in run()? Since you are using the Aer_simulator, I believe the circuit should run without transpilation. The issue seems to be that transpilation throws an error for a CVCircuit with unbound parameters (whereas a QuantumCircuit does not).

kevincsmith commented 1 year ago

Here is a code snippet which I believe reproduces and clarifies the error in question: `qmr = c2qa.QumodeRegister(num_qumodes=1, num_qubits_per_qumode=2) qbr = qiskit.QuantumRegister(1) circuit = c2qa.CVCircuit(qmr, qbr) theta = qiskit.circuit.Parameter('theta')

Add gates

circuit.cv_d(theta, qmr[0]) circuit.h(qbr[0]) circuit.ry(theta, qbr[0])

simulator = qiskit.Aer.get_backend('aer_simulator')

Run without transpiling -- works okay

simulator.run(circuit.bind_parameters([0.2]),shots=1000)

Bind parameter, then transpile and run -- works okay

circuit_transpiled = qiskit.transpile(circuit.bind_parameters([0.2]),simulator) simulator.run(circuit_transpiled,shots=1000)

Transpile with unbound parameter -- this throws an error

circuit_transpiled = qiskit.transpile(circuit,simulator) simulator.run(circuit_transpiled.bind_parameters([0.2]),shots=1000)`

This final transpilation throws the error "Error decomposing node D: ParameterExpression with unbound parameters ({Parameter(theta)}) cannot be cast to a complex.’”

tjstavenger-pnnl commented 1 year ago

The following link removes all context for the full call stack that results from calling regressor.fit(X, y). After further debugging the error, I found where in the call stack the fit() call is simulating the circuit for you -- including calling transpile. See https://github.com/Qiskit/qiskit-terra/blob/stable/0.22/qiskit/algorithms/gradients/utils.py#L99. Note that this utils.py changed in the current release, this is the one used with Qiskit 0.39.5 that we're currently targeting for bosonic-qiskit.

I'll still emphasize that in my testing many of the simulator runs/shots from calling regressor.fit(X, y) successfully bound the parameters. The complex cast error only happens after an unbound parameter is sent for one of the shots.

This shows where transpile is called in the Qiskit tutorial example, but I'll still stress that even if you simulate without transpiling in your own code, you'll still need to be sure that all parameters are bound before it will work without errors.

UnitaryGates in Qiskit (on which the bosonic-qiskit parameterized gates depend) require that all parameters have been bound with values before they will work. I'll continue to look at if there is a different approach we can take with our parameterized gates to work around this issue.

tjstavenger-pnnl commented 1 year ago

Getting deep into the weeds, here's the full stack:

MainThread:

_wait_for_tstate_lock (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\threading.py:1027)
join (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\threading.py:1011)
shutdown (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\concurrent\futures\thread.py:236)
__exit__ (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\concurrent\futures\_base.py:636)
submit (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\primitives\primitive_job.py:44)
run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\gradients\base_estimator_gradient.py:98)
_backward (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit_machine_learning\neural_networks\estimator_qnn.py:245)
backward (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit_machine_learning\neural_networks\neural_network.py:252)
gradient (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit_machine_learning\algorithms\objective_functions.py:129)
wrapped_gradient (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\optimizers\scipy_optimizer.py:172)
grad_wrapped (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_differentiable_functions.py:164)
update_grad (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_differentiable_functions.py:167)
_update_grad (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_differentiable_functions.py:256)
__init__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_differentiable_functions.py:177)
_prepare_scalar_function (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_optimize.py:263)
_minimize_lbfgsb (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_lbfgsb_py.py:306)
minimize (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\scipy\optimize\_minimize.py:699)
minimize (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\optimizers\scipy_optimizer.py:148)
_fit_internal (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit_machine_learning\algorithms\regressors\neural_network_regressor.py:46)
fit (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit_machine_learning\algorithms\trainable_model.py:201)
test_bosnic_qiskit (c:\Users\stav405\git\bosonic-qiskit\tests\test_ml.py:90)
pytest_pyfunc_call (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\python.py:183)
_multicall (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\callers.py:187)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:84)
_hookexec (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:93)
__call__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\hooks.py:286)
runtest (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\python.py:1641)
pytest_runtest_call (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:162)
_multicall (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\callers.py:187)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:84)
_hookexec (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:93)
__call__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\hooks.py:286)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:255)
from_call (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:311)
call_runtest_hook (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:254)
call_and_report (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:215)
runtestprotocol (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:126)
pytest_runtest_protocol (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\runner.py:109)
_multicall (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\callers.py:187)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:84)
_hookexec (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:93)
__call__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\hooks.py:286)
pytest_runtestloop (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\main.py:348)
_multicall (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\callers.py:187)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:84)
_hookexec (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:93)
__call__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\hooks.py:286)
_main (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\main.py:323)
wrap_session (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\main.py:269)
pytest_cmdline_main (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\main.py:316)
_multicall (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\callers.py:187)
<lambda> (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:84)
_hookexec (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\manager.py:93)
__call__ (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\pluggy\hooks.py:286)
main (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\_pytest\config\__init__.py:162)
_run_code (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\runpy.py:87)
_run_module_as_main (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\runpy.py:194)

Individual shot thread:

_define (c:\Users\stav405\git\bosonic-qiskit\c2qa\operators.py:72)
definition (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\circuit\instruction.py:237)
run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\passes\basis\unroll_custom_definitions.py:77)
_run_this_pass (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\runningpassmanager.py:201)
_do_pass (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\runningpassmanager.py:172)
run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\runningpassmanager.py:125)
_run_single_circuit (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\passmanager.py:283)
run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\passmanager.py:228)
run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\transpiler\passmanager.py:528)
_serial_transpile_circuit (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\compiler\transpiler.py:475)
transpile (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\compiler\transpiler.py:382)
_make_param_shift_gradient_circuit_data (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\gradients\utils.py:99)
_param_shift_preprocessing (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\gradients\utils.py:209)
_run (c:\Users\stav405\git\bosonic-qiskit\venv\Lib\site-packages\qiskit\algorithms\gradients\param_shift_estimator_gradient.py:71)
run (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\concurrent\futures\thread.py:57)
_worker (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\concurrent\futures\thread.py:80)
run (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\threading.py:870)
_bootstrap_inner (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\threading.py:932)
_bootstrap (c:\Users\stav405\AppData\Local\Programs\Python\Python38\Lib\threading.py:890)
tjstavenger-pnnl commented 1 year ago

We haven't forgotten about this -- I'm working with the qiskit-machine-learning devs on a support / feature request ticket to see if we can bypass the transpilation step while using qiskit-machine-learning. Though I wouldn't be surprised if we still run into a problem of the Parameter(x) being unbound.

See https://github.com/Qiskit/qiskit-machine-learning/issues/567

tjstavenger-pnnl commented 1 year ago

Still no real resolution on fixing this, but what I do know:

So until Qiskit supports parameterizing unitary gates or until I work out a better way to support it anyway, I don't think this will be resolved unfortunately. Please let me know if you still need the feature, othwise I'll likely close the issue.

tjstavenger-pnnl commented 1 year ago

Closing for now, please let me know if I need to look into this furhter.