Skip to content

Commit 6b91e5c

Browse files
authored
[Yaml] - fix ml embeddings issue round2 (#35756)
* initial code * rest of code with some improvements * address comments and update design * change tensorflow * fix lint * revert tensorflow version change
1 parent 804dd35 commit 6b91e5c

3 files changed

Lines changed: 115 additions & 9 deletions

File tree

sdks/python/apache_beam/ml/transforms/base.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,20 +183,23 @@ def append_transform(self, transform: BaseOperation):
183183
"""
184184

185185

186-
def _dict_input_fn(columns: Sequence[str],
187-
batch: Sequence[Dict[str, Any]]) -> List[str]:
186+
def _dict_input_fn(
187+
columns: Sequence[str], batch: Sequence[Union[Dict[str, Any],
188+
beam.Row]]) -> List[str]:
188189
"""Extract text from specified columns in batch."""
190+
if batch and hasattr(batch[0], '_asdict'):
191+
batch = [row._asdict() if hasattr(row, '_asdict') else row for row in batch]
192+
189193
if not batch or not isinstance(batch[0], dict):
190194
raise TypeError(
191195
'Expected data to be dicts, got '
192196
f'{type(batch[0])} instead.')
193-
194197
result = []
195198
expected_keys = set(batch[0].keys())
196199
expected_columns = set(columns)
197200
# Process one batch item at a time
198201
for item in batch:
199-
item_keys = item.keys()
202+
item_keys = item.keys() if isinstance(item, dict) else set()
200203
if set(item_keys) != expected_keys:
201204
extra_keys = item_keys - expected_keys
202205
missing_keys = expected_keys - item_keys
@@ -212,21 +215,31 @@ def _dict_input_fn(columns: Sequence[str],
212215

213216
# Get all columns for this item
214217
for col in columns:
215-
result.append(item[col])
218+
if isinstance(item, dict):
219+
result.append(item[col])
216220
return result
217221

218222

219223
def _dict_output_fn(
220224
columns: Sequence[str],
221-
batch: Sequence[Dict[str, Any]],
222-
embeddings: Sequence[Any]) -> List[Dict[str, Any]]:
225+
batch: Sequence[Union[Dict[str, Any], beam.Row]],
226+
embeddings: Sequence[Any]) -> list[Union[dict[str, Any], beam.Row]]:
223227
"""Map embeddings back to columns in batch."""
228+
is_beam_row = False
229+
if batch and hasattr(batch[0], '_asdict'):
230+
is_beam_row = True
231+
batch = [row._asdict() if hasattr(row, '_asdict') else row for row in batch]
232+
224233
result = []
225234
for batch_idx, item in enumerate(batch):
226235
for col_idx, col in enumerate(columns):
227236
embedding_idx = batch_idx * len(columns) + col_idx
228-
item[col] = embeddings[embedding_idx]
237+
if isinstance(item, dict):
238+
item[col] = embeddings[embedding_idx]
229239
result.append(item)
240+
241+
if is_beam_row:
242+
result = [beam.Row(**item) for item in result if isinstance(item, dict)]
230243
return result
231244

232245

sdks/python/apache_beam/yaml/yaml_ml.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
#
1717

1818
"""This module defines yaml wrappings for some ML transforms."""
19+
import logging
20+
import pkgutil
1921
from collections.abc import Callable
22+
from importlib import import_module
2023
from typing import Any
2124
from typing import Optional
2225

@@ -29,14 +32,42 @@
2932
from apache_beam.yaml import options
3033
from apache_beam.yaml.yaml_utils import SafeLineLoader
3134

35+
36+
def _list_submodules(package):
37+
"""
38+
Lists all submodules within a given package.
39+
"""
40+
submodules = []
41+
skip_modules = ['base', 'handlers', 'test', 'tft', 'utils']
42+
for _, module_name, _ in pkgutil.walk_packages(
43+
package.__path__, package.__name__ + '.'):
44+
if any(skip_name in module_name for skip_name in skip_modules):
45+
continue
46+
submodules.append(module_name)
47+
return submodules
48+
49+
50+
_transform_constructors = {}
3251
try:
3352
from apache_beam.ml.transforms import tft
3453
from apache_beam.ml.transforms.base import MLTransform
35-
# TODO(robertwb): Is this all of them?
3654
_transform_constructors = tft.__dict__
3755
except ImportError:
3856
tft = None # type: ignore
3957

58+
if tft:
59+
# Load all available ML Transform modules
60+
for module_name in _list_submodules(beam.ml.transforms):
61+
try:
62+
module = import_module(module_name)
63+
_transform_constructors |= module.__dict__
64+
except ImportError as e:
65+
logging.warning(
66+
'Could not load ML transform module %s: %s. Please '
67+
'install the necessary module dependencies',
68+
module_name,
69+
e)
70+
4071

4172
class ModelHandlerProvider:
4273
handler_types: dict[str, Callable[..., "ModelHandlerProvider"]] = {}

sdks/python/apache_beam/yaml/yaml_ml_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,68 @@ def test_ml_transform(self):
8686
equal_to([5]),
8787
label='CheckVocab')
8888

89+
def test_sentence_transformer_embedding(self):
90+
SENTENCE_EMBEDDING_DIMENSION = 384
91+
DATA = [{
92+
'id': 1, 'log_message': "Error in module A"
93+
}, {
94+
'id': 2, 'log_message': "Warning in module B"
95+
}, {
96+
'id': 3, 'log_message': "Info in module C"
97+
}]
98+
ml_opts = beam.options.pipeline_options.PipelineOptions(
99+
pickle_library='cloudpickle', yaml_experimental_features=['ML'])
100+
with tempfile.TemporaryDirectory() as tempdir:
101+
with beam.Pipeline(options=ml_opts) as p:
102+
elements = p | beam.Create(DATA)
103+
result = elements | YamlTransform(
104+
f'''
105+
type: MLTransform
106+
config:
107+
write_artifact_location: {tempdir}
108+
transforms:
109+
- type: SentenceTransformerEmbeddings
110+
config:
111+
model_name: all-MiniLM-L6-v2
112+
columns: [log_message]
113+
''')
114+
115+
# Perform a basic check to ensure that embeddings are generated
116+
# and that the dimension of those embeddings is correct.
117+
actual_output = result | beam.Map(lambda x: len(x['log_message']))
118+
assert_that(
119+
actual_output, equal_to([SENTENCE_EMBEDDING_DIMENSION] * len(DATA)))
120+
121+
def test_sentence_transformer_embedding_with_beam_rows(self):
122+
SENTENCE_EMBEDDING_DIMENSION = 384
123+
DATA = [
124+
beam.Row(id=1, log_message="Error in module A"),
125+
beam.Row(id=2, log_message="Warning in module B"),
126+
beam.Row(id=3, log_message="Info in module C"),
127+
]
128+
ml_opts = beam.options.pipeline_options.PipelineOptions(
129+
pickle_library='cloudpickle', yaml_experimental_features=['ML'])
130+
with tempfile.TemporaryDirectory() as tempdir:
131+
with beam.Pipeline(options=ml_opts) as p:
132+
elements = p | beam.Create(DATA)
133+
result = elements | YamlTransform(
134+
f'''
135+
type: MLTransform
136+
config:
137+
write_artifact_location: {tempdir}
138+
transforms:
139+
- type: SentenceTransformerEmbeddings
140+
config:
141+
model_name: all-MiniLM-L6-v2
142+
columns: [log_message]
143+
''')
144+
145+
# Perform a basic check to ensure that embeddings are generated
146+
# and that the dimension of those embeddings is correct.
147+
actual_output = result | beam.Map(lambda x: len(x.log_message))
148+
assert_that(
149+
actual_output, equal_to([SENTENCE_EMBEDDING_DIMENSION] * len(DATA)))
150+
89151

90152
if __name__ == '__main__':
91153
logging.getLogger().setLevel(logging.INFO)

0 commit comments

Comments
 (0)