Files
scam-guard-qwen06b/README.md

638 lines
28 KiB
Markdown
Raw Permalink Normal View History

---
license: apache-2.0
language:
- en
- ro
library_name: mlx
pipeline_tag: text-classification
tags:
- scam-detection
- fraud-detection
- smishing
- phishing
- on-device
- mlx
- gguf
- qwen3
- safety
- romanian
base_model: Qwen/Qwen3-0.6B
datasets:
- flowxai/scamguardbench
model-index:
- name: scam-guard-qwen06b
results:
- task:
type: text-classification
name: Scam verdict classification (ScamGuardBench v0.2, 120-item slice)
dataset:
name: ScamGuardBench v0.2
type: flowxai/scamguardbench
metrics:
- type: f1
name: verdict micro-F1
value: 0.958
- type: f1
name: verdict macro-F1
value: 0.926
- type: f1
name: tactic macro-F1
value: 0.951
- type: recall
name: evidence pass rate
value: 0.980
- type: false_positive_rate
name: legit-confusable FP-rate (scam_likely on legit)
value: 0.000
- task:
type: text-classification
name: Out-of-distribution fresh CERT-pattern messages (20-item hand-authored set)
dataset:
name: ScamGuardBench v0.2
type: flowxai/scamguardbench
metrics:
- type: accuracy
name: verdict accuracy (correct / 20)
value: 0.90
- type: f1
name: verdict macro-F1 (OOD)
value: 0.614
- type: false_positive_rate
name: OOD legit false-alarm rate
value: 0.000
---
## Inference contract
Running this model correctly requires its **frozen inference contract** — the exact
system prompt, output JSON schema, user-turn format, and constrained-decode spec it
was trained against. See [`inference_contract/`](./inference_contract):
- [`INFERENCE.md`](./inference_contract/INFERENCE.md) — wiring guide: system prompt, user turn `[channel: <tag>]\n<message>` (tag `sms`/`email`/`chat`), **constrained JSON decoding** (required — pins the enums), and the verbatim-evidence check.
- [`prompt_scamguard_sys_v1.txt`](./inference_contract/prompt_scamguard_sys_v1.txt) — the system prompt, verbatim.
- [`schema_scamguard_v1.json`](./inference_contract/schema_scamguard_v1.json) — output JSON Schema for constrained decoding.
Prompt version `scamguard_sys_v1`. Do not edit the prompt/schema; the weights are trained against them.
<!--
PREPARED FOR HUGGING FACE — NOT YET UPLOADED. Nothing here has been published.
Working NAME: "scam-guard". A final release name is chosen by a human at the
Phase-7 review STOP; search-replace "scam-guard" to rename in one pass. The ORG
is flowxai (fixed): dataset flowxai/scamguardbench, this model
flowxai/<name>-qwen06b, its sibling flowxai/<name>-qwen17b.
This is the 0.6B (on-device) card. The 1.7B (quality) card is a sibling repo:
release/README_qwen17b.md -> flowxai/scam-guard-qwen17b.
The fine-tuned synthetic-bench metrics are PROVISIONAL-on-synthetic-bench; the
out-of-distribution fresh-message results are measured and included. All fine-tuned
numbers on this card are the FINAL CUDA 3-epoch run (superseding the MLX first pass).
-->
# scam-guard 0.6B (working name) — the on-device pick
**An on-device scam & fraud message detector for SMS, email, and chat text (English + Romanian).**
This is the **0.6B** model — the **smallest and fastest** of the two scam-guard
sizes, and the **on-device target** (~0.4 GB at int4). For higher out-of-distribution
accuracy at a larger footprint, see the sibling **[1.7B quality
pick](https://huggingface.co/flowxai/scam-guard-qwen17b)**.
Given one message, scam-guard returns a **3-level verdict**, the **manipulation
tactics** it found (each with a verbatim evidence span quoted from the message), a
**calm plain-language explanation**, and a **recommended safe action** from a fixed
list. It is built for everyday people — explicitly including **elderly and
non-technical users**, who are the most targeted.
The product insight that shapes everything: **the `suspicious` middle level exists
to be honest about uncertainty rather than force a binary.** A consumer safety tool
that must answer "scam or not" will either cry wolf or wave real scams through; a
third honest verdict — "this might be fine, verify through your own channel first"
— lets the model say *I'm not sure* instead of guessing. Relatedly, scam-guard
**never emits a probability**: an uncalibrated confidence number on a consumer
safety tool is worse than none, so we give you honest per-class behaviour instead
(see [Calibration](#calibration--we-dont-give-you-a-probability)).
- **`flowxai/scam-guard-qwen06b`** (this card, 0.6B) — smallest and fastest; the on-device target.
- **`flowxai/scam-guard-qwen17b`** (1.7B, sibling) — the more robust choice out-of-distribution (see [Size decision](#size-decision)).
Both are LoRA-fine-tuned from Apache-2.0 Qwen3 base models. On-device formats:
**GGUF** (llama.cpp, int8 `Q8_0` + int4 `Q4_K_M`) and **MLX-quantized** (int4 + int8;
mlx-swift runs these on iOS too).
---
## How do I use it?
Three copy-pasteable ways to turn a message into a verdict. All run **fully
on-device** — no network at inference, ever.
Real example input (a fresh Romanian courier-fee smishing message):
```
Coletul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati taxa
vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata Livrarea se
anuleaza in 48h.
```
### (a) llama.cpp / GGUF
Download a GGUF (int8 `Q8_0` recommended) and run the message through it. The model
emits a single strict JSON object.
**`llama-cli` (CPU-only, `-ngl 0`):**
```bash
llama-cli -m scam-guard-qwen06b-Q8_0.gguf -ngl 0 --temp 0 -no-cnv \
-p "$(cat <<'EOF'
<system prompt: see src/scamguard/schema.py::SYSTEM_PROMPT>
[channel: sms]
Coletul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati taxa vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata Livrarea se anuleaza in 48h.
EOF
)"
```
**`llama-cpp-python`:**
```python
from llama_cpp import Llama
from scamguard.schema import SYSTEM_PROMPT, ScamGuardOutput # the fixed task prompt + schema
llm = Llama(model_path="scam-guard-qwen06b-Q8_0.gguf", n_gpu_layers=0)
msg = ("Coletul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati "
"taxa vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata "
"Livrarea se anuleaza in 48h.")
out = llm.create_chat_completion(
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"[channel: sms]\n{msg}"},
],
temperature=0.0,
)
raw = out["choices"][0]["message"]["content"]
verdict = ScamGuardOutput.model_validate_json(raw) # strict, extra="forbid"
```
### (b) MLX (Apple Silicon)
Off the MLX-quantized weights (int4/int8):
```bash
mlx_lm.generate --model scam-guard-qwen06b-mlx-int4 --temp 0 \
--prompt "$(printf '[channel: sms]\nColetul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati taxa vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata Livrarea se anuleaza in 48h.')"
```
```python
from mlx_lm import load, generate
from scamguard.schema import SYSTEM_PROMPT
model, tok = load("scam-guard-qwen06b-mlx-int4")
msg = ("Coletul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati "
"taxa vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata "
"Livrarea se anuleaza in 48h.")
prompt = tok.apply_chat_template(
[{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"[channel: sms]\n{msg}"}],
add_generation_prompt=True, enable_thinking=False,
)
raw = generate(model, tok, prompt=prompt, max_tokens=256, verbose=False)
```
Either backend returns the **same strict JSON** for this message:
```json
{
"verdict": "scam_likely",
"tactics": [
{
"tactic": "subscription_trap",
"evidence": "achitati taxa vamala de 3,20 lei",
"explanation": "It asks you to pay a small fee to release a parcel."
},
{
"tactic": "urgency_pressure",
"evidence": "se anuleaza in 48h",
"explanation": "It invents a 48-hour deadline to rush you."
}
],
"explanation": "This looks like a scam because it uses a fake fee to prompt a payment and pressures you with an artificial deadline; do not act on it, and check with the real organisation through a channel you already trust.",
"recommended_action": "verify_via_official_app_or_site"
}
```
### (c) The demo verdict card (`demo/check.py`)
The reference demo renders that JSON as a card a family member can read. It
hard-enforces the no-network privacy promise:
```bash
echo "Coletul dumneavoastra nu a putut fi livrat. Pentru reprogramare achitati taxa vamala de 3,20 lei aici: http://colet-reprogramare.example.net/plata Livrarea se anuleaza in 48h." | python demo/check.py
```
Rendered output (the actual card, from `reports/ood_fresh_demo.md`):
```
====================================================================
scam-guard — message safety check
====================================================================
[!] VERDICT: Likely a scam
This message shows clear signs of a scam. You do not need to do
anything it asks. Take your time — real organisations are fine
with you checking first.
--------------------------------------------------------------------
What we noticed:
- Sending a fake renewal or invoice to make you call or click
seen in: "achitati taxa vamala de 3,20 lei"
- Rushing you with a deadline or threat
seen in: "se anuleaza in 48h"
--------------------------------------------------------------------
What to do:
Check directly using the company's official app or website
that you open yourself — not the link here.
--------------------------------------------------------------------
In plain words:
This looks like a scam because it uses a fake renewal invoice to
prompt a call and it pressures you with an artificial deadline;
do not act on it, and check with the real organisation through a
channel you already trust.
====================================================================
scam-guard is a helper, not a guarantee. When in doubt, verify
through a channel you already trust. It never opens links.
====================================================================
```
`demo/check.py` defaults to the 0.6B MLX model (this one); `--backend gguf` and
`--model <path>` switch weights/backend, and `--size 1.7b` selects the sibling.
`--channel {sms,email,chat}` sets the channel tag.
---
## How it works
```mermaid
flowchart LR
SMS --> SG["scam-guard 0.6B"]
Email --> SG
Chat --> SG
SG --> V[verdict]
SG --> T[tactics]
SG --> E[evidence]
SG --> X[explanation]
SG --> A[action]
```
Compact ASCII flow — three channels in, one local model, five fields out:
```
SMS
\
Email ---> scam-guard 0.6B
Chat /
|
+ verdict
+ tactics
+ evidence
+ explanation
+ action
```
Under the hood, `scam-guard` is: `tokenizer → Qwen3-0.6B (LoRA fine-tuned) →
constrained JSON decode → evidence verifier (verbatim-substring kill-switch:
drops fabricated spans)`.
**The evidence kill-switch is the safety-critical stage.** Every tactic must cite a
span that is a **verbatim substring** of the input message (whitespace-normalized
only — no case/diacritic folding). A tactic whose evidence is not found verbatim is
**dropped and counted** as fabricated, so the model can never hallucinate a quote to
justify a warning.
---
## Output schema
A single strict JSON object (`extra="forbid"`, frozen — a spurious field like
`confidence` is rejected):
- `verdict` — `scam_likely` | `suspicious` | `no_indicators` (**never a probability**).
- `scam_likely` — a clear scam mechanism is present and driven by tactics.
- `suspicious` — the honest middle: signals present but plausibly legitimate, or
only weak indicators (urgency alone, a link alone, an authority claim with no
ask). "Verify through your own channel first."
- `no_indicators` — no scam mechanism; legitimate messages can be urgent and contain links.
- `tactics[]` — each `{tactic, evidence, explanation}`, where `tactic` is one of 13
fixed ids and `evidence` is a **verbatim substring** (see the kill-switch above).
- `explanation` — one or two calm, actionable sentences.
- `recommended_action` — one id from a fixed list of 10 safe actions; the model can
never compose free-text advice that points back at the scammer's own channel.
The 13 tactics: `urgency_pressure`, `authority_impersonation`, `payment_redirect`,
`credential_phishing`, `courier_customs_fee`, `prize_lottery`, `investment_too_good`,
`romance_advance_fee`, `family_emergency_impersonation`, `tech_support`,
`link_obfuscation`, `refund_overpayment`, `subscription_trap`.
The 10 safe actions: `call_bank_official_number`, `do_not_click_link`,
`verify_via_official_app_or_site`, `call_family_member_known_number`,
`do_not_share_codes_or_credentials`, `do_not_send_money`, `ignore_and_delete`,
`report_to_authorities`, `check_sender_address`, `no_action_needed`.
---
## On-device privacy promise
**scam-guard makes no network calls at inference, ever.** It reasons over the
message text only, fully locally. URL handling is **lexical only** — it inspects the
visible URL string (lookalike domains, userinfo tricks, shorteners, punycode hints)
and **never fetches anything**. This is the whole point: it works on messages people
would never upload to a cloud service. The reference demo (`demo/check.py`)
hard-enforces this with a no-network guard. At ~0.4 GB (int4), the 0.6B is the
smallest footprint of the two sizes — the one most comfortable on a phone.
---
## Evaluation (0.6B)
Two evaluations, in order of what they tell you:
1. **Out-of-distribution (OOD) fresh messages** — the honest real-world signal.
2. **ScamGuardBench v0.2 synthetic bench** — a large in-distribution slice the model has
a home-field advantage on (read the caveat).
3. **Calibration** — class-level behaviour, since there is no probability to calibrate.
All fine-tuned numbers below are the **FINAL CUDA 3-epoch** run (`qwen06b_cuda`),
which supersedes the MLX first pass.
### Out-of-distribution results (fresh CERT-pattern messages)
20 **fresh** messages — 10 realistic scam patterns modeled on current CERT/DNSC-style
alerts (RO+EN) and 10 genuinely legit messages — **hand-authored from public
alert-pattern descriptions, NOT run through the synthetic generator** the model
trained on, sanitized, and asserted text-disjoint from training + every ScamGuardBench
version (`tests/test_ood_fresh.py`). This is the honest generalization test. Full
write-up: `reports/ood_fresh_demo.md`.
| Model | Correct / 20 | Dangerous MISSES (scam→no_indicators) | FALSE ALARMS (legit→scam_likely) | verdict macro-F1 |
| --- | --- | --- | --- | --- |
| `flowxai/scam-guard-qwen06b` (0.6B, on-device) | **18/20 (90%)** | 1 | **0** | 0.614 |
| claude-haiku-4-5 (OOD reference) | **19/20 (95%)** | 0 | 0 | 0.649 |
| `flowxai/scam-guard-qwen17b` (1.7B, sibling) | **19/20 (95%)** | 1 | **0** | 0.967 |
**Bench → OOD gap (the home-field advantage, quantified):** the 0.6B macro-F1 drops
**0.926 → 0.614 (−0.31)** on fresh messages. On a 20-item set macro-F1 is noisy (one
slip on a rare class moves it a lot), so the plain-verdict accuracy (18/20) is the more
stable read. Legit-FP stays **0.000**.
Honest reading:
- **The one dangerous MISS:** the RO WhatsApp family-emergency scam *"Mama, am
pierdut telefonul … poti sa imi trimiti 850 lei"*, waved through as
`no_indicators`. Family-emergency framing without an explicit money-transfer
keyword can slip past the model. This is a known RO-dominant pattern and the top
recall gap to fix; the frontier reference (haiku) catches it. It **persists on the
1.7B at 3 epochs too**, confirming it is a training-data gap, not a size/compute gap.
- **Zero false alarms** — no legit message was flagged `scam_likely`. The
"disable-in-a-week" failure mode did not appear on fresh legit traffic. (The 0.6B
soft-hedged one legit-adjacent scam to `suspicious` — the RO energy-subsidy scam —
reported, not counted as a false alarm.)
- **Format robustness held:** JSON validity 1.000 and evidence pass 1.000 on fresh
text (no repair/fallback needed).
**RO vs EN, OOD (plain per-language accuracy over the 20-item OOD set — 9 RO / 11 EN;
a per-language F1 is not computed in the reports):**
| Model | RO correct | EN correct |
| --- | --- | --- |
| `flowxai/scam-guard-qwen06b` | 7 / 9 | 10 / 11 |
RO is the first-class target language: the RO miss is the single family-emergency
scam (the 0.6B also soft-hedges the RO energy-subsidy scam to `suspicious`). On the
in-distribution bench the RO-dominant tactics score at ceiling
(`family_emergency_impersonation` F1 **1.000**, `courier_customs_fee` **1.000**,
`refund_overpayment` **0.889**), which is exactly why the OOD RO family-emergency
miss is the honest gap to close.
### ScamGuardBench v0.2 (synthetic, in-distribution) — with the home-field caveat
> **Honesty caveat — home-field advantage (read before citing these numbers).** The
> fine-tuned numbers **beat the frontier reference on this benchmark, and that does NOT
> mean the model is a better real-world scam detector.** ScamGuardBench v0.2 is built from the
> **same synthetic generator** the model trained on (held-out split,
> contamination-verified — no leakage of specific messages, but the **same
> distribution**: same output format, phrasing, tactic-to-message style). The
> fine-tune learned exactly that style. **The frontier reference is the honest upper
> reference for a cold-prompted generalist** — a better OOD proxy than the fine-tuned
> column — and the OOD results above are the real-world signal these bench numbers
> flatter.
Seeded **120-message** slice (18 `suspicious`, 39 legit-confusable). A false positive
is a `scam_likely` verdict on a legitimate message; `suspicious` is reported but
**not** counted as an FP. FP-rate on the legit-confusable subset is the **release gate**.
| Metric | **qwen06b (CUDA)** | claude-haiku-4-5 (frontier ref) | keyword (lower ref) |
| --- | --- | --- | --- |
| JSON validity (deployed decoder) | 1.000 | 1.000 | 1.000 |
| **raw** JSON validity (no repair) | 1.000 | n/a | n/a |
| verdict micro-F1 | 0.958 | 0.900 | 0.483 |
| verdict macro-F1 | **0.926** | 0.829 | 0.482 |
| tactic macro-F1 | 0.951 | 0.770 | 0.319 |
| evidence pass rate | 0.980 | 0.969 | 1.000 |
| **legit-confusable FP-rate** | **0.000** | 0.051 | 0.026 |
The reference `claude-haiku-4-5` **fails the FP gate** (legit-confusable FP 0.051,
2/39 — it over-flags genuine family money requests), while the fine-tune holds
**0.000**. That is the home-field-advantage signal, not a claim of superior
real-world judgment. `family_money_request` (a genuine "send me money" from family)
is the hardest legit class — every frontier model over-flags it — while this
fine-tune holds **0.000**.
> **Full-budget note (MLX → CUDA).** The shipped 0.6B is the CUDA 3-epoch run. It
> supersedes the earlier MLX ~1-epoch first pass (macro-F1 0.903 → **0.926**,
> micro 0.942 → **0.958**), while **holding legit-FP at 0.000**. The full budget
> bought +2.3 macro-F1 points in-distribution.
### Calibration — we don't give you a probability
scam-guard **deliberately emits no confidence percentage**. An uncalibrated number
on a consumer safety tool is worse than none: it invites false precision on a
judgement that is genuinely uncertain. Instead of a probability to calibrate, we give
you **honest per-class behaviour** so you know where the model is weak and where it is
safe.
**Confusion matrix** (`qwen06b` CUDA, ScamGuardBench v0.2, 120-item slice; rows = gold,
cols = predicted):
| gold \ pred | scam_likely | suspicious | no_indicators | recall |
| --- | --- | --- | --- | --- |
| scam_likely | 63 | 0 | 0 | 1.000 (n=63) |
| suspicious | 0 | 13 | 5 | 0.722 (n=18) |
| no_indicators | 0 | 0 | 39 | 1.000 (n=39) |
**Per-class precision / recall / F1** (`reports/eval_frontier.md`) — micro-F1 0.958,
macro-F1 0.926:
| Verdict | Precision | Recall | F1 | Support |
| --- | --- | --- | --- | --- |
| scam_likely | 1.000 | 1.000 | 1.000 | 63 |
| suspicious | 1.000 | 0.722 | 0.839 | 18 |
| no_indicators | 0.886 | 1.000 | 0.940 | 39 |
**Where the model is weak, and where it is safe.** The weak class is `suspicious`
recall (0.722, 13/18) — the honest middle is the hardest to catch — but `suspicious`
**precision is 1.000** (it never over-calls the middle). **Crucially, every
`suspicious` miss bleeds into `no_indicators` (the safe direction), never into
`scam_likely`, and no legit item is ever flipped to `scam_likely`** — which is why
the legit-confusable FP-rate is **0.000**. The model under-warns on ambiguous
messages rather than over-warning on real ones; for a consumer guard that is the
failure mode you want.
### Size decision
- **This model (`flowxai/scam-guard-qwen06b`, 0.6B) — smallest and fastest, weaker
OOD.** Passes all three release gates on the bench (JSON >99%, evidence >95%,
legit-FP <3%) and is a genuinely defensible on-device ship at ~0.4 GB (int4), but
drops hardest out-of-distribution (macro-F1 −0.31, 18/20).
- **The sibling `flowxai/scam-guard-qwen17b` (1.7B) — the more robust choice.** On
fresh messages the extra capacity generalizes materially better (19/20, no macro-F1
drop), a gap the in-distribution bench (both ~0.9+) did not surface. Where
robustness matters more than size/latency, [ship the
1.7B](https://huggingface.co/flowxai/scam-guard-qwen17b).
- **Both sizes share the RO family-emergency recall gap** (the one dangerous OOD
miss) and both hold legit-FP at 0.000. That shared recall gap is the honest thing
to fix before any release claim.
---
## Formats & on-device latency (0.6B)
**We targeted 150 ms. We measured ~1.1 s (best path). This target is currently not
met.** scam-guard emits a full multi-field JSON card (~138 tokens), not a single
label, so decode dominates latency.
The two headline configs for the 0.6B on the representative 300-char SMS (median,
M3 Max):
| Config | median | meets 150 ms? |
| --- | --- | --- |
| MLX int4, Apple-Silicon GPU (Metal) | ~1.1 s | no |
| GGUF int8 (`Q8_0`), CPU-only (`-ngl 0`) | ~3.3 s | no |
int4 GGUF (`Q4_K_M`) is a poor trade on this small model — on CPU it is *slower*
than int8 and degrades quality (a spot-check sample became invalid JSON) — so
**`Q8_0` is the recommended GGUF quant**, and MLX int4 is the fastest quality-holding
path.
<details>
<summary>Full per-quant numbers (0.6B)</summary>
GGUF (llama.cpp), CPU-only (`-ngl 0`), M3 Max — full JSON card, median over N=12
(`reports/benchmark_gguf.json`):
| quant | file size | median | JSON spot-check |
| --- | --- | --- | --- |
| int8 (Q8_0) | 639 MB | 3204 ms | 4/4 valid |
| int4 (Q4_K_M) | 397 MB | 4000 ms | 3/4 (degraded) |
MLX-quantized, Apple-Silicon GPU (Metal), M3 Max (`reports/benchmark_mlx_quant.json`):
| quant | weights size | median | JSON spot-check |
| --- | --- | --- | --- |
| int4 | 335 MB | 1153 ms | 4/4 valid |
| int8 | 633 MB | 1281 ms | 4/4 valid |
bf16 MLX-Metal latency/memory and per-input-length detail are in
`reports/benchmark.md`. **Core ML** (.mlpackage) was attempted; the LLM→Core ML
conversion is finicky (stateful KV-cache handling) and the attempt is documented in
`PROGRESS.md` rather than shipped as a fabricated artifact. GGUF and MLX are the
recommended on-device paths today.
</details>
A human decision is needed at the release STOP: accept the ~1–3 s latency, ship the
~1.1 s GPU/MLX path, or shrink the output contract to approach 150 ms.
---
## Intended use & limitations
**Intended use.** A **consumer triage aid** that explains *why* a message looks risky
and points you to *your own* trusted channel to verify. Runs on-device. Languages: EN
and RO at v1 (Romanian is a first-class citizen, not an afterthought); PL/HU planned
fast-follow through the same pipeline.
**Out of scope & limitations.**
- **Not a guarantee.** A verdict is a signal, not proof. Scammers adapt continuously;
the benchmark is versioned because patterns rotate.
- **Verdicts can be wrong in both directions** — a real scam may score
`no_indicators` (the OOD RO family-emergency miss is a documented example), and a
legitimate message may score `suspicious`. The `suspicious` middle level exists to
be honest about uncertainty rather than force a binary.
- **Known recall gap:** the RO family-emergency pattern (framing without an explicit
money-transfer keyword) can slip past this model (and the 1.7B); it is a confirmed
training-data gap, fixable with a data addition before any release claim.
- **The model never fetches URLs.** It cannot tell you where a link *actually*
resolves, only what the visible string suggests. A lexically-clean URL can still be
malicious.
- Not a replacement for a bank's fraud line, a national anti-fraud service, or human
judgment. The recommended action always routes to *your own* channel.
- **Text only** at v1: no image/OCR, no audio, no attachment parsing, no
email-header/routing analysis.
---
## Training data
- **Public seed layer (relabeled):** UCI SMS Spam Collection (CC BY 4.0), enron_ham
(SetFit/enron_spam ham slice; no explicit license → reference/research-use),
phishing_email (zefang-liu; LGPL-3.0). Relabeled into the verdict+tactic scheme with
the evidence kill-switch; per-source human spot-check kept relabel disagreement
under the 10% gate.
- **Synthetic layer:** generated from specs (tactic × channel × language × register),
including a `suspicious` middle-ground tier and adversarial keyword-evasion
paraphrases (the `hard` subset). Every bench-destined message passes a sanitizer
audit (reserved domains, non-dialable phones).
- **Balance:** ≥45% legitimate messages, RO ≥35%, ~40% of RO diacritic-free,
`suspicious` ~14.5% of the SFT train set.
- Split discipline: synthetic by spec-family, public by source-text hash; paraphrases
follow their parent's split. **Train ∩ ScamGuardBench = ∅** (contamination-verified).
Both sizes trained from the **same data**; the size decision is evidence-based (above).
## Fine-tuning
**CUDA full-budget 3-epoch LoRA** (`training/train_cuda.py`, transformers + PEFT +
trl `SFTTrainer`), from `Qwen/Qwen3-0.6B`. LoRA rank 16 / alpha 32 / dropout 0.05,
all-linear modules, adamw, cosine 2e-5→2e-6 with 60-step warmup, effective batch 4,
seq 1280, bf16, gradient-checkpointing, seed 20260703; **3 real epochs** on an
on-demand NVIDIA L4 (~2h16m, 0 OOM), thinking disabled. This **supersedes** the MLX
first pass (~1 epoch, bounded by Metal stalls); the full budget bought +2.3 macro-F1
points in-distribution.
> **Base-model note.** The exact repo `Qwen3-0.6B-Instruct` does **not** exist on
> Hugging Face — Qwen3 merged instruct+thinking into the single base repo
> `Qwen/Qwen3-0.6B` (instruction-capable, Apache-2.0). We fine-tune it with thinking
> disabled.
---
## Dual-use statement
We release a **detector**, a **benchmark**, and **pattern-level** explanations. We do
**not** release the scam-variant generation prompts as a standalone tool. Public
benchmark scam texts carry no real dialable phone numbers and no working URLs
(reserved/example domains and clearly-fake numbers only). Output explanations describe
the manipulation *pattern*, never instructions for constructing one.
## Links
- **Sibling model (quality pick, 1.7B):** [`flowxai/scam-guard-qwen17b`](https://huggingface.co/flowxai/scam-guard-qwen17b) — higher OOD accuracy, larger footprint (~1.1 GB int4).
- **Benchmark / dataset:** [`flowxai/scamguardbench`](https://huggingface.co/datasets/flowxai/scamguardbench).
## License
Apache-2.0 (weights, code, and benchmark). Base model `Qwen/Qwen3-0.6B` is Apache-2.0.
</content>
</invoke>