Paso 3: Prepare sus datos

Antes de que se puedan ingresar datos en un modelo, se debe transformar a un formato que el modelo pueda comprender.

En primer lugar, las muestras de datos que recopilamos pueden estar en un orden específico. No queremos que ninguna información asociada con el orden de las muestras influya en la relación entre los textos y las etiquetas. Por ejemplo, si un conjunto de datos se ordena por clase y, luego, se divide en conjuntos de entrenamiento o validación, estos no representarán la distribución general de datos.

Una práctica recomendada simple para garantizar que el modelo no se vea afectado por el orden de los datos es mezclar los datos siempre antes de hacer cualquier otra cosa. Si tus datos ya están divididos en conjuntos de entrenamiento y validación, asegúrate de transformar tus datos de validación de la misma manera en que transformas tus datos de entrenamiento. Si aún no tienes conjuntos de entrenamiento y validación separados, puedes dividir las muestras después de la redistribución. Es normal usar un 80% de las muestras para entrenamiento y un 20% para validación.

En segundo lugar, los algoritmos de aprendizaje automático toman números como entradas. Esto significa que tendremos que convertir los textos en vectores numéricos. Este proceso consta de dos pasos:

  1. Asignación de token: divide los textos en palabras o subtextos más pequeños, lo que permitirá una buena generalización de la relación entre los textos y las etiquetas. Esto determina el “vocabulario” del conjunto de datos (conjunto de tokens únicos presentes en los datos).

  2. Vectorización: Define una buena medida numérica para caracterizar estos textos.

Veamos cómo realizar estos dos pasos para los vectores n-grama y los vectores de secuencia, además de cómo optimizar las representaciones vectoriales mediante la selección de atributos y las técnicas de normalización.

Vectores de N-grama [Opción A]

En los párrafos siguientes, veremos cómo realizar la asignación de token y la vectorización para modelos n-grama. También analizaremos cómo optimizar la representación de n-grama con técnicas de selección y normalización de atributos.

En un vector de n-grama, el texto se representa como una colección de n-gramas únicos: grupos de n tokens adyacentes (por lo general, palabras). Considera el texto The mouse ran up the clock. Aquí, la palabra unigramas (n = 1) es ['the', 'mouse', 'ran', 'up', 'clock'], la palabra bigramas (n = 2) son ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock'], y así sucesivamente.

Asignación de token

Descubrimos que la asignación de token a unigramas de palabra y bigramas proporciona una buena precisión y requiere menos tiempo de procesamiento.

Vectorización

Una vez que dividimos nuestras muestras de texto en n-gramas, debemos convertir estos n-gramas en vectores numéricos que nuestros modelos de aprendizaje automático pueden procesar. En el siguiente ejemplo, se muestran los índices asignados a los unigramas y bigramas generados para dos textos.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10,
  'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the
clock': 8, 'down': 1, 'ran down': 5}

Una vez que se asignan los índices a los n-gramas, por lo general, vectorizamos mediante una de las siguientes opciones.

Codificación one-hot: cada texto de muestra se representa como un vector que indica la presencia o ausencia de un token en el texto.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]

Codificación de recuentos: Cada texto de muestra se representa como un vector que indica el recuento de un token en el texto. Ten en cuenta que el elemento que corresponde al unigrama (el negrita a continuación) ahora se representa como 2 porque la palabra “el” aparece dos veces en el texto.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]

Codificación tf-idf: El problema con los dos enfoques anteriores es que las palabras comunes que ocurren en frecuencias similares en todos los documentos (es decir, palabras que no son exclusivas de las muestras de texto en el conjunto de datos) no se penalizan. Por ejemplo, palabras como "a" aparecerán con mucha frecuencia en todos los textos. Por lo tanto, un recuento de tokens más alto para "el" que para otras palabras más significativas no es muy útil.

'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33,
0.23, 0.33, 0.33] (See Scikit-learn TfidfTransformer)

Hay muchas otras representaciones de vector, pero las tres anteriores son las que más se usan.

Observamos que la codificación tf-idf es un poco mejor que las otras dos en términos de exactitud (en promedio: de 0.25 a 15% superior) y recomendamos usar este método para vectorizar n-gramas. Sin embargo, ten en cuenta que ocupa más memoria (ya que usa representación de punto flotante) y tarda más tiempo en procesarse, en especial para grandes conjuntos de datos (puede tomar el doble de tiempo en algunos casos).

Selección de los atributos

Cuando convertimos todos los textos en un conjunto de datos en tokens de palabra uni+bigram, podemos terminar con decenas de miles de tokens. No todos estos tokens o funciones contribuyen a la predicción de etiquetas. Podemos descartar ciertos tokens, por ejemplo, aquellos que ocurren muy pocas veces en el conjunto de datos. También podemos medir la importancia de los atributos (cuánto contribuye cada token a las predicciones de etiquetas) y solo incluir los tokens más informativos.

Existen muchas funciones estadísticas que toman atributos y las etiquetas correspondientes, y generan la puntuación de importancia de los atributos. Dos funciones que se usan con frecuencia son f_classif y chi2. Nuestros experimentos demuestran que ambas funciones tienen el mismo rendimiento.

Lo que es más importante, observamos que la precisión alcanza un máximo de alrededor de 20,000 atributos para muchos conjuntos de datos (consulta la Figura 6). Agregar más atributos por sobre este umbral contribuye muy poco y, a veces, incluso genera un sobreajuste y degrada el rendimiento.

K mejor frente a Exactitud

Figura 6: Comparación entre las funciones principales de K y la precisión. En los conjuntos de datos, la meseta de exactitud tiene aproximadamente 20,000 características.

Normalización

La normalización convierte todos los valores de atributos o muestras en valores pequeños y similares. Esto simplifica la convergencia del descenso de gradientes en los algoritmos de aprendizaje. Por lo que hemos visto, la normalización durante el procesamiento previo de los datos no parece agregar mucho valor a los problemas de clasificación de texto. Te recomendamos omitir este paso.

El siguiente código reúne todos los pasos anteriores:

  • Asignación de tokens de muestras de texto en palabras uni+bigramas,
  • Vectoriza mediante tf-idf,
  • Selecciona solo los 20,000 atributos principales del vector de tokens. Para ello, descarta los tokens que aparezcan menos de 2 veces y usa f_classif para calcular la importancia del atributo.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import f_classif

# Vectorization parameters
# Range (inclusive) of n-gram sizes for tokenizing text.
NGRAM_RANGE = (1, 2)

# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Whether text should be split into word or character n-grams.
# One of 'word', 'char'.
TOKEN_MODE = 'word'

# Minimum document/corpus frequency below which a token will be discarded.
MIN_DOCUMENT_FREQUENCY = 2

def ngram_vectorize(train_texts, train_labels, val_texts):
    """Vectorizes texts as n-gram vectors.

    1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams.

    # Arguments
        train_texts: list, training text strings.
        train_labels: np.ndarray, training labels.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val: vectorized training and validation texts
    """
    # Create keyword arguments to pass to the 'tf-idf' vectorizer.
    kwargs = {
            'ngram_range': NGRAM_RANGE,  # Use 1-grams + 2-grams.
            'dtype': 'int32',
            'strip_accents': 'unicode',
            'decode_error': 'replace',
            'analyzer': TOKEN_MODE,  # Split text into word tokens.
            'min_df': MIN_DOCUMENT_FREQUENCY,
    }
    vectorizer = TfidfVectorizer(**kwargs)

    # Learn vocabulary from training texts and vectorize training texts.
    x_train = vectorizer.fit_transform(train_texts)

    # Vectorize validation texts.
    x_val = vectorizer.transform(val_texts)

    # Select top 'k' of the vectorized features.
    selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1]))
    selector.fit(x_train, train_labels)
    x_train = selector.transform(x_train).astype('float32')
    x_val = selector.transform(x_val).astype('float32')
    return x_train, x_val

Con la representación vectorial n-grama, descartamos mucha información sobre el orden y la gramática de las palabras (en el mejor de los casos, podemos mantener cierta información de orden parcial cuando n > 1). Esto se conoce como un conjunto de palabras. Esta representación se usa junto con modelos que no tienen en cuenta el orden, como la regresión logística, los perceptrones de varias capas, las máquinas de boosting de gradientes y las máquinas vectoriales.

Vectores de secuencia [opción B]

En los párrafos siguientes, veremos cómo realizar la asignación de token y la vectorización para los modelos de secuencia. También analizaremos cómo optimizar la representación de secuencias mediante el uso de técnicas de selección y normalización.

Para algunas muestras de texto, el orden de las palabras es fundamental para el significado del texto. Por ejemplo, las oraciones: "Solía odiar mi viaje cotidiano. Mi nueva bicicleta cambió por completo” solo se puede entender cuando se lee en orden. Los modelos como las CNN o RNN pueden inferir el significado a partir del orden de las palabras en una muestra. Para estos modelos, representamos el texto como una secuencia de tokens que conservan el orden.

Asignación de token

El texto se puede representar como una secuencia de caracteres o una secuencia de palabras. Descubrimos que el uso de la representación a nivel de palabra proporciona un mejor rendimiento que los tokens de caracteres. Esta es también la norma general que sigue la industria. El uso de tokens de caracteres tiene sentido solo si los textos tienen muchos errores tipográficos, lo que no suele suceder.

Vectorización

Una vez que convertimos nuestras muestras de texto en secuencias de palabras, debemos convertir estas secuencias en vectores numéricos. En el siguiente ejemplo, se muestran los índices asignados a los unigramas generados para dos textos y, luego, la secuencia de índices de token a la que se convierte el primer texto.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}.
NOTE: 'the' occurs most frequently, so the index value of 1 is assigned to it.
Some libraries reserve index 0 for unknown tokens, as is the case here.
Sequence of token indexes: 'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

Hay dos opciones disponibles para vectorizar las secuencias de tokens:

Codificación one-hot: Las secuencias se representan mediante vectores de palabras en un espacio n-dimensional en el que n = tamaño del vocabulario. Esta representación funciona muy bien cuando asignamos tokens como caracteres y, por lo tanto, el vocabulario es pequeño. Cuando asignamos tokens como palabras, el vocabulario generalmente tiene decenas de miles de tokens, lo que hace que los vectores de un solo 1 sean muy dispersos e ineficientes. Ejemplo:

'The mouse ran up the clock' = [
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0]
]

Incorporaciones de palabras: las palabras tienen significados asociados. Como resultado, podemos representar tokens de palabras en un espacio vectorial denso (cerca de unos cientos de números reales), en el que la ubicación y la distancia entre las palabras indican qué tan similares son en términos semánticos (consulta la Figura 7). Esta representación se denomina incorporaciones de palabras.

Incorporaciones de palabras

Figura 7: Incorporaciones de palabras

Los modelos de secuencia a menudo tienen una capa de incorporación como su primera capa. Esta capa aprende a convertir secuencias de índices de palabras en vectores de incorporación de palabras durante el proceso de entrenamiento, de modo que cada índice de palabras se asigne a un vector denso de valores reales que representan la ubicación de esa palabra en el espacio semántico (consulta la Figura 8).

Capa de incorporación

Figura 8: Capa de incorporación

Selección de los atributos

No todas las palabras en nuestros datos contribuyen a las predicciones de etiquetas. Podemos optimizar nuestro proceso de aprendizaje descartando palabras poco frecuentes o irrelevantes de nuestro vocabulario. De hecho, observamos que, por lo general, basta con usar los 20,000 atributos más frecuentes. Esto también se aplica a los modelos n-grama (consulta la Figura 6).

Juntemos todos los pasos anteriores en la vectorización de secuencias. El siguiente código realiza estas tareas:

  • Asigna tokens a los textos en palabras.
  • Crea un vocabulario con los 20,000 tokens principales.
  • Convierte los tokens en vectores de secuencias
  • Rellena las secuencias con una longitud fija.
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text

# Vectorization parameters
# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Limit on the length of text sequences. Sequences longer than this
# will be truncated.
MAX_SEQUENCE_LENGTH = 500

def sequence_vectorize(train_texts, val_texts):
    """Vectorizes texts as sequence vectors.

    1 text = 1 sequence vector with fixed length.

    # Arguments
        train_texts: list, training text strings.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val, word_index: vectorized training and validation
            texts and word index dictionary.
    """
    # Create vocabulary with training texts.
    tokenizer = text.Tokenizer(num_words=TOP_K)
    tokenizer.fit_on_texts(train_texts)

    # Vectorize training and validation texts.
    x_train = tokenizer.texts_to_sequences(train_texts)
    x_val = tokenizer.texts_to_sequences(val_texts)

    # Get max sequence length.
    max_length = len(max(x_train, key=len))
    if max_length > MAX_SEQUENCE_LENGTH:
        max_length = MAX_SEQUENCE_LENGTH

    # Fix sequence length to max value. Sequences shorter than the length are
    # padded in the beginning and sequences longer are truncated
    # at the beginning.
    x_train = sequence.pad_sequences(x_train, maxlen=max_length)
    x_val = sequence.pad_sequences(x_val, maxlen=max_length)
    return x_train, x_val, tokenizer.word_index

Vectorización de etiquetas

Vimos cómo convertir datos de texto de muestra en vectores numéricos. Se debe aplicar un proceso similar a las etiquetas. Simplemente podemos convertir etiquetas en valores dentro del rango [0, num_classes - 1]. Por ejemplo, si hay 3 clases, podemos usar los valores 0, 1 y 2 para representarlas. De forma interna, la red usará vectores de un solo 1 para representar estos valores (a fin de evitar inferir una relación incorrecta entre las etiquetas). Esta representación depende de la función de pérdida y la función de activación de última capa que usamos en nuestra red neuronal. Aprenderemos más sobre esto en la siguiente sección.