Repository: Continvvm/continuum (language: python)

Issue:
Fix create_scenarios pb with list_transformation

Fix h5 subscenarios
To merge after #245.
Fix the problem of h5 subscenarios.
Online_Fellowship can now be created with a list of either cl_dataset or task_sets.


Discussion / hints:
https://github.com/Continvvm/continuum/issues/227
(I will do the fix for H5 in another PR, it looks complicate and I don't want to make a PR too big)


Fix patch:
diff --git a/continuum/scenarios/__init__.py b/continuum/scenarios/__init__.py
index b0eb5483..aeea23ca 100644
--- a/continuum/scenarios/__init__.py
+++ b/continuum/scenarios/__init__.py
@@ -11,7 +11,7 @@
 from continuum.scenarios.hashed import HashedScenario
 from continuum.scenarios.online_fellowship import OnlineFellowship
 
-from continuum.scenarios.scenario_utils import create_subscenario, encode_scenario
+from continuum.scenarios.scenario_utils import create_subscenario, encode_scenario, remap_class_vector, get_scenario_remapping
 
 __all__ = [
     "ContinualScenario",
diff --git a/continuum/scenarios/scenario_utils.py b/continuum/scenarios/scenario_utils.py
index a94862e3..b0710354 100644
--- a/continuum/scenarios/scenario_utils.py
+++ b/continuum/scenarios/scenario_utils.py
@@ -8,10 +8,64 @@
 from continuum.tasks import TaskType
 
 
+def update_remapping(new_classes, mapping=None):
+    """
+    Output a vector of classes existing class to get new class labels do:
+     new_label = np.array(list(map(lambda x: remapping.tolist().index(x), old_label)))
+
+    :param new_classes: list of new classes
+    :param current_mapping: vector with previous remapping
+    """
+    array_new_classes = np.array(new_classes)
+    if mapping is None:
+        mapping = np.unique(array_new_classes)
+    else:
+        mapping = mapping.tolist()
+        for c in np.unique(array_new_classes):
+            if c not in mapping:
+                mapping.append(c)
+        mapping = np.array(mapping)
+
+    return mapping
+
+
+def remap_class_vector(class_vector, remapping=None):
+    """
+    From a mapping vector and a vector of classes output a vector of remapped classes with the mapping eventually updated
+
+    :param class_vector: vector of class labels to remap
+    :param remapping: 1D vector with current mapping might be None if the mapping does not exist yet
+    """
+    remapping = update_remapping(class_vector, remapping)
+    new_class_vector = np.array(list(map(lambda x: remapping.tolist().index(x), class_vector)))
+    return new_class_vector.astype(int), remapping
+
+
+def get_original_targets(targets: np.ndarray, mapping) -> np.ndarray:
+    """Returns the original targets not changed by the custom class order.
+
+    :param targets: An array of targets, as provided by the task datasets.
+    :param mapping: An array of targets,  that map old classes to new classes
+    :return: An array of targets, with their original values.
+    """
+    return mapping[targets]
+
+
+def get_scenario_remapping(scenario):
+    list_classes = []
+    [list_classes.append(taskset.get_classes()) for taskset in scenario]
+    class_order = np.concatenate(list_classes)  # mapping
+    return class_order
+
+
 def create_subscenario(base_scenario, task_indexes):
     """
     In this function we want to create a subscenario from the different tasks, either by subsampling tasks or reodering
     or both.
+
+    :param base_scenario: scenario from which the subscenario will be created
+    :param task_indexes: array with new order of tasks
+    :return: A train PyTorch's Datasets.
     """
 
     if torch.is_tensor(task_indexes):
@@ -22,6 +76,7 @@ def create_subscenario(base_scenario, task_indexes):
     else:
         transformations = base_scenario.transformations
     sub_scenario = None
+
     if isinstance(base_scenario, OnlineFellowship):
         # We just want to changes base_scenario.cl_datasets order
         new_cl_datasets = [base_scenario.cl_datasets[i] for i in task_indexes]
@@ -37,7 +92,6 @@ def create_subscenario(base_scenario, task_indexes):
         new_x, new_y, new_t = None, None, None
         if base_scenario.cl_dataset.bounding_boxes is not None:
             raise ValueError("the function create_subscenario is not compatible with scenario with bounding_boxes yet.")
-
         for i, index in enumerate(task_indexes):
             taskset = base_scenario[index]
             all_task_indexes = np.arange(len(taskset))
diff --git a/docs/_tutorials/scenarios/scenarios_utils.rst b/docs/_tutorials/scenarios/scenarios_utils.rst
index 31e48eec..052f8877 100644
--- a/docs/_tutorials/scenarios/scenarios_utils.rst
+++ b/docs/_tutorials/scenarios/scenarios_utils.rst
@@ -20,6 +20,70 @@ to reorder classes with a new order of task indexes.
     subscenario = create_subscenario(base_scenario=scenario, task_indexes=[4,3,2,1,0])
 
 
+- ``get_scenario_remapping(scenario)->``
+This function provided a remapping of class that ensure that labels comes in a continuous increasing order.
+It is particularly useful if order of tasks has been changed (for example with ``create_subscenario``).
+This function is often use with the function ``remap_class_vector`` which will apply the remapping.
+
+.. code-block:: python
+    from continuum.scenarios import create_subscenario, get_scenario_remapping, get_original_targets
+    # let say you have a continuum scenario with 5 tasks.
+    from continuum.datasets import MNIST
+
+    scenario = ClassIncremental(
+        MNIST(data_path="my/data/path", download=True, train=True),
+        increment=2
+     )
+
+    # this create a new scenario with a different order of task
+    # Here the order of tasks is reversed.
+    subscenario = create_subscenario(base_scenario=scenario, task_indexes=[4,3,2,1,0])
+
+    # here the class order of subscenario will be:
+    # [task 0: (8,9)] -> [task 1: (6,7)] -> ...
+
+    # get the remapping for the whole classes of the scenario
+    remapping = get_scenario_remapping(subscenario)
+
+    for taskset  in subscenario:
+        loader = DataLoader(taskset)
+        for x, y, t in loader:
+            # remap the class vector here
+            # (the made the process explicit on purpose in order to see where the classes are remapped)
+            remap_y = remap_class_vector(y, mapping=remapping)
+
+            #inverse remapping can be achieved with
+            original_y = get_original_targets(remap_y, mapping=remapping)
+
+            assert np.all(y==original_y) # should be true
+
+The mapping can also be build online with `update_remapping(class_vector, mapping)` (it is more rigorous but it is also more computationnaly costly
+and it does not change the final results).
+
+.. code-block:: python
+    from continuum.scenarios import create_subscenario, update_remapping, remap_class_vector
+
+    scenario = some_scenario()
+
+    # this create a new scenario with a different order of task
+    # Here the order of tasks is reversed.
+    subscenario = create_subscenario(base_scenario=scenario, task_indexes=[4,3,2,1,0])
+    remapping = None
+
+    for taskset  in subscenario:
+        loader = DataLoader(taskset)
+        for x, y, t in loader:
+            # update online and automatically the mapping while receiving class vector
+            remapping = update_remapping(y, remapping)
+            # apply remapping after
+            remap_y = remap_class_vector(y, mapping=remapping)
+
+            #inverse remapping can be achieved with
+            original_y = get_original_targets(remap_y, mapping=remapping)
+
+            assert np.all(y==original_y) # should be true
+
+
 - ``encode_scenario(scenario, model, batch_size, file_name, inference_fct=None)``
 
 This function makes it possible to create a scenario with latent representation of a given model.


Test patch:
diff --git a/tests/test_scenario_utils.py b/tests/test_scenario_utils.py
index d9af7def..ea0f2389 100644
--- a/tests/test_scenario_utils.py
+++ b/tests/test_scenario_utils.py
@@ -8,7 +8,7 @@
 
 from continuum.datasets import InMemoryDataset, MNIST
 from torchvision import transforms
-from continuum.scenarios import ClassIncremental, create_subscenario, encode_scenario
+from continuum.scenarios import ClassIncremental, create_subscenario, encode_scenario, remap_class_vector, get_scenario_remapping
 from continuum.tasks import TaskType
 
 DATA_PATH = os.environ.get("CONTINUUM_DATA_PATH")
@@ -42,6 +42,54 @@ def gen_string():
     return x_train, y_train
 
 
+def test_classes_remapping():
+    mapping = None
+    y_0 = np.random.randint(low=0, high=10, size=100)
+    remap_y_0, mapping = remap_class_vector(y_0, mapping)
+    assert len(np.unique(y_0)) == max(remap_y_0) + 1
+    y_1 = np.random.randint(low=15, high=20, size=100)
+    remap_y_1, mapping = remap_class_vector(y_1, mapping)
+    y_2 = np.random.randint(low=90, high=100, size=100)
+    remap_y_2, mapping = remap_class_vector(y_2, mapping)
+    y_3 = np.random.randint(low=0, high=10, size=100)
+    remap_y_3, mapping = remap_class_vector(y_3, mapping)
+    y_4 = np.random.randint(low=0, high=45, size=100)
+    remap_y_4, mapping = remap_class_vector(y_4, mapping)
+
+    # check that remapping did not change
+    assert np.equal(remap_class_vector(y_0, mapping)[0], remap_y_0).all()
+
+    full_vector = np.concatenate([y_0, y_1, y_2, y_3, y_4], axis=0)
+    assert np.equal(np.unique(remap_class_vector(full_vector)[0]), np.arange(len(mapping))).all()
+    assert len(np.unique(full_vector)) <= len(mapping)
+
+def test_classes_remapping2():
+
+    y_0 = np.random.randint(low=0, high=10, size=100)
+    remap_y_0, mapping = remap_class_vector(y_0, remapping=None)
+    y_1 = np.random.randint(low=15, high=20, size=100)
+    remap_y_1, mapping = remap_class_vector(y_1, mapping)
+
+    assert np.equal(remap_class_vector(y_0, mapping)[0], remap_y_0).all()
+
+def test_scenario_remapping():
+    list_tasks = np.arange(9, -1, -1)
+    x_train, y_train, t_train = gen_data()
+    dummy = InMemoryDataset(x_train, y_train, t_train, data_type=TaskType.IMAGE_PATH)
+    scenario = ClassIncremental(dummy, increment=1)
+    subscenario = create_subscenario(scenario, list_tasks)
+
+    mapping = get_scenario_remapping(subscenario)
+    np_classes = np.zeros(0)
+    np_classes_remapped = np.zeros(0).astype(int)
+    for taskset in subscenario:
+        np_classes = np.concatenate([np_classes, taskset.get_classes()], axis=0)
+        np_classes_remapped = np.concatenate([np_classes_remapped, remap_class_vector(taskset.get_classes(), mapping)[0]], axis=0)
+    nb_classes_seen = len(np_classes)
+
+    assert np.equal(np_classes_remapped, np.arange(nb_classes_seen)).all()
+    assert not np.equal(np_classes, np.arange(nb_classes_seen)).all()
+
 @pytest.mark.parametrize("list_tasks", [
     np.arange(10),
     np.arange(5, 10),
@@ -81,6 +129,7 @@ def test_sequence_transforms(list_tasks):
     subscenario = create_subscenario(scenario, list_tasks)
     assert subscenario.nb_tasks == len(list_tasks), print(f"{len(subscenario)} - vs - {len(list_tasks)}")
 
+
 @pytest.mark.parametrize("list_tasks", [
     np.arange(10),
     np.arange(5, 10),