Okba-Sa-20 commited on
Commit
fdaaf7d
·
verified ·
1 Parent(s): d6c3df7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -362
app.py CHANGED
@@ -26,40 +26,40 @@ class FeatureExtractor(tf.keras.layers.Layer):
26
  super(FeatureExtractor, self).__init__(**kwargs)
27
 
28
  def build(self, input_shape):
29
- self.contrast_kernel = self.add_weight(
30
- name='contrast_kernel',
31
- shape=(input_shape[-1], 1),
32
- initializer='glorot_uniform'
33
- )
34
- self.negation_kernel = self.add_weight(
35
- name='negation_kernel',
36
- shape=(input_shape[-1], 1),
37
- initializer='glorot_uniform'
38
- )
39
- self.intensifier_kernel = self.add_weight(
40
- name='intensifier_kernel',
41
- shape=(input_shape[-1], 1),
42
- initializer='glorot_uniform'
43
- )
44
  super(FeatureExtractor, self).build(input_shape)
45
 
46
  def call(self, inputs):
47
- # Detect features
48
  contrast = tf.tensordot(inputs, self.contrast_kernel, axes=1)
49
  contrast = tf.squeeze(contrast, axis=-1)
 
50
 
 
51
  negation = tf.tensordot(inputs, self.negation_kernel, axes=1)
52
  negation = tf.squeeze(negation, axis=-1)
 
53
 
 
54
  intensifier = tf.tensordot(inputs, self.intensifier_kernel, axes=1)
55
  intensifier = tf.squeeze(intensifier, axis=-1)
 
56
 
57
  # Combine features
58
  features = tf.stack([contrast, negation, intensifier], axis=-1)
59
  return features
60
 
61
  def compute_output_shape(self, input_shape):
62
- return (input_shape[0], input_shape[1], 3)
63
 
64
  @tf.keras.utils.register_keras_serializable(package="CustomLayers")
65
  class SentimentAdjuster(tf.keras.layers.Layer):
@@ -119,11 +119,12 @@ class SentimentAdjuster(tf.keras.layers.Layer):
119
  return adjusted
120
 
121
  def compute_output_shape(self, input_shape):
 
122
  return input_shape[0]
123
 
124
  @tf.keras.utils.register_keras_serializable(package="CustomLayers")
125
  class SimpleAttention(tf.keras.layers.Layer):
126
- def __init__(self, **kwargs):
127
  super(SimpleAttention, self).__init__(**kwargs)
128
 
129
  def build(self, input_shape):
@@ -136,16 +137,17 @@ class SimpleAttention(tf.keras.layers.Layer):
136
  super(SimpleAttention, self).build(input_shape)
137
 
138
  def call(self, inputs):
139
- e = tf.keras.backend.tanh(tf.keras.backend.dot(inputs, self.W))
140
- e = tf.keras.backend.squeeze(e, axis=-1)
141
- alpha = tf.keras.backend.softmax(e, axis=1)
142
- alpha = tf.keras.backend.expand_dims(alpha, axis=-1)
143
  context = inputs * alpha
144
- return tf.keras.backend.sum(context, axis=1)
145
 
146
  def compute_output_shape(self, input_shape):
147
  return (input_shape[0], input_shape[2])
148
 
 
149
  # --- Text Preprocessing ---
150
  def preprocess_for_lstm(text, remove_stopwords=False):
151
  if not isinstance(text, str) or not text.strip():
@@ -233,15 +235,14 @@ def preprocess_for_lstm(text, remove_stopwords=False):
233
 
234
  except Exception:
235
  return text.lower()
236
-
237
-
238
  # --- Load model resources ---
239
  @st.cache_resource
240
  def load_model():
241
  MODEL_DIR = "model_files/models"
242
- model_path = f"{MODEL_DIR}/enhanced_lstm_20250624-222759_best.keras"
243
- tokenizer_path = f"{MODEL_DIR}/enhanced_lstm_20250624-222759_tokenizer.pickle"
244
- label_mapping_path = f"{MODEL_DIR}/enhanced_lstm_20250624-222759_label_mapping.pickle"
245
 
246
  # Verify files exist
247
  for path in [model_path, tokenizer_path, label_mapping_path]:
@@ -342,337 +343,4 @@ def predict_sentiment(text):
342
  except Exception as e:
343
  return "0.0", 0.0, "", {"error": str(e)}
344
 
345
- # [Keep the rest of your app.py UI code unchanged]
346
-
347
- # --- Streamlit App UI ---
348
- st.set_page_config(
349
- page_title="Professional Sentiment Analyzer",
350
- layout="wide",
351
- page_icon="📊"
352
- )
353
- st.title("📊 Professional Sentiment Analysis")
354
- st.markdown("""
355
- <style>
356
- .feature-badge {
357
- display: inline-block;
358
- padding: 0.25em 0.6em;
359
- font-size: 75%;
360
- font-weight: 700;
361
- line-height: 1;
362
- text-align: center;
363
- white-space: nowrap;
364
- vertical-align: baseline;
365
- border-radius: 0.25rem;
366
- margin-right: 5px;
367
- margin-bottom: 5px;
368
- }
369
- .positive-badge { background-color: #4CAF50; color: white; }
370
- .negative-badge { background-color: #F44336; color: white; }
371
- .neutral-badge { background-color: #2196F3; color: white; }
372
- .feature-badge-default { background-color: #6c757d; color: white; }
373
- .header-box {
374
- border-radius: 10px;
375
- padding: 20px;
376
- margin-bottom: 20px;
377
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
378
- }
379
- .success-box { background-color: #e8f5e9; border-left: 5px solid #4CAF50; }
380
- .info-box { background-color: #e3f2fd; border-left: 5px solid #2196F3; }
381
- .warning-box { background-color: #ffecb3; border-left: 5px solid #ffc107; }
382
- .danger-box { background-color: #ffebee; border-left: 5px solid #F44336; }
383
- </style>
384
- """, unsafe_allow_html=True)
385
-
386
- # Initialize session state
387
- if 'last_prediction' not in st.session_state:
388
- st.session_state.last_prediction = None
389
- if 'analysis_history' not in st.session_state:
390
- st.session_state.analysis_history = []
391
-
392
- # Model info sidebar
393
- with st.sidebar:
394
- st.header("Model Information")
395
- st.write(f"**Model Name:** Simplified LSTM with Attention")
396
- st.write(f"**Input Shape:** {model.input_shape}")
397
- st.write(f"**Classes:**")
398
- for label, data in SENTIMENT_MAP.items():
399
- st.markdown(f"- {data['display']} `{label}`")
400
-
401
- st.divider()
402
- st.header("Configuration")
403
- confidence_threshold = st.slider(
404
- "Confidence Threshold",
405
- min_value=0.5, max_value=0.9, value=0.65, step=0.05,
406
- help="Minimum confidence level for definitive sentiment classification"
407
- )
408
-
409
- st.divider()
410
- st.header("Analysis History")
411
- if st.session_state.analysis_history:
412
- for i, item in enumerate(st.session_state.analysis_history[:5]):
413
- st.caption(f"{i+1}. {item['text'][:50]}... → {SENTIMENT_MAP[item['label']]['display']}")
414
- else:
415
- st.caption("No history yet")
416
-
417
- # Validation tests with explanations
418
- test_cases = [
419
- ("I love this product! It's absolutely amazing 😍", "1.0", "Clear positive"),
420
- ("Terrible experience, worst purchase ever", "-1.0", "Clear negative"),
421
- ("The item is okay, nothing special", "0.0", "Neutral - baseline"),
422
- ("Not bad but could be better", "0.0", "Neutral - nuanced"),
423
- ("Avoid this company at all costs", "-1.0", "Negative - strong intent"),
424
- ("It's barely acceptable", "0.0", "Neutral - diminisher"),
425
- ("Service was not great", "0.0", "Neutral - negation"),
426
- ("Best decision I've ever made!", "1.0", "Positive - intensifier"),
427
- ("The product is good but the service is terrible", "0.0", "Mixed sentiment"),
428
- ("I'm extremely satisfied with my purchase", "1.0", "Positive with intensifier"),
429
- ("Somewhat disappointed with the quality", "0.0", "Neutral with diminisher"),
430
- ("Absolutely horrible customer service", "-1.0", "Negative with amplifier"),
431
- ("The design is excellent, however the battery life is poor", "0.0", "Contrast indicator"),
432
- ("Wow! This exceeded all my expectations", "1.0", "Positive exclamation"),
433
- ("Ugh, this is disgusting", "-1.0", "Negative exclamation")
434
- ]
435
-
436
- with st.expander("🧪 Validation Test Suite", expanded=True):
437
- cols = st.columns([3, 1])
438
- with cols[0]:
439
- st.subheader("Comprehensive Validation Tests")
440
- with cols[1]:
441
- if st.button("Run All Tests", type="primary", key="run_tests"):
442
- test_results = []
443
-
444
- with st.spinner("Running validation suite..."):
445
- for text, expected, desc in test_cases:
446
- label, confidence, _, debug_info = predict_sentiment(text)
447
- match = label == expected
448
- test_results.append({
449
- "Text": text,
450
- "Description": desc,
451
- "Expected": SENTIMENT_MAP[expected]["display"],
452
- "Predicted": SENTIMENT_MAP[label]["display"],
453
- "Confidence": f"{confidence:.1%}",
454
- "Result": "Pass ✓" if match else "Fail ✗"
455
- })
456
-
457
- # Display results
458
- df_results = pd.DataFrame(test_results)
459
-
460
- # Color coding
461
- def color_result(val):
462
- color = 'green' if val == "Pass ✓" else 'red'
463
- return f'color: {color}; font-weight: bold'
464
-
465
- st.dataframe(
466
- df_results.style.applymap(
467
- lambda x: color_result(x) if x in ["Pass ✓", "Fail ✗"] else ''
468
- )
469
- )
470
-
471
- # Calculate pass rate
472
- pass_rate = (df_results["Result"] == "Pass ✓").mean()
473
- st.metric("Validation Score", f"{pass_rate:.1%}",
474
- delta=f"{len(test_cases)} tests",
475
- delta_color="normal")
476
-
477
- # Single text analysis
478
- with st.form("analysis_form", clear_on_submit=False):
479
- st.subheader("🔍 Text Analysis")
480
- user_input = st.text_area("Enter text:", height=150,
481
- value="The product quality is excellent but delivery was late")
482
- submitted = st.form_submit_button("Analyze Sentiment", type="primary", use_container_width=True)
483
-
484
- if submitted and user_input.strip():
485
- with st.spinner("Analyzing text..."):
486
- label, confidence, processed_text, debug_info = predict_sentiment(user_input)
487
-
488
- # Save to history
489
- st.session_state.analysis_history.insert(0, {
490
- "text": user_input,
491
- "label": label,
492
- "confidence": confidence,
493
- "timestamp": time.time()
494
- })
495
-
496
- # Generate report
497
- report = generate_sentiment_report(label, confidence, debug_info)
498
- st.session_state.last_prediction = debug_info
499
-
500
- # Display results
501
- sentiment_class = "success-box" if label == "1.0" else \
502
- "danger-box" if label == "-1.0" else "info-box"
503
-
504
- st.markdown(f"""
505
- <div class="header-box {sentiment_class}">
506
- <h2 style="margin:0;">{report['sentiment']}</h2>
507
- <p style="font-size: 1.2rem; margin:0;">Confidence: <b>{report['confidence']}</b></p>
508
- </div>
509
- """, unsafe_allow_html=True)
510
-
511
- # Feature badges
512
- if report["features"]:
513
- st.subheader("Key Features Detected")
514
- cols = st.columns(3)
515
- for i, feature in enumerate(report["features"]):
516
- with cols[i % 3]:
517
- st.markdown(f"<div class='feature-badge feature-badge-default'>{feature}</div>",
518
- unsafe_allow_html=True)
519
-
520
- # Word cloud and probabilities
521
- col1, col2 = st.columns(2)
522
- with col1:
523
- if report["word_cloud"]:
524
- st.subheader("Keyword Analysis")
525
- st.pyplot(report["word_cloud"])
526
-
527
- with col2:
528
- st.subheader("Sentiment Probabilities")
529
- if st.session_state.last_prediction and "probabilities" in st.session_state.last_prediction:
530
- prob_data = {
531
- "Negative": st.session_state.last_prediction['probabilities']["Negative"],
532
- "Neutral": st.session_state.last_prediction['probabilities']["Neutral"],
533
- "Positive": st.session_state.last_prediction['probabilities']["Positive"]
534
- }
535
- st.bar_chart(prob_data)
536
-
537
- # Confidence indicator
538
- st.metric("Confidence Level", report["confidence"],
539
- delta="High confidence" if confidence > 0.8 else
540
- "Medium confidence" if confidence > 0.65 else "Low confidence")
541
-
542
- # Debug info
543
- with st.expander("Analysis Details"):
544
- st.write(f"**Processed Text:**")
545
- st.code(processed_text)
546
-
547
- if st.session_state.last_prediction:
548
- st.write("**Debug Information:**")
549
- st.json(st.session_state.last_prediction)
550
-
551
- # --- CSV Batch Processing Section ---
552
- st.subheader("📊 Batch Analysis from CSV")
553
- st.write("Analyze large datasets by uploading a CSV file with text content")
554
-
555
- uploaded_file = st.file_uploader("Upload CSV file", type=["csv"],
556
- help="File must contain a column named 'text'")
557
-
558
- if uploaded_file is not None:
559
- try:
560
- # Read CSV file
561
- df = pd.read_csv(uploaded_file)
562
-
563
- # Verify required column exists
564
- if 'text' not in df.columns:
565
- st.error("❌ CSV file must contain a column named 'text'")
566
- st.stop()
567
-
568
- st.success(f"✅ Successfully loaded {len(df)} records")
569
-
570
- with st.expander("Preview Data", expanded=True):
571
- st.dataframe(df.head(3))
572
-
573
- # Process in batches
574
- if st.button("Analyze Entire Dataset", type="primary", key="batch_analyze"):
575
- results = []
576
- sentiment_counts = Counter()
577
- feature_counts = Counter()
578
-
579
- progress_bar = st.progress(0)
580
- status_text = st.empty()
581
- status_placeholder = st.empty()
582
-
583
- # Process each row
584
- for i, row in enumerate(df.itertuples()):
585
- text = str(row.text)
586
- label, confidence, _, debug_info = predict_sentiment(text)
587
-
588
- # Get sentiment name
589
- sentiment_name = SENTIMENT_MAP[label]["name"]
590
- sentiment_counts[sentiment_name] += 1
591
-
592
- # Count features
593
- if debug_info and "features" in debug_info:
594
- for feature, present in debug_info["features"].items():
595
- if present:
596
- feature_counts[feature.replace('_', ' ').title()] += 1
597
-
598
- # Add to results
599
- results.append({
600
- "Original Text": text,
601
- "Processed Text": debug_info.get("processed_text", ""),
602
- "Sentiment": sentiment_name,
603
- "Label": label,
604
- "Confidence": confidence,
605
- "Features": ", ".join([
606
- k.replace('_', ' ').title()
607
- for k, v in debug_info.get("features", {}).items()
608
- if v
609
- ])
610
- })
611
-
612
- # Update progress
613
- progress = (i + 1) / len(df)
614
- progress_bar.progress(progress)
615
- status_text.text(f"Processed {i+1}/{len(df)} records ({progress:.0%})")
616
-
617
- # Update every 50 records
618
- if i % 50 == 0:
619
- with status_placeholder.container():
620
- st.caption(f"Current distribution: {dict(sentiment_counts)}")
621
-
622
- # Create results dataframe
623
- results_df = pd.DataFrame(results)
624
-
625
- # Show summary
626
- st.subheader("Analysis Summary")
627
- col1, col2, col3 = st.columns(3)
628
-
629
- with col1:
630
- st.metric("Total Records", len(df))
631
-
632
- with col2:
633
- st.metric("Positive", f"{sentiment_counts['Positive']} ({sentiment_counts['Positive']/len(df):.1%})")
634
-
635
- with col3:
636
- st.metric("Negative", f"{sentiment_counts['Negative']} ({sentiment_counts['Negative']/len(df):.1%})")
637
-
638
- # Sentiment distribution
639
- st.subheader("Sentiment Distribution")
640
- dist_col1, dist_col2 = st.columns([1, 2])
641
-
642
- with dist_col1:
643
- st.dataframe(pd.DataFrame.from_dict(sentiment_counts, orient='index', columns=['Count']))
644
-
645
- with dist_col2:
646
- st.bar_chart(pd.Series(sentiment_counts))
647
-
648
- # Feature prevalence
649
- st.subheader("Feature Frequency")
650
- if feature_counts:
651
- feature_df = pd.DataFrame.from_dict(feature_counts, orient='index', columns=['Count'])
652
- feature_df = feature_df.sort_values('Count', ascending=False)
653
- st.dataframe(feature_df)
654
- else:
655
- st.info("No linguistic features detected in this dataset")
656
-
657
- # Show results table
658
- st.subheader("Detailed Results")
659
- st.dataframe(results_df)
660
-
661
- # Download results
662
- csv = results_df.to_csv(index=False).encode('utf-8')
663
- st.download_button(
664
- label="Download Full Results as CSV",
665
- data=csv,
666
- file_name="sentiment_analysis_results.csv",
667
- mime="text/csv",
668
- type="primary"
669
- )
670
-
671
- except Exception as e:
672
- st.error(f"Error processing CSV file: {str(e)}")
673
-
674
- # Footer
675
- st.markdown("---")
676
- st.caption("Professional Sentiment Analysis System v3.0 | "
677
- "© 2025 Sentiment Analytics Inc. | "
678
- f"Model: Simplified LSTM with Attention")
 
26
  super(FeatureExtractor, self).__init__(**kwargs)
27
 
28
  def build(self, input_shape):
29
+ # We'll create trainable weights for feature detection
30
+ self.contrast_kernel = self.add_weight(name='contrast_kernel',
31
+ shape=(input_shape[-1], 1),
32
+ initializer='glorot_uniform')
33
+ self.negation_kernel = self.add_weight(name='negation_kernel',
34
+ shape=(input_shape[-1], 1),
35
+ initializer='glorot_uniform')
36
+ self.intensifier_kernel = self.add_weight(name='intensifier_kernel',
37
+ shape=(input_shape[-1], 1),
38
+ initializer='glorot_uniform')
 
 
 
 
 
39
  super(FeatureExtractor, self).build(input_shape)
40
 
41
  def call(self, inputs):
42
+ # Detect contrast indicators
43
  contrast = tf.tensordot(inputs, self.contrast_kernel, axes=1)
44
  contrast = tf.squeeze(contrast, axis=-1)
45
+ contrast = tf.sigmoid(contrast)
46
 
47
+ # Detect negation patterns
48
  negation = tf.tensordot(inputs, self.negation_kernel, axes=1)
49
  negation = tf.squeeze(negation, axis=-1)
50
+ negation = tf.sigmoid(negation)
51
 
52
+ # Detect intensifiers/diminishers
53
  intensifier = tf.tensordot(inputs, self.intensifier_kernel, axes=1)
54
  intensifier = tf.squeeze(intensifier, axis=-1)
55
+ intensifier = tf.sigmoid(intensifier)
56
 
57
  # Combine features
58
  features = tf.stack([contrast, negation, intensifier], axis=-1)
59
  return features
60
 
61
  def compute_output_shape(self, input_shape):
62
+ return (input_shape[0], input_shape[1], 3) # (batch_size, seq_length, 3 features)
63
 
64
  @tf.keras.utils.register_keras_serializable(package="CustomLayers")
65
  class SentimentAdjuster(tf.keras.layers.Layer):
 
119
  return adjusted
120
 
121
  def compute_output_shape(self, input_shape):
122
+ # Same as predictions shape
123
  return input_shape[0]
124
 
125
  @tf.keras.utils.register_keras_serializable(package="CustomLayers")
126
  class SimpleAttention(tf.keras.layers.Layer):
127
+ def __init__(self, **kwargs):
128
  super(SimpleAttention, self).__init__(**kwargs)
129
 
130
  def build(self, input_shape):
 
137
  super(SimpleAttention, self).build(input_shape)
138
 
139
  def call(self, inputs):
140
+ e = K.tanh(K.dot(inputs, self.W))
141
+ e = K.squeeze(e, axis=-1)
142
+ alpha = K.softmax(e, axis=1)
143
+ alpha = K.expand_dims(alpha, axis=-1)
144
  context = inputs * alpha
145
+ return K.sum(context, axis=1)
146
 
147
  def compute_output_shape(self, input_shape):
148
  return (input_shape[0], input_shape[2])
149
 
150
+
151
  # --- Text Preprocessing ---
152
  def preprocess_for_lstm(text, remove_stopwords=False):
153
  if not isinstance(text, str) or not text.strip():
 
235
 
236
  except Exception:
237
  return text.lower()
238
+
 
239
  # --- Load model resources ---
240
  @st.cache_resource
241
  def load_model():
242
  MODEL_DIR = "model_files/models"
243
+ model_path = f"{MODEL_DIR}/simplified_lstm_{TIMESTAMP}_best.keras"
244
+ tokenizer_path = f"{MODEL_DIR}/simplified_lstm_{TIMESTAMP}_tokenizer.pickle"
245
+ label_mapping_path = f"{MODEL_DIR}/simplified_lstm_{TIMESTAMP}_label_mapping.pickle"
246
 
247
  # Verify files exist
248
  for path in [model_path, tokenizer_path, label_mapping_path]:
 
343
  except Exception as e:
344
  return "0.0", 0.0, "", {"error": str(e)}
345
 
346
+ # [Keep the rest of your app.py UI code unchanged]