From 6f256c781a5c2cc841060435d3b732daa6adb108 Mon Sep 17 00:00:00 2001 From: Yuge Zhang Date: Tue, 24 Dec 2019 11:52:56 +0800 Subject: [PATCH 1/4] Single Path One Shot (#1849) --- examples/nas/.gitignore | 1 + examples/nas/spos/README.md | 88 +++++++ examples/nas/spos/architecture_final.json | 22 ++ examples/nas/spos/blocks.py | 89 +++++++ examples/nas/spos/config_search.yml | 16 ++ examples/nas/spos/dataloader.py | 106 +++++++++ examples/nas/spos/network.py | 156 ++++++++++++ examples/nas/spos/scratch.py | 128 ++++++++++ examples/nas/spos/supernet.py | 74 ++++++ examples/nas/spos/tester.py | 115 +++++++++ examples/nas/spos/tuner.py | 25 ++ examples/nas/spos/utils.py | 41 ++++ .../pynni/nni/nas/pytorch/spos/__init__.py | 6 + .../pynni/nni/nas/pytorch/spos/evolution.py | 222 ++++++++++++++++++ src/sdk/pynni/nni/nas/pytorch/spos/mutator.py | 63 +++++ src/sdk/pynni/nni/nas/pytorch/spos/trainer.py | 63 +++++ 16 files changed, 1215 insertions(+) create mode 100644 examples/nas/spos/README.md create mode 100644 examples/nas/spos/architecture_final.json create mode 100644 examples/nas/spos/blocks.py create mode 100644 examples/nas/spos/config_search.yml create mode 100644 examples/nas/spos/dataloader.py create mode 100644 examples/nas/spos/network.py create mode 100644 examples/nas/spos/scratch.py create mode 100644 examples/nas/spos/supernet.py create mode 100644 examples/nas/spos/tester.py create mode 100644 examples/nas/spos/tuner.py create mode 100644 examples/nas/spos/utils.py create mode 100644 src/sdk/pynni/nni/nas/pytorch/spos/__init__.py create mode 100644 src/sdk/pynni/nni/nas/pytorch/spos/evolution.py create mode 100644 src/sdk/pynni/nni/nas/pytorch/spos/mutator.py create mode 100644 src/sdk/pynni/nni/nas/pytorch/spos/trainer.py diff --git a/examples/nas/.gitignore b/examples/nas/.gitignore index 8eeb0c2a3f..e26f9a17a1 100644 --- a/examples/nas/.gitignore +++ b/examples/nas/.gitignore @@ -1,3 +1,4 @@ data checkpoints runs +nni_auto_gen_search_space.json diff --git a/examples/nas/spos/README.md b/examples/nas/spos/README.md new file mode 100644 index 0000000000..ed239f30a1 --- /dev/null +++ b/examples/nas/spos/README.md @@ -0,0 +1,88 @@ +# Single Path One-Shot Neural Architecture Search with Uniform Sampling + +Single Path One-Shot by Megvii Research. [Paper link](https://arxiv.org/abs/1904.00420). [Official repo](https://github.com/megvii-model/SinglePathOneShot). + +Block search only. Channel search is not supported yet. + +Only GPU version is provided here. + +## Preparation + +### Requirements + +* PyTorch >= 1.2 +* NVIDIA DALI >= 0.16 as we use DALI to accelerate the data loading of ImageNet. [Installation guide](https://docs.nvidia.com/deeplearning/sdk/dali-developer-guide/docs/installation.html) + +### Data + +Need to download the flops lookup table from [here](https://1drv.ms/u/s!Am_mmG2-KsrnajesvSdfsq_cN48?e=aHVppN). +Put `op_flops_dict.pkl` and `checkpoint-150000.pth.tar` (if you don't want to retrain the supernet) under `data` directory. + +Prepare ImageNet in the standard format (follow the script [here](https://gist.github.com/BIGBALLON/8a71d225eff18d88e469e6ea9b39cef4)). Link it to `data/imagenet` will be more convenient. + +After preparation, it's expected to have the following code structure: + +``` +spos +├── architecture_final.json +├── blocks.py +├── config_search.yml +├── data +│   ├── imagenet +│   │   ├── train +│   │   └── val +│   └── op_flops_dict.pkl +├── dataloader.py +├── network.py +├── readme.md +├── scratch.py +├── supernet.py +├── tester.py +├── tuner.py +└── utils.py +``` + +## Step 1. Train Supernet + +``` +python supernet.py +``` + +Will export the checkpoint to checkpoints directory, for the next step. + +NOTE: The data loading used in the official repo is [slightly different from usual](https://github.com/megvii-model/SinglePathOneShot/issues/5), as they use BGR tensor and keep the values between 0 and 255 intentionally to align with their own DL framework. The option `--spos-preprocessing` will simulate the behavior used originally and enable you to use the checkpoints pretrained. + +## Step 2. Evolution Search + +Single Path One-Shot leverages evolution algorithm to search for the best architecture. The tester, which is responsible for testing the sampled architecture, recalculates all the batch norm for a subset of training images, and evaluates the architecture on the full validation set. + +To have a search space ready for NNI framework, first run + +``` +nnictl ss_gen -t "python tester.py" +``` + +This will generate a file called `nni_auto_gen_search_space.json`, which is a serialized representation of your search space. + +Then search with evolution tuner. + +``` +nnictl create --config config_search.yml +``` + +The final architecture exported from every epoch of evolution can be found in `checkpoints` under the working directory of your tuner, which, by default, is `$HOME/nni/experiments/your_experiment_id/log`. + +## Step 3. Train from Scratch + +``` +python scratch.py +``` + +By default, it will use `architecture_final.json`. This architecture is provided by the official repo (converted into NNI format). You can use any architecture (e.g., the architecture found in step 2) with `--fixed-arc` option. + +## Current Reproduction Results + +Reproduction is still undergoing. Due to the gap between official release and original paper, we compare our current results with official repo (our run) and paper. + +* Evolution phase is almost aligned with official repo. Our evolution algorithm shows a converging trend and reaches ~65% accuracy at the end of search. Nevertheless, this result is not on par with paper. For details, please refer to [this issue](https://github.com/megvii-model/SinglePathOneShot/issues/6). +* Retrain phase is not aligned. Our retraining code, which uses the architecture released by the authors, reaches 72.14% accuracy, still having a gap towards 73.61% by official release and 74.3% reported in original paper. diff --git a/examples/nas/spos/architecture_final.json b/examples/nas/spos/architecture_final.json new file mode 100644 index 0000000000..512a73b9d6 --- /dev/null +++ b/examples/nas/spos/architecture_final.json @@ -0,0 +1,22 @@ +{ + "LayerChoice1": [false, false, true, false], + "LayerChoice2": [false, true, false, false], + "LayerChoice3": [true, false, false, false], + "LayerChoice4": [false, true, false, false], + "LayerChoice5": [false, false, true, false], + "LayerChoice6": [true, false, false, false], + "LayerChoice7": [false, false, true, false], + "LayerChoice8": [true, false, false, false], + "LayerChoice9": [false, false, true, false], + "LayerChoice10": [true, false, false, false], + "LayerChoice11": [false, false, true, false], + "LayerChoice12": [false, false, false, true], + "LayerChoice13": [true, false, false, false], + "LayerChoice14": [true, false, false, false], + "LayerChoice15": [true, false, false, false], + "LayerChoice16": [true, false, false, false], + "LayerChoice17": [false, false, false, true], + "LayerChoice18": [false, false, true, false], + "LayerChoice19": [false, false, false, true], + "LayerChoice20": [false, false, false, true] +} diff --git a/examples/nas/spos/blocks.py b/examples/nas/spos/blocks.py new file mode 100644 index 0000000000..5908ecf077 --- /dev/null +++ b/examples/nas/spos/blocks.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import torch +import torch.nn as nn + + +class ShuffleNetBlock(nn.Module): + """ + When stride = 1, the block receives input with 2 * inp channels. Otherwise inp channels. + """ + + def __init__(self, inp, oup, mid_channels, ksize, stride, sequence="pdp"): + super().__init__() + assert stride in [1, 2] + assert ksize in [3, 5, 7] + self.channels = inp // 2 if stride == 1 else inp + self.inp = inp + self.oup = oup + self.mid_channels = mid_channels + self.ksize = ksize + self.stride = stride + self.pad = ksize // 2 + self.oup_main = oup - self.channels + assert self.oup_main > 0 + + self.branch_main = nn.Sequential(*self._decode_point_depth_conv(sequence)) + + if stride == 2: + self.branch_proj = nn.Sequential( + # dw + nn.Conv2d(self.channels, self.channels, ksize, stride, self.pad, + groups=self.channels, bias=False), + nn.BatchNorm2d(self.channels, affine=False), + # pw-linear + nn.Conv2d(self.channels, self.channels, 1, 1, 0, bias=False), + nn.BatchNorm2d(self.channels, affine=False), + nn.ReLU(inplace=True) + ) + + def forward(self, x): + if self.stride == 2: + x_proj, x = self.branch_proj(x), x + else: + x_proj, x = self._channel_shuffle(x) + return torch.cat((x_proj, self.branch_main(x)), 1) + + def _decode_point_depth_conv(self, sequence): + result = [] + first_depth = first_point = True + pc = c = self.channels + for i, token in enumerate(sequence): + # compute output channels of this conv + if i + 1 == len(sequence): + assert token == "p", "Last conv must be point-wise conv." + c = self.oup_main + elif token == "p" and first_point: + c = self.mid_channels + if token == "d": + # depth-wise conv + assert pc == c, "Depth-wise conv must not change channels." + result.append(nn.Conv2d(pc, c, self.ksize, self.stride if first_depth else 1, self.pad, + groups=c, bias=False)) + result.append(nn.BatchNorm2d(c, affine=False)) + first_depth = False + elif token == "p": + # point-wise conv + result.append(nn.Conv2d(pc, c, 1, 1, 0, bias=False)) + result.append(nn.BatchNorm2d(c, affine=False)) + result.append(nn.ReLU(inplace=True)) + first_point = False + else: + raise ValueError("Conv sequence must be d and p.") + pc = c + return result + + def _channel_shuffle(self, x): + bs, num_channels, height, width = x.data.size() + assert (num_channels % 4 == 0) + x = x.reshape(bs * num_channels // 2, 2, height * width) + x = x.permute(1, 0, 2) + x = x.reshape(2, -1, num_channels // 2, height, width) + return x[0], x[1] + + +class ShuffleXceptionBlock(ShuffleNetBlock): + + def __init__(self, inp, oup, mid_channels, stride): + super().__init__(inp, oup, mid_channels, 3, stride, "dpdpdp") diff --git a/examples/nas/spos/config_search.yml b/examples/nas/spos/config_search.yml new file mode 100644 index 0000000000..fe27faefc8 --- /dev/null +++ b/examples/nas/spos/config_search.yml @@ -0,0 +1,16 @@ +authorName: unknown +experimentName: SPOS Search +trialConcurrency: 4 +maxExecDuration: 7d +maxTrialNum: 99999 +trainingServicePlatform: local +searchSpacePath: nni_auto_gen_search_space.json +useAnnotation: false +tuner: + codeDir: . + classFileName: tuner.py + className: EvolutionWithFlops +trial: + command: python tester.py --imagenet-dir /path/to/your/imagenet --spos-prep + codeDir: . + gpuNum: 1 diff --git a/examples/nas/spos/dataloader.py b/examples/nas/spos/dataloader.py new file mode 100644 index 0000000000..198d637ed1 --- /dev/null +++ b/examples/nas/spos/dataloader.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os + +import nvidia.dali.ops as ops +import nvidia.dali.types as types +import torch.utils.data +from nvidia.dali.pipeline import Pipeline +from nvidia.dali.plugin.pytorch import DALIClassificationIterator + + +class HybridTrainPipe(Pipeline): + def __init__(self, batch_size, num_threads, device_id, data_dir, crop, seed=12, local_rank=0, world_size=1, + spos_pre=False): + super(HybridTrainPipe, self).__init__(batch_size, num_threads, device_id, seed=seed + device_id) + color_space_type = types.BGR if spos_pre else types.RGB + self.input = ops.FileReader(file_root=data_dir, shard_id=local_rank, num_shards=world_size, random_shuffle=True) + self.decode = ops.ImageDecoder(device="mixed", output_type=color_space_type) + self.res = ops.RandomResizedCrop(device="gpu", size=crop, + interp_type=types.INTERP_LINEAR if spos_pre else types.INTERP_TRIANGULAR) + self.twist = ops.ColorTwist(device="gpu") + self.jitter_rng = ops.Uniform(range=[0.6, 1.4]) + self.cmnp = ops.CropMirrorNormalize(device="gpu", + output_dtype=types.FLOAT, + output_layout=types.NCHW, + image_type=color_space_type, + mean=0. if spos_pre else [0.485 * 255, 0.456 * 255, 0.406 * 255], + std=1. if spos_pre else [0.229 * 255, 0.224 * 255, 0.225 * 255]) + self.coin = ops.CoinFlip(probability=0.5) + + def define_graph(self): + rng = self.coin() + self.jpegs, self.labels = self.input(name="Reader") + images = self.decode(self.jpegs) + images = self.res(images) + images = self.twist(images, saturation=self.jitter_rng(), + contrast=self.jitter_rng(), brightness=self.jitter_rng()) + output = self.cmnp(images, mirror=rng) + return [output, self.labels] + + +class HybridValPipe(Pipeline): + def __init__(self, batch_size, num_threads, device_id, data_dir, crop, size, seed=12, local_rank=0, world_size=1, + spos_pre=False, shuffle=False): + super(HybridValPipe, self).__init__(batch_size, num_threads, device_id, seed=seed + device_id) + color_space_type = types.BGR if spos_pre else types.RGB + self.input = ops.FileReader(file_root=data_dir, shard_id=local_rank, num_shards=world_size, + random_shuffle=shuffle) + self.decode = ops.ImageDecoder(device="mixed", output_type=color_space_type) + self.res = ops.Resize(device="gpu", resize_shorter=size, + interp_type=types.INTERP_LINEAR if spos_pre else types.INTERP_TRIANGULAR) + self.cmnp = ops.CropMirrorNormalize(device="gpu", + output_dtype=types.FLOAT, + output_layout=types.NCHW, + crop=(crop, crop), + image_type=color_space_type, + mean=0. if spos_pre else [0.485 * 255, 0.456 * 255, 0.406 * 255], + std=1. if spos_pre else [0.229 * 255, 0.224 * 255, 0.225 * 255]) + + def define_graph(self): + self.jpegs, self.labels = self.input(name="Reader") + images = self.decode(self.jpegs) + images = self.res(images) + output = self.cmnp(images) + return [output, self.labels] + + +class ClassificationWrapper: + def __init__(self, loader, size): + self.loader = loader + self.size = size + + def __iter__(self): + return self + + def __next__(self): + data = next(self.loader) + return data[0]["data"], data[0]["label"].view(-1).long().cuda(non_blocking=True) + + def __len__(self): + return self.size + + +def get_imagenet_iter_dali(split, image_dir, batch_size, num_threads, crop=224, val_size=256, + spos_preprocessing=False, seed=12, shuffle=False, device_id=None): + world_size, local_rank = 1, 0 + if device_id is None: + device_id = torch.cuda.device_count() - 1 # use last gpu + if split == "train": + pipeline = HybridTrainPipe(batch_size=batch_size, num_threads=num_threads, device_id=device_id, + data_dir=os.path.join(image_dir, "train"), seed=seed, + crop=crop, world_size=world_size, local_rank=local_rank, + spos_pre=spos_preprocessing) + elif split == "val": + pipeline = HybridValPipe(batch_size=batch_size, num_threads=num_threads, device_id=device_id, + data_dir=os.path.join(image_dir, "val"), seed=seed, + crop=crop, size=val_size, world_size=world_size, local_rank=local_rank, + spos_pre=spos_preprocessing, shuffle=shuffle) + else: + raise AssertionError + pipeline.build() + num_samples = pipeline.epoch_size("Reader") + return ClassificationWrapper( + DALIClassificationIterator(pipeline, size=num_samples, fill_last_batch=split == "train", + auto_reset=True), (num_samples + batch_size - 1) // batch_size) diff --git a/examples/nas/spos/network.py b/examples/nas/spos/network.py new file mode 100644 index 0000000000..ba45095775 --- /dev/null +++ b/examples/nas/spos/network.py @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import pickle +import re + +import torch +import torch.nn as nn +from nni.nas.pytorch import mutables + +from blocks import ShuffleNetBlock, ShuffleXceptionBlock + + +class ShuffleNetV2OneShot(nn.Module): + block_keys = [ + 'shufflenet_3x3', + 'shufflenet_5x5', + 'shufflenet_7x7', + 'xception_3x3', + ] + + def __init__(self, input_size=224, first_conv_channels=16, last_conv_channels=1024, n_classes=1000, + op_flops_path="./data/op_flops_dict.pkl"): + super().__init__() + + assert input_size % 32 == 0 + with open(os.path.join(os.path.dirname(__file__), op_flops_path), "rb") as fp: + self._op_flops_dict = pickle.load(fp) + + self.stage_blocks = [4, 4, 8, 4] + self.stage_channels = [64, 160, 320, 640] + self._parsed_flops = dict() + self._input_size = input_size + self._feature_map_size = input_size + self._first_conv_channels = first_conv_channels + self._last_conv_channels = last_conv_channels + self._n_classes = n_classes + + # building first layer + self.first_conv = nn.Sequential( + nn.Conv2d(3, first_conv_channels, 3, 2, 1, bias=False), + nn.BatchNorm2d(first_conv_channels, affine=False), + nn.ReLU(inplace=True), + ) + self._feature_map_size //= 2 + + p_channels = first_conv_channels + features = [] + for num_blocks, channels in zip(self.stage_blocks, self.stage_channels): + features.extend(self._make_blocks(num_blocks, p_channels, channels)) + p_channels = channels + self.features = nn.Sequential(*features) + + self.conv_last = nn.Sequential( + nn.Conv2d(p_channels, last_conv_channels, 1, 1, 0, bias=False), + nn.BatchNorm2d(last_conv_channels, affine=False), + nn.ReLU(inplace=True), + ) + self.globalpool = nn.AvgPool2d(self._feature_map_size) + self.dropout = nn.Dropout(0.1) + self.classifier = nn.Sequential( + nn.Linear(last_conv_channels, n_classes, bias=False), + ) + + self._initialize_weights() + + def _make_blocks(self, blocks, in_channels, channels): + result = [] + for i in range(blocks): + stride = 2 if i == 0 else 1 + inp = in_channels if i == 0 else channels + oup = channels + + base_mid_channels = channels // 2 + mid_channels = int(base_mid_channels) # prepare for scale + choice_block = mutables.LayerChoice([ + ShuffleNetBlock(inp, oup, mid_channels=mid_channels, ksize=3, stride=stride), + ShuffleNetBlock(inp, oup, mid_channels=mid_channels, ksize=5, stride=stride), + ShuffleNetBlock(inp, oup, mid_channels=mid_channels, ksize=7, stride=stride), + ShuffleXceptionBlock(inp, oup, mid_channels=mid_channels, stride=stride) + ]) + result.append(choice_block) + + # find the corresponding flops + flop_key = (inp, oup, mid_channels, self._feature_map_size, self._feature_map_size, stride) + self._parsed_flops[choice_block.key] = [ + self._op_flops_dict["{}_stride_{}".format(k, stride)][flop_key] for k in self.block_keys + ] + if stride == 2: + self._feature_map_size //= 2 + return result + + def forward(self, x): + bs = x.size(0) + x = self.first_conv(x) + x = self.features(x) + x = self.conv_last(x) + x = self.globalpool(x) + + x = self.dropout(x) + x = x.contiguous().view(bs, -1) + x = self.classifier(x) + return x + + def get_candidate_flops(self, candidate): + conv1_flops = self._op_flops_dict["conv1"][(3, self._first_conv_channels, + self._input_size, self._input_size, 2)] + # Should use `last_conv_channels` here, but megvii insists that it's `n_classes`. Keeping it. + # https://github.com/megvii-model/SinglePathOneShot/blob/36eed6cf083497ffa9cfe7b8da25bb0b6ba5a452/src/Supernet/flops.py#L313 + rest_flops = self._op_flops_dict["rest_operation"][(self.stage_channels[-1], self._n_classes, + self._feature_map_size, self._feature_map_size, 1)] + total_flops = conv1_flops + rest_flops + for k, m in candidate.items(): + parsed_flops_dict = self._parsed_flops[k] + if isinstance(m, dict): # to be compatible with classical nas format + total_flops += parsed_flops_dict[m["_idx"]] + else: + total_flops += parsed_flops_dict[torch.max(m, 0)[1]] + return total_flops + + def _initialize_weights(self): + for name, m in self.named_modules(): + if isinstance(m, nn.Conv2d): + if 'first' in name: + nn.init.normal_(m.weight, 0, 0.01) + else: + nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1]) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + if m.weight is not None: + nn.init.constant_(m.weight, 1) + if m.bias is not None: + nn.init.constant_(m.bias, 0.0001) + nn.init.constant_(m.running_mean, 0) + elif isinstance(m, nn.BatchNorm1d): + nn.init.constant_(m.weight, 1) + if m.bias is not None: + nn.init.constant_(m.bias, 0.0001) + nn.init.constant_(m.running_mean, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + +def load_and_parse_state_dict(filepath="./data/checkpoint-150000.pth.tar"): + checkpoint = torch.load(filepath, map_location=torch.device("cpu")) + result = dict() + for k, v in checkpoint["state_dict"].items(): + if k.startswith("module."): + k = k[len("module."):] + k = re.sub(r"^(features.\d+).(\d+)", "\\1.choices.\\2", k) + result[k] = v + return result diff --git a/examples/nas/spos/scratch.py b/examples/nas/spos/scratch.py new file mode 100644 index 0000000000..3a944a7909 --- /dev/null +++ b/examples/nas/spos/scratch.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import argparse +import logging +import random + +import numpy as np +import torch +import torch.nn as nn +from dataloader import get_imagenet_iter_dali +from nni.nas.pytorch.fixed import apply_fixed_architecture +from nni.nas.pytorch.utils import AverageMeterGroup +from torch.utils.tensorboard import SummaryWriter + +from network import ShuffleNetV2OneShot +from utils import CrossEntropyLabelSmooth, accuracy + +logger = logging.getLogger("nni.spos.scratch") + + +def train(epoch, model, criterion, optimizer, loader, writer, args): + model.train() + meters = AverageMeterGroup() + cur_lr = optimizer.param_groups[0]["lr"] + + for step, (x, y) in enumerate(loader): + cur_step = len(loader) * epoch + step + optimizer.zero_grad() + logits = model(x) + loss = criterion(logits, y) + loss.backward() + optimizer.step() + + metrics = accuracy(logits, y) + metrics["loss"] = loss.item() + meters.update(metrics) + + writer.add_scalar("lr", cur_lr, global_step=cur_step) + writer.add_scalar("loss/train", loss.item(), global_step=cur_step) + writer.add_scalar("acc1/train", metrics["acc1"], global_step=cur_step) + writer.add_scalar("acc5/train", metrics["acc5"], global_step=cur_step) + + if step % args.log_frequency == 0 or step + 1 == len(loader): + logger.info("Epoch [%d/%d] Step [%d/%d] %s", epoch + 1, + args.epochs, step + 1, len(loader), meters) + + logger.info("Epoch %d training summary: %s", epoch + 1, meters) + + +def validate(epoch, model, criterion, loader, writer, args): + model.eval() + meters = AverageMeterGroup() + with torch.no_grad(): + for step, (x, y) in enumerate(loader): + logits = model(x) + loss = criterion(logits, y) + metrics = accuracy(logits, y) + metrics["loss"] = loss.item() + meters.update(metrics) + + if step % args.log_frequency == 0 or step + 1 == len(loader): + logger.info("Epoch [%d/%d] Validation Step [%d/%d] %s", epoch + 1, + args.epochs, step + 1, len(loader), meters) + + writer.add_scalar("loss/test", meters.loss.avg, global_step=epoch) + writer.add_scalar("acc1/test", meters.acc1.avg, global_step=epoch) + writer.add_scalar("acc5/test", meters.acc5.avg, global_step=epoch) + + logger.info("Epoch %d validation: top1 = %f, top5 = %f", epoch + 1, meters.acc1.avg, meters.acc5.avg) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("SPOS Training From Scratch") + parser.add_argument("--imagenet-dir", type=str, default="./data/imagenet") + parser.add_argument("--tb-dir", type=str, default="runs") + parser.add_argument("--architecture", type=str, default="architecture_final.json") + parser.add_argument("--workers", type=int, default=12) + parser.add_argument("--batch-size", type=int, default=1024) + parser.add_argument("--epochs", type=int, default=240) + parser.add_argument("--learning-rate", type=float, default=0.5) + parser.add_argument("--momentum", type=float, default=0.9) + parser.add_argument("--weight-decay", type=float, default=4E-5) + parser.add_argument("--label-smooth", type=float, default=0.1) + parser.add_argument("--log-frequency", type=int, default=10) + parser.add_argument("--lr-decay", type=str, default="linear") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--spos-preprocessing", default=False, action="store_true") + parser.add_argument("--label-smoothing", type=float, default=0.1) + + args = parser.parse_args() + + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + torch.backends.cudnn.deterministic = True + + model = ShuffleNetV2OneShot() + model.cuda() + apply_fixed_architecture(model, args.architecture) + if torch.cuda.device_count() > 1: # exclude last gpu, saving for data preprocessing on gpu + model = nn.DataParallel(model, device_ids=list(range(0, torch.cuda.device_count() - 1))) + criterion = CrossEntropyLabelSmooth(1000, args.label_smoothing) + optimizer = torch.optim.SGD(model.parameters(), lr=args.learning_rate, + momentum=args.momentum, weight_decay=args.weight_decay) + if args.lr_decay == "linear": + scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, + lambda step: (1.0 - step / args.epochs) + if step <= args.epochs else 0, + last_epoch=-1) + elif args.lr_decay == "cosine": + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, args.epochs, 1E-3) + else: + raise ValueError("'%s' not supported." % args.lr_decay) + writer = SummaryWriter(log_dir=args.tb_dir) + + train_loader = get_imagenet_iter_dali("train", args.imagenet_dir, args.batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing) + val_loader = get_imagenet_iter_dali("val", args.imagenet_dir, args.batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing) + + for epoch in range(args.epochs): + train(epoch, model, criterion, optimizer, train_loader, writer, args) + validate(epoch, model, criterion, val_loader, writer, args) + scheduler.step() + + writer.close() diff --git a/examples/nas/spos/supernet.py b/examples/nas/spos/supernet.py new file mode 100644 index 0000000000..3ab717868c --- /dev/null +++ b/examples/nas/spos/supernet.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import argparse +import logging +import random + +import numpy as np +import torch +import torch.nn as nn +from nni.nas.pytorch.callbacks import LRSchedulerCallback +from nni.nas.pytorch.callbacks import ModelCheckpoint +from nni.nas.pytorch.spos import SPOSSupernetTrainingMutator, SPOSSupernetTrainer + +from dataloader import get_imagenet_iter_dali +from network import ShuffleNetV2OneShot, load_and_parse_state_dict +from utils import CrossEntropyLabelSmooth, accuracy + +logger = logging.getLogger("nni.spos.supernet") + +if __name__ == "__main__": + parser = argparse.ArgumentParser("SPOS Supernet Training") + parser.add_argument("--imagenet-dir", type=str, default="./data/imagenet") + parser.add_argument("--load-checkpoint", action="store_true", default=False) + parser.add_argument("--spos-preprocessing", action="store_true", default=False, + help="When true, image values will range from 0 to 255 and use BGR " + "(as in original repo).") + parser.add_argument("--workers", type=int, default=4) + parser.add_argument("--batch-size", type=int, default=768) + parser.add_argument("--epochs", type=int, default=120) + parser.add_argument("--learning-rate", type=float, default=0.5) + parser.add_argument("--momentum", type=float, default=0.9) + parser.add_argument("--weight-decay", type=float, default=4E-5) + parser.add_argument("--label-smooth", type=float, default=0.1) + parser.add_argument("--log-frequency", type=int, default=10) + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--label-smoothing", type=float, default=0.1) + + args = parser.parse_args() + + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + torch.backends.cudnn.deterministic = True + + model = ShuffleNetV2OneShot() + if args.load_checkpoint: + if not args.spos_preprocessing: + logger.warning("You might want to use SPOS preprocessing if you are loading their checkpoints.") + model.load_state_dict(load_and_parse_state_dict()) + model.cuda() + if torch.cuda.device_count() > 1: # exclude last gpu, saving for data preprocessing on gpu + model = nn.DataParallel(model, device_ids=list(range(0, torch.cuda.device_count() - 1))) + mutator = SPOSSupernetTrainingMutator(model, flops_func=model.module.get_candidate_flops, + flops_lb=290E6, flops_ub=360E6) + criterion = CrossEntropyLabelSmooth(1000, args.label_smoothing) + optimizer = torch.optim.SGD(model.parameters(), lr=args.learning_rate, + momentum=args.momentum, weight_decay=args.weight_decay) + scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, + lambda step: (1.0 - step / args.epochs) + if step <= args.epochs else 0, + last_epoch=-1) + train_loader = get_imagenet_iter_dali("train", args.imagenet_dir, args.batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing) + valid_loader = get_imagenet_iter_dali("val", args.imagenet_dir, args.batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing) + trainer = SPOSSupernetTrainer(model, criterion, accuracy, optimizer, + args.epochs, train_loader, valid_loader, + mutator=mutator, batch_size=args.batch_size, + log_frequency=args.log_frequency, workers=args.workers, + callbacks=[LRSchedulerCallback(scheduler), + ModelCheckpoint("./checkpoints")]) + trainer.train() diff --git a/examples/nas/spos/tester.py b/examples/nas/spos/tester.py new file mode 100644 index 0000000000..b31b8f2fab --- /dev/null +++ b/examples/nas/spos/tester.py @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import argparse +import logging +import random +import time +from itertools import cycle + +import nni +import numpy as np +import torch +import torch.nn as nn +from nni.nas.pytorch.classic_nas import get_and_apply_next_architecture +from nni.nas.pytorch.utils import AverageMeterGroup + +from dataloader import get_imagenet_iter_dali +from network import ShuffleNetV2OneShot, load_and_parse_state_dict +from utils import CrossEntropyLabelSmooth, accuracy + +logger = logging.getLogger("nni.spos.tester") + + +def retrain_bn(model, criterion, max_iters, log_freq, loader): + with torch.no_grad(): + logger.info("Clear BN statistics...") + for m in model.modules(): + if isinstance(m, nn.BatchNorm2d): + m.running_mean = torch.zeros_like(m.running_mean) + m.running_var = torch.ones_like(m.running_var) + + logger.info("Train BN with training set (BN sanitize)...") + model.train() + meters = AverageMeterGroup() + for step in range(max_iters): + inputs, targets = next(loader) + logits = model(inputs) + loss = criterion(logits, targets) + metrics = accuracy(logits, targets) + metrics["loss"] = loss.item() + meters.update(metrics) + if step % log_freq == 0 or step + 1 == max_iters: + logger.info("Train Step [%d/%d] %s", step + 1, max_iters, meters) + + +def test_acc(model, criterion, log_freq, loader): + logger.info("Start testing...") + model.eval() + meters = AverageMeterGroup() + start_time = time.time() + with torch.no_grad(): + for step, (inputs, targets) in enumerate(loader): + logits = model(inputs) + loss = criterion(logits, targets) + metrics = accuracy(logits, targets) + metrics["loss"] = loss.item() + meters.update(metrics) + if step % log_freq == 0 or step + 1 == len(loader): + logger.info("Valid Step [%d/%d] time %.3fs acc1 %.4f acc5 %.4f loss %.4f", + step + 1, len(loader), time.time() - start_time, + meters.acc1.avg, meters.acc5.avg, meters.loss.avg) + return meters.acc1.avg + + +def evaluate_acc(model, criterion, args, loader_train, loader_test): + acc_before = test_acc(model, criterion, args.log_frequency, loader_test) + nni.report_intermediate_result(acc_before) + + retrain_bn(model, criterion, args.train_iters, args.log_frequency, loader_train) + acc = test_acc(model, criterion, args.log_frequency, loader_test) + assert isinstance(acc, float) + nni.report_intermediate_result(acc) + nni.report_final_result(acc) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("SPOS Candidate Tester") + parser.add_argument("--imagenet-dir", type=str, default="./data/imagenet") + parser.add_argument("--checkpoint", type=str, default="./data/checkpoint-150000.pth.tar") + parser.add_argument("--spos-preprocessing", action="store_true", default=False, + help="When true, image values will range from 0 to 255 and use BGR " + "(as in original repo).") + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--workers", type=int, default=6) + parser.add_argument("--train-batch-size", type=int, default=128) + parser.add_argument("--train-iters", type=int, default=200) + parser.add_argument("--test-batch-size", type=int, default=512) + parser.add_argument("--log-frequency", type=int, default=10) + + args = parser.parse_args() + + # use a fixed set of image will improve the performance + torch.manual_seed(args.seed) + torch.cuda.manual_seed_all(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + torch.backends.cudnn.deterministic = True + + assert torch.cuda.is_available() + + model = ShuffleNetV2OneShot() + criterion = CrossEntropyLabelSmooth(1000, 0.1) + get_and_apply_next_architecture(model) + model.load_state_dict(load_and_parse_state_dict(filepath=args.checkpoint)) + model.cuda() + + train_loader = get_imagenet_iter_dali("train", args.imagenet_dir, args.train_batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing, + seed=args.seed, device_id=0) + val_loader = get_imagenet_iter_dali("val", args.imagenet_dir, args.test_batch_size, args.workers, + spos_preprocessing=args.spos_preprocessing, shuffle=True, + seed=args.seed, device_id=0) + train_loader = cycle(train_loader) + + evaluate_acc(model, criterion, args, train_loader, val_loader) diff --git a/examples/nas/spos/tuner.py b/examples/nas/spos/tuner.py new file mode 100644 index 0000000000..fb8b9f2aa4 --- /dev/null +++ b/examples/nas/spos/tuner.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from nni.nas.pytorch.spos import SPOSEvolution + +from network import ShuffleNetV2OneShot + + +class EvolutionWithFlops(SPOSEvolution): + """ + This tuner extends the function of evolution tuner, by limiting the flops generated by tuner. + Needs a function to examine the flops. + """ + + def __init__(self, flops_limit=330E6, **kwargs): + super().__init__(**kwargs) + self.model = ShuffleNetV2OneShot() + self.flops_limit = flops_limit + + def _is_legal(self, cand): + if not super()._is_legal(cand): + return False + if self.model.get_candidate_flops(cand) > self.flops_limit: + return False + return True diff --git a/examples/nas/spos/utils.py b/examples/nas/spos/utils.py new file mode 100644 index 0000000000..70ad98b55f --- /dev/null +++ b/examples/nas/spos/utils.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import torch +import torch.nn as nn + + +class CrossEntropyLabelSmooth(nn.Module): + + def __init__(self, num_classes, epsilon): + super(CrossEntropyLabelSmooth, self).__init__() + self.num_classes = num_classes + self.epsilon = epsilon + self.logsoftmax = nn.LogSoftmax(dim=1) + + def forward(self, inputs, targets): + log_probs = self.logsoftmax(inputs) + targets = torch.zeros_like(log_probs).scatter_(1, targets.unsqueeze(1), 1) + targets = (1 - self.epsilon) * targets + self.epsilon / self.num_classes + loss = (-targets * log_probs).mean(0).sum() + return loss + + +def accuracy(output, target, topk=(1, 5)): + """ Computes the precision@k for the specified values of k """ + maxk = max(topk) + batch_size = target.size(0) + + _, pred = output.topk(maxk, 1, True, True) + pred = pred.t() + # one-hot case + if target.ndimension() > 1: + target = target.max(1)[1] + + correct = pred.eq(target.view(1, -1).expand_as(pred)) + + res = dict() + for k in topk: + correct_k = correct[:k].view(-1).float().sum(0) + res["acc{}".format(k)] = correct_k.mul_(1.0 / batch_size).item() + return res diff --git a/src/sdk/pynni/nni/nas/pytorch/spos/__init__.py b/src/sdk/pynni/nni/nas/pytorch/spos/__init__.py new file mode 100644 index 0000000000..ed432b0845 --- /dev/null +++ b/src/sdk/pynni/nni/nas/pytorch/spos/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .evolution import SPOSEvolution +from .mutator import SPOSSupernetTrainingMutator +from .trainer import SPOSSupernetTrainer diff --git a/src/sdk/pynni/nni/nas/pytorch/spos/evolution.py b/src/sdk/pynni/nni/nas/pytorch/spos/evolution.py new file mode 100644 index 0000000000..3541c81fd7 --- /dev/null +++ b/src/sdk/pynni/nni/nas/pytorch/spos/evolution.py @@ -0,0 +1,222 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json +import logging +import os +import re +from collections import deque + +import numpy as np +from nni.tuner import Tuner +from nni.nas.pytorch.classic_nas.mutator import LAYER_CHOICE, INPUT_CHOICE + + +_logger = logging.getLogger(__name__) + + +class SPOSEvolution(Tuner): + + def __init__(self, max_epochs=20, num_select=10, num_population=50, m_prob=0.1, + num_crossover=25, num_mutation=25): + """ + Initialize SPOS Evolution Tuner. + + Parameters + ---------- + max_epochs : int + Maximum number of epochs to run. + num_select : int + Number of survival candidates of each epoch. + num_population : int + Number of candidates at the start of each epoch. If candidates generated by + crossover and mutation are not enough, the rest will be filled with random + candidates. + m_prob : float + The probability of mutation. + num_crossover : int + Number of candidates generated by crossover in each epoch. + num_mutation : int + Number of candidates generated by mutation in each epoch. + """ + assert num_population >= num_select + self.max_epochs = max_epochs + self.num_select = num_select + self.num_population = num_population + self.m_prob = m_prob + self.num_crossover = num_crossover + self.num_mutation = num_mutation + self.epoch = 0 + self.candidates = [] + self.search_space = None + self.random_state = np.random.RandomState(0) + + # async status + self._to_evaluate_queue = deque() + self._sending_parameter_queue = deque() + self._pending_result_ids = set() + self._reward_dict = dict() + self._id2candidate = dict() + self._st_callback = None + + def update_search_space(self, search_space): + """ + Handle the initialization/update event of search space. + """ + self._search_space = search_space + self._next_round() + + def _next_round(self): + _logger.info("Epoch %d, generating...", self.epoch) + if self.epoch == 0: + self._get_random_population() + self.export_results(self.candidates) + else: + best_candidates = self._select_top_candidates() + self.export_results(best_candidates) + if self.epoch >= self.max_epochs: + return + self.candidates = self._get_mutation(best_candidates) + self._get_crossover(best_candidates) + self._get_random_population() + self.epoch += 1 + + def _random_candidate(self): + chosen_arch = dict() + for key, val in self._search_space.items(): + if val["_type"] == LAYER_CHOICE: + choices = val["_value"] + index = self.random_state.randint(len(choices)) + chosen_arch[key] = {"_value": choices[index], "_idx": index} + elif val["_type"] == INPUT_CHOICE: + raise NotImplementedError("Input choice is not implemented yet.") + return chosen_arch + + def _add_to_evaluate_queue(self, cand): + _logger.info("Generate candidate %s, adding to eval queue.", self._get_architecture_repr(cand)) + self._reward_dict[self._hashcode(cand)] = 0. + self._to_evaluate_queue.append(cand) + + def _get_random_population(self): + while len(self.candidates) < self.num_population: + cand = self._random_candidate() + if self._is_legal(cand): + _logger.info("Random candidate generated.") + self._add_to_evaluate_queue(cand) + self.candidates.append(cand) + + def _get_crossover(self, best): + result = [] + for _ in range(10 * self.num_crossover): + cand_p1 = best[self.random_state.randint(len(best))] + cand_p2 = best[self.random_state.randint(len(best))] + assert cand_p1.keys() == cand_p2.keys() + cand = {k: cand_p1[k] if self.random_state.randint(2) == 0 else cand_p2[k] + for k in cand_p1.keys()} + if self._is_legal(cand): + result.append(cand) + self._add_to_evaluate_queue(cand) + if len(result) >= self.num_crossover: + break + _logger.info("Found %d architectures with crossover.", len(result)) + return result + + def _get_mutation(self, best): + result = [] + for _ in range(10 * self.num_mutation): + cand = best[self.random_state.randint(len(best))].copy() + mutation_sample = np.random.random_sample(len(cand)) + for s, k in zip(mutation_sample, cand): + if s < self.m_prob: + choices = self._search_space[k]["_value"] + index = self.random_state.randint(len(choices)) + cand[k] = {"_value": choices[index], "_idx": index} + if self._is_legal(cand): + result.append(cand) + self._add_to_evaluate_queue(cand) + if len(result) >= self.num_mutation: + break + _logger.info("Found %d architectures with mutation.", len(result)) + return result + + def _get_architecture_repr(self, cand): + return re.sub(r"\".*?\": \{\"_idx\": (\d+), \"_value\": \".*?\"\}", r"\1", + self._hashcode(cand)) + + def _is_legal(self, cand): + if self._hashcode(cand) in self._reward_dict: + return False + return True + + def _select_top_candidates(self): + reward_query = lambda cand: self._reward_dict[self._hashcode(cand)] + _logger.info("All candidate rewards: %s", list(map(reward_query, self.candidates))) + result = sorted(self.candidates, key=reward_query, reverse=True)[:self.num_select] + _logger.info("Best candidate rewards: %s", list(map(reward_query, result))) + return result + + @staticmethod + def _hashcode(d): + return json.dumps(d, sort_keys=True) + + def _bind_and_send_parameters(self): + """ + There are two types of resources: parameter ids and candidates. This function is called at + necessary times to bind these resources to send new trials with st_callback. + """ + result = [] + while self._sending_parameter_queue and self._to_evaluate_queue: + parameter_id = self._sending_parameter_queue.popleft() + parameters = self._to_evaluate_queue.popleft() + self._id2candidate[parameter_id] = parameters + result.append(parameters) + self._pending_result_ids.add(parameter_id) + self._st_callback(parameter_id, parameters) + _logger.info("Send parameter [%d] %s.", parameter_id, self._get_architecture_repr(parameters)) + return result + + def generate_multiple_parameters(self, parameter_id_list, **kwargs): + """ + Callback function necessary to implement a tuner. This will put more parameter ids into the + parameter id queue. + """ + if "st_callback" in kwargs and self._st_callback is None: + self._st_callback = kwargs["st_callback"] + for parameter_id in parameter_id_list: + self._sending_parameter_queue.append(parameter_id) + self._bind_and_send_parameters() + return [] # always not use this. might induce problem of over-sending + + def receive_trial_result(self, parameter_id, parameters, value, **kwargs): + """ + Callback function. Receive a trial result. + """ + _logger.info("Candidate %d, reported reward %f", parameter_id, value) + self._reward_dict[self._hashcode(self._id2candidate[parameter_id])] = value + + def trial_end(self, parameter_id, success, **kwargs): + """ + Callback function when a trial is ended and resource is released. + """ + self._pending_result_ids.remove(parameter_id) + if not self._pending_result_ids and not self._to_evaluate_queue: + # a new epoch now + self._next_round() + assert self._st_callback is not None + self._bind_and_send_parameters() + + def export_results(self, result): + """ + Export a number of candidates to `checkpoints` dir. + + Parameters + ---------- + result : dict + """ + os.makedirs("checkpoints", exist_ok=True) + for i, cand in enumerate(result): + converted = dict() + for cand_key, cand_val in cand.items(): + onehot = [k == cand_val["_idx"] for k in range(len(self._search_space[cand_key]["_value"]))] + converted[cand_key] = onehot + with open(os.path.join("checkpoints", "%03d_%03d.json" % (self.epoch, i)), "w") as fp: + json.dump(converted, fp) diff --git a/src/sdk/pynni/nni/nas/pytorch/spos/mutator.py b/src/sdk/pynni/nni/nas/pytorch/spos/mutator.py new file mode 100644 index 0000000000..88a01eeeaf --- /dev/null +++ b/src/sdk/pynni/nni/nas/pytorch/spos/mutator.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import logging + +import numpy as np +from nni.nas.pytorch.random import RandomMutator + +_logger = logging.getLogger(__name__) + + +class SPOSSupernetTrainingMutator(RandomMutator): + def __init__(self, model, flops_func=None, flops_lb=None, flops_ub=None, + flops_bin_num=7, flops_sample_timeout=500): + """ + + Parameters + ---------- + model : nn.Module + flops_func : callable + Callable that takes a candidate from `sample_search` and returns its candidate. When `flops_func` + is None, functions related to flops will be deactivated. + flops_lb : number + Lower bound of flops. + flops_ub : number + Upper bound of flops. + flops_bin_num : number + Number of bins divided for the interval of flops to ensure the uniformity. Bigger number will be more + uniform, but the sampling will be slower. + flops_sample_timeout : int + Maximum number of attempts to sample before giving up and use a random candidate. + """ + super().__init__(model) + self._flops_func = flops_func + if self._flops_func is not None: + self._flops_bin_num = flops_bin_num + self._flops_bins = [flops_lb + (flops_ub - flops_lb) / flops_bin_num * i for i in range(flops_bin_num + 1)] + self._flops_sample_timeout = flops_sample_timeout + + def sample_search(self): + """ + Sample a candidate for training. When `flops_func` is not None, candidates will be sampled uniformly + relative to flops. + + Returns + ------- + dict + """ + if self._flops_func is not None: + for times in range(self._flops_sample_timeout): + idx = np.random.randint(self._flops_bin_num) + cand = super().sample_search() + if self._flops_bins[idx] <= self._flops_func(cand) <= self._flops_bins[idx + 1]: + _logger.debug("Sampled candidate flops %f in %d times.", cand, times) + return cand + _logger.warning("Failed to sample a flops-valid candidate within %d tries.", self._flops_sample_timeout) + return super().sample_search() + + def sample_final(self): + """ + Implement only to suffice the interface of Mutator. + """ + return self.sample_search() diff --git a/src/sdk/pynni/nni/nas/pytorch/spos/trainer.py b/src/sdk/pynni/nni/nas/pytorch/spos/trainer.py new file mode 100644 index 0000000000..ab23760bf9 --- /dev/null +++ b/src/sdk/pynni/nni/nas/pytorch/spos/trainer.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import logging + +import torch +from nni.nas.pytorch.trainer import Trainer +from nni.nas.pytorch.utils import AverageMeterGroup + +from .mutator import SPOSSupernetTrainingMutator + +logger = logging.getLogger(__name__) + + +class SPOSSupernetTrainer(Trainer): + """ + This trainer trains a supernet that can be used for evolution search. + """ + + def __init__(self, model, loss, metrics, + optimizer, num_epochs, train_loader, valid_loader, + mutator=None, batch_size=64, workers=4, device=None, log_frequency=None, + callbacks=None): + assert torch.cuda.is_available() + super().__init__(model, mutator if mutator is not None else SPOSSupernetTrainingMutator(model), + loss, metrics, optimizer, num_epochs, None, None, + batch_size, workers, device, log_frequency, callbacks) + + self.train_loader = train_loader + self.valid_loader = valid_loader + + def train_one_epoch(self, epoch): + self.model.train() + meters = AverageMeterGroup() + for step, (x, y) in enumerate(self.train_loader): + self.optimizer.zero_grad() + self.mutator.reset() + logits = self.model(x) + loss = self.loss(logits, y) + loss.backward() + self.optimizer.step() + + metrics = self.metrics(logits, y) + metrics["loss"] = loss.item() + meters.update(metrics) + if self.log_frequency is not None and step % self.log_frequency == 0: + logger.info("Epoch [%s/%s] Step [%s/%s] %s", epoch + 1, + self.num_epochs, step + 1, len(self.train_loader), meters) + + def validate_one_epoch(self, epoch): + self.model.eval() + meters = AverageMeterGroup() + with torch.no_grad(): + for step, (x, y) in enumerate(self.valid_loader): + self.mutator.reset() + logits = self.model(x) + loss = self.loss(logits, y) + metrics = self.metrics(logits, y) + metrics["loss"] = loss.item() + meters.update(metrics) + if self.log_frequency is not None and step % self.log_frequency == 0: + logger.info("Epoch [%s/%s] Validation Step [%s/%s] %s", epoch + 1, + self.num_epochs, step + 1, len(self.valid_loader), meters) From 80a49a102e5c8165f47a73a4b9a28b5ec9b94740 Mon Sep 17 00:00:00 2001 From: QuanluZhang Date: Tue, 24 Dec 2019 12:04:59 +0800 Subject: [PATCH 2/4] update (#1871) --- tools/nni_cmd/config_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nni_cmd/config_schema.py b/tools/nni_cmd/config_schema.py index 413f7b94a2..8017946ce9 100644 --- a/tools/nni_cmd/config_schema.py +++ b/tools/nni_cmd/config_schema.py @@ -65,7 +65,7 @@ def setPathCheck(key): 'builtinTunerName': 'SMAC', Optional('classArgs'): { 'optimize_mode': setChoice('optimize_mode', 'maximize', 'minimize'), - 'config_dedup': setType('config_dedup', bool) + Optional('config_dedup'): setType('config_dedup', bool) }, Optional('includeIntermediateResults'): setType('includeIntermediateResults', bool), Optional('gpuIndices'): Or(int, And(str, lambda x: len([int(i) for i in x.split(',')]) > 0), error='gpuIndex format error!'), From b0c0eb7b1f2ac9a983c550ee971cea275463d8fc Mon Sep 17 00:00:00 2001 From: Tang Lang Date: Tue, 24 Dec 2019 12:40:08 +0800 Subject: [PATCH 3/4] Add network trimming pruning algorithm and fix bias mask(testing) (#1867) --- .../Compressor/ActivationRankFilterPruner.md | 58 +++ docs/en_US/Compressor/Overview.md | 8 +- docs/en_US/Compressor/Pruner.md | 120 +++++- ...terPruner.md => WeightRankFilterPruner.md} | 40 +- docs/img/apoz.png | Bin 0 -> 36987 bytes docs/img/fpgm_fig1.png | Bin 0 -> 114044 bytes examples/model_compress/APoZ_torch_cifar10.py | 121 ++++++ .../MeanActivation_torch_cifar10.py | 121 ++++++ .../nni/compression/torch/builtin_pruners.py | 360 +++++++++++++++--- .../pynni/nni/compression/torch/compressor.py | 30 +- .../nni/compression/torch/lottery_ticket.py | 12 +- src/sdk/pynni/tests/test_compressor.py | 20 +- 12 files changed, 793 insertions(+), 97 deletions(-) create mode 100644 docs/en_US/Compressor/ActivationRankFilterPruner.md rename docs/en_US/Compressor/{L1FilterPruner.md => WeightRankFilterPruner.md} (52%) create mode 100644 docs/img/apoz.png create mode 100644 docs/img/fpgm_fig1.png create mode 100644 examples/model_compress/APoZ_torch_cifar10.py create mode 100644 examples/model_compress/MeanActivation_torch_cifar10.py diff --git a/docs/en_US/Compressor/ActivationRankFilterPruner.md b/docs/en_US/Compressor/ActivationRankFilterPruner.md new file mode 100644 index 0000000000..7c836cb140 --- /dev/null +++ b/docs/en_US/Compressor/ActivationRankFilterPruner.md @@ -0,0 +1,58 @@ +ActivationRankFilterPruner on NNI Compressor +=== + +## 1. Introduction + +ActivationRankFilterPruner is a series of pruners which prune filters according to some importance criterion calculated from the filters' output activations. + +| Pruner | Importance criterion | Reference paper | +| :----------------------------: | :-------------------------------: | :----------------------------------------------------------: | +| ActivationAPoZRankFilterPruner | APoZ(average percentage of zeros) | [Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/abs/1607.03250) | +| ActivationMeanRankFilterPruner | mean value of output activations | [Pruning Convolutional Neural Networks for Resource Efficient Inference](https://arxiv.org/abs/1611.06440) | + +## 2. Pruners + +### ActivationAPoZRankFilterPruner + +Hengyuan Hu, Rui Peng, Yu-Wing Tai and Chi-Keung Tang, + +"[Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/abs/1607.03250)", ICLR 2016. + +ActivationAPoZRankFilterPruner prunes the filters with the smallest APoZ(average percentage of zeros) of output activations. + +The APoZ is defined as: + +![](../../img/apoz.png) + +### ActivationMeanRankFilterPruner + +Pavlo Molchanov, Stephen Tyree, Tero Karras, Timo Aila and Jan Kautz, + +"[Pruning Convolutional Neural Networks for Resource Efficient Inference](https://arxiv.org/abs/1611.06440)", ICLR 2017. + +ActivationMeanRankFilterPruner prunes the filters with the smallest mean value of output activations + +## 3. Usage + +PyTorch code + +```python +from nni.compression.torch import ActivationAPoZRankFilterPruner +config_list = [{ 'sparsity': 0.8, 'op_types': ['Conv2d'], 'op_names': ['conv1', 'conv2'] }] +pruner = ActivationAPoZRankFilterPruner(model, config_list, statistics_batch_num=1) +pruner.compress() +``` + +#### User configuration for ActivationAPoZRankFilterPruner + +- **sparsity:** This is to specify the sparsity operations to be compressed to +- **op_types:** Only Conv2d is supported in ActivationAPoZRankFilterPruner + +## 4. Experiment + +TODO. + + + + + diff --git a/docs/en_US/Compressor/Overview.md b/docs/en_US/Compressor/Overview.md index 3782c578e7..f277de5c0f 100644 --- a/docs/en_US/Compressor/Overview.md +++ b/docs/en_US/Compressor/Overview.md @@ -14,10 +14,14 @@ We have provided several compression algorithms, including several pruning and q |---|---| | [Level Pruner](./Pruner.md#level-pruner) | Pruning the specified ratio on each weight based on absolute values of weights | | [AGP Pruner](./Pruner.md#agp-pruner) | Automated gradual pruning (To prune, or not to prune: exploring the efficacy of pruning for model compression) [Reference Paper](https://arxiv.org/abs/1710.01878)| -| [L1Filter Pruner](./Pruner.md#l1filter-pruner) | Pruning least important filters in convolution layers(PRUNING FILTERS FOR EFFICIENT CONVNETS)[Reference Paper](https://arxiv.org/abs/1608.08710) | -| [Slim Pruner](./Pruner.md#slim-pruner) | Pruning channels in convolution layers by pruning scaling factors in BN layers(Learning Efficient Convolutional Networks through Network Slimming)[Reference Paper](https://arxiv.org/abs/1708.06519) | | [Lottery Ticket Pruner](./Pruner.md#agp-pruner) | The pruning process used by "The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks". It prunes a model iteratively. [Reference Paper](https://arxiv.org/abs/1803.03635)| | [FPGM Pruner](./Pruner.md#fpgm-pruner) | Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration [Reference Paper](https://arxiv.org/pdf/1811.00250.pdf)| +| [L1Filter Pruner](./Pruner.md#l1filter-pruner) | Pruning filters with the smallest L1 norm of weights in convolution layers(PRUNING FILTERS FOR EFFICIENT CONVNETS)[Reference Paper](https://arxiv.org/abs/1608.08710) | +| [L2Filter Pruner](./Pruner.md#l2filter-pruner) | Pruning filters with the smallest L2 norm of weights in convolution layers | +| [ActivationAPoZRankFilterPruner](./Pruner.md#ActivationAPoZRankFilterPruner) | Pruning filters prunes the filters with the smallest APoZ(average percentage of zeros) of output activations(Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures)[Reference Paper](https://arxiv.org/abs/1607.03250) | +| [ActivationMeanRankFilterPruner](./Pruner.md#ActivationMeanRankFilterPruner) | Pruning filters prunes the filters with the smallest mean value of output activations(Pruning Convolutional Neural Networks for Resource Efficient Inference)[Reference Paper](https://arxiv.org/abs/1611.06440) | +| [Slim Pruner](./Pruner.md#slim-pruner) | Pruning channels in convolution layers by pruning scaling factors in BN layers(Learning Efficient Convolutional Networks through Network Slimming)[Reference Paper](https://arxiv.org/abs/1708.06519) | + **Quantization** diff --git a/docs/en_US/Compressor/Pruner.md b/docs/en_US/Compressor/Pruner.md index 298ade1d1f..a96414edae 100644 --- a/docs/en_US/Compressor/Pruner.md +++ b/docs/en_US/Compressor/Pruner.md @@ -10,7 +10,7 @@ We first sort the weights in the specified layer by their absolute values. And t ### Usage Tensorflow code -``` +```python from nni.compression.tensorflow import LevelPruner config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }] pruner = LevelPruner(model_graph, config_list) @@ -18,7 +18,7 @@ pruner.compress() ``` PyTorch code -``` +```python from nni.compression.torch import LevelPruner config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }] pruner = LevelPruner(model, config_list) @@ -40,8 +40,6 @@ This is an iterative pruner, In [To prune, or not to prune: exploring the effica ### Usage You can prune all weight from 0% to 80% sparsity in 10 epoch with the code below. -First, you should import pruner and add mask to model. - Tensorflow code ```python from nni.compression.tensorflow import AGP_Pruner @@ -71,7 +69,7 @@ pruner = AGP_Pruner(model, config_list) pruner.compress() ``` -Second, you should add code below to update epoch number when you finish one epoch in your training code. +you should add code below to update epoch number when you finish one epoch in your training code. Tensorflow code ```python @@ -133,13 +131,16 @@ The above configuration means that there are 5 times of iterative pruning. As th * **sparsity:** The final sparsity when the compression is done. *** -## FPGM Pruner +## WeightRankFilterPruner +WeightRankFilterPruner is a series of pruners which prune the filters with the smallest importance criterion calculated from the weights in convolution layers to achieve a preset level of network sparsity + +### 1, FPGM Pruner + This is an one-shot pruner, FPGM Pruner is an implementation of paper [Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration](https://arxiv.org/pdf/1811.00250.pdf) >Previous works utilized “smaller-norm-less-important” criterion to prune filters with smaller norm values in a convolutional neural network. In this paper, we analyze this norm-based criterion and point out that its effectiveness depends on two requirements that are not always met: (1) the norm deviation of the filters should be large; (2) the minimum norm of the filters should be small. To solve this problem, we propose a novel filter pruning method, namely Filter Pruning via Geometric Median (FPGM), to compress the model regardless of those two requirements. Unlike previous methods, FPGM compresses CNN models by pruning filters with redundancy, rather than those with “relatively less” importance. -### Usage -First, you should import pruner and add mask to model. +#### Usage Tensorflow code ```python @@ -163,7 +164,7 @@ pruner.compress() ``` Note: FPGM Pruner is used to prune convolutional layers within deep neural networks, therefore the `op_types` field supports only convolutional layers. -Second, you should add code below to update epoch number at beginning of each epoch. +you should add code below to update epoch number at beginning of each epoch. Tensorflow code ```python @@ -180,7 +181,7 @@ You can view example for more information *** -## L1Filter Pruner +### 2, L1Filter Pruner This is an one-shot pruner, In ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710), authors Hao Li, Asim Kadav, Igor Durdanovic, Hanan Samet and Hans Peter Graf. @@ -193,12 +194,16 @@ This is an one-shot pruner, In ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https: > 1. For each filter ![](http://latex.codecogs.com/gif.latex?F_{i,j}), calculate the sum of its absolute kernel weights![](http://latex.codecogs.com/gif.latex?s_j=\sum_{l=1}^{n_i}\sum|K_l|) > 2. Sort the filters by ![](http://latex.codecogs.com/gif.latex?s_j). > 3. Prune ![](http://latex.codecogs.com/gif.latex?m) filters with the smallest sum values and their corresponding feature maps. The -> kernels in the next convolutional layer corresponding to the pruned feature maps are also -> removed. +> kernels in the next convolutional layer corresponding to the pruned feature maps are also +> removed. > 4. A new kernel matrix is created for both the ![](http://latex.codecogs.com/gif.latex?i)th and ![](http://latex.codecogs.com/gif.latex?i+1)th layers, and the remaining kernel -> weights are copied to the new model. +> weights are copied to the new model. -``` +#### Usage + +PyTorch code + +```python from nni.compression.torch import L1FilterPruner config_list = [{ 'sparsity': 0.8, 'op_types': ['Conv2d'] }] pruner = L1FilterPruner(model, config_list) @@ -208,7 +213,90 @@ pruner.compress() #### User configuration for L1Filter Pruner - **sparsity:** This is to specify the sparsity operations to be compressed to -- **op_types:** Only Conv2d is supported in L1Filter Pruner +- **op_types:** Only Conv1d and Conv2d is supported in L1Filter Pruner + +*** + +### 3, L2Filter Pruner + +This is a structured pruning algorithm that prunes the filters with the smallest L2 norm of the weights. + +#### Usage + +PyTorch code + +```python +from nni.compression.torch import L2FilterPruner +config_list = [{ 'sparsity': 0.8, 'op_types': ['Conv2d'] }] +pruner = L2FilterPruner(model, config_list) +pruner.compress() +``` + +#### User configuration for L2Filter Pruner + +- **sparsity:** This is to specify the sparsity operations to be compressed to +- **op_types:** Only Conv1d and Conv2d is supported in L2Filter Pruner + +## ActivationRankFilterPruner +ActivationRankFilterPruner is a series of pruners which prune the filters with the smallest importance criterion calculated from the output activations of convolution layers to achieve a preset level of network sparsity + +### 1, ActivationAPoZRankFilterPruner + +This is an one-shot pruner, ActivationAPoZRankFilterPruner is an implementation of paper [Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/abs/1607.03250) + +#### Usage + +PyTorch code + +```python +from nni.compression.torch import ActivationAPoZRankFilterPruner +config_list = [{ + 'sparsity': 0.5, + 'op_types': ['Conv2d'] +}] +pruner = ActivationAPoZRankFilterPruner(model, config_list, statistics_batch_num=1) +pruner.compress() +``` + +Note: ActivationAPoZRankFilterPruner is used to prune convolutional layers within deep neural networks, therefore the `op_types` field supports only convolutional layers. + +You can view example for more information + +#### User configuration for ActivationAPoZRankFilterPruner + +- **sparsity:** How much percentage of convolutional filters are to be pruned. +- **op_types:** Only Conv2d is supported in ActivationAPoZRankFilterPruner + +*** + +### 2, ActivationMeanRankFilterPruner + +This is an one-shot pruner, ActivationMeanRankFilterPruner is an implementation of paper [Pruning Convolutional Neural Networks for Resource Efficient Inference](https://arxiv.org/abs/1611.06440) + +#### Usage + +PyTorch code + +```python +from nni.compression.torch import ActivationMeanRankFilterPruner +config_list = [{ + 'sparsity': 0.5, + 'op_types': ['Conv2d'] +}] +pruner = ActivationMeanRankFilterPruner(model, config_list) +pruner.compress() +``` + +Note: ActivationMeanRankFilterPruner is used to prune convolutional layers within deep neural networks, therefore the `op_types` field supports only convolutional layers. + +You can view example for more information + +#### User configuration for ActivationMeanRankFilterPruner + +- **sparsity:** How much percentage of convolutional filters are to be pruned. +- **op_types:** Only Conv2d is supported in ActivationMeanRankFilterPruner + +*** ## Slim Pruner @@ -222,7 +310,7 @@ This is an one-shot pruner, In ['Learning Efficient Convolutional Networks throu PyTorch code -``` +```python from nni.compression.torch import SlimPruner config_list = [{ 'sparsity': 0.8, 'op_types': ['BatchNorm2d'] }] pruner = SlimPruner(model, config_list) diff --git a/docs/en_US/Compressor/L1FilterPruner.md b/docs/en_US/Compressor/WeightRankFilterPruner.md similarity index 52% rename from docs/en_US/Compressor/L1FilterPruner.md rename to docs/en_US/Compressor/WeightRankFilterPruner.md index 2906fde271..ef99dcff03 100644 --- a/docs/en_US/Compressor/L1FilterPruner.md +++ b/docs/en_US/Compressor/WeightRankFilterPruner.md @@ -1,8 +1,20 @@ -L1FilterPruner on NNI Compressor +WeightRankFilterPruner on NNI Compressor === ## 1. Introduction +WeightRankFilterPruner is a series of pruners which prune filters according to some importance criterion calculated from the filters' weight. + +| Pruner | Importance criterion | Reference paper | +| :------------: | :-------------------------: | :----------------------------------------------------------: | +| L1FilterPruner | L1 norm of weights | [PRUNING FILTERS FOR EFFICIENT CONVNETS](https://arxiv.org/abs/1608.08710) | +| L2FilterPruner | L2 norm of weights | | +| FPGMPruner | Geometric Median of weights | [Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration](https://arxiv.org/pdf/1811.00250.pdf) | + +## 2. Pruners + +### L1FilterPruner + L1FilterPruner is a general structured pruning algorithm for pruning filters in the convolutional layers. In ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710), authors Hao Li, Asim Kadav, Igor Durdanovic, Hanan Samet and Hans Peter Graf. @@ -16,12 +28,26 @@ In ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710), > 1. For each filter ![](http://latex.codecogs.com/gif.latex?F_{i,j}), calculate the sum of its absolute kernel weights![](http://latex.codecogs.com/gif.latex?s_j=\sum_{l=1}^{n_i}\sum|K_l|) > 2. Sort the filters by ![](http://latex.codecogs.com/gif.latex?s_j). > 3. Prune ![](http://latex.codecogs.com/gif.latex?m) filters with the smallest sum values and their corresponding feature maps. The -> kernels in the next convolutional layer corresponding to the pruned feature maps are also -> removed. +> kernels in the next convolutional layer corresponding to the pruned feature maps are also +> removed. > 4. A new kernel matrix is created for both the ![](http://latex.codecogs.com/gif.latex?i)th and ![](http://latex.codecogs.com/gif.latex?i+1)th layers, and the remaining kernel -> weights are copied to the new model. +> weights are copied to the new model. + +### L2FilterPruner + +L2FilterPruner is similar to L1FilterPruner, but only replace the importance criterion from L1 norm to L2 norm + +### FPGMPruner + +Yang He, Ping Liu, Ziwei Wang, Zhilan Hu, Yi Yang + +"[Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration](https://arxiv.org/abs/1811.00250)", CVPR 2019. + +FPGMPruner prune filters with the smallest geometric median + + ![](../../img/fpgm_fig1.png) -## 2. Usage +## 3. Usage PyTorch code @@ -37,9 +63,9 @@ pruner.compress() - **sparsity:** This is to specify the sparsity operations to be compressed to - **op_types:** Only Conv2d is supported in L1Filter Pruner -## 3. Experiment +## 4. Experiment -We implemented one of the experiments in ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710), we pruned **VGG-16** for CIFAR-10 to **VGG-16-pruned-A** in the paper, in which $64\%$ parameters are pruned. Our experiments results are as follows: +We implemented one of the experiments in ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710) with **L1FilterPruner**, we pruned **VGG-16** for CIFAR-10 to **VGG-16-pruned-A** in the paper, in which $64\%$ parameters are pruned. Our experiments results are as follows: | Model | Error(paper/ours) | Parameters | Pruned | | --------------- | ----------------- | --------------- | -------- | diff --git a/docs/img/apoz.png b/docs/img/apoz.png new file mode 100644 index 0000000000000000000000000000000000000000..e0c452e97879b2d83ade86a9c90b0aa688436e9b GIT binary patch literal 36987 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 X;o8X+ow*-OjK|8{&aB?VH|c)>`dQk+ literal 0 HcmV?d00001 diff --git a/examples/model_compress/APoZ_torch_cifar10.py b/examples/model_compress/APoZ_torch_cifar10.py new file mode 100644 index 0000000000..52bcf8ffd3 --- /dev/null +++ b/examples/model_compress/APoZ_torch_cifar10.py @@ -0,0 +1,121 @@ +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import datasets, transforms +from nni.compression.torch import ActivationAPoZRankFilterPruner +from models.cifar10.vgg import VGG + + +def train(model, device, train_loader, optimizer): + model.train() + for batch_idx, (data, target) in enumerate(train_loader): + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.cross_entropy(output, target) + loss.backward() + optimizer.step() + if batch_idx % 100 == 0: + print('{:2.0f}% Loss {}'.format(100 * batch_idx / len(train_loader), loss.item())) + + +def test(model, device, test_loader): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + test_loss += F.nll_loss(output, target, reduction='sum').item() + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + acc = 100 * correct / len(test_loader.dataset) + + print('Loss: {} Accuracy: {}%)\n'.format( + test_loss, acc)) + return acc + + +def main(): + torch.manual_seed(0) + device = torch.device('cuda') + train_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=True, download=True, + transform=transforms.Compose([ + transforms.Pad(4), + transforms.RandomCrop(32), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=64, shuffle=True) + test_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=False, transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=200, shuffle=False) + + model = VGG(depth=16) + model.to(device) + + # Train the base VGG-16 model + print('=' * 10 + 'Train the unpruned base model' + '=' * 10) + optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4) + lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, 160, 0) + for epoch in range(160): + train(model, device, train_loader, optimizer) + test(model, device, test_loader) + lr_scheduler.step(epoch) + torch.save(model.state_dict(), 'vgg16_cifar10.pth') + + # Test base model accuracy + print('=' * 10 + 'Test on the original model' + '=' * 10) + model.load_state_dict(torch.load('vgg16_cifar10.pth')) + test(model, device, test_loader) + # top1 = 93.51% + + # Pruning Configuration, in paper 'PRUNING FILTERS FOR EFFICIENT CONVNETS', + # Conv_1, Conv_8, Conv_9, Conv_10, Conv_11, Conv_12 are pruned with 50% sparsity, as 'VGG-16-pruned-A' + configure_list = [{ + 'sparsity': 0.5, + 'op_types': ['default'], + 'op_names': ['feature.0', 'feature.24', 'feature.27', 'feature.30', 'feature.34', 'feature.37'] + }] + + # Prune model and test accuracy without fine tuning. + print('=' * 10 + 'Test on the pruned model before fine tune' + '=' * 10) + pruner = ActivationAPoZRankFilterPruner(model, configure_list) + model = pruner.compress() + test(model, device, test_loader) + # top1 = 88.19% + + # Fine tune the pruned model for 40 epochs and test accuracy + print('=' * 10 + 'Fine tuning' + '=' * 10) + optimizer_finetune = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4) + best_top1 = 0 + for epoch in range(40): + pruner.update_epoch(epoch) + print('# Epoch {} #'.format(epoch)) + train(model, device, train_loader, optimizer_finetune) + top1 = test(model, device, test_loader) + if top1 > best_top1: + best_top1 = top1 + # Export the best model, 'model_path' stores state_dict of the pruned model, + # mask_path stores mask_dict of the pruned model + pruner.export_model(model_path='pruned_vgg16_cifar10.pth', mask_path='mask_vgg16_cifar10.pth') + + # Test the exported model + print('=' * 10 + 'Test on the pruned model after fine tune' + '=' * 10) + new_model = VGG(depth=16) + new_model.to(device) + new_model.load_state_dict(torch.load('pruned_vgg16_cifar10.pth')) + test(new_model, device, test_loader) + # top1 = 93.53% + + +if __name__ == '__main__': + main() diff --git a/examples/model_compress/MeanActivation_torch_cifar10.py b/examples/model_compress/MeanActivation_torch_cifar10.py new file mode 100644 index 0000000000..40ad2bb023 --- /dev/null +++ b/examples/model_compress/MeanActivation_torch_cifar10.py @@ -0,0 +1,121 @@ +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import datasets, transforms +from nni.compression.torch import L1FilterPruner +from models.cifar10.vgg import VGG + + +def train(model, device, train_loader, optimizer): + model.train() + for batch_idx, (data, target) in enumerate(train_loader): + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.cross_entropy(output, target) + loss.backward() + optimizer.step() + if batch_idx % 100 == 0: + print('{:2.0f}% Loss {}'.format(100 * batch_idx / len(train_loader), loss.item())) + + +def test(model, device, test_loader): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + test_loss += F.nll_loss(output, target, reduction='sum').item() + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + acc = 100 * correct / len(test_loader.dataset) + + print('Loss: {} Accuracy: {}%)\n'.format( + test_loss, acc)) + return acc + + +def main(): + torch.manual_seed(0) + device = torch.device('cuda') + train_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=True, download=True, + transform=transforms.Compose([ + transforms.Pad(4), + transforms.RandomCrop(32), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=64, shuffle=True) + test_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=False, transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=200, shuffle=False) + + model = VGG(depth=16) + model.to(device) + + # Train the base VGG-16 model + print('=' * 10 + 'Train the unpruned base model' + '=' * 10) + optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4) + lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, 160, 0) + for epoch in range(160): + train(model, device, train_loader, optimizer) + test(model, device, test_loader) + lr_scheduler.step(epoch) + torch.save(model.state_dict(), 'vgg16_cifar10.pth') + + # Test base model accuracy + print('=' * 10 + 'Test on the original model' + '=' * 10) + model.load_state_dict(torch.load('vgg16_cifar10.pth')) + test(model, device, test_loader) + # top1 = 93.51% + + # Pruning Configuration, in paper 'PRUNING FILTERS FOR EFFICIENT CONVNETS', + # Conv_1, Conv_8, Conv_9, Conv_10, Conv_11, Conv_12 are pruned with 50% sparsity, as 'VGG-16-pruned-A' + configure_list = [{ + 'sparsity': 0.5, + 'op_types': ['default'], + 'op_names': ['feature.0', 'feature.24', 'feature.27', 'feature.30', 'feature.34', 'feature.37'] + }] + + # Prune model and test accuracy without fine tuning. + print('=' * 10 + 'Test on the pruned model before fine tune' + '=' * 10) + pruner = L1FilterPruner(model, configure_list) + model = pruner.compress() + test(model, device, test_loader) + # top1 = 88.19% + + # Fine tune the pruned model for 40 epochs and test accuracy + print('=' * 10 + 'Fine tuning' + '=' * 10) + optimizer_finetune = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4) + best_top1 = 0 + for epoch in range(40): + pruner.update_epoch(epoch) + print('# Epoch {} #'.format(epoch)) + train(model, device, train_loader, optimizer_finetune) + top1 = test(model, device, test_loader) + if top1 > best_top1: + best_top1 = top1 + # Export the best model, 'model_path' stores state_dict of the pruned model, + # mask_path stores mask_dict of the pruned model + pruner.export_model(model_path='pruned_vgg16_cifar10.pth', mask_path='mask_vgg16_cifar10.pth') + + # Test the exported model + print('=' * 10 + 'Test on the pruned model after fine tune' + '=' * 10) + new_model = VGG(depth=16) + new_model.to(device) + new_model.load_state_dict(torch.load('pruned_vgg16_cifar10.pth')) + test(new_model, device, test_loader) + # top1 = 93.53% + + +if __name__ == '__main__': + main() diff --git a/src/sdk/pynni/nni/compression/torch/builtin_pruners.py b/src/sdk/pynni/nni/compression/torch/builtin_pruners.py index b31a8dd77f..8e19ea394d 100644 --- a/src/sdk/pynni/nni/compression/torch/builtin_pruners.py +++ b/src/sdk/pynni/nni/compression/torch/builtin_pruners.py @@ -5,7 +5,8 @@ import torch from .compressor import Pruner -__all__ = ['LevelPruner', 'AGP_Pruner', 'SlimPruner', 'L1FilterPruner', 'L2FilterPruner', 'FPGMPruner'] +__all__ = ['LevelPruner', 'AGP_Pruner', 'SlimPruner', 'L1FilterPruner', 'L2FilterPruner', 'FPGMPruner', + 'ActivationAPoZRankFilterPruner', 'ActivationMeanRankFilterPruner'] logger = logging.getLogger('torch pruner') @@ -26,7 +27,7 @@ def __init__(self, model, config_list): """ super().__init__(model, config_list) - self.if_init_list = {} + self.mask_calculated_ops = set() def calc_mask(self, layer, config): """ @@ -39,22 +40,24 @@ def calc_mask(self, layer, config): layer's pruning config Returns ------- - torch.Tensor - mask of the layer's weight + dict + dictionary for storing masks """ weight = layer.module.weight.data op_name = layer.name - if self.if_init_list.get(op_name, True): + if op_name not in self.mask_calculated_ops: w_abs = weight.abs() k = int(weight.numel() * config['sparsity']) if k == 0: return torch.ones(weight.shape).type_as(weight) threshold = torch.topk(w_abs.view(-1), k, largest=False)[0].max() - mask = torch.gt(w_abs, threshold).type_as(weight) + mask_weight = torch.gt(w_abs, threshold).type_as(weight) + mask = {'weight': mask_weight} self.mask_dict.update({op_name: mask}) - self.if_init_list.update({op_name: False}) + self.mask_calculated_ops.add(op_name) else: + assert op_name in self.mask_dict, "op_name not in the mask_dict" mask = self.mask_dict[op_name] return mask @@ -94,8 +97,8 @@ def calc_mask(self, layer, config): layer's pruning config Returns ------- - torch.Tensor - mask of the layer's weight + dict + dictionary for storing masks """ weight = layer.module.weight.data @@ -104,7 +107,7 @@ def calc_mask(self, layer, config): freq = config.get('frequency', 1) if self.now_epoch >= start_epoch and self.if_init_list.get(op_name, True) \ and (self.now_epoch - start_epoch) % freq == 0: - mask = self.mask_dict.get(op_name, torch.ones(weight.shape).type_as(weight)) + mask = self.mask_dict.get(op_name, {'weight': torch.ones(weight.shape).type_as(weight)}) target_sparsity = self.compute_target_sparsity(config) k = int(weight.numel() * target_sparsity) if k == 0 or target_sparsity >= 1 or target_sparsity <= 0: @@ -112,11 +115,11 @@ def calc_mask(self, layer, config): # if we want to generate new mask, we should update weigth first w_abs = weight.abs() * mask threshold = torch.topk(w_abs.view(-1), k, largest=False)[0].max() - new_mask = torch.gt(w_abs, threshold).type_as(weight) + new_mask = {'weight': torch.gt(w_abs, threshold).type_as(weight)} self.mask_dict.update({op_name: new_mask}) self.if_init_list.update({op_name: False}) else: - new_mask = self.mask_dict.get(op_name, torch.ones(weight.shape).type_as(weight)) + new_mask = self.mask_dict.get(op_name, {'weight': torch.ones(weight.shape).type_as(weight)}) return new_mask def compute_target_sparsity(self, config): @@ -208,8 +211,8 @@ def calc_mask(self, layer, config): layer's pruning config Returns ------- - torch.Tensor - mask of the layer's weight + dict + dictionary for storing masks """ weight = layer.module.weight.data @@ -219,10 +222,17 @@ def calc_mask(self, layer, config): if op_name in self.mask_calculated_ops: assert op_name in self.mask_dict return self.mask_dict.get(op_name) - mask = torch.ones(weight.size()).type_as(weight) + base_mask = torch.ones(weight.size()).type_as(weight).detach() + mask = {'weight': base_mask.detach(), 'bias': base_mask.clone().detach()} try: + filters = weight.size(0) + num_prune = int(filters * config.get('sparsity')) + if filters < 2 or num_prune < 1: + return mask w_abs = weight.abs() - mask = torch.gt(w_abs, self.global_threshold).type_as(weight) + mask_weight = torch.gt(w_abs, self.global_threshold).type_as(weight) + mask_bias = mask_weight.clone() + mask = {'weight': mask_weight.detach(), 'bias': mask_bias.detach()} finally: self.mask_dict.update({layer.name: mask}) self.mask_calculated_ops.add(layer.name) @@ -230,7 +240,7 @@ def calc_mask(self, layer, config): return mask -class RankFilterPruner(Pruner): +class WeightRankFilterPruner(Pruner): """ A structured pruning base class that prunes the filters with the smallest importance criterion in convolution layers to achieve a preset level of network sparsity. @@ -248,10 +258,10 @@ def __init__(self, model, config_list): """ super().__init__(model, config_list) - self.mask_calculated_ops = set() + self.mask_calculated_ops = set() # operations whose mask has been calculated def _get_mask(self, base_mask, weight, num_prune): - return torch.ones(weight.size()).type_as(weight) + return {'weight': None, 'bias': None} def calc_mask(self, layer, config): """ @@ -265,20 +275,25 @@ def calc_mask(self, layer, config): layer's pruning config Returns ------- - torch.Tensor - mask of the layer's weight + dict + dictionary for storing masks """ weight = layer.module.weight.data op_name = layer.name op_type = layer.type - assert 0 <= config.get('sparsity') < 1 - assert op_type in ['Conv1d', 'Conv2d'] + assert 0 <= config.get('sparsity') < 1, "sparsity must in the range [0, 1)" + assert op_type in ['Conv1d', 'Conv2d'], "only support Conv1d and Conv2d" assert op_type in config.get('op_types') if op_name in self.mask_calculated_ops: assert op_name in self.mask_dict return self.mask_dict.get(op_name) - mask = torch.ones(weight.size()).type_as(weight) + mask_weight = torch.ones(weight.size()).type_as(weight).detach() + if hasattr(layer.module, 'bias') and layer.module.bias is not None: + mask_bias = torch.ones(layer.module.bias.size()).type_as(layer.module.bias).detach() + else: + mask_bias = None + mask = {'weight': mask_weight, 'bias': mask_bias} try: filters = weight.size(0) num_prune = int(filters * config.get('sparsity')) @@ -288,10 +303,10 @@ def calc_mask(self, layer, config): finally: self.mask_dict.update({op_name: mask}) self.mask_calculated_ops.add(op_name) - return mask.detach() + return mask -class L1FilterPruner(RankFilterPruner): +class L1FilterPruner(WeightRankFilterPruner): """ A structured pruning algorithm that prunes the filters of smallest magnitude weights sum in the convolution layers to achieve a preset level of network sparsity. @@ -319,31 +334,33 @@ def _get_mask(self, base_mask, weight, num_prune): Filters with the smallest sum of its absolute kernel weights are masked. Parameters ---------- - base_mask : torch.Tensor - The basic mask with the same shape of weight, all item in the basic mask is 1. + base_mask : dict + The basic mask with the same shape of weight or bias, all item in the basic mask is 1. weight : torch.Tensor Layer's weight num_prune : int Num of filters to prune + Returns ------- - torch.Tensor - Mask of the layer's weight + dict + dictionary for storing masks """ filters = weight.shape[0] w_abs = weight.abs() w_abs_structured = w_abs.view(filters, -1).sum(dim=1) threshold = torch.topk(w_abs_structured.view(-1), num_prune, largest=False)[0].max() - mask = torch.gt(w_abs_structured, threshold)[:, None, None, None].expand_as(weight).type_as(weight) + mask_weight = torch.gt(w_abs_structured, threshold)[:, None, None, None].expand_as(weight).type_as(weight) + mask_bias = torch.gt(w_abs_structured, threshold).type_as(weight) - return mask + return {'weight': mask_weight.detach(), 'bias': mask_bias.detach()} -class L2FilterPruner(RankFilterPruner): +class L2FilterPruner(WeightRankFilterPruner): """ A structured pruning algorithm that prunes the filters with the - smallest L2 norm of the absolute kernel weights are masked. + smallest L2 norm of the weights. """ def __init__(self, model, config_list): @@ -365,27 +382,28 @@ def _get_mask(self, base_mask, weight, num_prune): Filters with the smallest L2 norm of the absolute kernel weights are masked. Parameters ---------- - base_mask : torch.Tensor - The basic mask with the same shape of weight, all item in the basic mask is 1. + base_mask : dict + The basic mask with the same shape of weight or bias, all item in the basic mask is 1. weight : torch.Tensor Layer's weight num_prune : int Num of filters to prune Returns ------- - torch.Tensor - Mask of the layer's weight + dict + dictionary for storing masks """ filters = weight.shape[0] w = weight.view(filters, -1) w_l2_norm = torch.sqrt((w ** 2).sum(dim=1)) threshold = torch.topk(w_l2_norm.view(-1), num_prune, largest=False)[0].max() - mask = torch.gt(w_l2_norm, threshold)[:, None, None, None].expand_as(weight).type_as(weight) + mask_weight = torch.gt(w_l2_norm, threshold)[:, None, None, None].expand_as(weight).type_as(weight) + mask_bias = torch.gt(w_l2_norm, threshold).type_as(weight) - return mask + return {'weight': mask_weight.detach(), 'bias': mask_bias.detach()} -class FPGMPruner(RankFilterPruner): +class FPGMPruner(WeightRankFilterPruner): """ A filter pruner via geometric median. "Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration", @@ -410,20 +428,22 @@ def _get_mask(self, base_mask, weight, num_prune): Filters with the smallest sum of its absolute kernel weights are masked. Parameters ---------- - base_mask : torch.Tensor - The basic mask with the same shape of weight, all item in the basic mask is 1. + base_mask : dict + The basic mask with the same shape of weight and bias, all item in the basic mask is 1. weight : torch.Tensor Layer's weight num_prune : int Num of filters to prune Returns ------- - torch.Tensor - Mask of the layer's weight + dict + dictionary for storing masks """ min_gm_idx = self._get_min_gm_kernel_idx(weight, num_prune) for idx in min_gm_idx: - base_mask[idx] = 0. + base_mask['weight'][idx] = 0. + if base_mask['bias'] is not None: + base_mask['bias'][idx] = 0. return base_mask def _get_min_gm_kernel_idx(self, weight, n): @@ -471,3 +491,251 @@ def _get_distance_sum(self, weight, in_idx, out_idx): def update_epoch(self, epoch): self.mask_calculated_ops = set() + + +class ActivationRankFilterPruner(Pruner): + """ + A structured pruning base class that prunes the filters with the smallest + importance criterion in convolution layers to achieve a preset level of network sparsity. + Hengyuan Hu, Rui Peng, Yu-Wing Tai and Chi-Keung Tang, + "Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures", ICLR 2016. + https://arxiv.org/abs/1607.03250 + Pavlo Molchanov, Stephen Tyree, Tero Karras, Timo Aila and Jan Kautz, + "Pruning Convolutional Neural Networks for Resource Efficient Inference", ICLR 2017. + https://arxiv.org/abs/1611.06440 + """ + + def __init__(self, model, config_list, activation='relu', statistics_batch_num=1): + """ + Parameters + ---------- + model : torch.nn.module + Model to be pruned + config_list : list + support key for each list item: + - sparsity: percentage of convolutional filters to be pruned. + activation : str + Activation function + statistics_batch_num : int + Num of batches for activation statistics + """ + + super().__init__(model, config_list) + self.mask_calculated_ops = set() + self.statistics_batch_num = statistics_batch_num + self.collected_activation = {} + self.hooks = {} + assert activation in ['relu', 'relu6'] + if activation == 'relu': + self.activation = torch.nn.functional.relu + elif activation == 'relu6': + self.activation = torch.nn.functional.relu6 + else: + self.activation = None + + def compress(self): + """ + Compress the model, register a hook for collecting activations. + """ + modules_to_compress = self.detect_modules_to_compress() + for layer, config in modules_to_compress: + self._instrument_layer(layer, config) + self.collected_activation[layer.name] = [] + + def _hook(module_, input_, output, name=layer.name): + if len(self.collected_activation[name]) < self.statistics_batch_num: + self.collected_activation[name].append(self.activation(output.detach().cpu())) + + layer.module.register_forward_hook(_hook) + return self.bound_model + + def _get_mask(self, base_mask, activations, num_prune): + return {'weight': None, 'bias': None} + + def calc_mask(self, layer, config): + """ + Calculate the mask of given layer. + Filters with the smallest importance criterion which is calculated from the activation are masked. + + Parameters + ---------- + layer : LayerInfo + the layer to instrument the compression operation + config : dict + layer's pruning config + + Returns + ------- + dict + dictionary for storing masks + """ + + weight = layer.module.weight.data + op_name = layer.name + op_type = layer.type + assert 0 <= config.get('sparsity') < 1, "sparsity must in the range [0, 1)" + assert op_type in ['Conv2d'], "only support Conv2d" + assert op_type in config.get('op_types') + if op_name in self.mask_calculated_ops: + assert op_name in self.mask_dict + return self.mask_dict.get(op_name) + mask_weight = torch.ones(weight.size()).type_as(weight).detach() + if hasattr(layer.module, 'bias') and layer.module.bias is not None: + mask_bias = torch.ones(layer.module.bias.size()).type_as(layer.module.bias).detach() + else: + mask_bias = None + mask = {'weight': mask_weight, 'bias': mask_bias} + try: + filters = weight.size(0) + num_prune = int(filters * config.get('sparsity')) + if filters < 2 or num_prune < 1 or len(self.collected_activation[layer.name]) < self.statistics_batch_num: + return mask + mask = self._get_mask(mask, self.collected_activation[layer.name], num_prune) + finally: + if len(self.collected_activation[layer.name]) == self.statistics_batch_num: + self.mask_dict.update({op_name: mask}) + self.mask_calculated_ops.add(op_name) + return mask + + +class ActivationAPoZRankFilterPruner(ActivationRankFilterPruner): + """ + A structured pruning algorithm that prunes the filters with the + smallest APoZ(average percentage of zeros) of output activations. + Hengyuan Hu, Rui Peng, Yu-Wing Tai and Chi-Keung Tang, + "Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures", ICLR 2016. + https://arxiv.org/abs/1607.03250 + """ + + def __init__(self, model, config_list, activation='relu', statistics_batch_num=1): + """ + Parameters + ---------- + model : torch.nn.module + Model to be pruned + config_list : list + support key for each list item: + - sparsity: percentage of convolutional filters to be pruned. + activation : str + Activation function + statistics_batch_num : int + Num of batches for activation statistics + """ + super().__init__(model, config_list, activation, statistics_batch_num) + + def _get_mask(self, base_mask, activations, num_prune): + """ + Calculate the mask of given layer. + Filters with the smallest APoZ(average percentage of zeros) of output activations are masked. + + Parameters + ---------- + base_mask : dict + The basic mask with the same shape of weight, all item in the basic mask is 1. + activations : list + Layer's output activations + num_prune : int + Num of filters to prune + + Returns + ------- + dict + dictionary for storing masks + """ + apoz = self._calc_apoz(activations) + prune_indices = torch.argsort(apoz, descending=True)[:num_prune] + for idx in prune_indices: + base_mask['weight'][idx] = 0. + if base_mask['bias'] is not None: + base_mask['bias'][idx] = 0. + return base_mask + + def _calc_apoz(self, activations): + """ + Calculate APoZ(average percentage of zeros) of activations. + + Parameters + ---------- + activations : list + Layer's output activations + + Returns + ------- + torch.Tensor + Filter's APoZ(average percentage of zeros) of the activations + """ + activations = torch.cat(activations, 0) + _eq_zero = torch.eq(activations, torch.zeros_like(activations)) + _apoz = torch.sum(_eq_zero, dim=(0, 2, 3)) / torch.numel(_eq_zero[:, 0, :, :]) + return _apoz + + +class ActivationMeanRankFilterPruner(ActivationRankFilterPruner): + """ + A structured pruning algorithm that prunes the filters with the + smallest mean value of output activations. + Pavlo Molchanov, Stephen Tyree, Tero Karras, Timo Aila and Jan Kautz, + "Pruning Convolutional Neural Networks for Resource Efficient Inference", ICLR 2017. + https://arxiv.org/abs/1611.06440 + """ + + def __init__(self, model, config_list, activation='relu', statistics_batch_num=1): + """ + Parameters + ---------- + model : torch.nn.module + Model to be pruned + config_list : list + support key for each list item: + - sparsity: percentage of convolutional filters to be pruned. + activation : str + Activation function + statistics_batch_num : int + Num of batches for activation statistics + """ + super().__init__(model, config_list, activation, statistics_batch_num) + + def _get_mask(self, base_mask, activations, num_prune): + """ + Calculate the mask of given layer. + Filters with the smallest APoZ(average percentage of zeros) of output activations are masked. + + Parameters + ---------- + base_mask : dict + The basic mask with the same shape of weight, all item in the basic mask is 1. + activations : list + Layer's output activations + num_prune : int + Num of filters to prune + + Returns + ------- + dict + dictionary for storing masks + """ + mean_activation = self._cal_mean_activation(activations) + prune_indices = torch.argsort(mean_activation)[:num_prune] + for idx in prune_indices: + base_mask['weight'][idx] = 0. + if base_mask['bias'] is not None: + base_mask['bias'][idx] = 0. + return base_mask + + def _cal_mean_activation(self, activations): + """ + Calculate mean value of activations. + + Parameters + ---------- + activations : list + Layer's output activations + + Returns + ------- + torch.Tensor + Filter's mean value of the output activations + """ + activations = torch.cat(activations, 0) + mean_activation = torch.mean(activations, dim=(0, 2, 3)) + return mean_activation diff --git a/src/sdk/pynni/nni/compression/torch/compressor.py b/src/sdk/pynni/nni/compression/torch/compressor.py index e7965d837b..d8ae199d43 100644 --- a/src/sdk/pynni/nni/compression/torch/compressor.py +++ b/src/sdk/pynni/nni/compression/torch/compressor.py @@ -16,6 +16,7 @@ def __init__(self, name, module): self._forward = None + class Compressor: """ Abstract base PyTorch compressor @@ -193,10 +194,16 @@ def _instrument_layer(self, layer, config): layer._forward = layer.module.forward def new_forward(*inputs): + mask = self.calc_mask(layer, config) # apply mask to weight old_weight = layer.module.weight.data - mask = self.calc_mask(layer, config) - layer.module.weight.data = old_weight.mul(mask) + mask_weight = mask['weight'] + layer.module.weight.data = old_weight.mul(mask_weight) + # apply mask to bias + if mask.__contains__('bias') and hasattr(layer.module, 'bias') and layer.module.bias is not None: + old_bias = layer.module.bias.data + mask_bias = mask['bias'] + layer.module.bias.data = old_bias.mul(mask_bias) # calculate forward ret = layer._forward(*inputs) return ret @@ -224,12 +231,14 @@ def export_model(self, model_path, mask_path=None, onnx_path=None, input_shape=N for name, m in self.bound_model.named_modules(): if name == "": continue - mask = self.mask_dict.get(name) - if mask is not None: - mask_sum = mask.sum().item() - mask_num = mask.numel() + masks = self.mask_dict.get(name) + if masks is not None: + mask_sum = masks['weight'].sum().item() + mask_num = masks['weight'].numel() _logger.info('Layer: %s Sparsity: %.2f', name, 1 - mask_sum / mask_num) - m.weight.data = m.weight.data.mul(mask) + m.weight.data = m.weight.data.mul(masks['weight']) + if masks.__contains__('bias') and hasattr(m, 'bias') and m.bias is not None: + m.bias.data = m.bias.data.mul(masks['bias']) else: _logger.info('Layer: %s NOT compressed', name) torch.save(self.bound_model.state_dict(), model_path) @@ -258,7 +267,6 @@ def quantize_weight(self, weight, config, op, op_type, op_name): """ quantize should overload this method to quantize weight. This method is effectively hooked to :meth:`forward` of the model. - Parameters ---------- weight : Tensor @@ -272,7 +280,6 @@ def quantize_output(self, output, config, op, op_type, op_name): """ quantize should overload this method to quantize output. This method is effectively hooked to :meth:`forward` of the model. - Parameters ---------- output : Tensor @@ -286,7 +293,6 @@ def quantize_input(self, *inputs, config, op, op_type, op_name): """ quantize should overload this method to quantize input. This method is effectively hooked to :meth:`forward` of the model. - Parameters ---------- inputs : Tensor @@ -300,7 +306,6 @@ def quantize_input(self, *inputs, config, op, op_type, op_name): def _instrument_layer(self, layer, config): """ Create a wrapper forward function to replace the original one. - Parameters ---------- layer : LayerInfo @@ -365,7 +370,6 @@ def quant_backward(tensor, grad_output, quant_type): """ This method should be overrided by subclass to provide customized backward function, default implementation is Straight-Through Estimator - Parameters ---------- tensor : Tensor @@ -375,7 +379,6 @@ def quant_backward(tensor, grad_output, quant_type): quant_type : QuantType the type of quantization, it can be `QuantType.QUANT_INPUT`, `QuantType.QUANT_WEIGHT`, `QuantType.QUANT_OUTPUT`, you can define different behavior for different types. - Returns ------- tensor @@ -399,3 +402,4 @@ def _check_weight(module): return isinstance(module.weight.data, torch.Tensor) except AttributeError: return False + \ No newline at end of file diff --git a/src/sdk/pynni/nni/compression/torch/lottery_ticket.py b/src/sdk/pynni/nni/compression/torch/lottery_ticket.py index d8e4f78c76..233d90ced8 100644 --- a/src/sdk/pynni/nni/compression/torch/lottery_ticket.py +++ b/src/sdk/pynni/nni/compression/torch/lottery_ticket.py @@ -17,6 +17,7 @@ class LotteryTicketPruner(Pruner): 4. Reset the remaining parameters to their values in theta_0, creating the winning ticket f(x;m*theta_0). 5. Repeat step 2, 3, and 4. """ + def __init__(self, model, config_list, optimizer, lr_scheduler=None, reset_weights=True): """ Parameters @@ -55,7 +56,8 @@ def _validate_config(self, config_list): assert 'prune_iterations' in config, 'prune_iterations must exist in your config' assert 'sparsity' in config, 'sparsity must exist in your config' if prune_iterations is not None: - assert prune_iterations == config['prune_iterations'], 'The values of prune_iterations must be equal in your config' + assert prune_iterations == config[ + 'prune_iterations'], 'The values of prune_iterations must be equal in your config' prune_iterations = config['prune_iterations'] return prune_iterations @@ -67,8 +69,8 @@ def _print_masks(self, print_mask=False): if print_mask: print('mask: ', mask) # calculate current sparsity - mask_num = mask.sum().item() - mask_size = mask.numel() + mask_num = mask['weight'].sum().item() + mask_size = mask['weight'].numel() print('sparsity: ', 1 - mask_num / mask_size) torch.set_printoptions(profile='default') @@ -84,11 +86,11 @@ def _calc_mask(self, weight, sparsity, op_name): curr_sparsity = self._calc_sparsity(sparsity) assert self.mask_dict.get(op_name) is not None curr_mask = self.mask_dict.get(op_name) - w_abs = weight.abs() * curr_mask + w_abs = weight.abs() * curr_mask['weight'] k = int(w_abs.numel() * curr_sparsity) threshold = torch.topk(w_abs.view(-1), k, largest=False).values.max() mask = torch.gt(w_abs, threshold).type_as(weight) - return mask + return {'weight': mask} def calc_mask(self, layer, config): """ diff --git a/src/sdk/pynni/tests/test_compressor.py b/src/sdk/pynni/tests/test_compressor.py index 0632858cec..778f4341e9 100644 --- a/src/sdk/pynni/tests/test_compressor.py +++ b/src/sdk/pynni/tests/test_compressor.py @@ -136,12 +136,12 @@ def test_torch_fpgm_pruner(self): model.conv2.weight.data = torch.tensor(w).float() layer = torch_compressor.compressor.LayerInfo('conv2', model.conv2) masks = pruner.calc_mask(layer, config_list[0]) - assert all(torch.sum(masks, (1, 2, 3)).numpy() == np.array([45., 45., 45., 45., 0., 0., 45., 45., 45., 45.])) + assert all(torch.sum(masks['weight'], (1, 2, 3)).numpy() == np.array([45., 45., 45., 45., 0., 0., 45., 45., 45., 45.])) pruner.update_epoch(1) model.conv2.weight.data = torch.tensor(w).float() masks = pruner.calc_mask(layer, config_list[1]) - assert all(torch.sum(masks, (1, 2, 3)).numpy() == np.array([45., 45., 0., 0., 0., 0., 0., 0., 45., 45.])) + assert all(torch.sum(masks['weight'], (1, 2, 3)).numpy() == np.array([45., 45., 0., 0., 0., 0., 0., 0., 45., 45.])) @tf2 def test_tf_fpgm_pruner(self): @@ -190,8 +190,8 @@ def test_torch_l1filter_pruner(self): mask1 = pruner.calc_mask(layer1, config_list[0]) layer2 = torch_compressor.compressor.LayerInfo('conv2', model.conv2) mask2 = pruner.calc_mask(layer2, config_list[1]) - assert all(torch.sum(mask1, (1, 2, 3)).numpy() == np.array([0., 27., 27., 27., 27.])) - assert all(torch.sum(mask2, (1, 2, 3)).numpy() == np.array([0., 0., 0., 27., 27.])) + assert all(torch.sum(mask1['weight'], (1, 2, 3)).numpy() == np.array([0., 27., 27., 27., 27.])) + assert all(torch.sum(mask2['weight'], (1, 2, 3)).numpy() == np.array([0., 0., 0., 27., 27.])) def test_torch_slim_pruner(self): """ @@ -218,8 +218,10 @@ def test_torch_slim_pruner(self): mask1 = pruner.calc_mask(layer1, config_list[0]) layer2 = torch_compressor.compressor.LayerInfo('bn2', model.bn2) mask2 = pruner.calc_mask(layer2, config_list[0]) - assert all(mask1.numpy() == np.array([0., 1., 1., 1., 1.])) - assert all(mask2.numpy() == np.array([0., 1., 1., 1., 1.])) + assert all(mask1['weight'].numpy() == np.array([0., 1., 1., 1., 1.])) + assert all(mask2['weight'].numpy() == np.array([0., 1., 1., 1., 1.])) + assert all(mask1['bias'].numpy() == np.array([0., 1., 1., 1., 1.])) + assert all(mask2['bias'].numpy() == np.array([0., 1., 1., 1., 1.])) config_list = [{'sparsity': 0.6, 'op_types': ['BatchNorm2d']}] model.bn1.weight.data = torch.tensor(w).float() @@ -230,8 +232,10 @@ def test_torch_slim_pruner(self): mask1 = pruner.calc_mask(layer1, config_list[0]) layer2 = torch_compressor.compressor.LayerInfo('bn2', model.bn2) mask2 = pruner.calc_mask(layer2, config_list[0]) - assert all(mask1.numpy() == np.array([0., 0., 0., 1., 1.])) - assert all(mask2.numpy() == np.array([0., 0., 0., 1., 1.])) + assert all(mask1['weight'].numpy() == np.array([0., 0., 0., 1., 1.])) + assert all(mask2['weight'].numpy() == np.array([0., 0., 0., 1., 1.])) + assert all(mask1['bias'].numpy() == np.array([0., 0., 0., 1., 1.])) + assert all(mask2['bias'].numpy() == np.array([0., 0., 0., 1., 1.])) def test_torch_QAT_quantizer(self): model = TorchModel() From 0c7f22fb8e49d72ce91d65e52e058084ac63b0a3 Mon Sep 17 00:00:00 2001 From: Cjkkkk <656569648@qq.com> Date: Tue, 24 Dec 2019 12:41:14 +0800 Subject: [PATCH 4/4] add BNN quantization algorithm (#1832) --- docs/en_US/Compressor/Quantizer.md | 110 +++++++++++-- .../model_compress/BNN_quantizer_cifar10.py | 155 ++++++++++++++++++ .../compression/torch/builtin_quantizers.py | 34 +++- 3 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 examples/model_compress/BNN_quantizer_cifar10.py diff --git a/docs/en_US/Compressor/Quantizer.md b/docs/en_US/Compressor/Quantizer.md index 5dd99e3432..67791117e1 100644 --- a/docs/en_US/Compressor/Quantizer.md +++ b/docs/en_US/Compressor/Quantizer.md @@ -1,6 +1,5 @@ Quantizer on NNI Compressor === - ## Naive Quantizer We provide Naive Quantizer to quantizer weight to default 8 bits, you can use it to test quantize algorithm without any configure. @@ -53,11 +52,24 @@ You can view example for more information #### User configuration for QAT Quantizer * **quant_types:** : list of string -type of quantization you want to apply, currently support 'weight', 'input', 'output' + +type of quantization you want to apply, currently support 'weight', 'input', 'output'. + +* **op_types:** list of string + +specify the type of modules that will be quantized. eg. 'Conv2D' + +* **op_names:** list of string + +specify the name of modules that will be quantized. eg. 'conv1' + * **quant_bits:** int or dict of {str : int} -bits length of quantization, key is the quantization type, value is the length, eg. {'weight', 8}, -when the type is int, all quantization types share same bits length + +bits length of quantization, key is the quantization type, value is the length, eg. {'weight': 8}, +when the type is int, all quantization types share same bits length. + * **quant_start_step:** int + disable quantization until model are run by certain number of steps, this allows the network to enter a more stable state where activation quantization ranges do not exclude a significant fraction of values, default value is 0 @@ -71,17 +83,14 @@ In [DoReFa-Net: Training Low Bitwidth Convolutional Neural Networks with Low Bit ### Usage To implement DoReFa Quantizer, you can add code below before your training code -Tensorflow code -```python -from nni.compressors.tensorflow import DoReFaQuantizer -config_list = [{ 'q_bits': 8, 'op_types': 'default' }] -quantizer = DoReFaQuantizer(tf.get_default_graph(), config_list) -quantizer.compress() -``` PyTorch code ```python from nni.compressors.torch import DoReFaQuantizer -config_list = [{ 'q_bits': 8, 'op_types': 'default' }] +config_list = [{ + 'quant_types': ['weight'], + 'quant_bits': 8, + 'op_types': 'default' +}] quantizer = DoReFaQuantizer(model, config_list) quantizer.compress() ``` @@ -89,4 +98,79 @@ quantizer.compress() You can view example for more information #### User configuration for DoReFa Quantizer -* **q_bits:** This is to specify the q_bits operations to be quantized to +* **quant_types:** : list of string + +type of quantization you want to apply, currently support 'weight', 'input', 'output'. + +* **op_types:** list of string + +specify the type of modules that will be quantized. eg. 'Conv2D' + +* **op_names:** list of string + +specify the name of modules that will be quantized. eg. 'conv1' + +* **quant_bits:** int or dict of {str : int} + +bits length of quantization, key is the quantization type, value is the length, eg. {'weight': 8}, +when the type is int, all quantization types share same bits length. + + +## BNN Quantizer +In [Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830), + +>We introduce a method to train Binarized Neural Networks (BNNs) - neural networks with binary weights and activations at run-time. At training-time the binary weights and activations are used for computing the parameters gradients. During the forward pass, BNNs drastically reduce memory size and accesses, and replace most arithmetic operations with bit-wise operations, which is expected to substantially improve power-efficiency. + + +### Usage + +PyTorch code +```python +from nni.compression.torch import BNNQuantizer +model = VGG_Cifar10(num_classes=10) + +configure_list = [{ + 'quant_types': ['weight'], + 'quant_bits': 1, + 'op_types': ['Conv2d', 'Linear'], + 'op_names': ['features.0', 'features.3', 'features.7', 'features.10', 'features.14', 'features.17', 'classifier.0', 'classifier.3'] +}, { + 'quant_types': ['output'], + 'quant_bits': 1, + 'op_types': ['Hardtanh'], + 'op_names': ['features.6', 'features.9', 'features.13', 'features.16', 'features.20', 'classifier.2', 'classifier.5'] +}] + +quantizer = BNNQuantizer(model, configure_list) +model = quantizer.compress() +``` + +You can view example [examples/model_compress/BNN_quantizer_cifar10.py]( https://github.com/microsoft/nni/tree/master/examples/model_compress/BNN_quantizer_cifar10.py) for more information. + +#### User configuration for BNN Quantizer +* **quant_types:** : list of string + +type of quantization you want to apply, currently support 'weight', 'input', 'output'. + +* **op_types:** list of string + +specify the type of modules that will be quantized. eg. 'Conv2D' + +* **op_names:** list of string + +specify the name of modules that will be quantized. eg. 'conv1' + +* **quant_bits:** int or dict of {str : int} + +bits length of quantization, key is the quantization type, value is the length, eg. {'weight': 8}, +when the type is int, all quantization types share same bits length. + +### Experiment +We implemented one of the experiments in [Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830), we quantized the **VGGNet** for CIFAR-10 in the paper. Our experiments results are as follows: + +| Model | Accuracy | +| ------------- | --------- | +| VGGNet | 86.93% | + + +The experiments code can be found at [examples/model_compress/BNN_quantizer_cifar10.py]( https://github.com/microsoft/nni/tree/master/examples/model_compress/BNN_quantizer_cifar10.py) \ No newline at end of file diff --git a/examples/model_compress/BNN_quantizer_cifar10.py b/examples/model_compress/BNN_quantizer_cifar10.py new file mode 100644 index 0000000000..d4908885c3 --- /dev/null +++ b/examples/model_compress/BNN_quantizer_cifar10.py @@ -0,0 +1,155 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import datasets, transforms +from nni.compression.torch import BNNQuantizer + + +class VGG_Cifar10(nn.Module): + def __init__(self, num_classes=1000): + super(VGG_Cifar10, self).__init__() + self.features = nn.Sequential( + nn.Conv2d(3, 128, kernel_size=3, padding=1, bias=False), + nn.BatchNorm2d(128, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True), + + nn.Conv2d(128, 128, kernel_size=3, padding=1, bias=False), + nn.MaxPool2d(kernel_size=2, stride=2), + nn.BatchNorm2d(128, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True), + + nn.Conv2d(128, 256, kernel_size=3, padding=1, bias=False), + nn.BatchNorm2d(256, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True), + + + nn.Conv2d(256, 256, kernel_size=3, padding=1, bias=False), + nn.MaxPool2d(kernel_size=2, stride=2), + nn.BatchNorm2d(256, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True), + + + nn.Conv2d(256, 512, kernel_size=3, padding=1, bias=False), + nn.BatchNorm2d(512, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True), + + + nn.Conv2d(512, 512, kernel_size=3, padding=1, bias=False), + nn.MaxPool2d(kernel_size=2, stride=2), + nn.BatchNorm2d(512, eps=1e-4, momentum=0.1), + nn.Hardtanh(inplace=True) + ) + + self.classifier = nn.Sequential( + nn.Linear(512 * 4 * 4, 1024, bias=False), + nn.BatchNorm1d(1024), + nn.Hardtanh(inplace=True), + nn.Linear(1024, 1024, bias=False), + nn.BatchNorm1d(1024), + nn.Hardtanh(inplace=True), + nn.Linear(1024, num_classes), # do not quantize output + nn.BatchNorm1d(num_classes, affine=False) + ) + + + def forward(self, x): + x = self.features(x) + x = x.view(-1, 512 * 4 * 4) + x = self.classifier(x) + return x + + +def train(model, device, train_loader, optimizer): + model.train() + for batch_idx, (data, target) in enumerate(train_loader): + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.cross_entropy(output, target) + loss.backward() + optimizer.step() + for name, param in model.named_parameters(): + if name.endswith('old_weight'): + param = param.clamp(-1, 1) + if batch_idx % 100 == 0: + print('{:2.0f}% Loss {}'.format(100 * batch_idx / len(train_loader), loss.item())) + + +def test(model, device, test_loader): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + test_loss += F.nll_loss(output, target, reduction='sum').item() + pred = output.argmax(dim=1, keepdim=True) + correct += pred.eq(target.view_as(pred)).sum().item() + test_loss /= len(test_loader.dataset) + acc = 100 * correct / len(test_loader.dataset) + + print('Loss: {} Accuracy: {}%)\n'.format( + test_loss, acc)) + return acc + +def adjust_learning_rate(optimizer, epoch): + update_list = [55, 100, 150, 200, 400, 600] + if epoch in update_list: + for param_group in optimizer.param_groups: + param_group['lr'] = param_group['lr'] * 0.1 + return + +def main(): + torch.manual_seed(0) + device = torch.device('cuda') + train_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=True, download=True, + transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=64, shuffle=True) + test_loader = torch.utils.data.DataLoader( + datasets.CIFAR10('./data.cifar10', train=False, transform=transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) + ])), + batch_size=200, shuffle=False) + + model = VGG_Cifar10(num_classes=10) + model.to(device) + + configure_list = [{ + 'quant_types': ['weight'], + 'quant_bits': 1, + 'op_types': ['Conv2d', 'Linear'], + 'op_names': ['features.3', 'features.7', 'features.10', 'features.14', 'classifier.0', 'classifier.3'] + }, { + 'quant_types': ['output'], + 'quant_bits': 1, + 'op_types': ['Hardtanh'], + 'op_names': ['features.6', 'features.9', 'features.13', 'features.16', 'features.20', 'classifier.2', 'classifier.5'] + }] + + quantizer = BNNQuantizer(model, configure_list) + model = quantizer.compress() + + print('=' * 10 + 'train' + '=' * 10) + optimizer = torch.optim.Adam(model.parameters(), lr=1e-2) + best_top1 = 0 + for epoch in range(400): + print('# Epoch {} #'.format(epoch)) + train(model, device, train_loader, optimizer) + adjust_learning_rate(optimizer, epoch) + top1 = test(model, device, test_loader) + if top1 > best_top1: + best_top1 = top1 + print(best_top1) + + +if __name__ == '__main__': + main() diff --git a/src/sdk/pynni/nni/compression/torch/builtin_quantizers.py b/src/sdk/pynni/nni/compression/torch/builtin_quantizers.py index 7f9c3b144a..2204428574 100644 --- a/src/sdk/pynni/nni/compression/torch/builtin_quantizers.py +++ b/src/sdk/pynni/nni/compression/torch/builtin_quantizers.py @@ -3,7 +3,7 @@ import logging import torch -from .compressor import Quantizer +from .compressor import Quantizer, QuantGrad, QuantType __all__ = ['NaiveQuantizer', 'QAT_Quantizer', 'DoReFaQuantizer'] @@ -240,4 +240,34 @@ def quantize_weight(self, weight, config, **kwargs): def quantize(self, input_ri, q_bits): scale = pow(2, q_bits)-1 output = torch.round(input_ri*scale)/scale - return output \ No newline at end of file + return output + + +class ClipGrad(QuantGrad): + @staticmethod + def quant_backward(tensor, grad_output, quant_type): + if quant_type == QuantType.QUANT_OUTPUT: + grad_output[torch.abs(tensor) > 1] = 0 + return grad_output + + +class BNNQuantizer(Quantizer): + """Binarized Neural Networks, as defined in: + Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1 + (https://arxiv.org/abs/1602.02830) + """ + def __init__(self, model, config_list): + super().__init__(model, config_list) + self.quant_grad = ClipGrad + + def quantize_weight(self, weight, config, **kwargs): + out = torch.sign(weight) + # remove zeros + out[out == 0] = 1 + return out + + def quantize_output(self, output, config, **kwargs): + out = torch.sign(output) + # remove zeros + out[out == 0] = 1 + return out