-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathnatural_id_partitioner.py
More file actions
151 lines (122 loc) · 5.61 KB
/
natural_id_partitioner.py
File metadata and controls
151 lines (122 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# Copyright 2023 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Natural id partitioner class that works with Hugging Face Datasets."""
import numpy as np
from tqdm import tqdm
import datasets
from flwr_datasets.common.typing import NDArrayInt
from flwr_datasets.partitioner.partitioner import Partitioner
class NaturalIdPartitioner(Partitioner):
"""Partitioner for a dataset that can be divided by a column with partition ids.
Parameters
----------
partition_by: str
The name of the column that contains the unique values of partitions.
Examples
--------
"flwrlabs/shakespeare" dataset
>>> from flwr_datasets import FederatedDataset
>>> from flwr_datasets.partitioner import NaturalIdPartitioner
>>>
>>> partitioner = NaturalIdPartitioner(partition_by="character_id")
>>> fds = FederatedDataset(dataset="flwrlabs/shakespeare",
>>> partitioners={"train": partitioner})
>>> partition = fds.load_partition(0)
"sentiment140" (aka Twitter) dataset
>>> from flwr_datasets import FederatedDataset
>>> from flwr_datasets.partitioner import NaturalIdPartitioner
>>>
>>> partitioner = NaturalIdPartitioner(partition_by="user")
>>> fds = FederatedDataset(dataset="sentiment140",
>>> partitioners={"train": partitioner})
>>> partition = fds.load_partition(0)
"""
def __init__(
self,
partition_by: str,
):
super().__init__()
self._partition_id_to_natural_id: dict[int, str] = {}
self._natural_id_to_partition_id: dict[str, int] = {}
self._partition_id_to_indices: dict[int, NDArrayInt] = {}
self._partition_by = partition_by
def _create_int_partition_id_to_natural_id(self) -> None:
"""Create a mapping from int indices to unique client ids from dataset.
Natural ids come from the column specified in `partition_by`.
"""
unique_natural_ids = self.dataset.unique(self._partition_by)
self._partition_id_to_natural_id = dict(
zip(range(len(unique_natural_ids)), unique_natural_ids)
)
def _create_natural_id_to_int_partition_id(self) -> None:
"""Create a mapping from unique client ids from dataset to int indices.
Natural ids come from the column specified in `partition_by`. This object is
inverse of the `self._partition_id_to_natural_id`. This method assumes that
`self._partition_id_to_natural_id` already exist.
"""
self._natural_id_to_partition_id = {
value: key for key, value in self._partition_id_to_natural_id.items()
}
def _create_partition_id_to_indices(self) -> None:
natural_id_to_indices = {} # type: ignore
natural_ids = np.array(self.dataset[self._partition_by])
for index, natural_id in tqdm(
enumerate(natural_ids), desc="Generating partition_id_to_indices"
):
if natural_id not in natural_id_to_indices:
natural_id_to_indices[natural_id] = []
natural_id_to_indices[natural_id].append(index)
self._partition_id_to_indices = {
self._natural_id_to_partition_id[natural_id]: indices
for natural_id, indices in natural_id_to_indices.items()
}
def load_partition(self, partition_id: int) -> datasets.Dataset:
"""Load a single partition corresponding to a single `partition_id`.
The choice of the partition is based on unique integers assigned to each
natural id present in the dataset in the `partition_by` column.
Parameters
----------
partition_id : int
the index that corresponds to the requested partition
Returns
-------
dataset_partition : Dataset
single dataset partition
"""
if len(self._partition_id_to_natural_id) == 0:
self._create_int_partition_id_to_natural_id()
self._create_natural_id_to_int_partition_id()
if len(self._partition_id_to_indices) == 0:
self._create_partition_id_to_indices()
return self.dataset.select(self._partition_id_to_indices[partition_id])
@property
def num_partitions(self) -> int:
"""Total number of partitions."""
if len(self._partition_id_to_natural_id) == 0:
self._create_int_partition_id_to_natural_id()
self._create_natural_id_to_int_partition_id()
return len(self._partition_id_to_natural_id)
@property
def partition_id_to_natural_id(self) -> dict[int, str]:
"""Node id to corresponding natural id present.
Natural ids are the unique values in `partition_by` column in dataset.
"""
return self._partition_id_to_natural_id
@partition_id_to_natural_id.setter
def partition_id_to_natural_id(self, value: dict[int, str]) -> None:
"""Set the partition_id_to_natural_id property."""
raise AttributeError(
"Setting the partition_id_to_natural_id dictionary is not allowed."
)