multi-modal / scripts /extract_emb.py
Lomesh7777's picture
phase-1
96d8696
Raw
History Blame Contribute Delete
6.28 kB
#!/usr/bin/env python
"""Stage 02 β€” extract embeddings.
The one GPU-heavy stage: runs the frozen EmbeddingGemma + SigLIP2 encoders
over every row and caches the results as .npy files, so training (stage 03)
never has to re-run the encoders.
Order of operations (deliberate, not incidental):
1. Load the text encoder, extract text embeddings for train AND test.
2. Free the text encoder from RAM/VRAM.
3. Load the image encoder, extract image embeddings for train AND test.
4. Free the image encoder.
This keeps peak memory to "one encoder at a time" instead of both loaded
simultaneously, and means a crash during image extraction doesn't force
re-running text extraction too.
Every batch is checkpointed to disk (see src/encoders/batch_extract.py) β€”
if this script crashes or the session times out, re-running the exact same
command resumes from the last checkpoint instead of starting over.
Usage:
python scripts/02_extract_embeddings.py --config configs/base.yaml
"""
import argparse
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.encoders.batch_extract import free_encoder, resumable_batch_encode
from src.encoders.registry import build_image_encoder, build_text_encoder
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__)
SPLITS = ("train", "test")
def _load_split_df(processed_dir: Path, split: str) -> pd.DataFrame:
parquet_path = processed_dir / f"{split}_clean.parquet"
if not parquet_path.exists():
raise PricePredictorError(f"{parquet_path} not found β€” run scripts/01_preprocess.py first")
return pd.read_parquet(parquet_path)
def run(config_path: str) -> None:
config = load_config(config_path)
set_seed(config["seed"])
data_cfg = config["data"]
processed_dir = Path(data_cfg["processed_dir"])
embeddings_dir = Path(data_cfg["embeddings_dir"])
embeddings_dir.mkdir(parents=True, exist_ok=True)
dfs = {split: _load_split_df(processed_dir, split) for split in SPLITS}
for split, df in dfs.items():
logger.info("Loaded split=%s: %d rows", split, len(df))
df[["sample_id"]].to_csv(embeddings_dir / f"{split}_ids.csv", index=False)
if split == "train":
np.save(embeddings_dir / "train_price.npy", df["price"].to_numpy(dtype=np.float32))
# ---- Phase 1: text embeddings (train + test), then free the encoder ----
logger.info("=== Phase 1/2: text embeddings ===")
text_cfg = config["encoders"]["text"]
text_encoder = build_text_encoder(text_cfg)
text_output_dim = text_cfg.get("mrl_truncate") or text_cfg["output_dim"]
for split, df in dfs.items():
texts = df["catalog_content"].tolist()
resumable_batch_encode(
items=texts,
encode_batch_fn=lambda batch: text_encoder.encode(batch).numpy(),
output_dim=text_output_dim,
output_path=str(embeddings_dir / f"{split}_text.npy"),
batch_size=text_cfg["batch_size"],
desc=f"{split} text",
)
free_encoder(text_encoder)
logger.info("=== Phase 1/2 complete, text encoder freed from memory ===")
# ---- Phase 2: image embeddings (train + test), then free the encoder ----
logger.info("=== Phase 2/2: image embeddings ===")
image_cfg = config["encoders"]["image"]
image_encoder = build_image_encoder(image_cfg)
image_encoder.ensure_loaded() # detect the real output_dim now, not mid-batch
image_output_dim = image_encoder.output_dim
if image_output_dim != image_cfg.get("output_dim"):
logger.warning(
"configs/base.yaml declares encoders.image.output_dim=%s but the model "
"actually outputs %d β€” using %d. Update the config to match so training "
"(stage 03) builds the projection layer with the correct input size.",
image_cfg.get("output_dim"), image_output_dim, image_output_dim,
)
def _encode_image_batch(paths):
images = [Image.open(p).convert("RGB") for p in paths]
return image_encoder.encode(images).numpy()
for split, df in dfs.items():
image_paths = df["image_path"].tolist()
resumable_batch_encode(
items=image_paths,
encode_batch_fn=_encode_image_batch,
output_dim=image_output_dim,
output_path=str(embeddings_dir / f"{split}_image.npy"),
batch_size=image_cfg["batch_size"],
desc=f"{split} image",
)
free_encoder(image_encoder)
logger.info("=== Phase 2/2 complete, image encoder freed from memory ===")
# ---- Final sanity check: every split's text/image/row counts line up ----
for split, df in dfs.items():
text_matrix = np.load(embeddings_dir / f"{split}_text.npy")
image_matrix = np.load(embeddings_dir / f"{split}_image.npy")
if text_matrix.shape[0] != len(df) or image_matrix.shape[0] != len(df):
raise PricePredictorError(
f"Embedding count mismatch for split={split}: "
f"text={text_matrix.shape[0]} image={image_matrix.shape[0]} rows={len(df)}"
)
logger.info(
"Verified split=%s: text %s, image %s", split, text_matrix.shape, image_matrix.shape,
)
logger.info("All embeddings extracted and verified.")
def main() -> None:
parser = argparse.ArgumentParser(description="Stage 02: extract and cache embeddings")
parser.add_argument("--config", default="configs/base.yaml")
args = parser.parse_args()
try:
run(args.config)
except PricePredictorError as e:
logger.error("Embedding extraction failed: %s", e)
logger.error("Progress up to the last checkpoint was saved β€” re-run this exact command to resume.")
sys.exit(1)
except Exception as e:
logger.exception("Unexpected error during embedding extraction: %s", e)
logger.error("Progress up to the last checkpoint was saved β€” re-run this exact command to resume.")
sys.exit(1)
if __name__ == "__main__":
main()