Skip to main content

Comment ajuster BERT pour la classification de textes ?

Un coup d'envoi convivial pour le lecteur de code-first afin de peaufiner BERT pour la classification de textes, tf.data et tf.Hub.
Created on February 2|Last edited on May 6
Ceci est une traduction d'un article en anglais qui peut être trouvé ici.



Sections: 




Qu'est-ce que BERT ? 

BERT (Bidirectional Encoder Representations from Transformers), plus connu sous le nom de BERT, est un article révolutionnaire de Google qui a permis d'augmenter les performances de pointe pour diverses tâches NLP et a servi de tremplin à de nombreuses autres architectures révolutionnaires.
Il n'est pas exagéré de dire que BERT a donné une nouvelle orientation à l'ensemble du domaine. Il montre les avantages évidents de l'utilisation de modèles pré-entraînés (formés sur d'énormes ensembles de données) et de l'apprentissage par transfert, indépendamment des tâches en aval.
Dans ce rapport, nous allons examiner l'utilisation de BERT pour la classification de textes et fournir une tonne de code et d'exemples pour vous permettre de démarrer. Si vous souhaitez consulter vous-même la source primaire, voici un lien vers l'article annoté. 
Modèle de classification de l'ORET

Configuration de BERT pour la classification de texte

Nous allons tout d’abord installer TensorFlow et TensorFlow Model Garden :
import tensorflow as tf
print(tf.version.VERSION)
!git clone --depth 1 -b v2.4.0 https://github.com/tensorflow/models.git
Nous allons également cloner le Repo Github pour les modèles TensorFlow. Quelques points à noter :
  • -depth 1, pendant le clonage, Git n'obtiendra que la dernière copie des fichiers concernés. Cela peut vous faire gagner beaucoup d'espace et de temps.
  • -b nous permet de cloner une branche spécifique seulement.
Veuillez le faire correspondre à votre version de TensorFlow 2.x.
# install requirements to use tensorflow/models repository
!pip install -Uqr models/official/requirements.txt
# you may have to restart the runtime afterwards, also ignore any ERRORS popping up at this step
Il pleut des importations ici, mes amis.
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
import sys
sys.path.append('models')
from official.nlp.data import classifier_data_lib
from official.nlp.bert import tokenization
from official.nlp import optimization
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set()
import wandb
from wandb.keras import WandbCallback
Une vérification rapide des différentes versions et dépendances installées :
print("TF Version: ", tf.__version__)
print("Eager mode: ", tf.executing_eagerly())
print("Hub version: ", hub.__version__)
print("GPU is", "available" if tf.config.experimental.list_physical_devices("GPU") else "NOT AVAILABLE")

Récupérons le jeu de données

Le jeu de données que nous allons utiliser aujourd'hui est fourni par le concours de classification des questions insincères de Quora sur Kaggle.
N'hésitez pas à télécharger le jeu d'entraînement sur Kaggle ou à utiliser le lien ci-dessous pour télécharger le train.csv de cette compétition :

Décompressez et lisez les données dans un DataFrame pandas :

Exécutez ensuite ce qui suit :
# TO LOAD DATA FROM ARCHIVE LINK
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv('https://archive.org/download/quora_dataset_train.csv/quora_dataset_train.csv.zip',
compression='zip',
low_memory=False)
print(df.shape)
df.head(10)
# label 0 == non toxic
# label 1 == toxic
Très bien. Maintenant, visualisons rapidement ces données dans un tableau W&B :



Explorons

La distribution des étiquettes

Il est bon de comprendre les données avec lesquelles vous travaillez avant de vous lancer dans la modélisation. Ici, nous allons examiner la distribution des étiquettes, la longueur de nos points de données, nous assurer que nos ensembles de test et de formation sont bien distribués, et effectuer quelques autres tâches préliminaires. Mais tout d'abord, examinons la distribution des étiquettes en exécutant.
print(df['target'].value_counts())
df['target'].value_counts().plot.bar()
plt.yscale('log');
plt.title('Distribution of Labels')

Distribution des étiquettes

Longueur des mots et longueur des caractères

Maintenant, exécutons quelques lignes de code pour comprendre les données textuelles avec lesquelles nous travaillons ici.
print('Average word length of questions in dataset is {0:.0f}.'.format(np.mean(df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in dataset is {0:.0f}.'.format(np.max(df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in dataset is {0:.0f}.'.format(np.mean(df['question_text'].apply(lambda x: len(x)))))



Préparation des données d'entraînement et de test pour nos tâches de classification de texte BERT

Quelques notes sur notre approche ici :
💡
  • Nous utiliserons de petites portions des données, car l'ensemble des données prendrait un temps fou à former. Vous pouvez bien sûr inclure plus de données en modifiant train_size.
  • L'ensemble de données étant très déséquilibré, nous conserverons la même distribution dans les ensembles d’entraînement et de test en les stratifiant en fonction des étiquettes. Dans cette section, nous allons analyser nos données pour nous assurer que nous avons fait du bon travail.
train_df, remaining = train_test_split(df, random_state=42, train_size=0.1, stratify=df.target.values)
valid_df, _ = train_test_split(remaining, random_state=42, train_size=0.01, stratify=remaining.target.values)
print(train_df.shape)
print(valid_df.shape)
(130612, 3) (11755, 3)

Obtenir la longueur des mots et des caractères pour les ensembles échantillonnés

print("FOR TRAIN SET\n")
print('Average word length of questions in train set is {0:.0f}.'.format(np.mean(train_df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in train set is {0:.0f}.'.format(np.max(train_df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in train set is {0:.0f}.'.format(np.mean(train_df['question_text'].apply(lambda x: len(x)))))
print('Label Distribution in train set is \n{}.'.format(train_df['target'].value_counts()))
print("\n\nFOR VALIDATION SET\n")
print('Average word length of questions in valid set is {0:.0f}.'.format(np.mean(valid_df['question_text'].apply(lambda x: len(x.split())))))
print('Max word length of questions in valid set is {0:.0f}.'.format(np.max(valid_df['question_text'].apply(lambda x: len(x.split())))))
print('Average character length of questions in valid set is {0:.0f}.'.format(np.mean(valid_df['question_text'].apply(lambda x: len(x)))))
print('Label Distribution in validation set is \n{}.'.format(valid_df['target'].value_counts()))
En d'autres termes, il semble que l'ensemble d’entraînement et l'ensemble de validation soient similaires en termes de déséquilibre entre les classes et les différentes longueurs des textes des questions.


Analyse de la distribution de la longueur des textes de questions en mots

# TRAIN SET
train_df['question_text'].apply(lambda x: len(x.split())).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in words')


# VALIDATION SET
valid_df['question_text'].apply(lambda x: len(x.split())).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in words')




Analyse de la distribution de la longueur du texte de la question en caractères

En examinant nos ensembles de formation et de validation, nous voulons également vérifier si la longueur du texte de la question est généralement similaire entre les deux. Avoir des distributions à peu près similaires est généralement une bonne idée pour éviter de biaiser ou de surajuster notre modèle.
# TRAIN SET
train_df['question_text'].apply(lambda x: len(x)).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in characters')


# VALIDATION SET
valid_df['question_text'].apply(lambda x: len(x)).plot(kind='hist');
plt.yscale('log');
plt.title('Distribution of question text length in characters')

Et c'est le cas. Même la distribution de la longueur des questions en mots et en caractères est très similaire. Cela ressemble à une bonne répartition entraînement/test jusqu'à présent.


Maîtriser les données

Nous voulons ensuite que l'ensemble de données soit créé et prétraité sur le CPU :
with tf.device('/cpu:0'):
train_data = tf.data.Dataset.from_tensor_slices((train_df['question_text'].values, train_df['target'].values))
valid_data = tf.data.Dataset.from_tensor_slices((valid_df['question_text'].values, valid_df['target'].values))
# lets look at 3 samples from train set
for text,label in train_data.take(3):
print(text)
print(label)

print(len(train_data))
print(len(valid_data))
130612 11755
Ok. Allons-y pour BERT.

Commençons avec BERT : Obtenez le modèle BERT pré-entraîné à partir de TensorFlow Hub

Source
Nous utiliserons le BERT sans boîtier présent dans le tfhub.
Afin de préparer le texte qui sera donné à la couche BERT, nous devons d'abord tokeniser nos mots. Le tokéniseur est ici présent en tant qu'actif du modèle et fera le débasage pour nous aussi.
Définition de tous les paramètres sous la forme d'un dictionnaire, de sorte que toute modification, si nécessaire, peut être effectuée ici.

# Setting some parameters

config = {'label_list' : [0, 1], # Label categories
'max_seq_length' : 128, # maximum length of (token) input sequences
'train_batch_size' : 32,
'learning_rate': 2e-5,
'epochs':5,
'optimizer': 'adam',
'dropout': 0.5,
'train_samples': len(train_data),
'valid_samples': len(valid_data),
'train_split':0.1,
'valid_split': 0.01
}


Obtenir la couche BERT et le tokenizer.

# All details here: https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/2

bert_layer = hub.KerasLayer('https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/2',
trainable=True)
vocab_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
do_lower_case = bert_layer.resolved_object.do_lower_case.numpy() # checks if the bert layer we are using is uncased or not
tokenizer = tokenization.FullTokenizer(vocab_file, do_lower_case)


Vérification de certains échantillons d'entraînement et de leurs identifiants tokenisés.

input_string = "hello world, it is a wonderful day for learning"
print(tokenizer.wordpiece_tokenizer.tokenize(input_string))
print(tokenizer.convert_tokens_to_ids(tokenizer.wordpiece_tokenizer.tokenize(input_string)))
['hello', 'world', '##,', 'it', 'is', 'a', 'wonderful', 'day', 'for', 'learning'] [7592, 2088, 29623, 2009, 2003, 1037, 6919, 2154, 2005, 4083]


Préparons ces données : Tokeniser et prétraiter le texte pour BERT

Chaque ligne du jeu de données est composée du texte de la critique et de son étiquette. Le prétraitement des données consiste à transformer le texte en caractéristiques d'entrée BERT :
  • Ids de mots en entrée : Sortie de notre tokenizer, qui convertit chaque phrase en un ensemble d'identifiants de mots.
  • Masques d'entrée : Puisque nous remplissons toutes les séquences jusqu'à 128 (longueur maximale de la séquence), il est important de créer une sorte de masque pour s'assurer que ces remplissages n'interfèrent pas avec les mots de texte réels. Par conséquent, nous avons besoin d'un masque d'entrée généré qui bloque le remplissage. Le masque a 1 pour les tokens réels et 0 pour les tokens de remplissage. Seuls les vrais tokens sont pris en compte.
  • Ids de segments : Pour la tâche de classification de texte, puisqu'il n'y a qu'une seule séquence, les ID de segment/type d'entrée sont essentiellement un vecteur de 0.
Bert a été entraîné sur deux tâches :
  1. Complétez les mots masqués au hasard dans une phrase.
  2. Étant donné deux phrases, quelle phrase est venue en premier ?
# This provides a function to convert row to input features and label,
# this uses the classifier_data_lib which is a class defined in the tensorflow model garden we installed earlier

def create_feature(text, label, label_list=config['label_list'], max_seq_length=config['max_seq_length'], tokenizer=tokenizer):
"""
converts the datapoint into usable features for BERT using the classifier_data_lib

Parameters:
text: Input text string
label: label associated with the text
label_list: (list) all possible labels
max_seq_length: (int) maximum sequence length set for bert
tokenizer: the tokenizer object instantiated by the files in model assets

Returns:
feature.input_ids: The token ids for the input text string
feature.input_masks: The padding mask generated
feature.segment_ids: essentially here a vector of 0s since classification
feature.label_id: the corresponding label id from lable_list [0, 1] here

"""

# since we only have 1 sentence for classification purpose, textr_b is None
example = classifier_data_lib.InputExample(guid = None,
text_a = text.numpy(),
text_b = None,
label = label.numpy())
# since only 1 example, the index=0
feature = classifier_data_lib.convert_single_example(0, example, label_list,
max_seq_length, tokenizer)

return (feature.input_ids, feature.input_mask, feature.segment_ids, feature.label_id)

  • Vous voulez utiliser Dataset.map pour appliquer cette fonction à chaque élément de l'ensemble de données. Dataset.map fonctionne en mode graphique et les tenseurs Graph n'ont pas de valeur.
  • En mode graphique, vous pouvez uniquement utiliser les opérations et les fonctions TensorFlow.
Vous ne pouvez donc pas .map cette fonction directement : Vous devez l'envelopper dans tf.py_function. tf.py_function passera des tenseurs réguliers (avec une valeur et une méthode .numpy() pour y accéder), à la fonction python enveloppée.

Emballage de la fonction Python dans une opération TensorFlow pour une exécution rapide

def create_feature_map(text, label):
"""
A tensorflow function wrapper to apply the transformation on the dataset.
Parameters:
Text: the input text string.
label: the classification ground truth label associated with the input string

Returns:
A tuple of a dictionary and a corresponding label_id with it. The dictionary
contains the input_word_ids, input_mask, input_type_ids
"""

input_ids, input_mask, segment_ids, label_id = tf.py_function(create_feature, inp=[text, label],
Tout=[tf.int32, tf.int32, tf.int32, tf.int32])
max_seq_length = config['max_seq_length']

# py_func doesn't set the shape of the returned tensors.
input_ids.set_shape([max_seq_length])
input_mask.set_shape([max_seq_length])
segment_ids.set_shape([max_seq_length])
label_id.set_shape([])

x = {
'input_word_ids': input_ids,
'input_mask': input_mask,
'input_type_ids': segment_ids
}
return (x, label_id)
Le dernier point de données transmis au modèle a le format d'un dictionnaire comme x et labels (le dictionnaire a des clés qui doivent évidemment correspondre).

Laissez les données circuler : création du pipeline d'entrée final à l'aide de tf.data



Appliquer la transformation à nos jeux de données de formation et de test

# Now we will simply apply the transformation to our train and test datasets
with tf.device('/cpu:0'):
# train
train_data = (train_data.map(create_feature_map,
num_parallel_calls=tf.data.experimental.AUTOTUNE)

.shuffle(1000)
.batch(32, drop_remainder=True)
.prefetch(tf.data.experimental.AUTOTUNE))

# valid
valid_data = (valid_data.map(create_feature_map,
num_parallel_calls=tf.data.experimental.AUTOTUNE)
.batch(32, drop_remainder=True)
.prefetch(tf.data.experimental.AUTOTUNE))
Les tf.data.Datasets résultants renvoient des paires (features, labels), comme prévu par keras.Model.fit.
# train data spec, we can finally see the input datapoint is now converted to the
#BERT specific input tensor
train_data.element_spec



Création, formation et suivi de notre modèle de classification BERT.

Modélisons notre chemin vers la gloire ! !!

Créer le modèle

Il y a deux sorties de la couche BERT :
  • Un pooled_output de forme [batch_size, 768] avec des représentations pour l'ensemble des séquences d'entrée.
  • Une séquence_sortie de forme [batch_size, max_seq_length, 768] avec des représentations pour chaque token d'entrée (en contexte).
Pour la tâche de classification, nous ne sommes concernés que par le pooled_output.
# Building the model, input ---> BERT Layer ---> Classification Head
def create_model():
input_word_ids = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_word_ids")

input_mask = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_mask")

input_type_ids = tf.keras.layers.Input(shape=(config['max_seq_length'],),
dtype=tf.int32,
name="input_type_ids")


pooled_output, sequence_output = bert_layer([input_word_ids, input_mask, input_type_ids])
# for classification we only care about the pooled-output.
# At this point we can play around with the classification head based on the
# downstream tasks and its complexity

drop = tf.keras.layers.Dropout(config['dropout'])(pooled_output)
output = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(drop)

# inputs coming from the function
model = tf.keras.Model(
inputs={
'input_word_ids': input_word_ids,
'input_mask': input_mask,
'input_type_ids': input_type_ids},
outputs=output)

return model


Formation de votre modèle

# Calling the create model function to get the keras based functional model
model = create_model()

# using adam with a lr of 2*(10^-5), loss as binary cross entropy as only
# 2 classes and similarly binary accuracy
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=config['learning_rate']),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy(),
tf.keras.metrics.PrecisionAtRecall(0.5),
tf.keras.metrics.Precision(),
tf.keras.metrics.Recall()])
#model.summary()

Résumé du modèle
Résumé de l'architecture du modèle
L'un des inconvénients du hub tf est que nous importons le module entier comme une couche dans keras, ce qui fait que nous ne voyons pas les paramètres et les couches dans le résumé du modèle.
tf.keras.utils.plot_model(model=model, show_shapes=True, dpi=76, )

La page officielle de tfhub indique que "Tous les paramètres du module peuvent être entraînés, et le réglage fin de tous les paramètres est la pratique recommandée." Par conséquent, nous allons continuer et entraîner le modèle entier sans geler quoi que ce soit.

Suivi des expériences

Puisque vous êtes ici, je suis sûr que vous avez une bonne idée de ce qu’est Weights & Biases, mais si ce n'est pas le cas, lisez la suite :)
Afin de commencer le suivi de l'expérience, nous allons créer des "runs" sur W&B,
wandb.init(): Elle initialise l'exécution avec les paramètres d'information de base du projet :
  • projet : Le nom du projet, créera un nouvel onglet de projet où toutes les expériences de ce projet seront suivies.
  • config : Un dictionnaire de tous les paramètres et hyperparamètres que nous souhaitons suivre.
  • group : optionnel, mais nous aidera à regrouper par différents paramètres plus tard.
  • job_type : pour décrire le type de travail, cela aidera à regrouper les différentes expériences par la suite, par exemple "former", "évaluer", etc.
# Update CONFIG dict with the name of the model.
config['model_name'] = 'BERT_EN_UNCASED'
print('Training configuration: ', config)

# Initialize W&B run
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='train')
Maintenant, afin d'enregistrer toutes les différentes métriques, nous allons utiliser un simple rappel fourni par W&B.
WandCallback(): https://docs.wandb.ai/guides/integrations/keras
Oui, c'est aussi simple que d'ajouter un callback :D
# Train model
# setting low epochs as It starts to overfit with this limited data, please feel free to change
epochs = config['epochs']
history = model.fit(train_data,
validation_data=valid_data,
epochs=epochs,
verbose=1,
callbacks = [WandbCallback()])
run.finish()



Quelques mesures et graphiques de formation





Évaluons

Faisons une évaluation sur l'ensemble de validation et enregistrons les scores en utilisant W&B.
wandb.log(): Enregistre un dictionnaire de scalaires (métriques comme la précision et la perte) et tout autre type d'objet wandb. Ici, nous allons passer le dictionnaire d'évaluation tel quel et l'enregistrer.
# Initialize a new run for the evaluation-job
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='evaluate')



# Model Evaluation on validation set
evaluation_results = model.evaluate(valid_data,return_dict=True)

# Log scores using wandb.log()
wandb.log(evaluation_results)

# Finish the run
run.finish()


Sauvegarde des modèles et versionnement des modèles

Enfin, nous allons nous pencher sur la sauvegarde de modèles reproductibles avec W&B. Plus précisément, avec Artifacts.

W&B Artifacts

Pour sauvegarder les modèles et faciliter le suivi des différentes expériences, nous utiliserons wandb.artifacts. W&B Artifacts est un moyen de sauvegarder vos ensembles de données et vos modèles.
Dans une exécution, il y a trois étapes pour créer et sauvegarder un artefact de modèle.
  • · Créez un artefact vide avec wandb.Artifact().
  • · Ajoutez votre fichier modèle à l'artefact avec wandb.add_file().
  • · Appelez wandb.log_artifact() pour sauvegarder l'artefact.
# Save model
model.save(f"{config['model_name']}.h5")

# Initialize a new W&B run for saving the model, changing the job_type
run = wandb.init(project='Finetune-BERT-Text-Classification',
config=config,
group='BERT_EN_UNCASED',
job_type='save')


# Save model as Model Artifact
artifact = wandb.Artifact(name=f"{config['model_name']}", type='model')
artifact.add_file(f"{config['model_name']}.h5")
run.log_artifact(artifact)

# Finish W&B run
run.finish()


Aperçu rapide du tableau de bord de W&B

Des choses à noter :
  • Regroupement des expériences et des séries.
  • Visualisations de tous les journaux et paramètres d’entraînement.
  • Les visualisations des métriques du système pourraient être utiles lors de la formation sur des instances sur le cloud ou des machines GPU physiques.
  • Suivi des hyperparamètres sous forme de tableau.
  • Les artefacts : Versionnement et stockage des modèles.



Résumé et code de classification des tests BERT

J'espère que ce tutoriel pratique vous a été utile, et si vous avez lu jusqu'ici, j'espère que vous en avez retiré quelques points intéressants.
Le code complet de cet article se trouve ici.
Iterate on AI agents and models faster. Try Weights & Biases today.