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,**调度运行**由调优 ...@@ -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 功能一览** ## **NNI 功能一览**
...@@ -84,6 +84,7 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。 ...@@ -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/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/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/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/> <a href="docs/zh_CN/SupportedFramework_Library.md">更多...</a><br/>
</ul> </ul>
</ul> </ul>
...@@ -137,6 +138,10 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。 ...@@ -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#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#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#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> </ul>
<b>量化</b> <b>量化</b>
<ul> <ul>
...@@ -164,7 +169,7 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。 ...@@ -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/KubeflowMode.md">Kubeflow</a></li>
<li><a href="docs/zh_CN/TrainingService/FrameworkControllerMode.md">基于 Kubernetes(AKS 等)的 FrameworkController</a></li> <li><a href="docs/zh_CN/TrainingService/FrameworkControllerMode.md">基于 Kubernetes(AKS 等)的 FrameworkController</a></li>
</ul> </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> </ul>
</td> </td>
</tr> </tr>
...@@ -186,11 +191,12 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。 ...@@ -186,11 +191,12 @@ NNI 提供命令行工具以及友好的 WebUI 来管理训练的 Experiment。
<ul> <ul>
<li><a href="docs/zh_CN/Tuner/CustomizeTuner.md">自定义 Tuner</a></li> <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/Assessor/CustomizeAssessor.md">自定义 Assessor</a></li>
<li><a href="docs/zh_CN/Tutorial/InstallCustomizedAlgos.md">安装自定义的 Tuner,Assessor,Advisor</a></li>
</ul> </ul>
</td> </td>
<td style="border-top:#FF0000 solid 0px;"> <td style="border-top:#FF0000 solid 0px;">
<ul> <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> <li><a href="docs/zh_CN/TrainingService/HowToImplementTrainingService.md">实现训练平台</a></li>
</ul> </ul>
</td> </td>
...@@ -223,7 +229,7 @@ Linux 和 macOS 下 NNI 系统需求[参考这里](https://nni.readthedocs.io/zh ...@@ -223,7 +229,7 @@ Linux 和 macOS 下 NNI 系统需求[参考这里](https://nni.readthedocs.io/zh
注意: 注意:
* 如果遇到任何权限问题,可添加 `--user` 在用户目录中安装 NNI。 * 如果遇到任何权限问题,可添加 `--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) * 如果遇到如 `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 ...@@ -233,7 +239,7 @@ Linux 和 macOS 下 NNI 系统需求[参考这里](https://nni.readthedocs.io/zh
* 通过克隆源代码下载示例。 * 通过克隆源代码下载示例。
```bash ```bash
git clone -b v1.5 https://github.com/Microsoft/nni.git git clone -b v1.6 https://github.com/Microsoft/nni.git
``` ```
* 运行 MNIST 示例。 * 运行 MNIST 示例。
...@@ -316,8 +322,7 @@ You can use these commands to get more information about the experiment ...@@ -316,8 +322,7 @@ You can use these commands to get more information about the experiment
经作者许可的一些 NNI 用法示例和相关文档。 经作者许可的一些 NNI 用法示例和相关文档。
* ### **外部代码库** ### * ### **外部代码库** ###
* 在 NNI 中运行 [ENAS](examples/tuners/enas_nni/README_zh_CN.md) * 在 NNI 中运行 [ENAS](examples/nas/enas/README_zh_CN.md)
* 在 NNI 中运行 [神经网络架构结构搜索](examples/trials/nas_cifar10/README_zh_CN.md)
* [NNI 中的自动特征工程](examples/feature_engineering/auto-feature-engineering/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) * 使用 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 开发的超参搜索。 * [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 ...@@ -339,9 +344,9 @@ You can use these commands to get more information about the experiment
加入聊天组: 加入聊天组:
| Gitter | | 微信 | | 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) | | ![图片](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 ...@@ -19,7 +19,7 @@ NNI 提供了先进的评估算法,使用上也很简单。 下面是内置 As
<a name="MedianStop"></a> <a name="MedianStop"></a>
![](https://placehold.it/15/1589F0/000000?text=+) `Median Stop Assessor` ### Median Stop Assessor
> 名称:**Medianstop** > 名称:**Medianstop**
...@@ -47,20 +47,21 @@ assessor: ...@@ -47,20 +47,21 @@ assessor:
<a name="Curvefitting"></a> <a name="Curvefitting"></a>
![](https://placehold.it/15/1589F0/000000?text=+) `Curve Fitting Assessor` ### Curve Fitting Assessor
> 名称:**Curvefitting** > 名称:**Curvefitting**
**建议场景** **建议场景**
适用于各种性能曲线,可用到各种场景中来加速优化过程。 更好的地方是,它能处理并评估性能类似的曲线。 [详细说明](./CurvefittingAssessor.md) 适用于各种性能曲线,可用到各种场景中来加速优化过程。 更好的是,它能够处理并评估性能类似的曲线。 [详细说明](./CurvefittingAssessor.md)
**注意**,根据原始论文,仅支持递增函数。 因此,此 Assessor 仅可用于最大化优化指标的场景。 例如,它可用于准确度,但不能用于损失值。
**classArgs 要求:** **classArgs 要求:**
* **epoch_num** (*int, **必需***) - epoch 的总数。 需要此数据来决定需要预测点的总数。 * **epoch_num** (*int, **必需***) - epoch 的总数。 需要此数据来决定需要预测点的总数。
* **optimize_mode** (*maximize 或 minimize, 可选, 默认值为 maximize*) - 如果为 'maximize', Assessor 会在结果小于期望值时**终止** Trial。 如果为 'minimize',Assessor 会在结果大于期望值时**终止** Trial。
* **start_step** (*int, 可选, 默认值为 6*) - 只有收到 start_step 个中间结果后,才开始判断是否一个 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... 个中间结果。 * **gap** (*int, 可选, 默认值为 1*) - Assessor 两次评估之间的间隔次数。 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。
**使用示例:** **使用示例:**
...@@ -71,7 +72,6 @@ assessor: ...@@ -71,7 +72,6 @@ assessor:
builtinAssessorName: Curvefitting builtinAssessorName: Curvefitting
classArgs: classArgs:
epoch_num: 20 epoch_num: 20
optimize_mode: maximize
start_step: 6 start_step: 6
threshold: 0.95 threshold: 0.95
gap: 1 gap: 1
......
# NNI 中的 Curve Fitting Assessor # NNI 中的 Curve Fitting Assessor
## 1. 介绍 ## 介绍
Curve Fitting Assessor 是一个 LPA (learning, predicting, assessing,即学习、预测、评估) 的算法。 如果预测的 Trial X 在 step S 比性能最好的 Trial 要差,就会提前终止它。 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)。 学习曲线的形状与先验知识是一致的:都是典型的递增的、饱和的函数。 此算法中采用了 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,即学 ...@@ -30,36 +30,37 @@ Curve Fitting Assessor 是一个 LPA (learning, predicting, assessing,即学
下图显示了此算法在 MNIST Trial 历史数据上结果。其中绿点表示 Assessor 获得的数据,蓝点表示将来,但未知的数据,红色线条是 Curve fitting Assessor 的预测曲线。 下图显示了此算法在 MNIST Trial 历史数据上结果。其中绿点表示 Assessor 获得的数据,蓝点表示将来,但未知的数据,红色线条是 Curve fitting Assessor 的预测曲线。
![](../../img/curvefitting_example.PNG) ![示例](../../img/curvefitting_example.PNG)
## 2. 用法 ## 用法
要使用 Curve Fitting Assessor,需要在 Experiment 的 YAML 配置文件进行如下改动。 要使用 Curve Fitting Assessor,需要在 Experiment 的 YAML 配置文件进行如下改动。
assessor: ```yaml
builtinAssessorName: Curvefitting assessor:
classArgs: builtinAssessorName: Curvefitting
# (必须) epoch 的总数。 classArgs:
# 需要此数据来决定需要预测的点。 # (必须) epoch 的总数。
epoch_num: 20 # 需要此数据来决定需要预测的点。
# (可选) 选项: maximize, minimize epoch_num: 20
* optimize_mode 的默认值是 maximize # (可选) 为了节约计算资源,仅在收到 start_step 个中间结果后,才开始进行预测。
optimize_mode: maximize # start_step 的默认值是 6。
# (可选) 为了节约计算资源,在收到了 start_step 个中间结果后,才开始预测。 start_step: 6
# start_step 的默认值是 6。 # (可选) 决定是否提前终止的阈值。
start_step: 6 # 例如,如果 threshold = 0.95,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。
# (可选) 决定是否提前终止的阈值。 # 阈值的默认值是 0.95。
# 例如,如果 threshold = 0.95, optimize_mode = maximize,最好的历史结果是 0.9,那么会在 Trial 的预测值低于 0.95 * 0.9 = 0.855 时停止。 threshold: 0.95
* 阈值的默认值是 0.95。 # (可选) gap 是两次评估之间的间隔次数。
# 注意:如果选择了 minimize 模式,要让 threshold >= 1.0 (如 threshold=1.1) # 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。
threshold: 0.95 # gap 的默认值是 1。
# (可选) gap 是两次评估之间的间隔次数。 gap: 1
# 例如:如果 gap = 2, start_step = 6,就会评估第 6, 8, 10, 12... 个中间结果。 ```
* gap 的默认值是 1。
gap: 1 ## 局限性
根据原始论文,仅支持递增函数。 因此,此 Assessor 仅可用于最大化优化指标的场景。 例如,它可用于准确度,但不能用于损失值。
## 3. 文件结构
## 文件结构
Assessor 有大量的文件、函数和类。 在这里,会简要描述其中一部分。 Assessor 有大量的文件、函数和类。 在这里,会简要描述其中一部分。
...@@ -67,6 +68,6 @@ Assessor 有大量的文件、函数和类。 在这里,会简要描述其中 ...@@ -67,6 +68,6 @@ Assessor 有大量的文件、函数和类。 在这里,会简要描述其中
* `modelfactory.py` 包括学习和预测部分,并实现了相应的计算部分。 * `modelfactory.py` 包括学习和预测部分,并实现了相应的计算部分。
* `curvefitting_assessor.py` 是接收 Trial 历史数据并评估是否需要提前终止的 Assessor。 * `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。 _即将推出_...
# 设计文档 # 模型压缩框架概述
## 概述 ```eval_rst
模型压缩框架有两个主要组件: `Pruner``module 的包装` .. contents::
```
### Pruner 下图展示了模型压缩框架的组件概览。
`Pruner` 用于:
1. 提供 `cal_mask` 方法来计算权重和偏差的掩码(mask)。
2. 根据配置,用 `module 的包装`来替换原始的 module。
3. 修改优化器,来在 `step` 方法被调用时,调用 `cal_mask`
### module 的包装 ![](../../img/compressor_framework.jpg)
`module 的包装` 包含:
1. 原始的 module
2. `cal_mask` 使用的一些缓存
3. 新的 forward 方法,用于在运行原始的 forward 方法前应用掩码。
使用 `module 包装`的原因: NNI 模型压缩框架中主要有三个组件/类:`Compressor`, `Pruner``Quantizer`。 下面会逐个详细介绍:
1. 计算掩码所需要的 `cal_mask` 方法需要一些缓存,这些缓存需要注册在 `module 包装`里,这样就不需要修改原始的 module。
2. 新的 `forward` 方法用来在原始 `forward` 调用前,将掩码应用到权重上。 ## Compressor
Compressor 是 Pruner 和 Quantizer 的基类,提供了统一的接口,可用同样的方式使用它们。 例如,使用 Pruner:
## 工作原理
基本的 Pruner 用法:
```python ```python
from nni.compression.torch import LevelPruner
# 读取预训练的模型,或在使用 Pruner 前进行训练。
configure_list = [{ configure_list = [{
'sparsity': 0.7, '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) 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() model = pruner.compress()
# 剪枝已准备好,开始调优模型,
# 模型会在训练过程中自动剪枝
``` ```
Pruner 接收模型,配置和优化器作为参数。 在 `__init__` 方法中,优化器的 `step` 方法会被一个会调用 `cal_mask` 的新的 `step` 方法替换。 同样,所有 module 都会检查它们是否被配置为需要剪枝。如果 module 需要被剪枝,就会用 `module 包装`来替换它。 之后,会返回新的模型和优化器,并进行训练。 `compress` 方法会计算默认的掩码。 使用 Quantizer:
```python
from nni.compression.torch import DoReFaQuantizer
## 实现新的剪枝算法 configure_list = [{
要实现新的剪枝算法,需要继承 `Pruner` 来实现新的类,并重载 `cal_mask` 方法。 `cal_mask` 会被 `optimizer.step` 方法调用。 `Pruner` 基类提供了上述的基本功能,如替换 module 和优化器。 '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 有时,`calc_mask` 需要保存一些状态数据,可以像 PyTorch 的 module 一样,使用 `set_wrappers_attribute` API 来注册属性。 这些缓存会注册到 `module 包装`中。 用户可以通过 `module 包装`来直接访问这些缓存。 在上述示例中,使用了 `set_wrappers_attribute` 类设置缓冲 `if_calculated`,它用来标识某层的掩码是否已经计算过了。
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
# 更新掩码
```
### 在 forward 时收集数据 ### 在 forward 时收集数据
有时,需要在 forward 方法中收集数据,例如,需要激活的平均值。 这时,可以为 module 增加定制的收集方法。
有时,需要在 forward 方法中收集数据,例如,需要激活的平均值。 可通过向 module 中添加定制的 Collector 来做到。
```python ```python
class ActivationRankFilterPruner(Pruner): class MyMasker(WeightMasker):
def __init__(self, model, config_list, optimizer, activation='relu', statistics_batch_num=1): def __init__(self, model, pruner):
super().__init__(model, config_list, optimizer) super().__init__(model, pruner)
self.set_wrappers_attribute("if_calculated", False) # 为所有包装类设置 `collected_activation` 属性
self.set_wrappers_attribute("collected_activation", []) # 保存所有层的激活值
self.statistics_batch_num = statistics_batch_num self.pruner.set_wrappers_attribute("collected_activation", [])
self.activation = torch.nn.functional.relu
def collector(module_, input_, output):
if len(module_.collected_activation) < self.statistics_batch_num: def collector(wrapper, input_, output):
module_.collected_activation.append(self.activation(output.detach().cpu())) # 通过每个包装的 collected_activation 属性,来评估收到的激活值
self.add_activation_collector(collector) wrapper.collected_activation.append(self.activation(output.detach().cpu()))
assert activation in ['relu', 'relu6']
if activation == 'relu': self.pruner.hook_id = self.pruner.add_activation_collector(collector)
self.activation = torch.nn.functional.relu
elif activation == 'relu6':
self.activation = torch.nn.functional.relu6
else:
self.activation = None
``` ```
收集函数会在每次 forward 方法运行时调用。 收集函数会在每次 forward 方法运行时调用。
还可这样来移除收集方法: 还可这样来移除收集方法:
```python ```python
collector_id = self.add_activation_collector(collector) # 保存 Collector 的标识
# ... collector_id = self.pruner.add_activation_collector(collector)
self.remove_activation_collector(collector_id)
# 当 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 @@ ...@@ -17,9 +17,9 @@
## 用法 ## 用法
```python ```python
from nni.compression.speedup.torch import ModelSpeedup from nni.compression.torch import ModelSpeedup
# model: 要加速的模型 # model: 要加速的模型
# dummy_input: 模型的示输入,传给 `jit.trace` # dummy_input: 模型的示输入,传给 `jit.trace`
# masks_file: 剪枝算法创建的掩码文件 # masks_file: 剪枝算法创建的掩码文件
m_speedup = ModelSpeedup(model, dummy_input.to(device), masks_file) m_speedup = ModelSpeedup(model, dummy_input.to(device), masks_file)
m_speedup.speedup_model() m_speedup.speedup_model()
...@@ -30,7 +30,7 @@ print('elapsed time: ', time.time() - start) ...@@ -30,7 +30,7 @@ print('elapsed time: ', time.time() - start)
``` ```
完整示例参考[这里](https://github.com/microsoft/nni/tree/master/examples/model_compress/model_speedup.py) 完整示例参考[这里](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 或更高版本。
## 局限性 ## 局限性
......
This diff is collapsed.
This diff is collapsed.
NNI Compressor 中的 Quantizer # 支持的量化算法
===
支持的量化算法列表
* [Naive Quantizer](#naive-quantizer)
* [QAT Quantizer](#qat-quantizer)
* [DoReFa Quantizer](#dorefa-quantizer)
* [BNN Quantizer](#bnn-quantizer)
## Naive Quantizer ## Naive Quantizer
Naive Quantizer 将 Quantizer 权重默认设置为 8 位,可用它来测试量化算法。 Naive Quantizer 将 Quantizer 权重默认设置为 8 位,可用它来测试量化算法。
### 用法 ### 用法
pytorch PyTorch
```python ```python
model = nni.compression.torch.NaiveQuantizer(model).compress() model = nni.compression.torch.NaiveQuantizer(model).compress()
``` ```
...@@ -44,7 +50,8 @@ quantizer.compress() ...@@ -44,7 +50,8 @@ quantizer.compress()
查看示例进一步了解 查看示例进一步了解
#### QAT Quantizer 的用户配置 #### QAT Quantizer 的用户配置
压缩算法所需的常见配置可在[通用配置](./Overview.md#压缩算法中的用户配置)中找到。
压缩算法的公共配置可在 [`config_list` 说明](./QuickStart.md)中找到。
此算法所需的配置: 此算法所需的配置:
...@@ -53,13 +60,17 @@ quantizer.compress() ...@@ -53,13 +60,17 @@ quantizer.compress()
在运行到某步骤前,对模型禁用量化。这让网络在进入更稳定的 状态后再激活量化,这样不会配除掉一些分数显著的值,默认为 0 在运行到某步骤前,对模型禁用量化。这让网络在进入更稳定的 状态后再激活量化,这样不会配除掉一些分数显著的值,默认为 0
### 注意 ### 注意
当前不支持批处理规范化折叠。 当前不支持批处理规范化折叠。
*** ***
## DoReFa Quantizer ## 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-Net: Training Low Bitwidth Convolutional Neural Networks with Low Bitwidth Gradients](https://arxiv.org/abs/1606.06160) 中,作者 Shuchang Zhou 和 Yuxin Wu 提出了 DoReFa 算法在训练时量化权重,激活函数和梯度。
### 用法 ### 用法
要实现 DoReFa Quantizer,在训练代码前加入以下代码。 要实现 DoReFa Quantizer,在训练代码前加入以下代码。
PyTorch 代码 PyTorch 代码
...@@ -77,12 +88,15 @@ quantizer.compress() ...@@ -77,12 +88,15 @@ quantizer.compress()
查看示例进一步了解 查看示例进一步了解
#### DoReFa Quantizer 的用户配置 #### DoReFa Quantizer 的用户配置
压缩算法所需的常见配置可在[通用配置](./Overview.md#压缩算法中的用户配置)中找到。
压缩算法的公共配置可在 [`config_list` 说明](./QuickStart.md)中找到。
此算法所需的配置: 此算法所需的配置:
***
## BNN Quantizer ## BNN Quantizer
[Binarized Neural Networks: Training Deep Neural Networks with Weights and Activations Constrained to +1 or -1](https://arxiv.org/abs/1602.02830) 中, [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 会大大减少内存大小和访问,并将大多数算术运算替换为按位计算,可显著提高能源效率。 > 引入了一种训练二进制神经网络(BNN)的方法 - 神经网络在运行时使用二进制权重。 在训练时,二进制权重和激活用于计算参数梯度。 在 forward 过程中,BNN 会大大减少内存大小和访问,并将大多数算术运算替换为按位计算,可显著提高能源效率。
...@@ -113,12 +127,14 @@ model = quantizer.compress() ...@@ -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) 了解更多信息。 可以查看示例 [examples/model_compress/BNN_quantizer_cifar10.py](https://github.com/microsoft/nni/tree/master/examples/model_compress/BNN_quantizer_cifar10.py) 了解更多信息。
#### BNN Quantizer 的用户配置 #### 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,其它层保持不变。 编写配置来指定要剪枝的层。 以下配置表示剪枝所有的 `BatchNorm2d`,稀疏度设为 0.7,其它层保持不变。
...@@ -13,9 +21,9 @@ configure_list = [{ ...@@ -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()` 来压缩模型。 选择 Pruner 来修剪模型。 首先,使用模型来初始化 Pruner,并将配置作为参数传入,然后调用 `compress()` 来压缩模型。
...@@ -26,7 +34,7 @@ model = pruner.compress() ...@@ -26,7 +34,7 @@ model = pruner.compress()
然后,使用正常的训练方法来训练模型 (如,SGD),剪枝在训练过程中是透明的。 一些 Pruner 只在最开始剪枝一次,接下来的训练可被看作是微调优化。 有些 Pruner 会迭代的对模型剪枝,在训练过程中逐步修改掩码。 然后,使用正常的训练方法来训练模型 (如,SGD),剪枝在训练过程中是透明的。 一些 Pruner 只在最开始剪枝一次,接下来的训练可被看作是微调优化。 有些 Pruner 会迭代的对模型剪枝,在训练过程中逐步修改掩码。
## 导出压缩结果 ### 导出压缩结果
训练完成后,可获得剪枝后模型的精度。 可将模型权重到处到文件,同时将生成的掩码也导出到文件。 也支持导出 ONNX 模型。 训练完成后,可获得剪枝后模型的精度。 可将模型权重到处到文件,同时将生成的掩码也导出到文件。 也支持导出 ONNX 模型。
...@@ -36,7 +44,7 @@ pruner.export_model(model_path='pruned_vgg19_cifar10.pth', mask_path='mask_vgg19 ...@@ -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) 模型的完整示例代码在[这里](https://github.com/microsoft/nni/blob/master/examples/model_compress/model_prune_torch.py)
## 加速模型 ### 加速模型
掩码实际上并不能加速模型。 要基于导出的掩码,来对模型加速,因此,NNI 提供了 API 来加速模型。 在模型上调用 `apply_compression_results` 后,模型会变得更小,推理延迟也会减小。 掩码实际上并不能加速模型。 要基于导出的掩码,来对模型加速,因此,NNI 提供了 API 来加速模型。 在模型上调用 `apply_compression_results` 后,模型会变得更小,推理延迟也会减小。
...@@ -45,4 +53,118 @@ from nni.compression.torch import apply_compression_results ...@@ -45,4 +53,118 @@ from nni.compression.torch import apply_compression_results
apply_compression_results(model, 'mask_vgg19_cifar10.pth') apply_compression_results(model, 'mask_vgg19_cifar10.pth')
``` ```
参考[这里](ModelSpeedup.md),了解详情。 参考[这里](ModelSpeedup.md),了解详情。
\ No newline at end of file
## 使用指南
将压缩应用到模型的示例代码如下:
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 除了 [经典 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 算法通常会构建一个超网络,其中包含的子网作为此搜索空间的候选项。每一步,会训练一个或多个子网的组合。
.. 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,在搜索空间中,构建包含有所有候选网络的超网络,每一步中选择一个或几个子网络来训练。
当前,NNI 支持数种 One-Shot 方法。 例如,`DartsTrainer` 使用 SGD 来交替训练架构和模型权重,`ENASTrainer` [使用 Controller 来训练模型](https://arxiv.org/abs/1802.03268)。 新的、更高效的 NAS Trainer 在研究界不断的涌现出来,NNI 会在将来的版本中实现其中的一部分。 当前,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` 的简单示例。 每个 One-Shot NAS 算法都实现了 Trainer,可在每种算法说明中找到详细信息。 这是如何使用 `EnasTrainer` 的简单示例。
...@@ -99,7 +25,7 @@ def top1_accuracy(output, target): ...@@ -99,7 +25,7 @@ def top1_accuracy(output, target):
def metrics_fn(output, target): def metrics_fn(output, target):
# 指标函数接收输出和目标,并计算出指标 dict # 指标函数接收输出和目标,并计算出指标 dict
return {"acc1": reward_accuracy(output, target)} return {"acc1": top1_accuracy(output, target)}
from nni.nas.pytorch import enas from nni.nas.pytorch import enas
trainer = enas.EnasTrainer(model, trainer = enas.EnasTrainer(model,
...@@ -116,39 +42,17 @@ trainer.train() # 训练 ...@@ -116,39 +42,17 @@ trainer.train() # 训练
trainer.export(file="model_dir/final_architecture.json") # 将最终架构导出到文件 trainer.export(file="model_dir/final_architecture.json") # 将最终架构导出到文件
``` ```
用户可直接通过 `python3 train.py` 开始训练,不需要使用 `nnictl`。 训练完成后,可通过 `trainer.export()` 导出找到的最好的模型。 `model` 是一个[用户定义的搜索空间](./WriteSearchSpace.md)。 然后需要准备搜索数据和模型评估指标。 要从定义的搜索空间中进行搜索,需要实例化 One-Shot 算法,即 Trainer(如,EnasTrainer)。 Trainer 会提供一些可以自定义的参数。 如,损失函数,指标函数,优化器以及数据集。 这些功能可满足大部分需求,NNI 会尽力让内置 Trainer 能够处理更多的模型、任务和数据集。
通常,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)
可使用 [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 ```python
model = Net() model = Net()
...@@ -175,6 +79,6 @@ apply_fixed_architecture(model, "model_dir/final_architecture.json") ...@@ -175,6 +79,6 @@ apply_fixed_architecture(model, "model_dir/final_architecture.json")
} }
``` ```
应用后,模型会被固定,并准备好进行最终训练。 虽然它可能包含了更多的参数,但可作为单模型来使用。 这各有利弊。 好的方面是,可以在搜索阶段直接读取来自超网络的检查点,并开始重新训练。 但是,这也造成模型有冗余的参数,在计算模型所包含的参数数量时,可能会不准确。 更多深层次原因和解决方法可参考 [Trainer](./NasReference.md) 应用后,模型会被固定,并准备好进行最终训练。 该模型作为单独的模型来工作,未使用的参数和模块已被剪除
也可参考 [DARTS](./DARTS.md) 的重新训练代码。 也可参考 [DARTS](./DARTS.md) 的重新训练代码。
# 神经网络结构搜索在 NNI 上的应用 # 神经网络结构搜索在 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 的可行性,并发现了一些超越手动设计和调整的模型。 代表算法有 [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 模型),易于使用且灵活的编程接口非常重要。 但是,要实现NAS算法需要花费大量的精力,并且很难在新算法中重用现有算法的代码。 为了促进 NAS 创新(例如,设计、实现新的 NAS 模型,并列比较不同的 NAS 模型),易于使用且灵活的编程接口非常重要。
以此为动力,NNI 的目标是提供统一的体系结构,以加速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 算法 ## 支持的 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 学习在大的计算图中搜索最有子图的方式来发现神经网络。 它通过在子模型间共享参数来实现加速和出色的性能指标。 | | [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](DARTS.md) | [DARTS: Differentiable Architecture Search](https://arxiv.org/abs/1806.09055) 引入了一种在两级网络优化中使用的可微分算法。 | | [DARTS](https://nni.readthedocs.io/zh/latest/NAS/DARTS.html) | [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。 它引入了一种有效的算法,可在搜索过程中逐渐增加搜索的深度。 | | [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](SPOS.md) | 论文 [Single Path One-Shot Neural Architecture Search with Uniform Sampling](https://arxiv.org/abs/1904.00420) 构造了一个采用统一的路径采样方法来训练简化的超网络,并使用进化算法来提高搜索神经网络结构的效率。 | | [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](CDARTS.md) | [Cyclic Differentiable Architecture Search](https://arxiv.org/abs/****) 在搜索和评估的网络见构建了循环反馈的机制。 通过引入的循环的可微分架构搜索框架将两个网络集成为一个架构。 | | [CDARTS](https://nni.readthedocs.io/zh/latest/NAS/CDARTS.html) | [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). 它删除了代理,直接从大规模目标任务和目标硬件平台进行学习。 | | [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](TextNAS.md) | [TextNAS: A Neural Architecture Search Space tailored for Text Representation](https://arxiv.org/pdf/1912.10729.pdf)。 这是专门用于文本表示的神经网络架构搜索算法。 | | [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`. 这是运行示例的一些常见依赖项。 PyTorch 需要高于 1.2 才能使用 `BoolTensor`.
...@@ -30,26 +49,19 @@ One-shot 算法**不需要 nnictl,可单独运行**。 只实现了 PyTorch ...@@ -30,26 +49,19 @@ One-shot 算法**不需要 nnictl,可单独运行**。 只实现了 PyTorch
* PyTorch 1.2+ * PyTorch 1.2+
* git * 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. 在设计神经网络时,可能在层、子模型或连接上有多种选择,并且无法确定是其中一种或某些的组合的结果最好。 因此,需要简单的方法来表达候选的层或子模型。 1. 在设计神经网络时,可能在层、子模型或连接上有多种选择,并且无法确定是其中一种或某些的组合的结果最好。 因此,需要简单的方法来表达候选的层或子模型。
2. 在神经网络上应用 NAS 时,需要统一的方式来表达架构的搜索空间,这样不必为不同的搜索算法来更改代码。 2. 在神经网络上应用 NAS 时,需要统一的方式来表达架构的搜索空间,这样不必为不同的搜索算法来更改代码。
[这里](./NasGuide.md)是在 NNI 上开始使用 NAS 的用户指南 要使用 NNI NAS,建议先阅读[用 NAS API 构建搜索空间](./WriteSearchSpace.md)的教程
## NAS 可视化 ## NAS 可视化
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
TextNAS 的搜索空间包含: TextNAS 的搜索空间包含:
* 滤器尺寸为 1, 3, 5, 7 的一维卷积操作 * 滤器尺寸为 1, 3, 5, 7 的一维卷积操作
* 循环操作符(双向 GRU) * 循环操作符(双向 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