ArabGuard_Normalizer / train_model.py
3v324v23's picture
Deploy Docker Space with build-time training
f3dcb14
Raw
History Blame Contribute Delete
16.5 kB
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,
)
# =========================================================
# CONFIGURATION
# =========================================================
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
# =========================================================
# REPRODUCIBILITY
# =========================================================
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)
# =========================================================
# TEXT NORMALIZATION
# =========================================================
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)
# Normalize Unicode representations.
text = unicodedata.normalize("NFKC", text)
# Remove zero-width and direction control characters.
text = re.sub(
r"[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]",
"",
text,
)
text = remove_arabic_diacritics(text)
text = normalize_arabic_letters(text)
# Normalize URLs, emails and long numbers.
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,
)
# Reduce exaggerated repeated characters.
text = re.sub(
r"(.)\1{4,}",
r"\1\1",
text,
)
# Reduce repeated punctuation.
text = re.sub(
r"([!?.,،؛:])\1+",
r"\1",
text,
)
# Normalize whitespace.
text = re.sub(
r"\s+",
" ",
text,
).strip()
return text
# =========================================================
# LOAD DATASET FILES SEPARATELY
# =========================================================
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)
# =========================================================
# CLEAN 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
)
# Remove empty rows.
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])
# =========================================================
# LABEL CONFIGURATION
# =========================================================
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
# =========================================================
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
# =========================================================
model = AutoModelForSequenceClassification.from_pretrained(
MODEL_NAME,
num_labels=len(label_names),
id2label=id2label,
label2id=label2id,
)
# =========================================================
# METRICS
# =========================================================
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 ARGUMENTS
# =========================================================
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
# =========================================================
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
)
# =========================================================
# TRAIN MODEL
# =========================================================
print("\nTraining started...")
training_result = trainer.train()
print("\nTraining finished.")
# =========================================================
# EVALUATE NORMALIZED DATA
# =========================================================
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,
)
# =========================================================
# EVALUATE RAW DATA
# =========================================================
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,
)
# =========================================================
# CONFUSION MATRIX
# =========================================================
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
# =========================================================
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,
)
# =========================================================
# SAVE MODEL
# =========================================================
os.makedirs(
MODEL_OUTPUT_DIR,
exist_ok=True,
)
trainer.save_model(
MODEL_OUTPUT_DIR
)
tokenizer.save_pretrained(
MODEL_OUTPUT_DIR
)
# =========================================================
# SAVE DASHBOARD DATA
# =========================================================
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 FINAL RESULTS
# =========================================================
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}"
)