{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Produit matriciel avec une matrice creuse\n", "\n", "Les dictionnaires sont une fa\u00e7on assez de repr\u00e9senter les matrices creuses en ne conservant que les coefficients non nuls. Comment \u00e9crire alors le produit matriciel ?"]}, {"cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [{"data": {"text/html": ["
run previous cell, wait for 2 seconds
\n", ""], "text/plain": [""]}, "execution_count": 2, "metadata": {}, "output_type": "execute_result"}], "source": ["from jyquickhelper import add_notebook_menu\n", "add_notebook_menu()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Matrice creuse et dictionnaire\n", "\n", "Une [matrice creuse](https://fr.wikipedia.org/wiki/Matrice_creuse) ou [sparse matrix](https://en.wikipedia.org/wiki/Sparse_matrix) est constitu\u00e9e majoritairement de 0. On utilise un dictionnaire avec les coefficients non nuls. La fonction suivante pour cr\u00e9er une matrice al\u00e9atoire."]}, {"cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(2, 2): 1, (0, 1): 1, (1, 1): 1, (2, 0): 1, (1, 0): 1}"]}, "execution_count": 3, "metadata": {}, "output_type": "execute_result"}], "source": ["import random\n", "\n", "def random_matrix(n, m, ratio=0.1):\n", " mat = {}\n", " nb = min(n * m, int(ratio * n * m + 0.5))\n", " while len(mat) < nb:\n", " i = random.randint(0, n-1)\n", " j = random.randint(0, m-1)\n", " mat[i, j] = 1\n", " last = (n - 1, m - 1)\n", " if last not in mat:\n", " # pour \u00eatre s\u00fbr que la matrice a les bonnes dimensions\n", " mat[last] = 0\n", " return mat\n", "\n", "mat = random_matrix(3, 3, ratio=0.5)\n", "mat"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Calcul de la dimension\n", "\n", "Pour obtenir la dimension de la matrice, il faut parcourir toutes les cl\u00e9s du dictionnaire."]}, {"cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [{"data": {"text/plain": ["(3, 3)"]}, "execution_count": 4, "metadata": {}, "output_type": "execute_result"}], "source": ["def dimension(mat):\n", " maxi, maxj = 0, 0\n", " for k in mat:\n", " maxi = max(maxi, k[0])\n", " maxj = max(maxj, k[1])\n", " return maxi + 1, maxj + 1\n", "\n", "dimension(mat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Cette fonction poss\u00e8de l'inconv\u00e9nient de retourner une valeur fausse si la matrice ne poss\u00e8de aucun coefficient non nul sur la derni\u00e8re ligne ou la derni\u00e8re colonne. Cela peut \u00eatre embarrassant, tout d\u00e9pend de l'usage."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Produit matriciel classique\n", "\n", "On impl\u00e9mente le produit matriciel classique, \u00e0 trois boucles."]}, {"cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(0, 0): 1, (1, 1): 1}"]}, "execution_count": 5, "metadata": {}, "output_type": "execute_result"}], "source": ["def produit_classique(m1, m2):\n", " dim1 = dimension(m1)\n", " dim2 = dimension(m2)\n", " if dim1[1] != dim2[0]:\n", " raise Exception(\"Impossible de multiplier {0}, {1}\".format(dim1, dim2))\n", " res = {}\n", " for i in range(dim1[0]):\n", " for j in range(dim2[1]):\n", " s = 0\n", " for k in range(dim1[1]):\n", " s += m1.get((i, k), 0) * m2.get((k, j), 0)\n", " if s != 0: # Pour \u00e9viter de garder les coefficients non nuls.\n", " res[i, j] = s \n", " return res\n", "\n", "simple = {(0, 1): 1, (1, 0): 1}\n", "produit_classique(simple, simple)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Sur la matrice al\u00e9atoire..."]}, {"cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(0, 0): 1, (0, 1): 1, (1, 0): 1, (1, 1): 2, (2, 0): 1, (2, 1): 1, (2, 2): 1}"]}, "execution_count": 6, "metadata": {}, "output_type": "execute_result"}], "source": ["produit_classique(mat, mat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Produit matriciel plus \u00e9l\u00e9gant\n", "\n", "A-t-on vraiment besoin de s'enqu\u00e9rir des dimensions de la matrice pour faire le produit matriciel ? Ne peut-on pas tout simplement faire une boucle sur les coefficients non nul ?"]}, {"cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(0, 0): 1, (1, 1): 1}"]}, "execution_count": 7, "metadata": {}, "output_type": "execute_result"}], "source": ["def produit_elegant(m1, m2):\n", " res = {}\n", " for (i, k1), v1 in m1.items():\n", " if v1 == 0:\n", " continue\n", " for (k2, j), v2 in m2.items(): \n", " if v2 == 0:\n", " continue\n", " if k1 == k2:\n", " if (i, j) in res:\n", " res[i, j] += v1 * v2\n", " else :\n", " res[i, j] = v1 * v2\n", " return res\n", "\n", "produit_elegant(simple, simple)"]}, {"cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(2, 2): 1, (2, 0): 1, (0, 1): 1, (0, 0): 1, (1, 1): 2, (1, 0): 1, (2, 1): 1}"]}, "execution_count": 8, "metadata": {}, "output_type": "execute_result"}], "source": ["produit_elegant(mat, mat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Mesure du temps\n", "\n", "A priori, la seconde m\u00e9thode est plus rapide puisque son co\u00fbt est proportionnel au produit du nombre de coefficients non nuls dans les deux matrices. V\u00e9rifions."]}, {"cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["414 ms \u00b1 29 ms per loop (mean \u00b1 std. dev. of 7 runs, 1 loop each)\n"]}], "source": ["bigmat = random_matrix(100, 100)\n", "%timeit produit_classique(bigmat, bigmat)"]}, {"cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["99.9 ms \u00b1 6.22 ms per loop (mean \u00b1 std. dev. of 7 runs, 10 loops each)\n"]}], "source": ["%timeit produit_elegant(bigmat, bigmat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["C'est beaucoup mieux. Mais peut-on encore faire mieux ?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Dictionnaires de dictionnaires\n", "\n", "Ca sonne un peu comme [mille millions de mille sabords](https://fr.wikipedia.org/wiki/Vocabulaire_du_capitaine_Haddock) mais le dictionnaire que nous avons cr\u00e9\u00e9 a pour cl\u00e9 un couple de coordonn\u00e9es et valeur des coefficients. La fonction ``produit_elegant`` fait plein d'it\u00e9rations inutiles en quelque sorte puisque les coefficients sont nuls. Peut-on \u00e9viter \u00e7a ?\n", "\n", "Et si on utilisait des dictionnaires de dictionnaires : ``{ ligne : { colonne : valeur } }``."]}, {"cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [{"data": {"text/plain": ["{0: {1: 1}, 1: {0: 1}}"]}, "execution_count": 11, "metadata": {}, "output_type": "execute_result"}], "source": ["def matrice_dicodico(mat):\n", " res = {}\n", " for (i, j), v in mat.items():\n", " if i not in res:\n", " res[i] = {j: v}\n", " else:\n", " res[i][j] = v\n", " return res\n", "\n", "matrice_dicodico(simple)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Peut-on adapter le calcul matriciel \u00e9l\u00e9gant ? Il reste \u00e0 associer les indices de colonnes de la premi\u00e8re avec les indices de lignes de la seconde. Cela pose probl\u00e8me en l'\u00e9tat quand les indices de colonnes sont inaccessibles sans conna\u00eetre les indices de lignes d'abord \u00e0 moins d'\u00e9changer l'ordre pour la seconde matrice."]}, {"cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [{"data": {"text/plain": ["{1: {0: 1}, 0: {1: 1}}"]}, "execution_count": 12, "metadata": {}, "output_type": "execute_result"}], "source": ["def matrice_dicodico_lc(mat, ligne=True):\n", " res = {}\n", " if ligne:\n", " for (i, j), v in mat.items():\n", " if i not in res:\n", " res[i] = {j: v}\n", " else:\n", " res[i][j] = v\n", " else:\n", " for (j, i), v in mat.items():\n", " if i not in res:\n", " res[i] = {j: v}\n", " else:\n", " res[i][j] = v\n", " return res\n", "\n", "matrice_dicodico_lc(simple, ligne=False)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Maintenant qu'on a fait \u00e7a, on peut songer au produit matriciel."]}, {"cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(0, 0): 1, (1, 1): 1}"]}, "execution_count": 13, "metadata": {}, "output_type": "execute_result"}], "source": ["def produit_elegant_rapide(m1, m2):\n", " res = {}\n", " for k, vs in m1.items():\n", " if k in m2:\n", " for i, v1 in vs.items():\n", " for j, v2 in m2[k].items():\n", " if (i, j) in res:\n", " res[i, j] += v1 * v2\n", " else :\n", " res[i, j] = v1 * v2\n", "\n", " return res\n", "\n", "m1 = matrice_dicodico_lc(simple, ligne=False)\n", "m2 = matrice_dicodico_lc(simple)\n", "produit_elegant_rapide(m1, m2)"]}, {"cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(2, 2): 1, (2, 0): 1, (0, 1): 1, (0, 0): 1, (1, 1): 2, (1, 0): 1, (2, 1): 1}"]}, "execution_count": 14, "metadata": {}, "output_type": "execute_result"}], "source": ["m1 = matrice_dicodico_lc(mat, ligne=False)\n", "m2 = matrice_dicodico_lc(mat)\n", "produit_elegant_rapide(m1, m2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["On mesure le temps avec une grande matrice."]}, {"cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["3.25 ms \u00b1 193 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 100 loops each)\n"]}], "source": ["m1 = matrice_dicodico_lc(bigmat, ligne=False)\n", "m2 = matrice_dicodico_lc(bigmat)\n", "%timeit produit_elegant_rapide(m1, m2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Beaucoup plus rapide, il n'y a plus besoin de tester les coefficients non nuls. La comparaison n'est pas tr\u00e8s juste n\u00e9anmoins car il faut transformer les deux matrices avant de faire le calcul. Et si on l'incluait ?"]}, {"cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [{"data": {"text/plain": ["{(0, 0): 1, (1, 1): 1}"]}, "execution_count": 16, "metadata": {}, "output_type": "execute_result"}], "source": ["def produit_elegant_rapide_transformation(mat1, mat2):\n", " m1 = matrice_dicodico_lc(mat1, ligne=False)\n", " m2 = matrice_dicodico_lc(mat2)\n", " return produit_elegant_rapide(m1, m2)\n", "\n", "produit_elegant_rapide_transformation(simple, simple)"]}, {"cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["3.58 ms \u00b1 259 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 100 loops each)\n"]}], "source": ["%timeit produit_elegant_rapide_transformation(bigmat, bigmat)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finalement \u00e7a vaut le coup... mais est-ce le cas pour toutes les matrices."]}, {"cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": ["%matplotlib inline"]}, {"cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [{"name": "stdout", "output_type": "stream", "text": ["{'dicodico': 0.012003499999998724, 'ratio': 0.1, 'dico': 0.10777440000000027, 'classique': 0.3187974000000011}\n", "{'dicodico': 0.014974200000001048, 'ratio': 0.2, 'dico': 0.3366258000000002, 'classique': 0.3435348000000005}\n", "{'dicodico': 0.03159210000000101, 'ratio': 0.3, 'dico': 0.7631177000000022, 'classique': 0.3299744000000011}\n", "{'dicodico': 0.05182399999999987, 'ratio': 0.4, 'classique': 0.36676549999999963}\n", "{'dicodico': 0.09054219999999802, 'ratio': 0.5, 'classique': 0.3490755000000014}\n", "{'dicodico': 0.10983819999999866, 'ratio': 0.6, 'classique': 0.382511700000002}\n", "{'dicodico': 0.2340966999999985, 'ratio': 0.7, 'classique': 0.36933710000000275}\n", "{'dicodico': 0.3196471999999986, 'ratio': 0.8, 'classique': 0.40243699999999905}\n", "{'dicodico': 0.35430310000000276, 'ratio': 0.9, 'classique': 0.44061049999999824}\n", "{'dicodico': 0.3136302999999998, 'ratio': 0.99, 'classique': 0.4038351999999996}\n"]}], "source": ["import time\n", "mesures = []\n", "\n", "for ratio in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.99]:\n", " big = random_matrix(100, 100, ratio=ratio)\n", " \n", " t1 = time.perf_counter()\n", " produit_elegant_rapide_transformation(big, big)\n", " t2 = time.perf_counter()\n", " dt = (t2 - t1)\n", " obs = {\"dicodico\": dt, \"ratio\": ratio}\n", " \n", " if ratio <= 0.3:\n", " # apr\u00e8s c'est trop lent\n", " t1 = time.perf_counter()\n", " produit_elegant(big, big)\n", " t2 = time.perf_counter()\n", " dt = (t2 - t1)\n", " obs[\"dico\"] = dt\n", " \n", " t1 = time.perf_counter()\n", " produit_classique(big, big)\n", " t2 = time.perf_counter()\n", " dt = (t2 - t1)\n", " obs[\"classique\"] = dt\n", "\n", " mesures.append(obs)\n", " print(obs) "]}, {"cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [{"data": {"image/png": "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\n", "text/plain": ["
"]}, "metadata": {"needs_background": "light"}, "output_type": "display_data"}], "source": ["from pandas import DataFrame\n", "df = DataFrame(mesures)\n", "ax = df.plot(x=\"ratio\", y=\"dicodico\", label=\"dico dico\")\n", "df.plot(x=\"ratio\", y=\"dico\", label=\"dico\", ax=ax)\n", "df.plot(x=\"ratio\", y=\"classique\", label=\"classique\", ax=ax)\n", "ax.legend();"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Cette derni\u00e8re version est efficace."]}, {"cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": []}], "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.7.2"}}, "nbformat": 4, "nbformat_minor": 2}