Comment BERT a appris à lire votre boîte mail
Premier volet de notre série BERT : comment le modèle public XLM-RoBERTa de Klar transforme un e-mail en quatre scores, avec les figures des articles et le code du moteur actuel.
Voici le premier volet d’une série en deux parties. Il documente le moteur et le modèle publics qui propulsent Klar aujourd’hui. La suite étudiera comment une pile BERT de production évolue après les mesures et le déploiement.
ChatGPT a rendu célèbre une branche de la famille Transformer : on lui donne une consigne, puis il écrit. BERT a suivi la branche du lecteur. Il voit un texte complet, relie ses différentes parties et résume cette lecture dans un vecteur. Une petite tête utilise ensuite ce vecteur pour choisir une catégorie, par exemple le spam.
BERT est la moitié encodeur d’un Transformer
Le Transformer d’origine comportait un encodeur qui lisait l’entrée et un décodeur qui produisait la sortie. BERT, pour Bidirectional Encoder Representations from Transformers, a conservé la pile d’encodage. Il a été conçu pour comprendre un texte, pas pour le prolonger token après token.
Dans chaque couche, l’auto-attention permet à chaque token de pondérer les informations portées par les autres. Le mot « transfert » ne représente pas la même chose dans un avis bancaire et dans un compte rendu de football, car les mots voisins modifient sa représentation. Bidirectionnel signifie que le contexte de gauche et celui de droite sont disponibles. Un générateur causal ne peut pas consulter les mots qu’il n’a pas encore produits. Un encodeur BERT reçoit toute l’entrée d’un coup.
| Architecture | Ce qu’elle lit | Ce qu’elle produit | Usages adaptés |
|---|---|---|---|
| Encodeur de la famille BERT | L’entrée complète | Des vecteurs contextuels ou des scores de classe | Classification, recherche, extraction |
| Décodeur causal | Les tokens situés à gauche | Une probabilité pour le token suivant | Rédaction et génération ouverte |
Le pré-entraînement apprend la langue. L’entraînement spécialisé lui donne un métier.
Le BERT d’origine a appris à partir de textes non étiquetés grâce au masked language modelling : masquer certains tokens, puis les prédire grâce au contexte situé de part et d’autre. Il utilisait aussi la prédiction de la phrase suivante. Cette première étape coûteuse produisait un encodeur généraliste. Un jeu de données étiqueté bien plus petit suffisait ensuite à l’adapter aux questions-réponses, à l’analyse de sentiment ou à une autre tâche.
« Fine-tuner BERT » ne signifie pas toujours entraîner uniquement la dernière couche. La recette d’origine met à jour l’encodeur pré-entraîné avec la tête spécialisée. Geler l’encodeur et n’entraîner que la tête est un choix d’ingénierie distinct. Il réduit le coût et le risque des petites mises à jour locales, mais ne peut pas remodeler la représentation du langage.
Suivez la famille
Une idée d’encodeur, quatre étapes utiles
BERT désigne désormais autant une famille qu’un modèle précis. Sélectionnez une étape pour voir ce qu’elle a apporté et ce qu’il restait à construire.
Le modèle public que Klar exécute aujourd’hui
Le modèle en ligne aujourd’hui est icosha/spam-xlmr-v1, un classifieur de séquence XLM-RoBERTa-large. Son encodeur compte 24 couches, 16 têtes d’attention et une largeur cachée de 1 024. Klar convertit l’encodeur en GGUF quantifié, extrait la tête de classification, puis exécute les deux localement avec llama.cpp et ggml. Le code public est sous AGPLv3 ; le modèle public possède sa propre licence CC-BY-NC-4.0.
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 spamChaque flèche compte. Si l’entraînement utilise une représentation et la production une autre, la tête reçoit des vecteurs issus d’une distribution qu’elle n’a jamais apprise. Le moteur public impose donc un seul constructeur de l’entrée calibrée. Il enveloppe le message normalisé dans User (email): ..., puis ajoute les champs d’expéditeur uniquement lorsqu’ils existent. La classification et l’apprentissage local réutilisent exactement cette forme.
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";
}Étape 1 : des tokens à un vecteur pour toute la séquence
Le GgmlEncoder public utilise le tokenizer inclus dans le modèle GGUF. Il limite la longueur de la séquence, conserve le token de fin et vérifie que le token <s> de XLM-RoBERTa occupe la position zéro. Il joue le rôle de classification souvent décrit sous le nom [CLS]. Après le passage dans l’encodeur, le CLS pooling renvoie un vecteur pour l’ensemble du message.
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_);La vérification manuelle du début de séquence n’est pas décorative. Un convertisseur de modèle avait produit des métadonnées GGUF qui désactivaient le token spécial, même quand la tokenisation le demandait. Le pooling en position zéro renvoyait alors le premier mot ordinaire au lieu du résumé de la séquence. Le programme continuait à fonctionner, mais chaque embedding avait changé de sens. La parité de déploiement fait partie du modèle.
Étape 2 : du vecteur aux probabilités de classe
Le vecteur agrégé entre dans une petite tête de classification : une projection dense, tanh, une projection de sortie, puis softmax. Le moteur actuel expose quatre catégories neuronales. C’est cette partie qui transforme une représentation générale du langage en réponse propre à l’e-mail.
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
};La limite de tokens, l’encodeur et la tête de classification façonnent chacun une partie différente du modèle : modifier l’un apporte ce que les autres ne peuvent pas donner.
| Changement | Ce que cela apporte | Ce que cela coûte |
|---|---|---|
| Limite de tokens | L’encodeur lit une plus grande partie du message. | Un contexte plus long consomme mémoire et calcul, et le coût de l’attention augmente vite. La vue d’entraînement doit garder la même limite. |
| Encodeur | Chaque message reçoit une nouvelle représentation. | La tête doit être entraînée dans ce nouvel espace. Conversion, pooling, quantification et latence sur le matériel cible sont à revalider. |
| Tête de classification | La frontière entre les catégories se déplace. | Presque rien. Les connaissances linguistiques de l’encodeur restent fixes, et cette petite partie peut s’adapter localement. |
Personnaliser ne signifie pas réentraîner tout BERT
L’ajustement complet de l’encodeur appartient à une chaîne d’entraînement hors ligne, avec un corpus vaste et mesuré. Une correction utilisateur est plus petite et plus personnelle. Le chemin local de Klar calcule l’embedding avec l’encodeur gelé, mémorise le passage avant de la tête, rétropropage uniquement dans cette tête, puis applique une étape d’optimisation encadrée. La tête utilise un écrêtage des gradients, un rappel vers ses poids d’origine et une limite dure de dérive afin qu’une série de corrections dans un seul sens ne puisse pas la déplacer sans borne.
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;
}Cette séparation donne un rôle clair à chaque partie. Le grand encodeur gelé fournit une compréhension générale dans de nombreuses langues. La petite tête apprend où le propriétaire de la boîte place la frontière. Les deux fonctionnent sur l’appareil, mais seule la tête change après une correction locale.
Ce que BERT sait bien faire, et ce qu’il ne sait pas faire
- Adapté : classification, recherche et extraction quand toute l’entrée est disponible avant la réponse.
- Adapté : sorties compactes et fixes qui ne nécessitent ni modèle génératif ni aller-retour vers le cloud.
- Inadapté : rédaction ouverte. Un encodeur ne possède pas le décodeur autorégressif nécessaire à la production d’une réponse.
- Insuffisant seul : identité, authentification, réputation et préférences personnelles. Ces éléments exigent des preuves extérieures au texte.
BERT reste important parce que tous les problèmes d’IA ne sont pas des conversations. Certains des modèles les plus utiles lisent une fois, renvoient une réponse bornée, puis disparaissent dans le logiciel qui les entoure. C’est son rôle dans Klar. Le moteur AGPLv3 complet, avec tous les extraits ci-dessus, est consultable sur github.com/klar-im/engine.
La deuxième partie reviendra sur les effets d’un changement d’encodeur, de représentation d’entrée et de contraintes de déploiement.
Consulter les sources originales
Ces articles scientifiques et dépôts de code tracent le chemin utile entre le Transformer d’origine et le moteur présenté ici.
- Attention Is All You Need Vaswani et al. · 2017
- BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding Devlin et al. · 2018
- RoBERTa: A Robustly Optimized BERT Pretraining Approach Liu et al. · 2019
- Unsupervised Cross-lingual Representation Learning at Scale Conneau et al. · 2020
- Modèle anti-spam XLM-RoBERTa public de Klar icosha · CC-BY-NC-4.0
- Moteur anti-spam Klar et milter Postfix Klar · AGPLv3