DP-Embed-Invert: Practical Local Text Anonymization via Metric-DP on Inverter-Friendly Embeddings
1. Introduction
The "anonymize before sending" use case — strip PII from a document before pasting it into a third-party LLM, search engine, or message — is currently served by two largely disjoint families of tools:
- Heuristic redactors such as Microsoft Presidio, regex-based sanitizers (e.g. erguteb/local-text-anonymizer), and rule + small-NER pipelines. These are fast, fully local, and inspectable, but they have unbounded false-negative rate by construction: anything the rules miss is leaked verbatim.
- Neural redactors such as the OpenAI Privacy Filter (a 1.5B-parameter bidirectional token classifier with constrained Viterbi decoding, released as Apache-2.0 weights). These are higher-precision in the categories they cover, but a third-party benchmark from Tonic.ai reports recall in the 10–38 % range across web, EHR, legal, and call-transcript domains, and OpenAI itself describes the tool as a "redaction aid, not a safety guarantee."
Neither family produces text with a formal (ε, δ)-DP guarantee. Metric-DP-on-embeddings work (Feyisetan et al., 2020, and follow-ups) does, but most published systems either (a) embed and release the embedding rather than re-decoding to text, or (b) use a small bag-of-words inverter that does not produce fluent natural language.
We ask a narrower, more practical question:
Can a fully-local artifact, built from off-the-shelf open-weight components, produce a rewritten version of the input text with an explicit per-document
(ε, δ)-DP guarantee for the rewriting layer, and at what cost to utility?
The artifact has three pieces stitched together:
- The bundled regex sanitizer to placeholder spans it can identify (
[EMAIL],[PERSON], …). - A metric-DP rewriting layer: per-chunk L2-clipped GTR encoder + analytic Gaussian mechanism (Balle & Wang, 2018).
- The published
jxm/gtr__nq__32__correctcorrector (Morris et al., 2023) to decode noisy embeddings back to text.
Our claim is restricted to practicality under the stated constraints; we make no claim of SOTA on PII detection or DP-text utility benchmarks.
2. Methodology
2.1 Pipeline
For an input string T:
- Run
regex_privacy_sanitizer.py→ list of(start, end, category, placeholder)detections. - Split
Tinto a sequence of alternating spans: PII placeholder, plain-text chunk, PII placeholder, …. LetK= number of plain-text chunks. - For each plain-text chunk
c_k(k = 1…K):- Embed:
e_k = mean_pool(GTR-t5-base.encoder(c_k))∈ ℝ⁷⁶⁸. Encoder only — no Dense projection, no L2-normalize. (This matches the distributionvec2textwas trained on; see § 4.1.) - Clip:
ē_k = e_k · min(1, C / ‖e_k‖₂). We useC = 1.5, justified by the empirical norm distribution‖e_k‖ ≈ 1.016 ± 0.111on NQ texts. - Add noise:
ẽ_k = ē_k + 𝒩(0, σ² I_{768})with σ from the analytic Gaussian mechanism for(ε_chunk, δ_chunk)-DP at L2 sensitivityΔ = 2C = 3.0. - Decode:
c̃_k = vec2text.invert(ẽ_k, num_steps=s).
- Embed:
- Re-stitch placeholders and rewritten chunks.
2.2 DP accounting
- Mechanism: analytic Gaussian (Balle & Wang, 2018, Algorithm 1). With
scipyavailable we compute σ exactly; otherwise we fall back to the classical boundσ = Δ √(2 ln(1.25/δ)) / ε, which is strictly larger and so still satisfies(ε, δ)-DP. - Composition: basic sequential —
ε_chunk = ε / K,δ_chunk = δ / K. This is conservative; advanced (RDP / GDP) accounting would tighten the per-chunk budget. - Default:
(ε, δ) = (16, 10⁻³)per document.
2.3 Two composition strategies, two privacy notions
The pipeline exposes two orthogonal knobs:
Composition. composition ∈ {basic, zcdp}.
- basic: each chunk is
(ε/K, δ/K)-DP via the analytic Gaussian mechanism (Balle & Wang, 2018); the document is(ε, δ)-DP by basic sequential composition. - zcdp: convert the per-document
(ε, δ)-DP target to the largest ρ such that ρ-zCDP implies(ε, δ)-DP (Bun & Steinke, 2016, Prop. 1.3):ρ = (√(ε + ln(1/δ)) − √ln(1/δ))². Each chunk is then aρ/K-zCDP Gaussian step withσ = Δ / √(2 · ρ/K). The document-level guarantee is identical(ε, δ)-DP; σ is strictly smaller for K > 1.
Privacy notion. metric_unit ∈ {None, u} for u > 0.
- None: worst-case
(ε, δ)-DP at L2 sensitivityΔ = 2 C. Replacing one chunk's input with any other chunk's input is(ε, δ)-indistinguishable. - u:
(ε, δ)-d-privacy (Chatzikokolakis et al., 2013; Feyisetan et al., 2020) withΔ = u. Inputs whose embeddings differ byuunits in L2 are(ε, δ)-indistinguishable; inputs at distancec·uare(c·ε, c·δ)-indistinguishable. This is a weaker, different privacy notion. ε does not transfer between the two: ε=16 in d-privacy mode is not comparable to ε=16 in standard-DP mode.
The four combinations and their σ at the default (ε=16, δ=10⁻³, K=4, C=1.5):
| basic | zcdp | |
|---|---|---|
standard-DP Δ=2C=3 |
σ ≈ 2.72 | σ ≈ 1.96 |
d-privacy Δ=1.0 |
σ ≈ 0.91 | σ ≈ 0.65 |
2.4 Threat model — what is and is not covered
| Component | Covered by formal DP? |
|---|---|
| Noised rewriting of plain-text chunks (steps 3a–3d) | Yes (analytic Gaussian + basic composition). |
| Placeholder substitution (step 1) | No. The regex layer is heuristic, deterministic, and may miss spans (FN) or over-redact (FP). Anything it misses is protected only by the rewriting layer. |
K (number of plain-text chunks) |
Treated as public. A fully sound treatment would either pad K to a fixed value or include it in the accountant. |
| Membership-inference / reconstruction attacks against the inverter outputs | Not evaluated. |
We make these gaps explicit because metric-DP (ε, δ) is not the same notion as the DP-SGD (ε, δ) familiar from training-data privacy. ε in metric DP measures indistinguishability from nearby points in the embedding metric, not from arbitrary inputs. A reader should not compare ε=16 here against ε=1 in DP-SGD literature without re-reading the metric-DP definition.
2.5 Opt-out (whitelist) and skip-redaction (sentence-split)
whitelist_categories=[c₁, …] causes detections of those categories to not be placeholdered; their content joins the embed/noise/invert path like any other plain text. Rationale: the rewriting layer offers a (weaker) protection even for un-redacted PII.
redact_pii=False skips the regex layer entirely. The input is split into sentences (NLTK Punkt tokenizer with a regex fallback) and every sentence flows through the embed/noise/invert path. Motivation: the heuristic detector is then not in the privacy-critical path at all — privacy is provided exclusively by the formal mechanism. Cost is two-fold: (a) PII surface strings can be recovered by the inverter from the noised embedding (no exact removal); (b) empirically (§ 4.7) sem-sim is lower at the same ε on our benchmark, because the regex-placeholder version benefits from short placeholder tokens preserved verbatim. We document both findings; both modes carry the same formal guarantee under the chosen composition / metric_unit.
3. Implementation
src/regex_privacy_sanitizer.py— vendored verbatim from erguteb/local-text-anonymizer; pure stdlib.src/dp_embed_invert.py— pipeline + analytic Gaussian σ + CLI; ~400 LOC.vec2text == 0.0.13,transformers == 4.44.2,torch 2.4.1+cu121.- Hardware verified: NVIDIA H100 80 GB, driver 535, CUDA 12.2.
4. Results
All numbers in this section were produced from the bundled scripts at seed 0 on this physical machine. None are imported from external benchmarks.
4.1 Sanity check: GTR embedding normalization
vec2text was trained on the un-normalized output of the T5 encoder + mean-pool path of sentence-transformers/gtr-t5-base (cf. vec2text/models/model_utils.py:143–149), not the canonical SentenceTransformer pipeline (which adds a Dense projection and L2-normalization). On 500 NQ-corpus texts (max-len 32):
| input | mode | BLEU | Tok-F1 | EM |
|---|---|---|---|---|
| un-normalized | inverter only (0 steps) | 51.13 | 75.13 | 11.6 |
| un-normalized | + corrector (20 steps) | 83.85 | 93.01 | 59.0 |
| L2-normalized | inverter only | 37.64 | 66.08 | 3.0 |
| L2-normalized | + corrector | 50.13 | 73.01 | 14.4 |
We therefore feed the inverter un-normalized vectors throughout the rest of this work.
4.2 Three-way comparison vs. heuristic baselines
Dataset: nvidia/Nemotron-PII, 50 examples, length ≤ 800 chars, seed 0. (The originally-targeted AI4Privacy pii-masking-200k is gated on Hugging Face and not loadable from this environment.)
| System | n | leak↓ | sem-sim↑ | len-ratio | sec/ex |
|---|---|---|---|---|---|
| Presidio (analyzer + anonymizer) | 50 | 0.313 | 0.871 | 0.99 | 0.04 |
| Regex sanitizer alone | 50 | 0.543 | 0.843 | 0.90 | 0.04 |
| DP-Embed-Invert (ε=16, δ=10⁻³, 20 steps) | 50 | 0.000 | 0.078 | 0.98 | 5.5 |
leak = fraction of gold PII surface strings that appear (case-insensitive substring) in the output. sem-sim = cosine similarity of all-MiniLM-L6-v2 embeddings of input vs. output (a deliberately different embedding model from GTR, to avoid favoring our own pipeline).
The pipeline trades almost all utility for a zero substring-leak. As § 4.3 below shows, the contributions of the regex layer and the noise layer to that zero are entangled.
4.3 Ablation — privacy budget ε (num_steps = 20, δ = 10⁻³)
| ε | leak↓ | sem-sim↑ | len-ratio | sec/ex |
|---|---|---|---|---|
| ∞ (no noise) | 0.203 | 0.763 | 0.70 | 5.59 |
| 100 | 0.000 | 0.073 | 1.10 | 5.57 |
| 32 | 0.002 | 0.080 | 1.00 | 5.60 |
| 16 (default) | 0.000 | 0.078 | 0.98 | 5.60 |
| 8 | 0.002 | 0.061 | 1.10 | 5.58 |
| 4 | 0.000 | 0.085 | 1.03 | 5.59 |
Two findings worth flagging:
- The DP noise layer is doing real work. At ε = ∞ (no noise, just regex placeholder + lossless embed/invert) the substring-leak is 20.3 % — the inverter, given an exact embedding of a chunk, can recover enough surface text to surface PII surface strings the regex missed. Adding any noise at ε ≤ 100 drives leak to ~0.
- The transition to the noise floor is sharp. Between ε = ∞ and ε = 100 sem-sim drops from 0.76 to 0.07 and then stays there for all smaller ε down to 4. We did not sweep between ε = ∞ and ε = 100 in this run; the entire useful-utility region appears to live in a band of ε we did not cover. (Future work: a finer log-scale sweep around ε ∈ {200 … 1000}.)
4.4 Ablation — corrector iterations (ε = 16, δ = 10⁻³)
| num_steps | leak↓ | sem-sim↑ | len-ratio | sec/ex |
|---|---|---|---|---|
| 0 | 0.000 | 0.097 | 0.83 | 0.57 |
| 1 | 0.000 | 0.097 | 0.83 | 0.57 |
| 5 | 0.000 | 0.069 | 0.96 | 1.63 |
| 10 | 0.000 | 0.077 | 0.98 | 2.95 |
| 20 | 0.000 | 0.078 | 0.98 | 5.57 |
| 50 | 0.000 | 0.078 | 0.98 | 13.45 |
At our DP budget, additional corrector steps do not improve substring-leak or semantic similarity. Wall-clock scales linearly with steps. This contrasts with the no-noise regime in Morris et al. (2023, Table 2), where 20 steps lifts EM from 11.6 to 59.0 — see § 4.1 above. The intuition is that the corrector minimizes the gap between the target embedding and the re-embedding of its current text hypothesis; when the target is heavily noised it does not correspond to any real text, so additional iterations either stay on the same plateau (steps 0/1 are already enough at this budget) or wander to a different plateau of similar embedding distance (no consistent improvement). We therefore lower the default to num_steps = 5 in the bundled demo for ~3× speedup with no measurable utility loss.
4.5 Ablation — composition × privacy-notion at fixed (ε=16, δ=10⁻³)
To test whether a tighter composition or a weaker privacy notion lifts utility off the floor we observed in § 4.3, we compare the four composition × metric_unit combinations at the default budget. num_steps=5. K̄ ≈ 4.8 across the 50 examples.
| mode | σ̄ | leak↓ | sem-sim↑ |
|---|---|---|---|
| basic + standard-DP (Δ=2C) | 3.20 | 0.000 | 0.069 |
| zcdp + standard-DP | 2.05 | 0.000 | 0.068 |
| basic + d-privacy (Δ=1.0) | 1.07 | 0.002 | 0.082 |
| zcdp + d-privacy | 0.68 | 0.000 | 0.080 |
zCDP shrinks σ by ≈ 36 % at identical (ε, δ)-DP. Switching to d-privacy with Δ=1 (a weaker, well-defined notion) shrinks σ by another ≈ 3×. Sem-sim, however, only moves from 0.069 to 0.080 — vec2text's noise sensitivity is the binding constraint, not the analytic σ.
4.6 Ablation — d-privacy zcdp ε sweep up to 512 (num_steps = 20)
| ε | σ̄ | leak↓ | sem-sim↑ |
|---|---|---|---|
| 32 | 0.41 | 0.000 | 0.082 |
| 64 | 0.25 | 0.002 | 0.100 |
| 128 | 0.16 | 0.002 | 0.111 |
| 256 | 0.11 | 0.008 | 0.153 |
| 512 | 0.07 | 0.015 | 0.222 |
| ∞ (no noise) | 0.00 | 0.203 | 0.763 |
Utility lifts off slowly. At σ ≈ 0.07 (ε=512 in d-privacy zCDP) sem-sim reaches 0.222 — a real but partial recovery of the 0.763 no-noise reference. Substring-leak rises proportionally as σ drops: 0.000 at σ=0.41 → 0.015 at σ=0.07 → 0.203 at σ=0. This reproduces the observation from § 4.3 that the inverter alone leaks PII when noise is not present, and shows that the trade-off is genuinely smooth rather than a sharp transition.
4.7 Ablation — redact (regex placeholders) vs. no-redact (sentence-split) (num_steps = 5)
We compare redact_pii=True (the regex layer placeholders detected PII before noise) against redact_pii=False (no regex; the input is split into sentences and every sentence flows through the embed/noise/invert path).
| mode | ε | K̄ | σ̄ | leak↓ | sem-sim↑ |
|---|---|---|---|---|---|
| REDACT, std-DP basic | 16 | 4.8 | 3.20 | 0.000 | 0.069 |
| REDACT, std-DP basic | 64 | 4.8 | 1.05 | 0.002 | 0.069 |
| REDACT, std-DP basic | 256 | 4.8 | 0.39 | 0.000 | 0.076 |
| REDACT, d-priv zcdp | 16 | 4.8 | 0.68 | 0.000 | 0.080 |
| REDACT, d-priv zcdp | 64 | 4.8 | 0.25 | 0.002 | 0.098 |
| REDACT, d-priv zcdp | 256 | 4.8 | 0.11 | 0.008 | 0.167 |
| NO_REDACT, std-DP basic | 16 | 4.5 | 2.99 | 0.000 | 0.023 |
| NO_REDACT, std-DP basic | 64 | 4.5 | 0.99 | 0.000 | 0.050 |
| NO_REDACT, std-DP basic | 256 | 4.5 | 0.38 | 0.002 | 0.068 |
| NO_REDACT, d-priv zcdp | 16 | 4.5 | 0.67 | 0.006 | 0.058 |
| NO_REDACT, d-priv zcdp | 64 | 4.5 | 0.25 | 0.002 | 0.069 |
| NO_REDACT, d-priv zcdp | 256 | 4.5 | 0.11 | 0.002 | 0.129 |
Two findings:
- Removing the regex layer hurts sem-sim at every (ε, mode) we tested. At the strongest configuration (d-priv zcdp ε=256), REDACT 0.167 vs. NO_REDACT 0.129. Two reasons: (i) preserved placeholder tokens (
[PERSON],[EMAIL]) carry a small amount of free semantic signal that the utility model's encoder picks up; (ii) regex-split chunks are shorter than sentence-split chunks, so the inverter has less material to garble. - NO_REDACT does not leak much PII at the same ε — worst row is 0.6 % at ε=16 d-privacy. The regex-out-of-loop story is cleaner (the heuristic detector is no longer in the privacy-critical path) but on this benchmark the empirical leak benefit is small while the utility cost is consistent.
For users who insist on a privacy story that does not depend on the heuristic detector at all, NO_REDACT remains correct. For users who are willing to keep the regex as a removal layer alongside the formal rewriting layer, REDACT delivers more sem-sim per ε on this dataset.
4.8 Verbal comparison: OpenAI Privacy Filter
We did not benchmark the OpenAI Privacy Filter on the same data. The comparison below is verbal, drawn from OpenAI's blog post and the Tonic.ai third-party benchmark.
| Axis | OpenAI Privacy Filter | DP-Embed-Invert |
|---|---|---|
| Architecture | 1.5 B-param bidirectional token classifier (BIOES + Viterbi) | Regex + 220 M-param GTR encoder + 220 M-param T5 corrector |
| License | Apache-2.0, on HF (openai/privacy-filter) |
Open-weight components |
| Runs locally | Yes (laptop, browser) | Yes (GPU strongly recommended) |
| Action on detected PII | Replace span with mask | Replace span + rewrite the rest with DP noise |
| Formal privacy guarantee | None claimed; OpenAI describes it as a "redaction aid, not a safety guarantee." | (ε, δ)-DP for the rewriting layer under stated assumptions |
| Reported quality | OpenAI claims SOTA on PII-Masking-300k. The Tonic.ai bench on 500+ real-world docs reports F1 0.18–0.65 with recall 10–38 % ("high precision but low recall"). | Our regex backbone has comparable or worse recall; the rewriting layer is the differentiator. |
| Failure modes | Conversational ("Visa ending in 4427"), non-standard formats, layout-dependent PII | Anything the regex layer misses is protected only by the metric-DP rewriting; § 4.3 shows the rewriting is meaningful but the (ε, δ) is per-document, not per-token. |
The two systems target different points on the (utility, leak, guarantee) frontier and are not strict substitutes. OpenAI Privacy Filter is a more capable detect-and-mask model and will be more useful when downstream utility matters and the user is willing to trust the model's coverage. Our pipeline is more useful when an explicit DP accounting is required and the user can tolerate near-total loss of downstream utility at the chosen ε.
5. Discussion and limitations
- Utility loss at small ε is inevitable, not a flaw of the implementation. A formal
(ε, δ)-DP or(ε, δ)-d-privacy guarantee on the rewriting of a 768-d clipped embedding forces a Gaussian noise scale at least σ_min(ε, δ, Δ) given by the analytic Gaussian mechanism (Balle & Wang, 2018) — there is no(ε, δ)-DP mechanism in this family that uses less noise. The publishedvec2textcorrector was trained on un-noised embeddings and empirically saturates well above σ ≈ 0.5; once σ ≥ σ_min(ε, δ, Δ) > 0.5, the inverter produces fluent-but-unrelated text. So a near-zero sem-sim at ε = 16 is the expected behavior of any hybrid that combines this strong privacy model with this off-the-shelf inverter, not a bug we could engineer away under the same constraints. - The accountant moves σ within a hard envelope, not below it. Across {basic, zcdp} × {standard-DP, d-privacy} we tighten σ by ≈ 6× (3.20 → 0.68) at fixed
(ε, δ) = (16, 10⁻³); utility moves from 0.069 to 0.080. § 4.6 confirms that climbing the curve all the way to σ ≈ 0.07 (ε = 512 d-privacy zCDP) only reaches sem-sim ≈ 0.22 — well below the no-noise 0.76. Improving this further requires a noise-robust inverter (out of scope), not a tighter accountant. - Comparing ε across modes is incorrect. Standard-DP ε and d-privacy ε measure different things. We list both side-by-side in § 4.5/4.7 only as engineering options; a user who picks d-privacy mode should not advertise the result as a comparable strengthening of standard DP.
Kis treated as public. A fully sound analysis would padKto a fixed value or include it in the accountant.- Inverter OOD.
vec2textwas trained on NQ short passages (≤ 32 tokens). On clinical / legal / call-transcript text the distribution shift degrades inversion even before noise is added. - No empirical attack evaluation. A complete privacy story would include membership-inference and reconstruction attacks against the inverter outputs at various ε. We did not run those.
- Benchmark is small (N = 50) and substitute (
nvidia/Nemotron-PIIrather than the gated AI4Privacy). - OpenAI Privacy Filter comparison is verbal only.
6. Conclusion
mix-dp-anonymizer is a small, honest research artifact: a fully-local hybrid that combines a heuristic regex redactor with a formally-private rewriting layer built from off-the-shelf open-weight components, with explicit privacy accounting (standard (ε, δ)-DP or (ε, δ)-d-privacy; basic or zCDP composition). It is not SOTA on PII detection and not a substitute for closed neural redactors when downstream utility matters. It is a reproducible single-GPU pipeline that delivers a per-document formal-privacy guarantee for the rewriting of un-redacted text.
Four empirical findings deserve to be carried out of this note:
- The DP noise layer is not vacuous. At ε = ∞ the inverter alone leaks ≈ 20 % of gold PII surface strings (§ 4.3, § 4.6 row 6). Without the noise the pipeline would be strictly worse than Presidio.
- The utility loss observed throughout is the unavoidable price of the strong privacy model. Any
(ε, δ)-DP or d-privacy mechanism in this family must use σ ≥ σ_min(ε, δ, Δ) (the analytic Gaussian lower bound), and the off-the-shelf vec2text corrector saturates well above σ ≈ 0.5. So the near-zero sem-sim at small ε is inherent to the (strong-DP + this inverter) combination, not a tuning issue. - The accountant only moves σ within that hard envelope. Across {basic, zcdp} × {standard-DP, d-privacy}, σ shrinks ≈ 6× (3.20 → 0.68) at fixed
(ε=16, δ=10⁻³); sem-sim only moves 0.069 → 0.080 (§ 4.5). Useful utility requires σ ≈ 0.07, which costs ε = 512 in d-privacy zCDP mode (§ 4.6) — and even there sem-sim is 0.22, still well below 0.76. - Removing the regex layer hurts utility on this benchmark.
redact_pii=False(sentence-split, every chunk noised) gives a cleaner privacy story but consistently lower sem-sim thanredact_pii=Trueat every ε we tested (§ 4.7), and the empirical leak reduction is small.
7. Reproduction
./install.sh && source .venv/bin/activate
# 3-example demo
python demo/run_demo.py
# § 4.2 — three-way comparison (Presidio, regex, ours)
BENCH_N=50 python bench/run_bench.py
# → bench/results.json
# § 4.3, 4.4 — ε ablation and num_steps ablation
BENCH_N=50 python bench/run_ablation.py
# → bench/ablation_results.json
# § 4.5 — composition × privacy-notion (4 modes at ε=16, num_steps=5)
# + small d-privacy zCDP ε sweep at num_steps=5
BENCH_N=50 python bench/run_modes.py
# → bench/modes_results.json
# § 4.6 — d-privacy zCDP ε sweep up to 512 at num_steps=20
BENCH_N=50 python bench/run_modes_followup.py
# → bench/modes_followup_results.json
# § 4.7 — redact vs. no-redact (sentence-split) at num_steps=5
BENCH_N=50 python bench/run_redact_ablation.py
# → bench/redact_ablation_results.jsonAll scripts are deterministic at seed 0. On an H100 the heaviest run is run_modes_followup.py (≈ 30 min); the others are 5–15 min each.
8. References
- Balle, B., & Wang, Y.-X. (2018). Improving the Gaussian Mechanism for Differential Privacy: Analytical Calibration and Optimal Denoising. ICML.
- Bun, M., & Steinke, T. (2016). Concentrated Differential Privacy: Simplifications, Extensions, and Lower Bounds. TCC. (Used for ρ-zCDP composition.)
- Chatzikokolakis, K., Andrés, M. E., Bordenabe, N. E., & Palamidessi, C. (2013). Broadening the Scope of Differential Privacy Using Metrics. PETS. (Origin of the d-privacy notion used in our
metric_unitmode.) - Feyisetan, O., Balle, B., Drake, T., & Diethe, T. (2020). Privacy- and Utility-Preserving Textual Analysis via Calibrated Multivariate Perturbations. WSDM. (d-privacy applied to word embeddings.)
- Mironov, I. (2017). Rényi Differential Privacy. CSF.
- Morris, J. X., Kuleshov, V., Shmatikov, V., & Rush, A. M. (2023). Text Embeddings Reveal (Almost) As Much As Text. EMNLP. arXiv:2310.06816. https://github.com/vec2text/vec2text
Reproducibility: Skill File
Use this skill file to reproduce the research with an AI agent.
---
name: mix-dp-anonymizer
description: Local privacy-preserving text rewriter — heuristic regex PII redactor + (ε,δ)-DP Gaussian noise on un-normalized GTR-base embeddings + vec2text inversion (Morris et al., 2023). Produces a rewritten document with explicit per-document differential-privacy accounting for the rewriting layer. Open-weight, fully local, single-GPU.
allowed-tools: Bash(git *), Bash(python *), Bash(pip *), Bash(curl *), Bash(./install.sh), Read, Write
---
# mix-dp-anonymizer (skill)
Read this whole document before running anything. Skip-ahead readers will
make incorrect privacy claims. The DP guarantee covers **only** the
rewriting layer; the regex layer is heuristic with FP/FN. See § 6
"Threat model".
> Companion document: `docs/research_note.md` in the same repo. It
> contains the formal DP analysis, the ablation tables, and a verbal
> comparison against the [OpenAI Privacy Filter](https://openai.com/index/introducing-openai-privacy-filter/). Read it after
> this file.
---
## 0. What this skill does (one-screen overview)
**Input.** A natural-language string `T` (1 sentence to ~1 paragraph).
**Output.** A rewritten string `T'` such that:
1. PII spans the regex layer **detects** are replaced by category
placeholders (`[PERSON]`, `[EMAIL]`, …). These spans are *removed*,
not perturbed.
2. All other text is **rewritten** by: encode → L2-clip → add Gaussian
noise → decode back to text. The encode/decode pair is GTR-base +
`vec2text`. The noise is calibrated by the analytic Gaussian
mechanism (Balle & Wang, 2018) so that the rewriting of every
non-PII chunk is `(ε/K, δ/K)`-DP, and the document-level guarantee is
`(ε, δ)`-DP under basic sequential composition over `K` chunks.
**Defaults.** `ε = 16`, `δ = 10⁻³`, clip `C = 1.5`, `num_steps = 5`,
`composition = "basic"`, `metric_unit = None` (worst-case `(ε, δ)`-DP),
`redact_pii = True`.
**Other supported modes.** `composition = "zcdp"` for tighter
composition under the same `(ε, δ)`-DP guarantee. `metric_unit = u`
for `(ε, δ)`-d-privacy at unit metric distance `u` (a *different*,
weaker privacy notion — see § 3.4). `redact_pii = False` to skip the
regex layer; the input is then split by sentence and every sentence
goes through the embed/noise/invert path. See `docs/research_note.md`
§§ 2.3, 2.5, 4.5–4.7 for empirical trade-offs.
**Hardware.** Strongly recommended: a CUDA-capable GPU with ≥ 6 GB VRAM
and a CUDA driver ≥ 525 (CUDA ≥ 12.1). On CPU each chunk takes seconds.
Verified on NVIDIA H100 80 GB / driver 535 / CUDA 12.2 /
`torch 2.4.1+cu121`.
---
## 1. Pipeline diagram
```
input text T
│
▼
┌───────────────────────────────┐
│ regex_privacy_sanitizer.py │ → list of detections (start, end, category, placeholder)
└───────────────────────────────┘
│
▼
split T into [chunk₀, [PII], chunk₁, [PII], chunk₂, …, chunk_K]
│
│ ┌─────────────────────────────────────────────────────────┐
│ for │ e_k = mean_pool( GTR-t5-base.encoder(c_k) ) ∈ ℝ⁷⁶⁸ │
└─►each─┤ ē_k = e_k · min(1, C / ‖e_k‖₂) # L2 clip │
c_k │ ẽ_k = ē_k + 𝒩(0, σ² I₇₆₈) # analytic Gaussian│
│ c̃_k = vec2text.invert(ẽ_k, num_steps) │
└─────────────────────────────────────────────────────────┘
│
▼
re-stitch: [chunk₀ → c̃₀] [PII] [chunk₁ → c̃₁] [PII] … [chunk_K → c̃_K]
│
▼
output text T'
```
`σ` is computed from `(ε_chunk, δ_chunk) = (ε/K, δ/K)` via the
**analytic Gaussian mechanism** at L2 sensitivity `Δ = 2C`. With `scipy`
present we compute σ exactly; otherwise we fall back to the strictly-
larger classical bound `σ = Δ √(2 ln(1.25/δ)) / ε` (privacy still holds,
output more noised).
---
## 2. Installation — for AI agents
The repository contains all source. **Clone, then run the included
`install.sh`. Do not skip the verification step in § 2.3.**
### 2.1 Clone
```bash
git clone https://github.com/erguteb/mix-dp-anonymizer.git
cd mix-dp-anonymizer
```
After cloning you should have, at minimum:
```
mix-dp-anonymizer/
├── SKILL.md ← you are here
├── README.md
├── install.sh
├── requirements.txt
├── src/dp_embed_invert.py
├── src/regex_privacy_sanitizer.py
├── demo/run_demo.py
├── bench/run_bench.py
├── bench/run_ablation.py
├── bench/run_modes.py ← composition × privacy-notion ablation
├── bench/run_modes_followup.py ← d-privacy zCDP ε sweep
├── bench/run_redact_ablation.py ← redact vs. no-redact ablation
└── docs/research_note.md
```
Verify:
```bash
test -f src/dp_embed_invert.py \
&& test -f src/regex_privacy_sanitizer.py \
&& test -f install.sh \
&& test -f requirements.txt \
&& echo "OK clone" || echo "MISSING FILES — re-clone"
```
If any of those files are missing, the clone is incomplete (network
interrupted, partial fetch). Re-run `git clone` before continuing.
### 2.2 Run the bundled installer
```bash
chmod +x install.sh # in case the executable bit was dropped
./install.sh
```
`install.sh` is a regular bash script with no hidden side-effects — it
creates `.venv/` *inside the cloned repo* and installs the **pinned**
dependency set we verified on the reference machine. The pin set is
deliberate — newer `torch` wheels (≥ 2.6) require driver > 545, and
`transformers ≥ 4.45` breaks `vec2text 0.0.13`'s `from_pretrained`
path. If you cannot or do not want to use the script, the equivalent
manual command is:
```bash
python3 -m venv .venv && source .venv/bin/activate && pip install --upgrade pip
pip install --index-url https://download.pytorch.org/whl/cu121 'torch==2.4.1'
pip install -r requirements.txt
# Optional baseline (skip if you don't need the bench):
pip install presidio-analyzer presidio-anonymizer && python -m spacy download en_core_web_lg
```
| Component | Pin | Why |
|---|---|---|
| `torch` | `2.4.1+cu121` from `download.pytorch.org/whl/cu121` | Works with driver ≥ 525, compatible with `vec2text`. |
| `vec2text` | `0.0.13` | Published inverter/corrector entry-points used by this skill. |
| `transformers` | `4.44.2` | `vec2text 0.0.13` is incompatible with `transformers ≥ 4.45` (meta-device error in `from_pretrained`). |
| `accelerate` | `0.34.2` | Matches `transformers 4.44.2`. |
| `tokenizers` | `< 0.20` | Matches `transformers 4.44.2`. |
| `huggingface_hub` | `< 0.25` | Older API used by `vec2text 0.0.13`. |
| `sentence-transformers` | `3.0.1` | Utility-metric model in benchmarks. |
| `datasets` | `2.21.0` | Benchmark dataset loading. |
| `numpy` | `< 2` | Matches the rest of the pin set. |
| `scipy` | latest | **Optional** — exact analytic Gaussian σ. Falls back to a strict over-estimate if missing. |
| `presidio-analyzer` / `presidio-anonymizer` + `en_core_web_lg` spaCy model | latest | **Optional** — bench baseline only. Disable by setting `INSTALL_PRESIDIO=0`. |
Activate the venv before running anything below:
```bash
source .venv/bin/activate
```
### 2.3 Verify the install (mandatory)
Run **all four** checks. If any fails, jump to § 2.4.
```bash
# (a) torch + CUDA
python -c "import torch; assert torch.cuda.is_available(), 'No CUDA'; print('cuda OK', torch.version.cuda, torch.__version__)"
# (b) vec2text + corrector download (~600 MB, first run only)
python -c "import vec2text; vec2text.load_pretrained_corrector('gtr-base'); print('vec2text OK')"
# (c) GTR-base encoder download
python -c "from transformers import AutoModel; AutoModel.from_pretrained('sentence-transformers/gtr-t5-base').encoder; print('gtr OK')"
# (d) end-to-end: rewrite one short sentence
python src/dp_embed_invert.py --text "Contact Jane Doe at jane@example.com." --epsilon 16 --steps 5
```
Expected output of (d) is approximately:
```
=== ORIGINAL ===
Contact Jane Doe at jane@example.com.
=== OUTPUT ===
<some inverter-generated paraphrase> [PERSON] <something> [EMAIL] <something>
[DP] eps_total=16.0 delta_total=0.001 K=2 eps/chunk=8.0000 sigma=1.4992 clip=1.5
```
The exact wording of the rewritten parts will differ between runs (they
are noised samples). The placeholders, `K`, `eps/chunk`, and `sigma`
are deterministic functions of the input and the parameters and should
match those above for the same input.
### 2.4 Concrete fallbacks if install fails
These are listed in order of severity. Each includes a verification
command. Do **not** silently continue past a failure of (b) or (c) —
both are required for the DP rewriting layer.
**(F1) `pip install torch==2.4.1+cu121` fails (no GPU, or wrong driver).**
Fall back to the CPU-only torch wheel. The pipeline still runs, but
each example is ~30–60 s instead of ~5 s:
```bash
.venv/bin/pip install 'torch==2.4.1' # CPU wheel, no +cu121 suffix
python -c "import torch; print('cuda?', torch.cuda.is_available())" # expect False
python src/dp_embed_invert.py --text "Hello world." --epsilon 16 --steps 5
```
If your driver is ≥ 545 and you want a newer GPU wheel: stay on
`torch == 2.4.1` regardless. Newer torch (≥ 2.6) introduces a
meta-device guard that breaks `vec2text.load_pretrained_corrector`.
**(F2) `vec2text.load_pretrained_corrector('gtr-base')` fails or hangs.**
Most common cause: HF rate-limit or network instability during the
~600 MB download. Retry with the fast-transfer backend:
```bash
.venv/bin/pip install hf_transfer
HF_HUB_ENABLE_HF_TRANSFER=1 \
python -c "import vec2text; vec2text.load_pretrained_corrector('gtr-base')"
```
If a Hugging Face account is required, set `HF_TOKEN`:
```bash
export HF_TOKEN=hf_xxx # https://huggingface.co/settings/tokens
huggingface-cli login --token "$HF_TOKEN"
```
The inverter is **mandatory** for this skill. There is no offline
substitute. If the inverter cannot be loaded after the retries above,
the only honest fallback is **regex-only mode** (no DP rewriting):
```bash
python src/regex_privacy_sanitizer.py --text "..." --format json
```
In regex-only mode, **the DP guarantee no longer holds**. Document this
clearly in any downstream output and do not advertise the result as
"differentially private."
**(F3) `vec2text` import fails with a `from_pretrained` / meta-device
error like `RuntimeError: You are using from_pretrained with a meta
device context manager …`.**
This means `transformers ≥ 4.45` was pulled in by some other
dependency. Force the pin set:
```bash
.venv/bin/pip install 'transformers==4.44.2' 'accelerate==0.34.2' \
'tokenizers<0.20' 'huggingface_hub<0.25' --force-reinstall --no-deps
python -c "import vec2text; vec2text.load_pretrained_corrector('gtr-base'); print('OK')"
```
**(F4) `scipy` missing or fails to import.**
Not fatal. The pipeline prints a one-line warning and uses the classical
Gaussian bound, which is strictly larger than the analytic σ — output is
more noised, privacy still holds. To re-enable the exact bound:
```bash
.venv/bin/pip install scipy
```
**(F5) `presidio-*` or `en_core_web_lg` missing.**
Not fatal for the skill itself; only `bench/run_bench.py` uses Presidio.
The script auto-detects this and skips the Presidio baseline with a
`[skip] PRESIDIO unavailable: …` line.
**(F6) `nvidia/Nemotron-PII` (used in `bench/`) fails to load.**
The dataset is public and ungated as of 2026-04. If it becomes
unreachable, switch to a substitute:
```bash
BENCH_DATASET=gretelai/gretel-pii-masking-en-v1 BENCH_N=50 \
python bench/run_bench.py
```
`bench/run_bench.py` and `bench/run_ablation.py` both auto-detect span
schemas with keys `start/end/label`, `entity/types`, or `text/value`.
**(F7) Out of GPU memory.**
The default `max_len=32` and batch-implicit-by-K usage already keep
peak memory low (~3 GB on H100). If you OOM on a smaller GPU, lower
`max_len` (e.g. 24) or run on CPU per F1.
---
## 3. How to call the skill
### 3.1 CLI — single string
```bash
# default: standard-DP, basic composition, regex-redact
python src/dp_embed_invert.py \
--text "Contact Jane Doe at jane@example.com about the merger." \
--epsilon 16 --delta 1e-3 --steps 5 --seed 0
# tighter composition under the SAME (eps,delta)-DP guarantee
python src/dp_embed_invert.py --text "..." --epsilon 16 --composition zcdp
# d-privacy mode (weaker, different notion; smaller σ for same nominal ε)
python src/dp_embed_invert.py --text "..." --epsilon 16 --composition zcdp --metric-unit 1.0
# skip regex placeholders; sentence-split and noise every sentence
python src/dp_embed_invert.py --text "..." --epsilon 16 --no-redact
```
### 3.2 CLI — stdin → JSON
```bash
cat document.txt | python src/dp_embed_invert.py --epsilon 16 --json > out.json
```
### 3.3 Programmatic
```python
import sys; sys.path.insert(0, "src")
from dp_embed_invert import rewrite
res = rewrite(
"Contact Jane Doe at jane@example.com about the merger.",
epsilon=16.0,
delta=1e-3,
clip_radius=1.5,
num_steps=5,
composition="basic", # 'basic' | 'zcdp'
metric_unit=None, # None = worst-case (eps,delta)-DP; float = d-privacy unit
redact_pii=True, # False = no regex; sentence-split everything
whitelist_categories=None, # see § 3.5
max_len=32, # tokenizer max-len per chunk; matches vec2text training
device=None, # None = auto (CUDA if available)
seed=0,
)
print(res.output) # str — rewritten text
print(res.spans) # list[Span] — per-span trace, see § 4
print(res.sigma) # float — actual noise scale used
print(res.epsilon_per_chunk, res.delta_per_chunk, res.n_chunks)
```
### 3.4 Parameter reference
| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `text` | `str` | — | Input string. |
| `epsilon` | `float` | `16.0` | **Document-level** ε. Pass `math.inf` for the no-noise sanity setting (no DP claim then). |
| `delta` | `float` | `1e-3` | Document-level δ. |
| `clip_radius` | `float` | `1.5` | L2 clip radius `C`. The DP analysis assumes embeddings live in the ball of radius `C`. |
| `num_steps` | `int` | `5` | `vec2text` corrector iterations. At our default ε, going past 1 has no measurable utility effect (research note § 4.4); we keep 5 for a small margin. |
| `composition` | `'basic' \| 'zcdp'` | `'basic'` | Composition over the K plain-text chunks. `'zcdp'` is strictly tighter for K > 1 at the same `(ε, δ)`-DP. |
| `metric_unit` | `float \| None` | `None` | If `None`: worst-case `(ε, δ)`-DP with sensitivity `Δ = 2·clip_radius`. If a positive float `u`: `(ε, δ)`-d-privacy at metric unit `u` (Chatzikokolakis et al. 2013) — a **weaker, different** privacy notion. ε does **not** transfer between the two modes. |
| `redact_pii` | `bool` | `True` | If `True` (default): regex placeholders detected PII before noise. If `False`: no regex layer; input is split by sentence and every sentence goes through embed/noise/invert. The `False` mode keeps the heuristic detector out of the privacy-critical path; the empirical sem-sim is lower at the same ε on our benchmark (research note § 4.7). |
| `whitelist_categories` | `list[str] \| None` | `None` | Detection categories that should **not** be placeholdered (only relevant when `redact_pii=True`). See § 3.5. |
| `max_len` | `int` | `32` | Per-chunk tokenizer max-length. Matches `jxm/gtr__nq__32`'s training distribution. |
| `device` | `'cuda' \| 'cpu' \| None` | `None` | `None` = auto. |
| `seed` | `int \| None` | — | Seeds the Gaussian noise + corrector sampling. Set for reproducibility. |
### 3.5 Whitelist (opt-out of redacting)
`whitelist_categories=["organization", "location"]` causes those
detector categories to be left in place — they enter the embed/noise/
invert path like any other plain text. Useful when the user wants those
categories to *survive in some form* (perturbed) rather than be
replaced by `[ORG]` / `[LOCATION]`. Cost: their exact strings can still
leak with non-zero probability, modulated only by σ.
Category strings match what the regex sanitizer outputs (case-
insensitive): `full person name`, `email address`, `phone number`,
`organization`, `location`, `ip address`, `card number`, …. Run
`python src/regex_privacy_sanitizer.py --list-rules` for the full set.
---
## 4. Output format
### 4.1 Text mode (`python src/dp_embed_invert.py …`)
```
=== ORIGINAL ===
<input text verbatim>
=== OUTPUT ===
<rewritten text>
[DP] eps_total=16.0 delta_total=0.001 K=4 eps/chunk=4.0000 sigma=2.7196 clip=1.5
```
The trailing `[DP]` line is **not optional** — it is the privacy
receipt. The current code prints fields:
`notion`, `composition`, `eps_total`, `delta_total`, `K`,
`eps/chunk`, `sigma`, `sensitivity`, `clip`. A caller that stores or
forwards the output without storing this line is not recording the
privacy parameters used. In d-privacy mode (`metric_unit` set), the
receipt's `notion` will read `(eps,delta)-d-privacy, unit=u`.
### 4.2 JSON mode (`--json`)
```json
{
"original": "Contact Jane Doe at jane@example.com.",
"output": "<rewritten>",
"spans": [
{ "text": "Contact ", "is_pii": false, "category": null,
"placeholder": null, "rewritten": "<inverted chunk>", "chunk_idx": 0 },
{ "text": "Jane Doe", "is_pii": true, "category": "full person name",
"placeholder": "[PERSON]", "rewritten": null, "chunk_idx": null },
{ "text": " at ", "is_pii": false, "category": null,
"placeholder": null, "rewritten": "<inverted chunk>", "chunk_idx": 1 },
{ "text": "jane@example.com", "is_pii": true, "category": "email address",
"placeholder": "[EMAIL]", "rewritten": null, "chunk_idx": null },
{ "text": ".", "is_pii": false, "category": null, "placeholder": null,
"rewritten": "<inverted chunk>", "chunk_idx": 2 }
],
"epsilon_total": 16.0,
"delta_total": 0.001,
"epsilon_per_chunk": 5.333,
"delta_per_chunk": 0.000333,
"sigma": 2.012,
"clip_radius": 1.5,
"n_chunks": 3
}
```
`spans[*].rewritten` is the per-chunk inverter output **before**
re-stitching; comparing it to `spans[*].text` shows what the noise +
inverter did to that chunk specifically. `n_chunks` is the K used in
the basic-composition split.
### 4.3 Intermediate artifacts you can inspect
| Field | Type | What it tells you |
|---|---|---|
| `res.spans[k].text` (`is_pii=False`) | `str` | The original plain-text chunk fed to the encoder. |
| `res.spans[k].rewritten` | `str` | The inverter output for that chunk. |
| `res.spans[k].placeholder` (`is_pii=True`) | `str` | The category tag substituted into the output. |
| `res.sigma` | `float` | σ actually used (analytic Gaussian σ, or its classical fallback if `scipy` is missing). |
| `res.epsilon_per_chunk` | `float` | ε allocated to each chunk after composition. |
| `res.n_chunks` | `int` | K, the number of plain-text chunks. |
---
## 5. Demo
### 5.1 Three-example demo
```bash
python demo/run_demo.py
```
Runs three preset inputs at `(ε=16, δ=10⁻³, num_steps=5)`. Verbatim
output from one example on the reference machine (the rewritten text
will differ between runs — these are noised samples — but the bracketed
quantities are deterministic):
```
========= example 1 =========
--- input : Contact Jane Doe at jane@example.com or call 415-555-0188 about the merger. Our office is downtown.
--- output : at Albenius Palisade was Albe [PERSON] "The Matbos, remix mountain", [EMAIL] [PHONE] , and the genre limits the pagini
--- DP : K=4 eps/chunk=4.000 sigma=2.7196 clip=1.5
```
Read this output as:
- The placeholders (`[PERSON]`, `[EMAIL]`, `[PHONE]`) appear in the same
order as the corresponding PII spans in the input.
- Everything between placeholders is the *inverter's noised reconstruction*
of the corresponding plain-text chunk. At ε=16 split four ways
(K=4 → ε/chunk=4) the noise σ ≈ 2.7 dominates the embedding norm
(~1.0), so the rewritten chunks are fluent-but-unrelated to the
input. **This is the unavoidable price of the strong privacy model,
not a bug.** Any `(ε, δ)`-DP mechanism on a 768-d clipped embedding
must use σ ≥ σ_min(ε, δ, Δ) (analytic Gaussian lower bound), and the
off-the-shelf `vec2text` corrector saturates well above σ ≈ 0.5 — so
near-zero downstream utility at small ε is inherent to the
(formal-DP + this inverter) combination. To climb the utility/leak
curve, raise ε; see `docs/research_note.md` §§ 4.3, 4.6.
### 5.2 Step-by-step demo (showing intermediate artifacts)
```python
import sys; sys.path.insert(0, "src")
from dp_embed_invert import rewrite
res = rewrite(
"Contact Jane Doe at jane@example.com about the merger.",
epsilon=16.0, delta=1e-3, num_steps=5, seed=0,
)
print(f"K = {res.n_chunks}, ε/chunk = {res.epsilon_per_chunk:.3f}, σ = {res.sigma:.3f}")
for i, s in enumerate(res.spans):
tag = f"[PII:{s.category}]" if s.is_pii else f"[plain k={s.chunk_idx}]"
src = s.text
dst = s.placeholder if s.is_pii else s.rewritten
print(f" span {i} {tag:<28} {src!r:60} → {dst!r}")
print()
print("FINAL:", res.output)
```
The per-span listing makes it visible *which* PII categories the regex
caught (and which it missed — visible as PII strings still inside the
`text` of `is_pii=False` spans).
### 5.3 Reproducing the research-note tables
```bash
# § 4.2 — three-way comparison (Presidio, regex, ours)
BENCH_N=50 python bench/run_bench.py
# → bench/results.json
# § 4.3, 4.4 — ε ablation and num_steps ablation
BENCH_N=50 python bench/run_ablation.py
# → bench/ablation_results.json
# § 4.5 — composition × privacy-notion (4 modes at ε=16, num_steps=5)
# + d-privacy zCDP small ε sweep
BENCH_N=50 python bench/run_modes.py
# → bench/modes_results.json
# § 4.6 — d-privacy zCDP ε sweep up to 512 at num_steps=20
BENCH_N=50 python bench/run_modes_followup.py
# → bench/modes_followup_results.json
# § 4.7 — redact (regex placeholders) vs. no-redact (sentence-split)
BENCH_N=50 python bench/run_redact_ablation.py
# → bench/redact_ablation_results.json
```
All scripts are deterministic at seed 0. On an H100 the heaviest run
is `run_modes_followup.py` (≈ 30 min); the others are 5–15 min each.
---
## 6. Threat model — read this before using
The privacy guarantee covers **only** the rewriting layer (steps
3a–3d of § 1) under the following assumptions:
| Assumption | Holds? |
|---|---|
| Embedding L2 norm ≤ `C` after clipping. | **Yes — by construction.** |
| `K` (number of chunks) is public. | **We treat it as such.** A fully sound treatment would pad `K` to a fixed value or include it in the accountant. |
| Adversary observes only the final output `T'`, not the intermediate embeddings or noise. | Standard. |
| In `redact_pii=True` mode (default), the regex anonymizer's placeholder decisions don't leak. | **Does not hold.** The detector is heuristic with FP and FN. We make **no DP claim** over its decisions. Anything it misses is protected only by the rewriting layer. |
| In `redact_pii=False` mode, sentence boundaries are public. | **We treat them as such.** Sentence count is a function of text length and basic punctuation, not of PII content. |
| ε in d-privacy mode (`metric_unit` set) is comparable to ε in standard-DP mode. | **It is not.** d-privacy ε measures indistinguishability between inputs whose embeddings are at most one unit apart, scaling linearly with embedding distance. Do not compare ε across modes; do not compare ε=16 here against ε=1 in DP-SGD literature. |
If you need stronger semantics (ε per-token, advanced composition,
empirical attack robustness), this skill is not sufficient. See
`docs/research_note.md` § 5 for the full limitations list.
---
## 7. References
Citations for the methods this skill stitches together; full reference
list is in `docs/research_note.md` § 8.
- Morris, J. X., Kuleshov, V., Shmatikov, V., & Rush, A. M. (2023).
*Text Embeddings Reveal (Almost) As Much As Text.* EMNLP. arXiv:2310.06816.
Code: <https://github.com/vec2text/vec2text>
- Balle, B., & Wang, Y.-X. (2018). *Improving the Gaussian Mechanism for
Differential Privacy: Analytical Calibration and Optimal Denoising.* ICML.
- Bun, M., & Steinke, T. (2016). *Concentrated Differential Privacy.* TCC.
(zCDP composition for the `composition='zcdp'` mode.)
- Chatzikokolakis, K., et al. (2013). *Broadening the Scope of Differential
Privacy Using Metrics.* PETS. (d-privacy notion used by `metric_unit`.)
The bundled `src/regex_privacy_sanitizer.py` is vendored verbatim from
<https://github.com/erguteb/local-text-anonymizer>.
---
## 8. Quick checklist for an AI agent invoking this skill
1. `git clone https://github.com/erguteb/mix-dp-anonymizer.git && cd mix-dp-anonymizer`
2. `./install.sh && source .venv/bin/activate`
3. Run **all four** verification commands in § 2.3.
4. If any of them fails, follow the matching fallback in § 2.4 **before**
attempting to rewrite real text.
5. For each input document, call `rewrite(text, epsilon=…, delta=…)`
(programmatic) or the CLI in § 3.1.
6. Persist the `[DP]` receipt line / the
`epsilon_total / delta_total / sigma / n_chunks` fields together with
the output. Without those, downstream consumers cannot audit the
privacy claim.
7. **Do not** describe the output as "private" without the qualifying
statements from § 6.