การพัฒนา model แยกโน๊ตดนตรีในเพลง โดย Pytorch
เป็นโปรเจคการพัฒนา AI ครั้งแรกในชีวิตกับโครงการ “AI Builders” ในช่วงปิดเทอมม.5ขึ้นม.6 ถือเป็นโปรเจคที่ท้าทายมากๆอันนึงเลย
[Github : https://github.com/neennera/AI_pitch]
ที่มาของปัญหา
จริงๆแล้วจุดเริ่มต้นที่จะสร้างโมเดลนี้มาจากความคิดง่ายๆอย่าง “ถ้ามี AI ที่พอฟังเราฮัมเพลงแล้วทายโน๊ตถูกเลยคงดีนะ”
ในช่วงนั้นเราได้รู้จักคำว่า “Perfect Pitch” ซึ่งเป็นคำเรียกกลุ่มคนที่ฟังเพลงแล้วรู้โน๊ตในทันที สามารถแยกแยะเสียงโน๊ตได้ตั้งแต่เกิดหรือฝึกเอา จาก Ear training บ่อยๆ ประจวบกับโครงการ AI Builders ประกาศพอดี เลยถือโอกาสมาลองฝึก “Perfect Pitch” กับ Model ดู
Dataset
ทีแรกตั้งใจว่าจะเริ่มฝึกโดย Single note จากไฟล์ MIDI ก่อน เนื่องจากมีขนาดเล็กและโมเดลน่าจะฝึกได้ง่ายกว่าเมื่อมีโน๊ตเล่นแค่โน๊ตเดียวในช่วงเวลาหนึ่ง แต่ dataset ที่หามานั้นไม่สามารถใช้ได้โดยไม่ต้องเสียเงิน
หลังจากการค้นหาราวๆสามสัปดาห์ จึงยอมมาหยุดที่ MusicNet [Link : https://zenodo.org/record/5120004#.YrHWQLlBxQI]
Data นี้เป็นข้อมูลขนาดใหญ่ 11.72 GiB ของเพลงคลาสสิค รวม 330 ไฟล์ รูปแบบ data เป็น wave files (.wav) มีการ label ในส่วนของโน๊ตมาให้ใน .csv
ในการเตรียมข้อมูลสำหรับ Model ด้วยความที่เราจะเทรนเป็น Supervised Learning จึงได้แยก feature ของเสียงผ่าน Mel Filterbank และดึง start_time,end_time มาจากไฟล์ .csv สร้างเป็น data ขนาด [1500,120] สำหรับ feature และ [1500,88] สำหรับ label
โดย label เป็น array ขนาด [1500,120] ระหว่าง time/channel โดยในเวลาใดๆจะมีค่า feature ของไฟล์เสียง 120 channel ที่ได้มาจาก Mel Filterbank ส่วน label เป็น array ขนาด [1500,88] ระหว่าง time/channel ที่ประกอบด้วยค่า 0/1 ซึ่ง 0 แทนโน๊ตที่ไม่ได้เล่น และ 1 แทนโน๊ตที่เล่น
และด้วยความที่ data มีขนาดใหญ่ จึงต้องสร้าง Dataloader ขึ้นมาเองเพื่อนำ data เข้าสู่โมเดล ในส่วนนี้ได้ลองไปเล่นกับการเรียกและจัดการไฟล์ในเครื่อง ทำให้เข้าใจระบบไฟล์และ การใช้งาน libra OS มากขึ้น หนึ่งในสิ่งที่ท้าทายคือการทำความเข้าใจการใช้งาน self function ซึ่งเราพึ่งเคยเจอการเขียนโปรแกรมในรูปแบบนี้ครั้งแรก ทำให้ได้ลองแกะอะไรใหม่ๆเยอะเลย
Model
ในส่วนของโมเดล ได้มีการใช้ PyTorch เข้ามาช่วยเทรน ใช้ Optimiser : Adam และ Activation : Sigmoid
ในตัวของ loss เราได้ลองเลือกใช้ loss 3 ประเภท คือ
- Cross entropy loss : เป็นการวัดความคลาดเคลื่อนของข้อมูลที่ทายและข้อมูลจริง ซึ่งเหมาะกับ ข้อมูล แบบ One Hot Encoding ที่เรา label เป็น 0/1 แต่เรานำมาใช้แค่ในช่วงแรกก่อนเปลี่ยนไปเป็น MSE แทน เนื่องจากโมเดลทายค่าออกมาแค่ค่า 0
- MSE loss : เป็น loss ที่วัดจากระยะห่างระหว่างของข้อมูลที่ทายและข้อมูลจริง ซึ่งเป็นฟังก์ชั่นที่ใช้งานง่าย แต่อาจจะไม่เหมาะกับโมเดล Classification นัก ซึ่งการใช้ loss นี้ทำให้ค่า loss คงที่ไว แต่โมเดลยังทำนายแบบสุ่มอยู่ จึงเปลี่ยนไปใช้ BCE loss
- BCE loss : เป็นการวัด loss ที่มีการบาลานซ์ความสดดุลของข้อมูล ซึ่งเหมาะกับโมเดลของเราที่มีจำนวน label 0 มากกว่า 1 เป็นจำนวนมาก loss นี้เป็น loss สุดท้ายที่ได้ลองใช้ แต่ด้วยเวลาที่จำกัดจึงไม่ได้ลองเทรนจนผลออกมาชัดเจน
ทางด้านฝั่ง accuracy เราได้ทำการวัดโดยการปัดค่าที่โมเดลทำนายออกมาโดย ข้อมูลที่ <0.5 ให้เป็น 0 และข้อมูลที่ ≥0.5 ให้เป็น 1 ก่อนจะนำไปเทียบกับ ค่าจริง และนับจำนวนตำแหน่งที่ทายถูกออกมา
ในขั้นตอนการเทรน เรามีการปรับเปลี่ยน layer อยู่หลายครั้ง โดยในตอนแรกสุดจะเป็น Linear -> ReLU -> Linear ที่ได้ผล accuracy ออกมาค่อนข้างเหวี่ยง เนื่องจาก model เลือกทายเป็น 0 ตลอด
จึงมีการเปลี่ยน Conv1d -> Conv1d -> Linear โดยเลือกใช้ convo 1D มาใช้บน channel เสียงทั้ง 120 ช่อง รวมทั้งสร้าง baseline ของ loss ระหว่าง array ที่เป็น 0 ทั้งหมด มาเทียบกับค่าที่ predict ออกมา
หรือแม้แต่การเปลี่ยน layer ให้ซับซ้อนขึ้น เช่น Conv1d -> RELU -> Linear(บนแกน time) -> LSTM(บนแกน feature) หรือ Convo1d -> LSTM(time) -> Linear(time) -> LSTM(feature) ที่พบว่า loss ของ model มีค่ามากกว่า baseline loss
และการเพิ่มขนาด kernel size ให้มากขึ้น เช่น Conv1d(kernel = 6) -> ReLU -> Conv1d(kernel = 8) -> LSTM
ก็ยังพบปัญหาที่โมเดลทายค่าออกมาแบบ fix เสมอ
ซึ่งระหว่างการเทรนได้พบปัญหาหลายด้าน เช่น
- ขนาดของข้อมูล : มีขนาดค่อนข้างใหญ่ ใช้พื้นที่/เวลาประมวลผลนาน
- จำนวน layer ที่ใช้ train model : ใช้เวลานาน จนบางครั้งก็หลุดไปก่อน save
- โมเดลเมื่อเทรนไปซักพักมันจะทายออกมาค่าใดค่าหนึ่งเสมอ แม้ข้อมูล input จะแตกต่างกัน
Lesson learn
ในช่วง 8 สัปดาห์ที่ได้เข้ามาศึกษาเราก็ได้เรียนรู้อะไรมากมาย เช่น…
1.ได้เห็นการทำงานของ training loop แบบเต็มๆ
ได้รู้จักคำศัพท์ใหม่ๆใน field ของ machine learning เช่น Spectrogram ซึ่งเป็นแผนภาพแสดงความถี่ของคลื่นเสียงเทียบกับเวลา , Feed Forward คือ การป้อนข้อมูลให้โมเดลทำนายออกมา ฯลฯ รวมทั้งได้รู้จักกับการทำงานของ library ที่หลากหลาย ซึ่งหากเข้าใจก็จะช่วยให้งานง่ายขึ้น เช่น torch, OS หรือแม้แต่ pickle
2.ได้ลองทำ Dataloader มาส่งไฟล์เข้า model
ได้ลองใช้ python มาจัดการไฟล์ในเครื่อง ได้เรียนรู้ประเภทของข้อมูลและการจัดการมากขึ้น รวมทั้งได้ฝึกการใช้ OOP ที่เป็นตัวแปรที่สามารถประกาศฟังก์ชันภายในตนเองได้ ซึ่งการใช้ Dataloader ทำให้เราได้เห็นความสำคัญของการดีไซน์ flow การทำงานที่จะทำให้การเทรนลดเวลาไปได้มากหากเราเข้าใจการนำเข้าข้อมูลที่มีประสิทธิภาพ
3.ความสำคัญของการเลือก Loss Function
พบว่า data แต่ละประเภทมี loss function ที่เหมาะสมแตกต่างกัน ซึ่งในการเทรนโมเดลรอบนี้เรายังมีข้อเสียที่ไม่สามารถ define loss function ที่เหมาะสมได้ในตอนต้น อาจเป็นสาเหตุที่ผลการเทรนออกมาไม่ดีนัก
4.พื้นฐานและการ research
เราคิดว่าส่วนหนึ่งที่เราไม่สามารถมองเห็นปัญหาและแก้ไขโมเดลได้เพราะเรายังมีความรู้ในสาขานี้น้อยไปหน่อย ซึ่งการ research รอบนี้เราได้มีการอ่านอย่างจริงจังแค่เปเปอร์ “INVARIANCES AND DATA AUGMENTATION FOR SUPERVISED MUSIC TRANSCRIPTION” ของ John Thickstun ที่ศึกษาการใช้ neutral network มาแยกเสียงโน๊ตของ MusicNet แต่ด้วยความที่เป็นการพัฒนาโมเดลบน tensenflow 1 จึงนำมาปรับใช้ตามได้ยาก หากมีเวลามากกว่านี้ในภายภาคหน้า ก็คิดว่าคงจะต้องลงมือดูพื้นฐานและอ่านงานวิจัยที่เกี่ยวข้องให้เห็นภาพรวมของ field มากกว่านี้
5.การอ่านเอกสารคู่มือของ Pytorch
ได้ฝึกอ่านคู่มือการใช้งานและเห็นความสำคัญของการเข้าใจข้อมูลและเครื่องมือที่ใช้ เช่าการตั้งค่าข้อมูลที่จะนำเข้า nn.conv1d ให้มีแกน feature และ แกน time ตรงตามข้อกำหนด
future works
แม้ในตอนนี้จะไม่ได้สามารถสร้างโมเดลอย่างที่หวังไว้ได้ แต่ยังมีบางส่วนที่สามารถต่อเติมจากโมเดล ดังนี้
- ใช้จำนวน layers ที่มากขึ้น อาจเพิ่ม conv1d ที่มี feat มากกว่านี้
- ลองศึกษา papers ที่ทำ note classification และนำมาปรับใช้
- เปลี่ยนไปเทรนบน Collab หรือ AWS เพื่อให้ใช้เวลาเทรนที่น้อยลง