Tutoriel : Ajustement de BERT pour l'analyse de sentiments
Tutoriel : Ajustement de l'ORET pour l'analyse des sentiments
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
A - Introduction¶
Ces dernières années, la communauté du TAL a connu de nombreuses avancées dans le domaine du traitement du langage naturel, en particulier le passage à l'apprentissage par transfert. Des modèles comme ELMo, ULMFiT de fast.ai, Transformer et GPT d'OpenAI ont permis aux chercheurs d'obtenir des résultats de pointe sur de nombreux benchmarks et ont fourni à la communauté de grands modèles pré-entraînés très performants. Cette évolution du NLP est considérée comme le moment ImageNet du NLP, une évolution qui s'est produite il y a quelques années dans le domaine de la vision par ordinateur, lorsque les couches inférieures des réseaux d'apprentissage profond avec des millions de paramètres entraînés pour une tâche spécifique peuvent être réutilisées et affinées pour d'autres tâches, plutôt que d'entraîner de nouveaux réseaux à partir de zéro.
L'une des étapes les plus importantes dans l'évolution de la PNL est la publication du BERT de Google, qui est décrite comme le début d'une nouvelle ère pour la PNL. Dans ce bloc-notes, j'utiliserai l'outil HuggingFace's transformateurs
pour affiner le modèle BERT pré-entraîné pour une tâche de classification. Je comparerai ensuite les performances du BERT avec un modèle de référence, dans lequel j'utilise un vecteur TF-IDF et un classificateur Naive Bayes. Le modèle de l transformateurs
nous permettent d'affiner rapidement et efficacement le modèle BERT de pointe et d'obtenir un taux de précision 10% plus élevé que le modèle de base.
Référence:
Pour comprendre Transformateur (l'architecture sur laquelle le BERT est construit) et d'apprendre comment mettre en œuvre le BERT, je recommande vivement la lecture des sources suivantes :
- Le BERT illustré, ELMo, et cie.: Un guide très clair et bien écrit pour comprendre BERT.
- La documentation de la
transformateurs
bibliothèque - Tutoriel de réglage fin de BERT avec PyTorch par Chris McCormick: Un tutoriel très détaillé montrant comment utiliser BERT avec la bibliothèque PyTorch HuggingFace.
B - Mise en place¶
1. Charger les bibliothèques essentielles¶
Dans [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. Ensemble de données¶
2.1. Télécharger l'ensemble de données¶
Dans [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. Données sur les trains de charge¶
Les données sur les trains se composent de deux fichiers, chacun contenant 1 700 tweets plaintifs ou non plaintifs. Chaque tweet contient au moins un hashtag d'une compagnie aérienne.
Nous allons charger les données d'entraînement et les étiqueter. Comme nous n'utilisons que les données textuelles pour la classification, nous supprimerons les colonnes sans importance et ne conserverons que les éléments suivants id
, tweet
et étiquette
colonnes.
Dans [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 | étiquette | |
---|---|---|---|
1988 | 24991 | Quel bel accueil pour ce retour. C'est à mourir de rire. Deplanin... | 1 |
1294 | 72380 | Très déçu par @JetBlue ce soir. Les... | 0 |
1090 | 127893 | @united mes amis passent un sale quart d'heure... | 0 |
553 | 58278 | La plupart des gens ne savent pas ce qu'ils font, mais ils savent que les... | 0 |
2075 | 30695 | Je ne vais pas mentir... je suis très intéressé par l'idée d'essayer... | 1 |
Nous diviserons aléatoirement l'ensemble des données d'entraînement en deux ensembles : un ensemble d'entraînement avec 90% des données et un ensemble de validation avec 10% des données. Nous procéderons à l'ajustement des hyperparamètres par validation croisée sur l'ensemble d'entraînement et utiliserons l'ensemble de validation pour comparer les modèles.
Dans [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. Données de l'essai de charge¶
Les données de test contiennent 4555 exemples sans étiquette. Environ 300 exemples sont des tweets non plaintifs. Notre tâche consiste à identifier leurs id
et examiner manuellement si nos résultats sont corrects.
Dans [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 | Un vol @AmericanAir retardé de plus de 2 heures pour n... |
607 | 24101 | @SouthwestAir Toujours ce message d'erreur... |
333 | 13179 | J'attends à #SeaTac pour embarquer sur mon vol @JetBlue... |
2696 | 102948 | Je déteste passer par le processus de sélection des sièges à l'avance... |
3585 | 135638 | Honte à vous @AlaskaAir |
3. Mise en place d'un GPU pour la formation¶
Google Colab offre gratuitement des GPU et des TPU. Étant donné que nous allons entraîner un réseau neuronal de grande taille, il est préférable d'utiliser ces fonctionnalités.
Un GPU peut être ajouté en allant dans le menu et en sélectionnant :
Exécution -> Changer le type d'exécution -> Accélérateur matériel : GPU
Nous devons ensuite exécuter la cellule suivante pour spécifier le GPU comme périphérique.
Dans [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")
Il y a 1 GPU(s) disponible(s). Nom de l'appareil : Tesla T4
C - Base : TF-IDF + classificateur Naive Bayes¶
Dans cette approche de base, nous utiliserons d'abord le TF-IDF pour vectoriser nos données textuelles. Ensuite, nous utiliserons le modèle Naive Bayes comme classificateur.
Pourquoi Naive Bayse ? J'ai expérimenté différents algorithmes d'apprentissage automatique, notamment Random Forest, Support Vectors Machine et XGBoost, et j'ai observé que Naive Bayes offrait les meilleures performances. En Guide de Scikit-learn pour choisir le bon estimateur, il est également suggéré d'utiliser Naive Bayes pour les données textuelles. J'ai également essayé d'utiliser le SVD pour réduire la dimensionnalité, mais cela n'a pas donné de meilleurs résultats.
1. Préparation des données¶
1.1. Prétraitement¶
Dans le modèle du sac de mots, un texte est représenté comme le sac de ses mots, sans tenir compte de la grammaire et de l'ordre des mots. Par conséquent, nous voulons supprimer les mots vides, les ponctuations et les caractères qui ne contribuent pas beaucoup au sens de la phrase.
Dans [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] Téléchargement des mots-clés du paquet dans /root/nltk_data... [nltk_data] Le paquet stopwords est déjà à jour !
1.2. Vectoriseur TF-IDF¶
Dans la recherche d'informations, TF-IDF, abréviation de fréquence des termes - fréquence inverse des documentsLe TF-IDF est une statistique numérique destinée à refléter l'importance d'un mot dans un document d'une collection ou d'un corpus. Nous utiliserons le TF-IDF pour vectoriser nos données textuelles avant de les soumettre à des algorithmes d'apprentissage automatique.
Dans [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)
Temps CPU : user 5.47 s, sys : 519 ms, total : 5.99 s Durée du mur : 6 s
2. Entraînement du classificateur Naive Bayes¶
2.1. Réglage des hyperparamètres¶
Nous utiliserons la validation croisée et le score AUC pour ajuster les hyperparamètres de notre modèle. La fonction get_auc_CV
renvoie le score moyen de la SSC issu de la validation croisée.
Dans [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()
Les MultinominalNB
n'ont qu'un seul hypterparamètre - alpha. Le code ci-dessous nous aidera à trouver la valeur alpha qui nous donne le score CV AUC le plus élevé.
Dans [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()
Meilleur alpha : 1.3
2.2. Évaluation sur l'ensemble de validation¶
Pour évaluer la performance de notre modèle, nous calculons le taux de précision et le score AUC de notre modèle sur l'ensemble de validation.
Dans [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()
En combinant l'algorithme TF-IDF et l'algorithme Naive Bayes, nous obtenons un taux de précision de 72.65% sur l'ensemble de validation. Cette valeur est la performance de base et sera utilisée pour évaluer la performance de notre modèle BERT affiné.
Dans [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 Précision : 75,59%
D - Réglage fin du BERT¶
1. Installer la bibliothèque Hugging Face¶
La bibliothèque de transformateurs de Hugging Face contient des implémentations PyTorch de modèles NLP de pointe tels que BERT (de Google), GPT (d'OpenAI) ... et des poids de modèles pré-entraînés.
Dans [1] :
#!pip installer des transformateurs
2. Tokénisation et formatage des entrées¶
Avant de procéder à la tokenisation de notre texte, nous allons effectuer un léger traitement de notre texte, notamment en supprimant les mentions d'entités (par exemple @united) et certains caractères spéciaux. Le niveau de traitement est ici bien moindre que dans les approches précédentes car BERT a été entraîné avec des phrases entières.
Dans [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
Dans [0] :
# Print sentence 0 print('Original: ', X[0]) print('Processed: ', text_preprocessing(X[0]))
Original : @united J'ai des problèmes. Hier, j'ai fait une nouvelle réservation 24 heures après la date à laquelle je devais prendre l'avion, et maintenant je ne peux plus me connecter ni m'enregistrer. Pouvez-vous m'aider ? Traitée : J'ai des problèmes. Hier, j'ai effectué une nouvelle réservation 24 heures après la date prévue de mon vol, et maintenant je ne peux plus me connecter ni m'enregistrer. Pouvez-vous m'aider ?
2.1. Tokeniseur BERT¶
Afin d'appliquer le BERT pré-entraîné, nous devons utiliser le tokenizer fourni par la bibliothèque. En effet, (1) le modèle a un vocabulaire spécifique et fixe et (2) le tokenizer de l'ORET a une façon particulière de traiter les mots hors-vocabulaire.
En outre, nous sommes tenus d'ajouter des jetons spéciaux au début et à la fin de chaque phrase, de remplir et de tronquer toutes les phrases à une longueur unique et constante, et de spécifier explicitement les jetons de remplissage à l'aide du "masque d'attention".
Les encode_plus
du tokenizer BERT :
(1) diviser notre texte en jetons,
(2) ajouter la mention spéciale [CLS]
et [SEP]
des jetons, et
(3) convertir ces tokens en index du vocabulaire du tokenizer,
(4) les phrases sont tronquées ou complétées jusqu'à la longueur maximale, et
(5) créer un masque d'attention.
Dans [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: #encode_plus
will: # (1) Tokenize the sentence # (2) Add the[CLS]
et[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]
et[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
Avant de procéder à la tokenisation, nous devons spécifier la longueur maximale de nos phrases.
Dans [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)
Longueur maximale : 68
Maintenant, nous allons symboliser nos données.
Dans [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 functiontraitement_pour_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 J'ai des problèmes. Hier, j'ai fait une nouvelle réservation 24 heures après la date à laquelle je devais prendre l'avion, et maintenant je ne peux plus me connecter ni m'enregistrer. Pouvez-vous m'aider ? Token IDs : [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] Tokenisation des données...
2.2. Créer PyTorch DataLoader¶
Nous allons créer un itérateur pour notre ensemble de données en utilisant la classe torch DataLoader. Cela permettra d'économiser de la mémoire pendant l'apprentissage et d'augmenter la vitesse d'apprentissage.
Dans [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. Entraîner notre modèle¶
3.1. Créer un classificateur Bert¶
BERT-base se compose de 12 couches de transformation, chaque couche de transformation prend en charge une liste d'encastrements de jetons et produit le même nombre d'encastrements avec la même taille cachée (ou dimensions) en sortie. La sortie de la dernière couche de transformation du [CLS]
est utilisé comme caractéristique de la séquence pour alimenter un classificateur.
Les transformateurs
dispose de la bibliothèque BertForSequenceClassification
qui est conçue pour les tâches de classification. Cependant, nous allons créer une nouvelle classe afin de pouvoir spécifier notre propre choix de classificateurs.
Nous allons créer ci-dessous une classe BertClassifier avec un modèle BERT pour extraire la dernière couche cachée de l'arbre. [CLS]
et un réseau neuronal feed-forward à une seule couche cachée comme classificateur.
Dans [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): SetFaux
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
Temps CPU : user 38 µs, sys : 0 ns, total : 38 µs Temps du mur : 40,1 µs
3.2. Optimiseur et programmateur de taux d'apprentissage¶
Pour affiner notre classificateur Bert, nous devons créer un optimiseur. Les auteurs recommandent les hyperparamètres suivants :
- Taille des lots : 16 ou 32
- Taux d'apprentissage (Adam) : 5e-5, 3e-5 ou 2e-5
- Nombre d'époques : 2, 3, 4
Huggingface a fourni le run_glue.py un exemple de mise en œuvre du script transformateurs
bibliothèque. Dans le script, l'optimiseur AdamW est utilisé.
Dans [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. Boucle de formation¶
Nous allons entraîner notre classificateur de Bert pendant 4 époques. À chaque époque, nous entraînerons notre modèle et évaluerons ses performances sur l'ensemble de validation. Pour plus de détails, nous allons :
Formation :
- Décompresser nos données du dataloader et charger les données sur le GPU
- Remise à zéro des gradients calculés lors de la passe précédente
- Effectuer une passe avant pour calculer les logits et les pertes
- Effectuer une passe arrière pour calculer les gradients (
perte.backward()
) - Réduire la norme des gradients à 1,0 pour éviter les "gradients explosifs".
- Mettre à jour les paramètres du modèle (
optimizer.step()
) - Mettre à jour le taux d'apprentissage (
scheduler.step()
)
Évaluation :
- Déballer nos données et les charger sur le GPU
- Passe en avant
- Calculer la perte et le taux de précision sur l'ensemble de validation
Le script ci-dessous est commenté avec les détails de notre boucle de formation et d'évaluation.
Dans [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
Maintenant, commençons à former notre BertClassifier !
Dans [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)
Commencer la formation... 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 ---------------------------------------------------------------------- Époque | Lot | Perte de train | Perte de valeur | Accroissement de valeur | Délai écoulé ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formation terminée !
3.4. Évaluation sur l'ensemble de validation¶
L'étape de prédiction est similaire à l'étape d'évaluation que nous avons réalisée dans la boucle d'apprentissage, mais elle est plus simple. Nous effectuerons une passe avant pour calculer les logits et appliquerons la méthode softmax pour calculer les probabilités.
Dans [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
Dans [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 Précision : 80,59%
Bert Classifer obtient un score AUC de 0,90 et un taux de précision de 82,65% sur l'ensemble de validation. Ce résultat est supérieur de 10 points à celui de la méthode de base.
3.5. Entraîner notre modèle sur l'ensemble des données d'entraînement¶
Dans [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)
Commencer la formation... 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 ---------------------------------------------------------------------- Époque | Lot | Perte de train | Perte de valeur | Accroissement de valeur | Délai écoulé ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formation terminée !
4. Prédictions sur l'ensemble de test¶
4.1. Préparation des données¶
Revenons brièvement sur notre série de tests.
Dans [0] :
test_data.sample(5)
Out[0] :
id | tweet | |
---|---|---|
471 | 18654 | Amis et famille : Ne volez jamais avec @JetBlue. Absol... |
1971 | 76265 | @DeltaAssist @rogerioad Je n'ai jamais eu de pro... |
23 | 672 | Premier vol depuis des semaines. Je compte sur vous @Americ... |
2702 | 103263 | "@USAirways : Vous savez que nous ne pouvons pas rester sans... |
135 | 5137 | @southwestair Ici, à l'aéroport de SA, nous regardons le ... |
Avant de faire des prédictions sur l'ensemble de test, nous devons refaire les étapes de traitement et d'encodage effectuées sur les données d'apprentissage. Heureusement, nous avons écrit le traitement_pour_bert
pour le faire à notre place.
Dans [0] :
# Run traitement_pour_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)
La tokenisation des données...
4.2. Prédictions¶
Il y a environ 300 tweets non négatifs dans notre ensemble de test. Par conséquent, nous continuerons à ajuster le seuil de décision jusqu'à ce que nous ayons environ 300 tweets non négatifs.
Le seuil que nous utiliserons est de 0,992, ce qui signifie que les tweets dont la probabilité prédite est supérieure à 99,2% seront prédits positifs. Cette valeur est très élevée par rapport au seuil par défaut de 0,5.
Après avoir examiné manuellement l'ensemble de test, je constate que la tâche de classification des sentiments est même difficile pour un être humain. Par conséquent, un seuil élevé nous permettra d'obtenir des prédictions sûres.
Dans [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())
Nombre de tweets prédits non négatifs : 454
Nous allons maintenant examiner 20 tweets aléatoires issus de nos prédictions. 17 d'entre eux sont corrects, ce qui montre que le classificateur BERT obtient un taux de précision d'environ 0,85.
Dans [0] :
output = test_data[preds==1] list(output.sample(20).tweet)
Out[0] :
["@Delta @DeltaAssist Delta frappe à nouveau. Le salon de l'aéroport le plus fréquenté du pays est fermé le week-end. C'est stupide et bon marché. Vous nous manquez, @united", ' ;.@SouthwestAir a ramené les cacahuètes grillées au miel. Est-ce triste que cette réalisation soit le point culminant de ma journée ? #SmallThingsInLife' ;, '@DeltaAssist J'ai envoyé un courriel à kana@delta et à contactus.delta@delta pour résoudre des problèmes il y a deux semaines, sans réponse, "Woman With Kicked Off Flight By @AlaskaAir Because So Has #Cancer Plans to Donate Her Family's Airfare http://t.co/Uj6rispWLb" (Une femme expulsée d'un vol par @AlaskaAir parce qu'elle a un cancer prévoit de faire don des billets d'avion de sa famille), "@united (2/2) Je n'ai pas cassé le sac. Si je n'avais pas eu à payer pour l'enregistrer, je n'aurais pas été aussi contrarié. Je préfère voler avec @AmericanAir @SouthwestAir etc", "J'ai volé avec à peu près toutes les compagnies aériennes et je n'ai jamais eu une meilleure expérience qu'en volant avec @JetBlue. Qualité, service, confort et prix abordable. A++", '@JetBlue Best airline to work for miss u lots #keepingitminty ' ;, 'J'ai convaincu @firetweet de réserver un voyage de dernière minute pour me rejoindre à Austin tom ! Depuis, je chante la chanson de la sécurité de @VirginAmerica. Pauvre Eric ' ;, '@AmericanAir attendant patiemment de décoller de #DFW à #ord http://t.co/j1oDSc6fht' ;, 'Oh @JetBlue aujourd'hui est un jour triste pour les fidèles de B6. Je sais que vous vantez vos nouvelles "options", mais vos frais de service et d'absence de bagages sont ce qui fait notre force' ;, 'Les points positifs de ce vol : @Gogo et la grande expérience de @VirginAmerica. Moins bien : l'odeur de vomi de bébé et de thon pourri' ;, ' ;, ' ;, ' ;, '@USAirways @AmericanAir va manquer à USAir :(' ;, '@altonbrown @united Il est temps de passer à @VirginAmerica' ;, 'Ce n'est jamais le mauvais moment pour Chobani, @AmericanAir Admirals Club ! #brokenrecord #toomanywasabipeas #lunch' ;, "Sur mon vol, j'ai volé le téléphone de mon humain' pour essayer le nouvel IFE en streaming d'@alaskaair '. C'est'bien ! Dommage qu'elle n'ait pas d'iPad", "J'ai hâte que la fusion entre @USAirways et @AmericanAir soit achevée, quelle galère pour le client !", "@JetBlue Je'suis un étudiant fauché, alors $150 est une énorme affaire", "Je suis impatient de retourner dans la région de la Baie ce soir sur le vol 2256 de SouthwestAir ! !!!", 'Accroché à #SFO en attendant que le brouillard se dissipe pour la prochaine correspondance @VirginAmerica vers #sxsw ! #SXSW2015 #Austin', "@DeltaAssist si je peux changer de vol à partir de 1308 pour un vol qui n'est pas retardé indéfiniment.... And get back to dc !"]
E - Conclusion¶
En ajoutant un simple classificateur de réseau neuronal à une couche cachée au BERT et en affinant le BERT, nous pouvons atteindre une performance proche de l'état de l'art, soit 10 points de mieux que la méthode de base bien que nous n'ayons que 3 400 points de données.
En outre, bien que le BERT soit très grand, compliqué et qu'il comporte des millions de paramètres, nous n'avons besoin de l'affiner que dans 2 à 4 époques. Ce résultat peut être obtenu parce que le BERT a été entraîné sur une quantité énorme et qu'il code déjà beaucoup d'informations sur notre langue. Une performance impressionnante obtenue en peu de temps, avec une petite quantité de données, a montré pourquoi BERT est l'un des modèles de NLP les plus puissants actuellement disponibles.