diff --git a/docs/bag_of_words.ipynb b/docs/bag_of_words.ipynb new file mode 100644 index 0000000..78babea --- /dev/null +++ b/docs/bag_of_words.ipynb @@ -0,0 +1,1026 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Représentation de séries temporelles en sac de mots et classification

\n", + "\n", + "L'approximation est une notion courante en apprentissage automatique, notamment concernant le surapprentissage et la généralisation. Par exemple, dans un arbre de décision, chaque noeud regarde si la valeur d'une variable est inférieure ou supérieure à un seuil donné : la décision est la même pour deux valeurs différentes du moment qu'elles sont toutes les deux soit inférieures soit supérieures à ce seuil.\n", + "\n", + "Ce principe d'approximation a également été étudié pour la classification de séries temporelles. L'idée est de transformer une série temporelle (un vecteur ordonné de nombres réels) en un mot (un vecteur ordonné de symboles).\n", + "\n", + "En pratique, les motifs caractéristiques de séries temporelles sont plus courts que la série temporelle entière. Le principe est donc d'extraire des sous séries temporelles avec une fenêtre glissante et de transformer chaque sous série en un mot. Le résultat final est donc une liste de mots. Néanmoins, pour la plupart des jeux de données utilisés, la position des motifs est beaucoup moins importante que la présence elle-même des motifs. Ce sont donc les fréquences des mots qui sont calculés.\n", + "\n", + "En résumé, les algorithmes transformant des séries temporelles en sac de mots fonctionnent de la même manière suivante :\n", + "1. Extraction de sous séries temporelles avec une fenêtre glissante.\n", + "2. Approximation de chaque sous série temporelle en un mot.\n", + "3. Calcul de la fréquence de chaque mot.\n", + "4. Classification basée sur cette représentation.\n", + "\n", + "Il existe des variantes pour chacune de ces étapes. Dans ce tutoriel, nous allons nous focaliser sur deux étapes en particulier :\n", + "(2) l'approximation de chaque sous série temporelle en un mot, et\n", + "(4) la classification basée sur cette représentation.\n", + "\n", + "Il existe de grandes approches pour la transformation de chaque sous série temporelle en un mot : l'approximation dans le domaine temporel et l'approximation dans le domaine fréquentiel. Le tutoriel se décompose en deux parties pour chacune des approches, avec l'illustration de quelques algorithmes.\n", + "\n", + "Pour ce faire, nous allons utiliser un paquet Python appelé `pyts` dédié à la classification de séries temporelles et mettant à disposition des implémentations de nombreux algorithmes. Le projet est hébergé sur [GitHub](https://github.com/johannfaouzi/pyts) et sa documentation sur [ReadTheDocs](https://pyts.readthedocs.io/en/stable/index.html).\n", + "\n", + "Les paquets Python nécessaires pour ce tutoriel sont :\n", + "* `matplotlib`\n", + "* `numpy`\n", + "* `scipy`\n", + "* `numba`\n", + "* `joblib`\n", + "* `scikit-learn`\n", + "* `pyts`\n", + "\n", + "On installe les paquets si nécessaire en exécutant la cellule suivante (supprimer le # pour supprimer le commentaire) :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ! pip install pyts matplotlib" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On importe toutes les fonctions et classes nécessaires pour ce tutoriel :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Patch\n", + "import numpy as np\n", + "from pyts.approximation import (\n", + " PiecewiseAggregateApproximation, SymbolicAggregateApproximation, SymbolicFourierApproximation\n", + ")\n", + "from pyts.datasets import fetch_ucr_dataset\n", + "from pyts.classification import BOSSVS, KNeighborsClassifier, SAXVSM\n", + "from pyts.transformation import BagOfPatterns, BOSS\n", + "from sklearn.ensemble import VotingClassifier\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.pipeline import Pipeline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On télécharge ensuite le jeu de données utilisé dans ce tutoriel :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_train, X_test, y_train, y_test = fetch_ucr_dataset('Trace', return_X_y=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Enfin, on indique quelques hyper-paramètres courrament utilisés dans les algorithmes :\n", + "* `window_size` : la taille de la fenêtre glissante\n", + "* `word_size` et `n_coefs` : la longueur des mots\n", + "* `n_bins` : le nombre de symboles (lettres) dans l'alphabet\n", + "* `strategy` : la manière de calculer les intervalles pour la discrétisation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Domaine temporel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Symbolic Aggregate Approximation (SAX)\n", + "\n", + "Dans le domaine temporel, une série temporelle est approchée en deux temps :\n", + "1. La série est tout d'abord sous-échantillonnée (en prenant la valeur moyenne en général).\n", + "2. La série sous-échantillonnée est discrétisée.\n", + "\n", + "La première étape est connue dans la littérature sous le nom de *Piecewise Aggregate Approximation (PAA)*. L'ensemble des deux étapes est connue sous le nom *Symbolic Aggregate approXimation (SAX)*.\n", + "\n", + "Ces deux algorithmes sont implémentés dans les classes [pyts.approximation.PiecewiseAggregateApproximation](https://pyts.readthedocs.io/en/stable/generated/pyts.approximation.PiecewiseAggregateApproximation.html) et [pyts.approximation.SymbolicAggregateApproximation](https://pyts.readthedocs.io/en/stable/generated/pyts.approximation.SymbolicAggregateApproximation.html)\n", + "\n", + "**Attention** : `pyts.approximation.SymbolicAggregateApproximation` n'effectue que la deuxième étape (la discrétisation). Pour effectuer les deux étapes, il faut créer une chaîne.\n", + "\n", + "Le code suivant illustre l'utilisation de ces deux classes et la transformation obtenue pour la première série temporelle du jeu d'entraînement :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sax = Pipeline([\n", + " ('paa', PiecewiseAggregateApproximation(window_size=11)),\n", + " ('sax', SymbolicAggregateApproximation(strategy='normal'))\n", + "])\n", + "''.join(sax.transform(X_train[:1]).ravel())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_sax` illustre l'algorithme :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_sax(clf, x):\n", + "\n", + " from scipy.stats import norm\n", + " from pyts.utils import segmentation\n", + "\n", + " def _compute_paa():\n", + " window_size, output_size = clf['paa']._check_params(n_timestamps)\n", + " start, end, n_timestamps_new = segmentation(\n", + " n_timestamps, window_size, clf['paa'].overlapping, output_size\n", + " )\n", + " x_paa = clf['paa'].transform(x.reshape(1, -1))[0]\n", + " return x_paa, start, end - 1\n", + "\n", + " def _compute_bins():\n", + " if strategy == 'normal':\n", + " bins_edges = norm.ppf(np.linspace(0, 1, n_bins + 1)[1:-1])\n", + " elif strategy == 'uniform':\n", + " bins_edges = np.linspace(np.min(x_paa), np.max(x_paa), n_bins + 1)[1:-1]\n", + " else:\n", + " bins_edges = np.percentile(\n", + " x_paa, np.linspace(0, 100, n_bins + 1)[1:-1]\n", + " )\n", + " return bins_edges\n", + " \n", + " x = np.asarray(x)\n", + " n_timestamps = x.size\n", + " n_bins = sax['sax'].n_bins\n", + " strategy = sax['sax'].strategy\n", + "\n", + " x_paa, start, end = _compute_paa()\n", + " bin_edges = _compute_bins()\n", + " x_sax = clf.transform(x.reshape(1, -1))[0]\n", + " \n", + " plt.figure(figsize=(16, 6))\n", + " plt.plot(x)\n", + " plt.hlines(x_paa, start, end, color='r')\n", + " plt.hlines(bin_edges, 0, x.size, color='C1', linestyles='--', linewidth=1.5, label=strategy)\n", + " for i in range(start.size):\n", + " plt.text(0.5 * (start[i] + end[i]), plt.ylim()[0] - 0.2 * (plt.ylim()[1] - plt.ylim()[0]),\n", + " x_sax[i], color='r', fontsize=16, ha='center')\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Par exemple, voici la transformation obtenue pour la première série temporelle du jeu d'entraînement :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sax = Pipeline([\n", + " ('paa', PiecewiseAggregateApproximation(window_size=20)),\n", + " ('sax', SymbolicAggregateApproximation(strategy='normal'))\n", + "])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_sax(sax, X_train[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Changez les valeurs de certains hyper-paramètres et observez comment la représentation obtenue change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bag of Patterns (BOP)\n", + "\n", + "L'algorithme *Bag of Patterns (BOP)* consiste à extraire des sous séries temporelles, puis à les transformer en mots avec l'algorithme *SAX*, et enfin calculer la fréquence de chaque mot. La classification à partir de cette nouvelle représentation peut se faire avec n'importe quel algorithme classique de classification, mais nous utiliserons ici la méthode des plus proches voisins (car il s'agit de l'algorithme utilisé dans la publication originale de cet algorithme).\n", + "\n", + "L'algorithme est implémentée dans la classe [pyts.transformation.BagOfPatterns](https://pyts.readthedocs.io/en/stable/generated/pyts.transformation.BagOfPatterns.html). Le code suivant illustre une utilisation simple pour entraîner un modèle sur un jeu d'entraînement et l'évaluer sur un jeu de test :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bop = Pipeline([\n", + " ('bop', BagOfPatterns(word_size=4, n_bins=4, sparse=False)),\n", + " ('1nn', KNeighborsClassifier())\n", + "])\n", + "bop.fit(X_train, y_train)\n", + "bop.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_bop` illustre la transformation obtenue par cet algorithme : " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "def plot_bop(clf, x):\n", + " \n", + " from pyts.bag_of_words import BagOfWords\n", + " from pyts.utils import windowed_view\n", + " \n", + " def get_bop_words():\n", + " n_timestamps = x.size\n", + "\n", + " bow = BagOfWords(\n", + " window_size=clf_.window_size, word_size=clf_.word_size,\n", + " n_bins=clf_.n_bins, strategy=clf_.strategy,\n", + " numerosity_reduction=False,\n", + " window_step=clf_.window_step, norm_mean=clf_.norm_mean,\n", + " norm_std=clf_.norm_std, overlapping=clf_.overlapping,\n", + " alphabet=clf_.alphabet\n", + " )\n", + " window_size, word_size, window_step, _ = bow._check_params(n_timestamps)\n", + " n_windows = (n_timestamps - window_size + window_step) // window_step\n", + "\n", + " X_bow = bow.fit_transform(x.reshape(1, -1), None)\n", + " x_word = np.asarray(X_bow[0].split(' '))\n", + " not_equal_idx = np.r_[True, x_word[1:] != x_word[:-1]]\n", + " return x_word, not_equal_idx, window_size, word_size, window_step, n_windows\n", + "\n", + " if isinstance(clf, Pipeline):\n", + " clf_ = clf['bop']\n", + " else:\n", + " clf_ = clf\n", + "\n", + " x = np.asarray(x)\n", + " \n", + " x_word, not_equal_idx, window_size, word_size, window_step, n_windows = get_bop_words()\n", + "\n", + " fig, axes = plt.subplots(3, 1, figsize=(16, 8), gridspec_kw={'hspace': 0.5})\n", + "\n", + " # Plot raw time series\n", + " axes[0].plot(x)\n", + " xlim = axes[0].get_xlim()\n", + " ylim = axes[0].get_ylim()\n", + " \n", + " axes[0].text(0, 1.04, r'(a)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[0].transAxes)\n", + "\n", + " # Plot BOP words\n", + " for i, pos in enumerate(np.linspace(0, x.size - 1, n_windows, endpoint=False)):\n", + " if clf_.numerosity_reduction:\n", + " alpha = 1. if not_equal_idx[i] else 0.2\n", + " fontweight = 'bold' if not_equal_idx[i] else 'normal'\n", + " else:\n", + " alpha = 1.\n", + " fontweight = 'bold'\n", + " axes[1].text(pos, 1.2 - (i % (word_size + 5)) / 5, x_word[i],\n", + " color='C0', alpha=alpha, fontsize=8, fontweight=fontweight)\n", + "\n", + " axes[1].set_xlim(xlim)\n", + " axes[1].set_axis_off()\n", + " axes[1].text(0., 1.26, '(b)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[1].transAxes)\n", + "\n", + " # Plot BOP histogram\n", + " X_bop = clf_.transform(x.reshape(1, -1))\n", + " x_bop = X_bop[0]\n", + "\n", + " vocabulary_length = len(clf_.vocabulary_)\n", + " vocabulary = np.vectorize(clf_.vocabulary_.get)(np.arange(vocabulary_length))\n", + " axes[2].bar(np.arange(np.sum(x_bop > 0)), x_bop[x_bop > 0])\n", + " axes[2].set_xticks(np.arange(np.sum(x_bop > 0)))\n", + " axes[2].set_xticklabels(vocabulary[x_bop > 0], rotation=90)\n", + " axes[2].text(0, 1.04, '(c)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[2].transAxes)\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Par exemple, voici la transformation obtenue pour la première série temporelle du jeu d'entraînement :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_bop(bop, X_train[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Changez les valeurs de certains hyper-paramètres, observez comment la représentation obtenue change et regardez la performance prédictive obtenue. Si vous êtes familier avec `scikit-learn`, vous pouvez également essayer un autre algorithme de classification. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Symbolic Aggregate Approximation in Vector Space Model (SAXVSM)\n", + "\n", + "L'algorithme *Symbolic Aggregate Approximation in Vector Space Model (SAXVSM)* est inspiré du traitement du langage naturel, et plus particulièrement l'approche *term-frequency inverse-document-frequency (TF-IDF)*.\n", + "Les sacs de mots d'une même classe sont regroupés ensemble et forment un seul document. L'objectif est de trouver des mots qui sont spécifiques à chaque classe.\n", + "Pour ce faire, les fréquences de mots (*term frequencies*) vont être pondérées par l'inverse de leur fréquence dans les classes (*inverse document frequencies*).\n", + "Le principe derrière cette approche est intuitive : si un mot n'apparaît dans peu de classes, alors il lui est donné un poids plus élevé car il est discriminatif, tandis qu'un mot qui apparaît dans beaucoup de classes a un poids moins élevé car il est peu discriminatif.\n", + "\n", + "L'algorithme est implémentée dans la classe [pyts.classification.SAXVSM](https://pyts.readthedocs.io/en/stable/generated/pyts.classification.SAXVSM.html). Le code suivant illustre une utilisation simple pour entraîner un modèle sur un jeu d'entraînement et l'évaluer sur un jeu de test :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "saxvsm = SAXVSM(window_size=90, word_size=3, n_bins=4)\n", + "saxvsm.fit(X_train, y_train)\n", + "saxvsm.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_saxvsm` illustre les vecteurs TF-IDF appris par l'algorithme :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_saxvsm(clf):\n", + " n_classes = len(clf.classes_)\n", + " vocabulary_length = len(clf.vocabulary_)\n", + " vocabulary = np.vectorize(clf.vocabulary_.get)(np.arange(vocabulary_length))\n", + " \n", + " fig, axes = plt.subplots(n_classes, 1, figsize=(16, n_classes * 3),\n", + " sharex=True, gridspec_kw={'hspace': 0.2})\n", + " \n", + " for i in range(n_classes):\n", + " axes[i].bar(np.arange(vocabulary_length), clf.tfidf_[i], color=f'C{i}')\n", + " axes[i].set_title(f'Vecteur TF-IDF pour la classe {clf.classes_[i]}')\n", + "\n", + " if vocabulary_length < 100:\n", + " axes[-1].set_xticks(np.arange(len(vocabulary)))\n", + " axes[-1].set_xticklabels(vocabulary, rotation=90)\n", + " else:\n", + " axes[-1].set_xticks([])\n", + "\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_saxvsm(saxvsm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `saxvsm_cosine_similarity` calcule la similarité cosinus entre les vecteurs TF-IDF des classes et le vecteur des fréquences des mots d'une série temporelle. La classe prédite est la classe avec la similarité cosinus la plus élevée." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def saxvsm_cosine_similarity(clf, x):\n", + " \"\"\"Prints the cosine similarity between the tf-idf vector of each\n", + " class and a univariate time series.\n", + " \n", + " Parameters\n", + " ----------\n", + " clf : SAXVSM\n", + " A fitted instance of BOSSVS.\n", + " \n", + " x : array-like, shape = (n_timestamps,)\n", + " A univariate time series.\n", + " \n", + " \"\"\"\n", + " x = np.asarray(x)\n", + " cosine_similarity = clf.decision_function(x.reshape(1, -1)).ravel()\n", + " string = '\\n'.join(\n", + " [\n", + " f\"Similarité cosinus avec la classe {class_} = {score:.6f}\"\n", + " for class_, score in zip(clf.classes_, cosine_similarity)\n", + " ] + [\n", + " f\"La classe prédite est donc la classe \"\n", + " f\"{clf.classes_[np.argmax(cosine_similarity)]}.\"\n", + " ]\n", + " )\n", + " \n", + " print(string) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "saxvsm_cosine_similarity(saxvsm, X_test[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Modifiez les valeurs de certains hyper-paramètres, affichez les vecteurs TF-IDF calculés et évaluer la performance prédictive sur le jeu de test." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Domaine fréquentiel\n", + "\n", + "Si $x = (x_0, \\ldots, x_{N-1}) \\in \\mathbb{C}^n$ est un vecteur de taille $n$, sa transformée de Fourier discrète est un vecteur $s = (s_0, \\ldots, s_{N-1}) \\in \\mathbb{C}^n$ également de taille $n$ défini par :\n", + "\n", + "$$\n", + "s_k = \\sum_{n=0}^{N-1} x_n \\cdot \\mathrm e^{-2\\mathrm i \\pi k \\frac nN} \\qquad \\text{pour} \\qquad 0 \\leqslant k < N\n", + "$$\n", + "\n", + "Les premiers coefficients correspondent aux fréquences les plus faibles (donc au signal en général), tandis que les derniers coefficients correspondent aux fréquences les plus élevées (donc au bruit en général).\n", + "Il est donc possible de garder uniquement un sous-ensemble des coefficients de Fourier (les premeirs en général) pour approcher une série temporelle.\n", + "C'est sur ce principe que reposent les algorithmes présentés dans cette section." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Symbolic Fourier Approximation (SFA)\n", + "\n", + "L'algorithme *Symbolic Fourier Approximation (SFA)* consiste à calculer les premiers coefficients de Fourier d'une série temporelle, puis de discrétiser ces coefficients afin d'obtenir un mot.\n", + "\n", + "L'algorithme est implémentée dans la classe [pyts.approximation.SymbolicFourierApproximation](https://pyts.readthedocs.io/en/stable/generated/pyts.approximation.SymbolicFourierApproximation.html).\n", + "Le code suivant illustre une utilisation simple pour transformer les séries temporelles d'un jeu de données en mots avec cet algorithme :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sfa = SymbolicFourierApproximation(n_coefs=10, drop_sum=True)\n", + "sfa.fit(X_train, y_train)\n", + "''.join(sfa.transform(X_train)[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_sfa` illustre cet algorithme :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_sfa(clf, x):\n", + " \n", + " def get_idft(x_dft, n_timestamps):\n", + " n_coefs = x_dft.size\n", + " if n_coefs % 2 == 0:\n", + " real_idx = np.arange(1, n_coefs, 2)\n", + " imag_idx = np.arange(2, n_coefs, 2)\n", + " x_dft_new = np.r_[\n", + " x_dft[:1], x_dft[real_idx] + 1j * np.r_[x_dft[imag_idx], 0]\n", + " ]\n", + " else:\n", + " real_idx = np.arange(1, n_coefs, 2)\n", + " imag_idx = np.arange(2, n_coefs + 1, 2)\n", + " x_dft_new = np.r_[\n", + " x_dft[:1], x_dft[real_idx] + 1j * x_dft[imag_idx]\n", + " ]\n", + " return np.fft.irfft(x_dft_new, n_timestamps)\n", + "\n", + " \n", + " x = np.asarray(x)\n", + " X = x.reshape(1, -1)\n", + "\n", + " # Transformations\n", + " x_dft = clf._pipeline['dft'].transform(X)[0]\n", + " n_coefs = x_dft.size\n", + " x_sfa = clf.transform(X)[0]\n", + " x_idft = get_idft(x_dft, x.size)\n", + " \n", + " # Plot figure\n", + " fig, axes = plt.subplots(\n", + " 3, 1, figsize=(12, 6),\n", + " gridspec_kw={'height_ratios': [1, 1, 2], 'hspace': 0.5}\n", + " )\n", + "\n", + " # Plot raw time series\n", + " axes[0].plot(x)\n", + " axes[0].text(0, 1.04, '(a)', fontweight='bold',\n", + " va='bottom', ha='left', transform=axes[0].transAxes)\n", + "\n", + " # Plot approximated time series with DFT\n", + " axes[1].plot(x_idft)\n", + " axes[1].text(0, 1.04, '(b)', fontweight='bold',\n", + " va='bottom', ha='left', transform=axes[1].transAxes)\n", + " axes[1].set_xlim(axes[0].get_xlim())\n", + " axes[1].set_ylim(axes[0].get_ylim())\n", + "\n", + " # Plot bin edges\n", + " for i in range(n_coefs):\n", + " axes[2].hlines(sfa.bin_edges_[i], i, i + 1, color='k')\n", + "\n", + " # Plot vertical lines to separate DFT coefficients\n", + " ylim = np.asarray(axes[2].get_ylim()) * 1.2\n", + " axes[2].vlines(np.arange(0, n_coefs + 1), ylim[0], ylim[1], color='k')\n", + "\n", + " # Fill rectangles with colors as a legend\n", + " for i in range(n_coefs):\n", + " for k, color in enumerate(('C4', 'C9', 'C8', 'C5')):\n", + " y1 = ylim[0] if k == 0 else sfa.bin_edges_[i, k - 1]\n", + " y2 = ylim[1] if k == clf.n_bins - 1 else sfa.bin_edges_[i, k]\n", + " axes[2].fill_between([i, i + 1], y1, y2, color=color, alpha=0.25)\n", + " axes[2].set_xticks(np.arange(0.5, n_coefs + 0.5))\n", + " axes[2].set_xlabel('Discrete Fourier Transform coefficients', fontsize=8, labelpad=0)\n", + " axes[2].set_xticklabels([])\n", + " axes[2].legend(handles=[\n", + " Patch(facecolor='C4', edgecolor='k', label='a', alpha=0.25),\n", + " Patch(facecolor='C9', edgecolor='k', label='b', alpha=0.25),\n", + " Patch(facecolor='C8', edgecolor='k', label='c', alpha=0.25),\n", + " Patch(facecolor='C7', edgecolor='k', label='d', alpha=0.25),\n", + " ], loc=(1.01, 0.55), fontsize=9)\n", + "\n", + " # Display DFT coefficients\n", + " ylim_min, ylim_max = axes[2].get_ylim()\n", + " for j in range(n_coefs):\n", + " axes[2].text(0.5 + j, ylim_max + 0.05 * (ylim_max - ylim_min), '{:.3f}'.format(x_dft[j]),\n", + " color='C0', ha='center', va='center', fontsize=12)\n", + "\n", + " # Display SFA words\n", + " for j in range(n_coefs):\n", + " axes[2].text(0.5 + j, ylim_min - 0.2 * (ylim_max - ylim_min), '{}'.format(x_sfa[j]),\n", + " color='C0', ha='center', va='center', fontsize=20)\n", + "\n", + " axes[2].text(0, 1.02, '(c)', fontweight='bold',\n", + " va='bottom', ha='left', transform=axes[2].transAxes)\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Par exemple, voici la transformation obtenue pour la première série temporelle du jeu d'entraînement :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_sfa(sfa, X_train[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bag of Symbolic Fourier Approximation Symbols (BOSS)\n", + "\n", + "L'algorithme *Bag of Symbolic Fourier Approximation Symbols (BOSS)* est assez similaire à l'algorithme *Bag of Patterns*, mais avec deux différences :\n", + "* Chaque sous série temporelle est transformée en mot avec l'algorithme SFA au lieu de SAX.\n", + "* La métrique utilisée dans l'algorithme des plus proches voisins est une version modifiée de la distance euclidienne : seul les mots présents dans la série temporelle de référence sont pris en compte.\n", + "\n", + "L'algorithme est implémentée dans la classe [pyts.transformation.BOSS](https://pyts.readthedocs.io/en/stable/generated/pyts.transformation.BOSS.html). Le code suivant illustre une utilisation simple pour entraîner un modèle sur un jeu d'entraînement et l'évaluer sur un jeu de test :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "boss = Pipeline([\n", + " ('boss', BOSS(word_size=3, n_bins=4, sparse=False)),\n", + " ('1nn', KNeighborsClassifier(metric='boss'))\n", + "])\n", + "boss.fit(X_train, y_train)\n", + "boss.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_boss` illustre l'algorithme BOSS :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_boss(clf, x):\n", + " \"\"\"\n", + " Parameters\n", + " ----------\n", + " clf : BOSSVS\n", + " A fitted instance of BOSSVS.\n", + "\n", + " x : array-like, shape = (n_timestamps, )\n", + " A univariate time series.\n", + "\n", + " \"\"\"\n", + "\n", + " from pyts.utils import windowed_view\n", + " \n", + " def get_sfa_words():\n", + " \n", + " X_windowed = windowed_view(x.reshape(1, -1), window_size, window_step)\n", + " X_windowed = X_windowed.reshape(n_windows, window_size)\n", + "\n", + " sfa = SymbolicFourierApproximation(\n", + " n_coefs=clf_.word_size, n_bins=clf_.n_bins, strategy=clf_.strategy,\n", + " drop_sum=clf_.drop_sum, anova=clf_.anova, norm_mean=clf_.norm_mean,\n", + " norm_std=clf_.norm_std, alphabet=clf_.alphabet\n", + " )\n", + " X_sfa = sfa.fit_transform(X_windowed, None)\n", + "\n", + " X_word = np.asarray([''.join(X_sfa[i]) for i in range(n_windows)])\n", + "\n", + " X_word = X_word.reshape(1, n_windows)\n", + " not_equal_idx = np.c_[np.full(1, True), X_word[:, 1:] != X_word[:, :-1]]\n", + " return X_word[0], not_equal_idx[0]\n", + "\n", + " if isinstance(clf, Pipeline):\n", + " clf_ = clf['boss']\n", + " else:\n", + " clf_ = clf\n", + "\n", + " x = np.asarray(x)\n", + " n_timestamps = x.size\n", + " window_size, window_step = clf_._check_params(n_timestamps)\n", + " n_windows = (n_timestamps - window_size + window_step) // window_step\n", + "\n", + " fig, axes = plt.subplots(3, 1, figsize=(16, 8), gridspec_kw={'hspace': 0.5})\n", + "\n", + " # Plot raw time series\n", + " axes[0].plot(x)\n", + " xlim = axes[0].get_xlim()\n", + " ylim = axes[0].get_ylim()\n", + "\n", + " axes[0].text(0, 1.04, r'(a)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[0].transAxes)\n", + "\n", + " # Plot SFA words\n", + " x_word, not_equal_idx = get_sfa_words()\n", + "\n", + " for i in range(n_windows):\n", + " if clf_.numerosity_reduction:\n", + " alpha = 1. if not_equal_idx[i] else 0.2\n", + " fontweight = 'bold' if not_equal_idx[i] else 'normal'\n", + " else:\n", + " alpha = 1.\n", + " fontweight = 'bold'\n", + " axes[1].text(i, 1.2 - (i % (clf_.word_size + 5)) / 5, x_word[i],\n", + " color='C0', alpha=alpha, fontsize=8, fontweight=fontweight)\n", + "\n", + " axes[1].set_xlim(xlim)\n", + " axes[1].set_axis_off()\n", + "\n", + " axes[1].text(0., 1.26, '(b)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[1].transAxes)\n", + "\n", + " # Plot BOSS histogram\n", + " X_boss = clf_.transform(x.reshape(1, -1))\n", + " x_boss = X_boss[0]\n", + "\n", + " vocabulary_length = len(clf_.vocabulary_)\n", + " vocabulary = np.vectorize(clf_.vocabulary_.get)(np.arange(x_boss.size))\n", + " axes[2].bar(np.arange(np.sum(x_boss > 0)), x_boss[x_boss > 0])\n", + " axes[2].set_xticks(np.arange(np.sum(x_boss > 0)))\n", + " axes[2].set_xticklabels(vocabulary[x_boss > 0], rotation=90)\n", + "\n", + " axes[2].text(0, 1.04, '(c)', fontweight='bold', va='bottom',\n", + " ha='left', transform=axes[2].transAxes)\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Par exemple, voici la transformation obtenue pour la première série temporelle du jeu d'entraînement :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_boss(boss, X_train[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Changez les valeurs de certains hyper-paramètres, observez comment la représentation obtenue change et regardez la performance prédictive obtenue. Si vous êtes familier avec `scikit-learn`, vous pouvez également essayer un autre algorithme de classification. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bag of Symbolic Fourier Approximation Symbols in Vector Space (BOSSVS)\n", + "\n", + "L'idée de l'algorithme *Bag of Symbolic Fourier Approximation Symbols in Vector Space (BOSSVS)* est similaire à celle de l'algorithme *SAXVSM* : chaque classe est représentée par un vecteur représentant les fréquences (pondérées) des mots dans les séries temporelles de cette classe. La prédiction pour une nouvelle série temporelle correspond à la classe dont le vecteur est le plus similaire au vecteur de la série temporelle.\n", + "\n", + "L'algorithme est implémentée dans la classe [pyts.classification.BOSSVS](https://pyts.readthedocs.io/en/stable/generated/pyts.classification.BOSSVS.html). Le code suivant illustre une utilisation simple pour entraîner un modèle sur un jeu d'entraînement et l'évaluer sur un jeu de test :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bossvs = BOSSVS(window_size=30, word_size=3, n_bins=4)\n", + "bossvs.fit(X_train, y_train)\n", + "bossvs.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `plot_bossvs` illustre les vecteurs appris pour chaque classe :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_bossvs(clf):\n", + " n_classes = len(clf.classes_)\n", + " vocabulary_length = len(clf.vocabulary_)\n", + " vocabulary = np.vectorize(clf.vocabulary_.get)(np.arange(vocabulary_length))\n", + " \n", + " fig, axes = plt.subplots(n_classes, 1, figsize=(16, n_classes * 3),\n", + " sharex=True, gridspec_kw={'hspace': 0.2})\n", + " \n", + " for i in range(n_classes):\n", + " axes[i].bar(np.arange(vocabulary_length), clf.tfidf_[i], color=f'C{i}')\n", + " axes[i].set_title(f'Vecteur TF-IDF pour la classe {clf.classes_[i]}')\n", + "\n", + " if vocabulary_length < 100:\n", + " axes[-1].set_xticks(np.arange(len(vocabulary)))\n", + " axes[-1].set_xticklabels(vocabulary, rotation=90)\n", + " else:\n", + " axes[-1].set_xticks([])\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_bossvs(bossvs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "La fonction `bossvs_cosine_similarity` calcule la similarité cosinus entre les vecteurs TF-IDF des classes et le vecteur des fréquences des mots d'une série temporelle. La classe prédite est la classe avec la similarité cosinus la plus élevée." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def bossvs_cosine_similarity(clf, x):\n", + " \"\"\"Prints the cosine similarity between the tf-idf vector of each\n", + " class and a univariate time series.\n", + " \n", + " Parameters\n", + " ----------\n", + " clf : BOSSVS\n", + " A fitted instance of BOSSVS.\n", + " \n", + " x : array-like, shape = (n_timestamps,)\n", + " A univariate time series.\n", + " \n", + " \"\"\"\n", + " x = np.asarray(x)\n", + " cosine_similarity = clf.decision_function(x.reshape(1, -1)).ravel()\n", + " string = '\\n'.join(\n", + " [\n", + " f\"Similarité cosinus avec la classe {class_} = {score:.6f}\"\n", + " for class_, score in zip(clf.classes_, cosine_similarity)\n", + " ] + [\n", + " f\"La classe prédite est donc la classe \"\n", + " f\"{clf.classes_[np.argmax(cosine_similarity)]}.\"\n", + " ]\n", + " )\n", + " \n", + " print(string) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bossvs_cosine_similarity(bossvs, X_test[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Modifiez les valeurs de certains hyper-paramètres, affichez les vecteurs TF-IDF calculés et évaluer la performance prédictive sur le jeu de test." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Modèles ensemblistes\n", + "\n", + "En pratique, utiliser un seul modèle (c'est-à-dire une seule combinaison d'hyper-paramètres) peut limiter la performance prédictive. Pour y remédier, il est souvent conseillé d'utiliser un modèle ensembliste (c'est-à-dire un ensemble de modèles avec différentes combinaisons d'hyper-paramètres).\n", + "\n", + "Le code suivant illustre un ensemble de modèles BOSSVS avec différentes tailles de fenêtre glissante et différentes longueurs de mots :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bossvs_ensemble = VotingClassifier([\n", + " (f'bossvs_{window_size}_{word_size}', BOSSVS(word_size=word_size, window_size=window_size))\n", + " for window_size in range(10, 30, 5)\n", + " for word_size in range(3, 7)\n", + "])\n", + "\n", + "bossvs_ensemble.fit(X_train, y_train)\n", + "bossvs_ensemble.score(X_test, y_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercice** : Créer un autre modèle ensembliste constitué de plusieurs algorithmes vus au cours de ce tutoriel. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/bag_of_words.pdf b/docs/bag_of_words.pdf new file mode 100644 index 0000000..3e8db0f Binary files /dev/null and b/docs/bag_of_words.pdf differ diff --git a/docs/resources.md b/docs/resources.md index f47427f..5e380b2 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -2,11 +2,14 @@ * [Lien vers la doc `tslearn`](https://tslearn.readthedocs.io) * Articles de blog au sujet de [la DTW](https://rtavenar.github.io/blog/dtw.html) et de [la softDTW](https://rtavenar.github.io/blog/softdtw.html) +* [Lien vers la documentation de `pyts`](https://pyts.readthedocs.io/en/stable/) ## Notebooks utilisés * [Partie "tslearn" : métriques de comparaison de séries temporelles](dtw.ipynb) +* [Partie "pyts" : représentation par sacs de mots et classification](bag_of_words.ipynb) ## Diaporamas projetés * [Partie "tslearn" : métriques de comparaison de séries temporelles](dtw.pdf) +* [Partie "pyts" : représentation par sacs de mots et classification](bag_of_words.pdf)