ทั้งไฟล์อ่านเป็นประโยคเดียว: "เก็บสัญญาณเข้า buffer → พอครบหน้าต่างก็บีบเป็น feature → โชว์เป็นแท่ง → เลื่อนหน้าต่าง → วนต่อ"
ตัวเลขในโค้ด (
buf.append,features(...),buf = buf[-WIN:]) คือ 5 จุดที่คุณต้องเติมในไฟล์ฝึก — จำโครงนี้ไว้ เดี๋ยวไล่ทีละส่วน
ช่องเติมที่ 1: ในลูปเราอ่าน IMU ทีละจุด แล้วต่อท้าย buffer:
for _ in range(HOP): # เก็บทีละ HOP จุด
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# เติม: เก็บ accel แกน Z (az) ต่อท้าย buffer -> buf.append(az)
pass
time.sleep_ms(DT_MS) # 20 ms = 50 Hz
pass ด้วย buf.append(az) — สัญญาณ (แกน Z) ไหลเข้ามาสะสมใน bufsensors.bmi270.motion() คืน 6 ค่า เราหยิบแค่ az มาทำสัญญาณตัวอย่างbuf ว่างตลอด หน้าต่างไม่มีวันครบ แท่งไม่ขยับเลย
time.sleep_ms(20)คือสิ่งที่ทำให้อัตราสุ่มเป็น 50 Hz พอดี — อัตราสุ่มต้องตรงกับที่โมเดลคาดหวัง ไม่งั้นความหมายของ "หน้าต่าง 1 วินาที" จะเพี้ยน
ช่องเติมที่ 2 และ 3: หัวใจของ feature vector อยู่ในฟังก์ชัน features():
def features(win):
n = len(win)
# เติม: ค่าเฉลี่ยของหน้าต่าง -> mean = sum(win) / n
mean = 0.0
# เติม: ส่วนเบี่ยงเบนมาตรฐาน
# -> std = math.sqrt(sum((x - mean) ** 2 for x in win) / n)
std = 0.0
mean = sum(win) / n — ค่าเฉลี่ยของทั้งหน้าต่างstd = math.sqrt(sum((x - mean) ** 2 for x in win) / n) — ต้องคำนวณ mean ก่อนเสมอ เพราะ std ใช้ meanmean/std ค้างที่ศูนย์ ทั้งที่บอร์ดขยับสังเกตลำดับ:
stdพึ่งmean— ถ้าสลับบรรทัดstdจะใช้mean = 0.0ผิดทันที นี่คือกับดักที่ต้องระวัง
ช่องเติมที่ 4: แบ่งหน้าต่างเป็น 4 ช่วงเวลา แล้ววัดพลังงาน (variance) แต่ละช่วง:
band_e = []
seg = n // BANDS
for b in range(BANDS):
s = win[b * seg:(b + 1) * seg] # หนึ่งช่วงเวลา
m = sum(s) / len(s)
# เติม: พลังงาน (variance) ของย่านนี้ ต่อท้าย band_e
# -> band_e.append(sum((x - m) ** 2 for x in s) / len(s))
pass
return [mean, std] + band_e
pass ด้วย band_e.append(sum((x - m) ** 2 for x in s) / len(s))band_eband_e ว่าง feature vector เหลือแค่ 2 ตัว (mean, std) แท่ง band0..3 ไม่ขยับนี่คือ feature "รูปร่างตามเวลา" — ถ้าเปลี่ยนจากแบ่งเวลาเป็นแบ่งความถี่ (เอา FFT จาก บทเรียน 4.3–4.4 มาจัดย่าน) ก็จะกลายเป็น spectrogram ของเสียงทันที
ช่องเติมที่ 5: หลังคำนวณ feature เสร็จ ต้องเลื่อนหน้าต่างให้ซ้อนกับอันต่อไป:
if len(buf) >= WIN:
fv = features(buf[-WIN:]) # บีบ WIN จุดท้ายเป็น feature
...
for i, v in enumerate(fv):
bars[i].value(min(100, int(abs(v) * (2 if i < 2 else 0.02))))
# เติม: เก็บเฉพาะ WIN จุดท้ายไว้ให้หน้าต่างถัดไปซ้อน 50% -> buf = buf[-WIN:]
pass
pass ด้วย buf = buf[-WIN:] — ตัด buffer ให้เหลือแค่ WIN จุดท้ายbuf โตขึ้นเรื่อย ๆ ไม่มีที่สิ้นสุด → กินหน่วยความจำจนหมด (memory leak)
buf[-WIN:]เป็นสำนวน Python ที่หมายถึง "เอาแค่ WIN ตัวท้าย" — เข้าใจมันแล้วคุณคุมหน้าต่างเลื่อนได้ทั้งหมด
ส่วนแสดงผล เอา feature vector มาสเกลให้พอเห็นแล้วตั้งค่าแท่ง (ให้ไว้แล้วในไฟล์ฝึก):
for i, v in enumerate(fv):
scaled = min(100, int(abs(v) * (2 if i < 2 else 0.02)))
bars[i].value(scaled)
lcd.console(" fv = [" + ", ".join("%.2f" % v for v in fv) + "]")
mean/std ค่าน้อย (index < 2) คูณ 2 พอเห็น · band ค่าใหญ่ คูณ 0.02 กันล้นแท่งabs(v) เพราะ mean ติดลบได้ แต่แท่งเริ่มที่ 0lcd.console(...) พิมพ์ค่าจริงเป็นข้อความ — ดูตัวเลขเป๊ะ ๆ ควบคู่กับแท่งการสเกลนี้เป็นแค่ "ให้เห็นด้วยตา" — ตอนป้อนโมเดลจริง เราจะ normalize อย่างเป็นระบบ (โมดูล 5 (Training)) ไม่ใช่คูณค่าคงที่แบบนี้ แต่หลักการ "ทำให้ทุก feature อยู่สเกลใกล้กัน" เหมือนกัน
ไม่มีบอร์ดก็เริ่มได้เลย:
s10_windowing.pystd และ band ขยับตามwindows: เพิ่มขึ้นทุกครั้งที่บีบ feature ชุดใหม่ (ทุก HOP จุด)Emulator ใช้ IMU จำลอง แต่ pipeline window→feature เหมือนบอร์ดจริงทุกบรรทัด — ซ้อมที่บ้านได้เต็มที่
รันแล้วจอจะขึ้นแท่ง feature ทั้ง 6 ตัว ขยับตามสัญญาณ IMU จำลองแบบเรียลไทม์
รันบน BENTO Edge AI Emulator ในเบราว์เซอร์ได้ ไม่ต้องมีบอร์ดก็เห็น feature vector ขยับสด
mean / std (สถิติทั้งหน้าต่าง) · สี่อันถัดไปคือ band0..3 (พลังงานต่อช่วงเวลา)std พุ่งขึ้นชัด ๆจอนี้คือ MVP ของชุดบทเรียนนี้ตอนรันบน emulator — 6 แท่งเดียวกันจะขึ้นเหมือนกันเมื่อ Program to Device ลงบอร์ดจริง
บนบอร์ดจริงใช้ IMU จริง:
s10_windowing.py ใน BENTO IDE กด Program to Devicestd ต่ำ แท่งแทบไม่ขยับstd พุ่งขึ้น band ที่ตรงกับจังหวะสั่นสว่างขึ้นmean เปลี่ยน (ทิศแรงโน้มถ่วงต่อแกน Z เปลี่ยน) แต่ std ยังต่ำจุดที่ควรสังเกต:
meanกับstdเล่าคนละเรื่อง —mean= ท่าทางคงที่ (เอียงแค่ไหน),std= การเคลื่อนไหว (สั่นแค่ไหน) นี่คือเหตุผลที่เราต้องมีหลาย feature
เปิด s10_windowing.py มี # เติม: วางไว้ 5 จุด:
| # | จุด | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | ในลูปเก็บ sample | buf.append(az) |
หน้าต่างไม่ครบ แท่งไม่ขยับ |
| 2 | ใน features() |
mean = sum(win) / n |
แท่ง mean/std ค้างที่ 0 |
| 3 | ใน features() |
std = math.sqrt(sum((x-mean)**2 for x in win)/n) |
แท่ง std ค้างที่ 0 |
| 4 | ใน loop band | band_e.append(sum((x-m)**2 for x in s)/len(s)) |
band0..3 ไม่ขยับ |
| 5 | หลังบีบ feature | buf = buf[-WIN:] |
buffer โตไม่หยุด (memory leak) |
ขั้นตอน:
# เติม: ทีละจุด แทน pass/0.0 ด้วยคำสั่งตามคำใบ้ห้าช่องนี้คือทั้ง pipeline ของ feature front-end — เติมครบเมื่อไร คุณสร้าง "สิ่งที่โมเดลเห็น" ได้ด้วยมือเอง
อยากเข้าใจ spectrogram / mel filterbank / feature extraction ให้ลึกขึ้น ลองดูของดีเหล่านี้:
วิดีโอ (ภาพสวย เข้าใจง่าย)
ภาพ / บทความอ้างอิง
วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง
MVP ของบทเรียน 4.5–4.6 (เกณฑ์ผ่านของชุดบทเรียน): คุณสร้าง feature vector จากสัญญาณดิบด้วยมือเอง — ตัดหน้าต่าง (window+hop) แล้วบีบเป็น mean/std/band ที่เปลี่ยนตามการเคลื่อนไหว
std บอกอะไร"สร้าง feature ได้" ไม่ใช่แค่ "เห็นแท่งขยับ" — คุณต้องบอกได้ว่าทำไมวางนิ่งแล้ว
stdต่ำ และ feature vector นี้จะกลายเป็น dataset ของโมดูล 5 (Training) ยังไง
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 5 จุดในไฟล์ฝึก + ตารางหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs10_windowing.py มีโครงครบทั้งไฟล์แล้ว เหลือแค่ 5 บรรทัดให้เติมs10_windowing.py เติมครบพร้อมคอมเมนต์อธิบายทุกช่อง (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s10_windowing_full.py ฉบับขัดเรียบร้อย เพิ่มการเน้นย่านที่แรงสุด + ตัดสิน "นิ่ง/ขยับ" จาก std + อัตราหน้าต่างต่อวินาทีลองเขียนเองให้สุดก่อนนะ ถ้าติดจริง ๆ ค่อยเปิดเฉลยดูทีละช่อง แล้วกลับมาพิมพ์เอง — เดี๋ยวเราค่อย ๆ แกะไปด้วยกัน
การสร้าง feature front-end ด้วยมือ ซ่อนแนวคิด Analysis หลายชั้นที่จะใช้ต่อในโมดูล 5 (Training):
ฝั่ง DSP / Analysis
ฝั่ง MicroPython / โครงโปรแกรม
buf.append / buf[-WIN:] คุมหน้าต่างเลื่อน.value()ทั้งหมดนี้คือ "สิ่งที่โมเดลเห็น" — ชุดบทเรียนถัดไป (บทเรียน 5.1–5.2) เราจะเก็บ feature vector พวกนี้พร้อม label กลายเป็น dataset จริงที่เอาไปฝึกโมเดลได้
pipeline window→feature ที่เราทำวันนี้ ไม่ใช่ของสมมติ ทุกโมเดล Edge AI จริงมี front-end แบบนี้ก่อนถึงตัวโมเดลเสมอ:
6 โมเดลที่เล่นตั้งแต่บทเรียน 1.1–1.3 ทุกตัวมี feature front-end แบบนี้ซ่อนอยู่ วันนี้เราแค่เปิดฝากล่องมาทำเอง
feature vector ที่เราสร้างวันนี้ ไม่ได้จบแค่โชว์บนจอ — มันคือ หน่วยข้อมูลของ dataset ในโมดูล 5 (Training)
นี่คือเหตุผลที่คอร์สนี้จัด Analysis ไว้ ก่อน Training — คุณต้องรู้ว่า "โมเดลเห็นอะไร" ก่อนจะฝึกมันได้อย่างเข้าใจ
งานทำเอง (ท้ายบทเรียน):
s10_windowing.py ให้ครบทั้ง 5 ช่อง รันได้จริง (Emulator หรือบอร์ด)std ทั้งสองกรณี — ต่างกันกี่เท่า? อธิบายว่าทำไมHOP เป็น WIN (ไม่ซ้อน) แล้วสังเกตว่าอัตราหน้าต่าง (windows:) เปลี่ยนยังไง และเสี่ยงพลาดอะไรใบ้ข้อ 3 — HOP เล็กลง = ได้ feature ถี่ขึ้น (ตอบไว แต่เปลืองแรง) · HOP ใหญ่ = ห่างขึ้น (ประหยัด แต่อาจพลาดเหตุการณ์สั้น)
วันนี้เราได้: เข้าใจว่าโมเดลกิน feature vector ไม่ใช่สัญญาณดิบ · ทำ windowing + hop เอง · บีบหน้าต่างเป็น mean/std/band energy · เห็นว่ามันคือญาติของ mel spectrogram และคือ dataset ของชุดบทเรียนหน้า
ชุดบทเรียนถัดไป (บทเรียน 5.1–5.2 — Training I) เราจะเอา feature front-end นี้ไปเก็บเป็น dataset จริง พร้อม label แล้วเตรียมข้อมูลให้พร้อมฝึกโมเดล เจอกันครับ