Skip to content

Commit 011ba48

Browse files
committed
Add mxbai-embed model
1 parent 41faf96 commit 011ba48

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

keras_hub/src/utils/transformers/convert_bert.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def convert_backbone_config(transformers_config):
1919
"num_heads": transformers_config["num_attention_heads"],
2020
"hidden_dim": transformers_config["hidden_size"],
2121
"intermediate_dim": transformers_config["intermediate_size"],
22+
"max_sequence_length": transformers_config["max_position_embeddings"],
2223
}
2324

2425

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""
2+
Convert HuggingFace mixedbread-ai mxbai-embed checkpoints to KerasHub format.
3+
4+
Supports four presets across two architectures:
5+
6+
**BERT-based** (BertTextEmbedder):
7+
- mxbai-embed-large-v1 (CLS pooling, no normalization)
8+
- mxbai-embed-2d-large-v1 (CLS pooling, no normalization)
9+
- mxbai-embed-xsmall-v1 (mean pooling, no normalization)
10+
11+
**XLM-RoBERTa-based** (XLMRobertaTextEmbedder):
12+
- deepset-mxbai-embed-de-large-v1 (mean pooling, L2 normalization)
13+
14+
Setup:
15+
```shell
16+
pip install keras-hub keras sentence-transformers safetensors huggingface_hub
17+
```
18+
19+
Usage:
20+
```shell
21+
cd tools/checkpoint_conversion
22+
python convert_mxbai_embed_checkpoints.py \
23+
--preset mxbai_embed_large_v1_en
24+
```
25+
"""
26+
27+
import os
28+
29+
os.environ["KERAS_BACKEND"] = "torch"
30+
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
31+
32+
import numpy as np
33+
import torch
34+
from absl import app
35+
from absl import flags
36+
from keras import ops
37+
from sentence_transformers import SentenceTransformer
38+
39+
from keras_hub.src.models.bert.bert_text_embedder import BertTextEmbedder
40+
from keras_hub.src.models.xlm_roberta.xlm_roberta_text_embedder import (
41+
XLMRobertaTextEmbedder,
42+
)
43+
44+
FLAGS = flags.FLAGS
45+
46+
flags.DEFINE_string(
47+
"preset",
48+
None,
49+
"Preset name for output. Must be one of the keys in PRESET_MAP.",
50+
)
51+
52+
# BERT-based presets.
53+
BERT_PRESET_MAP = {
54+
"mxbai_embed_large_v1_en": "mixedbread-ai/mxbai-embed-large-v1",
55+
"mxbai_embed_2d_large_v1_en": "mixedbread-ai/mxbai-embed-2d-large-v1",
56+
"mxbai_embed_xsmall_v1_en": "mixedbread-ai/mxbai-embed-xsmall-v1",
57+
}
58+
59+
# XLM-RoBERTa-based presets.
60+
XLM_ROBERTA_PRESET_MAP = {
61+
"deepset_mxbai_embed_de_large_v1": (
62+
"mixedbread-ai/deepset-mxbai-embed-de-large-v1"
63+
),
64+
}
65+
66+
# Combined map for --preset lookup.
67+
PRESET_MAP = {**BERT_PRESET_MAP, **XLM_ROBERTA_PRESET_MAP}
68+
69+
70+
def validate_output(keras_model, hf_model_id, is_xlm_roberta=False):
71+
"""Print embedding diagnostics for manual review.
72+
73+
Prints parameter count, embedding statistics (mean logits),
74+
cosine similarity rankings, and semantic search results.
75+
Does NOT gate on pass/fail — always returns.
76+
77+
Args:
78+
keras_model: The converted KerasHub text embedder.
79+
hf_model_id: The HuggingFace model ID to compare against.
80+
is_xlm_roberta: Whether this is an XLM-RoBERTa model.
81+
"""
82+
print("\n" + "=" * 60)
83+
print("NUMERICAL VERIFICATION")
84+
print("=" * 60)
85+
86+
# Load HuggingFace model for comparison.
87+
print(f"\nLoading HuggingFace model: {hf_model_id}")
88+
hf_model = SentenceTransformer(hf_model_id)
89+
hf_model.eval()
90+
91+
# =========================================
92+
# PARAMETER COUNT
93+
# =========================================
94+
print("\n--- Parameter Count ---")
95+
keras_params = keras_model.count_params()
96+
hf_modules = list(hf_model._modules.values())
97+
hf_transformer = hf_modules[0]
98+
if is_xlm_roberta:
99+
hf_params = sum(
100+
p.numel()
101+
for name, p in hf_transformer.auto_model.named_parameters()
102+
if not name.startswith("pooler.")
103+
and name != "embeddings.token_type_embeddings.weight"
104+
)
105+
# Subtract the 2 reserved XLM-R position embedding padding rows.
106+
pos_emb = hf_transformer.auto_model.embeddings.position_embeddings
107+
hf_params -= 2 * pos_emb.weight.shape[1]
108+
else:
109+
hf_params = sum(
110+
p.numel() for p in hf_transformer.auto_model.parameters()
111+
)
112+
113+
print(f"KerasHub params: {keras_params:,}")
114+
print(f"HuggingFace params: {hf_params:,}")
115+
param_diff = abs(keras_params - hf_params)
116+
if param_diff == 0:
117+
print("✅ Parameter count EXACT MATCH")
118+
else:
119+
print(f"⚠️ Parameter count diff: {param_diff:,}")
120+
121+
# =========================================
122+
# EMBEDDING COMPARISON
123+
# =========================================
124+
print("\n--- Embedding Comparison ---")
125+
test_texts = [
126+
"The weather is lovely today.",
127+
"It's so sunny outside!",
128+
"He drove to the stadium.",
129+
]
130+
print(f"Test inputs: {test_texts}")
131+
132+
# HuggingFace embeddings.
133+
print("\nComputing HF embeddings...")
134+
with torch.no_grad():
135+
hf_embeddings = hf_model.encode(test_texts, convert_to_numpy=True)
136+
print(f"HF output shape: {hf_embeddings.shape}")
137+
print(f"HF embedding[0][:5]: {hf_embeddings[0][:5]}")
138+
print(f"HF embedding mean: {np.mean(hf_embeddings):.6e}")
139+
140+
# KerasHub embeddings.
141+
print("\nComputing KerasHub embeddings...")
142+
keras_embeddings = ops.convert_to_numpy(keras_model.predict(test_texts))
143+
print(f"KerasHub output shape: {keras_embeddings.shape}")
144+
print(f"KerasHub embedding[0][:5]: {keras_embeddings[0][:5]}")
145+
print(f"KerasHub embedding mean: {np.mean(keras_embeddings):.6e}")
146+
147+
# Differences.
148+
print("\n--- Mean Logits Comparison ---")
149+
max_diff = np.max(np.abs(hf_embeddings - keras_embeddings))
150+
mean_diff = np.mean(np.abs(hf_embeddings - keras_embeddings))
151+
print(f"Max absolute difference: {max_diff:.2e}")
152+
print(f"Mean absolute difference: {mean_diff:.2e}")
153+
154+
# Norm check.
155+
keras_norms = np.linalg.norm(keras_embeddings, axis=1)
156+
hf_norms = np.linalg.norm(hf_embeddings, axis=1)
157+
print(f"KerasHub norms: {keras_norms}")
158+
print(f"HF norms: {hf_norms}")
159+
160+
# =========================================
161+
# COSINE SIMILARITY RANKING
162+
# =========================================
163+
print("\n--- Cosine Similarity Matrix ---")
164+
keras_sims = keras_embeddings @ keras_embeddings.T
165+
hf_sims = hf_embeddings @ hf_embeddings.T
166+
print(
167+
f"KerasHub sim[0,1]={keras_sims[0, 1]:.4f} "
168+
f"sim[0,2]={keras_sims[0, 2]:.4f}"
169+
)
170+
print(
171+
f"HF sim[0,1]={hf_sims[0, 1]:.4f} sim[0,2]={hf_sims[0, 2]:.4f}"
172+
)
173+
174+
keras_ranking_ok = keras_sims[0, 1] > keras_sims[0, 2]
175+
hf_ranking_ok = hf_sims[0, 1] > hf_sims[0, 2]
176+
print(
177+
f"Ranking consistency: KerasHub={keras_ranking_ok}, HF={hf_ranking_ok}"
178+
)
179+
180+
# =========================================
181+
# SEMANTIC SEARCH
182+
# =========================================
183+
print("\n--- Semantic Search ---")
184+
query = "Which planet is known as the Red Planet?"
185+
documents = [
186+
"Venus is often called Earth's twin.",
187+
"Mars is often referred to as the Red Planet.",
188+
"Jupiter has a prominent red spot.",
189+
]
190+
print(f"Query: {query}")
191+
print(f"Documents: {documents}")
192+
193+
# KerasHub search.
194+
keras_q = keras_model.encode_text(query)
195+
keras_d = keras_model.encode_documents(documents)
196+
keras_search_sims = ops.convert_to_numpy(
197+
keras_model.similarity(keras_q, keras_d)
198+
)
199+
keras_best = int(np.argmax(keras_search_sims))
200+
201+
# HuggingFace search.
202+
with torch.no_grad():
203+
hf_q = hf_model.encode([query], convert_to_numpy=True)
204+
hf_d = hf_model.encode(documents, convert_to_numpy=True)
205+
hf_search_sims = hf_q @ hf_d.T
206+
hf_best = int(np.argmax(hf_search_sims))
207+
208+
print(
209+
f"\nKerasHub sims: {keras_search_sims[0]} -> "
210+
f"Best: {documents[keras_best]}"
211+
)
212+
print(f"HF sims: {hf_search_sims[0]} -> Best: {documents[hf_best]}")
213+
search_match = keras_best == hf_best
214+
print(f"Search ranking match: {search_match}")
215+
216+
print("\n" + "=" * 60)
217+
print("NUMERICAL VERIFICATION COMPLETE")
218+
print("=" * 60)
219+
220+
221+
def main(_):
222+
"""Main entry point: convert, validate, and save preset."""
223+
preset = FLAGS.preset
224+
if preset not in PRESET_MAP:
225+
raise ValueError(
226+
f"Invalid preset '{preset}'. "
227+
f"Must be one of: {list(PRESET_MAP.keys())}"
228+
)
229+
230+
hf_model_id = PRESET_MAP[preset]
231+
is_xlm_roberta = preset in XLM_ROBERTA_PRESET_MAP
232+
233+
print(f"\n{'=' * 60}")
234+
print(f"Converting: {hf_model_id} -> {preset}")
235+
arch = "XLM-RoBERTa" if is_xlm_roberta else "BERT"
236+
print(f"Architecture: {arch}")
237+
print(f"{'=' * 60}\n")
238+
239+
# Load and convert using the existing KerasHub HF converters.
240+
hf_uri = f"hf://{hf_model_id}"
241+
print(f"Loading from preset: {hf_uri}")
242+
if is_xlm_roberta:
243+
embedder = XLMRobertaTextEmbedder.from_preset(hf_uri)
244+
else:
245+
embedder = BertTextEmbedder.from_preset(hf_uri)
246+
247+
print(f"Backbone parameters: {embedder.backbone.count_params():,}")
248+
249+
# Validate embeddings for manual review.
250+
validate_output(embedder, hf_model_id, is_xlm_roberta)
251+
252+
# Always save the preset.
253+
print(f"\nSaving to preset: ./{preset}")
254+
embedder.save_to_preset(preset)
255+
print(f"\n✅ Successfully converted and saved to: ./{preset}")
256+
257+
258+
if __name__ == "__main__":
259+
flags.mark_flag_as_required("preset")
260+
app.run(main)

0 commit comments

Comments
 (0)