
JSON 문법 마스크가 샘플링 다양성을 없앨 것이라 예상했지만, 프롬프트가 먼저 그 역할을 수행했습니다.
요약
JSON 스키마 강제 시 발생하는 문법 마스크가 샘플링 다양성에 미치는 영향을 분석했습니다. 실험 결과, 문법 마스크보다 프롬프트에 포함된 스키마 정보가 모델의 토큰 선택 붕괴에 더 큰 영향을 미친다는 것을 발견했습니다.
핵심 포인트
- 문법 마스크는 확률 질량을 유지하며 토큰 ID의 대부분을 제거할 수 있음
- 샘플링 다양성 저하는 마스크보다 프롬프트 내 스키마 정보에 의해 더 크게 발생함
- Qwen2.5 모델 실험 시 문법 마스크가 핵(nucleus) 크기를 변경하는 사례는 거의 없었음
- 자유 선택 단계에서의 붕괴(collapse) 측정을 통해 샘플링 메커니즘 분석
저는 상당히 단순한 의구심을 가지고 이 프로젝트를 시작했습니다.
LLM (Large Language Model)이 JSON 스키마 (schema)를 따르도록 강제될 때, 문법 (grammar)은 매 단계마다 어휘의 대부분을 제거합니다. 만약 제가 temperature=0.7 및 top_p=0.9와 같은 일반적인 샘플링 (sampling) 설정을 계속 사용한다면, 해당 설정이 의미를 가질 만큼 충분한 분포가 남아 있을까요?
저는 문법 마스크 (grammar mask)가 샘플러 (sampler)를 거의 탐욕적 (greedy)으로 동작하게 만들 것이라고 예상했습니다. 로짓 (logits)을 캡처하고, 마스크를 확인하고, 6,144개의 샘플링된 출력을 실행한 결과, 저는 예상보다 덜 편리한 답을 얻었습니다.
Qwen2.5-1.5B-Instruct의 경우, 샘플러가 매우 자주 하나의 토큰으로 붕괴 (collapse)되었지만, 제 주요 실행 과정에서 문법 마스크가 그 원인이 되지는 않았습니다. 모델은 프롬프트 (prompt)에서 전체 스키마를 본 후 이미 결정을 내린 상태였습니다.
마스크는 모델의 확률 질량 (probability mass)을 거의 제거하지 않으면서 토큰 ID의 99%를 제거할 수 있습니다.
이 문장이 첫 번째 결과를 설명해 주었습니다. 그 후, 프롬프트에서 스키마를 제외하고 비교를 다시 수행했습니다. 허용 가능한 확률 질량 (Legal probability mass)은 0.9996에서 0.7962로 떨어졌고, 마스크가 핵 (nuclei)을 변화시키기 시작했습니다. 프롬프트가 제가 생각했던 것보다 더 많은 일을 하고 있었던 것입니다.
요약하자면: 스키마가 제시되었을 때 Qwen의 문법 마스크는 측정된 427개의 자유 선택 단계 중 0개에서 핵 (nucleus) 크기를 변경했습니다. 스키마를 제외한 후속 비교에서는 556개 중 120개를 변경했습니다. TinyLlama의 마스크는 원래 실행에서 504개 중 38개를 변경했습니다.
이 글의 내용
- 붕괴 (collapse) 측정
- 결과
- 전체 출력 다양성 (Whole-output diversity)
- 연산 순서 (Order of operations)
- 구현 노트 (Implementation notes)
- 이것이 실무에서 바꾸는 점
- 제가 지금 주장하고 싶은 것
붕괴 (collapse) 측정
측정 대상
Top-p 샘플링 (sampling)은 토큰을 확률 순으로 정렬하고, 총 확률이 p에 도달하는 가장 작은 접두사 (prefix)를 유지합니다. 샘플링은 그 접두사 내부에서 이루어집니다.
다음 토큰 확률이 다음과 같다고 가정해 봅시다:
token A: 0.93
token B: 0.04
token C: 0.02
...
top_p=0.9일 때, 핵 (nucleus)에는 token A만 포함됩니다. 이 호출은 기술적으로는 샘플링 (sampling)이지만, 샘플링할 대상이 아무것도 없습니다.
저는 이를 붕괴된 단계 (collapsed step)라고 불렀습니다. 더 정확하게는 다음과 같습니다:
collapse = nucleus size is 1
중요한 예외가 하나 있습니다. 만약 문법 (grammar)이 단 하나의 토큰만을 허용한다면, 그 단계는 구문 (syntax)에 의해 강제된 것입니다. 닫는 중괄호나 따옴표가 top-p에 의해 다양성이 제거되었다는 증거로 간주되어서는 안 됩니다. 저는 헤드라인 비율 (headline rate)을 계산하기 전에 이러한 단계들을 분리했습니다.
따라서 제가 관심을 가졌던 수치는 다음과 같습니다:
붕괴된 자유 선택 단계 (collapsed free-choice steps)
---------------------------
모든 자유 선택 단계 (all free-choice steps)
자유 선택 단계 (free-choice step)는 top-p가 적용되기 전에 최소 두 개의 유효한 토큰을 가집니다.
동일 로짓 비교 (The same-logit comparison)
이 질문을 해결하기에는 일반적인 생성 경로만으로는 충분하지 않습니다. 제약 생성 (constrained generation)과 자유 생성 (free generation)은 초기에 서로 다른 토큰을 선택할 수 있으므로, 그 이후의 로짓 (logits)은 더 이상 동일한 접두사 (prefix)를 설명하지 않기 때문입니다.
대신, 저는 각 제약 생성 단계에서 하나의 원시 로짓 벡터 (raw logit vector)를 캡처하여, 그로부터 두 가지 분포를 평가했습니다:
- 원래의 모델 분포 (original model distribution)
- 문법 마스크 (grammar mask)를 적용하고 재정규화 (renormalizing)한 후의 동일한 분포
각 분포에 대해 다양한 온도 (temperature)와 top-p 값의 그리드(grid)에 걸쳐 핵 크기 (nucleus size)를 다시 계산했습니다. 이를 통해 저는 동일 로짓 반사실적 (same-logit counterfactual)을 얻을 수 있었습니다. 이를 통해 생성된 궤적 (trajectory)이 달라지는 효과와 혼동하지 않고, 마스크가 핵을 변화시켰는지 여부를 물을 수 있었습니다.
또한 유효 확률 질량 (legal probability mass)도 기록했습니다:
Z_t = 문법적으로 유효한 토큰에 할당된 확률 질량
Z_t가 1에 가까울 때, 모델은 이미 문법 내에 머물고자 합니다. Z_t가 작을 때, 마스크는 많은 확률을 이동시켜야 합니다.
주요 실행에는 다음이 사용되었습니다:
- Qwen2.5-1.5B-Instruct
- TinyLlama-1.1B-Chat-v1.0
- 6개의 JSON 스키마 (schemas)
- 스키마당 2개의 프롬프트 (prompts)
- 0.0부터 1.5까지의 온도 (temperatures)
- 0.5부터 1.0까지의 top-p 값
스키마는 엄격한 열거형 (enum), 정수 (integer), 자유 문자열 (free string), 패턴 문자열 (patterned string), 혼합 객체 (mixed object), 그리고 추론 문자열 (reasoning string)이 먼저 나오는 혼합 객체를 포함했습니다.
한 가지 프롬프트 세부 사항이 필수적입니다: _prompt_text는 사용자 메시지에 전체 JSON 스키마 (schema)를 덧붙였습니다. Qwen은 제가 법적 확률 질량 (legal probability mass)을 측정하기 전에 필드 이름, 타입 (types), 열거형 값 (enum values), 그리고 필수 속성 (required properties)을 확인했습니다. 이러한 설정은 흔하지만, 구조화된 출력 (structured-output) 시스템이 구축되는 유일한 방법은 아닙니다.
마스크 (mask) 확인
모든 결과는 법적 토큰 마스크 (legal-token mask)가 올바른지에 달려 있으므로, 저는 수동으로 확인할 수 있을 만큼 작은 스키마로 시작했습니다:
{
"type": "object",
"properties": {
...
저는 컴팩트한 형태인 {"a":12}를 위한 10-상태 바이트 수준 인식기 (ten-state byte-level recognizer)를 작성하였고, 모든 접두사 (prefix)에서 허용된 토큰을 XGrammar와 비교했습니다. 또한 XGrammar를 Outlines와도 비교했습니다.
첫 번째 비교는 모든 접두사에서 정확히 271개의 토큰 ID (token IDs) 차이로 실패했습니다. 그 숫자가 단서가 되었습니다. Qwen의 모델 헤드 (model head)는 151,936개의 행을 가지고 있는 반면, 토크나이저 (tokenizer)는 151,665개의 항목을 가지고 있었습니다. 제가 만든 검사기는 패딩된 (padded) 모델 행을 빈 바이트 문자열로 취급하여 실수로 이를 no-op 토큰으로 허용하고 있었습니다.
해당 행들을 제외한 후, 직접 작성한 인식기, XGrammar, 그리고 Outlines는 확인된 모든 접두사에서 일치했습니다.
TinyLlama는 또 다른 문제를 드러냈습니다. SentencePiece는 여는 중괄호 ({)를 인코딩할 때, 일반적인 디코딩된 문자열은 동일한 중괄호처럼 보일지라도 문법 바이트 (grammar bytes)에 선행 공백 (leading space)을 포함하는 토큰을 사용할 수 있습니다. 원래의 수동 확인 작업은 잘못된 문장 시작 (beginning-of-sequence) 토큰화를 사용하고 있었습니다.
그 버그는 조기에 발견할 가치가 있었습니다. 나중에 동일한 선행 공백 동작은 생성된 토큰 ID와 재인코딩된 토큰 ID를 비교할 때 오해의 소지가 있는 결과를 만들어냈습니다. TinyLlama의 12개 시퀀스 모두 왕복 과정 (round trip)에서 첫 번째 토큰 ID가 변경되었지만, 디코딩된 바이트는 전혀 변하지 않았습니다. 이제 저는 ID 변경을 출력의 변경이 아닌 토크나이저 진단 (tokenizer diagnostic)으로 취급합니다.
결과
Qwen: 두 히스토그램은 동일합니다
프롬프트에 전체 스키마 (schema)가 포함된 상태에서, T=0.7 및 top_p=0.9 설정 시 Qwen은 12개의 제약된 생성 (constrained generations) 과정 동안 427개의 자유 선택 단계 (free-choice steps)를 가졌습니다. 그중 336개 단계에서 핵 (nucleus)에 단 하나의 토큰만 포함되었으며, 이는 78.7%의 통합 비율 (pooled rate)을 나타냅니다.
단계 수 (step count)는 독립적인 샘플 크기가 아닙니다. 하나의 생성 내에 있는 단계들은 프롬프트, 스키마, 그리고 접두사 (prefix)를 공유합니다. 12개의 생성 각각에 동일한 가중치를 부여했을 때, 평균 붕괴율 (collapse rate)은 82.5%였으며, 표준 편차는 10.0% 포인트, 범위는 70.0%에서 100% 사이였습니다.
놀라운 부분은 반사실적 (counterfactual) 결과였습니다:
| Qwen 측정값 | 결과 |
|---|---|
| 통합 붕괴율 (pooled collapse rate) | 78.7% |
| ... | |
| 자유 선택 (free) 히스토그램과 제약된 (constrained) 히스토그램이 서로 완전히 겹쳐져 있습니다. |

그림 1. T=0.7 및 top_p=0.9에서의 자유 선택 및 제약된 핵 (nucleus) 크기. 두 분포가 겹쳐져 있음.
Qwen은 프롬프트에 이미 스키마를 가지고 있었으며, 거의 모든 확률을 유효한 연속 (legal continuations)에 할당하고 있었습니다. 마스크 (mask)는 어휘 항목 (vocabulary entries)을 제거했지만, 확률은 거의 제거하지 않았습니다. 확률이 낮은 토큰들을 제거하는 것은 top-p 접두사 (prefix)를 변화시키지 않았습니다.
더 넓은 그리드 (grid)는 예상된 방향으로 작동했습니다. 더 낮은 온도 (temperature)와 더 낮은 top-p는 더 많은 '단일 토큰 핵'을 생성했습니다. top_p=1.0에서는 절단 (truncation)이 발생하지 않으므로, 이 정의에 따라 자유 선택 단계는 붕괴된 것으로 간주될 수 없습니다.

그림 2. 전체 온도 및 top-p 그리드에 따른 Qwen 붕괴율. top_p=1.0에서는 핵 절단 (nucleus truncation)이 발생하지 않음.
프롬프트가 먼저 도달했습니다
혼합 객체 스키마 (mixed-object schema)에 대해 쌍을 이룬 후속 테스트를 실행했습니다. 여기에는 문자열 (string), 열거형 (enum), 정수 (integer), 불리언 (boolean), 배열 (array), 그리고 중첩된 객체 (nested object)가 포함됩니다. 저는 스키마 없이도 여전히 의미가 통하는 하나의 프롬프트를 사용했습니다:
Return a plausible record for a failed API request.
모델, 문법 (grammar), 샘플러 (sampler) 설정, 그리고 시드 (seed) 101부터 108까지는 고정되었습니다. 유일한 변화는 사용자 메시지에 전체 스키마가 추가되었는지 여부였습니다.
| 측정 항목 | 스키마 표시됨 | 스키마 제외됨 |
|---|---|---|
| 생성 횟수 (generations) | 8 | 8 |
| ... | ||
![]() |
그림 3. 혼합 객체 스키마에 대한 8개의 일치하는 시드. 왼쪽 패널의 점들은 생성 평균값이며, 검은색 마커는 평균과 표준 편차를 나타냅니다.
16개의 제약된 생성 (constrained generations) 모두 완료되었으며 JSON으로 파싱되었습니다. 모든 일치하는 시드에서 스키마를 제외했을 때 법적 질량 (legal mass)의 평균이 더 낮았습니다. 쌍을 이룬 평균 하락 폭은 0.2034였습니다. 스키마가 제외된 조건에서, 마스크 (mask)는 자유 선택 핵 (free-choice nuclei)의 21.6%를 변경했으며, 106개 단계에서 최상위 토큰 (top token)을 변경했습니다.
이것이 제가 놓쳤던 메커니즘입니다. 마스크의 효과는 프롬프트가 이미 법적 토큰 (legal tokens)에 확률을 얼마나 집중시켰는지에 따라 부분적으로 달라집니다. 이는 모델과 문법만으로 결정되는 것이 아닙니다.
TinyLlama가 움직였지만, 그리 크지는 않았습니다
TinyLlama는 12개의 제약된 생성 과정 동안 504개의 자유 선택 단계를 가졌습니다. 통합 붕괴율 (pooled collapse rate)은 76.2%였습니다. 생성 수준의 평균은 76.7%였으며, 표준 편차는 11.6%포인트, 범위는 54.5%에서 90.9% 사이였습니다.
이번에는 마스크가 측정 가능한 효과를 나타냈습니다:
| TinyLlama 측정값 | 결과 |
|---|---|
| 자유 반사실적 붕괴율 (free counterfactual collapse rate) | 71.2% |
| ... | |
| 따라서 문법 (grammar)은 TinyLlama에서 약 5%포인트의 붕괴를 추가했습니다. 이는 이 샘플에서는 실제적인 결과이지만, 구조화된 디코딩 (structured decoding)이 일반적으로 top-p를 탐욕적 디코딩 (greedy decoding)으로 바꾼다고 말하는 것보다는 훨씬 좁은 범위의 결과입니다. |
TinyLlama를 위한 별도의 자유 대조군 (free control)은 더 큰 주장을 뒷받침할 만큼 충분히 깔끔하지 않았습니다. 12개의 자유 생성 (free generations) 중 JSON으로 파싱된 것이 하나도 없었으며, 두 프롬프트 모두 6개의 스키마 중 4개에서 128-토큰 제한 (128-token cap)에 걸렸습니다. 여기서 유용한 것은 동일 로짓 (same-logit) 비교입니다.
스키마 형태는 여전히 중요했습니다
저의 원래 순서 예측은 대부분 유지되었습니다. 엄격한 열거형 (tight enums)과 패턴화된 문자열 (patterned strings)이 일반적인 문자열 값보다 더 자주 붕계되었습니다. 그 격차는 예상보다 작았으며, 자유 문자열 (free strings)조차도 종종 하나의 토큰 핵 (one-token nucleus)을 가졌습니다.
참조 설정 (reference setting)에서의 Qwen의 경우, 스키마별 풀링된 비율은 자유 문자열 스키마의 73.5%에서 엄격한 열거형의 94.4% 사이였습니다. TinyLlama는 이 두 스키마에서 56.9%에서 90.9% 사이의 범위를 보였습니다.

그림 4. 스키마 및 대략적인 문법 위치에 따른 Qwen 붕괴율.
이 그림의 위치 레이블은 디코딩된 접두사 (decoded prefix)에서 가져왔습니다. 이는 대략적인 분류에는 유용하지만, 두 개의 문법 위치를 가로지르는 토큰이나 이스케이프된 문자열 (escaped string)은 태거 (tagger)를 속일 수 있습니다.
제약 압박은 붕괴를 예측하지 못했습니다
저는 낮은 법적 확률 질량 (low legal probability mass)이 하나의 토큰 핵과 일치할 것이라고 예상했습니다. 그렇지 않았습니다.
Z_t와 붕괴 플래그 (collapse flag) 사이의 피어슨 상관계수 (Pearson correlation)는 다음과 같았습니다:
Qwen: -0.020
TinyLlama: -0.056
이 샘플에서 두 값 모두 사실상 0입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기