Spaces:
No application file
No application file
| #!/usr/bin/env python | |
| """Stage 05 β predict on the test set, from cached embeddings. | |
| The test set was already fully embedded by stage 02 (test_text.npy, | |
| test_image.npy) β re-running EmbeddingGemma/SigLIP2 here would repeat that | |
| expensive GPU work for identical output. This script loads the cached | |
| embeddings and runs only the small trained model (projections + fusion + | |
| head), the same way stages 03/04 do. That's why this is fast: no encoder | |
| loading, no HTTP calls to Hugging Face, no per-row image decoding. | |
| The live encoder chain (src/inference/predictor.py) still exists and is | |
| still correct β but only for genuinely new, never-cached inputs, e.g. a | |
| live API request for a brand-new product. See api/main.py / frontend/app.py | |
| for that path. Don't use Predictor for a batch that was already embedded. | |
| Usage: | |
| python scripts/05_predict.py --config configs/base.yaml | |
| """ | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| import pandas as pd | |
| import torch | |
| from torch.utils.data import DataLoader | |
| 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.utils.config import load_config | |
| from src.utils.exceptions import CheckpointError, PricePredictorError | |
| from src.utils.logging import get_logger | |
| logger = get_logger(__name__) | |
| def run(config_path: str, output_path: str) -> None: | |
| config = load_config(config_path) | |
| embeddings_dir = Path(config["data"]["embeddings_dir"]) | |
| text_path = embeddings_dir / "test_text.npy" | |
| image_path = embeddings_dir / "test_image.npy" | |
| ids_path = embeddings_dir / "test_ids.csv" | |
| for p in (text_path, image_path, ids_path): | |
| if not p.exists(): | |
| raise PricePredictorError( | |
| f"{p} not found β run scripts/02_extract_embeddings.py first. " | |
| "This script predicts from cached test embeddings; it does not " | |
| "re-run the encoders." | |
| ) | |
| dataset = EmbeddingDataset(str(text_path), str(image_path)) | |
| ids_df = pd.read_csv(ids_path) | |
| if len(ids_df) != len(dataset): | |
| raise PricePredictorError( | |
| f"test_ids.csv has {len(ids_df)} rows but the embeddings have {len(dataset)} β " | |
| "these came from different runs. Re-run scripts/02_extract_embeddings.py " | |
| "so both are regenerated together." | |
| ) | |
| loader = DataLoader(dataset, batch_size=256, shuffle=False) | |
| model = PriceModel.from_config(config) | |
| checkpoint_path = Path(config["checkpoint_dir"]) / "best.pt" | |
| if not checkpoint_path.exists(): | |
| raise CheckpointError(f"No checkpoint found at {checkpoint_path} β run scripts/03_train.py first") | |
| checkpoint = torch.load(checkpoint_path, map_location="cpu") | |
| try: | |
| model.load_state_dict(checkpoint["model_state_dict"]) | |
| except RuntimeError as e: | |
| raise CheckpointError( | |
| f"Checkpoint at {checkpoint_path} does not match the current model config " | |
| f"(e.g. a fusion.type or head.hidden_dims mismatch): {e}" | |
| ) from e | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model.to(device) | |
| model.eval() | |
| logger.info("Predicting %d rows from cached embeddings on %s (no encoder loading needed)", len(dataset), device) | |
| all_prices = [] | |
| n_done = 0 | |
| with torch.no_grad(): | |
| for text_emb, image_emb in loader: | |
| text_emb, image_emb = text_emb.to(device), image_emb.to(device) | |
| raw_price = model(text_emb, image_emb) | |
| price = torch.clamp(raw_price, min=0.01) | |
| all_prices.extend(price.cpu().tolist()) | |
| n_done += text_emb.shape[0] | |
| logger.info("Predicted %d/%d rows", n_done, len(dataset)) | |
| out_df = pd.DataFrame({ | |
| "sample_id": ids_df["sample_id"], | |
| "price": [round(float(p), 2) for p in all_prices], | |
| }) | |
| out_df.to_csv(output_path, index=False) | |
| logger.info("Wrote predictions to %s (%d rows)", output_path, len(out_df)) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Stage 05: predict on the test set from cached embeddings") | |
| parser.add_argument("--config", default="configs/base.yaml") | |
| parser.add_argument("--output", default="data/raw/test_out.csv") | |
| args = parser.parse_args() | |
| try: | |
| run(args.config, args.output) | |
| except PricePredictorError as e: | |
| logger.error("Prediction failed: %s", e) | |
| sys.exit(1) | |
| except Exception as e: | |
| logger.exception("Unexpected error during prediction: %s", e) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |