콘텐츠로 이동

고급 예제

Quickstart보다 복잡한 FHE 활용 예제들입니다. 데이터의 회전이나 추출 같은 몇 가지 핵심 개념을 소개합니다.

A. 모든 요소의 합

data = [1, 2, 3, 4, 5, 6, 7, 8] 와 같은 벡터를 암호화했을때, 여덟 개의 모든 요소를 더하는 예제입니다. 본 예제에서는 데이터를 회전하고 더하여 암호화된 벡터의 8번째 요소에 요소들의 합을 저장합니다.

from desilofhe import Engine

engine = Engine()

secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(secret_key)

data = [1, 2, 3, 4, 5, 6, 7, 8]
encrypted = engine.encrypt(data, public_key)

rotated_data = encrypted
added = rotated_data
for i in range(7):
    rotated_data = engine.rotate(rotated_data, rotation_key, delta=1)
    added = engine.add(added, rotated_data)

decrypted = engine.decrypt(added, secret_key)

# 8번째 요소에 모든 요소의 합이 저장되어 있습니다.
print((decrypted[7])) # ~36

최적화된 방법

이전 예제를 좀 더 최적화한 예제입니다. 이전 예제에서는 오른쪽으로 한 칸씩 7번의 회전과 7번의 덧셈을 수행하여 전체 합을 얻었습니다. 하지만 오른쪽으로 각각 1칸, 2칸, 4칸 회전하는 3번의 회전과 3번의 덧셈만으로도 동일한 연산을 보다 효율적인 방식으로 수행할 수 있습니다.

from desilofhe import Engine

engine = Engine()

secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(secret_key)

data = [1, 2, 3, 4, 5, 6, 7, 8]
encrypted = engine.encrypt(data, public_key)

added = encrypted

# 1칸 회전하고 더하기
rotated_data = engine.rotate(added, rotation_key, 1)
added = engine.add(added, rotated_data)

# 2칸 회전하고 더하기
rotated_data = engine.rotate(added, rotation_key, 2)
added = engine.add(added, rotated_data)

# 4칸 회전하고 더하기
rotated_data = engine.rotate(added, rotation_key, 4)
added = engine.add(added, rotated_data)

decrypted = engine.decrypt(added, secret_key)

# 8번째 요소에 모든 요소의 합이 저장되어 있습니다.
print((decrypted[7])) # ~36

B. 데이터 추출 및 재구성

숫자 벡터를 암호화한 암호문은 CKKS 스킴이 SIMD를 지원하기 때문에 벡터 간의 덧셈이나 두 암호화된 벡터 간의 계수별 곱셈을 쉽게 수행할 수 있습니다.

이전 예제는 SIMD를 사용하지 않는 사례로, 암호화된 벡터의 모든 요소를 더하여 출력 암호문의 첫 번째 계수에 넣는 과정을 설명했습니다. 이때 주변의 다른 계수들은 원래 벡터 요소들의 부분합으로 채워지며, 이는 우리가 원하는 값이 아닙니다.

이번에는 암호문에서 필요한 데이터 일부를 추출하고, 이들을 새롭고 깔끔한 벡터로 재조합하는 방법을 살펴보겠습니다. 이 벡터는 필요한 값만 포함하고 나머지 모든 위치는 0으로 채워집니다.

해당 예제에서는 4개의 암호화된 벡터로부터 정보 일부를 추출하고 이를 재구성하여 [1, 2, 3, 4, 5, 6, 7, 8] 벡터를 결과로 얻습니다. 이 과정은 암호화된 벡터와 mask 된 데이터 간의 곱셈을 통해 올바른 요소를 추출하고, 추출된 요소를 회전시켜 올바른 위치로 옮긴 후, 마지막으로 덧셈을 통해 모든 정보를 하나의 암호문으로 병합하는 방식으로 진행됩니다.

from desilofhe import Engine

engine = Engine()

secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(secret_key)

# 추출할 값들은 index로 표시되어 있습니다.
# index         0         1
data1 = [12, 7, 1, 15, 9, 2, 11, 10]
# index  2  3
data2 = [3, 4, 20, 11, 17, 6, 9, 16]
# index         5     4
data3 = [9, 18, 6, 9, 5, 11, 13, 8]
# index                  6          7
data4 = [20, 19, 18, 17, 7, 14, 15, 8]

encrypted1 = engine.encrypt(data1, public_key)
encrypted2 = engine.encrypt(data2, public_key)
encrypted3 = engine.encrypt(data3, public_key)
encrypted4 = engine.encrypt(data4, public_key)

# data1에서 세 번째 요소를 추출
mask = [0, 0, 1, 0, 0, 0, 0, 0]
multiplied = engine.multiply(encrypted1, mask)
# 첫 번째 요소에 위치하도록 회전
rotated1 = engine.rotate(multiplied, rotation_key, -2)

# data1에서 여섯 번째 요소를 추출
mask = [0, 0, 0, 0, 0, 1, 0, 0]
multiplied = engine.multiply(encrypted1, mask)
# 두 번째 요소에 위치하도록 회전
rotated2 = engine.rotate(multiplied, rotation_key, -4)

# data2에서 첫 번째와 두 번째 요소를 추출
mask = [1, 1, 0, 0, 0, 0, 0, 0]
multiplied = engine.multiply(encrypted2, mask)
# 세 번째와 네 번째 요소에 위치하도록 회전
rotated34 = engine.rotate(multiplied, rotation_key, 2)

# data3에서 세 번째 요소를 추출
mask = [0, 0, 1, 0, 0, 0, 0, 0]
multiplied = engine.multiply(encrypted3, mask)
# 여섯 번째 요소에 위치하도록 회전
rotated6 = engine.rotate(multiplied, rotation_key, 3)

# data3에서 다섯 번째 요소를 추출
# (이미 올바른 위치에 있으므로 회전이 필요하지 않음)
mask = [0, 0, 0, 0, 1, 0, 0, 0]
rotated5 = engine.multiply(encrypted3, mask)

# data4에서 다섯 번째 요소를 추출
mask = [0, 0, 0, 0, 1, 0, 0, 0]
multiplied = engine.multiply(encrypted4, mask)
# 일곱 번째 요소에 위치하도록 회전
rotated7 = engine.rotate(multiplied, rotation_key, 2)

# data4에서 여덟 번째 요소를 추출
# (이미 올바른 위치에 있으므로 회전이 필요하지 않음)
mask = [0, 0, 0, 0, 0, 0, 0, 1]
rotated8 = engine.multiply(encrypted4, mask)

# 모든 요소를 더하여 하나의 암호문으로 병합
added = engine.add(rotated1, rotated2)
added = engine.add(added, rotated34)
added = engine.add(added, rotated5)
added = engine.add(added, rotated6)
added = engine.add(added, rotated7)
added = engine.add(added, rotated8)

# 복호화 후 결과 출력
decrypted = engine.decrypt(added, secret_key)
print((decrypted[:8])) # [~1 ~2 ~3 ~4 ~5 ~6 ~7 ~8]

C. 다항식 연산

이 예제에서는 SIMD 방식으로 다음과 같은 다항식을 평가하고자 합니다: x^3 - x^2 + sqrt(2)*x + 1. x의 입력값들은 [1, 2, 3, 4, 5, 6, 7, 8]을 암호화한 암호문에 저장됩니다. DESILO FHE 라이브러리를 사용하면 어떤 다항식이든 평가하는 것이 매우 간단합니다. 필요한 연산은 덧셈, 뺄셈, 곱셈입니다.

from desilofhe import Engine
import math

engine = Engine()

secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(secret_key)

data = [1, 2, 3, 4, 5, 6, 7, 8]
encrypted = engine.encrypt(data, public_key)

# 다항식 연산 p(x) = x^3 - x^2 + sqrt(2)*x + 1
coeff0 = 1
sqrt2 = math.sqrt(2)
# x^2를 연산
x2 = engine.square(encrypted, relinearization_key)
# x^3을 연산
x3 = engine.multiply(encrypted, x2, relinearization_key)
# sqrt(2)*x을 연산
x1 = engine.multiply(encrypted, sqrt2)
# 다항식 전체를 연산
polynomial = engine.subtract(x3, x2)
polynomial = engine.add(polynomial, x1)
polynomial = engine.add(polynomial, coeff0)

# 복호화 후 결과 출력
decrypted = engine.decrypt(polynomial, secret_key)
print((decrypted[:4])) # [~2.4142 ~7.8284 ~23.2426 ~54.6569 ~108.0711 ~189.4853 ~304.8995 ~460.3137]

D. 인공 뉴런

이 예제에서는 SIMD 방식으로 인공 뉴런을 계산하고자 합니다. 여기서 뉴런은 4개의 실수를 입력으로 받아, 가중치 벡터(크기 4)와의 내적을 계산하고, 여기에 편향(bias)을 더합니다. 마지막 연산에서는 내적 결과에 대해 ReLU 함수를 적용합니다. ReLU 함수는 입력 실수 x에 대해, x가 양수이면 x를 출력하고 그렇지 않으면 0을 출력하는 함수입니다. 이 함수는 위에서 설명한 바와 같이 다항식 근사를 통해 계산됩니다.

내적과 편향

우선 내적 계산과 편향의 덧셈부터 시작해 보겠습니다.

def inner_product_bias(encrypted_data, weight, bias):
    inner_product = bias
    for encrypted_data, weight in zip(encrypted, weights):
        product = engine.multiply(encrypted_data, weight)
        inner_product = engine.add(inner_product, product)
    return inner_product

ReLU

[-1,1] 범위의 실수에 대해 ReLU 함수의 다항식 근사를 계산하는 함수를 작성해 보겠습니다. 해당 예제는 다음 논문에서 제안된 방법을 활용합니다. paper. ReLU 함수와 그 근사 함수 간의 오차는 0.008보다 작습니다. 이보다 더 정밀한 근사치를 원한다면, 더 복잡한 다항식 근사를 통해 정확도를 높이는 것도 가능합니다.

def relu(x):
    # 다항식 근사 p(x) = 0.5 * (x + x * p_{7,2}(p_{7,1}(x)))
    p71 = [
        3.60471572275560*10**(-36), 7.30445164958251,
        -5.05471704202722*10**(-35), -3.46825871108659*10,
        1.16564665409095*10**(-34), 5.98596518298826*10,
        -6.54298492839531*10**(-35), -3.18755225906466*10
    ]
    p72 = [
        -9.46491402344260*10**(-49), 2.40085652217597,
        6.41744632725342*10**(-48), -2.63125454261783,
        -7.25338564676814*10**(-48), 1.54912674773593,
        2.06916466421812*10**(-48), -3.31172956504304*10**(-1)
    ]

    # p_{7,1}(x)
    power_x = []
    # 0: x1
    power_x.append(x)
    # 1: x2
    power_x.append(engine.square(power_x[0], relinearization_key))
    # 2: x3
    power_x.append(engine.multiply(power_x[0], power_x[1], relinearization_key))
    # 3: x4
    power_x.append(engine.square(power_x[1], relinearization_key))
    # 4: x5
    power_x.append(engine.multiply(power_x[3], power_x[0], relinearization_key))
    # 5: x6
    power_x.append(engine.multiply(power_x[3], power_x[1], relinearization_key))
    # 6: x7
    power_x.append(engine.multiply(power_x[5], power_x[0], relinearization_key))
    # 병합
    poly_p71 = [p71[0]] * 8
    for i in range (7):
        power_x[i] = engine.multiply(power_x[i], p71[i+1])
        poly_p71 = engine.add(poly_p71, power_x[i])

    # # p_{7,2}(p_{7,1}(x))
    power_x = []
    # 0: x1
    power_x.append(poly_p71)
    # 1: x2
    power_x.append(engine.square(power_x[0], relinearization_key))
    # 2: x3
    power_x.append(engine.multiply(power_x[0], power_x[1], relinearization_key))
    # 3: x4
    power_x.append(engine.square(power_x[1], relinearization_key))
    # 4: x5
    power_x.append(engine.multiply(power_x[3], power_x[0], relinearization_key))
    # 5: x6
    power_x.append(engine.multiply(power_x[3], power_x[1], relinearization_key))
    # 6: x7
    power_x.append(engine.multiply(power_x[5], power_x[0], relinearization_key))
    # 병합
    poly_p72 = [p72[0]] * 8
    for i in range (7):
        power_x[i] = engine.multiply(power_x[i], p72[i+1])
        poly_p72 = engine.add(poly_p72, power_x[i])

    # x * p_{7,2}(p_{7,1}(x))
    poly_result = engine.multiply(poly_p72, x, relinearization_key)

    # x + x * p_{7,2}(p_{7,1}(x))
    poly_result = engine.add(poly_result, x)

    # 0.5 * (x + x * p_{7,2}(p_{7,1}(x)))
    poly_result = engine.multiply(poly_result, 0.5)
    return poly_result

인공 뉴런

이제 앞서 작성한 두 함수를 합쳐 최종 코드를 작성할 수 있습니다. 연산을 더 빠르게 수행하기 위해, 더 많은 곱셈 레벨을 지원하는 엔진을 병렬 CPU 모드로 사용해 보겠습니다.

from desilofhe import Engine
import math

# CPU 병렬 모드로 높은 곱셈 레벨을 가진 엔진을 사용합니다.
engine = Engine(max_level=17, mode="parallel")

secret_key = engine.create_secret_key()
public_key = engine.create_public_key(secret_key)
relinearization_key = engine.create_relinearization_key(secret_key)
rotation_key = engine.create_rotation_key(secret_key)

# data는 SIMD 방식으로 뉴런을 한 번에 8번 연산할 수 있을 만큼 충분한 데이터를 저장하고 있습니다.
# [0.1, 0.9, 1.5, 0.8]이 첫 번째 입력, [0.2, 1.0, 0.3, 1.0]이 두 번째 입력 같은 방식으로 이어집니다.
data = [
    [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8,],
    [0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 0.7, 1.6,],
    [1.5, 0.3, 0.0, 0.7, 1.1, 1.3, 0.2, 0.8,],
    [0.8, 1.0, 1.6, 1.2, 0.3, 0.7, 0.1, 1.1,],
]

# 암호화
encrypted = [engine.encrypt(d, public_key) for d in data]

# 입력과 가중치의 내적에 편향을 더함
weights = [-0.4, -1.2, 0.6, 1.]
bias = 0.34
inner_product = inner_product_bias(encrypted, weights, bias) # [~0.92 ~0.24 ~0.5 ~0.36 ~-0.46 ~-0.1 ~-0.56 ~-0.32]

# ReLU 연산
neuron_output = relu(inner_product)

# 복호화 후 결과 출력
decrypted = engine.decrypt(neuron_output, secret_key)
print((decrypted[:8])) # [~0.92 ~0.24 ~0.5 ~0.36 ~0.0 ~0.0 ~0.0 ~0.0]