BERT (text encoder)¶
Google's BERT in pure Keras 3: the bidirectional transformer text encoder with its masked-LM, sequence-classification, and token-classification heads. One implementation runs unmodified on TensorFlow / Torch / JAX, with bit-close parity to Hugging Face on real checkpoints (see below).
Paper: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
| Class | Module | Output |
|---|---|---|
BertModel |
kerasformers.models.bert |
{"last_hidden_state": (B, L, embed_dim), "pooler_output": (B, embed_dim)} |
BertMaskedLM |
kerasformers.models.bert |
MLM logits (B, L, vocab_size) |
BertSequenceClassify |
kerasformers.models.bert |
sequence logits (B, num_classes) |
BertTokenClassify |
kerasformers.models.bert |
per-token logits (B, L, num_classes) |
BertNextSentencePredict |
kerasformers.models.bert |
next-sentence logits (B, 2) |
BertQnA |
kerasformers.models.bert |
{"start_logits": (B, L), "end_logits": (B, L)} |
BertMultipleChoice |
kerasformers.models.bert |
per-choice logits (B, num_choices) |
BertTokenizer |
kerasformers.models.bert |
WordPiece → input_ids / attention_mask / token_type_ids |
All models are functional FunctionalBaseModels; the head classes compose a BertModel
backbone. The next-sentence head is part of the pretrained checkpoint, so it
loads real weights; the other task heads are randomly initialized for the
official release (ready for fine-tuning) and load trained weights from a hf:
fine-tune. The architecture is identical across variants: only the vocabulary
and tokenizer casing differ.
Loading¶
Two paths, both via from_weights:
- Official release variant:
from_weights("bert_base_uncased")downloads the kerasformers-release.weights.h5. hf:community fine-tune:from_weights("hf:org/repo")reads the repo'sconfig.json(architecture +num_labels) and loads the checkpoint, including the fine-tuned classifier head.
from kerasformers.models.bert import BertModel, BertTokenizer
model = BertModel.from_weights("bert_base_uncased")
tokenizer = BertTokenizer.from_weights("bert_base_uncased")
out = model(tokenizer("Hello, world."))
out["last_hidden_state"] # (1, L, 768)
out["pooler_output"] # (1, 768)
Available variants¶
| Variant | vocab | casing |
|---|---|---|
bert_base_uncased |
30522 | lowercased |
bert_large_uncased |
30522 | lowercased |
bert_base_cased |
28996 | case-preserving |
bert_large_cased |
28996 | case-preserving |
The tokenizer follows the model: BertTokenizer.from_weights(variant) pulls the
matching vocab and casing, and from_weights("hf:org/repo") reads do_lower_case
from the repo's tokenizer_config.json.
Verified parity¶
Validated against the Hugging Face reference (eager attention) on a real forward
pass (the classification fine-tunes also agree with HF on argmax at every
position):
| Model | Checkpoint | max |Δ| |
|---|---|---|
BertModel |
google-bert/bert-base-uncased |
1.2e-5 |
BertModel |
google-bert/bert-base-cased |
6.1e-6 |
BertMaskedLM |
google-bert/bert-base-uncased |
1.7e-5 |
BertSequenceClassify |
textattack/bert-base-uncased-SST-2 (hf:) |
4.8e-7 |
BertTokenClassify |
dslim/bert-base-NER (hf:) |
2.9e-6 |
BertNextSentencePredict |
google-bert/bert-base-uncased |
4.8e-6 |
BertQnA |
deepset/bert-base-cased-squad2 (hf:) |
4.3e-6 |
BertMultipleChoice |
google-bert/bert-base-uncased |
3.3e-7 |
The kerasformers WordPiece tokenizer reproduces HF's input_ids /
token_type_ids / attention_mask exactly (single, pair, and [MASK] inputs;
both casings).
Forward pass¶
The models take a dict of token ids, an attention mask, and segment ids: exactly
what BertTokenizer returns:
inputs = {
"input_ids": input_ids, # (B, L) int
"attention_mask": attention_mask, # (B, L) int: 1 keep, 0 pad
"token_type_ids": token_type_ids, # (B, L) int: segment (0 / 1)
}
BertModel.from_weights("bert_base_uncased")(inputs)["last_hidden_state"]
These are token-id models: no spatial H/W axes, so channels_first/last
does not apply.
Fill-mask¶
from kerasformers.models.bert import BertMaskedLM, BertTokenizer
mlm = BertMaskedLM.from_weights("bert_base_uncased")
tokenizer = BertTokenizer.from_weights("bert_base_uncased")
inputs = tokenizer("the capital of france is [MASK].")
logits = mlm(inputs) # (1, L, vocab_size)
mask = int((inputs["input_ids"][0] == tokenizer.mask_token_id).argmax())
print(tokenizer.ids_to_tokens[int(logits[0, mask].argmax())]) # -> "paris"
Classification (community fine-tunes)¶
from kerasformers.models.bert import BertSequenceClassify, BertTokenClassify
# sentiment (2 labels)
clf = BertSequenceClassify.from_weights("hf:textattack/bert-base-uncased-SST-2")
# named-entity recognition (9 labels, cased)
ner = BertTokenClassify.from_weights("hf:dslim/bert-base-NER")
num_classes is read from the repo's config, so the head matches the fine-tune.
Other task heads¶
from kerasformers.models.bert import (
BertNextSentencePredict, BertQnA, BertMultipleChoice,
)
# next-sentence prediction: head is pretrained, so a base checkpoint works
nsp = BertNextSentencePredict.from_weights("hf:google-bert/bert-base-uncased")
# extractive QA: start/end span logits, from a SQuAD fine-tune
qa = BertQnA.from_weights("hf:deepset/bert-base-cased-squad2")
out = qa(tokenizer("Where is Paris?", text_pair="Paris is in France."))
out["start_logits"] # (B, L) out["end_logits"] # (B, L)
# multiple choice: inputs are (B, num_choices, seq); fix num_choices at build
mc = BertMultipleChoice.from_weights("hf:org/bert-swag", num_choices=4)
BertMultipleChoice takes a static num_choices (the choice axis is folded into
the batch through the shared backbone, then back out); its classifier head is
shape-independent of num_choices, so the same weights load for any value.
Tokenizer¶
BertTokenizer is a WordPiece tokenizer built on the tokenizers (Rust) library:
the BERT normalizer (clean text, handle CJK, optional lowercase + accent strip),
whitespace/punctuation pre-tokenization, greedy WordPiece over vocab.txt, and
[CLS] A [SEP] B [SEP] template post-processing with segment ids. It accepts a
string, a list of strings, or a sentence pair (text_pair=), and returns the
input_ids / attention_mask / token_type_ids dict the models consume.
Architecture notes¶
- Embeddings: summed word + absolute-position + token-type embeddings, then
LayerNorm + dropout. Position ids come from
cumsum(ones_like(input_ids)) - 1(rather thanarange) so the model stays shape-polymorphic across backends and supports variable sequence length. - Encoder:
num_layerspost-LayerNorm transformer blocks (LayerNorm(x + Sublayer(x))): multi-head self-attention with an additive padding mask, then amlp_dimfeed-forward withhidden_act(exactgelu). - Pooler: a
tanhdense projection of the[CLS]token (BertModel,BertSequenceClassify). - Heads:
BertMaskedLMadds the transform (dense +gelu+ LayerNorm) and a vocabulary projection; the classify models add dropout + a dense classifier.
Constructor arguments follow the kerasformers convention: embed_dim,
num_layers, num_heads, mlp_dim (plus vocab_size,
max_position_embeddings, type_vocab_size, hidden_act, layer_norm_eps).
convert_bert_hf_to_keras.py maps the HF BertModel / BertForMaskedLM
safetensors to Keras.