amarorn / scripts /hf_train_wc.py
beAnalytic's picture
feat: sync main with feature/superbet-live-inplay
16c19b8 verified
Raw
History Blame Contribute Delete
6.38 kB
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "scikit-learn>=1.4.0",
# "pandas>=2.2.0",
# "pyarrow>=15.0.0",
# "pydantic>=2.6.0",
# "pydantic-settings>=2.2.0",
# "structlog>=24.1.0",
# "huggingface_hub>=0.23.0",
# "httpx>=0.27.0",
# "pyyaml>=6.0.0",
# "numpy>=1.26.0",
# ]
# ///
"""Treina o modelo WC (train-wc) em Hugging Face Jobs (CPU).
Assume que o job roda com cwd na raiz do repositório clonado — HF Jobs monta
o repo em /workspace. O script adiciona a raiz ao ``sys.path``, baixa dados
do dataset Hub para ``./data/`` e persiste artefatos no artifact-repo.
Exemplo::
hf jobs uv run scripts/hf_train_wc.py --flavor cpu-upgrade --timeout 15m \\
--secrets HF_TOKEN -- \\
--dataset-repo USER/api-noticia-wc-train-data \\
--artifact-repo USER/api-noticia-wc-artifacts \\
--force
"""
from __future__ import annotations
import argparse
import os
import shutil
import sys
from datetime import UTC, datetime
from pathlib import Path
# LAKE_PRIMARY deve ser definido antes de qualquer import do projeto.
os.environ["LAKE_PRIMARY"] = "local"
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Treina modelo WC no Hugging Face Jobs e publica artefatos no Hub",
)
parser.add_argument(
"--dataset-repo",
type=str,
required=True,
help="Repo HF (dataset) com estrutura data/lake/fixtures, data/wc/, etc.",
)
parser.add_argument(
"--artifact-repo",
type=str,
required=True,
help="Repo HF (model) para upload de predictor.pkl e manifest.json",
)
parser.add_argument(
"--force",
action="store_true",
default=False,
help="Ignora artefato em cache e força retreino",
)
parser.add_argument(
"--data-prefix",
type=str,
default="",
help="Prefixo opcional no dataset-repo (ex.: v1/ → v1/data/...)",
)
return parser.parse_args()
def _resolve_data_root(snapshot_dir: Path, data_prefix: str) -> Path:
base = snapshot_dir
prefix = data_prefix.strip("/")
if prefix:
base = snapshot_dir / prefix
data_root = base / "data"
if not data_root.is_dir():
raise FileNotFoundError(
f"Diretório data/ não encontrado em {base}. "
"Verifique --data-prefix e a estrutura do dataset-repo."
)
return data_root
def download_and_sync_dataset(repo_id: str, data_prefix: str) -> None:
from huggingface_hub import snapshot_download
print(f"Baixando dataset {repo_id}...")
snapshot_dir = Path(
snapshot_download(
repo_id=repo_id,
repo_type="dataset",
local_dir_use_symlinks=False,
)
)
data_root = _resolve_data_root(snapshot_dir, data_prefix)
target = Path("data")
copied = 0
for src in data_root.rglob("*"):
if not src.is_file():
continue
rel = src.relative_to(data_root)
dst = target / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
copied += 1
print(f"Dataset sincronizado: {copied} arquivos em {target.resolve()}")
def upload_artifacts(repo_id: str, artifact_dir: Path) -> None:
from huggingface_hub import HfApi
api = HfApi()
stamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
for name in ("predictor.pkl", "manifest.json"):
path = artifact_dir / name
if not path.exists():
raise FileNotFoundError(f"Artefato ausente após treino: {path}")
print(f"Enviando {name}{repo_id}...")
api.upload_file(
path_or_fileobj=str(path),
path_in_repo=name,
repo_id=repo_id,
repo_type="model",
commit_message=f"wc train {stamp}",
)
print(f"Artefatos publicados em https://huggingface.co/{repo_id}")
def _print_manifest_metrics(manifest: dict) -> None:
training = manifest.get("training_metrics") or {}
collab = manifest.get("collab_metrics") or {}
print("--- Métricas do manifest ---")
print(f"created_at: {manifest.get('created_at')}")
print(f"fixture_rows: {manifest.get('fixture_rows')}")
if "holdout_accuracy" in training:
print(f"holdout_accuracy: {training['holdout_accuracy']}")
if "holdout_brier" in training:
print(f"holdout_brier: {training['holdout_brier']}")
if "holdout_log_loss" in training:
print(f"holdout_log_loss: {training['holdout_log_loss']}")
if "brier_score" in collab:
print(f"ensemble_brier: {collab['brier_score']}")
if "log_loss" in collab:
print(f"ensemble_log_loss: {collab['log_loss']}")
if "accuracy" in collab:
print(f"ensemble_accuracy: {collab['accuracy']}")
print(f"ensemble_weights: {manifest.get('ensemble_weights')}")
print(f"loaded_from_cache: {manifest.get('loaded_from_cache')}")
def main() -> None:
args = parse_args()
download_and_sync_dataset(args.dataset_repo, args.data_prefix)
import structlog
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer(),
]
)
from config import settings
from models.wc_artifact import load_or_train_wc_predictor
from models.wc_train_progress import WcTrainProgressReporter
fixtures = sorted(settings.fixtures_path.glob("world_cup_*.parquet"))
if not fixtures:
raise FileNotFoundError(
f"Nenhum fixture world_cup_*.parquet em {settings.fixtures_path}. "
"Verifique o conteúdo do dataset-repo."
)
print(f"Fixtures encontrados: {len(fixtures)} arquivos")
reporter = WcTrainProgressReporter(console=True)
if args.force:
print("Modo force: retreino completo")
_predictor, manifest = load_or_train_wc_predictor(
force=args.force,
progress=reporter,
enable_mlflow=False,
)
artifact_dir = settings.wc_artifact_dir
print(f"Artefato local: {artifact_dir.resolve()}")
_print_manifest_metrics(manifest)
upload_artifacts(args.artifact_repo, artifact_dir)
if __name__ == "__main__":
main()