Text Generation
Transformers
Safetensors
Arabic
llama
arabic
reasoning
chain-of-thought
math
gsm8k
small-language-model
slm
sft
conversational
text-generation-inference
File size: 5,379 Bytes
867d0f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Prepare Omartificial-Intelligence-Space/Arabic_Reasoning_Dataset for reasoning SFT.

Each row (instruction, answer) becomes a ChatML sample where the derivation lives
inside <think>...</think> and the conclusion follows it:

    <|im_start|>user\n{instruction}<|im_end|>\n<|im_start|>assistant\n<think>\n{reasoning}\n</think>\n{answer}<|im_end|>

Only rows whose derivation ends in an explicit conclusion line ("إذن، ...") are kept.
Expository rows without one have no real "final answer" to place after </think> — their
closing paragraph is a side remark, so training on them would teach the model to think and
then trail off. They are dropped rather than force-split.

Writes data/train.jsonl and data/eval.jsonl.
"""
import json, re, random, unicodedata, collections
from pathlib import Path

import pyarrow.parquet as pq

SRC      = Path("data/data/train-00000-of-00001.parquet")
OUT_DIR  = Path("data")
EVAL_N   = 400
SEED     = 42

# Lines that mark the final conclusion of a derivation.
CONCLUSION = ["إذن،", "إذن ", "لذا،", "لذلك،", "باختصار،", "وبالتالي،", "في النهاية،",
              "الخلاصة", "النتيجة النهائية", "النتيجة:", "الإجابة", "الجواب", "بالتالي،"]
# Chatty sign-offs that are not part of the answer.
FLUFF = ["تذكر", "آمل", "أتمنى", "يرجى", "لا تتردد", "إذا كان لديك أي", "هل لديك",
         "أرجو", "نصيحة:", "ملاحظة:", "إذا كانت لديك"]
# Prompt suffix present on a subset of instructions; stripped so reasoning is unconditional.
SUFFIX_RE = re.compile(r"\s*خذ\s+نفسًا?\s+عميقًا.*$", re.S)

MIN_THINK_CHARS  = 60
MIN_ANSWER_CHARS = 10

MD_NOISE_RE  = re.compile(r"\*\*|__|#{2,}")
# Some source rows bundle several problems; the conclusion of one is followed by the next
# problem's header. Anything from that header on is not part of the answer.
NEXT_PROBLEM_RE = re.compile(r"^\s*(المشكلة|المسألة|السؤال|التمرين|مثال|Problem|Question|Example)\b")
ANSWER_ARTIFACT_RE = re.compile(r"^\s*(\*\*)?Answer:\s*", re.I)


def norm(text: str) -> str:
    text = unicodedata.normalize("NFC", text.replace("‏", "").replace("‎", ""))
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


def clean_instruction(text: str) -> str:
    return norm(SUFFIX_RE.sub("", norm(text))).rstrip(". ").strip() or norm(text)


def is_fluff(line: str) -> bool:
    head = line.lstrip("*#- ").strip()
    return any(head.startswith(f) for f in FLUFF)


def is_conclusion(line: str) -> bool:
    head = line.lstrip("*#- ").strip()
    return any(head.startswith(m) for m in CONCLUSION)


def strip_markup(line: str) -> str:
    """Drop markdown emphasis and the stray "**Answer:" prefix some rows carry."""
    return norm(MD_NOISE_RE.sub("", ANSWER_ARTIFACT_RE.sub("", line)))


def split_answer(answer: str, instruction: str):
    """-> (reasoning, final_answer) or None when the row can't be split cleanly."""
    lines = [strip_markup(l) for l in norm(answer).split("\n")]
    lines = [l for l in lines if l]
    if len(lines) < 2:
        return None

    # Drop trailing chatter first — it belongs to neither part.
    while lines and is_fluff(lines[-1]):
        lines.pop()
    if len(lines) < 2:
        return None

    # A leading restatement of the question adds nothing to the derivation.
    if lines and lines[0][:40] == instruction.strip()[:40]:
        lines.pop(0)

    idx = next((i for i in range(len(lines) - 1, 0, -1) if is_conclusion(lines[i])), None)
    if idx is None:
        return None
    reasoning = "\n".join(lines[:idx])
    tail = lines[idx:]
    cut = next((i for i in range(1, len(tail)) if NEXT_PROBLEM_RE.match(tail[i])), len(tail))
    final = "\n".join(tail[:cut])

    if len(reasoning) < MIN_THINK_CHARS or len(final) < MIN_ANSWER_CHARS:
        return None
    # A "conclusion" longer than the derivation means the split went the wrong way.
    if len(final) > len(reasoning):
        return None
    return reasoning, final


def main():
    table = pq.read_table(SRC).to_pydict()
    rows  = list(zip(table["instruction"], table["answer"]))
    stats = collections.Counter(total=len(rows))

    seen, samples = set(), []
    for raw_ins, raw_ans in rows:
        ins = clean_instruction(raw_ins)
        key = re.sub(r"\W+", "", ins)
        if key in seen:
            stats["dropped_duplicate"] += 1
            continue
        seen.add(key)

        split = split_answer(raw_ans, ins)
        if split is None:
            stats["dropped_no_conclusion"] += 1
            continue
        reasoning, final = split
        stats["kept"] += 1
        samples.append({"instruction": ins, "reasoning": reasoning, "answer": final})

    random.Random(SEED).shuffle(samples)
    eval_set, train_set = samples[:EVAL_N], samples[EVAL_N:]

    OUT_DIR.mkdir(exist_ok=True)
    for name, split in (("train", train_set), ("eval", eval_set)):
        with open(OUT_DIR / f"{name}.jsonl", "w", encoding="utf-8") as fh:
            for s in split:
                fh.write(json.dumps(s, ensure_ascii=False) + "\n")
        print(f"[+] {name}: {len(split)} samples -> {OUT_DIR / f'{name}.jsonl'}")

    print("[*] stats:", dict(stats))


if __name__ == "__main__":
    main()