[TFLite] 9(2). 학습 후 양자화
개요
학습 후 양자화는 학습된 모델을 TFLite 모델로 변환할 때 적용합니다. 모델이 학습할 때에는 그대로 32비트 부동 소수점을 사용하고, 학습이 끝난 뒤 이를 16비트 부동 소수점이나 8비트 고정 소수점까지 줄임으로써 모델의 크기와 추론 소요 시간을 단축하는 방식입니다.
학습 후 양자화를 수행하면 모델의 정확도가 다소 떨어질 수 밖에 없기 때문에 학습 후에 정확도가 얼마나 손실되었는지 확인해야 합니다.
학습 후 양자화의 유형에는 Dynamic 양자화, Integer 양자화, Float16 양자화가 있습니다.
학습 후 양자화의 유형
유형 | 정밀도 | 양자화 대상 | 특징 |
---|---|---|---|
Dynamic Range | 8비트(1/4) | 가중치 | 활성화를 동적으로 양자화 |
Integer | 8비트(1/4) | 가중치+레이어 입출력 | 대표 데이터셋 필요 |
가중치+레이어 입출력+모델 입출력 | 대표 데이터셋 필요, Int 전용 기기 호환 | ||
Float16 | 16비트(1/2) | 가중치 | GPU 호환성 |
학습 후 양자화의 유형 선택을 위한 의사결정 트리
학습 후 양자화의 유형 별로 각 양자화를 적용한 모델을 생성하여 1. 각각의 모델 크기, 2. 정확도, 3. 추론 소요 시간을 비교해보겠습니다.
모델 크기는 모델을 변환하면서 바로 비교하고, 정확도는 모델을 모두 변환한 뒤 확인하며, 추론 소요 시간은 모델을 안드로이드 기기에 배포한 뒤 앱에서 측정할 것입니다.
양자화 모델의 크기 비교
먼저 양자화를 적용하지 않은 MobileNetV2 모델을 tflite 파일로 변환한 뒤 양자화를 적용한 모델과 비교해봅니다.
양자화 없이 tflite 파일로 변환
def save_model_tflite(model, path):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open(path, 'wb') as f:
f.write(tflite_model)
return os.path.getsize(path)
이 함수를 이용하여 MoblieNetV2 모델을 다음과 같이 변환합니다.
MoblieNetV2 모델을 tflite 파일로 변환한 후 용량 확인
mobilenet_model = tf.keras.applications.MobileNetV2(weights="imagenet")
origin_size = save_model_tflite(mobilenet_model, "mobilenet.tflite")
print(f'origin_size : {origin_size}')
out:
origin_size : 13989548
양자화를 적용하지 않은 MobileNetV2의 크기는 약 13메가바이트입니다. 이제 각 양자화 기법을 적용한 모델을 생성하면서 양자화를 적용하지 않은 모델과 비교해봅시다.
Dynamic Range 양자화
Dynavic Range 양자화는 학습이 완료된 모델을 변환할 때 32비트 부동 소수점인 가중치를 8비트의 정밀도로 정적으로 양자화합니다. 따라서 모델의 크기가 1/4 정도로 줄어듭니다.
추론할 때에는 8비트 값을 다시 32비트 부동 소수점으로 변환하고 부동 소수점 커널을 사용하여 계산합니다.
만약 수행할 연산이 양자화된 커널을 지원한다면 활성화는 계산 전에 동적으로 8비트 정수형으로 변환한 후 양자화된 커널을 사용하여 계산하고, 계산이 끝나면 다시 32비트로 역양자화de-quantization합니다. 성능을 위해 한 번 변환한 값을 캐싱하여 재사용합니다.
케라스 모델을 Dynamic Range 양자화하여 저장
def save_model_tflite_dynamic_range_optimization(model, path):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT] # Dynamic Range 양자화
tflite_model = converter.convert()
with open(path, 'wb') as f:
f.write(tflite_model)
return os.path.getsize(path)
converter를 생성한 뒤 converter.optimization에 옵션 값을 설정하는 방식으로 양자화를 적용합니다. 옵션 값은 tf.lite.Optimize.DEFAULT를 사용합니다.
Dynamic Range 양자화는 기본으로 동작하는 양자화 기법이라 추가적인 옵션이 필요하지 않습니다.
MobileNetV2 모델에 Dynamic Range 양자화 적용 후 tflite 파일 용량 확인
mobilenet_model = tf.keras.applications.MobileNetV2(weights="imagenet")
dr_size = save_model_tflite_dynamic_range_optimization(mobilenet_model,
"mobilenet_dynamic_range.tflite")
print(f'dr_size : {dr_size}')
dr_size : 3927728
Dynamic Range 양자화가 적용된 모델의 크기는 기본 모델 크기의 1/4 수준인 약 3.9메가 바이트입니다.
Float16 양자화
Float16 양자화는 모델의 가중치를 32비트 부동 소수점에서 16비트 부동 소수점 값으로 변환하므로 모델의 크기가 1/2로 줄어듭니다.
Float16으로 양자화된 모델은 CPU와 GPU를 이용하여 연산할 수 있습니다.
GPU는 Float16 값을 변환 없이 바로 처리할 수 있으며, Float32 값을 계산할 때보다 연산속도도 빠릅니다. 또한 병렬 처리도 가능하기 때문에 GPU 위임을 이용하여 Float16 으로 양자화된 모델을 실행하면 추론 성능이 개선됩니다.
CPU를 이용하여 Float16으로 양자화된 모델을 실행하면 첫 번째 추론 전에 Float32 값으로 업샘플링되어 계산됩니다. 따라서 모델의 정확도와 추론 성능의 영향을 최소화하면서 모델의 크기를 줄일 수 있습니다.
케라스 모델을 Float16 양자화하여 저장
def save_model_tflite_float16_optimization(model, path):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16]
tflite_model = converter.convert()
with open(path, 'wb') as f:
f.write(tflite_model)
return os.path.getsize(path)
converter의 optimizations는 Dynamic Range 양자화와 동일하게 tf.lite.Optimiza.DEFAULT로 설정하고, 추가로 target_spec.supported_types 값을 [tf.float16]으로 설정합니다.
target_spec은 타깃 디바이스의 세부 사항을 다루는 targetSpec 클래스로, TFLiteConverter는 여기에 명시된 디바이스를 대상으로 모델을 생성합니다.
TargetSpec 클래스는 타깃 디바이스에서 지원하는 연산 집합인 supported_ops와 타깃 디바이스의 타입 목록인 supported_types를 가지고 있습니다. supported_types의 기본값은 tf.float32인데, 이를 tf.float16으로 변경해 Float16 양자화를 적용하는 것입니다.
MobileNetV2 모델에 Float16 양자화 적용 후 tflite 모델 용량 확인
mobilenet_model = tf.keras.applications.MobileNetV2(weights="imagenet")
fl16_size = save_model_tflite_float16_optimization(mobilenet_model,
"mobilenet_float16.tflite")
print(f'fl16_size : {fl16_size}')
fl16_size : 7031728
Float16 양자화가 적용된 모델의 크기는 기본 모델의 1/2 수준인 7메가 바이트입니다.
Integer 양자화
Integer 양자화는 모델의 가중치, 중간 레이어의 입출력 값, 모델의 입출력 값을 32비트 부동 소수점에서 8비트 고정 소수점으로 변환하는 양자화 기법입니다.
따라서 모델의 크기가 1/4로 줄어들고 추론에 소요되는 시간도 줄어들며, int8 형만 지원하는 저전력 디바이스에서도 사용이 가능합니다.
Interger 양자화는 모델의 가중치와 중간 레이어의 입출력 값까지만 양자화하거나, 여기에 더하여 모델의 입출력 값까지 모두 양자화하도록 선택할 수 있습니다.
전자를 Interger 양자화, 후자를 Full Integer 양자화라고 하는데, 각 양자화를 적용하여 저장하는 함수를 구현하겠습니다.
케라스 모델을 Integer 양자화하여 저장
def save_model_tflite_int_optimization(model, path, representative_dataset):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
tflite_model = converter.convert()
with open(path, 'wb') as f:
f.write(tflite_model)
return os.path.getsize(path)
Integer 양자화를 적용하기 위해 converter에 optimizations와 representative_dataste을 설정했습니다. representative_dataset은 대표 데이터셋을 생성하는 제너레이터로 아직 구현하지 않았습니다.
Integer 양자화를 적용하려면 사전에 대표 데이터셋을 생성할 수 있는 제너레이터가 필요합니다. 대표 데이터셋이 전달되지 않으면 가중치만 양자화되고 활성화는 양자화할 수 없습니다. TFLiteConverter는 대표 데이터셋을 이용해 모델의 입출력 샘플을 생성하여 최적화를 평가하는 데 사용합니다.
대표 데이터셋을 전달하기 위해 비교적 용량이 작은 ImageNet의 검증 데이터를 다운로드하여 사용합니다.
https://academictorrents.com/collection/imagenet-2012
다운로드한 ImageNet 검증데이터를 프로젝트의 ILSVRC2012_img_val 폴더에 넣고, 이를 활용하여 대표 데이터셋을 생성하는 제너레이터를 작성합니다.
ImageNet 검증 데이터에서 대표 데이터 생성
# 이미지 데이터를 불러와 224x224 로 resize
def get_preprocessed_test_image(image_dir, count=100):
files = os.listdir(image_dir)
resized_images = np.array(np.zeros((count, 224, 224, 3)))
for i in range(count):
file = files[i]
path = os.path.join(image_dir, file)
image = np.array(Image.open(path))
if len(np.shape(image)) == 2: # 흑백 이미지의 경우
image = convert_channel(image) # 컬러 이미지처럼 변환(1채널 -> 3채널)
resized_images[i] = tf.image.resize(image, [224,224])
return resized_images
# 1채널 흑백 이미지를 3채널 컬러 이미지처럼 변환
def convert_channel(img):
return np.repeat(img[:, :, np.newaxis], 3, axis=2)
image_count = 100
image_data = get_preprocessed_test_image("./ILSVRC2012_img_val/", image_count)
# 모델에 맞게 이미지 전처리
image_data = np.array(tf.keras.applications.mobilenet.preprocess_input(image_data),
np.float32)
# 호출될 때마다 준비된 이미지에서 이미지를 하나씩 생성하여 총 100번 반환
def representative_dataset():
for input_value in tf.data.Dataset.from_tensor_slices(image_data).batch(1).take(image_count):
yield [input_value]
이제 Integer 양자화된 TFLite 모델을 만들고 크기를 확인합니다.
MobileNetV2 모델에 Integer 양자화 적용 후 tflite 파일 용량 확인
mobilenet_model = tf.keras.applications.MobileNetV2(weights="imagenet")
int_size = save_model_tflite_float16_optimization(mobilenet_model, "mobilenet_int.tflite",
representative_dataset)
print(f'int_size : {int_size}')
Integer 양자화가 적용된 모델의 크기는 기본 모델의 약 1/3 ~ 1/4 인 약 4.2메가 바이트입니다.
다음으로 모델의 입출력 값까지 모두 양자화하는 Full Integer 양자화를 살펴봅시다.
케라스 모델을 Full Integer 양자화하여 저장
def save_model_tflite_fullint_optimization(model, path, representative_dataset):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.unit8
converter.inference_output_type = tf.unit8
tflite_model = converter.convert()
with open(path, 'wb') as f:
f.write(tflite_model)
return os.path.getsize(path)
Full Integer 양자화를 위해 target_spec.supported_ops를 tf.lite.OpsSet.TFLITE_BUILTINS_INT8로 설정하고 inference_input_type과 inference_output_type을 모두 tf.unit8로 설정합니다.
앞서 언급했듯이 supported_ops는 지원되는 연산을 지칭하는 값으로, 기본 값은 텐서플로 라이트에 포함된 기본 연산을 나타내는 tf.lite.OpsSet.TFLITE_BUILTINS입니다.
이를 tf.lite.OpsSet.TFLITE_BUILTINS_INT8로 변경하면 int8로 양자화된 연산만을 사용하도록 모델이 변환됩니다. 만약 int8로 양자화된 구현이 없는 연산이 모델에 포함되어 있다면 오류가 발생합니다.
inference_input_type과 inference_output_type은 각각 입력 배열의 데이터 타입, 출력 배열의 데이터 타입입니다.
둘 다 기본값은 tf.float32인데 Full Integer 양자화에서는 이 값을 모두 tf.unit8로 설정합니다.
또한 Integer 양자화와 마찬가지로 Full Integer 양자화도 대표 데이터셋이 필요하므로 Integer 양자화에서 사용했던 representative_dataset() 함수를 그대로 사용합니다.
이제 Full Integer 양자화된 TFLite 모델을 만들고 크기를 확인합니다.
MobileNetV2 모델에 Full Integer 양자화 적용 후 tflite 파일 용량 확인
mobilenet_model = tf.keras.applications.MobileNetV2(weights="imagenet")
int_size = save_model_tflite_fullint_optimization(mobilenet_model, "mobilenet_fullint.tflite",
representative_dataset)
print(f'fullint_size : {fullint_size}')
Full Integer 양자화가 적용된 모델의 크기는 Integer 양자화와 거의 동일한 4.2 메가 바이트입니다. 기본 모델의 약 1/3 ~ 1/4 수준입니다.
양자화 모델의 정확도 비교
양자화 | 양자화 미적용 | Dynamic Range | Float16 | Integer | Full Integer |
---|---|---|---|---|---|
정확도 | 89% | 71% | 89% | 83% | 84% |
양자화 모델의 추론 소요 시간 비교
모델 | CPU의 스레드 1개 | CPU의 스레드 4개 | GPU | NNAPI |
---|---|---|---|---|
양자화 미적용 | 86ms | 39ms | 18ms | 25ms |
Dynamic Range | 88ms | 50ms | 88ms | 26ms |
Float 16 | 85ms | 37ms | 19ms | 381ms |
Integer | 65ms | 33ms | 91ms | 26ms |
Full Integer | 64ms | 31ms | 88ms | 23ms |
정리
- Dynamic 양자화
- 모델의 가중치의 범위를 변환합니다.
- 32비트 부동 소수점을 8비트 고정 소수점으로 변환하여 모델의 크기를 약 1/4로 줄입니다.
- Float16 양자화
- 모델의 가중치의 범위를 변환합니다.
- 32비트 부동 소수점을 16비트 부동 소수점으로 변환하여 모델의 크기를 약 1/2로 줄입니다.
- Integer 양자화
- 모델의 가중치의 범위와 중간 레이어의 입출력 값의 범위를 변환합니다.
- 32비트 부동 소수점을 8비트 고정 소수점으로 변환하여 모델의 크기를 약 1/4로 줄입니다.
- FullInteger 양자화
- 모델의 가중치의 범위와 중간 레이어, 모델의 입출력 값의 범위를 변환합니다.
- 32비트 부동 소수점을 8비트 고정 소수점으로 변환하여 모델의 크기를 약 1/4로 줄입니다.
- 모델의 크기는
양자화 미적용 > Float 16 > Integer, Full Integer > Dynamic Range
의 순입니다. - 모델의 정확도는
양자화 미적용 > Float 16 > Integer, Full Integer > Dynamic Range
의 순입니다. - 모델의 추론 소요 시간은 어떤 환경에서 추론을 진행하느냐에 따라 달라집니다.
Leave a comment