Commit
·
8e0d56d
1
Parent(s):
61cd73f
Rename to applicant and credit bureau
Browse files- app.py +27 -26
- backend.py +34 -33
- deployment_files/model/{pre_processor_user.pkl → pre_processor_applicant.pkl} +0 -0
- deployment_files/model/{pre_processor_cs_agency.pkl → pre_processor_credit_bureau.pkl} +0 -0
- deployment_files/{pre_processor_user.pkl → pre_processor_applicant.pkl} +0 -0
- deployment_files/{pre_processor_cs_agency.pkl → pre_processor_credit_bureau.pkl} +0 -0
- development.py +16 -16
- server.py +2 -2
- settings.py +10 -10
- utils/client_server_interface.py +2 -2
- utils/pre_processing.py +3 -3
app.py
CHANGED
|
@@ -22,9 +22,9 @@ from settings import (
|
|
| 22 |
)
|
| 23 |
from backend import (
|
| 24 |
keygen_send,
|
| 25 |
-
|
| 26 |
pre_process_encrypt_send_bank,
|
| 27 |
-
|
| 28 |
run_fhe,
|
| 29 |
get_output_and_decrypt,
|
| 30 |
explain_encrypt_run_decrypt,
|
|
@@ -61,7 +61,7 @@ with demo:
|
|
| 61 |
"""
|
| 62 |
)
|
| 63 |
|
| 64 |
-
gr.Markdown("#
|
| 65 |
|
| 66 |
gr.Markdown("## Step 1: Generate the keys.")
|
| 67 |
gr.Markdown(
|
|
@@ -90,10 +90,11 @@ with demo:
|
|
| 90 |
"""
|
| 91 |
Select the information that corresponds to the profile you want to evaluate. Three sources
|
| 92 |
of information are represented in this model:
|
| 93 |
-
-
|
| 94 |
-
- the
|
| 95 |
-
banking information relevant to the decision (here, we consider duration of
|
| 96 |
-
|
|
|
|
| 97 |
employment history) that could provide additional insight relevant to the decision.
|
| 98 |
|
| 99 |
Please always encrypt and send the values (through the buttons on the right) once updated
|
|
@@ -103,7 +104,7 @@ with demo:
|
|
| 103 |
|
| 104 |
with gr.Row():
|
| 105 |
with gr.Column():
|
| 106 |
-
gr.Markdown("###
|
| 107 |
bool_inputs = gr.CheckboxGroup(
|
| 108 |
["Car", "Property", "Mobile phone"],
|
| 109 |
label="Which of the following do you actively hold or own?"
|
|
@@ -167,15 +168,15 @@ with demo:
|
|
| 167 |
)
|
| 168 |
|
| 169 |
with gr.Column():
|
| 170 |
-
|
| 171 |
|
| 172 |
-
|
| 173 |
label="Encrypted input representation:", max_lines=2, interactive=False
|
| 174 |
)
|
| 175 |
|
| 176 |
with gr.Row():
|
| 177 |
with gr.Column(scale=2):
|
| 178 |
-
gr.Markdown("### Bank ")
|
| 179 |
account_age = gr.Slider(
|
| 180 |
**ACCOUNT_MIN_MAX,
|
| 181 |
step=1,
|
|
@@ -192,7 +193,7 @@ with demo:
|
|
| 192 |
|
| 193 |
with gr.Row():
|
| 194 |
with gr.Column(scale=2):
|
| 195 |
-
gr.Markdown("### Credit
|
| 196 |
employed = gr.Radio(["Yes", "No"], label="Is the person employed ?", value="Yes")
|
| 197 |
years_employed = gr.Dropdown(
|
| 198 |
choices=YEARS_EMPLOYED_BINS,
|
|
@@ -202,19 +203,19 @@ with demo:
|
|
| 202 |
)
|
| 203 |
|
| 204 |
with gr.Column():
|
| 205 |
-
|
| 206 |
|
| 207 |
-
|
| 208 |
label="Encrypted input representation:", max_lines=2, interactive=False
|
| 209 |
)
|
| 210 |
|
| 211 |
-
# Button to pre-process, generate the key, encrypt and send the
|
| 212 |
# side to the server
|
| 213 |
-
|
| 214 |
-
|
| 215 |
inputs=[client_id, bool_inputs, num_children, household_size, total_income, age, \
|
| 216 |
income_type, education_type, family_status, occupation_type, housing_type],
|
| 217 |
-
outputs=[
|
| 218 |
)
|
| 219 |
|
| 220 |
# Button to pre-process, generate the key, encrypt and send the bank inputs from the client
|
|
@@ -225,12 +226,12 @@ with demo:
|
|
| 225 |
outputs=[encrypted_input_bank],
|
| 226 |
)
|
| 227 |
|
| 228 |
-
# Button to pre-process, generate the key, encrypt and send the credit
|
| 229 |
# client side to the server
|
| 230 |
-
|
| 231 |
-
|
| 232 |
inputs=[client_id, years_employed, employed],
|
| 233 |
-
outputs=[
|
| 234 |
)
|
| 235 |
|
| 236 |
gr.Markdown("# Server side")
|
|
@@ -253,10 +254,10 @@ with demo:
|
|
| 253 |
# Button to send the encodings to the server using post method
|
| 254 |
execute_fhe_button.click(run_fhe, inputs=[client_id], outputs=[fhe_execution_time])
|
| 255 |
|
| 256 |
-
gr.Markdown("#
|
| 257 |
gr.Markdown(
|
| 258 |
"""
|
| 259 |
-
Once the server completed the inference, the encrypted output is returned to the
|
| 260 |
|
| 261 |
The three entities that provide the information to compute the credit score are the only
|
| 262 |
ones that can decrypt the result. They take part in a decryption protocol that allows to
|
|
@@ -269,7 +270,7 @@ with demo:
|
|
| 269 |
"""
|
| 270 |
The first value displayed below is a shortened byte representation of the actual encrypted
|
| 271 |
output.
|
| 272 |
-
The
|
| 273 |
"""
|
| 274 |
)
|
| 275 |
|
|
@@ -291,7 +292,7 @@ with demo:
|
|
| 291 |
gr.Markdown("## Step 6 (optional): Explain the prediction.")
|
| 292 |
gr.Markdown(
|
| 293 |
"""
|
| 294 |
-
In case the credit card is likely to be denied, the
|
| 295 |
employment would most likely be required in order to increase the chance of getting a
|
| 296 |
credit card approval.
|
| 297 |
|
|
|
|
| 22 |
)
|
| 23 |
from backend import (
|
| 24 |
keygen_send,
|
| 25 |
+
pre_process_encrypt_send_applicant,
|
| 26 |
pre_process_encrypt_send_bank,
|
| 27 |
+
pre_process_encrypt_send_credit_bureau,
|
| 28 |
run_fhe,
|
| 29 |
get_output_and_decrypt,
|
| 30 |
explain_encrypt_run_decrypt,
|
|
|
|
| 61 |
"""
|
| 62 |
)
|
| 63 |
|
| 64 |
+
gr.Markdown("# Applicant, Bank and Credit bureau setup")
|
| 65 |
|
| 66 |
gr.Markdown("## Step 1: Generate the keys.")
|
| 67 |
gr.Markdown(
|
|
|
|
| 90 |
"""
|
| 91 |
Select the information that corresponds to the profile you want to evaluate. Three sources
|
| 92 |
of information are represented in this model:
|
| 93 |
+
- the applicant's personal information in order to evaluate his/her credit card eligibility;
|
| 94 |
+
- the applicant bank account history, which provides any type of information on the
|
| 95 |
+
applicant's banking information relevant to the decision (here, we consider duration of
|
| 96 |
+
account);
|
| 97 |
+
- and credit bureau information, which represents any other information (here,
|
| 98 |
employment history) that could provide additional insight relevant to the decision.
|
| 99 |
|
| 100 |
Please always encrypt and send the values (through the buttons on the right) once updated
|
|
|
|
| 104 |
|
| 105 |
with gr.Row():
|
| 106 |
with gr.Column():
|
| 107 |
+
gr.Markdown("### Applicant information")
|
| 108 |
bool_inputs = gr.CheckboxGroup(
|
| 109 |
["Car", "Property", "Mobile phone"],
|
| 110 |
label="Which of the following do you actively hold or own?"
|
|
|
|
| 168 |
)
|
| 169 |
|
| 170 |
with gr.Column():
|
| 171 |
+
encrypt_button_applicant = gr.Button("Encrypt the inputs and send to server.")
|
| 172 |
|
| 173 |
+
encrypted_input_applicant = gr.Textbox(
|
| 174 |
label="Encrypted input representation:", max_lines=2, interactive=False
|
| 175 |
)
|
| 176 |
|
| 177 |
with gr.Row():
|
| 178 |
with gr.Column(scale=2):
|
| 179 |
+
gr.Markdown("### Bank information")
|
| 180 |
account_age = gr.Slider(
|
| 181 |
**ACCOUNT_MIN_MAX,
|
| 182 |
step=1,
|
|
|
|
| 193 |
|
| 194 |
with gr.Row():
|
| 195 |
with gr.Column(scale=2):
|
| 196 |
+
gr.Markdown("### Credit bureau information ")
|
| 197 |
employed = gr.Radio(["Yes", "No"], label="Is the person employed ?", value="Yes")
|
| 198 |
years_employed = gr.Dropdown(
|
| 199 |
choices=YEARS_EMPLOYED_BINS,
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
with gr.Column():
|
| 206 |
+
encrypt_button_credit_bureau = gr.Button("Encrypt the inputs and send to server.")
|
| 207 |
|
| 208 |
+
encrypted_input_credit_bureau = gr.Textbox(
|
| 209 |
label="Encrypted input representation:", max_lines=2, interactive=False
|
| 210 |
)
|
| 211 |
|
| 212 |
+
# Button to pre-process, generate the key, encrypt and send the applicant inputs from the client
|
| 213 |
# side to the server
|
| 214 |
+
encrypt_button_applicant.click(
|
| 215 |
+
pre_process_encrypt_send_applicant,
|
| 216 |
inputs=[client_id, bool_inputs, num_children, household_size, total_income, age, \
|
| 217 |
income_type, education_type, family_status, occupation_type, housing_type],
|
| 218 |
+
outputs=[encrypted_input_applicant],
|
| 219 |
)
|
| 220 |
|
| 221 |
# Button to pre-process, generate the key, encrypt and send the bank inputs from the client
|
|
|
|
| 226 |
outputs=[encrypted_input_bank],
|
| 227 |
)
|
| 228 |
|
| 229 |
+
# Button to pre-process, generate the key, encrypt and send the credit bureau inputs from the
|
| 230 |
# client side to the server
|
| 231 |
+
encrypt_button_credit_bureau.click(
|
| 232 |
+
pre_process_encrypt_send_credit_bureau,
|
| 233 |
inputs=[client_id, years_employed, employed],
|
| 234 |
+
outputs=[encrypted_input_credit_bureau],
|
| 235 |
)
|
| 236 |
|
| 237 |
gr.Markdown("# Server side")
|
|
|
|
| 254 |
# Button to send the encodings to the server using post method
|
| 255 |
execute_fhe_button.click(run_fhe, inputs=[client_id], outputs=[fhe_execution_time])
|
| 256 |
|
| 257 |
+
gr.Markdown("# Applicant, Bank and Credit bureau decryption")
|
| 258 |
gr.Markdown(
|
| 259 |
"""
|
| 260 |
+
Once the server completed the inference, the encrypted output is returned to the applicant.
|
| 261 |
|
| 262 |
The three entities that provide the information to compute the credit score are the only
|
| 263 |
ones that can decrypt the result. They take part in a decryption protocol that allows to
|
|
|
|
| 270 |
"""
|
| 271 |
The first value displayed below is a shortened byte representation of the actual encrypted
|
| 272 |
output.
|
| 273 |
+
The applicant is then able to decrypt the value using its private key.
|
| 274 |
"""
|
| 275 |
)
|
| 276 |
|
|
|
|
| 292 |
gr.Markdown("## Step 6 (optional): Explain the prediction.")
|
| 293 |
gr.Markdown(
|
| 294 |
"""
|
| 295 |
+
In case the credit card is likely to be denied, the applicant can ask for how many years of
|
| 296 |
employment would most likely be required in order to increase the chance of getting a
|
| 297 |
credit card approval.
|
| 298 |
|
backend.py
CHANGED
|
@@ -18,13 +18,13 @@ from settings import (
|
|
| 18 |
PROCESSED_INPUT_SHAPE,
|
| 19 |
INPUT_INDEXES,
|
| 20 |
INPUT_SLICES,
|
| 21 |
-
|
| 22 |
PRE_PROCESSOR_BANK_PATH,
|
| 23 |
-
|
| 24 |
CLIENT_TYPES,
|
| 25 |
-
|
| 26 |
BANK_COLUMNS,
|
| 27 |
-
|
| 28 |
YEARS_EMPLOYED_BINS,
|
| 29 |
YEARS_EMPLOYED_BIN_NAME_TO_INDEX,
|
| 30 |
)
|
|
@@ -37,13 +37,13 @@ DENIED_MESSAGE = "Credit card is likely to be denied ❌"
|
|
| 37 |
|
| 38 |
# Load pre-processor instances
|
| 39 |
with (
|
| 40 |
-
|
| 41 |
PRE_PROCESSOR_BANK_PATH.open('rb') as file_bank,
|
| 42 |
-
|
| 43 |
):
|
| 44 |
-
|
| 45 |
PRE_PROCESSOR_BANK = pickle.load(file_bank)
|
| 46 |
-
|
| 47 |
|
| 48 |
|
| 49 |
def shorten_bytes_object(bytes_object, limit=500):
|
|
@@ -114,8 +114,8 @@ def _get_client_file_path(name, client_id, client_type=None):
|
|
| 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
|
| 118 |
-
'
|
| 119 |
|
| 120 |
Returns:
|
| 121 |
pathlib.Path: The file path.
|
|
@@ -135,8 +135,8 @@ def _send_to_server(client_id, client_type, file_name):
|
|
| 135 |
|
| 136 |
Args:
|
| 137 |
client_id (int): The client ID to consider.
|
| 138 |
-
client_type (Optional[str]): The type of client to consider (either '
|
| 139 |
-
'
|
| 140 |
file_name (str): File name to send (either 'evaluation_key' or 'encrypted_inputs').
|
| 141 |
"""
|
| 142 |
# Get the paths to the encrypted inputs
|
|
@@ -208,7 +208,8 @@ def _encrypt_send(client_id, inputs, client_type):
|
|
| 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 '
|
|
|
|
| 212 |
|
| 213 |
Returns:
|
| 214 |
encrypted_inputs_short (str): A short representation of the encrypted input to send in hex.
|
|
@@ -244,8 +245,8 @@ def _encrypt_send(client_id, inputs, client_type):
|
|
| 244 |
return encrypted_inputs_short
|
| 245 |
|
| 246 |
|
| 247 |
-
def
|
| 248 |
-
"""Pre-process, encrypt and send the
|
| 249 |
|
| 250 |
Args:
|
| 251 |
client_id (str): The current client ID to consider.
|
|
@@ -262,7 +263,7 @@ def pre_process_encrypt_send_user(client_id, *inputs):
|
|
| 262 |
own_property = "Property" in bool_inputs
|
| 263 |
mobile_phone = "Mobile phone" in bool_inputs
|
| 264 |
|
| 265 |
-
|
| 266 |
"Own_car": [own_car],
|
| 267 |
"Own_property": [own_property],
|
| 268 |
"Mobile_phone": [mobile_phone],
|
|
@@ -277,11 +278,11 @@ def pre_process_encrypt_send_user(client_id, *inputs):
|
|
| 277 |
"Housing_type": [housing_type],
|
| 278 |
})
|
| 279 |
|
| 280 |
-
|
| 281 |
|
| 282 |
-
|
| 283 |
|
| 284 |
-
return _encrypt_send(client_id,
|
| 285 |
|
| 286 |
|
| 287 |
def pre_process_encrypt_send_bank(client_id, *inputs):
|
|
@@ -307,8 +308,8 @@ def pre_process_encrypt_send_bank(client_id, *inputs):
|
|
| 307 |
return _encrypt_send(client_id, preprocessed_bank_inputs, "bank")
|
| 308 |
|
| 309 |
|
| 310 |
-
def
|
| 311 |
-
"""Pre-process, encrypt and send the credit
|
| 312 |
|
| 313 |
Args:
|
| 314 |
client_id (str): The current client ID to consider.
|
|
@@ -322,15 +323,15 @@ def pre_process_encrypt_send_cs_agency(client_id, *inputs):
|
|
| 322 |
years_employed = YEARS_EMPLOYED_BIN_NAME_TO_INDEX[years_employed_bin]
|
| 323 |
is_employed = employed == "Yes"
|
| 324 |
|
| 325 |
-
|
| 326 |
"Years_employed": [years_employed],
|
| 327 |
"Employed": [is_employed],
|
| 328 |
})
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
|
| 333 |
-
return _encrypt_send(client_id,
|
| 334 |
|
| 335 |
|
| 336 |
def run_fhe(client_id):
|
|
@@ -426,7 +427,7 @@ def explain_encrypt_run_decrypt(client_id, prediction_output, *inputs):
|
|
| 426 |
"Explaining the prediction can only be done if the credit card is likely to be denied."
|
| 427 |
)
|
| 428 |
|
| 429 |
-
# Retrieve the credit
|
| 430 |
years_employed, employed = inputs
|
| 431 |
|
| 432 |
# Years_employed is divided into several ordered bins. Here, we retrieve the index representing
|
|
@@ -435,14 +436,14 @@ def explain_encrypt_run_decrypt(client_id, prediction_output, *inputs):
|
|
| 435 |
|
| 436 |
# If the bin is not the last (representing the most years of employment), we run the model in
|
| 437 |
# FHE for each bins "older" or equal to the given bin, in order. Then, we retrieve the first
|
| 438 |
-
# bin that changes the model's prediction to "approval" and display it to the
|
| 439 |
if bin_index != len(YEARS_EMPLOYED_BINS) - 1:
|
| 440 |
|
| 441 |
# Loop over the bins starting with "older" or equal to the given bin
|
| 442 |
for years_employed_bin in YEARS_EMPLOYED_BINS[bin_index:]:
|
| 443 |
|
| 444 |
# Send the new encrypted input
|
| 445 |
-
|
| 446 |
|
| 447 |
# Run the model in FHE
|
| 448 |
run_fhe(client_id)
|
|
@@ -450,16 +451,16 @@ def explain_encrypt_run_decrypt(client_id, prediction_output, *inputs):
|
|
| 450 |
# Retrieve the new prediction
|
| 451 |
output_prediction = get_output_and_decrypt(client_id)
|
| 452 |
|
| 453 |
-
# If the bin made the model predict an approval, share it to the
|
| 454 |
if "approved" in output_prediction[0]:
|
| 455 |
|
| 456 |
-
# If the approval was made using the given input, that means the
|
| 457 |
-
# tried the bin suggested in a previous explainability run. In that case, we
|
| 458 |
# confirm that the credit card is likely to be approved
|
| 459 |
if years_employed_bin == years_employed:
|
| 460 |
return APPROVED_MESSAGE
|
| 461 |
|
| 462 |
-
# Else, that means the
|
| 463 |
# suggest to try the obtained bin
|
| 464 |
return (
|
| 465 |
DENIED_MESSAGE + f" However, having at least {years_employed_bin} years of "
|
|
@@ -474,7 +475,7 @@ def explain_encrypt_run_decrypt(client_id, prediction_output, *inputs):
|
|
| 474 |
"bigger impact in this particular case."
|
| 475 |
)
|
| 476 |
|
| 477 |
-
# In case the
|
| 478 |
return (
|
| 479 |
DENIED_MESSAGE + " Unfortunately, you already have the maximum amount of years of "
|
| 480 |
f"employment ({years_employed} years). Other inputs like the income or the account's age "
|
|
|
|
| 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 |
)
|
|
|
|
| 37 |
|
| 38 |
# Load pre-processor instances
|
| 39 |
with (
|
| 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 |
|
| 49 |
def shorten_bytes_object(bytes_object, limit=500):
|
|
|
|
| 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.
|
|
|
|
| 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 inputs
|
|
|
|
| 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.
|
|
|
|
| 245 |
return encrypted_inputs_short
|
| 246 |
|
| 247 |
|
| 248 |
+
def 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.
|
|
|
|
| 263 |
own_property = "Property" in bool_inputs
|
| 264 |
mobile_phone = "Mobile phone" in bool_inputs
|
| 265 |
|
| 266 |
+
applicant_inputs = pandas.DataFrame({
|
| 267 |
"Own_car": [own_car],
|
| 268 |
"Own_property": [own_property],
|
| 269 |
"Mobile_phone": [mobile_phone],
|
|
|
|
| 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 |
|
| 288 |
def pre_process_encrypt_send_bank(client_id, *inputs):
|
|
|
|
| 308 |
return _encrypt_send(client_id, preprocessed_bank_inputs, "bank")
|
| 309 |
|
| 310 |
|
| 311 |
+
def 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.
|
|
|
|
| 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 |
|
| 337 |
def run_fhe(client_id):
|
|
|
|
| 427 |
"Explaining the prediction can only be done if the credit card is likely to be denied."
|
| 428 |
)
|
| 429 |
|
| 430 |
+
# Retrieve the credit bureau inputs
|
| 431 |
years_employed, employed = inputs
|
| 432 |
|
| 433 |
# Years_employed is divided into several ordered bins. Here, we retrieve the index representing
|
|
|
|
| 436 |
|
| 437 |
# If the bin is not the last (representing the most years of employment), we run the model in
|
| 438 |
# FHE for each bins "older" or equal to the given bin, in order. Then, we retrieve the first
|
| 439 |
+
# bin that changes the model's prediction to "approval" and display it to the applicant.
|
| 440 |
if bin_index != len(YEARS_EMPLOYED_BINS) - 1:
|
| 441 |
|
| 442 |
# Loop over the bins starting with "older" or equal to the given bin
|
| 443 |
for years_employed_bin in YEARS_EMPLOYED_BINS[bin_index:]:
|
| 444 |
|
| 445 |
# Send the new encrypted input
|
| 446 |
+
pre_process_encrypt_send_credit_bureau(client_id, years_employed_bin, employed)
|
| 447 |
|
| 448 |
# Run the model in FHE
|
| 449 |
run_fhe(client_id)
|
|
|
|
| 451 |
# Retrieve the new prediction
|
| 452 |
output_prediction = get_output_and_decrypt(client_id)
|
| 453 |
|
| 454 |
+
# If the bin made the model predict an approval, share it to the applicant
|
| 455 |
if "approved" in output_prediction[0]:
|
| 456 |
|
| 457 |
+
# If the approval was made using the given input, that means the applicant most
|
| 458 |
+
# likely tried the bin suggested in a previous explainability run. In that case, we
|
| 459 |
# confirm that the credit card is likely to be approved
|
| 460 |
if years_employed_bin == years_employed:
|
| 461 |
return APPROVED_MESSAGE
|
| 462 |
|
| 463 |
+
# Else, that means the applicant is looking for some explainability. We therefore
|
| 464 |
# suggest to try the obtained bin
|
| 465 |
return (
|
| 466 |
DENIED_MESSAGE + f" However, having at least {years_employed_bin} years of "
|
|
|
|
| 475 |
"bigger impact in this particular case."
|
| 476 |
)
|
| 477 |
|
| 478 |
+
# In case the applicant tried the "oldest" bin (but still got denied), explain why
|
| 479 |
return (
|
| 480 |
DENIED_MESSAGE + " Unfortunately, you already have the maximum amount of years of "
|
| 481 |
f"employment ({years_employed} years). Other inputs like the income or the account's age "
|
deployment_files/model/{pre_processor_user.pkl → pre_processor_applicant.pkl}
RENAMED
|
File without changes
|
deployment_files/model/{pre_processor_cs_agency.pkl → pre_processor_credit_bureau.pkl}
RENAMED
|
File without changes
|
deployment_files/{pre_processor_user.pkl → pre_processor_applicant.pkl}
RENAMED
|
File without changes
|
deployment_files/{pre_processor_cs_agency.pkl → pre_processor_credit_bureau.pkl}
RENAMED
|
File without changes
|
development.py
CHANGED
|
@@ -9,12 +9,12 @@ from settings import (
|
|
| 9 |
DEPLOYMENT_PATH,
|
| 10 |
DATA_PATH,
|
| 11 |
INPUT_SLICES,
|
| 12 |
-
|
| 13 |
PRE_PROCESSOR_BANK_PATH,
|
| 14 |
-
|
| 15 |
-
|
| 16 |
BANK_COLUMNS,
|
| 17 |
-
|
| 18 |
)
|
| 19 |
from utils.client_server_interface import MultiInputsFHEModelDev
|
| 20 |
from utils.model import MultiInputDecisionTreeClassifier, MultiInputDecisionTreeRegressor
|
|
@@ -31,9 +31,9 @@ def get_multi_inputs(data):
|
|
| 31 |
(Tuple[numpy.ndarray]): The inputs for all three parties.
|
| 32 |
"""
|
| 33 |
return (
|
| 34 |
-
data[:, INPUT_SLICES["
|
| 35 |
data[:, INPUT_SLICES["bank"]],
|
| 36 |
-
data[:, INPUT_SLICES["
|
| 37 |
)
|
| 38 |
|
| 39 |
|
|
@@ -47,18 +47,18 @@ data_x = data.copy()
|
|
| 47 |
data_y = data_x.pop("Target").copy().to_frame()
|
| 48 |
|
| 49 |
# Get data from all parties
|
| 50 |
-
|
| 51 |
data_bank = data_x[BANK_COLUMNS].copy()
|
| 52 |
-
|
| 53 |
|
| 54 |
# Feature engineer the data
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
| 58 |
preprocessed_data_bank = pre_processor_bank.fit_transform(data_bank)
|
| 59 |
-
|
| 60 |
|
| 61 |
-
preprocessed_data_x = numpy.concatenate((
|
| 62 |
|
| 63 |
|
| 64 |
print("\nTrain and compile the model")
|
|
@@ -83,12 +83,12 @@ fhe_model_dev.save(via_mlir=True)
|
|
| 83 |
|
| 84 |
# Save pre-processors
|
| 85 |
with (
|
| 86 |
-
|
| 87 |
PRE_PROCESSOR_BANK_PATH.open('wb') as file_bank,
|
| 88 |
-
|
| 89 |
):
|
| 90 |
-
pickle.dump(
|
| 91 |
pickle.dump(pre_processor_bank, file_bank)
|
| 92 |
-
pickle.dump(
|
| 93 |
|
| 94 |
print("\nDone !")
|
|
|
|
| 9 |
DEPLOYMENT_PATH,
|
| 10 |
DATA_PATH,
|
| 11 |
INPUT_SLICES,
|
| 12 |
+
PRE_PROCESSOR_APPLICANT_PATH,
|
| 13 |
PRE_PROCESSOR_BANK_PATH,
|
| 14 |
+
PRE_PROCESSOR_CREDIT_BUREAU_PATH,
|
| 15 |
+
APPLICANT_COLUMNS,
|
| 16 |
BANK_COLUMNS,
|
| 17 |
+
CREDIT_BUREAU_COLUMNS,
|
| 18 |
)
|
| 19 |
from utils.client_server_interface import MultiInputsFHEModelDev
|
| 20 |
from utils.model import MultiInputDecisionTreeClassifier, MultiInputDecisionTreeRegressor
|
|
|
|
| 31 |
(Tuple[numpy.ndarray]): The inputs for all three parties.
|
| 32 |
"""
|
| 33 |
return (
|
| 34 |
+
data[:, INPUT_SLICES["applicant"]],
|
| 35 |
data[:, INPUT_SLICES["bank"]],
|
| 36 |
+
data[:, INPUT_SLICES["credit_bureau"]]
|
| 37 |
)
|
| 38 |
|
| 39 |
|
|
|
|
| 47 |
data_y = data_x.pop("Target").copy().to_frame()
|
| 48 |
|
| 49 |
# Get data from all parties
|
| 50 |
+
data_applicant = data_x[APPLICANT_COLUMNS].copy()
|
| 51 |
data_bank = data_x[BANK_COLUMNS].copy()
|
| 52 |
+
data_credit_bureau = data_x[CREDIT_BUREAU_COLUMNS].copy()
|
| 53 |
|
| 54 |
# Feature engineer the data
|
| 55 |
+
pre_processor_applicant, pre_processor_bank, pre_processor_credit_bureau = get_pre_processors()
|
| 56 |
|
| 57 |
+
preprocessed_data_applicant = pre_processor_applicant.fit_transform(data_applicant)
|
| 58 |
preprocessed_data_bank = pre_processor_bank.fit_transform(data_bank)
|
| 59 |
+
preprocessed_data_credit_bureau = pre_processor_credit_bureau.fit_transform(data_credit_bureau)
|
| 60 |
|
| 61 |
+
preprocessed_data_x = numpy.concatenate((preprocessed_data_applicant, preprocessed_data_bank, preprocessed_data_credit_bureau), axis=1)
|
| 62 |
|
| 63 |
|
| 64 |
print("\nTrain and compile the model")
|
|
|
|
| 83 |
|
| 84 |
# Save pre-processors
|
| 85 |
with (
|
| 86 |
+
PRE_PROCESSOR_APPLICANT_PATH.open('wb') as file_applicant,
|
| 87 |
PRE_PROCESSOR_BANK_PATH.open('wb') as file_bank,
|
| 88 |
+
PRE_PROCESSOR_CREDIT_BUREAU_PATH.open('wb') as file_credit_bureau,
|
| 89 |
):
|
| 90 |
+
pickle.dump(pre_processor_applicant, file_applicant)
|
| 91 |
pickle.dump(pre_processor_bank, file_bank)
|
| 92 |
+
pickle.dump(pre_processor_credit_bureau, file_credit_bureau)
|
| 93 |
|
| 94 |
print("\nDone !")
|
server.py
CHANGED
|
@@ -19,8 +19,8 @@ def _get_server_file_path(name, client_id, client_type=None):
|
|
| 19 |
name (str): The desired file name (either 'evaluation_key', 'encrypted_inputs' or
|
| 20 |
'encrypted_outputs').
|
| 21 |
client_id (int): The client ID to consider.
|
| 22 |
-
client_type (Optional[str]): The type of
|
| 23 |
-
'
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
pathlib.Path: The file path.
|
|
|
|
| 19 |
name (str): The desired file name (either 'evaluation_key', 'encrypted_inputs' or
|
| 20 |
'encrypted_outputs').
|
| 21 |
client_id (int): The client ID to consider.
|
| 22 |
+
client_type (Optional[str]): The type of client to consider (either 'applicant', 'bank',
|
| 23 |
+
'credit_bureau' or None). Default to None, which is used for evaluation key and output.
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
pathlib.Path: The file path.
|
settings.py
CHANGED
|
@@ -16,9 +16,9 @@ SERVER_FILES = REPO_DIR / "server_files"
|
|
| 16 |
DEPLOYMENT_PATH = DEPLOYMENT_PATH / "model"
|
| 17 |
|
| 18 |
# Path targeting pre-processor saved files
|
| 19 |
-
|
| 20 |
PRE_PROCESSOR_BANK_PATH = DEPLOYMENT_PATH / 'pre_processor_bank.pkl'
|
| 21 |
-
|
| 22 |
|
| 23 |
# Create the necessary directories
|
| 24 |
FHE_KEYS.mkdir(exist_ok=True)
|
|
@@ -34,26 +34,26 @@ DATA_PATH = "data/data.csv"
|
|
| 34 |
# Development settings
|
| 35 |
PROCESSED_INPUT_SHAPE = (1, 39)
|
| 36 |
|
| 37 |
-
CLIENT_TYPES = ["
|
| 38 |
INPUT_INDEXES = {
|
| 39 |
-
"
|
| 40 |
"bank": 1,
|
| 41 |
-
"
|
| 42 |
}
|
| 43 |
INPUT_SLICES = {
|
| 44 |
-
"
|
| 45 |
-
"bank": slice(36, 37), # Second position: start from
|
| 46 |
-
"
|
| 47 |
}
|
| 48 |
|
| 49 |
# Fix column order for pre-processing steps
|
| 50 |
-
|
| 51 |
'Own_car', 'Own_property', 'Mobile_phone', 'Num_children', 'Household_size',
|
| 52 |
'Total_income', 'Age', 'Income_type', 'Education_type', 'Family_status', 'Housing_type',
|
| 53 |
'Occupation_type',
|
| 54 |
]
|
| 55 |
BANK_COLUMNS = ["Account_age"]
|
| 56 |
-
|
| 57 |
|
| 58 |
_data = pandas.read_csv(DATA_PATH, encoding="utf-8")
|
| 59 |
|
|
|
|
| 16 |
DEPLOYMENT_PATH = DEPLOYMENT_PATH / "model"
|
| 17 |
|
| 18 |
# Path targeting pre-processor saved files
|
| 19 |
+
PRE_PROCESSOR_APPLICANT_PATH = DEPLOYMENT_PATH / 'pre_processor_applicant.pkl'
|
| 20 |
PRE_PROCESSOR_BANK_PATH = DEPLOYMENT_PATH / 'pre_processor_bank.pkl'
|
| 21 |
+
PRE_PROCESSOR_CREDIT_BUREAU_PATH = DEPLOYMENT_PATH / 'pre_processor_credit_bureau.pkl'
|
| 22 |
|
| 23 |
# Create the necessary directories
|
| 24 |
FHE_KEYS.mkdir(exist_ok=True)
|
|
|
|
| 34 |
# Development settings
|
| 35 |
PROCESSED_INPUT_SHAPE = (1, 39)
|
| 36 |
|
| 37 |
+
CLIENT_TYPES = ["applicant", "bank", "credit_bureau"]
|
| 38 |
INPUT_INDEXES = {
|
| 39 |
+
"applicant": 0,
|
| 40 |
"bank": 1,
|
| 41 |
+
"credit_bureau": 2,
|
| 42 |
}
|
| 43 |
INPUT_SLICES = {
|
| 44 |
+
"applicant": slice(0, 36), # First position: start from 0
|
| 45 |
+
"bank": slice(36, 37), # Second position: start from n_feature_applicant
|
| 46 |
+
"credit_bureau": slice(37, 39), # Third position: start from n_feature_applicant + n_feature_bank
|
| 47 |
}
|
| 48 |
|
| 49 |
# Fix column order for pre-processing steps
|
| 50 |
+
APPLICANT_COLUMNS = [
|
| 51 |
'Own_car', 'Own_property', 'Mobile_phone', 'Num_children', 'Household_size',
|
| 52 |
'Total_income', 'Age', 'Income_type', 'Education_type', 'Family_status', 'Housing_type',
|
| 53 |
'Occupation_type',
|
| 54 |
]
|
| 55 |
BANK_COLUMNS = ["Account_age"]
|
| 56 |
+
CREDIT_BUREAU_COLUMNS = ["Years_employed", "Employed"]
|
| 57 |
|
| 58 |
_data = pandas.read_csv(DATA_PATH, encoding="utf-8")
|
| 59 |
|
utils/client_server_interface.py
CHANGED
|
@@ -46,8 +46,8 @@ class MultiInputsFHEModelClient(FHEModelClient):
|
|
| 46 |
Args:
|
| 47 |
x (numpy.ndarray): The input to consider. Here, the input should only represent a
|
| 48 |
single party.
|
| 49 |
-
input_index (int): The index representing the type of model (0: "
|
| 50 |
-
2: "
|
| 51 |
processed_input_shape (Tuple[int]): The total input shape (all parties combined) after
|
| 52 |
pre-processing.
|
| 53 |
input_slice (slice): The slices to consider for the given party.
|
|
|
|
| 46 |
Args:
|
| 47 |
x (numpy.ndarray): The input to consider. Here, the input should only represent a
|
| 48 |
single party.
|
| 49 |
+
input_index (int): The index representing the type of model (0: "applicant", 1: "bank",
|
| 50 |
+
2: "credit_bureau")
|
| 51 |
processed_input_shape (Tuple[int]): The total input shape (all parties combined) after
|
| 52 |
pre-processing.
|
| 53 |
input_slice (slice): The slices to consider for the given party.
|
utils/pre_processing.py
CHANGED
|
@@ -22,7 +22,7 @@ def _replace_values_eq(column, value):
|
|
| 22 |
return column
|
| 23 |
|
| 24 |
def get_pre_processors():
|
| 25 |
-
|
| 26 |
transformers=[
|
| 27 |
(
|
| 28 |
"replace_occupation_type_labor",
|
|
@@ -55,10 +55,10 @@ def get_pre_processors():
|
|
| 55 |
verbose_feature_names_out=False,
|
| 56 |
)
|
| 57 |
|
| 58 |
-
|
| 59 |
transformers=[],
|
| 60 |
remainder='passthrough',
|
| 61 |
verbose_feature_names_out=False,
|
| 62 |
)
|
| 63 |
|
| 64 |
-
return
|
|
|
|
| 22 |
return column
|
| 23 |
|
| 24 |
def get_pre_processors():
|
| 25 |
+
pre_processor_applicant = ColumnTransformer(
|
| 26 |
transformers=[
|
| 27 |
(
|
| 28 |
"replace_occupation_type_labor",
|
|
|
|
| 55 |
verbose_feature_names_out=False,
|
| 56 |
)
|
| 57 |
|
| 58 |
+
pre_processor_credit_bureau = ColumnTransformer(
|
| 59 |
transformers=[],
|
| 60 |
remainder='passthrough',
|
| 61 |
verbose_feature_names_out=False,
|
| 62 |
)
|
| 63 |
|
| 64 |
+
return pre_processor_applicant, pre_processor_bank, pre_processor_credit_bureau
|