OpenMed 聚合发布隐私预算台账(PrivacyBudgetLedger)实战指南:本地确定性的 ε/δ 支出记账与证据导出
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
openmed.risk.PrivacyBudgetLedger是 OpenMed 中面向重复聚合发布(aggregate release)的本地隐私预算台账:调用方为每个命名发布上下文注册 epsilon 与 delta 上限,并在每次发布前原子记账,超预算立即失败关闭。本文以 docs/security/privacy-budget.md 为骨架,结合 openmed/risk/privacy_budget.py 源码与 tests/unit/risk/test_privacy_budget.py 测试,完整讲解账台账的用法、边界约束、证据契约与底层实现原理,读完可直接在你的发布流水线中接入这一确定性闸门。
为什么需要「发布上下文」级别的预算台账
OpenMed 原本在 openmed/risk/differential_privacy.py 中提供PrivacyBudget——一个基于 basic composition 的单数据集差分隐私顺序记账器(sequential accountant):构造一个实例并传给每次发布函数,内部累积PrivacySpend,通过spent_epsilon/spent_delta/remaining_epsilon暴露支出与余量。
但它只面向"一个数据集"的视角。实际生产环境里,同一份脱敏资产往往会被多个发布上下文反复消费——例如每日统计发布(daily-release)、研究用途发布(research-release)、批量测试发布(batch-release)。每个上下文需要独立的预算天花板,且各上下文之间互不串扰。PrivacyBudgetLedger正是在这一层补位:它把"预算管理"从"数据集"提升到"命名发布上下文",并且:
- 不检查、不存储任何源行(source rows),接受的支出记录只包含安全上下文标识符、epsilon、delta 与序列号;
- epsilon 与 delta 在每个上下文内部按顺序相加(sequential addition)保守组合,即 basic composition,绝不低估累积隐私损耗;
- 超预算请求在追加支出之前就抛出
PrivacyBudgetLedgerExceeded,并携带数字化的预计支出与剩余预算,集成方可以 fail closed,且不需要把请求载荷塞进异常里。
专用的预算上限类型为ReleaseContextPrivacyBudget;原有的PrivacyBudget差分隐私记账器保持不变,两者在 openmed/risk/init.py 中一同导出,互不冲突。
快速开始:本地使用
PrivacyBudgetLedger是一个纯本地对象,不需要网络、不落盘、不调用服务。最小用法如下(原文示例):
from openmed.risk import PrivacyBudgetLedger ledger = PrivacyBudgetLedger( { "daily-release": {"epsilon": 1.0, "delta": 1e-5}, } ) ledger.record_release("daily-release", epsilon=0.25, delta=2e-6) evidence = ledger.render_counts_only()构造时传入的是一个context -> {epsilon, delta}映射:每个键是一个命名发布上下文,值为该上下文的累计隐私预算上限。record_release会把这一次支出追加进台账,并返回对应的PrivacyBudgetDecision。每次调用即完成"发布前记账"的动作——建议把记账放在真正发出聚合结果的前一刻。
check:非变异的预检操作
check是只读预检:它计算"如果记上这一笔会怎样",但不改变台账。典型用法是把 check 与 record 组合成"先确认、再提交":
decision = ledger.check("daily-release", epsilon=0.5, delta=2e-6) if decision.allowed: ledger.record_release("daily-release", epsilon=0.5, delta=2e-6)决策对象PrivacyBudgetDecision携带的字段(见 privacy_budget.py)包括:
| 字段 | 含义 |
|---|---|
allowed | 是否在预算内 |
requested_epsilon/requested_delta | 本次请求的支出 |
projected_epsilon/projected_delta | 记账后的预计累计支出 |
max_epsilon/max_delta | 该上下文配置的预算上限 |
remaining_epsilon/remaining_delta | 记账后的剩余额度 |
reason | 拒绝原因,取值within budget/epsilon and delta exceed budget/epsilon exceeds budget/delta exceeds budget |
因此即使不用check,调用record_release返回的 decision 里也同时含有 projected 与 remaining 预算,异常路径同样如此——这是"无载荷失败关闭"的关键设计:异常中只出现数字,绝不出现请求正文。
check 与 record 的边界纪律
台账本身不会替你发出聚合结果、持久化文件或联系服务,因此调用方应当把 check-and-release 的边界保持得很近,例如:计算好聚合输出后立刻record_release,再发送/落盘,避免"预检通过后、正式记账前"的时间窗口内被别人把预算耗尽。
并发安全:check 是建议性的,record_release 才是原子闸门
检查与扣费由同一把本地锁(RLock,见 privacy_budget.py)保护,因此并发调用方无法各自通过一次过期的 check、然后集体突破上下文上限。
check只是 advisory(建议性):多个 worker 可以同时看到"还有余量";record_release才是 atomic gate:它在锁内重新做完整检查,只有 epsilon 与 delta 两个维度都达标时才追加支出。
该行为由测试test_threaded_charges_cannot_overrun_one_context_budget直接验证(tests/unit/risk/test_privacy_budget.py):16 个 worker 对epsilon=1.0的上下文发起 100 次epsilon=0.02的记账,最终恰好 50 次成功、50 次拒绝,spent_epsilon == 1.0,一分不超。
极小支出的精度保证
即使在二进制浮点世界里,账也必须记准:
- 任意正的最小浮点支出都会被记账:向已耗尽的预算追加一个极小值(如
5e-324,二进制64 最小次正规数)也不会被舍入掉而"免费通过"; - 被拒绝的、四舍五入后会回到上限值的投影,会保守地报告为严格高于上限。实现上,
_check_unlocked用Decimal(精度 400 位)做投影加法,当拒绝的投影值经float转换恰好等于上限时,用math.nextafter(budget.epsilon, math.inf)取下一个可表示值上报(privacy_budget.py)。
对应测试覆盖了1e-17、1e-100、5e-324三种极小值对 epsilon / delta 两个维度(tests/unit/risk/test_privacy_budget.py),并验证"先记一笔极小支出、再尝试记大额支出"时极小支出不会被加丢。
边界与输入校验:宁可拒绝,不回显
台账的输入面被刻意收窄,边界常量集中在 privacy_budget.py:
| 限制项 | 取值 |
|---|---|
最大上下文数MAX_PRIVACY_BUDGET_CONTEXTS | 512 |
最大已接受支出数MAX_PRIVACY_BUDGET_SPENDS | 10,000 |
epsilon 上限MAX_PRIVACY_BUDGET_EPSILON | 1,000,000(有限、非负) |
| delta 取值范围 | 有限,且[0, 1)(严格小于 1) |
| 上下文标识符 | 闭合的 64 字符 ASCII 语法:^[A-Za-z][A-Za-z0-9._-]{0,63}$ |
在输入校验上,以下情况一律拒绝,且错误信息中不包含调用方传入的值(防止敏感数据经异常泄漏):
- 数字字符串(如
"1.0")与非有限值(NaN、inf、超大的10**10000)——见_epsilon_float/_delta_float(privacy_budget.py); - 布尔值、重复别名(同时给
epsilon与max_epsilon会报duplicate aliases)、未知预算字段、无界的自定义 Mapping(_snapshot_mapping通过itertools.islice做有界读取); - 恶意/抛异常的 Mapping 输入——测试
test_hostile_budget_mapping_fails_without_echoing_values证明异常值不会被带进错误文本(tests/unit/risk/test_privacy_budget.py)。
此外,上下文名还做了PHI 形状检测:_PHI_PATTERNS会拦截 SSN(123-45-6789)与美式电话号码形状的标识符(privacy_budget.py),防止有人把真实标识符伪装成上下文名写进台账。
配置好的预算只通过不可变快照暴露:ledger.budgets返回MappingProxyType包装的副本(privacy_budget.py),contexts返回排序后的元组、spends返回按记账顺序的元组,外部无法就地修改。测试test_budget_configuration_is_not_mutable_through_public_access验证了对ledger.budgets[...]的赋值会抛TypeError。
动态调整预算用register_context(别名set_budget):可以新增上下文,也可以安全地调高上限,但不允许把上限调到低于已记录支出(否则会抛ValueError,见 privacy_budget.py)——这从根上杜绝了"悄悄重置已花费额度"的作弊路径。
证据契约:确定性、聚合、零载荷
render_counts_only()、to_dict()与to_json()三者返回确定性的聚合证据(to_json为sort_keys=True、紧凑分隔符的规范 JSON,见 privacy_budget.py):
{ "schema_version": "openmed.privacy_budget_ledger.v1", "context_count": 1, "attempt_count": 2, "release_count": 1, "rejected_count": 1, "spent_epsilon": 0.25, "spent_delta": 2e-06, "contexts": { "daily-release": { "attempt_count": 2, "release_count": 1, "rejected_count": 1, "spent_epsilon": 0.25, "spent_delta": 2e-06, "budget_epsilon": 1.0, "budget_delta": 1e-05, "remaining_epsilon": 0.75, "remaining_delta": 8e-06 } } }报告内容(render_counts_only,privacy_budget.py):
- 每个上下文的尝试 / 发布 / 拒绝次数(
attempt_count、release_count、rejected_count); - 配置的上限(
budget_epsilon、budget_delta); - 已消耗的 epsilon / delta 总量(
spent_epsilon、spent_delta,用math.fsum做浮点求和); - 剩余额度(
remaining_epsilon、remaining_delta,以 Decimal 计算、下限为 0)。
同时它有意省略:单笔支出明细、以及一切源数据——行、单元格、文档、收件人、自由文本请求值。上下文名被限制为安全标识符,PHI 形状的标识符直接拒绝。测试test_counts_only_evidence_is_deterministic_and_payload_free断言输出里既没有spends键也没有payload键,甚至不会出现"Synthetic Patient"或"123-45-6789"这类样本值(tests/unit/risk/test_privacy_budget.py)。
这样的证据可以安全地写入审计日志、上报给合规看板或用于对账,而不必担心把请求内容带出受控环境。
与既有预算机制的关系
PrivacyBudgetLedger不是孤立的,它在 OpenMed 的 risk 模块中与一系列预算治理机制协同:
- openmed/risk/differential_privacy.py 的
PrivacyBudget:单数据集、逐查询的 basic-composition 记账器,负责"这一笔发布本身的 DP 参数是否合法";台账则负责"这个发布上下文的总预算还剩多少"。 - docs/security/budget-migrations.md:DP 预算台账的迁移校验(
openmed.risk.budget_migration),确保预算台账在迁移/升级时不会静默重置支出或削弱发布上限——台账的to_json()确定性输出正是迁移对账的稳定输入。 - docs/security/exception-budgets.md:隐私例外预算机制,处理"允许例外但要记账"的场景,与正常发布预算互补。
一个典型的纵深用法是:用PrivacyBudget选择机制与校验单次聚合,用PrivacyBudgetLedger在发布前扣减上下文额度,用budget_migration验证台账跨版本迁移的一致性,最后用 counts-only 证据接入审计管线。
适用边界与正确预期
文档明确把台账定位为accounting gate(记账闸门),而非合规认证或临床决策保证:
- 它不选择隐私机制(Laplace、Gaussian 等由
DifferentialPrivacy/PrivacyBudget负责); - 它不证明声称的 epsilon/delta 数值在统计上成立——它只保证"累计记账不超过你配置的上限";
- 它不替代对发布总体(release population)与威胁模型的人工评审。
换句话说,epsilon/delta 上限本身是否合理、发布内容是否可安全公开,仍然需要数据治理团队在台账之外把关。台账的价值在于:一旦上限被设定,重复发布在工程层面就有一个本地、确定、并发安全、可出具零载荷证据的强制执行点。
小结
PrivacyBudgetLedger为 OpenMed 的聚合发布提供了一台轻量而严谨的本地预算闸门:命名上下文隔离、ε/δ 顺序相加、check/record_release分离、锁内原子扣费、Decimal 高精度记账、严格的标识符与数值校验、以及零载荷的确定性证据输出。接入成本只有一个 Python 对象,收益是重复发布场景下可审计、可对账、可 fail closed 的隐私预算纪律。相关实现与验证可直接在 openmed/risk/privacy_budget.py、tests/unit/risk/test_privacy_budget.py 与 docs/security/privacy-budget.md 中继续深入。
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考