Vorstellung der agilen Sicherheitsklassifikatoren mit Gemma

1. Überblick

In diesem Codelab erfahren Sie, wie Sie einen benutzerdefinierten Textklassifikator mit Parameter-Effizienter Feinabstimmung (Parameter Effiziente Feinabstimmung) erstellen. Anstatt das gesamte Modell zu optimieren, aktualisieren PET-Methoden nur eine kleine Anzahl von Parametern, was das Training relativ einfach und schnell macht. Außerdem ist es für ein Modell einfacher, mit relativ wenigen Trainingsdaten neue Verhaltensweisen zu erlernen. Die Methodik wird ausführlich unter Towards Agile Text Classifiers for Everyone (in englischer Sprache) beschrieben. Hier wird gezeigt, wie diese Techniken auf eine Vielzahl von Sicherheitsaufgaben angewendet werden können und mit nur wenigen Hundert Trainingsbeispielen die höchste Leistung erzielen.

In diesem Codelab werden die LoRA und das kleinere Gemma-Modell (gemma_instruct_2b_en) verwendet, da es schneller und effizienter ausgeführt werden kann. Das Colab umfasst die Schritte der Datenaufnahme, der Formatierung für das LLM, des Trainings von LoRA-Gewichtungen und der anschließenden Auswertung der Ergebnisse. In diesem Codelab wird mit dem ETHOS-Dataset trainiert, einem öffentlich verfügbaren Dataset zur Erkennung von Hassrede, das auf YouTube- und Reddit-Kommentaren basiert. Wenn es nur mit 200 Beispielen (1/4 des Datasets) trainiert wird, erreicht es F1: 0,80 und ROC-AUC: 0,78, was etwas über dem SOTA-Wert liegt, der derzeit in der Bestenliste aufgeführt ist (zum Zeitpunkt der Erstellung dieses Artikels, 15. Februar 2024). Beim Training mit den 800 Beispielen erreicht es einen F1-Wert von 83,74 und einen ROC-AUC-Wert von 88,17. Größere Modelle wie gemma_instruct_7b_en funktionieren in der Regel besser, aber auch die Kosten für Training und Ausführung sind höher.

Trigger-Warnung: Da in diesem Codelab ein Sicherheitsklassifikator zum Erkennen von Hassreden entwickelt wurde, enthalten Beispiele und Auswertungen der Ergebnisse schreckliche Sprache.

2. Installation und Einrichtung

Für dieses Codelab benötigen Sie die aktuelle Version keras (3), keras-nlp (0.8.0) und ein Kaggle-Konto, um das Basismodell herunterzuladen.

!pip install -q -U keras-nlp
!pip install -q -U keras

Um sich bei Kaggle anzumelden, können Sie entweder Ihre kaggle.json-Anmeldedatendatei unter ~/.kaggle/kaggle.json speichern oder Folgendes in einer Colab-Umgebung ausführen:

import kagglehub

kagglehub.login()

3. ETHOS-Dataset laden

In diesem Abschnitt laden Sie das Dataset, mit dem der Klassifikator trainiert werden soll, und verarbeiten es in einem Trainings- und Test-Dataset. Sie verwenden das beliebte Forschungs-Dataset ETHOS, das erhoben wurde, um Hassrede in sozialen Medien zu erkennen. Weitere Informationen dazu, wie das Dataset erfasst wurde, finden Sie im Artikel ETHOS: an Online Hate Speech Detection Dataset.

import pandas as pd

gh_root = 'https://raw.githubusercontent.com'
gh_repo = 'intelligence-csd-auth-gr/Ethos-Hate-Speech-Dataset'
gh_path = 'master/ethos/ethos_data/Ethos_Dataset_Binary.csv'
data_url = f'{gh_root}/{gh_repo}/{gh_path}'

df = pd.read_csv(data_url, delimiter=';')
df['hateful'] = (df['isHate'] >= df['isHate'].median()).astype(int)

# Shuffle the dataset.
df = df.sample(frac=1, random_state=32)

# Split into train and test.
df_train, df_test = df[:800],  df[800:]

# Display a sample of the data.
df.head(5)[['hateful', 'comment']]

Die Anzeige sieht ungefähr so aus:

Label

Kommentar

0

0

You said he but still not convinced this is a ...

1

0

well, looks like its time to have another child.

2

0

What if we send every men to mars to start a n...

3

1

It doesn't matter if you're black or white, ...

4

0

Who ever disliked this video should be ashamed...

4. Modell herunterladen und instanziieren

Wie in der Dokumentation beschrieben, können Sie das Gemma-Modell ganz einfach auf viele Arten verwenden. Mit Keras müssen Sie Folgendes tun:

import keras
import keras_nlp

# For reproducibility purposes.
keras.utils.set_random_seed(1234)

# Download the model from Kaggle using Keras.
model = keras_nlp.models.GemmaCausalLM.from_preset('gemma_instruct_2b_en')

# Set the sequence length to a small enough value to fit in memory in Colab.
model.preprocessor.sequence_length = 128

Sie können testen, ob das Modell funktioniert, indem Sie Text generieren:

model.generate('Question: what is the capital of France? ', max_length=32)

5. Textvorverarbeitung und Trennzeichentokens

Damit das Modell unsere Absicht besser versteht, können Sie den Text vorverarbeiten und Trennzeichentokens verwenden. Dies verringert die Wahrscheinlichkeit, dass das Modell Text generiert, der nicht dem erwarteten Format entspricht. Sie können beispielsweise versuchen, eine Sentimentklassifizierung vom Modell anzufordern, indem Sie einen Prompt wie diesen schreiben:

Classify the following text into one of the following classes:[Positive,Negative]

Text: you look very nice today
Classification:

In diesem Fall gibt das Modell möglicherweise nicht das aus, wonach Sie suchen. Wenn der Text beispielsweise Zeilenumbruchzeichen enthält, hat dies wahrscheinlich negative Auswirkungen auf die Modellleistung. Ein robusterer Ansatz ist die Verwendung von Trennzeichentokens. Die Eingabeaufforderung sieht dann zu:

Classify the following text into one of the following classes:[Positive,Negative]
<separator>
Text: you look very nice today
<separator>
Prediction:

Dies kann mit einer Funktion abstrahiert werden, die den Text vorverarbeitet:

def preprocess_text(
    text: str,
    labels: list[str],
    instructions: str,
    separator: str,
) -> str:
  prompt = f'{instructions}:[{",".join(labels)}]'
  return separator.join([prompt, f'Text:{text}', 'Prediction:'])

Wenn Sie die Funktion jetzt mit demselben Prompt und Text wie zuvor ausführen, sollten Sie dieselbe Ausgabe erhalten:

text = 'you look very nice today'

prompt = preprocess_text(
    text=text,
    labels=['Positive', 'Negative'],
    instructions='Classify the following text into one of the following classes',
    separator='\n<separator>\n',
)

print(prompt)

Es sollte Folgendes ausgegeben werden:

Classify the following text into one of the following classes:[Positive,Negative]
<separator>
Text:well, looks like its time to have another child
<separator>
Prediction:

6. Ausgabenachverarbeitung

Die Ausgaben des Modells sind Tokens mit verschiedenen Wahrscheinlichkeiten. Normalerweise wählen Sie zum Generieren von Text aus den wahrscheinlichsten Tokens aus und erstellen Sätze, Absätze oder sogar vollständige Dokumente. Für die Klassifizierung kommt es jedoch darauf an, ob das Modell glaubt, dass Positive wahrscheinlicher ist als Negative oder umgekehrt.

Mit dem Modell, das Sie zuvor instanziiert haben, können Sie die Ausgabe so verarbeiten, dass die unabhängige Wahrscheinlichkeit dafür besteht, dass das nächste Token Positive oder Negative ist:

import numpy as np


def compute_output_probability(
    model: keras_nlp.models.GemmaCausalLM,
    prompt: str,
    target_classes: list[str],
) -> dict[str, float]:
  # Shorthands.
  preprocessor = model.preprocessor
  tokenizer = preprocessor.tokenizer

  # NOTE: If a token is not found, it will be considered same as "<unk>".
  token_unk = tokenizer.token_to_id('<unk>')

  # Identify the token indices, which is the same as the ID for this tokenizer.
  token_ids = [tokenizer.token_to_id(word) for word in target_classes]

  # Throw an error if one of the classes maps to a token outside the vocabulary.
  if any(token_id == token_unk for token_id in token_ids):
    raise ValueError('One of the target classes is not in the vocabulary.')

  # Preprocess the prompt in a single batch. This is done one sample at a time
  # for illustration purposes, but it would be more efficient to batch prompts.
  preprocessed = model.preprocessor.generate_preprocess([prompt])

  # Identify output token offset.
  padding_mask = preprocessed["padding_mask"]
  token_offset = keras.ops.sum(padding_mask) - 1

  # Score outputs, extract only the next token's logits.
  vocab_logits = model.score(
      token_ids=preprocessed["token_ids"],
      padding_mask=padding_mask,
  )[0][token_offset]

  # Compute the relative probability of each of the requested tokens.
  token_logits = [vocab_logits[ix] for ix in token_ids]
  logits_tensor = keras.ops.convert_to_tensor(token_logits)
  probabilities = keras.activations.softmax(logits_tensor)

  return dict(zip(target_classes, probabilities.numpy()))

Sie können diese Funktion testen, indem Sie sie mit einem Prompt ausführen, den Sie zuvor erstellt haben:

compute_output_probability(
    model=model,
    prompt=prompt,
    target_classes=['Positive', 'Negative'],
)

Die Ausgabe sollte in etwa so aussehen:

{'Positive': 0.99994016, 'Negative': 5.984089e-05}

7. Zusammenfassung als Klassifikator

Der Einfachheit halber können Sie alle Funktionen, die Sie gerade erstellt haben, in einem einzigen sklearn-ähnlichen Klassifikator mit nutzerfreundlichen und vertrauten Funktionen wie predict() und predict_score() zusammenfassen.

import dataclasses


@dataclasses.dataclass(frozen=True)
class AgileClassifier:
  """Agile classifier to be wrapped around a LLM."""

  # The classes whose probability will be predicted.
  labels: tuple[str, ...]

  # Provide default instructions and control tokens, can be overridden by user.
  instructions: str = 'Classify the following text into one of the following classes'
  separator_token: str = '<separator>'
  end_of_text_token: str = '<eos>'

  def encode_for_prediction(self, x_text: str) -> str:
    return preprocess_text(
        text=x_text,
        labels=self.labels,
        instructions=self.instructions,
        separator=self.separator_token,
    )

  def encode_for_training(self, x_text: str, y: int) -> str:
    return ''.join([
        self.encode_for_prediction(x_text),
        self.labels[y],
        self.end_of_text_token,
    ])

  def predict_score(
      self,
      model: keras_nlp.models.GemmaCausalLM,
      x_text: str,
  ) -> list[float]:
    prompt = self.encode_for_prediction(x_text)
    token_probabilities = compute_output_probability(
        model=model,
        prompt=prompt,
        target_classes=self.labels,
    )
    return [token_probabilities[token] for token in self.labels]

  def predict(
      self,
      model: keras_nlp.models.GemmaCausalLM,
      x_eval: str,
  ) -> int:
    return np.argmax(self.predict_score(model, x_eval))


agile_classifier = AgileClassifier(labels=('Positive', 'Negative'))

8. Modellabstimmung

LoRA steht für Low-Rank Adaptation. Es ist ein Feinabstimmungsverfahren, mit dem Large Language Models effizient abgestimmt werden können. Weitere Informationen dazu finden Sie im Artikel „LoRA: Low-Rank Adaptation of Large Language Models“.

Die Keras-Implementierung von Gemma bietet eine enable_lora()-Methode, die Sie für die Feinabstimmung verwenden können:

# Enable LoRA for the model and set the LoRA rank to 4.
model.backbone.enable_lora(rank=4)

Nachdem Sie LoRA aktiviert haben, können Sie mit der Feinabstimmung beginnen. Dies dauert in Colab ungefähr 5 Minuten pro Epoche:

import tensorflow as tf

# Create dataset with preprocessed text + labels.
map_fn = lambda xy: agile_classifier.encode_for_training(*xy)
x_train = list(map(map_fn, df_train[['comment', 'hateful']].values))
ds_train = tf.data.Dataset.from_tensor_slices(x_train).batch(2)

# Compile the model using the Adam optimizer and appropriate loss function.
model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=keras.optimizers.Adam(learning_rate=0.0005),
    weighted_metrics=[keras.metrics.SparseCategoricalAccuracy()],
)

# Begin training.
model.fit(ds_train, epochs=4)

Das Training für mehrere Epochen führt zu einer höheren Genauigkeit, bis es zu einer Überanpassung kommt.

9. Ergebnisse prüfen

Sie können jetzt die Ausgabe des gerade trainierten agilen Klassifikators überprüfen. Dieser Code gibt die vorhergesagte Klassenpunktzahl aus einem Textabschnitt aus:

text = 'you look really nice today'
scores = agile_classifier.predict_score(model, text)
dict(zip(agile_classifier.labels, scores))
{'Positive': 0.99899644, 'Negative': 0.0010035498}

10. Modellbewertung

Schließlich bewerten Sie die Leistung unseres Modells anhand von zwei gängigen Messwerten, dem F1-Wert und dem AUC-ROC. Der F1-Wert erfasst falsch negative und falsch positive Fehler, indem der harmonische Mittelwert von Precision und Recall bei einem bestimmten Klassifizierungsschwellenwert ausgewertet wird. AUC-ROC hingegen erfasst den Kompromiss zwischen der Rate richtig positiver und der falsch positiven Ergebnisse über eine Vielzahl von Schwellenwerten und berechnet den Bereich unter dieser Kurve.

from sklearn.metrics import f1_score, roc_auc_score

y_true = df_test['hateful'].values
# Compute the scores (aka probabilities) for each of the labels.
y_score = [agile_classifier.predict_score(model, x) for x in df_test['comment']]
# The label with highest score is considered the predicted class.
y_pred = np.argmax(y_score, axis=1)
# Extract the probability of a comment being considered hateful.
y_prob = [x[agile_classifier.labels.index('Negative')] for x in y_score]

# Compute F1 and AUC-ROC scores.
print(f'F1: {f1_score(y_true, y_pred):.2f}')
print(f'AUC-ROC: {roc_auc_score(y_true, y_prob):.2f}')
F1: 0.84
AUC-ROC: = 0.88

Eine weitere interessante Möglichkeit zur Bewertung von Modellvorhersagen sind Wahrheitsmatrizen. In einer Wahrheitsmatrix werden die verschiedenen Arten von Vorhersagefehlern visuell dargestellt.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(y_true, y_pred)
ConfusionMatrixDisplay(
  confusion_matrix=cm,
  display_labels=agile_classifier.labels,
).plot()

Wahrheitsmatrix

Schließlich können Sie sich auch die ROC-Kurve ansehen, um ein Gefühl für potenzielle Vorhersagefehler zu bekommen, wenn Sie verschiedene Bewertungsgrenzwerte verwenden.

from sklearn.metrics import RocCurveDisplay, roc_curve

fpr, tpr, _ = roc_curve(y_true, y_score, pos_label=1)
RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

ROC-Kurve