"""Переобучение и проверка моделей grabber-ai (используется из tools/collect.py). Шаги вынесены в функции, чтобы меню вызывало их по очереди: prepare() data/labels.csv -> data/processed train_segmenter(out_dir) обучение сегментатора страниц train_main() основная модель (тип, автор, 18+): долго run_tests() tests/run_tests.py compare(dirs) правила vs сегментаторы по трекерам promote(new_dir) новые веса -> models/segmenter (старые в архив) """ from __future__ import annotations import re import shutil import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MODELS = ROOT / "models" MIN_N = 30 # трекеры с меньшим числом страниц в вывод попадают, но решение не определяют RE_CELL = re.compile(r"(\w+) n=(\d+) P=([\d.]+) R=([\d.]+)") def run(args: list[str]) -> int: """Запуск шага тем же Python; вывод идёт прямо в консоль.""" print("\n$ python " + " ".join(args), flush=True) return subprocess.call([sys.executable, *args], cwd=ROOT) def prepare() -> bool: return run(["-m", "src.prepare_data"]) == 0 def train_segmenter(out_dir: str = "models/segmenter_v2", epochs: int = 4) -> bool: return run(["-m", "src.segmenter", "--train", "--out", out_dir, "--epochs", str(epochs)]) == 0 def backup_main() -> str | None: """Копия models/best перед обучением: src.train перезаписывает каталог.""" import shutil import time src = ROOT / "models" / "best" if not src.exists(): return None name = f"best_backup_{time.strftime('%Y%m%d_%H%M%S')}" shutil.copytree(src, ROOT / "models" / name) return name def train_main() -> bool: """Основная модель: 5-fold, ~15-20 минут на GPU. Старый models/best сохраняется в models/best_backup_*.""" saved = backup_main() if saved: print(f"копия прежней модели: models/{saved}", flush=True) return run(["-m", "src.train"]) == 0 PROFILES = { # те же имена, что в src/train.py:MODEL_CONFIGS (без импорта torch в меню) "tiny2": "rubert-tiny2, 30M — самая быстрая, годится для ONNX/CPU", "base": "ruBert-base, 178M — текущий рабочий выбор", "large": "ruBert-large, 427M — та же семья, втрое больше: обучение ~3× дольше, инференс на CPU медленнее", "modern": "RuModernBERT-base, 149M (2024) — новая архитектура; ONNX-экспорт проекта её не поддерживает", } def train_models(profiles: list[str], folds: int = 0, tag: str = "") -> bool: """Обучение профилей отдельными запусками в models/runs/: models/best и reports/ не меняются.""" args = ["-m", "src.train", "--runs", "--models", *profiles, "--folds", str(folds)] if tag: args += ["--tag", tag] return run(args) == 0 def evaluate_models(out_json: Path, refresh: bool = False) -> dict | None: """Оценка всех моделей на текущей тестовой выборке (подпроцесс: GPU освобождается после выхода).""" out_json.unlink(missing_ok=True) args = ["-m", "src.runs", "--out", str(out_json)] + (["--refresh"] if refresh else []) if run(args) != 0 or not out_json.exists(): return None import json return json.loads(out_json.read_text(encoding="utf-8")) def run_tests() -> bool: return run(["tests/run_tests.py"]) == 0 def _e2e(seg_dir: str) -> dict: """{'rules': {трекер: (n, P, R)}, 'model': {...}} на страницах, которых модель не видела.""" proc = subprocess.run([sys.executable, "tests/e2e_segmenter.py", seg_dir], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", errors="replace") if proc.returncode != 0: tail = "\n".join(proc.stderr.strip().splitlines()[-6:]) raise RuntimeError(f"e2e_segmenter упал на {seg_dir}:\n{tail}") test_block = proc.stdout.split("train-страницы")[0] out: dict[str, dict] = {} for line in test_block.splitlines(): m = re.match(r"\s+(rules|model)\s*:(.*)", line) if m: out[m.group(1)] = {t: (int(n), float(p), float(r)) for t, n, p, r in RE_CELL.findall(m.group(2))} return out def compare(seg_dirs: list[str]) -> dict: """Печатает таблицу: правила и каждый сегментатор по трекерам. Возвращает {'rules': {...},