Okba-Sa-20 commited on
Commit
fbcf632
·
verified ·
1 Parent(s): 77bba75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +601 -287
app.py CHANGED
@@ -13,357 +13,671 @@ from nltk.corpus import stopwords
13
  from tensorflow.keras.layers import Layer
14
  from tensorflow.keras import backend as K
15
  import time
 
 
16
 
17
- # Download NLTK resources
 
18
  nltk.download('punkt', quiet=True)
19
  nltk.download('stopwords', quiet=True)
20
 
21
- # --- Custom Attention Layer ---
22
- @tf.keras.utils.register_keras_serializable(package="CustomLayers")
23
- class SimpleAttention(Layer):
24
- def __init__(self, **kwargs):
25
- super(SimpleAttention, self).__init__(**kwargs)
 
 
26
 
27
  def build(self, input_shape):
28
  self.W = self.add_weight(
29
  name="attention_weight",
30
- shape=(input_shape[-1], 1),
31
  initializer="glorot_uniform",
32
  trainable=True
33
  )
34
- super(SimpleAttention, self).build(input_shape)
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def call(self, inputs):
37
- e = K.tanh(K.dot(inputs, self.W))
38
- e = K.squeeze(e, axis=-1)
39
- alpha = K.softmax(e, axis=1)
40
- alpha = K.expand_dims(alpha, axis=-1)
41
- context = inputs * alpha
42
- return K.sum(context, axis=1)
 
 
 
43
 
44
  def compute_output_shape(self, input_shape):
 
 
45
  return (input_shape[0], input_shape[2])
46
 
47
- # --- Enhanced Text Preprocessing ---
48
- def preprocess_for_lstm(text, remove_stopwords=False):
49
- if not isinstance(text, str) or not text.strip():
50
- return ""
51
-
52
- try:
53
- # Handle neutral/negation phrases
54
- text = re.sub(r'\b(not bad|not great|okay|so-so|meh)\b', ' neutral_term ', text, flags=re.IGNORECASE)
55
-
56
- # Improved negation handling
57
- negation_patterns = [
58
- r'\b(not|no|never|without)\b [\w]+',
59
- r'\b(less than|barely|hardly|scarcely)\b [\w]+',
60
- r'\b(avoid|skip|doubt|problem|issue)\b'
61
- ]
62
- for pattern in negation_patterns:
63
- text = re.sub(pattern, ' negation_term ', text, flags=re.IGNORECASE)
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- # Emoji handling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  text = emoji.demojize(text, delimiters=("", ""))
67
 
68
- # Contractions
69
  text = contractions.fix(text)
70
 
71
- # URL/mention replacement
72
  text = re.sub(r'https?://\S+|www\.\S+', ' URL ', text)
73
  text = re.sub(r'@\S+', ' USER ', text)
74
- text = re.sub(r'\s+', ' ', text).strip().lower()
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  # Emoticon preservation
77
  emoticons = re.findall(r'(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)
78
  text = re.sub(r'[^\w\s!?.,]', ' ', text)
79
 
80
- # Tokenization with special handling
81
  tokens = word_tokenize(text)
82
- processed_tokens = []
 
83
  for token in tokens:
84
  if not token.strip():
85
  continue
86
 
87
- # Boost neutral signal
88
- if token in {'neutral_term', 'negation_term'}:
89
- processed_tokens.append(token)
 
 
90
  continue
91
 
92
- # Handle intensifiers/diminishers
93
- if token in {'very', 'extremely', 'absolutely'}:
94
- processed_tokens.append('intensifier')
95
- elif token in {'slightly', 'somewhat', 'barely'}:
96
- processed_tokens.append('diminisher')
97
- else:
98
- processed_tokens.append(token)
99
-
100
- processed_tokens.extend(emoticons)
101
- return ' '.join(processed_tokens)
102
-
103
- except Exception:
104
- return text.lower()
105
-
106
- # --- Load model resources ---
107
- @st.cache_resource
108
- def load_model():
109
- MODEL_DIR = "model_files/models"
110
- model_path = f"{MODEL_DIR}/simplified_lstm_20250622-195716_best.keras"
111
- tokenizer_path = f"{MODEL_DIR}/simplified_lstm_20250622-195716_tokenizer.pickle"
112
- label_mapping_path = f"{MODEL_DIR}/simplified_lstm_20250622-195716_label_mapping.pickle"
113
-
114
- # Verify files exist
115
- for path in [model_path, tokenizer_path, label_mapping_path]:
116
- if not os.path.exists(path):
117
- st.error(f"Critical error: File not found - {path}")
118
- st.stop()
119
 
120
- # Load model with custom layers
121
- try:
122
- model = tf.keras.models.load_model(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  model_path,
124
  custom_objects={
125
- 'SimpleAttention': SimpleAttention,
126
  'SpatialDropout1D': tf.keras.layers.SpatialDropout1D
127
  },
128
  compile=False
129
  )
130
- except Exception as e:
131
- st.error(f"Model loading failed: {str(e)}")
132
- st.stop()
133
-
134
- # Load tokenizer and label mapping
135
- with open(tokenizer_path, "rb") as handle:
136
- tokenizer = pickle.load(handle)
137
-
138
- with open(label_mapping_path, "rb") as handle:
139
- label_mapping = pickle.load(handle)
140
-
141
- return model, tokenizer, label_mapping
142
-
143
- # --- Initialize resources ---
144
- try:
145
- MAX_LEN = 50
146
- model, tokenizer, label_mapping = load_model()
147
- index_to_label = {v: k for k, v in label_mapping.items()}
148
- except Exception as e:
149
- st.error(f"Initialization failed: {str(e)}")
150
- st.stop()
151
-
152
- # --- Enhanced Prediction Pipeline ---
153
- def predict_sentiment(text):
154
- start_time = time.time()
155
- processed_text = preprocess_for_lstm(text)
156
-
157
- # Handle empty sequences
158
- if not processed_text.strip():
159
- return "0.0", 0.0 # Default to neutral
160
-
161
- # Tokenize with fallback
162
- seq = tokenizer.texts_to_sequences([processed_text])
163
- if not seq or not any(seq[0]):
164
- seq = [[tokenizer.word_index.get('neutral_term', 1)]]
165
-
166
- padded = tf.keras.preprocessing.sequence.pad_sequences(
167
- seq,
168
- maxlen=MAX_LEN,
169
- padding='post',
170
- truncating='post',
171
- value=0
172
- )
173
-
174
- # Predict with confidence threshold
175
- try:
176
- prediction = model.predict(padded, verbose=0)[0]
177
- label_idx = np.argmax(prediction)
178
- confidence = prediction[label_idx]
179
-
180
- # Apply confidence-based adjustment
181
- if confidence < 0.65: # Uncertain predictions
182
- # Check for neutral indicators
183
- if 'neutral_term' in processed_text or 'diminisher' in processed_text:
184
- label_idx = list(label_mapping.values()).index(1) # Force neutral
185
 
186
- proc_time = time.time() - start_time
 
 
 
 
 
 
 
187
 
188
- # Store debug info
189
- st.session_state.last_prediction = {
190
- "raw_text": text,
191
- "processed_text": processed_text,
192
- "probabilities": prediction.round(4).tolist(),
193
- "predicted_label": index_to_label[label_idx],
194
- "confidence": float(confidence),
195
- "processing_time": proc_time
196
- }
197
 
198
- return index_to_label[label_idx], confidence
 
199
 
200
- except Exception as e:
201
- return "0.0", 0.0
202
-
203
- # --- Streamlit App UI ---
204
- st.set_page_config(page_title="High-Accuracy Sentiment Analysis", layout="wide")
205
- st.title("💬 Advanced Sentiment Analysis")
206
-
207
- # Initialize session state
208
- if 'last_prediction' not in st.session_state:
209
- st.session_state.last_prediction = None
210
-
211
- # Debug info
212
- with st.expander("🔧 Debug Information", expanded=False):
213
- st.write(f"**Input Shape:** {model.input_shape}")
214
- st.write(f"**Classes:** {label_mapping}")
215
-
216
- if st.session_state.last_prediction:
217
- st.json(st.session_state.last_prediction)
218
-
219
- # Validation tests with explanations
220
- test_cases = [
221
- ("I love this product! It's absolutely amazing 😍", "1.0", "Clear positive"),
222
- ("Terrible experience, worst purchase ever", "-1.0", "Clear negative"),
223
- ("The item is okay, nothing special", "0.0", "Neutral - baseline"),
224
- ("Not bad but could be better", "0.0", "Neutral - nuanced"),
225
- ("Avoid this company at all costs", "-1.0", "Negative - strong intent"),
226
- ("It's barely acceptable", "0.0", "Neutral - diminisher"),
227
- ("Service was not great", "0.0", "Neutral - negation"),
228
- ("Best decision I've ever made!", "1.0", "Positive - intensifier")
229
- ]
230
-
231
- with st.expander("🧪 Validation Tests", expanded=True):
232
- if st.button("Run Validation Suite", type="primary"):
233
- results = []
234
- for text, expected, desc in test_cases:
235
- label, conf = predict_sentiment(text)
236
- match = "✓" if label == expected else "✗"
237
- results.append({
238
- "Text": text,
239
- "Description": desc,
240
- "Expected": expected,
241
- "Predicted": label,
242
- "Confidence": f"{conf:.1%}",
243
- "Match": match
244
- })
245
 
246
- df_results = pd.DataFrame(results)
247
- st.dataframe(df_results.style.apply(
248
- lambda row: ['background-color: #ffcccc' if row.Match == "✗" else '' for _ in row],
249
- axis=1
250
- ))
251
-
252
- # Single text analysis
253
- with st.form("analysis_form"):
254
- st.subheader("🔍 Analyze Text")
255
- user_input = st.text_area("Enter text:", height=150,
256
- value="The service was acceptable but not outstanding")
257
- submitted = st.form_submit_button("Analyze Sentiment", type="primary")
258
-
259
- if submitted and user_input.strip():
260
- with st.spinner("Analyzing..."):
261
- label, confidence = predict_sentiment(user_input)
262
 
263
- # Display results with explanation
264
- sentiment_map = {
265
- "1.0": ("Positive 😊", "green"),
266
- "0.0": ("Neutral 😐", "blue"),
267
- "-1.0": ("Negative 😠", "red")
268
- }
269
 
270
- display_text, color = sentiment_map.get(label, ("Unknown", "gray"))
271
 
272
- st.markdown(f"""
273
- <div style="border-left: 5px solid {color}; padding: 10px; background-color: #f8f9fa; border-radius: 5px;">
274
- <h3 style="color: {color};">{display_text}</h3>
275
- <p>Confidence: <b>{confidence:.1%}</b></p>
276
- </div>
277
- """, unsafe_allow_html=True)
278
 
279
- # Show processing insights
280
- if st.session_state.last_prediction:
281
- with st.expander("Analysis Details"):
282
- st.write(f"**Processed Text:** `{st.session_state.last_prediction['processed_text']}`")
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
- # Sentiment probability visualization
285
- prob_data = {
286
- "Negative": st.session_state.last_prediction['probabilities'][0],
287
- "Neutral": st.session_state.last_prediction['probabilities'][1],
288
- "Positive": st.session_state.last_prediction['probabilities'][2]
289
- }
290
- st.bar_chart(prob_data)
291
-
292
- # --- CSV Batch Processing Section ---
293
- st.subheader("📊 Batch Analysis from CSV")
294
- st.write("Upload a CSV file containing text data to analyze multiple entries at once")
295
-
296
- uploaded_file = st.file_uploader("Choose a CSV file", type=["csv"],
297
- help="File must contain a 'text' column with content to analyze")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
- if uploaded_file is not None:
300
- try:
301
- # Read CSV file
302
- df = pd.read_csv(uploaded_file)
 
 
 
 
 
 
 
 
 
 
303
 
304
- # Verify required column exists
305
- if 'text' not in df.columns:
306
- st.error("❌ CSV file must contain a column named 'text'")
 
 
307
  st.stop()
308
 
309
- st.success(f"✅ Successfully loaded {len(df)} records")
310
- st.dataframe(df.head(3))
 
 
 
 
 
 
 
 
 
 
311
 
312
- # Process in batches
313
- if st.button("Analyze All Texts", type="primary", key="batch_analyze"):
314
- results = []
315
- progress_bar = st.progress(0)
316
- status_text = st.empty()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- # Process each row
319
- for i, row in enumerate(df.itertuples()):
320
- text = str(row.text)
321
- label, confidence = predict_sentiment(text)
322
 
323
- # Map to human-readable label
324
- sentiment_label = {
325
- "1.0": "Positive",
326
- "0.0": "Neutral",
327
- "-1.0": "Negative"
328
- }.get(label, "Unknown")
 
 
 
 
 
 
 
 
 
 
329
 
330
- results.append({
331
- "Original Text": text,
332
- "Predicted Sentiment": sentiment_label,
333
- "Confidence": f"{confidence:.1%}",
334
- "Raw Label": label
335
- })
336
 
337
- # Update progress
338
- progress = (i + 1) / len(df)
339
- progress_bar.progress(progress)
340
- status_text.text(f"Processed {i+1}/{len(df)} records ({progress:.0%})")
341
-
342
- # Create results dataframe
343
- results_df = pd.DataFrame(results)
344
-
345
- # Show results
346
- st.subheader("Analysis Results")
347
- st.dataframe(results_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
- # Download results
350
- csv = results_df.to_csv(index=False).encode('utf-8')
351
- st.download_button(
352
- label="Download Results as CSV",
353
- data=csv,
354
- file_name="sentiment_analysis_results.csv",
355
- mime="text/csv",
356
- type="primary"
357
  )
358
 
359
- # Sentiment distribution visualization
360
- st.subheader("Sentiment Distribution")
361
- sentiment_counts = results_df['Predicted Sentiment'].value_counts()
362
- st.bar_chart(sentiment_counts)
 
363
 
364
- except Exception as e:
365
- st.error(f"Error processing CSV file: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
- # Footer
368
- st.markdown("---")
369
- st.caption("Enhanced Sentiment Analysis | Neural Engine v2.1 | Confidence Threshold: 65%")
 
 
13
  from tensorflow.keras.layers import Layer
14
  from tensorflow.keras import backend as K
15
  import time
16
+ import matplotlib.pyplot as plt
17
+ import seaborn as sns
18
 
19
+ # Configure environment
20
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # Suppress TensorFlow logs
21
  nltk.download('punkt', quiet=True)
22
  nltk.download('stopwords', quiet=True)
23
 
24
+ # --- Professional Custom Attention Layer ---
25
+ @tf.keras.utils.register_keras_serializable(package="SentimentAnalysis")
26
+ class EnhancedAttention(Layer):
27
+ """Advanced attention mechanism with context preservation"""
28
+ def __init__(self, return_attention=False, **kwargs):
29
+ self.return_attention = return_attention
30
+ super(EnhancedAttention, self).__init__(**kwargs)
31
 
32
  def build(self, input_shape):
33
  self.W = self.add_weight(
34
  name="attention_weight",
35
+ shape=(input_shape[-1], input_shape[-1]),
36
  initializer="glorot_uniform",
37
  trainable=True
38
  )
39
+ self.b = self.add_weight(
40
+ name="attention_bias",
41
+ shape=(input_shape[-1],),
42
+ initializer="zeros",
43
+ trainable=True
44
+ )
45
+ self.u = self.add_weight(
46
+ name="context_vector",
47
+ shape=(input_shape[-1],),
48
+ initializer="glorot_uniform",
49
+ trainable=True
50
+ )
51
+ super(EnhancedAttention, self).build(input_shape)
52
 
53
  def call(self, inputs):
54
+ # Attention mechanism with learned context
55
+ v = K.tanh(K.dot(inputs, self.W) + self.b
56
+ vu = K.dot(v, K.expand_dims(self.u))
57
+ alphas = K.softmax(vu, axis=1)
58
+ output = K.sum(inputs * alphas, axis=1)
59
+
60
+ if self.return_attention:
61
+ return [output, alphas]
62
+ return output
63
 
64
  def compute_output_shape(self, input_shape):
65
+ if self.return_attention:
66
+ return [(input_shape[0], input_shape[2]), (input_shape[0], input_shape[1])]
67
  return (input_shape[0], input_shape[2])
68
 
69
+ # --- Professional Text Preprocessing ---
70
+ class TextPreprocessor:
71
+ """Advanced linguistic processor with domain-specific rules"""
72
+ def __init__(self):
73
+ self.negation_phrases = {
74
+ 'not', 'no', 'never', 'without', "don't", "isn't", "wasn't", "shouldn't",
75
+ "couldn't", "wouldn't", "aren't", "weren't", "doesn't", "didn't", "won't",
76
+ "can't", "cannot", "nobody", "none", "nothing", "nowhere", "neither", "nor"
77
+ }
78
+ self.intensifiers = {
79
+ 'very', 'extremely', 'absolutely', 'completely', 'totally', 'utterly',
80
+ 'highly', 'exceptionally', 'remarkably', 'incredibly', 'amazingly'
81
+ }
82
+ self.diminishers = {
83
+ 'slightly', 'somewhat', 'barely', 'hardly', 'scarcely', 'marginally',
84
+ 'partially', 'moderately', 'faintly', 'minimally', 'negligibly'
85
+ }
86
+ self.positive_indicators = {
87
+ 'love', 'excellent', 'awesome', 'fantastic', 'great', 'wonderful',
88
+ 'superb', 'outstanding', 'brilliant', 'perfect', 'favorite', 'best'
89
+ }
90
+ self.negative_indicators = {
91
+ 'hate', 'terrible', 'awful', 'horrible', 'worst', 'bad', 'poor',
92
+ 'disappointing', 'avoid', 'problem', 'issue', 'failure', 'disaster'
93
+ }
94
+ self.neutral_indicators = {
95
+ 'okay', 'average', 'adequate', 'sufficient', 'acceptable', 'moderate',
96
+ 'tolerable', 'passable', 'satisfactory', 'standard', 'neutral'
97
+ }
98
 
99
+ def preprocess(self, text):
100
+ """Full linguistic processing pipeline"""
101
+ if not isinstance(text, str) or not text.strip():
102
+ return ""
103
+
104
+ try:
105
+ # Phase 1: Structural normalization
106
+ text = self._normalize_structure(text)
107
+
108
+ # Phase 2: Semantic enrichment
109
+ text = self._enhance_semantics(text)
110
+
111
+ # Phase 3: Token-level processing
112
+ tokens = self._process_tokens(text)
113
+
114
+ return ' '.join(tokens)
115
+
116
+ except Exception as e:
117
+ return text.lower()
118
+
119
+ def _normalize_structure(self, text):
120
+ """Text structure normalization"""
121
+ # Emoji handling with sentiment preservation
122
  text = emoji.demojize(text, delimiters=("", ""))
123
 
124
+ # Advanced contraction handling
125
  text = contractions.fix(text)
126
 
127
+ # URL/mention standardization
128
  text = re.sub(r'https?://\S+|www\.\S+', ' URL ', text)
129
  text = re.sub(r'@\S+', ' USER ', text)
 
130
 
131
+ # Whitespace normalization
132
+ text = re.sub(r'\s+', ' ', text).strip()
133
+
134
+ # Case normalization
135
+ return text.lower()
136
+
137
+ def _enhance_semantics(self, text):
138
+ """Semantic enhancement for sentiment analysis"""
139
+ # Handle negations with context preservation
140
+ for phrase in self.negation_phrases:
141
+ text = re.sub(rf'\b{phrase}\b [\w]+', f' NEGATION_{phrase} ', text)
142
+
143
+ # Boost contrastive conjunctions
144
+ text = re.sub(r'\b(but|however|although|yet)\b', ' CONTRAST_TERM ', text)
145
+
146
+ # Enhance intensifiers/diminishers
147
+ for word in self.intensifiers:
148
+ text = re.sub(rf'\b{word}\b', f' INTENSIFIER_{word} ', text)
149
+ for word in self.diminishers:
150
+ text = re.sub(rf'\b{word}\b', f' DIMINISHER_{word} ', text)
151
+
152
+ # Sentiment indicator boosting
153
+ for word in self.positive_indicators:
154
+ text = re.sub(rf'\b{word}\b', f' POSITIVE_{word} ', text)
155
+ for word in self.negative_indicators:
156
+ text = re.sub(rf'\b{word}\b', f' NEGATIVE_{word} ', text)
157
+ for word in self.neutral_indicators:
158
+ text = re.sub(rf'\b{word}\b', f' NEUTRAL_{word} ', text)
159
+
160
+ # Handle numerical sentiment (ratings 1-5, 1-10)
161
+ text = re.sub(r'\b([1-9]|10)/10\b', lambda m: f' RATING_{m.group(1)}_10 ', text)
162
+ text = re.sub(r'\b([1-5])/5\b', lambda m: f' RATING_{m.group(1)}_5 ', text)
163
+
164
+ return text
165
+
166
+ def _process_tokens(self, text):
167
+ """Token-level processing pipeline"""
168
  # Emoticon preservation
169
  emoticons = re.findall(r'(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)
170
  text = re.sub(r'[^\w\s!?.,]', ' ', text)
171
 
172
+ # Tokenization
173
  tokens = word_tokenize(text)
174
+ processed = []
175
+
176
  for token in tokens:
177
  if not token.strip():
178
  continue
179
 
180
+ # Preserve enriched tokens
181
+ if token.startswith(('NEGATION_', 'CONTRAST_', 'INTENSIFIER_',
182
+ 'DIMINISHER_', 'POSITIVE_', 'NEGATIVE_', 'NEUTRAL_',
183
+ 'RATING_')):
184
+ processed.append(token)
185
  continue
186
 
187
+ processed.append(token)
188
+
189
+ return processed + emoticons
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ # --- Model Management System ---
192
+ class SentimentAnalyzer:
193
+ """Professional sentiment analysis system"""
194
+ def __init__(self, model_dir="model_files/models"):
195
+ self.model_dir = model_dir
196
+ self.model = None
197
+ self.tokenizer = None
198
+ self.label_mapping = None
199
+ self.preprocessor = TextPreprocessor()
200
+ self.max_len = 50
201
+ self.confidence_threshold = 0.6
202
+ self.load_resources()
203
+
204
+ def load_resources(self):
205
+ """Load model artifacts with enhanced validation"""
206
+ model_path = f"{self.model_dir}/simplified_lstm_20250622-195716_best.keras"
207
+ tokenizer_path = f"{self.model_dir}/simplified_lstm_20250622-195716_tokenizer.pickle"
208
+ label_path = f"{self.model_dir}/simplified_lstm_20250622-195716_label_mapping.pickle"
209
+
210
+ # Validate resources
211
+ missing = [p for p in [model_path, tokenizer_path, label_path] if not os.path.exists(p)]
212
+ if missing:
213
+ raise FileNotFoundError(f"Missing model resources: {', '.join(missing)}")
214
+
215
+ # Load model with custom components
216
+ self.model = tf.keras.models.load_model(
217
  model_path,
218
  custom_objects={
219
+ 'EnhancedAttention': EnhancedAttention,
220
  'SpatialDropout1D': tf.keras.layers.SpatialDropout1D
221
  },
222
  compile=False
223
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ # Load supporting artifacts
226
+ with open(tokenizer_path, "rb") as f:
227
+ self.tokenizer = pickle.load(f)
228
+ with open(label_path, "rb") as f:
229
+ self.label_mapping = pickle.load(f)
230
+
231
+ # Create reverse mapping
232
+ self.index_to_label = {v: k for k, v in self.label_mapping.items()}
233
 
234
+ def predict(self, text):
235
+ """Professional prediction pipeline with linguistic analysis"""
236
+ start_time = time.time()
 
 
 
 
 
 
237
 
238
+ # Preprocess with advanced linguistic rules
239
+ processed = self.preprocessor.preprocess(text)
240
 
241
+ # Fallback for empty content
242
+ if not processed.strip():
243
+ return "0.0", 0.0, processed, {}
244
+
245
+ # Tokenization with advanced fallback
246
+ seq = self.tokenizer.texts_to_sequences([processed])
247
+ if not seq or not any(seq[0]):
248
+ seq = [[self.tokenizer.word_index.get('neutral_term', 1)]]
249
+
250
+ # Padding to match training specs
251
+ padded = tf.keras.preprocessing.sequence.pad_sequences(
252
+ seq,
253
+ maxlen=self.max_len,
254
+ padding='post',
255
+ truncating='post',
256
+ value=0
257
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ # Prediction execution
260
+ try:
261
+ prediction = self.model.predict(padded, verbose=0)[0]
262
+ label_idx = np.argmax(prediction)
263
+ confidence = prediction[label_idx]
264
+ raw_label = self.index_to_label[label_idx]
265
+
266
+ # Advanced confidence adjustment
267
+ adjusted_label, confidence = self._apply_business_rules(
268
+ processed, raw_label, confidence, prediction
269
+ )
 
 
 
 
 
270
 
271
+ # Generate linguistic insights
272
+ insights = self._generate_insights(processed, prediction)
 
 
 
 
273
 
274
+ return adjusted_label, confidence, processed, insights
275
 
276
+ except Exception as e:
277
+ return "0.0", 0.0, processed, {"error": str(e)}
 
 
 
 
278
 
279
+ def _apply_business_rules(self, processed_text, raw_label, confidence, prediction):
280
+ """Apply domain-specific business rules to predictions"""
281
+ # Rule 1: Low confidence override
282
+ if confidence < self.confidence_threshold:
283
+ # Neutral content indicators
284
+ neutral_terms = {'neutral_term', 'contrast_term', 'diminisher'}
285
+ if any(term in processed_text for term in neutral_terms):
286
+ return "0.0", min(confidence + 0.15, 0.95) # Boost towards neutral
287
+
288
+ # Negation context handling
289
+ if 'negation_' in processed_text:
290
+ # Switch polarity for negated positives
291
+ if raw_label == "1.0":
292
+ return "-1.0", prediction[0] # Flip to negative
293
+ # Handle negated negatives
294
+ elif raw_label == "-1.0":
295
+ return "0.0", prediction[1] # Downgrade to neutral
296
 
297
+ # Rule 2: Contrast term handling
298
+ if 'contrast_term' in processed_text:
299
+ # Split text by contrast terms
300
+ parts = re.split(r'\bcontrast_term\b', processed_text)
301
+ if len(parts) > 1:
302
+ # Analyze sentiment of each part
303
+ sentiments = []
304
+ for part in parts:
305
+ if part.strip():
306
+ _, _, _, part_insights = self.predict(part)
307
+ sentiments.append(part_insights.get('dominant_sentiment', 'neutral'))
308
+
309
+ # Apply contrast rules
310
+ if sentiments:
311
+ if sentiments[0] == "positive" and sentiments[-1] == "negative":
312
+ return "-1.0", max(prediction[0], confidence)
313
+ elif sentiments[0] == "negative" and sentiments[-1] == "positive":
314
+ return "1.0", max(prediction[2], confidence)
315
+
316
+ # Rule 3: Rating-based override
317
+ rating_match = re.search(r'rating_(\d+)_(5|10)', processed_text)
318
+ if rating_match:
319
+ rating = int(rating_match.group(1))
320
+ scale = int(rating_match.group(2))
321
+ normalized = rating / scale
322
+
323
+ if normalized < 0.4:
324
+ return "-1.0", 0.95
325
+ elif normalized < 0.7:
326
+ return "0.0", 0.95
327
+ else:
328
+ return "1.0", 0.95
329
+
330
+ return raw_label, confidence
331
+
332
+ def _generate_insights(self, processed_text, prediction):
333
+ """Generate linguistic insights from text"""
334
+ insights = {
335
+ "dominant_sentiment": "",
336
+ "key_phrases": [],
337
+ "sentiment_score": float(prediction[2] - prediction[0]), # Positive - Negative
338
+ "confidence_level": ""
339
+ }
340
+
341
+ # Determine dominant sentiment
342
+ if prediction[2] > 0.7: # Positive
343
+ insights["dominant_sentiment"] = "positive"
344
+ elif prediction[0] > 0.7: # Negative
345
+ insights["dominant_sentiment"] = "negative"
346
+ else: # Neutral
347
+ insights["dominant_sentiment"] = "neutral"
348
+
349
+ # Confidence categorization
350
+ max_conf = max(prediction)
351
+ if max_conf > 0.9:
352
+ insights["confidence_level"] = "high"
353
+ elif max_conf > 0.7:
354
+ insights["confidence_level"] = "medium"
355
+ else:
356
+ insights["confidence_level"] = "low"
357
+
358
+ # Extract key phrases
359
+ key_terms = re.findall(
360
+ r'(POSITIVE_\w+|NEGATIVE_\w+|NEUTRAL_\w+|INTENSIFIER_\w+|DIMINISHER_\w+|NEGATION_\w+)',
361
+ processed_text
362
+ )
363
+ insights["key_phrases"] = list(set(key_terms))[:5] # Top 5 unique
364
+
365
+ return insights
366
 
367
+ # --- Streamlit Application ---
368
+ class SentimentAnalysisApp:
369
+ """Professional sentiment analysis application"""
370
+ def __init__(self):
371
+ self.analyzer = None
372
+ self.initialize()
373
+
374
+ def initialize(self):
375
+ """Initialize application resources"""
376
+ st.set_page_config(
377
+ page_title="Enterprise Sentiment Analyzer",
378
+ layout="wide",
379
+ page_icon="📊"
380
+ )
381
 
382
+ try:
383
+ self.analyzer = SentimentAnalyzer()
384
+ st.session_state.analyzer = self.analyzer
385
+ except Exception as e:
386
+ st.error(f"System Initialization Failed: {str(e)}")
387
  st.stop()
388
 
389
+ def run(self):
390
+ """Run main application"""
391
+ st.title("📈 Enterprise Sentiment Analysis System")
392
+ st.markdown("""
393
+ <style>
394
+ .positive { color: #4CAF50; font-weight: bold; }
395
+ .negative { color: #F44336; font-weight: bold; }
396
+ .neutral { color: #2196F3; font-weight: bold; }
397
+ .header { border-bottom: 2px solid #eee; padding-bottom: 10px; }
398
+ .highlight { background-color: #f0f9ff; border-radius: 5px; padding: 15px; }
399
+ </style>
400
+ """, unsafe_allow_html=True)
401
 
402
+ # Application sections
403
+ self.render_test_suite()
404
+ self.render_single_analysis()
405
+ self.render_batch_analysis()
406
+ self.render_footer()
407
+
408
+ def render_test_suite(self):
409
+ """Validation test suite with professional layout"""
410
+ with st.expander("🧪 VALIDATION TEST SUITE", expanded=True):
411
+ st.markdown("<h3 class='header'>System Performance Validation</h3>", unsafe_allow_html=True)
412
+
413
+ # Professional test cases
414
+ test_cases = [
415
+ ("I love this product! It's absolutely amazing 😍", "1.0", "Clear positive sentiment"),
416
+ ("Terrible experience, worst purchase ever", "-1.0", "Clear negative sentiment"),
417
+ ("The item is okay, nothing special", "0.0", "Neutral baseline"),
418
+ ("Not bad but could be better", "0.0", "Negated negative to neutral"),
419
+ ("Avoid this company at all costs", "-1.0", "Strong negative intent"),
420
+ ("It's barely acceptable", "0.0", "Diminisher indicating neutrality"),
421
+ ("Service was not great", "0.0", "Negated positive to neutral"),
422
+ ("The product is good but the service is poor", "0.0", "Contrasting sentiments"),
423
+ ("Best decision I've ever made!", "1.0", "Positive with intensifier"),
424
+ ("3/5 - Average performance", "0.0", "Numerical rating"),
425
+ ("Would not recommend to anyone", "-1.0", "Strong negation"),
426
+ ("Slightly better than expected", "1.0", "Diminisher with positive"),
427
+ ("Not what I hoped for", "-1.0", "Negative expectation mismatch"),
428
+ ("Exceptional quality and value", "1.0", "Strong positive indicators"),
429
+ ("Mediocre at best", "0.0", "Neutral with diminisher")
430
+ ]
431
 
432
+ if st.button("🚀 Execute Full Test Suite", type="primary", use_container_width=True):
433
+ results = []
434
+ progress_bar = st.progress(0)
435
+ status_text = st.empty()
436
 
437
+ for i, (text, expected, desc) in enumerate(test_cases):
438
+ label, conf, processed, insights = self.analyzer.predict(text)
439
+ match = "" if label == expected else ""
440
+ results.append({
441
+ "Text": text,
442
+ "Description": desc,
443
+ "Expected": expected,
444
+ "Predicted": label,
445
+ "Confidence": f"{conf:.1%}",
446
+ "Match": match
447
+ })
448
+
449
+ # Update progress
450
+ progress = (i + 1) / len(test_cases)
451
+ progress_bar.progress(progress)
452
+ status_text.text(f"Testing case {i+1}/{len(test_cases)}: {text[:30]}...")
453
 
454
+ # Display results
455
+ df_results = pd.DataFrame(results)
 
 
 
 
456
 
457
+ # Highlight mismatches
458
+ def highlight_mismatch(row):
459
+ return ['background-color: #ffdddd' if row.Match == "✗" else '' for _ in row]
460
+
461
+ st.dataframe(
462
+ df_results.style.apply(highlight_mismatch, axis=1),
463
+ height=600
464
+ )
465
+
466
+ # Calculate accuracy
467
+ accuracy = (df_results['Match'] == "✓").mean()
468
+ st.metric("Test Suite Accuracy", f"{accuracy:.1%}",
469
+ delta_color="normal",
470
+ help="Overall accuracy across test cases")
471
+
472
+ # Display failed cases
473
+ failed = df_results[df_results['Match'] == "✗"]
474
+ if not failed.empty:
475
+ st.subheader("Improvement Opportunities")
476
+ for _, row in failed.iterrows():
477
+ st.error(f"**Case:** {row['Description']}")
478
+ st.code(f"Text: {row['Text']}\nExpected: {row['Expected']} | Predicted: {row['Predicted']}")
479
+
480
+ def render_single_analysis(self):
481
+ """Single text analysis with professional presentation"""
482
+ with st.form("single_analysis_form"):
483
+ st.markdown("<h3 class='header'>Single Text Analysis</h3>", unsafe_allow_html=True)
484
 
485
+ # Text input with examples
486
+ user_input = st.text_area(
487
+ "Input Text:",
488
+ height=150,
489
+ placeholder="Enter text to analyze...",
490
+ value="The product quality was exceptional but delivery was delayed."
 
 
491
  )
492
 
493
+ col1, col2 = st.columns([3, 1])
494
+ with col1:
495
+ advanced = st.checkbox("Show linguistic insights", value=True)
496
+ with col2:
497
+ submitted = st.form_submit_button("🔍 Analyze Sentiment", type="primary", use_container_width=True)
498
 
499
+ if submitted and user_input.strip():
500
+ with st.spinner("Performing deep linguistic analysis..."):
501
+ # Perform analysis
502
+ label, confidence, processed, insights = self.analyzer.predict(user_input)
503
+
504
+ # Display main results
505
+ sentiment_info = {
506
+ "1.0": ("Positive Sentiment", "#4CAF50", "😊"),
507
+ "0.0": ("Neutral Sentiment", "#2196F3", "😐"),
508
+ "-1.0": ("Negative Sentiment", "#F44336", "😠")
509
+ }.get(label, ("Unknown Sentiment", "#9E9E9E", "❓"))
510
+
511
+ st.markdown(f"""
512
+ <div class='highlight'>
513
+ <div style="display: flex; align-items: center; margin-bottom: 15px;">
514
+ <h2 style="color: {sentiment_info[1]}; margin: 0;">{sentiment_info[0]} {sentiment_info[2]}</h2>
515
+ <div style="margin-left: auto; font-size: 1.2rem;">
516
+ Confidence: <b>{confidence:.1%}</b>
517
+ </div>
518
+ </div>
519
+ <div style="font-size: 1.1rem; margin-top: 10px;">
520
+ {user_input[:200]}{'...' if len(user_input) > 200 else ''}
521
+ </div>
522
+ </div>
523
+ """, unsafe_allow_html=True)
524
+
525
+ # Advanced insights
526
+ if advanced:
527
+ with st.expander("🧠 Linguistic Analysis Insights", expanded=True):
528
+ col1, col2 = st.columns(2)
529
+
530
+ with col1:
531
+ st.subheader("Text Processing")
532
+ st.markdown(f"**Processed Text:** \n`{processed}`")
533
+
534
+ if insights.get("key_phrases"):
535
+ st.subheader("Key Phrases Detected")
536
+ for phrase in insights["key_phrases"]:
537
+ st.markdown(f"- `{phrase}`")
538
+
539
+ with col2:
540
+ st.subheader("Sentiment Analysis")
541
+ # Sentiment distribution
542
+ fig, ax = plt.subplots(figsize=(6, 4))
543
+ sentiments = ['Negative', 'Neutral', 'Positive']
544
+ colors = ['#F44336', '#2196F3', '#4CAF50']
545
+ ax.bar(sentiments, insights.get('prediction', [0,0,0]), color=colors)
546
+ ax.set_title('Sentiment Probability Distribution')
547
+ ax.set_ylim(0, 1)
548
+ st.pyplot(fig)
549
+
550
+ # Confidence indicator
551
+ st.metric("Confidence Level", insights.get("confidence_level", "").title())
552
+
553
+ # Sentiment score
554
+ score = insights.get("sentiment_score", 0)
555
+ sentiment_val = "Positive" if score > 0 else "Negative" if score < 0 else "Neutral"
556
+ st.metric("Sentiment Score", f"{score:.2f} ({sentiment_val})")
557
+
558
+ def render_batch_analysis(self):
559
+ """Professional batch analysis section"""
560
+ st.markdown("<h3 class='header'>Batch Analysis</h3>", unsafe_allow_html=True)
561
+ st.markdown("Upload a CSV file for high-volume sentiment processing")
562
+
563
+ uploaded_file = st.file_uploader(
564
+ "Upload CSV File",
565
+ type=["csv"],
566
+ accept_multiple_files=False,
567
+ help="File must contain a column named 'text' with content to analyze"
568
+ )
569
+
570
+ if uploaded_file is not None:
571
+ try:
572
+ # Read and validate CSV
573
+ df = pd.read_csv(uploaded_file)
574
+
575
+ if 'text' not in df.columns:
576
+ st.error("Invalid file format: Missing 'text' column")
577
+ return
578
+
579
+ st.success(f"File loaded successfully: {len(df)} records detected")
580
+
581
+ if st.button("🚀 Process Entire Dataset", type="primary", use_container_width=True):
582
+ # Create containers for UI
583
+ progress_bar = st.progress(0)
584
+ status_text = st.empty()
585
+ results_container = st.empty()
586
+
587
+ # Process dataset
588
+ results = []
589
+ start_time = time.time()
590
+
591
+ for i, row in enumerate(df.itertuples()):
592
+ text = str(row.text)
593
+ label, confidence, processed, insights = self.analyzer.predict(text)
594
+
595
+ # Map to human-readable
596
+ sentiment_label = {
597
+ "1.0": "Positive",
598
+ "0.0": "Neutral",
599
+ "-1.0": "Negative"
600
+ }.get(label, "Unknown")
601
+
602
+ results.append({
603
+ "Original Text": text,
604
+ "Processed Text": processed,
605
+ "Sentiment": sentiment_label,
606
+ "Confidence": confidence,
607
+ "Raw Label": label
608
+ })
609
+
610
+ # Update UI every 10 items or last
611
+ if i % 10 == 0 or i == len(df)-1:
612
+ progress = (i + 1) / len(df)
613
+ progress_bar.progress(progress)
614
+
615
+ # Update status with performance metrics
616
+ elapsed = time.time() - start_time
617
+ speed = (i+1) / max(elapsed, 1) # Items per second
618
+ status_text.text(
619
+ f"Processed {i+1}/{len(df)} records | "
620
+ f"Speed: {speed:.1f} records/sec | "
621
+ f"Estimated: {((len(df)-i-1)/max(speed,1)):.0f}s remaining"
622
+ )
623
+
624
+ # Show preview
625
+ preview_df = pd.DataFrame(results[-10:])
626
+ results_container.dataframe(preview_df)
627
+
628
+ # Create final results
629
+ results_df = pd.DataFrame(results)
630
+
631
+ # Display analysis summary
632
+ st.success("Processing complete! Analysis summary:")
633
+
634
+ # Sentiment distribution
635
+ col1, col2 = st.columns(2)
636
+ with col1:
637
+ st.subheader("Sentiment Distribution")
638
+ fig, ax = plt.subplots(figsize=(8, 5))
639
+ sentiment_counts = results_df['Sentiment'].value_counts()
640
+ colors = ['#F44336' if s == 'Negative' else '#2196F3' if s == 'Neutral' else '#4CAF50'
641
+ for s in sentiment_counts.index]
642
+ ax.pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%',
643
+ colors=colors, startangle=90)
644
+ ax.axis('equal')
645
+ st.pyplot(fig)
646
+
647
+ with col2:
648
+ st.subheader("Confidence Distribution")
649
+ fig, ax = plt.subplots(figsize=(8, 5))
650
+ sns.histplot(results_df['Confidence'], bins=20, kde=True, ax=ax)
651
+ ax.set_xlabel("Confidence Score")
652
+ ax.set_ylabel("Count")
653
+ ax.set_title("Prediction Confidence Distribution")
654
+ st.pyplot(fig)
655
+
656
+ # Download functionality
657
+ csv = results_df.to_csv(index=False).encode('utf-8')
658
+ st.download_button(
659
+ "💾 Download Full Analysis",
660
+ csv,
661
+ "sentiment_analysis_results.csv",
662
+ mime="text/csv",
663
+ type="primary",
664
+ use_container_width=True
665
+ )
666
+
667
+ except Exception as e:
668
+ st.error(f"Batch processing failed: {str(e)}")
669
+
670
+ def render_footer(self):
671
+ """Professional application footer"""
672
+ st.markdown("---")
673
+ st.markdown("""
674
+ <div style="text-align: center; color: #777; padding: 20px;">
675
+ <p>Enterprise Sentiment Analysis System v3.1 • Powered by Deep Learning</p>
676
+ <p>© 2025 Sentiment Analytics Inc. • All rights reserved</p>
677
+ </div>
678
+ """, unsafe_allow_html=True)
679
 
680
+ # --- Application Execution ---
681
+ if __name__ == "__main__":
682
+ app = SentimentAnalysisApp()
683
+ app.run()