OpenVINO 모델을 optimization하기 위한 방법으로 Quantization에 대해 설명드립니다.
1. Quantization이란?
기존 Torch, ONNX model의 parameters(i.e. weights, bias)들은 각각이 float32로 표현되어 있습니다. Quantization은 float32의 data를 그 보다 낮은 bit(e.g. float16, int8)로 표현시켜 경량화시킵니다. 이렇게 함으로써 (1) inference time, (2) model size를 줄일 수 있다는 장점을 가집니다. 단점으로는 data의 정보손실이 발생하므로 Accuracy, mAP는 떨어지게 됩니다.
그럼, 다음과 같은 2가지 궁금증이 생기실 겁니다. 질문과 함께 답변드려볼게요.
- 몇 bit로 줄일 거냐?
- 기본적으로 TFLite에서 제공하는 타입 및 bit수는 float16(16bit), int8(8bit), uint8(8bit).
- 어떻게 줄일 거냐?
- float32로 표현된 parameters들을 줄이고자 하는 bit에 맞춰 mapping시킴.
2번에 대해 좀더 설명하기 위해 아래 uint8로 Quantization하는 예시를 보여드립니다.
오늘은 OpenVINO Quantization의 2가지 방법에 대해 설명드리겠습니다. 1번째는 FP16 Quantization, 2번째 Post-training Quantization입니다.
2. 환경 설정
OpenVINO모델을 optimization하는 방법을 설명드리기 위해서 사용한 model, device, package 정보는 다음과 같습니다.
- Packages
- openvino: 2022.1.0
- openvino-dev: 2022.1.0
- CPU: Intel(R) Xeon(R) Gold 5120 CPU @ 2.20GHz (가상 core수: 56)
- Model: yolov7 OpenVINO model
- yolov7.xml
- yolov7.bin
3. FP16 Quantization
원래 FP32로 표현되던 weight를 FP16으로 변경하는 것은 ONNX모델을 OpenVINO로 converting하는 CLI 명령어의 parameter로 줄 수 있습니다.
mo --input_model ${onnx_path} --output_dir ${output_dir} --data_type ${d_type}
# Ex) mo --input_model yolov7.onxx --output_dir yolov7_openvino_fp16 --data_type FP16
위와 같이 출력된다면 정상적으로 변환된것입니다. 여기서 실제로 inference와 model size가 줄어드는 지 확인하기 위해 Torch(FP32), OpenVINO(FP32)와 비교해보았습니다. Inference time은 총 50번 실행에 대해 평균을 내었습니다. 성능 측정코드는 이전 글을 참조바랍니다.
Model (data_type) | File size(MB) | Inference time (s) |
Yolov7 Torch (FP32) | 147.7 | 1.093 |
Yolov7 OpenVINO (FP32) | 148.1 | 0.118 |
Yolov7 OpenVINO (FP16) | 74.4 | 0.116 |
FP16으로 data type이 변경되면서 당연하게도 model size는 50% 정도로 줄어들었지만 Inference time은 거의 비슷함을 확인가능합니다. (Inference time도 줄어들었으면 좋앗을 걸..)
4. Post-Training Quantization
Post-Training Quantization(PTQ)는 말 그대로 Training이 끝난 모델에 대해 Quantization하겠다는 말입니다. OpenVINo의 PTQ는 int8 Quantization이 가능하므로 inference time과 model size모두 줄일 수 있습니다.
위는 PTQ process를 보여주는 그림으로 Quantization을 하고자 하는 모델을 training하는 데 사용된 dataset을 필요로 하는 것을 알 수 있습니다. (Label은 필요하지 않음.) 또한 PTQ를 하기위해서는 유저가 직접 DataLoader에 대한 구현이 필요합니다.
그렇다면 왜 PTQ을 하는데 dataset을 필요로 할까요?
Int8 Quantization은 float32인 data size를 int8로 줄이는 작업이기 때문에 정보의 손실이 상대적으로 크겠죠. 그만큼 Quantization을 잘해야 기존의 Accuracy(or mAP) 성능이 떨어지지 않겠죠. 그러기 위해서는 각각의 weight에 대해 적절한 rmin/rmax (Quantization mapping range)를 선택하는 것이 중요하게 됩니다. 그래서 실제 input data들을 model에 흘려보내면서 Accuracy(or mAP)성능을 떨어트리지 않는 적절한 rmin/rmax를 찾기위해 dataset이 필요하게 됩니다.
전체적인 PTQ process flow는 다음과 같습니다.
- Data와 dataset interface를 준비
- Quantization parameter를 설정
- Quantization process 실행
4.1 Data and Dataset Interface 준비
제가 사용한 yolov7 모델은 COCO dataset을 사용했으므로 COCO training data를 준비하였습니다. 그리고 openvino.tools.pot.DataLoader
interface를 통해 dataloader를 구성하여야합니다. 그리고 해당 DataLoader
class에서 구현해야 할 함수는 다음과 같습니다.
__len__()
: dataset의 크기를 return__getitem__()
: index에 의해 data에 access해야하는 데 model-specific한 preprocessing후에(data, annotation)
을 returndata
: numpy.array이거나 dictionary형태여야 함annotation
: quantization에 사용되지 않으므로 None값을 줌
import os
import numpy as np
import cv2 as cv
from openvino.tools.pot import DataLoader
class ImageLoader(DataLoader):
def __init__(self, dataset_path):
# folder로 부터 image files 이름 가져오기
self._files = []
all_files_in_dir = os.listdir(dataset_path)
for name in all_files_in_dir:
file = os.path.join(dataset_path, name)
if cv.haveImageReader(file):
self._files.append(file)
# model input의 shape정의
self._shape = (640, 640)
def __len__(self):
""" dataset의 총 image file 개수 return """
return len(self._files)
def __getitem__(self, index):
"""
index에 의해 image data return (NCHW shape)
"""
if index >= len(self):
raise IndexError("Index out of dataset size")
image = cv.imread(self._files[index]) # read image with OpenCV
image = cv.resize(image, self._shape) # resize to a target input size
image = np.expand_dims(image, 0) # add batch dimension
image = image.astype(np.float32) # input data type to float32
image /= 255. # normalize
image = image.transpose(0, 3, 1, 2) # convert to NCHW layout
return image, None # annotation is set to None
data_loader = ImageLoader(${coco_dataset_path})
# Ex) data_loader = ImageLoader("/usr/src/app/datasets/coco/images/val2017/")
위와 같이 COCO dataset용으로 DataLoader를 구성하였습니다. 마지막 줄 예시에서도 보이듯이 저는 COCO validation set을 load하였습니다.
4.2 Quantization Parameter 설정
PTQ를 진행하기 위한 parameter설정에 코드입니다.
q_params = [{
"name": "DefaultQuantization",
"params": {
"target_device": "CPU",
"preset": "performance",
"stat_subset_size": 1000},
}]
"name": "DefaultQuantization"
DefaultQuantization
은 PTQ의 가장 기본적인 방법이며 fast하며 accurate한 결과를 제공한다고 함
"target_device": "CPU"
- target device에 대한 명시이며 다른 옵션으로는
"GPU"
,"ANY"
이 가능함
- target device에 대한 명시이며 다른 옵션으로는
"preset": "performance"
preset
은 quantization mode로performance
값은 weight와 activation모두 symmetric quantization을 하며 모든 HW에 성능이 가장 우수함
"stat_subset_size": 300
- 위의 입력한 dataset path(validation set)에서 얼마만큼의 data를 사용할 건지 명시
- 300이라는 값이 OpenVINO에서 실험해보았을 때 가장 최적의 값이었다고 함
4.3 Quantization Process 실행
from openvino.tools.pot import IEEngine
from openvino.tools.pot import load_model, save_model
from openvino.tools.pot import compress_model_weights
from openvino.tools.pot import create_pipeline
model_config = {
"model_name": "yolov7",
"model": "/usr/src/app/yolov5_inference/yolov7_openvino_fp32/yolov7.xml",
"weights": "/usr/src/app/yolov5_inference/yolov7_openvino_fp32/yolov7.bin",
}
engine_config = {"device": "CPU"}
# Step 1: Load model
model = load_model(model_config=model_config)
# Step 2: Device, data loader config와 함께 engine을 초기화
engine = IEEngine(config=engine_config, data_loader=data_loader)
# Step 3: PTQ parameter와 함께 pipeline생성 및 실행
pipeline = create_pipeline(q_params, engine)
compressed_model = pipeline.run(model=model)
# Step 4 (Optional): .bin file size를 줄이기 위해 model weight를 compress함
compress_model_weights(compressed_model)
# Step 5: save_path에 model_name이름으로 PTQ진행한 model 저장
compressed_model_paths = save_model(
model=compressed_model,
save_path="yolov7_openvino_ptq",
model_name="optimized_yolov7",
)
load_model
: PTQ를 진행하고자 하는 model을 load함lEEngine
: PTQ에 필요한 device정보와 DataLoader를 입력하여 PTQ engine초기화create_pipeline
: 위에서 설정한 PTQ parameter를 입력으로 pipeline생성pipeline.run
: PTQ를 실행!save_model
: PTQ가 완료된 모델을 저장
위의 코드를 실행하여 PTQ가 완료되면 아래와 같이 정상적으로 int8로 quantization된 OpenVINO 모델이 생성된다.
model size면에서는 FP32모델(148MB)에 비해 거의 2배줄은 것을 위로부터 확인가능합니다. 50번의 inference에 대해 평균을 내어 inference time을 측정하였습니다. 그리하여 기존 모델들과 비교했을 때 아래와 같이 PTQ를 사용하여 생성된 int8 quantized모델이 뛰어난 성능을 보임을 알 수 있습니다.
Model (data_type) | File size(MB) | Inference time (s) |
Yolov7 Torch (FP32) | 147.7 | 1.093 |
Yolov7 OpenVINO (FP32) | 148.1 | 0.118 |
Yolov7 OpenVINO (FP16) | 74.4 | 0.116 |
Yolov7 OpenVINO (int8) | 38.0 | 0.073 |
추가적으로 int8로 quantized되었다면 mAP성능이 하락할 수도 있는데 해당 문제가 있는 지 확인하기위해 Yolov7 OpenVINO (FP32)모델의 detection결과와 비교를 해보았습니다. (detection에 사용한 코드와 모델은 Appendix에서 확인가능합니다.)
INT8로 quantization된 모델은 FP32의 모델의 detection결과에서 사람중에 하나를(빨간색 박스) detection하지 못하는 것을 볼 수 있다. INT8로 quantization하여 mAP성능이 조금 떨어짐을 볼 수 있다. 좀 더 정확하게 bbox의 좌표와 confidence를 비교해보겠습니다.
위의 결과를 보시면 INT8로 quantization되면서 조금씩 bbox 좌표와 confidence가 달라짐을 볼 수 있고 사람 하나를 detection못하였기 때문에 Detect 9에 대한 정보가 없음을 알 수 있다. 특징점은 FP32모델에서 confidence가 작은 Detect 9가 INT8에서 없어진 것을 보면 quantization을 통한 정보손실은 decision boundary근처에서 많이 일어나는 것을 추측가능하다.
Appendix
FP16, INT8로 quantization된 모델은 여기서 다운가능하고 detection에 사용한 코드와 파일은 여기에서 받으세요.
'AI Engineering > OpenVINO' 카테고리의 다른 글
OpenVINO 뽀개기 (2) OpenVINO Inference (0) | 2022.08.16 |
---|---|
OpenVINO 뽀개기 (1) OpenVINO 이해 및 변환 (0) | 2022.08.14 |