{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "TQwgNec5KDc7" }, "source": [ "Word2Vec\n", "========" ] }, { "cell_type": "markdown", "metadata": { "id": "XTR7HAz6KDdA" }, "source": [ "Introducción\n", "------------\n", "\n", "Word2Vec es un popular algoritmo desarrollado por Tomáš Mikolov en varias publicaciones [Mikolov et al., 2013b,a] que utiliza el concepto de representaciones densas y distribuidas. La idea principal de Word2Vec es que el significado de una palabra puede inferirse del contexto en el que aparece. En este sentido, el texto es **simplemente una secuencia de palabras donde el significado de cada una de ellas se puede entraer de las palabras que están antes y despues de la misma**. Podríamos decir que busca codificar cada palabra utilizando el contexto en el que aparece en lugar de la palabra propiamente dicha.\n", "\n", " \"Dime con quien andas y te diré quien eres\"" ] }, { "cell_type": "markdown", "metadata": { "id": "JA9GBeifKDdC" }, "source": [ "Intuición\n", "----------\n", "\n", "Si dos palabras distintas tienden a aparecer siempre en el mismo contexto, entonces las mismas deberían de compartir un espacio similar en las representaciones. Esto quiere decir que las representaciones de todo el vocabulario generan un espacio multidimensional donde la posición relativa de cada palabra con las otras tiene significado. Más aún, habría entonces direcciones dentro del espacio con signfiicados especificos.\n", "\n", "Definición de contexto\n", "----------------------\n", "\n", "La definición de contexto que utilicemos tiene un efecto importante en los resultados de los vectores que generamos para las palabras y en las propiedades que los mismos tienen. En la mayoría de los casos el contexto serán las palabras que rodean a cada palabra en cuestión - normalmente en la forma de una ventana alrededor de la misma - pero podría también ser la oración completa o el parrafo completo en donde aparece.\n", "\n", "En algunos incluso el contexto puede ser variable, donde el mismo es inferido dependiendo de la estructura sintáctica de donde aparece la palabra.\n", "\n", "### Ventana móvil\n", "\n", "Quizás la estrategia más típica es la de una ventana móvil en la cual el contexto está definido por la secuencia de `2m + 1` palabras. La pabra en la posición `m + 1` es la palabra *foco*, y las restantes palabras son las palabras *contexto*.\n", "\n", "El tamaño de la ventana tiene un efecto importante. Ventanas muy grandes tienden a generar directamente *tópicos* similares mientras que ventanas más pequeñas tenderán a generar vectores que son más funcionales o sintácticas.\n", "\n", "### Ventana móvil posicional\n", "\n", "El concepto de contexto utilizando una ventana movil hace que las palabras pierdan la noción del orden. Ni siquiera tenemos información de las palabras que aparecen antes o despues de la palabra foco. Esto se puede resolver utilizando contextos posicionales donde en lugar de codificar la palabra \"perro\" por ejemplo, codificamos \"perro:+2\" haciendo referencia a que la misma está 2 posiciones luego de la palabra foco.\n", "\n", "\n", "¿Cómo aprender estas representaciones?\n", "--------------------------------------\n", "\n", "La forma en la que Word2Vec genera estas representaciones es a través del modelado de una tarea de lenguaje especifica que es aprender una distribución probabilistica de palabras dado un contexto. Es decir que contruiremos un modelo que nos pueda decir, por cada palabra, la probabilidad de que una palabra dada aparezca en su contexto. Por ejemplo, dada la palabra \"sobiética\" la probabilidad de que la palabra \"unión\" o \"Rusia\" esté en el contexto es mucho más alta que la palabra \"perro\".\n", "\n", "Esto lo podemos lograr de dos formas:\n", "\n", "### Skip-grams (SG)\n", "\n", "En esta modalidad, la tarea a resolver consiste en dada una palabra y su contexto en el que aparece, por cada palabra en el contexto predecir cada una de las restantes palabras que la rodean.\n", "\n", "![Skip-grams](https://github.com/santiagxf/M72109/blob/master/docs/nlp/_images/word2vec_sg.png?raw=1)\n", "\n", "Esta variante desacopla las dependencias entre el contexto y cada palabra al asumir que cada elemento del contexto es independiente de cada uno. Esto quiere decir, que nuestro set de datos para resolver la tarea luciría algo como lo siguiente:\n", "\n", "![Skip-grams: conjunto de entrenamiento](https://github.com/santiagxf/M72109/blob/master/docs/nlp/_images/word2vec_sg_train.png?raw=1)\n", "\n", "La técnica de skip-gram fué la popularizada por Mikolov et al. [2013a] y es una de las técnicas más eficientes y róbustas para aprender vectores.\n", "\n", "### Continous-bag-of-words (CBOW)\n", "\n", "En esta modalidad, la tarea a resolver consiste en dada un contexto, predecir cada una de las palabras que están dentro del contexto utilizando las palabras restantes.\n", "\n", "![Continous-bag-of-words](https://github.com/santiagxf/M72109/blob/master/docs/nlp/_images/word2vec_cbow.png?raw=1)\n", "\n", "En esta variante el contexto se define como la suma de todas los vectores palabra, aunque perdiendo la información del orden. Esto quiere decir, que nuestro set de datos para resolver la tarea luciría algo como lo siguiente:\n", "\n", "![Continous-bag-of-words: conjunto de entrenamiento](https://github.com/santiagxf/M72109/blob/master/docs/nlp/_images/word2vec_cbow_train.png?raw=1)" ] }, { "cell_type": "markdown", "metadata": { "id": "vqUJcvGIKDdC" }, "source": [ "Entrenamiento\n", "-------------\n", "\n", "En cualquiera de las anteriores modalidades, nuestro modelo será una red neuronal que tendrá una sola capa interna (hidden layer) donde la cantidad de unidades de la dicha capa está dada por la dimensionalidad de los vectores que queremos aprender. En el caso de Word2Vec, esta dimensionalidad es 300. Estos vectores los debemos generar para todas las palabras del vocabulario, por lo tanto nuestra capa interna está representada por una matriz de dimensiones *|V| x 300* (una columna por cada unidad neuronal).\n", "\n", "Al final del proceso, nuestro objetivo entonces es aprender esta matriz de pesos en la red neuronal, con la esperanza de que los vectores que se aprendieron hayan captura de alguna forma la distribución probabilistica de cada una de las palabras. Si nos ponemos a revisar esta configuración, la matriz resultante no es más que una tabla de búsqueda de vectores, que dada una palabra de nuestro vocabulario, nos devuelve el vector resultante.\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "id": "SSD5ayfEKDdD" }, "source": [ "Modelo pre-entrenado en Español\n", "-------------------------------\n", "\n", "Utilizaremos un modelo pre-entrenado de Word2Vec que fué entrenado con un corpus en español (Spanish CoNLL17 corpus). Este modelo se entrenó trabajando con una ventana de 10 palabras, con el algoritmo Continuous bag of words y un negative-sampling de 5 palabras. Este modelo está almacenado en formato binario, por lo que lo cargamos utilizando el objeto `KeyedVectors` y el metodo `load_word2vec_format`:" ] }, { "cell_type": "markdown", "metadata": { "id": "YXrlNZHbKDdD" }, "source": [ "### Para ejecutar este notebook" ] }, { "cell_type": "markdown", "metadata": { "id": "NpwC-na5KDdE" }, "source": [ "Para ejecutar este notebook, instale las siguientes librerias:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "id": "TcUf7ChBKDdH" }, "outputs": [], "source": [ "!wget https://raw.githubusercontent.com/santiagxf/M72109/master/docs/nlp/vectorization/Word2Vec.txt \\\n", " --quiet --no-clobber\n", "!pip install -r Word2Vec.txt --quiet" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "tuto5q3DKDdJ" }, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "metadata": { "id": "OAJCQNeBKDdJ" }, "source": [ "### Descargar el modelo" ] }, { "cell_type": "markdown", "metadata": { "id": "y2d8M94vKDdK" }, "source": [ "El siguiente fragmento de codigo descarga el modelo preentrenado y lo almacena en el directorio `Models/Word2Vec`" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "id": "y0oCHMl3KDdK" }, "outputs": [], "source": [ "!mkdir -p ./Models/Word2Vec\n", "!wget https://santiagxf.blob.core.windows.net/public/Word2Vec/model-es.bin \\\n", " --quiet --no-clobber" ] }, { "cell_type": "markdown", "metadata": { "id": "CG6Uvb4OKDdK" }, "source": [ "Para comenzar a utilizar este modelo, lo cargaremos desde el directorio anterior:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "id": "htH_ryKDKDdL" }, "outputs": [], "source": [ "import sys, os, io, pathlib\n", "import numpy as np\n", "\n", "from gensim.models import KeyedVectors\n", "from gensim.test.utils import datapath\n", "\n", "path = os.path.abspath('/content/model-es.bin')\n", "embeddings = KeyedVectors.load_word2vec_format(datapath(path), binary=True)" ] }, { "cell_type": "markdown", "metadata": { "id": "FwH5UIAbKDdL" }, "source": [ "Si exploramos algunas de las propiedades del modelo veremos el tamaño del vocabulario y las dimensiones de nuestros vectores resultantes:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "id": "dS3LsF_uKDdL", "outputId": "0b2176c3-96b1-4f79-f171-1edf65bdca40", "colab": { "base_uri": "https://localhost:8080/" } }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "El tamaño del vocabulario es: 2656057\n", "El tamaño de los vectores es: 100\n" ] } ], "source": [ "print(\"El tamaño del vocabulario es:\", len(embeddings.key_to_index))\n", "print(\"El tamaño de los vectores es:\", embeddings.vector_size)" ] }, { "cell_type": "markdown", "metadata": { "id": "v1U5HZepKDdM" }, "source": [ "### Explorando las representaciones\n", "\n", "Podemos comenzar a indagar como lucen las representaciones vectoriales que se aprendieron utilizando el método most_similar. Este método nos devuelve las 10 primeras palabras más similares a la palabra que indicamos. La similaridad se computa utilizando cosine-similarity de las representaciones vectoriales de cada una de las palabras." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "id": "cJkmvc9sKDdM", "outputId": "666ba9f0-ff26-4a59-e552-c9bce2f81502", "colab": { "base_uri": "https://localhost:8080/" } }, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "[('emperatriz', 0.7729766964912415),\n", " ('princesa', 0.7713199853897095),\n", " ('sofía', 0.7699437141418457),\n", " ('coronaciónla', 0.7649693489074707),\n", " ('sofíala', 0.7604084610939026),\n", " ('virreina', 0.7558673024177551),\n", " ('categoría:talavera', 0.7512168288230896),\n", " ('isabel', 0.7497586607933044),\n", " ('sofíael', 0.7353644967079163),\n", " ('emperatríz', 0.7345834374427795)]" ] }, "metadata": {}, "execution_count": 6 } ], "source": [ "embeddings.most_similar(\"reina\")" ] }, { "cell_type": "markdown", "metadata": { "id": "03Hid8WvKDdN" }, "source": [ "También podemos consultar cuales son las palabras más disimilares a una determinada palabra. Esto lo hacemos especificando el parametro negative dentro del método `most_similar`" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "id": "RXFpVirNKDdN", "outputId": "7c73a2b8-5ab7-497b-ee04-f693bf608adf", "colab": { "base_uri": "https://localhost:8080/" } }, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "[('categoría:triplemanía', 0.31081730127334595),\n", " ('smr10', 0.30711960792541504),\n", " ('ausespecial=sub20', 0.3018966019153595),\n", " ('categoría:adiscus', 0.3002437353134155),\n", " ('aus17', 0.2890426814556122),\n", " ('categoría:agavoideae', 0.2831123173236847),\n", " ('€463.520', 0.280171275138855),\n", " ('categoría:maturinenses', 0.2768503427505493),\n", " ('categoría:errázuriz', 0.2763756513595581),\n", " ('categoría:odesitas', 0.2753686308860779)]" ] }, "metadata": {}, "execution_count": 7 } ], "source": [ "embeddings.most_similar(negative=\"reina\")" ] }, { "cell_type": "markdown", "metadata": { "id": "398cydGRKDdN" }, "source": [ "### Analogias" ] }, { "cell_type": "markdown", "metadata": { "id": "02Wrg5q6KDdO" }, "source": [ "El método anterior no es muy util en general, sin embargo nos permite introducir el concepto de aritmética sobre estos mismos vectores. Por ejemplo: Pordiamos consultar cual es la analogia de dos palabras. El siguiente ejemplo se leería así: A lo que Paris es a Francia, ¿cúal es la analogia de Madrid?. Esto se resolveria tomando \"Francia\", quitandole \"Paris\" y agregandole \"Madrid\"\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "kGN-PSmvKDdO", "outputId": "d989099d-f29f-4357-f37c-5fc9420cac2a" }, "outputs": [ { "data": { "text/plain": [ "'españa'" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "embeddings.most_similar(positive=[\"francia\", \"madrid\"], negative=[\"paris\"])[0][0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "uEHGBFnhKDdO", "outputId": "074f87db-d0f5-4c8d-cf4f-981a1141659e" }, "outputs": [ { "data": { "text/plain": [ "'barcelona'" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "embeddings.most_similar(positive=[\"paris\", \"españa\"], negative=[\"francia\"])[0][0]" ] }, { "cell_type": "code", "source": [ "embeddings.most_similar(positive=[\"paris\", \"españa\"], negative=[\"francia\"])[0:2]" ], "metadata": { "id": "4rve98kIMOZV", "outputId": "c28bcf36-f1e9-4f41-967c-fb5071435fd9", "colab": { "base_uri": "https://localhost:8080/" } }, "execution_count": 9, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "[('barcelona', 0.7425479888916016), ('madrid', 0.7323868274688721)]" ] }, "metadata": {}, "execution_count": 9 } ] }, { "cell_type": "markdown", "metadata": { "id": "4__9xDVpKDdP" }, "source": [ "Estos nos dice que quizás este espacio vectorial no es solamente un espacio donde vectores que están cerca los unos de los otros en el espacio tienen significados similares, sino que en realidad capturan el significado en una forma más produnda. Concretamente, que hay \"direcciones de significados\" en el espacio donde uno de puede mover. Veamos algunos ejemplos:" ] }, { "cell_type": "markdown", "metadata": { "id": "8LRYJo93KDdP" }, "source": [ "En la dirección de \"hacedor\"\n", " - Conducir es a conductor lo que limpiar es a?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Yv9ILwYuKDdP", "outputId": "3bdffdd0-45d5-424a-f84e-206123e541de" }, "outputs": [ { "data": { "text/plain": [ "'trapeador'" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "embeddings.most_similar(positive=[\"conductor\", \"limpiar\"], negative=[\"conducir\"])[0][0]" ] }, { "cell_type": "markdown", "metadata": { "id": "e_i1TcxlKDdP" }, "source": [ "En la dirección \"bebidas\"\n", " - El vino es a Francia lo que el whisky es a?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "kt457LwsKDdQ", "outputId": "88eb348a-bc5d-4fc8-a253-be19ccd79a05" }, "outputs": [ { "data": { "text/plain": [ "'bélgica'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "embeddings.most_similar(positive=[\"francia\", \"whisky\"], negative=[\"vino\"])[0][0]" ] }, { "cell_type": "markdown", "metadata": { "id": "4Myyk4unKDdQ" }, "source": [ "En la dirección \"extremar\"\n", " - Bueno es a genial lo que malo es a?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "CTJfr84QKDdQ", "outputId": "b2c92e66-5f6c-4d0c-f73e-b3233d02974c" }, "outputs": [ { "data": { "text/plain": [ "'malisimo'" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "embeddings.most_similar(positive=[\"buenisimo\", \"malo\"], negative=[\"bueno\"])[0][0]" ] }, { "cell_type": "markdown", "metadata": { "id": "pduz_pI6KDdQ" }, "source": [ "Visualización del espacio contino de word2vec\n", "---------------------------------------------\n", "\n", "El siguiente código utiliza un plugin de Tensorflow llamado Tensorboard que permite hacer una proyección en un espacio 3D (para luego hacer una proyección en un espacio 2D - el de la pantalla) de las representaciones de cada una de las palabras que están en el vocabulario. Esta visualización requiere tener instalado TensorFlow 2.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "7aUGud--KDdQ" }, "outputs": [], "source": [ "!mkdir -p '/tmp/logdir'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "R3dorMR9KDdR" }, "outputs": [], "source": [ "import tensorflow as tf\n", "from tqdm import tqdm\n", "from tensorboard.plugins import projector\n", "\n", "logdir = '/tmp/logdir'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MqDe5iN9KDdR", "outputId": "a164f84e-4b70-41af-973d-288a53b292d7" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2021-09-15 19:11:52.646960: I tensorflow/compiler/jit/xla_cpu_device.cc:41] Not creating XLA devices, tf_xla_enable_xla_devices not set\n", "2021-09-15 19:11:52.652627: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: SSE4.1 SSE4.2 AVX AVX2 AVX512F FMA\n", "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", "2021-09-15 19:11:52.655446: I tensorflow/core/common_runtime/process_util.cc:146] Creating new thread pool with default inter op setting: 2. Tune using inter_op_parallelism_threads for best performance.\n" ] } ], "source": [ "with tf.device('/CPU:0'):\n", " weights = tf.Variable(embeddings.vectors, )\n", " checkpoint = tf.train.Checkpoint(embedding=weights)\n", " checkpoint.save(os.path.join(logdir, 'embedding.ckpt'))" ] }, { "cell_type": "markdown", "metadata": { "id": "JBOe6pxOKDdR" }, "source": [ "El siguiente codigo genera los \"labels\" asociados al espacio vectorial que cargamos anteriormente. Pueden descargar estos labels ya preprocesados desde:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "i9e3x9jIKDdR" }, "outputs": [], "source": [ "!wget https://santiagxf.blob.core.windows.net/public/Word2Vec/meta.tsv \\\n", " --quiet --no-clobber --directory-prefix '/tmp/logdir'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "X0rY8p0vKDdS", "outputId": "84a762db-c474-44ab-e96f-f4e2136c35a7" }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "os.path.exists(logdir + '/vecs.tsv')" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-WfSh__TKDdS", "outputId": "c0302af7-bb7e-4a09-d133-88f51181b3a1" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "File vecs.tsv is already generated\n" ] } ], "source": [ "if os.path.exists(logdir + '/vecs.tsv') == False:\n", " with io.open(logdir + '/meta.tsv', 'w', encoding='utf-8') as metadata_file:\n", " with io.open(logdir + '/vecs.tsv', 'w', encoding='utf-8') as vectors_file:\n", " for index in tqdm(range(len(embeddings.index_to_key))):\n", " word = embeddings.index_to_key[index]\n", " vec = embeddings.vectors[index]\n", " metadata_file.write(word + \"\\n\")\n", " vectors_file.write('\\t'.join([str(x) for x in vec]) + \"\\n\")\n", "else:\n", " print('File vecs.tsv is already generated')" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ubWs7v6QKDdY" }, "outputs": [], "source": [ "# Set up config\n", "config = projector.ProjectorConfig()\n", "embedding = config.embeddings.add()\n", "# The name of the tensor will be suffixed by `/.ATTRIBUTES/VARIABLE_VALUE`\n", "embedding.tensor_name = \"embedding/.ATTRIBUTES/VARIABLE_VALUE\"\n", "embedding.metadata_path = 'meta.tsv'\n", "projector.visualize_embeddings(logdir, config)" ] }, { "cell_type": "markdown", "metadata": { "id": "Us4QcIKzKDdZ" }, "source": [ "Ejecute en una consola:\n", "\n", "```\n", "tensorboard --logdir /tmp/logdir\n", "```\n", "\n", "Debería poder visualizar en el navegador el espacio vectorial de las diferentes palabras capturadas por el modelo de Word2Vec. Pruebe buscando una palabra en la barra de navegación lateral, como por ejemplo \"avenida\" para ver cuales son las palabras cercanas dentro del espacio vectorial. Utilice el selector `neighbors` para limitar la cantidad de vecinos más próximos que se muestran en la pantalla." ] }, { "cell_type": "markdown", "metadata": { "id": "uisSj2K6KDdZ" }, "source": [ "![Espacio vectorial para la palabra avenida](https://github.com/santiagxf/M72109/blob/master/docs/nlp/_images/word2vec_tensorboard.png?raw=1)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.11" }, "colab": { "provenance": [] } }, "nbformat": 4, "nbformat_minor": 0 }