Chuẩn bị tập dữ liệu

Đầu tiên, ta sẽ import các thư viện cần thiết. Nếu có thư viện nào bị thiếu, bạn có thể sử dụng pip để install.

In [1]:
import os
from math import sqrt
from itertools import islice

import numpy as np
import pandas as pd
from scipy.sparse.linalg import svds
from sklearn.metrics import mean_squared_error
from sklearn.metrics.pairwise import pairwise_distances
from sklearn.model_selection import train_test_split

Tiếp theo, ta chuyển đổi định dạng plain text ban đầu thành .csv, đọc tập dữ liệu này và quan sát tổng quan.

In [2]:
%%time
d_path = "data/kaggle_visible_evaluation_triplets.txt"
new_path = "data/song_data.csv"
with open(d_path, "r") as f1, open(new_path, "w") as f2:
    i = 0
    f2.write("user_id,song_id,listen_count\n")
    while True:
        next_n_lines = list(islice(f1, 9))
        if not next_n_lines:
            break

        # process next_n_lines: get user_id,song_id,listen_count info
        output_line = ""
        for line in next_n_lines:
            user_id, song, listen_count = line.split("\t")
            output_line += "{},{},{}\n".format(user_id, song, listen_count.strip())
        f2.write(output_line)
        
        # print status
        i += 1
        if i % 20000 == 0:
            print "%d songs converted..." % i
20000 songs converted...
40000 songs converted...
60000 songs converted...
80000 songs converted...
100000 songs converted...
120000 songs converted...
140000 songs converted...
160000 songs converted...
CPU times: user 2.38 s, sys: 117 ms, total: 2.5 s
Wall time: 2.53 s
In [3]:
def load_music_data(file_name):
    """Get reviews data, from local csv."""
    if os.path.exists(file_name):
        print("-- " + file_name + " found locally")
        df = pd.read_csv(file_name)
 
    return df
 
# Load music data with sampling fraction = 0.01 for reduce processing time.
song_data = load_music_data(new_path)
song_data = song_data.sample(frac=0.01, replace=False)
 
print "-- Explore data"
display(song_data.head())
 
n_users = song_data.user_id.unique().shape[0]
n_items = song_data.song_id.unique().shape[0]
print "Number of users = " + str(n_users) + " | Number of songs = " + str(n_items)
-- data/song_data.csv found locally
-- Explore data
user_id song_id listen_count
36249 d09a1d62965a0b34b827543f2abb9a2308dea58d SOYCYVS12A8C13F107 5
1055500 5e1c0a2733a194063aadc08332ff61b810631922 SOIVKUR12A6310F0FF 1
960831 718d27712d6e6b45f046c784fa6ba4b4dae6e307 SOHKZSM12A8C13E5D5 5
614060 70cba6322ca65c3dd771d8df5ead420ffb3f021b SONHXJK12AAF3B5290 2
1176495 65fd23351e01ee04187286d1f7bea5d68d55a5a5 SOIYIMC12AC9097E45 1
Number of users = 13293 | Number of songs = 10304

Nếu bạn áp dụng phương pháp popularity, nghĩa là hiển thị top các bài nhạc được nghe nhiều nhất. Ta có thể thực hiện như sau:

In [4]:
print "-- Showing the most popular songs in the dataset"
unique, counts = np.unique(song_data["song_id"], return_counts=True)
popular_songs = dict(zip(unique, counts))
df_popular_songs = pd.DataFrame(popular_songs.items(), columns=["Song", "Count"])
df_popular_songs = df_popular_songs.sort_values(by=["Count"], ascending=False)
df_popular_songs.head()
-- Showing the most popular songs in the dataset
Out[4]:
Song Count
7893 SOFRQTD12A81C233C0 60
4672 SONYKOW12AB01849C9 42
7155 SOSXLTC12AF72A7F54 42
7318 SOBONKR12A58A7A7E0 39
7283 SOAUWYT12A81C206F1 38

Có thể thấy SOFRQTD12A81C233C0 được nghe nhiều nhất với 60 lượt nghe. Dù bạn là ai đi chăng nữa thì hệ thống của chúng ta cũng chỉ recommend cho người dùng bằng cách hiển thị top các bài nhạc được nghe nhiều nhất. Giả sử người dùng không thích các thể loại nhạc này thì hệ thống của chúng ta đã thất bại trong việc gợi ý.

Trước khi xây dựng hệ thống recommender, ta sẽ phân chia tập dữ liệu thành tập train và tập test với tỉ lệ 0.75/0.25.

In [5]:
train_data, test_data = train_test_split(song_data, test_size=0.25)

Memory-Based Collaborative Filtering

Để có thể áp dụng phương pháp user-item filtering và item-item filtering, ta cần xây dựng ma trận user-item.

Nếu tập dữ liệu của chúng ta có 13,293 user và 10,304 bài nhạc thì ta sẽ đi xây dựng ma trận user-item 13,293 dòng và 10,304 cột dữ liệu.

Ta sẽ tiến hành chuyển đổi mã user và mã bài nhạc sang chỉ số của ma trận như sau:

In [6]:
def values_to_map_index(values):
    map_index = {}
    idx = 0
    for val in values:
        map_index[val] = idx
        idx += 1
 
    return map_index
 
user_idx = values_to_map_index(song_data.user_id.unique())
song_idx = values_to_map_index(song_data.song_id.unique())

Khi đó, ta có thể xây dựng được hai ma trận user-item, một cho train dataset, hai cho test dataset. Lưu ý, line[1] là mã user, line[2] là mã bài nhạc, line[3] là số lượt nghe.

In [7]:
train_data_matrix = np.zeros((n_users, n_items))
for line in train_data.itertuples():
    train_data_matrix[user_idx[line[1]], song_idx[line[2]]] = line[3]

test_data_matrix = np.zeros((n_users, n_items))
for line in test_data.itertuples():
    test_data_matrix[user_idx[line[1]], song_idx[line[2]]] = line[3]

Sau khi xây dựng được ma trận user-item, ta sẽ xây dựng ma trận khoảng cách để tính độ tương tự (similarity distance) giữa các item và user lẫn nhau. Ta có nhiều độ đo khoảng cách để áp dụng tính toán. Thông thường, ta sử dụng độ đo khoảng cách cosine để tính.

Cosine similiarity giữa user k và a được tính dựa vào công thức như bên dưới. Trong đó, $x_{k,m}$ là số lượt nghe bài nhạc m của user k, $x_{a,m}$ là số lượt nghe bài nhạc m của user a.

$$s^{cos}_u (u_k, u_a) = \frac{u_k \cdot u_a}{||u_k|| ||u_a||} = \frac{\sum x_{k,m} x_{a,m}}{\sqrt{\sum x^2_{k,m} x^2_{a,m}}} $$

Tương tự, ta có độ đo khoảng cách giữa item m và b như bên dưới:

$$s^{cos}_u (i_m, i_b) = \frac{i_m \cdot i_b}{||i_m|| ||i_b||} = \frac{\sum x_{a,m} x_{a,b}}{\sqrt{\sum x^2_{a,m} x^2_{a,b}}} $$

Ta có thể dùng hàm pairwise_distances của sklearn để tính cosine similarity. Chú ý, output sẽ nằm trong khoảng 0 đến 1 do số lượt nghe đều là số không âm.

In [8]:
%%time
user_similarity = pairwise_distances(train_data_matrix, metric='cosine', n_jobs=-1)
item_similarity = pairwise_distances(train_data_matrix.T, metric='cosine', n_jobs=-1)
CPU times: user 1min 51s, sys: 19.7 s, total: 2min 11s
Wall time: 3min 47s

Để có thể dự đoán số lượt nghe bài nhạc m của user k. Số lượt nghe càng cao tương đương với user quan tâm đến bài nhạc này càng nhiều. Ta có thể sắp xếp lại kết quả dự đoán để cho ra các bài nhạc gợi ý cho user này. Công thức dự đoán theo user-based CF được tính như bên dưới:

$$\hat x_{k,m} = \bar x_k + \frac{\sum_{u_a} sim_u(u_k, u_a)(x_{a,m} - \bar x_{u_a})}{\sum_{u_a} |sim_u(u_k, u_a)|}$$

Ta có thể thấy độ đo khoảng cách giữa user k và a được sử dụng như trọng số của mô hình dự đoán. User k càng giống user a bao nhiêu thì kết quả dự đoán sẽ càng gần với a bấy nhiêu. Đồng thời, giá trị dự đoán sẽ được tính dựa vào số lượt nghe trung bình của user k đối với các bài nhạc trước đó. Tương tự, item-based CF được tính như sau:

$$\hat x_{k,m} = \frac{\sum_{i_b} sim_i(i_m, i_b)(x_{k,b})}{\sum_{i_b} |sim_i(i_m, i_b)|}$$
In [9]:
def predict(ratings, similarity, type='user'):
    if type == 'user':
        mean_user_rating = ratings.mean(axis=1)
        # You use np.newaxis so that mean_user_rating has same format as ratings
        ratings_diff = (ratings - mean_user_rating[:, np.newaxis])
        pred = mean_user_rating[:, np.newaxis] + similarity.dot(ratings_diff) / np.array(
            [np.abs(similarity).sum(axis=1)]).T
    elif type == 'item':
        pred = ratings.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
    return pred

item_prediction = predict(train_data_matrix, item_similarity, type='item')
user_prediction = predict(train_data_matrix, user_similarity, type='user')

Cuối cùng, ta sẽ sử dụng Root Mean Squared Error (RMSE) để đánh giá mô hình dựa vào tập dữ liệu test.

$$RMSE = \sqrt{\frac{1}{N} \sum (x_i - \hat x_i)^2}$$

sklearn có cung cấp hàm mean_square_error (MSE) để tính toán. Do ta đánh giá mô hình dựa vào tập test nên ta cần filter prediction matrix trước bằng lệnh prediction[ground_truth.nonzero()]. Giá trị của RMSE càng nhỏ thì mô hình của chúng ta càng tốt.

In [10]:
def rmse(prediction, ground_truth):
    prediction = prediction[ground_truth.nonzero()].flatten()
    ground_truth = ground_truth[ground_truth.nonzero()].flatten()
    return sqrt(mean_squared_error(prediction, ground_truth))

print 'User-based CF RMSE: ' + str(rmse(user_prediction, test_data_matrix))
print 'Item-based CF RMSE: ' + str(rmse(item_prediction, test_data_matrix))
User-based CF RMSE: 7.03399900018
Item-based CF RMSE: 7.03442652103

Ta có thể thấy mô hình Memory-based dễ dàng cài đặt và dự đoán. Khuyết điểm của mô hình này đó là khó mở rộng cho các hệ thống lớn và không giải quyết được cold-start problem. Nghĩa là hệ thống không có khả năng dự đoán cho một user mới chưa có lượt nghe ở bất kỳ bài nhạc nào.

Model-based Collaborative Filtering

Giả sử, ta có d topics cho từng user và từng bài nhạc. Ví dụ, ta có thể mô tả bài nhạc m ($R_m$) thông qua phần trăm các topic bên trong đó như 0.3% jazz, 0.01% pop, 1.5% dance, ... Tương tự, ta có thể mô tả gu âm nhạc của user u ($L_u$) thông qua phần trăm các topic như 2.5% jazz, 0% pop, 0.8% dance, ... Khi đó, ta sẽ có hai vector tương tự như sau:

$$R_m = [0.3, 0.01, 1.5, ...], L_u = [2.5, 0, 0.8, ...]$$

Và mức độ quan tâm của user đối với bài nhạc này sẽ là tích vector của $R_m, L_u$. Khi đó, ta có thể sắp xếp các bài nhạc theo mức độ quan tâm này cho user.

Trong bài viết này, ta sẽ áp dụng Singular value decomposition (SVD) để phân tích ma trận user-item thành tích các ma trận thành phần (matrix factorization).

Để dự đoán, ta chỉ việc tính tích các ma trận lại với nhau.

In [11]:
# get SVD components from train matrix. Choose k, rank of S.
u, s, vt = svds(train_data_matrix, k=20)
s_diag_matrix = np.diag(s)
X_pred = np.dot(np.dot(u, s_diag_matrix), vt)
print 'User-based CF MSE: ' + str(rmse(X_pred, test_data_matrix))
User-based CF MSE: 7.0344482344

Model-based CF có khả năng mở rộng và giải quyết vấn đề ma trận bị rời rạc (sparsity level) tốt hơn memory-based models. Nhưng vẫn không giải quyết được vấn đề khi có user mới chưa có nghe bài nhạc nào. Bạn có thể giải quyết vấn đề cold-start bằng các cách gom nhóm người dùng mới vào nhóm người dùng cũ, dựa vào các feature ta đã xây dựng như thời điểm nghe nhạc, thông tin tài khoản, các chủ đề quan tâm, ...

Hiện nay, bạn có thể cài đặt recommender system một cách dễ dàng hơn thông qua các thư viện hỗ trợ sẵn như Crab - Recommender systems in Python hay Spark Mlib cho các hệ thống Big Data.

Tham khảo thêm