ต่อจาก บทเรียน 3.1–3.2 ที่เราแปลงสัญญาณดิบเป็นค่าฟิสิกส์ วันนี้เราเดินอีกก้าวในขั้น Processing:
dsp.dew_point และ dsp.heat_index แปลง temp+humidity เป็นข้อมูลที่ "ตัดสินง่ายขึ้น"classify() ของตัวเอง ให้จอโชว์คำตัดสิน comfortable/hot/humid/... พร้อมสีปลายทางของวันนี้: บอร์ดอ่านอุณหภูมิ+ความชื้นจริง แล้วโชว์คำตัดสินเป็นคลาสที่เปลี่ยนตามอากาศตรงหน้า
วันนี้เน้น "ตัดสินใจด้วยกฎที่เราเข้าใจทุกบรรทัด" — ความเข้าใจนี้คือฐานที่ทำให้ ML ในบทเรียน 5.3–5.5 ไม่ใช่กล่องดำ
เรายังอยู่ที่ ขั้น 2 · Processing เหมือน บทเรียน 3.1–3.2 แต่ขยับจาก "แปลงค่า" มาเป็น "ตัดสินใจจากค่า"
น่าสนใจตรงที่ การจำแนก ปรากฏได้สองที่: ที่นี่ (ขั้น 2) ทำด้วยกฎมือ และที่ขั้น 4 ทำด้วย ML ที่เรียนเอง งานเดียวกัน คนละวิธีหาเส้นแบ่ง — วันนี้เราทำแบบแรกให้เข้าใจทะลุก่อน
Classifier คือฟังก์ชันที่รับข้อมูลตัวเลขเข้าไป แล้วคายออกมาเป็น "คลาส" — ป้ายชื่อหนึ่งจากชุดที่กำหนดไว้
comfortable / hot / humid / cold / dry / danger — 6 ป้ายidle/circle/shaking) เพียงแต่ "ข้างใน" เป็น neural networkจำรูปนี้ไว้: ตัวเลข → กล่องตัดสิน → คลาส ทั้งคอร์สจะเจอกล่องนี้ซ้ำ วันนี้เราเปิดกล่องแล้วเขียนข้างในด้วยมือเอง
งานจำแนกคือการ "ลากเส้นแบ่ง" ในพื้นที่ของค่า สองสำนักลากเส้นคนละวิธี:
อย่าเพิ่งด่ากฎมือว่าล้าสมัย ในงานจริงหลายอย่าง กฎ threshold ง่ายๆ ชนะ ML เพราะเบา อธิบายได้ ไม่ต้องมีข้อมูล วิศวกรเก่งคือคนที่รู้ว่าเมื่อไรควรใช้กฎ เมื่อไรควรใช้ ML — ชุดบทเรียนนี้ให้คุณรู้จักฝั่งกฎก่อน
ค่าดิบบางทีตัดสินยาก อุณหภูมิ 32°C ตอนแห้งกับตอนชื้นจัด "รู้สึก" ต่างกันมาก ค่าอนุพัทธ์รวมหลายค่าดิบเป็นตัวเดียวที่ "ตัดสินง่ายขึ้น"
บทเรียนสำคัญ: classifier จะดีแค่ไหน ขึ้นกับว่าเราป้อน "ค่าอะไร" ให้มัน ป้อนค่าดิบเปล่าๆ กฎอาจตัดสินพลาด แต่ป้อน heat index กฎจับ "ร้อนอบอ้าว" ได้แม่นขึ้นทันที
Dew point (จุดน้ำค้าง) คืออุณหภูมิที่อากาศชื้นจนไอน้ำเริ่มกลั่นเป็นหยดน้ำ ยิ่ง dew point สูงเทียบกับอุณหภูมิ อากาศยิ่ง "อึดอัดชื้น"
dsp.dew_point(t, rh) ใช้สูตร Magnus–Tetens คืนค่าเป็นองศาเซลเซียส:
import dsp
dp = dsp.dew_point(30.0, 75.0) # ~25.1 C -> ชื้นมาก ใกล้อุณหภูมิจริง
dp = dsp.dew_point(30.0, 30.0) # ~10.5 C -> แห้ง ห่างจากอุณหภูมิจริงเยอะ
dsp คำนวณให้ และ dew point ที่เข้าใกล้อุณหภูมิ = อากาศอิ่มตัว/ชื้นมากตัวเลขในคอมเมนต์ (
~25.1 C) มาจากสูตรจริงในเฟิร์มแวร์ (moddsp.c) ไม่ใช่เดา — ลองเรียกใน REPL เทียบดูได้
สูตร Magnus แบ่งเป็นสองก้าว: ก้าวแรกรวม t กับ RH เป็นตัวกลาง (แกมมา) ก้าวสองแปลง กลับเป็นอุณหภูมิจุดน้ำค้าง:
อ่านทีละสัญลักษณ์แบบภาษาคน:
dsp ใส่ให้แล้วทำไมต้องรู้แค่นี้ก็พอ: พจน์ คือหัวใจ — ตอนอากาศชื้นจัด ทำให้ (ค่าลบน้อยลง) จึงโตขึ้น แล้วดัน ให้ เข้าใกล้ พอ เกือบเท่า = อากาศอิ่มตัว น้ำเริ่มกลั่น
นี่คือเหตุผลที่บันไดกฎวันนี้ดูที่ ระยะห่าง เป็นสัญญาณความชื้น: ห่างมาก = แห้งสบาย, ห่างน้อย = ชื้นอึดอัด สูตรทำงานหนักแทนเรา เราแค่เอา "ระยะห่าง" ไปตั้งเส้นแบ่ง
Heat index (ดัชนีความร้อน) รวมอุณหภูมิกับความชื้นเป็น "อุณหภูมิที่ร่างกายรู้สึก" ตอนชื้นจัด เหงื่อระเหยยาก เลยรู้สึกร้อนกว่าตัวเลขจริง
dsp.heat_index(t, rh) ใช้ Rothfusz regression คืนค่าเป็นองศาเซลเซียส:
import dsp
hi = dsp.heat_index(32.0, 40.0) # ~32 C -> ชื้นน้อย รู้สึกใกล้อุณหภูมิจริง
hi = dsp.heat_index(32.0, 80.0) # ~44 C -> ชื้นจัด รู้สึกร้อนกว่าจริง ~12 องศา
สังเกตพลังของ derived metric: ถ้าเราจำแนกจากอุณหภูมิดิบอย่างเดียว จะพลาด "ร้อนอบอ้าวตอนชื้น" ทั้งที่มันคือภาวะอันตรายจริง — feature ที่ดีทำให้กฎง่ายๆ ฉลาดขึ้น
โมดูล dsp มีฟังก์ชันแปลงค่า+จำแนกสิ่งแวดล้อมสำเร็จรูปให้แล้ว ทั้งหมดเป็นคณิต/ฟิสิกส์ล้วน รันบน CM33 ไม่ใช้ NPU:
| คำสั่ง | รับ | คืน |
|---|---|---|
dsp.dew_point(t, rh) |
อุณหภูมิ°C, %RH | จุดน้ำค้าง °C |
dsp.heat_index(t, rh) |
อุณหภูมิ°C, %RH | "รู้สึกเหมือน" °C |
dsp.comfort_zone(t, rh) |
อุณหภูมิ°C, %RH | คลาส str: cold/hot/dry/humid/comfortable/acceptable |
dsp.comfort_zone คือ "ของที่ทำงานได้" ที่เราจะรันก่อน แล้วแกะ แล้วเขียนเวอร์ชันของเราเองแข่งกับมันเซนเซอร์ที่ป้อนค่าพวกนี้: DPS368 (ความดัน+อุณหภูมิ) กับ SHT40 (ความชื้น) — AI Kit มีทั้งคู่ Eva Kit ไม่มี ชุดบทเรียนนี้จึงรันบน AI Kit หรือ Emulator
dsp.comfort_zone ที่เรารันตอนเปิดบทเรียน ข้างในมันคือ บันไดกฎ เขียนด้วย C — ไม่มี AI สักนิด เทียบเป็น Python ได้ประมาณนี้:
def comfort_zone(t, rh): # กฎจริงใน moddsp.c (แปลงเป็น Python ให้อ่านง่าย)
if t < 18: return "cold"
if t > 27: return "hot"
if rh < 30: return "dry"
if rh > 70: return "humid"
if 20 <= t <= 25 and 30 <= rh <= 60:
return "comfortable"
return "acceptable"
if ไล่ทีละขั้น — นี่แหละบันไดกฎ ที่เราจะเขียนเองในไฟล์ฝึกการ "แกะของสำเร็จรูปให้เห็นว่าข้างในไม่มีเวทมนตร์" คือหัวใจของการเรียนแบบกลับด้าน — พอเห็นว่ากฎนี้เป็นแค่
ifไม่กี่บรรทัด คุณจะกล้าเขียนของตัวเองทันที
หน่วยย่อยที่สุดของกฎคือ เส้นแบ่งเดียว (single threshold): ค่าหนึ่งค่าเทียบกับเลขหนึ่งเลข แล้วแบ่งโลกเป็นสองฝั่ง
if heat_index >= 32:
verdict = "hot" # ฝั่งเกินเส้น
else:
verdict = "not hot" # ฝั่งไม่ถึงเส้น
เก็บคำถามนี้ไว้: "ตั้งเส้นที่ 32 เอามาจากไหน?" ในกฎมือเราตอบได้ (มาตรฐาน heat index) แต่พอเป็น ML โมเดลจะหาเส้นเองจากข้อมูล — นั่นคือความต่างที่เราจะเห็นชัดในโมดูล 5 (Training)
พอต้องการหลายคลาส เราเรียง if เป็นชั้นๆ ไล่จากบนลงล่าง ขั้นแรกที่เงื่อนไขจริงชนะทันที แล้ว return จบ:
def classify(t, h, hi):
if hi >= 41: return "danger" # ชั้น 1 (อันตรายสุด อยู่บนสุด)
if hi >= 32: return "hot" # ชั้น 2
if h >= 70: return "humid" # ชั้น 3
if t < 20: return "cold" # ชั้น 4
if h < 30: return "dry" # ชั้น 5
return "comfortable" # ตกทุกชั้น = สบาย
return ตัดจบทันที คลาสล่างๆ จะได้ก็ต่อเมื่อผ่านทุกชั้นบนมาแล้วบันไดกฎอ่านง่ายและ "อธิบายได้" ทุกคำตัดสิน แต่มีกับดักหนึ่งข้อที่ทำคนพลาดบ่อย — ลำดับของชั้น ไปดูหน้าถัดไป
บันไดกฎ return ขั้นแรกที่เจอ ดังนั้นถ้าวางชั้นผิดลำดับ คลาสที่ควรชนะจะโดนชั้นบนแย่งไปก่อน
กฎเหล็ก: เงื่อนไขที่เฉพาะ/รุนแรงกว่าต้องอยู่บน เงื่อนไขกว้างต้องอยู่ล่าง ถ้าวางสลับ ชั้นล่างจะกลายเป็น "โค้ดที่ไม่มีวันทำงาน" (dead branch) — บั๊กชนิดที่ compiler ไม่เตือน ต้องจับด้วยการทดสอบ