File size: 20,028 Bytes
0710b98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
#!/usr/bin/env python3
"""
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

# ── Config ──
EXISTING_PATH = Path("Base/Datasets/sft_clean/all_sft_clean.json")
OUT_DIR = Path("Base/Datasets/sft_clean")
MAX_TOKENS = 1000  # leave headroom below 1024 block size
PREFIX_CAP = 500   # max entries per repeated instruction prefix
SEED = 42

random.seed(SEED)

# ── Tokenizer ──
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()

# ═══════════════════════════════════════════
# STEP 1: Load existing data
# ═══════════════════════════════════════════
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):,}")

# ═══════════════════════════════════════════
# STEP 2: Cap repetitive instruction prefixes
# ═══════════════════════════════════════════
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):,}")

# Build hash set of existing entries for dedup
existing_hashes = {entry_hash(e) for e in existing}

# ═══════════════════════════════════════════
# STEP 3: Download diverse open datasets
# ═══════════════════════════════════════════
print("\n[3/8] Downloading diverse open datasets...")

from datasets import load_dataset

new_entries = []

# ── 3a: Databricks Dolly 15K (human-written, diverse, CC-licensed) ──
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()
        # Dolly uses instruction and context
        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}")

# ── 3b: Alpaca Cleaned (diverse instructions) ──
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}")

# ── 3c: SciQ (science Q&A - boost science category) ──
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:
            # Format as instruction with context
            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}")

# ── 3d: GSM8K (math reasoning - boost math category) ──
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}")

# ── 3e: TruthfulQA (factual accuracy) ──
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}")

# ── 3f: OpenBookQA (science & common sense) ──
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]
                # Format nicely
                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}")

# ── 3g: PIQA (physical/common sense reasoning) ──
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:,}")

# ═══════════════════════════════════════════
# STEP 4: Clean new entries
# ═══════════════════════════════════════════
print("\n[4/8] Cleaning new entries...")

# Identity rebranding patterns
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'),
]

# Quality filters
NON_ENGLISH_RE = re.compile(r'[\u4e00-\u9fff\u0600-\u06ff\u0900-\u097f\u3040-\u309f\u30a0-\u30ff]')  # CJK, Arabic, Devanagari, Japanese
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()
    
    # Skip empty
    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
    
    # Skip non-English
    combined = inst + " " + inp + " " + out
    if NON_ENGLISH_RE.search(combined):
        rejected["non_english"] += 1
        continue
    
    # Skip placeholders
    if PLACEHOLDER_RE.search(combined):
        rejected["placeholder"] += 1
        continue
    
    # Skip if mostly non-ASCII
    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
    
    # Rebrand AI identities
    for pattern, replacement in REBRAND:
        inst = pattern.sub(replacement, inst)
        out = pattern.sub(replacement, out)
        if inp:
            inp = pattern.sub(replacement, inp)
    
    # Fix double spaces and basic grammar
    for field_val in [inst, inp, out]:
        field_val = re.sub(r'  +', ' ', field_val)
    
    # Token length check
    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
    
    # Dedup against existing
    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:,}")

# ═══════════════════════════════════════════
# STEP 5: Create input+output only entries (no instruction)
# ═══════════════════════════════════════════
print("\n[5/8] Creating input+output format entries...")

# Convert some entries to input+output only format (reading comprehension style)
input_output_entries = []
# Use some Dolly/Alpaca entries that have context and rephrase
for source, entry in cleaned_new:
    if entry["input"] and len(entry["input"]) > 50 and random.random() < 0.15:
        # Merge instruction into input as a reading prompt
        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")

# ═══════════════════════════════════════════
# STEP 6: Diversify instruction phrasing
# ═══════════════════════════════════════════
print("\n[6/8] Diversifying instruction phrasing...")

# For new entries, ensure no instruction prefix repeats >100 times
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

# ═══════════════════════════════════════════
# STEP 7: Merge everything
# ═══════════════════════════════════════════
print("\n[7/8] Merging all data...")

all_data = existing + cleaned_new_entries + input_output_entries

# Final global dedup
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):,}")

# ═══════════════════════════════════════════
# STEP 8: Token count, split, save, report
# ═══════════════════════════════════════════
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)

# Shuffle and split
random.shuffle(final)
val_size = max(3000, int(len(final) * 0.02))
train = final[val_size:]
val = final[:val_size]

# Save
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)

# Category analysis of final dataset
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

# Format distribution
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 distribution
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!")