chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:55 +08:00
commit c8a779b1bb
1887 changed files with 3245738 additions and 0 deletions
@@ -0,0 +1,3 @@
*/weights
efficientnet/models/*
*/test_qdq_node_placement
@@ -0,0 +1,103 @@
# About
This folder contains the Quantization-Aware Training (QAT) workflow for [standard networks](#step-1-model-quantization-and-fine-tuning).
The QAT end-to-end workflow (TF2-to-ONNX) consists of the following steps:
- Model quantization using the `quantize_model` function with `NVIDIA` quantization scheme.
- QAT model fine-tuning (saves checkpoints).
- Baseline vs QAT models accuracy comparison.
- QAT model conversion to SavedModel format.
- Conversion of SavedModel to ONNX.
- TensorRT engine building via ONNX file and inference.
# Requirements
## 1. Base requirements
1. Install `tensorflow-quantization` toolkit.
2. Install additional requirements: `pip install -r requirements.txt`.
3. (Optional) Install TensorRT for full workflow support (needed for `infer_engine.py`).
**Note**: For CLI run, please go to the cloned repository's root directory and run `export PYTHONPATH=$PWD`, so that the `examples` folder is available for import.
## 2. Data preparation
### A. Raw data download
We are using the ImageNet 2012 dataset (task 1 - image classification), which requires manual downloads due to terms of access agreements.
Please login/sign-up on [the ImageNet website](https://image-net.org/challenges/LSVRC/2012/2012-downloads.php) and download the "train/validation data".
This is needed for the QAT model fine-tuning, and it is also used to evaluate the Baseline and QAT models.
### B. Conversion to tfrecord
Our workflow supports `tfrecord` format, so please follow the following instructions (modified from [TensorFlow's instructions](https://github.com/tensorflow/tpu/tree/master/tools/datasets#imagenet_to_gcspy)) to convert the downloaded `.tar` ImageNet files to the required format:
1. Set `IMAGENET_HOME=/path/to/imagenet/tar/files` in [`data/imagenet_data_setup.sh`](data/imagenet_data_setup.sh).
2. Download [`imagenet_to_gcs.py`](https://github.com/tensorflow/tpu/blob/master/tools/datasets/imagenet_to_gcs.py) to `$IMAGENET_HOME`.
3. Run `./data/imagenet_data_setup.sh`.
# Workflow
## Step 1: Model quantization and fine-tuning
Model quantization, fine-tuning, and conversion to ONNX.
Example models:
| Model | Task | Script - QAT Workflow |
|---------------|------------------|------------------------------|
| ResNet | Classification | [resnet](resnet) |
| EfficientNet | Classification | [efficientnet](efficientnet) |
| MobileNet | Classification | [mobilenet](mobilenet) |
| Inception | Classification | [inception](inception) |
> For each model's performance results, please refer to the toolkit's User Guide ("Model Zoo").
## Step 2: TensorRT deployment
Build the TensorRT engine and evaluate its latency and accuracy performances.
#### 2.1. Build TensorRT engine from ONNX
Convert the ONNX model into a TensorRT engine (also obtains latency measurements):
```sh
trtexec --onnx=model_qat.onnx --int8 --saveEngine=model_qat.engine --verbose
```
Arguments:
* `--onnx`: Path to QAT onnx graph.
* `--saveEngine`: Output filename of TensorRT engine.
* `--verbose`: Flag to enable verbose logging.
#### 2.2. TensorRT Inference
Obtain accuracy results on the validation dataset:
```sh
python infer_engine.py --engine=<path_to_trt_engine> --data_dir=<path_to_tfrecord_val_data> -b=<batch_size>
```
Arguments:
- `-e, --engine`: TensorRT engine filename (to load).
- `-m, --model_name`: Name of the model, needed to choose the appropriate input pre-processing. Options={`resnet_v1` (default), `resnet_v2`, `efficientnet_b0`, `efficientnet_b3`, `mobilenet_v1`, `mobilenet_v2`}.
- `-d, --data_dir`: Path to directory of input images in **tfrecord format** (`data["validation"]`).
- `-k, --top_k_value` (default=1): Value of `K` for the top-K predictions used in the accuracy calculation.
- `-b, --batch_size` (default=1): Number of inputs to send in parallel (up to max batch size of engine).
- `--log_file`: Filename to save logs.
Outputs:
- `.log` file: contains the engine's performance accuracy.
# Additional resources
The following resources provide a deeper understanding about Quantization aware training, TF2ONNX and importing a model into TensorRT using Python.
**Quantization Aware Training**
* <a href="https://developer.nvidia.com/blog/achieving-fp32-accuracy-for-int8-inference-using-quantization-aware-training-with-tensorrt/">Achieving FP32 Accuracy for INT8 Inference Using Quantization Aware Training with NVIDIA TensorRT</a>
- [Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference](https://arxiv.org/pdf/1712.05877.pdf)
- [Quantization Aware Training guide](https://www.tensorflow.org/model_optimization/guide/quantization/training)
- [Deep Residual Learning for Image Recognition](https://arxiv.org/pdf/1512.03385.pdf)
**Parsers**
- [TF2ONNX Converter](https://github.com/onnx/tensorflow-onnx)
- [ONNX Parser](https://docs.nvidia.com/deeplearning/sdk/tensorrt-api/python_api/parsers/Onnx/pyOnnx.html)
**Documentation**
- [Introduction To NVIDIAs TensorRT Samples](https://docs.nvidia.com/deeplearning/sdk/tensorrt-sample-support-guide/index.html#samples)
- [Working With TensorRT Using The Python API](https://docs.nvidia.com/deeplearning/sdk/tensorrt-developer-guide/index.html#python_topics)
- [Importing A Model Using A Parser In Python](https://docs.nvidia.com/deeplearning/sdk/tensorrt-developer-guide/index.html#import_model_python)
- [NVIDIAs TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,350 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright 2018 & 2016 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Changes made by NVIDIA (2022):
- Added: load_data_tfrecord_tf() and load_data() functions
- Modified preprocess_image_record(): preprocess_image() + tfrecord data deserialization + decode jpeg
- Updated global constants with supported models: _DEFAULT_IMAGE_SIZE and _RESIZE_MIN
About this file: Standalone script for ImageNet TFRecord data loading and input image pre-processing for supported
models. Follows TensorFlow's codebase data_loading + pre-processing workflow.
Important links:
- TF's codebase:
https://github.com/tensorflow/models/blob/master/official/legacy/image_classification/resnet/imagenet_preprocessing.py
- Deserialize tfrecord:
https://github.com/tensorflow/models/blob/master/official/vision/dataloaders/tf_example_decoder.py
"""
import os
import tensorflow as tf
import PIL.Image
import numpy as np
from typing import Dict, Union
_SUPPORTED_MODEL_NAMES = [
"resnet_v1",
"resnet_v2",
"efficientnet_b0",
"efficientnet_b3",
"mobilenet_v1",
"mobilenet_v2",
"inception_v3",
]
_NUM_CLASSES = 1000
_NUM_IMAGES = {
"train": 1281167,
"validation": 50000,
}
_DEFAULT_IMAGE_SIZE = {
"resnet_v1": 224,
"resnet_v2": 299,
"efficientnet_b0": 224,
"efficientnet_b3": 300,
"mobilenet_v1": 224,
"mobilenet_v2": 224,
"inception_v3": 299,
}
_NUM_CHANNELS = 3
_RESIZE_MIN = {
"resnet_v1": 256,
"resnet_v2": 342,
"efficientnet_b0": 256,
"efficientnet_b3": 342,
"mobilenet_v1": 256,
"mobilenet_v2": 256,
"inception_v3": 342,
}
def load_image_np(test_image, model_name: str = "resnet_v1"):
# Image is loaded in NHWC format
image_np = np.asarray(PIL.Image.open(test_image).convert('RGB'))
image = tf.constant(image_np)
image = _aspect_preserving_resize(image, _RESIZE_MIN[model_name])
image = _central_crop(image, _DEFAULT_IMAGE_SIZE[model_name], _DEFAULT_IMAGE_SIZE[model_name])
image = preprocess_model_func(image, model_name)
return image
def get_filenames(
data_dir: str,
is_training: bool = False,
num_train_files: int = 1024,
num_val_files: int = 128,
):
"""
Returns filenames for dataset.
Args:
data_dir (str): directory where data is stored.
is_training (bool): indicates whether to return the 'train' (True) or 'validation' (False) data filenames.
num_train_files (int): number of tfrecord shards available for training.
num_val_files (int): number of tfrecord shards available for validation.
Returns:
List: list of shards filenames to compose the dataset.
"""
if is_training:
return [
# Example: train-00000-of-01024
os.path.join(data_dir, "train-{:05d}-of-{:05d}".format(i, num_train_files))
for i in range(num_train_files)
]
else:
return [
os.path.join(
data_dir, "validation-{:05d}-of-{:05d}".format(i, num_val_files)
)
for i in range(num_val_files)
]
def _deserialize_image_record(record):
feature_map = {
"image/encoded": tf.io.FixedLenFeature([], tf.string, ""),
"image/class/label": tf.io.FixedLenFeature([], tf.int64, -1),
"image/class/text": tf.io.FixedLenFeature([], tf.string, ""),
"image/object/bbox/xmin": tf.io.VarLenFeature(dtype=tf.float32),
"image/object/bbox/ymin": tf.io.VarLenFeature(dtype=tf.float32),
"image/object/bbox/xmax": tf.io.VarLenFeature(dtype=tf.float32),
"image/object/bbox/ymax": tf.io.VarLenFeature(dtype=tf.float32),
}
with tf.name_scope("deserialize_image_record"):
obj = tf.io.parse_single_example(record, feature_map)
imgdata = obj["image/encoded"]
label = tf.cast(obj["image/class/label"], tf.int32)
bbox = tf.stack(
[
obj["image/object/bbox/%s" % x].values
for x in ["ymin", "xmin", "ymax", "xmax"]
]
)
bbox = tf.transpose(tf.expand_dims(bbox, 0), [0, 2, 1])
text = obj["image/class/text"]
return imgdata, label, bbox, text
def _aspect_preserving_resize(image: tf.Tensor, resize_min: Union[int, tf.Tensor]):
"""Resize images preserving the original aspect ratio.
Args:
image (tf.Tensor): A 3-D image `Tensor`.
resize_min (int): A python integer or scalar `Tensor` indicating the size of the smallest side after resize.
Returns:
resized_image (tf.Tensor): A 3-D `Tensor` containing the resized image.
"""
shape = tf.shape(image)
height, width = shape[0], shape[1]
new_height, new_width = _smallest_size_at_least(height, width, resize_min)
resized_image = tf.image.resize(
image, [new_height, new_width], method=tf.image.ResizeMethod.BILINEAR
)
return resized_image
def _smallest_size_at_least(height, width, resize_min):
resize_min = tf.cast(resize_min, tf.float32)
# Convert to floats to make subsequent calculations go smoothly.
height, width = tf.cast(height, tf.float32), tf.cast(width, tf.float32)
smaller_dim = tf.minimum(height, width)
scale_ratio = resize_min / smaller_dim
# Convert back to ints to make heights and widths that TF ops will accept.
new_height = tf.cast(height * scale_ratio, tf.int32)
new_width = tf.cast(width * scale_ratio, tf.int32)
return new_height, new_width
def _central_crop(image, crop_height, crop_width):
shape = tf.shape(image)
height, width = shape[0], shape[1]
amount_to_be_cropped_h = height - crop_height
crop_top = amount_to_be_cropped_h // 2
amount_to_be_cropped_w = width - crop_width
crop_left = amount_to_be_cropped_w // 2
return tf.slice(image, [crop_top, crop_left, 0], [crop_height, crop_width, -1])
def preprocess_image_record(record, min_size=256, image_height=224, image_width=224):
"""
This function performs image cropping so all images in the dataset have the same height and width dimensions.
No value pre-processing is done here.
"""
imgdata, label, _, _ = _deserialize_image_record(record)
# Subtract one so that ImageNet labels are in [0, 1000). This assumes your dataset contains 'background' as 0.
label -= 1
try:
image = tf.image.decode_jpeg(
imgdata,
channels=_NUM_CHANNELS,
fancy_upscaling=False,
dct_method="INTEGER_FAST",
)
except:
image = tf.image.decode_image(imgdata, channels=_NUM_CHANNELS)
image = tf.cast(image, tf.float32)
image = _aspect_preserving_resize(image, min_size)
image = _central_crop(image, image_height, image_width)
return image, label
def preprocess_model_func(image: tf.Tensor, model_name: str = "resnet_v1"):
if model_name == "resnet_v1":
return tf.keras.applications.resnet.preprocess_input(image)
elif model_name == "resnet_v2":
return tf.keras.applications.resnet_v2.preprocess_input(image)
elif model_name == "mobilenet_v1":
return tf.keras.applications.mobilenet.preprocess_input(image)
elif model_name == "mobilenet_v2":
return tf.keras.applications.mobilenet_v2.preprocess_input(image)
elif model_name == "inception_v3":
return tf.keras.applications.inception_v3.preprocess_input(image)
else:
# efficientnet doesn't need specific pre-processing (included in the model itself).
print("No further pre-processing found for {}".format(model_name))
return image
def load_data_tfrecord_tf(
data_dir: str = "./data/imagenet",
batch_size: int = 8,
num_train_files: int = 1024,
num_val_files: int = 128,
model_name: str = "resnet_v1",
) -> Dict[str, tf.data.Dataset]:
"""
Load ImageNet with TensorFlow Datasets (TFDS).
Args:
data_dir (str): directory where data is stored.
batch_size (int): batch_size for dataloader.
num_train_files (int): number of tfrecord shards available for training.
num_val_files (int): number of tfrecord shards available for validation.
model_name (str): Model name, used to decide which input pre-processing is needed.
Options={supported_model_names}.
Returns:
dataset_dict (Dict[str, tf.data.Dataset]): dictionary with 'train' and 'validation' datasets.
Raises:
ValueError: raised if 'model_name' is not supported.
""".format(
supported_model_names=_SUPPORTED_MODEL_NAMES
)
# 1. Load ImageNet2012 train dataset - needs to manually download the full ImageNet2012 dataset first.
assert os.path.exists(data_dir)
if model_name not in _SUPPORTED_MODEL_NAMES:
raise ValueError(
"Invalid model name ",
model_name,
" provided. Please select among {}".format(_SUPPORTED_MODEL_NAMES),
)
# 2. Make train/validation datasets
dataset_dict = {}
for key, is_training in zip(["train", "validation"], [True, False]):
filenames = get_filenames(
data_dir,
is_training=is_training,
num_train_files=num_train_files,
num_val_files=num_val_files,
)
dataset = tf.data.TFRecordDataset(filenames)
# Image cropping and resizing
if model_name in _DEFAULT_IMAGE_SIZE and model_name in _RESIZE_MIN:
dataset = dataset.map(
lambda record: preprocess_image_record(
record,
min_size=_RESIZE_MIN[model_name],
image_height=_DEFAULT_IMAGE_SIZE[model_name],
image_width=_DEFAULT_IMAGE_SIZE[model_name],
)
)
else:
dataset = dataset.map(preprocess_image_record)
dataset = dataset.map(lambda image, label: (preprocess_model_func(image, model_name), label))
# Divide dataset into batches
dataset = dataset.batch(batch_size, drop_remainder=True)
dataset_dict[key] = dataset
return dataset_dict
def load_data(
hyperparams: Dict, model_name: str = "resnet_v1"
) -> [tf.data.Dataset, tf.data.Dataset]:
""" Loads ImageNet data in `tfrecord` format (requires manual data download).
Args:
hyperparams (Dict): dictionary with necessary hyper-parameters for data loading.
model_name (str): Model name, used to decide which input pre-processing is needed.
Options={supported_model_names}.
Returns:
train_batches (tf.data.Dataset): 'train' dataset.
val_batches (tf.data.Dataset): 'validation' dataset.
""".format(
supported_model_names=_SUPPORTED_MODEL_NAMES
)
data_batches = load_data_tfrecord_tf(
data_dir=hyperparams["tfrecord_data_dir"],
batch_size=hyperparams["batch_size"],
model_name=model_name,
)
train_batches, val_batches = (data_batches["train"], data_batches["validation"])
if hyperparams["train_data_size"] is not None:
train_batches = train_batches.take(hyperparams["train_data_size"])
if hyperparams["val_data_size"] is not None:
val_batches = val_batches.take(hyperparams["val_data_size"])
return train_batches, val_batches
@@ -0,0 +1,44 @@
export IMAGENET_HOME=/media/Data/imagenet_data
# Setup folders
mkdir -p $IMAGENET_HOME/validation
mkdir -p $IMAGENET_HOME/train
# ###### Modification 1: set .tar files path to $IMAGENET_HOME #############
# Extract validation and training
tar xf $IMAGENET_HOME/ILSVRC2012_img_val.tar -C $IMAGENET_HOME/validation
tar xf $IMAGENET_HOME/ILSVRC2012_img_train.tar -C $IMAGENET_HOME/train
# ##########################################################################
# Extract and then delete individual training tar files This can be pasted
# directly into a bash command-line or create a file and execute.
cd $IMAGENET_HOME/train
for f in *.tar; do
d=`basename $f .tar`
mkdir $d
tar xf $f -C $d
done
cd $IMAGENET_HOME # Move back to the base folder
# [Optional] Delete tar files if desired as they are not needed
rm $IMAGENET_HOME/train/*.tar
# ###### Modification 2: Updated deprecated link #############
# Download labels file.
wget -O $IMAGENET_HOME/synset_labels.txt \
https://raw.githubusercontent.com/tensorflow/models/master/research/slim/datasets/imagenet_2012_validation_synset_labels.txt
# ############################################################
# Process the files. Remember to get the script from github first. The TFRecords
# will end up in the --local_scratch_dir. To upload to gcs with this method
# leave off `nogcs_upload` and provide gcs flags for project and output_path.
python imagenet_to_gcs.py \
--raw_data_dir=$IMAGENET_HOME \
--local_scratch_dir=$IMAGENET_HOME/tf_records \
--nogcs_upload
# ######## Modification 3: move train and validation files to root dir #######################
mv $IMAGENET_HOME/tf_records/train* $IMAGENET_HOME/tf_records
mv $IMAGENET_HOME/tf_records/validation* $IMAGENET_HOME/tf_records
# ############################################################################################
@@ -0,0 +1,168 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
This module contains test cases for our data loader, which contains data loading and pre-processing functions
for the ImageNet2012 dataset in 'tfrecord' format.
NOTE: the user needs to manually download the full ImageNet2012 dataset first.
"""
import tensorflow as tf
from examples.data.data_loader import load_data
import numpy as np
from collections import defaultdict
from typing import Dict
import pytest
DATA_HYPERPARAMS = {
"tfrecord_data_dir": "/media/Data/ImageNet/train-val-tfrecord",
"batch_size": 64,
"train_data_size": 100, # If 'None', consider all data, otherwise, consider subset.
"val_data_size": 100, # If 'None', consider all data, otherwise, consider subset.
}
def load_tfrecord_mean_min_max(model_name: str) -> [Dict, Dict, Dict]:
"""
Loads `tfrecord` dataset and calculates the data's mean, min, and max values.
Args:
model_name (str): model name for data pre-processing.
Returns:
total_mean_dict: dictionary with MEAN values in 'R', 'G', 'B'.
total_min_dict: dictionary with MIN values in 'R', 'G', 'B'.
total_max_dict: dictionary with MAX values in 'R', 'G', 'B'.
"""
# 1. Data loading
train_batches, val_batches = load_data(
hyperparams=DATA_HYPERPARAMS, model_name=model_name
)
assert isinstance(train_batches, tf.data.Dataset) and isinstance(
val_batches, tf.data.Dataset
)
# 2. Test input preprocessing
mean = defaultdict(list)
min = defaultdict(list)
max = defaultdict(list)
for batch in [train_batches]:
for examples in batch:
image, label = examples
image_dict = defaultdict()
for i, c in zip([0, 1, 2], ["R", "G", "B"]):
image_dict[c] = image[:, :, :, i]
mean[c].append(tf.math.reduce_mean(image_dict[c]))
min[c].append(tf.math.reduce_min(image_dict[c]))
max[c].append(tf.math.reduce_max(image_dict[c]))
total_mean_dict = defaultdict(list)
total_min_dict = defaultdict(list)
total_max_dict = defaultdict(list)
for c in ["R", "G", "B"]:
total_mean_dict[c] = tf.math.reduce_mean(mean[c])
total_min_dict[c] = tf.math.reduce_min(min[c])
total_max_dict[c] = tf.math.reduce_max(max[c])
return total_mean_dict, total_min_dict, total_max_dict
def test_imagenet_tfrecord_efficientnetb0():
"""
Tests data loading and pre-processing for EfficientNet-B0.
Note that EfficientNet doesn't have any input pre-processing methods besides image resizing and cropping.
See `data_loader.preprocess_image_record()`.
"""
print("------------ EfficientNet-B0 pre-processing test -------------")
# 1. Data loading and get mean, max, min of data without input preprocessing
total_mean_dict, total_min_dict, total_max_dict = load_tfrecord_mean_min_max(
model_name="efficientnet_b0"
)
# 2. Check if mean is as expected and max/min values
mean_RGB = [123.68, 116.779, 103.939]
mean_RGB_obtained = list(
total_mean_dict.values()
) # [total_mean_dict['R'], total_mean_dict['G'], total_mean_dict['B']]
mean_diff = abs(np.array(mean_RGB_obtained) - np.array(mean_RGB))
print(" Expected mean (RGB): {}".format(mean_RGB))
print(" Calculated mean (RGB): {}".format(np.array(mean_RGB_obtained)))
print(" Difference: {}".format(np.array(mean_diff)))
assert (mean_diff <= 3.0).all()
# 3. Values expected to be between 0 and 255
total_min = min(total_min_dict["R"], min(total_min_dict["G"], total_min_dict["B"]))
total_max = max(total_max_dict["R"], max(total_max_dict["G"], total_max_dict["B"]))
assert total_min >= 0.0 and total_max <= 255.0
def test_imagenet_tfrecord_resnetv1():
"""Tests data loading and pre-processing for ResNetv1.
ResNetv1 input pre-processing:
- Resizing + cropping
- "The images are converted from RGB to BGR, then each color channel is zero-centered with respect to the
ImageNet dataset, without scaling."
- Zero-center: (data - mean(data) / std(data)) -> In this case, std(data) = None.
"""
print("------------ ResNetv1 pre-processing test -------------")
# 1. Data loading and get mean, max, min of data without input preprocessing
total_mean_dict, total_min_dict, total_max_dict = load_tfrecord_mean_min_max(
model_name="resnet_v1"
)
# 2.1 Data should be zero-centered (mean=0)
mean_RGB_obtained = list(total_mean_dict.values())
print(" Expected mean (RGB): [0, 0, 0]")
print(" Calculated mean (RGB): {}".format(np.array(mean_RGB_obtained)))
print(" Min values: {}".format(np.array(list(total_min_dict.values()))))
print(" Max values: {}".format(np.array(list(total_max_dict.values()))))
assert (abs(np.array(mean_RGB_obtained)) <= 3.0).all()
# 2.2 No scaling, meaning values are between -255 and 255 (after zero-centering)
assert (np.array(list(total_min_dict.values())) >= -255.0).all()
assert (np.array(list(total_max_dict.values())) <= 255.0).all()
def test_imagenet_tfrecord_resnetv2():
"""Tests data loading and pre-processing for ResNetv2.
ResNetv2 input pre-processing:
- Resizing + cropping
- "The inputs pixel values are scaled between -1 and 1, sample-wise."
- Sample-wise normalization: https://stackoverflow.com/questions/37625272/keras-batchnormalization-what-exactly-is-sample-wise-normalization
MobileNet-v1/v2 input pre-processing:
- "The inputs pixel values are scaled between -1 and 1, sample-wise."
- This is the same as ResNet-v2, with the difference that the MobileNet model takes input shape 224x224x3
(same as ResNet-v1).
"""
print("------------ ResNetv2 pre-processing test -------------")
# 1. Data loading and get mean, max, min of data without input preprocessing
total_mean_dict, total_min_dict, total_max_dict = load_tfrecord_mean_min_max(
model_name="resnet_v2"
)
# 2. Check that values are between -1 and 1
print(" Min values: {}".format(np.array(list(total_min_dict.values()))))
print(" Max values: {}".format(np.array(list(total_max_dict.values()))))
print(" Mean values: {}".format(np.array(list(total_mean_dict.values()))))
assert (np.array(list(total_min_dict.values())) >= -1.0).all()
assert (np.array(list(total_max_dict.values())) <= 1.0).all()
@@ -0,0 +1,102 @@
## About
This script presents a QAT end-to-end workflow (TF2-to-ONNX) for [EfficientNet](https://github.com/tensorflow/models/tree/master/official/legacy/image_classification/efficientnet).
### Contents
[Requirements](#requirements) • [Workflow](#workflow) • [Results](#results)
## Requirements
1. Install base requirements and prepare data. Please refer to [examples' README](../README.md).
2. Clone the models from Tensorflow model garden:
```
git clone https://github.com/tensorflow/models.git
pushd models && git checkout tags/v2.8.0 && popd
export PYTHONPATH=$PWD/models:$PYTHONPATH
pip install -r models/official/requirements.txt
```
> cd models && git submodule init && git submodule update
3. Download pretrained checkpoints:
1. B0: https://tfhub.dev/tensorflow/efficientnet/b0/classification/1
2. B3: https://tfhub.dev/tensorflow/efficientnet/b3/classification/1
## Workflow
### Step 1: Model Quantization and Fine-tuning
* In `run_qat_workflow.py`, please set the `pretrained_ckpt_path` field to the directory of the downloaded checkpoint to start fine-tuning with QAT. All the required hyper-parameters can be set in the `HYPERPARAMS` dictionary.
Please run the following to quantize, fine-tune, and save the final graph in SavedModel format.
```sh
python run_qat_workflow.py
```
> Update `MODEL_VERSION` to the EfficientNet version you wish to quantize.
### Step 2: Exporting a QAT SavedModel
Once you've fine-tuned the QAT model, export it by running
```sh
python export.py --ckpt <path_to_pretrained_ckpt> --output <saved_model_output_name> --model_version b0
```
This script applies quantization to the model, restores the checkpoint, and exports it in a SavedModel format. This script will generate `eff` which is a directory containing saved model. We set the overall graph data format to `NCHW` by using `tf.keras.backend.set_image_data_format('channels_first')`. TensorRT expects `NCHW` format for graphs trained with QAT for better optimizations.
Arguments:
* `--ckpt` : Path to fine-tuned QAT checkpoint to be loaded.
* `--output` : Name of output TF saved model.
* `--model_version` : EfficientNet model version, currently supports {`b0`, `b3`}.
### Step 3: Conversion to ONNX
Convert the saved model into ONNX by running
```sh
python -m tf2onnx.convert --saved-model <path_to_saved_model> --output model_qat.onnx --opset 13
```
By default, tf2onnx uses TF's graph optimizers to performs constant folding after a saved model is loaded.
Arguments:
* `--saved-model` : Name of TF SavedModel
* `--output` : Name of ONNX output graph
* `--opset` : ONNX opset version (opset 13 or higher must be used)
### Step 4: TensorRT Deployment
Please refer to the [examples' README](../README.md).
## Results
This section presents the validation accuracy for the full ImageNet dataset on NVIDIA's A100 GPU and TensorRT 8.4 GA.
### EfficientNet-B0
| Model | Accuracy (%) | Latency (ms, bs=1) |
|-----------------------|--------------|--------------------|
| Baseline (TensorFlow) | 76.97 | 6.77 |
| PTQ (TensorRT) | 71.71 | 0.67 |
| **QAT** (TensorRT) | 75.82 | 0.68 |
> QAT fine-tuning hyper-parameters: `bs64, ep10, lr=0.001, steps_per_epoch=None`
### EfficientNet-B3
| Model | Accuracy (%) | Latency (ms, bs=1) |
|-----------------------|--------------|--------------------|
| Baseline (TensorFlow) | 81.36 | 10.33 |
| PTQ (TensorRT) | 78.88 | 1.24 |
| **QAT** (TensorRT) | 79.48 | 1.23 |
> QAT fine-tuning hyper-parameters: `bs=32, ep20, steps_per_epoch=None, lr=0.0001`
### Notes
- QAT fine-tuning hyper-parameters:
- Optimizer: `piecewise_sgd`, `lr_schedule=[(1.0, 1), (0.1, 3), (0.01, 6), (0.001, 9), (0.001, 15)]`.
- Other hyper-parameters are under each model's results table.
- PTQ calibration: `bs=64`
- EfficientNet model quantization:
- QDQ nodes added in Residual connection (fix added to ResidualQDQCustomCase for `Conv-BN-Activation-Dropout` pattern),
- Global Average Pooling,
- Multiply layer in SE block.
@@ -0,0 +1,59 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import tensorflow as tf
import argparse
from utils import create_efficientnet_model
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.custom_qdq_cases import EfficientNetQDQCase
def export_saved_model(model_version="b0"):
model = create_efficientnet_model(model_version=model_version)
q_model = quantize_model(model, custom_qdq_cases=[EfficientNetQDQCase()])
if args.ckpt:
q_model.load_weights(args.ckpt).expect_partial()
tf.keras.models.save_model(q_model, args.output)
print("Exported the model to {}".format(args.output))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Export saved model for efficientnet_b0"
)
parser.add_argument(
"--ckpt",
type=str,
default="qat/checkpoints_best",
help="Path to pretrained QAT efficientnet checkpoint.",
)
parser.add_argument(
"--output",
type=str,
default="qat/saved_model",
help="Path to pretrained QAT saved model.",
)
parser.add_argument(
"--model_version",
type=str,
default="b0",
help="EfficientNet model version, currently supports {'b0', 'b3'}.",
)
args = parser.parse_args()
export_saved_model(args.model_version)
@@ -0,0 +1,116 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import tensorflow as tf
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.custom_qdq_cases import EfficientNetQDQCase
from examples.data.data_loader import load_data
from examples.utils_finetuning import fine_tune
import numpy as np
import random
from utils import create_efficientnet_model
MODEL_VERSION = "b0" # Options={b0, b3}
HYPERPARAMS = {
# ################ Data loading ################
"tfrecord_data_dir": "/media/Data/imagenet_data/tf_records",
"batch_size": 64,
"train_data_size": None, # If 'None', consider all data, otherwise, consider subset.
"val_data_size": None, # If 'None', consider all data, otherwise, consider subset.
# ############## Fine-tuning ##################
"pretrained_ckpt_path": "./weights/efficientnet_{}/baseline".format(MODEL_VERSION),
"epochs": 10,
"steps_per_epoch": None, # 'None' if you want to use the default number of steps. If you use this, make sure the number of steps is <= the number of shards (total number of samples / batch_size). Otherwise, an error will occur.
"base_lr": 0.001,
"optimizer": "piecewise_sgd", # Options={sgd, piecewise_sgd, adam}
"save_ckpt_dir": "./weights/efficientnet_{}/qat".format(MODEL_VERSION),
# ############## Enable/disable tasks ##################
"evaluate_baseline_model": True,
"evaluate_qat_model": True,
"seed": 42,
}
# Set seed for reproducible results
os.environ["PYTHONHASHSEED"] = str(HYPERPARAMS["seed"])
random.seed(HYPERPARAMS["seed"])
np.random.seed(HYPERPARAMS["seed"])
tf.random.set_seed(HYPERPARAMS["seed"])
def evaluate_acc(model, validation_data):
# Compile model (needed to evaluate model)
model.compile(
optimizer="sgd",
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=["sparse_categorical_accuracy"],
)
_, val_accuracy = model.evaluate(validation_data)
return val_accuracy
def main():
# ------------- Initial settings -------------
# Load data
train_batches, val_batches = load_data(HYPERPARAMS, model_name="efficientnet_"+MODEL_VERSION)
# ------------- Baseline model -------------
model = create_efficientnet_model(model_version=MODEL_VERSION)
# Load pre-trained weights
if HYPERPARAMS["pretrained_ckpt_path"]:
model.load_weights(HYPERPARAMS["pretrained_ckpt_path"]).expect_partial()
if HYPERPARAMS["evaluate_baseline_model"]:
baseline_model_accuracy = evaluate_acc(model, val_batches)
print("Baseline model accuracy:", baseline_model_accuracy)
# ------------- QAT model -------------
# Quantize model
q_model = quantize_model(model, custom_qdq_cases=[EfficientNetQDQCase()])
# Fine-tuning + saving new checkpoints
print("Fine-tuning and saving QAT model checkpoint...")
lr_schedule_array = [(1.0, 1), (0.1, 3), (0.01, 6), (0.001, 9), (0.001, 15)]
fine_tune(
q_model, train_batches, val_batches,
qat_save_finetuned_weights=HYPERPARAMS["save_ckpt_dir"],
hyperparams=HYPERPARAMS,
lr_schedule_array=lr_schedule_array,
enable_tensorboard_callback=False
)
print("Fine-tuning done!")
# Loads best weights if they exist
best_checkpoint_path = os.path.join(HYPERPARAMS["save_ckpt_dir"], "checkpoints_best")
if os.path.exists(best_checkpoint_path + ".index"):
q_model.load_weights(best_checkpoint_path).expect_partial()
if HYPERPARAMS["evaluate_qat_model"]:
print("\nEvaluating QAT model...")
qat_model_accuracy = evaluate_acc(q_model, val_batches)
print("QAT val accuracy:", qat_model_accuracy)
if __name__ == "__main__":
main()
@@ -0,0 +1,74 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import tensorflow as tf
from tensorflow_quantization.custom_qdq_cases import EfficientNetQDQCase
from utils import create_efficientnet_model
from tests.onnx_graph_qdq_validator import validate_quantized_model
from tensorflow_quantization.utils import CreateAssetsFolders
import pytest
# Create a directory to save test models
test_assets = CreateAssetsFolders("test_qdq_node_placement")
def test_efficientnet_b0_quantize_full():
"""
EfficientNet-B0: Full model quantization.
Contains special patterns connected to Add layer:
1. (Conv->BatchNorm->Activation)->Add
2. (Conv->BatchNorm->Activation->Dropout)->Add
Previously, only (Conv)->Add and (Conv->BatchNorm)->Add checks were done when checking for quantizable Residual
connections (branches to `Add` layer). The new EfficientNet patterns have now also been added to the
ResidualCustomQDQCases check.
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = create_efficientnet_model()
custom_qdq_cases = [EfficientNetQDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
def test_efficientnet_b3_quantize_full():
"""
EfficientNet-B3: Full model quantization.
Contains the same special patterns as EfficientNet-B0.
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = create_efficientnet_model(model_version="b3")
custom_qdq_cases = [EfficientNetQDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
@@ -0,0 +1,44 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import sys
import tensorflow as tf
tf_models_path = os.path.realpath("./models")
sys.path.insert(1, tf_models_path)
try:
from official.legacy.image_classification.efficientnet import efficientnet_model
except Exception:
print("Error importing TF official models codebase.")
def create_efficientnet_model(model_version="b0"):
model_name = "efficientnet-" + model_version
model_configs = dict(efficientnet_model.MODEL_CONFIGS)
assert model_name in model_configs, "Model name is not valid!"
config = model_configs[model_name]
# Set the dataformat of the model to NCHW for training and inference
tf.keras.backend.set_image_data_format("channels_first")
# B0=(224, 224, 3); B3=(300, 300, 3)
image_input = tf.keras.layers.Input(
shape=(config.resolution, config.resolution, config.input_channels), name="image_input", dtype=tf.float32
)
outputs = efficientnet_model.efficientnet(image_input, config)
model = tf.keras.Model(inputs=image_input, outputs=outputs)
return model
@@ -0,0 +1,43 @@
## About
This script presents a QAT end-to-end workflow (TF2-to-ONNX) for [Inception models](https://keras.io/api/applications/inceptionv3/) in `tf.keras.applications`.
### Contents
[Requirements](#requirements) • [Workflow](#workflow) • [Results](#results)
## Requirements
Install base requirements and prepare data. Please refer to [examples' README](../README.md).
## Workflow
### Step 1: Model Quantization and Fine-tuning
> Similar to [ResNet](../resnet): different model and different input pre-processing.
Please run the following to quantize, fine-tune, and save the final graph in SavedModel format (checkpoints are also saved).
```sh
python run_qat_workflow.py
```
### Step 2: Conversion to ONNX
Step 1 already does the conversion from SavedModel to ONNX automatically. For manual steps, please see step 3 in [EfficientNet's README](../efficientnet_b0/README.md).
### Step 3: TensorRT Deployment
Please refer to the [examples' README](../README.md).
## Results
Results obtained on NVIDIA's A100 GPU and TensorRT 8.4.2.4 (GA Update 1).
### Inception-v3
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|--------|-----------------------|--------|------------------------|
| Baseline | 77.86 | 9.01 | 77.86 | 1.39 |
| PTQ | - | - | 77.73 | 0.82 |
| **QAT** | 78.11 | 101.97 | 78.08 | 0.82 |
### Notes
- Optimization: MaxPool needs to be quantized to trigger horizontal fusion in Concat layer.
- QAT fine-tuning hyper-params:
- Optimizer: `piecewise_sgd`, `lr_schedule=[(1.0, 1), (0.1, 2), (0.01, 7)]` (default)
- Hyper-parameters: `bs=64, ep=10, lr=0.001, steps_per_epoch=500`
- PTQ calibration: `bs=64`.
@@ -0,0 +1,178 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import tensorflow as tf
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.utils import convert_saved_model_to_onnx
from tensorflow_quantization.custom_qdq_cases import InceptionQDQCase
from examples.utils import ensure_dir, get_tfkeras_model
from examples.data.data_loader import load_data
from examples.utils_finetuning import (
get_finetuned_weights_dirname,
fine_tune,
compile_model,
)
import gc
import numpy as np
import random
import sys
import logging
MODEL_NAME = "inception_v3" # Options=[inception_v3]
HYPERPARAMS = {
# ################ Data loading ################
"tfrecord_data_dir": "/media/Data/imagenet_data/tf_records",
"batch_size": 64,
"train_data_size": None, # Only for `tfrecord`. If None, consider all data, otherwise, consider subset.
"val_data_size": None, # Only for `tfrecord`. If None, consider all data, otherwise, consider subset.
# ############## Fine-tuning ##################
"epochs": 10,
"steps_per_epoch": 500, # 500, # 'None' if you want to use the default number of steps. If you use this, make sure the number of steps is <= the number of shards (total number of samples / batch_size). Otherwise, an error will occur.
"base_lr": 0.001, # 0.0001
"optimizer": "piecewise_sgd", # Options={sgd, piecewise_sgd, adam}
"save_root_dir": "./weights/{}".format(
MODEL_NAME
), # DIR is updated to reflect hyperparams
# ############## Enable/disable tasks ##################
"finetune_qat_model": True, # If True, finetune QAT model. Otherwise, just quantize and load weights if existent.
"rewrite_weights_qat_finetuning": True, # If True, rewrites existing fine-tuned weights. Otherwise, just load weights if they exist.
"evaluate_baseline_model": True,
"evaluate_qat_model": True,
"save_baseline_model": True,
"seed": 42,
}
# Set seed for reproducible results
os.environ["PYTHONHASHSEED"] = str(HYPERPARAMS["seed"])
random.seed(HYPERPARAMS["seed"])
np.random.seed(HYPERPARAMS["seed"])
tf.random.set_seed(HYPERPARAMS["seed"])
# Create logger and save to out.log
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
def main():
# ------------- Initial settings -------------
# Create directory to save the fine-tuned weights + add relevant hyperparameters in the name
qat_save_finetuned_weights = get_finetuned_weights_dirname(HYPERPARAMS)
ensure_dir(qat_save_finetuned_weights)
# Add terminal and file handlers to logger
output_file_handler = logging.FileHandler(
os.path.join(qat_save_finetuned_weights, "out.log"), mode="w"
)
stdout_handler = logging.StreamHandler(sys.stdout)
LOGGER.addHandler(output_file_handler)
LOGGER.addHandler(stdout_handler)
# Load data
train_batches, val_batches = load_data(HYPERPARAMS, model_name=MODEL_NAME)
# ------------- Baseline model -------------
LOGGER.info("------------- Baseline model -------------")
# Instantiate Baseline model
model = get_tfkeras_model(model_name=MODEL_NAME)
if HYPERPARAMS["evaluate_baseline_model"]:
# Compile model (needed to evaluate model)
compile_model(model)
_, baseline_model_accuracy = model.evaluate(val_batches)
LOGGER.info("Baseline val accuracy: {}".format(baseline_model_accuracy))
if HYPERPARAMS["save_baseline_model"]:
tf.keras.models.save_model(
model, os.path.join(HYPERPARAMS["save_root_dir"], "saved_model_baseline")
)
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(
HYPERPARAMS["save_root_dir"], "saved_model_baseline"
),
onnx_model_path=os.path.join(
HYPERPARAMS["save_root_dir"], "model_baseline.onnx"
),
)
# ------------- QAT model -------------
# Quantize model
LOGGER.info("\n------------- QAT model -------------")
q_model = quantize_model(model, custom_qdq_cases=[InceptionQDQCase()])
# q_model = quantize_model(model)
finetuned_qat_weights_path = os.path.join(
qat_save_finetuned_weights, "checkpoints_best"
)
# Performs fine-tuning if `rewrite` is enabled or if fine-tuned weights don't exist yet
# (1st time fine-tuning model).
if HYPERPARAMS["finetune_qat_model"] and (
HYPERPARAMS["rewrite_weights_qat_finetuning"]
or not os.path.exists(finetuned_qat_weights_path)
):
# Fine-tuning + saving new checkpoints
LOGGER.info("\nFine-tuning model...")
fine_tune(
q_model,
train_batches,
val_batches,
qat_save_finetuned_weights,
HYPERPARAMS,
LOGGER,
)
LOGGER.info("Fine-tuning done!")
# Loads best weights if they exist
if os.path.exists(finetuned_qat_weights_path + ".index"):
LOGGER.info("Loading fine-tuned weights...")
q_model.load_weights(finetuned_qat_weights_path).expect_partial()
LOGGER.info("Loaded complete!")
compile_model(q_model)
if HYPERPARAMS["evaluate_qat_model"]:
LOGGER.info("\nEvaluating QAT model...")
_, qat_model_accuracy = q_model.evaluate(val_batches)
LOGGER.info("QAT val accuracy: {}".format(qat_model_accuracy))
# Save quantized model
LOGGER.info("\nSaving QAT model")
tf.keras.models.save_model(
q_model, os.path.join(qat_save_finetuned_weights, "saved_model")
)
# Clear GPU and invoke Garbage Collector to avoid script ending during ONNX conversion
tf.keras.backend.clear_session()
gc.collect()
del model
del q_model
# Convert SavedModel to ONNX
LOGGER.info("\nONNX conversion...")
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(qat_save_finetuned_weights, "saved_model"),
onnx_model_path=os.path.join(qat_save_finetuned_weights, "model.onnx"),
)
if __name__ == "__main__":
main()
@@ -0,0 +1,61 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import tensorflow as tf
from tensorflow_quantization.custom_qdq_cases import InceptionQDQCase
from examples.utils import get_tfkeras_model
from tests.onnx_graph_qdq_validator import validate_quantized_model
from tensorflow_quantization.utils import CreateAssetsFolders
from tensorflow_quantization.quantize import LayerConfig
import pytest
# Create a directory to save test models
test_assets = CreateAssetsFolders("test_qdq_node_placement")
EXPECTED_QDQ_INSERTION = [
LayerConfig(name="Conv2D", is_keras_class=True),
LayerConfig(name="Dense", is_keras_class=True),
LayerConfig(name="DepthwiseConv2D", is_keras_class=True),
LayerConfig(
name="AveragePooling2D", is_keras_class=True, quantize_weight=False
),
LayerConfig(
name="GlobalAveragePooling2D", is_keras_class=True, quantize_weight=False
)
]
def test_inceptionv3_quantize_full():
"""
Inception-v3: Full model quantization
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_tfkeras_model(model_name="inception_v3")
custom_qdq_cases = [InceptionQDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases,
expected_qdq_insertion=EXPECTED_QDQ_INSERTION
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
@@ -0,0 +1,276 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import argparse
import numpy as np
import tensorrt as trt
import pycuda.driver as cuda
# If you face the following issue:
# "pycuda._driver.LogicError: explicit_context_dependent failed: invalid device context - no currently active context?"
# Add "import pycuda.autoinit", this is needed to initialize cuda!
import pycuda.autoinit
import tensorflow as tf
from examples.data.data_loader import load_data_tfrecord_tf, load_image_np, _SUPPORTED_MODEL_NAMES
TRT_DYNAMIC_DIM = -1
class HostDeviceMem(object):
"""Simple helper data class to store Host and Device memory."""
def __init__(self, host_mem, device_mem):
self.host = host_mem
self.device = device_mem
def __str__(self):
return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device)
def __repr__(self):
return self.__str__()
def allocate_buffers(engine: trt.ICudaEngine, batch_size: int) -> [list, list, list]:
"""
Function to allocate buffers and bindings for TensorRT inference.
Args:
engine (trt.ICudaEngine):
batch_size (int): batch size to be used during inference.
Returns:
inputs (List): list of input buffers.
outputs (List): list of output buffers.
dbindings (List): list of device bindings.
"""
inputs = []
outputs = []
dbindings = []
for binding in engine:
binding_shape = engine.get_binding_shape(binding)
if binding_shape[0] == TRT_DYNAMIC_DIM: # dynamic shape
size = batch_size * abs(trt.volume(binding_shape))
else:
size = abs(trt.volume(binding_shape))
dtype = trt.nptype(engine.get_binding_dtype(binding))
# Allocate host and device buffers
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
# Append the device buffer to device bindings
dbindings.append(int(device_mem))
# Append to the appropriate list (input/output)
if engine.binding_is_input(binding):
inputs.append(HostDeviceMem(host_mem, device_mem))
else:
outputs.append(HostDeviceMem(host_mem, device_mem))
return inputs, outputs, dbindings
def infer(
engine_path: str,
val_batches,
batch_size: int = 8,
top_k_value: int = 1,
) -> None:
"""
Performs inference in TensorRT engine.
Args:
engine_path (str): path to the TensorRT engine.
val_batches (tf.data.Dataset): validation dataset (batches).
batch_size (int): batch size used for inference and dataset batch splitting.
top_k_value (int): value of `K` for the top K predictions used in the accuracy calculation.
Raises:
RuntimeError: raised when loading images in the host fails.
"""
def override_shape(shape: tuple) -> tuple:
"""Overrides batch dimension if dynamic."""
if TRT_DYNAMIC_DIM in shape:
shape = tuple(
[batch_size if dim == TRT_DYNAMIC_DIM else dim for dim in shape]
)
return shape
# Open engine as runtime
with open(engine_path, "rb") as f, trt.Runtime(
trt.Logger(trt.Logger.ERROR)
) as runtime:
engine = runtime.deserialize_cuda_engine(f.read())
# Allocate buffers and create a CUDA stream.
inputs, outputs, dbindings = allocate_buffers(engine, batch_size)
# Initiate test_accuracy
test_accuracy = tf.keras.metrics.SparseTopKCategoricalAccuracy(
k=top_k_value, name="top_k_accuracy", dtype=tf.float32
)
test_accuracy.reset_states()
# Contexts are used to perform inference.
with engine.create_execution_context() as context:
# Resolves dynamic shapes in the context
for binding in engine:
binding_idx = engine.get_binding_index(binding)
binding_shape = engine.get_binding_shape(binding_idx)
if engine.binding_is_input(binding_idx):
binding_shape = override_shape(binding_shape)
context.set_binding_shape(binding_idx, binding_shape)
if isinstance(val_batches, tf.Tensor):
# Load images in Host (flatten and copy to page-locked buffer in Host)
data = val_batches.numpy().astype(np.float32).ravel()
pagelocked_buffer = inputs[0].host
np.copyto(pagelocked_buffer, data)
inp = inputs[0]
# Transfer input data from Host to Device (GPU)
cuda.memcpy_htod(inp.device, inp.host)
# Run inference
context.execute_v2(dbindings)
# Transfer predictions back to Host from GPU
out = outputs[0]
cuda.memcpy_dtoh(out.host, out.device)
softmax_output = np.array(out.host)
top1_idx = np.argmax(softmax_output)
output_confidence = softmax_output[top1_idx]
print("Top-1 Index of the image : {} Confidence: {}".format(top1_idx, output_confidence))
elif isinstance(val_batches, tf.data.Dataset):
# Loop over number of steps to evaluate entire validation dataset
for step, example in enumerate(val_batches):
images, labels = example
if step % 100 == 0 and step != 0:
print(
"Evaluating batch {}: {:.4f}".format(
step, test_accuracy.result()
)
)
try:
# Load images in Host (flatten and copy to page-locked buffer in Host)
data = images.numpy().astype(np.float32).ravel()
pagelocked_buffer = inputs[0].host
np.copyto(pagelocked_buffer, data)
except RuntimeError:
raise RuntimeError(
"Failed to load images in Host at step {}".format(step)
)
inp = inputs[0]
# Transfer input data from Host to Device (GPU)
cuda.memcpy_htod(inp.device, inp.host)
# Run inference
context.execute_v2(dbindings)
# Transfer predictions back to Host from GPU
out = outputs[0]
cuda.memcpy_dtoh(out.host, out.device)
# Split 1-D output of length N*labels into 2-D array of (N, labels)
batch_outs = np.array(np.split(np.array(out.host), batch_size))
# Update test accuracy
test_accuracy.update_state(labels, batch_outs)
# Print final accuracy and save to log file
print("\n======================================\n")
result_str = "Top-{} accuracy: {:.4f}\n".format(
top_k_value, test_accuracy.result()
)
print(result_str)
# Save logs to file
results_dir = "/".join(args.engine.split("/")[:-1])
with open(os.path.join(results_dir, args.log_file), "w") as log_file:
log_file.write(result_str)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Run inference on TensorRT engines for Imagenet-based Classification models."
)
parser.add_argument(
"-e", "--engine", type=str, required=True, help="Path to TensorRT engine"
)
parser.add_argument(
"--image", type=str, help="Path to an image to perform single image inference"
)
parser.add_argument(
"-m",
"--model_name",
type=str,
default="resnet_v1",
help="Name of the model, needed to choose the appropriate input pre-processing."
"Options include {}".format(_SUPPORTED_MODEL_NAMES),
)
parser.add_argument(
"-d",
"--data_dir",
default="/media/Data/ImageNet/train-val-tfrecord",
type=str,
help="Path to directory of input images in tfrecord format (val data).",
)
parser.add_argument(
"-k",
"--top_k_value",
default=1,
type=int,
help="Value of `K` for the top-K predictions used in the accuracy calculation.",
)
parser.add_argument(
"-b",
"--batch_size",
default=1,
type=int,
help="Number of inputs to send in parallel (up to max batch size of engine).",
)
parser.add_argument(
"--log_file",
type=str,
default="engine_accuracy.log",
help="Filename to save logs.",
)
args = parser.parse_args()
if args.model_name not in _SUPPORTED_MODEL_NAMES:
raise ValueError(
"Invalid model name ",
args.model_name,
" provided. Please select among {}".format(_SUPPORTED_MODEL_NAMES),
)
# Load the test data and pre-process input
val_batches = None
if args.image:
val_batches = load_image_np(args.image, args.model_name)
else:
data_batches = load_data_tfrecord_tf(
data_dir=args.data_dir, batch_size=args.batch_size, model_name=args.model_name
)
val_batches = data_batches["validation"]
# Perform inference
infer(
args.engine,
val_batches,
batch_size=args.batch_size,
top_k_value=args.top_k_value,
)
@@ -0,0 +1,55 @@
## About
This script presents a QAT end-to-end workflow (TF2-to-ONNX) for [MobileNet models](https://keras.io/api/applications/mobilenet/) in `tf.keras.applications`.
### Contents
[Requirements](#requirements) • [Workflow](#workflow) • [Results](#results)
## Requirements
Install base requirements and prepare data. Please refer to [examples' README](../README.md).
## Workflow
### Step 1: Model Quantization and Fine-tuning
> Similar to [ResNet](../resnet): different model and different input pre-processing (`mobilenet`).
Please run the following to quantize, fine-tune, and save the final graph in SavedModel format (checkpoints are also saved).
```sh
python run_qat_workflow.py
```
### Step 2: Conversion to ONNX
Step 1 already does the conversion from SavedModel to ONNX automatically. For manual steps, please see step 3 in [EfficientNet's README](../efficientnet_b0/README.md).
### Step 3: TensorRT Deployment
Please refer to the [examples' README](../README.md).
## Results
Results obtained on NVIDIA's A100 GPU and TensorRT 8.4.10.1.
### MobileNet-v1
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|-------------|-----------------------|--------|------------------------|
| Baseline | 70.60 | 1.99 | 70.60 | 0.32 |
| PTQ | - | - | 69.31 | 0.16 |
| **QAT** | 70.51 (ep2) | 50.49 | 70.43 | 0.16 |
**Note**: no residual connections exist in MobileNet-v1.
### MobileNet-v2
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|-------------|-----------------------|----------|------------------------|
| Baseline | 71.77 | 3.71 | 71.77 | 0.55 |
| PTQ | - | - | 70.87 | 0.30 |
| **QAT** | 71.68 (ep1) | 74.27 | 71.62 | 0.30 |
**Note**: residual connections exist in MobileNet-v2.
### Notes
- QAT fine-tuning hyper-parameters:
- Optimizer: `piecewise_sgd`, `lr_schedule=[(1.0, 1), (0.1, 2), (0.01, 7)]` (default)
- Hyper-parameters: `bs=64, ep=10, lr=0.001`
- PTQ calibration: `bs=64`.
- MobileNet-v3 might not show good acceleration in TensorRT due to its architecture (`Conv->BN->((Add->Clip->Mul), ())->Mul`), which is not a kernel fusion in TRT.
@@ -0,0 +1,179 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import tensorflow as tf
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.utils import convert_saved_model_to_onnx
from tensorflow_quantization.custom_qdq_cases import MobileNetQDQCase
from examples.utils import ensure_dir
from examples.data.data_loader import load_data
from examples.utils import get_tfkeras_model
from examples.utils_finetuning import (
get_finetuned_weights_dirname,
fine_tune,
compile_model,
)
import gc
import numpy as np
import random
import sys
import logging
MODEL_NAME = "mobilenet_v2" # Options=[mobilenet_v1, mobilenet_v2]
HYPERPARAMS = {
# ################ Data loading ################
"tfrecord_data_dir": "/media/Data/imagenet_data/tf_records",
"batch_size": 64,
"train_data_size": None, # Only for `tfrecord`. If None, consider all data, otherwise, consider subset.
"val_data_size": None, # Only for `tfrecord`. If None, consider all data, otherwise, consider subset.
# ############## Fine-tuning ##################
"epochs": 10,
"steps_per_epoch": 500, # 500, # 'None' if you want to use the default number of steps. If you use this, make sure the number of steps is <= the number of shards (total number of samples / batch_size). Otherwise, an error will occur.
"base_lr": 0.001, # 0.0001
"optimizer": "piecewise_sgd", # Options={sgd, piecewise_sgd, adam}
"save_root_dir": "./weights/{}".format(
MODEL_NAME
), # DIR is updated to reflect hyperparams
# ############## Enable/disable tasks ##################
"finetune_qat_model": True, # If True, finetune QAT model. Otherwise, just quantize and load weights if existent.
"rewrite_weights_qat_finetuning": True, # If True, rewrites existing fine-tuned weights. Otherwise, just load weights if they exist.
"evaluate_baseline_model": True,
"evaluate_qat_model": True,
"save_baseline_model": False,
"seed": 42,
}
# Set seed for reproducible results
os.environ["PYTHONHASHSEED"] = str(HYPERPARAMS["seed"])
random.seed(HYPERPARAMS["seed"])
np.random.seed(HYPERPARAMS["seed"])
tf.random.set_seed(HYPERPARAMS["seed"])
# Create logger and save to out.log
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
def main():
# ------------- Initial settings -------------
# Create directory to save the fine-tuned weights + add relevant hyperparameters in the name
qat_save_finetuned_weights = get_finetuned_weights_dirname(HYPERPARAMS)
ensure_dir(qat_save_finetuned_weights)
# Add terminal and file handlers to logger
output_file_handler = logging.FileHandler(
os.path.join(qat_save_finetuned_weights, "out.log"), mode="w"
)
stdout_handler = logging.StreamHandler(sys.stdout)
LOGGER.addHandler(output_file_handler)
LOGGER.addHandler(stdout_handler)
# Load data
# MobileNet requires the input to be pre-processed like ResNet-v2 but with input 224x224x3 (as in ResNet-v1)
train_batches, val_batches = load_data(HYPERPARAMS, model_name=MODEL_NAME)
# ------------- Baseline model -------------
LOGGER.info("------------- Baseline model -------------")
# Instantiate Baseline model
model = get_tfkeras_model(model_name=MODEL_NAME)
if HYPERPARAMS["evaluate_baseline_model"]:
# Compile model (needed to evaluate model)
compile_model(model)
_, baseline_model_accuracy = model.evaluate(val_batches)
LOGGER.info("Baseline val accuracy: {}".format(baseline_model_accuracy))
if HYPERPARAMS["save_baseline_model"]:
tf.keras.models.save_model(
model, os.path.join(HYPERPARAMS["save_root_dir"], "saved_model_baseline")
)
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(
HYPERPARAMS["save_root_dir"], "saved_model_baseline"
),
onnx_model_path=os.path.join(
HYPERPARAMS["save_root_dir"], "model_baseline.onnx"
),
)
# ------------- QAT model -------------
# Quantize model
LOGGER.info("\n------------- QAT model -------------")
q_model = quantize_model(model, custom_qdq_cases=[MobileNetQDQCase()])
finetuned_qat_weights_path = os.path.join(
qat_save_finetuned_weights, "checkpoints_best"
)
# Performs fine-tuning if `rewrite` is enabled or if fine-tuned weights don't exist yet
# (1st time fine-tuning model).
if HYPERPARAMS["finetune_qat_model"] and (
HYPERPARAMS["rewrite_weights_qat_finetuning"]
or not os.path.exists(finetuned_qat_weights_path)
):
# Fine-tuning + saving new checkpoints
LOGGER.info("\nFine-tuning model...")
fine_tune(
q_model,
train_batches,
val_batches,
qat_save_finetuned_weights,
HYPERPARAMS,
LOGGER,
)
LOGGER.info("Fine-tuning done!")
# Loads best weights if they exist
if os.path.exists(finetuned_qat_weights_path + ".index"):
LOGGER.info("Loading fine-tuned weights...")
q_model.load_weights(finetuned_qat_weights_path).expect_partial()
LOGGER.info("Loaded complete!")
compile_model(q_model)
if HYPERPARAMS["evaluate_qat_model"]:
LOGGER.info("\nEvaluating QAT model...")
_, qat_model_accuracy = q_model.evaluate(val_batches)
LOGGER.info("QAT val accuracy: {}".format(qat_model_accuracy))
# Save quantized model
LOGGER.info("\nSaving QAT model")
tf.keras.models.save_model(
q_model, os.path.join(qat_save_finetuned_weights, "saved_model")
)
# Clear GPU and invoke Garbage Collector to avoid script ending during ONNX conversion
tf.keras.backend.clear_session()
gc.collect()
del model
del q_model
# Convert SavedModel to ONNX
LOGGER.info("\nONNX conversion...")
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(qat_save_finetuned_weights, "saved_model"),
onnx_model_path=os.path.join(qat_save_finetuned_weights, "model.onnx"),
)
if __name__ == "__main__":
main()
@@ -0,0 +1,66 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import tensorflow as tf
from tensorflow_quantization.custom_qdq_cases import MobileNetQDQCase
from examples.utils import get_tfkeras_model
from tests.onnx_graph_qdq_validator import validate_quantized_model
from tensorflow_quantization.utils import CreateAssetsFolders
import pytest
# Create a directory to save test models
test_assets = CreateAssetsFolders("test_qdq_node_placement")
def test_mobilenetv1_quantize_full():
"""
MobileNet-v1: Full model quantization
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_tfkeras_model(model_name="mobilenet_v1")
custom_qdq_cases = [MobileNetQDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
def test_mobilenetv2_quantize_full():
"""
MobileNet-v2: Full model quantization
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_tfkeras_model(model_name="mobilenet_v2")
custom_qdq_cases = [MobileNetQDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
@@ -0,0 +1,9 @@
# ImageNet: conversion to tfrecord
gcloud
google-cloud-storage
# Data loader
Pillow
# TensorRT: build/infer engine
pycuda
@@ -0,0 +1,67 @@
## About
This script presents a QAT end-to-end workflow (TF2-to-ONNX) for [ResNet models](https://keras.io/api/applications/resnet/) in `tf.keras.applications`.
### Contents
[Requirements](#requirements) • [Workflow](#workflow) • [Results](#results)
## Requirements
Install base requirements and prepare data. Please refer to [examples' README](../README.md).
## Workflow
### Step 1: Model Quantization and Fine-tuning
Please run the following to quantize, fine-tune, and save the final graph in SavedModel format (checkpoints are also saved).
```sh
python run_qat_workflow.py
```
### Step 2: Conversion to ONNX
Step 1 already does the conversion from SavedModel to ONNX automatically. For manual steps, please see step 3 in [EfficientNet's README](../efficientnet_b0/README.md).
### Step 3: TensorRT Deployment
Please refer to the [examples' README](../README.md).
## Results
Results obtained on NVIDIA's A100 GPU and TensorRT 8.4 EA.
### ResNet50-v1
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|-------------|-----------------------|--------|------------------------|
| Baseline | 75.05 | 7.95 | 75.05 | 1.96 |
| PTQ | - | - | 74.96 | 0.46 |
| **QAT** | 75.11 (ep5) | - | 75.12 | 0.45 |
### ResNet50-v2
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|--------------|-----------------------|---------|------------------------|
| Baseline | 75.36 | 6.16 | 75.37 | 2.35 |
| PTQ | - | - | 75.48 | 0.57 |
| **QAT** | 75.59 (ep5) | - | 75.65 | 0.57 |
### ResNet101-v1
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|--------------|-----------------------|--------|------------------------|
| Baseline | 76.47 | 15.92 | 76.48 | 3.84 |
| PTQ | - | - | 76.32 | 0.84 |
| **QAT** | 76.33 (ep30) | - | 76.26 | 0.84 |
### ResNet101-v2
| Model | TF (%) | TF latency (ms, bs=1) | TRT(%) | TRT latency (ms, bs=1) |
|----------|--------|-----------------------|--------|------------------------|
| Baseline | 76.89 | 14.13 | 76.88 | 4.55 |
| PTQ | - | - | 76.94 | 1.05 |
| **QAT** | 77.20 | - | 77.15 | 1.05 |
> QAT fine-tuning hyper-parameters for ResNet101-v2: `bs=32` (`bs=64` was OOM).
### Notes
- QAT fine-tuning hyper-parameters:
- Optimizer: `piecewise_sgd`, `lr_schedule=[(1.0,1),(0.1,2),(0.01,7)]` (default)
- Hyper-parameters: `bs=64, ep=10, lr=0.001`.
- Added QDQ nodes in Residual connection.
- PTQ calibration: `bs=64`.
@@ -0,0 +1,188 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import tensorflow as tf
from tensorflow_quantization.quantize import quantize_model
from tensorflow_quantization.utils import convert_saved_model_to_onnx
from tensorflow_quantization.custom_qdq_cases import ResNetV1QDQCase, ResNetV2QDQCase
from examples.utils import ensure_dir
from examples.data.data_loader import load_data
from examples.utils_finetuning import (
get_finetuned_weights_dirname,
fine_tune,
compile_model,
)
from utils import get_resnet_model
import gc
import numpy as np
import random
import sys
import logging
RESNET_DEPTH = "50" # Options=[50, 101]
RESNET_VERSION = "v1" # Options=[v1, v2]
# For reference: the baseline model used bs=256, train_epochs=90, train_steps=10.
HYPERPARAMS = {
# ################ Data loading ################
"tfrecord_data_dir": "/media/Data/imagenet_data/tf_records",
"batch_size": 64,
"train_data_size": None, # If None, consider all data, otherwise, consider subset.
"val_data_size": None, # If None, consider all data, otherwise, consider subset.
# ############## Fine-tuning ##################
"epochs": 2,
"steps_per_epoch": 500, # Set as 'None' if you want to use the default number of steps. If you use this, make sure the number of steps is <= the number of shards (total number of samples / batch_size). Otherwise, an error will occur.
"base_lr": 0.001,
"optimizer": "piecewise_sgd", # Options={sgd, piecewise_sgd, adam}
"save_root_dir": "./weights/resnet{}{}_test".format(
RESNET_DEPTH, RESNET_VERSION
), # DIR is updated to reflect hyperparams
# ############## Enable/disable tasks ##################
"finetune_qat_model": True, # If True, finetune QAT model. Otherwise, just quantize and load weights if existent.
"rewrite_weights_qat_finetuning": True, # If True, rewrites existing fine-tuned weights. Otherwise, just load weights if they exist.
"evaluate_baseline_model": True,
"evaluate_qat_model": True,
"save_baseline_model": True,
"seed": 42,
}
# Set seed for reproducible results
os.environ["PYTHONHASHSEED"] = str(HYPERPARAMS["seed"])
random.seed(HYPERPARAMS["seed"])
np.random.seed(HYPERPARAMS["seed"])
tf.random.set_seed(HYPERPARAMS["seed"])
# Create logger and save to out.log
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
def main():
# ------------- Initial settings -------------
# Create directory to save the fine-tuned weights + add relevant hyperparameters in the name
qat_save_finetuned_weights = get_finetuned_weights_dirname(HYPERPARAMS)
ensure_dir(qat_save_finetuned_weights)
# Add terminal and file handlers to logger
output_file_handler = logging.FileHandler(
os.path.join(qat_save_finetuned_weights, "out.log"), mode="w"
)
stdout_handler = logging.StreamHandler(sys.stdout)
LOGGER.addHandler(output_file_handler)
LOGGER.addHandler(stdout_handler)
# Load data
train_batches, val_batches = load_data(
HYPERPARAMS, model_name="resnet_{}".format(RESNET_VERSION)
)
# ------------- Baseline model -------------
LOGGER.info("------------- Baseline model -------------")
# Instantiate Baseline model
model = get_resnet_model(
resnet_depth=RESNET_DEPTH,
resnet_version=RESNET_VERSION,
)
if HYPERPARAMS["evaluate_baseline_model"]:
# Compile model (needed to evaluate model)
compile_model(model)
_, baseline_model_accuracy = model.evaluate(val_batches)
LOGGER.info("Baseline val accuracy: {}".format(baseline_model_accuracy))
if HYPERPARAMS["save_baseline_model"]:
tf.keras.models.save_model(
model, os.path.join(HYPERPARAMS["save_root_dir"], "saved_model_baseline")
)
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(
HYPERPARAMS["save_root_dir"], "saved_model_baseline"
),
onnx_model_path=os.path.join(
HYPERPARAMS["save_root_dir"], "model_baseline.onnx"
),
)
# ------------- QAT model -------------
# Quantize model
LOGGER.info("\n------------- QAT model -------------")
if "v1" == RESNET_VERSION:
q_model = quantize_model(model, custom_qdq_cases=[ResNetV1QDQCase()])
else:
q_model = quantize_model(model, custom_qdq_cases=[ResNetV2QDQCase()])
finetuned_qat_weights_path = os.path.join(
qat_save_finetuned_weights, "checkpoints_best"
)
# Performs fine-tuning if `rewrite` is enabled or if fine-tuned weights don't exist yet
# (1st time fine-tuning model).
if HYPERPARAMS["finetune_qat_model"] and (
HYPERPARAMS["rewrite_weights_qat_finetuning"]
or not os.path.exists(finetuned_qat_weights_path)
):
# Fine-tuning + saving new checkpoints
LOGGER.info("\nFine-tuning model...")
fine_tune(
q_model,
train_batches,
val_batches,
qat_save_finetuned_weights,
HYPERPARAMS,
LOGGER,
)
LOGGER.info("Fine-tuning done!")
# Loads best weights if they exist
if os.path.exists(finetuned_qat_weights_path + ".index"):
LOGGER.info("Loading fine-tuned weights...")
q_model.load_weights(finetuned_qat_weights_path).expect_partial()
LOGGER.info("Loaded complete!")
compile_model(q_model)
if HYPERPARAMS["evaluate_qat_model"]:
LOGGER.info("\nEvaluating QAT model...")
_, qat_model_accuracy = q_model.evaluate(val_batches)
LOGGER.info("QAT val accuracy: {}".format(qat_model_accuracy))
# Save quantized model
LOGGER.info("\nSaving QAT model")
tf.keras.models.save_model(
q_model, os.path.join(qat_save_finetuned_weights, "saved_model")
)
# Clear GPU and invoke Garbage Collector to avoid script ending during ONNX conversion
tf.keras.backend.clear_session()
gc.collect()
del model
del q_model
# Convert SavedModel to ONNX
LOGGER.info("\nONNX conversion...")
convert_saved_model_to_onnx(
saved_model_dir=os.path.join(qat_save_finetuned_weights, "saved_model"),
onnx_model_path=os.path.join(qat_save_finetuned_weights, "model.onnx"),
)
if __name__ == "__main__":
main()
@@ -0,0 +1,22 @@
## Scripts for TRT Deployment
For both baseline and QAT, change:
- `RESNET_DEPTH` for 50 or 101,
- `RESNET_VERSION` for v1 or v2,
- `BS` for which batch sizes you wish to evaluate the engine on.
#### Baseline
```
./scripts/deploy_engine_baseline.sh
```
> Change `ROOT_DIR` to where your ONNX file is.
#### QAT
```
./scripts/deploy_engine_qat.sh
```
> Change `QAT_SUBDIR` and `ROOT_DIR` to where your ONNX file is.
### Only accuracy
```
./scripts/infer_engine.sh
```
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Single run:
# ../../engine_builder/build_engine_single.py --root_dir=/home/nvidia/PycharmProjects/tensorflow-quantization/examples/resnet/weights/resnet50v1 --onnx=model_baseline_dynamic.onnx --engine=model_baseline_dynamic.engine --input=224,224,3 --min_bs=1 --max_bs=1 --opt_bs=1 --precision=fp32
#
RESNET_DEPTH=50
RESNET_VERSION=v1
ROOT_DIR=../weights/resnet${RESNET_DEPTH}${RESNET_VERSION}
LOGS_SUBDIR=baseline_engines_trtSource
LOGS_DIR=${ROOT_DIR}/${LOGS_SUBDIR}
mkdir $LOGS_DIR
echo "1/3. Building engine"
# bs=32 OOM in workstation
ONNX=model_baseline_dynamic.onnx
ENGINE=${LOGS_SUBDIR}/model_baseline_dynamic_bs{min1,opt8,max16}.engine
python ../../../engine_builder/build_engine_single.py --root_dir=$ROOT_DIR \
--onnx=$ONNX \
--engine=$ENGINE \
--input=224,224,3 \
--min_bs=1 --opt_bs=8 --max_bs=16 \
--precision=fp32
wait
for BS in 8 16; do # 8 32 128; do
echo "Model evaluation..."
echo "############### bs=${BS} ###############"
# Latency calculation from built engine
echo "2/3. Latency evaluation"
trtexec --device=0 \
--loadEngine=${ROOT_DIR}/${ENGINE} \
--shapes=input_1:0:${BS}x224x224x3 \
--workspace=2048 \
--separateProfileRun \
--dumpProfile \
--explicitBatch &> ${LOGS_DIR}/trtexec_latency_bs${BS}.log
wait
echo "3/3. Accuracy evaluation"
python ../infer_engine.py --engine=${ROOT_DIR}/${ENGINE} \
--log_file=engine_accuracy_bs${BS}.log \
--model_name=resnet_$RESNET_VERSION \
-b=$BS
wait
done
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Single run:
# ../../engine_builder/build_engine_single.py --root_dir=/home/nvidia/PycharmProjects/tensorflow-quantization/examples/resnet/weights/resnet50v1 --onnx=model_baseline_dynamic.onnx --engine=model_baseline_dynamic.engine --input=224,224,3 --min_bs=1 --max_bs=1 --opt_bs=1 --precision=fp32
#
RESNET_DEPTH=50
RESNET_VERSION=v1
QAT_SUBDIR=qat_tfrecord_ep10_steps500_l2False_baselr0.0001_piecewise_sgd_bs128
ROOT_DIR=../weights/resnet${RESNET_DEPTH}${RESNET_VERSION}/${QAT_SUBDIR}
LOGS_SUBDIR=engines_trtSource
LOGS_DIR=${ROOT_DIR}/${LOGS_SUBDIR}
mkdir $LOGS_DIR
echo "1/3. Building engine"
# bs=32 OOM in workstation
ONNX=model_dynamic.onnx
ENGINE=${LOGS_SUBDIR}/model_baseline_bs{min1,opt8,max128}.engine
python ../../../engine_builder/build_engine_single.py --root_dir=$ROOT_DIR \
--onnx=$ONNX \
--engine=$ENGINE \
--input=224,224,3 \
--min_bs=1 --opt_bs=8 --max_bs=128 \
--precision=int8
wait
for BS in 1 8 128; do # 8 32 128; do
echo "Model evaluation..."
echo "############### bs=${BS} ###############"
# Latency calculation from built engine
echo "2/3. Latency evaluation"
trtexec --device=0 \
--loadEngine=${ROOT_DIR}/${ENGINE} \
--shapes=input_1:0:${BS}x224x224x3 \
--workspace=1024 \
--separateProfileRun \
--dumpProfile \
--explicitBatch \
--int8 &> ${LOGS_DIR}/trtexec_latency_bs${BS}.log
wait
echo "3/3. Accuracy evaluation"
python ../infer_engine.py --engine=${ROOT_DIR}/${ENGINE} \
--log_file=engine_accuracy_bs${BS}.log \
--model_name=resnet_$RESNET_VERSION \
-b=$BS
wait
done
@@ -0,0 +1,17 @@
ROOT_DIR="/home/nvidia/PycharmProjects/tensorrt_qat/examples/resnet/"
RESNET_DEPTH="50"
RESNET_VERSION="v1"
MODEL_TYPE="baseline" # "qat"
PRECISION="fp32" # "int8"
ENGINES_DIR="engines_gtc_trt8.4_gittrt/${MODEL_TYPE}"
LOGS_DIR="logs_gtc_trt8.4_gittrt/${MODEL_TYPE}"
for BS in 1; do
SUBDIR="resnet${RESNET_DEPTH}${RESNET_VERSION}_${PRECISION}_${BS}_sparsity_disable_DLA_disabled"
python ../infer_engine.py --engine=${ROOT_DIR}/${ENGINES_DIR}/${SUBDIR}.plan \
--log_file=${ROOT_DIR}/${LOGS_DIR}/${SUBDIR}_accuracy.log \
--model_name=resnet_$RESNET_VERSION -b=1
done
@@ -0,0 +1,113 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import tensorflow as tf
from tensorflow_quantization.quantize import QuantizationSpec
from tensorflow_quantization.custom_qdq_cases import ResNetV1QDQCase, ResNetV2QDQCase
from examples.resnet.utils import get_resnet_model
from tests.onnx_graph_qdq_validator import validate_quantized_model
from tensorflow_quantization.utils import CreateAssetsFolders
import pytest
# Create a directory to save test models
test_assets = CreateAssetsFolders("test_qdq_node_placement")
def test_resnet50v1_quantize_full():
"""
ResNet-v1: Full model quantization
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_resnet_model(resnet_depth="50", resnet_version="v1")
custom_qdq_cases = [ResNetV1QDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
def test_resnet50v1_quantize_full_special():
"""
ResNet-v1: Full model quantization with the first Conv layer (conv2_block1_1_conv) not being quantized.
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_resnet_model(resnet_depth="50", resnet_version="v1")
# Full quantization
custom_qdq_cases = [ResNetV1QDQCase()]
# Special case
qspec = QuantizationSpec()
qspec.add(name="conv2_block1_1_conv", quantize_input=False, quantize_weight=False)
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
qspec=qspec, custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
def test_resnet50v2_quantize_full():
"""
ResNet-v2: Full model quantization
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_resnet_model(resnet_depth="50", resnet_version="v2")
custom_qdq_cases = [ResNetV2QDQCase()]
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
def test_resnet50v2_quantize_full_special():
"""
ResNet-v2: Full model quantization with the first MaxPool layer (pool1_pool) not being quantized.
"""
this_function_name = sys._getframe().f_code.co_name
# Instantiate Baseline model
nn_model_original = get_resnet_model(resnet_depth="50", resnet_version="v2")
custom_qdq_cases = [ResNetV2QDQCase()]
qspec = QuantizationSpec()
qspec.add(name="pool1_pool", quantize_input=False, quantize_weight=False)
q_model, validated = validate_quantized_model(
test_assets, nn_model_original, test_name=this_function_name,
qspec=qspec, custom_qdq_cases=custom_qdq_cases
)
assert validated, "ONNX QDQ validation for full network quantization failed!"
# necessary to clear model layer names from the memory
tf.keras.backend.clear_session()
@@ -0,0 +1,45 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import tensorflow as tf
from examples.data.data_loader import _DEFAULT_IMAGE_SIZE, _NUM_CHANNELS
from examples.utils import get_tfkeras_model
def get_resnet_model(resnet_depth: str = "50", resnet_version: str = "v1") -> tf.keras.Model:
"""
Creates a native tf.keras ResNet model.
Args:
resnet_depth (str): ResNet depth. Options=[50 (default), 101, 152].
resnet_version (str): ResNet version. Options=[v1 (default), v2].
Returns:
model (tf.keras.Model): model corresponding to 'resnet_depth' and 'resnet_version'.
"""
shape = (
_DEFAULT_IMAGE_SIZE["resnet_{}".format(resnet_version)],
_DEFAULT_IMAGE_SIZE["resnet_{}".format(resnet_version)],
_NUM_CHANNELS,
)
model_name = "resnet_" + resnet_depth + resnet_version
model = get_tfkeras_model(model_name=model_name, shape=shape)
return model
@@ -0,0 +1,95 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import tensorflow as tf
from examples.data.data_loader import _NUM_CLASSES, _DEFAULT_IMAGE_SIZE, _NUM_CHANNELS
from typing import Tuple
MODELS_CLASSES_DICT = {
"resnet_50v1": tf.keras.applications.ResNet50,
"resnet_101v1": tf.keras.applications.ResNet101,
"resnet_152v1": tf.keras.applications.ResNet152,
"resnet_50v2": tf.keras.applications.ResNet50V2,
"resnet_101v2": tf.keras.applications.ResNet101V2,
"resnet_152v2": tf.keras.applications.ResNet152V2,
"mobilenet_v1": tf.keras.applications.MobileNet,
"mobilenet_v2": tf.keras.applications.MobileNetV2,
"inception_v3": tf.keras.applications.InceptionV3,
}
def get_tfkeras_model(model_name: str = "mobilenet_v1", shape: Tuple = None) -> tf.keras.Model:
"""
Creates a native tf.keras.applications model.
Args:
model_name (str): Options={model_name_options}.
Returns:
model (tf.keras.Model): model corresponding to 'model_name'.
Raises:
ValueError: raised when 'model_name' is not supported.
""".format(
model_name_options=list(MODELS_CLASSES_DICT.keys())
)
try:
model_class = MODELS_CLASSES_DICT[model_name]
except ValueError:
raise ValueError("Model {} was not found!".format(model_name))
print("Loading model as {}".format(model_class))
if shape is None:
shape = (
_DEFAULT_IMAGE_SIZE[model_name],
_DEFAULT_IMAGE_SIZE[model_name],
_NUM_CHANNELS,
)
input_img = tf.keras.layers.Input(shape=shape, name="input_1")
model = model_class(
include_top=True,
weights="imagenet",
input_tensor=input_img,
input_shape=None,
pooling=None,
classes=_NUM_CLASSES,
classifier_activation="softmax",
)
return model
def print_model_weights_shapes(model):
"""
Print shape of each layer weight.
Args:
model: Keras model
"""
print([model.get_weights()[i].shape for i in range(len(model.get_weights()))])
def ensure_dir(dirname):
"""
Create directory is doesn't exist already.
Args:
dirname: Name of the directory to create.
"""
if not os.path.exists(dirname):
os.makedirs(dirname)
@@ -0,0 +1,279 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright 2022 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The only code snippet inherited from TensorFlow is the 'PiecewiseConstantDecayWithWarmup' class.
See that class description for the exact modifications.
"""
import os
import tensorflow as tf
import numpy as np
from examples.data.data_loader import _NUM_IMAGES
from datetime import datetime
from examples.utils import ensure_dir
from typing import Dict, List
import logging
def get_finetuned_weights_dirname(hyperparams: Dict) -> str:
"""
Generates the directory name to save all files relevant to the model's quantization.
Args:
hyperparams (Dict): dictionary with necessary fine-tuning hyper-parameters.
Returns:
full_dirpath (str): path to directory where the fine-tuned model, log files, ... will be saved.
"""
dirname = (
"qat_"
+ "ep"
+ str(hyperparams["epochs"])
+ "_steps"
+ str(hyperparams["steps_per_epoch"])
+ "_baselr"
+ str(hyperparams["base_lr"])
+ "_"
+ str(hyperparams["optimizer"])
+ "_bs"
+ str(hyperparams["batch_size"])
)
full_dirpath = os.path.join(hyperparams["save_root_dir"], dirname)
return full_dirpath
def compile_model(model):
model.compile(
optimizer="sgd",
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
metrics=["accuracy"],
)
def fine_tune(
q_model: tf.keras.Model,
train_batches: tf.data.Dataset,
val_batches: tf.data.Dataset,
qat_save_finetuned_weights: str,
hyperparams: Dict,
logger: logging.RootLogger = None,
lr_schedule_array: List[tuple] = [(1.0, 1), (0.1, 2), (0.01, 7)],
enable_tensorboard_callback: bool = True
) -> None:
"""
Helper function to fine-tune QAT model.
Args:
q_model (tf.keras.Model): Keras model.
train_batches (tf.data.Dataset): train dataset split in batches.
val_batches (tf.data.Dataset): validation dataset split in batches.
qat_save_finetuned_weights (str): path to directory where the fine-tuned model, log files, ... will be saved.
hyperparams (Dict): dictionary with necessary fine-tuning hyper-parameters.
logger (logging.RootLogger): used to save logs.
lr_schedule_array (List[tuple]): list of tuples in the format '(multiplier, epoch to start)'.
enable_tensorboard_callback (bool): enables tensorboard callback if True.
Returns:
None
Raises:
ValueError: raised when the given optimizer is not supported.
"""
if hyperparams["optimizer"] == "piecewise_sgd":
lr_schedule = PiecewiseConstantDecayWithWarmup(
batch_size=hyperparams["batch_size"],
epoch_size=_NUM_IMAGES["train"], # for tfrecord
warmup_epochs=lr_schedule_array[0][1],
boundaries=list(p[1] for p in lr_schedule_array[1:]),
multipliers=list(p[0] for p in lr_schedule_array),
compute_lr_on_cpu=True,
base_lr=hyperparams["base_lr"],
)
optimizer = tf.keras.optimizers.SGD(learning_rate=lr_schedule, momentum=0.9)
elif hyperparams["optimizer"] == "sgd":
optimizer = tf.keras.optimizers.SGD(
learning_rate=hyperparams["base_lr"], momentum=0.0
)
elif hyperparams["optimizer"] == "adam":
optimizer = tf.keras.optimizers.Adam(hyperparams["base_lr"])
else:
raise ValueError("Optimizer `{}` is not supported. Please add support.".format(hyperparams["optimizer"]))
q_model.compile(
optimizer=optimizer,
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
metrics=["accuracy"],
)
# Initialize TensorBoard visualization
callbacks = []
if enable_tensorboard_callback:
logdir_root = os.path.join(qat_save_finetuned_weights, "logs")
ensure_dir(logdir_root)
logdir = os.path.join(logdir_root, datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=logdir)
callbacks.append(tensorboard_callback)
# Initialize ModelCheckpoint callback
ckpt_callback = tf.keras.callbacks.ModelCheckpoint(
filepath=os.path.join(qat_save_finetuned_weights, "checkpoints_best"),
save_weights_only=True,
monitor="val_accuracy",
mode="max", # Save ckpt with max 'val_accuracy' (best)
save_best_only=True,
)
callbacks.append(ckpt_callback)
history = q_model.fit(
train_batches,
validation_data=val_batches,
batch_size=hyperparams["batch_size"],
epochs=hyperparams["epochs"],
steps_per_epoch=hyperparams["steps_per_epoch"],
callbacks=callbacks,
# verbose=2 if save_log is True else 1 # 0 = silent, 1 = progress bar, 2 = one line per epoch.
)
# Save fine-tuning history to logfile
if logger:
logger.info("------ Per epoch -------")
for ep in history.epoch:
log_str = "Epoch {ep}/{total_ep}".format(
ep=ep + 1, total_ep=history.params["epochs"]
)
for metric_name, metric_value in history.history.items():
log_str += " - " + metric_name + ": {}".format(metric_value[ep])
logger.info(log_str)
logger.info("------------------------")
# Save fine-tuned checkpoints
logger.info("Saving fine-tuned checkpoints")
q_model.save_weights(os.path.join(qat_save_finetuned_weights, "checkpoints_last"))
class PiecewiseConstantDecayWithWarmup(
tf.keras.optimizers.schedules.LearningRateSchedule
):
"""
Piecewise constant decay with warmup schedule.
Original codebase: TensorFlow's "official.vision.image_classification.resnet.common"
Original URL: https://github.com/tensorflow/models/blob/master/official/legacy/image_classification/resnet/common.py
Modification: base learning rate `base_lr` given as parameter instead of a global constant.
PREVIOUS: self.rescaled_lr = BASE_LEARNING_RATE * batch_size / base_lr_batch_size
CURRENT: self.rescaled_lr = base_lr
"""
def __init__(
self,
batch_size,
epoch_size,
warmup_epochs,
boundaries,
multipliers,
compute_lr_on_cpu=True,
name=None,
base_lr=0.1,
):
super(PiecewiseConstantDecayWithWarmup, self).__init__()
if len(boundaries) != len(multipliers) - 1:
raise ValueError(
"The length of boundaries must be 1 less than the "
"length of multipliers"
)
steps_per_epoch = epoch_size // batch_size
self.rescaled_lr = base_lr
self.step_boundaries = [np.int64(steps_per_epoch * x) for x in boundaries]
self.lr_values = [self.rescaled_lr * m for m in multipliers]
self.warmup_steps = warmup_epochs * steps_per_epoch
self.compute_lr_on_cpu = compute_lr_on_cpu
self.name = name
self.learning_rate_ops_cache = {}
def __call__(self, step):
if tf.executing_eagerly():
return self._get_learning_rate(step)
# In an eager function or graph, the current implementation of optimizer
# repeatedly call and thus create ops for the learning rate schedule. To
# avoid this, we cache the ops if not executing eagerly.
graph = tf.compat.v1.get_default_graph()
if graph not in self.learning_rate_ops_cache:
if self.compute_lr_on_cpu:
with tf.device("/device:CPU:0"):
self.learning_rate_ops_cache[graph] = self._get_learning_rate(step)
else:
self.learning_rate_ops_cache[graph] = self._get_learning_rate(step)
return self.learning_rate_ops_cache[graph]
def _get_learning_rate(self, step):
"""Compute learning rate at given step."""
with tf.name_scope("PiecewiseConstantDecayWithWarmup"):
def warmup_lr(step):
return self.rescaled_lr * (
tf.cast(step, tf.float32) / tf.cast(self.warmup_steps, tf.float32)
)
def piecewise_lr(step):
if step.dtype == tf.float32:
self.step_boundaries = [
np.float32(bound) for bound in self.step_boundaries
]
elif step.dtype == tf.int64:
self.step_boundaries = [
np.int64(bound) for bound in self.step_boundaries
]
step_lr = tf.compat.v1.train.piecewise_constant(
step, self.step_boundaries, self.lr_values
)
return step_lr
return tf.cond(
step < self.warmup_steps,
lambda: warmup_lr(step),
lambda: piecewise_lr(step),
)
def get_config(self):
return {
"rescaled_lr": self.rescaled_lr,
"step_boundaries": self.step_boundaries,
"lr_values": self.lr_values,
"warmup_steps": self.warmup_steps,
"compute_lr_on_cpu": self.compute_lr_on_cpu,
"name": self.name,
}