Skip to content

Commit e18a94f

Browse files
Enable Python workflow with BytePairTokenizer and StartEndPacker (#2628)
* Merge `v2.BytePairTokenizer` into `BytePairTokenizer` and `v2.StartEndPacker` to `StartEndPacker`. * Remove `print`. * Update tests and fix the docstrings. * Resolve Gemini CLI's comment. * Fix. * Add python workflow for `Qwen3_5Tokenizer` and update Qwen3.5 tests. * Fix gemini's comments. * Improve the performance of `StartAndPacker` and add notes.
1 parent 990c3d3 commit e18a94f

68 files changed

Lines changed: 1591 additions & 1688 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

keras_hub/api/layers/v2/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,3 @@
77
from keras_hub.src.layers.preprocessing.v2.multi_segment_packer import (
88
MultiSegmentPacker as MultiSegmentPacker,
99
)
10-
from keras_hub.src.layers.preprocessing.v2.start_end_packer import (
11-
StartEndPacker as StartEndPacker,
12-
)

keras_hub/api/tokenizers/v2/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
since your modifications would be overwritten.
55
"""
66

7-
from keras_hub.src.tokenizers.v2.byte_pair_tokenizer import (
8-
BytePairTokenizer as BytePairTokenizer,
9-
)
107
from keras_hub.src.tokenizers.v2.sentence_piece_tokenizer import (
118
SentencePieceTokenizer as SentencePieceTokenizer,
129
)

keras_hub/src/layers/preprocessing/preprocessing_layer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ class PreprocessingLayer(keras.layers.Layer):
77
"""Preprocessing layer base class."""
88

99
def __init__(self, **kwargs):
10-
assert_tf_libs_installed(self.__class__.__name__)
10+
_allow_python_workflow = kwargs.pop("_allow_python_workflow", False)
11+
if not _allow_python_workflow:
12+
assert_tf_libs_installed(self.__class__.__name__)
1113
super().__init__(**kwargs)
1214
# Don't convert inputs (we want tf tensors not backend tensors).
1315
self._convert_input_args = False
1416
# Allow raw inputs like python strings.
1517
self._allow_non_tensor_positional_args = True
18+
# Allow Python workflow. Historically, KerasHub preprocessing layers
19+
# required TF and TF text libraries.
20+
self._allow_python_workflow = _allow_python_workflow
1621
# Most pre-preprocessing has no build.
1722
if not hasattr(self, "build"):
1823
self.built = True

keras_hub/src/layers/preprocessing/start_end_packer.py

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import keras
2+
import numpy as np
3+
14
from keras_hub.src.api_export import keras_hub_export
25
from keras_hub.src.layers.preprocessing.preprocessing_layer import (
36
PreprocessingLayer,
47
)
8+
from keras_hub.src.utils.tensor_utils import (
9+
convert_preprocessing_outputs_python,
10+
)
11+
from keras_hub.src.utils.tensor_utils import convert_to_list
512
from keras_hub.src.utils.tensor_utils import convert_to_ragged_batch
13+
from keras_hub.src.utils.tensor_utils import in_tf_function
614
from keras_hub.src.utils.tensor_utils import pad
715
from keras_hub.src.utils.tensor_utils import preprocessing_function
816

@@ -74,7 +82,7 @@ class StartEndPacker(PreprocessingLayer):
7482
[ 1, 8, 9, 10, 11, 2]], dtype=int32)
7583
7684
Unbatched input (str).
77-
>>> inputs = tf.constant(["this", "is", "fun"])
85+
>>> inputs = ["this", "is", "fun"]
7886
>>> start_end_packer = keras_hub.layers.StartEndPacker(
7987
... sequence_length=6, start_value="<s>", end_value="</s>",
8088
... pad_value="<pad>"
@@ -84,7 +92,7 @@ class StartEndPacker(PreprocessingLayer):
8492
array(['<s>', 'this', 'is', 'fun', '</s>', '<pad>'], dtype='<U5')
8593
8694
Batched input (str).
87-
>>> inputs = tf.ragged.constant([["this", "is", "fun"], ["awesome"]])
95+
>>> inputs = [["this", "is", "fun"], ["awesome"]]
8896
>>> start_end_packer = keras_hub.layers.StartEndPacker(
8997
... sequence_length=6, start_value="<s>", end_value="</s>",
9098
... pad_value="<pad>"
@@ -95,7 +103,7 @@ class StartEndPacker(PreprocessingLayer):
95103
['<s>', 'awesome', '</s>', '<pad>', '<pad>', '<pad>']], dtype='<U7')
96104
97105
Multiple start tokens.
98-
>>> inputs = tf.ragged.constant([["this", "is", "fun"], ["awesome"]])
106+
>>> inputs = [["this", "is", "fun"], ["awesome"]]
99107
>>> start_end_packer = keras_hub.layers.StartEndPacker(
100108
... sequence_length=6, start_value=["</s>", "<s>"], end_value="</s>",
101109
... pad_value="<pad>"
@@ -117,7 +125,10 @@ def __init__(
117125
padding_side="right",
118126
**kwargs,
119127
):
120-
super().__init__(name=name, **kwargs)
128+
_allow_python_workflow = kwargs.pop("_allow_python_workflow", True)
129+
super().__init__(
130+
name=name, _allow_python_workflow=_allow_python_workflow, **kwargs
131+
)
121132

122133
self.sequence_length = sequence_length
123134

@@ -126,6 +137,8 @@ def __init__(
126137
self._end_value = end_value
127138

128139
def check_special_value_type(value, value_name):
140+
if value is None:
141+
return None
129142
if isinstance(value, (int, str)):
130143
return [value]
131144
if value and not isinstance(value, (list, tuple)):
@@ -146,7 +159,7 @@ def check_special_value_type(value, value_name):
146159
self.padding_side = padding_side
147160

148161
@preprocessing_function
149-
def call(
162+
def _call_tf(
150163
self,
151164
inputs,
152165
sequence_length=None,
@@ -203,6 +216,174 @@ def call(
203216
return outputs, mask
204217
return outputs
205218

219+
def _call_python(
220+
self,
221+
inputs,
222+
sequence_length=None,
223+
add_start_value=True,
224+
add_end_value=True,
225+
):
226+
# TODO(hongyuc): Improve the performance of `_call_python`. It becomes
227+
# slower when encountering large inputs compared to `_call_tf`.
228+
def _canonicalize_inputs(inputs):
229+
if isinstance(inputs, (tuple, list)):
230+
# Fast path for common cases:
231+
# If the inputs are just normal python types (or lists of
232+
# python types), it immediately returns.
233+
if not inputs:
234+
return [list(inputs)], False
235+
first = inputs[0]
236+
if isinstance(
237+
first, (int, str, float, bool, np.integer, np.floating)
238+
):
239+
return [list(inputs)], False
240+
if isinstance(first, (tuple, list)) and (
241+
not first
242+
or isinstance(
243+
first[0],
244+
(int, str, float, bool, np.integer, np.floating),
245+
)
246+
):
247+
return [list(x) for x in inputs], True
248+
249+
# `keras.tree.map_structure` is expensive.
250+
inputs = keras.tree.map_structure(convert_to_list, inputs)
251+
if inputs and isinstance(inputs[0], (tuple, list)):
252+
return inputs, True
253+
else:
254+
return [inputs], False
255+
elif tf is not None and isinstance(
256+
inputs, (tf.Tensor, tf.RaggedTensor)
257+
):
258+
unbatched = inputs.shape.rank == 1
259+
if unbatched:
260+
inputs = tf.expand_dims(inputs, 0)
261+
if isinstance(inputs, tf.Tensor):
262+
inputs = convert_to_list(inputs)
263+
else:
264+
inputs = inputs.to_list()
265+
return inputs, not unbatched
266+
elif keras.ops.is_tensor(inputs):
267+
inputs = convert_to_list(inputs)
268+
if inputs and isinstance(inputs[0], (tuple, list)):
269+
return inputs, True
270+
else:
271+
return [inputs], False
272+
else:
273+
raise ValueError(
274+
f"Input should be a list or a list of lists. "
275+
f"Received: {inputs}"
276+
)
277+
278+
def _get_type(inputs):
279+
for sequence in inputs:
280+
if sequence is not None and len(sequence) > 0:
281+
return type(sequence[0])
282+
return int # Default to int if all sequences are empty.
283+
284+
def _canonicalize_value(values, input_type):
285+
if input_type is str:
286+
return [str(v) for v in values]
287+
else:
288+
return [int(v) for v in values]
289+
290+
def _pad(x, pad_value, padding_side, sequence_length, input_type=None):
291+
if padding_side not in ("left", "right"):
292+
raise ValueError(
293+
"padding_side must be 'left' or 'right'. "
294+
f"Received: {padding_side}"
295+
)
296+
if pad_value is None:
297+
pad_value = "" if input_type is str else 0
298+
if padding_side == "right":
299+
x = [
300+
seq + [pad_value] * (sequence_length - len(seq))
301+
for seq in x
302+
]
303+
else:
304+
x = [
305+
[pad_value] * (sequence_length - len(seq)) + seq
306+
for seq in x
307+
]
308+
return x
309+
310+
def _canonicalize_outputs(outputs, dtype=None):
311+
flat_outputs = keras.tree.flatten(outputs)
312+
if not flat_outputs:
313+
return np.array(outputs, dtype=dtype or "int32")
314+
first_element = flat_outputs[0]
315+
if not isinstance(first_element, str):
316+
return np.array(outputs, dtype=dtype or "int32")
317+
else:
318+
return outputs
319+
320+
inputs, batched = _canonicalize_inputs(inputs)
321+
input_type = _get_type(inputs)
322+
sequence_length = sequence_length or self.sequence_length
323+
x = inputs
324+
325+
# Truncate and normalize to list of lists.
326+
truncation_length = sequence_length
327+
if add_start_value and self.start_value is not None:
328+
truncation_length -= len(self.start_value)
329+
if add_end_value and self.end_value is not None:
330+
truncation_length -= len(self.end_value)
331+
x = [list(seq)[:truncation_length] for seq in x]
332+
333+
# Concatenate start and end tokens.
334+
if add_start_value and self.start_value is not None:
335+
start_value = _canonicalize_value(self.start_value, input_type)
336+
x = [start_value + seq for seq in x]
337+
if add_end_value and self.end_value is not None:
338+
end_value = _canonicalize_value(self.end_value, input_type)
339+
x = [seq + end_value for seq in x]
340+
341+
# Pad to desired length.
342+
outputs = _pad(
343+
x,
344+
pad_value=self.pad_value,
345+
padding_side=self.padding_side,
346+
sequence_length=sequence_length,
347+
input_type=input_type,
348+
)
349+
outputs = _canonicalize_outputs(outputs)
350+
outputs = outputs[0] if not batched else outputs
351+
352+
if self.return_padding_mask:
353+
masks = keras.tree.map_structure(lambda _: True, x)
354+
masks = _pad(
355+
masks,
356+
pad_value=False,
357+
padding_side=self.padding_side,
358+
sequence_length=sequence_length,
359+
)
360+
masks = masks[0] if not batched else masks
361+
masks = _canonicalize_outputs(masks, dtype="bool")
362+
return convert_preprocessing_outputs_python((outputs, masks))
363+
return convert_preprocessing_outputs_python(outputs)
364+
365+
def call(
366+
self,
367+
inputs,
368+
sequence_length=None,
369+
add_start_value=True,
370+
add_end_value=True,
371+
):
372+
if not self._allow_python_workflow or in_tf_function():
373+
return self._call_tf(
374+
inputs,
375+
sequence_length=sequence_length,
376+
add_start_value=add_start_value,
377+
add_end_value=add_end_value,
378+
)
379+
else:
380+
return self._call_python(
381+
inputs,
382+
sequence_length=sequence_length,
383+
add_start_value=add_start_value,
384+
add_end_value=add_end_value,
385+
)
386+
206387
def get_config(self):
207388
config = super().get_config()
208389
config.update(
@@ -220,4 +401,6 @@ def get_config(self):
220401
def compute_output_shape(self, inputs_shape):
221402
inputs_shape = list(inputs_shape)
222403
inputs_shape[-1] = self.sequence_length
404+
if self.return_padding_mask:
405+
return tuple(inputs_shape), tuple(inputs_shape)
223406
return tuple(inputs_shape)

0 commit comments

Comments
 (0)