Spaces:
Runtime error
Runtime error
Upload 56 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- api/__init__.py +1 -0
- api/__pycache__/__init__.cpython-312.pyc +0 -0
- api/__pycache__/main.cpython-312.pyc +0 -0
- api/__pycache__/service.cpython-312.pyc +0 -0
- api/main.py +103 -0
- api/service.py +199 -0
- config/__init__.py +46 -0
- config/__pycache__/__init__.cpython-312.pyc +0 -0
- config/__pycache__/constants.cpython-312.pyc +0 -0
- config/__pycache__/emoji_map.cpython-312.pyc +0 -0
- config/__pycache__/paths.cpython-312.pyc +0 -0
- config/__pycache__/stopwords.cpython-312.pyc +0 -0
- config/constants.py +37 -0
- config/emoji_map.py +288 -0
- config/paths.py +26 -0
- config/stopwords.py +29 -0
- inference/__init__.py +5 -0
- inference/predict.py +45 -0
- models/__init__.py +17 -0
- models/__pycache__/__init__.cpython-312.pyc +0 -0
- models/__pycache__/registry.cpython-312.pyc +0 -0
- models/logistic_regression/__init__.py +5 -0
- models/logistic_regression/__pycache__/__init__.cpython-312.pyc +0 -0
- models/logistic_regression/__pycache__/model.cpython-312.pyc +0 -0
- models/logistic_regression/model.py +10 -0
- models/naive_bayes/__init__.py +5 -0
- models/naive_bayes/__pycache__/__init__.cpython-312.pyc +0 -0
- models/naive_bayes/__pycache__/model.cpython-312.pyc +0 -0
- models/naive_bayes/model.py +10 -0
- models/random_forest/__init__.py +5 -0
- models/random_forest/__pycache__/__init__.cpython-312.pyc +0 -0
- models/random_forest/__pycache__/model.cpython-312.pyc +0 -0
- models/random_forest/model.py +10 -0
- models/registry.py +10 -0
- models/svm/__init__.py +5 -0
- models/svm/__pycache__/__init__.cpython-312.pyc +0 -0
- models/svm/__pycache__/model.cpython-312.pyc +0 -0
- models/svm/model.py +10 -0
- preprocessing/__init__.py +50 -0
- preprocessing/__pycache__/__init__.cpython-312.pyc +0 -0
- preprocessing/__pycache__/arabic_normalizer.cpython-312.pyc +0 -0
- preprocessing/__pycache__/emoji_handler.cpython-312.pyc +0 -0
- preprocessing/__pycache__/pipeline.cpython-312.pyc +0 -0
- preprocessing/__pycache__/text_cleaning.cpython-312.pyc +0 -0
- preprocessing/arabic_normalizer.py +51 -0
- preprocessing/emoji_handler.py +67 -0
- preprocessing/pipeline.py +67 -0
- preprocessing/text_cleaning.py +66 -0
- utils/__init__.py +22 -0
- utils/__pycache__/__init__.cpython-312.pyc +0 -0
api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""HTTP layer that serves the saved model to the web app."""
|
api/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (226 Bytes). View file
|
|
|
api/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (4.24 kB). View file
|
|
|
api/__pycache__/service.cpython-312.pyc
ADDED
|
Binary file (8.46 kB). View file
|
|
|
api/main.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI app serving the trained model to the web front end.
|
| 2 |
+
|
| 3 |
+
cd ml
|
| 4 |
+
uvicorn api.main:app --reload --port 8000
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from fastapi.responses import FileResponse
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
|
| 15 |
+
from api.service import (
|
| 16 |
+
EmptyAfterCleaning,
|
| 17 |
+
analyze,
|
| 18 |
+
describe_model,
|
| 19 |
+
load_metrics,
|
| 20 |
+
plot_path,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def allowed_origins():
|
| 27 |
+
"""Browser origins permitted to call this API."""
|
| 28 |
+
raw = os.getenv("ASA_CORS_ORIGINS", "http://localhost:3000")
|
| 29 |
+
return [origin.strip() for origin in raw.split(",") if origin.strip()]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
app = FastAPI(
|
| 33 |
+
title="Arabic Sentiment Analysis API",
|
| 34 |
+
description="Serves the trained TF-IDF + classifier pipeline.",
|
| 35 |
+
version="1.0.0",
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
app.add_middleware(
|
| 39 |
+
CORSMiddleware,
|
| 40 |
+
allow_origins=allowed_origins(),
|
| 41 |
+
allow_credentials=False,
|
| 42 |
+
allow_methods=["GET", "POST"],
|
| 43 |
+
allow_headers=["*"],
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class PredictRequest(BaseModel):
|
| 48 |
+
"""One piece of raw Arabic text to classify."""
|
| 49 |
+
text: str = Field(min_length=1, max_length=5000)
|
| 50 |
+
convert_emojis: bool = True
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@app.get("/api/health")
|
| 54 |
+
def health():
|
| 55 |
+
try:
|
| 56 |
+
model = describe_model()
|
| 57 |
+
except FileNotFoundError:
|
| 58 |
+
raise HTTPException(
|
| 59 |
+
status_code=503,
|
| 60 |
+
detail="No saved model found. Run `python run_pipeline.py` first.",
|
| 61 |
+
)
|
| 62 |
+
return {"status": "ok", "model": model["name"]}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@app.get("/api/model")
|
| 66 |
+
def model_info():
|
| 67 |
+
"""What the served pipeline is, and which score types it can produce."""
|
| 68 |
+
try:
|
| 69 |
+
return describe_model()
|
| 70 |
+
except FileNotFoundError:
|
| 71 |
+
raise HTTPException(
|
| 72 |
+
status_code=503,
|
| 73 |
+
detail="No saved model found. Run `python run_pipeline.py` first.",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.get("/api/metrics")
|
| 78 |
+
def metrics():
|
| 79 |
+
"""Test-set scores for all four candidate models, plus the chart manifest."""
|
| 80 |
+
return load_metrics()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@app.get("/api/plots/{name}")
|
| 84 |
+
def plot(name: str):
|
| 85 |
+
"""Serve one of the pipeline's chart PNGs by file name."""
|
| 86 |
+
path = plot_path(name)
|
| 87 |
+
if path is None:
|
| 88 |
+
raise HTTPException(status_code=404, detail=f"No such chart: {name}")
|
| 89 |
+
return FileResponse(path, media_type="image/png")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@app.post("/api/predict")
|
| 93 |
+
def predict(request: PredictRequest):
|
| 94 |
+
"""Clean and classify one piece of text."""
|
| 95 |
+
try:
|
| 96 |
+
return analyze(request.text, convert_emojis=request.convert_emojis)
|
| 97 |
+
except EmptyAfterCleaning as error:
|
| 98 |
+
raise HTTPException(status_code=422, detail=str(error))
|
| 99 |
+
except FileNotFoundError:
|
| 100 |
+
raise HTTPException(
|
| 101 |
+
status_code=503,
|
| 102 |
+
detail="No saved model found. Run `python run_pipeline.py` first.",
|
| 103 |
+
)
|
api/service.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model loading, prediction and stored-metrics access for the API.
|
| 2 |
+
Only one model is served: whatever is saved as ``artifacts/best_model.joblib``.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from functools import lru_cache
|
| 7 |
+
|
| 8 |
+
from config.constants import MIN_WORDS_PER_SENTENCE, NEGATION_WORDS
|
| 9 |
+
from config.emoji_map import emojis as EMOJI_TO_ARABIC
|
| 10 |
+
from config.paths import ARTIFACTS_DIR, DEFAULT_MODEL_PATH
|
| 11 |
+
from preprocessing.pipeline import clean_text
|
| 12 |
+
from training.model_io import load_model
|
| 13 |
+
from utils.chart_style import PLOTS_SUBDIR
|
| 14 |
+
|
| 15 |
+
#: Written by ``run_pipeline.py``
|
| 16 |
+
METRICS_PATH = ARTIFACTS_DIR / "metrics.json"
|
| 17 |
+
|
| 18 |
+
#: Where ``--save-plots`` puts the PNGs.
|
| 19 |
+
PLOTS_DIR = ARTIFACTS_DIR / PLOTS_SUBDIR
|
| 20 |
+
|
| 21 |
+
#: Stable keys the web app asks for -> the file names the pipeline writes.
|
| 22 |
+
PLOT_FILES = {
|
| 23 |
+
"sentiment_distribution_raw": "01_sentiment_distribution_raw.png",
|
| 24 |
+
"sentiment_distribution_balanced": "02_sentiment_distribution_balanced.png",
|
| 25 |
+
"review_length": "03_review_length.png",
|
| 26 |
+
"top_tokens": "04_top_tokens.png",
|
| 27 |
+
"model_comparison": "05_model_comparison.png",
|
| 28 |
+
"confusion_matrix": "06_confusion_matrix.png",
|
| 29 |
+
"per_class_metrics": "07_per_class_metrics.png",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class EmptyAfterCleaning(ValueError):
|
| 34 |
+
"""Raised when cleaning leaves nothing for the model to classify.
|
| 35 |
+
Happens when the input has no Arabic content at all - the ``Removing_non_arabic`` step strips it to an empty string.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@lru_cache(maxsize=1)
|
| 40 |
+
def get_model():
|
| 41 |
+
"""Load the saved pipeline once and reuse it for every request."""
|
| 42 |
+
return load_model(DEFAULT_MODEL_PATH)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def describe_model():
|
| 46 |
+
"""Report what the served pipeline actually is."""
|
| 47 |
+
pipeline = get_model()
|
| 48 |
+
classifier = pipeline.named_steps["clf"]
|
| 49 |
+
vectorizer = pipeline.named_steps["vect"]
|
| 50 |
+
|
| 51 |
+
return {
|
| 52 |
+
"name": type(classifier).__name__,
|
| 53 |
+
"classes": [str(label) for label in classifier.classes_],
|
| 54 |
+
"vocabulary_size": len(vectorizer.vocabulary_),
|
| 55 |
+
"supports_probabilities": hasattr(pipeline, "predict_proba"),
|
| 56 |
+
"supports_margins": hasattr(pipeline, "decision_function"),
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _class_scores(pipeline, cleaned):
|
| 61 |
+
"""Per-class scores for one cleaned string, or ``None`` if unavailable.
|
| 62 |
+
|
| 63 |
+
Two different things can come back, and the caller must not conflate them:
|
| 64 |
+
|
| 65 |
+
``probabilities``
|
| 66 |
+
Real calibrated probabilities summing to 1, from ``predict_proba``.
|
| 67 |
+
Available for MultinomialNB / LogisticRegression / RandomForest.
|
| 68 |
+
|
| 69 |
+
``margins``
|
| 70 |
+
Signed distances from ``decision_function``. ``SVC`` has these but no
|
| 71 |
+
probabilities. They rank the classes but are *not* percentages, so the
|
| 72 |
+
UI labels them as margins and never renders them as a confidence.
|
| 73 |
+
"""
|
| 74 |
+
if hasattr(pipeline, "predict_proba"):
|
| 75 |
+
scores = pipeline.predict_proba([cleaned])[0]
|
| 76 |
+
return "probabilities", {
|
| 77 |
+
str(label): float(score)
|
| 78 |
+
for label, score in zip(pipeline.classes_, scores)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if hasattr(pipeline, "decision_function"):
|
| 82 |
+
scores = pipeline.decision_function([cleaned])[0]
|
| 83 |
+
return "margins", {
|
| 84 |
+
str(label): float(score)
|
| 85 |
+
for label, score in zip(pipeline.classes_, scores)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
return None, None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _find_negations(text):
|
| 92 |
+
"""Negation particles present in the raw input.
|
| 93 |
+
|
| 94 |
+
``KEEP_NEGATIONS`` is on, so these survive stopword removal - which is the
|
| 95 |
+
whole point, since dropping them would turn "لا احب" into "احب".
|
| 96 |
+
"""
|
| 97 |
+
tokens = set(text.split())
|
| 98 |
+
return sorted(tokens & NEGATION_WORDS)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _find_emojis(text):
|
| 102 |
+
"""Emoji in the raw input paired with the Arabic word they are replaced by."""
|
| 103 |
+
found = []
|
| 104 |
+
seen = set()
|
| 105 |
+
for character in text:
|
| 106 |
+
if character in EMOJI_TO_ARABIC and character not in seen:
|
| 107 |
+
seen.add(character)
|
| 108 |
+
found.append({"emoji": character, "arabic": EMOJI_TO_ARABIC[character]})
|
| 109 |
+
return found
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def analyze(text, convert_emojis=True):
|
| 113 |
+
"""Clean ``text``, classify it, and report everything worth showing.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
text: raw user input.
|
| 117 |
+
convert_emojis: run the emoji substitution steps first. ``True`` matches how the training corpus was built.
|
| 118 |
+
"""
|
| 119 |
+
pipeline = get_model()
|
| 120 |
+
original = text.strip()
|
| 121 |
+
cleaned = clean_text(original, convert_emojis=convert_emojis).strip()
|
| 122 |
+
|
| 123 |
+
if not cleaned:
|
| 124 |
+
raise EmptyAfterCleaning(
|
| 125 |
+
"Nothing left after cleaning - the input has no Arabic content."
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
label = str(pipeline.predict([cleaned])[0])
|
| 129 |
+
score_kind, scores = _class_scores(pipeline, cleaned)
|
| 130 |
+
|
| 131 |
+
word_count = len(original.split())
|
| 132 |
+
cleaned_word_count = len(cleaned.split())
|
| 133 |
+
|
| 134 |
+
notes = []
|
| 135 |
+
if word_count < MIN_WORDS_PER_SENTENCE:
|
| 136 |
+
notes.append(
|
| 137 |
+
f"Only {word_count} word(s). Reviews shorter than "
|
| 138 |
+
f"{MIN_WORDS_PER_SENTENCE} words were dropped from the training "
|
| 139 |
+
f"corpus, so this is outside what the model learned from."
|
| 140 |
+
)
|
| 141 |
+
if cleaned_word_count < MIN_WORDS_PER_SENTENCE <= word_count:
|
| 142 |
+
notes.append(
|
| 143 |
+
f"Cleaning reduced this to {cleaned_word_count} word(s). Most of the "
|
| 144 |
+
f"input was stopwords, punctuation or non-Arabic characters."
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"label": label,
|
| 149 |
+
"model": type(pipeline.named_steps["clf"]).__name__,
|
| 150 |
+
"original_text": original,
|
| 151 |
+
"cleaned_text": cleaned,
|
| 152 |
+
"word_count": word_count,
|
| 153 |
+
"cleaned_word_count": cleaned_word_count,
|
| 154 |
+
"score_kind": score_kind,
|
| 155 |
+
"scores": scores,
|
| 156 |
+
"negations_found": _find_negations(original),
|
| 157 |
+
"emojis_found": _find_emojis(original),
|
| 158 |
+
"notes": notes,
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def load_metrics():
|
| 163 |
+
"""Read ``artifacts/metrics.json``, or return an unavailable placeholder.
|
| 164 |
+
The file is written by ``run_pipeline.py``.
|
| 165 |
+
"""
|
| 166 |
+
available_plots = {
|
| 167 |
+
key: name
|
| 168 |
+
for key, name in PLOT_FILES.items()
|
| 169 |
+
if (PLOTS_DIR / name).is_file()
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if not METRICS_PATH.is_file():
|
| 173 |
+
return {
|
| 174 |
+
"available": False,
|
| 175 |
+
"best_model": None,
|
| 176 |
+
"selected_by": None,
|
| 177 |
+
"generated_at": None,
|
| 178 |
+
"dataset": None,
|
| 179 |
+
"models": [],
|
| 180 |
+
"plots": available_plots,
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
stored = json.loads(METRICS_PATH.read_text(encoding="utf-8"))
|
| 184 |
+
stored["available"] = True
|
| 185 |
+
stored.setdefault("plots", {})
|
| 186 |
+
# Trust the filesystem over the manifest: charts can be deleted or regenerated without rerunning training.
|
| 187 |
+
stored["plots"] = available_plots or stored["plots"]
|
| 188 |
+
return stored
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def plot_path(name):
|
| 192 |
+
"""Absolute path of a chart PNG, or ``None`` if it is not a known chart.
|
| 193 |
+
Only names listed in :data:`PLOT_FILES` resolve, so a request cannot walk
|
| 194 |
+
out of the plots directory.
|
| 195 |
+
"""
|
| 196 |
+
if name not in PLOT_FILES.values():
|
| 197 |
+
return None
|
| 198 |
+
candidate = PLOTS_DIR / name
|
| 199 |
+
return candidate if candidate.is_file() else None
|
config/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared configuration: paths, constants, emoji maps and stopwords."""
|
| 2 |
+
|
| 3 |
+
from config.constants import (
|
| 4 |
+
EMOJI_COUNT_COLUMN,
|
| 5 |
+
KEEP_NEGATIONS,
|
| 6 |
+
LABEL_COLUMN,
|
| 7 |
+
NEGATION_WORDS,
|
| 8 |
+
RANDOM_STATE,
|
| 9 |
+
RATING_TO_SENTIMENT,
|
| 10 |
+
SENTIMENT_PALETTE,
|
| 11 |
+
TEST_SIZE,
|
| 12 |
+
TEXT_COLUMN,
|
| 13 |
+
)
|
| 14 |
+
from config.emoji_map import emojis, emoticons_to_emoji
|
| 15 |
+
from config.paths import (
|
| 16 |
+
ARTIFACTS_DIR,
|
| 17 |
+
DEFAULT_CLEANED_DATA_PATH,
|
| 18 |
+
DEFAULT_MODEL_PATH,
|
| 19 |
+
DEFAULT_RAW_DATA_PATH,
|
| 20 |
+
DEFAULT_SHARING_DATA_PATH,
|
| 21 |
+
ORIGINAL_DIR,
|
| 22 |
+
PROJECT_ROOT,
|
| 23 |
+
)
|
| 24 |
+
from config.stopwords import get_arabic_stopwords
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
"ARTIFACTS_DIR",
|
| 28 |
+
"DEFAULT_CLEANED_DATA_PATH",
|
| 29 |
+
"DEFAULT_MODEL_PATH",
|
| 30 |
+
"DEFAULT_RAW_DATA_PATH",
|
| 31 |
+
"DEFAULT_SHARING_DATA_PATH",
|
| 32 |
+
"EMOJI_COUNT_COLUMN",
|
| 33 |
+
"KEEP_NEGATIONS",
|
| 34 |
+
"LABEL_COLUMN",
|
| 35 |
+
"NEGATION_WORDS",
|
| 36 |
+
"ORIGINAL_DIR",
|
| 37 |
+
"PROJECT_ROOT",
|
| 38 |
+
"RANDOM_STATE",
|
| 39 |
+
"RATING_TO_SENTIMENT",
|
| 40 |
+
"SENTIMENT_PALETTE",
|
| 41 |
+
"TEST_SIZE",
|
| 42 |
+
"TEXT_COLUMN",
|
| 43 |
+
"emojis",
|
| 44 |
+
"emoticons_to_emoji",
|
| 45 |
+
"get_arabic_stopwords",
|
| 46 |
+
]
|
config/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.02 kB). View file
|
|
|
config/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (1.01 kB). View file
|
|
|
config/__pycache__/emoji_map.cpython-312.pyc
ADDED
|
Binary file (11.4 kB). View file
|
|
|
config/__pycache__/paths.cpython-312.pyc
ADDED
|
Binary file (1.23 kB). View file
|
|
|
config/__pycache__/stopwords.cpython-312.pyc
ADDED
|
Binary file (1.26 kB). View file
|
|
|
config/constants.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TEXT_COLUMN = "text"
|
| 2 |
+
LABEL_COLUMN = "label"
|
| 3 |
+
EMOJI_COUNT_COLUMN = "emoji_count"
|
| 4 |
+
RAW_COLUMNS_TO_DROP = ["Unnamed: 0", "company"]
|
| 5 |
+
RATING_COLUMN = "rating"
|
| 6 |
+
RAW_TEXT_COLUMN = "Feed"
|
| 7 |
+
RAW_TEXT_COLUMN_CANDIDATES = ("Feed", "review_description")
|
| 8 |
+
SENTIMENT_COLUMN = "Sentiment"
|
| 9 |
+
RATING_TO_SENTIMENT = {1: "Positive", 0: "Neutral", -1: "Negative"}
|
| 10 |
+
SENTIMENT_PALETTE = {"Positive": "green", "Neutral": "gray", "Negative": "red"}
|
| 11 |
+
TEST_SIZE = 0.2
|
| 12 |
+
RANDOM_STATE = 42
|
| 13 |
+
MIN_WORDS_PER_SENTENCE = 3
|
| 14 |
+
|
| 15 |
+
# Arabic negation particles that NLTK lists as stopwords.
|
| 16 |
+
# Removing these destroys polarity - "لا احب" ("I don't like") collapses to "احب" ("I like")
|
| 17 |
+
# They are kept in the text when :data:`KEEP_NEGATIONS` is on.
|
| 18 |
+
NEGATION_WORDS = frozenset({
|
| 19 |
+
"لا", # no / not
|
| 20 |
+
"ما", # not (also the relative "what")
|
| 21 |
+
"لم", # did not
|
| 22 |
+
"لن", # will not
|
| 23 |
+
"غير", # not / other than
|
| 24 |
+
"ليس", # is not
|
| 25 |
+
"ليست", # is not (f.)
|
| 26 |
+
"ليسا", # are not (dual)
|
| 27 |
+
"ليسوا", # are not (pl.)
|
| 28 |
+
"لست", # you/I am not
|
| 29 |
+
"ولا", # and not / nor
|
| 30 |
+
"وما", # and not
|
| 31 |
+
"دون", # without
|
| 32 |
+
"أبدا", # never / at all
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
# Keep the words in :data:`NEGATION_WORDS` instead of filtering them out.
|
| 36 |
+
# Changing this changes the training corpus, so the model must be retrained to match.
|
| 37 |
+
KEEP_NEGATIONS = True
|
config/emoji_map.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Emoji / emoticon lookup tables.
|
| 2 |
+
|
| 3 |
+
``emojis`` -- emoji character -> Arabic word(s).
|
| 4 |
+
``emoticons_to_emoji`` -- ASCII emoticon -> emoji character (applied first).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
emojis = {
|
| 8 |
+
"🙂":"يبتسم",
|
| 9 |
+
"😂":"يضحك",
|
| 10 |
+
"💔":"قلب حزين",
|
| 11 |
+
"🙂":"يبتسم",
|
| 12 |
+
"❤️":"حب",
|
| 13 |
+
"❤":"حب",
|
| 14 |
+
"😍":"حب",
|
| 15 |
+
"😭":"يبكي",
|
| 16 |
+
"😢":"حزن",
|
| 17 |
+
"😔":"حزن",
|
| 18 |
+
"♥":"حب",
|
| 19 |
+
"💜":"حب",
|
| 20 |
+
"😅":"يضحك",
|
| 21 |
+
"🙁":"حزين",
|
| 22 |
+
"💕":"حب",
|
| 23 |
+
"💙":"حب",
|
| 24 |
+
"😞":"حزين",
|
| 25 |
+
"😊":"سعادة",
|
| 26 |
+
"👏":"يصفق",
|
| 27 |
+
"👌":"احسنت",
|
| 28 |
+
"😴":"ينام",
|
| 29 |
+
"😀":"يضحك",
|
| 30 |
+
"😌":"حزين",
|
| 31 |
+
"🌹":"وردة",
|
| 32 |
+
"🙈":"حب",
|
| 33 |
+
"😄":"يضحك",
|
| 34 |
+
"😐":"محايد",
|
| 35 |
+
"✌":"منتصر",
|
| 36 |
+
"✨":"نجمه",
|
| 37 |
+
"🤔":"تفكير",
|
| 38 |
+
"😏":"يستهزء",
|
| 39 |
+
"😒":"يستهزء",
|
| 40 |
+
"🙄":"ملل",
|
| 41 |
+
"😕":"عصبية",
|
| 42 |
+
"😃":"يضحك",
|
| 43 |
+
"🌸":"وردة",
|
| 44 |
+
"😓":"حزن",
|
| 45 |
+
"💞":"حب",
|
| 46 |
+
"💗":"حب",
|
| 47 |
+
"😑":"منزعج",
|
| 48 |
+
"💭":"تفكير",
|
| 49 |
+
"😎":"ثقة",
|
| 50 |
+
"💛":"حب",
|
| 51 |
+
"😩":"حزين",
|
| 52 |
+
"💪":"عضلات",
|
| 53 |
+
"👍":"موافق",
|
| 54 |
+
"🙏🏻":"رجاء طلب",
|
| 55 |
+
"😳":"مصدوم",
|
| 56 |
+
"👏🏼":"تصفيق",
|
| 57 |
+
"🎶":"موسيقي",
|
| 58 |
+
"🌚":"صمت",
|
| 59 |
+
"💚":"حب",
|
| 60 |
+
"🙏":"رجاء طلب",
|
| 61 |
+
"💘":"حب",
|
| 62 |
+
"🍃":"سلام",
|
| 63 |
+
"☺":"يضحك",
|
| 64 |
+
"🐸":"ضفدع",
|
| 65 |
+
"😶":"مصدوم",
|
| 66 |
+
"✌️":"مرح",
|
| 67 |
+
"✋🏻":"توقف",
|
| 68 |
+
"😉":"غمزة",
|
| 69 |
+
"🌷":"حب",
|
| 70 |
+
"🙃":"مبتسم",
|
| 71 |
+
"😫":"حزين",
|
| 72 |
+
"😨":"مصدوم",
|
| 73 |
+
"🎼 ":"موسيقي",
|
| 74 |
+
"🍁":"مرح",
|
| 75 |
+
"🍂":"مرح",
|
| 76 |
+
"💟":"حب",
|
| 77 |
+
"😪":"حزن",
|
| 78 |
+
"😆":"يضحك",
|
| 79 |
+
"😣":"استياء",
|
| 80 |
+
"☺️":"حب",
|
| 81 |
+
"😱":"كارثة",
|
| 82 |
+
"😁":"يضحك",
|
| 83 |
+
"😖":"استياء",
|
| 84 |
+
"🏃🏼":"يجري",
|
| 85 |
+
"😡":"غضب",
|
| 86 |
+
"🚶":"يسير",
|
| 87 |
+
"🤕":"مرض",
|
| 88 |
+
"‼️":"تعجب",
|
| 89 |
+
"🕊":"طائر",
|
| 90 |
+
"👌🏻":"احسنت",
|
| 91 |
+
"❣":"حب",
|
| 92 |
+
"🙊":"مصدوم",
|
| 93 |
+
"💃":"سعادة مرح",
|
| 94 |
+
"💃🏼":"سعادة مرح",
|
| 95 |
+
"😜":"مرح",
|
| 96 |
+
"👊":"ضربة",
|
| 97 |
+
"😟":"استياء",
|
| 98 |
+
"💖":"حب",
|
| 99 |
+
"😥":"حزن",
|
| 100 |
+
"🎻":"موسيقي",
|
| 101 |
+
"✒":"يكتب",
|
| 102 |
+
"🚶🏻":"يسير",
|
| 103 |
+
"💎":"الماظ",
|
| 104 |
+
"😷":"وباء مرض",
|
| 105 |
+
"☝":"واحد",
|
| 106 |
+
"🚬":"تدخين",
|
| 107 |
+
"💐" : "ورد",
|
| 108 |
+
"🌞" : "شمس",
|
| 109 |
+
"👆" : "الاول",
|
| 110 |
+
"⚠️" :"تحذير",
|
| 111 |
+
"🤗" : "احتواء",
|
| 112 |
+
"✖️": "غلط",
|
| 113 |
+
"📍" : "مكان",
|
| 114 |
+
"👸" : "ملكه",
|
| 115 |
+
"👑" : "تاج",
|
| 116 |
+
"✔️" : "صح",
|
| 117 |
+
"💌": "قلب",
|
| 118 |
+
"😲" : "مندهش",
|
| 119 |
+
"💦": "ماء",
|
| 120 |
+
"🚫" : "خطا",
|
| 121 |
+
"👏🏻" : "برافو",
|
| 122 |
+
"🏊" :"يسبح",
|
| 123 |
+
"👍🏻": "تمام",
|
| 124 |
+
"⭕️" :"دائره كبيره",
|
| 125 |
+
"🎷" : "ساكسفون",
|
| 126 |
+
"👋": "تلويح باليد",
|
| 127 |
+
"✌🏼": "علامه النصر",
|
| 128 |
+
"🌝":"مبتسم",
|
| 129 |
+
"➿" : "عقده مزدوجه",
|
| 130 |
+
"💪🏼" : "قوي",
|
| 131 |
+
"📩": "تواصل معي",
|
| 132 |
+
"☕️": "قهوه",
|
| 133 |
+
"😧" : "قلق و صدمة",
|
| 134 |
+
"🗨": "رسالة",
|
| 135 |
+
"❗️" :"تعجب",
|
| 136 |
+
"🙆🏻": "اشاره موافقه",
|
| 137 |
+
"👯" :"اخوات",
|
| 138 |
+
"©" : "رمز",
|
| 139 |
+
"👵🏽" :"سيده عجوزه",
|
| 140 |
+
"🐣": "كتكوت",
|
| 141 |
+
"🙌": "تشجيع",
|
| 142 |
+
"🙇": "شخص ينحني",
|
| 143 |
+
"👐🏽":"ايدي مفتوحه",
|
| 144 |
+
"👌🏽": "بالظبط",
|
| 145 |
+
"⁉️" : "استنكار",
|
| 146 |
+
"⚽️": "كوره",
|
| 147 |
+
"🕶" :"حب",
|
| 148 |
+
"🎈" :"بالون",
|
| 149 |
+
"🎀": "ورده",
|
| 150 |
+
"💵": "فلوس",
|
| 151 |
+
"😋": "جائع",
|
| 152 |
+
"😛": "يغيظ",
|
| 153 |
+
"😠": "غاضب",
|
| 154 |
+
"✍🏻": "يكتب",
|
| 155 |
+
"🌾": "ارز",
|
| 156 |
+
"👣": "اثر قدمين",
|
| 157 |
+
"❌":"رفض",
|
| 158 |
+
"🍟":"طعام",
|
| 159 |
+
"👬":"صداقة",
|
| 160 |
+
"🐰":"ارنب",
|
| 161 |
+
"☂":"مطر",
|
| 162 |
+
"⚜":"مملكة فرنسا",
|
| 163 |
+
"🐑":"خروف",
|
| 164 |
+
"🗣":"صوت مرتفع",
|
| 165 |
+
"👌🏼":"احسنت",
|
| 166 |
+
"☘":"مرح",
|
| 167 |
+
"😮":"صدمة",
|
| 168 |
+
"😦":"قلق",
|
| 169 |
+
"⭕":"الحق",
|
| 170 |
+
"✏️":"قلم",
|
| 171 |
+
"ℹ":"معلومات",
|
| 172 |
+
"🙍🏻":"رفض",
|
| 173 |
+
"⚪️":"نضارة نقاء",
|
| 174 |
+
"🐤":"حزن",
|
| 175 |
+
"💫":"مرح",
|
| 176 |
+
"💝":"حب",
|
| 177 |
+
"🍔":"طعام",
|
| 178 |
+
"❤︎":"حب",
|
| 179 |
+
"✈️":"سفر",
|
| 180 |
+
"🏃🏻♀️":"يسير",
|
| 181 |
+
"🍳":"ذكر",
|
| 182 |
+
"🎤":"مايك غناء",
|
| 183 |
+
"🎾":"كره",
|
| 184 |
+
"🐔":"دجاجة",
|
| 185 |
+
"🙋":"سؤال",
|
| 186 |
+
"📮":"بحر",
|
| 187 |
+
"💉":"دواء",
|
| 188 |
+
"🙏🏼":"رجاء طلب",
|
| 189 |
+
"💂🏿 ":"حارس",
|
| 190 |
+
"🎬":"سينما",
|
| 191 |
+
"♦️":"مرح",
|
| 192 |
+
"💡":"قكرة",
|
| 193 |
+
"‼":"تعجب",
|
| 194 |
+
"👼":"طفل",
|
| 195 |
+
"🔑":"مفتاح",
|
| 196 |
+
"♥️":"حب",
|
| 197 |
+
"🕋":"كعبة",
|
| 198 |
+
"🐓":"دجاجة",
|
| 199 |
+
"💩":"معترض",
|
| 200 |
+
"👽":"فضائي",
|
| 201 |
+
"☔️":"مطر",
|
| 202 |
+
"🍷":"عصير",
|
| 203 |
+
"🌟":"نجمة",
|
| 204 |
+
"☁️":"سحب",
|
| 205 |
+
"👃":"معترض",
|
| 206 |
+
"🌺":"مرح",
|
| 207 |
+
"🔪":"سكينة",
|
| 208 |
+
"♨":"سخونية",
|
| 209 |
+
"👊🏼":"ضرب",
|
| 210 |
+
"✏":"قلم",
|
| 211 |
+
"🚶🏾♀️":"يسير",
|
| 212 |
+
"👊":"ضربة",
|
| 213 |
+
"◾️":"وقف",
|
| 214 |
+
"😚":"حب",
|
| 215 |
+
"🔸":"مرح",
|
| 216 |
+
"👎🏻":"لا يعجبني",
|
| 217 |
+
"👊🏽":"ضربة",
|
| 218 |
+
"😙":"حب",
|
| 219 |
+
"🎥":"تصوير",
|
| 220 |
+
"👉":"جذب انتباه",
|
| 221 |
+
"👏🏽":"يصفق",
|
| 222 |
+
"💪🏻":"عضلات",
|
| 223 |
+
"🏴":"اسود",
|
| 224 |
+
"🔥":"حريق",
|
| 225 |
+
"😬":"عدم الراحة",
|
| 226 |
+
"👊🏿":"يضرب",
|
| 227 |
+
"🌿":"ورقه شجره",
|
| 228 |
+
"✋🏼":"كف ايد",
|
| 229 |
+
"👐":"ايدي مفتوحه",
|
| 230 |
+
"☠️":"وجه مرعب",
|
| 231 |
+
"🎉":"يهنئ",
|
| 232 |
+
"🔕" :"صامت",
|
| 233 |
+
"😿":"وجه حزين",
|
| 234 |
+
"☹️":"وجه يائس",
|
| 235 |
+
"😘" :"حب",
|
| 236 |
+
"😰" :"خوف و حزن",
|
| 237 |
+
"🌼":"ورده",
|
| 238 |
+
"💋": "بوسه",
|
| 239 |
+
"👇":"لاسفل",
|
| 240 |
+
"❣️":"حب",
|
| 241 |
+
"🎧":"سماعات",
|
| 242 |
+
"📝":"يكتب",
|
| 243 |
+
"😇":"دايخ",
|
| 244 |
+
"😈":"رعب",
|
| 245 |
+
"🏃":"يجري",
|
| 246 |
+
"✌🏻":"علامه النصر",
|
| 247 |
+
"🔫":"يضرب",
|
| 248 |
+
"❗️":"تعجب",
|
| 249 |
+
"👎":"غير موافق",
|
| 250 |
+
"🔐":"قفل",
|
| 251 |
+
"👈":"لليمين",
|
| 252 |
+
"™":"رمز",
|
| 253 |
+
"🚶🏽":"يتمشي",
|
| 254 |
+
"😯":"متفاجأ",
|
| 255 |
+
"✊":"يد مغلقه",
|
| 256 |
+
"😻":"اعجاب",
|
| 257 |
+
"🙉" :"قرد",
|
| 258 |
+
"👧":"طفله صغيره",
|
| 259 |
+
"🔴":"دائره حمراء",
|
| 260 |
+
"💪🏽":"قوه",
|
| 261 |
+
"💤":"ينام",
|
| 262 |
+
"👀":"ينظر",
|
| 263 |
+
"✍🏻":"يكتب",
|
| 264 |
+
"❄️":"تلج",
|
| 265 |
+
"💀":"رعب",
|
| 266 |
+
"😤":"وجه عابس",
|
| 267 |
+
"🖋":"قلم",
|
| 268 |
+
"🎩":"كاب",
|
| 269 |
+
"☕️":"قهوه",
|
| 270 |
+
"😹":"ضحك",
|
| 271 |
+
"💓":"حب",
|
| 272 |
+
"☄️ ":"نار",
|
| 273 |
+
"👻":"رعب",
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
emoticons_to_emoji = {
|
| 277 |
+
":)" : "🙂",
|
| 278 |
+
":(" : "🙁",
|
| 279 |
+
"xD" : "😆",
|
| 280 |
+
":=(": "😭",
|
| 281 |
+
":'(": "😢",
|
| 282 |
+
":'‑(": "😢",
|
| 283 |
+
"XD" : "😂",
|
| 284 |
+
":D" : "🙂",
|
| 285 |
+
"♬" : "موسيقي",
|
| 286 |
+
"♡" : "❤",
|
| 287 |
+
"☻" : "🙂",
|
| 288 |
+
}
|
config/paths.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Filesystem locations used by the pipeline."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 7 |
+
|
| 8 |
+
ORIGINAL_DIR = PROJECT_ROOT / "original"
|
| 9 |
+
|
| 10 |
+
DEFAULT_RAW_DATA_PATH = Path(
|
| 11 |
+
os.getenv("ASA_RAW_DATA_PATH", ORIGINAL_DIR / "original_dataset.xlsx")
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
ARTIFACTS_DIR = Path(os.getenv("ASA_ARTIFACTS_DIR", PROJECT_ROOT / "artifacts"))
|
| 15 |
+
|
| 16 |
+
DEFAULT_SHARING_DATA_PATH = Path(
|
| 17 |
+
os.getenv("ASA_SHARING_DATA_PATH", ARTIFACTS_DIR / "sharing_arabic.xlsx")
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
DEFAULT_CLEANED_DATA_PATH = Path(
|
| 21 |
+
os.getenv("ASA_CLEANED_DATA_PATH", ARTIFACTS_DIR / "cleaned.xlsx")
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
DEFAULT_MODEL_PATH = Path(
|
| 25 |
+
os.getenv("ASA_MODEL_PATH", ARTIFACTS_DIR / "best_model.joblib")
|
| 26 |
+
)
|
config/stopwords.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Arabic stopword list used by :func:`preprocessing.text_cleaning.remove_stop_words`."""
|
| 2 |
+
|
| 3 |
+
import nltk
|
| 4 |
+
from nltk.corpus import stopwords
|
| 5 |
+
|
| 6 |
+
from config.constants import KEEP_NEGATIONS, NEGATION_WORDS
|
| 7 |
+
|
| 8 |
+
_cache = {}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_arabic_stopwords(keep_negations=None):
|
| 12 |
+
"""Return the NLTK Arabic stopword list.
|
| 13 |
+
Downloads the corpus on first use and caches the result per setting.
|
| 14 |
+
"""
|
| 15 |
+
if keep_negations is None:
|
| 16 |
+
keep_negations = KEEP_NEGATIONS
|
| 17 |
+
|
| 18 |
+
if keep_negations not in _cache:
|
| 19 |
+
try:
|
| 20 |
+
nltk.data.find("corpora/stopwords")
|
| 21 |
+
except LookupError:
|
| 22 |
+
nltk.download("stopwords")
|
| 23 |
+
|
| 24 |
+
words = stopwords.words("arabic")
|
| 25 |
+
if keep_negations:
|
| 26 |
+
words = [w for w in words if w not in NEGATION_WORDS]
|
| 27 |
+
_cache[keep_negations] = words
|
| 28 |
+
|
| 29 |
+
return _cache[keep_negations]
|
inference/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loading a saved model and predicting on new text."""
|
| 2 |
+
|
| 3 |
+
from inference.predict import get_default_model, predict_sentiment
|
| 4 |
+
|
| 5 |
+
__all__ = ["get_default_model", "predict_sentiment"]
|
inference/predict.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sentiment prediction for new, unseen text."""
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
from config.constants import TEXT_COLUMN
|
| 6 |
+
from config.paths import DEFAULT_MODEL_PATH
|
| 7 |
+
from preprocessing.pipeline import preprocess_dataframe
|
| 8 |
+
from training.model_io import load_model
|
| 9 |
+
|
| 10 |
+
_default_model = None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_default_model(path=DEFAULT_MODEL_PATH):
|
| 14 |
+
"""Load (and cache) the saved best model."""
|
| 15 |
+
global _default_model
|
| 16 |
+
if _default_model is None:
|
| 17 |
+
_default_model = load_model(path)
|
| 18 |
+
return _default_model
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def predict_sentiment(text, model=None, convert_emojis=True):
|
| 22 |
+
"""Predict the sentiment of a single piece of raw text.
|
| 23 |
+
|
| 24 |
+
The text goes through the same cleaning chain the training corpus went
|
| 25 |
+
through - emoticons to emojis, emojis to Arabic words, remove stopwords,
|
| 26 |
+
remove non-Arabic characters, normalize, remove numbers, remove
|
| 27 |
+
hashtags/mentions, remove URLs, remove punctuation, light-stem - and is
|
| 28 |
+
then handed to the fitted TF-IDF + classifier pipeline.
|
| 29 |
+
"""
|
| 30 |
+
if model is None:
|
| 31 |
+
model = get_default_model()
|
| 32 |
+
|
| 33 |
+
# Create a DataFrame with the input text
|
| 34 |
+
text = [text]
|
| 35 |
+
daf = pd.DataFrame({TEXT_COLUMN: text})
|
| 36 |
+
|
| 37 |
+
# Preprocess the text
|
| 38 |
+
daf = preprocess_dataframe(daf, text_column=TEXT_COLUMN,
|
| 39 |
+
convert_emojis=convert_emojis,
|
| 40 |
+
drop_small_sentences=False)
|
| 41 |
+
|
| 42 |
+
# Make predictions
|
| 43 |
+
predictions = model.predict(daf[TEXT_COLUMN])
|
| 44 |
+
|
| 45 |
+
return predictions
|
models/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One sub-package per classifier, plus the registry that collects them."""
|
| 2 |
+
|
| 3 |
+
from models import logistic_regression, naive_bayes, random_forest, svm
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"build_models_dict",
|
| 7 |
+
"logistic_regression",
|
| 8 |
+
"naive_bayes",
|
| 9 |
+
"random_forest",
|
| 10 |
+
"svm",
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def build_models_dict():
|
| 15 |
+
from models.registry import build_models_dict as _build_models_dict
|
| 16 |
+
|
| 17 |
+
return _build_models_dict()
|
models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (608 Bytes). View file
|
|
|
models/__pycache__/registry.cpython-312.pyc
ADDED
|
Binary file (752 Bytes). View file
|
|
|
models/logistic_regression/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logistic Regression classifier."""
|
| 2 |
+
|
| 3 |
+
from models.logistic_regression.model import MODEL_NAME, build_model
|
| 4 |
+
|
| 5 |
+
__all__ = ["MODEL_NAME", "build_model"]
|
models/logistic_regression/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (355 Bytes). View file
|
|
|
models/logistic_regression/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (495 Bytes). View file
|
|
|
models/logistic_regression/model.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logistic Regression configuration."""
|
| 2 |
+
|
| 3 |
+
from sklearn.linear_model import LogisticRegression
|
| 4 |
+
|
| 5 |
+
MODEL_NAME = "LogisticRegression"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def build_model():
|
| 9 |
+
"""Return an untrained ``LogisticRegression``."""
|
| 10 |
+
return LogisticRegression()
|
models/naive_bayes/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multinomial Naive Bayes classifier."""
|
| 2 |
+
|
| 3 |
+
from models.naive_bayes.model import MODEL_NAME, build_model
|
| 4 |
+
|
| 5 |
+
__all__ = ["MODEL_NAME", "build_model"]
|
models/naive_bayes/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (343 Bytes). View file
|
|
|
models/naive_bayes/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (479 Bytes). View file
|
|
|
models/naive_bayes/model.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Multinomial Naive Bayes configuration."""
|
| 2 |
+
|
| 3 |
+
from sklearn.naive_bayes import MultinomialNB
|
| 4 |
+
|
| 5 |
+
MODEL_NAME = "MultinomialNB"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def build_model():
|
| 9 |
+
"""Return an untrained ``MultinomialNB``."""
|
| 10 |
+
return MultinomialNB()
|
models/random_forest/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Random Forest classifier."""
|
| 2 |
+
|
| 3 |
+
from models.random_forest.model import MODEL_NAME, build_model
|
| 4 |
+
|
| 5 |
+
__all__ = ["MODEL_NAME", "build_model"]
|
models/random_forest/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (337 Bytes). View file
|
|
|
models/random_forest/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (487 Bytes). View file
|
|
|
models/random_forest/model.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Random Forest configuration."""
|
| 2 |
+
|
| 3 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 4 |
+
|
| 5 |
+
MODEL_NAME = "RandomForestClassifier"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def build_model():
|
| 9 |
+
"""Return an untrained ``RandomForestClassifier``."""
|
| 10 |
+
return RandomForestClassifier()
|
models/registry.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Registry of the four classifiers compared by the pipeline."""
|
| 2 |
+
|
| 3 |
+
from models import logistic_regression, naive_bayes, random_forest, svm
|
| 4 |
+
|
| 5 |
+
MODEL_MODULES = (naive_bayes, random_forest, logistic_regression, svm)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def build_models_dict():
|
| 9 |
+
"""Return ``{model_name: untrained estimator}`` for every registered model."""
|
| 10 |
+
return {module.MODEL_NAME: module.build_model() for module in MODEL_MODULES}
|
models/svm/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Support Vector classifier."""
|
| 2 |
+
|
| 3 |
+
from models.svm.model import MODEL_NAME, build_model
|
| 4 |
+
|
| 5 |
+
__all__ = ["MODEL_NAME", "build_model"]
|
models/svm/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (318 Bytes). View file
|
|
|
models/svm/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (444 Bytes). View file
|
|
|
models/svm/model.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Support Vector Classifier configuration."""
|
| 2 |
+
|
| 3 |
+
from sklearn.svm import SVC
|
| 4 |
+
|
| 5 |
+
MODEL_NAME = "SVC"
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def build_model():
|
| 9 |
+
"""Return an untrained ``SVC``."""
|
| 10 |
+
return SVC()
|
preprocessing/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Text cleaning / normalization for Arabic sentiment analysis."""
|
| 2 |
+
|
| 3 |
+
from preprocessing.arabic_normalizer import Arabic_Light_Stemmer, normalizeArabic
|
| 4 |
+
from preprocessing.emoji_handler import (
|
| 5 |
+
emoji_counter,
|
| 6 |
+
extract_emoji,
|
| 7 |
+
remove_emoji,
|
| 8 |
+
replace_emojis_with_text,
|
| 9 |
+
replace_emoticon_with_emojis,
|
| 10 |
+
space_between_emojis,
|
| 11 |
+
)
|
| 12 |
+
from preprocessing.pipeline import (
|
| 13 |
+
CLEANING_STEPS,
|
| 14 |
+
EMOJI_STEPS,
|
| 15 |
+
clean_text,
|
| 16 |
+
preprocess_dataframe,
|
| 17 |
+
)
|
| 18 |
+
from preprocessing.text_cleaning import (
|
| 19 |
+
Removing_non_arabic,
|
| 20 |
+
Removing_numbers,
|
| 21 |
+
Removing_punctuations,
|
| 22 |
+
Removing_urls,
|
| 23 |
+
remove_extra_Space,
|
| 24 |
+
remove_hashtags_and_mentions,
|
| 25 |
+
remove_small_sentences,
|
| 26 |
+
remove_stop_words,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
__all__ = [
|
| 30 |
+
"Arabic_Light_Stemmer",
|
| 31 |
+
"CLEANING_STEPS",
|
| 32 |
+
"EMOJI_STEPS",
|
| 33 |
+
"Removing_non_arabic",
|
| 34 |
+
"Removing_numbers",
|
| 35 |
+
"Removing_punctuations",
|
| 36 |
+
"Removing_urls",
|
| 37 |
+
"clean_text",
|
| 38 |
+
"emoji_counter",
|
| 39 |
+
"extract_emoji",
|
| 40 |
+
"normalizeArabic",
|
| 41 |
+
"preprocess_dataframe",
|
| 42 |
+
"remove_emoji",
|
| 43 |
+
"remove_extra_Space",
|
| 44 |
+
"remove_hashtags_and_mentions",
|
| 45 |
+
"remove_small_sentences",
|
| 46 |
+
"remove_stop_words",
|
| 47 |
+
"replace_emojis_with_text",
|
| 48 |
+
"replace_emoticon_with_emojis",
|
| 49 |
+
"space_between_emojis",
|
| 50 |
+
]
|
preprocessing/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.14 kB). View file
|
|
|
preprocessing/__pycache__/arabic_normalizer.cpython-312.pyc
ADDED
|
Binary file (2.48 kB). View file
|
|
|
preprocessing/__pycache__/emoji_handler.cpython-312.pyc
ADDED
|
Binary file (3.55 kB). View file
|
|
|
preprocessing/__pycache__/pipeline.cpython-312.pyc
ADDED
|
Binary file (2.06 kB). View file
|
|
|
preprocessing/__pycache__/text_cleaning.cpython-312.pyc
ADDED
|
Binary file (3.76 kB). View file
|
|
|
preprocessing/arabic_normalizer.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Arabic-specific normalization and light stemming."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import warnings
|
| 5 |
+
|
| 6 |
+
with warnings.catch_warnings():
|
| 7 |
+
warnings.simplefilter("ignore", SyntaxWarning)
|
| 8 |
+
import pyarabic.araby as araby
|
| 9 |
+
from tashaphyne.stemming import ArabicLightStemmer
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def normalizeArabic(text):
|
| 13 |
+
"""Unify Arabic letter forms, collapse repetitions and strip diacritics/digits."""
|
| 14 |
+
text = text.strip()
|
| 15 |
+
text = re.sub("ى", "ي", text)
|
| 16 |
+
text = re.sub("ؤ", "ء", text)
|
| 17 |
+
text = re.sub("ئ", "ء", text)
|
| 18 |
+
text = re.sub("ة", "ه", text)
|
| 19 |
+
|
| 20 |
+
#remove repetetions
|
| 21 |
+
text = re.sub("[إأٱآا]", "ا", text)
|
| 22 |
+
text = text.replace('وو', 'و')
|
| 23 |
+
text = text.replace('يي', 'ي')
|
| 24 |
+
text = text.replace('ييي', 'ي')
|
| 25 |
+
text = text.replace('اا', 'ا')
|
| 26 |
+
|
| 27 |
+
## remove extra whitespace
|
| 28 |
+
text = re.sub(r'\s+', ' ', text)
|
| 29 |
+
|
| 30 |
+
# Remove longation
|
| 31 |
+
text = re.sub(r'(.)\1+', r"\1\1", text)
|
| 32 |
+
|
| 33 |
+
# Strip vowels from a text, include Shadda.
|
| 34 |
+
text = araby.strip_tashkeel(text)
|
| 35 |
+
|
| 36 |
+
# Strip diacritics from a text, include harakats and small lettres The striped marks are
|
| 37 |
+
text = araby.strip_diacritics(text)
|
| 38 |
+
text = ''.join([i for i in text if not i.isdigit()])
|
| 39 |
+
return text
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def Arabic_Light_Stemmer(text):
|
| 43 |
+
"""Light-stem every word of ``text`` with Tashaphyne's ``ArabicLightStemmer``."""
|
| 44 |
+
# Arabic Light Stemming is a specific type of stemming applied to Arabic words. Stemming is the process
|
| 45 |
+
# of reducing words to their root or base form, which helps in normalizing text for analysis.
|
| 46 |
+
Arabic_Stemmer = ArabicLightStemmer()
|
| 47 |
+
|
| 48 |
+
# stemming each word
|
| 49 |
+
text = [Arabic_Stemmer.light_stem(y) for y in text.split()]
|
| 50 |
+
|
| 51 |
+
return " ".join(text)
|
preprocessing/emoji_handler.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Emoji and emoticon handling."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
import emoji
|
| 6 |
+
import regex
|
| 7 |
+
|
| 8 |
+
from config.emoji_map import emojis, emoticons_to_emoji
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def extract_emoji(text):
|
| 12 |
+
"""Return the list of emoji grapheme clusters contained in ``text``."""
|
| 13 |
+
emoji_list = []
|
| 14 |
+
data = regex.findall(r'\X', text)
|
| 15 |
+
for word in data:
|
| 16 |
+
if any(emoji.distinct_emoji_list(char) for char in word):
|
| 17 |
+
emoji_list.append(word)
|
| 18 |
+
|
| 19 |
+
return emoji_list
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def emoji_counter(sentence):
|
| 23 |
+
"""Return the number of emojis in ``sentence`` (used for the EDA table)."""
|
| 24 |
+
return emoji.emoji_count(sentence)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def replace_emoticon_with_emojis(message):
|
| 28 |
+
"""Replace whitespace-delimited ASCII emoticons with their emoji equivalent."""
|
| 29 |
+
separate_words = message.split(' ')
|
| 30 |
+
modified_message = ""
|
| 31 |
+
|
| 32 |
+
for word in separate_words:
|
| 33 |
+
modified_message += emoticons_to_emoji.get(word, word) + " "
|
| 34 |
+
|
| 35 |
+
return modified_message.strip() # Remove trailing space
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def replace_emojis_with_text(message):
|
| 39 |
+
"""Replace each known emoji with the Arabic word(s) describing it."""
|
| 40 |
+
separate_words = regex.findall(r'\X', message)
|
| 41 |
+
modified_message = ""
|
| 42 |
+
|
| 43 |
+
for word in separate_words:
|
| 44 |
+
if any(emoji.distinct_emoji_list(char) for char in word):
|
| 45 |
+
modified_message += " " + emojis.get(word, word) + " "
|
| 46 |
+
else:
|
| 47 |
+
modified_message += emojis.get(word, word) + ""
|
| 48 |
+
|
| 49 |
+
return modified_message
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def remove_emoji(string):
|
| 53 |
+
"""Strip emoji characters from ``string`` (not part of the active pipeline)."""
|
| 54 |
+
emoji_pattern = re.compile("["
|
| 55 |
+
u"\U0001F600-\U0001F64F" # emoticons
|
| 56 |
+
u"\U0001F300-\U0001F5FF" # symbols & pictographs
|
| 57 |
+
u"\U0001F680-\U0001F6FF" # transport & map symbols
|
| 58 |
+
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
| 59 |
+
u"\U00002702-\U000027B0"
|
| 60 |
+
u"\U000024C2-\U0001F251"
|
| 61 |
+
"]+", flags=re.UNICODE)
|
| 62 |
+
return emoji_pattern.sub(r'', string).strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def space_between_emojis(s):
|
| 66 |
+
"""Pad every emoji with spaces (not part of the active pipeline)."""
|
| 67 |
+
return ''.join((' '+c+' ') if c in emoji.UNICODE_EMOJI['en'] else c for c in s)
|
preprocessing/pipeline.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The single preprocessing pipeline shared by training and inference.
|
| 2 |
+
|
| 3 |
+
Order of the steps:
|
| 4 |
+
1. ``replace_emoticon_with_emojis`` (emoji step)
|
| 5 |
+
2. ``replace_emojis_with_text`` (emoji step)
|
| 6 |
+
3. ``remove_stop_words``
|
| 7 |
+
4. ``Removing_non_arabic``
|
| 8 |
+
5. ``normalizeArabic``
|
| 9 |
+
6. ``Removing_numbers``
|
| 10 |
+
7. ``remove_hashtags_and_mentions``
|
| 11 |
+
8. ``Removing_urls``
|
| 12 |
+
9. ``Removing_punctuations``
|
| 13 |
+
10. ``Arabic_Light_Stemmer``
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from config.constants import TEXT_COLUMN
|
| 17 |
+
from preprocessing.arabic_normalizer import Arabic_Light_Stemmer, normalizeArabic
|
| 18 |
+
from preprocessing.emoji_handler import (
|
| 19 |
+
replace_emojis_with_text,
|
| 20 |
+
replace_emoticon_with_emojis,
|
| 21 |
+
)
|
| 22 |
+
from preprocessing.text_cleaning import (
|
| 23 |
+
Removing_non_arabic,
|
| 24 |
+
Removing_numbers,
|
| 25 |
+
Removing_punctuations,
|
| 26 |
+
Removing_urls,
|
| 27 |
+
remove_hashtags_and_mentions,
|
| 28 |
+
remove_small_sentences,
|
| 29 |
+
remove_stop_words,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
#: Steps 1-2 - emoticon/emoji substitution.
|
| 33 |
+
EMOJI_STEPS = (
|
| 34 |
+
replace_emoticon_with_emojis,
|
| 35 |
+
replace_emojis_with_text,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
#: Steps 3-10 - applied to both the training corpus and inference input.
|
| 39 |
+
CLEANING_STEPS = (
|
| 40 |
+
remove_stop_words,
|
| 41 |
+
Removing_non_arabic,
|
| 42 |
+
normalizeArabic,
|
| 43 |
+
Removing_numbers,
|
| 44 |
+
remove_hashtags_and_mentions,
|
| 45 |
+
Removing_urls,
|
| 46 |
+
Removing_punctuations,
|
| 47 |
+
Arabic_Light_Stemmer,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def clean_text(text, convert_emojis=True):
|
| 52 |
+
"""Run the full cleaning chain on a single string."""
|
| 53 |
+
steps = (EMOJI_STEPS + CLEANING_STEPS) if convert_emojis else CLEANING_STEPS
|
| 54 |
+
for step in steps:
|
| 55 |
+
text = step(text)
|
| 56 |
+
return text
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def preprocess_dataframe(df, text_column=TEXT_COLUMN, convert_emojis=True,
|
| 60 |
+
drop_small_sentences=True):
|
| 61 |
+
df[text_column] = df[text_column].apply(
|
| 62 |
+
lambda text: clean_text(text, convert_emojis=convert_emojis)
|
| 63 |
+
)
|
| 64 |
+
if drop_small_sentences:
|
| 65 |
+
# this function will convert the text which contains one or two words into null value
|
| 66 |
+
remove_small_sentences(df, text_column=text_column)
|
| 67 |
+
return df
|
preprocessing/text_cleaning.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generic text cleaning helpers (stopwords, punctuation, URLs, numbers, ...)."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from config.constants import MIN_WORDS_PER_SENTENCE, TEXT_COLUMN
|
| 8 |
+
from config.stopwords import get_arabic_stopwords
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def remove_stop_words(text):
|
| 12 |
+
"""Drop NLTK Arabic stopwords from ``text``."""
|
| 13 |
+
arabic_stopwords = get_arabic_stopwords()
|
| 14 |
+
Text = [i for i in str(text).split() if i not in arabic_stopwords]
|
| 15 |
+
return " ".join(Text)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def Removing_non_arabic(text):
|
| 19 |
+
"""Replace latin letter runs with a single space."""
|
| 20 |
+
text = re.sub('[A-Za-z]+', ' ', text)
|
| 21 |
+
return text
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def Removing_numbers(text):
|
| 25 |
+
"""Drop every digit character."""
|
| 26 |
+
text = ''.join([i for i in text if not i.isdigit()])
|
| 27 |
+
return text
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def Removing_punctuations(text):
|
| 31 |
+
"""Replace punctuation (latin + Arabic) with spaces and squeeze whitespace."""
|
| 32 |
+
## Remove punctuations
|
| 33 |
+
text = re.sub('[%s]' % re.escape(r"""!"#$%&'()*+,،-./:;<=>؟?@[\]^_`{|}~"""), ' ', text)
|
| 34 |
+
text = text.replace('؛', "", )
|
| 35 |
+
|
| 36 |
+
## remove extra whitespace
|
| 37 |
+
text = re.sub(r'\s+', ' ', text)
|
| 38 |
+
text = " ".join(text.split())
|
| 39 |
+
return text.strip()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def Removing_urls(text):
|
| 43 |
+
"""Strip http(s):// and www. URLs."""
|
| 44 |
+
url_pattern = re.compile(r'https?://\S+|www\.\S+')
|
| 45 |
+
return url_pattern.sub(r'', text)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def remove_extra_Space(text):
|
| 49 |
+
"""Collapse repeated whitespace into single spaces."""
|
| 50 |
+
text = re.sub(r'\s+', ' ', text)
|
| 51 |
+
return " ".join(text.split())
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def remove_hashtags_and_mentions(text):
|
| 55 |
+
"""Strip ``@mentions`` and ``#hashtags`` written with latin characters."""
|
| 56 |
+
text = re.sub("@[A-Za-z0-9_]+", "", text)
|
| 57 |
+
text = re.sub("#[A-Za-z0-9_]+", "", text)
|
| 58 |
+
return text
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def remove_small_sentences(df, text_column=TEXT_COLUMN):
|
| 62 |
+
"""Turn texts shorter than 3 words into NaN, in place."""
|
| 63 |
+
text_position = df.columns.get_loc(text_column)
|
| 64 |
+
for i in range(len(df)):
|
| 65 |
+
if len(df[text_column].iloc[i].split()) < MIN_WORDS_PER_SENTENCE:
|
| 66 |
+
df.iloc[i, text_position] = np.nan
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cross-cutting helpers: charts and exploratory statistics."""
|
| 2 |
+
|
| 3 |
+
from utils.emoji_stats import add_emoji_count, most_common_emojis
|
| 4 |
+
from utils.visualization import (
|
| 5 |
+
plot_confusion_matrix,
|
| 6 |
+
plot_model_comparison,
|
| 7 |
+
plot_per_class_metrics,
|
| 8 |
+
plot_review_length_distribution,
|
| 9 |
+
plot_sentiment_distribution,
|
| 10 |
+
plot_top_tokens,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"add_emoji_count",
|
| 15 |
+
"most_common_emojis",
|
| 16 |
+
"plot_confusion_matrix",
|
| 17 |
+
"plot_model_comparison",
|
| 18 |
+
"plot_per_class_metrics",
|
| 19 |
+
"plot_review_length_distribution",
|
| 20 |
+
"plot_sentiment_distribution",
|
| 21 |
+
"plot_top_tokens",
|
| 22 |
+
]
|
utils/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (630 Bytes). View file
|
|
|