Etapa 3: preparação dos dados

Antes que nossos dados possam ser alimentados em um modelo, eles precisam ser transformados para um formato que o modelo possa entender.

Primeiro, as amostras de dados que coletamos podem estar em uma ordem específica. Não queremos que nenhuma informação associada à ordenação de amostras influencie a relação entre textos e rótulos. Por exemplo, se um conjunto de dados for classificado por classe e, em seguida, dividido em conjuntos de treinamento/validação, esses conjuntos não representarão a distribuição geral dos dados.

Uma prática recomendada simples para garantir que o modelo não seja afetado pela ordem dos dados é sempre embaralhar os dados antes de fazer qualquer outra coisa. Se os dados já estiverem divididos em conjuntos de treinamento e validação, transforme os dados de validação da mesma forma que os dados de treinamento. Se você ainda não tiver conjuntos separados de treinamento e validação, poderá dividir as amostras após o embaralhamento. É comum usar 80% delas para treinamento e 20% para validação.

Segundo, os algoritmos de machine learning usam números como entradas. Isso significa que precisaremos converter os textos em vetores numéricos. Há duas etapas nesse processo:

  1. Tokenização: divida os textos em palavras ou subtextos menores, o que permitirá uma boa generalização da relação entre os textos e os rótulos. Isso determina o "vocabulário" do conjunto de dados (conjunto de tokens exclusivos presentes nos dados).

  2. Vetorização: defina uma boa medida numérica para caracterizar esses textos.

Vamos aprender a realizar essas duas etapas para vetores "n-gram" e vetores de sequência, além de como otimizar as representações vetoriais usando técnicas de seleção de atributos e normalização.

N-gram vetores [Opção A]

Nos parágrafos subsequentes, veremos como fazer tokenização e vetorização para modelos de n-gramas. Também abordaremos como otimizar a representação "n-grama" usando técnicas de seleção de atributos e normalização.

Em um vetor "n-gram", o texto é representado como uma coleção de n-gramas exclusivos: grupos de n tokens adjacentes (normalmente, palavras). Considere o texto The mouse ran up the clock. Aqui:

  • A palavra unigrama (n = 1) é ['the', 'mouse', 'ran', 'up', 'clock'].
  • As palavras bigramas (n = 2) são ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock']
  • E assim por diante.

Tokenização

Descobrimos que a tokenização em unigramas de palavras + bigramas fornece boa precisão e consome menos tempo de computação.

Vetorização

Depois de dividir nossas amostras de texto em n-gramas, precisamos transformar esses n-gramas em vetores numéricos que nossos modelos de machine learning podem processar. O exemplo abaixo mostra os índices atribuídos aos unigramas e bigramas gerados para dois 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}

Depois que os índices são atribuídos aos n-gramas, normalmente vetorizamos usando uma das opções a seguir.

Codificação one-hot: todo texto de amostra é representado como um vetor que indica a presença ou ausência de um token no texto.

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

Codificação de contagem: cada texto de amostra é representado como um vetor que indica a contagem de um token no texto. Observe que o elemento correspondente ao unigrama "the" agora é representado como 2 porque a palavra "the" aparece duas vezes no texto.

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

Codificação Tf-idf: o problema com as duas abordagens acima é que palavras comuns que ocorrem em frequências semelhantes em todos os documentos (ou seja, palavras que não são especialmente exclusivas das amostras de texto no conjunto de dados) não são penalizadas. Por exemplo, palavras como "um" aparecem com muita frequência em todos os textos. Portanto, uma contagem de tokens maior para "o" do que para outras palavras mais significativas não é muito ú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]

Consulte Scikit-learn TfidfTransformer.

Há muitas outras representações vetoriais, mas as três anteriores são as mais usadas.

Observamos que a codificação tf-idf é ligeiramente melhor do que os outros dois em termos de precisão (em média: 0,25-15% maior) e recomendamos o uso desse método para vetorizar n-gramas. No entanto, lembre-se de que isso ocupa mais memória (já que usa representação de ponto flutuante) e leva mais tempo para calcular, especialmente para grandes conjuntos de dados (pode levar duas vezes mais tempo em alguns casos).

Seleção de atributos

Quando convertemos todos os textos de um conjunto de dados em tokens uni+bigram de palavras, podemos acabar com dezenas de milhares de tokens. Nem todos esses tokens/recursos contribuem para a previsão de rótulos. Podemos descartar tokens, por exemplo, aqueles que ocorrem raramente no conjunto de dados. Também é possível medir a importância do atributo (quanto cada token contribui para as previsões de rótulo) e incluir apenas os tokens mais informativos.

Há muitas funções estatísticas que usam atributos e os rótulos correspondentes para gerar a pontuação de importância do recurso. Duas funções comumente usadas são f_classif e chi2. Nossos experimentos mostram que ambas as funções têm o mesmo desempenho.

Mais importante ainda, vimos que a precisão atinge o pico em cerca de 20.000 recursos para muitos conjuntos de dados (veja a Figura 6). Adicionar mais recursos acima desse limite contribui muito pouco e, às vezes, leva a overfitting (link em inglês) e prejudica o desempenho.

Top-K versus acurácia

Figura 6: atributos Top-K versus acurácia. Nos conjuntos de dados, a precisão alcança cerca de 20 mil recursos principais.

Normalização

A normalização converte todos os valores de atributos/amostras em valores pequenos e semelhantes. Isso simplifica a convergência do gradiente descendente nos algoritmos de aprendizado. Pelo que aprendemos, a normalização durante o pré-processamento de dados parece não agregar muito valor aos problemas de classificação de texto. Recomendamos pular esta etapa.

O código a seguir reúne todas as etapas acima:

  • tokenizar amostras de texto em uni+bigrams de palavras
  • Vetorize usando a codificação tf-idf.
  • Selecione apenas os 20.000 principais atributos do vetor de tokens descartando os tokens que aparecem menos de 2 vezes e usando f_classif para calcular a importância do 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

Com a representação de vetores "n-gram", descartamos muitas informações sobre a ordem das palavras e a gramática. Na melhor das hipóteses, podemos manter algumas informações de ordenação parcial quando n > 1. Isso é chamado de abordagem de "saco de palavras". Essa representação é usada em conjunto com modelos que não consideram a ordenação, como regressão logística, perceptrons multicamadas, máquinas de otimização de gradiente e máquinas de vetor de suporte.

Vetores da sequência [Option B]

Nos parágrafos subsequentes, veremos como fazer tokenização e vetorização para modelos sequenciais. Também vamos abordar como otimizar a representação de sequências usando técnicas de seleção de atributos e normalização.

Em alguns exemplos de texto, a ordem das palavras é fundamental para o significado do texto. Por exemplo, as frases: "Eu odiava meu deslocamento diário. "Minha nova bicicleta mudou completamente" só pode ser entendida quando lida em ordem. Modelos como CNNs/RNNs podem inferir o significado da ordem das palavras em uma amostra. Para esses modelos, representamos o texto como uma sequência de tokens, preservando a ordem.

Tokenização

O texto pode ser representado como uma sequência de caracteres ou de palavras. Descobrimos que o uso da representação em nível de palavra oferece um desempenho melhor do que tokens de caracteres. Essa também é a norma geral seguida pela indústria. O uso de tokens de caracteres só faz sentido se os textos têm muitos erros de digitação, o que normalmente não é o caso.

Vetorização

Depois de converter nossos exemplos de texto em sequências de palavras, precisamos transformar essas sequências em vetores numéricos. O exemplo abaixo mostra os índices atribuídos aos unigramas gerados para dois textos e, em seguida, a sequência de índices de token em que o primeiro texto é convertido.

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

Índice atribuído a cada token:

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

OBSERVAÇÃO: a palavra "the" ocorre com mais frequência, portanto, o valor de índice 1 é atribuído a ela. Algumas bibliotecas reservam o índice 0 para tokens desconhecidos, como neste caso.

Sequência de índices de token:

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

Há duas opções disponíveis para vetorizar as sequências de token:

Codificação one-hot: as sequências são representadas usando vetores de palavras em um espaço n-dimensional, em que n = tamanho do vocabulário. Essa representação funciona muito bem quando estamos tokenizando como caracteres, portanto, o vocabulário é pequeno. Quando estivermos tokenizando como palavras, o vocabulário geralmente terá dezenas de milhares de tokens, tornando os vetores one-hot muito esparsos e ineficientes. Exemplo:

'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]
]

Embeddings de palavras: as palavras têm significados associados a elas. Como resultado, podemos representar tokens de palavra em um espaço vetorial denso (cerca de algumas centenas de números reais), em que a localização e a distância entre as palavras indicam a semelhança semântica delas (veja a Figura 7). Essa representação é chamada de embeddings de palavras.

Embeddings de palavras

Figura 7: embeddings de palavras

Os modelos sequenciais geralmente têm uma camada de embedding como a primeira. Essa camada aprende a transformar sequências de índices de palavras em vetores de embedding de palavras durante o processo de treinamento, de modo que cada índice de palavras seja mapeado para um vetor denso de valores reais que representam a localização dessa palavra no espaço semântico (consulte a Figura 8).

Camada de embedding

Figura 8: camada de embedding

Seleção de atributos

Nem todas as palavras dos nossos dados contribuem para as previsões de rótulos. Podemos otimizar nosso processo de aprendizagem descartando palavras raras ou irrelevantes do nosso vocabulário. Na verdade, observamos que usar os 20.000 recursos mais frequentes geralmente é suficiente. Isso também se aplica a modelos "n-gram" (veja a Figura 6).

Vamos colocar todas as etapas acima na vetorização de sequência em conjunto. O código abaixo executa essas tarefas:

  • Tokeniza os textos em palavras
  • Cria um vocabulário usando os 20.000 tokens principais
  • Converte os tokens em vetores de sequência
  • Fixa as sequências em um tamanho fixo
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

Vetorização de rótulos

Vimos como converter dados de texto de amostra em vetores numéricos. Um processo semelhante precisa ser aplicado aos rótulos. Podemos simplesmente converter rótulos em valores no intervalo [0, num_classes - 1]. Por exemplo, se há três classes, podemos usar os valores 0, 1 e 2 para representá-las. Internamente, a rede usa vetores one-hot para representar esses valores, evitando inferir uma relação incorreta entre os rótulos. Essa representação depende da função de perda e da função de ativação da última camada que usamos na rede neural. Vamos aprender mais sobre isso na próxima seção.