CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
keras_callbacks.py414 linesDownload Raw Back to transformers
1import logging2import os3from pathlib import Path4from time import sleep5from typing import Callable, Optional, Union6 7import numpy as np8import tensorflow as tf9from huggingface_hub import Repository, create_repo10from packaging.version import parse11 12from . import IntervalStrategy, PreTrainedTokenizerBase13from .modelcard import TrainingSummary14from .modeling_tf_utils import keras15 16 17logger = logging.getLogger(__name__)18 19 20class KerasMetricCallback(keras.callbacks.Callback):21    """22    Callback to compute metrics at the end of every epoch. Unlike normal Keras metrics, these do not need to be23    compilable by TF. It is particularly useful for common NLP metrics like BLEU and ROUGE that require string24    operations or generation loops that cannot be compiled. Predictions (or generations) will be computed on the25    `eval_dataset` before being passed to the `metric_fn` in `np.ndarray` format. The `metric_fn` should compute26    metrics and return a dict mapping metric names to metric values.27 28    We provide an example of a suitable metric_fn that computes ROUGE scores for a summarization model below. Note that29    this example skips some post-processing for readability and simplicity, and should probably not be used as-is!30 31    ```py32    from datasets import load_metric33 34    rouge_metric = load_metric("rouge")35 36 37    def rouge_fn(predictions, labels):38        decoded_predictions = tokenizer.batch_decode(predictions, skip_special_tokens=True)39        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)40        result = rouge_metric.compute(predictions=decoded_predictions, references=decoded_labels)41        return {key: value.mid.fmeasure * 100 for key, value in result.items()}42    ```43 44    The above function will return a dict containing values which will be logged like any other Keras metric:45 46    ```47    {'rouge1': 37.4199, 'rouge2': 13.9768, 'rougeL': 34.361, 'rougeLsum': 35.078148    ```49 50    Args:51        metric_fn (`Callable`):52            Metric function provided by the user. It will be called with two arguments - `predictions` and `labels`.53            These contain the model's outputs and matching labels from the dataset. It should return a dict mapping54            metric names to numerical values.55        eval_dataset (`tf.data.Dataset` or `dict` or `tuple` or `np.ndarray` or `tf.Tensor`):56            Validation data to be used to generate predictions for the `metric_fn`.57        output_cols (`list[str], *optional*):58            A list of columns to be retained from the model output as the predictions. Defaults to all.59        label_cols ('`list[str]`, *optional*'):60            A list of columns to be retained from the input dataset as the labels. Will be autodetected if this is not61            supplied.62        batch_size (`int`, *optional*):63            Batch size. Only used when the data is not a pre-batched `tf.data.Dataset`.64        predict_with_generate (`bool`, *optional*, defaults to `False`):65            Whether we should use `model.generate()` to get outputs for the model.66        use_xla_generation (`bool`, *optional*, defaults to `False`):67            If we're generating, whether to compile model generation with XLA. This can massively increase the speed of68            generation (up to 100X speedup) but will require a new XLA compilation for each input shape. When using XLA69            generation, it's a good idea to pad your inputs to the same size, or to use the `pad_to_multiple_of`70            argument in your `tokenizer` or `DataCollator`, which will reduce the number of unique input shapes and71            save a lot of compilation time. This option has no effect is `predict_with_generate` is `False`.72        generate_kwargs (`dict`, *optional*):73            Keyword arguments to pass to `model.generate()` when generating. Has no effect if `predict_with_generate`74            is `False`.75 76    """77 78    def __init__(79        self,80        metric_fn: Callable,81        eval_dataset: Union[tf.data.Dataset, np.ndarray, tf.Tensor, tuple, dict],82        output_cols: Optional[list[str]] = None,83        label_cols: Optional[list[str]] = None,84        batch_size: Optional[int] = None,85        predict_with_generate: bool = False,86        use_xla_generation: bool = False,87        generate_kwargs: Optional[dict] = None,88    ):89        super().__init__()90        self.metric_fn = metric_fn91        self.batch_size = batch_size92        if not isinstance(eval_dataset, tf.data.Dataset):93            if batch_size is None:94                raise ValueError(95                    "When passing data to KerasMetricCallback that is not a pre-batched tf.data.Dataset "96                    "the batch_size argument must be set."97                )98            # Wrap a tf.data.Dataset around it99            eval_dataset = tf.data.Dataset.from_tensor_slices(eval_dataset).batch(batch_size, drop_remainder=False)100        self.eval_dataset = eval_dataset101        self.predict_with_generate = predict_with_generate102        self.output_cols = output_cols103 104        # This next block attempts to parse out which elements of the dataset should be appended to the labels list105        # that is passed to the metric_fn106        if isinstance(eval_dataset.element_spec, tuple) and len(eval_dataset.element_spec) == 2:107            input_spec, label_spec = eval_dataset.element_spec108        else:109            input_spec = eval_dataset.element_spec110            label_spec = None111        if label_cols is not None:112            for label in label_cols:113                if label not in input_spec:114                    raise ValueError(f"Label {label} is in label_cols but could not be found in the dataset inputs!")115            self.label_cols = label_cols116            self.use_keras_label = False117        elif label_spec is not None:118            # If the dataset inputs are split into a 2-tuple of inputs and labels,119            # assume the second element is the labels120            self.label_cols = None121            self.use_keras_label = True122        elif "labels" in input_spec:123            self.label_cols = ["labels"]124            self.use_keras_label = False125            logging.warning("No label_cols specified for KerasMetricCallback, assuming you want the 'labels' key.")126        elif "start_positions" in input_spec and "end_positions" in input_spec:127            self.label_cols = ["start_positions", "end_positions"]128            self.use_keras_label = False129            logging.warning(130                "No label_cols specified for KerasMetricCallback, assuming you want the "131                "start_positions and end_positions keys."132            )133        else:134            raise ValueError("Could not autodetect label_cols for KerasMetricCallback, please specify them!")135        if parse(tf.__version__) < parse("2.7"):136            logging.warning("TF versions less than 2.7 may encounter issues with KerasMetricCallback!")137 138        self.use_xla_generation = use_xla_generation139        self.generate_kwargs = {} if generate_kwargs is None else generate_kwargs140 141        self.generation_function = None142 143    @staticmethod144    def _concatenate_batches(batches, padding_index=-100):145        # If all batches are unidimensional or same length, do a simple concatenation146        if batches[0].ndim == 1 or all(batch.shape[1] == batches[0].shape[1] for batch in batches):147            return np.concatenate(batches, axis=0)148 149        # Welp, they're not the same length. Let's do some padding150        max_len = max([batch.shape[1] for batch in batches])151        num_samples = sum([batch.shape[0] for batch in batches])152        output = np.full_like(153            batches[0], fill_value=padding_index, shape=[num_samples, max_len] + list(batches[0].shape[2:])154        )155        # i keeps track of which part of the concatenated array we're writing the next batch to156        i = 0157        for batch in batches:158            output[i : i + len(batch), : batch.shape[1]] = batch159            i += len(batch)160        return output161 162    def _postprocess_predictions_or_labels(self, inputs):163        if isinstance(inputs[0], dict):164            outputs = {}165            for key in inputs[0]:166                outputs[key] = self._concatenate_batches([batch[key] for batch in inputs])167            # If it's a dict with only one key, just return the array168            if len(outputs) == 1:169                outputs = list(outputs.values())[0]170        elif isinstance(inputs[0], (tuple, list)):171            outputs = []172            for input_list in zip(*inputs):173                outputs.append(self._concatenate_batches(input_list))174            if len(outputs) == 1:175                outputs = outputs[0]  # If it's a list with only one element, just return the array176        elif isinstance(inputs[0], np.ndarray):177            outputs = self._concatenate_batches(inputs)178        elif isinstance(inputs[0], tf.Tensor):179            outputs = self._concatenate_batches([tensor.numpy() for tensor in inputs])180        else:181            raise TypeError(f"Couldn't handle batch of type {type(inputs[0])}!")182        return outputs183 184    def on_epoch_end(self, epoch, logs=None):185        if hasattr(self.model, "config"):186            ignore_keys = getattr(self.model.config, "keys_to_ignore_at_inference", [])187        else:188            ignore_keys = []189 190        main_input_name = None191        if self.predict_with_generate:192            # This dense conditional recognizes the case where we have an encoder-decoder model, but193            # avoids getting tangled up when we just have a model with a layer called 'encoder'194            if hasattr(self.model, "encoder") and hasattr(self.model.encoder, "main_input_name"):195                main_input_name = self.model.encoder.main_input_name196            else:197                main_input_name = getattr(self.model, "main_input_name", "input_ids")198 199            if self.use_xla_generation and self.generation_function is None:200 201                def generation_function(inputs, attention_mask):202                    return self.model.generate(inputs, attention_mask=attention_mask, **self.generate_kwargs)203 204                self.generation_function = tf.function(generation_function, jit_compile=True)205 206        prediction_list = []207        label_list = []208 209        # The whole predict/generate loop is handled inside this method210        for batch in self.eval_dataset:211            if isinstance(batch, tuple):212                batch, labels = batch213            else:214                labels = None215            if self.predict_with_generate:216                if isinstance(batch, dict):217                    generation_inputs = batch[main_input_name]218                    attention_mask = batch.get("attention_mask", None)219                else:220                    generation_inputs = batch221                    attention_mask = None222                if self.use_xla_generation:223                    predictions = self.generation_function(generation_inputs, attention_mask=attention_mask)224                else:225                    predictions = self.model.generate(226                        generation_inputs, attention_mask=attention_mask, **self.generate_kwargs227                    )228            else:229                predictions = self.model.predict_on_batch(batch)230                if isinstance(predictions, dict):231                    # This converts any dict-subclass to a regular dict232                    # Keras REALLY doesn't like it when we pass around a BatchEncoding or other derived class233                    predictions = dict(predictions)234                    if self.output_cols is not None:235                        predictions = {key: predictions[key] for key in self.output_cols}236                    else:237                        predictions = {238                            key: val for key, val in predictions.items() if key not in ignore_keys + ["loss"]239                        }240            prediction_list.append(predictions)241            if not self.use_keras_label:242                labels = {key: batch[key].numpy() for key in self.label_cols}243            elif isinstance(labels, dict):244                labels = {key: array.numpy() for key, array in labels.items()}245            elif isinstance(labels, (list, tuple)):246                labels = [array.numpy() for array in labels]247            elif isinstance(labels, tf.Tensor):248                labels = labels.numpy()249            else:250                raise TypeError(f"Confused by labels of type {type(labels)}")251            label_list.append(labels)252 253        all_preds = self._postprocess_predictions_or_labels(prediction_list)254        all_labels = self._postprocess_predictions_or_labels(label_list)255 256        metric_output = self.metric_fn((all_preds, all_labels))257        if not isinstance(metric_output, dict):258            raise TypeError(259                f"metric_fn should return a dict mapping metric names to values but instead returned {metric_output}"260            )261        # This is the critical bit - Keras passes a dict containing the loss and standard metric values for this epoch262        # in the logs argument. Ordinarily, this is so the callback can read them, but in this case we write a bunch of263        # new keys in there, which will then get read by the History callback and treated like any other metric value.264        # I promise that I have it in writing from Chollet that this is okay.265        logs.update(metric_output)266 267 268class PushToHubCallback(keras.callbacks.Callback):269    """270    Callback that will save and push the model to the Hub regularly. By default, it pushes once per epoch, but this can271    be changed with the `save_strategy` argument. Pushed models can be accessed like any other model on the hub, such272    as with the `from_pretrained` method.273 274    ```py275    from transformers.keras_callbacks import PushToHubCallback276 277    push_to_hub_callback = PushToHubCallback(278        output_dir="./model_save",279        tokenizer=tokenizer,280        hub_model_id="gpt5-7xlarge",281    )282 283    model.fit(train_dataset, callbacks=[push_to_hub_callback])284    ```285 286    Args:287        output_dir (`str`):288            The output directory where the model predictions and checkpoints will be written and synced with the289            repository on the Hub.290        save_strategy (`str` or [`~trainer_utils.IntervalStrategy`], *optional*, defaults to `"epoch"`):291            The checkpoint save strategy to adopt during training. Possible values are:292 293                - `"no"`: Save is done at the end of training.294                - `"epoch"`: Save is done at the end of each epoch.295                - `"steps"`: Save is done every `save_steps`296        save_steps (`int`, *optional*):297            The number of steps between saves when using the "steps" `save_strategy`.298        tokenizer (`PreTrainedTokenizerBase`, *optional*):299            The tokenizer used by the model. If supplied, will be uploaded to the repo alongside the weights.300        hub_model_id (`str`, *optional*):301            The name of the repository to keep in sync with the local `output_dir`. It can be a simple model ID in302            which case the model will be pushed in your namespace. Otherwise it should be the whole repository name,303            for instance `"user_name/model"`, which allows you to push to an organization you are a member of with304            `"organization_name/model"`.305 306            Will default to the name of `output_dir`.307        hub_token (`str`, *optional*):308            The token to use to push the model to the Hub. Will default to the token in the cache folder obtained with309            `hf auth login`.310        checkpoint (`bool`, *optional*, defaults to `False`):311            Whether to save full training checkpoints (including epoch and optimizer state) to allow training to be312            resumed. Only usable when `save_strategy` is `"epoch"`.313    """314 315    def __init__(316        self,317        output_dir: Union[str, Path],318        save_strategy: Union[str, IntervalStrategy] = "epoch",319        save_steps: Optional[int] = None,320        tokenizer: Optional[PreTrainedTokenizerBase] = None,321        hub_model_id: Optional[str] = None,322        hub_token: Optional[str] = None,323        checkpoint: bool = False,324        **model_card_args,325    ):326        super().__init__()327        if checkpoint and save_strategy != "epoch":328            raise ValueError("Cannot save checkpoints when save_strategy is not 'epoch'!")329        if isinstance(save_strategy, str):330            save_strategy = IntervalStrategy(save_strategy.lower())331        self.save_strategy = save_strategy332        if self.save_strategy == IntervalStrategy.STEPS and (not isinstance(save_steps, int) or save_steps <= 0):333            raise ValueError("Please supply a positive integer argument for save_steps when save_strategy == 'steps'!")334        self.save_steps = save_steps335        output_dir = Path(output_dir)336 337        # Create repo and retrieve repo_id338        if hub_model_id is None:339            hub_model_id = output_dir.absolute().name340        self.hub_model_id = create_repo(repo_id=hub_model_id, exist_ok=True, token=hub_token).repo_id341 342        self.output_dir = output_dir343        self.repo = Repository(str(self.output_dir), clone_from=self.hub_model_id, token=hub_token)344 345        self.tokenizer = tokenizer346        self.last_job = None347        self.checkpoint = checkpoint348        self.training_history = None349        self.model_card_args = model_card_args350 351    def on_train_begin(self, logs=None):352        # Although we can access model.history, we have no guarantees that the History callback will fire before this353        # one, so we keep track of it here too354        self.training_history = []355 356    def on_train_batch_end(self, batch, logs=None):357        if self.save_strategy == IntervalStrategy.STEPS and (batch + 1) % self.save_steps == 0:358            if self.last_job is not None and not self.last_job.is_done:359                return  # The last upload is still running, don't start another360            self.model.save_pretrained(self.output_dir)361            if self.tokenizer is not None:362                self.tokenizer.save_pretrained(self.output_dir)363            _, self.last_job = self.repo.push_to_hub(364                commit_message=f"Training in progress steps {batch}", blocking=False365            )366 367    def on_epoch_end(self, epoch, logs=None):368        logs = logs.copy()  # Don't accidentally write things that Keras will read later369        if "epoch" not in logs:370            logs["epoch"] = epoch371        self.training_history.append(logs)372        if self.save_strategy == IntervalStrategy.EPOCH:373            if self.last_job is not None and not self.last_job.is_done:374                return  # The last upload is still running, don't start another375            self.model.save_pretrained(self.output_dir)376            if self.tokenizer is not None:377                self.tokenizer.save_pretrained(self.output_dir)378            if self.checkpoint:379                checkpoint_dir = os.path.join(self.output_dir, "checkpoint")380                self.model._save_checkpoint(checkpoint_dir, epoch)381            train_summary = TrainingSummary.from_keras(382                model=self.model,383                model_name=self.hub_model_id,384                keras_history=self.training_history,385                **self.model_card_args,386            )387            model_card = train_summary.to_model_card()388            with (self.output_dir / "README.md").open("w") as f:389                f.write(model_card)390            _, self.last_job = self.repo.push_to_hub(391                commit_message=f"Training in progress epoch {epoch}", blocking=False392            )393 394    def on_train_end(self, logs=None):395        # Makes sure the latest version of the model is uploaded396        if self.last_job is not None and not self.last_job.is_done:397            logging.info("Pushing the last epoch to the Hub, this may take a while...")398            while not self.last_job.is_done:399                sleep(1)400        else:401            self.model.save_pretrained(self.output_dir)402            if self.tokenizer is not None:403                self.tokenizer.save_pretrained(self.output_dir)404            train_summary = TrainingSummary.from_keras(405                model=self.model,406                model_name=self.hub_model_id,407                keras_history=self.training_history,408                **self.model_card_args,409            )410            model_card = train_summary.to_model_card()411            with (self.output_dir / "README.md").open("w") as f:412                f.write(model_card)413            self.repo.push_to_hub(commit_message="End of training", blocking=True)414 
Aluode/PerceptionLabPortable · CoolFace