ppryan commited on
Commit
14de84f
·
verified ·
1 Parent(s): 2e1a060

Upload folder using huggingface_hub

Browse files
Files changed (33) hide show
  1. src/__pycache__/__init__.cpython-311.pyc +0 -0
  2. src/__pycache__/__init__.cpython-314.pyc +0 -0
  3. src/__pycache__/config.cpython-311.pyc +0 -0
  4. src/__pycache__/config.cpython-314.pyc +0 -0
  5. src/__pycache__/evaluate.cpython-311.pyc +0 -0
  6. src/__pycache__/evaluate.cpython-314.pyc +0 -0
  7. src/__pycache__/infer.cpython-311.pyc +0 -0
  8. src/__pycache__/infer.cpython-314.pyc +0 -0
  9. src/__pycache__/preprocess.cpython-311.pyc +0 -0
  10. src/__pycache__/preprocess.cpython-314.pyc +0 -0
  11. src/__pycache__/train.cpython-311.pyc +0 -0
  12. src/__pycache__/train.cpython-314.pyc +0 -0
  13. src/multilingual_sentiment_analysis.egg-info/PKG-INFO +8 -0
  14. src/multilingual_sentiment_analysis.egg-info/SOURCES.txt +15 -0
  15. src/multilingual_sentiment_analysis.egg-info/dependency_links.txt +1 -0
  16. src/multilingual_sentiment_analysis.egg-info/requires.txt +4 -0
  17. src/multilingual_sentiment_analysis.egg-info/top_level.txt +2 -0
  18. src/multilingual_sentiment_analysis/__init__.py +0 -0
  19. src/multilingual_sentiment_analysis/__pycache__/__init__.cpython-311.pyc +0 -0
  20. src/multilingual_sentiment_analysis/__pycache__/__init__.cpython-314.pyc +0 -0
  21. src/multilingual_sentiment_analysis/__pycache__/config.cpython-311.pyc +0 -0
  22. src/multilingual_sentiment_analysis/__pycache__/config.cpython-314.pyc +0 -0
  23. src/multilingual_sentiment_analysis/__pycache__/evaluate.cpython-314.pyc +0 -0
  24. src/multilingual_sentiment_analysis/__pycache__/infer.cpython-311.pyc +0 -0
  25. src/multilingual_sentiment_analysis/__pycache__/infer.cpython-314.pyc +0 -0
  26. src/multilingual_sentiment_analysis/__pycache__/preprocess.cpython-311.pyc +0 -0
  27. src/multilingual_sentiment_analysis/__pycache__/preprocess.cpython-314.pyc +0 -0
  28. src/multilingual_sentiment_analysis/__pycache__/train.cpython-314.pyc +0 -0
  29. src/multilingual_sentiment_analysis/config.py +17 -0
  30. src/multilingual_sentiment_analysis/evaluate.py +89 -0
  31. src/multilingual_sentiment_analysis/infer.py +62 -0
  32. src/multilingual_sentiment_analysis/preprocess.py +60 -0
  33. src/multilingual_sentiment_analysis/train.py +66 -0
src/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (190 Bytes). View file
 
src/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (180 Bytes). View file
 
src/__pycache__/config.cpython-311.pyc ADDED
Binary file (1.39 kB). View file
 
src/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.29 kB). View file
 
src/__pycache__/evaluate.cpython-311.pyc ADDED
Binary file (7.44 kB). View file
 
src/__pycache__/evaluate.cpython-314.pyc ADDED
Binary file (6.83 kB). View file
 
src/__pycache__/infer.cpython-311.pyc ADDED
Binary file (4.66 kB). View file
 
src/__pycache__/infer.cpython-314.pyc ADDED
Binary file (4.58 kB). View file
 
src/__pycache__/preprocess.cpython-311.pyc ADDED
Binary file (5.26 kB). View file
 
src/__pycache__/preprocess.cpython-314.pyc ADDED
Binary file (5.03 kB). View file
 
src/__pycache__/train.cpython-311.pyc ADDED
Binary file (4.54 kB). View file
 
src/__pycache__/train.cpython-314.pyc ADDED
Binary file (4.24 kB). View file
 
src/multilingual_sentiment_analysis.egg-info/PKG-INFO ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: multilingual_sentiment_analysis
3
+ Version: 0.1.0
4
+ Requires-Dist: transformers
5
+ Requires-Dist: datasets
6
+ Requires-Dist: torch
7
+ Requires-Dist: gradio
8
+ Dynamic: requires-dist
src/multilingual_sentiment_analysis.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ setup.py
3
+ src/multilingual_sentiment_analysis/__init__.py
4
+ src/multilingual_sentiment_analysis/config.py
5
+ src/multilingual_sentiment_analysis/evaluate.py
6
+ src/multilingual_sentiment_analysis/infer.py
7
+ src/multilingual_sentiment_analysis/preprocess.py
8
+ src/multilingual_sentiment_analysis/train.py
9
+ src/multilingual_sentiment_analysis.egg-info/PKG-INFO
10
+ src/multilingual_sentiment_analysis.egg-info/SOURCES.txt
11
+ src/multilingual_sentiment_analysis.egg-info/dependency_links.txt
12
+ src/multilingual_sentiment_analysis.egg-info/requires.txt
13
+ src/multilingual_sentiment_analysis.egg-info/top_level.txt
14
+ tests/test_app.py
15
+ tests/test_preprocess.py
src/multilingual_sentiment_analysis.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/multilingual_sentiment_analysis.egg-info/requires.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ transformers
2
+ datasets
3
+ torch
4
+ gradio
src/multilingual_sentiment_analysis.egg-info/top_level.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ app
2
+ multilingual_sentiment_analysis
src/multilingual_sentiment_analysis/__init__.py ADDED
File without changes
src/multilingual_sentiment_analysis/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (222 Bytes). View file
 
src/multilingual_sentiment_analysis/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (146 Bytes). View file
 
src/multilingual_sentiment_analysis/__pycache__/config.cpython-311.pyc ADDED
Binary file (1.43 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/config.cpython-314.pyc ADDED
Binary file (1.32 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/evaluate.cpython-314.pyc ADDED
Binary file (6.86 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/infer.cpython-311.pyc ADDED
Binary file (5.05 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/infer.cpython-314.pyc ADDED
Binary file (4.94 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/preprocess.cpython-311.pyc ADDED
Binary file (5.29 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/preprocess.cpython-314.pyc ADDED
Binary file (5.06 kB). View file
 
src/multilingual_sentiment_analysis/__pycache__/train.cpython-314.pyc ADDED
Binary file (4.27 kB). View file
 
src/multilingual_sentiment_analysis/config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
4
+ DATA_DIR = PROJECT_ROOT / "data"
5
+ AIRLINE_DATA_DIR = DATA_DIR / "airline_tweets"
6
+ MULTILINGUAL_DATA_DIR = DATA_DIR / "multilingual"
7
+ AIRLINE_RESULTS_DIR = PROJECT_ROOT / "results" / "airline_tweets"
8
+ RESULTS_DIR = PROJECT_ROOT / "results" / "multilingual"
9
+ AIRLINE_MODEL_DIR = PROJECT_ROOT / "sentiment_model" / "airline-tweets-sentiment-model"
10
+ MODEL_DIR = PROJECT_ROOT / "sentiment_model" / "general-multilingual-sentiment-model"
11
+
12
+ MODEL_CHECKPOINT = "xlm-roberta-base"
13
+ LABEL2ID = {"negative": 0, "neutral": 1, "positive": 2}
14
+ ID2LABEL = {value: key for key, value in LABEL2ID.items()}
15
+ MAX_LENGTH = 128
16
+ DATASET_NAME = "cardiffnlp/tweet_sentiment_multilingual"
17
+ LANGUAGES = ("arabic", "english", "french", "german", "hindi", "italian", "portuguese", "spanish")
src/multilingual_sentiment_analysis/evaluate.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate a saved sentiment model and produce plots."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import seaborn as sns
10
+ from datasets import load_from_disk
11
+ from sklearn.metrics import classification_report, confusion_matrix
12
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer, TrainingArguments
13
+
14
+ from .config import ID2LABEL, MODEL_DIR, MULTILINGUAL_DATA_DIR, RESULTS_DIR
15
+
16
+
17
+ def evaluate(model_dir: Path, test_dataset: Path, output_dir: Path, show: bool = False) -> None:
18
+ output_dir.mkdir(parents=True, exist_ok=True)
19
+ model = AutoModelForSequenceClassification.from_pretrained(model_dir)
20
+ tokenizer = AutoTokenizer.from_pretrained(model_dir)
21
+ trainer = Trainer(
22
+ model=model,
23
+ args=TrainingArguments(output_dir=str(output_dir), use_cpu=True, report_to="none"),
24
+ processing_class=tokenizer,
25
+ )
26
+ dataset = load_from_disk(str(test_dataset))
27
+ if "language" not in dataset.column_names:
28
+ raise ValueError("The test dataset must include a language column for per-language evaluation.")
29
+ predictions = trainer.predict(dataset)
30
+ y_true = predictions.label_ids
31
+ y_pred = np.argmax(predictions.predictions, axis=-1)
32
+ labels = list(sorted(ID2LABEL))
33
+ names = [ID2LABEL[label] for label in labels]
34
+ report = classification_report(y_true, y_pred, labels=labels, target_names=names, zero_division=0, output_dict=True)
35
+ print(classification_report(y_true, y_pred, labels=labels, target_names=names, zero_division=0))
36
+ per_language = {}
37
+ languages = np.array(dataset["language"])
38
+ for language in sorted(set(languages)):
39
+ mask = languages == language
40
+ per_language[language] = classification_report(
41
+ y_true[mask], y_pred[mask], labels=labels, target_names=names, zero_division=0, output_dict=True
42
+ )
43
+ with (output_dir / "metrics.json").open("w", encoding="utf-8") as file:
44
+ json.dump({"overall": report, "per_language": per_language}, file, ensure_ascii=False, indent=2)
45
+ cm = confusion_matrix(y_true, y_pred, labels=labels)
46
+
47
+ plt.figure(figsize=(6, 5))
48
+ sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=names, yticklabels=names)
49
+ plt.xlabel("Predicted label")
50
+ plt.ylabel("True label")
51
+ plt.title("Confusion Matrix — Test Set")
52
+ plt.tight_layout()
53
+ plt.savefig(output_dir / "confusion_matrix.png", dpi=150)
54
+
55
+ state_path = model_dir / "trainer_state.json"
56
+ if state_path.exists():
57
+ with state_path.open(encoding="utf-8") as file:
58
+ history = json.load(file).get("log_history", [])
59
+ train = [(entry["step"], entry["loss"]) for entry in history if "loss" in entry]
60
+ validation = [(entry["step"], entry["eval_loss"]) for entry in history if "eval_loss" in entry]
61
+ if train or validation:
62
+ plt.figure(figsize=(7, 4))
63
+ if train:
64
+ plt.plot(*zip(*train), label="Training loss")
65
+ if validation:
66
+ plt.plot(*zip(*validation), label="Validation loss")
67
+ plt.xlabel("Step")
68
+ plt.ylabel("Loss")
69
+ plt.title("Training vs. Validation Loss")
70
+ plt.legend()
71
+ plt.tight_layout()
72
+ plt.savefig(output_dir / "loss_curve.png", dpi=150)
73
+ if show:
74
+ plt.show()
75
+ plt.close("all")
76
+
77
+
78
+ def main() -> None:
79
+ parser = argparse.ArgumentParser(description="Evaluate a saved sentiment model.")
80
+ parser.add_argument("--model-dir", type=Path, default=MODEL_DIR)
81
+ parser.add_argument("--test-dataset", type=Path, default=MULTILINGUAL_DATA_DIR / "test_dataset")
82
+ parser.add_argument("--output-dir", type=Path, default=RESULTS_DIR / "eval")
83
+ parser.add_argument("--show", action="store_true")
84
+ args = parser.parse_args()
85
+ evaluate(args.model_dir, args.test_dataset, args.output_dir, args.show)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
src/multilingual_sentiment_analysis/infer.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference helpers for the saved sentiment model."""
2
+
3
+ import os
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+
7
+ import torch
8
+ from transformers import pipeline
9
+
10
+ from .config import MODEL_DIR
11
+
12
+
13
+ def resolve_model_path() -> str:
14
+ """Return an explicitly configured Hub/local model or the local default."""
15
+ configured = os.environ.get("SENTIMENT_MODEL_PATH")
16
+ if configured:
17
+ model_path = Path(configured).expanduser()
18
+ if model_path.is_dir():
19
+ return str(model_path)
20
+ if not model_path.is_absolute() and "/" in configured:
21
+ return configured # Hugging Face model ID, e.g. username/model-name
22
+ raise FileNotFoundError(f"Configured SENTIMENT_MODEL_PATH does not exist: {model_path}")
23
+
24
+ model_path = MODEL_DIR
25
+ if not model_path.is_dir():
26
+ raise FileNotFoundError(
27
+ f"No fine-tuned model found at {model_path}. Train one with `python -m multilingual_sentiment_analysis.train` "
28
+ "or set SENTIMENT_MODEL_PATH to a compatible model directory."
29
+ )
30
+ return str(model_path)
31
+
32
+
33
+ @lru_cache(maxsize=1)
34
+ def get_classifier():
35
+ use_gpu = os.environ.get("USE_GPU", "false").lower() == "true"
36
+ if use_gpu and not torch.cuda.is_available():
37
+ raise RuntimeError("USE_GPU=true but CUDA is not available.")
38
+ return pipeline(
39
+ "sentiment-analysis", model=resolve_model_path(), tokenizer=resolve_model_path(),
40
+ device=0 if use_gpu else -1,
41
+ )
42
+
43
+
44
+ def predict(text: str) -> dict[str, float | str]:
45
+ if not isinstance(text, str) or not text.strip():
46
+ raise ValueError("Input must be a non-empty string.")
47
+ result = get_classifier()(text.strip())[0]
48
+ return {"label": result["label"], "confidence": float(result["score"])}
49
+
50
+
51
+ def predict_batch(texts: list[str]) -> list[dict[str, float | str]]:
52
+ if not isinstance(texts, list) or not texts or any(not isinstance(text, str) or not text.strip() for text in texts):
53
+ raise ValueError("Input must be a non-empty list of non-empty strings.")
54
+ return [
55
+ {"label": result["label"], "confidence": float(result["score"])}
56
+ for result in get_classifier()([text.strip() for text in texts])
57
+ ]
58
+
59
+
60
+ if __name__ == "__main__":
61
+ for sample in ("The flight was delayed three hours.", "Merci, le vol était très agréable !", "الخدمة كانت ممتازة"):
62
+ print(f"{sample}\n -> {predict(sample)}\n")
src/multilingual_sentiment_analysis/preprocess.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prepare Cardiff NLP's multilingual tweet-sentiment dataset for fine-tuning."""
2
+
3
+ import argparse
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from datasets import DatasetDict, concatenate_datasets, load_dataset
8
+ from transformers import AutoTokenizer
9
+
10
+ from .config import DATASET_NAME, LANGUAGES, MAX_LENGTH, MODEL_CHECKPOINT, MULTILINGUAL_DATA_DIR
11
+
12
+
13
+ def clean_text(text: str) -> str:
14
+ """Remove tweet metadata while preserving Unicode text, emojis, and accents."""
15
+ text = str(text).lower()
16
+ text = re.sub(r"https?://\S+|www\.\S+", "", text)
17
+ text = re.sub(r"@\w+", "", text)
18
+ text = text.replace("#", "")
19
+ return re.sub(r"\s+", " ", text).strip()
20
+
21
+
22
+ def load_multilingual_splits(dataset_name: str = DATASET_NAME) -> DatasetDict:
23
+ """Load official splits and retain each row's language for later evaluation.
24
+
25
+ The dataset's ``all`` configuration merges languages but does not expose their
26
+ provenance. Loading each official configuration retains that information.
27
+ """
28
+ grouped = {"train": [], "validation": [], "test": []}
29
+ for language in LANGUAGES:
30
+ dataset = load_dataset(dataset_name, language)
31
+ for split in grouped:
32
+ grouped[split].append(dataset[split].add_column("language", [language] * len(dataset[split])))
33
+ return DatasetDict({split: concatenate_datasets(parts).shuffle(seed=42) for split, parts in grouped.items()})
34
+
35
+
36
+ def build_datasets(output_dir: Path, checkpoint: str = MODEL_CHECKPOINT) -> None:
37
+ tokenizer = AutoTokenizer.from_pretrained(checkpoint)
38
+
39
+ def tokenize(batch):
40
+ return tokenizer(batch["text"], truncation=True, max_length=MAX_LENGTH)
41
+
42
+ output_dir.mkdir(parents=True, exist_ok=True)
43
+ datasets = load_multilingual_splits()
44
+ for split, dataset in datasets.items():
45
+ cleaned = dataset.map(lambda row: {"text": clean_text(row["text"])})
46
+ cleaned.filter(lambda row: bool(row["text"])).map(tokenize, batched=True).save_to_disk(
47
+ str(output_dir / f"{split.replace('validation', 'val')}_dataset")
48
+ )
49
+
50
+
51
+ def main() -> None:
52
+ parser = argparse.ArgumentParser(description="Download and prepare multilingual tweet-sentiment data.")
53
+ parser.add_argument("--output-dir", type=Path, default=MULTILINGUAL_DATA_DIR)
54
+ parser.add_argument("--checkpoint", default=MODEL_CHECKPOINT)
55
+ args = parser.parse_args()
56
+ build_datasets(args.output_dir, args.checkpoint)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
src/multilingual_sentiment_analysis/train.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fine-tune the sentiment model."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ from datasets import load_from_disk
8
+ from sklearn.metrics import accuracy_score, f1_score
9
+ from transformers import (
10
+ AutoModelForSequenceClassification,
11
+ AutoTokenizer,
12
+ DataCollatorWithPadding,
13
+ Trainer,
14
+ TrainingArguments,
15
+ set_seed,
16
+ )
17
+
18
+ from .config import ID2LABEL, LABEL2ID, MODEL_CHECKPOINT, MODEL_DIR, MULTILINGUAL_DATA_DIR, RESULTS_DIR
19
+
20
+
21
+ def compute_metrics(eval_pred):
22
+ logits, labels = eval_pred
23
+ predictions = np.argmax(logits, axis=-1)
24
+ return {
25
+ "accuracy": accuracy_score(labels, predictions),
26
+ "f1": f1_score(labels, predictions, average="weighted"),
27
+ }
28
+
29
+
30
+ def train(data_dir: Path, output_dir: Path, model_dir: Path, checkpoint: str, epochs: float, use_cpu: bool) -> None:
31
+ set_seed(42)
32
+ tokenizer = AutoTokenizer.from_pretrained(checkpoint)
33
+ model = AutoModelForSequenceClassification.from_pretrained(
34
+ checkpoint, num_labels=len(LABEL2ID), id2label=ID2LABEL, label2id=LABEL2ID
35
+ )
36
+ args = TrainingArguments(
37
+ output_dir=str(output_dir), eval_strategy="epoch", save_strategy="epoch",
38
+ learning_rate=2e-5, per_device_train_batch_size=16, per_device_eval_batch_size=32,
39
+ num_train_epochs=epochs, weight_decay=0.01, load_best_model_at_end=True,
40
+ metric_for_best_model="f1", logging_steps=50, report_to="none", use_cpu=use_cpu,
41
+ )
42
+ trainer = Trainer(
43
+ model=model, args=args, train_dataset=load_from_disk(str(data_dir / "train_dataset")),
44
+ eval_dataset=load_from_disk(str(data_dir / "val_dataset")),
45
+ data_collator=DataCollatorWithPadding(tokenizer=tokenizer), processing_class=tokenizer,
46
+ compute_metrics=compute_metrics,
47
+ )
48
+ trainer.train()
49
+ trainer.save_model(str(model_dir))
50
+ tokenizer.save_pretrained(str(model_dir))
51
+
52
+
53
+ def main() -> None:
54
+ parser = argparse.ArgumentParser(description="Fine-tune XLM-R sentiment classification.")
55
+ parser.add_argument("--data-dir", type=Path, default=MULTILINGUAL_DATA_DIR)
56
+ parser.add_argument("--output-dir", type=Path, default=RESULTS_DIR)
57
+ parser.add_argument("--model-dir", type=Path, default=MODEL_DIR)
58
+ parser.add_argument("--checkpoint", default=MODEL_CHECKPOINT)
59
+ parser.add_argument("--epochs", type=float, default=3)
60
+ parser.add_argument("--cpu", action="store_true", help="Force CPU training.")
61
+ args = parser.parse_args()
62
+ train(args.data_dir, args.output_dir, args.model_dir, args.checkpoint, args.epochs, args.cpu)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()