จบชุดบทเรียนนี้เราจะเดินครบเรื่องการ deploy โมเดลข้ามเป้าหมาย แล้วปิดท้ายด้วยการพิสูจน์ parity:
convert_web.py สร้างมันยังไงai-edge-litert ไม่ต้องแก้อะไรปลายทางวันนี้: window เดียวกันให้ verdict ตรงกันทั้งฝั่ง PC และเบราว์เซอร์ (LiteRT.js) ภายในเกณฑ์ TOL
วันนี้เราไม่ได้เทรนโมเดลใหม่ เราเอาโมเดลจาก บทเรียน 5.3–5.5 มา ส่งต่อให้หลายเป้าหมาย — งานของวิศวกร deploy คือทำให้ทุกที่ได้คำตอบเดียวกัน
โมดูล 5 (Training) เดินเป็นสี่ชุดบทเรียน วันนี้คือชุดบทเรียนที่สาม — ขั้น "กระจายไปหลายเป้าหมาย"
model_int8.tflite + .norm.npz และ model.keras เมื่อรัน train.py --save-keras — วันนี้เราใช้ของสามอย่างนี้ต่อถ้าบทเรียนไหนหลุด กลับไปดู
shared/training/ได้ ทั้งtrain.pyeval_pc.pyconvert_web.pyอยู่ที่นั่น เป็นชุดเดียวต่อกัน
บทเรียน 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 | ไม่ |
eval_pc.py ได้เลย — สคริปต์ไม่ต้องแก้แม้แต่บรรทัดเดียวตารางนี้คือ "หนึ่งไฟล์ สี่เป้าหมาย" ฉบับย่อ — วันนี้เราลงมือกับสองช่องกลาง
การเอาโมเดลไปไว้ในเบราว์เซอร์ไม่ใช่แค่ของเล่น มันแก้ปัญหาจริงของการส่งมอบงาน AI:
Edge AI ในเบราว์เซอร์คือ "Edge" อีกแบบ — ขอบของเครือข่ายอยู่ที่แท็บของผู้ใช้ ข้อมูลไม่ออกไปไหน เหมือนที่รันบนชิป
ในเบราว์เซอร์มีหลายทางเลือกรัน 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 |
.tflite ฟอร์แมตเดียวกับบนบอร์ด — ไม่ต้องแปลงข้ามฟอร์แมต ลดโอกาสเพี้ยนmodel_int8.tflite เป็น ONNX ครั้งเดียวด้วย tf2onnx (int8 in/out) แล้วรันด้วย ONNX Runtime Web (WASM) จึงไม่ต้องมีไฟล์ float I/O แต่มีขั้นแปลงฟอร์แมตเพิ่มหนึ่งขั้นการเลือก runtime คือการตัดสินใจเชิงวิศวกรรม ไม่ใช่แค่ "อันไหนดัง" — LiteRT.js ชนะเพราะ ฟอร์แมตเดียวกับ MCU ทำให้ parity ง่ายขึ้นมาก
โหลดโมเดลในเบราว์เซอร์ด้วยสามบรรทัด แล้วเรียก 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— คนละภาษา แต่ท่าเดียวกัน
โมเดลที่เราเทรนให้ MCU เป็น full-integer int8: input int8, output int8 (เพราะ Ethos-U55 บังคับ) แต่...
นี่คือที่เดียวในทั้งคอร์สที่ต้อง แปลงโมเดลครั้งที่สอง ไม่ใช่แค่ compile — และต้องลองจริงว่าไฟล์ไหนโหลดได้ในเบราว์เซอร์
ตัวช่วยที่เตรียมไว้ให้แล้วใน 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)
Optimize.DEFAULT โดยไม่ให้ representative dataset = dynamic-range (น้ำหนัก int8, activation/IO float)
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 บนเครื่องตัวเอง
มีเรื่องหนึ่งที่ต้องซื่อสัตย์: โมเดล 6 ตัวที่ติดมากับเฟิร์มแวร์ ไม่ได้โหลดในเบราว์เซอร์ได้ทุกตัว
ethos-u custom op → เบราว์เซอร์/interpreter ทั่วไปโหลดไม่ได้.tflite ทำ browser-clean ได้)บทเรียน: สิ่งที่ทำให้ "browser-portable" คือ ไม่มี custom op เฉพาะ NPU — โมเดลที่คุณ export เองในชุดบทเรียนนี้ปลอดภัยอยู่แล้ว ส่วนไฟล์ Vela เก็บไว้ให้ MCU
หัวใจที่คนพลาดบ่อยที่สุด: โมเดล .tflite เห็นแค่ feature vector ที่แปลงเสร็จแล้ว ส่วนขั้นแปลงสัญญาณอยู่ นอกกราฟ
.norm.npz) + จัด windowนี่คือบทเรียน "โมเดลเห็นอะไรจริงๆ" (บทเรียน 4.1–4.2) กลับมาทวงคืน — ถ้า front-end คนละแบบ ต่อให้กราฟเดียวกัน verdict ก็เพี้ยน
คำเตือนสำคัญ: เบราว์เซอร์กับบอร์ด ไม่การันตีว่าได้เลขเป๊ะทุกบิต
max|score_pc - score_web|) แล้วตั้งเกณฑ์ยอมรับ TOL เช่น 0.02diff = float(np.max(np.abs(s_int8 - s_web)))
ok = diff <= TOL and s_int8.argmax() == s_web.argmax()
"ตอบตรงกัน" ในงาน embedded ไม่ได้แปลว่าเท่ากันเป๊ะ แต่แปลว่า คลาสที่ชนะตรงกัน + คะแนนต่างในเกณฑ์ — เราจึงต้องวัด ไม่ใช่หวังเอา
สองสูตรนี้คือหัวใจของ เติม 2 กับ เติม 4 ที่เราเพิ่งไล่โค้ด เขียนเป็นคณิตให้เห็นชัดว่าทำอะไร:
ตอนป้อนเข้ากราฟ (quantize float → int8):
ตอนอ่านผลออกมา (dequantize int8 → float):
อ่านทีละตัวแบบภาษาคน:
inp["quantization"] โดยตรงทำไมสำคัญกับชุดบทเรียนนี้: กับ ต้อง อ่านจากโมเดล ไม่ใช่เดา — ใส่ผิดแม้นิดเดียว input ก็เพี้ยนตั้งแต่ยังไม่ถึงกราฟ และไฟล์ web (float I/O) ไม่ต้องทำสองสูตรนี้เลย นั่นคือความต่างเดียวของเส้นทางเบราว์เซอร์
เกณฑ์ผ่าน MVP ของบทเรียน 5.6–5.7 ทั้งอันเขียนเป็นสมการได้สั้นๆ แค่สองบรรทัด:
max-abs-diff ในโค้ด)คะแนนเป็น softmax head ในกราฟ จึงรวมได้ราว เสมอ:
ทำไมสำคัญกับชุดบทเรียนนี้: XNNPACK (เบราว์เซอร์) กับ CMSIS-NN (บอร์ด) ไม่ bit-exact ต่อกัน จึงไม่จำเป็นต้องเป็น — สมการบรรทัดที่สองคือ MVP ของบทเรียน 5.6–5.7 ทั้งอัน "คลาสตรง + ต่างในเกณฑ์" เขียนเป็นคณิต
ถ้า 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]) // เบราว์เซอร์
mean/std เป็นไฟล์ติดไปกับโมเดลเสมอ (silent-failure point ที่คลาสสิกมาก)scale/zero ที่ อ่านจากโมเดล ไม่ใช่เดาจำประโยคนี้ไว้: "โมเดลเดียวกัน ข้อมูลคนละสเกล = คนละโมเดล" — parity เริ่มพังตั้งแต่ยังไม่ถึงกราฟ