pqcprep.training_tools
Collection of useful functions for network training purposes.
1""" 2Collection of useful functions for network training purposes. 3""" 4 5import numpy as np 6from qiskit_machine_learning.neural_networks import SamplerQNN 7from qiskit_machine_learning.connectors import TorchConnector 8from qiskit_algorithms.utils import algorithm_globals 9from qiskit.primitives import Sampler 10from torch.optim import Adam 11from torch import Tensor, no_grad 12import sys, time, os, warnings, torch 13 14from .binary_tools import bin_to_dec, dec_to_bin 15from .phase_tools import full_encode, phase_from_state 16from .pqc_tools import generate_network, binary_to_encode_param, A_generate_network, get_state_vec 17from .file_tools import compress_args,compress_args_ampl, vars_to_name_str, vars_to_name_str_ampl 18from .psi_tools import psi, A 19 20#--------------------------------------------------------------------------------------------------- 21 22def set_loss_func(loss_str, arg_dict, ampl=False): 23 r""" 24 Set the loss function to be used in training a network. 25 26 Arguments: 27 ---- 28 29 - **loss_str** : *str* 30 31 String specifying the loss function to use. Options are 32 33 - `'MSE'` : mean squared error loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html) with default settings. 34 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 35 36 - `'L1'` : mean absolute error loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html) with default settings. 37 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 38 39 - `'KLD'` : Kullback-Leibler divergence loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html) with default settings. 40 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 41 42 - `'CE'` : cross entropy loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html) with default settings. 43 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 44 45 - `'SAM'` : sign-adjusted mismatch. Defined as $$\text{SAM}(\ket{x}, \ket{y}) = 1 - \sum_k |x_k| |y_k|,$$ where $\ket{x}$, $\ket{y}$ are the 46 network output and desired output, respectively, and $x_k$, $y_k$ are the coefficients w.r.t the two-register computational basis states. `criterion` takes 47 two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 48 49 - `'WIM'` : weighted mismatch. Defined analogously to SAM but with additional weights: $$\text{WIM}(\ket{x}, \ket{y}) =1 - \sum_k w_k |x_k||y_k|.$$ 50 `criterion` takes three pytorch `Tensors` as inputs, corresponding to the network output, the desired output and the weights. See `set_WIM_weights()` 51 for information on how the weights are calculated. This loss function is not an option if `ampl` is True. 52 53 - `'WILL'` : weighted Lp loss. Defined as $$\text{WILL}(\ket{x}, \ket{y}; p,q) = \sum_k |x_k -y_k|^p + |x_k| |[k]_m - \Psi([k]_n)|^q,$$ where $p$ and $q$ are coefficients stored in 54 `arg_dict['WILL_p']`, `arg_dict['WILL_q']`, $[k]_n$ is the target register bit-string associated with the basis state $\ket{k}$, $[k]_n$ is the input register bit-string 55 associated with the basis state $\ket{k}$, $\Psi$ is the function to be evaluated for the network, and $x_k$, $y_k$ have the same meaning as above. `criterion` takes 56 two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. This loss function is not an option if `ampl` is True. 57 58 - **arg_dict** : *dict* 59 60 A dictionary containing information on training variables, created with `pqcprep.file_tools.compress_args()` (or created 61 with `pqcprep.file_tools.compress_args_ampl()` in the case of `ampl` being True). 62 63 - **ampl** : *boolean* 64 65 If True, the loss function is defined for an amplitude-encoding network, as opposed to a function evaluation network. Default is 66 False. 67 68 Returns: 69 ---- 70 71 - **criterion** : *callable* 72 73 The loss function as a callable object. Number and type of arguments depend on the chosen `loss_str` option (see above). 74 75 """ 76 77 if loss_str=="MSE": 78 from torch.nn import MSELoss 79 criterion=MSELoss() 80 elif loss_str=="L1": 81 from torch.nn import L1Loss 82 criterion=L1Loss() 83 elif loss_str=="KLD": 84 from torch.nn import KLDivLoss 85 criterion=KLDivLoss() 86 elif loss_str=="CE": 87 from torch.nn import CrossEntropyLoss 88 criterion=CrossEntropyLoss() 89 elif loss_str=="SAM": 90 def criterion(output, target): 91 return torch.abs(1. -torch.sum(torch.mul(output, target))) 92 elif loss_str=="WIM": 93 if arg_dict["train_superpos"]==False: 94 raise ValueError(f"The loss function {loss_str} requires training in superposition, i.e. 'train_superpos==True'.") 95 if ampl: 96 raise ValueError(f"The loss function {loss_str} is not available for amplitude training. Allowed options are 'CE', 'MSE', 'L1', 'KLD', 'SAM'.") 97 98 def criterion(output, target, weights): 99 output = torch.mul(output, weights) 100 output = output / torch.sum(torch.mul(output, output)) 101 return torch.abs(1. -torch.sum(torch.mul(output, target))) 102 elif loss_str=="WILL": 103 if arg_dict["train_superpos"]==False: 104 raise ValueError(f"The loss function {loss_str} requires training in superposition, i.e. 'train_superpos==True'.") 105 if ampl: 106 raise ValueError(f"The loss function {loss_str} is not available for amplitude training. Allowed options are 'CE', 'MSE', 'L1', 'KLD', 'SAM'.") 107 fx_arr = [psi(i, mode=arg_dict["func_str"]) for i in np.arange(0, 2**arg_dict["n"])] 108 if arg_dict["phase_reduce"]: 109 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 110 111 fx_arr_rounded = [bin_to_dec(dec_to_bin(i,arg_dict["m"],'unsigned mag', nint=arg_dict["mint"]),'unsigned mag',nint=arg_dict["mint"]) for i in fx_arr] 112 distance_arr = np.empty(2**(arg_dict["n"]+arg_dict["m"])) 113 114 for i in np.arange(2**arg_dict["n"]): 115 bin_i=dec_to_bin(i,arg_dict["n"],'unsigned mag') 116 for j in np.arange(2**arg_dict["m"]): 117 bin_j=dec_to_bin(j,arg_dict["m"],'unsigned mag') 118 ind=int(bin_j + bin_i,2) 119 distance_arr[ind] = np.abs(bin_to_dec(dec_to_bin(j,arg_dict["m"],'unsigned mag'),'unsigned mag',nint=arg_dict["mint"]) - fx_arr_rounded[i]) 120 distance=Tensor(distance_arr) 121 122 def criterion(output, target): 123 loss =torch.pow(torch.abs(output-target),arg_dict["WILL_p"]) + torch.mul(torch.abs(output),torch.pow(distance,arg_dict["WILL_q"])) 124 return torch.sum(loss)**(1/arg_dict["WILL_p"]) / torch.numel(loss) 125 else: 126 raise ValueError("Unrecognised loss function. Options are: 'CE', 'MSE', 'L1', 'KLD', 'SAM', 'WIM', 'WILL'.") 127 128 return criterion 129 130def set_WIM_weights(generated_weights, arg_dict): 131 """ 132 133 Determine the weight coefficients for the WIM loss function. 134 135 Arguments: 136 ---- 137 - **generated_weights** : *array_like* 138 139 The network weights generated in a given epoch. 140 141 - **arg_dict** : *dict* 142 143 A dictionary containing information on training variables, created with `pqcprep.file_tools.compress_args()`. 144 145 Returns: 146 ---- 147 - **WIM_weights_arr** : *array_like* 148 149 The calculated coefficients for the WIM loss function. 150 151 """ 152 153 # initialise arrays to store results 154 WIM_weights_arr= np.empty(2**(arg_dict["n"]+arg_dict["m"])) 155 156 # iterate over input states 157 x_arr_temp=np.arange(2**arg_dict["n"]) 158 fx_arr_temp = [psi(k, mode=arg_dict["func_str"]) for k in x_arr_temp] 159 160 if arg_dict["phase_reduce"]: fx_arr_temp = [np.modf(k/ (2* np.pi))[0] for k in fx_arr_temp] 161 162 for q in x_arr_temp: 163 164 # prepare circuit 165 enc=binary_to_encode_param(np.binary_repr(q,arg_dict["n"])) 166 params=np.concatenate((enc, generated_weights)) 167 168 qc = generate_network(arg_dict["n"],arg_dict["m"],arg_dict["L"], encode=True,toggle_IL=True, real=arg_dict["real"],repeat_params=arg_dict["repeat_params"]) 169 qc = qc.assign_parameters(params) 170 171 # get target array 172 target_arr_temp = np.zeros(2**(arg_dict["n"]+arg_dict["m"])) 173 174 index = int(dec_to_bin(fx_arr_temp[q],arg_dict["m"],'unsigned mag',nint=arg_dict["mint"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 175 target_arr_temp[index]=1 176 177 # get statevector 178 state_vector_temp = get_state_vec(qc) 179 180 # for each output state, calculate the "binary difference" to the target as well as the "coefficient difference" 181 sum = 0 182 for j in np.arange(2**arg_dict["m"]): 183 ind = int(dec_to_bin(j,arg_dict["m"],'unsigned mag',nint=arg_dict["m"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 184 185 num_dif =np.abs(fx_arr_temp[q] -bin_to_dec(dec_to_bin(j,arg_dict["m"],'unsigned mag', nint=arg_dict["mint"]),'unsigned mag',nint=arg_dict["mint"])) 186 coeff_dif = np.abs(state_vector_temp[ind]- target_arr_temp[ind]) 187 sum += num_dif * coeff_dif 188 189 # add to weights_arr 190 for j in np.arange(2**arg_dict["m"]): 191 ind = int(dec_to_bin(j,arg_dict["m"],'unsigned mag',nint=arg_dict["m"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 192 WIM_weights_arr[ind]= sum 193 194 # focus on outliers: double-weight on states 0.5 sigma or more above the mean 195 WIM_weights_arr += (WIM_weights_arr >= np.mean(WIM_weights_arr)+0.5 *np.std(WIM_weights_arr) ).astype(int) 196 197 # smoothen WIM weights 198 WIM_weights_arr=np.exp(0.8 * WIM_weights_arr) 199 200 return WIM_weights_arr 201 202def train_QNN(n,m,L, seed, epochs,func_str,loss_str,meta, recover_temp, nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta, DIR): 203 r""" 204 Train a QCNN to perform function evaluation $\ket{j}\ket{0} \mapsto \ket{j}\ket{\Psi(j)}$. 205 206 The QCNN is generated using `pqcprep.pqc_tools.generate_network()`. 207 208 Arguments: 209 --- 210 - **n** : *int* 211 212 Number of qubits in the input register. 213 214 - **m** : *int* 215 216 Number of qubits in the target register. 217 218 - **L** : *int* 219 220 Number of layers in the network. 221 222 - **seed** : *int* 223 224 Seed for random number generation. 225 226 - **epochs** : *int* 227 228 Number of training runs. 229 230 - **func_str** : *str* 231 232 String specifying the function $\Psi$ to be evaluated. Must be a valid option for the argument `mode` of `pqcprep.psi_tools.psi()`. 233 234 - **loss_str** : *str* 235 236 String specifying the loss function minimised by the optimiser. Must be a valid option for the argument `loss_str` of `set_loss_func()`. 237 238 - **meta** : *str* 239 240 String containing meta information to be included in output file names. 241 242 - **recover_temp** : *boolean* 243 244 If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten. 245 246 - **nint** : *int* 247 248 Number of integer qubits in input register. 249 250 - **mint** : *int* 251 252 Number of integer qubits in target register. 253 254 - **phase_reduce** : *boolean* 255 256 If True, reduce $\Psi(j)$ to the interval $[0, 2 \pi)$ i.e. perform the mapping $\Psi \to \Psi (\text{mod} \; 2 \pi)$. 257 258 - **train_superpos** : *boolean* 259 260 If True, train on a superposition of input states. If False, train on randomly sampled individual input states. 261 262 - **real** : *boolean* 263 264 If True, generate a network only involving CX and Ry rotations, resulting in real amplitudes. 265 266 - **repeat_params** : *str*, *optional* 267 268 Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 269 Options are `None` (do not keep parameters fixed), `'CL'` (keep parameters fixed for convolutional layers), 270 `'IL'` (keep parameters fixed for input layers), `'both'` (keep parameters fixed for both convolutional and input layers). 271 272 - **WILL_p** : *float* 273 274 The $p$ parameter of the WILL loss function, as described in `set_loss_func()`. 275 276 - **WILL_q** : *float* 277 278 The $q$ parameter of the WILL loss function, as described in `set_loss_func()`. 279 280 - **delta** : *float* 281 282 Hyper-parameter controlling the sampling of input state coefficients when training in superposition (`train_superpos = True`). Must be 283 between 0 and 1. `delta = 0` corresponds to coefficients fixed at $\\frac{1}{\\sqrt{2^n}}$ while `delta = 1` corresponds to coefficients randomly assuming values on the range $(0,1)$. 284 Intermediate values of `delta` result in coefficinets being randomly sampled on an interval around $\\frac{1}{\\sqrt{2^n}}$, with the range of the interval 285 determined by the value of `delta`. 286 287 - **DIR** : *str* 288 289 Parent directory for output files. 290 291 Returns: 292 ---- 293 294 The output produced by the training is saved in binary `.npy` files in the directory `DIR/outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 295 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()` and `<TYPE>` is one of: 296 297 - `weights` : file containing the QCNN weights determined by the optimiser; 298 299 - `loss` : file containing the loss value after each epoch; 300 301 - `mismatch` : file containg the mismatch value after each epoch; 302 303 - `grad` : file containing the squared weight gradient norm after each epoch; 304 305 - `vargrad` : file containing the variance of the weight gradients after each epoch. 306 307 """ 308 309 # compress arguments into dictionary 310 args =compress_args(n,m,L, seed, epochs,func_str,loss_str,meta,nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta) 311 312 # set precision strings 313 if nint==None: nint=n 314 if mint==None: mint=m 315 if phase_reduce: mint=0 316 317 # set seed for PRNG 318 algorithm_globals.random_seed= seed 319 rng = np.random.default_rng(seed=seed) 320 321 # generate circuit and set up as QNN 322 qc = generate_network(n,m,L, encode=not train_superpos, toggle_IL=True, initial_IL=True,input_Ry=train_superpos, real=real,repeat_params=repeat_params, wrap=False) 323 324 qnn = SamplerQNN( 325 circuit=qc.decompose(), 326 sampler=Sampler(options={"shots": 10000, "seed": algorithm_globals.random_seed}), 327 input_params=qc.parameters[:n], 328 weight_params=qc.parameters[n:], 329 input_gradients=True 330 ) 331 332 # choose initial weights 333 recovered_k =0 334 if recover_temp: 335 recover_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 336 recover_paths={} 337 for k in np.arange(100,epochs, step=100): 338 for e in np.arange(len(recover_labels)): 339 file=os.path.join(DIR,"outputs", f"__TEMP{k}_{recover_labels[e]}{vars_to_name_str(args)}.npy") 340 recover_paths[recover_labels[e]]= (file if os.path.isfile(file) else None) 341 342 if recover_labels[e]=="weights" and os.path.isfile(file): 343 recovered_k=k+1 344 345 if not None in list(recover_paths.values): 346 initial_weights=np.load(recover_paths["weights"]) 347 else: 348 initial_weights =np.zeros(len(qc.parameters[n:])) # initialise parameters to zero 349 else: 350 initial_weights =np.zeros(len(qc.parameters[n:])) # initialise parameters to zero 351 352 # initialise TorchConnector 353 model = TorchConnector(qnn, initial_weights) 354 355 # choose optimiser 356 optimizer = Adam(model.parameters(), lr=0.01, betas=(0.7, 0.999), weight_decay=0.005) 357 358 # set up arrays to store training outputs 359 if recover_temp and not None in list(recover_paths.values): 360 mismatch_vals=np.load(recover_paths["mismatch"]) 361 loss_vals=np.load(recover_paths["loss"]) 362 grad_vals=np.load(recover_paths["grad"]) 363 var_grad_vals=np.load(recover_paths["var_grad"]) 364 else: 365 mismatch_vals = np.empty(epochs) 366 loss_vals = np.empty(epochs) 367 grad_vals = np.empty(epochs) 368 var_grad_vals = np.empty(epochs) 369 370 # generate x and f(x) values 371 pn =n - nint 372 pm =m - mint 373 374 if train_superpos: 375 376 # sample all basis states of input register and convert to binary 377 x_arr = np.arange(0, 2**n) 378 x_arr_bin =[dec_to_bin(i,n,encoding="unsigned mag") for i in x_arr] 379 380 # apply function and reduce to phase value between 0 and 1 381 fx_arr = [psi(i, mode=func_str) for i in x_arr] 382 383 if phase_reduce: 384 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 385 386 # convert fx_arr to binary at available target register precision 387 fx_arr_bin = [dec_to_bin(i,m,nint=mint,encoding="unsigned mag") for i in fx_arr] 388 389 if np.max(fx_arr)> 2.**mint - 2.**(-pm) and mint != 0: 390 raise ValueError(f"Insufficient number of target (integer) qubits.") 391 392 # get bit strings corresponding to target arrays and convert to indices 393 target_bin = [fx_arr_bin[i]+x_arr_bin[i] for i in x_arr] 394 target_ind = [bin_to_dec(i, encoding='unsigned mag') for i in target_bin] 395 396 # prepare target array 397 target_arr = np.zeros(2**(n+m)) 398 for k in target_ind: 399 target_arr[int(k)]=1 400 401 else: 402 x_min = 0 403 x_max = 2.**nint - 2.**(-pn) 404 x_arr = np.array(x_min + (x_max - x_min) *rng.random(size=epochs)) 405 fx_arr = [psi(i, mode=func_str) for i in x_arr] 406 407 # reduce to phase value between 0 and 1: 408 if phase_reduce: 409 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 410 411 if np.max(fx_arr)> 2.**mint - 2.**(-pm) and mint != 0: 412 raise ValueError(f"Insufficient number of target (integer) qubits.") 413 414 # choose loss function 415 criterion=set_loss_func(loss_str, args) 416 417 # start training 418 print(f"\n\nTraining started. Epochs: {epochs}. Input qubits: {n}. Target qubits: {m}. QCNN layers: {L}. \n") 419 start = time.time() 420 421 warnings.filterwarnings("ignore", category=UserWarning) 422 423 for i in np.arange(epochs)[recovered_k:]: 424 425 if train_superpos == False: 426 # get input data 427 input = Tensor(binary_to_encode_param(dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint))) 428 429 # get target data 430 target_arr = np.zeros(2**(n+m)) 431 index = int(dec_to_bin(fx_arr[i],m,'unsigned mag',nint=mint)+dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint),2) 432 target_arr[index]=1 433 target=Tensor(target_arr) 434 else: 435 436 # generate random coefficients 437 coeffs = np.array(np.pi / 2 * (1+delta *(2 *rng.random(size=n)-1))) 438 439 # get input data 440 input=Tensor(coeffs) 441 442 # get target data 443 444 target_ampl = np.empty(2**(n+m)) 445 446 for j in np.arange(2**n): 447 for k in np.arange(2**m): 448 ind =int(dec_to_bin(k,m)+dec_to_bin(j,n),2) 449 bin_n = dec_to_bin(j,n)[:: -1] # reverse bit order ! 450 val=1 451 452 for l in np.arange(n): 453 val *= np.cos(coeffs[l]/2) if bin_n[l]=='0' else np.sin(coeffs[l]/2) 454 455 target_ampl[ind] = val * target_arr[ind] 456 457 target=Tensor(target_ampl**2) 458 459 # train model 460 optimizer.zero_grad() 461 462 # apply loss function 463 if loss_str=="WIM": 464 WIM_weights_arr=np.ones(2**(n+m)) if i==recovered_k else WIM_weights_arr 465 WIM_weights_tensor=Tensor(WIM_weights_arr) 466 loss =criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target), WIM_weights_tensor) # add small number in sqrt to avoid zero grad ! 467 else: 468 loss = criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target)) # add small number in sqrt to avoid zero grad ! 469 470 # propagate gradients and recompute weights 471 loss.backward() 472 optimizer.step() 473 474 # save loss and grad for plotting 475 loss_vals[i]=loss.item() 476 grad_vals[i]=np.sum(model.weight.grad.numpy()**2) 477 var_grad_vals[i]=np.std(model.weight.grad.numpy())**2 478 479 # set up circuit with calculated weights 480 circ = generate_network(n,m,L, encode=not train_superpos, toggle_IL=True, initial_IL=True,input_Ry=train_superpos, real=real,repeat_params=repeat_params, wrap=False) 481 482 with no_grad(): 483 generated_weights = model.weight.detach().numpy() 484 if train_superpos: 485 input_params = coeffs 486 params=np.concatenate((input_params, generated_weights)) 487 else: 488 input_params = binary_to_encode_param(dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint)) 489 params = np.concatenate((input_params, generated_weights)) 490 circ = circ.assign_parameters(params) 491 492 # get statevector 493 state_vector = get_state_vec(circ) 494 495 # calculate fidelity and mismatch 496 target_state = target_ampl**2 if train_superpos else target_arr 497 fidelity = np.abs(np.dot(np.sqrt(target_state),np.conjugate(state_vector)))**2 498 mismatch = 1. - np.sqrt(fidelity) 499 mismatch_vals[i]=mismatch 500 501 # set loss func weights for WIM 502 if loss_str=="WIM" and (i % 10 ==0) and (i >=1): WIM_weights_arr= set_WIM_weights(generated_weights, args) 503 504 # temporarily save outputs every hundred iterations 505 temp_ind = epochs - 100 506 507 if recover_temp: 508 temp_ind = recovered_k -1 509 510 if (i % 100 ==0) and (i != 0) and (i != epochs-1): 511 512 temp_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 513 temp_arrs=[generated_weights, mismatch_vals, loss_vals, grad_vals, var_grad_vals] 514 515 for e in np.arange(len(temp_labels)): 516 # save temp file 517 file=os.path.join(DIR,"outputs",f"__TEMP{i}_{temp_labels[e]}{vars_to_name_str(args)}") 518 np.save(file,temp_arrs[e]) 519 520 # delete previous temp file 521 old_file=os.path.join(DIR,"outputs",f"__TEMP{i-100}_{temp_labels[e]}{vars_to_name_str(args)}.npy") 522 os.remove(old_file) if os.path.isfile(old_file) else None 523 524 # make note of last created temp files 525 temp_ind = i 526 527 # print status 528 a = int(20*(i+1)/epochs) 529 530 if i==recovered_k: 531 time_str="--:--:--.--" 532 elif i==epochs-1: 533 time_str="00:00:00.00" 534 else: 535 if recover_temp: 536 remaining = ((time.time() - start) / (i-recovered_k)) * (epochs - i) 537 else: 538 remaining = ((time.time() - start) / i) * (epochs - i) 539 mins, sec = divmod(remaining, 60) 540 hours, mins = divmod(mins, 60) 541 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 542 543 prefix="\t" 544 print(f"{prefix}[{u'█'*a}{('.'*(20-a))}] {100.*((i+1)/epochs):.2f}% ; Loss {loss_vals[i]:.2e} ; Mismatch {mismatch:.2e} ; ETA {time_str}", end='\r', file=sys.stdout, flush=True) 545 546 547 print(" ", flush=True, file=sys.stdout) 548 549 warnings.filterwarnings("default", category=UserWarning) 550 551 elapsed = time.time()-start 552 mins, sec = divmod(elapsed, 60) 553 hours, mins = divmod(mins, 60) 554 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 555 556 # decompose circuit for gate count 557 num_CX = dict(circ.decompose(reps=4).count_ops())["cx"] 558 num_gates = num_CX + dict(circ.decompose(reps=4).count_ops())["u"] 559 print(f"\nTraining completed in {time_str}. Number of weights: {len(generated_weights)}. Number of gates: {num_gates} (of which CX gates: {num_CX}). \n\n") 560 561 # delete temp files 562 temp_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 563 for i in np.arange(len(temp_labels)): 564 file=os.path.join(DIR,"outputs",f"__TEMP{temp_ind}_{temp_labels[i]}{vars_to_name_str(args)}.npy") 565 os.remove(file) if os.path.isfile(file) else None 566 567 # save outputs 568 with no_grad(): 569 generated_weights = model.weight.detach().numpy() 570 outputs= [generated_weights, mismatch_vals, loss_vals, grad_vals, var_grad_vals] 571 output_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 572 for i in np.arange(len(outputs)): 573 np.save(os.path.join(DIR,"outputs", f"{output_labels[i]}{vars_to_name_str(args)}"), outputs[i]) 574 575 return 0 576 577def test_QNN(n,m,L,seed,epochs, func_str,loss_str,meta,nint,mint,phase_reduce,train_superpos,real,repeat_params,WILL_p, WILL_q,delta,DIR,verbose=True): 578 """ 579 Test performance of a QCNN trained for function evaluation with respect to different metrics. 580 581 This requires the existence of an appropriate `weights_<NAME_STR>.npy` file (as produced by `train_QNN()`) in the directory `DIR/outputs`, where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()`. 582 583 Arguments: 584 --- 585 586 Same arguments as `train_QNN()`. See there for a description. 587 588 Returns: 589 --- 590 591 The testing output produced is saved in binary `.npy` files in the directory `DIR/ampl_outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 592 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()` and `<TYPE>` is one of: 593 594 - `mismatch_by_state` : file containing the mismatch between the state produced by the network and the desired output state for each of the possible input 595 register states. Contains a dictionary with the input bit strings as keys and the associated mismatch as values. 596 597 - `phase` : file containing the phase function encoded by the network when the input register is in an equal superposition of input states. 598 599 - `metrics` : file containing a dictionary with several metrics evaluating the performance of the network. These metrics are 600 * `mu` : mean mismatch (mean of the data contained in `mismatch_by_state_<NAME_STR>.npy`); should be zero for ideal performance 601 * `sigma` : mismatch standard deviation (standard deviation of the data contained in `mismatch_by_state_<NAME_STR>.npy`); should be zero for ideal performance 602 * `eps` : normalisation error on the state vector associated with the data contained in `phase_<NAME_STR>.npy`; should be zero for ideal performance 603 * `chi` : mean absolute difference between the phase function contained in `phase_<NAME_STR>.npy` and the rounded desired phase function; should be zero for ideal performance 604 * `omega` : a super-metric defined as `1/(mu + sigma + eps + chi)` ; should be maximal for ideal performance 605 606 """ 607 # compress arguments into dictionary 608 args =compress_args(n,m,L, seed, epochs,func_str,loss_str,meta,nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta) 609 name_str=vars_to_name_str(args) 610 611 # set precision strings 612 if nint==None: nint=n 613 if mint==None: mint=m 614 if phase_reduce: mint=0 615 616 # load weights 617 if os.path.isfile(os.path.join(DIR,"outputs",f"weights{name_str}.npy")): 618 weights = np.load(os.path.join(DIR,"outputs",f"weights{name_str}.npy")) 619 else: 620 raise ValueError("No appropriate QCNN weights could be found. Check the network configuration as well as the relevant directory.") 621 622 # initialise array to store results 623 mismatch = np.empty(2**n) 624 signs = np.empty(2**n) 625 626 # iterate over input states 627 x_arr = np.arange(2**n) 628 fx_arr = [psi(i, mode=func_str) for i in x_arr] 629 630 if phase_reduce: 631 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 632 633 for i in x_arr: 634 635 # prepare circuit 636 enc=binary_to_encode_param(np.binary_repr(i,n)) 637 params=np.concatenate((enc, weights)) 638 639 circ = generate_network(n,m,L, encode=True,toggle_IL=True, real=real,repeat_params=repeat_params) 640 circ = circ.assign_parameters(params) 641 642 # get target array 643 target_arr = np.zeros(2**(n+m)) 644 645 index = int(dec_to_bin(fx_arr[i],m,'unsigned mag',nint=mint)+dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint),2) 646 target_arr[index]=1 647 648 # get statevector 649 state_vector = get_state_vec(circ) 650 651 signs[i]=np.sign(np.sum(np.real(state_vector)*np.sqrt(target_arr))) 652 653 # calculate fidelity and mismatch 654 fidelity = np.abs(np.dot(np.sqrt(target_arr),np.conjugate(state_vector)))**2 655 mismatch[i] = 1. - np.sqrt(fidelity) 656 657 # get phase target 658 fx_arr_bin = [dec_to_bin(i,m, "unsigned mag", 0) for i in fx_arr] 659 phase_target = np.array([bin_to_dec(i,"unsigned mag", mint) for i in fx_arr_bin]) 660 if phase_reduce: 661 phase_target *= 2 * np.pi 662 663 # get state vector for full phase extraction 664 state_vec= full_encode(n,m, weights_A_str=None, weights_p_str=weights, L_A=None, L_p=L,real_p=real,repeat_params=repeat_params,full_state_vec=False, no_UA=True, operators="QRQ") 665 phase = phase_from_state(state_vec) 666 667 # calculate metrics 668 mu = np.mean(mismatch) 669 sigma = np.std(mismatch) 670 eps = 1 - np.sum(np.abs(state_vec)**2) 671 chi = np.mean(np.abs(phase - phase_target)) 672 omega= 1/(mu+sigma+eps+chi) 673 674 # save outputs 675 full_dic = dict(zip(x_arr, mismatch)) 676 np.save(os.path.join(DIR,"outputs",f"mismatch_by_state{name_str}.npy"), full_dic) 677 678 metric_dic = {} 679 metric_dic["mu"]=mu 680 metric_dic["sigma"]=sigma 681 metric_dic["eps"]=eps 682 metric_dic["chi"]=chi 683 metric_dic["omega"]=omega 684 np.save(os.path.join(DIR,"outputs",f"metrics{name_str}.npy"), metric_dic) 685 686 np.save(os.path.join(DIR,"outputs",f"phase{name_str}.npy"), phase) 687 688 if verbose: 689 print("Mismatch by input state:") 690 for i in x_arr: 691 print(f"\t{np.binary_repr(i,n)}: {mismatch[i]:.2e} ({signs[i]})") 692 print("-----------------------------------") 693 print(f"Mu: \t{mu:.3e}") 694 print(f"Sigma: \t{sigma:.3e}") 695 print(f"Eps: \t{eps:.3e}") 696 print(f"Chi: \t{chi:.3e}") 697 print(f"Omega: \t{omega:.3f}") 698 print("-----------------------------------") 699 print("") 700 print("") 701 702 return 0 703 704def ampl_train_QNN(n,L,x_min,x_max,seed, epochs,func_str,loss_str,meta, recover_temp, nint, repeat_params, DIR): 705 """ 706 707 Train a QCNN to prepare an amplitude distribution: $\ket{0} \mapsto \sum_j A(j) \ket{j}$. 708 709 The QCNN is generated using `pqcprep.pqc_tools.A_generate_network()`. 710 711 Arguments: 712 --- 713 714 - **n** : *int* 715 716 Number of qubits in the register. 717 718 - **L** : *int* 719 720 Number of layers in the network. 721 722 - **x_min** : *float* 723 724 Minimum of function domain. 725 726 - **x_max** : *float* 727 728 Maximum of function domain. 729 730 - **seed** : *int* 731 732 Seed for random number generation. 733 734 - **epochs** : *int* 735 736 Number of training runs. 737 738 - **func_str** : *str* 739 740 String specifying the function $A$ to be prepared. Must be a valid option for the argument `mode` of `pqcprep.psi_tools.A()`. 741 742 - **loss_str** : *str* 743 744 String specifying the loss function minimised by the optimiser. Must be a valid option for the argument `loss_str` of `set_loss_func()`. 745 746 - **meta** : *str* 747 748 String containing meta information to be included in output file names. 749 750 - **recover_temp** : *boolean* 751 752 If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten. 753 754 - **nint** : *int* 755 756 Number of integer qubits in the register. 757 758 - **repeat_params** : *boolean* 759 760 If True, keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 761 762 - **DIR** : *str* 763 764 Parent directory for output files. 765 766 767 Returns: 768 --- 769 770 The output produced by the training is saved in binary `.npy` files in the directory `DIR/ampl_outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 771 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str_ampl()` and `<TYPE>` is one of: 772 773 - `weights` : file containing the QCNN weights determined by the optimiser; 774 775 - `state_vec` : file containing the statevector corresponding to the register after applying the QCNN; 776 777 - `loss` : file containing the loss value after each training run; 778 779 - `mismatch` : file containg the mismatch value after each training run. 780 781 782 """ 783 784 # compress arguments into dictionary 785 args=compress_args_ampl(n,L,x_min,x_max,seed, epochs,func_str,loss_str,meta, nint, repeat_params) 786 787 # set seed for PRNG 788 algorithm_globals.random_seed= seed 789 rng = np.random.default_rng(seed=seed) 790 791 # generate circuit and set up as QNN 792 qc = A_generate_network(n,L,repeat_params) 793 qnn = SamplerQNN( 794 circuit=qc.decompose(), # decompose to avoid data copying (?) 795 sampler=Sampler(options={"shots": 10000, "seed": algorithm_globals.random_seed}), 796 weight_params=qc.parameters, 797 input_params=[], 798 input_gradients=False 799 ) 800 801 # set precision 802 if nint==None: nint=n 803 804 # choose initial weights 805 recovered_k =0 806 807 if recover_temp: 808 recover_labels=["weights", "mismatch", "loss"] 809 recover_paths={} 810 811 for k in np.arange(100,epochs, step=100): 812 for e in np.arange(len(recover_labels)): 813 file=os.path.join(DIR,"ampl_outputs", f"__TEMP{k}_{recover_labels[e]}{vars_to_name_str_ampl(args)}.npy") 814 recover_paths[recover_labels[e]]= (file if os.path.isfile(file) else None) 815 816 if recover_labels[e]=="weights" and os.path.isfile(file): 817 recovered_k=k+1 818 819 if not None in list(recover_paths.values): 820 initial_weights=np.load(recover_paths["weights"]) 821 else: 822 initial_weights =np.zeros(len(qc.parameters)) # initialise parameters to zero 823 824 else: 825 initial_weights = np.zeros(len(qc.parameters)) # initialise parameters to zero 826 827 # initialise TorchConnector 828 model = TorchConnector(qnn, initial_weights) 829 830 # choose optimiser and loss function 831 optimizer = Adam(model.parameters(), lr=0.01, betas=(0.7, 0.999), weight_decay=0.005) # Adam optimizer 832 criterion=set_loss_func(loss_str, args, ampl=True) 833 834 # set up arrays to store training outputs 835 if recover_temp and not None in list(recover_paths.values): 836 mismatch_vals=np.load(recover_paths["mismatch"]) 837 loss_vals=np.load(recover_paths["loss"]) 838 else: 839 mismatch_vals = np.empty(epochs) 840 loss_vals = np.empty(epochs) 841 842 # calculate target and normalise 843 dx = (x_max-x_min)/(2**n) 844 target_arr = np.array([A(i, mode=func_str) for i in np.arange(x_min,x_max, dx)])**2 845 target_arr = target_arr / np.sum(target_arr) 846 847 # start training 848 print(f"\n\nTraining started. Epochs: {epochs}. Input qubits: {n}. Function range: [{x_min},{x_max}]. QCNN layers: {L}. \n") 849 start = time.time() 850 851 warnings.filterwarnings("ignore", category=UserWarning) 852 853 for i in np.arange(epochs)[recovered_k:]: 854 855 # get input data 856 input = Tensor([]) 857 858 # get target data 859 target=Tensor(target_arr) 860 861 # train model 862 optimizer.zero_grad() 863 loss = criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target)) # adding 1e-10 to prevent taking sqrt(0) ??!! 864 loss.backward() 865 optimizer.step() 866 867 # save loss for plotting 868 loss_vals[i]=loss.item() 869 870 # set up circuit with calculated weights 871 circ = A_generate_network(n,L, repeat_params) 872 873 with no_grad(): 874 generated_weights = model.weight.detach().numpy() 875 876 circ = circ.assign_parameters(generated_weights) 877 878 # get statevector 879 state_vector = get_state_vec(circ) 880 881 # calculate fidelity and mismatch 882 fidelity = np.abs(np.dot(np.sqrt(target_arr),np.conjugate(state_vector)))**2 883 mismatch = 1. - np.sqrt(fidelity) 884 885 # save mismatch for plotting 886 mismatch_vals[i]=mismatch 887 888 # temporarily save outputs every hundred iterations 889 temp_ind = epochs - 100 890 891 if recover_temp: 892 temp_ind = recovered_k -1 893 894 if (i % 100 ==0) and (i != 0) and (i != epochs-1): 895 896 temp_labels=["weights", "mismatch", "loss"] 897 temp_arrs=[generated_weights, mismatch_vals, loss_vals] 898 899 for e in np.arange(len(temp_labels)): 900 # save temp file 901 file=os.path.join(DIR,"ampl_outputs",f"__TEMP{i}_{temp_labels[e]}{vars_to_name_str_ampl(args)}") 902 np.save(file,temp_arrs[e]) 903 904 # delete previous temp file 905 old_file=os.path.join(DIR,"ampl_outputs",f"__TEMP{i-100}_{temp_labels[e]}{vars_to_name_str_ampl(args)}.npy") 906 os.remove(old_file) if os.path.isfile(old_file) else None 907 908 # make note of last created temp files 909 temp_ind = i 910 911 # print status 912 a = int(20*(i+1)/epochs) 913 914 if i==recovered_k: 915 time_str="--:--:--.--" 916 elif i==epochs-1: 917 time_str="00:00:00.00" 918 else: 919 if recover_temp: 920 remaining = ((time.time() - start) / (i-recovered_k)) * (epochs - i) 921 else: 922 remaining = ((time.time() - start) / i) * (epochs - i) 923 mins, sec = divmod(remaining, 60) 924 hours, mins = divmod(mins, 60) 925 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 926 927 prefix="\t" 928 print(f"{prefix}[{u'█'*a}{('.'*(20-a))}] {100.*((i+1)/epochs):.2f}% ; Loss {loss_vals[i]:.2e} ; Mismatch {mismatch:.2e} ; ETA {time_str}", end='\r', file=sys.stdout, flush=True) 929 930 warnings.filterwarnings("default", category=UserWarning) 931 932 print(" ", flush=True, file=sys.stdout) 933 934 elapsed = time.time()-start 935 mins, sec = divmod(elapsed, 60) 936 hours, mins = divmod(mins, 60) 937 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 938 939 # decompose circuit for gate count 940 num_CX = dict(circ.decompose(reps=4).count_ops())["cx"] 941 num_gates = num_CX + dict(circ.decompose(reps=4).count_ops())["u"] 942 943 print(f"\nTraining completed in {time_str}. Number of weights: {len(generated_weights)}. Number of gates: {num_gates} (of which CX gates: {num_CX}). \n\n") 944 945 # delete temp files 946 temp_labels=["weights", "mismatch", "loss"] 947 for i in np.arange(len(temp_labels)): 948 file=os.path.join(DIR,"ampl_outputs",f"__TEMP{temp_ind}_{temp_labels[i]}{vars_to_name_str_ampl(args)}.npy") 949 os.remove(file) if os.path.isfile(file) else None 950 951 # save outputs 952 with no_grad(): 953 generated_weights = model.weight.detach().numpy() 954 outputs= [generated_weights, mismatch_vals, loss_vals, state_vector] 955 output_labels=["weights", "mismatch", "loss", "statevec"] 956 for i in np.arange(len(outputs)): 957 np.save(os.path.join(DIR,"ampl_outputs", f"{output_labels[i]}{vars_to_name_str_ampl(args)}"), outputs[i]) 958 959 return 0
23def set_loss_func(loss_str, arg_dict, ampl=False): 24 r""" 25 Set the loss function to be used in training a network. 26 27 Arguments: 28 ---- 29 30 - **loss_str** : *str* 31 32 String specifying the loss function to use. Options are 33 34 - `'MSE'` : mean squared error loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html) with default settings. 35 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 36 37 - `'L1'` : mean absolute error loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html) with default settings. 38 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 39 40 - `'KLD'` : Kullback-Leibler divergence loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html) with default settings. 41 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 42 43 - `'CE'` : cross entropy loss. Using [pytorch's implementation](https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html) with default settings. 44 `criterion` takes two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 45 46 - `'SAM'` : sign-adjusted mismatch. Defined as $$\text{SAM}(\ket{x}, \ket{y}) = 1 - \sum_k |x_k| |y_k|,$$ where $\ket{x}$, $\ket{y}$ are the 47 network output and desired output, respectively, and $x_k$, $y_k$ are the coefficients w.r.t the two-register computational basis states. `criterion` takes 48 two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. 49 50 - `'WIM'` : weighted mismatch. Defined analogously to SAM but with additional weights: $$\text{WIM}(\ket{x}, \ket{y}) =1 - \sum_k w_k |x_k||y_k|.$$ 51 `criterion` takes three pytorch `Tensors` as inputs, corresponding to the network output, the desired output and the weights. See `set_WIM_weights()` 52 for information on how the weights are calculated. This loss function is not an option if `ampl` is True. 53 54 - `'WILL'` : weighted Lp loss. Defined as $$\text{WILL}(\ket{x}, \ket{y}; p,q) = \sum_k |x_k -y_k|^p + |x_k| |[k]_m - \Psi([k]_n)|^q,$$ where $p$ and $q$ are coefficients stored in 55 `arg_dict['WILL_p']`, `arg_dict['WILL_q']`, $[k]_n$ is the target register bit-string associated with the basis state $\ket{k}$, $[k]_n$ is the input register bit-string 56 associated with the basis state $\ket{k}$, $\Psi$ is the function to be evaluated for the network, and $x_k$, $y_k$ have the same meaning as above. `criterion` takes 57 two pytorch `Tensors` as inputs, corresponding to the network output and the desired output. This loss function is not an option if `ampl` is True. 58 59 - **arg_dict** : *dict* 60 61 A dictionary containing information on training variables, created with `pqcprep.file_tools.compress_args()` (or created 62 with `pqcprep.file_tools.compress_args_ampl()` in the case of `ampl` being True). 63 64 - **ampl** : *boolean* 65 66 If True, the loss function is defined for an amplitude-encoding network, as opposed to a function evaluation network. Default is 67 False. 68 69 Returns: 70 ---- 71 72 - **criterion** : *callable* 73 74 The loss function as a callable object. Number and type of arguments depend on the chosen `loss_str` option (see above). 75 76 """ 77 78 if loss_str=="MSE": 79 from torch.nn import MSELoss 80 criterion=MSELoss() 81 elif loss_str=="L1": 82 from torch.nn import L1Loss 83 criterion=L1Loss() 84 elif loss_str=="KLD": 85 from torch.nn import KLDivLoss 86 criterion=KLDivLoss() 87 elif loss_str=="CE": 88 from torch.nn import CrossEntropyLoss 89 criterion=CrossEntropyLoss() 90 elif loss_str=="SAM": 91 def criterion(output, target): 92 return torch.abs(1. -torch.sum(torch.mul(output, target))) 93 elif loss_str=="WIM": 94 if arg_dict["train_superpos"]==False: 95 raise ValueError(f"The loss function {loss_str} requires training in superposition, i.e. 'train_superpos==True'.") 96 if ampl: 97 raise ValueError(f"The loss function {loss_str} is not available for amplitude training. Allowed options are 'CE', 'MSE', 'L1', 'KLD', 'SAM'.") 98 99 def criterion(output, target, weights): 100 output = torch.mul(output, weights) 101 output = output / torch.sum(torch.mul(output, output)) 102 return torch.abs(1. -torch.sum(torch.mul(output, target))) 103 elif loss_str=="WILL": 104 if arg_dict["train_superpos"]==False: 105 raise ValueError(f"The loss function {loss_str} requires training in superposition, i.e. 'train_superpos==True'.") 106 if ampl: 107 raise ValueError(f"The loss function {loss_str} is not available for amplitude training. Allowed options are 'CE', 'MSE', 'L1', 'KLD', 'SAM'.") 108 fx_arr = [psi(i, mode=arg_dict["func_str"]) for i in np.arange(0, 2**arg_dict["n"])] 109 if arg_dict["phase_reduce"]: 110 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 111 112 fx_arr_rounded = [bin_to_dec(dec_to_bin(i,arg_dict["m"],'unsigned mag', nint=arg_dict["mint"]),'unsigned mag',nint=arg_dict["mint"]) for i in fx_arr] 113 distance_arr = np.empty(2**(arg_dict["n"]+arg_dict["m"])) 114 115 for i in np.arange(2**arg_dict["n"]): 116 bin_i=dec_to_bin(i,arg_dict["n"],'unsigned mag') 117 for j in np.arange(2**arg_dict["m"]): 118 bin_j=dec_to_bin(j,arg_dict["m"],'unsigned mag') 119 ind=int(bin_j + bin_i,2) 120 distance_arr[ind] = np.abs(bin_to_dec(dec_to_bin(j,arg_dict["m"],'unsigned mag'),'unsigned mag',nint=arg_dict["mint"]) - fx_arr_rounded[i]) 121 distance=Tensor(distance_arr) 122 123 def criterion(output, target): 124 loss =torch.pow(torch.abs(output-target),arg_dict["WILL_p"]) + torch.mul(torch.abs(output),torch.pow(distance,arg_dict["WILL_q"])) 125 return torch.sum(loss)**(1/arg_dict["WILL_p"]) / torch.numel(loss) 126 else: 127 raise ValueError("Unrecognised loss function. Options are: 'CE', 'MSE', 'L1', 'KLD', 'SAM', 'WIM', 'WILL'.") 128 129 return criterion
Set the loss function to be used in training a network.
Arguments:
loss_str : str
String specifying the loss function to use. Options are
'MSE': mean squared error loss. Using pytorch's implementation with default settings.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output.'L1': mean absolute error loss. Using pytorch's implementation with default settings.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output.'KLD': Kullback-Leibler divergence loss. Using pytorch's implementation with default settings.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output.'CE': cross entropy loss. Using pytorch's implementation with default settings.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output.'SAM': sign-adjusted mismatch. Defined as $$\text{SAM}(\ket{x}, \ket{y}) = 1 - \sum_k |x_k| |y_k|,$$ where $\ket{x}$, $\ket{y}$ are the network output and desired output, respectively, and $x_k$, $y_k$ are the coefficients w.r.t the two-register computational basis states.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output.'WIM': weighted mismatch. Defined analogously to SAM but with additional weights: $$\text{WIM}(\ket{x}, \ket{y}) =1 - \sum_k w_k |x_k||y_k|.$$criteriontakes three pytorchTensorsas inputs, corresponding to the network output, the desired output and the weights. Seeset_WIM_weights()for information on how the weights are calculated. This loss function is not an option ifamplis True.'WILL': weighted Lp loss. Defined as $$\text{WILL}(\ket{x}, \ket{y}; p,q) = \sum_k |x_k -y_k|^p + |x_k| |[k]_m - \Psi([k]_n)|^q,$$ where $p$ and $q$ are coefficients stored inarg_dict['WILL_p'],arg_dict['WILL_q'], $[k]_n$ is the target register bit-string associated with the basis state $\ket{k}$, $[k]_n$ is the input register bit-string associated with the basis state $\ket{k}$, $\Psi$ is the function to be evaluated for the network, and $x_k$, $y_k$ have the same meaning as above.criteriontakes two pytorchTensorsas inputs, corresponding to the network output and the desired output. This loss function is not an option ifamplis True.
arg_dict : dict
A dictionary containing information on training variables, created with
pqcprep.file_tools.compress_args()(or created withpqcprep.file_tools.compress_args_ampl()in the case ofamplbeing True).ampl : boolean
If True, the loss function is defined for an amplitude-encoding network, as opposed to a function evaluation network. Default is False.
Returns:
criterion : callable
The loss function as a callable object. Number and type of arguments depend on the chosen
loss_stroption (see above).
131def set_WIM_weights(generated_weights, arg_dict): 132 """ 133 134 Determine the weight coefficients for the WIM loss function. 135 136 Arguments: 137 ---- 138 - **generated_weights** : *array_like* 139 140 The network weights generated in a given epoch. 141 142 - **arg_dict** : *dict* 143 144 A dictionary containing information on training variables, created with `pqcprep.file_tools.compress_args()`. 145 146 Returns: 147 ---- 148 - **WIM_weights_arr** : *array_like* 149 150 The calculated coefficients for the WIM loss function. 151 152 """ 153 154 # initialise arrays to store results 155 WIM_weights_arr= np.empty(2**(arg_dict["n"]+arg_dict["m"])) 156 157 # iterate over input states 158 x_arr_temp=np.arange(2**arg_dict["n"]) 159 fx_arr_temp = [psi(k, mode=arg_dict["func_str"]) for k in x_arr_temp] 160 161 if arg_dict["phase_reduce"]: fx_arr_temp = [np.modf(k/ (2* np.pi))[0] for k in fx_arr_temp] 162 163 for q in x_arr_temp: 164 165 # prepare circuit 166 enc=binary_to_encode_param(np.binary_repr(q,arg_dict["n"])) 167 params=np.concatenate((enc, generated_weights)) 168 169 qc = generate_network(arg_dict["n"],arg_dict["m"],arg_dict["L"], encode=True,toggle_IL=True, real=arg_dict["real"],repeat_params=arg_dict["repeat_params"]) 170 qc = qc.assign_parameters(params) 171 172 # get target array 173 target_arr_temp = np.zeros(2**(arg_dict["n"]+arg_dict["m"])) 174 175 index = int(dec_to_bin(fx_arr_temp[q],arg_dict["m"],'unsigned mag',nint=arg_dict["mint"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 176 target_arr_temp[index]=1 177 178 # get statevector 179 state_vector_temp = get_state_vec(qc) 180 181 # for each output state, calculate the "binary difference" to the target as well as the "coefficient difference" 182 sum = 0 183 for j in np.arange(2**arg_dict["m"]): 184 ind = int(dec_to_bin(j,arg_dict["m"],'unsigned mag',nint=arg_dict["m"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 185 186 num_dif =np.abs(fx_arr_temp[q] -bin_to_dec(dec_to_bin(j,arg_dict["m"],'unsigned mag', nint=arg_dict["mint"]),'unsigned mag',nint=arg_dict["mint"])) 187 coeff_dif = np.abs(state_vector_temp[ind]- target_arr_temp[ind]) 188 sum += num_dif * coeff_dif 189 190 # add to weights_arr 191 for j in np.arange(2**arg_dict["m"]): 192 ind = int(dec_to_bin(j,arg_dict["m"],'unsigned mag',nint=arg_dict["m"])+dec_to_bin(x_arr_temp[q],arg_dict["n"],'unsigned mag',nint=arg_dict["nint"]),2) 193 WIM_weights_arr[ind]= sum 194 195 # focus on outliers: double-weight on states 0.5 sigma or more above the mean 196 WIM_weights_arr += (WIM_weights_arr >= np.mean(WIM_weights_arr)+0.5 *np.std(WIM_weights_arr) ).astype(int) 197 198 # smoothen WIM weights 199 WIM_weights_arr=np.exp(0.8 * WIM_weights_arr) 200 201 return WIM_weights_arr
Determine the weight coefficients for the WIM loss function.
Arguments:
generated_weights : array_like
The network weights generated in a given epoch.
arg_dict : dict
A dictionary containing information on training variables, created with
pqcprep.file_tools.compress_args().
Returns:
WIM_weights_arr : array_like
The calculated coefficients for the WIM loss function.
203def train_QNN(n,m,L, seed, epochs,func_str,loss_str,meta, recover_temp, nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta, DIR): 204 r""" 205 Train a QCNN to perform function evaluation $\ket{j}\ket{0} \mapsto \ket{j}\ket{\Psi(j)}$. 206 207 The QCNN is generated using `pqcprep.pqc_tools.generate_network()`. 208 209 Arguments: 210 --- 211 - **n** : *int* 212 213 Number of qubits in the input register. 214 215 - **m** : *int* 216 217 Number of qubits in the target register. 218 219 - **L** : *int* 220 221 Number of layers in the network. 222 223 - **seed** : *int* 224 225 Seed for random number generation. 226 227 - **epochs** : *int* 228 229 Number of training runs. 230 231 - **func_str** : *str* 232 233 String specifying the function $\Psi$ to be evaluated. Must be a valid option for the argument `mode` of `pqcprep.psi_tools.psi()`. 234 235 - **loss_str** : *str* 236 237 String specifying the loss function minimised by the optimiser. Must be a valid option for the argument `loss_str` of `set_loss_func()`. 238 239 - **meta** : *str* 240 241 String containing meta information to be included in output file names. 242 243 - **recover_temp** : *boolean* 244 245 If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten. 246 247 - **nint** : *int* 248 249 Number of integer qubits in input register. 250 251 - **mint** : *int* 252 253 Number of integer qubits in target register. 254 255 - **phase_reduce** : *boolean* 256 257 If True, reduce $\Psi(j)$ to the interval $[0, 2 \pi)$ i.e. perform the mapping $\Psi \to \Psi (\text{mod} \; 2 \pi)$. 258 259 - **train_superpos** : *boolean* 260 261 If True, train on a superposition of input states. If False, train on randomly sampled individual input states. 262 263 - **real** : *boolean* 264 265 If True, generate a network only involving CX and Ry rotations, resulting in real amplitudes. 266 267 - **repeat_params** : *str*, *optional* 268 269 Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 270 Options are `None` (do not keep parameters fixed), `'CL'` (keep parameters fixed for convolutional layers), 271 `'IL'` (keep parameters fixed for input layers), `'both'` (keep parameters fixed for both convolutional and input layers). 272 273 - **WILL_p** : *float* 274 275 The $p$ parameter of the WILL loss function, as described in `set_loss_func()`. 276 277 - **WILL_q** : *float* 278 279 The $q$ parameter of the WILL loss function, as described in `set_loss_func()`. 280 281 - **delta** : *float* 282 283 Hyper-parameter controlling the sampling of input state coefficients when training in superposition (`train_superpos = True`). Must be 284 between 0 and 1. `delta = 0` corresponds to coefficients fixed at $\\frac{1}{\\sqrt{2^n}}$ while `delta = 1` corresponds to coefficients randomly assuming values on the range $(0,1)$. 285 Intermediate values of `delta` result in coefficinets being randomly sampled on an interval around $\\frac{1}{\\sqrt{2^n}}$, with the range of the interval 286 determined by the value of `delta`. 287 288 - **DIR** : *str* 289 290 Parent directory for output files. 291 292 Returns: 293 ---- 294 295 The output produced by the training is saved in binary `.npy` files in the directory `DIR/outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 296 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()` and `<TYPE>` is one of: 297 298 - `weights` : file containing the QCNN weights determined by the optimiser; 299 300 - `loss` : file containing the loss value after each epoch; 301 302 - `mismatch` : file containg the mismatch value after each epoch; 303 304 - `grad` : file containing the squared weight gradient norm after each epoch; 305 306 - `vargrad` : file containing the variance of the weight gradients after each epoch. 307 308 """ 309 310 # compress arguments into dictionary 311 args =compress_args(n,m,L, seed, epochs,func_str,loss_str,meta,nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta) 312 313 # set precision strings 314 if nint==None: nint=n 315 if mint==None: mint=m 316 if phase_reduce: mint=0 317 318 # set seed for PRNG 319 algorithm_globals.random_seed= seed 320 rng = np.random.default_rng(seed=seed) 321 322 # generate circuit and set up as QNN 323 qc = generate_network(n,m,L, encode=not train_superpos, toggle_IL=True, initial_IL=True,input_Ry=train_superpos, real=real,repeat_params=repeat_params, wrap=False) 324 325 qnn = SamplerQNN( 326 circuit=qc.decompose(), 327 sampler=Sampler(options={"shots": 10000, "seed": algorithm_globals.random_seed}), 328 input_params=qc.parameters[:n], 329 weight_params=qc.parameters[n:], 330 input_gradients=True 331 ) 332 333 # choose initial weights 334 recovered_k =0 335 if recover_temp: 336 recover_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 337 recover_paths={} 338 for k in np.arange(100,epochs, step=100): 339 for e in np.arange(len(recover_labels)): 340 file=os.path.join(DIR,"outputs", f"__TEMP{k}_{recover_labels[e]}{vars_to_name_str(args)}.npy") 341 recover_paths[recover_labels[e]]= (file if os.path.isfile(file) else None) 342 343 if recover_labels[e]=="weights" and os.path.isfile(file): 344 recovered_k=k+1 345 346 if not None in list(recover_paths.values): 347 initial_weights=np.load(recover_paths["weights"]) 348 else: 349 initial_weights =np.zeros(len(qc.parameters[n:])) # initialise parameters to zero 350 else: 351 initial_weights =np.zeros(len(qc.parameters[n:])) # initialise parameters to zero 352 353 # initialise TorchConnector 354 model = TorchConnector(qnn, initial_weights) 355 356 # choose optimiser 357 optimizer = Adam(model.parameters(), lr=0.01, betas=(0.7, 0.999), weight_decay=0.005) 358 359 # set up arrays to store training outputs 360 if recover_temp and not None in list(recover_paths.values): 361 mismatch_vals=np.load(recover_paths["mismatch"]) 362 loss_vals=np.load(recover_paths["loss"]) 363 grad_vals=np.load(recover_paths["grad"]) 364 var_grad_vals=np.load(recover_paths["var_grad"]) 365 else: 366 mismatch_vals = np.empty(epochs) 367 loss_vals = np.empty(epochs) 368 grad_vals = np.empty(epochs) 369 var_grad_vals = np.empty(epochs) 370 371 # generate x and f(x) values 372 pn =n - nint 373 pm =m - mint 374 375 if train_superpos: 376 377 # sample all basis states of input register and convert to binary 378 x_arr = np.arange(0, 2**n) 379 x_arr_bin =[dec_to_bin(i,n,encoding="unsigned mag") for i in x_arr] 380 381 # apply function and reduce to phase value between 0 and 1 382 fx_arr = [psi(i, mode=func_str) for i in x_arr] 383 384 if phase_reduce: 385 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 386 387 # convert fx_arr to binary at available target register precision 388 fx_arr_bin = [dec_to_bin(i,m,nint=mint,encoding="unsigned mag") for i in fx_arr] 389 390 if np.max(fx_arr)> 2.**mint - 2.**(-pm) and mint != 0: 391 raise ValueError(f"Insufficient number of target (integer) qubits.") 392 393 # get bit strings corresponding to target arrays and convert to indices 394 target_bin = [fx_arr_bin[i]+x_arr_bin[i] for i in x_arr] 395 target_ind = [bin_to_dec(i, encoding='unsigned mag') for i in target_bin] 396 397 # prepare target array 398 target_arr = np.zeros(2**(n+m)) 399 for k in target_ind: 400 target_arr[int(k)]=1 401 402 else: 403 x_min = 0 404 x_max = 2.**nint - 2.**(-pn) 405 x_arr = np.array(x_min + (x_max - x_min) *rng.random(size=epochs)) 406 fx_arr = [psi(i, mode=func_str) for i in x_arr] 407 408 # reduce to phase value between 0 and 1: 409 if phase_reduce: 410 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 411 412 if np.max(fx_arr)> 2.**mint - 2.**(-pm) and mint != 0: 413 raise ValueError(f"Insufficient number of target (integer) qubits.") 414 415 # choose loss function 416 criterion=set_loss_func(loss_str, args) 417 418 # start training 419 print(f"\n\nTraining started. Epochs: {epochs}. Input qubits: {n}. Target qubits: {m}. QCNN layers: {L}. \n") 420 start = time.time() 421 422 warnings.filterwarnings("ignore", category=UserWarning) 423 424 for i in np.arange(epochs)[recovered_k:]: 425 426 if train_superpos == False: 427 # get input data 428 input = Tensor(binary_to_encode_param(dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint))) 429 430 # get target data 431 target_arr = np.zeros(2**(n+m)) 432 index = int(dec_to_bin(fx_arr[i],m,'unsigned mag',nint=mint)+dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint),2) 433 target_arr[index]=1 434 target=Tensor(target_arr) 435 else: 436 437 # generate random coefficients 438 coeffs = np.array(np.pi / 2 * (1+delta *(2 *rng.random(size=n)-1))) 439 440 # get input data 441 input=Tensor(coeffs) 442 443 # get target data 444 445 target_ampl = np.empty(2**(n+m)) 446 447 for j in np.arange(2**n): 448 for k in np.arange(2**m): 449 ind =int(dec_to_bin(k,m)+dec_to_bin(j,n),2) 450 bin_n = dec_to_bin(j,n)[:: -1] # reverse bit order ! 451 val=1 452 453 for l in np.arange(n): 454 val *= np.cos(coeffs[l]/2) if bin_n[l]=='0' else np.sin(coeffs[l]/2) 455 456 target_ampl[ind] = val * target_arr[ind] 457 458 target=Tensor(target_ampl**2) 459 460 # train model 461 optimizer.zero_grad() 462 463 # apply loss function 464 if loss_str=="WIM": 465 WIM_weights_arr=np.ones(2**(n+m)) if i==recovered_k else WIM_weights_arr 466 WIM_weights_tensor=Tensor(WIM_weights_arr) 467 loss =criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target), WIM_weights_tensor) # add small number in sqrt to avoid zero grad ! 468 else: 469 loss = criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target)) # add small number in sqrt to avoid zero grad ! 470 471 # propagate gradients and recompute weights 472 loss.backward() 473 optimizer.step() 474 475 # save loss and grad for plotting 476 loss_vals[i]=loss.item() 477 grad_vals[i]=np.sum(model.weight.grad.numpy()**2) 478 var_grad_vals[i]=np.std(model.weight.grad.numpy())**2 479 480 # set up circuit with calculated weights 481 circ = generate_network(n,m,L, encode=not train_superpos, toggle_IL=True, initial_IL=True,input_Ry=train_superpos, real=real,repeat_params=repeat_params, wrap=False) 482 483 with no_grad(): 484 generated_weights = model.weight.detach().numpy() 485 if train_superpos: 486 input_params = coeffs 487 params=np.concatenate((input_params, generated_weights)) 488 else: 489 input_params = binary_to_encode_param(dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint)) 490 params = np.concatenate((input_params, generated_weights)) 491 circ = circ.assign_parameters(params) 492 493 # get statevector 494 state_vector = get_state_vec(circ) 495 496 # calculate fidelity and mismatch 497 target_state = target_ampl**2 if train_superpos else target_arr 498 fidelity = np.abs(np.dot(np.sqrt(target_state),np.conjugate(state_vector)))**2 499 mismatch = 1. - np.sqrt(fidelity) 500 mismatch_vals[i]=mismatch 501 502 # set loss func weights for WIM 503 if loss_str=="WIM" and (i % 10 ==0) and (i >=1): WIM_weights_arr= set_WIM_weights(generated_weights, args) 504 505 # temporarily save outputs every hundred iterations 506 temp_ind = epochs - 100 507 508 if recover_temp: 509 temp_ind = recovered_k -1 510 511 if (i % 100 ==0) and (i != 0) and (i != epochs-1): 512 513 temp_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 514 temp_arrs=[generated_weights, mismatch_vals, loss_vals, grad_vals, var_grad_vals] 515 516 for e in np.arange(len(temp_labels)): 517 # save temp file 518 file=os.path.join(DIR,"outputs",f"__TEMP{i}_{temp_labels[e]}{vars_to_name_str(args)}") 519 np.save(file,temp_arrs[e]) 520 521 # delete previous temp file 522 old_file=os.path.join(DIR,"outputs",f"__TEMP{i-100}_{temp_labels[e]}{vars_to_name_str(args)}.npy") 523 os.remove(old_file) if os.path.isfile(old_file) else None 524 525 # make note of last created temp files 526 temp_ind = i 527 528 # print status 529 a = int(20*(i+1)/epochs) 530 531 if i==recovered_k: 532 time_str="--:--:--.--" 533 elif i==epochs-1: 534 time_str="00:00:00.00" 535 else: 536 if recover_temp: 537 remaining = ((time.time() - start) / (i-recovered_k)) * (epochs - i) 538 else: 539 remaining = ((time.time() - start) / i) * (epochs - i) 540 mins, sec = divmod(remaining, 60) 541 hours, mins = divmod(mins, 60) 542 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 543 544 prefix="\t" 545 print(f"{prefix}[{u'█'*a}{('.'*(20-a))}] {100.*((i+1)/epochs):.2f}% ; Loss {loss_vals[i]:.2e} ; Mismatch {mismatch:.2e} ; ETA {time_str}", end='\r', file=sys.stdout, flush=True) 546 547 548 print(" ", flush=True, file=sys.stdout) 549 550 warnings.filterwarnings("default", category=UserWarning) 551 552 elapsed = time.time()-start 553 mins, sec = divmod(elapsed, 60) 554 hours, mins = divmod(mins, 60) 555 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 556 557 # decompose circuit for gate count 558 num_CX = dict(circ.decompose(reps=4).count_ops())["cx"] 559 num_gates = num_CX + dict(circ.decompose(reps=4).count_ops())["u"] 560 print(f"\nTraining completed in {time_str}. Number of weights: {len(generated_weights)}. Number of gates: {num_gates} (of which CX gates: {num_CX}). \n\n") 561 562 # delete temp files 563 temp_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 564 for i in np.arange(len(temp_labels)): 565 file=os.path.join(DIR,"outputs",f"__TEMP{temp_ind}_{temp_labels[i]}{vars_to_name_str(args)}.npy") 566 os.remove(file) if os.path.isfile(file) else None 567 568 # save outputs 569 with no_grad(): 570 generated_weights = model.weight.detach().numpy() 571 outputs= [generated_weights, mismatch_vals, loss_vals, grad_vals, var_grad_vals] 572 output_labels=["weights", "mismatch", "loss", "grad", "vargrad"] 573 for i in np.arange(len(outputs)): 574 np.save(os.path.join(DIR,"outputs", f"{output_labels[i]}{vars_to_name_str(args)}"), outputs[i]) 575 576 return 0
Train a QCNN to perform function evaluation $\ket{j}\ket{0} \mapsto \ket{j}\ket{\Psi(j)}$.
The QCNN is generated using pqcprep.pqc_tools.generate_network().
Arguments:
n : int
Number of qubits in the input register.
m : int
Number of qubits in the target register.
L : int
Number of layers in the network.
seed : int
Seed for random number generation.
epochs : int
Number of training runs.
func_str : str
String specifying the function $\Psi$ to be evaluated. Must be a valid option for the argument
modeofpqcprep.psi_tools.psi().loss_str : str
String specifying the loss function minimised by the optimiser. Must be a valid option for the argument
loss_strofset_loss_func().meta : str
String containing meta information to be included in output file names.
recover_temp : boolean
If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten.
nint : int
Number of integer qubits in input register.
mint : int
Number of integer qubits in target register.
phase_reduce : boolean
If True, reduce $\Psi(j)$ to the interval $[0, 2 \pi)$ i.e. perform the mapping $\Psi \to \Psi (\text{mod} \; 2 \pi)$.
train_superpos : boolean
If True, train on a superposition of input states. If False, train on randomly sampled individual input states.
real : boolean
If True, generate a network only involving CX and Ry rotations, resulting in real amplitudes.
repeat_params : str, optional
Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. Options are
None(do not keep parameters fixed),'CL'(keep parameters fixed for convolutional layers),'IL'(keep parameters fixed for input layers),'both'(keep parameters fixed for both convolutional and input layers).WILL_p : float
The $p$ parameter of the WILL loss function, as described in
set_loss_func().WILL_q : float
The $q$ parameter of the WILL loss function, as described in
set_loss_func().delta : float
Hyper-parameter controlling the sampling of input state coefficients when training in superposition (
train_superpos = True). Must be between 0 and 1.delta = 0corresponds to coefficients fixed at $\frac{1}{\sqrt{2^n}}$ whiledelta = 1corresponds to coefficients randomly assuming values on the range $(0,1)$. Intermediate values ofdeltaresult in coefficinets being randomly sampled on an interval around $\frac{1}{\sqrt{2^n}}$, with the range of the interval determined by the value ofdelta.DIR : str
Parent directory for output files.
Returns:
The output produced by the training is saved in binary .npy files in the directory DIR/outputs using naming convention <TYPE>_<NAME_STR>.npy
where <NAME_STR> is the name string produced by pqcprep.file_tools.vars_to_name_str() and <TYPE> is one of:
weights: file containing the QCNN weights determined by the optimiser;loss: file containing the loss value after each epoch;mismatch: file containg the mismatch value after each epoch;grad: file containing the squared weight gradient norm after each epoch;vargrad: file containing the variance of the weight gradients after each epoch.
578def test_QNN(n,m,L,seed,epochs, func_str,loss_str,meta,nint,mint,phase_reduce,train_superpos,real,repeat_params,WILL_p, WILL_q,delta,DIR,verbose=True): 579 """ 580 Test performance of a QCNN trained for function evaluation with respect to different metrics. 581 582 This requires the existence of an appropriate `weights_<NAME_STR>.npy` file (as produced by `train_QNN()`) in the directory `DIR/outputs`, where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()`. 583 584 Arguments: 585 --- 586 587 Same arguments as `train_QNN()`. See there for a description. 588 589 Returns: 590 --- 591 592 The testing output produced is saved in binary `.npy` files in the directory `DIR/ampl_outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 593 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str()` and `<TYPE>` is one of: 594 595 - `mismatch_by_state` : file containing the mismatch between the state produced by the network and the desired output state for each of the possible input 596 register states. Contains a dictionary with the input bit strings as keys and the associated mismatch as values. 597 598 - `phase` : file containing the phase function encoded by the network when the input register is in an equal superposition of input states. 599 600 - `metrics` : file containing a dictionary with several metrics evaluating the performance of the network. These metrics are 601 * `mu` : mean mismatch (mean of the data contained in `mismatch_by_state_<NAME_STR>.npy`); should be zero for ideal performance 602 * `sigma` : mismatch standard deviation (standard deviation of the data contained in `mismatch_by_state_<NAME_STR>.npy`); should be zero for ideal performance 603 * `eps` : normalisation error on the state vector associated with the data contained in `phase_<NAME_STR>.npy`; should be zero for ideal performance 604 * `chi` : mean absolute difference between the phase function contained in `phase_<NAME_STR>.npy` and the rounded desired phase function; should be zero for ideal performance 605 * `omega` : a super-metric defined as `1/(mu + sigma + eps + chi)` ; should be maximal for ideal performance 606 607 """ 608 # compress arguments into dictionary 609 args =compress_args(n,m,L, seed, epochs,func_str,loss_str,meta,nint, mint, phase_reduce, train_superpos, real, repeat_params, WILL_p, WILL_q, delta) 610 name_str=vars_to_name_str(args) 611 612 # set precision strings 613 if nint==None: nint=n 614 if mint==None: mint=m 615 if phase_reduce: mint=0 616 617 # load weights 618 if os.path.isfile(os.path.join(DIR,"outputs",f"weights{name_str}.npy")): 619 weights = np.load(os.path.join(DIR,"outputs",f"weights{name_str}.npy")) 620 else: 621 raise ValueError("No appropriate QCNN weights could be found. Check the network configuration as well as the relevant directory.") 622 623 # initialise array to store results 624 mismatch = np.empty(2**n) 625 signs = np.empty(2**n) 626 627 # iterate over input states 628 x_arr = np.arange(2**n) 629 fx_arr = [psi(i, mode=func_str) for i in x_arr] 630 631 if phase_reduce: 632 fx_arr = [np.modf(i/ (2* np.pi))[0] for i in fx_arr] 633 634 for i in x_arr: 635 636 # prepare circuit 637 enc=binary_to_encode_param(np.binary_repr(i,n)) 638 params=np.concatenate((enc, weights)) 639 640 circ = generate_network(n,m,L, encode=True,toggle_IL=True, real=real,repeat_params=repeat_params) 641 circ = circ.assign_parameters(params) 642 643 # get target array 644 target_arr = np.zeros(2**(n+m)) 645 646 index = int(dec_to_bin(fx_arr[i],m,'unsigned mag',nint=mint)+dec_to_bin(x_arr[i],n,'unsigned mag',nint=nint),2) 647 target_arr[index]=1 648 649 # get statevector 650 state_vector = get_state_vec(circ) 651 652 signs[i]=np.sign(np.sum(np.real(state_vector)*np.sqrt(target_arr))) 653 654 # calculate fidelity and mismatch 655 fidelity = np.abs(np.dot(np.sqrt(target_arr),np.conjugate(state_vector)))**2 656 mismatch[i] = 1. - np.sqrt(fidelity) 657 658 # get phase target 659 fx_arr_bin = [dec_to_bin(i,m, "unsigned mag", 0) for i in fx_arr] 660 phase_target = np.array([bin_to_dec(i,"unsigned mag", mint) for i in fx_arr_bin]) 661 if phase_reduce: 662 phase_target *= 2 * np.pi 663 664 # get state vector for full phase extraction 665 state_vec= full_encode(n,m, weights_A_str=None, weights_p_str=weights, L_A=None, L_p=L,real_p=real,repeat_params=repeat_params,full_state_vec=False, no_UA=True, operators="QRQ") 666 phase = phase_from_state(state_vec) 667 668 # calculate metrics 669 mu = np.mean(mismatch) 670 sigma = np.std(mismatch) 671 eps = 1 - np.sum(np.abs(state_vec)**2) 672 chi = np.mean(np.abs(phase - phase_target)) 673 omega= 1/(mu+sigma+eps+chi) 674 675 # save outputs 676 full_dic = dict(zip(x_arr, mismatch)) 677 np.save(os.path.join(DIR,"outputs",f"mismatch_by_state{name_str}.npy"), full_dic) 678 679 metric_dic = {} 680 metric_dic["mu"]=mu 681 metric_dic["sigma"]=sigma 682 metric_dic["eps"]=eps 683 metric_dic["chi"]=chi 684 metric_dic["omega"]=omega 685 np.save(os.path.join(DIR,"outputs",f"metrics{name_str}.npy"), metric_dic) 686 687 np.save(os.path.join(DIR,"outputs",f"phase{name_str}.npy"), phase) 688 689 if verbose: 690 print("Mismatch by input state:") 691 for i in x_arr: 692 print(f"\t{np.binary_repr(i,n)}: {mismatch[i]:.2e} ({signs[i]})") 693 print("-----------------------------------") 694 print(f"Mu: \t{mu:.3e}") 695 print(f"Sigma: \t{sigma:.3e}") 696 print(f"Eps: \t{eps:.3e}") 697 print(f"Chi: \t{chi:.3e}") 698 print(f"Omega: \t{omega:.3f}") 699 print("-----------------------------------") 700 print("") 701 print("") 702 703 return 0
Test performance of a QCNN trained for function evaluation with respect to different metrics.
This requires the existence of an appropriate weights_<NAME_STR>.npy file (as produced by train_QNN()) in the directory DIR/outputs, where <NAME_STR> is the name string produced by pqcprep.file_tools.vars_to_name_str().
Arguments:
Same arguments as train_QNN(). See there for a description.
Returns:
The testing output produced is saved in binary .npy files in the directory DIR/ampl_outputs using naming convention <TYPE>_<NAME_STR>.npy
where <NAME_STR> is the name string produced by pqcprep.file_tools.vars_to_name_str() and <TYPE> is one of:
mismatch_by_state: file containing the mismatch between the state produced by the network and the desired output state for each of the possible input register states. Contains a dictionary with the input bit strings as keys and the associated mismatch as values.phase: file containing the phase function encoded by the network when the input register is in an equal superposition of input states.metrics: file containing a dictionary with several metrics evaluating the performance of the network. These metrics aremu: mean mismatch (mean of the data contained inmismatch_by_state_<NAME_STR>.npy); should be zero for ideal performancesigma: mismatch standard deviation (standard deviation of the data contained inmismatch_by_state_<NAME_STR>.npy); should be zero for ideal performanceeps: normalisation error on the state vector associated with the data contained inphase_<NAME_STR>.npy; should be zero for ideal performancechi: mean absolute difference between the phase function contained inphase_<NAME_STR>.npyand the rounded desired phase function; should be zero for ideal performanceomega: a super-metric defined as1/(mu + sigma + eps + chi); should be maximal for ideal performance
705def ampl_train_QNN(n,L,x_min,x_max,seed, epochs,func_str,loss_str,meta, recover_temp, nint, repeat_params, DIR): 706 """ 707 708 Train a QCNN to prepare an amplitude distribution: $\ket{0} \mapsto \sum_j A(j) \ket{j}$. 709 710 The QCNN is generated using `pqcprep.pqc_tools.A_generate_network()`. 711 712 Arguments: 713 --- 714 715 - **n** : *int* 716 717 Number of qubits in the register. 718 719 - **L** : *int* 720 721 Number of layers in the network. 722 723 - **x_min** : *float* 724 725 Minimum of function domain. 726 727 - **x_max** : *float* 728 729 Maximum of function domain. 730 731 - **seed** : *int* 732 733 Seed for random number generation. 734 735 - **epochs** : *int* 736 737 Number of training runs. 738 739 - **func_str** : *str* 740 741 String specifying the function $A$ to be prepared. Must be a valid option for the argument `mode` of `pqcprep.psi_tools.A()`. 742 743 - **loss_str** : *str* 744 745 String specifying the loss function minimised by the optimiser. Must be a valid option for the argument `loss_str` of `set_loss_func()`. 746 747 - **meta** : *str* 748 749 String containing meta information to be included in output file names. 750 751 - **recover_temp** : *boolean* 752 753 If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten. 754 755 - **nint** : *int* 756 757 Number of integer qubits in the register. 758 759 - **repeat_params** : *boolean* 760 761 If True, keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 762 763 - **DIR** : *str* 764 765 Parent directory for output files. 766 767 768 Returns: 769 --- 770 771 The output produced by the training is saved in binary `.npy` files in the directory `DIR/ampl_outputs` using naming convention `<TYPE>_<NAME_STR>.npy` 772 where `<NAME_STR>` is the name string produced by `pqcprep.file_tools.vars_to_name_str_ampl()` and `<TYPE>` is one of: 773 774 - `weights` : file containing the QCNN weights determined by the optimiser; 775 776 - `state_vec` : file containing the statevector corresponding to the register after applying the QCNN; 777 778 - `loss` : file containing the loss value after each training run; 779 780 - `mismatch` : file containg the mismatch value after each training run. 781 782 783 """ 784 785 # compress arguments into dictionary 786 args=compress_args_ampl(n,L,x_min,x_max,seed, epochs,func_str,loss_str,meta, nint, repeat_params) 787 788 # set seed for PRNG 789 algorithm_globals.random_seed= seed 790 rng = np.random.default_rng(seed=seed) 791 792 # generate circuit and set up as QNN 793 qc = A_generate_network(n,L,repeat_params) 794 qnn = SamplerQNN( 795 circuit=qc.decompose(), # decompose to avoid data copying (?) 796 sampler=Sampler(options={"shots": 10000, "seed": algorithm_globals.random_seed}), 797 weight_params=qc.parameters, 798 input_params=[], 799 input_gradients=False 800 ) 801 802 # set precision 803 if nint==None: nint=n 804 805 # choose initial weights 806 recovered_k =0 807 808 if recover_temp: 809 recover_labels=["weights", "mismatch", "loss"] 810 recover_paths={} 811 812 for k in np.arange(100,epochs, step=100): 813 for e in np.arange(len(recover_labels)): 814 file=os.path.join(DIR,"ampl_outputs", f"__TEMP{k}_{recover_labels[e]}{vars_to_name_str_ampl(args)}.npy") 815 recover_paths[recover_labels[e]]= (file if os.path.isfile(file) else None) 816 817 if recover_labels[e]=="weights" and os.path.isfile(file): 818 recovered_k=k+1 819 820 if not None in list(recover_paths.values): 821 initial_weights=np.load(recover_paths["weights"]) 822 else: 823 initial_weights =np.zeros(len(qc.parameters)) # initialise parameters to zero 824 825 else: 826 initial_weights = np.zeros(len(qc.parameters)) # initialise parameters to zero 827 828 # initialise TorchConnector 829 model = TorchConnector(qnn, initial_weights) 830 831 # choose optimiser and loss function 832 optimizer = Adam(model.parameters(), lr=0.01, betas=(0.7, 0.999), weight_decay=0.005) # Adam optimizer 833 criterion=set_loss_func(loss_str, args, ampl=True) 834 835 # set up arrays to store training outputs 836 if recover_temp and not None in list(recover_paths.values): 837 mismatch_vals=np.load(recover_paths["mismatch"]) 838 loss_vals=np.load(recover_paths["loss"]) 839 else: 840 mismatch_vals = np.empty(epochs) 841 loss_vals = np.empty(epochs) 842 843 # calculate target and normalise 844 dx = (x_max-x_min)/(2**n) 845 target_arr = np.array([A(i, mode=func_str) for i in np.arange(x_min,x_max, dx)])**2 846 target_arr = target_arr / np.sum(target_arr) 847 848 # start training 849 print(f"\n\nTraining started. Epochs: {epochs}. Input qubits: {n}. Function range: [{x_min},{x_max}]. QCNN layers: {L}. \n") 850 start = time.time() 851 852 warnings.filterwarnings("ignore", category=UserWarning) 853 854 for i in np.arange(epochs)[recovered_k:]: 855 856 # get input data 857 input = Tensor([]) 858 859 # get target data 860 target=Tensor(target_arr) 861 862 # train model 863 optimizer.zero_grad() 864 loss = criterion(torch.sqrt(torch.abs(model(input))+1e-10), torch.sqrt(target)) # adding 1e-10 to prevent taking sqrt(0) ??!! 865 loss.backward() 866 optimizer.step() 867 868 # save loss for plotting 869 loss_vals[i]=loss.item() 870 871 # set up circuit with calculated weights 872 circ = A_generate_network(n,L, repeat_params) 873 874 with no_grad(): 875 generated_weights = model.weight.detach().numpy() 876 877 circ = circ.assign_parameters(generated_weights) 878 879 # get statevector 880 state_vector = get_state_vec(circ) 881 882 # calculate fidelity and mismatch 883 fidelity = np.abs(np.dot(np.sqrt(target_arr),np.conjugate(state_vector)))**2 884 mismatch = 1. - np.sqrt(fidelity) 885 886 # save mismatch for plotting 887 mismatch_vals[i]=mismatch 888 889 # temporarily save outputs every hundred iterations 890 temp_ind = epochs - 100 891 892 if recover_temp: 893 temp_ind = recovered_k -1 894 895 if (i % 100 ==0) and (i != 0) and (i != epochs-1): 896 897 temp_labels=["weights", "mismatch", "loss"] 898 temp_arrs=[generated_weights, mismatch_vals, loss_vals] 899 900 for e in np.arange(len(temp_labels)): 901 # save temp file 902 file=os.path.join(DIR,"ampl_outputs",f"__TEMP{i}_{temp_labels[e]}{vars_to_name_str_ampl(args)}") 903 np.save(file,temp_arrs[e]) 904 905 # delete previous temp file 906 old_file=os.path.join(DIR,"ampl_outputs",f"__TEMP{i-100}_{temp_labels[e]}{vars_to_name_str_ampl(args)}.npy") 907 os.remove(old_file) if os.path.isfile(old_file) else None 908 909 # make note of last created temp files 910 temp_ind = i 911 912 # print status 913 a = int(20*(i+1)/epochs) 914 915 if i==recovered_k: 916 time_str="--:--:--.--" 917 elif i==epochs-1: 918 time_str="00:00:00.00" 919 else: 920 if recover_temp: 921 remaining = ((time.time() - start) / (i-recovered_k)) * (epochs - i) 922 else: 923 remaining = ((time.time() - start) / i) * (epochs - i) 924 mins, sec = divmod(remaining, 60) 925 hours, mins = divmod(mins, 60) 926 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 927 928 prefix="\t" 929 print(f"{prefix}[{u'█'*a}{('.'*(20-a))}] {100.*((i+1)/epochs):.2f}% ; Loss {loss_vals[i]:.2e} ; Mismatch {mismatch:.2e} ; ETA {time_str}", end='\r', file=sys.stdout, flush=True) 930 931 warnings.filterwarnings("default", category=UserWarning) 932 933 print(" ", flush=True, file=sys.stdout) 934 935 elapsed = time.time()-start 936 mins, sec = divmod(elapsed, 60) 937 hours, mins = divmod(mins, 60) 938 time_str = f"{int(hours):02}:{int(mins):02}:{sec:05.2f}" 939 940 # decompose circuit for gate count 941 num_CX = dict(circ.decompose(reps=4).count_ops())["cx"] 942 num_gates = num_CX + dict(circ.decompose(reps=4).count_ops())["u"] 943 944 print(f"\nTraining completed in {time_str}. Number of weights: {len(generated_weights)}. Number of gates: {num_gates} (of which CX gates: {num_CX}). \n\n") 945 946 # delete temp files 947 temp_labels=["weights", "mismatch", "loss"] 948 for i in np.arange(len(temp_labels)): 949 file=os.path.join(DIR,"ampl_outputs",f"__TEMP{temp_ind}_{temp_labels[i]}{vars_to_name_str_ampl(args)}.npy") 950 os.remove(file) if os.path.isfile(file) else None 951 952 # save outputs 953 with no_grad(): 954 generated_weights = model.weight.detach().numpy() 955 outputs= [generated_weights, mismatch_vals, loss_vals, state_vector] 956 output_labels=["weights", "mismatch", "loss", "statevec"] 957 for i in np.arange(len(outputs)): 958 np.save(os.path.join(DIR,"ampl_outputs", f"{output_labels[i]}{vars_to_name_str_ampl(args)}"), outputs[i]) 959 960 return 0
Train a QCNN to prepare an amplitude distribution: $\ket{0} \mapsto \sum_j A(j) \ket{j}$.
The QCNN is generated using pqcprep.pqc_tools.A_generate_network().
Arguments:
n : int
Number of qubits in the register.
L : int
Number of layers in the network.
x_min : float
Minimum of function domain.
x_max : float
Maximum of function domain.
seed : int
Seed for random number generation.
epochs : int
Number of training runs.
func_str : str
String specifying the function $A$ to be prepared. Must be a valid option for the argument
modeofpqcprep.psi_tools.A().loss_str : str
String specifying the loss function minimised by the optimiser. Must be a valid option for the argument
loss_strofset_loss_func().meta : str
String containing meta information to be included in output file names.
recover_temp : boolean
If True, continue training from TEMP files (should they exist). If False and TEMP files exist they will be overwritten.
nint : int
Number of integer qubits in the register.
repeat_params : boolean
If True, keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type.
DIR : str
Parent directory for output files.
Returns:
The output produced by the training is saved in binary .npy files in the directory DIR/ampl_outputs using naming convention <TYPE>_<NAME_STR>.npy
where <NAME_STR> is the name string produced by pqcprep.file_tools.vars_to_name_str_ampl() and <TYPE> is one of:
weights: file containing the QCNN weights determined by the optimiser;state_vec: file containing the statevector corresponding to the register after applying the QCNN;loss: file containing the loss value after each training run;mismatch: file containg the mismatch value after each training run.