2929 CSV with columns V1..V28, Amount, Class (0=legit, 1=fraud). Example: Kaggle
3030 "Credit Card Fraud Detection" (https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud).
3131 Pass path via --data-file. If no file, a small synthetic imbalanced dataset is used for smoke test.
32+
33+ Training always runs on GPU via lightning.gpu for fair comparison with QDP pipeline.
3234"""
3335
3436from __future__ import annotations
3941from typing import Any
4042
4143import numpy as np
44+ import torch
4245
4346try :
4447 import pennylane as qml
45- from pennylane import numpy as pnp
46- from pennylane .optimize import AdamOptimizer
4748except ImportError as e :
4849 raise SystemExit (
4950 "PennyLane is required. Install with: uv sync --group benchmark"
6970FEATURE_DIM = 2 ** NUM_QUBITS # amplitude embedding dimension (32 for 5 qubits)
7071
7172
72- def _layer (layer_weights : pnp . ndarray , wires : tuple [int , ...]) -> None :
73+ def _layer (layer_weights : torch . Tensor , wires : tuple [int , ...]) -> None :
7374 """Single variational layer: Rot on each wire + ring of CNOTs."""
7475 for i , w in enumerate (wires ):
75- qml .Rot (* layer_weights [i ], wires = w )
76+ qml .Rot (layer_weights [i , 0 ], layer_weights [ i , 1 ], layer_weights [ i , 2 ], wires = w )
7677 for i in range (len (wires )):
7778 qml .CNOT (wires = [wires [i ], wires [(i + 1 ) % len (wires )]])
7879
@@ -199,66 +200,86 @@ def run_training(
199200 lr : float ,
200201 seed : int ,
201202) -> dict [str , Any ]:
202- """Train 5-qubit amplitude VQC with class-weighted loss; report AUPRC, F1, compile/train time."""
203- dev = qml .device ("default.qubit" , wires = NUM_QUBITS )
203+ """Train 5-qubit amplitude VQC on GPU with class-weighted loss; report AUPRC, F1, compile/train time."""
204+ if not torch .cuda .is_available ():
205+ raise RuntimeError ("CUDA GPU is required for training. No CUDA device found." )
206+ try :
207+ dev = qml .device ("lightning.gpu" , wires = NUM_QUBITS )
208+ except Exception as e :
209+ raise RuntimeError (
210+ "lightning.gpu is required for GPU training. Install with: "
211+ "pip install pennylane-lightning[gpu]"
212+ ) from e
213+
214+ device = torch .device ("cuda" )
215+ dtype = torch .float64
204216 wires = tuple (range (NUM_QUBITS ))
205217
206- @qml .qnode (dev , interface = "autograd " , diff_method = "backprop " )
207- def circuit (weights : pnp . ndarray , features : pnp . ndarray ) -> pnp . ndarray :
218+ @qml .qnode (dev , interface = "torch " , diff_method = "adjoint " )
219+ def circuit (weights : torch . Tensor , features : torch . Tensor ) -> torch . Tensor :
208220 qml .AmplitudeEmbedding (features , wires = wires , normalize = True )
209221 for w in weights :
210222 _layer (w , wires )
211223 return qml .expval (qml .PauliZ (0 ))
212224
213- def model (weights : pnp .ndarray , bias : pnp .ndarray , x : pnp .ndarray ) -> pnp .ndarray :
225+ def model (
226+ weights : torch .Tensor , bias : torch .Tensor , x : torch .Tensor
227+ ) -> torch .Tensor :
214228 return circuit (weights , x ) + bias
215229
216230 def cost (
217- weights : pnp . ndarray ,
218- bias : pnp . ndarray ,
219- X_batch : pnp . ndarray ,
220- Y_batch : pnp . ndarray ,
221- w_batch : pnp . ndarray ,
222- ) -> pnp . ndarray :
231+ weights : torch . Tensor ,
232+ bias : torch . Tensor ,
233+ X_batch : torch . Tensor ,
234+ Y_batch : torch . Tensor ,
235+ w_batch : torch . Tensor ,
236+ ) -> torch . Tensor :
223237 # Y in {0,1} -> target in {-1, 1}
224- target = pnp . array ( Y_batch * 2.0 - 1.0 )
238+ target = Y_batch * 2.0 - 1.0
225239 pred = model (weights , bias , X_batch )
226- return pnp . sum (w_batch * (target - pred ) ** 2 ) / (pnp .sum (w_batch ) + 1e-12 )
240+ return (w_batch * (target - pred ) ** 2 ). sum () / (w_batch .sum () + 1e-12 )
227241
228242 n_train = len (y_train )
229- rng = np .random .default_rng (seed )
230- weights_init = 0.01 * pnp .random .randn (
231- num_layers , NUM_QUBITS , 3 , requires_grad = True
243+
244+ torch .manual_seed (seed )
245+ weights = torch .nn .Parameter (
246+ 0.01 * torch .randn (num_layers , NUM_QUBITS , 3 , device = device , dtype = dtype )
247+ )
248+ bias = torch .nn .Parameter (torch .tensor (0.0 , device = device , dtype = dtype ))
249+ opt = torch .optim .Adam ([weights , bias ], lr = lr )
250+
251+ X_train_t = torch .tensor (X_train , dtype = dtype , device = device )
252+ # Float so autograd does not try to differentiate ints
253+ Y_train_t = torch .tensor (
254+ np .asarray (y_train , dtype = np .float64 ), dtype = dtype , device = device
232255 )
233- bias_init = pnp .array (0.0 , requires_grad = True )
234- opt = AdamOptimizer (lr )
256+ W_train_t = torch .tensor (sample_weights , dtype = dtype , device = device )
235257
236- X_train_pnp = pnp .array (X_train , requires_grad = False )
237- # Float so PennyLane autograd does not try to differentiate ints (align with QDP pipeline)
238- Y_train_pnp = pnp .array (np .asarray (y_train , dtype = np .float64 ), requires_grad = False )
239- W_train_pnp = pnp .array (sample_weights , requires_grad = False )
258+ X_test_t = torch .tensor (X_test , dtype = dtype , device = device )
240259
241260 # Compile (first forward + cost)
242261 t0 = time .perf_counter ()
243- _ = circuit (weights_init , X_train_pnp [0 ])
244- _ = cost (weights_init , bias_init , X_train_pnp [:1 ], Y_train_pnp [:1 ], W_train_pnp [:1 ])
262+ _ = circuit (weights , X_train_t [0 ])
263+ _ = cost (weights , bias , X_train_t [:1 ], Y_train_t [:1 ], W_train_t [:1 ])
245264 compile_sec = time .perf_counter () - t0
246265
247266 # Train
267+ _batch_n = min (batch_size , n_train )
248268 t0 = time .perf_counter ()
249- weights , bias = weights_init , bias_init
250- for step in range (iterations ):
251- idx = rng .integers (0 , n_train , size = (min (batch_size , n_train ),))
252- Xb = X_train_pnp [idx ]
253- Yb = Y_train_pnp [idx ]
254- Wb = W_train_pnp [idx ]
255- out = opt .step (cost , weights , bias , Xb , Yb , Wb )
256- weights , bias = out [0 ], out [1 ]
269+ for _ in range (iterations ):
270+ opt .zero_grad ()
271+ idx = torch .randint (0 , n_train , (_batch_n ,), device = device )
272+ Xb = X_train_t [idx ]
273+ Yb = Y_train_t [idx ]
274+ Wb = W_train_t [idx ]
275+ loss = cost (weights , bias , Xb , Yb , Wb )
276+ loss .backward ()
277+ opt .step ()
257278 train_sec = time .perf_counter () - t0
258279
259280 # Test-set predictions and scores (for AUPRC we need continuous scores)
260- X_test_pnp = pnp . array ( X_test , requires_grad = False )
261- pred_scores = np . array ( model (weights , bias , X_test_pnp ) ).flatten ()
281+ with torch . no_grad ():
282+ pred_scores = model (weights , bias , X_test_t ). cpu (). numpy ( ).flatten ()
262283 pred_binary = (np .sign (pred_scores ) > 0 ).astype (np .int32 )
263284 # Map expval in [-1,1] to positive-class score in [0,1] for AUPRC
264285 scores_positive = (pred_scores + 1.0 ) / 2.0
@@ -272,7 +293,7 @@ def cost(
272293 return {
273294 "compile_time_sec" : compile_sec ,
274295 "train_time_sec" : train_sec ,
275- "samples_per_sec" : (iterations * min ( batch_size , n_train ) ) / train_sec
296+ "samples_per_sec" : (iterations * _batch_n ) / train_sec
276297 if train_sec > 0
277298 else 0.0 ,
278299 "auprc" : auprc ,
@@ -287,7 +308,7 @@ def cost(
287308
288309def main () -> None :
289310 parser = argparse .ArgumentParser (
290- description = "PennyLane Credit Card Fraud baseline (amplitude, 5 qubits, AUPRC/F1)"
311+ description = "PennyLane Credit Card Fraud baseline (amplitude, 5 qubits, AUPRC/F1, GPU training )"
291312 )
292313 parser .add_argument (
293314 "--data-file" ,
@@ -353,7 +374,7 @@ def main() -> None:
353374 val_size = 0.1 ,
354375 )
355376
356- print ("Credit Card Fraud amplitude baseline (PennyLane)" )
377+ print ("Credit Card Fraud amplitude baseline (PennyLane, GPU )" )
357378 print (
358379 f" Data: { data_src } → StandardScaler → PCA({ args .pca_dim } ) → pad to { FEATURE_DIM } → L2 norm"
359380 )
0 commit comments