File size: 15,145 Bytes
c83c617 | 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 | """Clean pinned V3 inputs, sample documents and write existing V2 token/window formats."""
import argparse
import hashlib
import json
import re
import sqlite3
import sys
import unicodedata
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "src"))
import numpy as np
import pyarrow.parquet as pq
import sentencepiece as spm
from vimeml.training.data import prepare_indexes, write_json
from vimeml.tokenizer.encode_parallel import output_lock
SPLITS = ("train", "validation", "test")
CONTROLS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
JAPANESE = re.compile(r"[\u3040-\u30ff\u3400-\u9fff]")
NOISE = re.compile(r"^(?:https?://\S+|ログイン|ログアウト|前のページ|次のページ|広告|Cookie設定)$")
MASK = re.compile(r"(?:\[PERSON\]|\[NAME\]|\[MASK\]|<PERSON>|<NAME>)", re.I)
def normalized(text):
return CONTROLS.sub(" ", unicodedata.normalize("NFC", text)).strip().lstrip("\ufeff")
def digest(text):
# Content identity for deduplication, not repeated file-integrity SHA256 scans.
return hashlib.blake2b(text.encode("utf-8"), digest_size=16).hexdigest()
def chunks(text, chat):
text = normalized(text)
if not text or "\ufffd" in text or MASK.search(text):
return []
if chat:
return [text] # Preserve short turns, punctuation, spaces and internal newlines.
lines = [line.strip() for line in text.splitlines() if line.strip()]
text = " ".join(line for line in lines if not NOISE.fullmatch(line))
if len(text) < 24 or len(JAPANESE.findall(text)) / len(text) < .20:
return []
pieces, pending = [], ""
for sentence in re.findall(r"[^。!?!?]+[。!?!?]*|[。!?!?]+", text):
if pending and len(pending) + len(sentence) > 1024:
pieces.append(pending)
pending = ""
while len(sentence) > 1024:
pieces.append(sentence[:1024])
sentence = sentence[1024:]
pending += sentence
if pending:
pieces.append(pending)
return [piece.strip() for piece in pieces if piece.strip()]
def ingest(db, sp, texts, identity, source, pool, seed, encoded=None):
doc_id = digest(identity)
if db.execute("SELECT 1 FROM docs WHERE id=?", (doc_id,)).fetchone():
return
number = int(digest(f"split:{seed}:{doc_id}"), 16) % 10000
split = "train" if number < 9800 else "validation" if number < 9900 else "test"
priority = int(digest(f"sample:{seed}:{doc_id}")[:15], 16)
if encoded is None:
encoded = sp.encode(texts, out_type=int, num_threads=1)
pairs = 0
reserved = {sp.bos_id(), sp.eos_id(), sp.pad_id(), sp.unk_id()}
for ordinal, (text, ids) in enumerate(zip(texts, encoded, strict=True)):
if not ids or any(token in reserved for token in ids):
continue
key = digest(text)
seen = db.execute("SELECT split,pool FROM seen WHERE sig=?", (key,)).fetchone()
if seen and (seen[0] != split or pool == "web"):
continue # No exact sequence crosses splits; preserve repeated chat within a split.
if db.execute("SELECT 1 FROM blocked WHERE sig=?", (key,)).fetchone():
continue
db.execute("INSERT OR IGNORE INTO seen VALUES (?,?,?)", (key, split, pool))
tokens = np.asarray([sp.bos_id(), *ids, sp.eos_id()], dtype="<u2").tobytes()
n = len(ids) + 1
db.execute("INSERT INTO seq(doc,ordinal,split,source,pool,text,tokens,pairs) VALUES (?,?,?,?,?,?,?,?)",
(doc_id, ordinal, split, source, pool, text, tokens, n))
pairs += n
db.execute("INSERT INTO docs VALUES (?,?,?,?,?,?)", (doc_id, split, pool, source, priority, pairs))
def build_store(db, root, tokenizer, sources, selection, pool):
root.mkdir(parents=True, exist_ok=True)
if (root / "manifest.json").exists():
return
tm = json.loads((tokenizer / "manifest.json").read_text(encoding="utf-8"))
stats = {}
for split in SPLITS:
query = "SELECT s.doc,s.ordinal,s.source,s.text,s.tokens FROM seq s JOIN selected d ON s.doc=d.id WHERE s.split=?"
if pool == "chat":
query += " AND s.pool='chat'"
query += " ORDER BY s.id"
handles = {suffix: (root / f"{split}.{suffix}").open("wb")
for suffix in ("tokens.bin", "offsets.bin", "sources.bin", "rows.bin", "jsonl")}
count = total = characters = longest = 0
source_counts = Counter()
handles["offsets.bin"].write(np.asarray([0], dtype="<u8").tobytes())
try:
for doc, ordinal, source, text, tokens in db.execute(query, (split,)):
position = handles["jsonl"].tell()
handles["jsonl"].write((json.dumps({"doc": doc, "ordinal": ordinal, "source": source, "text": text},
ensure_ascii=False) + "\n").encode("utf-8"))
length = len(tokens) // 2
total += length
count += 1
characters += len(text)
longest = max(longest, length)
source_counts[source] += 1
handles["tokens.bin"].write(tokens)
handles["offsets.bin"].write(np.asarray([total], dtype="<u8").tobytes())
handles["rows.bin"].write(np.asarray([position], dtype="<u8").tobytes())
handles["sources.bin"].write(bytes([sources[source]]))
finally:
for handle in handles.values():
handle.close()
if not count:
raise ValueError(f"Empty {pool}/{split}")
stats[split] = {"sentences": count, "stored_tokens": total, "content_tokens": total - 2 * count,
"prediction_pairs": total - count, "characters": characters,
"max_sequence_tokens": longest, "primary_sources": dict(source_counts)}
write_json(root / "stats.json", {"splits": stats})
write_json(root / "manifest.json", {
"status": "complete", "format": "vimeml_sentence_tokens_v1", "token_dtype": "uint16_le",
"offset_dtype": "uint64_le", "offset_unit": "tokens", "vocab_size": 16384,
"special_ids": tm["special_ids"], "tokenizer_dir": str(tokenizer),
"tokenizer_model_sha256": tm["model_sha256"], "source_ids": sources, "splits": stats,
"sequence_policy": "BOS + web paragraph chunk or independent chat turn + EOS; document/dialogue grouped splits",
"corpus_dir": str(root), "selection": selection, "pool": pool,
"input_verification": {"mode": "metadata", "full_sha256": False},
"output_sha256": {}, "corpus_ready_for_lm_training": True,
"limitations": "Exact chunk overlap exclusion only; no new full near-duplicate or substring contamination audit."})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, default=ROOT / "artifacts/token-data/corpus-v3-half")
parser.add_argument("--web-train-tokens", type=int, default=500_000_000, help="Prediction-pair budget; chat not included")
parser.add_argument("--parquets", type=int, choices=(8, 16), default=8)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
if args.web_train_tokens < 1:
parser.error("--web-train-tokens must be positive")
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
tokenizer = ROOT / "artifacts/tokenizers/ja-unigram-16k-v2"
sp = spm.SentencePieceProcessor(model_file=str(tokenizer / "tokenizer.model"))
if sp.get_piece_size() != 16384:
raise ValueError("Expected frozen 16K V2 tokenizer")
tm = json.loads((tokenizer / "manifest.json").read_text(encoding="utf-8"))
if any(getattr(sp, name + "_id")() != value for name, value in tm["special_ids"].items()):
raise ValueError("Tokenizer special IDs differ from its manifest")
download = json.loads((ROOT / "datasets/v3/download-manifest.json").read_text(encoding="utf-8"))
files = download["jpnmix"]["files"][::2 if args.parquets == 8 else 1]
inputs = [ROOT / "datasets/v3/JpnMix" / entry["path"] for entry in files]
chats = [("real-persona-chat", ROOT / "datasets/v3/real-persona-chat/real_persona_chat/dialogues"),
("mrmp-chat", ROOT / "datasets/v3/mrmp-chat/multi_relational_multi_party_chat_corpus/dialogues")]
metadata_paths = inputs + [ROOT / "datasets/v3/download-manifest.json", tokenizer / "tokenizer.model",
tokenizer / "manifest.json", Path(__file__).resolve()]
metadata_paths += [path for _, folder in chats for path in sorted(folder.rglob("*.json"))]
metadata_paths += [path for path in [ROOT / "outputs/corpus-fast-v1/validation.txt"] +
[ROOT / "artifacts/benchmarks" / folder / "evaluation_items.json" for folder in
("ajimee-jwtd-v2-v1", "ime-dev-v2", "ime-expanded-v21-candidates-v1/development")]
if path.exists()]
request = {"parquets": args.parquets, "seed": args.seed, "web_train_tokens": args.web_train_tokens,
"files": {str(path.relative_to(ROOT)): [path.stat().st_size, path.stat().st_mtime_ns]
for path in metadata_paths}}
with output_lock(output):
request_path = output / "request.json"
if request_path.exists() and json.loads(request_path.read_text(encoding="utf-8")) != request:
raise ValueError("Inputs/settings changed; use a new --output")
write_json(request_path, request)
db = sqlite3.connect(output / "staging.sqlite")
db.executescript("""
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;
CREATE TABLE IF NOT EXISTS docs(id TEXT PRIMARY KEY,split TEXT,pool TEXT,source TEXT,priority INT,pairs INT);
CREATE TABLE IF NOT EXISTS seq(id INTEGER PRIMARY KEY,doc TEXT,ordinal INT,split TEXT,source TEXT,pool TEXT,text TEXT,tokens BLOB,pairs INT);
CREATE TABLE IF NOT EXISTS seen(sig TEXT PRIMARY KEY,split TEXT,pool TEXT);
CREATE TABLE IF NOT EXISTS blocked(sig TEXT PRIMARY KEY);
CREATE TABLE IF NOT EXISTS done(input TEXT PRIMARY KEY);
CREATE TABLE IF NOT EXISTS selected(id TEXT PRIMARY KEY);
""")
# Exclude exact held-out V2 sequences and sufficiently long frozen IME references.
old_val = ROOT / "outputs/corpus-fast-v1/validation.txt"
if not db.execute("SELECT 1 FROM done WHERE input='exclusions'").fetchone():
if old_val.exists():
with old_val.open(encoding="utf-8") as stream:
db.executemany("INSERT OR IGNORE INTO blocked VALUES (?)", ((digest(normalized(t)),) for t in stream))
for folder in ("ajimee-jwtd-v2-v1", "ime-dev-v2", "ime-expanded-v21-candidates-v1/development"):
path = ROOT / "artifacts/benchmarks" / folder / "evaluation_items.json"
if not path.exists():
continue
for case in json.loads(path.read_text(encoding="utf-8")):
for text in case.get("expected_output", []):
if len(text) >= 8:
db.execute("INSERT OR IGNORE INTO blocked VALUES (?)", (digest(normalized(text)),))
db.execute("INSERT INTO done VALUES ('exclusions')")
db.commit()
jobs = [(name, folder) for name, folder in chats] + [(str(path.relative_to(ROOT)), path) for path in inputs]
for name, path in jobs:
if db.execute("SELECT 1 FROM done WHERE input=?", (name,)).fetchone():
continue
print(f"Cleaning/encoding {name}", flush=True)
write_json(output / "progress.json", {"status": "cleaning_encoding", "input": name})
if path.is_dir():
for position, file in enumerate(sorted(path.rglob("*.json")), 1):
doc = json.loads(file.read_text(encoding="utf-8"))
texts = [part for item in doc["utterances"] for part in chunks(item["text"], True)]
ingest(db, sp, texts, f"{name}:{file.stem}", name, "chat", args.seed)
if position % 2000 == 0:
print(f" {position:,} dialogues", flush=True)
else:
position = 0
for batch in pq.ParquetFile(path).iter_batches(batch_size=256, columns=["text", "source"]):
documents = []
for row in batch.to_pylist():
texts = chunks(row["text"], False)
if texts:
documents.append((texts, row["source"]))
encoded = sp.encode([text for texts, _ in documents for text in texts], out_type=int, num_threads=8)
offset = 0
for texts, source in documents:
ingest(db, sp, texts, "\n".join(texts), "jpnmix:" + source, "web", args.seed,
encoded[offset:offset + len(texts)])
offset += len(texts)
position += batch.num_rows
if position % 10240 == 0:
print(f" {position:,} documents", flush=True)
write_json(output / "progress.json", {"status": "cleaning_encoding", "input": name,
"processed_documents": position})
db.execute("INSERT INTO done VALUES (?)", (name,))
db.commit() # Interrupted current input rolls back; completed inputs are reused.
db.execute("DELETE FROM selected")
db.execute("INSERT INTO selected SELECT id FROM docs WHERE pool='chat' OR split!='train'")
used = 0
for doc, pairs in db.execute("SELECT id,pairs FROM docs WHERE pool='web' AND split='train' ORDER BY priority,id").fetchall():
if pairs and used + pairs <= args.web_train_tokens:
db.execute("INSERT INTO selected VALUES (?)", (doc,))
used += pairs
db.commit()
sources = {name: index for index, (name,) in enumerate(db.execute("SELECT DISTINCT source FROM seq ORDER BY source"))}
selection = {"parquets": [entry["path"] for entry in files], "seed": args.seed,
"web_train_prediction_pairs": used, "budget": args.web_train_tokens,
"split": "98/1/1 by document or complete dialogue", "source_versions": download}
for pool in ("mixed", "chat"):
print(f"Exporting {pool}", flush=True)
write_json(output / "progress.json", {"status": "exporting", "pool": pool})
build_store(db, output / pool, tokenizer, sources, selection, pool)
prepare_indexes(output / pool, output / "indexes" / pool, 128, verification="metadata")
db.close()
write_json(output / "progress.json", {"status": "complete", "web_train_prediction_pairs": used})
print(f"Complete: {output}", flush=True)
if __name__ == "__main__":
main()
|