[TFLite] 9(2). 학습 후 양자화

6 minute read

개요


학습 후 양자화는 학습된 모델을 TFLite 모델로 변환할 때 적용합니다. 모델이 학습할 때에는 그대로 32비트 부동 소수점을 사용하고, 학습이 끝난 뒤 이를 16비트 부동 소수점이나 8비트 고정 소수점까지 줄임으로써 모델의 크기와 추론 소요 시간을 단축하는 방식입니다.

학습 후 양자화를 수행하면 모델의 정확도가 다소 떨어질 수 밖에 없기 때문에 학습 후에 정확도가 얼마나 손실되었는지 확인해야 합니다.

학습 후 양자화의 유형에는 Dynamic 양자화, Integer 양자화, Float16 양자화가 있습니다.


학습 후 양자화의 유형

유형 정밀도 양자화 대상 특징
Dynamic Range 8비트(1/4) 가중치 활성화를 동적으로 양자화
Integer 8비트(1/4) 가중치+레이어 입출력 대표 데이터셋 필요
    가중치+레이어 입출력+모델 입출력 대표 데이터셋 필요, Int 전용 기기 호환
Float16 16비트(1/2) 가중치 GPU 호환성


학습 후 양자화의 유형 선택을 위한 의사결정 트리

image-20210824220149596


학습 후 양자화의 유형 별로 각 양자화를 적용한 모델을 생성하여 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의 순입니다.
  • 모델의 추론 소요 시간은 어떤 환경에서 추론을 진행하느냐에 따라 달라집니다.

Categories: ,

Updated:

Leave a comment