crowd-anon-0.1b — anonymity-set size estimator
Ethical conditions (read before anything else)
This model does not identify people, and cannot be made to. It maps a description to how many people in a reference population could match it. The reference population is a probability model built from published aggregate statistics; there is no index of individuals anywhere in the system, nothing takes a person as input, and no output type has a field that could carry an identity. This is enforced by
tests/test_no_identification_path.py, not promised in prose.Outputs are a risk estimate and a set of masking candidates. Nothing else. There is no code path that produces "this description refers to X".
Counts are censored at a reporting floor of 5. Any combination matching fewer than five people is reported as "≤5". The model is trained with a censored (Tobit) likelihood so that it learns to say that, rather than learning to interpolate below the floor. A model that could tell one person from two would be a re-identification oracle.
The attack code that evaluates this model reproduces published techniques only, and is applied only to this project's own outputs. No new attack is developed, and the attack modules do not take a person as input.
This model does not guarantee anonymity and must not be described as if it did. Information usable for re-identification is unbounded in principle. This is a tool for estimation and prioritisation. Every number it emits is conditional on the reference population and the assumptions listed below.
What it does
Input: a Japanese (or English) description of a person.
Output: μ, σ over log10(number of people in the reference population matching
the quasi-identifiers in the text), converted to a calibrated interval by a
split-conformal wrapper fitted on held-out data.
from crowd.estimator import NeuralEstimator
est = NeuralEstimator("crowd-anon-0.1b")
e = est.predict(["都内で四十代前半の男性が子どもの心臓を診る医師として働いている。"])[0]
print(e.human()) # "~33 people (90% interval 6–190)"
print(e.meets(k=1000)) # False -> this description needs masking
Paired with crowd.optimize.MinimalMaskOptimizer it becomes a redactor that
hides the minimum needed to reach a target crowd size.
Architecture
| parameters | 110.3M (0.11B — inside the 0.1–0.3B design target) |
| layers / d_model / heads | 12 / 768 / 12 |
| feed-forward | 3072 |
| context | 48 tokens (corpus p99 = 31) |
| vocabulary | 32,000, byte-level BPE trained on this corpus |
| head | mean-pooled → MLP → (μ, log σ²) |
| loss | censored Gaussian (Tobit) NLL at the reporting floor |
| base model | none — trained from scratch, no download |
Attention is written out in plain matmuls rather than using a fused op, because ONNX, Core ML and the hand-written numpy runtime each lower a fused attention differently (or refuse). Plain matmuls export identically to all three, which is what makes "same weights, four runtimes" a checkable claim.
Why a censored likelihood
About an eighth of the corpus sits at or below the reporting floor. Training
those rows as exact observations of the floor teaches the model that very rare
combinations sit at exactly five people, which pulls its estimate for the
rarest — most dangerous — descriptions upward. Over-estimating a crowd is the
failure mode that gets someone identified. The Tobit term instead maximises
P(Y ≤ floor), which is both true and safe.
Formats
Exported and verified by scripts/export.py, which fails the export if any
backend disagrees with the torch reference by more than 0.05 log10 (about a tenth
of the calibrated 90% interval width):
- ONNX (opset 17), verified with onnxruntime
- Core ML (
mlprogram, fp16, iOS 16+), verified with coremltools - GGUF — f16 and Q8_0, carrying weights and tokenizer, verified against
crowd/gguf.py's pure-numpy runtime (no torch, no tokenizers)
general.architecture is crowd-anon, which llama.cpp does not know: the file
is valid GGUF and any tool can read its metadata and tensors, but the bundled
numpy runtime is the reference implementation and the one the benchmark measures.
Training data
crowd-anonymity-sets — ~250k synthetic descriptions paired
with exactly computed anonymity-set sizes. The label is not annotated: it is
belief propagation over a tree-structured log-linear model raked to published
Japanese marginals. Splits are on a hash of the attribute combination, so the
test set contains crowds the model has never been told the size of.
The data describes nobody. It is generated from a distribution, not sampled from a population of people.
Measured results
All figures produced by scripts/evaluate.py, scripts/export.py and
benchmarks/ in this repository; the artefacts they were read from are
committed at github.com/NagaYu/crowd.
Accuracy and calibration (test split, unseen attribute combinations)
| estimator | MAE | RMSE | bias | ECE | 90% interval coverage | ms/doc | peak RSS |
|---|---|---|---|---|---|---|---|
analytic (no model) |
0.289 | 1.186 | -0.288 | 0.289 | 93.0% (nominal 90%, width 0.83 log10) | 0.89 | 84 MB |
crowd-anon-0.1b |
0.350 | 0.543 | +0.025 | 0.169 | 85.3% (nominal 90%, width 1.71 log10) | 4.12 | 1284 MB |
| same weights via GGUF + numpy | 0.347 | 0.557 | +0.061 | 0.170 | 86.6% (nominal 90%, width 1.61 log10) | 85.1 | 1384 MB |
All errors are in log10 head-count units: 0.30 means a factor of two.
The analytic path has the lower MAE and a much worse RMSE — it is exact when its lexicon matches and catastrophically wrong when it does not. Its PIT statistic is 0.50 (a point mass, not a distribution) against 0.17 for the model. The learned model is the better probabilistic estimator; the lexicon is the better point estimator on text it was built for.
Robustness to euphemism (matched pairs, identical attribute combinations)
Plain-form MAE 0.255 vs oblique-form 0.282 — a penalty of +0.028 log10 over 3000 pairs that differ only in register.
Out-of-lexicon text — read the sign, not the magnitude
40 hand-written sentences using only expressions absent from the reference bundle. Over-estimating the crowd is the unsafe direction: it declares a dangerous description safe and no masking follows.
| estimator | MAE | bias | over-estimated by >1 log10 (unsafe) |
|---|---|---|---|
analytic |
2.89 | +2.89 | 95% |
neural |
3.80 | -3.80 | 0% |
ensemble |
3.80 | -3.80 | 0% |
Neither estimator is trustworthy here. They fail asymmetrically, and that is
the point: the lexicon fails unsafely, the model fails safely. EnsembleEstimator
takes the minimum of the two for exactly this reason.
Export verification
An export fails if any backend disagrees with the torch reference by more than 0.05 log10.
| format | size | max diff vs torch | ms/doc | verified |
|---|---|---|---|---|
onnx |
441.1 MB | 0.0000 log10 | 25.2 | yes |
coreml |
220.7 MB | 0.0011 log10 | 30.8 | yes |
gguf:f16 |
224.4 MB | 0.0001 log10 | 93.4 | yes |
gguf:q8_0 |
121.2 MB | 0.0052 log10 | 93.0 | yes |
Training
3046 steps of a requested 4000 (stopped at a 91-minute wall-clock budget), batch 64, lr 0.00015, on mps. The model is not trained to convergence — validation loss was still improving when the budget ran out.
Intended use
- Deciding what to mask in a document before release, and how much.
- Showing a writer or a clinician which detail is carrying the disclosure risk, so they can make an informed trade.
- On-device triage where sending the text to a hosted model is not acceptable.
Out of scope / misuse
- Not for identifying anyone. It cannot; do not build something that tries.
- Not a compliance certificate. It does not establish that a document is anonymous under GDPR, HIPAA, 個人情報保護法 or any other regime.
- Not validated for languages, countries or domains other than the shipped Japanese reference bundle.
- Not a substitute for review by a person accountable for the release.
Limitations
- The reference bundle is a research fixture. Internally exact and
reproducible; only approximately faithful to Japan's published statistics.
Figures are tagged
published/approximate/derived/illustrativeper entry. Rebuild from authoritative extracts before any real disclosure decision. - Synthetic training text. Narrower vocabulary and syntax than real clinical or legal prose. Expect degradation on genuine documents.
- Document-level only. Aggregating several releases about the same subject
defeats per-document masking;
crowd.robustness'saggregationattack measures the size of that gap but the model does not close it. - One value per attribute family. Multi-morbid descriptions are over-estimated — the model thinks the crowd is larger than it is.
- Conditional independence off the tree. Dependencies the reference forest does not model are assumed away, with an allowance added to the interval.
- Sensitive attributes are modelled (health condition, nationality) because they are genuine quasi-identifiers; omitting them would understate risk. They appear only as aggregate distributions.
Evaluation
The four-condition comparison (direct-identifiers-only / NER-mask-all / prompted
LLM / Crowd), the Pareto frontier, the attack-resistance numbers and the figures
are in the repository README at
github.com/NagaYu/crowd, generated from
artifacts/*.json so the prose cannot drift from the measurements.
The calibration protocol is: reliability diagram + nominal-vs-empirical coverage
- PIT histogram, against exactly-computed truth, on attribute combinations held out by hash.
Reproduction
make all # reference bundle -> dataset -> train -> export -> evaluate -> figures
Deterministic given the seeds. No network access required at any stage.
Licence and attribution
Apache-2.0 for the code and weights. The reference statistics derive from
Japanese government publications released under 政府標準利用規約(第2.0版)
(CC BY 4.0 compatible, attribution required); sources are listed in the bundle's
provenance block and by crowd reference.
- Downloads last month
- -
8-bit
16-bit