#!/usr/bin/env python3 """ validate_training_data.py — BUILD-A3 torrkörning Validerar träningsdatans JSONL-integritet, chat-format, token-distribution och simulerar load_dataset_from_jsonl() utan unsloth. """ import json import sys import re from pathlib import Path from collections import Counter, defaultdict TRAIN_PATH = "/opt/amos/data/finetune-exports/aamos_train_v3.jsonl" VAL_PATH = "/opt/amos/data/finetune-exports/aamos_val_v3.jsonl" MAX_SEQ_LENGTH = 2048 # Den osäkra parametern # ─── Simulera load_dataset_from_jsonl() utan HuggingFace Dataset ────────────── def simulate_load_dataset(path: str): """Simulerar load_dataset_from_jsonl() exakt — returnerar list of {"messages": [...]}""" records = [] errors = [] with open(path, "r", encoding="utf-8") as f: for lineno, line in enumerate(f, 1): line = line.strip() if not line: continue try: obj = json.loads(line) except json.JSONDecodeError as e: errors.append({"lineno": lineno, "error": f"JSONDecodeError: {e}", "raw": line[:100]}) continue # Speglar exakt load_dataset_from_jsonl()-logiken if isinstance(obj, list): records.append({"lineno": lineno, "messages": obj, "source": "direct_list"}) elif "messages" in obj: records.append({"lineno": lineno, "messages": obj["messages"], "source": "messages_key"}) else: msgs = [] if "system" in obj: msgs.append({"role": "system", "content": obj["system"]}) if "user" in obj or "input" in obj: msgs.append({"role": "user", "content": obj.get("user", obj.get("input", ""))}) if "assistant" in obj or "output" in obj: msgs.append({"role": "assistant", "content": obj.get("assistant", obj.get("output", ""))}) if msgs: records.append({"lineno": lineno, "messages": msgs, "source": "flat_keys"}) else: errors.append({"lineno": lineno, "error": "Okänt format — ingen giltig nyckel hittades", "raw": line[:100]}) return records, errors def estimate_tokens(text: str) -> int: """Grov uppskattning: tecken / 4 (vanlig tumregel för Qwen2/GPT-tokenizers)""" return len(text) // 4 def validate_messages(messages, lineno): """Validerar ett enskilt examples messages-lista. Returnerar lista av problem.""" issues = [] if not isinstance(messages, list): issues.append(f"L{lineno}: messages är inte en lista (typ: {type(messages).__name__})") return issues if len(messages) == 0: issues.append(f"L{lineno}: tom messages-lista") return issues roles = [m.get("role") for m in messages if isinstance(m, dict)] # Kontrollera att alla messages har role + content for i, msg in enumerate(messages): if not isinstance(msg, dict): issues.append(f"L{lineno}: message[{i}] är inte ett dict") continue if "role" not in msg: issues.append(f"L{lineno}: message[{i}] saknar 'role'") if "content" not in msg: issues.append(f"L{lineno}: message[{i}] saknar 'content'") elif msg["content"] == "" or msg["content"] is None: issues.append(f"L{lineno}: message[{i}] (role={msg.get('role')}) har tomt content") return issues def analyze_file(path: str, label: str): print(f"\n{'='*70}") print(f"ANALYSERAR: {label}") print(f"Fil: {path}") print(f"{'='*70}") # ─── 1. Simulera laddning ────────────────────────────────────────────────── records, json_errors = simulate_load_dataset(path) total_lines = 0 with open(path, "r", encoding="utf-8") as f: for line in f: if line.strip(): total_lines += 1 print(f"\n[1] JSONL-integritet") print(f" Totalt icke-tomma rader: {total_lines}") print(f" Korrekt laddade records: {len(records)}") print(f" JSON-fel (parse-errors): {len(json_errors)}") if json_errors: print(" FELAKTIGA RADER:") for e in json_errors[:5]: print(f" L{e['lineno']}: {e['error']} | {e['raw']}") # Källformat-distribution source_counts = Counter(r.get("source", "unknown") for r in records) print(f" Format-distribution: {dict(source_counts)}") # ─── 2. Chat-format validering ───────────────────────────────────────────── print(f"\n[2] Chat-format validering") message_issues = [] role_profiles = Counter() has_system = 0 has_user = 0 has_assistant = 0 missing_system = 0 missing_user = 0 missing_assistant = 0 empty_content = [] for r in records: msgs = r["messages"] lineno = r["lineno"] issues = validate_messages(msgs, lineno) if issues: message_issues.extend(issues) roles = tuple(sorted(set(m.get("role","?") for m in msgs if isinstance(m, dict)))) role_profiles[roles] += 1 roles_list = [m.get("role") for m in msgs if isinstance(m, dict)] if "system" in roles_list: has_system += 1 else: missing_system += 1 if "user" in roles_list: has_user += 1 else: missing_user += 1 if "assistant" in roles_list: has_assistant += 1 else: missing_assistant += 1 # Kolla tomt content for msg in msgs: if isinstance(msg, dict): c = msg.get("content", "MISSING") if c == "" or c is None: empty_content.append((lineno, msg.get("role","?"))) print(f" Har system-meddelande: {has_system} / {len(records)}") print(f" Har user-meddelande: {has_user} / {len(records)}") print(f" Har assistant-meddelande: {has_assistant} / {len(records)}") print(f" Saknar system: {missing_system}") print(f" Saknar user: {missing_user}") print(f" Saknar assistant: {missing_assistant}") print(f" Tomma content: {len(empty_content)}") if empty_content[:5]: for lineno, role in empty_content[:5]: print(f" L{lineno}: role={role}") print(f" Roll-profiler (topp 5):") for combo, cnt in role_profiles.most_common(5): print(f" {combo}: {cnt}") print(f" Valideringsfel (meddelanden): {len(message_issues)}") if message_issues[:5]: for issue in message_issues[:5]: print(f" {issue}") # ─── 3. Token-längd-distribution ────────────────────────────────────────── print(f"\n[3] Token-längd-distribution (uppskattning: tecken/4)") token_counts = [] for r in records: # Simulera ChatML-format som tokenizern skulle se full_text = "" for msg in r["messages"]: if isinstance(msg, dict): role = msg.get("role", "") content = msg.get("content", "") # ChatML-format för Qwen2.5 full_text += f"<|im_start|>{role}\n{content}<|im_end|>\n" tok = estimate_tokens(full_text) token_counts.append(tok) if token_counts: token_counts_sorted = sorted(token_counts) n = len(token_counts) over_2048 = sum(1 for t in token_counts if t > MAX_SEQ_LENGTH) over_1024 = sum(1 for t in token_counts if t > 1024) over_4096 = sum(1 for t in token_counts if t > 4096) percentiles = {} for p in [50, 75, 90, 95, 99, 100]: idx = min(int(n * p / 100), n-1) percentiles[p] = token_counts_sorted[idx] print(f" Antal exempel: {n}") print(f" Min tokens: {min(token_counts)}") print(f" Max tokens: {max(token_counts)}") print(f" Medelvärde: {sum(token_counts)//n}") print(f" Percentiler:") for p, v in percentiles.items(): print(f" p{p:3d}: {v}") print(f"\n >>> Överstiger max_seq_length={MAX_SEQ_LENGTH}:") print(f" {over_2048} av {n} ({over_2048/n*100:.1f}%)") print(f" >>> Överstiger 1024: {over_1024} ({over_1024/n*100:.1f}%)") print(f" >>> Överstiger 4096: {over_4096} ({over_4096/n*100:.1f}%)") # Histogram buckets = [0]*10 # 0-200, 200-400, ..., 1800-2000, 2000+ bucket_labels = ["0-200","200-400","400-600","600-800","800-1000", "1000-1200","1200-1400","1400-1600","1600-1800","1800-2000","2000+"] hist = defaultdict(int) for t in token_counts: if t < 200: hist["0-200"] += 1 elif t < 400: hist["200-400"] += 1 elif t < 600: hist["400-600"] += 1 elif t < 800: hist["600-800"] += 1 elif t < 1000: hist["800-1000"] += 1 elif t < 1200: hist["1000-1200"] += 1 elif t < 1400: hist["1200-1400"] += 1 elif t < 1600: hist["1400-1600"] += 1 elif t < 1800: hist["1600-1800"] += 1 elif t < 2000: hist["1800-2000"] += 1 else: hist["2000+"] += 1 print(f"\n Histogram:") for label in bucket_labels: cnt = hist[label] bar = "█" * (cnt * 40 // n) print(f" {label:10s}: {cnt:6d} ({cnt/n*100:5.1f}%) {bar}") return { "total_lines": total_lines, "records": len(records), "json_errors": len(json_errors), "message_issues": len(message_issues), "empty_content": len(empty_content), "missing_system": missing_system, "missing_user": missing_user, "missing_assistant": missing_assistant, "over_2048": over_2048 if token_counts else 0, "over_4096": over_4096 if token_counts else 0, "max_tokens": max(token_counts) if token_counts else 0, "p95_tokens": percentiles.get(95, 0) if token_counts else 0, "p99_tokens": percentiles.get(99, 0) if token_counts else 0, "n": n if token_counts else 0, } def canon_check(path: str, label: str): """CANON-stickprov: verifiera injicerade CANON-exempel.""" print(f"\n[5] CANON-stickprov ({label})") canon_keywords = { "infrastrukturkontroll": 0, "559141-7042": 0, # korrekt org-nr "eskalera": 0, } bad_orgnr_pattern = "559316-3881" # felaktigt org-nr bad_orgnr_count = 0 with open(path, "r", encoding="utf-8") as f: for lineno, line in enumerate(f, 1): if not line.strip(): continue for kw in canon_keywords: if kw in line: canon_keywords[kw] += 1 if bad_orgnr_pattern in line: bad_orgnr_count += 1 for kw, cnt in canon_keywords.items(): status = "✅" if cnt > 0 else "❌ EJ FUNNET" print(f" '{kw}': {cnt} förekomster {status}") status = "✅ 0 förekomster (korrekt)" if bad_orgnr_count == 0 else f"❌ {bad_orgnr_count} FÖREKOMSTER (FEL!)" print(f" Felaktigt org-nr '{bad_orgnr_pattern}': {bad_orgnr_count} förekomster {status}") return canon_keywords, bad_orgnr_count # ─── Kör validering ──────────────────────────────────────────────────────────── print("BUILD-A3 — Torrkörning: Validering av ESLM-träningsdata (v3)") print("=" * 70) train_stats = analyze_file(TRAIN_PATH, "TRÄNINGSDATA (aamos_train_v3.jsonl)") val_stats = analyze_file(VAL_PATH, "VALIDERINGSDATA (aamos_val_v3.jsonl)") # CANON-check på båda filerna canon_train, bad_train = canon_check(TRAIN_PATH, "träningsdata") canon_val, bad_val = canon_check(VAL_PATH, "valideringsdata") # ─── Sammanfattning ──────────────────────────────────────────────────────────── print(f"\n{'='*70}") print("SAMMANFATTNING & DOM") print(f"{'='*70}") problems = [] warnings = [] # JSON-integritet if train_stats["json_errors"] > 0: problems.append(f"TRÄNING: {train_stats['json_errors']} JSON-parse-fel") if val_stats["json_errors"] > 0: problems.append(f"VALIDERING: {val_stats['json_errors']} JSON-parse-fel") # Format-problem if train_stats["missing_user"] > 0: problems.append(f"TRÄNING: {train_stats['missing_user']} exempel saknar user-meddelande") if train_stats["missing_assistant"] > 0: problems.append(f"TRÄNING: {train_stats['missing_assistant']} exempel saknar assistant-meddelande") if train_stats["empty_content"] > 0: problems.append(f"TRÄNING: {train_stats['empty_content']} tomma content-fält") if train_stats["missing_system"] > 0: warnings.append(f"TRÄNING: {train_stats['missing_system']} exempel saknar system-meddelande (varning, ej blockering)") # Token-längd over_pct = train_stats["over_2048"] / train_stats["n"] * 100 if train_stats["n"] > 0 else 0 if train_stats["over_2048"] > 0: msg = f"TRÄNING: {train_stats['over_2048']} ({over_pct:.1f}%) exempel överstiger max_seq_length=2048" if over_pct > 5: problems.append(msg + " — HÖJNING REKOMMENDERAS") else: warnings.append(msg + " — trunkeras vid träning") if train_stats["over_4096"] > 0: problems.append(f"TRÄNING: {train_stats['over_4096']} exempel överstiger 4096 tokens — kräver ännu längre kontext!") # CANON for kw, cnt in canon_train.items(): if cnt == 0: problems.append(f"CANON: nyckelord '{kw}' EJ FUNNET i träningsdata!") if bad_train > 0: problems.append(f"CANON: Felaktigt org-nr '559316-3881' hittades {bad_train} gånger i träningsdata!") if bad_val > 0: problems.append(f"CANON: Felaktigt org-nr '559316-3881' hittades {bad_val} gånger i valideringsdata!") print(f"\nTräningsdata: {train_stats['records']} records ({train_stats['total_lines']} rader)") print(f"Valideringsdata: {val_stats['records']} records ({val_stats['total_lines']} rader)") print(f"Max token-längd (träning): {train_stats['max_tokens']}") print(f"p95 token-längd (träning): {train_stats['p95_tokens']}") print(f"p99 token-längd (träning): {train_stats['p99_tokens']}") print(f"Överstiger 2048: {train_stats['over_2048']} ({over_pct:.1f}%)") print(f"\n⚠️ VARNINGAR ({len(warnings)}):") for w in warnings: print(f" - {w}") print(f"\n❌ PROBLEM ({len(problems)}):") for p in problems: print(f" - {p}") if not problems: print("\n🟢 DOM: DATAN ÄR KLAR FÖR TRÄNING") if train_stats["over_2048"] > 0 and over_pct <= 5: print(f" OBS: {over_pct:.1f}% av exemplen trunkeras vid max_seq_length=2048") print(f" max_seq_length=2048 bedöms som TILLRÄCKLIGT om p99={train_stats['p99_tokens']} ≤ 2048") elif train_stats["over_2048"] == 0: print(f" max_seq_length=2048 ÄR TILLRÄCKLIGT (inget exempel överstiger gränsen)") else: print("\n🔴 DOM: DATAN HAR PROBLEM — SE OVAN") print(f"\n{'='*70}") print("max_seq_length-rekommendation:") print(f" p95={train_stats['p95_tokens']} tokens") print(f" p99={train_stats['p99_tokens']} tokens") print(f" max={train_stats['max_tokens']} tokens") if train_stats['p99_tokens'] <= 2048: print(f" → max_seq_length=2048 täcker 99% av exemplen ✅") print(f" → VRAM-kalkyl från A2 gäller med 2048 som antaget") elif train_stats['p99_tokens'] <= 4096: print(f" → max_seq_length=4096 rekommenderas för full täckning") print(f" → ⚠️ PÅVERKAR VRAM-kalkyl (A2 måste räkna om!)") else: print(f" → max_seq_length > 4096 kan behövas — utred vidare") print(f" → ⚠️ PÅVERKAR VRAM-kalkyl KRAFTIGT (A2 måste räkna om!)")