# AIFlow Math Ink 0.6 — 행동 문맥 모델 전환 보고서 ## 결론 0.6의 남은 정확도는 고립 기호 분류기만 더 학습해서 해결되지 않는다. 동일한 교차획이 `x`, `X`, `\times`가 될 수 있고, 동일한 대문자도 분자·분모·첨자처럼 축소된 수식 슬롯에서는 식 전체 높이와 비교할 수 없기 때문이다. 따라서 다음 책임 분리를 채택한다. 1. trajectory/raster 신경망은 형태 후보 top-k와 확률을 만든다. 2. Tray·관계 레이어는 분자·분모·첨자·피개항 등 로컬 좌표계를 만든다. 3. 행동 문맥 레이어는 획 행동, 로컬 크기, 이웃 후보, 간격, Tray 역할을 사용해 같은 형태군 안의 의미만 재점수화한다. 4. 수식 gate와 LaTeX 문법은 그 뒤에서 작동한다. 5. 행동 증거가 부족하면 원래 top-k를 그대로 반환한다. ```text touch / raster → 128×128 정규화·trajectory encoder → 기호 top-k → segmentation lattice·Tray·2D relation → 행동 문맥 모델 ├─ 로컬 슬롯별 대소문자 ├─ x / X / \times 의미 역할 └─ O / o / 0 등 hard-family 후보 보존 → math/non-math gate → Formula Expert·LaTeX ``` 행동 모델은 glyph 인식기를 덮어쓰는 새 OCR이 아니다. 신경망이 보존한 후보군 안에서 사용자가 그 기호를 어떤 역할로 배치했는지 추정하는 작은 후단 모델이다. ## 지금까지 확인된 연구 결과 ### Online/shared encoder - 승인 HWRT·UJI Pen v1/v2·Pendigits 기반 128-step·19-channel 경로를 구축했다. - seed 17·31·47 paired test 평균은 exact top-1 `83.00%`, top-5 `97.78%`, family top-1 `91.37%`다. - logit ensemble은 exact `83.82%`, top-5 `98.04%`, casefold `90.93%`, shape-family `93.02%`다. - exact 오류의 `43.95%`가 대소문자만 다른 오류이고 `56.86%`가 같은 shape family 안의 오류다. - 이는 형태 후보는 상당 부분 살아 있으나 최종 의미 배정이 병목이라는 근거다. ### Raster→virtual stroke - top-4 virtual stroke와 shared encoder 재입력 경로를 구현했다. - 후보 다양성은 개선됐지만 최종 raster 정확도는 release gate에 크게 미달했다. - 3-seed 차이가 작아 seed variance보다 vectorizer와 online feature 분포 정렬이 주 병목이다. - 따라서 행동 문맥은 raster vectorizer 병목을 감추는 대체물이 아니며, 두 문제를 별도 gate로 측정해야 한다. ### 대소문자 문맥 - `k/p/y`처럼 획 자체가 다른 쌍은 exact head가 담당한다. - `c/o/s/u/v/w/x/z`처럼 주 차이가 상대 높이인 쌍만 context head가 담당한다. - 기존 행 전체 기준 proxy는 validation `70.31→72.14%`, test `62.19→67.34%`로 개선됐지만, 연속 수식 P 데이터의 제품 근거는 아니다. - 행 전체 기준은 축소된 분수·첨자 안의 대문자를 소문자로 내릴 수 있으므로 이번 구현에서 로컬 슬롯 기준으로 교체했다. ### 구조·Tray - 분수선은 완성된 분자·분모가 있을 때만 `FRACTION` Tray를 만든다. - 근호는 radicand와 선택적 degree, 적분은 integrand와 선택적 upper/lower/differential을 분리한다. - `=` 인식은 좌우 식의 완성 여부와 독립적으로 보존한다. - Tray는 기호 이름을 결정하지 않고 기호가 놓인 수학적 슬롯과 로컬 기준선만 제공한다. ## `x`, `X`, `\times` 판단 `2 ? 3`이라는 이웃 정보만으로 가운데를 곱하기로 확정할 수 없다. 사용자가 변수 `x` 또는 문자 `X`를 숫자 사이에 쓸 수도 있기 때문이다. 이 모호성은 문법만으로 완전히 제거할 수 없다. 현재 행동 레이어는 다음 원칙을 적용한다. - 양옆 숫자는 보조 feature로만 사용한다. - 교차획 시각 확률, infix 점유, 연산자형 간격, 명시적 operator context 중 하나 이상의 구조 증거가 없으면 no-op이다. - 조건을 통과해도 `x/X/\times` family의 총 logit 질량을 보존한 채 `\times` prior만 더한다. - 원 신경망의 `X` 증거가 충분히 강하면 행동 prior가 뒤집지 못한다. - 본질적으로 모호한 입력은 top-k와 낮은 confidence로 반환하며 거짓 확정을 하지 않는다. 향후 학습 feature는 다음과 같다. - 원 신경망의 family logits와 entropy - 앞뒤 token의 digit/identifier/operator 확률 질량 - 좌우 간격, 중심선 정렬, 로컬 높이비 - 교차획의 방향·획순서·pen-up·속도·시간 간격 - segmentation lattice의 infix 점수 - Tray ID·slot·관계 edge - observed/canonical timestamp와 missing mask 학습 target은 원 glyph 이름 하나가 아니라 `identifier_lower`, `identifier_upper`, `multiply_operator`, `other/abstain` 의미 역할로 둔다. ## 분수·첨자 안의 대소문자 대문자 여부는 식 전체 canvas 높이와 비교하지 않는다. - main row - fraction numerator - fraction denominator - superscript/subscript - root degree/radicand - integral upper/lower/integrand 각 슬롯을 별도의 `local_context_id`로 묶고, 같은 슬롯의 신뢰 가능한 숫자·형태 문자만 높이 anchor로 사용한다. 슬롯에 anchor가 없으면 case logits를 보존한다. 이 원칙은 작은 분자 안의 `X`를 main row보다 작다는 이유로 `x`로 강등하는 오류를 막는다. ## 구현 `behavior_context06.py`를 추가하고 `math_context_layer.py` 앞단에 연결했다. 공개 입력 계약은 record별 다음 선택 feature를 사용한다. ```json { "behavior_context": { "local_context_id": "fraction:f1:numerator", "cross_visual_probability": 0.91, "infix_occupancy": 0.94, "operator_spacing_score": 0.82, "explicit_operator_context": 0.0 } } ``` Tray가 있으면 `tray_id + tray_slot`, 일반 행이면 `baseline_group`, 아무 정보도 없으면 `main`을 사용한다. `FRACTION.numerator/denominator`와 구조 관계의 `SUPERSCRIPT/SUBSCRIPT/ABOVE/BELOW/CONTAINS`는 별도 수작업 없이 로컬 context로 자동 변환한다. 회귀 검증은 다음 네 항목을 고정했다. 1. 축소된 분자 안의 `X` 보존 2. 숫자 이웃만으로 `\times` 강제 금지 3. 획·infix·간격 증거가 겹친 애매한 교차획의 소프트 `\times` 선택과 family 질량 보존 4. 강한 원본 `X` logit 보존 관련 context 회귀 테스트는 `15/15`, 전체 Python 회귀 테스트는 `264/264` 통과했다. ## 행동 모델 파인튜닝 계획 ### 단계 A — shadow feature 수집 기존 모델 weight는 고정하고 각 기호에 top-k, geometry, stroke behavior, Tray slot을 붙여 저장한다. 이미지나 원 stroke를 서버로 보내지 않는 온디바이스 원칙은 유지한다. 연구 데이터에는 원본 provenance와 writer/source/device split을 보존한다. ### 단계 B — 작은 role head 행동 head는 모바일 student에 붙일 수 있는 작은 TCN/MLP로 시작한다. pixel encoder를 다시 크게 학습하지 않고 다음 multi-task를 사용한다. - cross semantic role CE - local case role CE - Tray slot consistency loss - 원 family logit 보존 KL - abstain/calibration loss 초기 loss weight는 validation sweep 대상이며 아직 배포 상수로 고정하지 않는다. ### 단계 C — 실제 수식 검증 합성 조합은 데이터 증강으로만 사용한다. 최종 선택은 writer/device/source-disjoint 연속 수식에서 수행한다. - `x/X/\times` role top-1·top-k와 confusion matrix - local slot별 case exact - unchanged-safe rate: 행동 증거가 없는 표본의 원 top-1 보존율 - family oracle 대비 회수율 - ECE와 abstain coverage/accuracy - main/numerator/denominator/script slice 최저 성능 비상용·용도 제한 데이터는 연구 평가에만 사용하고 배포 checkpoint 학습에는 포함하지 않는다. ## 현재 판단 - 행동 모델 방향 채택: **예** - 기존 case layer를 로컬 슬롯 행동 layer로 교체: **예** - `x/X/\times` hard rewrite: **아니오** - CROHME R-track 조건부 역할 정확도 상승 확인: **예** - 이를 제품 수치로 주장: **아니오** - 0.6 상용 카나리: **아직 아니오** ## 첫 GPU 행동학습 결과 기존 3-seed glyph teacher를 각각 동결하고 `128×19 stroke TCN + 49차 문맥 MLP` 행동 head를 GTX 1650 CUDA에서 학습했다. CROHME 2012 `trainData`는 writer 기준 fit 1,063식/validation 275식으로 나눴고 `testDataGT` 488식은 공식 held-out으로만 한 번 평가했다. 역할 분모는 다음과 같다. | split | lower identifier | upper identifier | multiply operator | |---|---:|---:|---:| | fit | 1,573 | 99 | 177 | | validation | 401 | 22 | 44 | | official test | 568 | 31 | 36 | head 입력에는 다음을 사용했다. - seed별 frozen 0.6 teacher의 lower/upper/times logits - target 원본 stroke의 순서·방향·pen-up을 보존한 128×19 sequence - 좌우 이웃의 정답 label이 아닌 teacher 예측 역할 확률 - bbox, 간격, 중심선, 로컬 높이·폭 비율 - 19개 cross visual feature와 stroke/point 수 | 지표 | seed 17 | seed 31 | seed 47 | 평균 ± 표준편차 | |---|---:|---:|---:|---:| | teacher role accuracy | 74.33% | 60.79% | 60.94% | 65.35 ± 6.35%p | | behavior role accuracy | 93.07% | 93.86% | 93.07% | **93.33 ± 0.37%p** | | behavior macro-F1 | 76.64% | 77.17% | 75.42% | **76.41 ± 0.73%p** | | lower recall | 95.07% | 96.30% | 95.60% | 95.66 ± 0.50%p | | upper recall | 61.29% | 51.61% | 54.84% | **55.91 ± 4.02%p** | | multiply recall | 88.89% | 91.67% | 86.11% | **88.89 ± 2.27%p** | | ECE | 4.73% | 5.09% | 4.82% | 4.88 ± 0.15%p | 평균 accuracy 이득은 `+27.98%p`이고 seed 간 편차도 작아 행동 문맥 방향은 유효하다. 특히 frozen teacher가 세 seed 모두 `\times`를 0% 회수하던 조건에서 행동 head는 86.11~91.67%를 회수했다. 그러나 다음 한계 때문에 제품 승격은 금지한다. 1. 정답 symbol grouping 이후의 조건부 역할 평가다. segmentation 오류는 분모에 없다. 2. CROHME R-track weight라 상용 checkpoint에 병합할 수 없다. 3. official test의 대문자 분모는 31개뿐이며 recall 평균도 55.91%다. 4. 분수·첨자 로컬 case의 실제 device-disjoint 제품 분모가 아니다. 5. 세 teacher별 head는 약 75.8KB지만 distillation/온디바이스 변환 단계로 넘기지 않는다. 따라서 첫 학습 결론은 `x/X/\times` 의미 역할 head 채택, 대문자 데이터 확장, predicted grouping end-to-end 재평가다. 제품용 다음 학습은 승인된 trajectory를 수학 배치로 합성한 약한 P-compatible proxy와 실제 상용 허용 연속식 데이터를 구분해 사용해야 한다. ## 후속 루프 — 합성 대문자와 predicted grouping ### 승인 trajectory 분모 HWRT·UJI Pen v1/v2의 writer-disjoint training split에는 상대크기군 각 대·소문자가 약 90~111개씩 있다. 이를 실제 stroke 그대로 사용하고 수식 행의 높이·좌우 숫자 anchor만 합성한 P-compatible proxy를 만들었다. 첫 후보는 8개 case family 1,590개를 loss weight 0.35로 추가했다. 두 번째 후보는 실제 CROHME fit 정규화 통계를 고정하고 `c/x/z` family의 대문자 284개와 소문자 77개만 weight 0.15로 추가했다. | seed 17 official test | 원 behavior | 전체 proxy | targeted proxy | |---|---:|---:|---:| | accuracy | 93.07% | 93.39% | 93.39% | | macro-F1 | 76.64% | 75.62% | 76.45% | | upper recall | **61.29%** | 51.61% | 54.84% | | multiply recall | 88.89% | 86.11% | 88.89% | 두 후보 모두 목표인 upper recall을 악화시켰다. 고립기호를 인위적으로 키운 데이터는 실제 연속식의 대문자 행동 분포를 대체하지 못하므로 조기 기각했고 seed 31·47로 확장하지 않았다. 작은 전체 accuracy 상승을 근거로 채택하지 않는다. 로컬 MathWriting 2024 excerpt는 train/valid/test 각 100식과 symbols/synthetic 각 100개가 있지만 formula InkML에 symbol-to-stroke group 정답이 없다. LaTeX 문자열과 전체 stroke만으로 대문자 역할 target을 강제 정렬하면 label·필순을 발명하게 되므로 supervised behavior 학습에 넣지 않는다. ### Predicted grouping 감사 공식 test 488식의 행동 대상 635개를 validation에서 고정한 lattice selector로 다시 평가했다. | grouping 경로 | 전체 target | lower | upper | `\times` | `x` exact group | |---|---:|---:|---:|---:|---:| | base OCR fusion | 81.89% | 79.93% | 96.77% | 100% | 75.56% | | Tray/infix joint | 81.42% | 79.58% | 93.55% | 100% | 75.11% | | cross-visual + Tray/infix, gap 20% | 91.02% | 90.32% | 93.55% | 100% | 88.89% | | full selector, gap 50% | 93.39% | 92.96% | 93.55% | 100% | **92.22%** | | **protected family selector, gap 40%** | **92.91%** | **92.43%** | 93.55% | 100% | 91.56% | base에서 실패 115건 중 `x`가 110건이고, 그중 96건이 두 획 oversplit이었다. 최초 cross-visual 결합은 전체 oversplit을 98→42건으로 줄이고 `x` exact grouping을 `+13.33%p` 올렸다. 반면 cross model 없는 Tray/infix 단독은 소폭 악화돼 행동 grouping 경로로 채택하지 않는다. 현재 병목은 다음처럼 분리된다. 1. `x`: 최종 결합 후 grouping recall은 91.56%로 90%대를 유지하며 잔여 38건은 oversplit 26, overmerge/혼합 12다. 2. `X/C`: upper 역할 grouping은 93.55%이므로 주 병목은 behavior role head와 실제 연속식 대문자 데이터다. 3. `\times`: grouping 100%, conditional role recall 88.89%이므로 의미 head가 병목이다. 4. grouping과 role 오류가 독립이라고 가정한 근사 end-to-end recall은 lower 88.42%, upper 52.31%, multiply 88.89%다. 이는 실측 end-to-end 수치가 아니므로 제품 지표로 사용하지 않는다. 5. 전체 488식 exact partition은 60.04%이며 fraction·superscript·단일기호 overmerge가 다음 segmentation 병목이다. 따라서 R-track grouping 기준선은 `equality + cross-visual + protected multistroke family + Tray/infix`, cross gap ratio `0.40`으로 갱신한다. 대문자는 합성 isolated proxy가 아니라 실제 symbol-group annotation이 있는 연속식만 다음 supervised 학습 후보로 허용한다. ### x prefilter 병목과 gap validation sweep 실패를 candidate 생성→cross head→exact-cover selector로 분해했다. 두 획 `x`의 lattice candidate recall은 validation/test 모두 100%이고 test cross threshold recall은 98.37%였다. 그런데 실패 후보의 cross 확률 중앙값도 0.929인데 `infix_signal=0`이었다. 원인은 cross head 앞의 bbox gap 20% prefilter가 좌우 곡선 `)(`형 x를 모델에 전달하지 않은 것이다. 전역 threshold와 merge weight를 고정하고 gap ratio `0.20~0.80`만 writer-validation에서 선택했다. Validation winner `0.50`을 equality head까지 포함한 full selector의 공식 test에 적용했다. | 지표 | gap 20% | **gap 50%** | 변화 | |---|---:|---:|---:| | validation `x` grouping | 82.31% | **88.45%** | +6.14%p | | validation exact partition | 63.27% | **65.82%** | +2.55%p | | validation pair-F1 | 91.00% | **91.69%** | +0.69%p | | official test `x` grouping | 88.89% | **92.22%** | +3.33%p | | official test exact partition | 53.69% | **54.51%** | +0.82%p | | official test pair-F1 | 88.38% | **88.72%** | +0.35%p | gap 50%는 이 단계에서 전 지표가 함께 올라 채택했다. 이후 같은 기준에서 threshold `0.5`, weight `10`의 공격 후보는 test `x`를 94.44%로 올렸지만 exact partition 54.51→53.89%, pair-F1 88.72→88.41%, overmerge 21.11→23.57%로 악화돼 기각했다. 다획 family boost와 결합한 validation 재선택에서는 동일 test 성능을 내는 더 좁은 gap `0.40`이 선택됐다. 최종 cross 값은 gap `0.40`, threshold `0.692841...`, infix weight `8`이다. ### 다획 family must-link와 geometry 보호 `sum/pi/rightarrow/neq/pm`은 candidate recall이 validation/test 모두 100%였지만 합친 OCR alias가 기존 reliable 목록과 연결되지 않거나 3개의 singleton 점수에 밀렸다. `Sigma/pi-family/arrow-family/neq/pm` 후보에만 추가 symbol signal을 주고 boost `0~6`을 writer-validation에서 선택했다. | 공식 test grouping | 기준 | boost 6 + 보호 | |---|---:|---:| | 다섯 family 전체 | 45.19% | **82.96%** | | `\sum` | 25.81% | **70.97%** | | `\pi` | 40.00% | **90.00%** | | `\rightarrow` | 71.43% | **89.80%** | | `\neq` | 23.08% | **76.92%** | | `\pm` | 25.00% | **75.00%** | 무보호 boost 6은 전체식 exact 57.38%까지 올렸지만 `hookrightarrow`, 좁은 `Sigma`, 세로로 긴 `pm` 오인이 단일기호를 먹었다. Training writer-validation의 실제 geometry 범위에서 arrow `pair_gap_max≤0.50`, `Sigma width_ref≥2.0`, `pm height_ref≤2.0` guard를 만들고 ontology 밖 `hookrightarrow` alias를 제거했다. 보호 후 공식 test exact는 **60.04%**, pair-F1은 **91.07%**, overmerge formula rate는 21.72%다. Gap-only 대비 각각 +5.53%p, +2.35%p이며 overmerge 증가는 +0.61%p로 제한됐다. ### 최종 잔여 segmentation 병목 최종 full selector의 488식 잔여 실패는 exact 293, oversplit 89, overmerge 72, 혼합 34다. 구조별 exact는 sqrt 68.00%, plain 63.95%, subscript 57.01%, superscript 52.75%, fraction 52.10%다. 3획 기호 correct는 34→63개로 늘었고 oversplit은 57→32개로 줄었다. | label | 오류/전체 | 주 실패 | |---|---:|---| | `x` | 38/450 | oversplit 26, overmerge·혼합 12 | | `1` | 36/482 | 주변 획과 overmerge | | `-` | 36/584 | overmerge | | `\sin` physical part | 35/297 | overmerge 15, oversplit 18 | | `\sum` | 9/31 | oversplit 8 | | `i` | 9/41 | oversplit | | `F` | 7/10 | oversplit | | `\div` | 7/11 | oversplit | 따라서 다음 우선순위는 전역 threshold가 아니라 분수·위첨자에서 `1/-/괄호`가 이웃 Tray로 넘어가지 않게 하는 local-baseline must-not-link, 그리고 남은 `F/div/i` 다획 family다. ### Local-baseline 감사와 학습형 boundary 행동 head 최종 selector의 overmerge를 정답 기호 단위로 다시 연결했다. 공식 test에는 299개 overmerge event와 149개 고유 선택 후보가 있었다. 오류가 집중된 정답은 `1` 36건, `-` 36건, 괄호 53건이며 구조별로 superscript 150건, fraction 112건, subscript 96건이 겹쳐 있었다. 기존 fraction penalty는 299건 중 4건에만 신호를 냈다. 두 규칙형 가드는 채택하지 않았다. 1. 합친 OCR이 구성획보다 약할 때 감점하는 component competition은 validation exact를 개선하지 못하고 family grouping을 악화시켜 weight 0을 선택했다. 2. base partition에서 `SUPERSCRIPT/SUBSCRIPT/ABOVE/BELOW/CONTAINS` edge를 만든 local-baseline guard는 validation 3,293개·test 5,061개 후보에 신호를 만들었지만 실제 149개 오병합 후보에는 유효 신호가 0개였다. 이미 오병합된 partition 뒤에서 관계를 만들었기 때문에 내부 경계를 복원할 수 없었다. 이에 grouping 이전의 모든 다획 후보를 대상으로, 둘 이상의 정답 기호를 침범하는지를 예측하는 geometry boundary 행동 head를 fit writer에 학습했다. 72,223개 후보 중 66,842개가 boundary 양성이며, validation에서 threshold 0.5·penalty weight 6을 선택했다. | 지표 | 보호 selector | boundary 행동 head | 변화 | |---|---:|---:|---:| | validation exact partition | 68.00% | **69.82%** | +1.82%p | | validation pair-F1 | 92.98% | **93.64%** | +0.66%p | | official exact partition | 60.04% | **60.25%** | +0.20%p | | official pair-F1 | 91.07% | **91.26%** | +0.19%p | | official overmerge formula | 21.72% | **20.49%** | -1.23%p | | official `x` grouping | 91.56% | 91.33% | -0.22%p | | official 다획 family grouping | 82.96% | 81.48% | -1.48%p | 사전 정의한 `x` -1%p, family -2%p, pair-F1 -0.25%p 보호 gate 안에서 exact와 pair-F1이 함께 올라 R-track head로 채택한다. 다만 CROHME 정답 경계로 학습한 비상업 연구 weight이므로 제품 checkpoint·teacher·distillation에는 사용할 수 없다. 상용 이전 경로는 동일 boundary target을 P-track 연속식으로 재학습하거나 shared trajectory encoder의 auxiliary boundary loss로 옮기는 것이다. 전체 Python 회귀는 271개가 통과했다. ### P-track boundary auxiliary smoke > 2026-07-24 정정: 아래 최초 smoke는 adapter checkpoint의 `shared_state_dict`를 적용하지 않은 base-only 결과이므로 현재 메인 모델 성능 근거에서 철회한다. 실험 이력으로만 보존한다. 별도 sklearn guard를 최종 구조로 고정하지 않고 `MathInk06Model`의 shared 384차 trajectory embedding 위에 선택적 binary boundary head를 추가했다. 기본값은 비활성이라 기존 checkpoint key와 `forward() → exact,family` 계약은 변하지 않는다. 활성 경로만 online/raster candidate별 boundary logit을 반환하며 class imbalance용 binary auxiliary loss를 제공한다. CROHME weight를 이전하지 않고 승인 paired training trajectory의 실제 단일기호를 음성으로, 서로 다른 두 기호를 같은 행·위첨자·아래첨자 위치에 배치한 후보를 양성으로 합성했다. Seed-17 base와 online adapter는 동결하고 head만 학습했다. | P proxy validation | 결과 | |---|---:| | balanced validation samples | 2,400 | | validation accuracy | 97.33% | | validation F1 | 97.34% | | validation ROC-AUC | 99.65% | | validation single-symbol recall | 97.25% | | validation cross-boundary recall | 97.42% | | paired-test single/boundary recall | 96.92% / 98.00% | | exact/family logit max abs 변화 | 0.0 / 0.0 | Validation에서 single≥95%·boundary≥85%를 동시에 요구해 threshold 0.70을 선택했고, 선택에 사용하지 않은 paired writer/device-disjoint proxy test 2,400개에서 accuracy 97.46%, F1 97.47%, AUC 99.62%를 기록했다. 이는 P-track target 재구성이 가능하다는 smoke 증거지만 실제 연속식 writer/device-disjoint 검증은 아니다. 다음 단계는 실제 P 연속식 또는 사용자 local-only pseudo formula에서 symbol boundary annotation을 확보하고 main joint fine-tuning weight를 선택하는 것이다. 전체 Python 회귀는 275개가 통과했다. ### 5-layout P boundary joint — 3 seed > 2026-07-24 정정: 아래 최초 joint 결과도 동일한 shared-state 로딩 누락을 상속했으므로 delta 채택을 철회한다. 공개 배포 대상에서 제거한다. Smoke의 배치를 같은 행·상첨자·하첨자에서 분자/분모 수직 슬롯과 넓은 중위식 양변까지 다섯 종류로 확장했다. Seed별 base·online adapter·boundary head를 독립 초기화하고 다음 module만 열었다. - `trajectory_encoder.blocks.3` - `trajectory_encoder.attention` - `exact_head`, `family_head`, `boundary_head` Authentic 4,800개에는 exact CE·family CE·single-symbol boundary loss를 주고, balanced boundary 후보 2,400개에는 boundary loss를 줬다. Validation 채택 조건은 authentic exact/family 각 -0.5%p 이내, single/boundary recall 각 95% 이상이다. | paired proxy test | seed 17 | seed 31 | seed 47 | 평균 ± 표준편차 | |---|---:|---:|---:|---:| | exact top-1 | 77.66% | 79.38% | 77.84% | **78.29 ± 0.77%p** | | exact 변화 | +3.07%p | +3.20%p | +2.70%p | **+2.99 ± 0.21%p** | | family top-1 | 86.22% | 87.70% | 86.46% | **86.80 ± 0.65%p** | | family 변화 | +2.78%p | +3.54%p | +2.62%p | **+2.98 ± 0.40%p** | | single-symbol recall | 96.75% | 98.25% | 97.25% | **97.42 ± 0.62%p** | | cross-boundary recall | 98.92% | 97.92% | 98.67% | **98.50 ± 0.42%p** | | boundary F1 | 97.86% | 98.08% | 97.97% | **97.97 ± 0.09%p** | 세 seed 모두 validation과 paired test gate를 개별 통과했고 exact/family도 모든 seed에서 개선돼 P formula-layout proxy delta로 채택한다. 이는 R-track head를 distill한 결과가 아니라 승인 P trajectory와 알려진 합성 boundary만 사용한 결과다. 다만 분리된 고립기호를 배치한 proxy이므로 실제 사용자의 연속식 stroke rhythm·co-articulation·device sampling 변동은 포함하지 않는다. 제품 검증은 false이며 다음 필수 gate는 실제 P 연속식 writer/device-disjoint boundary 평가다. ### 3-seed device contract stress 채택 delta를 별도 학습 없이 좌표 jitter, timestamp/speed 전체 결측, raw event 절반 희소화 후 6Hz 재보간, x 1.12/y 0.88 affine 조건에서 평가했다. 최초 sparse 실험은 canonical tensor를 직접 절반 삭제해 입력 계약을 위반했으므로 폐기했고, start/end·pen-up anchor를 보존한 재보간 경로로 다시 고정했다. 모든 seed·stress가 clean 대비 exact/family -3%p 이내, single/boundary recall 90% 이상 gate를 통과했다. 최악 exact 하락은 seed-47 affine `-1.53%p`, 최악 family 하락은 seed-31 affine `-1.48%p`였다. Stress 전체의 single-symbol/cross-boundary recall 최저는 각각 96.67%다. Timestamp 결측과 2배 희소 sampling은 최악 exact 하락이 각각 -0.26%p와 -0.48%p 이내였다. 이는 canonical 입력 계약의 변형 내성 증거이지 실제 Android digitizer·WDDM·battery/latency 검증이 아니다. 다음 실제 gate는 raw MotionEvent를 가진 P writer/device-disjoint 연속식이다. 전체 Python 회귀는 276개가 통과했다. ### Shared-state 로딩 결함 정정 및 3-seed 재학습 정식 배포 구성을 감사한 결과 `online_adapter.pt`는 외부 modality adapter뿐 아니라 공동 학습된 `trajectory_encoder`, `exact_head`, `family_head`의 `shared_state_dict`도 저장하지만, P boundary 로더는 외부 adapter만 읽고 shared update를 누락했다. 올바른 합성 순서는 다음과 같다. ```text base checkpoint → adapter shared_state_dict → modality adapter → 선택적 boundary head → 검증을 통과한 경우에만 joint delta ``` 로더를 수정하고 이를 보장하는 회귀 테스트를 추가한 뒤 auxiliary와 joint를 seed 17·31·47에서 다시 학습했다. | paired proxy test | seed 17 | seed 31 | seed 47 | 평균 | |---|---:|---:|---:|---:| | 정정된 main baseline exact | 82.95% | 83.24% | 82.92% | **83.03%** | | joint exact | 82.71% | 82.95% | 82.79% | 82.81% | | exact 변화 | -0.24%p | -0.29%p | -0.13%p | **-0.22%p** | | 정정된 main baseline family | 91.22% | 91.51% | 91.38% | **91.37%** | | joint family | 90.77% | 90.96% | 90.90% | 90.88% | | family 변화 | -0.45%p | -0.56%p | -0.48%p | **-0.49%p** | | single-symbol recall | 91.58% | 94.67% | 92.25% | 92.83% | | cross-boundary recall | 98.50% | 98.75% | 98.17% | 98.47% | 세 seed 모두 official proxy gate를 실패했다. Seed 17·47은 single-symbol 95% floor를, seed 31은 single-symbol floor와 family -0.5%p 비퇴행 조건을 실패했다. 따라서 corrected joint delta는 전부 기각하며 현재 유효 모델은 `base + shared_state_dict + modality adapter`다. 이전 78.29% 결과는 강한 main encoder를 빠뜨린 낮은 기준선과 비교한 것이므로 무효다. Joint delta 없이 정정된 main과 새 auxiliary head만 대상으로 device stress를 다시 수행했다. Clean exact/family 평균은 각각 83.03%/91.37%다. 모든 software stress가 -3%p와 boundary recall 90% floor를 통과했고, 최악 exact/family 하락은 affine에서 -0.98%p/-1.08%p였다. 이는 auxiliary shadow의 입력 내성만 의미하며 head의 clean single recall이 91.42~94.50%라 release 채택 근거는 아니다. ### Composite torch.export 기존 export script도 base checkpoint만 읽어 실제 main adapter를 우회했다. Online은 `online adapter → shared trajectory encoder`, raster는 `virtual top-4 → raster adapter → shared trajectory encoder`가 그래프 안에 포함되도록 수정했다. Dual adapter는 입력 데이터에 따른 런타임 분기 대신 online/raster branch를 export 시점에 고정한다. Seed 17·31·47 모두 online/raster 각각 대표 입력 76개에서 eager 대비 top-1 100% 일치, 최대 logit 절대오차 0.0으로 strict `torch.export` gate를 통과했다. Seed당 두 graph 합계는 18,657,779 bytes로 25MB 예산 안이다. 다만 `.pt2`는 Android LiteRT artifact가 아니다. 공식 LiteRT Torch 0.9.1은 Linux 환경을 요구하며 WSL 변환 환경 설치 중 WSL filesystem I/O 오류가 발생해 `.tflite` 변환과 runtime parity는 아직 미완료다. ### 실제 P 연속식 데이터 계약 실제 데이터가 도착하기 전에 `AIFlow P Formula v1` JSON Schema와 fail-closed preflight를 추가했다. 각 formula는 다음 근거를 모두 가져야 한다. - 고유 `formula_id`, 원본 추적용 `origin_id` - 비어 있지 않은 `writer_id`, `device_id`, `source_id` - `training|validation|test` split - `rights_track=P`, `commercial_training_allowed=true` - 양수 canvas 크기 - token과 원본 raw stroke가 포함된 정답 symbol group Origin·writer·device·source가 둘 이상의 split에 나타나면 제품 평가를 거부한다. Timestamp와 pressure가 없는 symbol은 삭제하거나 관측값으로 위장하지 않고 missing slice로 센다. 검증된 formula의 실제 symbol group은 boundary 음성, 인접한 두 symbol group 결합은 boundary 양성으로 만든다. 이 계약은 실제 성능값을 만들지는 않지만, 향후 P 입력이 CROHME 정답 group이나 합성 고립기호 proxy와 섞이는 것을 방지한다. 전체 회귀는 282개가 통과했다. ## 산출물 - `src/math_grid_drawer/research/behavior_context06.py` - `src/math_grid_drawer/research/behavior_role_head06.py` - `src/math_grid_drawer/research/math_context_layer.py` - `scripts/train_math_ink_06_behavior_role.py` - `scripts/summarize_math_ink_06_behavior_seeds.py` - `scripts/audit_math_ink_06_behavior_grouping.py` - `scripts/audit_math_ink_06_x_edge_bottleneck.py` - `scripts/sweep_math_ink_06_x_grouping_guard.py` - `scripts/sweep_math_ink_06_cross_gap_guard.py` - `scripts/audit_math_ink_06_multistroke_families.py` - `scripts/sweep_math_ink_06_multistroke_family_guard.py` - `scripts/audit_math_ink_06_multistroke_guard_regressions.py` - `scripts/audit_math_ink_06_local_baseline_overmerge.py` - `scripts/sweep_math_ink_06_component_competition_guard.py` - `scripts/sweep_math_ink_06_local_baseline_guard.py` - `scripts/train_math_ink_06_boundary_behavior_guard.py` - `scripts/train_math_ink_06_p_boundary_auxiliary.py` - `scripts/train_math_ink_06_p_boundary_joint.py` - `scripts/summarize_math_ink_06_p_boundary_joint.py` - `scripts/evaluate_math_ink_06_p_boundary_device_stress.py` - `scripts/export_math_ink_06_litert.py` - `scripts/preflight_math_ink_06_p_formula.py` - `scripts/analyze_crohme_lattice_failures.py` - `tests/test_behavior_context06.py` - `tests/test_behavior_role_head06.py` - `tests/test_p_formula_gate06.py` - `research/contracts/aiflow_p_formula_v1.schema.json` - `research/AIFlow-MATH-INK-0.6-BEHAVIOR-CONTEXT-REPORT-20260724.md` - `research/runs/math_ink_06_behavior_role_3seed_20260724/run_summary.json` - `research/runs/math_ink_06_behavior_grouping_audit_20260724/report.json` - `research/runs/math_ink_06_behavior_grouping_cross_joint_audit_20260724/report.json` - `research/runs/math_ink_06_p_boundary_joint_sharedfix_3seed_20260724/run_summary.json` - `research/runs/math_ink_06_p_boundary_device_stress_sharedfix_3seed_20260724` - `research/runs/math_ink_06_composite_export_seed17_20260724/export_manifest.json` - `research/runs/math_ink_06_x_grouping_guard_20260724/report.json` - `research/runs/math_ink_06_x_edge_bottleneck_20260724/report.json` - `research/runs/math_ink_06_cross_gap_guard_full_selector_20260724/report.json` - `research/runs/math_ink_06_behavior_grouping_full_selector_gap50_20260724/report.json` - `research/runs/math_ink_06_x_grouping_guard_gap50_full_selector_20260724/report.json` - `research/runs/math_ink_06_full_selector_gap50_failure_audit_20260724/report.json` - `research/runs/math_ink_06_cross_gap_guard_family6_full_selector_20260724/report.json` - `research/runs/math_ink_06_multistroke_family_guard_geometry_protected_20260724/report.json` - `research/runs/math_ink_06_multistroke_guard_regression_audit_20260724/report.json` - `research/runs/math_ink_06_full_selector_gap40_family6_protected_failure_audit_20260724/report.json` - `research/runs/math_ink_06_behavior_grouping_full_selector_gap40_family6_protected_20260724/report.json` - `research/runs/math_ink_06_component_competition_guard_20260724/report.json` - `research/runs/math_ink_06_local_baseline_guard_20260724/report.json` - `research/runs/math_ink_06_local_baseline_overmerge_audit_20260724/report.json` - `research/runs/math_ink_06_boundary_behavior_guard_20260724/report.json` - `research/runs/math_ink_06_p_boundary_auxiliary_smoke_20260724/report.json` - `research/runs/math_ink_06_p_boundary_auxiliary_fullsmoke_20260724/report.json` - `research/runs/math_ink_06_p_boundary_joint_3seed_20260724/run_summary.json` - `research/runs/math_ink_06_p_boundary_device_stress_3seed_20260724/report.json`