.. raw:: html Hệ thống Đề xuất có Nhận thức về Chuỗi ====================================== .. raw:: html Trong phần trước, ta trừu tượng hóa tác vụ đề xuất dưới dạng một bài toán hoàn thiện ma trận mà không xét đến hành vi ngắn hạn của người dùng. Trong phần này, chúng tôi sẽ giới thiệu một mô hình đề xuất cân nhắc đến nhật ký tương tác được sắp xếp theo trình tự thời gian của người dùng. Đây là một hệ thống đề xuất có nhận thức về chuỗi (*sequence-aware recommender*) :cite:`Quadrana.Cremonesi.Jannach.2018` với đầu vào là danh sách lịch sử thao tác của người dùng đã được sắp xếp và thường đi kèm với mốc thời gian diễn ra. Nhiều bài báo gần đây đã chứng minh được lợi ích của việc tích hợp những thông tin này vào việc mô hình hóa khuôn mẫu hành vi theo thời gian của người dùng và tìm ra được khuynh hướng sở thích của họ. .. raw:: html Mô hình mà chúng tôi sẽ giới thiệu, Caser :cite:`Tang.Wang.2018`, viết tắt của mô hình đề xuất embedding chuỗi tích chập (*convolutional sequence embedding recommendation model*), kế thừa mạng nơ-ron tích chập nhằm nắm bắt khuôn mẫu động có ảnh hưởng đến những hoạt động gần đây của người dùng. Thành phần chính của Caser bao gồm một mạng tích chập ngang và một mạng tích chập dọc, nhằm lần lượt khám phá khuôn mẫu cấp liên kết (*union-level*) và cấp điểm (*point-level*) của chuỗi. Khuôn mẫu cấp điểm ám chỉ tác động của một sản phẩm riêng lẻ trong lịch sử của chuỗi lên sản phẩm mục tiêu, trong khi khuôn mẫu cấp liên kết ám chỉ ảnh hưởng của nhiều thao tác trước đó lên các mục tiêu kế tiếp. Ví dụ, việc mua sữa cùng với bơ dẫn tới xác suất mua thêm cả bột mì cao hơn so với việc chỉ mua một trong hai. Hơn nữa, sở thích chung của người dùng, hay sở thích dài hạn cũng được mô hình hóa trong những tầng kết nối đầy đủ cuối cùng, dẫn đến sở thích của người dùng được mô hình hóa một cách toàn diện hơn. Chi tiết về mô hình này sẽ được mô tả tiếp theo. .. raw:: html Kiến trúc Mô hình ----------------- .. raw:: html Trong hệ thống đề xuất có nhận thức về chuỗi, mỗi người dùng tương tác với một chuỗi các sản phẩm từ tập sản phẩm. :math:`S^u = (S_1^u, ... S_{|S_u|}^u)` ký hiệu chuỗi có trình tự. Mục tiêu của Caser là đề xuất sản phẩm bằng cách xét thị hiếu chung của người dùng cũng như dự định ngắn hạn. Giả sử ta xét :math:`L` sản phẩm trước, ma trận embedding biểu diễn những tương tác xảy ra trước bước thời gian :math:`t` có thể được xây dựng như sau: .. math:: \mathbf{E}^{(u, t)} = [ \mathbf{q}_{S_{t-L}^u} , ..., \mathbf{q}_{S_{t-2}^u}, \mathbf{q}_{S_{t-1}^u} ]^\top, .. raw:: html trong đó :math:`\mathbf{Q} \in \mathbb{R}^{n \times k}` biểu diễn embedding sản phẩm và :math:`\mathbf{q}_i` ký hiệu hàng thứ :math:`i`. :math:`\mathbf{E}^{(u, t)} \in \mathbb{R}^{L \times k}` có thể được sử dụng để suy ra sở thích nhất thời của người dùng :math:`u` tại bước thời gian :math:`t`. Ta có thể coi ma trận đầu vào :math:`\mathbf{E}^{(u, t)}` như một ảnh đầu vào của hai tầng tích chập kế tiếp. .. raw:: html Tầng tích chập ngang có :math:`d` bộ lọc ngang :math:`\mathbf{F}^j \in \mathbb{R}^{h \times k}, 1 \leq j \leq d, h = \{1, ..., L\}`, và tầng tích chập dọc có :math:`d'` bộ lọc dọc :math:`\mathbf{G}^j \in \mathbb{R}^{ L \times 1}, 1 \leq j \leq d'`. Sau một chuỗi những thao tác tích chập và gộp, ta thu được hai đầu ra: .. math:: \mathbf{o} = \text{HConv}(\mathbf{E}^{(u, t)}, \mathbf{F}) \\ \mathbf{o}'= \text{VConv}(\mathbf{E}^{(u, t)}, \mathbf{G}) , .. raw:: html trong đó :math:`\mathbf{o} \in \mathbb{R}^d` là đầu ra của mạng tích chập ngang và :math:`\mathbf{o}' \in \mathbb{R}^{kd'}` là đầu ra của mạng tích chập dọc. Để đơn giản, ta bỏ qua chi tiết của các thao tác tích chập và thao tác gộp. Chúng được nối với nhau và đưa vào một tầng nơ-ron kết nối đầy đủ để thu được dạng biểu diễn cấp cao hơn. .. math:: \mathbf{z} = \phi(\mathbf{W}[\mathbf{o}, \mathbf{o}']^\top + \mathbf{b}), .. raw:: html trong đó :math:`\mathbf{W} \in \mathbb{R}^{k \times (d + kd')}` là ma trận trọng số và :math:`\mathbf{b} \in \mathbb{R}^k` là hệ số điều chỉnh. Vector học được :math:`\mathbf{z} \in \mathbb{R}^k` chính là dạng biểu diễn cho sở thích ngắn hạn của người dùng. .. raw:: html Cuối cùng, hàm dự đoán kết hợp thị hiếu ngắn hạn và thị hiếu chung của người dùng với nhau, hàm này được định nghĩa: .. math:: \hat{y}_{uit} = \mathbf{v}_i \cdot [\mathbf{z}, \mathbf{p}_u]^\top + \mathbf{b}'_i, .. raw:: html trong đó :math:`\mathbf{V} \in \mathbb{R}^{n \times 2k}` là một ma trận embedding sản phẩm khác. :math:`\mathbf{b}' \in \mathbb{R}^n` là độ chệch đặc thù của sản phẩm. :math:`\mathbf{P} \in \mathbb{R}^{m \times k}` là ma trận embedding thị hiếu chung của người dùng. :math:`\mathbf{p}_u \in \mathbb{R}^{ k}` là hàng thứ :math:`u` của :math:`P` và :math:`\mathbf{v}_i \in \mathbb{R}^{2k}` là hàng thứ :math:`i` của :math:`\mathbf{V}`. .. raw:: html Mô hình này có thể được học với mất mát BPR hoặc mất mát Hinge. Kiến trúc của Caser được mô tả như dưới đây. .. raw:: html .. figure:: ../img/rec-caser.svg Minh họa Mô hình Caser. .. raw:: html Đầu tiên ta nhập vào những thư viện cần thiết. .. code:: python from d2l import mxnet as d2l from mxnet import 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 cho mô hình Caser. Nó bao gồm một tầng tích chập ngang, một tầng tích chập dọc, và một tầng kết nối đầy đủ. .. code:: python class Caser(nn.Block): def __init__(self, num_factors, num_users, num_items, L=5, d=16, d_prime=4, drop_ratio=0.05, **kwargs): super(Caser, self).__init__(**kwargs) self.P = nn.Embedding(num_users, num_factors) self.Q = nn.Embedding(num_items, num_factors) self.d_prime, self.d = d_prime, d # Vertical convolution layer self.conv_v = nn.Conv2D(d_prime, (L, 1), in_channels=1) # Horizontal convolution layer h = [i + 1 for i in range(L)] self.conv_h, self.max_pool = nn.Sequential(), nn.Sequential() for i in h: self.conv_h.add(nn.Conv2D(d, (i, num_factors), in_channels=1)) self.max_pool.add(nn.MaxPool1D(L - i + 1)) # Fully-connected layer self.fc1_dim_v, self.fc1_dim_h = d_prime * num_factors, d * len(h) self.fc = nn.Dense(in_units=d_prime * num_factors + d * L, activation='relu', units=num_factors) self.Q_prime = nn.Embedding(num_items, num_factors * 2) self.b = nn.Embedding(num_items, 1) self.dropout = nn.Dropout(drop_ratio) def forward(self, user_id, seq, item_id): item_embs = np.expand_dims(self.Q(seq), 1) user_emb = self.P(user_id) out, out_h, out_v, out_hs = None, None, None, [] if self.d_prime: out_v = self.conv_v(item_embs) out_v = out_v.reshape(out_v.shape[0], self.fc1_dim_v) if self.d: for conv, maxp in zip(self.conv_h, self.max_pool): conv_out = np.squeeze(npx.relu(conv(item_embs)), axis=3) t = maxp(conv_out) pool_out = np.squeeze(t, axis=2) out_hs.append(pool_out) out_h = np.concatenate(out_hs, axis=1) out = np.concatenate([out_v, out_h], axis=1) z = self.fc(self.dropout(out)) x = np.concatenate([z, user_emb], axis=1) q_prime_i = np.squeeze(self.Q_prime(item_id)) b = np.squeeze(self.b(item_id)) res = (x * q_prime_i).sum(1) + b return res .. raw:: html Tập dữ liệu Tuần tự với phép Lấy mẫu Âm --------------------------------------- .. raw:: html Để xử lý dữ liệu tương tác tuần tự, ta cần lập trình lại lớp Dataset. Đoạn mã sau đây tạo một lớp dataset mới có tên là ``SeqDataset``. Với mỗi mẫu, lớp này trả về id của người dùng, một chuỗi :math:`L` sản phẩm mà người này đã tương tác trước đó và sản phẩm tiếp theo mà người này sẽ tương tác làm mục tiêu. Hình dưới đây mô tả quá trình nạp dữ liệu với một người dùng. Giả sử người dùng này thích 9 bộ phim, ta sắp xếp 9 bộ phim này theo thứ tự thời gian. Bộ phim cuối cùng được bỏ ra ngoài để làm sản phẩm kiểm tra. Với 8 bộ phim còn lại, ta có thể tạo ba mẫu huấn luyện, với mỗi mẫu bao gồm một chuỗi gồm năm (:math:`L=5`) bộ phim và bộ phim kế tiếp làm mục tiêu. Các mẫu âm cũng có thể được đưa vào trong tập dữ liệu tuỳ chỉnh. .. raw:: html .. figure:: ../img/rec-seq-data.svg Minh họa quá trình sinh dữ liệu .. code:: python class SeqDataset(gluon.data.Dataset): def __init__(self, user_ids, item_ids, L, num_users, num_items, candidates): user_ids, item_ids = np.array(user_ids), np.array(item_ids) sort_idx = np.array(sorted(range(len(user_ids)), key=lambda k: user_ids[k])) u_ids, i_ids = user_ids[sort_idx], item_ids[sort_idx] temp, u_ids, self.cand = {}, u_ids.asnumpy(), candidates self.all_items = set([i for i in range(num_items)]) [temp.setdefault(u_ids[i], []).append(i) for i, _ in enumerate(u_ids)] temp = sorted(temp.items(), key=lambda x: x[0]) u_ids = np.array([i[0] for i in temp]) idx = np.array([i[1][0] for i in temp]) self.ns = ns = int(sum([c - L if c >= L + 1 else 1 for c in np.array([len(i[1]) for i in temp])])) self.seq_items = np.zeros((ns, L)) self.seq_users = np.zeros(ns, dtype='int32') self.seq_tgt = np.zeros((ns, 1)) self.test_seq = np.zeros((num_users, L)) test_users, _uid = np.empty(num_users), None for i, (uid, i_seq) in enumerate(self._seq(u_ids, i_ids, idx, L + 1)): if uid != _uid: self.test_seq[uid][:] = i_seq[-L:] test_users[uid], _uid = uid, uid self.seq_tgt[i][:] = i_seq[-1:] self.seq_items[i][:], self.seq_users[i] = i_seq[:L], uid def _win(self, tensor, window_size, step_size=1): if len(tensor) - window_size >= 0: for i in range(len(tensor), 0, - step_size): if i - window_size >= 0: yield tensor[i - window_size:i] else: break else: yield tensor def _seq(self, u_ids, i_ids, idx, max_len): for i in range(len(idx)): stop_idx = None if i >= len(idx) - 1 else int(idx[i + 1]) for s in self._win(i_ids[int(idx[i]):stop_idx], max_len): yield (int(u_ids[i]), s) def __len__(self): return self.ns def __getitem__(self, idx): neg = list(self.all_items - set(self.cand[int(self.seq_users[idx])])) i = random.randint(0, len(neg) - 1) return (self.seq_users[idx], self.seq_items[idx], self.seq_tgt[idx], neg[i]) .. raw:: html Nạp Tập dữ liệu MovieLens 100K ------------------------------ .. raw:: html Kế tiếp, ta đọc và chia nhỏ tập dữ liệu MovieLens 100K ở chế độ nhận thức về chuỗi và nạp tập huấn luyện với bộ nạp dữ liệu tuần tự đã lập trình phía trên. .. code:: python TARGET_NUM, L, batch_size = 1, 5, 4096 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_seq_data = SeqDataset(users_train, items_train, L, num_users, num_items, candidates) train_iter = gluon.data.DataLoader(train_seq_data, batch_size, True, last_batch="rollover", num_workers=d2l.get_dataloader_workers()) test_seq_iter = train_seq_data.test_seq train_seq_data[0] .. parsed-literal:: :class: output (array(0, dtype=int32), array([241., 170., 110., 255., 4.]), array([101.]), 1069) .. raw:: html Cấu trúc dữ liệu huấn luyện được chỉ ra như trên. Phần tử đầu tiên là id người dùng, kế tiếp là danh sách ba sản phẩm đầu tiên mà người dùng này thích (:math:`L=3`), và phần tử cuối cùng là sản phẩm người dùng này thích sau ba sản phẩm trước. .. raw:: html Huấn luyện Mô hình ------------------ .. raw:: html Giờ hãy cùng huấn luyện mô hình. Ta sử dụng thiết lập giống với NeuMF trong phần trước, bao gồm tốc độ học, bộ tối ưu, và :math:`k`, để có thể so sánh kết quả. .. code:: python devices = d2l.try_all_gpus() net = Caser(10, num_users, num_items, L) net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01)) lr, num_epochs, wd, optimizer = 0.04, 8, 1e-5, 'adam' loss = d2l.BPRLoss() trainer = gluon.Trainer(net.collect_params(), optimizer, {"learning_rate": lr, 'wd': wd}) d2l.train_ranking(net, train_iter, test_iter, loss, trainer, test_seq_iter, num_users, num_items, num_epochs, devices, d2l.evaluate_ranking, candidates, eval_step=1) .. parsed-literal:: :class: output train loss 1.628, test hit rate 0.397, test AUC 0.754 66.2 examples/sec on [gpu(0)] .. figure:: output_seqrec_vn_a01f2d_9_1.svg Tóm tắt ------- .. raw:: html - Suy luận về sở thích ngắn hạn và dài hạn của một người dùng có thể giúp việc dự đoán sản phẩm tiếp theo người này thích trở nên hiệu quả hơn. - Mạng nơ-ron tích chập có thể được tận dụng để nắm bắt được sở thích ngắn hạn của người dùng từ chuỗi các tương tác. Bài tập ------- .. raw:: html - Thực hiện một nghiên cứu loại bỏ (*ablation study*) bằng cách bỏ một trong hai mạng tích chập ngang hoặc dọc, thành phần nào quan trọng hơn? - Thay đổi siêu tham số :math:`L`. Liệu lịch sử tương tác dài hơn có giúp tăng độ chính xác? - Ngoài tác vụ đề xuất nhận thức về chuỗi như chúng tôi giới thiệu ở trên, có một loại tác vụ đề xuất nhận thức về chuỗi khác được gọi là đề xuất dựa theo phiên (*session-based recommendation*) :cite:`Hidasi.Karatzoglou.Baltrunas.ea.2015`. Bạn có thể giải thích sự khác nhau giữa hai tác vụ này không? Thảo luận --------- - Tiếng Anh: `MXNet `__ - Tiếng Việt: `Diễn đàn Machine Learning Cơ Bản `__ 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 - Đỗ Trường Giang - Nguyễn Văn Cường - Phạm Hồng Vinh - Phạm Minh Đức - Nguyễn Lê Quang Nhật *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: 01/10/2020)*