Prabuddha21/encrypted_credit_scoring
0
1"""Backend functions used in the app."""2 3import os4import shutil5import gradio as gr6import numpy7import requests8import pickle9import pandas10from itertools import chain11 12from settings import (13 SERVER_URL,14 FHE_KEYS,15 CLIENT_FILES,16 SERVER_FILES,17 DEPLOYMENT_PATH,18 PROCESSED_INPUT_SHAPE,19 INPUT_INDEXES,20 INPUT_SLICES,21 PRE_PROCESSOR_APPLICANT_PATH, 22 PRE_PROCESSOR_BANK_PATH,23 PRE_PROCESSOR_CREDIT_BUREAU_PATH,24 CLIENT_TYPES,25 APPLICANT_COLUMNS,26 BANK_COLUMNS,27 CREDIT_BUREAU_COLUMNS,28 YEARS_EMPLOYED_BINS,29 YEARS_EMPLOYED_BIN_NAME_TO_INDEX,30)31 32from utils.client_server_interface import MultiInputsFHEModelClient33 34# Define the messages associated to the predictions35APPROVED_MESSAGE = "Credit card is likely to be approved ✅"36DENIED_MESSAGE = "Credit card is likely to be denied ❌"37 38# Load pre-processor instances39with (40 PRE_PROCESSOR_APPLICANT_PATH.open('rb') as file_applicant, 41 PRE_PROCESSOR_BANK_PATH.open('rb') as file_bank,42 PRE_PROCESSOR_CREDIT_BUREAU_PATH.open('rb') as file_credit_bureau,43):44 PRE_PROCESSOR_APPLICANT = pickle.load(file_applicant)45 PRE_PROCESSOR_BANK = pickle.load(file_bank)46 PRE_PROCESSOR_CREDIT_BUREAU = pickle.load(file_credit_bureau)47 48 49def shorten_bytes_object(bytes_object, limit=500):50 """Shorten the input bytes object to a given length.51 52 Encrypted data is too large for displaying it in the browser using Gradio. This function53 provides a shorten representation of it.54 55 Args:56 bytes_object (bytes): The input to shorten57 limit (int): The length to consider. Default to 500.58 59 Returns:60 str: Hexadecimal string shorten representation of the input byte object. 61 62 """63 # Define a shift for better display64 shift = 10065 return bytes_object[shift : limit + shift].hex()66 67 68def clean_temporary_files(n_keys=20):69 """Clean older keys and encrypted files.70 71 A maximum of n_keys keys and associated temporary files are allowed to be stored. Once this 72 limit is reached, the oldest files are deleted.73 74 Args:75 n_keys (int): The maximum number of keys and associated files to be stored. Default to 20.76 77 """78 # Get the oldest key files in the key directory79 key_dirs = sorted(FHE_KEYS.iterdir(), key=os.path.getmtime)80 81 # If more than n_keys keys are found, remove the oldest82 client_ids = []83 if len(key_dirs) > n_keys:84 n_keys_to_delete = len(key_dirs) - n_keys85 for key_dir in key_dirs[:n_keys_to_delete]:86 client_ids.append(key_dir.name)87 shutil.rmtree(key_dir)88 89 # Delete all files related to the IDs whose keys were deleted90 for directory in chain(CLIENT_FILES.iterdir(), SERVER_FILES.iterdir()):91 for client_id in client_ids:92 if client_id in directory.name:93 shutil.rmtree(directory)94 95 96def _get_client(client_id):97 """Get the client instance.98 99 Args:100 client_id (int): The client ID to consider.101 102 Returns:103 FHEModelClient: The client instance.104 """105 key_dir = FHE_KEYS / f"{client_id}"106 107 return MultiInputsFHEModelClient(DEPLOYMENT_PATH, key_dir=key_dir, nb_inputs=len(CLIENT_TYPES))108 109 110def _get_client_file_path(name, client_id, client_type=None):111 """Get the file path for the client.112 113 Args:114 name (str): The desired file name (either 'evaluation_key', 'encrypted_inputs' or 115 'encrypted_outputs').116 client_id (int): The client ID to consider.117 client_type (Optional[str]): The type of client to consider (either 'applicant', 'bank', 118 'credit_bureau' or None). Default to None, which is used for evaluation key and output.119 120 Returns:121 pathlib.Path: The file path.122 """123 client_type_suffix = "" 124 if client_type is not None:125 client_type_suffix = f"_{client_type}"126 127 dir_path = CLIENT_FILES / f"{client_id}"128 dir_path.mkdir(exist_ok=True)129 130 return dir_path / f"{name}{client_type_suffix}"131 132 133def _send_to_server(client_id, client_type, file_name):134 """Send the encrypted inputs or the evaluation key to the server.135 136 Args:137 client_id (int): The client ID to consider.138 client_type (Optional[str]): The type of client to consider (either 'applicant', 'bank', 139 'credit_bureau' or None).140 file_name (str): File name to send (either 'evaluation_key' or 'encrypted_inputs').141 """142 # Get the paths to the encrypted inputs143 encrypted_file_path = _get_client_file_path(file_name, client_id, client_type)144 145 # Define the data and files to post146 data = {147 "client_id": client_id,148 "client_type": client_type,149 "file_name": file_name,150 }151 152 files = [153 ("files", open(encrypted_file_path, "rb")),154 ]155 156 # Send the encrypted inputs or evaluation key to the server157 url = SERVER_URL + "send_file"158 with requests.post(159 url=url,160 data=data,161 files=files,162 ) as response:163 return response.ok164 165 166def keygen_send():167 """Generate the private and evaluation key, and send the evaluation key to the server.168 169 Returns:170 client_id (str): The current client ID to consider.171 """172 # Clean temporary files173 clean_temporary_files()174 175 # Create an ID for the current client to consider176 client_id = numpy.random.randint(0, 2**32)177 178 # Retrieve the client instance179 client = _get_client(client_id)180 181 # Generate the private and evaluation keys182 client.generate_private_and_evaluation_keys(force=True)183 184 # Retrieve the serialized evaluation key185 evaluation_key = client.get_serialized_evaluation_keys()186 187 file_name = "evaluation_key"188 189 # Save evaluation key as bytes in a file as it is too large to pass through regular Gradio190 # buttons (see https://github.com/gradio-app/gradio/issues/1877)191 evaluation_key_path = _get_client_file_path(file_name, client_id)192 193 with evaluation_key_path.open("wb") as evaluation_key_file:194 evaluation_key_file.write(evaluation_key)195 196 # Send the evaluation key to the server197 _send_to_server(client_id, None, file_name)198 199 # Create a truncated version of the evaluation key for display200 evaluation_key_short = shorten_bytes_object(evaluation_key)201 202 return client_id, evaluation_key_short, gr.update(value="Keys are generated and evaluation key is sent ✅")203 204 205def _encrypt_send(client_id, inputs, client_type):206 """Encrypt the given inputs for a specific client and send it to the server.207 208 Args:209 client_id (str): The current client ID to consider.210 inputs (numpy.ndarray): The inputs to encrypt.211 client_type (str): The type of client to consider (either 'applicant', 'bank' or 212 'credit_bureau').213 214 Returns:215 encrypted_inputs_short (str): A short representation of the encrypted input to send in hex. 216 """217 if client_id == "":218 raise gr.Error("Please generate the keys first.")219 220 # Retrieve the client instance221 client = _get_client(client_id)222 223 # Quantize, encrypt and serialize the inputs224 encrypted_inputs = client.quantize_encrypt_serialize_multi_inputs(225 inputs, 226 input_index=INPUT_INDEXES[client_type], 227 processed_input_shape=PROCESSED_INPUT_SHAPE, 228 input_slice=INPUT_SLICES[client_type],229 )230 231 file_name = "encrypted_inputs"232 233 # Save encrypted_inputs to bytes in a file, since too large to pass through regular Gradio234 # buttons, https://github.com/gradio-app/gradio/issues/1877235 encrypted_inputs_path = _get_client_file_path(file_name, client_id, client_type)236 237 with encrypted_inputs_path.open("wb") as encrypted_inputs_file:238 encrypted_inputs_file.write(encrypted_inputs)239 240 # Create a truncated version of the encrypted inputs for display241 encrypted_inputs_short = shorten_bytes_object(encrypted_inputs)242 243 _send_to_server(client_id, client_type, file_name)244 245 return encrypted_inputs_short, gr.update(value="Inputs are encrypted and sent to server. ✅")246 247 248def pre_process_encrypt_send_applicant(client_id, *inputs):249 """Pre-process, encrypt and send the applicant inputs for a specific client to the server.250 251 Args:252 client_id (str): The current client ID to consider.253 *inputs (Tuple[numpy.ndarray]): The inputs to pre-process.254 255 Returns:256 (str): A short representation of the encrypted input to send in hex. 257 """258 bool_inputs, num_children, household_size, total_income, age, income_type, education_type, \259 family_status, occupation_type, housing_type = inputs260 261 # Retrieve boolean values262 own_car = "Car" in bool_inputs263 own_property = "Property" in bool_inputs264 mobile_phone = "Mobile phone" in bool_inputs265 266 applicant_inputs = pandas.DataFrame({267 "Own_car": [own_car],268 "Own_property": [own_property],269 "Mobile_phone": [mobile_phone],270 "Num_children": [num_children],271 "Household_size": [household_size],272 "Total_income": [total_income],273 "Age": [age],274 "Income_type": [income_type],275 "Education_type": [education_type],276 "Family_status": [family_status],277 "Occupation_type": [occupation_type],278 "Housing_type": [housing_type],279 })280 281 applicant_inputs = applicant_inputs.reindex(APPLICANT_COLUMNS, axis=1)282 283 preprocessed_applicant_inputs = PRE_PROCESSOR_APPLICANT.transform(applicant_inputs)284 285 return _encrypt_send(client_id, preprocessed_applicant_inputs, "applicant")286 287 288def pre_process_encrypt_send_bank(client_id, *inputs):289 """Pre-process, encrypt and send the bank inputs for a specific client to the server.290 291 Args: 292 client_id (str): The current client ID to consider.293 *inputs (Tuple[numpy.ndarray]): The inputs to pre-process.294 295 Returns:296 (str): A short representation of the encrypted input to send in hex. 297 """298 account_age = inputs[0]299 300 bank_inputs = pandas.DataFrame({301 "Account_age": [account_age],302 })303 304 bank_inputs = bank_inputs.reindex(BANK_COLUMNS, axis=1)305 306 preprocessed_bank_inputs = PRE_PROCESSOR_BANK.transform(bank_inputs)307 308 return _encrypt_send(client_id, preprocessed_bank_inputs, "bank")309 310 311def pre_process_encrypt_send_credit_bureau(client_id, *inputs):312 """Pre-process, encrypt and send the credit bureau inputs for a specific client to the server.313 314 Args:315 client_id (str): The current client ID to consider.316 *inputs (Tuple[numpy.ndarray]): The inputs to pre-process.317 318 Returns:319 (str): A short representation of the encrypted input to send in hex. 320 """321 years_employed_bin, employed = inputs322 323 years_employed = YEARS_EMPLOYED_BIN_NAME_TO_INDEX[years_employed_bin]324 is_employed = employed == "Yes"325 326 credit_bureau_inputs = pandas.DataFrame({327 "Years_employed": [years_employed],328 "Employed": [is_employed],329 })330 331 credit_bureau_inputs = credit_bureau_inputs.reindex(CREDIT_BUREAU_COLUMNS, axis=1)332 preprocessed_credit_bureau_inputs = PRE_PROCESSOR_CREDIT_BUREAU.transform(credit_bureau_inputs)333 334 return _encrypt_send(client_id, preprocessed_credit_bureau_inputs, "credit_bureau")335 336 337def run_fhe(client_id):338 """Run the model on the encrypted inputs previously sent using FHE.339 340 Args:341 client_id (str): The current client ID to consider.342 """343 344 if client_id == "":345 raise gr.Error("Please generate the keys first.")346 347 data = {348 "client_id": client_id,349 }350 351 # Trigger the FHE execution on the encrypted inputs previously sent352 url = SERVER_URL + "run_fhe"353 with requests.post(354 url=url,355 data=data,356 ) as response:357 if response.ok:358 return response.json(), gr.update(value="FHE evaluation is done. ✅")359 else:360 raise gr.Error("Please send the inputs from all three parties to the server first.")361 362 363def get_output_and_decrypt(client_id):364 """Retrieve the encrypted output.365 366 Args:367 client_id (str): The current client ID to consider.368 369 Returns:370 (Tuple[str, bytes]): The output message based on the decrypted prediction as well as 371 a byte short representation of the encrypted output. 372 """373 374 if client_id == "":375 raise gr.Error("Please generate the keys first.")376 377 data = {378 "client_id": client_id,379 }380 381 # Retrieve the encrypted output382 url = SERVER_URL + "get_output"383 with requests.post(384 url=url,385 data=data,386 ) as response:387 if response.ok:388 encrypted_output_proba = response.content389 390 # Create a truncated version of the encrypted inputs for display391 encrypted_output_short = shorten_bytes_object(encrypted_output_proba)392 393 # Retrieve the client API394 client = _get_client(client_id)395 396 # Deserialize, decrypt and post-process the encrypted output397 output_proba = client.deserialize_decrypt_dequantize(encrypted_output_proba)398 399 # Determine the predicted class400 output = numpy.argmax(output_proba, axis=1).squeeze()401 402 return (403 APPROVED_MESSAGE if output == 1 else DENIED_MESSAGE,404 encrypted_output_short,405 gr.update(value="Encrypted outputs have been received from the server. ✅"),406 )407 408 else:409 raise gr.Error("Please run the FHE execution first and wait for it to be completed.")410 411 412def explain_encrypt_run_decrypt(client_id, prediction_output, *inputs):413 """Pre-process and encrypt the inputs, run the prediction in FHE and decrypt the output. 414 415 Args:416 client_id (str): The current client ID to consider.417 prediction_output (str): The initial prediction output. This parameter is only used to 418 throw an error in case the prediction was positive. 419 *inputs (Tuple[numpy.ndarray]): The inputs to consider.420 421 Returns:422 (str): A message indicating the number of additional years of employment that could be 423 required in order to increase the chance of credit card approval.424 """425 426 if "approved" in prediction_output:427 raise gr.Error(428 "Explaining the prediction can only be done if the credit card is likely to be denied."429 )430 431 button_update = gr.update(value="Prediction has been explained. ✅")432 433 # Retrieve the credit bureau inputs434 years_employed, employed = inputs435 436 # Years_employed is divided into several ordered bins. Here, we retrieve the index representing 437 # the bin from the input438 bin_index = YEARS_EMPLOYED_BIN_NAME_TO_INDEX[years_employed]439 440 # If the bin is not the last (representing the most years of employment), we run the model in 441 # FHE for each bins "older" or equal to the given bin, in order. Then, we retrieve the first 442 # bin that changes the model's prediction to "approval" and display it to the applicant. 443 if bin_index != len(YEARS_EMPLOYED_BINS) - 1:444 445 # Loop over the bins starting with "older" or equal to the given bin446 for years_employed_bin in YEARS_EMPLOYED_BINS[bin_index:]:447 448 # Send the new encrypted input449 pre_process_encrypt_send_credit_bureau(client_id, years_employed_bin, employed)450 451 # Run the model in FHE452 run_fhe(client_id)453 454 # Retrieve the new prediction455 output_prediction = get_output_and_decrypt(client_id)456 457 # If the bin made the model predict an approval, share it to the applicant 458 if "approved" in output_prediction[0]:459 460 # If the approval was made using the given input, that means the applicant most 461 # likely tried the bin suggested in a previous explainability run. In that case, we 462 # confirm that the credit card is likely to be approved463 if years_employed_bin == years_employed:464 return APPROVED_MESSAGE, button_update465 466 # Else, that means the applicant is looking for some explainability. We therefore 467 # suggest to try the obtained bin468 return (469 DENIED_MESSAGE + f" However, having at least {years_employed_bin} years of "470 "employment would increase your chance of having your credit card approved."471 ), button_update472 473 # In case no bins made the model predict an approval, explain why474 return (475 DENIED_MESSAGE + " Unfortunately, increasing the number of years of employment up to "476 f"{YEARS_EMPLOYED_BINS[-1]} years does not seem to be enough to get an approval based "477 "on the given inputs. Other inputs like the income or the account's age might have "478 "bigger impact in this particular case."479 ), button_update480 481 # In case the applicant tried the "oldest" bin (but still got denied), explain why482 return (483 DENIED_MESSAGE + " Unfortunately, you already have the maximum amount of years of "484 f"employment ({years_employed} years). Other inputs like the income or the account's age "485 "might have a bigger impact in this particular case."486 ), button_update487 