| import inspect |
| import json |
| import os |
| import random |
| import re |
| import unicodedata |
| from typing import Dict |
|
|
| import numpy as np |
| import pandas as pd |
| import torch |
|
|
| from datasets import DatasetDict, load_dataset |
| from sklearn.metrics import ( |
| accuracy_score, |
| classification_report, |
| confusion_matrix, |
| precision_recall_fscore_support, |
| ) |
| from transformers import ( |
| AutoModelForSequenceClassification, |
| AutoTokenizer, |
| DataCollatorWithPadding, |
| Trainer, |
| TrainingArguments, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| MODEL_NAME = os.getenv("MODEL_NAME", "xlm-roberta-base") |
| DATASET_REPO = os.getenv( |
| "DATASET_REPO", "d12o6aa/ArabGuard-Egyptian-V1" |
| ) |
|
|
| MODEL_OUTPUT_DIR = os.getenv("MODEL_OUTPUT_DIR", "./arabguard_model") |
| CHECKPOINT_DIR = os.getenv("CHECKPOINT_DIR", "./arabguard_checkpoints") |
| DASHBOARD_DATA_DIR = os.getenv("DASHBOARD_DATA_DIR", "./dashboard_data") |
|
|
| MAX_LENGTH = int(os.getenv("MAX_LENGTH", "128")) |
| NUM_EPOCHS = float(os.getenv("NUM_EPOCHS", "4")) |
| LEARNING_RATE = float(os.getenv("LEARNING_RATE", "2e-5")) |
| TRAIN_BATCH_SIZE = int(os.getenv("TRAIN_BATCH_SIZE", "8")) |
| EVAL_BATCH_SIZE = int(os.getenv("EVAL_BATCH_SIZE", "8")) |
| RANDOM_SEED = 42 |
|
|
|
|
| |
| |
| |
|
|
| def set_seed(seed: int) -> None: |
| random.seed(seed) |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
|
|
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| set_seed(RANDOM_SEED) |
|
|
|
|
| |
| |
| |
|
|
| def remove_arabic_diacritics(text: str) -> str: |
| arabic_diacritics = re.compile( |
| r""" |
| ّ | |
| َ | |
| ً | |
| ُ | |
| ٌ | |
| ِ | |
| ٍ | |
| ْ | |
| ـ |
| """, |
| re.VERBOSE, |
| ) |
|
|
| return re.sub(arabic_diacritics, "", text) |
|
|
|
|
| def normalize_arabic_letters(text: str) -> str: |
| replacements = { |
| "أ": "ا", |
| "إ": "ا", |
| "آ": "ا", |
| "ٱ": "ا", |
| "ى": "ي", |
| "ؤ": "و", |
| "ئ": "ي", |
| } |
|
|
| for old, new in replacements.items(): |
| text = text.replace(old, new) |
|
|
| return text |
|
|
|
|
| def normalize_text(text: str) -> str: |
| if text is None: |
| return "" |
|
|
| text = str(text) |
|
|
| |
| text = unicodedata.normalize("NFKC", text) |
|
|
| |
| text = re.sub( |
| r"[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]", |
| "", |
| text, |
| ) |
|
|
| text = remove_arabic_diacritics(text) |
| text = normalize_arabic_letters(text) |
|
|
| |
| text = re.sub( |
| r"https?://\S+|www\.\S+", |
| " URL ", |
| text, |
| flags=re.IGNORECASE, |
| ) |
|
|
| text = re.sub( |
| r"\b[\w.\-+]+@[\w.\-]+\.\w+\b", |
| " EMAIL ", |
| text, |
| flags=re.IGNORECASE, |
| ) |
|
|
| text = re.sub( |
| r"\b\d{5,}\b", |
| " NUMBER ", |
| text, |
| ) |
|
|
| |
| text = re.sub( |
| r"(.)\1{4,}", |
| r"\1\1", |
| text, |
| ) |
|
|
| |
| text = re.sub( |
| r"([!?.,،؛:])\1+", |
| r"\1", |
| text, |
| ) |
|
|
| |
| text = re.sub( |
| r"\s+", |
| " ", |
| text, |
| ).strip() |
|
|
| return text |
|
|
|
|
| |
| |
| |
|
|
| print("Loading dataset files...") |
|
|
| train_dataset = load_dataset( |
| "csv", |
| data_files=f"hf://datasets/{DATASET_REPO}/train.csv", |
| split="train", |
| ) |
|
|
| validation_dataset = load_dataset( |
| "csv", |
| data_files=f"hf://datasets/{DATASET_REPO}/val.csv", |
| split="train", |
| ) |
|
|
| test_dataset = load_dataset( |
| "csv", |
| data_files=f"hf://datasets/{DATASET_REPO}/test.csv", |
| split="train", |
| ) |
|
|
| dataset = DatasetDict( |
| { |
| "train": train_dataset, |
| "validation": validation_dataset, |
| "test": test_dataset, |
| } |
| ) |
|
|
| print(dataset) |
|
|
|
|
| |
| |
| |
|
|
| def clean_example(example: Dict) -> Dict: |
| text = str(example.get("text", "")).strip() |
| label = int(example.get("label", 0)) |
|
|
| return { |
| "text": text, |
| "normalized_text": normalize_text(text), |
| "label": label, |
| } |
|
|
|
|
| dataset = dataset.map(clean_example) |
|
|
| for split_name in dataset.keys(): |
| columns_to_remove = [ |
| column |
| for column in dataset[split_name].column_names |
| if column not in ["text", "normalized_text", "label"] |
| ] |
|
|
| if columns_to_remove: |
| dataset[split_name] = dataset[split_name].remove_columns( |
| columns_to_remove |
| ) |
|
|
|
|
| |
| def valid_example(example: Dict) -> bool: |
| return bool(example["text"].strip()) |
|
|
|
|
| dataset = dataset.filter(valid_example) |
|
|
| print("\nCleaned dataset:") |
| print(dataset) |
| print("\nExample:") |
| print(dataset["train"][0]) |
|
|
|
|
| |
| |
| |
|
|
| unique_labels = sorted( |
| set(dataset["train"]["label"]) |
| ) |
|
|
| label_names = [ |
| str(label) |
| for label in unique_labels |
| ] |
|
|
| label2id = { |
| label_name: index |
| for index, label_name in enumerate(label_names) |
| } |
|
|
| id2label = { |
| index: label_name |
| for index, label_name in enumerate(label_names) |
| } |
|
|
| print("\nLabel mappings:") |
| print("label2id:", label2id) |
| print("id2label:", id2label) |
|
|
|
|
| def encode_label(example: Dict) -> Dict: |
| example["labels"] = label2id[ |
| str(example["label"]) |
| ] |
|
|
| return example |
|
|
|
|
| dataset = dataset.map(encode_label) |
|
|
|
|
| |
| |
| |
|
|
| tokenizer = AutoTokenizer.from_pretrained( |
| MODEL_NAME |
| ) |
|
|
|
|
| def tokenize_normalized_batch(batch: Dict) -> Dict: |
| return tokenizer( |
| batch["normalized_text"], |
| truncation=True, |
| max_length=MAX_LENGTH, |
| ) |
|
|
|
|
| tokenized_dataset = dataset.map( |
| tokenize_normalized_batch, |
| batched=True, |
| ) |
|
|
| for split_name in tokenized_dataset.keys(): |
| columns_to_remove = [ |
| column |
| for column in tokenized_dataset[split_name].column_names |
| if column not in [ |
| "input_ids", |
| "attention_mask", |
| "labels", |
| ] |
| ] |
|
|
| if columns_to_remove: |
| tokenized_dataset[split_name] = ( |
| tokenized_dataset[split_name] |
| .remove_columns(columns_to_remove) |
| ) |
|
|
|
|
| data_collator = DataCollatorWithPadding( |
| tokenizer=tokenizer |
| ) |
|
|
|
|
| |
| |
| |
|
|
| model = AutoModelForSequenceClassification.from_pretrained( |
| MODEL_NAME, |
| num_labels=len(label_names), |
| id2label=id2label, |
| label2id=label2id, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def calculate_metrics_from_arrays( |
| labels: np.ndarray, |
| predictions: np.ndarray, |
| ) -> Dict[str, float]: |
| precision, recall, f1, _ = ( |
| precision_recall_fscore_support( |
| labels, |
| predictions, |
| average="weighted", |
| zero_division=0, |
| ) |
| ) |
|
|
| accuracy = accuracy_score( |
| labels, |
| predictions, |
| ) |
|
|
| return { |
| "accuracy": float(accuracy), |
| "precision": float(precision), |
| "recall": float(recall), |
| "f1": float(f1), |
| } |
|
|
|
|
| def compute_metrics(eval_prediction) -> Dict[str, float]: |
| logits, labels = eval_prediction |
|
|
| predictions = np.argmax( |
| logits, |
| axis=-1, |
| ) |
|
|
| return calculate_metrics_from_arrays( |
| labels, |
| predictions, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| training_argument_parameters = inspect.signature( |
| TrainingArguments.__init__ |
| ).parameters |
|
|
| training_arguments_dictionary = { |
| "output_dir": CHECKPOINT_DIR, |
| "learning_rate": LEARNING_RATE, |
| "num_train_epochs": NUM_EPOCHS, |
| "per_device_train_batch_size": TRAIN_BATCH_SIZE, |
| "per_device_eval_batch_size": EVAL_BATCH_SIZE, |
| "weight_decay": 0.01, |
| "save_strategy": "epoch", |
| "logging_strategy": "steps", |
| "logging_steps": 20, |
| "load_best_model_at_end": True, |
| "metric_for_best_model": "f1", |
| "greater_is_better": True, |
| "save_total_limit": 2, |
| "report_to": "none", |
| "fp16": torch.cuda.is_available(), |
| "seed": RANDOM_SEED, |
| "data_seed": RANDOM_SEED, |
| } |
|
|
| if "eval_strategy" in training_argument_parameters: |
| training_arguments_dictionary[ |
| "eval_strategy" |
| ] = "epoch" |
|
|
| elif "evaluation_strategy" in training_argument_parameters: |
| training_arguments_dictionary[ |
| "evaluation_strategy" |
| ] = "epoch" |
|
|
| training_arguments = TrainingArguments( |
| **training_arguments_dictionary |
| ) |
|
|
|
|
| |
| |
| |
|
|
| trainer_arguments = { |
| "model": model, |
| "args": training_arguments, |
| "train_dataset": tokenized_dataset["train"], |
| "eval_dataset": tokenized_dataset["validation"], |
| "data_collator": data_collator, |
| "compute_metrics": compute_metrics, |
| } |
|
|
| trainer_signature = inspect.signature( |
| Trainer.__init__ |
| ).parameters |
|
|
| if "processing_class" in trainer_signature: |
| trainer_arguments["processing_class"] = tokenizer |
|
|
| elif "tokenizer" in trainer_signature: |
| trainer_arguments["tokenizer"] = tokenizer |
|
|
| trainer = Trainer( |
| **trainer_arguments |
| ) |
|
|
|
|
| |
| |
| |
|
|
| print("\nTraining started...") |
| training_result = trainer.train() |
|
|
| print("\nTraining finished.") |
|
|
|
|
| |
| |
| |
|
|
| validation_results = trainer.evaluate( |
| tokenized_dataset["validation"], |
| metric_key_prefix="validation", |
| ) |
|
|
| normalized_test_output = trainer.predict( |
| tokenized_dataset["test"] |
| ) |
|
|
| normalized_predictions = np.argmax( |
| normalized_test_output.predictions, |
| axis=-1, |
| ) |
|
|
| normalized_labels = normalized_test_output.label_ids |
|
|
| normalized_metrics = calculate_metrics_from_arrays( |
| normalized_labels, |
| normalized_predictions, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def create_raw_tokenized_test_dataset(): |
| raw_test_dataset = dataset["test"].map( |
| lambda batch: tokenizer( |
| batch["text"], |
| truncation=True, |
| max_length=MAX_LENGTH, |
| ), |
| batched=True, |
| ) |
|
|
| columns_to_remove = [ |
| column |
| for column in raw_test_dataset.column_names |
| if column not in [ |
| "input_ids", |
| "attention_mask", |
| "labels", |
| ] |
| ] |
|
|
| if columns_to_remove: |
| raw_test_dataset = raw_test_dataset.remove_columns( |
| columns_to_remove |
| ) |
|
|
| return raw_test_dataset |
|
|
|
|
| raw_test_dataset = create_raw_tokenized_test_dataset() |
|
|
| raw_test_output = trainer.predict( |
| raw_test_dataset |
| ) |
|
|
| raw_predictions = np.argmax( |
| raw_test_output.predictions, |
| axis=-1, |
| ) |
|
|
| raw_labels = raw_test_output.label_ids |
|
|
| raw_metrics = calculate_metrics_from_arrays( |
| raw_labels, |
| raw_predictions, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| matrix = confusion_matrix( |
| normalized_labels, |
| normalized_predictions, |
| labels=list(range(len(label_names))), |
| ) |
|
|
| confusion_matrix_dataframe = pd.DataFrame( |
| matrix, |
| index=[ |
| f"Actual {id2label[index]}" |
| for index in range(len(label_names)) |
| ], |
| columns=[ |
| f"Predicted {id2label[index]}" |
| for index in range(len(label_names)) |
| ], |
| ) |
|
|
|
|
| |
| |
| |
|
|
| classification_report_data = classification_report( |
| normalized_labels, |
| normalized_predictions, |
| target_names=[ |
| id2label[index] |
| for index in range(len(label_names)) |
| ], |
| output_dict=True, |
| zero_division=0, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| os.makedirs( |
| MODEL_OUTPUT_DIR, |
| exist_ok=True, |
| ) |
|
|
| trainer.save_model( |
| MODEL_OUTPUT_DIR |
| ) |
|
|
| tokenizer.save_pretrained( |
| MODEL_OUTPUT_DIR |
| ) |
|
|
|
|
| |
| |
| |
|
|
| os.makedirs( |
| DASHBOARD_DATA_DIR, |
| exist_ok=True, |
| ) |
|
|
| training_history = trainer.state.log_history |
|
|
| history_dataframe = pd.DataFrame( |
| training_history |
| ) |
|
|
| history_dataframe.to_csv( |
| os.path.join( |
| DASHBOARD_DATA_DIR, |
| "training_history.csv", |
| ), |
| index=False, |
| ) |
|
|
| confusion_matrix_dataframe.to_csv( |
| os.path.join( |
| DASHBOARD_DATA_DIR, |
| "confusion_matrix.csv", |
| ), |
| ) |
|
|
| with open( |
| os.path.join( |
| DASHBOARD_DATA_DIR, |
| "classification_report.json", |
| ), |
| "w", |
| encoding="utf-8", |
| ) as file: |
| json.dump( |
| classification_report_data, |
| file, |
| ensure_ascii=False, |
| indent=4, |
| ) |
|
|
|
|
| normalization_accuracy_change = ( |
| normalized_metrics["accuracy"] |
| - raw_metrics["accuracy"] |
| ) |
|
|
| metrics_data = { |
| "model_name": MODEL_NAME, |
| "model_output_directory": MODEL_OUTPUT_DIR, |
| "max_length": MAX_LENGTH, |
| "epochs": NUM_EPOCHS, |
| "device_used_for_training": ( |
| "cuda" |
| if torch.cuda.is_available() |
| else "cpu" |
| ), |
| "dataset": { |
| "train_samples": len(dataset["train"]), |
| "validation_samples": len( |
| dataset["validation"] |
| ), |
| "test_samples": len(dataset["test"]), |
| }, |
| "raw_test_metrics": raw_metrics, |
| "normalized_test_metrics": normalized_metrics, |
| "normalization_accuracy_change": float( |
| normalization_accuracy_change |
| ), |
| "validation_metrics": { |
| key: float(value) |
| for key, value in validation_results.items() |
| if isinstance( |
| value, |
| ( |
| int, |
| float, |
| np.integer, |
| np.floating, |
| ), |
| ) |
| }, |
| "training_metrics": { |
| key: float(value) |
| for key, value in training_result.metrics.items() |
| if isinstance( |
| value, |
| ( |
| int, |
| float, |
| np.integer, |
| np.floating, |
| ), |
| ) |
| }, |
| "label_mapping": { |
| str(key): value |
| for key, value in id2label.items() |
| }, |
| } |
|
|
| with open( |
| os.path.join( |
| DASHBOARD_DATA_DIR, |
| "metrics.json", |
| ), |
| "w", |
| encoding="utf-8", |
| ) as file: |
| json.dump( |
| metrics_data, |
| file, |
| ensure_ascii=False, |
| indent=4, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| print("\n" + "=" * 60) |
| print("RAW TEST METRICS") |
| print("=" * 60) |
|
|
| for metric_name, metric_value in raw_metrics.items(): |
| print( |
| f"{metric_name}: " |
| f"{metric_value:.4f}" |
| ) |
|
|
| print("\n" + "=" * 60) |
| print("NORMALIZED TEST METRICS") |
| print("=" * 60) |
|
|
| for metric_name, metric_value in normalized_metrics.items(): |
| print( |
| f"{metric_name}: " |
| f"{metric_value:.4f}" |
| ) |
|
|
| print("\nNormalization accuracy change:") |
|
|
| print( |
| f"{normalization_accuracy_change:+.4f}" |
| ) |
|
|
| print( |
| f"\nModel saved to: " |
| f"{MODEL_OUTPUT_DIR}" |
| ) |
|
|
| print( |
| f"Dashboard data saved to: " |
| f"{DASHBOARD_DATA_DIR}" |
| ) |
|
|