Unverified Commit c5ac923a authored by Chi Song's avatar Chi Song Committed by GitHub
Browse files

Chinese translation (#2458)

parent 93f96d4f
......@@ -19,7 +19,7 @@ NNI 管理自动机器学习 (AutoML) 的 Experiment,**调度运行**由调优
* 想要更容易**实现或试验新的自动机器学习算法**的研究员或数据科学家,包括:超参调优算法,神经网络搜索算法以及模型压缩算法。
* 在机器学习平台中**支持自动机器学习**
### **[NNI v1.5 已发布!](https://github.com/microsoft/nni/releases) &nbsp;[<img width="48" src="docs/img/release_icon.png" />](#nni-released-reminder)**
### **[NNI v1.6 已发布!](https://github.com/microsoft/nni/releases) &nbsp;[<img width="48" src="docs/img/release_icon.png" />](#nni-released-reminder)**
## **NNI 功能一览**
......@@ -84,6 +84,7 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。
<li><a href="docs/zh_CN/TrialExample/Cifar10Examples.md">Cifar10-pytorch</li></a>
<li><a href="docs/zh_CN/TrialExample/SklearnExamples.md">Scikit-learn</a></li>
<li><a href="docs/zh_CN/TrialExample/EfficientNet.md">EfficientNet</a></li>
<li><a href="docs/zh_CN/TrialExample/OpEvoExamples.md">GPU Kernel 调优</li></a>
<a href="docs/zh_CN/SupportedFramework_Library.md">更多...</a><br/>
</ul>
</ul>
......@@ -137,6 +138,10 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。
<li><a href="docs/zh_CN/Compressor/Pruner.md#agp-pruner">AGP Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#slim-pruner">Slim Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#fpgm-pruner">FPGM Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#netadapt-pruner">NetAdapt Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#simulatedannealing-pruner">SimulatedAnnealing Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#admm-pruner">ADMM Pruner</a></li>
<li><a href="docs/zh_CN/Compressor/Pruner.md#autocompress-pruner">AutoCompress Pruner</a></li>
</ul>
<b>量化</b>
<ul>
......@@ -164,7 +169,7 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。
<li><a href="docs/zh_CN/TrainingService/KubeflowMode.md">Kubeflow</a></li>
<li><a href="docs/zh_CN/TrainingService/FrameworkControllerMode.md">基于 Kubernetes(AKS 等)的 FrameworkController</a></li>
</ul>
<ul><li><a href="docs/zh_CN/TrainingService/DLTSMode.md">DLWorkspace (又称 DLTS)</a></li>
<ul><li><a href="docs/zh_CN/TrainingService/DLTSMode.md">DLWorkspace (又称 DLTS)</a></li>
</ul>
</td>
</tr>
......@@ -186,11 +191,12 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。
<ul>
<li><a href="docs/zh_CN/Tuner/CustomizeTuner.md">自定义 Tuner</a></li>
<li><a href="docs/zh_CN/Assessor/CustomizeAssessor.md">自定义 Assessor</a></li>
<li><a href="docs/zh_CN/Tutorial/InstallCustomizedAlgos.md">安装自定义的 Tuner,Assessor,Advisor</a></li>
</ul>
</td>
<td style="border-top:#FF0000 solid 0px;">
<ul>
<li><a href="docs/zh_CN/TrainingService/SupportTrainingService.md">支持训练平台</li>
<li><a href="docs/zh_CN/TrainingService/Overview.md">支持训练平台</li>
<li><a href="docs/zh_CN/TrainingService/HowToImplementTrainingService.md">实现训练平台</a></li>
</ul>
</td>
......@@ -223,7 +229,7 @@ Linux 和 macOS 下 NNI 系统需求[参考这里](https://nni.readthedocs.io/zh
注意:
* 如果遇到任何权限问题,可添加 `--user` 在用户目录中安装 NNI。
* 目前,Windows 上的 NNI 支持本机,远程和 OpenPAI 模式。 强烈推荐使用 Anaconda 或 Miniconda 在 Windows 上安装 NNI。
* 目前,Windows 上的 NNI 支持本机,远程和 OpenPAI 模式。 强烈推荐使用 Anaconda 或 Miniconda [在 Windows 上安装 NNI](docs/zh_CN/Tutorial/InstallationWin.md)
* 如果遇到如 `Segmentation fault` 等错误参考[常见问题](docs/zh_CN/Tutorial/FAQ.md)。 Windows 上的 FAQ 参考[在 Windows 上使用 NNI](docs/zh_CN/Tutorial/InstallationWin.md#faq)
### **验证安装**
......@@ -233,7 +239,7 @@ Linux 和 macOS 下 NNI 系统需求[参考这里](https://nni.readthedocs.io/zh
* 通过克隆源代码下载示例。
```bash
git clone -b v1.5 https://github.com/Microsoft/nni.git
git clone -b v1.6 https://github.com/Microsoft/nni.git
```
* 运行 MNIST 示例。
......@@ -316,8 +322,7 @@ You can use these commands to get more information about the experiment
经作者许可的一些 NNI 用法示例和相关文档。
* ### **外部代码库** ###
* 在 NNI 中运行 [ENAS](examples/tuners/enas_nni/README_zh_CN.md)
* 在 NNI 中运行 [神经网络架构结构搜索](examples/trials/nas_cifar10/README_zh_CN.md)
* 在 NNI 中运行 [ENAS](examples/nas/enas/README_zh_CN.md)
* [NNI 中的自动特征工程](examples/feature_engineering/auto-feature-engineering/README_zh_CN.md)
* 使用 NNI 的 [矩阵分解超参调优](https://github.com/microsoft/recommenders/blob/master/notebooks/04_model_select_and_optimize/nni_surprise_svd.ipynb)
* [scikit-nni](https://github.com/ksachdeva/scikit-nni) 使用 NNI 为 scikit-learn 开发的超参搜索。
......@@ -339,9 +344,9 @@ You can use these commands to get more information about the experiment
加入聊天组:
| Gitter | | 微信 |
| ----------------------------------------------------------------------------------------------------------- | - | ------------------------------------------------------------------------------------------- |
| ![图片](https://user-images.githubusercontent.com/39592018/80665738-e0574a80-8acc-11ea-91bc-0836dc4cbf89.png) | 或 | ![图片](https://github.com/JSong-Jia/NNI-user-group/blob/master/user%20group%20code_0512.jpg) |
| Gitter | | 微信 |
| ----------------------------------------------------------------------------------------------------------- | - | ----------------------------------------------------------------------- |
| ![图片](https://user-images.githubusercontent.com/39592018/80665738-e0574a80-8acc-11ea-91bc-0836dc4cbf89.png) | 或 | ![image](https://github.com/scarlett2018/nniutil/raw/master/wechat.png) |
## 相关项目
......
......@@ -19,7 +19,7 @@ NNI 提供了先进的评估算法,使用上也很简单。 下面是内置 As
<a name="MedianStop"></a>
![](https://placehold.it/15/1589F0/000000?text=+) `Median Stop Assessor`
### Median Stop Assessor
> 名称:**Medianstop**
......@@ -47,20 +47,21 @@ assessor:
<a name="Curvefitting"></a>
![](https://placehold.it/15/1589F0/000000?text=+) `Curve Fitting Assessor`
### Curve Fitting Assessor
> 名称:**Curvefitting**
**建议场景**
适用于各种性能曲线,可用到各种场景中来加速优化过程。 更好的地方是,它能处理并评估性能类似的曲线。 [详细说明](./CurvefittingAssessor.md)
适用于各种性能曲线,可用到各种场景中来加速优化过程。 更好的是,它能够处理并评估性能类似的曲线。 [详细说明](./CurvefittingAssessor.md)
**注意**,根据原始论文,仅支持递增函数。 因此,此 Assessor 仅可用于最大化优化指标的场景。 例如,它可用于准确度,但不能用于损失值。
**classArgs 要求:**
* **epoch_num** (*int, **必需***) - epoch 的总数。 需要此数据来决定需要预测点的总数。
* **optimize_mode** (*maximize 或 minimize, 可选, 默认值为 maximize*) - 如果为 'maximize', Assessor 会在结果小于期望值时**终止** Trial。 如果为 'minimize',Assessor 会在结果大于期望值时**终止** Trial。
* **start_step** (*int, 可选, 默认值为 6*) - 只有收到 start_step 个中间结果后,才开始判断是否一个 Trial 应该被终止。
* **threshold** (*float, 可选, 默认值为 0.95*) - 用来确定提前终止较差结果的阈值。 例如,如果 threshold = 0.95, optimize_mode = maximize,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。
* **threshold** (*float, 可选, 默认值为 0.95*) - 用来确定提前终止较差结果的阈值。 例如,如果 threshold = 0.95,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。
* **gap** (*int, 可选, 默认值为 1*) - Assessor 两次评估之间的间隔次数。 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。
**使用示例:**
......@@ -71,7 +72,6 @@ assessor:
builtinAssessorName: Curvefitting
classArgs:
epoch_num: 20
optimize_mode: maximize
start_step: 6
threshold: 0.95
gap: 1
......
# NNI 中的 Curve Fitting Assessor
## 1. 介绍
## 介绍
Curve Fitting Assessor 是一个 LPA (learning, predicting, assessing,即学习、预测、评估) 的算法。 如果预测的 Trial X 在 step S 比性能最好的 Trial 要差,就会提前终止它。
此算法中采用了 12 种曲线来拟合学习曲线。 这组参数曲线模型来自于[参考论文](http://aad.informatik.uni-freiburg.de/papers/15-IJCAI-Extrapolation_of_Learning_Curves.pdf)。 学习曲线的形状与先验知识是一致的:都是典型的递增的、饱和的函数。
![](../../img/curvefitting_learning_curve.PNG)
![learning_curve](../../img/curvefitting_learning_curve.PNG)
所有学习曲线模型被合并到了单个,更强大的模型中。 合并的模型通过加权线性混合:
![](../../img/curvefitting_f_comb.gif)
![f_comb](../../img/curvefitting_f_comb.gif)
合并后的新参数向量
![](../../img/curvefitting_expression_xi.gif)
![expression_xi](../../img/curvefitting_expression_xi.gif)
假设增加一个高斯噪声,且噪声参数初始化为最大似然估计。
......@@ -30,36 +30,37 @@ Curve Fitting Assessor 是一个 LPA (learning, predicting, assessing,即学
下图显示了此算法在 MNIST Trial 历史数据上结果。其中绿点表示 Assessor 获得的数据,蓝点表示将来,但未知的数据,红色线条是 Curve fitting Assessor 的预测曲线。
![](../../img/curvefitting_example.PNG)
![示例](../../img/curvefitting_example.PNG)
## 2. 用法
## 用法
要使用 Curve Fitting Assessor,需要在 Experiment 的 YAML 配置文件进行如下改动。
assessor:
builtinAssessorName: Curvefitting
classArgs:
# (必须) epoch 的总数。
# 需要此数据来决定需要预测的点。
epoch_num: 20
# (可选) 选项: maximize, minimize
* optimize_mode 的默认值是 maximize
optimize_mode: maximize
# (可选) 为了节约计算资源,在收到了 start_step 个中间结果后,才开始预测。
# start_step 的默认值是 6。
start_step: 6
# (可选) 决定是否提前终止的阈值。
# 例如,如果 threshold = 0.95, optimize_mode = maximize,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。
* 阈值的默认值是 0.95。
# 注意:如果选择了 minimize 模式,要让 threshold >= 1.0 (如 threshold=1.1)
threshold: 0.95
# (可选) gap 是两次评估之间的间隔次数。
# 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。
* gap 的默认值是 1。
gap: 1
## 3. 文件结构
```yaml
assessor:
builtinAssessorName: Curvefitting
classArgs:
# (必须) epoch 的总数。
# 需要此数据来决定需要预测的点。
epoch_num: 20
# (可选) 为了节约计算资源,仅在收到 start_step 个中间结果后,才开始进行预测。
# start_step 的默认值是 6。
start_step: 6
# (可选) 决定是否提前终止的阈值。
# 例如,如果 threshold = 0.95,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。
# 阈值的默认值是 0.95。
threshold: 0.95
# (可选) gap 是两次评估之间的间隔次数。
# 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。
# gap 的默认值是 1。
gap: 1
```
## 局限性
根据原始论文,仅支持递增函数。 因此,此 Assessor 仅可用于最大化优化指标的场景。 例如,它可用于准确度,但不能用于损失值。
## 文件结构
Assessor 有大量的文件、函数和类。 在这里,会简要描述其中一部分。
......@@ -67,6 +68,6 @@ Assessor 有大量的文件、函数和类。 在这里,会简要描述其中
* `modelfactory.py` 包括学习和预测部分,并实现了相应的计算部分。
* `curvefitting_assessor.py` 是接收 Trial 历史数据并评估是否需要提前终止的 Assessor。
## 4. TODO
## TODO
* 进一步提高预测精度,并在更多模型上测试。
\ No newline at end of file
# 模型压缩 Python API 参考
```eval_rst
.. contents::
```
## 灵敏度工具
```eval_rst
.. autoclass:: nni.compression.torch.utils.sensitivity_analysis.SensitivityAnalysis
:members:
```
## 拓扑结构工具
```eval_rst
.. autoclass:: nni.compression.torch.utils.shape_dependency.ChannelDependency
:members:
.. autoclass:: nni.compression.torch.utils.shape_dependency.GroupDependency
:members:
.. autoclass:: nni.compression.torch.utils.mask_conflict.CatMaskPadding
:members:
.. autoclass:: nni.compression.torch.utils.mask_conflict.GroupMaskConflict
:members:
.. autoclass:: nni.compression.torch.utils.mask_conflict.ChannelMaskConflict
:members:
```
## 模型 FLOPs 和参数计数器
```eval_rst
.. autofunction:: nni.compression.torch.utils.counter.count_flops_params
```
\ No newline at end of file
# 模型压缩分析工具
```eval_rst
.. contents::
```
NNI 提供了几种易于使用的工具,在压缩时用于分析模型。
## 灵敏度分析
首先提供的是灵敏度分析工具 (**SensitivityAnalysis**),用于分析模型中每个卷积层的灵敏度。 具体来说,SensitiviyAnalysis 会为每层逐渐剪枝,同时测试模型的精度变化。 注意,敏感度分析一次只会对一层进行剪枝,其它层会使用它们原始的权重。 根据不同稀疏度下不同卷积层的精度,可以很容易的找出模型精度对哪些层的变化更敏感。
### 用法
下列代码是 SensitivityAnalysis 的基本用法。
```python
from nni.compression.torch.utils.sensitivity_analysis import SensitivityAnalysis
def val(model):
model.eval()
total = 0
correct = 0
with torch.no_grad():
for batchid, (data, label) in enumerate(val_loader):
data, label = data.cuda(), label.cuda()
out = model(data)
_, predicted = out.max(1)
total += data.size(0)
correct += predicted.eq(label).sum().item()
return correct / total
s_analyzer = SensitivityAnalysis(model=net, val_func=val)
sensitivity = s_analyzer.analysis(val_args=[net])
os.makedir(outdir)
s_analyzer.export(os.path.join(outdir, filename))
```
SensitivityAnalysis 的两个重要参数是 `model`, 和 `val_func``model` 是要分析的神经网络,`val_func` 是返回验证数据集的精度、损失或其它指标的验证函数。 根据不同的场景,可能需要不同的方法来计算损失和精度,因此用户需要定义能返回模型精度、损失的函数,并传给 SensitivityAnalysis。 上面的示例也展示了如何用 SensitivityAnalysis 将敏感度结果导出为 csv 文件。
除此之外,还可以使用可选参数 `sparsities` 来为每一层设置稀疏度值。
```python
s_analyzer = SensitivityAnalysis(model=net, val_func=val, sparsities=[0.25, 0.5, 0.75])
```
SensitivityAnalysis 会为每一层逐渐剪枝 25% 50% 75% 的权重,并同时记录模型精度 (SensitivityAnalysis 一次只修建一层,其他层会使用原始权重)。 如果没有设置稀疏度,SensitivityAnalysis 会将 numpy.arange(0.1, 1.0, 0.1) 作为默认的稀疏度值。
还可以通过 early_stop_mode 和 early_stop_value 选项来加快灵敏度分析。 默认情况下,SensitivityAnalysis 会为每一层测试所有的稀疏度值下的精度。 而设置了 early_stop_mode 和 early_stop_value 后,当精度或损失值到了 early_stop_value 所设置的阈值时,会停止灵敏度分析。 支持的提前终止模式包括:minimize, maximize, dropped, raised。
minimize: 当 val_func 的返回值低于 `early_stop_value` 时,会停止分析。
maximize: 当 val_func 的返回值大于 `early_stop_value` 时,会停止分析。
dropped: 当验证指标下降 `early_stop_value` 时,会停止分析。
raised: 当验证指标增加 `early_stop_value` 时,会停止分析。
```python
s_analyzer = SensitivityAnalysis(model=net, val_func=val, sparsities=[0.25, 0.5, 0.75], early_stop_mode='dropped', early_stop_value=0.1)
```
如果只想分析部分卷积层,可在分析函数中通过 `specified_layers` 指定。 `specified_layers` 是卷积层的 Pytorch 模块名称。 例如:
```python
sensitivity = s_analyzer.analysis(val_args=[net], specified_layers=['Conv1'])
```
在此例中,只会分析 `Conv1` 层。 另外,也可以通过并行启动多个进程,将同一个模型的不同层分给每个进程来加速。
### 输出示例
下面是从 SensitivityAnalysis 中导出的 csv 文件示例。 第一行由 'layername' 和稀疏度值的列表组成。 稀疏度值表示 SensitivityAnalysis 为每一层剪枝的权重比例。 每行表示某层在不同稀疏度下的模型精度。 注意,根据 early_stop 选项,某些层可能不会有所有稀疏度下的精度或损失值。比如,精度下降的值超过了定义的阈值。
```
layername,0.05,0.1,0.2,0.3,0.4,0.5,0.7,0.85,0.95
features.0,0.54566,0.46308,0.06978,0.0374,0.03024,0.01512,0.00866,0.00492,0.00184
features.3,0.54878,0.51184,0.37978,0.19814,0.07178,0.02114,0.00438,0.00442,0.00142
features.6,0.55128,0.53566,0.4887,0.4167,0.31178,0.19152,0.08612,0.01258,0.00236
features.8,0.55696,0.54194,0.48892,0.42986,0.33048,0.2266,0.09566,0.02348,0.0056
features.10,0.55468,0.5394,0.49576,0.4291,0.3591,0.28138,0.14256,0.05446,0.01578
```
## 拓扑结构分析
NNI 还提供了在模型压缩过程中,进行模型拓扑分析的工具。 这些工具可帮助用户更好的压缩模型。 压缩模型时,因为网络结构的复杂性,经常需要花时间检查压缩配置是否合理。 因此,NNI 提供了这些工具用于模型拓扑分析,来减轻用户负担。
### ChannelDependency
复杂模型中还会有残差或连接的操作。 对这些模型剪枝时,需要小心卷积层之间通道数量的依赖关系。 以 resnet18 中残差模块为例。 `layer2.0.conv2``layer2.0.downsample.0` 层输出的特征会加到一起,所以 `layer2.0.conv2``layer2.0.downsample.0` 的输出通道数量必须一样,否则会有 Tensor 形状的冲突。
![](../../img/channel_dependency_example.jpg)
如果有通道依赖的图层,被分配了不同的稀疏度 (此处仅讨论 L1FilterPruner/L2FilterPruner 的结构化剪枝),就会造成形状冲突。 即使剪枝后的掩码模型也能正常使用,剪枝后的模型也因为模型在加和、连接这些层的输出时有冲突,不能在设备上加速。 此工具可用于查找有通道依赖的层,帮助更好的剪枝模型。
#### 用法
```python
from nni.compression.torch.utils.shape_dependency import ChannelDependency
data = torch.ones(1, 3, 224, 224).cuda()
channel_depen = ChannelDependency(net, data)
channel_depen.export('dependency.csv')
```
#### Output Example
下列代码是 由 ChannelDependency 导出的 torchvision.models.resnet18 示例。 每行上,有相互依赖的输出通道。 例如,layer1.1.conv2, conv1 和 layer1.0.conv2 相互间有输出依赖。这表示这三个层的输出通道(滤波器)数量需要一致,否则模型会产生形状冲突。
```
Dependency Set,Convolutional Layers
Set 1,layer1.1.conv2,layer1.0.conv2,conv1
Set 2,layer1.0.conv1
Set 3,layer1.1.conv1
Set 4,layer2.0.conv1
Set 5,layer2.1.conv2,layer2.0.conv2,layer2.0.downsample.0
Set 6,layer2.1.conv1
Set 7,layer3.0.conv1
Set 8,layer3.0.downsample.0,layer3.1.conv2,layer3.0.conv2
Set 9,layer3.1.conv1
Set 10,layer4.0.conv1
Set 11,layer4.0.downsample.0,layer4.1.conv2,layer4.0.conv2
Set 12,layer4.1.conv1
```
### 掩码冲突
当不同层的掩码有冲突时,(例如,为通道依赖的层设置了不同的稀疏度),可通过 MaskConflict 来修复。 即,MaskConflict 可加载由 (L1FilterPruner, 等) 导出的掩码,并检查是否有掩码冲突。如果有 MaskConflict 会将冲突的掩码设置为相同的值。
```
from nni.compression.torch.utils.mask_conflict import fix_mask_conflict
fixed_mask = fix_mask_conflict('./resnet18_mask', net, data)
```
### 模型 FLOPs 和参数计数器
NNI 提供了模型计数器,用于计算模型的 FLOPs 和参数。 此计数器支持计算没有掩码模型的 FLOPs、参数,也可以计算有掩码模型的 FLOPs、参数,这有助于在模型压缩过程中检查模型的复杂度。 注意,对于结构化的剪枝,仅根据掩码来标识保留的滤波器,不会考虑剪枝的输入通道,因此,计算出的 FLOPs 会比实际数值要大(即,模型加速后的计算值)。
### 用法
```
from nni.compression.torch.utils.counter import count_flops_params
# 给定输入大小 (1, 1, 28, 28)
flops, params = count_flops_params(model, (1, 1, 28, 28))
print(f'FLOPs: {flops/1e6:.3f}M, Params: {params/1e6:.3f}M)
```
\ No newline at end of file
# 自定义压缩算法
```eval_rst
.. contents::
```
为了简化实现新压缩算法的过程,NNI 设计了简单灵活,同时支持剪枝和量化的接口。 首先会介绍如何自定义新的剪枝算法,然后介绍如何自定义新的量化算法。
**重要说明**,为了更好的理解如何定制新的剪枝、量化算法,应先了解 NNI 中支持各种剪枝算法的框架。 参考[模型压缩框架](https://nni.readthedocs.io/en/latest/Compressor/Framework.html)
## 自定义剪枝算法
要实现新的剪枝算法,需要实现`权重掩码`类,它是 `WeightMasker` 的子类,以及`Pruner` 类,它是 `Pruner` 的子类。
`权重掩码`的实现如下:
```python
class MyMasker(WeightMasker):
def __init__(self, model, pruner):
super().__init__(model, pruner)
# 此处可初始化,如为算法收集计算权重所需要的统计信息。
def calc_mask(self, sparsity, wrapper, wrapper_idx=None):
# 根据 wrapper.weight, 和 sparsity,
# 及其它信息来计算掩码
# mask = ...
return {'weight_mask': mask}
```
参考 NNI 提供的[权重掩码](https://github.com/microsoft/nni/blob/master/src/sdk/pynni/nni/compression/torch/pruning/structured_pruning.py)来实现自己的。
基础的 `Pruner` 如下:
```python
class MyPruner(Pruner):
def __init__(self, model, config_list, optimizer):
super().__init__(model, config_list, optimizer)
self.set_wrappers_attribute("if_calculated", False)
# 创建权重掩码实例
self.masker = MyMasker(model, self)
def calc_mask(self, wrapper, wrapper_idx=None):
sparsity = wrapper.config['sparsity']
if wrapper.if_calculated:
# 如果是一次性剪枝算法,不需要再次剪枝
return None
else:
# 调用掩码函数来实际计算当前层的掩码
masks = self.masker.calc_mask(sparsity=sparsity, wrapper=wrapper, wrapper_idx=wrapper_idx)
wrapper.if_calculated = True
return masks
```
参考 NNI 提供的[Pruner](https://github.com/microsoft/nni/blob/master/src/sdk/pynni/nni/compression/torch/pruning/one_shot.py) 来实现自己的。
***
## 自定义量化算法
要实现新的量化算法,需要继承 `nni.compression.torch.Quantizer`。 然后,根据算法逻辑来重写成员函数。 需要重载的成员函数是 `quantize_weight``quantize_weight` 直接返回量化后的权重,而不是 mask。这是因为对于量化算法,量化后的权重不能通过应用 mask 来获得。
```python
from nni.compression.torch import Quantizer
class YourQuantizer(Quantizer):
def __init__(self, model, config_list):
"""
建议使用 NNI 定义的规范来配置
"""
super().__init__(model, config_list)
def quantize_weight(self, weight, config, **kwargs):
"""
quantize 需要重载此方法来为权重提供掩码
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
weight : Tensor
要被量化的权重
config : dict
权重量化的配置
"""
# 此处逻辑生成 `new_weight`
return new_weight
def quantize_output(self, output, config, **kwargs):
"""
重载此方法输出量化
此方法挂载于模型的 `:meth:`forward`。
Parameters
----------
output : Tensor
需要被量化的输出
config : dict
输出量化的配置
"""
# 实现生成 `new_output`
return new_output
def quantize_input(self, *inputs, config, **kwargs):
"""
重载此方法量化输入
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
inputs : Tensor
需要被量化的张量
config : dict
输入量化的配置
"""
# 生成 `new_input` 的代码
return new_input
def update_epoch(self, epoch_num):
pass
def step(self):
"""
根据 bind_model 函数传入的模型或权重进行一些处理
"""
pass
```
### 定制 backward 函数
有时,量化操作必须自定义 backward 函数,例如 [Straight-Through Estimator](https://stackoverflow.com/questions/38361314/the-concept-of-straight-through-estimator-ste),可如下定制 backward 函数:
```python
from nni.compression.torch.compressor import Quantizer, QuantGrad, QuantType
class ClipGrad(QuantGrad):
@staticmethod
def quant_backward(tensor, grad_output, quant_type):
"""
此方法应被子类重载来提供定制的 backward 函数,
默认实现是 Straight-Through Estimator
Parameters
----------
tensor : Tensor
量化操作的输入
grad_output : Tensor
量化操作输出的梯度
quant_type : QuantType
量化类型,可为 `QuantType.QUANT_INPUT`, `QuantType.QUANT_WEIGHT`, `QuantType.QUANT_OUTPUT`,
可为不同的类型定义不同的行为。
Returns
-------
tensor
量化输入的梯度
"""
# 对于 quant_output 函数,如果张量的绝对值大于 1,则将梯度设置为 0
if quant_type == QuantType.QUANT_OUTPUT:
grad_output[torch.abs(tensor) > 1] = 0
return grad_output
class YourQuantizer(Quantizer):
def __init__(self, model, config_list):
super().__init__(model, config_list)
# 定制 backward 函数来重载默认的 backward 函数
self.quant_grad = ClipGrad
```
如果不定制 `QuantGrad`,默认的 backward 为 Straight-Through Estimator。 _即将推出_...
# 设计文档
# 模型压缩框架概述
## 概述
模型压缩框架有两个主要组件: `Pruner``module 的包装`
```eval_rst
.. contents::
```
### Pruner
`Pruner` 用于:
1. 提供 `cal_mask` 方法来计算权重和偏差的掩码(mask)。
2. 根据配置,用 `module 的包装`来替换原始的 module。
3. 修改优化器,来在 `step` 方法被调用时,调用 `cal_mask`
下图展示了模型压缩框架的组件概览。
### module 的包装
`module 的包装` 包含:
1. 原始的 module
2. `cal_mask` 使用的一些缓存
3. 新的 forward 方法,用于在运行原始的 forward 方法前应用掩码。
![](../../img/compressor_framework.jpg)
使用 `module 包装`的原因:
1. 计算掩码所需要的 `cal_mask` 方法需要一些缓存,这些缓存需要注册在 `module 包装`里,这样就不需要修改原始的 module。
2. 新的 `forward` 方法用来在原始 `forward` 调用前,将掩码应用到权重上。
NNI 模型压缩框架中主要有三个组件/类:`Compressor`, `Pruner``Quantizer`。 下面会逐个详细介绍:
## Compressor
Compressor 是 Pruner 和 Quantizer 的基类,提供了统一的接口,可用同样的方式使用它们。 例如,使用 Pruner:
## 工作原理
基本的 Pruner 用法:
```python
from nni.compression.torch import LevelPruner
# 读取预训练的模型,或在使用 Pruner 前进行训练。
configure_list = [{
'sparsity': 0.7,
'op_types': ['BatchNorm2d'],
'op_types': ['Conv2d', 'Linear'],
}]
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
pruner = SlimPruner(model, configure_list, optimizer)
pruner = LevelPruner(model, configure_list, optimizer)
model = pruner.compress()
# 剪枝已准备好,开始调优模型,
# 模型会在训练过程中自动剪枝
```
Pruner 接收模型,配置和优化器作为参数。 在 `__init__` 方法中,优化器的 `step` 方法会被一个会调用 `cal_mask` 的新的 `step` 方法替换。 同样,所有 module 都会检查它们是否被配置为需要剪枝。如果 module 需要被剪枝,就会用 `module 包装`来替换它。 之后,会返回新的模型和优化器,并进行训练。 `compress` 方法会计算默认的掩码。
使用 Quantizer:
```python
from nni.compression.torch import DoReFaQuantizer
## 实现新的剪枝算法
要实现新的剪枝算法,需要继承 `Pruner` 来实现新的类,并重载 `cal_mask` 方法。 `cal_mask` 会被 `optimizer.step` 方法调用。 `Pruner` 基类提供了上述的基本功能,如替换 module 和优化器。
configure_list = [{
'quant_types': ['weight'],
'quant_bits': {
'weight': 8,
},
'op_types':['Conv2d', 'Linear']
}]
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
quantizer = DoReFaQuantizer(model, configure_list, optimizer)
quantizer.compress()
基础的 Pruner 如下所示:
```python
class NewPruner(Pruner):
def __init__(self, model, config_list, optimizer)
super().__init__(model, config_list, optimizer)
# 进行初始化
def calc_mask(self, wrapper, **kwargs):
# 计算 weight_mask
wrapper.weight_mask = weight_mask
```
查看[示例代码](https://github.com/microsoft/nni/tree/master/examples/model_compress)了解更多信息。
`Compressor` 类提供了一些工具函数:
### 设置包装的属性
有时,`cal_mask` 需要保存一些状态数据,可以像 PyTorch 的 module 一样,使用 `set_wrappers_attribute` API 来注册属性。 这些缓存会注册到 `module 包装`中。 用户可以通过 `module 包装`来直接访问这些缓存。
```python
class NewPruner(Pruner):
def __init__(self, model, config_list, optimizer):
super().__init__(model, config_list, optimizer)
self.set_wrappers_attribute("if_calculated", False)
def calc_mask(self, wrapper):
# 计算 weight_mask
if wrapper.if_calculated:
pass
else:
wrapper.if_calculated = True
# 更新掩码
```
有时,`calc_mask` 需要保存一些状态数据,可以像 PyTorch 的 module 一样,使用 `set_wrappers_attribute` API 来注册属性。 这些缓存会注册到 `module 包装`中。 用户可以通过 `module 包装`来直接访问这些缓存。 在上述示例中,使用了 `set_wrappers_attribute` 类设置缓冲 `if_calculated`,它用来标识某层的掩码是否已经计算过了。
### 在 forward 时收集数据
有时,需要在 forward 方法中收集数据,例如,需要激活的平均值。 这时,可以为 module 增加定制的收集方法。
有时,需要在 forward 方法中收集数据,例如,需要激活的平均值。 可通过向 module 中添加定制的 Collector 来做到。
```python
class ActivationRankFilterPruner(Pruner):
def __init__(self, model, config_list, optimizer, activation='relu', statistics_batch_num=1):
super().__init__(model, config_list, optimizer)
self.set_wrappers_attribute("if_calculated", False)
self.set_wrappers_attribute("collected_activation", [])
self.statistics_batch_num = statistics_batch_num
def collector(module_, input_, output):
if len(module_.collected_activation) < self.statistics_batch_num:
module_.collected_activation.append(self.activation(output.detach().cpu()))
self.add_activation_collector(collector)
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
class MyMasker(WeightMasker):
def __init__(self, model, pruner):
super().__init__(model, pruner)
# 为所有包装类设置 `collected_activation` 属性
# 保存所有层的激活值
self.pruner.set_wrappers_attribute("collected_activation", [])
self.activation = torch.nn.functional.relu
def collector(wrapper, input_, output):
# 通过每个包装的 collected_activation 属性,来评估收到的激活值
wrapper.collected_activation.append(self.activation(output.detach().cpu()))
self.pruner.hook_id = self.pruner.add_activation_collector(collector)
```
收集函数会在每次 forward 方法运行时调用。
还可这样来移除收集方法:
```python
collector_id = self.add_activation_collector(collector)
# ...
self.remove_activation_collector(collector_id)
# 保存 Collector 的标识
collector_id = self.pruner.add_activation_collector(collector)
# 当 Collector 不再需要后,可以通过保存的 Collector 标识来删除
self.pruner.remove_activation_collector(collector_id)
```
### 多 GPU 支持
在多 GPU 训练中,缓存和参数会在每次 `forward` 方法被调用时,复制到多个 GPU 上。 如果缓存和参数要在 `forward` 更新,就需要通过`原地`更新来提高效率。 因为 `cal_mask` 会在 `optimizer.step` 方法中的调用,会在 `forward` 方法后才被调用,且只会发生在单 GPU 上,因此它天然的就支持多 GPU 的情况。
\ No newline at end of file
***
## Pruner
Pruner 接收 `model`, `config_list` 以及 `optimizer` 参数。 通过往 `optimizer.step()` 上增加回调,在训练过程中根据 `config_list` 来对模型剪枝。
Pruner 类是 Compressor 的子类,因此它包含了 Compressor 的所有功能,并添加了剪枝所需要的组件,包括:
### 权重掩码
`权重掩码`是剪枝算法的实现,可将由 `module 包装`所包装起来的一层根据稀疏度进行修建。
### 剪枝模块包装
`剪枝 module 的包装` 包含:
1. 原始的 module
2. `calc_mask` 使用的一些缓存
3. 新的 forward 方法,用于在运行原始的 forward 方法前应用掩码。
使用 `module 包装`的原因:
1. 计算掩码所需要的 `calc_mask` 方法需要一些缓存,这些缓存需要注册在 `module 包装`里,这样就不需要修改原始的 module。
2. 新的 `forward` 方法用来在原始 `forward` 调用前,将掩码应用到权重上。
### 剪枝回调
当 Pruner 构造时会添加剪枝的回调,用来在 `optimizer.step()` 被调用时,调用 Pruner 的 calc_mask。
***
## Quantizer
Quantizer 也是 `Compressor` 的子类,用来通过减少权重或激活值的位宽来压缩模型,这样可以减少模型推理时的计算时间。 它包含:
### 量化 module 包装
模型中每个要量化的模块和层,都需要量化包装,它通过提供 `forward` 方法来量化原始模型的权重、输入和输出。
### 量化回调
量化回调会在调用 `optimizer.step()` 时设置。
### 量化相关函数
`Quantizer` 类为子类提供一下方法来实现量化算法:
```python
class Quantizer(Compressor):
"""
PyTorch 的量化基类
"""
def quantize_weight(self, weight, wrapper, **kwargs):
"""
重载此方法实现权重的量化。
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
weight : Tensor
需要量化的权重
wrapper : QuantizerModuleWrapper
原始 module 的包装
"""
raise NotImplementedError('Quantizer must overload quantize_weight()')
def quantize_output(self, output, wrapper, **kwargs):
"""
重载此方法实现输出的量化。
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
output : Tensor
需要量化的输出
wrapper : QuantizerModuleWrapper
原始 module 的包装
"""
raise NotImplementedError('Quantizer must overload quantize_output()')
def quantize_input(self, *inputs, wrapper, **kwargs):
"""
重载此方法实现输入的量化。
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
inputs : Tensor
需要量化的输入
wrapper : QuantizerModuleWrapper
原始 module 的包装
"""
raise NotImplementedError('Quantizer must overload quantize_input()')
```
***
## 多 GPU 支持
在多 GPU 训练中,缓存和参数会在每次 `forward` 方法被调用时,复制到多个 GPU 上。 如果缓存和参数要在 `forward` 更新,就需要通过`原地`更新来提高效率。 因为 `calc_mask` 会在 `optimizer.step` 方法中的调用,会在 `forward` 方法后才被调用,且只会发生在单 GPU 上,因此它天然的就支持多 GPU 的情况。
# 加速掩码的模型
*此功能处于预览版。*
*此功能处于测试阶段。*
## 介绍
......@@ -17,9 +17,9 @@
## 用法
```python
from nni.compression.speedup.torch import ModelSpeedup
from nni.compression.torch import ModelSpeedup
# model: 要加速的模型
# dummy_input: 模型的示输入,传给 `jit.trace`
# dummy_input: 模型的示输入,传给 `jit.trace`
# masks_file: 剪枝算法创建的掩码文件
m_speedup = ModelSpeedup(model, dummy_input.to(device), masks_file)
m_speedup.speedup_model()
......@@ -30,7 +30,7 @@ print('elapsed time: ', time.time() - start)
```
完整示例参考[这里](https://github.com/microsoft/nni/tree/master/examples/model_compress/model_speedup.py)
注意:当前实现仅用于 torch 1.3.1 和 torchvision 0.4.2
注意:当前支持 PyTorch 1.3.1 或更高版本。
## 局限性
......
# 使用 NNI 进行模型压缩
随着更多层和节点大型神经网络的使用,降低其存储和计算成本变得至关重要,尤其是对于某些实时应用程序。 模型压缩可用于解决此问题。
我们很高兴的宣布,基于 NNI 的模型压缩工具发布了。该版本仍处于试验阶段,会根据用户反馈进行改进。 诚挚邀请您使用、反馈,或有更多贡献。
NNI 提供了易于使用的工具包来帮助用户设计并使用压缩算法。 当前支持基于 PyTorch 的统一接口。 只需要添加几行代码即可压缩模型。 NNI 中也内置了一些流程的模型压缩算法。 用户还可以通过 NNI 强大的自动调参功能来找到最好的压缩后的模型,详见[自动模型压缩](./AutoCompression.md)。 另外,用户还能使用 NNI 的接口,轻松定制新的压缩算法,详见[教程](#customize-new-compression-algorithms)。 关于模型压缩框架如何工作的详情可参考[这里](./Framework.md)
模型压缩方面的综述可参考:[Recent Advances in Efficient Computation of Deep Convolutional Neural Networks](https://arxiv.org/pdf/1802.00939.pdf)
## 支持的算法
NNI 提供了几种压缩算法,包括剪枝和量化算法:
**剪枝**
剪枝算法通过删除冗余权重或层通道来压缩原始网络,从而降低模型复杂性并解决过拟合问题。
| 名称 | 算法简介 |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| [Level Pruner](./Pruner.md#level-pruner) | 根据权重的绝对值,来按比例修剪权重。 |
| [AGP Pruner](./Pruner.md#agp-pruner) | 自动的逐步剪枝(是否剪枝的判断:基于对模型剪枝的效果)[参考论文](https://arxiv.org/abs/1710.01878) |
| [Lottery Ticket Pruner](./Pruner.md#agp-pruner) | "The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks" 提出的剪枝过程。 它会反复修剪模型。 [参考论文](https://arxiv.org/abs/1803.03635) |
| [FPGM Pruner](./Pruner.md#fpgm-pruner) | Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration [参考论文](https://arxiv.org/pdf/1811.00250.pdf) |
| [L1Filter Pruner](./Pruner.md#l1filter-pruner) | 在卷积层中具有最小 L1 权重规范的剪枝过滤器(用于 Efficient Convnets 的剪枝过滤器) [参考论文](https://arxiv.org/abs/1608.08710) |
| [L2Filter Pruner](./Pruner.md#l2filter-pruner) | 在卷积层中具有最小 L2 权重规范的剪枝过滤器 |
| [ActivationAPoZRankFilterPruner](./Pruner.md#ActivationAPoZRankFilterPruner) | 基于指标 APoZ(平均百分比零)的剪枝过滤器,该指标测量(卷积)图层激活中零的百分比。 [参考论文](https://arxiv.org/abs/1607.03250) |
| [ActivationMeanRankFilterPruner](./Pruner.md#ActivationMeanRankFilterPruner) | 基于计算输出激活最小平均值指标的剪枝过滤器 |
| [Slim Pruner](./Pruner.md#slim-pruner) | 通过修剪 BN 层中的缩放因子来修剪卷积层中的通道 (Learning Efficient Convolutional Networks through Network Slimming) [参考论文](https://arxiv.org/abs/1708.06519) |
**量化**
量化算法通过减少表示权重或激活所需的精度位数来压缩原始网络,这可以减少计算和推理时间。
| 名称 | 算法简介 |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Naive Quantizer](./Quantizer.md#naive-quantizer) | 默认将权重量化为 8 位 |
| [QAT Quantizer](./Quantizer.md#qat-quantizer) | 为 Efficient Integer-Arithmetic-Only Inference 量化并训练神经网络。 [参考论文](http://openaccess.thecvf.com/content_cvpr_2018/papers/Jacob_Quantization_and_Training_CVPR_2018_paper.pdf) |
| [DoReFa Quantizer](./Quantizer.md#dorefa-quantizer) | DoReFa-Net: 通过低位宽的梯度算法来训练低位宽的卷积神经网络。 [参考论文](https://arxiv.org/abs/1606.06160) |
| [BNN Quantizer](./Quantizer.md#BNN-Quantizer) | 二进制神经网络:使用权重和激活限制为 +1 或 -1 的深度神经网络。 [参考论文](https://arxiv.org/abs/1602.02830) |
## 内置压缩算法的用法
通过简单的示例来展示如何修改 Trial 代码来使用压缩算法。 比如,需要通过 Level Pruner 来将权重剪枝 80%,首先在代码中训练模型前,添加以下内容([完整代码](https://github.com/microsoft/nni/tree/master/examples/model_compress))。
PyTorch 代码
```python
from nni.compression.torch import LevelPruner
config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }]
pruner = LevelPruner(model, config_list)
pruner.compress()
```
TensorFlow 代码
```python
from nni.compression.tensorflow import LevelPruner
config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }]
pruner = LevelPruner(tf.get_default_graph(), config_list)
pruner.compress()
```
可使用 `nni.compression` 中的其它压缩算法。 此算法分别在 `nni.compression.torch``nni.compression.tensorflow` 中实现,支持 PyTorch 和 TensorFlow。 参考 [Pruner](./Pruner.md)[Quantizer](./Quantizer.md) 进一步了解支持的算法。 此外,如果要使用知识蒸馏算法,可参考 [KD 示例](../TrialExample/KDExample.md)
函数调用 `pruner.compress()` 来修改用户定义的模型(在 Tensorflow 中,通过 `tf.get_default_graph()` 来获得模型,而 PyTorch 中 model 是定义的模型类),并修改模型来插入 mask。 然后运行模型时,这些 mask 即会生效。 mask 可在运行时通过算法来调整。
实例化压缩算法时,会传入 `config_list`。 配置说明如下。
### 压缩算法中的用户配置
压缩模型时,用户可能希望指定稀疏率,为不同类型的操作指定不同的比例,排除某些类型的操作,或仅压缩某类操作。 配置规范可用于表达此类需求。 可将其视为一个 Python 的 `list` 对象,其中每个元素都是一个 `dict` 对象。
`list` 中的 `dict` 会依次被应用,也就是说,如果一个操作出现在两个配置里,后面的 `dict` 会覆盖前面的配置。
#### 通用键值
在每个 `dict` 中,有一些 NNI 压缩算法支持的键值:
* __op_types__:指定要压缩的操作类型。 'default' 表示使用算法的默认设置。
* __op_names__:指定需要压缩的操作的名称。 如果没有设置此字段,操作符不会通过名称筛选。
* __exclude__:默认为 False。 如果此字段为 True,表示要通过类型和名称,将一些操作从压缩中排除。
#### 量化算法的键值
**如果使用量化算法,则需要设置更多键值。 如果使用剪枝算法,则可以忽略这些键值**
* __quant_types__ : 字符串列表。
要应用量化的类型,当前支持 "权重","输入","输出"。 "权重"是指将量化操作应用到 module 的权重参数上。 "输入" 是指对 module 的 forward 方法的输入应用量化操作。 "输出"是指将量化运法应用于模块 forward 方法的输出,在某些论文中,这种方法称为"激活"。
* __quant_bits__ : int 或 dict {str : int}
量化的位宽,键是量化类型,值是量化位宽度,例如:
```
{
quant_bits: {
'weight': 8,
'output': 4,
},
}
```
当值为 int 类型时,所有量化类型使用相同的位宽。 例如:
```
{
quant_bits: 8, # 权重和输出的位宽都为 8 bits
}
```
#### 为每个压缩算法指定的其他键
`dict` 还有一些其它键值,由特定的压缩算法所使用。 例如, [Level Pruner](./Pruner.md#level-pruner) 需要 `sparsity` 键,用于指定修剪的量。
#### 示例
配置的简单示例如下:
```python
[
{
'sparsity': 0.8,
'op_types': ['default']
},
{
'sparsity': 0.6,
'op_names': ['op_name1', 'op_name2']
},
{
'exclude': True,
'op_names': ['op_name3']
}
]
```
其表示压缩操作的默认稀疏度为 0.8,但`op_name1``op_name2` 会使用 0.6,且不压缩 `op_name3`
### 其它 API
一些压缩算法使用 Epoch 来控制压缩进度(如[AGP](./Pruner.md#agp-pruner)),一些算法需要在每个批处理步骤后执行一些逻辑。 因此提供了另外两个 API。 一个是 `update_epoch`,可参考下例使用:
TensorFlow 代码
```python
pruner.update_epoch(epoch, sess)
```
PyTorch 代码
```python
pruner.update_epoch(epoch)
```eval_rst
.. contents::
```
另一个是 `step`,可在每个批处理后调用 `pruner.step()`。 注意,并不是所有的算法都需要这两个 API,对于不需要它们的算法,调用它们不会有影响
随着更多层和节点大型神经网络的使用,降低其存储和计算成本变得至关重要,尤其是对于某些实时应用程序。 模型压缩可用于解决此问题
使用下列 API 可轻松将压缩后的模型导出,稀疏模型的 `state_dict` 会保存在 `model.pth` 文件中,可通过 `torch.load('model.pth')` 加载。
NNI 的模型压缩工具包,提供了最先进的模型压缩算法和策略,帮助压缩并加速模型。 NNI 模型压缩支持的主要功能有:
```
pruner.export_model(model_path='model.pth')
```
* 支持多种流行的剪枝和量化算法。
* 通过 NNI 强大的自动调优功能,可使用最先进的策略来自动化模型的剪枝和量化过程。
* 加速压缩的模型,使其在推理时有更低的延迟,同时文件也会变小。
* 提供优化且易用的压缩工具,帮助用户深入了解压缩过程和结果。
* 提供简洁的接口,帮助用户实现自己的压缩算法。
`mask_dict``onnx` 格式的剪枝模型(需要指定 `input_shape`)可这样导出:
*注意,PyTorch 和 TensorFlow 有统一的 API 接口,当前仅支持 PyTorch 版本,未来会提供 TensorFlow 的支持。*
```python
pruner.export_model(model_path='model.pth', mask_path='mask.pth', onnx_path='model.onnx', input_shape=[1, 1, 28, 28])
```
## 定制新的压缩算法
## 支持的算法
为了简化压缩算法的编写,NNI 设计了简单且灵活的接口。 对于 Pruner 和 Quantizer 分别有相应的接口
包括剪枝和量化算法
### 剪枝算法
要实现新的剪枝算法,根据使用的框架,添加继承于 `nni.compression.tensorflow.Pruner``nni.compression.torch.Pruner` 的类。 然后,根据算法逻辑来重写成员函数。
```python
# TensorFlow 中定制 Pruner。
# PyTorch 的 Pruner,只需将
# nni.compression.tensorflow.Pruner 替换为
# nni.compression.torch.Pruner
class YourPruner(nni.compression.tensorflow.Pruner):
def __init__(self, model, config_list):
"""
建议使用 NNI 定义的规范来配置
"""
super().__init__(model, config_list)
def calc_mask(self, layer, config):
"""
Pruner 需要重载此方法来为权重提供掩码
掩码必须与权重有相同的形状和类型。
将对权重执行 ``mul()`` 操作。
此方法会挂载到模型的 ``forward()`` 方法上。
Parameters
----------
layer: LayerInfo
为 ``layer`` 的权重计算掩码
config: dict
生成权重所需要的掩码
"""
return your_mask
# PyTorch 版本不需要 sess 参数
def update_epoch(self, epoch_num, sess):
pass
# PyTorch 版本不需要 sess 参数
def step(self, sess):
"""
根据需要可基于 bind_model 方法中的模型或权重进行操作
"""
pass
```
对于最简单的算法,只需要重写 `calc_mask` 函数。 它会接收需要压缩的层以及其压缩配置。 可在此函数中为此权重生成 mask 并返回。 NNI 会应用此 mask。
剪枝算法通过删除冗余权重或层通道来压缩原始网络,从而降低模型复杂性并解决过拟合问题。
一些算法根据训练进度来生成 mask,如 Epoch 数量。 Pruner 可使用 `update_epoch` 来了解训练进度。 应在每个 Epoch 之前调用它。
| 名称 | 算法简介 |
| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [Level Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#level-pruner) | 根据权重的绝对值,来按比例修剪权重。 |
| [AGP Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#agp-pruner) | 自动的逐步剪枝(是否剪枝的判断:基于对模型剪枝的效果)[参考论文](https://arxiv.org/abs/1710.01878) |
| [Lottery Ticket Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#lottery-ticket-hypothesis) | "The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks" 提出的剪枝过程。 它会反复修剪模型。 [参考论文](https://arxiv.org/abs/1803.03635) |
| [FPGM Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#fpgm-pruner) | Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration [参考论文](https://arxiv.org/pdf/1811.00250.pdf) |
| [L1Filter Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#l1filter-pruner) | 在卷积层中具有最小 L1 权重规范的剪枝滤波器(用于 Efficient Convnets 的剪枝滤波器) [参考论文](https://arxiv.org/abs/1608.08710) |
| [L2Filter Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#l2filter-pruner) | 在卷积层中具有最小 L2 权重规范的剪枝滤波器 |
| [ActivationAPoZRankFilterPruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#activationapozrankfilterpruner) | 基于指标 APoZ(平均百分比零)的剪枝滤波器,该指标测量(卷积)图层激活中零的百分比。 [参考论文](https://arxiv.org/abs/1607.03250) |
| [ActivationMeanRankFilterPruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#activationmeanrankfilterpruner) | 基于计算输出激活最小平均值指标的剪枝滤波器 |
| [Slim Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#slim-pruner) | 通过修剪 BN 层中的缩放因子来修剪卷积层中的通道 (Learning Efficient Convolutional Networks through Network Slimming) [参考论文](https://arxiv.org/abs/1708.06519) |
| [TaylorFO Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#taylorfoweightfilterpruner) | 基于一阶泰勒展开的权重对滤波器剪枝 (Importance Estimation for Neural Network Pruning) [参考论文](http://jankautz.com/publications/Importance4NNPruning_CVPR19.pdf) |
| [ADMM Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#admm-pruner) | 基于 ADMM 优化技术的剪枝 [参考论文](https://arxiv.org/abs/1804.03294) |
| [NetAdapt Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#netadapt-pruner) | 在满足计算资源预算的情况下,对预训练的网络迭代剪枝 [参考论文](https://arxiv.org/abs/1804.03230) |
| [SimulatedAnnealing Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#simulatedannealing-pruner) | 通过启发式的模拟退火算法进行自动剪枝 [参考论文](https://arxiv.org/abs/1907.03141) |
| [AutoCompress Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#autocompress-pruner) | 通过迭代调用 SimulatedAnnealing Pruner 和 ADMM Pruner 进行自动剪枝 [参考论文](https://arxiv.org/abs/1907.03141) |
一些算法可能需要全局的信息来生成 mask,例如模型的所有权重(用于生成统计信息). 可在 Pruner 类中通过 `self.bound_model` 来访问权重。 如果需要优化器的信息(如在 Pytorch 中),可重载 `__init__` 来接收优化器等参数。 然后 `step` 可以根据算法来处理或更新信息。 可参考[内置算法的源码](https://github.com/microsoft/nni/tree/master/src/sdk/pynni/nni/compressors)作为示例。
### 量化算法
定制量化算法的接口与剪枝算法类似。 唯一的不同是使用 `quantize_weight` 替换了 `calc_mask``quantize_weight` 直接返回量化后的权重,而不是 mask。这是因为对于量化算法,量化后的权重不能通过应用 mask 来获得。
```python
from nni.compression.torch.compressor import Quantizer
class YourQuantizer(Quantizer):
def __init__(self, model, config_list):
"""
建议使用 NNI 定义的规范来配置
"""
super().__init__(model, config_list)
def quantize_weight(self, weight, config, **kwargs):
"""
quantize 需要重载此方法来为权重提供掩码
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
weight : Tensor
要被量化的权重
config : dict
权重量化的配置
"""
# 此处逻辑生成 `new_weight`
return new_weight
def quantize_output(self, output, config, **kwargs):
"""
重载此方法输出量化
此方法挂载于模型的 `:meth:`forward`。
Parameters
----------
output : Tensor
需要被量化的输出
config : dict
输出量化的配置
"""
# 实现生成 `new_output`
return new_output
def quantize_input(self, *inputs, config, **kwargs):
"""
重载此方法量化输入
此方法挂载于模型的 :meth:`forward`。
Parameters
----------
inputs : Tensor
需要被量化的张量
config : dict
输入量化的配置
"""
# 生成 `new_input` 的代码
量化算法通过减少表示权重或激活所需的精度位数来压缩原始网络,这可以减少计算和推理时间。
return new_input
| 名称 | 算法简介 |
| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Naive Quantizer](https://nni.readthedocs.io/zh/latest/Compressor/Quantizer.html#naive-quantizer) | 默认将权重量化为 8 位 |
| [QAT Quantizer](https://nni.readthedocs.io/zh/latest/Compressor/Quantizer.html#qat-quantizer) | 为 Efficient Integer-Arithmetic-Only Inference 量化并训练神经网络。 [参考论文](http://openaccess.thecvf.com/content_cvpr_2018/papers/Jacob_Quantization_and_Training_CVPR_2018_paper.pdf) |
| [DoReFa Quantizer](https://nni.readthedocs.io/zh/latest/Compressor/Quantizer.html#dorefa-quantizer) | DoReFa-Net: 通过低位宽的梯度算法来训练低位宽的卷积神经网络。 [参考论文](https://arxiv.org/abs/1606.06160) |
| [BNN Quantizer](https://nni.readthedocs.io/zh/latest/Compressor/Quantizer.html#bnn-quantizer) | 二进制神经网络:使用权重和激活限制为 +1 或 -1 的深度神经网络。 [参考论文](https://arxiv.org/abs/1602.02830) |
def update_epoch(self, epoch_num):
pass
## 自动模型压缩
def step(self):
"""
Can do some processing based on the model or weights binded
in the func bind_model
"""
pass
```
#### 定制 backward 函数
有时,量化操作必须自定义 backward 函数,例如 [Straight-Through Estimator](https://stackoverflow.com/questions/38361314/the-concept-of-straight-through-estimator-ste),可如下定制 backward 函数:
有时,给定的目标压缩率很难通过一次压缩就得到最好的结果。 自动模型压缩算法,通常需要通过对不同层采用不同的稀疏度来探索可压缩的空间。 NNI 提供了这样的算法,来帮助用户在模型中为每一层指定压缩度。 此外,还可利用 NNI 的自动调参功能来自动的压缩模型。 详细文档参考[这里](./AutoCompression.md)
```python
from nni.compression.torch.compressor import Quantizer, QuantGrad, QuantType
## 模型加速
class ClipGrad(QuantGrad):
@staticmethod
def quant_backward(tensor, grad_output, quant_type):
"""
此方法应被子类重载来提供定制的 backward 函数,
默认实现是 Straight-Through Estimator
Parameters
----------
tensor : Tensor
量化操作的输入
grad_output : Tensor
量化操作输出的梯度
quant_type : QuantType
量化类型,可为 `QuantType.QUANT_INPUT`, `QuantType.QUANT_WEIGHT`, `QuantType.QUANT_OUTPUT`,
可为不同的类型定义不同的行为。
Returns
-------
tensor
量化输入的梯度
"""
模型压缩的目的是减少推理延迟和模型大小。 但现有的模型压缩算法主要通过模拟的方法来检查压缩模型性能(如精度)。例如,剪枝算法中使用掩码,而量化算法中量化值仍然是以 32 位浮点数来存储。 只要给出这些算法产生的掩码和量化位,NNI 可真正的加速模型。 模型加速的详细文档参考[这里](./ModelSpeedup.md)
# 对于 quant_output 函数,如果张量的绝对值大于 1,则将梯度设置为 0
if quant_type == QuantType.QUANT_OUTPUT:
grad_output[torch.abs(tensor) > 1] = 0
return grad_output
## 压缩工具
压缩工具包括了一些有用的工具,能帮助用户理解并分析要压缩的模型。 例如,可检查每层对剪枝的敏感度。 可很容易的计算模型的 FLOPs 和参数数量。 [点击这里](./CompressionUtils.md),查看压缩工具的完整列表。
class YourQuantizer(Quantizer):
def __init__(self, model, config_list):
super().__init__(model, config_list)
# 定制 backward 函数来重载默认的 backward 函数
self.quant_grad = ClipGrad
```
## 自定义压缩算法
如果不定制 `QuantGrad`,默认的 backward 为 Straight-Through Estimator。 _即将推出_...
NNI 模型压缩提供了简洁的接口,用于自定义新的压缩算法。 接口的设计理念是,将框架相关的实现细节包装起来,让用户能聚焦于压缩逻辑。 点击[这里](./Framework.md),查看自定义新压缩算法(包括剪枝和量化算法)的详细教程。
## 参考和反馈
* 在 GitHub 中[提交此功能的 Bug](https://github.com/microsoft/nni/issues/new?template=bug-report.md)
......
NNI Compressor 中的 Pruner
===
# NNI 支持的剪枝算法
支持的剪枝算法
NNI 提供了一些支持细粒度权重剪枝和结构化的滤波器剪枝算法。 **细粒度的剪枝**通常会导致非结构化的模型,这需要特定的硬件或软件来加速这样的稀疏网络。 **滤波器剪枝**通过删除整个滤波器来实现加速。 NNI 还提供了算法来进行**剪枝规划**
**细粒度剪枝**
* [Level Pruner](#level-pruner)
**滤波器剪枝**
* [Slim Pruner](#slim-pruner)
* [FPGM Pruner](#fpgm-pruner)
* [L1Filter Pruner](#l1filter-pruner)
* [L2Filter Pruner](#l2filter-pruner)
* [APoZ Rank Pruner](#activationapozrankfilterpruner)
* [Activation Mean Rank Pruner](#activationmeanrankfilterpruner)
* [Taylor FO On Weight Pruner](#taylorfoweightfilterpruner)
**剪枝计划**
* [AGP Pruner](#agp-pruner)
* [NetAdapt Pruner](#netadapt-pruner)
* [SimulatedAnnealing Pruner](#simulatedannealing-pruner)
* [AutoCompress Pruner](#autocompress-pruner)
**其它**
* [ADMM Pruner](#admm-pruner)
* [Lottery Ticket 假设](#lottery-ticket-hypothesis)
* [Slim Pruner](#slim-pruner)
* [具有权重等级的 Filter Pruners](#weightrankfilterpruner)
* [FPGM Pruner](#fpgm-pruner)
* [L1Filter Pruner](#l1filter-pruner)
* [L2Filter Pruner](#l2filter-pruner)
* [具有激活等级的 Filter Pruners](#activationrankfilterpruner)
* [APoZ Rank Pruner](#activationapozrankfilterpruner)
* [Activation Mean Rank Pruner](#activationmeanrankfilterpruner)
* [具有梯度等级的 Filter Pruners](#gradientrankfilterpruner)
* [Taylor FO On Weight Pruner](#taylorfoweightfilterpruner)
## Level Pruner
......@@ -45,105 +54,6 @@ pruner.compress()
***
## AGP Pruner
这是一种迭代的 Pruner,在 [To prune, or not to prune: exploring the efficacy of pruning for model compression](https://arxiv.org/abs/1710.01878)中,作者 Michael Zhu 和 Suyog Gupta 提出了一种逐渐修建权重的算法。
> 我们引入了一种新的自动梯度剪枝算法。这种算法从初始的稀疏度值 si(一般为 0)开始,通过 n 步的剪枝操作,增加到最终所需的稀疏度 sf。从训练步骤 t0 开始,以 ∆t 为剪枝频率: ![](../../img/agp_pruner.png) 在神经网络训练时‘逐步增加网络稀疏度时,每训练 ∆t 步更新一次权重剪枝的二进制掩码。同时也允许训练步骤恢复因为剪枝而造成的精度损失。 根据我们的经验,∆t 设为 100 到 1000 个训练步骤之间时,对于模型最终精度的影响可忽略不计。 一旦模型达到了稀疏度目标 sf,权重掩码将不再更新。 公式背后的稀疏函数直觉。
### 用法
通过下列代码,可以在 10 个 Epoch 中将权重稀疏度从 0% 剪枝到 80%。
TensorFlow 代码
```python
from nni.compression.tensorflow import AGP_Pruner
config_list = [{
'initial_sparsity': 0,
'final_sparsity': 0.8,
'start_epoch': 0,
'end_epoch': 10,
'frequency': 1,
'op_types': 'default'
}]
pruner = AGP_Pruner(tf.get_default_graph(), config_list)
pruner.compress()
```
PyTorch 代码
```python
from nni.compression.torch import AGP_Pruner
config_list = [{
'initial_sparsity': 0,
'final_sparsity': 0.8,
'start_epoch': 0,
'end_epoch': 10,
'frequency': 1,
'op_types': ['default']
}]
pruner = AGP_Pruner(model, config_list)
pruner.compress()
```
在训练代码中每完成一个 Epoch,更新一下 Epoch 数值。
TensorFlow 代码
```python
pruner.update_epoch(epoch, sess)
```
PyTorch 代码
```python
pruner.update_epoch(epoch)
```
查看示例进一步了解
#### AGP Pruner 的用户配置
* **initial_sparsity:** 指定了 Compressor 开始压缩的稀疏度。
* **final_sparsity:** 指定了 Compressor 压缩结束时的稀疏度。
* **start_epoch:** 指定了 Compressor 开始压缩时的 Epoch 数值,默认为 0。
* **end_epoch:** 指定了 Compressor 结束压缩时的 Epoch 数值。
* **frequency:** 指定了 Compressor 每过多少个 Epoch 进行一次剪枝,默认 frequency=1。
***
## Lottery Ticket 假设
[The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks](https://arxiv.org/abs/1803.03635), 作者 Jonathan Frankle 和 Michael Carbin,提供了全面的测量和分析,并阐明了 *lottery ticket 假设*: 密集的、随机初始化的、包含子网络的前馈网络 (*winning tickets*) -- 在单独训练时 -- 在相似的迭代次数后达到了与原始网络相似的准确度。
本文中,作者使用叫做*迭代*修剪的方法:
> 1. 随机初始化一个神经网络 f(x;theta_0) (其中 theta_0 为 D_{theta}).
> 2. 将网络训练 j 次,得出参数 theta_j。
> 3. 在 theta_j 修剪参数的 p%,创建掩码 m。
> 4. 将其余参数重置为 theta_0 的值,创建获胜彩票 f(x;m*theta_0)。
> 5. 重复步骤 2、3 和 4。
如果配置的最终稀疏度为 P (e.g., 0.8) 并且有 n 次修建迭代,每次迭代修剪前一轮中剩余权重的 1-(1-P)^(1/n)。
### 用法
PyTorch 代码
```python
from nni.compression.torch import LotteryTicketPruner
config_list = [{
'prune_iterations': 5,
'sparsity': 0.8,
'op_types': ['default']
}]
pruner = LotteryTicketPruner(model, config_list, optimizer)
pruner.compress()
for _ in pruner.get_prune_iterations():
pruner.prune_iteration_start()
for epoch in range(epoch_num):
...
```
上述配置意味着有 5 次迭代修剪。 由于在同一次运行中执行了 5 次修剪,LotteryTicketPruner 需要 `model``optimizer` (**注意,如果使用 `lr_scheduler`,也需要添加**) 来在每次开始新的修剪迭代时,将其状态重置为初始值。 使用 `get_prune_iterations` 来获取修建迭代,并在每次迭代开始时调用 `prune_iteration_start`。 为了模型能较好收敛,`epoch_num` 最好足够大。因为假设是在后几轮中具有较高稀疏度的性能(准确度)可与第一轮获得的相当。 [这是](./LotteryTicketHypothesis.md)简单的重现结果。
*稍后支持 TensorFlow 版本。*
#### LotteryTicketPruner 的用户配置
* **prune_iterations:** 迭代修剪的次数。
* **sparsity:** 压缩完成后的最终稀疏度。
***
## Slim Pruner
这是一次性的 Pruner,在 ['Learning Efficient Convolutional Networks through Network Slimming'](https://arxiv.org/pdf/1708.06519.pdf) 中提出,作者 Zhuang Liu, Jianguo Li, Zhiqiang Shen, Gao Huang, Shoumeng Yan 以及 Changshui Zhang。
......@@ -168,20 +78,29 @@ pruner.compress()
- **sparsity:**,指定压缩的稀疏度。
- **op_types:** 在 Slim Pruner 中仅支持 BatchNorm2d。
### 重现实验
我们实现了 ['Learning Efficient Convolutional Networks through Network Slimming'](https://arxiv.org/pdf/1708.06519.pdf) 中的一项实验。根据论文,对 CIFAR-10 上的 **VGGNet** 剪除了 $70\%$ 的通道,即约 $88.5\%$ 的参数。 实验结果如下:
| 模型 | 错误率(论文/我们的) | 参数量 | 剪除率 |
| ------------- | ----------- | ------ | ----- |
| VGGNet | 6.34/6.40 | 20.04M | |
| Pruned-VGGNet | 6.20/6.26 | 2.03M | 88.5% |
实验代码在 [examples/model_compress](https://github.com/microsoft/nni/tree/master/examples/model_compress/)
## WeightRankFilterPruner
WeightRankFilterPruner 是一系列的 Pruner,在卷积层权重上,用最小的重要性标准修剪过滤器,来达到预设的网络稀疏度。
***
### FPGM Pruner
## FPGM Pruner
这是一种一次性的 Pruner,FPGM Pruner 是论文 [Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration](https://arxiv.org/pdf/1811.00250.pdf) 的实现
具有最小几何中位数的 FPGMPruner 修剪滤器
具有最小几何中位数的 FPGMPruner 修剪滤
![](../../img/fpgm_fig1.png)
> 以前的方法使用 “smaller-norm-less-important” 准则来修剪卷积神经网络中规范值较小的。 本文中,分析了基于规范的准则,并指出其所依赖的两个条件不能总是满足:(1) 滤器的规范偏差应该较大;(2) 滤器的最小规范化值应该很小。 为了解决此问题,提出了新的滤器修方法,即 Filter Pruning via Geometric Median (FPGM),可不考虑这两个要求来压缩模型。 与以前的方法不同,FPGM 通过修剪冗余的,而不是相关性更小的部分来压缩 CNN 模型。
> 以前的方法使用 “smaller-norm-less-important” 准则来修剪卷积神经网络中规范值较小的。 本文中,分析了基于规范的准则,并指出其所依赖的两个条件不能总是满足:(1) 滤器的规范偏差应该较大;(2) 滤器的最小规范化值应该很小。 为了解决此问题,提出了新的滤器修方法,即 Filter Pruning via Geometric Median (FPGM),可不考虑这两个要求来压缩模型。 与以前的方法不同,FPGM 通过修剪冗余的,而不是相关性更小的部分来压缩 CNN 模型。
#### 用法
### 用法
TensorFlow 代码
```python
......@@ -203,41 +122,29 @@ config_list = [{
pruner = FPGMPruner(model, config_list)
pruner.compress()
```
注意:FPGM Pruner 用于修剪深度神经网络中的卷积层,因此 `op_types` 字段仅支持卷积层。
需要在每个 epoch 开始的地方添加下列代码来更新 epoch 的编号。
TensorFlow 代码
```python
pruner.update_epoch(epoch, sess)
```
PyTorch 代码
```python
pruner.update_epoch(epoch)
```
查看示例进一步了解
#### FPGM Pruner 的用户配置
* **sparsity:** 卷积过滤器要修剪的百分比。
- **sparsity:** 卷积滤波器要修剪的百分比。
- **op_types:** 在 L1Filter Pruner 中仅支持 Conv2d。
***
### L1Filter Pruner
## L1Filter Pruner
这是一种一次性的 Pruner,由 ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710) 提出,作者 Hao Li, Asim Kadav, Igor Durdanovic, Hanan Samet 和 Hans Peter Graf。 [重现的实验结果](l1filterpruner.md)
这是一种一次性的 Pruner,由 ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710) 提出,作者 Hao Li, Asim Kadav, Igor Durdanovic, Hanan Samet 和 Hans Peter Graf。
![](../../img/l1filter_pruner.png)
> L1Filter Pruner 修剪**卷积层**中的滤器
> L1Filter Pruner 修剪**卷积层**中的滤
>
> 从第 i 个卷积层修剪 m 个滤器的过程如下:
> 从第 i 个卷积层修剪 m 个滤器的过程如下:
>
> 1. 对于每个滤器 ![](http://latex.codecogs.com/gif.latex?F_{i,j}),计算其绝对内核权重之和![](http://latex.codecogs.com/gif.latex?s_j=\sum_{l=1}^{n_i}\sum|K_l|)
> 2. 将滤器按 ![](http://latex.codecogs.com/gif.latex?s_j) 排序。
> 3. 修剪 ![](http://latex.codecogs.com/gif.latex?m) 具有最小求和值及其相应特征图的筛选器。 在 下一个卷积层中,被剪除的特征图所对应的内核也被移除。
> 1. 对于每个滤器 ![](http://latex.codecogs.com/gif.latex?F_{i,j}),计算其绝对内核权重之和![](http://latex.codecogs.com/gif.latex?s_j=\sum_{l=1}^{n_i}\sum|K_l|)
> 2. 将滤器按 ![](http://latex.codecogs.com/gif.latex?s_j) 排序。
> 3. 修剪 ![](http://latex.codecogs.com/gif.latex?m) 具有最小求和值及其相应特征图的滤波器。 在 下一个卷积层中,被剪除的特征图所对应的内核也被移除。
> 4. 为第 ![](http://latex.codecogs.com/gif.latex?i) 和 ![](http://latex.codecogs.com/gif.latex?i+1) 层创建新的内核举证,并保留剩余的内核 权重,并复制到新模型中。
#### 用法
### 用法
PyTorch 代码
......@@ -251,15 +158,26 @@ pruner.compress()
#### L1Filter Pruner 的用户配置
- **sparsity:**,指定压缩的稀疏度。
- **op_types:** 在 L1Filter Pruner 中仅支持 Conv1d 和 Conv2d。
- **op_types:** 在 L1Filter Pruner 中仅支持 Conv2d。
### 重现实验
我们通过 **L1FilterPruner** 实现了 ['PRUNING FILTERS FOR EFFICIENT CONVNETS'](https://arxiv.org/abs/1608.08710) 中的一项实验, 即论文中,在 CIFAR-10 数据集上修剪 **VGG-16****VGG-16-pruned-A**,其中大约剪除了 $64\%$ 的参数。 实验结果如下:
| 模型 | 错误率(论文/我们的) | 参数量 | 剪除率 |
| --------------- | ----------- | -------- | ----- |
| VGG-16 | 6.75/6.49 | 1.5x10^7 | |
| VGG-16-pruned-A | 6.60/6.47 | 5.4x10^6 | 64.0% |
实验代码在 [examples/model_compress](https://github.com/microsoft/nni/tree/master/examples/model_compress/)
***
### L2Filter Pruner
## L2Filter Pruner
这是一种结构化剪枝算法,用于修剪权重的最小 L2 规范筛选器。 它被实现为一次性修剪器。
这是一种结构化剪枝算法,用于修剪权重的最小 L2 规范滤波器。 它被实现为一次性修剪器。
#### 用法
### 用法
PyTorch 代码
......@@ -270,23 +188,22 @@ pruner = L2FilterPruner(model, config_list)
pruner.compress()
```
#### L2Filter Pruner 的用户配置
### L2Filter Pruner 的用户配置
- **sparsity:**,指定压缩的稀疏度。
- **op_types:** 在 L2Filter Pruner 中仅支持 Conv1d 和 Conv2d。
- **op_types:** 在 L2Filter Pruner 中仅支持 Conv2d。
## ActivationRankFilterPruner
ActivationRankFilterPruner 是一系列的 Pruner,从卷积层激活的输出,用最小的重要性标准修剪过滤器,来达到预设的网络稀疏度。
***
### ActivationAPoZRankFilterPruner
## ActivationAPoZRankFilterPruner
我们将其实现为一次性剪枝器,它基于 `APoZ` 修剪卷积层,参考论文 [Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/abs/1607.03250)。 基于迭代剪枝的 `APoZ` 将在以后的版本中支持
ActivationAPoZRankFilterPruner 是从卷积层激活的输出,用最小的重要性标准 `APoZ` 修剪滤波器,来达到预设的网络稀疏度。 剪枝标准 `APoZ` 的解释在论文 [Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/abs/1607.03250)
APoZ 定义为:
![](../../img/apoz.png)
#### 用法
### 用法
PyTorch 代码
......@@ -304,18 +221,18 @@ pruner.compress()
查看示例进一步了解
#### ActivationAPoZRankFilterPruner 的用户配置
### ActivationAPoZRankFilterPruner 的用户配置
- **sparsity:** 卷积滤器要修剪的百分比。
- **sparsity:** 卷积滤器要修剪的百分比。
- **op_types:** 在 ActivationAPoZRankFilterPruner 中仅支持 Conv2d。
***
### ActivationMeanRankFilterPruner
## ActivationMeanRankFilterPruner
其实现为一次性修剪器,基于 `平均激活` 准则来修剪卷积层,在论文 [Pruning Convolutional Neural Networks for Resource Efficient Inference](https://arxiv.org/abs/1611.06440) 的 2.2 节中有说明。 本文中提到的其他修剪标准将在以后的版本中支持。
ActivationMeanRankFilterPruner 是从卷积层激活的输出,用最小的重要性标准`平均激活`来修剪滤波器,来达到预设的网络稀疏度。 剪枝标准`平均激活`,在论文 [Pruning Convolutional Neural Networks for Resource Efficient Inference](https://arxiv.org/abs/1611.06440) 的 2.2 节中进行了介绍。 本文中提到的其他修剪标准将在以后的版本中支持。
#### 用法
### 用法
PyTorch 代码
......@@ -325,7 +242,7 @@ config_list = [{
'sparsity': 0.5,
'op_types': ['Conv2d']
}]
pruner = ActivationMeanRankFilterPruner(model, config_list)
pruner = ActivationMeanRankFilterPruner(model, config_list, statistics_batch_num=1)
pruner.compress()
```
......@@ -333,24 +250,21 @@ pruner.compress()
查看示例进一步了解
#### ActivationMeanRankFilterPruner 的用户配置
### ActivationMeanRankFilterPruner 的用户配置
- **sparsity:** 卷积滤器要修剪的百分比。
- **sparsity:** 卷积滤器要修剪的百分比。
- **op_types:** 在 ActivationMeanRankFilterPruner 中仅支持 Conv2d。
***
## GradientRankFilterPruner
GradientRankFilterPruner 是一系列的 Pruner,在卷积层梯度上,用最小的重要性标准修剪过滤器,来达到预设的网络稀疏度。
### TaylorFOWeightFilterPruner
## TaylorFOWeightFilterPruner
其实现为一次性 Pruner,会根据权重的一阶泰勒展开式来对卷积层进行剪枝。 过滤器的估计重要性在论文 [Importance Estimation for Neural Network Pruning](http://jankautz.com/publications/Importance4NNPruning_CVPR19.pdf) 中有定义。 本文中提到的其他修剪标准将在以后的版本中支持。
TaylorFOWeightFilterPruner 根据权重的一阶泰勒展开式,来估计重要性并进行剪枝,从而达到预设的网络稀疏度。 滤波器的估计重要性在论文 [Importance Estimation for Neural Network Pruning](http://jankautz.com/publications/Importance4NNPruning_CVPR19.pdf) 中有定义。 本文中提到的其他修剪标准将在以后的版本中支持。
>
![](../../img/importance_estimation_sum.png)
#### 用法
### 用法
PyTorch 代码
......@@ -360,17 +274,389 @@ config_list = [{
'sparsity': 0.5,
'op_types': ['Conv2d']
}]
pruner = TaylorFOWeightFilterPruner(model, config_list, optimizer)
pruner = TaylorFOWeightFilterPruner(model, config_list, statistics_batch_num=1)
pruner.compress()
```
查看示例进一步了解
#### GradientWeightSumFilterPruner 的用户配置
### TaylorFOWeightFilterPruner 的用户配置
- **sparsity:** 卷积滤器要修剪的百分比。
- **sparsity:** 卷积滤器要修剪的百分比。
- **op_types:** 当前 TaylorFOWeightFilterPruner 中仅支持 Conv2d。
***
## AGP Pruner
这是一种迭代的 Pruner,在 [To prune, or not to prune: exploring the efficacy of pruning for model compression](https://arxiv.org/abs/1710.01878)中,作者 Michael Zhu 和 Suyog Gupta 提出了一种逐渐修建权重的算法。
> 我们引入了一种新的自动梯度剪枝算法。这种算法从初始的稀疏度值 si(一般为 0)开始,通过 n 步的剪枝操作,增加到最终所需的稀疏度 sf。从训练步骤 t0 开始,以 ∆t 为剪枝频率: ![](../../img/agp_pruner.png) 在神经网络训练时‘逐步增加网络稀疏度时,每训练 ∆t 步更新一次权重剪枝的二进制掩码。同时也允许训练步骤恢复因为剪枝而造成的精度损失。 根据我们的经验,∆t 设为 100 到 1000 个训练步骤之间时,对于模型最终精度的影响可忽略不计。 一旦模型达到了稀疏度目标 sf,权重掩码将不再更新。 公式背后的稀疏函数直觉。
### 用法
通过下列代码,可以在 10 个 Epoch 中将权重稀疏度从 0% 剪枝到 80%。
PyTorch 代码
```python
from nni.compression.torch import AGP_Pruner
config_list = [{
'initial_sparsity': 0,
'final_sparsity': 0.8,
'start_epoch': 0,
'end_epoch': 10,
'frequency': 1,
'op_types': ['default']
}]
# 使用 Pruner 前,加载预训练模型、或训练模型。
# model = MyModel()
# model.load_state_dict(torch.load('mycheckpoint.pth'))
# AGP Pruner 会在 optimizer.step() 上回调,在微调模型时剪枝,
# 因此,必须要有 optimizer 才能完成模型剪枝。
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)
pruner = AGP_Pruner(model, config_list, optimizer, pruning_algorithm='level')
pruner.compress()
```
AGP Pruner 默认使用 `LevelPruner` 算法来修建权重,还可以设置 `pruning_algorithm` 参数来使用其它剪枝算法:
* `level`: LevelPruner
* `slim`: SlimPruner
* `l1`: L1FilterPruner
* `l2`: L2FilterPruner
* `fpgm`: FPGMPruner
* `taylorfo`: TaylorFOWeightFilterPruner
* `apoz`: ActivationAPoZRankFilterPruner
* `mean_activation`: ActivationMeanRankFilterPruner
在训练代码中每完成一个 Epoch,需要更新一下 Epoch 的值。
PyTorch 代码
```python
pruner.update_epoch(epoch)
```
查看示例进一步了解
#### AGP Pruner 的用户配置
* **initial_sparsity:** 指定了 Compressor 开始压缩的稀疏度。
* **final_sparsity:** 指定了 Compressor 压缩结束时的稀疏度。
* **start_epoch:** 指定了 Compressor 开始压缩时的 Epoch 数值,默认为 0。
* **end_epoch:** 指定了 Compressor 结束压缩时的 Epoch 数值。
* **frequency:** 指定了 Compressor 每过多少个 Epoch 进行一次剪枝,默认 frequency=1。
***
## NetAdapt Pruner
NetAdapt 在满足资源预算的情况下,自动简化预训练的网络。 给定整体稀疏度,NetAdapt 可通过迭代剪枝自动为不同层生成不同的稀疏分布。
参考 [NetAdapt: Platform-Aware Neural Network Adaptation for Mobile Applications](https://arxiv.org/abs/1804.03230) 了解详细信息。
![](../../img/algo_NetAdapt.png)
#### 用法
PyTorch 代码
```python
from nni.compression.torch import NetAdaptPruner
config_list = [{
'sparsity': 0.5,
'op_types': ['Conv2d']
}]
pruner = NetAdaptPruner(model, config_list, short_term_fine_tuner=short_term_fine_tuner, evaluator=evaluator,base_algo='l1', experiment_data_dir='./')
pruner.compress()
```
参考[示例](https://github.com/microsoft/nni/blob/master/examples/model_compress/auto_pruners_torch.py)了解更多信息。
#### NetAdapt Pruner 的用户配置
- **sparsity:** 整体的稀疏度目标。
- **op_types:** 要剪枝的操作类型。 如果 `base_algo``l1``l2`,那么 `op_types` 仅支持 `Conv2d`
- **short_term_fine_tuner:** 用于快速微调掩码模型。 此函数只有 `model` 参数,在每次剪枝迭代后,对模型进行快速微调。
示例:
```python
>>> def short_term_fine_tuner(model, epoch=3):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> train_loader = ...
>>> criterion = torch.nn.CrossEntropyLoss()
>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
>>> model.train()
>>> for _ in range(epoch):
>>> for batch_idx, (data, target) in enumerate(train_loader):
>>> data, target = data.to(device), target.to(device)
>>> optimizer.zero_grad()
>>> output = model(data)
>>> loss = criterion(output, target)
>>> loss.backward()
>>> optimizer.step()
```
- **evaluator:** 用于评估掩码模型。 此函数只有 `model` 参数,会返回一个标量值。
示例::
```python
>>> def evaluator(model):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> val_loader = ...
>>> model.eval()
>>> correct = 0
>>> with torch.no_grad():
>>> for data, target in val_loader:
>>> data, target = data.to(device), target.to(device)
>>> output = model(data)
>>> # 获得最大 log 概率分布的索引
>>> pred = output.argmax(dim=1, keepdim=True)
>>> correct += pred.eq(target.view_as(pred)).sum().item()
>>> accuracy = correct / len(val_loader.dataset)
>>> return accuracy
```
- **optimize_mode:** 优化模式,`maximize``minimize`,默认为`maximize`
- **base_algo:** 基础的剪枝算法。 `level``l1``l2`,默认为 `l1`。 给定不同运算符的系数分布,指定的 `base_algo` 会决定对哪个滤波器、通道、权重进行剪枝。
- **sparsity_per_iteration:** 每次迭代要剪枝的稀疏度。 NetAdapt Pruner 在每次迭代时,按相同水平对模型进行剪枝,以便逐步满足计算资源预算。
- **experiment_data_dir:** 保存实验数据的路径,包括为基本剪枝算法生成的 config_list,以及剪枝后模型的性能。
## SimulatedAnnealing Pruner
此 Pruner 基于先验经验,实现了引导式的启发搜索方法,模拟退火(SA)算法。 增强的模拟退火算法基于以下发现:具有更多权重的深度神经网络层通常具有较高的可压缩度,对整体精度的影响更小。
- 随机初始化剪枝率的分布(稀疏度)。
- 当 current_temperature < stop_temperature 时:
1. 对当前分布生成扰动
2. 对扰动的分布进行快速评估
3. 根据性能和概率来决定是否接受扰动,如果不接受,返回步骤 1
4. 冷却,current_temperature = current_temperature * cool_down_rate
更多详细信息,参考 [AutoCompress: An Automatic DNN Structured Pruning Framework for Ultra-High Compression Rates](https://arxiv.org/abs/1907.03141)
#### 用法
PyTorch 代码
```python
from nni.compression.torch import SimulatedAnnealingPruner
config_list = [{
'sparsity': 0.5,
'op_types': ['Conv2d']
}]
pruner = SimulatedAnnealingPruner(model, config_list, evaluator=evaluator, base_algo='l1', cool_down_rate=0.9, experiment_data_dir='./')
pruner.compress()
```
参考[示例](https://github.com/microsoft/nni/blob/master/examples/model_compress/auto_pruners_torch.py)了解更多信息。
#### SimulatedAnnealing Pruner 的用户配置
- **sparsity:** 整体的稀疏度目标。
- **op_types:** 要剪枝的操作类型。 如果 `base_algo``l1``l2`,那么 `op_types` 仅支持 `Conv2d`
- **evaluator:** 用于评估掩码模型。 此函数只有 `model` 参数,会返回一个标量值。 示例::
```python
>>> def evaluator(model):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> val_loader = ...
>>> model.eval()
>>> correct = 0
>>> with torch.no_grad():
>>> for data, target in val_loader:
>>> data, target = data.to(device), target.to(device)
>>> output = model(data)
>>> # 获得最大 log 概率分布的索引
>>> pred = output.argmax(dim=1, keepdim=True)
>>> correct += pred.eq(target.view_as(pred)).sum().item()
>>> accuracy = correct / len(val_loader.dataset)
>>> return accuracy
```
- **optimize_mode:** 优化模式,`maximize``minimize`,默认为`maximize`
- **base_algo:** 基础的剪枝算法。 `level``l1``l2`,默认为 `l1`。 给定不同运算符的系数分布,指定的 `base_algo` 会决定对哪个滤波器、通道、权重进行剪枝。
- **start_temperature:** 模拟退火算法相关参数。
- **stop_temperature:** 模拟退火算法相关参数。
- **cool_down_rate:** 模拟退火算法相关参数。
- **perturbation_magnitude:** 初始化对稀疏度的扰动幅度。 幅度会随着当前温度变小。
- **experiment_data_dir:** 保存实验数据的路径,包括为基本剪枝算法生成的 config_list,剪枝后模型的性能,以及剪枝历史。
## AutoCompress Pruner
每一轮中,AutoCompressPruner 会用相同的稀疏度对模型进行剪枝,从而达到总体的稀疏度:
1. 使用 SimualtedAnnealingPruner 生成稀疏度分布
2. 执行基于 ADMM 的结构化剪枝,为下一轮生成剪枝结果。
这里会使用 `speedup` 来执行真正的剪枝。
更多详细信息,参考 [AutoCompress: An Automatic DNN Structured Pruning Framework for Ultra-High Compression Rates](https://arxiv.org/abs/1907.03141)
#### 用法
PyTorch 代码
```python
from nni.compression.torch import ADMMPruner
config_list = [{
'sparsity': 0.5,
'op_types': ['Conv2d']
}]
pruner = AutoCompressPruner(
model, config_list, trainer=trainer, evaluator=evaluator,
dummy_input=dummy_input, num_iterations=3, optimize_mode='maximize', base_algo='l1',
cool_down_rate=0.9, admm_num_iterations=30, admm_training_epochs=5, experiment_data_dir='./')
pruner.compress()
```
参考[示例](https://github.com/microsoft/nni/blob/master/examples/model_compress/auto_pruners_torch.py)了解更多信息。
#### AutoCompress Pruner 的用户配置
- **sparsity:** 整体的稀疏度目标。
- **op_types:** 要剪枝的操作类型。 如果 `base_algo``l1``l2`,那么 `op_types` 仅支持 `Conv2d`
- **trainer:** 用于第一个子问题的函数。 用户需要实现此函数,来训练 PyTorch 模型,其参数包括:`model, optimizer, criterion, epoch, callback`。 这里的 `callback` 是 L2 规范化,参考原始论文中的公式 (7)。 `callback` 的逻辑在 Pruner 中实现,用户只需要在 `loss.backward()``optimizer.step()` 之间插入 `callback()` 即可。 示例:
```python
>>> def trainer(model, criterion, optimizer, epoch, callback):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> train_loader = ...
>>> 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 = criterion(output, target)
>>> loss.backward()
>>> # 在 loss.backward() 和 optimizer.step() 中插入 callback
>>> if callback:
>>> callback()
>>> optimizer.step()
```
- **evaluator:** 用于评估掩码模型。 此函数只有 `model` 参数,会返回一个标量值。 示例::
```python
>>> def evaluator(model):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> val_loader = ...
>>> model.eval()
>>> correct = 0
>>> with torch.no_grad():
>>> for data, target in val_loader:
>>> data, target = data.to(device), target.to(device)
>>> output = model(data)
>>> # 获得最大 log 概率分布的索引
>>> pred = output.argmax(dim=1, keepdim=True)
>>> correct += pred.eq(target.view_as(pred)).sum().item()
>>> accuracy = correct / len(val_loader.dataset)
>>> return accuracy
```
- **dummy_input:** 用于模型加速的模拟输入,在传入前应该复制到正确的设备上。
- **iterations:** 总共的迭代次数。
- **optimize_mode:** 优化模式,`maximize``minimize`,默认为`maximize`
- **base_algo:** 基础的剪枝算法。 `level``l1``l2`,默认为 `l1`。 给定不同运算符的系数分布,指定的 `base_algo` 会决定对哪个滤波器、通道、权重进行剪枝。
- **start_temperature:** 模拟退火算法相关参数。
- **stop_temperature:** 模拟退火算法相关参数。
- **cool_down_rate:** 模拟退火算法相关参数。
- **perturbation_magnitude:** 初始化对稀疏度的扰动幅度。 幅度会随着当前温度变小。
- **admm_num_iterations:** ADMM Pruner 的迭代次数。
- **admm_training_epochs:** ADMMPruner 的第一个优化子问题训练的 Epoch 数量。
- **experiment_data_dir:** 存储临时实验数据的目录。
## ADMM Pruner
Alternating Direction Method of Multipliers (ADMM) 是一种数学优化技术,它将原始的非凸问题分解为两个可以迭代解决的子问题。 在权重修剪问题中,这两个子问题分别通过 1) 梯度下降算法和 2) 欧几里得投影来解决。
在解决这两个子问题的过程中,原始模型的权重会被改变。 One-Shot Pruner 会根据给定的配置对模型剪枝。
此解决方案框架既适用于非结构化剪枝也适用于结构化剪枝的变体。
更多详细信息,参考 [A Systematic DNN Weight Pruning Framework using Alternating Direction Method of Multipliers](https://arxiv.org/abs/1804.03294)
#### 用法
PyTorch 代码
```python
from nni.compression.torch import ADMMPruner
config_list = [{
'sparsity': 0.8,
'op_types': ['Conv2d'],
'op_names': ['conv1']
}, {
'sparsity': 0.92,
'op_types': ['Conv2d'],
'op_names': ['conv2']
}]
pruner = ADMMPruner(model, config_list, trainer=trainer, num_iterations=30, epochs=5)
pruner.compress()
```
参考[示例](https://github.com/microsoft/nni/blob/master/examples/model_compress/auto_pruners_torch.py)了解更多信息。
#### ADMM Pruner 的用户配置
- **sparsity:**,指定压缩的稀疏度。
- **op_types:** 要剪枝的操作类型。 如果 `base_algo``l1``l2`,那么 `op_types` 仅支持 `Conv2d`
- **trainer:** 用于 ADMM 优化中第一个子问题的函数。注意,微调中不会使用它。 用户需要实现此函数,来训练 PyTorch 模型,其参数包括:`model, optimizer, criterion, epoch, callback`。 这里的 `callback` 是 L2 规范化,参考原始论文中的公式 (7)。 `callback` 的逻辑在 Pruner 中实现,用户只需要在 `loss.backward()``optimizer.step()` 之间插入 `callback()` 即可。
示例:
```python
>>> def trainer(model, criterion, optimizer, epoch, callback):
>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> train_loader = ...
>>> 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 = criterion(output, target)
>>> loss.backward()
>>> # 在 loss.backward() 和 optimizer.step() 中插入 callback
>>> if callback:
>>> callback()
>>> optimizer.step()
```
- **num_iterations:** 迭代次数。
- **training_epochs:** 第一个子问题训练的 Epoch 数量。
- **row:** ADMM 训练的惩罚参数。
- **base_algo:** 基础的剪枝算法。 `level``l1``l2`,默认为 `l1`。 给定不同运算符的系数分布,指定的 `base_algo` 会决定对哪个滤波器、通道、权重进行剪枝。
## Lottery Ticket 假设
[The Lottery Ticket Hypothesis: Finding Sparse, Trainable Neural Networks](https://arxiv.org/abs/1803.03635), 作者 Jonathan Frankle 和 Michael Carbin,提供了全面的测量和分析,并阐明了 *lottery ticket 假设*: 密集的、随机初始化的、包含子网络的前馈网络 (*winning tickets*) -- 在单独训练时 -- 在相似的迭代次数后达到了与原始网络相似的准确度。
本文中,作者使用叫做*迭代剪枝*的方法:
> 1. 随机初始化一个神经网络 f(x;theta_0) (其中 theta_0 为 D_{theta}).
> 2. 将网络训练 j 次,得出参数 theta_j。
> 3. 在 theta_j 修剪参数的 p%,创建掩码 m。
> 4. 将其余参数重置为 theta_0 的值,创建获胜彩票 f(x;m*theta_0)。
> 5. 重复步骤 2、3 和 4。
如果配置的最终稀疏度为 P (e.g., 0.8) 并且有 n 次修建迭代,每次迭代修剪前一轮中剩余权重的 1-(1-P)^(1/n)。
### 用法
PyTorch 代码
```python
from nni.compression.torch import LotteryTicketPruner
config_list = [{
'prune_iterations': 5,
'sparsity': 0.8,
'op_types': ['default']
}]
pruner = LotteryTicketPruner(model, config_list, optimizer)
pruner.compress()
for _ in pruner.get_prune_iterations():
pruner.prune_iteration_start()
for epoch in range(epoch_num):
...
```
上述配置意味着有 5 次迭代修剪。 由于在同一次运行中执行了 5 次修剪,LotteryTicketPruner 需要 `model``optimizer` (**注意,如果使用 `lr_scheduler`,也需要添加**) 来在每次开始新的修剪迭代时,将其状态重置为初始值。 使用 `get_prune_iterations` 来获取修建迭代,并在每次迭代开始时调用 `prune_iteration_start`。 为了模型能较好收敛,`epoch_num` 最好足够大。因为假设是在后几轮中具有较高稀疏度的性能(准确度)可与第一轮获得的相当。
*稍后支持 TensorFlow 版本。*
#### LotteryTicketPruner 的用户配置
* **prune_iterations:** 迭代修剪的次数。
* **sparsity:** 压缩完成后的最终稀疏度。
### 重现实验
在重现时,在 MNIST 使用了与论文相同的配置。 [此处](https://github.com/microsoft/nni/tree/master/examples/model_compress/lottery_torch_mnist_fc.py)为实现代码。 在此实验中,修剪了10次,在每次修剪后,训练了 50 个 epoch。
![](../../img/lottery_ticket_mnist_fc.png)
 
\ No newline at end of file
上图展示了全连接网络的结果。 `round0-sparsity-0.0` 是没有剪枝的性能。 与论文一致,修剪约 80% 也能获得与不修剪时相似的性能,收敛速度也会更快。 如果修剪过多(例如,大于 94%),则精度会降低,收敛速度会稍慢。 与本文稍有不同,论文中数据的趋势比较明显。
NNI Compressor 中的 Quantizer
===
# 支持的量化算法
支持的量化算法列表
* [Naive Quantizer](#naive-quantizer)
* [QAT Quantizer](#qat-quantizer)
* [DoReFa Quantizer](#dorefa-quantizer)
* [BNN Quantizer](#bnn-quantizer)
## Naive Quantizer
Naive Quantizer 将 Quantizer 权重默认设置为 8 位,可用它来测试量化算法。
### 用法
pytorch
PyTorch
```python
model = nni.compression.torch.NaiveQuantizer(model).compress()
```
......@@ -44,7 +50,8 @@ quantizer.compress()
查看示例进一步了解
#### QAT Quantizer 的用户配置
压缩算法所需的常见配置可在[通用配置](./Overview.md#压缩算法中的用户配置)中找到。
压缩算法的公共配置可在 [`config_list` 说明](./QuickStart.md)中找到。
此算法所需的配置:
......@@ -53,13 +60,17 @@ quantizer.compress()
在运行到某步骤前,对模型禁用量化。这让网络在进入更稳定的 状态后再激活量化,这样不会配除掉一些分数显著的值,默认为 0
### 注意
当前不支持批处理规范化折叠。
***
## DoReFa Quantizer
[DoReFa-Net: Training Low Bitwidth Convolutional Neural Networks with Low Bitwidth Gradients](https://arxiv.org/abs/1606.06160) 中,作者 Shuchang Zhou 和 Yuxin Wu 提出了 DoReFa 算法在训练时量化权重,激活函数和梯度。
### 用法
要实现 DoReFa Quantizer,在训练代码前加入以下代码。
PyTorch 代码
......@@ -77,12 +88,15 @@ quantizer.compress()
查看示例进一步了解
#### DoReFa Quantizer 的用户配置
压缩算法所需的常见配置可在[通用配置](./Overview.md#压缩算法中的用户配置)中找到。
压缩算法的公共配置可在 [`config_list` 说明](./QuickStart.md)中找到。
此算法所需的配置:
***
## BNN Quantizer
[Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830) 中,
> 引入了一种训练二进制神经网络(BNN)的方法 - 神经网络在运行时使用二进制权重。 在训练时,二进制权重和激活用于计算参数梯度。 在 forward 过程中,BNN 会大大减少内存大小和访问,并将大多数算术运算替换为按位计算,可显著提高能源效率。
......@@ -113,12 +127,14 @@ model = quantizer.compress()
可以查看示例 [examples/model_compress/BNN_quantizer_cifar10.py](https://github.com/microsoft/nni/tree/master/examples/model_compress/BNN_quantizer_cifar10.py) 了解更多信息。
#### BNN Quantizer 的用户配置
压缩算法所需的常见配置可在[通用配置](./Overview.md#压缩算法中的用户配置)中找到。
压缩算法的公共配置可在 [`config_list` 说明](./QuickStart.md)中找到。
此算法所需的配置:
### 实验
我们实现了 [Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830) 中的一个实验,对 CIFAR-10 上的 **VGGNet** 进行了量化操作。 我们的实验结果如下:
我们实现了 [Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830) 中的一个实验,对 CIFAR-10 上的 **VGGNet** 进行了量化操作。 实验结果如下:
| 模型 | 精度 |
| ------ | ------ |
......
# 模型压缩快速入门
# 模型压缩教程
NNI 为模型压缩提供了非常简单的 API。 压缩包括剪枝和量化算法。 它们的用法相同,这里通过 slim Pruner 来演示如何使用。
```eval_rst
.. contents::
```
本教程中,[第一部分](#模型压缩快速入门)会简单介绍 NNI 上模型压缩的用法。 然后在[第二部分](#使用指南)中进行详细介绍。
## 模型压缩快速入门
NNI 为模型压缩提供了非常简单的 API。 压缩包括剪枝和量化算法。 算法的用法相同,这里以 [slim Pruner](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#slim-pruner) 为例来介绍。
## 编写配置
### 编写配置
编写配置来指定要剪枝的层。 以下配置表示剪枝所有的 `BatchNorm2d`,稀疏度设为 0.7,其它层保持不变。
......@@ -13,9 +21,9 @@ configure_list = [{
}]
```
配置说明在[这里](Overview.md#user-configuration-for-a-compression-algorithm)。 注意,不同的 Pruner 可能有自定义的配置字段,例如,AGP Pruner 有 `start_epoch`。 详情参考每个 Pruner 的 [使用](Overview.md#supported-algorithms),来调整相应的配置。
配置说明在[这里](#config-list-说明)。 注意,不同的 Pruner 可能有自定义的配置字段,例如,AGP Pruner 有 `start_epoch`。 详情参考每个 Pruner 的[使用](./Pruner.md),来调整相应的配置。
## 选择压缩算法
### 选择压缩算法
选择 Pruner 来修剪模型。 首先,使用模型来初始化 Pruner,并将配置作为参数传入,然后调用 `compress()` 来压缩模型。
......@@ -26,7 +34,7 @@ model = pruner.compress()
然后,使用正常的训练方法来训练模型 (如,SGD),剪枝在训练过程中是透明的。 一些 Pruner 只在最开始剪枝一次,接下来的训练可被看作是微调优化。 有些 Pruner 会迭代的对模型剪枝,在训练过程中逐步修改掩码。
## 导出压缩结果
### 导出压缩结果
训练完成后,可获得剪枝后模型的精度。 可将模型权重到处到文件,同时将生成的掩码也导出到文件。 也支持导出 ONNX 模型。
......@@ -36,7 +44,7 @@ pruner.export_model(model_path='pruned_vgg19_cifar10.pth', mask_path='mask_vgg19
模型的完整示例代码在[这里](https://github.com/microsoft/nni/blob/master/examples/model_compress/model_prune_torch.py)
## 加速模型
### 加速模型
掩码实际上并不能加速模型。 要基于导出的掩码,来对模型加速,因此,NNI 提供了 API 来加速模型。 在模型上调用 `apply_compression_results` 后,模型会变得更小,推理延迟也会减小。
......@@ -45,4 +53,118 @@ from nni.compression.torch import apply_compression_results
apply_compression_results(model, 'mask_vgg19_cifar10.pth')
```
参考[这里](ModelSpeedup.md),了解详情。
\ No newline at end of file
参考[这里](ModelSpeedup.md),了解详情。
## 使用指南
将压缩应用到模型的示例代码如下:
PyTorch 代码
```python
from nni.compression.torch import LevelPruner
config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }]
pruner = LevelPruner(model, config_list)
pruner.compress()
```
TensorFlow 代码
```python
from nni.compression.tensorflow import LevelPruner
config_list = [{ 'sparsity': 0.8, 'op_types': ['default'] }]
pruner = LevelPruner(tf.get_default_graph(), config_list)
pruner.compress()
```
可使用 `nni.compression` 中的其它压缩算法。 此算法分别在 `nni.compression.torch``nni.compression.tensorflow` 中实现,支持 PyTorch 和 TensorFlow(部分支持)。 参考 [Pruner](./Pruner.md)[Quantizer](./Quantizer.md) 进一步了解支持的算法。 此外,如果要使用知识蒸馏算法,可参考 [KD 示例](../TrialExample/KDExample.md)
压缩算法首先通过传入 `config_list` 来实例化。 `config_list` 会稍后介绍。
函数调用 `pruner.compress()` 来修改用户定义的模型(在 Tensorflow 中,通过 `tf.get_default_graph()` 来获得模型,而 PyTorch 中 model 是定义的模型类),并修改模型来插入 mask。 然后运行模型时,这些掩码即会生效。 掩码可在运行时通过算法来调整。
*注意,`pruner.compress` 只会在模型权重上直接增加掩码,不包括调优的逻辑。 如果要想调优压缩后的模型,需要在 `pruner.compress` 后增加调优的逻辑。*
### `config_list` 说明
用户可为压缩算法指定配置 (即, `config_list`)。 例如,压缩模型时,用户可能希望指定稀疏率,为不同类型的操作指定不同的稀疏比例,排除某些类型的操作,或仅压缩某类操作。 配置规范可用于表达此类需求。 可将其视为一个 Python 的 `list` 对象,其中每个元素都是一个 `dict` 对象。
`list` 中的 `dict` 会依次被应用,也就是说,如果一个操作出现在两个配置里,后面的 `dict` 会覆盖前面的配置。
`dict` 中有不同的键值。 以下是所有压缩算法都支持的:
* __op_types__:指定要压缩的操作类型。 'default' 表示使用算法的默认设置。
* __op_names__:指定需要压缩的操作的名称。 如果没有设置此字段,操作符不会通过名称筛选。
* __exclude__:默认为 False。 如果此字段为 True,表示要通过类型和名称,将一些操作从压缩中排除。
其它算法的键值,可参考[剪枝算法](./Pruner.md)[量化算法](./Quantizer.md),查看每个算法的键值。
配置的简单示例如下:
```python
[
{
'sparsity': 0.8,
'op_types': ['default']
},
{
'sparsity': 0.6,
'op_names': ['op_name1', 'op_name2']
},
{
'exclude': True,
'op_names': ['op_name3']
}
]
```
其表示压缩操作的默认稀疏度为 0.8,但`op_name1``op_name2` 会使用 0.6,且不压缩 `op_name3`
#### 其它量化算法字段
**如果使用量化算法,则需要设置更多键值。 如果使用剪枝算法,则可以忽略这些键值**
* __quant_types__ : 字符串列表。
要应用量化的类型,当前支持 'weight', 'input', 'output'。 'weight' 是指将量化操作应用到 module 的权重参数上。 'input' 是指对 module 的 forward 方法的输入应用量化操作。 'output' 是指将量化运法应用于模块 forward 方法的输出,有些论文中将其称为 '激活(activation)'。
* __quant_bits__ : int 或 dict {str : int}
量化的位宽,键是量化类型,值是量化位宽度,例如:
```
{
quant_bits: {
'weight': 8,
'output': 4,
},
}
```
当值为 int 类型时,所有量化类型使用相同的位宽。 例如:
```
{
quant_bits: 8, # 权重和输出的位宽都为 8 bits
}
```
### 更新优化状态的 API
一些压缩算法使用 Epoch 来控制压缩过程(如,[AGP](https://nni.readthedocs.io/zh/latest/Compressor/Pruner.html#agp-pruner)),一些算法需要在每个批处理步骤后执行一些逻辑。 因此,NNI 提供了两个 API:`pruner.update_epoch(epoch)``pruner.step()`
`update_epoch` 会在每个 Epoch 时调用,而 `step` 会在每次批处理后调用。 注意,大多数算法不需要调用这两个 API。 详细情况可参考具体算法文档。 对于不需要这两个 API 的算法,可以调用它们,但不会有实际作用。
### 导出压缩模型
使用下列 API 可轻松将压缩后的模型导出,稀疏模型的 `state_dict` 会保存在 `model.pth` 文件中,可通过 `torch.load('model.pth')` 加载。 在导出的 `model.pth` 中,被掩码遮盖的权重为零。
```
pruner.export_model(model_path='model.pth')
```
`mask_dict``onnx` 格式的剪枝模型(需要指定 `input_shape`)可这样导出:
```python
pruner.export_model(model_path='model.pth', mask_path='mask.pth', onnx_path='model.onnx', input_shape=[1, 1, 28, 28])
```
如果需要实际加速压缩后的模型,参考 [NNI 模型加速](./ModelSpeedup.md)
\ No newline at end of file
# NAS 基准测试(测试版)
```eval_rst
.. toctree::
:hidden:
用法示例 <BenchmarksExample>
```
## 先决条件
* 准备目录来保存基准测试的数据库。 默认情况下,目录为 `${HOME}/.nni/nasbenchmark`。 可将其设置为任何位置,并在 import nni 前,通过 `NASBENCHMARK_DIR` 指定。
* 通过 `pip install peewee` 命令安装 `peewee`,NNI 用其连接数据库。
## 准备数据
为了避免存储和法规问题,NNI 不提供数据库。 强烈建议通过 Docker 来运行生成的脚本,减少安装依赖项的时间。 步骤:
**步骤 1.** 克隆 NNI 存储库。 将 `${NNI_VERSION}` 替换为发布的版本或分支名称,例如:`v1.6`
```bash
git clone -b ${NNI_VERSION} https://github.com/microsoft/nni
```
**步骤 2.** 运行 Docker。
对于 NAS-Bench-101,
```bash
docker run -v ${HOME}/.nni/nasbenchmark:/outputs -v /path/to/your/nni:/nni tensorflow/tensorflow:1.15.2-py3 /bin/bash /nni/examples/nas/benchmarks/nasbench101.sh
```
对于 NAS-Bench-201,
```bash
docker run -v ${HOME}/.nni/nasbenchmark:/outputs -v /path/to/your/nni:/nni ufoym/deepo:pytorch-cpu /bin/bash /nni/examples/nas/benchmarks/nasbench201.sh
```
对于 NDS,
```bash
docker run -v ${HOME}/.nni/nasbenchmark:/outputs -v /path/to/your/nni:/nni python:3.7 /bin/bash /nni/examples/nas/benchmarks/nds.sh
```
确保至少有 10GB 的可用磁盘空间,运行过程可能需要几个小时。
## 示例用法
参考[基准测试 API 的用法](./BenchmarksExample)
## NAS-Bench-101
[论文](https://arxiv.org/abs/1902.09635) &nbsp; &nbsp; [代码](https://github.com/google-research/nasbench)
NAS-Bench-101 包含 423,624 个独立的神经网络,再加上 4 个 Epoch (4, 12, 36, 108) 时的变化,以及每个都要训练 3 次。 这是基于 Cell 的搜索空间,通过枚举最多 7 个有向图的运算符来构造并堆叠 Cell,连接数量不超过 9 个。 除了第一个 (必须为 `INPUT`) 和最后一个运算符 (必须为 `OUTPUT`),可选的运算符有 `CONV3X3_BN_RELU`, `CONV1X1_BN_RELU``MAXPOOL3X3`
注意,NAS-Bench-101 消除了非法的 Cell(如,从输入到输出没有路径,或存在冗余的计算)。 此外,同构的 Cell 会被去掉,即,所有的 Cell 从计算上看是一致的。
### API 文档
```eval_rst
.. autofunction:: nni.nas.benchmarks.nasbench101.query_nb101_trial_stats
.. autoattribute:: nni.nas.benchmarks.nasbench101.INPUT
.. autoattribute:: nni.nas.benchmarks.nasbench101.OUTPUT
.. autoattribute:: nni.nas.benchmarks.nasbench101.CONV3X3_BN_RELU
.. autoattribute:: nni.nas.benchmarks.nasbench101.CONV1X1_BN_RELU
.. autoattribute:: nni.nas.benchmarks.nasbench101.MAXPOOL3X3
.. autoclass:: nni.nas.benchmarks.nasbench101.Nb101TrialConfig
.. autoclass:: nni.nas.benchmarks.nasbench101.Nb101TrialStats
.. autoclass:: nni.nas.benchmarks.nasbench101.Nb101IntermediateStats
.. autofunction:: nni.nas.benchmarks.nasbench101.graph_util.nasbench_format_to_architecture_repr
.. autofunction:: nni.nas.benchmarks.nasbench101.graph_util.infer_num_vertices
.. autofunction:: nni.nas.benchmarks.nasbench101.graph_util.hash_module
```
## NAS-Bench-201
[论文](https://arxiv.org/abs/2001.00326) &nbsp; &nbsp; [API](https://github.com/D-X-Y/NAS-Bench-201) &nbsp; &nbsp;[实现](https://github.com/D-X-Y/AutoDL-Projects)
NAS-Bench-201 是单元格的搜索空间,并将张量当作节点,运算符当作边。 搜索空间包含了 4 个节点所有密集连接的有向图,共有 15,625 个候选项。 每个运算符(即:边)从预定义的运算符集中选择 (`NONE`, `SKIP_CONNECT`, `CONV_1X1`, `CONV_3X3``AVG_POOL_3X3`)。 训练方法根据数据集 (CIFAR-10, CIFAR-100, ImageNet) 和 Epoch 数量 (12 和 200),而有所不同。 每个架构和训练方法的组合会随机重复 1 到 3 次。
### API 文档
```eval_rst
.. autofunction:: nni.nas.benchmarks.nasbench201.query_nb201_trial_stats
.. autoattribute:: nni.nas.benchmarks.nasbench201.NONE
.. autoattribute:: nni.nas.benchmarks.nasbench201.SKIP_CONNECT
.. autoattribute:: nni.nas.benchmarks.nasbench201.CONV_1X1
.. autoattribute:: nni.nas.benchmarks.nasbench201.CONV_3X3
.. autoattribute:: nni.nas.benchmarks.nasbench201.AVG_POOL_3X3
.. autoclass:: nni.nas.benchmarks.nasbench201.Nb201TrialConfig
.. autoclass:: nni.nas.benchmarks.nasbench201.Nb201TrialStats
.. autoclass:: nni.nas.benchmarks.nasbench201.Nb201IntermediateStats
```
## NDS
[论文](https://arxiv.org/abs/1905.13214) &nbsp; &nbsp; [代码](https://github.com/facebookresearch/nds)
_On Network Design Spaces for Visual Recognition_ 发布了来自多个模型系列,超过 100,000 个配置(模型加超参组合)的统计,包括 vanilla (受 VGG 启发的松散前馈网络), ResNet 和 ResNeXt (残差基本模块和残差瓶颈模块) 以及 NAS 单元格 (遵循 NASNet, Ameoba, PNAS, ENAS 和 DARTS 的设计)。 大部分配置只采用固定的随机种子训练一次,但少部分会训练两到三次。
NNI 会将不同配置的结果存到单个数据库中,而不是单独的文件中,以便从各个维度进行比较。 在实现上,`model_family` 用来保存模型类型,`model_spec` 用来保存构建模型所需的参数,在使用 NAS 时,`cell_spec` 保存运算符和连接的详细信息,`generator` 表示配置生成的采样策略。 详情可参考 API 文档。
## 可用的运算符
NDS 中可用的运算符列表。
```eval_rst
.. autoattribute:: nni.nas.benchmarks.nds.constants.NONE
.. autoattribute:: nni.nas.benchmarks.nds.constants.SKIP_CONNECT
.. autoattribute:: nni.nas.benchmarks.nds.constants.AVG_POOL_3X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.MAX_POOL_3X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.MAX_POOL_5X5
.. autoattribute:: nni.nas.benchmarks.nds.constants.MAX_POOL_7X7
.. autoattribute:: nni.nas.benchmarks.nds.constants.CONV_1X1
.. autoattribute:: nni.nas.benchmarks.nds.constants.CONV_3X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.CONV_3X1_1X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.CONV_7X1_1X7
.. autoattribute:: nni.nas.benchmarks.nds.constants.DIL_CONV_3X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.DIL_CONV_5X5
.. autoattribute:: nni.nas.benchmarks.nds.constants.SEP_CONV_3X3
.. autoattribute:: nni.nas.benchmarks.nds.constants.SEP_CONV_5X5
.. autoattribute:: nni.nas.benchmarks.nds.constants.SEP_CONV_7X7
.. autoattribute:: nni.nas.benchmarks.nds.constants.DIL_SEP_CONV_3X3
```
### API 文档
```eval_rst
.. autofunction:: nni.nas.benchmarks.nds.query_nds_trial_stats
.. autoclass:: nni.nas.benchmarks.nds.NdsTrialConfig
.. autoclass:: nni.nas.benchmarks.nds.NdsTrialStats
.. autoclass:: nni.nas.benchmarks.nds.NdsIntermediateStats
```
\ No newline at end of file
{
"nbformat": 4,
"nbformat_minor": 2,
"metadata": {
"language_info": {
"name": "python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"version": "3.6.10-final"
},
"orig_nbformat": 2,
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"npconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": 3,
"kernelspec": {
"name": "python361064bitnnilatestcondabff8d66a619a4d26af34fe0fe687c7b0",
"display_name": "Python 3.6.10 64-bit ('nnilatest': conda)"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# NAS 基准测试示例"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import pprint\n",
"import time\n",
"\n",
"from nni.nas.benchmarks.nasbench101 import query_nb101_trial_stats\n",
"from nni.nas.benchmarks.nasbench201 import query_nb201_trial_stats\n",
"from nni.nas.benchmarks.nds import query_nds_trial_stats\n",
"\n",
"ti = time.time()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## NAS-Bench-101"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'config': {'arch': {'input1': [0],\n 'input2': [1],\n 'input3': [2],\n 'input4': [0],\n 'input5': [0, 3, 4],\n 'input6': [2, 5],\n 'op1': 'conv3x3-bn-relu',\n 'op2': 'maxpool3x3',\n 'op3': 'conv3x3-bn-relu',\n 'op4': 'conv3x3-bn-relu',\n 'op5': 'conv1x1-bn-relu'},\n 'hash': '00005c142e6f48ac74fdcf73e3439874',\n 'id': 4,\n 'num_epochs': 108,\n 'num_vertices': 7},\n 'id': 10,\n 'parameters': 8.55553,\n 'test_acc': 92.11738705635071,\n 'train_acc': 100.0,\n 'training_time': 106147.67578125,\n 'valid_acc': 92.41786599159241}\n{'config': {'arch': {'input1': [0],\n 'input2': [1],\n 'input3': [2],\n 'input4': [0],\n 'input5': [0, 3, 4],\n 'input6': [2, 5],\n 'op1': 'conv3x3-bn-relu',\n 'op2': 'maxpool3x3',\n 'op3': 'conv3x3-bn-relu',\n 'op4': 'conv3x3-bn-relu',\n 'op5': 'conv1x1-bn-relu'},\n 'hash': '00005c142e6f48ac74fdcf73e3439874',\n 'id': 4,\n 'num_epochs': 108,\n 'num_vertices': 7},\n 'id': 11,\n 'parameters': 8.55553,\n 'test_acc': 91.90705418586731,\n 'train_acc': 100.0,\n 'training_time': 106095.05859375,\n 'valid_acc': 92.45793223381042}\n{'config': {'arch': {'input1': [0],\n 'input2': [1],\n 'input3': [2],\n 'input4': [0],\n 'input5': [0, 3, 4],\n 'input6': [2, 5],\n 'op1': 'conv3x3-bn-relu',\n 'op2': 'maxpool3x3',\n 'op3': 'conv3x3-bn-relu',\n 'op4': 'conv3x3-bn-relu',\n 'op5': 'conv1x1-bn-relu'},\n 'hash': '00005c142e6f48ac74fdcf73e3439874',\n 'id': 4,\n 'num_epochs': 108,\n 'num_vertices': 7},\n 'id': 12,\n 'parameters': 8.55553,\n 'test_acc': 92.15745329856873,\n 'train_acc': 100.0,\n 'training_time': 106138.55712890625,\n 'valid_acc': 93.04887652397156}\n"
}
],
"source": [
"arch = {\n",
" 'op1': 'conv3x3-bn-relu',\n",
" 'op2': 'maxpool3x3',\n",
" 'op3': 'conv3x3-bn-relu',\n",
" 'op4': 'conv3x3-bn-relu',\n",
" 'op5': 'conv1x1-bn-relu',\n",
" 'input1': [0],\n",
" 'input2': [1],\n",
" 'input3': [2],\n",
" 'input4': [0],\n",
" 'input5': [0, 3, 4],\n",
" 'input6': [2, 5]\n",
"}\n",
"for t in query_nb101_trial_stats(arch, 108):\n",
" pprint.pprint(t)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## NAS-Bench-201"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'config': {'arch': {'0_1': 'avg_pool_3x3',\n '0_2': 'conv_1x1',\n '0_3': 'conv_1x1',\n '1_2': 'skip_connect',\n '1_3': 'skip_connect',\n '2_3': 'skip_connect'},\n 'dataset': 'cifar100',\n 'id': 7,\n 'num_cells': 5,\n 'num_channels': 16,\n 'num_epochs': 200},\n 'flops': 15.65322,\n 'id': 3,\n 'latency': 0.013182918230692545,\n 'ori_test_acc': 53.11,\n 'ori_test_evaluation_time': 1.0195916947864352,\n 'ori_test_loss': 1.7307863704681397,\n 'parameters': 0.135156,\n 'seed': 999,\n 'test_acc': 53.07999995727539,\n 'test_evaluation_time': 0.5097958473932176,\n 'test_loss': 1.731276072692871,\n 'train_acc': 57.82,\n 'train_loss': 1.5116578379058838,\n 'training_time': 2888.4371995925903,\n 'valid_acc': 53.14000000610351,\n 'valid_evaluation_time': 0.5097958473932176,\n 'valid_loss': 1.7302966793060304}\n{'config': {'arch': {'0_1': 'avg_pool_3x3',\n '0_2': 'conv_1x1',\n '0_3': 'conv_1x1',\n '1_2': 'skip_connect',\n '1_3': 'skip_connect',\n '2_3': 'skip_connect'},\n 'dataset': 'cifar100',\n 'id': 7,\n 'num_cells': 5,\n 'num_channels': 16,\n 'num_epochs': 200},\n 'flops': 15.65322,\n 'id': 7,\n 'latency': 0.013182918230692545,\n 'ori_test_acc': 51.93,\n 'ori_test_evaluation_time': 1.0195916947864352,\n 'ori_test_loss': 1.7572312774658203,\n 'parameters': 0.135156,\n 'seed': 777,\n 'test_acc': 51.979999938964845,\n 'test_evaluation_time': 0.5097958473932176,\n 'test_loss': 1.7429540189743042,\n 'train_acc': 57.578,\n 'train_loss': 1.5114233912658692,\n 'training_time': 2888.4371995925903,\n 'valid_acc': 51.88,\n 'valid_evaluation_time': 0.5097958473932176,\n 'valid_loss': 1.7715086591720581}\n{'config': {'arch': {'0_1': 'avg_pool_3x3',\n '0_2': 'conv_1x1',\n '0_3': 'conv_1x1',\n '1_2': 'skip_connect',\n '1_3': 'skip_connect',\n '2_3': 'skip_connect'},\n 'dataset': 'cifar100',\n 'id': 7,\n 'num_cells': 5,\n 'num_channels': 16,\n 'num_epochs': 200},\n 'flops': 15.65322,\n 'id': 11,\n 'latency': 0.013182918230692545,\n 'ori_test_acc': 53.38,\n 'ori_test_evaluation_time': 1.0195916947864352,\n 'ori_test_loss': 1.7281623031616211,\n 'parameters': 0.135156,\n 'seed': 888,\n 'test_acc': 53.67999998779297,\n 'test_evaluation_time': 0.5097958473932176,\n 'test_loss': 1.7327697801589965,\n 'train_acc': 57.792,\n 'train_loss': 1.5091403088760376,\n 'training_time': 2888.4371995925903,\n 'valid_acc': 53.08000000610352,\n 'valid_evaluation_time': 0.5097958473932176,\n 'valid_loss': 1.7235548280715942}\n"
}
],
"source": [
"arch = {\n",
" '0_1': 'avg_pool_3x3',\n",
" '0_2': 'conv_1x1',\n",
" '1_2': 'skip_connect',\n",
" '0_3': 'conv_1x1',\n",
" '1_3': 'skip_connect',\n",
" '2_3': 'skip_connect'\n",
"}\n",
"for t in query_nb201_trial_stats(arch, 200, 'cifar100'):\n",
" pprint.pprint(t)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## NDS"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'best_test_acc': 90.48,\n 'best_train_acc': 96.356,\n 'best_train_loss': 0.116,\n 'config': {'base_lr': 0.1,\n 'cell_spec': {},\n 'dataset': 'cifar10',\n 'generator': 'random',\n 'id': 45505,\n 'model_family': 'residual_bottleneck',\n 'model_spec': {'bot_muls': [0.0, 0.25, 0.25, 0.25],\n 'ds': [1, 16, 1, 4],\n 'num_gs': [1, 2, 1, 2],\n 'ss': [1, 1, 2, 2],\n 'ws': [16, 64, 128, 16]},\n 'num_epochs': 100,\n 'proposer': 'resnext-a',\n 'weight_decay': 0.0005},\n 'final_test_acc': 90.39,\n 'final_train_acc': 96.298,\n 'final_train_loss': 0.116,\n 'flops': 69.890986,\n 'id': 45505,\n 'iter_time': 0.065,\n 'parameters': 0.083002,\n 'seed': 1}\n"
}
],
"source": [
"model_spec = {\n",
" 'bot_muls': [0.0, 0.25, 0.25, 0.25],\n",
" 'ds': [1, 16, 1, 4],\n",
" 'num_gs': [1, 2, 1, 2],\n",
" 'ss': [1, 1, 2, 2],\n",
" 'ws': [16, 64, 128, 16]\n",
"}\n",
"# Use none as a wildcard\n",
"for t in query_nds_trial_stats('residual_bottleneck', None, None, model_spec, None, 'cifar10'):\n",
" pprint.pprint(t)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'best_test_acc': 93.58,\n 'best_train_acc': 99.772,\n 'best_train_loss': 0.011,\n 'config': {'base_lr': 0.1,\n 'cell_spec': {},\n 'dataset': 'cifar10',\n 'generator': 'random',\n 'id': 108998,\n 'model_family': 'residual_basic',\n 'model_spec': {'ds': [1, 12, 12, 12],\n 'ss': [1, 1, 2, 2],\n 'ws': [16, 24, 24, 40]},\n 'num_epochs': 100,\n 'proposer': 'resnet',\n 'weight_decay': 0.0005},\n 'final_test_acc': 93.49,\n 'final_train_acc': 99.772,\n 'final_train_loss': 0.011,\n 'flops': 184.519578,\n 'id': 108998,\n 'iter_time': 0.059,\n 'parameters': 0.594138,\n 'seed': 1}\n"
}
],
"source": [
"model_spec = {'ds': [1, 12, 12, 12], 'ss': [1, 1, 2, 2], 'ws': [16, 24, 24, 40]}\n",
"for t in query_nds_trial_stats('residual_basic', 'resnet', 'random', model_spec, {}, 'cifar10'):\n",
" pprint.pprint(t)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'best_test_acc': 84.5,\n 'best_train_acc': 89.66499999999999,\n 'best_train_loss': 0.302,\n 'config': {'base_lr': 0.1,\n 'cell_spec': {},\n 'dataset': 'cifar10',\n 'generator': 'random',\n 'id': 139492,\n 'model_family': 'vanilla',\n 'model_spec': {'ds': [1, 12, 12, 12],\n 'ss': [1, 1, 2, 2],\n 'ws': [16, 24, 32, 40]},\n 'num_epochs': 100,\n 'proposer': 'vanilla',\n 'weight_decay': 0.0005},\n 'final_test_acc': 84.35,\n 'final_train_acc': 89.633,\n 'final_train_loss': 0.303,\n 'flops': 208.36393,\n 'id': 154692,\n 'iter_time': 0.058,\n 'parameters': 0.68977,\n 'seed': 1}\n"
}
],
"source": [
"# get the first one\n",
"pprint.pprint(next(query_nds_trial_stats('vanilla', None, None, None, None, None)))"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "{'best_test_acc': 93.37,\n 'best_train_acc': 99.91,\n 'best_train_loss': 0.006,\n 'config': {'base_lr': 0.1,\n 'cell_spec': {'normal_0_input_x': 0,\n 'normal_0_input_y': 1,\n 'normal_0_op_x': 'avg_pool_3x3',\n 'normal_0_op_y': 'conv_7x1_1x7',\n 'normal_1_input_x': 2,\n 'normal_1_input_y': 0,\n 'normal_1_op_x': 'sep_conv_3x3',\n 'normal_1_op_y': 'sep_conv_5x5',\n 'normal_2_input_x': 2,\n 'normal_2_input_y': 2,\n 'normal_2_op_x': 'dil_sep_conv_3x3',\n 'normal_2_op_y': 'dil_sep_conv_3x3',\n 'normal_3_input_x': 4,\n 'normal_3_input_y': 4,\n 'normal_3_op_x': 'skip_connect',\n 'normal_3_op_y': 'dil_sep_conv_3x3',\n 'normal_4_input_x': 2,\n 'normal_4_input_y': 4,\n 'normal_4_op_x': 'conv_7x1_1x7',\n 'normal_4_op_y': 'sep_conv_3x3',\n 'normal_concat': [3, 5, 6],\n 'reduce_0_input_x': 0,\n 'reduce_0_input_y': 1,\n 'reduce_0_op_x': 'avg_pool_3x3',\n 'reduce_0_op_y': 'dil_sep_conv_3x3',\n 'reduce_1_input_x': 0,\n 'reduce_1_input_y': 0,\n 'reduce_1_op_x': 'sep_conv_3x3',\n 'reduce_1_op_y': 'sep_conv_3x3',\n 'reduce_2_input_x': 2,\n 'reduce_2_input_y': 0,\n 'reduce_2_op_x': 'skip_connect',\n 'reduce_2_op_y': 'sep_conv_7x7',\n 'reduce_3_input_x': 4,\n 'reduce_3_input_y': 4,\n 'reduce_3_op_x': 'conv_7x1_1x7',\n 'reduce_3_op_y': 'skip_connect',\n 'reduce_4_input_x': 0,\n 'reduce_4_input_y': 5,\n 'reduce_4_op_x': 'conv_7x1_1x7',\n 'reduce_4_op_y': 'conv_7x1_1x7',\n 'reduce_concat': [3, 6]},\n 'dataset': 'cifar10',\n 'generator': 'random',\n 'id': 1,\n 'model_family': 'nas_cell',\n 'model_spec': {'aux': False,\n 'depth': 12,\n 'drop_prob': 0.0,\n 'num_nodes_normal': 5,\n 'num_nodes_reduce': 5,\n 'width': 32},\n 'num_epochs': 100,\n 'proposer': 'amoeba',\n 'weight_decay': 0.0005},\n 'final_test_acc': 93.27,\n 'final_train_acc': 99.91,\n 'final_train_loss': 0.006,\n 'flops': 664.400586,\n 'id': 1,\n 'iter_time': 0.281,\n 'parameters': 4.190314,\n 'seed': 1}\n"
}
],
"source": [
"# count number\n",
"model_spec = {'num_nodes_normal': 5, 'num_nodes_reduce': 5, 'depth': 12, 'width': 32, 'aux': False, 'drop_prob': 0.0}\n",
"cell_spec = {\n",
" 'normal_0_op_x': 'avg_pool_3x3',\n",
" 'normal_0_input_x': 0,\n",
" 'normal_0_op_y': 'conv_7x1_1x7',\n",
" 'normal_0_input_y': 1,\n",
" 'normal_1_op_x': 'sep_conv_3x3',\n",
" 'normal_1_input_x': 2,\n",
" 'normal_1_op_y': 'sep_conv_5x5',\n",
" 'normal_1_input_y': 0,\n",
" 'normal_2_op_x': 'dil_sep_conv_3x3',\n",
" 'normal_2_input_x': 2,\n",
" 'normal_2_op_y': 'dil_sep_conv_3x3',\n",
" 'normal_2_input_y': 2,\n",
" 'normal_3_op_x': 'skip_connect',\n",
" 'normal_3_input_x': 4,\n",
" 'normal_3_op_y': 'dil_sep_conv_3x3',\n",
" 'normal_3_input_y': 4,\n",
" 'normal_4_op_x': 'conv_7x1_1x7',\n",
" 'normal_4_input_x': 2,\n",
" 'normal_4_op_y': 'sep_conv_3x3',\n",
" 'normal_4_input_y': 4,\n",
" 'normal_concat': [3, 5, 6],\n",
" 'reduce_0_op_x': 'avg_pool_3x3',\n",
" 'reduce_0_input_x': 0,\n",
" 'reduce_0_op_y': 'dil_sep_conv_3x3',\n",
" 'reduce_0_input_y': 1,\n",
" 'reduce_1_op_x': 'sep_conv_3x3',\n",
" 'reduce_1_input_x': 0,\n",
" 'reduce_1_op_y': 'sep_conv_3x3',\n",
" 'reduce_1_input_y': 0,\n",
" 'reduce_2_op_x': 'skip_connect',\n",
" 'reduce_2_input_x': 2,\n",
" 'reduce_2_op_y': 'sep_conv_7x7',\n",
" 'reduce_2_input_y': 0,\n",
" 'reduce_3_op_x': 'conv_7x1_1x7',\n",
" 'reduce_3_input_x': 4,\n",
" 'reduce_3_op_y': 'skip_connect',\n",
" 'reduce_3_input_y': 4,\n",
" 'reduce_4_op_x': 'conv_7x1_1x7',\n",
" 'reduce_4_input_x': 0,\n",
" 'reduce_4_op_y': 'conv_7x1_1x7',\n",
" 'reduce_4_input_y': 5,\n",
" 'reduce_concat': [3, 6]\n",
"}\n",
"\n",
"for t in query_nds_trial_stats('nas_cell', None, None, model_spec, cell_spec, 'cifar10'):\n",
" assert t['config']['model_spec'] == model_spec\n",
" assert t['config']['cell_spec'] == cell_spec\n",
" pprint.pprint(t)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "NDS (amoeba) count: 5107\n"
}
],
"source": [
"# count number\n",
"print('NDS (amoeba) count:', len(list(query_nds_trial_stats(None, 'amoeba', None, None, None, None, None))))"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "Elapsed time: 1.9107539653778076 seconds\n"
}
],
"source": [
"print('Elapsed time: ', time.time() - ti, 'seconds')"
]
}
]
}
\ No newline at end of file
# 经典 NAS 算法
在经典 NAS 算法中,每个结构都作为 Trial 来训练,而 NAS 算法来充当 Tuner。 因此,训练过程能使用 NNI 中的超参调优框架,Tuner 为下一个 Trial 生成新的结构,Trial 在训练平台中运行。
## 快速入门
下例展示了如何使用经典 NAS 算法。 与 NNI 超参优化非常相似。
```python
model = Net()
# 从 Tuner 中获得选择的架构,并应用到模型上
get_and_apply_next_architecture(model)
train(model) # 训练模型的代码
acc = test(model) # 测试训练好的模型
nni.report_final_result(acc) # 报告所选架构的性能
```
首先,实例化模型。 模型中,搜索空间通过 `LayerChoice``InputChoice` 来定义。 然后,调用 `get_and_apply_next_architecture(model)` 来获得特定的结构。 此函数会从 Tuner (即,经典的 NAS 算法)中接收结构,并应用到 `model` 上。 此时,`model` 成为了某个结构,不再是搜索空间。 然后可以像普通 PyTorch 模型一样训练此模型。 获得模型精度后,调用 `nni.report_final_result(acc)` 来返回给 Tuner。
至此,Trial 代码已准备好了。 然后,准备好 NNI 的 Experiment,即搜索空间文件和 Experiment 配置文件。 与 NNI 超参优化不同的是,要通过运行命令(详情参考[这里](../Tutorial/Nnictl.md))从 Trial 代码中自动生成搜索空间文件。
`nnictl ss_gen --trial_command="运行 Trial 代码的命令"`
此命令会自动生成 `nni_auto_gen_search_space.json` 文件。 然后,将生成的搜索空间文件路径填入 Experiment 配置文件的 `searchSpacePath` 字段。 配置文件中的其它字段,可参考[此教程](../Tutorial/QuickStart.md)
目前,经典 NAS 仅支持 [PPO Tuner](../Tuner/BuiltinTuner.md)[随机 Tuner ](https://github.com/microsoft/nni/tree/master/examples/tuners/random_nas_tuner)。 未来将支持更多经典 NAS 算法。
完整的 [PyTorch 示例](https://github.com/microsoft/nni/tree/master/examples/nas/classic_nas),以及 [TensorFlow 示例](https://github.com/microsoft/nni/tree/master/examples/nas/classic_nas-tf)
## 用于调试的独立模式
为了便于调试,其支持独立运行模式,可直接运行 Trial 命令,而不启动 NNI Experiment。 可以通过此方法来检查 Trial 代码是否可正常运行。 在独立模式下,`LayerChoice``InputChoice` 会选择第一个的候选项。
\ No newline at end of file
# 指南:在 NNI 上使用 NAS
# One-Shot NAS algorithms
```eval_rst
.. contents::
.. Note:: 此 API 初始试验阶段。 当前接口可能会更改。
```
![](../../img/nas_abstract_illustration.png)
现代神经架构搜索(NAS)方法通常包含 [三个维度](https://arxiv.org/abs/1808.05377):搜索空间、搜索策略和性能估计策略。 搜索空间通常是要搜索的一个有限的神经网络架构,而搜索策略会采样来自搜索空间的架构,评估性能,并不断演进。 理想情况下,搜索策略会找到搜索空间中最好的架构,并返回给用户。 在获得了 "最好架构" 后,很多方法都会有 "重新训练" 的步骤,会像普通神经网络模型一样训练。
## 实现搜索空间
假设已经有了基础的模型,该如何使用 NAS 来提升? 以 [PyTorch 上的 MNIST](https://github.com/pytorch/examples/blob/master/mnist/main.py) 为例,代码如下:
```python
from nni.nas.pytorch import mutables
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = mutables.LayerChoice([
nn.Conv2d(1, 32, 3, 1),
nn.Conv2d(1, 32, 5, 3)
]) # try 3x3 kernel and 5x5 kernel
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
# ... 与原始的一样 ...
返回输出
```
以上示例在 conv1 上添加了 conv5x5 的选项。 修改非常简单,只需要声明 `LayerChoice` 并将原始的 conv3x3 和新的 conv5x5 作为参数即可。 就这么简单! 不需要修改 forward 函数。 可将 conv1 想象为没有 NAS 的模型。
如何表示可能的连接? 通过 `InputChoice` 来实现。 要在 MNIST 示例上使用跳过连接,需要增加另一层 conv3。 下面的示例中,从 conv2 的可能连接加入到了 conv3 的输出中。
```python
from nni.nas.pytorch import mutables
class Net(nn.Module):
def __init__(self):
# ... 相同 ...
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.conv3 = nn.Conv2d(64, 64, 1, 1)
# 声明搜索策略,来选择最多一个选项
self.skipcon = mutables.InputChoice(n_candidates=1)
# ... 相同 ...
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x0 = self.skipcon([x]) # 从 [x] 中选择 0 或 1 个
x = self.conv3(x)
if x0 is not None: # 允许跳过连接
x += x0
x = F.max_pool2d(x, 2)
# ... 相同 ...
返回输出
```
Input Choice 可被视为可调用的模块,它接收张量数组,输出其中部分的连接、求和、平均(默认为求和),或没有选择时输出 `None`。 与 Layer Choice 一样,Input Choice 要**`__init__` 中初始化,并在 `forward` 中调用。 稍后的例子中会看到搜索算法如何识别这些 Choice,并进行相应的准备。</p>
`LayerChoice``InputChoice` 都是 **Mutable**。 Mutable 表示 "可变化的"。 与传统深度学习层、模型都是固定的不同,使用 Mutable 的模块,是一组可能选择的模型。
用户可为每个 Mutable 指定 **key**。 默认情况下,NNI 会分配全局唯一的,但如果需要共享 Choice(例如,两个 `LayerChoice` 有同样的候选操作,希望共享同样的 Choice。即,如果一个选择了第 i 个操作,第二个也要选择第 i 个操作),那么就应该给它们相同的 key。 key 标记了此 Choice,并会在存储的检查点中使用。 如果要增加导出架构的可读性,可为每个 Mutable 的 key 指派名称。 高级用法参考 [Mutable](./NasReference.md)
## 使用搜索算法
除了使用搜索空间外,还可以通过其他两种方式进行搜索。 一种是分布式运行 NAS,可从头枚举运行所有架构。或者利用更多高级功能,如 [SMASH](https://arxiv.org/abs/1708.05344), [ENAS](https://arxiv.org/abs/1802.03268), [DARTS](https://arxiv.org/abs/1808.05377), [FBNet](https://arxiv.org/abs/1812.03443), [ProxylessNAS](https://arxiv.org/abs/1812.00332), [SPOS](https://arxiv.org/abs/1904.00420), [Single-Path NAS](https://arxiv.org/abs/1904.02877), [Understanding One-shot](http://proceedings.mlr.press/v80/bender18a) 以及 [GDAS](https://arxiv.org/abs/1910.04465)。 由于很多不同架构搜索起来成本较高,另一类方法,即 One-Shot NAS,在搜索空间中,构建包含有所有候选网络的超网络,每一步中选择一个或几个子网络来训练。
除了 [经典 NAS 算法](./ClassicNas.md),还可以使用更先进的 One-Shot NAS 算法来从搜索空间中找到更好的模型。 One-Shot NAS 算法已有了大量的相关工作,如 [SMASH](https://arxiv.org/abs/1708.05344), [ENAS](https://arxiv.org/abs/1802.03268), [DARTS](https://arxiv.org/abs/1808.05377), [FBNet](https://arxiv.org/abs/1812.03443), [ProxylessNAS](https://arxiv.org/abs/1812.00332), [SPOS](https://arxiv.org/abs/1904.00420), [Single-Path NAS](https://arxiv.org/abs/1904.02877), [Understanding One-shot](http://proceedings.mlr.press/v80/bender18a) 以及 [GDAS](https://arxiv.org/abs/1910.04465)。 One-Shot NAS 算法通常会构建一个超网络,其中包含的子网作为此搜索空间的候选项。每一步,会训练一个或多个子网的组合。
当前,NNI 支持数种 One-Shot 方法。 例如,`DartsTrainer` 使用 SGD 来交替训练架构和模型权重,`ENASTrainer` [使用 Controller 来训练模型](https://arxiv.org/abs/1802.03268)。 新的、更高效的 NAS Trainer 在研究界不断的涌现出来,NNI 会在将来的版本中实现其中的一部分。
### One-Shot NAS
## 使用 One-Shot NAS 算法进行搜索
每个 One-Shot NAS 算法都实现了 Trainer,可在每种算法说明中找到详细信息。 这是如何使用 `EnasTrainer` 的简单示例。
......@@ -99,7 +25,7 @@ def top1_accuracy(output, target):
def metrics_fn(output, target):
# 指标函数接收输出和目标,并计算出指标 dict
return {"acc1": reward_accuracy(output, target)}
return {"acc1": top1_accuracy(output, target)}
from nni.nas.pytorch import enas
trainer = enas.EnasTrainer(model,
......@@ -116,39 +42,17 @@ trainer.train() # 训练
trainer.export(file="model_dir/final_architecture.json") # 将最终架构导出到文件
```
用户可直接通过 `python3 train.py` 开始训练,不需要使用 `nnictl`。 训练完成后,可通过 `trainer.export()` 导出找到的最好的模型。
通常,Trainer 会提供一些可以自定义的参数。 如,损失函数,指标函数,优化器以及数据集。 这些功能可满足大部分需求,NNI 会尽力让内置 Trainer 能够处理更多的模型、任务和数据集。 但无法保证全面的支持。 例如,一些 Trainer 假设必须是分类任务;一些 Trainer 对 "Epoch" 的定义有所不同(例如,ENAS 的 epoch 表示一部分子步骤加上一些 Controller 的步骤);大多数 Trainer 不支持分布式训练,不会将模型通过 `DataParallel``DistributedDataParallel` 进行包装。 如果通过试用,想要在定制的应用中使用 Trainer,可能需要[自定义 Trainer](./Advanced.md#extend-the-ability-of-one-shot-trainers)
此外,可以使用 NAS 可视化来显示 One-Shot NAS。 [了解详情](./Visualization.md)
### 分布式 NAS
神经网络架构搜索通过在 Trial 任务中独立运行单个子模型来实现。 NNI 同样支持这种搜索方法,其天然适用于 NNI 的超参搜索框架。Tuner 为每个 Trial 生成子模型,并在训练平台上运行。
要使用此模式,不需要修改 NNI NAS API 的搜索空间定义 (即, `LayerChoice`, `InputChoice`, `MutableScope`)。 模型初始化后,在模型上调用 `get_and_apply_next_architecture`。 One-shot NAS Trainer 不能在此模式中使用。 简单示例:
```python
model = Net()
# 从 Tuner 中获得选择的架构,并应用到模型上
get_and_apply_next_architecture(model)
train(model) # 训练模型的代码
acc = test(model) # 测试训练好的模型
nni.report_final_result(acc) # 报告所选架构的性能
```
搜索空间应生成,并发送给 Tuner。 与 NNI NAS API 一样,搜索空间嵌入到了用户代码中。 用户可以使用 "[nnictl ss_gen](../Tutorial/Nnictl.md)" 以生成搜索空间文件。 然后,将生成的搜索空间文件路径填入 `config.yml``searchSpacePath``config.yml` 中的其它字段参考[教程](../Tutorial/QuickStart.md)
`model` 是一个[用户定义的搜索空间](./WriteSearchSpace.md)。 然后需要准备搜索数据和模型评估指标。 要从定义的搜索空间中进行搜索,需要实例化 One-Shot 算法,即 Trainer(如,EnasTrainer)。 Trainer 会提供一些可以自定义的参数。 如,损失函数,指标函数,优化器以及数据集。 这些功能可满足大部分需求,NNI 会尽力让内置 Trainer 能够处理更多的模型、任务和数据集。
可使用 [NNI Tuner](../Tuner/BuiltinTuner.md) 来搜索。 目前,只有 PPO Tuner 支持 NAS 搜索空间
**注意**,在使用 One-Shot NAS 算法时,不需要启动 NNI Experiment。 不需要 `nnictl`,可直接运行 Python 脚本(即:`train.py`),如:`python3 train.py`。 训练完成后,可通过 `trainer.export()` 导出找到的最好的模型
为了便于调试,其支持独立运行模式,可直接运行 Trial 命令,而不启动 NNI Experiment。 可以通过此方法来检查 Trial 代码是否可正常运行。 在独立模式下,`LayerChoice``InputChoice` 会选择最开始的候选项
NNI 中每个 Trainer 都用其对应的场景和用法。 一些 Trainer 假定任务是分类任务;一些 Trainer 对 "epoch" 有不同的定义(如:ENAS 的每个 Epoch 是 一些子步骤加上 Controller 的步骤)。 大部分 Trainer 不支持分布式训练:没有使用 `DataParallel``DistributedDataParallel` 来包装模型。 因此,在试用后,如果要在自己的应用中使用 Trainer,需要[自定义 Trainer](./Advanced.md#extend-the-ability-of-one-shot-trainers)
[此处](https://github.com/microsoft/nni/tree/master/examples/nas/classic_nas/config_nas.yml)是完整示例
此外,可以使用 NAS 可视化来显示 One-Shot NAS。 [了解详情](./Visualization.md)
### 使用导出的架构重新训练
搜索阶段后,就该训练找到的架构了。 与很多开源 NAS 算法不同,这些算法为重新训练实现了新的模型。 实际上搜索模型和重新训练模型的过程非常相似,因而可直接将一样的模型代码用到最终模型上。 例如
搜索阶段后,就该训练找到的架构了。 与很多开源 NAS 算法不同,它们为重新训练专门写了新的模型。 实际上搜索模型和重新训练模型的过程非常相似,因而可直接将一样的模型代码用到最终模型上。 例如
```python
model = Net()
......@@ -175,6 +79,6 @@ apply_fixed_architecture(model, "model_dir/final_architecture.json")
}
```
应用后,模型会被固定,并准备好进行最终训练。 虽然它可能包含了更多的参数,但可作为单模型来使用。 这各有利弊。 好的方面是,可以在搜索阶段直接读取来自超网络的检查点,并开始重新训练。 但是,这也造成模型有冗余的参数,在计算模型所包含的参数数量时,可能会不准确。 更多深层次原因和解决方法可参考 [Trainer](./NasReference.md)
应用后,模型会被固定,并准备好进行最终训练。 该模型作为单独的模型来工作,未使用的参数和模块已被剪除
也可参考 [DARTS](./DARTS.md) 的重新训练代码。
# 神经网络结构搜索在 NNI 上的应用
```eval_rst
.. contents::
```
## 概述
自动化的神经网络架构(NAS)搜索在寻找更好的模型方面发挥着越来越重要的作用。 最近的研究工作证明了自动化 NAS 的可行性,并发现了一些超越手动设计和调整的模型。 代表算法有 [NASNet](https://arxiv.org/abs/1707.07012)[ENAS](https://arxiv.org/abs/1802.03268)[DARTS](https://arxiv.org/abs/1806.09055)[Network Morphism](https://arxiv.org/abs/1806.10282),以及 [Evolution](https://arxiv.org/abs/1703.01041) 等。 此外,新的创新不断涌现。
但是,要实现NAS算法需要花费大量的精力,并且很难在新算法中重用现有算法的代码。 为了促进 NAS 创新(例如,设计、实现新的 NAS 模型,并列比较不同的 NAS 模型),易于使用且灵活的编程接口非常重要。
以此为动力,NNI 的目标是提供统一的体系结构,以加速NAS上的创新,并将最新的算法更快地应用于现实世界中的问题上。
通过统一的接口,有两种方法来使用神经网络架构搜索。 [一种](#supported-one-shot-nas-algorithms)称为 one-shot NAS,基于搜索空间构建了一个超级网络,并使用 one-shot 训练来生成性能良好的子模型。 [第二种](#支持的分布式-nas-算法)是传统的搜索方法,搜索空间中每个子模型作为独立的 Trial 运行。 将性能结果发给 Tuner,由 Tuner 来生成新的子模型。
通过统一的接口,有两种方法来使用神经网络架构搜索。 [一种](#supported-one-shot-nas-algorithms)称为 one-shot NAS,基于搜索空间构建了一个超级网络,并使用 one-shot 训练来生成性能良好的子模型。 <a href="#支持的经典-nas-算法"">第二种</a>是经典的搜索方法,搜索空间中每个子模型作为独立的 Trial 运行。 称之为经典的 NAS。
NNI 还提供了专门的[可视化工具](#nas-可视化),用于查看神经网络架构搜索的过程。
## 支持的经典 NAS 算法
经典 NAS 算法的过程类似于超参调优,通过 `nnictl` 来启动 Experiment,每个子模型会作为 Trial 运行。 不同之处在于,搜索空间文件是通过运行 `nnictl ss_gen`,从用户模型(已包含搜索空间)中自动生成。 下表列出了经典 NAS 模式支持的算法。 将来版本会支持更多算法。
| 名称 | 算法简介 |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| [Random Search(随机搜索)](https://github.com/microsoft/nni/tree/master/examples/tuners/random_nas_tuner) | 从搜索空间中随机选择模型 |
| [PPO Tuner](https://nni.readthedocs.io/zh/latest/Tuner/BuiltinTuner.html#PPOTuner) | PPO Tuner 是基于 PPO 算法的强化学习 Tuner。 [参考论文](https://arxiv.org/abs/1707.06347) |
参考[这里](ClassicNas.md),了解如何使用经典 NAS 算法。
## 支持的 One-shot NAS 算法
NNI 目前支持下面列出的 NAS 算法,并且正在添加更多算法。 用户可以重现算法或在自己的数据集上使用它。 鼓励用户使用 [NNI API](#use-nni-api) 实现其它算法,以使更多人受益。
NNI 目前支持下面列出的 One-Shot NAS 算法,并且正在添加更多算法。 用户可以重现算法或在自己的数据集上使用它。 鼓励用户使用 [NNI API](#use-nni-api) 实现其它算法,以使更多人受益。
| 名称 | 算法简介 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ENAS](ENAS.md) | [Efficient Neural Architecture Search via Parameter Sharing](https://arxiv.org/abs/1802.03268). 在 ENAS 中,Contoller 学习在大的计算图中搜索最有子图的方式来发现神经网络。 它通过在子模型间共享参数来实现加速和出色的性能指标。 |
| [DARTS](DARTS.md) | [DARTS: Differentiable Architecture Search](https://arxiv.org/abs/1806.09055) 引入了一种在两级网络优化中使用的可微分算法。 |
| [P-DARTS](PDARTS.md) | [Progressive Differentiable Architecture Search: Bridging the Depth Gap between Search and Evaluation](https://arxiv.org/abs/1904.12760) 基于DARTS。 它引入了一种有效的算法,可在搜索过程中逐渐增加搜索的深度。 |
| [SPOS](SPOS.md) | 论文 [Single Path One-Shot Neural Architecture Search with Uniform Sampling](https://arxiv.org/abs/1904.00420) 构造了一个采用统一的路径采样方法来训练简化的超网络,并使用进化算法来提高搜索神经网络结构的效率。 |
| [CDARTS](CDARTS.md) | [Cyclic Differentiable Architecture Search](https://arxiv.org/abs/****) 在搜索和评估的网络见构建了循环反馈的机制。 通过引入的循环的可微分架构搜索框架将两个网络集成为一个架构。 |
| [ProxylessNAS](Proxylessnas.md) | [ProxylessNAS: Direct Neural Architecture Search on Target Task and Hardware](https://arxiv.org/abs/1812.00332). 它删除了代理,直接从大规模目标任务和目标硬件平台进行学习。 |
| [TextNAS](TextNAS.md) | [TextNAS: A Neural Architecture Search Space tailored for Text Representation](https://arxiv.org/pdf/1912.10729.pdf)。 这是专门用于文本表示的神经网络架构搜索算法。 |
| 名称 | 算法简介 |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ENAS](https://nni.readthedocs.io/zh/latest/NAS/ENAS.html) | [Efficient Neural Architecture Search via Parameter Sharing](https://arxiv.org/abs/1802.03268). 在 ENAS 中,Contoller 学习在大的计算图中搜索最有子图的方式来发现神经网络。 它通过在子模型间共享参数来实现加速和出色的性能指标。 |
| [DARTS](https://nni.readthedocs.io/zh/latest/NAS/DARTS.html) | [DARTS: Differentiable Architecture Search](https://arxiv.org/abs/1806.09055) 引入了一种在两级网络优化中使用的可微分算法。 |
| [P-DARTS](https://nni.readthedocs.io/zh/latest/NAS/PDARTS.html) | [Progressive Differentiable Architecture Search: Bridging the Depth Gap between Search and Evaluation](https://arxiv.org/abs/1904.12760) 基于DARTS。 它引入了一种有效的算法,可在搜索过程中逐渐增加搜索的深度。 |
| [SPOS](https://nni.readthedocs.io/zh/latest/NAS/SPOS.html) | 论文 [Single Path One-Shot Neural Architecture Search with Uniform Sampling](https://arxiv.org/abs/1904.00420) 构造了一个采用统一的路径采样方法来训练简化的超网络,并使用进化算法来提高搜索神经网络结构的效率。 |
| [CDARTS](https://nni.readthedocs.io/zh/latest/NAS/CDARTS.html) | [Cyclic Differentiable Architecture Search](https://arxiv.org/abs/****) 在搜索和评估的网络见构建了循环反馈的机制。 通过引入的循环的可微分架构搜索框架将两个网络集成为一个架构。 |
| [ProxylessNAS](https://nni.readthedocs.io/zh/latest/NAS/Proxylessnas.html) | [ProxylessNAS: Direct Neural Architecture Search on Target Task and Hardware](https://arxiv.org/abs/1812.00332). 它删除了代理,直接从大规模目标任务和目标硬件平台进行学习。 |
| [TextNAS](https://nni.readthedocs.io/zh/latest/NAS/TextNAS.html) | [TextNAS: A Neural Architecture Search Space tailored for Text Representation](https://arxiv.org/pdf/1912.10729.pdf)。 这是专门用于文本表示的神经网络架构搜索算法。 |
One-shot 算法**不需要 nnictl,可单独运行**只实现了 PyTorch 版本。 将来的版本会支持 Tensorflow 2.x。
One-shot 算法**不需要 nnictl,可单独运行**NNI 支持 PyTorch TensorFlow 2.x。
这是运行示例的一些常见依赖项。 PyTorch 需要高于 1.2 才能使用 `BoolTensor`.
......@@ -30,26 +49,19 @@ One-shot 算法**不需要 nnictl,可单独运行**。 只实现了 PyTorch
* PyTorch 1.2+
* git
一次性 NAS 可以通过可视化工具来查看。 点击[这里](./Visualization.md),了解详情
参考[这里](NasGuide.md),了解如何使用 One-Shot NAS 算法
## 支持的分布式 NAS 算法
One-Shot NAS 可以通过可视化工具来查看。 点击[这里](./Visualization.md),了解详情。
| 名称 | 算法简介 |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [SPOS 的第二阶段](SPOS.md) | 论文 [Single Path One-Shot Neural Architecture Search with Uniform Sampling](https://arxiv.org/abs/1904.00420) 构造了一个采用统一的路径采样方法来训练简化的超网络,并使用进化算法来提高搜索神经网络结构的效率。 |
```eval_rst
.. 注意:SPOS 是一种两阶段算法,第一阶段是 one-shot,第二阶段是分布式的,利用第一阶段的结果作为检查点。
```
## 使用 NNI API
## 使用 NNI API 来编写搜索空间
在两种场景下需要用于设计和搜索模型的编程接口。
1. 在设计神经网络时,可能在层、子模型或连接上有多种选择,并且无法确定是其中一种或某些的组合的结果最好。 因此,需要简单的方法来表达候选的层或子模型。
2. 在神经网络上应用 NAS 时,需要统一的方式来表达架构的搜索空间,这样不必为不同的搜索算法来更改代码。
[这里](./NasGuide.md)是在 NNI 上开始使用 NAS 的用户指南
要使用 NNI NAS,建议先阅读[用 NAS API 构建搜索空间](./WriteSearchSpace.md)的教程
## NAS 可视化
......
......@@ -6,7 +6,7 @@
TextNAS 的搜索空间包含:
* 滤器尺寸为 1, 3, 5, 7 的一维卷积操作
* 滤器尺寸为 1, 3, 5, 7 的一维卷积操作
* 循环操作符(双向 GRU)
* 自注意操作符
* 池化操作符(最大值、平均值)
......
# 编写搜索空间
通常,搜索空间是要在其中找到最好结构的候选项。 无论是经典 NAS 还是 One-Shot NAS,不同的搜索算法都需要搜索空间。 NNI 提供了统一的 API 来表达神经网络架构的搜索空间。
搜索空间可基于基础模型来构造。 这也是在已有模型上使用 NAS 的常用方法。 以 [PyTorch 上的 MNIST](https://github.com/pytorch/examples/blob/master/mnist/main.py) 为例。 注意,NNI 为 PyTorch 和 TensorFlow 提供了同样的搜索空间 API。
```python
from nni.nas.pytorch import mutables
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = mutables.LayerChoice([
nn.Conv2d(1, 32, 3, 1),
nn.Conv2d(1, 32, 5, 3)
]) # 尝试 3x3 和 5x5 的核
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout2d(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
# ... 与原始代码一样 ...
return output
```
以上示例在 conv1 上添加了 conv5x5 的选项。 修改非常简单,只需要声明 `LayerChoice` 并将原始的 conv3x3 和新的 conv5x5 作为参数即可。 就这么简单! 不需要修改 forward 函数。 可将 conv1 想象为没有 NAS 的模型。
如何表示可能的连接? 通过 `InputChoice` 来实现。 要在 MNIST 示例上使用跳过连接,需要增加另一层 conv3。 下面的示例中,从 conv2 的可能连接加入到了 conv3 的输出中。
```python
from nni.nas.pytorch import mutables
class Net(nn.Module):
def __init__(self):
# ... 相同 ...
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.conv3 = nn.Conv2d(64, 64, 1, 1)
# 声明只从搜索策略中选择一个或零个候选项
self.skipcon = mutables.InputChoice(n_candidates=1)
# ... 相同 ...
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x0 = self.skipcon([x]) # 从 [x] 中选择一个或 None
x = self.conv3(x)
if x0 is not None: # 跳接可用
x += x0
x = F.max_pool2d(x, 2)
# ... 相同 ...
return output
```
Input Choice 可被视为可调用的模块,它接收张量数组,输出其中部分的连接、求和、平均(默认为求和),或没有选择时输出 `None`。 与 Layer Choice 一样,Input Choice 要**`__init__` 中初始化,并在 `forward` 中调用。 这会让搜索算法找到这些 Choice,并进行所需的准备。</p>
`LayerChoice``InputChoice` 都是 **Mutable**。 Mutable 表示 "可变化的"。 与传统深度学习层、模型都是固定的不同,使用 Mutable 的模块,是一组可能选择的模型。
用户可为每个 Mutable 指定 **key**。 默认情况下,NNI 会分配全局唯一的,但如果需要共享 Choice(例如,两个 `LayerChoice` 有同样的候选操作,希望共享同样的 Choice。即,如果一个选择了第 i 个操作,第二个也要选择第 i 个操作),那么就应该给它们相同的 key。 key 标记了此 Choice,并会在存储的检查点中使用。 如果要增加导出架构的可读性,可为每个 Mutable 的 key 指派名称。 Mutable 高级用法(如,`LayerChoice``InputChoice`),参考 [Mutables](./NasReference.md)
定义了搜索空间后,下一步是从中找到最好的模型。 参考 [经典 NAS 算法](./ClassicNas.md)[One-Shot NAS 算法](./NasGuide.md)来查看如何从定义的搜索空间中进行搜索。
\ No newline at end of file
One-Shot NAS 算法
=======================
One-Shot NAS 算法利用了搜索空间中模型间的权重共享来训练超网络,并使用超网络来指导选择出更好的模型。 与从头训练每个模型(我们称之为 "经典 NAS")算法相比,此类算法大大减少了使用的计算资源。 NNI 支持下列流行的 One-Shot NAS 算法。
.. toctree::
:maxdepth: 1
快速入门 <NasGuide>
ENAS <ENAS>
DARTS <DARTS>
P-DARTS <PDARTS>
SPOS <SPOS>
CDARTS <CDARTS>
ProxylessNAS <Proxylessnas>
TextNAS <TextNAS>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment