จบชุดบทเรียนนี้เราจะเข้าใจ 4 เรื่องของ DAQ (Data Acquisition) แล้วปิดท้ายด้วยไฟล์ dataset จริง:
label,ax,ay,az,gx,gy,gzปลายทางของวันนี้: กดปุ่ม label ทำท่าค้างไว้ ระบบเก็บ 200 samples/ครั้งลง /gestures.csv — ครบทุกท่าแล้วได้ dataset พร้อม train
เรากำลังเดินถอยจากปลายน้ำ (บทเรียน 1.1–1.3 และ 1.6–1.7 รันโมเดลสำเร็จรูป) มาที่ ต้นน้ำ — ที่ซึ่งข้อมูลเกิดขึ้นจริง นี่คือ Pillar 1 ของทั้งวงจร
จำวงจร 5 ขั้นจากบทเรียน 1.1–1.3 ได้ไหม — DAQ → Processing → Analysis → Training → Apps เราเริ่มจากขั้น Apps (ปลายน้ำ) มาสามชุดบทเรียน วันนี้ถอยกลับมาที่ขั้น 1 · DAQ (ต้นน้ำ)
ทำไมถอยมาต้นน้ำตอนนี้? เพราะจะเข้าใจว่าโมเดล "เห็นอะไร" ต้องเริ่มจากการเป็นคนป้อนข้อมูลให้มันเองก่อน — ชุดบทเรียนนี้คือก้าวแรกของการเป็นเจ้าของ dataset
DAQ (Data Acquisition) คือการ เก็บข้อมูลดิบจากเซนเซอร์ ให้เป็นชุดที่จัดเก็บได้ ค้นได้ เอาไปใช้ต่อได้ — นี่คือวัตถุดิบตั้งต้นของทุกโมเดล
"DAQ" เป็นคำที่วิศวกรใช้จริงในสายวัดคุม/instrumentation มานานก่อนยุค AI — หัวใจเหมือนเดิม: อ่านสัญญาณให้ถูกจังหวะ แล้วเก็บให้ครบถ้วนตรงเวลา
มีคำพูดในวงการ ML ที่จริงเสมอ: "ข้อมูลเข้าเป็นขยะ ผลออกก็เป็นขยะ" โมเดลเก่งแค่ไหนก็แพ้ dataset ที่เก็บมั่ว
shaking แต่ดันวางบอร์ดนิ่ง ข้อมูลนั้นจะสอนโมเดลผิดidle 1000 แต่ shaking 50 โมเดลจะเอนไปทาย idleชุดบทเรียนนี้เราจะลงมือเก็บจริง แล้วคุณจะรู้สึกเองว่า "การเก็บข้อมูลดีๆ ก็เป็นงานฝีมือ" — ไม่ใช่แค่กดปุ่มมั่วๆ นี่คือเหตุผลที่ dataset ดีมีค่า
อัตราสุ่ม คือ "จำนวนครั้งที่เราอ่านเซนเซอร์ต่อวินาที" หน่วยเป็น เฮิรตซ์ (Hz) — 50 Hz แปลว่าอ่าน 50 ครั้งใน 1 วินาที คือทุกๆ 20 มิลลิวินาที
กฎง่ายๆ: ถ้าจะจับสัญญาณที่แกว่งเร็วแค่ไหน ต้องสุ่มให้ถี่กว่านั้นอย่างน้อยเท่าตัว (เดี๋ยวเรื่อง FFT ในโมดูล 4 (Analysis) จะเจาะลึกกฎนี้)
ในไฟล์เราตั้ง RATE_MS = 20 ซึ่งคือ 50 Hz ไม่ใช่เลขสุ่ม แต่เลือกให้ ตรงกับอัตราที่โมเดล Motion บนบอร์ดกินจริง
RATE_MS = 20 # 50 Hz — ตรงกับอัตราที่โมเดล Motion บนบอร์ดกิน
BURST = 200 # 200 sample = ~4 วินาที ที่ 50 Hz
BURST = 200 เลือกให้หนึ่งครั้งที่กดปุ่ม ได้ข้อมูล ~4 วินาที — ยาวพอให้ทำท่าได้เต็มรอบจำเลข 20 ms นี้ไว้ให้ดี — ในบทเรียน 1.1–1.3 เราเคยเห็น
edge_aiอ่านผลทุก ~180 ms แต่ การป้อนข้อมูลให้โมเดล ต้องถี่กว่านั้นมาก เพราะโมเดลต้องเห็นรูปคลื่นละเอียด
ทำไมต้องสุ่ม "เร็วกว่าสองเท่า" ของสัญญาณที่อยากจับ? มีกฎคณิตศาสตร์รองรับ เรียกว่า กฎการสุ่มของ Nyquist–Shannon:
อ่านสัญลักษณ์ทีละตัวแบบง่ายๆ:
ถ้าสุ่มช้ากว่าเกณฑ์นี้ สัญญาณเร็วจะ "แปลงร่าง" กลายเป็นคลื่นช้าปลอมๆ เรียกว่า aliasing — dataset จะโกหกโดยที่เราไม่รู้ตัว นี่คือเหตุผลลึกๆ ที่ชุดบทเรียนนี้ย้ำเรื่องอัตราสุ่มคงที่
รู้อัตราสุ่มแล้ว คำนวณต่อได้เลยว่า หนึ่ง burst ที่เก็บ sample กินเวลากี่วินาที:
BURST = 200แทนค่าจริงของชุดบทเรียนนี้:
นี่คือที่มาของ "~4 วินาที" ที่พูดถึงตอนเลือก BURST. ลองสังเกต: ถ้าลด เหลือ 25 Hz แต่ยังเก็บ เท่าเดิม จะได้ วินาที — นานขึ้นสองเท่า ตรงกับการบ้านข้อ 3 พอดี
ทำไมต้องรู้ ? เพราะตอน train โมเดลกินข้อมูล ทีละหน้าต่าง ( วินาที) เราต้องมั่นใจว่าหน้าต่างยาวพอครอบท่าเต็มรอบ — สูตรสั้นๆ นี้บอกได้ทันทีโดยไม่ต้องเดา
เราตั้ง BURST = 200 หนึ่งครั้งที่กดปุ่ม จะเก็บ 200 sample ต่อเนื่อง ทำไมต้อง 200 ไม่ใช่ 5 หรือ 5000?
circle ออกจาก shaking ได้เดี๋ยวในบทเรียน Analysis (บทเรียน 4.5–4.6) เราจะเจอคำว่า window (หน้าต่าง) — โมเดลจริงกินข้อมูลทีละหน้าต่าง เช่น 1-2 วินาที
BURSTวันนี้คือการเก็บให้ยาวพอครอบหลายหน้าต่าง
CSV (Comma-Separated Values) คือไฟล์ข้อความธรรมดา แต่ละบรรทัดคือหนึ่งแถว แต่ละค่าคั่นด้วยจุลภาค บรรทัดแรกคือ หัวตาราง (schema) บอกว่าคอลัมน์ไหนคืออะไร
label = ชื่อท่า (เราเลือกก่อนกด) · ax..az = ความเร่ง 3 แกน · gx..gz = ไจโร 3 แกนschema คือ "สัญญา" ระหว่างคนเก็บข้อมูลกับคนเทรน — ถ้าเรียงคอลัมน์สลับ หรือหัวไม่ตรง เครื่อง train จะอ่านผิดทันที ต้องเป๊ะ
จุดที่น่าทึ่ง: เราเขียนไฟล์ CSV ลง หน่วยความจำ flash บนบอร์ดเอง ด้วย open() / write() แบบเดียวกับ Python บนคอมเป๊ะ
with open("/gestures.csv", "a") as f: # "a" = append ต่อท้าย
f.write("shaking,0.91,-0.04,9.78,12.4,-3.1,0.9\n")
"/gestures.csv" — path เริ่มด้วย / คือ root ของ filesystem บนบอร์ด (LittleFS)"a" (append) = เขียนต่อท้าย ไม่ลบของเก่า — สำคัญมาก ถ้าใช้ "w" ทุกครั้งจะทับ dataset หายหมดwith open(...) as f: — เปิดแล้วปิดให้อัตโนมัติ ข้อมูลถูก flush ลง flash เรียบร้อยตอนออกจาก withข้อมูลอยู่บนอุปกรณ์ ไม่ได้ขึ้นคลาวด์ — สอดคล้องกับหัวใจ Edge AI ตั้งแต่ชุดบทเรียนแรก: ข้อมูลเกิดที่ไหน ประมวลผล/เก็บที่นั่น เดี๋ยวโมดูล 5 (Training) เราค่อยดึงไฟล์นี้ออกไป train บนคอม
หัวใจของการ sample คือคำสั่งเดียว มันคืน 6 ค่าพร้อมกัน: ความเร่ง 3 แกน + ไจโร 3 แกน
import sensors
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# |__ accelerometer (m/s^2) __| |__ gyroscope (dps) __|
ax, ay, az — ความเร่งแนวแกน X/Y/Z หน่วย m/s² (วางนิ่งแกนที่ตั้งฉากพื้นจะอ่านได้ ~9.8 จากแรงโน้มถ่วง)gx, gy, gz — ความเร็วเชิงมุม (การหมุน) หน่วยองศา/วินาทีmotion() ไม่ใช่ acceleration() + gyroscope() แยก? เพราะ motion() อ่านทั้ง 6 แกน ใต้ bus lock เดียว = ทุกแกนเป็นเวลาเดียวกันเป๊ะ (snapshot ตรงเวลา)เวลาตรงกันของทุกแกนสำคัญมากตอน train — ถ้า ax กับ gx มาจากคนละจังหวะเวลา รูปคลื่นจะเพี้ยน
motion()แก้ปัญหานี้ให้เราแล้ว
นิสัยที่ดีจากบทเรียน 1.1–1.3: ถามฮาร์ดแวร์ก่อน อย่าเดา ก่อนเขียน logger ลองเรียก motion() ใน REPL ดูว่าได้อะไรกลับมาจริง
>>> import sensors
>>> sensors.bmi270.motion()
(0.0412, -0.0231, 9.7810, 0.44, -0.12, 0.08) # วางนิ่งบนโต๊ะ
>>> sensors.bmi270.motion()
(1.9044, 0.8830, 8.2210, 45.20, -18.5, 12.3) # ขณะเขย่า
gx,gy,gz) พุ่งขึ้นเพราะมีการหมุนลองสองสามครั้งใน REPL ก่อนรัน logger — จะได้ feel ว่าค่านิ่งกับค่าเขย่าต่างกันแค่ไหน นี่คือ "การทำความรู้จักข้อมูล" ที่วิศวกรทำก่อนเก็บจริงเสมอ