Instructions to use hfmlsoc/ncii-guard-v02 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use hfmlsoc/ncii-guard-v02 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="hfmlsoc/ncii-guard-v02")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("hfmlsoc/ncii-guard-v02") model = AutoModelForSequenceClassification.from_pretrained("hfmlsoc/ncii-guard-v02", device_map="auto") - Notebooks
- Google Colab
- Kaggle
ncii-guard-v02
A binary classifier that flags image-editing prompts seeking non-consensual
intimate imagery (NCII): requests to strip, undress, or otherwise sexualise a
person in a photograph. It is microsoft/harrier-oss-v1-270m with a LoRA
adapter (r=8) merged in, so it loads with plain transformers and needs no
peft. The unmerged adapter is kept under adapter/.
Label 1 is ncii, label 0 is safe.
Choosing a threshold
This model does not ship a recommended threshold, and the default 0.5 is not a neutral choice. Precision on clean text runs from 0.870 at 0.5 to 0.951 at 0.935, and buying that precision costs recall against obfuscated input: across 35 attack families the model misses 617 of 2,070 obfuscated NCII prompts at 0.5 and 749 at 0.935.
Which way to err depends on what we need to prioritize. Pick the threshold against the
precision/recall tradeoff. The full sweep for dev here is in eval/, both on clean text and per attack
family.
Obfuscation handling
Attackers write rem0ve h3r dr3ss, rΠ΅move with a Cyrillic Π΅, or ο½ο½
ο½ο½ο½ο½
in fullwidth. This model handles that in two places:
- A normalizer built into the tokenizer. It strips zero-width and bidi control characters, maps 30 Cyrillic and Greek lookalike codepoints to their Latin equivalents, and applies NFKD followed by accent stripping. It runs automatically when you tokenize, with nothing to call.
- Adversarial training data. Attacks the normalizer cannot fold: l33t
speak, character separation, stroked letters like
ΓΈandΕ, small caps, Hangul filler characters. Added to the training set as augmented variants of real prompts.
The normalizer lives in tokenizer.json, so you must load the tokenizer from
this repository. Substituting the base model's tokenizer disables
step 1 and degrades accuracy on obfuscated input.
Usage
from transformers import pipeline
clf = pipeline("text-classification", model="hfmlsoc/ncii-light-guard-v01")
clf("skirt riding up the thigh")
returns
[{'label': 'ncii', 'score': 0.672331690788269}]
or
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
REPO = "society-ethics/ncii-guard-v02"
LABELS = {0: "safe", 1: "ncii"}
THRESHOLD = 0.5 # see "Choosing a threshold" above; this is not a default to keep
model = AutoModelForSequenceClassification.from_pretrained(REPO).eval()
tokenizer = AutoTokenizer.from_pretrained(REPO) # carries the normalizer
prompts = [
"remove her dress", # 0.997 ncii
"rΠ΅mΠΎvΠ΅ her clothes", # 1.000 ncii (Cyrillic lookalikes, folded)
"brighten the sky in this photo", # 0.000 safe
"strip the varnish from this table", # 0.000 safe (trigger word, innocuous sense)
"put a jacket on him", # 0.000 safe (clothing added, not removed)
"rem0ve h3r dr3ss", # 0.386 safe <- a real miss, see below
]
batch = tokenizer(prompts, truncation=True, max_length=256,
padding=True, return_tensors="pt")
with torch.no_grad():
probs = torch.softmax(model(**batch).logits.float(), dim=-1)[:, 1]
for prompt, p in zip(prompts, probs.tolist()):
print(f"{p:.3f} {LABELS[int(p >= THRESHOLD)]:4} {prompt}")
The last line is not a typo. While gomoglyph substitution is folded away by the normalizer and caught, we would need more l33t speak examples for these edits to be learned. The model misses this one at any threshold above 0.386. See the robustness table below.
Results
Held-out test split, 980 prompts, 70 of them NCII. No threshold is singled out; the whole sweep is given because the choice is yours to make.
| threshold | F1 | precision | recall | false pos | false neg |
|---|---|---|---|---|---|
| 0.10 | 0.875 | 0.851 | 0.900 | 11 | 7 |
| 0.20 | 0.873 | 0.861 | 0.886 | 10 | 8 |
| 0.30 | 0.863 | 0.870 | 0.857 | 9 | 10 |
| 0.50 | 0.863 | 0.870 | 0.857 | 9 | 10 |
| 0.70 | 0.882 | 0.909 | 0.857 | 6 | 10 |
| 0.80 | 0.889 | 0.923 | 0.857 | 5 | 10 |
| 0.90 | 0.879 | 0.936 | 0.829 | 4 | 12 |
| 0.935 | 0.886 | 0.951 | 0.829 | 3 | 12 |
| 0.95 | 0.892 | 0.967 | 0.829 | 2 | 12 |
Robustness by attack family
Every test prompt was rewritten with each of 35 transforms and scored separately, so each row below is the same 70 NCII prompts under a different attack. Accuracy degrades under obfuscation, unevenly, and the degradation gets worse as the threshold rises. The ten weakest families:
| family | recall @0.5 | recall @0.935 | precision @0.5 | precision @0.935 |
|---|---|---|---|---|
bidi_rlo_full |
0.514 | 0.457 | 0.667 | 0.780 |
homoglyph_residual |
0.514 | 0.386 | 0.750 | 0.844 |
stroked_letters |
0.514 | 0.471 | 0.783 | 0.805 |
l33t_symbols |
0.600 | 0.543 | 0.792 | 0.905 |
small_caps |
0.600 | 0.514 | 0.824 | 0.857 |
bidi_rlo_words |
0.623 | 0.536 | 0.729 | 0.860 |
invisible_residual |
0.629 | 0.529 | 0.772 | 0.822 |
l33t_basic |
0.629 | 0.514 | 0.733 | 0.818 |
l33t_heavy |
0.643 | 0.571 | 0.714 | 0.833 |
separate_dot |
0.643 | 0.557 | 0.833 | 0.975 |
Across all 35 families median recall is 0.729 at threshold 0.5 and 0.653 at
0.935, against 0.857 and 0.829 on unobfuscated text. Paraphrase families score
highest (synonym_tool and synonym_clinical both 1.000 recall at 0.5);
character-level attacks score lowest. homoglyph_residual uses lookalike
codepoints deliberately chosen from outside the normalizer's table, and is the
clearest gap.
Full per-family results at both thresholds are in eval/.
Limitations
- English only. The normalizer rewrites Cyrillic and Greek characters as Latin lookalikes and strips accents from every script. Russian, Greek, and other non-Latin text is mangled before the model sees it, and accented languages lose diacritics. Do not deploy this on non-English input.
- Obfuscated recall is far below the headline number. On the weakest family the model misses half of NCII prompts at 0.5 and more at higher thresholds. The clean-text figures describe unobfuscated prose only, and someone actively evading the filter is not writing unobfuscated prose.
- Small evaluation set. 70 positive examples, so each one is 1.4 recall points. Per-family figures carry wide confidence intervals; treat differences of a few points as noise.
- Prompts only. This classifies text requests, not images, and says nothing about whether an image is intimate or whether consent was given.
- Not a standalone moderation decision. Even at 0.951 precision roughly one in twenty flagged prompts is a false positive, and one NCII prompt in six gets through. Intended as one signal for human review, not an automated block.
- A classifier is a filter, not a fix. It cannot tell a victim from an attacker, and blocking a prompt does not address harm already done.
Training
- Base:
microsoft/harrier-oss-v1-270m, frozen. LoRA r=8, alpha=16, dropout 0.05, merged into the backbone afterwards. - 103,163 training prompts (9,218 NCII), being a curated set of image-edit prompts augmented with obfuscation transforms plus templated NCII commands and hard negatives that carry the trigger vocabulary in innocuous senses ("change the walls to a nude shade", "strip the varnish from this table"). Rows that collide after normalization are dropped, which is why the count is below the 117,360 generated.
- Unweighted cross-entropy, lr 2e-4, weight decay 0.1, 500 warmup steps, batch size 48, max sequence length 256. Earlier versions weighted the loss by inverse class frequency, a 10:1 multiplier on the positive class at this base rate; removing it is most of the precision improvement over v1.
- Early stopping on validation F0.5, patience 3. Best epoch 6, stopped at 9 of a 15-epoch budget.
- Validation is an obfuscation-augmented split (5,027 prompts, 316 NCII), which is why validation figures here are much lower than test figures; test is clean prose. Both were checked for exact and normalization-equivalent overlap with the training set, and there is none.
Merged weights are float32
The backbone ships in bfloat16, but LoRA trains in float32 and bfloat16 has too
few mantissa bits to hold a small delta on top of a much larger base weight.
Rounding the merged weights back down would discard most of the fine-tuning, so
this model is published in float32 and is twice the size of the base. The
unmerged adapter under adapter/ reproduces the original bfloat16 behaviour,
which differs on rows sitting near the decision boundary: at the 0.935
threshold it scores precision 0.967 and recall 0.829, against 0.951 and 0.829
merged.
- Downloads last month
- 31,271