CoolFace
Apppublic

mr-kush/urgency-classifier-retraining

sourceHugging Faceapache-2.0updated 11mo agoView on Hugging Face
0likes
model_pipeline.py807 linesDownload Raw Back to root
1#model_piepline.py2 3import os4import json5from datetime import datetime, UTC, timezone, timedelta6from huggingface_hub import HfApi7from huggingface_hub.utils import HfHubHTTPError8import numpy as np9import torch10import torch.nn as nn11import torch.nn.functional as F12import wandb13from wandb import AlertLevel14import matplotlib.pyplot as plt15import seaborn as sns16import requests17 18from transformers import (19    AutoTokenizer, DataCollatorWithPadding,20    AutoModelForSequenceClassification, TrainingArguments,21    Trainer, EarlyStoppingCallback22)23from sklearn.metrics import (24    accuracy_score, f1_score,25    precision_score, recall_score,26    classification_report, confusion_matrix27)28 29 30 31# function to tokenize a batch of examples32def tokenize_function(examples, tokenizer, text_column: str):33    """Helper function for tokenization (pickle-safe for HF caching)."""34    return tokenizer(examples[text_column], truncation=True)35 36def sanitize_training_args(training_args):37    """Convert TrainingArguments to JSON-serializable dictionary."""38    if not training_args:39        return {}40    args_dict = training_args.to_dict()41    clean_dict = {}42    for k, v in args_dict.items():43        try:44            json.dumps({k: v})45            clean_dict[k] = v46        except TypeError:47            clean_dict[k] = str(v)  # fallback: convert to string48    return clean_dict49 50 51 52 53 54class FocalLossMultiClass(nn.Module):55    """Implementation of Focal Loss for multi-class classification."""56 57    def __init__(self, gamma: float = 2.0, alpha: float = 0.25, reduction: str = 'mean'):58        """59        Args:60            gamma (float): Focusing parameter. Default=2.061            alpha (float): Weighting factor for class imbalance. Default=0.2562            reduction (str): 'mean', 'sum', or 'none'. Default='mean'63        """64        super().__init__()65        self.gamma = gamma66        self.alpha = alpha67        self.reduction = reduction68 69    def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:70        ce_loss = F.cross_entropy(logits, targets, reduction='none')71        pt = torch.exp(-ce_loss)72        focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss73 74        if self.reduction == 'mean':75            return focal_loss.mean()76        elif self.reduction == 'sum':77            return focal_loss.sum()78        return focal_loss79 80 81class FocalLossTrainer(Trainer):82    """Custom Hugging Face Trainer using Focal Loss."""83 84    def __init__(self, class_weights: torch.Tensor = None, *args, **kwargs):85        """86        Args:87            class_weights (torch.Tensor, optional): Tensor for weighting classes in loss.88        """89        super().__init__(*args, **kwargs)90        # self.class_weights = class_weights.to(self.model.device)  # optional91 92    def compute_loss(self, model: nn.Module, inputs: dict, return_outputs: bool = False, **kwargs) -> torch.Tensor:93        labels = inputs.get("labels")94        outputs = model(**inputs)95        logits = outputs.get("logits")96        loss_fct = FocalLossMultiClass()97        loss = loss_fct(logits, labels)98        return (loss, outputs) if return_outputs else loss99 100 101class GrievanceClassifier:102    """Grievance classification model wrapper with training, evaluation, and HF Hub integration."""103 104    def __init__(105        self,106        model_checkpoint: str,107        num_labels: int,108        id2label: dict,109        label2id: dict, 110        hf_token: str,111        wandb_api_key: str,112        wandb_project_name: str, 113    ):114        """115        Args:116            hf_token(str): HF-token for HF Hub Write Acess117            model_checkpoint (str): HF model checkpoint, e.g., 'xlm-roberta-base'118            num_labels (int): Number of classes for classification119            id2label (dict): Mapping from label IDs to string labels120            label2id (dict): Mapping from string labels to label IDs121            wandb_api_key (str) : WandB Access API key 122            wandb_project_name (str): WandB project name for experiment tracking123        """124        self.model_checkpoint = model_checkpoint125        self.num_labels = num_labels126        self.id2label = id2label127        self.label2id = label2id128        self.hf_token = hf_token129        self.api = HfApi()130        131        # Login wandb132        wandb.login(key=wandb_api_key)133        self.wandb_project_name = wandb_project_name134        135 136 137 138 139        # Load tokenizer and model140        self.tokenizer = AutoTokenizer.from_pretrained(model_checkpoint,141         use_fast=True, 142         token= self.hf_token)143        self.model = AutoModelForSequenceClassification.from_pretrained(144            model_checkpoint,145            num_labels=num_labels,146            id2label=id2label,147            label2id=label2id, 148            token= self.hf_token149        )150 151    def tokenize_dataset(self, dataset, text_column: str = "grievance", remove_columns: bool = True, batched: bool = True):152        """153        Tokenize a HF Dataset or DatasetDict using the class tokenizer.154 155        Args:156            dataset: HF Dataset or DatasetDict to tokenize157            text_column (str): Name of the column containing the text. Default="grievance"158            remove_columns (bool): Whether to remove the original text column after tokenization. Default=True159            batched (bool): Whether to batch examples during tokenization. Default=True160 161        Returns:162            tokenized_dataset: Tokenized HF Dataset or DatasetDict163        """164 165 166        tokenized_dataset = dataset.map(167            lambda examples: tokenize_function(examples, self.tokenizer, text_column),168            batched=batched169        )170 171        if remove_columns and text_column in tokenized_dataset.column_names:172            tokenized_dataset = tokenized_dataset.remove_columns([text_column])173        174        return tokenized_dataset175 176    @staticmethod177    def compute_metrics(eval_pred: tuple) -> dict:178        """179        Compute classification metrics.180 181        Args:182            eval_pred (tuple): (logits, labels) from trainer.predict183 184        Returns:185            dict: Accuracy, F1 (macro & weighted), precision, recall186        """187        logits, labels = eval_pred188        predictions = np.argmax(logits, axis=-1)189        return {190            "accuracy": accuracy_score(labels, predictions),191            "f1_macro": f1_score(labels, predictions, average="macro", zero_division=0),192            "f1_weighted": f1_score(labels, predictions, average="weighted", zero_division=0),193            "precision_macro": precision_score(labels, predictions, average="macro", zero_division=0),194            "recall_macro": recall_score(labels, predictions, average="macro", zero_division=0),195            "precision_weighted": precision_score(labels, predictions, average="weighted", zero_division=0),196            "recall_weighted": recall_score(labels, predictions, average="weighted", zero_division=0)197        }198 199    def train(200        self,201        train_dataset,202        eval_dataset,203        output_dir: str | None = None,204        hf_training_args: dict | None = None,205        early_stopping_patience: int = 2,206        early_stopping_threshold: float=0.001207    ):208        """209        Train the model using HF Trainer with Focal Loss.210 211        Args:212            train_dataset: HF Dataset or DatasetDict for training213            eval_dataset: HF Dataset or DatasetDict for validation214            wandb_project_name (str): WandB project name for experiment tracking215            output_dir (str, optional): Directory to save checkpoints216            hf_training_args (dict, optional): Dictionary of HuggingFace TrainingArguments to override defaults217            early_stopping_patience (int): Patience for early stopping218        """219        # Early stopping callback220        early_stopping_callback = EarlyStoppingCallback(221            early_stopping_patience=early_stopping_patience,222            early_stopping_threshold=early_stopping_threshold223        )224        225 226 227        # Tokenize datasets228        train_dataset = self.tokenize_dataset(train_dataset)229        eval_dataset = self.tokenize_dataset(eval_dataset)230 231        # Default training arguments with no logging and no step-wise saving232        self.default_args = {233            "num_train_epochs": 3,234            "per_device_train_batch_size": 16,235            "per_device_eval_batch_size": 32,236            "learning_rate": 2e-5,237            "weight_decay": 0.01,238            "eval_strategy": "steps",239            "eval_steps": 50,240            "logging_steps": 50,241            "load_best_model_at_end": True,242            "metric_for_best_model": "f1_macro",243            "greater_is_better": True,244            "fp16": True,245            "push_to_hub": False,246            "hub_model_id": None,247            "report_to": ["wandb"],248            "logging_dir": "./logs",249            "gradient_accumulation_steps": 1250        }251        252        253        254        # Merge user-provided overrides255        if hf_training_args:256            self.default_args.update(hf_training_args)257            # initalizing hub id258            self.hub_model_id= self.default_args.get('hub_model_id')259 260        # Initialize TrainingArguments261        self.training_args = TrainingArguments(**self.default_args)262        263        # sanitize training arg for metadata file:264        self.sanitize_training_args = (sanitize_training_args(getattr(self, 265                                                              "training_args",266                                                              None))267                                if hasattr(self, "training_args")268                                else {}269                                )270 271 272        # Initialize trainer273        self.trainer = FocalLossTrainer(274            model=self.model,275            args=self.training_args,276            train_dataset=train_dataset,277            eval_dataset=eval_dataset,278            compute_metrics=self.compute_metrics,279            callbacks=[early_stopping_callback],280            data_collator=DataCollatorWithPadding(tokenizer=self.tokenizer),281            processing_class=self.tokenizer282        )283 284        # Start training285        self.trainer.train()286 287    def log_wandb_eval_metrics(288        self,289        y_true,290        y_pred,291        classification_report_dict,292        confusion_matrix_array,293        label_names,294        prefix="final_eval"295    ):296        """Logs classification metrics and confusion matrix to Weights & Biases."""297        try:298            # 1️ Create classification report as W&B Table299            # Prepare rows300            rows = [301                [label,302                round(metrics["precision"], 4),303                round(metrics["recall"], 4),304                round(metrics["f1-score"], 4),305                int(metrics["support"])]306                for label, metrics in classification_report_dict.items()307                if isinstance(metrics, dict) and all(k in metrics for k in ["precision", "recall", "f1-score", "support"])308            ]309 310            # Create table using columns + data argument311            table = wandb.Table(columns=["Class", "Precision", "Recall", "F1-score", "Support"], data=rows)312            313            # 2️ Plot confusion matrix314            fig, ax = plt.subplots(figsize=(6, 6))315            sns.heatmap(confusion_matrix_array, annot=True, fmt="d", cmap="Blues",316                        xticklabels=label_names, yticklabels=label_names, ax=ax)317            ax.set_xlabel("Predicted Label")318            ax.set_ylabel("True Label")319            ax.set_title("Confusion Matrix")320            plt.tight_layout()321            cm_image = wandb.Image(fig)322            plt.close(fig)323 324            # 3️ Log both to W&B325            wandb.log({326                f"{prefix}/classification_report_table": table, 327                f"{prefix}/confusion_matrix": cm_image328            }, commit=True)329 330        except Exception as e:331            print(f"[W&B Logging Error] {type(e).__name__}: {e}", flush=True)332 333 334    def _query_deployed_model(self, 335                              texts: list[str],336                              api_endpoint: str,337                              timeout: int = 8) -> list[int]:338        """339        Query a deployed model API and return predicted label IDs.340 341        Args:342            texts (list[str]): List of raw text inputs.343            api_endpoint (str): POST /predict endpoint URL.344            timeout (int): Request timeout in seconds.345 346        Returns:347            List[int]: Predicted label IDs (-1 if prediction failed or unknown).348        """349 350        pred_ids = []351        for txt in texts:352            try:353                resp = requests.post(api_endpoint, json={"text": txt}, timeout=timeout)354                if resp.status_code == 200:355                    data = resp.json()356                    label_str = data.get("label")357                    # Map string label to ID358                    pred_id = self.label2id.get(label_str, None)359                    if pred_id is None:360                        try:361                            pred_id = int(label_str)362                        except Exception:363                            pred_id = -1364                    pred_ids.append(pred_id if pred_id is not None else -1)365                else:366                    pred_ids.append(-1)367            except Exception:368                pred_ids.append(-1)369 370        return pred_ids371 372 373    def evaluate(374        self,375        test_dataset,376        api_endpoint: str | None = None,377        threshold: float = 0.00,378        deployed_sample_size: int = 300379    ):380        """381        Pure evaluation function: tokenizes test data, predicts labels, computes metrics,382        optionally compares against deployed model, and returns outcomes.383 384        Args:385            test_dataset: Hugging Face Dataset for testing.386            api_endpoint (str, optional): Deployed model /predict API endpoint.387            threshold (float): Minimum F1 macro improvement over deployed model for decision.388            deployed_sample_size (int): Number of samples to query deployed model for F1 comparison.389 390        Returns:391            dict: {392                "predictions": np.ndarray,393                "y_true": np.ndarray,394                "confusion_matrix": np.ndarray,395                "classification_report": dict,396                "f1_macro": float,397                "deployed_f1_macro": float | None,398                "decision": "accepted" | "rejected"399            }400        """401 402        # 1️ Tokenize test dataset403        test_dataset_tokenized = self.tokenize_dataset(test_dataset)404 405        # 2️ Run model predictions406        predictions = self.trainer.predict(test_dataset_tokenized)407        y_true = predictions.label_ids408        y_pred = np.argmax(predictions.predictions, axis=-1)409 410        # 3️ Compute classification report and confusion matrix411        classification_report_dict = classification_report(412            y_true,413            y_pred,414            target_names=list(self.id2label.values()),415            output_dict=True416        )417        labels = list(self.id2label.keys())418        cm = confusion_matrix(y_true, y_pred, labels=labels)419 420        # 4️ Compute current model F1 macro421        current_trained_f1_macro = f1_score(y_true, y_pred, average="macro", zero_division=0)422 423        # 5️ Optionally compare with deployed model F1424        deployed_f1_macro = None425        if api_endpoint:426            raw_test = test_dataset.shuffle(seed=42)427            n = min(deployed_sample_size, len(raw_test))428            texts = raw_test["grievance"][:n]429            true_labels = raw_test["label"][:n] if "label" in raw_test.column_names else raw_test["labels"][:n]430 431            deployed_preds_ids = self._query_deployed_model(texts, api_endpoint)432 433            # Filter out failed predictions (-1)434            paired_true, paired_pred = [], []435            for t, p in zip(true_labels, deployed_preds_ids):436                if p != -1:437                    paired_true.append(int(t))438                    paired_pred.append(int(p))439 440            if paired_true:441                deployed_f1_macro = f1_score(paired_true, paired_pred, average="macro", zero_division=0)442            else:443                deployed_f1_macro = 0.0444 445        # 6️ Decision logic446        deployed_f1_to_compare = deployed_f1_macro if deployed_f1_macro is not None else 0.0447        decision = "accepted" if current_trained_f1_macro > deployed_f1_to_compare + threshold else "rejected"448 449        # 7️  Return all evaluation outcomes450        return {451            "predictions": y_pred,452            "y_true": y_true,453            "confusion_matrix": cm,454            "classification_report": classification_report_dict,455            "current_trained_f1_macro": current_trained_f1_macro,456            "deployed_f1_macro": deployed_f1_macro,457            "decision": decision458        }459 460    def push_model_to_hub(461            self,462            hub_model_id: str | None = None,463            use_trainer: bool = False,464            commit_message: str = "Push model and tokenizer to Hugging Face Hub",465        ):466            """467            Push the model and tokenizer (or trainer) to the Hugging Face Hub with proper468            version tagging, metadata logging, and safe cleanup.469 470            Args:471                hub_model_id (str): Repository ID on Hugging Face Hub.472                use_trainer (bool): Whether to use trainer.push_to_hub().473                commit_message (str): Custom commit message.474            """475            # version tag476            timestamp= datetime.now(UTC).strftime("%Y%m%d_%H%M%S")477            self.version_tag = f"v{timestamp}"478 479            if hub_model_id is None:480                hub_model_id = getattr(self.training_args, "hub_model_id", None)481                if hub_model_id is None:482                    raise ValueError("You must provide a hub_model_id or define it in TrainingArguments.")483 484            self.commit_message = f"{commit_message} ({self.version_tag})"485            metadata_path = "model_metadata.json"486 487 488            try:489                print("Starting model push to Hugging Face Hub...", flush=True)490 491                # Step 1: Push model and tokenizer (or trainer)492                if use_trainer and hasattr(self, "trainer") and self.trainer is not None:493                    self.trainer.push_to_hub(commit_message=self.commit_message, token=self.hf_token)494                else:495                    self.model.push_to_hub(496                        hub_model_id,497                        commit_message=self.commit_message,498                        token=self.hf_token,499                    )500                    self.tokenizer.push_to_hub(501                        hub_model_id,502                        commit_message=self.commit_message, 503                        token=self.hf_token,504                    )505 506                # pushing the log files 507                # self.push_latest_tensorboard_log(logs_dir='logs', 508                # hf_model_repo=self.hub_model_id, 509                # hf_token= self.hf_token510                # )511                512                513 514                # Step 2: Generate model metadata515                metadata = {516                    "model_name": hub_model_id,517                    "self.version_tag": self.version_tag,518                    "commit_message": commit_message,519                    "timestamp_utc": timestamp,520                    "author": "mr-kush",521                    "training_args": self.sanitize_training_args,522                    "eval_metrics": getattr(self, "classification_report", {}),523                }524 525                with open(metadata_path, "w") as f:526                    json.dump(metadata, f, indent=4)527 528                # Step 3: Upload metadata to the Hub529                self.api.upload_file(530                    path_or_fileobj=metadata_path,531                    path_in_repo="model_metadata.json",532                    repo_id=hub_model_id,533                    repo_type="model",534                    token=self.hf_token,535                    commit_message= f"Upload model_metadata.json ({self.version_tag})"536                )537 538                # Step 4: Create version tag539                self.api.create_tag(540                    repo_id=hub_model_id,541                    repo_type="model",542                    tag=self.version_tag,543                    token=self.hf_token,544                )545 546                print(f"Model successfully pushed and tagged as {self.version_tag} on {hub_model_id}", flush=True)547                548                549                550 551            except Exception as e:552                print(f"Push failed: {e}", flush=True)553 554            finally:555                # Step 5: Clean up temporary metadata file556                if os.path.exists(metadata_path):557                    try:558                        os.remove(metadata_path)559                        print("Temporary file model_metadata.json removed successfully.", flush=True)560                    except Exception as cleanup_error:561                        print(f"Warning: Could not delete model_metadata.json ({cleanup_error})", flush=True)562 563    def train_pipeline(564        self,565        train_dataset,566        eval_dataset,567        test_dataset,568        dataset_metadata: dict, 569        space_repo_id: str | None = None,570        hf_training_args: dict | None = None,571        api_endpoint: str | None = None,572        early_stopping_patience: int = 2,573        early_stopping_threshold: float = 0.001,574        deployed_sample_size: int = 300,575        decision_threshold: float = 0.001576    ):577        """578        Complete training, evaluation, decision-making, and optional auto-deployment pipeline.579 580        Args:581            train_dataset: Hugging Face Dataset for training.582            eval_dataset: Hugging Face Dataset for validation.583            test_dataset: Hugging Face Dataset for testing.584            dataset_metadata: Metadata about Data for Logging 585            hf_training_args (dict, optional): Hugging Face TrainingArguments overrides.586            api_endpoint (str, optional): Endpoint of deployed model to compare F1.587            space_repo_id (str): HF Space Repo Id. 588            early_stopping_patience (int): Patience for early stopping callback.589            early_stopping_threshold (float): Threshold for early stopping.590            deployed_sample_size (int): Sample size to query deployed model for comparison.591            decision_threshold (float): Minimum F1 improvement for auto-deploy.592        Returns:593            dict: Contains evaluation metrics, decision, and deployed F1 (if applicable).594        """595        self.space_repo_id= space_repo_id596        self.dataset_metadata = dataset_metadata597        598        # 1. Initialize W&B run599        wandb.init(600            project=self.wandb_project_name,601            name=f"train_pipeline_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}",602            config={603                "model_checkpoint": self.model_checkpoint,604                "num_labels": self.num_labels,605                "dataset_metadata": self.dataset_metadata606            }607        )608 609        # 2. Train the model610        self.train(611            train_dataset=train_dataset,612            eval_dataset=eval_dataset,613            hf_training_args=hf_training_args,614            early_stopping_patience=early_stopping_patience,615            early_stopping_threshold=early_stopping_threshold616        )617        618        #  Log sanitized training args to W&B config619        wandb.config.update(self.sanitize_training_args)620 621        # 3. Evaluate model on test dataset (no logging inside evaluate)622        eval_results = self.evaluate(623            test_dataset=test_dataset,624            api_endpoint=api_endpoint,625            threshold=decision_threshold,626            deployed_sample_size=deployed_sample_size627        )628        629 630 631        # 4. Extract outputs632        y_true = eval_results["y_true"]633        y_pred = eval_results["predictions"]634        cm = eval_results["confusion_matrix"]635        classification_report = eval_results["classification_report"]636        current_trained_f1_macro = eval_results["current_trained_f1_macro"]637        deployed_f1_macro = eval_results.get("deployed_f1_macro", None)638        decision = eval_results["decision"]639 640        # 5. Log evaluation metrics to W&B641        self.log_wandb_eval_metrics(642            y_true=y_true,643            y_pred=y_pred,644            classification_report_dict=classification_report,645            confusion_matrix_array=cm,646            label_names=list(self.id2label.values()),647            prefix="train_pipeline_eval"648        )649 650        # 6. Decision logic for auto-deployment651        deployed_f1_to_compare = deployed_f1_macro if deployed_f1_macro is not None else 0.0652        decision = "accepted" if current_trained_f1_macro > deployed_f1_to_compare + decision_threshold else "rejected"653 654        # 7. Log decision and F1 metrics to W&B655        wandb.log({656            "current_trained_model_f1_macro": current_trained_f1_macro,657            "deployed_model_f1_macro": deployed_f1_to_compare,658            "decision": decision,659            "timestamp": datetime.now(UTC).isoformat()660        })661 662        # 8. Tag run and summarize663        wandb.run.tags = ["train_pipeline", decision]664        wandb.run.summary["accepted"] = (decision == "accepted")665 666 667        # 9. Auto-deploy if decision accepted668        if decision == "accepted":669            try:670                # 9.1: push model to hub 671                self.push_model_to_hub(672                    hub_model_id=self.hub_model_id,673                    use_trainer=True,674                    commit_message=f"Auto-deploy: ΔF1 >= {decision_threshold:.4f}"675                )676                677                # 9.2: restart th space678                self.restart_space(679                    space_repo_id=self.space_repo_id680                    )681                682                683            except Exception as e:684                wandb.log({"push_error": str(e)})685                raise RuntimeError(f"Warning: push to hub failed: {e}")686 687        # 10a. Send summary alert before finishing the run688        wandb.alert(689            title=f"Run Summary: {self.hub_model_id} ",690            text=(691                f"Decision: {decision}\n"692                f"Current Trained F1 Macro: {current_trained_f1_macro}\n"693                f"Deployed F1 Macro: {deployed_f1_macro}"694            ),695            level=AlertLevel.INFO,696            wait_duration=timedelta(minutes=1)  # optional delay before sending697        )698 699 700        # 10.b Finish W&B run cleanly701        wandb.join()702        wandb.finish()703 704        # 11. Return outcomes705        return {706            "decision": decision,707            "current_trained_f1_macro": current_trained_f1_macro, 708            "deployed_f1": deployed_f1_macro,709            "classification_report": classification_report,710            "confusion_matrix": cm,711            "y_true": y_true,712            "y_pred": y_pred713        }714 715 716 717 718 719    def restart_space(self, 720                      space_repo_id: str721                      ):722        """723        Restarts the Hugging Face Space programmatically.724        725        Args:726            space_repo_id (str): HF Space Repo Id 727 728        Raises:729            ValueError: If 'repo_id' or 'token' is empty.730            RuntimeError: If the restart operation fails.731        """732        if not self.space_repo_id: 733            self.space_repo_id = space_repo_id734        735        if not self.space_repo_id or not self.hf_token:736            raise ValueError("Failed to Restart Space: Both 'repo_id' and 'token' must be provided.")737 738        try:739            self.api.restart_space(repo_id=self.space_repo_id,token=self.hf_token)740            print(f"Successfully restarted Space: {self.space_repo_id}", flush=True)741        except HfHubHTTPError as e:742            raise RuntimeError(f"Failed to restart Space '{self.space_repo_id}': {e}")743        except Exception as e:744            raise RuntimeError(f"An unexpected error occurred: {e}")745 746 747    # # def push_latest_tensorboard_log(self, logs_dir: str,748    #                                     hf_model_repo: str,749    #                                     hf_token: str,750    #                                     runs_dir: str = "runs"):751    #         """752    #         Upload the latest TensorBoard event file from a logs directory753    #         to a Hugging Face model repo under a TensorBoard-style folder.754 755    #         Folder and file host IDs will match, e.g.:756    #         runs/Sep26_05-06-52_0646998ee581/events.out.tfevents.<timestamp>.0646998ee581.<pid>.0757    #         """758 759    #         # Step 1: List all event files760    #         event_files = [761    #             f for f in os.listdir(logs_dir)762    #             if f.startswith("events.out.tfevents")763    #         ]764 765    #         if not event_files:766    #             print(f"No TensorBoard event files found in {logs_dir}.")767    #             return768 769    #         # Step 2: Find latest by modification time770    #         latest_file = max(event_files, key=lambda f: os.path.getmtime(os.path.join(logs_dir, f)))771    #         latest_file_path = os.path.join(logs_dir, latest_file)772 773    #         # Step 3: Extract hostname part (index 4)774    #         parts = latest_file.split('.')775    #         if len(parts) >= 5:776    #             host_id = parts[4]  # e.g., '0646998ee581'777    #         else:778    #             # fallback: use system hostname779    #             host_id = socket.gethostname()[:12]780 781    #         # Step 4: Create TensorBoard-like folder name782    #         timestamp = datetime.now(UTC).strftime("%b%d_%H-%M-%S")783    #         new_folder_name = f"{timestamp}_{host_id}"784 785    #         # Step 5: Construct path in HF repo786    #         hf_upload_path = f"{runs_dir}/{new_folder_name}/{latest_file}"787 788    #         # Step 6: Upload to Hugging Face Hub789    #         self.api.upload_file(790    #             path_or_fileobj=latest_file_path,791    #             path_in_repo=hf_upload_path,792    #             repo_id=hf_model_repo,793    #             repo_type="model",794    #             token=hf_token,795    #             commit_message=f"Upload latest TensorBoard log: {latest_file}"796    #         )797 798    #         print(f"Uploaded '{latest_file}' → '{hf_model_repo}/{hf_upload_path}'")799            800    #         # Step 7: Delete local event file 801    #         try:802    #             os.remove(latest_file_path)803    #             print(f"Cleared local file: {latest_file_path}")804    #         except Exception as e:805    #             print(f"Could not delete file '{latest_file_path}': {e}")806 807