File size: 4,615 Bytes
96d8696
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/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()