Fix Python composite runtime and atomic export
Browse files- MANIFEST.json +14 -14
- README.md +20 -8
- colab/aiflow_math_ink_06_litert_bundle.manifest.json +7 -7
- colab/aiflow_math_ink_06_litert_bundle.zip +2 -2
- reports/RESEARCH_REPORT.md +4 -2
- scripts/export_math_ink_06_litert.py +3 -21
- src/math_ink_06.py +83 -3
MANIFEST.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
{
|
| 2 |
-
"schema": "aiflow-hf-research-snapshot-
|
| 3 |
-
"generated_at": "2026-07-23T20:
|
| 4 |
"track": "R_noncommercial_plus_rejected_P_proxy",
|
| 5 |
"product_validation": false,
|
| 6 |
"public_release": true,
|
| 7 |
"contains_raw_dataset": false,
|
| 8 |
-
"tests": "
|
| 9 |
"retracted_paths": [
|
| 10 |
"models/auxiliary/boundary_auxiliary_head.pt",
|
| 11 |
"models/auxiliary/seed17/boundary_joint_delta.pt",
|
|
@@ -30,8 +30,8 @@
|
|
| 30 |
},
|
| 31 |
{
|
| 32 |
"path": "reports/RESEARCH_REPORT.md",
|
| 33 |
-
"bytes":
|
| 34 |
-
"sha256": "
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"path": "scripts/evaluate_crohme_tray_joint_selector.py",
|
|
@@ -90,8 +90,8 @@
|
|
| 90 |
},
|
| 91 |
{
|
| 92 |
"path": "src/math_ink_06.py",
|
| 93 |
-
"bytes":
|
| 94 |
-
"sha256": "
|
| 95 |
},
|
| 96 |
{
|
| 97 |
"path": "src/ink06_export.py",
|
|
@@ -135,8 +135,8 @@
|
|
| 135 |
},
|
| 136 |
{
|
| 137 |
"path": "scripts/export_math_ink_06_litert.py",
|
| 138 |
-
"bytes":
|
| 139 |
-
"sha256": "
|
| 140 |
},
|
| 141 |
{
|
| 142 |
"path": "scripts/evaluate_math_ink_06_p_boundary_device_stress.py",
|
|
@@ -170,13 +170,13 @@
|
|
| 170 |
},
|
| 171 |
{
|
| 172 |
"path": "colab/aiflow_math_ink_06_litert_bundle.zip",
|
| 173 |
-
"bytes":
|
| 174 |
-
"sha256": "
|
| 175 |
},
|
| 176 |
{
|
| 177 |
"path": "colab/aiflow_math_ink_06_litert_bundle.manifest.json",
|
| 178 |
"bytes": 2978,
|
| 179 |
-
"sha256": "
|
| 180 |
},
|
| 181 |
{
|
| 182 |
"path": "exports/seed17/export_manifest.json",
|
|
@@ -220,8 +220,8 @@
|
|
| 220 |
},
|
| 221 |
{
|
| 222 |
"path": "README.md",
|
| 223 |
-
"bytes":
|
| 224 |
-
"sha256": "
|
| 225 |
},
|
| 226 |
{
|
| 227 |
"path": "assets/stroke_encoding.svg",
|
|
|
|
| 1 |
{
|
| 2 |
+
"schema": "aiflow-hf-research-snapshot-v10",
|
| 3 |
+
"generated_at": "2026-07-23T20:20:53.2135890Z",
|
| 4 |
"track": "R_noncommercial_plus_rejected_P_proxy",
|
| 5 |
"product_validation": false,
|
| 6 |
"public_release": true,
|
| 7 |
"contains_raw_dataset": false,
|
| 8 |
+
"tests": "285 passed",
|
| 9 |
"retracted_paths": [
|
| 10 |
"models/auxiliary/boundary_auxiliary_head.pt",
|
| 11 |
"models/auxiliary/seed17/boundary_joint_delta.pt",
|
|
|
|
| 30 |
},
|
| 31 |
{
|
| 32 |
"path": "reports/RESEARCH_REPORT.md",
|
| 33 |
+
"bytes": 34631,
|
| 34 |
+
"sha256": "46946707f85cd6860161e301c21970b885dfd742c122eb9d9f0f44c3173c208b"
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"path": "scripts/evaluate_crohme_tray_joint_selector.py",
|
|
|
|
| 90 |
},
|
| 91 |
{
|
| 92 |
"path": "src/math_ink_06.py",
|
| 93 |
+
"bytes": 58690,
|
| 94 |
+
"sha256": "657a720f32bfe8548ec859986bc3b6bcafbf3ec70f0abfdbf50127946c655052"
|
| 95 |
},
|
| 96 |
{
|
| 97 |
"path": "src/ink06_export.py",
|
|
|
|
| 135 |
},
|
| 136 |
{
|
| 137 |
"path": "scripts/export_math_ink_06_litert.py",
|
| 138 |
+
"bytes": 11027,
|
| 139 |
+
"sha256": "68510ea84f778bc415c3b5b29e3445cd0c5102a5febb1bb126c92262c75ccebb"
|
| 140 |
},
|
| 141 |
{
|
| 142 |
"path": "scripts/evaluate_math_ink_06_p_boundary_device_stress.py",
|
|
|
|
| 170 |
},
|
| 171 |
{
|
| 172 |
"path": "colab/aiflow_math_ink_06_litert_bundle.zip",
|
| 173 |
+
"bytes": 17276876,
|
| 174 |
+
"sha256": "d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47"
|
| 175 |
},
|
| 176 |
{
|
| 177 |
"path": "colab/aiflow_math_ink_06_litert_bundle.manifest.json",
|
| 178 |
"bytes": 2978,
|
| 179 |
+
"sha256": "d6876734bbb23394b40c3b484e2c44d9e06cf3f67de77d3f50205e554b41f758"
|
| 180 |
},
|
| 181 |
{
|
| 182 |
"path": "exports/seed17/export_manifest.json",
|
|
|
|
| 220 |
},
|
| 221 |
{
|
| 222 |
"path": "README.md",
|
| 223 |
+
"bytes": 11437,
|
| 224 |
+
"sha256": "5a160ef5bb7753ad36fb39f692aecb3171c243743d8fdd4f64def16a3714af42"
|
| 225 |
},
|
| 226 |
{
|
| 227 |
"path": "assets/stroke_encoding.svg",
|
README.md
CHANGED
|
@@ -95,11 +95,6 @@ models/
|
|
| 95 |
...
|
| 96 |
seed47/
|
| 97 |
...
|
| 98 |
-
auxiliary/
|
| 99 |
-
boundary_auxiliary_head.pt
|
| 100 |
-
seed17/boundary_joint_delta.pt
|
| 101 |
-
seed31/boundary_joint_delta.pt
|
| 102 |
-
seed47/boundary_joint_delta.pt
|
| 103 |
artifacts/
|
| 104 |
boundary_behavior_guard.joblib
|
| 105 |
```
|
|
@@ -110,8 +105,6 @@ artifacts/
|
|
| 110 |
| `online_adapter.pt` | 실제 온라인 stroke 보정 | `dual_tcn_v3`, `top4-skeleton-128x19` |
|
| 111 |
| `behavior_role_head.pt` | `x/X/×` 역할 문맥 | 128×19 stroke + 49 context feature |
|
| 112 |
| `boundary_behavior_guard.joblib` | 잘못된 다기호 병합 억제 | geometry 17 feature, threshold 0.5, weight 6 |
|
| 113 |
-
| `boundary_auxiliary_head.pt` | shared embedding용 P-track boundary smoke | 384→1, main exact/family head 비퇴행 |
|
| 114 |
-
| `boundary_joint_delta.pt` | seed별 제한 joint fine-tuning delta | last TCN + attention + exact/family/boundary heads |
|
| 115 |
|
| 116 |
세 seed teacher를 그대로 모바일에 넣는 것이 최종 목표는 아니다. release 경로는 seed ensemble을 하나의 student로 distillation한 뒤 LiteRT INT8/FP16을 비교하는 것이다.
|
| 117 |
|
|
@@ -136,7 +129,26 @@ print(adapter["adapter_architecture"], adapter["feature_contract"])
|
|
| 136 |
print(role["role_labels"], role["context_features"])
|
| 137 |
```
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
## 현재 성능
|
| 142 |
|
|
|
|
| 95 |
...
|
| 96 |
seed47/
|
| 97 |
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
artifacts/
|
| 99 |
boundary_behavior_guard.joblib
|
| 100 |
```
|
|
|
|
| 105 |
| `online_adapter.pt` | 실제 온라인 stroke 보정 | `dual_tcn_v3`, `top4-skeleton-128x19` |
|
| 106 |
| `behavior_role_head.pt` | `x/X/×` 역할 문맥 | 128×19 stroke + 49 context feature |
|
| 107 |
| `boundary_behavior_guard.joblib` | 잘못된 다기호 병합 억제 | geometry 17 feature, threshold 0.5, weight 6 |
|
|
|
|
|
|
|
| 108 |
|
| 109 |
세 seed teacher를 그대로 모바일에 넣는 것이 최종 목표는 아니다. release 경로는 seed ensemble을 하나의 student로 distillation한 뒤 LiteRT INT8/FP16을 비교하는 것이다.
|
| 110 |
|
|
|
|
| 129 |
print(role["role_labels"], role["context_features"])
|
| 130 |
```
|
| 131 |
|
| 132 |
+
실제 Python composite 추론은 Colab bundle을 푼 디렉터리에서 다음처럼 실행한다. Adapter를 생략하면 정정된 메인 모델이 아니라 base-only 경로가 되므로 반드시 함께 전달한다.
|
| 133 |
+
|
| 134 |
+
```python
|
| 135 |
+
from pathlib import Path
|
| 136 |
+
from math_grid_drawer.research.math_ink_06 import MathInk06Engine
|
| 137 |
+
|
| 138 |
+
engine = MathInk06Engine(
|
| 139 |
+
Path("artifacts/base_378.pt"),
|
| 140 |
+
adapter_checkpoint=Path("artifacts/online_adapter.pt"),
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
result = engine.recognize_online(
|
| 144 |
+
strokes,
|
| 145 |
+
canvas_width=128,
|
| 146 |
+
canvas_height=128,
|
| 147 |
+
top_k=5,
|
| 148 |
+
)
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Checkpoint 내부 경로는 lineage 기록이며 로컬 절대경로에 의존해 추론하지 않는다.
|
| 152 |
|
| 153 |
## 현재 성능
|
| 154 |
|
colab/aiflow_math_ink_06_litert_bundle.manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
|
| 3 |
-
"generated_at": "2026-07-23T20:
|
| 4 |
"seed": 17,
|
| 5 |
"litert_torch_version": "0.9.1",
|
| 6 |
"representative_samples": 76,
|
|
@@ -32,8 +32,8 @@
|
|
| 32 |
},
|
| 33 |
{
|
| 34 |
"path": "src/math_grid_drawer/research/math_ink_06.py",
|
| 35 |
-
"bytes":
|
| 36 |
-
"sha256": "
|
| 37 |
},
|
| 38 |
{
|
| 39 |
"path": "src/math_grid_drawer/research/raster_skeleton06.py",
|
|
@@ -52,8 +52,8 @@
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"path": "scripts/export_math_ink_06_litert.py",
|
| 55 |
-
"bytes":
|
| 56 |
-
"sha256": "
|
| 57 |
},
|
| 58 |
{
|
| 59 |
"path": "artifacts/base_378.pt",
|
|
@@ -73,8 +73,8 @@
|
|
| 73 |
],
|
| 74 |
"product_validation": false,
|
| 75 |
"bundle": "research\\runs\\math_ink_06_litert_colab_20260724\\aiflow_math_ink_06_litert_bundle.zip",
|
| 76 |
-
"bundle_bytes":
|
| 77 |
-
"bundle_sha256": "
|
| 78 |
"verification": {
|
| 79 |
"schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
|
| 80 |
"files": 13,
|
|
|
|
| 1 |
{
|
| 2 |
"schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
|
| 3 |
+
"generated_at": "2026-07-23T20:19:13.374433+00:00",
|
| 4 |
"seed": 17,
|
| 5 |
"litert_torch_version": "0.9.1",
|
| 6 |
"representative_samples": 76,
|
|
|
|
| 32 |
},
|
| 33 |
{
|
| 34 |
"path": "src/math_grid_drawer/research/math_ink_06.py",
|
| 35 |
+
"bytes": 58690,
|
| 36 |
+
"sha256": "657a720f32bfe8548ec859986bc3b6bcafbf3ec70f0abfdbf50127946c655052"
|
| 37 |
},
|
| 38 |
{
|
| 39 |
"path": "src/math_grid_drawer/research/raster_skeleton06.py",
|
|
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"path": "scripts/export_math_ink_06_litert.py",
|
| 55 |
+
"bytes": 11027,
|
| 56 |
+
"sha256": "68510ea84f778bc415c3b5b29e3445cd0c5102a5febb1bb126c92262c75ccebb"
|
| 57 |
},
|
| 58 |
{
|
| 59 |
"path": "artifacts/base_378.pt",
|
|
|
|
| 73 |
],
|
| 74 |
"product_validation": false,
|
| 75 |
"bundle": "research\\runs\\math_ink_06_litert_colab_20260724\\aiflow_math_ink_06_litert_bundle.zip",
|
| 76 |
+
"bundle_bytes": 17276876,
|
| 77 |
+
"bundle_sha256": "d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47",
|
| 78 |
"verification": {
|
| 79 |
"schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
|
| 80 |
"files": 13,
|
colab/aiflow_math_ink_06_litert_bundle.zip
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d5357732c9f1654d65664e3fe23acab88a7374e88e3f2bd5b60ab46120aefc47
|
| 3 |
+
size 17276876
|
reports/RESEARCH_REPORT.md
CHANGED
|
@@ -409,7 +409,9 @@ Joint delta 없이 정정된 main과 새 auxiliary head만 대상으로 device s
|
|
| 409 |
|
| 410 |
Seed 17·31·47 모두 online/raster 각각 대표 입력 76개에서 eager 대비 top-1 100% 일치, 최대 logit 절대오차 0.0으로 strict `torch.export` gate를 통과했다. 기존 출력 경로를 재사용할 때 오래된 `.pt2`가 남는 문제를 발견해 `.part → atomic replace`로 수정했고, fresh seed-17 두 graph 합계는 24,302,579 bytes로 25MiB 예산 안이다. 다만 `.pt2`는 Android LiteRT artifact가 아니다. 공식 LiteRT Torch 0.9.1은 Linux 환경을 요구하며 WSL 변환 환경 설치 중 WSL filesystem I/O 오류가 발생해 `.tflite` 변환과 runtime parity는 아직 미완료다.
|
| 411 |
|
| 412 |
-
Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster 대표 입력 76개를 5,721,772-byte cache로 고정했다. 최소 bundle은 composite base·adapter, 필요한 source
|
|
|
|
|
|
|
| 413 |
|
| 414 |
### 실제 P 연속식 데이터 계약
|
| 415 |
|
|
@@ -422,7 +424,7 @@ Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster
|
|
| 422 |
- 양수 canvas 크기
|
| 423 |
- token과 원본 raw stroke가 포함된 정답 symbol group
|
| 424 |
|
| 425 |
-
Origin·writer·device·source가 둘 이상의 split에 나타나면 제품 평가를 거부한다. Timestamp와 pressure가 없는 symbol은 삭제하거나 관측값으로 위장하지 않고 missing slice로 센다. 검증된 formula의 실제 symbol group은 boundary 음성, 인접한 두 symbol group 결합은 boundary 양성으로 만든다. 이 계약은 실제 성능값을 만들지는 않지만, 향후 P 입력이 CROHME 정답 group이나 합성 고립기호 proxy와 섞이는 것을 방지한다. 전체 회귀는
|
| 426 |
|
| 427 |
## 산출물
|
| 428 |
|
|
|
|
| 409 |
|
| 410 |
Seed 17·31·47 모두 online/raster 각각 대표 입력 76개에서 eager 대비 top-1 100% 일치, 최대 logit 절대오차 0.0으로 strict `torch.export` gate를 통과했다. 기존 출력 경로를 재사용할 때 오래된 `.pt2`가 남는 문제를 발견해 `.part → atomic replace`로 수정했고, fresh seed-17 두 graph 합계는 24,302,579 bytes로 25MiB 예산 안이다. 다만 `.pt2`는 Android LiteRT artifact가 아니다. 공식 LiteRT Torch 0.9.1은 Linux 환경을 요구하며 WSL 변환 환경 설치 중 WSL filesystem I/O 오류가 발생해 `.tflite` 변환과 runtime parity는 아직 미완료다.
|
| 411 |
|
| 412 |
+
Linux/Colab 변환을 위해 label이 없는 실제 HWRT-derived online/raster 대표 입력 76개를 5,721,772-byte cache로 고정했다. 최소 bundle은 composite base·adapter, 필요한 source, converter와 대표 cache만 포함하며 13개 entry·17,276,876 bytes다. Bundle 내부 SHA-256 전체 검증과 별도 디렉터리 추출 후 composite export smoke를 통과했다. Colab notebook은 공식 `litert-torch==0.9.1` 설치, 공개 Hugging Face bundle 다운로드, 해시 검증, online/raster `.tflite` 변환, 76개 전체 top-1 100%·logit 오차≤0.02·합계≤25MiB gate를 자동 실행한다.
|
| 413 |
+
|
| 414 |
+
Python 공개 런타임의 `MathInk06Engine`도 base-only였으므로 `adapter_checkpoint`를 공식 생성자 인자로 추가했다. 엔진은 base→adapter `shared_state_dict`→dual modality adapter 순서로 구성하고 online은 `adapter.online`, raster virtual trajectory는 `adapter.raster`를 거친다. 실제 seed-17 representative 입력에서 엔진과 export wrapper의 online/raster 최대 logit 오차는 모두 0.0이었다. Adapter를 생략한 호출은 호환용 base-only 경로로만 남긴다.
|
| 415 |
|
| 416 |
### 실제 P 연속식 데이터 계약
|
| 417 |
|
|
|
|
| 424 |
- 양수 canvas 크기
|
| 425 |
- token과 원본 raw stroke가 포함된 정답 symbol group
|
| 426 |
|
| 427 |
+
Origin·writer·device·source가 둘 이상의 split에 나타나면 제품 평가를 거부한다. Timestamp와 pressure가 없는 symbol은 삭제하거나 관측값으로 위장하지 않고 missing slice로 센다. 검증된 formula의 실제 symbol group은 boundary 음성, 인접한 두 symbol group 결합은 boundary 양성으로 만든다. 이 계약은 실제 성능값을 만들지는 않지만, 향후 P 입력이 CROHME 정답 group이나 합성 고립기호 proxy와 섞이는 것을 방지한다. 전체 회귀는 285개가 통과했다.
|
| 428 |
|
| 429 |
## 산출물
|
| 430 |
|
scripts/export_math_ink_06_litert.py
CHANGED
|
@@ -21,10 +21,7 @@ from math_grid_drawer.research.ink06_export import (
|
|
| 21 |
OnlineExportWrapper06, RasterExportWrapper06, exported_equivalence06,
|
| 22 |
)
|
| 23 |
from math_grid_drawer.research.math_ink_06 import MathInk06Engine
|
| 24 |
-
from math_grid_drawer.research.skeleton_adapter06 import
|
| 25 |
-
DualModalityTrajectoryAdapter06, SkeletonTrajectoryAdapter06,
|
| 26 |
-
)
|
| 27 |
-
from math_grid_drawer.research.math_ink_06 import VirtualTrajectoryAdapter06
|
| 28 |
|
| 29 |
|
| 30 |
def _representative_inputs(baseline_report: Path, data_path: Path) -> tuple[list[tuple[torch.Tensor, ...]], list[tuple[torch.Tensor, ...]]]:
|
|
@@ -127,24 +124,9 @@ def _load_composite06(
|
|
| 127 |
) -> tuple[MathInk06Engine, torch.nn.Module, dict]:
|
| 128 |
"""필요 변수: base·adapter checkpoint. 작동 원리: base→shared state→modality adapter 순서로 배포 모델을 합성한다."""
|
| 129 |
|
| 130 |
-
engine = MathInk06Engine(checkpoint)
|
| 131 |
payload = torch.load(adapter_checkpoint, map_location="cpu", weights_only=False)
|
| 132 |
-
|
| 133 |
-
if architecture == "local_v1":
|
| 134 |
-
adapter: torch.nn.Module = VirtualTrajectoryAdapter06()
|
| 135 |
-
elif architecture == "tcn_v2":
|
| 136 |
-
adapter = SkeletonTrajectoryAdapter06()
|
| 137 |
-
elif architecture == "dual_tcn_v3":
|
| 138 |
-
adapter = DualModalityTrajectoryAdapter06()
|
| 139 |
-
else:
|
| 140 |
-
raise ValueError(f"지원하지 않는 adapter architecture입니다: {architecture}")
|
| 141 |
-
adapter.load_state_dict(payload["state_dict"])
|
| 142 |
-
shared_state = payload.get("shared_state_dict") or {}
|
| 143 |
-
if shared_state:
|
| 144 |
-
incompatible = engine.model.load_state_dict(shared_state, strict=False)
|
| 145 |
-
if incompatible.unexpected_keys:
|
| 146 |
-
raise ValueError(f"adapter shared state key 오류: {incompatible.unexpected_keys}")
|
| 147 |
-
return engine, adapter.eval(), payload
|
| 148 |
|
| 149 |
|
| 150 |
def _adapter_branches06(adapter: torch.nn.Module) -> tuple[torch.nn.Module, torch.nn.Module]:
|
|
|
|
| 21 |
OnlineExportWrapper06, RasterExportWrapper06, exported_equivalence06,
|
| 22 |
)
|
| 23 |
from math_grid_drawer.research.math_ink_06 import MathInk06Engine
|
| 24 |
+
from math_grid_drawer.research.skeleton_adapter06 import DualModalityTrajectoryAdapter06
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
def _representative_inputs(baseline_report: Path, data_path: Path) -> tuple[list[tuple[torch.Tensor, ...]], list[tuple[torch.Tensor, ...]]]:
|
|
|
|
| 124 |
) -> tuple[MathInk06Engine, torch.nn.Module, dict]:
|
| 125 |
"""필요 변수: base·adapter checkpoint. 작동 원리: base→shared state→modality adapter 순서로 배포 모델을 합성한다."""
|
| 126 |
|
| 127 |
+
engine = MathInk06Engine(checkpoint, adapter_checkpoint=adapter_checkpoint)
|
| 128 |
payload = torch.load(adapter_checkpoint, map_location="cpu", weights_only=False)
|
| 129 |
+
return engine, engine.composite_adapter, payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
def _adapter_branches06(adapter: torch.nn.Module) -> tuple[torch.nn.Module, torch.nn.Module]:
|
src/math_ink_06.py
CHANGED
|
@@ -874,7 +874,11 @@ class SymbolCandidate06:
|
|
| 874 |
class MathInk06Engine:
|
| 875 |
"""필요 변수: 0.6 checkpoint. 작동 원리: 원본 stroke 또는 raster에서 텍스트 후보만 반환한다."""
|
| 876 |
|
| 877 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
payload = torch.load(checkpoint, map_location=device, weights_only=False)
|
| 879 |
self.labels = tuple(str(value) for value in payload["exact_labels"])
|
| 880 |
self.family_labels = tuple(str(value) for value in payload["family_labels"])
|
|
@@ -896,6 +900,46 @@ class MathInk06Engine:
|
|
| 896 |
"symmetry_weight": 0.0, "symmetry_mode": "logsumexp",
|
| 897 |
**dict(payload.get("raster_fusion", {})),
|
| 898 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
family_to_index = {label: index for index, label in enumerate(self.family_labels)}
|
| 900 |
# checkpoint가 가진 ontology와 동일한 exact→family 사상을 기기 내 상수 tensor로 유지한다.
|
| 901 |
from .trajectory_sequence import shape_family
|
|
@@ -933,6 +977,40 @@ class MathInk06Engine:
|
|
| 933 |
contribution = joint.gather(2, predicted[:, None, None].expand(-1, joint.shape[1], 1)).squeeze(-1)
|
| 934 |
return fused, contribution.argmax(dim=1)
|
| 935 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 936 |
def _result(self, logits: Tensor, started: float, top_k: int) -> dict[str, Any]:
|
| 937 |
"""필요 변수: fused logits·시작시각·k. 작동 원리: stroke/image 없이 모바일 공개 SymbolResult를 만든다."""
|
| 938 |
|
|
@@ -952,7 +1030,7 @@ class MathInk06Engine:
|
|
| 952 |
ink = canonicalize_ink06(strokes, canvas_width=canvas_width, canvas_height=canvas_height)
|
| 953 |
sequence = torch.from_numpy(ink.features).unsqueeze(0).to(self.device)
|
| 954 |
with torch.inference_mode():
|
| 955 |
-
exact, _family = self.model.forward_online(sequence)
|
| 956 |
return self._result(exact, started, top_k)
|
| 957 |
|
| 958 |
def recognize_raster(self, image: Image.Image, *, top_k: int = 5, debug: bool = False) -> dict[str, Any]:
|
|
@@ -962,7 +1040,9 @@ class MathInk06Engine:
|
|
| 962 |
normalized = image.convert("L").resize((128, 128), Image.Resampling.LANCZOS)
|
| 963 |
raster = 1.0 - torch.from_numpy(np.asarray(normalized, dtype=np.float32) / 255.0)
|
| 964 |
with torch.inference_mode():
|
| 965 |
-
output = self.
|
|
|
|
|
|
|
| 966 |
symmetry_weight = float(self.raster_fusion["symmetry_weight"])
|
| 967 |
if symmetry_weight:
|
| 968 |
symmetry_exact, symmetry_family = raster_symmetry_logits06(
|
|
|
|
| 874 |
class MathInk06Engine:
|
| 875 |
"""필요 변수: 0.6 checkpoint. 작동 원리: 원본 stroke 또는 raster에서 텍스트 후보만 반환한다."""
|
| 876 |
|
| 877 |
+
def __init__(
|
| 878 |
+
self, checkpoint: Path, *, adapter_checkpoint: Path | None = None, device: str = "cpu",
|
| 879 |
+
) -> None:
|
| 880 |
+
"""필요 변수: base와 선택 composite adapter. 작동 원리: base→shared state→modality adapter 순서로 런타임을 구성한다."""
|
| 881 |
+
|
| 882 |
payload = torch.load(checkpoint, map_location=device, weights_only=False)
|
| 883 |
self.labels = tuple(str(value) for value in payload["exact_labels"])
|
| 884 |
self.family_labels = tuple(str(value) for value in payload["family_labels"])
|
|
|
|
| 900 |
"symmetry_weight": 0.0, "symmetry_mode": "logsumexp",
|
| 901 |
**dict(payload.get("raster_fusion", {})),
|
| 902 |
}
|
| 903 |
+
self.composite_adapter: nn.Module = nn.Identity()
|
| 904 |
+
self.online_adapter: nn.Module = nn.Identity()
|
| 905 |
+
self.raster_adapter: nn.Module = nn.Identity()
|
| 906 |
+
if adapter_checkpoint is not None:
|
| 907 |
+
adapter_payload = torch.load(
|
| 908 |
+
adapter_checkpoint, map_location=device, weights_only=False,
|
| 909 |
+
)
|
| 910 |
+
shared_state = adapter_payload.get("shared_state_dict") or {}
|
| 911 |
+
if shared_state:
|
| 912 |
+
incompatible = self.model.load_state_dict(shared_state, strict=False)
|
| 913 |
+
if incompatible.unexpected_keys:
|
| 914 |
+
raise ValueError(
|
| 915 |
+
f"adapter shared state key 오류: {incompatible.unexpected_keys}"
|
| 916 |
+
)
|
| 917 |
+
architecture = str(adapter_payload["adapter_architecture"])
|
| 918 |
+
if architecture == "local_v1":
|
| 919 |
+
adapter: nn.Module = VirtualTrajectoryAdapter06()
|
| 920 |
+
else:
|
| 921 |
+
from .skeleton_adapter06 import (
|
| 922 |
+
DualModalityTrajectoryAdapter06, SkeletonTrajectoryAdapter06,
|
| 923 |
+
)
|
| 924 |
+
if architecture == "tcn_v2":
|
| 925 |
+
adapter = SkeletonTrajectoryAdapter06()
|
| 926 |
+
elif architecture == "dual_tcn_v3":
|
| 927 |
+
adapter = DualModalityTrajectoryAdapter06()
|
| 928 |
+
else:
|
| 929 |
+
raise ValueError(
|
| 930 |
+
f"지원하지 않는 adapter architecture입니다: {architecture}"
|
| 931 |
+
)
|
| 932 |
+
adapter.load_state_dict(adapter_payload["state_dict"])
|
| 933 |
+
adapter = adapter.to(self.device).eval()
|
| 934 |
+
self.composite_adapter = adapter
|
| 935 |
+
if architecture == "dual_tcn_v3":
|
| 936 |
+
self.online_adapter = adapter.online # type: ignore[attr-defined]
|
| 937 |
+
self.raster_adapter = adapter.raster # type: ignore[attr-defined]
|
| 938 |
+
else:
|
| 939 |
+
self.online_adapter = self.raster_adapter = adapter
|
| 940 |
+
self.model_version = (
|
| 941 |
+
f"{self.model_version}+{adapter_payload.get('model_version', architecture)}"
|
| 942 |
+
)
|
| 943 |
family_to_index = {label: index for index, label in enumerate(self.family_labels)}
|
| 944 |
# checkpoint가 가진 ontology와 동일한 exact→family 사상을 기기 내 상수 tensor로 유지한다.
|
| 945 |
from .trajectory_sequence import shape_family
|
|
|
|
| 977 |
contribution = joint.gather(2, predicted[:, None, None].expand(-1, joint.shape[1], 1)).squeeze(-1)
|
| 978 |
return fused, contribution.argmax(dim=1)
|
| 979 |
|
| 980 |
+
def _forward_raster_composite06(self, raster: Tensor) -> dict[str, Tensor]:
|
| 981 |
+
"""필요 변수: 정규화 raster. 작동 원리: virtual top-4를 외부 raster adapter까지 거쳐 shared head로 분류한다."""
|
| 982 |
+
|
| 983 |
+
if isinstance(self.raster_adapter, nn.Identity):
|
| 984 |
+
return self.model.forward_raster(raster)
|
| 985 |
+
coordinates, states, progress, hypothesis_scores = self.model.decode_raster_trajectories(raster)
|
| 986 |
+
features = virtual_features06(
|
| 987 |
+
coordinates, states,
|
| 988 |
+
None if self.model.raster_architecture == "spatial_flat_v1" else progress,
|
| 989 |
+
contract=self.model.virtual_contract,
|
| 990 |
+
)
|
| 991 |
+
batch, hypotheses, steps, channels = features.shape
|
| 992 |
+
if self.model.use_virtual_adapter:
|
| 993 |
+
raw_features = features
|
| 994 |
+
internal = self.model.virtual_adapter(
|
| 995 |
+
features.reshape(batch * hypotheses, steps, channels),
|
| 996 |
+
).reshape(batch, hypotheses, steps, channels)
|
| 997 |
+
features = (
|
| 998 |
+
raw_features
|
| 999 |
+
+ self.model.virtual_adapter_weight * (internal - raw_features)
|
| 1000 |
+
)
|
| 1001 |
+
flat = self.raster_adapter(
|
| 1002 |
+
features.reshape(batch * hypotheses, steps, channels),
|
| 1003 |
+
)
|
| 1004 |
+
exact, family = self.model.classify_trajectory(flat)
|
| 1005 |
+
return {
|
| 1006 |
+
"coordinates": coordinates,
|
| 1007 |
+
"state_logits": states,
|
| 1008 |
+
"stroke_progress": progress,
|
| 1009 |
+
"hypothesis_scores": hypothesis_scores,
|
| 1010 |
+
"exact_logits": exact.reshape(batch, hypotheses, -1),
|
| 1011 |
+
"family_logits": family.reshape(batch, hypotheses, -1),
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
def _result(self, logits: Tensor, started: float, top_k: int) -> dict[str, Any]:
|
| 1015 |
"""필요 변수: fused logits·시작시각·k. 작동 원리: stroke/image 없이 모바일 공개 SymbolResult를 만든다."""
|
| 1016 |
|
|
|
|
| 1030 |
ink = canonicalize_ink06(strokes, canvas_width=canvas_width, canvas_height=canvas_height)
|
| 1031 |
sequence = torch.from_numpy(ink.features).unsqueeze(0).to(self.device)
|
| 1032 |
with torch.inference_mode():
|
| 1033 |
+
exact, _family = self.model.forward_online(self.online_adapter(sequence))
|
| 1034 |
return self._result(exact, started, top_k)
|
| 1035 |
|
| 1036 |
def recognize_raster(self, image: Image.Image, *, top_k: int = 5, debug: bool = False) -> dict[str, Any]:
|
|
|
|
| 1040 |
normalized = image.convert("L").resize((128, 128), Image.Resampling.LANCZOS)
|
| 1041 |
raster = 1.0 - torch.from_numpy(np.asarray(normalized, dtype=np.float32) / 255.0)
|
| 1042 |
with torch.inference_mode():
|
| 1043 |
+
output = self._forward_raster_composite06(
|
| 1044 |
+
raster.view(1, 1, 128, 128).to(self.device),
|
| 1045 |
+
)
|
| 1046 |
symmetry_weight = float(self.raster_fusion["symmetry_weight"])
|
| 1047 |
if symmetry_weight:
|
| 1048 |
symmetry_exact, symmetry_family = raster_symmetry_logits06(
|