Tutorial: Messa a punto del BERT per l'analisi dei sentimenti
Tutorial: Messa a punto del BERT per l'analisi dei sentimenti
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
A - Introduzione¶
Negli ultimi anni la comunità NLP ha assistito a molte innovazioni nell'elaborazione del linguaggio naturale, in particolare il passaggio all'apprendimento per trasferimento. Modelli come ELMo, ULMFiT di fast.ai, Transformer e GPT di OpenAI hanno permesso ai ricercatori di raggiungere risultati all'avanguardia su diversi benchmark e hanno fornito alla comunità modelli pre-addestrati di grandi dimensioni con prestazioni elevate. Questo cambiamento in NLP è visto come il momento ImageNet di NLP, un cambiamento avvenuto qualche anno fa nella computer vision, quando gli strati inferiori delle reti di deep learning con milioni di parametri addestrati per un compito specifico possono essere riutilizzati e messi a punto per altri compiti, piuttosto che addestrare nuove reti da zero.
Una delle pietre miliari più importanti nell'evoluzione della PNL è il rilascio del BERT di Google, descritto come l'inizio di una nuova era nella PNL. In questo quaderno utilizzerò il sistema HuggingFace. trasformatori
per mettere a punto il modello BERT preaddestrato per un compito di classificazione. Quindi confronterò le prestazioni del BERT con un modello di base, in cui utilizzo un vettorizzatore TF-IDF e un classificatore Naive Bayes. Il trasformatori
ci aiutano a mettere a punto in modo rapido ed efficiente il modello BERT allo stato dell'arte e a ottenere un tasso di accuratezza 10% superiore a quello del modello di base.
Riferimento:
Per capire Trasformatore (l'architettura su cui si basa il BERT) e imparare a implementare il BERT, consiglio vivamente di leggere le seguenti fonti:
- Il BERT illustrato, ELMo e co.: Una guida molto chiara e ben scritta per comprendere il BERT.
- La documentazione del
trasformatori
biblioteca - Esercitazione sulla messa a punto del BERT con PyTorch da Chris McCormick: Un tutorial molto dettagliato che mostra come utilizzare BERT con la libreria HuggingFace di PyTorch.
B - Impostazione¶
1. Caricare le librerie essenziali¶
In [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. Set di dati¶
2.1. Scarica il set di dati¶
In [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. Dati del treno di carico¶
I dati del treno sono costituiti da 2 file, ciascuno contenente 1700 tweet di reclamo/non reclamo. Ogni tweet nei dati contiene almeno un hashtag di una compagnia aerea.
Carichiamo i dati del treno e li etichettiamo. Poiché utilizziamo solo i dati di testo per la classificazione, elimineremo le colonne non importanti e manterremo solo le colonne id
, tweet
e etichetta
colonne.
In [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)
Out[0]:
id | tweet | etichetta | |
---|---|---|---|
1988 | 24991 | Che bel bentornato. Ridicolo. Sbarco... | 1 |
1294 | 72380 | Molto deluso da @JetBlue stasera. Volo... | 0 |
1090 | 127893 | @united i miei amici stanno passando un periodo infernale... | 0 |
553 | 58278 | @united tutto quello che voglio per Natale è una borsa smarrita che... | 0 |
2075 | 30695 | Sì, non voglio mentire... sono super interessato a provare... | 1 |
Divideremo casualmente gli interi dati di addestramento in due set: un set di addestramento con 90% di dati e un set di validazione con 10% di dati. Eseguiremo la regolazione degli iperparametri utilizzando la convalida incrociata sull'insieme di addestramento e useremo l'insieme di convalida per confrontare i modelli.
In [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. Dati della prova di carico¶
I dati del test contengono 4555 esempi senza etichetta. Circa 300 esempi sono tweets non lamentosi. Il nostro compito è identificare i loro id
ed esaminare manualmente se i nostri risultati sono corretti.
In [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)
Out[0]:
id | tweet | |
---|---|---|
1539 | 59336 | Volo @AmericanAir in ritardo di oltre 2 ore per n... |
607 | 24101 | @SouthwestAir Ricevo ancora questo messaggio di errore... |
333 | 13179 | in attesa al #SeaTac per imbarcarmi sul mio volo @JetBlue... |
2696 | 102948 | Odio quando vado a selezionare i sedili in anticipo... |
3585 | 135638 | vergogna @AlaskaAir |
3. Impostazione della GPU per la formazione¶
Google Colab offre GPU e TPU gratuite. Poiché dovremo addestrare una rete neurale di grandi dimensioni, è meglio utilizzare queste funzionalità.
È possibile aggiungere una GPU accedendo al menu e selezionando:
Runtime -> Cambia tipo di runtime -> Acceleratore hardware: GPU
Quindi è necessario eseguire la seguente cella per specificare la GPU come dispositivo.
In [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")
Sono disponibili 1 GPU. Nome dispositivo: Tesla T4
C - Linea di base: TF-IDF + Classificatore Naive Bayes¶
In questo approccio di base, per prima cosa utilizzeremo TF-IDF per vettorizzare i dati di testo. Poi utilizzeremo il modello Naive Bayes come classificatore.
Perché Naive Bayes? Ho sperimentato diversi algoritmi di apprendimento automatico, tra cui Random Forest, Support Vectors Machine, XGBoost e ho osservato che Naive Bayes offre le migliori prestazioni. In Guida di Scikit-learn per scegliere lo stimatore giusto, si suggerisce anche di usare Naive Bayes per i dati di testo. Ho anche provato a utilizzare SVD per ridurre la dimensionalità, ma le prestazioni non sono migliorate.
1. Preparazione dei dati¶
1.1. Preelaborazione¶
Nel modello bag-of-words, un testo è rappresentato come il bagaglio delle sue parole, senza tenere conto della grammatica e dell'ordine delle parole. Per questo motivo, si vogliono rimuovere le stop words, le punteggiature e i caratteri che non contribuiscono molto al significato della frase.
In [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] Download del pacchetto stopwords in /root/nltk_data... [nltk_data] Il pacchetto stopwords è già aggiornato!
1.2. Vettorizzatore TF-IDF¶
Nel recupero delle informazioni, TF-IDF, abbreviazione di frequenza dei termini - frequenza inversa dei documentiè una statistica numerica che riflette l'importanza di una parola per un documento in una raccolta o in un corpus. Utilizzeremo TF-IDF per vettorializzare i nostri dati testuali prima di darli in pasto agli algoritmi di apprendimento automatico.
In [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)
Tempi della CPU: utente 5,47 s, sistema: 519 ms, totale: 5,99 s Tempo di parete: 6 s
2. Addestrare il classificatore Naive Bayes¶
2.1. Regolazione degli iperparametri¶
Utilizzeremo la convalida incrociata e il punteggio AUC per mettere a punto gli iperparametri del nostro modello. La funzione get_auc_CV
restituirà il punteggio medio AUC della convalida incrociata.
In [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()
Il MultinominaleNB
hanno un solo ipterparametro - alfa. Il codice seguente ci aiuterà a trovare il valore alfa che fornisce il punteggio CV AUC più alto.
In [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()
Miglior alfa: 1.3
2.2. Valutazione sull'insieme di convalida¶
Per valutare le prestazioni del nostro modello, calcoleremo il tasso di precisione e il punteggio AUC del nostro modello sul set di validazione.
In [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 TF-IDF e l'algoritmo Naive Bayes, si ottiene un tasso di accuratezza di 72.65% sul set di validazione. Questo valore rappresenta la prestazione di base e sarà utilizzato per valutare le prestazioni del nostro modello BERT a regolazione fine.
In [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 Precisione: 75,59%
D - Messa a punto del BERT¶
1. Installare la libreria Hugging Face¶
La libreria di trasformatori di Hugging Face contiene l'implementazione in PyTorch di modelli NLP all'avanguardia, tra cui BERT (di Google), GPT (di OpenAI) ... e i pesi dei modelli pre-addestrati.
In [1]:
#!pip installare trasformatori
2. Tokenizzazione e formattazione dell'input¶
Prima di tokenizzare il nostro testo, effettueremo una leggera elaborazione del testo, tra cui la rimozione delle menzioni di entità (ad esempio, @united) e di alcuni caratteri speciali. Il livello di elaborazione è molto inferiore rispetto agli approcci precedenti, perché BERT è stato addestrato con le frasi intere.
In [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
In [0]:
# Print sentence 0 print('Original: ', X[0]) print('Processed: ', text_preprocessing(X[0]))
Originale: @united Sto avendo problemi. Ieri ho rifatto la prenotazione per 24 ore dopo che avrei dovuto volare, ora non riesco ad accedere e fare il check-in. Potete aiutarmi? Elaborato: Ho dei problemi. Ieri ho rifatto la prenotazione per 24 ore dopo il volo previsto, ora non riesco ad accedere e a fare il check-in. Potete aiutarmi?
2.1. Tokenizzatore BERT¶
Per applicare il BERT pre-addestrato, dobbiamo usare il tokenizer fornito dalla libreria. Questo perché (1) il modello ha un vocabolario specifico e fisso e (2) il tokenizer del BERT ha un modo particolare di gestire le parole fuori vocabolario.
Inoltre, dobbiamo aggiungere dei token speciali all'inizio e alla fine di ogni frase, troncare tutte le frasi a una lunghezza costante e specificare esplicitamente quali sono i token di riempimento con la "maschera di attenzione".
Il codificare_in_più
del tokenizer BERT:
(1) dividere il testo in token,
(2) aggiungere lo speciale [CLS]
e [SEP]
gettoni e
(3) convertire questi token in indici del vocabolario del tokenizer,
(4) aggiungere o troncare le frasi alla lunghezza massima e
(5) creare una maschera di attenzione.
In [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: #codificare_in_più
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
Prima di procedere alla tokenizzazione, è necessario specificare la lunghezza massima delle frasi.
In [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)
Lunghezza massima: 68
Ora tokenizziamo i nostri dati.
In [0]:
# SpecifyMAX_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 functionpreelaborazione_per_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)
Originale: @united Sto avendo problemi. Ieri ho rifatto la prenotazione per 24 ore dopo che avrei dovuto volare, ora non riesco ad accedere e fare il check-in. Potete aiutarmi? ID gettone: [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] Tokenizzazione dei dati...
2.2. Creare il DataLoader di PyTorch¶
Creeremo un iteratore per il nostro set di dati usando la classe torch DataLoader. Questo ci aiuterà a risparmiare memoria durante l'addestramento e ad aumentare la velocità dell'addestramento.
In [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. Addestrare il nostro modello¶
3.1. Creare BertClassifier¶
BERT-base è composto da 12 strati trasformatori; ogni strato trasformatore riceve un elenco di embeddings di token e produce in uscita lo stesso numero di embeddings con la stessa dimensione nascosta (o dimensioni). L'uscita dello strato trasformatore finale del sistema [CLS]
viene utilizzato come caratteristica della sequenza per alimentare un classificatore.
Il trasformatori
ha la libreria BertForSequenceClassification
che è stata progettata per compiti di classificazione. Tuttavia, creeremo una nuova classe in modo da poter specificare la nostra scelta di classificatori.
Di seguito, creeremo una classe BertClassifier con un modello BERT per estrarre l'ultimo strato nascosto del modello BERT. [CLS]
e una rete neurale feed-forward a singolo strato nascosto come classificatore.
In [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): SetFalso
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
Tempi CPU: utente 38 µs, sistema: 0 ns, totale: 38 µs Tempo di parete: 40,1 µs
3.2. Ottimizzatore e programmatore del tasso di apprendimento¶
Per mettere a punto il nostro classificatore Bert, dobbiamo creare un ottimizzatore. Gli autori raccomandano i seguenti iperparametri:
- Dimensione del lotto: 16 o 32
- Tasso di apprendimento (Adam): 5e-5, 3e-5 o 2e-5
- Numero di epoche: 2, 3, 4
Huggingface ha fornito il run_glue.py un esempio di implementazione dello script trasformatori
libreria. Nello script viene utilizzato l'ottimizzatore AdamW.
In [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. Ciclo di formazione¶
Alleneremo il nostro classificatore Bert per 4 epoche. In ogni epoch, alleneremo il nostro modello e valuteremo le sue prestazioni sul set di validazione. Più in dettaglio, ci occuperemo di:
Formazione:
- Disimballare i dati dal dataloader e caricarli sulla GPU
- Azzeramento dei gradienti calcolati nel passaggio precedente
- Eseguire un passaggio in avanti per calcolare i logit e le perdite.
- Eseguire un passaggio all'indietro per calcolare i gradienti (
perdita.indietro()
) - Ridurre la norma dei gradienti a 1,0 per evitare "gradienti esplosivi".
- Aggiornare i parametri del modello (
ottimizzatore.step()
) - Aggiornare il tasso di apprendimento (
scheduler.step()
)
Valutazione:
- Disimballare i dati e caricarli sulla GPU
- Passaggio in avanti
- Calcolo della perdita e del tasso di accuratezza sull'insieme di validazione
Lo script sottostante è commentato con i dettagli del nostro ciclo di formazione e valutazione.
In [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
Ora, iniziamo ad addestrare il nostro BertClassifier!
In [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)
Iniziare la formazione... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formazione completata!
3.4. Valutazione sull'insieme di convalida¶
La fase di previsione è simile alla fase di valutazione eseguita nel ciclo di addestramento, ma più semplice. Eseguiremo un passaggio in avanti per calcolare i logit e applicheremo softmax per calcolare le probabilità.
In [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
In [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 Precisione: 80,59%
Bert Classifer ottiene un punteggio AUC di 0,90 e un tasso di accuratezza di 82,65% sul set di validazione. Questo risultato è migliore di 10 punti rispetto al metodo di base.
3.5. Addestrare il modello su tutti i dati di addestramento¶
In [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)
Iniziare la formazione... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formazione completata!
4. Previsioni sul set di prova¶
4.1. Preparazione dei dati¶
Rivediamo a breve il nostro set di test.
In [0]:
test_data.sample(5)
Out[0]:
id | tweet | |
---|---|---|
471 | 18654 | Amici e familiari: Non volate mai con @JetBlue. Assolutamente... |
1971 | 76265 | @DeltaAssist @rogerioad Non ho mai avuto un pro... |
23 | 672 | Primo volo dopo settimane. Contiamo su di voi @Americ... |
2702 | 103263 | "@USAirways: Sapete che non possiamo rimanere senza... |
135 | 5137 | @southwestair Qui all'aeroporto di SA a guardare il ... |
Prima di fare previsioni sull'insieme di test, è necessario ripetere le operazioni di elaborazione e codifica effettuate sui dati di addestramento. Fortunatamente, abbiamo scritto il programma preelaborazione_per_bert
per farlo al posto nostro.
In [0]:
# Run preelaborazione_per_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)
Tokenizzazione dei dati...
4.2. Previsioni¶
Nel nostro set di test ci sono circa 300 tweet non negativi. Pertanto, continueremo a regolare la soglia di decisione finché non avremo circa 300 tweet non negativi.
La soglia che utilizzeremo è 0,992, il che significa che i tweet con una probabilità di previsione superiore a 99,2% saranno considerati positivi. Questo valore è molto elevato rispetto alla soglia predefinita di 0,5.
Dopo aver esaminato manualmente il set di test, ho scoperto che il compito di classificazione del sentiment è difficile anche per gli esseri umani. Pertanto, una soglia elevata ci darà previsioni sicure.
In [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())
Numero di tweet previsti non negativi: 454
Ora esamineremo 20 tweet casuali dalle nostre previsioni. 17 di essi sono corretti, il che dimostra che il classificatore BERT acquisisce un tasso di precisione dello 0,85 circa.
In [0]:
output = test_data[preds==1] list(output.sample(20).tweet)
Out[0]:
[@Delta @DeltaAssist Delta colpisce ancora. La Sky lounge dell'aeroporto più trafficato del paese'chiusa nei fine settimana. Stupido ed economico. ci manchi @united", @SouthwestAir ha riportato le arachidi tostate al miele. È triste che questa realizzazione sia il punto più alto della mia giornata? #SmallThingsInLife', '@DeltaAssist Ho inviato un'e-mail a kana@delta e a contactus.delta@delta per risolvere i problemi due settimane fa senza ottenere risposta. consigli su chi contattare?', "La donna cacciata dal volo da @AlaskaAir perché ha il cancro ha intenzione di donare alla sua famiglia il biglietto aereo http://t.co/Uj6rispWLb", "@united (2/2) Non ho rotto la borsa. Se non avessi dovuto pagare per il check-in, non sarei così arrabbiato. Preferisco volare con @AmericanAir @SouthwestAir ecc", "Ho volato con quasi tutte le compagnie aeree e non ho mai avuto un'esperienza migliore di quella di @JetBlue. Qualità, servizio, comfort e convenienza. A++", '@JetBlue La migliore compagnia aerea per cui lavorare, mi manchi molto #keepingitminty ', 'Ho convinto @firetweet a prenotare un viaggio last minute per raggiungermi ad Austin! Da allora canto la canzone della sicurezza di @VirginAmerica. Povero Eric.', '@AmericanAir aspetta pazientemente di decollare da #DFW a #ord http://t.co/j1oDSc6fht', @JetBlue oggi è un giorno triste per i fedelissimi del B6. So che state pubblicizzando le vostre nuove "opzioni", ma il vostro servizio e le vostre tariffe senza bagaglio sono ciò che vi rende grandi', 'Le cose positive di questo volo: @Gogo e la grande esperienza di @VirginAmerica. Non così buone: l'odore di vomito di bambino/tonno marcio.', '@USAirways @AmericanAir sentirà la mancanza di USAir :(', '@altonbrown @united È ora di passare a @VirginAmerica', Non è mai il momento sbagliato per Chobani, @AmericanAir Admirals Club! #rovato il record #aomanywasabipeas #lunch', "Durante il mio volo, ho rubato il telefono del mio umano'per provare il nuovo IFE in streaming di @alaskaair '. È buono! Peccato che non possieda un iPad", "Non vedo l'ora che la fusione tra @USAirways e @AmericanAir sia completata, che seccatura per i clienti!", "@JetBlue Io'sono un ragazzo universitario al verde, quindi $150 è un affare enorme", "Non vedo l'ora di tornare nella Bay Area stasera con il volo 2256 di @SouthwestAir", 'Appeso al #SFO in attesa che la nebbia si diradi per il prossimo collegamento @VirginAmerica per il #sxsw! #SXSW2015 #Austin', "@DeltaAssist in ogni caso posso cambiare volo dal 1308 a uno che non sia'indefinitamente in ritardo.... E tornare a Washington!"]
E - Conclusione¶
Aggiungendo al BERT un semplice classificatore di rete neurale a uno strato nascosto e regolando con precisione il BERT, possiamo ottenere prestazioni vicine allo stato dell'arte, con un miglioramento di 10 punti rispetto al metodo di base, pur disponendo di soli 3.400 punti dati.
Inoltre, sebbene BERT sia molto grande, complicato e con milioni di parametri, abbiamo bisogno di metterlo a punto in sole 2-4 epoche. Questo risultato può essere ottenuto perché BERT è stato addestrato su una quantità enorme di parametri e codifica già molte informazioni sulla nostra lingua. Una performance impressionante ottenuta in poco tempo, con una piccola quantità di dati, ha dimostrato perché BERT è uno dei modelli NLP più potenti disponibili al momento.