2021SC@SDUSC
模型量化简单来说就是用更低比特数据(如8位整型)代替原浮点数据(32位)。听上去似乎是非常简单,但是实际操作下来会发现这个坑远远比想象中的大。量化最核心的挑战,是如何在减少模型数据位宽的同时,模型的准确度不要掉下来,也就是在压缩率与准确率损失间作trade-off。这衍生出很多有趣的子问题,比如量化对象是谁(weight,activation,gradient),量化到几位(8位,4位,2位,1位),量化参数选哪些(如moving rate,clipping value),量化参数是否可以自动优化,不同层是否需要不同的量化参数,如何在量化后恢复准确率或者在训练时考虑量化,等等……
PACT量化(PArameterized Clipping acTivation)是一种新的量化方法,该方法通过在量化激活值之前去掉一些离群点,将模型量化带来的精度损失降到最低,甚至比原模型准确率更高。
量化的好处
量化是将以往用32bit或者64bit表达的浮点数用8bit、16bit甚至1bit、2bit等占用较少内存空间的形式进行存储。这样的好处是:
-
减少模型体积,降低模型存储空间需求;
-
降低内存带宽需求;
-
加速计算。现代移动端设备,基本都支持SIMD(单指令流多数据流),以128-bit
寄存器为例,单个指令可以同时运算4个32位单精度浮点,或者8个16 位整型,或者16个8位整型。显然8位整型数在 SIMD的加持下,运算速率要更快。
量化面临的最大问题——精度受损
量化映射方式
通常,模型量化是将32bit浮点数转换成8bit整数,映射过程分为三种方式:
- 非饱和方式:将浮点数正负绝对值的最大值对应映射到整数的最大最小值。
- 饱和方式:先计算浮点数的阈值,然后将浮点数的正负阈值对应映射到整数的最大最小值。
- 仿射方式:将浮点数的最大最小值对应映射到整数的最大最小值。
红色代表非饱和方式,黄色代表饱和方式,绿色代表仿射方式
量化精度损失原因
无论哪种映射方式,都会受到离群点、float参数分布不均匀的影响,造成量化损失增加。
如上图,由于左侧的离群点,使得量化的范围更大,让量化后的右侧数值点变的过度密集,增大了量化损失。
以实际模型为例,下图的分布直方图,取自经典的CNN模型MobileNetV3中的一部分激活值,可以看到它的分布十分不均匀,最小值和最大值分别是-60和+80,但是绝大多数数据都分布在-20到+20之间。
如果按照-60到+80的范围去确定量化scale,就会对分布密集的-20到+20范围带来较大的量化损失。
PACT量化原理
模型量化主要包括两个部分,一是对权重Weight量化,一是针对激活值Activation量化。同时对两部分进行量化,才能获得最大的计算效率收益。权重可以借助网络正则化等手段,让权重分布尽量紧凑,减少离群点、不均匀分布情况发生,而对于激活值还缺乏有效的手段。
PACT量化(PArameterized Clipping acTivation)是一种新的量化方法,该方法提出在量化激活值之前去掉一些离群点来降低模型量化带来的精度损失。提出方法的背景是作者发现:“在运用权重量化方案来量化activation时,激活值的量化结果和全精度结果相差较大”。作者发现,activation的量化可能引起的误差很大(相较于weight基本在 0到1范围内,activation的值的范围是无限大的,这是RELU的结果),所以提出截断式RELU 的激活函数。该截断的上界,即α 是可学习的参数,这保证了每层能够通过训练学习到不一样的量化范围,最大程度降低量化带来的舍入误差。
如上图,PACT解决问题的方法是,不断裁剪激活值范围,使得激活值分布收窄,从而降低量化映射损失。
PACT通过对激活数值做裁剪,从而减少激活分布中的离群点,使量化模型能够得到一个更合理的量化scale,降低量化损失。
PACT的公式如下所示:
可以看出PACT原始的思想是用PACT代替ReLU函数,对大于零的部分进行一个截断操作,截断阈值为a。
PaddleSlim改进版——PACT量化实现
实际应用场景中,各种模型结构并不一定都是用ReLU,为了使PACT方法能够得到更广泛的应用,就需要我们对其进行一定改进。改进后的PACT公式如下:
优势:飞桨推出的PaddleSlim提供了改进版的PACT方法,在激活值和待量化的OP(卷积,全连接等)之间插入这个PACT预处理,不只对大于0的分布进行截断,同时也对小于0的部分做同样的限制,从而更好地得到待量化的范围,降低量化损失。同时,截断阈值是一个可训练的参数,在量化训练过程中,模型会自动的找到一个合理的截断阈值,从而进一步降低量化精度损失。
OCR模型PACT量化收益
本次实验选取的是PaddleOCR主打的文本检测和识别以及方向分类模型。其中识别模型基于CRNN结构,原模型大小4.5M(包含参数文件和模型结构信息),识别准确率为66.49%; 检测模型基于DB结构,原模型大小2.5M, 原模型检测hmean评价指标为74.4;方向分类器基于MobileNetV3模型,原模型大小0.9M,分类准确率为94.03%。
我们可以用下图所示的流程对OCR模型进行量化压缩,本次我们使用的是PACT在线量化方案。
通过PACT在线量化,可以在不过分损伤模型准确率的前提下,降低模型的大小,提高预测性能。
量化后,识别模型大小从4.5M缩减到1.5M,识别准确率为65.33%,量化损失相对较小;检测模型大小从2.5M缩减到1.4M,检测hmean指标为74.9,PACT量化不仅没有降低模型精度反而带来了0.5%的提升;方向分类模型大小从0.9M缩减到0.45M,分类准确率为94.65%,量化同样带来了0.62%的精度提升。我们可以发现,合理使用量化方法,不仅不会对模型准确率带来过多损伤,甚至在一些场景下会带来一定的提升,同时可以享受模型体积缩小和预测速度提高带来的便利。
PaddleSlim改进版——PACT量化的实现步骤
PACT量化功能来自飞桨PaddleSlim模型压缩工具库。PaddleSlim是一个模型压缩工具库,不仅支持PACT量化,还包含模型剪裁、知识蒸馏、超参搜索和模型结构搜索等一系列模型压缩策略。
使用PaddleSlim对模型进行PACT量化操作只需要以下4步:
-
选择量化配置
-
转换量化模型
-
启动量化训练
-
保存量化模型
下面分别对这几点进行讲解:
1.选择量化配置
首先我们需要对本次量化的一些基本量化配置做一些选择,例如weight量化类型,activation量化类型等。如果没有特殊需求,可以直接拷贝我们默认的量化配置。全部可选的配置可以参考PaddleSlim量化文档,例如我们用的量化配置如下:
2. 转换量化模型
在确认好我们的量化配置以后,我们可以根据这个配置把我们定义好的一个普通模型转换为一个模拟量化模型。我们根据量化原理中介绍的PACT方法,定义好PACT函数pact和其对应的优化器pact_opt。在这之后就可以进行转换,转换的方式也很简单:
3. 启动量化训练
得到了量化模型后就可以启动量化训练了,量化训练与普通的浮点数模型训练并无区别,无需增加新的代码或逻辑,直接按照浮点数模型训练的流程进行即可。
4.保存量化模型
量化训练结束后,我们需要对量化模型做一个转化。PaddleSlim会对底层的一些量化OP顺序做调整,以便预测使用。转换及保存的基本流程如下所示:
OCR PACT量化实例
export_model.py(引入)
# Copyright (c) 2020 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 os
import sys
__dir__ = os.path.dirname(os.path.abspath(__file__))
sys.path.append(__dir__)
sys.path.append(os.path.abspath(os.path.join(__dir__, '..', '..', '..')))
sys.path.append(
os.path.abspath(os.path.join(__dir__, '..', '..', '..', 'tools')))
import argparse
import paddle
from paddle.jit import to_static
from ppocr.modeling.architectures import build_model
from ppocr.postprocess import build_post_process
from ppocr.utils.save_load import init_model
from ppocr.utils.logging import get_logger
from tools.program import load_config, merge_config, ArgsParser
from ppocr.metrics import build_metric
import tools.program as program
from paddleslim.dygraph.quant import QAT
from ppocr.data import build_dataloader
def export_single_model(quanter, model, infer_shape, save_path, logger):
quanter.save_quantized_model(
model,
save_path,
input_spec=[
paddle.static.InputSpec(
shape=[None] + infer_shape, dtype='float32')
])
logger.info('inference QAT model is saved to {}'.format(save_path))
def main():
############################################################################################################
# 1. quantization configs
############################################################################################################
quant_config = {
# weight preprocess type, default is None and no preprocessing is performed.
'weight_preprocess_type': None,
# activation preprocess type, default is None and no preprocessing is performed.
'activation_preprocess_type': None,
# weight quantize type, default is 'channel_wise_abs_max'
'weight_quantize_type': 'channel_wise_abs_max',
# activation quantize type, default is 'moving_average_abs_max'
'activation_quantize_type': 'moving_average_abs_max',
# weight quantize bit num, default is 8
'weight_bits': 8,
# activation quantize bit num, default is 8
'activation_bits': 8,
# data type after quantization, such as 'uint8', 'int8', etc. default is 'int8'
'dtype': 'int8',
# window size for 'range_abs_max' quantization. default is 10000
'window_size': 10000,
# The decay coefficient of moving average, default is 0.9
'moving_rate': 0.9,
# for dygraph quantization, layers of type in quantizable_layer_type will be quantized
'quantizable_layer_type': ['Conv2D', 'Linear'],
}
FLAGS = ArgsParser().parse_args()
config = load_config(FLAGS.config)
merge_config(FLAGS.opt)
logger = get_logger()
# build post process
post_process_class = build_post_process(config['PostProcess'],
config['Global'])
# build model
# for rec algorithm
if hasattr(post_process_class, 'character'):
char_num = len(getattr(post_process_class, 'character'))
if config['Architecture']["algorithm"] in ["Distillation",
]: # distillation model
for key in config['Architecture']["Models"]:
config['Architecture']["Models"][key]["Head"][
'out_channels'] = char_num
else: # base rec model
config['Architecture']["Head"]['out_channels'] = char_num
model = build_model(config['Architecture'])
# get QAT model
quanter = QAT(config=quant_config)
quanter.quantize(model)
init_model(config, model)
model.eval()
# build metric
eval_class = build_metric(config['Metric'])
# build dataloader
valid_dataloader = build_dataloader(config, 'Eval', device, logger)
use_srn = config['Architecture']['algorithm'] == "SRN"
model_type = config['Architecture']['model_type']
# start eval
metric = program.eval(model, valid_dataloader, post_process_class,
eval_class, model_type, use_srn)
logger.info('metric eval ***************')
for k, v in metric.items():
logger.info('{}:{}'.format(k, v))
infer_shape = [3, 32, 100] if config['Architecture'][
'model_type'] != "det" else [3, 640, 640]
save_path = config["Global"]["save_inference_dir"]
arch_config = config["Architecture"]
if arch_config["algorithm"] in ["Distillation", ]: # distillation model
for idx, name in enumerate(model.model_name_list):
sub_model_save_path = os.path.join(save_path, name, "inference")
export_single_model(quanter, model.model_list[idx], infer_shape,
sub_model_save_path, logger)
else:
save_path = os.path.join(save_path, "inference")
export_single_model(quanter, model, infer_shape, save_path, logger)
if __name__ == "__main__":
config, device, logger, vdl_writer = program.preprocess()
main()
quant.py(量化)
# Copyright (c) 2020 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import sys
__dir__ = os.path.dirname(os.path.abspath(__file__))
sys.path.append(__dir__)
sys.path.append(os.path.abspath(os.path.join(__dir__, '..', '..', '..')))
sys.path.append(
os.path.abspath(os.path.join(__dir__, '..', '..', '..', 'tools')))
import yaml
import paddle
import paddle.distributed as dist
paddle.seed(2)
from ppocr.data import build_dataloader
from ppocr.modeling.architectures import build_model
from ppocr.losses import build_loss
from ppocr.optimizer import build_optimizer
from ppocr.postprocess import build_post_process
from ppocr.metrics import build_metric
from ppocr.utils.save_load import init_model
import tools.program as program
from paddleslim.dygraph.quant import QAT
dist.get_world_size()
class PACT(paddle.nn.Layer):
def __init__(self):
super(PACT, self).__init__()
alpha_attr = paddle.ParamAttr(
name=self.full_name() + ".pact",
initializer=paddle.nn.initializer.Constant(value=20),
learning_rate=1.0,
regularizer=paddle.regularizer.L2Decay(2e-5))
self.alpha = self.create_parameter(
shape=[1], attr=alpha_attr, dtype='float32')
def forward(self, x):
out_left = paddle.nn.functional.relu(x - self.alpha)
out_right = paddle.nn.functional.relu(-self.alpha - x)
x = x - out_left + out_right
return x
quant_config = {
# weight preprocess type, default is None and no preprocessing is performed.
'weight_preprocess_type': None,
# activation preprocess type, default is None and no preprocessing is performed.
'activation_preprocess_type': None,
# weight quantize type, default is 'channel_wise_abs_max'
'weight_quantize_type': 'channel_wise_abs_max',
# activation quantize type, default is 'moving_average_abs_max'
'activation_quantize_type': 'moving_average_abs_max',
# weight quantize bit num, default is 8
'weight_bits': 8,
# activation quantize bit num, default is 8
'activation_bits': 8,
# data type after quantization, such as 'uint8', 'int8', etc. default is 'int8'
'dtype': 'int8',
# window size for 'range_abs_max' quantization. default is 10000
'window_size': 10000,
# The decay coefficient of moving average, default is 0.9
'moving_rate': 0.9,
# for dygraph quantization, layers of type in quantizable_layer_type will be quantized
'quantizable_layer_type': ['Conv2D', 'Linear'],
}
def main(config, device, logger, vdl_writer):
# init dist environment
if config['Global']['distributed']:
dist.init_parallel_env()
global_config = config['Global']
# build dataloader
train_dataloader = build_dataloader(config, 'Train', device, logger)
if config['Eval']:
valid_dataloader = build_dataloader(config, 'Eval', device, logger)
else:
valid_dataloader = None
# build post process
post_process_class = build_post_process(config['PostProcess'],
global_config)
# build model
# for rec algorithm
if hasattr(post_process_class, 'character'):
char_num = len(getattr(post_process_class, 'character'))
if config['Architecture']["algorithm"] in ["Distillation",
]: # distillation model
for key in config['Architecture']["Models"]:
config['Architecture']["Models"][key]["Head"][
'out_channels'] = char_num
else: # base rec model
config['Architecture']["Head"]['out_channels'] = char_num
model = build_model(config['Architecture'])
quanter = QAT(config=quant_config, act_preprocess=PACT)
quanter.quantize(model)
if config['Global']['distributed']:
model = paddle.DataParallel(model)
# build loss
loss_class = build_loss(config['Loss'])
# build optim
optimizer, lr_scheduler = build_optimizer(
config['Optimizer'],
epochs=config['Global']['epoch_num'],
step_each_epoch=len(train_dataloader),
parameters=model.parameters())
# build metric
eval_class = build_metric(config['Metric'])
# load pretrain model
pre_best_model_dict = init_model(config, model, logger, optimizer)
logger.info('train dataloader has {} iters, valid dataloader has {} iters'.
format(len(train_dataloader), len(valid_dataloader)))
# start train
program.train(config, train_dataloader, valid_dataloader, device, model,
loss_class, optimizer, lr_scheduler, post_process_class,
eval_class, pre_best_model_dict, logger, vdl_writer)
if __name__ == '__main__':
config, device, logger, vdl_writer = program.preprocess(is_train=True)
main(config, device, logger, vdl_writer)