Downloads · 30 days
0
jonathonbrown/closing-doc-extraction-qwen3-8b-lora
closing-doc-extraction-qwen3-8b-lora is a machine learning model from jonathonbrown. Use it for the machine learning task on the model card, and read the license before you ship it in a product. It is set up for mlx. The card lists the license as mit.
A LoRA adapter for mlx-community/Qwen3-8B-4bit that extracts a fixed JSON schema of key fields from U.S. (Florida) real-estate closing documents.
Downloads · 30 days
0
Access
Public
Updated Aug 29, 2026
Repo size
38.8 MB
Likes
0
Public
Click a slice to open those files.
.safetensors38.8 MB · 100%
From the Hugging Face model README
A LoRA adapter for mlx-community/Qwen3-8B-4bit that extracts a fixed JSON
schema of key fields from U.S. (Florida) real-estate closing documents.
This adapter was trained on a corpus that is overwhelmingly synthetic:
qwen3:32b from a small set of Florida closing-document templates (deeds,
mortgages, closing disclosures, title commitments, condo/HOA disclosures, filled
contracts) with fabricated names, parcels, book/page, and instrument numbers.
Only 10 documents (real CFPB and FAR/BAR reference PDFs) are genuinely real.qwen3:32b and validated for schema conformance. There is
no independent human ground truth, so the adapter learns to reproduce
qwen3:32b's extractions on templated text.Treat this as a demonstration of the fine-tuning workflow and an
in-distribution result, not a production-ready document extractor. The only
genuinely out-of-distribution measurement here is the 10 real forms (below), and
even those carry qwen3:32b-generated labels.
Given the raw text of a closing document in the user turn, the model returns a
single JSON object with exactly these keys (null where a field is absent):
{
"document_type": null,
"parties": {"grantor": null, "grantee": null, "borrower": null, "lender": null},
"property_address": null,
"legal_description": null,
"loan_amount": null,
"purchase_price": null,
"dates": {"executed": null, "recorded": null, "closing": null},
"recording_info": {"book": null, "page": null, "instrument": null},
"documentary_stamp_tax": null
}
Money fields are plain JSON numbers; dates are YYYY-MM-DD strings. The adapter
was trained with no system prompt — supply only the user message. Adding a
schema-describing system prompt at inference measurably degrades results (see
below), so don't.
Held-out test set, n = 30, base model mlx-community/Qwen3-8B-4bit, greedy
decoding. "Naive" = base model, no schema given. "Schema prompt" = base model with
the full schema in a system prompt. The meaningful comparison is
fine-tuned (no schema prompt) vs schema-prompted baseline: the adapter
reaches full schema conformance and field accuracy with a shorter prompt and no
schema at inference.
| Metric | Naive | Schema prompt | Fine-tuned, no schema | Fine-tuned, schema prompt |
|---|---|---|---|---|
| Valid-JSON rate | 96.67% | 100.00% | 100.00% | 100.00% |
| Schema-conformance rate | 0.00% | 66.67% | 100.00% | 100.00% |
| Field accuracy (overall) | 5.00% | 90.00% | 100.00% | 95.21% |
Per-field accuracy:
| Field | Naive | Schema prompt | Fine-tuned, no schema | Fine-tuned, schema prompt |
|---|---|---|---|---|
| document_type | 40.00% | 93.33% | 100.00% | 80.00% |
| parties.grantor | 0.00% | 83.33% | 100.00% | 93.33% |
| parties.grantee | 0.00% | 83.33% | 100.00% | 93.33% |
| parties.borrower | 0.00% | 63.33% | 100.00% | 100.00% |
| parties.lender | 0.00% | 66.67% | 100.00% | 100.00% |
| property_address | 3.33% | 100.00% | 100.00% | 100.00% |
| legal_description | 3.33% | 100.00% | 100.00% | 100.00% |
| loan_amount | 0.00% | 100.00% | 100.00% | 100.00% |
| purchase_price | 3.33% | 93.33% | 100.00% | 86.67% |
| dates.executed | 0.00% | 93.33% | 100.00% | 90.00% |
| dates.recorded | 0.00% | 100.00% | 100.00% | 100.00% |
| dates.closing | 0.00% | 100.00% | 100.00% | 100.00% |
| recording_info.book | 0.00% | 93.33% | 100.00% | 90.00% |
| recording_info.page | 0.00% | 93.33% | 100.00% | 90.00% |
| recording_info.instrument | 0.00% | 93.33% | 100.00% | 100.00% |
| documentary_stamp_tax | 30.00% | 83.33% | 100.00% | 100.00% |
Adding the schema prompt to the fine-tuned model is a net regression (100.00% to 95.21% overall): the adapter was trained with no system message, so injecting one at inference is off-distribution. Serve it with the bare user message.
The only genuinely out-of-distribution data is the 10 real CFPB/FAR-BAR forms, which differ from the synthetic templates in length, layout, and content. Fine-tuned model, no schema prompt, generation cap raised to 4096 tokens (2048 was enough to truncate one dense document mid-JSON):
| Metric | Fine-tuned, no schema prompt (4096) |
|---|---|
| Valid-JSON rate | 100.00% |
| Schema-conformance rate | 90.00% |
| Field accuracy (overall) | 96.25% |
With only 10 documents each is worth 10 points, so these are coarse. Eight of ten
were extracted with full schema conformance and all 16 fields matching the
reference. The residual errors are genuine model behavior on unfamiliar documents
(one value disagreement; one real structure/extraction miss), not measurement
artifacts. Note the reference labels here were also produced by qwen3:32b, so
this measures agreement with the label-generator on real documents, not human
ground truth.
mlx-lm LoRA (fine_tune_type: lora).mlx-community/Qwen3-8B-4bit.Download the adapter and run generation, pointing --adapter-path at the folder.
Supply only the user message — no system prompt.
# 1. fetch the adapter
hf download jonathonbrown/closing-doc-extraction-qwen3-8b-lora --local-dir ./closing-doc-lora
# 2. generate (base model is pulled automatically)
python -m mlx_lm.generate \
--model mlx-community/Qwen3-8B-4bit \
--adapter-path ./closing-doc-lora \
--max-tokens 4096 \
--prompt "Extract the key fields from this document as JSON:
<document text here>"
Python API:
from mlx_lm import load, generate
from mlx_lm.sample_utils import make_sampler
model, tokenizer = load(
"mlx-community/Qwen3-8B-4bit",
adapter_path="./closing-doc-lora",
)
doc = "<document text here>"
messages = [{"role": "user",
"content": f"Extract the key fields from this document as JSON:\n\n{doc}"}]
prompt = tokenizer.apply_chat_template(
messages, add_generation_prompt=True, enable_thinking=False)
out = generate(model, tokenizer, prompt=prompt,
max_tokens=4096, sampler=make_sampler(temp=0.0), verbose=False)
print(out) # a single JSON object matching the schema above
MIT.
Training data pipeline, eval.py, the full RESULTS.md, and the committed
per-example evaluation outputs (so every number above can be recomputed) live in
the companion repository: