Skip to content

Commit c5b81bb

Browse files
Enable python workflows for ImageConverter, MaskedLMMaskGenerator, MultiSegmentPacker, CausalLMPreprocessor, MaskedLMPreprocessor, TextClassifierPreprocessor (#2769)
* Merge python workflow into `MultiSegmentPacker`. * Add python workflow to `MaskedLMPreprocessor` and `TextClassifierPreprocessor`. * Enable python workflow for `MaskedLMMaskGenerator`. * Remove redundant `preprocessing_function` and update the implementations. * Enable python workflow to `ImageConverter`. * Enable python workflow to PaliGemma. * Update. * Fix gemini's comments. * Fix `ESMProteinClassifierPreprocessor`. * Fix list input preprocessing in ImageConverter._call_python * Address code review comments from gemini-code-assistant * Move canonicalize_python_inputs and compute_padding_mask to keras_hub.src.utils.tensor_utils. Simplify the call functions in ESM and RoformerV2.
1 parent 91f913e commit c5b81bb

37 files changed

Lines changed: 825 additions & 1304 deletions

keras_hub/api/layers/__init__.py

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

7-
from keras_hub.layers import v2 as v2
87
from keras_hub.src.layers.modeling.alibi_bias import AlibiBias as AlibiBias
98
from keras_hub.src.layers.modeling.anchor_generator import (
109
AnchorGenerator as AnchorGenerator,

keras_hub/api/layers/v2/__init__.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

keras_hub/api/models/__init__.py

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

7-
from keras_hub.models import v2 as v2
87
from keras_hub.src.models.albert.albert_backbone import (
98
AlbertBackbone as AlbertBackbone,
109
)

keras_hub/api/models/v2/__init__.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

keras_hub/src/layers/preprocessing/image_converter.py

Lines changed: 42 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import math
22

33
import keras
4-
import ml_dtypes
54
import numpy as np
65
from keras import ops
76

@@ -20,95 +19,6 @@
2019
from keras_hub.src.utils.tensor_utils import preprocessing_function
2120

2221

23-
# TODO: Use `keras.layers.Resizing` once `antialias` is configurable.
24-
# https://github.com/keras-team/keras/pull/20972
25-
def _saturate_cast(x, dtype, backend_module):
26-
def get_dtype_min_max(dtype):
27-
if "bool" == dtype:
28-
dtype_min = 0
29-
dtype_max = 1
30-
elif "int" in dtype:
31-
dtype_min = ml_dtypes.iinfo(dtype).min
32-
dtype_max = ml_dtypes.iinfo(dtype).max
33-
else:
34-
dtype_min = ml_dtypes.finfo(dtype).min
35-
dtype_max = ml_dtypes.finfo(dtype).max
36-
return dtype_min, dtype_max
37-
38-
dtype = keras.backend.standardize_dtype(dtype)
39-
in_dtype = keras.backend.standardize_dtype(x.dtype)
40-
in_min, in_max = get_dtype_min_max(in_dtype)
41-
out_min, out_max = get_dtype_min_max(dtype)
42-
43-
min_limit = np.maximum(in_min, out_min).astype(in_dtype)
44-
if min_limit < out_min:
45-
min_limit = np.nextafter(min_limit, 0, dtype=in_dtype)
46-
max_limit = np.minimum(in_max, out_max).astype(in_dtype)
47-
if max_limit > out_max:
48-
max_limit = np.nextafter(max_limit, 0, dtype=in_dtype)
49-
50-
x = backend_module.numpy.clip(x, min_limit, max_limit)
51-
return backend_module.cast(x, dtype)
52-
53-
54-
class ResizingAntialiasConfigurable(keras.layers.Resizing):
55-
"""A preprocessing layer which resizes images.
56-
57-
This class is the same as `keras.layers.Resizing` but exposes `antialias` as
58-
a configurable parameter.
59-
"""
60-
61-
def __init__(
62-
self,
63-
height,
64-
width,
65-
interpolation="bilinear",
66-
antialias=False,
67-
crop_to_aspect_ratio=False,
68-
pad_to_aspect_ratio=False,
69-
fill_mode="constant",
70-
fill_value=0.0,
71-
data_format=None,
72-
**kwargs,
73-
):
74-
super().__init__(
75-
height=height,
76-
width=width,
77-
interpolation=interpolation,
78-
crop_to_aspect_ratio=crop_to_aspect_ratio,
79-
pad_to_aspect_ratio=pad_to_aspect_ratio,
80-
fill_mode=fill_mode,
81-
fill_value=fill_value,
82-
data_format=data_format,
83-
**kwargs,
84-
)
85-
self.antialias = bool(antialias)
86-
87-
def transform_images(self, images, transformation=None, training=True):
88-
size = (self.height, self.width)
89-
resized = self.backend.image.resize(
90-
images,
91-
size=size,
92-
interpolation=self.interpolation,
93-
antialias=self.antialias, # Added.
94-
data_format=self.data_format,
95-
crop_to_aspect_ratio=self.crop_to_aspect_ratio,
96-
pad_to_aspect_ratio=self.pad_to_aspect_ratio,
97-
fill_mode=self.fill_mode,
98-
fill_value=self.fill_value,
99-
)
100-
if resized.dtype == images.dtype:
101-
return resized
102-
if keras.backend.is_int_dtype(images.dtype):
103-
resized = self.backend.numpy.round(resized)
104-
return _saturate_cast(resized, images.dtype, self.backend)
105-
106-
def get_config(self):
107-
config = super().get_config()
108-
config.update({"antialias": self.antialias})
109-
return config
110-
111-
11222
@keras_hub_export("keras_hub.layers.ImageConverter")
11323
class ImageConverter(PreprocessingLayer):
11424
"""Preprocess raw images into model ready inputs.
@@ -213,7 +123,10 @@ def __init__(
213123
scale = [scale / s for s in std]
214124
offset = [-m / s for m, s in zip(kwargs.pop("mean"), std)]
215125

216-
super().__init__(**kwargs)
126+
_allow_python_workflow = kwargs.pop("_allow_python_workflow", True)
127+
super().__init__(
128+
_allow_python_workflow=_allow_python_workflow, **kwargs
129+
)
217130

218131
if crop_to_aspect_ratio and pad_to_aspect_ratio:
219132
raise ValueError(
@@ -226,7 +139,7 @@ def __init__(
226139
resizing_kwargs = {}
227140
if check_bounding_box_support():
228141
resizing_kwargs["bounding_box_format"] = bounding_box_format
229-
self.resizing = ResizingAntialiasConfigurable(
142+
self.resizing = keras.layers.Resizing(
230143
height=image_size[0] if image_size else None,
231144
width=image_size[1] if image_size else None,
232145
crop_to_aspect_ratio=crop_to_aspect_ratio,
@@ -262,8 +175,33 @@ def image_size(self, value):
262175
self.resizing.height = value[0]
263176
self.resizing.width = value[1]
264177

265-
@preprocessing_function
266-
def call(self, inputs):
178+
def _call_python(self, inputs):
179+
if isinstance(inputs, list):
180+
try:
181+
inputs = ops.convert_to_tensor(np.array(inputs))
182+
except ValueError:
183+
return [self._call_python(x) for x in inputs]
184+
elif isinstance(inputs, tuple):
185+
try:
186+
inputs = ops.convert_to_tensor(np.array(inputs))
187+
except ValueError:
188+
return tuple(self._call_python(x) for x in inputs)
189+
190+
def canonicalize(x):
191+
if isinstance(x, dict):
192+
return {k: canonicalize(v) for k, v in x.items()}
193+
if isinstance(x, tuple):
194+
return tuple(canonicalize(v) for v in x)
195+
if isinstance(x, list):
196+
try:
197+
return ops.convert_to_tensor(np.array(x))
198+
except ValueError:
199+
return [ops.convert_to_tensor(item) for item in x]
200+
if isinstance(x, np.ndarray):
201+
return ops.convert_to_tensor(x)
202+
return x
203+
204+
inputs = canonicalize(inputs)
267205
if self.image_size is not None:
268206
inputs = self.resizing(inputs)
269207
# Allow dictionary input for handling bounding boxes.
@@ -287,6 +225,16 @@ def call(self, inputs):
287225
inputs = x
288226
return inputs
289227

228+
@preprocessing_function
229+
def _call_tf(self, inputs):
230+
return self._call_python(inputs)
231+
232+
def call(self, inputs):
233+
if not self._allow_python_workflow or in_tf_function():
234+
return self._call_tf(inputs)
235+
else:
236+
return self._call_python(inputs)
237+
290238
def _expand_non_channel_dims(self, value, inputs):
291239
"""Expand non channel dims so value is broadcastable with inputs."""
292240
unbatched = len(ops.shape(inputs)) == 3

keras_hub/src/layers/preprocessing/image_converter_test.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,38 @@
1717

1818

1919
class ImageConverterTest(TestCase):
20+
def setUp(self):
21+
super().setUp()
22+
self._allow_python_workflow = True
23+
2024
def test_resize_simple(self):
21-
converter = ImageConverter(height=4, width=4, scale=1 / 255.0)
25+
converter = ImageConverter(
26+
height=4,
27+
width=4,
28+
scale=1 / 255.0,
29+
_allow_python_workflow=self._allow_python_workflow,
30+
)
2231
inputs = np.ones((10, 10, 3)) * 255.0
2332
outputs = converter(inputs)
2433
self.assertAllClose(outputs, ops.ones((4, 4, 3)))
2534

2635
def test_resize_dataset(self):
27-
converter = ImageConverter(image_size=(4, 4), scale=1 / 255.0)
36+
converter = ImageConverter(
37+
image_size=(4, 4),
38+
scale=1 / 255.0,
39+
_allow_python_workflow=self._allow_python_workflow,
40+
)
2841
ds = tf.data.Dataset.from_tensor_slices(tf.zeros((8, 10, 10, 3)))
2942
batch = ds.batch(2).map(converter).take(1).get_single_element()
3043
self.assertAllClose(batch, tf.zeros((2, 4, 4, 3)))
3144

3245
def test_resize_in_model(self):
33-
converter = ImageConverter(height=4, width=4, scale=1 / 255.0)
46+
converter = ImageConverter(
47+
height=4,
48+
width=4,
49+
scale=1 / 255.0,
50+
_allow_python_workflow=self._allow_python_workflow,
51+
)
3452
inputs = keras.Input(shape=(10, 10, 3))
3553
outputs = converter(inputs)
3654
model = keras.Model(inputs, outputs)
@@ -42,6 +60,7 @@ def test_unbatched(self):
4260
image_size=(4, 4),
4361
scale=(1.0 / 255.0, 0.8 / 255.0, 1.2 / 255.0),
4462
offset=(0.2, -0.1, 0.25),
63+
_allow_python_workflow=self._allow_python_workflow,
4564
)
4665
inputs = np.ones((10, 10, 3)) * 128
4766
outputs = converter(inputs)
@@ -51,15 +70,22 @@ def test_unbatched(self):
5170
self.assertAllClose(outputs[:, :, 2], np.ones((4, 4)) * 0.852353)
5271

5372
def test_dtypes(self):
54-
converter = ImageConverter(image_size=(4, 4), scale=1.0 / 255.0)
73+
converter = ImageConverter(
74+
image_size=(4, 4),
75+
scale=1.0 / 255.0,
76+
_allow_python_workflow=self._allow_python_workflow,
77+
)
5578
int_image = ops.ones((10, 10, 3), dtype="uint8") * 255
5679
float_image = ops.ones((10, 10, 3), dtype="float64") * 255
5780
self.assertDTypeEqual(converter(int_image), "float32")
5881
self.assertDTypeEqual(converter(float_image), "float32")
5982
self.assertAllClose(converter(int_image), np.ones((4, 4, 3)))
6083
self.assertAllClose(converter(float_image), np.ones((4, 4, 3)))
6184
converter = ImageConverter(
62-
image_size=(4, 4), scale=1.0 / 255.0, dtype="bfloat16"
85+
image_size=(4, 4),
86+
scale=1.0 / 255.0,
87+
dtype="bfloat16",
88+
_allow_python_workflow=self._allow_python_workflow,
6389
)
6490
self.assertDTypeEqual(converter(int_image), "bfloat16")
6591
self.assertDTypeEqual(converter(float_image), "bfloat16")
@@ -77,6 +103,7 @@ def test_resize_batch(self, crop_to_aspect_ratio, pad_to_aspect_ratio):
77103
offset=(0.2, -0.1, 0.25),
78104
crop_to_aspect_ratio=crop_to_aspect_ratio,
79105
pad_to_aspect_ratio=pad_to_aspect_ratio,
106+
_allow_python_workflow=self._allow_python_workflow,
80107
)
81108
inputs = np.ones((2, 10, 10, 3)) * 128
82109
outputs = converter(inputs)
@@ -92,6 +119,7 @@ def test_pad_and_crop_to_aspect_ratio(self):
92119
scale=1 / 255.0,
93120
crop_to_aspect_ratio=True,
94121
pad_to_aspect_ratio=True,
122+
_allow_python_workflow=self._allow_python_workflow,
95123
)
96124

97125
def test_config(self):
@@ -101,6 +129,7 @@ def test_config(self):
101129
offset=(0.2, -0.1, 0.25),
102130
crop_to_aspect_ratio=False,
103131
interpolation="nearest",
132+
_allow_python_workflow=self._allow_python_workflow,
104133
)
105134
clone = ImageConverter.from_config(converter.get_config())
106135
test_batch = np.random.rand(4, 10, 20, 3) * 255
@@ -134,6 +163,7 @@ def test_save_to_preset(self):
134163
converter = ImageConverter.from_preset(
135164
"resnet_50_imagenet",
136165
interpolation="nearest",
166+
_allow_python_workflow=self._allow_python_workflow,
137167
)
138168
converter.save_to_preset(save_dir)
139169
# Save a tiny backbone so the preset is valid.
@@ -156,3 +186,31 @@ def test_save_to_preset(self):
156186
restored = ImageConverter.from_preset(save_dir)
157187
test_image = np.random.rand(100, 100, 3) * 255
158188
self.assertAllClose(restored(test_image), converter(test_image))
189+
190+
def test_inhomogeneous_batch(self):
191+
if not self._allow_python_workflow:
192+
self.skipTest("TF workflow does not support inhomogeneous batches.")
193+
converter = ImageConverter(
194+
image_size=(4, 4),
195+
scale=1 / 255.0,
196+
_allow_python_workflow=self._allow_python_workflow,
197+
)
198+
inputs = [
199+
np.ones((10, 10, 3)) * 255.0,
200+
np.ones((20, 20, 3)) * 128.0,
201+
]
202+
outputs = converter(inputs)
203+
self.assertIsInstance(outputs, list)
204+
self.assertEqual(len(outputs), 2)
205+
self.assertEqual(ops.shape(outputs[0]), (4, 4, 3))
206+
self.assertEqual(ops.shape(outputs[1]), (4, 4, 3))
207+
self.assertAllClose(outputs[0], ops.ones((4, 4, 3)))
208+
self.assertAllClose(outputs[1], ops.ones((4, 4, 3)) * (128.0 / 255.0))
209+
210+
211+
class ImageConverterTFTest(ImageConverterTest):
212+
"""Set `_allow_python_workflow=False` to test TF execution."""
213+
214+
def setUp(self):
215+
super().setUp()
216+
self._allow_python_workflow = False

0 commit comments

Comments
 (0)