File size: 3,705 Bytes
1c0d385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Build conservative v4 SFT mix for TinyLiquid.

v4 is designed to avoid the v3 failure mode: full-model SFT overfit into
broken analyst jargon. The mix keeps short, fully-visible assistant answers,
uses a smaller forensic/SOP slice, and adds much more raw language replay.

Output: data/sft_mix_v4.jsonl
"""
import json
import random
from collections import Counter
from pathlib import Path

from data.tokenizer import load_tokenizer

rng = random.Random(20260801)
SEQ = 256
MIN_ASSISTANT = 24
MAX_USER = 160
MAX_ASSISTANT = 180
OUT = Path('data/sft_mix_v4.jsonl')

PERSONA_T = {'analyst': '<|analyst|>', 'skeptic': '<|skeptic|>', 'none': ''}


def load_jsonl(path):
    return [json.loads(line) for line in open(path, encoding='utf-8') if line.strip()]


def write_jsonl(path, rows):
    with open(path, 'w', encoding='utf-8') as f:
        for row in rows:
            f.write(json.dumps(row, ensure_ascii=False) + '\n')


def dedupe(rows):
    seen = set()
    out = []
    for row in rows:
        key = (row.get('persona', 'analyst'), row.get('user', '')[:180])
        if key in seen:
            continue
        seen.add(key)
        out.append(row)
    return out


def visible_chat(row, tok, u_id, a_id, eot_id):
    if 'raw' in row:
        return True
    if not row.get('user') or not row.get('assistant'):
        return False
    persona = row.get('persona', 'analyst')
    p_ids = tok.encode(PERSONA_T.get(persona, '<|analyst|>')).ids if persona != 'none' else []
    user = tok.encode(row['user']).ids
    assistant = tok.encode(row['assistant']).ids
    if len(user) > MAX_USER or len(assistant) > MAX_ASSISTANT or len(assistant) < MIN_ASSISTANT:
        return False
    ids = p_ids + [u_id] + user + [a_id] + assistant + [eot_id]
    return len(ids) <= SEQ


def sample(rows, n):
    rows = list(rows)
    rng.shuffle(rows)
    return rows[:min(n, len(rows))]


def main():
    tok = load_tokenizer('data/tokenizer.json')
    u_id = tok.token_to_id('<|user|>')
    a_id = tok.token_to_id('<|assistant|>')
    eot_id = tok.token_to_id('<|endoftext|>')

    v3 = dedupe(load_jsonl('data/sft_mix_v3.jsonl'))
    truth = [r for r in v3 if r.get('user', '').startswith('Answer truthfully:')]
    chatish = [r for r in v3 if r.get('persona') == 'analyst' and 'raw' not in r and len(r.get('user', '')) < 90]

    forensic = dedupe(load_jsonl('data/sft_forensic.jsonl'))
    forensic_a = [r for r in forensic if r.get('persona') != 'skeptic']
    forensic_s = [r for r in forensic if r.get('persona') == 'skeptic']

    mix = []
    mix += load_jsonl('data/general_chat.jsonl')
    mix += load_jsonl('data/persona_dialogue.jsonl')
    mix += load_jsonl('data/tool_use.jsonl')
    mix += sample(truth, 40)
    mix += sample(chatish, 80)
    mix += sample(load_jsonl('data/sft_distill_mix.jsonl'), 160)
    mix += sample(load_jsonl('data/sft_sop_mix.jsonl'), 120)
    mix += sample(forensic_a, 140)
    mix += sample(forensic_s, 40)

    clean = []
    dropped = 0
    for row in dedupe(mix):
        if visible_chat(row, tok, u_id, a_id, eot_id):
            clean.append(row)
        else:
            dropped += 1

    # Raw replay is intentionally large. For this tiny model, preserving fluent
    # language is more important than forcing domain style in one pass.
    lines = [line.strip() for line in open('data/TinyStoriesV2-GPT4-train.txt', encoding='utf-8') if line.strip()]
    rng.shuffle(lines)
    for line in lines[:700]:
        clean.append({'raw': line, 'persona': 'none'})

    rng.shuffle(clean)
    write_jsonl(OUT, clean)
    print('total', len(clean), 'dropped_chat', dropped, dict(Counter(r.get('persona', '?') for r in clean)))


if __name__ == '__main__':
    main()