สร้างสรรบทเพลง undertale ผ่าน LSTM และ Teacher forcing

34 noppawit Tantisiriwat
5 min readJun 22, 2022

ในวัยเด็กผมเคยมีประสบการณ์เล่มเกมที่มีชื่อว่า undertale มันเป็นเกมที่มีเนื้อเรื่องแปลกใหม่ ระบบการเล่นที่สนุก และที่สำคัญที่สุด มันมีเพลงประกอบตัวเกมที่ไพเราะเกินบรรยาย

เพลงประกอบเกมนี้ได้กวาดรางวัลมาแล้วมากมาย ขายเป็นแผ่นเสียงได้เป็นกอบเป็นกำ และได้ถูกจัดแสดงบนเวทีระดับโลกมาแล้ว

ตัวผมจึงอยากลองค้นหาความมหัศจรรย์ของบทเพลงเหล่านี้

ทว่าผมขาดความรู้และพรสวรรค์ทางด้านดนตรีจึงไม่สามารถเข้าใจความสัมพันธ์ของบทเพลงเหล่านี้ได้

ผมจึงลองศึกษาศาสตร์AI ในการวิเคราะห์ความสัมพันธ์เหล่านี้ดู

NLP ( Natural Language Processing) คือกระบวนการที่ทำให้โมเดลของเราเข้าใจกระบวนการทำงานของภาษาของมนุษย์ได้ ซึ่งปกติแล้วเราจะใช้มันในการวิเคราะห์โครงสร้างของภาษาต่างๆ แต่เราสามารถใช้มันกับการเขียนทำนองเพลงได้หรือไม่

คำตอบคือได้ …

ภาษาและดนตรีนั้นต่างกันเพียงเส้นบางๆ ถ้าเราเปรียบเทียบตัวโน็ตเป็นตัวอักษร ท่อนๆหนึ่งเป็นประโยค เพลงทั้งเพลงเป็นบทความหนึ่งบท แค่นี้โมเดลของเราก็จะเข้าใจตัวโน๊ตผ่าน NLPได้

ในบทความนี้ ผมลองโมเดลจากNLP มาสร้างสรรค์บทเพลงดู …

ผมได้ลองหารูปแบบของไฟล์เสียงดนตรีหลายแบบ แต่ก็ได้มาหยุดที่ ไฟล์ประเภท midi นี้ เนื่องจากว่า midi เป็นหนึ่งในตัวแทนของโน๊ตดนตรีที่นิยมใช้กันในเกมส์ และในงานที่เกี่ยวข้องกับการตัดต่อหรือดัดแปลงเพลง มี preset ของเสียงเครื่องดนตรีที่หลากหลายและเป็นมาตรฐาน อีกทั้งยังสามารถแปลงให้อยู่ในรูปแบบที่โมเดลNLP สามารถเรียนรู้ได้อีกด้วย ด้วยปัจจัยทั้งหมดนี้ผมจึงเลือกใช้ midi เป็น dataset ครับ

ลักษณะเบื้องต้นของ midi

midi มีความแตกต่างจากไฟล์เสียงตัวไปคือเป็นการเก็บลักษณะของการเล่นของนักดนตรีแทนการบันทึกเสียงจริงๆ

ตัวอย่าง message ของ midi

การบันทึกนั้นจะเรียกว่าการเขียน message ลงใน ไฟล์ ซึ่ง message ก็จะมีด้วยกันหลายประเภท

รูปแบบ message ต่างๆของ midi
8546ช1ค่าต่างๆที่เป็นไปได้ใน message แต่ละประเภท

หลักๆที่เราจะใช้ คือ ค่า channel program และ note ที่บรรจุใน note_on และ note_off ซึ่งเป็นสิ่งที่ทำให้เกิดการเล่น note และการเปลี่ยน note

ข้อมูลเพิ่มเติมเกี่ยวกับ midi : https://cecm.indiana.edu/361/midi.html

ผมเลือกใช้dataset จากเกมundertale ที่หาใด้ในkaggle

ต่อไปผมจะทำการวิเคราะห์datasetsที่ผมรวบรวมมา…

  1. ค่า resolution ดูว่าโน๊ตตัวดำ1ตัวแบ่งออกเป็นกี่ส่วน เช่น ถ้า resolution=96 โน๊ตตัวดำจะถูกแบ่งออกเป็น96ส่วน
ค่า resolution ของ raw data

2. การกระจายตัวของความยาวเพลง หรือ จำนวณโน๊ตในแต่ละเพลง

ViolinPlot แสดงการกระจายตัวของความยาวเพลงในหน่วยโน๊ต

3. ความถี่ของตัวโน็ตที่พบในแต่ละเพลง

กราฟแท่งแสดงความถี่ของตัวโน๊ตที่พบมากที่สุด 20 ลำดับแรก

หลังจากที่เราได้ศึกษาข้อมูลกันแล้ว เราก็จะมาทำการทำความสะดาดข้อมูลกัน …

เนื่องจากว่าdata ที่ได้รับมามีลักษณะคือ

  1. มีเครื่องดนตรีเล่นพร้อมกันหลาย track ใน 1 file midi ทำให้ยากต่อการเลือก track ที่มีความสำคัญมากที่สุด
  2. จากการทำการสำรวจข้อมูล ผมได้รู้ว่า data ของเรามีค่า resolution ที่กระจายตัวหลากหลาย ค่า resolution ที่มากทำให้สามารถอ่านข้อมูลได้ช้า และสิ้นเปลืองพื้นที่หน่วยความจำ

ดั้นนั้นผมจึงตัดสินใจศึกษาวิธีการแปลงไฟล์midi ให้เหลือเพียง 1 track และมีเพียง 1 เสียงเครื่องดนตรี เนื่องจากว่า library mido ไม่สามารถทำได้โดยตรง ผมจึงอาศัยความช่วยเหลือจากการแปลง midi ให้กลายเป็น pypianoroll ก่อน แล้วจึงนำ pianoroll นั้นไปเขียนเป็น midi ใหม่อีกครั้ง

การแปลง midi ให้เป็น pianoroll

ผมศึกษาต้นแบบจาก tutorial ใน Medium

และดัดแปลงวิธีการด้วยตัวเองจากการศึกษา documentation

MIDI Files — Mido 1.2.10 documentation

เนื่องจากไฟล์ที่ได้รับมานั้นมาจากหลายเครื่องดนตรีที่อาจจะมีย่านของโทนเสียงที่ต่างกันผมจึงเลือกที่จะดัดแปลงโค้ดต้นฉบับให้สามารถรองรับค่าทั้งหมดที่เป็นไปได้ของscale โน๊ต (0–127)

จากโค้ดตัวอย่างมีการอ่าน midi message ไปเรื่องๆตามลำดับในทุก track จากนั้นนำมา เก็บเป็น array โดยมีตัวเลขแสดงค่าความดังของโน๊ต หรือ velocity กำกับ

ลักษณะของ pianoroll ที่มีเพียง 1 track ภายหลังจากการรวมหลายๆ track เข้าด้วยกัน

ภายหลังจากการแปลงให้เป็น 1 track แล้ว เราจะแปลง track นี้ ให้กลายเป็น midi ผ่าน library pypianoroll โดยการสร้าง multitrack object ผ่าน numpy array โดยตรง สังเกตว่าขั้นตอนนี้จะมีการใส่ tempo หรือจังหวะ และ ค่า resolution ของ file ที่ต้องการจะบันทึกด้วย

ดังนั้น …

เราจะใช้โอกาสในการเขียน midi ใหม่นี้ในการลด resolution ของ midi เสียเลย!

เริ่มจากการเก็บ tempo เดิมของ midi ไว้ใน array
จากนั้นทำการเตรียมค่า resolution, pianoroll array, และ tempo ใหม่ที่จะต้องใช้ในการเขียนเป็น midi
เขียน midi ผ่าน library pypianoroll

การลด resolution โดยที่ส่งผลเสียต่อความเร็วและจังหวะของเพลงน้อยคือการปรับ tempo ให้สูงขึ้นเป็นอัตราส่วนที่เท่ากันกับอัตราส่วนของ resolution ที่ลดลง ยกตัวอย่างเช่น… เราต้องการจะแปลงจากเพลงที่มี resolution 1024 และ tempo มาตรฐาน 120 ให้เหลือ resolution 96 เราจะต้องปรับ tempo ขึ้นเป็น 1024/96 เท่าของ tempo เดิม นั่นคือประมาณ tempo 1280 นั่นเอง

ผมเลือกปรับ resolution ของ data ทั้งหมดให้เหลือเป็น 96

หลังจากที่เราทำการทำความสะอาดข้อมูลแล้ว เราก็จะมาทำให้ข้อมูลนี้สามารถเป็น input ให้กับ model ได้ครับ …

เริ่มจากการอ่านไฟล์ midi ของเราให้กลายเป็นลำดับของโน๊ตที่เรียงต่อกันไปเรื่อยๆครับ ขั้นตอนนี้อาจจะใช้เวลานิดนึงเนื่องจากมีไฟล์บางไฟล์ที่ยาวกว่าเพื่อนครับ

เราจะใช้ string ที่เป็นชื่อของโน๊ต แทนตัวโน๊ตตัวนั้นๆ และเราจะใช้ ลำดับของ note ในคอร์ดที่เล่นพร้อมกันมาเชื่อมกันด้วย “.” แทนคอร์ด 1 คอร์ดครับ

อ่าน midi ให้อยู่ในรูปลำดับของโน๊ตเรียงกัน จากนั้นทำการเปลี่ยนชื่อคอร์ดให้สามารถอ่านได้ง่าย

แต่ว่า … โมเดลไม่สามารถรับ input เป็น string ได้ เราเลยต้องแปลงให้กลายเป็นตัวเลขก่อน

ผมใช้วิธีการสร้าง dictionary ขึ้นมาเพื่อแปลงจาก string ของโน๊ต เป็นตัวเลขค่า 1

หลังแปลงให้โน๊ตเป็นตัวเลขเพื่อให้โมเดลสามารถอ่านได้แล้ว เตรียมข้อมูลโดยการใช้เทคนิค window sliding

sliding window : https://res.cloudinary.com/practicaldev/image/fetch/s--QbLzhl5A--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/i/h3h2h4s11pjgla88pqzp.png

เราจะทำการใส่ input ของโมเดลเป็น sequence ของโน๊ตที่มีความยาวคงที่ค่าหนึ่ง ในที่นั้นเราใช้ 100 และ output ของ model จะเป็นโน๊ตตัวที่ 101 เราจะทำการเลื่อนแบบนี้ไปจนสุดความยาวของเพลง

dataset ที่ใช้ในการเตรียมข้อมูลสำหรับการ train

เมื่อเราจะทำการแต่งเพลงเราจะใช้การสุ่มโน๊ตขึ้นมา 100 ตัว จากนั้นจะให้ model ทำนายตัวที่ 101 นำไปต่อท้าย จากนั้นใช้โน๊ตตัวที่ 2 ถึง 101 ทำนายโน๊ตตัวที่ 102 และเป็นเช่นนี้เรื่อยไปจนถึงโน๊ตตัวสุดท้ายที่เราต้องการ

ต่อมาผมจะพูดถึงโมเดลที่ผมใช้งาน…

  1. LSTM (Long Short Term Memory networks)

LSTM จัดว่าเป็น RNN ประเภทหนึ่งที่มีการทำงานที่ซับซ้อนกว่า กล่าวโดยสรุป RNN นั้น จะเป็นการตัวโน็ต/หนังสือโดยอ้างอิงจากการทำนายก่อนหน้า แต่LSTM จะมีสิ่งที่เรียกว่าเกท และ cell memory ที่สามารถพัฒนาความจำของผลก่อนหน้าได้ ว่าจะเก็บ ทิ้ง หรือเรียกใช้ เหมือนตม.ที่ตรวจสิ่งของ ที่เอาของเข้า ออก บันทึกผลการตรวจ และพัฒนาการตรวจรอบถัดไปครับ

an LSTM Cell
class ของ model TobyFox ที่ใช้ LSTM

2. Embedding layer

embedding layer เป็น layer ที่จะเปลี่ยนตัวโน๊ตแต่ละตัวด้วยชุดตัวเลขชุดหนึ่งที่ไม่ซ้ำกัน embedding layer สามารถเทรนให้โน๊ตที่มีความคล้ายคลึงกันแทนที่ด้วยชุดตัวเลขที่ใกล้กันมากขึ้น ผมจึงใช้ embedding layer แทนการใช้ onehot และการหาร label ด้วยจำนวน class ทั้งหมดแบบดั้งเดิมใน tutorial ซึ่งทำให้ model สามารถเรียนรู้ความสัมพันธ์ของโน๊ตแต่ละตัวได้ดียิ่งขึ้น

3. teacher forcing

เป็นวิธีการ train RNN แบบหนึ่ง แต่แทนที่จะอ้างอิงจากผลลัพธ์ก่อนหน้า teacher forcingจะใช้ground truth (ผลลัพธ์ที่แท้จริง)

การใช้งาน teacher forcing จำเป็นจะต้องมีการดัดแปลงข้อมูลจาการทำ sliding window เล็กน้อย

สำหรับในข้อมูล input เราจะทำการเพิ่ม “Start Token” หรือในที่นี้คือ <S> เข้าไปเป็นลำดับที่ 1 และ shift ข้อมูลไป 1 ช่อง แล้วตัดตัวสุดท้ายของลำดับออก ส่วน output จะเป็น ลำดับเดิมที่ไม่มีการเปลี่ยนแปลง

Teacher Forcing Dataset

อีกข้อดีของการใช้วิธีการนี้คือเราสามารถแต่งเพลงได้เลยได้มีเพียงแค่โมเดลและ <S> เท่านั้น ไม่จำเป็นต้องไปสุ่มลำดับโน๊ตมาก่อน

Teacher Forcing Model

teacher forcing model จะมีความแตกต่างกันระหว่าการ train และการ eval หรือ inference

ระหว่างการ inference โมเดลจะรับ input step แรกเป็น start token หรือเทียบได้กับตั๋วกาชา หลังจากที่ได้รับ start token model ก็จะทำนายความน่าจะเป็นของโน๊ตตัวถัดไปออกมา ขั้นตอนนี้ก็จะคล้ายกับการเอา item ต่างๆที่มีความ rare ต่างกันใส่ในกล่องกาชา จากนั้นก็จะทำการสุ่มโน๊ตจากความน่าจะเป็นนั้น เหมือนกับการสุ่มกาชา จากนั้นก็จะรับโน๊ตตัวนั้นเป็น input ถัดไป เรื่อยๆ

ผมจะทำการสร้างข้อมูลออกมา6ประเภท

  1. สุ่มตัวโน๊ตเอาจากจำนวนที่พบดื้อๆเลย (random)
  2. ใช้LSTM 1 ชั้น (toby) ใช้วิธีการเทรนแบบ window sliding
  3. ใช้LSTM 3 ชั้น (จาก tutorial) ใช้วิธีการเทรนแบบ window sliding

4. ใช้BiLSTM (LSTM ที่คิดพร้อมกัน2รอบไปกลับ) 3ชั้น (3fold)

5. ใช้ warm-start/Teacherforcing (wstf)

6. ใช้กระบวนท่า warm-start + BiLSTM (ws3)

เป็นวิธีการที่ใช้ผลลัพธ์จากโมเดล teacher forcing มาเป็น input ให้กับ model BiLSTM ผมมีความเชื่อว่าการใช้ข้อมูลเริ่มต้นจากโมเดล teacher forcing ที่สุ่มโดยมีการหาความสัมพันธ์ระหว่างตัวโน๊ต จะเป็น input ที่ดีกว่าให้กับ model ตัวอื่นๆ เมื่อเทียบกับการสุ่มตัวโน๊ตโดยไม่ได้ใช้ความสัมพันธ์ระหว่างโน๊ต จึงได้ลองทำการทดลองนี้ดูครับ

หลังจากที่เราเทรนโมเดลกันไปแล้วก็เข้าสู่การประเมินผล …

  1. การใช้ loss ของ validation set ในการประเมิน

ค่า loss เป็นค่าที่ model ตั้งเป้าไว้ว่าจะต้องลดให้ได้มากที่สุด ซึ่งในกรณีนี้เป็น cross entropy loss ซึ่งมีสมการดังนี้

CrossEntropyLoss

หากโมเดลสามารถทำนายไปยังโน๊ตที่ถูกต้องได้มั่นใจมาเท่าไหร่ ค่า loss ก็จะต่ำลง ดังนั้นค่า loss ใน validation set ก็จะสามารถบอกได้ว่า model สามารถทำนายได้ถูกต้องและมั่นใจขนาดไหนบนข้อมูลที่ไม่เคยเห็นมาก่อน

ค่า validation loss(ตรวจความถูกต้องระหว่างเทรน) และ training loss(ตรวจความถูกต้องระหว่างเทรน)

กราฟแสดง validation loss และ training loss ของ model BiThreeFold (สีส้ม validation สีฟ้า training)

อย่างไรก็ดี ค่า loss ของ model ทั้งบน train set และ validation set ก็ไม่สามารถบ่งบอกถึงคุณภาพของเพลงที่แต่งออกมาได้ เราจึงใช้การทำแบบสอบถามเพื่อประเมินความพึงพอใจของผู้ใช้งานโมเดล

โดยแบบสอบถามของผมจะให้ผู้ทดสอบลองฟังเพลงและลองตอบคำถามสองข้อ

  1. ให้เดาว่าเป็นเพลงที่มาจากการสุ่ม จากโมเดล หรือจาก groundtruth
  2. ให้ผู้ฟังลองให้คะแนน จาก 1(แย่มาก)-5(ดีมาก) จากนั้นจะทำการหาค่าคะแนนเฉลี่ยจาก MOS

ผลจากแบบสอบถาม

พบว่าเพลงที่ได้จากโมเดล WarmStart + BiThreeFold ได้คะแนน ความพึงพอใจสูงที่สุด ตามมาด้วย WarmStart , BiThreeFold , TobyFox และ tutorial ตามลำดับ

โดยที่ model ที่ได้ค่าความพึงพอใจสูงกว่า ground truth และ random ได้แก่ BiThreeFold, WarmStart, WarmStart + BiThreeFold และ TobyFox

วิเคราะห์จุดผิดพลาด และการพัฒนา…

หลังจากที่เราทำการประเมญ

จุดผิดพลาดที่ 1 : model ไม่สามารถเรียนรู้จังหวะ, ความยาวของโน๊ต และไม่สามารถแต่งเพลงที่มีความหลากหลายของจังหวะได้ อาจจะส่งผลต่อความรู้สึกต่อคุณภาพของเพลงของผู้ตอบแบบสอบถาม เนื่องจากว่าความไพเราะของเพลงมีเรื่องจังหวะและทำนองเข้ามาเกี่ยวข้องด้วย

วิธีการแก้ไข :

  1. ใช้อีก model ในการเรียนรู้ ช่วยทำนายจังหวะ และความยาวของโน๊ตภายหลังจากที่แต่งเพลงเสร็จแล้ว
  2. ใช้การเรียนรู้ผ่าน pianoroll โดยตรง

จุดผิดพลาดที่ 2 : การประเมินผลอาจจะมี bias เนื่องจากความหลากหลายและประสบการณ์ของผู้ทำแบบสอบถาม

วิธีการแก้ไข :

  1. แบ่งกลุ่มผู้ทำแบบสอบถามตามประสบการณ์การทำงาน และทักษะความสามารถทางดนตรี

จุดผิดพลาดที่ 3 : สังเกตว่า model อาจจะ overfit และการแต่งเพลงอาจมีโอกาสสำเร็จต่ำ

แนวทางแก้ไข:

  1. สังเกตว่า model อาจจะ overfit และการแต่งเพลงอาจมีโอกาสสำเร็จต่ำ

จุดผิดพลาดที่ 4 : ชุดข้อมูลอาจมีความหลายหลายและมีความซับซ้อนมากเกินไป ทำให้การเรียนรู้ของโมเดลทำได้ยาก ส่งผลให้การเรียน

แนวทางการแก้ไข:

  1. ทำการแบ่งชุดข้อมูลออกเป็นหลายๆส่วนตามอารมณ์ของเพลง จากนั้นทำการ train โมเดลแยกจากกัน เพื่อให้โมเดลสามารถเรียนรู้และแต่งเพลงที่มี mood ต่างกันได้ และ อาจจะทำให้โมเดลแต่งเพลงโดยดีขึ้น และได้คะแนนความพึงพอใจมากขึ้น

สรุปผล…

การได้ทำโครงงานนี้ทำให้ผมได้เรียนรู้ขั้นตอนการทำโครงงาน machine learning ตั้งแต่การเก็บข้อมูล ศึกษาและทำความสะอาดข้อมูล ไปจนถึงการทำ model และการประเมินผล และเป็นโอกาสที่ดีที่ผมได้เรียนรู้การทำงานของไฟล์เพลงประเภท midi ได้ใช้ความคิดสร้างสรรค์ในการแก้ไขปัญหาและสร้างแนวทางการแก้ไขปัญหาระหว่างการทำ preprocessing ได้ทดลองและทำความเข้าใจกับ deep learning model มากยิ่งขึ้น

นี่เป็นเพียงจุดเริ่มต้นของผมกับ AI และผมก็เชื่อว่าประสบการณ์ทั้งในด้านความสำเร็จและข้อผิดพลาดในครั้งนี้ จะทำให้ผมสามารถนำกลับไปพัฒนาโครงงานและสามารถต่อยอดในการทำโครงงานอื่นๆต่อไปได้ครับ

ขอขอบพระคุณพี่ๆ mentor พี่ แคน และพี่มะเฟือง พี่ TA หมูกรอบ และพี่ๆคนอื่นๆทุกคนในโครงการที่ได้จัดโครงการดีๆแบบนี้ขึ้นมาครับ

สามารถลองเล่นลองใช้งานโมเดลของผมเพิ่มเติมได้ที่ : https://share.streamlit.io/kangkengkhadev/midigenv2/main/app.py

Code เพิ่มเติมได้ที่ :

https://github.com/Noppawit-Tantisiriwat/AIB2022-Undertale-Music-Generation

--

--