dinov2-base pooler_output ONNX(224x224, opset 20), 내보낸 도구, 검증 숫자
Browse files- README.md +56 -0
- leaf_kind_onnx.py +280 -0
- model.onnx +3 -0
README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: facebook/dinov2-base
|
| 4 |
+
library_name: onnx
|
| 5 |
+
pipeline_tag: image-feature-extraction
|
| 6 |
+
tags:
|
| 7 |
+
- onnx
|
| 8 |
+
- dinov2
|
| 9 |
+
- robotics
|
| 10 |
+
- openarm-sciedu
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# dinov2-base-onnx
|
| 14 |
+
|
| 15 |
+
[facebook/dinov2-base](https://huggingface.co/facebook/dinov2-base)(커밋 `f9e44c814b77203eaa57a6bdbbd535f21ede1415`)의
|
| 16 |
+
`pooler_output`(최종 layernorm 뒤 CLS 토큰, 768)만 내는 ONNX 그래프다.
|
| 17 |
+
|
| 18 |
+
OpenArm SciEdu 대화층의 로컬 나뭇잎 판정기([leapshared/leaf_kind_dinov2_logreg](https://huggingface.co/leapshared/leaf_kind_dinov2_logreg))가
|
| 19 |
+
쓴다. torch·transformers가 없는 대화층 컨테이너에서 onnxruntime(CPU)으로 특징을 낸다.
|
| 20 |
+
|
| 21 |
+
## 입출력
|
| 22 |
+
- 입력 `pixel_values`: [N, 3, 224, 224] float32. `AutoImageProcessor(facebook/dinov2-base)` 전처리를 마친 값이다.
|
| 23 |
+
- 짧은 변 256으로 bicubic 줄이기(antialias) → 가운데 224 자르기 → /255 → ImageNet mean/std, RGB
|
| 24 |
+
- 출력 `pooler_output`: [N, 768] float32. N은 1~64.
|
| 25 |
+
- **224×224 전용이다.** 위치 임베딩(37×37 → 16×16 bicubic 보간)을 torch로 한 번 셈해 상수로 넣었다. 그래서 그래프에 보간이
|
| 26 |
+
없고, 다른 크기는 받지 않는다.
|
| 27 |
+
|
| 28 |
+
## 만든 법
|
| 29 |
+
- `leaf_kind_onnx.py export`(이 저장소에 사본이 있다. 원본은 `LEAP-LAB-KUS/openarmsciedu-robot`의
|
| 30 |
+
`scripts/diagnostics/leaf_kind_onnx.py`). CPU에서 내보냈다.
|
| 31 |
+
- torch 2.11.0, transformers 5.5.4, onnx 1.23.1, onnxscript 0.7.2. `torch.onnx.export(dynamo=True)`, opset 20.
|
| 32 |
+
- 가중치는 파일 안에 있다(외부 데이터 없음). 노드마다 붙는 stack_trace 메타데이터는 지웠다(내보낸 PC의 경로). 그래서 같은
|
| 33 |
+
도구로 다시 내보내면 같은 파일이 나온다(두 번 내보내 sha256이 같음을 확인).
|
| 34 |
+
- `model.onnx`: 343.0 MB, sha256 `8f905c48a542eddc7e43e77d98918683dfc59ecb827ab5789ef8c09a4ac0cc2a`
|
| 35 |
+
- 모델 메타데이터 `source_model`, `source_revision`에 원본을 적었다.
|
| 36 |
+
|
| 37 |
+
## 검증 (2026-10-04, 로봇 PC CPU, onnxruntime 1.30.0)
|
| 38 |
+
녹화 `leapshared/Leaf_rules_20261002_225313_subtask`의 가슴 카메라 프레임 698장을 서빙 카메라처럼 JPEG(품질 80)으로 묶었다.
|
| 39 |
+
그 사진에서 torch 길(transformers 처리기 + `Dinov2Model`)과 onnx 길(numpy 전처리 + 이 파일)을 견줬다.
|
| 40 |
+
- 전처리: numpy로 옮긴 것(`dialogue/leaf_onnx.py`)이 transformers 처리기와 원소 105,068,544개 중 다른 것 0
|
| 41 |
+
- 특징: 코사인 최소 0.999999999994(1 − 코사인 ≤ 6.4e-12), 원소 최대 차이 2.4e-5(원소 크기 중앙 1.07)
|
| 42 |
+
- 판정(`leaf_kind_dinov2_logreg`): 한 장 판정 698/698 같음, 대화층 판정(세 장 한 번 포함) 654/654 같음
|
| 43 |
+
- 검증 에피소드 6·7·46·47의 카드 22장(세 장 한 번): 두 길 모두 22/22
|
| 44 |
+
- 그리퍼가 카드를 쥔 프레임 22장, 빈 줄 4장: 두 길 모두 none
|
| 45 |
+
- 3장 판정 지연(JPEG 풀기 포함, AMD Ryzen 5 7500F 4스레드, 두 번 잼): onnx 중앙 324·353 ms, torch 중앙 425·431 ms
|
| 46 |
+
|
| 47 |
+
## 쓰는 법
|
| 48 |
+
```python
|
| 49 |
+
import onnxruntime as ort
|
| 50 |
+
session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
|
| 51 |
+
features = session.run(None, {"pixel_values": pixel_values})[0] # [N, 768]
|
| 52 |
+
```
|
| 53 |
+
전처리를 numpy로 똑같이 하는 코드는 `LEAP-LAB-KUS/openarmsciedu-robot`의 `dialogue/leaf_onnx.py`(`pixel_values`)에 있다.
|
| 54 |
+
|
| 55 |
+
## 라이선스
|
| 56 |
+
원본 DINOv2(Meta AI)의 Apache-2.0을 따른다. 가중치는 원본 그대로이고, 위치 임베딩만 224×224로 미리 보간한 값으로 바꿨다.
|
leaf_kind_onnx.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DINOv2-base 특징(pooler_output)을 ONNX로 내보내고, 로컬 나뭇잎 판정기의 두 길(torch·onnxruntime)을 녹화 사진으로
|
| 2 |
+
견준다(이슈 #28). 로봇에 붙지 않고 GPU도 쓰지 않는다(CPU로 내보내고 잰다).
|
| 3 |
+
|
| 4 |
+
# 1. 내보내기: torch·transformers에 onnx·onnxscript·onnxruntime을 얹은 파이썬
|
| 5 |
+
CUDA_VISIBLE_DEVICES= HF_HUB_OFFLINE=1 <python> scripts/diagnostics/leaf_kind_onnx.py export --out outputs/leaf_onnx/model.onnx
|
| 6 |
+
# 2. 견주기: 위 파이썬 + 녹화 leapshared/Leaf_rules_20261002_225313_subtask의 로컬 사본
|
| 7 |
+
CUDA_VISIBLE_DEVICES= HF_HUB_OFFLINE=1 <python> scripts/diagnostics/leaf_kind_onnx.py compare --onnx outputs/leaf_onnx/model.onnx
|
| 8 |
+
|
| 9 |
+
저장소 venv에는 onnx·onnxruntime이 없다. 2026-10-04에는 venv의 site-packages를 .pth로 읽는 임시 venv에
|
| 10 |
+
onnx 1.23.1·onnxscript 0.7.2·onnx-ir 1.0.0·ml-dtypes 0.6.0·onnxruntime 1.30.0·flatbuffers 25.12.19만 더 깔아 돌렸다.
|
| 11 |
+
|
| 12 |
+
export는 facebook/dinov2-base(고정 커밋)의 pooler_output만 내는 그래프를 만든다.
|
| 13 |
+
- 입력은 pixel_values [N, 3, 224, 224] float32(전처리를 마친 값), 출력은 pooler_output [N, 768]이다.
|
| 14 |
+
- 224x224의 위치 임베딩(37x37 → 16x16 bicubic)은 torch로 한 번 셈해 상수로 넣는다. 대화층은 늘 224x224를 넣으므로
|
| 15 |
+
그래프에 보간이 남지 않는다.
|
| 16 |
+
- 내보낸 뒤 같은 입력에서 torch와 견주고, 메타데이터(원본 모델·커밋·도구 판)를 적는다.
|
| 17 |
+
|
| 18 |
+
compare는 녹화에서 서빙 카메라처럼(RGB → BGR → JPEG 품질 80) 사진을 만들어 두 길에 넣는다.
|
| 19 |
+
- 검증 에피소드 6·7·46·47의 카드 22장: 결정 프레임(팔이 출발하기 5프레임 전)과 그 −12·−6 프레임으로 세 장 한 번 판정
|
| 20 |
+
- 같은 카드를 그리퍼가 쥐는 프레임 22장, 그 에피소드의 첫 프레임(빈 줄) 4장: 한 장씩. none이어야 한다
|
| 21 |
+
- 나머지 에피소드의 카드 결정 프레임과 쥐는 프레임: 한 장씩. 특징을 넓게 견준다
|
| 22 |
+
- 찍는 것: 전처리(numpy와 transformers의 원소 차이), 특징 코사인, 판정 일치, 판정기 만들기, 3장 판정 지연(JPEG 풀기 포함)
|
| 23 |
+
|
| 24 |
+
허브 leapshared/dinov2-base-onnx에 올린 사본은 export만 쓴다(compare는 이 저장소의 dialogue/와 녹화 도구가 있어야 한다).
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import hashlib
|
| 31 |
+
import json
|
| 32 |
+
import statistics
|
| 33 |
+
import sys
|
| 34 |
+
import time
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
|
| 37 |
+
import numpy as np
|
| 38 |
+
|
| 39 |
+
HERE = Path(__file__).resolve()
|
| 40 |
+
REPO = next((p for p in HERE.parents if (p / "pyproject.toml").is_file()), HERE.parent) # 허브 사본은 저장소 밖
|
| 41 |
+
sys.path.insert(0, str(REPO))
|
| 42 |
+
|
| 43 |
+
SOURCE = "facebook/dinov2-base"
|
| 44 |
+
REVISION = "f9e44c814b77203eaa57a6bdbbd535f21ede1415" # 2026-10-04 main(이 PC HF 캐시)
|
| 45 |
+
SIZE = 224
|
| 46 |
+
DATASET = "Leaf_rules_20261002_225313_subtask"
|
| 47 |
+
HELD_OUT = (6, 7, 46, 47)
|
| 48 |
+
KIND = {"복엽": "compound", "단엽": "simple"}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def sha256(path: Path) -> str:
|
| 52 |
+
h = hashlib.sha256()
|
| 53 |
+
with open(path, "rb") as f:
|
| 54 |
+
for block in iter(lambda: f.read(1 << 20), b""):
|
| 55 |
+
h.update(block)
|
| 56 |
+
return h.hexdigest()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def cosines(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
| 60 |
+
a, b = np.asarray(a, np.float64), np.asarray(b, np.float64)
|
| 61 |
+
return (a * b).sum(1) / (np.linalg.norm(a, axis=1) * np.linalg.norm(b, axis=1))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ── export ──
|
| 65 |
+
def export(out: Path, revision: str, opset: int | None) -> None:
|
| 66 |
+
import onnx
|
| 67 |
+
import onnxruntime as ort
|
| 68 |
+
import torch
|
| 69 |
+
import transformers
|
| 70 |
+
from transformers import AutoModel
|
| 71 |
+
|
| 72 |
+
torch.manual_seed(0)
|
| 73 |
+
model = AutoModel.from_pretrained(SOURCE, revision=revision).eval()
|
| 74 |
+
probe = torch.randn(3, 3, SIZE, SIZE)
|
| 75 |
+
with torch.no_grad():
|
| 76 |
+
want = model(pixel_values=probe).pooler_output # 고치기 전 모델의 답
|
| 77 |
+
|
| 78 |
+
class Pooler(torch.nn.Module):
|
| 79 |
+
"""pixel_values [N, 3, 224, 224] → pooler_output [N, 768]. 위치 임베딩은 224x224로 미리 셈한 상수다."""
|
| 80 |
+
|
| 81 |
+
def __init__(self, inner) -> None:
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.inner = inner
|
| 84 |
+
embeddings = inner.embeddings
|
| 85 |
+
tokens = torch.zeros(1, 1 + (SIZE // embeddings.patch_size) ** 2, inner.config.hidden_size)
|
| 86 |
+
with torch.no_grad():
|
| 87 |
+
self.register_buffer("pos", embeddings.interpolate_pos_encoding(tokens, SIZE, SIZE).clone())
|
| 88 |
+
embeddings.interpolate_pos_encoding = lambda _embeddings, _height, _width: self.pos
|
| 89 |
+
|
| 90 |
+
def forward(self, pixel_values):
|
| 91 |
+
return self.inner(pixel_values=pixel_values).pooler_output
|
| 92 |
+
|
| 93 |
+
pooler = Pooler(model).eval()
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
got = pooler(probe)
|
| 96 |
+
print(f"상수 위치 임베딩: 원래 모델과 최대 차이 {float((got - want).abs().max()):.3g}")
|
| 97 |
+
|
| 98 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
batch = torch.export.Dim("batch", min=1, max=64)
|
| 100 |
+
torch.onnx.export(pooler, (probe,), str(out), input_names=["pixel_values"], output_names=["pooler_output"],
|
| 101 |
+
dynamic_shapes={"pixel_values": {0: batch}}, opset_version=opset, dynamo=True,
|
| 102 |
+
external_data=False)
|
| 103 |
+
graph = onnx.load(str(out))
|
| 104 |
+
for node in graph.graph.node: # 노드마다 붙는 stack_trace 등: 내보낸 PC의 경로·줄 번호라 계산과 상관없다. 지우면 파일이 늘 같다
|
| 105 |
+
del node.metadata_props[:]
|
| 106 |
+
onnx.helper.set_model_props(graph, {
|
| 107 |
+
"source_model": SOURCE, "source_revision": revision, "output": "pooler_output",
|
| 108 |
+
"input": f"pixel_values [N, 3, {SIZE}, {SIZE}] float32: AutoImageProcessor 전처리를 마친 값",
|
| 109 |
+
"position_embeddings": f"{SIZE}x{SIZE} 입력으로 미리 셈한 상수",
|
| 110 |
+
"exported_with": f"torch {torch.__version__}, transformers {transformers.__version__}, onnx {onnx.__version__}",
|
| 111 |
+
"made_by": "LEAP-LAB-KUS/openarmsciedu-robot scripts/diagnostics/leaf_kind_onnx.py export",
|
| 112 |
+
})
|
| 113 |
+
onnx.save(graph, str(out))
|
| 114 |
+
onnx.checker.check_model(str(out))
|
| 115 |
+
opsets = {o.domain or "ai.onnx": o.version for o in graph.opset_import}
|
| 116 |
+
session = ort.InferenceSession(str(out), providers=["CPUExecutionProvider"])
|
| 117 |
+
for n in (1, 3):
|
| 118 |
+
ort_out = session.run(None, {"pixel_values": probe[:n].numpy()})[0]
|
| 119 |
+
diff = np.abs(ort_out - want[:n].numpy()).max()
|
| 120 |
+
print(f"onnxruntime {ort.__version__}, {n}장: 최대 차이 {diff:.3g}, 코사인 최소 "
|
| 121 |
+
f"{cosines(ort_out, want[:n].numpy()).min():.8f}")
|
| 122 |
+
print(f"썼다: {out} ({out.stat().st_size / 1e6:.1f} MB, sha256 {sha256(out)}, opset {opsets})")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ── compare ──
|
| 126 |
+
def serving_jpeg(image) -> bytes:
|
| 127 |
+
"""서빙 카메라(serving/cameras.py)처럼: RGB → BGR → JPEG 품질 80."""
|
| 128 |
+
import cv2
|
| 129 |
+
|
| 130 |
+
rgb = np.asarray(image.convert("RGB"), dtype=np.uint8)
|
| 131 |
+
ok, buf = cv2.imencode(".jpg", cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, 80])
|
| 132 |
+
assert ok
|
| 133 |
+
return buf.tobytes()
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def photos(root: Path) -> list[dict]:
|
| 137 |
+
"""녹화의 사진들: {"name", "kind", "group", "held_out", "jpeg"}. 검증 카드는 세 장(−12·−6·0)이 한 판정이다."""
|
| 138 |
+
sys.path.insert(0, str(REPO / "scripts" / "diagnostics"))
|
| 139 |
+
sys.path.insert(0, str(REPO / "scripts" / "verifier"))
|
| 140 |
+
import common
|
| 141 |
+
from leaf_view import all_cards
|
| 142 |
+
|
| 143 |
+
meta = common.episodes_meta(root)
|
| 144 |
+
by_episode: dict[int, list[dict]] = {}
|
| 145 |
+
for card in all_cards(root):
|
| 146 |
+
by_episode.setdefault(card["episode"], []).append(card)
|
| 147 |
+
rows = []
|
| 148 |
+
for episode, cards in sorted(by_episode.items()):
|
| 149 |
+
held = episode in HELD_OUT
|
| 150 |
+
want = {0} if held else set()
|
| 151 |
+
for c in cards:
|
| 152 |
+
want |= {c["decide"], c["close"]} | ({max(0, c["decide"] - 12), max(0, c["decide"] - 6)} if held else set())
|
| 153 |
+
got = common.read_frames(root, meta, episode, common.CHEST, sorted(want))
|
| 154 |
+
if held:
|
| 155 |
+
rows.append({"name": f"ep{episode:02d} 첫 프레임", "kind": "none", "group": "first", "held_out": True,
|
| 156 |
+
"jpeg": [serving_jpeg(got[0])]})
|
| 157 |
+
for c in cards:
|
| 158 |
+
tag = f"ep{episode:02d}#{c['card']}"
|
| 159 |
+
offsets = (-12, -6, 0) if held else (0,)
|
| 160 |
+
rows.append({"name": tag, "kind": KIND[c["kind"]], "group": "card", "held_out": held,
|
| 161 |
+
"jpeg": [serving_jpeg(got[max(0, c["decide"] + o)]) for o in offsets]})
|
| 162 |
+
rows.append({"name": f"{tag} 쥠", "kind": "none", "group": "close", "held_out": held,
|
| 163 |
+
"jpeg": [serving_jpeg(got[c["close"]])]})
|
| 164 |
+
return rows
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def compare(onnx_path: Path, root: Path, threads: int, repeat: int, out: Path | None) -> None:
|
| 168 |
+
import torch
|
| 169 |
+
import transformers
|
| 170 |
+
|
| 171 |
+
from dialogue.leaf_kind.leaf_kind import LeafKind
|
| 172 |
+
from dialogue.leaf_local import LocalLeafJudge, rgb_frame
|
| 173 |
+
from dialogue.leaf_onnx import OnnxLeafKind, pixel_values
|
| 174 |
+
|
| 175 |
+
classifier = REPO / "dialogue" / "leaf_kind" / "classifier.json"
|
| 176 |
+
torch.set_num_threads(threads)
|
| 177 |
+
started = time.perf_counter()
|
| 178 |
+
reference = LeafKind(classifier=classifier, device="cpu", threshold=0.9)
|
| 179 |
+
torch_load = time.perf_counter() - started
|
| 180 |
+
started = time.perf_counter()
|
| 181 |
+
candidate = OnnxLeafKind(onnx_path, classifier, threshold=0.9, threads=threads)
|
| 182 |
+
onnx_load = time.perf_counter() - started
|
| 183 |
+
|
| 184 |
+
rows = photos(root)
|
| 185 |
+
x0, y0, x1, y1 = reference.crop
|
| 186 |
+
pixel_diff = pixel_total = 0
|
| 187 |
+
feats_t, feats_o, frame_rows, judged = [], [], [], []
|
| 188 |
+
for row in rows:
|
| 189 |
+
frames = [rgb_frame(j) for j in row["jpeg"]]
|
| 190 |
+
for frame in frames: # 한 장씩: 전처리와 특징
|
| 191 |
+
crop = np.ascontiguousarray(frame[y0:y1, x0:x1])
|
| 192 |
+
ref_px = reference.processor(images=[crop], return_tensors="np")["pixel_values"]
|
| 193 |
+
pixel_diff += int((ref_px != pixel_values([crop])).sum())
|
| 194 |
+
pixel_total += ref_px.size
|
| 195 |
+
ft, fo = reference.features([frame]), candidate.features([frame])
|
| 196 |
+
feats_t.append(ft[0])
|
| 197 |
+
feats_o.append(fo[0])
|
| 198 |
+
pt, po = reference.proba([frame])[0], candidate.proba([frame])[0]
|
| 199 |
+
frame_rows.append({"name": row["name"], "kind": row["kind"], "torch": reference.classes[int(pt.argmax())],
|
| 200 |
+
"onnx": candidate.classes[int(po.argmax())], "dp": float(np.abs(pt - po).max())})
|
| 201 |
+
a, b = reference.judge(frames), candidate.judge(frames) # 대화층처럼 한 번에
|
| 202 |
+
judged.append({"name": row["name"], "kind": row["kind"], "group": row["group"], "held_out": row["held_out"],
|
| 203 |
+
"frames": len(frames), "torch": a, "onnx": b})
|
| 204 |
+
|
| 205 |
+
cos = cosines(np.stack(feats_t), np.stack(feats_o))
|
| 206 |
+
absdiff = np.abs(np.stack(feats_t) - np.stack(feats_o))
|
| 207 |
+
same_vote = sum(r["torch"] == r["onnx"] for r in frame_rows)
|
| 208 |
+
same_judge = sum((j["torch"]["kind"], j["torch"]["votes"]) == (j["onnx"]["kind"], j["onnx"]["votes"]) for j in judged)
|
| 209 |
+
print(f"판정기 만들기(꾸러미를 불러온 뒤): torch {torch_load:.2f} s, onnx {onnx_load:.2f} s (스레드 {threads})")
|
| 210 |
+
print(f"전처리: 다른 원소 {pixel_diff}/{pixel_total}")
|
| 211 |
+
print(f"특징 {len(cos)}장: 코사인 최소 {cos.min():.12f}(1 − 코사인 최대 {1 - cos.min():.2g}), 원소 최대 차이 "
|
| 212 |
+
f"{absdiff.max():.3g}(특징 원소 크기 중앙 {np.median(np.abs(np.stack(feats_t))):.3g}), 세 부류 확률 최대 차이 "
|
| 213 |
+
f"{max(r['dp'] for r in frame_rows):.3g}")
|
| 214 |
+
print(f"한 장 판정(argmax) 같음 {same_vote}/{len(frame_rows)}, 판정(kind·votes) 같음 {same_judge}/{len(judged)}")
|
| 215 |
+
|
| 216 |
+
def tally(select, label):
|
| 217 |
+
chosen = [j for j in judged if select(j)]
|
| 218 |
+
for path in ("torch", "onnx"):
|
| 219 |
+
right = sum(j[path]["kind"] == j["kind"] for j in chosen)
|
| 220 |
+
other = {}
|
| 221 |
+
for j in chosen:
|
| 222 |
+
if j[path]["kind"] != j["kind"]:
|
| 223 |
+
other[str(j[path]["kind"])] = other.get(str(j[path]["kind"]), 0) + 1
|
| 224 |
+
print(f" {label} {path}: 맞음 {right}/{len(chosen)}" + (f", 다른 답 {other}" if other else ""))
|
| 225 |
+
|
| 226 |
+
tally(lambda j: j["held_out"] and j["group"] == "card", "검증 카드(세 장 한 번)")
|
| 227 |
+
tally(lambda j: j["held_out"] and j["group"] == "close", "검증 쥐는 프레임")
|
| 228 |
+
tally(lambda j: j["held_out"] and j["group"] == "first", "검증 첫 프레임(빈 줄)")
|
| 229 |
+
tally(lambda j: not j["held_out"] and j["group"] == "card", "나머지 카드(한 장)")
|
| 230 |
+
tally(lambda j: not j["held_out"] and j["group"] == "close", "나머지 쥐는 프레임")
|
| 231 |
+
|
| 232 |
+
# 지연: 대화층과 같은 길(JPEG 세 장 → 풀기 → 자르기 → 전처리 → 특징 → 판정)
|
| 233 |
+
three = next(r for r in rows if r["held_out"] and r["group"] == "card")["jpeg"]
|
| 234 |
+
latency = {}
|
| 235 |
+
for path, clf in (("torch", reference), ("onnx", candidate)):
|
| 236 |
+
judge = LocalLeafJudge(clf, path)
|
| 237 |
+
for _ in range(3):
|
| 238 |
+
judge.judge(three)
|
| 239 |
+
times = []
|
| 240 |
+
for _ in range(repeat):
|
| 241 |
+
started = time.perf_counter()
|
| 242 |
+
judge.judge(three)
|
| 243 |
+
times.append((time.perf_counter() - started) * 1000)
|
| 244 |
+
latency[path] = times
|
| 245 |
+
print(f"3장 판정 {path}: 중앙 {statistics.median(times):.0f} ms, 최대 {max(times):.0f} ms ({repeat}번)")
|
| 246 |
+
if out:
|
| 247 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 248 |
+
out.write_text(json.dumps({
|
| 249 |
+
"onnx": str(onnx_path), "onnx_sha256": sha256(onnx_path), "threads": threads,
|
| 250 |
+
"versions": {"torch": torch.__version__, "transformers": transformers.__version__,
|
| 251 |
+
"onnxruntime": __import__("onnxruntime").__version__},
|
| 252 |
+
"load_s": {"torch": torch_load, "onnx": onnx_load}, "pixel_diff": [pixel_diff, pixel_total],
|
| 253 |
+
"cosine": {"min": float(cos.min()), "median": float(np.median(cos))}, "feature_absdiff_max": float(absdiff.max()),
|
| 254 |
+
"frames": frame_rows, "judged": judged, "latency_ms": latency}, ensure_ascii=False, indent=1))
|
| 255 |
+
print("썼다:", out)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def main() -> int:
|
| 259 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 260 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 261 |
+
ex = sub.add_parser("export", help="dinov2-base pooler_output → ONNX(CPU)")
|
| 262 |
+
ex.add_argument("--out", type=Path, default=REPO / "outputs" / "leaf_onnx" / "model.onnx")
|
| 263 |
+
ex.add_argument("--revision", default=REVISION)
|
| 264 |
+
ex.add_argument("--opset", type=int, default=None, help="비우면 torch 내보내기의 기본값")
|
| 265 |
+
co = sub.add_parser("compare", help="녹화 사진으로 torch 길과 onnxruntime 길 견주기")
|
| 266 |
+
co.add_argument("--onnx", type=Path, required=True)
|
| 267 |
+
co.add_argument("--root", type=Path, default=Path.home() / ".cache" / "huggingface" / "lerobot" / "leapshared" / DATASET)
|
| 268 |
+
co.add_argument("--threads", type=int, default=4, help="대화층 설정 leaf.local.threads와 같게")
|
| 269 |
+
co.add_argument("--repeat", type=int, default=30)
|
| 270 |
+
co.add_argument("--out", type=Path, default=REPO / "outputs" / "leaf_onnx" / "compare.json")
|
| 271 |
+
args = parser.parse_args()
|
| 272 |
+
if args.command == "export":
|
| 273 |
+
export(args.out, args.revision, args.opset)
|
| 274 |
+
else:
|
| 275 |
+
compare(args.onnx, args.root, args.threads, args.repeat, args.out)
|
| 276 |
+
return 0
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
raise SystemExit(main())
|
model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8f905c48a542eddc7e43e77d98918683dfc59ecb827ab5789ef8c09a4ac0cc2a
|
| 3 |
+
size 343041303
|