Klar-Blog

Wie BERT lernte, Ihr Postfach zu lesen

Teil eins unserer BERT-Serie: wie Klars öffentliches XLM-RoBERTa-Modell eine E-Mail in vier Werte verwandelt, mit Abbildungen aus den Papers und Code aus der heutigen Engine.

Dies ist der erste Teil einer zweiteiligen Serie. Er dokumentiert die öffentliche Engine und das öffentliche Modell, die Klar heute antreiben. Ein späterer Teil untersucht, wie sich ein produktiver BERT-Stack nach Messung und Einsatz weiterentwickelt.

ChatGPT machte einen Zweig der Transformer-Familie bekannt: Es erhält eine Eingabe und schreibt. BERT nahm den Leser-Zweig. Das Modell sieht einen vollständigen Text, verbindet seine Teile und verdichtet die Lektüre zu einem Vektor. Ein kleiner Kopf wählt daraus eine Kategorie wie Spam.

BERT ist die Encoder-Hälfte eines Transformers

Der ursprüngliche Transformer besaß einen Encoder zum Lesen der Eingabe und einen Decoder zum Erzeugen der Ausgabe. BERT, kurz für Bidirectional Encoder Representations from Transformers, behielt den Encoder-Stapel. Das Modell sollte Text verstehen, nicht Token für Token fortsetzen.

In jeder Encoder-Schicht kann jedes Token durch Self-Attention Informationen aus den anderen Tokens gewichten. „Transfer“ bedeutet in einer Bankmitteilung etwas anderes als in einem Fußballbericht, weil die umgebenden Wörter seine Repräsentation verändern. Bidirektional heißt, dass der Kontext links und rechts einfließen kann. Ein kausaler Textgenerator kann nicht auf Wörter schauen, die er noch nicht erzeugt hat. Ein BERT-Encoder erhält die ganze Eingabe auf einmal.

Oben: Jedes Wort eines Satzes ist in beide Richtungen mit jedem anderen verbunden und ergibt eine einzige Klasse. Unten: Jedes Wort ist nur mit dem vorherigen verbunden und sagt das nächste voraus.
Der entscheidende Unterschied. Ein Encoder gewichtet den Kontext auf beiden Seiten eines Wortes und endet mit einem Vektor für die ganze Nachricht; ein Decoder sieht nur das Vorherige und sagt das Nächste voraus. Klar-Diagramm, nach Devlin et al., 2018
ModellformLiestErzeugtPassende Aufgaben
Encoder der BERT-FamilieDie vollständige EingabeKontextvektoren oder KlassenwerteKlassifikation, Suche, Extraktion
Kausaler DecoderTokens auf der linken SeiteEine Wahrscheinlichkeit für das nächste TokenSchreiben und offene Generierung

Vortraining lernt Sprache. Aufgabentraining gibt ihr einen Zweck.

Das ursprüngliche BERT lernte aus unbeschriftetem Text mit Masked Language Modelling: Einige Tokens werden verdeckt und aus dem Kontext auf beiden Seiten vorhergesagt. Hinzu kam die Vorhersage des nächsten Satzes. Diese aufwendige erste Stufe ergab einen allgemeinen Encoder. Ein deutlich kleinerer beschrifteter Datensatz konnte ihn anschließend an Fragen und Antworten, Stimmungsanalyse oder eine andere Aufgabe anpassen.

„BERT feinabstimmen“ bedeutet nicht immer, nur die letzte Schicht zu trainieren. Das ursprüngliche Verfahren aktualisiert den vortrainierten Encoder zusammen mit dem aufgabenspezifischen Kopf. Den Encoder einzufrieren und nur den Kopf zu trainieren, ist eine eigene technische Entscheidung. Kleine lokale Anpassungen werden dadurch billiger und kontrollierbarer, die Sprachrepräsentation selbst lässt sich so aber nicht umformen.

Die Modellfamilie

Eine Encoder-Idee in vier Schritten

BERT ist heute ebenso ein Familienname wie ein einzelnes Modell. Wählen Sie eine Stufe, um ihren Beitrag und die verbleibende Lücke zu sehen.

Der Beitrag Ein tiefer bidirektionaler Encoder, mit verdeckten Tokens vortrainiert und danach an viele Sprachaufgaben angepasst.
Was noch fehlte Die ursprünglichen Modelle und das Vokabular waren vor allem auf Englisch ausgerichtet. Das Trainingsverfahren war erst ein Anfang.

Das öffentliche Modell, das Klar heute ausführt

Das heute laufende Modell ist icosha/spam-xlmr-v1, ein Sequenzklassifikator auf Basis von XLM-RoBERTa-large. Sein Encoder hat 24 Schichten, 16 Attention-Köpfe und eine verborgene Breite von 1.024. Klar wandelt den Encoder in quantisiertes GGUF um, extrahiert den Klassifikationskopf und führt beide lokal mit llama.cpp und ggml aus. Der öffentliche Code steht unter AGPLv3; für das öffentliche Modell gilt eine eigene CC-BY-NC-4.0-Lizenz.

Eine E-Mail-Zeile wird zu XLM-RoBERTa-Tokens, beginnend mit dem Spezial-Token <s>, und läuft durch 24 Encoder-Schichten zu einem Vektor aus 1.024 Werten.
Die exakte Eingabe, die Klar aufbaut. Der Rahmentext, der Tokenizer, der „verifying“ in zwei Teile zerlegt, und das führende <s>, dessen Vektor für die ganze Nachricht steht, sind durch das darauf trainierte Modell festgelegt. Klar-Diagramm, nach Devlin et al., 2018
Klar-Klassifikationsweg
RFC 822 email
  -> MIME parse and HTML-to-text
  -> normalized subject, sender and body
  -> canonical "User (email)" input
  -> XLM-RoBERTa-large encoder
  -> 1024-value CLS vector
  -> four-class head
  -> gibberish, marketing, regular or spam

Jeder Pfeil zählt. Nutzt das Training eine Eingabeform und die Produktion eine andere, erhält der Kopf Vektoren aus einer Verteilung, die er nie gelernt hat. Die öffentliche Engine besitzt deshalb genau einen Konstruktor für kalibrierten Eingabetext. Er verpackt die normalisierte Nachricht als User (email): ... und ergänzt Absenderfelder nur, wenn sie vorhanden sind. Klassifikation und lokales Training verwenden genau diese Form.

engine/spam_engine.cpp, gekürzt
for (const auto& exchange : transcript) {
  std::string from_type = exchange.from_type;
  if (!from_type.empty()) {
    from_type[0] = static_cast<char>(std::toupper(from_type[0]));
  }
  input_text += from_type + " (" + exchange.origin + "): "
              + exchange.text + "\n";
}

if (has_any_customer_signal) {
  input_text += "Customer Info:\n";
  if (!customer.name.empty()) input_text += "Name: " + customer.name + "\n";
  if (!customer.email.empty()) input_text += "Email: " + customer.email + "\n";
}

Schritt 1: Aus Tokens wird ein Vektor für die ganze Sequenz

Der öffentliche GgmlEncoder nutzt den im GGUF-Modell gespeicherten Tokenizer. Er begrenzt die Sequenzlänge, erhält das End-Token und stellt sicher, dass XLM-RoBERTas <s>-Token an Position null steht. Es erfüllt die Klassifikationsrolle, die meist [CLS] heißt. Nach dem Encoder-Lauf liefert CLS-Pooling einen Vektor für die ganze Nachricht.

engine/ggml_encoder.h, gekürzt
int n = llama_tokenize(vocab_, text.c_str(), text.size(),
                        token_buf_.data(), token_buf_.size(),
                        /* add_special = */ true,
                        /* parse_special = */ false);

if (n > max_tokens_) {
  n = max_tokens_;
  token_buf_[n - 1] = llama_vocab_eos(vocab_);
}

const llama_token bos = llama_vocab_bos(vocab_);
if (token_buf_.empty() || token_buf_.front() != bos) {
  token_buf_.insert(token_buf_.begin(), bos);
}

auto batch = llama_batch_get_one(token_buf_.data(), token_buf_.size());
llama_encode(ctx_, batch);

const float* emb = llama_get_embeddings_seq(ctx_, 0);
return std::vector<float>(emb, emb + n_embd_);

Die manuelle Prüfung des Sequenzanfangs ist kein Schmuck. Ein Modellkonverter hatte einmal GGUF-Metadaten erzeugt, die das Sondertoken selbst dann unterdrückten, wenn die Tokenisierung es anforderte. Das Pooling an Position null lieferte dadurch das erste gewöhnliche Wort statt der Sequenzzusammenfassung. Das Programm lief weiter, aber die Bedeutung jedes Embeddings war falsch. Deployment-Parität gehört zum Modell.

Schritt 2: Aus dem Vektor werden Klassenwahrscheinlichkeiten

Der gepoolte Vektor geht in einen kleinen Klassifikationskopf: eine dichte Projektion, tanh, eine Ausgabeprojektion und danach Softmax. Die aktuelle Engine gibt vier neuronale Kategorien aus. Dieser Teil macht aus einer allgemeinen Sprachrepräsentation eine E-Mail-spezifische Antwort.

engine/spam_engine.cpp, gekürzt
const auto logits = impl_->trainable_head->forward(
    embedding, cache_for_training);
const auto probabilities = impl_->trainable_head->softmax(logits);

return ClassScores{
    probabilities[0],  // gibberish
    probabilities[1],  // marketing
    probabilities[2],  // regular
    probabilities[3],  // spam
};

Token-Grenze, Encoder und Klassifikationskopf formen jeweils einen anderen Teil des Modells. Eine Änderung an einem bringt deshalb etwas, das die anderen nicht liefern können.

ÄnderungWas es bringtWas es kostet
Token-GrenzeDer Encoder liest mehr von der Nachricht.Längerer Kontext kostet Speicher und Rechenzeit, und der Aufwand für Attention wächst schnell. Die Trainingsansicht muss dieselbe Grenze behalten.
EncoderJede Nachricht erhält eine neue Repräsentation.Der Kopf muss für diesen Embedding-Raum trainiert werden. Konvertierung, Pooling, Quantisierung und Hardware-Latenz sind neu zu prüfen.
KlassifikationskopfDie Grenze zwischen den Klassen verschiebt sich.Fast nichts. Das Sprachwissen des Encoders bleibt fest, und dieser kleine Teil kann sich lokal anpassen.

Personalisierung heißt nicht, ganz BERT neu zu trainieren

Die vollständige Feinabstimmung des Encoders gehört in eine Offline-Trainingspipeline mit einem großen, vermessenen Korpus. Eine Korrektur durch den Nutzer ist kleiner und persönlicher. Klars lokaler Weg berechnet das Embedding mit dem eingefrorenen Encoder, speichert den Vorwärtslauf des Kopfes, propagiert nur durch diesen Kopf zurück und führt einen abgesicherten Optimierungsschritt aus. Gradient Clipping, eine Rückbindung an die ursprünglichen Gewichte und eine harte Driftgrenze verhindern, dass eine einseitige Reihe von Korrekturen den Kopf unbegrenzt verschiebt.

engine/spam_engine.cpp
float SpamEngine::train_embedding(
    const std::vector<float>& embedding,
    int correct_label) {
  (void)classify_embedding(embedding, true);
  const float loss = impl_->trainable_head->backward(correct_label);
  impl_->trainable_head->step(1);
  return loss;
}

Diese Aufteilung gibt jedem Teil eine klare Aufgabe. Der große eingefrorene Encoder liefert allgemeine Bedeutung in vielen Sprachen. Der kleine Kopf lernt, wo der Postfachbesitzer die Grenze zieht. Beide laufen auf dem Gerät, aber nur der Kopf verändert sich nach einer lokalen Korrektur.

Was BERT gut kann und was nicht

  • Gut geeignet: Klassifikation, Suche und Extraktion, wenn die ganze Eingabe vor der Antwort vorliegt.
  • Gut geeignet: kompakte, feste Ausgaben ohne generatives Modell und ohne Umweg über eine Cloud.
  • Nicht geeignet: offenes Schreiben. Ein Encoder besitzt keinen autoregressiven Decoder zum Erzeugen einer Antwort.
  • Allein nicht ausreichend: Identität, Authentifizierung, Ruf und persönliche Vorlieben. Dafür braucht es Belege außerhalb des Textes.

BERT bleibt wichtig, weil nicht jedes KI-Problem ein Gespräch ist. Manche der nützlichsten Modelle lesen einmal, geben eine begrenzte Antwort zurück und verschwinden in der Software um sie herum. Diese Rolle übernimmt BERT in Klar. Die vollständige AGPLv3-Engine mit allen Ausschnitten oben steht unter github.com/klar-im/engine.

Teil zwei kehrt zu der Frage zurück, was bei Änderungen an Encoder, Eingaberepräsentation und Einsatzbedingungen geschieht.

Die Originalquellen

Diese Veröffentlichungen und Quellcode-Repositorien führen vom ursprünglichen Transformer bis zur oben gezeigten Engine.

  1. Attention Is All You Need Vaswani et al. · 2017
  2. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding Devlin et al. · 2018
  3. RoBERTa: A Robustly Optimized BERT Pretraining Approach Liu et al. · 2019
  4. Unsupervised Cross-lingual Representation Learning at Scale Conneau et al. · 2020
  5. Klars öffentliches XLM-RoBERTa-Spammodell icosha · CC-BY-NC-4.0
  6. Klar-Spamerkennungs-Engine und Postfix-Milter Klar · AGPLv3

Alle Artikel

Die lokale Option

Gratis, privat, in Apple Mail.

Laden im App Store