<jupyter_start><jupyter_text>Credit Card Fraud - A Growing IssueCredit card fraud is a growing issue with \\$28.5 billion lost globally due to credit card fraud in 2020 and is expected to exceed \\$49 billion by 2030 [1]). In 2020, around 1 out of 4 digital interactions were credit card fraud attempts (Cite Arkose Labs). Since there are so many non fraudulent transactions, it is challenging to detect the fraudulent transactions. In this notebook, we will be using a quantum auto-encoder to perform anomaly detection. We can use the quantum auto encoder to encode the 4 qubit state into a 3 qubit state and then use a decoder to decode the 3 qubit state back into 4 qubit state. The quantum auto encoder is trained on the normal dataset (or in this case the non fraudulent transactions) which means the quantum auto To tell if a datapoint is an anomaly,  Import the dataset<jupyter_code>df = pd.read_csv('creditcard.csv')<jupyter_output><empty_output><jupyter_text>We are only going to print the first 5 rows because the dataset contains over 280,000 rows. Each row represents a transaction. Time shows the time passed between the current and first transactions and amount shows the dollar amount spent on the transaction. There are also 28 more features represented by V1, V2, ... , V28 which come from principal component analysis. Finally, there is the class, where a '0' represents no fraud committed and a '1' represents a fraudulent transaction Let's now check the class distribution<jupyter_code>print('No Frauds: ', df['Class'].value_counts()[0])
print('Frauds: ', df['Class'].value_counts()[1])<jupyter_output>No Frauds:  284315
Frauds:  492<jupyter_text>Credit card fraud is relatively rare, this creates a very imbalanced distribution. A very imbalanced distribution is not ideal as this can lead to overfitting and our model assuming no fraud most of the time. It is also challenging to find the true correlations between the features and class.<jupyter_code>plot_correlation_matrix(df, "Original Correlation Matrix")<jupyter_output><empty_output><jupyter_text>As you can see, nothing can really be inferred from this correlation matrix since the data is so imbalanced. We are going to create a sub sample dataset with equal amounts of non fraudulent data and fraudulent data. We are also going to scale the 'Time' and 'Amount' values for better processing.<jupyter_code>#from sklearn.preprocessing import StandardScaler, RobustScaler
from sklearn.preprocessing import RobustScaler

# Scaling amount and time for the subsample
df['scaled_amount'] = RobustScaler().fit_transform(df['Amount'].values.reshape(-1,1))
df['scaled_time'] = RobustScaler().fit_transform(df['Time'].values.reshape(-1,1))
df.drop(['Time','Amount'], axis=1, inplace=True) # Drop the original time and amount values

# Add scaled amount and times to the data frame
scaled_amount = df['scaled_amount'] 
scaled_time = df['scaled_time']
df.drop(['scaled_amount', 'scaled_time'], axis=1, inplace=True)
df.insert(0, 'scaled_amount', scaled_amount)
df.insert(1, 'scaled_time', scaled_time)

# Create the balanced subsample of 49
df = df.sample(frac=1)
fraud_df = df.loc[df['Class'] == 1]
non_fraud_df = df.loc[df['Class'] == 0][:492]
normal_distributed_df = pd.concat([fraud_df, non_fraud_df])
sub_sample_df = normal_distributed_df.sample(frac=1, random_state=42)

# Display the first 5 rows t ose
sub_sample_df.head()<jupyter_output><empty_output><jupyter_text>We can now plot the correlation matrix of our new sub sample to get a better idea of the true correlations between features and 'Class'<jupyter_code>sub_sample_corr = sub_sample_df.corr()
plot_correlation_matrix(sub_sample_corr, "Sub Sample Correlation Matrix")<jupyter_output><empty_output><jupyter_text>The correlations are now much more noticeable. Now, we can find the features with the strongest correlation to class. Half are the strongest positive correlations, half are the strongest negative correlations.<jupyter_code>def find_strongest_correlations(dataframe, latent_qubits):
    
    num_features = latent_qubits**2
    
    class_correlations = dataframe.loc['Class', :]
    class_correlations = class_correlations.drop(index = 'Class')
    
    feature_list = list(class_correlations.index)
    correlation_list = [class_correlations[x] for x in feature_list]
    
    features = []
    correlations = []
    
    for i in range(int(num_features/2)):
        
        
        correlations.append(max(correlation_list))
        features.append(feature_list[correlation_list.index(max(correlation_list))])
        
        del feature_list[correlation_list.index(max(correlation_list))]
        del correlation_list[correlation_list.index(max(correlation_list))]                        
                                      
        correlations.append(min(correlation_list))
        features.append(feature_list[correlation_list.index(min(correlation_list))])
        
        del feature_list[correlation_list.index(min(correlation_list))]
        del correlation_list[correlation_list.index(min(correlation_list))] 
    
    return features, correlations

feature_list, correlations = find_strongest_correlations(sub_sample_corr, 4)

print(find_strongest_correlations(sub_sample_corr, 4))<jupyter_output>(['V4', 'V14', 'V11', 'V12', 'V2', 'V10', 'V19', 'V16', 'V20', 'V9', 'V21', 'V3', 'V28', 'V17', 'V27', 'V7'], [0.7211980893293823, -0.7517209646906146, 0.6934701844282471, -0.6848674234383837, 0.492405053522152, -0.6192313003963814, 0.278836598111022, -0.5913009967861621, 0.18495339501702748, -0.5831508529466832, 0.12784531878929187, -0.5674471056206869, 0.08757252744115403, -0.5581046843201575, 0.0713419111838715, -0.4801694818094904])<jupyter_text>We now have 16 features that are the most correlated with 'Class'. In this use case, we will be using 4 qubits to represent the data which means we will need 2$^{4}$ = 16 features to encode into the 4 qubits though amplitude encoding. Later, we will use the quantum autoencoder to encode the 4 qubits into 3 qubits and then use a decoder to decode those 3 qubits back to 4 qubits.<jupyter_code># Dataframe of all non fraudulent transactions
branch = df
non_fraud = branch[branch["Class"]!="1"][:200]

# All examples of non fraudulent data with 16 features
non_fraud = non_fraud[feature_list][:200]
non_fraud.head()

input_data = non_fraud.to_numpy()<jupyter_output><empty_output><jupyter_text>Training<jupyter_code>shots = 2500 #The amount of shots used for each epoch of training
nr_trash= 1   # Number of qubits 'thrown away'
nr_latent= 3  # Number of qubits left after the encoder is used
nr_ent = 0

epochs = 500   # Number of iterations of training to perform to find the final encoder parameters
learning_rate = .005 # Learning rate for the optimizer, dictates how fast the optimizer trains
batch_size = 2
num_samples = 50  # Number of training samples used for each epoch

beta1 = 0.9
beta2 = 0.999
opt = AdamOptimizer(learning_rate, beta1=beta1, beta2=beta2)

# Organizes and specifies our qubits for the device
spec = QubitsArrangement(nr_trash, nr_latent, nr_swap=1, nr_ent=nr_ent)


#set up the device 

#dev = qml.device("default.qubit", wires=spec.num_qubits)
dev = qml.device("braket.local.qubit", wires=spec.num_qubits, shots = 2500)

print("Qubits:", spec.qubits)
@qml.qnode(dev, interface = "autograd")
def training_circuit_example(init_params, encoder_params, reinit_state):
    #initilaization
    setAB_amplitude(spec, init_params)

    setAux(spec, reinit_state)

    setEnt(spec, inputs=[1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)])

    #encoder 

    for params in encoder_params:
        e2_classic(params, [*spec.latent_qubits, *spec.trash_qubits])

    #swap test 
    swap_t(spec)

    return [qml.probs(i) for i in spec.swap_qubits]

def fid_func(output):
    # Implemented as the Fidelity Loss
    # output[0] because we take the probability that the state after the 
    # SWAP test is ket(0), like the reference state
    fidelity_loss = 1 / output[0]
    return fidelity_loss
# Define cost function
def cost(encoder_params, X):
    reinit_state = [0 for i in range(2 ** len(spec.aux_qubits))]
    reinit_state[0] = 1.0
    loss = 0.0
    for x in X:
        output = training_circuit_example(init_params=x[0], encoder_params=encoder_params, reinit_state=reinit_state)[0]
        f = fid_func(output)
        loss = loss + f
    return loss / len(X)
# Define fidelity function
def fidelity(encoder_params, X):
    reinit_state = [0 for i in range(2 ** len(spec.aux_qubits))]
    reinit_state[0] = 1.0
    loss = 0.0
    for x in X:
        output = training_circuit_example(init_params=x[0], encoder_params=encoder_params, reinit_state=reinit_state)[0]
        f = output[0]
        loss = loss + f
    return loss / len(X)
def iterate_batches(X, batch_size):
    
    random.shuffle(X)

    batch_list = []
    batch = []
    for x in X:
        if len(batch) < batch_size:
            batch.append(x)

        else:
            batch_list.append(batch)
            batch = []
    if len(batch) != 0:
        batch_list.append(batch)
    return batch_list

training_data = [ torch.tensor([input_data[i]]) for i in range(num_samples)]
test_data = [ torch.tensor([input_data[i]]) for i in range(num_samples,num_samples+num_samples)]

X_training = training_data
X_tes = test_data


# initialize random encoder parameters
nr_encod_qubits = len(spec.trash_qubits) + len(spec.latent_qubits)
nr_par_encoder =  15 * int(nr_encod_qubits*(nr_encod_qubits-1)/2)
encoder_params = np.random.uniform(size=(1, nr_par_encoder), requires_grad=True)
print(encoder_params)
encoder_params=[[-0.45793534, -0.60180382,  1.36706854, -0.39273726,  0.78967496,
         -0.11834691,  1.21601293,  0.7257659 ,  0.16775198,  0.87110514,
          0.5825973 ,  1.35786832,  1.6198694 , -0.21858262,  1.41542989,
          1.2968311 ,  1.8630585 ,  0.50511886,  0.75524677,  0.82482716,
          1.02949018, -0.023521  ,  0.55110408,  0.15877528,  0.62316124,
          0.37113699,  0.4557925 ,  0.62940097,  0.61549768,  0.95122916,
          0.22349399,  0.86457997,  0.81546047,  1.47984623,  1.72818011,
         -0.30175269,  0.67999145, -0.22226086,  0.94370564,  1.48028116,
          0.72720142,  0.20210445,  0.14995309,  0.19133051, -0.35101019,
          0.40932117, -0.09846242,  0.65960454,  0.78151562,  1.17058629,
          0.23858532,  0.71485483,  0.31327769,  1.63693523,  0.95525645,
          0.58935465,  0.76165831,  0.62729872,  0.55561916,  0.19378356,
          0.41408805,  1.01374824,  0.37282255, -0.06769513,  0.45583351,
         -0.05101048,  0.83344398,  1.58156091,  1.46059524,  0.9371276 ,
          0.96522386,  0.27626285,  0.19818911,  0.11227637,  0.38220371,
          0.64166103,  0.92703234,  0.3736458 ,  0.21161801,  0.62412085,
          0.3278856 , -0.18893975,  0.86769553,  0.78573112,  0.50142613,
          0.96622037,  0.40300401,  0.55802604,  0.12912973,  0.14822851]]
'''
print(encoder_params)

#reinit_state = [0 for i in range(2 ** 4)]
#reinit_state[0] = 1.0
print(reinit_state)
print(len(spec.aux_qubits))
#print(training_circuit_example(encoder_params, encoder_params, reinit_state = reinit_state))
'''
# Create a tensor dataset with only fraud data and most correlated features 
# for finding the fidelity of the quantum autoencoder on fraud transactions

fraud = fraud_df[feature_list]
np_fraud = fraud.to_numpy()
fraud_data = [ torch.tensor([np_fraud[i]]) for i in range(len(fraud.to_numpy()))] 

fraud.head()

loss_hist=[]
fid_hist=[]

loss_hist_test=[]
fid_hist_test=[]

fraud_fid=[]

for epoch in range(epochs):
    batches = iterate_batches(X=training_data, batch_size=batch_size)
    for xbatch in batches:
        encoder_params = opt.step(cost, encoder_params, X=xbatch)

        
    if epoch%5 == 0:
        
        loss_training = cost(encoder_params, X_training )
        fidel = fidelity(encoder_params, X_training )
        
        loss_hist.append(loss_training)
        fid_hist.append(fidel)
        print("Epoch:{} | Loss:{} | Fidelity:{}".format(epoch, loss_training, fidel))

        loss_test = cost(encoder_params, X_tes )
        fidel = fidelity(encoder_params, X_tes )
        loss_hist_test.append(loss_test)
        fid_hist_test.append(fidel)
        print("Test-Epoch:{} | Loss:{} | Fidelity:{}".format(epoch, loss_test, fidel))
        
        f_fidel = fidelity(encoder_params, fraud_data )
        fraud_fid.append(f_fidel)
        print("Fraud Fidelity:{}".format(f_fidel))
fig = plt.figure()
plt.plot([x for x in range(0,len(loss_hist)*5,5)],np.array(fid_hist),label="train fidelity")
plt.plot([x for x in range(0,len(loss_hist)*5,5)],np.array(fid_hist_test),label="test fidelity")
plt.plot([x for x in range(0,len(loss_hist)*5,5)],np.array(fraud_fid),label="fraud fidelity")

plt.legend()
plt.title("Fraud 4-3-4 Compression Fidelity e2 Braket",)
plt.xlabel("Epoch")
plt.ylabel("Fidelity")
plt.savefig("Fraud 4-3-4 Compression Fidelity e2 Braket")

print("fidelity:",fid_hist[-1])
fig = plt.figure()
plt.plot([x for x in range(0,len(loss_hist)*5,5)],np.array(loss_hist),label="train loss")
plt.plot([x for x in range(0,len(loss_hist)*5,5)],np.array(loss_hist_test),label="test loss")


plt.legend()
plt.title("Fraud 4-3-4 Compression Loss e2",)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.savefig("Fraud 4-3-4 Compression Loss e2")

print("loss:",loss_hist[-1])
name = "fraud_434_training_e2"

Circuit_prop={   "shots":shots, "nr_trash":nr_trash, "nr_latent":nr_latent, "nr_ent":nr_ent  }
Training_param = { "num_samples" == num_samples,
                    "batch_size" == batch_size,
                    #"nr_layers"== nr_layers,
                    "epochs" == epochs,
                    "learning_rate" == learning_rate, 
                    "beta1" == beta1,
                    "beta2 "== beta2,
                     "optimizer"=="Adam"}

performance={"loss_hist":loss_hist, "fid_hist":fid_hist,
             "loss_hist_test":loss_hist_test, "fid_hist_test":fid_hist_test,
             "encoder_params":encoder_params}

experiment_data={"Circuit_prop":Circuit_prop,
                "Training_param":Training_param,
                "performance:":performance,
                "Name":name}

# open file for writing
f = open(name+".txt","w")
f.write( str(experiment_data) )
experiment_parameters={"autoencoder":"e2","params":encoder_params}
f=open("Params_Fraud_encoder_e2-CorrelatedFeautures.txt","w")
f.write(str(experiment_parameters))
branch = df 
non_fraud_df = branch.loc[branch["Class"]!=1][:200]


non_fraud = non_fraud_df[feature_list]
np_non_fraud = non_fraud.to_numpy()
non_fraud_data = [ torch.tensor([np_non_fraud[i]]) for i in range(len(non_fraud.to_numpy()))]

non_fraud_flist=[]
for b in non_fraud_data:
    f=fidelity(encoder_params, [b])
    non_fraud_flist.append(f.item())
    
print(min(non_fraud_flist))
print(max(non_fraud_flist))
#np_ilegal= ilegal.to_numpy()
#ilegal_data = [ torch.tensor([np_ilegal[i]]) for i in range(len(ilegal.to_numpy()))]
fraud_df = branch.loc[branch["Class"]==1][:200]
non_fraud = fraud_df[feature_list]
np_non_fraud = non_fraud.to_numpy()
fraud_data = [ torch.tensor([np_non_fraud[i]]) for i in range(len(non_fraud.to_numpy()))]



fraud_flist=[]
for b in fraud_data:
    f=fidelity(encoder_params, [b])
    fraud_flist.append(f.item())
    
print(min(fraud_flist))
print(max(fraud_flist))

plt.hist(non_fraud_flist, bins =100 ,label="non_fraud",color = "red",alpha=0.4)
plt.hist(fraud_flist, bins = 100 ,label="fraud", color = "skyblue",alpha=0.4)
plt.title("Compression fidelity",)
plt.legend()
plt.savefig("Compression_fidelity")
plt.show()
split=0.75


print("split:",split)
b_e=[]
for i in fraud_flist:
    if i<split:
        b_e.append(1)
    else:
        b_e.append(0)
ab_ac=sum(b_e)/len(b_e)
print("benign classification accuracy:",ab_ac)
m_e=[]
for i in non_fraud_flist:
    if i>split:
        m_e.append(1)
    else:
        m_e.append(0)
am_ac=sum(m_e)/len(m_e)
print("malignification accuracy:",am_ac)
t_ac=(sum(b_e)+sum(m_e))/(len(b_e)+len(m_e))
print("total accuracy:",t_ac)<jupyter_output>split: 0.75
benign classification accuracy: 0.83
malignification accuracy: 0.93
total accuracy: 0.88
{'fraction_non_alphanumeric': 0.11685421046123408, 'fraction_numerical': 0.08352912081117843, 'mean_word_length': 4.2244832041343665, 'pattern_counts': {'":': 15, '<': 32, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '508239', 'license': 'MIT', 'n_tokens_mistral': 6598, 'n_tokens_neox': 5640, 'n_words': 1362, 'path': 'Use-case_Fraud_detection/QuantumCreditFraud-best_pre_Braket.ipynb', 'repo_name': 'VoicuTomut/Autoencoders---Anomaly-detection---Entanglement-enhanced-attempts-to-improve-results', 'size': '16174'}