<jupyter_start><jupyter_text>Quantizing a model with Intel Neural Compressor (INC) for text classification tasks This notebook shows how to apply different quantization approaches such as dynamic, post-training and aware training quantization, using the [Intel Neural Compressor](https://github.com/intel/neural-compressor) (INC) library, for any tasks of the GLUE benchmark. This is made possible thanks to 🤗 [Optimum](https://github.com/huggingface/optimum), an extension of 🤗 [Transformers](https://github.com/huggingface/transformers), providing a set of performance optimization tools enabling maximum efficiency to train and run models on targeted hardwares.   If you're opening this Notebook on colab, you will probably need to install 🤗 Transformers, 🤗 Datasets and 🤗 Optimum. Uncomment the following cell and run it.<jupyter_code>#! pip install datasets transformers optimum[intel]<jupyter_output><empty_output><jupyter_text>Make sure your version of Transformers is at least 4.12.0 since the functionality was introduced in that version:<jupyter_code>import transformers

print(transformers.__version__)<jupyter_output>4.12.3<jupyter_text>Note that quantization is currently only supported for CPUs, so we will not be utilizing GPUs / CUDA in this notebook.<jupyter_code>import os

os.environ["CUDA_VISIBLE_DEVICES"] = ""<jupyter_output><empty_output><jupyter_text>The General Language Understanding Evaluation (GLUE) benchmark is a collection of 9 natural language understanding tasks.<jupyter_code>GLUE_TASKS = ["cola", "mnli", "mnli-mm", "mrpc", "qnli", "qqp", "rte", "sst2", "stsb", "wnli"]
task = "sst2"
model_checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
batch_size = 16<jupyter_output><empty_output><jupyter_text>We can set our `quantization_approach` to either `dynamic`, `static` or `aware_training` in order to apply respectively dynamic, post-training or aware training quantization. - Dynamic quantization : activations scaling factors are dynamically computed based on the data observed at runtime. - Post-training quantization : introduces an additional calibration step to estimate the activations quantization range. This step consists in feeding data through the network in order to compute the activations distributions.- Quantization aware training : simulates the effects of quantization during training in order to alleviate its effects on the model's performance.Quantization will be applied on the linear layers as well as on their corresponding input activations.<jupyter_code>SUPPORTED_QUANTIZATION_APPROACH = ["dynamic", "static", "aware_training"]

quantization_approach = "static"<jupyter_output><empty_output><jupyter_text>Loading the dataset We will use the [🤗 Datasets](https://github.com/huggingface/datasets) library to download the data and get the metric we need to use for evaluation (to compare our quantized model to the baseline). This can be easily done with the functions `load_dataset` and `load_metric`.<jupyter_code>from datasets import load_dataset, load_metric<jupyter_output><empty_output><jupyter_text>Apart from `mnli-mm` being a special code, we can directly pass our task name to those functions. `load_dataset` will cache the dataset to avoid downloading it again the next time you run this cell.<jupyter_code>actual_task = "mnli" if task == "mnli-mm" else task
dataset = load_dataset("glue", actual_task)
metric = load_metric("glue", actual_task)<jupyter_output>Reusing dataset glue (/home/ella/.cache/huggingface/datasets/glue/sst2/1.0.0/dacbe3125aa31d7f70367a07a8a9e72a5a0bfeb5fc42e75c9db75b96da6053ad)<jupyter_text>The `dataset` object itself is [`DatasetDict`](https://huggingface.co/docs/datasets/package_reference/main_classes.htmldatasetdict), which contains one key for the training, validation and test set (with more keys for the mismatched validation and test set in the special case of `mnli`).<jupyter_code>dataset<jupyter_output><empty_output><jupyter_text>To access an actual element, you need to select a split first, then give an index:<jupyter_code>dataset["train"][0]<jupyter_output><empty_output><jupyter_text>Note that `load_metric` has loaded the proper metric associated to your task, which is:- for CoLA: [Matthews Correlation Coefficient](https://en.wikipedia.org/wiki/Matthews_correlation_coefficient)- for MNLI (matched or mismatched): Accuracy- for MRPC: Accuracy and [F1 score](https://en.wikipedia.org/wiki/F1_score)- for QNLI: Accuracy- for QQP: Accuracy and [F1 score](https://en.wikipedia.org/wiki/F1_score)- for RTE: Accuracy- for SST-2: Accuracy- for STS-B: [Pearson Correlation Coefficient](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient) and [Spearman's_Rank_Correlation_Coefficient](https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient)- for WNLI: Accuracyso the metric object only computes the one(s) needed for your task.  Preprocessing the data Before we can feed those texts to our model, we need to preprocess them. This is done by a 🤗 Transformers `Tokenizer` which will (as the name indicates) tokenize the inputs (including converting the tokens to their corresponding IDs in the pretrained vocabulary) and put it in a format the model expects, as well as generate the other inputs that model requires.To do all of this, we instantiate our tokenizer with the `AutoTokenizer.from_pretrained` method, which will ensure:- we get a tokenizer that corresponds to the model architecture we want to use,- we download the vocabulary used when pretraining this specific checkpoint.That vocabulary will be cached, so it's not downloaded again the next time we run the cell.<jupyter_code>from transformers import AutoTokenizer
    
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)<jupyter_output><empty_output><jupyter_text>You can directly call this tokenizer on one sentence or a pair of sentences:<jupyter_code>tokenizer("Hello, this one sentence!", "And this sentence goes with it.")<jupyter_output><empty_output><jupyter_text>Depending on the model you selected, you will see different keys in the dictionary returned by the cell above. We don't need to care about them (just know they are required by the model we will instantiate later), you can learn more about them in [this tutorial](https://huggingface.co/transformers/preprocessing.html) if you're interested.To preprocess our dataset, we will thus need the names of the columns containing the sentence(s). The following dictionary keeps track of the correspondence task to column names:<jupyter_code>task_to_keys = {
    "cola": ("sentence", None),
    "mnli": ("premise", "hypothesis"),
    "mnli-mm": ("premise", "hypothesis"),
    "mrpc": ("sentence1", "sentence2"),
    "qnli": ("question", "sentence"),
    "qqp": ("question1", "question2"),
    "rte": ("sentence1", "sentence2"),
    "sst2": ("sentence", None),
    "stsb": ("sentence1", "sentence2"),
    "wnli": ("sentence1", "sentence2"),
}<jupyter_output><empty_output><jupyter_text>We can double check it does work on our current dataset:<jupyter_code>sentence1_key, sentence2_key = task_to_keys[task]
if sentence2_key is None:
    print(f"Sentence: {dataset['train'][0][sentence1_key]}")
else:
    print(f"Sentence 1: {dataset['train'][0][sentence1_key]}")
    print(f"Sentence 2: {dataset['train'][0][sentence2_key]}")<jupyter_output>Sentence: hide new secretions from the parental units<jupyter_text>We can them write the function that will preprocess our samples. We just feed them to the `tokenizer` with the argument `truncation=True`. This will ensure that an input longer that what the model selected can handle will be truncated to the maximum length accepted by the model.<jupyter_code>max_seq_length = min(128, tokenizer.model_max_length)
padding = "max_length"

def preprocess_function(examples):
    args = (
        (examples[sentence1_key],) if sentence2_key is None else (examples[sentence1_key], examples[sentence2_key])
    )
    return tokenizer(*args, padding=padding, max_length=max_seq_length, truncation=True)<jupyter_output><empty_output><jupyter_text>To apply this function on all the sentences (or pairs of sentences) in our dataset, we just use the `map` method of our `dataset` object we created earlier. This will apply the function on all the elements of all the splits in `dataset`, so our training, validation and testing data will be preprocessed in one single command.<jupyter_code>encoded_dataset = dataset.map(preprocess_function, batched=True)<jupyter_output>Loading cached processed dataset at /home/ella/.cache/huggingface/datasets/glue/sst2/1.0.0/dacbe3125aa31d7f70367a07a8a9e72a5a0bfeb5fc42e75c9db75b96da6053ad/cache-0f2a061fdc4584db.arrow
Loading cached processed dataset at /home/ella/.cache/huggingface/datasets/glue/sst2/1.0.0/dacbe3125aa31d7f70367a07a8a9e72a5a0bfeb5fc42e75c9db75b96da6053ad/cache-32d01f4d33804350.arrow
Loading cached processed dataset at /home/ella/.cache/huggingface/datasets/glue/sst2/1.0.0/dacbe3125aa31d7f70367a07a8a9e72a5a0bfeb5fc42e75c9db75b96da6053ad/cache-8105f2045c8703d0.arrow<jupyter_text>Even better, the results are automatically cached by the 🤗 Datasets library to avoid spending time on this step the next time you run your notebook. The 🤗 Datasets library is normally smart enough to detect when the function you pass to map has changed (and thus requires to not use the cache data). For instance, it will properly detect if you change the task in the first cell and rerun the notebook. 🤗 Datasets warns you when it uses cached files, you can pass `load_from_cache_file=False` in the call to `map` to not use the cached files and force the preprocessing to be applied again.Note that we passed `batched=True` to encode the texts by batches together. This is to leverage the full benefit of the fast tokenizer we loaded earlier, which will use multi-threading to treat the texts in a batch concurrently.  Applying quantization on the model Now that our data is ready, we can download the pretrained model and fine-tune it. Since all our tasks are about sentence classification, we use the `AutoModelForSequenceClassification` class. Like with the tokenizer, the `from_pretrained` method will download and cache the model for us. The only thing we have to specify is the number of labels for our problem (which is always 2, except for STS-B which is a regression problem and MNLI where we have 3 labels):<jupyter_code>from transformers import AutoModelForSequenceClassification, TrainingArguments, Trainer

fp_model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)<jupyter_output><empty_output><jupyter_text>The warning is telling us we are throwing away some weights (the `vocab_transform` and `vocab_layer_norm` layers) and randomly initializing some other (the `pre_classifier` and `classifier` layers). This is absolutely normal in this case, because we are removing the head used to pretrain the model on a masked language modeling objective and replacing it with a new head for which we don't have pretrained weights, so the library warns us we should fine-tune this model before using it for inference, which is exactly what we are going to do. To instantiate a `Trainer`, we will need to define two more things. The most important is the [`TrainingArguments`](https://huggingface.co/transformers/main_classes/trainer.htmltransformers.TrainingArguments), which is a class that contains all the attributes to customize the training. It requires one folder name, which will be used to save the checkpoints of the model, and all other arguments are optional:<jupyter_code>model_name = model_checkpoint.split("/")[-1]
output = f"{model_name}-finetuned-{task}"

args = TrainingArguments(
    output,
    evaluation_strategy = "epoch",
    save_strategy = "epoch",
    per_device_train_batch_size=batch_size,
    per_device_eval_batch_size=batch_size,
    dataloader_drop_last=False if quantization_approach == "dynamic" else True,
)<jupyter_output><empty_output><jupyter_text>The last thing to define for our `Trainer` is how to compute the metrics from the predictions. We need to define a function for this, which will just use the `metric` we loaded earlier, the only preprocessing we have to do is to take the argmax of our predicted logits (our just squeeze the last axis in the case of STS-B):<jupyter_code>import numpy as np

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    if task != "stsb":
        predictions = np.argmax(predictions, axis=1)
    else:
        predictions = predictions[:, 0]
    return metric.compute(predictions=predictions, references=labels)<jupyter_output><empty_output><jupyter_text>Then we just need to pass all of this along with our datasets to the `Trainer`:<jupyter_code>from transformers import default_data_collator

validation_key = "validation_mismatched" if task == "mnli-mm" else "validation_matched" if task == "mnli" else "validation"
trainer = Trainer(
    model=fp_model,
    args=args,
    train_dataset=encoded_dataset["train"] if quantization_approach == "aware_training" else None,
    eval_dataset=encoded_dataset[validation_key],
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
    data_collator=default_data_collator,
)<jupyter_output><empty_output><jupyter_text>In the case where we want to apply quantization aware training, we need to pass to the Intel Neural Compressor (INC) library a training function. Note that, as we are using a `Trainer`, we need to set its `model` attribute to the quantized model resulting from the INC library.<jupyter_code>def train_func(model):
    trainer.model_wrapped = model
    trainer.model = model    
    train_result = trainer.train()
    metrics = train_result.metrics
    trainer.save_model() 
    trainer.save_metrics("train", metrics)
    trainer.save_state()<jupyter_output><empty_output><jupyter_text>In order to evaluate the model's performance before and after quantization, we need to define an evaluation function. The metric chosen to evaluate the drop in performance resulting from quantization will be Matthews correlation coefficient (MCC) for CoLA, Pearson correlation coefficient (PCC) for STS-B and accuracy for any other tasks.<jupyter_code>metric_name = "eval_" + ("pearson" if task == "stsb" else "matthews_correlation" if task == "cola" else "accuracy")

def eval_func(model):
    trainer.model = model
    metrics = trainer.evaluate()
    return metrics.get(metric_name)

fp_model_metric = eval_func(fp_model)
print(f"The full-precision model has an {metric_name} of {fp_model_metric}.")<jupyter_output>The following columns in the evaluation set  don't have a corresponding argument in `DistilBertForSequenceClassification.forward` and have been ignored: idx, sentence.
***** Running Evaluation *****
  Num examples = 872
  Batch size = 16<jupyter_text>We instantiate `IncConfig` using a configuration file containing all the informations related to quantization and tuning objective. We can set the quantization approach as well as the accuracy target, currently tolerating a 0.02 relative performance drop when compared to our baseline which is the full-precision model.<jupyter_code>from optimum.intel.neural_compressor import IncConfig, IncQuantizationMode

config = "echarlaix/bert-base-dynamic-quant-test"
q8_config = IncConfig.from_pretrained(config, config_file_name="quantization.yml")

accuracy_criterion = 0.02
q8_config.set_config("tuning.accuracy_criterion.relative", accuracy_criterion)
quant_approach = getattr(IncQuantizationMode, quantization_approach.upper()).value
q8_config.set_config("quantization.approach", quant_approach)<jupyter_output><empty_output><jupyter_text>For both post-training quantization and quantization aware training, we use PyTorch FX Graph Mode Quantization.<jupyter_code>input_names = None
if q8_config.get_config("quantization.approach") != IncQuantizationMode.DYNAMIC.value:
    from transformers.utils.fx import symbolic_trace

    q8_config.set_config("model.framework", "pytorch_fx")
    fp_model.config.save_pretrained(output)
    input_names = ["input_ids", "attention_mask", "token_type_ids", "labels"]
    fp_model = symbolic_trace(
        fp_model,
        input_names=input_names,
        batch_size=batch_size,
        sequence_length=max_seq_length,
    )<jupyter_output>Configuration saved in distilbert-base-uncased-finetuned-sst-2-english-finetuned-sst2/config.json
/home/ella/miniconda3/envs/tmp_notebook/lib/python3.8/site-packages/torch/fx/symbolic_trace.py:434: UserWarning: Was not able to add assertion to guarantee correct inputs to specialized function. It is up to the user to make sure that your inputs match the inputs you specialized the function with.
  torch.warnings.warn(<jupyter_text>To instantiate an `IncQuantizer`, we need a config containing all the informations relative to quantization and tuning (which can be either a path to a YAML file or an `IncConfig` object), the model to quantize and finally an evaluation function which will be used to evaluate the quantization impact and thus verify if it fits the tolerance defined by the user.`IncQuantizer` can now take care of the all quantization process through the Intel Neural Compression (INC) library.<jupyter_code>from optimum.intel.neural_compressor import IncQuantizer

quantizer = IncQuantizer(q8_config, fp_model, eval_func=eval_func)

if quantizer.approach == IncQuantizationMode.DYNAMIC.value:
    q_model = quantizer.fit_dynamic()
elif quantizer.approach == IncQuantizationMode.STATIC.value:
    quantizer.calib_dataloader = trainer.get_eval_dataloader()
    q_model = quantizer.fit_static()
elif quantizer.approach == IncQuantizationMode.AWARE_TRAINING.value:
    quantizer.train_func = train_func
    q_model = quantizer.fit_aware_training()
q_model_metric = eval_func(q_model.model)
print(f"The resulting quantized model has an {metric_name} of {q_model_metric}.")
print(f"This results in a drop of {fp_model_metric - q_model_metric} in {metric_name} when compared to the full-precision model.")
import torch

def get_model_size(model):
    torch.save(model.state_dict(), "tmp.pt")
    model_size = os.path.getsize("tmp.pt") / 10**6
    os.remove("tmp.pt")
    return round(model_size, 2)

fp_model_size = get_model_size(fp_model) 
q_model_size = get_model_size(q_model.model) 

print(f"The full-precision model size is {fp_model_size} MB while the quantized model one is {q_model_size} MB.")
print(f"This results in a gain in memory of {fp_model_size - q_model_size} MB.")<jupyter_output>The full-precision model size is 267.86 MB while the quantized model one is 139.43 MB.
This results in a gain in memory of 128.43 MB.<jupyter_text>We save the resulting quantized model as well as the config YAML file containing all the informations relative to the model quantization.<jupyter_code>import yaml

trainer.save_model(output)
config_path = os.path.join(output, "best_configure.yaml")
with open(config_path, "w") as f:
    yaml.dump(q_model.tune_cfg, f, default_flow_style=False)<jupyter_output>Saving model checkpoint to distilbert-base-uncased-finetuned-sst-2-english-finetuned-sst2
Trainer.model is not a `PreTrainedModel`, only saving its state dict.
tokenizer config file saved in distilbert-base-uncased-finetuned-sst-2-english-finetuned-sst2/tokenizer_config.json
Special tokens file saved in distilbert-base-uncased-finetuned-sst-2-english-finetuned-sst2/special_tokens_map.json<jupyter_text>Loading the quantized model The previously saved config file containing all the informations relative to the model quantization is used to instantiate an`IncOptimizedConfig`. We then load the model using `IncQuantizedModelForSequenceClassification`.<jupyter_code>from optimum.intel.neural_compressor.quantization import IncQuantizedModelForSequenceClassification
from optimum.intel.neural_compressor.config import IncOptimizedConfig

inc_config = IncOptimizedConfig(config_path)

loaded_model = IncQuantizedModelForSequenceClassification.from_pretrained(
    output,
    inc_config=inc_config,
    input_names=input_names,
    batch_size=batch_size,
    sequence_length=max_seq_length,
)
loaded_model.eval()
loaded_model_metric = eval_func(loaded_model)
if loaded_model_metric == q_model_metric:
    print("The quantized model was successfully loaded.")
else:
    print("The quantized model was NOT successfully loaded.")<jupyter_output>The quantized model was successfully loaded.
{'fraction_non_alphanumeric': 0.07794630147744563, 'fraction_numerical': 0.014381779806606784, 'mean_word_length': 5.979787598492634, 'pattern_counts': {'":': 11, '<': 96, '<?xml version=': 0, '>': 96, 'https://': 12, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '26', 'dir': 'jupyter-structured-clean-dedup', 'id': '180750', 'license': 'Apache-2.0', 'n_tokens_mistral': 6208, 'n_tokens_neox': 5885, 'n_words': 2064, 'path': 'examples/text_classification_quantization_inc.ipynb', 'repo_name': 'jflam/notebooks', 'size': '20373'}