Lavender825's picture
Deploy 3W 0624 BERT metadata fusion model
38136ed
Raw
History Blame Contribute Delete
10.9 kB
"""User-facing inference for the Proposed model."""
from typing import Dict, List, Optional
import numpy as np
import pandas as pd
import torch
from . import config as cfg
from .evaluator import load_meta_acsa, format_aspect_summary
_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])
def _attention_names(out: Dict, attn_array) -> List[str]:
names = out.get("meta_token_names")
if names is not None:
return list(names)
arr = np.asarray(attn_array)
width = int(arr.shape[-1]) if arr.ndim else int(arr.size)
if width == 3:
return ["features", "categories", "numeric"]
return [f"meta_chunk_{i + 1}" for i in range(width)]
def _attention_dict(names: List[str], weights) -> Dict[str, float]:
arr = np.asarray(weights, dtype=np.float64).reshape(-1)
return {name: float(w) for name, w in zip(names, arr)}
def _top_attention_item(attn: Dict[str, float]) -> Dict[str, float]:
if not attn:
return {"source": "", "weight": 0.0}
source = max(attn, key=attn.get)
return {"source": source, "weight": float(attn[source])}
def _meta_summary(meta: Dict) -> Dict[str, str]:
features = str(meta.get("features_text") or meta.get("features") or "")[:420]
categories = str(meta.get("categories_text") or meta.get("category") or "")[:220]
numeric = []
for key in ("price", "average_rating", "rating_number"):
val = meta.get(key)
if val is not None:
numeric.append(f"{key}={val}")
return {
"features": features,
"categories": categories,
"numeric": ", ".join(numeric) if numeric else "not available",
}
_ASPECT_EVIDENCE_KEYWORDS = {
"SIZE": {"size", "fit", "fits", "fitting", "small", "large", "big", "tight", "loose", "xl", "medium", "waist", "length"},
"MATERIAL": {"material", "fabric", "cotton", "polyester", "soft", "scratchy", "thin", "thick", "stretch", "leather", "wool"},
"QUALITY": {"quality", "stitch", "stitching", "seam", "wash", "washed", "durable", "cheap", "broke", "tear", "torn"},
"APPEARANCE": {"look", "looks", "color", "colour", "photo", "picture", "beautiful", "cute", "print", "design"},
"STYLE": {"style", "stylish", "flattering", "casual", "formal", "dress", "shirt", "fashion", "compliments"},
"VALUE": {"price", "worth", "value", "money", "cheap", "expensive", "discount", "penny", "cost"},
}
_SENTIMENT_EVIDENCE_KEYWORDS = {
"good", "great", "love", "loved", "perfect", "nice", "excellent", "comfortable", "soft",
"bad", "poor", "cheap", "terrible", "awful", "small", "large", "tight", "loose", "thin",
"worth", "disappointed", "return", "returned", "recommend", "flattering", "beautiful",
}
_STOPWORDS = {
"a", "an", "the", "and", "or", "but", "if", "then", "than", "so", "as", "at", "by", "for", "from",
"in", "into", "of", "on", "to", "with", "without", "is", "are", "was", "were", "be", "been", "being",
"it", "its", "this", "that", "these", "those", "i", "me", "my", "we", "our", "you", "your", "he", "she", "they",
"them", "his", "her", "their", "very", "really", "just", "also", "too", "would", "could", "should", "can",
"will", "did", "do", "does", "have", "has", "had", "there", "here", "about", "after", "before",
}
def _clean_term(term: str) -> str:
return str(term).lower().strip(".,!?;:'\"()[]{}<>/\\|`~@#$%^&*_+=")
def _is_informative_term(term: str) -> bool:
clean = _clean_term(term)
if len(clean) < 2 or clean in _STOPWORDS:
return False
return any(ch.isalpha() for ch in clean)
def _text_evidence_by_aspect(review_text: str, top_k: int = 8) -> Dict[str, List[str]]:
raw_terms = [_clean_term(t) for t in str(review_text).split()]
result = {}
for aspect, aspect_terms in _ASPECT_EVIDENCE_KEYWORDS.items():
hits = []
for term in raw_terms:
if not _is_informative_term(term):
continue
if term in aspect_terms or term in _SENTIMENT_EVIDENCE_KEYWORDS:
if term not in hits:
hits.append(term)
if len(hits) >= top_k:
break
result[aspect] = hits
return result
def _format_confidence(out: Dict, row_idx: int = 0) -> Dict[str, Dict[str, float]]:
probs = torch.softmax(out["logits"][row_idx], dim=-1).detach().cpu().numpy()
result = {}
for i, aspect in enumerate(cfg.ASPECTS):
cls = int(np.argmax(probs[i]))
result[aspect] = {
"label": cfg.LABEL_NAMES[cls],
"confidence": float(probs[i, cls]),
"class_probs": {
cfg.LABEL_NAMES[j]: float(probs[i, j])
for j in range(len(cfg.LABEL_NAMES))
},
}
return result
def _attention_insights(attn_payload: Dict) -> Dict:
insights = {}
if "meta_attention" in attn_payload:
insights["top_meta_source"] = _top_attention_item(attn_payload["meta_attention"])
if "meta_attention_by_aspect" in attn_payload:
insights["top_meta_source_by_aspect"] = {
aspect: _top_attention_item(weights)
for aspect, weights in attn_payload["meta_attention_by_aspect"].items()
}
return insights
def _format_attention(out: Dict) -> Dict:
"""Format legacy 1D attention or new aspect-specific 2D attention."""
if "meta_attn_weights" not in out or out["meta_attn_weights"] is None:
return {}
aspect_attn = out["meta_attn_weights"].detach().cpu().numpy()[0]
names = _attention_names(out, aspect_attn)
if aspect_attn.ndim == 1:
return {"meta_attention": _attention_dict(names, aspect_attn)}
result = {
"meta_attention_by_aspect": {
aspect: _attention_dict(names, aspect_attn[i])
for i, aspect in enumerate(cfg.ASPECTS)
}
}
if "global_meta_attn_weights" in out and out["global_meta_attn_weights"] is not None:
global_attn = out["global_meta_attn_weights"].detach().cpu().numpy()[0]
result["meta_attention"] = _attention_dict(names, global_attn)
else:
result["meta_attention"] = _attention_dict(names, aspect_attn.mean(axis=0))
return result
def _slice_model_out(out: Dict, start: int, end: int) -> Dict:
sliced = {}
for k, v in out.items():
sliced[k] = v[start:end] if torch.is_tensor(v) else v
return sliced
class AspectPredictor:
"""Load the Proposed model and expose one-shot and batch predictions."""
def __init__(self, checkpoint_dir=None, device=None):
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()
result = {
"aspects": format_aspect_summary(preds),
"aspect_details": _format_confidence(out, 0),
"metadata_summary": _meta_summary(product_meta),
}
if "overall_logits" in out and out["overall_logits"] is not None:
overall_probs = torch.softmax(out["overall_logits"][0], dim=-1).detach().cpu().numpy()
overall_cls = int(np.argmax(overall_probs))
result["overall"] = {
"label": cfg.OVERALL_LABEL_NAMES[overall_cls],
"confidence": float(overall_probs[overall_cls]),
"class_probs": {
cfg.OVERALL_LABEL_NAMES[j]: float(overall_probs[j])
for j in range(len(cfg.OVERALL_LABEL_NAMES))
},
}
if return_attention:
attn_payload = _format_attention(out)
result.update(attn_payload)
result.update(_attention_insights(attn_payload))
return result
def predict_batch(self, reviews: List[Dict], batch_size: int = 16) -> List[Dict]:
"""Each 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()
for j in range(len(chunk)):
item = {
"aspects": format_aspect_summary(preds[j]),
"aspect_details": _format_confidence(out, j),
"metadata_summary": _meta_summary(chunk[j]["product_meta"]),
}
if "overall_logits" in out and out["overall_logits"] is not None:
overall_probs = torch.softmax(out["overall_logits"][j], dim=-1).detach().cpu().numpy()
overall_cls = int(np.argmax(overall_probs))
item["overall"] = {
"label": cfg.OVERALL_LABEL_NAMES[overall_cls],
"confidence": float(overall_probs[overall_cls]),
"class_probs": {
cfg.OVERALL_LABEL_NAMES[k]: float(overall_probs[k])
for k in range(len(cfg.OVERALL_LABEL_NAMES))
},
}
attn_payload = _format_attention(_slice_model_out(out, j, j + 1))
item.update(attn_payload)
item.update(_attention_insights(attn_payload))
results.append(item)
return results