矩阵运算#
HEU 提供了矩阵化运算接口,其功能主要位于 heu.numpy 模块。
小技巧
heu.numpy 大部分接口都使用并行计算的方式实现,其性能比纯 Python 实现的矩阵运算库高得多
初始化#
备注
运行本文示例代码之前请先通过以下方式 import 相关模块
from heu import numpy as hnp
from heu import phe
heu.numpy 环境初始化的方法与 heu.phe 类似:
1kit = hnp.setup(phe.SchemaType.ZPaillier, 2048)
2encryptor = kit.encryptor()
3decryptor = kit.decryptor()
4evaluator = kit.evaluator()
另外,由于 heu.numpy 模块仅仅是 heu.phe 模块的装饰器,如果您已经有 phe kit 对象,则可以通过 phe kit 对象快速构造 hnp kit 对象,此时同态加密的密钥不会重新生成:
1phe_kit = phe.setup(phe.SchemaType.ZPaillier, 2048)
2kit = hnp.HeKit(phe_kit)
在典型的使用场景下,HEU 的使用方由 sk_keeper 和 evaluator 组成,sk_keeper 生成 key pair 后会把公钥传给 evaluator,以便 evaluator 执行后续运算。
以下是一段多方场景下的环境初始化示例:
1import cloudpickle as pickle
2
3# sk_keeper (client)
4ckit = hnp.setup(phe.SchemaType.ZPaillier, 2048)
5pk_buffer = pickle.dumps(ckit.public_key())
6
7# ... send pk_buffer through network ...
8
9# evaluator (server)
10skit = hnp.setup(pickle.loads(pk_buffer))
11evaluator = skit.evaluator()
I/O#
环境准备好之后,下面介绍 heu.numpy 如何处理 Python 中的数据。矩阵在 Python 中有多种表示方式,例如嵌套的 List,嵌套的 Tuple 以及 numpy.ndarray,HEU 把所有 Python 中原始的数据类型称为 Cleartext(原文)。参考 快速入门,而 HEU 内部仅有 hnp.PlaintextArray 和 hnp.CiphertextArray 两种类型,分别存储明文矩阵和密文矩阵,Python 中的原始数据类型需要先转换为 hnp.PlaintextArray 后才能被 hnp.numpy 模块进一步处理。
本节介绍如何使用 heu.numpy 模块将数据在原文(cleartext)与明文(plaintext)之间做转换。
输入#
与 heu.phe 一样,原文(cleartext)到明文(plaintext)之间的转换依赖编码器,heu.numpy 与 heu.phe 共用相同的编码器。同时,heu.numpy 模块提供了 .array() 接口用于识别并转换 Cleartext 矩阵。
1# Init from nested lists
2harr = kit.array([[1, 2, 3], [4, 5, 6]], phe.IntegerEncoderParams())
3print(harr)
4# [[1000000 2000000 3000000]
5# [4000000 5000000 6000000]]
6
7# Init using default encoder (phe.BigintEncoder())
8harr = kit.array([[1, 2, 3], [4, 5, 6]])
9print(harr)
10# [[1 2 3]
11# [4 5 6]]
12
13# Init from numpy.ndarray
14import numpy as np
15harr = kit.array(np.arange(10).reshape(2, 5))
16print(harr)
17# [[0 1 2 3 4]
18# [5 6 7 8 9]]
备注
phe.IntegerEncoder、phe.FloatEncoder、phe.BatchIntegerEncoder 和 phe.BatchFloatEncoder 默认并行编码原文矩阵,而 phe.BigintEncoder 受限于全局解释器锁(GIL)的存在尚不支持并行编码。
小技巧
phe.IntegerEncoder、phe.FloatEncoder 本质上就是把原始数据乘了一个 scale,如果我自己把原文乘上一个 scale 再转换成 hnp.PlaintextArray 行吗?
答案是可以,但性能会低一些。HEU 会尽可能地做并行化,而自己在 Python 端做 scale 的话一定是串行的。另外,受限于形式,hnp.array() 必须要指定一个 encoder,如果原文本来就是整数或者您已经做了 scale,您可以向 hnp.array() 传入一个 scale=1 的 encoder,或者直接使用默认的 BigintEncoder
# create an encoder with scale=1
edr = phe.IntegerEncoder(phe.SchemaType.ZPaillier, 1)
hnp.array(np.arange(100), edr)
# or create a encoder using an equivalent form
kit.array(np.arange(100), phe.IntegerEncoderParams(1))
# or just use default encoder
kit.array(np.arange(100))
辅助函数#
HEU 提供了一些辅助函数用以快速创建 hnp.PlaintextArray 对象。
1# generate a random matrix with shape 10x10 in interval [-100, 100)
2min = phe.Plaintext(phe.SchemaType.ZPaillier, -100)
3max = phe.Plaintext(phe.SchemaType.ZPaillier, 100)
4hnp.random.randint(min, max, (10, 10))
5# generate a random matrix with shape 2x2 where each element is 2048 bits long
6hnp.random.randbits(phe.SchemaType.ZPaillier, 2048, (2, 2))
输出#
hnp.PlaintextArray 提供了 to_numpy() 接口用于将明文转换成原文,同理,转换的过程依赖编码器。
备注
编码器同时拥有编码和解码功能,一般来说, to_numpy() 需要传入与 hnp.array() 相同的编码器对象,否则可能导致解码后的原文不正确
1edr = phe.FloatEncoder(phe.SchemaType.ZPaillier)
2harr = hnp.array([1.1, 2.1], edr)
3nparr = harr.to_numpy(edr)
4print(nparr) # [1.1 2.1]
5print(type(nparr)) # <class 'numpy.ndarray'>
与 hnp.array() 一样,如果 to_numpy() 不指定编码器,则默认使用 BigintEncoder。
加解密和运算#
基本操作#
encryptor 和 decryptor 分别提供了加密和解密接口,evaluator 提供了明文、密文运算的能力,用法如下:
1# encrypt
2ct_arr = encryptor.encrypt(kit.array([1, 2, 3, 4]))
3# decrypt
4pt_arr = decryptor.decrypt(ct_arr)
5# evaluate
6c2 = evaluator.add(ct_arr, pt_arr)
7c2 = evaluator.sub(ct_arr, pt_arr)
8c2 = evaluator.mul(ct_arr, pt_arr)
9c2 = evaluator.matmul(ct_arr, pt_arr)
10print(decryptor.decrypt(c2)) # [[30]]
警告
同态加密的密文无法做 truncation,因此如果 plaintext/ciphertext 是经过 scale 的,则乘法会导致 scale 变成两倍,这时可以用以下两种方式解决:
保证乘法的其中一个乘数的 scale 为 1
arr1 = kit.array([1.4], phe.FloatEncoderParams()) arr2 = kit.array([2]) print(evaluator.mul(arr1, arr2).to_numpy(edr)) # [2.8]
在结果转为明文或原文后手动除以 scale
scale = 100 edr = phe.FloatEncoder(phe.SchemaType.ZPaillier, scale) res = evaluator.mul(hnp.array([1.4], edr), hnp.array([2.5], edr)) print(res.to_numpy(edr) / scale) # [3.5] # or print(res.to_numpy(kit.float_encoder(scale**2))) # [3.5]
切片#
hnp.PlaintextArray 和 hnp.CiphertextArray 支持切片,示例如下:
1# 1d array
2arr = kit.array([1, 2, 3, 4, 5, 6, 7])
3arr[1]
4arr[-1]
5arr[1:5] # Slice elements from index 1 to index 5
6arr[4:] # Slice elements from index 4 to the end of the array
7arr[:4] # Slice elements from the beginning to index 4 (not included)
8arr[-3:-1] # Slice from the index 3 from the end to index 1 from the end
9arr[1:5:2]
10arr[::2]
11arr[[1,2,3]]
12
13# 2d array
14nparr = np.arange(49).reshape((7, 7))
15harr = kit.array(nparr)
16
17harr[5:1, 1]
18harr[4:, [1, 2, 2, 3]]
19harr[:4, (5,)]
20harr[-3:-1, (1, 3)]
21harr[1:5:2, -1]
22harr[::2, [-7, 5]]
23harr[[1, 2, 3]]
24harr[1]
25harr[:]
hnp 的切片 key 支持 scalar,sequence,slice 类型,其中 sequence 是 list,tuple 等类型的统称。如果 key 是 scalar,则切片在该维度上降维,如果 key 是 sequence 和 slice,则切片保留维度,举例来说:
harr[sequence/slice, sequence/slice]: 结果为 matrix
harr[sequence/slice, scalar] or harr[scalar, sequence/slice]: 结果为 vector
harr[scalar, scalar]: 结果为 scalar
警告
hnp 切片的用法与 numpy 类似,但并不完全一样,以下是结果不一致的地方
1# case 1
2narr = np.arange(49).reshape((7, 7))
3harr = kit.array(narr)
4
5harr[[1, 2], [0, -1]] # returns a 2x2 matrix
6narr[[1, 2], [0, -1]] # returns a 2-len vector
7
8# case 2
9narr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
10harr = kit.array(narr)
11harr[(0,)] # got 1d array: [0]
12narr[(0,)] # got scalar: 0