| |
| """ |
| LUNA SFT Dataset Augmentation & Purification Pipeline |
| ===================================================== |
| 1. Load existing cleaned SFT data |
| 2. Deduplicate repetitive instruction prefixes (cap at 500 per prefix) |
| 3. Download diverse, high-quality open datasets (Dolly, Alpaca, SciQ, GSM8K) |
| 4. Clean, format, filter, and rebrand all new data |
| 5. Add input+output only entries (no instruction) for format diversity |
| 6. Balance underrepresented categories |
| 7. Validate token limits (<=1024), dedup, final merge |
| 8. Report |
| """ |
|
|
| import json, re, os, random, hashlib, collections, sys |
| from pathlib import Path |
| from transformers import AutoTokenizer |
|
|
| |
| EXISTING_PATH = Path("Base/Datasets/sft_clean/all_sft_clean.json") |
| OUT_DIR = Path("Base/Datasets/sft_clean") |
| MAX_TOKENS = 1000 |
| PREFIX_CAP = 500 |
| SEED = 42 |
|
|
| random.seed(SEED) |
|
|
| |
| print("Loading tokenizer...") |
| tok = AutoTokenizer.from_pretrained("Base/checkpoints/EleutherAI/pythia-160m") |
|
|
| def count_tokens(entry): |
| text = entry.get("instruction", "") |
| if entry.get("input", ""): |
| text += "\n" + entry["input"] |
| text += "\n" + entry["output"] |
| return len(tok.encode(text)) |
|
|
| def entry_hash(entry): |
| key = (entry.get("instruction","").strip().lower() + "||" + |
| entry.get("input","").strip().lower()) |
| return hashlib.md5(key.encode()).hexdigest() |
|
|
| |
| |
| |
| print("\n[1/8] Loading existing dataset...") |
| with open(EXISTING_PATH, "r", encoding="utf-8") as f: |
| existing = json.load(f) |
| print(f" Existing: {len(existing):,}") |
|
|
| |
| |
| |
| print("\n[2/8] Capping repetitive instruction prefixes...") |
|
|
| prefix_groups = collections.defaultdict(list) |
| for i, entry in enumerate(existing): |
| prefix = entry["instruction"].strip()[:50].lower() |
| prefix_groups[prefix].append(i) |
|
|
| capped = set() |
| removed_for_repetition = 0 |
| for prefix, indices in prefix_groups.items(): |
| if len(indices) > PREFIX_CAP: |
| keep = set(random.sample(indices, PREFIX_CAP)) |
| for idx in indices: |
| if idx not in keep: |
| capped.add(idx) |
| removed_for_repetition += 1 |
|
|
| existing = [e for i, e in enumerate(existing) if i not in capped] |
| print(f" Removed {removed_for_repetition:,} over-represented entries") |
| print(f" Remaining: {len(existing):,}") |
|
|
| |
| existing_hashes = {entry_hash(e) for e in existing} |
|
|
| |
| |
| |
| print("\n[3/8] Downloading diverse open datasets...") |
|
|
| from datasets import load_dataset |
|
|
| new_entries = [] |
|
|
| |
| print(" Downloading databricks-dolly-15k...") |
| try: |
| dolly = load_dataset("databricks/databricks-dolly-15k", split="train") |
| for row in dolly: |
| inst = (row.get("instruction") or row.get("context") or "").strip() |
| inp = (row.get("context") or "").strip() |
| out = (row.get("response") or "").strip() |
| |
| if row.get("instruction","").strip() and out: |
| entry = {"instruction": row["instruction"].strip(), "input": inp, "output": out} |
| new_entries.append(("dolly", entry)) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='dolly'):,} from Dolly") |
| except Exception as e: |
| print(f" Dolly failed: {e}") |
|
|
| |
| print(" Downloading alpaca-cleaned...") |
| try: |
| alpaca = load_dataset("yahma/alpaca-cleaned", split="train") |
| for row in alpaca: |
| inst = (row.get("instruction") or "").strip() |
| inp = (row.get("input") or "").strip() |
| out = (row.get("output") or "").strip() |
| if inst and out: |
| new_entries.append(("alpaca", {"instruction": inst, "input": inp, "output": out})) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='alpaca'):,} from Alpaca") |
| except Exception as e: |
| print(f" Alpaca failed: {e}") |
|
|
| |
| print(" Downloading SciQ...") |
| try: |
| sciq = load_dataset("allenai/sciq", split="train") |
| for row in sciq: |
| question = (row.get("question") or "").strip() |
| answer = (row.get("correct_answer") or "").strip() |
| support = (row.get("support") or "").strip() |
| if question and answer: |
| |
| if support: |
| out = f"{answer}\n\n{support}" if len(support) > 30 else answer |
| new_entries.append(("sciq", { |
| "instruction": question, |
| "input": support[:500] if len(support) > 500 else support, |
| "output": answer |
| })) |
| else: |
| new_entries.append(("sciq", { |
| "instruction": question, |
| "input": "", |
| "output": answer |
| })) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='sciq'):,} from SciQ") |
| except Exception as e: |
| print(f" SciQ failed: {e}") |
|
|
| |
| print(" Downloading GSM8K...") |
| try: |
| gsm = load_dataset("openai/gsm8k", "main", split="train") |
| for row in gsm: |
| question = (row.get("question") or "").strip() |
| answer = (row.get("answer") or "").strip() |
| if question and answer: |
| new_entries.append(("gsm8k", { |
| "instruction": question, |
| "input": "", |
| "output": answer |
| })) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='gsm8k'):,} from GSM8K") |
| except Exception as e: |
| print(f" GSM8K failed: {e}") |
|
|
| |
| print(" Downloading TruthfulQA...") |
| try: |
| tqa = load_dataset("truthfulqa/truthful_qa", "generation", split="validation") |
| for row in tqa: |
| question = (row.get("question") or "").strip() |
| best_answer = (row.get("best_answer") or "").strip() |
| if question and best_answer: |
| new_entries.append(("truthfulqa", { |
| "instruction": question, |
| "input": "", |
| "output": best_answer |
| })) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='truthfulqa'):,} from TruthfulQA") |
| except Exception as e: |
| print(f" TruthfulQA failed: {e}") |
|
|
| |
| print(" Downloading OpenBookQA...") |
| try: |
| obqa = load_dataset("allenai/openbookqa", "main", split="train") |
| for row in obqa: |
| question = (row.get("question_stem") or "").strip() |
| choices = row.get("choices", {}) |
| answer_key = row.get("answerKey", "") |
| if question and choices and answer_key: |
| labels = choices.get("label", []) |
| texts = choices.get("text", []) |
| if answer_key in labels: |
| idx = labels.index(answer_key) |
| answer = texts[idx] |
| |
| choices_text = "\n".join(f"{l}) {t}" for l, t in zip(labels, texts)) |
| new_entries.append(("openbookqa", { |
| "instruction": question, |
| "input": choices_text, |
| "output": f"The answer is {answer_key}) {answer}." |
| })) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='openbookqa'):,} from OpenBookQA") |
| except Exception as e: |
| print(f" OpenBookQA failed: {e}") |
|
|
| |
| print(" Downloading PIQA...") |
| try: |
| piqa = load_dataset("ybisk/piqa", split="train", trust_remote_code=True) |
| for row in piqa: |
| goal = (row.get("goal") or "").strip() |
| sol1 = (row.get("sol1") or "").strip() |
| sol2 = (row.get("sol2") or "").strip() |
| label = row.get("label", -1) |
| if goal and sol1 and sol2 and label in [0, 1]: |
| answer = sol1 if label == 0 else sol2 |
| new_entries.append(("piqa", { |
| "instruction": f"How would you accomplish the following goal?\n{goal}", |
| "input": "", |
| "output": answer |
| })) |
| print(f" Got {sum(1 for s,_ in new_entries if s=='piqa'):,} from PIQA") |
| except Exception as e: |
| print(f" PIQA failed: {e}") |
|
|
| total_downloaded = len(new_entries) |
| print(f"\n Total downloaded: {total_downloaded:,}") |
|
|
| |
| |
| |
| print("\n[4/8] Cleaning new entries...") |
|
|
| |
| REBRAND = [ |
| (re.compile(r'\bChatGPT\b', re.I), 'LUNA'), |
| (re.compile(r'\bGPT-?4o?\b', re.I), 'LUNA'), |
| (re.compile(r'\bGPT-?3\.?5?\b', re.I), 'LUNA'), |
| (re.compile(r'\bOpenAI\b', re.I), 'Asterizer'), |
| (re.compile(r'\bAnthrop(?:ic|ics)\b', re.I), 'Asterizer'), |
| (re.compile(r'\bGoogle AI\b', re.I), 'Asterizer'), |
| (re.compile(r'\bMeta AI\b', re.I), 'Asterizer'), |
| (re.compile(r"(?:I am|I'm) (?:an AI (?:language )?model|a large language model) (?:created|developed|trained) by \w+", re.I), |
| 'I am LUNA, an AI assistant created by Asterizer'), |
| ] |
|
|
| |
| NON_ENGLISH_RE = re.compile(r'[\u4e00-\u9fff\u0600-\u06ff\u0900-\u097f\u3040-\u309f\u30a0-\u30ff]') |
| PLACEHOLDER_RE = re.compile(r'\[insert\b|\[your (?:name|company|topic)\]|<your |_{5,}|XX{3,}', re.I) |
|
|
| cleaned_new = [] |
| rejected = collections.Counter() |
|
|
| for source, entry in new_entries: |
| inst = entry["instruction"].strip() |
| inp = entry.get("input", "").strip() |
| out = entry["output"].strip() |
| |
| |
| if not out or len(out) < 10: |
| rejected["too_short_output"] += 1 |
| continue |
| if not inst and not inp: |
| rejected["no_instruction_or_input"] += 1 |
| continue |
| |
| |
| combined = inst + " " + inp + " " + out |
| if NON_ENGLISH_RE.search(combined): |
| rejected["non_english"] += 1 |
| continue |
| |
| |
| if PLACEHOLDER_RE.search(combined): |
| rejected["placeholder"] += 1 |
| continue |
| |
| |
| ascii_ratio = sum(1 for c in combined if ord(c) < 128) / max(len(combined), 1) |
| if ascii_ratio < 0.85: |
| rejected["low_ascii"] += 1 |
| continue |
| |
| |
| for pattern, replacement in REBRAND: |
| inst = pattern.sub(replacement, inst) |
| out = pattern.sub(replacement, out) |
| if inp: |
| inp = pattern.sub(replacement, inp) |
| |
| |
| for field_val in [inst, inp, out]: |
| field_val = re.sub(r' +', ' ', field_val) |
| |
| |
| entry_clean = {"instruction": inst, "input": inp, "output": out} |
| tlen = count_tokens(entry_clean) |
| if tlen > MAX_TOKENS: |
| rejected["over_token_limit"] += 1 |
| continue |
| if tlen < 15: |
| rejected["too_few_tokens"] += 1 |
| continue |
| |
| |
| h = entry_hash(entry_clean) |
| if h in existing_hashes: |
| rejected["duplicate_existing"] += 1 |
| continue |
| |
| existing_hashes.add(h) |
| cleaned_new.append((source, entry_clean)) |
|
|
| print(f" Cleaned new entries: {len(cleaned_new):,}") |
| print(f" Rejected breakdown:") |
| for reason, count in rejected.most_common(): |
| print(f" {reason}: {count:,}") |
|
|
| |
| |
| |
| print("\n[5/8] Creating input+output format entries...") |
|
|
| |
| input_output_entries = [] |
| |
| for source, entry in cleaned_new: |
| if entry["input"] and len(entry["input"]) > 50 and random.random() < 0.15: |
| |
| combined_input = entry["input"] |
| if entry["instruction"]: |
| combined_input = entry["input"] + "\n\nQuestion: " + entry["instruction"] |
| io_entry = { |
| "instruction": "", |
| "input": combined_input, |
| "output": entry["output"] |
| } |
| tlen = count_tokens(io_entry) |
| if tlen <= MAX_TOKENS: |
| h = entry_hash(io_entry) |
| if h not in existing_hashes: |
| existing_hashes.add(h) |
| input_output_entries.append(io_entry) |
|
|
| print(f" Created {len(input_output_entries):,} input+output only entries") |
|
|
| |
| |
| |
| print("\n[6/8] Diversifying instruction phrasing...") |
|
|
| |
| new_prefix_count = collections.Counter() |
| diversified = [] |
| for source, entry in cleaned_new: |
| prefix = entry["instruction"][:50].lower().strip() |
| new_prefix_count[prefix] += 1 |
| if new_prefix_count[prefix] <= 100: |
| diversified.append(entry) |
|
|
| dropped_new_repetition = len(cleaned_new) - len(diversified) |
| print(f" Dropped {dropped_new_repetition:,} new entries with repeated prefixes") |
| cleaned_new_entries = diversified |
|
|
| |
| |
| |
| print("\n[7/8] Merging all data...") |
|
|
| all_data = existing + cleaned_new_entries + input_output_entries |
|
|
| |
| seen = set() |
| final = [] |
| dup_count = 0 |
| for entry in all_data: |
| h = entry_hash(entry) |
| if h not in seen: |
| seen.add(h) |
| final.append(entry) |
| else: |
| dup_count += 1 |
|
|
| print(f" Final dedup removed: {dup_count:,}") |
| print(f" Total final entries: {len(final):,}") |
|
|
| |
| |
| |
| print("\n[8/8] Counting tokens and saving...") |
|
|
| total_tokens = 0 |
| for entry in final: |
| total_tokens += count_tokens(entry) |
|
|
| avg_tokens = total_tokens / len(final) |
|
|
| |
| random.shuffle(final) |
| val_size = max(3000, int(len(final) * 0.02)) |
| train = final[val_size:] |
| val = final[:val_size] |
|
|
| |
| for path, subset in [ |
| (OUT_DIR / "all_sft_clean.json", final), |
| (OUT_DIR / "train.json", train), |
| (OUT_DIR / "val.json", val), |
| ]: |
| with open(path, "w", encoding="utf-8") as f: |
| json.dump(subset, f, indent=2, ensure_ascii=False) |
|
|
| |
| cats = collections.Counter() |
| for d in final: |
| inst = (d.get("instruction","") + " " + d.get("input","")).lower() |
| if any(w in inst for w in ['code','program','function','python','javascript','html','css','sql','api','debug']): |
| cats['coding'] += 1 |
| elif any(w in inst for w in ['math','calculat','equation','algebra','geometry','statistic','probability','solve']): |
| cats['math'] += 1 |
| elif any(w in inst for w in ['science','physics','chemistry','biology','atom','molecule','cell','gravity','evolution','experiment']): |
| cats['science'] += 1 |
| elif any(w in inst for w in ['history','war','century','ancient','empire','civilization','revolution']): |
| cats['history'] += 1 |
| elif any(w in inst for w in ['geography','country','continent','ocean','mountain','river','capital']): |
| cats['geography'] += 1 |
| elif any(w in inst for w in ['health','medicine','disease','symptom','treatment','doctor','nutrition']): |
| cats['health'] += 1 |
| elif any(w in inst for w in ['econom','finance','market','invest','business','trade','gdp','inflation']): |
| cats['economics'] += 1 |
| elif any(w in inst for w in ['write','story','poem','essay','creative','novel','fiction']): |
| cats['creative_writing'] += 1 |
| elif any(w in inst for w in ['explain','what is','define','describe','meaning of']): |
| cats['explanation'] += 1 |
| elif any(w in inst for w in ['summariz','summary','brief','tldr','condense']): |
| cats['summarization'] += 1 |
| elif any(w in inst for w in ['translat']): |
| cats['translation'] += 1 |
| elif any(w in inst for w in ['logic','reason','puzzle','riddle','brain','think step','goal']): |
| cats['reasoning'] += 1 |
| elif any(w in inst for w in ['ethic','moral','philosoph','right wrong','dilemma']): |
| cats['philosophy'] += 1 |
| else: |
| cats['general'] += 1 |
|
|
| |
| fmt_inst_out = sum(1 for d in final if d.get("instruction","").strip() and not d.get("input","").strip()) |
| fmt_inst_inp_out = sum(1 for d in final if d.get("instruction","").strip() and d.get("input","").strip()) |
| fmt_inp_out = sum(1 for d in final if not d.get("instruction","").strip() and d.get("input","").strip()) |
|
|
| |
| source_info = { |
| "existing (cleaned)": len(existing), |
| "dolly": sum(1 for s,_ in cleaned_new if s=='dolly' if _ in cleaned_new_entries), |
| "new entries": len(cleaned_new_entries), |
| "input+output format": len(input_output_entries), |
| } |
|
|
| report = f"""LUNA SFT Augmentation & Purification Report |
| {'='*55} |
| |
| BEFORE: |
| Entries: 304,475 |
| Tokens: 86,633,618 |
| |
| CLEANING: |
| Repetitive prefix cap: -{removed_for_repetition:,} |
| New datasets downloaded: {total_downloaded:,} |
| New entries after clean: {len(cleaned_new_entries):,} |
| Input+output entries: {len(input_output_entries):,} |
| Final dedup removed: {dup_count:,} |
| |
| AFTER: |
| Total entries: {len(final):>10,} |
| Total tokens: {total_tokens:>10,} |
| Avg tokens/entry:{avg_tokens:>10.1f} |
| Train set: {len(train):>10,} |
| Val set: {len(val):>10,} |
| |
| FORMAT DISTRIBUTION: |
| instruction + output: {fmt_inst_out:>8,} ({fmt_inst_out/len(final)*100:.1f}%) |
| instruction + input + output: {fmt_inst_inp_out:>8,} ({fmt_inst_inp_out/len(final)*100:.1f}%) |
| input + output only: {fmt_inp_out:>8,} ({fmt_inp_out/len(final)*100:.1f}%) |
| |
| CATEGORY DISTRIBUTION: |
| """ |
| for cat, count in cats.most_common(): |
| report += f" {cat:20s}: {count:>8,} ({count/len(final)*100:.1f}%)\n" |
|
|
| report += f""" |
| Files saved: |
| {OUT_DIR / 'all_sft_clean.json'} |
| {OUT_DIR / 'train.json'} |
| {OUT_DIR / 'val.json'} |
| """ |
|
|
| report_path = OUT_DIR / "AUGMENTATION_REPORT.txt" |
| with open(report_path, "w", encoding="utf-8") as f: |
| f.write(report) |
|
|
| print(report) |
| print("Done!") |
|
|