Spaces:
Sleeping
Sleeping
| """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"} | |
| 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() | |