{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"name":"2022-01-26-lambda-learner.ipynb","provenance":[{"file_id":"https://github.com/recohut/nbs/blob/main/raw/T392297%20%7C%20Lambda%20Learner%20for%20Incremental%20Learning%20using%20GLMix%20models.ipynb","timestamp":1644674508862}],"collapsed_sections":[],"toc_visible":true,"authorship_tag":"ABX9TyPAiA9DpL8rLPHedxhv5EMA"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"markdown","metadata":{"id":"rz1oxa1MDU3j"},"source":["# Lambda Learner for Incremental Learning using GLMix models"]},{"cell_type":"markdown","metadata":{"id":"e0Ul2LEnDj_F"},"source":["***A framework for incremental learning for personalization at scale.***\n","\n","Lambda Learner is a framework to incrementally train the memorization part of the model as a booster over the generalization part. We incrementally update the memorization model between full batch offline updates of the generalization model to balance training performance against model stability.\n","\n","The key concept is to prepare mini-batches of data from an incoming stream of logged data to train an incremental update to the memorization model. We approximate the loss function on previously observed data by a local quadratic approximation around the previous optimal value and combine it with the loss function on the most recent mini-batch. This allows us to incrementally update the model without relying on all historical data yet do better than just relying on the new incoming data. This results in a regularized learning problem with a weighted penalty. In the Bayesian interpretation, the local quadratic approximation is equivalent to using the posterior distribution of the previous model as prior for training the update.\n","\n","---\n","\n","Lambda Learner is a library for iterative incremental training of a class of supervised machine learning models. Using the Generalized Additive Mixed-Effect (GAME) framework, one can divide a model into two components, (a) Fixed Effects - a typically large \"fixed effects\" model (generalization) that is trained on the whole dataset to improve the model’s performance on previously unseen user-item pairs, and (b) Random Effects - a series of simpler linear \"random-effects\" models (memorization) trained on data corresponding to each entity (e.g. user or article or ad) for more granular personalization.\n","\n","The two main choices in defining a GAME architecture are 1) choosing the model class for the fixed effects model, and 2) choosing which random effects to include. The fixed effects model can be of any model class, typically Tensorflow, DeText, GDMix, XGBoost. As for the random effects, this choice is framed by your training data; specifically by the keys/ids of your training examples. If your training examples are keyed by a single id space (say userId), then you will have one series of random effects keyed by userId (per-user random effects). If your data is keyed by multiple id spaces (say userId, movieId), then you can have up to one series of random effects for every id type (per-user random effects, and per-movie random effects). However it's not necessary to have random effects for all ids, with the choice being largely a modeling concern.\n","\n","Lambda Learner currently supports using any fixed-effects model, but only random effects for a single id type.\n","\n","Bringing these two pieces together, the residual score from the fixed effects model is improved using a random effect linear model, with the global model's output score acting as the bias/offset for the linear model. Once the fixed effects model has been trained, the training of random effects can occur independently and in parallel. The library supports incremental updates to the random effects components of a GAME model in response to mini-batches from data streams. Currently the following algorithms for updating a random effect are supported:\n","\n","- Linear regression.\n","- Logistic regression.\n","- Sequential Bayesian logistic regression (as described in the [Lambda Learner paper](https://arxiv.org/abs/2010.05154)).\n","\n","The library supports maintaining a model coefficient Hessian matrix, representing uncertainty about model coefficient values, in addition to point estimates of the coefficients. This allows us to use the random effects as a multi-armed bandit using techniques such as Thompson Sampling.\n","\n","One of the most well-established applications of machine learning is in deciding what content to show website visitors. When observation data comes from high-velocity, user-generated data streams, machine learning methods perform a balancing act between model complexity, training time, and computational costs. Furthermore, when model freshness is critical, the training of models becomes time-constrained. Parallelized batch offline training, although horizontally scalable, is often not time-considerate or cost effective.\n","\n","Lambda Learner is capable of incrementally training the memorization part of the model (the random-effects components) as a performance booster over the generalization part. The frequent updates to these booster models over already powerful fixed-effect models improve personalization. Additionally, it allows for applications that require online bandits that are updated quickly.\n","\n","[Lambda Learner: Nearline learning on data streams](https://engineering.linkedin.com/blog/2021/lambda-learner--nearline-learning-on-data-streams)\n","\n","In the GAME paradigm, random effects components can be trained independently of each other. This means that their update can be easily parallelized across nodes in a distributed computation framework. For example, this library can be used on top of Python Beam or PySpark. The distributed compute framework is used for parallelization and data orchestration, while the Lambda Learner library implements the update of random effects in individual compute tasks (DoFns in Beam or Task closures in PySpark)."]},{"cell_type":"markdown","metadata":{"id":"ETAOarPKaXoX"},"source":["![](https://github.com/recohut/incremental-learning/raw/a6fdcde2e8af7ebfd9f5efd278c487e0e9560cb3/docs/_images/T392297_1.png)"]},{"cell_type":"markdown","metadata":{"id":"V5H89ByI8yzW"},"source":["## Setup"]},{"cell_type":"code","metadata":{"id":"x6DfWPUE7C_z"},"source":["!pip install lambda-learner"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"colab":{"base_uri":"https://localhost:8080/"},"id":"JzUeB5E57fiu","executionInfo":{"status":"ok","timestamp":1635240685074,"user_tz":-330,"elapsed":2466,"user":{"displayName":"Sparsh Agarwal","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"13037694610922482904"}},"outputId":"9d7ce8b0-5f70-4f8b-8d34-0c2eb2f70b75"},"source":["!wget -q --show-progress https://github.com/linkedin/lambda-learner/raw/main/test/resource/mock-ntv-data-1k.json\n","!wget -q --show-progress https://github.com/linkedin/lambda-learner/raw/main/test/resource/movie-lense-training-data-u359\n","!wget -q --show-progress https://github.com/linkedin/lambda-learner/raw/main/test/resource/movie-lense-test-data-u359\n","!wget -q --show-progress https://github.com/linkedin/lambda-learner/raw/main/test/resource/movie-lense-initital-fixed-effect-model\n","!wget -q --show-progress https://github.com/linkedin/lambda-learner/raw/main/test/resource/movie-lense-final-model-u359"],"execution_count":null,"outputs":[{"output_type":"stream","name":"stdout","text":["mock-ntv-data-1k.js 100%[===================>] 357.45K --.-KB/s in 0.03s \n","movie-lense-trainin 100%[===================>] 642.10K --.-KB/s in 0.04s \n","movie-lense-test-da 100%[===================>] 86.28K --.-KB/s in 0.01s \n","movie-lense-initita 100%[===================>] 383 --.-KB/s in 0s \n","movie-lense-final-m 100%[===================>] 624 --.-KB/s in 0s \n"]}]},{"cell_type":"code","metadata":{"id":"fLsHZm4V7Na8"},"source":["import json\n","import os\n","import pathlib\n","from typing import Any, Dict, List\n","\n","import numpy as np\n","from scipy import sparse\n","from itertools import permutations\n","\n","from linkedin.learner.ds.feature import Feature\n","from linkedin.learner.ds.indexed_dataset import IndexedDataset\n","from linkedin.learner.ds.record import TrainingRecord\n","from linkedin.learner.ds.indexed_model import IndexedModel\n","from linkedin.learner.prediction.linear_scorer import score_linear_model\n","from linkedin.learner.utils.functions import sparse_diag_matrix\n","from linkedin.learner.ds.index_map import IndexMap\n","from linkedin.learner.prediction.evaluator import evaluate\n","from linkedin.learner.prediction.hessian_type import HessianType\n","from linkedin.learner.prediction.trainer_square_loss_with_l2 import TrainerSquareLossWithL2\n","from linkedin.learner.prediction.trainer_logistic_loss_with_l2 import TrainerLogisticLossWithL2\n","from linkedin.learner.prediction.trainer_sequential_bayesian_logistic_loss_with_l2 import \\\n"," TrainerSequentialBayesianLogisticLossWithL2\n","from linkedin.learner.ds.representation_utils import (\n"," index_domain_coeffs_to_nt_domain_coeffs,\n"," nt_domain_coeffs_to_index_domain_coeffs,\n"," nt_domain_data_to_index_domain_data)\n","\n","import unittest"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"FqGKmOle7Zho"},"source":["MOCK_NTV_DATASET_PATH = \"mock-ntv-data-1k.json\"\n","MOVIE_LENS_TRAINING_DATA_PATH = \"movie-lense-training-data-u359\"\n","MOVIE_LENS_TEST_DATA_PATH = \"movie-lense-test-data-u359\"\n","MOVIE_LENS_INITIAL_MODEL_PATH = \"movie-lense-initital-fixed-effect-model\"\n","MOVIE_LENS_EXPECTED_TRAINED_MODEL_PATH = \"movie-lense-final-model-u359\""],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"PQnO3ABG7udI"},"source":["OFFLINE_RESPONSE = \"label\"\n","WEIGHT = \"weight\"\n","FEATURES = \"features\"\n","OFFSET = \"offset\""],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"NZmN9mc38O6P"},"source":["# Features are logged as name, term, value\n","FEATURE_NAME = \"name\"\n","FEATURE_TERM = \"term\"\n","FEATURE_VALUE = \"value\""],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"XaOxH-7Y8oov"},"source":["# How precise to make \"almost equal\" comparisons\n","PLACES_PRECISION = 8\n","RELATIVE_PRECISION = 10e-6"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"xF98l08W80g3"},"source":["## Utils"]},{"cell_type":"code","metadata":{"id":"s4qYcmcD8pe3"},"source":["def sequences_almost_equal(a, b, rel_precision: float = RELATIVE_PRECISION):\n"," \"\"\"Test whether two sequences are uniformly pointwise different by at most a given factor.\n"," This is a test helper intended to be used with [[assertTrue]] in a [[unittest.TestCase]]\n"," \"\"\"\n"," a_ndarray = np.array(a)\n"," b_ndarray = np.array(b)\n"," zero_adjustment = ((b_ndarray == 0) + 0) * (rel_precision / 1000)\n"," return all((abs(1 - (a_ndarray + zero_adjustment) / (b_ndarray + zero_adjustment)) < rel_precision).flatten())\n","\n","\n","def matrices_almost_equal(a, b, rel_precision: float = RELATIVE_PRECISION):\n"," \"\"\"Test whether two matrices are uniformly pointwise different by at most a given factor.\n"," This is a test helper intended to be used with [[assertTrue]] in a [[unittest.TestCase]]\n"," \"\"\"\n"," zero_adjustment = ((b == 0) + 0) * (rel_precision / 1000)\n"," return all((np.array(abs(1 - (a + zero_adjustment) / (b + zero_adjustment)) < rel_precision)).flatten())\n","\n","\n","def ensure_str(bytes_or_str, encoding: str = \"utf-8\"):\n"," \"\"\"\n"," Ensures that an object which is either a string or bytes is treated as a string.\n"," \"\"\"\n"," return str(bytes_or_str, encoding) if isinstance(bytes_or_str, bytes) else bytes_or_str"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"iEGDJOfW8Qb_"},"source":["def read_ntv_records_from_json(num_examples: int = -1, data_path=MOCK_NTV_DATASET_PATH):\n"," \"\"\"Read records from a file with one ntv json-formatted record per line.\n"," :param num_examples: How many records to load? -1 means no limit.\n"," :param data_path: Path to data file.\n"," :return: List of loaded records.\n"," \"\"\"\n"," with open(data_path, \"rb\") as file:\n"," if num_examples > 0:\n"," records = [json.loads(next(file)) for _ in range(num_examples)]\n"," else:\n"," records = [json.loads(line) for line in file]\n"," return records"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"G3brdWqY8WRX"},"source":["def read_model(file):\n"," \"\"\"Read an index-domain model file with one coefficient per line.\n"," :param file: Path to model file.\n"," :returns: NDArray of coefficients.\n"," \"\"\"\n"," with open(file, \"r\") as file:\n"," theta = [float(line) for line in file.readlines()]\n"," return np.array(theta)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"Y3bJI1jc8X2J"},"source":["def read_tsv_data(file):\n"," \"\"\"\"Read a tab-separated value file of index-domain data, with one record per line.\n"," :param file: Path to data file.\n"," :returns: IndexedDataset representing the loaded data.\n"," \"\"\"\n"," with open(file, \"r\") as file:\n"," example_list = [line.split(\"\\t\") for line in file.readlines()]\n"," num_examples = len(example_list)\n","\n"," labels = []\n"," values = []\n"," cols = []\n"," rows = []\n"," offsets = [0] * num_examples # temporary value\n"," weights = [1] * num_examples\n","\n"," for idx, example in enumerate(example_list):\n"," timestamp, y, *feature_values = example\n"," timestamp = int(timestamp)\n"," y = float(y)\n"," feature_values = [float(v) for v in feature_values]\n","\n"," # Add intercept\n"," values.append(1.0)\n"," cols.append(0)\n"," rows.append(idx)\n","\n"," num_features = len(feature_values)\n"," feature_idx = range(1, num_features + 1)\n","\n"," # Add other features\n"," values.extend(feature_values)\n"," cols.extend(feature_idx)\n"," rows.extend([idx] * num_features)\n","\n"," labels.append(y)\n","\n"," data = sparse.coo_matrix((values, (rows, cols)), shape=(num_examples, 1 + num_features), dtype=np.float64).tocsc()\n","\n"," offsets = np.array(offsets, dtype=np.float64)\n"," weights = np.array(weights, dtype=np.float64)\n"," labels = np.array(labels)\n","\n"," return IndexedDataset(data, labels, weights, offsets)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"0VM8bM4q8Zw0"},"source":["def read_movie_lens_data():\n"," \"\"\"Read the movie lens data for testing.\n"," :returns: Movie Lens training and test datasets.\n"," \"\"\"\n"," theta = read_model(MOVIE_LENS_INITIAL_MODEL_PATH)\n"," training_data = read_tsv_data(MOVIE_LENS_TRAINING_DATA_PATH)\n"," test_data = read_tsv_data(MOVIE_LENS_TEST_DATA_PATH)\n","\n"," training_data.offsets = training_data.X * theta\n"," test_data.offsets = test_data.X * theta\n","\n"," return training_data, test_data"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"iSbCDmX_8bVy"},"source":["def read_expected_movie_lens_training_result():\n"," \"\"\"Read the expected movie lens training result (the trained model).\n"," :returns: NDArray representing the loaded model.\n"," \"\"\"\n"," return read_model(MOVIE_LENS_EXPECTED_TRAINED_MODEL_PATH)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"mPU8AIlh8c_m"},"source":["def features_from_json(features_json: List[Any], feature_name_translation_map: Dict[str, str] = None) -> List[Feature]:\n"," \"\"\"Convert a bag of json-like features to Feature format.\n"," :param features_json: A list of features as parsed directly from json.\n"," :param feature_name_translation_map: (Optional) Mapping for using different names internally.\n"," :return: A list of Features.\n"," \"\"\"\n"," feature_list = []\n"," for feature in features_json:\n"," name, term, value = feature[FEATURE_NAME], feature[FEATURE_TERM], feature[FEATURE_VALUE]\n"," if not feature_name_translation_map:\n"," # use all features as they are\n"," feature_list.append(Feature(name, term, value))\n"," elif name in feature_name_translation_map:\n"," # use a subset of features, possibly translated to a different name\n"," # otherwise we drop this feature and don't use it in retraining\n"," translated_feature_name = feature_name_translation_map[name]\n"," feature_list.append(Feature(translated_feature_name, term, value))\n"," return feature_list"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"Cy6nWree8edf"},"source":["def training_record_from_json(record_json) -> TrainingRecord:\n"," \"\"\"Convert a json-like record in TrainingRecord format.\n"," :param record_json: A record as parsed directly from json.\n"," :return: A TrainingRecord object.\n"," \"\"\"\n"," features_json = record_json[FEATURES]\n"," features = features_from_json(features_json)\n"," return TrainingRecord(label=record_json[OFFLINE_RESPONSE], weight=record_json[WEIGHT], offset=record_json[OFFSET], features=features)"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"jzpasumK9xMD"},"source":["def generate_mock_training_data(num_examples: int = -1):\n"," \"\"\"Load large mock data and index it.\n"," :param num_examples: How many examples to load.\n"," :returns: A tuple of IndexedDataset, IndexMap\n"," \"\"\"\n"," raw_data = [training_record_from_json(_) for _ in read_ntv_records_from_json(num_examples)]\n"," imap, _ = IndexMap.from_records_means_and_variances(raw_data, [], [])\n"," indexed_data = nt_domain_data_to_index_domain_data(raw_data, imap)\n"," return indexed_data, imap\n","\n","\n","def simple_mock_data():\n"," \"\"\"Load a very small mock dataset.\n"," :returns: A tuple of IndexedDataset, IndexedModel\n"," \"\"\"\n"," row = np.array([0, 0, 0, 1, 1, 1])\n"," col = np.array([0, 1, 2, 0, 1, 2])\n"," data = np.array([1, 0.5, 3, 0.5, 0, 1.0])\n"," X = sparse.coo_matrix((data, (row, col)), shape=(2, 3), dtype=np.float64)\n"," y = np.array([-1.0, 1.0])\n"," offsets = np.array([-0.25, 1.0])\n"," w = np.array([1.0, 1.0])\n"," indexed_data = IndexedDataset(X, y, w, offsets)\n","\n"," theta = np.array([0.25, 0.75, -1.5])\n"," model = IndexedModel(theta=theta, hessian=sparse_diag_matrix([1, 1, 1]))\n","\n"," return indexed_data, model"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"eFCC9p-47Da4"},"source":["## Linear Scorer"]},{"cell_type":"code","metadata":{"id":"10mQmbcI85b4"},"source":["def mock_indexed_data():\n"," \"\"\"Get a very small mock indexed dataset.\"\"\"\n"," labels = np.array([1.0, -1.0, 1.0])\n"," weights = np.array([1, 1, 1])\n"," offsets = np.array([0.2, 0.3, 0.4])\n"," design_matrix = sparse.csc_matrix(np.array([[1.0, 1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0.5, 1.0, 1.3, 0.8]]))\n"," return IndexedDataset(design_matrix, labels, weights, offsets)\n","\n","\n","class LinearScorerTest(unittest.TestCase):\n"," def test_score_linear_model(self):\n"," \"\"\"Test linear scoring in the general case.\"\"\"\n"," theta = np.array([1.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6])\n"," hessian = sparse_diag_matrix([2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6])\n"," indexed_model = IndexedModel(theta, hessian)\n","\n"," indexed_data = mock_indexed_data()\n","\n"," scores = score_linear_model(indexed_model, indexed_data)\n","\n"," expected_scores = np.array(\n"," [0.2 + 1.0 * 1.0 + 1.0 * 0.1, 0.3 + 1.0 * 1.0, 0.4 + 1.0 * 1.0 + 0.5 * 0.3 + 1.0 * 0.4 + 1.3 * 0.5 + 0.8 * 0.6]\n"," )\n","\n"," self.assertTrue(all(scores == expected_scores), f\"Scores match expected scores. Actual {scores} == expected {expected_scores}.\")\n","\n"," scores_with_exploration = score_linear_model(indexed_model, indexed_data, exploration_threshold=1)\n","\n"," self.assertTrue(\n"," all(scores_with_exploration != expected_scores),\n"," f\"Scores with exploration don't exactly match expected scores. Actual {scores} == expected {expected_scores}.\",\n"," )\n","\n"," def test_score_linear_model_offsets_only(self):\n"," \"\"\"Test linear scoring when all coefficients are zero.\"\"\"\n"," num_features = 7\n"," theta = np.zeros(num_features)\n"," indexed_model = IndexedModel(theta)\n"," indexed_data = mock_indexed_data()\n","\n"," scores = score_linear_model(indexed_model, indexed_data)\n","\n"," expected_scores = np.array([0.2, 0.3, 0.4])\n","\n"," self.assertTrue(\n"," all(scores == expected_scores),\n"," f\"Offsets are used for scores when model coeffs are all zero/missing. Actual {scores} == expected {expected_scores}.\",\n"," )"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"woXlqkqh9R0j"},"source":["## Trainers"]},{"cell_type":"code","metadata":{"id":"qx960-Gj-qB1"},"source":["class TrainerSquareLossWithL2Test(unittest.TestCase):\n"," def test_square_loss_with_l2_loss_and_gradient(self):\n"," \"\"\"Test the loss and gradient functions.\n"," The expected values in this test are set using this implementation, so this\n"," is a test against regression, rather than strictly a test of correctness.\n"," \"\"\"\n"," training_data, test_data = read_movie_lens_data()\n","\n"," theta_0 = np.zeros(training_data.num_features)\n"," initial_model = IndexedModel(theta=theta_0)\n","\n"," trainer = TrainerSquareLossWithL2(training_data=training_data, initial_model=initial_model, penalty=10)\n"," final_model, final_loss, metadata = trainer.train(precision=1e4, num_corrections=7, max_iterations=50)\n","\n"," actual_scores = score_linear_model(model=final_model, test_data=test_data)\n"," final_rmse = evaluate(metric_list=[\"rmse\"], y_scores=actual_scores, y_targets=test_data.y)[\"rmse\"]\n","\n"," expected_theta = read_expected_movie_lens_training_result()\n"," expected_model = IndexedModel(theta=expected_theta)\n"," expected_scores = score_linear_model(expected_model, test_data)\n"," expected_rmse = evaluate(metric_list=[\"rmse\"], y_scores=expected_scores, y_targets=test_data.y)[\"rmse\"]\n","\n"," self.assertAlmostEqual(expected_rmse, final_rmse)\n","\n"," self.assertTrue(\n"," sequences_almost_equal(expected_theta, final_model.theta),\n"," f\"Theta as expected: Expected = {expected_theta}, Actual = {final_model.theta}\",\n"," )\n","\n"," def test_square_loss_with_l2_update_hessian(self):\n"," \"\"\"Test the Hessian update.\"\"\"\n"," indexed_data, model = simple_mock_data()\n","\n"," lr = TrainerSquareLossWithL2(training_data=indexed_data, initial_model=model, penalty=10, hessian_type=HessianType.FULL)\n"," hessian = lr._update_full_hessian(model.theta)\n","\n"," expected_hessian = np.array(\n"," [\n"," [0.0760066037, 0.00782668920, 0.1676665858],\n"," [0.00782668920, 0.00391334460, 0.02348006762],\n"," [0.1676665858, 0.02348006762, 0.382293306],\n"," ]\n"," )\n","\n"," self.assertTrue(\n"," matrices_almost_equal(hessian, expected_hessian),\n"," f\"Hessian computation is correct. Actual {hessian} == Expected {expected_hessian}.\",\n"," )"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"27o3eKxv9lMl"},"source":["class TrainerLogisticLossWithL2Test(unittest.TestCase):\n"," def test_lr_loss_and_gradient(self):\n"," \"\"\"Test the loss and gradient functions.\n"," The expected values in this test are set using this implementation, so this\n"," is a test against regression, rather than strictly a test of correctness.\n"," \"\"\"\n"," indexed_data, _ = generate_mock_training_data()\n","\n"," theta_0 = np.array([4, 100, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.float64)\n"," initial_model = IndexedModel(theta=theta_0)\n","\n"," lr = TrainerLogisticLossWithL2(training_data=indexed_data, initial_model=initial_model, penalty=10)\n"," initial_loss = lr.loss(theta_0)\n","\n"," self.assertAlmostEqual(initial_loss, 153042.61089443817, PLACES_PRECISION, \"Expect loss is calculated correctly.\")\n","\n"," initial_gradient = lr.gradient(theta_0)\n","\n"," self.assertTrue(\n"," sequences_almost_equal(initial_gradient, [1038.0, 1998.0, 879.0, 879.0, 1008.0, 1008.0, 134.0, 134.0, 15.0, 15.0]),\n"," \"Expect gradient is calculated correctly.\",\n"," )\n","\n"," final_model, final_loss, metadata = lr.train()\n","\n"," self.assertTrue(\n"," sequences_almost_equal(\n"," final_model.theta,\n"," [\n"," -0.14982811627991494,\n"," -0.14982824552049578,\n"," -0.19636393879356379,\n"," -0.19636393879356379,\n"," -0.1498281122411468,\n"," -0.1498281122411468,\n"," 0.04935191200563128,\n"," 0.04935191200563128,\n"," -0.0028160881457262527,\n"," -0.0028160881457262527,\n"," ],\n"," ),\n"," \"Expect theta after optimization is correct.\",\n"," )\n"," self.assertAlmostEqual(final_loss, 15.666764037573559, PLACES_PRECISION, \"Expect lost after optimization is correct.\")\n"," self.assertEqual(metadata[\"warnflag\"], 0, \"Expect convergence status is 0, meaning successful convergence.\")\n"," self.assertEqual(metadata[\"nit\"], 13, \"Expect number of iterations is correct.\")\n"," self.assertEqual(metadata[\"funcalls\"], 18, \"Expect number of function calls is correct.\")\n"," self.assertEqual(ensure_str(metadata[\"task\"]), \"CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL\", f\"Expect convergence reason is as expected\")\n","\n"," # Score (on the training data)\n"," initial_scores = score_linear_model(model=initial_model, test_data=indexed_data)\n"," initial_auc = evaluate(metric_list=[\"auc\"], y_scores=initial_scores, y_targets=indexed_data.y)[\"auc\"]\n"," final_scores = score_linear_model(model=final_model, test_data=indexed_data)\n"," final_auc = evaluate(metric_list=[\"auc\"], y_scores=final_scores, y_targets=indexed_data.y)[\"auc\"]\n","\n"," expected_initial_auc = 0.5290581162324649\n"," expected_final_auc = 0.6137274549098197\n","\n"," self.assertAlmostEqual(\n"," initial_auc, expected_initial_auc, PLACES_PRECISION, f\"Initial AUC: expected {expected_initial_auc} == actual {initial_auc}\"\n"," )\n"," self.assertAlmostEqual(\n"," final_auc, expected_final_auc, PLACES_PRECISION, f\"Final AUC: expected {expected_final_auc} == actual {final_auc}\"\n"," )\n","\n"," def test_lr_update_hessian(self):\n"," \"\"\"Test the Hessian update.\"\"\"\n"," indexed_data, model = simple_mock_data()\n","\n"," lr = TrainerLogisticLossWithL2(training_data=indexed_data, initial_model=model, penalty=10, hessian_type=HessianType.FULL)\n"," hessian = lr._update_full_hessian(model.theta)\n","\n"," expected_hessian = np.array(\n"," [\n"," [10.076006603, 0.007826689207, 0.16766658587],\n"," [0.007826689207, 10.003913344, 0.023480067621],\n"," [0.16766658587, 0.023480067621, 10.382293306],\n"," ]\n"," )\n","\n"," self.assertTrue(\n"," matrices_almost_equal(hessian, expected_hessian),\n"," f\"Hessian computation is correct. Actual {hessian} == Expected {expected_hessian}.\",\n"," )"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"UTNimmea-_sx"},"source":["class TrainerSequentialBayesianLogisticLossWithL2Test(unittest.TestCase):\n"," def test_incremental_lr_loss_and_gradient(self):\n"," \"\"\"Test the loss and gradient functions.\n"," The expected values in this test are set using this implementation, so this\n"," is a test against regression, rather than strictly a test of correctness.\n"," \"\"\"\n"," indexed_data, _ = generate_mock_training_data()\n","\n"," theta_0 = np.array([4, 100, 1, 1, 1, 1, 1, 1, 1, 1], dtype=np.float64)\n"," initial_model = IndexedModel(theta=theta_0)\n"," # Discrepancies with offline: (1) Labels are 0/1; in reality the data processing will set it to -1/+1.\n"," # (2) Intercept gets regularized nearline but not offline, (3) This test should ideally only set initial_theta\n"," # without changing hist_theta, but current implementation does not allow that.\n","\n"," lr = TrainerSequentialBayesianLogisticLossWithL2(training_data=indexed_data, initial_model=initial_model, penalty=10, delta=0.8)\n"," initial_loss = lr.loss(theta_0)\n","\n"," self.assertAlmostEqual(initial_loss, 112946.61089443817, PLACES_PRECISION, \"Expect loss is calculated correctly.\")\n","\n"," initial_gradient = lr.gradient(theta_0)\n","\n"," self.assertTrue(\n"," sequences_almost_equal(initial_gradient, [1006.0, 1198.0, 871.0, 871.0, 1000.0, 1000.0, 126.0, 126.0, 7.0, 7.0]),\n"," \"Expect gradient is calculated correctly.\",\n"," )\n","\n"," final_model, final_loss, metadata = lr.train()\n","\n"," self.assertTrue(\n"," sequences_almost_equal(\n"," final_model.theta,\n"," [\n"," -13.37418015,\n"," 63.42582255,\n"," -7.97896804,\n"," -7.97896804,\n"," -15.77418024,\n"," -15.77418024,\n"," -6.49519736,\n"," -6.49519736,\n"," 0.29998522,\n"," 0.29998522,\n"," ],\n"," ),\n"," \"Expect theta after optimization is correct.\",\n"," )\n"," self.assertAlmostEqual(final_loss, 15104.873256903807, PLACES_PRECISION, \"Expect lost after optimization is correct.\")\n"," self.assertEqual(metadata[\"warnflag\"], 0, \"Expect convergence status is 0, meaning successful convergence.\")\n"," self.assertEqual(metadata[\"nit\"], 20, \"Expect number of iterations is correct.\")\n"," self.assertEqual(metadata[\"funcalls\"], 28, \"Expect number of function calls is correct.\")\n"," self.assertEqual(ensure_str(metadata[\"task\"]), \"CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH\", f\"Expect convergence reason is as expected\")\n","\n"," # Score (on the training data)\n"," initial_scores = score_linear_model(model=initial_model, test_data=indexed_data)\n"," initial_auc = evaluate(metric_list=[\"auc\"], y_scores=initial_scores, y_targets=indexed_data.y)[\"auc\"]\n"," final_scores = score_linear_model(model=final_model, test_data=indexed_data)\n"," final_auc = evaluate(metric_list=[\"auc\"], y_scores=final_scores, y_targets=indexed_data.y)[\"auc\"]\n","\n"," expected_initial_auc = 0.5290581162324649\n"," expected_final_auc = 0.7059118236472945\n","\n"," self.assertAlmostEqual(\n"," initial_auc, expected_initial_auc, PLACES_PRECISION, f\"Initial AUC: expected {expected_initial_auc} == actual {initial_auc}\"\n"," )\n"," self.assertAlmostEqual(\n"," final_auc, expected_final_auc, PLACES_PRECISION, f\"Final AUC: expected {expected_final_auc} == actual {final_auc}\"\n"," )\n","\n"," def test_incremental_lr_update_hessian(self):\n"," \"\"\"Test the Hessian update when using a full Hessian.\"\"\"\n"," indexed_data, model = simple_mock_data()\n"," lr = TrainerSequentialBayesianLogisticLossWithL2(\n"," training_data=indexed_data, initial_model=model, hessian_type=HessianType.FULL, penalty=10, delta=1.0\n"," )\n"," hessian = lr._update_hessian(model.theta)\n"," expected_hessian = np.array(\n"," [[1.076006603, 0.007826689, 0.167666585], [0.007826689, 1.003913344, 0.023480067], [0.167666585, 0.023480067, 1.382293306]]\n"," )\n"," self.assertTrue(\n"," matrices_almost_equal(hessian, expected_hessian),\n"," f\"(Full) Hessian computation is correct. Actual {hessian} == Expected {expected_hessian}.\",\n"," )\n","\n"," def test_incremental_lr_update_hessian_diagonal(self):\n"," \"\"\"Test the Hessian update when using a diagonal Hessian.\"\"\"\n"," indexed_data, model = simple_mock_data()\n"," lr = TrainerSequentialBayesianLogisticLossWithL2(\n"," training_data=indexed_data, initial_model=model, hessian_type=HessianType.DIAGONAL, penalty=10, delta=1.0\n"," )\n"," hessian = lr._update_hessian(model.theta).toarray()\n"," expected_hessian = np.diag([1.076006603, 1.003913344, 1.382293306])\n"," self.assertTrue(\n"," matrices_almost_equal(hessian, expected_hessian),\n"," f\"(Diagonal) Hessian computation is correct. Actual {hessian} == Expected {expected_hessian}.\",\n"," )\n","\n"," def test_incremental_lr_update_hessian_identity(self):\n"," \"\"\"Test the Hessian update when using an identity Hessian.\"\"\"\n"," indexed_data, model = simple_mock_data()\n"," lr = TrainerSequentialBayesianLogisticLossWithL2(\n"," training_data=indexed_data, initial_model=model, hessian_type=HessianType.IDENTITY, penalty=10, delta=1.0\n"," )\n"," hessian = lr._update_hessian(model.theta).toarray()\n"," expected_hessian = np.diag([1.0, 1.0, 1.0])\n"," self.assertTrue(\n"," matrices_almost_equal(hessian, expected_hessian),\n"," f\"Hessian computation is correct. Actual {hessian} == Expected {expected_hessian}.\",\n"," )\n","\n"," def test_hessian_computation_is_optional(self):\n"," \"\"\"Test that the Hessian update is optional.\"\"\"\n"," indexed_data, model = simple_mock_data()\n","\n"," trainer = TrainerSequentialBayesianLogisticLossWithL2(\n"," training_data=indexed_data, initial_model=model, penalty=10, hessian_type=HessianType.NONE\n"," )\n"," final_model, _, _ = trainer.train(precision=1e4, num_corrections=7, max_iterations=50)\n","\n"," self.assertTrue(final_model.hessian is None, \"Hessian computation optional.\")"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"kX9C0eyOCwrG"},"source":["## Data Representations"]},{"cell_type":"code","metadata":{"id":"kYEONK1qC3xb"},"source":["class IndexMapTest(unittest.TestCase):\n"," def test_default_imap_construction(self):\n"," imap = IndexMap()\n"," self.assertEqual(len(imap), 1, \"Intercept is already indexed.\")\n"," self.assertEqual(imap.intercept_nt, (\"intercept\", \"intercept\"), 'Intercept name and term are both \"intercept\" by default.')\n"," self.assertEqual(imap.intercept_index, 0, \"Intercept is indexed first.\")\n","\n"," def test_custom_imap_construction(self):\n"," imap = IndexMap(\"iname\", \"iterm\")\n"," self.assertEqual(len(imap), 1, \"Intercept is already indexed.\")\n"," self.assertEqual(imap.intercept_nt, (\"iname\", \"iterm\"), 'Intercept name and term are both \"intercept\" by default.')\n"," self.assertEqual(imap.intercept_index, 0, \"Intercept is indexed first.\")\n","\n"," def test_imap_index_errors(self):\n"," imap = IndexMap()\n","\n"," with self.assertRaises(IndexError, msg=\"Trying to get unindexed features results in IndexError\"):\n"," imap.get_nt(1)\n","\n"," n1t1 = (\"n1\", \"t1\")\n"," imap.push(n1t1)\n","\n"," self.assertEqual(imap.get_nt(1), n1t1, \"IndexError no longer raised once index has been assigned.\")\n","\n"," def test_one_at_a_time_imap_construction(self):\n"," imap = IndexMap()\n"," self.assertEqual(len(imap), 1, \"Intercept is already indexed.\")\n","\n"," n1t1 = (\"n1\", \"t1\")\n"," imap.push(n1t1)\n","\n"," self.assertEqual(len(imap), 2, \"Pushing a new one feature increases the imap size by 1.\")\n"," self.assertEqual(imap.get_index(n1t1), 1, \"The next available index is used.\")\n"," self.assertEqual(imap.get_nt(1), n1t1, \"The feature can be retrieved using its index.\")\n","\n"," imap.push(n1t1)\n","\n"," self.assertEqual(len(imap), 2, \"Pushing an already added feature does nothing.\")\n","\n"," n1t2 = (\"n1\", \"t2\")\n"," imap.push(n1t2)\n","\n"," self.assertEqual(len(imap), 3, \"Pushing another new feature increases the imap size by 1.\")\n","\n"," def test_batch_imap_construction(self):\n"," imap = IndexMap()\n","\n"," imap.batch_push([(\"n1\", \"t1\"), (\"n1\", \"t2\"), (\"n1\", \"t3\"), (\"n2\", \"t1\"), (\"n2\", \"t2\")])\n","\n"," self.assertEqual(len(imap), 6, \"Pushing 5 distinct features results in 6 indexed features (including the intercept).\")\n","\n"," for i in range(len(imap)):\n"," self.assertEqual(\n"," imap.get_index(imap.get_nt(i)),\n"," i,\n"," \"The imap defines a one-to-one mapping from the index domain to the feature name-term domain.\",\n"," )\n","\n"," expected_nts_1 = [(\"intercept\", \"intercept\"), (\"n1\", \"t1\"), (\"n1\", \"t2\"), (\"n1\", \"t3\"), (\"n2\", \"t1\"), (\"n2\", \"t2\")]\n","\n"," for nt in expected_nts_1:\n"," # Success here just means it did not throw. The actual mapping is an implementation detail.\n"," self.assertGreaterEqual(imap.get_index(nt), 0, f\"Can forward-lookup an added feature: {nt}.\")\n","\n"," imap.batch_push(\n"," [\n"," # Already indexed\n"," (\"n1\", \"t1\"),\n"," (\"n1\", \"t2\"),\n"," (\"n1\", \"t3\"),\n"," # Not yet indexed\n"," (\"n3\", \"t1\"),\n"," (\"n3\", \"t2\"),\n"," ]\n"," )\n","\n"," self.assertEqual(len(imap), 8, \"Pushing a mix of indexed and un-indexed features, only the un-indexed are added.\")\n","\n"," for i in range(len(imap)):\n"," self.assertEqual(imap.get_index(imap.get_nt(i)), i, \"After multiple batch pushes the index mapping remains correct.\")\n","\n"," expected_nts_2 = [\n"," (\"intercept\", \"intercept\"),\n"," (\"n1\", \"t1\"),\n"," (\"n1\", \"t2\"),\n"," (\"n1\", \"t3\"),\n"," (\"n2\", \"t1\"),\n"," (\"n2\", \"t2\"),\n"," (\"n3\", \"t1\"),\n"," (\"n3\", \"t2\"),\n"," ]\n","\n"," for nt in expected_nts_2:\n"," # Success here just means it did not throw. The actual mapping is an implementation detail.\n"," self.assertGreaterEqual(imap.get_index(nt), 0, f\"Can forward-lookup an added feature: {nt}.\")\n","\n"," def test_from_records_means_and_variances_builder(self):\n"," # coefficients\n"," means = [Feature(\"n1\", \"t1\", 1.0), Feature(\"n1\", \"t2\", 1.0), Feature(\"n2\", \"t3\", 1.0)]\n"," variances = [Feature(\"n1\", \"t1\", 1.0), Feature(\"n1\", \"t2\", 1.0), Feature(\"n2\", \"t3\", 1.0)]\n","\n"," # training data\n"," records = [\n"," TrainingRecord(1, 1, 0, [Feature(\"n1\", \"t1\", 1.0)]),\n"," TrainingRecord(1, 1, 0, [Feature(\"n2\", \"t3\", 1.0)]),\n"," TrainingRecord(\n"," 1, 1, 0, [Feature(\"n3\", \"t1\", 1.0), Feature(\"n3\", \"t2\", 1.0), Feature(\"n3\", \"t3\", 1.0), Feature(\"n3\", \"t4\", 1.0)]\n"," ),\n"," ]\n","\n"," imap, meta = IndexMap.from_records_means_and_variances(records, means, variances)\n","\n"," self.assertEqual(len(imap), 8)\n","\n"," for i in range(len(imap)):\n"," self.assertEqual(\n"," imap.get_index(imap.get_nt(i)),\n"," i,\n"," \"The imap defines a one-to-one mapping from the index domain to the feature name-term domain.\",\n"," )\n","\n"," expected_nts = [\n"," (\"intercept\", \"intercept\"),\n"," (\"n1\", \"t1\"),\n"," (\"n1\", \"t2\"),\n"," (\"n2\", \"t3\"),\n"," (\"n3\", \"t1\"),\n"," (\"n3\", \"t2\"),\n"," (\"n3\", \"t3\"),\n"," (\"n3\", \"t4\"),\n"," ]\n","\n"," for nt in expected_nts:\n"," # Success here just means it did not throw. The actual mapping is an implementation detail.\n"," self.assertGreaterEqual(imap.get_index(nt), 0, f\"Can forward-lookup an added feature: {nt}.\")\n","\n"," self.assertEqual(meta[\"num_nts_in_both\"], 2, \"Correct metadata: num features in shared by records and coeffs.\")\n"," self.assertEqual(meta[\"num_nts_in_coeffs_only\"], 1, \"Correct metadata: num features only in coeffs.\")\n"," self.assertEqual(meta[\"num_nts_in_records_only\"], 4, \"Correct metadata: num features only in records.\")"],"execution_count":null,"outputs":[]},{"cell_type":"code","metadata":{"id":"NCEV36ZPC0D3"},"source":["class RepresentationUtilsTest(unittest.TestCase):\n"," def test_nt_domain_data_to_index_domain_data(self):\n"," records = [\n"," TrainingRecord(1.0, 1.0, 0.2, [Feature(\"n1\", \"t1\", 1.0)]),\n"," TrainingRecord(0.0, 1.0, 0.3, [Feature(\"n2\", \"t3\", 0.0)]),\n"," TrainingRecord(\n"," 1.0, 1.0, 0.4, [Feature(\"n3\", \"t1\", 0.5), Feature(\"n3\", \"t2\", 1.0), Feature(\"n3\", \"t3\", 1.3), Feature(\"n3\", \"t4\", 0.8)]\n"," ),\n"," ]\n","\n"," imap, _ = IndexMap.from_records_means_and_variances(records, [], [])\n","\n"," indexed_data = nt_domain_data_to_index_domain_data(records, imap)\n","\n"," expected_y = np.array([1.0, -1.0, 1.0])\n"," expected_w = np.array([1, 1, 1])\n"," expected_offsets = np.array([0.2, 0.3, 0.4])\n","\n"," self.assertTrue(all(indexed_data.y == expected_y), \"Index-domain labels are correct.\")\n"," self.assertTrue(all(indexed_data.w == expected_w), \"Index-domain weights are correct.\")\n"," self.assertTrue(all(indexed_data.offsets == expected_offsets), \"Index-domain offsets are correct.\")\n","\n"," expected_sparse_matrix = sparse.csc_matrix(\n"," np.array([[1.0, 1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0.5, 1.0, 1.3, 0.8]])\n"," )\n","\n"," num_mismatches = (indexed_data.X != expected_sparse_matrix).nnz\n","\n"," self.assertEqual(num_mismatches, 0, \"Index-domain design matrix is correct.\")\n","\n"," def test_nt_domain_coeffs_to_index_domain_coeffs(self):\n"," means = [Feature(\"n1\", \"t1\", 0.1), Feature(\"n1\", \"t2\", 0.2), Feature(\"n2\", \"t3\", 0.3)]\n"," variances = [Feature(\"n1\", \"t1\", 1.1), Feature(\"n1\", \"t2\", 1.2), Feature(\"n2\", \"t3\", 1.3)]\n"," imap, _ = IndexMap.from_records_means_and_variances([], means, variances)\n"," indexed_model = nt_domain_coeffs_to_index_domain_coeffs(means, variances, imap, regularization_param=0)\n","\n"," expected_theta = np.array([0.0, 0.1, 0.2, 0.3])\n"," expected_hessian = sparse_diag_matrix([0.0, 1 / 1.1, 1 / 1.2, 1 / 1.3])\n","\n"," self.assertTrue(\n"," all(indexed_model.theta == expected_theta),\n"," f\"(Index-domain) theta is correct. Actual {indexed_model.theta} == expected {expected_theta}.\",\n"," )\n"," self.assertTrue(\n"," all((indexed_model.hessian == expected_hessian).toarray().flatten()),\n"," f\"(Index-domain) hessian is correct. Actual {indexed_model.hessian} == expected {expected_hessian}.\",\n"," )\n","\n"," def test_index_domain_coeffs_to_nt_domain_coeffs(self):\n"," means = [Feature(\"intercept\", \"intercept\", 1.0), Feature(\"n1\", \"t1\", 0.1), Feature(\"n1\", \"t2\", 0.2), Feature(\"n1\", \"t3\", 0.2)]\n"," variances = [Feature(\"intercept\", \"intercept\", 2.0), Feature(\"n1\", \"t1\", 1.1), Feature(\"n1\", \"t2\", 1.2), Feature(\"n1\", \"t3\", 1.3)]\n"," imap, _ = IndexMap.from_records_means_and_variances([], means, variances)\n"," theta = np.array([1.0, 0.1, 0.2, 0.3])\n"," hessian = sparse_diag_matrix([1 / 2.0, 1 / 1.1, 1 / 1.2, 1 / 1.3])\n"," indexed_model = IndexedModel(theta, hessian)\n"," mean_dict, variance_dict = index_domain_coeffs_to_nt_domain_coeffs(indexed_model, imap)\n","\n"," expected_mean_dict = {(\"intercept\", \"intercept\"): 1.0, (\"n1\", \"t1\"): 0.1, (\"n1\", \"t2\"): 0.2, (\"n1\", \"t3\"): 0.3}\n"," expected_variance_dict = {(\"intercept\", \"intercept\"): 2.0, (\"n1\", \"t1\"): 1.1, (\"n1\", \"t2\"): 1.2, (\"n1\", \"t3\"): 1.3}\n","\n"," self.assertEqual(mean_dict, expected_mean_dict, \"(Index-domain) theta is correct.\")\n"," self.assertEqual(variance_dict, expected_variance_dict, \"(Index-domain) hessian is correct.\")\n","\n"," def test_nt_index_domain_full_cycle(self):\n"," features = [Feature(\"intercept\", \"intercept\", 1.0), Feature(\"n1\", \"t1\", 0.1), Feature(\"n1\", \"t2\", 0.2), Feature(\"n1\", \"t3\", 0.3)]\n","\n"," expected_coeff_dict = {(\"intercept\", \"intercept\"): 1.0, (\"n1\", \"t1\"): 0.1, (\"n1\", \"t2\"): 0.2, (\"n1\", \"t3\"): 0.3}\n","\n"," feature_permutations = list(permutations(features))\n","\n"," # Test all permutations, to ensure correctness is independent of the particular mapping the imap produces.\n"," for ordered_features in feature_permutations:\n"," means, variances = ordered_features, ordered_features\n"," imap, _ = IndexMap.from_records_means_and_variances([], means, variances)\n"," indexed_model = nt_domain_coeffs_to_index_domain_coeffs(means, variances, imap, regularization_param=0)\n"," mean_dict, variance_dict = index_domain_coeffs_to_nt_domain_coeffs(indexed_model, imap)\n","\n"," self.assertEqual(\n"," mean_dict, expected_coeff_dict, f\"(Index-domain) theta is correct, given feature ordering: {ordered_features}.\"\n"," )\n"," self.assertEqual(\n"," variance_dict, expected_coeff_dict, f\"(Index-domain) hessian is correct, given feature ordering: {ordered_features}.\"\n"," )"],"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"tiqs46Zj-v_S"},"source":["## UnitTest"]},{"cell_type":"code","metadata":{"colab":{"base_uri":"https://localhost:8080/"},"id":"qppi_NeH7Iql","executionInfo":{"status":"ok","timestamp":1635242478791,"user_tz":-330,"elapsed":541,"user":{"displayName":"Sparsh Agarwal","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"13037694610922482904"}},"outputId":"7d9ee7b4-6fc2-432d-c7d0-1949e614a749"},"source":["unittest.main(argv=[''], verbosity=2, exit=False)"],"execution_count":null,"outputs":[{"output_type":"stream","name":"stderr","text":["test_batch_imap_construction (__main__.IndexMapTest) ... ok\n","test_custom_imap_construction (__main__.IndexMapTest) ... ok\n","test_default_imap_construction (__main__.IndexMapTest) ... ok\n","test_from_records_means_and_variances_builder (__main__.IndexMapTest) ... ok\n","test_imap_index_errors (__main__.IndexMapTest) ... ok\n","test_one_at_a_time_imap_construction (__main__.IndexMapTest) ... ok\n","test_score_linear_model (__main__.LinearScorerTest)\n","Test linear scoring in the general case. ... ok\n","test_score_linear_model_offsets_only (__main__.LinearScorerTest)\n","Test linear scoring when all coefficients are zero. ... ok\n","test_index_domain_coeffs_to_nt_domain_coeffs (__main__.RepresentationUtilsTest) ... ok\n","test_nt_domain_coeffs_to_index_domain_coeffs (__main__.RepresentationUtilsTest) ... /usr/lib/python3.7/unittest/case.py:628: SparseEfficiencyWarning: Comparing sparse matrices using == is inefficient, try using != instead.\n"," testMethod()\n","ok\n","test_nt_domain_data_to_index_domain_data (__main__.RepresentationUtilsTest) ... ok\n","test_nt_index_domain_full_cycle (__main__.RepresentationUtilsTest) ... ok\n","test_lr_loss_and_gradient (__main__.TrainerLogisticLossWithL2Test)\n","Test the loss and gradient functions. ... ok\n","test_lr_update_hessian (__main__.TrainerLogisticLossWithL2Test)\n","Test the Hessian update. ... ok\n","test_hessian_computation_is_optional (__main__.TrainerSequentialBayesianLogisticLossWithL2Test)\n","Test that the Hessian update is optional. ... ok\n","test_incremental_lr_loss_and_gradient (__main__.TrainerSequentialBayesianLogisticLossWithL2Test)\n","Test the loss and gradient functions. ... ok\n","test_incremental_lr_update_hessian (__main__.TrainerSequentialBayesianLogisticLossWithL2Test)\n","Test the Hessian update when using a full Hessian. ... ok\n","test_incremental_lr_update_hessian_diagonal (__main__.TrainerSequentialBayesianLogisticLossWithL2Test)\n","Test the Hessian update when using a diagonal Hessian. ... ok\n","test_incremental_lr_update_hessian_identity (__main__.TrainerSequentialBayesianLogisticLossWithL2Test)\n","Test the Hessian update when using an identity Hessian. ... ok\n","test_square_loss_with_l2_loss_and_gradient (__main__.TrainerSquareLossWithL2Test)\n","Test the loss and gradient functions. ... ok\n","test_square_loss_with_l2_update_hessian (__main__.TrainerSquareLossWithL2Test)\n","Test the Hessian update. ... ok\n","\n","----------------------------------------------------------------------\n","Ran 21 tests in 0.281s\n","\n","OK\n"]},{"output_type":"execute_result","data":{"text/plain":[""]},"metadata":{},"execution_count":41}]}]}