Tutoriel : Ajustement de BERT pour l'analyse de sentiments
- Tutoriel : Ajustement de l'ORET pour l'analyse des sentiments
- A - Introduction¶
- B - Setup¶
- C - Base : TF-IDF + Classificateur Naive Bayes¶
- D - Réglage fin de BERT¶
- E - Conclusion¶
Tutoriel : Ajustement de l'ORET pour l'analyse des sentiments
Publié à l'origine par Chris Tran, chercheur en apprentissage automatique chez Skim AI.
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 en tant que np import pandas sous pd import matplotlib.pyplot en tant que plt %matplotlib en ligne
2. Ensemble de données¶
2.1. Télécharger l'ensemble de données¶
Dans [0] :
# Données à télécharger importer des requêtes request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf") avec open("data.zip", "wb") comme fichier : file.write(request.content) # Décompresser les données import zipfile avec zipfile.ZipFile('data.zip' ;) as zip : zip.extractall('données' ;)
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] :
# Charger les données et définir les étiquettes 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 # Concaténer les données relatives aux plaintes et aux non plaintes data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True) # Drop 'compagnie aérienne' ; colonne data.drop(['airline' ;], inplace=True, axis=1) # Afficher 5 échantillons aléatoires 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] :
# Données du test de chargement test_data = pd.read_csv('data/test_data.csv' ;) # Conserver les colonnes importantes test_data = test_data[['id' ;, 'tweet' ;]]] # Afficher 5 échantillons des données de test 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. Comme nous allons entraîner un grand réseau neuronal, 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'Il y a {torch.cuda.device_count()} GPU(s) disponible(s).' ;) print('Nom du périphérique:' ;, torch.cuda.get_device_name(0)) else : print('Pas de GPU disponible, utilisation du CPU à la place.' ;) 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 # Uncommentaire pour télécharger "stopwords" nltk.download("stopwords") from nltk.corpus import stopwords def text_preprocessing(s) : """ - Mettre la phrase en minuscules - Remplacer "'t" par "not" - Supprimer "@nom" - Isoler et supprimer les ponctuations à l'exception de " ?" - Supprimer les autres caractères spéciaux - Supprimer les mots vides à l'exception de "not" et "can". - Supprimer les espaces blancs de fin """ s = s.lower() # Remplacer 't par 'not' ; s = re.sub(r"'t", "not", s) # Supprimer @nom s = re.sub(r' ;(@.* ?)[s]' ;, ' ; ' ;, s) # Isoler et supprimer les ponctuations sauf '?' ; s = re.sub(r' ;([' ;".()!?\/,])' ;, r' ; 1 ' ;, s) s = re.sub(r' ;[^ws ?]' ;, ' ; ' ;, s) # Suppression de certains caractères spéciaux s = re.sub(r' ;([;:|-"n])' ;, ' ; ' ;, s) # Supprimez les mots d'arrêt sauf 'not' ; et 'can' ; s = " ".join([word for word in s.split() if word not in stopwords.words('english' ;) ou mot dans ['not' ;, 'can' ;]]) # Suppression de l'espacement des caractères de fin de phrase s = re.sub(r's+' ;, ' ; ' ;, s).strip() retourner 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 # Prétraitement du texte 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]) # Calcul du 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) : """ Renvoie le score AUC moyen issu de la validation croisée. """ # Définir KFold pour mélanger les données avant la division kf = StratifiedKFold(5, shuffle=True, random_state=1) # Obtenir les scores AUC auc = cross_val_score( modèle, 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('Meilleur alpha : ' ;, meilleur_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) : """ - Affiche la SSC et la précision sur l'ensemble de test - Tracer la courbe ROC @params probs (np.array) : un tableau de probabilités prédites avec la forme (len(y_true), 2) @params y_true (np.array) : un tableau de valeurs réelles avec la forme (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}') # Obtenir la précision sur l'ensemble de test y_pred = np.where(preds >= 0.5, 1, 0) accuracy = accuracy_score(y_true, y_pred) print(f'Accuracy: {accuracy*100:.2f}%') # Tracé ROC AUC plt.title('Caractéristique d'exploitation du récepteur' ;) 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('Taux de vrais positifs' ;) plt.xlabel('Taux de faux positifs' ;) 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] :
# Calcul des probabilités prédites nb_model = MultinomialNB(alpha=1.8) nb_model.fit(X_train_tfidf, y_train) probs = nb_model.predict_proba(X_val_tfidf) # Évaluation du classificateur 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) : """ - Supprimer les mentions d'entités (ex. '@united' ;) - Corriger les erreurs (ex. '&' ; à '&' ;) @param text (str) : une chaîne de caractères à traiter. @return text (Str) : la chaîne traitée. """ # Supprimer '@nom' ; text = re.sub(r' ;(@.* ?)[s]' ;, ' ; ' ;, text) # Remplacer '&' ; par '&' ; texte = re.sub(r'&' ;, '&' ;, texte) # Suppression des espaces blancs de fin de texte text = re.sub(r's+' ;, ' ; ' ;, text).strip() Retourner le texte
Dans [0] :
# Phrase d'impression 0 print('Original : ' ;, X[0]) print('Traitée : ' ;, 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 # Charger le tokenizer BERT tokenizer = BertTokenizer.from_pretrained('bert-base-uncased' ;, do_lower_case=True) # Création d'une fonction de tokenisation d'un ensemble de textes def preprocessing_for_bert(data) : """Effectuer les étapes de prétraitement requises pour le BERT pré-entraîné. @param data (np.array) : Tableau de textes à traiter. @return input_ids (torch.Tensor) : Tenseur d'identifiants de jetons à fournir à un modèle. @return attention_masks (torch.Tensor) : Tenseur d'indices spécifiant quels qui doivent être pris en compte par le modèle. """ # Créer des listes vides pour stocker les sorties input_ids = [] masques d'attention = [] # Pour chaque phrase... pour les données envoyées : #encode_plus
volonté : # (1) Tokéniser la phrase # (2) Ajouter le[CLS]
et[SEP]
au début et à la fin # (3) Transférer la phrase du tronçon/de l'étui à la longueur maximale # (4) Associer les jetons à leurs identifiants # (5) Créer un masque d'attention # (6) Renvoi d'un dictionnaire de sorties encoded_sent = tokenizer.encode_plus( text=text_preprocessing(sent), # Prétraiter la phrase add_special_tokens=True, # Ajouter[CLS]
et[SEP]
max_length=MAX_LEN, # Longueur maximale pour truncate/pad pad_to_max_length=True, # Remplir la phrase à la longueur maximale #return_tensors='pt' ;, # Retour du tenseur PyTorch return_attention_mask=True # Renvoyer le masque d'attention ) # Ajouter les sorties aux listes input_ids.append(encoded_sent.get('input_ids' ;)) attention_masks.append(encoded_sent.get('attention_mask' ;)) # Conversion des listes en tenseurs input_ids = torch.tensor(input_ids) attention_masques = torch.tensor(attention_masques) return input_ids, attention_masks
Avant de procéder à la tokenisation, nous devons spécifier la longueur maximale de nos phrases.
Dans [0] :
# Concaténation des données de formation et des données de test all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values]) # Encoder nos données concaténées encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets] # Recherche de la longueur maximale max_len = max([len(sent) for sent in encoded_tweets]) print('Longueur maximale : ' ;, max_len)
Longueur maximale : 68
Maintenant, nous allons symboliser nos données.
Dans [0] :
# SpécifierMAX_LEN
MAX_LEN = 64 # Impression de la phrase 0 et de ses identifiants encodés token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy()) print('Original : ' ;, X[0]) print('Token IDs : ' ;, token_ids) # Fonction d'exécutiontraitement_pour_bert
sur l'ensemble d'entraînement et l'ensemble de validation 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 # Conversion d'autres types de données en torch.Tensor train_labels = torch.tensor(y_train) val_labels = torch.tensor(y_val) # Pour un réglage fin de BERT, les auteurs recommandent une taille de lot de 16 ou 32. batch_size = 32 # Créer le DataLoader pour notre ensemble d'entraînement 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) # Créer le DataLoader pour notre jeu de validation 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 # Création de la classe BertClassfier class BertClassifier(nn.Module) : """Modèle Bert pour les tâches de classification. """ def __init__(self, freeze_bert=False) : """ @param bert : un objet BertModel @param classifier : un classificateur torch.nn.Module @param freeze_bert (bool) : SetFaux
pour affiner le modèle de l'ORET """ super(BertClassifier, self).__init__() # Spécifier la taille cachée de BERT, la taille cachée de notre classificateur, et le nombre d'étiquettes D_in, H, D_out = 768, 50, 2 # Instanciation du modèle BERT self.bert = BertModel.from_pretrained('bert-base-uncased' ;) # Instanciation d'un classificateur feed-forward à une couche self.classifier = nn.Sequential( nn.Linear(D_in, H), nn.ReLU(), #nn.Dropout(0.5), nn.Linear(H, D_out) ) # Geler le modèle BERT if freeze_bert : for param in self.bert.parameters() : param.requires_grad = False def forward(self, input_ids, attention_mask) : """ Alimente l'entrée de BERT et le classificateur pour calculer les logits. @param input_ids (torch.Tensor) : un tenseur d'entrée avec la forme (batch_size, max_length) @param attention_mask (torch.Tensor) : un tenseur contenant des informations sur le masque d'attention, de forme (batch_size, max_length). d'attention avec la forme (batch_size, max_length) @return logits (torch.Tensor) : un tenseur de sortie de forme (batch_size, nombre d'étiquettes) """ # Transmettre l'entrée à BERT outputs = self.bert(input_ids=input_ids, masque_d'attention=masque_d'attention) # Extraire le dernier état caché du jeton[CLS]
pour la tâche de classification last_hidden_state_cls = outputs[0][ :, 0, :] # Transmettre l'entrée au classificateur pour calculer les 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) : """Initialiser le classificateur de Bert, l'optimiseur et le planificateur de taux d'apprentissage. """ # Instanciation du classificateur de Bert bert_classifier = BertClassifier(freeze_bert=False) # Demander à PyTorch d'exécuter le modèle sur le GPU bert_classifier.to(device) # Créer l'optimiseur optimizer = AdamW(bert_classifier.parameters(), lr=5e-5, # Taux d'apprentissage par défaut eps=1e-8 # Valeur epsilon par défaut ) # Nombre total d'étapes d'apprentissage total_steps = len(train_dataloader) * epochs # Configuration de l'ordonnanceur de taux d'apprentissage scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, # Valeur par défaut 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) # Définir la graine pour la reproductibilité 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) : """Effectuer une passe avant sur le modèle BERT entraîné pour prédire les probabilités sur l'ensemble de test. """ # Mettre le modèle en mode évaluation. Les couches d'abandon sont désactivées pendant # le temps de test. model.eval() all_logits = [] # Pour chaque lot de notre ensemble de tests... pour lot dans test_dataloader : # Chargement du lot sur le GPU b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2] # Calcul des logits avec torch.no_grad() : logits = model(b_input_ids, b_attn_mask) all_logits.append(logits) # Concaténation des logits de chaque lot all_logits = torch.cat(all_logits, dim=0) # Appliquer softmax pour calculer les probabilités probs = F.softmax(all_logits, dim=1).cpu().numpy() return probs
Dans [0] :
# Calcul des probabilités prédites sur l'ensemble de test probs = bert_predict(bert_classifier, val_dataloader) # Évaluer le classificateur de Bert 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] :
# Concaténation de l'ensemble d'entraînement et de l'ensemble de validation 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) # Entraînement du classificateur de Bert sur l'ensemble des données d'entraînement 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
sur l'ensemble de test
print('Tokenizing data...' ;)
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Créer le DataLoader pour notre jeu de données de test
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] :
# Calcul des probabilités prédites sur l'ensemble de test probs = bert_predict(bert_classifier, test_dataloader) # Obtenir des prédictions à partir des probabilités threshold = 0.9 preds = np.where(probs[ :, 1] > threshold, 1, 0) # Nombre de tweets prédits non négatifs print("Nombre de tweets prédits non négatifs : ", 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.