简介:面向需要完成毕业设计、课程设计或工程实训的高校学生,以及希望探索隐私保护与深度学习结合方向的开发者。系统基于Python卷积神经网络,解决加密云端图像的隐私保护分类需求,尤其适合安全与AI交叉领域的初期项目。压缩包共2004个文件,体积约148.84MB,文件类型以js脚本(1296个)、Markdown文档(356个)和JSON配置(341个)为主,同时包含少量Python源文件和HTML页面。这些文件分别承担前端交互展示、说明文档、参数配置与核心模型代码等角色。目前已有116人学习下载,资源内容较为完整,初学者可按文档逐步搭建,进阶者可在此基础上调整网络结构或加密策略。借助这份资源,可快速搭建一个带可视化界面的加密图像分类系统,理解从图像加密、CNN训练到类别输出的全流程,并利用JSON配置灵活切换实验参数,为论文写作或二次开发提供扎实的代码参考。
1. 加密图像分类:让云端在噪声里替你做卷积
一个常见的隐私保护场景:医院想用云端服务对眼底照片做疾病分类,但不想把病人原始影像交给云厂商;安防平台想对监控图像做目标分类,又不想让第三方服务商留存原始画面。基于Python卷积神经网络的加密图像分类系统,做的就是这件事——用同态加密把图像变成密文再上传,云端在完全看不到明文内容的情况下完成CNN分类,最后把加密的分类结果返回给你,由你本地解密。对算法工程师来说,它的价值在于把“明文训练、密文推理”这条路径变成可落地的工程方案,而不是停留在论文里。
2. 为什么卷积神经网络能藏在密文里:加法、乘法和平方激活的拼图
2.1 同态加密只认识加减乘,卷积和池化恰好是加减乘
同态加密里最接近工程可用的是CKKS方案,它允许在密文上直接做浮点数加法和乘法,而且结果解密后等于明文做同样运算的结果。这个特性与CNN的计算结构高度吻合:卷积层本质上是“乘累加”,即每个输出像素是邻域像素与卷积核系数的乘积之和;平均池化本质上是“加权和”,也就是把几个像素加起来再乘一个固定系数。这两类操作在CKKS下都能直接用密文加法、密文与明文标量乘法实现,不需要额外引入近似。
所以一个直觉上的判断是:只要把网络里的非线性部分处理掉,整条CNN推理链路都可以在密文上串起来。云端手里只有噪声一样的密文张量,既看不到单个像素,也推断不出原始图像内容,但它能正确执行卷积和池化,把分类所需的信息一层层传递下去。
2.2 ReLU不敢进密文,平方激活成了CNN的替代品
CNN里真正让同态加密头疼的是ReLU和Softmax。ReLU本质上是“和0比较后取较大值”,这个比较操作在同态密文里代价极高,通常需要深电路或者复杂的多项式近似。Softmax里带有指数和对数,在密文上做更是灾难,噪声和深度预算都扛不住。
工程上的常见做法是:把ReLU替换成平方函数。平方激活在明文上是一次密文自乘,代价小,而且梯度简单,网络依然能训练。代价是网络的非线性表达能力略有下降,分类精度相比同结构ReLU网络会掉几个点,但在小规模图像分类任务上通常可控。Softmax则干脆不做,训练时用CrossEntropyLoss直接吃logits,推理时直接比较密文解密后的logits大小,省掉指数运算。
这里有一个必须强调的要点:替换激活函数之后,网络必须重新训练,而不是把训练好的ReLU模型权重直接搬过来用。很多第一次尝试的人在这里翻车,后面避坑章节会细说。
2.3 三条工程路线:明文训练-密文推理、蒸馏折半网络、全流程密文
加密图像分类的落地路线并不是只有一条,我梳理成三档:
| 路线 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 明文训练-密文推理 | 明文环境训练CNN,推理阶段把图像加密上传云,云端在密文上跑前向 | 训练无约束、成熟稳定,是本方案主线 | 推理网络结构必须同态友好,深度受噪声预算限制 |
| 蒸馏探路 | 用一个大的明文教师网络蒸馏出小规模、平方激活的密文可用学生网络 | 精度更接近大模型,推理深度可控 | 需要额外做蒸馏流程,工程链路变长 |
| 全流程加密训练 | 云端直接在密文上更新梯度 | 隐私保护覆盖训练和推理 | 目前算力开销不可接受,仅适合小型原型 |
我给大多数业务场景的建议是第一档:明文侧随便用大数据集训练,上线部署时把模型换成同态友好结构并做一次微调。这样既保住训练阶段的精度,又把上线阶段的隐私风险控制住。
3. 用Python与TenSEAL把加密图像分类跑起来:从明文CNN到密文CNN
3.1 环境准备:torch与tenseal是最小依赖
我在原型阶段最常用的组合是PyTorch负责明文CNN训练,TenSEAL负责CKKS加密和密文推理。TenSEAL是Python里封装比较完整的一个同态加密库,提供了CKKS上下文、密文向量和密文张量的基本算子,适合快速验证加密分类链路。
python -m venv he_env source he_env/bin/activate pip install torch torchvision tenseal这段命令创建虚拟环境并安装两个核心依赖。建议用Python 3.8到3.10之间的版本,tenseal对过新的Python版本支持往往滞后,遇到安装失败先检查Python版本。torch版本不必追最新,2.x即可。如果只用CPU推理,torch的CPU版就够,密文推理阶段本来也用不到GPU。
3.2 第一步:训练一个同态友好的CNN并导出权重
为了让密文推理能逐层对齐,明文CNN必须限制成“卷积+平方激活”结构,不要有ReLU、BatchNorm的均值方差归一化、也不要Softmax。下面这个模型以MNIST单通道28x28图像为例,两个卷积层之间用平方激活,最后用全局平均池化得到每类logits。
import torch import torch.nn as nn import torch.nn.functional as F class HomomorphicCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.conv1 = nn.Conv2d(1, 8, 3, padding=1) self.conv2 = nn.Conv2d(8, num_classes, 3, padding=1) def forward(self, x): x = self.conv1(x) x = torch.square(x) x = self.conv2(x) x = torch.square(x) x = x.mean(dim=(2, 3)) return x模型结构说明:输入1x28x28的灰度图,conv1输出8个通道,平方激活后在28x28空间上保持尺寸不变;conv2把8个通道映射到10个类别通道,再做一次平方,最后在空间维度上做全局平均,得到10个logits。没有中间池化层,是为了减少密文推理时的空间尺寸切换,也让旋转实现卷积的边界逻辑更容易对齐。
训练循环就是标准PyTorch训练,损失函数用CrossEntropyLoss,输入输出都是明文。训练完后导出权重,后续密文阶段直接读这些权重。
model = HomomorphicCNN() # 训练代码省略:标准交叉熵训练 # 导出state_dict为numpy,便于密文推理直接使用 state = model.state_dict() weights = { "conv1.weight": state["conv1.weight"].numpy(), "conv1.bias": state["conv1.bias"].numpy(), "conv2.weight": state["conv2.weight"].numpy(), "conv2.bias": state["conv2.bias"].numpy(), }导出的是conv1的(8,1,3,3)权重、conv2的(10,8,3,3)权重以及对应偏置。注意这里必须是训练完成后新模型的权重,不能用普通ReLU模型再改结构,否则精度对不上。
3.3 第二步:创建CKKS上下文并加密单张图像
CKKS上下文是密文推理的“运载火箭”,它决定了密文能算多深、能打包多少数据。下面的配置把一个MNIST图像展平后加密进一个密文向量。
import tenseal as ts import numpy as np def create_ckks_context(): context = ts.context( ts.SCHEME_TYPE.CKKS, poly_modulus_degree=16384, coeff_mod_bit_sizes=[60, 40, 40, 40, 60] ) context.global_scale = 2 ** 40 context.generate_galois_keys() return context def encrypt_image(context, img_tensor): img = img_tensor.numpy().astype(np.float64) img = (img - 0.1307) / 0.3081 flat = img.flatten().tolist() enc_vec = ts.ckks_vector(context, flat) return enc_vecpoly_modulus_degree决定了一次能打包的slot数量,16384对应8192个浮点slot,单个MNIST图只占784个slot,剩余空间以后可以塞其他图片做打包。coeff_mod_bit_sizes里的四个40对应四次密文乘法的深度预算,正好覆盖本网络显式的4次乘法:两层卷积、两次平方激活。global_scale用2的40次方,保证密文乘法后数值精度不因缩放而崩塌。
3.4 第三步:在密文上完成卷积、池化和平方激活
密文卷积用旋转实现:卷积核3x3的每一个位置,等价于把输入特征图整体平移一个偏移量后与对应系数相乘并累加。对密文调用rotate方法即可完成同态移位。
def enc_conv2d(enc_channels, weight, bias, W=28, kernel=3): in_channels = weight.shape[1] out_channels = weight.shape[0] outputs = [] for oc in range(out_channels): acc = None for ic in range(in_channels): for kh in range(kernel): for kw in range(kernel): shift = (kh - 1) * W + (kw - 1) rotated = enc_channels[ic].rotate(shift) term = rotated * float(weight[oc, ic, kh, kw]) acc = term if acc is None else acc + term outputs.append(acc + float(bias[oc])) return outputs def enc_square(enc_vec): return enc_vec * enc_vec def enc_global_avg_pool(enc_vec, length): ones = [1.0] * length return enc_vec.dot(ones) / length这段代码把卷积层变成“9次旋转、9次密文乘明文标量、9次密文累加”。shift计算里,kh减1和kw减1是为了把核中心对齐到当前像素。enc_square是平方激活,直接在密文上做一次自乘。全局平均池化用全1明文向量与密文做点积再除以长度,等价于所有像素求和取平均。
边界问题是这段代码最大的简化点:rotate是循环移位,图像边缘的像素会卷到对面去。完整实现里必须针对每个shift生成一个明文mask,先乘mask再乘权重,把跨边界的非法位置清零。演示代码省略了mask,所以密文结果与明文模型在边缘位置会有偏差,后面避坑章节专门说。
主推理流程把两层卷积和两个平方激活串起来:
context = create_ckks_context() enc_img = encrypt_image(context, sample_img) enc_x = enc_conv2d([enc_img], weights["conv1.weight"], weights["conv1.bias"]) enc_x = [enc_square(v) for v in enc_x] enc_logits = enc_conv2d(enc_x, weights["conv2.weight"], weights["conv2.bias"]) enc_logits = [enc_square(v) for v in enc_logits] cipher_logits = [enc_global_avg_pool(v, 28 * 28).decrypt() for v in enc_logits]每一层的输入输出始终是长度为784的密文向量列表,通道数从1变8再变10。执行到这里,云端的推理过程已经结束,密文logits被解密回明文,得到10个浮点数。
3.5 第四步:解密分类结果,并与明文结果对齐
解密后的logits直接比较大小取最大下标,就是分类结果。为了确认密文推理没有跑偏,我习惯把同一张图在明文模型上也forward一次,对比两者预测类别和logits分布。
def predict_from_logits(logits): return int(np.argmax(logits)) def compare_plain_cipher(plain_logits, cipher_logits): plain_pred = predict_from_logits(plain_logits) cipher_pred = predict_from_logits(cipher_logits) diff = np.max(np.abs(plain_logits - cipher_logits)) return plain_pred, cipher_pred, diff这里plain_logits是明文模型输出的10个浮点值,cipher_logits是上面解密得到的10个浮点值。diff能反映密文推理的数值误差,误差在1e-2量级时分类通常仍然正确;如果diff到了1或者更大,基本可以判断是深度预算或边界处理出了问题。
4. 参数与性能边界:poly_modulus_degree、打包策略与推理代价怎么权衡
4.1 一张参数表:把CKKS关键配置对齐到网络深度
CKKS参数不能照抄,必须按网络的实际乘法深度来配。下面是我每次动手前都会过一遍的对照表:
| 参数 | 作用 | 我的起点值 | 调大影响 | 调小影响 |
|---|---|---|---|---|
| poly_modulus_degree | 决定slot数量和最大模数 | 16384 | slot变多、速度变慢 | slot变少、装不下大图 |
| coeff_mod_bit_sizes | 决定可承受的乘法深度 | [60,40,40,40,60] | 深度更深、密文更大 | 深度不够、解密出噪声 |
| global_scale | 控制浮点精度与噪声预算 | 2**40 | 精度更高、噪声消耗快 | 精度下降、数值误差大 |
| 密文打包数 | 一个密文里塞几张图 | 1 | 并行度更高、串扰风险增加 | 更安全、带宽浪费 |
关键判断点在于算出网络的显式乘法次数。本网络两层卷积加两次平方,共4次乘法,所以coeff_mod_bit_sizes里给了4个40。如果你的网络只有3次乘法,可以把参数缩成[60,40,40,60];如果有5次乘法,就用16384甚至32768的degree,否则ctx创建时就会因为模数总位数超限而报错。
4.2 图像打包:单张密文和多张共享一条密文的取舍
一个poly_modulus_degree=16384的CKKS上下文,有8192个slot,而28x28的MNIST图展平只有784个slot,直接加密浪费了大半空间。实际系统中更常见的做法是batch打包:把多张图按通道或按像素位置塞进同一条密文,让一次密文乘法同时处理多条数据。
打包的核心是slot布局要跟卷积的rotate操作对齐。举例来说,如果一条密文里连续放了4张图的784个像素,那么rotate一个偏移量会把上一张图的像素卷到下一张图的位置,造成跨图串扰。解决思路是按图像维度分块排布,并在每张图之间留保护带,或者用mask把非法slot清零。打包能显著降低上传带宽和总推理时间,代价是mask设计和边界检查更繁琐。建议第一版先单图一条密文,跑通后再做多图打包。
4.3 性能预期:毫秒级明文推理与分钟级密文推理的真实差距
明文CNN在CPU上跑MNIST单张图在毫秒级,而密文推理单张图通常要几十秒甚至更久,主要时间花在两方面:一是每层卷积对每个输出通道要做九次rotate和九次密文乘明文,密文乘法的开销远大于明文;二是CKKS每次乘法后要rescaling控制噪声,这个步骤也会消耗算力。云端如果用单核CPU跑,整体等待时间会非常明显。
所以加密图像分类并不适合所有实时场景。它最合适的落点是离线的、批量化的敏感图像分析,比如夜间批量筛查医疗影像、森林巡检图像的分批分类。这类场景对单张响应时间不敏感,但对数据隐私有硬性要求。想降延迟,优先考虑多线程并行跑多张图,而不是指望单条密文算得更快。
5. 避坑:密文CNN最容易翻车的五个位置
5.1 明文模型精度97%,密文推理精度却掉到50%
现象:同一个测试集,明文模型分类准确率在97%左右,密文推理却接近随机猜测。 原因:直接把ReLU模型换成了平方激活,但没有重新训练。平方激活的拟合能力与ReLU不同,旧权重在输入分布上完全不匹配。 解决:用替换激活后的HomomorphicCNN从头训练,并先在明文侧验证平方激活版本的准确率。如果这个明文版本本身就掉到90%以下,不要急着上密文,先加宽卷积层或调整学习率把明文基线提起来。
5.2 边缘像素被循环移位卷回,密文logits在边界上错乱
现象:单张图密文推理的diff值很大,中央区域分类基本正常,但图像旋转后错判率升高。 原因:rotate在CKKS里就是循环移位,图像最右边一列会卷到最左边,上边一行会卷到下边。卷积核扫描到边缘时,邻域里混入了本不应该出现的远端像素。 解决:对每个卷积核偏移量生成一张与特征图等长的明文mask,合法位置标1、越界位置标0,把rotate后的密文先乘mask再乘权重。这个mask在明文侧预计算,推理时只多一次密文与明文向量的逐元素乘法,开销可控。
5.3 coeff_mod_bit_sizes配置不够,推理跑到中间层解密就崩
现象:前两层密文推理正常,第三层开始解密出来的数值变成随机大数。 原因:CKKS的乘法深度预算不足。每做一次密文乘法,噪声预算会消耗一个等级,超过预算后后续乘法结果完全失真。 解决:核对模型里的显式乘法次数,把coeff_mod_bit_sizes里的40数量对齐到乘法深度。宁可多配一个level也不要少配,多出的深度不影响正确性,只是密文变大、速度变慢。如果总位数超过poly_modulus_degree允许的上限,就把degree升到16384或32768。
5.4 在密文上强行做Softmax,解密结果全变成同一个类
现象:logits解密后数值相差很小,甚至全部接近同一个值,argmax总是固定一类。 原因:Softmax里的指数运算在CKKS上要用高次多项式近似,深度和噪声消耗都极大,而且数值范围压缩严重,最终把logits差异全部抹平。 解决:训练和推理都别碰Softmax。训练用nn.CrossEntropyLoss直接吃logits,推理直接比较解密后的logits。如果业务上必须输出概率,也在客户端解密后补一道softmax,不放在云端。
5.5 以为租个便宜GPU云端就能加速,结果密文推理反而更慢
现象:把密文推理搬到GPU上,总耗时比CPU单机更差。 原因:同态加密的主流开源库,底层核心运算和密钥切换是针对CPU指令集优化的,GPU上的实现要么没有要么需要额外拷贝开销。把密文张量从CPU搬到GPU的传输成本,可能比密文运算本身还要高。 解决:第一版就用CPU多线程跑,把一张图拆成多个通道并行处理,或者多张图并行,比盲目上GPU更有效。如果未来对延迟有硬需求,再考虑专用同态加密加速硬件或FPGA方案,而不是普通云端显卡。
6. 验证与进阶:密文分类一致性怎么测,以及值得继续投入的方向
密文分类系统上线前,建议用三类指标验证密文与明文的一致性。第一是Top-1类别一致率,最直接,反映业务可用性;第二是logits的Spearman相关系数,看排序是否接近,这比只看正确类别更严格;第三是logits数值最大绝对误差,用来排查具体哪一层产生了异常偏差。
from scipy.stats import spearmanr def validate_cipher_system(plain_logits_batch, cipher_logits_batch): plain_preds = [int(np.argmax(p)) for p in plain_logits_batch] cipher_preds = [int(np.argmax(c)) for c in cipher_logits_batch] top1_match = sum(a == b for a, b in zip(plain_preds, cipher_preds)) / len(plain_preds) rho = spearmanr(plain_logits_batch, cipher_logits_batch).statistic return top1_match, rho如果Top-1一致率在测试集上低于95%,先查是不是深度预算不够,其次查卷积边界mask是否完整,最后再看score是否过大。这三步走完,大多数问题都能定位到具体层。
值得继续投入的方向有两个。一个是多图打包,把前面提到的slot浪费补回来,用一次密文乘法同时处理多张图像,吞吐量能提升好几倍。另一个是知识蒸馏,用一个表达能力强的明文大模型蒸出“平方激活小模型”,把密文推理的深度和宽度控制在预算内,同时尽量保住精度。这两个方向叠加,加密图像分类就能从演示走向小规模生产。
我自己第一次跑通这个系统时,被一个低级错误卡了整整两天:明文网络里明明做了平均池化,密文侧却忘了实现同一步,导致特征图尺寸错位,解密出来的logits怎么看都不对。从那以后,我养成了一个习惯:先逐层对比明文和密文的中间结果,再对比最终分类,而不是只看最后的准确率。希望帮到你。
本文还有配套的精品资源,点击获取