{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "DnLV1HUefFtW"
   },
   "source": [
    "# Text Features and Embeddings In CatBoost"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "0UAHpnD8fFtZ"
   },
   "source": [
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/catboost/tutorials/blob/master/events/2020_11_18_catboost_tutorial/text_embedding_features.ipynb)\n",
    "\n",
    "\n",
    "**Set GPU as hardware accelerator**\n",
    "\n",
    "First of all, you need to select GPU as hardware accelerator. There are two simple steps to do so:\n",
    "Step 1. Navigate to **Runtime** menu and select **Change runtime type**\n",
    "Step 2. Choose **GPU** as hardware accelerator.\n",
    "That's all!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "9FM0IRyi8NOw"
   },
   "source": [
    "Let's install CatBoost."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 361
    },
    "colab_type": "code",
    "id": "TpJdgt63fSOv",
    "outputId": "d62a776e-f741-4192-b919-91903ea0441b"
   },
   "outputs": [],
   "source": [
    "!pip install catboost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    },
    "colab_type": "code",
    "id": "MNC1tP0UfFtd",
    "outputId": "2c0abe55-df9c-4a0f-daa4-dc8c8d858f63"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "np.set_printoptions(precision=4)\n",
    "\n",
    "import catboost\n",
    "print(catboost.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "OkexL1k7fFti"
   },
   "source": [
    "## Preparing data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "viF18QJqfFtd"
   },
   "source": [
    "In this tutorial we will use dataset **IMDB** from [Kaggle](https://www.kaggle.com) competition for our experiments. Data can be downloaded [here](https://www.kaggle.com/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!wget https://transfersh.com/ou7jB/imdb.csv -O imdb.csv\n",
    "df = pd.read_csv('imdb.csv')\n",
    "df['label'] = (df['sentiment'] == 'positive').astype(int)\n",
    "df.drop(['sentiment'], axis=1, inplace=True)\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from catboost import Pool\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "train_df, test_df = train_test_split(df, train_size=0.8, random_state=0)\n",
    "y_train, X_train = train_df['label'], train_df.drop(['label'], axis=1)\n",
    "y_test, X_test = test_df['label'], test_df.drop(['label'], axis=1)\n",
    "\n",
    "train_pool = Pool(data=X_train, label=y_train, text_features=['review'])\n",
    "test_pool = Pool(data=X_test, label=y_test, text_features=['review'])\n",
    "\n",
    "print('Train dataset shape: {}\\n'.format(train_pool.shape))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 269
    },
    "colab_type": "code",
    "id": "VTi3eN58fFt6",
    "outputId": "e694fed2-1341-45a3-c799-334b32fbc01e"
   },
   "outputs": [],
   "source": [
    "from catboost import CatBoostClassifier\n",
    "\n",
    "def fit_model(train_pool, test_pool, **kwargs):\n",
    "    model = CatBoostClassifier(\n",
    "        iterations=1000,\n",
    "        learning_rate=0.05,\n",
    "        eval_metric='AUC',\n",
    "        **kwargs\n",
    "    )\n",
    "\n",
    "    return model.fit(\n",
    "        train_pool,\n",
    "        eval_set=test_pool,\n",
    "        verbose=100,\n",
    "    )\n",
    "\n",
    "model = fit_model(train_pool, test_pool, task_type='GPU')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "IiHpTGfbfFuV"
   },
   "source": [
    "## How it works?\n",
    "\n",
    "1. **Text Tokenization**\n",
    "2. **Dictionary Creation**\n",
    "3. **Feature Calculation**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "MszSnbqH8NR3"
   },
   "source": [
    "## Text Tokenization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "mOBGuexjb8tr"
   },
   "source": [
    "Usually we get our text as a sequence of Unicode symbols. So, if the task isn't a DNA classification we don't need such granularity, moreover, we need to extract more complicated entities, e.g. words. The process of extraction tokens -- words, numbers, punctuation symbols or special symbols which defines emoji from a sequence is called **tokenization**.<br>\n",
    "\n",
    "Tokenization is the first part of text preprocessing in CatBoost and performed as a simple splitting a sequence on a string pattern (e.g. space)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "NAeELULufFuV"
   },
   "outputs": [],
   "source": [
    "text_small = [\n",
    "    \"Cats are so cute :)\",\n",
    "    \"Mouse scare...\",\n",
    "    \"The cat defeated the mouse\",\n",
    "    \"Cute: Mice gather an army!\",\n",
    "    \"Army of mice defeated the cat :(\",\n",
    "    \"Cat offers peace\",\n",
    "    \"Cat is scared :(\",\n",
    "    \"Cat and mouse live in peace :)\"\n",
    "]\n",
    "\n",
    "target_small = [1, 0, 1, 1, 0, 1, 0, 1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 161
    },
    "colab_type": "code",
    "id": "E21CQ8ocfFuX",
    "outputId": "f78b995b-29fc-41c9-b28c-b3adee167ba7"
   },
   "outputs": [],
   "source": [
    "from catboost.text_processing import Tokenizer\n",
    "\n",
    "simple_tokenizer = Tokenizer()\n",
    "\n",
    "def tokenize_texts(texts):\n",
    "    return [simple_tokenizer.tokenize(text) for text in texts]\n",
    "\n",
    "simple_tokenized_text = tokenize_texts(text_small)\n",
    "simple_tokenized_text"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ChZQ5cpJfFuZ"
   },
   "source": [
    "### More preprocessing!\n",
    "\n",
    "Lets take a closer look on the tokenization result of small text example -- the tokens contains a lot of mistakes:\n",
    "\n",
    "1. They are glued with punctuation 'Cute:', 'army!', 'skare...'.\n",
    "2. The words 'Cat' and 'cat', 'Mice' and 'mice' seems to have same meaning, perhaps they should be the same tokens.\n",
    "3. The same problem with tokens 'are'/'is' -- they are inflected forms of same token 'be'.\n",
    "\n",
    "**Punctuation handling**, **lowercasing**, and **lemmatization** processes help to overcome these problems."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "qaoTjEmR8NSM"
   },
   "source": [
    "### Punctuation handling and lowercasing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 161
    },
    "colab_type": "code",
    "id": "6cPpYpmtfFuZ",
    "outputId": "2bc7abef-5828-43af-d588-48edb490eed9"
   },
   "outputs": [],
   "source": [
    "tokenizer = Tokenizer(\n",
    "    lowercasing=True,\n",
    "    separator_type='BySense',\n",
    "    token_types=['Word', 'Number']\n",
    ")\n",
    "\n",
    "tokenized_text = [tokenizer.tokenize(text) for text in text_small]\n",
    "tokenized_text"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "JDhBkZzJfFua"
   },
   "source": [
    "### Removing stop words\n",
    "\n",
    "**Stop words** - the words that are considered to be uninformative in this task, e.g. function words such as *the, is, at, which, on*.\n",
    "Usually stop words are removed during text preprocessing to reduce the amount of information that is considered for further algorithms.\n",
    "Stop words are collected manually (in dictionary form) or automatically, for example taking the most frequent words."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 161
    },
    "colab_type": "code",
    "id": "d1MYzKgTfFub",
    "outputId": "865f655e-0cb9-4626-9d40-e459b9487b0f"
   },
   "outputs": [],
   "source": [
    "stop_words = set(('be', 'is', 'are', 'the', 'an', 'of', 'and', 'in'))\n",
    "\n",
    "def filter_stop_words(tokens):\n",
    "    return list(filter(lambda x: x not in stop_words, tokens))\n",
    "    \n",
    "tokenized_text_no_stop = [filter_stop_words(tokens) for tokens in tokenized_text]\n",
    "tokenized_text_no_stop"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "vxofPVc1fFuc"
   },
   "source": [
    "### Lemmatization\n",
    "\n",
    "Lemma (Wikipedia) -- is the canonical form, dictionary form, or citation form of a set of words.<br>\n",
    "For example, the lemma \"go\" represents the inflected forms \"go\", \"goes\", \"going\", \"went\", and \"gone\".<br>\n",
    "The process of convertation word to its lemma called **lemmatization**.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 89
    },
    "colab_type": "code",
    "id": "HWrijpMGfFud",
    "outputId": "1b6b8015-8cf9-47c5-89cf-5d5fc8b5f794"
   },
   "outputs": [],
   "source": [
    "import nltk\n",
    "\n",
    "nltk_data_path = os.path.join(os.path.dirname(nltk.__file__), 'nltk_data')\n",
    "nltk.data.path.append(nltk_data_path)\n",
    "nltk.download('wordnet', nltk_data_path)\n",
    "\n",
    "lemmatizer = nltk.stem.WordNetLemmatizer()\n",
    "\n",
    "def lemmatize_tokens_nltk(tokens):\n",
    "    return list(map(lambda t: lemmatizer.lemmatize(t), tokens))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 161
    },
    "colab_type": "code",
    "id": "XfyhV9ONfFuf",
    "outputId": "4b0568c9-3bb8-483a-8f86-dd358c6fd2c5"
   },
   "outputs": [],
   "source": [
    "text_small_lemmatized_nltk = [lemmatize_tokens_nltk(tokens) for tokens in tokenized_text_no_stop]\n",
    "text_small_lemmatized_nltk"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "y63KVna4fFui"
   },
   "source": [
    "Now words with same meaning represented by the same token, tokens are not glued with punctuation.\n",
    "\n",
    "<span style=\"color:red\">Be carefull.</span> You should verify for your own task:<br>\n",
    "Is it realy necessary to remove punctuation, lowercasing sentences or performing a lemmatization and/or by word tokenization?<br>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "qFWoSX-kfFui"
   },
   "source": [
    "### Let's check up accuracy with new text preprocessing\n",
    "\n",
    "Since CatBoost doesn't perform spacing punctuation, lowercasing letters and lemmatization, we need to preprocess text manually and then pass it to learning algorithm.\n",
    "\n",
    "Since the natural text features is only synopsis and review, we will preprocess only them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 53
    },
    "colab_type": "code",
    "id": "ZHL3x7NwfFuj",
    "outputId": "85135452-02ea-4644-882d-726fcc568605"
   },
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "def preprocess_data(X):\n",
    "    X_preprocessed = X.copy()\n",
    "    X_preprocessed['review'] = X['review'].apply(lambda x: ' '.join(lemmatize_tokens_nltk(tokenizer.tokenize(x))))\n",
    "    return X_preprocessed\n",
    "\n",
    "X_preprocessed_train = preprocess_data(X_train)\n",
    "X_preprocessed_test = preprocess_data(X_test)\n",
    "\n",
    "train_processed_pool = Pool(\n",
    "    X_preprocessed_train, y_train, \n",
    "    text_features=['review'],\n",
    ")\n",
    "\n",
    "test_processed_pool = Pool(\n",
    "    X_preprocessed_test, y_test, \n",
    "    text_features=['review'],\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 269
    },
    "colab_type": "code",
    "id": "0jJJSrFJfFuk",
    "outputId": "6baeef42-d430-4793-fc33-556095416a9b"
   },
   "outputs": [],
   "source": [
    "model_on_processed_data = fit_model(train_processed_pool, test_processed_pool, task_type='GPU')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 35
    },
    "colab_type": "code",
    "id": "AXDdPAgyfFum",
    "outputId": "61e26e81-b858-4675-ab58-aaf3384428ae"
   },
   "outputs": [],
   "source": [
    "def print_score_diff(first_model, second_model):\n",
    "    first_accuracy = first_model.best_score_['validation']['AUC']\n",
    "    second_accuracy = second_model.best_score_['validation']['AUC']\n",
    "\n",
    "    gap = (second_accuracy - first_accuracy) / first_accuracy * 100\n",
    "\n",
    "    print('{} vs {} ({:+.2f}%)'.format(first_accuracy, second_accuracy, gap))\n",
    "    \n",
    "print_score_diff(model, model_on_processed_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "CJr7fXN7fFun"
   },
   "source": [
    "## Dictionary Creation\n",
    "\n",
    "After the first stage, preprocessing of text and tokenization, the second stage starts. The second stage uses the prepared text to select a set of units, which will be used for building new numerical features.\n",
    "\n",
    "A set of selected units is called dictionary. It might contain words, word bigramms, or character n-gramms."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "D6H1MXf9fFuo"
   },
   "outputs": [],
   "source": [
    "from catboost.text_processing import Dictionary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "text_small_lemmatized_nltk"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "Rn402k78fFuq"
   },
   "outputs": [],
   "source": [
    "dictionary = Dictionary(occurence_lower_bound=0, max_dictionary_size=10)\n",
    "\n",
    "dictionary.fit(text_small_lemmatized_nltk);\n",
    "#dictionary.fit(text_small, tokenizer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 253
    },
    "colab_type": "code",
    "id": "KJr0UBzOfFur",
    "outputId": "4ab23b42-0fb7-4ac4-c878-63da839c8635"
   },
   "outputs": [],
   "source": [
    "dictionary.save('dictionary.tsv')\n",
    "!cat dictionary.tsv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dictionary.apply([text_small_lemmatized_nltk[0]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "U1wLb5MX8NTY"
   },
   "source": [
    "## Feature Calculation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "KYzNqXgcfFut"
   },
   "source": [
    "### Convertation into fixed size vectors\n",
    "\n",
    "The majority of classic ML algorithms are computing and performing predictions on a fixed number of features $F$.<br>\n",
    "That means that learning set $X = \\{x_i\\}$ contains vectors $x_i = (a_0, a_1, ..., a_F)$ where $F$ is constant.\n",
    "\n",
    "Since text object $x$ is not a fixed length vector, we need to perform preprocessing of the origin set $D$.<br>\n",
    "One of the simplest text to vector encoding technique is **Bag of words (BoW)**.\n",
    "\n",
    "### Bag of words algorithm\n",
    "\n",
    "The algorithm takes in a dictionary and a text.<br>\n",
    "During the algorithm text $x = (a_0, a_1, ..., a_k)$ converted into vector $\\tilde x = (b_0, b_1, ..., b_F)$,<br> where $b_i$ is 0/1 (depending on whether there is a word with id=$i$ from dictionary into text $x$)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_proc_train_small, y_train_small = X_preprocessed_train[:1000]['review'].to_list(), y_train[:1000]\n",
    "X_proc_train_small = list(map(simple_tokenizer.tokenize, X_proc_train_small))\n",
    "X_proc_test_small, y_test_small = X_preprocessed_test[:1000]['review'].to_list(), y_test[:1000]\n",
    "X_proc_test_small = list(map(simple_tokenizer.tokenize, X_proc_test_small))\n",
    "\n",
    "dictionary = Dictionary(max_dictionary_size=100)\n",
    "dictionary.fit(X_proc_train_small);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 305
    },
    "colab_type": "code",
    "id": "ga0AfpT8fFuv",
    "outputId": "6b6e9abb-3e2a-4a8e-eac9-dacbac3c33fd"
   },
   "outputs": [],
   "source": [
    "def bag_of_words(tokenized_text, dictionary):\n",
    "    features = np.zeros((len(tokenized_text), dictionary.size))\n",
    "    for i, tokenized_sentence in enumerate(tokenized_text):\n",
    "        indices = np.array(dictionary.apply([tokenized_sentence])[0])\n",
    "        if len(indices) > 0:\n",
    "            features[i, indices] = 1\n",
    "    return features\n",
    "\n",
    "X_bow_train_small = bag_of_words(X_proc_train_small, dictionary)\n",
    "X_bow_test_small = bag_of_words(X_proc_test_small, dictionary)\n",
    "X_bow_train_small.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "vhr-EyPyfFuy"
   },
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.naive_bayes import MultinomialNB\n",
    "from scipy.sparse import csr_matrix\n",
    "from sklearn.metrics import roc_auc_score\n",
    "\n",
    "def fit_linear_model(X, y):\n",
    "    model = LogisticRegression()\n",
    "    model.fit(X, y)\n",
    "    return model\n",
    "\n",
    "def evaluate_model_auc(model, X, y):\n",
    "    y_pred = model.predict_proba(X)[:,1]\n",
    "    metric = roc_auc_score(y, y_pred)\n",
    "    print('AUC: ' + str(metric))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 125
    },
    "colab_type": "code",
    "id": "GekNCx5ofFuz",
    "outputId": "5b218b73-c7fd-4628-f218-29d0d30686eb"
   },
   "outputs": [],
   "source": [
    "def evaluate_models(X_train, y_train, X_test, y_test):\n",
    "    linear_model = fit_linear_model(X_train, y_train)\n",
    "        \n",
    "    print('Linear model')\n",
    "    evaluate_model_auc(linear_model, X_test, y_test)\n",
    "    print('Comparing to constant prediction')\n",
    "    auc_constant_prediction = roc_auc_score(y_test, np.ones(shape=(len(y_test), 1)) * 0.5)\n",
    "    print('AUC: ' + str(auc_constant_prediction))\n",
    "    \n",
    "evaluate_models(X_bow_train_small, y_train_small, X_bow_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 125
    },
    "colab_type": "code",
    "id": "uFsAWNE9fFu2",
    "outputId": "7197acdf-71ac-4c81-b507-4f06cafdbea8"
   },
   "outputs": [],
   "source": [
    "unigram_dictionary = Dictionary(occurence_lower_bound=0, max_dictionary_size=1000)\n",
    "unigram_dictionary.fit(X_proc_train_small)\n",
    "\n",
    "X_bow_train_small = bag_of_words(X_proc_train_small, unigram_dictionary)\n",
    "X_bow_test_small = bag_of_words(X_proc_test_small, unigram_dictionary)\n",
    "print(X_bow_train_small.shape)\n",
    "\n",
    "evaluate_models(X_bow_train_small, y_train_small, X_bow_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "yvjUACB_fFu6"
   },
   "source": [
    "### Looking at sequences of letters / words\n",
    "\n",
    "Let's look at the example: texts 'The cat defeated the mouse' and 'Army of mice defeated the cat :('<br>\n",
    "Simplifying it we have three tokens in each sentence 'cat defeat mouse' and 'mouse defeat cat'.<br>\n",
    "After applying BoW we get two equal vectors with the opposite meaning:\n",
    "\n",
    "| cat | mouse | defeat |\n",
    "|-----|-------|--------|\n",
    "| 1   | 1     | 1      |\n",
    "| 1   | 1     | 1      |\n",
    "\n",
    "How to distinguish them?\n",
    "Lets add sequences of words as a single tokens into our dictionary:\n",
    "\n",
    "| cat | mouse | defeat | cat_defeat | mouse_defeat | defeat_cat | defeat_mouse |\n",
    "|-----|-------|--------|------------|--------------|------------|--------------|\n",
    "| 1   | 1     | 1      | 1          | 0            | 0          | 1            |\n",
    "| 1   | 1     | 1      | 0          | 1            | 1          | 0            |\n",
    "\n",
    "**N-gram** is a continguous sequence of $n$ items from a given sample of text or speech (Wikipedia).<br>\n",
    "In example above Bi-gram (Bigram) = 2-gram of words.\n",
    "\n",
    "Ngrams help to add into vectors more information about text structure, moreover there are n-grams has no meanings in separation, for example, 'Mickey Mouse company'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 379
    },
    "colab_type": "code",
    "id": "WU6iWFPZClrf",
    "outputId": "b666b9a2-0782-472a-a729-0fa1b15bd9f2"
   },
   "outputs": [],
   "source": [
    "dictionary = Dictionary(occurence_lower_bound=0, gram_order=2)\n",
    "dictionary.fit(text_small_lemmatized_nltk)\n",
    "\n",
    "dictionary.save('dictionary.tsv')\n",
    "!cat dictionary.tsv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 125
    },
    "colab_type": "code",
    "id": "ypPTi_XXfFu7",
    "outputId": "59136696-c457-4f99-b884-cf1e2e68fb80"
   },
   "outputs": [],
   "source": [
    "bigram_dictionary = Dictionary(occurence_lower_bound=0, max_dictionary_size=5000, gram_order=2)\n",
    "bigram_dictionary.fit(X_proc_train_small)\n",
    "\n",
    "X_bow_train_small = bag_of_words(X_proc_train_small, bigram_dictionary)\n",
    "X_bow_test_small = bag_of_words(X_proc_test_small, bigram_dictionary)\n",
    "print(X_bow_train_small.shape)\n",
    "\n",
    "evaluate_models(X_bow_train_small, y_train_small, X_bow_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "1uLlIfJHodEL"
   },
   "source": [
    "### Unigram + Bigram"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 125
    },
    "colab_type": "code",
    "id": "XaRC74kNfFu8",
    "outputId": "f67a5ea4-0795-4b16-db80-2bff733109e9"
   },
   "outputs": [],
   "source": [
    "X_bow_train_small = np.concatenate((\n",
    "    bag_of_words(X_proc_train_small, unigram_dictionary),\n",
    "    bag_of_words(X_proc_train_small, bigram_dictionary)\n",
    "), axis=1)\n",
    "X_bow_test_small = np.concatenate((\n",
    "    bag_of_words(X_proc_test_small, unigram_dictionary),\n",
    "    bag_of_words(X_proc_test_small, bigram_dictionary)\n",
    "), axis=1)\n",
    "print(X_bow_train_small.shape)\n",
    "\n",
    "evaluate_models(X_bow_train_small, y_train_small, X_bow_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "oFR_rMfH8NT_"
   },
   "source": [
    "## CatBoost Configuration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "8xoFAOiz8NT_"
   },
   "source": [
    "Parameter names:\n",
    "\n",
    "1. **Text Tokenization** - `tokenizers`\n",
    "2. **Dictionary Creation** - `dictionaries`\n",
    "3. **Feature Calculation** - `feature_calcers`\n",
    "\n",
    "\\* More complex configuration with `text_processing` parameter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "Wntt3XrYgkhf"
   },
   "source": [
    "### `tokenizers`\n",
    "\n",
    "Tokenizers used to preprocess Text type feature columns before creating the dictionary.\n",
    "\n",
    "[Documentation](https://catboost.ai/docs/references/tokenizer_options.html).\n",
    "\n",
    "```\n",
    "tokenizers = [{\n",
    "\t'tokenizerId': 'Space',\n",
    "\t'delimiter': ' ',\n",
    "\t'separator_type': 'ByDelimiter',\n",
    "},{\n",
    "\t'tokenizerId': 'Sense',\n",
    "\t'separator_type': 'BySense',\n",
    "}]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "aKqHyav7fFu-"
   },
   "source": [
    "### `dictionaries`\n",
    "\n",
    "Dictionaries used to preprocess Text type feature columns.\n",
    "\n",
    "[Documentation](https://catboost.ai/docs/references/dictionaries_options.html).\n",
    "\n",
    "```\n",
    "dictionaries = [{\n",
    "\t'dictionaryId': 'Unigram',\n",
    "\t'max_dictionary_size': '50000',\n",
    "\t'gram_count': '1',\n",
    "},{\n",
    "\t'dictionaryId': 'Bigram',\n",
    "\t'max_dictionary_size': '50000',\n",
    "\t'gram_count': '2',\n",
    "},{\n",
    "\t'dictionaryId': 'Trigram',\n",
    "\t'token_level_type': 'Letter',\n",
    "\t'max_dictionary_size': '50000',\n",
    "\t'gram_count': '3',\n",
    "}]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "JT6I_LN98NUC"
   },
   "source": [
    "### `feature_calcers`\n",
    "\n",
    "Feature calcers used to calculate new features based on preprocessed Text type feature columns.\n",
    "\n",
    "1. **`BoW`**<br>\n",
    "Bag of words: 0/1 features (text sample has or not token_id).<br>\n",
    "Number of produced numeric features = dictionary size.<br>\n",
    "Parameters: `top_tokens_count` - maximum number of tokens that will be used for vectorization in bag of words, the most frequent $n$ tokens are taken (**highly affect both on CPU ang GPU RAM usage**).\n",
    "\n",
    "2. **`NaiveBayes`**<br>\n",
    "NaiveBayes: [Multinomial naive bayes](https://en.wikipedia.org/wiki/Naive_Bayes_classifier#Multinomial_naive_Bayes) model. As many new features as classes are added. This feature is calculated by analogy with counters in CatBoost by permutation ([estimation of CTRs](https://catboost.ai/docs/concepts/algorithm-main-stages_cat-to-numberic.html)). In other words, a random permutation is made and then we go from top to bottom on the dataset and calculate the probability of its belonging to this class for each object.\n",
    "\n",
    "3. **`BM25`**<br>\n",
    "[BM25](https://en.wikipedia.org/wiki/Okapi_BM25). As many new features as classes are added. The idea is the same as in Naive Bayes, but for each class we calculate not the conditional probability, but a certain relevance, which is similar to tf-idf, where the tokens instead of the words and the classes instead of the documents (or rather, the unification of all texts of this class). Only the tf multiplier in BM25 is replaced with another multiplier, which gives an advantage to classes that contain rare tokens.\n",
    "\n",
    "```\n",
    "feature_calcers = [\n",
    "\t'BoW:top_tokens_count=1000',\n",
    "\t'NaiveBayes',\n",
    "\t'BM25',\n",
    "]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "02lH5f1PgpYM"
   },
   "source": [
    "### `text_processing`\n",
    "\n",
    "```\n",
    "text_processing = {\n",
    "    \"tokenizers\" : [{\n",
    "        \"tokenizer_id\" : \"Space\",\n",
    "        \"separator_type\" : \"ByDelimiter\",\n",
    "        \"delimiter\" : \" \"\n",
    "    }],\n",
    "\n",
    "    \"dictionaries\" : [{\n",
    "        \"dictionary_id\" : \"BiGram\",\n",
    "        \"max_dictionary_size\" : \"50000\",\n",
    "        \"occurrence_lower_bound\" : \"3\",\n",
    "        \"gram_order\" : \"2\"\n",
    "    }, {\n",
    "        \"dictionary_id\" : \"Word\",\n",
    "        \"max_dictionary_size\" : \"50000\",\n",
    "        \"occurrence_lower_bound\" : \"3\",\n",
    "        \"gram_order\" : \"1\"\n",
    "    }],\n",
    "\n",
    "    \"feature_processing\" : {\n",
    "        \"default\" : [{\n",
    "            \"dictionaries_names\" : [\"BiGram\", \"Word\"],\n",
    "            \"feature_calcers\" : [\"BoW\"],\n",
    "            \"tokenizers_names\" : [\"Space\"]\n",
    "        }, {\n",
    "            \"dictionaries_names\" : [\"Word\"],\n",
    "            \"feature_calcers\" : [\"NaiveBayes\"],\n",
    "            \"tokenizers_names\" : [\"Space\"]\n",
    "        }],\n",
    "    }\n",
    "}\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "xlo77dzufFvE"
   },
   "source": [
    "## Summary: Text features in CatBoost\n",
    "\n",
    "### The algorithm:\n",
    "1. Input text is loaded as a usual column. ``text_column: [string]``.\n",
    "2. Each text sample is tokenized via splitting by space. ``tokenized_column: [[string]]``.\n",
    "3. Dictionary estimation.\n",
    "4. Each string in tokenized column is converted into token_id from dictionary. ``text: [[token_id]]``.\n",
    "5. Depending on the parameters CatBoost produce features basing on the resulting text column: Bag of words, Multinomial naive bayes or Bm25.\n",
    "6. Computed float features are passed into the usual CatBoost learning algorithm."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "_A87DhGF8SIa"
   },
   "source": [
    "# Embeddings In CatBoost"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Get Embeddings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# from sentence_transformers import SentenceTransformer\n",
    "# big_model = SentenceTransformer('roberta-large-nli-stsb-mean-tokens')\n",
    "# X_embed_train = big_model.encode(X_train['review'].to_list())\n",
    "# X_embed_test = big_model.encode(X_test['review'].to_list())\n",
    "\n",
    "!wget https://transfersh.com/HDHxy/embedded_train.npy -O embedded_train.npy\n",
    "X_embed_train = np.load('embedded_train.npy')\n",
    "\n",
    "!wget https://transfersh.com/whOm3/embedded_test.npy -O embedded_test.npy\n",
    "X_embed_test = np.load('embedded_test.npy')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Experiments"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_embed_first_train_small, y_first_train_small = X_embed_train[:5000], y_train[:5000]\n",
    "X_embed_second_train_small, y_second_train_small = X_embed_train[5000:10000], y_train[5000:10000]\n",
    "X_embed_test_small, y_test_small = X_embed_test[:5000], y_test[:5000]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Pure embeddings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "evaluate_models(X_embed_second_train_small, y_second_train_small, X_embed_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Linear Discriminant Analysis"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "\n",
    "lda = LinearDiscriminantAnalysis(solver='svd')\n",
    "lda.fit(X_embed_first_train_small, y_first_train_small)\n",
    "\n",
    "X_lda_train_small = lda.transform(X_embed_second_train_small)\n",
    "X_lda_test_small = lda.transform(X_embed_test_small)\n",
    "print(X_lda_train_small.shape)\n",
    "evaluate_models(X_lda_train_small, y_second_train_small, X_lda_test_small, y_test_small)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Embeddings in CatBoost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import csv\n",
    "with open('train_embed_text.tsv', 'w') as f:\n",
    "    writer = csv.writer(f, delimiter='\\t', quotechar='\"')\n",
    "    for y, text, row in zip(y_train, X_preprocessed_train['review'].to_list(), X_embed_train):\n",
    "        writer.writerow((str(y), text, ';'.join(map(str, row))))\n",
    "\n",
    "with open('test_embed_text.tsv', 'w') as f:\n",
    "    writer = csv.writer(f, delimiter='\\t', quotechar='\"')\n",
    "    for y, text, row in zip(y_test, X_preprocessed_test['review'].to_list(), X_embed_test):\n",
    "        writer.writerow((str(y), text, ';'.join(map(str, row))))\n",
    "        \n",
    "with open('pool_text.cd', 'w') as f:\n",
    "    f.write(\n",
    "        '0\\tLabel\\n'\\\n",
    "        '1\\tText\\n'\\\n",
    "        '2\\tNumVector'\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from catboost import Pool\n",
    "train_embed_pool = Pool('train_embed_text.tsv', column_description='pool_text.cd')\n",
    "test_embed_pool = Pool('test_embed_text.tsv', column_description='pool_text.cd')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model_text_embeddings = fit_model(train_embed_pool, test_embed_pool)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print_score_diff(model, model_text_embeddings)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Thanks!"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "kernelspec": {
   "display_name": "Python 3",
   "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.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
