Etapa 3: preparação dos dados

Para que nossos dados possam ser alimentados em um modelo, ele precisa ser transformado em um formato que o modelo entenda.

Primeiro, as amostras de dados que coletamos podem estar em uma ordem específica. Não queremos que qualquer informação associada à ordem das amostras influencie a relação entre textos e rótulos. Por exemplo, se um conjunto de dados for classificado por classe e 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. Caso seus dados já estejam divididos em conjuntos de treinamento e validação, não se esqueça de transformar os dados de validação da mesma maneira que você transforma 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. É normal usar 80% das amostras para treinamento e 20% para validação.

Segundo, os algoritmos de machine learning consideram os números como entradas. Isso significa que precisaremos converter os textos em vetores numéricos. Há duas etapas para esse 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, ou seja, o conjunto de tokens exclusivos presentes nos dados.

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

Vamos ver como executar essas duas etapas para vetores n-grama e vetores de sequência, bem como como otimizar as representações vetoriais usando técnicas de seleção de atributos e normalização.

Vetores gramados [Opção A]

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

Em um vetor de n-grama, 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 unigramas (n = 1) é ['the', 'mouse', 'ran', 'up', 'clock'], a palavra bigram (n = 2) é ['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 + bigram fornece boa precisão e reduz o tempo de computação.

Vetorização

Depois de dividir nossas amostras de texto em n-gramas, precisamos transformá-las em vetores numéricos que nossos modelos de machine learning podem processar. O exemplo abaixo mostra os índices atribuídos aos unigramas e Bigrams 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: cada 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' (negrito abaixo) 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 particularmente exclusivas às amostras de texto no conjunto de dados) não são penalizadas. Por exemplo, palavras como "a" ocorrerão com muita frequência em todos os textos. Portanto, uma contagem de tokens mais alta 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] (See Scikit-learn TfidfTransformer)

Existem muitas outras representações vetoriais, mas as três acima são as mais usadas.

Observamos que a codificação tf-idf é ligeiramente melhor do que os outros dois em termos de acurácia (em média, de 0,25 a 15% maior) e recomendamos o uso desse método para vetorizar n-gramas. No entanto, lembre-se de que ele 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 em um conjunto de dados em tokens de palavra uni+bigram, podemos acabar com dezenas de milhares de tokens. Nem todos esses tokens/recursos contribuem para a previsão de rótulos. Por isso, podemos soltar alguns tokens, por exemplo, os que ocorrem raramente, em todo o conjunto de dados. Também podemos medir a importância do recurso (quanto cada token contribui para as previsões de rótulos) e incluir apenas os tokens mais informativos.

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

Mais importante, vimos que a precisão aumenta em cerca de 20.000 recursos para muitos conjuntos de dados (consulte a Figura 6). Adicionar mais recursos acima desse limite contribui com muito pouco e, às vezes, pode até mesmo causar overfitting (link em inglês).

Principais resultados de K vs Precisão

Figura 6: principais recursos K e precisão. Em conjuntos de dados, plataus de precisão em torno de 20 mil atributos principais.

Normalização

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

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

  • Tokenizar as amostras de texto em palavras uni+bigrams,
  • Vectoring usando codificação tf-idf,
  • Selecione apenas os 20.000 principais atributos do vetor de tokens descartando tokens que aparecem menos de duas vezes e usando f_classif para calcular a importância do recurso.
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 vetorial de n-grama, descartamos muitas informações sobre a ordem das palavras e a gramática (no melhor, podemos manter algumas informações de ordem parcial quando n > 1). Isso é chamado de abordagem "saco de palavras". Essa representação é usada em conjunto com modelos que não consideram a ordem, como regressão logística, perceptrons de várias camadas, máquinas de otimização de gradiente, máquinas de vetor de suporte.

Vetores de sequência [Opção B]

Nos parágrafos subsequentes, veremos como realizar a tokenização e a vetorização para modelos sequenciais. Também falaremos sobre como otimizar a representação da sequência usando técnicas de seleção e normalização de atributos.

Para algumas amostras, a ordem das palavras é fundamental para o significado do texto. Por exemplo, as frases "Eu costumava odiar meu deslocamento diário. Minha nova bicicleta mudou totalmente " pode ser entendido apenas quando lido 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 uma sequência de palavras. Descobrimos que usar a representação no nível da palavra oferece melhor desempenho do que os tokens de caracteres. Esse também é o padrão geral seguido pelo setor. Só é recomendável usar tokens de caracteres quando os textos têm muitos erros de digitação, o que não costuma acontecer.

Vetorização

Depois de convertermos as amostras de texto em sequências de palavras, precisamos transformá-las 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 tokens em que o primeiro texto é convertido.

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]

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

Codificação one-hot: as sequências são representadas por vetores de palavras em espaço n-dimensional, em que n = tamanho do vocabulário. Essa representação funciona muito bem quando tokenizamos como caracteres, e o vocabulário é, portanto, pequeno. Quando tokenizamos como palavras, o vocabulário geralmente tem 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]
]

Incorporações de palavras: as palavras têm significados associados a elas. Como resultado, podemos representar tokens de palavras em um espaço vetorial denso (cerca de cem números reais), em que a localização e a distância entre as palavras indicam a semelhança entre elas semânticas (consulte a Figura 7). Essa representação é chamada de embeddings de palavras.

Embeddings de palavras

Figura 7: embeddings de palavras

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

Camada de embedding

Figura 8: camada de embedding

Seleção de atributos

Nem todas as palavras nos nossos dados contribuem para previsões de identificadores. É possível otimizar o processo de aprendizado descartando palavras raras ou irrelevantes do nosso vocabulário. Na realidade, observamos que o uso dos 20.000 recursos mais frequentes geralmente é suficiente. Isso também vale para modelos n-grama (veja a Figura 6).

Vamos unir todas as etapas acima na vetorização da sequência. O código a seguir executa essas tarefas:

  • Tokeniza os textos em palavras
  • Cria um vocabulário com os 20.000 tokens principais
  • Converte os tokens em vetores sequenciais
  • Fixar as sequências a uma duração fixa
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 houver três classes, podemos usar os valores 0, 1 e 2 para representá-las. Internamente, a rede usará vetores one-hot para representar esses valores (para evitar a inferência de uma relação incorreta entre rótulos). Essa representação depende da função de perda e da função de ativação de última camada que usamos na rede neural. Vamos aprender mais sobre isso na próxima seção.