Upload 4 files
Browse files- README.md +122 -14
- app.py +74 -0
- requirements.txt +10 -0
- setup.py +14 -0
README.md
CHANGED
|
@@ -1,14 +1,122 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
# Multilingual Sentiment Analysis
|
| 4 |
+
|
| 5 |
+
An end-to-end NLP project for three-class sentiment classification: **negative**, **neutral**, and **positive**. The repository contains two XLM-RoBERTa experiments:
|
| 6 |
+
|
| 7 |
+
1. an archived English airline-tweet model, and
|
| 8 |
+
2. the current general multilingual Twitter-sentiment model.
|
| 9 |
+
|
| 10 |
+
The current model is exposed through a Gradio app and is evaluated both overall and separately for every supported language.
|
| 11 |
+
|
| 12 |
+
## 🎮 Demo: Gradio App
|
| 13 |
+
|
| 14 |
+
Run the interactive demo locally:
|
| 15 |
+
```bash
|
| 16 |
+
python app.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+

|
| 20 |
+
|
| 21 |
+
## Results at a glance
|
| 22 |
+
|
| 23 |
+
| Experiment | Data | Evaluation | Result |
|
| 24 |
+
| --- | --- | --- | --- |
|
| 25 |
+
| Airline tweets (legacy) | English airline tweets | Validation set | 84.24% accuracy, 0.842 weighted F1 |
|
| 26 |
+
| General multilingual (current) | 8-language Twitter sentiment dataset | Held-out test set (6,960 tweets) | 66.52% accuracy, 0.663 macro F1 |
|
| 27 |
+
|
| 28 |
+
The two scores are **not directly comparable**: the airline model was evaluated on a narrow, English-only domain, whereas the multilingual model is evaluated across eight languages and broader Twitter content.
|
| 29 |
+
|
| 30 |
+
### Current multilingual model: confusion matrix
|
| 31 |
+
|
| 32 |
+
<img src="results/multilingual/eval/confusion_matrix.png" width="500">
|
| 33 |
+
|
| 34 |
+
### Airline-tweet model: evaluation plots
|
| 35 |
+
|
| 36 |
+
<img src="results/airline_tweets/eval/confusion_matrix.png" width="400">
|
| 37 |
+
|
| 38 |
+
<img src="results/airline_tweets/eval/loss_curve.png" width="400">
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
### Current multilingual model: test macro F1 by language
|
| 42 |
+
|
| 43 |
+
| Language | Macro F1 |
|
| 44 |
+
| --- | ---: |
|
| 45 |
+
| German | 0.731 |
|
| 46 |
+
| French | 0.721 |
|
| 47 |
+
| Portuguese | 0.699 |
|
| 48 |
+
| English | 0.692 |
|
| 49 |
+
| Spanish | 0.667 |
|
| 50 |
+
| Arabic | 0.647 |
|
| 51 |
+
| Italian | 0.625 |
|
| 52 |
+
| Hindi | 0.516 |
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
The neutral class is the most difficult overall (0.588 F1). Hindi is the weakest supported language, so this project does not claim equal quality across all languages.
|
| 56 |
+
|
| 57 |
+
## How it works
|
| 58 |
+
|
| 59 |
+
1. `multilingual_sentiment_analysis.preprocess` downloads and prepares the multilingual dataset.
|
| 60 |
+
2. Text is cleaned by removing URLs and mentions, normalizing whitespace, and removing only the `#` marker. Unicode text, accents, emojis, and non-Latin scripts are preserved.
|
| 61 |
+
3. `multilingual_sentiment_analysis.train` fine-tunes `xlm-roberta-base` for three-class sequence classification.
|
| 62 |
+
4. `multilingual_sentiment_analysis.evaluate` reports overall metrics, per-language metrics, and a confusion matrix.
|
| 63 |
+
5. `app.py` provides single-text and batch inference with the saved model.
|
| 64 |
+
|
| 65 |
+
## Dataset
|
| 66 |
+
|
| 67 |
+
The current experiment uses [Cardiff NLP's Tweet Sentiment Multilingual dataset](https://huggingface.co/datasets/cardiffnlp/tweet_sentiment_multilingual). It provides the same label mapping used by this project:
|
| 68 |
+
|
| 69 |
+
```text
|
| 70 |
+
0 = negative
|
| 71 |
+
1 = neutral
|
| 72 |
+
2 = positive
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
It includes Arabic, English, French, German, Hindi, Italian, Portuguese, and Spanish. The preparation script loads each language configuration separately so it can preserve the official train/validation/test split and retain the language for evaluation.
|
| 76 |
+
|
| 77 |
+
## Repository layout
|
| 78 |
+
|
| 79 |
+
```text
|
| 80 |
+
src/
|
| 81 |
+
multilingual_sentiment_analysis/
|
| 82 |
+
config.py # shared paths, labels, and dataset settings
|
| 83 |
+
preprocess.py # dataset download, cleaning, tokenization, and saving
|
| 84 |
+
train.py # fine-tuning entry point
|
| 85 |
+
evaluate.py # test and per-language evaluation
|
| 86 |
+
infer.py # lazy model loading and inference helpers
|
| 87 |
+
data/
|
| 88 |
+
airline_tweets/ # archived English-only source data and prepared splits
|
| 89 |
+
multilingual/ # prepared multilingual splits
|
| 90 |
+
results/
|
| 91 |
+
airline_tweets/ # archived English-only checkpoints
|
| 92 |
+
multilingual/ # current checkpoints and evaluation outputs
|
| 93 |
+
sentiment_model/
|
| 94 |
+
airline-tweets-sentiment-model/ # archived English-only model
|
| 95 |
+
general-multilingual-sentiment-model/ # current app model
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Data, checkpoints, and model weights are intentionally ignored by Git.
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
## Training configuration
|
| 102 |
+
|
| 103 |
+
- Base checkpoint: `xlm-roberta-base`
|
| 104 |
+
- Maximum token length: 128
|
| 105 |
+
- Epochs: 3
|
| 106 |
+
- Learning rate: 2e-5 with the Trainer's linear schedule
|
| 107 |
+
- Train/evaluation batch sizes: 16 / 32
|
| 108 |
+
- Best checkpoint criterion: weighted F1 on the validation set
|
| 109 |
+
- Random seed: 42
|
| 110 |
+
|
| 111 |
+
The multilingual training run completed in approximately 4 minutes 40 seconds on an NVIDIA RTX 5060 Laptop GPU.
|
| 112 |
+
|
| 113 |
+
## Limitations and next steps
|
| 114 |
+
|
| 115 |
+
- The model was evaluated on Twitter-style text only; it is not validated for reviews, support tickets, or other domains.
|
| 116 |
+
- It is evaluated for eight languages, not all languages supported by the XLM-R tokenizer.
|
| 117 |
+
- Hindi and neutral-sentiment performance require further improvement.
|
| 118 |
+
- A useful next experiment is to initialize from a Twitter-adapted multilingual encoder such as XLM-T and compare per-language macro F1 against this baseline.
|
| 119 |
+
|
| 120 |
+
## Attribution
|
| 121 |
+
|
| 122 |
+
The multilingual dataset is provided by Cardiff NLP. See its [dataset card](https://huggingface.co/datasets/cardiffnlp/tweet_sentiment_multilingual) and the associated [XLM-T paper](https://aclanthology.org/2022.lrec-1.27/).
|
app.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
from multilingual_sentiment_analysis.infer import predict, predict_batch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def analyze_single(text: str):
|
| 7 |
+
if not text or not text.strip():
|
| 8 |
+
return "⚠️ Please enter some text", ""
|
| 9 |
+
try:
|
| 10 |
+
result = predict(text.strip())
|
| 11 |
+
emoji = {"positive": "🟢", "neutral": "🟡", "negative": "🔴"}.get(result["label"], "⚪")
|
| 12 |
+
return f"{emoji} {result['label'].upper()}", f"{result['confidence'] * 100:.1f}%"
|
| 13 |
+
except Exception as error:
|
| 14 |
+
return f"❌ Error: {error}", ""
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def analyze_batch(batch_text: str):
|
| 18 |
+
if not batch_text or not batch_text.strip():
|
| 19 |
+
return "⚠️ Please enter texts (one per line)", []
|
| 20 |
+
texts = [line.strip() for line in batch_text.splitlines() if line.strip()]
|
| 21 |
+
if not texts:
|
| 22 |
+
return "⚠️ Please enter at least one text", []
|
| 23 |
+
try:
|
| 24 |
+
results = predict_batch(texts)
|
| 25 |
+
emojis = {"positive": "🟢", "neutral": "🟡", "negative": "🔴"}
|
| 26 |
+
rows = [
|
| 27 |
+
[text[:70] + "..." if len(text) > 70 else text,
|
| 28 |
+
f"{emojis.get(result['label'], '⚪')} {result['label'].upper()}",
|
| 29 |
+
f"{result['confidence'] * 100:.1f}%"]
|
| 30 |
+
for text, result in zip(texts, results)
|
| 31 |
+
]
|
| 32 |
+
sentiments = [result["label"] for result in results]
|
| 33 |
+
summary = (
|
| 34 |
+
f"✅ Analyzed {len(texts)} texts | 😊 Positive: {sentiments.count('positive')} | "
|
| 35 |
+
f"😐 Neutral: {sentiments.count('neutral')} | 😞 Negative: {sentiments.count('negative')}"
|
| 36 |
+
)
|
| 37 |
+
return summary, rows
|
| 38 |
+
except Exception as error:
|
| 39 |
+
return f"❌ Error: {error}", []
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
custom_theme = gr.themes.Base(primary_hue="cyan", secondary_hue="slate").set(
|
| 43 |
+
body_background_fill="#000000", body_text_color="#00FFFF",
|
| 44 |
+
button_primary_background_fill="#00FFFF", button_primary_text_color="#000000",
|
| 45 |
+
button_primary_background_fill_hover="#00DDDD", block_title_text_color="#00FFFF",
|
| 46 |
+
block_label_text_color="#00FFFF", input_background_fill="#111111",
|
| 47 |
+
input_border_color="#00FFFF", input_placeholder_color="#666666", border_color_primary="#00FFFF",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
with gr.Blocks(title="🌍 Multilingual Sentiment Analysis", theme=custom_theme) as demo:
|
| 51 |
+
gr.Markdown("# 🌍 Multilingual Sentiment Analysis")
|
| 52 |
+
gr.Markdown("Analyze sentiment using a fine-tuned XLM-RoBERTa model.")
|
| 53 |
+
with gr.Tabs():
|
| 54 |
+
with gr.TabItem("📝 Single Text"):
|
| 55 |
+
with gr.Row():
|
| 56 |
+
with gr.Column(scale=3):
|
| 57 |
+
text_input = gr.Textbox(label="Enter text to analyze", placeholder="Type something to analyze...", lines=4)
|
| 58 |
+
with gr.Column(scale=1):
|
| 59 |
+
analyze_btn = gr.Button("🔍 Analyze", size="lg", variant="primary")
|
| 60 |
+
with gr.Row():
|
| 61 |
+
sentiment_output = gr.Textbox(label="Sentiment", interactive=False)
|
| 62 |
+
confidence_output = gr.Textbox(label="Confidence", interactive=False)
|
| 63 |
+
analyze_btn.click(analyze_single, inputs=text_input, outputs=[sentiment_output, confidence_output])
|
| 64 |
+
with gr.TabItem("📚 Batch Analysis"):
|
| 65 |
+
batch_input = gr.Textbox(label="Enter multiple texts (one per line)", placeholder="Text 1...\nText 2...", lines=8)
|
| 66 |
+
batch_btn = gr.Button("🚀 Batch Analyze", size="lg", variant="primary")
|
| 67 |
+
batch_summary = gr.Textbox(label="Summary", interactive=False)
|
| 68 |
+
batch_results = gr.Dataframe(headers=["Text", "Sentiment", "Confidence"], label="Results", interactive=False)
|
| 69 |
+
batch_btn.click(analyze_batch, inputs=batch_input, outputs=[batch_summary, batch_results])
|
| 70 |
+
gr.Markdown("---\nBuilt with ❤️ using Gradio • XLM-RoBERTa")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.46,<6
|
| 2 |
+
datasets>=2.19,<4
|
| 3 |
+
scikit-learn>=1.4,<2
|
| 4 |
+
matplotlib>=3.8,<4
|
| 5 |
+
seaborn>=0.13,<1
|
| 6 |
+
pandas>=2.2,<3
|
| 7 |
+
torch>=2.2,<3
|
| 8 |
+
accelerate>=0.30,<2
|
| 9 |
+
gradio>=4.0,<7
|
| 10 |
+
-e .
|
setup.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="multilingual_sentiment_analysis",
|
| 5 |
+
version="0.1.0",
|
| 6 |
+
packages=find_packages("src"),
|
| 7 |
+
package_dir={"": "src"},
|
| 8 |
+
install_requires=[
|
| 9 |
+
"transformers",
|
| 10 |
+
"datasets",
|
| 11 |
+
"torch",
|
| 12 |
+
"gradio"
|
| 13 |
+
],
|
| 14 |
+
)
|