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
Comment
Sektionen:
Sektionen: Was ist BERT? BERT für die Textklassifizierung einrichtenHolen wir uns den DatensatzLassen Sie uns forschenZähmung der DatenLet's BERT: Holen Sie sich das vortrainierte BERT-Modell vom TensorFlow HubLassen Sie die Daten fließen: Erstellen der endgültigen Eingabe-Pipeline mit tf.dataErstellen, Trainieren & Tracking unseres BERT-Klassifizierungsmodells. Speichern der Modelle und ModellversionierungBERT Test Klassifizierung Zusammenfassung & Code
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 tfprint(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 npimport tensorflow as tfimport tensorflow_hub as hubimport syssys.path.append('models')from official.nlp.data import classifier_data_libfrom official.nlp.bert import tokenizationfrom official.nlp import optimizationimport matplotlib.pyplot as plt%matplotlib inlineimport seaborn as snssns.set()import wandbfrom 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 LINKimport numpy as npimport pandas as pdfrom sklearn.model_selection import train_test_splitdf = 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 SETtrain_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 SETvalid_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 SETtrain_df['question_text'].apply(lambda x: len(x)).plot(kind='hist');plt.yscale('log');plt.title('Distribution of question text length in characters')

# VALIDATION SETvalid_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 setfor 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
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 parametersconfig = {'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/2bert_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 nottokenizer = 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:
- füllen Sie zufällig maskierte Wörter aus einem Satz aus.
- 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 earlierdef 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_libParameters:text: Input text stringlabel: label associated with the textlabel_list: (list) all possible labelsmax_seq_length: (int) maximum sequence length set for berttokenizer: the tokenizer object instantiated by the files in model assetsReturns:feature.input_ids: The token ids for the input text stringfeature.input_masks: The padding mask generatedfeature.segment_ids: essentially here a vector of 0s since classificationfeature.label_id: the corresponding label id from lable_list [0, 1] here"""# since we only have 1 sentence for classification purpose, textr_b is Noneexample = classifier_data_lib.InputExample(guid = None,text_a = text.numpy(),text_b = None,label = label.numpy())# since only 1 example, the index=0feature = 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 stringReturns:A tuple of a dictionary and a corresponding label_id with it. The dictionarycontains 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 datasetswith tf.device('/cpu:0'):# traintrain_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))# validvalid_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 tensortrain_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 Headdef 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 complexitydrop = tf.keras.layers.Dropout(config['dropout'])(pooled_output)output = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(drop)# inputs coming from the functionmodel = 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 modelmodel = create_model()
# using adam with a lr of 2*(10^-5), loss as binary cross entropy as only# 2 classes and similarly binary accuracymodel.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 runrun = 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 changeepochs = 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-jobrun = wandb.init(project='Finetune-BERT-Text-Classification',config=config,group='BERT_EN_UNCASED',job_type='evaluate')# Model Evaluation on validation setevaluation_results = model.evaluate(valid_data,return_dict=True)# Log scores using wandb.log()wandb.log(evaluation_results)# Finish the runrun.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 modelmodel.save(f"{config['model_name']}.h5")# Initialize a new W&B run for saving the model, changing the job_typerun = wandb.init(project='Finetune-BERT-Text-Classification',config=config,group='BERT_EN_UNCASED',job_type='save')# Save model as Model Artifactartifact = wandb.Artifact(name=f"{config['model_name']}", type='model')artifact.add_file(f"{config['model_name']}.h5")run.log_artifact(artifact)# Finish W&B runrun.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.
Add a comment
Iterate on AI agents and models faster. Try Weights & Biases today.
