Tutorial: BERT-Feinabstimmung für die Sentiment-Analyse
Tutorial: BERT-Feinabstimmung für die Sentiment-Analyse
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
A - Einleitung¶
In den letzten Jahren hat die NLP-Gemeinschaft viele Durchbrüche in der natürlichen Sprachverarbeitung erlebt, insbesondere den Wechsel zum Transfer-Lernen. Modelle wie ELMo, ULMFiT von fast.ai, Transformer und GPT von OpenAI haben es den Forschern ermöglicht, bei mehreren Benchmarks Spitzenergebnisse zu erzielen und der Community große vortrainierte Modelle mit hoher Leistung zur Verfügung zu stellen. Dieser Wandel im NLP wird als der ImageNet-Moment im NLP angesehen, ein Wandel im Computer Vision vor einigen Jahren, als untere Schichten von Deep-Learning-Netzwerken mit Millionen von Parametern, die für eine bestimmte Aufgabe trainiert wurden, wiederverwendet und für andere Aufgaben feinabgestimmt werden können, anstatt neue Netzwerke von Grund auf zu trainieren.
Einer der größten Meilensteine in der Entwicklung des NLP in letzter Zeit ist die Veröffentlichung von Googles BERT, die als Beginn einer neuen Ära im NLP beschrieben wird. In diesem Notizbuch werde ich das HuggingFace's Transformatoren
Bibliothek zur Feinabstimmung des vortrainierten BERT-Modells für eine Klassifizierungsaufgabe. Anschließend werde ich die Leistung des BERT mit einem Basismodell vergleichen, bei dem ich einen TF-IDF-Vektorisierer und einen Naive Bayes-Klassifikator verwende. Die Transformatoren
Bibliothek helfen uns bei der schnellen und effizienten Feinabstimmung des hochmodernen BERT-Modells und liefern eine Genauigkeitsrate 10% höher als beim Basismodell.
Referenz:
Zu verstehen Transformator (die Architektur, auf der BERT aufbaut) und lernen, wie man BERT implementiert, empfehle ich dringend die Lektüre der folgenden Quellen:
- Die Illustrierte BERT, ELMo und Co.: Ein sehr klarer und gut geschriebener Leitfaden zum Verständnis von BERT.
- Die Dokumentation der
Transformatoren
Bibliothek - BERT Feinabstimmung Tutorial mit PyTorch von Chris McCormick: Ein sehr detailliertes Tutorial, das zeigt, wie man BERT mit der HuggingFace PyTorch-Bibliothek verwendet.
B - Einrichtung¶
1. Wesentliche Bibliotheken laden¶
In [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. Datensatz¶
2.1. Datensatz herunterladen¶
In [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. Lastzugdaten¶
Die Zugdaten bestehen aus 2 Dateien, die jeweils 1700 beschwerdeführende/nicht beschwerdeführende Tweets enthalten. Alle Tweets in den Daten enthalten mindestens einen Hashtag einer Fluggesellschaft.
Wir laden die Trainingsdaten und beschriften sie. Da wir nur die Textdaten zur Klassifizierung verwenden, lassen wir unwichtige Spalten weg und behalten nur id
, tweet
und Etikett
Spalten.
In [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 | Etikett | |
---|---|---|---|
1988 | 24991 | Was für ein tolles Willkommen zurück. Lachhaft. Deplanin... | 1 |
1294 | 72380 | Sehr enttäuscht von @JetBlue heute Abend. Flug... | 0 |
1090 | 127893 | @united meine Freunde haben eine höllische Zeit... | 0 |
553 | 58278 | @united Alles, was ich mir zu Weihnachten wünsche, ist eine verlorene Tasche, die... | 0 |
2075 | 30695 | Ja, ich werde nicht lügen... ich bin sehr daran interessiert, es zu versuchen... | 1 |
Die gesamten Trainingsdaten werden nach dem Zufallsprinzip in zwei Gruppen aufgeteilt: eine Trainingsgruppe mit 90% der Daten und eine Validierungsgruppe mit 10% der Daten. Wir führen die Abstimmung der Hyperparameter mittels Kreuzvalidierung auf dem Trainingssatz durch und verwenden den Validierungssatz zum Vergleich der Modelle.
In [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. Lasttest-Daten¶
Die Testdaten enthalten 4555 Beispiele ohne Kennzeichnung. Etwa 300 Beispiele sind nicht-beschwerende Tweets. Unsere Aufgabe ist es, ihre id
und prüfen Sie manuell, ob unsere Ergebnisse korrekt sind.
In [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 | @AmericanAir Flug verspätet sich über 2 Stunden wegen... |
607 | 24101 | @SouthwestAir Ich bekomme immer noch diese Fehlermeldung... |
333 | 13179 | Ich warte am #SeaTac, um an Bord meines @JetBlue-Flugs zu gehen... |
2696 | 102948 | Ich hasse es, wenn ich durch das Sitzauswahlverfahren gehe... |
3585 | 135638 | Schande über Sie @AlaskaAir |
3. GPU für Schulungen einrichten¶
Google Colab bietet kostenlose GPUs und TPUs. Da wir ein großes neuronales Netzwerk trainieren werden, ist es am besten, diese Funktionen zu nutzen.
Eine GPU kann hinzugefügt werden, indem man das Menü aufruft und auswählt:
Laufzeit -> Laufzeittyp ändern -> Hardwarebeschleuniger: GPU
Dann müssen wir die folgende Zelle ausführen, um die GPU als Gerät anzugeben.
In [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")
Es sind 1 GPU(s) verfügbar. Name des Geräts: Tesla T4
C - Grundlinie: TF-IDF + Naive Bayes Klassifikator¶
In diesem Basisansatz verwenden wir zunächst TF-IDF, um unsere Textdaten zu vektorisieren. Dann verwenden wir das Naive-Bayes-Modell als Klassifikator.
Warum Naive Bayes? Ich habe verschiedene Algorithmen für maschinelles Lernen ausprobiert, darunter Random Forest, Support Vectors Machine und XGBoost, und festgestellt, dass Naive Bayes die beste Leistung erbringt. Unter Leitfaden für Scikit-learn Um den richtigen Schätzer zu wählen, wird auch vorgeschlagen, Naive Bayes für Textdaten zu verwenden. Ich habe auch versucht, die Dimensionalität mit Hilfe von SVD zu reduzieren, was jedoch nicht zu einer besseren Leistung führte.
1. Datenaufbereitung¶
1.1. Vorverarbeitung¶
Beim Bag-of-Words-Modell wird ein Text als Tasche seiner Wörter dargestellt, wobei Grammatik und Wortreihenfolge außer Acht gelassen werden. Daher wollen wir Stoppwörter, Interpunktionszeichen und Zeichen, die nicht viel zur Bedeutung des Satzes beitragen, entfernen.
In [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] Herunterladen des Pakets stopwords nach /root/nltk_data... [nltk_data] Das Paket stopwords ist bereits aktuell!
1.2. TF-IDF-Vektorisierer¶
In der Informationsbeschaffung, TF-IDF, kurz für Begriffshäufigkeit - inverse Dokumentenhäufigkeitist eine numerische Statistik, die wiedergeben soll, wie wichtig ein Wort für ein Dokument in einer Sammlung oder einem Korpus ist. Wir werden TF-IDF verwenden, um unsere Textdaten zu vektorisieren, bevor wir sie in Algorithmen für maschinelles Lernen einspeisen.
In [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)
CPU-Zeiten: user 5,47 s, sys: 519 ms, gesamt: 5.99 s Wandzeit: 6 s
2. Naive Bayes Klassifikator trainieren¶
2.1. Abstimmung der Hyperparameter¶
Wir werden die Kreuzvalidierung und den AUC-Score verwenden, um die Hyperparameter unseres Modells abzustimmen. Die Funktion get_auc_CV
gibt den durchschnittlichen AUC-Wert aus der Kreuzvalidierung zurück.
In [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()
Die MultinominalNB
Klasse haben nur einen Hypterparameter - alpha. Der nachstehende Code hilft uns, den Alpha-Wert zu finden, der uns den höchsten CV-AUC-Wert beschert.
In [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()
Bestes Alpha: 1.3
2.2. Auswertung der Validierungsmenge¶
Um die Leistung unseres Modells zu bewerten, berechnen wir die Genauigkeitsrate und den AUC-Wert unseres Modells für die Validierungsmenge.
In [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()
Durch die Kombination von TF-IDF und dem Naive-Bayes-Algorithmus erreichen wir eine Trefferquote von 72.65% auf dem Validierungssatz. Dieser Wert ist die Basisleistung und wird zur Bewertung der Leistung unseres BERT-Feinabstimmungsmodells verwendet.
In [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 Genauigkeit: 75,59%
D - Feinabstimmung BERT¶
1. Installieren Sie die Hugging Face Library¶
Die Transformer-Bibliothek von Hugging Face enthält PyTorch-Implementierungen von hochmodernen NLP-Modellen wie BERT (von Google), GPT (von OpenAI) ... und vortrainierte Modellgewichte.
In [1]:
#!pip Transformatoren installieren
2. Tokenisierung und Eingabeformatierung¶
Bevor wir unseren Text in Token umwandeln, führen wir eine leichte Bearbeitung durch, bei der wir Erwähnungen von Entitäten (z. B. @united) und einige Sonderzeichen entfernen. Der Grad der Verarbeitung ist hier viel geringer als bei früheren Ansätzen, da BERT mit den gesamten Sätzen trainiert wurde.
In [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
In [0]:
# Print sentence 0 print('Original: ', X[0]) print('Processed: ', text_preprocessing(X[0]))
Original: @united I'm having issues. Gestern habe ich für 24 Stunden nach dem geplanten Flug umgebucht, und jetzt kann ich mich nicht mehr anmelden und einchecken. Können Sie mir helfen? Bearbeitet: I'm having issues. Gestern habe ich 24 Stunden, nachdem ich fliegen sollte, umgebucht, und jetzt kann ich mich nicht mehr anmelden und einchecken. Können Sie mir helfen?
2.1. BERT Tokenizer¶
Um den vortrainierten BERT anzuwenden, müssen wir den von der Bibliothek bereitgestellten Tokenizer verwenden. Der Grund dafür ist, dass (1) das Modell ein bestimmtes, festes Vokabular hat und (2) der BERT-Tokenizer eine bestimmte Art hat, Wörter außerhalb des Vokabulars zu behandeln.
Darüber hinaus müssen wir am Anfang und am Ende jedes Satzes spezielle Token hinzufügen, alle Sätze auf eine einzige konstante Länge auffüllen und abschneiden und mit der "Aufmerksamkeitsmaske" explizit angeben, welche Token aufgefüllt werden sollen.
Die kodieren_plus
Methode von BERT tokenizer wird:
(1) unseren Text in Token aufteilen,
(2) fügen Sie die besondere [CLS]
und [SEP]
Wertmarken, und
(3) diese Token in Indizes des Tokenizer-Vokabulars umwandeln,
(4) Auffüllen oder Kürzen von Sätzen auf maximale Länge und
(5) Aufmerksamkeitsmaske erstellen.
In [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: #kodieren_plus
will: # (1) Tokenize the sentence # (2) Add the[CLS]
und[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]
und[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
Vor der Tokenisierung müssen wir die maximale Länge unserer Sätze angeben.
In [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)
Maximale Länge: 68
Lassen Sie uns nun unsere Daten tokenisieren.
In [0]:
# SpecifyMAX_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 functionvorverarbeitung_für_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 I'm having issues. Gestern habe ich für 24 Stunden nach dem geplanten Flug umgebucht, und jetzt kann ich mich nicht mehr anmelden und einchecken. Können Sie mir helfen? 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] Tokenisierung von Daten...
2.2. PyTorch DataLoader erstellen¶
Wir werden einen Iterator für unseren Datensatz mithilfe der Torch DataLoader-Klasse erstellen. Dadurch wird beim Training Speicherplatz gespart und die Trainingsgeschwindigkeit erhöht.
In [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. Unser Modell trainieren¶
3.1. BertClassifier erstellen¶
BERT-base besteht aus 12 Transformationsschichten, wobei jede Transformationsschicht eine Liste von Token-Einbettungen aufnimmt und am Ausgang die gleiche Anzahl von Einbettungen mit der gleichen versteckten Größe (oder Dimension) erzeugt. Die Ausgabe der letzten Transformatorschicht der [CLS]
Token wird als Merkmal der Sequenz verwendet, um einen Klassifikator zu speisen.
Die Transformatoren
Bibliothek hat die BertForSequenceClassification
Klasse, die für Klassifizierungsaufgaben konzipiert ist. Wir werden jedoch eine neue Klasse erstellen, damit wir unsere eigene Auswahl an Klassifikatoren angeben können.
Im Folgenden wird eine BertClassifier-Klasse mit einem BERT-Modell erstellt, um die letzte versteckte Schicht der [CLS]
Token und ein einschichtiges neuronales Netz mit Vorwärtskopplung als Klassifikator.
In [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): SetFalsch
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
CPU-Zeiten: user 38 µs, sys: 0 ns, gesamt: 38 µs Wandzeit: 40,1 µs
3.2. Optimierer & Lernratenplaner¶
Zur Feinabstimmung unseres Bert-Klassifikators müssen wir einen Optimierer erstellen. Die Autoren empfehlen folgende Hyper-Parameter:
- Chargengröße: 16 oder 32
- Lernrate (Adam): 5e-5, 3e-5 oder 2e-5
- Anzahl der Epochen: 2, 3, 4
Huggingface lieferte die run_glue.py Skript, ein Beispiel für die Implementierung des Transformatoren
Bibliothek. In dem Skript wird der AdamW-Optimierer verwendet.
In [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. Trainingsschleife¶
Wir werden unseren Bert-Klassifikator für 4 Epochen trainieren. In jeder Epoche trainieren wir unser Modell und bewerten seine Leistung anhand der Validierungsmenge. Genauer gesagt, werden wir:
Ausbildung:
- Entpacken Sie unsere Daten aus dem Dataloader und laden Sie die Daten auf die GPU
- Nullen der im vorherigen Durchgang berechneten Gradienten
- Durchführen eines Vorwärtsdurchlaufs zur Berechnung von Logits und Verlusten
- Führen Sie einen Rückwärtsdurchlauf zur Berechnung der Gradienten durch (
verlust.rückwärts()
) - Begrenzung der Norm der Gradienten auf 1,0, um "explodierende Gradienten" zu verhindern
- Aktualisieren Sie die Parameter des Modells (
optimizer.step()
) - Aktualisieren Sie die Lernrate (
scheduler.step()
)
Bewertung:
- Entpacken unserer Daten und Laden auf die GPU
- Vorwärtspass
- Berechnung der Verlust- und Genauigkeitsrate über die Validierungsmenge
Das nachstehende Skript ist mit den Einzelheiten unserer Schulungs- und Bewertungsschleife kommentiert.
In [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
Beginnen wir nun mit dem Training unseres BertClassifiers!
In [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)
Ausbildung beginnen... Epoche | 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 ---------------------------------------------------------------------- Epoche | Batch | Zugverlust | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Ausbildung abgeschlossen!
3.4. Auswertung der Validierungsmenge¶
Der Vorhersageschritt ähnelt dem Bewertungsschritt, den wir in der Trainingsschleife durchgeführt haben, ist aber einfacher. Wir führen einen Vorwärtsdurchlauf zur Berechnung von Logits durch und wenden Softmax an, um Wahrscheinlichkeiten zu berechnen.
In [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
In [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 Genauigkeit: 80,59%
Der Bert Classifer erreicht einen AUC-Wert von 0,90 und eine Genauigkeitsrate von 82,65% in der Validierungsmenge. Dieses Ergebnis ist 10 Punkte besser als die Basismethode.
3.5. Trainieren Sie unser Modell mit den gesamten Trainingsdaten¶
In [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)
Ausbildung beginnen... Epoche | 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 ---------------------------------------------------------------------- Epoche | Batch | Zugverlust | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Ausbildung abgeschlossen!
4. Vorhersagen auf dem Testsatz¶
4.1. Datenaufbereitung¶
Lassen Sie uns kurz auf unseren Testsatz zurückkommen.
In [0]:
test_data.sample(5)
Out[0]:
id | tweet | |
---|---|---|
471 | 18654 | Freunde und Familie: Fliegen Sie niemals mit @JetBlue. Absolut... |
1971 | 76265 | @DeltaAssist @rogerioad Ich hatte noch nie einen Pro... |
23 | 672 | Erster Flug seit Wochen. Ich zähle auf dich @Americ... |
2702 | 103263 | "@USAirways: You know that we can__t stay no m... |
135 | 5137 | @southwestair Hier am Flughafen SA beobachten die ... |
Bevor wir Vorhersagen für den Testsatz treffen, müssen wir die Verarbeitungs- und Kodierungsschritte wiederholen, die wir für die Trainingsdaten durchgeführt haben. Glücklicherweise haben wir die vorverarbeitung_für_bert
Funktion, um dies für uns zu tun.
In [0]:
# Run vorverarbeitung_für_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)
Tokenisierung von Daten...
4.2. Vorhersagen¶
In unserem Testsatz befinden sich etwa 300 nicht-negative Tweets. Daher werden wir die Entscheidungsschwelle so lange anpassen, bis wir etwa 300 nicht-negative Tweets haben.
Der von uns verwendete Schwellenwert beträgt 0,992, was bedeutet, dass Tweets mit einer Vorhersagewahrscheinlichkeit von mehr als 99,2% als positiv eingestuft werden. Dieser Wert ist im Vergleich zum Standardwert von 0,5 sehr hoch.
Nach einer manuellen Prüfung der Testmenge stelle ich fest, dass die Stimmungs-Klassifizierung hier sogar für Menschen schwierig ist. Daher wird uns ein hoher Schwellenwert sichere Vorhersagen liefern.
In [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())
Anzahl der Tweets mit nicht-negativer Vorhersage: 454
Nun werden wir 20 zufällige Tweets aus unseren Vorhersagen untersuchen. 17 von ihnen sind richtig, was zeigt, dass der BERT Classifier eine Genauigkeit von 0,85 erreicht.
In [0]:
output = test_data[preds==1] list(output.sample(20).tweet)
Out[0]:
[@Delta @DeltaAssist Delta streikt erneut. Sky Lounge am verkehrsreichsten Flughafen des Landes'an Wochenenden geschlossen. Dumb & cheap. miss you @united", '.@SouthwestAir hat honiggeröstete Erdnüsse zurückgebracht. Ist es traurig, dass diese Erkenntnis der Höhepunkt meines Tages sein könnte?#SmallThingsInLife', '@DeltaAssist Ich habe vor zwei Wochen eine E-Mail an kana@delta und contactus.delta@delta geschickt, um Probleme zu lösen, ohne eine Antwort zu erhalten, "Woman With Kicked Off Flight By @AlaskaAir Because So Has #Cancer Plans to Donate Her Family's Airfare http://t.co/Uj6rispWLb", "@united (2/2) I didn't break the bag. Wenn ich nicht'für das Einchecken bezahlen müsste, wäre ich'nicht so verärgert. Ich fliege lieber mit @AmericanAir @SouthwestAir usw.", "I've flog fast jede Fluggesellschaft & nie hatte ich eine bessere Erfahrung als mit @JetBlue. Qualität, Service, Komfort und Erschwinglichkeit. A++", '@JetBlue Beste Fluggesellschaft, für die man arbeiten kann, vermisse dich sehr #keepingitminty ', 'Überzeugte @firetweet, eine Last-Minute-Reise zu buchen, um mich in Austin tom! Seitdem singe ich das @VirginAmerica-Sicherheitslied. Armer Eric. ', '@AmericanAir wartet geduldig auf den Abflug von #DFW nach #ord http://t.co/j1oDSc6fht', 'Oh @JetBlue heute ist ein trauriger Tag für die B6-Treuen. Ich weiß, dass ihr eure neuen "Optionen" anpreist, aber euer Service und die Gebühren für das Gepäck SIND das, was euch großartig macht', 'Dinge, die an diesem Flug gut sind: @Gogo und die großartige Erfahrung von @VirginAmerica. Nicht so gut: der Geruch von Babykotze/verfaultem Thunfisch', '@USAirways @AmericanAir wird USAir vermissen :(', '@altonbrown @united Zeit, zu @VirginAmerica zu wechseln', 'Es ist nie der falsche Zeitpunkt für Chobani, @AmericanAir Admirals Club! #brokenrecord #toomanywasabipeas #lunch', "Auf meinem Flug habe ich das Telefon meines Menschen gestohlen, um @alaskaair 's neues Streaming IFE auszuprobieren. Es'ist gut! Schade, dass sie kein iPad besitzt.", "Can't wait for the @USAirways and @AmericanAir merger to be completed, what a hassle for the customer!", "@JetBlue I'm a broke college kid so $150 is a huge deal.", "Can't wait to fly back to the Bay Area tonight on @SouthwestAir flight 2256!!!!", 'Ich hänge an #SFO und warte darauf, dass sich der Nebel lichtet, um die nächste Verbindung @VirginAmerica nach #sxsw zu erreichen! #SXSW2015 #Austin', "@DeltaAssist irgendwie kann ich Flüge von 1308 zu einer, die isn't auf unbestimmte Zeit verzögert.... And get back to dc!"]
E - Schlussfolgerung¶
Durch das Hinzufügen eines einfachen Klassifizierers mit einer verborgenen Schicht eines neuronalen Netzwerks zu BERT und die Feinabstimmung von BERT können wir eine Leistung erzielen, die fast dem Stand der Technik entspricht und die 10 Punkte besser ist als die Basismethode, obwohl wir nur 3.400 Datenpunkte haben.
Und obwohl BERT sehr groß und kompliziert ist und Millionen von Parametern hat, müssen wir es nur in 2-4 Epochen feinabstimmen. Dieses Ergebnis kann erreicht werden, weil BERT auf der riesigen Menge trainiert wurde und bereits eine Menge Informationen über unsere Sprache kodiert. Eine beeindruckende Leistung, die in kurzer Zeit und mit einer geringen Datenmenge erzielt wurde, hat gezeigt, warum BERT eines der leistungsfähigsten NLP-Modelle ist, die derzeit verfügbar sind.