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

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

    Originalmente publicado por Chris Tran, investigador de aprendizagem automática da Skim AI.



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

importar os
importar 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]:

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

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

 # Carregar dados e definir etiquetas
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
# Concatenar dados de reclamação e de não reclamação
dados = pd.concat([dados_reclamação, dados_não_reclamação], eixo=0).reset_index(drop=True)
# Drop 'companhia aérea' coluna
data.drop(['companhia aérea'], inplace=True, axis=1)
# Apresentar 5 amostras aleatórias
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 = dados.tweet.values
y = dados.label.values
X_treinamento, X_val, y_treinamento, 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]:

# Dados do teste de carga
dados_teste = pd.read_csv('dados/teste_dados.csv')
# Manter colunas importantes
dados_teste = dados_teste[['id', 'tweet']]
# Apresentar 5 amostras dos dados de teste
dados_teste.amostra(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]:

importar torch
se torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'Existem {torch.cuda.device_count()} GPU(s) disponíveis.')
    print('Nome do dispositivo:', torch.cuda.get_device_name(0))
senão:
    print('Nenhuma GPU disponível, usando a CPU em seu lugar.')
    dispositivo = 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]:

importar nltk
# Descomente para descarregar "stopwords"
nltk.download("stopwords")
from nltk.corpus import stopwords
def text_preprocessing(s):
    """
    - Reduzir a frase a minúsculas
    - Alterar "'t" para "não"
    - Remover "@nome"
    - Isolar e remover as pontuações, exceto "?"
    - Remover outros caracteres especiais
    - Remover palavras de paragem, exceto "not" e "can"
    - Remover os espaços em branco no final
    """
    s = s.lower()
    # Alterar 't para 'not'
    s = re.sub(r"'t", " not", s)
    # Remover @nome
    s = re.sub(r'(@.*?)[s]', ' ' ', s)
    # Isolar e remover as pontuações, exceto '?'
    s = re.sub(r'(['".()!?\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ' ', s)
    # Remover alguns caracteres especiais
    s = re.sub(r'([;:|-"n])', ' ' ', s)
    # Remover as stopwords exceto 'not' e 'can'
    s = " ".join([palavra for palavra in s.split()
                  se palavra não estiver em stopwords.words('english')
                  ou palavra em ['not', 'can']])
    # Remover espaços em branco à direita
    s = re.sub(r's+', ' ' ', s).strip()
    retornar 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
# Pré-processar texto
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])
# Calcular 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):
    """
    Devolve a pontuação AUC média da validação cruzada.
    """
    # Definir KFold para baralhar os dados antes da divisão
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # Obter pontuações AUC
    auc = cross_val_score(
        modelo, X_treino_tfidf, y_treino, pontuação="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]:

de 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))
melhor_alfa = np.round(res.idxmax(), 2)
print('Melhor alfa: ', melhor_alfa)
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):
    """
    - Imprimir AUC e exatidão no conjunto de teste
    - Traçar ROC
    @params probs (np.array): um conjunto de probabilidades previstas com a forma (len(y_true), 2)
    @params y_true (np.array): uma matriz dos valores verdadeiros com forma (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}')
    # Obter a exatidão do conjunto de teste
    y_pred = np.where(preds >= 0.5, 1, 0)
    exatidão = exactidão_pontuação(y_verdadeiro, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # Plotar ROC AUC
    plt.title('Característica de funcionamento do recetor')
    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('Taxa de Verdadeiros Positivos')
    plt.xlabel('Taxa de falsos positivos')
    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]:

# Calcular probabilidades previstas
nb_model = MultinomialNB(alpha=1.8)
nb_model.fit(X_train_tfidf, y_train)
probs = nb_model.predict_proba(X_val_tfidf)
# Avaliar o classificador
avaliar_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):
    """
    - Remover menções de entidades (ex.: '@united')
    - Corrigir erros (por exemplo, '&' para '&')
    @param text (str): uma cadeia de caracteres a ser processada.
    @retorno text (Str): a cadeia de caracteres processada.
    """
    # Remove '@nome'
    text = re.sub(r'(@.*?)[s]', ' ' ', text)
    # Substituir '&' por '&'
    texto = re.sub(r'&', '&', texto)
    # Remover os espaços em branco à direita
    text = re.sub(r's+', ' ' ', text).strip()
    devolver texto

Em [0]:

# Imprimir frase 0
print('Original: ', X[0])
print('Processado: ', 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
# Carrega o tokenizador BERT
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# Criar uma função para tokenizar um conjunto de textos
def preprocessing_for_bert(data):
    """Executa os passos de pré-processamento necessários para o BERT pré-treinado.
    @param dados (np.array): Conjunto de textos a serem processados.
    @return input_ids (torch.Tensor): Tensor de ids de tokens a serem alimentados para um modelo.
    @return attention_masks (torch.Tensor): Tensor de índices que especificam quais
                  tokens devem ser atendidos pelo modelo.
    """
    # Criar listas vazias para armazenar as saídas
    input_ids = []
    máscaras_de_atenção = []
    # Para cada frase...
    para enviada nos dados:
        # codificar_mais vai:
        # (1) Tokenizar a frase
        # (2) Acrescentar o [CLS] e [SEP] ficha no início e no fim
        # (3) Truncar/encaminhar a frase para o comprimento máximo
        # (4) Mapear os tokens para os seus IDs
        # (5) Criar máscara de atenção
        # (6) Devolver um dicionário de resultados
        codificado_sent = tokenizer.encode_plus(
            text=text_preprocessing(sent), # Pré-processar frase
            add_special_tokens=True, # Adicionar [CLS] e [SEP]
            max_length=MAX_LEN, # Comprimento máximo para truncar/colocar
            pad_to_max_length=True, # Comprimento máximo da frase
            #return_tensors='pt', # Devolver tensor PyTorch
            return_attention_mask=True # Devolver máscara de atenção
            )
        # Acrescentar os outputs às listas
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # Converter listas em tensores
    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]:

# Concatenar dados de treino e dados de teste
todos_tweets = np.concatenate([dados.tweet.valores, dados_teste.tweet.valores])
# Codificar os nossos dados concatenados
tweets_codificados = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]
# Encontrar o comprimento máximo
max_len = max([len(sent) for sent in encoded_tweets])
print('Comprimento máximo: ', max_len)
Comprimento máximo: 68

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

# Especificar MAX_LEN
MAX_LEN = 64
# Imprimir a frase 0 e as suas identificações simbólicas codificadas
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('Original: ', X[0])
print('Token IDs: ', token_ids)
# Função de execução pré-processamento_para_bert no conjunto de treino e no conjunto de validação
print('Tokenizar dados...')
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
# Converter outros tipos de dados para torch.Tensor
train_labels = torch.tensor(y_train)
val_labels = torch.tensor(y_val)
# Para um ajuste fino do BERT, os autores recomendam um tamanho de lote de 16 ou 32.
tamanho_do_lote = 32
# Criar o DataLoader para o nosso conjunto de treino
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)
# Criar o DataLoader para o nosso conjunto de validação
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
importar torch
import torch.nn as nn
from transformers import BertModel
# Criar a classe BertClassfier
class BertClassifier(nn.Module):
    """Modelo Bert para tarefas de classificação.
    """
    def __init__(self, freeze_bert=False):
        """
        @param bert: um objeto BertModel
        @param classificador: um classificador torch.nn.Module
        @param freeze_bert (bool): Definir Falso para afinar o modelo BERT
        """
        super(BertClassifier, self).__init__()
        # Especificar o tamanho oculto do BERT, o tamanho oculto do nosso classificador e o número de etiquetas
        D_in, H, D_out = 768, 50, 2
        # Instanciar o modelo BERT
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # Instanciar um classificador de avanço de uma camada
        self.classifier = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Linear(H, D_out)
        )
        # Congelar o modelo BERT
        if freeze_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        Fornece a entrada ao BERT e ao classificador para calcular os logits.
        @param input_ids (torch.Tensor): um tensor de entrada com forma (batch_size,
                      max_length)
        @param mascara_de_atenção (torch.Tensor): um tensor que contém informações sobre a máscara de
                      com a forma (tamanho_do_lote, comprimento_máximo)
        @return logits (torch.Tensor): um tensor de saída com a forma (batch_size,
                      num_labels)
        """
        # Alimentar o BERT com dados de entrada
        outputs = self.bert(input_ids=input_ids,
                            attention_mask=attention_mask)
        # Extrair o último estado oculto da ficha [CLS] para a tarefa de classificação
        last_hidden_state_cls = outputs[0][:, 0, :]
        # Introduzir a entrada no classificador para calcular 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):
    """Inicializar o Classificador Bert, o optimizador e o programador da taxa de aprendizagem.
    """
    # Instanciar o Classificador Bert
    bert_classificador = BertClassificador(freeze_bert=False)
    # Dizer ao PyTorch para executar o modelo na GPU
    bert_classificador.to(dispositivo)
    # Criar o optimizador
    optimizador = AdamW(bert_classifier.parameters(),
                      lr=5e-5, # Taxa de aprendizagem predefinida
                      eps=1e-8 # Valor epsilon por defeito
                      )
    # Número total de passos de treino
    total_steps = len(train_dataloader) * epochs
    # Configurar o programador da taxa de aprendizagem
    programador = get_linear_schedule_with_warmup(optimizador,
                                                num_warmup_steps=0, # Valor por defeito
                                                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) # Definir semente para reprodutibilidade
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):
    """Efectua uma passagem para a frente no modelo BERT treinado para prever probabilidades
    no conjunto de teste.
    """
    # Colocar o modelo no modo de avaliação. As camadas de abandono são desactivadas durante
    # o tempo de teste.
    model.eval()
    todos_logits = []
    # Para cada lote do nosso conjunto de testes...
    for batch in test_dataloader:
        # Carrega o lote para a GPU
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]
        # Calcular logits
        com torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # Concatenar os logits de cada lote
    todos_logits = torch.cat(todos_logits, dim=0)
    # Aplicar softmax para calcular as probabilidades
    probs = F.softmax(todos_logits, dim=1).cpu().numpy()
    return probs

Em [0]:

# Calcular as probabilidades previstas no conjunto de teste
probs = bert_predict(bert_classifier, val_dataloader)
# Avaliar o classificador de Bert
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]:

# Concatenar o conjunto de treino e o conjunto de validação
dados_treino_completos = torch.utils.data.ConcatDataset([dados_treino, dados_valor])
full_train_sampler = RandomSampler(full_train_data)
full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32)
# Treinar o classificador Bert em todos os dados de treino
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]:

dados_teste.amostra(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]:

# Executar pré-processamento_para_bert no conjunto de teste
print('Tokenizando dados...')
entradas_teste, máscaras_teste = pré-processamento_para_bert(dados_teste.tweet)
# Criar o DataLoader para o nosso conjunto de teste
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]:

# Calcular as probabilidades previstas no conjunto de teste
probs = bert_predict(bert_classifier, test_dataloader)
# Obter previsões a partir das probabilidades
limiar = 0,9
preds = np.where(probs[:, 1] > limiar, 1, 0)
# Número de tweets previstos não negativos
print("Número de tweets previstos como não-negativos: ", 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 = dados_teste[preds==1]
lista(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.

Vamos discutir a sua ideia

    Publicações relacionadas

    Pronto para impulsionar o seu negócio

    VAMOS
    TALK
    pt_PTPortuguês