File size: 10,719 Bytes
000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 7484310 000c932 | 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 | """
Build the Japanese-heavy conversational calibration set
(calib_conversational.jsonl). An English-only UltraChat draw of the same
size is also written as a byproduct (calib_en_ultrachat.jsonl).
Mix (Section 6 of the project plan), target 512 samples total:
- 40% JA multi-turn instruction/chat -> llm-jp/oasst2-33k-ja
- 20% JA knowledge/long-form -> JA Wikipedia passages, instruction-wrapped
- 15% JA reasoning/math -> Kendamarron/magpie-japanese-math-instruction-17k
- 10% Code (JA-instructed) -> ronantakizawa/python-code-instructions-japanese
- 15% English chat -> HuggingFaceH4/ultrachat_200k
Every sample is stored as a `messages` list (chat format) and later fed
through `tokenizer.apply_chat_template(messages, add_generation_prompt=False)`
at PTQ time, so calibration activations see the same role tokens /
`<|...|>` specials / structure the model sees in real deployment.
IMPORTANT: the Wikipedia articles used here for the knowledge/long-form slice
must stay disjoint from the articles used in perplexity_ja.py's held-out set
(recorded in results/*/perplexity_ja.json `corpus.article_ids`). The held-out
set consumed articles from the very start of the stream (skip 0, ~50 articles);
we skip --wiki-skip-articles (default 5000) before collecting, guaranteeing
disjointness with two orders of magnitude of margin.
The EN-only set is drawn from the SAME single UltraChat pass as the
main mix's en_chat slice and split by index, so the two are disjoint by
construction (no skip heuristics).
"""
import argparse
import json
import random
from datasets import load_dataset
from transformers import AutoTokenizer
SEED = 1234
TARGET_TOTAL = 512
MAX_TOKENS = 4096
MIX = {
"ja_chat": 0.40,
"ja_knowledge": 0.20,
"ja_math": 0.15,
"code": 0.10,
"en_chat": 0.15,
}
def iter_ja_chat(rng):
# oasst2-33k-ja alone fills the ja_chat pool target, so it is the sole
# source actually drawn from (see the datasheet).
sources = [
("llm-jp/oasst2-33k-ja", "apache-2.0"),
]
for name, license_ in sources:
ds = load_dataset(name, split="train", streaming=True)
for ex in ds:
convo = ex["conversations"]
if len(convo) < 2:
continue
yield {
"messages": [{"role": m["role"], "content": m["content"]} for m in convo],
"source": name,
"license": license_,
}
def iter_ja_knowledge(skip_articles: int):
ds = load_dataset("wikimedia/wikipedia", "20231101.ja", split="train", streaming=True)
it = iter(ds)
for _ in range(skip_articles):
next(it)
prompts = [
"次のトピックについて、知っていることを詳しく説明してください:{title}",
"「{title}」について解説してください。",
"次の見出し語を要約・解説する文章を書いてください:{title}",
]
i = 0
for ex in it:
text = ex["text"].strip()
if len(text) < 800:
continue
prompt = prompts[i % len(prompts)].format(title=ex["title"])
yield {
"messages": [
{"role": "user", "content": prompt},
{"role": "assistant", "content": text[:3000]},
],
"source": f"wikimedia/wikipedia:20231101.ja#{ex['id']}",
"license": "cc-by-sa-4.0",
}
i += 1
def iter_ja_math():
ds = load_dataset(
"Kendamarron/magpie-japanese-math-instruction-17k-qwen2.5-bakeneko-32b-instruct",
split="train",
streaming=True,
)
for ex in ds:
if not ex.get("instruction") or not ex.get("output"):
continue
yield {
"messages": [
{"role": "user", "content": ex["instruction"]},
{"role": "assistant", "content": ex["output"]},
],
"source": "Kendamarron/magpie-japanese-math-instruction-17k",
"license": "apache-2.0",
}
def iter_code():
ds = load_dataset("ronantakizawa/python-code-instructions-japanese", split="train", streaming=True)
for ex in ds:
instruction = ex.get("instruction", "")
input_ = ex.get("input", "")
output = ex.get("output", "")
if not instruction or not output:
continue
user_content = instruction if not input_ else f"{instruction}\n\n{input_}"
yield {
"messages": [
{"role": "user", "content": user_content},
{"role": "assistant", "content": output},
],
"source": "ronantakizawa/python-code-instructions-japanese",
"license": "mit",
}
def iter_en_chat():
ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft", streaming=True)
for ex in ds:
messages = ex.get("messages", [])
if len(messages) < 2:
continue
yield {
"messages": [{"role": m["role"], "content": m["content"]} for m in messages],
"source": "HuggingFaceH4/ultrachat_200k",
"license": "mit",
}
def collect(gen, tokenizer, count, rng, max_tokens=MAX_TOKENS, pool_multiplier=4, label=""):
"""Pull samples from a generator, keep ones that fit the token budget,
then randomly downsample to `count` for an unbiased draw from the pool."""
pool = []
pool_target = count * pool_multiplier
for sample in gen:
# Template to a string, then encode — apply_chat_template(tokenize=True)'s
# return type varies across transformers versions (list of ids vs
# BatchEncoding, whose len() is its dict key count and silently
# broke this filter on 5.5.x). This two-step form is unambiguous.
templated = tokenizer.apply_chat_template(
sample["messages"], add_generation_prompt=False, tokenize=False
)
n_tokens = len(tokenizer.encode(templated, add_special_tokens=False))
if n_tokens > max_tokens or n_tokens < 8:
continue
sample["num_tokens"] = n_tokens
pool.append(sample)
if len(pool) % 100 == 0:
print(f" [{label}] pool {len(pool)}/{pool_target}", flush=True)
if len(pool) >= pool_target:
break
rng.shuffle(pool)
return pool[:count]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--tokenizer", default="./models/llm-jp-4-8b-instruct")
parser.add_argument("--output-dir", default="data/calib")
parser.add_argument("--total", type=int, default=TARGET_TOTAL)
parser.add_argument(
"--wiki-skip-articles",
type=int,
default=5000,
help="Skip this many leading JA-Wikipedia articles to stay well clear "
"of perplexity_ja.py's held-out set (which consumes articles from "
"the very start of the stream).",
)
args = parser.parse_args()
rng = random.Random(SEED)
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer, trust_remote_code=True)
counts = {k: round(v * args.total) for k, v in MIX.items()}
# Fix up rounding so counts sum exactly to args.total.
diff = args.total - sum(counts.values())
counts[max(counts, key=counts.get)] += diff
print("Target mix:", counts)
all_samples = []
datasheet = {"seed": SEED, "total": args.total, "max_tokens": MAX_TOKENS, "mix": {}}
print("Collecting ja_chat...", flush=True)
ja_chat = collect(iter_ja_chat(rng), tokenizer, counts["ja_chat"], rng, label="ja_chat")
all_samples += ja_chat
print("Collecting ja_knowledge...", flush=True)
ja_knowledge = collect(
iter_ja_knowledge(args.wiki_skip_articles),
tokenizer,
counts["ja_knowledge"],
rng,
label="ja_knowledge",
)
all_samples += ja_knowledge
print("Collecting ja_math...", flush=True)
ja_math = collect(iter_ja_math(), tokenizer, counts["ja_math"], rng, label="ja_math")
all_samples += ja_math
print("Collecting code...", flush=True)
code = collect(iter_code(), tokenizer, counts["code"], rng, label="code")
all_samples += code
# Collect both EN slices in one filtered pass over one UltraChat
# stream, then split by index — disjoint by construction.
print("Collecting en_chat (single pass)...", flush=True)
en_all = collect(
iter_en_chat(),
tokenizer,
counts["en_chat"] + args.total,
rng,
pool_multiplier=2,
label="en_chat",
)
en_chat = en_all[: counts["en_chat"]]
en_only_extra = en_all[counts["en_chat"] :]
all_samples += en_chat
for name, samples in [
("ja_chat", ja_chat),
("ja_knowledge", ja_knowledge),
("ja_math", ja_math),
("code", code),
("en_chat", en_chat),
]:
sources = sorted(set(s["source"].split("#")[0] for s in samples))
licenses = sorted(set(s["license"] for s in samples))
datasheet["mix"][name] = {
"count": len(samples),
"target": counts[name],
"sources": sources,
"licenses": licenses,
"mean_tokens": sum(s["num_tokens"] for s in samples) / max(len(samples), 1),
}
datasheet["en_only_extra"] = {
"count": len(en_only_extra),
"target": args.total,
"sources": ["HuggingFaceH4/ultrachat_200k"],
"licenses": ["mit"],
"mean_tokens": sum(s["num_tokens"] for s in en_only_extra) / max(len(en_only_extra), 1),
"note": "drawn from the same single-pass pool as the main mix's "
"en_chat slice and split by index — disjoint by construction",
}
rng.shuffle(all_samples)
out_path = f"{args.output_dir}/calib_conversational.jsonl"
with open(out_path, "w") as f:
for s in all_samples:
f.write(json.dumps({"messages": s["messages"], "source": s["source"]}, ensure_ascii=False) + "\n")
datasheet["actual_total"] = len(all_samples)
with open(f"{args.output_dir}/calibration_datasheet_conversational.json", "w") as f:
json.dump(datasheet, f, ensure_ascii=False, indent=2)
print(f"\nWrote {len(all_samples)} samples to {out_path}", flush=True)
print(json.dumps(datasheet, ensure_ascii=False, indent=2), flush=True)
en_out_path = f"{args.output_dir}/calib_en_ultrachat.jsonl"
with open(en_out_path, "w") as f:
for s in en_only_extra:
f.write(json.dumps({"messages": s["messages"], "source": s["source"]}, ensure_ascii=False) + "\n")
print(f"Wrote {len(en_only_extra)} EN-only samples to {en_out_path}", flush=True)
if __name__ == "__main__":
main()
|