Skip to content

Commit b96151e

Browse files
Add Microsoft Harrier Model to Hub (keras-team#2764)
* Add microsoft/harrier-oss-v1-0.6b Model to Hub * Fix gemini code assist review comments * Fix gemini code assist review comments and colab error * Fix gemini code assist review comments * Fix gemini code assist review comments * Add qwen3 text embedder and preprossor * Fix gemini code assist review comments * Update dtype to float32 for numerics match * Remove gemini assist safe check code suggestions * Add harrier embedding oss 270m model support * Fix Gemini Code assist review comments * Fix review comments for gemma3 embedding models * Fix Gemini Code assist review comments * Update Harrier Kaggle Handles * Fix preset name for harrier model --------- Co-authored-by: Vijay Dabur <vijay_dabur@epam.com>
1 parent b7f1b57 commit b96151e

17 files changed

Lines changed: 1613 additions & 27 deletions

keras_hub/api/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@
322322
from keras_hub.src.models.gemma3.gemma3_causal_lm_preprocessor import (
323323
Gemma3CausalLMPreprocessor as Gemma3CausalLMPreprocessor,
324324
)
325+
from keras_hub.src.models.gemma3.gemma3_text_embedder import (
326+
Gemma3TextEmbedder as Gemma3TextEmbedder,
327+
)
328+
from keras_hub.src.models.gemma3.gemma3_text_embedder_preprocessor import (
329+
Gemma3TextEmbedderPreprocessor as Gemma3TextEmbedderPreprocessor,
330+
)
325331
from keras_hub.src.models.gemma3.gemma3_tokenizer import (
326332
Gemma3Tokenizer as Gemma3Tokenizer,
327333
)
@@ -609,6 +615,12 @@
609615
from keras_hub.src.models.qwen3.qwen3_causal_lm_preprocessor import (
610616
Qwen3CausalLMPreprocessor as Qwen3CausalLMPreprocessor,
611617
)
618+
from keras_hub.src.models.qwen3.qwen3_text_embedder import (
619+
Qwen3TextEmbedder as Qwen3TextEmbedder,
620+
)
621+
from keras_hub.src.models.qwen3.qwen3_text_embedder_preprocessor import (
622+
Qwen3TextEmbedderPreprocessor as Qwen3TextEmbedderPreprocessor,
623+
)
612624
from keras_hub.src.models.qwen3.qwen3_tokenizer import (
613625
Qwen3Tokenizer as Qwen3Tokenizer,
614626
)

keras_hub/src/models/gemma3/gemma3_presets.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,18 @@
303303
},
304304
"kaggle_handle": "kaggle://keras/embeddinggemma/keras/embedding_gemma3_300m/2",
305305
},
306+
# Microsoft harrier model presets
307+
"harrier_embedding_oss_270m": {
308+
"metadata": {
309+
"description": (
310+
"Microsoft harrier-oss-v1 270M multilingual text embedding "
311+
"model based on the Gemma3-270M architecture, fine-tuned for "
312+
"dense retrieval and semantic similarity across 94+ languages. "
313+
"Achieves 66.5 on Multilingual MTEB v2."
314+
),
315+
"params": 268098176,
316+
"path": "gemma3",
317+
},
318+
"kaggle_handle": "kaggle://keras/harrier/keras/harrier_embedding_oss_270m/1",
319+
},
306320
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
from keras import ops
2+
3+
from keras_hub.src.api_export import keras_hub_export
4+
from keras_hub.src.models.gemma3.gemma3_backbone import Gemma3Backbone
5+
from keras_hub.src.models.gemma3.gemma3_text_embedder_preprocessor import (
6+
Gemma3TextEmbedderPreprocessor,
7+
)
8+
from keras_hub.src.models.text_embedder import TextEmbedder
9+
10+
11+
@keras_hub_export("keras_hub.models.Gemma3TextEmbedder")
12+
class Gemma3TextEmbedder(TextEmbedder):
13+
"""An end-to-end Gemma3 model for generating sentence embeddings.
14+
15+
This model attaches a pooling and optional L2 normalization head to a
16+
`keras_hub.models.Gemma3Backbone` instance, mapping from the backbone
17+
outputs to fixed-size sentence embeddings suitable for semantic
18+
similarity, clustering, and retrieval tasks.
19+
20+
This is the architecture used by Microsoft's harrier-oss-v1-270m embedding
21+
model. For usage of this model with pre-trained weights, use the
22+
`from_preset()` constructor.
23+
24+
This model can optionally be configured with a `preprocessor` layer, in
25+
which case it will automatically apply preprocessing to raw inputs during
26+
`fit()`, `predict()`, and `evaluate()`. This is done by default when
27+
creating the model with `from_preset()`.
28+
29+
Disclaimer: Pre-trained models are provided on an "as is" basis, without
30+
warranties or conditions of any kind.
31+
32+
Args:
33+
backbone: A `keras_hub.models.Gemma3Backbone` instance.
34+
preprocessor: A `keras_hub.models.Gemma3TextEmbedderPreprocessor` or
35+
`None`. If `None`, this model will not apply preprocessing, and
36+
inputs should be preprocessed before calling the model.
37+
pooling_mode: string. The pooling strategy to use. One of `"last"` or
38+
`"mean"`. Defaults to `"last"`.
39+
- `"last"`: Use the hidden state of the last non-padding token.
40+
This is the standard pooling strategy for decoder-only models
41+
and matches the approach used by harrier-oss-v1-270m.
42+
- `"mean"`: Attention-mask-aware mean pooling over all tokens.
43+
normalize: bool. Whether to L2 normalize the output embeddings.
44+
Defaults to `True`.
45+
46+
Examples:
47+
48+
Raw string data.
49+
```python
50+
embedder = keras_hub.models.Gemma3TextEmbedder.from_preset(
51+
"hf://microsoft/harrier-oss-v1-270m",
52+
)
53+
54+
# Semantic search.
55+
query = "Which planet is known as the Red Planet?"
56+
documents = [
57+
"Mars is often referred to as the Red Planet.",
58+
"Venus is often called Earth's twin.",
59+
]
60+
q_emb = embedder.encode_text(
61+
"Instruct: Given a web search query, retrieve relevant passages\n"
62+
"Query: " + query
63+
)
64+
d_embs = embedder.encode_text(documents)
65+
sims = embedder.similarity(q_emb, d_embs)
66+
print("Best match:", documents[sims.numpy().argmax()])
67+
```
68+
69+
Preprocessed integer data.
70+
```python
71+
features = {
72+
"token_ids": np.ones(shape=(2, 12), dtype="int32"),
73+
"padding_mask": np.ones(shape=(2, 12), dtype="int32"),
74+
}
75+
76+
embedder = keras_hub.models.Gemma3TextEmbedder.from_preset(
77+
"hf://microsoft/harrier-oss-v1-270m",
78+
preprocessor=None,
79+
)
80+
embeddings = embedder.predict(features)
81+
```
82+
83+
Reference:
84+
- [harrier-oss-v1 model page](https://huggingface.co/microsoft/harrier-oss-v1-270m)
85+
"""
86+
87+
backbone_cls = Gemma3Backbone
88+
preprocessor_cls = Gemma3TextEmbedderPreprocessor
89+
90+
def __init__(
91+
self,
92+
backbone,
93+
preprocessor=None,
94+
pooling_mode="last",
95+
normalize=True,
96+
**kwargs,
97+
):
98+
# === Layers ===
99+
self.backbone = backbone
100+
self.preprocessor = preprocessor
101+
102+
# === Functional Model ===
103+
inputs = backbone.input
104+
backbone_output = backbone(inputs)
105+
padding_mask = inputs["padding_mask"]
106+
107+
if (
108+
isinstance(backbone_output, dict)
109+
and "pooled_output" in backbone_output
110+
):
111+
pooled = backbone_output["pooled_output"]
112+
else:
113+
if isinstance(backbone_output, dict):
114+
sequence_output = backbone_output["sequence_output"]
115+
else:
116+
sequence_output = backbone_output
117+
118+
# Apply pooling.
119+
if pooling_mode == "last":
120+
pooled = self._last_token_pooling(sequence_output, padding_mask)
121+
elif pooling_mode == "mean":
122+
pooled = self._mean_pooling(sequence_output, padding_mask)
123+
else:
124+
raise ValueError(
125+
f"Invalid pooling_mode: '{pooling_mode}'. "
126+
"Expected one of 'last' or 'mean'."
127+
)
128+
129+
# Apply L2 normalization.
130+
if normalize:
131+
pooled = self._l2_normalize(pooled)
132+
133+
super().__init__(
134+
inputs=inputs,
135+
outputs=pooled,
136+
**kwargs,
137+
)
138+
139+
# === Config ===
140+
self.pooling_mode = pooling_mode
141+
self.normalize = normalize
142+
143+
@staticmethod
144+
def _last_token_pooling(sequence_output, padding_mask):
145+
"""Pool the hidden state of the last non-padding token."""
146+
mask = ops.cast(padding_mask, sequence_output.dtype)
147+
# Shift mask left to find the last real token (where mask=1, next=0).
148+
mask_shifted = ops.pad(mask, [[0, 0], [0, 1]])[:, 1:]
149+
last_token_mask = mask * (1.0 - mask_shifted)
150+
return ops.sum(
151+
sequence_output * ops.expand_dims(last_token_mask, axis=-1),
152+
axis=1,
153+
)
154+
155+
@staticmethod
156+
def _mean_pooling(sequence_output, padding_mask):
157+
"""Attention-mask-aware mean pooling over non-padding tokens."""
158+
mask = ops.cast(
159+
ops.expand_dims(padding_mask, axis=-1), sequence_output.dtype
160+
)
161+
sum_embeddings = ops.sum(sequence_output * mask, axis=1)
162+
sum_mask = ops.maximum(ops.sum(mask, axis=1), 1e-5)
163+
return sum_embeddings / sum_mask
164+
165+
@staticmethod
166+
def _l2_normalize(embeddings):
167+
"""L2-normalize embeddings along the last axis."""
168+
return ops.nn.normalize(embeddings, axis=-1, order=2)
169+
170+
def get_config(self):
171+
config = super().get_config()
172+
config.update(
173+
{
174+
"pooling_mode": self.pooling_mode,
175+
"normalize": self.normalize,
176+
}
177+
)
178+
return config
179+
180+
def similarity(self, query_embeddings, document_embeddings):
181+
"""Compute similarity between query and document embeddings.
182+
183+
Computes the dot product between query and document embeddings.
184+
When embeddings are L2 normalized (the default), this is
185+
equivalent to cosine similarity. Returns a similarity matrix
186+
of shape `(num_queries, num_documents)`.
187+
188+
Args:
189+
query_embeddings: An array or tensor of shape
190+
`(num_queries, embedding_dim)`.
191+
document_embeddings: An array or tensor of shape
192+
`(num_documents, embedding_dim)`.
193+
194+
Returns:
195+
A tensor of shape `(num_queries, num_documents)` containing
196+
similarity scores.
197+
"""
198+
query_tensor = ops.convert_to_tensor(query_embeddings)
199+
document_tensor = ops.convert_to_tensor(document_embeddings)
200+
return ops.matmul(query_tensor, ops.transpose(document_tensor))
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from keras_hub.src.api_export import keras_hub_export
2+
from keras_hub.src.models.gemma3.gemma3_backbone import Gemma3Backbone
3+
from keras_hub.src.models.gemma3.gemma3_tokenizer import Gemma3Tokenizer
4+
from keras_hub.src.models.text_embedder_preprocessor import (
5+
TextEmbedderPreprocessor,
6+
)
7+
8+
9+
@keras_hub_export("keras_hub.models.Gemma3TextEmbedderPreprocessor")
10+
class Gemma3TextEmbedderPreprocessor(TextEmbedderPreprocessor):
11+
"""A Gemma3 preprocessing layer which tokenizes and packs inputs for
12+
sentence embedding.
13+
14+
This preprocessing layer will do three things:
15+
16+
1. Tokenize any number of input segments using the `tokenizer`.
17+
2. Pack the inputs together using a `keras_hub.layers.MultiSegmentPacker`
18+
with the appropriate `<bos>`, `<eos>`, and `<pad>` tokens.
19+
3. Construct a dictionary with keys `"token_ids"` and `"padding_mask"`
20+
that can be passed directly to a `Gemma3TextEmbedder` model.
21+
22+
This layer can be used directly with `tf.data.Dataset.map` to preprocess
23+
string data in the `(x, y, sample_weight)` format used by
24+
`keras.Model.fit`.
25+
26+
Args:
27+
tokenizer: A `keras_hub.models.Gemma3Tokenizer` instance.
28+
sequence_length: int. The length of the packed inputs. Defaults to
29+
`256`.
30+
truncate: string. The algorithm to truncate a list of batched segments
31+
to fit within `sequence_length`. The value can be either
32+
`round_robin` or `waterfall`:
33+
- `"round_robin"`: Available space is assigned one token at a
34+
time in a round-robin fashion to the inputs that still need
35+
some, until the limit is reached.
36+
- `"waterfall"`: The allocation of the budget is done using a
37+
"waterfall" algorithm that allocates quota in a
38+
left-to-right manner and fills up the buckets until we run
39+
out of budget. It supports an arbitrary number of segments.
40+
41+
Call arguments:
42+
x: A tensor of single string sequences, or a tuple of multiple
43+
tensor sequences to be packed together. Inputs may be batched or
44+
unbatched. For single sequences, raw python inputs will be
45+
converted to tensors. For multiple sequences, pass tensors
46+
directly.
47+
y: Any label data. Will be passed through unaltered.
48+
sample_weight: Any label weight data. Will be passed through
49+
unaltered.
50+
51+
Examples:
52+
53+
Directly calling the layer on data.
54+
```python
55+
preprocessor = keras_hub.models.Gemma3TextEmbedderPreprocessor.from_preset(
56+
"harrier_embedding_oss_270m",
57+
)
58+
59+
# Tokenize and pack a single sentence.
60+
preprocessor("The quick brown fox jumped.")
61+
62+
# Tokenize a batch of single sentences.
63+
preprocessor(["The quick brown fox jumped.", "Call me Ishmael."])
64+
```
65+
66+
Mapping with `tf.data.Dataset`.
67+
```python
68+
preprocessor = keras_hub.models.Gemma3TextEmbedderPreprocessor.from_preset(
69+
"harrier_embedding_oss_270m",
70+
)
71+
72+
first = tf.constant(["The quick brown fox jumped.", "Call me Ishmael."])
73+
74+
# Map unlabeled single sentences.
75+
ds = tf.data.Dataset.from_tensor_slices(first)
76+
ds = ds.map(preprocessor, num_parallel_calls=tf.data.AUTOTUNE)
77+
```
78+
"""
79+
80+
backbone_cls = Gemma3Backbone
81+
tokenizer_cls = Gemma3Tokenizer
82+
83+
def _format_output(self, token_ids, segment_ids):
84+
"""Format packer output into Gemma3-compatible input dictionary."""
85+
return {
86+
"token_ids": token_ids,
87+
"padding_mask": token_ids != self.tokenizer.pad_token_id,
88+
}

0 commit comments

Comments
 (0)