Zum Inhalt springen

Sentiment Analyse mit BERT und TensorFlow

Das Bidirectional Encoder Representation from Transformers (kurz: BERT) Modell stammt von Google aus dem Jahr 2018. Zu dieser Zeit wurden vor allem Recurrent Neural Networks zur Textverarbeitung genutzt. In einem früheren Beitrag haben wir uns bereits im Detail damit beschäftigt, warum die sogenannten Transformer Modelle dafür deutlich besser geeignet sind.

Theoretische Grundlagen

Das BERT-Modell war eines der ersten Beispiele, wie Transformer Modelle für Natural Language Processing Aufgaben, wie beispielsweise Sentiment Analyse (ist eine Bewertung positiv oder negativ) oder allgemeiner zur Textklassifizierung genutzt wurde. Die Grundidee dahinter stammt aus dem Bereich des Transfer Learning. Das BERT-Modell wurde auf einen riesigen sprachlichen Datensatz vortrainiert, mit dem Ziel möglichst gut fehlende Wörter in einem Text vorhersagen zu können. Je nachdem welches BERT Modell wir genau nutzen, wurde es beispielsweise auf der kompletten englischen Wikipedia trainiert.

Das Bild zeigt ein BERT Trainingsbeispiel. In dem Text ist ein einzelnes Wort verdeckt und das Modell muss lernen dieses Wort anhand des Kontextes zu erraten.
Beispiel für BERT Trainingsaufgabe

Wie sich herausgestellt hat, können diese vortrainierten Modelle dann mit deutlich weniger Aufwand auf eine spezielle Anwendung „verfeinert“ werden und liefern dabei noch sehr gute Ergebnisse. In dem sogenannten Pre-Training auf dem große Datensatz wurde das grundlegende Verständnis für die Sprache, wie beispielsweise Grammatik oder Wortschatz erlernt. Im Fine-Tuning dann konzentriert sich das BERT-Modell ausschließlich auf den Anwendungsfall und liefert dadurch auch mit vergleichsweise wenigen Daten noch sehr gute Ergebnisse.

BERT für Sentiment Analyse

Der IMDb Datensatz von Kaggle enthält insgesamt 50.000 Film- und Serienkritiken und ein Label, welches beschreibt, ob es sich dabei um eine positive oder negative Rezension handelt.

import pandas as pd

movie_reviews = pd.read_csv("IMDB Dataset.csv")
print(f"Review: {movie_reviews.iloc[0]['review']}")
print()
print(f"Sentiment: {movie_reviews.iloc[0]['sentiment']}")

Out:
Review: One of the other reviewers has mentioned that after watching just 1 Oz episode you'll be hooked. They are right, as this is exactly what happened with me.<br /><br />The first thing that struck me about Oz was its brutality and unflinching scenes of violence, which set in right from the word GO. Trust me, this is not a show for the faint hearted or timid. This show pulls no punches with regards to drugs, sex or violence. Its is hardcore, in the classic use of the word.<br /><br />It is called OZ as that is the nickname given to the Oswald Maximum Security State Penitentary. It focuses mainly on Emerald City, an experimental section of the prison where all the cells have glass fronts and face inwards, so privacy is not high on the agenda. Em City is home to many..Aryans, Muslims, gangstas, Latinos, Christians, Italians, Irish and more....so scuffles, death stares, dodgy dealings and shady agreements are never far away.<br /><br />I would say the main appeal of the show is due to the fact that it goes where other shows wouldn't dare. Forget pretty pictures painted for mainstream audiences, forget charm, forget romance...OZ doesn't mess around. The first episode I ever saw struck me as so nasty it was surreal, I couldn't say I was ready for it, but as I watched more, I developed a taste for Oz, and got accustomed to the high levels of graphic violence. Not just violence, but injustice (crooked guards who'll be sold out for a nickel, inmates who'll kill on order and get away with it, well mannered, middle class inmates being turned into prison bitches due to their lack of street skills or prison experience) Watching Oz, you may become comfortable with what is uncomfortable viewing....thats if you can get in touch with your darker side.

Sentiment: positive

Diesen Datensatz wollen wir nutzen, um mithilfe von BERT Embedding ein Modell darauf zu spezialisieren, aus allgemeinen Filmrezensionen abzuleiten, ob der Person der Film gefallen hat oder nicht.

Datenaufbereitung

Zunächst einmal müssen wir alle Module importieren, die wir für diese Aufgabe benötigen werden.

# Regular imports
import numpy as np
import pandas as pd
import tqdm # for progress bar
import math
import random
import re

# Tensorflow Import
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow.keras import layers

# Bert Import for Tokenizer
import bert

Damit wir den Text nutzen können, müssen wir ihn entsprechend aufbereiten. Im ersten Schritt erstellen wir eine Funktion, die beispielsweise die Zeilenumbrüche (<br/>) und andere HTML Überbleibsel aus dem Text entfernt. In diesem Schritt filtern wir auch andere Textverunreinigungen mithilfe von Regular Expressions heraus.

TAG_RE = re.compile(r'<[^>]+>')

def remove_tags(text):
    return TAG_RE.sub('', text)

def preprocess_text(sen):
    # Removing html tags
    sentence = remove_tags(sen)

    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sentence)

    # Single character removal
    sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)

    # Removing multiple spaces
    sentence = re.sub(r'\s+', ' ', sentence)

    return sentence

# Clean all Reviews in DataFrame
reviews = []
sentences = list(movie_reviews['review'])
for sen in sentences:
    reviews.append(preprocess_text(sen))

Die Sentiment „positive“ und „negative“ werden wir ab jetzt mit den Zahlen 1 und 0 repräsentieren, da das Machine Learning Modell sowieso nur mit Zahlen umgehen kann und nicht mit Text.

# Save sentiments as dependent variable y
y = movie_reviews['sentiment']

# Set 1 for positive reviews and 0 for negative reviews
y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))

Die Kritiken müssen wir nun auch in Ganzzahlen übersetzen. Dazu nutzen wir den sogenannten Tokenizer von einem BERT-Modell. Er splittet den Satz in einzelne Tokens, also einzelne Wörter oder sogar Silben, und repräsentiert diese durch eine Zahl. Gleichzeitig legt er ein Dictionary an, das als Vokabelheft dient, sodass wir nach dem Modelltraining immer noch in der Lage sind ein Wort oder eine Silbe genau einer Zahl zuordnen zu können.

# Load Tokenizer and Model
BertTokenizer = bert.bert_tokenization.FullTokenizer
bert_layer = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1",
 trainable=False)
vocabulary_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
to_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
tokenizer = BertTokenizer(vocabulary_file, to_lower_case)

# Try Tokenizer
tokenizer.tokenize("don't be so rude")
Out:
['don', "'", 't', 'be', 'so', 'rude']

tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so rude"))
Out:
[2123, 2102, 2022, 2061, 12726]

Wir laden ein bestimmtes BERT-Modell von Tensorflow Hub. Das Training von Transformer und BERT-Modellen ist normalerweise sehr aufwendig und ressourcenintensiv. Vor allem wenn man mit solch großen Datensätzen hantiert. Von Tensorflow können wir kostenlos die vortrainierten Modelle von Google und anderen Unternehmen nutzen.

Anschließend tokenizen wir alle Filmkritiken in unserem Datensatz, sodass unsere Daten nur noch aus Zahlen und nicht mehr aus Text bestehen.

# Tokenize all reviews
def tokenize_reviews(text_reviews):
    return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_reviews))
tokenized_reviews = [tokenize_reviews(review) for review in reviews]

# Create list of list with review, sentiment, and length of review for each entry
reviews_with_len = [[review, y[i], len(review)]
 
                     for i, review in tqdm.tqdm(enumerate(tokenized_reviews))]

# Shuffle dataset
random.shuffle(reviews_with_len)

# Sort by length of review
reviews_with_len.sort(key=lambda x: x[2])

# Drop review
sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in tqdm.tqdm(reviews_with_len)]

Wir werden die Daten in einem Tensorflow Dataset speichern und an das Modell weitergeben. Das lässt das Modell unter anderem deutlich effizienter trainieren.

# Create Tensorflow datasets
processed_dataset = tf.data.Dataset.from_generator(lambda: sorted_reviews_labels, output_types=(tf.int32, tf.int32))

# Define batch size and cut datasets by batch size
BATCH_SIZE = 32
batched_dataset = processed_dataset.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))

Wie sonst auch unterteilen wir die Informationen in einen Trainings- und einen Testsatz. Dadurch können wir untersuchen, wie gut unser Modell auf noch nicht gesehene Daten generalisiert. Wir werden zehn Prozent der Daten als Testdatensatz nutzen.

TOTAL_BATCHES = math.ceil(len(sorted_reviews_labels) / BATCH_SIZE)
TEST_BATCHES = TOTAL_BATCHES // 10
batched_dataset.shuffle(TOTAL_BATCHES)
test_data = batched_dataset.take(TEST_BATCHES)
train_data = batched_dataset.skip(TEST_BATCHES)

Modellerstellung

Nun kommt der spannendste Teil dieser Anwendung: Wir definieren das Modell.

class TEXT_MODEL(tf.keras.Model):
    
    def __init__(self,
                 vocabulary_size,
                 embedding_dimensions=128,
                 cnn_filters=50,
                 dnn_units=512,
                 model_output_classes=2,
                 dropout_rate=0.1,
                 training=False,
                 name="text_model"):
        super(TEXT_MODEL, self).__init__(name=name)
        
        self.embedding = layers.Embedding(vocabulary_size,
                                          embedding_dimensions)
        self.cnn_layer1 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=2,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer2 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=3,
                                        padding="valid",
                                        activation="relu")
        self.cnn_layer3 = layers.Conv1D(filters=cnn_filters,
                                        kernel_size=4,
                                        padding="valid",
                                        activation="relu")
        self.pool = layers.GlobalMaxPool1D()
        
        self.dense_1 = layers.Dense(units=dnn_units, activation="relu")
        self.dropout = layers.Dropout(rate=dropout_rate)
        if model_output_classes == 2:
            self.last_dense = layers.Dense(units=1,
                                           activation="sigmoid")
        else:
            self.last_dense = layers.Dense(units=model_output_classes,
                                           activation="softmax")
    
    def call(self, inputs, training):
        l = self.embedding(inputs)
        l_1 = self.cnn_layer1(l) 
        l_1 = self.pool(l_1) 
        l_2 = self.cnn_layer2(l) 
        l_2 = self.pool(l_2)
        l_3 = self.cnn_layer3(l)
        l_3 = self.pool(l_3) 
        
        concatenated = tf.concat([l_1, l_2, l_3], axis=-1) # (batch_size, 3 * cnn_filters)
        concatenated = self.dense_1(concatenated)
        concatenated = self.dropout(concatenated, training)
        model_output = self.last_dense(concatenated)
        
        return model_output

Das Modell ist an sich ist eher unspektakulär. Wir nutzen den BERT-Tokenizer für das Embedding der Wörter und in der ersten Schicht des Modells. Anschließend lassen wir diese Inputs durch insgesamt drei Convolutional Blocks mit MaxPool Schichten laufen. Anschließend verringern wir die Anzahl der Neuronen über zwei Dense Schichten, sodass wir in der Output Schicht wieder die Anzahl der Sentimente haben. In unserem Fall haben wir nur zwei Arten der Kritik, nämlich positiv und negativ. Diese Klasse könnte jedoch auch für mehr als zwei Output Labels genutzt werden.

Bevor wir das Modell trainieren können müssen wir noch die Hyperparameter definieren und das Modell kompilieren.

# Hyperparameters
VOCAB_LENGTH = len(tokenizer.vocab)
EMB_DIM = 200
CNN_FILTERS = 100
DNN_UNITS = 256
OUTPUT_CLASSES = 2
DROPOUT_RATE = 0.2
NB_EPOCHS = 2

# Build Model
text_model = TEXT_MODEL(vocabulary_size=VOCAB_LENGTH,
                        embedding_dimensions=EMB_DIM,
                        cnn_filters=CNN_FILTERS,
                        dnn_units=DNN_UNITS,
                        model_output_classes=OUTPUT_CLASSES,
                        dropout_rate=DROPOUT_RATE)

# Compile Model
 text_model.compile(loss="binary_crossentropy",
                       optimizer="adam",
                       metrics=["accuracy"])

Jetzt sind wir soweit, dass wir das Modell für insgesamt zwei Epochen trainieren können.

text_model.fit(train_data, epochs=NB_EPOCHS)

Out:
Epoch 1/2
1407/1407 [==============================] - 463s 326ms/step - loss: 0.3029 - accuracy: 0.8667
Epoch 2/2
1407/1407 [==============================] - 452s 320ms/step - loss: 0.1293 - accuracy: 0.9531

Im Trainingsdatensatz haben wir bereits eine Genauigkeit von 95% erreicht. Um sicherzugehen, dass das Modell nicht nur die Trainingsdaten auswendig gelernt hat, schauen wir uns noch die Genauigkeit im Testdatensatz an, um zu sehen, ob es auch gut generalisieren kann.

text_model.evaluate(test_data)

Out:
156/156 [==============================] - 2s 14ms/step - loss: 0.3774 - accuracy: 0.8812

Auch hier erreichen wir eine zufriedenstelle Accuracy von 88,12 %.

# Test model on two individual reviews
test_reviews = ['This was an awesome movie. I watch it twice my time watching this beautiful movie if I have known it was this good',
                'One of the worst movies of all time. I cannot believe I wasted two hours of my life for this movie']
test_reviews_with_len = [[tokenize_reviews(test_reviews[0]), 
                          1, 
                          len(tokenize_reviews(test_reviews[0]))],
                         [tokenize_reviews(test_reviews[1]), 
                          0,  
                          len(tokenize_reviews(test_reviews[1]))]]
test_sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in tqdm.tqdm(test_reviews_with_len)]

predict_input = tf.data.Dataset.from_generator(lambda: test_sorted_reviews_labels, output_types=(tf.int32, tf.int32))
BATCH_SIZE = 2
test_batched_dataset = predict_input.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))

# Get model prediction
text_model.predict(test_batched_dataset)

Out: 
array([[0.9999305 ],[0.00232589]], dtype=float32)

Auch für komplett frei erfundene Kritiken klassifiziert das Modell zielsicher und hat erkannt, dass der erste Text eine positive Rezension ist und die zweite eine negative.

Das solltest Du mitnehmen

  • BERT-Modelle können für viele verschiedene NLP Anwendungen genutzt werden.
  • Dieses Beispiel hat gezeigt, wie man das BERT Embedding für die Textklassifizierung nutzen kann.
  • Auch ohne die BERT Encoding Layer liefert das reine Embedding bereits sehr gute Ergebnisse.

Andere Beiträge zum Thema BERT

  • Eine Auswahl von größeren und kleineren vortrainierten BERT-Modellen findest Du hier.
  • Wir nutzen zu großen Teilen den Code von Stackabuse und ergänzen diesen mit eigenen Kommentaren.
Cookie Consent mit Real Cookie Banner