Paso 3: Prepare sus datos

Antes de que nuestros datos puedan ingresarse a un modelo, deben transformarse a un formato que el modelo pueda comprender.

Primero, 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 conjuntos no serán representativos de la distribución general de los datos.

Una práctica recomendada simple para garantizar que el modelo no se vea afectado por el orden de los datos es siempre redistribuir los datos antes de hacer cualquier otra cosa. Si tus datos ya están divididos en conjuntos de entrenamiento y validación, asegúrate de transformar los datos de validación de la misma manera en que lo haces. Si aún no tienes conjuntos separados de entrenamiento y validación, puedes dividir las muestras después de la redistribución. Se suele usar el 80% de las muestras para el entrenamiento y el 20% para la 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. El 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 vectores n-grama y vectores de secuencia, además de optimizar las representaciones vectoriales con técnicas de normalización y selección de atributos.

Vectores n-grama [Opción A]

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

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 y bigramas de palabra proporciona una buena precisión y toma menos tiempo de procesamiento.

Vectorización

Una vez que dividimos las muestras de texto en n-gramas, debemos convertir los 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, generalmente vectorizamos con 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 recuento: 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 correspondiente al unigrama "el" 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 de los dos enfoques anteriores es que no se penalizan las palabras comunes que aparecen con frecuencias similares en todos los documentos (es decir, las palabras que no son particularmente exclusivas de las muestras de texto del conjunto de datos). Por ejemplo, palabras como "a" aparecerán con mucha frecuencia en todos los textos. Por eso, 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]

(Consulta TfidfTransformer de scikit-learn).

Existen muchas otras representaciones vectoriales, pero las tres anteriores son las más usadas.

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

Selección de los atributos

Cuando convertimos todos los textos de un conjunto de datos en tokens de uni+bigram de palabras, podemos terminar con decenas de miles de tokens. No todos estos tokens o atributos contribuyen a la predicción de etiquetas. Podemos descartar ciertos tokens, como los que ocurren muy rara vez 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.

Hay muchas funciones estadísticas que toman atributos y las etiquetas correspondientes y generan la puntuación de importancia de los atributos. Dos funciones de uso general son f_classif y chi2. Nuestros experimentos muestran que ambas funciones tienen el mismo rendimiento.

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

Top K versus exactitud

Figura 6: Funciones de Top K frente a la exactitud. En todos los conjuntos de datos, la exactitud se estanca en los 20,000 atributos principales.

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 vimos, la normalización durante el procesamiento previo de datos no parece agregar mucho valor a los problemas de clasificación de texto; recomendamos omitir este paso.

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

  • Asignar tokens a muestras de texto en uni+bigramas de palabras
  • Vectorizar con la codificación 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 del vector de n-grama, descartamos mucha información sobre el orden de las palabras y la gramática (en el mejor de los casos, podemos mantener información de orden parcial cuando n > 1). Esto se denomina enfoque de bolsa 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 multicapas, las máquinas de boosting de gradientes y las máquinas de vectores de soporte.

Vectores de secuencia [opción B]

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

En 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 diario. Mi bicicleta nueva cambió eso por completo” se puede entender solo cuando se lee en orden. Modelos como CNN o RNN pueden inferir el significado del orden de las palabras en una muestra. Para estos modelos, representamos el texto como una secuencia de tokens que preserva el orden.

Asignación de token

El texto se puede representar como una secuencia de caracteres o de palabras. Descubrimos que usar 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, que no suele ser el caso.

Vectorización

Una vez que hayamos convertido las 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 los que se convierte el primer texto.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'

Índice asignado para cada token:

{'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}

NOTA: La palabra “el” aparece con mayor frecuencia, por lo que se le asigna el valor de índice de 1. Algunas bibliotecas reservan el índice 0 para tokens desconocidos, como en este caso.

Secuencia de índices de tokens:

'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

Hay dos opciones disponibles para vectorizar las secuencias de token:

Codificación one-hot: Las secuencias se representan con 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 suele tener decenas de miles de tokens, lo que hace que los vectores one-hot sean muy ineficientes y dispersos. 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 (aproximadamente unos pocos cientos de números reales), en el que la ubicación y la distancia entre las palabras indican qué tan similares son semánticamente (consulta la figura 7). Esta representación se llama incorporaciones de palabras.

Incorporaciones de palabras

Figura 7: Incorporaciones de palabras

Los modelos de secuencias suelen tener una capa de incorporación de este tipo como primera capa. Esta capa aprende a convertir las secuencias de índices de palabras en vectores de incorporación de palabras durante el proceso de entrenamiento, de modo que cada índice de palabra se asigne a un vector denso de valores reales que representen 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 de nuestros datos contribuyen a las predicciones de etiquetas. Podemos optimizar nuestro proceso de aprendizaje descartando palabras raras o irrelevantes de nuestro vocabulario. De hecho, observamos que usar los 20,000 atributos más frecuentes suele ser suficiente. 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:

  • Convierte los textos en tokens de palabras
  • Crea un vocabulario con los 20,000 tokens principales
  • Convierte los tokens en vectores de secuencia
  • Rellena las secuencias a una longitud de secuencia 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 las etiquetas en valores en el 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 one-hot 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 de la función de activación de la última capa que usamos en nuestra red neuronal. Aprenderemos más sobre esto en la próxima sección.