未验证 提交 6fa05ff3 编写于 作者: W whs 提交者: GitHub

Support pruning dygraph (#506)

上级 da4c6757
[submodule "demo/ocr/PaddleOCR"]
path = demo/ocr/PaddleOCR
url = https://github.com/PaddlePaddle/PaddleOCR
# Paddle动态图卷积模型Filter剪裁教程
该教程以MobileNetV1模型和Cifar10分类任务为例,介绍如何使用PaddleSlim对动态图卷积模型进行filter剪裁。
## 1. 模型定义
PaddlePaddle提供的`vision`模块提供了一些构建好的分类模型结构,并提供在`ImageNet`数据集上的预训练模型。为了简化教程,我们不再重新定义网络结构,而是直接从`vision`模块导入模型结构。代码如下所示,我们导入`MobileNetV1`模型,并查看模型的结构信息。
```python
import paddle
from paddle.vision.models import mobilenet_v1
net = mobilenet_v1(pretrained=False)
paddle.summary(net, (1, 3, 32, 32))
```
## 2. 准备数据
我们直接使用`vision`模块提供的`Cifar10`数据集,并通过飞桨高层API `paddle.vision.transforms`对数据进行预处理。在声明`paddle.vision.datasets.Cifar10`对象时,会自动下载数据并缓存到本地文件系统。代码如下所示:
```python
import paddle.vision.transforms as T
transform = T.Compose([
T.Transpose(),
T.Normalize([127.5], [127.5])
])
train_dataset = paddle.vision.datasets.Cifar10(mode="train", backend="cv2",transform=transform)
val_dataset = paddle.vision.datasets.Cifar10(mode="test", backend="cv2",transform=transform)
```
我们可以通过以下代码查看训练集和测试集的样本数量,并尝试取出训练集中的第一个样本,观察其图片的`shape`和对应的`label`
```python
from __future__ import print_function
print(f'train samples count: {len(train_dataset)}')
print(f'val samples count: {len(val_dataset)}')
for data in train_dataset:
print(f'image shape: {data[0].shape}; label: {data[1]}')
break
```
## 3. 模型训练准备工作
在对卷积网络进行剪裁之前,我们需要在测试集上评估网络中各层的重要性。在剪裁之后,我们需要对得到的小模型进行重训练。在本示例中,我们将会使用Paddle高层API `paddle.Model`进行训练和评估工作。以下代码声明了`paddle.Model`实例,并指定了训练相关的一些设置,包括:
- 输入的shape
- 优化器
- 损失函数
- 模型评估指标
```python
from paddle.static import InputSpec as Input
optimizer = paddle.optimizer.Momentum(
learning_rate=0.1,
parameters=net.parameters())
inputs = [Input([None, 3, 224, 224], 'float32', name='image')]
labels = [Input([None, 1], 'int64', name='label')]
model = paddle.Model(net, inputs, labels)
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
```
以上代码声明了用于训练的`model`对象,接下来可以调用`model``fit`接口和`evaluate`接口分别进行训练和评估:
```python
model.fit(train_dataset, epochs=2, batch_size=128, verbose=1)
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(result)
```
## 4. 剪裁
本节内容分为两部分:卷积层重要性分析和`Filters`剪裁,其中『卷积层重要性分析』也可以被称作『卷积层敏感度分析』,我们定义越重要的卷积层越敏感。
PaddleSlim提供了工具类`Pruner`来进行重要性分析和剪裁操作,不同的`Pruner`的子类对应不同的分析和剪裁策略,本示例以`L1NormFilterPruner`为例说明。首先我们声明一个`L1NormFilterPruner`对象,如下所示:
```python
from paddleslim.dygraph import L1NormFilterPruner
pruner = L1NormFilterPruner(net, [1, 3, 224, 224])
```
如果本地文件系统已有一个存储敏感度信息(见4.1节)的文件,声明`L1NormFilterPruner`对象时,可以通过指定`sen_file`选项加载计算好的敏感度信息,如下:
```python
#pruner = L1NormFilterPruner(net, [1, 3, 224, 224]), sen_file="./sen.pickle")
```
### 4.1 卷积重要性分析
在对卷积网络中的filters进行剪裁时,我们需要判断哪些`filters`不重要,然后优先剪掉不重要的`filters`
在一个卷积内,我们使用`filter``L1 Norm`来代表重要性,`L1 Norm`越大的`filters`越重要。在多个卷积间,我们通过敏感度代表卷积的重要性,越敏感的卷积越重要,重要的卷积会被剪掉相对较少的`filters`
单个卷积内的filters重要性计算会在剪裁时进行,无需用户关注。本小节,我们只介绍多个卷积间如何分析重要性,即『敏感度分析』。
#### 敏感度定义
如图4-1所示,某个卷积网络包含K个卷积层,每个卷积层有4个`filters`,原始网络精度为90。
第一步:从『卷积1』中剪裁掉25%的filters,也就是『卷积1』中第2个Filters,然后直接在测试集上评估精度结果为85,得到左边坐标图的第二个红点。恢复模型到初始状态。
第二步:从『卷积1』中裁掉2个卷积,然后在测试集上评估精度为70,得到坐标图的第3个红点。恢复模型到初始状态。
第三步:同理得到第4个红点。把『卷积1』对应的4个红点链接成曲线,即为『卷积1』的敏感度曲线。
第四步:同理得到『卷积K』的敏感度曲线。
<div align="center">
<img src="filter_pruning/4-1.png" width="600" height="300">
</div>
<div align="center">
图4-1 敏感度计算过程示意图
</div>
如图4-2所示,为VGG-16在CIFAR10分类任务上的敏感度曲线示意图:
<div align="center">
<img src="filter_pruning/4-2.png" width="600" height="500">
</div>
<div align="center">
图4-2 VGG-16敏感度示例
</div>
考虑到不同的模型在不同的任务上的精度数值差别较大,甚至不在同一个量级,所以,PaddleSlim在计算和存储卷积层敏感度时,使用的是精度的损失比例。如图4-3所示,为PaddleSlim计算出的MobileNetV1-YOLOv3在VOC检测任务上的敏感度示意图,其中,纵轴为精度损失:
<div align="center">
<img src="filter_pruning/4-3.png" width="600" height="600">
</div>
<div align="center">
图4-3 用精度损失表示的敏感度
</div>
#### 敏感度计算
调用`pruner`对象的`sensitive`方法进行敏感度分析,在调用`sensitive`之前,我们简单对`model.evaluate`进行包装,使其符合`sensitive`接口的规范。执行如下代码,会进行敏感度计算,并将计算结果存入本地文件系统:
```python
def eval_fn():
result = model.evaluate(
val_dataset,
batch_size=128)
return result['acc_top1']
pruner.sensitive(eval_func=eval_fn, sen_file="./sen.pickle")
```
上述代码执行完毕后,敏感度信息会存放在pruner对象中,可以通过以下方式查看敏感度信息内容:
```python
print(pruner.sensitive())
```
### 4.2 剪裁
`pruner`对象提供了`sensitive_prune`方法根据敏感度信息对模型进行剪裁,用户只需要传入期望的FLOPs减少比例。首先,我们记录下剪裁之前的模型的FLOPs数值,如下:
```python
from paddleslim.analysis import dygraph_flops
flops = dygraph_flops(net, [1, 3, 32, 32])
print(f"FLOPs before pruning: {flops}")
```
执行剪裁操作,期望跳过最后一层卷积层并剪掉40%的FLOPs:
```python
plan = pruner.sensitive_prune(0.4, skip_vars=["conv2d_26.w_0"])
flops = dygraph_flops(net, [1, 3, 32, 32])
print(f"FLOPs after pruning: {flops}")
print(f"Pruned FLOPs: {round(plan.pruned_flops*100, 2)}%")
```
剪裁之后,在测试集上重新评估精度,会发现精度大幅下降,如下所示:
```python
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(f"before fine-tuning: {result}")
```
对剪裁后的模型重新训练, 并再测试集上测试精度,如下:
```python
optimizer = paddle.optimizer.Momentum(
learning_rate=0.1,
parameters=net.parameters())
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
model.fit(train_dataset, epochs=2, batch_size=128, verbose=1)
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(f"after fine-tuning: {result}")
```
经过重新训练,精度有所提升,最后看下剪裁后模型的结构信息,如下:
```python
paddle.summary(net, (1, 3, 32, 32))
```
# Paddle动态图自定义剪裁策略教程
## 1. 概述
该教程介绍如果在PaddleSlim提供的接口基础上快速自定义`Filters`剪裁策略。
在PaddleSlim中,所有剪裁`Filters``Pruner`继承自基类`FilterPruner``FilterPruner`中自定义了一系列通用方法,用户只需要重载实现`FilterPruner``cal_mask`接口,`cal_mask`接口定义如下:
```python
def cal_mask(self, var_name, pruned_ratio, group):
raise NotImplemented()
```
`cal_mask`接口接受的参数说明如下:
- **var_name:** 要剪裁的目标变量,一般为卷积层的权重参数的名称。在Paddle中,卷积层的权重参数格式为`[output_channel, input_channel, kernel_size, kernel_size]`,其中,`output_channel`为当前卷积层的输出通道数,`input_channel`为当前卷积层的输入通道数,`kernel_size`为卷积核大小。
- **pruned_ratio:** 对名称为`var_name`的变量的剪裁率。
- **group:** 与待裁目标变量相关的所有变量的信息。
### 1.1 Group概念介绍
<div align="center">
<img src="self_define_filter_pruning/1-1.png" width="600" height="230">
</div>
<div align="center">
<strong>图1-1 卷积层关联关系示意图</strong>
</div>
如图1-1所示,在给定模型中有两个卷积层,第一个卷积层有3个`filters`,第二个卷积层有2个`filters`。如果删除第一个卷积绿色的`filter`,第一个卷积的输出特征图的通道数也会减1,同时需要删掉第二个卷积层绿色的`kernels`。如上所述的两个卷积共同组成一个group,表示如下:
```python
group = {
"conv_1.weight":{
"pruned_dims": [0],
"layer": conv_layer_1,
"var": var_instance_1,
"value": var_value_1,
},
"conv_2.weight":{
"pruned_dims": [1],
"layer": conv_layer_2,
"var": var_instance_2,
"value": var_value_2,
}
}
```
在上述表示`group`的数据结构示例中,`conv_1.weight`为第一个卷积权重参数的名称,其对应的value也是一个dict实例,存放了当前参数的一些信息,包括:
- **pruned_dims:** 类型为`list<int>`,表示当前参数在哪些维度上被裁。
- **layer:** 类型为[paddle.nn.Layer](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/dygraph_cn/Layer_cn.html#layer), 表示当前参数所在`Layer`
- **var:** 类型为[paddle.Tensor](https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/fluid_cn/Variable_cn.html#variable), 表示当前参数对应的实例。
- **value:** 类型为numpy.array类型,待裁参数所存的具体数值,方便开发者使用。
图1-2为更复杂的情况,其中,`Add`操作的所有输入的通道数需要保持一致,`Concat`操作的输出通道数的调整可能会影响到所有输入的通道数,因此`group`中可能包含多个卷积的参数或变量,可以是:卷积权重、卷积bias、`batch norm`相关参数等。
<div align="center">
<img src="self_define_filter_pruning/1-2.png" width="388" height="350">
</div>
<div align="center">
<strong>图1-2 复杂网络示例</strong>
</div>
## 2. 定义模型
```python
import paddle
from paddle.vision.models import mobilenet_v1
net = mobilenet_v1(pretrained=False)
paddle.summary(net, (1, 3, 32, 32))
```
## 3. L2NormFilterPruner
该小节参考`L1NormFilterPruner`实现`L2NormFilterPruner`,方式为集成`FIlterPruner`并重载`cal_mask`接口。代码如下所示:
```python
import numpy as np
from paddleslim.dygraph import FilterPruner
class L2NormFilterPruner(FilterPruner):
def __init__(self, model, input_shape, sen_file=None):
super(L2NormFilterPruner, self).__init__(
model, input_shape, sen_file=sen_file)
def cal_mask(self, var_name, pruned_ratio, group):
value = group[var_name]['value']
pruned_dims = group[var_name]['pruned_dims']
reduce_dims = [
i for i in range(len(value.shape)) if i not in pruned_dims
]
# scores = np.mean(np.abs(value), axis=tuple(reduce_dims))
scores = np.sqrt(np.sum(np.square(value), axis=tuple(reduce_dims)))
sorted_idx = scores.argsort()
pruned_num = int(round(len(sorted_idx) * pruned_ratio))
pruned_idx = sorted_idx[:pruned_num]
mask_shape = [value.shape[i] for i in pruned_dims]
mask = np.ones(mask_shape, dtype="int32")
mask[pruned_idx] = 0
return mask
```
如上述代码所示,我们重载了`FilterPruner`基类的`cal_mask`方法,并在`L1NormFilterPruner`代码基础上,修改了计算通道重要性的语句,将其修改为了计算L2Norm的逻辑:
```python
scores = np.sqrt(np.sum(np.square(value), axis=tuple(reduce_dims)))
```
接下来定义一个`L2NormFilterPruner`对象,并调用`prune_var`方法对单个卷积层进行剪裁,`prune_var`方法继承自`FilterPruner`,开发者不用再重载实现。
按以下代码调用`prune_var`方法后,参数名称为`conv2d_0.w_0`的卷积层会被裁掉50%的`filters`,与之相关关联的后续卷积和`BatchNorm`相关的参数也会被剪裁。`prune_var`不仅会对待裁模型进行`inplace`的裁剪,还会返回保存裁剪详细信息的`PruningPlan`对象,用户可以直接打印`PruningPlan`对象内容。
最后,可以通过调用`Pruner``restore`方法,将已被裁剪的模型恢复到初始状态。
```python
pruner = L2NormFilterPruner(net, [1, 3, 32, 32])
plan = pruner.prune_var("conv2d_0.w_0", 0, 0.5)
print(plan)
pruner.restore()
```
## 4. FPGMFilterPruner
参考:[Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration](https://arxiv.org/abs/1811.00250)
### 4.1 原理介绍
如图4-1所示,传统基于Norm统计方法的filter重要性评估方式的有效性取决于卷积层权重数值的分布,比较理想的分布式要满足两个条件:
- 偏差(deviation)要大
- 最小值要小(图4-1中v1)
满足上述条件后,我们才能裁掉更多Norm统计值较小的参数,如图4-1中红色部分所示。
<div align="center">
<img src="self_define_filter_pruning/4-1.png" width="600" height="170">
</div>
<div align="center">
<strong>图4-1</strong>
</div>
而现实中的模型的权重分布如图4-2中绿色分布所示,总是有较小的偏差或较大的最小值。
<div align="center">
<img src="self_define_filter_pruning/4-2.png" width="600" height="224">
</div>
<div align="center">
<strong>图4-2</strong>
</div>
考虑到上述传统方法的缺点,FPGM则用filter之间的几何距离来表示重要性,其遵循的原则就是:几何距离比较近的filters,作用也相近。
如图4-3所示,有3个filters,将各个filter展开为向量,并两两计算几何距离。其中,绿色filter的重要性得分就是它到其它两个filter的距离和,即0.7071+0.5831=1.2902。同理算出另外两个filters的得分,绿色filter得分最高,其重要性最高。
<div align="center">
<img src="self_define_filter_pruning/4-3.png" width="400" height="560">
</div>
<div align="center">
<strong>图4-3</strong>
</div>
### 4.2 实现
以下代码通过继承`FilterPruner`并重载`cal_mask`实现了`FPGMFilterPruner`,其中,`get_distance_sum`用于计算第`out_idx`个filter的重要性。
```python
import numpy as np
from paddleslim.dygraph import FilterPruner
class FPGMFilterPruner(FilterPruner):
def __init__(self, model, input_shape, sen_file=None):
super(FPGMFilterPruner, self).__init__(
model, input_shape, sen_file=sen_file)
def cal_mask(self, var_name, pruned_ratio, group):
value = group[var_name]['value']
pruned_dims = group[var_name]['pruned_dims']
assert(pruned_dims == [0])
dist_sum_list = []
for out_i in range(value.shape[0]):
dist_sum = self.get_distance_sum(value, out_i)
dist_sum_list.append(dist_sum)
scores = np.array(dist_sum_list)
sorted_idx = scores.argsort()
pruned_num = int(round(len(sorted_idx) * pruned_ratio))
pruned_idx = sorted_idx[:pruned_num]
mask_shape = [value.shape[i] for i in pruned_dims]
mask = np.ones(mask_shape, dtype="int32")
mask[pruned_idx] = 0
return mask
def get_distance_sum(self, value, out_idx):
w = value.view()
w.shape = value.shape[0], np.product(value.shape[1:])
selected_filter = np.tile(w[out_idx], (w.shape[0], 1))
x = w - selected_filter
x = np.sqrt(np.sum(x * x, -1))
return x.sum()
```
接下来声明一个FPGMFilterPruner对象进行验证:
```python
pruner = FPGMFilterPruner(net, [1, 3, 32, 32])
plan = pruner.prune_var("conv2d_0.w_0", 0, 0.5)
print(plan)
pruner.restore()
```
## 5. 敏感度剪裁
在第3节和第4节,开发者自定义实现的`L2NormFilterPruner``FPGMFilterPruner`也继承了`FilterPruner`的敏感度计算方法`sensitive`和剪裁方法`sensitive_prune`
### 5.1 预训练
```python
import paddle.vision.transforms as T
transform = T.Compose([
T.Transpose(),
T.Normalize([127.5], [127.5])
])
train_dataset = paddle.vision.datasets.Cifar10(mode="train", backend="cv2",transform=transform)
val_dataset = paddle.vision.datasets.Cifar10(mode="test", backend="cv2",transform=transform)
from paddle.static import InputSpec as Input
optimizer = paddle.optimizer.Momentum(
learning_rate=0.1,
parameters=net.parameters())
inputs = [Input([None, 3, 32, 32], 'float32', name='image')]
labels = [Input([None, 1], 'int64', name='label')]
model = paddle.Model(net, inputs, labels)
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
model.fit(train_dataset, epochs=2, batch_size=128, verbose=1)
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(result)
```
### 5.2 计算敏感度
```python
pruner = FPGMFilterPruner(net, [1, 3, 32, 32])
def eval_fn():
result = model.evaluate(
val_dataset,
batch_size=128)
return result['acc_top1']
sen = pruner.sensitive(eval_func=eval_fn, sen_file="./fpgm_sen.pickle")
print(sen)
```
### 5.3 剪裁
```python
from paddleslim.analysis import dygraph_flops
flops = dygraph_flops(net, [1, 3, 32, 32])
print(f"FLOPs before pruning: {flops}")
plan = pruner.sensitive_prune(0.4, skip_vars=["conv2d_26.w_0"])
flops = dygraph_flops(net, [1, 3, 32, 32])
print(f"FLOPs after pruning: {flops}")
print(f"Pruned FLOPs: {round(plan.pruned_flops*100, 2)}%")
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(f"before fine-tuning: {result}")
```
### 5.4 重训练
```python
optimizer = paddle.optimizer.Momentum(
learning_rate=0.1,
parameters=net.parameters())
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
model.fit(train_dataset, epochs=2, batch_size=128, verbose=1)
result = model.evaluate(val_dataset,batch_size=128, log_freq=10)
print(f"after fine-tuning: {result}")
```
Subproject commit 2bdaea56566257cf73bd1cbbf834a16f4f7ac4cf
...@@ -11,8 +11,11 @@ ...@@ -11,8 +11,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from .flops import flops from .flops import flops, dygraph_flops
from .model_size import model_size from .model_size import model_size
from .latency import LatencyEvaluator, TableLatencyEvaluator from .latency import LatencyEvaluator, TableLatencyEvaluator
__all__ = ['flops', 'model_size', 'LatencyEvaluator', 'TableLatencyEvaluator'] __all__ = [
'flops', 'dygraph_flops', 'model_size', 'LatencyEvaluator',
'TableLatencyEvaluator'
]
...@@ -11,11 +11,12 @@ ...@@ -11,11 +11,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import paddle
import numpy as np import numpy as np
import paddle.jit as jit
from ..core import GraphWrapper from ..core import GraphWrapper
__all__ = ["flops"] __all__ = ["flops", "dygraph_flops"]
def flops(program, only_conv=True, detail=False): def flops(program, only_conv=True, detail=False):
...@@ -80,3 +81,12 @@ def _graph_flops(graph, only_conv=True, detail=False): ...@@ -80,3 +81,12 @@ def _graph_flops(graph, only_conv=True, detail=False):
return flops, params2flops return flops, params2flops
else: else:
return flops return flops
def dygraph_flops(model, input_shape, only_conv=False, detail=False):
data = np.ones(tuple(input_shape)).astype("float32")
in_var = paddle.to_tensor(data)
_, traced = paddle.jit.TracedLayer.trace(model, [in_var])
program = traced.program
graph = GraphWrapper(program)
return _graph_flops(graph, only_conv=only_conv, detail=detail)
...@@ -160,13 +160,21 @@ class OpWrapper(object): ...@@ -160,13 +160,21 @@ class OpWrapper(object):
""" """
Get all the varibales by the input name. Get all the varibales by the input name.
""" """
return [self._graph.var(var_name) for var_name in self._op.input(name)] if name in self._op.input_names:
return [
self._graph.var(var_name) for var_name in self._op.input(name)
]
return []
def outputs(self, name): def outputs(self, name):
""" """
Get all the varibales by the output name. Get all the varibales by the output name.
""" """
return [self._graph.var(var_name) for var_name in self._op.output(name)] if name in self._op.output_names:
return [
self._graph.var(var_name) for var_name in self._op.output(name)
]
return []
def set_attr(self, key, value): def set_attr(self, key, value):
""" """
......
from . import var_group
from .var_group import *
from . import l1norm_pruner
from .l1norm_pruner import *
from . import pruner
from .pruner import *
from . import filter_pruner
from .filter_pruner import *
from . import l2norm_pruner
from .l2norm_pruner import *
from . import fpgm_pruner
from .fpgm_pruner import *
__all__ = []
__all__ += var_group.__all__
__all__ += l1norm_pruner.__all__
__all__ += l2norm_pruner.__all__
__all__ += fpgm_pruner.__all__
__all__ += pruner.__all__
__all__ += filter_pruner.__all__
import os
import logging
import numpy as np
import pickle
import paddle
from ..common import get_logger
from .var_group import *
from .pruning_plan import *
from .pruner import Pruner
from ..analysis import dygraph_flops as flops
from .var_group import VarGroup
__all__ = ['Status', 'FilterPruner']
_logger = get_logger(__name__, logging.INFO)
CONV_OP_TYPE = paddle.nn.Conv2D
FILTER_DIM = [0]
CONV_WEIGHT_NAME = "weight"
class Status():
def __init__(self, src=None):
self.sensitivies = {}
self.accumulates = {}
self.is_ckp = True
if src is not None:
self.load(src)
def save(self, dst):
with open(dst, 'wb') as f:
pickle.dump(self, f)
_logger.info("Save status into {}".format(dst))
def load(self, src):
with open(src, 'rb') as f:
data = pickle.load(f)
self.sensitivies = data.sensitivies
self.accumulates = data.accumulates
self.is_ckp = data.is_ckp
_logger.info("Load status from {}".format(src))
class FilterPruner(Pruner):
"""
Pruner used to prune filter structure in convolution layer.
Args:
model(paddle.nn.Layer): The target model to be pruned.
input_shape(list<int>): The input shape of model. It is used to trace the graph of the model.
sen_file(str, optional): The absolute path of file that stores computed sensitivities. If it is
set rightly, 'FilterPruner::sensitive' function can not be called anymore
in next step. Default: None.
"""
def __init__(self, model, input_shape, sen_file=None):
super(FilterPruner, self).__init__(model, input_shape)
self._status = Status(sen_file)
# sensitive and var_group are just used in filter pruning
self.var_group = VarGroup(model, input_shape)
def sensitive(self,
eval_func=None,
sen_file=None,
target_vars=None,
skip_vars=None):
"""
Compute or get sensitivities of model in current pruner. It will return a cached sensitivities when all the arguments are "None".
This function return a dict storing sensitivities as below:
.. code-block:: python
{"weight_0":
{0.1: 0.22,
0.2: 0.33
},
"weight_1":
{0.1: 0.21,
0.2: 0.4
}
}
``weight_0`` is parameter name of convolution. ``sensitivities['weight_0']`` is a dict in which key is pruned ratio and value is the percent of losses.
Args:
eval_func(function, optional): The function to evaluate the model in current pruner. This function should have an empy arguments list and return a score with type "float32". Default: None.
sen_file(str, optional): The absolute path of file to save sensitivities into local filesystem. Default: None.
target_vars(list, optional): The names of tensors whose sensitivity will be computed. "None" means all weights in convolution layer will be computed. Default: None.
skip_vars(list, optional): The names of tensors whose sensitivity won't be computed. "None" means skip nothing. Default: None.
Returns:
dict: A dict storing sensitivities.
"""
if eval_func is None and sen_file is None:
return self._status.sensitivies
if sen_file is not None and os.path.isfile(sen_file):
self._status.load(sen_file)
if not self._status.is_ckp:
return self._status
self._cal_sensitive(
self.model,
eval_func,
status_file=sen_file,
target_vars=target_vars,
skip_vars=skip_vars)
self._status.is_ckp = False
return self._status.sensitivies
def _get_ratios_by_loss(self, sensitivities, loss, skip_vars=[]):
"""
Get the max ratio of each parameter. The loss of accuracy must be less than given `loss`
when the single parameter was pruned by the max ratio.
Args:
sensitivities(dict): The sensitivities used to generate a group of pruning ratios. The key of dict
is name of parameters to be pruned. The value of dict is a list of tuple with
format `(pruned_ratio, accuracy_loss)`.
loss(float): The threshold of accuracy loss.
skip_vars(list, optional): The names of tensors whose sensitivity won't be computed. "None" means skip nothing. Default: None.
Returns:
dict: A group of ratios. The key of dict is name of parameters while the value is the ratio to be pruned.
"""
ratios = {}
for param, losses in sensitivities.items():
if param in skip_vars:
continue
losses = losses.items()
losses = list(losses)
losses.sort()
for i in range(len(losses))[::-1]:
if losses[i][1] <= loss:
if i == (len(losses) - 1):
ratios[param] = float(losses[i][0])
else:
r0, l0 = losses[i]
r1, l1 = losses[i + 1]
r0 = float(r0)
r1 = float(r1)
d0 = loss - l0
d1 = l1 - loss
ratio = r0 + (loss - l0) * (r1 - r0) / (l1 - l0)
ratios[param] = ratio
if ratio > 1:
_logger.info(losses, ratio, (r1 - r0) / (l1 - l0),
i)
break
return ratios
def _round_to(self, ratios, dims=[0], factor=8):
ret = {}
for name in ratios:
ratio = ratios[name]
dim = self._var_shapes[name][dims[0]]
remained = round((1 - ratio) * dim / factor) * factor
if remained == 0:
remained = factor
ratio = float(dim - remained) / dim
ratio = ratio if ratio > 0 else 0.
ret[name] = ratio
return ret
def get_ratios_by_sensitivity(self,
pruned_flops,
align=None,
dims=[0],
skip_vars=[]):
"""
Get a group of ratios by sensitivities.
Args:
pruned_flops(float): The excepted rate of FLOPs to be pruned. It should be in range (0, 1).
align(int, optional): Round the size of each pruned dimension to multiple of 'align' if 'align' is not None. Default: None.
dims(list, optional): The dims to be pruned on. [0] means pruning channels of output for convolution. Default: [0].
skip_vars(list, optional): The names of tensors whose sensitivity won't be computed. "None" means skip nothing. Default: None.
Returns:
tuple: A tuple with format ``(ratios, pruned_flops)`` . "ratios" is a dict whose key is name of tensor and value is ratio to be pruned. "pruned_flops" is the ratio of total pruned FLOPs in the model.
"""
base_flops = flops(self.model, self.input_shape)
_logger.info("Base FLOPs: {}".format(base_flops))
low = 0.
up = 1.0
history = set()
while low < up:
loss = (low + up) / 2
ratios = self._get_ratios_by_loss(
self._status.sensitivies, loss, skip_vars=skip_vars)
_logger.debug("pruning ratios: {}".format(ratios))
if align is not None:
ratios = self._round_to(ratios, dims=dims, factor=align)
plan = self.prune_vars(ratios, axis=dims)
_logger.debug("pruning plan: {}".format(plan))
c_flops = flops(self.model, self.input_shape)
_logger.debug("FLOPs after pruning: {}".format(c_flops))
c_pruned_flops = (base_flops - c_flops) / base_flops
plan.restore(self.model)
_logger.debug("Seaching ratios, pruned FLOPs: {}".format(
c_pruned_flops))
key = str(round(c_pruned_flops, 4))
if key in history:
return ratios, c_pruned_flops
history.add(key)
if c_pruned_flops < pruned_flops:
low = loss
elif c_pruned_flops > pruned_flops:
up = loss
else:
return ratios, c_pruned_flops
return ratios, c_pruned_flops
def _cal_sensitive(self,
model,
eval_func,
status_file=None,
target_vars=None,
skip_vars=None):
sensitivities = self._status.sensitivies
baseline = eval_func()
ratios = np.arange(0.1, 1, step=0.1)
for group in self.var_group.groups:
var_name = group[0][0]
dims = group[0][1]
if target_vars is not None and var_name not in target_vars:
continue
if skip_vars is not None and var_name in skip_vars:
continue
if var_name not in sensitivities:
sensitivities[var_name] = {}
for ratio in ratios:
ratio = round(ratio, 2)
if ratio in sensitivities[var_name]:
_logger.debug("{}, {} has computed.".format(var_name,
ratio))
continue
plan = self.prune_var(var_name, dims, ratio, apply="lazy")
pruned_metric = eval_func()
loss = (baseline - pruned_metric) / baseline
_logger.info("pruned param: {}; {}; loss={}".format(
var_name, ratio, loss))
sensitivities[var_name][ratio] = loss
self._status.save(status_file)
plan.restore(model)
return sensitivities
def sensitive_prune(self, pruned_flops, skip_vars=[], align=None):
# skip depthwise convolutions
for layer in self.model.sublayers():
if isinstance(layer,
paddle.nn.layer.conv.Conv2D) and layer._groups > 1:
for param in layer.parameters(include_sublayers=False):
skip_vars.append(param.name)
_logger.debug("skip vars: {}".format(skip_vars))
self.restore()
ratios, pruned_flops = self.get_ratios_by_sensitivity(
pruned_flops, align=align, dims=FILTER_DIM, skip_vars=skip_vars)
_logger.debug("ratios: {}".format(ratios))
self.plan = self.prune_vars(ratios, FILTER_DIM)
self.plan._pruned_flops = pruned_flops
return self.plan
def restore(self):
if self.plan is not None:
self.plan.restore(self.model)
def cal_mask(self, var_name, pruned_ratio, group):
"""
{
var_name: {
'layer': sub_layer,
'var': variable,
'value': np.array([]),
'pruned_dims': [1],
}
}
"""
raise NotImplemented("cal_mask is not implemented")
def prune_var(self, var_name, pruned_dims, pruned_ratio, apply="impretive"):
"""
Pruning a variable.
Parameters:
var_name(str): The name of variable.
pruned_dims(list<int>): The axies to be pruned. For convolution with format [out_c, in_c, k, k],
'axis=[0]' means pruning filters and 'axis=[0, 1]' means pruning kernels.
pruned_ratio(float): The ratio of pruned values in one variable.
Returns:
plan: An instance of PruningPlan that can be applied on model by calling 'plan.apply(model)'.
"""
if isinstance(pruned_dims, int):
pruned_dims = [pruned_dims]
group = self.var_group.find_group(var_name, pruned_dims)
_logger.debug("found group with {}: {}".format(var_name, group))
plan = PruningPlan(self.model.full_name)
for sub_layer in self.model.sublayers():
for param in sub_layer.parameters(include_sublayers=False):
if param.name in group:
group[param.name]['layer'] = sub_layer
group[param.name]['var'] = param
group[param.name]['value'] = np.array(param.value()
.get_tensor())
_logger.debug(f"set value of {param.name} into group")
mask = self.cal_mask(var_name, pruned_ratio, group)
for _name in group:
dims = group[_name]['pruned_dims']
if isinstance(dims, int):
dims = [dims]
plan.add(_name, PruningMask(dims, mask, pruned_ratio))
if apply == "lazy":
plan.apply(self.model, lazy=True)
elif apply == "impretive":
plan.apply(self.model, lazy=False)
return plan
import logging
import numpy as np
import paddle
from ..common import get_logger
from .var_group import *
from .pruning_plan import *
from .filter_pruner import FilterPruner
__all__ = ['FPGMFilterPruner']
_logger = get_logger(__name__, logging.INFO)
class FPGMFilterPruner(FilterPruner):
def __init__(self, model, input_shape, sen_file=None):
super(FPGMFilterPruner, self).__init__(
model, input_shape, sen_file=sen_file)
def cal_mask(self, var_name, pruned_ratio, group):
value = group[var_name]['value']
pruned_dims = group[var_name]['pruned_dims']
assert (pruned_dims == [0])
dist_sum_list = []
for out_i in range(value.shape[0]):
dist_sum = self.get_distance_sum(value, out_i)
dist_sum_list.append(dist_sum)
scores = np.array(dist_sum_list)
sorted_idx = scores.argsort()
pruned_num = int(round(len(sorted_idx) * pruned_ratio))
pruned_idx = sorted_idx[:pruned_num]
mask_shape = [value.shape[i] for i in pruned_dims]
mask = np.ones(mask_shape, dtype="int32")
mask[pruned_idx] = 0
return mask
def get_distance_sum(self, value, out_idx):
w = value.view()
w.shape = value.shape[0], np.product(value.shape[1:])
selected_filter = np.tile(w[out_idx], (w.shape[0], 1))
x = w - selected_filter
x = np.sqrt(np.sum(x * x, -1))
return x.sum()
import logging
import numpy as np
import paddle
from ..common import get_logger
from .var_group import *
from .pruning_plan import *
from .filter_pruner import FilterPruner
__all__ = ['L1NormFilterPruner']
_logger = get_logger(__name__, logging.INFO)
class L1NormFilterPruner(FilterPruner):
def __init__(self, model, input_shape, sen_file=None):
super(L1NormFilterPruner, self).__init__(
model, input_shape, sen_file=sen_file)
def cal_mask(self, var_name, pruned_ratio, group):
value = group[var_name]['value']
pruned_dims = group[var_name]['pruned_dims']
reduce_dims = [
i for i in range(len(value.shape)) if i not in pruned_dims
]
l1norm = np.mean(np.abs(value), axis=tuple(reduce_dims))
sorted_idx = l1norm.argsort()
pruned_num = int(round(len(sorted_idx) * pruned_ratio))
pruned_idx = sorted_idx[:pruned_num]
mask_shape = [value.shape[i] for i in pruned_dims]
mask = np.ones(mask_shape, dtype="int32")
mask[pruned_idx] = 0
return mask
import logging
import numpy as np
import paddle
from ..common import get_logger
from .var_group import *
from .pruning_plan import *
from .filter_pruner import FilterPruner
__all__ = ['L2NormFilterPruner']
_logger = get_logger(__name__, logging.INFO)
class L2NormFilterPruner(FilterPruner):
def __init__(self, model, input_shape, sen_file=None):
super(L2NormFilterPruner, self).__init__(
model, input_shape, sen_file=sen_file)
def cal_mask(self, var_name, pruned_ratio, group):
value = group[var_name]['value']
pruned_dims = group[var_name]['pruned_dims']
reduce_dims = [
i for i in range(len(value.shape)) if i not in pruned_dims
]
# scores = np.mean(np.abs(value), axis=tuple(reduce_dims))
scores = np.sqrt(np.sum(np.square(value), axis=tuple(reduce_dims)))
sorted_idx = scores.argsort()
pruned_num = int(round(len(sorted_idx) * pruned_ratio))
pruned_idx = sorted_idx[:pruned_num]
mask_shape = [value.shape[i] for i in pruned_dims]
mask = np.ones(mask_shape, dtype="int32")
mask[pruned_idx] = 0
return mask
import os
import pickle
import numpy as np
import logging
from .pruning_plan import PruningPlan
from ..common import get_logger
__all__ = ["Pruner"]
_logger = get_logger(__name__, level=logging.INFO)
class Pruner(object):
"""
Pruner used to resize or mask dimensions of variables.
Args:
model(paddle.nn.Layer): The target model to be pruned.
input_shape(list<int>): The input shape of model. It is used to trace the graph of the model.
"""
def __init__(self, model, input_shape):
self.model = model
self.input_shape = input_shape
self._var_shapes = {}
for var in model.parameters():
self._var_shapes[var.name] = var.shape
self.plan = None
def status(self, data=None, eval_func=None, status_file=None):
raise NotImplemented("status is not implemented")
def prune_var(self, var_name, axis, pruned_ratio, apply="impretive"):
raise NotImplemented("prune_var is not implemented")
def prune_vars(self, ratios, axis, apply="impretive"):
"""
Pruning variables by given ratios.
Args:
ratios(dict<str, float>): The key is the name of variable to be pruned and the
value is the pruned ratio.
axis(list): The dimensions to be pruned on.
Returns:
plan(PruningPlan): The pruning plan.
"""
global_plan = PruningPlan(self.model.full_name)
for var, ratio in ratios.items():
if not global_plan.contains(var, axis):
plan = self.prune_var(var, axis, ratio, apply=None)
global_plan.extend(plan)
if apply == "lazy":
global_plan.apply(self.model, lazy=True)
elif apply == "impretive":
global_plan.apply(self.model, lazy=False)
return global_plan
import paddle
import collections
import numpy as np
import logging
from ..common import get_logger
from paddle.fluid import core
_logger = get_logger(__name__, level=logging.INFO)
__all__ = ['PruningPlan', 'PruningMask']
class PruningMask():
def __init__(self, dims, mask, ratio):
self._dims = dims
self._mask = mask
self._pruned_ratio = ratio
@property
def dims(self):
return self._dims
@dims.setter
def dims(self, value):
if not isinstance(value, collections.Iterator):
raise ValueError(
"The dims of PruningMask must be instance of collections.Iterator."
)
if self._mask is not None:
assert (
len(self._mask.shape) == len(value),
"The length of value must be same with shape of mask in current PruningMask instance."
)
self._dims = list(value)
@property
def mask(self):
return self._mask
@mask.setter
def mask(self, value):
assert (isinstance(value, PruningMask))
if self._dims is not None:
assert (
len(self._mask.shape) == len(value),
"The length of value must be same with shape of mask in current PruningMask instance."
)
self._mask = value
def __str__(self):
return "{}\t{}".format(self._pruned_ratio, self._dims)
class PruningPlan():
def __init__(self, model_name=None):
# {"conv_weight": (axies, mask)}
self._model_name = model_name
self._plan_id = model_name
self._masks = {} #{param_name: pruning_mask}
self._dims = {}
self._pruned_size = None
self._total_size = None
self._pruned_flops = None
self._pruned_size = None
self._model_size = None
@property
def pruned_flops(self):
return self._pruned_flops
@pruned_flops.setter
def pruned_flops(self, value):
self._pruned_flops = value
def add(self, var_name, pruning_mask):
assert (isinstance(pruning_mask, PruningMask))
if var_name not in self._masks:
self._masks[var_name] = []
self._masks[var_name].append(pruning_mask)
if var_name not in self._dims:
self._dims[var_name] = []
self._dims[var_name].append(pruning_mask.dims)
@property
def masks(self):
return self._masks
def extend(self, plan):
assert (isinstance(plan, PruningPlan))
for var_name in plan.masks:
for mask in plan.masks[var_name]:
if not self.contains(var_name, mask.dims):
self.add(var_name, mask)
def contains(self, var_name, dims=None):
return (var_name in self._dims) and (dims is None or
dims in self._dims[var_name])
def __str__(self):
details = "\npruned FLOPs: {}".format(self._pruned_flops)
head = "variable name\tpruned ratio\tpruned dims\n"
return head + "\n".join([
"{}:\t{}".format(name, ",".join([str(m) for m in mask]))
for name, mask in self._masks.items()
]) + details
def apply(self, model, lazy=False):
if lazy:
self.lazy_apply(model)
else:
self.imperative_apply(model)
def lazy_apply(self, model):
for name, sub_layer in model.named_sublayers():
for param in sub_layer.parameters(include_sublayers=False):
if param.name in self._masks:
for _mask in self._masks[param.name]:
dims = _mask.dims
mask = _mask.mask
t_value = param.value().get_tensor()
value = np.array(t_value).astype("float32")
# The name of buffer can not contains "."
backup_name = param.name.replace(".", "_") + "_backup"
if backup_name not in sub_layer._buffers:
sub_layer.register_buffer(backup_name,
paddle.to_tensor(value))
_logger.debug("Backup values of {} into buffers.".
format(param.name))
expand_mask_shape = [1] * len(value.shape)
for i in dims:
expand_mask_shape[i] = value.shape[i]
_logger.debug("Expanded mask shape: {}".format(
expand_mask_shape))
expand_mask = mask.reshape(expand_mask_shape).astype(
"float32")
p = t_value._place()
if p.is_cpu_place():
place = paddle.CPUPlace()
elif p.is_cuda_pinned_place():
place = paddle.CUDAPinnedPlace()
else:
p = core.Place()
p.set_place(t_value._place())
place = paddle.CUDAPlace(p.gpu_device_id())
t_value.set(value * expand_mask, place)
def imperative_apply(self, model):
"""
Pruning values of variable imperatively. It is valid when pruning
on one dimension.
"""
for name, sub_layer in model.named_sublayers():
for param in sub_layer.parameters(include_sublayers=False):
if param.name in self._masks:
for _mask in self._masks[param.name]:
dims = _mask.dims
mask = _mask.mask
assert (
len(dims) == 1,
"Imperative mode only support for pruning"
"on one dimension, but get dims {} when pruning parameter {}".
format(dims, param.name))
t_value = param.value().get_tensor()
value = np.array(t_value).astype("float32")
# The name of buffer can not contains "."
backup_name = param.name.replace(".", "_") + "_backup"
if backup_name not in sub_layer._buffers:
sub_layer.register_buffer(backup_name,
paddle.to_tensor(value))
_logger.debug("Backup values of {} into buffers.".
format(param.name))
bool_mask = mask.astype(bool)
pruned_value = np.apply_along_axis(
lambda data: data[bool_mask], dims[0], value)
p = t_value._place()
if p.is_cpu_place():
place = paddle.CPUPlace()
elif p.is_cuda_pinned_place():
place = paddle.CUDAPinnedPlace()
else:
p = core.Place()
p.set_place(t_value._place())
place = paddle.CUDAPlace(p.gpu_device_id())
t_value.set(pruned_value, place)
if isinstance(sub_layer, paddle.nn.layer.conv.Conv2D):
if sub_layer._groups > 1:
_logger.debug(
"Update groups of conv form {} to {}".
format(sub_layer._groups,
pruned_value.shape[0]))
sub_layer._groups = pruned_value.shape[0]
# for training
if param.trainable:
param.clear_gradient()
def restore(self, model):
for name, sub_layer in model.named_sublayers():
for param in sub_layer.parameters(include_sublayers=False):
backup_name = "_".join([param.name.replace(".", "_"), "backup"])
if backup_name in sub_layer._buffers:
_logger.debug("Restore values of variable: {}".format(
param.name))
t_value = param.value().get_tensor()
t_backup = sub_layer._buffers[backup_name].value(
).get_tensor()
p = t_value._place()
if p.is_cpu_place():
place = paddle.CPUPlace()
elif p.is_cuda_pinned_place():
place = paddle.CUDAPinnedPlace()
else:
p = core.Place()
p.set_place(t_value._place())
place = paddle.CUDAPlace(p.gpu_device_id())
t_value.set(np.array(t_backup).astype("float32"), place)
if isinstance(sub_layer, paddle.nn.layer.conv.Conv2D):
if sub_layer._groups > 1:
_logger.debug(
"Update groups of conv form {} to {}".format(
sub_layer._groups, t_value.shape()[0]))
sub_layer._groups = t_value.shape()[0]
del sub_layer._buffers[backup_name]
import numpy as np
import logging
import paddle
from paddle.fluid.dygraph import TracedLayer
from ..core import GraphWrapper
from ..prune import collect_convs
from ..common import get_logger
__all__ = ["VarGroup"]
_logger = get_logger(__name__, level=logging.INFO)
class VarGroup():
def __init__(self, model, input_shape):
self.groups = []
self._parse_model(model, input_shape)
def _to_dict(self, group):
ret = {}
for _name, _axis, _idx in group:
if isinstance(_axis, int):
_axis = [_axis] # TODO: fix
ret[_name] = {'pruned_dims': _axis, 'pruned_idx': _idx}
return ret
def find_group(self, var_name, axis):
for group in self.groups:
for _name, _axis, _ in group:
if isinstance(_axis, int):
_axis = [_axis] # TODO: fix
if _name == var_name and _axis == axis:
return self._to_dict(group)
def _parse_model(self, model, input_shape):
_logger.debug("Parsing model with input: {}".format(input_shape))
data = np.ones(tuple(input_shape)).astype("float32")
in_var = paddle.to_tensor(data)
out_dygraph, static_layer = TracedLayer.trace(model, inputs=[in_var])
graph = GraphWrapper(static_layer.program)
visited = {}
for name, param in model.named_parameters():
group = collect_convs([param.name], graph,
visited)[0] # [(name, axis, pruned_idx)]
if len(group) > 0:
self.groups.append(group)
_logger.debug("Found {} groups.".format(len(self.groups)))
def __str__(self):
return "\n".join([str(group) for group in self.groups])
...@@ -16,7 +16,11 @@ from __future__ import absolute_import ...@@ -16,7 +16,11 @@ from __future__ import absolute_import
from .util import image_classification from .util import image_classification
from .slimfacenet import SlimFaceNet_A_x0_60, SlimFaceNet_B_x0_75, SlimFaceNet_C_x0_75 from .slimfacenet import SlimFaceNet_A_x0_60, SlimFaceNet_B_x0_75, SlimFaceNet_C_x0_75
from .slim_mobilenet import SlimMobileNet_v1, SlimMobileNet_v2, SlimMobileNet_v3, SlimMobileNet_v4, SlimMobileNet_v5 from .slim_mobilenet import SlimMobileNet_v1, SlimMobileNet_v2, SlimMobileNet_v3, SlimMobileNet_v4, SlimMobileNet_v5
from .mobilenet import MobileNet from ..models import mobilenet
from .resnet import ResNet50 from .mobilenet import *
from ..models import resnet
from .resnet import *
__all__ = ["image_classification", "MobileNet", "ResNet50"] __all__ = ["image_classification"]
__all__ += mobilenet.__all__
__all__ += resnet.__all__
...@@ -13,11 +13,15 @@ ...@@ -13,11 +13,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging
from ..core import GraphWrapper from ..core import GraphWrapper
from ..common import get_logger
from .prune_walker import PRUNE_WORKER from .prune_walker import PRUNE_WORKER
__all__ = ["collect_convs"] __all__ = ["collect_convs"]
_logger = get_logger(__name__, level=logging.INFO)
def collect_convs(params, graph, visited={}): def collect_convs(params, graph, visited={}):
"""Collect convolution layers of graph into groups. The layers in the same group is relative on pruning operation. """Collect convolution layers of graph into groups. The layers in the same group is relative on pruning operation.
...@@ -66,6 +70,11 @@ def collect_convs(params, graph, visited={}): ...@@ -66,6 +70,11 @@ def collect_convs(params, graph, visited={}):
break break
else: else:
cls = PRUNE_WORKER.get(target_op.type()) cls = PRUNE_WORKER.get(target_op.type())
if cls is None:
_logger.info("No walker for operator: {}".format(target_op.type(
)))
groups.append(pruned_params)
continue
walker = cls(target_op, walker = cls(target_op,
pruned_params=pruned_params, pruned_params=pruned_params,
visited=visited) visited=visited)
......
...@@ -94,7 +94,29 @@ class conv2d(PruneWorker): ...@@ -94,7 +94,29 @@ class conv2d(PruneWorker):
def __init__(self, op, pruned_params, visited={}): def __init__(self, op, pruned_params, visited={}):
super(conv2d, self).__init__(op, pruned_params, visited) super(conv2d, self).__init__(op, pruned_params, visited)
def _is_depthwise_conv(self, op):
data_format = self.op.attr("data_format")
channel_axis = 1
if data_format == "NHWC":
channel_axis = 3
filter_shape = self.op.inputs("Filter")[0].shape()
input_shape = self.op.inputs("Input")[0].shape()
num_channels = input_shape[channel_axis]
groups = self.op.attr("groups")
num_filters = filter_shape[0]
return (num_channels == groups and num_channels != 1 and
num_filters % num_channels == 0)
def _prune(self, var, pruned_axis, pruned_idx): def _prune(self, var, pruned_axis, pruned_idx):
if self._is_depthwise_conv(self.op):
_logger.debug(f"Meet conv2d who is depthwise conv2d actually.")
walker = depthwise_conv2d(
self.op, self.pruned_params, visited=self.visited)
walker._prune(var, pruned_axis, pruned_idx)
return
data_format = self.op.attr("data_format") data_format = self.op.attr("data_format")
channel_axis = 1 channel_axis = 1
if data_format == "NHWC": if data_format == "NHWC":
...@@ -234,8 +256,11 @@ class elementwise_op(PruneWorker): ...@@ -234,8 +256,11 @@ class elementwise_op(PruneWorker):
def _prune(self, var, pruned_axis, pruned_idx): def _prune(self, var, pruned_axis, pruned_idx):
axis = self.op.attr("axis") axis = self.op.attr("axis")
if axis == -1: # TODO if axis == -1:
axis = 0 x = self.op.inputs("X")[0]
y = self.op.inputs("Y")[0]
axis = len(x.shape()) - len(y.shape())
if var in self.op.outputs("Out"): if var in self.op.outputs("Out"):
for name in ["X", "Y"]: for name in ["X", "Y"]:
actual_axis = pruned_axis actual_axis = pruned_axis
...@@ -251,19 +276,27 @@ class elementwise_op(PruneWorker): ...@@ -251,19 +276,27 @@ class elementwise_op(PruneWorker):
else: else:
if var in self.op.inputs("X"): if var in self.op.inputs("X"):
in_var = self.op.inputs("Y")[0] in_var = self.op.inputs("Y")[0]
if not (len(in_var.shape()) == 1 and in_var.shape()[0] == 1): y_pruned_axis = pruned_axis
if in_var.is_parameter(): if len(in_var.shape()) != len(var.shape()):
self.pruned_params.append( assert (len(var.shape()) > len(in_var.shape()))
(in_var, pruned_axis - axis, pruned_idx)) if axis == -1:
axis = len(var.shape()) - len(in_var.shape())
y_pruned_axis = pruned_axis - axis
if y_pruned_axis >= 0 and not (len(in_var.shape()) == 1 and
in_var.shape()[0] == 1):
self.pruned_params.append(
(in_var, y_pruned_axis, pruned_idx))
pre_ops = in_var.inputs() pre_ops = in_var.inputs()
for op in pre_ops: for op in pre_ops:
self._prune_op(op, in_var, pruned_axis - axis, self._prune_op(op, in_var, y_pruned_axis, pruned_idx)
pruned_idx)
elif var in self.op.inputs("Y"): elif var in self.op.inputs("Y"):
in_var = self.op.inputs("X")[0] in_var = self.op.inputs("X")[0]
if not (len(in_var.shape()) == 1 and in_var.shape()[0] == 1): if len(in_var.shape()) != len(var.shape()):
pre_ops = in_var.inputs() assert (len(var.shape()) < len(in_var.shape()))
pruned_axis = pruned_axis + axis pruned_axis = pruned_axis + axis
if pruned_axis <= len(in_var.shape()):
pre_ops = in_var.inputs()
for op in pre_ops: for op in pre_ops:
self._prune_op(op, in_var, pruned_axis, pruned_idx) self._prune_op(op, in_var, pruned_axis, pruned_idx)
......
...@@ -111,8 +111,10 @@ class Pruner(): ...@@ -111,8 +111,10 @@ class Pruner():
"The weights have been pruned once.") "The weights have been pruned once.")
group_values = [] group_values = []
for name, axis, pruned_idx in group: for name, axis, pruned_idx in group:
values = np.array(scope.find_var(name).get_tensor()) var = scope.find_var(name)
group_values.append((name, values, axis, pruned_idx)) if var is not None:
values = np.array(var.get_tensor())
group_values.append((name, values, axis, pruned_idx))
scores = self.criterion( scores = self.criterion(
group_values, graph) # [(name, axis, score, pruned_idx)] group_values, graph) # [(name, axis, score, pruned_idx)]
......
# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
sys.path.append("../../")
import unittest
import time
import numpy as np
import paddle
import paddle.fluid as fluid
import paddle.vision.transforms as T
from paddle.static import InputSpec as Input
from paddleslim.dygraph import L1NormFilterPruner, L2NormFilterPruner, FPGMFilterPruner
from paddleslim.dygraph import Status
class TestStatus(unittest.TestCase):
def runTest(self):
status = Status()
status.sensitivies = {
"conv2d_1.weights": {
0.1: 0.11,
0.2: 0.22,
0.3: 0.33
}
}
local_file = "./sen_{}.pickle".format(time.time())
status.save(local_file)
status1 = Status(local_file)
for _name in status.sensitivies:
for _ratio, _loss in status.sensitivies[_name].items():
self.assertTrue(status1.sensitivies[_name][_ratio], _loss)
class TestFilterPruner(unittest.TestCase):
def __init__(self, methodName='runTest', param_names=[]):
super(TestFilterPruner, self).__init__(methodName)
self._param_names = param_names
transform = T.Compose([T.Transpose(), T.Normalize([127.5], [127.5])])
self.train_dataset = paddle.vision.datasets.MNIST(
mode="train", backend="cv2", transform=transform)
self.val_dataset = paddle.vision.datasets.MNIST(
mode="test", backend="cv2", transform=transform)
def _reader():
for data in self.val_dataset:
yield data
self.val_reader = _reader
def runTest(self):
with fluid.unique_name.guard():
net = paddle.vision.models.LeNet()
optimizer = paddle.optimizer.Adam(
learning_rate=0.001, parameters=net.parameters())
inputs = [Input([None, 1, 28, 28], 'float32', name='image')]
labels = [Input([None, 1], 'int64', name='label')]
model = paddle.Model(net, inputs, labels)
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
model.fit(self.train_dataset, epochs=1, batch_size=128, verbose=1)
pruners = []
pruner = L1NormFilterPruner(net, [1, 1, 28, 28])
pruners.append(pruner)
pruner = FPGMFilterPruner(net, [1, 1, 28, 28])
pruners.append(pruner)
pruner = L2NormFilterPruner(net, [1, 1, 28, 28])
pruners.append(pruner)
def eval_fn():
result = model.evaluate(
self.val_dataset, batch_size=128, verbose=1)
return result['acc_top1']
sen_file = "_".join(["./dygraph_sen_", str(time.time())])
for pruner in pruners:
sen = pruner.sensitive(
eval_func=eval_fn,
sen_file=sen_file,
target_vars=self._param_names)
base_acc = eval_fn()
plan = pruner.sensitive_prune(0.01)
pruner.restore()
restore_acc = eval_fn()
self.assertTrue(restore_acc == base_acc)
plan = pruner.sensitive_prune(0.01, align=4)
for param in net.parameters():
if param.name in self._param_names:
self.assertTrue(param.shape[0] % 4 == 0)
pruner.restore()
def add_cases(suite):
suite.addTest(TestStatus())
suite.addTest(TestFilterPruner(param_names=["conv2d_0.w_0"]))
def load_tests(loader, standard_tests, pattern):
suite = unittest.TestSuite()
add_cases(suite)
return suite
if __name__ == '__main__':
unittest.main()
import sys
sys.path.append("../../")
import unittest
from paddleslim.analysis import dygraph_flops as flops
from paddle.vision.models import mobilenet_v1, resnet50
class TestFlops(unittest.TestCase):
def __init__(self, methodName='runTest', net=None, gt=None):
super(TestFlops, self).__init__(methodName)
self._net = net
self._gt = gt
def runTest(self):
net = self._net(pretrained=False)
FLOPs = flops(net, (1, 3, 32, 32))
self.assertTrue(FLOPs == self._gt)
def add_cases(suite):
suite.addTest(TestFlops(net=mobilenet_v1, gt=11792896.0))
suite.addTest(TestFlops(net=resnet50, gt=83872768.0))
def load_tests(loader, standard_tests, pattern):
suite = unittest.TestSuite()
add_cases(suite)
return suite
if __name__ == '__main__':
unittest.main()
# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
sys.path.append("../../")
import unittest
import paddle
import paddle.fluid as fluid
from paddleslim.dygraph import L1NormFilterPruner
from paddle.vision.models import mobilenet_v1, resnet50
from paddleslim.prune import Pruner
class TestPrune(unittest.TestCase):
def __init__(self, methodName='runTest', ratios=None, net=None):
super(TestPrune, self).__init__(methodName)
self._net = net
self._ratios = ratios
def runTest(self):
static_shapes = self.static_prune(self._net, self._ratios)
dygraph_shapes = self.dygraph_prune(self._net, self._ratios)
all_right = True
for _name, _shape in static_shapes.items():
if dygraph_shapes[_name] != list(_shape):
print(
f"name: {_name}; static shape: {_shape}, dygraph shape: {dygraph_shapes[_name]}"
)
all_right = False
self.assertTrue(all_right)
def dygraph_prune(self, net, ratios):
paddle.disable_static()
model = net(pretrained=False)
pruner = L1NormFilterPruner(model, [1, 3, 16, 16])
pruner.prune_vars(ratios, [0])
shapes = {}
for param in model.parameters():
shapes[param.name] = param.shape
return shapes
def static_prune(self, net, ratios):
paddle.enable_static()
main_program = fluid.Program()
startup_program = fluid.Program()
with fluid.unique_name.guard():
with fluid.program_guard(main_program, startup_program):
input = fluid.data(name="image", shape=[None, 3, 16, 16])
model = net(pretrained=False)
out = model(input)
place = fluid.CPUPlace()
exe = fluid.Executor(place)
scope = fluid.Scope()
exe.run(startup_program, scope=scope)
pruner = Pruner()
main_program, _, _ = pruner.prune(
main_program,
scope,
params=ratios.keys(),
ratios=ratios.values(),
place=place,
lazy=False,
only_graph=False,
param_backup=None,
param_shape_backup=None)
shapes = {}
for param in main_program.global_block().all_parameters():
shapes[param.name] = param.shape
return shapes
def add_cases(suite):
suite.addTest(
TestPrune(
net=mobilenet_v1,
ratios={"conv2d_22.w_0": 0.5,
"conv2d_8.w_0": 0.6}))
suite.addTest(
TestPrune(
net=resnet50, ratios={"conv2d_22.w_0": 0.5,
"conv2d_8.w_0": 0.6}))
def load_tests(loader, standard_tests, pattern):
suite = unittest.TestSuite()
add_cases(suite)
return suite
if __name__ == '__main__':
unittest.main()
# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
sys.path.append("../../")
import unittest
import time
import numpy as np
import paddle
import paddle.fluid as fluid
from paddleslim.prune import sensitivity
import paddle.vision.transforms as T
from paddle.static import InputSpec as Input
from paddleslim.dygraph import L1NormFilterPruner
class TestSensitivity(unittest.TestCase):
def __init__(self, methodName='runTest', pruner=None, param_names=[]):
super(TestSensitivity, self).__init__(methodName)
self._pruner = pruner
self._param_names = param_names
transform = T.Compose([T.Transpose(), T.Normalize([127.5], [127.5])])
self.train_dataset = paddle.vision.datasets.MNIST(
mode="train", backend="cv2", transform=transform)
self.val_dataset = paddle.vision.datasets.MNIST(
mode="test", backend="cv2", transform=transform)
def _reader():
for data in self.val_dataset:
yield data
self.val_reader = _reader
def runTest(self):
dygraph_sen, params = self.dygraph_sen()
static_sen = self.static_sen(params)
all_right = True
for _name, _value in dygraph_sen.items():
_losses = {}
for _ratio, _loss in static_sen[_name].items():
_losses[round(_ratio, 2)] = _loss
for _ratio, _loss in _value.items():
if not np.allclose(_losses[_ratio], _loss, atol=1e-2):
print(
f'static loss: {static_sen[_name][_ratio]}; dygraph loss: {_loss}'
)
all_right = False
self.assertTrue(all_right)
def dygraph_sen(self):
paddle.disable_static()
net = paddle.vision.models.LeNet()
optimizer = paddle.optimizer.Adam(
learning_rate=0.001, parameters=net.parameters())
inputs = [Input([None, 1, 28, 28], 'float32', name='image')]
labels = [Input([None, 1], 'int64', name='label')]
model = paddle.Model(net, inputs, labels)
model.prepare(
optimizer,
paddle.nn.CrossEntropyLoss(),
paddle.metric.Accuracy(topk=(1, 5)))
model.fit(self.train_dataset, epochs=1, batch_size=128, verbose=1)
result = model.evaluate(self.val_dataset, batch_size=128, verbose=1)
pruner = None
if self._pruner == 'l1norm':
pruner = L1NormFilterPruner(net, [1, 1, 28, 28])
elif self._pruner == 'fpgm':
pruner = FPGMFilterPruner(net, [1, 1, 28, 28])
def eval_fn():
result = model.evaluate(self.val_dataset, batch_size=128)
return result['acc_top1']
sen = pruner.sensitive(
eval_func=eval_fn,
sen_file="_".join(["./dygraph_sen_", str(time.time())]),
#sen_file="sen.pickle",
target_vars=self._param_names)
params = {}
for param in net.parameters():
params[param.name] = np.array(param.value().get_tensor())
print(f'dygraph sen: {sen}')
return sen, params
def static_sen(self, params):
paddle.enable_static()
main_program = fluid.Program()
startup_program = fluid.Program()
with fluid.unique_name.guard():
with fluid.program_guard(main_program, startup_program):
input = fluid.data(name="image", shape=[None, 1, 28, 28])
label = fluid.data(name="label", shape=[None, 1], dtype="int64")
model = paddle.vision.models.LeNet()
out = model(input)
acc_top1 = fluid.layers.accuracy(input=out, label=label, k=1)
eval_program = main_program.clone(for_test=True)
place = fluid.CUDAPlace(0)
scope = fluid.global_scope()
exe = fluid.Executor(place)
exe.run(startup_program)
val_reader = paddle.fluid.io.batch(self.val_reader, batch_size=128)
def eval_func(program):
feeder = fluid.DataFeeder(
feed_list=['image', 'label'], place=place, program=program)
acc_set = []
for data in val_reader():
acc_np = exe.run(program=program,
feed=feeder.feed(data),
fetch_list=[acc_top1])
acc_set.append(float(acc_np[0]))
acc_val_mean = np.array(acc_set).mean()
return acc_val_mean
for _name, _value in params.items():
t = scope.find_var(_name).get_tensor()
t.set(_value, place)
print(f"static base: {eval_func(eval_program)}")
criterion = None
if self._pruner == 'l1norm':
criterion = 'l1_norm'
elif self._pruner == 'fpgm':
criterion = 'geometry_median'
sen = sensitivity(
eval_program,
place,
self._param_names,
eval_func,
sensitivities_file="_".join(
["./sensitivities_file", str(time.time())]),
criterion=criterion)
return sen
def add_cases(suite):
suite.addTest(
TestSensitivity(
pruner="l1norm", param_names=["conv2d_0.w_0"]))
def load_tests(loader, standard_tests, pattern):
suite = unittest.TestSuite()
add_cases(suite)
return suite
if __name__ == '__main__':
unittest.main()
...@@ -45,8 +45,9 @@ class TestPrune(StaticCase): ...@@ -45,8 +45,9 @@ class TestPrune(StaticCase):
["conv1_weights", "conv2_weights", "conv3_weights"], main_program) ["conv1_weights", "conv2_weights", "conv3_weights"], main_program)
while [] in groups: while [] in groups:
groups.remove([]) groups.remove([])
print(groups)
self.assertTrue(len(groups) == 2) self.assertTrue(len(groups) == 2)
self.assertTrue(len(groups[0]) == 18) self.assertTrue(len(groups[0]) == 20)
self.assertTrue(len(groups[1]) == 6) self.assertTrue(len(groups[1]) == 6)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册