Skip to content

anava122002/YouTube-video-impact-classification

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NLP desde cero. Construyendo un modelo clásico sin librerías

Una de las lecciones más importantes que he aprendido estudiando matemáticas es que nunca dominarás una disciplina si no entiendes sus fundamentos, y quiero aprovechar la oportunidad que me da el introducirme en el mundo del NLP para volverla a llevar a la práctica.

Los clasificadores de texto son esenciales para el procesamiento del lenguaje natural y, por tanto, es también esencial para cualquiera que quiera usarlos el conocer en profundidad su arquitectura y función. Aunque construir uno desde cero es muy ambicioso, en este proyecto pretendo al menos explorar sus fundamentos teóricos y ponerme a prueba intentando traducirlos en código.

Estructura del Repositorio

YouTube-video-impact-classification/
    data/
        YoutubeCommentsDataSet.csv
    src/
        __init__.py
        model.py
        preprocessing.py
        utils.py
        vectorizer.py
    .gitignore
    main.py
    README.md
    requirements.txt

Datos

Los datos usados para este proyecto se encuentran en el siguiente dataset de Kaggle: Youtube Comments Dataset.

  • Comment: 18.4k comentarios de Youtube en varios idiomas (17872 únicos).

  • Sentiment: variable categórica con los valores positive (62%), neutral (25%) y negative (13%), según la intencionalidad del come tario.

Objetivos

Este proyecto tiene dos objetivos principales:

  1. Aprender la lógica detrás de cómo los ordenadores procesan el lenguaje humano.

  2. Contruir un modelo de análisis de sentimiento que clasifique los comentarios según su intención.

Fundamentos Teóricos

A continuación se presentan los aspectos teóricos a partir de los cuales se diseña el modelo.

1. El problema de representar texto

Un modelo de ML no entiende palabras, solo números. El primer reto es convertir texto en algo con lo que el ordenador pueda trabajar, es decir, buscar la forma de convertir el texto en números y organizar todas las palabras usadas en los comentarios para que el modelo sepa cuáles aparecen, cuántas veces y la relación entre ellas.

Podríamos construir un diccionario que asigne a cada palabra un número pero esto es, además de tedioso, inútil porque se está obviando su relación. Otra solución es asignarles una posición (en un vector) en vez de un número, de modo que:

  • Sabemos cuáles aparecen porque marcamos como != 0 las posiciones que están en el comentario y como 0 las que no.

  • Sabemos cuántas veces aparecen cada una porque sumamos 1 por cada aparición.

  • El vector refleja la relación entre ellas (las palabras son las dimensiones y sus coeficientes indican una posición en el espacio que describen).

2. Lidiar con cantidades grandes de palabras

En más de 15k comentarios (en inglés) que contiene el dataset podemos encontrar perfectamente 50.000 palabras o más, muchas de ellas erratas o inconsistencias en la capitalización, nombres propios... La solución más rápida es filtrarlas según la frecuencia de aparición, dejando sólo las que aparezcan un número indicado de veces.

3. Representación de la negación

Una única palabra negativa puede cambiar el significado de la frase entera por lo que es necesario registrar su efecto. Si se marcan aquellas palabras dentro de la ventana de negación (porque en muchas ocasiones no es solo la siguiente, sino que afecta a un conjunto entero de significación), se crean nuevos tokens con un significado opuesto al original.

En este caso se elimina la palabra negativa y se marcan las palabras afectadas con el prefijo NOT_. Sólo se aplicará a un conjunto de palabras concreto, principalmente adjetivos y adverbios, ya que no todas las palabras dentro de la ventana se ven afectadas (por ejemplo, dentro de la frase That is not an apple registrar el token NOT_apple no tendría sentido porque la carga negativa no cambia el significado de apple, pero NOT_is sí que tiene un significado diferente de simplemente is).

4. Análisis Discrminante con Naive Bayes

Decir que se quiere determinar la carga sentimental de un comentario es como decir que se quiere clasificar ese comentario como positivo, neutro o negativo. Para ello vamos a intentar responder la siguiente pregunta: ¿qué probabilidad tiene de ser x?

Según el Teorema de Bayes:

P(clase ∣ comentario) ∝ P(comentario ∣ clase)⋅P(clase)

siendo:

  • P(clase) la probabilidad de pertenecer a uno de los grupos.

  • P(comentario) la probabiliad de encontrarse la combinación de palabras del comentario.

Como calcular la probabilidad de que aparezca un comentario concreto es casi imposible usamos Naive Bayes, de modo que:

P(comentario ∣ clase) = ∏w ​P(w ∣ clase)

siendo:

P(w ∣ clase) = veces que w aparece en esa clase / total de palabras en esa clase​

Por último, para evitar que las palabras que no aparecen en una frase (0 en el vector) anulen la probabilidad sumamos uno al numerador y el cardinal del conjunto de palabras al denominador:

P(w ∣ clase) = (count(w,clase)+1)​ / (total palabras en clase+∣vocabulario∣)

5. Aplicación de logaritmos

El conjunto de palabras con el que se va a trabajar es lo suficientemente grande para que al multiplicar todas sus probabilidades el ordenador devuelva 0 en lugar del valor real. Para evitarlo calculamos el logaritmo. Así, el score final de cada clase es:

score(clase) = logP(clase) + ∑w​freqw​⋅logP(w ∣ clase)

donde freqw​ es la frecuencia de esa palabra en el comentario nuevo.

6. Entrenamiento y valoración de resultados

El entrenamiento de Naive Bayes es solo contar. No hay iteraciones ni optimización. Se recorre el dataset una vez y se guardan:

  • Conteo de cada palabra por clase.

  • Total de palabras por clase.

  • Proporción de comentarios de cada clase (el prior).

Con esos tres datos el modelo está entrenado.

Los resultados se recogen en una matriz de confusión de donde se saca el porcentaje de "accuracy". Esta métrica tiene un problema principal, y es que puede ser engañosa si los datos están desequilibrados porque el modelo aprende que la clase con más registros es una apuesta segura y devuelve muchos resultados erróneamente clasificados en esta, lo que no necesariamente se corresponde con un porcentaje bajo, más bien al contrario.

En el apartado de soluciones se verá cómo tratar el overfitting.

Arquitectura del Código

El proyecto se compone de tres partes principales: preprocesado de datos para su adecuación, vectorización del texto y entrenamiento/evaluación del modelo.

Todo el proceso se registra en un notebook.

Preprocesado de datos (preprocesing.py)

Se descartan los comentarios que no son en inglés, se quitan las mayúsculas y se limpian de símbolos que no tienen ningún significado. Después, se expanden las contracciones de palabras (por ejemplo, se pasa de it's a it is) y las negaciones.

Para comprobar que todo se ha hecho correctamente se calcula el número de elementos en total y se compara con la suma de letras minúsculas, mayúsculas, signos y números, de modo que si algún elemento no deseado se ha colado el los datos finales puede ser identificado y eliminado.

Vectorización del texto (vectorizer.py)

Se construye un diccionario donde se almacenan las palabras que aparecen al menos tres veces y su posición en el vector, de modo que cada comentario puede ser transformado en vector mapeando y sumando cada aparición.

Construcción, entrenamiento y evaluación del modelo (model.py)

Se dividen los datos en dos grupos, uno para entrenar (80%) y otro para evaluar (20%). De los datos de entrenamiento se extraen el total de comentarios, frecuencia de palabras, total de palabras, proporción de la clase en el total y proporción de las palabras de cada clase y se guardan en diccionarios para posteriormente calcular las probabilidades de Naive Bayes.

Se calcula y devuelve tanto los resultados como la matriz de confusión para poder evaluarlos.

Primeros Resultados y Mejoras

La matriz de confusión resultante tras la primera ejecución del código completo es la siguiente:

Positivo Neutro Negativo
Positivo 2041.0 134.0 71.0
Neutro 283.0 210.0 114.0
Negativo 117.0 32.0 112.0

siendo las filas los valores reales y las columnas las predicciones.

Vamos por partes:

  • En general tenemos una accuracy del 75,9%. El modelo ha clasificado correctamente 2363 comentarios de los 3114 que se usaron en la evaluación. Se puede observar a simple vista que el total de comentarios positivos (2246) supone el 72,1% del total y, además, es la clase con mejores resultados. Se deben de revisar los datos usados para el entrenamiento porque si el problema se repite significa que estamos ante una situación de overfitting.

  • La clase de comentarios positivos tiene una accuracy del 90,9%. Sólo 205 comentarios entre un total de 2246 fueron mal clasificados, mayoritariamente como neutros. La comparación con los resultados generales también parece indicar overfitting.

  • La clase de neutros tiene una accuracy del 34,6%. Teniendo en cuenta los anteriores resultados, que la mayoría (71,3%) de los comentarios mal clasificados han sido clasificados como positivos era esperable.

  • La clase de negativos tiene una accuracy del 42,9%, ligeramente mejor que antes pero igualmente un mal resultado. El 78,5% de los comentarios mal clasificados han sido clasificados, otra vez, como positivos.

Detectando overfitting

Hay varias formas de confirmar que, efectivamente, hay overfitting. La más rápida es darle al modelo los datos usados para entrenarlo, de modo que si los resultados son excepcionalmente buenos en comparación a los de los datos de evaluación se confirma que el problema está ahí.

En este caso, al proceder como tal obtenemos la matriz de confusión siguiente:

Positivo Neutro Negativo
Positivo 7633.0 206.0 157.0
Neutro 873.0 1344.0 298.0
Negativo 414.0 91.0 1436.0

La accuracy general es de 83,6%, ligeramente superior a la de los datos de evaluación, pero no demasiado. El problema viene cuando examinamos los resultados de cada clase individualmente: el porcentaje de aciertos para los grupos positivo, neutro y negativo son de 95,5%, 53,4% y 74% respectivamente, mucho mayores a los vistos anteriormente (especialmente para los resultados negativos).

Intentando mejorar el resultado

Aquí vamos a proceder como sigue. La probabilidad prior se cambia a 1/3 para que cada clase tenga la misma probabilidad inicial. Esto nos permite eliminar la "respuesta segura" que la desproporción de datos generaba. Los resultados son:

Positivo Neutro Negativo Porcentaje de aciertos
Positivo 2216.0 29.0 1.0 98,7%
Neutro 235.0 365.0 7.0 60%
Negativo 116.0 15.0 130.0 49,8%
Positivo Neutro Negativo Porcentaje de aciertos
Positivo 7293.0 362.0 341.0 91,2%
Neutro 571.0 1510.0 434.0 60%
Negativo 239.0 96.0 1606.0 82,4%

Los porcentajes de acierto mejoran bastante pero en la clase de negativos sigue existiendo una diferencia bastante notoria. Probamos ahora a modificar el parámetro alpha del suavizado de Laplace para que el modelo confíe menos en las palabras que aparecen poco. Obtenemos:

Positivo Neutro Negativo Porcentaje de aciertos
Positivo 2129.0 62.0 55.0 94,8%
Neutro 208.0 367.0 32.0 60,5%
Negativo 85.0 14.0 162.0 62,1%
Positivo Neutro Negativo Porcentaje de aciertos
Positivo 6342.0 402.0 1252.0 79,3%
Neutro 892.0 638.0 985.0 25,4%
Negativo 633.0 333.0 975.0 50%

Los porcentajes de aciertos generales son menores en ambos casos (81,2% y 63,9% respectivamente) y los resultados para los datos de entrenamiento se resienten especialmente. Aún así, lo más interesante es el aumento de casi un 20% de la accuracy para la clase negativa en el test.

Un modelo que funciona peor en train que en test tampoco es ideal, significa que no está aprendiendo bien los patrones sino que está adivinando de forma más equilibrada (consecuencia directa de cambiar el alpha). Pero que dude en el test cuando hay ruido también es beneficioso a nivel general, pues un mejor resultado en el test es preferible aunque el aprendizaje en el entrenamiento sea más laxo.

Otras soluciones al problema del overfitting que se han probado y no han dado resultado son:

  • Truncar positivos en train con split proporcional: se limita el número de positivos usados en la fase de entrenamiento asignando a cada categoría un porcentaje fijo de comentarios. El principal problema es que no refleja los porcentajes reales de cada clase dentro del grupo total. Además, en este caso, al ser la diferencia entre comentarios positivos y no positivos tan grande en el dataset, el conjunto de datos para la evaluación solo tenía comentarios positivos (para poner contexto, los comentarios son el 65,8% positivos, el 20,05% neutros y el 14,15% negativos).

  • Split 80/20 real por clase y truncar positivos en train después: soluciona el problema de falta de datos pero el aprendizaje sigue siendo balanceado y, por tanto, irreal. El resultado es, igualmente, un porcentaje de acierto para el entrenamiento perfecto pero para el test horrible.

  • Ampliar léxico de sentiment words: se añaden más palabras que pueden ser afectadas por la negación y se modifica la ventana para comprobar si el problema es falta de palabras clave para los comentarios negativos. El resultado mejora pero menos de un 1%, así que se descarta el cambio.

Naive Bayes tiene un techo con este problema. La combinación de prior igualado y datos originales es la que mejor resultado da, pero para mejorar significativamente haría falta oversampling real de las clases pequeñas o un modelo que capture el orden y la distancia entre palabras arbitrarias. El overfitting en Naive Bayes es un poco especial porque el modelo es muy simple y no tiene parámetros que ajustar iterativamente, de modo que no es un overfitting clásico, sino más bien que el modelo ha memorizado la distribución sesgada de los datos.

Resultado Final y Conclusiones

En este proyecto se ha construido un modelo de clasificación de sentimiento que usa las probabilidades Naive Bayes con una probabilidad prior equilibrada para asignar a cada comentario dado una clase. El resultado final nos devuelve una clasificación con un porcentaje de acierto del 87,1%, lo cual es engañoso pues para los comentarios que no sean positivos es demasiado bajo como para que el modelo pueda considerarse fiable (60% de acierto para los comentarios neutros y 49,8% para los negativos).

Aún así, el pipeline es bastante sólido y que el modelo funcione sin usar ninguna librería es un logro. Lo más valioso del proyecto no es su resultado, sino las herramientas implementadas en el proceso: la creación de una ventana de negación para no perder la carga semántica implícita en la frase, la vectorización de los comentarios y una traducción a código la teoría detrás de Naive Bayes cuyos parámetros se han modificado progresivamente para ajustarla a las necesidades de los datos.

Se ha llegado a un clasificador de texto lo suficientemente competente para problemas simples y capaz de disminuir el efecto de datos no balanceados, pero para comentarios de YouTube, donde el lenguaje es informal, irónico y desbalanceado por naturaleza, se necesita un modelo que capture relaciones globales entre palabras. El siguiente paso natural sería volverlo a construir usando librerías y funciones más competentes, no solo para mejorar resultados, sino para ampliar el horizonte que este modelo ha empezado a dibujar.

Cómo Replicar el Proyecto

  • Ejecutar archivo main.py. Este devuelve los resultados del entrenamiento y la matriz de confusión impresa en terminal.

About

Los clasificadores de texto son esenciales para el procesamiento del lenguaje natural y, por tanto, es también esencial para cualquiera que quiera usarlos el conocer en profundidad su arquitectura y función. En este proyecto pretendo explorar sus fundamentos teóricos y ponerme a prueba intentando programar un modelo desde cero.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages