Tutoriel : Ajustement de BERT pour l'analyse de sentiments

Tutoriel : Ajustement de l'ORET pour l'analyse des sentiments

    Publié à l'origine par Chris Tran, chercheur en apprentissage automatique chez Skim AI.



BERT_pour_l'analyse_des_sentiments





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 :

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&#39 ;) as zip :
    zip.extractall('données&#39 ;)

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&#39 ;)
data_complaint['label&#39 ;] = 0
data_non_complaint = pd.read_csv('data/noncomplaint1700.csv&#39 ;)
data_non_complaint['label&#39 ;] = 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&#39 ; colonne
data.drop(['airline&#39 ;], inplace=True, axis=1)
# Afficher 5 échantillons aléatoires
data.sample(5)

Out[0] :

idtweetétiquette
198824991Quel bel accueil pour ce retour. C'est à mourir de rire. Deplanin...1
129472380Très déçu par @JetBlue ce soir. Les...0
1090127893@united mes amis passent un sale quart d'heure...0
55358278La plupart des gens ne savent pas ce qu'ils font, mais ils savent que les...0
207530695Je 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&#39 ;)
# Conserver les colonnes importantes
test_data = test_data[['id&#39 ;, 'tweet&#39 ;]]]
# Afficher 5 échantillons des données de test
test_data.sample(5)

Out[0] :

idtweet
153959336Un vol @AmericanAir retardé de plus de 2 heures pour n...
60724101@SouthwestAir Toujours ce message d'erreur...
33313179J'attends à #SeaTac pour embarquer sur mon vol @JetBlue...
2696102948Je déteste passer par le processus de sélection des sièges à l'avance...
3585135638Honte à 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'Il y a {torch.cuda.device_count()} GPU(s) disponible(s).&#39 ;)
    print('Nom du périphérique:&#39 ;, torch.cuda.get_device_name(0))
else :
    print('Pas de GPU disponible, utilisation du CPU à la place.&#39 ;)
    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&#39 ;
    s = re.sub(r"'t", "not", s)
    # Supprimer @nom
    s = re.sub(r&#39 ;(@.* ?)[s]&#39 ;, &#39 ; &#39 ;, s)
    # Isoler et supprimer les ponctuations sauf '?&#39 ;
    s = re.sub(r&#39 ;([&#39 ;".()!?\/,])&#39 ;, r&#39 ; 1 &#39 ;, s)
    s = re.sub(r&#39 ;[^ws ?]&#39 ;, &#39 ; &#39 ;, s)
    # Suppression de certains caractères spéciaux
    s = re.sub(r&#39 ;([;:|-"n])&#39 ;, &#39 ; &#39 ;, s)
    # Supprimez les mots d'arrêt sauf 'not&#39 ; et 'can&#39 ;
    s = " ".join([word for word in s.split()
                  if word not in stopwords.words('english&#39 ;)
                  ou mot dans ['not&#39 ;, 'can&#39 ;]])
    # Suppression de l'espacement des caractères de fin de phrase
    s = re.sub(r's+&#39 ;, &#39 ; &#39 ;, 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 : &#39 ;, meilleur_alpha)
plt.plot(res)
plt.title('AUC vs. Alpha&#39 ;)
plt.xlabel('Alpha&#39 ;)
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&#39 ;)
    plt.plot(fpr, tpr, 'b&#39 ;, label = 'AUC = %0.2f&#39 ; % roc_auc)
    plt.legend(loc = 'lower right&#39 ;)
    plt.plot([0, 1], [0, 1],'r--&#39 ;)
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('Taux de vrais positifs&#39 ;)
    plt.xlabel('Taux de faux positifs&#39 ;)
    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&#39 ;)
    - Corriger les erreurs (ex. '&&#39 ; à '&&#39 ;)
    @param text (str) : une chaîne de caractères à traiter.
    @return text (Str) : la chaîne traitée.
    """
    # Supprimer '@nom&#39 ;
    text = re.sub(r&#39 ;(@.* ?)[s]&#39 ;, &#39 ; &#39 ;, text)
    # Remplacer '&&#39 ; par '&&#39 ;
    texte = re.sub(r'&&#39 ;, '&&#39 ;, texte)
    # Suppression des espaces blancs de fin de texte
    text = re.sub(r's+&#39 ;, &#39 ; &#39 ;, text).strip()
    Retourner le texte

Dans [0] :

# Phrase d'impression 0
print('Original : &#39 ;, X[0])
print('Traitée : &#39 ;, 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&#39 ;, 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&#39 ;, # 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&#39 ;))
        attention_masks.append(encoded_sent.get('attention_mask&#39 ;))
    # 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 : &#39 ;, max_len)
Longueur maximale : 68

Maintenant, nous allons symboliser nos données.
Dans [0] :

# Spécifier MAX_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 : &#39 ;, X[0])
print('Token IDs : &#39 ;, token_ids)
# Fonction d'exécution traitement_pour_bert sur l'ensemble d'entraînement et l'ensemble de validation
print('Tokenizing data...&#39 ;)
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) : Set Faux 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&#39 ;)
        # 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] :

idtweet
47118654Amis et famille : Ne volez jamais avec @JetBlue. Absol...
197176265@DeltaAssist @rogerioad Je n'ai jamais eu de pro...
23672Premier vol depuis des semaines. Je compte sur vous @Americ...
2702103263"@USAirways : Vous savez que nous ne pouvons pas rester sans...
1355137@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...&#39 ;)
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",
 &#39 ;.@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&#39 ;,
 '@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 &#39 ;,
 '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 &#39 ;,
 '@AmericanAir attendant patiemment de décoller de #DFW à #ord http://t.co/j1oDSc6fht&#39 ;,
 '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&#39 ;,
 '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&#39 ;, &#39 ;, &#39 ;, &#39 ;,
 '@USAirways @AmericanAir va manquer à USAir :(&#39 ;,
 '@altonbrown @united Il est temps de passer à @VirginAmerica&#39 ;,
 'Ce n'est jamais le mauvais moment pour Chobani, @AmericanAir Admirals Club ! #brokenrecord #toomanywasabipeas #lunch&#39 ;,
 "Sur mon vol, j'ai volé le téléphone de mon humain&#39 pour essayer le nouvel IFE en streaming d'@alaskaair &#39. 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.

Discutons de votre idée

    Articles connexes

    Prêt à donner un coup de fouet à votre entreprise

    LAISSONS
    PARLER
    fr_FRFrançais