From 0036011d2141c665ae5152283dcd24ae9a08fa34 Mon Sep 17 00:00:00 2001 From: Neta Zmora <31280975+nzmora@users.noreply.github.com> Date: Mon, 23 Sep 2019 14:56:59 +0300 Subject: [PATCH] User-registered model (#391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a jupyter notebook showing how to register a user's (external) image-classification model. Contains fixes to the previous models extension mechanism, and relaxation of the `args' requirements in apputils/image_classifier.py. apputils/image_classifier.py – *when self.logdir is None: -use NullLogger -skip save_checkpoint *return training log from run_training_loop() *don’t log if script_dir or output_dir are not set. *Fix params_nnz_cnt in update_training_scores_history() data_loggers/logger.py – add NullLogger which does not log --- distiller/apputils/image_classifier.py | 45 ++++--- distiller/data_loggers/__init__.py | 2 +- distiller/data_loggers/logger.py | 5 +- distiller/models/__init__.py | 10 +- jupyter/tutorial__adding_new_model.ipynb | 158 +++++++++++++++++++++++ 5 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 jupyter/tutorial__adding_new_model.ipynb diff --git a/distiller/apputils/image_classifier.py b/distiller/apputils/image_classifier.py index b008746..23c07a4 100755 --- a/distiller/apputils/image_classifier.py +++ b/distiller/apputils/image_classifier.py @@ -62,8 +62,11 @@ class ClassifierCompressor(object): # Create a couple of logging backends. TensorBoardLogger writes log files in a format # that can be read by Google's Tensor Board. PythonLogger writes to the Python logger. - self.tflogger = TensorBoardLogger(msglogger.logdir) - self.pylogger = PythonLogger(msglogger) + if not self.logdir: + self.pylogger = self.tflogger = NullLogger() + else: + self.tflogger = TensorBoardLogger(msglogger.logdir) + self.pylogger = PythonLogger(msglogger) (self.model, self.compression_scheduler, self.optimizer, self.start_epoch, self.ending_epoch) = _init_learner(args) @@ -133,15 +136,16 @@ class ClassifierCompressor(object): def _finalize_epoch(self, epoch, perf_scores_history, top1, top5): # Update the list of top scores achieved so far, and save the checkpoint - update_training_scores_history(perf_scores_history, self.model, + update_training_scores_history(perf_scores_history, self.model, top1, top5, epoch, self.args.num_best_scores) is_best = epoch == perf_scores_history[0].epoch checkpoint_extras = {'current_top1': top1, 'best_top1': perf_scores_history[0].top1, 'best_epoch': perf_scores_history[0].epoch} - apputils.save_checkpoint(epoch, self.args.arch, self.model, optimizer=self.optimizer, - scheduler=self.compression_scheduler, extras=checkpoint_extras, - is_best=is_best, name=self.args.name, dir=msglogger.logdir) + if msglogger.logdir: + apputils.save_checkpoint(epoch, self.args.arch, self.model, optimizer=self.optimizer, + scheduler=self.compression_scheduler, extras=checkpoint_extras, + is_best=is_best, name=self.args.name, dir=msglogger.logdir) def run_training_loop(self): @@ -152,12 +156,6 @@ class ClassifierCompressor(object): validate_one_epoch finalize_epoch """ - if self.start_epoch >= self.ending_epoch: - msglogger.error( - 'epoch count is too low, starting epoch is {} but total epochs set to {}'.format( - self.start_epoch, self.ending_epoch)) - raise ValueError('Epochs parameter is too low. Nothing to do.') - # Load the datasets lazily self.load_datasets() @@ -166,6 +164,7 @@ class ClassifierCompressor(object): msglogger.info('\n') top1, top5, loss = self.train_validate_with_scheduling(epoch) self._finalize_epoch(epoch, perf_scores_history, top1, top5) + return perf_scores_history def validate(self, epoch=-1): self.load_datasets() @@ -231,7 +230,7 @@ def init_classifier_compression_arg_parser(): help='collect activation statistics on phases: train, valid, and/or test' ' (WARNING: this slows down training)') parser.add_argument('--activation-histograms', '--act-hist', - type=float_range(exc_min=True), + type=distiller.utils.float_range_argparse_checker(exc_min=True), metavar='PORTION_OF_TEST_SET', help='Run the model in evaluation mode on the specified portion of the test dataset and ' 'generate activation histograms. NOTE: This slows down evaluation significantly') @@ -252,6 +251,8 @@ def init_classifier_compression_arg_parser(): help='an optional parameter for sensitivity testing ' 'providing the range of sparsities to test.\n' 'This is equivalent to creating sensitivities = np.arange(start, stop, step)') + parser.add_argument('--extras', default=None, type=str, + help='file with extra configuration information') parser.add_argument('--deterministic', '--det', action='store_true', help='Ensure deterministic execution for re-producible results.') parser.add_argument('--seed', type=int, default=None, @@ -291,9 +292,11 @@ def init_classifier_compression_arg_parser(): def _init_logger(args, script_dir): - module_path = os.path.abspath(os.path.join(script_dir, '..', '..')) global msglogger - + if not script_dir or not hasattr(args, "output_dir") or not args.output_dir: + msglogger.logdir = None + return None + module_path = os.path.abspath(os.path.join(script_dir, '..', '..')) if not os.path.exists(args.output_dir): os.makedirs(args.output_dir) msglogger = apputils.config_pylogger(os.path.join(script_dir, 'logging.conf'), @@ -307,6 +310,7 @@ def _init_logger(args, script_dir): msglogger.debug("Distiller: %s", distiller.__version__) return msglogger.logdir + def _config_determinism(args): if args.evaluate: args.deterministic = True @@ -328,6 +332,7 @@ def _config_determinism(args): cudnn.benchmark = True msglogger.info("Random seed: %d", args.seed) + def _config_compute_device(args): if args.cpu or not torch.cuda.is_available(): # Set GPU index to -1 if using CPU @@ -400,7 +405,13 @@ def _init_learner(args): elif compression_scheduler is None: compression_scheduler = distiller.CompressionScheduler(model) - return model, compression_scheduler, optimizer, start_epoch, args.epochs + ending_epoch = args.epochs + if start_epoch >= ending_epoch: + msglogger.error( + 'epoch count is too low, starting epoch is {} but total epochs set to {}'.format( + start_epoch, ending_epoch)) + raise ValueError('Epochs parameter is too low. Nothing to do.') + return model, compression_scheduler, optimizer, start_epoch, ending_epoch def create_activation_stats_collectors(model, *phases): @@ -725,7 +736,7 @@ def update_training_scores_history(perf_scores_history, model, top1, top5, epoch perf_scores_history.sort(key=operator.attrgetter('params_nnz_cnt', 'top1', 'top5', 'epoch'), reverse=True) for score in perf_scores_history[:num_best_scores]: msglogger.info('==> Best [Top1: %.3f Top5: %.3f Sparsity:%.2f Params: %d on epoch: %d]', - score.top1, score.top5, score.sparsity, -score.params_nnz_cnt, score.epoch) + score.top1, score.top5, score.sparsity, score.params_nnz_cnt, score.epoch) def earlyexit_loss(output, target, criterion, args): diff --git a/distiller/data_loggers/__init__.py b/distiller/data_loggers/__init__.py index a3351f0..f40d718 100755 --- a/distiller/data_loggers/__init__.py +++ b/distiller/data_loggers/__init__.py @@ -15,7 +15,7 @@ # from .collector import * -from .logger import PythonLogger, TensorBoardLogger, CsvLogger +from .logger import PythonLogger, TensorBoardLogger, CsvLogger, NullLogger del logger del collector diff --git a/distiller/data_loggers/logger.py b/distiller/data_loggers/logger.py index bc99a0d..6a4206d 100755 --- a/distiller/data_loggers/logger.py +++ b/distiller/data_loggers/logger.py @@ -36,7 +36,7 @@ from contextlib import ExitStack import os #msglogger = logging.getLogger() -__all__ = ['PythonLogger', 'TensorBoardLogger', 'CsvLogger'] +__all__ = ['PythonLogger', 'TensorBoardLogger', 'CsvLogger', 'NullLogger'] class DataLogger(object): @@ -64,6 +64,9 @@ class DataLogger(object): def log_model_buffers(self, model, buffer_names, tag_prefix, epoch, completed, total, freq): pass +# Log to null-space +NullLogger = DataLogger + class PythonLogger(DataLogger): def __init__(self, logger): diff --git a/distiller/models/__init__.py b/distiller/models/__init__.py index 83e6539..95edfcb 100755 --- a/distiller/models/__init__.py +++ b/distiller/models/__init__.py @@ -103,9 +103,9 @@ def create_model(pretrained, dataset, arch, parallel=True, device_ids=None): model = _create_mnist_model(arch, pretrained) except ValueError: if _is_registered_extension(arch, dataset, pretrained): - model = _get_extension_model(arch, dataset) + model = _create_extension_model(arch, dataset) else: - raise ValueError('Could not recognize dataset {} and model {} pair'.format(dataset, arch)) + raise ValueError('Could not recognize dataset {} and arch {} pair'.format(dataset, arch)) msglogger.info("=> created a %s%s model with the %s dataset" % ('pretrained ' if pretrained else '', arch, dataset)) @@ -200,10 +200,10 @@ def register_user_model(arch, dataset, model): def _is_registered_extension(arch, dataset, pretrained): try: - return _model_extensions[(arch, dataset)] + return _model_extensions[(arch, dataset)] is not None except KeyError: return None -def _get_extension_model(arch, dataset): - return _model_extensions[(arch, dataset)] \ No newline at end of file +def _create_extension_model(arch, dataset): + return _model_extensions[(arch, dataset)]() \ No newline at end of file diff --git a/jupyter/tutorial__adding_new_model.ipynb b/jupyter/tutorial__adding_new_model.ipynb new file mode 100644 index 0000000..a1a467a --- /dev/null +++ b/jupyter/tutorial__adding_new_model.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tutorial: Adding a new image-classification model" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import distiller\n", + "import torch.nn as nn\n", + "from distiller.models import register_user_model\n", + "import distiller.apputils.image_classifier as classifier" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class MyModel(nn.Module): \n", + " def __init__(self):\n", + " super().__init__()\n", + " self.conv1 = nn.Conv2d(1, 20, 5, 1)\n", + " self.relu1 = nn.ReLU(inplace=False)\n", + " self.pool1 = nn.MaxPool2d(2, 2)\n", + " self.conv2 = nn.Conv2d(20, 50, 5, 1)\n", + " self.relu2 = nn.ReLU(inplace=False)\n", + " self.pool2 = nn.MaxPool2d(2, 2)\n", + " self.avgpool = nn.AvgPool2d(4, stride=1)\n", + " self.fc = nn.Linear(50, 10)\n", + "\n", + " def forward(self, x):\n", + " x = self.pool1(self.relu1(self.conv1(x)))\n", + " x = self.pool2(self.relu2(self.conv2(x)))\n", + " x = self.avgpool(x)\n", + " x = x.view(x.size(0), -1)\n", + " x = self.fc(x)\n", + " return x\n", + "\n", + "def my_model():\n", + " return MyModel()\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'params_nnz_cnt': -26000.0, 'sparsity': 0.0, 'top1': 92.80000000000001, 'top5': 99.63333333333333, 'epoch': 0}]\n" + ] + } + ], + "source": [ + "distiller.models.register_user_model(arch=\"MyModel\", dataset=\"mnist\", model=my_model)\n", + "model = distiller.models.create_model(pretrained=True, dataset=\"mnist\", arch=\"MyModel\")\n", + "assert model is not None\n", + "\n", + "\n", + "def init_jupyter_default_args(args):\n", + " args.output_dir = None\n", + " args.evaluate = False\n", + " args.seed = None\n", + " args.deterministic = False\n", + " args.cpu = True\n", + " args.gpus = None\n", + " args.load_serialized = False\n", + " args.deprecated_resume = None\n", + " args.resumed_checkpoint_path = None\n", + " args.load_model_path = None\n", + " args.reset_optimizer = False\n", + " args.lr = args.momentum = args.weight_decay = 0.\n", + " args.compress = None\n", + " args.epochs = 0\n", + " args.activation_stats = list()\n", + " args.batch_size = 1\n", + " args.workers = 1\n", + " args.validation_split = 0.1\n", + " args.effective_train_size = args.effective_valid_size = args.effective_test_size = 1.\n", + " args.log_params_histograms = False\n", + " args.print_freq = 1\n", + " args.masks_sparsity = False\n", + " args.display_confusion = False\n", + " args.num_best_scores = 1\n", + " args.name = \"\"\n", + "\n", + "\n", + "def config_learner_args(args, arch, dataset, dataset_path, pretrained, sgd_args, batch, epochs):\n", + " args.arch = \"MyModel\"\n", + " args.dataset = \"mnist\"\n", + " args.data = \"/datasets/mnist/\"\n", + " args.pretrained = False\n", + " args.lr = sgd_args[0]\n", + " args.momentum = sgd_args[1]\n", + " args.weight_decay = sgd_args[2]\n", + " args.batch_size = 256\n", + " args.epochs = epochs\n", + "\n", + "args = classifier.init_classifier_compression_arg_parser()\n", + "init_jupyter_default_args(args)\n", + "config_learner_args(args, \"MyModel\", \"mnist\", \"/datasets/mnist/\", False, (0.1, 0.9, 1e-4) , 256, 1)\n", + "app = classifier.ClassifierCompressor(args, script_dir=os.path.dirname(\".\"))\n", + "\n", + "# Run the training loop\n", + "perf_scores_history = app.run_training_loop()\n", + "print(perf_scores_history)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} -- GitLab