File size: 6,344 Bytes
f059534
da0dba0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
08b628b
da0dba0
 
 
 
 
 
 
 
 
08b628b
da0dba0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f059534
da0dba0
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import streamlit as st
import torch
import numpy as np
from transformers import AutoTokenizer, AutoModelForSequenceClassification

# ── Config ────────────────────────────────────────────────────────────────────
HF_MODEL_ID = "ishaan1402/cbt-thought-pattern-classifier"
THRESHOLD   = 0.15
MAX_LENGTH  = 256

PATTERN_CLASSES = [
    "Catastrophizing",
    "Discounting the positive",
    "Labeling and mislabeling",
    "Mental filtering",
    "Jumping to conclusions: mind reading",
    "Jumping to conclusions: Fortune-telling",
    "Overgeneralization",
    "Personalization",
    "Black-and-white or polarized thinking / All or nothing thinking",
    "Should statements",
    "None"
]

PATTERN_DESCRIPTIONS = {
    "Catastrophizing":              "Giving greater weight to the worst possible outcome.",
    "Discounting the positive":     "Rejecting positive experiences by insisting they don't count.",
    "Labeling and mislabeling":     "Attributing actions to character rather than situation.",
    "Mental filtering":             "Dwelling only on the negative details of a situation.",
    "Jumping to conclusions: mind reading": "Inferring negative thoughts from someone's behaviour.",
    "Jumping to conclusions: Fortune-telling": "Predicting negative outcomes of events.",
    "Overgeneralization":           "Making faulty generalisations from insufficient evidence.",
    "Personalization":              "Assigning disproportionate personal blame to oneself.",
    "Black-and-white or polarized thinking / All or nothing thinking":
                                    "Viewing things as either all good or all bad with no middle ground.",
    "Should statements":            "Demanding particular behaviours regardless of realistic circumstances.",
    "None":                         "No unhelpful thought pattern detected.",
}

# ── Model loading (cached so it only runs once per session) ───────────────────
@st.cache_resource
def load_model():
    tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_ID)
    model     = AutoModelForSequenceClassification.from_pretrained(HF_MODEL_ID)
    model.eval()
    return tokenizer, model

# ── Inference ─────────────────────────────────────────────────────────────────
def classify(thought: str, persona: str, tokenizer, model) -> dict:
    input_text = f"Persona: {persona} | Thought: {thought}" if persona.strip() \
                 else f"Persona:  | Thought: {thought}"

    inputs = tokenizer(
        input_text,
        return_tensors="pt",
        max_length=MAX_LENGTH,
        truncation=True,
        padding="max_length"
    )

    with torch.no_grad():
        logits = model(**inputs).logits

    probs      = torch.softmax(logits, dim=-1).squeeze().numpy()
    top_idx    = int(np.argmax(probs))
    top_label  = PATTERN_CLASSES[top_idx]
    confidence = float(probs[top_idx])

    is_unhelpful = (top_label != "None") and (confidence >= THRESHOLD)

    return {
        "is_unhelpful":      is_unhelpful,
        "predicted_pattern": top_label if is_unhelpful else "None",
        "confidence":        confidence,
        "distribution":      {PATTERN_CLASSES[i]: float(p) for i, p in enumerate(probs)},
    }

# ── UI ────────────────────────────────────────────────────────────────────────
st.set_page_config(page_title="CBT Thought Classifier", page_icon="🧠", layout="centered")

st.title("CBT Thought Pattern Classifier")
st.caption(
    "Enter a thought below. The model will identify which unhelpful cognitive pattern "
    "it exhibits, if any, based on Cognitive Behavioral Therapy (CBT) research."
)

st.divider()

persona = st.text_input(
    "Persona (optional)",
    placeholder="e.g. I am a college student. I love playing the guitar.",
    help="Adding context about the person improves classification accuracy."
)

thought = st.text_area(
    "Thought",
    placeholder="e.g. I failed this test, I'm going to fail my entire degree.",
    height=120
)

classify_btn = st.button("Classify", type="primary", use_container_width=True)

if classify_btn:
    if not thought.strip():
        st.warning("Please enter a thought to classify.")
    else:
        with st.spinner("Loading model...") if "tokenizer" not in st.session_state else st.spinner("Classifying..."):
            tokenizer, model = load_model()
            result = classify(thought, persona, tokenizer, model)

        st.divider()

        pattern     = result["predicted_pattern"]
        confidence  = result["confidence"]
        unhelpful   = result["is_unhelpful"]

        # ── Result banner ──
        if unhelpful:
            st.error(f"**Unhelpful pattern detected:** {pattern}")
        else:
            st.success("**No unhelpful pattern detected** β€” this thought looks okay.")

        # ── Pattern description ──
        if pattern in PATTERN_DESCRIPTIONS:
            st.info(f"πŸ“– **{pattern}:** {PATTERN_DESCRIPTIONS[pattern]}")

        # ── Confidence ──
        st.metric("Model confidence", f"{confidence:.1%}")

        # ── Full distribution ──
        with st.expander("See full probability distribution"):
            dist = result["distribution"]
            sorted_dist = sorted(dist.items(), key=lambda x: x[1], reverse=True)
            for label, prob in sorted_dist:
                bar_pct = int(prob * 100)
                # Highlight the predicted class
                label_display = f"**{label}**" if label == pattern else label
                col1, col2 = st.columns([3, 1])
                with col1:
                    st.markdown(label_display)
                    st.progress(bar_pct)
                with col2:
                    st.markdown(f"`{prob:.3f}`")

st.divider()
st.caption("Model trained on the [PATTERNREFRAME dataset](https://github.com/facebookresearch/ParlAI/tree/main/projects/reframe_thoughts) Β· Built with RoBERTa-large")