diff --git a/Tutoriales/Week6/S6_LSC3_LDA/S6_LSC3_LDA.ipynb b/Tutoriales/Week6/S6_LSC3_LDA/S6_LSC3_LDA.ipynb index cfdfc3e8..30671cf2 100644 --- a/Tutoriales/Week6/S6_LSC3_LDA/S6_LSC3_LDA.ipynb +++ b/Tutoriales/Week6/S6_LSC3_LDA/S6_LSC3_LDA.ipynb @@ -15,7 +15,7 @@ "source": [ "# Modelado de Tópicos \n", "\n", - "Este *cuaderno* trata sobre modelado de tópicos a partir de datos de texto. El objetivo del *cuaderno* es que usted obtenga una visión general del modelo de asignación latente de Dirichlet (LDA, por sus siglas en inglés). Busca tambien que sea capaz de crear e implementar este modelo en `Python` y que sea capaz evaluar de interpretar los resultados e identificar el mejor modelo de tópicos para un determinado problema. \n", + "Este *cuaderno* trata sobre modelado de tópicos a partir de datos de texto. El objetivo del *cuaderno* es que usted obtenga una visión general del modelo de asignación latente de Dirichlet (LDA, por sus siglas en inglés). Busca también que sea capaz de crear e implementar este modelo en `Python` y que sea capaz evaluar de interpretar los resultados e identificar el mejor modelo de tópicos para un determinado problema. \n", "\n", "**NO** es necesario editar el archivo o hacer una entrega. Sin embargo, los ejemplos contienen celdas con código ejecutable (`en gris`), que podrá modificar libremente. Esta puede ser una buena forma de aprender nuevas funcionalidades del *cuaderno*, o experimentar variaciones en los códigos de ejemplo.\n", "\n" @@ -27,7 +27,9 @@ "source": [ "## Introducción\n", "\n", - "El modelado de tópicos o temas es una faceta del procesamiento del lenguaje natural (NLP, por sus siglas en inglés). Como vimos anteriormente utilizar el lenguaje, textos, como datos puede ser extremadamente poderoso. En este *cuaderno* nos centraremos sobre el modelado de tópicos. Inmediatamente nos surge la pregunta ¿qué son los tópicos? Responderemos esa pregunta con un ejemplo. Habremos notado que en los días en que se llevan a cabo eventos importantes (como elecciones nacionales, desastres naturales o eventos deportivos), las publicaciones de las redes sociales tienden a centrarse en esos eventos. Las publicaciones de alguna manera reflejan los eventos del día, y lo hacen de diferentes maneras. Las publicaciones pueden tener, y tendrán, puntos de vista divergentes que pueden ser agrupados en clústeres de tópicos de alto nivel. Si tuviéramos tweets sobre la final del Mundial, los tópicos de esos tweets podrían cubrir puntos de vista divergentes, que van desde la calidad del arbitraje al comportamiento de los aficionados. En Estados Unidos, el presidente realiza un discurso anual entre mediados y finales de enero llamado Estado de la Unión. Con un número suficiente de publicaciones en las redes sociales, podríamos inferir o predecir las reacciones de alto nivel (tópicos) al discurso. Esto lo lograríamos agrupando las publicaciones usando las palabras claves contenidas en ellos. Por ejemplo la siguiente figura con un breve texto sobre ciencia de datos muestra como se pueden identificar palabras y asignarlas a tópicos." + "Todo texto presenta una variedad de tópicos o temas que se expresan a través de palabras. El modelado de tópicos o temas es una faceta del procesamiento del lenguaje natural (NLP, por sus siglas en inglés). Como vimos anteriormente utilizar el lenguaje, textos, como datos puede ser extremadamente poderoso. \n", + "\n", + "En este *cuaderno* nos centraremos sobre el modelado de tópicos. Inmediatamente nos surge la pregunta ¿qué son los tópicos? Responderemos esa pregunta con un ejemplo. Habremos notado que en los días en que se llevan a cabo eventos importantes (como elecciones nacionales, desastres naturales o eventos deportivos), las publicaciones de las redes sociales tienden a centrarse en esos eventos. Las publicaciones de alguna manera reflejan los eventos del día, y lo hacen de diferentes formas. Las publicaciones pueden tener, puntos de vista divergentes que permiten que las agrupemos en grupos o clústeres de tópicos. Por ejemplo, si tuviéramos tweets sobre la final del Mundial, los tópicos de esos tweets podrían cubrir puntos de vista divergentes, que van desde la calidad del arbitraje al comportamiento de los aficionados. Esto lo lograríamos agrupando las publicaciones usando las palabras claves contenidas en ellos. Por ejemplo, la siguiente figura con un breve texto sobre ciencia de datos muestra cómo se pueden identificar palabras y asignarlas a tópicos." ] }, { @@ -43,16 +45,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "En la figura entonces se muestra como palabras como información, predicció, estadística son asignadas al tópico de modelado; mientras que computacional, produccion y escala son asignadas al tópico de ingeniería. Los modelos de tópicos son importantes porque ofrecen la misma función para los datos textuales que las estadísticas clásicas para los datos numéricos. Es decir que proporcionan un resumen significativo de los datos. \n", + "En la figura entonces se muestra como palabras como información, predicción, estadística son asignadas al tópico de modelado; mientras que computacional, producción y escala son asignadas al tópico de ingeniería. Los modelos de tópicos son importantes porque ofrecen la misma función para los datos textuales que las estadísticas clásicas para los datos numéricos. Es decir que proporcionan un resumen significativo de los datos. \n", "\n", "Los modelos de tópicos entran en la categoría de aprendizaje no supervisado porque, casi siempre, no se conocen de antemano los tópicos subyacentes de los documentos. Por lo tanto, no existe una variable que guie el aprendizaje. En términos de aprendizaje no supervisado, los modelos de tópicos se pueden pensar como parte del análisis de clusters, más específicamente a K-medias. Recordemos que con K-medias, primero se establece el número de clusters y luego el modelo asigna cada uno de los datos a uno de los clústers predeterminados. Lo mismo ocurre generalmente con los modelos de tópicos. Seleccionamos el número de tópicos al inicio y luego el modelo aísla las palabras que forman esa cantidad de tópicos. Este es un excelente punto de partida para una descripción general de modelado de tópicos de alto nivel.\n", "\n", "\n", - "Los modelos de tópicos buscan encontrar patrones comunes en el texto en el sentido de que los documentos descrien tópicos similares. Es decir, estos modelos identifican los tópicos abstractos en una colección de documentos (también referidos como corpus), utilizando las palabras contenidas en los documentos. Para ello asumen que las palabras en el mismo documento están relacionadas y usan esa suposición para definir tópicos abstractos al encontrar grupos de palabras que aparecen con frecuencia una cerca de otra. Es decir, si una oración contiene las palabras salario, empleado, y reunión, podemos asumir que esa oración trata o que su tópico es el trabajo. . \n", + "Los modelos de tópicos buscan encontrar patrones comunes en el texto en el sentido de que los documentos describen tópicos similares. Es decir, estos modelos identifican los tópicos abstractos en una colección de documentos (también referidos como corpus), utilizando las palabras contenidas en los documentos. Para ello asumen que las palabras en el mismo documento están relacionadas y usan esa suposición para definir tópicos abstractos al encontrar grupos de palabras que aparecen con frecuencia una cerca de otra. Es decir, si una oración contiene las palabras salario, empleado, y reunión, podemos asumir que esa oración trata o que su tópico es el trabajo.\n", "\n", - "Este tipo de algoritmos por lo general tratan de primero determinar el número de tópicos, luego identificar palabras o frases concurrentes en los documentos. A partir de esto buscar clusters de palabras que caracterizan el documentos y finalmente retornar un conjunto de tópicos abstractos que caracterizan el corpus.\n", + "Este tipo de algoritmos por lo general tratan de primero determinar el número de tópicos, luego identificar palabras o frases concurrentes en los documentos. A partir de esto buscar clusters de palabras que caracterizan el documento y finalmente retornar un conjunto de tópicos abstractos que caracterizan el corpus.\n", "\n", - "Un aspecto clave de los modelos de tópicos es que no producen tópicos específicos de una palabra o una frase, sino conjunto de palabras, cada una de las cuales representa un tópico abstracto. Esto se debe a que los modelos de tópicos entienden la proximidad de las palabras, no el contexto. Por ejemplo en la figura siguiente, el modelo no tiene idea de lo que significan ala, elevar, piloto, equipaje, pasajer, o mosca; sólo sabe que estas palabras, generalmente, siempre que aparecen, aparecen muy próximas entre sí. Será nuestra tarea darle una interpretación (o no) a este tópico." + "Un aspecto clave de los modelos de tópicos es que no producen tópicos específicos de una palabra o una frase, sino conjunto de palabras, cada una de las cuales representa un tópico abstracto. Esto se debe a que los modelos de tópicos entienden la proximidad de las palabras, no el contexto. Por ejemplo, en la figura siguiente, el modelo no tiene idea de lo que significan ala, elevar, piloto, equipaje, pasajero, o mosca; sólo sabe que estas palabras, generalmente, siempre que aparecen, aparecen muy próximas entre sí. Será nuestra tarea darle una interpretación (o no) a este tópico." ] }, { @@ -90,10 +92,10 @@ "source": [ "### Set-up del modelo\n", "\n", - "LDA representa los documentos como una mezcla de tópicos que generan palabras con ciertas probabilidad. Assume que los documentos se generan siguiendo un proceso definido. Al empezar a escribir un documento:\n", + "LDA representa los documentos como una mezcla de tópicos que generan palabras con ciertas probabilidades. Asume que los documentos se generan siguiendo un proceso definido. Al empezar a escribir un documento:\n", "\n", - "1. Decidimos el número de palabras que el documento tendrá, que surge de una distrución de Poisson.\n", - "2. Elegimos la mezcla de tópicos del documendo, esta mezcla surge de una distribución de Dirichlet sobre un conjunto fijo de K tópicos. Por ejemplo, siguiendo del ejemplo anterior, podríamos elegir que el documento consista 1/3 sobre aviones y 2/3 sobre automóviles. Intuitivamente, cuando utilizamos la distribución de Dirichlet estamos asumiento que los documentos dentro del corpus se distribuirían a lo largo del símplex en donde cada vértice se representa un tópico. Luego, cada documento se ubicaría más cercano a los vértices que representan los tópicos contenidos en él. Por ejemplo, supongamos que tenemos 7 documentos y tres tópicos posibles (aviones, automóviles y barcos), podríamos representar los documentos dentro del símplex de la siguiente manera:\n", + "1. Decidimos el número de palabras que el documento tendrá, que surge de una distribución de Poisson.\n", + "2. Elegimos la mezcla de tópicos del documento, esta mezcla surge de una distribución de Dirichlet sobre un conjunto fijo de K tópicos. Por ejemplo, siguiendo el ejemplo anterior, podríamos elegir que el documento consista 1/3 sobre aviones y 2/3 sobre automóviles. Intuitivamente, cuando utilizamos la distribución de Dirichlet estamos asumiendo que los documentos dentro del corpus se distribuirían a lo largo del simplex en donde cada vértice se representa un tópico. Luego, cada documento se ubicaría más cercano a los vértices que representan los tópicos contenidos en él. Por ejemplo, supongamos que tenemos 7 documentos y tres tópicos posibles (aviones, automóviles y barcos), podríamos representar los documentos dentro del simplex de la siguiente manera:\n", "\n", "
\n", "\"LDA\"\n", @@ -104,14 +106,13 @@ "\n", "3. Generamos cada palabra en el documento siguiendo el siguiente esquema:\n", " \n", - " 3.1. Elegimos un tópico, de acuerdo a la distribución multinomial que sampleamos en el paso anterior, por ejemplo, podemos elegir el tópico de avions con probabilidad 1/3 y el tópico de automóviles con 2/3.\n", + " 3.1. Elegimos un tópico, de acuerdo con la distribución multinomial que muestreamos en el paso anterior, por ejemplo, podemos elegir el tópico de avions con probabilidad 1/3 y el tópico de automóviles con 2/3.\n", " \n", - " 3.2 Usando el tópico generamos la palabra (de acuerdo a la distribución multinomiál). Por ejemplo, si seleccionamos el tópico de aviones, podriamos generar la palabra \"piloto\" con probabilidad del 20%, y \"equipaje\" con probabilidad del 10%, y asi sucesivamente.\n", - "\n", + " 3.2 Usando el tópico generamos la palabra (de acuerdo con la distribución multinomial). Por ejemplo, si seleccionamos el tópico de aviones, podríamos generar la palabra “piloto” con probabilidad del 20%, y “equipaje” con probabilidad del 10%, y así sucesivamente.\n", "\n", - "Formalmente, definimos una palabra como un item de un vocabulario indexado por $\\{1, \\cdots, V\\}$. Las palabras se representan mediante vectores de base uno, es decir, un vector donde sólo un elemento es 1 y el resto son 0. Así, usando superíndices para denotar componentes, la v-ésima palabra en el vocabulario se representa mediante un V-vector $w$ tal que $w^v = 1$ y $w^u = 0$ para $u\\neq v$. Un *documento* es una secuencia de $N$ palabras denotadas por $\\mathbf{w}=(w_1,w_2,\\cdots,w_N)$, en donde $w_n$ es la n-ésima palabra de la secuencia. Un *corpus* es una colección de $M$ documentos dentodas por $\\mathbf{D}=(\\mathbf{w_1},\\mathbf{w_2},\\cdots,\\mathbf{w_m})$.\n", + "Formalmente, definimos una palabra como un ítem de un vocabulario indexado por $\\{1, \\cdots, V\\}$. Las palabras se representan mediante vectores de base uno, es decir, un vector donde sólo un elemento es 1 y el resto son 0. Así, usando superíndices para denotar componentes, la v-ésima palabra en el vocabulario se representa mediante un V-vector $w$ tal que $w^v = 1$ y $w^u = 0$ para $u\\neq v$. Un *documento* es una secuencia de $N$ palabras denotadas por $\\mathbf{w}=(w_1,w_2,\\cdots,w_N)$, en donde $w_n$ es la n-ésima palabra de la secuencia. Un *corpus* es una colección de $M$ documentos denotados por $\\mathbf{D}=(\\mathbf{w_1},\\mathbf{w_2},\\cdots,\\mathbf{w_m})$.\n", "\n", - "Asi, para cada documento del corpus $D$, LDA supone los siguientes pasos que generan cada documento:\n", + "Así, para cada documento del corpus $D$, LDA supone los siguientes pasos que generan cada documento:\n", "\n", "1. Decidimos $N\\sim Poisson(\\xi)$, donde $N$ son palabras del documento que surgen de un proceso de Poisson con parámetro $\\xi$\n", "2. Elegimos $\\theta\\sim Dir(\\alpha)$ donde $\\theta$ es la distribución de tópicos que asumimos surgen de una distribución Dirichlet con $K$ categorias.\n", @@ -125,7 +126,8 @@ "Estos tres pasos se repiten para cada documento en el corpus.\n", "\n", "Es importante que notemos que este modelo inicial cuenta con algunas simplificaciones: \n", - " - En primer lugar, la dimensionalidad $K$ de la distribución Dirichlet (y por consiguiente la dimensionalidad de la variables de los tópicos $z$) se supone fija y conocida. \n", + "\n", + " - En primer lugar, la dimensionalidad $K$ de la distribución Dirichlet (y por consiguiente la dimensionalidad de la variable de los tópicos $z$) se supone fija y conocida. \n", " - Segundo, las probabilidades de cada palabra son parametrizadas por una matriz $\\beta$ de tamaño $k\\times V$ en donde $\\beta_{ij}=p(w^j = 1|z^i = 1)$, que trataremos como una cantidad fija que será estimada. \n", "\n", "Tomando como dados los parámetros $\\alpha$ y $\\beta$, la distribución de probabilidad conjunta de una mezcla de tópicos $\\theta$, un conjunto de $N$ temas $\\mathbf{z}$ y un conjunto de $N$ palabras $\\mathbf{w}$ esta dada por:\n", @@ -161,7 +163,7 @@ "source": [ "### Inferencia variacional\n", "\n", - "El principal desfio de este modelo es el cálculo de la distribución posterior:\n", + "El principal desafío de este modelo es el cálculo de la distribución posterior:\n", "\n", "$$p(\\theta, \\mathbf{z}|\\mathbf{w},\\alpha, \\beta)=\\frac{p(\\theta, \\mathbf{z}, \\mathbf{w}|\\alpha, \\beta)}{p(\\mathbf{w}|\\alpha, \\beta)}$$\n", "\n", @@ -172,7 +174,7 @@ "#### Intuición\n", "\n", "\n", - "La intución detrás de la inferencia variacional es que, si la distribución real es intratable, entonces se debe encontrar una distribución más simple, llamémosla distribución variacional, muy cercana a la distribución verdadera, que es manejable, para que la inferencia sea posible. En otras palabras, dado que es imposible inferir la distribución real debido a su complejidad, buscamos encontrar una distribución más simple que sea una buena aproximación de la distribución real.\n" + "La intuición detrás de la inferencia variacional es que, si la distribución real es intratable, entonces se debe encontrar una distribución más simple, llamémosla distribución variacional, muy cercana a la distribución verdadera, que es manejable, para que la inferencia sea posible. En otras palabras, dado que es imposible inferir la distribución real debido a su complejidad, buscamos encontrar una distribución más simple que sea una buena aproximación de la distribución real.\n" ] }, { @@ -189,9 +191,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "La inferencia variacional es como tratar de ver animales en un zoológico lleno de gente. Los animales del zoológico están en un hábitat cerrado que, en este ejemplo, es la distribución posterior. Los visitantes en realidad no pueden ingresar al hábitat, por lo que los visitantes deben conformarse con ver el hábitat desde la posición más cercana posible, que es la aproximación posterior (es decir, la mejor aproximación del hábitat). Si hay mucha gente en el zoológico, puede ser difícil llegar a ese punto de vista óptimo. La gente generalmente comienza en la parte de atrás de la multitud. y avanza estratégicamente hacia ese punto de vista óptimo. El paso de los visitantes desde la parte trasera de la multitud al punto de vista óptimo, es el camino de optimización. La inferencia variacional es simplemente el proceso de acercarse lo mejor posible al punto deseado sabiendo que en realidad no se puede alcanzar el punto deseado.\n", + "La inferencia variacional es como tratar de ver animales en un zoológico lleno de gente. Los animales del zoológico están en un hábitat cerrado que, en este ejemplo, es la distribución posterior. Los visitantes en realidad no pueden ingresar al hábitat, por lo que los visitantes deben conformarse con ver el hábitat desde la posición más cercana posible, que es la aproximación posterior (es decir, la mejor aproximación del hábitat). Si hay mucha gente en el zoológico, puede ser difícil llegar a ese punto de vista óptimo. La gente generalmente comienza en la parte de atrás de la multitud. y avanza estratégicamente hacia ese punto de vista óptimo. El paso de los visitantes desde la parte trasera de la multitud al punto de vista óptimo es el camino de optimización. La inferencia variacional es simplemente el proceso de acercarse lo mejor posible al punto deseado sabiendo que en realidad no se puede alcanzar el punto deseado.\n", "\n", - "Con la intuicón desarrollada veamos una aplicación en `Python`" + "Con la intuición desarrollada veamos una aplicación en `Python`" ] }, { @@ -201,43 +203,13 @@ "## LDA en `Python`\n", "\n", "\n", - "Ilustremos ahora la implementación de LDA en `Phyton`. Para ello vamos a usar una muestra de comentarios sobre restaurantes en Bogotá que provienen del sitio web [tripadvisor](https://www.tripadvisor.com/). Comenzamos entonces cargando las librerias y las stopwords:" + "Ilustremos ahora la implementación de LDA en `Phyton`. Para ello vamos a usar textos que contienen páginas sobre distintos ensayos. Por la frecuencia de palabras tenemos la sospecha de que estas páginas se refieren a [Chomsky](https://en.wikipedia.org/wiki/Noam_Chomsky), [Freud](https://en.wikipedia.org/wiki/Sigmund_Freud), y [Voltaire](https://en.wikipedia.org/wiki/Voltaire). El objetivo será entonces ver si podemos encontrar de forma no supervisada estas páginas." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], - "source": [ - "# Cargamos las librerías a utilizar\n", - "import pandas as pd\n", - "import numpy as np\n", - "import unidecode\n", - "import regex\n", - "import spacy\n", - "nlp = spacy.load(\"es_core_news_sm\")\n", - "# Creamos una lista de stopwords\n", - "from nltk.corpus import stopwords\n", - "lista_stopwords = stopwords.words(\"spanish\")\n", - "# Cargamos extra stop words\n", - "extra_stopwords = pd.read_csv('data/stopword_extend.csv', sep=',')\n", - "extra_stopwords=extra_stopwords['palabra'].to_list()\n", - "lista_stopwords=lista_stopwords+extra_stopwords\n", - "lista_stopwords=np.unique(lista_stopwords)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Luego los datos:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, "outputs": [ { "data": { @@ -309,22 +281,46 @@ "4 de la lingüística y de las ciencias cognitivas... Chomsky 5" ] }, - "execution_count": 2, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "# Cargamos las librerías a utilizar\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", "# Cargamos los datos \n", "ensayos= pd.read_csv('data/ensayos.csv', sep=',')\n", "ensayos.head()" ] }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(269, 3)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ensayos.shape" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "En total tenemos más de 130 mil comentarios (los cuales representan cada una de las filas del dataframe) y 25 variables que describen el restaurante y el comentario." + "Los datos entonces contienen 269 entradas donde fueron catalogadas como pertenecientes a alguno de estos 3 autores. De estas 101 páginas se refieren a Voltaire, 85 a Freud, y 83 a Chomsky. En nuestra tarea no supervisada ignoraremos por ahora esta información." ] }, { @@ -335,7 +331,10 @@ { "data": { "text/plain": [ - "(269, 3)" + "Voltaire 101\n", + "Freud 85\n", + "Chomsky 83\n", + "Name: titulo, dtype: int64" ] }, "execution_count": 3, @@ -344,19 +343,47 @@ } ], "source": [ - "ensayos.shape " + "ensayos.titulo.value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Primero vamos a quedarnos solo con las columnas que nos interesan: `titulo_comentario` y `contenido_comentario`. Luego las unimos." + "Antes de proceder con el análisis de datos tenemos que \"limpiarlos\", para ello cargaremos las librerías que nos ayudaran en la tarea y la lista de *stopwords*:" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Cargamos las librerías a utilizar\n", + "import unidecode\n", + "import regex\n", + "import spacy\n", + "nlp = spacy.load(\"es_core_news_sm\")\n", + "# Creamos una lista de stopwords\n", + "from nltk.corpus import stopwords\n", + "lista_stopwords = stopwords.words(\"spanish\")\n", + "# Cargamos extra stop words\n", + "extra_stopwords = pd.read_csv('data/stopword_extend.csv', sep=',')\n", + "extra_stopwords=extra_stopwords['palabra'].to_list()\n", + "lista_stopwords=lista_stopwords+extra_stopwords\n", + "lista_stopwords=np.unique(lista_stopwords)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Procedemos entonces a crear una función que nos ayude con la limpieza del texto como lo hicimos en el *cuaderno: Sistemas de Recomendación basado en Contenidos*: " + ] + }, + { + "cell_type": "code", + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -372,22 +399,36 @@ " out = nlp(out)\n", " out = [x.lemma_ for x in out]\n", " out = [regex.sub(\"él\", \"\", i) for i in out]\n", - " out = [i for i in out if len(i) >= 3]\n", + " out = [i for i in out if len(i) >= 2]\n", " return out" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "y aplicamos esta función sobre las filas que contienen el texto:" + ] + }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "clean = list(map(text_cleaning, ensayos['texto']))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Así la fila 100 lucirá de la siguiente manera y podemos ver que se refiere a Freud:" + ] + }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -403,40 +444,60 @@ ] }, { - "cell_type": "code", - "execution_count": 27, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "# Para aplicar LDA necesitamos construir un diccionario\n", - "import gensim.corpora as corpora\n", + "Con los datos tokenizados vamos a remover palabras que sean raras y demasiado comunes, en este caso removeremos palabras que aparecen en menos de 20 páginas o en más de 50% de las páginas. Para ello primero utilizaremos la librería [gensim](https://radimrehurek.com/gensim/index.html) que contiene múltiples funciones que facilitan el modelado de tópicos. \n", "\n", - "id2word = corpora.Dictionary(clean)" + "El primer paso entonces es generar una representación de diccionario del documento, esto en términos de [gensim](https://radimrehurek.com/gensim/index.html), implica crear un mapeo entre palabras y un identificador " ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Create Corpus\n", - "texts = clean\n", + "# Cargamos la función \n", + "from gensim.corpora import Dictionary\n", "\n", - "# Term Document Frequency\n", - "corpus = [id2word.doc2bow(text) for text in texts]\n", - "\n", - "# View\n", - "#print(corpus[:1])" + "# Creamos la representación de diccionario del documento\n", + "dictionary = Dictionary(clean)\n", + "dictionary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ahora aprovechamos la función `filter_extremes` para remover palabras que aparecen en menos de 20 páginas o en más de 50% de las páginas: " ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ - "?LdaMulticore" + "dictionary.filter_extremes(no_below=20, no_above=0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ahora estamos en condiciones de vectorizar el documento. Para ello utilizamos la función `doc2bow` que va a crear la matriz de frecuencia de los documentos" ] }, { @@ -445,29 +506,104 @@ "metadata": {}, "outputs": [], "source": [ - "#[[(id2word[id], freq) for id, freq in cp] for cp in corpus[:1]]" + "corpus = [dictionary.doc2bow(doc) for doc in clean]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Con esto podemos ver el número de palabras únicas con el que vamos a estimar LDA:" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Numero de palabras únicas: 337\n" + ] + } + ], + "source": [ + "print('Numero de palabras únicas: %d' % len(dictionary))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Estamos listos entonces para estimar LDA. Para ello utilizaremos la función [LDAModel](https://radimrehurek.com/gensim/models/ldamodel.html?highlight=ldamodel#module-gensim.models.ldamode) disponible en [gensim](https://radimrehurek.com/gensim/index.html). Carguemos entonces esta función." + ] + }, + { + "cell_type": "code", + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ - "# Aplicamos LDA\n", - "from gensim.models.ldamulticore import LdaMulticore\n", - "from pprint import pprint\n", - "\n", - "lda_model = LdaMulticore(corpus=corpus,\n", - " id2word=id2word,\n", - " num_topics=3, \n", - " random_state=123,\n", - " passes=80)" + "from gensim.models import LdaModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Antes de ajustarla discutiremos algunos de los parámetros:\n", + "\n", + " - `corpus` es donde especificamos la matriz de frecuencia de los documentos.\n", + " - `id2word` es donde especificamos el diccionario que mapea palabras ocn identificadores, este sirve para determinar el tamaño del vocabulario y para mostrar luego los tópicos con las palabras encontradas\n", + " - `num_topics` donde especificamos el número de tópicos a buscar. \n", + " - `chunksize` controla cuántos documentos se procesan a la vez en el entrenamiento. Aumentar el tamaño de los fragmentos acelerará el entrenamiento, siempre y cuando el fragmento de documentos entre fácilmente en la memoria. \n", + " - `passes` controla la frecuencia con la que entrenamos el modelo en todo el corpus.\n", + " - `iterations` controla la frecuencia con la que repetimos un bucle particular sobre cada documento. Cuando entrenamos es importante establecer el número de \"passes\" e \"iterations\" lo suficientemente alto.\n", + " - `alpha` es el parámetro que controla el \"prior\" de la distribución de tópicos. \n", + " - `eta` es el parámetro que controla el \"prior\" de la distribución de palabras-tópicos.\n", + " \n", + "Estimemos entonces el modelo:" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "Estimacion = LdaModel(\n", + " corpus=corpus,\n", + " id2word=dictionary,\n", + " num_topics=3,\n", + " chunksize=1000,\n", + " passes=20,\n", + " iterations=400,\n", + " alpha='auto',\n", + " eta='auto',\n", + " random_state=123,\n", + " eval_every=None\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notemos que elegimos 3 tópicos ya que tenemos la sospecha de que los textos se refieren a tres escritores distintos. Vamos a fijar `chunksize = 1000`, que es más que la cantidad de documentos, así que proceso todos los datos de una sola vez. No obstante, el tamaño de los fragmentos puede influir en la calidad del modelo (Hoffman et al., 2010) por eso te invito a que pruebes con distintos tamaños. Especificamos también 20 passes y 400 iteraciones. Es importante asignar estos parámetros lo suficientemente altos de forma tal que haya convergencia en la estimación. Nuevamente te invito a que pruebes con distintos valores. Fijamos `alpha = 'auto'` y `eta = 'auto'` de forma que el modelo \"aprenda\" automáticamente estos dos parámetros. Luego, establecemos el `random_state` para lograr reproducibilidad y finalmente fijamos `eval_every=None` esto evitará el cálculo de la perplejidad, que discutiremos más adelante, haciendo que el proceso sea más rapido." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Con el modelo estimado podemos utilizar `pprint` para ver los tópicos con las palabras asociadas y su peso dentro del tópico" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -475,27 +611,43 @@ "output_type": "stream", "text": [ "[(0,\n", - " '0.013*\"lenguaje\" + 0.011*\"chomsky\" + 0.011*\"poder\" + 0.009*\"linguistico\" + '\n", - " '0.006*\"frase\" + 0.006*\"generativo\" + 0.006*\"ser\" + 0.006*\"gramatico\" + '\n", - " '0.005*\"estructura\" + 0.005*\"decir\"'),\n", + " '0.066*\"voltaire\" + 0.020*\"hombre\" + 0.019*\"hacer\" + 0.014*\"diccionario\" + '\n", + " '0.012*\"historia\" + 0.011*\"ano\" + 0.011*\"propio\" + 0.011*\"bien\" + '\n", + " '0.010*\"filosofico\" + 0.010*\"mundo\"'),\n", " (1,\n", - " '0.020*\"freud\" + 0.006*\"poder\" + 0.006*\"humano\" + 0.005*\"hombre\" + '\n", - " '0.005*\"ser\" + 0.004*\"primero\" + 0.004*\"decir\" + 0.004*\"mismo\" + '\n", - " '0.003*\"parte\" + 0.003*\"caso\"'),\n", + " '0.059*\"freud\" + 0.019*\"humano\" + 0.017*\"hombre\" + 0.013*\"primero\" + '\n", + " '0.011*\"parte\" + 0.011*\"sociedad\" + 0.010*\"vida\" + 0.009*\"caso\" + '\n", + " '0.009*\"manera\" + 0.009*\"gran\"'),\n", " (2,\n", - " '0.018*\"voltaire\" + 0.006*\"poder\" + 0.005*\"hacer\" + 0.005*\"ser\" + '\n", - " '0.005*\"hombre\" + 0.004*\"mismo\" + 0.004*\"diccionario\" + 0.004*\"historia\" + '\n", - " '0.003*\"politico\" + 0.003*\"ano\"')]\n" + " '0.040*\"chomsky\" + 0.038*\"lenguaje\" + 0.028*\"linguistico\" + 0.018*\"frase\" + '\n", + " '0.018*\"generativo\" + 0.017*\"gramatico\" + 0.016*\"estructura\" + '\n", + " '0.016*\"teoria\" + 0.016*\"mente\" + 0.015*\"ejemplo\"')]\n" ] } ], "source": [ - "pprint(lda_model.print_topics())" + "from pprint import pprint\n", + "\n", + "pprint(Estimacion.print_topics())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Vemos entonces que Voltaire tiene el mayor peso en el primer tópico, Freud en el segundo, y Chomsky en el tercero. El modelo parece haber hecho un gran trabajo encontrando los tres autores." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Podemos también generar una visualización interactiva de los resultados utilizando [pyLDAvis](https://pyldavis.readthedocs.io/en/latest/readme.html) que permite ayudar a la interpretación de los tópicos:" ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -504,8 +656,6 @@ "text": [ "/Users/iggy/opt/anaconda3/lib/python3.9/site-packages/pyLDAvis/_prepare.py:246: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only.\n", " default_term_info = default_term_info.sort_values(\n", - "/Users/iggy/opt/anaconda3/lib/python3.9/site-packages/joblib/externals/loky/process_executor.py:702: UserWarning: A worker stopped while some jobs were given to the executor. This can be caused by a too short worker timeout or by a memory leak.\n", - " warnings.warn(\n", "/Users/iggy/opt/anaconda3/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses\n", " from imp import reload\n", "/Users/iggy/opt/anaconda3/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses\n", @@ -523,22 +673,7 @@ "/Users/iggy/opt/anaconda3/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses\n", " from imp import reload\n" ] - } - ], - "source": [ - "# Visualizamos los resultados\n", - "import pyLDAvis\n", - "import pyLDAvis.gensim_models as gensimvis\n", - "\n", - "pyLDAvis.enable_notebook()\n", - "LDA_visualization = gensimvis.prepare(lda_model, corpus, id2word)" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ + }, { "data": { "text/html": [ @@ -546,10 +681,10 @@ "\n", "\n", "\n", - "
\n", + "
\n", "" ], "text/plain": [ - "PreparedData(topic_coordinates= x y topics cluster Freq\n", - "topic \n", - "70 -0.084136 -0.067119 1 1 2.501875\n", - "99 0.011429 -0.145120 2 1 1.902649\n", - "9 -0.144837 -0.010665 3 1 1.814886\n", - "33 -0.160066 0.069987 4 1 1.793124\n", - "180 -0.141616 -0.000181 5 1 1.756624\n", - "... ... ... ... ... ...\n", - "100 -0.010932 0.083149 186 1 0.003002\n", - "108 0.139475 0.029170 187 1 0.003002\n", - "112 0.026870 0.003389 188 1 0.003002\n", - "118 -0.011739 0.027096 189 1 0.003002\n", - "189 0.111065 0.122624 190 1 0.003002\n", - "\n", - "[190 rows x 5 columns], topic_info= Term Freq Total Category logprob loglift\n", - "6473 voltaire 310.000000 310.000000 Default 30.0000 30.0000\n", - "3292 freud 329.000000 329.000000 Default 29.0000 29.0000\n", - "128 poder 370.000000 370.000000 Default 28.0000 28.0000\n", - "91 lenguaje 209.000000 209.000000 Default 27.0000 27.0000\n", - "17 chomsky 235.000000 235.000000 Default 26.0000 26.0000\n", - "... ... ... ... ... ... ...\n", - "494 capaz 0.002692 35.113027 Topic190 -6.2599 0.9378\n", - "69 generativo 0.002967 92.861424 Topic190 -6.1628 0.0623\n", - "128 poder 0.003738 370.168954 Topic190 -5.9317 -1.0893\n", - "411 decir 0.003001 190.337338 Topic190 -6.1514 -0.6439\n", - "751 mental 0.002517 54.292497 Topic190 -6.3273 0.4346\n", + "PreparedData(topic_coordinates= x y topics cluster Freq\n", + "topic \n", + "0 -0.011085 0.006843 1 1 34.786916\n", + "3 -0.002494 -0.012497 2 1 33.001933\n", + "2 0.021288 0.003044 3 1 16.795092\n", + "1 -0.007710 0.002610 4 1 15.416059, topic_info= Term Freq Total Category logprob loglift\n", + "129 frase 109.000000 109.000000 Default 30.0000 30.0000\n", + "7 chomsky 246.000000 246.000000 Default 29.0000 29.0000\n", + "311 freud 368.000000 368.000000 Default 28.0000 28.0000\n", + "126 estructura 108.000000 108.000000 Default 27.0000 27.0000\n", + "59 deber 116.000000 116.000000 Default 26.0000 26.0000\n", + ".. ... ... ... ... ... ...\n", + "126 estructura 16.253749 108.671492 Topic4 -5.1220 -0.0302\n", + "144 teoria 17.144011 129.331953 Topic4 -5.0687 -0.1510\n", + "132 hacer 18.310744 173.450119 Topic4 -5.0028 -0.3786\n", + "79 ver 15.549404 114.047329 Topic4 -5.1663 -0.1228\n", + "72 propio 15.514745 123.626208 Topic4 -5.1685 -0.2057\n", "\n", - "[12725 rows x 6 columns], token_table= Topic Freq Term\n", - "term \n", - "2125 36 0.293790 a yo\n", - "6765 13 0.671186 abad\n", - "7781 13 0.286326 abadia\n", - "6512 6 0.610802 abanderado\n", - "9147 10 0.401321 abandonado\n", - "... ... ... ...\n", - "1708 17 0.398770 zona\n", - "8758 13 0.331055 zoroastro\n", - "8758 41 0.331055 zoroastro\n", - "3498 23 0.224903 zweig\n", - "3498 89 0.224903 zweig\n", + "[257 rows x 6 columns], token_table= Topic Freq Term\n", + "term \n", + "252 1 0.361465 absoluto\n", + "252 2 0.225916 absoluto\n", + "252 3 0.180732 absoluto\n", + "252 4 0.225916 absoluto\n", + "267 1 0.309139 acabar\n", + "... ... ... ...\n", + "266 4 0.222923 vivir\n", + "335 1 0.509320 voltaire\n", + "335 2 0.249463 voltaire\n", + "335 3 0.101344 voltaire\n", + "335 4 0.140323 voltaire\n", "\n", - "[16149 rows x 3 columns], R=30, lambda_step=0.01, plot_opts={'xlab': 'PC1', 'ylab': 'PC2'}, topic_order=[71, 100, 10, 34, 181, 112, 117, 80, 60, 153, 83, 144, 70, 123, 67, 51, 29, 180, 55, 115, 61, 104, 103, 11, 146, 118, 165, 38, 89, 75, 156, 54, 132, 108, 3, 58, 120, 43, 98, 127, 40, 17, 56, 185, 92, 91, 102, 162, 111, 184, 163, 161, 33, 157, 72, 2, 7, 57, 25, 178, 179, 4, 62, 16, 151, 27, 69, 23, 137, 82, 143, 173, 50, 12, 124, 49, 158, 63, 128, 116, 85, 125, 6, 18, 168, 141, 122, 155, 42, 93, 136, 134, 65, 107, 154, 59, 172, 15, 36, 169, 148, 140, 167, 138, 106, 114, 160, 94, 105, 129, 97, 74, 131, 166, 130, 142, 28, 110, 186, 84, 183, 182, 64, 150, 87, 48, 13, 177, 149, 77, 164, 66, 176, 175, 44, 95, 126, 39, 96, 22, 159, 147, 53, 86, 20, 79, 133, 45, 5, 35, 26, 9, 99, 135, 8, 152, 188, 14, 171, 31, 139, 174, 187, 170, 189, 1, 145, 121, 19, 21, 24, 30, 32, 37, 41, 46, 47, 52, 68, 73, 76, 78, 81, 88, 90, 101, 109, 113, 119, 190])" + "[644 rows x 3 columns], R=30, lambda_step=0.01, plot_opts={'xlab': 'PC1', 'ylab': 'PC2'}, topic_order=[1, 4, 3, 2])" ] }, - "execution_count": 55, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "lda_model_opt = LdaMulticore(corpus = corpus,\n", + " id2word = dictionary,\n", + " num_topics = 4,\n", + " random_state=123)\n", + "pyLDAvis.enable_notebook()\n", + "LDA_visualization = gensimvis.prepare(lda_model_opt, corpus, dictionary)\n", "LDA_visualization" ] }, @@ -1143,23 +1061,120 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note que puede seleccionar manualmente cada tema para ver sus términos más frecuentes y/o \"relevantes\", utilizando diferentes valores del parámetro $\\lambda$. Esto puede ayudar cuando intenta asignar un nombre interpretable por humanos o un \"significado\" a cada tema.\n", + "Los resultados sugieren que hay 3 tipos de tópicos en estas páginas. Los tres encontrados anteriormente, y páginas que hablan sobre estos 3 autores simultáneamente.\n", "\n", - "Los valores de lambda ($\\lambda$) que están muy cerca de cero mostrarán términos que son más específicos para un tema elegido. Lo que significa que verá términos que son \"importantes\" para ese tema específico pero no necesariamente \"importantes\" para todo el corpus.\n", + "Es importante destacar que la medida de coherencia no sólo del tema en sí, sino también del conjunto de datos que se utiliza como referencia. Asimismo, parte de la literatura suele favorecerla ya que lleva a mejor interpretabilidad que otras medidas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Medida de Perplejidad\n", + "\n", + "Otra de las medidas utilizadas es la medida de Perplejidad. A diferencia de la medida de coherencia lo que buscaremos es minimizar la perplejidad.\n", + "\n", + "Esta medida calcula el número de palabras diferentes que son igualmente probables a seguir a cualquier palabra dada. Para aclarar que queremos decir con esto, consideremos un ejemplo con dos palabras: *el* y *anunciar*. La palabra *el* puede anteceder a una gran cantidad de palabras de igual probabilidad, mientras que el número de palabras de igual probabilidad que anteceden a *anunciar* será relativamente menor. \n", + "\n", + "La idea intuitiva es que palabras que, en promedio, pueden ir seguidas de un menor número de palabras de igual probabilidad, son más específicas y por lo tanto pueden estar más estrechamente vinculadas a los tópicos. Entonces puntuaciones más bajas de perplejidad implican mejores modelos de lenguaje.\n", + "\n", + "El cálculo es sencillo usando [gensim](https://radimrehurek.com/gensim/index.html) ya que tanto [LDAModel](https://radimrehurek.com/gensim/models/ldamodel.html?highlight=ldamodel#module-gensim.models.ldamode) como [LdaMulticore](https://radimrehurek.com/gensim/models/ldamulticore.html) retornan el límite por palabra de la verosimilitud, por lo que para convertirlo en perplejidad necesitamos hacer $perplejidad=2^{(-limite)}$:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "53.72978311890707" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.exp2(-lda_model_opt.log_perplexity(corpus))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dado que nos interesa minimizar esta perplejidad comparemos esta medida para distinto números de tópicos. Creemos entonces una función similar a la que construimos en la sección anterior:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "def perplejidad_ntopicos(dictionary, corpus, texts, start=1, limit=10, step=1):\n", + " \n", + " perplejidad_values = []\n", + " model_list = []\n", + " \n", + " for num_topics in range(start, limit, step):\n", + " model = LdaMulticore(corpus=corpus,\n", + " id2word=dictionary,\n", + " num_topics=num_topics, \n", + " random_state=123,\n", + " passes=20)\n", + " model_list.append(model)\n", + " perplejidad_values.append(np.exp2(-model.log_perplexity(corpus)))\n", "\n", - "Los valores de lambda que están muy cerca de uno mostrarán aquellos términos que tienen la relación más alta entre la frecuencia de los términos para ese tema específico y la frecuencia general de los términos del corpus." + " return model_list, perplejidad_values" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ - "# Guardamos la visualización como un html. \n", - "# Es mucho más sencillo interactuar con la gráfica desde el archivo que \n", - "# desde el notebook\n", - "#pyLDAvis.save_html(LDA_visualization, 'visualizacion_LDA.html')" + "modelos, valores_p = perplejidad_ntopicos(dictionary=dictionary, corpus=corpus, texts=clean, start=1, limit=10, step=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "# Show graph\n", + "limit=10; start=1; step=1;\n", + "x = range(start, limit, step)\n", + "plt.plot(x, valores_p)\n", + "plt.xlabel(\"Número de Tópicos\")\n", + "plt.ylabel(\"Medida de Perplejidad\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Tenemos entonces un resultado análogo al anterior. En este caso ambos indicadores coinciden, pero a priori esto no tiene que ser así. En la práctica se tienden a utilizar ambos y dan una indicación del rango de posibles valores. Por esta razón, es recomendable calcular los dos y en función de los resultados unidos al conocimiento del investigador decidir. " ] }, { @@ -1168,41 +1183,17 @@ "source": [ "## Consideraciones Finales\n", "\n", - "Cuando debemos extraer información de una gran colección de documentos aún no vista, el modelado de tópicos es un gran enfoque, ya que proporciona información sobre la estructura subyacente de los documentos. Es decir, los modelos de tópicos encuentran agrupaciones de palabras utilizando la proximidad, no el contexto.\n", - "\n", - "En este cuaderno, aprendimos cómo aplicar dos de los algoritmos de modelado de tópicos más comunes y efectivos: la asignación de Dirichlet latente y la factorización de matriz no negativa. Ahora deberíamos sentirnos cómodos limpiando documentos de texto sin formato utilizando varias técnicas diferentes; técnicas que se pueden utilizar en muchos otros escenarios de modelado. Continuaremos aprendiendo cómo convertir el corpus limpio en la estructura de datos adecuada de recuentos de palabras sin procesar o pesos de palabras por documento mediante la aplicación de modelos de bolsa de palabras.\n", - "\n", - "El enfoque principal del cuaderno fue ajustar los dos modelos de tópicos, incluida la optimización de la cantidad de tópicos, la conversión de la salida en tablas fáciles de interpretar y la visualización de los resultados. Con esta información, deberíamos poder aplicar modelos de tópicos completamente funcionales para obtener valor e información para cualquier negocio.\n", + "Cuando queremos extraer información de una colección de documentos, el modelado de tópicos es un enfoque que puede ser potencialmente útil, ya que proporciona información sobre la estructura subyacente de los documentos. \n", "\n", + "En este sentido los modelos de tópicos se pueden usar para predecir los tópicos que pertenecen a documentos no vistos, pero si vamos a hacer predicciones, es importante reconocer que los modelos de tópicos sólo conocen las palabras que se usan para entrenarlos. Es decir, si los documentos no vistos tienen palabras que no estaban en los datos de entrenamiento, el modelo no podrá procesar esas palabras incluso si se vinculan a uno de los tópico identificados en los datos de entrenamiento. Debido a este hecho, los modelos de tópicos tienden a usarse más para el análisis exploratorio y la inferencia que para la predicción.\n", "\n", + "Cuando se analiza el modelado de tópicos, es importante reforzar continuamente el hecho de que los grupos de palabras que representan los tópicos no están relacionados conceptualmente; están relacionados solo por proximidad. La proximidad frecuente de ciertas palabras en los documentos es suficiente para definir tópicos debido a que estamos asumiendo que todas las palabras en el mismo documento están relacionadas.\n", "\n", - "Los modelos de tópicos se pueden usar para predecir los tópicos que pertenecen a documentos no vistos, pero si vamos a hacer predicciones, es importante reconocer que los modelos de tópicos sólo conocen las palabras que se usan para entrenarlos. Es decir, si los documentos no vistos tienen palabras que no estaban en los datos de entrenamiento, el modelo no podrá procesar esas palabras incluso si se vinculan a uno de los tópico identificados en los datos de entrenamiento. Debido a este hecho, los modelos de tópicos tienden a usarse más para el análisis exploratorio y la inferencia que para la predicción.\n", + "Sin embargo, esta suposición puede no ser cierta o las palabras pueden ser demasiado genéricas para formar tópicos coherentes. La interpretación de tópicos abstractos implica equilibrar las características innatas de los datos de texto con las agrupaciones de palabras generadas. Los datos de texto, y el lenguaje en general, son muy variables, complejos y contextuales, lo que significa que cualquier resultado generalizado debe interpretarse con cautela.\n", "\n", - "Cada modelo de tópicos genera dos matrices. La primera matriz contiene palabras contra tópicos. Esta enumera cada palabra relacionada con cada tópico con alguna cuantificación de la relación. Dada la cantidad de palabras que considera el modelo, cada tópico sólo se describirá con una cantidad relativamente pequeña de palabras.\n", + "Es importante tener en cuenta también que la naturaleza ruidosa de los datos de texto puede hacer que los modelos de tópico asignen palabras no relacionadas con uno de los tópicos a ese tópico en particular. Esto no es necesariamente una falla en el modelo. En cambio, es una característica que, dados datos ruidosos, el modelo podría extraer peculiaridades de los datos que podrían afectar negativamente los resultados. Las correlaciones espurias podrían ser el resultado de cómo, dónde o cuándo se recopilaron los datos. \n", "\n", - "Las palabras se pueden asignar a un tópico o a varios tópicos con diferentes cuantificaciones. Si las palabras se asignan a uno o varios tópicos depende del algoritmo. De manera similar, la segunda matriz contiene documentos contra tópicos. Esta asigna cada documento a cada tópico mediante alguna cuantificación de la relación de cada combinación de tópico del documento.\n", - "\n", - "Cuando se analiza el modelado de tópicos, es importante reforzar continuamente el hecho de que los grupos de palabras que representan los tópicos no están relacionados conceptualmente; están relacionados solo por proximidad. La proximidad frecuente de ciertas palabras en los documentos es suficiente para definir tópicos debido a una suposición establecida anteriormente: que todas las palabras en el mismo documento están relacionadas.\n", - "\n", - "Sin embargo, esta suposición puede no ser cierta o las palabras pueden ser demasiado genéricas para formar tópicos coherentes. La interpretación de tópicos abstractos implica equilibrar las características innatas de los datos de texto con las agrupaciones de palabras generadas. Los datos de texto, y el lenguaje en general, son muy variables, complejos y contextuales, lo que significa que cualquier resultado generalizado debe consumirse con cautela.\n", - "\n", - "Esto no es para minimizar o invalidar los resultados del modelo. Dados documentos cuidadosamente limpios y una cantidad adecuada de tópicos, las agrupaciones de palabras, como veremos, pueden ser una buena guía sobre lo que contiene un corpus y pueden incorporarse de manera efectiva en sistemas de datos más grandes.\n", - "\n", - "Ya discutimos algunas de las limitaciones de los modelos de tópicos, pero hay algunos puntos adicionales que debemos considerar. La naturaleza ruidosa de los datos de texto puede hacer que los modelos de tópico asignen palabras no relacionadas con uno de los tópicos a ese tópico en particular.\n", - "\n", - "Nuevamente, consideremos la oración sobre el trabajo de antes. La palabra reunión podría aparecer en la agrupación de palabras que representa el tópico de trabajo. También es posible que la palabra larga pueda estar en ese grupo, pero la palabra larga no está directamente relacionada con el trabajo. Larga puede estar en el grupo porque aparece con frecuencia muy cerca de la palabra reunión. Por lo tanto, larga probablemente se consideraría falsamente (o espuriamente) correlacionado con el trabajo y probablemente debería eliminarse de la agrupación de tópicos, si es posible. Las palabras falsamente correlacionadas en grupos de palabras pueden causar problemas significativos cuando analizando los datos.\n", - "\n", - "Esto no es necesariamente una falla en el modelo. En cambio, es una característica que, dados datos ruidosos, el modelo podría extraer peculiaridades de los datos que podrían afectar negativamente los resultados. Las correlaciones espurias podrían ser el resultado de cómo, dónde o cuándo se recopilaron los datos. Si los documentos se recopilaron sólo en una región geográfica específica, las palabras asociadas con esa región podrían vincularse incorrectamente, aunque accidentalmente, a una o varias de las agrupaciones de palabras resultantes del modelo.\n", - "\n", - "Tenga en cuenta que, con palabras adicionales en el grupo de palabras, podríamos adjuntar más documentos a ese tópico de los que deberían adjuntarse. Si reducimos la cantidad de palabras que pertenecen a un tópico, ese tópico se asignará a menos documentos. Tenga en cuenta que esto no es algo malo. Queremos que cada grupo de palabras contenga solo palabras que tengan sentido para que podamos asignar los tópicos apropiados a los documentos apropiados.\n", - "\n", - "\n", - "Este modelo tiene algunos supuestos como:\n", - "- Cada documento es solo una colección de palabras o una \"bolsa de palabras\". Así, el orden de las palabras y el rol gramatical de las palabras (sujeto, predicado, verbos, ...) no se consideran en el modelo.\n", - "- Palabras como a/el/la/pero/y/o/... no contienen ninguna información sobre los \"temas\" y, por lo tanto, pueden eliminarse de los documentos como un paso de preprocesamiento. De hecho, podemos eliminar palabras que aparecen en al menos %80 ~ %90 de los documentos, sin perder ninguna información. Por ejemplo, si nuestro corpus contiene solo documentos médicos, palabras como humano, cuerpo, salud, etc. pueden estar presentes en la mayoría de los documentos y, por lo tanto, pueden eliminarse ya que no agregan ninguna información específica que haga que el documento se diferencie del resto. \n", - "- Sabemos de antemano cuántos temas queremos. $k$ está predeterminado.\n", - "- Los documentos son una combinación de temas.\n", - "- Los temas son una combinación de palabras.\n" + "Esto no es para minimizar o invalidar los resultados del modelo. Dados documentos cuidadosamente limpios y una cantidad adecuada de tópicos, las agrupaciones de palabras, como vimos, pueden ser una buena guía sobre lo que contiene un corpus y pueden incorporarse de manera efectiva en sistemas de datos más grandes como ser los sistemas de recomendación vistos en este módulo." ] }, { @@ -1217,10 +1208,15 @@ "\n", "- Fradejas Rueda, J. M. (2020). Cuentapalabras. Estilometrıa y análisis de texto con R para filólogos.\n", "\n", + "- Hoffman, M., Bach, F., & Blei, D. (2010). Online learning for latent dirichlet allocation. advances in neural information processing systems, 23.\n", + "\n", "- Murphy, K. P. (2012). Machine learning: a probabilistic perspective. MIT press.\n", "\n", "- Patel, A. A. (2019). Hands-on unsupervised learning using Python: how to build applied machine learning solutions from unlabeled data. O'Reilly Media.\n", - "\n" + "\n", + "- Rehurek, R., & Sojka, P. (2011). Gensim–python framework for vector space modeling. NLP Centre, Faculty of Informatics, Masaryk University, Brno, Czech Republic, 3(2).\n", + "\n", + "- Röder, M., Both, A., & Hinneburg, A. (2015). Exploring the space of topic coherence measures. In Proceedings of the eighth ACM international conference on Web search and data mining (pp. 399-408)." ] } ],