.. raw:: html
Lọc Cộng tác Nơ-ron cho Cá nhân hóa Xếp hạng
============================================
.. raw:: html
Vượt ra khỏi phản hồi trực tiếp, phần này sẽ giới thiệu một framework
nơ-ron lọc cộng tác (*neural collaborative filtering framework* - NCF)
cho bài toán đề xuất sử dụng phản hồi gián tiếp. Phản hồi gián tiếp có
mặt khắp mọi nơi trong các hệ thống đề xuất. Các hành động như nhấn
chọn, mua và xem là những phản hồi gián tiếp phổ biến có thể dễ dàng thu
thập và thể hiện được sở thích của người dùng. Mô hình được giới thiệu
là phân rã ma trận nơ-ron (*neural matrix factorization*) viết tắt là
NeuMF :cite:`He.Liao.Zhang.ea.2017`, hướng tới việc giải quyết tác vụ
xếp hạng cá nhân hóa sử dụng phản hồi gián tiếp. Mô hình này tận dụng
tính linh hoạt và tính phi tuyến của mạng nơ-ron để thay thế tích vô
hướng trong phân rã ma trận, nhằm nâng cao tính biểu diễn của mô hình.
Cụ thể, mô hình này gồm hai mạng con là phân rã ma trận tổng quát
(*Generalized Matrix Factorization* - GMF) và MLP, và mô hình hóa các
tương tác theo hai mạng này thay vì các tích vô hướng đơn giản. Kết quả
đầu ra của hai mạng này được ghép nối với nhau để tính điểm dự đoán cuối
cùng. Không giống như tác vụ dự đoán đánh giá trong AutoRec, mô hình này
sinh ra danh sách đề xuất đã được xếp hạng cho từng người dùng dựa trên
phản hồi gián tiếp. Chúng ta sẽ sử dụng mất mát xếp hạng cá nhân hóa đã
được giới thiệu trong phần trước để huấn luyện mô hình này.
.. raw:: html
Mô hình NeuMF
-------------
.. raw:: html
Như đã đề cập, NeuMF kết hợp hai mạng con với nhau. GMF là một phiên bản
mạng nơ-ron tổng quát của phép phân rã ma trận, có đầu vào là tích theo
từng phần tử giữa các đặc trưng ẩn của người dùng và sản phẩm. Nó bao
gồm hai tầng nơ-ron sau:
.. math::
\mathbf{x} = \mathbf{p}_u \odot \mathbf{q}_i \\
\hat{y}_{ui} = \alpha(\mathbf{h}^\top \mathbf{x}),
.. raw:: html
trong đó :math:`\odot` là phép nhân Hadamard của hai vector.
:math:`\mathbf{P} \in \mathbb{R}^{m \times k}` và
:math:`\mathbf{Q} \in \mathbb{R}^{n \times k}` lần lượt là ma trận đặc
trưng tiềm ẩn của người dùng và sản phẩm.
:math:`\mathbf{p}_u \in \mathbb{R}^{ k}` là hàng thứ :math:`u` của ma
trận :math:`P` và :math:`\mathbf{q}_i \in \mathbb{R}^{ k}` hàng thứ
:math:`i` của ma trận :math:`Q`. :math:`\alpha` và :math:`h` ký hiệu hàm
kích hoạt và trọng số của tầng đầu ra. :math:`\hat{y}_{ui}` là điểm dự
đoán mà người dùng :math:`u` có thể đưa ra cho sản phẩm :math:`i`.
.. raw:: html
Thành phần còn lại của mô hình này là MLP. Để tăng tính linh hoạt của mô
hình, MLP không dùng chung các embedding người dùng và sản phẩm với GMF,
mà có đầu vào là vector ghép nối của hai embedding người dùng và sản
phẩm. Với các kết nối phức tạp và các phép biến đổi phi tuyến, nó có thể
ước lượng các tương tác phức tạp giữa người dùng và sản phẩm. Chính xác
hơn, MLP được định nghĩa như sau:
.. math::
\begin{aligned}
z^{(1)} &= \phi_1(\mathbf{U}_u, \mathbf{V}_i) = \left[ \mathbf{U}_u, \mathbf{V}_i \right] \\
\phi^{(2)}(z^{(1)}) &= \alpha^1(\mathbf{W}^{(2)} z^{(1)} + b^{(2)}) \\
&... \\
\phi^{(L)}(z^{(L-1)}) &= \alpha^L(\mathbf{W}^{(L)} z^{(L-1)} + b^{(L)})) \\
\hat{y}_{ui} &= \alpha(\mathbf{h}^\top\phi^L(z^{(L-1)}))
\end{aligned}
.. raw:: html
trong đó :math:`\mathbf{W}^*, \mathbf{b}^*` và :math:`\alpha^*` là ma
trận trọng số, vector hệ số điều chỉnh, và hàm kích hoạt. Hàm của tầng
tương ứng được ký hiệu là :math:`\phi^*`. Đầu ra của tầng tương ứng được
ký hiệu là :math:`\mathbf{z}^*`.
.. raw:: html
Để kết hợp các đầu ra của GMF và MLP, thay vì phép cộng đơn giản, NeuMF
ghép nối các tầng áp chót của hai mạng con để tạo thành một vector đặc
trưng có thể được truyền vào các tầng tiếp theo. Sau đó, các đầu ra sẽ
được chiếu bởi ma trận :math:`\mathbf{h}` và hàm kích hoạt sigmoid. Tầng
dự đoán có công thức như sau:
.. math::
\hat{y}_{ui} = \sigma(\mathbf{h}^\top[\mathbf{x}, \phi^L(z^{(L-1)})]).
.. raw:: html
Hình dưới đây minh họa kiến trúc mô hình NeuMF.
.. raw:: html
.. figure:: ../img/rec-neumf.svg
Minh họa mô hình NeuMF
.. code:: python
from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
import mxnet as mx
import random
import sys
npx.set_np()
.. raw:: html
Lập trình Mô hình
-----------------
.. raw:: html
Đoạn mã dưới đây lập trình mô hình NeuMF, bao gồm GMF và MLP với các
vector embedding người dùng và sản phẩm khác nhau. Kiến trúc của MLP
được quy định qua tham số ``nums_hiddens``. Hàm kích hoạt mặc định là
ReLU.
.. code:: python
class NeuMF(nn.Block):
def __init__(self, num_factors, num_users, num_items, nums_hiddens,
**kwargs):
super(NeuMF, self).__init__(**kwargs)
self.P = nn.Embedding(num_users, num_factors)
self.Q = nn.Embedding(num_items, num_factors)
self.U = nn.Embedding(num_users, num_factors)
self.V = nn.Embedding(num_items, num_factors)
self.mlp = nn.Sequential()
for num_hiddens in nums_hiddens:
self.mlp.add(nn.Dense(num_hiddens, activation='relu',
use_bias=True))
self.prediction_layer = nn.Dense(1, activation='sigmoid', use_bias=False)
def forward(self, user_id, item_id):
p_mf = self.P(user_id)
q_mf = self.Q(item_id)
gmf = p_mf * q_mf
p_mlp = self.U(user_id)
q_mlp = self.V(item_id)
mlp = self.mlp(np.concatenate([p_mlp, q_mlp], axis=1))
con_res = np.concatenate([gmf, mlp], axis=1)
return self.prediction_layer(con_res)
.. raw:: html
Tập Dữ liệu Tùy chỉnh với phép Lấy mẫu Âm
-----------------------------------------
.. raw:: html
Một bước quan trọng trong mất mát xếp hạng theo cặp là lấy mẫu âm. Với
mỗi người dùng, các sản phẩm mà người đó chưa tương tác là các sản phẩm
tiềm năng (các mục chưa được quan sát). Hàm dưới đây có đầu vào là danh
tính người dùng và các sản phẩm tiềm năng, và lấy mẫu âm các sản phẩm
ngẫu nhiên cho từng người dùng từ tập tiềm năng của người dùng đó. Trong
quá trình huấn luyện, mô hình đảm bảo rằng các sản phẩm mà một người
dùng thích sẽ được xếp hạng cao hơn các sản phẩm mà người này không
thích hoặc chưa từng tương tác.
.. code:: python
class PRDataset(gluon.data.Dataset):
def __init__(self, users, items, candidates, num_items):
self.users = users
self.items = items
self.cand = candidates
self.all = set([i for i in range(num_items)])
def __len__(self):
return len(self.users)
def __getitem__(self, idx):
neg_items = list(self.all - set(self.cand[int(self.users[idx])]))
indices = random.randint(0, len(neg_items) - 1)
return self.users[idx], self.items[idx], neg_items[indices]
.. raw:: html
Đánh giá
--------
.. raw:: html
Trong phần này, ta sẽ áp dụng chiến lược chia tách theo thời gian để xây
dựng tập huấn luyện và tập kiểm tra. Hai phép đánh giá bao gồm tỷ lệ
chọn đúng (*hit rate*) theo ngưỡng :math:`\ell`
(:math:`\text{Hit}@\ell`) cho trước và diện tích dưới đường cong ROC
(AUC) được sử dụng để đánh giá hiệu quả của mô hình. Tỷ lệ chọn đúng tại
ngưỡng :math:`\ell` cho trước với mỗi người dùng cho thấy rằng liệu sản
phẩm được đề xuất có được đưa vào danh sách :math:`\ell` sản phẩm xếp
hạng hàng đầu hay không. Định nghĩa toán học như sau:
.. math::
\text{Hit}@\ell = \frac{1}{m} \sum_{u \in \mathcal{U}} \textbf{1}(rank_{u, g_u} <= \ell),
.. raw:: html
trong đó :math:`\textbf{1}` là hàm biểu thị, hàm này bằng 1 nếu sản phẩm
nhãn gốc được xếp hạng trong danh sách :math:`\ell` sản phẩm hàng đầu,
ngược lại hàm trả về 0. :math:`rank_{u, g_u}` ký hiệu xếp hạng của sản
phẩm nhãn gốc :math:`g_u` của người dùng :math:`u` trong danh sách đề
xuất (xếp hạng lý tưởng là 1). :math:`m` là số lượng người dùng.
:math:`\mathcal{U}` là tập người dùng.
.. raw:: html
Định nghĩa AUC được mô tả dưới đây:
.. math::
\text{AUC} = \frac{1}{m} \sum_{u \in \mathcal{U}} \frac{1}{|\mathcal{I} \backslash S_u|} \sum_{j \in I \backslash S_u} \textbf{1}(rank_{u, g_u} < rank_{u, j}),
.. raw:: html
trong đó :math:`\mathcal{I}` là tập các sản phẩm. :math:`S_u` là các sản
phẩm tiềm năng của người dùng :math:`u`. Chú ý rằng có rất nhiều phép
đánh giá khác như precision, recall, hay NDCG (*Normalized Discounted
Cumulative Gain*) cũng có thể được sử dụng.
.. raw:: html
Hàm sau đây tính toán số lần chọn đúng và AUC cho mỗi người dùng.
.. code:: python
#@save
def hit_and_auc(rankedlist, test_matrix, k):
hits_k = [(idx, val) for idx, val in enumerate(rankedlist[:k])
if val in set(test_matrix)]
hits_all = [(idx, val) for idx, val in enumerate(rankedlist)
if val in set(test_matrix)]
max = len(rankedlist) - 1
auc = 1.0 * (max - hits_all[0][0]) / max if len(hits_all) > 0 else 0
return len(hits_k), auc
.. raw:: html
Sau đó, tỷ lệ chọn đúng và AUC tổng thể được tính như sau.
.. code:: python
#@save
def evaluate_ranking(net, test_input, seq, candidates, num_users, num_items,
devices):
ranked_list, ranked_items, hit_rate, auc = {}, {}, [], []
all_items = set([i for i in range(num_users)])
for u in range(num_users):
neg_items = list(all_items - set(candidates[int(u)]))
user_ids, item_ids, x, scores = [], [], [], []
[item_ids.append(i) for i in neg_items]
[user_ids.append(u) for _ in neg_items]
x.extend([np.array(user_ids)])
if seq is not None:
x.append(seq[user_ids, :])
x.extend([np.array(item_ids)])
test_data_iter = gluon.data.DataLoader(
gluon.data.ArrayDataset(*x), shuffle=False, last_batch="keep",
batch_size=1024)
for index, values in enumerate(test_data_iter):
x = [gluon.utils.split_and_load(v, devices, even_split=False)
for v in values]
scores.extend([list(net(*t).asnumpy()) for t in zip(*x)])
scores = [item for sublist in scores for item in sublist]
item_scores = list(zip(item_ids, scores))
ranked_list[u] = sorted(item_scores, key=lambda t: t[1], reverse=True)
ranked_items[u] = [r[0] for r in ranked_list[u]]
temp = hit_and_auc(ranked_items[u], test_input[u], 50)
hit_rate.append(temp[0])
auc.append(temp[1])
return np.mean(np.array(hit_rate)), np.mean(np.array(auc))
.. raw:: html
Huấn luyện và Đánh giá Mô hình
------------------------------
.. raw:: html
Hàm huấn luyện được định nghĩa như sau. Ta huấn luyện mô hình theo từng
cặp.
.. code:: python
#@save
def train_ranking(net, train_iter, test_iter, loss, trainer, test_seq_iter,
num_users, num_items, num_epochs, devices, evaluator,
candidates, eval_step=1):
timer, hit_rate, auc = d2l.Timer(), 0, 0
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1],
legend=['test hit rate', 'test AUC'])
for epoch in range(num_epochs):
metric, l = d2l.Accumulator(3), 0.
for i, values in enumerate(train_iter):
input_data = []
for v in values:
input_data.append(gluon.utils.split_and_load(v, devices))
with autograd.record():
p_pos = [net(*t) for t in zip(*input_data[0:-1])]
p_neg = [net(*t) for t in zip(*input_data[0:-2],
input_data[-1])]
ls = [loss(p, n) for p, n in zip(p_pos, p_neg)]
[l.backward(retain_graph=False) for l in ls]
l += sum([l.asnumpy() for l in ls]).mean()/len(devices)
trainer.step(values[0].shape[0])
metric.add(l, values[0].shape[0], values[0].size)
timer.stop()
with autograd.predict_mode():
if (epoch + 1) % eval_step == 0:
hit_rate, auc = evaluator(net, test_iter, test_seq_iter,
candidates, num_users, num_items,
devices)
animator.add(epoch + 1, (hit_rate, auc))
print(f'train loss {metric[0] / metric[1]:.3f}, '
f'test hit rate {float(hit_rate):.3f}, test AUC {float(auc):.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(devices)}')
.. raw:: html
Lúc này ta có thể nạp tập dữ liệu MovieLens 100k và huấn luyện mô hình.
Vì tập dữ liệu MovieLens chỉ chứa các đánh giá xếp hạng, ta sẽ nhị phân
hóa các đánh giá xếp hạng này thành 0 và 1 với một vài mất mát về độ
chính xác. Nếu một người dùng đã đánh giá một sản phẩm, ta xem phản hồi
gián tiếp bằng 1, bằng 0 nếu ngược lại. Hành động đánh giá một sản phẩm
có thể được xem như là một hình thức cung cấp phản hồi gián tiếp. Ở đây,
ta phân tách tập dữ liệu ở chế độ ``seq-aware``, trong đó các sản phẩm
được tương tác gần đây nhất sẽ được tách ra để kiểm tra.
.. code:: python
batch_size = 1024
df, num_users, num_items = d2l.read_data_ml100k()
train_data, test_data = d2l.split_data_ml100k(df, num_users, num_items,
'seq-aware')
users_train, items_train, ratings_train, candidates = d2l.load_data_ml100k(
train_data, num_users, num_items, feedback="implicit")
users_test, items_test, ratings_test, test_iter = d2l.load_data_ml100k(
test_data, num_users, num_items, feedback="implicit")
train_iter = gluon.data.DataLoader(
PRDataset(users_train, items_train, candidates, num_items ), batch_size,
True, last_batch="rollover", num_workers=d2l.get_dataloader_workers())
.. raw:: html
Sau đó, ta tạo một mô hình và khởi tạo nó. Ta sử dụng mạng MLP 3 tầng
với kích thước ẩn không đổi bằng 10.
.. code:: python
devices = d2l.try_all_gpus()
net = NeuMF(10, num_users, num_items, nums_hiddens=[10, 10, 10])
net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))
.. raw:: html
Đoạn mã nguồn dưới đây được sử dụng để huấn luyện mô hình.
.. code:: python
lr, num_epochs, wd, optimizer = 0.01, 10, 1e-5, 'adam'
loss = d2l.BPRLoss()
trainer = gluon.Trainer(net.collect_params(), optimizer,
{"learning_rate": lr, 'wd': wd})
train_ranking(net, train_iter, test_iter, loss, trainer, None, num_users,
num_items, num_epochs, devices, evaluate_ranking, candidates)
.. parsed-literal::
:class: output
train loss 33.964, test hit rate 0.075, test AUC 0.531
33.9 examples/sec on [gpu(0)]
.. figure:: output_neumf_vn_127b54_17_1.svg
Tóm tắt
-------
.. raw:: html
- Bổ sung thêm tính phi tuyến vào mô hình phân rã ma trận giúp cải
thiện khả năng và tính hiệu quả của mô hình.
- NeuMF là sự kết hợp giữa mô hình phân rã ma trận và perceptron đa
tầng. Perceptron đa tầng có đầu vào là vector được ghép nối bởi
embedding người dùng và embedding sản phẩm.
Bài tập
-------
.. raw:: html
- Thay đổi kích thước của các nhân tố tiềm ẩn. Kích thước này tác động
như thế nào đến chất lượng mô hình?
- Thay đổi kiến trúc của MLP (ví dụ: số lượng tầng, số lượng nơ-ron của
mỗi tầng) và cho biết tác động đến chất lượng của mô hình.
- Hãy thử các bộ tối ưu, tốc độ học và tốc độ suy giảm trọng số khác
nhau.
- Hãy thử sử dụng mất mát hinge được định nghĩa ở phần trước để tối ưu
mô hình này.
Thảo luận
---------
- `Tiếng Anh - MXNet `__
- `Tiếng Việt `__
Những người thực hiện
---------------------
Bản dịch trong trang này được thực hiện bởi:
- Đoàn Võ Duy Thanh
- Nguyễn Văn Quang
- Đỗ Trường Giang
- Nguyễn Văn Cường
*Cập nhật lần cuối: 06/10/2020. (Cập nhật lần cuối từ nội dung gốc:
20/09/2020)*