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.
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.
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.
Os dejo una serie de videos de Dot CSV donde explica en detalle qué es y cómo funcionan las 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:
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
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:
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.
Agradecimientos
Este proyecto se ha realizado en el marco de la formación de IA aplicada a l’Administració Pública de Diputació de Tarragona. Agradecer a Pascual Olivas (Ajuntament de Reus), Jordi Besora (Ajuntament de Valls) y Lluís Galimany (Ajuntament de El Vendrell) por haber compartido su conocimiento para superar el reto del curso.