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.



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

# Specify MAX_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 function traitement_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): Set Faux 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",
 &#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.

Let’s Discuss Your Idea

    Related Posts

    Ready To Supercharge Your Business

    LET’S
    TALK
    fr_FRFrançais