Skip to main content

Feinabstimmung von BERT für die Textklassifizierung

Ein leserfreundlicher Einstieg in die Feinabstimmung von BERT für die Textklassifizierung, tf.data und tf.Hub
Created on February 2|Last edited on February 2

Sektionen: 




Was ist BERT?

Bidirectional Encoder Representations from Transformers, besser bekannt als BERT, ist eine revolutionäre Arbeit von Google, welche die State-of-the-Art-Leistung für verschiedene NLP-Aufgaben steigerte und das Sprungbrett für viele andere revolutionäre Architekturen war.
Es ist keine Übertreibung zu sagen, dass BERT eine neue Richtung für den gesamten Bereich vorgegeben hat. Es zeigt die eindeutigen Vorteile der Verwendung von vortrainierten Modellen (die auf riesigen Datensätzen trainiert wurden) und des Transferlernens unabhängig von den nachgelagerten Aufgaben.
In diesem Bericht befassen wir uns mit der Verwendung von BERT für die Textklassifizierung und stellen eine Menge Code und Beispiele zur Verfügung, damit Sie sofort loslegen können. Wenn Sie sich die Primärquelle selbst ansehen möchten, finden Sie hier einen Link zu dem kommentierten Papier. 

BERT-Klassifizierungsmodell

BERT für die Textklassifizierung einrichten

Zuerst werden wir TensorFlow und TensorFlow Model Garden installieren:
import tensorflow as tf
print(tf.version.VERSION)
!git clone --depth 1 -b v2.4.0 https://github.com/tensorflow/models.git
Wir werden auch das Github Repo für TensorFlow Modelle klonen. Ein paar Dinge sind zu beachten:
  • -Tiefe 1, erhält Git beim Klonen nur die neueste Kopie der betreffenden Dateien. Dadurch können Sie viel Platz und Zeit sparen.
  • -b lässt uns nur einen bestimmten Zweig klonen.
Bitte stimmen Sie es mit Ihrer TensorFlow 2.x Version ab.
# 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
Hier drinnen regnet es Importe, Freunde.
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
Eine schnelle Überprüfung der verschiedenen installierten Versionen und Abhängigkeiten:
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")

Holen wir uns den Datensatz

Der Datensatz, den wir heute verwenden werden, wird über den Quora-Wettbewerb zur Klassifizierung unaufrichtiger Fragen auf Kaggle bereitgestellt.
Sie können das Trainingsset von Kaggle herunterladen oder den unten stehenden Link benutzen, um die train.csv aus diesem Wettbewerb herunterzuladen:

Dekomprimieren und Lesen der Daten in einen Pandas DataFrame:

Führen Sie dann Folgendes aus:
# 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
Nun gut. Lassen Sie uns nun diese Daten schnell in einer W&B-Tabelle visualisieren:



Lassen Sie uns forschen

Die Label Distribution

Es ist eine gute Idee, die Daten zu verstehen, mit denen man arbeitet, bevor man sich mit der Modellierung beschäftigt. Hier werden wir die Distribution unserer Bezeichnungen durchgehen. Wie lang sind unsere Datenpunkte, stellen Sie sicher, dass unsere Test- und Trainingsmengen gut verteilt sind, und einige andere vorbereitende Aufgaben. Zunächst wollen wir uns jedoch die Distribution der Bezeichnungen ansehen, indem wir sie ausführen.
print(df['target'].value_counts())
df['target'].value_counts().plot.bar()
plt.yscale('log');
plt.title('Distribution of Labels')

Label-Distribution

Wortlänge und Zeichenlänge

Führen wir nun ein paar Zeilen Code aus, um die Textdaten zu verstehen, mit denen wir hier arbeiten.
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)))))



Vorbereiten von Trainings- und Testdaten für unsere BERT-Textklassifizierungsaufgaben

Hier ein paar Anmerkungen zu unserem Ansatz:
💡
  • Wir werden kleine Teile der Daten verwenden, da das Training des gesamten Datensatzes Ewigkeiten dauern würde. Sie können natürlich auch mehr Daten verwenden, indem Sie train_size ändern
  • Da der Datensatz sehr unausgewogen ist, werden wir die gleiche Distribution im Trainings- und im Testdatensatz beibehalten, indem wir ihn auf der Grundlage der Bezeichnungen schichten. In diesem Abschnitt werden wir unsere Daten analysieren, um sicherzustellen, dass wir dabei gute Arbeit geleistet haben.

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)

Ermittlung der Wort- und Zeichenlänge für die abgetasteten Sätze

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()))
Mit anderen Worten, es sieht so aus, als ob der Trainings- und der Validierungssatz in Bezug auf das Klassenungleichgewicht und die unterschiedlichen Längen der Fragentexte ähnlich sind.


Analyse der Distribution der Länge des Fragentextes in Wörtern

# 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 der Distribution der Länge des Fragetextes in Zeichen

Wenn wir unsere Trainings- und Validierungssets untersuchen, wollen wir auch prüfen, ob die Länge des Fragentextes in den beiden Sets weitgehend ähnlich ist. Eine annähernd ähnliche Distribution ist im Allgemeinen eine gute Idee, um eine Verzerrung oder Überanpassung unseres Modells zu vermeiden.
# 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')

Und das ist es auch. Sogar die Distribution der Fragelänge in Wörtern und Zeichen ist sehr ähnlich. Bis jetzt sieht es nach einer guten Aufteilung zwischen Zug und Test aus.


Zähmung der Daten

Als Nächstes soll der Datensatz auf der CPU erstellt und vorverarbeitet werden:
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
Na gut. Let's BERT.

Let's BERT: Holen Sie sich das vortrainierte BERT-Modell vom TensorFlow Hub


Quelle
Wir werden den unverpackten BERT aus dem tfhub verwenden.
Um den Text für die BERT-Schicht vorzubereiten, müssen wir zunächst unsere Wörter tokenisieren. Der Tokenizer ist hier als Modell-Asset vorhanden und wird auch das Uncasing für uns übernehmen.
Einstellung aller Parameter in Form eines Verzeichnisses, sodass Änderungen, falls erforderlich, hier vorgenommen werden können.

# 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
}


Abrufen der BERT-Schicht und des Tokenizers.

# 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)


Prüfen einiger Trainingsbeispiele und ihrer tokenisierten IDs

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]


Machen wir die Daten bereit: Tokenisieren und Vorverarbeiten von Text für BERT

Jede Zeile des Datensatzes besteht aus dem Bewertungstext und seiner Bezeichnung. Die Vorverarbeitung der Daten besteht in der Umwandlung von Text in BERT-Eingangsmerkmale:
  • Eingabe Wort-IDs: Ausgabe unseres Tokenizers, der jeden Satz in eine Reihe von Token-IDs umwandelt.
  • Eingabemasken: Da wir alle Sequenzen auf 128 auffüllen (maximale Sequenzlänge), ist es wichtig, dass wir eine Art Maske erstellen, um sicherzustellen, dass diese Auffüllungen nicht mit den eigentlichen Text-Token interferieren. Daher müssen wir eine Eingabemaske erzeugen, die die Auffüllungen blockiert. Die Maske hat den Wert 1 für echte Zeichen und den Wert 0 für Auffüllungszeichen. Nur echte Zeichen werden berücksichtigt.
  • Segment-IDs: Da es bei unserer Aufgabe der Textklassifizierung nur eine Sequenz gibt, sind die segment_ids/input_type_ids im Wesentlichen nur ein Vektor von 0s.
Bert wurde auf zwei Aufgaben trainiert:
  1. füllen Sie zufällig maskierte Wörter aus einem Satz aus.
  2. Bei zwei Sätzen: Welcher Satz kam zuerst?
# 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)

  • Sie möchten Dataset.map verwenden, um diese Funktion auf jedes Element des Datensatzes anzuwenden. Dataset.map wird im Graphmodus ausgeführt und Graph-Tensoren haben keinen Wert.
  • Im Graphmodus können Sie nur TensorFlow Ops und Funktionen verwenden.
Sie können diese Funktion also nicht direkt zuordnen: Sie müssen sie in eine tf.py_function einpacken. Die tf.py_function übergibt reguläre Tensoren (mit einem Wert und einer .numpy()-Methode, um darauf zuzugreifen) an die umhüllte Python-Funktion.

Einpacken der Python-Funktion in ein TensorFlow-op für eifrige Ausführung

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)
Der endgültige Datenpunkt, der an das Modell übergeben wird, hat das Format eines Verzeichnisses als x und labels (das Verzeichnis hat Schlüssel, die natürlich übereinstimmen sollten).

Lassen Sie die Daten fließen: Erstellen der endgültigen Eingabe-Pipeline mit tf.data



Anwendung der Transformation auf unsere Trainings- und Testdatensätze

# 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))
Die resultierenden tf.data.Datasets liefern (Features, Labels) Paare, wie von keras.Model.fit erwartet
# train data spec, we can finally see the input datapoint is now converted to the
#BERT specific input tensor
train_data.element_spec


Erstellen, Trainieren & Tracking unseres BERT-Klassifizierungsmodells.

Lassen Sie uns unseren Weg zum Ruhm modellieren!!!

Das Modell erstellen

Der BERT-Layer verfügt über zwei Ausgänge:
  • Ein pooled_output der Form [batch_size, 768] mit Darstellungen für die gesamten Eingabesequenzen.
  • Ein sequence_output der Form [batch_size, max_seq_length, 768] mit Repräsentationen für jedes Eingabe-Token (im Kontext).
Bei der Klassifizierungsaufgabe geht es nur um den 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


Schulung Ihres Modells

# 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()

Modell Zusammenfassung
Zusammenfassung der Modellarchitektur
Ein Nachteil des tf-Hubs ist, dass wir das gesamte Modul als Layer in keras importieren, wodurch wir die Parameter und Layer in der Modellzusammenfassung nicht sehen.
tf.keras.utils.plot_model(model=model, show_shapes=True, dpi=76, )

Auf der offiziellen tfhub-Seite heißt es: »Alle Parameter des Moduls können trainiert werden, und die Feinabstimmung aller Parameter ist die empfohlene Praxis.« Daher werden wir fortfahren und das gesamte Modell trainieren, ohne etwas einzufrieren

Experiment-Tracking

Da Sie hier sind, bin ich sicher, dass Sie eine gute Vorstellung von Weights und Biases haben, aber wenn nicht, dann lesen Sie weiter :)
Um mit der Verfolgung des Experiments zu beginnen, werden wir 'Durchläufe' auf W&B erstellen,
wandb.init(): Sie initialisiert den Durchlauf mit grundlegenden Projektinformationsparametern:
  • Projekt: Der Projektname erstellt eine neue Projekt-Registerkarte, auf der alle Experimente für dieses Projekt verfolgt werden.
  • Konfig: Ein Verzeichnis mit allen Parametern und Hyperparametern, die wir verfolgen wollen
  • group: optional, aber es würde uns helfen, später nach verschiedenen Parametern zu gruppieren
  • job_type: zur Beschreibung des Auftragstyps, der später bei der Gruppierung verschiedener Experimente hilfreich ist, z. B. »train«, »evaluate« usw.
# 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')
Um die verschiedenen Metriken zu protokollieren, verwenden wir einen einfachen Callback, der von W&B bereitgestellt wird.
WandCallback() : https://docs.wandb.ai/guides/integrations/keras
Ja, es ist so einfach wie das Hinzufügen eines Rückrufs :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()



Einige Trainingsmetriken und Diagramme





Lassen Sie uns evaluieren

Führen wir eine Bewertung mit der Validierungsmenge durch und protokollieren die Ergebnisse mit W&B.
wandb.log(): Protokolliert ein Verzeichnis mit Skalaren (Metriken wie Genauigkeit und Verlust) und jede andere Art von wandb-Objekt. Hier wird das Auswertungsverzeichnis so übergeben, wie es ist, und es wird protokolliert. t.
# 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()


Speichern der Modelle und Modellversionierung

Schließlich werden wir uns mit dem Speichern von reproduzierbaren Modellen mit W&B beschäftigen. Nämlich mit Artefakten.

W&B Artefakte

Um die Modelle zu speichern und die Nachverfolgung verschiedener Experimente zu erleichtern, werden wir wandb.artifacts verwenden. W&B-Artefakte sind eine Möglichkeit, Ihre Datensätze und Modelle zu speichern.
Innerhalb eines Durchlaufs gibt es drei Schritte zum Erstellen und Speichern eines Modellartefakts.
  • Erstellen Sie ein leeres Artefakt mit wandb.Artifact().
  • Fügen Sie Ihre Modelldatei mit wandb.add_file() zum Artefakt hinzu.
  • Aufruf von wandb.log_artifact() zum Speichern des Artefakts
# 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()


Kurzer Einblick in das W&B Dashboard

Zu beachtende Punkte:
  • Gruppierung von Experimenten und Durchläufen.
  • Visualisierungen aller Trainingsprotokolle und Metriken.
  • Visualisierungen für Systemmetriken könnten beim Training auf Cloud-Instanzen oder physischen GPU-Maschinen nützlich sein
  • Hyperparameter-Verfolgung in Tabellenform.
  • Artefakte: Versionierung und Speicherung von Modellen.



BERT Test Klassifizierung Zusammenfassung & Code

Ich hoffe, dass dieses praktische Tutorial für Sie nützlich war, und wenn Sie bis hierher gelesen haben, hoffe ich, dass Sie einige gute Punkte daraus mitnehmen können.
Der vollständige Code dieses Beitrags ist hier zu finden
Iterate on AI agents and models faster. Try Weights & Biases today.