Packages
Dataset management and caching for AI research benchmarks
Retired package: Deprecated - Use 0.5.0+
Current section
Files
Jump to
Current section
Files
lib/dataset_manager/dataset_dict.ex
defmodule CrucibleDatasets.DatasetDict do
@moduledoc """
A dictionary of Dataset splits.
DatasetDict provides a convenient way to work with multiple splits of a dataset
(e.g., train, test, validation) as a single unit. It supports Python-like
bracket access and provides operations that work across all splits.
## Example
# Create from splits
dataset_dict = DatasetDict.new(%{
"train" => train_dataset,
"test" => test_dataset
})
# Access splits
train = dataset_dict["train"]
# Apply operations to all splits
processed = DatasetDict.map(dataset_dict, fn ds ->
Dataset.filter(ds, &(&1.score > 0.5))
end)
"""
alias CrucibleDatasets.Dataset
@type split_name :: String.t()
@type t :: %__MODULE__{
splits: MapSet.t(split_name()),
datasets: %{split_name() => Dataset.t()}
}
@enforce_keys [:splits, :datasets]
defstruct [:splits, :datasets]
@doc """
Create a new DatasetDict from a map of splits.
Keys can be strings or atoms.
## Example
DatasetDict.new(%{"train" => train, "test" => test})
DatasetDict.new(%{train: train, test: test})
"""
@spec new(%{(String.t() | atom()) => Dataset.t()}) :: t()
def new(datasets_map) when is_map(datasets_map) do
normalized =
Map.new(datasets_map, fn {k, v} ->
key = if is_atom(k), do: Atom.to_string(k), else: k
{key, v}
end)
%__MODULE__{
splits: MapSet.new(Map.keys(normalized)),
datasets: normalized
}
end
@doc """
Create a DatasetDict from a keyword list.
## Example
DatasetDict.from_splits(train: train_dataset, test: test_dataset)
"""
@spec from_splits(keyword(Dataset.t())) :: t()
def from_splits(splits) when is_list(splits) do
new(Map.new(splits))
end
@doc """
Get a split by name.
## Example
train = DatasetDict.get(dd, "train")
train = DatasetDict.get(dd, :train)
"""
@spec get(t(), String.t() | atom()) :: Dataset.t() | nil
def get(%__MODULE__{datasets: datasets}, name) when is_atom(name) do
Map.get(datasets, Atom.to_string(name))
end
def get(%__MODULE__{datasets: datasets}, name) when is_binary(name) do
Map.get(datasets, name)
end
@doc """
Get all split names.
## Example
names = DatasetDict.split_names(dd)
# => ["train", "test", "validation"]
"""
@spec split_names(t()) :: [String.t()]
def split_names(%__MODULE__{splits: splits}) do
MapSet.to_list(splits)
end
@doc """
Get the number of splits.
"""
@spec num_splits(t()) :: non_neg_integer()
def num_splits(%__MODULE__{splits: splits}) do
MapSet.size(splits)
end
@doc """
Add or replace a split.
## Example
updated = DatasetDict.put(dd, "validation", validation_dataset)
"""
@spec put(t(), String.t() | atom(), Dataset.t()) :: t()
def put(%__MODULE__{splits: splits, datasets: datasets}, name, dataset) do
key = normalize_key(name)
%__MODULE__{
splits: MapSet.put(splits, key),
datasets: Map.put(datasets, key, dataset)
}
end
@doc """
Remove a split.
## Example
updated = DatasetDict.delete(dd, "validation")
"""
@spec delete(t(), String.t() | atom()) :: t()
def delete(%__MODULE__{splits: splits, datasets: datasets}, name) do
key = normalize_key(name)
%__MODULE__{
splits: MapSet.delete(splits, key),
datasets: Map.delete(datasets, key)
}
end
@doc """
Apply a function to all splits.
The function receives a Dataset and should return a Dataset.
## Example
# Add index to all items in all splits
result = DatasetDict.map(dd, fn dataset ->
Dataset.add_column(dataset, :idx, fn _, i -> i end)
end)
"""
@spec map(t(), (Dataset.t() -> Dataset.t())) :: t()
def map(%__MODULE__{splits: splits, datasets: datasets}, fun) when is_function(fun, 1) do
new_datasets = Map.new(datasets, fn {name, dataset} -> {name, fun.(dataset)} end)
%__MODULE__{
splits: splits,
datasets: new_datasets
}
end
@doc """
Filter items in all splits.
## Example
# Keep only items with score > 0.5 in all splits
result = DatasetDict.filter(dd, fn item -> item.score > 0.5 end)
"""
@spec filter(t(), (map() -> boolean())) :: t()
def filter(%__MODULE__{} = dd, fun) when is_function(fun, 1) do
map(dd, fn dataset -> Dataset.filter(dataset, fun) end)
end
@doc """
Select columns from all splits.
## Example
result = DatasetDict.select(dd, [:id, :input])
"""
@spec select(t(), [atom() | String.t()]) :: t()
def select(%__MODULE__{} = dd, columns) when is_list(columns) do
map(dd, fn dataset -> Dataset.select(dataset, columns) end)
end
@doc """
Shuffle all splits.
## Options
* `:seed` - Random seed for reproducible shuffling
"""
@spec shuffle(t(), keyword()) :: t()
def shuffle(%__MODULE__{} = dd, opts \\ []) do
map(dd, fn dataset -> Dataset.shuffle(dataset, opts) end)
end
@doc """
Flatten all splits into a single Dataset.
## Example
combined = DatasetDict.flatten(dd)
"""
@spec flatten(t()) :: Dataset.t()
def flatten(%__MODULE__{datasets: datasets}) do
datasets
|> Map.values()
|> Dataset.concat()
end
@doc """
Convert to a plain map.
"""
@spec to_map(t()) :: %{String.t() => Dataset.t()}
def to_map(%__MODULE__{datasets: datasets}), do: datasets
@doc """
Get summary information about the DatasetDict.
"""
@spec info(t()) :: map()
def info(%__MODULE__{} = dd) do
split_counts =
Map.new(dd.datasets, fn {name, dataset} -> {name, length(dataset.items)} end)
%{
num_splits: num_splits(dd),
total_items: Enum.sum(Map.values(split_counts)),
splits: split_counts
}
end
@doc """
Rename a split.
## Example
result = DatasetDict.rename_split(dd, "train", "training")
"""
@spec rename_split(t(), String.t() | atom(), String.t() | atom()) :: t()
def rename_split(%__MODULE__{} = dd, old_name, new_name) do
old_key = normalize_key(old_name)
new_key = normalize_key(new_name)
case Map.pop(dd.datasets, old_key) do
{nil, _} ->
dd
{dataset, remaining} ->
%__MODULE__{
splits: dd.splits |> MapSet.delete(old_key) |> MapSet.put(new_key),
datasets: Map.put(remaining, new_key, dataset)
}
end
end
defp normalize_key(key) when is_atom(key), do: Atom.to_string(key)
defp normalize_key(key) when is_binary(key), do: key
# ===========================================================================
# Access Behaviour
# ===========================================================================
@behaviour Access
@impl Access
def fetch(%__MODULE__{} = dd, key) do
case get(dd, key) do
nil -> :error
dataset -> {:ok, dataset}
end
end
@impl Access
def get_and_update(%__MODULE__{} = dd, key, fun) do
current = get(dd, key)
case fun.(current) do
{get_value, new_value} ->
{get_value, put(dd, key, new_value)}
:pop ->
{current, delete(dd, key)}
end
end
@impl Access
def pop(%__MODULE__{} = dd, key) do
current = get(dd, key)
{current, delete(dd, key)}
end
# ===========================================================================
# Enumerable Protocol
# ===========================================================================
defimpl Enumerable do
def count(dd) do
{:ok, MapSet.size(dd.splits)}
end
def member?(dd, {name, _dataset}) do
{:ok, MapSet.member?(dd.splits, name)}
end
def member?(_dd, _other), do: {:ok, false}
def slice(dd) do
list = Enum.to_list(dd.datasets)
{:ok, length(list), &Enum.slice(list, &1, &2)}
end
def reduce(dd, acc, fun) do
Enumerable.Map.reduce(dd.datasets, acc, fun)
end
end
end