AI 다이어트 실패 사건 · 스케일과 영점 · 이론
스케일과 영점 — 양자화는 자 하나를 고르는 일이다
한 줄 요약
양자화는 실수 축에 눈금 간격(scale)과 0 의 자리(zero_point) 를 정하는 일이고, 그 둘만 정해지면 나머지는 나눗셈과 반올림뿐이다.
왜 이걸 알아야 하나
변환 도구를 부르면 int8 모델이 나온다. 정확도가 떨어지면 무엇을 고쳐야 할까. 보정 자료를 더 줄까, 채널별로 나눌까, 활성만 float 로 남길까. 이 선택지들은 전부 스케일이 어떻게 정해지는가 에 대한 선택이다. 안에서 무슨 산술이 도는지 모르면 고르지 못하고 시행착오만 남는다.
그리고 실제로 틀리는 자리가 있다. 가장 흔한 것이 0 이 0 으로 돌아오지 않는 경우다. 관찰 범위가 [0.4, 7.0] 이라고 그대로 스케일을 잡으면 실수 0.0 은 표현 가능한 눈금 밖에 있다. 패딩과 ReLU 가 만들어 낸 0 이 0 이 아닌 값으로 되살아나고, 그 오차가 층을 타고 불어난다. 그래서 규격을 따르는 구현은 범위에 0 을 반드시 포함시킨다.
어떻게 동작하나
ONNX 는 두 연산자로 이 일을 한다. [QuantizeLinear](https://onnx.ai/onnx/operators/onnx__QuantizeLinear.html)는 실수를 정수로 내리고, [DequantizeLinear](https://onnx.ai/onnx/operators/onnx__DequantizeLinear.html)는 정수를 실수로 올린다. 정의는 짧다.
QuantizeLinear y = saturate(round(x / y_scale) + y_zero_point)DequantizeLinear x = (q - x_zero_point) * x_scaleround 는 짝수 쪽으로 붙이는 반올림이다. 0.5 는 0 으로, 1.5 도 2 로, 2.5 도 2 로 간다. saturate 는 자료형의 양끝으로 자르는 것이고, uint8 이면 0 과 255, int8 이면 -128 과 127 이다.
스케일과 영점은 관찰 범위 [lo, hi] 에서 나온다. 두 갈래가 있다.
- 비대칭(uint8):
scale = (hi - lo) / 255,zero_point = round(0 - lo / scale). 256 개 눈금을 범위에 통째로 배분한다. 영점이 0 이 아닌 값이 되고, 그 자리가 실수 0.0 의 정수 좌표다. - 대칭(int8):
reach = max(|lo|, |hi|),scale = reach / 127,zero_point = 0. 0 을 가운데에 못박고 양쪽으로 같은 폭을 준다. 영점이 0 이라 정수 커널에서 뺄셈 한 번이 사라진다 — 가중치를 대칭으로 다루는 이유가 이것이다.
여기서 중요한 성질 하나가 따라 나온다. 왕복 오차는 스케일의 절반을 넘지 않는다. 양자화는 값을 스케일의 배수 격자에 가장 가까운 점으로 옮기는 일이고, 격자 간격이 스케일이니 가장 멀어도 절반이다. 단, 범위 안에 있을 때 만 그렇다. 밖으로 나간 값은 벽에 붙어 버려서 오차가 얼마든지 커진다. 오차가 스케일의 절반을 크게 넘었다면 반올림 문제가 아니라 범위 문제다.
현장에서 만나는 모습
첫째, 대칭으로 활성을 다루면 절반을 버린다. ReLU 뒤의 활성은 전부 0 이상이다. 여기에 대칭 int8 을 걸면 음수 쪽 127 개 눈금이 아무 값도 안 받는다. 남은 눈금이 절반이니 스케일이 두 배로 거칠어지고, 오차도 정확히 두 배가 된다. 그래서 가중치는 대칭, 활성은 비대칭이 기본값인 경우가 많다.
둘째, 이상값 하나가 텐서 전체를 망친다. 가중치 행렬에서 한 자리만 값이 크면 텐서 전체 스케일이 그 값에 맞춰 커지고, 나머지 자리는 전부 몇 개 안 되는 눈금 위에 몰린다. 이때 채널별 스케일(per-channel) 이 답이다. 출력 채널마다 스케일을 따로 주면 피해가 이상값이 든 채널 안에 갇힌다. QuantizeLinear 의 axis 속성과 1차원 스케일 텐서가 이것을 표현한다.
셋째, 스케일을 float64 로 계산하면 런타임과 어긋난다. 런타임은 float32 로 나눈다. 파이썬 float 으로 나눈 값이 경계에 걸리면 반올림이 다른 쪽으로 가고, 원소의 1퍼센트쯤에서 한 눈금 차이가 난다. 큰 문제는 아니지만 "내 계산과 모델 출력이 왜 다르지" 로 시간을 잃는 자리다.
넷째, 오차를 재려면 기준이 있어야 한다. "정확도가 1퍼센트 떨어졌다" 는 배포 판단이고, "왕복 오차가 스케일의 절반을 넘었다" 는 원인 판단이다. 후자는 층 하나만 봐도 알 수 있고 고칠 곳을 바로 가리킨다.
실무에서 진짜 중요한 것
- 범위에 0 을 넣는다. 관찰 범위가 한쪽으로 쏠려 있어도 0 쪽으로 늘린다.
- 오차가 스케일의 절반을 넘으면 포화를 의심한다. 반올림이 아니라 범위가 문제다.
- 자료의 모양에 맞는 방식을 고른다. 0 이상만 나오는 자료에 대칭을 걸지 않는다.
- 이상값이 있으면 채널을 나눈다. 텐서 전체 스케일은 가장 큰 값 하나에 인질로 잡힌다.
다음 실습에서 할 것
qmath.py 를 한 단계씩 키워 스케일·영점 계산, 양자화, 역양자화, 왕복 오차, 채널별 스케일을 직접 구현한다. 채점기는 여러분이 적어 낸 숫자를 믿지 않는다 — 매번 다른 씨앗으로 배열을 만들어 여러분의 도구를 실제로 실행하고, 같은 값을 ONNX 의 QuantizeLinear·DequantizeLinear 연산자에 그대로 태워 나온 결과와 대조한다.