AspectBERT / app.py
itismeTithi's picture
Fix model loading - use trained AspectBERT
00f1e25
Raw
History Blame Contribute Delete
7.01 kB
"""AspectBERT Streamlit app — aspect-based sentiment analysis for product reviews.
Run locally:
streamlit run app.py
Configure the model source via the HF_MODEL_NAME environment variable
(a HuggingFace Hub repo id). If unset, falls back to base distilbert-base-uncased
weights with an untrained classification head (for UI smoke-testing only).
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
import plotly.graph_objects as go
import streamlit as st
from constants import ASPECTS, ID2LABEL # noqa: E402
from inference import explain_with_lime, load_model, predict_all_aspects # noqa: E402
st.set_page_config(page_title="AspectBERT - Aspect-Based Sentiment Analysis", layout="wide")
EXAMPLE_REVIEWS = [
"The battery life is incredible, lasts two days on a single charge! However, "
"the camera quality is disappointing in low light, and the price feels a bit "
"too high for what you get.",
"Beautiful sleek design and the display is gorgeous with vibrant colors. The "
"software has some annoying bugs and crashes occasionally, but performance "
"overall is snappy.",
"Customer service was unhelpful when I tried to return a defective unit. The "
"build quality feels cheap and the screen scratches easily.",
"Great value for money! Performance is fast and smooth for everyday tasks, "
"the design feels premium, and battery easily lasts all day.",
]
LABEL_COLORS = {"positive": "green", "neutral": "orange", "negative": "red"}
@st.cache_resource(show_spinner="Loading AspectBERT model...")
def get_model():
model_name = os.environ.get("HF_MODEL_NAME", "itismeTithi/AspectBERT")
return load_model(model_name)
def render_radar_chart(results):
categories = list(results.keys())
values = [results[a]["scores"]["positive"] for a in categories]
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=values + values[:1],
theta=[c.replace("_", " ").title() for c in categories] +
[categories[0].replace("_", " ").title()],
fill="toself",
name="Positive score",
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
showlegend=False,
margin=dict(l=40, r=40, t=20, b=20),
)
return fig
def vader_label(compound):
if compound >= 0.05:
return "positive"
if compound <= -0.05:
return "negative"
return "neutral"
def main():
st.title("AspectBERT: Aspect-Based Sentiment Analysis")
st.caption(
"Fine-tuned DistilBERT that detects sentiment (positive / neutral / "
"negative) for 8 product aspects from a single review."
)
with st.sidebar:
st.header("Options")
use_vader = st.checkbox("Compare with VADER baseline", value=False)
use_lime = st.checkbox("Show LIME word importance", value=False)
selected_aspects = st.multiselect("Aspects to analyze", ASPECTS, default=ASPECTS)
st.divider()
model_source = os.environ.get("HF_MODEL_NAME", "distilbert-base-uncased (untrained head)")
st.caption(f"Model: `{model_source}`")
st.subheader("Try an example review")
cols = st.columns(len(EXAMPLE_REVIEWS))
for i, col in enumerate(cols):
if col.button(f"Example {i + 1}", key=f"example_{i}", use_container_width=True):
st.session_state["review_text"] = EXAMPLE_REVIEWS[i]
review_text = st.text_area(
"Paste a product review",
value=st.session_state.get("review_text", ""),
height=150,
key="review_text",
)
analyze_clicked = st.button("Analyze", type="primary")
if not selected_aspects:
st.warning("Select at least one aspect in the sidebar.")
return
if analyze_clicked and review_text.strip():
model, tokenizer, device = get_model()
with st.spinner("Analyzing aspects..."):
results = predict_all_aspects(model, tokenizer, device, review_text,
aspects=selected_aspects)
st.subheader("Sentiment per aspect")
for aspect, res in results.items():
label = res["label"]
st.markdown(
f"**{aspect.replace('_', ' ').title()}** — "
f":{LABEL_COLORS[label]}[{label.upper()}]"
)
cols = st.columns(3)
for i, lbl in enumerate(["negative", "neutral", "positive"]):
score = res["scores"][lbl]
cols[i].progress(score, text=f"{lbl}: {score:.2f}")
st.subheader("Radar chart: positive sentiment across aspects")
st.plotly_chart(render_radar_chart(results), use_container_width=True)
if use_vader:
st.subheader("VADER baseline comparison")
try:
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
analyzer = SentimentIntensityAnalyzer()
compound = analyzer.polarity_scores(review_text)["compound"]
overall = vader_label(compound)
st.write(
f"VADER overall sentiment (whole review, not aspect-aware): "
f"**:{LABEL_COLORS[overall]}[{overall.upper()}]** "
f"(compound score = {compound:.3f})"
)
st.caption(
"VADER is a lexicon-based, rule-based sentiment scorer that does "
"not support aspect-level sentiment — it produces one score per "
"review. AspectBERT predicts sentiment independently per aspect."
)
except ImportError:
st.warning("vaderSentiment is not installed. Install with "
"`pip install vaderSentiment` to enable this feature.")
if use_lime:
st.subheader("LIME word importance")
try:
lime_aspect = st.selectbox(
"Select aspect for LIME explanation", selected_aspects, key="lime_aspect"
)
with st.spinner("Computing LIME explanation (this can take a moment)..."):
exp = explain_with_lime(model, tokenizer, device, review_text, lime_aspect)
pred_label = results[lime_aspect]["label"]
label_idx = [i for i, name in ID2LABEL.items() if name == pred_label][0]
html = exp.as_html(labels=[label_idx])
st.components.v1.html(html, height=400, scrolling=True)
except ImportError:
st.warning("lime is not installed. Install with `pip install lime` "
"to enable this feature.")
except Exception as exc:
st.error(f"LIME explanation failed: {exc}")
elif not review_text.strip():
st.info("Paste a review above and click Analyze, or try an example.")
if __name__ == "__main__":
main()