Skip to content

Instantly share code, notes, and snippets.

@angrymeir
Last active January 20, 2021 16:33
Show Gist options
  • Save angrymeir/b2657897bc50d4d3a134237a20a2a19f to your computer and use it in GitHub Desktop.
Save angrymeir/b2657897bc50d4d3a134237a20a2a19f to your computer and use it in GitHub Desktop.
Sequential PySS3.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Sequential PySS3.ipynb",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true,
"authorship_tag": "ABX9TyPp7Q+RyKWBGdoxpib5gfeH",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/angrymeir/b2657897bc50d4d3a134237a20a2a19f/sequential-pyss3.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "cZIWcU0tesn2",
"outputId": "a1618b57-4b95-4b92-97c2-a9f0d6c1a0f0"
},
"source": [
"!pip3 install numpy sklearn iterative-stratification pandas pyss3"
],
"execution_count": 1,
"outputs": [
{
"output_type": "stream",
"text": [
"Requirement already satisfied: numpy in /usr/local/lib/python3.6/dist-packages (1.19.5)\n",
"Requirement already satisfied: sklearn in /usr/local/lib/python3.6/dist-packages (0.0)\n",
"Collecting iterative-stratification\n",
" Downloading https://files.pythonhosted.org/packages/9d/79/9ba64c8c07b07b8b45d80725b2ebd7b7884701c1da34f70d4749f7b45f9a/iterative_stratification-0.1.6-py3-none-any.whl\n",
"Requirement already satisfied: pandas in /usr/local/lib/python3.6/dist-packages (1.1.5)\n",
"Collecting pyss3\n",
"\u001b[?25l Downloading https://files.pythonhosted.org/packages/67/d9/93197c8cfcb1c689f3ff9693256c33e51294d26f4af4d079708c4ff089b7/pyss3-0.6.3-py3-none-any.whl (2.0MB)\n",
"\u001b[K |████████████████████████████████| 2.0MB 4.1MB/s \n",
"\u001b[?25hRequirement already satisfied: scikit-learn in /usr/local/lib/python3.6/dist-packages (from sklearn) (0.22.2.post1)\n",
"Requirement already satisfied: scipy in /usr/local/lib/python3.6/dist-packages (from iterative-stratification) (1.4.1)\n",
"Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas) (2018.9)\n",
"Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.6/dist-packages (from pandas) (2.8.1)\n",
"Requirement already satisfied: matplotlib in /usr/local/lib/python3.6/dist-packages (from pyss3) (3.2.2)\n",
"Requirement already satisfied: tqdm>=4.8.4 in /usr/local/lib/python3.6/dist-packages (from pyss3) (4.41.1)\n",
"Requirement already satisfied: cython in /usr/local/lib/python3.6/dist-packages (from pyss3) (0.29.21)\n",
"Requirement already satisfied: six in /usr/local/lib/python3.6/dist-packages (from pyss3) (1.15.0)\n",
"Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.6/dist-packages (from scikit-learn->sklearn) (1.0.0)\n",
"Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib->pyss3) (2.4.7)\n",
"Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib->pyss3) (1.3.1)\n",
"Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib->pyss3) (0.10.0)\n",
"Installing collected packages: iterative-stratification, pyss3\n",
"Successfully installed iterative-stratification-0.1.6 pyss3-0.6.3\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "mMKs8ai7eqDd"
},
"source": [
"%matplotlib inline\n",
"\n",
"from pyss3 import SS3\n",
"from pyss3.util import Dataset, Evaluation, span\n",
"from pyss3.server import Live_Test\n",
"\n",
"from sklearn.metrics import accuracy_score"
],
"execution_count": 1,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "3l1QQffNojyX",
"outputId": "1b3c60fb-7e86-4717-fa1a-50db9081472b",
"colab": {
"base_uri": "https://localhost:8080/"
}
},
"source": [
"!cat /usr/local/lib/python3.6/dist-packages/pyss3/__init__.py"
],
"execution_count": 2,
"outputs": [
{
"output_type": "stream",
"text": [
"# -*- coding: utf-8 -*-\n",
"\"\"\"\n",
"This is the main module containing the implementation of the SS3 classifier.\n",
"\n",
"(Please, visit https://github.com/sergioburdisso/pyss3 for more info)\n",
"\"\"\"\n",
"from __future__ import print_function\n",
"import os\n",
"import re\n",
"import json\n",
"import errno\n",
"import numpy as np\n",
"\n",
"from io import open\n",
"from time import time\n",
"from tqdm import tqdm\n",
"from math import pow, tanh\n",
"from .util import is_a_collection, Print, VERBOSITY, Preproc as Pp\n",
"\n",
"# python 2 and 3 compatibility\n",
"from functools import reduce\n",
"from six.moves import xrange\n",
"\n",
"__version__ = \"0.6.3\"\n",
"\n",
"ENCODING = \"utf-8\"\n",
"\n",
"PARA_DELTR = \"\\n\"\n",
"SENT_DELTR = r\"\\.\"\n",
"WORD_DELTR = r\"\\s\"\n",
"WORD_REGEX = r\"\\w+(?:'\\w+)?\"\n",
"\n",
"STR_UNKNOWN, STR_MOST_PROBABLE = \"unknown\", \"most-probable\"\n",
"STR_OTHERS_CATEGORY = \"[others]\"\n",
"STR_UNKNOWN_CATEGORY = \"[unknown]\"\n",
"IDX_UNKNOWN_CATEGORY = -1\n",
"STR_UNKNOWN_WORD = ''\n",
"IDX_UNKNOWN_WORD = -1\n",
"STR_VANILLA, STR_XAI = \"vanilla\", \"xai\"\n",
"STR_GV, STR_NORM_GV, STR_NORM_GV_XAI = \"gv\", \"norm_gv\", \"norm_gv_xai\"\n",
"\n",
"STR_MODEL_FOLDER = \"ss3_models\"\n",
"STR_MODEL_EXT = \"ss3m\"\n",
"\n",
"VERBOSITY = VERBOSITY # to allow \"from pyss3 import VERBOSITY\"\n",
"\n",
"NAME = 0\n",
"VOCAB = 1\n",
"\n",
"NEXT = 0\n",
"FR = 1\n",
"CV = 2\n",
"SG = 3\n",
"GV = 4\n",
"LV = 5\n",
"EMPTY_WORD_INFO = [0, 0, 0, 0, 0, 0]\n",
"\n",
"NOISE_FR = 1\n",
"MIN_MAD_SD = .03\n",
"\n",
"\n",
"class SS3:\n",
" \"\"\"\n",
" The SS3 classifier class.\n",
"\n",
" The SS3 classifier was originally defined in Section 3 of\n",
" https://dx.doi.org/10.1016/j.eswa.2019.05.023\n",
" (preprint avialable here: https://arxiv.org/abs/1905.08772)\n",
"\n",
" :param s: the \"smoothness\"(sigma) hyperparameter value\n",
" :type s: float\n",
" :param l: the \"significance\"(lambda) hyperparameter value\n",
" :type l: float\n",
" :param p: the \"sanction\"(rho) hyperparameter value\n",
" :type p: float\n",
" :param a: the alpha hyperparameter value (i.e. all terms with a\n",
" confidence value (cv) less than alpha will be ignored during\n",
" classification)\n",
" :type a: float\n",
" :param name: the model's name (to save and load the model from disk)\n",
" :type name: str\n",
" :param cv_m: method used to compute the confidence value (cv) of each\n",
" term (word or n-grams), options are:\n",
" \"norm_gv_xai\", \"norm_gv\" and \"gv\" (default: \"norm_gv_xai\")\n",
" :type cv_m: str\n",
" :param sg_m: method used to compute the significance (sg) function, options\n",
" are: \"vanilla\" and \"xai\" (default: \"xai\")\n",
" :type sg_m: str\n",
" \"\"\"\n",
"\n",
" __name__ = \"model\"\n",
" __models_folder__ = STR_MODEL_FOLDER\n",
"\n",
" __s__ = .45\n",
" __l__ = .5\n",
" __p__ = 1\n",
" __a__ = .0\n",
"\n",
" __multilabel__ = False\n",
"\n",
" __l_update__ = None\n",
" __s_update__ = None\n",
" __p_update__ = None\n",
"\n",
" __cv_cache__ = None\n",
" __last_x_test__ = None\n",
" __last_x_test_idx__ = None\n",
"\n",
" __prun_floor__ = 10\n",
" __prun_trigger__ = 1000000\n",
" __prun_counter__ = 0\n",
"\n",
" __zero_cv__ = None\n",
"\n",
" __parag_delimiter__ = PARA_DELTR\n",
" __sent_delimiter__ = SENT_DELTR\n",
" __word_delimiter__ = WORD_DELTR\n",
" __word_regex__ = WORD_REGEX\n",
"\n",
" def __init__(\n",
" self, s=None, l=None, p=None, a=None,\n",
" name=\"\", cv_m=STR_NORM_GV_XAI, sg_m=STR_XAI\n",
" ):\n",
" \"\"\"\n",
" Class constructor.\n",
"\n",
" :param s: the \"smoothness\"(sigma) hyperparameter value\n",
" :type s: float\n",
" :param l: the \"significance\"(lambda) hyperparameter value\n",
" :type l: float\n",
" :param p: the \"sanction\"(rho) hyperparameter value\n",
" :type p: float\n",
" :param a: the alpha hyperparameter value (i.e. all terms with a\n",
" confidence value (cv) less than alpha will be ignored during\n",
" classification)\n",
" :type a: float\n",
" :param name: the model's name (to save and load the model from disk)\n",
" :type name: str\n",
" :param cv_m: method used to compute the confidence value (cv) of each\n",
" term (word or n-grams), options are:\n",
" \"norm_gv_xai\", \"norm_gv\" and \"gv\" (default: \"norm_gv_xai\")\n",
" :type cv_m: str\n",
" :param sg_m: method used to compute the significance (sg) function, options\n",
" are: \"vanilla\" and \"xai\" (default: \"xai\")\n",
" :type sg_m: str\n",
" :raises: ValueError\n",
" \"\"\"\n",
" self.__name__ = (name or self.__name__).lower()\n",
"\n",
" self.__s__ = self.__s__ if s is None else s\n",
" self.__l__ = self.__l__ if l is None else l\n",
" self.__p__ = self.__p__ if p is None else p\n",
" self.__a__ = self.__a__ if a is None else a\n",
"\n",
" try:\n",
" float(self.__s__ + self.__l__ + self.__p__ + self.__a__)\n",
" except BaseException:\n",
" raise ValueError(\"hyperparameter values must be numbers\")\n",
"\n",
" self.__categories_index__ = {}\n",
" self.__categories__ = []\n",
" self.__max_fr__ = []\n",
" self.__max_gv__ = []\n",
"\n",
" self.__index_to_word__ = {}\n",
" self.__word_to_index__ = {}\n",
"\n",
" if cv_m == STR_NORM_GV_XAI:\n",
" self.__cv__ = self.__cv_norm_gv_xai__\n",
" elif cv_m == STR_NORM_GV:\n",
" self.__cv__ = self.__cv_norm_gv__\n",
" elif cv_m == STR_GV:\n",
" self.__cv__ = self.__gv__\n",
"\n",
" if sg_m == STR_XAI:\n",
" self.__sg__ = self.__sg_xai__\n",
" elif sg_m == STR_VANILLA:\n",
" self.__sg__ = self.__sg_vanilla__\n",
"\n",
" self.__cv_mode__ = cv_m\n",
" self.__sg_mode__ = sg_m\n",
"\n",
" self.original_sumop_ngrams = self.summary_op_ngrams\n",
" self.original_sumop_sentences = self.summary_op_sentences\n",
" self.original_sumop_paragraphs = self.summary_op_paragraphs\n",
"\n",
" def __lv__(self, ngram, icat, cache=True):\n",
" \"\"\"Local value function.\"\"\"\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[LV]\n",
" else:\n",
" try:\n",
" ilength = len(ngram) - 1\n",
" fr = self.__trie_node__(ngram, icat)[FR]\n",
" if fr > NOISE_FR:\n",
" max_fr = self.__max_fr__[icat][ilength]\n",
" local_value = (fr / float(max_fr)) ** self.__s__\n",
" return local_value\n",
" else:\n",
" return 0\n",
" except TypeError:\n",
" return 0\n",
" except IndexError:\n",
" return 0\n",
"\n",
" def __sn__(self, ngram, icat):\n",
" \"\"\"The sanction (sn) function.\"\"\"\n",
" m_values = [\n",
" self.__sg__(ngram, ic)\n",
" for ic in xrange(len(self.__categories__)) if ic != icat\n",
" ]\n",
"\n",
" c = len(self.__categories__)\n",
"\n",
" s = sum([min(v, 1) for v in m_values])\n",
"\n",
" try:\n",
" return pow((c - (s + 1)) / ((c - 1) * (s + 1)), self.__p__)\n",
" except ZeroDivisionError: # if c <= 1\n",
" return 1.\n",
"\n",
" def __sg_vanilla__(self, ngram, icat, cache=True):\n",
" \"\"\"The original significance (sg) function definition.\"\"\"\n",
" try:\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[SG]\n",
" else:\n",
" ncats = len(self.__categories__)\n",
" l = self.__l__\n",
" lvs = [self.__lv__(ngram, ic) for ic in xrange(ncats)]\n",
" lv = lvs[icat]\n",
"\n",
" M, sd = mad(lvs, ncats)\n",
"\n",
" if not sd and lv:\n",
" return 1.\n",
" else:\n",
" return sigmoid(lv - M, l * sd)\n",
" except TypeError:\n",
" return 0.\n",
"\n",
" def __sg_xai__(self, ngram, icat, cache=True):\n",
" \"\"\"\n",
" A variation of the significance (sn) function.\n",
"\n",
" This version of the sg function adds extra checks to\n",
" improve visual explanations.\n",
" \"\"\"\n",
" try:\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[SG]\n",
" else:\n",
" ncats = len(self.__categories__)\n",
" l = self.__l__\n",
"\n",
" lvs = [self.__lv__(ngram, ic) for ic in xrange(ncats)]\n",
" lv = lvs[icat]\n",
"\n",
" M, sd = mad(lvs, ncats)\n",
"\n",
" if l * sd <= MIN_MAD_SD:\n",
" sd = MIN_MAD_SD / l if l else 0\n",
"\n",
" # stopwords filter\n",
" stopword = (M > .2) or (\n",
" sum(map(lambda v: v > 0.09, lvs)) == ncats\n",
" )\n",
" if (stopword and sd <= .1) or (M >= .3):\n",
" return 0.\n",
"\n",
" if not sd and lv:\n",
" return 1.\n",
"\n",
" return sigmoid(lv - M, l * sd)\n",
" except TypeError:\n",
" return 0.\n",
"\n",
" def __gv__(self, ngram, icat, cache=True):\n",
" \"\"\"\n",
" The global value (gv) function.\n",
"\n",
" This is the original way of computing the confidence value (cv)\n",
" of a term.\n",
" \"\"\"\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[GV]\n",
" else:\n",
" lv = self.__lv__(ngram, icat)\n",
" weight = self.__sg__(ngram, icat) * self.__sn__(ngram, icat)\n",
" return lv * weight\n",
"\n",
" def __cv_norm_gv__(self, ngram, icat, cache=True):\n",
" \"\"\"\n",
" Alternative way of computing the confidence value (cv) of terms.\n",
"\n",
" This variations normalizes the gv value and uses that value as the cv.\n",
" \"\"\"\n",
" try:\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[CV]\n",
" else:\n",
" try:\n",
" cv = self.__gv__(ngram, icat)\n",
" return cv / self.__max_gv__[icat][len(ngram) - 1]\n",
" except (ZeroDivisionError, IndexError):\n",
" return .0\n",
"\n",
" except TypeError:\n",
" return 0\n",
"\n",
" def __cv_norm_gv_xai__(self, ngram, icat, cache=True):\n",
" \"\"\"\n",
" Alternative way of computing the confidence value (cv) of terms.\n",
"\n",
" This variations not only normalizes the gv value but also adds extra\n",
" checks to improve visual explanations.\n",
" \"\"\"\n",
" try:\n",
" if cache:\n",
" return self.__trie_node__(ngram, icat)[CV]\n",
" else:\n",
" try:\n",
" max_gv = self.__max_gv__[icat][len(ngram) - 1]\n",
" if (len(ngram) > 1):\n",
" # stopwords guard\n",
" n_cats = len(self.__categories__)\n",
" cats = xrange(n_cats)\n",
" sum_words_gv = sum([\n",
" self.__gv__([w], ic) for w in ngram for ic in cats\n",
" ])\n",
" if (sum_words_gv < .05):\n",
" return .0\n",
" elif len([\n",
" w for w in ngram\n",
" if self.__gv__([w], icat) >= .01\n",
" ]) == len(ngram):\n",
" gv = self.__gv__(ngram, icat)\n",
" return gv / max_gv + sum_words_gv\n",
" # return gv / max_gv * len(ngram)\n",
"\n",
" gv = self.__gv__(ngram, icat)\n",
" return gv / max_gv\n",
" except (ZeroDivisionError, IndexError):\n",
" return .0\n",
"\n",
" except TypeError:\n",
" return 0\n",
"\n",
" def __apply_fn__(self, fn, ngram, cat):\n",
" \"\"\"Private method used by gv, lv, sn, sg functions.\"\"\"\n",
" icat = self.get_category_index(cat)\n",
" if icat == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" if ngram.strip() == '':\n",
" return 0\n",
"\n",
" ngram = [self.get_word_index(w)\n",
" for w in re.split(self.__word_delimiter__, ngram)\n",
" if w]\n",
" return fn(ngram, icat) if IDX_UNKNOWN_WORD not in ngram else 0\n",
"\n",
" def __summary_ops_are_pristine__(self):\n",
" \"\"\"Return True if summary operators haven't changed.\"\"\"\n",
" return self.original_sumop_ngrams == self.summary_op_ngrams and \\\n",
" self.original_sumop_sentences == self.summary_op_sentences and \\\n",
" self.original_sumop_paragraphs == self.summary_op_paragraphs\n",
"\n",
" def __classify_ngram__(self, ngram):\n",
" \"\"\"Classify the given n-gram.\"\"\"\n",
" cv = [\n",
" self.__cv__(ngram, icat)\n",
" for icat in xrange(len(self.__categories__))\n",
" ]\n",
" cv[:] = [(v if v > self.__a__ else 0) for v in cv]\n",
" return cv\n",
"\n",
" def __classify_sentence__(self, sent, prep, json=False, prep_func=None):\n",
" \"\"\"Classify the given sentence.\"\"\"\n",
" classify_trans = self.__classify_ngram__\n",
" categories = self.__categories__\n",
" cats = xrange(len(categories))\n",
" word_index = self.get_word_index\n",
" word_delimiter = self.__word_delimiter__\n",
" word_regex = self.__word_regex__\n",
"\n",
" if not json:\n",
" if prep or prep_func is not None:\n",
" prep_func = prep_func or Pp.clean_and_ready\n",
" sent = prep_func(sent)\n",
" sent_words = [\n",
" (w, w)\n",
" for w in re_split_keep(word_regex, sent)\n",
" if w\n",
" ]\n",
" else:\n",
" if prep or prep_func is not None:\n",
" sent_words = [\n",
" (w, Pp.clean_and_ready(w, dots=False) if prep_func is None else prep_func(w))\n",
" for w in re_split_keep(word_regex, sent)\n",
" if w\n",
" ]\n",
" else:\n",
" sent_words = [\n",
" (w, w)\n",
" for w in re_split_keep(word_regex, sent)\n",
" if w\n",
" ]\n",
"\n",
" if not sent_words:\n",
" sent_words = [(u'.', u'.')]\n",
"\n",
" sent_iwords = [word_index(w) for _, w in sent_words]\n",
" sent_len = len(sent_iwords)\n",
" sent_parsed = []\n",
" wcur = 0\n",
" while wcur < sent_len:\n",
" cats_ngrams_cv = [[0] for icat in cats]\n",
" cats_ngrams_offset = [[0] for icat in cats]\n",
" cats_ngrams_iword = [[-1] for icat in cats]\n",
" cats_max_cv = [.0 for icat in cats]\n",
"\n",
" for icat in cats:\n",
" woffset = 0\n",
" word_raw = sent_words[wcur + woffset][0]\n",
" wordi = sent_iwords[wcur + woffset]\n",
" word_info = categories[icat][VOCAB]\n",
"\n",
" if wordi in word_info:\n",
" cats_ngrams_cv[icat][0] = word_info[wordi][CV]\n",
" word_info = word_info[wordi][NEXT]\n",
" cats_ngrams_iword[icat][0] = wordi\n",
" cats_ngrams_offset[icat][0] = woffset\n",
"\n",
" # if it is a learned word (not unknown and seen for this category),\n",
" # then try to recognize learned n-grams too\n",
" if wordi != IDX_UNKNOWN_WORD and wordi in categories[icat][VOCAB]:\n",
" # while word or word delimiter (e.g. space)\n",
" while wordi != IDX_UNKNOWN_WORD or re.match(word_delimiter, word_raw):\n",
" woffset += 1\n",
" if wcur + woffset >= sent_len:\n",
" break\n",
"\n",
" word_raw = sent_words[wcur + woffset][0]\n",
" wordi = sent_iwords[wcur + woffset]\n",
"\n",
" # if word is a word:\n",
" if wordi != IDX_UNKNOWN_WORD:\n",
" # if this word belongs to this category\n",
" if wordi in word_info:\n",
" cats_ngrams_cv[icat].append(word_info[wordi][CV])\n",
" cats_ngrams_iword[icat].append(wordi)\n",
" cats_ngrams_offset[icat].append(woffset)\n",
" word_info = word_info[wordi][NEXT]\n",
" else:\n",
" break\n",
"\n",
" cats_max_cv[icat] = (max(cats_ngrams_cv[icat])\n",
" if cats_ngrams_cv[icat] else .0)\n",
"\n",
" max_gv = max(cats_max_cv)\n",
" use_ngram = True\n",
" if (max_gv > self.__a__):\n",
" icat_max_gv = cats_max_cv.index(max_gv)\n",
" ngram_max_gv = cats_ngrams_cv[icat_max_gv].index(max_gv)\n",
" offset_max_gv = cats_ngrams_offset[icat_max_gv][ngram_max_gv] + 1\n",
"\n",
" max_gv_sum_1_grams = max([\n",
" sum([\n",
" (categories[ic][VOCAB][wi][CV]\n",
" if wi in categories[ic][VOCAB]\n",
" else 0)\n",
" for wi\n",
" in cats_ngrams_iword[ic]\n",
" ])\n",
" for ic in cats\n",
" ])\n",
"\n",
" if (max_gv_sum_1_grams > max_gv):\n",
" use_ngram = False\n",
" else:\n",
" use_ngram = False\n",
"\n",
" if not use_ngram:\n",
" offset_max_gv = 1\n",
" icat_max_gv = 0\n",
" ngram_max_gv = 0\n",
"\n",
" sent_parsed.append(\n",
" (\n",
" u\"\".join([raw_word for raw_word, _ in sent_words[wcur:wcur + offset_max_gv]]),\n",
" cats_ngrams_iword[icat_max_gv][:ngram_max_gv + 1]\n",
" )\n",
" )\n",
" wcur += offset_max_gv\n",
"\n",
" get_word = self.get_word\n",
" if not json:\n",
" words_cvs = [classify_trans(seq) for _, seq in sent_parsed]\n",
" if words_cvs:\n",
" return self.summary_op_ngrams(words_cvs)\n",
" return self.__zero_cv__\n",
" else:\n",
" get_tip = self.__trie_node__\n",
" local_value = self.__lv__\n",
" info = [\n",
" {\n",
" \"token\": u\"→\".join(map(get_word, sequence)),\n",
" \"lexeme\": raw_sequence,\n",
" \"cv\": classify_trans(sequence),\n",
" \"lv\": [local_value(sequence, ic) for ic in cats],\n",
" \"fr\": [get_tip(sequence, ic)[FR] for ic in cats]\n",
" }\n",
" for raw_sequence, sequence in sent_parsed\n",
" ]\n",
" return {\n",
" \"words\": info,\n",
" \"cv\": self.summary_op_ngrams([v[\"cv\"] for v in info]),\n",
" \"wmv\": reduce(vmax, [v[\"cv\"] for v in info]) # word max value\n",
" }\n",
"\n",
" def __classify_paragraph__(self, parag, prep, json=False, prep_func=None):\n",
" \"\"\"Classify the given paragraph.\"\"\"\n",
" if not json:\n",
" sents_cvs = [\n",
" self.__classify_sentence__(sent, prep=prep, prep_func=prep_func)\n",
" for sent in re.split(self.__sent_delimiter__, parag)\n",
" if sent\n",
" ]\n",
" if sents_cvs:\n",
" return self.summary_op_sentences(sents_cvs)\n",
" return self.__zero_cv__\n",
" else:\n",
" info = [\n",
" self.__classify_sentence__(sent, prep=prep, prep_func=prep_func, json=True)\n",
" for sent in re_split_keep(self.__sent_delimiter__, parag)\n",
" if sent\n",
" ]\n",
" if info:\n",
" sents_cvs = [v[\"cv\"] for v in info]\n",
" cv = self.summary_op_sentences(sents_cvs)\n",
" wmv = reduce(vmax, [v[\"wmv\"] for v in info])\n",
" else:\n",
" cv = self.__zero_cv__\n",
" wmv = cv\n",
" return {\n",
" \"sents\": info,\n",
" \"cv\": cv,\n",
" \"wmv\": wmv # word max value\n",
" }\n",
"\n",
" def __trie_node__(self, ngram, icat):\n",
" \"\"\"Get the trie's node for this n-gram.\"\"\"\n",
" try:\n",
" word_info = self.__categories__[icat][VOCAB][ngram[0]]\n",
" for word in ngram[1:]:\n",
" word_info = word_info[NEXT][word]\n",
" return word_info\n",
" except BaseException:\n",
" return EMPTY_WORD_INFO\n",
"\n",
" def __get_category__(self, name):\n",
" \"\"\"\n",
" Given the category name, return the category data.\n",
"\n",
" If category name doesn't exist, creates a new one.\n",
" \"\"\"\n",
" try:\n",
" return self.__categories_index__[name]\n",
" except KeyError:\n",
" self.__max_fr__.append([])\n",
" self.__max_gv__.append([])\n",
" self.__categories_index__[name] = len(self.__categories__)\n",
" self.__categories__.append([name, {}]) # name, vocabulary\n",
" self.__zero_cv__ = (0,) * len(self.__categories__)\n",
" return self.__categories_index__[name]\n",
"\n",
" def __get_category_length__(self, icat):\n",
" \"\"\"\n",
" Return the category length.\n",
"\n",
" The category length is the total number of words seen during training.\n",
" \"\"\"\n",
" size = 0\n",
" vocab = self.__categories__[icat][VOCAB]\n",
" for word in vocab:\n",
" size += vocab[word][FR]\n",
" return size\n",
"\n",
" def __get_most_probable_category__(self):\n",
" \"\"\"Return the index of the most probable category.\"\"\"\n",
" sizes = []\n",
" for icat in xrange(len(self.__categories__)):\n",
" sizes.append((icat, self.__get_category_length__(icat)))\n",
" return sorted(sizes, key=lambda v: v[1])[-1][0]\n",
"\n",
" def __get_vocabularies__(self, icat, vocab, preffix, n_grams, output):\n",
" \"\"\"Get category list of n-grams with info.\"\"\"\n",
" senq_ilen = len(preffix)\n",
" get_name = self.get_word\n",
"\n",
" seq = preffix + [None]\n",
" if len(seq) > n_grams:\n",
" return\n",
"\n",
" for word in vocab:\n",
" seq[-1] = word\n",
" if (self.__cv__(seq, icat) > 0):\n",
" output[senq_ilen].append(\n",
" (\n",
" \"_\".join([get_name(wi) for wi in seq]),\n",
" vocab[word][FR],\n",
" self.__gv__(seq, icat),\n",
" self.__cv__(seq, icat)\n",
" )\n",
" )\n",
" self.__get_vocabularies__(\n",
" icat, vocab[word][NEXT], seq, n_grams, output\n",
" )\n",
"\n",
" def __get_category_vocab__(self, icat):\n",
" \"\"\"Get category list of n-grams ordered by confidence value.\"\"\"\n",
" category = self.__categories__[icat]\n",
" vocab = category[VOCAB]\n",
" w_seqs = ([w] for w in vocab)\n",
"\n",
" vocab_icat = (\n",
" (\n",
" self.get_word(wseq[0]),\n",
" vocab[wseq[0]][FR],\n",
" self.__lv__(wseq, icat),\n",
" self.__gv__(wseq, icat),\n",
" self.__cv__(wseq, icat)\n",
" )\n",
" for wseq in w_seqs if self.__gv__(wseq, icat) > self.__a__\n",
" )\n",
" return sorted(vocab_icat, key=lambda k: -k[-1])\n",
"\n",
" def __get_def_cat__(self, def_cat):\n",
" \"\"\"Given the `def_cat` argument, get the default category value.\"\"\"\n",
" if def_cat is not None and (def_cat not in [STR_MOST_PROBABLE, STR_UNKNOWN] and\n",
" self.get_category_index(def_cat) == IDX_UNKNOWN_CATEGORY):\n",
" raise ValueError(\n",
" \"the default category must be 'most-probable', 'unknown', or a category name\"\n",
" \" (current value is '%s').\" % str(def_cat)\n",
" )\n",
" def_cat = None if def_cat == STR_UNKNOWN else def_cat\n",
" return self.get_most_probable_category() if def_cat == STR_MOST_PROBABLE else def_cat\n",
"\n",
" def __get_next_iwords__(self, sent, icat):\n",
" \"\"\"Return the list of possible following words' indexes.\"\"\"\n",
" if not self.get_category_name(icat):\n",
" return []\n",
"\n",
" vocab = self.__categories__[icat][VOCAB]\n",
" word_index = self.get_word_index\n",
" sent = Pp.clean_and_ready(sent)\n",
" sent = [\n",
" word_index(w)\n",
" for w in sent.strip(\".\").split(\".\")[-1].split(\" \") if w\n",
" ]\n",
"\n",
" tips = []\n",
" for word in sent:\n",
" if word is None:\n",
" tips[:] = []\n",
" continue\n",
"\n",
" tips.append(vocab)\n",
"\n",
" tips[:] = (\n",
" tip[word][NEXT]\n",
" for tip in tips if word in tip and tip[word][NEXT]\n",
" )\n",
"\n",
" if len(tips) == 0:\n",
" return []\n",
"\n",
" next_words = tips[0]\n",
" next_nbr_words = float(sum([next_words[w][FR] for w in next_words]))\n",
" return sorted(\n",
" [\n",
" (\n",
" word1,\n",
" next_words[word1][FR],\n",
" next_words[word1][FR] / next_nbr_words\n",
" )\n",
" for word1 in next_words\n",
" ],\n",
" key=lambda k: -k[1]\n",
" )\n",
"\n",
" def __prune_cat_trie__(self, vocab, prune=False, min_n=None):\n",
" \"\"\"Prune the trie of the given category.\"\"\"\n",
" prun_floor = min_n or self.__prun_floor__\n",
" remove = []\n",
" for word in vocab:\n",
" if prune and vocab[word][FR] <= prun_floor:\n",
" vocab[word][NEXT] = None\n",
" remove.append(word)\n",
" else:\n",
" self.__prune_cat_trie__(vocab[word][NEXT], prune=True)\n",
"\n",
" for word in remove:\n",
" del vocab[word]\n",
"\n",
" def __prune_tries__(self):\n",
" \"\"\"Prune the trie of every category.\"\"\"\n",
" Print.info(\"pruning tries...\", offset=1)\n",
" for category in self.__categories__:\n",
" self.__prune_cat_trie__(category[VOCAB])\n",
" self.__prun_counter__ = 0\n",
"\n",
" def __cache_lvs__(self, icat, vocab, preffix):\n",
" \"\"\"Cache all local values.\"\"\"\n",
" for word in vocab:\n",
" sequence = preffix + [word]\n",
" vocab[word][LV] = self.__lv__(sequence, icat, cache=False)\n",
" self.__cache_lvs__(icat, vocab[word][NEXT], sequence)\n",
"\n",
" def __cache_gvs__(self, icat, vocab, preffix):\n",
" \"\"\"Cache all global values.\"\"\"\n",
" for word in vocab:\n",
" sequence = preffix + [word]\n",
" vocab[word][GV] = self.__gv__(sequence, icat, cache=False)\n",
" self.__cache_gvs__(icat, vocab[word][NEXT], sequence)\n",
"\n",
" def __cache_sg__(self, icat, vocab, preffix):\n",
" \"\"\"Cache all significance weight values.\"\"\"\n",
" for word in vocab:\n",
" sequence = preffix + [word]\n",
" vocab[word][SG] = self.__sg__(sequence, icat, cache=False)\n",
" self.__cache_sg__(icat, vocab[word][NEXT], sequence)\n",
"\n",
" def __cache_cvs__(self, icat, vocab, preffix):\n",
" \"\"\"Cache all confidence values.\"\"\"\n",
" for word in vocab:\n",
" sequence = preffix + [word]\n",
" vocab[word][CV] = self.__cv__(sequence, icat, False)\n",
" self.__cache_cvs__(icat, vocab[word][NEXT], sequence)\n",
"\n",
" def __update_max_gvs__(self, icat, vocab, preffix):\n",
" \"\"\"Update all maximum global values.\"\"\"\n",
" gv = self.__gv__\n",
" max_gvs = self.__max_gv__[icat]\n",
" sentence_ilength = len(preffix)\n",
"\n",
" sequence = preffix + [None]\n",
" for word in vocab:\n",
" sequence[-1] = word\n",
" sequence_gv = gv(sequence, icat)\n",
" if sequence_gv > max_gvs[sentence_ilength]:\n",
" max_gvs[sentence_ilength] = sequence_gv\n",
" self.__update_max_gvs__(icat, vocab[word][NEXT], sequence)\n",
"\n",
" def __update_needed__(self):\n",
" \"\"\"Return True if an update is needed, false otherwise.\"\"\"\n",
" return (self.__s__ != self.__s_update__ or\n",
" self.__l__ != self.__l_update__ or\n",
" self.__p__ != self.__p_update__)\n",
"\n",
" def __save_cat_vocab__(self, icat, path, n_grams):\n",
" \"\"\"Save the category vocabulary inside ``path``.\"\"\"\n",
" if n_grams == -1:\n",
" n_grams = 20 # infinite\n",
"\n",
" category = self.__categories__[icat]\n",
" cat_name = self.get_category_name(icat)\n",
" vocab = category[VOCAB]\n",
" vocabularies_out = [[] for _ in xrange(n_grams)]\n",
"\n",
" terms = [\"words\", \"bigrams\", \"trigrams\"]\n",
"\n",
" self.__get_vocabularies__(icat, vocab, [], n_grams, vocabularies_out)\n",
"\n",
" Print.info(\"saving '%s' vocab\" % cat_name)\n",
"\n",
" for ilen in xrange(n_grams):\n",
" if vocabularies_out[ilen]:\n",
" term = terms[ilen] if ilen <= 2 else \"%d-grams\" % (ilen + 1)\n",
" voc_path = os.path.join(\n",
" path, \"ss3_vocab_%s(%s).csv\" % (cat_name, term)\n",
" )\n",
" f = open(voc_path, \"w+\", encoding=ENCODING)\n",
" vocabularies_out[ilen].sort(key=lambda k: -k[-1])\n",
" f.write(u\"%s,%s,%s,%s\\n\" % (\"term\", \"fr\", \"gv\", \"cv\"))\n",
" for trans in vocabularies_out[ilen]:\n",
" f.write(u\"%s,%d,%f,%f\\n\" % tuple(trans))\n",
" f.close()\n",
" Print.info(\"\\t[ %s stored in '%s'\" % (term, voc_path))\n",
"\n",
" def __update_cv_cache__(self):\n",
" \"\"\"Update numpy darray confidence values cache.\"\"\"\n",
" if self.__cv_cache__ is None:\n",
" self.__cv_cache__ = np.zeros((len(self.__index_to_word__), len(self.__categories__)))\n",
" cv = self.__cv__\n",
" for term_idx, cv_vec in enumerate(self.__cv_cache__):\n",
" for cat_idx, _ in enumerate(cv_vec):\n",
" try:\n",
" cv_vec[cat_idx] = cv([term_idx], cat_idx)\n",
" except KeyError:\n",
" cv_vec[cat_idx] = 0\n",
"\n",
" def __predict_fast__(\n",
" self, x_test, def_cat=STR_MOST_PROBABLE, labels=True,\n",
" multilabel=False, proba=False, prep=True, leave_pbar=True\n",
" ):\n",
" \"\"\"A faster version of the `predict` method (using numpy).\"\"\"\n",
" if not def_cat or def_cat == STR_UNKNOWN:\n",
" def_cat = IDX_UNKNOWN_CATEGORY\n",
" elif def_cat == STR_MOST_PROBABLE:\n",
" def_cat = self.__get_most_probable_category__()\n",
" else:\n",
" def_cat = self.get_category_index(def_cat)\n",
" if def_cat == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" # does the special \"[others]\" category exist? (only used in multilabel classification)\n",
" __other_idx__ = self.get_category_index(STR_OTHERS_CATEGORY)\n",
"\n",
" if self.__update_needed__():\n",
" self.update_values()\n",
"\n",
" if self.__cv_cache__ is None:\n",
" self.__update_cv_cache__()\n",
" self.__last_x_test__ = None # could have learned a new word (in `learn`)\n",
" cv_cache = self.__cv_cache__\n",
"\n",
" x_test_hash = list_hash(x_test)\n",
" if x_test_hash == self.__last_x_test__:\n",
" x_test_idx = self.__last_x_test_idx__\n",
" else:\n",
" self.__last_x_test__ = x_test_hash\n",
" self.__last_x_test_idx__ = [None] * len(x_test)\n",
" x_test_idx = self.__last_x_test_idx__\n",
" word_index = self.get_word_index\n",
" for doc_idx, doc in enumerate(tqdm(x_test, desc=\"Caching documents\",\n",
" leave=False, disable=Print.is_quiet())):\n",
" x_test_idx[doc_idx] = [\n",
" word_index(w)\n",
" for w\n",
" in re.split(self.__word_delimiter__, Pp.clean_and_ready(doc) if prep else doc)\n",
" if word_index(w) != IDX_UNKNOWN_WORD\n",
" ]\n",
"\n",
" y_pred = [None] * len(x_test)\n",
" for doc_idx, doc in enumerate(tqdm(x_test_idx, desc=\"Classification\",\n",
" leave=leave_pbar, disable=Print.is_quiet())):\n",
" if self.__a__ > 0:\n",
" doc_cvs = cv_cache[doc]\n",
" doc_cvs[doc_cvs <= self.__a__] = 0\n",
" pred_cv = np.add.reduce(doc_cvs, 0)\n",
" else:\n",
" pred_cv = np.add.reduce(cv_cache[doc], 0)\n",
"\n",
" if proba:\n",
" y_pred[doc_idx] = list(pred_cv)\n",
" continue\n",
"\n",
" if not multilabel:\n",
" if pred_cv.sum() == 0:\n",
" y_pred[doc_idx] = def_cat\n",
" else:\n",
" y_pred[doc_idx] = np.argmax(pred_cv)\n",
"\n",
" if labels:\n",
" if y_pred[doc_idx] != IDX_UNKNOWN_CATEGORY:\n",
" y_pred[doc_idx] = self.__categories__[y_pred[doc_idx]][NAME]\n",
" else:\n",
" y_pred[doc_idx] = STR_UNKNOWN_CATEGORY\n",
" else:\n",
" if pred_cv.sum() == 0:\n",
" if def_cat == IDX_UNKNOWN_CATEGORY:\n",
" y_pred[doc_idx] = []\n",
" else:\n",
" y_pred[doc_idx] = [self.get_category_name(def_cat) if labels else def_cat]\n",
" else:\n",
" r = sorted([(i, pred_cv[i])\n",
" for i in range(pred_cv.size)],\n",
" key=lambda e: -e[1])\n",
" if labels:\n",
" y_pred[doc_idx] = [self.get_category_name(cat_i)\n",
" for cat_i, _ in r[:kmean_multilabel_size(r)]]\n",
" else:\n",
" y_pred[doc_idx] = [cat_i for cat_i, _ in r[:kmean_multilabel_size(r)]]\n",
"\n",
" # if the special \"[others]\" category exists\n",
" if __other_idx__ != IDX_UNKNOWN_CATEGORY:\n",
" # if its among the predicted labels, remove (hide) it\n",
" if labels:\n",
" if STR_OTHERS_CATEGORY in y_pred[doc_idx]:\n",
" y_pred[doc_idx].remove(STR_OTHERS_CATEGORY)\n",
" else:\n",
" if __other_idx__ in y_pred[doc_idx]:\n",
" y_pred[doc_idx].remove(__other_idx__)\n",
"\n",
" return y_pred\n",
"\n",
" def summary_op_ngrams(self, cvs):\n",
" \"\"\"\n",
" Summary operator for n-gram confidence vectors.\n",
"\n",
" By default it returns the addition of all confidence\n",
" vectors. However, in case you want to use a custom\n",
" summary operator, this function must be replaced\n",
" as shown in the following example:\n",
"\n",
" >>> def my_summary_op(cvs):\n",
" >>> return cvs[0]\n",
" >>> ...\n",
" >>> clf = SS3()\n",
" >>> ...\n",
" >>> clf.summary_op_ngrams = my_summary_op\n",
"\n",
" Note that any function receiving a list of vectors and\n",
" returning a single vector could be used. In the above example\n",
" the summary operator is replaced by the user-defined\n",
" ``my_summary_op`` which ignores all confidence vectors\n",
" returning only the confidence vector of the first n-gram\n",
" (which besides being an illustrative example, makes no real sense).\n",
"\n",
" :param cvs: a list n-grams confidence vectors\n",
" :type cvs: list (of list of float)\n",
" :returns: a sentence confidence vector\n",
" :rtype: list (of float)\n",
" \"\"\"\n",
" return reduce(vsum, cvs)\n",
"\n",
" def summary_op_sentences(self, cvs):\n",
" \"\"\"\n",
" Summary operator for sentence confidence vectors.\n",
"\n",
" By default it returns the addition of all confidence\n",
" vectors. However, in case you want to use a custom\n",
" summary operator, this function must be replaced\n",
" as shown in the following example:\n",
"\n",
" >>> def dummy_summary_op(cvs):\n",
" >>> return cvs[0]\n",
" >>> ...\n",
" >>> clf = SS3()\n",
" >>> ...\n",
" >>> clf.summary_op_sentences = dummy_summary_op\n",
"\n",
" Note that any function receiving a list of vectors and\n",
" returning a single vector could be used. In the above example\n",
" the summary operator is replaced by the user-defined\n",
" ``dummy_summary_op`` which ignores all confidence vectors\n",
" returning only the confidence vector of the first sentence\n",
" (which besides being an illustrative example, makes no real sense).\n",
"\n",
" :param cvs: a list sentence confidence vectors\n",
" :type cvs: list (of list of float)\n",
" :returns: a paragraph confidence vector\n",
" :rtype: list (of float)\n",
" \"\"\"\n",
" return reduce(vsum, cvs)\n",
"\n",
" def summary_op_paragraphs(self, cvs):\n",
" \"\"\"\n",
" Summary operator for paragraph confidence vectors.\n",
"\n",
" By default it returns the addition of all confidence\n",
" vectors. However, in case you want to use a custom\n",
" summary operator, this function must be replaced\n",
" as shown in the following example:\n",
"\n",
" >>> def dummy_summary_op(cvs):\n",
" >>> return cvs[0]\n",
" >>> ...\n",
" >>> clf = SS3()\n",
" >>> ...\n",
" >>> clf.summary_op_paragraphs = dummy_summary_op\n",
"\n",
" Note that any function receiving a list of vectors and\n",
" returning a single vector could be used. In the above example\n",
" the summary operator is replaced by the user-defined\n",
" ``dummy_summary_op`` which ignores all confidence vectors\n",
" returning only the confidence vector of the first paragraph\n",
" (which besides being an illustrative example, makes no real sense).\n",
"\n",
" :param cvs: a list paragraph confidence vectors\n",
" :type cvs: list (of list of float)\n",
" :returns: the document confidence vector\n",
" :rtype: list (of float)\n",
" \"\"\"\n",
" return reduce(vsum, cvs)\n",
"\n",
" def get_name(self):\n",
" \"\"\"\n",
" Return the model's name.\n",
"\n",
" :returns: the model's name.\n",
" :rtype: str\n",
" \"\"\"\n",
" return self.__name__\n",
"\n",
" def set_name(self, name):\n",
" \"\"\"\n",
" Set the model's name.\n",
"\n",
" :param name: the model's name.\n",
" :type name: str\n",
" \"\"\"\n",
" self.__name__ = name\n",
"\n",
" def set_hyperparameters(self, s=None, l=None, p=None, a=None):\n",
" \"\"\"\n",
" Set hyperparameter values.\n",
"\n",
" :param s: the \"smoothness\" (sigma) hyperparameter\n",
" :type s: float\n",
" :param l: the \"significance\" (lambda) hyperparameter\n",
" :type l: float\n",
" :param p: the \"sanction\" (rho) hyperparameter\n",
" :type p: float\n",
" :param a: the alpha hyperparameter (i.e. all terms with a\n",
" confidence value (cv) less than alpha will be ignored during\n",
" classification)\n",
" :type a: float\n",
" \"\"\"\n",
" if s is not None:\n",
" self.set_s(s)\n",
" if l is not None:\n",
" self.set_l(l)\n",
" if p is not None:\n",
" self.set_p(p)\n",
" if a is not None:\n",
" self.set_a(a)\n",
"\n",
" def get_hyperparameters(self):\n",
" \"\"\"\n",
" Get hyperparameter values.\n",
"\n",
" :returns: a tuple with hyperparameters current values (s, l, p, a)\n",
" :rtype: tuple\n",
" \"\"\"\n",
" return self.__s__, self.__l__, self.__p__, self.__a__\n",
"\n",
" def set_model_path(self, path):\n",
" \"\"\"\n",
" Overwrite the default path from which the model will be loaded (or saved to).\n",
"\n",
" Note: be aware that the PySS3 Command Line tool looks for\n",
" a local folder called ``ss3_models`` to load models.\n",
" Therefore, the ``ss3_models`` folder will be always automatically\n",
" append to the given ``path`` (e.g. if ``path=\"my/path/\"``, it will\n",
" be converted into ``my/path/ss3_models``).\n",
"\n",
" :param path: the path\n",
" :type path: str\n",
" \"\"\"\n",
" self.__models_folder__ = os.path.join(path, STR_MODEL_FOLDER)\n",
"\n",
" def set_block_delimiters(self, parag=None, sent=None, word=None):\n",
" r\"\"\"Overwrite the default delimiters used to split input documents into blocks.\n",
"\n",
" delimiters are any regular expression from simple ones (e.g. ``\" \"``) to\n",
" more complex ones (e.g. ``r\"[^\\s\\w\\d]\"``).\n",
" Note: remember that there are certain reserved characters for regular expression,\n",
" for example, the dot (.), in which case use the backslash to indicate you're\n",
" referring the character itself and not its interpretation (e.g. ``\\.``)\n",
"\n",
" e.g.\n",
"\n",
" >>> ss3.set_block_delimiters(word=\"\\s\")\n",
" >>> ss3.set_block_delimiters(word=\"\\s\", parag=\"\\n\\n\")\n",
" >>> ss3.set_block_delimiters(parag=\"\\n---\\n\")\n",
" >>> ss3.set_block_delimiters(sent=\"\\.\")\n",
" >>> ss3.set_block_delimiters(word=\"\\|\")\n",
" >>> ss3.set_block_delimiters(word=\" \")\n",
"\n",
" :param parag: the paragraph new delimiter\n",
" :type parag: str\n",
" :param sent: the sentence new delimiter\n",
" :type sent: str\n",
" :param word: the word new delimiter\n",
" :type word: str\n",
" \"\"\"\n",
" if parag:\n",
" self.set_delimiter_paragraph(parag)\n",
" if sent:\n",
" self.set_delimiter_sentence(sent)\n",
" if word:\n",
" self.set_delimiter_word(word)\n",
"\n",
" def set_delimiter_paragraph(self, regex):\n",
" r\"\"\"\n",
" Set the delimiter used to split documents into paragraphs.\n",
"\n",
" Remember that there are certain reserved characters for regular expression,\n",
" for example, the dot (.), in which case use the backslash to indicate you're\n",
" referring the character itself and not its interpretation (e.g. ``\\.``)\n",
"\n",
" :param regex: the regular expression of the new delimiter\n",
" :type regex: str\n",
" \"\"\"\n",
" self.__parag_delimiter__ = regex\n",
"\n",
" def set_delimiter_sentence(self, regex):\n",
" r\"\"\"\n",
" Set the delimiter used to split documents into sentences.\n",
"\n",
" Remember that there are certain reserved characters for regular expression,\n",
" for example, the dot (.), in which case use the backslash to indicate you're\n",
" referring the character itself and not its interpretation (e.g. ``\\.``)\n",
"\n",
" :param regex: the regular expression of the new delimiter\n",
" :type regex: str\n",
" \"\"\"\n",
" self.__sent_delimiter__ = regex\n",
"\n",
" def set_delimiter_word(self, regex):\n",
" r\"\"\"\n",
" Set the delimiter used to split documents into words.\n",
"\n",
" Remember that there are certain reserved characters for regular expression,\n",
" for example, the dot (.), in which case use the backslash to indicate you're\n",
" referring the character itself and not its interpretation (e.g. ``\\.``)\n",
"\n",
" :param regex: the regular expression of the new delimiter\n",
" :type regex: str\n",
" \"\"\"\n",
" self.__word_delimiter__ = regex\n",
"\n",
" def set_s(self, value):\n",
" \"\"\"\n",
" Set the \"smoothness\" (sigma) hyperparameter value.\n",
"\n",
" :param value: the hyperparameter value\n",
" :type value: float\n",
" \"\"\"\n",
" self.__s__ = float(value)\n",
"\n",
" def get_s(self):\n",
" \"\"\"\n",
" Get the \"smoothness\" (sigma) hyperparameter value.\n",
"\n",
" :returns: the hyperparameter value\n",
" :rtype: float\n",
" \"\"\"\n",
" return self.__s__\n",
"\n",
" def set_l(self, value):\n",
" \"\"\"\n",
" Set the \"significance\" (lambda) hyperparameter value.\n",
"\n",
" :param value: the hyperparameter value\n",
" :type value: float\n",
" \"\"\"\n",
" self.__l__ = float(value)\n",
"\n",
" def get_l(self):\n",
" \"\"\"\n",
" Get the \"significance\" (lambda) hyperparameter value.\n",
"\n",
" :returns: the hyperparameter value\n",
" :rtype: float\n",
" \"\"\"\n",
" return self.__l__\n",
"\n",
" def set_p(self, value):\n",
" \"\"\"\n",
" Set the \"sanction\" (rho) hyperparameter value.\n",
"\n",
" :param value: the hyperparameter value\n",
" :type value: float\n",
" \"\"\"\n",
" self.__p__ = float(value)\n",
"\n",
" def get_p(self):\n",
" \"\"\"\n",
" Get the \"sanction\" (rho) hyperparameter value.\n",
"\n",
" :returns: the hyperparameter value\n",
" :rtype: float\n",
" \"\"\"\n",
" return self.__p__\n",
"\n",
" def set_a(self, value):\n",
" \"\"\"\n",
" Set the alpha hyperparameter value.\n",
"\n",
" All terms with a confidence value (cv) less than alpha\n",
" will be ignored during classification.\n",
"\n",
" :param value: the hyperparameter value\n",
" :type value: float\n",
" \"\"\"\n",
" self.__a__ = float(value)\n",
"\n",
" def get_a(self):\n",
" \"\"\"\n",
" Get the alpha hyperparameter value.\n",
"\n",
" :returns: the hyperparameter value\n",
" :rtype: float\n",
" \"\"\"\n",
" return self.__a__\n",
"\n",
" def get_categories(self, all=False):\n",
" \"\"\"\n",
" Get the list of category names.\n",
"\n",
" :returns: the list of category names\n",
" :rtype: list (of str)\n",
" \"\"\"\n",
" return [\n",
" self.get_category_name(ci)\n",
" for ci in range(len(self.__categories__))\n",
" if all or self.get_category_name(ci) != STR_OTHERS_CATEGORY\n",
" ]\n",
"\n",
" def get_most_probable_category(self):\n",
" \"\"\"\n",
" Get the name of the most probable category.\n",
"\n",
" :returns: the name of the most probable category\n",
" :rtype: str\n",
" \"\"\"\n",
" return self.get_category_name(self.__get_most_probable_category__())\n",
"\n",
" def get_ngrams_length(self):\n",
" \"\"\"\n",
" Return the length of longest learned n-gram.\n",
"\n",
" :returns: the length of longest learned n-gram.\n",
" :rtype: int\n",
" \"\"\"\n",
" return len(self.__max_fr__[0]) if len(self.__max_fr__) > 0 else 0\n",
"\n",
" def get_category_index(self, name):\n",
" \"\"\"\n",
" Given its name, return the category index.\n",
"\n",
" :param name: The category name\n",
" :type name: str\n",
" :returns: the category index (or ``IDX_UNKNOWN_CATEGORY``\n",
" if the category doesn't exist).\n",
" :rtype: int\n",
" \"\"\"\n",
" try:\n",
" return self.__categories_index__[name]\n",
" except KeyError:\n",
" return IDX_UNKNOWN_CATEGORY\n",
"\n",
" def get_category_name(self, index):\n",
" \"\"\"\n",
" Given its index, return the category name.\n",
"\n",
" :param index: The category index\n",
" :type index: int\n",
" :returns: the category name (or ``STR_UNKNOWN_CATEGORY``\n",
" if the category doesn't exist).\n",
" :rtype: str\n",
" \"\"\"\n",
" try:\n",
" if isinstance(index, list):\n",
" index = index[0]\n",
" return self.__categories__[index][NAME]\n",
" except IndexError:\n",
" return STR_UNKNOWN_CATEGORY\n",
"\n",
" def get_word_index(self, word):\n",
" \"\"\"\n",
" Given a word, return its index.\n",
"\n",
" :param name: a word\n",
" :type name: str\n",
" :returns: the word index (or ``IDX_UNKNOWN_WORD`` if the word doesn't exist).\n",
" :rtype: int\n",
" \"\"\"\n",
" try:\n",
" return self.__word_to_index__[word]\n",
" except KeyError:\n",
" return IDX_UNKNOWN_WORD\n",
"\n",
" def get_word(self, index):\n",
" \"\"\"\n",
" Given the index, return the word.\n",
"\n",
" :param index: the word index\n",
" :type index: int\n",
" :returns: the word (or ``STR_UNKNOWN_WORD`` if the word doesn't exist).\n",
" :rtype: int\n",
" :rtype: str\n",
" \"\"\"\n",
" return (\n",
" self.__index_to_word__[index]\n",
" if index in self.__index_to_word__ else STR_UNKNOWN_WORD\n",
" )\n",
"\n",
" def get_next_words(self, sent, cat, n=None):\n",
" \"\"\"\n",
" Given a sentence, return the list of ``n`` (possible) following words.\n",
"\n",
" :param sent: a sentence (e.g. \"an artificial\")\n",
" :type sent: str\n",
" :param cat: the category name\n",
" :type cat: str\n",
" :param n: the maximum number of possible answers\n",
" :type n: int\n",
" :returns: a list of tuples (word, frequency, probability)\n",
" :rtype: list (of tuple)\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" icat = self.get_category_index(cat)\n",
"\n",
" if icat == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" guessedwords = [\n",
" (self.get_word(iword), fr, P)\n",
" for iword, fr, P in self.__get_next_iwords__(sent, icat) if fr\n",
" ]\n",
" if n is not None and guessedwords:\n",
" return guessedwords[:n]\n",
" return guessedwords\n",
"\n",
" def get_stopwords(self, sg_threshold=.01):\n",
" \"\"\"\n",
" Get the list of (recognized) stopwords.\n",
"\n",
" :param sg_threshold: significance (sg) value used as a threshold to\n",
" consider words as stopwords (i.e. words with\n",
" sg < ``sg_threshold`` for all categories will\n",
" be considered as \"stopwords\")\n",
" :type sg_threshold: float\n",
" :returns: a list of stopwords\n",
" :rtype: list (of str)\n",
" \"\"\"\n",
" if not self.__categories__:\n",
" return\n",
"\n",
" iwords = self.__index_to_word__\n",
" sg_threshold = float(sg_threshold or .01)\n",
" categories = self.__categories__\n",
" cats_len = len(categories)\n",
" sg = self.__sg__\n",
" stopwords = []\n",
" vocab = categories[0][VOCAB]\n",
"\n",
" for word0 in iwords:\n",
" word_sg = [\n",
" sg([word0], c_i)\n",
" for c_i in xrange(cats_len)\n",
" ]\n",
" word_cats_len = len([v for v in word_sg if v < sg_threshold])\n",
" if word_cats_len == cats_len:\n",
" stopwords.append(word0)\n",
"\n",
" stopwords = [\n",
" iwords[w0]\n",
" for w0, v\n",
" in sorted(\n",
" [\n",
" (w0, vocab[w0][FR] if w0 in vocab else 0)\n",
" for w0 in stopwords\n",
" ],\n",
" key=lambda k: -k[1]\n",
" )\n",
" ]\n",
"\n",
" return stopwords\n",
"\n",
" def save_model(self, path=None):\n",
" \"\"\"\n",
" Save the model to disk.\n",
"\n",
" if a ``path`` is not present, the default will be used (\"./\"),\n",
" However, if a ``path`` is given, it will not only used to save\n",
" the model but also will overwrite the default path calling the\n",
" ``SS3``'s ``set_model_path(path)`` method (see ``set_model_path``\n",
" method documentation for more detail).\n",
"\n",
" :param path: the path to save the model to\n",
" :type path: str\n",
"\n",
" :raises: OSError\n",
" \"\"\"\n",
" if path:\n",
" self.set_model_path(path)\n",
"\n",
" stime = time()\n",
" Print.info(\n",
" \"saving model (%s/%s.%s)...\"\n",
" %\n",
" (self.__models_folder__, self.__name__, STR_MODEL_EXT),\n",
" False\n",
" )\n",
" json_file_format = {\n",
" \"__a__\": self.__a__,\n",
" \"__l__\": self.__l__,\n",
" \"__p__\": self.__p__,\n",
" \"__s__\": self.__s__,\n",
" \"__max_fr__\": self.__max_fr__,\n",
" \"__max_gv__\": self.__max_gv__,\n",
" \"__categories__\": self.__categories__,\n",
" \"__categories_index__\": self.__categories_index__,\n",
" \"__index_to_word__\": self.__index_to_word__,\n",
" \"__word_to_index__\": self.__word_to_index__,\n",
" \"__cv_mode__\": self.__cv_mode__,\n",
" \"__sg_mode__\": self.__sg_mode__,\n",
" \"__multilabel__\": self.__multilabel__\n",
" }\n",
"\n",
" try:\n",
" os.makedirs(self.__models_folder__)\n",
" except OSError as ose:\n",
" if ose.errno == errno.EEXIST and os.path.isdir(self.__models_folder__):\n",
" pass\n",
" else:\n",
" raise\n",
"\n",
" json_file = open(\n",
" \"%s/%s.%s\" % (\n",
" self.__models_folder__,\n",
" self.__name__,\n",
" STR_MODEL_EXT\n",
" ), \"w\", encoding=ENCODING\n",
" )\n",
"\n",
" try: # python 3\n",
" json_file.write(json.dumps(json_file_format))\n",
" except TypeError: # python 2\n",
" json_file.write(json.dumps(json_file_format).decode(ENCODING))\n",
"\n",
" json_file.close()\n",
" Print.info(\"(%.1fs)\" % (time() - stime))\n",
"\n",
" def load_model(self, path=None):\n",
" \"\"\"\n",
" Load model from disk.\n",
"\n",
" if a ``path`` is not present, the default will be used (\"./\"),\n",
" However, if a ``path`` is given, it will not only used to load\n",
" the model but also will overwrite the default path calling the\n",
" ``SS3``'s ``set_model_path(path)`` method (see ``set_model_path``\n",
" method documentation for more detail).\n",
"\n",
" :param path: the path to load the model from\n",
" :type path: str\n",
"\n",
" :raises: OSError\n",
" \"\"\"\n",
" if path:\n",
" self.set_model_path(path)\n",
"\n",
" stime = time()\n",
" Print.info(\"loading '%s' model from disk...\" % self.__name__)\n",
"\n",
" json_file = open(\n",
" \"%s/%s.%s\" % (\n",
" self.__models_folder__,\n",
" self.__name__,\n",
" STR_MODEL_EXT\n",
" ), \"r\", encoding=ENCODING\n",
" )\n",
" jmodel = json.loads(json_file.read(), object_hook=key_as_int)\n",
" json_file.close()\n",
"\n",
" self.__max_fr__ = jmodel[\"__max_fr__\"]\n",
" self.__max_gv__ = jmodel[\"__max_gv__\"]\n",
" self.__l__ = jmodel[\"__l__\"]\n",
" self.__p__ = jmodel[\"__p__\"]\n",
" self.__s__ = jmodel[\"__s__\"]\n",
" self.__a__ = jmodel[\"__a__\"]\n",
" self.__categories__ = jmodel[\"__categories__\"]\n",
" self.__categories_index__ = jmodel[\"__categories_index__\"]\n",
" self.__index_to_word__ = jmodel[\"__index_to_word__\"]\n",
" self.__word_to_index__ = jmodel[\"__word_to_index__\"]\n",
" self.__cv_mode__ = jmodel[\"__cv_mode__\"]\n",
" self.__multilabel__ = jmodel[\"__multilabel__\"] if \"__multilabel__\" in jmodel else False\n",
" self.__sg_mode__ = (jmodel[\"__sg_mode__\"]\n",
" if \"__sg_mode__\" in jmodel\n",
" else jmodel[\"__sn_mode__\"])\n",
"\n",
" self.__zero_cv__ = (0,) * len(self.__categories__)\n",
" self.__s_update__ = self.__s__\n",
" self.__l_update__ = self.__l__\n",
" self.__p_update__ = self.__p__\n",
"\n",
" Print.info(\"(%.1fs)\" % (time() - stime))\n",
"\n",
" def save_cat_vocab(self, cat, path=\"./\", n_grams=-1):\n",
" \"\"\"\n",
" Save category vocabulary to disk.\n",
"\n",
" :param cat: the category name\n",
" :type cat: str\n",
" :param path: the path in which to store the vocabulary\n",
" :type path: str\n",
" :param n_grams: indicates the n-grams to be stored (e.g. only 1-grams,\n",
" 2-grams, 3-grams, etc.). Default -1 stores all\n",
" learned n-grams (1-grams, 2-grams, 3-grams, etc.)\n",
" :type n_grams: int\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" if self.get_category_index(cat) == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" self.__save_cat_vocab__(self.get_category_index(cat), path, n_grams)\n",
"\n",
" def save_vocab(self, path=\"./\", n_grams=-1):\n",
" \"\"\"\n",
" Save learned vocabularies to disk.\n",
"\n",
" :param path: the path in which to store the vocabularies\n",
" :type path: str\n",
" :param n_grams: indicates the n-grams to be stored (e.g. only 1-grams,\n",
" 2-grams, 3-grams, etc.). Default -1 stores all\n",
" learned n-grams (1-grams, 2-grams, 3-grams, etc.)\n",
" :type n_grams: int\n",
" \"\"\"\n",
" for icat in xrange(len(self.__categories__)):\n",
" self.__save_cat_vocab__(icat, path, n_grams)\n",
"\n",
" def update_values(self, force=False):\n",
" \"\"\"\n",
" Update model values (cv, gv, lv, etc.).\n",
"\n",
" :param force: force update (even if hyperparameters haven't changed)\n",
" :type force: bool\n",
" \"\"\"\n",
" update = 0\n",
" if force or self.__s_update__ != self.__s__:\n",
" update = 3\n",
" elif self.__l_update__ != self.__l__:\n",
" update = 2\n",
" elif self.__p_update__ != self.__p__:\n",
" update = 1\n",
"\n",
" if update == 0:\n",
" Print.info(\"nothing to update...\", offset=1)\n",
" return\n",
"\n",
" category_len = len(self.__categories__)\n",
" categories = xrange(category_len)\n",
" category_names = [self.get_category_name(ic) for ic in categories]\n",
" stime = time()\n",
" Print.info(\"about to start updating values...\", offset=1)\n",
" if update == 3: # only if s has changed\n",
" Print.info(\"caching lv values\", offset=1)\n",
" for icat in categories:\n",
" Print.info(\n",
" \"lv values for %d (%s)\" % (icat, category_names[icat]),\n",
" offset=4\n",
" )\n",
" self.__cache_lvs__(icat, self.__categories__[icat][VOCAB], [])\n",
"\n",
" if update >= 2: # only if s or l have changed\n",
" Print.info(\"caching sg values\", offset=1)\n",
" for icat in categories:\n",
" Print.info(\n",
" \"sg values for %d (%s)\" % (icat, category_names[icat]),\n",
" offset=4\n",
" )\n",
" self.__cache_sg__(icat, self.__categories__[icat][VOCAB], [])\n",
"\n",
" Print.info(\"caching gv values\")\n",
" for icat in categories:\n",
" Print.info(\n",
" \"gv values for %d (%s)\" % (icat, category_names[icat]),\n",
" offset=4\n",
" )\n",
" self.__cache_gvs__(icat, self.__categories__[icat][VOCAB], [])\n",
"\n",
" if self.__cv_mode__ != STR_GV:\n",
" Print.info(\"updating max gv values\", offset=1)\n",
" for icat in categories:\n",
" Print.info(\n",
" \"max gv values for %d (%s)\" % (icat, category_names[icat]),\n",
" offset=4\n",
" )\n",
" self.__max_gv__[icat] = list(\n",
" map(lambda _: 0, self.__max_gv__[icat])\n",
" )\n",
" self.__update_max_gvs__(\n",
" icat, self.__categories__[icat][VOCAB], []\n",
" )\n",
"\n",
" Print.info(\"max gv values have been updated\", offset=1)\n",
"\n",
" Print.info(\"caching confidence values (cvs)\", offset=1)\n",
" for icat in categories:\n",
" Print.info(\n",
" \"cvs for %d (%s)\" % (icat, category_names[icat]),\n",
" offset=4\n",
" )\n",
" self.__cache_cvs__(icat, self.__categories__[icat][VOCAB], [])\n",
" Print.info(\"finished --time: %.1fs\" % (time() - stime), offset=1)\n",
"\n",
" self.__s_update__ = self.__s__\n",
" self.__l_update__ = self.__l__\n",
" self.__p_update__ = self.__p__\n",
"\n",
" if self.__cv_cache__ is not None:\n",
" self.__update_cv_cache__()\n",
"\n",
" def print_model_info(self):\n",
" \"\"\"Print information regarding the model.\"\"\"\n",
" print()\n",
" print(\" %s: %s\\n\" % (\n",
" Print.style.green(Print.style.ubold(\"NAME\")),\n",
" Print.style.warning(self.get_name())\n",
" ))\n",
"\n",
" def print_hyperparameters_info(self):\n",
" \"\"\"Print information about hyperparameters.\"\"\"\n",
" print()\n",
" print(\n",
" \" %s:\\n\" % Print.style.green(Print.style.ubold(\"HYPERPARAMETERS\"))\n",
" )\n",
" print(\"\\tSmoothness(s):\", Print.style.warning(self.__s__))\n",
" print(\"\\tSignificance(l):\", Print.style.warning(self.__l__))\n",
" print(\"\\tSanction(p):\", Print.style.warning(self.__p__))\n",
" print(\"\")\n",
" print(\"\\tAlpha(a):\", Print.style.warning(self.__a__))\n",
"\n",
" def print_categories_info(self):\n",
" \"\"\"Print information about learned categories.\"\"\"\n",
" if not self.__categories__:\n",
" print(\n",
" \"\\n %s: None\\n\"\n",
" % Print.style.green(Print.style.ubold(\"CATEGORIES\"))\n",
" )\n",
" return\n",
"\n",
" cat_len = max([\n",
" len(self.get_category_name(ic))\n",
" for ic in xrange(len(self.__categories__))\n",
" ])\n",
" cat_len = max(cat_len, 8)\n",
" row_template = Print.style.warning(\"\\t{:^%d} \" % cat_len)\n",
" row_template += \"| {:^5} | {:^10} | {:^11} | {:^13} | {:^6} |\"\n",
" print()\n",
" print(\"\\n %s:\\n\" % Print.style.green(Print.style.ubold(\"CATEGORIES\")))\n",
" print(\n",
" row_template\n",
" .format(\n",
" \"Category\", \"Index\", \"Length\",\n",
" \"Vocab. Size\", \"Word Max. Fr.\", \"N-gram\"\n",
" )\n",
" )\n",
" print(\n",
" (\n",
" \"\\t{:-<%d}-|-{:-<5}-|-{:-<10}-|-{:-<11}-|-{:-<13}-|-{:-<6}-|\"\n",
" % cat_len\n",
" )\n",
" .format('', '', '', '', '', '')\n",
" )\n",
"\n",
" mpci = self.__get_most_probable_category__()\n",
" mpc_size = 0\n",
" mpc_total = 0\n",
" for icat, category in enumerate(self.__categories__):\n",
" icat_size = self.__get_category_length__(icat)\n",
" \"\"\"print(\n",
" row_template\n",
" .format(\n",
" category[NAME],\n",
" icat, icat_size,\n",
" len(category[VOCAB]),\n",
" self.__max_fr__[icat][0],\n",
" len(self.__max_fr__[icat])\n",
" )\n",
" )\"\"\"\n",
" print(category[NAME], len(category[VOCAB]))\n",
"\n",
" mpc_total += icat_size\n",
" if icat == mpci:\n",
" mpc_size = icat_size\n",
"\n",
" print(\n",
" \"\\n\\t%s: %s %s\"\n",
" %\n",
" (\n",
" Print.style.ubold(\"Most Probable Category\"),\n",
" Print.style.warning(self.get_category_name(mpci)),\n",
" Print.style.blue(\"(%.2f%%)\" % (100.0 * mpc_size / mpc_total))\n",
" )\n",
" )\n",
" print()\n",
"\n",
" def print_ngram_info(self, ngram):\n",
" \"\"\"\n",
" Print debugging information about a given n-gram.\n",
"\n",
" Namely, print the n-gram frequency (fr), local value (lv),\n",
" global value (gv), confidence value (cv), sanction (sn) weight,\n",
" significance (sg) weight.\n",
"\n",
" :param ngram: the n-gram (e.g. \"machine\", \"machine learning\", etc.)\n",
" :type ngram: str\n",
" \"\"\"\n",
" if not self.__categories__:\n",
" return\n",
"\n",
" word_index = self.get_word_index\n",
" n_gram_str = ngram\n",
" ngram = [word_index(w)\n",
" for w in re.split(self.__word_delimiter__, ngram)\n",
" if w]\n",
"\n",
" print()\n",
" print(\n",
" \" %s: %s (%s)\" % (\n",
" Print.style.green(\n",
" \"%d-GRAM\" % len(ngram) if len(ngram) > 1 else \"WORD\"\n",
" ),\n",
" Print.style.warning(n_gram_str),\n",
" \"is unknown\"\n",
" if IDX_UNKNOWN_WORD in ngram\n",
" else \"index: \" + str(ngram if len(ngram) > 1 else ngram[0])\n",
" )\n",
" )\n",
"\n",
" if IDX_UNKNOWN_WORD in ngram:\n",
" print()\n",
" return\n",
"\n",
" cat_len = max([\n",
" len(self.get_category_name(ic))\n",
" for ic in xrange(len(self.__categories__))\n",
" ])\n",
" cat_len = max(cat_len, 8)\n",
" header_template = Print.style.bold(\n",
" \" {:<%d} | fr | lv | sg | sn | gv | cv |\"\n",
" % cat_len\n",
" )\n",
" print()\n",
" print(header_template.format(\"Category\"))\n",
" header_template = (\n",
" \" {:-<%d}-|----------|-------|-------|-------|-------|-------|\"\n",
" % cat_len\n",
" )\n",
" print(header_template.format(''))\n",
" row_template = (\n",
" \" %s | {:^8} | {:.3f} | {:.3f} | {:.3f} | {:.3f} | {:.3f} |\"\n",
" % (Print.style.warning(\"{:<%d}\" % cat_len))\n",
" )\n",
" for icat in xrange(len(self.__categories__)):\n",
" n_gram_tip = self.__trie_node__(ngram, icat)\n",
" if n_gram_tip:\n",
" print(\n",
" row_template\n",
" .format(\n",
" self.get_category_name(icat)[:16],\n",
" n_gram_tip[FR],\n",
" self.__lv__(ngram, icat),\n",
" self.__sg__(ngram, icat),\n",
" self.__sn__(ngram, icat),\n",
" self.__gv__(ngram, icat),\n",
" self.__cv__(ngram, icat),\n",
" )\n",
" )\n",
" print()\n",
"\n",
" def plot_value_distribution(self, cat):\n",
" \"\"\"\n",
" Plot the category's global and local value distribution.\n",
"\n",
" :param cat: the category name\n",
" :type cat: str\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" if self.get_category_index(cat) == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" import matplotlib.pyplot as plt\n",
"\n",
" icat = self.get_category_index(cat)\n",
" vocab_metrics = self.__get_category_vocab__(icat)\n",
"\n",
" x = []\n",
" y_lv = []\n",
" y_gv = []\n",
" vocab_metrics_len = len(vocab_metrics)\n",
"\n",
" for i in xrange(vocab_metrics_len):\n",
" metric = vocab_metrics[i]\n",
" x.append(i + 1)\n",
" y_lv.append(metric[2])\n",
" y_gv.append(metric[3])\n",
"\n",
" plt.figure(figsize=(20, 10))\n",
" plt.title(\n",
" \"Word Value Distribution (%s)\" % self.get_category_name(icat)\n",
" )\n",
"\n",
" plt.xlabel(\"Word Rank\")\n",
" plt.ylabel(\"Value\")\n",
" plt.xlim(right=max(x))\n",
"\n",
" plt.plot(\n",
" x, y_lv, \"-\", label=\"local value ($lv$)\",\n",
" linewidth=2, color=\"#7f7d7e\"\n",
" )\n",
" plt.plot(\n",
" x, y_gv, \"g-\", label=\"global value ($gv$)\",\n",
" linewidth=4, color=\"#2ca02c\")\n",
" plt.legend()\n",
"\n",
" plt.show()\n",
"\n",
" def extract_insight(\n",
" self, doc, cat='auto', level='word', window_size=3, min_cv=0.01, sort=True\n",
" ):\n",
" \"\"\"\n",
" Get the list of text blocks involved in the classification decision.\n",
"\n",
" Given a document, return the pieces of text that were involved in the\n",
" classification decision, along with the confidence values associated\n",
" with them. If a category is given, perform the process as if the\n",
" given category were the one assigned by the classifier.\n",
"\n",
" :param doc: the content of the document\n",
" :type doc: str\n",
" :param cat: the category in relation to which text blocks are obtained.\n",
" If not present, it will automatically use the category assigned\n",
" by SS3 after classification.\n",
" Options are 'auto' or a given category name. (default: 'auto')\n",
" :type cat: str\n",
" :param level: the level at which text blocks are going to be extracted.\n",
" Options are 'word', 'sentence' or 'paragraph'. (default: 'word')\n",
" :type level: str\n",
" :param window_size: the number of words, before and after each identified word,\n",
" to be also included along with the identified word. For instance,\n",
" ``window_size=0`` means return only individual words,\n",
" ``window_size=1`` means also include the word that was\n",
" before and the one that was after them. If multiple selected\n",
" words are close enough for their word windows to be overlapping,\n",
" then those word windows will be merged into a longer and single one.\n",
" This argument is ignored when ``level`` is not equal to 'word'.\n",
" (default: 3)\n",
" :type window_size: int\n",
" :param min_cv: the minimum confidence value each text block must have to be\n",
" included in the output. (default 0.01)\n",
" :type min_cv: float\n",
" :param sort: whether to return the text blocks ordered by their confidence value\n",
" or not. If ``sort=False`` then blocks will be returned\n",
" following the order they had in the input document. (default: True)\n",
" :type sort: bool\n",
" :returns: a list of pairs (text, confidence value) containing the text (blocks) involved,\n",
" and to what degree (*), in the classification decision.\n",
" (*) given by the confidence value\n",
" :rtype: list\n",
" :raises: InvalidCategoryError, ValueError\n",
" \"\"\"\n",
" r = self.classify(doc, json=True)\n",
" word_regex = self.__word_regex__\n",
"\n",
" if cat == 'auto':\n",
" c_i = r[\"cvns\"][0][0]\n",
" else:\n",
" c_i = self.get_category_index(cat)\n",
" if c_i == IDX_UNKNOWN_CATEGORY:\n",
" Print.error(\n",
" \"The excepted values for the `cat` argument are 'auto' \"\n",
" \"or a valid category name, found '%s' instead\" % str(cat),\n",
" raises=InvalidCategoryError\n",
" )\n",
"\n",
" if level == 'paragraph':\n",
" insights = [\n",
" (\n",
" \"\".join([word[\"lexeme\"]\n",
" for s in p[\"sents\"]\n",
" for word in s[\"words\"]]),\n",
" p[\"cv\"][c_i]\n",
" )\n",
" for p in r[\"pars\"]\n",
" if p[\"cv\"][c_i] > min_cv\n",
" ]\n",
" elif level == 'sentence':\n",
" insights = [\n",
" (\n",
" \"\".join([word[\"lexeme\"]\n",
" for word in s[\"words\"]]),\n",
" s[\"cv\"][c_i]\n",
" )\n",
" for p in r[\"pars\"] for s in p[\"sents\"]\n",
" if s[\"cv\"][c_i] > min_cv\n",
" ]\n",
" elif level == 'word':\n",
" ww_size = window_size\n",
" insights = []\n",
" for p in r[\"pars\"]:\n",
" words = [w for s in p[\"sents\"] for w in s[\"words\"]]\n",
" w_i = 0\n",
" while w_i < len(words):\n",
" w = words[w_i]\n",
" if w[\"cv\"][c_i] > min_cv:\n",
" ww = []\n",
" ww_cv = 0\n",
" ww_left = min(w_i, ww_size) + 1\n",
" w_i -= ww_left - 1\n",
" while ww_left > 0 and w_i < len(words):\n",
"\n",
" ww.append(words[w_i][\"lexeme\"])\n",
" ww_cv += words[w_i][\"cv\"][c_i]\n",
"\n",
" if words[w_i][\"cv\"][c_i] > min_cv:\n",
" ww_left += min(ww_size, (len(words) - 1) - w_i)\n",
"\n",
" if re.search(word_regex, words[w_i][\"lexeme\"]):\n",
" ww_left -= 1\n",
"\n",
" w_i += 1\n",
"\n",
" insights.append((\"\".join(ww), ww_cv))\n",
" else:\n",
" w_i += 1\n",
" else:\n",
" raise ValueError(\n",
" \"expected values for the `level` argument are \"\n",
" \"'word', 'sentence', or 'paragraph', found '%s' instead.\"\n",
" % str(level)\n",
" )\n",
"\n",
" if sort:\n",
" insights.sort(key=lambda b_cv: -b_cv[1])\n",
" return insights\n",
"\n",
" def learn(self, doc, cat, n_grams=1, prep=True, update=True):\n",
" \"\"\"\n",
" Learn a new document for a given category.\n",
"\n",
" :param doc: the content of the document\n",
" :type doc: str\n",
" :param cat: the category name\n",
" :type cat: str\n",
" :param n_grams: indicates the maximum ``n``-grams to be learned\n",
" (e.g. a value of ``1`` means only 1-grams (words),\n",
" ``2`` means 1-grams and 2-grams,\n",
" ``3``, 1-grams, 2-grams and 3-grams, and so on.\n",
" :type n_grams: int\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :param update: enables model auto-update after learning (default: True)\n",
" :type update: bool\n",
" \"\"\"\n",
" self.__cv_cache__ = None\n",
"\n",
" if not doc or cat is None:\n",
" return\n",
"\n",
" try:\n",
" doc = doc.decode(ENCODING)\n",
" except UnicodeEncodeError: # for python 2 compatibility\n",
" doc = doc.encode(ENCODING).decode(ENCODING)\n",
" except AttributeError:\n",
" pass\n",
"\n",
" icat = self.__get_category__(cat)\n",
" cat = self.__categories__[icat]\n",
" word_to_index = self.__word_to_index__\n",
" word_regex = self.__word_regex__\n",
"\n",
" if prep:\n",
" Print.info(\"preprocessing document...\", offset=1)\n",
" stime = time()\n",
" doc = Pp.clean_and_ready(doc)\n",
" Print.info(\"finished --time: %.1fs\" % (time() - stime), offset=1)\n",
" doc = re.findall(\"%s|[^%s]+\" % (word_regex, self.__word_delimiter__), doc)\n",
"\n",
" text_len = len(doc)\n",
" Print.info(\n",
" \"about to learn new document (%d terms)\" % text_len, offset=1\n",
" )\n",
"\n",
" vocab = cat[VOCAB] # getting cat vocab\n",
"\n",
" index_to_word = self.__index_to_word__\n",
" max_frs = self.__max_fr__[icat]\n",
" max_gvs = self.__max_gv__[icat]\n",
"\n",
" stime = time()\n",
" Print.info(\"learning...\", offset=1)\n",
" tips = []\n",
" for word in doc:\n",
" if re.match(word_regex, word):\n",
" self.__prun_counter__ += 1\n",
" # if word doesn't exist yet, then...\n",
" try:\n",
" word = word_to_index[word]\n",
" except KeyError:\n",
" new_index = len(word_to_index)\n",
" word_to_index[word] = new_index\n",
" index_to_word[new_index] = word\n",
" word = new_index\n",
"\n",
" tips.append(vocab)\n",
"\n",
" if len(tips) > n_grams:\n",
" del tips[0]\n",
"\n",
" tips_length = len(tips)\n",
"\n",
" for i in xrange(tips_length):\n",
" tips_i = tips[i]\n",
"\n",
" try:\n",
" max_frs[i]\n",
" except IndexError:\n",
" max_frs.append(1)\n",
" max_gvs.append(0)\n",
"\n",
" try:\n",
" word_info = tips_i[word]\n",
" word_info[FR] += 1\n",
"\n",
" if word_info[FR] > max_frs[(tips_length - 1) - i]:\n",
" max_frs[(tips_length - 1) - i] = word_info[FR]\n",
" except KeyError:\n",
" tips_i[word] = [\n",
" {}, # NEXT/VOCAB\n",
" 1, # FR\n",
" 0, # CV\n",
" 0, # SG\n",
" 0, # GV\n",
" 0 # LV\n",
" ]\n",
" word_info = tips_i[word]\n",
"\n",
" # print i, index_to_word[ word ], tips_i[word][FR]\n",
" tips[i] = word_info[NEXT]\n",
" else:\n",
" tips[:] = []\n",
" if self.__prun_counter__ >= self.__prun_trigger__:\n",
" # trie data-structures pruning\n",
" self.__prune_tries__()\n",
"\n",
" Print.info(\"finished --time: %.1fs\" % (time() - stime), offset=1)\n",
" # updating values\n",
" if update:\n",
" self.update_values(force=True)\n",
"\n",
" def classify(self, doc, prep=True, sort=True, json=False, prep_func=None):\n",
" \"\"\"\n",
" Classify a given document.\n",
"\n",
" :param doc: the content of the document\n",
" :type doc: str\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :param sort: sort the classification result (from best to worst)\n",
" :type sort: bool\n",
" :param json: return a debugging version of the result in JSON format.\n",
" :type json: bool\n",
" :param prep_func: the custom preprocessing function to be applied to\n",
" the given document before classifying it.\n",
" If not given, the default preprocessing function will\n",
" be used (as long as ``prep=True``)\n",
" :type prep_func: function\n",
" :returns: the document confidence vector if ``sort`` is False.\n",
" If ``sort`` is True, a list of pairs\n",
" (category index, confidence value) ordered by confidence value.\n",
" :rtype: list\n",
" :raises: EmptyModelError\n",
" \"\"\"\n",
" if not self.__categories__:\n",
" raise EmptyModelError\n",
"\n",
" if self.__update_needed__():\n",
" self.update_values()\n",
"\n",
" doc = doc or ''\n",
" try:\n",
" doc = doc.decode(ENCODING)\n",
" except UnicodeEncodeError: # for python 2 compatibility\n",
" doc = doc.encode(ENCODING).decode(ENCODING)\n",
" except BaseException:\n",
" pass\n",
"\n",
" if not json:\n",
" paragraphs_cvs = [\n",
" self.__classify_paragraph__(parag, prep=prep, prep_func=prep_func)\n",
" for parag in re.split(self.__parag_delimiter__, doc)\n",
" if parag\n",
" ]\n",
" if paragraphs_cvs:\n",
" cv = self.summary_op_paragraphs(paragraphs_cvs)\n",
" else:\n",
" cv = self.__zero_cv__\n",
" if sort:\n",
" return sorted(\n",
" [\n",
" (i, cv[i])\n",
" for i in xrange(len(cv))\n",
" ],\n",
" key=lambda e: -e[1]\n",
" )\n",
" return cv\n",
" else:\n",
" info = [\n",
" self.__classify_paragraph__(parag, prep=prep, prep_func=prep_func, json=True)\n",
" for parag in re_split_keep(self.__parag_delimiter__, doc)\n",
" if parag\n",
" ]\n",
"\n",
" nbr_cats = len(self.__categories__)\n",
" cv = self.summary_op_paragraphs([v[\"cv\"] for v in info])\n",
" max_v = max(cv)\n",
"\n",
" if max_v > 1:\n",
" norm_cv = map(lambda x: x / max_v, cv)\n",
" else:\n",
" norm_cv = cv\n",
"\n",
" norm_cv_sorted = sorted(\n",
" [(i, nv, cv[i]) for i, nv in enumerate(norm_cv)],\n",
" key=lambda e: -e[1]\n",
" )\n",
"\n",
" return {\n",
" \"pars\": info,\n",
" \"cv\": cv,\n",
" \"wmv\": reduce(vmax, [v[\"wmv\"] for v in info]),\n",
" \"cvns\": norm_cv_sorted,\n",
" \"ci\": [self.get_category_name(ic) for ic in xrange(nbr_cats)]\n",
" }\n",
"\n",
" def classify_label(self, doc, def_cat=STR_MOST_PROBABLE, labels=True, prep=True):\n",
" \"\"\"\n",
" Classify a given document returning the category label.\n",
"\n",
" :param doc: the content of the document\n",
" :type doc: str\n",
" :param def_cat: default category to be assigned when SS3 is not\n",
" able to classify a document. Options are\n",
" \"most-probable\", \"unknown\" or a given category name.\n",
" (default: \"most-probable\")\n",
" :type def_cat: str\n",
" :param labels: whether to return the category label or just the\n",
" category index (default: True)\n",
" :type labels: bool\n",
" :param prep: enables the default input preprocessing process (default: True)\n",
" :type prep: bool\n",
" :returns: the category label or the category index.\n",
" :rtype: str or int\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" r = self.classify(doc, sort=True, prep=prep)\n",
"\n",
" if not r or not r[0][1]:\n",
" if not def_cat or def_cat == STR_UNKNOWN:\n",
" cat = STR_UNKNOWN_CATEGORY\n",
" elif def_cat == STR_MOST_PROBABLE:\n",
" cat = self.get_most_probable_category()\n",
" else:\n",
" if self.get_category_index(def_cat) == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
" cat = def_cat\n",
" else:\n",
" cat = self.get_category_name(r[0][0])\n",
"\n",
" return cat if labels else self.get_category_index(cat)\n",
"\n",
" def classify_multilabel(self, doc, def_cat=STR_UNKNOWN, labels=True, prep=True):\n",
" \"\"\"\n",
" Classify a given document returning multiple category labels.\n",
"\n",
" This method could be used to perform multi-label classification. Internally, it\n",
" uses k-mean clustering on the confidence vector to select the proper group of\n",
" labels.\n",
"\n",
" :param doc: the content of the document\n",
" :type doc: str\n",
" :param def_cat: default category to be assigned when SS3 is not\n",
" able to classify a document. Options are\n",
" \"most-probable\", \"unknown\" or a given category name.\n",
" (default: \"unknown\")\n",
" :type def_cat: str\n",
" :param labels: whether to return the category labels or just the\n",
" category indexes (default: True)\n",
" :type labels: bool\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :returns: the list of category labels (or indexes).\n",
" :rtype: list (of str or int)\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" r = self.classify(doc, sort=True, prep=prep)\n",
"\n",
" if not r or not r[0][1]:\n",
" if not def_cat or def_cat == STR_UNKNOWN:\n",
" return []\n",
" elif def_cat == STR_MOST_PROBABLE:\n",
" cat = self.get_most_probable_category()\n",
" else:\n",
" if self.get_category_index(def_cat) == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
" cat = def_cat\n",
" if cat != STR_OTHERS_CATEGORY:\n",
" return [cat] if labels else [self.get_category_index(cat)]\n",
" else:\n",
" return []\n",
" else:\n",
" __other_idx__ = self.get_category_index(STR_OTHERS_CATEGORY)\n",
" if labels:\n",
" result = [self.get_category_name(cat_i)\n",
" for cat_i, _ in r[:kmean_multilabel_size(r)]]\n",
" # removing \"hidden\" special category (\"[other]\")\n",
" if __other_idx__ != IDX_UNKNOWN_CATEGORY and STR_OTHERS_CATEGORY in result:\n",
" result.remove(STR_OTHERS_CATEGORY)\n",
" else:\n",
" result = [cat_i for cat_i, _ in r[:kmean_multilabel_size(r)]]\n",
" # removing \"hidden\" special category (\"[other]\")\n",
" if __other_idx__ != IDX_UNKNOWN_CATEGORY and __other_idx__ in result:\n",
" result.remove(__other_idx__)\n",
" return result\n",
"\n",
" def fit(self, x_train, y_train, n_grams=1, prep=True, leave_pbar=True):\n",
" \"\"\"\n",
" Train the model given a list of documents and category labels.\n",
"\n",
" :param x_train: the list of documents\n",
" :type x_train: list (of str)\n",
" :param y_train: the list of document labels\n",
" :type y_train: list of str for singlelabel classification;\n",
" list of list of str for multilabel classification.\n",
" :param n_grams: indicates the maximum ``n``-grams to be learned\n",
" (e.g. a value of ``1`` means only 1-grams (words),\n",
" ``2`` means 1-grams and 2-grams,\n",
" ``3``, 1-grams, 2-grams and 3-grams, and so on.\n",
" :type n_grams: int\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :param leave_pbar: controls whether to leave the progress bar or\n",
" remove it after finishing.\n",
" :type leave_pbar: bool\n",
" :raises: ValueError\n",
" \"\"\"\n",
" stime = time()\n",
" x_train, y_train = list(x_train), list(y_train)\n",
"\n",
" if len(x_train) != len(y_train):\n",
" raise ValueError(\"`x_train` and `y_train` must have the same length\")\n",
"\n",
" if len(y_train) == 0:\n",
" raise ValueError(\"`x_train` and `y_train` are empty\")\n",
"\n",
" # if it's a multi-label classification problem\n",
" if is_a_collection(y_train[0]):\n",
" # flattening y_train\n",
" labels = [l for y in y_train for l in y]\n",
" self.__multilabel__ = True\n",
" else:\n",
" labels = y_train\n",
"\n",
" cats = sorted(list(set(labels)))\n",
"\n",
" # if it's a single-label classification problem\n",
" if not is_a_collection(y_train[0]):\n",
" x_train = [\n",
" \"\".join([\n",
" x_train[i]\n",
" if x_train[i] and x_train[i][-1] == '\\n'\n",
" else\n",
" x_train[i] + '\\n'\n",
" for i in xrange(len(x_train))\n",
" if y_train[i] == cat\n",
" ])\n",
" for cat in cats\n",
" ]\n",
" y_train = list(cats)\n",
"\n",
" Print.info(\"about to start training\", offset=1)\n",
" Print.verbosity_region_begin(VERBOSITY.NORMAL)\n",
" progress_bar = tqdm(total=len(x_train), desc=\"Training\",\n",
" leave=leave_pbar, disable=Print.is_quiet())\n",
"\n",
" # if it's a multi-label classification problem\n",
" if is_a_collection(y_train[0]):\n",
" __others__ = [STR_OTHERS_CATEGORY]\n",
" for i in range(len(x_train)):\n",
" for label in (y_train[i] if y_train[i] else __others__):\n",
" self.learn(\n",
" x_train[i], label,\n",
" n_grams=n_grams, prep=prep, update=False\n",
" )\n",
" progress_bar.update(1)\n",
" else:\n",
" for i in range(len(x_train)):\n",
" progress_bar.set_description_str(\"Training on '%s'\" % str(y_train[i]))\n",
" self.learn(\n",
" x_train[i], y_train[i],\n",
" n_grams=n_grams, prep=prep, update=False\n",
" )\n",
" progress_bar.update(1)\n",
" progress_bar.close()\n",
" self.__prune_tries__()\n",
" Print.verbosity_region_end()\n",
" Print.info(\"finished --time: %.1fs\" % (time() - stime), offset=1)\n",
" self.update_values(force=True)\n",
"\n",
" def predict_proba(self, x_test, prep=True, leave_pbar=True):\n",
" \"\"\"\n",
" Classify a list of documents returning a list of confidence vectors.\n",
"\n",
" :param x_test: the list of documents to be classified\n",
" :type x_test: list (of str)\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :param leave_pbar: controls whether to leave the progress bar after\n",
" finishing or remove it.\n",
" :type leave_pbar: bool\n",
" :returns: the list of confidence vectors\n",
" :rtype: list (of list of float)\n",
" :raises: EmptyModelError\n",
" \"\"\"\n",
" if not self.__categories__:\n",
" raise EmptyModelError\n",
"\n",
" if self.get_ngrams_length() == 1 and self.__summary_ops_are_pristine__():\n",
" return self.__predict_fast__(x_test, prep=prep,\n",
" leave_pbar=leave_pbar, proba=True)\n",
"\n",
" x_test = list(x_test)\n",
" classify = self.classify\n",
" return [\n",
" classify(x, sort=False)\n",
" for x in tqdm(x_test, desc=\"Classification\", disable=Print.is_quiet())\n",
" ]\n",
"\n",
" def predict(\n",
" self, x_test, def_cat=None,\n",
" labels=True, multilabel=False, prep=True, leave_pbar=True\n",
" ):\n",
" \"\"\"\n",
" Classify a list of documents.\n",
"\n",
" :param x_test: the list of documents to be classified\n",
" :type x_test: list (of str)\n",
" :param def_cat: default category to be assigned when SS3 is not\n",
" able to classify a document. Options are\n",
" \"most-probable\", \"unknown\" or a given category name.\n",
" (default: \"most-probable\", or \"unknown\" for\n",
" multi-label classification)\n",
" :type def_cat: str\n",
" :param labels: whether to return the list of category names or just\n",
" category indexes\n",
" :type labels: bool\n",
" :param multilabel: whether to perform multi-label classification or not.\n",
" if enabled, for each document returns a ``list`` of labels\n",
" instead of a single label (``str``).\n",
" If the model was trained using multilabeled data, then this\n",
" argument will be ignored and set to True.\n",
" :type multilabel: bool\n",
" :param prep: enables the default input preprocessing (default: True)\n",
" :type prep: bool\n",
" :param leave_pbar: controls whether to leave the progress bar or\n",
" remove it after finishing.\n",
" :type leave_pbar: bool\n",
" :returns: if ``labels`` is True, the list of category names,\n",
" otherwise, the list of category indexes.\n",
" :rtype: list (of int or str)\n",
" :raises: EmptyModelError, InvalidCategoryError\n",
" \"\"\"\n",
" if not self.__categories__:\n",
" raise EmptyModelError\n",
"\n",
" multilabel = multilabel or self.__multilabel__\n",
"\n",
" if def_cat is None:\n",
" def_cat = STR_UNKNOWN if multilabel else STR_MOST_PROBABLE\n",
"\n",
" if not def_cat or def_cat == STR_UNKNOWN:\n",
" if not multilabel:\n",
" Print.info(\n",
" \"default category was set to 'unknown' (its index will be -1)\",\n",
" offset=1\n",
" )\n",
" else:\n",
" if def_cat == STR_MOST_PROBABLE:\n",
" Print.info(\n",
" \"default category was automatically set to '%s' \"\n",
" \"(the most probable one)\" % self.get_most_probable_category(),\n",
" offset=1\n",
" )\n",
" else:\n",
" Print.info(\"default category was set to '%s'\" % def_cat, offset=1)\n",
" if self.get_category_index(def_cat) == IDX_UNKNOWN_CATEGORY:\n",
" raise InvalidCategoryError\n",
"\n",
" if self.get_ngrams_length() == 1 and self.__summary_ops_are_pristine__():\n",
" return self.__predict_fast__(x_test, def_cat=def_cat, labels=labels,\n",
" multilabel=multilabel, prep=prep,\n",
" leave_pbar=leave_pbar)\n",
"\n",
" stime = time()\n",
" Print.info(\"about to start classifying test documents\", offset=1)\n",
" classify = self.classify_label if not multilabel else self.classify_multilabel\n",
" x_test = list(x_test)\n",
" y_pred = [\n",
" classify(doc, def_cat=def_cat, labels=labels, prep=prep)\n",
" for doc in tqdm(x_test, desc=\"Classification\",\n",
" leave=leave_pbar, disable=Print.is_quiet())\n",
" ]\n",
"\n",
" Print.info(\"finished --time: %.1fs\" % (time() - stime), offset=1)\n",
" return y_pred\n",
"\n",
" def cv(self, ngram, cat):\n",
" \"\"\"\n",
" Return the \"confidence value\" of a given word n-gram for the given category.\n",
"\n",
" This value is obtained applying a final transformation on the global value\n",
" of the given word n-gram using the gv function [*].\n",
"\n",
" These transformation are given when creating a new SS3 instance (see the\n",
" SS3 class constructor's ``cv_m`` argument for more information).\n",
"\n",
" [*] the gv function is defined in Section 3.2.2 of the original paper:\n",
" https://arxiv.org/pdf/1905.08772.pdf\n",
"\n",
" Examples:\n",
"\n",
" >>> clf.cv(\"chicken\", \"food\")\n",
" >>> clf.cv(\"roast chicken\", \"food\")\n",
" >>> clf.cv(\"chicken\", \"sports\")\n",
"\n",
" :param ngram: the word or word n-gram\n",
" :type ngram: str\n",
" :param cat: the category label\n",
" :type cat: str\n",
" :returns: the confidence value\n",
" :rtype: float\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" return self.__apply_fn__(self.__cv__, ngram, cat)\n",
"\n",
" def gv(self, ngram, cat):\n",
" \"\"\"\n",
" Return the \"global value\" of a given word n-gram for the given category.\n",
"\n",
" (gv function is defined in Section 3.2.2 of the original paper:\n",
" https://arxiv.org/pdf/1905.08772.pdf)\n",
"\n",
" Examples:\n",
"\n",
" >>> clf.gv(\"chicken\", \"food\")\n",
" >>> clf.gv(\"roast chicken\", \"food\")\n",
" >>> clf.gv(\"chicken\", \"sports\")\n",
"\n",
" :param ngram: the word or word n-gram\n",
" :type ngram: str\n",
" :param cat: the category label\n",
" :type cat: str\n",
" :returns: the global value\n",
" :rtype: float\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" return self.__apply_fn__(self.__gv__, ngram, cat)\n",
"\n",
" def lv(self, ngram, cat):\n",
" \"\"\"\n",
" Return the \"local value\" of a given word n-gram for the given category.\n",
"\n",
" (lv function is defined in Section 3.2.2 of the original paper:\n",
" https://arxiv.org/pdf/1905.08772.pdf)\n",
"\n",
" Examples:\n",
"\n",
" >>> clf.lv(\"chicken\", \"food\")\n",
" >>> clf.lv(\"roast chicken\", \"food\")\n",
" >>> clf.lv(\"chicken\", \"sports\")\n",
"\n",
" :param ngram: the word or word n-gram\n",
" :type ngram: str\n",
" :param cat: the category label\n",
" :type cat: str\n",
" :returns: the local value\n",
" :rtype: float\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" return self.__apply_fn__(self.__lv__, ngram, cat)\n",
"\n",
" def sg(self, ngram, cat):\n",
" \"\"\"\n",
" Return the \"significance factor\" of a given word n-gram for the given category.\n",
"\n",
" (sg function is defined in Section 3.2.2 of the original paper:\n",
" https://arxiv.org/pdf/1905.08772.pdf)\n",
"\n",
" Examples:\n",
"\n",
" >>> clf.sg(\"chicken\", \"food\")\n",
" >>> clf.sg(\"roast chicken\", \"food\")\n",
" >>> clf.sg(\"chicken\", \"sports\")\n",
"\n",
" :param ngram: the word or word n-gram\n",
" :type ngram: str\n",
" :param cat: the category label\n",
" :type cat: str\n",
" :returns: the significance factor\n",
" :rtype: float\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" return self.__apply_fn__(self.__sg__, ngram, cat)\n",
"\n",
" def sn(self, ngram, cat):\n",
" \"\"\"\n",
" Return the \"sanction factor\" of a given word n-gram for the given category.\n",
"\n",
" (sn function is defined in Section 3.2.2 of the original paper:\n",
" https://arxiv.org/pdf/1905.08772.pdf)\n",
"\n",
" Examples:\n",
"\n",
" >>> clf.sn(\"chicken\", \"food\")\n",
" >>> clf.sn(\"roast chicken\", \"food\")\n",
" >>> clf.sn(\"chicken\", \"sports\")\n",
"\n",
" :param ngram: the word or word n-gram\n",
" :type ngram: str\n",
" :param cat: the category label\n",
" :type cat: str\n",
" :returns: the sanction factor\n",
" :rtype: float\n",
" :raises: InvalidCategoryError\n",
" \"\"\"\n",
" return self.__apply_fn__(self.__sn__, ngram, cat)\n",
"\n",
"\n",
"class EmptyModelError(Exception):\n",
" \"\"\"Exception to be thrown when the model is empty.\"\"\"\n",
"\n",
" def __init__(self, msg=''):\n",
" \"\"\"Class constructor.\"\"\"\n",
" Exception.__init__(\n",
" self,\n",
" \"The model is empty (it hasn't been trained yet).\"\n",
" )\n",
"\n",
"\n",
"class InvalidCategoryError(Exception):\n",
" \"\"\"Exception to be thrown when a category is not valid.\"\"\"\n",
"\n",
" def __init__(self, msg=''):\n",
" \"\"\"Class constructor.\"\"\"\n",
" Exception.__init__(\n",
" self,\n",
" \"The given category is not valid\"\n",
" )\n",
"\n",
"\n",
"def kmean_multilabel_size(res):\n",
" \"\"\"\n",
" Use k-means to tell where to split the ``SS3.classify'''s output.\n",
"\n",
" Given a ``SS3.classify``'s output (``res``), tell where to partition it\n",
" into 2 clusters so that one of the cluster holds the category labels that\n",
" the classifier should output when performing multi-label classification.\n",
" To achieve this, implement k-means (i.e. 2-means) clustering over the\n",
" category confidence values in ``res``.\n",
"\n",
" :param res: the classification output of ``SS3.classify``\n",
" :type res: list (of sorted pairs (category, confidence value))\n",
" :returns: a positive integer indicating where to split ``res``\n",
" :rtype: int\n",
" \"\"\"\n",
" cent = {\"neg\": -1, \"pos\": -1} # centroids (2 clusters: \"pos\" and \"neg\")\n",
" clust = {\"neg\": [], \"pos\": []} # clusters (2 clusters: \"pos\" and \"neg\")\n",
" new_cent_neg = res[-1][1]\n",
" new_cent_pos = res[0][1]\n",
"\n",
" if new_cent_neg == new_cent_pos:\n",
" return 0\n",
"\n",
" while (cent[\"pos\"] != new_cent_pos) or (cent[\"neg\"] != new_cent_neg):\n",
" cent[\"neg\"], cent[\"pos\"] = new_cent_neg, new_cent_pos\n",
" clust[\"neg\"], clust[\"pos\"] = [], []\n",
" for _, cat_cv in res:\n",
" if abs(cent[\"neg\"] - cat_cv) < abs(cent[\"pos\"] - cat_cv):\n",
" clust[\"neg\"].append(cat_cv)\n",
" else:\n",
" clust[\"pos\"].append(cat_cv)\n",
" if len(clust[\"neg\"]) > 0:\n",
" new_cent_neg = sum(clust[\"neg\"]) / len(clust[\"neg\"])\n",
" if len(clust[\"pos\"]) > 0:\n",
" new_cent_pos = sum(clust[\"pos\"]) / len(clust[\"pos\"])\n",
" return len(clust[\"pos\"])\n",
"\n",
"\n",
"def sigmoid(v, l):\n",
" \"\"\"A sigmoid function.\"\"\"\n",
" try:\n",
" return .5 * tanh((3. / l) * v - 3) + .5\n",
" except ZeroDivisionError:\n",
" return 0\n",
"\n",
"\n",
"def mad(values, n):\n",
" \"\"\"Median absolute deviation mean.\"\"\"\n",
" if len(values) < n:\n",
" values += [0] * int(n - len(values))\n",
" values.sort()\n",
" if n == 2:\n",
" return (values[0], values[0])\n",
" values_m = n // 2 if n % 2 else n // 2 - 1\n",
" m = values[values_m] # Median\n",
" sd = sum([abs(m - lv) for lv in values]) / float(n) # sd Mean\n",
" return m, sd\n",
"\n",
"\n",
"def key_as_int(dct):\n",
" \"\"\"Cast the given dictionary (numerical) keys to int.\"\"\"\n",
" keys = list(dct)\n",
" if len(keys) and keys[0].isdigit():\n",
" new_dct = {}\n",
" for key in dct:\n",
" new_dct[int(key)] = dct[key]\n",
" return new_dct\n",
" return dct\n",
"\n",
"\n",
"def re_split_keep(regex, string):\n",
" \"\"\"\n",
" Force the inclusion of unmatched items by re.split.\n",
"\n",
" This allows keeping the original content after splitting the input\n",
" document for later use (e.g. for using it from the Live Test)\n",
" \"\"\"\n",
" if not re.match(r\"\\(.*\\)\", regex):\n",
" regex = \"(%s)\" % regex\n",
" return re.split(regex, string)\n",
"\n",
"\n",
"def list_hash(str_list):\n",
" \"\"\"\n",
" Return a hash value for a given list of string.\n",
"\n",
" :param str_list: a list of strings (e.g. x_test)\n",
" :type str_list: list (of str)\n",
" :returns: an MD5 hash value\n",
" :rtype: str\n",
" \"\"\"\n",
" import hashlib\n",
" m = hashlib.md5()\n",
" for doc in str_list:\n",
" try:\n",
" m.update(doc)\n",
" except (TypeError, UnicodeEncodeError):\n",
" m.update(doc.encode('ascii', 'ignore'))\n",
" return m.hexdigest()\n",
"\n",
"\n",
"def vsum(v0, v1):\n",
" \"\"\"Vectorial version of sum.\"\"\"\n",
" return [v0[i] + v1[i] for i in xrange(len(v0))]\n",
"\n",
"\n",
"def vmax(v0, v1):\n",
" \"\"\"Vectorial version of max.\"\"\"\n",
" return [max(v0[i], v1[i]) for i in xrange(len(v0))]\n",
"\n",
"\n",
"def vdiv(v0, v1):\n",
" \"\"\"Vectorial version of division.\"\"\"\n",
" return [v0[i] / v1[i] if v1[i] else 0 for i in xrange(len(v0))]\n",
"\n",
"\n",
"def set_verbosity(level):\n",
" \"\"\"\n",
" Set the verbosity level.\n",
"\n",
" - ``0`` (quiet): do not output any message (only error messages)\n",
" - ``1`` (normal): default behavior, display only warning messages and progress bars\n",
" - ``2`` (verbose): display also the informative non-essential messages\n",
"\n",
" The following built-in constants can also be used to refer to these 3 values:\n",
" ``VERBOSITY.QUIET``, ``VERBOSITY.NORMAL``, and ``VERBOSITY.VERBOSE``, respectively.\n",
"\n",
" For example, if you want PySS3 to hide everything, even progress bars, you could simply do:\n",
"\n",
" >>> import pyss3\n",
" ...\n",
" >>> pyss3.set_verbosity(0)\n",
" ...\n",
" >>> # here's the rest of your code :D\n",
"\n",
" or, equivalently:\n",
"\n",
" >>> import pyss3\n",
" >>> from pyss3 import VERBOSITY\n",
" ...\n",
" >>> pyss3.set_verbosity(VERBOSITY.QUIET)\n",
" ...\n",
" >>> # here's the rest of your code :D\n",
"\n",
" :param level: the verbosity level\n",
" :type level: int\n",
" \"\"\"\n",
" Print.set_verbosity(level)\n",
"\n",
"\n",
"# user-friendly aliases\n",
"SS3.set_smoothness = SS3.set_s\n",
"SS3.get_smoothness = SS3.get_s\n",
"SS3.set_significance = SS3.set_l\n",
"SS3.get_significance = SS3.get_l\n",
"SS3.set_sanction = SS3.set_p\n",
"SS3.get_sanction = SS3.get_p\n",
"SS3.set_alpha = SS3.set_a\n",
"SS3.get_alpha = SS3.get_a\n",
"SS3.get_alpha = SS3.get_a\n",
"SS3.train = SS3.fit\n",
"SS3.save = SS3.save_model\n",
"SS3.load = SS3.load_model\n",
"SS3.update = SS3.update_values\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Ik6zWFeBe2kf",
"outputId": "e8c25721-d46f-4e00-bd18-372096cdb551"
},
"source": [
"!curl https://raw.githubusercontent.com/sergioburdisso/pyss3/master/examples/datasets/topic.zip --output topic.zip"
],
"execution_count": 3,
"outputs": [
{
"output_type": "stream",
"text": [
" % Total % Received % Xferd Average Speed Time Time Time Current\n",
" Dload Upload Total Spent Left Speed\n",
"100 6485k 100 6485k 0 0 10.5M 0 --:--:-- --:--:-- --:--:-- 10.5M\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "ZW4LvzfxfOJG",
"outputId": "a8857b93-f027-4739-e426-41222b653b6e"
},
"source": [
"!unzip -u topic.zip"
],
"execution_count": 4,
"outputs": [
{
"output_type": "stream",
"text": [
"Archive: topic.zip\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "zD9KDwgyh_SC"
},
"source": [
"# Train with whole dataset at once"
]
},
{
"cell_type": "code",
"metadata": {
"id": "1G1UvpL2ffhz"
},
"source": [
"clf = SS3()"
],
"execution_count": 5,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "GWJY9sJNfqMw",
"outputId": "9dbf5e06-28d8-4ed2-9962-ae1ee84a2cff"
},
"source": [
"x_train, y_train = Dataset.load_from_files(\"topic/train\", folder_label=False)\n",
"x_test, y_test = Dataset.load_from_files(\"topic/test\", folder_label=False)"
],
"execution_count": 6,
"outputs": [
{
"output_type": "stream",
"text": [
"Loading 'music' documents: 100%|██████████| 8/8 [00:00<00:00, 54.60it/s]\n",
"Loading 'music' documents: 100%|██████████| 8/8 [00:00<00:00, 492.72it/s]\n"
],
"name": "stderr"
}
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Bj7VIXFnfwFp",
"outputId": "dd64402f-060e-47d9-9fab-7d1d07a160e0"
},
"source": [
"clf.train(x_train, y_train)\n",
"clf.print_categories_info()"
],
"execution_count": 7,
"outputs": [
{
"output_type": "stream",
"text": [
"Training on 'sports': 100%|██████████| 8/8 [00:26<00:00, 3.30s/it]\n"
],
"name": "stderr"
},
{
"output_type": "stream",
"text": [
"\n",
"\n",
" \u001b[92m\u001b[4m\u001b[1mCATEGORIES\u001b[0m\u001b[0m\u001b[0m:\n",
"\n",
"\u001b[93m\t Category \u001b[0m| Index | Length | Vocab. Size | Word Max. Fr. | N-gram |\n",
"\t-------------------|-------|------------|-------------|---------------|--------|\n",
"art&photography 35597\n",
"beauty&fashion 29072\n",
"business&finance 19851\n",
"food 35993\n",
"health 25677\n",
"music 25298\n",
"science&technology 27361\n",
"sports 20019\n",
"\n",
"\t\u001b[4m\u001b[1mMost Probable Category\u001b[0m\u001b[0m: \u001b[93mhealth\u001b[0m \u001b[94m(15.96%)\u001b[0m\n",
"\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 920
},
"id": "uHjkVHcuhgtz",
"outputId": "da11443d-4508-4010-c9d4-d2ae79acb953"
},
"source": [
"Evaluation.test(clf, x_test, y_test)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"Classification: 100%|██████████| 800/800 [00:00<00:00, 33122.19it/s]\n"
],
"name": "stderr"
},
{
"output_type": "stream",
"text": [
"\n",
" precision recall f1-score support\n",
"\n",
" art&photography 0.63 0.88 0.73 100\n",
" beauty&fashion 0.77 0.68 0.72 100\n",
" business&finance 0.76 0.71 0.73 100\n",
" food 0.76 0.25 0.38 100\n",
" health 0.53 0.80 0.64 100\n",
" music 0.85 0.75 0.80 100\n",
"science&technology 0.60 0.75 0.67 100\n",
" sports 0.99 0.81 0.89 100\n",
"\n",
" accuracy 0.70 800\n",
" macro avg 0.74 0.70 0.69 800\n",
" weighted avg 0.74 0.70 0.69 800\n",
"\n",
"\n",
" \u001b[1mAccuracy\u001b[0m: 0.704\n",
"\n",
"\u001b[94m[ updating evaluations cache ]\u001b[0m\n"
],
"name": "stdout"
},
{
"output_type": "display_data",
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 576x576 with 2 Axes>"
]
},
"metadata": {
"tags": [],
"needs_background": "light"
}
},
{
"output_type": "execute_result",
"data": {
"text/plain": [
"0.70375"
]
},
"metadata": {
"tags": []
},
"execution_count": 8
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "KMxtRq0KiFBy"
},
"source": [
"# Train with fractions of the dataset one by one"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "qGMuzKfofzQU",
"outputId": "6041ce1d-2b15-446b-daf0-466b8403914e"
},
"source": [
"clf2 = SS3()\n",
"x_train1, x_train2 = x_train[:len(x_train)//2], x_train[len(x_train)//2:]\n",
"y_train1, y_train2 = y_train[:len(y_train)//2], y_train[len(y_train)//2:]\n",
"clf2.train(x_train1, y_train1)\n",
"clf2.train(x_train2, y_train2)\n",
"clf2.print_categories_info()"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"Training on 'sports': 100%|██████████| 5/5 [00:13<00:00, 2.71s/it]\n",
"Training on 'science&technology': 100%|██████████| 4/4 [00:12<00:00, 3.18s/it]\n"
],
"name": "stderr"
},
{
"output_type": "stream",
"text": [
"\n",
"\n",
" \u001b[92m\u001b[4m\u001b[1mCATEGORIES\u001b[0m\u001b[0m\u001b[0m:\n",
"\n",
"\u001b[93m\t Category \u001b[0m| Index | Length | Vocab. Size | Word Max. Fr. | N-gram |\n",
"\t-------------------|-------|------------|-------------|---------------|--------|\n",
"\u001b[93m\t beauty&fashion \u001b[0m| 0 | 574151 | 29072 | 27599 | 1 |\n",
"\u001b[93m\t business&finance \u001b[0m| 1 | 424383 | 19851 | 20855 | 1 |\n",
"\u001b[93m\t food \u001b[0m| 2 | 725110 | 35993 | 40588 | 1 |\n",
"\u001b[93m\t health \u001b[0m| 3 | 727389 | 25677 | 45729 | 1 |\n",
"\u001b[93m\t sports \u001b[0m| 4 | 498989 | 20019 | 33277 | 1 |\n",
"\u001b[93m\t art&photography \u001b[0m| 5 | 580241 | 35597 | 20913 | 1 |\n",
"\u001b[93m\t music \u001b[0m| 6 | 437098 | 25298 | 27204 | 1 |\n",
"\u001b[93m\tscience&technology \u001b[0m| 7 | 589144 | 27361 | 21620 | 1 |\n",
"\n",
"\t\u001b[4m\u001b[1mMost Probable Category\u001b[0m\u001b[0m: \u001b[93mhealth\u001b[0m \u001b[94m(15.96%)\u001b[0m\n",
"\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 885
},
"id": "O18cJOoFhoso",
"outputId": "7242105c-4c6d-4339-e021-a6e33c99228d"
},
"source": [
"Evaluation.test(clf2, x_test, y_test)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"\n",
" precision recall f1-score support\n",
"\n",
" art&photography 0.63 0.88 0.73 100\n",
" beauty&fashion 0.77 0.68 0.72 100\n",
" business&finance 0.76 0.71 0.73 100\n",
" food 0.76 0.25 0.38 100\n",
" health 0.53 0.80 0.64 100\n",
" music 0.85 0.75 0.80 100\n",
"science&technology 0.60 0.75 0.67 100\n",
" sports 0.99 0.81 0.89 100\n",
"\n",
" accuracy 0.70 800\n",
" macro avg 0.74 0.70 0.69 800\n",
" weighted avg 0.74 0.70 0.69 800\n",
"\n",
"\n",
" \u001b[1mAccuracy\u001b[0m: 0.704\n",
"\n"
],
"name": "stdout"
},
{
"output_type": "display_data",
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 576x576 with 2 Axes>"
]
},
"metadata": {
"tags": [],
"needs_background": "light"
}
},
{
"output_type": "execute_result",
"data": {
"text/plain": [
"0.70375"
]
},
"metadata": {
"tags": []
},
"execution_count": 10
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment