How to calculate ReLU function through ciphertext conversion?

I want to calculate the ReLU function through ciphertext conversion. First, I use the EvalCKKStoFHEW function to convert it to FHEW ciphertext. Then, I calculate the ReLU function by looking up(LUT) table on the FHEW ciphertext. Finally, I use EvalHEWtoCKKS to convert it to CKKS ciphertext. However, the code I wrote reported an error and I don’t know how to modify it. My code is written based on scheme-switching’s FuncViaSchemeSwitching function. The code is as follows:

    // Step 1: Setup CryptoContext for CKKS
    // 1 for CKKS to FHEW, 14 for FHEW to CKKS
    uint32_t multDepth    = 9 + 3 + 2;
    uint32_t scaleModSize = 50;
    uint32_t ringDim      = 2048;
    SecurityLevel sl      = HEStd_NotSet;
    BINFHE_PARAMSET slBin = TOY;
    uint32_t logQ_ccLWE   = 25;
    uint32_t slots        = 16;  // sparsely-packed
    uint32_t batchSize    = slots;

    CCParams<CryptoContextCKKSRNS> parameters;
    parameters.SetMultiplicativeDepth(multDepth);
    parameters.SetScalingModSize(scaleModSize);
    parameters.SetScalingTechnique(FIXEDAUTO);
    parameters.SetSecurityLevel(sl);
    parameters.SetRingDim(ringDim);
    parameters.SetBatchSize(batchSize);

    CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);

    // Enable the features that you wish to use
    cc->Enable(PKE);
    cc->Enable(KEYSWITCH);
    cc->Enable(LEVELEDSHE);
    cc->Enable(ADVANCEDSHE);
    cc->Enable(SCHEMESWITCH);

    std::cout << "CKKS scheme is using ring dimension " << cc->GetRingDimension();
    std::cout << ", and number of slots " << slots << std::endl << std::endl;

    // Generate encryption keys.
    auto keys = cc->KeyGen();

    // Step 2: Prepare the FHEW cryptocontext and keys for FHEW and scheme switching
    SchSwchParams params;
    params.SetSecurityLevelCKKS(sl);
    params.SetSecurityLevelFHEW(slBin);
    params.SetArbitraryFunctionEvaluation(true);
    params.SetCtxtModSizeFHEWLargePrec(logQ_ccLWE);
    params.SetNumSlotsCKKS(slots);
    params.SetNumValues(slots);
    auto privateKeyFHEW = cc->EvalSchemeSwitchingSetup(params);
    auto ccLWE          = cc->GetBinCCForSchemeSwitch();

    cc->EvalSchemeSwitchingKeyGen(keys, privateKeyFHEW);

    // Generate the bootstrapping keys for EvalFunc in FHEW
    ccLWE->BTKeyGen(privateKeyFHEW);

    std::cout << "FHEW scheme is using lattice parameter " << ccLWE->GetParams()->GetLWEParams()->Getn();
    std::cout << ", logQ " << logQ_ccLWE;
    std::cout << ", and modulus q " << ccLWE->GetParams()->GetLWEParams()->Getq() << std::endl << std::endl;

    // Set the scaling factor to be able to decrypt; under the hood, the LWE mod switch will be performed on the ciphertext at the last level
    auto modulus_LWE     = 1 << logQ_ccLWE;
    auto beta            = ccLWE->GetBeta().ConvertToInt();
    auto pLWE           = modulus_LWE / (2 * beta);  // Large precision
    double scaleCF = 1.0;

    cc->EvalCKKStoFHEWPrecompute(scaleCF);

    // Step 3: Initialize the function

    // Initialize Function relu
    auto fp = [](NativeInteger x, NativeInteger p) -> NativeInteger { 
        if (x <= p) return p; 
        else return x; 
    };
    
    // Generate LUT from function f(x)
    auto lut = ccLWE->GenerateLUTviaFunction(fp, pLWE);

    // Step 4: Encoding and encryption of inputs
    // Inputs
    std::vector<double> x1={1,2,3,4,5,6,7,8,9,-1,-2,-3,-4,-5,-6,-78};
    // Encoding as plaintexts
    Plaintext ptxt1 = cc->MakeCKKSPackedPlaintext(x1, 1, 0, nullptr);

    // Encrypt the encoded vectors
    auto c1 = cc->Encrypt(keys.publicKey, ptxt1);

    // Step 5: Scheme switching from CKKS to FHEW
    auto cTemp = cc->EvalCKKStoFHEW(c1, slots); 

    std::cout << "Input x1: " << ptxt1->GetRealPackedValue() << std::endl;
    std::cout << "FHEW decryption: ";
    LWEPlaintext result;
    for (uint32_t i = 0; i < cTemp.size(); ++i) {
        ccLWE->Decrypt(privateKeyFHEW, cTemp[i], &result, pLWE);
        std::cout << result << " ";
    }

    // Step 6: Evaluate the function
    std::vector<LWECiphertext> cFunc(cTemp.size());
    for (uint32_t i = 0; i < cTemp.size(); i++) {
        cFunc[i] = ccLWE->EvalFunc(cTemp[i], lut);
    }

    std::cout << "\nExpected result relu ";
    for (uint32_t i = 0; i < slots; ++i) {
        std::cout << fp(static_cast<int>(x1[i]), 0) << " ";
    }
    LWEPlaintext pFunc;
    std::cout << "\nFHEW decryption mod " << NativeInteger(pLWE) << ": ";
    for (uint32_t i = 0; i < cFunc.size(); ++i) {
        ccLWE->Decrypt(privateKeyFHEW, cFunc[i], &pFunc, pLWE);
        std::cout << pFunc << " ";
    }
    std::cout << "\n" << std::endl;

    // Step 7: Scheme switching from FHEW to CKKS
    auto cTemp2 = cc->EvalFHEWtoCKKS(cFunc, slots, slots, pLWE, 0, pLWE);

    Plaintext plaintextDec2;
    cc->Decrypt(keys.secretKey, cTemp2, &plaintextDec2);
    plaintextDec2->SetLength(slots);
    std::cout << "\nSwitched decryption modulus_LWE mod " << NativeInteger(pLWE)
              << " works only for messages << p: " << plaintextDec2 << std::endl;

    // Transform through arcsine
    cTemp2 = cc->EvalFHEWtoCKKS(cFunc, slots, slots, 4, 0, 2);

    cc->Decrypt(keys.secretKey, cTemp2, &plaintextDec2);
    plaintextDec2->SetLength(slots);
    std::cout << "Arcsin(switched result) * p/2pi gives the correct result if messages are < p/4: ";
    for (uint32_t i = 0; i < slots; i++) {
        double x = std::max(std::min(plaintextDec2->GetRealPackedValue()[i], 1.0), -1.0);
        std::cout << std::asin(x) * pLWE / (2 * Pi) << " ";
    }
    std::cout << "\n";

But there is an error here:

Output precision is only wrt the operations in CKKS after switching back.

CKKS scheme is using ring dimension 2048, and number of slots 16

FHEW scheme is using lattice parameter 32, logQ 25, and modulus q 2048

terminate called after throwing an instance of 'lbcrypto::OpenFHEException'
  what():  /openfhe-development/src/core/include/math/hal/intnat/ubintnat.h:l.481:DividedBy(): NativeIntegerT DividedBy: zero

May I ask how I can solve this problem?

I ran your code and no exception is being thrown on my side.

Nevertheless, there are many misunderstandings in your code, and it does not do what you intend to do.

  • GenerateLUTviaFunction only works for p <= 8. This is written in SCHEME_SWITCHING_CAPABILITY.md, which judging by the input x you are feeding, is not sufficient for your application.
  • You combine code from FuncViaSchemeSwitching() but replace the line auto pLWE = ccLWE->GetMaxPlaintextSpace().ConvertToInt(); // Small precision because GenerateLUTviaFunction needs p < q with pLWE from different functions that work with larger precision. You also make scaleCF 1 instead of 1/pLWE.
  • FHEW works with modular arithmetic, so every message is modulo p. This means that your condition in fp (x <= p) is always true. In modular arithmetic modulo p, positive numbers are less than p/2 and negative numbers are between p/2 and p.
  • When you print the expected result relu, you feed 0 instead of p.

To obtain the relu functionality, it is better to use the sign/compare capability using CKKS<->FHEW conversion, which returns a vector of elements encrypting zero for positive values and 1 for negative values (in real arithmetic), and then in CKKS you should multiply c1 by (1 - cTemp2). You can try to adapt the solution from Can FHE be used for comparing 2 ciphertexts? - #8 by Pavanranganath.

Are you saying that it would be better for me to use the EvalCompareSchemeSwitching function to calculate the ReLU function? May I ask what is the difference between using the EvalCompareSchemeSwitching function and directly calculating the ReLU function

I assume that by “directly” calculating ReLU function, you mean by FuncViaSchemeSwitching(). As I mentioned before, this function works with maximum plaintext space of 3 bits (i.e., p = 8). Therefore, you can achieve ReLU in this way for messages smaller than 8. If you need to work with larger messages, it is better to use functionality for multi-precision, which currently is implemented in OpenFHE for sign-like capabilities, e.g., EvalCompareSchemeSwitching().

Can I understand that if the ReLU function needs to be compared bit by bit to return the final result for a FHEW ciphertext, the calculation will be slower for data with larger q, so only obtaining symbols (single bit) is more efficient?

FHEW bootstrapping becomes slow if we are dealing with large p or q. Therefore, it is more efficient to decompose the large numbers into small digits and work over them. In OpenFHE, homomorphic digit decomposition for FHEW is available. But evaluating the desired function over the digits and then combining the results back is less efficient for arbitrary functions and is not yet implemented in OpenFHE. On the other hand, for sign/comparison, the combination of the individual digits operations is more efficient, and is implemented in OpenFHE.

Please read https://eprint.iacr.org/2021/1337.pdf to understand the underlying mechanisms.