Hands-on: a web verdict that matches the PC, and the Cortex-A story
Module 5 — Training and deploying to several targets · Slides: slides.md · Module overview · Course page
Fill five points in s13_web.py so it performs the same four steps as the browser — normalize, quantize, invoke, dequantize — giving a PC verdict as ground truth, then measure parity against the web file with max-abs-diff. Close with the Cortex-A story, where the same eval_pc.py just runs, and choosing a target with reasons.
Objectives
Section titled “Objectives”By the end of this lesson, you will:
- Fill the five points in practice/s13_web.py until it prints a PC verdict whose conf equals the highest score in scores, and whose scores sum to about 1.
- Measure parity for at least one window (with s13_web_full.py on the PC, or a web page running LiteRT.js), record the max-abs-diff and the winning class, and judge pass or fail against TOL = 0.02.
- Choose a target (MCU, web, Cortex-A or PC) for a given scenario, with at least two trade-offs, and explain why eval_pc.py runs on Cortex-A unchanged.
Before you start
Section titled “Before you start”You’ve been through lesson 5.6, and understand quantize, dequantize, and the definition of parity. Copy practice/s13_web.py into shared/training, which has dataset_tools.py, model_int8.tflite and .norm.npz.
- Hardware: your computer, no board needed (or use the BENTO Emulator inside BENTO IDE alongside it) — a PC (Python with ai-edge-litert, or the same Docker image). Comparing in a browser needs your own web page loading LiteRT.js, since the BENTO Emulator can’t load our own file.
- Prior lesson: lesson 5.6 — Running the model on the web: LiteRT.js, int8 I/O and parity
Concepts
Section titled “Concepts”web_verdict() walks the same four steps a web page must do. The five points we fill in are: (1) x = (window - z["mean"]) / z["std"], normalizing with the exact same set used during training (2) q = np.clip(np.round(x / in_scale + in_zero), -128, 127).astype(np.int8), reading scale/zero from inp["quantization"] (3) it.set_tensor(inp["index"], q), then it.invoke() (4) o = (o - out_zero) * out_scale, turning softmax back into probabilities, and (5) conf = float(o[top]). The result returns a dict {label, top, conf, scores}, in exactly the same shape as what the JS side (webVerdict) returns. The JS side uses the web file, whose I/O is float, so it skips the quantize and dequantize steps — but the normalize step must be the exact same formula, letter for letter.
The PC-side ground truth is python s13_web.py. Then measure parity: s13_web_full.py --export-web builds model_web.tflite from model.keras, then python s13_web_full.py runs the int8 file against the web file over a 12-window test set, printing the max-abs-diff and the winning class — this is the first checkpoint, on the PC. The next step is your own web page loading LiteRT.js, using the code from --show-js. If it fails, check in order: normalize → quantize → invoke, before blaming the kernel. The fastest way to debug is feeding the exact same input to both sides and comparing the result step by step — wherever they first diverge is where the front-end doesn’t match.
Cortex-A is the easiest: pip install ai-edge-litert on a Raspberry Pi or Jetson, then run eval_pc.py with the same .tflite file directly, because it has real Linux and Python — none of the NPU’s int8 restrictions, and it can be accelerated further with a delegate (XNNPACK on CPU, a GPU delegate on Jetson). Choosing a target is a trade-off: the MCU is the smallest and most power-efficient but hardest to convert for; the web shares instantly but needs a front-end built in JS; Cortex-A is powerful and flexible; the PC is the training bench and the ground truth.
Worked example
Section titled “Worked example”s13_web_full.py is a full parity lab on the PC: --export-web builds a web file from model.keras (produced by train.py --save-keras); running it plainly compares the int8 file against the web file over 12 windows and reports pass or fail; and --show-js prints LiteRT.js code to paste into a web page. Worth knowing: the training file’s --index option isn’t used yet — the script always picks the first test window.
| File | What this file teaches |
|---|---|
| examples/s13_web_full.py | A full parity lab: PC vs. web, from one single .tflite model (full version) |
This lesson’s slides also reference files in another lesson and under shared/:
- shared/training
- shared/training/convert_web.py — Prepare the trained model for the browser (BENTO Edge AI Emulator / any web page).
- shared/training/dataset_tools.py — Dataset tools for the IMU gesture classifier (Pillar 4 / Training).
- 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.
Practice
Section titled “Practice”The # TODO comments are at lines 38 (normalize), 50 (quantize), 55 (invoke), 60 (dequantize), and 65 (conf). If conf is 0.0000, point 65 is still empty. If scores come out as whole numbers like −128 or 127, point 60 is still empty.
| Practice file | Topic |
|---|---|
| practice/s13_web.py | Taking our model to the web, then verifying results match the PC (the fill-in-the-code version) |
Solution
Section titled “Solution”Open the solution after trying on your own at least once, and read how to use the solutions first.
| Solution | Pairs with |
|---|---|
| solution/s13_web.py | practice/s13_web.py |
Check your understanding
Section titled “Check your understanding”The same questions are in quiz.yaml for automated checking.
-
You run it, and scores look correct, but conf = 0.0000 every time. Which point is still empty? (single choice · objective 1)
- a) Point 1, normalize
- b) Point 3, invoke
- c) Point 4, dequantize
- d) Point 5, conf = float(o[top])
Solution
d — the placeholder conf = 0.0 is still there. It needs the winning class’s score pulled out with float(o[top]).
-
scores come out as [‘-128.0000’, ‘127.0000’, ‘-128.0000’]. Which point is still empty? (single choice · objective 1)
- a) Point 2, quantize
- b) Point 4, dequantize — the value is still raw int8, not a probability
- c) Point 1, normalize
- d) Nothing is wrong
Solution
b — it must be converted back with (o − out_zero) × out_scale, so the value falls in the 0..1 range and sums to about 1.
-
s13_web_full.py reports the winning class matching on 12/12 windows, with a max max-abs-diff of 0.0117 at TOL 0.02. What’s the conclusion? (single choice · objective 2)
- a) It passes on the PC — the next step is confirming in a real browser
- b) It fails, because the diff isn’t zero
- c) It passes, and no further testing is needed anywhere
- d) It fails, because it must reach 100%
Solution
a — both conditions pass, but s13_web_full.py runs the web file with an interpreter on the PC, so this is only the first checkpoint. A real browser might use a different kernel, so it’s worth confirming again.
-
You need to send a demo a customer can open immediately, with nothing to install and no board. Which target should you choose? (single choice · objective 3)
- a) MCU + Ethos-U55
- b) Web, in a browser
- c) Cortex-A
- d) PC in Docker
Solution
b — the web shares instantly with a link, and data stays on the user’s machine, in exchange for having to build the front-end in JS and measure parity yourself.
-
Why does eval_pc.py run on a Raspberry Pi without changing even a single line? (single choice · objective 3)
- a) Because a Raspberry Pi has an Ethos-U55
- b) Because Cortex-A has real Linux and Python, can install ai-edge-litert, and uses the same .tflite file with no forced int8
- c) Because eval_pc.py is compiled into C
- d) Because it uses Vela
Solution
b — Cortex-A is a small PC with a full operating system, so the same script and model file used on a PC can move over immediately.
The MVP for lessons 5.6–5.7: the web file’s verdict matches the PC side within tolerance (max|score_pc − score_web| ≤ TOL, and the winning class matches), and you can explain why they don’t need to match bit for bit.
- All five points in the practice file are filled in. Run
python s13_web.pyand note the label, conf and scores in your learning log. - Run
s13_web_full.py --export-web, thens13_web_full.py, and note the highest max-abs-diff and how many windows’ classes matched. - Temporarily disable normalize on one side, and see how far max-abs-diff spikes.
- If you can, confirm at least one window in a web page loading LiteRT.js, using the code from
--show-js.
Going further
Section titled “Going further”In the next pair of lessons (5.8–5.9), we’ll take the same model onto the MCU through Vela, then compare three targets on latency, accuracy and power.
Next lesson: lesson 5.8 — Quantizing and Vela: getting our model onto the Ethos-U55
Reflect
Section titled “Reflect”- If you had to build an audio demo instead of an IMU one, where would the JS-side front-end get harder?
- In your own work, what TOL would be acceptable, and who should be the one to decide that threshold?
Review questions
Answer on your own first, then open the answer.
-
The scores look right but conf is always 0.0000. Which point is still empty? (Objective 1)
- เติม 1 normalize
- เติม 3 invoke
- เติม 4 dequantize
- เติม 5 conf = float(o[top])
Show answer
Answer: D. เติม 5 conf = float(o[top])
placeholder conf = 0.0 ค้างอยู่ ต้องดึงคะแนนของคลาสที่ชนะออกมาด้วย float(o[top])
-
The scores come out as ['-128.0000', '127.0000', '-128.0000']. Which point is still empty? (Objective 1)
- เติม 2 quantize
- เติม 4 dequantize ค่ายังเป็น int8 ดิบ ไม่ใช่ความน่าจะเป็น
- เติม 1 normalize
- ไม่มีจุดใดผิด
Show answer
Answer: B. เติม 4 dequantize ค่ายังเป็น int8 ดิบ ไม่ใช่ความน่าจะเป็น
ต้องแปลงกลับด้วย (o − out_zero) × out_scale ค่าจึงอยู่ในช่วง 0..1 และรวมกันได้ราว 1
-
s13_web_full.py reports the winning class agrees on 12/12 windows and the largest max-abs-diff is 0.0117 with TOL 0.02. What do you conclude? (Objective 2)
- ผ่านบน PC แล้ว ขั้นต่อไปคือยืนยันในเบราว์เซอร์จริง
- ไม่ผ่าน เพราะ diff ไม่เป็นศูนย์
- ผ่าน และไม่ต้องทดสอบที่ไหนอีก
- ไม่ผ่าน เพราะต้องได้ 100%
Show answer
Answer: A. ผ่านบน PC แล้ว ขั้นต่อไปคือยืนยันในเบราว์เซอร์จริง
ครบทั้งสองเงื่อนไข แต่ s13_web_full.py รันไฟล์ web ด้วย interpreter บน PC จึงเป็นด่านแรก เบราว์เซอร์จริงอาจใช้ kernel ต่างกันจึงควรยืนยันซ้ำ
-
You must send a demo a client can open at once, with nothing to install and no board. Which target? (Objective 3)
- MCU + Ethos-U55
- Web ในเบราว์เซอร์
- Cortex-A
- PC ใน Docker
Show answer
Answer: B. Web ในเบราว์เซอร์
เว็บแชร์ด้วยลิงก์ได้ทันทีและข้อมูลอยู่ในเครื่องผู้ใช้ แลกกับการต้องทำ front-end ใน JS และวัด parity เอง
-
Why does eval_pc.py run on a Raspberry Pi without changing a single line? (Objective 3)
- เพราะ Raspberry Pi มี Ethos-U55
- เพราะ Cortex-A มี Linux กับ Python จริง ติดตั้ง ai-edge-litert ได้ และใช้ไฟล์ .tflite เดิมโดยไม่บังคับ int8
- เพราะ eval_pc.py ถูกคอมไพล์เป็นภาษา C
- เพราะใช้ Vela
Show answer
Answer: B. เพราะ Cortex-A มี Linux กับ Python จริง ติดตั้ง ai-edge-litert ได้ และใช้ไฟล์ .tflite เดิมโดยไม่บังคับ int8
Cortex-A คือ PC ตัวเล็กที่มีระบบปฏิบัติการเต็ม สคริปต์และไฟล์โมเดลชุดเดียวกับบน PC จึงย้ายไปได้ทันที
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Hands-on: a web verdict that matches the PC, and the Cortex-A story" 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
Thai attribution: "ลงมือทำ: verdict บนเว็บให้ตรงกับ PC และเรื่องราว Cortex-A" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/edge-ai-developer/m05-training/l07-web-parity-lab/
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA