Clasificación de texto con redes neuronales convolucionales

Clasificación de texto con redes neuronales convolucionales

Introducción

La inteligencia artificial ha visto en los últimos años un desarrollo tecnológico notable, y se ha convertido en una tendencia imparable para solucionar múltiples problemas. En las AAPP también encontramos casos en los que se puede aplicar soluciones avanzadas basadas en inteligencia artificial.

Aplicación AI

El Machine Learning o aprendizaje automático es un subgrupo de la Inteligencia Artificial. Se basa en crear sistemas que puedan aprender automáticamente, es decir, pueden descubrir patrones complejos enterrados en grandes conjuntos de datos sin la necesidad de interferencia humana.

Algoritmos del Machine Learning:

Los algoritmos de aprendizaje automático (ML) se pueden dividir en:

  • Aprendizaje supervisado. Aprende por medio del ejemplo. El algoritmo se entrena por medio de preguntas (características) y respuestas. Cuando el algoritmo tiene la respuesta a cierta pregunta, guarda esa información para hacer previsiones futuras. Existen dos tipos de aprendizaje supervisado:
    • Regresión: Tiene el objetivo de predecir valores continuos. Un ejemplo sería predecir el precio de un producto o una propiedad.
    • Clasificación: El algoritmo encuentra diferentes patrones y clasifica los elementos en diferentes grupos. Un ejemplo sería los filtros antispam del correo electrónico.
  • Aprendizaje no supervisado. Trata de extraer conocimiento de conjuntos de datos donde a priori no existe una clasificación. El algoritmo cataloga los datos por similitud y crea grupos. Existen dos modelos:
    • Análisis clúster: Se trata de clasificar un conjunto de datos en grupos lo más homogéneos entre si y lo más heterogéneos entre ellos. Un ejemplo es la segmentación que llevan a cabo las empresas para sus campañas, dividiéndolas por tipo de cliente.
    • Reducción de la dimensionalidad: convertir un conjunto de datos de dimensiones elevadas en un conjunto de datos de dimensiones menores, asegurando que la información que proporciona en similar en ambos casos. Los datos de alta dimensión, el reconocimiento de voz, visualización de datos, reducción de ruido o el procesamiento de señales, entre otros, son los principales campos de aplicación de la reducción de dimensionalidad.

Uno de los métodos avanzados para hacer este tipo clasificación (aprendizaje supervisado y no supervisado) son las Redes Neuronales.

Como resumen podemos observar un conjunto de técnicas existentes para soluciones de machine learning.

Técnicas ML

Redes neuronales

Sin entrar a fondo en el detalle de funcionamiento de las redes neuronales, presentamos una descripcion general de su funcionamiento. Las redes neuronales es un conjunto de algoritmos que clasificamos dentro del Machine Learning y son el núcleo de los algoritmos de Deep Learning. El nombre y la estructura se inspira en el cerebro humano, por la forma como se comunican las neuronas pasando información.

Las redes neuronales artificiales (ANN) se componen de capas formadas por nodos, que contienen una capa de entrada, una o más capas ocultas y una capa de salida. Cada nodo, o neurona artificial, se conecta a otro y tiene un peso y un umbral asociados. Si la salida de cualquier nodo individual está por encima del valor de umbral especificado, ese nodo se activa y envía datos a la siguiente capa de la red. De lo contrario, no se transmiten datos a la siguiente capa de la red.

Red Neuronal

Os dejo una serie de videos de Dot CSV donde explica en detalle qué es y cómo funcionan las redes neuronales:

Dot CSV Redes Neuronales

Existen diferentes implentaciones de redes neuronales’que se aplicarán en función del problema a resolver.

Aquí mostramos la tipología de redes neuronales existentes:

Tipología

Proyecto

Durante el mes de octubre he participado junto a otros compañeros de la administración local de Tarragona al curso de “Inteligencia Artificial aplicada a las AAPP”, organizado por Diputació de Tarragona y impartido por el equipo de Saturdays.ai

Como resultado de la formación se ha desarrollado un pequeño proyecto, a modo de prueba de concepto sobre una de las técnicas aprendidas.

El proyecto presentado se basa en la posibilidad de clasificar diferentes mensajes que recibe una AAPP de forma automática. Las administraciones reciben múltiples mensajes o solicitudes que se han de gestionar, tramitar y dar respuesta. Existen infinidad de canales, desde los más formales, como son las entradas en registro vía trámite o instancia general, las entradas interadministrativas o los formularios de quejas y sugerencias, hasta los más informales, como las redes sociales o llamadas.

Al tratarse de solicitudes genéricas, el trabajador público debe revisar el contenido del mensaje, clasificarlo, y en función de su experiencia, asignarlo a un departamento de la organización para poder gestionar la entrada y dar una respuesta adecuada.

Este proceso de clasificación manual puede ser un problema en organizaciones con un nivel de entradas importante. La posible solución al problema sería automatizar la clasificación de estas entradas y asignarlas de forma automática, en función del contenido, a un departamento de la organización para realizar su gestión.

Para simplificar el problema y realizar una prueba de concepto, se realizará la clasificación a partir de mensajes enviados a las AAPP a través de la red social Twitter. La metodología de desarrollo del proyecto será aplicable a otras fuentes de entrada.

Entorno

Para desarrollar la prueba de concepto usaremos Python. Existen muchas librerías y recursos para poder desarrollar proyectos de Data Science como lo muestra esta cheatsheet. En el campo específico de las redes neuronales, existen librerías como Torch que es un framework para desarrollar soluciones de Machine Learning en Python.

Como entorno de desarrollo hemos utilizado Colab. Colaboratory, o “Colab” para abreviar, es un producto de Google Research. Permite a cualquier usuario escribir y ejecutar código arbitrario de Python en el navegador. Es especialmente adecuado para tareas de aprendizaje automático, análisis de datos y educación.

También podemos utilizar Jupyter Lab, herramienta incluida en la distribución Anaconda, que permite desarrolar en Python en el navegador pero en modalidad self-hosted.

Obtención de datos

Como hemos comentado, la fuente de datos que utilizaremos para realizar el caso serán los tweets realizados por ciudadanos a las cuentas de Twitter institucionales de 4 grandes ayuntamientos de Catalunya. El proceso de obtención de datos es clave para el éxito de este tipo de proyectos, puesto que debemos tener cantidad suficiente y sobre todo, calidad. El método para obtener los datos es análogo al utilizado en este post. Utilizaremos las mismas claves para el consumo de la API de Twitter, pero esta vez a través de las librerías de Python.

El código conecta con la API, obtiene los Tweets en base a una query que se le especifique y guarda el resultado en un CSV. Podeis descargar el notebook aquí

Una vez hemos obtenido un conjunto de datos suficiente, normalizamos las entradas en un fichero CSV. Como estamos realizando un proceso de aprendizaje automático supervisado, debemos clasificar manualmente estos tweets y asignarles un departamento gestor. Esta categorización servirá para entrenar y validar el modelo de red neuronal realizada. Cuanta más calidad de datos tengamos más ajustada será la clasficación. El conjunto inicial de tweets es limitado, alrededor de 500. En un inicio se pensó en realizar una clasificación de los Tweets en 10 categorías, pero pronto pudimos comprobar que no teníamos una muestra suficiente de datos por cada clasificación. Finalmente decidimos clasificarlos en 4 grupos: [“Otros”,“Política y Hacienda”,“Ciudad”,“Ciudadano y Seguridad”], con valores 0,1,2,3 (columna classer)

Podeis descargar el fichero aquí

Proceso de datos y entrada

Como hemos comentado, la entrada de datos al sistema es uno de los elementos críticos en los proyectos de ML. Una vez tenemos el fichero con los tweets clasificados debemos:

  • Filtramos emojis de los textos
  • Quitamos los propios replies de las cuentas Twitter que estamos estudiando
  • Eliminamos menciones (@), retweets o enlaces
  • Eliminamos duplicados.

También eliminamos las palabras que no aportan contenido. Al tratarse de Tweets en català cogemos como referencia este listado

A partir de aquí, debemos tokenizar las entradas. Otro aspecto importante a destacar es que utilizaremos una CNN (Red Neuronal Convolucional), que se usa principalmente para obtener características en el tratamiento de imagenes o clasificaciones de textos. Un buen video explicativo es este de Dot CSV

Dot CSV Redes Neuronales Convolucionales

La entrada a una red nuronal convolucional es una matriz con los tokens (palabras) y una serie de características asociadas a cada palabra (token). Para ello usamos un corpus en leguaje català, de 800.000 palabras, que aporta un vector de características por cada palabra. El archivo se obtiene de aquí, concretamente la URL http://vectors.nlpl.eu/repository/20/34.zip

Si no se encuentra el token en el corpus añadimos un 0. La entrada de datos, en la CNN está fijada a un tamaño determinado, en nuestro caso 200 tokens.

Resumiendo:

  • Por cada texto a evaluar, limpiamos los datos que no aportan valor,
  • tokenizamos eliminando palabras tipo stop words
  • combinamos la palabra con un corpus en catlà, para añadir características a cada token
  • Fijamos el tamaño de la entrada a la red en 200

Modelo CNN

Para realizar la clasificación de textos nos hemos basado en el siguiente ejemplo Una vez tenemos preparados los datos de entrada podemos modelar nuestra red neuronal convolucional.

Entreno, Test y Validación

Para poder crear el modelo, debemos dividir nuestro conjunto de datos en tres:

  • Set de entreno, que se utiliza para entrenar la red nuronal, que aprende y produce resultados. Incluye tanto los datos de entrada como los resultados esperados.
  • Set de validación, usado para ajustar los hiperparámetros (arquitectura de la red neuronal).
  • Set de test, usado para realizar una evaluación sin sesgo del model ajustado por el set de entreno.

En código, considerando X nuestra matriz de entrada de 200 posiciones

train_x, X_resta, train_y, y_resta = train_test_split(X,
                                                    etiquetes,
                                                    test_size = 0.3,
                                                    random_state = 0)

# Dividimos los datos restantes que no son entreno, en test y validación
test_idx = int(len(X_resta) * 0.5)
val_x, test_x = X_resta[:test_idx], X_resta[test_idx:]
val_y, test_y = y_resta[:test_idx], y_resta[test_idx:]

Una vez tenemos definidos los conjuntos de entreno, test y validación podemos crear el modelo. A continuación mostramos como es el clasificador usando PyTorch

class ClassificaCNN(nn.Module):
    """
     model_embedding:              modelo que contiene el corpus del idioma (contienen el vector de N características por cada palabra)
     mida_vocabulari:              cantidad de palabras del corpus
     mida_sortida:                 tamaño de la salida, que será el número de clases a obtener.
     mida_vector_caracteristiques: longitud del vector de características del corpus
     num_filtres:                  número de filtros que se usarán en la convolución
     mides_kernels:                tamaño del kernel a aplicar ==> los kernels seran de:
                                   [3, 100], [4, 100] i [5, 100]    ==> [3 o 4 o 5, mida_vector_caracteristiques]
     freeze_embeddings:            documentación oficial "If True, the tensor does not get updated in the learning process"
     drop_prob:                    probabilidad a aplicar a la capa de dropout
    """
    def __init__(self, 
                 model_embedding,
                 mida_vocabulari,
                 mida_sortida,
                 mida_vector_caracteristiques,
                 num_filtres = 100,
                 mides_kernels = [3, 4, 5],
                 freeze_embeddings = True,
                 drop_prob = 0.5):

        super(ClassificaCNN, self).__init__()

        self.num_filtres = num_filtres
        self.mida_vector_caracteristiques = mida_vector_caracteristiques
        
        # 1. capa de embedding
        self.embedding = nn.Embedding(mida_vocabulari, mida_vector_caracteristiques)
        #    pasamos los pesos del model_embedding a la capa
        self.embedding.weight = nn.Parameter(torch.from_numpy(model_embedding.vectors))
        #    (opcional) Documentación oficial "If True, the tensor does not get updated in the learning process"
        if freeze_embeddings:
            self.embedding.requires_grad = False
        
        # 2. capas convolucionales
        #    Se crean tantas capas convolucionales como kernels queramos, por defecto son 3.
        #    La entrada de cada capa es 1: 1 palabra
        #    La salida de cada capa es igual al tamaño del vector de característiques del corpus, normalment 100, 200 o 300
        self.convs_1d = nn.ModuleList([nn.Conv2d(1, 
                                                 num_filtres, 
                                                 (k, mida_vector_caracteristiques),    # [3, 100], [4, 100] i [5, 100]
                                                 padding = (k - 2, 0))                 # (1, 0), (2, 0) i (3, 0)
                                       for k in mides_kernels
                                      ]
                                     )
        
        # 3. capa fully-connected para la clasificación final
        self.fc = nn.Linear(len(mides_kernels) * num_filtres, mida_sortida) 
        
        # 4. capa de dropout
        self.dropout = nn.Dropout(drop_prob)
        
    
    def conv_and_pool(self, x, conv):
        x = F.relu(conv(x)).squeeze(3)
        
        x_max = F.max_pool1d(x, x.size(2)).squeeze(2)
        return x_max

    def forward(self, x):
        embeds = self.embedding(x)
        embeds = embeds.unsqueeze(1)
        
        conv_results = [self.conv_and_pool(embeds, conv) for conv in self.convs_1d]
        
        x = torch.cat(conv_results, 1)
        x = self.dropout(x)
        
        # flatten de la matriz al vector
        x = x.view(-1, len(mides_kernels) * num_filtres)
        
        x = self.fc(x)
        return x

Definimos la arquitectura (hiperparámetros)

mida_vocabulari = len(model_embedding.index2word)           # cantidad de palabras del corpus en català

mida_sortida = 4                                          # tamaño del resultado. IMPORTANTE QUE LOS DATOS DE LAS ETIQUETAS y_train, y_test tengan valores comprendidos entre 0 i (mida_sortida-1), de cara a calcular la función de costes CrossEntropyLoss
                                                            
        
mida_vector_caracteristiques = model_embedding.vector_size  # longitud del vector de características del corpus en català
num_filtres = 100                                           # número de filtros que se usarán en la convolución
mides_kernels = [3, 4, 5]                                   # tamaño del kernel a aplicar

net = ClassificaCNN(model_embedding, mida_vocabulari, mida_sortida, mida_vector_caracteristiques, num_filtres, mides_kernels)
                    
print(net)

ClassificaCNN(
  (embedding): Embedding(799020, 100)
  (convs_1d): ModuleList(
    (0): Conv2d(1, 100, kernel_size=(3, 100), stride=(1, 1), padding=(1, 0))
    (1): Conv2d(1, 100, kernel_size=(4, 100), stride=(1, 1), padding=(2, 0))
    (2): Conv2d(1, 100, kernel_size=(5, 100), stride=(1, 1), padding=(3, 0))
  )
  (fc): Linear(in_features=300, out_features=4, bias=True)
  (dropout): Dropout(p=0.5, inplace=False)
)
 


Ahora procedemos a entrenar el modelo:

#Definimos el ratio de entreno (learning rate), la función de costes (CrossEntropyLoss) i el optimizador.
lr = 0.001

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr = lr)
def train(net, train_loader, epochs):

    if(train_on_gpu):
        net.cuda()

    valid_loss_min = np.Inf

    for epoch in range(1, epochs+1):
        train_loss = 0.0
        valid_loss = 0.0

        net.train()
        for batch_idx, (inputs, labels) in enumerate(train_loader):
            if train_on_gpu:
                inputs, labels = inputs.cuda(), labels.cuda()

            net.zero_grad()
            
            inputs = inputs.type(torch.LongTensor)
            output = net(inputs)
            loss = criterion(output.squeeze(), labels.long())
            loss.backward()
            optimizer.step()
            train_loss += loss.item() * inputs.size(0)

        net.eval()
        for batch_idx, (inputs, labels) in enumerate(valid_loader):
            if train_on_gpu:
                inputs, labels = inputs.cuda(), labels.cuda()

            inputs = inputs.type(torch.LongTensor)
            output = net(inputs)
            loss = criterion(output.squeeze(), labels.long())
            valid_loss += loss.item() * inputs.size(0)

        train_loss = train_loss/len(train_loader.sampler)
        valid_loss = valid_loss/len(valid_loader.sampler)

        print('Època: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format(epoch, train_loss, valid_loss))

        if valid_loss <= valid_loss_min:
            print('Validation loss disminueix ({:.6f} --> {:.6f}).  Desant el model ...'.format(valid_loss_min, valid_loss))
            torch.save(net.state_dict(), 'model_classificacio.pt')
            valid_loss_min = valid_loss

Definimos 20 épocas y entrenamos

epochs = 20

train(net, train_loader, epochs)

Època: 1 	Training Loss: 1.156036 	Validation Loss: 1.366987
Validation loss disminueix (inf --> 1.366987).  Desant el model ...
Època: 2 	Training Loss: 1.031535 	Validation Loss: 1.347878
Validation loss disminueix (1.366987 --> 1.347878).  Desant el model ...
Època: 3 	Training Loss: 0.950746 	Validation Loss: 1.249238
Validation loss disminueix (1.347878 --> 1.249238).  Desant el model ...
Època: 4 	Training Loss: 0.873077 	Validation Loss: 1.205186
Validation loss disminueix (1.249238 --> 1.205186).  Desant el model ...
Època: 5 	Training Loss: 0.813947 	Validation Loss: 1.177342
Validation loss disminueix (1.205186 --> 1.177342).  Desant el model ...
Època: 6 	Training Loss: 0.745349 	Validation Loss: 1.161120
Validation loss disminueix (1.177342 --> 1.161120).  Desant el model ...
Època: 7 	Training Loss: 0.681362 	Validation Loss: 1.158835
Validation loss disminueix (1.161120 --> 1.158835).  Desant el model ...
Època: 8 	Training Loss: 0.626599 	Validation Loss: 1.132438
Validation loss disminueix (1.158835 --> 1.132438).  Desant el model ...
Època: 9 	Training Loss: 0.543402 	Validation Loss: 1.130284
Validation loss disminueix (1.132438 --> 1.130284).  Desant el model ...
Època: 10 	Training Loss: 0.480014 	Validation Loss: 1.123914
Validation loss disminueix (1.130284 --> 1.123914).  Desant el model ...
Època: 11 	Training Loss: 0.417576 	Validation Loss: 1.115234
Validation loss disminueix (1.123914 --> 1.115234).  Desant el model ...
Època: 12 	Training Loss: 0.369162 	Validation Loss: 1.106707
Validation loss disminueix (1.115234 --> 1.106707).  Desant el model ...
Època: 13 	Training Loss: 0.324353 	Validation Loss: 1.128301
Època: 14 	Training Loss: 0.264766 	Validation Loss: 1.163419
Època: 15 	Training Loss: 0.234643 	Validation Loss: 1.138339
Època: 16 	Training Loss: 0.196656 	Validation Loss: 1.173140
Època: 17 	Training Loss: 0.163963 	Validation Loss: 1.169462
Època: 18 	Training Loss: 0.148523 	Validation Loss: 1.175767
Època: 19 	Training Loss: 0.121358 	Validation Loss: 1.238442
Època: 20 	Training Loss: 0.105058 	Validation Loss: 1.250098

Una vez entrenado el modelo, podemos realizar el test del mismo, para ver qué % de aciertos tiene. En nuestro caso, con los parámetros de arquitectura, épocas y entradas tenemos:

Test Accuracy of Altres: 33% ( 1/ 3)
Test Accuracy of Politica i hisenda: 47% ( 8/17)
Test Accuracy of Ciutat: 86% (37/43)
Test Accuracy of Ciutada i Seguretat:  0% ( 0/ 9)

Test Accuracy (Overall): 63% (46/72)

Observamos que para la clasificación de ‘Ciudad’ tiene un acierto elevado, pero en cambio no así para ‘Ciudadano y Seguridad’, seguramente por la falta de muestras en el entreno.

Demo

Una vez tenemos un modelo entrenado podemos crear una pequeña prueba para demostrar el funcionamiento. Para ello usaremos Gradio.app.

Instalamos Gradio y cargamos el modelo entrenado.

!pip install gradio
import gradio as gr

net.load_state_dict(torch.load('model_classificacio.pt'))

Se define la función que ejecutaráGradio para clasificar los mensajes:

def classifica_tweets(el_tweet):
    tweet = []
    tweet = [neteja_text(el_tweet)]
    #print('el tweet: ', tweet)
    tt = tokenitza_textos(model_embedding, tweet)
    #print('tt: ', tt)
    X = padding(tt, 200)
    #print('X: ', X)

    net.eval()
    X = torch.from_numpy(X).type(torch.LongTensor)
    if train_on_gpu:
        net().cuda()
        X = X.cuda()

    output = F.softmax(net(X), dim=1)
    #print('output: ', output)

    return {classes[i]: float(output[0][i]) for i in range(len(classes))}

Y lanzamos la aplicación:

inputs = gr.inputs.Textbox(lines=5, label="Enganxa el Tweet aquí: ")
outputs = gr.outputs.Label(num_top_classes=3)
gr.Interface(fn=classifica_tweets, inputs=inputs, outputs=outputs).launch()

Algunos resultados:

Test Gradio

Test Gradio

A pesar de tener un conjunto de entreno limitado que hace que las clasificaciones no tengan un porcentaje elevado de selección, podemos comprobar que la metodología aplicada funciona y es cuestión de tener más datos de calidad para poder hacer mejores clasificaciones.


See also