บทเรียน 5.1 — วิศวกรรมชุดข้อมูล: สมดุลคลาส หน้าต่าง และการแบ่ง train/val/test

เก็บ · ติดป้าย · แบ่ง

โมดูล 5 — ฝึกโมเดลและนำไปใช้หลายเป้าหมาย

เปิด โมดูล 5 — Training (โต๊ะฝึกโมเดล)

คาถาประจำบทเรียน: "โมเดลจะแม่นแค่ไหน ตัดสินกันตั้งแต่ก่อนเขียนโค้ด train บรรทัดแรก — ที่ dataset ที่สมดุลและแบ่งถูก"

MicroPython บนบอร์ด BENTO เก็บข้อมูล → CSV → แบ่ง train/val/test บน PC

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เปิดบทเรียนด้วยของจริงก่อน

ยังใช้แนว กลับด้าน เหมือนเดิม — แต่รอบนี้เราจะรัน "ท่อ (pipeline) เตรียมข้อมูล" ที่ทำงานได้จริงก่อนด้วยข้อมูลปลอม แล้วค่อยแกะว่าอะไรทำให้ dataset "ดีพอจะ train" จริง

รันท่อจริงก่อน synthesize + split แกะว่าดีเพราะอะไร balance · split · leak เก็บของจริงเอง MPY → CSV บนบอร์ด อยากสร้างต่อ 5.3–5.5 train.py

นี่คือขั้น Investigate → Modify ของ PRIMM: เห็นท่อทั้งเส้นทำงานด้วยข้อมูล synthesize ก่อน (Run) แล้วเปิดฝาดูว่าอะไรทำให้ dataset ใช้ได้ (Investigate) จากนั้นเปลี่ยนข้อมูลปลอมเป็นข้อมูลจริงที่เราเก็บเอง (Modify)

ชุดบทเรียนก่อน ๆ เราเรียก "โมเดลที่ฝึกมาแล้ว" มาใช้ ชุดบทเรียนนี้เราเริ่มทำสิ่งที่วิศวกรทำก่อนการ train เสมอ — เตรียมข้อมูล ซึ่งเป็นงานที่กินเวลาจริงมากที่สุดในสาย ML

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เป้าหมายของชุดบทเรียนนี้

จบชุดบทเรียนนี้เราจะเดินครบ 4 เรื่อง แล้วปิดท้ายด้วย dataset ที่พร้อม train จริง:

  1. dataset คืออะไร — CSV ของ sample ที่ติดป้าย (label) แล้ว และเส้นทาง capture → label → window → split
  2. class balance — ทำไมจำนวน sample ของแต่ละคลาสควรใกล้เคียงกัน และเก็บให้สมดุลยังไง
  3. train / val / test split — ทำไมต้องแบ่ง 3 กอง แบ่งแบบ stratified และกฎ "ห้ามให้ข้อมูลรั่ว (no leakage)"
  4. MPY → CSV — เก็บข้อมูล IMU ที่ติดป้ายลงไฟล์บนบอร์ดด้วย sensors.bmi270.motion()
  5. ลงมือ: เติม s11_dataset.py เก็บ dataset ที่ สมดุล แล้วส่งต่อให้ dataset_tools.py แบ่งบน PC

ปลายทางของวันนี้: ไฟล์ gestures.csv ที่มี 3 คลาสสมดุล พร้อมแบ่ง train/val/test — dataset ที่ "train ได้จริง"

ชุดบทเรียนถัดไป (บทเรียน 5.3–5.5) เราจะเอา dataset นี้ไป train เป็นโมเดลจริงใน Docker — วันนี้คือการปูฐานให้โมเดลนั้นแม่น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ชุดบทเรียนนี้อยู่ตรงไหนของคอร์ส

บทเรียน 5.1–5.2 เป็นบทเรียน เปิด โมดูล 5 (Training) — เราหยุด "ใช้" โมเดลสำเร็จ แล้วเริ่ม "สร้าง" โมเดลของเราเอง โดยเริ่มจากสิ่งที่มาก่อนการ train เสมอ

โมดูล 2 · DAQ เก็บ sensor ลง CSV 4.5–4.6 · Analysis windowing / feature 5.1–5.2 (วันนี้) dataset engineering 5.3–5.5+ train → deploy
  • โมดูล 2 สอนวิธี "เก็บข้อมูล sensor ลง CSV" (s04_daq_logger.py) — เราจะต่อยอดจากตรงนั้น
  • บทเรียน 4.5–4.6 สอนว่าโมเดลเห็น "หน้าต่าง (window)" ไม่ใช่ sample เดี่ยว — แนวคิดนั้นกลับมาใช้ตอนแบ่งข้อมูล
  • บทเรียน 5.1–5.2 วันนี้ รวมสามอย่าง: เก็บให้สมดุล + ติดป้ายถูก + แบ่งเป็น train/val/test — แล้ว บทเรียน 5.3–5.5 จึงเอาไป train

"กลับด้าน" ยังทำงาน: เห็นท่อสำเร็จก่อน (synthetic) แล้วย้อนเข้าใจ — พอถึง บทเรียน 5.3–5.9 คุณจะ train โมเดลตัวเองบน dataset ที่เก็บวันนี้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ทบทวนเร็ว — จาก บทเรียน 2.1–2.2 DAQ logger

บทเรียน 2.1–2.2 เราเขียน s04_daq_logger.py ที่อ่าน IMU แล้วเขียนลง CSV — ชุดบทเรียนนี้ยืนบนโครงเดิมนั้นเป๊ะ แค่ยกระดับจาก "เก็บข้อมูล" เป็น "เก็บ dataset ที่ดีพอจะ train"

# หัวใจของ s04 — อ่าน sample แล้วเขียนลงไฟล์ (เราจะต่อยอดจากตรงนี้)
with open(PATH, "a") as f:
    for _ in range(BURST):
        ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
        f.write("%s,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n"
                % (label, ax, ay, az, gx, gy, gz))
        time.sleep_ms(RATE_MS)     # 20 ms = 50 Hz
  • บทเรียน 2.1–2.2 ตอบคำถาม "เก็บข้อมูลลงไฟล์ยังไง" — ชุดบทเรียนนี้ตอบ "เก็บยังไงให้ สมดุล + แบ่งได้ + ไม่รั่ว"
  • ความต่าง: บทเรียน 2.1–2.2 เก็บดะ · บทเรียน 5.1–5.2 เก็บโดยเฝ้าดู class balance ตลอด แล้วส่งต่อให้ split บน PC

ถ้ายังไม่แม่นเรื่องเขียน CSV บนบอร์ด เปิด s04_daq_logger.py อ่านทวนก่อน — ชุดบทเรียนนี้เพิ่มแค่ "สมองของการเตรียมข้อมูล" ทับลงไป

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

dataset คืออะไร

dataset ในงานของเราคือไฟล์ CSV ธรรมดา หนึ่งบรรทัดต่อหนึ่ง sample โดยมี ป้าย (label) บอกว่าตอนเก็บ sample นั้นเรากำลังทำท่าอะไร:

gestures.csv — หนึ่งบรรทัด = หนึ่ง sample ที่ติดป้ายแล้ว label,ax,ay,az,gx,gy,gz ← หัวตาราง (header) idle,0.006,-0.007,9.842,0.05,-0.27,0.18 circle,2.01,1.98,9.91,40.2,39.7,0.4 ป้ายคือ "คำตอบที่ถูก" ที่เราสอนโมเดล · 6 ช่องหลังคือค่าจาก IMU (accel 3 + gyro 3)
  • 6 คอลัมน์หลัง (ax,ay,az,gx,gy,gz) คือค่าจาก sensors.bmi270.motion() — เหมือนที่โมเดล Motion บนบอร์ดกินเป๊ะ
  • คอลัมน์ label คือ "เฉลย" ที่มนุษย์ติดให้ ตอน train โมเดลจะเรียนความสัมพันธ์ "ค่า 6 ช่องนี้ → ป้ายนี้"

เราจงใจใช้ 3 คลาสเดียวกับโมเดล Motion บนบอร์ด (idle / circle / shaking) เพื่อจะเทียบโมเดลที่เรา train เอง กับของสำเร็จได้ในชุดบทเรียนหลัง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ท่อเตรียมข้อมูล — 4 ขั้นก่อนถึง train

dataset ดิบยัง train ไม่ได้ทันที ต้องผ่าน 4 ขั้น กว่าจะกลายเป็นสิ่งที่โมเดลเรียนได้ — ชุดบทเรียนนี้เราทำครบทั้งเส้น:

1 · capture อ่าน IMU 50 Hz บนบอร์ด (MPY) 2 · label ติดป้ายแต่ละท่า idle/circle/shaking 3 · window ตัดเป็นหน้าต่าง 50 sample/หน้าต่าง 4 · split แบ่ง train/val/test บน PC train 5.3–5.5 (Docker) → .tflite บนบอร์ด (s11_dataset.py) บน PC (dataset_tools.py) ชุดบทเรียนนี้ทำครบขั้น 1–4 · ขั้น train เก็บไว้ 5.3–5.5

ขั้น 1–2 (capture + label) เกิดบนบอร์ดด้วย MicroPython · ขั้น 3–4 (window + split) เกิดบน PC ด้วย dataset_tools.py — ชุดบทเรียนนี้เราแตะทั้งสองฝั่ง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

รันของจริงก่อน — สร้าง dataset ปลอมแล้วส่อง

ก่อนเก็บข้อมูลจริง (ที่ใช้เวลานาน) เรารันท่อทั้งเส้นด้วย ข้อมูล synthesize ก่อน เพื่อพิสูจน์ว่าโค้ดทำงาน แล้วเห็นว่า dataset ที่ดี "หน้าตาเป็นยังไง":

# 1) สร้าง dataset ปลอม (idle=นิ่ง, circle=หมุน, shaking=สั่นแรง)
python dataset_tools.py --synthesize --out data/gestures.csv

# 2) ส่องว่าได้อะไร — จำนวน sample, จำนวนหน้าต่าง, จำนวนต่อคลาส
python dataset_tools.py --out data/gestures.csv
# samples (3600, 6) windows (143, 50, 6) class counts [48 48 47]
  • class counts [48 48 47] = จำนวนหน้าต่างของแต่ละคลาส ใกล้เคียงกัน → นี่คือ dataset ที่ สมดุล
  • windows (143, 50, 6) = 143 หน้าต่าง แต่ละหน้าต่างมี 50 sample × 6 ช่อง — นี่คือรูปร่างที่โมเดลกิน

เราให้ synthesize ไว้ตั้งแต่แรก เพื่อให้ท่อ "รันได้" ก่อนมีข้อมูลจริง — เป็นเชื้อเพลิงของการกลับด้าน: เห็นของสำเร็จก่อน แล้วค่อยเอาข้อมูลจริงมาแทน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

class balance — ทำไมความสมดุลถึงสำคัญ

ถ้าคลาสหนึ่งมี sample เยอะกว่าเพื่อนมาก โมเดลจะ "ขี้เกียจ" — มันเดาคลาสที่เจอบ่อยไว้ก่อนก็ถูกบ่อยแล้ว โดยไม่ต้องเรียนรู้จริง

ไม่สมดุล (แย่) idle circle shaking โมเดลเดา "circle" ไว้ก่อน สมดุล (ดี) idle circle shaking ต้องเรียน จริงทุกคลาส
  • ตัวอย่างสุดโต่ง: ถ้า 95% ของข้อมูลเป็น idle โมเดลที่ตอบ "idle" ตลอดจะแม่น 95% บนกระดาษ แต่ใช้งานจริงไม่ได้เลย
  • ทางแก้ที่ตรงที่สุด: เก็บให้แต่ละคลาสมีจำนวนใกล้เคียงกัน ตั้งแต่ตอนเก็บข้อมูล — นี่คือสิ่งที่ s11_dataset.py เฝ้าดูให้

ชุดบทเรียนนี้เราจึงไม่เก็บข้อมูลแบบ "ดะ" แต่เก็บพร้อม แถบสมดุลต่อคลาส บนจอ ถ้าคลาสไหนน้อย โปรแกรมจะบอกให้เก็บเพิ่ม — เก็บอย่างมีสติ ไม่ใช่เก็บเยอะ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

windowing — โมเดลเห็น "หน้าต่าง" ไม่ใช่ sample เดี่ยว

sample เดียว (ค่า IMU ณ วินาทีหนึ่ง) บอกท่าไม่ได้ — shaking กับ idle ที่จังหวะหนึ่งอาจมีค่าเท่ากันบังเอิญ โมเดลต้องเห็น ช่วงเวลา ถึงจะแยกออก (นี่คือแนวคิดจาก บทเรียน 4.5–4.6 ที่กลับมาใช้)

สาย sample ต่อเนื่อง (50 Hz) หน้าต่าง 1 (WIN=50) หน้าต่าง 2 (เลื่อน HOP=25) หน้าต่าง 3 แต่ละหน้าต่าง = 50 sample × 6 ช่อง · เลื่อนทีละ 25 (ซ้อน 50%) · ป้ายของหน้าต่าง = ป้ายส่วนใหญ่ในนั้น
  • WIN = 50 (1 วินาทีที่ 50 Hz) คือ "ความยาวหนึ่งท่า" ที่โมเดลตัดสิน · HOP = 25 คือเลื่อนหน้าต่างทีละครึ่ง (ได้ข้อมูลมากขึ้นจากสายเดียว)
  • make_windows() ใน dataset_tools.py ทำขั้นนี้ให้ — และมันคือ การจัดหน้าต่างแบบเดียวกับที่ feed บนบอร์ดทำ ตอนอนุมานจริง

ทำไมต้องเหมือนกันเป๊ะ? เพราะโมเดลที่ฝึกด้วยหน้าต่าง 50 sample จะทำงานถูกก็ต่อเมื่อตอนใช้จริงมันก็เห็นหน้าต่าง 50 sample เช่นกัน — "train เห็นแบบไหน ใช้จริงต้องเห็นแบบนั้น"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

train / val / test — ทำไมต้องแบ่ง 3 กอง

เราไม่เอาข้อมูลทั้งหมดไป train แล้ววัดความแม่นบนข้อมูลเดิม เพราะนั่นเหมือนให้ผู้เรียนดูเฉลยก่อนสอบ — ต้องกันข้อมูลไว้ "สอบจริง" ต่างหาก

train (70%) — โมเดลเรียนจากกองนี้ val 15% test 15% ใช้ปรับโมเดล จูนระหว่าง train สอบครั้งเดียว test = แตะครั้งเดียวตอนจบ · ถ้าแอบดู test ระหว่างจูน ตัวเลขจะโกหก
  • train — โมเดลเรียนจากกองนี้ · val — ใช้เช็กระหว่างฝึกว่าเริ่ม overfit หรือยัง (จูนได้) · test — สอบจริง แตะครั้งเดียวตอนจบ
  • split() ใน dataset_tools.py ทำให้ครบ 3 กอง ด้วยสัดส่วน val=0.15, test=0.15 (ที่เหลือ 70% เป็น train)

ทำไมต้องมี val แยกจาก test? เพราะเราจูนโมเดลโดยดู val ซ้ำๆ — val จึง "ปนเปื้อน" การตัดสินใจของเราไปแล้ว test ที่ไม่เคยแตะเลยเท่านั้นถึงบอกความแม่นจริงได้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

stratified split — สัดส่วนคลาสเท่ากันทุกกอง

ถ้าแบ่งแบบสุ่มดื้อๆ อาจซวยได้ว่า test ดันไม่มี shaking เลย — วัดความแม่นของ shaking ไม่ได้ ทางแก้คือแบ่งแบบ stratified: แยกทีละคลาสแล้วค่อยหั่น

# หัวใจของ split() — แยก index ของแต่ละคลาสก่อน แล้วหั่น 70/15/15 ในแต่ละคลาส
idx = {c: rng.permutation(np.where(y == c)[0]) for c in np.unique(y)}
for c, ii in idx.items():
    n = len(ii)
    nte, nva = int(n * test), int(n * val)
    te += list(ii[:nte])            # 15% แรกของคลาสนี้ → test
    va += list(ii[nte:nte + nva])   # 15% ถัดไป → val
    tr += list(ii[nte + nva:])      # ที่เหลือ → train
  • ผลคือทั้ง 3 กองมีสัดส่วน idle : circle : shaking เท่ากัน — ไม่มีคลาสไหนหายจากกองใด
  • rng.permutation สลับก่อนหั่น เพื่อไม่ให้ลำดับการเก็บข้อมูล (เช่น เก็บ idle ทั้งหมดก่อน) มาทำให้กองเอนเอียง

"stratified" แปลว่า "แบ่งเป็นชั้นตามคลาส" — เป็นค่าเริ่มต้นที่ควรใช้เสมอในงาน classification ที่คลาสมีความสำคัญเท่ากัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตเบื้องหลัง — สมดุลคลาส กับสัดส่วน 70/15/15

สามแนวคิดที่เพิ่งดูไป เขียนเป็นสูตรสั้นๆ ได้ ช่วยให้เห็นว่า "สมดุล" กับ "แบ่ง" วัดกันด้วยตัวเลขจริง

สัดส่วนของแต่ละคลาส (class balance)

pc  =  ncN,สมดุลเมื่อpc  ≈  1Kp_c \;=\; \frac{n_c}{N}, \qquad \text{สมดุลเมื่อ}\quad p_c \;\approx\; \frac{1}{K}

  • ncn_c = จำนวน sample (หรือหน้าต่าง) ของคลาส cc · N=∑cncN=\sum_c n_c = จำนวนทั้งหมด
  • KK = จำนวนคลาส — ชุดบทเรียนนี้ K=3K=3 (idle / circle / shaking)
  • pcp_c = สัดส่วนของคลาส cc · สมดุลคือทุกคลาสได้ราว 1/3≈0.331/3 \approx 0.33 เท่าๆ กัน

แบ่งเป็น 3 กอง (train / val / test)

Ntest=⌊0.15 N⌋,Nval=⌊0.15 N⌋,Ntrain=N−Ntest−Nval  ≈0.70 NN_{\text{test}}=\lfloor 0.15\,N\rfloor,\quad N_{\text{val}}=\lfloor 0.15\,N\rfloor,\quad N_{\text{train}}=N-N_{\text{test}}-N_{\text{val}}\;\approx 0.70\,N

  • ⌊ ⋅ ⌋\lfloor\,\cdot\,\rfloor = ปัดลงเป็นจำนวนเต็ม (แบ่ง sample เป็นเศษไม่ได้) · NtrainN_{\text{train}} รับเศษที่เหลือ เลยรวมกันได้ครบ NN พอดี
  • ทำไมสำคัญ: ถ้า pcp_c เบ้ไปคลาสเดียว โมเดลเดาคลาสนั้นก็ "แม่น" บนกระดาษโดยไม่ได้เรียนอะไร — ตรงกับสไลด์ class balance ที่เราเพิ่งดู

อยากได้ Ntrain:Nval:Ntest=70:15:15N_{\text{train}}:N_{\text{val}}:N_{\text{test}} = 70:15:15 ก็คูณ NN ด้วย 0.15 แล้วปัดลงให้ test และ val ที่เหลือเป็น train — นี่คือสิ่งที่ split() ทำให้ในบรรทัด int(n*test), int(n*val)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตเบื้องหลัง — stratified: แบ่งทีละคลาสให้สัดส่วนคงเดิม

การแบ่งแบบ stratified ไม่ได้หั่นทั้งกองรวด แต่หั่น ทีละคลาส ด้วยสัดส่วนเดียวกัน แล้วเอามารวม

nc,test=⌊0.15 nc⌋,nc,val=⌊0.15 nc⌋,nc,train=nc−nc,test−nc,valn_{c,\text{test}}=\lfloor 0.15\,n_c\rfloor,\quad n_{c,\text{val}}=\lfloor 0.15\,n_c\rfloor,\quad n_{c,\text{train}}=n_c-n_{c,\text{test}}-n_{c,\text{val}}

  • nc,trainn_{c,\text{train}} = จำนวนของคลาส cc ที่ตกไปกอง train (val/test อ่านทำนองเดียวกัน)
  • ทำแบบนี้กับ ทุกคลาส cc แล้วต่อกัน — คือความหมายของโค้ด for c in np.unique(y) ในฟังก์ชัน split()

สิ่งที่รับประกัน (invariant): สัดส่วนของทุกคลาสในทุกกองเท่ากับสัดส่วนเดิม pcp_c

nc,trainNtrain  ≈  nc,valNval  ≈  nc,testNtest  ≈  pc\frac{n_{c,\text{train}}}{N_{\text{train}}}\;\approx\;\frac{n_{c,\text{val}}}{N_{\text{val}}}\;\approx\;\frac{n_{c,\text{test}}}{N_{\text{test}}}\;\approx\;p_c

  • ทำไมสำคัญ: สูตรนี้บอกว่า ไม่มีกองไหนขาดคลาส — ถ้าเก็บ shaking มา nshaking=20n_{\text{shaking}}=20 หน้าต่าง test ก็ยังได้ราว ⌊0.15×20⌋=3\lfloor 0.15\times 20\rfloor=3 หน้าต่าง ไม่หลุดเป็น 0 เหมือนสุ่มดื้อๆ
  • ตรงกับ np.bincount ที่เราสั่งพิมพ์ตอนตรวจ dataset — ตัวเลขทั้ง 3 กองจะสะท้อน pcp_c เดียวกัน

นี่คือเหตุผลที่ "stratified" เป็นค่าเริ่มต้นที่ควรใช้เสมอ — คณิตข้างบนพิสูจน์ว่ามันกันเหตุ "test ไม่มี shaking" ได้ตั้งแต่ก่อน train

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

no leakage — กฎเหล็กของการเตรียมข้อมูล

ข้อผิดพลาดที่ทำให้ตัวเลขความแม่น "สวยหลอกๆ" คือ ข้อมูลรั่ว (leakage) — ปล่อยให้สถิติของ val/test แอบเข้าไปมีอิทธิพลตอน train จุดที่พลาดบ่อยสุดคือตอน normalize

# ถูก: คิด mean/std จาก TRAIN เท่านั้น แล้วเอาไปใช้กับ val/test
def normalize(X_train, *others):
    mean = X_train.reshape(-1, X_train.shape[-1]).mean(0)
    std  = X_train.reshape(-1, X_train.shape[-1]).std(0) + 1e-6
    norm = lambda A: (A - mean) / std
    return (norm(X_train), *[norm(o) for o in others]), (mean, std)
  • ถ้าคิด mean/std จากข้อมูล ทั้งหมด (รวม test) = test รั่วเข้า train แล้ว ความแม่นที่วัดได้จะดีเกินจริง
  • กฎ: fit บน train · apply ทุกกอง — สถิติของ test ต้องไม่มีวันถูกโมเดลเห็นก่อนสอบ

leakage เป็นบั๊กที่ไม่ crash — โปรแกรมรันผ่าน ตัวเลขออกมาสวย แต่พอ deploy จริงกลับพัง นี่คือเหตุผลที่วิศวกร ML ระวังเรื่องนี้เป็นพิเศษ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

dataset_tools.py — เครื่องมือฝั่ง PC

ฝั่ง PC เรามีไฟล์เดียว dataset_tools.py (ให้ไว้แล้ว) ที่รวม 5 ฟังก์ชันของการเตรียมข้อมูล — ชุดบทเรียนนี้เราเรียกใช้มัน ไม่ต้องเขียนเอง แต่ต้องอ่านให้เข้าใจ:

ฟังก์ชัน ทำอะไร
load_csv(path) อ่าน CSV ของบอร์ด → (samples[N,6], labels[N])
make_windows(s, l) ตัดสายเป็นหน้าต่างซ้อนกัน → (X[W,50,6], y[W])
normalize(X_train, ...) มาตรฐานต่อช่อง (fit บน train เท่านั้น)
split(X, y) แบ่ง train/val/test แบบ stratified
synthesize(path) สร้างข้อมูลปลอมให้ท่อรันได้ก่อนมีข้อมูลจริง
  • ค่าคงที่สำคัญอยู่หัวไฟล์: CLASSES = ["idle","circle","shaking"], WIN = 50, HOP = 25 — ตรงกับที่บอร์ดเก็บ
  • ทั้ง 5 ฟังก์ชันคือ "สมองการเตรียมข้อมูล" ที่ชุดบทเรียนนี้สอน — โค้ดฝั่งบอร์ดแค่ป้อน CSV ที่ถูกฟอร์แมตให้มัน

เราให้ dataset_tools.py มาแล้ว เพราะโฟกัสของ คุณ ชุดบทเรียนนี้คือ "เก็บ CSV ให้ดี" (ฝั่งบอร์ด) ส่วนการ split เป็นตรรกะ PC ที่อ่านเข้าใจแล้วเรียกใช้ได้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0