| """Math Ink 0.6์ online/raster ๊ฒฝ๋ก๋ฅผ torch.export์ LiteRT ์นํ ์ถ๋ ฅ์ผ๋ก ๊ณ ์ ํ๋ค.""" |
|
|
| from __future__ import annotations |
|
|
| from typing import Iterable |
|
|
| import torch |
| from torch import Tensor, nn |
|
|
| from .math_ink_06 import MathInk06Model, fuse_raster_logits06, virtual_features06 |
|
|
|
|
| class OnlineExportWrapper06(nn.Module): |
| """ํ์ ๋ณ์: 0.6 ๋ชจ๋ธยทonline adapter. ์๋ ์๋ฆฌ: ์ค์ composite ๊ฒฝ๋ก์ exact/family logits๋ฅผ ๋ฐํํ๋ค.""" |
|
|
| def __init__( |
| self, model: MathInk06Model, adapter: nn.Module | None = None, *, |
| family_weight: float = 0.0, exact_family_index: Tensor | None = None, |
| ) -> None: |
| super().__init__() |
| self.model = model |
| self.adapter = adapter if adapter is not None else nn.Identity() |
| self.family_weight = float(family_weight) |
| if not 0.0 <= self.family_weight <= 1.0: |
| raise ValueError("online family fusion weight๋ 0~1 ๋ฒ์์ฌ์ผ ํฉ๋๋ค.") |
| if self.family_weight and exact_family_index is None: |
| raise ValueError("family fusion์๋ exact_family_index๊ฐ ํ์ํฉ๋๋ค.") |
| self.register_buffer( |
| "exact_family_index", |
| exact_family_index if exact_family_index is not None else torch.empty(0, dtype=torch.long), |
| ) |
|
|
| def forward(self, sequence: Tensor) -> tuple[Tensor, Tensor]: |
| """ํ์ ๋ณ์: Bร128ร19 canonical trajectory. ์๋ ์๋ฆฌ: shared encoder์ ๋ ๋ถ๋ฅ head๋ฅผ ์ง์ ์คํํ๋ค.""" |
|
|
| exact, family = self.model.forward_online(self.adapter(sequence)) |
| if self.family_weight: |
| exact = ( |
| exact.log_softmax(dim=-1) |
| + self.family_weight |
| * family.log_softmax(dim=-1)[:, self.exact_family_index] |
| ) |
| return exact, family |
|
|
|
|
| class PFormulaStudentExportWrapper06(nn.Module): |
| """ํ์ ๋ณ์: 0.6 ๋ชจ๋ธยทonline adapterยท์ฆ๋ฅ formula adapter. ์๋ ์๋ฆฌ: P ์์์ฉ ๋ adapter๋ฅผ ์์๋๋ก ํฉ์ฑํ๋ค.""" |
|
|
| def __init__( |
| self, |
| model: MathInk06Model, |
| online_adapter: nn.Module, |
| formula_adapter: nn.Module, |
| *, |
| family_weight: float = 0.0, |
| exact_family_index: Tensor | None = None, |
| ) -> None: |
| super().__init__() |
| self.model = model |
| self.online_adapter = online_adapter |
| self.formula_adapter = formula_adapter |
| self.family_weight = float(family_weight) |
| if not 0.0 <= self.family_weight <= 1.0: |
| raise ValueError("formula family fusion weight๋ 0~1 ๋ฒ์์ฌ์ผ ํฉ๋๋ค.") |
| if self.family_weight and exact_family_index is None: |
| raise ValueError("formula family fusion์๋ exact_family_index๊ฐ ํ์ํฉ๋๋ค.") |
| self.register_buffer( |
| "exact_family_index", |
| exact_family_index if exact_family_index is not None |
| else torch.empty(0, dtype=torch.long), |
| ) |
|
|
| def forward(self, sequence: Tensor) -> tuple[Tensor, Tensor]: |
| """ํ์ ๋ณ์: Bร128ร19 formula-relative trajectory. ์๋ ์๋ฆฌ: online ๋ณด์ ๋ค student formula ๋ณด์ ์ ์ ์ฉํด ๋ logit์ ๋ฐํํ๋ค.""" |
|
|
| adapted = self.formula_adapter(self.online_adapter(sequence)) |
| exact, family = self.model.classify_trajectory(adapted) |
| if self.family_weight: |
| exact = ( |
| exact.log_softmax(dim=-1) |
| + self.family_weight |
| * family.log_softmax(dim=-1)[:, self.exact_family_index] |
| ) |
| return exact, family |
|
|
|
|
| class RasterExportWrapper06(nn.Module): |
| """ํ์ ๋ณ์: 0.6 ๋ชจ๋ธยทraster adapterยทfusion ์์. ์๋ ์๋ฆฌ: top-4๋ฅผ composite trajectory ๊ฒฝ๋ก๋ก ๋ถ๋ฅํ๋ค.""" |
|
|
| def __init__( |
| self, model: MathInk06Model, *, adapter: nn.Module | None = None, |
| fusion_mode: str, score_weight: float, |
| ) -> None: |
| super().__init__() |
| self.model = model |
| self.adapter = adapter if adapter is not None else nn.Identity() |
| self.fusion_mode = fusion_mode |
| self.score_weight = float(score_weight) |
|
|
| def forward(self, raster: Tensor) -> Tensor: |
| """ํ์ ๋ณ์: Bร1ร128ร128 raster. ์๋ ์๋ฆฌ: direct raster-label shortcut ์์ด shared trajectory ๋ถ๋ฅ๋ฅผ ๊ฒฐํฉํ๋ค.""" |
|
|
| coordinates, states, progress, hypothesis_scores = self.model.decode_raster_trajectories(raster) |
| features = virtual_features06( |
| coordinates, states, |
| None if self.model.raster_architecture == "spatial_flat_v1" else progress, |
| contract=self.model.virtual_contract, |
| ) |
| batch, hypotheses, steps, channels = features.shape |
| if self.model.use_virtual_adapter: |
| raw_features = features |
| internal = self.model.virtual_adapter( |
| features.reshape(batch * hypotheses, steps, channels), |
| ).reshape(batch, hypotheses, steps, channels) |
| features = raw_features + self.model.virtual_adapter_weight * (internal - raw_features) |
| flat_features = self.adapter(features.reshape(batch * hypotheses, steps, channels)) |
| exact, family = self.model.classify_trajectory(flat_features) |
| output = { |
| "hypothesis_scores": hypothesis_scores, |
| "exact_logits": exact.reshape(batch, hypotheses, -1), |
| "family_logits": family.reshape(batch, hypotheses, -1), |
| } |
| fused, _selected = fuse_raster_logits06( |
| output, mode=self.fusion_mode, score_weight=self.score_weight, |
| ) |
| return fused |
|
|
|
|
| class RasterDebugExportWrapper06(RasterExportWrapper06): |
| """ํ์ ๋ณ์: raster modelยทadapterยทfusion. ์๋ ์๋ฆฌ: logits์ top-4 ๊ฐ์ stroke ๊ฒ์ฆ ์ถ๋ ฅ์ ํจ๊ป ๊ณ ์ ํ๋ค.""" |
|
|
| def forward( |
| self, |
| raster: Tensor, |
| ) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: |
| """ํ์ ๋ณ์: Bร1ร128ร128 raster. ์๋ ์๋ฆฌ: direct shortcut ์์ด ๋ถ๋ฅํ๊ณ trajectory ์์ ์ถ๋ ฅ์ ๋ณด์กดํ๋ค.""" |
|
|
| coordinates, states, progress, hypothesis_scores = ( |
| self.model.decode_raster_trajectories(raster) |
| ) |
| features = virtual_features06( |
| coordinates, |
| states, |
| None if self.model.raster_architecture == "spatial_flat_v1" else progress, |
| contract=self.model.virtual_contract, |
| ) |
| batch, hypotheses, steps, channels = features.shape |
| if self.model.use_virtual_adapter: |
| raw_features = features |
| internal = self.model.virtual_adapter( |
| features.reshape(batch * hypotheses, steps, channels), |
| ).reshape(batch, hypotheses, steps, channels) |
| features = raw_features + self.model.virtual_adapter_weight * ( |
| internal - raw_features |
| ) |
| flat_features = self.adapter( |
| features.reshape(batch * hypotheses, steps, channels), |
| ) |
| exact, family = self.model.classify_trajectory(flat_features) |
| output = { |
| "hypothesis_scores": hypothesis_scores, |
| "exact_logits": exact.reshape(batch, hypotheses, -1), |
| "family_logits": family.reshape(batch, hypotheses, -1), |
| } |
| fused, _selected = fuse_raster_logits06( |
| output, |
| mode=self.fusion_mode, |
| score_weight=self.score_weight, |
| ) |
| return fused, coordinates, states, progress, hypothesis_scores |
|
|
|
|
| def exported_equivalence06( |
| eager: nn.Module, exported: torch.export.ExportedProgram, inputs: Iterable[tuple[Tensor, ...]], |
| ) -> dict[str, float | int | bool]: |
| """ํ์ ๋ณ์: eager/export ๋ชจ๋ธยท๋ํ ์
๋ ฅ. ์๋ ์๋ฆฌ: ๋ชจ๋ ์ถ๋ ฅ tensor์ top-1 ์ผ์น์ ์ต๋ logit ์ค์ฐจ๋ฅผ ๊ณ์ฐํ๋ค.""" |
|
|
| exported_module = exported.module() |
| samples = top1_matches = 0 |
| max_error = 0.0 |
| eager.eval() |
| with torch.inference_mode(): |
| for arguments in inputs: |
| eager_output = eager(*arguments) |
| export_output = exported_module(*arguments) |
| eager_values = eager_output if isinstance(eager_output, tuple) else (eager_output,) |
| export_values = export_output if isinstance(export_output, tuple) else (export_output,) |
| if len(eager_values) != len(export_values): |
| raise ValueError("eager/export ์ถ๋ ฅ ๊ฐ์๊ฐ ๋ค๋ฆ
๋๋ค.") |
| for eager_value, export_value in zip(eager_values, export_values): |
| max_error = max(max_error, float((eager_value - export_value).abs().max())) |
| samples += int(eager_values[0].shape[0]) |
| top1_matches += int((eager_values[0].argmax(dim=-1) == export_values[0].argmax(dim=-1)).sum()) |
| return { |
| "samples": samples, "top1_matches": top1_matches, |
| "top1_agreement": top1_matches / max(samples, 1), "max_absolute_logit_error": max_error, |
| "gate_passed": top1_matches == samples and max_error <= 0.02, |
| } |
|
|