diff --git a/tutorials/kdd22/README.md b/tutorials/kdd22/README.md
new file mode 100644
index 0000000..2f3e3d2
--- /dev/null
+++ b/tutorials/kdd22/README.md
@@ -0,0 +1,24 @@
+# KDD 2022 Hands-on Tutorial - PECOS: Prediction for Enormous and Correlated Output Spaces
+
+In this tutorial, we will introduce several key functions and features of the PECOS library.
+By way of real-world examples, the attendees will learn how to efficiently train large-scale machine learning models for enormous output spaces, and obtain predictions in less than 1 millisecond for a data input with million labels, in the context of product recommendation and natural language processing.
+We will also show the flexibility of dealing with diverse machine learning problems and data formats with assorted built-in utilities in PECOS.
+By the end of the tutorial, we believe that attendees will be easily capable of adopting certain concepts to their own projects and address different machine learning problems with enormous output spaces.
+
+* Presenters: Hsiang-Fu Yu (Amazon Search), Jiong Zhang (Amazon Search), Wei-Cheng Chang (Amazon Search), Jyun-Yu Jiang (Amazon Search), and Cho-Jui Hsieh (UCLA)
+
+* Contributer: Wei Li (Amazon Search)
+
+## Agenda
+
+| Time | Session | Material |
+|---|---|---|
+| 8:00 AM - 8:30 AM | Check-in and Environment Setup | |
+| 8:30 AM - 8:50 AM | Session 1: Introduction to PECOS | |
+| 8:50 AM - 9:30 AM | Session 2: Extreme Multi-label Ranking with PECOS | [Notebook](https://github.com/amzn/pecos/blob/tutorials/kdd22/Session%202%20Extreme%20Multi-label%20Ranking%20with%20PECOS.ipynb) |
+| 9:30 AM - 10:00 AM | Coffee Break | |
+| 10:00 AM - 10:30 AM | Session 3: Approximate Nearest Neighbor (ANN) Search in PECOS | [Notebook](https://github.com/amzn/pecos/blob/tutorials/kdd22/Session%203%20Approximate%20Nearest%20Neighbor%20Search%20in%20PECOS.ipynb) |
+| 10:30 AM - 11:10 AM | Session 4: Utilities in PECOS | [Notebook](https://github.com/amzn/pecos/blob/tutorials/kdd22/Session%204%20Utilities%20in%20PECOS) |
+| 11:10 AM - 11:40 AM | Session 5: XR-Transformer cookbook and Distributed PECOS | [Notebook](https://github.com/amzn/pecos/blob/tutorials/kdd22/Session%205%20XR-Transformer%20cookbook%20and%20Distributed%20PECOS) |
+| 11:40 AM - 11:50 AM | Session 6: Research with PECOS | |
+| 11:50 AM - 12:00 PM | Closing Remarks | |
diff --git a/tutorials/kdd22/Session 2 Extreme Multi-label Ranking with PECOS.ipynb b/tutorials/kdd22/Session 2 Extreme Multi-label Ranking with PECOS.ipynb
new file mode 100644
index 0000000..81f6ce5
--- /dev/null
+++ b/tutorials/kdd22/Session 2 Extreme Multi-label Ranking with PECOS.ipynb
@@ -0,0 +1,1493 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "67e70878",
+ "metadata": {},
+ "source": [
+ "# eXtreme Multi-label Ranking (XMR) Problem and PECOS\n",
+ "\n",
+ "Prediction for Enormous and Correlated Output Spaces (PECOS) is a versatile and modular machine learning framework for solving prediction problems with very large outputs spaces. For a given input instance, we apply PECOS to the eXtreme Multilabel Ranking (XMR) problem to find and rank the most relevant items from an enormous but fixed and finite output space.\n",
+ "\n",
+ "
\n",
+ "\n",
+ "As shown in the above figure, to address the XMR problem, PECOS conceptually consists of three stages, including semantic label indexing, machine-learned matching, and ranking. For more details about XMR problem and model formulation, please refer to presentations in the PECOS Day. In this part of the tutorial, we will use XR-Linear as an example to demonstrate how to use PECOS to tackle real-world problems and understrand the model architecture in PECOS."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41d87d24",
+ "metadata": {},
+ "source": [
+ "## Experimental Dataset\n",
+ "\n",
+ "`eurlex-4k`, `wiki10-31k`, `amazoncat-13k`, `amazon-670k`, `wiki-500k`, and `amazon-3m` are available."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "1073ac9c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2022-07-14 08:54:02 URL:https://ia802308.us.archive.org/21/items/pecos-dataset/xmc-base/wiki10-31k.tar.gz [162277861/162277861] -> \"wiki10-31k.tar.gz\" [1]\n",
+ "xmc-base/wiki10-31k/output-items.txt\n",
+ "xmc-base/wiki10-31k/tfidf-attnxml\n",
+ "xmc-base/wiki10-31k/tfidf-attnxml/X.trn.npz\n",
+ "xmc-base/wiki10-31k/tfidf-attnxml/X.tst.npz\n",
+ "xmc-base/wiki10-31k/X.trn.txt\n",
+ "xmc-base/wiki10-31k/X.tst.txt\n",
+ "xmc-base/wiki10-31k/Y.trn.npz\n",
+ "xmc-base/wiki10-31k/Y.trn.txt\n",
+ "xmc-base/wiki10-31k/Y.tst.npz\n",
+ "xmc-base/wiki10-31k/Y.tst.txt\n"
+ ]
+ }
+ ],
+ "source": [
+ "DATASET = \"wiki10-31k\"\n",
+ "! wget -nv -nc https://archive.org/download/pecos-dataset/xmc-base/{DATASET}.tar.gz\n",
+ "! tar --skip-old-files -zxf {DATASET}.tar.gz \n",
+ "! find xmc-base/{DATASET}/*"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "73f0fa78",
+ "metadata": {},
+ "source": [
+ "### Analyze Sparse Features and Label Space"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "d680e1e0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import scipy.sparse as smat\n",
+ "import matplotlib.pyplot as plt\n",
+ "X_trn = smat.load_npz(f\"xmc-base/{DATASET}/tfidf-attnxml/X.trn.npz\")\n",
+ "Y_trn = smat.load_npz(f\"xmc-base/{DATASET}/Y.trn.npz\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "0b34281d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'14146 instances with 101938 features.'"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\"{} instances with {} features.\".format(*X_trn.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "7f16a000",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Overall Sparsity: 99.34%'"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\"Overall Sparsity: {:.2f}%\".format(100 * (1 - X_trn.nnz / (X_trn.shape[0] * X_trn.shape[1])))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "dcf0f0cf",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "counts, bins = np.histogram(100 - 100 * X_trn.getnnz(1) / X_trn.shape[1], bins=20)\n",
+ "plt.hist(bins[:-1], bins, weights=counts)\n",
+ "plt.title(DATASET);\n",
+ "plt.xlabel(\"Feature Sparsity (%)\");\n",
+ "plt.ylabel(\"Number of Instances\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a1e0157b",
+ "metadata": {},
+ "source": [
+ "### Extremely large label space"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "49f8fe28",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'14146 instances with 30938 labels.'"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\"{} instances with {} labels.\".format(*Y_trn.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "64f5fc9b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Overall Sparsity: 99.94%'"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "\"Overall Sparsity: {:.2f}%\".format(100 * (1 - Y_trn.nnz / (Y_trn.shape[0] * Y_trn.shape[1])))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "b5cb2084",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "counts, bins = np.histogram(100 - 100 * Y_trn.getnnz(1) / Y_trn.shape[1], bins=20, range=(99.85, 100))\n",
+ "plt.hist(bins[:-1], bins, weights=counts)\n",
+ "plt.title(DATASET);\n",
+ "plt.xlabel(\"Label Sparsity (%)\");\n",
+ "plt.ylabel(\"Number of Instances\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "057fb642",
+ "metadata": {},
+ "source": [
+ "## Numerical Feature and Label Format in PECOS\n",
+ "\n",
+ "In PECOS, numerical features of instances can be in either a [dense NumPy matrix](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) or a [Compressed Sparse Row (CSR) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html) of shape `(nr_inst, nr_feat)`, where `nr_inst` and `nr_feat` are numbers of instances and features. Similary, labels of instances can be also presented as a dense or a sparse matrix of shape `(nr_inst, nr_labels)`, where `nr_labels` is the number of labels in the XMR problem. Note that for the sparse format, training labels should be a [Compressed Sparse Column (CSC) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html) while testing labels should be a CSR matrix for the purpose of computational efficiency. For convenience, PECOS also provides APIs for loading features and labels from binary files in arbitary formats.\n",
+ "\n",
+ "In addition to numerical features, PECOS also supports handling text data with transformer. Please refer to [Part 2](Part%202%20-%20Text%20Processing.ipynb) in this tutorial for more details about text processing in PECOS."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "c518d892",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training features X_trn is a csr matrix of shape (14146, 101938).\n",
+ "Training labels Y_trn is a csc matrix of shape (14146, 30938).\n",
+ "Testing features X_tst is a csr matrix of shape (6616, 101938).\n",
+ "Testing labels Y_tst is a csr matrix of shape (6616, 30938).\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from pecos.xmc.xlinear.model import XLinearModel\n",
+ "\n",
+ "DATASET = \"wiki10-31k\"\n",
+ "\n",
+ "X_trn = XLinearModel.load_feature_matrix(\"xmc-base/{}/tfidf-attnxml/X.trn.npz\".format(DATASET))\n",
+ "Y_trn = XLinearModel.load_label_matrix(\"xmc-base/{}/Y.trn.npz\".format(DATASET), for_training=True)\n",
+ "\n",
+ "X_tst = XLinearModel.load_feature_matrix(\"xmc-base/{}/tfidf-attnxml/X.tst.npz\".format(DATASET))\n",
+ "Y_tst = XLinearModel.load_label_matrix(\"xmc-base/{}/Y.tst.npz\".format(DATASET), for_training=False)\n",
+ "\n",
+ "print(f\"Training features X_trn is a {X_trn.getformat()} matrix of shape {X_trn.shape}.\")\n",
+ "print(f\"Training labels Y_trn is a {Y_trn.getformat()} matrix of shape {Y_trn.shape}.\")\n",
+ "print(f\"Testing features X_tst is a {X_tst.getformat()} matrix of shape {X_tst.shape}.\")\n",
+ "print(f\"Testing labels Y_tst is a {Y_tst.getformat()} matrix of shape {Y_tst.shape}.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b0c731f5",
+ "metadata": {},
+ "source": [
+ "## Hands-on Example: XMR with XR-Linear\n",
+ "\n",
+ "XR-LINEAR is a recursive linear machine learned realization of our PECOS framework. As shown in the below figure, XR-Linear treats machine-learned matching as a smaller XMR problem, thereby recursively apply the three-stage framework of PECOS to address the problem.\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "150fea14",
+ "metadata": {},
+ "source": [
+ "### Semantic Label Indexing and Cluster Chain in XR-Linear\n",
+ "\n",
+ "The first step of training an XR-Linear model is to conduct semantic label indexing and establish the *hierarchial label tree* for resursive training the XR-Linear model and its inference. \n",
+ "\n",
+ "PECOS supports any method for semantic label indexing. In the PECOS library, as a build-in method, we provide Label Representation via Positive Instance Feature Aggregation (PIFA) for semantic label indexing with only the need of positive instances and their features in training data. PECOS can also consider additional label features `Z` of shape `(nr_labels, nr_label_feat)` in either dense or sparse matrix format, where `nr_label_feat` is the number of label features. These representations and features for each label are concatenated or combined as label embedding in `LabelEmbeddingFactory` in PECOS.\n",
+ "\n",
+ "To conduct semantic label indexing, PECOS learns an indexer based on label embedding. PECOS currently supports to use the Hierarchical K-Means for semantic label indexing with a hyper-parameter `nr_splits` (the number of clusters in each layer, or `B` in [our report](https://arxiv.org/pdf/2010.05878.pdf)), which decides the depth `D` of the hierarchical label tree. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "26794215",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "4 layers in the trained hierarchical label tree.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc import Indexer, LabelEmbeddingFactory\n",
+ "\n",
+ "label_feat = LabelEmbeddingFactory.create(Y_trn, X_trn, method=\"pifa\")\n",
+ "# label_feat = LabelEmbeddingFactory.create(Y_trn, X_trn, Z, method=\"pifa_lf_concat\") # for using label features Z\n",
+ "\n",
+ "cluster_chain = Indexer.gen(label_feat, nr_splits=8, indexer_type=\"hierarchicalkmeans\")\n",
+ "\n",
+ "print(f\"{len(cluster_chain)} layers in the trained hierarchical label tree.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "02ffda21",
+ "metadata": {},
+ "source": [
+ "### Training XR-Linear Negative Sampling and Sparsification\n",
+ "\n",
+ "Negative sampling plays an important role in solving the XMR problem. PECOS currently provides two negative sampling schemes, including Teacher Forcing Negatives (TFN) and Matcher Aware Negatives (MAN). Please refer to [our report](https://arxiv.org/pdf/2010.05878.pdf)) and presentations in the [PECOS Day](https://w.amazon.com/bin/view/Search/MIDAS/Projects/PECOS/PecosDay/) for more details about negative sampling schemes.\n",
+ "\n",
+ "To reduce model sizes and improve efficiency, PECOS conduct model sparsification with a hyper-parameter `threshold`. The model weights with absolute values smaller than the threshold will be discarded."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "bd3d6527",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training time: 40.9793 seconds.\n"
+ ]
+ }
+ ],
+ "source": [
+ "import time\n",
+ "start_time = time.time()\n",
+ "\n",
+ "# For negative_sampling_scheme in model training, \"man\" and tfn+man\" are also available.\n",
+ "xlm = XLinearModel.train(X_trn, Y_trn, C=cluster_chain, threshold=0.1, negative_sampling_scheme=\"tfn\")\n",
+ "\n",
+ "training_time = time.time() - start_time\n",
+ "print(f\"Training time: {training_time:.4f} seconds.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20f5cfa7",
+ "metadata": {},
+ "source": [
+ "PECOS supports serializing and loading the trained model into binary on disk with convenient interfaces. Note that model loading with `is_predict_only=True` could lead to faster prediction speed by disabling the flexibility of model modification."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "3d5d468d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xlm.save(\"{}.xlm.model\".format(DATASET))\n",
+ "xlm = XLinearModel.load(\"{}.xlm.model\".format(DATASET), is_predict_only=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4b6038ec",
+ "metadata": {},
+ "source": [
+ "### Prediction and Evaluation\n",
+ "\n",
+ "As a tree model, the inference method significantly affects the prediction efficiency of XR-Linear in PECOS. As illustrated in the following figure, the prediction process in PECOS employs a beam search with a hyper-parameter `beam_size`. The other hyper-parameter `only_topk` also needs to be decided to limit the predicted most relevant labels for each instance. The `predict` function of the trained model will result in a CSR matrix of shape `(nr_inst, nr_labels)` and exactly `only_topk` non-zero columns for each row (or instance).\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "7f851bc1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Y_pred is a csr matrix of shape (6616, 30938) and 66160 non-zero elements.\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "Y_pred = xlm.predict(X_tst, beam_size=10, only_topk=10)\n",
+ "\n",
+ "print(f\"Y_pred is a {Y_pred.getformat()} matrix of shape {Y_pred.shape} and {Y_pred.nnz} non-zero elements.\")\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.plot(range(Y_pred.shape[0]), Y_pred.getnnz(1))\n",
+ "plt.xlabel(\"Instance ID\");\n",
+ "plt.ylabel(\"Number of Predictions in Y_pred\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fb1ed22c",
+ "metadata": {},
+ "source": [
+ "For evaluation, we evaluate the trained model with conventional ranking metrics, including Precision@K and Recall@K. PECOS also provides the evaluation interface for predicted sparse matrices."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "4c57da1a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "prec = 84.07 78.17 72.68 67.79 63.79 60.06 56.63 53.51 50.83 48.33\n",
+ "recall = 4.97 9.16 12.68 15.60 18.25 20.49 22.40 24.05 25.60 26.95\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.utils import smat_util\n",
+ "metrics = smat_util.Metrics.generate(Y_tst, Y_pred, topk=10)\n",
+ "print(metrics)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32908310",
+ "metadata": {},
+ "source": [
+ "### Dive Deep in Cluster Chain\n",
+ "\n",
+ "Specifically, PECOS trains a *cluster_chain* of `D` matching matrices `C[d]`, where `C[d]` is a CSC matrix of shape `(L[d], K[d])`; `L[d]` and `K[d]` are the numbers of labels and clusters in the layer `d`. Note that the clusters of a layer would be the labels of the next layer. The labels of the last layer `L[D - 1]` would be the labels of the overall XMR problem `nr_labels`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "6b0cb55e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "4 layers in the trained hierarchical label tree with C[d] as:\n",
+ "cluster_chain[0] is a csc matrix of shape (8, 1)\n",
+ "cluster_chain[1] is a csc matrix of shape (64, 8)\n",
+ "cluster_chain[2] is a csc matrix of shape (512, 64)\n",
+ "cluster_chain[3] is a csc matrix of shape (30938, 512)\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(f\"{len(cluster_chain)} layers in the trained hierarchical label tree with C[d] as:\")\n",
+ "for d, C in enumerate(cluster_chain):\n",
+ " print(f\"cluster_chain[{d}] is a {C.getformat()} matrix of shape {C.shape}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "55eeb4e5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "from pecos.core import clib\n",
+ "from pecos.utils import smat_util\n",
+ "\n",
+ "fig, axes = plt.subplots(nrows=len(cluster_chain), ncols=1)\n",
+ "fig.tight_layout()\n",
+ "\n",
+ "cur_Y = Y_tst\n",
+ "\n",
+ "counts, bins = np.histogram(cur_Y.getnnz(1), bins=16) \n",
+ "ax = plt.subplot(len(cluster_chain), 1, len(cluster_chain))\n",
+ "ax.hist(bins[:-1], bins, weights=counts)\n",
+ "ax.set_title(\"Layer {}\".format(len(cluster_chain) - 1))\n",
+ "plt.ylabel(\"#Instances\")\n",
+ "\n",
+ "for d in range(len(cluster_chain) - 1, 0, -1):\n",
+ " cur_Y = smat_util.binarized(clib.sparse_matmul(cur_Y, cluster_chain[d]))\n",
+ " counts, bins = np.histogram(cur_Y.getnnz(1), bins=min(16, cluster_chain[d].shape[1])) \n",
+ " ax = plt.subplot(len(cluster_chain), 1, d)\n",
+ " ax.hist(bins[:-1], bins, weights=counts)\n",
+ " ax.set_title(\"Layer {}\".format(d - 1))\n",
+ " plt.ylabel(\"#Instances\")\n",
+ " \n",
+ " \n",
+ "plt.subplot(len(cluster_chain), 1, len(cluster_chain))\n",
+ "plt.xlabel(\"Number of Belonged Clusters/Labels\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "790b21dc",
+ "metadata": {},
+ "source": [
+ "### Dive Deep in Model Weights\n",
+ "\n",
+ "Model weights in an XR-Linear model are also accessible as `model_chain` for analysis and computations. For the i-th layer in the hierarchy, the model weights of matchers/rankers are available as a CSC matrix of shape `(nr_feat + 1, L[i])`, which concatenates weights for features and the bias term. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "9e101f6b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "model_chain[0].W is a csc matrix of shape (101939, 8)\n",
+ "model_chain[1].W is a csc matrix of shape (101939, 64)\n",
+ "model_chain[2].W is a csc matrix of shape (101939, 512)\n",
+ "model_chain[3].W is a csc matrix of shape (101939, 30938)\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "for d, m in enumerate(xlm.model.model_chain):\n",
+ " print(\"model_chain[{}].W is a {} matrix of shape {}\".format(d, m.W.getformat(), m.W.shape))\n",
+ "\n",
+ "layer_d = 1\n",
+ "from sklearn.decomposition import TruncatedSVD\n",
+ "svd = TruncatedSVD(n_components=2, random_state=0)\n",
+ "Wt = svd.fit_transform(xlm.model.model_chain[layer_d].W.transpose())\n",
+ "\n",
+ "import numpy as np\n",
+ "color = cluster_chain[layer_d].tocsr() * np.arange(cluster_chain[layer_d].shape[1])\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "plt.scatter(Wt[:, 0], Wt[:, 1], c=color)\n",
+ "plt.xlim(-4, 4);\n",
+ "plt.ylim(-10, 10);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae9baf18",
+ "metadata": {},
+ "source": [
+ "### PECOS and One-versus-All (OVA) Model\n",
+ "\n",
+ "PECOS also supports to train an OVA model without leveraing clustering hierarchy if needed.\n",
+ "\n",
+ "**Training OVA models is time-consuming, we suggest to try it offline after the tutorial.**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "c95f0acf",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training time for the OVA model: 1047.3194 seconds.\n",
+ "XR-Linear is 25.56 times faster than the OVA model\n"
+ ]
+ }
+ ],
+ "source": [
+ "import time\n",
+ "start_time = time.time()\n",
+ "\n",
+ "xlm_ova = XLinearModel.train(X_trn, Y_trn, C=None, negative_sampling_scheme=\"tfn\") \n",
+ "\n",
+ "training_time_ova = time.time() - start_time\n",
+ "print(\"Training time for the OVA model: {:.4f} seconds.\".format(training_time_ova))\n",
+ "\n",
+ "print(\"XR-Linear is {:.2f} times faster than the OVA model\".format(training_time_ova / training_time))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "73a599a0",
+ "metadata": {},
+ "source": [
+ "## Customized Parameters and Advanced Training Options\n",
+ "\n",
+ "PECOS also supports using customized parameters and several advanced training options, such as different solvers and cost-sensitive learning.\n",
+ "\n",
+ "### Customized Parameters\n",
+ "\n",
+ "The parameters for either of indexing, training, and inference can be easily customized by feeding a dictionary into the corresponding parameter class and its constructor:\n",
+ "\n",
+ "* Semantic Indexing (Hierarchical K-Means): `HierarchicalKMeans.TrainParams.from_dict(dict)`\n",
+ "* Training: `XLinearModel.TrainParams.from_dict(dict)`\n",
+ "* Inference: `XLinearModel.PredParams.from_dict(dict)`\n",
+ "\n",
+ "Although most of the parameters can be also passed by `kwargs` of Python methods, **we encourage to use the dictionary to designate the parameters because it is easier to manage, modularize, and store parameters in certain formats like JSON.**\n",
+ "\n",
+ "For XR-Linear models, the default values and skeleton of the parameters can be revealed and generated by the following command:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "1ddc9bfa",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{\r\n",
+ " \"train_params\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.xlinear.model###XLinearModel.TrainParams\"\r\n",
+ " },\r\n",
+ " \"mode\": \"full-model\",\r\n",
+ " \"ranker_level\": 1,\r\n",
+ " \"nr_splits\": 16,\r\n",
+ " \"min_codes\": null,\r\n",
+ " \"shallow\": false,\r\n",
+ " \"rel_mode\": \"disable\",\r\n",
+ " \"rel_norm\": \"no-norm\",\r\n",
+ " \"hlm_args\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.base###HierarchicalMLModel.TrainParams\"\r\n",
+ " },\r\n",
+ " \"neg_mining_chain\": \"tfn\",\r\n",
+ " \"model_chain\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.base###MLModel.TrainParams\"\r\n",
+ " },\r\n",
+ " \"threshold\": 0.1,\r\n",
+ " \"max_nonzeros_per_label\": null,\r\n",
+ " \"solver_type\": \"L2R_L2LOSS_SVC_DUAL\",\r\n",
+ " \"Cp\": 1.0,\r\n",
+ " \"Cn\": 1.0,\r\n",
+ " \"max_iter\": 100,\r\n",
+ " \"eps\": 0.1,\r\n",
+ " \"bias\": 1.0,\r\n",
+ " \"threads\": -1,\r\n",
+ " \"verbose\": 0,\r\n",
+ " \"newton_eps\": 0.01\r\n",
+ " }\r\n",
+ " }\r\n",
+ " },\r\n",
+ " \"pred_params\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.xlinear.model###XLinearModel.PredParams\"\r\n",
+ " },\r\n",
+ " \"hlm_args\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.base###HierarchicalMLModel.PredParams\"\r\n",
+ " },\r\n",
+ " \"model_chain\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.base###MLModel.PredParams\"\r\n",
+ " },\r\n",
+ " \"only_topk\": 20,\r\n",
+ " \"post_processor\": \"l3-hinge\"\r\n",
+ " }\r\n",
+ " }\r\n",
+ " },\r\n",
+ " \"indexer_params\": {\r\n",
+ " \"__meta__\": {\r\n",
+ " \"class_fullname\": \"pecos.xmc.base###HierarchicalKMeans.TrainParams\"\r\n",
+ " },\r\n",
+ " \"nr_splits\": 16,\r\n",
+ " \"min_codes\": null,\r\n",
+ " \"max_leaf_size\": 100,\r\n",
+ " \"imbalanced_ratio\": 0.0,\r\n",
+ " \"imbalanced_depth\": 100,\r\n",
+ " \"spherical\": true,\r\n",
+ " \"seed\": 0,\r\n",
+ " \"kmeans_max_iter\": 20,\r\n",
+ " \"threads\": -1\r\n",
+ " }\r\n",
+ "}\r\n"
+ ]
+ }
+ ],
+ "source": [
+ "! python3 -m pecos.xmc.xlinear.train --generate-params-skeleton"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "35472517",
+ "metadata": {},
+ "source": [
+ "### Training Parameters for Hierarchial Models in XR-Linear\n",
+ "\n",
+ "Hierarchical models could have different parameters over layers. To have customized parameters for the hierarchical model, `hlm_args` needs to be designated in the parameter dictionary. The values of `model_chain` and `neg_mining_chain` in `hlm_args` can be **a single dictionary** of general parameters for all layers or **a list of dictinoaries** for specific parameters of individual layers.\n",
+ "\n",
+ "#### General Parameters for All Layers\n",
+ "\n",
+ "```\n",
+ "train_params_l1 = XLinearModel.TrainParams.from_dict(\n",
+ " {\n",
+ " ...\n",
+ " \"hlm_args\": {\n",
+ " ...\n",
+ " \"neg_mining_chain\": \"tfn\", # Negative sampling scheme for all layers\n",
+ " \"model_chain\":{...}, # Parameters for all layers\n",
+ " }\n",
+ " ...\n",
+ " })\n",
+ "```\n",
+ "\n",
+ "#### Specific Parameters of Individual Layers\n",
+ "\n",
+ "```\n",
+ "train_params_l1 = XLinearModel.TrainParams.from_dict(\n",
+ " {\n",
+ " ...\n",
+ " \"hlm_args\": {\n",
+ " ...\n",
+ " \"neg_mining_chain\": [\n",
+ " \"tfn\", # Negative sampling scheme for layer-0\n",
+ " \"tfn\", # Negative sampling scheme for layer-1\n",
+ " \"tfn+man\", # Negative sampling scheme for layer-2\n",
+ " ...\n",
+ " ],\n",
+ " \"model_chain\": [\n",
+ " {...}, # Parameters for layer-0\n",
+ " {...}, # Parameters for layer-1\n",
+ " {...}, # Parameters for layer-2\n",
+ " ...\n",
+ " ],\n",
+ " }\n",
+ " ...\n",
+ " })\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "81f86b8a",
+ "metadata": {},
+ "source": [
+ "### Variety of Solvers\n",
+ "\n",
+ "The solver for optimization can be adjusted by the argument `solver_type` in the `train` function. PECOS currently provides the following solvers for training each matcher/ranker:\n",
+ "\n",
+ "* \"L2R_L2LOSS_SVC_DUAL\" (default): L2-regularized L2-loss Dual SVM\n",
+ "* \"L2R_L1LOSS_SVC_DUAL\": : L2-regularized L1-loss Dual SVM\n",
+ "* \"L2R_LR_DUAL\": L2-reguarlized Logistic Regression"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "8f26ee42",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xlm_l1_kwargs = XLinearModel.train(\n",
+ " X_trn, Y_trn,\n",
+ " C=cluster_chain,\n",
+ " threshold=0.1,\n",
+ " negative_sampling_scheme=\"tfn\",\n",
+ " solver_type=\"L2R_L1LOSS_SVC_DUAL\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "197926a7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_params_l1 = XLinearModel.TrainParams.from_dict(\n",
+ " {\n",
+ " \"hlm_args\": {\n",
+ " \"threshold\": 0.1,\n",
+ " \"neg_mining_chain\": \"tfn\",\n",
+ " \"model_chain\":{\n",
+ " \"solver_type\": \"L2R_L1LOSS_SVC_DUAL\",\n",
+ " },\n",
+ " }\n",
+ " }\n",
+ ")\n",
+ "\n",
+ "xlm_l1_dict = XLinearModel.train(\n",
+ " X_trn, Y_trn,\n",
+ " C=cluster_chain,\n",
+ " train_params=train_params_l1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "eddf91a6",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Evaluation Metrics with L2R_L1LOSS_SVC_DUAL (by method kwargs)\n",
+ "prec = 83.43 77.66 72.47 67.67 63.73 60.18 56.90 54.04 51.45 49.04\n",
+ "recall = 4.93 9.11 12.62 15.59 18.19 20.51 22.49 24.28 25.92 27.36\n",
+ "\n",
+ "Evaluation Metrics with L2R_L1LOSS_SVC_DUAL (by dictionary)\n",
+ "prec = 83.43 77.66 72.47 67.67 63.73 60.18 56.90 54.04 51.45 49.04\n",
+ "recall = 4.93 9.11 12.62 15.59 18.19 20.51 22.49 24.28 25.92 27.36\n"
+ ]
+ }
+ ],
+ "source": [
+ "Y_pred_l1_kwargs = xlm_l1_kwargs.predict(X_tst, beam_size=10, only_topk=10)\n",
+ "Y_pred_l1_dict = xlm_l1_dict.predict(X_tst, beam_size=10, only_topk=10)\n",
+ "metrics_l1_kwargs = smat_util.Metrics.generate(Y_tst, Y_pred_l1_kwargs, topk=10)\n",
+ "metrics_l1_dict = smat_util.Metrics.generate(Y_tst, Y_pred_l1_dict, topk=10)\n",
+ "\n",
+ "print(\"Evaluation Metrics with L2R_L1LOSS_SVC_DUAL (by method kwargs)\")\n",
+ "print(metrics_l1_kwargs)\n",
+ "\n",
+ "print(\"\\nEvaluation Metrics with L2R_L1LOSS_SVC_DUAL (by dictionary)\")\n",
+ "print(metrics_l1_dict)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9f481ed3",
+ "metadata": {},
+ "source": [
+ "### Cost-sensitive Learning\n",
+ "\n",
+ "PECOS supports to adjust the cost of each training instance. To enable cost-sensitive learning, we need to provide a **relevance matrix** `R_trn` with the same shape to the label matrix `Y_trn` for the argument `R`. When `R` is `None` (default), cost-sensitive learning is disable. \n",
+ "\n",
+ "Since PECOS models are usually hierarhical, costs for upper layers also need to be decided as the cost-sensitive learning mode by the argument `rel_mode`. Currently, PECOS supports the following cost-sensitive learning modes:\n",
+ "\n",
+ "* `\"disable\"` (default): The cost-sensitive learning is disable.\n",
+ "* `\"induce\"`: Induce the costs into upper layers by the clustering chain.\n",
+ "* `\"ranker-only\"`: Only apply cost-sensitive learning to the model in the last ranker layer without induction.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "382277f3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# An exmaple of using training label frequency scores as costs. \n",
+ "import copy\n",
+ "from sklearn.preprocessing import normalize\n",
+ "\n",
+ "R_trn = copy.deepcopy(Y_trn)\n",
+ "\n",
+ "# Training parameters for cost-sensitive learning.\n",
+ "train_params_cost = XLinearModel.TrainParams.from_dict(\n",
+ " {\n",
+ " \"rel_mode\": \"induce\",\n",
+ " \"rel_norm\": \"l1\",\n",
+ " \"hlm_args\": {\n",
+ " \"neg_mining_chain\": \"tfn\",\n",
+ " \"model_chain\":\n",
+ " [\n",
+ " {\n",
+ " \"threshold\": 0.1,\n",
+ " \"Cp\": 1.0,\n",
+ " \"Cn\": 1.0,\n",
+ " },\n",
+ " {\n",
+ " \"threshold\": 0.1,\n",
+ " \"Cp\": 8.0,\n",
+ " \"Cn\": 1.0,\n",
+ " },\n",
+ " {\n",
+ " \"threshold\": 0.1,\n",
+ " \"Cp\": 4.0,\n",
+ " \"Cn\": 1.0,\n",
+ " },\n",
+ " {\n",
+ " \"threshold\": 0.1,\n",
+ " \"Cp\": 4.0,\n",
+ " \"Cn\": 1.0,\n",
+ " },\n",
+ " ],\n",
+ " }\n",
+ " })\n",
+ " \n",
+ "# Cost-sensitive learning.\n",
+ "xlm_cost = XLinearModel.train(\n",
+ " X_trn, Y_trn,\n",
+ " C=cluster_chain,\n",
+ " R=R_trn,\n",
+ " train_params=train_params_cost)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "559c15cb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Evaluation Metrics with Cost-sensitive Learning\n",
+ "prec = 85.02 80.58 74.57 69.37 64.79 60.82 57.34 54.29 51.37 48.81\n",
+ "recall = 5.02 9.46 13.02 15.99 18.54 20.74 22.69 24.42 25.92 27.27\n",
+ "\n",
+ "Original Evaluation Metrics\n",
+ "prec = 84.07 78.17 72.68 67.79 63.79 60.06 56.63 53.51 50.83 48.33\n",
+ "recall = 4.97 9.16 12.68 15.60 18.25 20.49 22.40 24.05 25.60 26.95\n"
+ ]
+ }
+ ],
+ "source": [
+ "Y_pred_cost = xlm_cost.predict(X_tst, beam_size=10, only_topk=10)\n",
+ "metrics_cost = smat_util.Metrics.generate(Y_tst, Y_pred_cost, topk=10)\n",
+ "print(\"Evaluation Metrics with Cost-sensitive Learning\")\n",
+ "print(metrics_cost)\n",
+ "print(\"\\nOriginal Evaluation Metrics\")\n",
+ "print(metrics)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1663277d",
+ "metadata": {},
+ "source": [
+ "# Customized PECOS Model\n",
+ "\n",
+ "Besides pre-defined models in PECOS, such as XR-Linear, it is also convenient for users to customize PECOS for specific purposes and usage. Specifically, we suggest to establishing a model class to wrap fundamental PECOS functions and tailored operations. As a result, the customized model can be easily constructed and consumed for arbitrary data types and feature extractors. \n",
+ "\n",
+ "## Structure of a Customized PECOS Model\n",
+ "\n",
+ "Even though a customized machine learning pipeline can be seperated into several independent scripts, we recommend declaring a customized PECOS model as a **model class** for better re-usability and code maintenance.\n",
+ "\n",
+ "A customized PECOS model should at least consist of the following components:\n",
+ "\n",
+ "* `preprocessor` or `encoder`: The procedure, which can be a method or a functionable object, pre-processes or encodes an arbitrary input with the designated data format into features. For example, text data and image data can be encoded by BERT and ResNet.\n",
+ "* `train()`: The training method takes a set of training data with a preprocessor, learns a primitive PECOS model, and returns a PECOS-based customized machine learning model. The training function could be a class method to construct the model object with the learned model and essential components after training.\n",
+ "* `model`: A primitive PECOS model taking pre-processed features is capable of deriving the predictions for arbitrary testing data. The model weights should be learned by `train()`. \n",
+ "* `predict()`: The prediction method takes arbitrary testing data and infers the prediction based on the pre-processor and the learned model.\n",
+ "* `save()`: The saving function serializes the trained model, including model weights and configuration, for further usage.\n",
+ "* `load()`: The loading function reads the serialized model so that the trained model can be loaded and re-used.\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "\n",
+ "In this part of the tutorial, we will use the task of *extreme multi-label text classification* as an example to demonstrate how to **customize a PECOS model that can handle text data with either a conventional bag-of-words (BoW) model or a deep learning model as the text encoder for feature extraction**.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c3acc325",
+ "metadata": {},
+ "source": [
+ "## Example: eXtreme Multi-label Text Classification (XMTC)\n",
+ "\n",
+ "The task of extreme multi-label text classification (XMTC) seeks to find relevant labels from an extreme large label collection for a given text input. Many real-world applications can be formulated as XMTC tasks, such as recommendation systems, document tagging, and semantic search. \n",
+ "\n",
+ "In this section, we guide through how to establish a customized PECOS model for XMTC tasks. We will walk through (1) PECOS' built-in BOW model for text preprocessing and vectorizing; (2) how to customize a PECOS model; and (3) \n",
+ "advanced usage of XR-Transformer based on deep learning.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3b2ec0e6",
+ "metadata": {},
+ "source": [
+ "### Preprocessor: Text Preprocessing and Vectorizing\n",
+ "\n",
+ "The preprocessor plays a role of encoding input data into machine readable vector representations. Any encoder that can transform text data into a vector representation can be considered as the preprocessor or encoder of a customized PECOS model for XMTC tasks.\n",
+ "\n",
+ "In the PECOS library, we provide [various text vectorizers](https://github.com/amzn/pecos/blob/mainline/pecos/utils/featurization/text/vectorizers.py), such as TF-IDF, hashing, and pretrained transformer, as **built-in preprocessors** to deal with text data. In this tutorial, we will utilize the [n-gram](https://en.wikipedia.org/wiki/N-gram) [TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf) model as our preprocessor.\n",
+ "\n",
+ "#### Label Space File Format for Built-in Text Preprocessors\n",
+ "\n",
+ "Label space is also essential for text preprocessors, especially for understanding the label space size to create the appropriate label matrix. The label IDs start from zero and can be referred to the line numbers and corresponding text descriptions in the label space file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "3f48f4f7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Artificial intelligence researchers\r\n",
+ "Computability theorists\r\n",
+ "British computer scientists\r\n",
+ "Machine learning researchers\r\n",
+ "Turing Award laureates\r\n",
+ "Deep Learning\r\n"
+ ]
+ }
+ ],
+ "source": [
+ "! cat \"./text2text_demo/output-labels.txt\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e0862645",
+ "metadata": {},
+ "source": [
+ "#### Data File Format for Built-in Text Preprocessors\n",
+ "\n",
+ "PECOS built-in text preprocessors majorly take the files of text data with labels in a tab-separated values (TSV) format. Each line in the TSV file consists of two elements that represent the comma-separated label IDs and the input text of a data instance. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "bd5ebfc6",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0,1,2\tAlan Turing is widely considered to be the father of theoretical computer science and artificial intelligence.\r\n",
+ "0,2,3\tHinton was co-author of a highly cited paper published in 1986 that popularized the backpropagation algorithm for training multi-layer neural networks.\r\n",
+ "3,4,5\tHinton received the 2018 Turing Award, together with Yoshua Bengio and Yann LeCun, for their work on artificial intelligence and deep learning.\r\n",
+ "0,3,5\tYoshua Bengio is a Canadian computer scientist, most noted for his work on artificial neural networks and deep learning.\r\n"
+ ]
+ }
+ ],
+ "source": [
+ "! cat ./text2text_demo/training-data.txt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "566d1eb5",
+ "metadata": {},
+ "source": [
+ "The data file format also supports to represent the label relevance for cost-sensitive learning by using double colons to separate a label and its relevance.\n",
+ "\n",
+ "\n",
+ "0::0.1,1::0.2,2::0.8 <TAB> Alan Turing is widely considered to be the father of theoretical computer science and artificial intelligence.
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4d4e4419",
+ "metadata": {},
+ "source": [
+ "#### Training a Text Preprocessor\n",
+ "\n",
+ "The preprocessor model `Preprocessor` is defined in `pecos.utils.featurization.text.preprocess`. Given a training text corpus and the configuration dictionary, the class method `Preprocessor.train` will train a corresponding text preprocesssor. Besides, the built-in preprocessors also support serialization with the function `save()` for the re-usability.\n",
+ "\n",
+ "With the previously mentioned data and label space file formats, the utility function `Preprocessor.load_data_from_file(input_text_path, output_text_path)` returns a dictionary with three keys:\n",
+ "\n",
+ "* `label_matrix`: a `(num_inst, num_labels)` CSR matrix for the labels of each instance.\n",
+ "* `label_relevance`: `None` or a `(num_inst, num_labels)` CSR matrix for the relevance of each label in cost-sensitive learning if available.\n",
+ "* `corpus`: a list of string as the text corpus in the input_text_path.\n",
+ "\n",
+ "The configuration settings of text preprocessor including the preprocessor type and hyper-parameters should be defined in a dictionary. Specifially, the key `type` defines the preprocessor choice while the key `kwargs` represents the hyper-parameters. In this tutorial, we adopt n-gram TFIDF features containing *word unigrams*, *word bigrams*, and *character trigrams*. Note that each of the n-gram feature can have different hyper-parameters, such as `max_feature` and `max_df`. Users need to properly set max_feature (e.g., hundred of thousands or millions) based on the corpus size and downstream tasks."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "b7f70a8f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pecos.utils.featurization.text.preprocess import Preprocessor\n",
+ "\n",
+ "input_text_path = \"./text2text_demo/training-data.txt\"\n",
+ "output_text_path = \"./text2text_demo/output-labels.txt\"\n",
+ "model_folder = \"./text2text_demo/pecos-text2text-model\"\n",
+ "\n",
+ "parsed_result = Preprocessor.load_data_from_file(input_text_path, output_text_path) # Read files\n",
+ "corpus = parsed_result[\"corpus\"] # Corpus input text: List of strings\n",
+ "\n",
+ "vectorizer_config = {\n",
+ " \"type\": \"tfidf\",\n",
+ " \"kwargs\": {\n",
+ " \"base_vect_configs\": [\n",
+ " \n",
+ " {\n",
+ " \"ngram_range\": [1, 1],\n",
+ " \"max_df_ratio\": 0.98,\n",
+ " \"analyzer\": \"word\",\n",
+ " },\n",
+ " {\n",
+ " \"ngram_range\": [2, 2],\n",
+ " \"max_df_ratio\": 0.98,\n",
+ " \"analyzer\": \"word\",\n",
+ " },\n",
+ " {\n",
+ " \"ngram_range\": [3, 3],\n",
+ " \"max_df_ratio\": 0.98,\n",
+ " \"analyzer\": \"char_wb\",\n",
+ " },\n",
+ " ],\n",
+ " },\n",
+ " }\n",
+ "\n",
+ "preprocessor = Preprocessor.train(corpus, vectorizer_config)\n",
+ "preprocessor.save(model_folder) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a0300f8c",
+ "metadata": {},
+ "source": [
+ "#### Preprocessing with a Trained Text Preprocessor\n",
+ "\n",
+ "The function `predict` of a trained text preprocessor encodes texts in a **text data file** into a CSR matrix of shape `(num_inst, dim)` as numerical vector representations, where `num_inst` is the number of instances in the file; `dim` is the number of feature dimensions."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "3b182171",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The file consists of 4 instances with 405-dimensional features in a csr matrix.\n",
+ "\n",
+ "Text 0: Alan Turing is widely considered to be the father of theoretical computer science and artificial intelligence.\n",
+ "Text 1: Hinton was co-author of a highly cited paper published in 1986 that popularized the backpropagation algorithm for training multi-layer neural networks.\n",
+ "Text 2: Hinton received the 2018 Turing Award, together with Yoshua Bengio and Yann LeCun, for their work on artificial intelligence and deep learning.\n",
+ "Text 3: Yoshua Bengio is a Canadian computer scientist, most noted for his work on artificial neural networks and deep learning.\n",
+ "\n",
+ "The cosine similarity is 0.0076 between text 0 and text 1.\n",
+ "The cosine similarity is 0.0325 between text 0 and text 2.\n",
+ "The cosine similarity is 0.0082 between text 1 and text 2.\n",
+ "The cosine similarity is 0.0366 between text 0 and text 3.\n",
+ "The cosine similarity is 0.0267 between text 1 and text 3.\n",
+ "The cosine similarity is 0.0943 between text 2 and text 3.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Obtaining numerical vectors from text\n",
+ "X = preprocessor.predict(corpus)\n",
+ "\n",
+ "print(\"The file consists of {} instances \"\n",
+ " \"with {}-dimensional features \"\n",
+ " \"in a {} matrix.\\n\".format(*X.shape, X.getformat()))\n",
+ "\n",
+ "from sklearn.metrics.pairwise import cosine_similarity\n",
+ "\n",
+ "sim = cosine_similarity(X)\n",
+ "\n",
+ "for i, ti in enumerate(corpus):\n",
+ " print(\"Text {}: {}\".format(i, ti))\n",
+ "\n",
+ "print(\"\")\n",
+ "for i in range(X.shape[0]):\n",
+ " for j in range(i):\n",
+ " print(\"The cosine similarity is {:.4f} between text {} and text {}.\".format(sim[i][j], j, i))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18fcd09b",
+ "metadata": {},
+ "source": [
+ "#### Efficiency of PECOS Built-in TF-IDF Vectorizer\n",
+ "\n",
+ "Moreover, the TF-IDF vectorizer in PECOS is implemented in C++ and efficient."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "a3d6f675",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "PECOS TFIDF time: 27.30768s, result shaepe=(14146, 10858825), nnz=37194670\n"
+ ]
+ }
+ ],
+ "source": [
+ "vectorizer_config = {\n",
+ " \"type\": \"tfidf\",\n",
+ " \"kwargs\": {\n",
+ " \"base_vect_configs\": [ \n",
+ " {\n",
+ " \"ngram_range\": [1, 2],\n",
+ " \"max_df_ratio\": 0.98,\n",
+ " \"analyzer\": \"word\",\n",
+ " },\n",
+ " ],\n",
+ " },\n",
+ " }\n",
+ "\n",
+ "input_text_path = \"xmc-base/wiki10-31k/X.trn.txt\"\n",
+ "corpus = Preprocessor.load_data_from_file(input_text_path, text_pos=0)[\"corpus\"]\n",
+ "\n",
+ "import time\n",
+ "start_time = time.time()\n",
+ "preprocessor = Preprocessor.train(corpus, vectorizer_config)\n",
+ "X = preprocessor.predict(input_text_path)\n",
+ "print(f\"PECOS TFIDF time: {time.time() - start_time:.5f}s, result shaepe={X.shape}, nnz={X.nnz}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e63c62ce",
+ "metadata": {},
+ "source": [
+ "As a baseline method, we compare with the [Sklearn TFIDF vectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "id": "677f77de",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Sklearn TFIDF time: 221.65870s, result shaepe=(14146, 7269690), nnz=33505461\n"
+ ]
+ }
+ ],
+ "source": [
+ "start_time = time.time()\n",
+ "preprocessor = Preprocessor.train(\n",
+ " corpus,\n",
+ " {\"type\": \"sklearntfidf\", \"kwargs\":{\"ngram_range\": [1, 2], \"max_df\": 0.98}},\n",
+ ")\n",
+ "X = preprocessor.predict(corpus)\n",
+ "print(f\"Sklearn TFIDF time: {time.time() - start_time:.5f}s, result shaepe={X.shape}, nnz={X.nnz}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "75f5aaf8",
+ "metadata": {},
+ "source": [
+ "### Customized PECOS Model with TF-IDF Preprocessor\n",
+ "\n",
+ "\n",
+ "After being powered with text preprocessors, following the [aforementioned illustration](#Structure-of-a-Customized-PECOS-Model), we demonstrate an example of declaring a **customized PECOS model class** based on a TF-IDF preprocessor and a XR-Linear model."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "3893c23b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import json\n",
+ "from os import path\n",
+ "import pathlib\n",
+ "from pecos.utils.featurization.text.preprocess import Preprocessor\n",
+ "from pecos.xmc.xlinear.model import XLinearModel\n",
+ "from pecos.xmc import Indexer, LabelEmbeddingFactory\n",
+ "from pecos.utils import smat_util\n",
+ "\n",
+ "class CustomPECOS:\n",
+ " def __init__(self, preprocessor=None, xlinear_model=None, output_items=None):\n",
+ " self.preprocessor = preprocessor\n",
+ " self.xlinear_model = xlinear_model\n",
+ " self.output_items = output_items\n",
+ " \n",
+ " @classmethod\n",
+ " def train(cls, input_text_path, output_text_path):\n",
+ " \"\"\"Train a CustomPECOS model\n",
+ " \n",
+ " Args: \n",
+ " input_text_path (str): Text input file name. \n",
+ " output_text_path (str): The file path for output text items.\n",
+ " vectorizer_config (str): Json_format string for vectorizer config (default None). e.g. {\"type\": \"tfidf\", \"kwargs\": {}}\n",
+ " \n",
+ " Returns:\n",
+ " A CustomPECOS object\n",
+ " \"\"\"\n",
+ " # Obtain X_text, Y\n",
+ " parsed_result = Preprocessor.load_data_from_file(input_text_path, output_text_path)\n",
+ " Y = parsed_result[\"label_matrix\"]\n",
+ " corpus = parsed_result[\"corpus\"]\n",
+ "\n",
+ " # Train TF-IDF vectorizer\n",
+ " preprocessor = Preprocessor.train(corpus, {\"type\": \"tfidf\", \"kwargs\":{}}) \n",
+ " X = preprocessor.predict(corpus) \n",
+ " \n",
+ " # Train a XR-Linear model with TF-IDF features\n",
+ " label_feat = LabelEmbeddingFactory.create(Y, X, method=\"pifa\")\n",
+ " cluster_chain = Indexer.gen(label_feat)\n",
+ " xlinear_model = XLinearModel.train(X, Y, C=cluster_chain)\n",
+ " \n",
+ " # Load output items\n",
+ " with open(output_text_path, \"r\", encoding=\"utf-8\") as f:\n",
+ " output_items = [q.strip() for q in f]\n",
+ " \n",
+ " return cls(preprocessor, xlinear_model, output_items)\n",
+ " \n",
+ " def predict(self, corpus):\n",
+ " \"\"\"Predict labels for given inputs\n",
+ " \n",
+ " Args:\n",
+ " corpus (list of strings): input strings.\n",
+ " Returns:\n",
+ " csr_matrix: predicted label matrix (num_samples x num_labels)\n",
+ " \"\"\"\n",
+ " X = self.preprocessor.predict(corpus)\n",
+ " Y_pred = self.xlinear_model.predict(X)\n",
+ " return smat_util.sorted_csr(Y_pred)\n",
+ "\n",
+ " def save(self, model_folder):\n",
+ " \"\"\"Save the CustomPECOS model\n",
+ "\n",
+ " Args:\n",
+ " model_folder (str): folder name to save\n",
+ " \"\"\"\n",
+ " self.preprocessor.save(f\"{model_folder}/preprocessor\")\n",
+ " self.xlinear_model.save(f\"{model_folder}/xlinear_model\")\n",
+ " with open(f\"{model_folder}/output_items.json\", \"w\", encoding=\"utf-8\") as fp:\n",
+ " json.dump(self.output_items, fp)\n",
+ "\n",
+ " @classmethod\n",
+ " def load(cls, model_folder):\n",
+ " \"\"\"Load the CustomPECOS model\n",
+ "\n",
+ " Args:\n",
+ " model_folder (str): folder name to load\n",
+ " Returns:\n",
+ " CustomPECOS\n",
+ " \"\"\"\n",
+ " preprocessor = Preprocessor.load(f\"{model_folder}/preprocessor\")\n",
+ " xlinear_model = XLinearModel.load(f\"{model_folder}/xlinear_model\")\n",
+ " with open(f\"{model_folder}/output_items.json\", \"r\", encoding=\"utf-8\") as fin:\n",
+ " output_items = json.load(fin)\n",
+ " return cls(preprocessor, xlinear_model, output_items)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fcdbb2c6",
+ "metadata": {},
+ "source": [
+ "### Operating the Customized PECOS Model\n",
+ "\n",
+ "With a well-declared model class, the customized PECOS model can be modularized and very convenient to use."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "24134357",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Declare the path for model serialization and preprocessor configuration.\n",
+ "model_folder = \"./text2text_demo/pecos-CustomPECOS-model\"\n",
+ "\n",
+ "# Train and save the trained model\n",
+ "input_text_path = \"./text2text_demo/training-data.txt\"\n",
+ "output_text_path = \"./text2text_demo/output-labels.txt\"\n",
+ "model = CustomPECOS.train(input_text_path, output_text_path)\n",
+ "model.save(model_folder)\n",
+ "\n",
+ "# Load the trained model and predict\n",
+ "model = model.load(model_folder)\n",
+ "testing_text_path = \"./text2text_demo/testing-data.txt\"\n",
+ "Y_pred = model.predict(testing_text_path)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "id": "31efd9ac",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Text Input: In 1989, Yann LeCun et al. applied the standard backpropagation algorithm on neural networks for hand digit recognition.\n",
+ "Score 0.9515: Machine learning researchers\n",
+ "Score 0.8233: Artificial intelligence researchers\n",
+ "Score 0.4659: Deep Learning\n",
+ "Score 0.2779: British computer scientists\n",
+ "Score 0.0569: Turing Award laureates\n",
+ "Score 0.0129: Computability theorists\n"
+ ]
+ }
+ ],
+ "source": [
+ "test_texts = Preprocessor.load_data_from_file(testing_text_path, output_text_path)[\"corpus\"]\n",
+ "\n",
+ "for i, text in enumerate(test_texts):\n",
+ " print(\"Text Input: {}\".format(text))\n",
+ " for j in range(Y_pred.indptr[i], Y_pred.indptr[i + 1]):\n",
+ " pred_label = model.output_items[Y_pred.indices[j]]\n",
+ " pred_score = Y_pred.data[j]\n",
+ " print(\"Score {:.4f}: {}\".format(pred_score, pred_label))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/kdd22/Session 3 Approximate Nearest Neighbor Search in PECOS.ipynb b/tutorials/kdd22/Session 3 Approximate Nearest Neighbor Search in PECOS.ipynb
new file mode 100644
index 0000000..2574bd8
--- /dev/null
+++ b/tutorials/kdd22/Session 3 Approximate Nearest Neighbor Search in PECOS.ipynb
@@ -0,0 +1,495 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "e5073aac",
+ "metadata": {},
+ "source": [
+ "# Approximate Nearest Neighbor (ANN) Search in PECOS \n",
+ "\n",
+ "PECOS provides the efficient approach for **approximate nearest neighbor (ANN) search**. More specifically, after training an hierarchical navigable small world (HNSW) model (or buildling the **PECOS-HNSW indexer**) with a corpus of vectors, PECOS supports to efficiently infer top-K approximated nearest indexed vectors for an arbitrary query vector. In this part of the tutorial, we will demonstrate how to use PECOS-HNSW tackle the approximate nearest neighbor (ANN) search problem and how to integrate HNSW with PECOS XMR models.\n",
+ "\n",
+ "#### HNSW at a glimpse\n",
+ "The search procedure of HNSW can be summarized as:\n",
+ "* traverse from top layer (course-grain graph, long-range link) to bottom layer (fine-grain graph, short-range link)\n",
+ "* best first search traversal on each graph, where the best candidate serves as initial to next layer\n",
+ " \n",
+ "\n",
+ "\n",
+ "## Highlight of PECOS-HNSW\n",
+ "\n",
+ "* Support both sparse and dense input features\n",
+ "* Support SIMD instructions (SSE, AVX256, and AVX512)\n",
+ "* Modularity implementation\n",
+ "\n",
+ "## Comparison of PECOS and NMSLIB on the sparse data\n",
+ "\n",
+ "#### Disclaimer \n",
+ "The benchmarking results listed in this notebook are based on an `r5dn-24xlarge` AWS instance with 96 Intel(R) Xeon(R) Platinum 8259CL CPUs @ 2.50GHz. With distinct environments, the magnitude of improvments could be also different.\n",
+ "\n",
+ "#### Results\n",
+ "* We compare two implementations of HNSW: `PECOS` and `NMSLIB` on a sparse dataset (i.e., RCV1).\n",
+ "* For RCV1, the instances in training/test set are `781,265` and `23,149`, respectively. The feature dimension is `47,236`.\n",
+ "* The HNSW index is constructed under `M=16` and `efConstruction=500`.\n",
+ "* From the table below, we see that, under similar Recall@10, `PECOS` achieves `[88%,93%]` speedup compared to the `NMSLIB` package.\n",
+ "\n",
+ "| M=16, efC=500 | | | HNSW (PECOS) | | | HNSW (NMSLIB) | speedup (PECOS/NMSLIB) |\n",
+ "|:-------------:|:---------:|:-----------------------:|:------------------:|:---------:|:-----------------------:|:------------------:|:----------------------------:|\n",
+ "| efS | Recall@10 | Throughput (#query/sec) | Latency (ms/query) | Recall@10 | Throughput (#query/sec) | Latency (ms/query) | |\n",
+ "| 10 | 0.7733 | 5250.297 | 0.1905 | 0.7790 | 2710.256 | 0.3690 | 93.72% |\n",
+ "| 20 | 0.8545 | 3677.292 | 0.2719 | 0.8581 | 1924.505 | 0.5196 | 91.08% |\n",
+ "| 40 | 0.9043 | 2409.959 | 0.4149 | 0.9055 | 1271.085 | 0.7867 | 89.60% |\n",
+ "| 80 | 0.9325 | 1508.349 | 0.6630 | 0.9326 | 800.999 | 1.2484 | 88.31% |\n",
+ "| 120 | 0.9434 | 1125.047 | 0.8889 | 0.9426 | 597.873 | 1.6726 | 88.17% |\n",
+ "| 200 | 0.9533 | 763.752 | 1.3093 | 0.9523 | 404.518 | 2.4721 | 88.81% |\n",
+ "| 400 | 0.9621 | 433.872 | 2.3048 | 0.9608 | 229.553 | 4.3563 | 89.01% |\n",
+ "| 600 | 0.9657 | 305.747 | 3.2707 | 0.9644 | 161.879 | 6.1775 | 88.87% |\n",
+ "| 800 | 0.9678 | 237.651 | 4.2078 | 0.9663 | 124.806 | 8.0124 | 90.42% |\n",
+ "\n",
+ "## Hands-on Tutorial\n",
+ "\n",
+ "The life cycle of a PECOS-HNSW model consists of two stages:\n",
+ "\n",
+ "* building the indexer (training)\n",
+ "* inference (testing).\n",
+ "\n",
+ "### Data Loading"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "140a0d24",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "--2022-07-15 21:03:07-- https://archive.org/download/pecos-dataset/ann-benchmarks/rcv1-angular-47236.tar.gz\n",
+ "Resolving archive.org (archive.org)... 207.241.224.2\n",
+ "Connecting to archive.org (archive.org)|207.241.224.2|:443... connected.\n",
+ "HTTP request sent, awaiting response... 302 Found\n",
+ "Location: https://ia802308.us.archive.org/21/items/pecos-dataset/ann-benchmarks/rcv1-angular-47236.tar.gz [following]\n",
+ "--2022-07-15 21:03:07-- https://ia802308.us.archive.org/21/items/pecos-dataset/ann-benchmarks/rcv1-angular-47236.tar.gz\n",
+ "Resolving ia802308.us.archive.org (ia802308.us.archive.org)... 207.241.228.48\n",
+ "Connecting to ia802308.us.archive.org (ia802308.us.archive.org)|207.241.228.48|:443... connected.\n",
+ "HTTP request sent, awaiting response... 200 OK\n",
+ "Length: 317972212 (303M) [application/octet-stream]\n",
+ "Saving to: ‘rcv1-angular-47236.tar.gz’\n",
+ "\n",
+ "100%[======================================>] 317,972,212 11.0MB/s in 40s \n",
+ "\n",
+ "2022-07-15 21:03:47 (7.68 MB/s) - ‘rcv1-angular-47236.tar.gz’ saved [317972212/317972212]\n",
+ "\n",
+ "rcv1-angular-47236/\n",
+ "rcv1-angular-47236/X.trn.npz\n",
+ "rcv1-angular-47236/X.tst.npz\n",
+ "rcv1-angular-47236/Y.tst.npy\n"
+ ]
+ }
+ ],
+ "source": [
+ "! wget https://archive.org/download/pecos-dataset/ann-benchmarks/rcv1-angular-47236.tar.gz\n",
+ "! tar -zxvf ./rcv1-angular-47236.tar.gz"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "46dc982b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "n_trn 781265 n_tst 23149 data_dim 47236\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from pecos.utils import smat_util\n",
+ "X_trn = smat_util.load_matrix(\"./rcv1-angular-47236/X.trn.npz\").astype(np.float32)\n",
+ "X_tst = smat_util.load_matrix(\"./rcv1-angular-47236/X.tst.npz\").astype(np.float32)\n",
+ "Y_tst = smat_util.load_matrix(\"./rcv1-angular-47236/Y.tst.npy\")\n",
+ "print(\"n_trn {:7d} n_tst {:7d} data_dim {:7d}\".format(\n",
+ " X_trn.shape[0], X_tst.shape[0], X_trn.shape[1])\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bb73c619",
+ "metadata": {},
+ "source": [
+ "### Training Indexer\n",
+ "\n",
+ "To train a PECOS-HNSW model, training parameters need to be defined in an object of HNSW.TrainParams as the argument train_params. The key parameters of training a PECOS-HNSW model include:\n",
+ "* `M` (default 32): The maximum number of edges per node for each layer. A larger M leads to a larger model size and greater memory consumption. Higher/lower M are more suitable for high/low dimensional data or the pursue of high/low recall.\n",
+ "* `efC` (default 100): The size of the priority queue for best first search in construction. `efC` can be considered as the trade-off between efficiency and accuracy for indexing. A higher `efC` results in longer construction time but better quality of indexing.\n",
+ "* `metric_type` (default ip): The distance metric type for ANN search. PECOS-HNSW currently supports Euclidean distance (`l2`); and inner product (`ip`)\n",
+ "* `threads` (default -1): The number of threads for training, or -1 to use all available cores.\n",
+ "\n",
+ "The parameters for inference can be also decided as the argument pred_params during model construction so that the model can be directly applied for inference without further parameter designation.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "553aaf55",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "HNSW Indexer | M 32 efC 100 metric ip | time(s) 11.980276823043823\n"
+ ]
+ }
+ ],
+ "source": [
+ "import time\n",
+ "from pecos.ann.hnsw import HNSW\n",
+ "\n",
+ "M, efC = 32, 100\n",
+ "metric = \"ip\"\n",
+ "train_params = HNSW.TrainParams(\n",
+ " M=M,\n",
+ " efC=efC,\n",
+ " metric_type=metric,\n",
+ " threads=-1,\n",
+ ")\n",
+ "start_time = time.time()\n",
+ "model = HNSW.train(X_trn, train_params=train_params, pred_params=None)\n",
+ "print(\"HNSW Indexer | M {} efC {} metric {} | time(s) {}\".format(\n",
+ " M, efC, metric, time.time() - start_time),\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6a150c44",
+ "metadata": {},
+ "source": [
+ "### Save and Load Indexer"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "bf7905f3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model_folder = \"./rcv1.pecos-hnsw.index\"\n",
+ "model.save(model_folder)\n",
+ "del model\n",
+ "model = HNSW.load(model_folder)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b1af6ee7",
+ "metadata": {},
+ "source": [
+ "### Inference and Evaluation\n",
+ "\n",
+ "To conduct inference with a train HNSW model, prediction parameters need to be defined in an object of HNSW.PredParams as the argument pred_params. The key parameters of inference with a PECOS-HNSW model include:\n",
+ "\n",
+ "* `efS` (default 100): The size of the priority queue for best first search during inference. Similar to efC, efS can be considered as the trade-off between search efficiency and accuracy. A higher efS results in more accurate results with slower speed. efS is required to be greater than topk.\n",
+ "* `topk` (default 10): The number of approximate nearest neighbor to be returned. \n",
+ "* `threads` (default -1): The number of searchers for parallel inference, -1 to use all available searchers.\n",
+ "\n",
+ "The predict function derives the search results based on a query matrix of shape (# of data points for inference, # of dimentions) and `pred_params`, as well as searchers. The argument `ret_csr` (default `true`) decides the format of returned results as:\n",
+ "\n",
+ "* If `ret_csr` is false, the returned results would be two matrices of shape (# of data points, topk), which indicate the topk indices in the training corpus and the corresponding distances for each testing instance.\n",
+ "* If `ret_csr` is true, the returned results would be a [Compressed Sparse Row (CSR) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html) of shape (# of data points, # of points in the training corpus). Each row contains sorted topk distance values at the corresponding columns (i.e., indices in training corpus). The data for each row (i.e., `data[indptr[i]:indptr[i + 1]]`) are also sorted by the distance values.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "e25e31d4",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Prediction Time = 15.7988 seconds.\n"
+ ]
+ }
+ ],
+ "source": [
+ "pred_params = HNSW.PredParams(efS=100, topk=10)\n",
+ "searchers = model.searchers_create(num_searcher=1)\n",
+ "start_time = time.time()\n",
+ "indices, distances = model.predict(\n",
+ " X_tst,\n",
+ " pred_params=pred_params,\n",
+ " searchers=searchers,\n",
+ " ret_csr=False,\n",
+ ")\n",
+ "pred_time = time.time() - start_time\n",
+ "print(f\"Prediction Time = {pred_time:.4f} seconds.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0ce9aefa",
+ "metadata": {},
+ "source": [
+ "### Evaluation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "38401700",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def compute_recall(neighbors, true_neighbors):\n",
+ " total = 0\n",
+ " for gt_row, row in zip(true_neighbors, neighbors):\n",
+ " total += np.intersect1d(gt_row, row).shape[0]\n",
+ " return total / true_neighbors.size"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "b0b6d72a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "HNSW inference | R@10 0.9025 Throughput(q/s) 1465.236 latency(ms/q) 0.6825\n"
+ ]
+ }
+ ],
+ "source": [
+ "recall = compute_recall(indices, Y_tst)\n",
+ "throughput = indices.shape[0] / pred_time\n",
+ "latency = 1.0 / throughput * 1000.\n",
+ "print(f\"HNSW inference | R@10 {recall:.4f} Throughput(q/s) {throughput:8.3f} latency(ms/q) {latency:8.4f}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "75880f0d",
+ "metadata": {},
+ "source": [
+ "## Recall vs Throughput Trade-off"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "12bd7fb6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def run_pecos(X_trn, X_tst, Y_tst):\n",
+ " metric = \"ip\"\n",
+ " M_list = [16]\n",
+ " efC = 500\n",
+ " topk = 10\n",
+ " efS_list = [10, 20, 40, 80, 120, 200, 400, 600, 800]\n",
+ " for M in M_list:\n",
+ " train_params = HNSW.TrainParams(M=M, efC=efC, metric_type=metric, threads=-1)\n",
+ " start_time = time.time()\n",
+ " model = HNSW.train(X_trn, train_params=train_params, pred_params=None)\n",
+ " print(\"Indexer | M {} efC {} metric {} | train time(s) {}\".format(\n",
+ " M, efC, metric, time.time() - start_time)\n",
+ " )\n",
+ " \n",
+ " for efS in efS_list:\n",
+ " pred_params = HNSW.PredParams(efS=efS, topk=topk)\n",
+ " searchers = model.searchers_create(num_searcher=1)\n",
+ " \n",
+ " start_time = time.time()\n",
+ " indices, distances = model.predict(X_tst, pred_params=pred_params, searchers=searchers, ret_csr=False)\n",
+ " pred_time = time.time() - start_time\n",
+ " \n",
+ " recall = compute_recall(indices, Y_tst)\n",
+ " throughput = indices.shape[0] / pred_time\n",
+ " latency = 1.0 / throughput * 1000.\n",
+ " print(\"inference | efS {:3d} R@10 {:.4f} Throughput(q/s) {:8.3f} latency(ms/q) {:8.4f}\".format(\n",
+ " efS, recall, throughput, latency)\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "0b4af0fb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Indexer | M 16 efC 500 metric ip | train time(s) 46.87919640541077\n",
+ "inference | efS 10 R@10 0.7733 Throughput(q/s) 5250.297 latency(ms/q) 0.1905\n",
+ "inference | efS 20 R@10 0.8545 Throughput(q/s) 3677.292 latency(ms/q) 0.2719\n",
+ "inference | efS 40 R@10 0.9043 Throughput(q/s) 2409.959 latency(ms/q) 0.4149\n",
+ "inference | efS 80 R@10 0.9325 Throughput(q/s) 1508.349 latency(ms/q) 0.6630\n",
+ "inference | efS 120 R@10 0.9434 Throughput(q/s) 1125.047 latency(ms/q) 0.8889\n",
+ "inference | efS 200 R@10 0.9533 Throughput(q/s) 763.752 latency(ms/q) 1.3093\n",
+ "inference | efS 400 R@10 0.9621 Throughput(q/s) 433.872 latency(ms/q) 2.3048\n",
+ "inference | efS 600 R@10 0.9657 Throughput(q/s) 305.747 latency(ms/q) 3.2707\n",
+ "inference | efS 800 R@10 0.9678 Throughput(q/s) 237.651 latency(ms/q) 4.2078\n"
+ ]
+ }
+ ],
+ "source": [
+ "run_pecos(X_trn, X_tst, Y_tst)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b58276b2",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Appendix: Install PECOS and NMSLIB\n",
+ "\n",
+ "### Install via Conda \n",
+ "```bash\n",
+ "conda create -n pecos-hnsw-tutorial python=3.8\n",
+ "conda activate pecos-hnsw-tutorial\n",
+ "\n",
+ "pip install pyarrow pandas ipython jupyterlab\n",
+ "```\n",
+ "\n",
+ "### Install PECOS from Source\n",
+ "\n",
+ "We will install PECOS from source with the -march=native flag to optimize the best SIMD instruction available in your machine. More details available in https://github.com/amzn/pecos#installation-from-source\n",
+ "\n",
+ "```bash\n",
+ "# prerequisite, assuming amazon linux 2 \n",
+ "sudo yum -y install python3 python3-devel python3-distutils python3-venv && sudo yum -y groupinstall 'Development Tools' \n",
+ "sudo amazon-linux-extras install epel -y\n",
+ "sudo yum install openblas-devel -y\n",
+ "# pecos with -march=native flag\n",
+ "git clone https://github.com/amzn/pecos\n",
+ "cd pecos\n",
+ "PECOS_MANUAL_COMPILE_ARGS=\"-march=native\" python -m pip install --editable .\n",
+ "```\n",
+ "\n",
+ "### Install NMSLIB from Source\n",
+ "\n",
+ "We follow the install guide [install guide](https://github.com/erikbern/ann-benchmarks/blob/master/install/Dockerfile.nmslib) from ANN-Benchmark to install NMSLIB from source for the best performance.\n",
+ "\n",
+ "```bash\n",
+ "# pre-requisite, assuming amazon linux 2\n",
+ "sudo yum -y install cmake boost-devel eigen3-devel\n",
+ "git clone https://github.com/searchivarius/nmslib.git\n",
+ "cd nmslib/similarity_search\n",
+ "cmake . -DWITH_EXTRAS=1\n",
+ "make -j4\n",
+ "pip install pybind11\n",
+ "cd ../python_bindings/\n",
+ "python setup.py build\n",
+ "python setup.py install\n",
+ "python -c 'import nmslib'\n",
+ "```\n",
+ "\n",
+ "### Install via Docker (as in ANN-Benchmkark)\n",
+ "\n",
+ "```bash\n",
+ "# install some basic stuff\n",
+ "sudo yum -y update\n",
+ "sudo yum install -y git curl zip unzip vim gcc-c++ htop\n",
+ "\n",
+ "# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html\n",
+ "# sudo yum update -y\n",
+ "# sudo amazon-linux-extras install docker\n",
+ "sudo service docker start\n",
+ "sudo systemctl enable docker\n",
+ "sudo usermod -a -G docker ec2-user\n",
+ "docker info\n",
+ "```\n",
+ "\n",
+ "### Install Docker Image\n",
+ "\n",
+ "```bash\n",
+ "# install miniconda fist!\n",
+ "conda create -n ann-benchmarks python=3.8\n",
+ "conda activate ann-benchmarks\n",
+ "\n",
+ "# install ANN package supported by ann-benchmarks\n",
+ "git clone https://github.com/erikbern/ann-benchmarks.git\n",
+ "cd ann-benchmarks\n",
+ "pip install -r requirements.txt\n",
+ "\n",
+ "# install docker containers\n",
+ "python -u install.py --algorithm faiss\n",
+ "python -u install.py --algorithm hnswlib\n",
+ "python -u install.py --algorithm n2\n",
+ "python -u install.py --algorithm pecos\n",
+ "python -u install.py --algorithm scann\n",
+ "python -u install.py --algorithm ngt\n",
+ "python -u install.py --algorithm nmslib\n",
+ "python -u install.py --algorithm diskann\n",
+ "python -u install.py --algorithm pynndescent\n",
+ "\n",
+ "# list all dockers\n",
+ "docker image ls\n",
+ "REPOSITORY TAG IMAGE ID CREATED SIZE\n",
+ "ann-benchmarks-hnswlib latest 2e1ea8d11df7 2 hours ago 1.04GB\n",
+ "ann-benchmarks-nmslib latest 1e094d3e96f7 3 hours ago 1.64GB\n",
+ "ann-benchmarks-faiss latest 44e5bd15bfcd 5 hours ago 4.9GB\n",
+ "ann-benchmarks-scann latest 5151abe3b09e 5 hours ago 2.76GB\n",
+ "ann-benchmarks latest c2c612131da4 5 hours ago 938MB\n",
+ "```\n",
+ "\n",
+ "### Enter Docker Env\n",
+ "\n",
+ "```bash\n",
+ "EFS_DIR=/PATH/TO/pecos-hnsw-kdd22\n",
+ "DOCKER_IMAGE=ann-benchmarks-nmslib\n",
+ "\n",
+ "docker run --rm -it -v ${EFS_DIR}:/home/app/ws \\\n",
+ " --entrypoint /bin/bash ${DOCKER_IMAGE}\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b0b85b8a-f1a6-42f0-acf0-6596361e35b3",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/kdd22/Session 4 Utilities in PECOS.ipynb b/tutorials/kdd22/Session 4 Utilities in PECOS.ipynb
new file mode 100644
index 0000000..18cd0c8
--- /dev/null
+++ b/tutorials/kdd22/Session 4 Utilities in PECOS.ipynb
@@ -0,0 +1,1105 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b1ebc316",
+ "metadata": {},
+ "source": [
+ "# Utilities in PECOS\n",
+ "\n",
+ "PECOS provides various useful interfaces and utility functions for XMR problems and related tasks. In this session, we will introduce how to tackle arbitrary data formats for XMR, and then present some utilities in PECOS for efficient matrix operations and hierarchical clustering."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3e9cc45c",
+ "metadata": {},
+ "source": [
+ "## Working with Arbitrary Data Formats\n",
+ "\n",
+ "PECOS is a general machine learning framework and able to fit arbitary data format and interact with different data manipulation and analysis libraries like [Pandas](https://pandas.pydata.org/). In the following example, we will show how to learn a PECOS model with Pandas-loaded data of text, categorical, and numerical features."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "9f0f17a7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pecos\n",
+ "import pandas as pd\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "526820f8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Archive: drugLib_raw.zip\r\n",
+ " inflating: drugLibTest_raw.tsv \r\n",
+ " inflating: drugLibTrain_raw.tsv \r\n"
+ ]
+ }
+ ],
+ "source": [
+ "! wget -nv -nc https://archive.ics.uci.edu/ml/machine-learning-databases/00461/drugLib_raw.zip\n",
+ "! unzip -o drugLib_raw.zip"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "ac8ab46f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training DataFrame consists of 3107 instances.\n",
+ "Testing DataFrame consists of 1036 instances.\n",
+ "Index(['Unnamed: 0', 'urlDrugName', 'rating', 'effectiveness', 'sideEffects',\n",
+ " 'condition', 'benefitsReview', 'sideEffectsReview', 'commentsReview'],\n",
+ " dtype='object')\n"
+ ]
+ }
+ ],
+ "source": [
+ "train_df = pd.read_csv(\"drugLibTrain_raw.tsv\", sep=\"\\t\")\n",
+ "test_df = pd.read_csv(\"drugLibTest_raw.tsv\", sep=\"\\t\")\n",
+ "print(f\"Training DataFrame consists of {len(train_df)} instances.\")\n",
+ "print(f\"Testing DataFrame consists of {len(test_df)} instances.\")\n",
+ "print(train_df.columns)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "6d6ee989",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "label_name = \"effectiveness\"\n",
+ "text_features = [\"condition\", \"benefitsReview\", \"sideEffectsReview\", \"commentsReview\"]\n",
+ "categorical_features = [\"sideEffects\"]\n",
+ "numerical_features = [\"rating\"]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "c25b1cb2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "X_trn_list = []\n",
+ "X_tst_list = []"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f72d047",
+ "metadata": {},
+ "source": [
+ "### Label Encoding\n",
+ "\n",
+ "To encode labels into the sparse matrix format compatible to PECOS, [OneHotEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MultiLabelBinarizer.html#sklearn.preprocessing.MultiLabelBinarizer) and [MultiLabelBinarizer](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MultiLabelBinarizer.html#sklearn.preprocessing.MultiLabelBinarizer) are helpful for the scenarios of multi-class and multi-label classification."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "4f0d9ec7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Y_trn is a csr matrix with a shape (3107, 5) and 3107 non-zero values.\n",
+ "Y_tst is a csr matrix with a shape (1036, 5) and 1036 non-zero values.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.preprocessing import OneHotEncoder\n",
+ "\n",
+ "label_encoder = OneHotEncoder(dtype=np.float32)\n",
+ "Y_trn = label_encoder.fit_transform(train_df[[label_name]])\n",
+ "Y_tst = label_encoder.transform(test_df[[label_name]])\n",
+ "\n",
+ "print(f\"Y_trn is a {Y_trn.getformat()} matrix with a shape {Y_trn.shape} and {Y_trn.nnz} non-zero values.\")\n",
+ "print(f\"Y_tst is a {Y_tst.getformat()} matrix with a shape {Y_tst.shape} and {Y_tst.nnz} non-zero values.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "62ec7371",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Y_trn_mlb is a csr matrix with a shape (3107, 5) and 3107 non-zero values.\n",
+ "Y_tst_mlb is a csr matrix with a shape (1036, 5) and 1036 non-zero values.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.preprocessing import MultiLabelBinarizer\n",
+ "\n",
+ "label_encoder_multilabel = MultiLabelBinarizer(sparse_output=True)\n",
+ "Y_trn_mlb = label_encoder.fit_transform([[lbl] for lbl in train_df[label_name].tolist()])\n",
+ "Y_tst_mlb = label_encoder.fit_transform([[lbl] for lbl in test_df[label_name].tolist()])\n",
+ "print(f\"Y_trn_mlb is a {Y_trn_mlb.getformat()} matrix with a shape {Y_trn_mlb.shape} and {Y_trn_mlb.nnz} non-zero values.\")\n",
+ "print(f\"Y_tst_mlb is a {Y_tst_mlb.getformat()} matrix with a shape {Y_tst_mlb.shape} and {Y_tst_mlb.nnz} non-zero values.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "260a9a8a",
+ "metadata": {},
+ "source": [
+ "### Text Feature Encoding\n",
+ "\n",
+ "As introduced in Session 1, we can use PECOS vectorizer for featurize text data. In addition, the encoder of [XR-Transformer](https://github.com/amzn/pecos/tree/mainline/pecos/xmc/xtransformer) can be also utilized for deriving text features with proper fine-tuning."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "96fef619",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "condition: (3107, 3759) and (1036, 3759) in training and testing.\n",
+ "benefitsReview: (3107, 72861) and (1036, 72861) in training and testing.\n",
+ "sideEffectsReview: (3107, 64321) and (1036, 64321) in training and testing.\n",
+ "commentsReview: (3107, 91731) and (1036, 91731) in training and testing.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.utils.featurization.text.vectorizers import Vectorizer\n",
+ "\n",
+ "for feature_name in text_features:\n",
+ " vectorizer_config = {\n",
+ " \"type\": \"tfidf\",\n",
+ " \"kwargs\": {\n",
+ " \"base_vect_configs\": [\n",
+ "\n",
+ " {\n",
+ " \"ngram_range\": [1, 2],\n",
+ " \"max_df_ratio\": 0.98,\n",
+ " \"analyzer\": \"word\",\n",
+ " },\n",
+ " ],\n",
+ " },\n",
+ " } \n",
+ " train_texts = [str(x) for x in train_df[feature_name].tolist()]\n",
+ " test_texts = test_df[feature_name].tolist()\n",
+ " vectorizer = Vectorizer.train(train_texts, config=vectorizer_config)\n",
+ " X_trn_local = vectorizer.predict(train_texts)\n",
+ " X_tst_local = vectorizer.predict(test_texts)\n",
+ " print(f\"{feature_name}: {X_trn_local.shape} and {X_tst_local.shape} in training and testing.\")\n",
+ " \n",
+ " X_trn_list.append(X_trn_local)\n",
+ " X_tst_list.append(X_tst_local)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "38e75fa2",
+ "metadata": {},
+ "source": [
+ "### Categorical Feature Encoding\n",
+ "\n",
+ "Similar to labels, categorical features can also be considered as one-hot or multi-hot embeddings."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "386b96ad",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "sideEffects: (3107, 5) and (1036, 5) in training and testing.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.preprocessing import OneHotEncoder\n",
+ "\n",
+ "for feature_name in categorical_features:\n",
+ " local_encoder = OneHotEncoder(dtype=np.float32)\n",
+ " X_trn_local = local_encoder.fit_transform(train_df[[feature_name]])\n",
+ " X_tst_local = local_encoder.transform(test_df[[feature_name]])\n",
+ " print(f\"{feature_name}: {X_trn_local.shape} and {X_tst_local.shape} in training and testing.\")\n",
+ " \n",
+ " X_trn_list.append(X_trn_local)\n",
+ " X_tst_list.append(X_tst_local)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6deaf4e8",
+ "metadata": {},
+ "source": [
+ "### Numerical Features Encoding\n",
+ "\n",
+ "Numberical features can be directly incorporated as model inputs after some simple normalization."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "90668ea4",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "rating: (3107, 1) and (1036, 1) in training and testing.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from scipy.sparse import csr_matrix\n",
+ "from sklearn.preprocessing import StandardScaler\n",
+ "\n",
+ "for feature_name in numerical_features:\n",
+ " X_trn_values = train_df[[\"rating\"]].values\n",
+ " X_tst_values = test_df[[\"rating\"]].values\n",
+ " scaler = StandardScaler()\n",
+ " X_trn_local = csr_matrix(scaler.fit_transform(X_trn_values), dtype=np.float32)\n",
+ " X_tst_local = csr_matrix(scaler.transform(X_tst_values), dtype=np.float32)\n",
+ " print(f\"{feature_name}: {X_trn_local.shape} and {X_tst_local.shape} in training and testing.\")\n",
+ " \n",
+ " X_trn_list.append(X_trn_local)\n",
+ " X_tst_list.append(X_tst_local)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d2441580",
+ "metadata": {},
+ "source": [
+ "### Feature Concatenation\n",
+ "\n",
+ "PECOS provides easy-going utility functions for efficient matrix operations. The `hstack_csr` function can concatenate different features for each individual instance. More detils about other utilities will be introduced later in this session."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "d0d3e69c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "X_trn is a csr matrix with a shape (3107, 232678) and 653987 non-zero values.\n",
+ "X_tst is a csr matrix with a shape (1036, 232678) and 164272 non-zero values.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.utils import smat_util\n",
+ "\n",
+ "X_trn = smat_util.hstack_csr(X_trn_list)\n",
+ "X_tst = smat_util.hstack_csr(X_tst_list)\n",
+ "\n",
+ "print(f\"X_trn is a {X_trn.getformat()} matrix with a shape {X_trn.shape} and {X_trn.nnz} non-zero values.\")\n",
+ "print(f\"X_tst is a {X_tst.getformat()} matrix with a shape {X_tst.shape} and {X_tst.nnz} non-zero values.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e61775b",
+ "metadata": {},
+ "source": [
+ "### Model Training and Testing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "38189597",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "prec = 52.80 40.69 30.92 24.52 20.00\n",
+ "recall = 52.80 81.37 92.76 98.07 100.00\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.xlinear.model import XLinearModel\n",
+ "xlm = XLinearModel.train(X_trn, Y_trn)\n",
+ "\n",
+ "Y_pred = xlm.predict(X_tst, beam_size=10, only_topk=5)\n",
+ "metrics = smat_util.Metrics.generate(Y_tst, Y_pred, topk=5)\n",
+ "print(metrics)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c2b3f61e",
+ "metadata": {},
+ "source": [
+ "## Sparse Matrix Operations\n",
+ "\n",
+ "Most of the computations in PECOS are based on sparse matrices, so PECOS also provides various useful and efficient operation utilities for sparse matrices."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5fba58d4",
+ "metadata": {},
+ "source": [
+ "### Genric Matriox IO and Conversion\n",
+ "\n",
+ "`smat_util.load_matrix` and `smat_util.save_matrix` provide generic interfaces for loading and storing matrices in arbitrary common formats, including [dense matrix](https://numpy.org/doc/stable/reference/generated/numpy.array.html) in NumPy or different sparse matrix formats (i.e., [sparse Compressed Sparse Row (CSR) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html), [sparse Compressed Sparse Column (CSC) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html), and [sparse COOrdinate (COO) matrix](https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.html))."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "385ed0ba",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Dense Matrtix IO\n",
+ "mat is a matrix with a shape (2, 3).\n",
+ "[[0.6757516 0.42168422 0.40557039]\n",
+ " [0.86806547 0.9198075 0.7494449 ]]\n",
+ "mat_loaded is a matrix with a shape (2, 3).\n",
+ "[[0.6757516 0.42168422 0.40557039]\n",
+ " [0.86806547 0.9198075 0.7494449 ]]\n",
+ "\n",
+ "csr Sparse Matrix IO\n",
+ "mat is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (1, 2)\t0.17821196669035588\n",
+ " (2, 1)\t0.8259001065480657\n",
+ " (4, 2)\t0.5111159408743305\n",
+ " (4, 3)\t0.6337428297507509\n",
+ "mat_loaded is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (1, 2)\t0.17821196669035588\n",
+ " (2, 1)\t0.8259001065480657\n",
+ " (4, 2)\t0.5111159408743305\n",
+ " (4, 3)\t0.6337428297507509\n",
+ "\n",
+ "csc Sparse Matrix IO\n",
+ "mat is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (0, 1)\t0.868116915403953\n",
+ " (1, 2)\t0.7454473997071077\n",
+ " (0, 3)\t0.21167432752493887\n",
+ " (1, 3)\t0.4685535255015949\n",
+ "mat_loaded is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (0, 1)\t0.868116915403953\n",
+ " (1, 2)\t0.7454473997071077\n",
+ " (0, 3)\t0.21167432752493887\n",
+ " (1, 3)\t0.4685535255015949\n",
+ "\n",
+ "coo Sparse Matrix IO\n",
+ "mat is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (1, 0)\t0.3217900085041965\n",
+ " (3, 3)\t0.15316424313380772\n",
+ " (2, 3)\t0.7835729602784944\n",
+ " (2, 2)\t0.396664789900256\n",
+ "mat_loaded is a matrix with a shape (5, 4) and 4 non-zero values.\n",
+ " (1, 0)\t0.3217900085041965\n",
+ " (3, 3)\t0.15316424313380772\n",
+ " (2, 3)\t0.7835729602784944\n",
+ " (2, 2)\t0.396664789900256\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.utils import smat_util\n",
+ "import numpy as np\n",
+ "import scipy.sparse as smat\n",
+ "\n",
+ "print(\"Dense Matrtix IO\")\n",
+ "mat = np.random.rand(2, 3)\n",
+ "print(f\"mat is a {type(mat)} matrix with a shape {mat.shape}.\")\n",
+ "print(mat)\n",
+ "smat_util.save_matrix(\"mat.npz\", mat)\n",
+ "mat_loaded = smat_util.load_matrix(\"mat.npz\")\n",
+ "print(f\"mat_loaded is a {type(mat_loaded)} matrix with a shape {mat_loaded.shape}.\")\n",
+ "print(mat)\n",
+ "print(\"\") \n",
+ "\n",
+ "for matrix_format in [\"csr\", \"csc\", \"coo\"]:\n",
+ " print(f\"{matrix_format} Sparse Matrix IO\")\n",
+ " mat = smat.random(5, 4, density=0.2, format=matrix_format)\n",
+ " print(f\"mat is a {type(mat)} matrix\"\n",
+ " f\" with a shape {mat.shape} and {mat.nnz} non-zero values.\")\n",
+ " print(mat)\n",
+ " \n",
+ " smat_util.save_matrix(\"mat.npz\", mat)\n",
+ " mat_loaded = smat_util.load_matrix(\"mat.npz\")\n",
+ " print(f\"mat_loaded is a {type(mat_loaded)} matrix\"\n",
+ " f\" with a shape {mat_loaded.shape} and {mat_loaded.nnz} non-zero values.\")\n",
+ " print(mat_loaded)\n",
+ " print(\"\") "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "2579b855",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Original Matrix mat\n",
+ " [[7.75480297e-01 3.06141999e-01 1.48439313e-01 4.82371302e-01\n",
+ " 7.94080899e-01 4.22684703e-04]\n",
+ " [1.22368102e-01 9.06639305e-02 5.88889479e-01 3.37880581e-01\n",
+ " 7.45595442e-01 9.58851999e-01]\n",
+ " [5.65962202e-01 7.18344997e-01 1.99721347e-01 2.02474399e-01\n",
+ " 5.86650110e-01 1.93471414e-01]\n",
+ " [5.72012816e-02 1.20470044e-01 1.27986695e-01 6.43206432e-01\n",
+ " 5.42874998e-01 7.05113274e-01]] \n",
+ "\n",
+ "csr_mat = dense_to_csr(mat)\n",
+ "csr_mat is a matrix with a shape (4, 6) and 24 non-zero values.\n",
+ "[[7.75480297e-01 3.06141999e-01 1.48439313e-01 4.82371302e-01\n",
+ " 7.94080899e-01 4.22684703e-04]\n",
+ " [1.22368102e-01 9.06639305e-02 5.88889479e-01 3.37880581e-01\n",
+ " 7.45595442e-01 9.58851999e-01]\n",
+ " [5.65962202e-01 7.18344997e-01 1.99721347e-01 2.02474399e-01\n",
+ " 5.86650110e-01 1.93471414e-01]\n",
+ " [5.72012816e-02 1.20470044e-01 1.27986695e-01 6.43206432e-01\n",
+ " 5.42874998e-01 7.05113274e-01]] \n",
+ "\n",
+ "csr_mat_topk = dense_to_csr(mat, topk=2)\n",
+ "csr_mat is a matrix with a shape (4, 6) and 8 non-zero values.\n",
+ "[[0.7754803 0. 0. 0. 0.7940809 0. ]\n",
+ " [0. 0. 0. 0. 0.74559544 0.958852 ]\n",
+ " [0.5659622 0. 0. 0. 0.58665011 0. ]\n",
+ " [0. 0. 0. 0. 0.542875 0.70511327]] \n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "mat = np.random.rand(4, 6)\n",
+ "\n",
+ "print(f\"Original Matrix mat\\n\", mat, \"\\n\")\n",
+ "\n",
+ "print(\"csr_mat = dense_to_csr(mat)\")\n",
+ "csr_mat = smat_util.dense_to_csr(mat)\n",
+ "print(f\"csr_mat is a {type(csr_mat)} matrix\"\n",
+ " f\" with a shape {csr_mat.shape} and {csr_mat.nnz} non-zero values.\")\n",
+ "print(csr_mat.toarray(), \"\\n\")\n",
+ "\n",
+ "print(\"csr_mat_topk = dense_to_csr(mat, topk=2)\")\n",
+ "csr_mat_topk = smat_util.dense_to_csr(mat, topk=2)\n",
+ "print(f\"csr_mat is a {type(csr_mat_topk)} matrix\"\n",
+ " f\" with a shape {csr_mat_topk.shape} and {csr_mat_topk.nnz} non-zero values.\")\n",
+ "print(csr_mat_topk.toarray(), \"\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b2746e3c",
+ "metadata": {},
+ "source": [
+ "### Memory-efficient Sparse Matrix Operations\n",
+ "\n",
+ "To manipulate with sparse matrix, PECOS provides many useful memory-efficient functions. For example, for CSR matrices, we have following functions to combine multiple matrices.\n",
+ "\n",
+ "* `hstack_csr([mat, mat, mat]`\n",
+ "* `vstack_csr([mat, mat, mat]`\n",
+ "* `block_diag_csr([mat, mat, mat]`\n",
+ "\n",
+ "These funcations are also available for CSC matrices as `hstack_csc`, `vstack_csr`, and `block_diag_csr`.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b9dac617",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Original Matrix mat\n",
+ " [[0.45989041 0. ]\n",
+ " [0.77400378 0. ]\n",
+ " [0. 0.44557291]] \n",
+ "\n",
+ "hstack_csr([mat, mat, mat])\n",
+ "[[0.45989041 0. 0.45989041 0. 0.45989041 0. ]\n",
+ " [0.77400378 0. 0.77400378 0. 0.77400378 0. ]\n",
+ " [0. 0.44557291 0. 0.44557291 0. 0.44557291]] \n",
+ "\n",
+ "vstack_csr([mat, mat, mat])\n",
+ "[[0.45989041 0. ]\n",
+ " [0.77400378 0. ]\n",
+ " [0. 0.44557291]\n",
+ " [0.45989041 0. ]\n",
+ " [0.77400378 0. ]\n",
+ " [0. 0.44557291]\n",
+ " [0.45989041 0. ]\n",
+ " [0.77400378 0. ]\n",
+ " [0. 0.44557291]] \n",
+ "\n",
+ "block_diag_csr([mat, mat, mat])\n",
+ "[[0.45989041 0. 0. 0. 0. 0. ]\n",
+ " [0.77400378 0. 0. 0. 0. 0. ]\n",
+ " [0. 0.44557291 0. 0. 0. 0. ]\n",
+ " [0. 0. 0.45989041 0. 0. 0. ]\n",
+ " [0. 0. 0.77400378 0. 0. 0. ]\n",
+ " [0. 0. 0. 0.44557291 0. 0. ]\n",
+ " [0. 0. 0. 0. 0.45989041 0. ]\n",
+ " [0. 0. 0. 0. 0.77400378 0. ]\n",
+ " [0. 0. 0. 0. 0. 0.44557291]] \n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.utils import smat_util\n",
+ "import scipy.sparse as smat\n",
+ "\n",
+ "mat = smat.random(3, 2, density=0.5, format=\"csr\")\n",
+ "print(f\"Original Matrix {type(mat)} mat\\n\", mat.toarray(), \"\\n\")\n",
+ "\n",
+ "print(f\"hstack_csr([mat, mat, mat])\")\n",
+ "print(smat_util.hstack_csr([mat, mat, mat]).toarray(), \"\\n\")\n",
+ "\n",
+ "print(f\"vstack_csr([mat, mat, mat])\")\n",
+ "print(smat_util.vstack_csr([mat, mat, mat]).toarray(), \"\\n\")\n",
+ "\n",
+ "print(f\"block_diag_csr([mat, mat, mat])\")\n",
+ "print(smat_util.block_diag_csr([mat, mat, mat]).toarray(), \"\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5f9cf7f1",
+ "metadata": {},
+ "source": [
+ "### Sparse-to-sparse Matrix Multiplication (SpMM)\n",
+ "\n",
+ "Many operations in PECOS or XMR problems rely on Sparse-to-sparse Matrix Multiplication (SpMM), such as the computation of PIFA features. It is also one of the key primitives in large-scale linear algebra operations, with a broad range of applications in machine learning and natural language processing.\n",
+ "\n",
+ "For SpMM, PECOS provides a highly optimized multi-core CPU implementation with state-of-the-art performance, where the underlying operations are implemented and optimized in C/C++.\n",
+ "Specifically, the Python interface and parameters are as follows:\n",
+ "\n",
+ "```python\n",
+ "from pecos.core import clib as pecos_clib\n",
+ "Z = pecos_clib.sparse_matmul(X, Y, eliminate_zeros=False, sorted_indices=True, threads=-1)\n",
+ "```\n",
+ "* Parameters\n",
+ " * `X` (scipy.sparse.csr_matrix or scipy.sparse.csc_matrix): the first sparse matrix to be multiplied.\n",
+ " * `Y` (scipy.sparse.csr_matrix or scipy.sparse.csc_matrix): the second sparse matrix to be multiplied.\n",
+ " * `eliminate_zeros` (bool, optional): if true, then eliminate (potential) zeros created by maxnnz in output matrix Z. Default is false.\n",
+ " * `sorted_indices` (bool, optional): if true, then sort the Z.indices for the output matrix Z. Default is true.\n",
+ " * `threads` (int, optional): The number of threads. Default -1 to use all CPU cores."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "b2e6c54a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "||Z_true - Z_pred|| = 0.0\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import scipy.sparse as smat\n",
+ "from scipy.sparse import linalg\n",
+ "from pecos.core import clib as pecos_clib\n",
+ "X = smat.random(1000, 1000, density=0.01, format='csr', dtype=np.float32)\n",
+ "Y = smat.random(1000, 1000, density=0.01, format='csr', dtype=np.float32)\n",
+ "Z_true = X.dot(Y)\n",
+ "Z_pred = pecos_clib.sparse_matmul(X, Y)\n",
+ "print(\"||Z_true - Z_pred|| = \", linalg.norm(Z_true - Z_pred))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "14e4bded",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "DATASET = \"wiki10-31k\"\n",
+ "X = smat_util.load_matrix(f\"xmc-base/{DATASET}/tfidf-attnxml/X.trn.npz\").astype(np.float32)\n",
+ "Y = smat_util.load_matrix(f\"xmc-base/{DATASET}/Y.trn.npz\").astype(np.float32)\n",
+ "YT_csr = Y.T.tocsr()\n",
+ "X_csr = X.tocsr()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bb5af1e9",
+ "metadata": {},
+ "source": [
+ "#### Benchmarking Sparse Matrix Muplication\n",
+ "\n",
+ "The SpMM utility has state-of-the-art performance in efficiency as shown in the following figure.\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "In this part, we provide some hands-on instructions for benchmarking different methods for SpMM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "c71cffdb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Benchmarking SciPy\n",
+ "\n",
+ "start = time.time()\n",
+ "Z = YT_csr.dot(X_csr)\n",
+ "Z.sort_indices()\n",
+ "run_time_scipy = time.time() - start"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "2f2b9795",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Benchmarking PyTorch\n",
+ "\n",
+ "import torch\n",
+ "\n",
+ "def csr_to_coo(A):\n",
+ " A_coo = smat.coo_matrix(A)\n",
+ " indices = np.vstack([A_coo.row, A_coo.col]).T\n",
+ " values = A_coo.data\n",
+ " return indices, values\n",
+ "\n",
+ "def get_pt_data(A_csr):\n",
+ " A_indices, A_values = csr_to_coo(A_csr)\n",
+ " A_pt = torch.sparse_coo_tensor(\n",
+ " A_indices.T.astype(np.int64),\n",
+ " A_values.astype(np.float32),\n",
+ " A_csr.shape,\n",
+ " )\n",
+ " return A_pt\n",
+ " \n",
+ "YT_pt = get_pt_data(YT_csr)\n",
+ "X_pt = get_pt_data(X_csr)\n",
+ "start = time.time()\n",
+ "Z_pt = torch.sparse.mm(YT_pt, X_pt)\n",
+ "run_time_pytorch = time.time() - start"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "54b24694",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Benchmarking PECOS\n",
+ "\n",
+ "start = time.time()\n",
+ "Z = pecos_clib.sparse_matmul(\n",
+ " YT_csr, X_csr,\n",
+ " eliminate_zeros=False,\n",
+ " sorted_indices=True\n",
+ ")\n",
+ "run_time_pecos = time.time() - start"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "e12f29e9",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from matplotlib import pyplot as plt\n",
+ "plt.bar(\n",
+ " [1,2,3],\n",
+ " [run_time_scipy, run_time_pytorch, run_time_pecos],\n",
+ " tick_label = [\"SciPy\", \"PyTorch\", \"PECOS\"])\n",
+ "\n",
+ "plt.ylabel(\"Matrix Multiplication Time (seconds)\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1f833846",
+ "metadata": {},
+ "source": [
+ "## Hierarchical Clustering\n",
+ "\n",
+ "Hierarchical clustering is an essential function for tree-based XMR models and plays a role of the indexer in PECOS. Accordingly, PECOS also implements hierarchical K-means algorithms in the manner of efficient C/C++, which can also be considered as useful functions for arbitrary tasks. The Python interface of PECOS hierarchical K-means algorithms is as follows:\n",
+ "\n",
+ "```python\n",
+ "from pecos.xmc import HierarchicalKMeans\n",
+ "HierarchicalKMeans.gen(feature_matrix, ... [training parameters])\n",
+ "```\n",
+ "* Training Parameters\n",
+ " * `nr_splits` (int, optional): The out-degree of each internal node of the tree. Ignored if `imbalanced_ratio != 0` because imbalanced clustering supports only 2-means. Default is `16`.\n",
+ " * `min_codes` (int): The number of direct child nodes that the top level of the hierarchy should have.\n",
+ " * `max_leaf_size` (int, optional): The maximum size of each leaf node of the tree. Default is `100`.\n",
+ " * `spherical` (bool, optional): True will l2-normalize the centroids of k-means after each iteration. Default is `True`.\n",
+ " * `seed` (int, optional): Random seed. Default is `0`.\n",
+ " * `kmeans_max_iter` (int, optional): Maximum number of iterations for each k-means problem. Default is `20`.\n",
+ " * `threads` (int, optional): Number of threads to use. `-1` denotes all CPUs. Default is `-1`.\n",
+ " \n",
+ "#### Clustering Chains\n",
+ "\n",
+ "Similar to the results of semantic label indexing in PECOS, the hierarchical clustering results will be returned as a list of `D` CSC matrices `C[d]` to denote hierarchical cluster assignments over layers, where `D` is the layers of resulting hierarchical clusters."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3b3b073c",
+ "metadata": {},
+ "source": [
+ "### Naive Clustering as Degenerated Hierarchical Clustering\n",
+ "\n",
+ "When `min_codes` and `max_leaf_size` as the stopping criteria are large enough, the hierarchical clustering will be degenerated to conventional naive clustering."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "4a5da8c1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pecos.utils import smat_util\n",
+ "import time\n",
+ "DATASET = \"wiki10-31k\"\n",
+ "X = smat_util.load_matrix(f\"xmc-base/{DATASET}/tfidf-attnxml/X.trn.npz\").astype(np.float32)\n",
+ "Y = smat_util.load_matrix(f\"xmc-base/{DATASET}/Y.trn.npz\").astype(np.float32)\n",
+ "YT_csr = Y.T.tocsr()\n",
+ "X_csr = X.tocsr()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "088d87a3",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2 layers in the trained hierarchical clusters with C[d] as:\n",
+ "cluster_chain[0] is a csc matrix of shape (4, 1).\n",
+ "cluster_chain[1] is a csc matrix of shape (14146, 4).\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.base import HierarchicalKMeans\n",
+ "import scipy.sparse as smat\n",
+ "import numpy as np\n",
+ "\n",
+ "num_splits = 4\n",
+ "cluster_chain = HierarchicalKMeans.gen(\n",
+ " X_csr,\n",
+ " min_codes=num_splits,\n",
+ " nr_splits=num_splits,\n",
+ " max_leaf_size=np.ceil(X_csr.shape[0]/num_splits))\n",
+ "\n",
+ "print(f\"{len(cluster_chain)} layers in the trained hierarchical clusters with C[d] as:\")\n",
+ "for d, C in enumerate(cluster_chain):\n",
+ " print(f\"cluster_chain[{d}] is a {C.getformat()} matrix of shape {C.shape}.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "ef1c9ee1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(14146,) (14146,)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from sklearn.decomposition import TruncatedSVD\n",
+ "svd = TruncatedSVD(n_components=2)\n",
+ "X_svd = svd.fit_transform(X_csr)\n",
+ "\n",
+ "from matplotlib import pyplot as plt\n",
+ "cluster_x, cluster_y = X_svd[:, 0], X_svd[:, 1]\n",
+ "cluster_c = list(cluster_chain[-1].tocsr().indices)\n",
+ "print(cluster_x.shape, cluster_y.shape)\n",
+ "plt.scatter(cluster_x, cluster_y, c=cluster_c, s=25)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3acd550d",
+ "metadata": {},
+ "source": [
+ "### Tracing Cluster in Hierarchical Clustering"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "b6c75c21",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "4 layers in the trained hierarchical clusters with C[d] as:\n",
+ "cluster_chain[0] is a csc matrix of shape (4, 1).\n",
+ "cluster_chain[1] is a csc matrix of shape (32, 4).\n",
+ "cluster_chain[2] is a csc matrix of shape (256, 32).\n",
+ "cluster_chain[3] is a csc matrix of shape (14146, 256).\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.base import HierarchicalKMeans\n",
+ "import scipy.sparse as smat\n",
+ "import numpy as np\n",
+ "\n",
+ "cluster_chain = HierarchicalKMeans.gen(X_csr, nr_splits=8)\n",
+ "\n",
+ "print(f\"{len(cluster_chain)} layers in the trained hierarchical clusters with C[d] as:\")\n",
+ "for d, C in enumerate(cluster_chain):\n",
+ " print(f\"cluster_chain[{d}] is a {C.getformat()} matrix of shape {C.shape}.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "dc644526",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "55 instances belong to the first cluster in the layer-3.\n",
+ "442 instances belong to the first cluster in the layer-2.\n",
+ "3536 instances belong to the first cluster in the layer-1.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from scipy.sparse import linalg\n",
+ "from pecos.core import clib as pecos_clib\n",
+ "\n",
+ "current_cluster = cluster_chain[-1]\n",
+ "for i in range(len(cluster_chain) - 2, -1, -1):\n",
+ " print(f\"{current_cluster.getnnz(0)[0]} instances belong to the first cluster in the layer-{i + 1}.\")\n",
+ " current_cluster = pecos_clib.sparse_matmul(current_cluster, cluster_chain[i])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "f2203831",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The 10-th instance belongs to the cluster-228 in the layer-3.\n",
+ "The 10-th instance belongs to the cluster-28 in the layer-2.\n",
+ "The 10-th instance belongs to the cluster-3 in the layer-1.\n"
+ ]
+ }
+ ],
+ "source": [
+ "inst_idx = 10\n",
+ "\n",
+ "current_cluster = cluster_chain[-1]\n",
+ "for i in range(len(cluster_chain) - 2, -1, -1):\n",
+ " print(f\"The {inst_idx}-th instance belongs to the cluster-{current_cluster.tocsr().indices[inst_idx]} in the layer-{i + 1}.\")\n",
+ " current_cluster = pecos_clib.sparse_matmul(current_cluster, cluster_chain[i])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "080f044a",
+ "metadata": {},
+ "source": [
+ "### Performance Benchmarking\n",
+ "\n",
+ "Here we benchmark the efficiency performance of PECOS hierarchicaly clustering and compare with a pure Python implementation based on [sklearn.cluster.KMeans](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "cbcacb51",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "PECOS takes 1.0543 seconds for hierarchical clustering with a depth 5.\n"
+ ]
+ }
+ ],
+ "source": [
+ "import time\n",
+ "from pecos.xmc.base import HierarchicalKMeans\n",
+ "\n",
+ "nr_splits = 4\n",
+ "\n",
+ "start_time = time.time()\n",
+ "cluster_chain = HierarchicalKMeans.gen(X_csr, nr_splits=nr_splits)\n",
+ "pred_time = time.time() - start_time\n",
+ "\n",
+ "cluster_depth = len(cluster_chain)\n",
+ "print(f\"PECOS takes {pred_time:.4f} seconds for hierarchical clustering with a depth {cluster_depth}.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "5c8179cd",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "scikit-learn takes 49.1826 seconds for hierarchical clustering with a depth 5.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.cluster import KMeans\n",
+ "import numpy as np\n",
+ "\n",
+ "start_time = time.time()\n",
+ "current_clusters = [X_csr]\n",
+ "for d in range(cluster_depth):\n",
+ " next_clusters = []\n",
+ " for cur_X in current_clusters:\n",
+ " if cur_X.shape[0] >= nr_splits:\n",
+ " kmeans = KMeans(n_clusters=nr_splits).fit(cur_X)\n",
+ " next_clusters.append(cur_X[kmeans.labels_ == 0])\n",
+ " next_clusters.append(cur_X[kmeans.labels_ == 1])\n",
+ " else:\n",
+ " next_clusters.append(cur_X)\n",
+ " \n",
+ " current_clusters = next_clusters\n",
+ "pred_time = time.time() - start_time\n",
+ "\n",
+ "print(f\"scikit-learn takes {pred_time:.4f} seconds for hierarchical clustering with a depth {cluster_depth}.\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/kdd22/Session 5 XR-Transformer cookbook and Distributed PECOS.ipynb b/tutorials/kdd22/Session 5 XR-Transformer cookbook and Distributed PECOS.ipynb
new file mode 100644
index 0000000..684272f
--- /dev/null
+++ b/tutorials/kdd22/Session 5 XR-Transformer cookbook and Distributed PECOS.ipynb
@@ -0,0 +1,883 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "3ee4c624-46ff-4a69-b315-097a4a471737",
+ "metadata": {},
+ "source": [
+ "# eXtreme Multi-label Ranking (XMR) with Transformers\n",
+ "In many XMC applications, XR-Transformer is able to yield better performance than XR-Linear due to better extraction of semantic information. However, unlike the linear models, the training hyper-parameters need to be carefully set to achieve the best performance. Naively using the default setting will often lead to sub-optimal results.\n",
+ "\n",
+ "In this section, we will discuss about crucial components in training a good XR-Transformer model.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "235e6dce-80eb-48f7-8604-f1695f04c878",
+ "metadata": {},
+ "source": [
+ "## 1. Overview: Multi-Resolution Fine-tuning\n",
+ "\n",
+ "One important thing to note is that XR-Transformer leverages multi-resolution fine-tuning to allow tuning from easy to hard tasks. The training can be separated into three steps:\n",
+ "\n",
+ "* **Step1**: Label features are computed (usually via PIFA) and is used to build preliminary hierarchical label tree (HLT) via hierarchical k-means.\n",
+ "* **Step2**: Fine-tune the transformer encoder on the chosen levels of the preliminary HLT.\n",
+ "* **Step3**: Concatenate final instance embeddings and sparse features and train the linear rankers on the refined HLT.\n",
+ "\n",
+ " \n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f0a355aa-3f50-4471-9b8e-08538f3bc57e",
+ "metadata": {},
+ "source": [
+ "## 2. Parameter structure\n",
+ "\n",
+ "Although we provide basic functionalities to supply training and prediction parameters in the CLI API `pecos.xmc.xtransformer.train`, `pecos.xmc.xtransformer.predict` and `pecos.xmc.xtransformer.encode`, for advanced users it is recommended to give parameters via JSON format.\n",
+ "\n",
+ "You can generate a `.json` file with all of the parameters that you can edit and fill in via\n",
+ "```bash\n",
+ "python3 -m pecos.xmc.xtransformer.train --generate-params-skeleton &> params.json\n",
+ "```\n",
+ "\n",
+ "After filling in the desired parameters into `params.json`, the training can be done end2end via:\n",
+ "```bash\n",
+ "python3 -m pecos.xmc.xtransformer.train -t ${T_path} -x ${X_path} -y ${Y_path} -m ${model_dir} --params-path params.json\n",
+ "```\n",
+ "\n",
+ "The high-level structure of the training and prediction parameters for XR-Transformer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "24e47f23-8525-4588-ab44-97c8f52f6cec",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training Parameters of XTransformer.\n",
+ "\n",
+ " preliminary_indexer_params (HierarchicalKMeans.TrainParams): params to generate preliminary hierarchial label tree.\n",
+ " ignored if clustering is given\n",
+ " refined_indexer_params (HierarchicalKMeans.TrainParams): params to generate refined hierarchial label tree.\n",
+ " ignored if fix_clustering is True\n",
+ " matcher_params_chain (TransformerMatcher.TrainParams or list): chain of params for TransformerMatchers.\n",
+ " ranker_params (XLinearModel.TrainParams): train params for linear ranker\n",
+ "\n",
+ " do_fine_tune (bool, optional): if False, skip fine-tuning steps and directly use pre-trained transformer models.\n",
+ " Default True\n",
+ " only_encoder (bool, optional): if True, skip linear ranker training. Default False\n",
+ " fix_clustering (bool, optional): if True, use the same hierarchial label tree for fine-tuning and final prediction. Default false.\n",
+ " max_match_clusters (int, optional): max number of clusters on which to fine-tune transformer. Default 32768\n",
+ " \n",
+ "Pred Parameters of XTransformer.\n",
+ "\n",
+ " matcher_params_chain (TransformerMatcher.PredParams or list): chain of params for TransformerMatchers\n",
+ " ranker_params (XLinearModel.PredParams): pred params for linear ranker\n",
+ " \n"
+ ]
+ }
+ ],
+ "source": [
+ "import logging\n",
+ "from pecos.xmc.xtransformer.model import XTransformer\n",
+ "from pecos.utils import logging_util\n",
+ "\n",
+ "LOGGER = logging.getLogger(__name__)\n",
+ "\n",
+ "logging_util.setup_logging_config(level=1)\n",
+ "\n",
+ "print(XTransformer.TrainParams.__doc__)\n",
+ "print(XTransformer.PredParams.__doc__)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "960b84bd-5c52-425c-b31f-93eb7c6b5e49",
+ "metadata": {},
+ "source": [
+ "We provide the fexibility to control almost every aspect of XR-Transformer taining, let's cover the main components."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e101dc60-0fed-47dd-8c2b-de3ecbc7bd97",
+ "metadata": {},
+ "source": [
+ "### 2.1 Specify the Label Hierarchy\n",
+ "\n",
+ "The structure and construction of the preliminary HLT and the refined-HLT are controlled by `preliminary_indexer_params` and `refined_indexer_params`. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "22f9523a-b7e5-49ff-9874-bcb21bb55eee",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training Parameters of Hierarchical K-means.\n",
+ "\n",
+ " nr_splits (int, optional): The out-degree of each internal node of the tree. Ignored if `imbalanced_ratio != 0` because imbalanced clustering supports only 2-means. Default is `16`.\n",
+ " min_codes (int): The number of direct child nodes that the top level of the hierarchy should have.\n",
+ " max_leaf_size (int, optional): The maximum size of each leaf node of the tree. Default is `100`.\n",
+ " imbalanced_ratio (float, optional): Value between `0.0` and `0.5` (inclusive). Indicates how relaxed the balancedness constraint of 2-means can be. Specifically, if an iteration of 2-means is clustering `L` labels, the size of the output 2 clusters will be within approx `imbalanced_ratio * 2 * L` of each other. Default is `0.0`.\n",
+ " imbalanced_depth (int, optional): Maximum depth of imbalanced clustering. After depth `imbalanced_depth` is reached, balanced clustering will be used. Default is `100`.\n",
+ " spherical (bool, optional): True will l2-normalize the centroids of k-means after each iteration. Default is `True`.\n",
+ " seed (int, optional): Random seed. Default is `0`.\n",
+ " kmeans_max_iter (int, optional): Maximum number of iterations for each k-means problem. Default is `20`.\n",
+ " threads (int, optional): Number of threads to use. `-1` denotes all CPUs. Default is `-1`.\n",
+ " \n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.base import HierarchicalKMeans; print(HierarchicalKMeans.TrainParams.__doc__)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18c61c61-20ad-4a4f-93a3-85a736215310",
+ "metadata": {},
+ "source": [
+ "Here is an example of the parameters related to label hierarchy in `eurlex-4k` model:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "2759001f-413f-49be-bfad-38f58b9147d1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{\n",
+ " \"__meta__\": {\n",
+ " \"class_fullname\": \"pecos.xmc.base###HierarchicalKMeans.TrainParams\"\n",
+ " },\n",
+ " \"nr_splits\": 16,\n",
+ " \"min_codes\": 16,\n",
+ " \"max_leaf_size\": 16,\n",
+ " \"imbalanced_ratio\": 0.0,\n",
+ " \"imbalanced_depth\": 100,\n",
+ " \"spherical\": true,\n",
+ " \"seed\": 0,\n",
+ " \"kmeans_max_iter\": 20,\n",
+ " \"threads\": -1\n",
+ "}\n"
+ ]
+ }
+ ],
+ "source": [
+ "import json\n",
+ "import pecos\n",
+ "import requests\n",
+ "import numpy as np\n",
+ "from pecos.utils import smat_util\n",
+ "from pecos.xmc import Indexer, LabelEmbeddingFactory\n",
+ "\n",
+ "param_url = \"https://raw.githubusercontent.com/amzn/pecos/mainline/examples/xr-transformer-neurips21/params/eurlex-4k/bert/params.json\"\n",
+ "params = json.loads(requests.get(param_url).text)\n",
+ " \n",
+ "eurlex4k_train_params = XTransformer.TrainParams.from_dict(params[\"train_params\"])\n",
+ "eurlex4k_pred_params = XTransformer.PredParams.from_dict(params[\"pred_params\"])\n",
+ "\n",
+ "print(json.dumps(eurlex4k_train_params.preliminary_indexer_params.to_dict(), indent=True))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "90802b2f-55ef-4a64-9252-08f7a1e30bcc",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Preliminary HLT structure [16, 256, 3956]\n"
+ ]
+ }
+ ],
+ "source": [
+ "X_feat = smat_util.load_matrix(\"work_dir/xmc-base/eurlex-4k/X.trn.npz\", dtype=np.float32)\n",
+ "Y = smat_util.load_matrix(\"work_dir/xmc-base/eurlex-4k/Y.trn.npz\", dtype=np.float32)\n",
+ "\n",
+ "with open(\"work_dir/xmc-base/eurlex-4k/X.trn.txt\", 'r') as fin:\n",
+ " X_txt = [xx.strip() for xx in fin.readlines()]\n",
+ "\n",
+ "preliminary_hlt = Indexer.gen(\n",
+ " LabelEmbeddingFactory.create(Y, X_feat, method=\"pifa\"),\n",
+ " train_params=eurlex4k_train_params.preliminary_indexer_params,\n",
+ ")\n",
+ "\n",
+ "print(f\"Preliminary HLT structure {[c.shape[0] for c in preliminary_hlt]}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11232266-fceb-464b-8b71-ceaf723d7b39",
+ "metadata": {},
+ "source": [
+ "In this case the preliminiary HLT has 3 levels (16-256-3956) and the refined HLT has 4 levels ( 4-32-256-3956).\n",
+ "As we choose the `max_match_clusters` to be `32768`, the fine-tuning will happen on all 3 levels of preliminary HLT.\n",
+ "\n",
+ "The preliminary HLT is usually constructed such that:\n",
+ "* The initial fine-tuning task has low enough label resolution (i.e. < 1000 labels, in this case 128). This is to ensure Transformers can start from simple task to 'warm-up'.\n",
+ "* The final fine-tuning task has high enough label resolution (controlled by `max_match_clusters`, in this case 32768). The is to ensure training efficiency."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ac812a91-024f-45cc-9d3c-f97b7f98ac94",
+ "metadata": {},
+ "source": [
+ "### 2.2 Control fine-tuning at each level\n",
+ "\n",
+ "At each level of the fine-tuning task, user can independently specify the training parameters such as `loss_function`, `batch_size` and etc.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "be25c125-3dc6-4ec7-b83f-ef289312778f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training Parameters of MLModel\n",
+ "\n",
+ " model_shortcut (str): string of pre-trained model shortcut. Default 'bert-base-cased'\n",
+ " negative_sampling (str): negative sampling types. Default tfn\n",
+ " loss_function (str): type of loss function to use for transformer\n",
+ " training. Default 'squared-hinge'\n",
+ " bootstrap_method (str): algorithm to bootstrap text_model. If not None, initialize\n",
+ " TransformerMatcher projection layer with one of:\n",
+ " 'linear' (default): linear model trained on final embeddings of parent layer\n",
+ " 'inherit': inherit weights from parent labels\n",
+ " lr_schedule (str): learning rate schedule. See transformers.SchedulerType for details.\n",
+ " Default 'linear'\n",
+ "\n",
+ " threshold (float): threshold to sparsify the model weights. Default 0.1\n",
+ " hidden_dropout_prob (float): hidden dropout prob in deep transformer models. Default 0.1\n",
+ " batch_size (int): batch size for transformer training. Default 8\n",
+ " batch_gen_workers (int): number of workers for batch generation. Default 4\n",
+ " max_active_matching_labels (int): max number of active matching labels,\n",
+ " will sub-sample from existing negative samples if necessary. Default None\n",
+ " to ignore\n",
+ " max_num_labels_in_gpu (int): Upper limit on labels to put output layer in GPU.\n",
+ " Default 65536.\n",
+ " max_steps (int): if > 0: set total number of training steps to perform.\n",
+ " Override num-train-epochs. Default -1.\n",
+ " max_no_improve_cnt (int): if > 0, training will stop when this number of\n",
+ " validation steps result in no improvement. Default -1.\n",
+ " num_train_epochs (int): total number of training epochs to perform. Default 5\n",
+ " gradient_accumulation_steps (int): number of updates steps to accumulate\n",
+ " before performing a backward/update pass. Default 1.\n",
+ " weight_decay (float): weight decay rate for regularization. Default 0 to ignore\n",
+ " max_grad_norm (float): max gradient norm used for gradient clipping. Default 1.0\n",
+ " learning_rate (float): maximum learning rate for Adam. Default 5e-5\n",
+ " adam_epsilon (float): epsilon for Adam optimizer.Default 1e-8\n",
+ " warmup_steps (float): learning rate warmup over warmup-steps. Default 0\n",
+ " logging_steps (int): log training information every NUM updates steps. Default 50\n",
+ " save_steps (int): save checkpoint every NUM updates steps. Default 100\n",
+ "\n",
+ " cost_sensitive_ranker (bool, optional): if True, use clustering count aggregating for ranker's cost-sensitive learnin\n",
+ " Default False\n",
+ " pre_tokenize (bool, optional): if True, will tokenize training instances before training\n",
+ " This could potentially accelerate batch-generation but increases memory cost.\n",
+ " Default False\n",
+ " use_gpu (bool, optional): whether to use GPU even if available. Default True\n",
+ " eval_by_true_shorlist (bool, optional): if True, will compute validation scores by true label\n",
+ " shortlisting at intermediat layer. Default False\n",
+ "\n",
+ " checkpoint_dir (str): path to save training checkpoints. Default empty to use a temp dir.\n",
+ " cache_dir (str): dir to store the pre-trained models downloaded from\n",
+ " s3. Default empty to use a temp dir.\n",
+ " init_model_dir (str): path to load checkpoint of TransformerMatcher. If given,\n",
+ " start from the given checkpoint rather than downloading a\n",
+ " pre-trained model from S3. Default empty to ignore\n",
+ " \n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.xtransformer.matcher import TransformerMatcher; print(TransformerMatcher.TrainParams.__doc__)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "38cc7480-d6a7-4c81-96cf-cb35aa823fc2",
+ "metadata": {},
+ "source": [
+ "For the `eurlex-4k` model, we are fine-tuning the `bert-base-uncased` pre-trained model at 3 levels of the preliminary HLT:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "db634cb8-6763-411a-8f6e-6100ff1d4ead",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "========== matcher_params_chain[0] (len=3) ==========\n",
+ "{\n",
+ " \"__meta__\": {\n",
+ " \"class_fullname\": \"pecos.xmc.xtransformer.matcher###TransformerMatcher.TrainParams\"\n",
+ " },\n",
+ " \"adam_epsilon\": 1e-08,\n",
+ " \"batch_gen_workers\": 16,\n",
+ " \"batch_size\": 32,\n",
+ " \"bootstrap_method\": \"weighted-linear\",\n",
+ " \"cache_dir\": \"\",\n",
+ " \"checkpoint_dir\": \"\",\n",
+ " \"cost_sensitive_ranker\": false,\n",
+ " \"eval_by_true_shorlist\": false,\n",
+ " \"gradient_accumulation_steps\": 1,\n",
+ " \"hidden_dropout_prob\": 0.1,\n",
+ " \"init_model_dir\": \"\",\n",
+ " \"learning_rate\": 5e-05,\n",
+ " \"logging_steps\": 50,\n",
+ " \"loss_function\": \"weighted-squared-hinge\",\n",
+ " \"lr_schedule\": \"linear\",\n",
+ " \"max_active_matching_labels\": 1000,\n",
+ " \"max_grad_norm\": 1.0,\n",
+ " \"max_no_improve_cnt\": -1,\n",
+ " \"max_num_labels_in_gpu\": 65536,\n",
+ " \"max_steps\": 600,\n",
+ " \"model_shortcut\": \"bert-base-uncased\",\n",
+ " \"negative_sampling\": \"tfn+man\",\n",
+ " \"num_train_epochs\": 10,\n",
+ " \"pre_tensorize_labels\": false,\n",
+ " \"pre_tokenize\": false,\n",
+ " \"save_steps\": 200,\n",
+ " \"threshold\": 0.001,\n",
+ " \"use_gpu\": true,\n",
+ " \"warmup_steps\": 100,\n",
+ " \"weight_decay\": 0.0\n",
+ "}\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"=\"*10, f\"matcher_params_chain[0] (len={len(eurlex4k_train_params.matcher_params_chain)})\", \"=\"*10)\n",
+ "print(json.dumps(eurlex4k_train_params.matcher_params_chain[0].to_dict(), sort_keys=True, indent=True))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "888b1b30-a300-42e2-b8e0-295c426fe655",
+ "metadata": {},
+ "source": [
+ "Though the best parameters may vary a lot for different tasks, there are some common notes you should alwasy\n",
+ "* It's recommended to finish at least one epoch at each level. This will allow the model to visit the label matrix at least once.\n",
+ " * i.e. `max_steps * batch_size * num_gpus > num_instances` (if `max_steps` is null, it will be infered from `num_train_epochs`)\n",
+ "* `model_shortcut` will only be used in the first fine-tuning layer, as the later ones will just continue on the same encoder.\n",
+ "* Learning rate and its schedule is controlled by `learning_rate`, `lr_schedule`, `warmup_steps`, `max_steps`. For more info, refer to: https://huggingface.co/docs/transformers/main_classes/optimizer_schedules"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a7f5f39-9d20-4776-970c-2d46464ea983",
+ "metadata": {},
+ "source": [
+ "#### 2.2.1 Use pre-trained models\n",
+ "\n",
+ "There are two ways to provide pre-trained Transformer encoder:\n",
+ "* **Download from huggingface repo** (https://huggingface.co/models): model name provided by `model_shortcut`. (e.x. `bert-base-uncased` or `w11wo/javanese-distilbert-small`)\n",
+ "* **Load from local disk**: model path provided by `init_model_dir`. Model should be loadable through `TransformerMatcher.load()`\n",
+ "\n",
+ "Note that both `model_shortcut` and `init_model_dir` will only be used in the first fine-tuning layer, as the later ones will just continue on the final state from parent encoder.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "9570261c-10b4-4c74-b018-0bd3c8b524d7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " model loaded with encoder_type=distilbert num_labels=2\n"
+ ]
+ }
+ ],
+ "source": [
+ "import os\n",
+ "import scipy.sparse as smat\n",
+ "from pecos.xmc.xtransformer.matcher import TransformerMatcher\n",
+ "from transformers import AutoTokenizer, AutoModelForSequenceClassification\n",
+ "\n",
+ "init_model_dir = \"work_dir/my_pre_trained_model\"\n",
+ "os.makedirs(init_model_dir, exist_ok=True)\n",
+ "\n",
+ "# example to use your own pre-trained model, here we use huggingface model as an example\n",
+ "my_tokenizer = AutoTokenizer.from_pretrained(\"distilbert-base-uncased\")\n",
+ "my_encoder = AutoModelForSequenceClassification.from_pretrained(\"distilbert-base-uncased\")\n",
+ "\n",
+ "# do my own pre-training/tuning/etc\n",
+ "# ...\n",
+ "\n",
+ "# save my own model to disk\n",
+ "my_tokenizer.save_pretrained(f\"{init_model_dir}/text_tokenizer\")\n",
+ "my_encoder.save_pretrained(f\"{init_model_dir}/text_encoder\")\n",
+ "\n",
+ "# then the `work_dir` can be fed as `init_model_dir` as initial model.\n",
+ "# Sanity check: if this dir can be loaded via TransformerMatcher.load(*)\n",
+ "matcher = TransformerMatcher.load(init_model_dir)\n",
+ "print(f\"{matcher.__class__} model loaded with encoder_type={matcher.model_type} num_labels={matcher.nr_labels}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "7561e709-f14c-47c4-b14b-d80c62d156bf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "DATASET=\"eurlex-4k\"\n",
+ "wget -q https://archive.org/download/xr-transformer-encoders/${DATASET}.tar.gz\n",
+ "mkdir -p ./work_dir/xr-transformer-encoder\n",
+ "tar -zxf ./${DATASET}.tar.gz -C ./work_dir/xr-transformer-encoder"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "9e04d9f9-0e1c-4000-a947-a417d04e84b7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " model loaded with encoder_type=bert num_labels=3956\n"
+ ]
+ }
+ ],
+ "source": [
+ "matcher = TransformerMatcher.load(\"./work_dir/xr-transformer-encoder/eurlex-4k/bert/text_encoder\")\n",
+ "print(f\"{matcher.__class__} model loaded with encoder_type={matcher.model_type} num_labels={matcher.nr_labels}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f374aaf-c2cd-4daf-937b-f5974b0d4a75",
+ "metadata": {},
+ "source": [
+ "#### 2.2.2 Bootstrapping and Cost Sensitive Leanring\n",
+ "\n",
+ "We provide three options to boostrap the XMC head at child level (i.e. W^(t+1)) from parent level (i.e. W^(t)):\n",
+ "* `bootstrap_method=None`: No bootstrap, W^(t+1) will be randomly initialized.\n",
+ "* `bootstrap_method='inherit'`: Bootstrap by inherit the weight vector from parent node. \n",
+ "* `bootstrap_method='linear'`(default): linear model will be trained on final embeddings of parent layer and be used as initial point for W^(t+1).\n",
+ "\n",
+ "In most cases the default linear bootstrapper would give good enough initial point the XMC heads.\n",
+ "Compared with linear bootstrapper, the inherit bootstrapper has less memory/time overhead. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2be8776a-6e46-488c-97cd-d72a5842078b",
+ "metadata": {},
+ "source": [
+ "XR-Transformer allows taking magnutute of label strength into consideration via cost-sensitive learning.\n",
+ "This is available even when input label matrix is binary. In this case, the cost (at non-leaf level) will be inferred via label aggregation.\n",
+ "\n",
+ "To use cost-sensitive fine-tuning, use the `weighted-` version of loss functions. I.e."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "6217ae9b-eb10-4dd7-a41d-3b36024ea915",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "['weighted-hinge', 'weighted-squared-hinge']\n"
+ ]
+ }
+ ],
+ "source": [
+ "print([lf for lf in TransformerMatcher.LOSS_FUNCTION_TYPES.keys() if 'weighted-' in lf])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5a01e03b-427d-47c9-ba5d-8ae45e647996",
+ "metadata": {},
+ "source": [
+ "### 2.3 Linear models with concatenated feature\n",
+ "\n",
+ "The training of linear models is controlled by the `ranker_params`, which is of the same format as PECOS XR-Linear.\n",
+ "\n",
+ "User should pay special attention to the `threshold` which controls the sparsification of final linear models.\n",
+ "Unlike purely sparse features, the linear models trained on sparse+dense concatenated features are more sensitive to the sparsification.\n",
+ "Usually `threshold=0.01` or `0.001` is recommended for XR-Transformer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "f8531b6d-f96f-476e-836e-01a2f6daa35f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "prec = 84.97 78.05 71.25 64.93 58.97 53.42 48.24 43.70 39.92 36.81\n",
+ "recall = 17.26 31.35 42.42 51.00 57.39 62.01 65.08 67.23 68.95 70.56\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pecos.xmc.xtransformer.module import MLProblemWithText\n",
+ "prob = MLProblemWithText(X_txt, Y, X_feat=X_feat)\n",
+ "\n",
+ "# disable fine-tuning, use pre-trained bert model from huggingface\n",
+ "eurlex4k_train_params.do_fine_tune = False\n",
+ "\n",
+ "xtf_pretrained = XTransformer.train(\n",
+ " prob,\n",
+ " clustering=preliminary_hlt,\n",
+ " train_params=eurlex4k_train_params,\n",
+ " pred_params=eurlex4k_pred_params,\n",
+ ")\n",
+ "\n",
+ "X_feat_tst = smat_util.load_matrix(\"work_dir/xmc-base/eurlex-4k/X.tst.npz\", dtype=np.float32)\n",
+ "Y_tst = smat_util.load_matrix(\"work_dir/xmc-base/eurlex-4k/Y.tst.npz\", dtype=np.float32)\n",
+ "\n",
+ "with open(\"work_dir/xmc-base/eurlex-4k/X.tst.txt\", 'r') as fin:\n",
+ " X_txt_tst = [xx.strip() for xx in fin.readlines()]\n",
+ "\n",
+ "P_pretrained = xtf_pretrained.predict(X_txt_tst, X_feat=X_feat_tst)\n",
+ "metrics = smat_util.Metrics.generate(Y_tst, P_pretrained, topk=10)\n",
+ "print(metrics)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "34ecca84-75a5-4d38-b1ad-3256761fb695",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "prec = 87.17 80.98 74.50 68.13 61.61 55.58 50.18 45.46 41.51 38.12\n",
+ "recall = 17.72 32.60 44.41 53.52 59.92 64.44 67.58 69.79 71.55 72.89\n"
+ ]
+ }
+ ],
+ "source": [
+ "# use fine-tuned bert model\n",
+ "eurlex4k_train_params.matcher_params_chain[0].init_model_dir = \"./work_dir/xr-transformer-encoder/eurlex-4k/bert/text_encoder\"\n",
+ "\n",
+ "xtf_fine_tuned = XTransformer.train(\n",
+ " prob,\n",
+ " clustering=preliminary_hlt,\n",
+ " train_params=eurlex4k_train_params,\n",
+ " pred_params=eurlex4k_pred_params,\n",
+ ")\n",
+ "\n",
+ "P_fine_tuned = xtf_fine_tuned.predict(X_txt_tst, X_feat=X_feat_tst)\n",
+ "metrics = smat_util.Metrics.generate(Y_tst, P_fine_tuned, topk=10)\n",
+ "print(metrics)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "175c3e79-97bb-4cfa-968b-5a018c5ef2f9",
+ "metadata": {},
+ "source": [
+ "# (BETA) Distributed PECOS\n",
+ "\n",
+ "`pecos.distributed` is a PECOS module that enables distributed training.\n",
+ "\n",
+ "Currently the following sub-modules are implemented:\n",
+ "\n",
+ "* Distributed X-Linear ([`pecos.distributed.xmc.xlinear`](xmc/xlinear/README.md))\n",
+ "\n",
+ "We are working to implement more distributed algorithms for PECOS existing models, please watch out for our newest releases."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "de65b077-2339-48b6-92e9-554d676a2b6b",
+ "metadata": {},
+ "source": [
+ "## 1. Distributed XR-Linear\n",
+ "\n",
+ "`pecos.distributed.xmc.xlinear` enables distributed training for PECOS XLinear model (`pecos.xmc.xlinear`).\n",
+ "\n",
+ "### Prerequisites\n",
+ "\n",
+ "* **Hardware**: \n",
+ " * Cluster of machines connected by network which can password-less SSH to each other.\n",
+ " * IP address of every machine in the cluster is known.\n",
+ " * Shared network disk mounted on all machines.\n",
+ " * For accessing data and saving trained models.\n",
+ "\n",
+ "* **Software**: Install the following software on **every** machine of your cluster\n",
+ " * MPI and mpi4py\n",
+ " \n",
+ "Due to the hardware constraint during the tutorial, we only include a basic example in local mode here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "2834997c-4bd9-4732-acb6-f7e4e7f6a090",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Using built-in specs.\n",
+ "COLLECT_GCC=/usr/bin/gcc\n",
+ "COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/7/lto-wrapper\n",
+ "Target: x86_64-redhat-linux\n",
+ "Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --enable-libmpx --enable-libsanitizer --enable-gnu-indirect-function --enable-libcilkrts --enable-libatomic --enable-libquadmath --enable-libitm --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux\n",
+ "Thread model: posix\n",
+ "gcc version 7.3.1 20180712 (Red Hat 7.3.1-15) (GCC) \n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "/opt/amazon/openmpi/bin/mpiexec\n",
+ "Hello, World! I am process 3 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 0 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 1 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 2 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 4 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 5 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 6 of 8 on ip-172-31-8-94.ec2.internal.\n",
+ "Hello, World! I am process 7 of 8 on ip-172-31-8-94.ec2.internal.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%bash\n",
+ "# check the required dependencies\n",
+ "mpicc -v\n",
+ "which mpiexec\n",
+ "python3 -m pip install mpi4py\n",
+ "mpiexec -n 8 python3 -m mpi4py.bench helloworld"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b04cd3aa-3d7a-4e11-8b15-5074d24525ec",
+ "metadata": {},
+ "source": [
+ "### Basic Usage\n",
+ "\n",
+ "Below is a simple showcase of the usage of `pecos.distributed.xmc.xlinear.train`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "f3b7e689-480d-4c81-a408-d4b840695880",
+ "metadata": {
+ "collapsed": true,
+ "jupyter": {
+ "outputs_hidden": true
+ },
+ "tags": []
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "07/07/2022 22:33:34 - INFO - pecos.utils.profile_util - psutil module installed, will print memory info.\n",
+ "07/07/2022 22:33:34 - INFO - __main__ - Started loading data on Rank 1 ... RSS 91.0 MB. Full mem info: pmem(rss=95412224, vms=35591151616, shared=47382528, text=2732032, lib=0, data=166395904, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - pecos.utils.profile_util - psutil module installed, will print memory info.\n",
+ "07/07/2022 22:33:34 - INFO - __main__ - Started loading data on Rank 0 ... RSS 91.2 MB. Full mem info: pmem(rss=95682560, vms=35591151616, shared=47669248, text=2732032, lib=0, data=166395904, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - __main__ - Done loading data on Rank 0. RSS 126.0 MB. Full mem info: pmem(rss=132136960, vms=35626360832, shared=47800320, text=2732032, lib=0, data=201605120, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - pecos.distributed.xmc.base - Starts creating label embedding PIFA for meta tree on Rank 0 node... RSS 126.0 MB. Full mem info: pmem(rss=132136960, vms=35626360832, shared=47800320, text=2732032, lib=0, data=201605120, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - __main__ - Done loading data on Rank 1. RSS 125.8 MB. Full mem info: pmem(rss=131928064, vms=35626360832, shared=47579136, text=2732032, lib=0, data=201605120, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - pecos.distributed.xmc.base - Done creating label embedding PIFA for meta tree on Rank 0 node. RSS 198.7 MB. Full mem info: pmem(rss=208375808, vms=35776598016, shared=48451584, text=2732032, lib=0, data=285478912, dirty=0)\n",
+ "07/07/2022 22:33:34 - INFO - pecos.distributed.xmc.base - Starts generating meta tree cluster on main node...\n",
+ "07/07/2022 22:33:34 - INFO - pecos.distributed.xmc.base - Determined meta-tree leaf clusters number: 4. 2 nodes will train 4 sub-trees. Number of data labels: 3956, nr_splits: 16\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done generating meta tree cluster. RSS 225.4 MB. Full mem info: pmem(rss=236306432, vms=35804254208, shared=48713728, text=2732032, lib=0, data=313135104, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Rank 0 get 2 sub-tree assignments.\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Rank 1 get 2 sub-tree assignments.\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - On rank 0, 0th sub-tree assignment has 989 labels: [0, 1, 2, 4, 5, 6, 7, 8, 9, 11]...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - On rank 1, 0th sub-tree assignment has 989 labels: [18, 22, 31, 32, 35, 37, 38, 39, 57, 62]...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts creating label embedding PIFA for 0th sub-tree on rank 0... RSS 155.5 MB. Full mem info: pmem(rss=163016704, vms=35730997248, shared=48934912, text=2732032, lib=0, data=239878144, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts creating label embedding PIFA for 0th sub-tree on rank 1... RSS 125.8 MB. Full mem info: pmem(rss=131928064, vms=35626622976, shared=47579136, text=2732032, lib=0, data=201867264, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done creating label embedding PIFA for 0th sub-tree on rank 0. RSS 160.6 MB. Full mem info: pmem(rss=168361984, vms=35734740992, shared=49242112, text=2732032, lib=0, data=245108736, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts generating 0th sub-tree cluster on rank 0...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done creating label embedding PIFA for 0th sub-tree on rank 1. RSS 148.8 MB. Full mem info: pmem(rss=156049408, vms=35724161024, shared=48783360, text=2732032, lib=0, data=233041920, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts generating 0th sub-tree cluster on rank 1...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done generating 0th sub-tree cluster on rank 0. RSS 160.8 MB. Full mem info: pmem(rss=168628224, vms=35734740992, shared=49242112, text=2732032, lib=0, data=245108736, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - On rank 0, 1th sub-tree assignment has 989 labels: [3, 10, 12, 13, 17, 21, 26, 33, 34, 36]...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts creating label embedding PIFA for 1th sub-tree on rank 0... RSS 160.8 MB. Full mem info: pmem(rss=168628224, vms=35734740992, shared=49242112, text=2732032, lib=0, data=245108736, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done creating label embedding PIFA for 1th sub-tree on rank 0. RSS 179.4 MB. Full mem info: pmem(rss=188153856, vms=35754147840, shared=49242112, text=2732032, lib=0, data=264515584, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts generating 1th sub-tree cluster on rank 0...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done generating 0th sub-tree cluster on rank 1. RSS 159.7 MB. Full mem info: pmem(rss=167444480, vms=35735547904, shared=48979968, text=2732032, lib=0, data=244428800, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - On rank 1, 1th sub-tree assignment has 989 labels: [14, 20, 30, 46, 50, 54, 60, 78, 85, 100]...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts creating label embedding PIFA for 1th sub-tree on rank 1... RSS 159.7 MB. Full mem info: pmem(rss=167444480, vms=35735547904, shared=48979968, text=2732032, lib=0, data=244428800, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done generating 1th sub-tree cluster on rank 0. RSS 179.4 MB. Full mem info: pmem(rss=188153856, vms=35754147840, shared=49242112, text=2732032, lib=0, data=264515584, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done creating label embedding PIFA for 1th sub-tree on rank 1. RSS 176.8 MB. Full mem info: pmem(rss=185393152, vms=35751538688, shared=49176576, text=2732032, lib=0, data=261906432, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts generating 1th sub-tree cluster on rank 1...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done generating 1th sub-tree cluster on rank 1. RSS 176.8 MB. Full mem info: pmem(rss=185393152, vms=35751538688, shared=49176576, text=2732032, lib=0, data=261906432, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Starts assmebling cluster chain... RSS 129.2 MB. Full mem info: pmem(rss=135426048, vms=35701280768, shared=49242112, text=2732032, lib=0, data=211648512, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done assmebling cluster chain. Split depth: 1. Chain length: 3 RSS 129.2 MB. Full mem info: pmem(rss=135426048, vms=35701280768, shared=49242112, text=2732032, lib=0, data=211648512, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Broadcasting distributed cluster chain from Node 0...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - Done broadcast distributed cluster chain from Node 0.\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - meta, sub negative samples: 32 61\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Starts receiving sub-training jobs from source 0 for rank 1...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - meta_tree_leaf_cluster: (3956, 64)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Main node workload: 69941.31147540984\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Min worker node workload, machine rank: (69387, 0). Max worker node workload, machine rank: (69387, 0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Training jobs for all Sub-trees divided onto 2 machines: Main node will train for 13 sub-trees, Worker nodes will train for [51] sub-trees, worker receive order: [1].\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Starts sending sub-training jobs from node 0 to 1...\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Done sending sub-training jobs from node 0 to 1.\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Rank 0 starts meta-tree training... RSS 130.0 MB. Full mem info: pmem(rss=136282112, vms=35702648832, shared=49307648, text=2732032, lib=0, data=213016576, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Done receiving sub-training jobs from source 0 for rank 1.\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Rank 1 get 51 sub-trees to train\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.xlinear.model - Rank 1 starts sub-tree training... RSS 129.0 MB. Full mem info: pmem(rss=135274496, vms=35701280768, shared=49176576, text=2732032, lib=0, data=211648512, dirty=0)\n",
+ "07/07/2022 22:33:35 - INFO - pecos.distributed.xmc.base - meta_tree_leaf_cluster: (3956, 64)\n",
+ "07/07/2022 22:33:39 - INFO - pecos.distributed.xmc.xlinear.model - Rank 0 done meta-tree training. RSS 163.5 MB. Full mem info: pmem(rss=171479040, vms=35735203840, shared=49307648, text=2732032, lib=0, data=250142720, dirty=0)\n",
+ "07/07/2022 22:33:39 - INFO - pecos.distributed.xmc.xlinear.model - Rank 0 get 13 sub-trees to train\n",
+ "07/07/2022 22:33:39 - INFO - pecos.distributed.xmc.xlinear.model - Rank 0 starts sub-tree training... RSS 163.5 MB. Full mem info: pmem(rss=171479040, vms=35735203840, shared=49307648, text=2732032, lib=0, data=250142720, dirty=0)\n",
+ "07/07/2022 22:33:42 - INFO - pecos.distributed.xmc.xlinear.model - Rank 0 total 13 sub-tree training finished. RSS 163.5 MB. Full mem info: pmem(rss=171479040, vms=35735203840, shared=49307648, text=2732032, lib=0, data=250142720, dirty=0)\n",
+ "07/07/2022 22:33:42 - INFO - pecos.distributed.xmc.xlinear.model - Main node start recv 51 sub-tree models from rank 1\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Rank 1 total 51 sub-tree training finished. RSS 148.8 MB. Full mem info: pmem(rss=155975680, vms=35721224192, shared=49369088, text=2732032, lib=0, data=232181760, dirty=0)\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Rank 1 node starts sending 51 sub-tree models.\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Main node done receive 51 sub-tree models from rank 1\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Rank 1 node done sending 51 sub-tree models.\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Reconstruct full model on Rank 0 node... RSS 163.9 MB. Full mem info: pmem(rss=171864064, vms=35735465984, shared=49446912, text=2732032, lib=0, data=250404864, dirty=0)\n",
+ "07/07/2022 22:33:48 - INFO - pecos.distributed.xmc.xlinear.model - Done reconstruct full model on Rank 0 node. RSS 164.7 MB. Full mem info: pmem(rss=172675072, vms=35735990272, shared=49446912, text=2732032, lib=0, data=250929152, dirty=0)\n",
+ "07/07/2022 22:33:48 - INFO - __main__ - Saving model to work_dir/dist_xlinear_model...\n",
+ "07/07/2022 22:33:49 - INFO - __main__ - Done saving model.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%bash\n",
+ "mpiexec -n 2 \\\n",
+ "python3 -m pecos.distributed.xmc.xlinear.train \\\n",
+ "-x work_dir/xmc-base/eurlex-4k/X.trn.npz \\\n",
+ "-y work_dir/xmc-base/eurlex-4k/Y.trn.npz \\\n",
+ "-m work_dir/dist_xlinear_model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "afb08655-7de1-426c-b203-0c012ac50c7a",
+ "metadata": {},
+ "source": [
+ "We didn't setup the multi-node cluster therefore only single machine is used here. In practice, you can store your machines' addresses in `hostfile` and run the distributed training via \n",
+ "```\n",
+ "mpiexec -f hostfile -n ${NUM_MACHINE} python3 -m pecos.distributed.xmc.xlinear.train [..]\n",
+ "```\n",
+ "\n",
+ "The distributed trained model is serialized in the same way as the single node trained model. We can use the same way to predict and evaluate the model:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "b39cbf96-6cf2-4721-86d7-90cc27bdbe1f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "==== evaluation results ====\n",
+ "prec = 82.25 74.92 68.58 62.92 57.58 52.55 47.68 43.56 40.08 37.08\n",
+ "recall = 16.62 29.97 40.75 49.33 55.96 60.91 64.26 66.97 69.14 70.97\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%bash\n",
+ "python3 -m pecos.xmc.xlinear.predict \\\n",
+ "-x work_dir/xmc-base/eurlex-4k/X.tst.npz \\\n",
+ "-y work_dir/xmc-base/eurlex-4k/Y.tst.npz \\\n",
+ "-m work_dir/dist_xlinear_model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ed14fb96-7ab3-4e1c-8b23-f1ef76d83a0d",
+ "metadata": {},
+ "source": [
+ "## Distributed Training Algorithm\n",
+ "\n",
+ "Because of the model separability of PECOS XR-Linear model, we can split the original problem into multiple independent problems:\n",
+ "* **One meta-problem**: XMC problem to match an input X to K clusters\n",
+ "* **K sub-problems**: XMC problem to rank the labels in one of the K clusters for an input X.\n",
+ "\n",
+ "\n",
+ "\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1522bf81-3d13-4821-815e-ddb24cb412d0",
+ "metadata": {},
+ "source": [
+ "In addition to distributed training, `pecos.distributed.xmc.xlinear` also has the following features:\n",
+ "\n",
+ "* **Distributed Hierarchical Clustering**: We leverage the same meta-sub problem split to build the Hierarchical label tree. Since that building label feature for a huge dataset could be memory intensive for meta node, we provide option to use simpler label embedding for meta-tree generation:`--meta-label-embedding-method pii`\n",
+ "* **Load Balancing**: Beacuse of the long tail distribution in most XMC problems, workload to train each sub-problem varies a lot. To address that, the distributed training algorithm does load balancing when K > #workers. The sub-tree number K can be controlled via `--min-n-sub-tree`.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/kdd22/imgs/dist-xlinear.png b/tutorials/kdd22/imgs/dist-xlinear.png
new file mode 100644
index 0000000..c7f2e95
Binary files /dev/null and b/tutorials/kdd22/imgs/dist-xlinear.png differ
diff --git a/tutorials/kdd22/imgs/hnsw_example.png b/tutorials/kdd22/imgs/hnsw_example.png
new file mode 100644
index 0000000..93f59b3
Binary files /dev/null and b/tutorials/kdd22/imgs/hnsw_example.png differ
diff --git a/tutorials/kdd22/imgs/illus_customized_model.jpg b/tutorials/kdd22/imgs/illus_customized_model.jpg
new file mode 100644
index 0000000..a292f01
Binary files /dev/null and b/tutorials/kdd22/imgs/illus_customized_model.jpg differ
diff --git a/tutorials/kdd22/imgs/pecos_beam_search.png b/tutorials/kdd22/imgs/pecos_beam_search.png
new file mode 100644
index 0000000..f13ac0b
Binary files /dev/null and b/tutorials/kdd22/imgs/pecos_beam_search.png differ
diff --git a/tutorials/kdd22/imgs/pecos_matcher_ranker.png b/tutorials/kdd22/imgs/pecos_matcher_ranker.png
new file mode 100644
index 0000000..8302b8b
Binary files /dev/null and b/tutorials/kdd22/imgs/pecos_matcher_ranker.png differ
diff --git a/tutorials/kdd22/imgs/pecos_spmm.png b/tutorials/kdd22/imgs/pecos_spmm.png
new file mode 100644
index 0000000..6f7278b
Binary files /dev/null and b/tutorials/kdd22/imgs/pecos_spmm.png differ
diff --git a/tutorials/kdd22/imgs/pecos_xmr_framework.png b/tutorials/kdd22/imgs/pecos_xmr_framework.png
new file mode 100644
index 0000000..dcef9d1
Binary files /dev/null and b/tutorials/kdd22/imgs/pecos_xmr_framework.png differ