Spaces:
Sleeping
Sleeping
OliverPerrin commited on
Commit ·
6a7a381
1
Parent(s): 1601799
Improve emotion task architecture and fix visualizations
Browse filesArchitecture improvements:
- Added hidden_dim option to ClassificationHead for 2-layer MLP
- Emotion head now uses 768→384→28 MLP for better multi-label capacity
- Updated task weights: emotion=1.5, summarization=1.0, topic=0.8
Visualization fixes:
- LR schedule now generates theoretical curve when no metrics logged
- Added learning_rate logging to MLflow in trainer
- Log warmup_steps and scheduler_type to MLflow params
Gradio UI modernization:
- New yellow/black research theme (lexitheme)
- Removed emojis for cleaner academic presentation
- Progress bar style emotion scores
- Updated tab names and descriptions
- configs/training/full.yaml +3 -3
- scripts/demo_gradio.py +177 -119
- scripts/visualize_training.py +54 -10
- src/models/factory.py +6 -1
- src/models/heads.py +13 -1
- src/training/trainer.py +6 -0
configs/training/full.yaml
CHANGED
|
@@ -30,9 +30,9 @@ trainer:
|
|
| 30 |
validation_max_length: 128
|
| 31 |
label_smoothing: 0.1
|
| 32 |
task_weights:
|
| 33 |
-
summarization: 1.
|
| 34 |
-
emotion: 1.
|
| 35 |
-
topic:
|
| 36 |
max_train_samples: 50000 # Cap training for speed
|
| 37 |
max_val_samples: 3000 # Faster validation
|
| 38 |
early_stopping_patience: 3
|
|
|
|
| 30 |
validation_max_length: 128
|
| 31 |
label_smoothing: 0.1
|
| 32 |
task_weights:
|
| 33 |
+
summarization: 1.0 # Reduced from 1.2 to give emotion room
|
| 34 |
+
emotion: 1.5 # Increased to prevent degradation
|
| 35 |
+
topic: 0.8 # Reduced since topic already near SOTA
|
| 36 |
max_train_samples: 50000 # Cap training for speed
|
| 37 |
max_val_samples: 3000 # Faster validation
|
| 38 |
early_stopping_patience: 3
|
scripts/demo_gradio.py
CHANGED
|
@@ -48,27 +48,26 @@ NEWS_FILE = DEMO_DATA_DIR / "news_samples.jsonl"
|
|
| 48 |
EVAL_REPORT_PATH = OUTPUTS_DIR / "evaluation_report.json"
|
| 49 |
TRAINING_HISTORY_PATH = OUTPUTS_DIR / "training_history.json"
|
| 50 |
|
| 51 |
-
# Emotion display
|
| 52 |
-
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
-
"realization": "💡", "relief": "😌", "remorse": "😔",
|
| 62 |
}
|
| 63 |
|
| 64 |
-
# Topic display
|
| 65 |
-
|
| 66 |
-
"World": "
|
| 67 |
-
"Sci/Tech": "
|
| 68 |
-
"Education & Reference": "
|
| 69 |
-
"Health": "
|
| 70 |
-
"Society & Culture": "
|
| 71 |
-
"Computers & Internet": "
|
| 72 |
}
|
| 73 |
|
| 74 |
# --------------- Data Loading ---------------
|
|
@@ -202,24 +201,25 @@ def analyze_text(text: str) -> tuple[str, str, str]:
|
|
| 202 |
emotions = pipe.predict_emotions([text], threshold=0.3)[0]
|
| 203 |
topic = pipe.predict_topics([text])[0]
|
| 204 |
|
| 205 |
-
# Format emotions - sort by score descending
|
| 206 |
if emotions.labels:
|
| 207 |
-
# Pair labels with scores and sort by score descending
|
| 208 |
paired = list(zip(emotions.labels, emotions.scores, strict=False))
|
| 209 |
paired_sorted = sorted(paired, key=lambda x: x[1], reverse=True)[:5]
|
| 210 |
|
| 211 |
emotion_parts = []
|
| 212 |
for lbl, score in paired_sorted:
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
else:
|
| 218 |
-
emotion_str = "
|
| 219 |
|
| 220 |
-
# Format topic
|
| 221 |
-
|
| 222 |
-
topic_str = f"
|
| 223 |
|
| 224 |
return summary, emotion_str, topic_str
|
| 225 |
|
|
@@ -257,7 +257,7 @@ def get_book_info(title: str) -> str:
|
|
| 257 |
num_paras = len(book["paragraphs"])
|
| 258 |
word_count = book["word_count"]
|
| 259 |
topic = book.get("topic", "Unknown")
|
| 260 |
-
return f"**{title}**\n\n
|
| 261 |
return ""
|
| 262 |
|
| 263 |
|
|
@@ -271,8 +271,8 @@ def get_book_summary_and_emotions(title: str) -> tuple[str, str, str]:
|
|
| 271 |
|
| 272 |
# Get topic
|
| 273 |
topic = book.get("topic", "Unknown")
|
| 274 |
-
|
| 275 |
-
topic_str = f"
|
| 276 |
|
| 277 |
# Get emotions - already sorted by score in library.json
|
| 278 |
emotions = book.get("emotions", [])
|
|
@@ -281,11 +281,13 @@ def get_book_summary_and_emotions(title: str) -> tuple[str, str, str]:
|
|
| 281 |
for emo in emotions[:5]:
|
| 282 |
label = emo.get("label", "")
|
| 283 |
score = emo.get("score", 0)
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
else:
|
| 288 |
-
emotion_str = "
|
| 289 |
|
| 290 |
return summary, emotion_str, topic_str
|
| 291 |
return "No summary available.", "", ""
|
|
@@ -358,17 +360,17 @@ def load_metrics() -> str:
|
|
| 358 |
val_final = train_metrics.get("val_epoch_3", {})
|
| 359 |
|
| 360 |
md = """
|
| 361 |
-
##
|
| 362 |
|
| 363 |
-
### Training Results
|
| 364 |
|
| 365 |
| Task | Metric | Score |
|
| 366 |
|------|--------|-------|
|
| 367 |
| **Topic Classification** | Accuracy | **{topic_acc:.1%}** |
|
| 368 |
-
| **Emotion Detection** | F1 | {emo_f1:.1%} |
|
| 369 |
| **Summarization** | ROUGE-like | {rouge:.1%} |
|
| 370 |
|
| 371 |
-
### Evaluation Results
|
| 372 |
|
| 373 |
| Metric | Value |
|
| 374 |
|--------|-------|
|
|
@@ -376,6 +378,8 @@ def load_metrics() -> str:
|
|
| 376 |
| Emotion F1 (macro) | {eval_emo:.1%} |
|
| 377 |
| ROUGE-like | {eval_rouge:.1%} |
|
| 378 |
| BLEU | {eval_bleu:.3f} |
|
|
|
|
|
|
|
| 379 |
""".format(
|
| 380 |
topic_acc=val_final.get("topic_accuracy", 0),
|
| 381 |
emo_f1=val_final.get("emotion_f1", 0),
|
|
@@ -391,36 +395,83 @@ def load_metrics() -> str:
|
|
| 391 |
|
| 392 |
# --------------- Gradio Interface ---------------
|
| 393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
with gr.Blocks(
|
| 395 |
-
title="LexiMind
|
| 396 |
-
theme=
|
| 397 |
css="""
|
| 398 |
-
.book-card { padding:
|
| 399 |
-
.book-card p { color: #
|
| 400 |
.results-panel { min-height: 200px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
"""
|
| 402 |
) as demo:
|
| 403 |
gr.Markdown(
|
| 404 |
"""
|
| 405 |
-
#
|
| 406 |
-
##
|
| 407 |
|
| 408 |
-
|
| 409 |
-
- 📝 **Summarization** - Generate concise summaries
|
| 410 |
-
- 😊 **Emotion Detection** - Identify emotional tones
|
| 411 |
-
- 📂 **Topic Classification** - Categorize by subject
|
| 412 |
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
"""
|
| 415 |
)
|
| 416 |
|
| 417 |
# ===================== TAB 1: EXPLORE BOOKS =====================
|
| 418 |
-
with gr.Tab("
|
| 419 |
gr.Markdown(
|
| 420 |
"""
|
| 421 |
### Classic Literature Collection
|
| 422 |
-
Select a book to
|
| 423 |
-
|
| 424 |
"""
|
| 425 |
)
|
| 426 |
|
|
@@ -428,26 +479,26 @@ with gr.Blocks(
|
|
| 428 |
with gr.Column(scale=1):
|
| 429 |
book_dropdown = gr.Dropdown(
|
| 430 |
choices=get_book_titles(),
|
| 431 |
-
label="
|
| 432 |
value=get_book_titles()[0] if get_book_titles() else None,
|
| 433 |
)
|
| 434 |
book_info = gr.Markdown(elem_classes=["book-card"])
|
| 435 |
|
| 436 |
gr.Markdown("---")
|
| 437 |
-
gr.Markdown("**
|
| 438 |
para_slider = gr.Slider(
|
| 439 |
minimum=0,
|
| 440 |
maximum=19,
|
| 441 |
step=1,
|
| 442 |
value=0,
|
| 443 |
-
label="Excerpt
|
| 444 |
-
info="Navigate through
|
| 445 |
)
|
| 446 |
|
| 447 |
-
analyze_excerpt_btn = gr.Button("
|
| 448 |
|
| 449 |
with gr.Column(scale=2):
|
| 450 |
-
gr.Markdown("####
|
| 451 |
book_summary = gr.Textbox(
|
| 452 |
label="",
|
| 453 |
lines=4,
|
|
@@ -457,14 +508,14 @@ with gr.Blocks(
|
|
| 457 |
|
| 458 |
with gr.Row():
|
| 459 |
with gr.Column():
|
| 460 |
-
gr.Markdown("####
|
| 461 |
book_emotions = gr.Markdown(value="*Select a book*")
|
| 462 |
with gr.Column():
|
| 463 |
-
gr.Markdown("####
|
| 464 |
book_topic = gr.Markdown(value="*Select a book*")
|
| 465 |
|
| 466 |
gr.Markdown("---")
|
| 467 |
-
gr.Markdown("####
|
| 468 |
book_excerpt = gr.Textbox(
|
| 469 |
label="",
|
| 470 |
lines=6,
|
|
@@ -476,7 +527,7 @@ with gr.Blocks(
|
|
| 476 |
with gr.Row():
|
| 477 |
with gr.Column():
|
| 478 |
excerpt_summary = gr.Textbox(
|
| 479 |
-
label="
|
| 480 |
lines=3,
|
| 481 |
interactive=False,
|
| 482 |
)
|
|
@@ -510,12 +561,12 @@ with gr.Blocks(
|
|
| 510 |
)
|
| 511 |
|
| 512 |
# ===================== TAB 2: EXPLORE NEWS =====================
|
| 513 |
-
with gr.Tab("
|
| 514 |
gr.Markdown(
|
| 515 |
"""
|
| 516 |
-
### CNN/DailyMail
|
| 517 |
-
|
| 518 |
-
|
| 519 |
"""
|
| 520 |
)
|
| 521 |
|
|
@@ -526,13 +577,13 @@ with gr.Blocks(
|
|
| 526 |
maximum=99,
|
| 527 |
step=1,
|
| 528 |
value=0,
|
| 529 |
-
label="
|
| 530 |
)
|
| 531 |
-
random_news_btn = gr.Button("
|
| 532 |
-
analyze_news_btn = gr.Button("
|
| 533 |
|
| 534 |
gr.Markdown("### Reference Summary")
|
| 535 |
-
gr.Markdown("*
|
| 536 |
reference_summary = gr.Textbox(
|
| 537 |
label="",
|
| 538 |
lines=4,
|
|
@@ -542,7 +593,7 @@ with gr.Blocks(
|
|
| 542 |
|
| 543 |
with gr.Column(scale=2):
|
| 544 |
news_text = gr.Textbox(
|
| 545 |
-
label="
|
| 546 |
lines=12,
|
| 547 |
max_lines=15,
|
| 548 |
interactive=False,
|
|
@@ -551,19 +602,19 @@ with gr.Blocks(
|
|
| 551 |
with gr.Row():
|
| 552 |
with gr.Column():
|
| 553 |
news_summary = gr.Textbox(
|
| 554 |
-
label="
|
| 555 |
lines=4,
|
| 556 |
interactive=False,
|
| 557 |
)
|
| 558 |
with gr.Column():
|
| 559 |
with gr.Row():
|
| 560 |
news_emotions = gr.Markdown(
|
| 561 |
-
label="
|
| 562 |
-
value="*
|
| 563 |
)
|
| 564 |
news_topic = gr.Markdown(
|
| 565 |
-
label="
|
| 566 |
-
value="*
|
| 567 |
)
|
| 568 |
|
| 569 |
# News event handlers
|
|
@@ -591,41 +642,41 @@ with gr.Blocks(
|
|
| 591 |
)
|
| 592 |
|
| 593 |
# ===================== TAB 3: FREE TEXT =====================
|
| 594 |
-
with gr.Tab("
|
| 595 |
gr.Markdown(
|
| 596 |
"""
|
| 597 |
-
###
|
| 598 |
-
Enter any text
|
| 599 |
-
|
| 600 |
"""
|
| 601 |
)
|
| 602 |
|
| 603 |
with gr.Row():
|
| 604 |
with gr.Column(scale=3):
|
| 605 |
free_text_input = gr.Textbox(
|
| 606 |
-
label="
|
| 607 |
lines=8,
|
| 608 |
-
placeholder="Paste or type
|
| 609 |
)
|
| 610 |
|
| 611 |
with gr.Row():
|
| 612 |
-
analyze_free_btn = gr.Button("
|
| 613 |
-
clear_btn = gr.Button("
|
| 614 |
|
| 615 |
-
gr.Markdown("**Sample
|
| 616 |
with gr.Row():
|
| 617 |
-
sample1 = gr.Button("
|
| 618 |
-
sample2 = gr.Button("
|
| 619 |
-
sample3 = gr.Button("
|
| 620 |
|
| 621 |
with gr.Column(scale=2):
|
| 622 |
free_summary = gr.Textbox(
|
| 623 |
-
label="
|
| 624 |
lines=4,
|
| 625 |
interactive=False,
|
| 626 |
)
|
| 627 |
with gr.Row():
|
| 628 |
-
free_emotions = gr.Markdown(value="*Enter text and
|
| 629 |
free_topic = gr.Markdown(value="")
|
| 630 |
|
| 631 |
# Sample texts
|
|
@@ -647,7 +698,7 @@ with gr.Blocks(
|
|
| 647 |
)
|
| 648 |
|
| 649 |
# ===================== TAB 4: METRICS =====================
|
| 650 |
-
with gr.Tab("
|
| 651 |
with gr.Row():
|
| 652 |
with gr.Column(scale=2):
|
| 653 |
gr.Markdown(load_metrics())
|
|
@@ -657,46 +708,53 @@ with gr.Blocks(
|
|
| 657 |
gr.Image(str(confusion_path), label="Topic Confusion Matrix")
|
| 658 |
|
| 659 |
# ===================== TAB 5: ABOUT =====================
|
| 660 |
-
with gr.Tab("
|
| 661 |
gr.Markdown(
|
| 662 |
"""
|
| 663 |
-
###
|
| 664 |
|
| 665 |
-
LexiMind is a **multi-task
|
| 666 |
-
demonstrating end-to-end
|
| 667 |
|
| 668 |
-
####
|
| 669 |
|
| 670 |
-
|
| 671 |
-
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
|
|
|
|
|
|
|
|
|
| 675 |
|
| 676 |
-
####
|
| 677 |
|
| 678 |
-
|
|
| 679 |
-
|------|---------
|
| 680 |
-
| Summarization |
|
| 681 |
-
| Emotion |
|
| 682 |
-
| Topic |
|
| 683 |
-
| Books | Project Gutenberg | 8 classic novels for evaluation |
|
| 684 |
|
| 685 |
-
####
|
| 686 |
|
| 687 |
-
|
| 688 |
-
-
|
| 689 |
-
|
|
|
|
|
|
|
| 690 |
|
| 691 |
-
####
|
| 692 |
|
| 693 |
-
-
|
| 694 |
-
-
|
| 695 |
-
-
|
| 696 |
|
| 697 |
---
|
| 698 |
|
| 699 |
-
**
|
|
|
|
|
|
|
|
|
|
| 700 |
"""
|
| 701 |
)
|
| 702 |
|
|
|
|
| 48 |
EVAL_REPORT_PATH = OUTPUTS_DIR / "evaluation_report.json"
|
| 49 |
TRAINING_HISTORY_PATH = OUTPUTS_DIR / "training_history.json"
|
| 50 |
|
| 51 |
+
# Emotion display - clean labels without emojis for research aesthetic
|
| 52 |
+
EMOTION_LABELS = {\n "joy": "Joy", "love": "Love", "anger": "Anger", "fear": "Fear",
|
| 53 |
+
"sadness": "Sadness", "surprise": "Surprise", "neutral": "Neutral",
|
| 54 |
+
"admiration": "Admiration", "amusement": "Amusement", "annoyance": "Annoyance",
|
| 55 |
+
"approval": "Approval", "caring": "Caring", "confusion": "Confusion",
|
| 56 |
+
"curiosity": "Curiosity", "desire": "Desire", "disappointment": "Disappointment",
|
| 57 |
+
"disapproval": "Disapproval", "disgust": "Disgust", "embarrassment": "Embarrassment",
|
| 58 |
+
"excitement": "Excitement", "gratitude": "Gratitude", "grief": "Grief",
|
| 59 |
+
"nervousness": "Nervousness", "optimism": "Optimism", "pride": "Pride",
|
| 60 |
+
"realization": "Realization", "relief": "Relief", "remorse": "Remorse",
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
+
# Topic display - clean labels
|
| 64 |
+
TOPIC_LABELS = {
|
| 65 |
+
"World": "World News", "Sports": "Sports", "Business": "Business/Finance",
|
| 66 |
+
"Sci/Tech": "Science & Technology", "Science & Mathematics": "Science",
|
| 67 |
+
"Education & Reference": "Education", "Entertainment & Music": "Entertainment",
|
| 68 |
+
"Health": "Health", "Family & Relationships": "Lifestyle",
|
| 69 |
+
"Society & Culture": "Society", "Politics & Government": "Politics",
|
| 70 |
+
"Computers & Internet": "Technology",
|
| 71 |
}
|
| 72 |
|
| 73 |
# --------------- Data Loading ---------------
|
|
|
|
| 201 |
emotions = pipe.predict_emotions([text], threshold=0.3)[0]
|
| 202 |
topic = pipe.predict_topics([text])[0]
|
| 203 |
|
| 204 |
+
# Format emotions - sort by score descending, clean presentation
|
| 205 |
if emotions.labels:
|
|
|
|
| 206 |
paired = list(zip(emotions.labels, emotions.scores, strict=False))
|
| 207 |
paired_sorted = sorted(paired, key=lambda x: x[1], reverse=True)[:5]
|
| 208 |
|
| 209 |
emotion_parts = []
|
| 210 |
for lbl, score in paired_sorted:
|
| 211 |
+
clean_label = EMOTION_LABELS.get(lbl.lower(), lbl.title())
|
| 212 |
+
# Use progress bar style
|
| 213 |
+
bar_width = int(score * 10)
|
| 214 |
+
bar = "█" * bar_width + "░" * (10 - bar_width)
|
| 215 |
+
emotion_parts.append(f"**{clean_label}** \n`{bar}` {score:.0%}")
|
| 216 |
+
emotion_str = "\n\n".join(emotion_parts)
|
| 217 |
else:
|
| 218 |
+
emotion_str = "*No strong emotions detected*"
|
| 219 |
|
| 220 |
+
# Format topic - clean presentation
|
| 221 |
+
clean_topic = TOPIC_LABELS.get(topic.label, topic.label)
|
| 222 |
+
topic_str = f"**{clean_topic}**\n\nConfidence: {topic.confidence:.0%}"
|
| 223 |
|
| 224 |
return summary, emotion_str, topic_str
|
| 225 |
|
|
|
|
| 257 |
num_paras = len(book["paragraphs"])
|
| 258 |
word_count = book["word_count"]
|
| 259 |
topic = book.get("topic", "Unknown")
|
| 260 |
+
return f"**{title}**\n\n{word_count:,} words | {num_paras} excerpts\n\nTopic: {topic}"
|
| 261 |
return ""
|
| 262 |
|
| 263 |
|
|
|
|
| 271 |
|
| 272 |
# Get topic
|
| 273 |
topic = book.get("topic", "Unknown")
|
| 274 |
+
clean_topic = TOPIC_LABELS.get(topic, topic)
|
| 275 |
+
topic_str = f"**{clean_topic}**"
|
| 276 |
|
| 277 |
# Get emotions - already sorted by score in library.json
|
| 278 |
emotions = book.get("emotions", [])
|
|
|
|
| 281 |
for emo in emotions[:5]:
|
| 282 |
label = emo.get("label", "")
|
| 283 |
score = emo.get("score", 0)
|
| 284 |
+
clean_label = EMOTION_LABELS.get(label.lower(), label.title())
|
| 285 |
+
bar_width = int(score * 10)
|
| 286 |
+
bar = "█" * bar_width + "░" * (10 - bar_width)
|
| 287 |
+
emotion_parts.append(f"**{clean_label}** \n`{bar}` {score:.0%}")
|
| 288 |
+
emotion_str = "\n\n".join(emotion_parts)
|
| 289 |
else:
|
| 290 |
+
emotion_str = "*No strong emotions detected*"
|
| 291 |
|
| 292 |
return summary, emotion_str, topic_str
|
| 293 |
return "No summary available.", "", ""
|
|
|
|
| 360 |
val_final = train_metrics.get("val_epoch_3", {})
|
| 361 |
|
| 362 |
md = """
|
| 363 |
+
## Model Performance
|
| 364 |
|
| 365 |
+
### Training Results (Final Epoch)
|
| 366 |
|
| 367 |
| Task | Metric | Score |
|
| 368 |
|------|--------|-------|
|
| 369 |
| **Topic Classification** | Accuracy | **{topic_acc:.1%}** |
|
| 370 |
+
| **Emotion Detection** | F1 (macro) | {emo_f1:.1%} |
|
| 371 |
| **Summarization** | ROUGE-like | {rouge:.1%} |
|
| 372 |
|
| 373 |
+
### Evaluation Set Results
|
| 374 |
|
| 375 |
| Metric | Value |
|
| 376 |
|--------|-------|
|
|
|
|
| 378 |
| Emotion F1 (macro) | {eval_emo:.1%} |
|
| 379 |
| ROUGE-like | {eval_rouge:.1%} |
|
| 380 |
| BLEU | {eval_bleu:.3f} |
|
| 381 |
+
|
| 382 |
+
*Training on CNN/DailyMail, GoEmotions, and AG News datasets.*
|
| 383 |
""".format(
|
| 384 |
topic_acc=val_final.get("topic_accuracy", 0),
|
| 385 |
emo_f1=val_final.get("emotion_f1", 0),
|
|
|
|
| 395 |
|
| 396 |
# --------------- Gradio Interface ---------------
|
| 397 |
|
| 398 |
+
# Custom yellow/black research theme
|
| 399 |
+
lexitheme = gr.themes.Base(
|
| 400 |
+
primary_hue=gr.themes.colors.amber,
|
| 401 |
+
secondary_hue=gr.themes.colors.stone,
|
| 402 |
+
neutral_hue=gr.themes.colors.stone,
|
| 403 |
+
font=gr.themes.GoogleFont("Inter"),
|
| 404 |
+
font_mono=gr.themes.GoogleFont("JetBrains Mono"),
|
| 405 |
+
).set(
|
| 406 |
+
body_background_fill="#0a0a0a",
|
| 407 |
+
body_background_fill_dark="#0a0a0a",
|
| 408 |
+
body_text_color="#e5e5e5",
|
| 409 |
+
body_text_color_dark="#e5e5e5",
|
| 410 |
+
block_background_fill="#171717",
|
| 411 |
+
block_background_fill_dark="#171717",
|
| 412 |
+
block_border_color="#262626",
|
| 413 |
+
block_border_color_dark="#262626",
|
| 414 |
+
block_label_text_color="#fbbf24",
|
| 415 |
+
block_label_text_color_dark="#fbbf24",
|
| 416 |
+
block_title_text_color="#fbbf24",
|
| 417 |
+
block_title_text_color_dark="#fbbf24",
|
| 418 |
+
input_background_fill="#262626",
|
| 419 |
+
input_background_fill_dark="#262626",
|
| 420 |
+
input_border_color="#404040",
|
| 421 |
+
input_border_color_dark="#404040",
|
| 422 |
+
button_primary_background_fill="#fbbf24",
|
| 423 |
+
button_primary_background_fill_dark="#fbbf24",
|
| 424 |
+
button_primary_text_color="#0a0a0a",
|
| 425 |
+
button_primary_text_color_dark="#0a0a0a",
|
| 426 |
+
button_primary_background_fill_hover="#f59e0b",
|
| 427 |
+
button_primary_background_fill_hover_dark="#f59e0b",
|
| 428 |
+
button_secondary_background_fill="#262626",
|
| 429 |
+
button_secondary_background_fill_dark="#262626",
|
| 430 |
+
button_secondary_text_color="#fbbf24",
|
| 431 |
+
button_secondary_text_color_dark="#fbbf24",
|
| 432 |
+
button_secondary_border_color="#fbbf24",
|
| 433 |
+
button_secondary_border_color_dark="#fbbf24",
|
| 434 |
+
slider_color="#fbbf24",
|
| 435 |
+
slider_color_dark="#fbbf24",
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
with gr.Blocks(
|
| 439 |
+
title="LexiMind | Multi-Task NLP Research Demo",
|
| 440 |
+
theme=lexitheme,
|
| 441 |
css="""
|
| 442 |
+
.book-card { padding: 16px; border-radius: 8px; background: #1f1f1f; border: 1px solid #333; }
|
| 443 |
+
.book-card p { color: #e5e5e5 !important; }
|
| 444 |
.results-panel { min-height: 200px; }
|
| 445 |
+
.metric-box { background: #1f1f1f; border: 1px solid #fbbf24; border-radius: 8px; padding: 12px; }
|
| 446 |
+
h1, h2, h3 { color: #fbbf24 !important; }
|
| 447 |
+
.prose { color: #d4d4d4 !important; }
|
| 448 |
+
a { color: #fbbf24 !important; }
|
| 449 |
"""
|
| 450 |
) as demo:
|
| 451 |
gr.Markdown(
|
| 452 |
"""
|
| 453 |
+
# LexiMind
|
| 454 |
+
## Multi-Task Transformer for Document Understanding
|
| 455 |
|
| 456 |
+
A custom encoder-decoder architecture initialized from FLAN-T5-base, trained jointly on:
|
|
|
|
|
|
|
|
|
|
| 457 |
|
| 458 |
+
| Task | Dataset | Output |
|
| 459 |
+
|------|---------|--------|
|
| 460 |
+
| **Abstractive Summarization** | CNN/DailyMail | Condensed text |
|
| 461 |
+
| **Emotion Detection** | GoEmotions | 28-class multi-label |
|
| 462 |
+
| **Topic Classification** | AG News | 4-class single-label |
|
| 463 |
+
|
| 464 |
+
---
|
| 465 |
"""
|
| 466 |
)
|
| 467 |
|
| 468 |
# ===================== TAB 1: EXPLORE BOOKS =====================
|
| 469 |
+
with gr.Tab("Literature"):
|
| 470 |
gr.Markdown(
|
| 471 |
"""
|
| 472 |
### Classic Literature Collection
|
| 473 |
+
Select a book to view pre-computed analysis (summary, emotions, topic).
|
| 474 |
+
Browse excerpts and run real-time inference on individual passages.
|
| 475 |
"""
|
| 476 |
)
|
| 477 |
|
|
|
|
| 479 |
with gr.Column(scale=1):
|
| 480 |
book_dropdown = gr.Dropdown(
|
| 481 |
choices=get_book_titles(),
|
| 482 |
+
label="Select Book",
|
| 483 |
value=get_book_titles()[0] if get_book_titles() else None,
|
| 484 |
)
|
| 485 |
book_info = gr.Markdown(elem_classes=["book-card"])
|
| 486 |
|
| 487 |
gr.Markdown("---")
|
| 488 |
+
gr.Markdown("**Browse Excerpts**")
|
| 489 |
para_slider = gr.Slider(
|
| 490 |
minimum=0,
|
| 491 |
maximum=19,
|
| 492 |
step=1,
|
| 493 |
value=0,
|
| 494 |
+
label="Excerpt Index",
|
| 495 |
+
info="Navigate through passages"
|
| 496 |
)
|
| 497 |
|
| 498 |
+
analyze_excerpt_btn = gr.Button("Analyze Excerpt", variant="secondary", size="sm")
|
| 499 |
|
| 500 |
with gr.Column(scale=2):
|
| 501 |
+
gr.Markdown("#### Summary")
|
| 502 |
book_summary = gr.Textbox(
|
| 503 |
label="",
|
| 504 |
lines=4,
|
|
|
|
| 508 |
|
| 509 |
with gr.Row():
|
| 510 |
with gr.Column():
|
| 511 |
+
gr.Markdown("#### Detected Emotions")
|
| 512 |
book_emotions = gr.Markdown(value="*Select a book*")
|
| 513 |
with gr.Column():
|
| 514 |
+
gr.Markdown("#### Topic Classification")
|
| 515 |
book_topic = gr.Markdown(value="*Select a book*")
|
| 516 |
|
| 517 |
gr.Markdown("---")
|
| 518 |
+
gr.Markdown("#### Current Excerpt")
|
| 519 |
book_excerpt = gr.Textbox(
|
| 520 |
label="",
|
| 521 |
lines=6,
|
|
|
|
| 527 |
with gr.Row():
|
| 528 |
with gr.Column():
|
| 529 |
excerpt_summary = gr.Textbox(
|
| 530 |
+
label="Excerpt Summary",
|
| 531 |
lines=3,
|
| 532 |
interactive=False,
|
| 533 |
)
|
|
|
|
| 561 |
)
|
| 562 |
|
| 563 |
# ===================== TAB 2: EXPLORE NEWS =====================
|
| 564 |
+
with gr.Tab("News Articles"):
|
| 565 |
gr.Markdown(
|
| 566 |
"""
|
| 567 |
+
### CNN/DailyMail Dataset
|
| 568 |
+
Browse news articles from the training data. Compare generated summaries
|
| 569 |
+
against human-written reference summaries.
|
| 570 |
"""
|
| 571 |
)
|
| 572 |
|
|
|
|
| 577 |
maximum=99,
|
| 578 |
step=1,
|
| 579 |
value=0,
|
| 580 |
+
label="Article Index",
|
| 581 |
)
|
| 582 |
+
random_news_btn = gr.Button("Random Article", variant="secondary")
|
| 583 |
+
analyze_news_btn = gr.Button("Run Inference", variant="primary")
|
| 584 |
|
| 585 |
gr.Markdown("### Reference Summary")
|
| 586 |
+
gr.Markdown("*Human-written summary from dataset:*")
|
| 587 |
reference_summary = gr.Textbox(
|
| 588 |
label="",
|
| 589 |
lines=4,
|
|
|
|
| 593 |
|
| 594 |
with gr.Column(scale=2):
|
| 595 |
news_text = gr.Textbox(
|
| 596 |
+
label="Source Article",
|
| 597 |
lines=12,
|
| 598 |
max_lines=15,
|
| 599 |
interactive=False,
|
|
|
|
| 602 |
with gr.Row():
|
| 603 |
with gr.Column():
|
| 604 |
news_summary = gr.Textbox(
|
| 605 |
+
label="Generated Summary",
|
| 606 |
lines=4,
|
| 607 |
interactive=False,
|
| 608 |
)
|
| 609 |
with gr.Column():
|
| 610 |
with gr.Row():
|
| 611 |
news_emotions = gr.Markdown(
|
| 612 |
+
label="Emotions",
|
| 613 |
+
value="*Run inference*",
|
| 614 |
)
|
| 615 |
news_topic = gr.Markdown(
|
| 616 |
+
label="Topic",
|
| 617 |
+
value="*Run inference*",
|
| 618 |
)
|
| 619 |
|
| 620 |
# News event handlers
|
|
|
|
| 642 |
)
|
| 643 |
|
| 644 |
# ===================== TAB 3: FREE TEXT =====================
|
| 645 |
+
with gr.Tab("Custom Input"):
|
| 646 |
gr.Markdown(
|
| 647 |
"""
|
| 648 |
+
### Interactive Inference
|
| 649 |
+
Enter any text for real-time analysis. Best results on news-style articles
|
| 650 |
+
and literary prose matching training distribution.
|
| 651 |
"""
|
| 652 |
)
|
| 653 |
|
| 654 |
with gr.Row():
|
| 655 |
with gr.Column(scale=3):
|
| 656 |
free_text_input = gr.Textbox(
|
| 657 |
+
label="Input Text",
|
| 658 |
lines=8,
|
| 659 |
+
placeholder="Paste or type text here...\n\nOptimal for: news articles, literary passages, formal writing.",
|
| 660 |
)
|
| 661 |
|
| 662 |
with gr.Row():
|
| 663 |
+
analyze_free_btn = gr.Button("Run Inference", variant="primary")
|
| 664 |
+
clear_btn = gr.Button("Clear", variant="secondary")
|
| 665 |
|
| 666 |
+
gr.Markdown("**Sample Inputs**")
|
| 667 |
with gr.Row():
|
| 668 |
+
sample1 = gr.Button("Business", size="sm")
|
| 669 |
+
sample2 = gr.Button("Science", size="sm")
|
| 670 |
+
sample3 = gr.Button("Sports", size="sm")
|
| 671 |
|
| 672 |
with gr.Column(scale=2):
|
| 673 |
free_summary = gr.Textbox(
|
| 674 |
+
label="Generated Summary",
|
| 675 |
lines=4,
|
| 676 |
interactive=False,
|
| 677 |
)
|
| 678 |
with gr.Row():
|
| 679 |
+
free_emotions = gr.Markdown(value="*Enter text and run inference*")
|
| 680 |
free_topic = gr.Markdown(value="")
|
| 681 |
|
| 682 |
# Sample texts
|
|
|
|
| 698 |
)
|
| 699 |
|
| 700 |
# ===================== TAB 4: METRICS =====================
|
| 701 |
+
with gr.Tab("Metrics"):
|
| 702 |
with gr.Row():
|
| 703 |
with gr.Column(scale=2):
|
| 704 |
gr.Markdown(load_metrics())
|
|
|
|
| 708 |
gr.Image(str(confusion_path), label="Topic Confusion Matrix")
|
| 709 |
|
| 710 |
# ===================== TAB 5: ABOUT =====================
|
| 711 |
+
with gr.Tab("Architecture"):
|
| 712 |
gr.Markdown(
|
| 713 |
"""
|
| 714 |
+
### Model Architecture
|
| 715 |
|
| 716 |
+
LexiMind is a **multi-task encoder-decoder Transformer** built from scratch in PyTorch,
|
| 717 |
+
demonstrating end-to-end ML engineering from data processing to deployment.
|
| 718 |
|
| 719 |
+
#### Architecture Details
|
| 720 |
|
| 721 |
+
| Component | Specification |
|
| 722 |
+
|-----------|---------------|
|
| 723 |
+
| **Encoder** | 12-layer Transformer, 768-dim, 12 heads |
|
| 724 |
+
| **Decoder** | 12-layer Transformer with causal masking |
|
| 725 |
+
| **Normalization** | Pre-LN with RMSNorm |
|
| 726 |
+
| **Position Encoding** | T5 Relative Position Bias |
|
| 727 |
+
| **Initialization** | FLAN-T5-base pretrained weights |
|
| 728 |
+
| **Parameters** | 272.5M total |
|
| 729 |
|
| 730 |
+
#### Task Heads
|
| 731 |
|
| 732 |
+
| Head | Architecture | Loss |
|
| 733 |
+
|------|--------------|------|
|
| 734 |
+
| **Summarization** | LM head (shared embeddings) | Cross-entropy w/ label smoothing |
|
| 735 |
+
| **Emotion** | Mean-pooled encoder → Linear(768, 28) | BCE (multi-label) |
|
| 736 |
+
| **Topic** | Mean-pooled encoder → Linear(768, 4) | Cross-entropy |
|
|
|
|
| 737 |
|
| 738 |
+
#### Training Data
|
| 739 |
|
| 740 |
+
| Task | Dataset | Size |
|
| 741 |
+
|------|---------|------|
|
| 742 |
+
| Summarization | CNN/DailyMail | ~100K articles |
|
| 743 |
+
| Emotion | GoEmotions | ~43K samples |
|
| 744 |
+
| Topic | AG News | ~120K articles |
|
| 745 |
|
| 746 |
+
#### Known Limitations
|
| 747 |
|
| 748 |
+
- **Domain specificity**: Optimized for news/literary text
|
| 749 |
+
- **Emotion task**: Multi-label F1 limited by class imbalance in GoEmotions
|
| 750 |
+
- **Summarization**: Constrained by 512 token context
|
| 751 |
|
| 752 |
---
|
| 753 |
|
| 754 |
+
**Repository**: [github.com/OliverPerrin/LexiMind](https://github.com/OliverPerrin/LexiMind)
|
| 755 |
+
**Model**: [huggingface.co/OliverPerrin/LexiMind-Model](https://huggingface.co/OliverPerrin/LexiMind-Model)
|
| 756 |
+
|
| 757 |
+
*Oliver Perrin • Appalachian State University • 2025-2026*
|
| 758 |
"""
|
| 759 |
)
|
| 760 |
|
scripts/visualize_training.py
CHANGED
|
@@ -397,11 +397,55 @@ def plot_learning_rate(run) -> None:
|
|
| 397 |
|
| 398 |
fig, ax = plt.subplots(figsize=(12, 5))
|
| 399 |
|
| 400 |
-
if not lr_metrics:
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
else:
|
| 406 |
steps = [m.step for m in lr_metrics]
|
| 407 |
values = [m.value for m in lr_metrics]
|
|
@@ -410,8 +454,9 @@ def plot_learning_rate(run) -> None:
|
|
| 410 |
ax.fill_between(steps, values, alpha=0.3, color=COLORS["primary"])
|
| 411 |
ax.plot(steps, values, linewidth=2.5, color=COLORS["primary"])
|
| 412 |
|
| 413 |
-
# Mark warmup region
|
| 414 |
-
|
|
|
|
| 415 |
if warmup_steps < max(steps):
|
| 416 |
ax.axvline(warmup_steps, color=COLORS["secondary"], linestyle="--",
|
| 417 |
alpha=0.7, linewidth=2, label="Warmup End")
|
|
@@ -419,9 +464,8 @@ def plot_learning_rate(run) -> None:
|
|
| 419 |
label="Warmup Phase")
|
| 420 |
ax.legend(loc="upper right")
|
| 421 |
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
ax.set_xlabel("Step")
|
| 426 |
ax.set_ylabel("Learning Rate")
|
| 427 |
ax.set_title("Learning Rate Schedule (Cosine Annealing with Warmup)")
|
|
|
|
| 397 |
|
| 398 |
fig, ax = plt.subplots(figsize=(12, 5))
|
| 399 |
|
| 400 |
+
if not lr_metrics or len(lr_metrics) < 2:
|
| 401 |
+
# No LR data logged - generate theoretical schedule from config
|
| 402 |
+
logger.info(" No LR metrics found - generating theoretical schedule...")
|
| 403 |
+
|
| 404 |
+
# Get config from run params
|
| 405 |
+
params = run.data.params
|
| 406 |
+
lr_max = float(params.get("learning_rate", params.get("lr", 5e-5)))
|
| 407 |
+
warmup_steps = int(params.get("warmup_steps", 500))
|
| 408 |
+
max_epochs = int(params.get("max_epochs", 5))
|
| 409 |
+
|
| 410 |
+
# Estimate total steps from training loss history
|
| 411 |
+
train_loss = client.get_metric_history(run.info.run_id, "train_total_loss")
|
| 412 |
+
if train_loss:
|
| 413 |
+
epochs_completed = len(train_loss)
|
| 414 |
+
# Estimate ~800 steps per epoch based on typical config
|
| 415 |
+
estimated_steps_per_epoch = 800
|
| 416 |
+
total_steps = max_epochs * estimated_steps_per_epoch
|
| 417 |
+
else:
|
| 418 |
+
total_steps = 4000 # Default fallback
|
| 419 |
+
|
| 420 |
+
# Generate cosine schedule with warmup
|
| 421 |
+
steps = np.arange(0, total_steps)
|
| 422 |
+
values = []
|
| 423 |
+
for step in steps:
|
| 424 |
+
if step < warmup_steps:
|
| 425 |
+
lr = lr_max * (step / max(1, warmup_steps))
|
| 426 |
+
else:
|
| 427 |
+
progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
|
| 428 |
+
lr = lr_max * max(0.1, 0.5 * (1 + np.cos(np.pi * progress)))
|
| 429 |
+
values.append(lr)
|
| 430 |
+
|
| 431 |
+
ax.fill_between(steps, values, alpha=0.3, color=COLORS["primary"])
|
| 432 |
+
ax.plot(steps, values, linewidth=2.5, color=COLORS["primary"], label="Cosine + Warmup")
|
| 433 |
+
|
| 434 |
+
# Mark warmup region
|
| 435 |
+
ax.axvline(warmup_steps, color=COLORS["secondary"], linestyle="--",
|
| 436 |
+
alpha=0.7, linewidth=2, label=f"Warmup End ({warmup_steps})")
|
| 437 |
+
ax.axvspan(0, warmup_steps, alpha=0.1, color=COLORS["highlight"])
|
| 438 |
+
|
| 439 |
+
# Add annotation
|
| 440 |
+
ax.annotate(f"Peak LR: {lr_max:.1e}", xy=(warmup_steps, lr_max),
|
| 441 |
+
xytext=(warmup_steps + 200, lr_max * 0.9),
|
| 442 |
+
fontsize=10, color=COLORS["dark"],
|
| 443 |
+
arrowprops=dict(arrowstyle="->", color=COLORS["dark"], alpha=0.5))
|
| 444 |
+
|
| 445 |
+
ax.legend(loc="upper right")
|
| 446 |
+
ax.text(0.98, 0.02, "(Theoretical - actual LR not logged)",
|
| 447 |
+
transform=ax.transAxes, ha="right", va="bottom",
|
| 448 |
+
fontsize=9, color="gray", style="italic")
|
| 449 |
else:
|
| 450 |
steps = [m.step for m in lr_metrics]
|
| 451 |
values = [m.value for m in lr_metrics]
|
|
|
|
| 454 |
ax.fill_between(steps, values, alpha=0.3, color=COLORS["primary"])
|
| 455 |
ax.plot(steps, values, linewidth=2.5, color=COLORS["primary"])
|
| 456 |
|
| 457 |
+
# Mark warmup region (get from params if available)
|
| 458 |
+
params = run.data.params
|
| 459 |
+
warmup_steps = int(params.get("warmup_steps", 500))
|
| 460 |
if warmup_steps < max(steps):
|
| 461 |
ax.axvline(warmup_steps, color=COLORS["secondary"], linestyle="--",
|
| 462 |
alpha=0.7, linewidth=2, label="Warmup End")
|
|
|
|
| 464 |
label="Warmup Phase")
|
| 465 |
ax.legend(loc="upper right")
|
| 466 |
|
| 467 |
+
# Scientific notation for y-axis if needed
|
| 468 |
+
ax.ticklabel_format(axis="y", style="scientific", scilimits=(-3, 3))
|
|
|
|
| 469 |
ax.set_xlabel("Step")
|
| 470 |
ax.set_ylabel("Learning Rate")
|
| 471 |
ax.set_title("Learning Rate Schedule (Cosine Annealing with Warmup)")
|
src/models/factory.py
CHANGED
|
@@ -548,10 +548,15 @@ def build_multitask_model(
|
|
| 548 |
"summarization",
|
| 549 |
LMHead(d_model=cfg.d_model, vocab_size=vocab_size, tie_embedding=decoder.embedding),
|
| 550 |
)
|
|
|
|
| 551 |
model.add_head(
|
| 552 |
"emotion",
|
| 553 |
ClassificationHead(
|
| 554 |
-
d_model=cfg.d_model,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
),
|
| 556 |
)
|
| 557 |
model.add_head(
|
|
|
|
| 548 |
"summarization",
|
| 549 |
LMHead(d_model=cfg.d_model, vocab_size=vocab_size, tie_embedding=decoder.embedding),
|
| 550 |
)
|
| 551 |
+
# Emotion head with 2-layer MLP for better multi-label capacity (28 classes)
|
| 552 |
model.add_head(
|
| 553 |
"emotion",
|
| 554 |
ClassificationHead(
|
| 555 |
+
d_model=cfg.d_model,
|
| 556 |
+
num_labels=num_emotions,
|
| 557 |
+
pooler="mean",
|
| 558 |
+
dropout=cfg.dropout,
|
| 559 |
+
hidden_dim=cfg.d_model // 2, # 384-dim hidden layer
|
| 560 |
),
|
| 561 |
)
|
| 562 |
model.add_head(
|
src/models/heads.py
CHANGED
|
@@ -25,6 +25,7 @@ class ClassificationHead(nn.Module):
|
|
| 25 |
num_labels: number of output classes
|
| 26 |
pooler: one of 'mean', 'cls', 'max' - how to pool the sequence
|
| 27 |
dropout: dropout probability before final linear layer
|
|
|
|
| 28 |
"""
|
| 29 |
|
| 30 |
def __init__(
|
|
@@ -33,12 +34,23 @@ class ClassificationHead(nn.Module):
|
|
| 33 |
num_labels: int,
|
| 34 |
pooler: Literal["mean", "cls", "max"] = "mean",
|
| 35 |
dropout: float = 0.1,
|
|
|
|
| 36 |
):
|
| 37 |
super().__init__()
|
| 38 |
assert pooler in ("mean", "cls", "max"), "pooler must be 'mean'|'cls'|'max'"
|
| 39 |
self.pooler = pooler
|
| 40 |
self.dropout = nn.Dropout(dropout)
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 44 |
"""
|
|
|
|
| 25 |
num_labels: number of output classes
|
| 26 |
pooler: one of 'mean', 'cls', 'max' - how to pool the sequence
|
| 27 |
dropout: dropout probability before final linear layer
|
| 28 |
+
hidden_dim: optional intermediate dimension for 2-layer MLP (improves capacity)
|
| 29 |
"""
|
| 30 |
|
| 31 |
def __init__(
|
|
|
|
| 34 |
num_labels: int,
|
| 35 |
pooler: Literal["mean", "cls", "max"] = "mean",
|
| 36 |
dropout: float = 0.1,
|
| 37 |
+
hidden_dim: Optional[int] = None,
|
| 38 |
):
|
| 39 |
super().__init__()
|
| 40 |
assert pooler in ("mean", "cls", "max"), "pooler must be 'mean'|'cls'|'max'"
|
| 41 |
self.pooler = pooler
|
| 42 |
self.dropout = nn.Dropout(dropout)
|
| 43 |
+
|
| 44 |
+
# Optional 2-layer MLP for more capacity (useful for multi-label)
|
| 45 |
+
if hidden_dim is not None:
|
| 46 |
+
self.out_proj = nn.Sequential(
|
| 47 |
+
nn.Linear(d_model, hidden_dim),
|
| 48 |
+
nn.GELU(),
|
| 49 |
+
nn.Dropout(dropout),
|
| 50 |
+
nn.Linear(hidden_dim, num_labels),
|
| 51 |
+
)
|
| 52 |
+
else:
|
| 53 |
+
self.out_proj = nn.Linear(d_model, num_labels)
|
| 54 |
|
| 55 |
def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 56 |
"""
|
src/training/trainer.py
CHANGED
|
@@ -262,6 +262,9 @@ class Trainer:
|
|
| 262 |
self.optimizer.zero_grad()
|
| 263 |
if self.scheduler:
|
| 264 |
self.scheduler.step()
|
|
|
|
|
|
|
|
|
|
| 265 |
self.global_step += 1
|
| 266 |
|
| 267 |
if step_loss > 0:
|
|
@@ -398,6 +401,9 @@ class Trainer:
|
|
| 398 |
"gradient_clip_norm": self.config.gradient_clip_norm,
|
| 399 |
"label_smoothing": self.config.label_smoothing,
|
| 400 |
"task_weights": str(self.config.task_weights),
|
|
|
|
|
|
|
|
|
|
| 401 |
})
|
| 402 |
|
| 403 |
def _log_metrics(self, metrics: Dict[str, float], prefix: str, epoch: int) -> None:
|
|
|
|
| 262 |
self.optimizer.zero_grad()
|
| 263 |
if self.scheduler:
|
| 264 |
self.scheduler.step()
|
| 265 |
+
# Log learning rate to MLflow
|
| 266 |
+
current_lr = self.scheduler.get_last_lr()[0]
|
| 267 |
+
mlflow.log_metric("learning_rate", current_lr, step=self.global_step)
|
| 268 |
self.global_step += 1
|
| 269 |
|
| 270 |
if step_loss > 0:
|
|
|
|
| 401 |
"gradient_clip_norm": self.config.gradient_clip_norm,
|
| 402 |
"label_smoothing": self.config.label_smoothing,
|
| 403 |
"task_weights": str(self.config.task_weights),
|
| 404 |
+
"warmup_steps": self.config.warmup_steps,
|
| 405 |
+
"scheduler_type": self.config.scheduler_type,
|
| 406 |
+
"learning_rate": self.optimizer.param_groups[0]["lr"],
|
| 407 |
})
|
| 408 |
|
| 409 |
def _log_metrics(self, metrics: Dict[str, float], prefix: str, epoch: int) -> None:
|