Spaces:
No application file
No application file
| #!/usr/bin/env python | |
| """Stage 03 — train. | |
| Trains only the projections + fusion + regression head on cached embeddings | |
| (stage 02 output). Backbones are never loaded here. | |
| Usage: | |
| python scripts/03_train.py --config configs/base.yaml | |
| """ | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch.utils.data import DataLoader, random_split | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from src.data.dataset import EmbeddingDataset | |
| from src.models.price_model import PriceModel | |
| from src.training.trainer import Trainer | |
| from src.utils.config import load_config | |
| from src.utils.exceptions import PricePredictorError | |
| from src.utils.logging import get_logger | |
| from src.utils.seed import set_seed | |
| logger = get_logger(__name__) | |
| def run(config_path: str) -> None: | |
| config = load_config(config_path) | |
| set_seed(config["seed"]) | |
| data_cfg = config["data"] | |
| embeddings_dir = Path(data_cfg["embeddings_dir"]) | |
| price_path = embeddings_dir / "train_price.npy" | |
| if not price_path.exists(): | |
| raise PricePredictorError( | |
| f"{price_path} not found — run scripts/02_extract_embeddings.py first" | |
| ) | |
| prices = np.load(price_path) | |
| full_dataset = EmbeddingDataset( | |
| text_embeddings_path=str(embeddings_dir / "train_text.npy"), | |
| image_embeddings_path=str(embeddings_dir / "train_image.npy"), | |
| prices=prices, | |
| ) | |
| val_split = config["training"].get("val_split", 0.15) | |
| n_val = max(1, int(len(full_dataset) * val_split)) | |
| n_train = len(full_dataset) - n_val | |
| if n_train <= 0: | |
| raise PricePredictorError( | |
| f"val_split={val_split} leaves zero training rows for dataset of size {len(full_dataset)}" | |
| ) | |
| generator = torch.Generator().manual_seed(config["seed"]) | |
| train_ds, val_ds = random_split(full_dataset, [n_train, n_val], generator=generator) | |
| batch_size = config["training"]["batch_size"] | |
| train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, drop_last=True) | |
| val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False) | |
| model = PriceModel.from_config(config) | |
| trainer = Trainer(model, config["training"], checkpoint_dir=config["checkpoint_dir"]) | |
| history = trainer.fit(train_loader, val_loader) | |
| logger.info("Training complete. Best val_smape=%.4f", trainer.best_val_loss) | |
| return history | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Stage 03: train fusion + head") | |
| parser.add_argument("--config", default="configs/base.yaml") | |
| args = parser.parse_args() | |
| try: | |
| run(args.config) | |
| except PricePredictorError as e: | |
| logger.error("Training failed: %s", e) | |
| sys.exit(1) | |
| except Exception as e: | |
| logger.exception("Unexpected error during training: %s", e) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() | |