Tutorial: Ajuste fino do BERT para análise de sentimentos

Tutorial: Afinação do BERT para Análise de Sentimentos

    Originally published by Skim AI's Machine Learning Researcher, Chris Tran.



BERT_for_Sentiment_Analysis





A - Introdução

Nos últimos anos, a comunidade de PNL assistiu a muitos avanços no Processamento de Linguagem Natural, especialmente a mudança para a aprendizagem por transferência. Modelos como o ELMo, o ULMFiT da fast.ai, o Transformer e o GPT da OpenAI permitiram aos investigadores obter resultados de ponta em vários parâmetros de referência e forneceram à comunidade grandes modelos pré-treinados com elevado desempenho. Esta mudança na PNL é vista como o momento ImageNet da PNL, uma mudança na visão computacional há alguns anos, quando as camadas inferiores das redes de aprendizagem profunda com milhões de parâmetros treinados numa tarefa específica podem ser reutilizadas e afinadas para outras tarefas, em vez de treinar novas redes a partir do zero.

Um dos maiores marcos na evolução da PNL recentemente é o lançamento do BERT da Google, que é descrito como o início de uma nova era na PNL. Neste bloco de notas, vou utilizar a função HuggingFace's transformadores para afinar o modelo BERT pré-treinado para uma tarefa de classificação. Em seguida, comparo o desempenho do BERT com um modelo de base, no qual utilizo um vectorizador TF-IDF e um classificador Naive Bayes. O transformadores A biblioteca ajuda-nos a afinar rápida e eficazmente o modelo BERT de última geração e a obter uma taxa de precisão 10% mais elevado do que o modelo de base.

Referência:

Para compreender Transformador (a arquitetura em que se baseia o BERT) e aprender a implementar o BERT, recomendo vivamente a leitura das seguintes fontes:

B - Configuração

1. Carregar bibliotecas essenciais

Em [0]:

import os
import re
from tqdm import tqdm
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

2. Conjunto de dados

2.1. Descarregar o conjunto de dados

Em [0]:

# Download data
import requests
request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf")
with open("data.zip", "wb") as file:
    file.write(request.content)
# Unzip data
import zipfile
with zipfile.ZipFile('data.zip') as zip:
    zip.extractall('data')

2.2. Dados do comboio de carga

Os dados do comboio têm 2 ficheiros, cada um contendo 1700 tweets com queixas/não queixas. Todos os tweets nos dados contêm pelo menos uma hashtag de uma companhia aérea.

Vamos carregar os dados de treino e rotulá-los. Como utilizamos apenas os dados de texto para classificar, eliminaremos as colunas sem importância e manteremos apenas id, tweet e etiqueta colunas.
Em [0]:

 # Load data and set labels
data_complaint = pd.read_csv('data/complaint1700.csv')
data_complaint['label'] = 0
data_non_complaint = pd.read_csv('data/noncomplaint1700.csv')
data_non_complaint['label'] = 1
# Concatenate complaining and non-complaining data
data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True)
# Drop 'airline' column
data.drop(['airline'], inplace=True, axis=1)
# Display 5 random samples
data.sample(5)

Saída[0]:

id tweet etiqueta
1988 24991 Que grande regresso de boas-vindas. É de rir. Desembarque... 1
1294 72380 Muito desiludido com a @JetBlue esta noite. O voo... 0
1090 127893 @united os meus amigos estão a passar um mau bocado... 0
553 58278 @united tudo o que eu quero para o Natal é um saco perdido que... 0
2075 30695 sim, não vou mentir... super interessado em experimentar... 1

Dividiremos aleatoriamente todos os dados de treino em dois conjuntos: um conjunto de treino com 90% dos dados e um conjunto de validação com 10% dos dados. Efectuaremos a afinação dos hiperparâmetros utilizando a validação cruzada no conjunto de treino e utilizaremos o conjunto de validação para comparar modelos.
Em [0]:

from sklearn.model_selection import train_test_split
X = data.tweet.values
y = data.label.values
X_train, X_val, y_train, y_val =
    train_test_split(X, y, test_size=0.1, random_state=2020)

2.3. Dados do ensaio de carga

Os dados de teste contêm 4555 exemplos sem etiqueta. Cerca de 300 exemplos são tweets não queixosos. A nossa tarefa é identificar os seus id e verificar manualmente se os nossos resultados estão correctos.
Em [0]:

# Load test data
test_data = pd.read_csv('data/test_data.csv')
# Keep important columns
test_data = test_data[['id', 'tweet']]
# Display 5 samples from the test data
test_data.sample(5)

Saída[0]:

id tweet
1539 59336 Voo da @AmericanAir atrasado mais de 2 horas por n...
607 24101 @SouthwestAir Continuo a receber esta mensagem de erro...
333 13179 à espera no #SeaTac para embarcar no meu voo @JetBlue...
2696 102948 Odeio quando passo pelo processo de seleção antecipada de lugares...
3585 135638 que vergonha @AlaskaAir

3. Criar uma GPU para a formação

O Google Colab oferece GPUs e TPUs gratuitas. Uma vez que vamos treinar uma rede neural grande, é melhor utilizar estas funcionalidades.

É possível adicionar uma GPU indo ao menu e seleccionando:

Tempo de execução -> Alterar tipo de tempo de execução -> Acelerador de hardware: GPU

Em seguida, precisamos de executar a seguinte célula para especificar a GPU como o dispositivo.
Em [0]:

import torch
if torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'There are {torch.cuda.device_count()} GPU(s) available.')
    print('Device name:', torch.cuda.get_device_name(0))
else:
    print('No GPU available, using the CPU instead.')
    device = torch.device("cpu")
Há 1 GPU(s) disponível(is).
Nome do dispositivo: Tesla T4

C - Linha de base: TF-IDF + Classificador Naive Bayes

Nesta abordagem de base, primeiro utilizamos o TF-IDF para vetorizar os nossos dados de texto. De seguida, utilizamos o modelo Naive Bayes como classificador.

Porquê o Naive Bayse? Experimentei diferentes algoritmos de aprendizagem automática, incluindo Random Forest, Support Vectors Machine, XGBoost e observei que o Naive Bayes apresenta o melhor desempenho. Em Guia do Scikit-learn para escolher o estimador correto, também se sugere que o Naive Bayes deve ser utilizado para dados de texto. Também tentei utilizar o SVD para reduzir a dimensionalidade; no entanto, não obteve um melhor desempenho.

1. Preparação dos dados

1.1. Pré-processamento

No modelo de saco de palavras, um texto é representado como o saco das suas palavras, sem ter em conta a gramática e a ordem das palavras. Por isso, queremos remover palavras de paragem, pontuações e caracteres que não contribuem muito para o significado da frase.
Em [0]:

import nltk
# Uncomment to download "stopwords"
nltk.download("stopwords")
from nltk.corpus import stopwords
def text_preprocessing(s):
    """
    - Lowercase the sentence
    - Change "'t" to "not"
    - Remove "@name"
    - Isolate and remove punctuations except "?"
    - Remove other special characters
    - Remove stop words except "not" and "can"
    - Remove trailing whitespace
    """
    s = s.lower()
    # Change 't to 'not'
    s = re.sub(r"'t", " not", s)
    # Remove @name
    s = re.sub(r'(@.*?)[s]', ' ', s)
    # Isolate and remove punctuations except '?'
    s = re.sub(r'(['".()!?\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ', s)
    # Remove some special characters
    s = re.sub(r'([;:|•«n])', ' ', s)
    # Remove stopwords except 'not' and 'can'
    s = " ".join([word for word in s.split()
                  if word not in stopwords.words('english')
                  or word in ['not', 'can']])
    # Remove trailing whitespace
    s = re.sub(r's+', ' ', s).strip()
    return s
[nltk_data] Baixando o pacote stopwords para /root/nltk_data...
[nltk_data] O pacote stopwords já está atualizado!

1.2. Vectorização TF-IDF

Na recuperação de informação, TF-IDF, abreviatura de frequência de termos - frequência inversa de documentosé uma estatística numérica que se destina a refletir a importância de uma palavra para um documento numa coleção ou corpus. Utilizaremos o TF-IDF para vetorizar os nossos dados de texto antes de os alimentarmos com algoritmos de aprendizagem automática.
Em [0]:

%%time
from sklearn.feature_extraction.text import TfidfVectorizer
# Preprocess text
X_train_preprocessed = np.array([text_preprocessing(text) for text in X_train])
X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val])
# Calculate TF-IDF
tf_idf = TfidfVectorizer(ngram_range=(1, 3),
                         binary=True,
                         smooth_idf=False)
X_train_tfidf = tf_idf.fit_transform(X_train_preprocessed)
X_val_tfidf = tf_idf.transform(X_val_preprocessed)
Tempos de CPU: utilizador 5.47 s, sys: 519 ms, total: 5,99 s
Tempo de parede: 6 s

2. Treinar o classificador Naive Bayes

2.1. Afinação de hiperparâmetros

Utilizaremos a validação cruzada e a pontuação AUC para ajustar os hiperparâmetros do nosso modelo. A função get_auc_CV devolverá a pontuação AUC média da validação cruzada.
Em [0]:

from sklearn.model_selection import StratifiedKFold, cross_val_score
def get_auc_CV(model):
    """
    Return the average AUC score from cross-validation.
    """
    # Set KFold to shuffle data before the split
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # Get AUC scores
    auc = cross_val_score(
        model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf)
    return auc.mean()

O MultinominalNB têm apenas um hipterparâmetro - alfa. O código abaixo ajudar-nos-á a encontrar o valor alfa que nos dá a pontuação CV AUC mais elevada.
Em [0]:

from sklearn.naive_bayes import MultinomialNB
res = pd.Series([get_auc_CV(MultinomialNB(i))
                 for i in np.arange(1, 10, 0.1)],
                index=np.arange(1, 10, 0.1))
best_alpha = np.round(res.idxmax(), 2)
print('Best alpha: ', best_alpha)
plt.plot(res)
plt.title('AUC vs. Alpha')
plt.xlabel('Alpha')
plt.ylabel('AUC')
plt.show()
Melhor alfa:  1.3

2.2. Avaliação no conjunto de validação

Para avaliar o desempenho do nosso modelo, calcularemos a taxa de precisão e a pontuação AUC do nosso modelo no conjunto de validação.
Em [0]:

from sklearn.metrics import accuracy_score, roc_curve, auc
def evaluate_roc(probs, y_true):
    """
    - Print AUC and accuracy on the test set
    - Plot ROC
    @params    probs (np.array): an array of predicted probabilities with shape (len(y_true), 2)
    @params    y_true (np.array): an array of the true values with shape (len(y_true),)
    """
    preds = probs[:, 1]
    fpr, tpr, threshold = roc_curve(y_true, preds)
    roc_auc = auc(fpr, tpr)
    print(f'AUC: {roc_auc:.4f}')
    # Get accuracy over the test set
    y_pred = np.where(preds >= 0.5, 1, 0)
    accuracy = accuracy_score(y_true, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # Plot ROC AUC
    plt.title('Receiver Operating Characteristic')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = 'lower right')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')
    plt.show()

Combinando o TF-IDF e o algoritmo Naive Bayes, atingimos uma taxa de precisão de 72.65% no conjunto de validação. Este valor é o desempenho de base e será utilizado para avaliar o desempenho do nosso modelo BERT de afinação fina.
Em [0]:

# Compute predicted probabilities
nb_model = MultinomialNB(alpha=1.8)
nb_model.fit(X_train_tfidf, y_train)
probs = nb_model.predict_proba(X_val_tfidf)
# Evaluate the classifier
evaluate_roc(probs, y_val)
AUC: 0.8451
Precisão: 75,59%

D - Afinação do BERT

1. Instalar a biblioteca Hugging Face

A biblioteca de transformadores do Hugging Face contém a implementação PyTorch de modelos de PNL de última geração, incluindo BERT (da Google), GPT (da OpenAI) ... e pesos de modelos pré-treinados.
Em [1]:

#!pip instalar transformadores

2. Tokenização e formatação de entrada

Antes de tokenizar o nosso texto, vamos efetuar um ligeiro processamento no nosso texto, incluindo a remoção de menções a entidades (por exemplo, @united) e alguns caracteres especiais. O nível de processamento aqui é muito menor do que nas abordagens anteriores porque o BERT foi treinado com as frases inteiras.
Em [0]:

def text_preprocessing(text):
    """
    - Remove entity mentions (eg. '@united')
    - Correct errors (eg. '&' to '&')
    @param    text (str): a string to be processed.
    @return   text (Str): the processed string.
    """
    # Remove '@name'
    text = re.sub(r'(@.*?)[s]', ' ', text)
    # Replace '&' with '&'
    text = re.sub(r'&', '&', text)
    # Remove trailing whitespace
    text = re.sub(r's+', ' ', text).strip()
    return text

Em [0]:

# Print sentence 0
print('Original: ', X[0])
print('Processed: ', text_preprocessing(X[0]))
Original:  @united I'estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo fazer o login e o check-in. Podem ajudar-me?
Processado:  Estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo'fazer o login e o check-in. Podem ajudar-me?

2.1. Tokenizador BERT

Para aplicar o BERT pré-treinado, temos de utilizar o tokenizador fornecido pela biblioteca. Isto deve-se ao facto de (1) o modelo ter um vocabulário específico e fixo e (2) o tokenizador do BERT ter uma forma particular de lidar com palavras fora do vocabulário.

Além disso, temos de acrescentar símbolos especiais no início e no fim de cada frase, preencher e truncar todas as frases para um comprimento único e constante e especificar explicitamente o que são símbolos de preenchimento com a "máscara de atenção".

O codificar_mais do tokenizador BERT:

(1) dividir o nosso texto em tokens,

(2) acrescentar a menção especial [CLS] e [SEP] fichas, e

(3) converter esses tokens em índices do vocabulário do tokenizador,

(4) preencher ou truncar frases até ao comprimento máximo, e

(5) criar uma máscara de atenção.
Em [0]:

from transformers import BertTokenizer
# Load the BERT tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# Create a function to tokenize a set of texts
def preprocessing_for_bert(data):
    """Perform required preprocessing steps for pretrained BERT.
    @param    data (np.array): Array of texts to be processed.
    @return   input_ids (torch.Tensor): Tensor of token ids to be fed to a model.
    @return   attention_masks (torch.Tensor): Tensor of indices specifying which
                  tokens should be attended to by the model.
    """
    # Create empty lists to store outputs
    input_ids = []
    attention_masks = []
    # For every sentence...
    for sent in data:
        # codificar_mais will:
        #    (1) Tokenize the sentence
        #    (2) Add the [CLS] e [SEP] token to the start and end
        #    (3) Truncate/Pad sentence to max length
        #    (4) Map tokens to their IDs
        #    (5) Create attention mask
        #    (6) Return a dictionary of outputs
        encoded_sent = tokenizer.encode_plus(
            text=text_preprocessing(sent),  # Preprocess sentence
            add_special_tokens=True,        # Add [CLS] e [SEP]
            max_length=MAX_LEN,                  # Max length to truncate/pad
            pad_to_max_length=True,         # Pad sentence to max length
            #return_tensors='pt',           # Return PyTorch tensor
            return_attention_mask=True      # Return attention mask
            )
        # Add the outputs to the lists
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # Convert lists to tensors
    input_ids = torch.tensor(input_ids)
    attention_masks = torch.tensor(attention_masks)
    return input_ids, attention_masks

Antes da tokenização, precisamos de especificar o comprimento máximo das nossas frases.
Em [0]:

# Concatenate train data and test data
all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values])
# Encode our concatenated data
encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]
# Find the maximum length
max_len = max([len(sent) for sent in encoded_tweets])
print('Max length: ', max_len)
Comprimento máximo: 68

Agora vamos tokenizar os nossos dados.
Em [0]:

# Specify MAX_LEN
MAX_LEN = 64
# Print sentence 0 and its encoded token ids
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('Original: ', X[0])
print('Token IDs: ', token_ids)
# Run function pré-processamento_para_bert on the train set and the validation set
print('Tokenizing data...')
train_inputs, train_masks = preprocessing_for_bert(X_train)
val_inputs, val_masks = preprocessing_for_bert(X_val)
Original:  @united I'estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo fazer o login e o check-in. Podem ajudar-me?
IDs de token:  [101, 1045, 1005, 1049, 2383, 3314, 1012, 7483, 1045, 2128, 8654, 2098, 2005, 2484, 2847, 2044, 1045, 2001, 4011, 2000, 4875, 1010, 2085, 1045, 2064, 1005, 1056, 8833, 2006, 1004, 4638, 1999, 1012, 2064, 2017, 2393, 1029, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Tokenização de dados...

2.2. Criar DataLoader PyTorch

Vamos criar um iterador para o nosso conjunto de dados utilizando a classe DataLoader do torch. Isto ajudará a poupar memória durante o treino e aumentará a velocidade de treino.
Em [0]:

from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
# Convert other data types to torch.Tensor
train_labels = torch.tensor(y_train)
val_labels = torch.tensor(y_val)
# For fine-tuning BERT, the authors recommend a batch size of 16 or 32.
batch_size = 32
# Create the DataLoader for our training set
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
# Create the DataLoader for our validation set
val_data = TensorDataset(val_inputs, val_masks, val_labels)
val_sampler = SequentialSampler(val_data)
val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=batch_size)

3. Treinar o nosso modelo

3.1. Criar BertClassifier

O BERT-base é composto por 12 camadas transformadoras, cada uma das quais recebe uma lista de token embeddings e produz o mesmo número de embeddings com o mesmo tamanho (ou dimensões) oculto na saída. A saída da camada de transformação final da [CLS] é utilizado como caraterística da sequência para alimentar um classificador.

O transformadores biblioteca tem o BertForSequenceClassification que foi concebida para tarefas de classificação. No entanto, vamos criar uma nova classe para podermos especificar a nossa própria escolha de classificadores.

De seguida, vamos criar uma classe BertClassifier com um modelo BERT para extrair a última camada oculta do [CLS] e uma rede neural feed-forward de camada única oculta como classificador.
Em [0]:

%%time
import torch
import torch.nn as nn
from transformers import BertModel
# Create the BertClassfier class
class BertClassifier(nn.Module):
    """Bert Model for Classification Tasks.
    """
    def __init__(self, freeze_bert=False):
        """
        @param    bert: a BertModel object
        @param    classifier: a torch.nn.Module classifier
        @param    freeze_bert (bool): Set Falso to fine-tune the BERT model
        """
        super(BertClassifier, self).__init__()
        # Specify hidden size of BERT, hidden size of our classifier, and number of labels
        D_in, H, D_out = 768, 50, 2
        # Instantiate BERT model
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # Instantiate an one-layer feed-forward classifier
        self.classifier = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Linear(H, D_out)
        )
        # Freeze the BERT model
        if freeze_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        Feed input to BERT and the classifier to compute logits.
        @param    input_ids (torch.Tensor): an input tensor with shape (batch_size,
                      max_length)
        @param    attention_mask (torch.Tensor): a tensor that hold attention mask
                      information with shape (batch_size, max_length)
        @return   logits (torch.Tensor): an output tensor with shape (batch_size,
                      num_labels)
        """
        # Feed input to BERT
        outputs = self.bert(input_ids=input_ids,
                            attention_mask=attention_mask)
        # Extract the last hidden state of the token [CLS] for classification task
        last_hidden_state_cls = outputs[0][:, 0, :]
        # Feed input to classifier to compute logits
        logits = self.classifier(last_hidden_state_cls)
        return logits
Tempos de CPU: utilizador 38 µs, sys: 0 ns, total: 38 µs
Tempo de parede: 40,1 µs

3.2. Optimizador e programador da taxa de aprendizagem

Para afinar o nosso classificador Bert, precisamos de criar um optimizador. Os autores recomendam os seguintes hiper-parâmetros:

  • Tamanho do lote: 16 ou 32
  • Taxa de aprendizagem (Adam): 5e-5, 3e-5 ou 2e-5
  • Número de épocas: 2, 3, 4

Huggingface forneceu a executar_cola.py um exemplo de implementação do script transformadores biblioteca. No script, é utilizado o optimizador AdamW.
Em [0]:

from transformers import AdamW, get_linear_schedule_with_warmup
def initialize_model(epochs=4):
    """Initialize the Bert Classifier, the optimizer and the learning rate scheduler.
    """
    # Instantiate Bert Classifier
    bert_classifier = BertClassifier(freeze_bert=False)
    # Tell PyTorch to run the model on GPU
    bert_classifier.to(device)
    # Create the optimizer
    optimizer = AdamW(bert_classifier.parameters(),
                      lr=5e-5,    # Default learning rate
                      eps=1e-8    # Default epsilon value
                      )
    # Total number of training steps
    total_steps = len(train_dataloader) * epochs
    # Set up the learning rate scheduler
    scheduler = get_linear_schedule_with_warmup(optimizer,
                                                num_warmup_steps=0, # Default value
                                                num_training_steps=total_steps)
    return bert_classifier, optimizer, scheduler

3.3. Circuito de formação

Treinaremos o nosso classificador Bert durante 4 épocas. Em cada época, treinaremos o nosso modelo e avaliaremos o seu desempenho no conjunto de validação. Em mais pormenor, iremos:

Formação:

  • Desempacotar os nossos dados do carregador de dados e carregá-los na GPU
  • Zerar os gradientes calculados na passagem anterior
  • Efetuar uma passagem para a frente para calcular logits e perdas
  • Efetuar uma passagem para trás para calcular gradientes (perda.para trás())
  • Recorte a norma dos gradientes para 1,0 para evitar "gradientes explosivos"
  • Atualizar os parâmetros do modelo (optimizador.passo())
  • Atualizar a taxa de aprendizagem (programador.step())

Avaliação:

  • Descompactar os nossos dados e carregá-los na GPU
  • Passe para a frente
  • Calcular a perda e a taxa de precisão no conjunto de validação

O guião abaixo é comentado com os detalhes do nosso ciclo de formação e avaliação.
Em [0]:

import random
import time
# Specify loss function
loss_fn = nn.CrossEntropyLoss()
def set_seed(seed_value=42):
    """Set seed for reproducibility.
    """
    random.seed(seed_value)
    np.random.seed(seed_value)
    torch.manual_seed(seed_value)
    torch.cuda.manual_seed_all(seed_value)
def train(model, train_dataloader, val_dataloader=None, epochs=4, evaluation=False):
    """Train the BertClassifier model.
    """
    # Start training loop
    print("Start training...n")
    for epoch_i in range(epochs):
        # =======================================
        #               Training
        # =======================================
        # Print the header of the result table
        print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Val Acc':^9} | {'Elapsed':^9}")
        print("-"*70)
        # Measure the elapsed time of each epoch
        t0_epoch, t0_batch = time.time(), time.time()
        # Reset tracking variables at the beginning of each epoch
        total_loss, batch_loss, batch_counts = 0, 0, 0
        # Put the model into the training mode
        model.train()
        # For each batch of training data...
        for step, batch in enumerate(train_dataloader):
            batch_counts +=1
            # Load batch to GPU
            b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
            # Zero out any previously calculated gradients
            model.zero_grad()
            # Perform a forward pass. This will return logits.
            logits = model(b_input_ids, b_attn_mask)
            # Compute loss and accumulate the loss values
            loss = loss_fn(logits, b_labels)
            batch_loss += loss.item()
            total_loss += loss.item()
            # Perform a backward pass to calculate gradients
            loss.backward()
            # Clip the norm of the gradients to 1.0 to prevent "exploding gradients"
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            # Update parameters and the learning rate
            optimizer.step()
            scheduler.step()
            # Print the loss values and time elapsed for every 20 batches
            if (step % 20 == 0 and step != 0) or (step == len(train_dataloader) - 1):
                # Calculate time elapsed for 20 batches
                time_elapsed = time.time() - t0_batch
                # Print training results
                print(f"{epoch_i + 1:^7} | {step:^7} | {batch_loss / batch_counts:^12.6f} | {'-':^10} | {'-':^9} | {time_elapsed:^9.2f}")
                # Reset batch tracking variables
                batch_loss, batch_counts = 0, 0
                t0_batch = time.time()
        # Calculate the average loss over the entire training data
        avg_train_loss = total_loss / len(train_dataloader)
        print("-"*70)
        # =======================================
        #               Evaluation
        # =======================================
        if evaluation == True:
            # After the completion of each training epoch, measure the model's performance
            # on our validation set.
            val_loss, val_accuracy = evaluate(model, val_dataloader)
            # Print performance over the entire training data
            time_elapsed = time.time() - t0_epoch
            print(f"{epoch_i + 1:^7} | {'-':^7} | {avg_train_loss:^12.6f} | {val_loss:^10.6f} | {val_accuracy:^9.2f} | {time_elapsed:^9.2f}")
            print("-"*70)
        print("n")
    print("Training complete!")
def evaluate(model, val_dataloader):
    """After the completion of each training epoch, measure the model's performance
    on our validation set.
    """
    # Put the model into the evaluation mode. The dropout layers are disabled during
    # the test time.
    model.eval()
    # Tracking variables
    val_accuracy = []
    val_loss = []
    # For each batch in our validation set...
    for batch in val_dataloader:
        # Load batch to GPU
        b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
        # Compute logits
        with torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        # Compute loss
        loss = loss_fn(logits, b_labels)
        val_loss.append(loss.item())
        # Get the predictions
        preds = torch.argmax(logits, dim=1).flatten()
        # Calculate the accuracy rate
        accuracy = (preds == b_labels).cpu().numpy().mean() * 100
        val_accuracy.append(accuracy)
    # Compute the average accuracy and loss over the validation set.
    val_loss = np.mean(val_loss)
    val_accuracy = np.mean(val_accuracy)
    return val_loss, val_accuracy

Agora, vamos começar a treinar o nosso BertClassifier!
Em [0]:

set_seed(42)    # Set seed for reproducibility
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)

Iniciar a formação...

 Época | Lote | Perda de trem | Perda de valor | Acerto de valor | Decorrido
----------------------------------------------------------------------
   1 | 20 | 0.630467 | - | - | 7.58
   1 | 40 | 0.497330 | - | - | 7.01
   1 | 60 | 0.502320 | - | - | 7.11
   1 | 80 | 0.491438 | - | - | 7.19
   1 | 95 | 0.486125 | - | - | 5.35
----------------------------------------------------------------------
   1 | - | 0.524515 | 0.439601 | 78.81 | 35.54
----------------------------------------------------------------------


 Época | Lote | Perda de comboio | Perda de valor | Acerto de valor | Decorrido
----------------------------------------------------------------------
   2 | 20 | 0.287401 | - | - | 7.83
   2 | 40 | 0.260870 | - | - | 7.60
   2 | 60 | 0.287706 | - | - | 7.67
   2 | 80 | 0.283311 | - | - | 7.87
   2 | 95 | 0.280315 | - | - | 5.87
----------------------------------------------------------------------
   2 | - | 0.279978 | 0.454067 | 80.40 | 38.31
----------------------------------------------------------------------


Formação concluída!

3.4. Avaliação no conjunto de validação

O passo de previsão é semelhante ao passo de avaliação que efectuámos no ciclo de formação, mas mais simples. Vamos efetuar uma passagem para a frente para calcular logits e aplicar softmax para calcular probabilidades.
Em [0]:

import torch.nn.functional as F
def bert_predict(model, test_dataloader):
    """Perform a forward pass on the trained BERT model to predict probabilities
    on the test set.
    """
    # Put the model into the evaluation mode. The dropout layers are disabled during
    # the test time.
    model.eval()
    all_logits = []
    # For each batch in our test set...
    for batch in test_dataloader:
        # Load batch to GPU
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]
        # Compute logits
        with torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # Concatenate logits from each batch
    all_logits = torch.cat(all_logits, dim=0)
    # Apply softmax to calculate probabilities
    probs = F.softmax(all_logits, dim=1).cpu().numpy()
    return probs

Em [0]:

# Compute predicted probabilities on the test set
probs = bert_predict(bert_classifier, val_dataloader)
# Evaluate the Bert classifier
evaluate_roc(probs, y_val)
AUC: 0.9048
Precisão: 80,59%


O Bert Classifer atinge uma pontuação AUC de 0,90 e uma taxa de precisão de 82,65% no conjunto de validação. Este resultado é 10 pontos melhor do que o método de base.

3.5. Treinar o nosso modelo em todos os dados de treino

Em [0]:

# Concatenate the train set and the validation set
full_train_data = torch.utils.data.ConcatDataset([train_data, val_data])
full_train_sampler = RandomSampler(full_train_data)
full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32)
# Train the Bert Classifier on the entire training data
set_seed(42)
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, full_train_dataloader, epochs=2)
Iniciar a formação...

 Época | Lote | Perda de trem | Perda de valor | Acerto de valor | Decorrido
----------------------------------------------------------------------
   1 | 20 | 0.664452 | - | - | 8.63
   1 | 40 | 0.587205 | - | - | 8.42
   1 | 60 | 0.522831 | - | - | 8.44
   1 | 80 | 0.476442 | - | - | 8.23
   1 | 100 | 0.467542 | - | - | 8.10
   1 | 106 | 0.483039 | - | - | 2.14
----------------------------------------------------------------------


 Época | Lote | Perda de comboio | Perda de valor | Acerto de valor | Decorrido
----------------------------------------------------------------------
   2 | 20 | 0.338174 | - | - | 8.36
   2 | 40 | 0.296080 | - | - | 7.93
   2 | 60 | 0.295626 | - | - | 7.96
   2 | 80 | 0.277470 | - | - | 7.99
   2 | 100 | 0.314746 | - | - | 8.07
   2 | 106 | 0.293359 | - | - | 2.17
----------------------------------------------------------------------


Formação concluída!

4. Previsões no conjunto de teste

4.1. Preparação dos dados

Vamos rever o nosso conjunto de testes em breve.
Em [0]:

test_data.sample(5)

Saída[0]:

id tweet
471 18654 Amigos e familiares: Nunca voem na @JetBlue. Absolutamente...
1971 76265 @DeltaAssist @rogerioad Nunca tive um profissional...
23 672 Primeiro voo em semanas. Estou a contar convosco @Americ...
2702 103263 "@USAirways: Vocês sabem que nós não podemos ficar no m...
135 5137 @southwestair Aqui no Aeroporto SA a ver o ...

Antes de fazer previsões no conjunto de teste, precisamos de refazer os passos de processamento e codificação efectuados nos dados de treino. Felizmente, escrevemos o pré-processamento_para_bert para o fazer por nós.
Em [0]:

# Run pré-processamento_para_bert on the test set
print('Tokenizing data...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Create the DataLoader for our test set
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
Tokenização de dados...

4.2. Previsões

Existem cerca de 300 tweets não negativos no nosso conjunto de teste. Por conseguinte, continuaremos a ajustar o limiar de decisão até termos cerca de 300 tweets não negativos.

O limite que usaremos é 0,992, o que significa que os tweets com uma probabilidade prevista maior que 99,2% serão considerados positivos. Esse valor é muito alto em comparação com o limite padrão de 0,5.

Depois de examinar manualmente o conjunto de testes, verifico que a tarefa de classificação do sentimento é difícil até para o ser humano. Por conseguinte, um limiar elevado permite-nos fazer previsões seguras.
Em [0]:

# Compute predicted probabilities on the test set
probs = bert_predict(bert_classifier, test_dataloader)
# Get predictions from the probabilities
threshold = 0.9
preds = np.where(probs[:, 1] > threshold, 1, 0)
# Number of tweets predicted non-negative
print("Number of tweets predicted non-negative: ", preds.sum())
Número de tweets com previsão de não-negatividade: 454

Agora vamos examinar 20 tweets aleatórios das nossas previsões. 17 deles estão correctos, mostrando que o classificador BERT obtém uma taxa de precisão de cerca de 0,85.
Em [0]:

output = test_data[preds==1]
list(output.sample(20).tweet)

Saída[0]:

["@Delta @DeltaAssist A Delta volta a atacar. Sky lounge no aeroporto mais movimentado do país'fechado aos fins-de-semana. Estúpido e barato. saudades vossas @united",
 '.@SouthwestAir trouxe de volta os amendoins torrados com mel. É triste que esta constatação possa ser o ponto alto do meu dia?#SmallThingsInLife',
 '@DeltaAssist Enviei um e-mail para kana@delta e contactus.delta@delta para resolver problemas há duas semanas sem resposta. conselhos sobre quem contactar? ',
 "Mulher expulsa do voo pela @AlaskaAir porque tem cancro de #C planeia doar a passagem aérea da sua família'http://t.co/Uj6rispWLb",
 "@united (2/2) Eu não'parti o saco. Se não tivesse de pagar para a despachar, não estaria tão aborrecido. Prefiro voar na @AmericanAir @SouthwestAir etc",
 "Já voei em quase todas as companhias aéreas e nunca tive uma experiência melhor do que voar na @JetBlue. Qualidade, serviço, conforto e preço acessível. A++",
 '@JetBlue A melhor companhia aérea para se trabalhar sinto muito a sua falta #keepingitminty ',
 'Convenceu @firetweet a reservar uma viagem de última hora para se juntar a mim em Austin! Desde então, tenho cantado a canção de segurança da @VirginAmerica. Pobre Eric... ',
 '@AmericanAir espera pacientemente para descolar de #DFW para #ord http://t.co/j1oDSc6fht',
 'Oh @JetBlue hoje é um dia triste para os fiéis à B6. Sei que estão a promover as vossas novas "opções", mas as vossas taxas de serviço/sem bagagem SÃO o que nos torna excelentes',
 'Coisas boas deste voo: @Gogo e a óptima experiência da @VirginAmerica. Coisas menos boas: o cheiro a vomitado de bebé/atum podre',
 '@USAirways @AmericanAir vai sentir falta da USAir :(',
 '@altonbrown @united Está na altura de mudar para a @VirginAmerica',
 'Nunca é a altura errada para Chobani, @AmericanAir Admirals Club! #brokenrecord #toomanywasabipeas #lunch',
 "No meu voo, roubei o telemóvel do meu humano&#39 para experimentar o novo streaming IFE da @alaskaair&#39. É bom! É pena que ela não tenha um iPad",
 "Mal posso esperar pela conclusão da fusão entre a @USAirways e a @AmericanAir, que chatice para o cliente!",
 "@JetBlue I'm a broke college kid so $150 is a huge deal.",
 "Mal posso esperar para voar de volta para a Bay Area esta noite no voo 2256 da @SouthwestAir!!!!",
 'Pendurado em #SFO à espera que o nevoeiro se dissipe para a próxima ligação @VirginAmerica para #sxsw! #SXSW2015 #Austin',
 "@DeltaAssist, como posso mudar de voo do 1308 para um que não esteja indefinidamente atrasado.... e regressar a dc!"].

E - Conclusão

Adicionando um classificador simples de rede neural de uma camada oculta ao BERT e afinando o BERT, podemos alcançar um desempenho próximo do estado da arte, que é 10 pontos melhor do que o método de base, embora só tenhamos 3400 pontos de dados.

Além disso, embora o BERT seja muito grande, complicado e tenha milhões de parâmetros, só precisamos de o afinar em apenas 2-4 épocas. Este resultado pode ser alcançado porque o BERT foi treinado numa quantidade enorme e já codifica muita informação sobre a nossa língua. Um desempenho impressionante alcançado num curto espaço de tempo, com uma pequena quantidade de dados, mostrou porque é que o BERT é um dos mais poderosos modelos de PNL disponíveis neste momento.

Let’s Discuss Your Idea

    Related Posts

    Ready To Supercharge Your Business

    LET’S
    TALK
    pt_PTPortuguês