AI 다이어트 실패 사건 · 스케일과 영점 · 실습
자를 직접 만든다 — 스케일·영점·왕복 오차
목표
qmath.py 를 만들어 QuantizeLinear·DequantizeLinear 의 정의대로 스케일과 영점을 계산하고, 양자화와 역양자화를 구현하고, 왕복 오차가 스케일의 절반 안에 있는지 확인하고, 대칭과 비대칭 그리고 채널별 스케일과 텐서 전체 스케일을 같은 자료 위에서 견준다.
왜 중요한가
변환 도구가 내놓은 int8 모델의 정확도가 떨어졌을 때 고칠 곳을 고르려면, 그 도구가 안에서 하는 산술을 알아야 한다. 스케일은 관찰 범위를 눈금 수로 나눈 값이고, 영점은 실수 0.0 이 앉을 정수 자리다. 범위에 0 이 없으면 패딩과 ReLU 가 만든 0 이 0 이 아닌 값으로 되살아난다.
왕복 오차에는 상한이 있다. 값이 범위 안에 있는 한 오차는 스케일의 절반을 넘지 않는다 — 양자화가 값을 스케일 간격의 격자로 옮기는 일이기 때문이다. 그래서 오차가 그 절반을 크게 넘었다면 반올림이 아니라 범위가 문제다. 이 한 가지 잣대가 원인 판단을 빠르게 만든다.
자료의 모양도 방식을 정한다. ReLU 뒤의 활성은 전부 0 이상이라 대칭 int8 을 걸면 음수 쪽 눈금이 통째로 놀고 스케일이 두 배 거칠어진다. 가중치 행렬에 이상값이 하나 있으면 텐서 전체 스케일이 그 값에 끌려가 나머지가 전부 뭉개진다 — 채널을 나누면 피해가 그 채널 안에 갇힌다.
채점기는 여러분이 적어 낸 숫자를 믿지 않는다. 매번 다른 씨앗으로 배열을 만들어 여러분의 qmath.py 를 실제로 실행하고, 같은 입력을 ONNX 의 QuantizeLinear·DequantizeLinear 연산자에 그대로 태워 나온 결과와 대조한다.
단계
1. /root/onnxq-scale/gen_data.py 를 만들어 실행해 /root/onnxq-scale/data 아래 배열 세 벌을 만들고, /root/onnxq-scale/qmath.py 에 params 를 구현하세요.
2. q 를 더해 실수 배열을 정수 배열로 내리세요.
3. dq 를 더해 정수 배열을 실수로 되돌리세요.
4. roundtrip 을 더해 왕복 오차와 그 상한을 함께 내게 하세요.
5. 범위를 일부러 좁혀 포화를 만들고 결과를 /root/onnxq-scale/saturate.json 에 적으세요.
6. 전부 0 이상인 자료에 대칭과 비대칭을 걸어 /root/onnxq-scale/symmetry.json 에 적으세요.
7. channel 을 더해 채널별 스케일과 텐서 전체 스케일을 견주고 /root/onnxq-scale/channel.json 에 적으세요.
8. /root/onnxq-scale/report.json 과 /root/onnxq-scale/report.md 로 한 장에 정리하세요.
참고
- 파이썬은
/opt/onnx-lab/bin/python입니다. 시스템python3에는 numpy 가 없습니다. 실행 예:/opt/onnx-lab/bin/python /root/onnxq-scale/qmath.py params -3.2 5.1 asym-u8. - 실행 계약: 성공하면 종료 코드 0 이고 표준출력에 JSON 한 덩어리를 냅니다. 인자 수가 맞지 않으면 2 입니다.
params <lo> <hi> <mode>응답:{"scale": 실수, "zero_point": 정수, "qmin": 정수, "qmax": 정수, "mode": 문자열}.q <입력.npy> <출력.npy> <mode> [lo hi]응답:{"scale": 실수, "zero_point": 정수, "clipped": 정수, "dtype": 문자열, "count": 정수}. lo·hi 를 주지 않으면 입력 배열의 최솟값·최댓값을 씁니다. clipped 는 잘린 원소 수입니다.dq <양자.npy> <출력.npy> <scale> <zero_point> <mode>응답:{"min": 실수, "max": 실수, "count": 정수}.roundtrip <입력.npy> <mode> [lo hi]응답:{"mode", "scale", "zero_point", "lo", "hi", "clipped", "max_abs_error", "bound", "within_bound"}. bound 는 스케일의 절반이고 within_bound 는 불리언입니다.channel <입력.npy> <axis>응답:{"axis", "channels", "per_tensor_scale", "per_tensor_max_error", "per_channel_scales", "per_channel_max_error", "worst_channel", "channels_improved"}. 대칭 int8 로 계산합니다.- mode 는 둘입니다.
asym-u8은 uint8 · qmin 0 · qmax 255 · 비대칭이고,sym-i8은 int8 · qmin -128 · qmax 127 · 영점 0 인 대칭입니다. 대칭에서는 한쪽 폭을 127 로 나눕니다(128 이 아닙니다). - 범위에는 0 을 반드시 포함시킵니다. lo 가 0 보다 크면 0 으로 내리고, hi 가 0 보다 작으면 0 으로 올립니다.
- 반올림은 짝수 쪽으로 붙이는 반올림입니다. numpy 의
np.rint가 그렇게 동작합니다.round()내장 함수나int(x + 0.5)를 쓰면 답이 달라집니다. - 스케일을 만들 때
np.float32(...)로 감싸고 나눗셈도 float32 로 하세요. 파이썬 float 으로 나누면 경계에 걸린 원소에서 런타임과 한 눈금 어긋납니다. - 재료 배열은 /root/onnxq-scale/data 아래 spread.npy · positive.npy · weights.npy 세 개입니다. gen_data.py 가 만들며 채점기는 이 파일들을 읽습니다.
- 공식 문서: [QuantizeLinear](https://onnx.ai/onnx/operators/onnx__QuantizeLinear.html) · [DequantizeLinear](https://onnx.ai/onnx/operators/onnx__DequantizeLinear.html) · [ONNX Concepts](https://onnx.ai/onnx/intro/concepts.html) · [ONNX Runtime 양자화](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)
- 흔한 실수: 범위에 0 을 안 넣기, 대칭에서 128 로 나누기,
np.rint대신 반내림 반올림 쓰기, 포화가 난 경우에도 상한을 넘지 않는다고 적기.
단계 8개
- 관찰 범위를 스케일과 영점으로
- 실수를 정수 눈금으로 내리기
- 정수를 실수로 되돌리기
- 왕복 오차와 그 상한
- 범위를 좁히면 무엇이 깨지는가
- 0 이상만 나오는 자료에 대칭을 걸면
- 이상값 하나를 가두기
- 잰 것을 한 장으로