chugr 0.2.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chugr/__init__.py +46 -0
- chugr/app/test.py +105 -0
- chugr/common/__init__.py +9 -0
- chugr/common/cli.py +234 -0
- chugr/common/collate.py +104 -0
- chugr/common/config.py +335 -0
- chugr/common/random.py +76 -0
- chugr/common/serialize.py +150 -0
- chugr/common/task_config.py +24 -0
- chugr/common/types.py +122 -0
- chugr/common/urls.py +88 -0
- chugr/doc/__init__.py +17 -0
- chugr/doc/constants.py +15 -0
- chugr/doc/doc_processor.py +248 -0
- chugr/doc/doc_read_processor.py +249 -0
- chugr/doc/doc_vqa_processor.py +113 -0
- chugr/doc/synth_text.py +212 -0
- chugr/hfds/__init__.py +2 -0
- chugr/hfds/collate.py +83 -0
- chugr/hfds/loader.py +385 -0
- chugr/hfds/wrappers.py +108 -0
- chugr/image/__init__.py +4 -0
- chugr/image/build_transforms_doc.py +427 -0
- chugr/image/build_transforms_image.py +203 -0
- chugr/image/transforms_alb.py +112 -0
- chugr/image/transforms_factory.py +58 -0
- chugr/image/transforms_torch.py +137 -0
- chugr/loader.py +249 -0
- chugr/naflex.py +357 -0
- chugr/task_pipeline/__init__.py +7 -0
- chugr/task_pipeline/pipeline_doc_read.py +66 -0
- chugr/task_pipeline/pipeline_doc_vqa.py +124 -0
- chugr/task_pipeline/pipeline_factory.py +32 -0
- chugr/task_pipeline/pipeline_gtparse.py +93 -0
- chugr/task_pipeline/pipeline_image_text.py +90 -0
- chugr/task_pipeline/pipeline_manual.py +27 -0
- chugr/text/__init__.py +1 -0
- chugr/text/tokenization.py +222 -0
- chugr/version.py +1 -0
- chugr/wds/__init__.py +7 -0
- chugr/wds/dataset_info.py +65 -0
- chugr/wds/decode.py +319 -0
- chugr/wds/filters.py +142 -0
- chugr/wds/helpers.py +68 -0
- chugr/wds/loader.py +187 -0
- chugr/wds/pipeline.py +182 -0
- chugr/wds/shardlists.py +134 -0
- chugr/wds/tariterators.py +70 -0
- chugr-0.2.0.dev0.dist-info/METADATA +436 -0
- chugr-0.2.0.dev0.dist-info/RECORD +53 -0
- chugr-0.2.0.dev0.dist-info/WHEEL +4 -0
- chugr-0.2.0.dev0.dist-info/entry_points.txt +4 -0
- chugr-0.2.0.dev0.dist-info/licenses/LICENSE +201 -0
chugr/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from .common import (
|
|
2
|
+
ImageInputCfg,
|
|
3
|
+
ImageAugCfg,
|
|
4
|
+
LoaderBundle,
|
|
5
|
+
ImageFeatureInfo,
|
|
6
|
+
FeatureInfo,
|
|
7
|
+
ShardSpec,
|
|
8
|
+
SourceSpec,
|
|
9
|
+
DataArg,
|
|
10
|
+
DataCfg,
|
|
11
|
+
DistributedCfg,
|
|
12
|
+
)
|
|
13
|
+
from .hfds import create_loader_hf
|
|
14
|
+
from .image import (
|
|
15
|
+
build_image_transforms,
|
|
16
|
+
build_transforms_image_basic,
|
|
17
|
+
build_transforms_image_timm,
|
|
18
|
+
build_transforms_doc_basic,
|
|
19
|
+
build_transforms_doc_better,
|
|
20
|
+
build_transforms_doc_nougat,
|
|
21
|
+
create_image_preprocessor,
|
|
22
|
+
)
|
|
23
|
+
from .loader import create_loader, create_loader_from_config_hf, create_loader_from_config_wds
|
|
24
|
+
from .task_pipeline import (
|
|
25
|
+
create_task_pipeline,
|
|
26
|
+
build_task_pipeline_doc_read,
|
|
27
|
+
build_task_pipeline_doc_vqa,
|
|
28
|
+
build_task_pipeline_gtparse,
|
|
29
|
+
build_task_pipeline_image_text,
|
|
30
|
+
build_task_pipeline_manual,
|
|
31
|
+
DataTaskDocReadCfg,
|
|
32
|
+
DataTaskDocVqaCfg,
|
|
33
|
+
DataTaskImageTextCfg,
|
|
34
|
+
DataTaskManualCfg,
|
|
35
|
+
)
|
|
36
|
+
from .text import tokenize, text_input_to_target, prepare_text_input, create_text_preprocessor
|
|
37
|
+
from .version import __version__
|
|
38
|
+
from .wds import (
|
|
39
|
+
create_loader_wds,
|
|
40
|
+
build_data_pipeline,
|
|
41
|
+
decode_image_pages,
|
|
42
|
+
decode_pdf_pages,
|
|
43
|
+
create_image_decoder,
|
|
44
|
+
DecodeDoc,
|
|
45
|
+
)
|
|
46
|
+
|
chugr/app/test.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
""" Iterate a document loader for throughput benchmarking & sample inspection.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
python -m chugr.app.test --data.source "idl-train-0000{0..3}.tar" --data.batch-size 8 --data.num-samples 1000 \\
|
|
5
|
+
--input.transform-type doc_naflex --input.patch-size 16 --input.max-seq-len 4096 --task doc_read
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
import chugr
|
|
17
|
+
from chugr.common import DataArg, DataCfg, ImageInputCfg, Serializable, parse_args
|
|
18
|
+
|
|
19
|
+
_logger = logging.getLogger('chugr.test')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TestArgs(Serializable):
|
|
24
|
+
data: DataArg = field(default_factory=lambda: DataArg(source=''))
|
|
25
|
+
input: ImageInputCfg = field(default_factory=lambda: ImageInputCfg(transform_type='doc_better', size=(1024, 768)))
|
|
26
|
+
task: str = 'doc_read' # one of 'doc_read', 'doc_vqa'
|
|
27
|
+
tokenizer: Optional[str] = None # HF tokenizer name, text left as strings if not set
|
|
28
|
+
text_max_length: int = 1024
|
|
29
|
+
is_training: bool = True
|
|
30
|
+
num_batches: int = 50
|
|
31
|
+
dump_dir: Optional[str] = None # write first batch images (un-normalized) here
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _dump_image(x, input_cfg: ImageInputCfg, path: str):
|
|
35
|
+
from torchvision.utils import save_image
|
|
36
|
+
if isinstance(x, dict):
|
|
37
|
+
# reassemble naflex patches into an image
|
|
38
|
+
valid = x['patch_valid']
|
|
39
|
+
patches = x['patches'][valid]
|
|
40
|
+
coords = x['patch_coord'][valid]
|
|
41
|
+
p = input_cfg.patch_size
|
|
42
|
+
c = patches.shape[-1] // (p * p)
|
|
43
|
+
h, w = (coords.max(0).values + 1).tolist()
|
|
44
|
+
img = torch.zeros(c, h * p, w * p)
|
|
45
|
+
for patch, (py, px) in zip(patches, coords.tolist()):
|
|
46
|
+
img[:, py * p:(py + 1) * p, px * p:(px + 1) * p] = patch.reshape(p, p, c).permute(2, 0, 1)
|
|
47
|
+
x = img
|
|
48
|
+
mean = torch.tensor(input_cfg.mean).view(-1, 1, 1)
|
|
49
|
+
std = torch.tensor(input_cfg.std).view(-1, 1, 1)
|
|
50
|
+
save_image((x * std + mean).clamp(0, 1), path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv=None):
|
|
54
|
+
logging.basicConfig(level=logging.INFO)
|
|
55
|
+
args: TestArgs = parse_args(TestArgs, argv=argv, description='chugr loader test')
|
|
56
|
+
print(args.dumps_yaml())
|
|
57
|
+
|
|
58
|
+
img_fn = chugr.create_image_preprocessor(args.input, is_training=args.is_training)
|
|
59
|
+
txt_fn = None
|
|
60
|
+
if args.tokenizer:
|
|
61
|
+
txt_fn = chugr.create_text_preprocessor(args.tokenizer, max_length=args.text_max_length)
|
|
62
|
+
if args.task == 'doc_read':
|
|
63
|
+
task_cfg = chugr.DataTaskDocReadCfg(
|
|
64
|
+
image_process_fn=img_fn,
|
|
65
|
+
text_process_fn=txt_fn,
|
|
66
|
+
text_target_feat=chugr.FeatureInfo('text_target', input_key=None) if txt_fn else None,
|
|
67
|
+
error_handler='dump_and_reraise',
|
|
68
|
+
)
|
|
69
|
+
elif args.task == 'doc_vqa':
|
|
70
|
+
task_cfg = chugr.DataTaskDocVqaCfg(
|
|
71
|
+
image_process_fn=img_fn,
|
|
72
|
+
text_process_fn=txt_fn,
|
|
73
|
+
text_target_feat=chugr.FeatureInfo('text_target', input_key=None) if txt_fn else None,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
assert False, f'Unknown task {args.task}'
|
|
77
|
+
|
|
78
|
+
loader = chugr.create_loader(DataCfg.from_arg(args.data), task_cfg, is_training=args.is_training)
|
|
79
|
+
start = time.time()
|
|
80
|
+
num_samples = 0
|
|
81
|
+
for i, batch in enumerate(loader):
|
|
82
|
+
image = batch['image_input']
|
|
83
|
+
bs = (image['patches'] if isinstance(image, dict) else image).shape[0]
|
|
84
|
+
num_samples += bs
|
|
85
|
+
if i == 0:
|
|
86
|
+
if isinstance(image, dict):
|
|
87
|
+
_logger.info(f"patches {tuple(image['patches'].shape)}, valid {image['patch_valid'].sum(1).tolist()}")
|
|
88
|
+
else:
|
|
89
|
+
_logger.info(f'image {tuple(image.shape)}')
|
|
90
|
+
if args.dump_dir:
|
|
91
|
+
os.makedirs(args.dump_dir, exist_ok=True)
|
|
92
|
+
for j in range(bs):
|
|
93
|
+
x = {k: v[j] for k, v in image.items()} if isinstance(image, dict) else image[j]
|
|
94
|
+
_dump_image(x, args.input, os.path.join(args.dump_dir, f'sample_{j}.png'))
|
|
95
|
+
text = batch['text_input'][j]
|
|
96
|
+
with open(os.path.join(args.dump_dir, f'sample_{j}.txt'), 'w') as f:
|
|
97
|
+
f.write(text if isinstance(text, str) else str(text))
|
|
98
|
+
if i + 1 >= args.num_batches:
|
|
99
|
+
break
|
|
100
|
+
elapsed = time.time() - start
|
|
101
|
+
_logger.info(f'{num_samples} samples in {elapsed:.1f}s, {num_samples / elapsed:.1f} samples/s')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == '__main__':
|
|
105
|
+
sys.exit(main())
|
chugr/common/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .cli import parse_args, str2bool
|
|
2
|
+
from .collate import collate, collate_naflex, is_naflex_sample
|
|
3
|
+
from .config import ImageInputCfg, ImageAugCfg, PreprocessCfg, image_mode_to_chs
|
|
4
|
+
from .config import DataArg, DataCfg, DistributedCfg, source_to_shard_spec
|
|
5
|
+
from .serialize import Serializable, to_dict, from_dict
|
|
6
|
+
from .random import get_pytorch_worker_seed, rank_seed, seed_worker, shuffled_interval_indices
|
|
7
|
+
from .task_config import DataTaskCfg
|
|
8
|
+
from .types import SourceSpec, SplitInfo, ShardSpec, SharedCount, LoaderBundle, FeatureInfo, ImageFeatureInfo
|
|
9
|
+
from .urls import expand_urls
|
chugr/common/cli.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
""" Nested dataclass <-> command line arguments
|
|
2
|
+
|
|
3
|
+
A small replacement for the subset of `simple_parsing` used by chugr & pixparse apps.
|
|
4
|
+
|
|
5
|
+
* nested dataclass fields map to dotted args, e.g. `--task.opt.learning-rate 1e-4`
|
|
6
|
+
(dashes and underscores are both accepted)
|
|
7
|
+
* `subgroups` select the dataclass type of a field by name, e.g. `--task cruller_pretrain`
|
|
8
|
+
* a `--config` yaml/json file provides defaults that command line args override
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import dataclasses
|
|
12
|
+
import sys
|
|
13
|
+
import typing
|
|
14
|
+
from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar
|
|
15
|
+
|
|
16
|
+
from .serialize import _is_union, _unwrap_optional, from_dict, resolve_field_types, _load_file
|
|
17
|
+
|
|
18
|
+
T = TypeVar('T')
|
|
19
|
+
|
|
20
|
+
_NONE_STRINGS = ('none', 'null')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def str2bool(v: str) -> bool:
|
|
24
|
+
if isinstance(v, bool):
|
|
25
|
+
return v
|
|
26
|
+
lv = v.lower()
|
|
27
|
+
if lv in ('true', 't', 'yes', 'y', '1'):
|
|
28
|
+
return True
|
|
29
|
+
if lv in ('false', 'f', 'no', 'n', '0'):
|
|
30
|
+
return False
|
|
31
|
+
raise argparse.ArgumentTypeError(f'Boolean value expected, got {v}.')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _scalar_parser(tp):
|
|
35
|
+
def _parse(v: str):
|
|
36
|
+
if v.lower() in _NONE_STRINGS:
|
|
37
|
+
return None
|
|
38
|
+
if tp is bool:
|
|
39
|
+
return str2bool(v)
|
|
40
|
+
if tp is int:
|
|
41
|
+
return int(v)
|
|
42
|
+
if tp is float:
|
|
43
|
+
return float(v)
|
|
44
|
+
if _is_union(tp):
|
|
45
|
+
# e.g. Union[int, str] -> try members in order
|
|
46
|
+
for a in typing.get_args(tp):
|
|
47
|
+
if a is type(None):
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
return _scalar_parser(a)(v)
|
|
51
|
+
except (ValueError, argparse.ArgumentTypeError):
|
|
52
|
+
continue
|
|
53
|
+
return v
|
|
54
|
+
return v
|
|
55
|
+
return _parse
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _arg_spec(tp):
|
|
59
|
+
""" Return (nargs, item parser) for a field type. """
|
|
60
|
+
tp = _unwrap_optional(tp)
|
|
61
|
+
origin = typing.get_origin(tp)
|
|
62
|
+
args = typing.get_args(tp)
|
|
63
|
+
if origin is tuple:
|
|
64
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
65
|
+
return '+', _scalar_parser(args[0])
|
|
66
|
+
item_tp = args[0] if args else str
|
|
67
|
+
return '+', _scalar_parser(item_tp) # length validated when building the dataclass
|
|
68
|
+
if origin is list:
|
|
69
|
+
return '+', _scalar_parser(args[0] if args else str)
|
|
70
|
+
if _is_union(tp):
|
|
71
|
+
# union containing a sequence type, accept one or more values
|
|
72
|
+
for a in args:
|
|
73
|
+
if typing.get_origin(a) in (list, tuple):
|
|
74
|
+
return '+', _scalar_parser(typing.get_args(a)[0] if typing.get_args(a) else str)
|
|
75
|
+
return None, _scalar_parser(tp)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _flatten_fields(cls, prefix: str = '') -> Dict[str, Any]:
|
|
79
|
+
""" dotted name -> field type for all leaf fields of a (nested) dataclass type """
|
|
80
|
+
out = {}
|
|
81
|
+
field_types = resolve_field_types(cls)
|
|
82
|
+
for f in dataclasses.fields(cls):
|
|
83
|
+
if not f.init:
|
|
84
|
+
continue
|
|
85
|
+
tp = field_types[f.name]
|
|
86
|
+
name = f'{prefix}{f.name}'
|
|
87
|
+
inner = _unwrap_optional(tp)
|
|
88
|
+
if dataclasses.is_dataclass(inner):
|
|
89
|
+
out.update(_flatten_fields(inner, prefix=name + '.'))
|
|
90
|
+
else:
|
|
91
|
+
out[name] = tp
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _set_nested(d: Dict[str, Any], dotted: str, value):
|
|
96
|
+
keys = dotted.split('.')
|
|
97
|
+
for k in keys[:-1]:
|
|
98
|
+
if d.get(k) is None:
|
|
99
|
+
d[k] = {}
|
|
100
|
+
d = d[k]
|
|
101
|
+
d[keys[-1]] = value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _norm(name: str) -> str:
|
|
105
|
+
return name.replace('-', '_')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_args(
|
|
109
|
+
cls: Type[T],
|
|
110
|
+
argv: Optional[Sequence[str]] = None,
|
|
111
|
+
subgroups: Optional[Dict[str, Dict[str, type]]] = None,
|
|
112
|
+
subgroup_defaults: Optional[Dict[str, str]] = None,
|
|
113
|
+
description: Optional[str] = None,
|
|
114
|
+
) -> T:
|
|
115
|
+
""" Parse command line args into dataclass `cls`.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
cls: The top level config dataclass.
|
|
119
|
+
argv: Arguments, defaults to sys.argv[1:].
|
|
120
|
+
subgroups: Mapping of dotted field name -> {choice name: dataclass type}. The choice is selected
|
|
121
|
+
with `--<field> <name>` and the selected type's fields become `--<field>.<subfield>` args.
|
|
122
|
+
subgroup_defaults: Default choice name per subgroup field.
|
|
123
|
+
description: Parser description.
|
|
124
|
+
"""
|
|
125
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
126
|
+
subgroups = subgroups or {}
|
|
127
|
+
subgroup_defaults = subgroup_defaults or {}
|
|
128
|
+
|
|
129
|
+
# stage 1: config file + subgroup selection
|
|
130
|
+
pre = argparse.ArgumentParser(add_help=False)
|
|
131
|
+
pre.add_argument('--config', type=str, default=None)
|
|
132
|
+
for sg_name, choices in subgroups.items():
|
|
133
|
+
pre.add_argument(
|
|
134
|
+
f'--{sg_name.replace("_", "-")}', f'--{sg_name}', dest=sg_name, type=str,
|
|
135
|
+
default=None, choices=list(choices.keys()))
|
|
136
|
+
pre_args, _ = pre.parse_known_args(argv)
|
|
137
|
+
|
|
138
|
+
file_cfg = _load_file(pre_args.config) if pre_args.config else {}
|
|
139
|
+
|
|
140
|
+
selected = {}
|
|
141
|
+
for sg_name, choices in subgroups.items():
|
|
142
|
+
choice = getattr(pre_args, sg_name)
|
|
143
|
+
if choice is None:
|
|
144
|
+
# allow config file to select via a `_name` key in the subgroup dict
|
|
145
|
+
sg_file = file_cfg.get(sg_name)
|
|
146
|
+
if isinstance(sg_file, dict) and '_name' in sg_file:
|
|
147
|
+
choice = sg_file.pop('_name')
|
|
148
|
+
else:
|
|
149
|
+
choice = subgroup_defaults.get(sg_name)
|
|
150
|
+
assert choice is not None, f'A value for --{sg_name} is required (one of {list(choices.keys())}).'
|
|
151
|
+
selected[sg_name] = choice
|
|
152
|
+
|
|
153
|
+
# stage 2: build full parser over all leaf fields
|
|
154
|
+
leaf_types = {}
|
|
155
|
+
field_types = resolve_field_types(cls)
|
|
156
|
+
for f in dataclasses.fields(cls):
|
|
157
|
+
if not f.init:
|
|
158
|
+
continue
|
|
159
|
+
if f.name in subgroups:
|
|
160
|
+
sub_cls = subgroups[f.name][selected[f.name]]
|
|
161
|
+
leaf_types.update(_flatten_fields(sub_cls, prefix=f.name + '.'))
|
|
162
|
+
continue
|
|
163
|
+
tp = field_types[f.name]
|
|
164
|
+
inner = _unwrap_optional(tp)
|
|
165
|
+
if dataclasses.is_dataclass(inner):
|
|
166
|
+
leaf_types.update(_flatten_fields(inner, prefix=f.name + '.'))
|
|
167
|
+
else:
|
|
168
|
+
leaf_types[f.name] = tp
|
|
169
|
+
|
|
170
|
+
parser = argparse.ArgumentParser(description=description)
|
|
171
|
+
parser.add_argument('--config', type=str, default=None, help='yaml or json config file')
|
|
172
|
+
for sg_name, choices in subgroups.items():
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
f'--{sg_name.replace("_", "-")}', f'--{sg_name}', dest=f'__sg_{sg_name}', type=str,
|
|
175
|
+
default=None, choices=list(choices.keys()))
|
|
176
|
+
|
|
177
|
+
for dotted, tp in leaf_types.items():
|
|
178
|
+
nargs, item_parser = _arg_spec(tp)
|
|
179
|
+
flags = {f'--{dotted}', f'--{dotted.replace("_", "-")}'}
|
|
180
|
+
kwargs = dict(dest=dotted, type=item_parser, default=argparse.SUPPRESS)
|
|
181
|
+
if nargs is not None:
|
|
182
|
+
kwargs['nargs'] = nargs
|
|
183
|
+
parser.add_argument(*sorted(flags), **kwargs)
|
|
184
|
+
|
|
185
|
+
args = parser.parse_args(argv)
|
|
186
|
+
|
|
187
|
+
# merge: file config < command line
|
|
188
|
+
merged: Dict[str, Any] = dict(file_cfg)
|
|
189
|
+
for dotted, value in vars(args).items():
|
|
190
|
+
if dotted == 'config' or dotted.startswith('__sg_'):
|
|
191
|
+
continue
|
|
192
|
+
if isinstance(value, list) and len(value) == 1 and value[0] is None:
|
|
193
|
+
value = None # e.g. `--input.size none` for an Optional[Tuple]
|
|
194
|
+
_set_nested(merged, dotted, value)
|
|
195
|
+
|
|
196
|
+
# build nested dataclasses
|
|
197
|
+
kwargs = {}
|
|
198
|
+
for f in dataclasses.fields(cls):
|
|
199
|
+
if not f.init or f.name not in merged:
|
|
200
|
+
if f.name in subgroups:
|
|
201
|
+
kwargs[f.name] = subgroups[f.name][selected[f.name]]()
|
|
202
|
+
continue
|
|
203
|
+
value = merged[f.name]
|
|
204
|
+
if f.name in subgroups:
|
|
205
|
+
sub_cls = subgroups[f.name][selected[f.name]]
|
|
206
|
+
kwargs[f.name] = _build_with_defaults(sub_cls, value or {})
|
|
207
|
+
continue
|
|
208
|
+
tp = _unwrap_optional(field_types[f.name])
|
|
209
|
+
if dataclasses.is_dataclass(tp) and isinstance(value, dict):
|
|
210
|
+
kwargs[f.name] = _build_with_defaults(tp, value)
|
|
211
|
+
else:
|
|
212
|
+
from .serialize import from_value
|
|
213
|
+
kwargs[f.name] = from_value(field_types[f.name], value)
|
|
214
|
+
return cls(**kwargs)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_with_defaults(cls, overrides: Dict[str, Any]):
|
|
218
|
+
""" Build dataclass `cls` from partial (nested) overrides, keeping field defaults for the rest. """
|
|
219
|
+
from .serialize import from_value
|
|
220
|
+
field_types = resolve_field_types(cls)
|
|
221
|
+
kwargs = {}
|
|
222
|
+
for f in dataclasses.fields(cls):
|
|
223
|
+
if not f.init or f.name not in overrides:
|
|
224
|
+
continue
|
|
225
|
+
value = overrides[f.name]
|
|
226
|
+
tp = _unwrap_optional(field_types[f.name])
|
|
227
|
+
if dataclasses.is_dataclass(tp) and isinstance(value, dict):
|
|
228
|
+
kwargs[f.name] = _build_with_defaults(tp, value)
|
|
229
|
+
else:
|
|
230
|
+
kwargs[f.name] = from_value(field_types[f.name], value)
|
|
231
|
+
unknown = set(overrides.keys()) - {f.name for f in dataclasses.fields(cls)}
|
|
232
|
+
if unknown:
|
|
233
|
+
raise TypeError(f'Unknown fields {sorted(unknown)} for {cls.__name__}.')
|
|
234
|
+
return cls(**kwargs)
|
chugr/common/collate.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
from typing import Callable, Dict, List, Optional, Tuple, Type, Union
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch.utils.data._utils.collate import default_collate_fn_map, default_collate_err_msg_format
|
|
6
|
+
|
|
7
|
+
NAFLEX_KEYS = ('patches', 'patch_coord', 'patch_valid')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_naflex_sample(elem) -> bool:
|
|
11
|
+
return isinstance(elem, collections.abc.Mapping) and all(k in elem for k in NAFLEX_KEYS)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collate_naflex(
|
|
15
|
+
batch: List[Dict[str, torch.Tensor]],
|
|
16
|
+
pad_multiple: int = 8,
|
|
17
|
+
pad_to: Optional[int] = None,
|
|
18
|
+
) -> Dict[str, torch.Tensor]:
|
|
19
|
+
""" Pad variable length patch sequences (NaFlex) to the longest in the batch.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
batch: list of dicts w/ 'patches' [N, D] (or [N, Ph, Pw, C]), 'patch_coord' [N, 2], 'patch_valid' [N]
|
|
23
|
+
pad_multiple: round padded length up to a multiple of this (kernel friendly shapes)
|
|
24
|
+
pad_to: pad to this fixed length (e.g. the NaFlex seq_len bucket) instead of the batch max
|
|
25
|
+
"""
|
|
26
|
+
max_len = max(b['patches'].shape[0] for b in batch)
|
|
27
|
+
if pad_to is not None:
|
|
28
|
+
assert max_len <= pad_to, f'NaFlex sample length {max_len} exceeds bucket seq_len {pad_to}'
|
|
29
|
+
max_len = pad_to
|
|
30
|
+
elif pad_multiple > 1:
|
|
31
|
+
max_len = ((max_len + pad_multiple - 1) // pad_multiple) * pad_multiple
|
|
32
|
+
batch_size = len(batch)
|
|
33
|
+
patch_shape = batch[0]['patches'].shape[1:]
|
|
34
|
+
patches = torch.zeros((batch_size, max_len, *patch_shape), dtype=batch[0]['patches'].dtype)
|
|
35
|
+
patch_coord = torch.zeros((batch_size, max_len, 2), dtype=torch.int64)
|
|
36
|
+
patch_valid = torch.zeros((batch_size, max_len), dtype=torch.bool)
|
|
37
|
+
for i, b in enumerate(batch):
|
|
38
|
+
n = b['patches'].shape[0]
|
|
39
|
+
patches[i, :n] = b['patches']
|
|
40
|
+
patch_coord[i, :n] = b['patch_coord']
|
|
41
|
+
patch_valid[i, :n] = b['patch_valid']
|
|
42
|
+
return {'patches': patches, 'patch_coord': patch_coord, 'patch_valid': patch_valid}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def collate(batch):
|
|
46
|
+
r"""
|
|
47
|
+
A customized collate function that handles collection type of element within each batch.
|
|
48
|
+
|
|
49
|
+
This collate function has been tweaked to provide different functionality when handling
|
|
50
|
+
dictionary samples. Certain keys are excluded or not tensorized.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
batch: a single batch to be collated
|
|
54
|
+
"""
|
|
55
|
+
elem = batch[0]
|
|
56
|
+
elem_type = type(elem)
|
|
57
|
+
|
|
58
|
+
if elem_type in default_collate_fn_map:
|
|
59
|
+
return default_collate_fn_map[elem_type](batch)
|
|
60
|
+
|
|
61
|
+
for collate_type in default_collate_fn_map:
|
|
62
|
+
if isinstance(elem, collate_type):
|
|
63
|
+
return default_collate_fn_map[collate_type](batch)
|
|
64
|
+
|
|
65
|
+
if is_naflex_sample(elem):
|
|
66
|
+
return collate_naflex(batch)
|
|
67
|
+
|
|
68
|
+
if isinstance(elem, collections.abc.Mapping):
|
|
69
|
+
try:
|
|
70
|
+
out = {}
|
|
71
|
+
for key in elem:
|
|
72
|
+
if key.startswith('__'):
|
|
73
|
+
# skip keys starting with '__', e.g. '__key__',
|
|
74
|
+
continue
|
|
75
|
+
elif key.startswith('_'):
|
|
76
|
+
# do not recurse or tensorize values for keys starting with '_', e.g. '_parse'
|
|
77
|
+
out[key] = [d[key] for d in batch]
|
|
78
|
+
else:
|
|
79
|
+
out[key] = collate([d[key] for d in batch])
|
|
80
|
+
out = elem_type(out)
|
|
81
|
+
return out
|
|
82
|
+
except TypeError:
|
|
83
|
+
# The mapping type may not support `__init__(iterable)`.
|
|
84
|
+
return {key: collate([d[key] for d in batch]) for key in elem}
|
|
85
|
+
elif isinstance(elem, tuple) and hasattr(elem, '_fields'): # namedtuple
|
|
86
|
+
return elem_type(*(collate(samples) for samples in zip(*batch)))
|
|
87
|
+
elif isinstance(elem, collections.abc.Sequence):
|
|
88
|
+
# check to make sure that the elements in batch have consistent size
|
|
89
|
+
it = iter(batch)
|
|
90
|
+
elem_size = len(next(it))
|
|
91
|
+
if not all(len(elem) == elem_size for elem in it):
|
|
92
|
+
raise RuntimeError('each element in list of batch should be of equal size')
|
|
93
|
+
transposed = list(zip(*batch)) # It may be accessed twice, so we use a list.
|
|
94
|
+
|
|
95
|
+
if isinstance(elem, tuple):
|
|
96
|
+
return [collate(samples) for samples in transposed] # Backwards compatibility.
|
|
97
|
+
else:
|
|
98
|
+
try:
|
|
99
|
+
return elem_type([collate(samples) for samples in transposed])
|
|
100
|
+
except TypeError:
|
|
101
|
+
# The sequence type may not support `__init__(iterable)` (e.g., `range`).
|
|
102
|
+
return [collate(samples) for samples in transposed]
|
|
103
|
+
|
|
104
|
+
raise TypeError(default_collate_err_msg_format.format(elem_type))
|