| """User-facing inference for the Proposed model. |
| |
| Typical use: |
| |
| from src.inference import AspectPredictor |
| p = AspectPredictor() |
| result = p.predict( |
| review_text="The size runs small but the fabric feels great.", |
| product_meta={ |
| "features_text": "100% cotton tee, slim fit", |
| "categories_text": "Clothing > Men > T-Shirts", |
| "price": 19.99, |
| "average_rating": 4.3, |
| "rating_number": 217, |
| }, |
| ) |
| # result == { |
| # "aspects": {"SIZE": "Negative", "MATERIAL": "Positive", ...}, |
| # "meta_attention": {"meta_chunk_1": 0.31, ...}, |
| # } |
| """ |
| from typing import Dict, List, Optional |
|
|
| import numpy as np |
| import pandas as pd |
| import torch |
|
|
| from src import config as cfg |
| from src.evaluator import load_meta_acsa, format_aspect_summary |
| from src.explainer import META_TOKEN_NAMES |
|
|
|
|
| |
| _EXPECTED_META_KEYS = ( |
| "features_text", "categories_text", |
| "price", "average_rating", "rating_number", |
| ) |
|
|
|
|
| def _meta_dict_to_df(meta: Dict) -> pd.DataFrame: |
| """Build a one-row DataFrame the MetaEncoder can transform.""" |
| row = {k: meta.get(k) for k in _EXPECTED_META_KEYS} |
| return pd.DataFrame([row]) |
|
|
|
|
| class AspectPredictor: |
| """Lightweight wrapper that loads the Proposed model on init and exposes |
| one-shot and batch predictions.""" |
|
|
| def __init__(self, checkpoint_dir=None, device=None): |
| if checkpoint_dir is None: |
| from pathlib import Path |
| root_path = Path(__file__).resolve().parent.parent |
| checkpoint_dir = str(root_path / "checkpoints" / "meta_acsa") |
|
|
| self.model, self.tokenizer, self.meta_encoder, self.device = load_meta_acsa( |
| checkpoint_dir=checkpoint_dir, device=device, |
| ) |
|
|
| |
| def predict(self, review_text: str, product_meta: Dict, |
| return_attention: bool = True) -> Dict: |
| enc = self.tokenizer( |
| review_text, max_length=cfg.MAX_LENGTH, truncation=True, |
| padding="max_length", return_tensors="pt", |
| ) |
| input_ids = enc["input_ids"].to(self.device) |
| attn_mask = enc["attention_mask"].to(self.device) |
|
|
| meta_df = _meta_dict_to_df(product_meta) |
| meta_vec = torch.from_numpy(self.meta_encoder.transform(meta_df)).float().to(self.device) |
|
|
| with torch.no_grad(): |
| out = self.model(input_ids, attn_mask, meta_vec) |
| preds = out["logits"][0].argmax(dim=-1).cpu().numpy() |
| meta_attn = out["meta_attn_weights"][0].cpu().numpy() |
|
|
| result = {"aspects": format_aspect_summary(preds)} |
| if return_attention: |
| result["meta_attention"] = { |
| name: float(w) for name, w in zip(META_TOKEN_NAMES, meta_attn) |
| } |
| return result |
|
|
| |
| def predict_batch(self, reviews: List[Dict], batch_size: int = 16) -> List[Dict]: |
| """Each `reviews` item is {'review_text': str, 'product_meta': {...}}.""" |
| results = [] |
| for i in range(0, len(reviews), batch_size): |
| chunk = reviews[i:i + batch_size] |
| enc = self.tokenizer( |
| [r["review_text"] for r in chunk], |
| max_length=cfg.MAX_LENGTH, truncation=True, |
| padding="max_length", return_tensors="pt", |
| ) |
| input_ids = enc["input_ids"].to(self.device) |
| attn_mask = enc["attention_mask"].to(self.device) |
|
|
| meta_df = pd.concat( |
| [_meta_dict_to_df(r["product_meta"]) for r in chunk], |
| ignore_index=True, |
| ) |
| meta_vec = torch.from_numpy(self.meta_encoder.transform(meta_df)).float().to(self.device) |
|
|
| with torch.no_grad(): |
| out = self.model(input_ids, attn_mask, meta_vec) |
| preds = out["logits"].argmax(dim=-1).cpu().numpy() |
| meta_attn = out["meta_attn_weights"].cpu().numpy() |
|
|
| for j in range(len(chunk)): |
| results.append({ |
| "aspects": format_aspect_summary(preds[j]), |
| "meta_attention": {name: float(w) |
| for name, w in zip(META_TOKEN_NAMES, meta_attn[j])}, |
| }) |
| return results |
|
|