什么是PRelu算子?PRelu算子调优经历-先行篇
最近小编收到一个客户模型,其中使用了一个叫做PRelu的算子,想要运行在RT170上。本来小编是信心满满的答应客户说:速度上放心,我们这主频1GHz的CPU绝对没问题,包您满意。没想到跑分结果出炉直接给了小编沉重一击。
(资料图片)
直接依赖TFLm推理引擎的默认实现,PRelu算子的运行时间竟然高达188ms。于是小编本着工程师本有的探索精神,决定迎难而上,彻底将它优化一下。
所谓知己知彼,百战不殆,首先我们来看一下什么叫做PRelu算子。
PRelu,看着好像特别的高大上,我们将其拆分来看,将其分成P+Relu,是不是瞬间就觉得熟悉了。没错,他实际上就是我们常用的Relu算子的变种。其中,P,是Parametric的缩写,因此,所谓PRelu就是带参数的Relu,只不过,这里的参数实际上是可以被训练的,而非一个固定值。那么PRelu到底长什么样呢?小编马上揭开它的神秘面纱:
上图就是PRelu的庐山真面目。i 表示不同的通道。alpha的个数是跟着通道走的,不过好消息是,各个通道之间参数是可以共享的,这样看着清爽了不少。特殊的,如果我们配置H和W通道共享参数,那么参数alpha就变成了类似于bias的功能,逐通道共享一个参数,因此,其shape = (1, 1, c);
为了对客户负责,外加能够更加方便地进行模型测试,小编首先收利用Keras手动构建一个具有PRelu算子的小巧模型。正所谓小巧而又不失优雅,我们构建模型如下所示:
这个小巧的模型本身具备了我们所常见的多个算子,例如Conv2D,MaxPool2D,FullyConnect等,因此作为PRelu算子的测试模型也不至于显得过于寒酸。
接下来和大家聊聊小编的调试经历:
第一步,就是要对TFLm的源码进行分析,了解为何其运行缓慢。
PRelu算子实际上就是一个进阶版本的Relu算子,根据其输入值的正负分别进行计算,当输入为正是,就等于本身;当输入为负时,将结果乘以一个系数alpha。看似非常简单的计算方式,为啥TFLm的参考实现能算的这么慢呢?口说无凭,show me the code:
if (input_value >= 0) { output_value = MultiplyByQuantizedMultiplier( input_value, params.output_multiplier_1, params.output_shift_1); } else { auto alpha_index = SubscriptToIndex(desc2, b, y, x, c); const int32_t alpha_value = params.alpha_offset + alpha_data[alpha_index]; output_value = MultiplyByQuantizedMultiplier( input_value * alpha_value, params.output_multiplier_2, params.output_shift_2); }
看到这里,恰似风平浪静,的确是按照我们分析的那样,按照输入值的正负进行计算。但是...细心的读友可能发现了问题:
1) 这里的alpha_data有个index,没错,这就是运行慢的第一个原因。刚才说过,PRelu中的alpha参数是和通道数相关的,也就是说每个通道都可以拥有自己的值,而且各通道之间还可以共享,因此并不能直接顺序访问,而是需要根据index进行查找。
2) 这里多了一个叫做MultiplyByQuantizedMultiplier的函数,为啥不直接计算?这是另一个原因。熟悉深度学习的伙伴们一定知道,MCU平台常被称作:资源受限平台,这里的受限不仅体现在算力上,还有内存空间上,相较于MPU那种动辄几个G的DDR的庞然大物,MCU上的内存资源也是捉襟见肘。
想要将我们训练的模型部署运行在MCU上,第一步就是对模型进行量化操作,将浮点类型的模型转换为int8类型的模型,这样直接缩小到之前的1/4。同时由于量化后的模型采用int8表示,同时能够借助于优化后的运行库进行加速。
这样一来,为了既想要使用int8模型带来的遍历,又保证模型精度,就需要对输出结果进行反量化表示,已达到使用int8类型结果表达浮点数的效果。因此,就需要调用类似MultiplyByQuantizedMultiplier这样的函数进行反量化处理。
基于以上两点,我们也就发现了算法本身所存在的慢速隐患,接下来的优化操作也正是基于此展开。
下一篇会继续介绍通过内存外加反量化函数的改造提升算法执行速度。
标签: