Downloads · 30 days
9
20% of all-time downloads
meharshh/qwen-contract-clause-extractor
qwen-contract-clause-extractor is a text generation model from meharshh. Use it when you need the model to write or continue text. It is set up for peft. The card lists the license as apache-2.0.
QLoRA fine-tune of Qwen2.5-1.5B-Instruct that extracts 12 key clause types from commercial contracts into structured JSON.
Downloads · 30 days
9
20% of all-time downloads
All-time downloads
46
Public
Repo size
234 MB
Likes
1
Public
Click a slice to open those files.
.pt148 MB · 51%
From the Hugging Face model README
QLoRA fine-tune of Qwen2.5-1.5B-Instruct that extracts 12 key clause types from commercial contracts into structured JSON.
Given raw contract text, the model returns a single JSON object with 12 fields, using null for any clause category not present in the contract:
parties, agreement_date, effective_date, governing_law, termination_for_convenience, cap_on_liability, non_compete, exclusivity, expiration_date, license_grant, document_name, renewal_term
These map to 12 of the 41 clause categories in CUAD (Contract Understanding Atticus Dataset), chosen for being common across most commercial contract types.
Not intended for: standalone legal advice, or any use where extracted output is relied upon without human review. This model has a non-trivial hallucination rate on absent fields (see Limitations).
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
base_model_id = "Qwen/Qwen2.5-1.5B-Instruct"
adapter_id = "meharshh/qwen-contract-clause-extractor"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(adapter_id)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_id, quantization_config=bnb_config, device_map="auto"
)
model = PeftModel.from_pretrained(base_model, adapter_id)
instruction = (
"Extract the following fields from this contract. "
"Return valid JSON only, with null for any field not present.\n"
"Fields: parties, agreement_date, effective_date, governing_law, "
"termination_for_convenience, cap_on_liability, non_compete, exclusivity, "
"expiration_date, license_grant, document_name, renewal_term\n\n"
"Contract text:\n<your contract text here>"
)
messages = [{"role": "user", "content": instruction}]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
output = model.generate(**inputs, max_new_tokens=512, do_sample=False)
print(tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
CUAD — 510 real commercial contracts with expert legal annotations, originally SQuAD-style span extraction. Reshaped into one instruction→JSON example per contract (285 train / 61 validation / 62 test).
| Quantization | 4-bit NF4 (bitsandbytes), bf16 compute |
| LoRA rank / alpha / dropout | 16 / 32 / 0.05 |
| LoRA target modules | q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj |
| Epochs | 3 |
| Learning rate | 2e-4, cosine schedule |
| Effective batch size | 8 |
| Hardware | Single consumer GPU, 4GB VRAM |
Evaluated on a 62-example held-out test set. Metric: per-field exact match (case-insensitive, whitespace-normalized) computed only over outputs that parsed as valid JSON, plus overall JSON validity rate.
| Metric | Base model | Fine-tuned | Change |
|---|---|---|---|
| JSON validity | 98.4% | 95.2% | -3.2pp |
parties | 0% | 24% | +24pp |
agreement_date | 41% | 63% | +22pp |
effective_date | 33% | 44% | +11pp |
governing_law | 10% | 2% | -8pp |
termination_for_convenience | 57% | 51% | -6pp |
cap_on_liability | 33% | 19% | -14pp |
non_compete | 62% | 85% | +23pp |
exclusivity | 48% | 59% | +11pp |
expiration_date | 18% | 10% | -8pp |
license_grant | 41% | 53% | +12pp |
document_name | 49% | 83% | +34pp |
renewal_term | 52% | 69% | +17pp |
| Overall field accuracy | 37.0% | 46.8% | +9.8pp |
Fine-tuning improved 8 of the 12 fields, with a net +9.8 percentage point gain in overall field accuracy.
| Epoch | Train loss (end of epoch) | Eval loss |
|---|---|---|
| 1 | 1.45 | 1.4133 |
| 2 | 1.28 | 1.3891 |
| 3 | 1.23 | 1.3875 |
Train loss decreases steadily across all 3 epochs, but eval loss is nearly flat after epoch 1 — the model is data-limited rather than undertrained; more epochs on this dataset size wouldn't meaningfully help, more/varied training examples would.
Input: a 1996 technology license agreement.
{
"parties": "ENTRUST TECHNOLOGIES INC.",
"agreement_date": "31 December, 1996",
"effective_date": "31 December, 1996",
"governing_law": "This Agreement will be governed by and construed in accordance with the laws of the Province of Ontario without regard to conflicts of law principles.",
"termination_for_convenience": null,
"cap_on_liability": "IN NO EVENT SHALL EITHER PARTY BE LIABLE TO THE OTHER FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, EVEN IF SUCH PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.",
"non_compete": null,
"exclusivity": null,
...
}
Manual review of the 4 regressed fields (governing_law, cap_on_liability, termination_for_convenience, expiration_date) shows the exact-match metric understates real quality:
cap_on_liability); the model sometimes extracts a different, equally valid sentence than the one CUAD's annotators chose as ground truth.null in ground truth), the model occasionally still generates a confident-sounding but fabricated clause instead of returning null.non_compete, exclusivity, renewal_term) had under 40% non-null coverage in training data, limiting their ceiling.null — outputs should always be reviewed by a human before use.