mirror of
https://github.com/luxfhe/ml-sdk.git
synced 2026-10-30 18:13:04 +00:00
Machine Learning with FHE - fork of Zama Concrete-ML - Built-in FHE-friendly ML models - scikit-learn compatible API - PyTorch model conversion - Quantization-aware training
13 KiB
13 KiB
In [1]:
from pprint import pprint
import numpy as np
from concrete.fhe.tracing import Tracer
from quant_framework import DualArray, Quantizer
from concrete import fheIn [2]:
np.random.seed(42)
n_values = 10
x_calib = np.random.randn(n_values)In [3]:
# Create the quantizer. Higher N_BITS will give better results for the floating point comparison
N_BITS = 8
quantizer = Quantizer(n_bits=N_BITS)
def finalize(x):
"""Finalize the output value.
If the DualArray's integer array is a Tracer, an object used during compilation, return it
as is. Else, return the DualArray. This is called at the end of the run_numpy method because
the compiler can only consider Tracer objects or Numpy arrays as input and outputs.
"""
if isinstance(x.int_array, Tracer):
return x.int_array
return x
def f(q_x):
"""Function made of quantized operators to compile."""
# Convert the inputs to a DualArray instance using the stored calibration data. This is
# necessary as Concrete Python can only compile functions that inputs Numpy arrays, while we
# still want to be able to propagate the quantization parameters throughout the different
# operators
dual_x = DualArray(float_array=x_calib, int_array=q_x, quantizer=quantizer)
dual_y = dual_x.exp(key="exp")
dual_x = dual_x.add(dual_y, "add")
dual_x = dual_x.truediv(3, key="truediv")
dual_x = dual_x.quantize(key="output")
return finalize(dual_x)In [4]:
def expected_f(x):
"""Expected function made of float operators."""
return (np.exp(x) + x) / 3In [5]:
# Convert the input to a DualArray and quantize it
dual_x = DualArray(x_calib, quantizer=quantizer).quantize(key="input")
# Calibrate the function in order to compute and store the quantization parameters
dual_result = f(dual_x.int_array)In [6]:
# At this step, the quantizer contains all the needed quantization parameters (scale, zero_point)
pprint(quantizer.scale_dict){'add_sub_add_requant': (0.012416286069347535, 0),
'add_sub_add_self': (0.012416286069347535, 0),
'exp': (0.012416286069347535, 0),
'input': (0.012434746578798358, 0),
'output': (0.016833222749391915, 0),
'truediv': (0.012405140942975472, 0)}
In [7]:
expected_output = expected_f(x_calib)
# De-quantize the output
float_output = dual_result.dequantize("output").float_array
# Compare the float output with the expected output using the MAE score
output_mae = np.mean(np.abs(float_output - expected_output))
print(f"MAE between the expected result and the computed float result: {output_mae:.4f}")MAE between the expected result and the computed float result: 0.0051
In [8]:
# Create a new input and quantize it
new_x = np.random.randn(10)
new_int_x = quantizer.quantize(new_x, key="input")
# Retrieve the integer result
new_int_result = f(new_int_x).int_array
# Dequantize the output
new_float_output = quantizer.dequantize(new_int_result, key="output")
# Compare the float output with the expected output using the MAE score
new_expected_output = expected_f(new_x)
new_output_mae = np.mean(np.abs(new_float_output - new_expected_output))
print(f"MAE between the expected result and the computed float result: {new_output_mae:.4f}")MAE between the expected result and the computed float result: 0.0028
In [9]:
# Instantiate the compiler
compiler = fhe.Compiler(f, {"q_x": "encrypted"})
# Build the inputset as a batch of single quantized input
inputset = list(quantizer.quantize(x_calib, key="input"))
# Compile the function using the inputset and print the computation graph
circuit = compiler.compile(inputset, show_graph=True)
# Generate the keys
circuit.keygen()Computation Graph
--------------------------------------------------------------------------------
%0 = q_x # EncryptedScalar<int8> ∈ [-38, 127]
%1 = 0 # ClearScalar<uint1> ∈ [0, 0]
%2 = subtract(%0, %1) # EncryptedScalar<int8> ∈ [-38, 127]
%3 = 0 # ClearScalar<uint1> ∈ [0, 0]
%4 = subtract(%0, %3) # EncryptedScalar<int8> ∈ [-38, 127]
%5 = subgraph(%4) # EncryptedScalar<int8> ∈ [-38, 127]
%6 = subgraph(%2) # EncryptedScalar<uint9> ∈ [50, 390]
%7 = 1 # ClearScalar<uint1> ∈ [1, 1]
%8 = multiply(%7, %6) # EncryptedScalar<uint9> ∈ [50, 390]
%9 = add(%5, %8) # EncryptedScalar<uint10> ∈ [12, 517]
%10 = 0 # ClearScalar<uint1> ∈ [0, 0]
%11 = subtract(%9, %10) # EncryptedScalar<uint10> ∈ [12, 517]
%12 = subgraph(%11) # EncryptedScalar<uint7> ∈ [3, 127]
return %12
Subgraphs:
%5 = subgraph(%4):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012416286069347535 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = 0.012416286069347535 # ClearScalar<float64>
%4 = divide(%2, %3) # EncryptedScalar<float64>
%5 = 0 # ClearScalar<uint1>
%6 = add(%4, %5) # EncryptedScalar<float64>
%7 = rint(%6) # EncryptedScalar<float64>
%8 = astype(%7, dtype=int_) # EncryptedScalar<uint1>
return %8
%6 = subgraph(%2):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012416286069347535 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = exp(%2) # EncryptedScalar<float64>
%4 = 0.012416286069347535 # ClearScalar<float64>
%5 = divide(%3, %4) # EncryptedScalar<float64>
%6 = 0 # ClearScalar<uint1>
%7 = add(%5, %6) # EncryptedScalar<float64>
%8 = rint(%7) # EncryptedScalar<float64>
%9 = astype(%8, dtype=int_) # EncryptedScalar<uint1>
return %9
%12 = subgraph(%11):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012405140942975472 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = 3 # ClearScalar<uint2>
%4 = divide(%2, %3) # EncryptedScalar<float64>
%5 = 0.016833222749391915 # ClearScalar<float64>
%6 = divide(%4, %5) # EncryptedScalar<float64>
%7 = 0 # ClearScalar<uint1>
%8 = add(%6, %7) # EncryptedScalar<float64>
%9 = rint(%8) # EncryptedScalar<float64>
%10 = astype(%9, dtype=int_) # EncryptedScalar<uint1>
return %10
--------------------------------------------------------------------------------
In [10]:
input_0 = new_int_x[0]
# Compute the result in the clear directly using the quantized operators
clear_evaluation = f(input_0)
# Compute the result in the clear using FHE simulation
simulated_evaluation = circuit.simulate(input_0)
# Compute the result in FHE
fhe_evaluation = circuit.encrypt_run_decrypt(input_0)
print((clear_evaluation.int_array, simulated_evaluation, fhe_evaluation))
print(
"Results are identical:",
all((clear_evaluation.int_array, simulated_evaluation, fhe_evaluation)),
)(3, 3, 3) Results are identical: True