{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Langages de script – Python\n", "\n", "## Modèle vectoriel\n", "\n", "### M2 Ingénierie Multilingue – INaLCO\n", "\n", "clement.plancq@ens.fr" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Modélisation vectorielle des documents\n", "Modèle symbolique : un document est une suite de symboles (mots)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "with open(\"zola_ventre-de-paris.txt\") as in_stream:\n", " doc = [word for line in in_stream for word in line.strip().split()]\n", "doc[5:10] " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ " - Quand on peut/veut écrire des règles de traitement à la main, c'est pratique et Unitex est votre ami\n", " - Pour faire de l'apprentissage/non-supervisé, on ne sait pas bien le gérer\n", " - Parce que l'apprentissage artificiel c'est des maths, et que les maths discrètes c'est dur™" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Ce qu'on sait bien gérer : les nombres !\n", "\n", " - On va donc plutôt représenter nos documents par des nombres\n", " - Ou plutôt des suites de nombres\n", " \n", "Le problème est donc : comment représenter des documents par une suite de nombres en conservant les informations qu'on veut exploiter ?" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Les sacs de mots\n", "\n", "Idée : représenter un document par la distribution brute de son lexique (le nombre d'occurences de chaque mot)\n", "\n", " - On l'a déjà fait dans [le TP vocabulaire commun](voc-commun.ipynb)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from collections import Counter\n", "with open(\"zola_ventre-de-paris.txt\") as in_stream:\n", " doc1 = Counter(word for line in in_stream for word in line.strip().split())\n", "doc1" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ " - C'est assez primitif, mais ça marche étonnamment bien\n", " - Pour pouvoir faire des comparaisons entre documents il faut utiliser le même lexique pour tous" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with open(\"balzac_petits-bourgeois.txt\") as in_stream:\n", " doc2 = Counter(word for line in in_stream for word in line.strip().split())\n", "\n", "voc = sorted(set(doc1.keys()).union(doc2.keys()))\n", "vec_1 = [doc1[word] for word in voc]\n", "vec_2 = [doc2[word] for word in voc]\n", "\n", "vec_1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ici `vec_1` et `vec_2` sont des listes des nombres (des *vecteurs*) de même longueur auxquelles on va pouvoir appliquer les merveilleux outils que nous offre la statistique." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Exercice\n", " - Écrire un script qui prend en entrée un dossier contenant des documents (sous forme de fichier textes) et sort un fichier TSV donnant pour chaque document sa représentation en sac de mots (en nombre d'occurences des mots du vocabulaire commun)\n", " - Dans le sens habituel : un fichier par ligne, un mot par colonne\n", " - Pour itérer sur les fichiers dans un dossier on peut utiliser `for f in pathlib.Path(chemin_du_dossier).glob('*')`\n", " - Pour récupérer des arguments en ligne de commande : [`argparse`](https://docs.python.org/3/library/argparse.html) ou [`sys.argv`](https://docs.python.org/3/library/argparse.html)\n", " - Tester sur la partie positive du [mini-corpus imdb](imdb_smol.tar.gz)\n", "\n", "On pourrait déjà se servir de ce genre de représentation pour faire des travaux intéressants, au hasard de la classification de documents, pourquoi pas avec des SVMs…" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Listes de nombres\n", "### Opérations sur les lignes\n", "Les sacs de mots en nombre d'occurences c'est bien gentil, mais si on a des documents de tailles homogènes on a vite des problèmes" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "# Quick 'n dirty BOWization\n", "import re\n", "import math\n", "\n", "def to_bow(texts):\n", " freqs = [Counter(re.split(r'\\W+', t.lower())) for t in texts]\n", " voc = sorted(set().union(*[c.keys() for c in freqs]))\n", " return [[c[word] for word in voc] for c in freqs]\n", "\n", "def l2(b1, b2):\n", " return math.sqrt(sum((x-y)**2 for x, y in zip(b1, b2)))\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "d1 = \"Un crocodile\"\n", "d2 = \"Un crocodile, un crocodile, un crocodile\"\n", "d3 = \"Un éléphant\"\n", "\n", "b1, b2, b3 = to_bow([d1, d2, d3])\n", "\n", "print(\n", " '\\n'.join('\\t'.join([str(x) for x in l]) for l in [b1, b2, b3])\n", ")\n", "\n", "print(l2(b1, b2))\n", "print(l2(b1, b3))\n" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Heureusement ce n'est pas très compliqué de normaliser pour avoir des fréquences relatives" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n1 = [x/sum(b1) for x in b1]\n", "n2 = [x/sum(b2) for x in b2]\n", "n3 = [x/sum(b3) for x in b3]\n", "\n", "print(l2(n1, n2))\n", "print(l2(n1, n3))\n", "print(n1, n2, n3)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "#### Exercice\n", "Modifier le script précédent pour qu'il génère des sacs de mots utilisant les fréquences relatives plutot que les nombres d'occurences" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Opérations sur les colonnes\n", " - Fréquences relatives : sont des bons indicateurs du contenu lexical d'**un** texte\n", " - Mais pas forcément suffisant pour de la fouille de corpus\n", " - On a besoin d'indicateurs relatifs à l'ensemble des textes\n", "\n", "Par exemple : un indicateur de spécificité assez grossier d'un mot $W$ dans un texte $T$.\n", "$$\n", "S(W, T) = \\frac{\\text{nombre d'occurrences de W dans le texte T}}{\\text{nombre d'occurences de W dans l'ensemble du corpus}}\n", "$$\n", "\n", "Le calcul pour un seul mot n'est pas beaucoup plus compliqué que le calcul des fréquences relatives" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d1 = \"Un crocodile\"\n", "d2 = \"Un crocodile, un crocodile, un crocodile\"\n", "d3 = \"Un éléphant\"\n", "\n", "bows = to_bow([d1, d2, d3])\n", "total_crocodile = sum(l[0] for l in bows)\n", "[l[0]/total_crocodile for l in bows]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ " - Si on veut le faire pour tous les mots c'est un peu plus pénible à écrire\n", " - Et encore : c'est une des opérations les plus simples !\n", "\n", "Une solution c'est de voir l'ensemble des sacs-de-mots non plus comme une liste de listes, mais comme un tableau à deux dimensions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('\\n'.join(' '.join([str(x) for x in l]) for l in bows))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Avec cette vision, les deux opérations sont en fait quasiment identiques\n", "\n", " - Pour calculer les fréquences relatives, on divise chaque ligne par sa somme\n", " - Pour calculer les spécificités, on divise chaque colonne par sa somme\n", " \n", "Il ne nous manque plus qu'une interface agréable pour le faire" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Numpy à la rescousse\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Numpy](http://numpy.org) est une bibliothèque de calcul numérique pour Python. Elle est à la base de nombreux autres (dont [`scikit-learn`](https://scikit-learn.org) et [`gensim`](https://radimrehurek.com/gensim) dont on reparlera) et est assez incontournable si on veut faire du calcul en Python.\n", "\n", "Ses possibilités sont très nombreuses, mais on se contentra ici de ses fonctions de base : la manipulation de tableaux de nombres." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### `numpy.array`\n", "\n", "La classe de base de numpy est [`numpy.ndarray`](https://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html), qui permet de représenter des tableaux de nombres de dimensions arbitraires (chez les informaticiens on appelle ça des **tenseurs**, ce qui fait ricaner les mathématiciens).\n", "\n", "On les créé en général avec la fonction [`numpy.array`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.array.html)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n", "a" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "Il y a [beaucoup de façons de créer des `ndarray`s](https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html#routines-array-creation). Comme toujours, il est recommandé de jeter un œil sur [la doc](https://docs.scipy.org/doc/numpy)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "a" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "Contrairement à nos listes de listes précédentes, où on n'a accès qu'à une dimension à la fois, ici on peut accéder librement aux deux dimensions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(a[0, 0])\n", "print(a[2, 1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Par convention, les coordonnées sont données dans l'ordre $(\\text{ligne}, \\text{colonne})$." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "a" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "On peut aussi — comme pour les listes habituelles de Python — adresser des tranches" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a[0, 1:3]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a[:2, 1]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a[1:3, :2]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a[0, :]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "a" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le tableau reste quand même une séquence indexable, sur laquelle on peut itérer, dans l'ordre de ses dimensions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a[0][1] # Strictement équivalent à `a[0, 1]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for l in a:\n", " print(l)\n", " for x in l:\n", " print(x)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Manipulations de tableaux\n", "Quelques fonctions bien pratiques" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.max()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.min()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.mean()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.sum()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.transpose()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a.sum(axis=0)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Opérations\n", "Les opérations habituelles sont définies et ont le sens qu'on a envie qu'elles aient" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a*2" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a/3" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a + 4" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a - 5" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "a" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "Les opérations sont aussi définies entre deux tableaux" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b = np.array([[1, 2, 3], [4, 5, 6], [0, 0, 0]])\n", "b" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a+b" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a*b" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a/b" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Broadcasting\n", "Une notion un peu plus compliquée mais qui va nous servir tout de suite" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "a" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c = np.array([2, 4, 8])\n", "c" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a*c" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Explication : si un des tableaux a moins de dimensions que l'autre, numpy fait automatiquement la conversion pour que tout se passe comme si on avait multiplié par" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.broadcast_to(c, [3,3])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multiplier par un tableau à une dimension revient donc à multiplier colonne par colonne" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Revenons à nos moutons\n", "Rappel : on avait une liste de sacs de mots et on voulait obtenir un tableau de spécificités" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bows" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Il suffit de sommer chaque colonne" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "bows_array = np.array(bows)\n", "cols_total = bows_array.sum(axis=0)\n", "print(bows_array)\n", "print(cols_total)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Puis de diviser" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bows_array/cols_total" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Exercice\n", "Modifier le script de BoWization précédent pour qu'il renvoie non plus les fréquences relatives de chaque mot mais leur tf⋅idf avec la définition suivante pour un mot $w$, un document $D$ et un corpus $C$\n", "\n", " - $\\mathrm{tf}(w, D)$ est la fréquence relative de $w$ dans $D$\n", " - $$\\mathrm{idf}(w, C) = \\log\\!\\left(\\frac{\\text{nombre de documents dans $C$}}{\\text{nombre de documents de $C$ qui contiennent $w$}}\\right)$$\n", " - $\\log$ est le logarithme naturel [`np.log`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.log.html)\n", " - $\\mathrm{tfidf}(w, D, C) = \\mathrm{tf}(w, D)×\\mathrm{idf}(w, C)$\n", " \n", "Pistes de recherche:\n", " - L'option `keepdims` de `np.sum`\n", " - `np.transpose`\n", " - `np.count_nonzero`\n", " - Regarder ce que donne `np.array([[1, 0], [2, 0]]) > 0`" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Représentations denses de documents\n", "Dans les représentations qu'on a vu, les vecteurs qui composent chaque texte sont essentiellement composés de zéros\n", " \n", " - On dit qu'il s'agit de vecteurs *creux*\n", " - Ils sont aussi de très haute dimensions : il y a en général plus de mots que de documents\n", " - Merci la loi de Zipf\n", " \n", "Pour certaines applications ce n'est pas un problème\n", " - Les classifieurs SVM et *Random Forest* fonctionnent bien avec ce genre de données\n", "\n", "Mais pour les réseaux de neurones, par exemple, c'est très peu efficace." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "Il y a plusieurs autres solutions:\n", "\n", " - Doc2vec et al. → voir [gensim](https://radimrehurek.com/gensim/models/doc2vec.html)\n", " - Utiliser des techniques de réduction de dimension → voir [le notebook visualisation](visualisation.ipynb)" ] } ], "metadata": { "celltoolbar": "Slideshow", "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.7.1" }, "livereveal": { "scroll": true } }, "nbformat": 4, "nbformat_minor": 2 }