CoolFace
Apppublic

Prabuddha21/encrypted_credit_scoring

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
backend.py487 linesDownload Raw Back to root
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