รันโมเดลบนเว็บ: LiteRT.js, int8 I/O และ parity
โมดูล 5 — ฝึกโมเดลและนำไปใช้หลายเป้าหมาย · สไลด์: slides.md · ภาพรวมโมดูล · หน้าหลักสูตร
พาโมเดลตัวเดิมไปรันในเบราว์เซอร์ เลือก runtime อย่างมีเหตุผล (LiteRT.js, ONNX Runtime Web, tfjs-tflite, WebNN) เข้าใจกับดัก int8 I/O ที่ทำให้ต้องมีไฟล์ web อีกใบ front-end ที่อยู่นอกกราฟ คณิตของ quantize กับ dequantize และนิยาม parity ที่วัดด้วย max-abs-diff กับเกณฑ์ TOL
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”เมื่อจบบทเรียนนี้ คุณจะ:
- เปรียบเทียบ runtime บนเบราว์เซอร์สี่ตัวและให้เหตุผลการเลือกได้ รวมถึงอธิบายว่าทำไม BENTO Emulator จึงแปลงโมเดลเป็น ONNX แล้วรันด้วย ONNX Runtime Web
- อธิบายว่าทำไมโมเดล int8 เต็มของ MCU อาจต้องมีไฟล์ web อีกใบ และ convert_web.py สร้างไฟล์ weight-only int8 ที่ I/O เป็น float จากน้ำหนัก Keras ชุดเดียวกันอย่างไร
- คำนวณ quantize q = clip(round(x/s + z), −128, 127) และ dequantize x̂ = (q − z)·s จากค่า scale และ zero-point ที่กำหนด
- ตัดสิน parity ด้วย d = max|s_pc − s_web| ≤ TOL และคลาสที่ชนะตรงกัน พร้อมอธิบายว่าทำไม d ไม่จำเป็นต้องเป็นศูนย์ และทำไม front-end คือจุดที่พังบ่อยที่สุด
ก่อนเริ่ม
หัวข้อที่มีชื่อว่า “ก่อนเริ่ม”ผ่านชุดบทเรียน 5.3–5.5 มาแล้ว มี model_int8.tflite กับ model_int8.tflite.norm.npz และถ้าจะทำไฟล์ web ให้รัน train.py --save-keras เพื่อได้ model.keras
เปิด BENTO Emulator เลือกโมเดล Motion แล้วเปิดสวิตช์ REAL ไว้ดูประกอบ
- อุปกรณ์: คอมพิวเตอร์ของคุณ ไม่ต้องใช้บอร์ด (หรือใช้ BENTO Emulator ใน BENTO IDE ประกอบ) — ใช้ PC และเบราว์เซอร์ BENTO Emulator ใช้ดูตัวอย่างโมเดลท่ามือที่รันจริงในเบราว์เซอร์
- เรียนมาก่อน: บทเรียน 5.5 — ลงมือทำ: เติมสคริปต์ฝึกแล้วรันใน Docker
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”ในแผง Edge AI ของ BENTO Emulator เลือกโมเดล Motion แล้วเปิดสวิตช์ REAL (ป้ายเปลี่ยนจาก MOCK เป็น REAL) แถบความมั่นใจสามคลาสจะขยับตาม IMU จำลอง นี่คือโมเดลท่ามือที่ฝึกด้วย pipeline เดียวกับเรา รันในแท็บเบราว์เซอร์โดยไม่มีเซิร์ฟเวอร์ ถามตัวเองว่ามันรู้ mean/std กับ scale ของโมเดลได้อย่างไร
โมเดลในเบราว์เซอร์ไม่ต้องติดตั้งอะไร ส่งลิงก์ก็เห็น verdict ลองก่อน flash ได้ และข้อมูลไม่ออกจากเครื่องผู้ใช้ ผู้เขียนสำรวจ runtime ไว้ดังนี้:
LiteRT.js (@litertjs/core) โหลด .tflite ฟอร์แมตเดียวกับ MCU ผ่าน WASM หรือ WebGPU ด้วย loadLiteRt(), loadAndCompile() และ run() จึงเป็นตัวเลือกหลัก
@tensorflow/tfjs-tflite ไม่มีการพัฒนาต่อแล้ว ONNX Runtime Web โตเต็มที่และรองรับ int8 ดี แต่ต้องแปลง TFLite เป็น ONNX เพิ่มหนึ่งขั้น และ WebNN ยังไม่พร้อมใช้งานจริง
BENTO Emulator เลือกทางของ ONNX Runtime Web: แปลง model_int8.tflite ด้วย tf2onnx ครั้งเดียว (int8 in/out) แล้วทำ front-end เองใน JS
โมเดลของ MCU เป็น full-integer int8 (int8 in/out) ตามที่ Ethos-U55 ต้องการ ผู้เขียนพบว่า LiteRT.js รับ I/O เป็น float32/int32 ไฟล์นี้จึงอาจโหลดไม่ได้
convert_web.py แก้ด้วยการแปลง Keras ตัวเดิม เป็น model_web.tflite แบบ dynamic-range (Optimize.DEFAULT โดยไม่มี representative dataset)
น้ำหนักเป็น int8 แต่ I/O เป็น float เล็กกว่า float ล้วนราวสี่เท่าและมาจากน้ำหนักชุดเดียวกับ MCU สิ่งที่ทำให้ไฟล์ “browser-clean” คือไม่มี custom op ของ NPU
ไฟล์ที่ผ่าน Vela แล้วจึงเก็บไว้ให้ MCU เท่านั้น
กราฟเห็นแค่ feature ที่เตรียมเสร็จแล้ว front-end อยู่นอกกราฟ ของเราคือ normalize ด้วย mean/std จาก .norm.npz (โมเดลเสียงหนักกว่านั้นมาก คือ FFT, Mel, log)
ไฟล์ int8 ต้อง quantize ขาเข้า $q = \mathrm{clip}(\mathrm{round}(x/s + z), -128, 127)$ และ dequantize ขาออก $\hat{x} = (q - z)\cdot s$ โดยอ่าน $s, z$ จากโมเดลเสมอ
parity คือการวัด ไม่ใช่ข้อสมมติ: เบราว์เซอร์ใช้ kernel ของ XNNPACK ส่วนบอร์ดใช้ CMSIS-NN ที่ไม่ bit-exact ต่อกัน เราจึงตัดสินด้วย
$d = \max_k |s^{pc}_k - s^{web}_k| \le \mathrm{TOL}$ (เช่น 0.02) และคลาสที่ชนะต้องตรงกัน ถ้าไม่ผ่าน เก้าในสิบครั้งปัญหาอยู่ที่ front-end โดยเฉพาะ normalization
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”สไลด์ของบทเรียนนี้อ้างถึงไฟล์ที่อยู่ในบทเรียนอื่นหรือใน shared/ ด้วย:
- shared/training
- shared/training/convert_web.py — Prepare the trained model for the browser (BENTO Edge AI Emulator / any web page).
- shared/training/eval_pc.py — Run the exported int8 .tflite on the PC and report accuracy + confusion.
- shared/training/model_int8.tflite
- shared/training/train.py — Train a tiny IMU gesture classifier and export it as int8 TFLite.
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”คำถามชุดเดียวกันอยู่ใน quiz.yaml สำหรับระบบที่ตรวจอัตโนมัติ
-
ข้อได้เปรียบหลักของ LiteRT.js เหนือ ONNX Runtime Web ในคอร์สนี้คืออะไร (เลือกหนึ่งข้อ · เป้าหมายข้อ 1)
- ก) เร็วกว่าเสมอทุกเครื่อง
- ข) โหลด .tflite ฟอร์แมตเดียวกับ MCU ได้ตรง ๆ ไม่ต้องแปลงข้ามฟอร์แมต
- ค) รองรับเฉพาะ int8
- ง) ไม่ต้องทำ front-end
เฉลย
ข — ลดขั้นแปลงที่อาจทำให้เพี้ยน ส่วน ONNX Runtime Web ต้องแปลง TFLite เป็น ONNX ก่อน ซึ่งเป็นทางที่ BENTO Emulator เลือกเพื่อรันไฟล์ int8 in/out
-
convert_web.py ตั้ง Optimize.DEFAULT โดยไม่ผูก representative dataset ได้ไฟล์แบบใด (เลือกหนึ่งข้อ · เป้าหมายข้อ 2)
- ก) full-integer int8 (int8 in/out)
- ข) dynamic-range: น้ำหนัก int8 แต่ activation และ I/O เป็น float
- ค) float16 ล้วน
- ง) ไฟล์ที่ผ่าน Vela แล้ว
เฉลย
ข — ไม่มีตัวอย่างไว้ calibrate converter จึงบีบได้แค่น้ำหนัก ไฟล์เล็กลงราวสี่เท่าและ I/O ยังเป็น float ที่เบราว์เซอร์รับได้
-
input มี scale s = 0.05 และ zero-point z = −10 ค่า x = 1.2 ถูก quantize เป็นเท่าไร (เลือกหนึ่งข้อ · เป้าหมายข้อ 3)
- ก) 14
- ข) 24
- ค) 34
- ง) −10
เฉลย
ก — round(1.2 / 0.05 + (−10)) = round(24 − 10) = 14 และ dequantize กลับได้ (14 + 10) × 0.05 = 1.2
-
s_pc = [0.10, 0.85, 0.05] และ s_web = [0.07, 0.88, 0.05] ด้วย TOL = 0.02 ผลเป็นอย่างไร (เลือกหนึ่งข้อ · เป้าหมายข้อ 4)
- ก) ผ่าน เพราะคลาสที่ชนะตรงกัน
- ข) ไม่ผ่าน เพราะ d = 0.03 เกิน TOL แม้คลาสที่ชนะจะตรงกัน
- ค) ผ่าน เพราะ d = 0.00
- ง) ไม่ผ่าน เพราะคลาสที่ชนะต่างกัน
เฉลย
ข — ต้องผ่านทั้งสองเงื่อนไข d = max(0.03, 0.03, 0.00) = 0.03 > 0.02 จึงไม่ผ่าน ให้ไล่ตรวจ front-end ก่อน
-
เบราว์เซอร์ทายคนละคลาสกับ PC เกือบทุก window ควรตรวจอะไรก่อน (เลือกหนึ่งข้อ · เป้าหมายข้อ 4)
- ก) kernel XNNPACK กับ CMSIS-NN
- ข) normalization ว่าทั้งสองฝั่งใช้ mean/std ชุดเดียวกันจาก .norm.npz หรือไม่
- ค) ความเร็วอินเทอร์เน็ต
- ง) จำนวน epoch
เฉลย
ข — kernel ต่างกันทำให้คะแนนต่างเล็กน้อยสม่ำเสมอ แต่ถ้าคลาสเพี้ยนทั้งกระดาน front-end โดยเฉพาะ normalize มักเป็นต้นเหตุ
- เขียนตารางเปรียบเทียบ runtime สี่ตัวในบันทึกการเรียน พร้อมเหตุผลว่าคุณจะเลือกตัวใดสำหรับหน้าเว็บเดโมของคุณ
- ถ้าติดตั้ง Docker ไว้ รัน
train.py --save-kerasแล้วconvert_web.pyเทียบขนาดmodel_int8.tfliteกับmodel_web.tflite - คำนวณ quantize และ dequantize ของ x = 0.8 ด้วย s = 0.04, z = −5 แล้วดูว่าค่าที่ได้กลับมาคลาดจากเดิมเท่าไร
บทเรียน 5.7 เราจะเติม s13_web.py ให้ครบสี่ขั้น สร้าง ground truth ฝั่ง PC วัด parity และฟังเรื่องราวของ Cortex-A
บทเรียนถัดไป: บทเรียน 5.7 — ลงมือทำ: verdict บนเว็บให้ตรงกับ PC และเรื่องราว Cortex-A
สะท้อนคิด
หัวข้อที่มีชื่อว่า “สะท้อนคิด”- ถ้าต้องส่งเดโมให้ลูกค้าที่ไม่มีบอร์ด คุณจะเลือก runtime ใด และต้องส่งไฟล์อะไรไปบ้าง
- ทำไม “คลาสที่ชนะตรงกัน” อย่างเดียวจึงยังไม่พอเป็นหลักฐานของ parity
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
ข้อได้เปรียบหลักของ LiteRT.js เหนือ ONNX Runtime Web ในคอร์สนี้คืออะไร (เป้าหมายข้อ 1)
- เร็วกว่าเสมอทุกเครื่อง
- โหลด .tflite ฟอร์แมตเดียวกับ MCU ได้ตรง ๆ ไม่ต้องแปลงข้ามฟอร์แมต
- รองรับเฉพาะ int8
- ไม่ต้องทำ front-end
ดูเฉลย
คำตอบ: B. โหลด .tflite ฟอร์แมตเดียวกับ MCU ได้ตรง ๆ ไม่ต้องแปลงข้ามฟอร์แมต
ลดขั้นแปลงที่อาจทำให้เพี้ยน ส่วน ONNX Runtime Web ต้องแปลง TFLite เป็น ONNX ก่อน ซึ่งเป็นทางที่ BENTO Emulator เลือกเพื่อรันไฟล์ int8 in/out
-
convert_web.py ตั้ง Optimize.DEFAULT โดยไม่ผูก representative dataset ได้ไฟล์แบบใด (เป้าหมายข้อ 2)
- full-integer int8 (int8 in/out)
- dynamic-range: น้ำหนัก int8 แต่ activation และ I/O เป็น float
- float16 ล้วน
- ไฟล์ที่ผ่าน Vela แล้ว
ดูเฉลย
คำตอบ: B. dynamic-range: น้ำหนัก int8 แต่ activation และ I/O เป็น float
ไม่มีตัวอย่างไว้ calibrate converter จึงบีบได้แค่น้ำหนัก ไฟล์เล็กลงราวสี่เท่าและ I/O ยังเป็น float ที่เบราว์เซอร์รับได้
-
input มี scale s = 0.05 และ zero-point z = −10 ค่า x = 1.2 ถูก quantize เป็นเท่าไร (เป้าหมายข้อ 3)
- 14
- 24
- 34
- −10
ดูเฉลย
คำตอบ: A. 14
round(1.2 / 0.05 + (−10)) = round(24 − 10) = 14 และ dequantize กลับได้ (14 + 10) × 0.05 = 1.2
-
s_pc = [0.10, 0.85, 0.05] และ s_web = [0.07, 0.88, 0.05] ด้วย TOL = 0.02 ผลเป็นอย่างไร (เป้าหมายข้อ 4)
- ผ่าน เพราะคลาสที่ชนะตรงกัน
- ไม่ผ่าน เพราะ d = 0.03 เกิน TOL แม้คลาสที่ชนะจะตรงกัน
- ผ่าน เพราะ d = 0.00
- ไม่ผ่าน เพราะคลาสที่ชนะต่างกัน
ดูเฉลย
คำตอบ: B. ไม่ผ่าน เพราะ d = 0.03 เกิน TOL แม้คลาสที่ชนะจะตรงกัน
ต้องผ่านทั้งสองเงื่อนไข d = max(0.03, 0.03, 0.00) = 0.03 > 0.02 จึงไม่ผ่าน ให้ไล่ตรวจ front-end ก่อน
-
เบราว์เซอร์ทายคนละคลาสกับ PC เกือบทุก window ควรตรวจอะไรก่อน (เป้าหมายข้อ 4)
- kernel XNNPACK กับ CMSIS-NN
- normalization ว่าทั้งสองฝั่งใช้ mean/std ชุดเดียวกันจาก .norm.npz หรือไม่
- ความเร็วอินเทอร์เน็ต
- จำนวน epoch
ดูเฉลย
คำตอบ: B. normalization ว่าทั้งสองฝั่งใช้ mean/std ชุดเดียวกันจาก .norm.npz หรือไม่
kernel ต่างกันทำให้คะแนนต่างเล็กน้อยสม่ำเสมอ แต่ถ้าคลาสเพี้ยนทั้งกระดาน front-end โดยเฉพาะ normalize มักเป็นต้นเหตุ
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"รันโมเดลบนเว็บ: LiteRT.js, int8 I/O และ parity" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Running the model on the web: LiteRT.js, int8 I/O and parity" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/edge-ai-developer/m05-training/l06-web-runtime/
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA