Blog
Proyecto: Desarrollar un Modelo Neural de Bolsa de Palabras para el Análisis de Sentimientos
- Publicado por: Rafael Fernandez
- Categoría: Natural Language Processing
Las críticas o comentarios de películas pueden ser clasificadas como favorables o no. La evaluación del texto de la revisión de la película es un problema de la clasificación a menudo llamado análisis de sentimientos. Una técnica popular para desarrollar modelos de análisis de sentimientos es usar un modelo de bolsa de palabras que transforma los documentos en vectores, donde a cada palabra del documento se le asigna una puntuación. En esta parte, descubrirás cómo puedes desarrollar un modelo predictivo de Deep Learning utilizando la representación de la bolsa de palabras para la clasificación de sentimientos de la crítica cinematográfica. Después de completar esta parte, tú sabrás:
- Cómo preparar los datos del texto de revisión para el modelado con un vocabulario restringido.
- Cómo usar el modelo de la bolsa de palabras para preparar los datos del entrenamiento, y las pruebas.
- Cómo desarrollar un modelo de bolsa de palabras multicapa de Perceptron, y utilizarlo para hacer predicciones sobre nuevos datos de texto de revisión.
Vamos a empezar.
Descripción del tutorial: Desarrollar un Modelo Neural de Bolsa de Palabras para el Análisis de Sentimientos
Este tutorial está dividido en las siguientes partes:
- Conjunto de datos de revisión de películas
- Preparación de datos
- Representación de la bolsa de palabras
- Modelos de Análisis de Sentimientos
- Comparación de métodos de valoración de palabras
- Prediciendo Sentimientos para Nuevas Revisiones
Conjunto de datos de revisión de películas
En este tutorial, utilizaremos el conjunto de datos de Movie Review. Este conjunto de datos diseñado para el análisis de sentimientos se describió anteriormente en el tutorial 3.2. Puede descargar el conjunto de datos desde aquí:
- Movie Review Polarity Dataset (review polarity.tar.gz, 3MB).
http://www.cs.cornell.edu/people/pabo/movie-review-data/review_polarity.tar.gz
Después de descomprimir el archivo, tendrá un directorio llamado txt sentoken con dos subdirectorios que contienen el texto neg y pos para comentarios negativos y positivos. Las revisiones se almacenan una por archivo con una convención de nomenclatura cv000 a cv999 para cada uno de los formatos neg y pos. A continuación, veamos cómo cargar los datos de texto.
Preparación de datos
La preparación del conjunto de datos de críticas de películas se describió por primera vez en el post anterior.
En esta sección, veremos tres cosas:
- Separación de datos en equipos de entrenamiento y de prueba.
- Cargar y limpiar los datos para eliminar la puntuación y los números.
- Definir un vocabulario de palabras preferidas
Dividir en entrenamiento y conjuntos de pruebas
Estamos pretendiendo desarrollar un sistema que puede predecir el sentimiento de una crítica cinematográfica textual como positivo o negativo. Esto significa que después de que el modelo sea desarrollado, necesitaremos hacer predicciones sobre nuevas revisiones textuales. Esto requerirá que se lleve a cabo la misma preparación de datos en las nuevas revisiones que se realiza en los datos de capacitación para el modelo.
Nos aseguraremos de que esta restricción se incorpore en la evaluación de nuestros modelos dividiendo los conjuntos de datos de formación y de prueba antes de cualquier preparación de datos. Esto significa que cualquier conocimiento en el conjunto de pruebas que pueda ayudarnos a preparar mejor los datos (por ejemplo, las palabras utilizadas) no está disponible durante la preparación de los datos y la capacitación del modelo. Dicho esto, utilizaremos las últimas 100 revisiones positivas y las últimas 100 negativas como conjunto de pruebas (100 revisiones) y las restantes 1.800 revisiones como conjunto de datos de formación. Esto es un entrenamiento del 90%, con una división del 10% de los datos. La división puede imponerse fácilmente utilizando los nombres de archivo de las revisiones, donde las revisiones nombradas de 000 a 899 son para datos de formación y las nombradas de 900 en adelante son para probar el modelo.
Comentarios sobre la carga y la limpieza
Los datos de texto ya están bastante limpios, por lo que no se requiere mucha preparación. Sin entrar demasiado en detalles, prepararemos los datos utilizando el siguiente método:
- Dividir tokens en espacios en blanco
- Eliminar todos los signos de puntuación de las palabras.
- Elimine todas las palabras que no estén compuestas únicamente de caracteres alfabéticos.
- Elimine todas las palabras que son palabras de parada conocidas.
- Elimine todas las palabras que tengan una longitud de ≤ 1 carácter.
Podemos poner todos estos pasos en una función llamada clean_doc() que toma como argumento el texto crudo cargado desde un archivo y devuelve una lista de tokens limpios. También podemos definir una función load_doc() que carga un documento desde un archivo listo para su uso con la función clean_doc(). Un ejemplo de la limpieza de la primera revisión positiva se enumeran a continuación.
from nltk.corpus import stopwords
import string
import re
# cargar el documento en la memoria
def load_doc(filename):
# abra el archivo como de solo lectura
file = open(filename, 'r')
# leer todo el texto
text = file.read()
# cierra el archivo
file.close()
return text
# convertir un documento en tokens limpios
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminar puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no son alfabéticos
tokens = [word for word in tokens if word.isalpha()]
# filtrar las palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
#cargar el documento
filename = 'txt_sentoken/pos/cv000_29590.txt'
text = load_doc(filename)
tokens = clean_doc(text)
print(tokens)
Al ejecutar el ejemplo se imprime una larga lista de tokens limpios. Hay muchos más pasos de limpieza que tal vez queramos explorar, y los dejo como ejercicios adicionales. Me encantaría ver qué se te ocurre.
['films', 'adapted', 'comic', 'books', 'plenty', 'success', 'whether', 'theyre', 'superheroes', 'batman', 'superman', 'spawn', 'geared', 'toward', 'kids', 'casper', 'arthouse', 'crowd', 'ghost', 'world', 'theres', 'never', 'really', 'comic', 'book', 'like', 'hell', 'starters', 'created', 'alan', 'moore', 'eddie', 'campbell', 'brought', 'medium', 'whole', 'new', 'level', 'mid', 'series', 'called', 'watchmen', 'say', 'moore', 'campbell', 'thoroughly', 'researched', 'subject', 'jack', 'ripper', 'would', 'like', 'saying', 'michael', 'jackson', 'starting', 'look', 'little', 'odd', 'book', 'graphic', 'novel', 'pages', 'long', 'includes', 'nearly', 'consist', 'nothing', 'footnotes', 'words', 'dont', 'dismiss', 'film', 'source', 'get', 'past', 'whole', 'comic', 'book', 'thing', 'might', 'find', 'another', 'stumbling', 'block', 'hells', 'directors', 'albert', 'allen', 'hughes', 'getting', 'hughes', 'brothers', 'direct', 'seems', 'almost', 'ludicrous', 'casting', 'carrot', 'top', 'well', 'anything', 'riddle', 'better', 'direct', 'film', 'thats', 'set', 'ghetto', 'features', 'really', 'violent', 'street', 'crime', 'mad', 'geniuses', 'behind', 'menace', 'ii', 'society', 'ghetto', 'question', 'course', 'whitechapel', 'londons', 'east', 'end', 'filthy', 'sooty', 'place', 'whores', 'called', 'unfortunates', 'starting', 'get', 'little', 'nervous', 'mysterious', 'psychopath', 'carving', 'profession', 'surgical', 'precision', 'first', 'stiff', 'turns', 'copper', 'peter', 'godley', 'robbie', 'coltrane', 'world', 'enough', 'calls', 'inspector', 'frederick', 'abberline', 'johnny', 'depp', 'blow', 'crack', 'case', 'abberline', 'widower', 'prophetic', 'dreams', 'unsuccessfully', 'tries', 'quell', 'copious', 'amounts', 'absinthe', 'opium', 'upon', 'arriving', 'whitechapel', 'befriends', 'unfortunate', 'named', 'mary', 'kelly', 'heather', 'graham', 'say', 'isnt', 'proceeds', 'investigate', 'horribly', 'gruesome', 'crimes', 'even', 'police', 'surgeon', 'cant', 'stomach', 'dont', 'think', 'anyone', 'needs', 'briefed', 'jack', 'ripper', 'wont', 'go', 'particulars', 'say', 'moore', 'campbell', 'unique', 'interesting', 'theory', 'identity', 'killer', 'reasons', 'chooses', 'slay', 'comic', 'dont', 'bother', 'cloaking', 'identity', 'ripper', 'screenwriters', 'terry', 'hayes', 'vertical', 'limit', 'rafael', 'yglesias', 'les', 'mis', 'rables', 'good', 'job', 'keeping', 'hidden', 'viewers', 'end', 'funny', 'watch', 'locals', 'blindly', 'point', 'finger', 'blame', 'jews', 'indians', 'englishman', 'could', 'never', 'capable', 'committing', 'ghastly', 'acts', 'hells', 'ending', 'whistling', 'stonecutters', 'song', 'simpsons', 'days', 'holds', 'back', 'electric', 'carwho', 'made', 'steve', 'guttenberg', 'star', 'dont', 'worry', 'itll', 'make', 'sense', 'see', 'onto', 'hells', 'appearance', 'certainly', 'dark', 'bleak', 'enough', 'surprising', 'see', 'much', 'looks', 'like', 'tim', 'burton', 'film', 'planet', 'apes', 'times', 'seems', 'like', 'sleepy', 'hollow', 'print', 'saw', 'wasnt', 'completely', 'finished', 'color', 'music', 'finalized', 'comments', 'marilyn', 'manson', 'cinematographer', 'peter', 'deming', 'dont', 'say', 'word', 'ably', 'captures', 'dreariness', 'victorianera', 'london', 'helped', 'make', 'flashy', 'killing', 'scenes', 'remind', 'crazy', 'flashbacks', 'twin', 'peaks', 'even', 'though', 'violence', 'film', 'pales', 'comparison', 'blackandwhite', 'comic', 'oscar', 'winner', 'martin', 'childs', 'shakespeare', 'love', 'production', 'design', 'turns', 'original', 'prague', 'surroundings', 'one', 'creepy', 'place', 'even', 'acting', 'hell', 'solid', 'dreamy', 'depp', 'turning', 'typically', 'strong', 'performance', 'deftly', 'handling', 'british', 'accent', 'ians', 'holm', 'joe', 'goulds', 'secret', 'richardson', 'dalmatians', 'log', 'great', 'supporting', 'roles', 'big', 'surprise', 'graham', 'cringed', 'first', 'time', 'opened', 'mouth', 'imagining', 'attempt', 'irish', 'accent', 'actually', 'wasnt', 'half', 'bad', 'film', 'however', 'good', 'strong', 'violencegore', 'sexuality', 'language', 'drug', 'content']
Definir un vocabulario
Es importante definir un vocabulario de palabras conocidas cuando se utiliza un modelo de bolsa de palabras. Cuantas más palabras, mayor será la representación de los documentos, por lo que es importante limitar las palabras a sólo las que se consideran predictivas. Esto es difícil de saber de antemano, y a menudo es importante probar diferentes hipótesis sobre cómo construir un vocabulario útil. Ya hemos visto cómo podemos eliminar la puntuación y los números del vocabulario en la sección anterior. Podemos repetir esto para todos los documentos y construir un conjunto de todas las palabras conocidas.
Podemos desarrollar un vocabulario como Counter, que es un diccionario de mapeo de palabras y su conteom que nos permite actualizar y consultar fácilmente. Cada documento puede ser añadido al contador (una nueva función llamada add_doc_to_vocab()) y podemos pasar por encima de todas las revisiones en el directorio negativo y luego en el positivo (una nueva función llamada process_docs()).
El ejemplo completo se enumera a continuación.
import string
import re
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
# cargar documento en la memoria
def load_doc(filename):
# abrir documento en modo solo lectura
file = open(filename, 'r')
# leer todo el texto
text = file.read()
# cerrar el documento
file.close()
return text
# convertir un documento en tokens limpias
def clean_doc(doc):
# dividir en tokens por espacio en blanco
tokens = doc.split()
# preparar regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminar puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no son alfabéticos
tokens = [word for word in tokens if word.isalpha()]
# filtrar palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# cargar documento y añadir al vocabulario
def add_doc_to_vocab(filename, vocab):
# cargar doc
doc = load_doc(filename)
# limpiar doc
tokens = clean_doc(doc)
# actualizar contadores
vocab.update(tokens)
# cargar todos los documentos en el directorio
def process_docs(directory, vocab):
# recorrer todos los archivos de la carpeta
for filename in listdir(directory):
# omitir cualquier revisión en el conjunto de pruebas
if filename.startswith('cv9'):
continue
# crear la ruta de acceso completa del archivo para abrir
path = directory + '/' + filename
# añadir doc al vocabulario
add_doc_to_vocab(path, vocab)
# define el vocabulario
vocab = Counter()
# añadir todos los documentos al vocabulario
process_docs('txt_sentoken/pos', vocab)
process_docs('txt_sentoken/neg', vocab)
# imprimir el tamaño del vocabulario
print(len(vocab))
# imprime las palabras más importantes en el vocabulario
print(vocab.most_common(50))
El ejemplo muestra que tenemos un vocabulario de 44.276 palabras. También podemos ver una muestra de las 50 palabras más usadas en las reseñas de películas. Tenga en cuenta que este vocabulario se construyó basándose sólo en las revisiones del conjunto de datos de la capacitación.
44276
[('film', 7983), ('one', 4946), ('movie', 4826), ('like', 3201), ('even', 2262), ('good', 2080), ('time', 2041), ('story', 1907), ('films', 1873), ('would', 1844), ('much', 1824), ('also', 1757), ('characters', 1735), ('get', 1724), ('character', 1703), ('two', 1643), ('first', 1588), ('see', 1557), ('way', 1515), ('well', 1511), ('make', 1418), ('really', 1407), ('little', 1351), ('life', 1334), ('plot', 1288), ('people', 1269), ('could', 1248), ('bad', 1248), ('scene', 1241), ('movies', 1238), ('never', 1201), ('best', 1179), ('new', 1140), ('scenes', 1135), ('man', 1131), ('many', 1130), ('doesnt', 1118), ('know', 1092), ('dont', 1086), ('hes', 1024), ('great', 1014), ('another', 992), ('action', 985), ('love', 977), ('us', 967), ('go', 952), ('director', 948), ('end', 946), ('something', 945), ('still', 936)]
Podemos repasar el vocabulario y eliminar todas las palabras que tengan una baja ocurrencia, como por ejemplo, que sólo se usen una o dos veces en todas las revisiones. Por ejemplo, el siguiente fragmento recuperará sólo los tokens que aparecen 2 o más veces en todas las revisiones.
# mantener tokens con una ocurrencia mínima min_occurrence = 2 tokens = [k for k,c in vocab.items() if c >= min_occurrence] print(len(tokens))
Finalmente, el vocabulario puede ser guardado en un nuevo archivo llamado vocab.txt que luego podemos cargar y usar para filtrar las reseñas de películas antes de codificarlas para modelarlas. Definimos una nueva función llamada save_list() que guarda el vocabulario en un archivo, con una palabra por línea. Por ejemplo:
# guardar lista en el archivo def save_list(lines, filename): # convertir líneas en una sola gota de texto data = '\n'.join(lines) # abrir documento file = open(filename, 'w') # escribir texto file.write(data) # cerrar documento file.close() # guardar tokens en un archivo de vocabulario save_list(tokens, 'vocab.txt')
Reuniendo todo esto, el ejemplo completo es puesto en una lista abajo.
import string
import re
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
# cargar dos en la memoria
def load_doc(filename):
# abrir en archivo en modo solo lectura
file = open(filename, 'r')
# leer todo el texto
text = file.read()
# cerrar el documento
file.close()
return text
# convertir a doc en tokens limpias
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar a regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminar la puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no estén en orden alfabético
tokens = [word for word in tokens if word.isalpha()]
# filtrar las palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar los tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# cargar doc y añadir al vocabulario
def add_doc_to_vocab(filename, vocab):
# cargar doc
doc = load_doc(filename)
# limpiar doc
tokens = clean_doc(doc)
# actualizar contadores
vocab.update(tokens)
# cargar todos los documentos en un directorio
def process_docs(directory, vocab):
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# añadir doc al vocabulario
add_doc_to_vocab(path, vocab)
# guardar lista en un archivo
def save_list(lines, filename):
# convertir líneas en una sola gota de texto
data = '\n'.join(lines)
# abrir archivo
file = open(filename, 'w')
# escribir texto
file.write(data)
# cerrar archivo
file.close()
# definir vocabulario
vocab = Counter()
# añadir todos los documentos al vocabulario
process_docs('txt_sentoken/pos', vocab)
process_docs('txt_sentoken/neg', vocab)
# imprimir el tamaño del vocabulario
print(len(vocab))
# guardar tokens con una ocurrencia mínima
min_occurrence = 2
tokens = [k for k,c in vocab.items() if c >= min_occurrence]
print(len(tokens))
# guardar tokens en un archivo de vocabulario
save_list(tokens, 'vocab.txt')
Ejecutando el ejemplo anterior con esta adición muestra que el tamaño del vocabulario baja un poco más de la mitad de su tamaño, de unas 44.000 a unas 25.000 palabras.
44276 25767
Ejecutando el filtro de ocurrencia mínima en el vocabulario y guardándolo en un archivo, ahora debería tener un nuevo archivo llamado vocab.txt con sólo las palabras que nos interesan
El orden de las palabras en su archivo será diferente, pero debe parecerse al siguiente:
aberdeen dupe burt libido hamlet arlene available corners web columbia ...
Representación de la bolsa de palabras
En esta sección, veremos cómo podemos convertir cada revisión en una representación que podamos proporcionar a un modelo de Perceptrón multicapa. Un modelo de bolsa de palabras es una forma de extraer características del texto para que la entrada de texto se pueda utilizar con algoritmos de aprendizaje automático como las redes neuronales. Cada documento, en este caso una revisión, se convierte en una representación vectorial. El número de elementos en el vector que representa un documento corresponde al número de palabras del vocabulario. Cuanto mayor sea el vocabulario, más larga será la representación vectorial, de ahí la preferencia por vocabularios más pequeños en la sección anterior. El modelo de la bolsa de palabras se introdujo anteriormente en el Capítulo 8.
Las palabras de un documento se califican y las puntuaciones se colocan en el lugar correspondiente de la representación. En la siguiente sección veremos los diferentes métodos de puntuación de palabras. En esta sección, nos ocupamos de convertir las revisiones en vectores listos para entrenar un primer modelo de red neuronal. Esta sección se divide en 2 pasos:
1. Convertir comentarios en líneas de tokens.
2. Codificación de revisiones con una representación de un modelo de bolsa de palabras.
Reseñas de Líneas de Tokens
Antes de que podamos convertir las revisiones en vectores para modelar, primero debemos limpiarlas. Esto implica cargarlos, realizar la operación de limpieza desarrollada anteriormente, filtrar las palabras que no están en el vocabulario elegido y convertir los tokens restantes en una sola cadena o línea lista para ser codificada. En primer lugar, necesitamos una función para preparar un documento. Abajo se muestra la función doc_ to_line() que cargará un documento, lo limpiará, filtrará los tokens que no están en el vocabulario, y luego devolverá el documento como una cadena de tokens separados por espacios en blanco.
# carga del doc, limpieza y línea de retorno de los tokens def doc_to_line(filename, vocab): # cargar doc doc = load_doc(filename) # limpiar doc tokens = clean_doc(doc) # filtrar el vocabulario tokens = [w for w in tokens if w in vocab] return ' '.join(tokens)
A continuación, necesitamos una función para trabajar a través de todos los documentos en un directorio (como pos y neg) para convertir los documentos en líneas. A continuación se muestra la función process_docs() que hace precisamente esto, esperando un nombre de directorio y un conjunto de vocabulario como argumentos de entrada y devolviendo una lista de los documentos procesados.
# cargar todos los documentos en un directorio
def process_docs(directory, vocab):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# cargar y limpiar doc
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
Podemos llamar al process_focs() consistentemente para revisiones positivas y negativas para construir un conjunto de datos de texto de revisión y sus etiquetas de salida asociadas, 0 para negativas y 1 para positivas. La función load_clean_dataset() de abajo implementa este comportamiento.
# cargar y limpiar un conjunto de datos
def load_clean_dataset(vocab):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab)
pos = process_docs('txt_sentoken/pos', vocab)
docs = neg + pos
# preparar las etiquetas
labels = [0 for _ in range(len(neg))] + [1 for _ in range(len(pos))]
return docs, labels
Finalmente, necesitamos cargar el vocabulario y convertirlo en un conjunto para su uso en revisiones de limpieza.
# cargar vocabulario vocab_filename = 'vocab.txt' vocab = load_doc(vocab_filename) vocab = set(vocab.split())
Todo esto lo podemos agrupar, reutilizando las funciones de carga y limpieza desarrolladas en los apartados anteriores. El ejemplo completo se enumera a continuación, demostrando cómo preparar las revisiones positivas y negativas del conjunto de datos de la capacitación.
import string
import re
from os import listdir
from nltk.corpus import stopwords
# cargar doc en la memoria
def load_doc(filename):
# abrir el archivo en solo lectura
file = open(filename, 'r')
# leer todo el documento
text = file.read()
# cerrar el documento
file.close()
return text
# convertir el Doc en tokens limpios
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar a regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminar la puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no estén en orden alfabético
tokens = [word for word in tokens if word.isalpha()]
# filtrar las palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar los tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# cargar doc, limpieza y línea de retorno de tokens
def doc_to_line(filename, vocab):
# cargar doc
doc = load_doc(filename)
# limpiar doc
tokens = clean_doc(doc)
# filtrar el vocabulario
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# cargar todos los documentos en el vocabulario
def process_docs(directory, vocab):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# cargar y limpiar doc
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
# cargar y limpiar un conjunto de datos
def load_clean_dataset(vocab):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab)
pos = process_docs('txt_sentoken/pos', vocab)
docs = neg + pos
# preparar etiquetas
labels = [0 for _ in range(len(neg))] + [1 for _ in range(len(pos))]
return docs, labels
# cargar el vocabulario
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)
# cargar todas las revisiones de entrenamiento
docs, labels = load_clean_dataset(vocab)
# resumir lo que tenemos
print(len(docs), len(labels))
Al ejecutar este ejemplo se carga y limpia el texto de revisión y se devuelven las etiquetas.
1800 1800
Comentarios de películas sobre los vectores de la bolsa de palabras
Usaremos la API de Keras para convertir revisiones a vectores de documentos codificados. Keras proporciona la clase Tokenizer que puede hacer algunas de las tareas de limpieza y definición de vocabulario de las que nos encargamos en la sección anterior. Es mejor hacerlo nosotros mismos para saber exactamente qué se hizo y por qué. Sin embargo, la clase Tokenizer es conveniente y transformará fácilmente documentos en vectores codificados. Primero, se debe crear el Tokenizer, y luego encajar en los documentos de texto en el conjunto de datos de formación. En este caso, se trata de la agregación de las matrices de líneas positivas y negativas desarrolladas en el apartado anterior.
# resumir lo que tenemos def create_tokenizer(lines): tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer
Este proceso determina una manera consistente de convertir el vocabulario a un vector de longitud fija con 25,768 elementos, que es el número total de palabras en el archivo de vocabulario vocab.txt. Luego, los documentos pueden ser codificados usando el Tokenizer llamando textos a matrix(). La función toma tanto una lista de documentos a codificar como un modo de codificación, que es el método utilizado para puntuar palabras en el documento. Aquí especificamos freq para puntuar las palabras en función de su frecuencia en el documento. Esto se puede utilizar para codificar los datos de entrenamiento y de prueba cargados, por ejemplo:
# datos codificados Xtrain = tokenizer.texts_to_matrix(train_docs, mode='freq') Xtest = tokenizer.texts_to_matrix(test_docs, mode='freq')
Esto codifica todas las revisiones positivas y negativas del conjunto de datos de formación.
A continuación, la función process_docs() de la sección anterior necesita ser modificada para procesar selectivamente las revisiones en el conjunto de datos de prueba o entrenamiento. Apoyamos la carga de los conjuntos de datos de entrenamiento y de prueba añadiendo un argumento is_train y usándolo para decidir qué nombres de archivo de revisión omitir.
#cargar todos los docuentos en el directorio
def process_docs(directory, vocab, is_train):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if is_train and filename.startswith('cv9'):
continue
if not is_train and not filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# cargar y limpiar el documento
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
Del mismo modo, el conjunto de datos load_clean_dataset() debe actualizarse para cargar los datos de entrenamiento o de la prueba y asegurarse de que devuelve una matriz NumPy
# cargar y limpiar el conjunto de datos
def load_clean_dataset(vocab, is_train):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab, is_train)
pos = process_docs('txt_sentoken/pos', vocab, is_train)
docs = neg + pos
# preparar etiquetas
labels = array([0 for _ in range(len(neg))] + [1 for _ in range(len(pos))])
return docs, labels
Podemos poner todo esto junto en un solo ejemplo.
import string
import re
from os import listdir
from numpy import array
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
# cargar a doc en la memoria
def load_doc(filename):
# abrir el archivo en modo solo lectra
file = open(filename, 'r')
# leer todo el texto
text = file.read()
# cerrar el archivo
file.close()
return text
# convertir dos en tokens limpios
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar a regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# remover la puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# remover los tokens que no están en orden alfabético
tokens = [word for word in tokens if word.isalpha()]
# filtrar palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar los tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# cargar doc, limpieza y línea de retorno de las tokens
def doc_to_line(filename, vocab):
# cargar documento
doc = load_doc(filename)
# limpiar documento
tokens = clean_doc(doc)
# filtrar vocabulario
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# cargar todos los documentos en el directorio
def process_docs(directory, vocab, is_train):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if is_train and filename.startswith('cv9'):
continue
if not is_train and not filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# cargar y limpiar doc
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
# cargar y limpiar el comjunto de datos
def load_clean_dataset(vocab, is_train):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab, is_train)
pos = process_docs('txt_sentoken/pos', vocab, is_train)
docs = neg + pos
# preparar etiquetas
labels = array([0 for _ in range(len(neg))] + [1 for _ in range(len(pos))])
return docs, labels
# instalar un tokenizador
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# cargar el vocabulario
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = set(vocab.split())
# cargar todas las revisiones
train_docs, ytrain = load_clean_dataset(vocab, True)
test_docs, ytest = load_clean_dataset(vocab, False)
# crear el tokenizer
tokenizer = create_tokenizer(train_docs)
# codificar datos
Xtrain = tokenizer.texts_to_matrix(train_docs, mode='freq')
Xtest = tokenizer.texts_to_matrix(test_docs, mode='freq')
print(Xtrain.shape, Xtest.shape)
Al ejecutar el ejemplo, se imprime tanto la forma del conjunto de datos de entrenamiento codificado como el conjunto de datos de prueba con 1.800 y 200 documentos respectivamente, cada uno con el mismo tamaño de vocabulario codificado (longitud vectorial).
(1800, 25768) (200, 25768)
Modelos de Análisis de Sentimientos
En esta sección, desarrollaremos modelos de Perceptrón multicapa (MLP) para clasificar documentos codificados como positivos o negativos. Los modelos serán simples modelos de red avanzados con capas totalmente conectadas llamadas densas en la biblioteca de aprendizaje profundo de Keras. Esta sección está dividida en 3 secciones:
1. Modelo de análisis del primer sentimiento
2. Comparación de modos de puntuación de palabras
3. Hacer una predicción para nuevas revisiones
Primer Modelo de Análisis de Sentimientos
Podemos desarrollar un modelo simple de MLP para predecir el sentimiento de las revisiones codificadas. El modelo tendrá una capa de entrada que iguala el número de palabras en el vocabulario y, a su vez, la longitud de los documentos de entrada. Podemos almacenar esto en una nueva variable llamada n_words, como sigue:
n_words = Xtest.shape[1]
Ahora podemos definir la red. Toda la configuración del modelo fue encontrada con muy poco ensayo y error y no debe ser considerada ajustada para este problema. Usaremos una sola capa oculta con 50 neuronas y una función de activación lineal rectificada. La capa de salida es una sola neurona con una función de activación sigmoide para predecir 0 para las revisiones negativas y 1 para las positivas. La red será entrenada utilizando la eficiente implementación de Adam de descenso en gradiente y la función binaria de pérdida de entropía cruzada, adecuada para problemas de clasificación binaria. Mantendremos un registro de la precisión cuando entrenemos y evaluemos el modelo.
# definir el modelo def define_model(n_words): # definir la red model = Sequential() model.add(Dense(50, input_shape=(n_words,), activation='relu')) model.add(Dense(1, activation='sigmoid')) # compilar red model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # resumir el modelo definido model.summary() return model
A continuación, podemos ajustar el modelo a los datos de entrenamiento; en este caso, el modelo es pequeño y se ajusta fácilmente en 10 épocas.
# red adecuada model.fit(Xtrain, ytrain, epochs=10, verbose=2)
Finalmente, una vez que el modelo es entrenado, podemos evaluar su desempeño haciendo predicciones en el conjunto de datos de prueba e imprimiendo la precisión.
# evaluar
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
print('Test Accuracy: %f' % (acc*100))
El ejemplo completo se enumera a continuación:
import string
import re
from os import listdir
from numpy import array
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
from keras.utils.vis_utils import plot_model
from keras.models import Sequential
from keras.layers import Dense
# cargar doc en la memoria
def load_doc(filename):
# abrir el archivo como de sólo lectura
file = open(filename, 'r')
# leer todo el texto
text = file.read()
# cerrar el archivo
file.close()
return text
# convertir a doc en tokens limpios
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar a regex para el filtrado de caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminar la puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no estén en orden alfabético
tokens = [word for word in tokens if word.isalpha()]
# filtrar las palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar los tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# cargar doc, limpiar y retornar línea de tokens
def doc_to_line(filename, vocab):
# cargar doc
doc = load_doc(filename)
# limpiar doc
tokens = clean_doc(doc)
# filtrar por vocabulario
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# cargar todos los documentos en un directorio
def process_docs(directory, vocab, is_train):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if is_train and filename.startswith('cv9'):
continue
if not is_train and not filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory + '/' + filename
# cargar y limpiar el documento
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
# cargar y limpiar un conjunto de datos
def load_clean_dataset(vocab, is_train):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab, is_train)
pos = process_docs('txt_sentoken/pos', vocab, is_train)
docs = neg + pos
# prepar labels
labels = array([0 for _ in range(len(neg))] + [1 for _ in range(len(pos))])
return docs, labels
# instalar un tokenizador
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# definir el modelo
def define_model(n_words):
# definir la red
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compilar la red
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# resumir el modelo definido
model.summary()
return model
# cargar el vocabulario
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = set(vocab.split())
# cargar todos los comentarios
train_docs, ytrain = load_clean_dataset(vocab, True)
test_docs, ytest = load_clean_dataset(vocab, False)
# crear el tokenizador
tokenizer = create_tokenizer(train_docs)
# codificador de datos
Xtrain = tokenizer.texts_to_matrix(train_docs, mode='freq')
Xtest = tokenizer.texts_to_matrix(test_docs, mode='freq')
# definir el modelo
n_words = Xtest.shape[1]
model = define_model(n_words)
# red adecuada
model.fit(Xtrain, ytrain, epochs=10, verbose=2)
# evaluar
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
print('Precision del Test: %f' % (acc*100))
Al ejecutar el ejemplo primero se imprime un resumen del modelo definido.
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_1 (Dense) (None, 50) 1288450 _________________________________________________________________ dense_2 (Dense) (None, 1) 51 ================================================================= Total params: 1,288,501 Trainable params: 1,288,501 Non-trainable params: 0 _________________________________________________________________
Un gráfico del modelo definido se guarda en un archivo con el nombre model.png.

Podemos ver que el modelo se ajusta fácilmente a los datos de entrenamiento dentro de las 10 épocas, logrando una precisión cercana al 100%. Evaluando el modelo en el conjunto de datos de la prueba, podemos ver que el modelo funciona bien, logrando una precisión por encima del 85%, muy por debajo del valor aproximado de los niveles bajos a mediados de los 80 visto en el documento original. Sin embargo, es importante notar que esta no es una comparación de manzanas con manzanas, ya que el documento original usó una validación cruzada 10 veces mayor para estimar la habilidad del modelo en lugar de una sola división entrenamiento/prueba.
Dada la naturaleza estocástica de las redes neuronales, sus resultados específicos pueden variar. Considere la posibilidad de ejecutar el ejemplo varias veces.
... Epoch 7/10 - 1s - loss: 0.5221 - acc: 0.9444 Epoch 8/10 - 1s - loss: 0.4787 - acc: 0.9539 Epoch 9/10 - 1s - loss: 0.4370 - acc: 0.9567 Epoch 10/10 - 1s - loss: 0.3971 - acc: 0.9617 Precision del Test: 85.500000
A continuación, veamos cómo probar diferentes métodos de puntuación de palabras para el modelo de bolsa de palabras.
Comparación de métodos de valoración de palabras
La función texts_to_matrix() para el Tokenizer en la API de Keras proporciona 4 métodos diferentes para puntuar palabras;
- binario Donde las palabras están marcadas como presentes (1) o ausentes (0).
- Conteo Donde el conteo de ocurrencias para cada palabra está marcado como un número entero.
- tfidf Donde cada palabra es puntuada en base a su frecuencia, donde las palabras que son comunes a todos los documentos son penalizadas.
- freq Cuando las palabras se califican en base a su frecuencia de ocurrencia dentro del documento.
Podemos evaluar la habilidad del modelo desarrollado en la sección anterior usando cada uno de los 4 modos de puntuación de palabras soportados. Esto implica el desarrollo de una función para crear una codificación de los documentos cargados basada en un modelo de puntuación elegido. La función crea el tokenizer, lo encaja en los documentos de formación y, a continuación, crea el entrenamiento y prueba los encodings utilizando el modelo elegido. La función prepare_data() implementa este comportamiento con listas de documentos de entrenamiento y prueba.
# preparar la codificación de los documentos en bolsas de palabras def prepare_data(train_docs, test_docs, mode): # crear el tokenizer tokenizer = Tokenizer() # ajustar el tokenizador a los documentos tokenizer.fit_on_texts(train_docs) # codificar el conjunto de datos de formación Xtrain = tokenizer.texts_to_matrix(train_docs, mode=mode) # codificar el conjunto de datos de formación Xtest = tokenizer.texts_to_matrix(test_docs, mode=mode) return Xtrain, Xtest
También necesitamos una función para evaluar el MLP dada una codificación específica de los datos. Debido a que las redes neuronales son estocásticas, pueden producir resultados diferentes cuando el mismo modelo encaja en los mismos datos. Esto se debe principalmente a los pesos iniciales aleatorios y a la mezcla de patrones durante el descenso del gradiente de minilotes. Esto significa que cualquier puntuación de un modelo es poco fiable y debemos estimar la habilidad del modelo basándonos en un promedio de múltiples ejecuciones. La siguiente función, denominada evaluate_mode(), toma documentos codificados y evalúa la MLP entrenándola en el conjunto de ejercicios y estimando la habilidad en el conjunto de pruebas 10 veces y devuelve una lista de las puntuaciones de precisión en todas estas ejecuciones.
# evaluar un modelo de red neuronal
def evaluate_mode(Xtrain, ytrain, Xtest, ytest):
scores = list()
n_repeats = 30
n_words = Xtest.shape[1]
for i in range(n_repeats):
# definir red
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compilar red
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit network
model.fit(Xtrain, ytrain, epochs=10, verbose=2)
# evaluar
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
scores.append(acc)
print('%d precision: %s'% ((i+1), acc))
return scores
Ahora estamos listos para evaluar el rendimiento de los 4 diferentes métodos de puntuación de palabras. Juntando todo esto, el ejemplo completo se muestra a continuación.
import string
import re
from os import listdir
from numpy import array
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
from keras.models import Sequential
from keras.layers import Dense
from pandas import DataFrame
from matplotlib import pyplot
# carga doc en la memoria
def load_doc(filename):
# abrir el archivo como de sólo lectura
file = open(filename,'r')
# leer todo el texto
text = file.read()
# cerrar el archivo
file.close()
return text
# convertir a un médico en tokens limpias
def clean_doc(doc):
# dividido en tokens por espacio en blanco
tokens = doc.split()
# preparar a regex para el filtrado de caracteres
re_punc = re.compile('[%s]'% re.escape(string.punctuation))
# eliminar la puntuación de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# eliminar los tokens restantes que no estén en orden alfabético
tokens = [word for word in tokens if word.isalpha()]
# filtrar las palabras de parada
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filtrar los tokens cortos
tokens = [word for word in tokens if len(word) > 1]
return tokens
# doc de carga, limpieza y línea de retorno de las tokens
def doc_to_line(filename, vocab):
# cargar el documento
doc = load_doc(filename)
# expediente limpio
tokens = clean_doc(doc)
# filtrar por vocabulario
tokens = [w for w in tokens if w in vocab]
return''.join(tokens)
# cargar todos los documentos en un directorio
def process_docs(directory, vocab, is_train):
lines = list()
# revisar todos los archivos de la carpeta
for filename in listdir(directory):
# omita cualquier comentario en el juego de pruebas
if is_train and filename.startswith('cv9'):
continue
if not is_train and not filename.startswith('cv9'):
continue
# crear la ruta completa del archivo a abrir
path = directory +'/'+ filename
# cargar y limpiar el documento
line = doc_to_line(path, vocab)
# añadir a la lista
lines.append(line)
return lines
# cargar y limpiar un conjunto de datos
def load_clean_dataset(vocab, is_train):
# cargar documentos
neg = process_docs('txt_sentoken/neg', vocab, is_train)
pos = process_docs('txt_sentoken/pos', vocab, is_train)
docs = neg + pos
# preparar las etiquetas
labels = array([0 for _ in range(len(neg))] + [1 for _ in range(len(pos))])
return docs, labels
# definir el modelo
def define_model(n_words):
# definir la red
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compilar red
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# evaluar un modelo de red neuronal
def evaluate_mode(Xtrain, ytrain, Xtest, ytest):
scores = list()
n_repeats = 10
n_words = Xtest.shape[1]
for i in range(n_repeats):
# definir la red
model = define_model(n_words)
# red adecuada
model.fit(Xtrain, ytrain, epochs=10, verbose=0)
# evaluar
_, acc = model.evaluate(Xtest, ytest, verbose=0)
scores.append(acc)
print('%d precision: %s'% ((i+1), acc))
return scores
# preparar la bolsa de palabras que codifican los documentos
def prepare_data(train_docs, test_docs, mode):
# crear el tokenizador
tokenizer = Tokenizer()
# ajustar el tokenizador a los documentos
tokenizer.fit_on_texts(train_docs)
# codificar el conjunto de datos de formación
Xtrain = tokenizer.texts_to_matrix(train_docs, mode=mode)
# codificar el conjunto de datos de formación
Xtest = tokenizer.texts_to_matrix(test_docs, mode=mode)
return Xtrain, Xtest
# cargar el vocabulario
vocab_filename ='vocab.txt'
vocab = load_doc(vocab_filename)
vocab = set(vocab.split())
# cargar todos los comentarios
train_docs, ytrain = load_clean_dataset(vocab, True)
test_docs, ytest = load_clean_dataset(vocab, False)
# experimento de ejecución
modes = ['binary','count','tfidf','freq']
results = DataFrame()
for mode in modes:
# preparar los datos para el modo
Xtrain, Xtest = prepare_data(train_docs, test_docs, mode)
# evaluar el modelo sobre los datos para el modo
results[mode] = evaluate_mode(Xtrain, ytrain, Xtest, ytest)
# resumir los resultados
print(results.describe())
# resultados de gráficos
results.boxplot()
pyplot.show()
Al final de la ejecución, se proporcionan estadísticas resumidas para cada método de puntuación de palabras, resumiendo la distribución de las puntuaciones de habilidades del modelo en cada una de las 10 ejecuciones por modo. Podemos ver que la puntuación media tanto del método de recuento como del método binario parece ser mejor que la de freq y tfidf.
Dada la naturaleza estocástica de las redes neuronales, sus resultados específicos pueden variar. Considere la posibilidad de ejecutar el ejemplo varias veces.
binary count tfidf freq count 10.000000 10.000000 10.000000 10.000000 mean 0.927000 0.903500 0.876500 0.871000 std 0.011595 0.009144 0.017958 0.005164 min 0.910000 0.885000 0.855000 0.865000 25% 0.921250 0.900000 0.861250 0.866250 50% 0.927500 0.905000 0.875000 0.870000 75% 0.933750 0.908750 0.888750 0.875000 max 0.945000 0.915000 0.910000 0.880000
También se presenta un gráfico de caja y margenes de los resultados, resumiendo las distribuciones de precisión por configuración. Podemos ver que el binario obtuvo los mejores resultados con una dispersión modesta y podría ser el enfoque preferido para este conjunto de datos.

Prediciendo Sentimientos para Nuevas Revisiones
Finalmente, podemos desarrollar y utilizar un modelo final para hacer predicciones para nuevas revisiones textuales. Por eso queríamos el modelo en primer lugar. Primero entrenaremos un modelo final sobre todos los datos disponibles. Usaremos el modo binario para puntuar el modelo de bolsa de palabras que se ha demostrado que da los mejores resultados en la sección anterior.
Predecir el sentimiento de nuevas revisiones implica seguir los mismos pasos utilizados para preparar los datos de la prueba. Específicamente, cargar el texto, limpiar el documento, filtrar los tokens por el vocabulario elegido, convertir los tokens restantes en una línea, codificarlos usando el Tokenizer, y hacer una predicción. Podemos hacer una predicción de un valor de clase directamente con el modelo de ajuste llamando a predict() que devolverá un número entero de 0 para una revisión negativa y 1 para una revisión positiva. Todos estos pasos se pueden poner en una nueva función llamada predict_sentiment() que requiere el texto de revisión, el vocabulario, el tokenizer y el modelo de ajuste, y devuelve el sentimiento predicho y un porcentaje asociado o un resultado similar a la confianza.
# clasificar una crítica como negativa o positiva
def predict_sentiment(review, vocab, tokenizer, model):
# pulir
tokens = clean_doc(review)
# filtrar por vocabulario
tokens = [w for w in tokens if w in vocab]
# convertir a línea
line =''.join(tokens)
# codificar
encoded = tokenizer.texts_to_matrix([line], mode='binary')
# predecir el sentimiento
yhat = model.predict(encoded, verbose=0)
# recuperar el porcentaje previsto y etiquetar
percent_pos = yhat[0,0]
if round(percent_pos) == 0:
return (1-percent_pos),'NEGATIVE'
return percent_pos,'POSITIVE'
Ahora podemos hacer predicciones para nuevos textos de revisión. A continuación se muestra un ejemplo con una revisión claramente positiva y otra claramente negativa utilizando el simple MLP desarrollado anteriormente con el modo de puntuación de palabras de frecuencia.
#texto positivo
text ='Best movie ever! It was great, I recommend it.'
percent, sentiment = predict_sentiment(text, vocab, tokenizer, model)
print('Review: [%s]\nSentiment: %s (%.3f%%)'% (text, sentiment, percent*100))
# texto negativo
text ='This is a bad movie.'
percent, sentiment = predict_sentiment(text, vocab, tokenizer, model)
print('Review: [%s]\nSentiment: %s (%.3f%%)'% (text, sentiment, percent*100))
Juntando todo esto, el ejemplo completo para hacer predicciones para nuevas revisiones se enumera a continuación.
import string
import re
from os import listdir
from numpy import array
from nltk.corpus import stopwords
from keras.preprocessing.text import Tokenizer
from keras.models import Sequential
from keras.layers import Dense
# cargamos el documento
def load_doc(filename):
# abre el archivo solo para leer
file = open(filename, 'r')
# lee todo el texto
text = file.read()
# cierra el archivo
file.close()
return text
# cambia el documento a token filtrados
def clean_doc(doc):
# dividir en token por espacios
tokens = doc.split()
# exp regulares para filtar caracteres
re_punc = re.compile('[%s]' % re.escape(string.punctuation))
# eliminamos puntacion de cada palabra
tokens = [re_punc.sub('', w) for w in tokens]
# remove remaining tokens that are not alphabetic
tokens = [word for word in tokens if word.isalpha()]
# filter out stop words
stop_words = set(stopwords.words('english'))
tokens = [w for w in tokens if not w in stop_words]
# filter out short tokens
tokens = [word for word in tokens if len(word) > 1]
return tokens
# carga doc, limpieza y devuelve linea de tokens
def doc_to_line(filename, vocab):
# carga del doc
doc = load_doc(filename)
# limpieza del doc
tokens = clean_doc(doc)
# filtro del vocab
tokens = [w for w in tokens if w in vocab]
return ' '.join(tokens)
# cargamos los doc en el directorio
def process_docs(directory, vocab):
lines = list()
# recorremos los archivos de la carpeta
for filename in listdir(directory):
# crea la ruta completa del archivo para abrir
path = directory + '/' + filename
# carga and limpeza del doc
line = doc_to_line(path, vocab)
# añadimos a la lista
lines.append(line)
return lines
# carga and limpeza del dataset
def load_clean_dataset(vocab):
# cargamos documentos
neg = process_docs('txt_sentoken/neg', vocab)
pos = process_docs('txt_sentoken/pos', vocab)
docs = neg + pos
# preparacion de etiquetas
labels = array([0 for _ in range(len(neg))] + [1 for _ in range(len(pos))])
return docs, labels
# ajuste del tokenizer
def create_tokenizer(lines):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
return tokenizer
# definimos el modelo
def define_model(n_words):
# definimos la network
model = Sequential()
model.add(Dense(50, input_shape=(n_words,), activation='relu'))
model.add(Dense(1, activation='sigmoid'))
# compilamos la network
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# sumarizamos el modelo definido
model.summary()
#plot_model(model, to_file='model.png', show_shapes=True)
return model
# clasificacion de criticas como negativa o positiva
def predict_sentiment(review, vocab, tokenizer, model):
# limpio
tokens = clean_doc(review)
# filtro del vocabulario
tokens = [w for w in tokens if w in vocab]
# convertimos a linea
line = ' '.join(tokens)
# encode
encoded = tokenizer.texts_to_matrix([line], mode='binary')
# prediccion del sentimento
yhat = model.predict(encoded, verbose=0)
# recuperamos el % predicho por la etiqueta
percent_pos = yhat[0,0]
if round(percent_pos) == 0:
return (1-percent_pos), 'NEGATIVO'
return percent_pos, 'POSITIVO'
# cargamos el vocabulario
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = set(vocab.split())
# cargamos todas las criticas
train_docs, ytrain = load_clean_dataset(vocab)
test_docs, ytest = load_clean_dataset(vocab)
# creacion del tokenizer
tokenizer = create_tokenizer(train_docs)
# encode de data
Xtrain = tokenizer.texts_to_matrix(train_docs, mode='binary')
Xtest = tokenizer.texts_to_matrix(test_docs, mode='binary')
# define la network
n_words = Xtrain.shape[1]
model = define_model(n_words)
# ajuste de la network
model.fit(Xtrain, ytrain, epochs=10, verbose=2)
# test texto positivo
text = 'Best movie ever! It was great, I recommend it.'
percent, sentiment = predict_sentiment(text, vocab, tokenizer, model)
print('Cristica: [%s]\nSentimento: %s (%.3f%%)' % (text, sentiment, percent*100))
# test texto negativo
text = 'This is a bad movie.'
percent, sentiment = predict_sentiment(text, vocab, tokenizer, model)
print('Critica: [%s]\nSentimento: %s (%.3f%%)' % (text, sentiment, percent*100))
Ejecutando el ejemplo correctamente clasifica estas revisiones.
Critica: [Best movie ever! It was great, I recommend it.] Sentimento: POSITIVO (57.524%) Critica: [This is a bad movie.] Sentimento: NEGATIVO (64.7404%)
Idealmente, encajaríamos el modelo en todos los datos disponibles (entrenamiento y prueba) para crear un modelo final y guardar el modelo y el tokenizer en un archivo para que puedan ser cargados y utilizados en un nuevo software.
Extensiones
Esta sección enumera algunas extensiones si desea obtener más de este tutorial.
- Manejar el vocabulario. Explore usando un vocabulario más grande o más pequeño. Tal vez pueda obtener un mejor rendimiento con un conjunto más pequeño de palabras.
- Ajuste la topología de la red. Explore topologías de red alternativas, como redes más profundas o más amplias. Tal vez pueda obtener un mejor rendimiento con una red más adecuada.
- Usar Regularización. Explore el uso de técnicas de regularización, como la deserción. Tal vez pueda retrasar la convergencia del modelo y lograr un mejor rendimiento del equipo de prueba.
- Más limpieza de datos. Explore más o menos la limpieza del texto de revisión y vea cómo afecta la habilidad del modelo.
- Diagnóstico de formación. Utilice el conjunto de datos de prueba como conjunto de datos de validación durante la formación y cree diagramas de pérdidas de entrenamiento y de prueba. Utilice estos diagnósticos para ajustar el tamaño del lote y el número de épocas de formación.
- Palabras desencadenantes. Explore si hay palabras específicas en las críticas que sean altamente predictivas del sentimiento.
- Usa Bigrams. Preparar el modelo para calificar bigrams de palabras y evaluar el desempeño bajo diferentes esquemas de calificación.
- Revisiones truncadas. Explore cómo el uso de una versión truncada de los resultados de las críticas de películas impacta la habilidad del modelo, intente truncar el comienzo, el final y la mitad de las críticas.
- Modelos de conjuntos. Crear modelos con diferentes esquemas de puntuación de palabras y ver si el uso de conjuntos de los modelos resulta en una mejora de la habilidad del modelo.
- Críticas de este año. Entrenar un modelo final con todos los datos y evaluar el modelo con las críticas de películas de este año tomadas de Internet.
Si exploras alguna de estas extensiones, me encantaría saberlo.
➡ Continúa aprendiendo de Procesamiento de Lenguaje Natural en nuestro curso: