エッジAI

量子化

モデルを軽く・速くする数値圧縮技術。

概要

量子化(Quantization)とは、ニューラルネットワークモデルのパラメータ(重みや活性化値)を、高精度な浮動小数点数(FP32)から低ビット幅の整数型(INT8、INT4など)に変換することで、モデルのサイズを小さくし、推論速度を高め、消費電力を削減する最適化技術です。

量子化は組み込み・エッジAIにおける最重要技術の一つであり、以下の恩恵をもたらします。

効果FP32→INT8変換の場合
モデルサイズ削減約1/4(4倍小さく)
推論速度向上2〜4倍高速化(CPU)、最大8倍(NPU)
メモリ帯域削減約1/4(キャッシュ効率向上)
消費電力削減数倍〜数十倍の改善
精度低下通常0.1〜2%(タスクによる)

量子化はモデルの学習を再度行わずに適用できる「ポスト学習量子化(PTQ:Post-Training Quantization)」と、量子化を考慮しながら学習する「量子化認識学習(QAT:Quantization-Aware Training)」の二種類が主流です。

歴史・背景

ニューラルネットワークの量子化に関する研究は2016〜2017年頃から活発化しました。BinaryNetやXNOR-Netなどの1bitネットワークの研究が先行しましたが、精度低下が大きく実用化は限定的でした。

量子化研究の主な流れ:

  • 2016年:BinaryNet(重みを±1に二値化)の提案
  • 2017年:GoogleがINT8量子化をTensorFlowに統合、実用化の道筋が開ける
  • 2018年:Jacob et al.による「Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference」論文でINT8量子化の理論が確立
  • 2019年:TFLite Micro向けINT8完全量子化(入出力含む)が実現
  • 2020年:GPTQ、SmoothQuantなどの高精度量子化手法が登場
  • 2022年以降:LLM(大規模言語モデル)向けの4bit量子化(GPTQ、AWQ)が急速に発展

技術仕様

量子化の数学的定義

実数値(浮動小数点)から整数への変換は以下の式で表されます。

線形量子化(アフィン量子化):

量子化(Q): q = clamp(round(r / s) + z, q_min, q_max)
逆量子化(D): r ≈ s × (q - z)

記号定義:
  r = 実数値(real value, FP32)
  q = 量子化値(quantized value, INT8)
  s = スケール(scale factor, FP32の正値)
  z = ゼロ点(zero point, 整数)
  q_min = -128(INT8符号付き)
  q_max = 127(INT8符号付き)

スケールとゼロ点の計算(対称量子化の場合):

import numpy as np

def compute_scale_zeropoint(data, n_bits=8, symmetric=True):
    """量子化パラメータを計算"""
    q_min = -(2 ** (n_bits - 1))      # INT8: -128
    q_max = (2 ** (n_bits - 1)) - 1   # INT8: 127
    
    r_min = data.min()
    r_max = data.max()
    
    if symmetric:
        # 対称量子化: ゼロ点=0
        abs_max = max(abs(r_min), abs(r_max))
        scale = abs_max / q_max
        zero_point = 0
    else:
        # 非対称量子化: ゼロ点≠0
        scale = (r_max - r_min) / (q_max - q_min)
        zero_point = round(q_min - r_min / scale)
        zero_point = int(np.clip(zero_point, q_min, q_max))
    
    return scale, zero_point

def quantize(data, scale, zero_point, n_bits=8):
    """実数→整数量子化"""
    q_min = -(2 ** (n_bits - 1))
    q_max = (2 ** (n_bits - 1)) - 1
    q = np.round(data / scale) + zero_point
    return np.clip(q, q_min, q_max).astype(np.int8)

def dequantize(q_data, scale, zero_point):
    """整数→実数逆量子化"""
    return scale * (q_data.astype(np.float32) - zero_point)

量子化の粒度(Granularity)

量子化パラメータ(スケール・ゼロ点)をどの単位で設定するかが精度に大きく影響します。

粒度説明精度効率
テンソルごと(Per-tensor)テンソル全体で1組のパラメータ最高
チャンネルごと(Per-channel)出力チャンネル単位でパラメータ
グループごと(Per-group)N個の要素ごとにパラメータ最高
# Per-channel量子化(チャンネルごとにスケールを持つ)
# Conv2Dの重み: shape = [出力ch, 入力ch, kH, kW]
weight = np.random.randn(64, 32, 3, 3)

scales = []
zero_points = []
quantized_weight = np.zeros_like(weight, dtype=np.int8)

for oc in range(weight.shape[0]):
    scale, zp = compute_scale_zeropoint(
        weight[oc], symmetric=True
    )
    scales.append(scale)
    zero_points.append(zp)
    quantized_weight[oc] = quantize(weight[oc], scale, zp)

データ型別の特性比較

データ型ビット幅値範囲精度モデルサイズ速度向上
FP3232bit±3.4e38基準基準1x
FP1616bit±655041/22x
BF1616bit±3.4e38FP32近似1/22x
INT88bit-128〜1271/42〜4x
INT44bit-8〜7低め1/84〜8x
INT2/12/1bit限定1/16理論最大

動作原理

ポスト学習量子化(PTQ)の種類

1. ダイナミック量子化(最も簡単)

重みのみ事前に量子化し、活性化値は推論時に動的に量子化します。

import torch

model = torch.load('model.pth')
model.eval()

# PyTorchダイナミック量子化
quantized_model = torch.quantization.quantize_dynamic(
    model,
    {torch.nn.Linear, torch.nn.LSTM},  # 対象レイヤー
    dtype=torch.qint8
)
torch.save(quantized_model, 'model_dynamic_int8.pth')

2. 静的量子化(最も高速)

重みも活性化値も事前に量子化。キャリブレーションデータが必要。

import torch
from torch.quantization import prepare, convert

model = MyModel()
model.eval()

# 量子化設定
model.qconfig = torch.quantization.get_default_qconfig('x86')

# 量子化の準備(観測ノードを挿入)
model_prepared = prepare(model)

# キャリブレーション(代表データを1パス流す)
with torch.no_grad():
    for data, _ in calibration_loader:
        model_prepared(data)

# 量子化モデルに変換
model_int8 = convert(model_prepared)
print(f"FP32モデルサイズ: {get_model_size(model):.1f}MB")
print(f"INT8モデルサイズ: {get_model_size(model_int8):.1f}MB")

3. 量子化認識学習(QAT:最も高精度)

学習中に量子化をシミュレートすることで、量子化誤差を学習で補正します。

# QAT(Quantization-Aware Training)
model_qat = copy.deepcopy(model)
model_qat.train()
model_qat.qconfig = torch.quantization.get_default_qat_qconfig('x86')

# フェイク量子化ノードを挿入
model_qat_prepared = torch.quantization.prepare_qat(model_qat)

# ファインチューニング(数エポック追加学習)
optimizer = torch.optim.SGD(model_qat_prepared.parameters(), lr=1e-4)
for epoch in range(5):
    for data, labels in train_loader:
        output = model_qat_prepared(data)
        loss = criterion(output, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# INT8モデルに変換
model_qat_int8 = convert(model_qat_prepared.eval())

TFLite向けINT8量子化

import tensorflow as tf
import numpy as np

# 代表データセットの準備(キャリブレーション用)
def representative_dataset_gen():
    dataset = tf.data.Dataset.from_tensor_slices(
        calibration_images
    ).batch(1)
    for data in dataset.take(200):
        yield [data]

# コンバーター設定
converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset_gen

# 完全INT8量子化(入出力含む)
converter.target_spec.supported_ops = [
    tf.lite.OpsSet.TFLITE_BUILTINS_INT8
]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8

# 変換実行
tflite_model = converter.convert()
with open('model_full_int8.tflite', 'wb') as f:
    f.write(tflite_model)

用途・ユースケース

マイコン(MCU)向け推論

Cortex-M4/M7マイコンではFP32の浮動小数点演算も可能ですが、INT8演算はCMSIS-NNの__SMLAD命令で2倍並列化できます。Cortex-M55のMVE(Helium)ではINT8を最大16並列処理できます。

Cortex-M4 @ 168MHz の推論性能比較(小型CNNの場合):
  FP32: 80ms
  INT8(CMSIS-NN): 15ms → 5.3倍高速化

NPU向け量子化

NPUは一般にINT8またはINT16演算に特化しており、FP32をそのままNPUで実行することはできません。NPUを活用するにはINT8量子化が必須です。

LLM(大規模言語モデル)の4bit量子化

2023年以降、LLMをPC・スマートフォン・エッジデバイスで動かすために4bit量子化が急速に普及しています。

# llama.cppを使ったLLMの4bit量子化(例)
# $ ./quantize llama-7b-f32.gguf llama-7b-q4_k_m.gguf Q4_K_M
# 
# モデルサイズ比較:
# FP32: 28GB → Q4_K_M: 4.1GB(約1/7に圧縮)

実装・開発のポイント

量子化の精度評価

def evaluate_accuracy(interpreter, test_dataset):
    """量子化前後の精度を比較"""
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    
    is_quantized = (
        input_details[0]['dtype'] == np.int8
    )
    
    correct = 0
    total = 0
    
    for images, labels in test_dataset:
        for image, label in zip(images, labels):
            if is_quantized:
                # INT8モデルは入力を量子化
                scale = input_details[0]['quantization'][0]
                zp = input_details[0]['quantization'][1]
                image_q = (image / scale + zp).astype(np.int8)
                interpreter.set_tensor(
                    input_details[0]['index'],
                    image_q[np.newaxis]
                )
            else:
                interpreter.set_tensor(
                    input_details[0]['index'],
                    image[np.newaxis]
                )
            
            interpreter.invoke()
            pred = np.argmax(
                interpreter.get_tensor(output_details[0]['index'])
            )
            if pred == label:
                correct += 1
            total += 1
    
    return correct / total

fp32_acc = evaluate_accuracy(fp32_interpreter, test_dataset)
int8_acc = evaluate_accuracy(int8_interpreter, test_dataset)
print(f"FP32精度: {fp32_acc*100:.2f}%")
print(f"INT8精度: {int8_acc*100:.2f}%")
print(f"精度低下: {(fp32_acc - int8_acc)*100:.2f}%")

量子化が難しいレイヤー

一部のレイヤーは量子化後に精度が大きく低下することがあります。

レイヤー量子化の難しさ対処法
Softmax△(出力分布が偏る)FP32で実行
LayerNormPer-tokenスケール
Attention(QKV積)× (値域が広い)FP16 or 特殊量子化
最初の畳み込みPer-channel量子化
残差加算スケール合わせが必要

他技術との比較

量子化はプルーニングと並ぶモデル圧縮の二大技術です。

比較軸量子化プルーニング知識蒸留
効果4〜8倍圧縮2〜10倍圧縮タスク依存
実装難易度低〜中中〜高
再学習の必要性不要(PTQ)or あり(QAT)必要必要(学習全体)
ハードウェア対応NPU/DSP必須で最大効果汎用的ハードウェア非依存
精度劣化小(INT8)小〜中小(うまく行けば)

量子化はTensorFlow LiteONNX推論アクセラレータと組み合わせることでエッジAIの実用化に直結します。NPUの性能を最大限に引き出すために不可欠な技術です。

関連用語

参考リンク