Capítulo 4. Vectorización del texto y tuberías de transformación
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Los algoritmos de aprendizaje automático operan en un espacio de características numéricas, esperando la entrada como una matriz bidimensional en la que las filas son instancias y las columnas son características. Para aplicar el aprendizaje automático al texto, tenemos que transformar nuestros documentos en representaciones vectoriales que nos permitan aplicar el aprendizaje automático numérico. Este proceso se denomina extracción de rasgos o, más sencillamente, vectorización, y es un primer paso esencial hacia el análisis consciente del lenguaje.
Representar numéricamente los documentos nos da la capacidad de realizar análisis significativos y también crea las instancias sobre las que operan los algoritmos de aprendizaje automático. En el análisis de textos, las instancias son documentos enteros o enunciados, cuya longitud puede variar desde citas o tweets hasta libros enteros, pero cuyos vectores tienen siempre una longitud uniforme. Cada propiedad de la representación vectorial es una característica. En el caso del texto, las características representan atributos y propiedades de los documentos, incluido su contenido, así como metaatributos, como la longitud del documento, el autor, la fuente y la fecha de publicación. Cuando se consideran conjuntamente, las características de un documento describen un espacio de características multidimensional en el que se pueden aplicar métodos de aprendizaje automático.
Por esta razón, ahora debemos hacer un cambio crítico en nuestra forma de pensar sobre el lenguaje: de una secuencia de palabras a puntos que ocupan un espacio semántico de alta dimensión. Los puntos en el espacio pueden estar muy juntos o muy separados, muy agrupados o distribuidos uniformemente. Por lo tanto, el espacio semántico se traza de tal forma que los documentos con significados similares están más próximos entre sí y los que son diferentes están más alejados. Al codificar la similitud como distancia, podemos empezar a deducir los componentes primarios de los documentos y trazar límites de decisión en nuestro espacio semántico.
La codificación más sencilla del espacio semántico es el modelo de bolsa de palabras, cuya idea principal es que el significado y la similitud se codifican en el vocabulario. Por ejemplo, los artículos de Wikipedia sobre béisbol y Babe Ruth son probablemente muy similares. No sólo aparecerán muchas de las mismas palabras en ambos, sino que no compartirán muchas palabras en común con los artículos sobre cacerolas o flexibilización cuantitativa. Este modelo, aunque sencillo, es extremadamente eficaz y constituye el punto de partida de los modelos más complejos que exploraremos.
En este capítulo, demostraremos cómo utilizar el proceso de vectorización para combinar las técnicas lingüísticas de NLTK con las técnicas de aprendizaje automático de Scikit-Learn y Gensim, creando transformadores personalizados que pueden utilizarse dentro de pipelines repetibles y reutilizables. Al final de este capítulo, estaremos preparados para trabajar con nuestro corpus preprocesado, transformando los documentos al espacio del modelo para que podamos empezar a hacer predicciones.
Palabras en el espacio
Para vectorizar un corpus con un enfoque de bolsa de palabras (BOW), representamos cada documento del corpus como un vector cuya longitud es igual al vocabulario del corpus. Podemos simplificar el cálculo ordenando las posiciones de los tokens del vector por orden alfabético, como se muestra en la Figura 4-1. También podemos mantener un diccionario que asigne los tokens a las posiciones del vector. De cualquier forma, obtenemos un mapa vectorial del corpus que nos permite representar de forma única cada documento.
¿Qué debe ser cada elemento del vector de documentos? En los próximos apartados, exploraremos varias opciones, cada una de las cuales amplía o modifica el modelo base de bolsa de palabras para describir el espacio semántico. Examinaremos cuatro tipos de codificación vectorial -frecuencia, one-hot, TF-IDF y representaciones distribuidas- y discutiremos sus implementaciones en Scikit-Learn, Gensim y NLTK. Operaremos con un pequeño corpus de las tres frases de las figuras de ejemplo.
Para ponerlo en marcha, vamos a crear una lista de nuestros documentos y a tokenizarlos para los ejemplos de vectorización siguientes. El método tokenize
realiza una normalización ligera, eliminando los signos de puntuación mediante el conjunto de caracteres string.punctuation
y poniendo el texto en minúsculas. Esta función también realiza alguna reducción de rasgos utilizando el SnowballStemmer
para eliminar afijos como la pluralidad ("murciélagos" y "murciélago" son el mismo token). Los ejemplos de la siguiente sección utilizarán este corpus de ejemplo y algunos utilizarán el método de tokenización.
import
nltk
import
string
def
tokenize
(
text
):
stem
=
nltk
.
stem
.
SnowballStemmer
(
'english'
)
text
=
text
.
lower
()
for
token
in
nltk
.
word_tokenize
(
text
):
if
token
in
string
.
punctuation
:
continue
yield
stem
.
stem
(
token
)
corpus
=
[
"The elephant sneezed at the sight of potatoes."
,
"Bats can see via echolocation. See the bat sight sneeze!"
,
"Wondering, she opened the door to the studio."
,
]
La elección de una técnica de vectorización concreta vendrá determinada en gran medida por el espacio del problema. Del mismo modo, nuestra elección de implementación -ya sea NLTK, Scikit-Learn o Gensim- deberá venir dictada por los requisitos de la aplicación. Por ejemplo, NLTK ofrece muchos métodos que se adaptan especialmente bien a los datos de texto, pero es una gran dependencia. Scikit-Learn no se diseñó pensando en el texto, pero ofrece una API robusta y muchas otras comodidades (que exploraremos más adelante en este capítulo) especialmente útiles en un contexto aplicado. Gensim puede serializar diccionarios y referencias en formato de mercado matricial, lo que lo hace más flexible para múltiples plataformas. Sin embargo, a diferencia de Scikit-Learn, Gensim no hace ningún trabajo en nombre de tus documentos para la tokenización o stemming.
Por esta razón, mientras recorremos cada uno de los cuatro enfoques de la codificación, mostraremos algunas opciones de implementación: "Con NLTK", "En Scikit-Learn" y "A la manera de Gensim".
Vectores de frecuencia
El modelo de codificación vectorial más sencillo consiste simplemente en rellenar el vector con la frecuencia de cada palabra tal y como aparece en el documento. En este esquema de codificación, cada documento se representa como el multiconjunto de los tokens que lo componen y el valor de cada posición de palabra en el vector es su recuento. Esta representación puede ser una codificación de recuento directo (entero), como se muestra en la Figura 4-2, o una codificación normalizada en la que cada palabra se pondera por el número total de palabras del documento.
Con NLTK
NLTK espera las características como un objeto dict
cuyas claves son los nombres de las características y cuyos valores son booleanos o numéricos. Para codificar nuestros documentos de esta forma, crearemos una función vectorize
que cree un diccionario cuyas claves sean los tokens del documento y cuyos valores sean el número de veces que aparece ese token en el documento.
El objeto defaultdict
nos permite especificar qué devolverá el diccionario para una clave que aún no se le ha asignado. Al establecer defaultdict(int)
estamos especificando que se devuelva un 0
, creando así un diccionario de recuento simple. Podemos map
esta función a cada elemento del corpus utilizando la última línea de código, creando un iterable de documentos vectorizados.
from
collections
import
defaultdict
def
vectorize
(
doc
):
features
=
defaultdict
(
int
)
for
token
in
tokenize
(
doc
):
features
[
token
]
+=
1
return
features
vectors
=
map
(
vectorize
,
corpus
)
En Scikit-Learn
El transformador CountVectorizer
del modelo sklearn.feature_extraction
tiene sus propios métodos internos de tokenización y normalización. El método fit
del vectorizador espera un iterable o lista de cadenas u objetos de archivo, y crea un diccionario del vocabulario del corpus. Cuando se llama a transform
, cada documento individual se transforma en una matriz dispersa cuya tupla índice es la fila (el ID del documento) y el ID del token del diccionario, y cuyo valor es el recuento:
from
sklearn.feature_extraction.text
import
CountVectorizer
vectorizer
=
CountVectorizer
()
vectors
=
vectorizer
.
fit_transform
(
corpus
)
Nota
Los vectores pueden volverse extremadamente dispersos, sobre todo a medida que aumentan los vocabularios, lo que puede tener un impacto significativo en la velocidad y el rendimiento de los modelos de aprendizaje automático. Para corpus muy grandes, se recomienda utilizar Scikit-Learn HashingVectorizer
, que utiliza un truco de hashing para encontrar el nombre de la cadena de tokens para el índice de características. Esto significa que utiliza muy poca memoria y se adapta a grandes conjuntos de datos, ya que no necesita almacenar todo el vocabulario y es más rápido de recoger y ajustar, puesto que no hay estado. Sin embargo, no hay transformación inversa (de vector a texto), puede haber colisiones y no hay ponderación inversa de frecuencia de documentos.
A la manera de Gensim
El codificador de frecuencias de Gensim se llama doc2bow
. Para utilizar doc2bow
, primero creamos un diccionario Gensim Dictionary
que asigna tokens a índices basándose en el orden observado (eliminando la sobrecarga de la ordenación lexicográfica). El objeto diccionario puede cargarse o guardarse en disco, e implementa una biblioteca doc2bow
que acepta un documento pretokenizado y devuelve una matriz dispersa de tuplas (id, count)
donde id
es el id del token en el diccionario. Como el método doc2bow
sólo toma una única instancia de documento, utilizamos la comprensión de lista para restaurar todo el corpus, cargando los documentos tokenizados en memoria para no agotar nuestro generador:
import
gensim
corpus
=
[
tokenize
(
doc
)
for
doc
in
corpus
]
id2word
=
gensim
.
corpora
.
Dictionary
(
corpus
)
vectors
=
[
id2word
.
doc2bow
(
doc
)
for
doc
in
corpus
]
Codificación en caliente
Como no tienen en cuenta la gramática ni la posición relativa de las palabras en los documentos, los métodos de codificación basados en la frecuencia adolecen de la larga cola, o distribución Zipfiana, que caracteriza al lenguaje natural. Como resultado, los tokens que aparecen con mucha frecuencia son órdenes de magnitud más "significativos" que otros menos frecuentes. Esto puede tener un impacto significativo en algunos modelos (por ejemplo, los modelos lineales generalizados) que esperan características distribuidas normalmente.
Una solución a este problema es la codificación de un punto, un método booleano de codificación vectorial que marca un índice vectorial concreto con un valor de verdadero (1) si el token existe en el documento y de falso (0) si no existe. En otras palabras, cada elemento de un vector codificado con un punto refleja la presencia o ausencia del token en el texto descrito, como se muestra en la Figura 4-3.
La codificación en un solo paso reduce el problema del desequilibrio de la distribución de los tokens, simplificando un documento a sus componentes constituyentes. Esta reducción es más eficaz en documentos muy pequeños (frases, tweets) que no contienen muchos elementos repetidos, y suele aplicarse a modelos que tienen muy buenas propiedades de suavizado. La codificación en un punto también se utiliza habitualmente en las redes neuronales artificiales, cuyas funciones de activación requieren que la entrada esté en el intervalo discreto de [0,1]
o [-1,1]
.
Con NLTK
La implementación NLTK de la codificación de un solo golpe es un diccionario cuyas claves son los tokens y cuyo valor es True
:
def
vectorize
(
doc
):
return
{
token
:
True
for
token
in
doc
}
vectors
=
map
(
vectorize
,
corpus
)
Los diccionarios actúan como simples matrices dispersas en el caso de NLTK, porque no es necesario marcar cada palabra ausente False
. Además de los valores booleanos del diccionario, también es aceptable utilizar un valor entero; 1 para presente y 0 para ausente.
En Scikit-Learn
En Scikit-Learn, la codificación one-hot se implementa con el transformador Binarizer
del módulo preprocessing
. El Binarizer
sólo toma datos numéricos, por lo que los datos de texto deben transformarse en un espacio numérico utilizando el CountVectorizer
antes de la codificación de un solo golpe. La clase Binarizer
utiliza un valor umbral (0 por defecto) de tal forma que todos los valores del vector que sean menores o iguales que el umbral se ponen a cero, mientras que los que sean mayores que el umbral se ponen a 1. Por lo tanto, por defecto, la clase Binarizer
convierte todos los valores de frecuencia a 1
manteniendo las frecuencias de valor cero.
from
sklearn.preprocessing
import
Binarizer
freq
=
CountVectorizer
()
corpus
=
freq
.
fit_transform
(
corpus
)
onehot
=
Binarizer
()
corpus
=
onehot
.
fit_transform
(
corpus
.
toarray
())
El método corpus.toarray()
es opcional; convierte la representación de matriz dispersa en una densa. En corpus con vocabularios grandes, la representación matricial dispersa es mucho mejor. Ten en cuenta que también podríamos utilizar CountVectorizer(binary=True)
para lograr la codificación de un solo golpe en lo anterior, obviando el Binarizer
.
Precaución
A pesar de su nombre, el transformador OneHotEncoder
del módulo sklearn.preprocessing
no es exactamente el más adecuado para esta tarea. El OneHotEncoder
trata cada componente del vector (columna) como una variable categórica independiente, ampliando la dimensionalidad del vector para cada valor observado en cada columna. En este caso, el componente (sight, 0)
y (sight, 1)
se tratarían como dos dimensiones categóricas en lugar de como un único componente vectorial codificado en binario.
A la manera de Gensim
Aunque Gensim no tiene un codificador específico de una sola vez, su método doc2bow
devuelve una lista de tuplas que podemos manejar sobre la marcha. Ampliando el código del ejemplo de vectorización de frecuencias de Gensim de la sección anterior, podemos codificar nuestros vectores de una sola vez con nuestro diccionario id2word
. Para obtener nuestro vectors
, una comprensión interna de la lista convierte la lista de tuplas devuelta por el método doc2bow
en una lista de tuplas (token_id, 1)
y la comprensión externa aplica ese conversor a todos los documentos del corpus:
corpus
=
[
tokenize
(
doc
)
for
doc
in
corpus
]
id2word
=
gensim
.
corpora
.
Dictionary
(
corpus
)
vectors
=
[
[(
token
[
0
],
1
)
for
token
in
id2word
.
doc2bow
(
doc
)]
for
doc
in
corpus
]
La codificación de una sola palabra representa la similitud y la diferencia a nivel de documento, pero como todas las palabras se representan equidistantes, no es capaz de codificar la similitud por palabra. Además, como todas las palabras son igual de distantes, la forma de la palabra adquiere una importancia increíble: ¡los tokens "intentar" e "intentar" serán igual de distantes de tokens no relacionados como "rojo" o "bicicleta"! La normalización de los tokens a una sola clase de palabras, ya sea mediante el stemming o la lematización, que exploraremos más adelante en este capítulo, garantiza que las diferentes formas de los tokens que incorporan pluralidad, caso, género, cardinalidad, tiempo, etc., se traten como componentes de vector único, reduciendo el espacio de características y haciendo que los modelos sean más eficaces.
Frecuencia de términos-Frecuencia inversa de documentos
Las representaciones de bolsa de palabras que hemos explorado hasta ahora sólo describen un documento de forma aislada, sin tener en cuenta el contexto del corpus. Un enfoque mejor sería considerar la frecuencia relativa o rareza de los tokens en el documento frente a su frecuencia en otros documentos. La idea central es que lo más probable es que el significado esté codificado en los términos más raros de un documento. Por ejemplo, en un corpus de textos deportivos, términos como "árbitro", "base" y "banquillo" aparecen con más frecuencia en los documentos que hablan de béisbol, mientras que otros términos que aparecen con frecuencia en todo el corpus, como "carrera", "marcador" y "jugada", son menos importantes.
La codificación TF-IDF, frecuencia de términos-frecuencia inversa de documentos, normaliza la frecuencia de los tokens de un documento con respecto al resto del corpus. Este enfoque de codificación acentúa los términos que son muy relevantes para un caso concreto, como se muestra en la Figura 4-4, donde el token studio
tiene una mayor relevancia para este documento, ya que sólo aparece allí.
El TF-IDF se calcula por término, de modo que la relevancia de un token para un documento se mide por la frecuencia escalada de aparición del término en el documento, normalizada por la inversa de la frecuencia escalada del término en todo el corpus.
Con NLTK
Para vectorizar el texto de esta forma con NLTK, utilizamos la clase TextCollection
, un envoltorio para una lista de textos o un corpus formado por uno o más textos. Esta clase proporciona soporte para el recuento, la concordancia, el descubrimiento de colocaciones y, lo que es más importante, el cálculo de tf_idf
.
Como el TF-IDF requiere todo el corpus, nuestra nueva versión de vectorize
no acepta un solo documento, sino todos los documentos. Tras aplicar nuestra función de tokenización y crear la colección de textos, la función recorre cada documento del corpus y obtiene un diccionario cuyas claves son los términos y cuyos valores son la puntuación TF-IDF del término en ese documento concreto.
from
nltk.text
import
TextCollection
def
vectorize
(
corpus
):
corpus
=
[
tokenize
(
doc
)
for
doc
in
corpus
]
texts
=
TextCollection
(
corpus
)
for
doc
in
corpus
:
yield
{
term
:
texts
.
tf_idf
(
term
,
doc
)
for
term
in
doc
}
En Scikit-Learn
Scikit-Learn proporciona un transformador llamado TfidfVectorizer
en el módulo llamado feature_extraction.text
para vectorizar documentos con puntuaciones TF-IDF. Bajo el capó, el TfidfVectorizer
utiliza el estimador CountVectorizer
que usamos para producir la codificación de la bolsa de palabras para contar las ocurrencias de los tokens, seguido de un TfidfTransformer
, que normaliza estos recuentos de ocurrencias por la frecuencia inversa del documento.
Se espera que la entrada de un TfidfVectorizer
sea una secuencia de nombres de archivo, objetos similares a archivos o cadenas que contengan una colección de documentos en bruto, similar a la del CountVectorizer
. Como resultado, se aplica un método de tokenización y preprocesamiento por defecto, a menos que se especifiquen otras funciones. El vectorizador devuelve una representación matricial dispersa en forma de ((doc, term), tfidf)
donde cada clave es un par de documento y término y el valor es la puntuación TF-IDF.
from
sklearn.feature_extraction.text
import
TfidfVectorizer
tfidf
=
TfidfVectorizer
()
corpus
=
tfidf
.
fit_transform
(
corpus
)
A la manera de Gensim
En Gensim, la estructura de datos TfidfModel
es similar al objeto Dictionary
en el sentido de que almacena un mapeo de términos y sus posiciones vectoriales en el orden en que se observan, pero además almacena la frecuencia del corpus de esos términos para poder vectorizar los documentos bajo demanda. Como antes, Gensim nos permite aplicar nuestro propio método de tokenización, esperando un corpus que sea una lista de listas de tokens. Primero construimos el léxico y lo utilizamos para instanciar el TfidfModel
, que calcula la frecuencia inversa normalizada de los documentos. A continuación, podemos obtener la representación TF-IDF de cada vector utilizando una sintaxis similar a la de un diccionario getitem
, tras aplicar el método doc2bow
a cada documento utilizando el léxico.
corpus
=
[
tokenize
(
doc
)
for
doc
in
corpus
]
lexicon
=
gensim
.
corpora
.
Dictionary
(
corpus
)
tfidf
=
gensim
.
models
.
TfidfModel
(
dictionary
=
lexicon
,
normalize
=
True
)
vectors
=
[
tfidf
[
lexicon
.
doc2bow
(
doc
)]
for
doc
in
corpus
]
Gensim proporciona una funcionalidad de ayuda para escribir diccionarios y modelos en disco en un formato compacto, lo que significa que puedes guardar cómodamente en disco tanto el modelo TF-IDF como el léxico para cargarlos más tarde y vectorizar nuevos documentos. Es posible (aunque un poco más trabajoso) conseguir el mismo resultado utilizando el módulo pickle
en combinación con Scikit-Learn. Para guardar un modelo Gensim en el disco:
lexicon
.
save_as_text
(
'lexicon.txt'
,
sort_by_word
=
True
)
tfidf
.
save
(
'tfidf.pkl'
)
Esto guardará el léxico como un archivo de texto delimitado, ordenado lexicográficamente, y el modelo TF-IDF como una matriz dispersa decapada. Ten en cuenta que el objeto Dictionary
también se puede guardar de forma más compacta en un formato binario utilizando su método save
, pero save_as_text
permite inspeccionar fácilmente el diccionario para un trabajo posterior. Para cargar los modelos desde el disco
lexicon
=
gensim
.
corpora
.
Dictionary
.
load_from_text
(
'lexicon.txt'
)
tfidf
=
gensim
.
models
.
TfidfModel
.
load
(
'tfidf.pkl'
)
Una de las ventajas del TF-IDF es que aborda de forma natural el problema de las palabras vacías, es decir, las palabras con más probabilidades de aparecer en todos los documentos del corpus (por ejemplo, "a", "el", "de", etc.), por lo que acumularán pesos muy pequeños con este esquema de codificación. Esto sesga el modelo TF-IDF hacia palabras moderadamente raras. Como resultado, el TF-IDF se utiliza ampliamente para los modelos de bolsa de palabras, y es un excelente punto de partida para la mayoría de los análisis de texto.
Representación distribuida
Aunque la codificación de frecuencias, de un punto y TF-IDF nos permiten colocar los documentos en un espacio vectorial, a menudo resulta útil codificar también las similitudes entre documentos en el contexto de ese mismo espacio vectorial. Por desgracia, estos métodos de vectorización producen vectores de documentos con elementos no negativos, lo que significa que no podremos comparar documentos que no compartan términos (porque dos vectores con una distancia coseno de 1 se considerarán muy alejados, aunque sean semánticamente similares).
Cuando la similitud de los documentos es importante en el contexto de una aplicación, codificamos en cambio el texto a lo largo de una escala continua con una representación distribuida, como se muestra en la Figura 4-5. Esto significa que el vector del documento resultante no es una simple correspondencia entre la posición y la puntuación de los tokens. En su lugar, el documento se representa en un espacio de características que se ha incrustado para representar la similitud de las palabras. La complejidad de este espacio (y la longitud del vector resultante) es el producto de cómo se aprende la correspondencia con esa representación. La complejidad de este espacio (y la longitud del vector resultante) es el producto de cómo se entrena esa representación y no está directamente ligada al propio documento.
Word2vec, creado por un equipo de investigadores de Google dirigido por Tomáš Mikolov, implementa un modelo de incrustación de palabras que permite crear este tipo de representaciones distribuidas. El algoritmo word2vec entrena representaciones de palabras basadas en un modelo de bolsa continua de palabras (CBOW) o en un modelo de salto de grama, de modo que las palabras se incrustan en el espacio junto con palabras similares en función de su contexto. Por ejemplo, la implementación de Gensim utiliza una red feedforward.
El algoritmo doc2vec1 es una extensión de word2vec. Propone un vector de párrafos, unalgoritmo no supervisado que aprende representaciones de características de longitud fija a partir de documentos de longitud variable. Esta representación intenta heredar las propiedades semánticas de las palabras, de modo que "rojo" y "colorido" se parecen más entre sí que a "río" o "gobierno". Además, el vector de párrafos tiene en cuenta la ordenación de las palabras dentro de un contexto estrecho, de forma similar a un modelo de n-gramas. El resultado combinado es mucho más eficaz que un modelo de bolsa de palabras o de bolsa de n-gramas, porque generaliza mejor y tiene una dimensionalidad menor, pero sigue teniendo una longitud fija para que pueda utilizarse en los algoritmos habituales de aprendizaje automático.
A la manera de Gensim
Ni NLTK ni Scikit-Learn proporcionan implementaciones de este tipo de incrustaciones de palabras. La implementación de Gensim permite a los usuarios entrenar los modelos word2vec y doc2vec en corpus personalizados, y también viene convenientemente con un modelo preentrenado en el corpus de noticias de Google.
Nota
Para utilizar los modelos preentrenados de Gensim, tendrás que descargar el archivo bin del modelo, que pesa 1,5 GB. Para aplicaciones que requieren dependencias extremadamente ligeras (por ejemplo, si tienen que ejecutarse en una instancia lambda de AWS), esto puede no ser factible.
Podemos entrenar nuestro propio modelo del siguiente modo. En primer lugar, utilizamos una comprensión de listas para cargar nuestro corpus en memoria. (Gensim admite el streaming, pero esto nos permitirá evitar agotar el generador). A continuación, creamos una lista de objetos TaggedDocument
, que amplían la LabeledSentence
, y a su vez la representación distribuida de word2vec. Los objetos TaggedDocument
constan de palabras y etiquetas. Podemos instanciar el documento etiquetado con la lista de tokens junto con una sola etiqueta, una que identifique unívocamente la instancia. En este ejemplo, hemos etiquetado cada documento como "d{}".format(idx)
, por ejemplo d0
, d1
, d2
y así sucesivamente.
Una vez que tenemos una lista de documentos etiquetados, instanciamos el modelo Doc2Vec
y especificamos el tamaño del vector, así como el recuento mínimo, que ignora todos los tokens que tienen una frecuencia inferior a ese número. El parámetro size
no suele tener una dimensionalidad tan baja como 5; seleccionamos un número tan pequeño sólo a efectos de demostración. También fijamos el parámetro min_count
a cero para asegurarnos de que consideramos todos los tokens, pero generalmente se fija entre 3 y 5, dependiendo de cuánta información necesite captar el modelo. Una vez instanciada, se entrena una red neuronal no supervisada para aprender las representaciones vectoriales, a las que se puede acceder mediante la propiedad docvecs
.
from
gensim.models.doc2vec
import
TaggedDocument
,
Doc2Vec
corpus
=
[
list
(
tokenize
(
doc
))
for
doc
in
corpus
]
corpus
=
[
TaggedDocument
(
words
,
[
'd{}'
.
format
(
idx
)])
for
idx
,
words
in
enumerate
(
corpus
)
]
model
=
Doc2Vec
(
corpus
,
size
=
5
,
min_count
=
0
)
(
model
.
docvecs
[
0
])
# [ 0.01797447 -0.01509272 0.0731937 0.06814702 -0.0846546 ]
Las representaciones distribuidas mejorarán drásticamente los resultados respecto a los modelos TF-IDF cuando se utilicen correctamente. El propio modelo puede guardarse en el disco y volver a entrenarse de forma activa, lo que lo hace extremadamente flexible para una gran variedad de casos de uso. Sin embargo, en corpus grandes, el entrenamiento puede ser lento y consumir mucha memoria, y puede que no sea tan bueno como un modelo TF-IDF con Análisis de Componentes Principales (ACP) o Descomposición de Valores Singulares (DVE) aplicados para reducir el espacio de características. Al final, sin embargo, esta representación es un trabajo innovador que ha supuesto una mejora espectacular de las capacidades de procesamiento de texto de los productos de datos en los últimos años.
De nuevo, la elección de la técnica de vectorización (así como la implementación de la biblioteca) tiende a ser específica para cada caso de uso y aplicación, como se resume en la Tabla 4-1.
Método de vectorización | Función | Bueno para | Consideraciones |
---|---|---|---|
Frecuencia |
Cuenta las frecuencias de los términos |
Modelos bayesianos |
Las palabras más frecuentes no siempre son las más informativas |
Codificación en caliente |
Binariza la aparición del término (0, 1) |
Redes neuronales |
Todas las palabras son equidistantes, por lo que la normalización es extra importante |
TF-IDF |
Normaliza las frecuencias de términos en los documentos |
Uso general |
Los términos moderadamente frecuentes pueden no ser representativos de los temas del documento |
Representaciones distribuidas |
Codificación de similitud de términos continua basada en el contexto |
Modelar relaciones más complejas |
Rendimiento intensivo; difícil de escalar sin herramientas adicionales (por ejemplo, Tensorflow) |
Más adelante en este capítulo exploraremos el objeto Pipeline
de Scikit-Learn, que nos permite agilizar la vectorización junto con las frases de modelado posteriores. Por ello, a menudo preferimos utilizar vectorizadores que se ajusten a la API de Scikit-Learn. En la siguiente sección, discutiremos cómo está organizada la API y demostraremos cómo integrar la vectorización en una canalización completa para construir el núcleo de una aplicación de aprendizaje automático textual totalmente operativa (¡y personalizable!).
La API de Scikit-Learn
Scikit-Learn es una extensión de SciPy (un scikit) cuyo objetivo principal es proporcionar algoritmos de aprendizaje automático, así como las herramientas y utilidades necesarias para realizar modelizaciones con éxito. Su principal contribución es una "API para el aprendizaje automático" que expone las implementaciones de una amplia gama de familias de modelos en una única interfaz fácil de usar. El resultado es que Scikit-Learn puede utilizarse para entrenar simultáneamente una asombrosa variedad de modelos, evaluarlos y compararlos, y luego utilizar el modelo ajustado para hacer predicciones sobre nuevos datos. Como Scikit-Learn proporciona una API estandarizada, esto puede hacerse con poco esfuerzo y los modelos pueden prototiparse y evaluarse simplemente cambiando unas pocas líneas de código.
La interfaz BaseEstimator
La propia API está orientada a objetos y describe una jerarquía de interfaces para diferentes tareas de aprendizaje automático. La raíz de la jerarquía es un Estimator
, en general cualquier objeto que pueda aprender de los datos. Los principales objetos de Estimator
implementan clasificadores, regresores o algoritmos de agrupación. Sin embargo, también pueden incluir una amplia gama de manipulaciones de datos, desde la reducción de la dimensionalidad hasta la extracción de características a partir de datos brutos. Estimator
sirve esencialmente como interfaz, y las clases que implementan la funcionalidad de Estimator
deben tener dos métodos -fit
y predict
-, como se muestra aquí:
from
sklearn.base
import
BaseEstimator
class
Estimator
(
BaseEstimator
):
def
fit
(
self
,
X
,
y
=
None
):
"""
Accept input data, X, and optional target data, y. Returns self.
"""
return
self
def
predict
(
self
,
X
):
"""
Accept input data, X and return a vector of predictions for each row.
"""
return
yhat
El método Estimator.fit
establece el estado del estimador basándose en los datos de entrenamiento, X
y y
. Se espera que los datos de entrenamiento X
sean de tipo matricial; por ejemplo, una matriz NumPy bidimensional de forma (n_samples
, n_features
) o un Pandas DataFrame
cuyas filas son las instancias y cuyas columnas son las características. Los estimadores supervisados también se ajustan con una matriz NumPy unidimensional, y
, que contiene las etiquetas correctas. El proceso de ajuste modifica el estado interno del estimador para que esté listo o sea capaz de hacer predicciones. Este estado se almacena en variables de instancia a las que se suele añadir un guión bajo (por ejemplo, Estimator.coefs_
). Como este método modifica un estado interno, devuelve self
para que el método pueda encadenarse.
El método Estimator.predict
crea predicciones utilizando el estado interno ajustado del modelo sobre los nuevos datos, X
. La entrada para el método debe tener el mismo número de columnas que los datos de entrenamiento pasados a fit
, y puede tener tantas filas como predicciones se necesiten. Este método devuelve un vector, yhat
, que contiene las predicciones para cada fila de los datos de entrada.
Nota
La ampliación de BaseEstimator
de Scikit-Learn dota automáticamente a Estimator
de un método fit_predict
, que te permite combinar fit
y predict
en una simple llamada.
Estimator
tienen parámetros (también llamados hiperparámetros) que definen cómo se realiza el proceso de ajuste. Estos parámetros se establecen cuando se instancian los objetos (y si no se especifican, se establecen en valores predeterminados razonables), y pueden modificarse con los métodos y que también están disponibles en la superclase . Estimator
get_param
set_param
BaseEstimator
Activamos la API Scikit-Learn especificando el paquete y el tipo de estimador. Aquí seleccionamos la familia de modelos Naive Bayes, y un miembro concreto de la familia, un modelo multinomial (que es adecuado para la clasificación de textos). El modelo se define cuando se instancia la clase y se pasan los hiperparámetros. Aquí pasamos un parámetro alfa que se utiliza para el suavizado aditivo, así como probabilidades a priori para cada una de nuestras dos clases. El modelo se entrena con datos específicos (documents
y labels
) y en ese momento se convierte en un modelo ajustado. Este uso básico es el mismo para todos los modelos (Estimator
) de Scikit-Learn, desde los conjuntos de árboles de decisión de bosques aleatorios hasta las regresiones logísticas y más allá.
from
sklearn.naive_bayes
import
MultinomialNB
model
=
MultinomialNB
(
alpha
=
0.0
,
class_prior
=
[
0.4
,
0.6
])
model
.
fit
(
documents
,
labels
)
Ampliación de TransformerMixin
Scikit-Learn también especifica utilidades para realizar aprendizaje automático de forma repetible. No podríamos hablar de Scikit-Learn sin hablar también de la interfaz Transformer
. Un Transformer
es un tipo especial de Estimator
que crea un nuevo conjunto de datos a partir de uno antiguo basándose en reglas que ha aprendido del proceso de ajuste. La interfaz es la siguiente:
from
sklearn.base
import
TransformerMixin
class
Transfomer
(
BaseEstimator
,
TransformerMixin
):
def
fit
(
self
,
X
,
y
=
None
):
"""
Learn how to transform data based on input data, X.
"""
return
self
def
transform
(
self
,
X
):
"""
Transform X into a new dataset, Xprime and return it.
"""
return
Xprime
El método Transformer.transform
toma un conjunto de datos y devuelve un nuevo conjunto de datos, X`
, con nuevos valores basados en el proceso de transformación. Hay varios transformadores incluidos en Scikit-Learn, entre ellos transformadores para normalizar o escalar características, manejar valores perdidos (imputación), realizar reducción de dimensionalidad, extraer o seleccionar características, o realizar mapeados de un espacio de características a otro.
Aunque tanto NLTK como Gensim, e incluso bibliotecas de análisis de texto más recientes como SpaCy, tienen sus propias API internas y mecanismos de aprendizaje, el alcance y la amplitud de los modelos y metodologías de aprendizaje automático de Scikit-Learn lo convierten en una parte esencial del flujo de trabajo de modelado. En consecuencia, proponemos utilizar la API para crear nuestros propios objetos Transformer
y Estimator
que implementen métodos de NLTK y Gensim. Por ejemplo, podemos crear estimadores de modelado temático que incluyan los modelos LDA y LSA de Gensim (que actualmente no están incluidos en Scikit-Learn) o crear transformadores que utilicen los métodos de etiquetado de parte del habla y de agrupación de entidades con nombre de NLTK.
Creación de un transformador de vectorización Gensim personalizado
Las técnicas de vectorización de Gensim son un caso de estudio interesante, porque los corpus de Gensim pueden guardarse y cargarse desde el disco de forma que permanezcan desacoplados de la tubería. Sin embargo, es posible construir un transformador personalizado que utilice la vectorización Gensim. Nuestro transformador GensimVectorizer
envolverá un objeto Gensim Dictionary
generado durante fit()
y cuyo método doc2bow
se utiliza durante transform()
. El objeto Dictionary
(al igual que el TfidfModel
) puede guardarse y cargarse desde el disco, por lo que nuestro transformador utiliza esa metodología tomando una ruta en la instanciación. Si existe un archivo en esa ruta, se carga inmediatamente. Además, un método save()
nos permite escribir nuestro Dictionary
en el disco, lo que podemos hacer en fit()
.
El método fit()
construye el objeto Dictionary
pasando documentos ya tokenizados y normalizados al constructor Dictionary
. A continuación, el Dictionary
se guarda inmediatamente en el disco, de modo que el transformador pueda cargarse sin necesidad de reiniciarlo. El método transform()
utiliza el método Dictionary.doc2bow
, que devuelve una representación dispersa del documento como una lista de tuplas (token_id, frequency)
. Sin embargo, esta representación puede presentar dificultades con Scikit-Learn, por lo que utilizamos una función de ayuda de Gensim, sparse2full
, para convertir la representación dispersa en una matriz NumPy.
import
os
from
gensim.corpora
import
Dictionary
from
gensim.matutils
import
sparse2full
class
GensimVectorizer
(
BaseEstimator
,
TransformerMixin
):
def
__init__
(
self
,
path
=
None
):
self
.
path
=
path
self
.
id2word
=
None
self
.
load
()
def
load
(
self
):
if
os
.
path
.
exists
(
self
.
path
):
self
.
id2word
=
Dictionary
.
load
(
self
.
path
)
def
save
(
self
):
self
.
id2word
.
save
(
self
.
path
)
def
fit
(
self
,
documents
,
labels
=
None
):
self
.
id2word
=
Dictionary
(
documents
)
self
.
save
()
return
self
def
transform
(
self
,
documents
):
for
document
in
documents
:
docvec
=
self
.
id2word
.
doc2bow
(
document
)
yield
sparse2full
(
docvec
,
len
(
self
.
id2word
))
Es fácil ver cómo las metodologías de vectorización que hemos discutido anteriormente en el capítulo pueden ser envueltas por los transformadores de Scikit-Learn. Esto nos da más flexibilidad en los enfoques que adoptamos, al tiempo que nos permite aprovechar las utilidades de aprendizaje automático de cada biblioteca. Dejaremos que el lector amplíe este ejemplo e investigue los transformadores TF-IDF y de representación distribuida que se implementan del mismo modo.
Crear un transformador de normalización de texto personalizado
Muchas familias de modelos sufren "la maldición de la dimensionalidad"; a medida que el espacio de características aumenta en dimensiones, los datos se vuelven más escasos y menos informativos para el espacio de decisión subyacente. La normalización del texto reduce el número de dimensiones, disminuyendo la escasez. Además del simple filtrado de los tokens (eliminación de la puntuación y las palabras vacías), existen dos métodos principales para la normalización del texto: el stemming y la lematización.
El despunte utiliza una serie de reglas (o un modelo) para cortar una cadena en una subcadena más pequeña. El objetivo es eliminar los afijos de las palabras (sobre todo los sufijos) que modifican el significado. Por ejemplo, eliminar un 's'
o 'es'
, que suele indicar pluralidad en las lenguas latinas. La lematización, en cambio, utiliza un diccionario para buscar cada palabra y devuelve la palabra "cabeza" canónica del diccionario, llamada lema. Como busca los tokens a partir de una verdad básica, puede tratar casos irregulares y tokens con distintas partes de la oración. Por ejemplo, el verbo 'gardening'
debería lematizarse a 'to garden'
, mientras que los sustantivos 'garden'
y 'gardener'
son lemas diferentes. El stemming capturaría todos estos tokens en un único token 'garden'
.
La separación de palabras y la lematización tienen sus ventajas e inconvenientes. Como sólo requiere que empalmemos cadenas de palabras, el stemming es más rápido. La lematización, en cambio, requiere consultar un diccionario o una base de datos, y utiliza etiquetas de parte del discurso para identificar el lema raíz de una palabra, por lo que es notablemente más lenta que la separación por raíces, pero también más eficaz.
Para realizar la normalización del texto de forma sistemática, escribiremos un transformador personalizado que reúna estas piezas. Nuestra clase TextNormalizer
toma como entrada un idioma que se utiliza para cargar las palabras clave correctas del corpus NLTK. También podríamos personalizar el TextNormalizer
para permitir que los usuarios elijan entre stemming y lematización, y pasar el idioma al SnowballStemmer
. Para filtrar los tokens extraños, creamos dos métodos. El primero, is_punct()
, comprueba si cada carácter del token tiene una categoría Unicode que empiece por 'P'
(para la puntuación); el segundo, is_stopword()
determina si el token está en nuestro conjunto de palabras clave.
import
unicodedata
from
sklearn.base
import
BaseEstimator
,
TransformerMixin
class
TextNormalizer
(
BaseEstimator
,
TransformerMixin
):
def
__init__
(
self
,
language
=
'english'
):
self
.
stopwords
=
set
(
nltk
.
corpus
.
stopwords
.
words
(
language
))
self
.
lemmatizer
=
WordNetLemmatizer
()
def
is_punct
(
self
,
token
):
return
all
(
unicodedata
.
category
(
char
)
.
startswith
(
'P'
)
for
char
in
token
)
def
is_stopword
(
self
,
token
):
return
token
.
lower
()
in
self
.
stopwords
A continuación, podemos añadir un método normalize()
que tome un único documento compuesto por una lista de párrafos, que son listas de frases, que son listas de tuplas (token, tag)
-el formato de datos al que preprocesamos el HTML sin procesar en el Capítulo 3.
def
normalize
(
self
,
document
):
return
[
self
.
lemmatize
(
token
,
tag
)
.
lower
()
for
paragraph
in
document
for
sentence
in
paragraph
for
(
token
,
tag
)
in
sentence
if
not
self
.
is_punct
(
token
)
and
not
self
.
is_stopword
(
token
)
]
Este método aplica las funciones de filtrado para eliminar los tokens no deseados y luego los lematiza. El método lemmatize()
convierte primero las etiquetas de parte de habla de Penn Treebank, que son el conjunto de etiquetas por defecto de la función nltk.pos_tag
, en etiquetas de WordNet, seleccionando por defecto los sustantivos.
def
lemmatize
(
self
,
token
,
pos_tag
):
tag
=
{
'N'
:
wn
.
NOUN
,
'V'
:
wn
.
VERB
,
'R'
:
wn
.
ADV
,
'J'
:
wn
.
ADJ
}
.
get
(
pos_tag
[
0
],
wn
.
NOUN
)
return
self
.
lemmatizer
.
lemmatize
(
token
,
tag
)
Por último, debemos añadir la interfaz Transformer
, que nos permitirá añadir esta clase a una canalización de Scikit-Learn, que exploraremos en la siguiente sección:
def
fit
(
self
,
X
,
y
=
None
):
return
self
def
transform
(
self
,
documents
):
for
document
in
documents
:
yield
self
.
normalize
(
document
)
Ten en cuenta que la normalización del texto es sólo una metodología, y además utiliza mucho NLTK, lo que puede añadir una sobrecarga innecesaria a tu aplicación. Otras opciones podrían ser eliminar los tokens que aparezcan por encima o por debajo de un umbral de recuento determinado, o eliminar las palabras vacías y seleccionar sólo las primeras cinco o diez mil palabras más comunes. Otra opción es simplemente calcular la frecuencia acumulada y seleccionar sólo las palabras que contengan entre el 10% y el 50% de la distribución de frecuencia acumulada. Estos métodos nos permitirían ignorar tanto loshapaxes de muy baja frecuencia (términos que sólo aparecen una vez) como las palabras más comunes, permitiéndonos identificar los términos más potencialmente predictivos del corpus.
Precaución
El acto de normalizar el texto debe ser opcional y aplicarse con cuidado, porque la operación es destructiva, en el sentido de que elimina información. Las mayúsculas y minúsculas, la puntuación, las palabras vacías y las distintas construcciones de palabras son fundamentales para comprender el lenguaje. Algunos modelos pueden requerir indicadores como las mayúsculas y minúsculas. Por ejemplo, un clasificador de reconocimiento de entidades con nombre, porque en inglés los nombres propios se escriben en mayúsculas.
Un enfoque alternativo es realizar una reducción de la dimensionalidad con Análisis de Componentes Principales (ACP) o Descomposición de Valores Singulares (SVD), para reducir el espacio de características a una dimensionalidad específica (por ejemplo, cinco o diez mil dimensiones) basada en la frecuencia de las palabras. Estos transformadores tendrían que aplicarse después de un transformador vectorizador, y tendrían el efecto de fusionar las palabras que son similares en el mismo vector espacio.
Tuberías
El proceso de aprendizaje automático suele combinar una serie de transformadores sobre los datos brutos, transformando el conjunto de datos en cada paso del camino hasta pasarlo al método de ajuste de un estimador final. Pero si no vectorizamos nuestros documentos de la misma manera exacta, acabaremos obteniendo resultados erróneos o, como mínimo, ininteligibles. El objeto Scikit-Learn Pipeline
es la solución a este dilema.
Pipeline
nos permiten integrar una serie de transformadores que combinan la normalización, la vectorización y el análisis de rasgos en un mecanismo único y bien definido. Como se muestra en la Figura 4-6, los objetos Pipeline
mueven los datos desde un cargador (un objeto que envolverá nuestro CorpusReader
del Capítulo 2) a mecanismos de extracción de rasgos para, finalmente, llegar a un objeto estimador que implemente nuestros modelos predictivos. Las tuberías son grafos acíclicos dirigidos (DAG) que pueden ser desde simples cadenas lineales de transformadores hasta rutas de ramificación y unión arbitrariamente complejas.
Conceptos básicos sobre tuberías
La finalidad de un Pipeline
es encadenar varios estimadores que representen una secuencia fija de pasos en una sola unidad. Todos los estimadores de la cadena, excepto el último, deben ser transformadores, es decir, implementar el método transform
, mientras que el último estimador puede ser de cualquier tipo, incluidos los estimadores predictivos. Las canalizaciones resultan cómodas; fit
y transform
pueden invocarse para entradas únicas en varios objetos a la vez. Las tuberías también proporcionan una interfaz única para la búsqueda en cuadrícula de múltiples estimadores a la vez. Y lo que es más importante, los pipelines proporcionan la operacionalización de los modelos de texto acoplando una metodología de vectorización con un modelo predictivo.
Las tuberías se construyen describiendo una lista de pares (key, value)
, donde key
es una cadena que nombra el paso y value
es el objeto estimador. Las tuberías se pueden crear utilizando la función de ayuda make_pipeline
, que determina automáticamente los nombres de los pasos, o especificándolos directamente. En general, es mejor especificar los pasos directamente para proporcionar una buena documentación al usuario, mientras que make_pipeline
se utiliza más a menudo para la construcción automática de tuberías.
Pipeline
son una utilidad específica de Scikit-Learn, pero también son el punto crítico de integración con NLTK y Gensim. He aquí un ejemplo que une los objetos TextNormalizer
y GensimVectorizer
que creamos en la última sección, antes de un modelo bayesiano. Utilizando la API Transformer
, tal y como se ha comentado anteriormente en el capítulo, podemos utilizar TextNormalizer
para envolver los objetos CorpusReader
de NLTK y realizar el preprocesamiento y la extracción de características lingüísticas. Nuestro GensimVectorizer
se encarga de la vectorización, y Scikit-Learn de la integración mediante Pipelines, utilidades como la validación cruzada, y los numerosos modelos que utilizaremos, desde Naive Bayes a Regresión Logística.
from
sklearn.pipeline
import
Pipeline
from
sklearn.naive_bayes
import
MultinomialNB
model
=
Pipeline
([
(
'normalizer'
,
TextNormalizer
()),
(
'vectorizer'
,
GensimVectorizer
()),
(
'bayes'
,
MultinomialNB
()),
])
La tubería puede utilizarse entonces como una única instancia de un modelo completo. Llamar a model.fit
es lo mismo que llamar a fit
en cada estimador en secuencia, transformando la entrada y pasándola al siguiente paso. Otros métodos como fit_transform
se comportan de forma similar. La cadena también tendrá todos los métodos que tenga el último estimador de la cadena. Si el último estimador es un transformador, también lo será la canalización. Si el último estimador es un clasificador, como en el ejemplo anterior, entonces la canalización también tendrá los métodos predict
y score
para que todo el modelo pueda utilizarse como clasificador.
Los estimadores de la canalización se almacenan en forma de lista, y se puede acceder a ellos por índice. Por ejemplo, model.steps[1]
devuelve la tupla ('vectorizer', GensimVectorizer(path=None))
. Sin embargo, lo más habitual es identificar los estimadores por su nombre utilizando la propiedad named_steps
del diccionario del objeto Pipeline
. La forma más sencilla de acceder al modelo predictivo es utilizar model.named_steps["bayes"]
y obtener el estimador directamente.
Búsqueda en cuadrícula para la optimización de hiperparámetros
En el Capítulo 5, hablaremos más sobre el ajuste del modelo y la iteración, pero por ahora nos limitaremos a introducir una extensión de Pipeline
, GridSearch
, que es útil para la optimización de hiperparámetros. La búsqueda en cuadrícula puede implementarse para modificar los parámetros de todos los estimadores de la tubería como si fuera un único objeto. Para acceder a los atributos de los estimadores, utilizarías los métodos de la tubería set_params
o get_params
con una representación dunderscore de los nombres del estimador y del parámetro, como se indica a continuación: estimator__parameter
.
Supongamos que queremos codificar de una sola vez sólo los términos que aparecen al menos tres veces en el corpus; podríamos modificar la Binarizer
del siguiente modo:
model
.
set_params
(
onehot__threshold
=
3.0
)
Utilizando este principio, podríamos ejecutar una búsqueda reticular definiendo los parámetros de búsqueda reticular mediante la sintaxis de parámetros dunderscore. Considera la siguiente búsqueda reticular para determinar el mejor modelo bayesiano de clasificación de textos codificado en un solo punto:
from
sklearn.model_selection
import
GridSearchCV
search
=
GridSearchCV
(
model
,
param_grid
=
{
'count__analyzer'
:
[
'word'
,
'char'
,
'char_wb'
],
'count__ngram_range'
:
[(
1
,
1
),
(
1
,
2
),
(
1
,
3
),
(
1
,
4
),
(
1
,
5
),
(
2
,
3
)],
'onehot__threshold'
:
[
0.0
,
1.0
,
2.0
,
3.0
],
'bayes__alpha'
:
[
0.0
,
1.0
],
})
La búsqueda nomina tres posibilidades para el parámetro del analizador CountVectorizer
(crear n-gramas en los límites de las palabras, en los límites de los caracteres, o sólo en los caracteres que están entre los límites de las palabras), y varias posibilidades para los rangos de n-gramas contra los que tokenizar. También especificamos el umbral de binarización, es decir, que el n-grama tiene que aparecer un determinado número de veces antes de ser incluido en el modelo. Por último, la búsqueda especifica dos parámetros de suavizado (el parámetro bayes_alpha
): sin suavizado (añadir 0,0) o suavizado laplaciano (añadir 1,0).
La búsqueda en la cuadrícula instanciará una tubería de nuestro modelo para cada combinación de características, y luego utilizará la validación cruzada para puntuar el modelo y seleccionar la mejor combinación de características (en este caso, la combinación que maximiza la puntuación F1).
Enriquecer la extracción de rasgos con uniones de rasgos
Las tuberías no tienen por qué ser simples secuencias lineales de pasos; de hecho, pueden ser arbitrariamente complejas mediante la implementación de uniones de características. El objeto FeatureUnion
combina varios objetos transformadores en un nuevo y único transformador similar al objeto Pipline
. Sin embargo, en lugar de ajustar y transformar los datos en secuencia a través de cada transformador, se evalúan independientemente y los resultados se concatenan en un vector compuesto.
Considera el ejemplo de la Figura 4-7. Podríamos imaginar un transformador analizador HTML que utiliza BeautifulSoup o una biblioteca XML para analizar el HTML y devolver el cuerpo de cada documento. A continuación, realizamos un paso de ingeniería de características, en el que las entidades y las frases clave se extraen de los documentos y los resultados se pasan a la unión de características. Utilizar la codificación de frecuencias en las entidades es más sensato, ya que son relativamente pequeñas, pero TF-IDF tiene más sentido para las frases clave. A continuación, la unión de características concatena los dos vectores resultantes, de modo que nuestro espacio de decisión antes de la regresión logística separa las dimensiones de las palabras en el título de las dimensiones de las palabras en el cuerpo.
FeatureUnion
se instancian de forma similar como objetos Pipeline
con una lista de pares (key, value)
donde key
es el nombre del transformador y value
es el objeto transformador. También existe una función de ayuda make_union
que puede determinar automáticamente los nombres y que se utiliza de forma similar a la función de ayuda make_pipeline
, para canalizaciones automáticas o generadas. También se puede acceder a los parámetros de los estimadores de la misma manera, y para implementar una búsqueda en una unión de características, basta con anidar el dunderscore de cada transformador en la unión de características.
Dados los transformadores no implementados EntityExtractor
y KeyphraseExtractor
mencionados anteriormente, podemos construir nuestro pipeline como sigue:
from
sklearn.pipeline
import
FeatureUnion
from
sklearn.linear_model
import
LogisticRegression
model
=
Pipeline
([
(
'parser'
,
HTMLParser
()),
(
'text_union'
,
FeatureUnion
(
transformer_list
=
[
(
'entity_feature'
,
Pipeline
([
(
'entity_extractor'
,
EntityExtractor
()),
(
'entity_vect'
,
CountVectorizer
()),
])),
(
'keyphrase_feature'
,
Pipeline
([
(
'keyphrase_extractor'
,
KeyphraseExtractor
()),
(
'keyphrase_vect'
,
TfidfVectorizer
()),
])),
],
transformer_weights
=
{
'entity_feature'
:
0.6
,
'keyphrase_feature'
:
0.2
,
}
)),
(
'clf'
,
LogisticRegression
()),
])
Ten en cuenta que los objetos HTMLParser
, EntityExtractor
y KeyphraseExtractor
no están implementados actualmente, pero se utilizan a título ilustrativo. La unión de rasgos se ajusta en secuencia con respecto al resto de la cadena, pero cada transformador dentro de la unión de rasgos se ajusta de forma independiente, lo que significa que cada transformador ve los mismos datos como entrada a la unión de rasgos. Durante la transformación, cada transformador se aplica en paralelo y los vectores que emiten se concatenan en un único vector mayor, que puede ponderarse opcionalmente, como se muestra en la Figura 4-8.
En este ejemplo, estamos ponderando más el transformador entity_feature
que el transformador keyphrase_feature
. Utilizando combinaciones de transformadores personalizados, uniones de rasgos y canalizaciones, es posible definir una extracción y transformación de rasgos increíblemente rica de forma repetible. Al reunir nuestra metodología en una única secuencia, podemos aplicar las transformaciones de forma repetible, sobre todo en documentos nuevos cuando queremos hacer predicciones en un entorno de producción .
Conclusión
En este capítulo, hemos realizado una visión general de las técnicas de vectorización y hemos empezado a considerar sus casos de uso para diferentes tipos de datos y diferentes algoritmos de aprendizaje automático. En la práctica, lo mejor es seleccionar un esquema de codificación en función del problema que se plantee; ciertos métodos superan sustancialmente a otros para determinadas tareas.
Por ejemplo, para los modelos de redes neuronales recurrentes suele ser mejor utilizar la codificación de un solo punto, pero para dividir el espacio textual se podría crear un vector combinado para el resumen del documento, el encabezamiento del documento, el cuerpo, etc. La codificación de frecuencias debe normalizarse, pero distintos tipos de codificación de frecuencias pueden beneficiar a métodos probabilísticos como los modelos bayesianos. TF-IDF es una excelente codificación de uso general y suele utilizarse primero en modelización, pero también puede cubrir muchos pecados. Las representaciones distribuidas son la nueva moda, pero consumen mucho rendimiento y son difíciles de escalar.
Los modelos de bolsa de palabras tienen una dimensionalidad muy alta, lo que significa que el espacio es extremadamente disperso, lo que dificulta la generalización del espacio de datos. El orden de las palabras, la gramática y otras características estructurales se pierden de forma nativa, y es difícil añadir conocimientos (por ejemplo, recursos léxicos, codificaciones ontológicas) al proceso de aprendizaje. Las codificaciones locales (por ejemplo, las representaciones no distribuidas) requieren muchas muestras, lo que podría llevar a un sobreentrenamiento o a un ajuste insuficiente, pero las representaciones distribuidas son complejas y añaden una capa de "misticismo representacional".
En última instancia, gran parte del trabajo de las aplicaciones que tienen en cuenta el lenguaje procede del análisis de características específicas del dominio, no de la simple vectorización. En la sección final de este capítulo hemos explorado el uso de los objetos FeatureUnion
y Pipeline
para crear metodologías de extracción significativas mediante la combinación de transformadores. A medida que avancemos, la práctica de construir pipelines de transformadores y estimadores seguirá siendo nuestro principal mecanismo para realizar aprendizaje automático. En el Capítulo 5 exploraremos los modelos de clasificación y sus aplicaciones; después, en el Capítulo 6, echaremos un vistazo a los modelos de agrupación, a menudo denominados modelos temáticos en el análisis de textos. En el Capítulo 7, exploraremos algunos métodos más complejos de análisis y exploración de características que nos ayudarán a afinar nuestros modelos basados en vectores para obtener mejores resultados. No obstante, los modelos sencillos que sólo tienen en cuenta las frecuencias de las palabras suelen tener mucho éxito. Según nuestra experiencia, ¡un modelo de bolsa de palabras puro funciona aproximadamente el 85% de las veces!
1 Quoc V. Le y Tomas Mikolov, Representaciones distribuidas de frases y documentos, (2014) http://bit.ly/2GJBHjZ
Get Análisis de Texto Aplicado con Python now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.