# Import thư viện cần thiết
import requests
import time
import csv
import re
from bs4 import BeautifulSoup
1 Giới thiệu
Liệu rằng phần lời của một bài hát có đầy đủ thông tin để chúng ta phân loại chủ đề bài hát đó? Trong bài viết này, chúng ta sẽ dùng machine learning để trả lời câu hỏi này với các phương pháp Logistic Regression (PyTorch) / Naive Bayes / Genetic Algorithm / Decision Tree nhé!
2 Tải và xử lý dữ liệu
2.1 Trích xuất dữ liệu
Cảm ơn loibaihathot.com đã có một kho lời bài hát khá nhiều và dễ trích xuất. Tất cả những gì chúng ta cần chỉ là thư viện requests để lấy nội dung html của trang web. Và, BeautifulSoup để parse nội dung html ra element cho dễ trích xuất.
Xác định 1 số chủ đề có sẵn trên trang web để tài về. Base URL sẽ là url gốc, từ đây chúng ta replace {genre} để tải lời cho bài hát thuộc chủ đề tương ứng.
Lưu ý: Ở đây mình dùng từ genre không sát nghĩa “chủ đề” đâu nghen.
= ['cach-mang', 'que-huong', 'thieu-nhi', 'tre']
genre_list
= "https://loibaihathot.com/{genre}" base_url
Tiếp theo, chúng ta tải hết tất cả các URL của các bài hát, phân loại theo chủ đề
= {}
lyric_url_list
for genre in genre_list:
= base_url.format(genre=genre)
url = requests.get(url).text
html_text = BeautifulSoup(html_text, 'html.parser')
soup = list(map(lambda e: e['href'], soup.find_all('a')))
all_links = list(set(filter(lambda url: '/20' in url, all_links))) lyric_url_list[genre]
Tạo thư mục data để lưu trữ dữ liệu, tránh việc phải chạy Trích xuất lại lần nữa:
import os
= 'data/'
DATA_FOLDER
def create_path_if_nonexist(path):
if not os.path.exists(path):
os.mkdir(path)
create_path_if_nonexist(DATA_FOLDER)
Viết hàm để tải nội dung lời bài hát cho một bài hát, với input là đường dẫn đến bài hát đó:
import re
def download_lyric(song_url):
= requests.get(song_url).text
html_text = BeautifulSoup(html_text, 'html.parser')
soup = soup.find('div', class_="entry-content content mt-6").get_text(separator = '\n', strip = True)
lyric = re.sub('\n.+','',lyric, count=3).strip()
lyric return lyric
Còn đây là hàm để tải lời nhiều bài cùng 1 lúc, hàm này sẽ dùng để phân luồng threading -> tăng tốc download:
def download_songs(song_urls, genre):
= os.path.join(DATA_FOLDER, genre)
full_path
create_path_if_nonexist(full_path)for url in song_urls:
= download_lyric(url)
lyric if len(lyric) < 10:
continue
=re.findall('/[^\/]+$', url)[0][1:-5]
file_name with open(f'{full_path}/{file_name}.txt', 'w') as f:
f.write(lyric)
Với mỗi chủ đề, chúng ta sẽ dùng threading để tải hết lời bài hát về. Để tải nhanh hơn, chúng ta có thể chia thêm nhiều threads con nữa. Nhưng mình muốn mọi thứ đơn giản trước đã:
import threading
= []
thread_list
# create list of threads
for genre in genre_list:
= threading.Thread(target=download_songs, args=(lyric_url_list[genre],genre))
thread
thread_list.append(thread)
print("Download starting...")
# start each thread
for thread in thread_list:
thread.start()
# wait for all to finish
for thread in thread_list:
thread.join()
# successfully excecuted
print("Download finished!")
Download starting...
Download finished!
2.2 Tiền xử lý (Pre-processing)
Sau khi mình và train thử cho bộ dữ liệu ban đầu thì thấy độ chính xác đều dưới 20%. Kiểm tra lại thì thấy có nhiều bài download về vào sai thư mục chủ đề. Điều này có thể gây nhiễu dữ liệu, nên chúng ta sẽ xóa thủ xông những files sau để tăng độ chính xác khi huấn luyện:
# Clean-up data
= {
to_delete "thieu-nhi":[
"cam-on-nguoi-da-roi-xa-toi.txt",
"chac-ai-do-se-ve.txt",
"dung-tin-em-manh-me.txt",
"hay-ra-khoi-nguoi-do-di.txt",
"khuon-mat-dang-thuong.txt",
],
"cach-mang":[
"cam-on-nguoi-da-roi-xa-toi.txt",
"chac-ai-do-se-ve.txt",
"dung-tin-em-manh-me.txt",
"hay-ra-khoi-nguoi-do-di.txt",
"khuon-mat-dang-thuong.txt",
],
"que-huong":[
"cam-on-nguoi-da-roi-xa-toi.txt",
"chac-ai-do-se-ve.txt",
"dung-tin-em-manh-me.txt",
"hay-ra-khoi-nguoi-do-di.txt",
"khuon-mat-dang-thuong.txt",
],
}
for subfolder, filenames in to_delete.items():
for filename in filenames:
try:
= f"{DATA_FOLDER}{subfolder}/{filename}"
filepath
os.remove(filepath)except Exception as e:
print(e)
Cài thêm thư viện underthesea để hỗ trợ tokenize ngôn ngữ Việt:
!pip install -q underthesea==1.3.5a3
Tiền xử lý lời bài hát, chủ yếu là xóa ký tự đặc biệt:
def preprocess_lyric(text):
# remove special characters
import re
= re.sub('[^\w\s]','', text).lower()
text return text
'Ngày mai??!! 13 em đi!') preprocess_lyric(
'ngày mai 13 em đi'
3 Phương pháp 1: Logistic Regression với PyTorch
Dữ liệu text không thể huấn luyện trực tiếp được. Vì vậy chúng ta sẽ phải xây dựng features cho data. Ở đây, với mỗi lyrics, mình sẽ đếm tần suất của mỗi từ trong lyrics xuất hiện trong từng label (hay genre).
import numpy as np
= len(genre_list)
n_labels = 1 + n_labels # cộng 1 vì thêm bias n_features
Xây dựng từ điền freqs để đếm tần suất của mỗi từ theo từng label. freqs sẽ sử dụng để tạo features ở bước sau:
import glob
from underthesea import word_tokenize
# build freq dict with key pair as (word, label)
= {}
freqs for label, genre in enumerate(genre_list):
= os.path.join(DATA_FOLDER, genre)
lyric_folder for filepath in glob.glob(f"{lyric_folder}/*txt"):
with open(filepath, 'r') as file:
= preprocess_lyric(file.read())
lyric = word_tokenize(lyric)
lyric_tokens for word in lyric_tokens:
= (word, label)
pair if pair not in freqs:
= 1
freqs[pair] else:
+= 1 freqs[pair]
Viết hàm chuyển lời bài hát thành features.
def lyric_to_features(lyric_raw, n_labels):
= preprocess_lyric(lyric_raw)
lyric = word_tokenize(lyric)
lyric_tokens
= [0] * n_labels
features for word in lyric_tokens:
for label in range(n_labels):
= (word, label)
pair = freqs.get(pair, 0)
count += count
features[label] = [1] + features
features # print(features)
return features
Đọc data từ ổ cứng và chuyển chúng thành features và labels:
= [] # features of songs
data
for lyric_label, genre in enumerate(genre_list):
= os.path.join(DATA_FOLDER, genre)
lyric_folder for filepath in glob.glob(f"{lyric_folder}/*txt"):
with open(filepath, 'r') as file:
= file.read()
lyric_raw = lyric_to_features(lyric_raw, n_labels) + [lyric_label]
features data.append(features)
Sau đó, ta trộn và chia data thành X và y là những numpy array để dễ thao tác và train so với list truyền thống.
= np.array(data)
data
np.random.shuffle(data)
= data[:,-1]
y = data[:,:-1] X
Trộn (shuffle) và chia data thành 2 tập train và test.
import torch
import matplotlib.pyplot as plt
from torch import nn
from torch import optim
from torchvision import datasets, transforms
from sklearn.model_selection import train_test_split
= train_test_split(X, y, test_size=0.2, random_state=42)
X_train, X_test, y_train, y_test = torch.from_numpy(X_train).float()
X_train_tensor = torch.as_tensor(y_train, dtype=torch.int64)
y_train_tensor = torch.nn.functional.one_hot(y_train_tensor, num_classes=n_labels).float() y_train_onehot_tensor
3.1 Xây dựng mô hình
Chúng ta import những library cần thiết của PyTorch
import torch
import matplotlib.pyplot as plt
from torch import nn
from torch import optim
from torchvision import datasets, transforms
= 69
seed torch.manual_seed(seed)
<torch._C.Generator at 0x7f4a9c335130>
Khai báo mô hình gồm 2 lớp: 1. Linear: Nhận input là tensor của bài hát được tokenize thành tensor có n_features và output là tensor có số chiều là n_labels tương ứng với số lượng chủ đề cần phân loại 2. LogSoftMax: Đây là dạng activation function cho bài toán phân loại đa lớp
= nn.Sequential(nn.Linear(n_features, n_labels),nn.LogSoftmax(dim=1))
model
= nn.CrossEntropyLoss()
criterion = optim.SGD(model.parameters(), lr=0.001) optimizer
Hàm tính độ chính xác của mô hình:
def calculate_accuracy(model, X, labels):
= model(torch.as_tensor(X).float())
y_hat = y_hat.max(axis=1, keepdim=True)[1].numpy().squeeze()
preds = np.sum(preds == labels)
correct = correct / len(labels)
accuracy return accuracy
Tiến hành train model
= 300
epochs
= []
losses = []
accs for e in range(epochs+1):
optimizer.zero_grad()# tính y_hat
= model(X_train_tensor)
output
# tính loss
= criterion(output, y_train_onehot_tensor)
loss
# tính gradient
loss.backward()
# tối ưu gradient
optimizer.step()
# cập nhật loss
= loss.item()
running_loss
losses.append(running_loss)
# tính accuracy
= calculate_accuracy(model, X_test, y_test)
acc
accs.append(acc)
# in thông số training
if e % 100 == 0:
print(f"Training epoch {e} : loss: {running_loss:.3f}; accuracy: {acc:.2%}")
Training epoch 0 : loss: 9201.797; accuracy: 40.00%
Training epoch 100 : loss: 16386.709; accuracy: 76.67%
Training epoch 200 : loss: 6421.723; accuracy: 56.67%
Training epoch 300 : loss: 906.633; accuracy: 86.67%
Mô hình chúng ta sau khi train 300 epoch có accuracy 86.67%.
plt.plot(losses)'epoch')
plt.xlabel('Loss')
plt.ylabel('Model losses over time (epoch)')
plt.title( plt.show()
plt.plot(accs)'epochs')
plt.xlabel('Loss')
plt.ylabel('Model accuracy over time (epoch)')
plt.title( plt.show()
Hàm sau dùng để phân loại lời bài hát bất kỳ.
def predict_label(model, lyric):
= lyric_to_features(lyric, n_labels)
x = model(torch.as_tensor(np.array([x])).float())
y_hat = y_hat.max(axis=1, keepdim=True)[1].numpy().squeeze()
pred return genre_list[pred]
3.2 Predict
Ở đây chúng ta sẽ phân loại thử một số bài hát một ách cách thủ công nhé.
Đây là bài “Hát về anh”, thuộc thể loại cách mạng. Mô hình đã phân loại chính xác trường hợp này.
= '''Hát Về Anh
lyric Một ba lô, cây súng trên vai,
Người chiến sĩ quen với gian lao,
Ngày dài đêm thâu vẫn có những người lính trẻ,
Nặng tình quê hương canh giữ trên miền đất mẹ.
Rừng âm u, mây núi mênh mông
Ngày nắng cháy, đêm giá lạnh đầy.
Rừng mờ sương khuya bóng tối quân thù trước mặt,
Nặng tình non sông anh dâng trọn tuổi đời thanh xuân.
Cho em thơ ngủ ngon và vui bước sớm hôm đến trường
Cho yên vui mùa xuân đôi lứa còn hẹn hò ước mơ
Ðã có những hy sinh khó nói hết bằng lời
Nên đọng lại trong tôi những nghĩ suy.
Cho tôi bài ca về người chiến sĩ nơi tuyến đầu.
Nơi biên cường rừng sâu, anh âm thầm chịu đựng gió sương.
Đã có những gian lao, đã có những nhọc nhằn
Mang trong trái tim anh trọn niềm tin.
Xin hát mãi về anh người chiến sĩ biên cương
Xin hát mãi về anh người chiến sĩ biên cương
Nghe lời bài hát Hát Về Anh
Hát Về Anh '''
predict_label(model, lyric)
'cach-mang'
Bài hát Dây Đủng Đỉnh Buồn được phân loại Quê Hương trong dữ liệu. Đây cũng là một dự đoán chính xác.
= '''Dây Đủng Đỉnh Buồn (Remix)
lyric Em đi theo chồng xa thôn làng cách biệt dòng sông.
Em đi theo chồng anh nơi này mỏi mon` đợi trông
Như dây đủng đỉnh nuôi trái tình bào tháng ngày qua.
Tình đã trọng xanh rồi người nỡ đem đi hái cho đành.
Ai xuôi chỉ mình ôm nỗi buồn cho người ta vui.
Ai xuôi chỉ mình xây duyên tình giờ đây lẻ loi.
Nhìn con nước chảy theo con thuyền lạc bến đời nhau.
Lời ước hẹn xưa giờ thì cũng xa xa cuối chân trời.
ĐK:
Đau thương thui thủi đêm trương, gió lạnh từng đêm lẻ bóng đơn côi.
Buồn miên man thầm trách cho đời lơ lửng chi rồi bỏ bạn mình ên.
Yêu thương xin trả cho người nuốt lệ nhìn theo đám cưới người ta.
Để bên đây đủng đỉnh u buồn, sao mang ân tình trao tặng người ta.
Nghe lời bài hát Dây Đủng Đỉnh Buồn (Remix)
Dây Đủng Đỉnh Buồn (Remix)'''
predict_label(model, lyric)
'que-huong'
Bài hát thiếu nhi này cũng được phân loại đúng:
= '''Ai yêu bác Hồ Chí Minh hơn thiếu nhi Việt Nam
lyric Bác chúng em dáng cao cao, người thanh thanh
Bác chúng em mắt như sao, râu hơi dài
Bác chúng em nước da nâu vì sương gió
Bác chúng em thề cương quyết trả thù nhà
Hồ Chí Minh kính yêu, chúng em kính yêu Bác Hồ Chí Minh trọn một đời
Hồ Chí Minh kính yêu Bác đã bao phen bôn ba nước ngoài vì giống nòi
Bác nay tuy đã già rồi
Già rồi nhưng vẫn vui tươi
Ngày ngày chúng cháu ước mơ
Mong sao Bác sống muôn đời để dẫn dắt nhi đồng thành người và kiến thiết nước nhà bằng Người
Hồ Chí Minh kính yêu, chúng em kính yêu Bác Hồ Chí Minh trọn một đời
Hồ Chí Minh kính yêu, chúng em ước sao Bác Hồ Chí Minh sống muôn đời
Ai Yêu Bác Hồ Chí Minh Hơn Chúng Em Nhi Đồng'''
predict_label(model, lyric)
'thieu-nhi'
Bài hát “Ánh nắng của anh” được phân loại chính xác cho thể loại “Trẻ”
= '''Những phút giâу trôi qua tầm taу
lyric Ϲhờ một ai đó đến bên anh
Lặng nghe những tâm tư nàу
Là tia nắng ấm
Là em đến bên anh cho vơi đi ưu phiền ngàу hôm qua
Nhẹ nhàng xóa đi bao mâу đen vâу quanh cuộc đời nơi anh
Phút giâу anh mong đến tình уêu ấу
Giờ đâу là em, người anh mơ ước bao đêm
Ѕẽ luôn thật gần bên em
Ѕẽ luôn là vòng taу ấm êm
Ѕẽ luôn là người уêu em
Ϲùng em đi đến chân trời
Lắng nghe từng nhịp tim anh
Lắng nghe từng lời anh muốn nói
Vì em luôn đẹp nhất khi em cười'''
predict_label(model, lyric)
'tre'
Bây giờ mình sẽ lấy 2 bài hát không trong tập dữ liệu
Bài thứ nhất là một bài nhạc trẻ mới nổi gần đây “Anh Chưa Thương Em Đến Vậy Đâu”:
= '''Sao mình không gạt bỏ đi hết những lời nói ngoài kia
lyric Và sao mình không gạt bỏ đi hết những định kiến ngoài kia
Giữa ngân hà em biết đâu là
Biết đâu là thế gian này mà
Mình bên nhau, được yêu nhau, được trao nhau, tình yêu sâu trái tim đậm sâu
Giữa ngân hà em biết đâu là
Biết đâu một sớm mai khi mà
Cần bao lâu, chờ bao lâu, đợi bao lâu, tình trao nhau mãi thôi đậm sâu
Giữa ngân hà, giữa ngân hà, giữa ngân hà
Biết đâu là, biết đâu là, biết đâu là
Hành tinh của hai chúng ta
Một nơi của riêng chúng ta
Giữa ngân hà, giữa ngân hà, giữa ngân hà
Biết đâu là, biết đâu là, biết đâu là
Hành tinh của hai chúng ta
Ở 1 thế giới còn rất xa'''
predict_label(model, lyric)
'tre'
Với bài hát “Bước qua mùa cô đơn”:
= '''Mùa thu rơi vào em, vào trong giấc mơ hôm qua
lyric Mùa thu ôm mình em, chạy xa vòng tay vội vã
Lời em nói ngày xưa đâu đây
Vẫn âm thầm chìm vào trong mây
Đến bao giờ, dặn lòng anh không mong nhớ
Mùa thu rơi vào em, vào trong chiếc hôn ngây thơ
Mùa thu không cần anh, vì em giờ đây còn mãi hững hờ
Ngày mai kia nếu có phút giây vô tình thấy nhau sẽ nói câu gì...
Hay ta chỉ nhìn
Lặng lẽ
Đi qua
Chào cơn mưa
Làm sao cứ kéo ta quay lại
Những rung động con tim
Lần đầu hai ta gặp gỡ'''
predict_label(model, lyric)
'tre'
4 Phương pháp 2: Naive Bayes
Đây là phương pháp dựa vào xác suất thay vì đạo hàm như logistic regression.
Đầu tiên, chúng ta sẽ tạo dựng corpus của lời bài hát và label (thể loại) của chúng:
import glob
from underthesea import word_tokenize
# build freq dict with key pair as (word, label)
= []
corpus = []
labels
for label, genre in enumerate(genre_list):
= os.path.join(DATA_FOLDER, genre)
lyric_folder for filepath in glob.glob(f"{lyric_folder}/*txt"):
with open(filepath, 'r') as file:
= preprocess_lyric(file.read())
lyric
corpus.append(lyric)
labels.append(label)
# shuffle data
import random
= list(zip(corpus, labels))
bundle
random.shuffle(bundle)= zip(*bundle)
corpus, labels = list(corpus)
corpus = list(labels) labels
Với corpus và labels đã tìm built ở trên, chúng ta tiến hành xây dựng dictionary freqs. Cái này giúp tính xác suất likelihood ở những bước sau:
def build_freqs_dict(corpus, labels):
= {}
freqs for text, label in zip(corpus, labels):
for word in word_tokenize(preprocess_lyric(text)):
= (word, label)
key_pair if key_pair not in freqs:
= 1
freqs[key_pair] else:
+= 1
freqs[key_pair] return freqs
Tiếp tục, xây dựng hàm để tính likelihood cho từng từ, phân loại theo labels:
def calc_likelihood(freqs, labels):
= set([key_pair[0] for key_pair in freqs.keys()])
vocab = len(vocab)
V = {}
Ns = set(labels)
unique_labels for label in unique_labels:
= len([key_pair for key_pair in freqs if key_pair[1]==label])
Ns[label] = {}
p_likelihood for word in vocab:
for label in unique_labels:
= freqs.get((word, label), 0)
freq = Ns[label]
N = (freq + 1) / (N + V)
likelihood if word not in p_likelihood:
= {label: likelihood}
p_likelihood[word] else:
= likelihood
p_likelihood[word][label] return p_likelihood
Tính xác suất Prior cho từng label:
def calc_prior_prop(y):
= {}
priors = np.unique(y, return_counts=True)
labels, counts = sum(counts)
total for label, count in zip(labels, counts):
= count / total
priors[label] return priors
Và mô hình được “train” với hàm sau:
def train_naive_bayes(X_train, y_train):
= build_freqs_dict(corpus=X_train, labels=y_train)
freqs = calc_prior_prop(y_train)
priors = calc_likelihood(freqs, y_train)
likelihood
return priors, likelihood
Sau khi có đủ các hàm cần thiết, chúng ta bắt đầu split dữ liệu và train:
= int(len(labels) * 0.8)
train_size = corpus[:train_size]
X_train = labels[:train_size]
y_train
= corpus[train_size:]
X_test = labels[train_size:]
y_test
= train_naive_bayes(X_train, y_train) p_priors, p_likelihood
Tiếp tục, chúng ta viết hàm để phân loại cho một lyric cho trước. Lần đầu khi mình viết hàm này thì mình không dùng log, và kết quả là hàm lúc nào cũng trả về xác suất bằng 0. Sau một hồi debug thì mình phát hiện ra do likelihood của mỗi từ đều gần bằng 0, nên nếu nhân chúng lại với số lượng lớn thì kết quả lúc nào cũng bằng 0.
Thế nên, mình sử dụng hàm log để tránh tình trạng này:
from numpy import log
def predict_naive_bayes(lyric, p_priors, p_likelihood):
= {}
preds = word_tokenize(preprocess_lyric(lyric))
words # print(words)
for label, prior in p_priors.items():
= log(prior)
p for word in words:
if word in p_likelihood:
+= log(p_likelihood[word][label])
p = p
preds[label] = -1
predicted_label = min(preds.values())
predicted_logprob for label, prop in preds.items():
if prop > predicted_logprob:
= label
predicted_label = prop
predicted_logprob
return predicted_label
Chúng ta thử tính xem mô hình có độ chính xác là bao nhiêu nhé:
# test accuracy
= 0
correct = len(y_test)
total
for text, label in zip(X_test, y_test):
= predict_naive_bayes(text, p_priors, p_likelihood)
pred += (pred == label)
correct
print(f"Accuracy: {correct} / {total} = {correct/total:.2%}")
Accuracy: 16 / 30 = 53.33%
Sau vài lần chạy thì mô hình đạt độ chính xác 30 ~ 53.33%.
4.0.1 Một số ví dụ
= '''Hát Về Anh
lyric Một ba lô, cây súng trên vai,
Người chiến sĩ quen với gian lao,
Ngày dài đêm thâu vẫn có những người lính trẻ,
Nặng tình quê hương canh giữ trên miền đất mẹ.
Rừng âm u, mây núi mênh mông
Ngày nắng cháy, đêm giá lạnh đầy.
Rừng mờ sương khuya bóng tối quân thù trước mặt,
Nặng tình non sông anh dâng trọn tuổi đời thanh xuân.
Cho em thơ ngủ ngon và vui bước sớm hôm đến trường
Cho yên vui mùa xuân đôi lứa còn hẹn hò ước mơ
Ðã có những hy sinh khó nói hết bằng lời
Nên đọng lại trong tôi những nghĩ suy.
Cho tôi bài ca về người chiến sĩ nơi tuyến đầu.
Nơi biên cường rừng sâu, anh âm thầm chịu đựng gió sương.
Đã có những gian lao, đã có những nhọc nhằn
Mang trong trái tim anh trọn niềm tin.
Xin hát mãi về anh người chiến sĩ biên cương
Xin hát mãi về anh người chiến sĩ biên cương
Nghe lời bài hát Hát Về Anh
Hát Về Anh '''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'cach-mang'
Bài hát Dây Đủng Đỉnh Buồn được phân loại Quê Hương trong dữ liệu. Đây cũng là một dự đoán chính xác.
= '''Dây Đủng Đỉnh Buồn (Remix)
lyric Em đi theo chồng xa thôn làng cách biệt dòng sông.
Em đi theo chồng anh nơi này mỏi mon` đợi trông
Như dây đủng đỉnh nuôi trái tình bào tháng ngày qua.
Tình đã trọng xanh rồi người nỡ đem đi hái cho đành.
Ai xuôi chỉ mình ôm nỗi buồn cho người ta vui.
Ai xuôi chỉ mình xây duyên tình giờ đây lẻ loi.
Nhìn con nước chảy theo con thuyền lạc bến đời nhau.
Lời ước hẹn xưa giờ thì cũng xa xa cuối chân trời.
ĐK:
Đau thương thui thủi đêm trương, gió lạnh từng đêm lẻ bóng đơn côi.
Buồn miên man thầm trách cho đời lơ lửng chi rồi bỏ bạn mình ên.
Yêu thương xin trả cho người nuốt lệ nhìn theo đám cưới người ta.
Để bên đây đủng đỉnh u buồn, sao mang ân tình trao tặng người ta.
Nghe lời bài hát Dây Đủng Đỉnh Buồn (Remix)
Dây Đủng Đỉnh Buồn (Remix)'''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'que-huong'
Tuy nhiên, thì bài hát thiếu nhi này lại bị phân loại sai:
= '''Ai yêu bác Hồ Chí Minh hơn thiếu nhi Việt Nam
lyric Bác chúng em dáng cao cao, người thanh thanh
Bác chúng em mắt như sao, râu hơi dài
Bác chúng em nước da nâu vì sương gió
Bác chúng em thề cương quyết trả thù nhà
Hồ Chí Minh kính yêu, chúng em kính yêu Bác Hồ Chí Minh trọn một đời
Hồ Chí Minh kính yêu Bác đã bao phen bôn ba nước ngoài vì giống nòi
Bác nay tuy đã già rồi
Già rồi nhưng vẫn vui tươi
Ngày ngày chúng cháu ước mơ
Mong sao Bác sống muôn đời để dẫn dắt nhi đồng thành người và kiến thiết nước nhà bằng Người
Hồ Chí Minh kính yêu, chúng em kính yêu Bác Hồ Chí Minh trọn một đời
Hồ Chí Minh kính yêu, chúng em ước sao Bác Hồ Chí Minh sống muôn đời
Ai Yêu Bác Hồ Chí Minh Hơn Chúng Em Nhi Đồng'''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'cach-mang'
Bài hát “Ánh nắng của anh” được phân loại chính xác cho thể loại “Trẻ”
= '''Những phút giâу trôi qua tầm taу
lyric Ϲhờ một ai đó đến bên anh
Lặng nghe những tâm tư nàу
Là tia nắng ấm
Là em đến bên anh cho vơi đi ưu phiền ngàу hôm qua
Nhẹ nhàng xóa đi bao mâу đen vâу quanh cuộc đời nơi anh
Phút giâу anh mong đến tình уêu ấу
Giờ đâу là em, người anh mơ ước bao đêm
Ѕẽ luôn thật gần bên em
Ѕẽ luôn là vòng taу ấm êm
Ѕẽ luôn là người уêu em
Ϲùng em đi đến chân trời
Lắng nghe từng nhịp tim anh
Lắng nghe từng lời anh muốn nói
Vì em luôn đẹp nhất khi em cười'''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'tre'
Bây giờ mình sẽ lấy 2 bài hát không trong tập dữ liệu
Bài thứ nhất là một bài nhạc trẻ mới nổi gần đây “Anh Chưa Thương Em Đến Vậy Đâu”:
= '''Sao mình không gạt bỏ đi hết những lời nói ngoài kia
lyric Và sao mình không gạt bỏ đi hết những định kiến ngoài kia
Giữa ngân hà em biết đâu là
Biết đâu là thế gian này mà
Mình bên nhau, được yêu nhau, được trao nhau, tình yêu sâu trái tim đậm sâu
Giữa ngân hà em biết đâu là
Biết đâu một sớm mai khi mà
Cần bao lâu, chờ bao lâu, đợi bao lâu, tình trao nhau mãi thôi đậm sâu
Giữa ngân hà, giữa ngân hà, giữa ngân hà
Biết đâu là, biết đâu là, biết đâu là
Hành tinh của hai chúng ta
Một nơi của riêng chúng ta
Giữa ngân hà, giữa ngân hà, giữa ngân hà
Biết đâu là, biết đâu là, biết đâu là
Hành tinh của hai chúng ta
Ở 1 thế giới còn rất xa'''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'tre'
Với bài hát “Bước qua mùa cô đơn”:
= '''Mùa thu rơi vào em, vào trong giấc mơ hôm qua
lyric Mùa thu ôm mình em, chạy xa vòng tay vội vã
Lời em nói ngày xưa đâu đây
Vẫn âm thầm chìm vào trong mây
Đến bao giờ, dặn lòng anh không mong nhớ
Mùa thu rơi vào em, vào trong chiếc hôn ngây thơ
Mùa thu không cần anh, vì em giờ đây còn mãi hững hờ
Ngày mai kia nếu có phút giây vô tình thấy nhau sẽ nói câu gì...
Hay ta chỉ nhìn
Lặng lẽ
Đi qua
Chào cơn mưa
Làm sao cứ kéo ta quay lại
Những rung động con tim
Lần đầu hai ta gặp gỡ'''
genre_list[predict_naive_bayes(lyric, p_priors, p_likelihood)]
'tre'
Khá thú vị rằng, mô hình Naive Bayes dù đạt độ chính xác thấp hơn Logistic Regression, nhưng khi test thủ công lại cho kết quả tương tự.
5 [Update] Phương pháp 3: Genetic Algorithm - Giải thuật di truyền
Đây cũng là một giải thuật thú vị cho bài toán phân loại. Ở đây mình xài thư viện pygad để tập trung vào việc chọn tham số thay vì code tay lại mô hình.
!pip install -q pygad
Viết hàm softmax dùng cho bài toán phân loại đa lớp:
def softmax(vec):
=1e-7
epsilon= np.exp(np.clip(vec, a_min=1e-7, a_max=300))
e_vec return e_vec / (np.sum(e_vec) + epsilon)
Mình chưa tìm ra cách để pygad hỗ trợ matrix, nên dùng giải pháp truyền vào 1 vector, rồi sau đó khi tính predict thì sẽ reshape lại thành ma trận:
=(n_features, n_labels)
solution_shape
def predict(theta, X):
= np.reshape(theta, newshape=solution_shape)
theta_reshaped = softmax(X @ theta_reshaped).argmax(axis=1)
y_hat return y_hat
Hàm fitness này chủ yếu là tính độ chính xác (accuracy):
def compute_fitness(theta, theta_idx):
= predict(theta, X_train)
predictions = np.sum(np.equal(predictions, y_train))
corrects return corrects / len(predictions)
Đây là tham số sẽ dùng để train. Mình phát hiện rằng sử dụng nhiều cá thể hơn và mating nhiều hơn thì mô hình sẽ hội tụ nhanh hơn:
= compute_fitness
fitness_function
= 100
num_generations
= 1500
sol_per_pop = int(.8 * sol_per_pop)
num_parents_mating
= n_features * n_labels
num_genes
= -100
init_range_low = 100
init_range_high
# parent_selection_type = "sss"
= "rank"
parent_selection_type
= int(.05 * sol_per_pop)
keep_parents = int(.3 * sol_per_pop)
keep_elitism
= "single_point"
crossover_type
= "random"
mutation_type = 80 mutation_percent_genes
Với pygad thì việc train rất đơn giản, chỉ là khởi tạo với tham số như trên rồi gọi hàm run():
import pygad
= pygad.GA(num_generations=num_generations,
ga_instance =True,
save_best_solutions=False,
allow_duplicate_genes=num_parents_mating,
num_parents_mating=fitness_function,
fitness_func=sol_per_pop,
sol_per_pop=num_genes,
num_genes=init_range_low,
init_range_low=init_range_high,
init_range_high=parent_selection_type,
parent_selection_type=keep_parents,
keep_parents=keep_elitism,
keep_elitism=crossover_type,
crossover_type=mutation_type,
mutation_type=mutation_percent_genes)
mutation_percent_genes
ga_instance.run()
Hàm sau để tính accuracy trên tập test. Hàm fitness ở trên là ở tập train
def compute_accuracy(theta):
= predict(theta, X_test)
predictions = np.sum(np.equal(predictions, y_test))
corrects return corrects / len(predictions)
Mô hình hội tụ chỉ với hoảng 40 thế hệ:
= ga_instance.plot_fitness() fig
Vì accuracy trên tập train sẽ khác với tập test. Nên, chúng ta sẽ loop qua các nghiệm best rồi chọn ra cái có accuracy trên test cao nhất:
= ga_instance.best_solutions
best_solutions = []
accs for sol in best_solutions:
= compute_accuracy(sol)
acc
accs.append(acc)
= np.argmax(accs)
best_sol_idx = best_solutions[best_sol_idx] best_sol
Đây là accuracy của các best solutions:
import matplotlib.pyplot as plt
plt.plot(accs) plt.show()
Có thể thấy rằng sau gen thứ 40 thì acc nhảy qua lại giữa 0.7 và 0.725
Tiếp theo, hãy xem nghiệm best nhất có độ chính xác như thế nào nhé.
= compute_accuracy(best_sol)
best_sol_acc print(f"Accuracy of the best solution: {best_sol_acc:.2%}")
print(f"Best solution:\n", best_sol.reshape(solution_shape).round(4))
Accuracy of the best solution: 73.33%
Best solution:
[[ 45.0618 -71.966 6.4488 -33.7314]
[ 43.0644 61.1347 18.9067 -91.6843]
[-27.579 3.3492 -98.9866 73.9414]
[-24.5389 29.5234 -65.0851 -15.6497]
[ -4.3924 -23.3623 -70.5927 31.2352]]
6 [Update] Phương pháp 4: Decision Tree
Với SciKit-Learn thì việc train Decision Tree vô cùng đơn giản. Đầu tiên ta sẽ import mô hình và metrics
from sklearn.tree import DecisionTreeClassifier
from sklearn import metrics
Sau đó chỉ cần gọi hàm fit để train. Dùng predict để tính predictions trên tập test:
# Create Decision Tree classifer object
= DecisionTreeClassifier()
clf
# Train Decision Tree Classifer
= clf.fit(X_train,y_train)
clf
#Predict the response for test dataset
= clf.predict(X_test) y_pred
Chỉ với vài dòng code như vậy là bạn đã có một mô hình chính xác khá cao.
print(f"Decision Tree Accuracy: {metrics.accuracy_score(y_test, y_pred):.2%}")
Decision Tree Accuracy: 70.00%
7 Kết luận
Trong project này, chúng ta đã cùng: 1. Trích xuất và xử lý dữ liệu lời bài hát 2. Xây dựng mô hình để phân loại bài hát dựa trên phần lời (lyrics) với hai mô hình phổ biến là Naive Bayes và Logistic Regression 3. So sánh độ chính xác của mô hình dựa trên tập test và cả test thủ công
Nếu bạn có hứng thú, hãy xem app demo ở đây nhé: lukang-vnsongclassifier.streamlitapp.com