จบชุดบทเรียนนี้เราจะเดินครบ 4 เรื่อง แล้วปิดท้ายด้วยการสตรีมเหตุการณ์ Edge AI ขึ้นคลาวด์:
wifi.connect() ต่อเน็ต แล้ว mqtt.publish() ส่งเหตุการณ์ออกไปs17_fusion_iot.py — fuse verdict กับ gyro ดิบ แล้ว publish ขึ้น MQTTปลายทางของวันนี้: พอโมเดลจับ shaking และ gyro ดิบแรงพอ แอปจะยิงเหตุการณ์เดียว ขึ้น MQTT broker (หรือลง console ถ้าอยู่ Emulator)
วันนี้เราต่อยอดจาก "verdict → action" (บทเรียน 6.3–6.4) ไปอีกสองชั้น: action ที่ ผ่านการยืนยัน (fusion) และ action ที่ ไปถึงคลาวด์ (IoT)
บทเรียน 6.5–6.6 เป็นบทเรียน ปิดกล่อง โมดูล 6 (Apps) — จากอ่านผล (บทเรียน 6.1–6.2) สู่สั่งการ (บทเรียน 6.3–6.4) จนถึงตัดสินใจแบบรวมสัญญาณ + ส่งออก (บทเรียน 6.5–6.6) ก่อนเข้าสู่ โมดูล 7 (Researcher)
"กลับด้าน" ยังทำงานเหมือนเดิม: เห็นแอปหลายเซนเซอร์สำเร็จก่อน (
10_motion_alarm.py) แล้วย้อนเข้าใจ จนต่อยอดเป็นระบบ IoT ของเราเองได้
ก่อนเติม fusion เรียกของเดิมกลับมาในหัวก่อน ชุดบทเรียนนี้ยืนบน pattern เดิมเป๊ะ แค่เพิ่มสองชั้นบนสุด:
| แนวคิด | มาจากบทเรียน | ชุดบทเรียนนี้ใช้ยังไง |
|---|---|---|
edge_ai.select() / result() |
1, 3, 15 | เลือกโมเดล + อ่าน verdict (เหมือนเดิม) |
label + conf >= CONF_FLOOR |
3, 16 | เงื่อนไข "โมเดลมั่นใจพอ" (ชั้นแรกของ fusion) |
edge-trigger (fired) |
3, 16 | ยิงเหตุการณ์ครั้งเดียวต่อการเจอ (ไม่ spam broker) |
sensors.bmi270.motion() |
4, 7 | อ่านเซนเซอร์ดิบ (ชั้นที่สองของ fusion) |
ชุดบทเรียนนี้เพิ่มสองแนวคิดใหม่บนฐานนี้: การรวม verdict กับเซนเซอร์ดิบ (fusion) และ การส่งเหตุการณ์ออกทางเน็ต (wifi + mqtt)
ถ้ายังไม่แม่นเรื่อง verdict → action เปิด
s03_anatomy_edgeai.pyอ่านทวนก่อน — ชุดบทเรียนนี้ต่อจากตรงนั้นพอดี
ก่อนแกะโค้ด รันแอปอ้างอิงให้เห็นการรวมสัญญาณด้วยตาก่อน (ทำได้บน Emulator และ PSoC Edge AI Kit ส่วนบน TESAIoT Dev Kit ตัวอย่างนี้ยังเปิดไมโครโฟน PDM ไม่ได้ ดูหมายเหตุหัวไฟล์):
10_motion_alarm.py — ระบบเฝ้าระวัง 3 เซนเซอร์!! INTRUDER !!จับความรู้สึกนี้ไว้: "เซนเซอร์ตัวเดียวไม่พอ ต้องให้หลายตัวเห็นตรงกันก่อนถึงจะเชื่อ" — ชุดบทเรียนนี้เราจะเอาหลักเดียวกันมาใช้กับ verdict ของโมเดล + เซนเซอร์ดิบ
โมเดลไม่ได้ถูกเสมอ บางท่า/บางเสียงมันก็ "เดา" คลาสผิดด้วยความมั่นใจพอสมควร ถ้าเราสั่งการทุกครั้งที่ verdict เข้าเงื่อนไข action จะยิงพลาดบ่อย
นี่คือเหตุผลที่ระบบจริงไม่ค่อยเชื่อเซนเซอร์ตัวเดียว — รถยนต์ใช้ทั้งกล้อง+เรดาร์+lidar ก่อนเบรก เพราะแต่ละตัวพลาดคนละแบบ เอามารวมกันจึงน่าเชื่อถือ
Sensor fusion = รวมข้อมูลจากหลายแหล่งเข้าเป็นการตัดสินใจเดียวที่ดีกว่าใช้แหล่งเดียว ชุดบทเรียนนี้เราทำแบบง่ายที่สุดแต่ทรงพลัง: model verdict + raw sensor gate
เราเลือก fusion แบบ AND ในชุดบทเรียนนี้เพราะเข้าใจง่ายและกัน false positive ได้ทันที ในงานจริงมี fusion ที่ซับซ้อนกว่านี้ (ถ่วงน้ำหนัก · Kalman) แต่หลักคิดเริ่มจากตรงนี้
fusion ในชุดบทเรียนนี้ (verdict + raw gate) เป็นแบบ corroboration แต่รู้ไว้ว่ามีอีกแบบที่แอปอ้างอิงใช้ (10_motion_alarm.py):
examples/) จะให้ลองสลับ raw gate เป็น sensors.radar() เพื่อชิมรสของ multi-modal ด้วยไม่มีแบบไหน "ถูกกว่า" — เลือกตามความเสี่ยง งานปลุกภัยเลือก vote (พลาดไม่ได้) งานสั่งการอัตโนมัติเลือก AND (ยิงพลาดแล้วกวนใจ)
การ "โหวต 2 ใน 3" ที่เห็นตอนรัน 10_motion_alarm.py เขียนเป็นสูตรสั้นๆ ได้แบบนี้ — ไม่ต้องกลัว เดี๋ยวเราค่อยๆ แกะทีละตัว:
อ่านสัญลักษณ์ทีละตัว (ภาษาคนธรรมดา):
ทำไมสำคัญกับชุดบทเรียนนี้: ตั้ง คือ majority vote ที่ทน "เซนเซอร์ตัวหนึ่งพลาด" ได้ ( ตัวเงียบยัง fire ได้) — ตรงกับความรู้สึกตอนเขย่าเบาๆ แล้วมัน "ไม่ยอมปลุก"
จำภาพนี้ไว้: ยิ่ง ใกล้ ยิ่งเข้มงวด (ต้องเห็นตรงกันหลายตัว) — พอ เมื่อไร มันก็กลายเป็น AND ที่เราใช้ในชุดบทเรียนนี้พอดี
บางเซนเซอร์เชื่อได้มากกว่าตัวอื่น เราจึงให้ "น้ำหนัก" ต่างกันได้ แล้วรวมเป็นคะแนนเดียว:
corroboration gate (AND) ที่เราเขียนชุดบทเรียนนี้เป็น กรณีพิเศษ ของสูตรข้างบน — มีแค่สองสัญญาณ (: verdict กับ raw gyro), ให้ เท่ากัน, แล้วตั้ง :
model_hit and raw_okเห็นไหมว่าทั้ง vote, AND และ weighted เป็นสูตรเดียวกัน ต่างกันแค่ค่า , , — โค้ด
fused = model_hit and raw_okในชุดบทเรียนนี้คือมุมที่เข้มงวดที่สุดของสูตรนี้
หัวใจของ fusion ชุดบทเรียนนี้: โมเดลกับเซนเซอร์ดิบ ตอบคนละคำถาม — เอามาประกบกันจึงได้ภาพครบ
โมเดล (edge_ai) |
เซนเซอร์ดิบ (sensors) |
|
|---|---|---|
| ตอบอะไร | "นี่คือท่าอะไร" (label) | "แรงเท่าไรจริงๆ" (ตัวเลข) |
| หน่วย | ความน่าจะเป็น 0..1 (conf) |
ฟิสิกส์ (องศา/วินาที, dBFS, เมตร) |
| รันที่ไหน | CM55 + NPU | CM33 (Python ของเรา) |
| จุดอ่อน | เดาผิดได้ (false positive) | ไม่รู้ว่ารูปแบบคืออะไร |
shaking ตอนที่คุณแค่ยกบอร์ดเร็วๆ — conf อาจสูงพอผ่าน CONF_FLOOR ด้วยซ้ำจำประโยคนี้ไว้: โมเดลบอก "อะไร" เซนเซอร์ดิบบอก "แค่ไหน" — เอาสองมิตินี้มา AND กัน คือ fusion ที่ง่ายที่สุดและได้ผลจริง
ประตูยืนยัน (gmag > MOTION_FLOOR) คือ rule classifier ที่เราทำเป็นแล้วในบทเรียน 3.3–3.4 เราแค่เอามันมาต่อท้าย verdict ของโมเดล
# ฟีเจอร์ดิบจาก IMU (เหมือนบทเรียน 2.1–2.2 + 3.3–3.4): พลังงานการหมุนรวม 3 แกน
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
gmag = abs(gx) + abs(gy) + abs(gz) # องศา/วินาที รวม — ยิ่งสูง ยิ่งหมุนแรง
raw_ok = gmag > MOTION_FLOOR # ประตูฟิสิกส์ (rule จากบทเรียน 3.3–3.4)
gmag คือฟีเจอร์แบบเดียวกับที่ 10_motion_alarm.py ใช้ (abs(gx)+abs(gy)+abs(gz) เทียบ baseline)MOTION_FLOOR เป็นค่าคงที่บนหัวไฟล์ — remix ได้: ตั้งสูง = เข้มงวด, ตั้งต่ำ = ไวขึ้นfusion ไม่ใช่เวทมนตร์ — มันคือ "โมเดล (บทเรียน 1.6–1.7) + rule เซนเซอร์ (บทเรียน 3.3–3.4)" ที่คุณทำเป็นทั้งคู่แล้ว เอามาต่อกัน ชุดบทเรียนนี้แค่ประกอบร่าง
แอปชุดบทเรียนนี้ต่อจากโครง 3 จังหวะของบทเรียน 1.6–1.7 (select → result → action) แล้วแทรก fuse เข้าไปกลาง แล้วขยาย action เป็น publish
จังหวะ 3 กับ 4 คือสิ่งที่ทำให้แอปชุดบทเรียนนี้ต่างจากบทเรียน 6.3–6.4 — เดิม action จบบนบอร์ด คราวนี้ action ผ่านการยืนยัน แล้ว เดินทางออกไปหาคลาวด์
จังหวะ fuse เริ่มที่อ่านสัญญาณดิบมาประกบ verdict sensors.bmi270.motion() คืน 6 ค่าในครั้งเดียว:
import sensors
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# ax,ay,az = ความเร่ง 3 แกน (m/s²) · gx,gy,gz = อัตราหมุน 3 แกน (°/s)
gmag = abs(gx) + abs(gy) + abs(gz) # พลังงานการหมุนรวม
motion() เป็นการอ่าน ดิบ บน CM33 — ไม่ผ่าน NPU ไม่ผ่านโมเดล เป็นตัวเลขฟิสิกส์ตรงๆgx,gy,gz) เพราะ "เขย่า/หมุน" เห็นชัดที่อัตราหมุน — ตรงกับโมเดล Motionsensors.radar()["presence"] (มีคนไหม) เป็น gate แบบ multi-modalเซนเซอร์ตัวเดียวกับที่ป้อนโมเดล (IMU) แต่เราอ่าน คนละเส้นทาง: โมเดลเห็นหน้าต่างสัญญาณผ่าน NPU ส่วนเราอ่านค่าปัจจุบันดิบๆ — สองมุมมองของสัญญาณเดียวกัน
การตัดสินใจแบบ fused = verdict ของโมเดลผ่าน และ เซนเซอร์ดิบผ่าน ทั้งสองด่านต้องจริงพร้อมกัน:
model_hit = (r['label'] == TARGET_CLASS # 1) ใช่คลาสเป้าหมาย
and r['conf'] >= edge_ai.CONF_FLOOR) # + โมเดลมั่นใจพอ
raw_ok = gmag > MOTION_FLOOR # 2) เซนเซอร์ดิบแรงพอจริง
fused = model_hit and raw_ok # AND — ต้องผ่านทั้งคู่
model_hit) คือของเดิมจากบทเรียน 1.6–1.7 / 6.3–6.4 — โมเดลตอบถูกคลาส + มั่นใจถึงเกณฑ์raw_ok) คือของใหม่ — ค่าฟิสิกส์ยืนยันว่า "แรงจริง" ไม่ใช่โมเดลเดาเอาand ทำให้ false positive ของโมเดลถูกกรองออก ถ้าเซนเซอร์ดิบไม่เห็นด้วยลองคิดกลับกัน: ถ้าคุณ เขย่าแรงมาก แต่โมเดลบังเอิญตอบ
idle—fusedก็ยังเป็นFalseเพราะmodel_hitไม่ผ่าน สอง
เหมือนบทเรียน 6.3–6.4 เราไม่อยากส่ง MQTT รัวทุกเฟรมที่ยัง fused อยู่ — ใช้ธง fired ยิง "ตอนขอบขาขึ้น" ครั้งเดียว
if fused and not fired: # เพิ่งเข้าเงื่อนไข fused → ส่งครั้งเดียว
publish_event(r['conf'], gmag)
fired = True
elif not fused: # ออกจากเงื่อนไขแล้ว → รีเซ็ต
fired = False
fired ตั้ง True ตอนส่ง เคลียร์ False ตอนหลุดเงื่อนไข → หนึ่งเหตุการณ์ = หนึ่งข้อความ MQTTpattern เดียวกับปุ่มกด (บทเรียน 1.6–1.7) และ debounce (บทเรียน 6.3–6.4) — เหตุการณ์ที่ส่งออกนอกบอร์ดยิ่งต้องคุมจังหวะ เพราะปลายทางคือระบบที่เราไม่ได้คุมคนเดียว