จบชุดบทเรียนนี้เราจะเดินครบ แล้วปิดท้ายด้วยการรันสเปกตรัมสด:
FS/N คุมความละเอียด, FS/2 คือเพดานความถี่ปลายทางของวันนี้: เขย่าบอร์ด แล้วแท่งสเปกตรัมขึ้น พร้อมความถี่เด่น (peak) ที่เปลี่ยนตามจังหวะที่เขย่าเร็ว-ช้า
วันนี้เราเน้น "เห็นสเปกตรัม + อ่าน peak เป็น" — ส่วน mel-spectrogram ที่โมเดลเสียงใช้จริง เก็บไว้ต่อยอดในบทเรียน 4.5–4.6
เราอยู่ Pillar 3 (Analysis) — ขั้นที่เปลี่ยนสัญญาณดิบให้กลายเป็น "สิ่งที่โมเดลเห็นจริง" บทเรียน 4.1–4.2 เราทำ filter ในโดเมนเวลา วันนี้เปิดมุมมองใหม่: โดเมนความถี่
ทำไม Analysis ถึงสำคัญกับ Edge AI: โมเดลไม่ได้ฉลาดกว่าสิ่งที่มันเห็น ถ้า feature ที่ป้อนเข้าไปดี โมเดลเล็ก ๆ ก็แม่นได้ FFT คือหนึ่งในเครื่องมือหลักที่ทำให้ feature เสียง/การสั่นสะเทือน "อ่านง่าย" สำหรับโมเดล
สัญญาณคลื่นเดียวกัน มองได้สองแบบ ทั้งสองถูกต้อง แต่บอกคนละเรื่อง:
ลองนึกถึงเสียงเปียโน: โดเมนเวลาคือ "คลื่นความดันอากาศที่สั่น" ส่วนโดเมนความถี่คือ "โน้ตอะไรถูกกด" — หูเราชอบคิดแบบหลัง และโมเดลเสียงก็เช่นกัน
คลื่นเสียงดิบเปลี่ยนเร็วมากและ "หน้าตา" ต่างกันทุกครั้งแม้จะเป็นเสียงเดียวกัน แต่ สเปกตรัม ของมันคงรูปกว่ามาก
นี่คือเหตุผลที่ front-end ของโมเดลเสียงเกือบทุกตัว เริ่มด้วย FFT (แล้วต่อด้วย mel/log ในบทเรียน 4.5–4.6) — เราไม่ได้ส่งคลื่นดิบเข้าโมเดลตรง ๆ เราส่ง "ภาพความถี่" ของมัน
หัวใจของ Fourier: สัญญาณไหน ๆ ก็เขียนเป็นผลบวกของคลื่นไซน์/โคไซน์ที่ความถี่ต่าง ๆ ได้ FFT แค่ถามกลับว่า "ในสัญญาณนี้ มีไซน์ความถี่ไหนบ้าง แต่ละอันแรงแค่ไหน"
พูดง่าย ๆ: FFT คือ "เครื่องแยกส่วนผสม" ของสัญญาณ ป้อนคลื่นที่ดูยุ่ง ๆ เข้าไป มันบอกกลับว่า "จริง ๆ แล้วเธอประกอบจากไซน์ความถี่พวกนี้ ในสัดส่วนนี้"
▸ ลองเล่นสด (GeoGebra): เปิด Interactive Math Lab — ลากจุด/เลื่อนสไลเดอร์ดูสมการขยับตาม
คลื่นผสมทางซ้ายถูกแยกเป็นแท่งความถี่ทางขวา — แท่งที่เด่นคือความถี่หลักที่ซ่อนอยู่
FFT (Fast Fourier Transform) รับสัญญาณ N จุดในโดเมนเวลา แล้วคืน N ค่าเชิงซ้อนในโดเมนความถี่ — หนึ่งค่าต่อหนึ่ง "bin" ความถี่
N log N แทนที่จะเป็น N² แบบคำนวณตรง ๆ (นี่คือที่มาของคำว่า "Fast")re[k] + i·im[k] เดี๋ยวเราจะแปลงเป็น "ขนาด" ให้อ่านง่ายในไฟล์ฝึก ฟังก์ชัน
fft()เขียนไว้ให้ครบแล้ว (เหมือนmodels()ในบทเรียน 1.1–1.3 ที่ให้มา) เราไม่ต้องแก้ตัว FFT แต่ควรอ่านให้เห็นว่ามันเป็นแค่การบวก-คูณเป็นระเบียบ ไม่ใช่เวทมนตร์
FFT เป็นแค่วิธีคำนวณ เร็ว ของสูตรตั้งต้นที่ชื่อ DFT (Discrete Fourier Transform) หน้าตาแบบนี้:
อ่านทีละสัญลักษณ์แบบภาษาคน — ไม่ต้องจำ แค่รู้ว่าตัวไหนคืออะไร:
az ที่เราเก็บใส่ buf)re[k] + i·im[k] บอกว่า "ความถี่ของ bin มีอยู่แรงแค่ไหน"ทำไมสำคัญกับชุดบทเรียนนี้: สูตรนี้คือสิ่งที่ฟังก์ชัน
fft()ในไฟล์ฝึกทำให้เราแบบเร็ว ๆ เราไม่ต้องคำนวณเอง แต่พอเห็นว่ามันคือ "คูณคลื่นอ้างอิงแล้วบวก" ก็จะเลิกกลัวว่ามันเป็นกล่องดำ — มันคือเลขคณิตเป็นระเบียบเท่านั้น
X[k] ให้ค่ามาเป็น "หมายเลข bin" () ยังไม่ใช่ Hz ที่คนอ่านออก สูตรแปลงกลับสั้นนิดเดียว:
| bin | หมายความว่า | |
|---|---|---|
| 0 | Hz | DC / แรงโน้มถ่วง (ตัวที่เราตัดทิ้ง) |
| 1 | Hz | เขย่าช้ามาก ~1-2 ครั้ง/วินาที |
| 3 | Hz | เขย่าปานกลาง |
| 4 | Hz | ตรงกับไซน์ทดสอบในสไลด์ตรวจ FFT |
| Hz | เพดาน Nyquist () อ่านเกินนี้ไม่ได้ |
ทำไมสำคัญกับชุดบทเรียนนี้: บรรทัด
peak_hz = kmax * FS / Nในโค้ดของเรา ก็คือสูตรนี้เป๊ะ ๆ พอเขย่าเร็วขึ้น พลังงานย้ายไป bin ที่ สูงขึ้น สูตรก็คืน Hz ที่สูงขึ้นตาม — นี่คือเหตุผลที่ "เขย่า 3 ครั้ง/วินาที แล้วได้ peak ~3 Hz"
output ของ FFT เป็น "ช่อง" (bin) เรียงตามความถี่ แต่ละ bin แทนช่วงความถี่แคบ ๆ สองสูตรที่ต้องจำ:
FS/N = ความละเอียดความถี่ ยิ่ง N มาก bin ยิ่งแคบ แยกสองความถี่ที่ใกล้กันได้ดีขึ้น (แต่เก็บ N จุดใช้เวลานานขึ้น)FS/2 = ความถี่สูงสุดที่อ่านได้ถูก ที่ FS=50 Hz เราเห็นได้ถึง 25 Hz เท่านั้นf = k * FS / N (เช่น N=32, FS=50 → bin 4 = 6.25 Hz)ถ้าสัญญาณจริงมีความถี่ เกิน Nyquist มันจะ "พับ" กลับมาโผล่ผิดที่ (aliasing) — นี่คือเหตุผลที่อัตราสุ่มต้องสูงพอเสมอ เราจะเจอเรื่องนี้อีกตอนทำ feature เสียง
N (จำนวนจุดต่อหน้าต่าง) เป็นปุ่มปรับที่แลกกันสองด้าน ไม่มีค่าที่ "ดีที่สุด" ตายตัว ขึ้นกับงาน:
| N | bin width (FS/N) ที่ FS=50 |
เก็บ N จุดใช้เวลา | เหมาะกับ |
|---|---|---|---|
| 16 | 3.13 Hz (หยาบ) | ~0.32 s (ไว) | ดูคร่าว ๆ ตอบเร็ว |
| 32 | 1.56 Hz | ~0.64 s | ชุดบทเรียนนี้ (สมดุลดี) |
| 64 | 0.78 Hz (ละเอียด) | ~1.28 s (หน่วง) | แยกความถี่ใกล้กัน |
| 128 | 0.39 Hz (ละเอียดมาก) | ~2.56 s (ช้า) | วิเคราะห์นิ่ง ๆ |
นี่คือ การตัดสินใจเชิงวิศวกรรม ที่เจอตลอดในงาน DSP: "ละเอียดในความถี่" กับ "ไวในเวลา" แลกกันเสมอ (หลักความไม่แน่นอนของ time-frequency) เราเลือก N=32 เพราะพอดีกับการเขย่ามือ
accel ที่วางนิ่งอ่านได้ ~1g จากแรงโน้มถ่วง เป็นค่า คงที่ — ในโลกความถี่ ค่าคงที่คือความถี่ 0 Hz พอดี = bin 0 (DC)
mean = sum(buf)/N แล้วใช้ buf[i] - mean"ตัด DC" ที่จริงคือการเอา "ระดับพื้น" ที่ไม่สั่นออก เหลือแต่ส่วนที่แกว่ง — เพราะเราสนใจ "การเปลี่ยนแปลง" ไม่ใช่ "ค่าคงที่"
เราตัดสัญญาณเป็นท่อน ๆ ยาว N จุด ปัญหาคือขอบของท่อนมัก "ไม่พอดีคาบ" ทำให้พลังงานของความถี่หนึ่ง รั่ว ไปเปื้อน bin ข้างเคียง เรียก spectral leakage
re = [(buf[i]-mean) * (0.5 - 0.5*math.cos(2*math.pi*i/(N-1))) for i in range(N)]ราคาที่จ่าย: ยอด peak จะ "อ้วน" ขึ้นเล็กน้อย แลกกับการรั่วที่น้อยลงมาก ในงาน feature เสียงจริง Hann (หรือญาติ ๆ ของมัน) เป็นมาตรฐาน
output ของ FFT ต่อ bin เป็นเลขเชิงซ้อน re[k] + i·im[k] เราไม่สนเฟส สนแค่ "ความถี่นี้แรงแค่ไหน" = ขนาด (magnitude) ของเลขเชิงซ้อน
mag = [math.sqrt(re[k]*re[k] + im[k]*im[k]) for k in range(HALF)]HALF = N//2) เพราะครึ่งหลังเป็นภาพสะท้อนของสัญญาณจริง (สมมาตร) ไม่ให้ข้อมูลเพิ่ม
sqrt(re² + im²)คือทฤษฎีบทพีทาโกรัสตรง ๆ — re กับ im เป็นสองด้านของสามเหลี่ยม magnitude คือด้านตรงข้ามมุมฉาก นั่นคือ "ขนาด" ของความถี่นั้น
มีสเปกตรัมแล้ว คำถามที่คนอยากรู้ที่สุดคือ "ความถี่ไหนเด่นสุด" = bin ที่ magnitude สูงสุด (ข้าม bin 0 ที่เป็น DC ที่ยังหลงเหลือ)
kmax = max(range(1, HALF), key=lambda k: mag[k]) # bin เด่น (ข้าม bin 0)
peak_hz = kmax * FS / N # แปลง bin -> Hz
kmax ด้วย max(..., key=lambda k: mag[k])f = k * FS / N — เขย่าเร็ว kmax เลื่อนไปทางขวา (Hz สูง), เขย่าช้า kmax เลื่อนซ้าย (Hz ต่ำ)เริ่มจาก
range(1, HALF)ไม่ใช่range(0, ...)เพราะ bin 0 คือ DC ถ้ายังตัดไม่หมดเกลี้ยง มันอาจแอบชนะ เราเลยข้ามมันไปเลยเพื่อความชัวร์