mirror of
https://github.com/luxfi/torus-ml.git
synced 2026-10-30 15:15:23 +00:00
The distribution called itself torus-ml while shipping a package directory named concrete, so the name a user installs and the name they import disagreed — and most of the tree already said torus.ml, which could not resolve against src/concrete. Moved src/concrete to src/torus and pointed the include at it. The dependency was Zama's concrete-python from their index. It is torus-fhe from ours, which is what luxfhe/torus already ships: that frontend declares torus-fhe and ships a torus/fhe module, so every 'from torus.fhe' in this tree was already naming our compiler and only the dependency lagged. Two distributions share one namespace, which is the shape the upstream this forked from used and the reason the split works: torus-fhe gives torus.fhe, torus-ml gives torus.ml. Swept every other spelling — identifiers, enum members, workflow variables, filenames, notebooks. LICENSE and NOTICE keep the upstream attribution a fork owes; the figures carry the old name in JPEG metadata a text sweep cannot reach.
26 KiB
26 KiB
In [1]:
import time
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_openml
from sklearn.model_selection import GridSearchCV, ShuffleSplit, train_test_split
from xgboost import XGBClassifier
from torus.ml.sklearn import XGBClassifier as TorusXGBClassifierNo CUDA runtime is found, using CUDA_HOME='/usr/local/cuda'
In [2]:
# pylint: disable-next=no-member
all_data = fetch_openml(data_id=40945, as_frame=True, cache=True).frame
all_data = all_data.convert_dtypes()
all_data["survived"] = all_data["survived"].astype(int)
print(all_data.columns)Index(['pclass', 'survived', 'name', 'sex', 'age', 'sibsp', 'parch', 'ticket',
'fare', 'cabin', 'embarked', 'boat', 'body', 'home.dest'],
dtype='object')
In [3]:
train_data, test_data = train_test_split(all_data, test_size=len(all_data.index) - 891)
datasets = [train_data, test_data]In [4]:
train_data.head()Out [4]:
| pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 995 | 3 | 0.0 | Markoff, Mr. Marin | male | 35.0 | 0 | 0 | 349213 | 7.8958 | <NA> | C | <NA> | <NA> | <NA> |
| 1147 | 3 | 0.0 | Riihivouri, Miss. Susanna Juhantytar 'Sanni' | female | 22.0 | 0 | 0 | 3101295 | 39.6875 | <NA> | S | <NA> | <NA> | <NA> |
| 485 | 2 | 0.0 | Levy, Mr. Rene Jacques | male | 36.0 | 0 | 0 | SC/Paris 2163 | 12.875 | D | C | <NA> | <NA> | Montreal, PQ |
| 1265 | 3 | 0.0 | Van Impe, Miss. Catharina | female | 10.0 | 0 | 2 | 345773 | 24.15 | <NA> | S | <NA> | <NA> | <NA> |
| 632 | 3 | 0.0 | Andersson, Mrs. Anders Johan (Alfrida Konstant... | female | 39.0 | 1 | 5 | 347082 | 31.275 | <NA> | S | <NA> | <NA> | Sweden Winnipeg, MN |
In [6]:
print("-" * 3, "Train data", "-" * 3)
print(train_data.isnull().sum(), "\n")
print("-" * 3, "Test data", "-" * 3)
print(test_data.isnull().sum())--- Train data --- pclass 0 survived 0 name 0 sex 0 age 173 sibsp 0 parch 0 ticket 0 fare 0 cabin 691 embarked 1 boat 556 body 806 home.dest 378 dtype: int64 --- Test data --- pclass 0 survived 0 name 0 sex 0 age 90 sibsp 0 parch 0 ticket 0 fare 1 cabin 323 embarked 1 boat 267 body 382 home.dest 186 dtype: int64
In [7]:
for incomp_var in train_data.columns:
missing_val = pd.concat(datasets)[incomp_var].isnull().sum()
if missing_val > 0 and incomp_var != "survived":
total_val = pd.concat(datasets).shape[0]
print(f"Percentage of missing values in {incomp_var}: {missing_val/total_val*100:.1f}%")Percentage of missing values in age: 20.1% Percentage of missing values in fare: 0.1% Percentage of missing values in cabin: 77.5% Percentage of missing values in embarked: 0.2% Percentage of missing values in boat: 62.9% Percentage of missing values in body: 90.8% Percentage of missing values in home.dest: 43.1%
In [8]:
drop_column = ["cabin", "boat", "body", "home.dest", "ticket"]
for dataset in datasets:
# Remove irrelevant variables
dataset.drop(drop_column, axis=1, inplace=True)In [9]:
for dataset in datasets:
# Complete missing Age values with median
dataset.age.fillna(dataset.age.median(), inplace=True)
# Complete missing Embarked values with the most common one
dataset.embarked.fillna(dataset.embarked.mode()[0], inplace=True)
# Complete missing Fare values with median
dataset.fare.fillna(dataset.fare.median(), inplace=True)In [10]:
def get_bin_labels(bin_name, number_of_bins):
labels = []
for i in range(number_of_bins):
labels.append(bin_name + f"_{i}")
return labels
for dataset in datasets:
# Emphasize on relatives
dataset["FamilySize"] = dataset.sibsp + dataset.parch + 1
dataset["IsAlone"] = 1
dataset.IsAlone[dataset.FamilySize > 1] = 0
# Consider an individual's social status
dataset["Title"] = dataset.name.str.extract(r" ([A-Za-z]+)\.", expand=False)
# Group fares and ages in "bins" or "buckets"
dataset["FareBin"] = pd.qcut(dataset.fare, 4, labels=get_bin_labels("FareBin", 4))
dataset["AgeBin"] = pd.cut(dataset.age.astype(int), 5, labels=get_bin_labels("AgeBin", 5))
# Remove now-irrelevant variables
drop_column = ["name", "sibsp", "parch", "fare", "age"]
dataset.drop(drop_column, axis=1, inplace=True)In [11]:
data = pd.concat(datasets)
titles = data.Title.value_counts()
print(titles)Title Mr 757 Miss 260 Mrs 197 Master 61 Dr 8 Rev 8 Col 4 Ms 2 Major 2 Mlle 2 Lady 1 Mme 1 Countess 1 Dona 1 Sir 1 Jonkheer 1 Capt 1 Don 1 Name: count, dtype: Int64
In [12]:
uncommon_titles = titles[titles < 10].index
for dataset in datasets:
dataset.Title.replace(uncommon_titles, "Rare", inplace=True)In [13]:
categorical_features = train_data.select_dtypes(exclude=np.number).columns
x_train = pd.get_dummies(train_data, prefix=categorical_features)
x_test = pd.get_dummies(test_data, prefix=categorical_features)In [14]:
target = "survived"
x_train = x_train.drop(columns=[target])
y_train = train_data[target]
x_test = x_test.drop(columns=[target])
y_test = test_data[target]In [15]:
# Instantiate the Cross-Validation generator
cv = ShuffleSplit(n_splits=5, test_size=0.3, random_state=0)
# Set the parameters to tune.
# Those ranges are voluntarily small in order to keep the FHE execution time per inference
# relatively low. In fact, we found out that, in this particular Titanic example, models with
# larger numbers of estimators or maximum depth don't score a much better accuracy.
param_grid = {
"max_depth": list(range(1, 5)),
"n_estimators": list(range(1, 5)),
"learning_rate": [0.01, 0.1, 1],
}
# Instantiate and fit the model through grid-search cross-validation
time_begin = time.time()
model = GridSearchCV(XGBClassifier(), param_grid, cv=cv, scoring="roc_auc")
model.fit(x_train, y_train)
cv_xgb_duration = time.time() - time_begin
print(f"Best hyper-parameters found in {cv_xgb_duration:.2f}s :", model.best_params_)Best hyper-parameters found in 7.98s : {'learning_rate': 1, 'max_depth': 2, 'n_estimators': 4}
In [16]:
# The Torus ML model needs an additional parameter used for quantization
param_grid["n_bits"] = [2]
# Instantiate and fit the model through grid-search cross-validation
time_begin = time.time()
torus_model = GridSearchCV(TorusXGBClassifier(), param_grid, cv=cv, scoring="roc_auc")
torus_model.fit(x_train, y_train)
cv_torus_duration = time.time() - time_begin
print(f"Best hyper-parameters found in {cv_torus_duration:.2f}s :", torus_model.best_params_)Best hyper-parameters found in 40.99s : {'learning_rate': 0.1, 'max_depth': 4, 'n_bits': 2, 'n_estimators': 4}
In [17]:
# Compute the predictions in clear using XGBoost
clear_predictions = model.predict(x_test)
# Compute the predictions in clear using Torus ML
clear_quantized_predictions = torus_model.predict(x_test)
# Compile the Torus ML model on a subset
fhe_circuit = torus_model.best_estimator_.compile(x_train.head(100))
# Generate the keys
# This step is not absolutely necessary, as keygen() is called, when necessary,
# within the predict method.
# However, it is useful to run it beforehand in order to be able to
# measure the prediction executing time separately from the key generation one
time_begin = time.time()
fhe_circuit.keygen()
key_generation_duration = time.time() - time_begin
# Compute the predictions in FHE using Torus ML
time_begin = time.time()
fhe_predictions = torus_model.best_estimator_.predict(x_test, fhe="execute")
prediction_duration = time.time() - time_begin
print(f"Key generation time: {key_generation_duration:.2f}s")
print(f"Total execution time for {len(clear_predictions)} inferences: {prediction_duration:.2f}s")
print(f"Execution time per inference in FHE: {prediction_duration / len(clear_predictions):.2f}s")Key generation time: 0.94s Total execution time for 418 inferences: 65.49s Execution time per inference in FHE: 0.16s
In [18]:
number_of_equal_preds = np.sum(fhe_predictions == clear_quantized_predictions)
pred_similarity = number_of_equal_preds / len(clear_predictions) * 100
print(
"Prediction similarity between both Torus ML models (quantized clear and FHE): "
f"{pred_similarity:.2f}%",
)
accuracy_fhe = np.mean(fhe_predictions == y_test) * 100
print(
"Accuracy of prediction in FHE on the test set " f"{accuracy_fhe:.2f}%",
)Prediction similarity between both Torus ML models (quantized clear and FHE): 100.00% Accuracy of prediction in FHE on the test set 78.23%