compliance-copilot / data /prepare_data.py
SaitejaDubbas's picture
Initial commit: Phase 1 (data prep) and Phase 2 (QLoRA fine-tuning) complete
3cb0c0f
Raw
History Blame Contribute Delete
5.73 kB
"""
Compliance Copilot -- Phase 1: Data Preparation
================================================
Goal of this file (one sentence):
Take a big public dataset of real contract clauses and turn it into small,
clean files we can feed to our model in Phase 2.
Why LEDGAR?
LEDGAR is a public dataset of contract provisions ("clauses"), where each
clause is labelled with its type (e.g. "Confidentiality", "Governing Laws",
"Terminations"). This is exactly the "what kind of clause is this / is it
compliant" problem a compliance team faces every day. It is public and
license-friendly, so we can safely showcase it (unlike private company data).
Where to run this:
In Google Colab (free) or on your own laptop. It needs internet the first
time so it can download the dataset from the Hugging Face Hub.
Install the one thing it needs:
pip install -U datasets
Output (all written into this ./data folder):
labels.json -> the list of clause types we keep
train.jsonl -> training examples (chat format, used to teach the model)
val.jsonl -> validation examples (chat format, used to watch progress)
test.jsonl -> held-out examples (raw text + label, used to score accuracy)
"""
import json
import random
from collections import Counter
from pathlib import Path
from datasets import load_dataset
# ---------------------------------------------------------------------------
# Settings you are allowed to play with
# ---------------------------------------------------------------------------
TOP_K_LABELS = 10 # keep only the K most common clause types
MAX_PER_LABEL_TRAIN = 400 # cap training examples per label (keeps training fast + free)
MAX_PER_LABEL_EVAL = 60 # cap validation/test examples per label
SEED = 42
OUT_DIR = Path(__file__).resolve().parent # writes next to this script (the data/ folder)
# This is the "job description" we hand the model. IMPORTANT: the SAME text must
# be used again when we serve the model in Phase 3, otherwise it gets confused.
# That is why we keep it here in one place and reuse it everywhere.
SYSTEM_PROMPT = (
"You are a compliance assistant. You read a single contract clause and reply "
"with the one clause type it belongs to, and nothing else."
)
def build_user_prompt(clause_text: str, labels: list) -> str:
"""Turn a clause + the menu of allowed answers into the question we ask the model."""
label_menu = ", ".join(labels)
return (
f"Classify the following contract clause into exactly one of these types: "
f"{label_menu}.\n\n"
f"Clause:\n{clause_text}\n\n"
f"Clause type:"
)
# ---------------------------------------------------------------------------
random.seed(SEED)
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
print("Step 1/5 Downloading LEDGAR from the Hugging Face Hub ...")
ds = load_dataset("coastalcph/lex_glue", "ledgar")
# The dataset stores labels as numbers (0, 1, 2, ...). This list turns a
# number back into a human name, e.g. 12 -> "Confidentiality".
label_names = ds["train"].features["label"].names
print(f" The full dataset has {len(label_names)} clause types.")
print("Step 2/5 Finding the most common clause types ...")
counts = Counter(ds["train"]["label"]) # how often each label appears
top_ids = [label_id for label_id, _ in counts.most_common(TOP_K_LABELS)]
kept_labels = [label_names[i] for i in top_ids] # the names we keep
id_to_name = {i: label_names[i] for i in top_ids}
print(f" Keeping the top {TOP_K_LABELS}: {kept_labels}")
# Save the label list so every other part of the project agrees on it.
(OUT_DIR / "labels.json").write_text(json.dumps(kept_labels, indent=2))
def collect(split_name: str, cap_per_label: int):
"""Grab up to cap_per_label examples for each label so classes stay balanced."""
buckets = {i: [] for i in top_ids}
for row in ds[split_name]:
lid = row["label"]
if lid in buckets and len(buckets[lid]) < cap_per_label:
buckets[lid].append(row["text"])
pairs = [(text, id_to_name[lid]) for lid, texts in buckets.items() for text in texts]
random.shuffle(pairs)
return pairs
print("Step 3/5 Selecting + balancing examples ...")
train_pairs = collect("train", MAX_PER_LABEL_TRAIN)
val_pairs = collect("validation", MAX_PER_LABEL_EVAL)
test_pairs = collect("test", MAX_PER_LABEL_EVAL)
print(f" train={len(train_pairs)} val={len(val_pairs)} test={len(test_pairs)}")
print("Step 4/5 Writing training + validation files (chat format) ...")
def write_chat(path: Path, pairs):
with path.open("w") as f:
for text, label in pairs:
example = {
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": build_user_prompt(text, kept_labels)},
{"role": "assistant", "content": label},
]
}
f.write(json.dumps(example) + "\n")
write_chat(OUT_DIR / "train.jsonl", train_pairs)
write_chat(OUT_DIR / "val.jsonl", val_pairs)
print("Step 5/5 Writing the test file (raw text + label, for scoring) ...")
with (OUT_DIR / "test.jsonl").open("w") as f:
for text, label in test_pairs:
f.write(json.dumps({"text": text, "label": label}) + "\n")
print("\nDone. Files written into:", OUT_DIR)
print("Next: open the Phase 2 fine-tuning notebook.")
if __name__ == "__main__":
main()