From 8198d8152d0cf5f0726176db7ecec4cc2c7648fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=81=92=E6=AD=A3?= <1771467123@qq.com> Date: Tue, 31 May 2022 06:01:34 +0000 Subject: [PATCH 1/4] First commit Mixnet --- .../cv/Mixnet_ID2072_for_Tensorflow/README.md | 49 + .../boot_modelarts.py | 53 ++ .../eval_ckpt_main.py | 130 +++ .../help_modelarts.py | 90 ++ .../imagenet_input.py | 370 ++++++++ .../lars_optimizer.py | 202 ++++ .../mnas_utils.py | 44 + .../mnasnet_main.py | 895 ++++++++++++++++++ .../mnasnet_model.py | 490 ++++++++++ .../mnasnet_models.py | 358 +++++++ .../modelzoo_level.txt | 7 + .../post_quantization.py | 124 +++ .../preprocessing.py | 211 +++++ .../requirements.txt | 0 .../cv/Mixnet_ID2072_for_Tensorflow/run_1p.sh | 22 + .../cv/Mixnet_ID2072_for_Tensorflow/utils.py | 599 ++++++++++++ 16 files changed, 3644 insertions(+) create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/README.md create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/boot_modelarts.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/eval_ckpt_main.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/help_modelarts.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/imagenet_input.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/lars_optimizer.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnas_utils.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_main.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_model.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_models.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/post_quantization.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/preprocessing.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/requirements.txt create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/run_1p.sh create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/utils.py diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/README.md b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/README.md new file mode 100644 index 000000000..f5f22e177 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/README.md @@ -0,0 +1,49 @@ +# MIXNET +mixnet即混合深度卷积,在一次卷积中自然地混合多个卷积核大小,大内核提取高级语义信息,小内盒提取位置边缘信息,以此获得更好的精度和效率。参考文章为 MixConv: Mixed Depthwise Convolutional Kernels 参考项目: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet/mixnet. + +# setup +* python 3.7.5+ +* tensorflow-gpu 1.15.0+ +* numpy 1.14.1+ + + +# Train + +数据集: 请用户自行准备好数据集,包含训练集和验证集两部分,可选用的数据集包括ImageNet2012,CIFAR10、Flower等,包含train和val两部分。格式为TFRecord文件。 + +Boot File:boot_modelarts.py + +Pretrained: obs://cann-lhz/data_url/result_mix + +# 精度对比 +测试集:Imagenet2012 + +论文精度: + +| Model | Top1 Accuracy | Top5 Accuracy | +| :--------: |:----------: | :-----------: | +| MixNet-S | 73.8% | 92.8% | + + +GPU目标精度: + +| Model | Top1 Accuracy | Top5 Accuracy | +| :--------: |:----------: | :-----------: | +| MixNet-S | 74.1% | 92.6% | + + +Ascend精度: + +| Model | Top1 Accuracy | Top5 Accuracy | +| :--------: |:----------: | :-----------: | +| MixNet-S | 74.3% | 92.8% | + + +# 性能对比: + +| GPU V100 | Ascend 910 | +| :--------: | --------| +| 5.2global_step/s | 11.0global_step/s | + + + diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/boot_modelarts.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/boot_modelarts.py new file mode 100644 index 000000000..a1e412644 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/boot_modelarts.py @@ -0,0 +1,53 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 is the boot file for ModelArts platform. +Firstly, the train datasets are copyed from obs to ModelArts. +Then, the string of train shell command is concated and using 'os.system()' to execute +""" +import os +import numpy as np +import argparse +from help_modelarts import obs_data2modelarts + +print(os.system('env')) + +if __name__ == '__main__': + # Note: the code dir is not the same as work dir on ModelArts Platform!!! + code_dir = os.path.dirname(__file__) + work_dir = os.getcwd() + print("===>>>code_dir:{}, work_dir:{}".format(code_dir, work_dir)) + + parser = argparse.ArgumentParser() + parser.add_argument("--data_url", type=str, default=None) + parser.add_argument("--train_url", type=str, default=None) + parser.add_argument("--modelarts_data_dir", type=str, + default="/cache/data_url") + config = parser.parse_args() + + print("--------config----------") + for k in list(vars(config).keys()): + print("key:{}: value:{}".format(k, vars(config)[k])) + print("--------config----------") + + # copy dataset from obs to modelarts + obs_data2modelarts(config) + + # start to train on Modelarts platform + bash_header = os.path.join(code_dir, 'run_1p.sh') + arg_url = '%s %s %s %s' % (code_dir, config.modelarts_data_dir+'/ILSVRC2012', + config.modelarts_data_dir+'/result_mix', config.train_url) + bash_command = 'bash %s %s' % (bash_header, arg_url) + print("bash command:", bash_command) + os.system(bash_command) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/eval_ckpt_main.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/eval_ckpt_main.py new file mode 100644 index 000000000..98eab5f9e --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/eval_ckpt_main.py @@ -0,0 +1,130 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Eval checkpoint driver. + +This is an example evaluation script for users to understand the model +checkpoints on CPU. To serve the model, please consider to export a `SavedModel` +from checkpoints and use tf-serving to serve. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from absl import app +from absl import flags +import tensorflow.compat.v1 as tf + +import imagenet_input +import mnas_utils +import mnasnet_models +import preprocessing +from mixnet import mixnet_builder + +flags.DEFINE_string('model_name', 'mixnet-s', 'Model name to eval.') +flags.DEFINE_string('runmode', 'examples', + 'Running mode: examples or imagenet') +flags.DEFINE_string( + 'imagenet_eval_glob', None, 'Imagenet eval image glob, ' + 'such as /imagenet/ILSVRC2012*.JPEG') +flags.DEFINE_string( + 'imagenet_eval_label', None, 'Imagenet eval label file path, ' + 'such as /imagenet/ILSVRC2012_validation_ground_truth.txt') +flags.DEFINE_string('ckpt_dir', '/tmp/ckpt/', 'Checkpoint folders') +flags.DEFINE_boolean('enable_ema', True, 'Enable exponential moving average.') +flags.DEFINE_string('export_ckpt', None, 'Exported ckpt for eval graph.') +flags.DEFINE_string('example_img', '/tmp/panda.jpg', + 'Filepath for a single example image.') +flags.DEFINE_string('labels_map_file', '/tmp/labels_map.txt', + 'Labels map from label id to its meaning.') +flags.DEFINE_bool('include_background_label', False, + 'Whether to include background as label #0') +flags.DEFINE_integer('num_images', 5000, + 'Number of images to eval. Use -1 to eval all images.') + + +class EvalCkptDriver(mnas_utils.EvalCkptDriver): + """A driver for running eval inference.""" + + def build_model(self, features, is_training): + """Build model with input features.""" + if self.model_name.startswith('mixnet'): + model_builder = mixnet_builder + elif self.model_name.startswith('mnasnet'): + model_builder = mnasnet_models + else: + raise ValueError('Unknown model name {}'.format(self.model_name)) + + features -= tf.constant( + imagenet_input.MEAN_RGB, shape=[1, 1, 3], dtype=features.dtype) + features /= tf.constant( + imagenet_input.STDDEV_RGB, shape=[1, 1, 3], dtype=features.dtype) + logits, _ = model_builder.build_model(features, self.model_name, + is_training) + probs = tf.nn.softmax(logits) + probs = tf.squeeze(probs) + return probs + + def get_preprocess_fn(self): + """Build input dataset.""" + return preprocessing.preprocess_image + + +def get_eval_driver(model_name, include_background_label=False): + """Get a eval driver.""" + return EvalCkptDriver( + model_name=model_name, + batch_size=1, + image_size=224, + include_background_label=include_background_label) + + +# FLAGS should not be used before main. +FLAGS = flags.FLAGS + + +def main(unused_argv): + """The main function of evaluation.""" + tf.logging.set_verbosity(tf.logging.ERROR) + driver = get_eval_driver(FLAGS.model_name, FLAGS.include_background_label) + if FLAGS.runmode == 'examples': + # Run inference for an example image. + driver.eval_example_images(FLAGS.ckpt_dir, [FLAGS.example_img], + FLAGS.labels_map_file, FLAGS.enable_ema, + FLAGS.export_ckpt) + elif FLAGS.runmode == 'imagenet': + # Run inference for imagenet. + driver.eval_imagenet(FLAGS.ckpt_dir, FLAGS.imagenet_eval_glob, + FLAGS.imagenet_eval_label, FLAGS.num_images, + FLAGS.enable_ema, FLAGS.export_ckpt) + else: + print('must specify runmode: examples or imagenet') + + +if __name__ == '__main__': + app.run(main) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/help_modelarts.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/help_modelarts.py new file mode 100644 index 000000000..d2c380344 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/help_modelarts.py @@ -0,0 +1,90 @@ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Help mox files between obs and modelarts.""" +import os +import datetime +import moxing as mox + + +def obs_data2modelarts(config): + """ + Copy train data from obs to modelarts by using moxing api. + """ + start = datetime.datetime.now() + print("===>>>Copy files from obs:{} to modelarts dir:{}".format( + config.data_url, config.modelarts_data_dir)) + mox.file.copy_parallel(src_url=config.data_url, + dst_url=config.modelarts_data_dir) + end = datetime.datetime.now() + print("===>>>Copy from obs to modelarts, time use:{}(s)".format( + (end - start).seconds)) + files = os.listdir(config.modelarts_data_dir) + print("===>>>Files:", files) + print("===>>>File_nums:", len(files)) + + +def modelarts_result2obs(FLAGS): + """ + Copy debug data from modelarts to obs. + According to the swich flags, the debug data may contains auto tune repository, + dump data for precision comparision, even the computation graph and profiling data. + """ + work_dir = os.getcwd() + + # copy result from modelarts to obs + obs_result_dir = os.path.join(FLAGS.obs_dir, 'result') + if not mox.file.exists(obs_result_dir): + mox.file.make_dirs(obs_result_dir) + mox.file.copy_parallel(src_url=FLAGS.model_dir, dst_url=obs_result_dir) + print("===>>>Copy Event or Checkpoint from modelarts dir:{} to obs:{}".format( + FLAGS.model_dir, obs_result_dir)) + + # Copy dump data. Comment this snippets if npu_dump_data is off. + if FLAGS.npu_dump_data: + obs_dump_data_dir = os.path.join(FLAGS.obs_dir, 'npu_dump_data') + if not mox.file.exists(obs_dump_data_dir): + mox.file.make_dirs(obs_dump_data_dir) + mox.file.copy_parallel(FLAGS.dump_dir, obs_dump_data_dir) + print("===>>>Dumped graph:{} on OBS dir:{}".format( + mox.file.list_directory(obs_dump_data_dir), obs_dump_data_dir)) + + if FLAGS.npu_profiling: + obs_profiling_dir = os.path.join(FLAGS.obs_dir, 'npu_profiling') + if not mox.file.exists(obs_profiling_dir): + mox.file.make_dirs(obs_profiling_dir) + mox.file.copy_parallel(FLAGS.profiling_dir, obs_profiling_dir) + print("===>>>Profiling data:{} on OBS dir:{}".format( + mox.file.list_directory(obs_profiling_dir), obs_profiling_dir)) + + # Copy compute graph. Comment this snippets if npu_dump_graph is off. + if FLAGS.npu_dump_graph: + modelarts_dump_graph_dir = os.path.join(work_dir, "npu_dump_graph") + obs_dump_graph_dir = os.path.join(FLAGS.obs_dir, 'npu_dump_graph') + if not mox.file.exists(obs_dump_graph_dir): + mox.file.make_dirs(obs_dump_graph_dir) + mox.file.copy_parallel(modelarts_dump_graph_dir, obs_dump_graph_dir) + print("===>>>Dumped data:{} on OBS dir:{}".format( + mox.file.list_directory(obs_dump_graph_dir), obs_dump_graph_dir)) + + # Copy profiling data. Comment this snippets if npu_profiling is off. + + # Copy auto tune repository. Comment this snippets if npu_auto_tune is off. + if FLAGS.npu_auto_tune: + modelarts_auto_tune_dir = os.path.join(work_dir, "npu_auto_tune") + obs_auto_tune_dir = os.path.join(FLAGS.obs_dir, 'npu_auto_tune') + if not mox.file.exists(obs_auto_tune_dir): + mox.file.make_dirs(obs_auto_tune_dir) + mox.file.copy_parallel(modelarts_auto_tune_dir, obs_auto_tune_dir) + print("===>>>Auto tune:{} on OBS dir:{}".format( + mox.file.list_directory(obs_auto_tune_dir), obs_auto_tune_dir)) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/imagenet_input.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/imagenet_input.py new file mode 100644 index 000000000..b9e0c2d66 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/imagenet_input.py @@ -0,0 +1,370 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Efficient ImageNet input pipeline using tf.data.Dataset.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import abc +import collections +import functools +import os +from absl import logging +import tensorflow.compat.v1 as tf + +import preprocessing + + +# The input tensor is in the range of [0, 255], we need to scale them. +MEAN_RGB = [0.485 * 255, 0.456 * 255, 0.406 * 255] +STDDEV_RGB = [0.229 * 255, 0.224 * 255, 0.225 * 255] + + +def build_image_serving_input_fn(image_size): + """Builds a serving input fn for raw images.""" + def _image_serving_input_fn(): + """Serving input fn for raw images.""" + def _preprocess_image(image_bytes): + """Preprocess a single raw image.""" + image = preprocessing.preprocess_image( + image_bytes=image_bytes, is_training=False, image_size=image_size) + return image + + image_bytes_list = tf.placeholder( + shape=[None], + dtype=tf.string, + ) + images = tf.map_fn( + _preprocess_image, image_bytes_list, back_prop=False, dtype=tf.float32) + return tf.estimator.export.ServingInputReceiver( + images, {'image_bytes': image_bytes_list}) + return _image_serving_input_fn + + +class ImageNetTFExampleInput(object): + """Base class for ImageNet input_fn generator. + + Attributes: + image_preprocessing_fn: function to preprocess images + is_training: `bool` for whether the input is for training + use_bfloat16: If True, use bfloat16 precision; else use float32. + num_cores: `int` for the number of TPU cores + image_size: `int` for image size (both width and height). + transpose_input: 'bool' for whether to use the double transpose trick + """ + __metaclass__ = abc.ABCMeta + + def __init__(self, + is_training, + use_bfloat16, + num_cores=8, + image_size=224, + transpose_input=False): + self.image_preprocessing_fn = preprocessing.preprocess_image + self.is_training = is_training + self.use_bfloat16 = use_bfloat16 + self.num_cores = num_cores + self.transpose_input = transpose_input + self.image_size = image_size + + def set_shapes(self, batch_size, images, labels): + """Statically set the batch_size dimension.""" + if self.transpose_input: + images.set_shape(images.get_shape().merge_with( + tf.TensorShape([None, None, None, batch_size]))) + labels.set_shape(labels.get_shape().merge_with( + tf.TensorShape([batch_size]))) + else: + images.set_shape(images.get_shape().merge_with( + tf.TensorShape([batch_size, None, None, None]))) + labels.set_shape(labels.get_shape().merge_with( + tf.TensorShape([batch_size]))) + + return images, labels + + def dataset_parser(self, value): + """Parses an image and its label from a serialized ResNet-50 TFExample. + + Args: + value: serialized string containing an ImageNet TFExample. + + Returns: + Returns a tuple of (image, label) from the TFExample. + """ + keys_to_features = { + 'image/encoded': tf.FixedLenFeature((), tf.string, ''), + 'image/class/label': tf.FixedLenFeature([], tf.int64, -1), + } + + parsed = tf.parse_single_example(value, keys_to_features) + image_bytes = tf.reshape(parsed['image/encoded'], shape=[]) + + image = self.image_preprocessing_fn( + image_bytes=image_bytes, + is_training=self.is_training, + image_size=self.image_size, + use_bfloat16=self.use_bfloat16) + + # Subtract one so that labels are in [0, 1000). + label = tf.cast( + tf.reshape(parsed['image/class/label'], shape=[]), dtype=tf.int32) - 1 + + return image, label + + @abc.abstractmethod + def make_source_dataset(self, index, num_hosts): + """Makes dataset of serialized TFExamples. + + The returned dataset will contain `tf.string` tensors, but these strings are + serialized `TFExample` records that will be parsed by `dataset_parser`. + + If self.is_training, the dataset should be infinite. + + Args: + index: current host index. + num_hosts: total number of hosts. + + Returns: + A `tf.data.Dataset` object. + """ + return + + def input_fn(self, params): + """Input function which provides a single batch for train or eval. + + Args: + params: `dict` of parameters passed from the `TPUEstimator`. + `params['batch_size']` is always provided and should be used as the + effective batch size. + + Returns: + A `tf.data.Dataset` object. + """ + # Retrieves the batch size for the current shard. The # of shards is + # computed according to the input pipeline deployment. See + # tf.estimator.tpu.RunConfig for details. + batch_size = params['train_batch_size'] + + if 'context' in params: + current_host = params['context'].current_input_fn_deployment()[1] + num_hosts = params['context'].num_hosts + else: + current_host = 0 + num_hosts = 1 + + dataset = self.make_source_dataset(current_host, num_hosts) + + # Use the fused map-and-batch operation. + # + # For XLA, we must used fixed shapes. Because we repeat the source training + # dataset indefinitely, we can use `drop_remainder=True` to get fixed-size + # batches without dropping any training examples. + # + # When evaluating, `drop_remainder=True` prevents accidentally evaluating + # the same image twice by dropping the final batch if it is less than a full + # batch size. As long as this validation is done with consistent batch size, + # exactly the same images will be used. + dataset = dataset.apply( + tf.data.experimental.map_and_batch( + self.dataset_parser, + batch_size=batch_size, + num_parallel_batches=self.num_cores, + drop_remainder=True)) + + # Transpose for performance on TPU + if self.transpose_input: + dataset = dataset.map( + lambda images, labels: (tf.transpose( + images, [1, 2, 3, 0]), labels), + num_parallel_calls=self.num_cores) + + # Assign static batch size dimension + dataset = dataset.map(functools.partial(self.set_shapes, batch_size)) + + # Prefetch overlaps in-feed with training + dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE) + return dataset + + +class ImageNetInput(ImageNetTFExampleInput): + """Generates ImageNet input_fn from a series of TFRecord files. + + The training data is assumed to be in TFRecord format with keys as specified + in the dataset_parser below, sharded across 1024 files, named sequentially: + + train-00000-of-01024 + train-00001-of-01024 + ... + train-01023-of-01024 + + The validation data is in the same format but sharded in 128 files. + + The format of the data required is created by the script at: + https://github.com/tensorflow/tpu/blob/master/tools/datasets/imagenet_to_gcs.py + """ + + def __init__(self, + is_training, + use_bfloat16, + transpose_input, + data_dir, + image_size=224, + num_parallel_calls=64, + cache=False): + """Create an input from TFRecord files. + + Args: + is_training: `bool` for whether the input is for training + use_bfloat16: If True, use bfloat16 precision; else use float32. + transpose_input: 'bool' for whether to use the double transpose trick + data_dir: `str` for the directory of the training and validation data; + if 'null' (the literal string 'null') or implicitly False + then construct a null pipeline, consisting of empty images + and blank labels. + image_size: `int` for image size (both width and height). + num_parallel_calls: concurrency level to use when reading data from disk. + cache: if true, fill the dataset by repeating from its cache + """ + super(ImageNetInput, self).__init__( + is_training=is_training, + image_size=image_size, + use_bfloat16=use_bfloat16, + transpose_input=transpose_input) + self.data_dir = data_dir + if self.data_dir == 'null' or not self.data_dir: + self.data_dir = None + self.num_parallel_calls = num_parallel_calls + self.cache = cache + + def _get_null_input(self, data): + """Returns a null image (all black pixels). + + Args: + data: element of a dataset, ignored in this method, since it produces + the same null image regardless of the element. + + Returns: + a tensor representing a null image. + """ + del data # Unused since output is constant regardless of input + return tf.zeros([self.image_size, self.image_size, 3], tf.bfloat16 + if self.use_bfloat16 else tf.float32) + + def dataset_parser(self, value): + """See base class.""" + if not self.data_dir: + return value, tf.constant(0, tf.int32) + return super(ImageNetInput, self).dataset_parser(value) + + def make_source_dataset(self, index, num_hosts): + """See base class.""" + if not self.data_dir: + tf.logging.info('Undefined data_dir implies null input') + return tf.data.Dataset.range(1).repeat().map(self._get_null_input) + + # Shuffle the filenames to ensure better randomization. + file_pattern = os.path.join( + self.data_dir, 'train-*' if self.is_training else 'validation-*') + + # For multi-host training, we want each hosts to always process the same + # subset of files. Each host only sees a subset of the entire dataset, + # allowing us to cache larger datasets in memory. + dataset = tf.data.Dataset.list_files(file_pattern, shuffle=False) + dataset = dataset.shard(num_hosts, index) + + if self.is_training and not self.cache: + dataset = dataset.repeat() + + def fetch_dataset(filename): + """Fetch the dataset""" + buffer_size = 8 * 1024 * 1024 # 8 MiB per file + dataset = tf.data.TFRecordDataset( + filename, buffer_size=buffer_size) + return dataset + + # Read the data from disk in parallel + dataset = dataset.apply( + tf.data.experimental.parallel_interleave( + fetch_dataset, cycle_length=self.num_parallel_calls, sloppy=True)) + + if self.cache: + dataset = dataset.cache().apply( + tf.data.experimental.shuffle_and_repeat(1024 * 16)) + else: + dataset = dataset.shuffle(1024) + return dataset + + +# Defines a selection of data from a Cloud Bigtable. +BigtableSelection = collections.namedtuple('BigtableSelection', [ + 'project', 'instance', 'table', 'prefix', 'column_family', + 'column_qualifier' +]) + + +class ImageNetBigtableInput(ImageNetTFExampleInput): + """Generates ImageNet input_fn from a Bigtable for training or evaluation. + """ + + def __init__(self, is_training, use_bfloat16, transpose_input, selection): + """Constructs an ImageNet input from a BigtableSelection. + + Args: + is_training: `bool` for whether the input is for training + use_bfloat16: If True, use bfloat16 precision; else use float32. + transpose_input: 'bool' for whether to use the double transpose trick + selection: a BigtableSelection specifying a part of a Bigtable. + """ + super(ImageNetBigtableInput, self).__init__( + is_training=is_training, + use_bfloat16=use_bfloat16, + transpose_input=transpose_input) + self.selection = selection + + def make_source_dataset(self, index, num_hosts): + """See base class.""" + data = self.selection + try: + from tensorflow.contrib.cloud import BigtableClient # pylint: disable=g-import-not-at-top + except ImportError as e: + logging.exception('Bigtable is not supported in TensorFlow 2.x.') + raise e + + client = BigtableClient(data.project, data.instance) + table = client.table(data.table) + ds = table.parallel_scan_prefix(data.prefix, + columns=[(data.column_family, + data.column_qualifier)]) + # The Bigtable datasets will have the shape (row_key, data) + ds_data = ds.map(lambda index, data: data) + + if self.is_training: + ds_data = ds_data.repeat() + + return ds_data diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/lars_optimizer.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/lars_optimizer.py new file mode 100644 index 000000000..6cc2f8d8d --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/lars_optimizer.py @@ -0,0 +1,202 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Layer-wise Adaptive Rate Scaling optimizer for large-batch training.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow.compat.v1 as tf + + +class LARSOptimizer(tf.train.Optimizer): + """Layer-wise Adaptive Rate Scaling for large batch training. + + Introduced by "Large Batch Training of Convolutional Networks" by Y. You, + I. Gitman, and B. Ginsburg. (https://arxiv.org/abs/1708.03888) + + Implements the LARS learning rate scheme presented in the paper above. This + optimizer is useful when scaling the batch size to up to 32K without + significant performance degradation. It is recommended to use the optimizer + in conjunction with: + - Gradual learning rate warm-up + - Linear learning rate scaling + - Poly rule learning rate decay + + Note, LARS scaling is currently only enabled for dense tensors. Sparse tensors + use the default momentum optimizer. + """ + + def __init__( + self, + learning_rate, + momentum=0.9, + weight_decay=0.0001, + # The LARS coefficient is a hyperparameter + eeta=0.001, + epsilon=0.0, + name="LARSOptimizer", + # Enable skipping variables from LARS scaling. + + # todo(sameerkm): Enable a direct mechanism to pass a + # subset of variables to the optimizer. + + skip_list=None, + use_nesterov=False): + """Construct a new LARS Optimizer. + + Args: + learning_rate: A `Tensor` or floating point value. The base learning rate. + momentum: A floating point value. Momentum hyperparameter. + weight_decay: A floating point value. Weight decay hyperparameter. + eeta: LARS coefficient as used in the paper. Dfault set to LARS + coefficient from the paper. (eeta / weight_decay) determines the highest + scaling factor in LARS. + epsilon: Optional epsilon parameter to be set in models that have very + small gradients. Default set to 0.0. + name: Optional name prefix for variables and ops created by LARSOptimizer. + skip_list: List of strings to enable skipping variables from LARS scaling. + If any of the strings in skip_list is a subset of var.name, variable + 'var' is skipped from LARS scaling. For a typical classification model + with batch normalization, the skip_list is ['batch_normalization', + 'bias'] + use_nesterov: when set to True, nesterov momentum will be enabled + + Raises: + ValueError: If a hyperparameter is set to a non-sensical value. + """ + if momentum < 0.0: + raise ValueError("momentum should be positive: %s" % momentum) + if weight_decay < 0.0: + raise ValueError( + "weight_decay should be positive: %s" % weight_decay) + super(LARSOptimizer, self).__init__(use_locking=False, name=name) + + self._learning_rate = learning_rate + self._momentum = momentum + self._weight_decay = weight_decay + self._eeta = eeta + self._epsilon = epsilon + self._name = name + self._skip_list = skip_list + self._use_nesterov = use_nesterov + + if callable(learning_rate): + learning_rate = learning_rate() + self._learning_rate_tensor = tf.convert_to_tensor( + learning_rate, name="learning_rate") + if callable(momentum): + momentum = momentum() + self._momentum_tensor = tf.convert_to_tensor(momentum, name="momentum") + + def _create_slots(self, var_list): + for v in var_list: + self._zeros_slot(v, "momentum", self._name) + + def compute_lr(self, grad, var): + """Comepute the learning rate according to the weight regularization gradient and var""" + scaled_lr = self._learning_rate + if self._skip_list is None or not any(v in var.name + for v in self._skip_list): + w_norm = tf.norm(var, ord=2) + g_norm = tf.norm(grad, ord=2) + trust_ratio = tf.where( + tf.math.greater(w_norm, 0), + tf.where( + tf.math.greater(g_norm, 0), + (self._eeta * w_norm / + (g_norm + self._weight_decay * w_norm + self._epsilon)), 1.0), + 1.0) + scaled_lr = self._learning_rate * trust_ratio + # Add the weight regularization gradient + grad = grad + self._weight_decay * var + return scaled_lr, grad + + def _apply_dense(self, grad, var): + scaled_lr, grad = self.compute_lr(grad, var) + mom = self.get_slot(var, "momentum") + return tf.raw_ops.ApplyMomentum( + var, + mom, + tf.cast(1.0, var.dtype.base_dtype), + grad * scaled_lr, + self._momentum, + use_locking=False, + use_nesterov=self._use_nesterov) + + def _resource_apply_dense(self, grad, var): + scaled_lr, grad = self.compute_lr(grad, var) + mom = self.get_slot(var, "momentum") + return tf.raw_ops.ResourceApplyMomentum( + var=var.handle, + accum=mom.handle, + lr=tf.cast(1.0, var.dtype.base_dtype), + grad=grad * scaled_lr, + momentum=self._momentum, + use_locking=False, + use_nesterov=self._use_nesterov) + + # Fallback to momentum optimizer for sparse tensors + def _apply_sparse(self, grad, var): + mom = self.get_slot(var, "momentum") + return tf.raw_ops.SparseApplyMomentum( + var, + mom, + tf.cast(self._learning_rate_tensor, var.dtype.base_dtype), + grad.values, + grad.indices, + tf.cast(self._momentum_tensor, var.dtype.base_dtype), + use_locking=self._use_locking, + use_nesterov=self._use_nesterov).op + + def _resource_apply_sparse(self, grad, var, indices): + mom = self.get_slot(var, "momentum") + return tf.raw_ops.ResourceSparseApplyMomentum( + var.handle, + mom.handle, + tf.cast(self._learning_rate_tensor, grad.dtype), + grad, + indices, + tf.cast(self._momentum_tensor, grad.dtype), + use_locking=self._use_locking, + use_nesterov=self._use_nesterov) + + def _prepare(self): + """ + # error: Attribute '_xxxx' defined outside _init__ + learning_rate = self._learning_rate + if callable(learning_rate): + learning_rate = learning_rate() + self._learning_rate_tensor = tf.convert_to_tensor( + learning_rate, name="learning_rate") + momentum = self._momentum + if callable(momentum): + momentum = momentum() + self._momentum_tensor = tf.convert_to_tensor(momentum, name="momentum") + """ + pass diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnas_utils.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnas_utils.py new file mode 100644 index 000000000..667b8f768 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnas_utils.py @@ -0,0 +1,44 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Utils for MnasNet.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import utils as efficientnet_utils + + + +# Import common utils from efficientnet. +archive_ckpt = efficientnet_utils.archive_ckpt +build_learning_rate = efficientnet_utils.build_learning_rate +build_optimizer = efficientnet_utils.build_optimizer +drop_connect = efficientnet_utils.drop_connect +get_ema_vars = efficientnet_utils.get_ema_vars +DepthwiseConv2D = efficientnet_utils.DepthwiseConv2D +EvalCkptDriver = efficientnet_utils.EvalCkptDriver diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_main.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_main.py new file mode 100644 index 000000000..e3e1bc050 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_main.py @@ -0,0 +1,895 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Train a MnasNet on ImageNet on TPU.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import time +from absl import app +from absl import flags +from absl import logging +import numpy as np +import tensorflow.compat.v1 as tf +import tensorflow.compat.v2 as tf2 + +from hyperparameters import common_hparams_flags +from hyperparameters import common_tpu_flags +from hyperparameters import flags_to_params +from hyperparameters import params_dict +import imagenet_input +import mnas_utils +import mnasnet_models +from configs import mnasnet_config +from mixnet import mixnet_builder + +from npu_bridge.npu_init import * +from npu_bridge.estimator.npu.npu_config import NPURunConfig +from npu_bridge.estimator import npu_ops +from npu_bridge.estimator.npu.npu_estimator import NPUEstimator, NPUEstimatorSpec + +common_tpu_flags.define_common_tpu_flags() +common_hparams_flags.define_common_hparams_flags() + +FLAGS = flags.FLAGS + +FAKE_DATA_DIR = 'gs://cloud-tpu-test-datasets/fake_imagenet' + +# Model specific flags +flags.DEFINE_string( + 'model_name', + default=None, + help=( + 'The model name to select models among existing MnasNet configurations.' + )) + +flags.DEFINE_enum('mode', 'train_and_eval', + ['train_and_eval', 'train', 'eval', 'export_only'], + 'One of {"train_and_eval", "train", "eval", "export_only"}.') + +flags.DEFINE_integer('input_image_size', default=None, + help='Input image size.') + +flags.DEFINE_integer( + 'num_train_images', default=None, help='Size of training data set.') + +flags.DEFINE_integer( + 'num_eval_images', default=None, help='Size of evaluation data set.') + +flags.DEFINE_integer( + 'steps_per_eval', + default=6255, + help=('Controls how often evaluation is performed. Since evaluation is' + ' fairly expensive, it is advised to evaluate as infrequently as' + ' possible (i.e. up to --train_steps, which evaluates the model only' + ' after finishing the entire training regime).')) + +flags.DEFINE_integer( + 'eval_timeout', + default=None, + help='Maximum seconds between checkpoints before evaluation terminates.') + +flags.DEFINE_integer( + 'num_parallel_calls', + default=None, + help=('Number of parallel threads in CPU for the input pipeline')) + +flags.DEFINE_string( + 'bigtable_project', None, + 'The Cloud Bigtable project. If None, --gcp_project will be used.') +flags.DEFINE_string('bigtable_instance', None, + 'The Cloud Bigtable instance to load data from.') +flags.DEFINE_string('bigtable_table', 'imagenet', + 'The Cloud Bigtable table to load data from.') +flags.DEFINE_string('bigtable_train_prefix', 'train_', + 'The prefix identifying training rows.') +flags.DEFINE_string('bigtable_eval_prefix', 'validation_', + 'The prefix identifying evaluation rows.') +flags.DEFINE_string('bigtable_column_family', 'tfexample', + 'The column family storing TFExamples.') +flags.DEFINE_string('bigtable_column_qualifier', 'example', + 'The column name storing TFExamples.') + +flags.DEFINE_string( + 'data_format', + default=None, + help=('A flag to override the data format used in the model. The value' + ' is either channels_first or channels_last. To run the network on' + ' CPU or TPU, channels_last should be used. For GPU, channels_first' + ' will improve performance.')) +flags.DEFINE_integer( + 'num_label_classes', default=None, help='Number of classes, at least 2') +flags.DEFINE_float( + 'batch_norm_momentum', + default=None, + help=('Batch normalization layer momentum of moving average to override.')) +flags.DEFINE_float( + 'batch_norm_epsilon', + default=None, + help=('Batch normalization layer epsilon to override..')) + +flags.DEFINE_bool( + 'transpose_input', + default=None, + help='Use TPU double transpose optimization') + +flags.DEFINE_string( + 'export_dir', + default=None, + help=('The directory where the exported SavedModel will be stored.')) +flags.DEFINE_bool( + 'export_to_tpu', + default=False, + help=('Whether to export additional metagraph with "serve, tpu" tags' + ' in addition to "serve" only metagraph.')) +flags.DEFINE_bool( + 'post_quantize', default=True, help=('Enable post quantization.')) + +flags.DEFINE_bool( + 'quantized_training', + default=False, + help=('Enable quantized training as it is required for Edge TPU.' + 'This should be used for fine-tuning rather than pre-training.')) + +flags.DEFINE_integer( + 'quantization_delay_epochs', + default=0, + help=('The number of epochs after which weights and activations are' + ' quantized during training.')) + +flags.DEFINE_bool( + 'export_moving_average', + default=True, + help=('Replace variables with corresponding moving average variables in ' + 'saved model export.')) + +flags.DEFINE_string( + 'init_checkpoint', + default=None, + help=('Initial checkpoint from a pre-trained MnasNet model.')) + +flags.DEFINE_float( + 'base_learning_rate', + default=None, + help=('Base learning rate when train batch size is 256.')) + +flags.DEFINE_float( + 'momentum', + default=None, + help=('Momentum parameter used in the MomentumOptimizer.')) + +flags.DEFINE_float( + 'moving_average_decay', default=None, help=('Moving average decay rate.')) + +flags.DEFINE_float( + 'weight_decay', + default=None, + help=('Weight decay coefficiant for l2 regularization.')) + +flags.DEFINE_float( + 'label_smoothing', + default=None, + help=('Label smoothing parameter used in the softmax_cross_entropy')) + +flags.DEFINE_float( + 'dropout_rate', + default=None, + help=('Dropout rate for the final output layer.')) + +flags.DEFINE_integer( + 'log_step_count_steps', 64, 'The number of steps at ' + 'which the global step information is logged.') + +flags.DEFINE_bool( + 'add_summaries', + default=None, + help=('Whether to write training/eval summaries for visualization.')) + +flags.DEFINE_bool( + 'use_cache', default=None, help=('Enable cache for training input.')) + +flags.DEFINE_float( + 'depth_multiplier', default=None, help=('Depth multiplier per layer.')) + +flags.DEFINE_float( + 'depth_divisor', default=None, help=('Depth divisor (default to 8).')) + +flags.DEFINE_float( + 'min_depth', default=None, help=('Minimal depth (default to None).')) + +flags.DEFINE_bool( + 'use_async_checkpointing', default=None, help=('Enable async checkpoint')) + +flags.DEFINE_bool( + 'use_keras', + default=None, + help=('Whether to use tf.keras.layers to construct networks.')) + +# Learning rate schedule +LR_SCHEDULE = [ # (multiplier, epoch to start) tuples + (1.0, 5), (0.1, 30), (0.01, 60), (0.001, 80) +] + + +def get_pretrained_variables_to_restore(checkpoint_path, + load_moving_average=False): + """Gets veriables_to_restore mapping from pretrained checkpoint. + + Args: + checkpoint_path: String. Path of checkpoint. + load_moving_average: Boolean, whether load moving average variables to + replace variables. + + Returns: + Mapping of variables to restore. + """ + checkpoint_reader = tf.train.load_checkpoint(checkpoint_path) + variable_shape_map = checkpoint_reader.get_variable_to_shape_map() + + variables_to_restore = {} + ema_vars = mnas_utils.get_ema_vars() + for v in tf.global_variables(): + # Skip variables if they are in excluded scopes. + is_excluded = False + for scope in ['global_step', 'ExponentialMovingAverage']: + if scope in v.op.name: + is_excluded = True + break + if is_excluded: + tf.logging.info( + 'Exclude [%s] from loading from checkpoint.', v.op.name) + continue + variable_name_ckpt = v.op.name + if load_moving_average and v in ema_vars: + # To load moving average variables into non-moving version for + # fine-tuning, maps variables here manually. + variable_name_ckpt = v.op.name + '/ExponentialMovingAverage' + + if variable_name_ckpt not in variable_shape_map: + tf.logging.info( + 'Skip init [%s] from [%s] as it is not in the checkpoint', + v.op.name, variable_name_ckpt) + continue + + variables_to_restore[variable_name_ckpt] = v + tf.logging.info('Init variable [%s] from [%s] in ckpt', v.op.name, + variable_name_ckpt) + return variables_to_restore + + +def build_model_fn(features, labels, mode, params): + """The model_fn for MnasNet to be used with TPUEstimator. + + Args: + features: `Tensor` of batched images. + labels: `Tensor` of labels for the data samples + mode: one of `tf.estimator.ModeKeys.{TRAIN,EVAL,PREDICT}` + params: `dict` of parameters passed to the model from the TPUEstimator, + `params['batch_size']` is always provided and should be used as the + effective batch size. + + Returns: + A `TPUEstimatorSpec` for the model + """ + is_training = (mode == tf.estimator.ModeKeys.TRAIN) + # This is essential, if using a keras-derived model. + tf.keras.backend.set_learning_phase(is_training) + + if isinstance(features, dict): + features = features['feature'] + + if mode == tf.estimator.ModeKeys.PREDICT: + # Adds an identify node to help TFLite export. + features = tf.identity(features, 'float_image_input') + + # In most cases, the default data format NCHW instead of NHWC should be + # used for a significant performance boost on GPU. NHWC should be used + # only if the network needs to be run on CPU since the pooling operations + # are only supported on NHWC. TPU uses XLA compiler to figure out best layout. + if params['data_format'] == 'channels_first': + assert not params['transpose_input'] # channels_first only for GPU + features = tf.transpose(features, [0, 3, 1, 2]) + stats_shape = [3, 1, 1] + else: + stats_shape = [1, 1, 3] + + if params['transpose_input'] and mode != tf.estimator.ModeKeys.PREDICT: + features = tf.transpose(features, [3, 0, 1, 2]) # HWCN to NHWC + + # Normalize the image to zero mean and unit variance. + features -= tf.constant( + imagenet_input.MEAN_RGB, shape=stats_shape, dtype=features.dtype) + features /= tf.constant( + imagenet_input.STDDEV_RGB, shape=stats_shape, dtype=features.dtype) + + has_moving_average_decay = (params['moving_average_decay'] > 0) + + tf.logging.info('Using open-source implementation for MnasNet definition.') + override_params = {} + if params['batch_norm_momentum']: + override_params['batch_norm_momentum'] = params['batch_norm_momentum'] + if params['batch_norm_epsilon']: + override_params['batch_norm_epsilon'] = params['batch_norm_epsilon'] + if params['dropout_rate']: + override_params['dropout_rate'] = params['dropout_rate'] + if params['data_format']: + override_params['data_format'] = params['data_format'] + if params['num_label_classes']: + override_params['num_classes'] = params['num_label_classes'] + if params['depth_multiplier']: + override_params['depth_multiplier'] = params['depth_multiplier'] + if params['depth_divisor']: + override_params['depth_divisor'] = params['depth_divisor'] + if params['min_depth']: + override_params['min_depth'] = params['min_depth'] + override_params['use_keras'] = params['use_keras'] + + def _build_model(model_name): + """Build the model for a given model name.""" + if model_name.startswith('mnasnet'): + return mnasnet_models.build_mnasnet_model( + features, + model_name=model_name, + training=is_training, + override_params=override_params) + elif model_name.startswith('mixnet'): + return mixnet_builder.build_model( + features, + model_name=model_name, + training=is_training, + override_params=override_params) + else: + raise ValueError('Unknown model name {}'.format(model_name)) + + if params['precision'] == 'bfloat16': + with tf.tpu.bfloat16_scope(): + logits, _ = _build_model(params['model_name']) + logits = tf.cast(logits, tf.float32) + else: # params['precision'] == 'float32' + logits, _ = _build_model(params['model_name']) + + if params['quantized_training']: + try: + from tensorflow.contrib import quantize # pylint: disable=g-import-not-at-top + except ImportError as e: + logging.exception( + 'Quantized training is not supported in TensorFlow 2.x') + raise e + + if is_training: + tf.logging.info('Adding fake quantization ops for training.') + quantize.create_training_graph( + quant_delay=int(params['steps_per_epoch'] * + FLAGS.quantization_delay_epochs)) + else: + tf.logging.info('Adding fake quantization ops for evaluation.') + quantize.create_eval_graph() + + if mode == tf.estimator.ModeKeys.PREDICT: + scaffold_fn = None + if FLAGS.export_moving_average: + # If the model is trained with moving average decay, to match evaluation + # metrics, we need to export the model using moving average variables. + restore_checkpoint = tf.train.latest_checkpoint(FLAGS.model_dir) + variables_to_restore = get_pretrained_variables_to_restore( + restore_checkpoint, load_moving_average=True) + tf.logging.info('Restoring from the latest checkpoint: %s', + restore_checkpoint) + tf.logging.info(str(variables_to_restore)) + + saver = tf.train.Saver(variables_to_restore) + + predictions = { + 'classes': tf.argmax(logits, axis=1), + 'probabilities': tf.nn.softmax(logits, name='softmax_tensor') + } + return NPUEstimatorSpec( + mode=mode, + predictions=predictions, + export_outputs={ + 'classify': tf.estimator.export.PredictOutput(predictions) + }, + scaffold=tf.train.Scaffold(saver=saver)) + + # If necessary, in the model_fn, use params['batch_size'] instead the batch + # size flags (--train_batch_size or --eval_batch_size). + batch_size = params['train_batch_size'] # pylint: disable=unused-variable + + # Calculate loss, which includes softmax cross entropy and L2 regularization. + one_hot_labels = tf.one_hot(labels, params['num_label_classes']) + cross_entropy = tf.losses.softmax_cross_entropy( + logits=logits, + onehot_labels=one_hot_labels, + label_smoothing=params['label_smoothing']) + + # Add weight decay to the loss for non-batch-normalization variables. + loss = cross_entropy + params['weight_decay'] * tf.add_n([ + tf.nn.l2_loss(v) + for v in tf.trainable_variables() + if 'batch_normalization' not in v.name + ]) + + global_step = tf.train.get_global_step() + if has_moving_average_decay: + ema = tf.train.ExponentialMovingAverage( + decay=params['moving_average_decay'], num_updates=global_step) + ema_vars = mnas_utils.get_ema_vars() + + host_call = None + if is_training: + # Compute the current epoch and associated learning rate from global_step. + current_epoch = ( + tf.cast(global_step, tf.float32) / params['steps_per_epoch']) + + scaled_lr = params['base_learning_rate'] * \ + (params['train_batch_size'] / + 256.0) # pylint: disable=line-too-long + learning_rate = mnas_utils.build_learning_rate(scaled_lr, global_step, + params['steps_per_epoch']) + optimizer = mnas_utils.build_optimizer(learning_rate) + if params['use_tpu']: + # When using TPU, wrap the optimizer with CrossShardOptimizer which + # handles synchronization details between different TPU cores. To the + # user, this should look like regular synchronous training. + optimizer = tf.tpu.CrossShardOptimizer(optimizer) + + if params['add_summaries']: + summary_writer = tf2.summary.create_file_writer( + FLAGS.model_dir, max_queue=params['iterations_per_loop']) + with summary_writer.as_default(): + should_record = tf.equal(global_step % params['iterations_per_loop'], + 0) + with tf2.summary.record_if(should_record): + tf2.summary.scalar('loss', loss, step=global_step) + tf2.summary.scalar( + 'learning_rate', learning_rate, step=global_step) + tf2.summary.scalar( + 'current_epoch', current_epoch, step=global_step) + + # Batch normalization requires UPDATE_OPS to be added as a dependency to + # the train operation. + update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) + + with tf.control_dependencies(update_ops + tf.summary.all_v2_summary_ops()): + train_op = optimizer.minimize(loss, global_step) + + if has_moving_average_decay: + with tf.control_dependencies([train_op]): + train_op = ema.apply(ema_vars) + + else: + train_op = None + + eval_metrics = None + if mode == tf.estimator.ModeKeys.EVAL: + + def metric_fn(labels, logits): + """Evaluation metric function. + + Evaluates accuracy. + + This function is executed on the CPU and should not directly reference + any Tensors in the rest of the `model_fn`. To pass Tensors from the model + to the `metric_fn`, provide as part of the `eval_metrics`. See + https://www.tensorflow.org/api_docs/python/tf/estimator/tpu/TPUEstimatorSpec + for more information. + + Arguments should match the list of `Tensor` objects passed as the second + element in the tuple passed to `eval_metrics`. + + Args: + labels: `Tensor` with shape `[batch]`. + logits: `Tensor` with shape `[batch, num_classes]`. + + Returns: + A dict of the metrics to return from evaluation. + """ + predictions = tf.argmax(logits, axis=1) + top_1_accuracy = tf.metrics.accuracy(labels, predictions) + in_top_5 = tf.cast(tf.nn.in_top_k(logits, labels, 5), tf.float32) + top_5_accuracy = tf.metrics.mean(in_top_5) + + return { + 'top_1_accuracy': top_1_accuracy, + 'top_5_accuracy': top_5_accuracy, + } + + #eval_metrics = (metric_fn, [labels, logits]) + eval_metrics = metric_fn(labels, logits) + + num_params = np.sum([np.prod(v.shape) for v in tf.trainable_variables()]) + tf.logging.info('number of trainable parameters: {}'.format(num_params)) + + # Prepares scaffold_fn if needed. + scaffold_fn = None + if is_training and FLAGS.init_checkpoint: + variables_to_restore = get_pretrained_variables_to_restore( + FLAGS.init_checkpoint, has_moving_average_decay) + tf.logging.info('Initializing from pretrained checkpoint: %s', + FLAGS.init_checkpoint) + if FLAGS.use_tpu: + + def init_scaffold(): + tf.train.init_from_checkpoint(FLAGS.init_checkpoint, + variables_to_restore) + return tf.train.Scaffold() + + scaffold_fn = init_scaffold + else: + tf.train.init_from_checkpoint( + FLAGS.init_checkpoint, variables_to_restore) + + restore_vars_dict = None + if not is_training and has_moving_average_decay: + # Load moving average variables for eval. + restore_vars_dict = ema.variables_to_restore(ema_vars) + + saver = tf.train.Saver(restore_vars_dict) + + return NPUEstimatorSpec( + mode=mode, + loss=loss, + train_op=train_op, + eval_metric_ops=eval_metrics, + scaffold=tf.train.Scaffold(saver=saver)) + + +def _verify_non_empty_string(value, field_name): + """Ensures that a given proposed field value is a non-empty string. + + Args: + value: proposed value for the field. + field_name: string name of the field, e.g. `project`. + + Returns: + The given value, provided that it passed the checks. + + Raises: + ValueError: the value is not a string, or is a blank string. + """ + if not isinstance(value, str): + raise ValueError( + 'Bigtable parameter "%s" must be a string.' % field_name) + if not value: + raise ValueError( + 'Bigtable parameter "%s" must be non-empty.' % field_name) + return value + + +def _select_tables_from_flags(): + """Construct training and evaluation Bigtable selections from flags. + + Returns: + [training_selection, evaluation_selection] + """ + project = _verify_non_empty_string( + FLAGS.bigtable_project or FLAGS.gcp_project, 'project') + instance = _verify_non_empty_string(FLAGS.bigtable_instance, 'instance') + table = _verify_non_empty_string(FLAGS.bigtable_table, 'table') + train_prefix = _verify_non_empty_string(FLAGS.bigtable_train_prefix, + 'train_prefix') + eval_prefix = _verify_non_empty_string(FLAGS.bigtable_eval_prefix, + 'eval_prefix') + column_family = _verify_non_empty_string(FLAGS.bigtable_column_family, + 'column_family') + column_qualifier = _verify_non_empty_string(FLAGS.bigtable_column_qualifier, + 'column_qualifier') + return [ + imagenet_input.BigtableSelection( + project=project, + instance=instance, + table=table, + prefix=p, + column_family=column_family, + column_qualifier=column_qualifier) + for p in (train_prefix, eval_prefix) + ] + + +def export(est, export_dir, params, post_quantize=True): + """Export graph to SavedModel and TensorFlow Lite. + + Args: + est: estimator instance. + export_dir: string, exporting directory. + params: `ParamsDict` passed to the model from the TPUEstimator. + post_quantize: boolean, whether to quantize model checkpoint after training. + + Raises: + ValueError: the export directory path is not specified. + """ + if not export_dir: + raise ValueError('The export directory path is not specified.') + # The guide to serve a exported TensorFlow model is at: + # https://www.tensorflow.org/serving/serving_basic + image_serving_input_fn = imagenet_input.build_image_serving_input_fn( + params.input_image_size) + tf.logging.info('Starting to export model.') + subfolder = est.export_saved_model( + export_dir_base=export_dir, + serving_input_receiver_fn=image_serving_input_fn) + + tf.logging.info('Starting to export TFLite.') + converter = tf.lite.TFLiteConverter.from_saved_model( + subfolder, input_arrays=['truediv'], output_arrays=['logits']) + if params.quantized_training: + # Export quantized tflite if it is trained with quantized ops. + converter.inference_type = tf.uint8 + converter.quantized_input_stats = {'truediv': (0., 2.)} + tflite_model = converter.convert() + tflite_file = os.path.join(export_dir, params.model_name + '.tflite') + tf.gfile.GFile(tflite_file, 'wb').write(tflite_model) + + if post_quantize: + tf.logging.info('Starting to export quantized TFLite.') + converter = tf.lite.TFLiteConverter.from_saved_model( + subfolder, input_arrays=['truediv'], output_arrays=['logits']) + converter.post_training_quantize = True + quant_tflite_model = converter.convert() + quant_tflite_file = os.path.join(export_dir, + params.model_name + '_postquant.tflite') + tf.gfile.GFile(quant_tflite_file, 'wb').write(quant_tflite_model) + + +def main(unused_argv): + params = params_dict.ParamsDict( + mnasnet_config.MNASNET_CFG, mnasnet_config.MNASNET_RESTRICTIONS) + params = params_dict.override_params_dict( + params, FLAGS.config_file, is_strict=True) + params = params_dict.override_params_dict( + params, FLAGS.params_override, is_strict=True) + + params = flags_to_params.override_params_from_input_flags(params, FLAGS) + + additional_params = { + 'steps_per_epoch': params.num_train_images / params.train_batch_size, + 'quantized_training': FLAGS.quantized_training, + 'add_summaries': FLAGS.add_summaries, + } + + params = params_dict.override_params_dict( + params, additional_params, is_strict=False) + + params.validate() + params.lock() + + if FLAGS.tpu or params.use_tpu: + tpu_cluster_resolver = tf.distribute.cluster_resolver.TPUClusterResolver( + FLAGS.tpu, zone=FLAGS.tpu_zone, project=FLAGS.gcp_project) + else: + tpu_cluster_resolver = None + + if params.use_async_checkpointing: + save_checkpoints_steps = None + else: + save_checkpoints_steps = max(100, params.iterations_per_loop) + + # Enables automatic outside compilation. Required in order to + # automatically detect summary ops to run on CPU instead of TPU. + tf.config.set_soft_device_placement(True) + + from tensorflow.core.protobuf.rewriter_config_pb2 import RewriterConfig + config = tf.ConfigProto() + custom_op = config.graph_options.rewrite_options.custom_optimizers.add() + custom_op.name = "NpuOptimizer" + + # Set the precision_mode:allow_mix_precision + custom_op.parameter_map['precision_mode'].s = tf.compat.as_bytes( + 'allow_mix_precision') + custom_op.parameter_map["use_off_line"].b = True + + # Set the dump path. + os.mkdir(FLAGS.dump_dir) + custom_op.parameter_map['dump_path'].s = tf.compat.as_bytes(FLAGS.dump_dir) + # Set the dump debug. + custom_op.parameter_map['enable_dump_debug'].b = True + custom_op.parameter_map['dump_debug_mode'].s = tf.compat.as_bytes('all') + + config.graph_options.rewrite_options.remapping = RewriterConfig.OFF # Must be set OFF. + config.graph_options.rewrite_options.memory_optimization = RewriterConfig.OFF # Must be set OFF. + + """ + # Set the profiling_config. + os.mkdir(FLAGS.profiling_dir) + profiling_options = '{"output":"%s","task_trace":"on"}' % FLAGS.profiling_dir + profiling_config = ProfilingConfig( + enable_profiling=True, profiling_options=profiling_options) + """ + runconfig = NPURunConfig( + # profiling_config=profiling_config, + model_dir=FLAGS.model_dir, + save_checkpoints_steps=save_checkpoints_steps, + log_step_count_steps=FLAGS.log_step_count_steps, + session_config=config) # pylint: disable=line-too-long + + # Validates Flags. + if params.precision == 'bfloat16' and params.use_keras: + raise ValueError( + 'Keras layers do not have full support to bfloat16 activation training.' + ' You have set precision as %s and use_keras as %s' % + (params.precision, params.use_keras)) + + # Initializes model parameters. + mnasnet_est = NPUEstimator( + model_fn=build_model_fn, + config=runconfig, + model_dir=FLAGS.model_dir, + params=params.as_dict()) + + if FLAGS.mode == 'export_only': + export(mnasnet_est, FLAGS.export_dir, params, FLAGS.post_quantize) + return + + # Input pipelines are slightly different (with regards to shuffling and + # preprocessing) between training and evaluation. + if FLAGS.bigtable_instance: + tf.logging.info('Using Bigtable dataset, table %s', + FLAGS.bigtable_table) + select_train, select_eval = _select_tables_from_flags() + imagenet_train, imagenet_eval = [imagenet_input.ImageNetBigtableInput( + is_training=is_training, + use_bfloat16=False, + transpose_input=params.transpose_input, + selection=selection) for (is_training, selection) in + [(True, select_train), + (False, select_eval)]] + else: + if FLAGS.data_dir == FAKE_DATA_DIR: + tf.logging.info('Using fake dataset.') + else: + tf.logging.info('Using dataset: %s', FLAGS.data_dir) + imagenet_train, imagenet_eval = [ + imagenet_input.ImageNetInput( + is_training=is_training, + data_dir=FLAGS.data_dir, + transpose_input=params.transpose_input, + cache=params.use_cache and is_training, + image_size=params.input_image_size, + num_parallel_calls=params.num_parallel_calls, + use_bfloat16=(params.precision == 'bfloat16')) for is_training in [True, False] + ] + + if FLAGS.mode == 'eval': + eval_steps = params.num_eval_images // params.eval_batch_size + # Run evaluation when there's a new checkpoint + for ckpt in tf.train.checkpoints_iterator( + FLAGS.model_dir, timeout=FLAGS.eval_timeout): + tf.logging.info('Starting to evaluate.') + try: + start_timestamp = time.time() # This time will include compilation time + eval_results = mnasnet_est.evaluate( + input_fn=imagenet_eval.input_fn, + steps=eval_steps, + checkpoint_path=ckpt) + elapsed_time = int(time.time() - start_timestamp) + tf.logging.info('Eval results: %s. Elapsed seconds: %d', eval_results, + elapsed_time) + mnas_utils.archive_ckpt( + eval_results, eval_results['top_1_accuracy'], ckpt) + + # Terminate eval job when final checkpoint is reached + current_step = int(os.path.basename(ckpt).split('-')[1]) + if current_step >= params.train_steps: + tf.logging.info('Evaluation finished after training step %d', + current_step) + break + + except tf.errors.NotFoundError: + # Since the coordinator is on a different job than the TPU worker, + # sometimes the TPU worker does not finish initializing until long after + # the CPU job tells it to start evaluating. In this case, the checkpoint + # file could have been deleted already. + tf.logging.info('Checkpoint %s no longer exists, skipping checkpoint', + ckpt) + + if FLAGS.export_dir: + export(mnasnet_est, FLAGS.export_dir, params, FLAGS.post_quantize) + else: # FLAGS.mode == 'train' or FLAGS.mode == 'train_and_eval' + try: + current_step = tf.train.load_variable(FLAGS.model_dir, + tf.GraphKeys.GLOBAL_STEP) + except (TypeError, ValueError, tf.errors.NotFoundError): + current_step = 0 + + tf.logging.info( + 'Training for %d steps (%.2f epochs in total). Current' + ' step %d.', params.train_steps, + params.train_steps / params.steps_per_epoch, current_step) + + start_timestamp = time.time() # This time will include compilation time + + if FLAGS.mode == 'train': + hooks = [] + if params.use_async_checkpointing: + try: + from tensorflow.contrib.tpu.python.tpu import async_checkpoint # pylint: disable=g-import-not-at-top + except ImportError as e: + logging.exception( + 'Async checkpointing is not supported in TensorFlow 2.x') + raise e + + hooks.append( + async_checkpoint.AsyncCheckpointSaverHook( + checkpoint_dir=FLAGS.model_dir, + save_steps=max(100, params.iterations_per_loop))) + mnasnet_est.train( + input_fn=imagenet_train.input_fn, + max_steps=params.train_steps, + hooks=hooks) + + else: + assert FLAGS.mode == 'train_and_eval' + while current_step < params.train_steps: + # Train for up to steps_per_eval number of steps. + # At the end of training, a checkpoint will be written to --model_dir. + next_checkpoint = min(current_step + FLAGS.steps_per_eval, + params.train_steps) + mnasnet_est.train( + input_fn=imagenet_train.input_fn, max_steps=next_checkpoint) + current_step = next_checkpoint + + tf.logging.info('Finished training up to step %d. Elapsed seconds %d.', + next_checkpoint, int(time.time() - start_timestamp)) + + # Evaluate the model on the most recent model in --model_dir. + # Since evaluation happens in batches of --eval_batch_size, some images + # may be excluded modulo the batch size. As long as the batch size is + # consistent, the evaluated images are also consistent. + tf.logging.info('Starting to evaluate.') + eval_results = mnasnet_est.evaluate( + input_fn=imagenet_eval.input_fn, + steps=params.num_eval_images // params.eval_batch_size) + tf.logging.info('Eval results at step %d: %s', next_checkpoint, + eval_results) + ckpt = tf.train.latest_checkpoint(FLAGS.model_dir) + mnas_utils.archive_ckpt( + eval_results, eval_results['top_1_accuracy'], ckpt) + + elapsed_time = int(time.time() - start_timestamp) + tf.logging.info('Finished training up to step %d. Elapsed seconds %d.', + params.train_steps, elapsed_time) + if FLAGS.export_dir: + export(mnasnet_est, FLAGS.export_dir, + params, FLAGS.post_quantize) + + from help_modelarts import modelarts_result2obs + modelarts_result2obs(FLAGS) + + +if __name__ == '__main__': + tf.logging.set_verbosity(tf.logging.INFO) + tf.disable_v2_behavior() + + flags.mark_flag_as_required("data_dir") + flags.mark_flag_as_required("model_dir") + flags.mark_flag_as_required("obs_dir") + flags.mark_flag_as_required("model_name") + + app.run(main) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_model.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_model.py new file mode 100644 index 000000000..6d51d0aa5 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_model.py @@ -0,0 +1,490 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Contains definitions for MnesNet model. + +[1] Mingxing Tan, Bo Chen, Ruoming Pang, Vijay Vasudevan, Quoc V. Le + MnasNet: Platform-Aware Neural Architecture Search for Mobile. + arXiv:1807.11626 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import numpy as np +import six +from six.moves import xrange # pylint: disable=redefined-builtin +import tensorflow.compat.v1 as tf + +import mnas_utils + +GlobalParams = collections.namedtuple('GlobalParams', [ + 'batch_norm_momentum', 'batch_norm_epsilon', 'dropout_rate', 'data_format', + 'num_classes', 'depth_multiplier', 'depth_divisor', 'min_depth', + 'stem_size', 'use_keras' +]) +GlobalParams.__new__.__defaults__ = (None,) * len(GlobalParams._fields) + +# todo(hongkuny): Consider rewrite an argument class with encoding/decoding. +BlockArgs = collections.namedtuple('BlockArgs', [ + 'kernel_size', 'num_repeat', 'input_filters', 'output_filters', + 'expand_ratio', 'id_skip', 'strides', 'se_ratio' +]) +# defaults will be a public argument for namedtuple in Python 3.7 +# https://docs.python.org/3/library/collections.html#collections.namedtuple +BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) + + +def conv_kernel_initializer(shape, dtype=None, partition_info=None): + """Initialization for convolutional kernels. + + The main difference with tf.variance_scaling_initializer is that + tf.variance_scaling_initializer uses a truncated normal with an uncorrected + standard deviation, whereas here we use a normal distribution. Similarly, + tf.contrib.layers.variance_scaling_initializer uses a truncated normal with + a corrected standard deviation. + + Args: + shape: shape of variable + dtype: dtype of variable + partition_info: unused + + Returns: + an initialization for the variable + """ + del partition_info + kernel_height, kernel_width, _, out_filters = shape + fan_out = int(kernel_height * kernel_width * out_filters) + return tf.random_normal( + shape, mean=0.0, stddev=np.sqrt(2.0 / fan_out), dtype=dtype) + + +def dense_kernel_initializer(shape, dtype=None, partition_info=None): + """Initialization for dense kernels. + + This initialization is equal to + tf.variance_scaling_initializer(scale=1.0/3.0, mode='fan_out', + distribution='uniform'). + It is written out explicitly here for clarity. + + Args: + shape: shape of variable + dtype: dtype of variable + partition_info: unused + + Returns: + an initialization for the variable + """ + del partition_info + init_range = 1.0 / np.sqrt(shape[1]) + return tf.random_uniform(shape, -init_range, init_range, dtype=dtype) + + +def round_filters(filters, global_params): + """Round number of filters based on depth multiplier.""" + multiplier = global_params.depth_multiplier + divisor = global_params.depth_divisor + min_depth = global_params.min_depth + if not multiplier: + return filters + + filters *= multiplier + min_depth = min_depth or divisor + new_filters = max(min_depth, int( + filters + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_filters < 0.9 * filters: + new_filters += divisor + return new_filters + + +def _get_conv2d(filters, + kernel_size, + strides, + kernel_initializer, + padding, + use_bias, + data_format='channels_last', + use_keras=True): + """A helper function to create Conv2D layer.""" + if use_keras: + return tf.keras.layers.Conv2D( + filters=filters, + kernel_size=kernel_size, + strides=strides, + kernel_initializer=kernel_initializer, + padding=padding, + data_format=data_format, + use_bias=use_bias) + else: + return tf.layers.Conv2D( + filters=filters, + kernel_size=kernel_size, + strides=strides, + kernel_initializer=kernel_initializer, + padding=padding, + data_format=data_format, + use_bias=use_bias) + + +class MnasBlock(object): + """A class of MnasNet Inveretd Residual Bottleneck. + + Attributes: + has_se: boolean. Whether the block contains a Squeeze and Excitation layer + inside. + endpoints: dict. A list of internal tensors. + """ + + def __init__(self, block_args, global_params): + """Initializes a MnasNet block. + + Args: + block_args: BlockArgs, arguments to create a MnasBlock. + global_params: GlobalParams, a set of global parameters. + """ + self._block_args = block_args + self._batch_norm_momentum = global_params.batch_norm_momentum + self._batch_norm_epsilon = global_params.batch_norm_epsilon + self._use_keras = global_params.use_keras + self._data_format = global_params.data_format + if self._data_format == 'channels_first': + self._channel_axis = 1 + self._spatial_dims = [2, 3] + else: + self._channel_axis = -1 + self._spatial_dims = [1, 2] + self.has_se = (self._block_args.se_ratio is not None) and ( + self._block_args.se_ratio > 0) and (self._block_args.se_ratio <= 1) + + self.endpoints = None + + # Builds the block accordings to arguments. + self._build() + + def block_args(self): + """Call the function of block_args.""" + return self._block_args + + def _build(self): + """Builds MnasNet block according to the arguments.""" + filters = self._block_args.input_filters * self._block_args.expand_ratio + if self._block_args.expand_ratio != 1: + # Expansion phase: + self._expand_conv = _get_conv2d( + filters=filters, + kernel_size=[1, 1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + data_format=self._data_format, + use_keras=self._use_keras) + # todo(hongkuny): b/120622234 need to manage update ops directly. + self._bn0 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + kernel_size = self._block_args.kernel_size + # Depth-wise convolution phase: + if self._use_keras: + self._depthwise_conv = tf.keras.layers.DepthwiseConv2D( + [kernel_size, kernel_size], + strides=self._block_args.strides, + depthwise_initializer=conv_kernel_initializer, + padding='same', + data_format=self._data_format, + use_bias=False) + else: + self._depthwise_conv = mnas_utils.DepthwiseConv2D( + [kernel_size, kernel_size], + strides=self._block_args.strides, + depthwise_initializer=conv_kernel_initializer, + padding='same', + data_format=self._data_format, + use_bias=False) + self._bn1 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + if self.has_se: + num_reduced_filters = max( + 1, int(self._block_args.input_filters * self._block_args.se_ratio)) + # Squeeze and Excitation layer. + self._se_reduce = _get_conv2d( + num_reduced_filters, + kernel_size=[1, 1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=True, + data_format=self._data_format, + use_keras=self._use_keras) + self._se_expand = _get_conv2d( + filters, + kernel_size=[1, 1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=True, + data_format=self._data_format, + use_keras=self._use_keras) + + # Output phase: + filters = self._block_args.output_filters + self._project_conv = _get_conv2d( + filters, + kernel_size=[1, 1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + data_format=self._data_format, + use_keras=self._use_keras) + self._bn2 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + def _call_se(self, input_tensor): + """Call Squeeze and Excitation layer. + + Args: + input_tensor: Tensor, a single input tensor for Squeeze/Excitation layer. + + Returns: + A output tensor, which should have the same shape as input. + """ + se_tensor = tf.reduce_mean( + input_tensor, self._spatial_dims, keepdims=True) + se_tensor = self._se_expand(tf.nn.relu(self._se_reduce(se_tensor))) + tf.logging.info('Built Squeeze and Excitation with tensor shape: %s' % + (se_tensor.shape)) + return tf.sigmoid(se_tensor) * input_tensor + + def call(self, inputs, training=True): + """Implementation of MnasBlock call(). + + Args: + inputs: the inputs tensor. + training: boolean, whether the model is constructed for training. + + Returns: + A output tensor. + """ + tf.logging.info('Block input: %s shape: %s' % + (inputs.name, inputs.shape)) + if self._block_args.expand_ratio != 1: + x = tf.nn.relu( + self._bn0(self._expand_conv(inputs), training=training)) + else: + x = inputs + tf.logging.info('Expand: %s shape: %s' % (x.name, x.shape)) + + x = tf.nn.relu(self._bn1(self._depthwise_conv(x), training=training)) + tf.logging.info('DWConv: %s shape: %s' % (x.name, x.shape)) + + if self.has_se: + with tf.variable_scope('se'): + x = self._call_se(x) + + self.endpoints = {'expansion_output': x} + + x = self._bn2(self._project_conv(x), training=training) + if self._block_args.id_skip: + if all( + s == 1 for s in self._block_args.strides + ) and self._block_args.input_filters == self._block_args.output_filters: + x = tf.add(x, inputs) + tf.logging.info('Project: %s shape: %s' % (x.name, x.shape)) + return tf.identity(x) + + +class MnasNetModel(tf.keras.Model): + """A class implements tf.keras.Model for MnesNet model. + + Reference: https://arxiv.org/abs/1807.11626 + """ + + def __init__(self, blocks_args=None, global_params=None): + """Initializes an `MnasNetModel` instance. + + Args: + blocks_args: A list of BlockArgs to construct MnasNet block modules. + global_params: GlobalParams, a set of global parameters. + + Raises: + ValueError: when blocks_args is not specified as a list. + """ + super(MnasNetModel, self).__init__() + if not isinstance(blocks_args, list): + raise ValueError('blocks_args should be a list.') + self._global_params = global_params + self._blocks_args = blocks_args + self.endpoints = None + self._build() + + def _build(self): + """Builds a MnasNet model.""" + self._blocks = [] + # Builds blocks. + for block_args in self._blocks_args: + assert block_args.num_repeat > 0 + # Update block input and output filters based on depth multiplier. + block_args = block_args._replace( + input_filters=round_filters(block_args.input_filters, + self._global_params), + output_filters=round_filters(block_args.output_filters, + self._global_params)) + + # The first block needs to take care of stride and filter size increase. + self._blocks.append(MnasBlock(block_args, self._global_params)) + if block_args.num_repeat > 1: + # pylint: disable=protected-access + block_args = block_args._replace( + input_filters=block_args.output_filters, strides=[1, 1]) + # pylint: enable=protected-access + for _ in xrange(block_args.num_repeat - 1): + self._blocks.append(MnasBlock(block_args, self._global_params)) + + batch_norm_momentum = self._global_params.batch_norm_momentum + batch_norm_epsilon = self._global_params.batch_norm_epsilon + if self._global_params.data_format == 'channels_first': + channel_axis = 1 + else: + channel_axis = -1 + + # Stem part. + stem_size = self._global_params.stem_size + self._conv_stem = _get_conv2d( + filters=round_filters(stem_size, self._global_params), + kernel_size=[3, 3], + strides=[2, 2], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + data_format=self._global_params.data_format, + use_keras=self._global_params.use_keras) + self._bn0 = tf.layers.BatchNormalization( + axis=channel_axis, + momentum=batch_norm_momentum, + epsilon=batch_norm_epsilon, + fused=True) + + # Head part. + self._conv_head = _get_conv2d( + filters=1280, + kernel_size=[1, 1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + data_format=self._global_params.data_format, + use_keras=self._global_params.use_keras) + self._bn1 = tf.layers.BatchNormalization( + axis=channel_axis, + momentum=batch_norm_momentum, + epsilon=batch_norm_epsilon, + fused=True) + + self._avg_pooling = tf.keras.layers.GlobalAveragePooling2D( + data_format=self._global_params.data_format) + if self._global_params.use_keras: + self._fc = tf.keras.layers.Dense( + self._global_params.num_classes, + kernel_initializer=dense_kernel_initializer) + else: + self._fc = tf.layers.Dense( + self._global_params.num_classes, + kernel_initializer=dense_kernel_initializer) + if self._global_params.dropout_rate > 0: + self._dropout = tf.keras.layers.Dropout( + self._global_params.dropout_rate) + else: + self._dropout = None + + def call(self, inputs, training=True, features_only=None): + """Implementation of MnasNetModel call(). + + Args: + inputs: input tensors. + training: boolean, whether the model is constructed for training. + features_only: build the base feature network only. + + Returns: + output tensors. + """ + outputs = None + self.endpoints = {} + # Calls Stem layers + with tf.variable_scope('mnas_stem'): + outputs = tf.nn.relu( + self._bn0(self._conv_stem(inputs), training=training)) + tf.logging.info('Built stem layers with output shape: %s' % + outputs.shape) + self.endpoints['stem'] = outputs + + # Calls blocks. + reduction_idx = 0 + for idx, block in enumerate(self._blocks): + is_reduction = False + if ((idx == len(self._blocks) - 1) or + self._blocks[idx + 1].block_args().strides[0] > 1): + is_reduction = True + reduction_idx += 1 + + with tf.variable_scope('mnas_blocks_%s' % idx): + outputs = block.call(outputs, training=training) + self.endpoints['block_%s' % idx] = outputs + if is_reduction: + self.endpoints['reduction_%s' % reduction_idx] = outputs + if block.endpoints: + for k, v in six.iteritems(block.endpoints): + self.endpoints['block_%s/%s' % (idx, k)] = v + if is_reduction: + self.endpoints['reduction_%s/%s' % + (reduction_idx, k)] = v + self.endpoints['global_pool'] = outputs + + if not features_only: + # Calls final layers and returns logits. + with tf.variable_scope('mnas_head'): + outputs = tf.nn.relu( + self._bn1(self._conv_head(outputs), training=training)) + outputs = self._avg_pooling(outputs) + if self._dropout: + outputs = self._dropout(outputs, training=training) + outputs = self._fc(outputs) + self.endpoints['head'] = outputs + return outputs diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_models.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_models.py new file mode 100644 index 000000000..ea4150820 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mnasnet_models.py @@ -0,0 +1,358 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Predefined MnasNet models.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import re +import tensorflow.compat.v1 as tf + +import mnasnet_model + + +class MnasNetDecoder(object): + """A class of MnasNet decoder to get model configuration.""" + + def _decode_block_string(self, block_string): + """Gets a MNasNet block through a string notation of arguments. + + E.g. r2_k3_s2_e1_i32_o16_se0.25_noskip: r - number of repeat blocks, + k - kernel size, s - strides (1-9), e - expansion ratio, i - input filters, + o - output filters, se - squeeze/excitation ratio + + Args: + block_string: a string, a string representation of block arguments. + + Returns: + A BlockArgs instance. + Raises: + ValueError: if the strides option is not correctly specified. + """ + assert isinstance(block_string, str) + ops = block_string.split('_') + options = {} + for op in ops: + splits = re.split(r'(\d.*)', op) + if len(splits) >= 2: + key, value = splits[:2] + options[key] = value + + if 's' not in options or len(options['s']) != 2: + raise ValueError('Strides options should be a pair of integers.') + + return mnasnet_model.BlockArgs( + kernel_size=int(options['k']), + num_repeat=int(options['r']), + input_filters=int(options['i']), + output_filters=int(options['o']), + expand_ratio=int(options['e']), + id_skip=('noskip' not in block_string), + se_ratio=float(options['se']) if 'se' in options else None, + strides=[int(options['s'][0]), int(options['s'][1])]) + + def _encode_block_string(self, block): + """Encodes a MnasNet block to a string.""" + args = [ + 'r%d' % block.num_repeat, + 'k%d' % block.kernel_size, + 's%d%d' % (block.strides[0], block.strides[1]), + 'e%s' % block.expand_ratio, + 'i%d' % block.input_filters, + 'o%d' % block.output_filters + ] + if (block.se_ratio is not None and block.se_ratio > 0 and + block.se_ratio <= 1): + args.append('se%s' % block.se_ratio) + if block.id_skip is False: + args.append('noskip') + return '_'.join(args) + + def decode(self, string_list): + """Decodes a list of string notations to specify blocks inside the network. + + Args: + string_list: a list of strings, each string is a notation of MnasNet + block. + + Returns: + A list of namedtuples to represent MnasNet blocks arguments. + """ + assert isinstance(string_list, list) + blocks_args = [] + for block_string in string_list: + blocks_args.append(self._decode_block_string(block_string)) + return blocks_args + + def encode(self, blocks_args): + """Encodes a list of MnasNet Blocks to a list of strings. + + Args: + blocks_args: A list of namedtuples to represent MnasNet blocks arguments. + Returns: + a list of strings, each string is a notation of MnasNet block. + """ + block_strings = [] + for block in blocks_args: + block_strings.append(self._encode_block_string(block)) + return block_strings + + +def mnasnet_b1(depth_multiplier=None): + """Creates a mnasnet-b1 model. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal MnasNet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_s11_e1_i32_o16_noskip', 'r3_k3_s22_e3_i16_o24', + 'r3_k5_s22_e3_i24_o40', 'r3_k5_s22_e6_i40_o80', 'r2_k3_s11_e6_i80_o96', + 'r4_k5_s22_e6_i96_o192', 'r1_k3_s11_e6_i192_o320_noskip' + ] + decoder = MnasNetDecoder() + global_params = mnasnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.2, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=32, + use_keras=True) + return decoder.decode(blocks_args), global_params + + +def mnasnet_a1(depth_multiplier=None): + """Creates a mnasnet-a1 model. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal MnasNet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_s11_e1_i32_o16_noskip', 'r2_k3_s22_e6_i16_o24', + 'r3_k5_s22_e3_i24_o40_se0.25', 'r4_k3_s22_e6_i40_o80', + 'r2_k3_s11_e6_i80_o112_se0.25', 'r3_k5_s22_e6_i112_o160_se0.25', + 'r1_k3_s11_e6_i160_o320' + ] + global_params = mnasnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.2, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=32, + use_keras=True) + decoder = MnasNetDecoder() + return decoder.decode(blocks_args), global_params + + +def mnasnet_small(depth_multiplier=None): + """Creates a mnasnet-a1 model. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal MnasNet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_s11_e1_i16_o8', 'r1_k3_s22_e3_i8_o16', + 'r2_k3_s22_e6_i16_o16', 'r4_k5_s22_e6_i16_o32_se0.25', + 'r3_k3_s11_e6_i32_o32_se0.25', 'r3_k5_s22_e6_i32_o88_se0.25', + 'r1_k3_s11_e6_i88_o144' + ] + global_params = mnasnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=8, + use_keras=True) + decoder = MnasNetDecoder() + return decoder.decode(blocks_args), global_params + + +def mnasnet_d1(depth_multiplier=None): + """Creates a jointly searched mnasnet backbone for mnas-fpn. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal MnasNet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_s11_e9_i32_o24', 'r3_k3_s22_e9_i24_o36', + 'r5_k3_s22_e9_i36_o48', 'r4_k5_s22_e9_i48_o96', + 'r5_k7_s11_e3_i96_o96', 'r3_k3_s22_e9_i96_o80', + 'r1_k7_s11_e6_i80_o320_noskip' + ] + global_params = mnasnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.2, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=32, + use_keras=False) + decoder = MnasNetDecoder() + return decoder.decode(blocks_args), global_params + + +def mnasnet_d1_320(depth_multiplier=None): + """Creates a jointly searched mnasnet backbone for 320x320 input size. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal MnasNet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r3_k5_s11_e6_i32_o24', 'r4_k7_s22_e9_i24_o36', + 'r5_k5_s22_e9_i36_o48', 'r5_k7_s22_e6_i48_o96', + 'r5_k3_s11_e9_i96_o144', 'r5_k5_s22_e6_i144_o160', + 'r1_k7_s11_e9_i160_o320' + ] + + global_params = mnasnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.2, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=32, + use_keras=False) + decoder = MnasNetDecoder() + return decoder.decode(blocks_args), global_params + + +def get_model_params(model_name, override_params): + """Get the block args and global params for a given model.""" + if model_name == 'mnasnet-a1': + blocks_args, global_params = mnasnet_a1() + elif model_name == 'mnasnet-b1': + blocks_args, global_params = mnasnet_b1() + elif model_name == 'mnasnet-small': + blocks_args, global_params = mnasnet_small() + elif model_name == 'mnasnet-d1': + blocks_args, global_params = mnasnet_d1() + elif model_name == 'mnasnet-d1-320': + blocks_args, global_params = mnasnet_d1_320() + else: + raise NotImplementedError( + 'model name is not pre-defined: %s' % model_name) + + if override_params: + # ValueError will be raised here if override_params has fields not included + # in global_params. + global_params = global_params._replace(**override_params) + return blocks_args, global_params + + +def build_mnasnet_model(images, model_name, training, override_params=None): + """A helper functiion to create a MnasNet model and return predicted logits. + + Args: + images: input images tensor. + model_name: string, the model name of a pre-defined MnasNet. + training: boolean, whether the model is constructed for training. + override_params: A dictionary of params for overriding. Fields must exist in + mnasnet_model.GlobalParams. + + Returns: + logits: the logits tensor of classes. + endpoints: the endpoints for each layer. + Raises: + When model_name specified an undefined model, raises NotImplementedError. + When override_params has invalid fields, raises ValueError. + """ + assert isinstance(images, tf.Tensor) + blocks_args, global_params = get_model_params(model_name, override_params) + with tf.variable_scope(model_name): + model = mnasnet_model.MnasNetModel(blocks_args, global_params) + logits = model(images, training=training) + + logits = tf.squeeze(tf.expand_dims(logits, 0), 0) + logits = tf.identity(logits, 'logits') + return logits, model.endpoints + + +def build_mnasnet_base(images, model_name, training, override_params=None): + """A helper functiion to create a MnasNet base model and return global_pool. + + Args: + images: input images tensor. + model_name: string, the model name of a pre-defined MnasNet. + training: boolean, whether the model is constructed for training. + override_params: A dictionary of params for overriding. Fields must exist in + mnasnet_model.GlobalParams. + + Returns: + features: global pool features. + endpoints: the endpoints for each layer. + Raises: + When model_name specified an undefined model, raises NotImplementedError. + When override_params has invalid fields, raises ValueError. + """ + assert isinstance(images, tf.Tensor) + blocks_args, global_params = get_model_params(model_name, override_params) + + with tf.variable_scope(model_name): + model = mnasnet_model.MnasNetModel(blocks_args, global_params) + features = model(images, training=training, features_only=True) + + features = tf.identity(features, 'global_pool') + return features, model.endpoints diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt new file mode 100644 index 000000000..06fa8a2ed --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt @@ -0,0 +1,7 @@ +GPUStatus:OK +NPUMigrationStatus:POK + +FuncStatus:OK +PrecisionStatus:OK +AutoTune:POK +PerfStatus:POK \ No newline at end of file diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/post_quantization.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/post_quantization.py new file mode 100644 index 000000000..fe654ed8a --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/post_quantization.py @@ -0,0 +1,124 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Post-training full quantization script from TF to TFLite.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from absl import app +from absl import flags +import tensorflow.compat.v1 as tf + +import imagenet_input + +flags.DEFINE_string("saved_model_dir", None, + "Path to input savedmodel bundle.") +flags.DEFINE_enum( + "input_name", "float_image_input", ["float_image_input", "truediv"], + "Name of the input node. `float_image_input` is for image " + "array input and `truediv` is for normalized input. Please " + "use `truediv` if require_int8=True and be aware that " + "users need to handle normalization in the client side.") +flags.DEFINE_string("output_name", "logits", "Name of the output node.") +flags.DEFINE_integer( + "num_steps", 1000, + "Number of post-training quantization calibration steps to run.") +flags.DEFINE_integer("image_size", 224, "Size of the input image.") +flags.DEFINE_integer("batch_size", 1, "Batch size of input tensor.") +flags.DEFINE_string("output_tflite", None, "Path to output tflite file.") +flags.DEFINE_string("data_dir", None, "Image dataset directory.") +flags.DEFINE_bool( + "require_int8", False, "Whether all ops should be built-in" + " int8, which is necessary for EdgeTPU.") + +FLAGS = flags.FLAGS + + +def representative_dataset_gen(): + """Gets a python generator of image numpy arrays for ImageNet.""" + params = dict(batch_size=FLAGS.batch_size) + imagenet_eval = imagenet_input.ImageNetInput( + is_training=False, + data_dir=FLAGS.data_dir, + transpose_input=False, + cache=False, + image_size=FLAGS.image_size, + num_parallel_calls=1, + use_bfloat16=False) + + data = imagenet_eval.input_fn(params) + + def preprocess_map_fn(images, labels): + """The function of preprocess the map.""" + del labels + if FLAGS.input_name == "truediv": + images -= tf.constant( + imagenet_input.MEAN_RGB, shape=[1, 1, 3], dtype=images.dtype) + images /= tf.constant( + imagenet_input.STDDEV_RGB, shape=[1, 1, 3], dtype=images.dtype) + return images + + data = data.map(preprocess_map_fn) + iterator = data.make_one_shot_iterator() + for _ in range(FLAGS.num_steps): + # In eager context, we can get a python generator from a dataset iterator. + images = iterator.get_next() + yield [images] + + +def main(_): + """ Enables eager context for TF 1.x. TF 2.x will use eager by default. + This is used to conveniently get a representative dataset generator using + TensorFlow training input helper. + """ + tf.enable_eager_execution() + + converter = tf.lite.TFLiteConverter.from_saved_model( + FLAGS.saved_model_dir, + input_arrays=[FLAGS.input_name], + output_arrays=[FLAGS.output_name]) + # Chooses a tf.lite.Optimize mode: + # https://www.tensorflow.org/api_docs/python/tf/lite/Optimize + converter.optimizations = [tf.lite.Optimize.DEFAULT] + converter.representative_dataset = tf.lite.RepresentativeDataset( + representative_dataset_gen) + if FLAGS.require_int8: + converter.target_spec.supported_ops = [ + tf.lite.OpsSet.TFLITE_BUILTINS_INT8] + + tflite_buffer = converter.convert() + tf.gfile.GFile(FLAGS.output_tflite, "wb").write(tflite_buffer) + print("tflite model written to %s" % FLAGS.output_tflite) + + +if __name__ == "__main__": + flags.mark_flag_as_required("saved_model_dir") + flags.mark_flag_as_required("output_tflite") + flags.mark_flag_as_required("data_dir") + app.run(main) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/preprocessing.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/preprocessing.py new file mode 100644 index 000000000..75a33bd89 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/preprocessing.py @@ -0,0 +1,211 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""ImageNet preprocessing for MnasNet.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow.compat.v1 as tf + +IMAGE_SIZE = 224 +CROP_PADDING = 32 + + +def distorted_bounding_box_crop(image_bytes, + bbox, + min_object_covered=0.1, + aspect_ratio_range=None, + area_range=None, + max_attempts=100, + scope=None): + """Generates cropped_image using one of the bboxes randomly distorted. + + See `tf.image.sample_distorted_bounding_box` for more documentation. + + Args: + image_bytes: `Tensor` of binary image data. + bbox: `Tensor` of bounding boxes arranged `[1, num_boxes, coords]` + where each coordinate is [0, 1) and the coordinates are arranged + as `[ymin, xmin, ymax, xmax]`. If num_boxes is 0 then use the whole + image. + min_object_covered: An optional `float`. Defaults to `0.1`. The cropped + area of the image must contain at least this fraction of any bounding + box supplied. + aspect_ratio_range: An optional list of `float`s. The cropped area of the + image must have an aspect ratio = width / height within this range. + area_range: An optional list of `float`s. The cropped area of the image + must contain a fraction of the supplied image within in this range. + max_attempts: An optional `int`. Number of attempts at generating a cropped + region of the image of the specified constraints. After `max_attempts` + failures, return the entire image. + scope: Optional `str` for name scope. + Returns: + cropped image `Tensor` + """ + if aspect_ratio_range is None: + spect_ratio_range=(0.75, 1.33) + if area_range is None: + area_range=(0.05, 1.0) + with tf.name_scope(scope, 'distorted_bounding_box_crop', [image_bytes, bbox]): + shape = tf.image.extract_jpeg_shape(image_bytes) + sample_distorted_bounding_box = tf.image.sample_distorted_bounding_box( + shape, + bounding_boxes=bbox, + min_object_covered=min_object_covered, + aspect_ratio_range=aspect_ratio_range, + area_range=area_range, + max_attempts=max_attempts, + use_image_if_no_bounding_boxes=True) + bbox_begin, bbox_size, _ = sample_distorted_bounding_box + + # Crop the image to the specified bounding box. + offset_y, offset_x, _ = tf.unstack(bbox_begin) + target_height, target_width, _ = tf.unstack(bbox_size) + crop_window = tf.stack( + [offset_y, offset_x, target_height, target_width]) + image = tf.image.decode_and_crop_jpeg( + image_bytes, crop_window, channels=3) + + return image + + +def _at_least_x_are_equal(a, b, x): + """At least `x` of `a` and `b` `Tensors` are equal.""" + match = tf.equal(a, b) + match = tf.cast(match, tf.int32) + return tf.greater_equal(tf.reduce_sum(match), x) + + +def _decode_and_random_crop(image_bytes, image_size): + """Make a random crop of image_size.""" + bbox = tf.constant([0.0, 0.0, 1.0, 1.0], dtype=tf.float32, shape=[1, 1, 4]) + image = distorted_bounding_box_crop( + image_bytes, + bbox, + min_object_covered=0.1, + aspect_ratio_range=(3. / 4, 4. / 3.), + area_range=(0.08, 1.0), + max_attempts=10, + scope=None) + original_shape = tf.image.extract_jpeg_shape(image_bytes) + bad = _at_least_x_are_equal(original_shape, tf.shape(image), 3) + + image = tf.cond( + bad, + lambda: _decode_and_center_crop(image_bytes, image_size), + # pylint: disable=g-long-lambda + lambda: tf.image.resize([image], [image_size, image_size])[0]) + # pylint: enable=g-long-lambda + + return image + + +def _decode_and_center_crop(image_bytes, image_size): + """Crops to center of image with padding then scales image_size.""" + shape = tf.image.extract_jpeg_shape(image_bytes) + image_height = shape[0] + image_width = shape[1] + + padded_center_crop_size = tf.cast( + ((image_size / (image_size + CROP_PADDING)) * + tf.cast(tf.minimum(image_height, image_width), tf.float32)), tf.int32) + + offset_height = ((image_height - padded_center_crop_size) + 1) // 2 + offset_width = ((image_width - padded_center_crop_size) + 1) // 2 + crop_window = tf.stack([ + offset_height, offset_width, padded_center_crop_size, + padded_center_crop_size + ]) + image = tf.image.decode_and_crop_jpeg(image_bytes, crop_window, channels=3) + image = tf.image.resize([image], [image_size, image_size])[0] + + return image + + +def _flip(image): + """Random horizontal image flip.""" + image = tf.image.random_flip_left_right(image) + return image + + +def preprocess_for_train(image_bytes, use_bfloat16, image_size=IMAGE_SIZE): + """Preprocesses the given image for evaluation. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + use_bfloat16: `bool` for whether to use bfloat16. + image_size: image size. + + Returns: + A preprocessed image `Tensor`. + """ + image = _decode_and_random_crop(image_bytes, image_size) + image = _flip(image) + image = tf.reshape(image, [image_size, image_size, 3]) + image = tf.image.convert_image_dtype( + image, dtype=tf.bfloat16 if use_bfloat16 else tf.float32) + return image + + +def preprocess_for_eval(image_bytes, use_bfloat16, image_size=IMAGE_SIZE): + """Preprocesses the given image for evaluation. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + use_bfloat16: `bool` for whether to use bfloat16. + image_size: image size. + + Returns: + A preprocessed image `Tensor`. + """ + image = _decode_and_center_crop(image_bytes, image_size) + image = tf.reshape(image, [image_size, image_size, 3]) + image = tf.image.convert_image_dtype( + image, dtype=tf.bfloat16 if use_bfloat16 else tf.float32) + return image + + +def preprocess_image(image_bytes, + is_training=False, + use_bfloat16=False, + image_size=IMAGE_SIZE): + """Preprocesses the given image. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + is_training: `bool` for whether the preprocessing is for training. + use_bfloat16: `bool` for whether to use bfloat16. + image_size: image size. + + Returns: + A preprocessed image `Tensor` with value range of [0, 255]. + """ + if is_training: + return preprocess_for_train(image_bytes, use_bfloat16, image_size) + else: + return preprocess_for_eval(image_bytes, use_bfloat16, image_size) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/requirements.txt b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/run_1p.sh b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/run_1p.sh new file mode 100644 index 000000000..deb54415c --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/run_1p.sh @@ -0,0 +1,22 @@ +#!/bin/bash +### Do not need to Configure CANN Environment on Modelarts Platform, because it has been set already. +### Modelarts Platform command for train +export TF_CPP_MIN_LOG_LEVEL=2 ## Tensorflow api print Log Config +export ASCEND_SLOG_PRINT_TO_STDOUT=0 ## Print log on terminal on(1), off(0) + +code_dir=${1} +data_dir=${2} +result_dir=${3} +obs_url=${4} + +current_time=`date "+%Y-%m-%d-%H-%M-%S"` + +python3.7 ${code_dir}/mnasnet_main.py \ + --data_dir=${data_dir} \ + --model_dir=${result_dir} \ + --obs_dir=${obs_url} \ + --model_name='mixnet-s' \ + --npu_dump_data=True \ + --npu_profiling=True \ + --npu_dump_graph=False \ + --npu_auto_tune=False 2>&1 | tee ${result_dir}/${current_time}_train_npu.log \ No newline at end of file diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/utils.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/utils.py new file mode 100644 index 000000000..0b3c80bf8 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/utils.py @@ -0,0 +1,599 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Model utilities.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import json +import os +import sys + +from absl import flags +from absl import logging +import numpy as np +import tensorflow.compat.v1 as tf + +import lars_optimizer +from tensorflow.python.tpu import tpu_function # pylint:disable=g-direct-tensorflow-import + +FLAGS = flags.FLAGS + + +def build_learning_rate(initial_lr, + global_step, + steps_per_epoch=None, + lr_decay_type='exponential', + decay_factor=0.97, + decay_epochs=2.4, + total_steps=None, + warmup_epochs=5): + """Build learning rate.""" + if lr_decay_type == 'exponential': + assert steps_per_epoch is not None + decay_steps = steps_per_epoch * decay_epochs + lr = tf.train.exponential_decay( + initial_lr, global_step, decay_steps, decay_factor, staircase=True) + elif lr_decay_type == 'cosine': + assert total_steps is not None + lr = 0.5 * initial_lr * ( + 1 + tf.cos(np.pi * tf.cast(global_step, tf.float32) / total_steps)) + elif lr_decay_type == 'constant': + lr = initial_lr + elif lr_decay_type == 'poly': + tf.logging.info('Using poly LR schedule') + assert steps_per_epoch is not None + assert total_steps is not None + warmup_steps = int(steps_per_epoch * warmup_epochs) + min_step = tf.constant(1, dtype=tf.int64) + decay_steps = tf.maximum( + min_step, tf.subtract(global_step, warmup_steps)) + lr = tf.train.polynomial_decay( + initial_lr, + decay_steps, + total_steps - warmup_steps + 1, + end_learning_rate=0.1, + power=2.0) + else: + assert False, 'Unknown lr_decay_type : %s' % lr_decay_type + + if warmup_epochs: + logging.info('Learning rate warmup_epochs: %d', warmup_epochs) + warmup_steps = int(warmup_epochs * steps_per_epoch) + warmup_lr = ( + initial_lr * tf.cast(global_step, tf.float32) / tf.cast( + warmup_steps, tf.float32)) + lr = tf.cond(global_step < warmup_steps, lambda: warmup_lr, lambda: lr) + + return lr + + +def build_optimizer(learning_rate, + optimizer_name='rmsprop', + decay=0.9, + epsilon=0.001, + momentum=0.9, + lars_weight_decay=None, + lars_epsilon=None): + """Build optimizer.""" + if optimizer_name == 'sgd': + logging.info('Using SGD optimizer') + optimizer = tf.train.GradientDescentOptimizer( + learning_rate=learning_rate) + elif optimizer_name == 'momentum': + logging.info('Using Momentum optimizer') + optimizer = tf.train.MomentumOptimizer( + learning_rate=learning_rate, momentum=momentum) + elif optimizer_name == 'rmsprop': + logging.info('Using RMSProp optimizer') + optimizer = tf.train.RMSPropOptimizer(learning_rate, decay, momentum, + epsilon) + elif optimizer_name == 'lars': + logging.info('Using LARS optimizer') + assert lars_weight_decay is not None, 'LARS weight decay is None.' + assert lars_epsilon is not None, 'LARS epsilon is None.' + optimizer = lars_optimizer.LARSOptimizer( + learning_rate, + momentum=momentum, + weight_decay=lars_weight_decay, + skip_list=['batch_normalization', 'bias', 'beta', 'gamma'], + epsilon=lars_epsilon) + else: + logging.fatal('Unknown optimizer: %s', optimizer_name) + + return optimizer + + +class TpuBatchNormalization(tf.layers.BatchNormalization): + # class TpuBatchNormalization(tf.layers.BatchNormalization): + """Cross replica batch normalization.""" + + def __init__(self, fused=False, **kwargs): + if fused in (True, None): + raise ValueError( + 'TpuBatchNormalization does not support fused=True.') + super(TpuBatchNormalization, self).__init__(fused=fused, **kwargs) + + def _cross_replica_average(self, t, num_shards_per_group): + """Calculates the average value of input tensor across TPU replicas.""" + num_shards = tpu_function.get_tpu_context().number_of_shards + group_assignment = None + if num_shards_per_group > 1: + if num_shards % num_shards_per_group != 0: + raise ValueError('num_shards: %d mod shards_per_group: %d, should be 0' + % (num_shards, num_shards_per_group)) + num_groups = num_shards // num_shards_per_group + group_assignment = [[ + x for x in range(num_shards) if x // num_shards_per_group == y + ] for y in range(num_groups)] + return tf.tpu.cross_replica_sum(t, group_assignment) / tf.cast( + num_shards_per_group, t.dtype) + + def _moments(self, inputs, reduction_axes, keep_dims): + """Compute the mean and variance: it overrides the original _moments.""" + shard_mean, shard_variance = super(TpuBatchNormalization, self)._moments( + inputs, reduction_axes, keep_dims=keep_dims) + + num_shards = tpu_function.get_tpu_context().number_of_shards or 1 + if num_shards <= 8: # Skip cross_replica for 2x2 or smaller slices. + num_shards_per_group = 1 + else: + num_shards_per_group = max(8, num_shards // 8) + logging.info('TpuBatchNormalization with num_shards_per_group %s', + num_shards_per_group) + if num_shards_per_group > 1: + # Compute variance using: Var[X]= E[X^2] - E[X]^2. + shard_square_of_mean = tf.math.square(shard_mean) + shard_mean_of_square = shard_variance + shard_square_of_mean + group_mean = self._cross_replica_average( + shard_mean, num_shards_per_group) + group_mean_of_square = self._cross_replica_average( + shard_mean_of_square, num_shards_per_group) + group_variance = group_mean_of_square - tf.math.square(group_mean) + return (group_mean, group_variance) + else: + return (shard_mean, shard_variance) + + +class BatchNormalization(tf.layers.BatchNormalization): + """Fixed default name of BatchNormalization to match TpuBatchNormalization.""" + + def __init__(self, name='tpu_batch_normalization', **kwargs): + super(BatchNormalization, self).__init__(name=name, **kwargs) + + +def train_batch_norm(**kwargs): + if 'optimizer' in FLAGS and FLAGS.optimizer == 'lars': + return DistributedBatchNormalization(**kwargs) + return TpuBatchNormalization(**kwargs) + + +def eval_batch_norm(**kwargs): + if 'optimizer' in FLAGS and FLAGS.optimizer == 'lars': + return DistributedBatchNormalization(**kwargs) + return BatchNormalization(**kwargs) + + +class DistributedBatchNormalization(object): + """Distributed batch normalization used in https://arxiv.org/abs/2011.00071.""" + + def __init__(self, axis, momentum, epsilon): + self.axis = axis + self.momentum = momentum + self.epsilon = epsilon + + def __call__(self, x, training, distname='batch_normalization'): + shape = [x.shape[-1]] + with tf.variable_scope('batch_normalization'): + ones = tf.initializers.ones() + zeros = tf.initializers.zeros() + gamma = tf.get_variable( + 'gamma', shape, initializer=ones, trainable=True, use_resource=True) + beta = tf.get_variable( + 'beta', shape, initializer=zeros, trainable=True, use_resource=True) + moving_mean = tf.get_variable( + 'moving_mean', + shape, + initializer=zeros, + trainable=False, + use_resource=True) + moving_variance = tf.get_variable( + 'moving_variance', + shape, + initializer=ones, + trainable=False, + use_resource=True) + num_replicas = FLAGS.num_replicas + + x = tf.cast(x, tf.float32) + if training: + if num_replicas <= 8: + group_assign = None + group_shards = tf.cast(num_replicas, tf.float32) + else: + + group_shards = max( + 1, + int(FLAGS.batch_norm_batch_size / + (FLAGS.train_batch_size / num_replicas))) + group_assign = np.arange(num_replicas, dtype=np.int32) + group_assign = group_assign.reshape([-1, group_shards]) + group_assign = group_assign.tolist() + group_shards = tf.cast(group_shards, tf.float32) + + mean = tf.reduce_mean(x, [0, 1, 2]) + mean = tf.tpu.cross_replica_sum(mean, group_assign) / group_shards + + # Var[x] = E[x^2] - E[x]^2 + mean_sq = tf.reduce_mean(tf.math.square(x), [0, 1, 2]) + mean_sq = tf.tpu.cross_replica_sum( + mean_sq, group_assign) / group_shards + variance = mean_sq - tf.math.square(mean) + + decay = tf.cast(1. - self.momentum, tf.float32) + + def u(moving, normal, name): + """The util of subtract.""" + num_replicas_fp = tf.cast(num_replicas, tf.float32) + normal = tf.tpu.cross_replica_sum(normal) / num_replicas_fp + diff = decay * (moving - normal) + return tf.assign_sub(moving, diff, use_locking=True, name=name) + + tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, + u(moving_mean, mean, name='moving_mean')) + tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, + u(moving_variance, variance, name='moving_variance')) + + x = tf.nn.batch_normalization( + x, + mean=mean, + variance=variance, + offset=beta, + scale=gamma, + variance_epsilon=self.epsilon) + else: + + x, _, _ = tf.nn.fused_batch_norm( + x, + scale=gamma, + offset=beta, + mean=moving_mean, + variance=moving_variance, + epsilon=self.epsilon, + is_training=False) + + return x + + +def drop_connect(inputs, is_training, survival_prob): + """Drop the entire conv with given survival probability.""" + # "Deep Networks with Stochastic Depth", https://arxiv.org/pdf/1603.09382.pdf + if not is_training: + return inputs + + # Compute tensor. + batch_size = tf.shape(inputs)[0] + random_tensor = survival_prob + random_tensor += tf.random_uniform([batch_size, + 1, 1, 1], dtype=inputs.dtype) + binary_tensor = tf.floor(random_tensor) + # Unlike conventional way that multiply survival_prob at test time, here we + # divide survival_prob at training time, such that no addition compute is + # needed at test time. + output = tf.div(inputs, survival_prob) * binary_tensor + return output + + +def archive_ckpt(ckpt_eval, ckpt_objective, ckpt_path): + """Archive a checkpoint if the metric is better.""" + ckpt_dir, ckpt_name = os.path.split(ckpt_path) + + saved_objective_path = os.path.join(ckpt_dir, 'best_objective.txt') + saved_objective = float('-inf') + if tf.gfile.Exists(saved_objective_path): + with tf.gfile.GFile(saved_objective_path, 'r') as f: + saved_objective = float(f.read()) + if saved_objective > ckpt_objective: + logging.info('Ckpt %s is worse than %s', + ckpt_objective, saved_objective) + return False + + filenames = tf.gfile.Glob(ckpt_path + '.*') + if filenames is None: + logging.info('No files to copy for checkpoint %s', ckpt_path) + return False + + # Clear the old folder. + dst_dir = os.path.join(ckpt_dir, 'archive') + if tf.gfile.Exists(dst_dir): + tf.gfile.DeleteRecursively(dst_dir) + tf.gfile.MakeDirs(dst_dir) + + # Write checkpoints. + for f in filenames: + dest = os.path.join(dst_dir, os.path.basename(f)) + tf.gfile.Copy(f, dest, overwrite=True) + ckpt_state = tf.train.generate_checkpoint_state_proto( + dst_dir, + model_checkpoint_path=ckpt_name, + all_model_checkpoint_paths=[ckpt_name]) + with tf.gfile.GFile(os.path.join(dst_dir, 'checkpoint'), 'w') as f: + f.write(str(ckpt_state)) + with tf.gfile.GFile(os.path.join(dst_dir, 'best_eval.txt'), 'w') as f: + f.write('%s' % ckpt_eval) + + # Update the best objective. + with tf.gfile.GFile(saved_objective_path, 'w') as f: + f.write('%f' % ckpt_objective) + + logging.info('Copying checkpoint %s to %s', ckpt_path, dst_dir) + return True + + +def get_ema_vars(): + """Get all exponential moving average (ema) variables.""" + ema_vars = tf.trainable_variables() + tf.get_collection('moving_vars') + for v in tf.global_variables(): + # We maintain mva for batch norm moving mean and variance as well. + if 'moving_mean' in v.name or 'moving_variance' in v.name: + ema_vars.append(v) + return list(set(ema_vars)) + + +class DepthwiseConv2D(tf.keras.layers.DepthwiseConv2D, tf.layers.Layer): + """Wrap keras DepthwiseConv2D to tf.layers.""" + + pass + + +class Conv2D(tf.layers.Conv2D): + """Wrapper for Conv2D with specialization for fast inference.""" + + def _bias_activation(self, outputs): + if self.use_bias: + outputs = tf.nn.bias_add(outputs, self.bias, data_format='NCHW') + if self.activation is not None: + return self.activation(outputs) + return outputs + + def _can_run_fast_1x1(self, inputs): + batch_size = inputs.shape.as_list()[0] + return (self.data_format == 'channels_first' and + batch_size == 1 and + self.kernel_size == (1, 1)) + + def _call_fast_1x1(self, inputs): + # Compute the 1x1 convolution as a matmul. + inputs_shape = tf.shape(inputs) + flat_inputs = tf.reshape(inputs, [inputs_shape[1], -1]) + flat_outputs = tf.matmul( + tf.squeeze(self.kernel), + flat_inputs, + transpose_a=True) + outputs_shape = tf.concat( + [[1, self.filters], inputs_shape[2:]], axis=0) + outputs = tf.reshape(flat_outputs, outputs_shape) + + # Handle the bias and activation function. + return self._bias_activation(outputs) + + def call(self, inputs): + """Rewrite the function of call. """ + if self._can_run_fast_1x1(inputs): + return self._call_fast_1x1(inputs) + return super(Conv2D, self).call(inputs) + + +class EvalCkptDriver(object): + """A driver for running eval inference. + + Attributes: + model_name: str. Model name to eval. + batch_size: int. Eval batch size. + image_size: int. Input image size, determined by model name. + num_classes: int. Number of classes, default to 1000 for ImageNet. + include_background_label: whether to include extra background label. + advprop_preprocessing: whether to use advprop preprocessing. + """ + + def __init__(self, + model_name, + batch_size=1, + image_size=224, + num_classes=1000, + include_background_label=False, + advprop_preprocessing=False): + """Initialize internal variables.""" + self.model_name = model_name + self.batch_size = batch_size + self.num_classes = num_classes + self.include_background_label = include_background_label + self.image_size = image_size + self.advprop_preprocessing = advprop_preprocessing + + def restore_model(self, sess, ckpt_dir, enable_ema=True, export_ckpt=None): + """Restore variables from checkpoint dir.""" + sess.run(tf.global_variables_initializer()) + checkpoint = tf.train.latest_checkpoint(ckpt_dir) + if enable_ema: + ema = tf.train.ExponentialMovingAverage(decay=0.0) + ema_vars = get_ema_vars() + var_dict = ema.variables_to_restore(ema_vars) + ema_assign_op = ema.apply(ema_vars) + else: + var_dict = get_ema_vars() + ema_assign_op = None + + tf.train.get_or_create_global_step() + sess.run(tf.global_variables_initializer()) + saver = tf.train.Saver(var_dict, max_to_keep=1) + saver.restore(sess, checkpoint) + + if export_ckpt: + if ema_assign_op is not None: + sess.run(ema_assign_op) + saver = tf.train.Saver(max_to_keep=1, save_relative_paths=True) + saver.save(sess, export_ckpt) + + def build_model(self, features, is_training): + """Build model with input features.""" + del features, is_training + raise ValueError('Must be implemented by subclasses.') + + def get_preprocess_fn(self): + """Raise the error.""" + raise ValueError('Must be implemented by subclsses.') + + def build_dataset(self, filenames, labels, is_training): + """Build input dataset.""" + batch_drop_remainder = False + if 'condconv' in self.model_name and not is_training: + # CondConv layers can only be called with known batch dimension. Thus, we + # must drop all remaining examples that do not make up one full batch. + # To ensure all examples are evaluated, use a batch size that evenly + # divides the number of files. + batch_drop_remainder = True + num_files = len(filenames) + if num_files % self.batch_size != 0: + tf.logging.warn('Remaining examples in last batch are not being ' + 'evaluated.') + filenames = tf.constant(filenames) + labels = tf.constant(labels) + dataset = tf.data.Dataset.from_tensor_slices((filenames, labels)) + + def _parse_function(filename, label): + image_string = tf.read_file(filename) + preprocess_fn = self.get_preprocess_fn() + image_decoded = preprocess_fn( + image_string, is_training, image_size=self.image_size) + image = tf.cast(image_decoded, tf.float32) + return image, label + + dataset = dataset.map(_parse_function) + dataset = dataset.batch(self.batch_size, + drop_remainder=batch_drop_remainder) + + iterator = dataset.make_one_shot_iterator() + images, labels = iterator.get_next() + return images, labels + + def run_inference(self, + ckpt_dir, + image_files, + labels, + enable_ema=True, + export_ckpt=None): + """Build and run inference on the target images and labels.""" + label_offset = 1 if self.include_background_label else 0 + with tf.Graph().as_default(), tf.Session() as sess: + images, labels = self.build_dataset(image_files, labels, False) + probs = self.build_model(images, is_training=False) + if isinstance(probs, tuple): + probs = probs[0] + + self.restore_model(sess, ckpt_dir, enable_ema, export_ckpt) + + prediction_idx = [] + prediction_prob = [] + for _ in range(len(image_files) // self.batch_size): + out_probs = sess.run(probs) + idx = np.argsort(out_probs)[::-1] + prediction_idx.append(idx[:5] - label_offset) + prediction_prob.append([out_probs[pid] for pid in idx[:5]]) + + # Return the top 5 predictions (idx and prob) for each image. + return prediction_idx, prediction_prob + + def eval_example_images(self, + ckpt_dir, + image_files, + labels_map_file, + enable_ema=True, + export_ckpt=None): + """Eval a list of example images. + + Args: + ckpt_dir: str. Checkpoint directory path. + image_files: List[str]. A list of image file paths. + labels_map_file: str. The labels map file path. + enable_ema: enable expotential moving average. + export_ckpt: export ckpt folder. + + Returns: + A tuple (pred_idx, and pred_prob), where pred_idx is the top 5 prediction + index and pred_prob is the top 5 prediction probability. + """ + classes = json.loads(tf.gfile.Open(labels_map_file).read()) + pred_idx, pred_prob = self.run_inference( + ckpt_dir, image_files, [0] * len(image_files), enable_ema, export_ckpt) + for i in range(len(image_files)): + print('predicted class for image {}: '.format(image_files[i])) + for j, idx in enumerate(pred_idx[i]): + print(' -> top_{} ({:4.2f}%): {} '.format(j, pred_prob[i][j] * 100, + classes[str(idx)])) + return pred_idx, pred_prob + + def eval_imagenet(self, ckpt_dir, imagenet_eval_glob, + imagenet_eval_label, num_images, enable_ema, export_ckpt): + """Eval ImageNet images and report top1/top5 accuracy. + + Args: + ckpt_dir: str. Checkpoint directory path. + imagenet_eval_glob: str. File path glob for all eval images. + imagenet_eval_label: str. File path for eval label. + num_images: int. Number of images to eval: -1 means eval the whole + dataset. + enable_ema: enable expotential moving average. + export_ckpt: export checkpoint folder. + + Returns: + A tuple (top1, top5) for top1 and top5 accuracy. + """ + imagenet_val_labels = [int(i) + for i in tf.gfile.GFile(imagenet_eval_label)] + imagenet_filenames = sorted(tf.gfile.Glob(imagenet_eval_glob)) + if num_images < 0: + num_images = len(imagenet_filenames) + image_files = imagenet_filenames[:num_images] + labels = imagenet_val_labels[:num_images] + + pred_idx, _ = self.run_inference( + ckpt_dir, image_files, labels, enable_ema, export_ckpt) + top1_cnt, top5_cnt = 0.0, 0.0 + for i, label in enumerate(labels): + top1_cnt += label in pred_idx[i][:1] + top5_cnt += label in pred_idx[i][:5] + if i % 100 == 0: + print('Step {}: top1_acc = {:4.2f}% top5_acc = {:4.2f}%'.format( + i, 100 * top1_cnt / (i + 1), 100 * top5_cnt / (i + 1))) + sys.stdout.flush() + top1, top5 = 100 * top1_cnt / num_images, 100 * top5_cnt / num_images + print( + 'Final: top1_acc = {:4.2f}% top5_acc = {:4.2f}%'.format(top1, top5)) + return top1, top5 -- Gitee From 1c6f33442691ac5c8529466f1f703bb0cf737667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=81=92=E6=AD=A3?= <1771467123@qq.com> Date: Tue, 31 May 2022 06:07:11 +0000 Subject: [PATCH 2/4] First commit Mixnet --- .../configs/__init__.py | 28 ++ .../configs/cloud/gpu.yaml | 7 + .../configs/cloud/v2-32.yaml | 10 + .../configs/cloud/v2-8.yaml | 8 + .../configs/cloud/v3-32.yaml | 10 + .../configs/cloud/v3-8.yaml | 8 + .../configs/mnasnet_config.py | 63 +++ .../g3doc/mnasnet_vs_mobilenetv2.png | Bin 0 -> 23372 bytes .../g3doc/mnasnet_vs_mobilenetv2_2.png | Bin 0 -> 48505 bytes .../hyperparameters/__init__.py | 28 ++ .../hyperparameters/common_hparams_flags.py | 124 +++++ .../hyperparameters/common_tpu_flags.py | 54 +++ .../hyperparameters/flags_to_params.py | 101 ++++ .../hyperparameters/params_dict.py | 441 +++++++++++++++++ .../mixnet/__init__.py | 28 ++ .../mixnet/custom_layers.py | 153 ++++++ .../mixnet/g3doc/mixnet-flops.png | Bin 0 -> 189229 bytes .../mixnet/mixnet_builder.py | 307 ++++++++++++ .../mixnet/mixnet_model.py | 452 ++++++++++++++++++ 19 files changed, 1822 insertions(+) create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/__init__.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/gpu.yaml create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-32.yaml create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-8.yaml create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-32.yaml create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-8.yaml create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/mnasnet_config.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2.png create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2_2.png create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/__init__.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_hparams_flags.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_tpu_flags.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/flags_to_params.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/params_dict.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/__init__.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/custom_layers.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/g3doc/mixnet-flops.png create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/mixnet_builder.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/mixnet_model.py diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/__init__.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/__init__.py new file mode 100644 index 000000000..20c4af897 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 initial file of config.""" \ No newline at end of file diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/gpu.yaml b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/gpu.yaml new file mode 100644 index 000000000..3dd114758 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/gpu.yaml @@ -0,0 +1,7 @@ +use_tpu: False +train_steps: 3503192 # 1281167 * 350 / train_batch_size +train_batch_size: 128 # 1024 / 8 +eval_batch_size: 128 # 1024 / 8 +model_name: 'mnasnet-a1' +dropout_rate: null +depth_multiplier: null diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-32.yaml b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-32.yaml new file mode 100644 index 000000000..66a606796 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-32.yaml @@ -0,0 +1,10 @@ +train_steps: 109474 +train_batch_size: 4096 +eval_batch_size: 256 +iterations_per_loop: 100 +skip_host_call: false +model_name: 'mnasnet-a1' +dropout_rate: null +depth_multiplier: null +use_keras: true +precision: 'float32' diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-8.yaml b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-8.yaml new file mode 100644 index 000000000..f6a8eab2e --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v2-8.yaml @@ -0,0 +1,8 @@ +train_steps: 437899 +train_batch_size: 1024 +eval_batch_size: 1024 +iterations_per_loop: 1251 +skip_host_call: True +model_name: 'mnasnet-a1' +dropout_rate: null +depth_multiplier: null diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-32.yaml b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-32.yaml new file mode 100644 index 000000000..66a606796 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-32.yaml @@ -0,0 +1,10 @@ +train_steps: 109474 +train_batch_size: 4096 +eval_batch_size: 256 +iterations_per_loop: 100 +skip_host_call: false +model_name: 'mnasnet-a1' +dropout_rate: null +depth_multiplier: null +use_keras: true +precision: 'float32' diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-8.yaml b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-8.yaml new file mode 100644 index 000000000..f6a8eab2e --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/cloud/v3-8.yaml @@ -0,0 +1,8 @@ +train_steps: 437899 +train_batch_size: 1024 +eval_batch_size: 1024 +iterations_per_loop: 1251 +skip_host_call: True +model_name: 'mnasnet-a1' +dropout_rate: null +depth_multiplier: null diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/mnasnet_config.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/mnasnet_config.py new file mode 100644 index 000000000..2773e5e6a --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/configs/mnasnet_config.py @@ -0,0 +1,63 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. + +"""Config to train MNasNet.""" + +MNASNET_CFG = { + 'use_tpu': False, + 'train_batch_size': 64, + 'eval_batch_size': 64, + 'num_train_images': 1281167, + 'num_eval_images': 50000, + 'iterations_per_loop': 1251, + 'num_parallel_calls': 64, + 'num_label_classes': 1000, + 'transpose_input': True, + 'base_learning_rate': 0.016, + 'momentum': 0.9, + 'moving_average_decay': 0.9999, + 'weight_decay': 0.00001, + 'label_smoothing': 0.1, + 'dropout_rate': 0.2, + 'use_cache': False, + 'use_async_checkpointing': False, + 'precision': 'float32', + 'use_keras': True, + 'skip_host_call': False, + 'input_image_size': 224, + 'train_steps': 150000, + 'model_name': 'mixnet-s', + 'data_format': 'channels_last', + 'batch_norm_momentum': None, + 'batch_norm_epsilon': None, + 'depth_multiplier': None, + 'depth_divisor': None, + 'min_depth': 0, +} + +MNASNET_RESTRICTIONS = [] diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2.png b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2.png new file mode 100644 index 0000000000000000000000000000000000000000..baecdf256932bfb467514e019d43617a820cc04e GIT binary patch literal 23372 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 KcaCdV=05?H_?5u` literal 0 HcmV?d00001 diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2_2.png b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/g3doc/mnasnet_vs_mobilenetv2_2.png new file mode 100644 index 0000000000000000000000000000000000000000..eb59c4d207ae47dbaaa580ef00666fe677d1f278 GIT binary patch literal 48505 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
_im`IMZ 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 S{QZc)pT3Uq^-68~i2nzJ!or;Z literal 0 HcmV?d00001 diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/__init__.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/__init__.py new file mode 100644 index 000000000..2a59a24e7 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 initial file of hyperparameters.""" diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_hparams_flags.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_hparams_flags.py new file mode 100644 index 000000000..e2c32276d --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_hparams_flags.py @@ -0,0 +1,124 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Defining common model params used across all the models.""" + +from absl import flags + + +def define_common_hparams_flags(): + """Define the common flags across models.""" + + flags.DEFINE_string( + 'data_dir', default=None, + help='The directory where the ImageNet input data is stored. Please see the README.md for the expected data format.') + + flags.DEFINE_string( + 'model_dir', default=None, + help='The directory where the model and training/evaluation summaries are stored.') + + flags.DEFINE_string( + 'dump_dir', default="/cache/dump_dir", + help='The directory where the dump data are stored.') + flags.DEFINE_string( + 'profiling_dir', default="/cache/profiling_dir", + help='The directory where the profiling data are stored.You need create it first and have the permissions of read and write.') + + flags.DEFINE_string("obs_dir", None, "obs dir") + + # The following params only useful on NPU chip mode + flags.DEFINE_boolean("npu_dump_data", True, + "dump data for precision or not") + flags.DEFINE_boolean("npu_profiling", False, + "profiling for performance or not") + flags.DEFINE_boolean("npu_dump_graph", False, "dump graph or not") + flags.DEFINE_boolean("npu_auto_tune", False, + "auto tune or not. And you must set tune_bank_path param.") + + flags.DEFINE_bool( + 'use_tpu', default=None, + help=('Use TPU to execute the model for training and evaluation. If' + ' --use_tpu=false, will use whatever devices are available to' + ' TensorFlow by default (e.g. CPU and GPU)')) + + flags.DEFINE_integer( + 'train_batch_size', default=None, help='Batch size for training.') + + flags.DEFINE_integer( + 'train_steps', default=None, + help=('The number of steps to use for training. This flag' + ' should be adjusted according to the --train_batch_size flag.')) + + flags.DEFINE_integer( + 'eval_batch_size', default=None, help='Batch size for evaluation.') + + flags.DEFINE_bool( + 'skip_host_call', default=None, + help=('Skip the host_call which is executed every training step. This is' + ' generally used for generating training summaries (train loss,' + ' learning rate, etc...). When --skip_host_call=false, there could' + ' be a performance drop if host_call function is slow and cannot' + ' keep up with the TPU-side computation.')) + + flags.DEFINE_integer( + 'iterations_per_loop', default=None, + help=('Number of steps to run on TPU before outfeeding metrics to the ' + 'CPU. If the number of iterations in the loop would exceed the ' + 'number of train steps, the loop will exit before reaching' + ' --iterations_per_loop. The larger this value is, the higher the' + ' utilization on the TPU.')) + + flags.DEFINE_string( + 'precision', default=None, + help=('Precision to use; one of: {bfloat16, float32}')) + + flags.DEFINE_string( + 'config_file', default=None, + help=('A YAML file which specifies overrides. Note that this file can be ' + 'used as an override template to override the default parameters ' + 'specified in Python. If the same parameter is specified in both ' + '`--config_file` and `--params_override`, the one in ' + '`--params_override` will be used finally.')) + + flags.DEFINE_string( + 'params_override', default=None, + help=('a YAML/JSON string or a YAML file which specifies additional ' + 'overrides over the default parameters and those specified in ' + '`--config_file`. Note that this is supposed to be used only to ' + 'override the model parameters, but not the parameters like TPU ' + 'specific flags. One canonical use case of `--config_file` and ' + '`--params_override` is users first define a template config file ' + 'using `--config_file`, then use `--params_override` to adjust the ' + 'minimal set of tuning parameters, for example setting up different' + ' `train_batch_size`. ' + 'The final override order of parameters: default_model_params --> ' + 'params from config_file --> params in params_override.' + 'See also the help message of `--config_file`.')) + + flags.DEFINE_bool( + 'eval_after_training', default=False, + help='Run one eval after the training finishes.') diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_tpu_flags.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_tpu_flags.py new file mode 100644 index 000000000..95ec67647 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/common_tpu_flags.py @@ -0,0 +1,54 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Defining common TPU flags used across all the models.""" + +from absl import flags + + +def define_common_tpu_flags(): + """Define the flags related to TPU's.""" + flags.DEFINE_string( + 'tpu', default=None, + help='The Cloud TPU to use for training. This should be either the name ' + 'used when creating the Cloud TPU, or a grpc://ip.address.of.tpu:8470 ' + 'url.') + + flags.DEFINE_string( + 'gcp_project', default=None, + help='Project name for the Cloud TPU-enabled project. If not specified, we ' + 'will attempt to automatically detect the GCE project from metadata.') + + flags.DEFINE_string( + 'tpu_zone', default=None, + help='GCE zone where the Cloud TPU is located in. If not specified, we ' + 'will attempt to automatically detect the GCE project from metadata.') + + flags.DEFINE_string( + 'eval_master', default=None, + help='GRPC URL of the eval master. Set to an appropiate value when running ' + 'on CPU/GPU.') diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/flags_to_params.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/flags_to_params.py new file mode 100644 index 000000000..348d90c54 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/flags_to_params.py @@ -0,0 +1,101 @@ +# Copyright 2018 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Functions to override model parameters from command-line flags.""" + +from absl import logging +from hyperparameters import params_dict + +ESSENTIAL_FLAGS = ['tpu', 'data_dir', 'model_dir'] + + +def override_params_from_input_flags(params, input_flags): + """Update params dictionary with input flags. + + Args: + params: ParamsDict object containing dictionary of model parameters. + input_flags: All the flags with non-null value of overridden model + parameters. + + Returns: + ParamsDict object containing dictionary of model parameters. + """ + if not isinstance(params, params_dict.ParamsDict): + raise ValueError( + 'The base parameter set must be a ParamsDict, was: {}'.format( + type(params))) + + essential_flag_dict = {} + for key in ESSENTIAL_FLAGS: + flag_value = input_flags.get_flag_value(key, None) + + if flag_value is None: + logging.warning('Flag %s is None.', key) + else: + essential_flag_dict[key] = flag_value + + params_dict.override_params_dict(params, + essential_flag_dict, + is_strict=False) + + normal_flag_dict = get_dictionary_from_flags(params.as_dict(), input_flags) + + params_dict.override_params_dict(params, + normal_flag_dict, + is_strict=False) + + return params + + +def get_dictionary_from_flags(params, input_flags): + """Generate dictionary from non-null flags. + + Args: + params: Python dictionary of model parameters. + input_flags: All the flags with non-null value of overridden model + parameters. + + Returns: + Python dict of overriding model parameters. + """ + if not isinstance(params, dict): + raise ValueError('The base parameter set must be a dict. ' + 'Was: {}'.format(type(params))) + flag_dict = {} + for k, v in params.items(): + if isinstance(v, dict): + d = get_dictionary_from_flags(v, input_flags) + flag_dict[k] = d + else: + try: + flag_value = input_flags.get_flag_value(k, None) + if flag_value is not None: + flag_dict[k] = flag_value + except AttributeError: + flag_dict[k] = v + + return flag_dict diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/params_dict.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/params_dict.py new file mode 100644 index 000000000..32e165828 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/hyperparameters/params_dict.py @@ -0,0 +1,441 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""A parameter dictionary class which supports the nest structure.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import copy +import re +import six +import tensorflow.compat.v1 as tf +import yaml + +# regex pattern that matches on key-value pairs in a comma-separated +# key-value pair string. It splits each k-v pair on the = sign, and +# matches on values that are within single quotes, double quotes, single +# values (e.g. floats, ints, etc.), and a lists within brackets. +_PARAM_RE = re.compile(r""" + (?P[a-zA-Z][\w\.]*) # variable name: "var" or "x" + \s*=\s* + ((?P\'(.*?)\' # single quote + | + \"(.*?)\" # double quote + | + [^,\[]* # single value + | + \[[^\]]*\])) # list of values + ($|,\s*)""", re.VERBOSE) + + +class ParamsDict(object): + """A hyperparameter container class.""" + + RESERVED_ATTR = ['_locked', '_restrictions'] + + def __init__(self, default_params=None, restrictions=None): + """Instantiate a ParamsDict. + + Instantiate a ParamsDict given a set of default parameters and a list of + restrictions. Upon initialization, it validates itself by checking all the + defined restrictions, and raise error if it finds inconsistency. + + Args: + default_params: a Python dict or another ParamsDict object including the + default parameters to initialize. + restrictions: a list of strings, which define a list of restrictions to + ensure the consistency of different parameters internally. Each + restriction string is defined as a binary relation with a set of + operators, including {'==', '!=', '<', '<=', '>', '>='}. + """ + self._locked = False + self._restrictions = [] + if restrictions: + self._restrictions = restrictions + if default_params is None: + default_params = {} + self.override(default_params, is_strict=False) + self.validate() + + def _set(self, k, v): + if isinstance(v, dict): + self.__dict__[k] = ParamsDict(v) + else: + self.__dict__[k] = copy.deepcopy(v) + + def __setattr__(self, k, v): + """Sets the value of the existing key. + + Note that this does not allow directly defining a new key. Use the + `override` method with `is_strict=False` instead. + + Args: + k: the key string. + v: the value to be used to set the key `k`. + + Raises: + KeyError: if k is not defined in the ParamsDict. + """ + if k not in ParamsDict.RESERVED_ATTR: + if k not in self.__dict__.keys(): + raise KeyError('The key `%{}` does not exist. ' + 'To extend the existing keys, use ' + '`override` with `is_strict` = True.'.format(k)) + if self._locked: + raise ValueError('The ParamsDict has been locked. ' + 'No change is allowed.') + self._set(k, v) + + def __getattr__(self, k): + """Gets the value of the existing key. + + Args: + k: the key string. + + Returns: + the value of the key. + + Raises: + KeyError: if k is not defined in the ParamsDict. + """ + if k == '__getstate__': + return self.as_dict + elif k == '__setstate__': + return self.__init__ + elif k not in self.__dict__.keys(): + raise KeyError('The key `{}` does not exist. '.format(k)) + return self.__dict__[k] + + def __delattr__(self, k): + """Deletes the key and removes its values. + + Args: + k: the key string. + + Raises: + KeyError: if k is not defined in the ParamsDict. + """ + if k in ParamsDict.RESERVED_ATTR: + raise KeyError('The key `{}` is reserved. ' + 'No change is allowes. '.format(k)) + if k not in self.__dict__.keys(): + raise KeyError('The key `{}` does not exist. '.format(k)) + if self._locked: + raise ValueError( + 'The ParamsDict has been locked. No change is allowed.') + del self.__dict__[k] + + def override(self, override_params, is_strict=True): + """Override the ParamsDict with a set of given params. + + Args: + override_params: a dict or a ParamsDict specifying the parameters to + be overridden. + is_strict: a boolean specifying whether override is strict or not. If + True, keys in `override_params` must be present in the ParamsDict. + If False, keys in `override_params` can be different from what is + currently defined in the ParamsDict. In this case, the ParamsDict will + be extended to include the new keys. + """ + if self._locked: + raise ValueError( + 'The ParamsDict has been locked. No change is allowed.') + if isinstance(override_params, ParamsDict): + override_params = override_params.as_dict() + self._override(override_params, + is_strict) # pylint: disable=protected-access + + def _override(self, override_dict, is_strict=True): + """The implementation of `override`.""" + for k, v in six.iteritems(override_dict): + if k in ParamsDict.RESERVED_ATTR: + raise KeyError('The key `%{}` is internally reserved. ' + 'Can not be overridden.') + if k not in self.__dict__.keys(): + if is_strict: + raise KeyError('The key `{}` does not exist. ' + 'To extend the existing keys, use ' + '`override` with `is_strict` = False.'.format(k)) + else: + self._set(k, v) + else: + if isinstance(v, dict): + self.__dict__[k]._override( + v, is_strict) # pylint: disable=protected-access + elif isinstance(v, ParamsDict): + self.__dict__[k]._override( + v.as_dict(), is_strict) # pylint: disable=protected-access + else: + self.__dict__[k] = copy.deepcopy(v) + + def lock(self): + """Makes the ParamsDict immutable.""" + self._locked = True + + def as_dict(self): + """Returns a dict representation of ParamsDict. + + For the nested ParamsDict, a nested dict will be returned. + """ + params_dict = {} + for k, v in six.iteritems(self.__dict__): + if k not in ParamsDict.RESERVED_ATTR: + if isinstance(v, ParamsDict): + params_dict[k] = v.as_dict() + else: + params_dict[k] = copy.deepcopy(v) + return params_dict + + def validate(self): + """Validate the parameters consistency based on the restrictions. + + This method validates the internal consistency using the pre-defined list of + restrictions. A restriction is defined as a string which specfiies a binary + operation. The supported binary operations are {'==', '!=', '<', '<=', '>', + '>='}. Note that the meaning of these operators are consistent with the + underlying Python immplementation. Users should make sure the define + restrictions on their type make sense. + + For example, for a ParamsDict like the following + ``` + a: + a1: 1 + a2: 2 + b: + bb: + bb1: 10 + bb2: 20 + ccc: + a1: 1 + a3: 3 + ``` + one can define two restrictions like this + ['a.a1 == b.ccc.a1', 'a.a2 <= b.bb.bb2'] + + What it enforces are: + - a.a1 = 1 == b.ccc.a1 = 2 + - a.a2 = 2 <= b.bb.bb2 = 20 + + Raises: + KeyError: if any of the following happens + (1) any of parameters in any of restrictions is not defined in + ParamsDict, + (2) any inconsistency violating the restriction is found. + ValueError: if the restriction defined in the string is not supported. + """ + def _get_kv(dotted_string, params_dict): + tokenized_params = dotted_string.split('.') + v = params_dict + for t in tokenized_params: + v = v[t] + return tokenized_params[-1], v + + def _get_kvs(tokens, params_dict): + if len(tokens) != 2: + raise ValueError( + 'Only support binary relation in restriction.') + stripped_tokens = [t.strip() for t in tokens] + left_k, left_v = _get_kv(stripped_tokens[0], params_dict) + right_k, right_v = _get_kv(stripped_tokens[1], params_dict) + return [left_k, left_v, right_k, right_v] + + params_dict = self.as_dict() + for restriction in self._restrictions: + if '==' in restriction: + tokens = restriction.split('==') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v != right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + elif '!=' in restriction: + tokens = restriction.split('!=') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v == right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + elif '<' in restriction: + tokens = restriction.split('<') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v >= right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + elif '<=' in restriction: + tokens = restriction.split('<=') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v > right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + elif '>' in restriction: + tokens = restriction.split('>') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v <= right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + elif '>=' in restriction: + tokens = restriction.split('>=') + _, left_v, _, right_v = _get_kvs(tokens, params_dict) + if left_v < right_v: + raise KeyError('Found inconsistncy between key `{}` and key `{}`.' + .format(tokens[0], tokens[1])) + else: + raise ValueError('Unsupported relation in restriction.') + + +def read_yaml_to_params_dict(file_path): + """Reads a YAML file to a ParamsDict.""" + with tf.gfile.Open(file_path, 'r') as f: + params_dict = yaml.load(f) + return ParamsDict(params_dict) + + +def save_params_dict_to_yaml(params, file_path): + """Saves the input ParamsDict to a YAML file.""" + with tf.gfile.Open(file_path, 'w') as f: + def _my_list_rep(dumper, data): + # u'tag:yaml.org,2002:seq' is the YAML internal tag for sequence. + return dumper.represent_sequence( + u'tag:yaml.org,2002:seq', data, flow_style=True) + yaml.add_representer(list, _my_list_rep) + yaml.dump(params.as_dict(), f, default_flow_style=False) + + +def nested_csv_str_to_json_str(csv_str): + """Converts a nested (using '.') comma-separated k=v string to a JSON string. + + Converts a comma-separated string of key/value pairs that supports + nesting of keys to a JSON string. Nesting is implemented using + '.' between levels for a given key. + + Spacing between commas and = is supported (e.g. there is no difference between + "a=1,b=2", "a = 1, b = 2", or "a=1, b=2") but there should be no spaces before + keys or after values (e.g. " a=1,b=2" and "a=1,b=2 " are not supported). + + Note that this will only support values supported by CSV, meaning + values such as nested lists (e.g. "a=[[1,2,3],[4,5,6]]") are not + supported. Strings are supported as well, e.g. "a='hello'". + + An example conversion would be: + + "a=1, b=2, c.a=2, c.b=3, d.a.a=5" + + to + + "{ a: 1, b : 2, c: {a : 2, b : 3}, d: {a: {a : 5}}}" + + Args: + csv_str: the comma separated string. + + Returns: + the converted JSON string. + + Raises: + ValueError: If csv_str is not in a comma separated string or + if the string is formatted incorrectly. + """ + if not csv_str: + return '' + + formatted_entries = [] + nested_map = collections.defaultdict(list) + pos = 0 + while pos < len(csv_str): + m = _PARAM_RE.match(csv_str, pos) + if not m: + raise ValueError('Malformed hyperparameter value while parsing ' + 'CSV string: %s' % csv_str[pos:]) + pos = m.end() + # Parse the values. + m_dict = m.groupdict() + name = m_dict['name'] + v = m_dict['val'] + + # If a GCS path (e.g. gs://...) is provided, wrap this in quotes + # as yaml.load would otherwise throw an exception + if re.match(r'(?=[^\"\'])(?=[gs://])', v): + v = '\'{}\''.format(v) + + name_nested = name.split('.') + if len(name_nested) > 1: + grouping = name_nested[0] + value = '.'.join(name_nested[1:]) + '=' + v + nested_map[grouping].append(value) + else: + formatted_entries.append('%s : %s' % (name, v)) + + for grouping, value in nested_map.items(): + value = ','.join(value) + value = nested_csv_str_to_json_str(value) + formatted_entries.append('%s : %s' % (grouping, value)) + return '{' + ', '.join(formatted_entries) + '}' + + +def override_params_dict(params, dict_or_string_or_yaml_file, is_strict): + """Override a given ParamsDict using a dict, JSON/YAML/CSV string or YAML file. + + The logic of the function is outlined below: + 1. Test that the input is a dict. If not, proceed to 2. + 2. Tests that the input is a string. If not, raise unknown ValueError + 2.1. Test if the string is in a CSV format. If so, parse. + If not, proceed to 2.2. + 2.2. Try loading the string as a YAML/JSON. If successful, parse to + dict and use it to override. If not, proceed to 2.3. + 2.3. Try using the string as a file path and load the YAML file. + + Args: + params: a ParamsDict object to be overridden. + dict_or_string_or_yaml_file: a Python dict, JSON/YAML/CSV string or + path to a YAML file specifying the parameters to be overridden. + is_strict: a boolean specifying whether override is strict or not. + + Returns: + params: the overridden ParamsDict object. + + Raises: + ValueError: if failed to override the parameters. + """ + if not dict_or_string_or_yaml_file: + return params + if isinstance(dict_or_string_or_yaml_file, dict): + params.override(dict_or_string_or_yaml_file, is_strict) + elif isinstance(dict_or_string_or_yaml_file, six.string_types): + try: + dict_or_string_or_yaml_file = ( + nested_csv_str_to_json_str(dict_or_string_or_yaml_file)) + except ValueError: + pass + params_dict = yaml.load(dict_or_string_or_yaml_file) + if isinstance(params_dict, dict): + params.override(params_dict, is_strict) + else: + with tf.gfile.Open(dict_or_string_or_yaml_file) as f: + params.override(yaml.load(f), is_strict) + else: + raise ValueError('Unknown input type to parse.') + return params diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/__init__.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/__init__.py new file mode 100644 index 000000000..d02ecafe1 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 initial file of mixnet.""" diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/custom_layers.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/custom_layers.py new file mode 100644 index 000000000..a74f4931a --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/custom_layers.py @@ -0,0 +1,153 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Custom layers.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow.compat.v1 as tf + + +def _get_conv2d(filters, kernel_size, use_keras, **kwargs): + """A helper function to create Conv2D layer.""" + if use_keras: + return tf.keras.layers.Conv2D( + filters=filters, kernel_size=kernel_size, **kwargs) + else: + return tf.layers.Conv2D(filters=filters, kernel_size=kernel_size, **kwargs) + + +def _split_channels(total_filters, num_groups): + split = [total_filters // num_groups for _ in range(num_groups)] + split[0] += total_filters - sum(split) + return split + + +def _get_shape_value(maybe_v2_shape): + if maybe_v2_shape is None: + return None + elif isinstance(maybe_v2_shape, int): + return maybe_v2_shape + else: + return maybe_v2_shape.value + + +class GroupedConv2D(object): + """Groupped convolution. + + Currently tf.keras and tf.layers don't support group convolution, so here we + use split/concat to implement this op. It reuses kernel_size for group + definition, where len(kernel_size) is number of groups. Notably, it allows + different group has different kernel size. + """ + + def __init__(self, filters, kernel_size, use_keras, **kwargs): + """Initialize the layer. + + Args: + filters: Integer, the dimensionality of the output space. + kernel_size: An integer or a list. If it is a single integer, then it is + same as the original Conv2D. If it is a list, then we split the channels + and perform different kernel for each group. + use_keras: An boolean value, whether to use keras layer. + **kwargs: other parameters passed to the original conv2d layer. + """ + self._groups = len(kernel_size) + self._channel_axis = -1 + + self._convs = [] + splits = _split_channels(filters, self._groups) + for i in range(self._groups): + self._convs.append( + _get_conv2d(splits[i], kernel_size[i], use_keras, **kwargs)) + + def __call__(self, inputs): + if len(self._convs) == 1: + return self._convs[0](inputs) + + filters = _get_shape_value(inputs.shape[self._channel_axis]) + splits = _split_channels(filters, len(self._convs)) + x_splits = tf.split(inputs, splits, self._channel_axis) + x_outputs = [c(x) for x, c in zip(x_splits, self._convs)] + x = tf.concat(x_outputs, self._channel_axis) + return x + + +class MixConv(object): + """MixConv with mixed depthwise convolutional kernels. + + MDConv is an improved depthwise convolution that mixes multiple kernels (e.g. + 3x3, 5x5, etc). Right now, we use an naive implementation that split channels + into multiple groups and perform different kernels for each group. + + See Mixnet paper for more details. + """ + + def __init__(self, kernel_size, strides, dilated=False, **kwargs): + """Initialize the layer. + + Most of args are the same as tf.keras.layers.DepthwiseConv2D except it has + an extra parameter "dilated" to indicate whether to use dilated conv to + simulate large kernel size. If dilated=True, then dilation_rate is ignored. + + Args: + kernel_size: An integer or a list. If it is a single integer, then it is + same as the original tf.keras.layers.DepthwiseConv2D. If it is a list, + then we split the channels and perform different kernel for each group. + strides: An integer or tuple/list of 2 integers, specifying the strides of + the convolution along the height and width. + dilated: Bool. indicate whether to use dilated conv to simulate large + kernel size. + **kwargs: other parameters passed to the original depthwise_conv layer. + """ + self._channel_axis = -1 + self._dilated = dilated + + self._convs = [] + for s in kernel_size: + d = 1 + if strides[0] == 1 and self._dilated: + # Only apply dilated conv for stride 1 if needed. + d, s = (s - 1) // 2, 3 + tf.logging.info( + 'Use dilated conv with dilation rate = {}'.format(d)) + self._convs.append( + tf.keras.layers.DepthwiseConv2D( + s, strides=strides, dilation_rate=d, **kwargs)) + + def __call__(self, inputs): + if len(self._convs) == 1: + return self._convs[0](inputs) + + filters = _get_shape_value(inputs.shape[self._channel_axis]) + splits = _split_channels(filters, len(self._convs)) + x_splits = tf.split(inputs, splits, self._channel_axis) + x_outputs = [c(x) for x, c in zip(x_splits, self._convs)] + x = tf.concat(x_outputs, self._channel_axis) + return x diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/g3doc/mixnet-flops.png b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/g3doc/mixnet-flops.png new file mode 100644 index 0000000000000000000000000000000000000000..885fc69c8d3ebf883329ec7c65d7f02411bb9570 GIT binary patch literal 189229 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≪bOF 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ÐCc>c8(VFU~d#SNoaUw>Rm>wd|5 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 2: + key, value = splits[:2] + options[key] = value + + if 's' not in options or len(options['s']) != 2: + raise ValueError('Strides options should be a pair of integers.') + + def _parse_ksize(ss): + return [int(k) for k in ss.split('.')] + + return mixnet_model.BlockArgs( + expand_ksize=_parse_ksize(options['a']), + dw_ksize=_parse_ksize(options['k']), + project_ksize=_parse_ksize(options['p']), + num_repeat=int(options['r']), + input_filters=int(options['i']), + output_filters=int(options['o']), + expand_ratio=int(options['e']), + id_skip=('noskip' not in block_string), + se_ratio=float(options['se']) if 'se' in options else None, + strides=[int(options['s'][0]), int(options['s'][1])], + swish=('sw' in block_string), + dilated=('dilated' in block_string)) + + def _encode_block_string(self, block): + """Encodes a Mixnet block to a string.""" + def _encode_ksize(arr): + return '.'.join([str(k) for k in arr]) + + args = [ + 'r%d' % block.num_repeat, + 'k%s' % _encode_ksize(block.dw_ksize), + 'a%s' % _encode_ksize(block.expand_ksize), + 'p%s' % _encode_ksize(block.project_ksize), + 's%d%d' % (block.strides[0], block.strides[1]), + 'e%s' % block.expand_ratio, + 'i%d' % block.input_filters, + 'o%d' % block.output_filters + ] + if (block.se_ratio is not None and block.se_ratio > 0 and + block.se_ratio <= 1): + args.append('se%s' % block.se_ratio) + if block.id_skip is False: # pylint: disable=g-bool-id-comparison + args.append('noskip') + if block.swish: + args.append('sw') + if block.dilated: + args.append('dilated') + return '_'.join(args) + + def decode(self, string_list): + """Decodes a list of string notations to specify blocks inside the network. + + Args: + string_list: a list of strings, each string is a notation of Mixnet + block.build_model_base + + Returns: + A list of namedtuples to represent Mixnet blocks arguments. + """ + assert isinstance(string_list, list) + blocks_args = [] + for block_string in string_list: + blocks_args.append(self._decode_block_string(block_string)) + return blocks_args + + def encode(self, blocks_args): + """Encodes a list of Mixnet Blocks to a list of strings. + + Args: + blocks_args: A list of namedtuples to represent Mixnet blocks arguments. + Returns: + a list of strings, each string is a notation of Mixnet block. + """ + block_strings = [] + for block in blocks_args: + block_strings.append(self._encode_block_string(block)) + return block_strings + + +def mixnet_s(depth_multiplier=None): + """Creates mixnet-s model. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal Mixnet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_a1_p1_s11_e1_i16_o16', + 'r1_k3_a1.1_p1.1_s22_e6_i16_o24', + 'r1_k3_a1.1_p1.1_s11_e3_i24_o24', + + 'r1_k3.5.7_a1_p1_s22_e6_i24_o40_se0.5_sw', + 'r3_k3.5_a1.1_p1.1_s11_e6_i40_o40_se0.5_sw', + + 'r1_k3.5.7_a1_p1.1_s22_e6_i40_o80_se0.25_sw', + 'r2_k3.5_a1_p1.1_s11_e6_i80_o80_se0.25_sw', + + 'r1_k3.5.7_a1.1_p1.1_s11_e6_i80_o120_se0.5_sw', + 'r2_k3.5.7.9_a1.1_p1.1_s11_e3_i120_o120_se0.5_sw', + + 'r1_k3.5.7.9.11_a1_p1_s22_e6_i120_o200_se0.5_sw', + 'r2_k3.5.7.9_a1_p1.1_s11_e6_i200_o200_se0.5_sw', + ] + global_params = mixnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.2, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=16, + use_keras=True, + feature_size=1536) + decoder = MixnetDecoder() + return decoder.decode(blocks_args), global_params + + +def mixnet_m(depth_multiplier=None): + """Creates a mixnet-m model. + + Args: + depth_multiplier: multiplier to number of filters per layer. + + Returns: + blocks_args: a list of BlocksArgs for internal Mixnet blocks. + global_params: GlobalParams, global parameters for the model. + """ + blocks_args = [ + 'r1_k3_a1_p1_s11_e1_i24_o24', + 'r1_k3.5.7_a1.1_p1.1_s22_e6_i24_o32', + 'r1_k3_a1.1_p1.1_s11_e3_i32_o32', + + 'r1_k3.5.7.9_a1_p1_s22_e6_i32_o40_se0.5_sw', + 'r3_k3.5_a1.1_p1.1_s11_e6_i40_o40_se0.5_sw', + + 'r1_k3.5.7_a1_p1_s22_e6_i40_o80_se0.25_sw', + 'r3_k3.5.7.9_a1.1_p1.1_s11_e6_i80_o80_se0.25_sw', + + 'r1_k3_a1_p1_s11_e6_i80_o120_se0.5_sw', + 'r3_k3.5.7.9_a1.1_p1.1_s11_e3_i120_o120_se0.5_sw', + + 'r1_k3.5.7.9_a1_p1_s22_e6_i120_o200_se0.5_sw', + 'r3_k3.5.7.9_a1_p1.1_s11_e6_i200_o200_se0.5_sw', + ] + global_params = mixnet_model.GlobalParams( + batch_norm_momentum=0.99, + batch_norm_epsilon=1e-3, + dropout_rate=0.25, + data_format='channels_last', + num_classes=1000, + depth_multiplier=depth_multiplier, + depth_divisor=8, + min_depth=None, + stem_size=24, + use_keras=True, + feature_size=1536) + decoder = MixnetDecoder() + return decoder.decode(blocks_args), global_params + + +def mixnet_l(depth_multiplier=None): + d = 1.3 * depth_multiplier if depth_multiplier else 1.3 + return mixnet_m(d) + + +def get_model_params(model_name, override_params): + """Get the block args and global params for a given model.""" + if model_name == 'mixnet-s': + blocks_args, global_params = mixnet_s() + elif model_name == 'mixnet-m': + blocks_args, global_params = mixnet_m() + elif model_name == 'mixnet-l': + blocks_args, global_params = mixnet_l() + else: + raise NotImplementedError( + 'model name is not pre-defined: %s' % model_name) + + if override_params: + # ValueError will be raised here if override_params has fields not included + # in global_params. + global_params = global_params._replace(**override_params) + return blocks_args, global_params + + +def build_model(images, model_name, training, override_params=None): + """A helper functiion to create a Mixnet model and return predicted logits. + + Args: + images: input images tensor. + model_name: string, the model name of a pre-defined Mixnet. + training: boolean, whether the model is constructed for training. + override_params: A dictionary of params for overriding. Fields must exist in + mixnet_model.GlobalParams. + + Returns: + logits: the logits tensor of classes. + endpoints: the endpoints for each layer. + Raises: + When model_name specified an undefined model, raises NotImplementedError. + When override_params has invalid fields, raises ValueError. + """ + assert isinstance(images, tf.Tensor) + blocks_args, global_params = get_model_params(model_name, override_params) + tf.logging.info('blocks_args= {}'.format(blocks_args)) + tf.logging.info('global_params= {}'.format(global_params)) + with tf.variable_scope(model_name): + model = mixnet_model.MixnetModel(blocks_args, global_params) + logits = model(images, training=training) + + logits = tf.identity(logits, 'logits') + return logits, model.endpoints + + +def build_model_base(images, model_name, training, override_params=None): + """A helper functiion to create a Mixnet base model and return global_pool. + + Args: + images: input images tensor. + model_name: string, the model name of a pre-defined Mixnet. + training: boolean, whether the model is constructed for training. + override_params: A dictionary of params for overriding. Fields must exist in + mixnet_model.GlobalParams. + + Returns: + features: global pool features. + endpoints: the endpoints for each layer. + Raises: + When model_name specified an undefined model, raises NotImplementedError. + When override_params has invalid fields, raises ValueError. + """ + assert isinstance(images, tf.Tensor) + blocks_args, global_params = get_model_params(model_name, override_params) + + with tf.variable_scope(model_name): + model = mixnet_model.MixnetModel(blocks_args, global_params) + features = model(images, training=training, features_only=True) + + features = tf.identity(features, 'global_pool') + return features, model.endpoints diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/mixnet_model.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/mixnet_model.py new file mode 100644 index 000000000..830039bc3 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/mixnet/mixnet_model.py @@ -0,0 +1,452 @@ +# Copyright 2019 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. +# ============================================================================== +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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. +"""Contains definitions for mixnet model (branched from MnasNet). + +[1] Mingxing Tan, Quoc V. Le + MixNet: Mixed Depthwise Convolutional Kernels. + BMVC 2019. https://arxiv.org/abs/1907.09595 +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import numpy as np +import six +from six.moves import xrange # pylint: disable=redefined-builtin +import tensorflow.compat.v1 as tf + +from mixnet import custom_layers + + +GlobalParams = collections.namedtuple('GlobalParams', [ + 'batch_norm_momentum', 'batch_norm_epsilon', 'dropout_rate', 'data_format', + 'num_classes', 'depth_multiplier', 'depth_divisor', 'min_depth', + 'use_keras', 'stem_size', 'feature_size', +]) +GlobalParams.__new__.__defaults__ = (None,) * len(GlobalParams._fields) + +BlockArgs = collections.namedtuple('BlockArgs', [ + 'dw_ksize', 'expand_ksize', 'project_ksize', 'num_repeat', 'input_filters', + 'output_filters', 'expand_ratio', 'id_skip', 'strides', 'se_ratio', + 'swish', 'dilated', +]) +# defaults will be a public argument for namedtuple in Python 3.7 +# https://docs.python.org/3/library/collections.html#collections.namedtuple +BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) + + +def conv_kernel_initializer(shape, dtype=None, partition_info=None): + """Initialization for convolutional kernels. + + The main difference with tf.variance_scaling_initializer is that + tf.variance_scaling_initializer uses a truncated normal with an uncorrected + standard deviation, whereas here we use a normal distribution. Similarly, + tf.contrib.layers.variance_scaling_initializer uses a truncated normal with + a corrected standard deviation. + + Args: + shape: shape of variable + dtype: dtype of variable + partition_info: unused + + Returns: + an initialization for the variable + """ + del partition_info + kernel_height, kernel_width, _, out_filters = shape + fan_out = int(kernel_height * kernel_width * out_filters) + return tf.random_normal( + shape, mean=0.0, stddev=np.sqrt(2.0 / fan_out), dtype=dtype) + + +def dense_kernel_initializer(shape, dtype=None, partition_info=None): + """Initialization for dense kernels. + + This initialization is equal to + tf.variance_scaling_initializer(scale=1.0/3.0, mode='fan_out', + distribution='uniform'). + It is written out explicitly here for clarity. + + Args: + shape: shape of variable + dtype: dtype of variable + partition_info: unused + + Returns: + an initialization for the variable + """ + del partition_info + init_range = 1.0 / np.sqrt(shape[1]) + return tf.random_uniform(shape, -init_range, init_range, dtype=dtype) + + +def round_filters(filters, global_params): + """Round number of filters based on depth multiplier.""" + multiplier = global_params.depth_multiplier + divisor = global_params.depth_divisor + min_depth = global_params.min_depth + if not multiplier: + return filters + + filters *= multiplier + min_depth = min_depth or divisor + new_filters = max(min_depth, int( + filters + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_filters < 0.9 * filters: + new_filters += divisor + return new_filters + + +class MixnetBlock(object): + """A class of Mixnet block. + + Attributes: + endpoints: dict. A list of internal tensors. + """ + + def __init__(self, block_args, global_params): + """Initializes the block. + + Args: + block_args: BlockArgs, arguments to create a MixnetBlock. + global_params: GlobalParams, a set of global parameters. + """ + self._block_args = block_args + self._batch_norm_momentum = global_params.batch_norm_momentum + self._batch_norm_epsilon = global_params.batch_norm_epsilon + self._use_keras = global_params.use_keras + self._data_format = global_params.data_format + if self._data_format == 'channels_first': + self._channel_axis = 1 + self._spatial_dims = [2, 3] + else: + self._channel_axis = -1 + self._spatial_dims = [1, 2] + self._has_se = (self._block_args.se_ratio is not None) and ( + self._block_args.se_ratio > 0) and (self._block_args.se_ratio <= 1) + self._relu_fn = tf.nn.swish if self._block_args.swish else tf.nn.relu + + self.endpoints = None + + # Builds the block accordings to arguments. + self._build() + + def block_args(self): + return self._block_args + + def _build(self): + """Builds block according to the arguments.""" + filters = self._block_args.input_filters * self._block_args.expand_ratio + if self._block_args.expand_ratio != 1: + # Expansion phase: + self._expand_conv = custom_layers.GroupedConv2D( + filters=filters, + kernel_size=self._block_args.expand_ksize, + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + use_keras=self._use_keras) + self._bn0 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + kernel_size = self._block_args.dw_ksize + # Depth-wise convolution phase: + self._depthwise_conv = custom_layers.MixConv( + kernel_size, + strides=self._block_args.strides, + depthwise_initializer=conv_kernel_initializer, + padding='same', + data_format=self._data_format, + use_bias=False, + dilated=self._block_args.dilated) + self._bn1 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + if self._has_se: + num_reduced_filters = max( + 1, int(self._block_args.input_filters * self._block_args.se_ratio)) + # Squeeze and Excitation layer. + self._se_reduce = custom_layers.GroupedConv2D( + num_reduced_filters, + kernel_size=[1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=True, + data_format=self._data_format, + use_keras=self._use_keras) + self._se_expand = custom_layers.GroupedConv2D( + filters, + kernel_size=[1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=True, + data_format=self._data_format, + use_keras=self._use_keras) + + # Output phase: + filters = self._block_args.output_filters + self._project_conv = custom_layers.GroupedConv2D( + filters, + kernel_size=self._block_args.project_ksize, + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + data_format=self._data_format, + use_keras=self._use_keras) + self._bn2 = tf.layers.BatchNormalization( + axis=self._channel_axis, + momentum=self._batch_norm_momentum, + epsilon=self._batch_norm_epsilon, + fused=True) + + def _call_se(self, input_tensor): + """Call Squeeze and Excitation layer. + + Args: + input_tensor: Tensor, a single input tensor for Squeeze/Excitation layer. + + Returns: + A output tensor, which should have the same shape as input. + """ + se_tensor = tf.reduce_mean( + input_tensor, self._spatial_dims, keepdims=True) + se_tensor = self._se_expand(self._relu_fn(self._se_reduce(se_tensor))) + tf.logging.info('Built Squeeze and Excitation with tensor shape: %s' % + (se_tensor.shape)) + return tf.sigmoid(se_tensor) * input_tensor + + def call(self, inputs, training=True): + """Implementation of MixnetBlock call(). + + Args: + inputs: the inputs tensor. + training: boolean, whether the model is constructed for training. + + Returns: + A output tensor. + """ + tf.logging.info('Block input: %s shape: %s' % + (inputs.name, inputs.shape)) + if self._block_args.expand_ratio != 1: + x = self._relu_fn( + self._bn0(self._expand_conv(inputs), training=training)) + else: + x = inputs + tf.logging.info('Expand: %s shape: %s' % (x.name, x.shape)) + + x = self._relu_fn( + self._bn1(self._depthwise_conv(x), training=training)) + tf.logging.info('DWConv: %s shape: %s' % (x.name, x.shape)) + + if self._has_se: + with tf.variable_scope('se'): + x = self._call_se(x) + + self.endpoints = {'expansion_output': x} + + x = self._bn2(self._project_conv(x), training=training) + if self._block_args.id_skip: + if all( + s == 1 for s in self._block_args.strides + ) and self._block_args.input_filters == self._block_args.output_filters: + x = tf.add(x, inputs) + tf.logging.info('Project: %s shape: %s' % (x.name, x.shape)) + return tf.identity(x) + + +class MixnetModel(tf.keras.Model): + """A class implements tf.keras.Model for mixnet model. + + Reference: https://arxiv.org/abs/1807.11626 + """ + + def __init__(self, blocks_args=None, global_params=None): + """Initializes an `MixnetModel` instance. + + Args: + blocks_args: A list of BlockArgs to construct Mixnet block modules. + global_params: GlobalParams, a set of global parameters. + + Raises: + ValueError: when blocks_args is not specified as a list. + """ + super(MixnetModel, self).__init__() + if not isinstance(blocks_args, list): + raise ValueError('blocks_args should be a list.') + self._global_params = global_params + self._blocks_args = blocks_args + # Use relu in default for head and stem. + self._relu_fn = tf.nn.relu + self.endpoints = None + self._build() + + def _build(self): + """Builds a Mixnet model.""" + self._blocks = [] + # Builds blocks. + for block_args in self._blocks_args: + assert block_args.num_repeat > 0 + # Update block input and output filters based on depth multiplier. + block_args = block_args._replace( + input_filters=round_filters(block_args.input_filters, + self._global_params), + output_filters=round_filters(block_args.output_filters, + self._global_params)) + + # The first block needs to take care of stride and filter size increase. + self._blocks.append(MixnetBlock(block_args, self._global_params)) + if block_args.num_repeat > 1: + # pylint: disable=protected-access + block_args = block_args._replace( + input_filters=block_args.output_filters, strides=[1, 1]) + # pylint: enable=protected-access + for _ in xrange(block_args.num_repeat - 1): + self._blocks.append(MixnetBlock( + block_args, self._global_params)) + + batch_norm_momentum = self._global_params.batch_norm_momentum + batch_norm_epsilon = self._global_params.batch_norm_epsilon + if self._global_params.data_format == 'channels_first': + channel_axis = 1 + else: + channel_axis = -1 + + # Stem part. + stem_size = self._global_params.stem_size + self._conv_stem = custom_layers.GroupedConv2D( + filters=round_filters(stem_size, self._global_params), + kernel_size=[3], + strides=[2, 2], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + use_keras=self._global_params.use_keras) + self._bn0 = tf.layers.BatchNormalization( + axis=channel_axis, + momentum=batch_norm_momentum, + epsilon=batch_norm_epsilon, + fused=True) + + # Head part. + self._conv_head = custom_layers.GroupedConv2D( + filters=self._global_params.feature_size, + kernel_size=[1], + strides=[1, 1], + kernel_initializer=conv_kernel_initializer, + padding='same', + use_bias=False, + use_keras=self._global_params.use_keras) + self._bn1 = tf.layers.BatchNormalization( + axis=channel_axis, + momentum=batch_norm_momentum, + epsilon=batch_norm_epsilon, + fused=True) + + self._avg_pooling = tf.keras.layers.GlobalAveragePooling2D( + data_format=self._global_params.data_format) + if self._global_params.use_keras: + self._fc = tf.keras.layers.Dense( + self._global_params.num_classes, + kernel_initializer=dense_kernel_initializer) + else: + self._fc = tf.layers.Dense( + self._global_params.num_classes, + kernel_initializer=dense_kernel_initializer) + if self._global_params.dropout_rate > 0: + self._dropout = tf.keras.layers.Dropout( + self._global_params.dropout_rate) + else: + self._dropout = None + + def call(self, inputs, training=True, features_only=None): + """Implementation of MixnetModel call(). + + Args: + inputs: input tensors. + training: boolean, whether the model is constructed for training. + features_only: build the base feature network only. + + Returns: + output tensors. + """ + outputs = None + self.endpoints = {} + # Calls Stem layers + with tf.variable_scope('stem'): + outputs = self._relu_fn( + self._bn0(self._conv_stem(inputs), training=training)) + tf.logging.info('Built stem layers with output shape: %s' % + outputs.shape) + self.endpoints['stem'] = outputs + + # Calls blocks. + reduction_idx = 0 + for idx, block in enumerate(self._blocks): + is_reduction = False + if ((idx == len(self._blocks) - 1) or + self._blocks[idx + 1].block_args().strides[0] > 1): + is_reduction = True + reduction_idx += 1 + + with tf.variable_scope('blocks_%s' % idx): + outputs = block.call(outputs, training=training) + self.endpoints['block_%s' % idx] = outputs + if is_reduction: + self.endpoints['reduction_%s' % reduction_idx] = outputs + if block.endpoints: + for k, v in six.iteritems(block.endpoints): + self.endpoints['block_%s/%s' % (idx, k)] = v + if is_reduction: + self.endpoints['reduction_%s/%s' % + (reduction_idx, k)] = v + self.endpoints['global_pool'] = outputs + + if not features_only: + # Calls final layers and returns logits. + with tf.variable_scope('head'): + outputs = self._relu_fn( + self._bn1(self._conv_head(outputs), training=training)) + outputs = self._avg_pooling(outputs) + if self._dropout: + outputs = self._dropout(outputs, training=training) + outputs = self._fc(outputs) + self.endpoints['head'] = outputs + return outputs -- Gitee From 727dd4c9cb931836ef17011acb5fdd586692c80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=81=92=E6=AD=A3?= <1771467123@qq.com> Date: Tue, 31 May 2022 06:10:12 +0000 Subject: [PATCH 3/4] update TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt. --- .../cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt index 06fa8a2ed..83f8226cf 100644 --- a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelzoo_level.txt @@ -1,7 +1,7 @@ GPUStatus:OK -NPUMigrationStatus:POK +NPUMigrationStatus:OK FuncStatus:OK PrecisionStatus:OK -AutoTune:POK -PerfStatus:POK \ No newline at end of file +AutoTune:OK +PerfStatus:OK \ No newline at end of file -- Gitee From cbaa0d62cb00fdc947922a1551ba5748dd60017e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=81=92=E6=AD=A3?= <1771467123@qq.com> Date: Tue, 31 May 2022 11:05:23 +0000 Subject: [PATCH 4/4] First commit Mixnet --- .../modelarts_entry_acc.py | 63 ++++++ .../modelarts_entry_perf.py | 63 ++++++ .../test/train_full_1p.sh | 185 +++++++++++++++++ .../test/train_performance_1p.sh | 186 ++++++++++++++++++ 4 files changed, 497 insertions(+) create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_acc.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_perf.py create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_full_1p.sh create mode 100644 TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_performance_1p.sh diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_acc.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_acc.py new file mode 100644 index 000000000..13077b10e --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_acc.py @@ -0,0 +1,63 @@ +# Copyright 2017 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. +# ============================================================================ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 sys + +# 解析输入参数data_url +parser = argparse.ArgumentParser() +parser.add_argument("--data_url", type=str, default="/home/ma-user/modelarts/inputs/data_url_0") +parser.add_argument("--train_url", type=str, default="/home/ma-user/modelarts/outputs/train_url_0/") +config = parser.parse_args() + +print("[CANN-Modelzoo] code_dir path is [%s]" % (sys.path[0])) +code_dir = sys.path[0] +os.chdir(code_dir) +print("[CANN-Modelzoo] work_dir path is [%s]" % (os.getcwd())) + +print("[CANN-Modelzoo] before train - list my run files:") +os.system("ls -al /usr/local/Ascend/ascend-toolkit/") + +print("[CANN-Modelzoo] before train - list my dataset files:") +os.system("ls -al %s" % config.data_url) + +print("[CANN-Modelzoo] start run train shell") +# 设置sh文件格式为linux可执行 +os.system("dos2unix ./test/*") + +# 执行train_full_1p.sh或者train_performance_1p.sh,需要用户自己指定 +# full和performance的差异,performance只需要执行很少的step,控制在15分钟以内,主要关注性能FPS +os.system("bash ./test/train_full_1p.sh --data_path=%s --output_path=%s " % (config.data_url, config.train_url)) + +print("[CANN-Modelzoo] finish run train shell") + +# 将当前执行目录所有文件拷贝到obs的output进行备份 +print("[CANN-Modelzoo] after train - list my output files:") +os.system("cp -r %s %s " % (code_dir, config.train_url)) +os.system("ls -al %s" % config.train_url) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_perf.py b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_perf.py new file mode 100644 index 000000000..14384e227 --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/modelarts_entry_perf.py @@ -0,0 +1,63 @@ +# Copyright 2017 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. +# ============================================================================ +# Copyright 2021 Huawei Technologies Co., Ltd +# +# 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 sys + +# 解析输入参数data_url +parser = argparse.ArgumentParser() +parser.add_argument("--data_url", type=str, default="/home/ma-user/modelarts/inputs/data_url_0") +parser.add_argument("--train_url", type=str, default="/home/ma-user/modelarts/outputs/train_url_0/") +config = parser.parse_args() + +print("[CANN-Modelzoo] code_dir path is [%s]" % (sys.path[0])) +code_dir = sys.path[0] +os.chdir(code_dir) +print("[CANN-Modelzoo] work_dir path is [%s]" % (os.getcwd())) + +print("[CANN-Modelzoo] before train - list my run files:") +os.system("ls -al /usr/local/Ascend/ascend-toolkit/") + +print("[CANN-Modelzoo] before train - list my dataset files:") +os.system("ls -al %s" % config.data_url) + +print("[CANN-Modelzoo] start run train shell") +# 设置sh文件格式为linux可执行 +os.system("dos2unix ./test/*") + +# 执行train_full_1p.sh或者train_performance_1p.sh,需要用户自己指定 +# full和performance的差异,performance只需要执行很少的step,控制在15分钟以内,主要关注性能FPS +os.system("bash ./test/train_performance_1p.sh --data_path=%s --output_path=%s " % (config.data_url, config.train_url)) + +print("[CANN-Modelzoo] finish run train shell") + +# 将当前执行目录所有文件拷贝到obs的output进行备份 +print("[CANN-Modelzoo] after train - list my output files:") +os.system("cp -r %s %s " % (code_dir, config.train_url)) +os.system("ls -al %s" % config.train_url) diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_full_1p.sh b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_full_1p.sh new file mode 100644 index 000000000..418c62acc --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_full_1p.sh @@ -0,0 +1,185 @@ +#!/bin/bash + +########################################################## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +########################################################## +# shell脚本所在路径 +cur_path=`echo $(cd $(dirname $0);pwd)` + +# 判断当前shell是否是performance +perf_flag=`echo $0 | grep performance | wc -l` + +# 当前执行网络的名称 +Network=`echo $(cd $(dirname $0);pwd) | awk -F"/" '{print $(NF-1)}'` + +export RANK_SIZE=1 +export RANK_ID=0 +export JOB_ID=10087 + +# 路径参数初始化 +data_path="" +output_path="" + +# 帮助信息,不需要修改 +if [[ $1 == --help || $1 == -h ]];then + echo"usage:./train_performance_1P.sh " + echo " " + echo "parameter explain: + --data_path # dataset of training + --output_path # output of training + --train_steps # max_step for training + --train_epochs # max_epoch for training + --batch_size # batch size + -h/--help show help message + " + exit 1 +fi + +# 参数校验,不需要修改 +for para in $* +do + if [[ $para == --data_path* ]];then + data_path=`echo ${para#*=}` + elif [[ $para == --output_path* ]];then + output_path=`echo ${para#*=}` + elif [[ $para == --train_steps* ]];then + train_steps=`echo ${para#*=}` + elif [[ $para == --train_epochs* ]];then + train_epochs=`echo ${para#*=}` + elif [[ $para == --batch_size* ]];then + batch_size=`echo ${para#*=}` + fi +done + +# 校验是否传入data_path,不需要修改 +if [[ $data_path == "" ]];then + echo "[Error] para \"data_path\" must be config" + exit 1 +fi + +# 校验是否传入output_path,不需要修改 +if [[ $output_path == "" ]];then + output_path="./test/output/${ASCEND_DEVICE_ID}" +fi + +# 设置打屏日志文件名,请保留,文件名为${print_log} +print_log="./test/output/${ASCEND_DEVICE_ID}/train_${ASCEND_DEVICE_ID}.log" +modelarts_flag=`cat /etc/passwd |grep ma-user` +if [ x"${modelarts_flag}" != x ]; +then + echo "running with modelarts_flag..." + print_log_name=`ls /home/ma-user/modelarts/log/ | grep proc-rank` + print_log="/home/ma-user/modelarts/log/${print_log_name}" +fi +echo "### get your log here : ${print_log}" + +CaseName="" +function get_casename() +{ + if [ x"${perf_flag}" = x1 ]; + then + CaseName=${Network}_bs${batch_size}_${RANK_SIZE}'p'_'perf' + else + CaseName=${Network}_bs${batch_size}_${RANK_SIZE}'p'_'acc' + fi +} + +# 跳转到code目录 +cd ${cur_path}/../ +rm -rf ./test/output/${ASCEND_DEVICE_ID} +mkdir -p ./test/output/${ASCEND_DEVICE_ID} + +# 训练开始时间记录,不需要修改 +start_time=$(date +%s) +########################################################## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +########################################################## + +#========================================================= +#========================================================= +#========训练执行命令,需要根据您的网络进行修改============== +#========================================================= +#========================================================= +# 基础参数,需要模型审视修改 +# 您的训练数据集在${data_path}路径下,请直接使用这个变量获取 +# 您的训练输出目录在${output_path}路径下,请直接使用这个变量获取 +# 您的其他基础参数,可以自定义增加,但是batch_size请保留,并且设置正确的值 +batch_size=64 + +if [ x"${modelarts_flag}" != x ]; +then + python3.7 ./mnasnet_main.py --data_dir=${data_dir}/ILSVRC2012 --model_dir=${data_dir}/result_mix --obs_dir=${output_path} --model_name='mixnet-s' --train_steps=${train_steps} +else + python3.7 ./mnasnet_main.py --data_dir=${data_dir}/ILSVRC2012 --model_dir=${data_dir}/result_mix --obs_dir=${output_path} --model_name='mixnet-s' --train_steps=${train_steps} 1>${print_log} 2>&1 +fi + +# 性能相关数据计算 +StepTime=`grep "sec/step :" ${print_log} | tail -n 10 | awk '{print $NF}' | awk '{sum+=$1} END {print sum/NR}'` +FPS=`awk 'BEGIN{printf "%.2f\n", '${batch_size}'/'${StepTime}'}'` + +# 精度相关数据计算 +train_accuracy=`grep "Final Accuracy accuracy" ${print_log} | awk '{print $NF}'` +# 提取所有loss打印信息 +grep "loss :" ${print_log} | awk -F ":" '{print $4}' | awk -F "-" '{print $1}' > ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt + + +########################################################### +#########后面的所有内容请不要修改########################### +#########后面的所有内容请不要修改########################### +#########后面的所有内容请不要修改########################### +########################################################### + +# 判断本次执行是否正确使用Ascend NPU +tf_flag=`echo ${Network} | grep TensorFlow | wc -l` +use_npu_flag=`grep "The model has been compiled on the Ascend AI processor" ${print_log} | wc -l` +if [ x"${use_npu_flag}" == x0 -a x"${tf_flag}" == x1 ]; +then + echo "------------------ ERROR NOTICE START ------------------" + echo "ERROR, your task haven't used Ascend NPU, please check your npu Migration." + echo "------------------ ERROR NOTICE END------------------" +else + echo "------------------ INFO NOTICE START------------------" + echo "INFO, your task have used Ascend NPU, please check your result." + echo "------------------ INFO NOTICE END------------------" +fi + +# 获取最终的casename,请保留,case文件名为${CaseName} +get_casename + +# 重命名loss文件 +if [ -f ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt ]; +then + mv ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt ./test/output/${ASCEND_DEVICE_ID}/${CaseName}_loss.txt +fi + +# 训练端到端耗时 +end_time=$(date +%s) +e2e_time=$(( $end_time - $start_time )) + +echo "------------------ Final result ------------------" +# 输出性能FPS/单step耗时/端到端耗时 +echo "Final Performance images/sec : $FPS" +echo "Final Performance sec/step : $StepTime" +echo "E2E Training Duration sec : $e2e_time" + +# 输出训练精度 +echo "Final Train Accuracy : ${train_accuracy}" + +# 最后一个迭代loss值,不需要修改 +ActualLoss=(`awk 'END {print $NF}' $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}_loss.txt`) + +#关键信息打印到${CaseName}.log中,不需要修改 +echo "Network = ${Network}" > $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "RankSize = ${RANK_SIZE}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "BatchSize = ${batch_size}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "DeviceType = `uname -m`" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "CaseName = ${CaseName}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "ActualFPS = ${FPS}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "TrainingTime = ${StepTime}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "ActualLoss = ${ActualLoss}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "E2ETrainingTime = ${e2e_time}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "TrainAccuracy = ${train_accuracy}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log \ No newline at end of file diff --git a/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_performance_1p.sh b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_performance_1p.sh new file mode 100644 index 000000000..843d4abed --- /dev/null +++ b/TensorFlow/contrib/cv/Mixnet_ID2072_for_Tensorflow/test/train_performance_1p.sh @@ -0,0 +1,186 @@ +#!/bin/bash + +########################################################## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +########################################################## +# shell脚本所在路径 +cur_path=`echo $(cd $(dirname $0);pwd)` + +# 判断当前shell是否是performance +perf_flag=`echo $0 | grep performance | wc -l` + +# 当前执行网络的名称 +Network=`echo $(cd $(dirname $0);pwd) | awk -F"/" '{print $(NF-1)}'` + +export RANK_SIZE=1 +export RANK_ID=0 +export JOB_ID=10087 + +# 路径参数初始化 +data_path="" +output_path="" + +# 帮助信息,不需要修改 +if [[ $1 == --help || $1 == -h ]];then + echo"usage:./train_performance_1P.sh " + echo " " + echo "parameter explain: + --data_path # dataset of training + --output_path # output of training + --train_steps # max_step for training + --train_epochs # max_epoch for training + --batch_size # batch size + -h/--help show help message + " + exit 1 +fi + +# 参数校验,不需要修改 +for para in $* +do + if [[ $para == --data_path* ]];then + data_path=`echo ${para#*=}` + elif [[ $para == --output_path* ]];then + output_path=`echo ${para#*=}` + elif [[ $para == --train_steps* ]];then + train_steps=`echo ${para#*=}` + elif [[ $para == --train_epochs* ]];then + train_epochs=`echo ${para#*=}` + elif [[ $para == --batch_size* ]];then + batch_size=`echo ${para#*=}` + fi +done + +# 校验是否传入data_path,不需要修改 +if [[ $data_path == "" ]];then + echo "[Error] para \"data_path\" must be config" + exit 1 +fi + +# 校验是否传入output_path,不需要修改 +if [[ $output_path == "" ]];then + output_path="./test/output/${ASCEND_DEVICE_ID}" +fi + +# 设置打屏日志文件名,请保留,文件名为${print_log} +print_log="./test/output/${ASCEND_DEVICE_ID}/train_${ASCEND_DEVICE_ID}.log" +modelarts_flag=`cat /etc/passwd |grep ma-user` +if [ x"${modelarts_flag}" != x ]; +then + echo "running with modelarts..." + print_log_name=`ls /home/ma-user/modelarts/log/ | grep proc-rank` + print_log="/home/ma-user/modelarts/log/${print_log_name}" +fi +echo "### get your log here : ${print_log}" + +CaseName="" +function get_casename() +{ + if [ x"${perf_flag}" = x1 ]; + then + CaseName=${Network}_bs${batch_size}_${RANK_SIZE}'p'_'perf' + else + CaseName=${Network}_bs${batch_size}_${RANK_SIZE}'p'_'acc' + fi +} + +# 跳转到code目录 +cd ${cur_path}/../ +rm -rf ./test/output/${ASCEND_DEVICE_ID} +mkdir -p ./test/output/${ASCEND_DEVICE_ID} + +# 训练开始时间记录,不需要修改 +start_time=$(date +%s) +########################################################## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +#########第3行 至 100行,请一定不要、不要、不要修改########## +########################################################## + +#========================================================= +#========================================================= +#========训练执行命令,需要根据您的网络进行修改============== +#========================================================= +#========================================================= +# 基础参数,需要模型审视修改 +# 您的训练数据集在${data_path}路径下,请直接使用这个变量获取 +# 您的训练输出目录在${output_path}路径下,请直接使用这个变量获取 +# 您的其他基础参数,可以自定义增加,但是batch_size请保留,并且设置正确的值 +train_epochs=2 +train_steps=100 +batch_size=64 + +if [ x"${modelarts_flag}" != x ]; +then + python3.7 ./mnasnet_main.py --data_dir=${data_dir}/ILSVRC2012 --model_dir=${data_dir}/result_mix --obs_dir=${output_path} --model_name='mixnet-s' --train_steps=${train_steps} +else + python3.7 ./mnasnet_main.py --data_dir=${data_dir}/ILSVRC2012 --model_dir=${data_dir}/result_mix --obs_dir=${output_path} --model_name='mixnet-s' --train_steps=${train_steps} 1>${print_log} 2>&1 +fi + +# 性能相关数据计算 +StepTime=`grep "sec/step :" ${print_log} | tail -n 10 | awk '{print $NF}' | awk '{sum+=$1} END {print sum/NR}'` +FPS=`awk 'BEGIN{printf "%.2f\n", '${batch_size}'/'${StepTime}'}'` + +# 精度相关数据计算 +train_accuracy=`grep "Final Accuracy accuracy" ${print_log} | awk '{print $NF}'` +# 提取所有loss打印信息 +grep "loss :" ${print_log} | awk -F ":" '{print $4}' | awk -F "-" '{print $1}' > ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt + + +########################################################### +#########后面的所有内容请不要修改########################### +#########后面的所有内容请不要修改########################### +#########后面的所有内容请不要修改########################### +########################################################### + +# 判断本次执行是否正确使用Ascend NPU +tf_flag=`echo ${Network} | grep TensorFlow | wc -l` +use_npu_flag=`grep "The model has been compiled on the Ascend AI processor" ${print_log} | wc -l` +if [ x"${use_npu_flag}" == x0 -a x"${tf_flag}" == x1 ]; +then + echo "------------------ ERROR NOTICE START ------------------" + echo "ERROR, your task haven't used Ascend NPU, please check your npu Migration." + echo "------------------ ERROR NOTICE END------------------" +else + echo "------------------ INFO NOTICE START------------------" + echo "INFO, your task have used Ascend NPU, please check your result." + echo "------------------ INFO NOTICE END------------------" +fi + +# 获取最终的casename,请保留,case文件名为${CaseName} +get_casename + +# 重命名loss文件 +if [ -f ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt ]; +then + mv ./test/output/${ASCEND_DEVICE_ID}/my_output_loss.txt ./test/output/${ASCEND_DEVICE_ID}/${CaseName}_loss.txt +fi + +# 训练端到端耗时 +end_time=$(date +%s) +e2e_time=$(( $end_time - $start_time )) + +echo "------------------ Final result ------------------" +# 输出性能FPS/单step耗时/端到端耗时 +echo "Final Performance images/sec : $FPS" +echo "Final Performance sec/step : $StepTime" +echo "E2E Training Duration sec : $e2e_time" + +# 输出训练精度 +echo "Final Train Accuracy : ${train_accuracy}" + +# 最后一个迭代loss值,不需要修改 +ActualLoss=(`awk 'END {print $NF}' $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}_loss.txt`) + +#关键信息打印到${CaseName}.log中,不需要修改 +echo "Network = ${Network}" > $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "RankSize = ${RANK_SIZE}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "BatchSize = ${batch_size}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "DeviceType = `uname -m`" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "CaseName = ${CaseName}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "ActualFPS = ${FPS}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "TrainingTime = ${StepTime}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "ActualLoss = ${ActualLoss}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log +echo "E2ETrainingTime = ${e2e_time}" >> $cur_path/output/$ASCEND_DEVICE_ID/${CaseName}.log \ No newline at end of file -- Gitee