จบชุดบทเรียนนี้เราจะเปลี่ยนจาก "รันโมเดล" เป็น "สร้างแอป" ครบ 4 เรื่อง แล้วปิดท้ายด้วยแอปที่คุณสร้างเอง:
find_model() — เล็งโมเดลด้วยคีย์เวิร์ด แทนที่จะ hard-code เลข indexscores ทุกคลาส + latency_ms ให้เป็น ไม่ใช่แค่คลาสที่ชนะCONF_FLOOR (ก้าวแรกของ Apps)s15_apps.py ให้เป็น แอป Cough ที่นับจำนวนครั้งที่ไอบนจอปลายทางวันนี้: แอปโฟกัสหน้าตาสะอาด รันโมเดลเดียว โชว์คลาสที่ชนะ + แถบทุกคลาส และตัวนับที่เพิ่มขึ้นจริงเมื่อเจอเสียงเป้าหมาย
วันนี้เรายังไม่แตะ RGB/เสียง/WiFi (นั่นคือ บทเรียน 6.3–6.6) เราโฟกัสที่ "โครงของแอปต่อโมเดล" กับการกระทำเบาที่สุดหนึ่งอย่าง คือการนับ
เราเดินมาครบสี่ Pillar แรกแล้ว (DAQ → Processing → Analysis → Training) ชุดบทเรียนนี้เข้าสู่ Pillar 5 · Apps — ขั้นที่เอาโมเดลไป "ใช้งาน" จริง
เราปิดวงกลับมาที่จุดเริ่ม — แต่รอบนี้คุณเข้าใจทุกขั้นที่อยู่เบื้องหลังโมเดลแล้ว การสร้างแอปจึงไม่ใช่กล่องดำอีกต่อไป
บทเรียน 1.6–1.7 เราแกะแอป Edge AI จนเห็น "เส้นทางของผล" — จากเซนเซอร์ไปจนถึงการกระทำ วันนี้เราต่อจากปลายเส้นนั้น
label + conf ขึ้นจอ"นับ" คือ action ที่ปลอดภัยที่สุดสำหรับเรียนรู้ — ไม่มีอะไรพัง แต่คุณจะได้เจอปัญหาจริงของ Apps ทันที: นับเฟ้อเพราะเสียงกระพริบ, false positive จาก conf ต่ำ เดี๋ยวเราแก้ทีละอย่าง
ทั้งคู่ใช้ edge_ai ตัวเดียวกัน ต่างกันที่ เจตนาของการออกแบบ — และงานจริงเกือบทั้งหมดเป็นแบบขวา
คิดง่ายๆ: เมนูคือ "รีโมตรวม" ที่คุมทีวีได้ทุกยี่ห้อ ส่วนแอปโฟกัสคือ "ปุ่มเดียว" บนเครื่องที่ทำงานหนึ่งอย่างให้ดีที่สุด งานวิศวกรรมจริงต้องการอย่างหลังมากกว่า
ก่อนสร้างเอง มาดูของที่มีอยู่ ทั้งสี่ตัวคือ "แอปต่อโมเดล" ที่โครงเกือบเหมือนกันเป๊ะ ต่างแค่โมเดลกับ UI นิดหน่อย
| # | ไฟล์ | โมเดล/เซนเซอร์ | จุดเด่นที่เพิ่มจากเมนู |
|---|---|---|---|
| 13 | 13_edge_ai_motion.py |
Motion (IMU) | โฟกัสตัวเดียว + find_model + latency |
| 14 | 14_edge_ai_babycry.py |
Baby Cry (MIC) | การ์ดผลลัพธ์ + แถบความมั่นใจต่อคลาส |
| 15 | 15_edge_ai_radar_push.py |
Push (RADAR) | โมเดล float32 + คำใบ้ระยะยืน |
| 16 | 16_edge_ai_sound_events.py |
Cough/Alarm/Siren (MIC) | คัดเฉพาะโมเดลไมค์เข้ามาในเมนูย่อย |
find_model() → select() → ลูป result() → วาด verdict + แถบ → finally: stop()จำ pattern นี้ไว้ให้ขึ้นใจ เพราะแอป Edge AI 90% ที่คุณจะเขียนต่อจากนี้ ล้วนเป็นโครงนี้ทั้งนั้น เปลี่ยนแค่ "โมเดลอะไร" กับ "ทำอะไรกับผล"
ทะเบียนโมเดลยังเป็น 6 ตัวเดิมจากบทเรียน 1.1–1.3 ชุดบทเรียนนี้เราจะหยิบ โมเดลเสียง มาทำแอป เพราะทดสอบง่าย (ส่งเสียงเอง) และเป็นสนามใหญ่ของ Edge AI จริง
| # | ชื่อโมเดล | เซนเซอร์ | คลาส (labels) | เหมาะทำแอป |
|---|---|---|---|---|
| 0 | Motion Detection | IMU | idle, circle, shaking | ตัวอย่าง 13 |
| 1 | Baby Cry Detection | MIC | unlabelled, baby_cry | ตัวอย่าง 14 |
| 2 | Push Detection | RADAR | unlabelled, Push | ตัวอย่าง 15 |
| 3 | Cough Detection | MIC | unlabelled, cough | ชุดบทเรียนนี้ (ค่าตั้งต้น) |
| 4 | Alarm Detection | MIC | unlabelled, alarm | รีทาร์เก็ต |
| 5 | Siren Detection | MIC | unlabelled, sirens | รีทาร์เก็ต |
s15_apps.py เล็งที่ Cough Detection — เราสร้างแอป "เครื่องนับเสียงไอ"unlabelled กับคลาสเป้าหมายเราเลือก Cough เป็นตัวตั้งต้นเพราะ "ไอ" เป็นเสียงสั้นชัด นับง่าย เห็นตัวเลขขยับทันที — เหมาะกับการเรียนเรื่อง "ขอบขาขึ้น" กับ debounce ที่จะเจอต่อไป
แอปโฟกัสยืนอยู่บนคำสั่งเดิมจาก บทเรียน 1.1–1.3 แต่ชุดบทเรียนนี้เราตัด count()/models() ออกจากลูปหลัก เหลือแค่สามตัวที่ทำงานจริงตอนรัน
| คำสั่ง | ชุดบทเรียนนี้ใช้ทำอะไร |
|---|---|
edge_ai.models() |
เรียกครั้งเดียวตอนเปิด — ให้ find_model() ค้นทะเบียน |
edge_ai.select(n) |
เริ่มโมเดลเป้าหมายตัวเดียว (ครั้งเดียว ไม่มีปุ่ม Load) |
edge_ai.result() |
ดึง verdict ล่าสุดในลูป — หัวใจของแอป |
edge_ai.stop() |
ปิดเครื่องยนต์ตอนออก (finally) |
edge_ai.CONF_FLOOR |
เส้นแบ่ง 0.50 — ใช้ตัดสินว่าจะ "นับ" ไหม |
edge_ai.SENSOR_MIC |
ค่าคงที่ = 2 — เซนเซอร์สำรองให้ find_model() |
select() เรียก ครั้งเดียวตอนเปิดแอป ไม่ต้องรอผู้ใช้กด Load — แอปโฟกัสรู้อยู่แล้วว่าจะรันอะไรCONF_FLOOR คราวนี้ไม่ใช่แค่เปลี่ยนสี แต่เป็น เงื่อนไขของ action จริง (นับหรือไม่นับ)น้อยลงแต่คมขึ้น — เมื่อคุณรู้ว่าจะรันโมเดลไหน โค้ดก็สั้นลง เหลือแต่แก่น: อ่านผล แล้วตัดสินใจ
result() คืน dict เดิมจาก บทเรียน 1.1–1.3 แต่ชุดบทเรียนนี้เราจะใช้ฟิลด์ให้ครบขึ้น โดยเฉพาะ scores (ทุกคลาส) และ latency_ms
r = edge_ai.result()
# {'index': 3, 'label': 'cough', 'top': 1, 'conf': 0.88,
# 'scores': [0.12, 0.88], 'latency_ms': 6.4, 'seq': 210, 'running': True}
| ฟิลด์ | ชุดบทเรียนนี้ใช้ยังไง |
|---|---|
label + conf |
คำตอบสั้น + ใช้เทียบ CONF_FLOOR เพื่อตัดสินใจนับ |
top |
index คลาสที่ชนะ — ระบายแถบตัวชนะเป็นเขียว |
scores |
คะแนน ทุกคลาส — วาดแถบครบทุกแถว เห็น "ความสูสี" |
latency_ms |
เวลาอนุมานจริง — โชว์บนการ์ด เทียบข้ามเซนเซอร์ได้ |
seq |
เลขลำดับผล — กันวาดจอซ้ำ (วาดเฉพาะตอน seq เปลี่ยน) |
ใน บทเรียน 1.1–1.3 เราสนใจแค่
label/conf— ชุดบทเรียนนี้scoresกับlatency_msเลื่อนมาเป็นพระเอก เพราะแอปที่ดีต้องบอกผู้ใช้ได้ว่า "มั่นใจแค่ไหน" และ "เร็วแค่ไหน" ไม่ใช่แค่ "คืออะไร"
หัวใจที่ทำให้แอปโฟกัส "ทน" คือไม่ผูกกับเลข index ตายตัว เราค้นทะเบียนจากชื่อแทน แบบเดียวกับตัวอย่าง 13–16
def find_model(keywords, sensor):
ms = edge_ai.models()
for m in ms: # 1) ลองจับชื่อก่อน
for k in keywords:
if k.lower() in m['name'].lower():
return m
for m in ms: # 2) ไม่เจอชื่อ -> ใช้เซนเซอร์ที่ตรงกันตัวแรก
if m['sensor'] == sensor:
return m
return ms[0] # 3) กันเหนียว -> ตัวแรกสุด
("cough",) เข้าไป มันจะคืน dict ของโมเดล Cough ไม่ว่ามันจะอยู่ index เท่าไรselect(3) ที่จะพังทันทีนี่คือนิสัยเดียวกับ บทเรียน 1.1–1.3 — "ถามฮาร์ดแวร์ อย่าเดา" แค่คราวนี้เราถามแบบเจาะจง "ขอโมเดลที่ชื่อมีคำว่า cough" แล้วปล่อยให้เฟิร์มแวร์ตอบว่ามันอยู่ตรงไหน
จับโครงให้ได้ก่อนดูโค้ดจริง แอปโฟกัสทุกตัวเดินตามห้าจังหวะนี้ — เป็นญาติสนิทของ "โครงร่วม" จาก บทเรียน 1.4–1.5 แต่ select ขยับมาอยู่ต้นเรื่อง
ต่างจากเมนู บทเรียน 1.1–1.3 ตรงที่ ไม่มีปุ่ม Load —
select()อยู่ก่อนลูปเลย เพราะแอปโฟกัส "รู้ตั้งแต่เปิด" ว่าจะรันโมเดลอะไร ผู้ใช้ไม่ต้องเลือก
scores คือหัวใจที่ทำให้ผู้ใช้ "เห็นความมั่นใจ" ไม่ใช่แค่คลาสเดียว เราวาดหนึ่งแถบต่อหนึ่งคลาส ตัวที่ชนะระบายเขียว
top = r['top']
for i, (lb, br) in enumerate(rows):
if i < len(r['scores']):
br.value(int(r['scores'][i] * 100)) # ความสูงของแถบ = คะแนนคลาสนั้น
br.color(GREEN if i == top else DIM) # คลาสที่ชนะ = เขียว, ที่เหลือ = จาง
['unlabelled', 'cough'] เราจึงเห็นสองแถบ ขยับสวนทางกันcough จะพุ่งเข้าใกล้ 100% แถบ unlabelled จะยุบลงconf ต่ำกว่า CONF_FLOORแถบทุกคลาสไม่ได้มีไว้สวยงามเฉยๆ มันบอกคุณว่าโมเดล "ลังเล" หรือ "ชัวร์" ด้วยตาเปล่า — ก่อนจะเชื่อคำตอบ ให้ดูว่าตัวชนะทิ้งห่างตัวอื่นแค่ไหน
latency_ms คือเวลาที่ NPU ใช้อนุมานหนึ่งครั้ง แอปที่ดีควรโชว์ให้เห็น เพราะมันบอก "ความสด" ของคำตอบ
lat.text("latency: %.1f ms" % r['latency_ms'])
ลองสังเกตในแล็บ: สลับระหว่างแอป Cough (ไมค์) กับตัวอย่าง 13 (IMU) แล้วเทียบ
latency_msคุณจะเห็นด้วยตาว่า "โมเดลคนละแบบ กินเวลาคนละอย่าง" — นี่คือข้อมูลที่วิศวกรใช้เลือกโมเดล
นี่คือของใหม่จริงๆ ของชุดบทเรียนนี้ เราไม่หยุดที่การโชว์ผล แต่ "ทำอะไรสักอย่าง" เมื่อเจอคลาสเป้าหมายแบบมั่นใจ
is_target = (r['label'] == TARGET_CLASS
and r['conf'] >= edge_ai.CONF_FLOOR) # เชื่อก็ต่อเมื่อมั่นใจถึงเกณฑ์
if is_target and not was_target: # นับเฉพาะ "ขอบขาขึ้น"
hits += 1
hits_lbl.text(str(hits))
was_target = is_target
conf ต้องถึง CONF_FLOOR — ถ้าปล่อยเงื่อนไข conf ทิ้ง ตัวนับจะเก็บ false positive เต็มไปหมดCONF_FLOOR เลื่อนสถานะจาก "ตัวเปลี่ยนสี" (บทเรียน 1.1–1.3) มาเป็น "ประตูของ action" จริงaction แรกของคุณคือการนับ — เล็กแต่จริง ในบทเรียน 6.3–6.4 เราจะเปลี่ยน
hits += 1เป็น "จุด RGB / เล่นเสียง / เขียน log" ด้วยโครงตัดสินใจอันเดียวกันนี้
โมเดลไม่ได้คืนแค่ "คลาสเดียว" แต่คืน scores เป็นคะแนนของทุกคลาส เราต้องเลือก "ตัวที่ชนะ" ออกมาหนึ่งตัว วิธีคิดคือหาตำแหน่งที่คะแนนสูงสุด:
โดยที่ คือเวกเตอร์คะแนนของ คลาส และ คือ index ของคลาสที่ชนะ (ในโค้ดคือ r['top'])
ตัวอย่างโมเดล Cough ที่มีสองคลาส ['unlabelled', 'cough']: ได้ → → คลาส cough ชนะ, และ conf ของผลนี้คือ
argmax บอกแค่ "ใครชนะ" มันไม่เคยบอกว่า "ชนะขาดหรือชนะเฉียด" — เพราะแม้คะแนน ต่อ ก็ยังมีผู้ชนะ นั่นคือเหตุผลที่เราต้องดู conf ต่ออีกชั้นในสไลด์ถัดไป ก่อนจะเชื่อผล
เพราะ argmax เลือกผู้ชนะเสมอ แม้ตอนคะแนนสูสี เราจึงตั้ง "เส้นแบ่งความมั่นใจ" ไว้อีกชั้น ก่อนจะยอมนับ ความมั่นใจของผลก็คือคะแนนของตัวที่ชนะ:
edge_ai.CONF_FLOOR = ในชุดบทเรียนนี้ทำไมมันสำคัญกับชุดบทเรียนนี้: ถ้าไม่มี ทุกครั้งที่ cough แค่ชนะแบบ ต่อ ก็จะถูกนับ ตัวนับจะเฟ้อไปด้วย false positive การใส่ คือการเขียนกฎว่า "เชื่อก็ต่อเมื่อมั่นใจถึงเกณฑ์" ซึ่งเป็นหัวใจของการเปลี่ยน verdict ให้เป็น action
เลข ไม่ใช่ของศักดิ์สิทธิ์ — ตั้งสูง () ได้ความชัวร์แต่พลาดเสียงเบาๆ ตั้งต่ำ () จับได้ไวแต่ false positive เยอะ การเลือก คือการชั่งน้ำหนักสองอย่างนี้ให้เข้ากับงาน
ถ้านับทุกผลที่เข้าคลาสเป้าหมาย จะได้ตัวเลขเฟ้อมหาศาล เพราะไอหนึ่งครั้งกินเวลาหลายผลอนุมาน เราจึงนับเฉพาะ จังหวะที่เพิ่งเข้า
was_target จำสถานะรอบก่อน เรานับเฉพาะตอน "รอบนี้เข้าเป้า แต่รอบก่อนยังไม่เข้า" (ขอบขาขึ้น false → true)examples/) เราเสริม debounce เบาๆ อีกชั้น: ต้องเจอติดกันสองผลถึงนับ กันเสียงกระพริบชั่วขณะเทคนิค "ขอบขาขึ้น" นี้เหมือนกับการอ่านปุ่มกดในคอร์สอื่นเป๊ะ — เรานับ "การกด" หนึ่งครั้ง ไม่ใช่นับทุกมิลลิวินาทีที่นิ้วยังกดค้างอยู่
โมเดล Cough/Alarm/Siren เป็น DEEPCRAFT Ready-Model แบบ eval มีเงื่อนไขที่ต้องรู้ก่อนทดสอบ ไม่งั้นจะงงว่าทำไมผลหยุดนิ่ง
seq ไม่ขยับ) นั่นแปลว่าถึงขีดจำกัดแล้ว — รีบูตบอร์ด จะรีเซ็ตจุดนี้สำคัญเชิงวิศวกรรม: โมเดลที่ "ให้ลองฟรี" มักมีเงื่อนไขซ่อนอยู่ พอเราฝึกโมเดลเองได้ (โมดูล 5 (Training) ที่เพิ่งผ่านมา) เราก็หลุดจากข้อจำกัดนี้ — นี่คือเหตุผลหนึ่งที่คอร์สพาไปถึงการเทรนเอง