In [ ]:
!pip install pythainlp
!wget http://www.donlapark.cmustat.com/229352/thai_lyrics.csv
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting pythainlp
  Downloading pythainlp-3.1.0-py3-none-any.whl (9.6 MB)
     |████████████████████████████████| 9.6 MB 29.8 MB/s 
Requirement already satisfied: requests>=2.22.0 in /usr/local/lib/python3.7/dist-packages (from pythainlp) (2.23.0)
Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.22.0->pythainlp) (2.10)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests>=2.22.0->pythainlp) (1.24.3)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.22.0->pythainlp) (2022.6.15)
Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.22.0->pythainlp) (3.0.4)
Installing collected packages: pythainlp
Successfully installed pythainlp-3.1.0
--2022-09-27 03:17:43--  http://www.donlapark.cmustat.com/229352/thai_lyrics.csv
Resolving www.donlapark.cmustat.com (www.donlapark.cmustat.com)... 150.107.31.67
Connecting to www.donlapark.cmustat.com (www.donlapark.cmustat.com)|150.107.31.67|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 44871532 (43M) [text/csv]
Saving to: ‘thai_lyrics.csv’

thai_lyrics.csv     100%[===================>]  42.79M  3.32MB/s    in 17s     

2022-09-27 03:18:01 (2.45 MB/s) - ‘thai_lyrics.csv’ saved [44871532/44871532]

Song lyrics generation¶

In [ ]:
import pandas as pd
from itertools import chain
from pythainlp import word_tokenize
from collections import Counter
import matplotlib.pyplot as plt
import seaborn as sns
import csv

import numpy as np

df = pd.read_csv('thai_lyrics.csv', engine='python')
df.head()
Out[ ]:
url soup title artist_name full_lyrics lyrics
0 https://www.siamzone.com/music/thailyric/14050 <!DOCTYPE HTML>\n\n<html lang="th">\n<head>\n<... เนื้อเพลง แบบว่าหวั่นไหว (Ost. เด็ดปีกนางฟ้า) กลม อรวี พินิจสารภิรมย์ ก็เธอน่ะทำให้ใจมันเต้นแรง แรง\nแต่เขาก็ทำให้ใจ... ก็เธอน่ะทำให้ใจมันเต้นแรง แรง\nแต่เขาก็ทำให้ใจ...
1 https://www.siamzone.com/music/thailyric/14051 <!DOCTYPE HTML>\n\n<html lang="th">\n<head>\n<... เนื้อเพลง ที่ผ่านมา เยิ้ม Yerm วันแห่งความรัก ขอมอบเพลงนี้ให้ใครสักคน\nที่ยัง... วันแห่งความรัก ขอมอบเพลงนี้ให้ใครสักคน\nที่ยัง...
2 https://www.siamzone.com/music/thailyric/14052 <!DOCTYPE HTML>\n\n<html lang="th">\n<head>\n<... เนื้อเพลง ในตอนนั้น โอม รัธพงศ์ ภูรีสิทธิ์ Youngohm เมื่อก่อนนี้มีแค่เรา\nในความคิดเป็นแค่เงา\nไม่... เมื่อก่อนนี้มีแค่เรา\nในความคิดเป็นแค่เงา\nไม่...
3 https://www.siamzone.com/music/thailyric/14053 <!DOCTYPE HTML>\n\n<html lang="th">\n<head>\n<... เนื้อเพลง อยู่ในใจ ปู พงษ์สิทธิ์ คำภีร์ ขอบคุณวันเวลา ที่ผ่านมา ขอบคุณสิ่งดีที่เธอให้\... ขอบคุณวันเวลา ที่ผ่านมา ขอบคุณสิ่งดีที่เธอให้\...
4 https://www.siamzone.com/music/thailyric/14054 <!DOCTYPE HTML>\n\n<html lang="th">\n<head>\n<... เนื้อเพลง ชู้กะชู้ เอ๊ะ พงศ์จักร พิษฐานพร Aeh Syndrome เอ๊ะ ขาวใสๆ ช่างน่ามอง\nเอ๊ะ รอยยิ้มของเธอ ทำฉ... เอ๊ะ ขาวใสๆ ช่างน่ามอง\nเอ๊ะ รอยยิ้มของเธอ ทำฉ...
In [ ]:
tokenized_lyrics = df['lyrics'].map(word_tokenize)
print(tokenized_lyrics[0])
['ก็', 'เธอ', 'น่ะ', 'ทำให้', 'ใจ', 'มัน', 'เต้น', 'แรง', ' ', 'แรง', '\n', 'แต่', 'เขา', 'ก็', 'ทำให้', 'ใจ', 'อยาก', 'กรี๊ด', 'รัว', ' ', 'รัว', '\n', 'ผลัดกัน', 'มา', 'ทำคะแนน', 'จน', 'ฉัน', 'กลัว', ' ', 'กลัว', ' ', 'หัวใจ', 'วุ่นวาย', '\n', '\n', 'ก็', 'เธอ', 'น่ะ', 'คอย', 'เอาใจ', 'แบบ', 'โอ๊ย', 'คือ', 'ดี', '\n', 'แต่', 'เค้า', 'ก็', 'มี', 'สไตล์', 'แบบ', 'โอ๊ย', 'คือ', 'โดน', '\n', 'จิตใจ', 'เลย', 'มี', 'อาการ', 'แบบ', 'เริ่ม', 'ซน', ' ', 'ซน', ' ', 'นิดหนึ่ง', '\n', '\n', 'แบบ', 'ว่า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'วา', ' ', 'หวั่นไหว', '\n', 'แบบ', 'ว่า', 'น่ารัก', ' ', 'น่ารัก', ' ', 'น่ารัก', ' ', 'ทั้งคู่', 'เลย', ' ', 'มัน', 'ยิ่ง', 'คิด', 'ยิ่ง', 'คิดไม่ออก', '\n', 'กูเกิ้ล', 'ก็', 'ไม่', 'บอก', ' ', 'เธอ', 'น่า', 'เก็บ', 'ไว้', 'ทั้งสอง', 'คน', '\n', 'แบบ', 'มัน', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นอ', 'ยด์', 'กว่า', 'ใคร', '\n', 'จะ', 'ให้', 'เลือก', 'ก็', 'ไม่', 'ไหว', ' ', 'โจทย์', 'มัน', 'ยาก', 'เกิน', 'ทน', '\n', 'มัน', 'เลือกไม่ได้', 'สักที', ' ', 'เลย', 'เลือกไม่ได้', 'สักที', ' ', 'ทั้งสอง', 'คน', '\n', '\n', 'อย่า', 'หาว่า', 'ฉัน', 'เป็น', 'คนเจ้าชู้', 'บี', 'ดู', '\n', 'ไม่', 'ได้', 'ตั้งใจ', 'อย่า', 'เพิ่ง', 'ใส่ร้าย', 'เลย', 'ยู', '\n', 'ไม่', 'เคย', 'จะ', 'เป็น', 'อย่างนี้', 'มา', 'มะ', 'มา', 'ดู', ' ', 'หน้า', 'คน', 'กลุ้มใจ', '\n', '\n', 'ก็', 'เธอ', 'น่ะ', 'คอย', 'เอาใจ', 'แบบ', 'โอ๊ย', 'คือ', 'ดี', '\n', 'แต่', 'เค้า', 'ก็', 'มี', 'สไตล์', 'แบบ', 'โอ๊ย', 'คือ', 'โดน', '\n', 'จิตใจ', 'เลย', 'มี', 'อาการ', 'แบบ', 'เริ่ม', 'ซน', ' ', 'ซน', ' ', 'นิดหนึ่ง', '\n', '\n', 'แบบ', 'ว่า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'วา', ' ', 'หวั่นไหว', '\n', 'แบบ', 'ว่า', 'น่ารัก', ' ', 'น่ารัก', ' ', 'น่ารัก', ' ', 'ทั้งคู่', 'เลย', ' ', 'มัน', 'ยิ่ง', 'คิด', 'ยิ่ง', 'คิดไม่ออก', '\n', 'กูเกิ้ล', 'ก็', 'ไม่', 'บอก', ' ', 'เธอ', 'น่า', 'เก็บ', 'ไว้', 'ทั้งสอง', 'คน', '\n', 'แบบ', 'มัน', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นอ', 'ยด์', 'กว่า', 'ใคร', '\n', 'จะ', 'ให้', 'เลือก', 'ก็', 'ไม่', 'ไหว', ' ', 'โจทย์', 'มัน', 'ยาก', 'เกิน', 'ทน', '\n', 'มัน', 'เลือกไม่ได้', 'สักที', ' ', 'เลย', 'เลือกไม่ได้', 'สักที', ' ', 'ทั้งสอง', 'คน', '\n', '\n', 'แบบ', 'ว่า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'ว้า', ' ', 'วา', ' ', 'หวั่นไหว', '\n', 'แบบ', 'ว่า', 'น่ารัก', ' ', 'น่ารัก', ' ', 'น่ารัก', ' ', 'ทั้งคู่', 'เลย', ' ', 'มัน', 'ยิ่ง', 'คิด', 'ยิ่ง', 'คิดไม่ออก', '\n', 'กูเกิ้ล', 'ก็', 'ไม่', 'บอก', ' ', 'เธอ', 'น่า', 'เก็บ', 'ไว้', 'ทั้งสอง', 'คน', '\n', 'แบบ', 'มัน', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นะ', ' ', 'นอ', 'ยด์', 'กว่า', 'ใคร', '\n', 'จะ', 'ให้', 'เลือก', 'ก็', 'ไม่', 'ไหว', ' ', 'โจทย์', 'มัน', 'ยาก', 'เกิน', 'ทน', '\n', 'อย่า', 'เพิ่ง', 'หนี', 'และ', 'อย่า', 'เพิ่ง', 'บ่น', ' ', 'ถ้า', 'รัก', 'ฉัน', 'แล้ว', 'ต้อง', 'ทน', ' ', 'นะ', 'ทั้งสอง', 'คน']

Convert from words to numbers¶

In [ ]:
#[[song , number , one],[song , number , two]] -> [song , number , one , song , number , two]
def flatten(ls):
    """
    Flatten list of list
    """
    return list(chain.from_iterable(ls))

#[song , number ,one, number, two] -> [1,2,3,2,4] and [1,2,3] -> [song , number , one]
def create_lookup_dict(tokenized_lyrics, n_min=None):
    """
    Create lookup dictionary from list of words (lyrics)
    """
    word_counts = Counter(tokenized_lyrics)
    sorted_vocab = sorted(word_counts, key=word_counts.get, reverse=True)
    if n_min is not None:
        sorted_vocab = {k: v for k, v in word_counts.items() if v >= n_min}
    vocab_to_int = {word: i for i, word in enumerate(sorted_vocab, 0)}
    int_to_vocab = {i: word for word, i in vocab_to_int.items()}
    return (vocab_to_int, int_to_vocab)
In [ ]:
tokenized_lyrics = flatten(tokenized_lyrics)
tokenized_lyrics = [token if token is not '\n' else ' ' for token in tokenized_lyrics]
word_counts = Counter(tokenized_lyrics)
vocab_to_int, int_to_vocab = create_lookup_dict(tokenized_lyrics, n_min=None)
In [ ]:
vocab_to_int["ใคร"]
Out[ ]:
28
In [ ]:
len(vocab_to_int)
Out[ ]:
10035
In [ ]:
int_to_vocab[12]
Out[ ]:
'ว่า'

Create Features (20 words in a song) and Target (the next word)¶

In [ ]:
sequence_length = 20

tokenized_indices = [vocab_to_int.get(token, 0) for token in tokenized_lyrics]

X, target = [], []
for n in range(0, len(tokenized_indices) - sequence_length, 1):
  x = tokenized_indices[n: n + sequence_length]
  y = tokenized_indices[n + sequence_length]
  X.append(np.array(x))
  target.append(y)
X = np.array(X)
target = np.array(target)
In [ ]:
X[0]
Out[ ]:
array([   7,    1,  292,   65,   22,   10,  501,  361,    0,  361,    0,
         17,   32,    7,   65,   22,   25, 4481, 1487,    0])
In [ ]:
target[0]
Out[ ]:
1487
In [ ]:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader

class MyDataSet(torch.utils.data.Dataset):
  def __init__(self, X, y):
    super(MyDataSet, self).__init__()
    self._X = X
    self._y = y

  def __len__(self):
    return self._X.shape[0]

  def __getitem__(self, index):
    X = self._X[index]
    y = self._y[index]
    return X, y
In [ ]:
# Hyperparameters
LEARNING_RATE = 0.001
BATCH_SIZE = 128
NUM_EPOCHS = 15

# Classification
NUM_CLASSES = 10035

dataset = MyDataSet(X, target)

trainloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)

New layers¶

  1. nn.Embedding(num_vocabs, hidden_dim)

emb

In [ ]:
embedding = nn.Embedding(10, 3)
# a batch of 2 samples of 4 indices each
input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
output = embedding(input)
output.shape
Out[ ]:
torch.Size([2, 4, 3])
  1. LSTM

lstm

In [ ]:
lstm = nn.LSTM(10, 20, 2)
input = torch.randn(5, 3, 10)
h0 = torch.randn(2, 3, 20)
c0 = torch.randn(2, 3, 20)
output, (h1, c1) = lstm(input, (h0, c0))
output.shape
Out[ ]:
torch.Size([5, 3, 20])

Exercise 1: fill in the code below¶

In [ ]:
from typing_extensions import Self
class Simple_LSTM(nn.Module):
    def __init__(self):
        super(Simple_LSTM, self).__init__()

        # TODO 1: Fill in the layers' parameters, with all hidden dimensions = 128
        self.embeddings = nn.Embedding(, )
        self.lstm = nn.LSTM(, , dropout = 0.2, num_layers = 2)
        self.fc = nn.Linear(, )
    
    def forward(self, x):
        # for LSTM, input should be (Sequnce_length,batchsize,hidden_layer), 
        # so we need to transpose the input
        x = x.t()
        # TODO 2: Apply the Embedding layer
        x = self.embeddings(x)
        # TODO 3: Apply the LSTM layer (note: LSTM's output is a tuple!)
        h, _ = self.lstm(x)
        # Only need to keep the last element of the sequence
        ht=h[-1] 
        out = self.fc(ht)
        return out
In [ ]:
model = Simple_LSTM().to('cuda')
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)  

Exercise 2: fill in the code below¶

In [ ]:
def train_loop(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        X = X.to('cuda')
        y = y.to('cuda')
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 1000 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")


def generate(model, start_word, int_to_vocab, pad_value=0, predict_len=100):
    
    words = word_tokenize(start_word)
    start_word_ids = []
    predicted = words
    
    word_ids = [vocab_to_int.get(word, pad_value) for word in words]
    #[28,15,16] -> [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,15,16]

    current_seq = [np.pad(word_ids, (20 - len(word_ids), pad_value), 'constant')]
    
    for _ in range(predict_len):
        current_seq = torch.LongTensor(current_seq).to('cuda')
        # get the next word probabilities
        p = model(current_seq)
        p = nn.Softmax(dim=1)(p).cpu().detach().numpy()
        # p = [[0.1,0.2,0.05,0.03,0.02,0.3,0.2,0.1]]
        p = p[0]
        # p = [0.1,0.2,0.05,0.03,0.02,0.3,0.2,0.1]


        # Sample from probability distribution p
        word_i = np.random.choice(np.arange(0,p.shape[0]),p=p)
        #word_i is an integer representing a word.
        
        #### TODO: Fill in the following two lines of code#########
        # Convert from word_i (int)--> word (str)
        # and append the word from 1. into `predicted` list.

        ##################end#####################################
        
        # the generated word becomes the next "current sequence" and the cycle can continue
        current_seq = current_seq.cpu().detach().numpy()
        current_seq = np.roll(current_seq, -1, 1)
        current_seq[-1][-1] = word_i
    gen_sentences = ''.join(predicted)    
    return gen_sentences

Exercise 3: use generate function to generate three more songs. You may try using different starting words.¶

In [ ]:
for t in range(NUM_EPOCHS):
    print(f"Epoch {t+1}\n-------------------------------")
    train_loop(trainloader, model, loss_fn, optimizer)
    print(generate(model, 'ใคร', int_to_vocab, predict_len=100))
print("Done!")
Epoch 1
-------------------------------
loss: 9.203841  [    0/370475]
loss: 5.516397  [128000/370475]
loss: 5.002436  [256000/370475]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-22-d3717313b1f5> in <module>
      1 for t in range(NUM_EPOCHS):
      2     print(f"Epoch {t+1}\n-------------------------------")
----> 3     train_loop(trainloader, model, loss_fn, optimizer)
      4     print(generate(model, 'ใคร', int_to_vocab, predict_len=100))
      5 print("Done!")

<ipython-input-21-5a29d62dc645> in train_loop(dataloader, model, loss_fn, optimizer)
     10         # Backpropagation
     11         optimizer.zero_grad()
---> 12         loss.backward()
     13         optimizer.step()
     14 

/usr/local/lib/python3.7/dist-packages/torch/_tensor.py in backward(self, gradient, retain_graph, create_graph, inputs)
    394                 create_graph=create_graph,
    395                 inputs=inputs)
--> 396         torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
    397 
    398     def register_hook(self, hook):

/usr/local/lib/python3.7/dist-packages/torch/autograd/__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)
    173     Variable._execution_engine.run_backward(  # Calls into the C++ engine to run the backward pass
    174         tensors, grad_tensors_, retain_graph, create_graph, inputs,
--> 175         allow_unreachable=True, accumulate_grad=True)  # Calls into the C++ engine to run the backward pass
    176 
    177 def grad(

KeyboardInterrupt: 

Web scraping with Beautiful soup¶

In [ ]:
from itertools import chain
from collections import Counter
import requests
from bs4 import BeautifulSoup



def scrape_siamzone_url(d):
    soup = BeautifulSoup(requests.get('https://www.siamzone.com/music/thailyric/%d' % d).content, 'html.parser')
    title, artist_name = soup.find('title').text.split('|')
    title, artist_name = title.strip(), artist_name.strip()
    n_shares = int(soup.find('span', attrs={'class': 'sz-social-number'}).text.replace(',', ''))
    full_lyrics = soup.find('div', attrs={'itemprop': 'articleBody'}).text.strip()
    return {
        'url': 'https://www.siamzone.com/music/thailyric/%d' % d,
        'soup': soup, 
        'title': title,
        'artist_name': artist_name,
        'n_shares': n_shares,
        'full_lyrics': full_lyrics
    }

def scrape_siamzone():
    data = []
    for i in range(14050, 16041):
        try:
            data.append(scrape_siamzone_url(i))
        except:
            pass

    df = pd.DataFrame(data)
    df['lyrics'] = df.full_lyrics.map(clean_lyrics)
    return df