Fasttext para clasificacion de sentimientos en NLP (textos en español)

6 minute read

Introducción

Ya habia hablado un poco de fasttext, como habia dicho es un implementación de wordvectors pero que aprovecha las llamadas subword-representation caracteristicas para añadir mas información al modelo, para ver el post anterior aquí esta la liga https://adrian-rdz.github.io/NLP_wordvectors_p2/

Anteriormente habia usado la librería directamente en bash, pero en la misma pagina de github existe un wrapper para usarlo en python entonces quise aplicarlo a un conjunto de datos de los que tengo por ahi y el cual habia usado para otro post https://adrian-rdz.github.io/Naive-Bayes-Seniment-Analysis/ , al igual que en ese post el objetivo sera clasificación del sentimientopero ahora usando las representaciones vectoriales, con la librería fasttext nos podemos permitir realizar aprendizaje supervisado y no supervisado, en el aprendizaje supervisado al igual que cuando lo use directamente en consola hay que armar un archivo el cual contenga una linea por documento, donde cada linea tiene las etiquetas con el formato __label__etiqueta, para este caso la etiqueta es yes y no para sentimiento positivo y negativo resepectivamente.

Algo interesante de la implementación de fasttext para clasificación es que permite realizar multilabel classification esto, es que una misma instancia o documento puede tener mas de una etiqueta asignada, tal como los tags en un post web, o en una imagen detectar la presencia de mas de un objeto etc..

Código

Ahora si el código

importamos las librerías necesarias

import os
import numpy
import fastText
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

Se crea una función para representar la salida formateada (tomada del código de ejemplo de fasttext)

def print_results(N, p, r):
    print("N\t" + str(N))
    print("P@{}\t{:.3f}".format(1, p))
    print("R@{}\t{:.3f}".format(1, r))

Vemos en que directorio estamos posicionado con los comandos magicos de ipython

!pwd
/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP

Vemos los directorios disponibles

ls SFU_spanish_reviews_train_test/train/
no  yes

Conatamos el numero de archivos disponibles de etiqueta positiva

!ls SFU_spanish_reviews_train_test/train/yes/ | wc -l
160

Conatamos el numero de archivos disponibles de etiqueta negativa

!ls SFU_spanish_reviews_train_test/train/no/ | wc -l
160

Creamos unas variables para guardar los paths postivo y negativo

path_yes="/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/yes/"
path_no="/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/no/"

Vemos igualmente ahora con python el numero de positivos y negativos

print(len(os.listdir(path_yes))) # numero de archivos yes
print(len(os.listdir(path_no))) # numero de archivos no
160
160

A partir de aquí voy hacer algo que quizas sea algo arcaico dado que voy a mezclar código python con bash, dado que realizare unos preprocesos y pues como no soy experto en bash y no tengo a la mano ciertos comandos de python mezclare ambos lenguajes para lograr mi objetivo.

He aqui un ejemplo de mezclar python con bash (Se puede ver que la ultima linea del siguiente bloque de código es un comando de bash y para hacer que funcione en jupyter-notebook o en ipython es necesario anteponer un signo de admiracion )

for i in os.listdir(path_yes):
    archivo_temp = path_yes+i
    !cat $archivo_temp | wc -l
6
97
32
23
....

Dado que mis archivos estan en un encoding diferente a UTF-8 (ISO-8859-1) hare un preproceso mezclando python y bash, es algo medio sucio el código pero resuelve el problema

## archivo con encoding raro
!cat $archivo_temp

No se muestra la salida, pero al verlo sin cambiar el encoding se ven caracteres extraños

Otro ejemplo de mezclar python con bash es guardar la salida de un comando bash en una variable python como el código que sigue:

#Guardamos la salida de un comando bash en una variable de python
archivo_temp_salida =!echo $archivo_temp

Lo que se hara para convertir a utf8 los documentos de texto sera crera 2 carpetas donde se almancene un copia en ese formato destino no_utf8 y yes_utf8

path_yes_new = path_yes+".."
!ls $path_yes_new
no  no_utf8  yes  yes_utf8

Para convertir el encoding usare el comando o utileria iconv que se puede llamar desde bash

código de ejemplo:

# hay una utileria en bash para convertir entre encodings la usare
##aqui convierto un archivo de ISO a utf-8
!iconv -f ISO-8859-1 -t UTF-8 $archivo_temp

Cremos variables python que apunten a los destinos donde residiran los documentos convertidos a formato utf8

path_yes_new = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/yes_utf8/"
path_no_new = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/no_utf8/"

Creamos las carpetas

#Creo las 2 carpetas destino
%mkdir $path_yes_new
%mkdir $path_no_new

Si ya las tienes creadas te mostrara error claramente:

mkdir: no se puede crear el directorio «/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/yes_utf8/»: El archivo ya existe
mkdir: no se puede crear el directorio «/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/train/no_utf8/»: El archivo ya existe

Ahora lo mismo para las carpetas de Test

path_yes_test = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/test/yes/"
path_no_test = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/test/no/"
path_yes_new_test = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/test/yes_utf8/"
path_no_new_test = "/home/adrianrdzv/Escritorio/VACACIONES_DIC2018/NLP/SFU_spanish_reviews_train_test/test/no_utf8/"
#Creo las 2 carpetas destino
%mkdir $path_yes_new_test
%mkdir $path_no_new_test

códigos para convertir a utf8

Convertir a utf8 toda la carpeta yes para Train

#Mezclando python con bash
for i in os.listdir(path_yes):
    archivo_temp = path_yes+i
    archivo_destino = path_yes_new+i
    !iconv -f ISO-8859-1 -t UTF-8 $archivo_temp > $archivo_destino

Convertirt a utf8 toda la carpeta no

#Mezclando python con bash
for i in os.listdir(path_no):
    archivo_temp = path_no+i
    archivo_destino = path_no_new+i
    !iconv -f ISO-8859-1 -t UTF-8 $archivo_temp > $archivo_destino

Convertir a utf8 toda la carpeta yes para Test

#Mezclando python con bash
for i in os.listdir(path_yes_test):
    archivo_temp = path_yes_test+i
    archivo_destino = path_yes_new_test+i
    !iconv -f ISO-8859-1 -t UTF-8 $archivo_temp > $archivo_destino
#Mezclando python con bash
for i in os.listdir(path_no_test):
    archivo_temp = path_no_test+i
    archivo_destino = path_no_new_test+i
    !iconv -f ISO-8859-1 -t UTF-8 $archivo_temp > $archivo_destino

Ya habiendo terminado el preproceso usando iconv para cambiar el formato a los archivos, ahora procedemos a dejar en el formato que lo espera fasttext

Crear archivos con documentos etiquetado con el formato que espera fasttext

Crear un solo archivo etiquetado para train

lista_yes = [] 
for i in os.listdir(path_yes_new):
    file = open(path_yes_new+i,"r")
    texto = file.read()
    lista_yes.append(texto)
    
lista_no = [] 
for i in os.listdir(path_no_new):
    file = open(path_no_new+i,"r")
    texto = file.read()
    lista_no.append(texto)

Crear un solo archivo etiquetado para test

lista_yes_test = [] 
for i in os.listdir(path_yes_new_test):
    file = open(path_yes_new_test+i,"r")
    texto = file.read()
    lista_yes_test.append(texto)
    
lista_no_test = [] 
for i in os.listdir(path_no_new_test):
    file = open(path_no_new_test+i,"r")
    texto = file.read()
    lista_no_test.append(texto)
lista_yes[0][0:20]
'Vamos a ser sinceros'
lista_no[0][0:20]
'Hola voy a hablaros '
lista_yes_test[0][0:20]
'Hace unos meses se n'

El formato que soliocitar fasttext para hacer clasificación es una linea por texto o documento y que se tenga el marcado __label__clase donde clase es la etiqueta en este caso sera yes y no

with open(path_yes+"../"+"etiquetado_train.txt","w") as file:
    for i in lista_yes:
        file.write("__label__yes "+i.replace("\n"," ")+"\n")
with open(path_yes+"../"+"etiquetado_train.txt","a") as file:
    for i in lista_no:
        file.write("__label__no "+i.replace("\n"," ")+"\n")

Ahora crear archivo etiquetado para test

with open(path_yes+"../"+"etiquetado_test.txt","w") as file:
    for i in lista_yes_test:
        file.write("__label__yes "+i.replace("\n"," ")+"\n")
with open(path_yes+"../"+"etiquetado_test.txt","a") as file:
    for i in lista_no_test:
        file.write("__label__no "+i.replace("\n"," ")+"\n")

Entrenar modelo de clasificación con fasttext

Para entrenar el modelo supervisado con fasttext es algo similar a como se hace directo con la aplicación de consola, y los parametros mantienen los mismos nombres la única diferencia es que aquí se usan unos metodos wrapper de python.

train_data = path_yes+"../"+"etiquetado_train.txt"
test_data = path_yes+"../"+"etiquetado_test.txt"
    # train_supervised uses the same arguments and defaults as the fastText cli
    model = fastText.train_supervised(
        input=train_data, epoch=50, lr=1, wordNgrams=4, verbose=2, minCount=2,dim=200,neg=5,loss="hs"
    )
    print_results(*model.test(train_data))

Accuracy en train

N	320
P@1	0.994
R@1	0.994

Accuracy en test

En train parece jalar o sobreajustar muy bien ahora en test, veamos que tal

print_results(*model.test(test_data))
N	80
P@1	0.775
R@1	0.775

A diferencia de los modelos del post previo lo mas alto que se lograba era un 70% de accuracy en test, aqui el accuracy ronda entre 77 y 80 en varias corridas por lo cual si representa una mejora y ademas falta realizar mas tuneo de paraemtros, lo cual podría mejorar aun mas el accuracy en test

Referencias

  • https://fasttext.cc/docs/en/supervised-tutorial.html
  • https://fasttext.cc/docs/en/options.html
  • https://github.com/facebookresearch/fastText/blob/master/python/doc/examples/train_supervised.py
  • https://stackoverflow.com/questions/147741/character-reading-from-file-in-python
  • https://stackoverflow.com/questions/20263909/converting-ansi-to-utf-8-in-shell
  • https://www.pythonforbeginners.com/files/reading-and-writing-files-in-python