หัวใจของการเก็บคือคำสั่งเดียว: อ่าน IMU 6 แกนพร้อมกันในทูเพิลเดียว — accelerometer 3 แกน + gyroscope 3 แกน
import sensors
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# ax,ay,az = ความเร่ง (m/s^2) — az ~ 9.81 ตอนวางนิ่ง (แรงโน้มถ่วง)
# gx,gy,gz = ความเร็วเชิงมุม (deg/s) — ~0 ตอนไม่หมุน
motion() คืน 6 ค่าในครั้งเดียว = 6 คอลัมน์ของ CSV เป๊ะ (ไม่ต้องเรียก accel/gyro แยก)20 ms ต่อครั้ง = 50 Hz ตรงกับอัตราที่โมเดล Motion บนบอร์ดกิน — สำคัญเพื่อให้ dataset เข้ากับโมเดลได้ลองใน REPL ก่อน:
import sensors; sensors.bmi270.motion()แล้ววางบอร์ดนิ่งๆ ดูว่าazใกล้ 9.81 ไหม — นี่คือการ "ถามฮาร์ดแวร์" เพื่อเช็กว่า sensor พร้อมก่อนเก็บจริง
s11_dataset.py เดินตามโครงร่วมเดียวกับทุกโปรแกรม MicroPython ในคอร์ส (import → สร้างครั้งเดียว → ลูป → ui.poll) — คุ้นแล้วจากชุดบทเรียนก่อน ๆ
โครงเดิมเป๊ะ — สิ่งใหม่ชุดบทเรียนนี้อยู่ใน
record()(จังหวะ 3): เพิ่ม "นับสมดุล" ทับการอ่าน-เขียนแบบ บทเรียน 2.1–2.2 นี่คือจุดที่คุณจะเติมโค้ด
ทั้งไฟล์อ่านเป็นประโยคเดียว: "เลือกป้าย → อ่าน IMU เป็นชุด → เขียนลง CSV → นับจำนวนต่อคลาส → บอกว่าคลาสไหนยังน้อย → พอครบทุกคลาสถึงเป้า = dataset พร้อม"
ตัวเลขช่อง (1–4) ชี้จุดที่ต้องเติม — ช่อง 1–2 คือ "อ่าน+เขียน" ที่ยกมาจาก บทเรียน 2.1–2.2 · ช่อง 3–4 คือ สมองใหม่ของชุดบทเรียนนี้: การเฝ้า class balance
ช่องเติมที่ 1: ในลูปเก็บ อ่านค่า IMU 6 แกนมาเก็บในตัวแปร 6 ตัว:
def record(label):
with open(PATH, "a") as f:
for _ in range(BURST): # BURST=200 sample ต่อการกดหนึ่งครั้ง
# เติม: อ่าน IMU 6 แกน -> ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
ax = ay = az = gx = gy = gz = 0.0
pass
f.write(...) # (ช่อง 2)
time.sleep_ms(RATE_MS) # 20 ms = 50 Hz
pass ด้วย ax, ay, az, gx, gy, gz = sensors.bmi270.motion() — อ่าน 6 ค่าในบรรทัดเดียว0.0 หมด (ค่าเริ่มต้น) — dataset ไม่มีข้อมูลจริง train ไม่ได้อ่านที่
50 Hz(ทุก 20 ms) เพราะโมเดลบนบอร์ดกินอัตรานี้ — ถ้าเก็บที่อัตราอื่น ท่าเดียวกันจะ "ยืด/หด" ในหน้าต่าง โมเดลจะสับสน
ช่องเติมที่ 2: เขียน sample ที่อ่านได้เป็นหนึ่งบรรทัด โดยขึ้นต้นด้วยป้าย:
# เติม: เขียนบรรทัด "label,ax,ay,az,gx,gy,gz" ลงไฟล์
# f.write("%s,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n"
# % (label, ax, ay, az, gx, gy, gz))
pass
label,ax,ay,az,gx,gy,gz เป๊ะ — ตรงกับ CHANNELS ใน dataset_tools.py ไม่งั้น load_csv() อ่านผิดช่อง%.4f = ทศนิยม 4 ตำแหน่ง พอสำหรับ IMU และไม่ทำให้ไฟล์ใหญ่เกินป้าย (
label) มาก่อนเสมอ —dataset_tools.pyใช้CLASSES.index(row["label"])แปลงชื่อคลาสเป็นตัวเลข ถ้าสะกดป้ายผิด (เช่นIdleตัวใหญ่) มันจะหาไม่เจอแล้ว error
ช่องเติมที่ 3: หลังเขียนครบชุด บวกจำนวนที่เก็บเข้าตัวนับของคลาสนั้น — นี่คือหัวใจ "dataset engineering" ที่ต่างจาก บทเรียน 2.1–2.2:
# (จบลูปเขียนไฟล์แล้ว)
# เติม: บวกจำนวน sample ของคลาสนี้ -> counts[label] += BURST
pass
refresh_balance() # อัปเดตแถบสมดุลบนจอ (ให้ไว้แล้ว)
counts เป็น dict {"idle":0, "circle":0, "shaking":0} เก็บจำนวนสะสมต่อคลาสpass ด้วย counts[label] += BURST — ทุกครั้งที่เก็บครบชุด ตัวนับของคลาสนั้นเพิ่มการนับต่อคลาสคือสิ่งที่ทำให้เราเก็บ "อย่างมีสติ" — เห็นตัวเลขจริงว่าคลาสไหนน้อย แล้วเก็บเพิ่มให้สมดุล ไม่ใช่เดา
ช่องเติมที่ 4: หาคลาสที่เก็บได้ น้อยที่สุด ตอนนี้ แล้วบอกผู้ใช้ให้ไปเก็บคลาสนั้นเพิ่ม:
def refresh_balance():
for c in CLASSES: # อัปเดตแถบต่อคลาส (ให้ไว้แล้ว)
bars[c].value(min(counts[c], TARGET))
# เติม: หาคลาสที่มี sample น้อยสุด -> fewest = min(counts, key=counts.get)
fewest = CLASSES[0]
pass
if counts[fewest] < TARGET:
hint.text("เก็บ '%s' เพิ่ม (น้อยสุดตอนนี้)" % fewest)
else:
hint.text("ครบทุกคลาสถึงเป้า — dataset พร้อม split!")
pass ด้วย fewest = min(counts, key=counts.get) — คืน ชื่อคลาส ที่ค่าน้อยสุดใน dictmin(counts, key=counts.get) วนคีย์ทั้งหมด เทียบด้วยค่า (counts.get) แล้วคืนคีย์ที่ค่าน้อยสุดidle ตลอด (ค่าเริ่มต้น) — นำทางผิด เก็บไม่สมดุลนี่คือความต่างที่จับต้องได้ระหว่าง "logger" (บทเรียน 2.1–2.2) กับ "dataset engineering" (บทเรียน 5.1–5.2) — โปรแกรมช่วยคุณ ตัดสินใจว่าจะเก็บอะไรต่อ เพื่อให้ผลลัพธ์สมดุล
ก่อนลงบอร์ดจริง เปิด BENTO Edge AI Emulator ดูหน้าตาโปรแกรมเก็บข้อมูลได้ก่อน — เห็นค่า IMU 6 แกนสดๆ และปุ่ม label ทำงานเหมือนบนบอร์ด

จอ BENTO Emulator ขณะรันแดชบอร์ด IMU (ตัวอย่าง 01) — ค่าที่เห็นมาจากตัวจำลอง IMU ตัวเดียวกับที่ s11_dataset.py อ่าน พอรันโปรแกรมเก็บข้อมูล จอจะมีปุ่ม label และแถบสมดุลต่อคลาสแทน
sensors.bmi270.motion() ทำงานก่อนเก็บจริงใช้ emulator เพื่อทำความคุ้นเคยกับหน้าจอและปุ่ม label ก่อน แล้วค่อยไปเก็บของจริงบนบอร์ด — ประหยัดเวลาลองผิดลองถูก
เปิด s11_dataset.py ในไฟล์มี pass วางไว้ 4 จุด ตรงที่ต้องเติมคำสั่งจริง:
| # | จุด | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | ในลูป record | ax, ay, az, gx, gy, gz = sensors.bmi270.motion() |
CSV เป็น 0.0 หมด |
| 2 | หลังอ่าน | f.write("%s,%.4f,...\n" % (label, ax, ...)) |
ไฟล์ว่าง / บรรทัดหาย |
| 3 | จบชุด | counts[label] += BURST |
แถบสมดุลไม่ขยับ |
| 4 | refresh | fewest = min(counts, key=counts.get) |
คำแนะนำชี้ผิดคลาส |
ขั้นตอน:
# เติม: ทีละจุด แล้วแทน pass ด้วยคำสั่งตามคำใบ้สี่ช่องนี้คือ "อ่าน → เขียน → นับ → นำทาง" ครบวงของการเก็บ dataset ที่สมดุล เติมครบเมื่อไร คุณได้ไฟล์ที่ train ได้จริง
พอเก็บครบแล้ว คัดลอก /gestures.csv ออกจากบอร์ดมาที่ PC แล้วให้ dataset_tools.py ทำ 2 ขั้นสุดท้าย (window + split):
import dataset_tools as dt
s, l = dt.load_csv("data/gestures.csv") # อ่าน CSV ของบอร์ด
X, y = dt.make_windows(s, l) # ตัดเป็นหน้าต่าง 50 sample
(Xtr,ytr),(Xva,yva),(Xte,yte) = dt.split(X, y) # แบ่ง train/val/test
(Xtr,Xva,Xte),(mean,std) = dt.normalize(Xtr, Xva, Xte) # fit บน train เท่านั้น
print("train", Xtr.shape, "val", Xva.shape, "test", Xte.shape)
train.py ในบทเรียน 5.3–5.5 — dataset ที่ "จบงานเตรียม" แล้วสังเกตว่าโค้ด PC สั้นมาก เพราะงานหนัก (เก็บให้สมดุล ติดป้ายถูก) ทำเสร็จตั้งแต่ฝั่งบอร์ดแล้ว — dataset ที่ดีทำให้ขั้นต่อไปง่ายลงทั้งหมด
ก่อนบอกว่า "พร้อม train" ตรวจสองอย่างให้ผ่านก่อน คือ สมดุล และ แบ่งครบทุกคลาส:
import numpy as np
print("windows/class :", np.bincount(y)) # ควรใกล้เคียงกันทุกคลาส
for name, yy in [("train",ytr),("val",yva),("test",yte)]:
print(name, "class counts", np.bincount(yy, minlength=3))
# train class counts [70 71 69] ← ทุกกองมีครบ 3 คลาส สัดส่วนใกล้กัน
# val class counts [15 15 14]
# test class counts [15 15 14]
test เป็น [20 20 0]) แปลว่าเก็บ shaking น้อยเกิน — กลับไปเก็บเพิ่มบนบอร์ดการตรวจนี้คือ "การสอบผ่าน" ของชุดบทเรียน — ตัวเลขต้องบอกเองว่า dataset สมดุลและแบ่งถูก ไม่ใช่แค่ "มีไฟล์"
อยากเข้าใจ dataset / train-test split ให้ลึกขึ้น ลองดูจากแหล่งที่อธิบายดีเหล่านี้ (ลิงก์ไปต้นทาง ไม่ได้ฝังวิดีโอในสไลด์)
วิดีโอ (ช่องการศึกษาที่น่าเชื่อถือ)
บทความ / เอกสารอ้างอิง (ภาพ + คำอธิบาย)
train_test_split — เอกสารทางการ scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html (ที่มา: scikit-learn docs, BSD-3-Clause)วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง
MVP ของบทเรียน 5.1–5.2 (เกณฑ์ผ่านของชุดบทเรียน): คุณเก็บ dataset จากบอร์ดที่ สะอาด · สมดุล · แบ่งแล้ว — ทุกกอง (train/val/test) มีครบ 3 คลาส สัดส่วนใกล้เคียงกัน
dataset_tools.py"dataset พร้อม" ไม่ใช่แค่ "มีไฟล์ CSV" — คุณต้องชี้ตัวเลข
np.bincountให้เห็นว่าสมดุลจริง และเล่ากฎ no-leakage ได้
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 4 จุดในไฟล์ฝึก + ตารางหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs11_dataset.py มีโครงครบทั้งไฟล์ (UI + ลูป + refresh_balance) เหลือแค่ 4 บรรทัดให้เติมs11_dataset.py เติมครบพร้อมคอมเมนต์อธิบายทุกช่อง (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s11_dataset_full.py ฉบับขัดเรียบร้อย: เพิ่มการเตือนคลาสไม่สมดุล + สรุปสัดส่วนตอนออก + ปุ่มล้างไฟล์ลองเขียนเองให้สุดก่อนนะ ถ้าติดจริงๆ ค่อยเปิดเฉลยดูทีละช่อง แล้วกลับมาพิมพ์เอง — เดี๋ยวเราค่อย ๆ แกะไปด้วยกัน
การเก็บ dataset หนึ่งชุดซ่อนแนวคิดของสาย ML ที่จะใช้ไปตลอดคอร์ส:
ฝั่ง dataset engineering
ฝั่ง MicroPython / โครงโปรแกรม
sensors.bmi270.motion() + f.write() เก็บข้อมูลลงไฟล์บนบอร์ดui.poll (เหมือนทุกบทเรียน)counts ต่อคลาสระหว่างเก็บ ไม่ใช่เก็บดะทั้งหมดนี้ยืนบนไฟล์ CSV ธรรมดา — พลังของการเข้าใจ dataset คือ ตอน train แม่นหรือพัง คุณจะรู้ว่าย้อนไปแก้ที่ข้อมูล ไม่ใช่ไล่จูนโมเดลอย่างเดียว
ทุกทีมที่ทำ Edge AI จริงใช้เวลากับการเตรียมข้อมูลมากกว่าการ train โมเดลเสียอีก — งานที่เราทำวันนี้คือหัวใจของสายอาชีพ ML:
ที่เราเฝ้าแถบสมดุลกับกันข้อมูลรั่ววันนี้ ไม่ใช่พิธีกรรมในห้องเรียน — มันคือสิ่งที่ทีม ML มืออาชีพทำจริงทุกวัน เพราะ "ข้อมูลขยะเข้า ก็ได้โมเดลขยะออก"
งานทำเอง (ท้ายบทเรียน):
s11_dataset.py ให้ครบทั้ง 4 ช่อง เก็บ dataset จริงบนบอร์ด — ทั้ง 3 คลาสถึงเป้าgestures.csv มา PC แล้วรัน split ด้วย dataset_tools.py พิมพ์ np.bincount ของทั้ง 3 กองidle เยอะกว่าเพื่อน 3 เท่า) แล้วอธิบายว่าสัดส่วนใน train/val/test เปลี่ยนยังไงใบ้ข้อ 3 — เพราะ split เป็น stratified สัดส่วนที่ไม่สมดุลจะ ติดไปทั้ง 3 กอง เท่าๆ กัน ไม่ได้หายไปเอง ปัญหาต้องแก้ที่ "ตอนเก็บ" ไม่ใช่ตอน split
วันนี้เราได้: เข้าใจว่า dataset ที่ดีคืออะไร · เก็บ IMU ที่ติดป้ายลง CSV บนบอร์ด · เฝ้า class balance ระหว่างเก็บ · แบ่ง train/val/test แบบ stratified โดยไม่ให้ข้อมูลรั่ว
ชุดบทเรียนถัดไป (บทเรียน 5.3–5.5) เราจะเอา dataset นี้ไป train เป็นโมเดลจริง ใน Docker ด้วย TensorFlow แล้วส่งออกเป็น
.tflite— โมเดลตัวแรกที่เป็นของเราเองทั้งตัว เจอกันครับ