Skip to content

Commit 8317295

Browse files
authored
Add Quantizable MobilenetV3 architecture for Classification (#3323)
* Refactoring mobilenetv3 to make code reusable. * Adding quantizable MobileNetV3 architecture. * Fix bug on reference script. * Moving documentation of quantized models in the right place. * Update documentation. * Workaround for loading correct weights of quant model. * Update weight URL and readme. * Adding eval.
1 parent 17393cb commit 8317295

File tree

7 files changed

+303
-89
lines changed

7 files changed

+303
-89
lines changed

docs/source/models.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,53 @@ MNASNet
263263
.. autofunction:: mnasnet1_0
264264
.. autofunction:: mnasnet1_3
265265

266+
Quantized Models
267+
----------------
268+
269+
The following architectures provide support for INT8 quantized models. You can get
270+
a model with random weights by calling its constructor:
271+
272+
.. code:: python
273+
274+
import torchvision.models as models
275+
googlenet = models.quantization.googlenet()
276+
inception_v3 = models.quantization.inception_v3()
277+
mobilenet_v2 = models.quantization.mobilenet_v2()
278+
mobilenet_v3_large = models.quantization.mobilenet_v3_large()
279+
mobilenet_v3_small = models.quantization.mobilenet_v3_small()
280+
resnet18 = models.quantization.resnet18()
281+
resnet50 = models.quantization.resnet50()
282+
resnext101_32x8d = models.quantization.resnext101_32x8d()
283+
shufflenet_v2_x0_5 = models.quantization.shufflenet_v2_x0_5()
284+
shufflenet_v2_x1_0 = models.quantization.shufflenet_v2_x1_0()
285+
shufflenet_v2_x1_5 = models.quantization.shufflenet_v2_x1_5()
286+
shufflenet_v2_x2_0 = models.quantization.shufflenet_v2_x2_0()
287+
288+
Obtaining a pre-trained quantized model can be done with a few lines of code:
289+
290+
.. code:: python
291+
292+
import torchvision.models as models
293+
model = models.quantization.mobilenet_v2(pretrained=True, quantize=True)
294+
model.eval()
295+
# run the model with quantized inputs and weights
296+
out = model(torch.rand(1, 3, 224, 224))
297+
298+
We provide pre-trained quantized weights for the following models:
299+
300+
================================ ============= =============
301+
Model Acc@1 Acc@5
302+
================================ ============= =============
303+
MobileNet V2 71.658 90.150
304+
MobileNet V3 Large 73.004 90.858
305+
ShuffleNet V2 68.360 87.582
306+
ResNet 18 69.494 88.882
307+
ResNet 50 75.920 92.814
308+
ResNext 101 32x8d 78.986 94.480
309+
Inception V3 77.176 93.354
310+
GoogleNet 69.826 89.404
311+
================================ ============= =============
312+
266313

267314
Semantic Segmentation
268315
=====================

references/classification/README.md

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,27 +74,6 @@ python -m torch.distributed.launch --nproc_per_node=8 --use_env train.py\
7474
```
7575

7676
## Quantized
77-
### INT8 models
78-
We add INT8 quantized models to follow the quantization support added in PyTorch 1.3.
79-
80-
Obtaining a pre-trained quantized model can be obtained with a few lines of code:
81-
```
82-
model = torchvision.models.quantization.mobilenet_v2(pretrained=True, quantize=True)
83-
model.eval()
84-
# run the model with quantized inputs and weights
85-
out = model(torch.rand(1, 3, 224, 224))
86-
```
87-
We provide pre-trained quantized weights for the following models:
88-
89-
| Model | Acc@1 | Acc@5 |
90-
|:-----------------:|:------:|:------:|
91-
| MobileNet V2 | 71.658 | 90.150 |
92-
| ShuffleNet V2: | 68.360 | 87.582 |
93-
| ResNet 18 | 69.494 | 88.882 |
94-
| ResNet 50 | 75.920 | 92.814 |
95-
| ResNext 101 32x8d | 78.986 | 94.480 |
96-
| Inception V3 | 77.176 | 93.354 |
97-
| GoogleNet | 69.826 | 89.404 |
9877

9978
### Parameters used for generating quantized models:
10079

@@ -106,6 +85,10 @@ For all post training quantized models (All quantized models except mobilenet-v2
10685
4. eval_batch_size: 128
10786
5. backend: 'fbgemm'
10887

88+
```
89+
python train_quantization.py --device='cpu' --post-training-quantize --backend='fbgemm' --model='<model_name>'
90+
```
91+
10992
For Mobilenet-v2, the model was trained with quantization aware training, the settings used are:
11093
1. num_workers: 16
11194
2. batch_size: 32
@@ -118,15 +101,38 @@ For Mobilenet-v2, the model was trained with quantization aware training, the se
118101
9. momentum: 0.9
119102
10. lr_step_size:30
120103
11. lr_gamma: 0.1
104+
12. weight-decay: 0.0001
105+
106+
```
107+
python -m torch.distributed.launch --nproc_per_node=8 --use_env train_quantization.py --model='mobilenet_v2'
108+
```
121109

122110
Training converges at about 10 epochs.
123111

124-
For post training quant, device is set to CPU. For training, the device is set to CUDA
112+
For Mobilenet-v3 Large, the model was trained with quantization aware training, the settings used are:
113+
1. num_workers: 16
114+
2. batch_size: 32
115+
3. eval_batch_size: 128
116+
4. backend: 'qnnpack'
117+
5. learning-rate: 0.001
118+
6. num_epochs: 90
119+
7. num_observer_update_epochs:4
120+
8. num_batch_norm_update_epochs:3
121+
9. momentum: 0.9
122+
10. lr_step_size:30
123+
11. lr_gamma: 0.1
124+
12. weight-decay: 0.00001
125+
126+
```
127+
python -m torch.distributed.launch --nproc_per_node=8 --use_env train_quantization.py --model='mobilenet_v3_large' \
128+
--wd 0.00001 --lr 0.001
129+
```
130+
131+
For post training quant, device is set to CPU. For training, the device is set to CUDA.
125132

126133
### Command to evaluate quantized models using the pre-trained weights:
127-
For all quantized models:
134+
128135
```
129-
python references/classification/train_quantization.py --data-path='imagenet_full_size/' \
130-
--device='cpu' --test-only --backend='fbgemm' --model='<model_name>'
136+
python train_quantization.py --device='cpu' --test-only --backend='<backend>' --model='<model_name>'
131137
```
132138

references/classification/train.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,12 @@ def load_data(traindir, valdir, args):
9292
print("Loading dataset_train from {}".format(cache_path))
9393
dataset, _ = torch.load(cache_path)
9494
else:
95+
auto_augment_policy = getattr(args, "auto_augment", None)
96+
random_erase_prob = getattr(args, "random_erase", 0.0)
9597
dataset = torchvision.datasets.ImageFolder(
9698
traindir,
97-
presets.ClassificationPresetTrain(crop_size=crop_size, auto_augment_policy=args.auto_augment,
98-
random_erase_prob=args.random_erase))
99+
presets.ClassificationPresetTrain(crop_size=crop_size, auto_augment_policy=auto_augment_policy,
100+
random_erase_prob=random_erase_prob))
99101
if args.cache_dataset:
100102
print("Saving dataset_train to {}".format(cache_path))
101103
utils.mkdir(os.path.dirname(cache_path))

references/classification/train_quantization.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ def main(args):
3737
train_dir = os.path.join(args.data_path, 'train')
3838
val_dir = os.path.join(args.data_path, 'val')
3939

40-
dataset, dataset_test, train_sampler, test_sampler = load_data(train_dir, val_dir,
41-
args.cache_dataset, args.distributed)
40+
dataset, dataset_test, train_sampler, test_sampler = load_data(train_dir, val_dir, args)
4241
data_loader = torch.utils.data.DataLoader(
4342
dataset, batch_size=args.batch_size,
4443
sampler=train_sampler, num_workers=args.workers, pin_memory=True)

torchvision/models/mobilenetv3.py

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import partial
44
from torch import nn, Tensor
55
from torch.nn import functional as F
6-
from typing import Any, Callable, List, Optional, Sequence
6+
from typing import Any, Callable, Dict, List, Optional, Sequence
77

88
from torchvision.models.utils import load_state_dict_from_url
99
from torchvision.models.mobilenetv2 import _make_divisible, ConvBNActivation
@@ -24,14 +24,18 @@ def __init__(self, input_channels: int, squeeze_factor: int = 4):
2424
super().__init__()
2525
squeeze_channels = _make_divisible(input_channels // squeeze_factor, 8)
2626
self.fc1 = nn.Conv2d(input_channels, squeeze_channels, 1)
27+
self.relu = nn.ReLU(inplace=True)
2728
self.fc2 = nn.Conv2d(squeeze_channels, input_channels, 1)
2829

29-
def forward(self, input: Tensor) -> Tensor:
30+
def _scale(self, input: Tensor, inplace: bool) -> Tensor:
3031
scale = F.adaptive_avg_pool2d(input, 1)
3132
scale = self.fc1(scale)
32-
scale = F.relu(scale, inplace=True)
33+
scale = self.relu(scale)
3334
scale = self.fc2(scale)
34-
scale = F.hardsigmoid(scale, inplace=True)
35+
return F.hardsigmoid(scale, inplace=inplace)
36+
37+
def forward(self, input: Tensor) -> Tensor:
38+
scale = self._scale(input, True)
3539
return scale * input
3640

3741

@@ -55,7 +59,8 @@ def adjust_channels(channels: int, width_mult: float):
5559

5660
class InvertedResidual(nn.Module):
5761

58-
def __init__(self, cnf: InvertedResidualConfig, norm_layer: Callable[..., nn.Module]):
62+
def __init__(self, cnf: InvertedResidualConfig, norm_layer: Callable[..., nn.Module],
63+
se_layer: Callable[..., nn.Module] = SqueezeExcitation):
5964
super().__init__()
6065
if not (1 <= cnf.stride <= 2):
6166
raise ValueError('illegal stride value')
@@ -76,7 +81,7 @@ def __init__(self, cnf: InvertedResidualConfig, norm_layer: Callable[..., nn.Mod
7681
stride=stride, dilation=cnf.dilation, groups=cnf.expanded_channels,
7782
norm_layer=norm_layer, activation_layer=activation_layer))
7883
if cnf.use_se:
79-
layers.append(SqueezeExcitation(cnf.expanded_channels))
84+
layers.append(se_layer(cnf.expanded_channels))
8085

8186
# project
8287
layers.append(ConvBNActivation(cnf.expanded_channels, cnf.out_channels, kernel_size=1, norm_layer=norm_layer,
@@ -179,7 +184,56 @@ def forward(self, x: Tensor) -> Tensor:
179184
return self._forward_impl(x)
180185

181186

182-
def _mobilenet_v3(
187+
def _mobilenet_v3_conf(arch: str, params: Dict[str, Any]):
188+
# non-public config parameters
189+
reduce_divider = 2 if params.pop('_reduced_tail', False) else 1
190+
dilation = 2 if params.pop('_dilated', False) else 1
191+
width_mult = params.pop('_width_mult', 1.0)
192+
193+
bneck_conf = partial(InvertedResidualConfig, width_mult=width_mult)
194+
adjust_channels = partial(InvertedResidualConfig.adjust_channels, width_mult=width_mult)
195+
196+
if arch == "mobilenet_v3_large":
197+
inverted_residual_setting = [
198+
bneck_conf(16, 3, 16, 16, False, "RE", 1, 1),
199+
bneck_conf(16, 3, 64, 24, False, "RE", 2, 1), # C1
200+
bneck_conf(24, 3, 72, 24, False, "RE", 1, 1),
201+
bneck_conf(24, 5, 72, 40, True, "RE", 2, 1), # C2
202+
bneck_conf(40, 5, 120, 40, True, "RE", 1, 1),
203+
bneck_conf(40, 5, 120, 40, True, "RE", 1, 1),
204+
bneck_conf(40, 3, 240, 80, False, "HS", 2, 1), # C3
205+
bneck_conf(80, 3, 200, 80, False, "HS", 1, 1),
206+
bneck_conf(80, 3, 184, 80, False, "HS", 1, 1),
207+
bneck_conf(80, 3, 184, 80, False, "HS", 1, 1),
208+
bneck_conf(80, 3, 480, 112, True, "HS", 1, 1),
209+
bneck_conf(112, 3, 672, 112, True, "HS", 1, 1),
210+
bneck_conf(112, 5, 672, 160 // reduce_divider, True, "HS", 2, dilation), # C4
211+
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1, dilation),
212+
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1, dilation),
213+
]
214+
last_channel = adjust_channels(1280 // reduce_divider) # C5
215+
elif arch == "mobilenet_v3_small":
216+
inverted_residual_setting = [
217+
bneck_conf(16, 3, 16, 16, True, "RE", 2, 1), # C1
218+
bneck_conf(16, 3, 72, 24, False, "RE", 2, 1), # C2
219+
bneck_conf(24, 3, 88, 24, False, "RE", 1, 1),
220+
bneck_conf(24, 5, 96, 40, True, "HS", 2, 1), # C3
221+
bneck_conf(40, 5, 240, 40, True, "HS", 1, 1),
222+
bneck_conf(40, 5, 240, 40, True, "HS", 1, 1),
223+
bneck_conf(40, 5, 120, 48, True, "HS", 1, 1),
224+
bneck_conf(48, 5, 144, 48, True, "HS", 1, 1),
225+
bneck_conf(48, 5, 288, 96 // reduce_divider, True, "HS", 2, dilation), # C4
226+
bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1, dilation),
227+
bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1, dilation),
228+
]
229+
last_channel = adjust_channels(1024 // reduce_divider) # C5
230+
else:
231+
raise ValueError("Unsupported model type {}".format(arch))
232+
233+
return inverted_residual_setting, last_channel
234+
235+
236+
def _mobilenet_v3_model(
183237
arch: str,
184238
inverted_residual_setting: List[InvertedResidualConfig],
185239
last_channel: int,
@@ -205,34 +259,9 @@ def mobilenet_v3_large(pretrained: bool = False, progress: bool = True, **kwargs
205259
pretrained (bool): If True, returns a model pre-trained on ImageNet
206260
progress (bool): If True, displays a progress bar of the download to stderr
207261
"""
208-
# non-public config parameters
209-
reduce_divider = 2 if kwargs.pop('_reduced_tail', False) else 1
210-
dilation = 2 if kwargs.pop('_dilated', False) else 1
211-
width_mult = 1.0
212-
213-
bneck_conf = partial(InvertedResidualConfig, width_mult=width_mult)
214-
adjust_channels = partial(InvertedResidualConfig.adjust_channels, width_mult=width_mult)
215-
216-
inverted_residual_setting = [
217-
bneck_conf(16, 3, 16, 16, False, "RE", 1, 1),
218-
bneck_conf(16, 3, 64, 24, False, "RE", 2, 1), # C1
219-
bneck_conf(24, 3, 72, 24, False, "RE", 1, 1),
220-
bneck_conf(24, 5, 72, 40, True, "RE", 2, 1), # C2
221-
bneck_conf(40, 5, 120, 40, True, "RE", 1, 1),
222-
bneck_conf(40, 5, 120, 40, True, "RE", 1, 1),
223-
bneck_conf(40, 3, 240, 80, False, "HS", 2, 1), # C3
224-
bneck_conf(80, 3, 200, 80, False, "HS", 1, 1),
225-
bneck_conf(80, 3, 184, 80, False, "HS", 1, 1),
226-
bneck_conf(80, 3, 184, 80, False, "HS", 1, 1),
227-
bneck_conf(80, 3, 480, 112, True, "HS", 1, 1),
228-
bneck_conf(112, 3, 672, 112, True, "HS", 1, 1),
229-
bneck_conf(112, 5, 672, 160 // reduce_divider, True, "HS", 2, dilation), # C4
230-
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1, dilation),
231-
bneck_conf(160 // reduce_divider, 5, 960 // reduce_divider, 160 // reduce_divider, True, "HS", 1, dilation),
232-
]
233-
last_channel = adjust_channels(1280 // reduce_divider) # C5
234-
235-
return _mobilenet_v3("mobilenet_v3_large", inverted_residual_setting, last_channel, pretrained, progress, **kwargs)
262+
arch = "mobilenet_v3_large"
263+
inverted_residual_setting, last_channel = _mobilenet_v3_conf(arch, kwargs)
264+
return _mobilenet_v3_model(arch, inverted_residual_setting, last_channel, pretrained, progress, **kwargs)
236265

237266

238267
def mobilenet_v3_small(pretrained: bool = False, progress: bool = True, **kwargs: Any) -> MobileNetV3:
@@ -244,27 +273,6 @@ def mobilenet_v3_small(pretrained: bool = False, progress: bool = True, **kwargs
244273
pretrained (bool): If True, returns a model pre-trained on ImageNet
245274
progress (bool): If True, displays a progress bar of the download to stderr
246275
"""
247-
# non-public config parameters
248-
reduce_divider = 2 if kwargs.pop('_reduced_tail', False) else 1
249-
dilation = 2 if kwargs.pop('_dilated', False) else 1
250-
width_mult = 1.0
251-
252-
bneck_conf = partial(InvertedResidualConfig, width_mult=width_mult)
253-
adjust_channels = partial(InvertedResidualConfig.adjust_channels, width_mult=width_mult)
254-
255-
inverted_residual_setting = [
256-
bneck_conf(16, 3, 16, 16, True, "RE", 2, 1), # C1
257-
bneck_conf(16, 3, 72, 24, False, "RE", 2, 1), # C2
258-
bneck_conf(24, 3, 88, 24, False, "RE", 1, 1),
259-
bneck_conf(24, 5, 96, 40, True, "HS", 2, 1), # C3
260-
bneck_conf(40, 5, 240, 40, True, "HS", 1, 1),
261-
bneck_conf(40, 5, 240, 40, True, "HS", 1, 1),
262-
bneck_conf(40, 5, 120, 48, True, "HS", 1, 1),
263-
bneck_conf(48, 5, 144, 48, True, "HS", 1, 1),
264-
bneck_conf(48, 5, 288, 96 // reduce_divider, True, "HS", 2, dilation), # C4
265-
bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1, dilation),
266-
bneck_conf(96 // reduce_divider, 5, 576 // reduce_divider, 96 // reduce_divider, True, "HS", 1, dilation),
267-
]
268-
last_channel = adjust_channels(1024 // reduce_divider) # C5
269-
270-
return _mobilenet_v3("mobilenet_v3_small", inverted_residual_setting, last_channel, pretrained, progress, **kwargs)
276+
arch = "mobilenet_v3_small"
277+
inverted_residual_setting, last_channel = _mobilenet_v3_conf(arch, kwargs)
278+
return _mobilenet_v3_model(arch, inverted_residual_setting, last_channel, pretrained, progress, **kwargs)
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .mobilenetv2 import QuantizableMobileNetV2, mobilenet_v2, __all__ as mv2_all
2+
from .mobilenetv3 import QuantizableMobileNetV3, mobilenet_v3_large, mobilenet_v3_small, __all__ as mv3_all
23

3-
__all__ = mv2_all
4+
__all__ = mv2_all + mv3_all

0 commit comments

Comments
 (0)