LabHub
배우기 러닝패스 코스

AI 다이어트 실패 사건 · 보정 자료가 범위를 정한다 · 이론

보정 자료가 자를 만든다 — 좁으면 잘리고 넓으면 뭉개진다

LabHub 에서 이어서 보기

한 줄 요약

정적 양자화에서 스케일은 보정 자료가 정한다. 보정이 본 범위가 실제 분포보다 좁으면 값이 잘리고, 넓으면 눈금이 성글어진다 — 어느 쪽이든 정확도는 보정 자료의 함수다.

왜 여기서 자꾸 무너지나

정적 양자화는 활성의 스케일을 변환 시점에 못박는다. 못박으려면 활성이 어느 범위에 사는지 알아야 하고, 그것을 알려 주는 것이 보정 자료다. 변환기는 이 자료를 흘려 넣어 층마다 최솟값과 최댓값을 관찰하고, 그 범위로 스케일과 영점을 만든다.

그래서 보정 자료는 설정값이 아니라 입력값이다. 같은 모델, 같은 변환 명령인데 보정 자료만 바꾸면 완전히 다른 모델이 나온다. 그런데 이 자료는 코드에 남지 않고 로그에도 안 남는다. 몇 달 뒤 "이 모델 정확도가 왜 이렇지" 를 보면 모델 파일과 변환 스크립트만 있고, 정작 답을 정한 것은 그날 쓴 배열들이다.

무너지는 방향은 둘이다. 좁으면 잘린다. 보정 때 본 최댓값보다 큰 값이 들어오면 정수 자료형의 끝에 붙어 버리고 역양자화로 돌아오지 않는다. 오차가 왕복 상한의 수십 배로 커진다. 넓으면 뭉개진다. 실제로는 쓰이지 않는 넓은 범위에 눈금 256개를 배분하므로, 자료가 실제로 사는 구간에는 몇 개 안 되는 눈금만 놓인다. 잘리지는 않지만 해상도를 잃는다.

어떻게 동작하나

[ONNX Runtime 의 양자화 문서](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)가 정적 양자화에 CalibrationDataReader 를 넘기라고 한다. 계약은 짧다.

class 내_공급기(CalibrationDataReader):    def get_next(self):        더 줄 것이 있으면 -> {"입력이름": 배열}  사전 하나        더 없으면        -> None

변환기는 None 이 나올 때까지 get_next() 를 계속 부른다. 그래서 두 가지 실수가 흔하다. 하나는 끝난 뒤에 None 대신 예외를 던지는 것이고(보정 도중에 죽는다), 다른 하나는 한 번 소진한 공급기를 다시 쓰는 것이다(두 번째 변환은 보정 자료를 하나도 못 받는다).

보정이 남긴 흔적은 모델 안에 그대로 있다. 그래프를 열어 입력 바로 뒤의 QuantizeLinear 를 찾아 그 스케일 초기자를 읽으면, 스케일에 눈금 수를 곱한 값이 보정 때 관찰한 범위의 폭이다. 이 숫자 하나로 "보정 자료가 어디까지 봤는가" 를 사후에 확인할 수 있다.

형식도 두 가지다. QDQ 는 원래 연산자를 그대로 두고 앞뒤에 QuantizeLinearDequantizeLinear 를 끼워 넣는다. 그래프에 MatMul 이 그대로 보이고, 정수화는 런타임이 실행 시에 합친다. QOperatorQLinearMatMul 같은 정수 연산자로 아예 갈아 끼운다. 노드 수가 적고 파일도 작지만 그래프를 읽기는 어렵다.

여기서 흔한 오해 하나를 짚어야 한다. 같은 보정에서 뽑은 두 형식은 같은 스케일과 영점을 쓰고 오차의 크기도 같은 자릿수지만, 원소별로 똑같지는 않다. QDQ 는 원래 연산자를 그래프에 남겨 두고 실행 방식을 런타임에 맡기기 때문이다. 런타임이 Q/DQ 를 접어 정수 커널로 돌릴 수도 있고, 역양자화한 값으로 그냥 실수 연산을 할 수도 있다. 그래서 "형식을 바꿨더니 출력이 조금 달라졌다" 는 정상이고, 자릿수가 달라졌다면 그때는 보정을 의심해야 한다.

현장에서 만나는 모습

첫째, 표본을 늘려도 안 고쳐지는 종류가 있다. 보정 자료의 분포 자체가 어긋나 있으면, 그 자료를 몇 백 배로 늘려도 관찰 범위는 조금밖에 안 넓어진다. 좁은 보정을 512배로 늘려 얻는 개선이, 맞는 분포로 몇 묶음만 준 것보다 못하다. 수가 아니라 분포가 문제일 때가 있다는 것을 재어서 구분해야 한다.

둘째, 정적이 동적보다 항상 정확하지는 않다. "정적이 더 좋다" 는 말은 보정이 제대로 됐을 때의 이야기다. 보정이 어긋나면 동적 양자화보다 훨씬 나쁜 모델이 나온다. 그리고 그 사실은 평가해 보기 전까지 아무 데도 안 나타난다.

셋째, 잘림과 뭉개짐은 증상이 다르다. 잘림은 큰 입력에서만 크게 틀리고 작은 입력에서는 멀쩡하다. 뭉개짐은 모든 입력에서 고르게 조금씩 틀린다. 오차를 입력 크기별로 나눠 보면 어느 쪽인지 갈린다.

넷째, 보정 자료를 기록으로 남긴다. 어떤 표본을 몇 개 썼는지, 그 자료의 범위가 얼마였는지를 모델 옆에 적어 둔다. 모델 안의 스케일과 대조할 수 있어야 나중에 원인을 찾는다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

CalibrationDataReader 를 직접 구현하고, 같은 모델을 좁은·맞는·넓은 보정 자료 세 벌로 양자화해 그래프에 박힌 입력 스케일과 자기가 잰 최대 절대 오차를 나란히 적는다. 그다음 좁은 보정을 1묶음에서 512묶음까지 늘려 가며 그것으로는 못 고친다는 것을 재고, 마지막으로 QDQ 와 QOperator 를 같은 보정에서 뽑아, 두 형식의 오차 크기와 둘 사이의 차이를 함께 잰다. 채점기는 여러분의 공급기를 직접 몰아 보고, 적어 낸 오차를 여러분의 모델 파일로 다시 계산해 대조한다.