☰
加密图像分类实战:基于Python与CKKS同态加密的CNN推理
2026/10/1 13:11:00 网站建设 项目流程

简介:面向隐私保护与深度学习的交叉方向,这套资源以 Python 卷积神经网络为核心,解决加密云端图像在不泄露原始内容的前提下完成分类的问题。适用人群覆盖零基础入门者和进阶学习者,可作为毕业设计、课程设计、大作业或初期项目立项的参照方案。压缩包共 2004 个文件,主体由 1296 个 JS 脚本、356 个 Markdown 文档和 341 个 JSON 配置/数据文件组成,另含少量 HTML 测试页、Python 源码与 TXT 说明,整体约 148.84MB。其中 Markdown 文档适合梳理系统架构与分类流程,JSON 文件便于调整参数和数据集,配合前端请求脚本与测试演示页面可快速搭建交互环境,直观理解加密图像分类的请求与响应机制。当前已有 116 人学习浏览,通过学习目录组织与关键代码,可完整掌握从模型推理到前端交互的实现链路,方便二次开发成自定义系统原型。

1. 加密云端图像的隐私分类:让CNN在看不见明文的云端把类别算出来

想象一个真实场景:医院把CT影像交给云端做病灶分型,合同上写着“数据不出域”,影像本身却早已被传了上去。加密图像分类就是把这张图像先加密再上云,让云端服务在密文上完成卷积、全连接和类别判定,最后只把标签送回来。这不等同于“加密传输”,传输加密只保护链路,云端模型看到的还是明文;而这里要解决的是模型也看不到明文还能给出正确分类。基于python卷积神经网络的加密图像分类系统,难点从来不在训练一个图像分类模型,而在让CNN的卷积和激活函数屈从于同态加密的运算规则。适合动手的人群:正在做隐私计算应用、医工交叉项目,或评估云端SaaS图像服务合规方案的技术人员。下面先讲清楚为什么CNN在密文里会“失灵”,再给一套能在本地复现的最小闭环。

2. 全同态推理与混合架构:加密图像分类的选型依据

2.1 同态加密与CKKS:能在密文上做加法和乘法的黑匣子

普通加密注重“解密后还原”,RSA这类公钥算法虽然也有少量同态性质,但运算次数非常受限,拿来做CNN推理基本不可行。真正能支撑图像分类的是全同态加密(Fully Homomorphic Encryption, FHE):给定密文 c1、c2,不需要密钥就可以算出 Enc(m1+m2)、Enc(m1×m2)。CNN里的卷积、全连接、批归一化(推理期)本质就是乘加组合,理论上都在FHE射程内。

FHE的实现算法主要有BFV和CKKS两系。BFV做整数精确运算,适合统计查询这类“结果必须可整除”的场景;CKKS做浮点近似运算,允许结果带少量可控误差,而图像特征图恰恰能容忍这种近似。所以落地加密图像分类,我一般优先选CKKS。用CKKS有一个概念必须先立住:密文域每次乘法后噪声会膨胀,系统通过 rescale 操作把噪声压回去,而 rescale 需要消耗“层级(level)”。层级用尽之前必须解密,否则误差会盖过信号。这就是为什么CKKS参数表里总有一串 coeff_mod_bit_sizes,它直接决定了模型能跑多深。

2.2 为什么不在密文域训练:区分训练态与推理态

把训练好的CNN放进密文域推理,是隐私分类项目实施时的通用做法。有些团队会先问:能不能把训练也搬进密文域?答案是代价极大。反向传播里的梯度计算涉及除法、比较、softmax,FHE能做,但一轮 epoch 跑上几个小时很正常;数据增强、洗牌、Mini-batch 采样这些训练期依赖随机性的环节,在密文域也会全部失效。更别说模型收敛时你需要反复看验证集曲线,训练过程本身没法全黑盒。

所以常见做法是明文侧训练、密文侧推理。这里要分清两个安全边界:被保护的是推理时的输入图像,不是模型参数。训练好的权重以明文配置形式交给云端推理节点,这个环节通过部署流程和密钥管理去管控;图像由客户端加密后上云,云端的推理程序只见过 Enc(image)、Enc(feature_map),私钥只留在客户端手里。以这个边界为前提,整个系统的落地难度会降一个数量级。

顺便说一句,transformer图像分类模型的自注意力核心是矩阵乘,理论上也能同态实现,但注意力矩阵需要 softmax,密文域做除法会剧烈消耗噪声预算。目前实际跑通的隐私图像分类方案几乎都是CNN,不是越来越大的transformer结构。这不是原理上禁止,而是工程代价暂时不划算。

2.3 混合推理架构:可落地的密文分类系统数据流

纯全同态推理(整网所有层都在密文域)论文里很常见,真正部署到云上却很吃力。最不好啃的是激活函数:ReLU需要比较操作,同态实现要用多项式近似,近似程度差一点,分类精度就掉几个点。工程上因此普遍采用混合架构:将网络按层拆成“线性层”和“非线性层”。卷积、全连接、平均池化保留在密文域;ReLU、最大池化、softmax 这类非线性挪到客户端明文域执行。中间特征图在客户端解密、激活、再加密回云端,私钥不下放。

一张 32×32 灰度图在整条链路中的流转可以归纳成下表的顺序:客户端明文做 padding、切块加密后上传;云端用明文卷积核对密文块做内积,返回密文特征图;客户端解密,做平方激活、平均池化、再加密送回;重复这个过程直到算出最后一层 logits,客户端解密取 argmax。表格里的每个节点,后端开发都能对应到一段具体代码。

环节数据形态归属方
图像采集与预处理明文图像客户端
切块与加密上传密文向量序列客户端
卷积与全连接计算密文中间特征云端
激活与池化解密后的明文特征(短暂)客户端
类别判定明文标签客户端或授权业务方

这套混合架构还有一个明显好处:客户端不需要持有模型权重,云端不需要接触明文图像,双方只交换密文中间结果。就算云端日志被拖走,攻击者拿到的也只是一堆没有私钥就无法还原的密文张量。

3. 设计加密友好型CNN结构:把ReLU换成平方激活后的关键调整

3.1 为什么ReLU是密文域最先被淘汰的那一层

卷积和全连接在密文域实现很直接:明文权重与密文向量做内积,FHE天然支持。但ReLU不一样,它对每个元素做 max(0, x),涉及密文域的大小比较,CKKS自身没有原生的比较算子,只能用多项式近似逼近。多项式近似引入的误差会随网络深度逐层累积,加密图像分类系统的精度崩盘,十次有八次出在这一层。

我在第一个版本里试过用泰勒展开的ReLU近似,多项式阶数取低了,分类准确率从基准的 94% 掉到 82%;阶数取高了,乘法次数增加,噪声预算又不够。反复调之后结论很明确:与其费劲近似ReLU,不如直接换一个“天生同态友好”的激活函数。平方激活 x² 只需要一次密文乘法,不涉及比较,不动用旋转密钥,是工程上最省事的替代品。代价是它只有单侧响应,网络容量会损失一些,要靠后面几层补回来。

3.2 把ReLU换成平方激活:训练收敛和数值范围的调整

换成 x² 之后,第一个要改的就是数值范围。ReLU输出与输入同量级,x² 会把大于1的数值平方放大。卷积层的输出如果落在 0~2 之间,经过一次平方后输出变成 0~4,两层之后特征图数值就可能膨胀到几十。密文域里数值越大,与明文缩放因子的比值就越难控制,最终导致解密后精度急剧下降。

训练时我一般做三件事:第一,把网络初始化标准差从默认的 xavier 缩小到 0.5 倍,控制卷积输出在 -1~1 之间;第二,加大权重衰减,让卷积核的模长偏小,避免大权重把激活推向高值区;第三,在平方激活之前加一个可学习的线性缩放系数,这个系数在明文侧训练,推理时折算进前一层的权重。这个系数是一维标量,解密后乘上即可,不增加密文乘法次数。经过这几处调整,x² 网络的收敛速度会慢一些,但最终精度可以追到 ReLU 版本的 2~3 个百分点以内。

3.3 一个加密友好的小型CNN示例:层表与训练参数

下面是我在实际项目里用过的一个加密友好网络结构,输入是 32×32 单通道图像,面向 10 分类。它的原则很明确:卷积和全连接层全部保留,激活统一为 x²,池化选择平均池化并放到客户端明文侧执行。最大池化需要比较操作,在密文域代价高,这里弃用;批归一化在推理期可以折叠进卷积权重,但训练期涉及除法和均值统计,我直接用权重衰减替代它的正则作用。

网络层配置输出尺寸执行环境
Conv13×3, 16通道, stride=116×30×30云端密文域
Act1x²16×30×30客户端明文域
AvgPool12×2, stride=216×15×15客户端明文域
Conv23×3, 32通道, stride=132×13×13云端密文域
Act2x²32×13×13客户端明文域
AvgPool22×2, stride=232×6×6客户端明文域
Conv33×3, 64通道, stride=164×4×4云端密文域
Act3x²64×4×4客户端明文域
FC11024→128128云端密文域
Act4x²128客户端明文域
FC2128→1010云端密文域

训练参数上,我用 Adam,学习率 1e-3,批大小 128,训练 60 个 epoch,权重衰减 5e-4。注意学习率不能沿用 ReLU 网络常用的 1e-2 起步,x² 的梯度形态与 ReLU 差异大,太大的学习率在第三个 epoch 就会让损失直接震荡。图像分类数据集下载后也不要全量训练,先切出 500 张图跑通加密推理链路,再上全量。

4. 用 Python + TenSEAL 跑通密文分类最小闭环:代码与参数说明

4.1 环境准备:Python 里的同态加密库怎么选

Python 生态里做同态加密,TenSEAL 是目前最顺手的库,它封装了CKKS的编码、加密、内积和 rescale,API 风格贴近 NumPy。PySyft 也能做,但它的设计更偏向多方安全计算框架,把简单内积包装得比较重,初次上手容易迷失在协议概念里。我的建议是先掌握 TenSEAL 再扩展到其他框架。环境安装很简单,Python 3.8 以上执行pip install tenseal即可;如果安装过程中卡在编译,多半是系统缺少 C++ 构建工具链,python 安装教程里关于编译器环境的部分值得提前看一眼。

pip install tenseal torch

安装后不要急着写业务代码,先做一个 10 秒的冒烟测试:生成上下文、加密一个向量、解密对比数值误差。CKKS是近似加密,解密结果与明文存在 1e-3 到 1e-4 量级的误差,这是正常现象;如果误差到了 1e-1 以上,说明参数配置不合理,后面所有推理结果都不可信。

4.2 明文预处理:切块、编码与加密

CKKS 密文本质是一个多项式向量,一次能打包 8192 个实数(以 8192 多项式模数为例)。CNN 的滑动窗在密文域没法直接索引,所以工程上常见的妥协是:在明文侧把图像切块,每个块加密成一个独立的 CKKS 向量,云端对该向量与卷积核做内积。切块操作可以用torch.nn.functional.unfold一次完成。

import torch import torch.nn.functional as F import tenseal as ts def build_context(): # 多项式模数 8192,系数模数三段式:60bit 输入层 + 40bit 中间层 + 60bit 输出层 ctx = ts.context( ts.SCHEME_TYPE.CKKS, poly_modulus_degree=8192, coeff_mod_bit_sizes=[60, 40, 40, 60] ) ctx.global_scale = 2**40 # 生成旋转密钥,后续特征图重排或矩阵乘法会用到 ctx.generate_galois_keys() return ctx def encrypt_image_patches(image_tensor, kernel_size=3, padding=1): # image_tensor: [1, 1, 32, 32] patches = F.unfold(image_tensor, kernel_size=kernel_size, padding=padding) # patches shape: [1, 9, 1024],1024 是滑动窗口数量 ctx = build_context() enc_patches = [] for i in range(patches.shape[-1]): enc_patches.append(ts.ckks_vector(ctx, patches[0, :, i].tolist())) return enc_patches, ctx

这里每个 patch 是 9 维向量,加密成一个 CKKS 密文。滑动窗口总数等于输出特征图的像素数,在 32×32 输入下是 1024 个。代码里的poly_modulus_degree=8192决定了单次密文可打包的数据维度与安全强度,越低越快但越不安全;global_scale=2**40是明文在编码时的放大倍数,图像像素是浮点数,缩放越大精度越高,但噪声预算消耗也更快。4096 的模数多项式跑这个小网络性能更好,但安全强度会降到 80bit 以下,不建议公网部署使用。

4.3 密文域卷积:明文权重与密文内积

切块加密完成后,云端收到的是 1024 个密文向量。卷积计算变成对每个密文向量执行一次同态内积:密文向量不动,卷积核展开成明文向量,调用dot方法。这一步是密文域的全部核心计算,不涉及任何解密操作。

def conv3x3_enc(enc_patches, kernel_bias_list, out_h=30, out_w=30): """ kernel_bias_list: 每个输出通道一个 (weight_flat, bias) 元组 weight_flat 长度 = 16 * 1 * 3 * 3 对应的展平 """ conv_features = [] for weights, bias in kernel_bias_list: channel_out = [] for enc_p in enc_patches: # 明文权重向量与密文向量做内积 val = enc_p.dot(weights) channel_out.append(val + bias) conv_features.append(channel_out) # 组织成 [C, H, W] 的密文张量,后续交给客户端解密激活 return conv_features

dot方法在 TenSEAL 内部会把明文向量编码进同一个 CKKS 多项式的相位位置,再做一次 SIMD 式乘加,然后通过 internal rescale 降低噪声。这里必须注意偏置bias不能以明文直接加在密文上——加法可以,但 bias 数值过大会推高噪声,最好在明文训练阶段就把bias合并进卷积核的某个常数维度。做法是给输入 patch 额外补一个固定为 1 的分量,让卷积核最后一个权重等于 bias。这样偏置也变成乘法的一部分,噪声控制更均匀。

整个流程里我没有做旋转操作,因为切块已经把空间索引关系暴露在明文侧。代价是密文数量等于输出像素数,一次卷积会产生上千个密文对象。要减少密文数量,可以在预处理阶段加大滑动步长,或者把相邻 patch 打包进同一个 CKKS 向量——打包能充分利用 SIMD,但会让代码复杂度上一个台阶。先跑通,再优化,是我做这类系统的习惯。

4.4 客户端非线性层与二次加密:混合推理循环

密文卷积结束后,云端把密文特征图交还客户端。客户端解密后做平方激活与平均池化,再加密送回云端继续下一轮卷积。整个过程中传输的都是密文,客户端私钥始终不出本地。下面的函数演示了单个卷积层之后的“解密→激活→池化→再加密”过程。

def client_side_nonlinear(enc_feature, patch_h=30, patch_w=30, in_ch=16, ctx): # 解密所有通道特征 plain_features = [] for ch_data in enc_feature: channel_plain = [] for val in ch_data: channel_plain.append(val.decrypt()[0]) plain_features.append(torch.tensor(channel_plain).view(1, 1, patch_h, patch_w)) x = torch.cat(plain_features, dim=1) # [1, C, H, W] x = x ** 2 # 平方激活 x = F.avg_pool2d(x, kernel_size=2, stride=2) # 平均池化 # 展平并重新切块加密 flat = x.flatten().tolist()[0] enc_new = ts.ckks_vector(ctx, flat) return enc_new

注意最后一层全连接输出不需要激活,客户端解密后直接取 argmax 就是分类标签。如果想要概率值,就在明文侧跑一个 softmax,这一段不在密文域,不泄露中间特征之外的信息。整套流程跑完,我得到的准确率大约是基准明文模型低 3~5 个百分点,推理耗时在个人电脑上为秒级,换到云端 GPU 实例后可压缩到毫秒级。

5. 避坑与排查:加密图像分类系统最容易翻车的五个地方

5.1 像素缩放到 [0, 1] 时精度反而更差

现象:明文模型在 [0, 1] 归一化下训练得很好,换到加密推理后准确率掉到 50% 上下,接近随机猜测。排查发现密文解码的特征图数值几乎被噪声淹没。原因:CKKS 的global_scale=2**40是针对有效数值范围设计的,图像像素在 [0, 1] 区间时,卷积核均值也接近 0.01 量级,乘积累加后信号幅度远低于量化噪声。解决:推理链路里把输入像素调整到 [0, 16] 或 [0, 32] 范围,或者把第一层卷积核整体乘上一个放大系数,训练时同步调整;输出 logits 对应的分类阈值要重新标定。

5.2 深度超过 3 层后噪声预算断崖

现象:第 1、2 个卷积层解密后特征图还干净,第 3 层开始特征图变成白噪声。原因:每做一次密文乘法,噪声按多项式模数的底数增长;coeff_mod_bit_sizes=[60, 40, 40, 60]中间只有两级 40bit 用于 rescale,只够支撑有限次乘法。解决:一是把网络改浅,比如去掉一个卷积层,换大卷积核;二是增大系数模数,把中间层改成[60, 45, 45, 45, 60],但密文体积和运算耗时同步上涨;三是检查每一层是否有多余的密文与密文乘法——明文权重与密文内积只产生一层噪声,密文与密文相乘会让噪声翻倍,要坚决避免。

5.3 程序卡在矩阵运算前:Galois 密钥缺失

现象:4.2 节代码里不调用generate_galois_keys(),程序跑到某个张量旋转或矩阵乘法时报错,提示 Galois keys 不存在。原因:TenSEAL 中旋转密钥不是默认生成,而矩阵乘、通道重排这些操作在 CKKS 底层依赖旋转。解决:在构建上下文后立即生成,并把密钥序列化保存;云端只需要公钥和旋转密钥,私钥由客户端持有。私钥一旦在生成后丢失,所有密文都无法解密,这不算 bug,但要在系统设计时把密钥备份纳入运维流程。

5.4 最后输出层用了 softmax 导致解密值溢出

现象:全连接层输出正确,但客户端做 softmax 时 exp 后数值爆炸,返回 nan。原因:密文域最后一层输出没有做 scale 控制,logits 可能高达几十,softmax 里的 exp(30) 在双精度浮点下已经接近溢出阈值。解决:不要在密文域或解密后立刻做 softmax。把全连接输出减去最大值(明文侧做这一步),再对差值做 exp 和归一化;或者干脆不做 softmax,直接用 argmax。分类任务在乎的是排名顺序,softmax 只是让输出像概率,不是必需的。

5.5 密钥和中间结果分离做错,隐私保护沦为摆设

现象:系统上线后审计发现,云端日志里明文中间特征图偶尔出现,原因是客户端解密激活后,调试时顺手把特征图写进了日志文件。原因:混合架构中客户端短暂持有明文特征图,这是架构设计允许的,但明文数据被持久化后,攻击者拿到日志就等于拿到原始图像的稀疏表征。解决:明文特征图只允许驻留在内存,禁止写磁盘和日志;在所有调试接口后面加一层脱敏校验,凡是包含明文特征图的响应体都要拦截。密码学解决不了运维纪律问题,这是隐私计算项目上线前最容易忽略的一条。

6. 验收加密图像分类系统:从精度、噪声预算和推理耗时三个维度把关

6.1 三个必测指标

第一个指标是精度损失。明文模型准确率为基准,加密推理准确率与它的差值控制在 5 个百分点以内才可接受。第二个指标是噪声剩余预算。解密前检查密文对象的noise_budget,低于 20bit 说明参数余量不足,上线后有波动就可能翻车。第三个指标是单次推理耗时。32×32 输入在网络带宽 100Mbps 的模拟环境下,单次分类延迟要低于 3 秒才具备线上体验价值,否则只能走异步任务队列。

指标合格线说明
精度损失≤5%与明文基准对比
噪声预算剩余≥20bit低于 20bit 需调整参数
单次推理延迟≤3s本地 CPU 参考值

6.2 我常用的两个调参起点

第一,输入分辨率先缩到 32×32 再放大。很多隐私分类需求来自医疗影像,原始图动辄 1024×1024,直接加密会让 patch 数量爆炸。我会先在 32×32 上把链路跑通,确认精度损失符合预期,再用双线性插值下采样或裁剪推进到 128×128。第二,通道数从 16 起步,不要一上来就堆 64、128。密文域的计算量正比于乘积通道数,通道翻倍,延迟接近指数上涨。先用小通道模型拿到可接受精度,再逐步加宽,同时观察噪声预算是否还撑得住。

做第一个加密分类版本时,我在噪声预算上栽过一次:网络结构和明文版完全一样,只是换了激活函数,结果第三层卷积直接失效,特征图全是雪花点。后来才意识到,问题不在激活函数本身,而是我把池化也留在了密文域,平均池化里的除法一样会产生噪声。从那以后我给自己定了一条规矩:密文域只留乘加,任何除法、比较、非多项式运算全部挪到明文侧。这个习惯后来帮我少踩了很多坑。如果你是第一次搭这类系统,忘掉“端到端全密文”的执念,先按混合架构跑通一条最小链路,性能和安全边界都会清晰得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询