บทเรียน 5.6 — รันโมเดลบนเว็บ: LiteRT.js, int8 I/O และ parity

รันโมเดลบน Web และเรื่องราว Cortex-A

โมดูล 5 — ฝึกโมเดลและนำไปใช้หลายเป้าหมาย

โมดูล 5 · Training (Pillar 4) — เป้าหมายเดียว หลายที่รัน

คาถาประจำบทเรียน: "โมเดล .tflite ไฟล์เดียวที่เราเทรน รันได้ทั้งในเบราว์เซอร์และบน Linux SBC — แต่ 'รันได้' กับ 'ตอบตรงกัน' เป็นคนละเรื่อง"

จาก model_int8.tflite ตัวเดิม → LiteRT.js ในเบราว์เซอร์ + ai-edge-litert บน Cortex-A

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เปิดบทเรียนด้วยของจริงก่อน

เหมือนทุกบทเรียน เราเริ่มแบบ กลับด้าน — เปิดของที่ทำงานได้จริงก่อน แล้วค่อยแกะว่าทำไมมันถึงทำงาน

รันในเบราว์เซอร์ verdict เด้งบนหน้าเว็บ แกะดูข้างใน LiteRT.js + front-end พิสูจน์ parity Web = PC ไหม ต่อไป Cortex-A ไฟล์เดิมบน Linux

ในบทเรียน 5.3–5.5 เราเทรนโมเดล IMU (idle / circle / shaking) จนได้ model_int8.tflite แล้วรันบน PC ผ่าน eval_pc.py วันนี้เอาไฟล์นั้นไปเปิดในเบราว์เซอร์ — เห็น verdict เด้งจากในหน้าเว็บโดยไม่ต้องลงอะไรเลย นั่นคือความอัศจรรย์ที่เราจะแกะ

ชุดบทเรียนนี้เป้าหมายไม่ใช่แค่ "รันได้ในเว็บ" แต่คือ พิสูจน์ให้ได้ว่าเบราว์เซอร์ตอบเหมือน PC ภายในเกณฑ์ที่ยอมรับ — นี่คือ MVP ของบทเรียน 5.6–5.7

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เป้าหมายของชุดบทเรียนนี้

จบชุดบทเรียนนี้เราจะเดินครบเรื่องการ deploy โมเดลข้ามเป้าหมาย แล้วปิดท้ายด้วยการพิสูจน์ parity:

  1. runtime บนเบราว์เซอร์ — ทำไมเลือก LiteRT.js (ไม่ใช่ tfjs-tflite ที่ตายแล้ว หรือ ONNX-Runtime-Web)
  2. กับดัก int8 I/O — ทำไมโมเดล MCU อาจต้องมี "ไฟล์ web" อีกใบ และ convert_web.py สร้างมันยังไง
  3. front-end นอกกราฟ — สิ่งที่โมเดลไม่เห็น (normalize/DSP) ต้องทำซ้ำใน JS ให้ตรงเป๊ะ
  4. parity เป็นแล็บ — วัด max-abs-diff ระหว่าง PC กับ Web ไม่ใช่เชื่อว่าตรงเอง
  5. เรื่องราว Cortex-A — ไฟล์เดียวกันรันบน RPi/Jetson ด้วย ai-edge-litert ไม่ต้องแก้อะไร

ปลายทางวันนี้: window เดียวกันให้ verdict ตรงกันทั้งฝั่ง PC และเบราว์เซอร์ (LiteRT.js) ภายในเกณฑ์ TOL

วันนี้เราไม่ได้เทรนโมเดลใหม่ เราเอาโมเดลจาก บทเรียน 5.3–5.5 มา ส่งต่อให้หลายเป้าหมาย — งานของวิศวกร deploy คือทำให้ทุกที่ได้คำตอบเดียวกัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ย้อนดูว่าเรามาถึงไหนใน Pillar 4

โมดูล 5 (Training) เดินเป็นสี่ชุดบทเรียน วันนี้คือชุดบทเรียนที่สาม — ขั้น "กระจายไปหลายเป้าหมาย"

5.1–5.2 · Dataset เก็บ+จัดข้อมูลบนบอร์ด 5.3–5.5 · Train + PC TensorFlow → .tflite 5.6–5.7 · Web + Cortex-A วันนี้ · LiteRT.js + parity 5.8–5.9 · MCU/Vela int8 → Ethos-U55 "ไฟล์เดียว หลายเป้าหมาย" — วันนี้คือ Web กับ Linux SBC
  • บทเรียน 5.1–5.2 ให้ข้อมูล · บทเรียน 5.3–5.5 ให้ model_int8.tflite + .norm.npz และ model.keras เมื่อรัน train.py --save-keras — วันนี้เราใช้ของสามอย่างนี้ต่อ
  • บทเรียน 5.8–5.9 (ชุดบทเรียนถัดไป) จะพาไป MCU ผ่าน Vela ซึ่งเป็นเป้าหมายเดียวที่ต้องคอมไพล์เพิ่ม ที่เหลือใช้ไฟล์เดิม

ถ้าบทเรียนไหนหลุด กลับไปดู shared/training/ ได้ ทั้ง train.py eval_pc.py convert_web.py อยู่ที่นั่น เป็นชุดเดียวต่อกัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

train once, run everywhere — ซูมเข้าที่ Web กับ Cortex-A

บทเรียน 1.1–1.3 เราเห็นสเปกตรัมเป้าหมายผ่านๆ วันนี้เจาะสองช่องกลาง โดยยึด model_int8.tflite เป็นแหล่งความจริงเดียว

เป้าหมาย ทำอะไรกับไฟล์ runtime int8 บังคับ?
MCU + Ethos-U55 vela compile เพิ่ม (บทเรียน 5.8–5.9) TFLite-Micro ใช่ (NPU)
Web (เบราว์เซอร์) อาจต้องแปลงเป็น float I/O LiteRT.js ไม่ (§1)
Cortex-A (Linux) ใช้ไฟล์เดิม ไม่แก้ ai-edge-litert ไม่
PC / Docker ใช้ไฟล์เดิม TF / LiteRT ไม่
  • มีแค่ MCU ที่ต้องคอมไพล์เพิ่ม (Vela) เพราะ NPU อ่าน custom op ของมันเอง
  • Web เป็นที่เดียวที่อาจต้อง "แปลงครั้งที่สอง" ของโมเดลตัวเดิม (ไม่ใช่แค่ compile) — เดี๋ยวเราจะเห็นว่าทำไม
  • Cortex-A เอาไฟล์เดิมไปวางแล้วรัน eval_pc.py ได้เลย — สคริปต์ไม่ต้องแก้แม้แต่บรรทัดเดียว

ตารางนี้คือ "หนึ่งไฟล์ สี่เป้าหมาย" ฉบับย่อ — วันนี้เราลงมือกับสองช่องกลาง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ทำไมต้องรันบน Web

การเอาโมเดลไปไว้ในเบราว์เซอร์ไม่ใช่แค่ของเล่น มันแก้ปัญหาจริงของการส่งมอบงาน AI:

  • ไม่ต้องติดตั้งอะไร — ส่งลิงก์ให้ลูกค้าหรือผู้สอน เปิดปุ๊บเห็น verdict ปั๊บ ไม่ต้องมีบอร์ด ไม่ต้อง flash
  • pre-flight ก่อน flash — ลองโมเดลในเว็บก่อนเสียเวลา compile ลงบอร์ด (Edge Impulse ก็ทำแบบนี้)
  • เดโม + สอน — BENTO Emulator รันโมเดลท่ามือจริงในเบราว์เซอร์ (ผ่าน ONNX Runtime Web) ผู้เรียนเปิดเล่นต่อที่บ้านได้
  • ความเป็นส่วนตัวยังอยู่ — โมเดลรันในเครื่องผู้ใช้ ข้อมูล (เสียง/ท่าทาง) ไม่ต้องขึ้นเซิร์ฟเวอร์
.tflite ไฟล์จาก 5.3–5.5 เบราว์เซอร์ LiteRT.js · WebGPU verdict ไม่มีเซิร์ฟเวอร์

Edge AI ในเบราว์เซอร์คือ "Edge" อีกแบบ — ขอบของเครือข่ายอยู่ที่แท็บของผู้ใช้ ข้อมูลไม่ออกไปไหน เหมือนที่รันบนชิป

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

runtime บนเบราว์เซอร์ — เลือก LiteRT.js

ในเบราว์เซอร์มีหลายทางเลือกรัน ML แต่ไม่ใช่ทุกอันจะเหมาะ ผู้เขียนสำรวจแล้วสรุปไว้ดังนี้:

ทางเลือก สถานะ ตัดสิน
LiteRT.js (Google, 2026) โหลด .tflite ตรงๆ ผ่าน WASM/WebGPU เลือกอันนี้
@tensorflow/tfjs-tflite alpha ตั้งแต่ 2023 ไม่มี commit แล้ว ตายแล้ว อย่าใช้
ONNX-Runtime-Web โตเต็มที่ int8 ดี ต้องแปลง TFLite→ONNX (เพิ่ม hop) → fallback
WebNN origin-trial Chrome/Edge (~2027) ยังไม่พร้อม production
  • LiteRT.js ใช้ .tflite ฟอร์แมตเดียวกับบนบอร์ด — ไม่ต้องแปลงข้ามฟอร์แมต ลดโอกาสเพี้ยน
  • ORT-Web เก็บเป็นทางสำรอง: ถ้าต้อง ONNX อยู่แล้ว หรืออยากได้ int8 WASM ที่ปรับจูนมานาน แต่ต้องแปลงเพิ่มหนึ่งขั้น
  • BENTO Edge AI Emulator ใช้ทางสำรองนี้: แปลง model_int8.tflite เป็น ONNX ครั้งเดียวด้วย tf2onnx (int8 in/out) แล้วรันด้วย ONNX Runtime Web (WASM) จึงไม่ต้องมีไฟล์ float I/O แต่มีขั้นแปลงฟอร์แมตเพิ่มหนึ่งขั้น

การเลือก runtime คือการตัดสินใจเชิงวิศวกรรม ไม่ใช่แค่ "อันไหนดัง" — LiteRT.js ชนะเพราะ ฟอร์แมตเดียวกับ MCU ทำให้ parity ง่ายขึ้นมาก

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

LiteRT.js ทำงานยังไง

โหลดโมเดลในเบราว์เซอร์ด้วยสามบรรทัด แล้วเรียก run() เหมือน interpreter บน PC:

import {loadLiteRt, loadAndCompile} from '@litertjs/core';
await loadLiteRt('.../wasm/');               // โหลด WASM runtime ครั้งเดียว
const model = await loadAndCompile(
    'model_web.tflite', {accelerator: 'webgpu'});   // WebGPU ถ้ามี, ไม่งั้น WASM
const out = model.run([x]);                  // x = feature ที่เตรียมไว้แล้ว
  • loadLiteRt() โหลด WASM (XNNPACK) ครั้งเดียว · loadAndCompile() คอมไพล์กราฟ · run() อนุมาน
  • accelerator:'webgpu' ใช้ GPU ของเครื่องผ่านเบราว์เซอร์ ถ้าไม่รองรับก็ถอยไป WASM CPU อัตโนมัติ
  • สังเกตว่า run() รับ x ที่ เตรียม feature เสร็จแล้ว — กราฟไม่ได้ทำ front-end ให้ (จุดสำคัญของชุดบทเรียนนี้)

เทียบกับ PC: loadAndCompile ≈ Interpreter(model_path=...) + allocate_tensors(), และ model.run ≈ set_tensor + invoke + get_tensor — คนละภาษา แต่ท่าเดียวกัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

กับดัก int8 I/O — ทำไม Web อาจต้องไฟล์ที่สอง

โมเดลที่เราเทรนให้ MCU เป็น full-integer int8: input int8, output int8 (เพราะ Ethos-U55 บังคับ) แต่...

model_int8.tflite (MCU) int8 in → กราฟ → int8 out Ethos-U55 บังคับ full-integer model_web.tflite (เบราว์เซอร์) float32 in → กราฟ → float32 out LiteRT.js บังคับ I/O = float32/int32
  • LiteRT.js จำกัด I/O tensor เป็น float32/int32 ไฟล์ int8 เต็ม (int8 in/out) อาจโหลดไม่ได้ในเบราว์เซอร์
  • ทางแก้: ทำ weight-only / dynamic-range int8 (น้ำหนัก int8 แต่ I/O เป็น float) หรือ float32 ล้วน
  • ไฟล์ web เล็กกว่า float ~4 เท่า (เพราะน้ำหนักยัง int8) แต่ผ่านข้อจำกัด I/O ของเบราว์เซอร์

นี่คือที่เดียวในทั้งคอร์สที่ต้อง แปลงโมเดลครั้งที่สอง ไม่ใช่แค่ compile — และต้องลองจริงว่าไฟล์ไหนโหลดได้ในเบราว์เซอร์

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

convert_web.py — สร้างไฟล์ web จากน้ำหนักชุดเดียว

ตัวช่วยที่เตรียมไว้ให้แล้วใน convert_web.py แปลง model.keras เดิม → ไฟล์ web:

model = tf.keras.models.load_model("model.keras")   # Keras ตัวเดียวกับที่เทรน
conv = tf.lite.TFLiteConverter.from_keras_model(model)
conv.optimizations = [tf.lite.Optimize.DEFAULT]      # dynamic-range: int8 weights, float I/O
tflite = conv.convert()
open("model_web.tflite", "wb").write(tflite)
  • แปลงจาก Keras ตัวเดิม ไม่ใช่จากไฟล์ int8 — จึงพิสูจน์ได้ว่ามาจากน้ำหนักชุดเดียวกับโมเดล MCU
  • Optimize.DEFAULT โดยไม่ให้ representative dataset = dynamic-range (น้ำหนัก int8, activation/IO float)
  • ไฟล์ที่ได้ browser-clean: ไม่มี ethos-u custom op รันได้ทั้งในเบราว์เซอร์และบน Cortex-A

model.keras ได้จาก train.py --save-keras แล้วรันผ่าน Docker เดียวกับ บทเรียน 5.3–5.5: docker run ... python convert_web.py --keras model.keras --out model_web.tflite — ไม่ต้องลง TensorFlow บนเครื่องตัวเอง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ของจริงที่ต้องพูดตรงๆ — โมเดล DEEPCRAFT ที่ shipped

มีเรื่องหนึ่งที่ต้องซื่อสัตย์: โมเดล 6 ตัวที่ติดมากับเฟิร์มแวร์ ไม่ได้โหลดในเบราว์เซอร์ได้ทุกตัว

  • โมเดล int8 ของ NPU (Motion, Baby Cry) ถูก Vela-compile มาแล้ว มี ethos-u custom op → เบราว์เซอร์/interpreter ทั่วไปโหลดไม่ได้
  • โมเดล radar เป็น float32 ไม่มี custom op → โหลดในเบราว์เซอร์ได้ (เป็นหลักฐานว่า DEEPCRAFT .tflite ทำ browser-clean ได้)
  • โมเดลที่ผู้เรียนเทรนเอง (export จาก TF แบบ CPU-only ตามชุดบทเรียนนี้) → browser-portable โดยปริยาย
Vela int8 (Motion/BabyCry) มี ethos-u custom op → เบราว์เซอร์โหลดไม่ได้ โมเดลของคุณ / radar float32 ops มาตรฐาน → รันในเบราว์เซอร์ได้

บทเรียน: สิ่งที่ทำให้ "browser-portable" คือ ไม่มี custom op เฉพาะ NPU — โมเดลที่คุณ export เองในชุดบทเรียนนี้ปลอดภัยอยู่แล้ว ส่วนไฟล์ Vela เก็บไว้ให้ MCU

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

สิ่งที่กราฟไม่เห็น — front-end อยู่นอกกราฟ

หัวใจที่คนพลาดบ่อยที่สุด: โมเดล .tflite เห็นแค่ feature vector ที่แปลงเสร็จแล้ว ส่วนขั้นแปลงสัญญาณอยู่ นอกกราฟ

front-end (นอกกราฟ) normalize · window · (เสียง: FFT/Mel/log) กราฟ .tflite เห็นแค่ feature ที่แปลงเสร็จ scores ความน่าจะเป็นต่อคลาส คุณต้องทำซ้ำขั้นนี้ใน JS/Python ให้ตรง
  • โมเดล IMU ของเรา front-end คือ normalize ด้วย mean/std (จาก .norm.npz) + จัด window
  • โมเดลเสียง front-end หนักกว่ามาก: FFT → Mel → log (เป็น C ที่เรียก CMSIS-DSP ตอนอยู่บนบอร์ด)
  • ถ้าจะรันในเบราว์เซอร์/PC ต้อง สร้าง front-end ขึ้นมาใหม่ ให้ผลออกมาเหมือนกันเป๊ะ

นี่คือบทเรียน "โมเดลเห็นอะไรจริงๆ" (บทเรียน 4.1–4.2) กลับมาทวงคืน — ถ้า front-end คนละแบบ ต่อให้กราฟเดียวกัน verdict ก็เพี้ยน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

parity คือแล็บ ไม่ใช่ข้อสมมติ

คำเตือนสำคัญ: เบราว์เซอร์กับบอร์ด ไม่การันตีว่าได้เลขเป๊ะทุกบิต

  • เบราว์เซอร์ใช้ XNNPACK · บอร์ดใช้ CMSIS-NN · CMSIS-NN bit-exact กับ TFLite reference kernel ไม่ใช่กับ XNNPACK
  • แปลว่าคะแนนอาจต่างกันนิดหน่อยแม้เป็นโมเดลไฟล์เดียวกัน — เรื่องปกติ ไม่ใช่บั๊ก
  • วิธีที่ถูก: วัด ความต่าง (max|score_pc - score_web|) แล้วตั้งเกณฑ์ยอมรับ TOL เช่น 0.02
diff = float(np.max(np.abs(s_int8 - s_web)))
ok = diff <= TOL and s_int8.argmax() == s_web.argmax()

"ตอบตรงกัน" ในงาน embedded ไม่ได้แปลว่าเท่ากันเป๊ะ แต่แปลว่า คลาสที่ชนะตรงกัน + คะแนนต่างในเกณฑ์ — เราจึงต้องวัด ไม่ใช่หวังเอา

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตเบื้องหลัง int8 — quantize / dequantize

สองสูตรนี้คือหัวใจของ เติม 2 กับ เติม 4 ที่เราเพิ่งไล่โค้ด เขียนเป็นคณิตให้เห็นชัดว่าทำอะไร:

ตอนป้อนเข้ากราฟ (quantize float → int8):

q=clip ⁣(round⁡ ⁣(xs+z), −128, 127)q = \mathrm{clip}\!\left(\operatorname{round}\!\left(\frac{x}{s} + z\right),\,-128,\,127\right)

ตอนอ่านผลออกมา (dequantize int8 → float):

x^=(q−z)⋅s\hat{x} = (q - z)\cdot s

อ่านทีละตัวแบบภาษาคน:

  • xx — ค่า feature แบบ float หลัง normalize (ผลของ เติม 1)
  • ss — scale คือ "float กี่หน่วยต่อ int8 หนึ่งขั้น" อ่านมาจากโมเดล inp["quantization"] โดยตรง
  • zz — zero-point คือค่า int8 ที่แทน 0.00.0 พอดี ก็อ่านจากโมเดลเช่นกัน
  • qq — ค่า int8 ที่ป้อนเข้ากราฟจริง · x^\hat{x} — ค่า float ที่ได้กลับมาหลัง dequantize
  • ช่วง int8 มี 28=2562^{8}=256 ระดับ (−128…127-128 \ldots 127) จึงต้อง clip\mathrm{clip} กันค่าล้น

ทำไมสำคัญกับชุดบทเรียนนี้: ss กับ zz ต้อง อ่านจากโมเดล ไม่ใช่เดา — ใส่ผิดแม้นิดเดียว input ก็เพี้ยนตั้งแต่ยังไม่ถึงกราฟ และไฟล์ web (float I/O) ไม่ต้องทำสองสูตรนี้เลย นั่นคือความต่างเดียวของเส้นทางเบราว์เซอร์

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตของ parity — วัดความต่าง ไม่ใช่หวังให้เท่า

เกณฑ์ผ่าน MVP ของบทเรียน 5.6–5.7 ทั้งอันเขียนเป็นสมการได้สั้นๆ แค่สองบรรทัด:

d  =  max⁡k ∣ skpc−skweb ∣d \;=\; \max_{k}\,\bigl|\,s^{\text{pc}}_{k} - s^{\text{web}}_{k}\,\bigr|

PASS  ⟺  d≤TOL    ∧    arg max⁡k skpc=arg max⁡k skweb\textbf{PASS} \iff d \le \mathrm{TOL} \;\;\wedge\;\; \operatorname*{arg\,max}_{k}\, s^{\text{pc}}_{k} = \operatorname*{arg\,max}_{k}\, s^{\text{web}}_{k}

  • skpc, skwebs^{\text{pc}}_{k},\, s^{\text{web}}_{k} — คะแนน softmax ของคลาส kk ฝั่ง PC และฝั่งเบราว์เซอร์
  • dd — ความต่างสูงสุดข้ามทุกคลาส (ก็คือ max-abs-diff ในโค้ด)
  • TOL\mathrm{TOL} — เกณฑ์ยอมรับ เช่น 0.020.02 · arg max⁡\operatorname{arg\,max} — ดัชนีคลาสที่ชนะ

คะแนนเป็น softmax head ในกราฟ จึงรวมได้ราว 1.01.0 เสมอ:

σ(o)k=e ok∑je oj,∑kσ(o)k=1\sigma(o)_k = \frac{e^{\,o_k}}{\sum_j e^{\,o_j}}, \qquad \sum_k \sigma(o)_k = 1

ทำไมสำคัญกับชุดบทเรียนนี้: XNNPACK (เบราว์เซอร์) กับ CMSIS-NN (บอร์ด) ไม่ bit-exact ต่อกัน dd จึงไม่จำเป็นต้องเป็น 00 — สมการบรรทัดที่สองคือ MVP ของบทเรียน 5.6–5.7 ทั้งอัน "คลาสตรง + ต่างในเกณฑ์" เขียนเป็นคณิต

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

front-end คือจุดที่ parity พังบ่อยที่สุด

ถ้า parity ไม่ผ่าน 9 ใน 10 ครั้งปัญหาอยู่ที่ front-end ไม่ใช่ตัวโมเดล และตัวที่พังบ่อยสุดคือ normalization

# ตอนเทรน (train.py) เราเซฟ mean/std ที่ fit จาก train set ไว้:
np.savez(out + ".norm.npz", mean=mean, std=std)

# ตอน deploy ทุกเป้าหมายต้อง normalize ด้วยชุดเดียวกันนี้:
x = (window - z["mean"]) / z["std"]      # PC / Cortex-A
// x = window.map((v,i) => (v - mean[i%6]) / std[i%6])   // เบราว์เซอร์
  • ถ้าฝั่งหนึ่งลืม normalize หรือใช้ mean/std คนละชุด โมเดลจะเห็นข้อมูลคนละสเกล → verdict เพี้ยนทันที
  • เราจึงเซฟ mean/std เป็นไฟล์ติดไปกับโมเดลเสมอ (silent-failure point ที่คลาสสิกมาก)
  • quantize (float→int8) ก็ต้องใช้ scale/zero ที่ อ่านจากโมเดล ไม่ใช่เดา

จำประโยคนี้ไว้: "โมเดลเดียวกัน ข้อมูลคนละสเกล = คนละโมเดล" — parity เริ่มพังตั้งแต่ยังไม่ถึงกราฟ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0