Die LAMBADA-Methode: Wie benutzt man Datenaugmentierung in NLU?

Februar 2022
November 2020

Bessere NLG und NLU mit Datenaugmentierung

Einer unserer früheren Artikel befasste sich mit der LAMBADA-Methode. Diese nutzt Natural Language Generation (NLG), um *Trainings-Utterances für eine Natural Language Understanding (NLU)-Aufgabe zu generieren, genauer gesagt für die *Intent-Klassifikation. In diesem Tutorial führen wir Sie durch den Code, um unsere PoC-Implementierung von der LAMBADA-Methode bzw. von Datenaugmentierung zu reproduzieren. 

Bevor Sie mit diesem Tutorial fortfahren, schlagen wir vor, einen Blick auf unseren Artikel zu werfen, in dem wir die grundlegenden Ideen und Konzepte ausführlicher erläutern, die von der LAMBADA-Methode angewendet werden. In diesem Tutorial stellen wir wesentliche Methoden in einem COLAB notebook zur Verfügung. Insgesamt erklären wir einige der Kernpunkte des Codes und demonstrieren, wie Sie die Parameter so anpassen können, dass sie Ihren Anforderungen entsprechen, während die weniger wichtigen Teile weggelassen werden. Sie können das Notebook über Ihr Google-Konto kopieren, um dem Code zu folgen. Für Training und Tests können Sie Ihre eigenen Daten einsetzen oder von uns bereitgestellte Daten verwenden. 

Datenaugmentierung in NLU: Step 1 - Einrichten der Umgebung

Wir verwenden distilBERT als Klassifikationsmodell und GPT-2 als Textgenerierungsmodell. Für beide laden wir vortrainierte Gewichte und justieren sie nach. Im Falle von GPT-2 verwenden wir die Huggingface Transfomers-Bibliothek, um ein vortrainiertes Modell zu laden und es anschließend weiter zu verfeinern. Zum Laden und Finetuning von distilBERT verwenden wir Ktrain, eine Bibliothek, die ein High-Level-Interface für Sprachmodelle bietet, so dass wir uns nicht mehr um Tokenisierung und andere Pre-Processing-Aufgaben kümmern müssen.

Zunächst installieren wir beide Bibliotheken in unserer COLAB runtime:

    
    
    !pip install ktrain
    !pip install transformers
    

Chatbots mit Datenaugmentierung: Step 2 - Daten

Wir verwenden einen der vorgelabelten Chitchat-Datensätze von Microsofts Azure QnA Maker. Als nächstes teilen wir den Chitchat-Datensatz so auf, dass wir zehn *Intents mit jeweils zehn Utterances als einen anfänglichen Trainingsdatensatz und die restlichen 1047 Samples als einen Testdatensatz erhalten. Im Folgenden verwenden wir den Testdatensatz, um die verschiedenen Intent-Klassifikatoren zu vergleichen, die wir in diesem Tutorial trainieren.

Anschließend laden wir die Trainingsdaten aus der Datei  train.csv und teilen sie so auf, dass wir sechs Utterances pro Intent für das Training und vier Utterances pro Intent für die Validierung erhalten.

    
    
    NUMBER_OF_TRAINING_UTTERANCES = 6
    import pandas
    from sklearn.model_selection import train_test_split
    data_train = pandas.read_csv('train.csv')
    intents = data_train['intent'].unique()
    X_train = []
    X_valid = []
    y_train = []
    y_valid = []
    for intent in intents:
        intent_X_train, intent_X_valid, intent_y_train, intent_y_valid = train_test_split(
        data_train[data_train['intent'] == intent]['utterance'],
            data_train[data_train['intent'] == intent]['intent'],
            train_size=NUMBER_OF_TRAINING_UTTERANCES,
            random_state=43
        )
        X_train.extend(intent_X_train)
        X_valid.extend(intent_X_valid)
        y_train.extend(intent_y_train)
        y_valid.extend(intent_y_valid)            
    
    

NLU mit LAMBADA-Methode: Step 3 – Training des ursprünglichen Intent-Klassifikators

Wir downloaden das vortrainierte distilBERT-Modell, transformieren die Trainings- und Validierungsdaten von reinem Text in das für unser Modell gültige Format und initialisieren ein learner-Objekt, das in KTrain zum Trainieren des Modells verwendet wird.

    
    import ktrain
		from ktrain import text
    distil_bert = text.Transformer('distilbert-base-cased', maxlen=50, classes=intents)
    processed_train = distil_bert.preprocess_train(X_train, y_train)
		processed_test = distil_bert.preprocess_test(X_valid, y_valid)
    model = distil_bert.get_classifier()
		learner = ktrain.get_learner(model, train_data=processed_train, val_data=processed_test, batch_size=10)            
    

Jetzt ist es an der Zeit, das Modell zu trainieren. Wir speisen die Trainingsdaten mehrfach in das Netzwerk ein, spezifiziert durch die Anzahl der Epochen. Zu Beginn sollten beide überwachten Metriken, also die Verlustfunktion (Abnahme) und die Genauigkeit (Zunahme), eine Verbesserung des Modells mit jeder vergangenen Epoche anzeigen. Nachdem das Modell jedoch eine gewisse Zeit lang trainiert wurde, wird der Validierungsverlust zunehmen und die Validierungsgenauigkeit fallen. Dies ist eine Folge von Overfitting der Trainingsdaten, und es ist an der Zeit, die Einspeisung derselben Daten in das Netzwerk zu beenden.

Die optimale Anzahl von Epochen hängt von Ihrem Datensatz, Ihrem Modell und Ihren Trainingsparametern ab. Wenn Sie die richtige Anzahl von Epochen im Voraus nicht kennen, können Sie eine hohe Anzahl von Epochen verwenden und Checkpoints aktivieren, indem Sie den Parameter checkpoint_folder setzen, um anschließend das Modell mit der besten Performance auszuwählen.

    
    N_TRAINING_EPOCHS = 12
    learner.fit_onecycle(5e-5, N_TRAINING_EPOCHS)            
    
    
    begin training using onecycle policy with max lr of 5e-05...
    Train for 6 steps, validate for 2 steps
    Epoch 1/12
    6/6 [==============================] - 7s 1s/step - loss: 2.3088 - accuracy: 0.1167 - val_loss: 2.3236 - val_accuracy: 0.1000
    Epoch 2/12
    6/6 [==============================] - 0s 68ms/step - loss: 2.2913 - accuracy: 0.1333 - val_loss: 2.3084 - val_accuracy: 0.1000
    Epoch 3/12
    6/6 [==============================] - 0s 68ms/step - loss: 2.2728 - accuracy: 0.1167 - val_loss: 2.2741 - val_accuracy: 0.1000
    Epoch 4/12
    6/6 [==============================] - 0s 66ms/step - loss: 2.2039 - accuracy: 0.4167 - val_loss: 2.1981 - val_accuracy: 0.4500
    Epoch 5/12
    6/6 [==============================] - 0s 68ms/step - loss: 2.0552 - accuracy: 0.7333 - val_loss: 2.0282 - val_accuracy: 0.6000
    Epoch 6/12
    6/6 [==============================] - 0s 66ms/step - loss: 1.7596 - accuracy: 0.9000 - val_loss: 1.7276 - val_accuracy: 0.7500
    Epoch 7/12
    6/6 [==============================] - 0s 66ms/step - loss: 1.3359 - accuracy: 0.9667 - val_loss: 1.4421 - val_accuracy: 0.8250
    Epoch 8/12
    6/6 [==============================] - 0s 67ms/step - loss: 0.9690 - accuracy: 1.0000 - val_loss: 1.2494 - val_accuracy: 0.8500
    Epoch 9/12
    6/6 [==============================] - 0s 67ms/step - loss: 0.7366 - accuracy: 1.0000 - val_loss: 1.0965 - val_accuracy: 0.8750
    Epoch 10/12
    6/6 [==============================] - 0s 68ms/step - loss: 0.5735 - accuracy: 1.0000 - val_loss: 1.0089 - val_accuracy: 0.8750
    Epoch 11/12
    6/6 [==============================] - 0s 67ms/step - loss: 0.5007 - accuracy: 1.0000 - val_loss: 0.9680 - val_accuracy: 0.8750
    Epoch 12/12
    6/6 [==============================] - 0s 69ms/step - loss: 0.4451 - accuracy: 1.0000 - val_loss: 0.9504 - val_accuracy: 0.8750            
    

Um die Leistung unseres trainierten Klassifikators zu überprüfen, verwenden wir unsere Testdaten in der Datei eval.csv.

    
    import numpy

    data_test = pandas.read_csv('eval.csv')
    test_intents = data_test["intent"].tolist()
    test_utterances = data_test["utterance"].tolist()

    predictions = predictor.predict(test_utterances)

    np_test_intents = numpy.array(test_intents)
    np_predictions = numpy.array(predictions)

    result = (np_test_intents == np_predictions)

    print("Accuracy: {:.2f}%".format(result.sum()/len(result)*100))
    

Beachten Sie, dass wir dank des KTrain-Interface die Liste der Utterances einfach in den Predictor einspeisen können, ohne die ursprünglichen Strings vorher verarbeiten zu müssen. Als Ausgabe erhalten wir die Genauigkeit unseres Klassifikators:

Accuracy: 84.24%

NLU mit Datenaugmentierung: Step 4 – Finetuning von GPT-2 zur Erzeugung von Utterances

Zum Finetuning von GPT-2 verwenden wir ein Python Skript, das von Huggingface auf deren Github-Repository zur Verfügung gestellt wird. Unter anderem spezifizieren wir die folgenden Parameter:

  • das vortrainierte Modell, das wir verwenden wollen (gpt2-medium). Größere Modelle erzeugen in der Regel bessere Textausgaben. Bitte beachten Sie, dass diese Modelle während des Trainings sehr viel Speicherplatz benötigen. Stellen Sie also sicher, dass Sie ein Modell auswählen, das in Ihren (GPU-)Speicher passt.
  • die Anzahl der Epochen. Dieser Parameter gibt an, wie oft die Trainingsdaten durch das Netzwerk gespeist werden. Ist die Anzahl der Epochen zu klein, lernt das Modell nicht, nützliche Äußerungen zu generieren. Ist die Anzahl andererseits zu groß gewählt, wird das Modell wahrscheinlich überladen und die Variabilität der generierten Textdaten ist begrenzt - das Modell wird sich im Grunde nur an die Trainingsdaten erinnern. 
  • die batch size. Diese bestimmt, wie viele Utterances parallel für das Training verwendet werden. Je größer die batch size, desto schneller das Training, größere batch sizes benötigen jedoch mehr Speicherplatz.
  • die Blockgröße. Die Blockgröße definiert eine Obergrenze für die Anzahl der betrachteten Tokens aus jeder verwendeten Trainingsdateninstanz. Stellen Sie sicher, dass diese Anzahl ausreicht, damit Utterances nicht abgeschnitten werden.
    
    !python finetune_gpt.py \
        --output_dir='/content/transformers/output' \
        --model_type=gpt2-medium \
        --model_name_or_path=gpt2-medium \
        --num_train_epochs=3.0 \
        --do_train \
        --train_data_file=/content/train.csv \
        --per_gpu_train_batch_size=4 \
        --block_size=50 \
        --gradient_accumulation_steps=1 \
        --line_by_line \
        --overwrite_output_dir            
		

Let’s load our model and generate some utterances! To trigger the generation of new utterances for a specific intent we provide the model with this intent as seed ('<intent>,', e.g. ‘inform_hungry,’).</intent>

 		
    from transformers import GPT2Tokenizer, TFGPT2LMHeadModel

    tokenizer = GPT2Tokenizer.from_pretrained("gpt2-medium")
    model = TFGPT2LMHeadModel.from_pretrained('/content/transformers/output/', pad_token_id=tokenizer.eos_token_id, from_pt=True)
		input_ids = tokenizer.encode('inform_hungry,', return_tensors='tf')
		sample_outputs = model.generate(
        input_ids,
        do_sample=True, 
        max_length=50, 
        top_k=1, 
        top_p=0.9, 
        num_return_sequences=10
    )

    print("Output:\n" + 100 * '-')
    for i, sample_output in enumerate(sample_outputs):
    print("{}: {}".format(i, tokenizer.decode(sample_output, skip_special_tokens=True)))
  	
    
    Output:------------------------------------------------------------------------- 
    0: inform_hungry,I want a snack!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    1: inform_hungry,I want to eat!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    2: inform_hungry,I want some food!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
    3: inform_hungry,I'm so hungry I could eat!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!...            
    

Das sieht gut aus! Die künstlich erzeugten Utterances passen zum Intent, aber um eine nützliche Ergänzung zu sein und unser Modell zu verbessern, müssen sich diese Utterances von denen unterscheiden, die für das Training verwendet werden. Die Trainingsdaten für die Intent inform_hungry waren die folgenden:

    
    inform_hungry,I want a snack
    inform_hungry,I am very hungry
    inform_hungry,I'm hangry
    inform_hungry,Need food
    inform_hungry,I want to eat
    inform_hungry,I'm a bit peckish
    inform_hungry,My stomach is rumbling
    inform_hungry,I'm so hungry I could eat a horse
    inform_hungry,I'm feeling hangry
    inform_hungry,I could eat            
		

Wir sehen, dass die beiden Utterances " I want some food" und "I'm so hungry I could eat" nicht Teil der Trainingsdaten sind.

Wenn wir mit unseren generierten Utterances nicht zufrieden sind, weil sie alle sehr ähnlich sind oder, wenn sie nicht dem zugrunde liegenden Intent entsprechen, können wir die Variabilität des generierten Outputs anpassen, indem wir die folgenden Parameter modifizieren:

  • do_sample. Dieser Parameter muss auf True gesetzt werden, sonst gibt das Modell immer wieder den gleichen Output zurück.
  • oben_k. Dieser Parameter gibt die Anzahl der verschiedenen Token an, die für jeden Schritt des Samples berücksichtigt werden. Je höher Sie diesen Parameter setzen, desto vielfältiger wird die Ausgabe sein.
  • nach oben_p. Dieser Parameter gibt die kumulative Wahrscheinlichkeit der Token an, die am wahrscheinlichsten für die Stichprobe in Betracht gezogen werden. Beispielsweise wird bei top_p = 0,92 aus 92% der wahrscheinlichsten Wörter eine Stichprobe gezogen. Je höher top_p, desto vielfältiger ist die Ausgabe. Der maximale Wert ist 1.

Step 5 – Erzeugen und Filtern neuer Utterances

Wir erzeugen jetzt die neuen Utterances für alle Intents. Um eine ausreichend große Probe zu haben, aus der wir die besten Utterances auswählen können, erzeugen wir 200 pro Intent.

    
    NUMBER_OF_GENERATED_UTTERANCES_PER_INTENT = 200
    def generate_utterances_df(n_generated, tokenizer, model, intent):
      input_ids = tokenizer.encode(intent + ',', return_tensors='tf')
      sample_outputs = model.generate(
        input_ids,
        do_sample=True, 
        max_length=50, 
        top_k=n_generated, 
        top_p=0.92, 
        num_return_sequences=n_generated
      )

      list_of_intent_and_utterances = [
        (
            intent,
            tokenizer.decode(sample_output, skip_special_tokens=True)[len(intent)+1:]
        )
        for sample_output in sample_outputs
      ]

      return pandas.DataFrame(list_of_intent_and_utterances, columns=['intent', 'utterance'])
      intents = data_train["intent"].unique()

      generated_utterances_df = pandas.DataFrame(columns=['intent', 'utterance'])

      for intent in intents:
        print("Generating for intent " + intent)
        utterances_for_intent_df = generate_utterances_df(NUMBER_OF_GENERATED_UTTERANCES_PER_INTENT, tokenizer, model, intent)
        generated_utterances_df = generated_utterances_df.append(utterances_for_intent_df)            
    

Nach einer Weile sind die Daten generiert, und wir können sie uns genauer ansehen. Zunächst verwenden wir unseren alten DestilBERT-Klassifikator, um den Intent für alle generierten Utterances vorherzusagen. Außerdem beobachten wir die Vorhersagewahrscheinlichkeit, die das Confidence Level jeder einzelnen Vorhersage unseres Modells angibt.

    
    predictions_for_generated = numpy.array(predictor.predict(generated_data['utterance'].tolist(), return_proba=False))
    proba_for_predictions_for_gen = predictor.predict(generated_data['utterance'].tolist(), return_proba=True)
    predicted_proba = numpy.array([max(probas) for probas in proba_for_predictions_for_gen])

    generated_data_predicted = pandas.DataFrame({"intent": generated_data['intent'],
                                                 "utterance": generated_data['utterance'],
                                                 "predicted_intent": predictions_for_generated,
                                                 "prediction_proba": predicted_proba})
    
intent utterances predicted_intent predicted_proba
0 body_related_question Do you chew? body_related_question 0.701058
1 body_related_question Do you have a stomach? body_related_question 0.737520
2 body_related_question Do you sneeze? body_related_question 0.741122
3 body_related_question Do you have teeth? body_related_question 0.714836
4 body_related_question Do you have legs? body_related_question 0.726910

Werfen wir einen Blick auf einige der Utterances, bei denen der für die Erzeugung generierte Intent nicht mit dem vorhergesagten übereinstimmt.

    
    generated_data_predicted[generated_data_predicted['intent'] != generated_data_predicted['predicted_intent']].head(20)
    
intent utterances predicted_intent predicted_proba
7 ask_purpose Where do you live?? get_location 0.745748
20 ask_purpose What was your greatest passion growing up? needs_love 0.192401
70 ask_purpose Why are you here? get_location 0.683455
182 ask_purpose Where are you from? get_location 0.697122
49 get_location Are you in a computer? body_related_question 0.498938
162 get_location Tell me what you're doing ask_purpose 0.571899
3 make_sing I sing a song inform_hungry 0.358060
18 make_sing I want to sing inform_hungry 0.604815
20 make_sing You're so cute needs_love 0.266433
41 make_sing You're singing inform_hungry 0.323076

In einigen Fällen ist die Vorhersage eindeutig falsch. Es gibt jedoch auch Fälle, in denen die Vorhersage mit der Utterance übereinstimmt, aber nicht mit dem Intent, der für die Generierung verwendet wurde. Dies deutet darauf hin, dass unser GPT-2-Modell nicht perfekt ist, da es nicht immer übereinstimmende Utterances für einen Intent generiert.

Um unseren Klassifikator nicht mehr mit korrupten Daten zu trainieren, streichen wir alle Utterances, bei denen der Basis-Intent nicht mit dem vorhergesagten Intent übereinstimmt. Von solchen mit übereinstimmenden Instanzen behalten wir nur diejenigen mit den höchsten Vorhersagewahrscheinlichkeitswerten.

    
    correctly_predicted_data.drop_duplicates(subset='utterance', keep='first').sort_values(by=['intent', 'prediction_proba'], ascending=[True, False]).drop_duplicates(keep='first').groupby('intent').count()             
    
intent utterance predicted_intent predicted_proba
ask_purpose 60 60 60
body_related_question 41 41 41
get_location 48 48 48
greet 77 77 77
humor related 50 50 50
inform_hungry 35 35 35
inform_tired 67 67 67
make_sing 68 68 68
needs_love 71 71 71
suicide risk 67 67 67

Wir sehen, dass es für jeden Intent mindestens 35 unterschiedliche Utterances gibt. Um einen ausgewogenen Datensatz zu bewahren, wählen wir die besten 30 Utterances pro Intent entsprechend der Vorhersagewahrscheinlichkeit aus.

    
    TOP_N = 30
    top_predictions_per_intent = correctly_predicted_data.drop_duplicates(subset='utterance', keep='first').sort_values(by=['intent', 'prediction_proba'], ascending=[True, False]).drop_duplicates(keep='first').groupby('intent').head(TOP_N)            
    

NLU mit LAMBADA's Datenaugmentierung Step 6 - Trainieren des Intent-Klassifikators mit augmentierten Daten

Wir kombinieren nun die generierten Daten mit den ursprünglichen Trainingsdaten und teilen den angereicherten Datensatz in Trainings- und Validierungsdaten auf.

 
  
      data_train_aug = data_train.append(top_predictions_per_intent[['intent', 'utterance']], ignore_index=True)

      intents = data_train_aug['intent'].unique()

      X_train_aug = []
      X_valid_aug = []
      y_train_aug = []
      y_valid_aug = []
      for intent in intents:
        intent_X_train, intent_X_valid, intent_y_train, intent_y_valid = train_test_split(
            data_train_aug[data_train_aug['intent'] == intent]['utterance'],
            data_train_aug[data_train_aug['intent'] == intent]['intent'],
            train_size=0.8,
            random_state=43
        )

        X_train_aug.extend(intent_X_train)
        X_valid_aug.extend(intent_X_valid)
        y_train_aug.extend(intent_y_train)
        y_valid_aug.extend(intent_y_valid)            
	

Jetzt ist es an der Zeit, unser neues Intent-Klassifikationsmodell zu trainieren. Der Code ist wie der obige:

    
    distil_bert_augmented = text.Transformer('distilbert-base-cased', maxlen=50, classes=intents)
    
    processed_train_aug = distil_bert_augmented.preprocess_train(X_train_aug, y_train_aug)
    processed_test_aug = distil_bert_augmented.preprocess_test(X_valid_aug, y_valid_aug)
    
    model_aug = distil_bert_augmented.get_classifier()
    learner_aug = ktrain.get_learner(model_aug, train_data=processed_train_aug, val_data=processed_test_aug, batch_size=50)
    
    N_TRAINING_EPOCHS_AUGMENTED = 11
    learner_aug.fit_onecycle(5e-5, N_TRAINING_EPOCHS_AUGMENTED)            
    

Schließlich verwenden wir unseren Evaluationsdatensatz, um die Genauigkeit unseres neuen Intent-Klassifikators zu überprüfen.

    
    Genauigkeit: 91,40%
    

Wir können feststellen, dass sich die Performance um 7% verbessert hat. Insgesamt betrug die Verbesserung der Vorhersagegenauigkeit über alle von uns durchgeführten Experimente hinweg konstant mehr als 4%.

LAMBADA AI: Fazit

Wir setzten die LAMBADA-Methode zur Data Augmentation für Natural Language Understanding (NLU) Tasks ein. Wir trainierten ein GPT-2-Modell zur Generierung neuer Trainings-Utterances und nutzten sie als Trainingsdaten für unser Intent-Klassifikationsmodell (distilBERT). Die Leistung des Intent-Klassifikationsmodells verbesserte sich in jedem unserer Tests um mindestens 4%.

Darüber hinaus konnten wir feststellen, dass high-level-Bibliotheken wie KTrain und Huggingface Transformers dazu beitragen, die Komplexität der Anwendung hochmoderner Transformermodelle für Natural Language Generation (NLG) und andere Natural Language Processing (NLP)-Aufgaben wie die Klassifikation zu reduzieren und diese Ansätze auf breiter Basis anwendbar zu machen.

Mehr zu technologischen Themen

Ausgewählte Beiträge

Mehr anzeigen
Kein Spam, versprochen
Erhalten Sie wertvolle Insights von unserem Expertenteam.