Skip to content
This repository was archived by the owner on Dec 16, 2022. It is now read-only.

Commit 9007258

Browse files
committed
Add HuggingfaceDatasetReader for using Huggingface datasets
Introduced new dependency - "datasets>=1.5.0,<1.6.0"" Added a new reader to allow for reading huggingface datasets as instance Mapped limited `datasets.features` to `allenlp.data.fields` Added Tests for the same Verified for selective dataset and/or dataset configurations Added `test-with-cov-html` to provide contributor friendly html coverage report Signed-off-by: Abhishek P (VMware) <pab@vmware.com>
1 parent 0c7d60b commit 9007258

6 files changed

Lines changed: 472 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ __pycache__
4545
.coverage
4646
.pytest_cache/
4747
.benchmarks
48+
htmlcov/
4849

4950
# documentation build artifacts
5051

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## Unreleased
99

1010
### Added
11-
11+
- Add `HuggingfaceDatasetReader` for using huggingface datasets in AllenNLP with known support for limited datasets
1212
- The test for distributed metrics now takes a parameter specifying how often you want to run it.
1313

14-
1514
## [v2.3.0](https://github.com/allenai/allennlp/releases/tag/v2.3.0) - 2021-04-14
1615

1716
### Added
1817

18+
=======
19+
>>>>>>> Fix Doc mistake and the dataset availability check
1920
- Ported the following Huggingface `LambdaLR`-based schedulers: `ConstantLearningRateScheduler`, `ConstantWithWarmupLearningRateScheduler`, `CosineWithWarmupLearningRateScheduler`, `CosineHardRestartsWithWarmupLearningRateScheduler`.
2021
- Added new `sub_token_mode` parameter to `pretrained_transformer_mismatched_embedder` class to support first sub-token embedding
2122
- Added a way to run a multi task model with a dataset reader as part of `allennlp predict`.

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ test-with-cov :
6666
--cov=$(SRC) \
6767
--cov-report=xml
6868

69+
.PHONY : test-with-cov-html
70+
test-with-cov-html :
71+
pytest --color=yes -rf --durations=40 \
72+
--cov-config=.coveragerc \
73+
--cov=$(SRC) \
74+
--cov-report=html
75+
6976
.PHONY : gpu-test
7077
gpu-test : check-for-cuda
7178
pytest --color=yes -v -rf -m gpu
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import typing
2+
from typing import Iterable, Optional
3+
4+
from allennlp.data import DatasetReader, Token, Field, Tokenizer
5+
from allennlp.data.fields import TextField, LabelField, ListField
6+
from allennlp.data.instance import Instance
7+
from datasets import load_dataset, DatasetDict, Split, list_datasets
8+
from datasets.features import ClassLabel, Sequence, Translation, TranslationVariableLanguages
9+
from datasets.features import Value
10+
11+
12+
@DatasetReader.register("huggingface-datasets")
13+
class HuggingfaceDatasetReader(DatasetReader):
14+
"""
15+
Reads instances from the given huggingface supported dataset
16+
17+
This reader implementation wraps the huggingface datasets package
18+
19+
Following dataset and configurations have been verified and work with this reader
20+
21+
Dataset Dataset Configuration
22+
`xnli` `ar`
23+
`xnli` `en`
24+
`xnli` `de`
25+
`xnli` `all_languages`
26+
`glue` `cola`
27+
`glue` `mrpc`
28+
`glue` `sst2`
29+
`glue` `qqp`
30+
`glue` `mnli`
31+
`glue` `mnli_matched`
32+
`universal_dependencies` `en_lines`
33+
`universal_dependencies` `ko_kaist`
34+
`universal_dependencies` `af_afribooms`
35+
`swahili` `NA`
36+
`conll2003` `NA`
37+
`dbpedia_14` `NA`
38+
`trec` `NA`
39+
`emotion` `NA`
40+
Note: universal_dependencies will require you to install `conllu` package separately
41+
42+
Registered as a `DatasetReader` with name `huggingface-datasets`
43+
44+
# Parameters
45+
46+
dataset_name : `str`
47+
Name of the dataset from huggingface datasets the reader will be used for.
48+
config_name : `str`, optional (default=`None`)
49+
Configuration(mandatory for some datasets) of the dataset.
50+
preload : `bool`, optional (default=`False`)
51+
If `True` all splits for the dataset is loaded(includes download etc) as part of the initialization,
52+
otherwise each split is loaded on when `read()` is used for the same for the first time.
53+
tokenizer : `Tokenizer`, optional (default=`None`)
54+
If specified is used for tokenization of string and text fields from the dataset.
55+
This is useful since text in allennlp is dealt with as a series of tokens.
56+
"""
57+
58+
SUPPORTED_SPLITS = [Split.TRAIN, Split.TEST, Split.VALIDATION]
59+
60+
def __init__(
61+
self,
62+
dataset_name: str = None,
63+
config_name: Optional[str] = None,
64+
preload: Optional[bool] = False,
65+
tokenizer: Optional[Tokenizer] = None,
66+
**kwargs,
67+
) -> None:
68+
super().__init__(
69+
manual_distributed_sharding=True,
70+
manual_multiprocess_sharding=True,
71+
**kwargs,
72+
)
73+
74+
# It would be cleaner to create a separate reader object for diferent dataset
75+
if dataset_name not in list_datasets():
76+
raise ValueError(f"Dataset {dataset_name} not available in huggingface datasets")
77+
self.dataset: DatasetDict = DatasetDict()
78+
self.dataset_name = dataset_name
79+
self.config_name = config_name
80+
self.tokenizer = tokenizer
81+
82+
if preload:
83+
self.load_dataset()
84+
85+
def load_dataset(self):
86+
if self.config_name is not None:
87+
self.dataset = load_dataset(self.dataset_name, self.config_name)
88+
else:
89+
self.dataset = load_dataset(self.dataset_name)
90+
91+
def load_dataset_split(self, split: str):
92+
# TODO add support for datasets.split.NamedSplit
93+
if split in self.SUPPORTED_SPLITS:
94+
if self.config_name is not None:
95+
self.dataset[split] = load_dataset(self.dataset_name, self.config_name, split=split)
96+
else:
97+
self.dataset[split] = load_dataset(self.dataset_name, split=split)
98+
else:
99+
raise ValueError(
100+
f"Only default splits:{self.SUPPORTED_SPLITS} are currently supported."
101+
)
102+
103+
def _read(self, file_path: str) -> Iterable[Instance]:
104+
"""
105+
Reads the dataset and converts the entry to AllenNLP friendly instance
106+
"""
107+
if file_path is None:
108+
raise ValueError("parameter split cannot be None")
109+
110+
# If split is not loaded, load the specific split
111+
if file_path not in self.dataset:
112+
self.load_dataset_split(file_path)
113+
114+
# TODO see if use of Dataset.select() is better
115+
for entry in self.shard_iterable(self.dataset[file_path]):
116+
yield self.text_to_instance(file_path, entry)
117+
118+
def raise_feature_not_supported_value_error(self, value):
119+
raise ValueError(f"Datasets feature type {type(value)} is not supported yet.")
120+
121+
def text_to_instance(self, *inputs) -> Instance:
122+
"""
123+
Takes care of converting dataset entry into AllenNLP friendly instance
124+
Currently it is implemented in an unseemly catch-up model
125+
where it converts datasets.features that are required for the supported dataset,
126+
ideally it would require design where we cleanly deliberate, decide
127+
map dataset.feature to an allenlp.data.field and then go ahead with converting it
128+
Doing that would provide the best chance of providing largest possible coverage with datasets
129+
130+
Currently this is how datasets.features types are mapped to AllenNLP Fields
131+
132+
dataset.feature type allennlp.data.fields
133+
`ClassLabel` `LabelField` in feature name namespace
134+
`Value.string` `TextField` with value as Token
135+
`Value.*` `LabelField` with value being label in feature name namespace
136+
`Sequence.string` `ListField` of `TextField` with individual string as token
137+
`Sequence.ClassLabel` `ListField` of `ClassLabel` in feature name namespace
138+
`Translation` `ListField` of 2 ListField (ClassLabel and TextField)
139+
`TranslationVariableLanguages` `ListField` of 2 ListField (ClassLabel and TextField)
140+
"""
141+
142+
# features indicate the different information available in each entry from dataset
143+
# feature types decide what type of information they are
144+
# e.g. In a Sentiment dataset an entry could have one feature (of type text/string) indicating the text
145+
# and another indicate the sentiment (of typeint32/ClassLabel)
146+
147+
split = inputs[0]
148+
features = self.dataset[split].features
149+
fields = dict()
150+
151+
# TODO we need to support all different datasets features described
152+
# in https://huggingface.co/docs/datasets/features.html
153+
for feature in features:
154+
fields_to_be_added: typing.Dict[str, Field] = dict()
155+
item_field: Field
156+
field_list: list
157+
value = features[feature]
158+
159+
# datasets ClassLabel maps to LabelField
160+
if isinstance(value, ClassLabel):
161+
fields_to_be_added[feature] = LabelField(
162+
inputs[1][feature], label_namespace=feature, skip_indexing=True
163+
)
164+
165+
# datasets Value can be of different types
166+
elif isinstance(value, Value):
167+
168+
# String value maps to TextField
169+
if value.dtype == "string":
170+
# datasets.Value[string] maps to TextField
171+
# If tokenizer is provided we will use it to split it to tokens
172+
# Else put whole text as a single token
173+
if self.tokenizer is not None:
174+
fields_to_be_added[feature] = TextField(
175+
self.tokenizer.tokenize(inputs[1][feature])
176+
)
177+
178+
else:
179+
fields_to_be_added[feature] = TextField([Token(inputs[1][feature])])
180+
181+
else:
182+
fields_to_be_added[feature] = LabelField(
183+
inputs[1][feature], label_namespace=feature, skip_indexing=True
184+
)
185+
186+
elif isinstance(value, Sequence):
187+
# We do not know if the string is token or text, we will assume text and make each a TextField
188+
# datasets.features.Sequence of strings maps to ListField of TextField
189+
if hasattr(value.feature, "dtype") and value.feature.dtype == "string":
190+
field_list2: typing.List[TextField] = list()
191+
for item in inputs[1][feature]:
192+
# If tokenizer is provided we will use it to split it to tokens
193+
# Else put whole text as a single token
194+
tokens: typing.List[Token]
195+
if self.tokenizer is not None:
196+
tokens = self.tokenizer.tokenize(item)
197+
198+
else:
199+
tokens = [Token(item)]
200+
201+
item_field = TextField(tokens)
202+
field_list2.append(item_field)
203+
204+
fields_to_be_added[feature] = ListField(field_list2)
205+
206+
# datasets Sequence of strings to ListField of LabelField
207+
elif isinstance(value.feature, ClassLabel):
208+
field_list = list()
209+
for item in inputs[1][feature]:
210+
item_field = LabelField(
211+
label=item, label_namespace=feature, skip_indexing=True
212+
)
213+
field_list.append(item_field)
214+
215+
fields_to_be_added[feature] = ListField(field_list)
216+
217+
else:
218+
self.raise_feature_not_supported_value_error(value)
219+
220+
# datasets.Translation cannot be mapped directly
221+
# but it's dict structure can be mapped to a ListField of 2 ListField
222+
elif isinstance(value, Translation):
223+
if value.dtype == "dict":
224+
input_dict = inputs[1][feature]
225+
langs = list(input_dict.keys())
226+
texts = list()
227+
for lang in langs:
228+
if self.tokenizer is not None:
229+
tokens = self.tokenizer.tokenize(input_dict[lang])
230+
231+
else:
232+
tokens = [Token(input_dict[lang])]
233+
texts.append(TextField(tokens))
234+
235+
fields_to_be_added[feature + "-languages"] = ListField(
236+
[LabelField(lang, label_namespace="languages") for lang in langs]
237+
)
238+
fields_to_be_added[feature + "-texts"] = ListField(texts)
239+
240+
else:
241+
raise ValueError(f"Datasets feature type {type(value)} is not supported yet.")
242+
243+
# datasets.TranslationVariableLanguages
244+
# is functionally a pair of Lists and hence mapped to a ListField of 2 ListField
245+
elif isinstance(value, TranslationVariableLanguages):
246+
if value.dtype == "dict":
247+
input_dict = inputs[1][feature]
248+
fields_to_be_added[feature + "-language"] = ListField(
249+
[
250+
LabelField(lang, label_namespace=feature + "-language")
251+
for lang in input_dict["language"]
252+
]
253+
)
254+
255+
if self.tokenizer is not None:
256+
fields_to_be_added[feature + "-translation"] = ListField(
257+
[
258+
TextField(self.tokenizer.tokenize(text))
259+
for text in input_dict["translation"]
260+
]
261+
)
262+
else:
263+
fields_to_be_added[feature + "-translation"] = ListField(
264+
[TextField([Token(text)]) for text in input_dict["translation"]]
265+
)
266+
267+
else:
268+
raise ValueError(f"Datasets feature type {type(value)} is not supported yet.")
269+
270+
else:
271+
raise ValueError(f"Datasets feature type {type(value)} is not supported yet.")
272+
273+
for field_key in fields_to_be_added:
274+
fields[field_key] = fields_to_be_added[field_key]
275+
276+
return Instance(fields)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"lmdb",
7373
"more-itertools",
7474
"wandb>=0.10.0,<0.11.0",
75+
"datasets>=1.5.0,<1.6.0",
7576
],
7677
entry_points={"console_scripts": ["allennlp=allennlp.__main__:run"]},
7778
include_package_data=True,

0 commit comments

Comments
 (0)