ช่องเติมที่ 1: ทำ front-end ให้เหมือนตอนเทรน — จุดที่ parity พังบ่อยสุด:
def web_verdict(model_path, window, z):
# เติม 1: ใช้ mean/std ชุดเดียวกับตอนเทรน
x = (window - z["mean"]) / z["std"]
...
z คือไฟล์ model_int8.tflite.norm.npz ที่ train.py เซฟไว้ มี mean กับ std อย่างละ 6 ค่า (ต่อแกน IMU)x = window) โมเดลเห็นข้อมูลดิบคนละสเกล — verdict จะมั่วทันที(v - mean[i%6]) / std[i%6]ลองเล่นให้เห็นภัย: comment บรรทัดนี้ทิ้งแล้วรัน จะเห็น conf เพี้ยนจนคลาสที่ชนะเปลี่ยน — นั่นคือพลังของ front-end
ช่องเติมที่ 2: โมเดล MCU เป็น int8 in/out เราจึงต้องแปลง feature เป็น int8 ก่อนป้อน:
in_scale, in_zero = inp["quantization"] # อ่านค่ามาจากตัวโมเดล ไม่ใช่เดา
# เติม 2: quantize ด้วย scale/zero ของ input tensor
q = np.clip(np.round(x / in_scale + in_zero), -128, 127).astype(np.int8)
scale/zero มาจาก inp["quantization"] — ค่าที่ converter คำนวณไว้ตอน quantize อ่านจากโมเดลตรงๆnp.clip(..., -128, 127) กันค่าล้นช่วง int8 · np.round ปัดให้เป็นจำนวนเต็มquantize คือการ "ย่อ" float ให้ลงช่วง 256 ระดับของ int8 — สูตรนี้ต้องเป๊ะ ไม่งั้น parity พังตั้งแต่ input
ช่องเติมที่ 3: ขั้นที่กราฟทำงานเอง — ป้อน tensor แล้วสั่ง invoke:
# เติม 3: ป้อน q เข้าโมเดลแล้วสั่งอนุมาน
it.set_tensor(inp["index"], q)
it.invoke()
o = it.get_tensor(out["index"])[0].astype(np.float32)
set_tensor วางข้อมูลเข้าช่อง input · invoke() รันกราฟ · get_tensor ดึงผลออกจากช่อง outputmodel.run([x]) บรรทัดเดียวของ LiteRT.js — คนละสไตล์ API งานเดียวกันai-edge-litert ลงบน RPi/Jetson ได้ตรงๆขั้นนี้แทบไม่มีอะไรให้ parity พลาด เพราะเป็นตัวกราฟทำงาน — ความต่างเล็กน้อยที่เหลือมาจาก kernel (XNNPACK vs CMSIS-NN) ซึ่งเรายอมรับผ่าน TOL
ช่องเติมที่ 4 และ 5: แปลง output int8 กลับเป็น float แล้วสรุป verdict:
# เติม 4: dequantize output กลับเป็นความน่าจะเป็น (โมเดลมี softmax head)
o = (o - out_zero) * out_scale
top = int(o.argmax())
# เติม 5: conf = คะแนนของคลาสที่ชนะ (ตัวเลขที่ใช้วัด parity)
conf = float(o[top])
return {"label": dt.CLASSES[top], "top": top, "conf": conf, "scores": o.tolist()}
(int8 - zero) * scale เอาจำนวนเต็มกลับเป็นทศนิยม — output softmax จึงรวมได้ ~1.0argmax = คลาสที่ชนะ · conf = คะแนนของคลาสนั้น — คู่นี้แหละที่เอาไปเทียบกับเบราว์เซอร์scores ทั้งชุดคืนออกไปด้วย เพราะ parity วัดจาก max|scores_pc - scores_web| ทุกคลาสเทียบกันชัดๆ: ฝั่ง PC ได้
{label, conf, scores}— ฝั่งเบราว์เซอร์ต้องได้ dict หน้าตาเดียวกัน ตัวเลขตรงในเกณฑ์ TOL = ผ่าน MVP ของบทเรียน 5.6–5.7
โค้ดฝั่งเบราว์เซอร์อยู่ในไฟล์ฝึกเป็นค่าคงที่ JS_LITERT (พิมพ์ออกมาด้วย --show-js) เทียบทีละขั้นกับ Python:
function webVerdict(window, mean, std) {
const x = window.map((v, i) => (v - mean[i % 6]) / std[i % 6]); // (1) normalize เหมือน PC
const out = model.run([x]); // (2)(3) float I/O + invoke
const scores = out[0]; // (4) softmax อยู่ในกราฟ
let top = 0;
for (let i = 1; i < scores.length; i++) if (scores[i] > scores[top]) top = i;
return {top, conf: scores[top], scores};
}
set_tensor int8 ไป{top, conf, scores} หน้าตาเดียวกับ dict ฝั่ง PC ตั้งใจให้เทียบกันได้ทันทีอ่านสองไฟล์คู่กัน (Python กับ JS) แล้วจะเห็นชัดว่า deploy ข้ามเป้าหมายคือ "เขียนท่าเดียวกันหลายภาษา" ไม่ใช่เวทมนตร์
เปิด s13_web.py ในฟังก์ชัน web_verdict() มีช่องให้เติม 5 จุด ตามโครงสี่ขั้น:
| # | ขั้น | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | normalize | x = (window - z["mean"]) / z["std"] |
verdict มั่ว (คนละสเกล) |
| 2 | quantize | q = np.clip(np.round(x/in_scale+in_zero),-128,127).astype(np.int8) |
input เพี้ยน |
| 3 | invoke | it.set_tensor(inp["index"], q) + it.invoke() |
ได้ scores เป็นศูนย์ |
| 4 | dequantize | o = (o - out_zero) * out_scale |
conf ไม่ใช่ความน่าจะเป็น |
| 5 | verdict | conf = float(o[top]) |
conf ค้างที่ 0.0 |
ขั้นตอน:
# เติม: ทีละจุด แทน placeholder ด้วยคำสั่งจริงตามคำใบ้shared/training/: python s13_web.py — ดู verdict ฝั่ง PC ที่พิมพ์ออกมาห้าช่องนี้คือโครงสี่ขั้นของทุกการ deploy — เติมครบเมื่อไร คุณมี ground truth ฝั่ง PC ไว้วัด parity กับเบราว์เซอร์
Web เป็นเป้าหมายที่ "ยุ่ง" ที่สุด (ต้องแปลงไฟล์ + reproduce front-end ใน JS) แต่ Cortex-A ง่ายที่สุด:
# บน Raspberry Pi / Jetson / mini-PC (Linux):
pip install ai-edge-litert
python eval_pc.py --model model_int8.tflite --data data/gestures.csv
eval_pc.py ตัวเดิมเป๊ะ รันบน Cortex-A ได้โดยไม่แก้แม้แต่บรรทัดเดียว — ai-edge-litert ลงบน ARM Linux ได้.tflite เดิม ไม่ต้องแปลง (int8 หรือ float ก็รันได้ ไม่มีข้อจำกัด I/O แบบเบราว์เซอร์)Cortex-A คือ "PC ตัวเล็ก" ที่รัน Linux เต็ม — ต่างจาก MCU ตรงที่มี OS + Python จริง จึงเอาสคริปต์ PC ไปวางรันได้เลย
Cortex-A ไม่ได้มีแค่ CPU หลายรุ่นมี GPU/NPU ในตัว เร่งการอนุมานผ่าน delegate ของ LiteRT:
| บอร์ด | ตัวเร่ง | delegate |
|---|---|---|
| Raspberry Pi 5 | CPU (Cortex-A76) | XNNPACK (มากับ runtime) |
| Jetson (Orin/Nano) | GPU (CUDA) | GPU delegate |
| บอร์ด NNAPI เดิม | NPU | LiteRT GPU delegate (NNAPI เลิกใช้ A15) |
| บอร์ด Arm ML | Arm NN | Arm NN = TFLite delegate |
tflite-runtime เดิมถูกเปลี่ยนชื่อเป็น ai-edge-litert (แค่สลับชื่อ import) — ของใหม่ที่ยัง maintainจุดตัดสินใจ: ถ้าต้องเร็ว+กินไฟต่ำมากให้ MCU · ถ้าต้องแรง+ยืดหยุ่น+มี OS ให้ Cortex-A · ถ้าต้องแชร์ทันทีไม่ติดตั้งให้ Web
คำถามที่คอร์สนี้ฝึกให้ตอบ: "โมเดลตัวนี้ควรรันที่ไหน?" — แต่ละที่แลกอะไรกับอะไร
ไม่มีเป้าหมายไหน "ดีที่สุด" มีแค่ "เหมาะกับงานนี้ที่สุด" — วิศวกรที่เก่งคือคนที่ตอบคำถามนี้ได้ พร้อมเหตุผล
ก่อนเทียบกับเบราว์เซอร์ เราต้องมี "คำตอบที่ถือว่าถูก" ก่อน — นั่นคือ verdict ฝั่ง PC:
shared/training/ (ที่มี dataset_tools.py + model_int8.tflite)s13_web.py ให้ครบ 5 ช่องpython s13_web.py — จะเห็น verdict: label, conf, และ scores ครบทุกคลาสscores ไว้ — นี่คือ ground truth ที่เบราว์เซอร์ต้องตามให้ทันverdict ฝั่ง PC (ground truth ที่เบราว์เซอร์ต้องได้ตรงกัน):
label = circle conf = 0.9xxx (คลาสจริง = circle)
scores = ['0.0xxx', '0.9xxx', '0.0xxx']
ถ้ายังไม่มี
data/gestures.csvสคริปต์จะสร้างชุดสังเคราะห์ให้ก่อน (เหมือนdataset_tools.py --synthesize) — pipeline เดินได้ก่อนมีข้อมูลจริง
ตอนนี้เอา window เดียวกันไปรันฝั่งเบราว์เซอร์ (หน้าเว็บของคุณที่โหลด LiteRT.js — BENTO Emulator โหลดไฟล์ของเราเองไม่ได้) แล้ววัด parity:
python convert_web.py --keras model.keras --out model_web.tflite (ต้องมี model.keras จาก train.py --save-keras ก่อน ส่วน [[s13_web_full.py](https://github.com/tesaiot/tesa-qualification-program/blob/main/courses/edge-ai-developer/m05-training/l07-web-parity-lab/examples/s13_web_full.py)](https://github.com/tesaiot/tesa-qualification-program/blob/main/courses/edge-ai-developer/m05-training/l07-web-parity-lab/examples/s13_web_full.py) --export-web ทำขั้นเดียวกันนี้)model_web.tflite ใน LiteRT.js แล้วรัน webVerdict(window, mean, std) กับ window ตัวเดิมscores สองฝั่ง คำนวณ max|score_pc - score_web|s13_web_full.py ทำขั้นเดียวกันบน PC ในสคริปต์เดียว (รันทั้งไฟล์ int8 และไฟล์ web ด้วย interpreter บน PC บนชุดทดสอบ แล้วสรุป PASS/FAIL) ใช้เป็นด่านแรกก่อนเปิดเบราว์เซอร์จริง
ถ้ายังไม่มีหน้าเว็บของตัวเอง เริ่มจาก
s13_web_full.pyเทียบไฟล์ int8 กับไฟล์ web บน PC ก่อน แล้วค่อยยืนยันในเบราว์เซอร์ด้วยโค้ดจาก--show-js
นี่ไม่ใช่ภาพประกอบ แต่เป็นเบราว์เซอร์ที่กำลังอนุมานจริง (BENTO Emulator รันโมเดลท่ามือผ่าน ONNX Runtime Web):

จอ emulator ที่รันได้จริง — BENTO Edge AI Emulator (MicroPython-WASM + ONNX Runtime Web ในแท็บเดียว)
model_int8.tflite เป็น ONNX ครั้งเดียว แล้วให้ verdict สดๆ บนหน้าเว็บeval_pc.py: normalize → quantize → run → dequantize โดยป้อน window จาก IMU จำลองเปิดของจริงก่อน แล้วค่อยแกะว่าทำไมมันถึงทำงาน — สี่ขั้นในโค้ด 5 ช่องของเราคือสี่ขั้นเดียวกับที่ทำงานอยู่หลังจอนี้
ถ้า max-abs-diff เกิน TOL หรือคลาสที่ชนะไม่ตรง อย่าเพิ่งโทษตัวโมเดล ไล่ตามลำดับนี้ (เรียงจากที่ผิดบ่อยสุด):
| อาการ | สงสัยตรงไหนก่อน | เช็กอะไร |
|---|---|---|
| verdict คนละคลาสเลย | normalize (ช่อง 1) | ทั้งสองฝั่งใช้ mean/std ชุดเดียวกันจาก .norm.npz ไหม |
| conf เพี้ยนเป็นระบบ | quantize (ช่อง 2) | อ่าน scale/zero จากโมเดลจริง ไม่ใช่ค่าเดา |
| scores เป็นศูนย์หมด | invoke (ช่อง 3) | ลืม set_tensor/invoke หรือ input shape ผิด |
| ต่างกันนิดเดียวสม่ำเสมอ | kernel (ยอมรับได้) | XNNPACK vs CMSIS-NN — ถ้า ≤ TOL ถือว่าผ่าน |
| โหลดไฟล์ไม่ขึ้นในเบราว์เซอร์ | ไฟล์ผิด | ใช้ model_web.tflite (float I/O) ไม่ใช่ไฟล์ int8 เต็ม |
scores ทีละคลาส ไม่ใช่ดูแค่ label — ตัวเลขบอกว่าเพี้ยนที่ขั้นไหนวิธีดีบักที่เร็วที่สุด: feed input ชุดเดียวกันเป๊ะ ทั้งสองฝั่ง แล้วไล่เทียบ output ทีละขั้น (หลัง normalize, หลัง quantize, หลัง invoke) — จุดที่เริ่มต่างคือจุดที่ front-end ไม่ตรง
อยากต่อยอดเรื่องรันโมเดลในเบราว์เซอร์และ deploy บน edge ลองดูของดีเหล่านี้ (ลิงก์ต้นทาง เปิดดูได้ตามสะดวก):
วิดีโอ (ช่องที่น่าเชื่อถือ)
ภาพ / เอกสารอ้างอิง
วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง — ลิงก์เหล่านี้ไว้ขุดต่อเอง
MVP ของบทเรียน 5.6–5.7 (เกณฑ์ผ่านของชุดบทเรียน): โมเดล .tflite ที่เทรนใน บทเรียน 5.3–5.5 ให้ verdict ในเบราว์เซอร์ ตรงกับฝั่ง PC ภายในเกณฑ์ (max|score_pc - score_web| ≤ TOL, คลาสที่ชนะตรงกัน)
--show-js) เทียบกับ s13_web.py ฝั่ง PC ถ้ายังไม่มีหน้าเว็บ ใช้ s13_web_full.py เทียบไฟล์ int8 กับไฟล์ web บน PC เป็นด่านแรก"ตรงกัน" ไม่ใช่แค่ "คลาสเดียวกัน" — คุณต้องวัดตัวเลขและบอกได้ว่าความต่างอยู่ในเกณฑ์ที่ยอมรับหรือไม่ เพราะอะไร
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 5 จุดในไฟล์ฝึก + ตารางช่องเติมหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs13_web.py มีโครงครบทั้งไฟล์ (โหลดข้อมูล/interpreter/JS snippet) เหลือแค่ 5 บรรทัดให้เติมs13_web.py เติมครบพร้อมคอมเมนต์อธิบายทุกขั้น (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s13_web_full.py แล็บ parity ครบวง: export ไฟล์ web + รันทั้ง int8/web ทั้งชุดทดสอบ + สรุป PASS/FAIL ตามเกณฑ์ MVP ของบทเรียน 5.6–5.7ลองเขียนเองให้สุดก่อนนะ ถ้าติดจริงๆ ค่อยเปิดเฉลยดูทีละช่อง แล้วกลับมาพิมพ์เอง — เดี๋ยวเราค่อย ๆ แกะไปด้วยกัน
การ deploy โมเดลข้ามเป้าหมายซ่อนแนวคิดหลายชั้นที่จะใช้ต่อในบทเรียน 5.8–5.9 และ โมดูล 6 (Apps):
ฝั่ง deploy / ระบบ
.tflite เดียวไป Web/Cortex-A ได้ MCU ต้อง Vela เพิ่ม (บทเรียน 5.8–5.9)convert_web.py)max-abs-diff + TOL ตัดสินฝั่งเครื่องมือ / runtime
.tflite ในเบราว์เซอร์ (เลือกเหนือ tfjs-tflite/ORT-Web)tflite-runtime โฉมใหม่ รันบน PC และ Cortex-A ด้วยสคริปต์เดียวทั้งหมดนี้ทำให้โมเดลของคุณจาก บทเรียน 5.3–5.5 ไม่ได้ติดอยู่บนบอร์ดตัวเดียว — มันพร้อมไปอยู่ทุกที่ที่งานต้องการ
การรันโมเดลในเบราว์เซอร์ไม่ใช่ของแปลกใหม่ มีสินค้า/เครื่องมือจริงทำแบบเดียวกัน:
จุดต่างของเรา: MicroPython-first + หลายเป้าหมาย + emulator ที่อนุมานจริง + ครบทั้ง pipeline — เท่าที่ผู้เขียนสำรวจ ยังไม่พบคอร์สหรือเครื่องมือที่รวมสี่อย่างนี้ไว้ด้วยกัน
งานทำเอง (ท้ายบทเรียน):
s13_web.py ให้ครบทั้ง 5 ช่อง รันได้จริงจาก shared/training/ แล้วอ่าน verdict ฝั่ง PC ออกs13_web_full.py เทียบ scores กับฝั่ง PC แล้วจดค่า max-abs-diffใบ้ข้อ 3 — ลองปิด normalization (ช่อง 1) ฝั่งใดฝั่งหนึ่งดู แล้วเทียบว่า max-abs-diff พุ่งขึ้นแค่ไหน จะเห็นว่า front-end สำคัญกว่าที่คิด
วันนี้เราได้: เข้าใจว่าโมเดลไฟล์เดียวไป Web/Cortex-A ได้ยังไง · ทำไมเบราว์เซอร์ต้องไฟล์ float I/O · reproduce front-end + วัด parity ด้วย max-abs-diff/TOL · เห็นว่า Cortex-A ใช้สคริปต์เดิม
ชุดบทเรียนถัดไป (บทเรียน 5.8–5.9) เราจะพาโมเดลตัวเดียวกันนี้ไป MCU ผ่าน Vela แล้วเทียบทั้งสามเป้าหมาย (MCU/Web/Cortex-A) ด้าน latency/accuracy/power — ปิดวง "train once, run everywhere" ให้ครบ เจอกันครับ