จบชุดบทเรียนนี้เราจะเดินครบ 4 เรื่อง แล้วปิดท้ายด้วยการ remix แอป Edge AI ของเราเอง:
select → result → actionfind_model() ไม่ hard-code indexon_result()s03_anatomy_edgeai.py — สลับโมเดล + สั่งการเมื่อเจอคลาสเป้าหมายปลายทางของวันนี้: แอปเฝ้าจับ "โมเดลเดียว 1 คลาส" ที่พอเจอคลาสเป้าหมายเกินเกณฑ์ บี๊บ + ขึ้นแบนเนอร์ เอง
วันนี้เราไม่ได้แค่ "อ่านผล" (บทเรียน 1.1–1.3) แต่ทำให้ผลนั้น สั่งการอะไรบางอย่างได้ — นี่คือประตูสู่ โมดูล 6 (Apps)
บทเรียน 1.6–1.7 เป็นบทเรียน ปิดกล่อง Onboarding — สองชุดบทเรียนก่อนกลับด้าน "แอปเซนเซอร์" และตอนนี้เรากลับด้าน "แอป Edge AI"
models/select/result/stop"กลับด้าน" ยังทำงานเหมือนเดิม: เห็นของสำเร็จก่อน แล้วย้อนเข้าใจ — พอถึง โมดูล 6 (Apps) คุณจะกลับมามองแอปวันนี้แล้วต่อยอดเป็นระบบจริงได้
ก่อนแกะแอป เรียก 4 คำสั่งของ edge_ai ที่เจอในบทเรียน 1.1–1.3 กลับมาในหัวก่อน ชุดบทเรียนนี้เราจะต่อยอดจากตรงนี้พอดี:
| คำสั่ง | ทำอะไร |
|---|---|
edge_ai.models() |
คืน list ของ dict — ทะเบียนโมเดลทั้งหมด {index, name, sensor, labels} |
edge_ai.select(n) |
สั่งให้ CM55 รันโมเดลหมายเลข n (ส่งคำสั่งแล้วรอยืนยัน) |
edge_ai.result() |
ผลอนุมานล่าสุดเป็น dict หรือ None — มี label/conf/scores/seq/latency_ms |
edge_ai.stop() |
หยุดเครื่องยนต์ให้ว่าง (idle) |
edge_ai.CONF_FLOOR |
ค่าคงที่ 0.50 — ต่ำกว่านี้ถือว่า "ยังไม่ชัวร์" |
ชุดบทเรียนนี้เพิ่มอีกสองแนวคิดบนฐานนี้: เลือกโมเดลจากชื่อ (แทน index) และ on_result() (แทนการ poll เอง)
ถ้ายังไม่แม่น เปิด
s01_first_inference.pyอ่านทวนก่อน — ชุดบทเรียนนี้ยืนบนคำสั่งเดิมเป๊ะ แค่ประกอบมันเป็น "แอปที่ลงมือทำ" ได้
บทเรียน 1.1–1.3 ใช้ 12_edge_ai_menu.py — เมนูสลับได้ทั้ง 6 โมเดล เหมาะกับ "สำรวจว่ามีอะไรบ้าง" แต่แอปใช้งานจริงมักโฟกัส โมเดลเดียว แล้วทำงานให้ดีที่สุด
13_edge_ai_motion.py คือฝาแฝดโมเดลเดียวของ 12 — เลือก Motion ด้วย find_model() แล้ววนอ่านผลs03_anatomy_edgeai.py) ต่อยอดจาก 13: เพิ่ม "สั่งการเมื่อเจอคลาสเป้าหมาย" เข้าไปโลกจริงส่วนใหญ่ไม่ได้ให้ผู้ใช้เลือกโมเดลเอง — นาฬิกาตรวจการล้ม รันโมเดลเดียวตลอด แล้วลงมือเมื่อเจอ นี่คือรูปแบบที่เรากำลังแกะ
ก่อนแกะโค้ด รันสองแอปอ้างอิงให้เห็นของจริงในมือก่อน (ทำได้ทั้ง Emulator และบอร์ด):
13_edge_ai_motion.py — โมเดล Motion โมเดลเดียวshaking/circle/idle) + แถบคะแนน + latency เปลี่ยนสด12_edge_ai_menu.py เทียบด้วย เห็นว่า "โมเดลเดียว" กับ "เมนู" ต่างกันตรงไหนความรู้สึก "มันทำงานได้แล้ว ฉันอยากรู้ว่าทำไม" คือเชื้อเพลิงของชุดบทเรียนนี้ — จับความสงสัยนั้นไว้ แล้วเราจะเปิดฝาดูข้างในกันทีละส่วน
ไม่มีบอร์ดในมือก็เริ่มได้ — นี่คือหน้า Edge AI บน BENTO Emulator ที่รันในเบราว์เซอร์ล้วนๆ ทุกคนได้ลองมือพร้อมกัน:

จอ emulator ที่รันได้จริง — หน้า Edge AI ของ BENTO (เลือกโมเดล ดู verdict + แถบคะแนน + conf สดๆ)
select) → คลาสที่ชนะตัวใหญ่กลางจอ (result) → แถบ scores ทุกคลาส + conf % ด้านข้างEmulator กับบอร์ดจริงใช้โค้ดชุดเดียวกันเป๊ะ — เขียนบน Emulator ให้เข้าใจก่อน แล้วยกไฟล์เดิมไปลงบอร์ดได้ทันที ไม่ต้องแก้อะไร
แอป Edge AI ที่โฟกัสโมเดลเดียว อ่านเป็น 3 จังหวะ เสมอ — จำโครงนี้ไว้ แล้วทุกแอปในคอร์สจะเข้าใจง่ายขึ้น:
จังหวะ 1–2 คือของเดิมจากบทเรียน 1.1–1.3 (เลือกโมเดล อ่านผลขึ้นจอ) จังหวะ 3 — action — คือหัวใจใหม่ที่ทำให้ Edge AI "ใช้งานได้จริง" ไม่ใช่แค่โชว์ตัวเลข
edge_ai.models() คืน ทะเบียน ที่เฟิร์มแวร์ถืออยู่ แต่ละแถวคือโมเดลหนึ่งตัว การแกะแอปเริ่มที่เข้าใจโครงสร้างนี้:
index (ใช้ select), name (โชว์/ค้นหา), sensor (0=IMU,1=RADAR,2=MIC), labels (คลาสที่ตอบได้)labels สำคัญมากชุดบทเรียนนี้ — คลาสเป้าหมายที่เราจะดักจับต้องเป็นชื่อจากลิสต์นี้เป๊ะทะเบียนคือ "แหล่งความจริง" ของแอป — เราไม่เดาว่าโมเดลไหนคลาสอะไร เราอ่านจากที่นี่ ถ้าเฟิร์มแวร์เพิ่มโมเดล แอปเห็นเองทันที
บทเรียน 1.1–1.3 เราส่ง index ตรงๆ (select(0)) แต่ index อาจสลับได้ถ้าเฟิร์มแวร์เปลี่ยน แอปที่ทนทานกว่าคือ ค้นหาโมเดลจากชื่อ แล้วค่อยเอา index ไป select:
def find_model(keyword):
ms = edge_ai.models()
for m in ms:
if keyword.lower() in m['name'].lower():
return m # เจอชื่อที่ตรง คืน dict ทั้งก้อน
return ms[0] # ไม่เจอ ใช้ตัวแรกกันแอปพัง
model = find_model("Motion") # remix: เปลี่ยน keyword = เปลี่ยนแอป
edge_ai.select(model['index']) # เอา index จาก dict ไปสั่งจริง
find_model() ยืมมาจาก 13_edge_ai_motion.py — คืน dict ทั้งก้อน เราจึงได้ทั้ง index และ labels มาใช้ต่อkeyword เดียว แอปก็ remix เป็นโมเดลอื่นได้ — นี่คือจุด remix แรกของชุดบทเรียนนิสัยเดิมจากบทเรียน 1.1–1.3 ยังอยู่: ถามฮาร์ดแวร์ก่อน อย่าเดา แต่รอบนี้เราถามด้วย "ชื่อ" ที่มนุษย์อ่านออก แทนตัวเลข index ที่จำยาก
result() คืน dict ก้อนเดิมจากบทเรียน 1.1–1.3 ชุดบทเรียนนี้เราจะใช้ 2 คีย์เป็นหลักในการตัดสิน "จะสั่งการไหม": label กับ conf
r = edge_ai.result()
# r = {'label': 'shaking', # คลาสที่ชนะ (ข้อความ) ← ใช้เทียบเป้าหมาย
# 'top': 2, # index ของคลาสที่ชนะ
# 'conf': 0.92, # ความมั่นใจ 0..1 ← ใช้เทียบ CONF_FLOOR
# 'scores': [.03,.05,.92],# คะแนนทุกคลาส → ทำแถบ
# 'latency_ms': 4.1,
# 'seq': 137, 'running': True}
label = "เจออะไร" · conf = "มั่นใจแค่ไหน" — สองตัวนี้รวมกันคือเงื่อนไขของ actionseq เพิ่มทุกครั้งที่มีผลใหม่ — ยังใช้กันวาดจอซ้ำเหมือนบทเรียน 1.1–1.3result() เป็น pull ถามเมื่อไรก็ได้ในลูป คืนผลล่าสุด ไม่บล็อกรอบทเรียน 1.1–1.3 เราเอา
labelขึ้นจอเฉยๆ ชุดบทเรียนนี้เราจะ เอาlabel+confไปตัดสินใจ ว่าจะลงมือทำหรือไม่ — ความต่างอยู่ตรงนี้
scores กับ conf ที่ result() คืนมาไม่ได้โผล่มาลอยๆ — มันมาจากสูตรชื่อ softmax โมเดลปล่อย "คะแนนดิบ" (logits) ของแต่ละคลาสออกมาก่อน แล้ว softmax บีบให้เป็นความน่าจะเป็นที่รวมกันได้ 1:
อ่านทีละตัวแบบไม่ต้องกลัวสูตร:
ผลลัพธ์ คือ ความน่าจะเป็นของคลาส — นี่แหละคือค่าที่ไปโผล่ในทุกช่องของ r['scores'] (แต่ละแถบที่เห็นบนจอ)
ทำไมไม่ใช้คะแนนดิบตรงๆ? เพราะเราอยากได้เลข 0..1 ที่ตีความเป็น "มั่นใจกี่เปอร์เซ็นต์" ได้จริง — และเส้น
CONF_FLOOR = 0.50จะมีความหมายก็ต่อเมื่อคะแนนถูก normalize แล้วเท่านั้น
จาก scores ที่ softmax ให้มา เฟิร์มแวร์คำนวณสองค่าที่เราเอาไปตัดสินใจ ด้วยตัวดำเนินการคู่กัน:
r['top'] แล้วแปลงเป็นชื่อ r['label']r['conf'] ความมั่นใจ 0..1ลองแทนตัวเลขจริงของโมเดล Motion 3 คลาส:
label = "shaking", top = 2conf = 0.92 = มั่นใจ 92%hit เป็น True → สั่งการนี่คือสะพานเชื่อมสูตรกับโค้ดของเราตรงๆ:
r['label']มาจาก argmax,r['conf']มาจาก max ของ scores ก้อนเดียวกัน — สองบรรทัดเงื่อนไขhitในชุดบทเรียนนี้ (label == TARGETและconf >= CONF_FLOOR) ก็คือ argmax คู่กับ max นั่นเอง
ก่อนต่อ action มาปิดเส้นทางเดิมให้ขาดก่อน: verdict หนึ่งก้อนกลายเป็นภาพบนจอได้ยังไง นี่คือ "จังหวะ 2" แบบละเอียด
seq ก่อน → วาดเฉพาะตอนมีผลใหม่ (ไม่รัดจอ) · แล้วกระจายไป verdict.text + แถบ scores + confscores คือคะแนน ทุกคลาส เอาไปทำแถบ คลาสที่ i == top ระบายเขียว ผู้ใช้เห็นทั้งคำตอบและความสูสีนี่คือ "จังหวะ 2" ที่คุณเติมมาแล้วในบทเรียน 1.1–1.3 — ชุดบทเรียนนี้เราต่อ "จังหวะ 3" (action) จากจุดเดียวกันนี้ หลังวาดจอเสร็จ
จนถึงบทเรียน 1.1–1.3 เราหยุดที่ "เอาคำตอบขึ้นจอ" แต่แอป Edge AI จริงต้อง ลงมือทำอะไรต่อ เมื่อคำตอบเข้าเงื่อนไข นี่คือก้าวจาก inference ไปสู่ application
"โมเดลรู้ว่าเจออะไร" เป็นแค่ครึ่งทาง — คุณค่าจริงเกิดตอน การกระทำ ที่ตามมา นี่คือสิ่งที่ โมดูล 6 (Apps) ทั้งบล็อกจะขยายให้ลึก
action ไม่ควรยิงทุกครั้งที่โมเดลตอบ — ต้องยิงเฉพาะเมื่อ ใช่คลาสที่เราสนใจ และ มั่นใจพอ สองเงื่อนไขนี้คู่กันเสมอ:
hit = (r['label'] == TARGET_CLASS # 1) ใช่คลาสเป้าหมายไหม
and r['conf'] >= edge_ai.CONF_FLOOR) # 2) มั่นใจถึงเกณฑ์ไหม (>= 0.50)
if hit and not fired:
fire_action(r['conf']) # ลงมือ
fired = True
label อย่างเดียวไม่พอ — ถ้าโมเดลตอบ "shaking" แต่ conf แค่ 30% แปลว่ามัน "เดา" เราไม่ควรลงมือCONF_FLOOR = 0.50 คือเส้นที่เฟิร์มแวร์แนะนำ (จากบทเรียน 1.1–1.3) — เป็นตัวกัน false positiveนี่คือบทเรียน Edge AI สำคัญ: action ต้องตั้งอยู่บนความมั่นใจ ไม่ใช่แค่ป้ายคลาส เส้น
CONF_FLOORคือปุ่มที่คุณจะปรับจริงจังในชุดบทเรียน Apps (บทเรียน 6.3–6.4)
ปัญหา: ถ้าคุณเขย่าค้าง 3 วินาที โมเดลตอบ "shaking" ทุกเฟรม — ถ้ายิง action ทุกเฟรม เสียงจะบี๊บรัวจนน่ารำคาญ ทางแก้คือ จำว่ายิงไปแล้ว
if hit and not fired: # เพิ่งเข้าคลาสเป้าหมาย → ยิงครั้งเดียว
fire_action(r['conf'])
fired = True
elif not hit: # ออกจากคลาสเป้าหมายแล้ว → รีเซ็ต
fired = False
fired เป็นธง: ตั้ง True ตอนยิง เคลียร์ False ตอนออกจากคลาส — action จึงยิง "ตอนขอบขาขึ้น" ครั้งเดียวpattern เดียวกับปุ่มกดในงาน embedded: เรากด หนึ่งครั้ง ได้ event หนึ่งครั้ง ไม่ใช่ event รัวตราบที่ยังกดค้าง — action ที่ดีต้องคุมจังหวะการยิงเสมอ
fire_action() คือที่ที่ verdict กลายเป็นการกระทำจริง ในบทเรียนนี้เราให้มันบี๊บ + ขึ้นแบนเนอร์ + จดคอนโซล:
def fire_action(conf_val):
banner.text("! เจอ %s !" % TARGET_CLASS)
banner.color(GREEN)
if hasattr(ui, "tone"):
ui.tone(72, ui.WAVE_SINE, 120, 150) # โน้ต MIDI 72, sine, ดัง 120, 150 ms
lcd.console('<span class=ok> ACTION: เจอ %s (conf %.0f%%)</span>'
% (TARGET_CLASS, conf_val * 100))
ui.tone(note, wave, vol, ms) เล่นเสียงผ่าน SFX mixer ฝั่ง CM55 — note เป็น MIDI (60 = โดกลาง)hasattr(ui, "tone") เผื่อพื้นผิวที่ไม่มีลำโพง — แอปไม่พังถ้าเล่นเสียงไม่ได้fire_action() เป็นฟังก์ชัน = remix ง่าย: อยากเปลี่ยน action แก้ที่เดียวเก็บ "การตัดสินใจ" (เงื่อนไข hit) แยกจาก "การลงมือ" (fire_action) — โครงนี้ทำให้เปลี่ยน action โดยไม่แตะ logic ตรวจจับได้ นี่คือนิสัยออกแบบที่ดี
การ poll เอง (result() ในลูป + เช็ก seq) ใช้ได้ดี แต่ edge_ai มีทางที่สะอาดกว่า: ให้เฟิร์มแวร์ เรียกฟังก์ชันของเราให้ เมื่อมีคำตัดสิน (ทันทีที่คลาสเปลี่ยน และทวนคลาสเดิมราววินาทีละครั้ง)
def on_change(r): # เฟิร์มแวร์เรียกให้เมื่อมีคำตัดสิน (scheduler context — ปลอดภัยกับ UI)
verdict.text(r['label'] or '-')
if r['label'] == TARGET_CLASS and r['conf'] >= edge_ai.CONF_FLOOR:
fire_action(r['conf'])
edge_ai.on_result(on_change) # ลงทะเบียนครั้งเดียว
on_resultยิง cb ให้เราทันทีที่คลาสเปลี่ยน (และทวนคลาสเดิมราววินาทีละครั้ง) เราจึงไม่ต้องเช็กseqเอง แต่ยังต้องมีธงfiredกันยิง action ซ้ำ —s03_anatomy_edgeai_full.pyใช้วิธีนี้ ทำให้ลูปหลักเหลือแค่รับปุ่ม back
สองวิธีให้ผลเดียวกัน ต่างที่ "ใครถาม" — เข้าใจข้อแลกเปลี่ยนแล้วเลือกใช้ให้เหมาะกับงาน:
| ประเด็น | poll เอง (result()) |
on_result(cb) |
|---|---|---|
| ใครขับ | โค้ดเราถามเองในลูป | เฟิร์มแวร์เรียก cb ให้ |
เช็ก seq |
ต้องเช็กเอง | ไม่ต้อง (ยิงตอนคลาสเปลี่ยน และทวนคลาสเดิมราววินาทีละครั้ง) |
| คุมจังหวะ | ตรงไปตรงมา เห็นทั้งลูป | logic กระจายไปอยู่ใน cb |
| เหมาะกับ | เริ่มเรียน · อยากเห็นทุกจังหวะ | แอปที่ action ต้องไวและสะอาด |
practice/) เราใช้ poll เพราะเห็นทั้ง 3 จังหวะในลูปเดียว เข้าใจง่ายon_result จะทำงานก็ต่อเมื่อโปรแกรมเรียก edge_ai.result() หรือ edge_ai.active() ถ้าลูปมีแค่ ui.poll() บน Emulator จะไม่เห็นผลเลย (บนบอร์ดไม่มีข้อจำกัดนี้)examples/) เราใช้ on_result เพราะแอปโตขึ้น — ย้าย logic ไปที่ cb ทำให้ลูปสะอาดทั้งคู่ถูกต้อง ไม่มีผิด — เริ่มจาก poll ให้เข้าใจกลไกก่อน แล้วค่อยยกไป on_result เมื่อแอปต้องการความสะอาด (ลองเป็นโจทย์ต่อยอดดูได้)