From 160275390d05aa7b09b9af2ae992ac4a721836bb Mon Sep 17 00:00:00 2001 From: FineArtz Date: Fri, 1 Aug 2025 07:21:51 +0000 Subject: [PATCH 1/9] update MARA, AOGS, AEAGS --- .gitignore | 1 + StreamLearn/Config/AEAGS.py | 20 + StreamLearn/Config/AOGS.py | 20 + StreamLearn/Config/EnsembleUpdate.py | 2 +- StreamLearn/Config/MARA.py | 69 +++ StreamLearn/Config/Simulator.py | 2 + StreamLearn/Config/SwiftCOD.py | 2 +- StreamLearn/Dataset/CIFAR10_Dataset.py | 1 + StreamLearn/Simulator/StreamEnv.py | 30 +- StreamLearn/Simulator/env/StreamEnv.py | 298 ++++++++++ StreamLearn/Simulator/env/StreamGenerator.py | 162 ++++++ StreamLearn/Simulator/policy/AEAGS.py | 194 +++++++ StreamLearn/Simulator/policy/AOGS.py | 230 ++++++++ StreamLearn/Simulator/policy/__init__.py | 0 StreamLearn/Simulator/policy/mat_policy.py | 199 +++++++ StreamLearn/Simulator/policy/mat_runner.py | 374 +++++++++++++ StreamLearn/Simulator/policy/mat_trainer.py | 231 ++++++++ StreamLearn/Simulator/policy/networks.py | 520 ++++++++++++++++++ StreamLearn/Simulator/policy/policy.py | 274 +++++++++ StreamLearn/Simulator/sim_utils/predictor.py | 58 ++ .../Simulator/sim_utils/replay_buffer.py | 358 ++++++++++++ StreamLearn/Simulator/sim_utils/utils.py | 298 ++++++++++ StreamLearn/Simulator/task/__init__.py | 0 StreamLearn/Simulator/task/dataset.py | 209 +++++++ StreamLearn/Simulator/task/models.py | 250 +++++++++ StreamLearn/Simulator/task/rewards.py | 91 +++ StreamLearn/Simulator/task/rl_agent.py | 189 +++++++ StreamLearn/Simulator/task/task.py | 126 +++++ StreamLearn/Simulator/task/task_configs.py | 250 +++++++++ StreamLearn/Simulator/test/main.py | 12 +- StreamLearn/Simulator/test/test_AEAGS.py | 103 ++++ StreamLearn/Simulator/test/test_AOGS.py | 103 ++++ StreamLearn/Simulator/test/test_MARA.py | 91 +++ StreamLearn/legacy/README.md | 162 ++++++ 34 files changed, 4917 insertions(+), 12 deletions(-) create mode 100644 StreamLearn/Config/AEAGS.py create mode 100644 StreamLearn/Config/AOGS.py create mode 100644 StreamLearn/Config/MARA.py create mode 100644 StreamLearn/Simulator/env/StreamEnv.py create mode 100644 StreamLearn/Simulator/env/StreamGenerator.py create mode 100644 StreamLearn/Simulator/policy/AEAGS.py create mode 100644 StreamLearn/Simulator/policy/AOGS.py create mode 100644 StreamLearn/Simulator/policy/__init__.py create mode 100644 StreamLearn/Simulator/policy/mat_policy.py create mode 100644 StreamLearn/Simulator/policy/mat_runner.py create mode 100644 StreamLearn/Simulator/policy/mat_trainer.py create mode 100644 StreamLearn/Simulator/policy/networks.py create mode 100644 StreamLearn/Simulator/policy/policy.py create mode 100644 StreamLearn/Simulator/sim_utils/predictor.py create mode 100644 StreamLearn/Simulator/sim_utils/replay_buffer.py create mode 100644 StreamLearn/Simulator/sim_utils/utils.py create mode 100644 StreamLearn/Simulator/task/__init__.py create mode 100644 StreamLearn/Simulator/task/dataset.py create mode 100644 StreamLearn/Simulator/task/models.py create mode 100644 StreamLearn/Simulator/task/rewards.py create mode 100644 StreamLearn/Simulator/task/rl_agent.py create mode 100644 StreamLearn/Simulator/task/task.py create mode 100644 StreamLearn/Simulator/task/task_configs.py create mode 100644 StreamLearn/Simulator/test/test_AEAGS.py create mode 100644 StreamLearn/Simulator/test/test_AOGS.py create mode 100644 StreamLearn/Simulator/test/test_MARA.py diff --git a/.gitignore b/.gitignore index 97d4a15..4c811f8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ __pycache__/ build .vscode/ /dataset +/datasets /logs .DS_Store diff --git a/StreamLearn/Config/AEAGS.py b/StreamLearn/Config/AEAGS.py new file mode 100644 index 0000000..b109089 --- /dev/null +++ b/StreamLearn/Config/AEAGS.py @@ -0,0 +1,20 @@ +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--task_list", type=str, default="task_list_cifar10") +parser.add_argument("--seed", type=int, default=0) +parser.add_argument("--debug", action="store_true", default=False) +parser.add_argument("--num_nodes", type=int, default=3) +parser.add_argument("--node_comp_ability", type=float, default=1.0) +parser.add_argument("--predictor_gamma", type=float, default=0.9) +parser.add_argument("--final_time", type=int, default=5) + +parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") +parser.add_argument("--generate_gap", type=int, default=1) +parser.add_argument("--pre_generate", action="store_true", default=True) + +parser.add_argument("--num_episodes", type=int, default=2) + +parser.add_argument("--exploration_round", type=int, default=10) + +args, unknown = parser.parse_known_args() \ No newline at end of file diff --git a/StreamLearn/Config/AOGS.py b/StreamLearn/Config/AOGS.py new file mode 100644 index 0000000..b109089 --- /dev/null +++ b/StreamLearn/Config/AOGS.py @@ -0,0 +1,20 @@ +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--task_list", type=str, default="task_list_cifar10") +parser.add_argument("--seed", type=int, default=0) +parser.add_argument("--debug", action="store_true", default=False) +parser.add_argument("--num_nodes", type=int, default=3) +parser.add_argument("--node_comp_ability", type=float, default=1.0) +parser.add_argument("--predictor_gamma", type=float, default=0.9) +parser.add_argument("--final_time", type=int, default=5) + +parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") +parser.add_argument("--generate_gap", type=int, default=1) +parser.add_argument("--pre_generate", action="store_true", default=True) + +parser.add_argument("--num_episodes", type=int, default=2) + +parser.add_argument("--exploration_round", type=int, default=10) + +args, unknown = parser.parse_known_args() \ No newline at end of file diff --git a/StreamLearn/Config/EnsembleUpdate.py b/StreamLearn/Config/EnsembleUpdate.py index 04d3ca6..8dee227 100644 --- a/StreamLearn/Config/EnsembleUpdate.py +++ b/StreamLearn/Config/EnsembleUpdate.py @@ -1,6 +1,6 @@ import argparse -parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser(allow_abbrev=False) # Basic parameters parser.add_argument('--T', type=int, default=2000, help='Number of time steps') parser.add_argument('--dimension', type=int, default=3, help='Feature dimension') diff --git a/StreamLearn/Config/MARA.py b/StreamLearn/Config/MARA.py new file mode 100644 index 0000000..49fcf86 --- /dev/null +++ b/StreamLearn/Config/MARA.py @@ -0,0 +1,69 @@ +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--task_list", type=str, default="task_list_cifar10") +parser.add_argument("--seed", type=int, default=0) +parser.add_argument("--debug", action="store_true", default=False) +parser.add_argument("--algorithm_name", type=str, default="MARA") +parser.add_argument("--experiment_name", type=str, default="test_mara") + +parser.add_argument("--num_nodes", type=int, default=3) +parser.add_argument("--node_comp_ability", type=float, default=1.0) +parser.add_argument("--predictor_gamma", type=float, default=0.9) +parser.add_argument("--final_time", type=int, default=5) + +parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") +parser.add_argument("--generate_gap", type=int, default=1) +parser.add_argument("--pre_generate", action="store_true", default=True) + +parser.add_argument("--lr", type=float, default=5e-4) +parser.add_argument("--opti_eps", type=float, default=1e-5) +parser.add_argument("--weight_decay", type=float, default=0.0) +parser.add_argument("--n_block", type=int, default=1) +parser.add_argument("--n_embd", type=int, default=64) +parser.add_argument("--n_head", type=int, default=1) +parser.add_argument("--encode_state", action="store_true", default=False) +parser.add_argument("--dec_actor", action="store_true", default=False) +parser.add_argument("--share_actor", action="store_true", default=False) +parser.add_argument("--use_policy_active_masks", action="store_true", default=True) + +parser.add_argument("--clip_param", type=float, default=0.2) +parser.add_argument("--ppo_epoch", type=int, default=15) +parser.add_argument("--num_mini_batch", type=int, default=1) +parser.add_argument("--data_chunk_length", type=int, default=10) +parser.add_argument("--value_loss_coef", type=float, default=1) +parser.add_argument("--entropy_coef", type=float, default=0.01) +parser.add_argument("--max_grad_norm", type=float, default=10.0) +parser.add_argument("--huber_delta", type=float, default=10.0) + +parser.add_argument("--episode_length", type=int, default=100) +parser.add_argument("--n_rollout_threads", type=int, default=1) +parser.add_argument("--action_type", type=str, default="Discrete", choices=["Discrete", "Continuous"]) +parser.add_argument("--hidden_size", type=int, default=64) +parser.add_argument("--recurrent_N", type=int, default=1) +parser.add_argument("--gamma", type=float, default=0.99) +parser.add_argument("--gae_lambda", type=float, default=0.95) +parser.add_argument("--use_gae", action="store_true", default=True) +parser.add_argument("--use_popart", action="store_true", default=False) +parser.add_argument("--use_proper_time_limits", action="store_true", default=False) + +parser.add_argument("--use_recurrent_policy", action="store_true", default=False) +parser.add_argument("--use_naive_recurrent_policy", action="store_true", default=False) +parser.add_argument("--use_max_grad_norm", action="store_true", default=True) +parser.add_argument("--use_clipped_value_loss", action="store_true", default=True) +parser.add_argument("--use_huber_loss", action="store_true", default=True) +parser.add_argument("--use_valuenorm", action="store_true", default=True) +parser.add_argument("--use_value_active_masks", action="store_true", default=True) +parser.add_argument("--use_centralized_V", action="store_true", default=True) +parser.add_argument("--use_linear_lr_decay", action="store_true", default=False) + +parser.add_argument("--num_env_steps", type=int, default=1000000) +parser.add_argument("--use_wandb", action="store_true", default=False) +parser.add_argument("--eval_episodes", type=int, default=2) +parser.add_argument("--save_interval", type=int, default=1000) +parser.add_argument("--use_eval", action="store_true", default=True) +parser.add_argument("--eval_interval", type=int, default=1000) +parser.add_argument("--log_interval", type=int, default=100) +parser.add_argument("--run_dir", type=str, default="logs/mat") + +args, unknown = parser.parse_known_args() \ No newline at end of file diff --git a/StreamLearn/Config/Simulator.py b/StreamLearn/Config/Simulator.py index 14ea557..153c9ff 100644 --- a/StreamLearn/Config/Simulator.py +++ b/StreamLearn/Config/Simulator.py @@ -119,3 +119,5 @@ model_config = { # simulator params seed = 0 num_nodes = 2 +generator_name = "ProbStreamGenerator" +generator_params = dict(generate_prob=0.8) \ No newline at end of file diff --git a/StreamLearn/Config/SwiftCOD.py b/StreamLearn/Config/SwiftCOD.py index d58a30f..8de8d60 100644 --- a/StreamLearn/Config/SwiftCOD.py +++ b/StreamLearn/Config/SwiftCOD.py @@ -1,6 +1,6 @@ import argparse -parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("--epochs", default=10000, type=int) parser.add_argument("-N", default=5000, type=int) parser.add_argument("--m_x", default=300, type=int) diff --git a/StreamLearn/Dataset/CIFAR10_Dataset.py b/StreamLearn/Dataset/CIFAR10_Dataset.py index c7cd403..abe38e1 100644 --- a/StreamLearn/Dataset/CIFAR10_Dataset.py +++ b/StreamLearn/Dataset/CIFAR10_Dataset.py @@ -14,6 +14,7 @@ class CIFAR10_Dataset(StreamDataset): raise ValueError("Unknown mode {}.".format(args.dataset_mode)) self.dataset_mode = args.dataset_mode # ['balance', 'imbalance'] self.root_path = './dataset/cifar10_1' + self.data_loader() self.images, self.cluster_labels, self.original_labels = self.load_original_data() self.data = self.split_cifar10() self.m = len(self.data) diff --git a/StreamLearn/Simulator/StreamEnv.py b/StreamLearn/Simulator/StreamEnv.py index 2834829..9333959 100644 --- a/StreamLearn/Simulator/StreamEnv.py +++ b/StreamLearn/Simulator/StreamEnv.py @@ -1,14 +1,13 @@ from enum import Enum from typing import Any, Dict, List, Tuple, Type from queue import PriorityQueue + import numpy as np -import torch from StreamLearn.Dataset import StreamDataset from StreamLearn.Simulator.StreamGenerator import StreamGenerator from StreamLearn.Base.SemiEstimator import StreamEstimator from StreamLearn.Simulator.TaskRepr import TaskRepr -from StreamLearn.Simulator.utils import restrict_to_throughput from StreamLearn.Simulator.model import model_dispatch DEFAULT_NODE_THROUGHPUT = 1024 @@ -78,15 +77,23 @@ class Env: self, num_nodes: int, # number of computing nodes stream_generator: StreamGenerator, # stream generator to generate stream tasks - tasks: List[str], # list of model_desc task_repr: TaskRepr = None, # task representation + rl_mode: bool = False, + marl_mode: bool = False, + debug: bool = False, ) -> None: self.num_node = num_nodes self.stream_generator = stream_generator self.task_repr = task_repr + self.rl_mode = rl_mode + self.marl_mode = marl_mode + self.debug = debug + + self.obs_dim = self.num_node + 1 if rl_mode or marl_mode else None + self.act_dim = self.num_node if rl_mode or marl_mode else None self._init() - self.obs, _ = self.reset() + self.obs, *_ = self.reset() def _init(self): self.t = 0 @@ -131,6 +138,16 @@ class Env: } return self.obs, terminal + def get_rl_obs(self, obs) -> np.ndarray: + node_status = obs['node_status'] + est_time = obs['estimated_time'] + full_obs = np.concatenate([ + np.array(node_status), [est_time] + ]) + if self.marl_mode: + full_obs = np.repeat(full_obs[None], self.num_node, axis=0) + return full_obs + def _forward_time(self, t: int): self.t += t self.stream_generator.forward_time(t) @@ -147,7 +164,10 @@ class Env: self._init() self.stream_generator.reset() obs, terminal = self._get_next_observation() - return obs, terminal + if self.marl_mode: + return obs, obs, terminal, {}, None + else: + return obs, terminal def step(self, act): ''' diff --git a/StreamLearn/Simulator/env/StreamEnv.py b/StreamLearn/Simulator/env/StreamEnv.py new file mode 100644 index 0000000..d823474 --- /dev/null +++ b/StreamLearn/Simulator/env/StreamEnv.py @@ -0,0 +1,298 @@ +from typing import Any, Dict, List, Tuple +import numpy as np + +from StreamLearn.Simulator.env.StreamGenerator import StreamGenerator +from StreamLearn.Simulator.task.task import Task +from StreamLearn.Simulator.sim_utils.predictor import Predictor +from StreamLearn.Simulator.sim_utils import utils + + +class Node: + + def __init__( + self, + comp_ability: float = 1.0, + ) -> None: + self.comp_ability = comp_ability + self._init() + + def _init(self): + self.current_task: Task = None + self.t = 0 + self.cur_task_start_time = 0 + self.last_reward = 0.0 + # A list of (start_time, end_time, task_id or None for idle time) + self.train_history: List[Tuple[int, int, int]] = [] + + def reset(self): + self._init() + + def assign_task(self, task: Task) -> int: + if task is None: + # release the node + if self.current_task is None: + return None + # if the node is idle, do nothing + self.train_history.append((self.cur_task_start_time, self.t, self.current_task.task_id)) + old_task = self.current_task + self.current_task = None + return old_task.task_id + + if self.current_task is None: + self.train_history.append((self.cur_task_start_time, self.t, None)) + elif self.current_task.task_id == task.task_id: + return None + else: + self.train_history.append((self.cur_task_start_time, self.t, self.current_task.task_id)) + old_task = self.current_task + self.current_task = task + self.cur_task_start_time = self.t + return None if old_task is None else old_task.task_id + + def train_task(self) -> int: + self.t += 1 + if self.current_task is None: + self.last_reward = 0.0 + return -1 + self.current_task.stream_train(num_batches=self.comp_ability) + # check if the task is timed out + if not self.current_task.success and self.t > self.current_task.end_time: + self.current_task.time_out = True + self.current_task.success = False + finished_id = -1 + if self.current_task.success or self.current_task.time_out: + self.train_history.append((self.cur_task_start_time, self.t, self.current_task.task_id)) + finished_id = self.current_task.task_id + self.current_task = None + return finished_id + + @property + def is_idle(self): + return self.current_task is None + + +class Env: + + def __init__( + self, + nodes: List[Node], + stream_generator: StreamGenerator, # stream generator to generate stream tasks + predictor: Predictor, + final_time: int = 1000, # no task is generated after final_time + max_active_tasks: int = 10, # maximum number of active tasks + rl_mode: bool = False, # whether to use RL mode + marl_mode: bool = False, # whether to use MARL mode + debug: bool = False, + **kwargs, + ) -> None: + self.nodes = nodes + self.num_nodes = len(nodes) + assert self.num_nodes > 0, 'The number of nodes should be greater than 0.' + self.stream_generator = stream_generator + self.predictor = predictor + self.final_time = final_time + self.max_active_tasks = max_active_tasks + self.rl_mode = rl_mode + self.marl_mode = marl_mode + self.debug = debug + + self.obs_dim = 2 + self.max_active_tasks * 2 if rl_mode or marl_mode else None + self.act_dim = self.max_active_tasks + 1 # +1 for the idle action + + self._init() + + def _init(self): + self.t = 0 + self.task_cnt = 0 + self.active_tasks: Dict[int, Task] = {} + # self.waiting_tasks: Dict[int, Task] = {} + self.finished_tasks: Dict[int, Task] = {} + + # task_id -> node_id, only active tasks are in this map + self.task_node_map: Dict[int, int] = {} + # task_id -> statistics + self.task_statistics: Dict[int, Any] = {} + # self.num_idle = self.num_nodes + self._task_pred_res: Dict[int, Tuple[List[int], List[float]]] = {} # task_id -> (used resources, predicted resources) + + def reset(self): + self._init() + for node in self.nodes: + node.reset() + self.stream_generator.reset() + obs, terminal = self._get_next_observation() + available_actions = np.zeros((self.num_nodes, self.act_dim), dtype=bool) + available_actions[:, -1] = True + if self.marl_mode: + return obs, obs, terminal, {}, available_actions + else: + return obs, terminal + + def step(self, act: np.ndarray): + ''' + act: an list of integers, indicating the action of each node + act[i] == j means the j-th task is assigned to the i-th node + act[i] == max_active_task means keep the current task in the i-th node or the i-th node is idle + ''' + assert len(act) == self.num_nodes + act = act.copy() + act[act == -1] = self.max_active_tasks + total_rew = 0 + upd_act = np.array(act, dtype=np.int32) # action for updating the nodes + # check if the action is valid + for i, task_id in enumerate(upd_act): + assert task_id == self.max_active_tasks or task_id in self.active_tasks, f"Invalid task id: {task_id}" + # no duplicate task assignment + non_neg_upd = upd_act[upd_act < self.max_active_tasks] + assert len(non_neg_upd) == len(set(non_neg_upd)), f"Duplicate task assignment detected: {upd_act}" + + + # assign tasks to nodes + self.task_node_map = {task_id: -1 for task_id in self.active_tasks.keys()} # reset the task-node map + for i, task_id in enumerate(upd_act): + if task_id == self.max_active_tasks: + self.nodes[i].assign_task(None) # release the node + else: + self.nodes[i].assign_task(self.active_tasks[task_id]) + self.task_node_map[task_id] = i + + total_rew = self._forward_time() + obs, terminal = self._get_next_observation() + if self.marl_mode: + available_actions = np.zeros(self.act_dim, dtype=bool) + for tid in self.active_tasks.keys(): + available_actions[tid] = True + available_actions[-1] = True + available_actions = np.repeat(available_actions[None], self.num_nodes, axis=0) + return obs, obs, total_rew, terminal, {}, available_actions + else: + return obs, total_rew, terminal, {} + + def _finish_task(self, task_id: int) -> float: + ''' + Finish a task and return the reward. + ''' + task = self.active_tasks[task_id] + node_id = self.task_node_map[task_id] + rew = task.get_reward() + self.task_statistics[task_id] = rew + self.finished_tasks[task_id] = task + del self.active_tasks[task_id] + del self.task_node_map[task_id] + if self.debug: + print(f'Task {task_id} is finished in node {node_id}, loss = {task.losses[-1]:.3f}, thresh = {task.epsilon}, reward = {rew :.3f}.') + return rew + + def _forward_time(self) -> float: + ''' + Forward the time by 1 unit. + All nodes train the current task if there is any, + and return the reward if the task is finished. + The generator may generate a new task. + ''' + self.t += 1 + total_rew = 0.0 + rew = 0.0 + for node in self.nodes: + task_id = node.train_task() + if task_id > -1: # -1 means no task is finished + rew = self._finish_task(task_id) + total_rew += rew + node.last_reward = rew + elif node.current_task is not None: + rew = node.current_task.reward_func(node.current_task.losses[-1]) + total_rew += rew + node.last_reward = rew + else: + rew = 0.0 + active_task_ids = list(self.active_tasks.keys()) + for task_id in active_task_ids: + task = self.active_tasks[task_id] + if self.t > task.end_time: + if not task.success: + task.time_out = True + task.success = False + total_rew += self._finish_task(task_id) + + if self.t <= self.final_time: + self.stream_generator.forward_time() + if len(self.active_tasks) < self.max_active_tasks: + new_task = next(self.stream_generator) + if new_task is not None: # None means no new task is generated + new_task.to(utils.device) + self.active_tasks[self.task_cnt] = new_task + self.task_node_map[self.task_cnt] = -1 # -1 means the task is not assigned to any node + self._task_pred_res[self.task_cnt] = ([], []) + self.task_cnt += 1 + return total_rew + + def _get_next_observation(self): + ''' + Get the current observation and check if the simulation is terminated. + ''' + pred_resc = [] + pred_rews = [] + task_ids = [] + for task_id, task in self.active_tasks.items(): + pred_resource, pred_loss = self.predictor.predict(task) + pred_resc.append(pred_resource) + pred_rew = task.reward_func(pred_loss) + pred_rews.append(pred_rew) + task_ids.append(task_id) + if pred_resource != -1: + self._task_pred_res[task_id][0].append(task.resources[-1]) # append the used resources + self._task_pred_res[task_id][1].append(pred_resource) # append the predicted resources + + pred_resc = np.array(pred_resc) + pred_rews = np.array(pred_rews) + task_ids = np.array(task_ids, dtype=int) + task_stats = np.stack([pred_resc, pred_rews], axis=0) + obs = { + 't': self.t, + 'nodes': self.nodes, + 'tasks': self.active_tasks, + 'task_ids': task_ids, + 'task_stats': task_stats, + } + terminal = (self.t > self.final_time) and len(self.active_tasks) == 0 + return obs, terminal + + def get_rl_obs(self, obs) -> np.ndarray: + nodes = obs['nodes'] + tasks = obs['tasks'] + num_nodes = len(nodes) + # num_task = len(tasks) + task_ids = obs['task_ids'] + pred_rsc = obs['task_stats'][0] + t = obs['t'] + pred_time = [pred_rsc[i] / tasks[tid].N if tid in tasks else 0.0 for i, tid in enumerate(task_ids)] + task_available_time = [tasks[tid].end_time - t if tid in tasks else 0.0 for tid in task_ids] + # pad them to self.max_active_tasks + if (num_task := len(pred_time)) < self.max_active_tasks: + pred_time += [0.0] * (self.max_active_tasks - num_task) + task_available_time += [0.0] * (self.max_active_tasks - num_task) + + assert not np.any(np.isinf(pred_time)) + + full_obs = np.concatenate([ + np.array([num_nodes, num_task], dtype=np.float32), + pred_time, + task_available_time, + ]) + if self.marl_mode: + full_obs = np.repeat(full_obs[None], self.num_nodes, axis=0) + return full_obs + + def log_statistics(self): + print(f'----- Time: {self.t} -----') + for task_id in range(self.task_cnt): + if task_id in self.finished_tasks: + task = self.finished_tasks[task_id] + print(f'Task {task_id} finished, loss: {task.losses[-1] if len(task.losses) > 0 else np.inf:.3f}, thresh: {task.epsilon}, reward: {self.task_statistics[task_id]:.3f}.') + elif task_id in self.active_tasks: + task = self.active_tasks[task_id] + if self.task_node_map[task_id] == -1: + print(f'Task {task_id} is active but not assigned to any node.') + else: + print(f'Task {task_id} is trained in Node {self.task_node_map[task_id]}. Current loss: {task.losses[-1]:.3f}, thresh: {task.epsilon}.') + \ No newline at end of file diff --git a/StreamLearn/Simulator/env/StreamGenerator.py b/StreamLearn/Simulator/env/StreamGenerator.py new file mode 100644 index 0000000..3089aca --- /dev/null +++ b/StreamLearn/Simulator/env/StreamGenerator.py @@ -0,0 +1,162 @@ +import random +from typing import Dict, List +import numpy as np + +from StreamLearn.Simulator.task.task import Task +from StreamLearn.Simulator.task.task_configs import TaskConfig + + +class StreamGenerator: + + def __init__( + self, + available_task_list: List[TaskConfig], + debug: bool = False, + **kwargs: Dict[str, any], + ) -> None: + self.available_task_list = available_task_list + self.num_tasks = len(available_task_list) + self.debug = debug + self._init() + + def _init(self): + self.task_cnt = 0 + self.t = 0 + self.flag = False + + def __iter__(self): + return self + + def __next__(self) -> Task: + task = None + config_id = self._generate_new_task() + if config_id == -1: + return None + task_config = self.available_task_list[config_id]() + + # Task id starts from 0, and is incremented after each task is generated + task = Task( + config = task_config, + start_time=self.t, + task_id=self.task_cnt, + ) + # if self.debug: + print(f'New task {task_config.__class__.__name__} is generated, task id: {self.task_cnt}, start time: {self.t}, end time: {task.end_time}') + self.task_cnt += 1 + return task + + def reset(self): + self._init() + + def forward_time(self): + self.t += 1 + + def _generate_new_task(self) -> int: + # This function determines to generate new task using which config + # -1 for no new task + # Implement this in the subclass + return NotImplementedError() + + +class TestStreamGenerator(StreamGenerator): + """ + Instantiate all tasks at initialization. + Only for testing. + """ + def __init__(self, available_task_list, **kwargs): + super().__init__(available_task_list, **kwargs) + for task_config_class in available_task_list: + task_config = task_config_class() + print(f"Loading task {task_config.__class__.__name__}") + + task = Task( + config = task_config, + start_time=self.t, + task_id=self.task_cnt, + ) + self.task_cnt += 1 + print(f"Task {task_config.__class__.__name__} created") + + +class UniformStreamGenerator(StreamGenerator): + + def __init__( + self, + available_task_list: List[TaskConfig], + generate_gap: int = 10, + seed: int = None, + pre_generate: bool = False, + **kwargs: Dict[str, any], + ) -> None: + super().__init__(available_task_list, **kwargs) + assert generate_gap > 0 + self.generate_gap = generate_gap + self.last_generation = -np.inf + if seed is not None: + np.random.seed(seed) + self.current_config_id = -1 # To track the last generated task config ID + self.pre_generate = pre_generate + if pre_generate: + assert "final_time" in kwargs, "final_time must be provided for pre-generation" + self.final_time = kwargs["final_time"] + self._pre_generate() + print(f"Pre-generated {len(self.task_ids)} tasks until time {self.final_time}") + print(f"Task IDs: {self.task_ids}") + + def _pre_generate(self): + self.task_ids = [] + while self.t < self.final_time: + config_id = self._generate_new_task(in_pre_gen=True) + if config_id != -1: + self.task_ids.append((self.t, config_id)) + self.forward_time() + self.t = 0 # Reset time after pre-generation + self.current_config_id = 0 + + def _generate_new_task(self, in_pre_gen: bool = False) -> int: + if not in_pre_gen and self.pre_generate: + # return pre-generated task IDs + if self.t >= self.task_ids[self.current_config_id][0]: + config_id = self.task_ids[self.current_config_id][1] + self.current_config_id += 1 + return config_id + else: + return -1 + if self.t - self.last_generation >= self.generate_gap: + self.last_generation = self.t + # return random.randint(0, self.num_tasks - 1) + if self.current_config_id >= self.num_tasks - 1: + return np.random.randint(0, self.num_tasks) + else: + self.current_config_id += 1 + return self.current_config_id + else: + return -1 + + def reset(self): + super().reset() + self.last_generation = -np.inf + self.current_config_id = -1 + if self.pre_generate: + self._pre_generate() + + +class ProbStreamGenerator(StreamGenerator): + + def __init__( + self, + available_task_list: List[TaskConfig], + generate_prob: float = 0.1, + seed: int = None, + **kwargs: Dict[str, any], + ) -> None: + super().__init__(available_task_list, **kwargs) + self.generate_prob = generate_prob + if seed is not None: + np.random.seed(seed) + + def _generate_new_task(self) -> int: + if np.random.rand() < self.generate_prob: + return random.randint(0, self.num_tasks - 1) + else: + return -1 diff --git a/StreamLearn/Simulator/policy/AEAGS.py b/StreamLearn/Simulator/policy/AEAGS.py new file mode 100644 index 0000000..bda4e4a --- /dev/null +++ b/StreamLearn/Simulator/policy/AEAGS.py @@ -0,0 +1,194 @@ +import random +import numpy as np +import math +from collections import defaultdict + +from StreamLearn.Simulator.policy.policy import BasePolicy + + +class AEAGSPolicy(BasePolicy): + ''' + AEAGS policy for task scheduling. + ''' + + def __init__(self, exploration_round: int = 10): + self.initialized = False + self.mu_hat = None + self.T = None + self.UCB = None + self.LCB = None + self.Better = None + self.arm_preferences = None + + self.last_actions = None + self.round_count = 0 + self.last_task_ids = None + self.exploration_round = exploration_round + + def reset(self): + self.initialized = False + self.round_count = 0 + + def act(self, obs): + nodes = obs['nodes'] + num_nodes = len(nodes) + task_ids = obs['task_ids'] + num_tasks = len(task_ids) + + act = np.array([-1] * num_nodes, dtype=np.int32) + current_actions = [-1] * num_nodes + + if self.last_task_ids is not None and set(self.last_task_ids) != set(task_ids): + self.initialized = False + self.last_task_ids = task_ids.copy() + + if not self.initialized: + self._initialize(obs, num_nodes) + self.initialized = True + + if num_tasks <= num_nodes: + assignments = [] + for i in range(num_nodes): + for task in task_ids: + pref_rank = self.arm_preferences[task].index(i) if task in self.arm_preferences else 0 + assignments.append((i, task, -pref_rank)) + assignments.sort(key=lambda x: x[2], reverse=True) + + assigned_tasks = set() + assigned_nodes = set() + for i, task, pref in assignments: + if i not in assigned_nodes and task not in assigned_tasks: + act[i] = task + assigned_nodes.add(i) + assigned_tasks.add(task) + if len(assigned_tasks) == num_tasks: + break + + for i in range(num_nodes): + if act[i] == -1 and self.last_actions is not None and i < len(self.last_actions): + act[i] = self.last_actions[i] + + self.last_actions = np.array(act) + return act + + self._update_player_statistics(num_nodes, num_tasks) + matched_actions = self._central_matching(num_nodes, task_ids) + act = np.array([-1] * num_nodes, dtype=np.int32) + current_actions = [-1] * num_nodes + for i, task in matched_actions.items(): + act[i] = task + current_actions[i] = task + self.last_actions = current_actions + self.round_count += 1 + return act + + def _initialize(self, obs, num_nodes): + self.mu_hat = [defaultdict(float) for _ in range(num_nodes)] + self.T = [defaultdict(int) for _ in range(num_nodes)] + self.UCB = [defaultdict(lambda: float('inf')) for _ in range(num_nodes)] + self.LCB = [defaultdict(lambda: float('-inf')) for _ in range(num_nodes)] + self.Better = [defaultdict(lambda: defaultdict(int)) for _ in range(num_nodes)] + self.last_actions = None + self.round_count = 0 + + self.arm_preferences = {} + for task in obs['task_ids']: + pref = list(range(num_nodes)) + random.shuffle(pref) + self.arm_preferences[task] = pref + + for i in range(num_nodes): + for task in obs['task_ids']: + self.mu_hat[i][task] = 0.0 + self.T[i][task] = 0 + self.UCB[i][task] = float('inf') + self.LCB[i][task] = float('-inf') + + def _update_stats(self, rewards): + for i in range(len(self.mu_hat)): + task = self.last_actions[i] + if task == -1: + continue + + r = rewards[i] + n = self.T[i][task] + 1 + old_mu = self.mu_hat[i][task] + self.mu_hat[i][task] = old_mu + (r - old_mu) / n + self.T[i][task] = n + + def _update_player_statistics(self, num_nodes, num_tasks): + for i in range(num_nodes): + total_pulls = sum(self.T[i].values()) + + for task in self.T[i]: + if self.T[i][task] > 0: + # Compute confidence bounds (using sqrt(6) as in algorithm) + delta = math.sqrt(6 * math.log(max(1, total_pulls)) / self.T[i][task]) + self.UCB[i][task] = self.mu_hat[i][task] + delta + self.LCB[i][task] = self.mu_hat[i][task] - delta + else: + self.UCB[i][task] = float('inf') + self.LCB[i][task] = float('-inf') + + for j in range(num_tasks): + for j_prime in range(num_tasks): + if j == j_prime: + continue + task_j = self.last_task_ids[j] + task_j_prime = self.last_task_ids[j_prime] + if (task_j in self.LCB[i] and task_j_prime in self.UCB[i] and + self.LCB[i][task_j] > self.UCB[i][task_j_prime]): + self.Better[i][task_j][task_j_prime] = 1 + else: + self.Better[i][task_j][task_j_prime] = 0 + + def _central_matching(self, num_nodes, task_ids): + """ + Implement the Subroutine-of-AE-AGS algorithm (Algorithm 2) + Returns a matching dictionary: node_id -> task + """ + A = [set() for _ in range(num_nodes)] + m = [-1] * num_nodes + m_inv = {t: -1 for t in task_ids} + s = {t: 0 for t in task_ids} + + while True: + active_arms = [t for t in task_ids if m_inv[t] == -1 and s[t] < num_nodes] + if not active_arms: + break + for task in active_arms: + if s[task] >= len(self.arm_preferences[task]): + continue + i = self.arm_preferences[task][s[task]] + A[i].add(task) + D_i = set() + for j in A[i]: + for j_prime in A[i]: + if j != j_prime and self.Better[i][j_prime][j] == 1: + D_i.add(j) + break + + candidate_arms = A[i] - D_i + if candidate_arms: + min_T = min(self.T[i][t] for t in candidate_arms) + best_arms = [t for t in candidate_arms if self.T[i][t] == min_T] + selected_arm = random.choice(best_arms) if best_arms else None + + if selected_arm is not None: + prev_match = m_inv[selected_arm] + if prev_match != -1: + m[prev_match] = -1 + m[i] = selected_arm + m_inv[selected_arm] = i + + for other_arm in A[i]: + if other_arm != selected_arm: + s[other_arm] += 1 + if m_inv[other_arm] == i: + m_inv[other_arm] = -1 + A[i] = {selected_arm} + + if m_inv[task] != i: + s[task] += 1 + + return {i: m[i] for i in range(num_nodes) if m[i] != -1} diff --git a/StreamLearn/Simulator/policy/AOGS.py b/StreamLearn/Simulator/policy/AOGS.py new file mode 100644 index 0000000..aaaac78 --- /dev/null +++ b/StreamLearn/Simulator/policy/AOGS.py @@ -0,0 +1,230 @@ +import random +import numpy as np +import math +from collections import defaultdict + +from StreamLearn.Simulator.policy.policy import BasePolicy + + +class AOGSPolicy(BasePolicy): + ''' + AOGS policy for task scheduling. + ''' + + def __init__(self, exploration_round: int = 10): + self.initialized = False + self.mu_hat = None + self.T = None + self.UCB = None + self.LCB = None + self.D = None + self.A = None + self.E = None + self.next_arm_index = None + self.preferences = None + self.last_actions = None + self.round_count = 0 + self.last_task_ids = None + self.exploration_round = exploration_round + + def reset(self): + self.initialized = False + self.round_count = 0 + + def act(self, obs): + nodes = obs['nodes'] + num_nodes = len(nodes) + task_ids = obs['task_ids'] + num_tasks = len(task_ids) + + act = np.array([-1] * num_nodes, dtype=np.int32) + current_actions = [-1] * num_nodes + + if self.last_task_ids is not None and set(self.last_task_ids) != set(task_ids): + self.initialized = False + self.last_task_ids = task_ids.copy() + + if not self.initialized: + self._initialize(obs, num_nodes) + self.initialized = True + + if num_tasks <= num_nodes: + assignments = [] + for i in range(num_nodes): + for task in task_ids: + assignments.append((i, task, self.preferences[i][task])) + assignments.sort(key=lambda x: x[2], reverse=True) + assigned_tasks = set() + assigned_nodes = set() + + for i, task, pref in assignments: + if i not in assigned_nodes and task not in assigned_tasks: + act[i] = task + assigned_nodes.add(i) + assigned_tasks.add(task) + if len(assigned_tasks) == num_tasks: + break + + for i in range(num_nodes): + if act[i] == -1 and self.last_actions is not None and i < len(self.last_actions): + act[i] = self.last_actions[i] + self.last_actions = np.array(act) + return act + + rewards = [node.last_reward for node in nodes] + if self.last_actions is not None: + self._update_stats(rewards) + + candidate_actions = {} + for i in range(num_nodes): + if self.round_count > self.exploration_round: + if self.A[i]: + best_task = max(self.A[i], key=lambda t: self.UCB[i][t]) + candidate_actions[i] = best_task + else: + candidate_actions[i] = self.last_actions[i] \ + if self.last_actions and i < len(self.last_actions) else -1 + elif self.E[i]: + available = sorted(list(self.A[i])) + if available: + idx = self.next_arm_index[i] % len(available) + selected = available[idx] + candidate_actions[i] = selected + self.next_arm_index[i] = (idx + 1) % len(available) + else: + if len(self.A[i]) == 1: + selected = next(iter(self.A[i])) + candidate_actions[i] = selected + + matched_actions = self._gale_shapley(candidate_actions, num_nodes, task_ids) + for i, task in matched_actions.items(): + act[i] = task + current_actions[i] = task + + if self.round_count < self.exploration_round: + self._update_internal_state() + self.last_actions = current_actions + self.round_count += 1 + + return act + + def _initialize(self, obs, num_nodes): + self.mu_hat = [defaultdict(float) for _ in range(num_nodes)] + self.T = [defaultdict(int) for _ in range(num_nodes)] + self.UCB = [defaultdict(lambda: float('inf')) for _ in range(num_nodes)] + self.LCB = [defaultdict(lambda: float('-inf')) for _ in range(num_nodes)] + self.D = [set() for _ in range(num_nodes)] + self.A = [set(obs['task_ids']) for _ in range(num_nodes)] + self.E = [True] * num_nodes + self.next_arm_index = [0] * num_nodes + self.last_actions = None + self.preferences = [{} for _ in range(num_nodes)] + for i in range(num_nodes): + for task in obs['task_ids']: + self.preferences[i][task] = random.uniform(0.5, 1.0) + + for i in range(num_nodes): + for task in obs['task_ids']: + self.mu_hat[i][task] = 0.0 + self.T[i][task] = 0 + self.UCB[i][task] = float('inf') + self.LCB[i][task] = float('-inf') + + def _update_stats(self, rewards): + for i in range(len(self.mu_hat)): + task = self.last_actions[i] + if task == -1: + continue + + r = rewards[i] + n = self.T[i][task] + 1 + old_mu = self.mu_hat[i][task] + self.mu_hat[i][task] = old_mu + (r - old_mu) / n + self.T[i][task] = n + total_pulls = sum(self.T[i].values()) + for t in self.T[i]: + if self.T[i][t] > 0 and total_pulls > 0: + delta = math.sqrt(2 * math.log(total_pulls) / self.T[i][t]) + self.UCB[i][t] = self.mu_hat[i][t] + delta + self.LCB[i][t] = self.mu_hat[i][t] - delta + + def _update_internal_state(self): + num_nodes = len(self.mu_hat) + for i in range(num_nodes): + if len(self.A[i]) > num_nodes: + max_LCB = max(self.LCB[i][a] for a in self.A[i]) + to_remove = [a for a in self.A[i] if self.UCB[i][a] < max_LCB] + self.A[i] -= set(to_remove) + for i in range(num_nodes): + for a in list(self.A[i]): + if all(self.LCB[i][a] > self.UCB[i][ap] for ap in self.A[i] if ap != a): + self.A[i] = {a} + self.E[i] = False + break + for i in range(num_nodes): + for j in range(num_nodes): + if i == j or self.E[j]: + continue + if not self.A[j]: + continue + a_j = next(iter(self.A[j])) + if self.preferences[j][a_j] > self.preferences[i][a_j]: + self.D[i].add(a_j) + self.A[i] = set(self.T[i].keys()) - self.D[i] + + if not self.E[i] and a_j in self.D[i]: + self.E[i] = True + + def _gale_shapley(self, candidate_actions, num_nodes, task_ids): + task_conflicts = defaultdict(list) + for node, task in candidate_actions.items(): + if task != -1: + task_conflicts[task].append(node) + conflicted_tasks = {task for task, nodes in task_conflicts.items() if len(nodes) > 1} + if not conflicted_tasks: + return candidate_actions + + node_prefs = {} + task_prefs = {} + for i in range(num_nodes): + available_tasks = sorted(list(self.A[i]), key=lambda t: self.UCB[i][t], reverse=True) + node_prefs[i] = available_tasks + for task in task_ids: + task_prefs[task] = sorted(range(num_nodes), key=lambda i: self.preferences[i].get(task, 0), reverse=True) + + free_nodes = list(range(num_nodes)) + node_matches = {i: -1 for i in range(num_nodes)} + task_matches = {t: -1 for t in task_ids} + next_proposal = [0] * num_nodes + + while free_nodes: + node = free_nodes.pop(0) + prefs = node_prefs[node] + if next_proposal[node] >= len(prefs): + continue + + task = prefs[next_proposal[node]] + next_proposal[node] += 1 + current_match = task_matches[task] + if current_match == -1: + task_matches[task] = node + node_matches[node] = task + else: + pref_list = task_prefs[task] + if pref_list.index(node) < pref_list.index(current_match): + task_matches[task] = node + node_matches[node] = task + node_matches[current_match] = -1 + free_nodes.append(current_match) + else: + free_nodes.append(node) + + for i in range(num_nodes): + if node_matches[i] == -1 and candidate_actions[i] != -1: + task = candidate_actions[i] + if task_matches[task] == -1: + node_matches[i] = task + task_matches[task] = i + + return node_matches + \ No newline at end of file diff --git a/StreamLearn/Simulator/policy/__init__.py b/StreamLearn/Simulator/policy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StreamLearn/Simulator/policy/mat_policy.py b/StreamLearn/Simulator/policy/mat_policy.py new file mode 100644 index 0000000..309f180 --- /dev/null +++ b/StreamLearn/Simulator/policy/mat_policy.py @@ -0,0 +1,199 @@ +import torch +import numpy as np + +from StreamLearn.Simulator.env.StreamEnv import Env +from StreamLearn.Simulator.policy import networks +from StreamLearn.Simulator.policy.policy import RLPolicy +from StreamLearn.Simulator.sim_utils import utils + + +class MATPolicy(RLPolicy): + ''' + Multi-Agent Transformer Policy + This policy uses a transformer encoder to process the observations and outputs actions for each agent. + ''' + + def __init__( + self, + env: Env, + lr: float = 5e-4, + opti_eps: float = 1e-5, + weight_decay: float = 0.0, + n_block: int = 1, + n_embd: int = 64, + n_head: int = 1, + encode_state: bool = False, + dec_actor: bool = False, + share_actor: bool = False, + use_policy_active_masks: bool = True, + + debug: bool = False, + **kwargs, + ): + self.env = env + self.lr = lr + self.opti_eps = opti_eps + self.weight_decay = weight_decay + self.use_policy_active_masks = use_policy_active_masks + + self.action_type = 'Discrete' + self.obs_dim = env.obs_dim + self.share_obs_dim = env.obs_dim # actually unused + self.act_dim = env.act_dim + self.act_num = 1 + self.num_agents = env.num_nodes + + self.transformer = networks.MultiAgentTransformer( + self.share_obs_dim, self.obs_dim, self.act_dim, self.num_agents, + n_block=n_block, n_embd=n_embd, n_head=n_head, + encode_state=encode_state, action_type=self.action_type, + dec_actor=dec_actor, share_actor=share_actor + ) + self.optimizer = torch.optim.Adam( + self.transformer.parameters(), + lr=self.lr, + eps=self.opti_eps, + weight_decay=self.weight_decay + ) + + def lr_decay(self, episode, episodes): + lr = self.lr - (self.lr * (episode / float(episodes))) + for param_group in self.optimizer.param_groups: + param_group['lr'] = lr + + def get_actions(self, cent_obs, obs, rnn_states_actor, rnn_states_critic, masks, available_actions=None, + deterministic: bool = False): + """ + Compute actions and value function predictions for the given inputs. + :param cent_obs (np.ndarray): centralized input to the critic. + :param obs (np.ndarray): local agent inputs to the actor. + :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor. + :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic. + :param masks: (np.ndarray) denotes points at which RNN states should be reset. + :param available_actions: (np.ndarray) denotes which actions are available to agent + (if None, all actions available) + :param deterministic: (bool) whether the action should be mode of distribution or should be sampled. + + :return values: (torch.Tensor) value function predictions. + :return actions: (torch.Tensor) actions to take. + :return action_log_probs: (torch.Tensor) log probabilities of chosen actions. + :return rnn_states_actor: (torch.Tensor) updated actor network RNN states. + :return rnn_states_critic: (torch.Tensor) updated critic network RNN states. + """ + + cent_obs = cent_obs.reshape(-1, self.num_agents, self.share_obs_dim) + obs = obs.reshape(-1, self.num_agents, self.obs_dim) + if available_actions is not None: + available_actions = available_actions.reshape(-1, self.num_agents, self.act_dim) + + actions, action_log_probs, values = self.transformer.get_actions(cent_obs, + obs, + available_actions, + deterministic) + + actions = actions.view(-1, self.act_num) + action_log_probs = action_log_probs.view(-1, self.act_num) + values = values.view(-1, 1) + + # unused, just for compatibility + rnn_states_actor = utils.from_numpy(rnn_states_actor) + rnn_states_critic = utils.from_numpy(rnn_states_critic) + return values, actions, action_log_probs, rnn_states_actor, rnn_states_critic + + def get_values(self, cent_obs, obs, rnn_states_critic, masks, available_actions=None): + """ + Get value function predictions. + :param cent_obs (np.ndarray): centralized input to the critic. + :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic. + :param masks: (np.ndarray) denotes points at which RNN states should be reset. + + :return values: (torch.Tensor) value function predictions. + """ + + cent_obs = cent_obs.reshape(-1, self.num_agents, self.share_obs_dim) + obs = obs.reshape(-1, self.num_agents, self.obs_dim) + if available_actions is not None: + available_actions = available_actions.reshape(-1, self.num_agents, self.act_dim) + + values = self.transformer.get_values(cent_obs, obs, available_actions) + + values = values.view(-1, 1) + + return values + + def evaluate_actions(self, cent_obs, obs, rnn_states_actor, rnn_states_critic, actions, masks, + available_actions=None, active_masks=None): + """ + Get action logprobs / entropy and value function predictions for actor update. + :param cent_obs (np.ndarray): centralized input to the critic. + :param obs (np.ndarray): local agent inputs to the actor. + :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor. + :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic. + :param actions: (np.ndarray) actions whose log probabilites and entropy to compute. + :param masks: (np.ndarray) denotes points at which RNN states should be reset. + :param available_actions: (np.ndarray) denotes which actions are available to agent + (if None, all actions available) + :param active_masks: (torch.Tensor) denotes whether an agent is active or dead. + + :return values: (torch.Tensor) value function predictions. + :return action_log_probs: (torch.Tensor) log probabilities of the input actions. + :return dist_entropy: (torch.Tensor) action distribution entropy for the given inputs. + """ + cent_obs = cent_obs.reshape(-1, self.num_agents, self.share_obs_dim) + obs = obs.reshape(-1, self.num_agents, self.obs_dim) + actions = actions.reshape(-1, self.num_agents, self.act_num) + if available_actions is not None: + available_actions = available_actions.reshape(-1, self.num_agents, self.act_dim) + + action_log_probs, values, entropy = self.transformer(cent_obs, obs, actions, available_actions) + + action_log_probs = action_log_probs.view(-1, self.act_num) + values = values.view(-1, 1) + entropy = entropy.view(-1, self.act_num) + + if self.use_policy_active_masks and active_masks is not None: + entropy = (entropy*active_masks).sum()/active_masks.sum() + else: + entropy = entropy.mean() + + return values, action_log_probs, entropy + + def act(self, cent_obs, obs, rnn_states_actor, masks, available_actions=None, deterministic=True): + """ + Compute actions using the given inputs. + :param obs (np.ndarray): local agent inputs to the actor. + :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor. + :param masks: (np.ndarray) denotes points at which RNN states should be reset. + :param available_actions: (np.ndarray) denotes which actions are available to agent + (if None, all actions available) + :param deterministic: (bool) whether the action should be mode of distribution or should be sampled. + """ + + # this function is just a wrapper for compatibility + rnn_states_critic = np.zeros_like(rnn_states_actor) + _, actions, _, rnn_states_actor, _ = self.get_actions(cent_obs, + obs, + rnn_states_actor, + rnn_states_critic, + masks, + available_actions, + deterministic) + + return actions, rnn_states_actor + + def save(self, path, timestep: int): + torch.save(self.transformer.state_dict(), str(path) + "/transformer_" + str(timestep) + ".pt") + + def load(self, path): + transformer_state_dict = torch.load(path) + self.transformer.load_state_dict(transformer_state_dict) + # self.transformer.reset_std() + + def train(self): + self.transformer.train() + + def eval(self): + self.transformer.eval() + + def to(self, device: torch.device): + self.transformer.to(device) \ No newline at end of file diff --git a/StreamLearn/Simulator/policy/mat_runner.py b/StreamLearn/Simulator/policy/mat_runner.py new file mode 100644 index 0000000..95e1d41 --- /dev/null +++ b/StreamLearn/Simulator/policy/mat_runner.py @@ -0,0 +1,374 @@ +from pathlib import Path +import time +from typing import Dict, Any +import os +import wandb +import numpy as np +import torch +from tensorboardX import SummaryWriter + +from StreamLearn.Simulator.env.StreamEnv import Env +from StreamLearn.Simulator.sim_utils.replay_buffer import SharedReplayBuffer +from StreamLearn.Simulator.sim_utils import utils +from StreamLearn.Simulator.policy.mat_trainer import MATTrainer +from StreamLearn.Simulator.policy.mat_policy import MATPolicy + + +class Runner: + """ + Base class for training recurrent policies. + :param config: (dict) Config dictionary containing parameters for training. + """ + def __init__( + self, + env: Env, + config: Dict[str, Any], + eval_env: Env = None, + ): + self.env = env + self.eval_env = eval_env + self.config = config + self.debug = config["debug"] + self.num_agents = env.num_nodes + + # # parameters + self.algorithm_name = config["algorithm_name"] + self.experiment_name = config["experiment_name"] + self.use_centralized_V = config["use_centralized_V"] + self.num_env_steps = config["num_env_steps"] + self.episode_length = config["episode_length"] + self.n_rollout_threads = config["n_rollout_threads"] + self.use_linear_lr_decay = config["use_linear_lr_decay"] + self.hidden_size = config["hidden_size"] + self.use_wandb = config["use_wandb"] + self.recurrent_N = config["recurrent_N"] + self.eval_episodes = config["eval_episodes"] + + # # interval + self.save_interval = config["save_interval"] + self.use_eval = config["use_eval"] + self.eval_interval = config["eval_interval"] + self.log_interval = config["log_interval"] + + # # dir + self.model_dir = config.get("model_dir", None) + + if self.use_wandb: + self.save_dir = str(wandb.run.dir) + self.run_dir = str(wandb.run.dir) + else: + self.run_dir = Path(config["run_dir"]) + self.log_dir = str(self.run_dir / 'logs') + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) + self.writter = SummaryWriter(self.log_dir) + self.save_dir = str(self.run_dir / 'models') + if not os.path.exists(self.save_dir): + os.makedirs(self.save_dir) + + # policy network + self.policy = MATPolicy(self.env, **config) + + if self.model_dir is not None: + self.load(self.model_dir) + + # algorithm + self.trainer = MATTrainer(self.policy, self.num_agents, **config) + + # buffer + self.buffer = SharedReplayBuffer(self.env, **config) + + def run(self): + self.warmup() + + start = time.time() + episodes = int(self.num_env_steps) // self.episode_length // self.n_rollout_threads + + train_episode_rewards = [0 for _ in range(self.n_rollout_threads)] + done_episodes_rewards = [] + + for episode in range(episodes): + if self.use_linear_lr_decay: + self.trainer.policy.lr_decay(episode, episodes) + + for step in range(self.episode_length): + # Sample actions + values, actions, action_log_probs, rnn_states, rnn_states_critic = self.collect(step) + + # Obser reward and next obs + obs, share_obs, rewards, dones, infos, available_actions = self.env.step(actions.reshape(-1)) + + if isinstance(dones, bool): + dones = np.array([dones] * self.num_agents, dtype=bool).reshape(1, -1) + dones_env = np.all(dones, axis=1) + if isinstance(rewards, float): + rewards = np.array([rewards] * self.num_agents, dtype=float).reshape(1, -1, 1) + reward_env = np.mean(rewards, axis=1).flatten() + train_episode_rewards += reward_env + for t in range(self.n_rollout_threads): + if dones_env[t]: + done_episodes_rewards.append(train_episode_rewards[t]) + train_episode_rewards[t] = 0 + + if isinstance(obs, dict): + obs = self.env.get_rl_obs(obs) + share_obs = self.env.get_rl_obs(share_obs) + + data = obs, share_obs, rewards, dones, infos, available_actions, \ + values, actions, action_log_probs, \ + rnn_states, rnn_states_critic + + # insert data into buffer + self.insert(data) + + # compute return and update network + self.compute() + train_infos = self.train() + + # post process + total_num_steps = (episode + 1) * self.episode_length * self.n_rollout_threads + # save model + if (episode % self.save_interval == 0 or episode == episodes - 1): + self.save(episode) + + # log information + if episode % self.log_interval == 0: + end = time.time() + print("\n Algo {} Exp {} updates {}/{} episodes, total num timesteps {}/{}, FPS {}.\n" + .format(self.algorithm_name, + self.experiment_name, + episode, + episodes, + total_num_steps, + self.num_env_steps, + int(total_num_steps / (end - start)))) + + self.log_train(train_infos, total_num_steps) + + if len(done_episodes_rewards) > 0: + aver_episode_rewards = np.mean(done_episodes_rewards) + print("some episodes done, average rewards: ", aver_episode_rewards) + self.writter.add_scalars("train_episode_rewards", {"aver_rewards": aver_episode_rewards}, total_num_steps) + done_episodes_rewards = [] + + # eval + if episode % self.eval_interval == 0 and self.use_eval: + self.eval(total_num_steps) + + if self.debug: + print("Early break for debugging.") + break + + def warmup(self): + obs, share_obs, *_ = self.env.reset() + + # replay buffer + if not self.use_centralized_V: + share_obs = obs + + if isinstance(obs, dict): + obs = self.env.get_rl_obs(obs) + share_obs = self.env.get_rl_obs(share_obs) + self.buffer.share_obs[0] = share_obs.copy() + self.buffer.obs[0] = obs.copy() + + @torch.no_grad() + def collect(self, step: int): + self.trainer.prep_rollout() + available_actions = np.zeros(self.env.act_dim, dtype=int) # +1 for the idle action + available_actions[-1] = 1 + for tid in self.env.active_tasks.keys(): + available_actions[tid] = 1 + available_actions = np.repeat(available_actions[None], self.num_agents, axis=0) + value, action, action_log_prob, rnn_state, rnn_state_critic \ + = self.trainer.policy.get_actions(np.concatenate(self.buffer.share_obs[step]), + np.concatenate(self.buffer.obs[step]), + np.concatenate(self.buffer.rnn_states[step]), + np.concatenate(self.buffer.rnn_states_critic[step]), + np.concatenate(self.buffer.masks[step]), + available_actions=available_actions) + # [self.envs, agents, dim] + values = np.array(np.split(utils.get_numpy(value), self.n_rollout_threads)) + actions = np.array(np.split(utils.get_numpy(action), self.n_rollout_threads)) + action_log_probs = np.array(np.split(utils.get_numpy(action_log_prob), self.n_rollout_threads)) + rnn_states = np.array(np.split(utils.get_numpy(rnn_state), self.n_rollout_threads)) + rnn_states_critic = np.array(np.split(utils.get_numpy(rnn_state_critic), self.n_rollout_threads)) + + return values, actions, action_log_probs, rnn_states, rnn_states_critic + + def insert(self, data): + obs, share_obs, rewards, dones, infos, available_actions, \ + values, actions, action_log_probs, rnn_states, rnn_states_critic = data + + if isinstance(dones, bool): + dones_env = np.array([dones], dtype=bool) + else: + dones_env = np.all(dones, axis=1) + # dones_env = np.all(dones, axis=1) + + rnn_states[dones_env == True] = np.zeros(((dones_env == True).sum(), self.num_agents, self.recurrent_N, self.hidden_size), dtype=np.float32) + rnn_states_critic[dones_env == True] = np.zeros(((dones_env == True).sum(), self.num_agents, *self.buffer.rnn_states_critic.shape[3:]), dtype=np.float32) + + masks = np.ones((self.n_rollout_threads, self.num_agents, 1), dtype=np.float32) + masks[dones_env == True] = np.zeros(((dones_env == True).sum(), self.num_agents, 1), dtype=np.float32) + + active_masks = np.ones((self.n_rollout_threads, self.num_agents, 1), dtype=np.float32) + active_masks[dones == True] = np.zeros(((dones == True).sum(), 1), dtype=np.float32) + active_masks[dones_env == True] = np.ones(((dones_env == True).sum(), self.num_agents, 1), dtype=np.float32) + + if not self.use_centralized_V: + share_obs = obs + + self.buffer.insert(share_obs, obs, rnn_states, rnn_states_critic, + actions, action_log_probs, values, rewards, masks, None, active_masks, + available_actions) + + @torch.no_grad() + def compute(self): + """Calculate returns for the collected data.""" + self.trainer.prep_rollout() + if self.buffer.available_actions is None: + next_values = self.trainer.policy.get_values(np.concatenate(self.buffer.share_obs[-1]), + np.concatenate(self.buffer.obs[-1]), + np.concatenate(self.buffer.rnn_states_critic[-1]), + np.concatenate(self.buffer.masks[-1])) + else: + next_values = self.trainer.policy.get_values(np.concatenate(self.buffer.share_obs[-1]), + np.concatenate(self.buffer.obs[-1]), + np.concatenate(self.buffer.rnn_states_critic[-1]), + np.concatenate(self.buffer.masks[-1]), + np.concatenate(self.buffer.available_actions[-1])) + next_values = np.array(np.split(utils.get_numpy(next_values), self.n_rollout_threads)) + self.buffer.compute_returns(next_values, self.trainer.value_normalizer) + + def train(self): + """Train policies with data in buffer. """ + self.trainer.prep_training() + train_infos = self.trainer.train(self.buffer) + self.buffer.after_update() + return train_infos + + def save(self, episode): + """Save policy's actor and critic networks.""" + self.policy.save(self.save_dir, episode) + + def load(self, model_dir): + """Restore policy's networks from a saved model.""" + self.policy.load(model_dir) + + def log_train(self, train_infos, total_num_steps): + """ + Log training info. + :param train_infos: (dict) information about training update. + :param total_num_steps: (int) total number of training env steps. + """ + for k, v in train_infos.items(): + if self.use_wandb: + wandb.log({k: v}, step=total_num_steps) + else: + self.writter.add_scalars(k, {k: v}, total_num_steps) + + def log_env(self, env_infos, total_num_steps): + """ + Log env info. + :param env_infos: (dict) information about env state. + :param total_num_steps: (int) total number of training env steps. + """ + for k, v in env_infos.items(): + if len(v)>0: + if self.use_wandb: + wandb.log({k: np.mean(v)}, step=total_num_steps) + else: + self.writter.add_scalars(k, {k: np.mean(v)}, total_num_steps) + + @torch.no_grad() + def eval(self, total_num_steps: int): + eval_episode = 0 + eval_episode_rewards = [] + one_episode_rewards = [0 for _ in range(self.eval_episodes)] + + eval_obs, eval_share_obs, *_ = self.eval_env.reset() + eval_rnn_states = np.zeros((self.eval_episodes, self.num_agents, self.recurrent_N, + self.hidden_size), dtype=np.float32) + eval_masks = np.ones((self.eval_episodes, self.num_agents, 1), dtype=np.float32) + + while True: + self.trainer.prep_rollout() + eval_actions, eval_rnn_states = \ + self.trainer.policy.act(np.concatenate(eval_share_obs), + np.concatenate(eval_obs), + np.concatenate(eval_rnn_states), + np.concatenate(eval_masks), + deterministic=True) + eval_actions = np.array(np.split(utils.get_numpy(eval_actions), self.eval_episodes)) + eval_rnn_states = np.array(np.split(utils.get_numpy(eval_rnn_states), self.eval_episodes)) + + # Obser reward and next obs + eval_obs, eval_share_obs, eval_rewards, eval_dones, eval_infos, _ = self.eval_env.step(eval_actions) + eval_rewards = np.mean(eval_rewards, axis=1).flatten() + one_episode_rewards += eval_rewards + + eval_dones_env = np.all(eval_dones, axis=1) + eval_rnn_states[eval_dones_env == True] = np.zeros(((eval_dones_env == True).sum(), self.num_agents, + self.recurrent_N, self.hidden_size), dtype=np.float32) + eval_masks = np.ones((self.eval_episodes, self.num_agents, 1), dtype=np.float32) + eval_masks[eval_dones_env == True] = np.zeros(((eval_dones_env == True).sum(), self.num_agents, 1), + dtype=np.float32) + + for eval_i in range(self.eval_episodes): + if eval_dones_env[eval_i]: + eval_episode += 1 + eval_episode_rewards.append(one_episode_rewards[eval_i]) + one_episode_rewards[eval_i] = 0 + + if eval_episode >= self.eval_episodes: + eval_env_infos = {'eval_average_episode_rewards': eval_episode_rewards, + 'eval_max_episode_rewards': [np.max(eval_episode_rewards)]} + + self.log_env(eval_env_infos, total_num_steps) + print("eval_average_episode_rewards is {}.".format(np.mean(eval_episode_rewards))) + break + + def simulate(self): + succ_list = [] + task_cnt_list = [] + for episode in range(self.eval_episodes): + eval_obs, eval_share_obs, eval_dones, _, available_actions = self.eval_env.reset() + eval_rnn_states = np.zeros((1, self.num_agents, self.recurrent_N, + self.hidden_size), dtype=np.float32) + eval_masks = np.ones((1, self.num_agents, 1), dtype=np.float32) + step = 0 + total_reward = 0 + while not eval_dones: + self.trainer.prep_rollout() + eval_obs = self.eval_env.get_rl_obs(eval_obs) + eval_share_obs = self.eval_env.get_rl_obs(eval_share_obs) + with torch.no_grad(): + eval_actions, eval_rnn_states = \ + self.trainer.policy.act(np.concatenate(eval_share_obs), + np.concatenate(eval_obs), + np.concatenate(eval_rnn_states), + np.concatenate(eval_masks), + np.concatenate(available_actions), + deterministic=True) + eval_actions = np.array(np.split(utils.get_numpy(eval_actions), 1)) + eval_rnn_states = np.array(np.split(utils.get_numpy(eval_rnn_states), 1)) + + eval_obs, eval_share_obs, eval_rewards, eval_dones, eval_infos, available_actions = self.eval_env.step(eval_actions.reshape(-1)) + total_reward += eval_rewards + + step += 1 + if step % 50 == 0: + self.eval_env.log_statistics() + self.eval_env.log_statistics() + succ_list.append(total_reward) + generator = self.eval_env.stream_generator + task_cnt_list.append(generator.task_cnt) + print(f"=== Episode {episode + 1}/{self.eval_episodes} finished. " + f"Success tasks: {total_reward}/{generator.task_cnt}. " + f"Success rate: {total_reward / generator.task_cnt:.3f}\n") + print(f"Average success rate over {self.eval_episodes} episodes: {sum(succ_list) / sum(task_cnt_list):.3f}") + + def to(self, device: torch.device): + self.trainer.to(device) + self.policy.to(device) diff --git a/StreamLearn/Simulator/policy/mat_trainer.py b/StreamLearn/Simulator/policy/mat_trainer.py new file mode 100644 index 0000000..be71684 --- /dev/null +++ b/StreamLearn/Simulator/policy/mat_trainer.py @@ -0,0 +1,231 @@ +import numpy as np +import torch +import torch.nn as nn + +from StreamLearn.Simulator.policy.networks import ValueNorm +from StreamLearn.Simulator.policy.mat_policy import MATPolicy +from StreamLearn.Simulator.sim_utils import utils +from StreamLearn.Simulator.sim_utils.replay_buffer import SharedReplayBuffer + + +class MATTrainer: + """ + Trainer class for MAT to update policies. + :param args: (argparse.Namespace) arguments containing relevant model, policy, and env information. + :param policy: (R_MAPPO_Policy) policy to update. + :param device: (torch.device) specifies the device to run on (cpu/gpu). + """ + def __init__( + self, + policy: MATPolicy, + num_agents: int, + clip_param: float = 0.2, + ppo_epoch: int = 15, + num_mini_batch: int = 1, + data_chunk_length: int = 10, + value_loss_coef: float = 1, + entropy_coef: float = 0.01, + max_grad_norm: float = 10.0, + huber_delta: float = 10.0, + + use_recurrent_policy: bool = False, + use_naive_recurrent_policy: bool = False, + use_max_grad_norm: bool = True, + use_clipped_value_loss: bool = True, + use_huber_loss: bool = True, + use_valuenorm: bool = True, + use_value_active_masks: bool = True, + use_policy_active_masks: bool = True, + dec_actor: bool = False, + + debug: bool = False, + **kwargs, + ): + + self.tpdv = dict(dtype=torch.float32) + self.policy = policy + self.num_agents = num_agents + + self.clip_param = clip_param + self.ppo_epoch = ppo_epoch + self.num_mini_batch = num_mini_batch + self.data_chunk_length = data_chunk_length + self.value_loss_coef = value_loss_coef + self.entropy_coef = entropy_coef + self.max_grad_norm = max_grad_norm + self.huber_delta = huber_delta + + self._use_recurrent_policy = use_recurrent_policy + self._use_naive_recurrent = use_naive_recurrent_policy + self._use_max_grad_norm = use_max_grad_norm + self._use_clipped_value_loss = use_clipped_value_loss + self._use_huber_loss = use_huber_loss + self._use_valuenorm = use_valuenorm + self._use_value_active_masks = use_value_active_masks + self._use_policy_active_masks = use_policy_active_masks + self.dec_actor = dec_actor + + if self._use_valuenorm: + self.value_normalizer = ValueNorm(1) + else: + self.value_normalizer = None + + def cal_value_loss(self, values, value_preds_batch, return_batch, active_masks_batch): + """ + Calculate value function loss. + :param values: (torch.Tensor) value function predictions. + :param value_preds_batch: (torch.Tensor) "old" value predictions from data batch (used for value clip loss) + :param return_batch: (torch.Tensor) reward to go returns. + :param active_masks_batch: (torch.Tensor) denotes if agent is active or dead at a given timesep. + + :return value_loss: (torch.Tensor) value function loss. + """ + + value_pred_clipped = value_preds_batch + (values - value_preds_batch).clamp(-self.clip_param, + self.clip_param) + + if self._use_valuenorm: + self.value_normalizer.update(return_batch) + error_clipped = self.value_normalizer.normalize(return_batch) - value_pred_clipped + error_original = self.value_normalizer.normalize(return_batch) - values + else: + error_clipped = return_batch - value_pred_clipped + error_original = return_batch - values + + if self._use_huber_loss: + value_loss_clipped = utils.huber_loss(error_clipped, self.huber_delta) + value_loss_original = utils.huber_loss(error_original, self.huber_delta) + else: + value_loss_clipped = utils.mse_loss(error_clipped) + value_loss_original = utils.mse_loss(error_original) + + if self._use_clipped_value_loss: + value_loss = torch.max(value_loss_original, value_loss_clipped) + else: + value_loss = value_loss_original + + # if self._use_value_active_masks and not self.dec_actor: + if self._use_value_active_masks: + value_loss = (value_loss * active_masks_batch).sum() / active_masks_batch.sum() + else: + value_loss = value_loss.mean() + + return value_loss + + def ppo_update(self, sample): + """ + Update actor and critic networks. + :param sample: (Tuple) contains data batch with which to update networks. + :update_actor: (bool) whether to update actor network. + + :return value_loss: (torch.Tensor) value function loss. + :return critic_grad_norm: (torch.Tensor) gradient norm from critic up9date. + ;return policy_loss: (torch.Tensor) actor(policy) loss value. + :return dist_entropy: (torch.Tensor) action entropies. + :return actor_grad_norm: (torch.Tensor) gradient norm from actor update. + :return imp_weights: (torch.Tensor) importance sampling weights. + """ + share_obs_batch, obs_batch, rnn_states_batch, rnn_states_critic_batch, actions_batch, \ + value_preds_batch, return_batch, masks_batch, active_masks_batch, old_action_log_probs_batch, \ + adv_targ, available_actions_batch = sample + + old_action_log_probs_batch = utils.from_numpy(old_action_log_probs_batch).to(**self.tpdv) + adv_targ = utils.from_numpy(adv_targ).to(**self.tpdv) + value_preds_batch = utils.from_numpy(value_preds_batch).to(**self.tpdv) + return_batch = utils.from_numpy(return_batch).to(**self.tpdv) + active_masks_batch = utils.from_numpy(active_masks_batch).to(**self.tpdv) + + # Reshape to do in a single forward pass for all steps + values, action_log_probs, dist_entropy = self.policy.evaluate_actions(share_obs_batch, + obs_batch, + rnn_states_batch, + rnn_states_critic_batch, + actions_batch, + masks_batch, + available_actions_batch, + active_masks_batch) + # actor update + imp_weights = torch.exp(action_log_probs - old_action_log_probs_batch) + + surr1 = imp_weights * adv_targ + surr2 = torch.clamp(imp_weights, 1.0 - self.clip_param, 1.0 + self.clip_param) * adv_targ + + if self._use_policy_active_masks: + policy_loss = (-torch.sum(torch.min(surr1, surr2), + dim=-1, + keepdim=True) * active_masks_batch).sum() / active_masks_batch.sum() + else: + policy_loss = -torch.sum(torch.min(surr1, surr2), dim=-1, keepdim=True).mean() + + # critic update + value_loss = self.cal_value_loss(values, value_preds_batch, return_batch, active_masks_batch) + + loss = policy_loss - dist_entropy * self.entropy_coef + value_loss * self.value_loss_coef + + self.policy.optimizer.zero_grad() + loss.backward() + + if self._use_max_grad_norm: + grad_norm = nn.utils.clip_grad_norm_(self.policy.transformer.parameters(), self.max_grad_norm) + else: + grad_norm = utils.get_grad_norm(self.policy.transformer.parameters()) + + self.policy.optimizer.step() + + return value_loss, grad_norm, policy_loss, dist_entropy, grad_norm, imp_weights + + def train(self, buffer: SharedReplayBuffer): + """ + Perform a training update using minibatch GD. + :param buffer: (SharedReplayBuffer) buffer containing training data. + :param update_actor: (bool) whether to update actor network. + + :return train_info: (dict) contains information regarding training update (e.g. loss, grad norms, etc). + """ + advantages_copy = buffer.advantages.copy() + advantages_copy[buffer.active_masks[:-1] == 0.0] = np.nan + mean_advantages = np.nanmean(advantages_copy) + std_advantages = np.nanstd(advantages_copy) + advantages = (buffer.advantages - mean_advantages) / (std_advantages + 1e-5) + + + train_info = {} + + train_info['value_loss'] = 0 + train_info['policy_loss'] = 0 + train_info['dist_entropy'] = 0 + train_info['actor_grad_norm'] = 0 + train_info['critic_grad_norm'] = 0 + train_info['ratio'] = 0 + + for _ in range(self.ppo_epoch): + data_generator = buffer.feed_forward_generator_transformer(advantages, self.num_mini_batch) + + for sample in data_generator: + + value_loss, critic_grad_norm, policy_loss, dist_entropy, actor_grad_norm, imp_weights \ + = self.ppo_update(sample) + + train_info['value_loss'] += value_loss.item() + train_info['policy_loss'] += policy_loss.item() + train_info['dist_entropy'] += dist_entropy.item() + train_info['actor_grad_norm'] += actor_grad_norm + train_info['critic_grad_norm'] += critic_grad_norm + train_info['ratio'] += imp_weights.mean() + + num_updates = self.ppo_epoch * self.num_mini_batch + + for k in train_info.keys(): + train_info[k] /= num_updates + + return train_info + + def prep_training(self): + self.policy.train() + + def prep_rollout(self): + self.policy.eval() + + def to(self, device: torch.device): + if self.value_normalizer is not None: + self.value_normalizer.to(device) \ No newline at end of file diff --git a/StreamLearn/Simulator/policy/networks.py b/StreamLearn/Simulator/policy/networks.py new file mode 100644 index 0000000..29427bb --- /dev/null +++ b/StreamLearn/Simulator/policy/networks.py @@ -0,0 +1,520 @@ +""" +Networks for RL policies. +""" +import math +from typing import Tuple +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.distributions import Categorical, Normal + +from StreamLearn.Simulator.sim_utils import utils + + +def init_(m, gain=0.01, activate=False): + if activate: + gain = nn.init.calculate_gain('relu') + nn.init.orthogonal_(m.weight, gain=gain) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + return m + + +class SoftQ(nn.Module): + def __init__(self, input_dim, output_dim, hidden_dim=256): + super().__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) + self.fc2 = nn.Linear(hidden_dim, hidden_dim) + self.fc3 = nn.Linear(hidden_dim, output_dim) + + def forward(self, x: torch.Tensor, a: torch.Tensor) -> torch.Tensor: + x = torch.cat([x, a], dim=1) # Concatenate state and action + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + x = self.fc3(x) + return x + + +class NCatActor(nn.Module): + def __init__(self, input_dim, output_dim, hidden_dim=256): + super().__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) + self.fc2 = nn.Linear(hidden_dim, hidden_dim) + self.fc_logits = nn.Linear(hidden_dim, output_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + logits = self.fc_logits(x) + return logits + + def get_action(self, x: torch.Tensor, deterministic: bool = False, sample_num: int = 1) -> Tuple: + logits = self.forward(x) + # use Gumbel top-k trick to sample k indices + if deterministic: + action = torch.topk(logits, k=sample_num, dim=-1).indices + log_probs = F.log_softmax(logits, dim=-1) + action_log_probs = torch.gather(log_probs, dim=-1, index=action) + else: + gumbel = -torch.log(-torch.log(torch.rand_like(logits) + 1e-10)) + noisy_logits = logits + gumbel + action = torch.topk(noisy_logits, k=sample_num, dim=-1).indices + log_probs = F.log_softmax(logits, dim=-1) + action_log_probs = torch.gather(log_probs, dim=-1, index=action) + return action, action_log_probs.sum(-1, keepdim=True), log_probs + + +class SelfAttention(nn.Module): + + def __init__(self, n_embd, n_head, n_agent, masked=False): + super(SelfAttention, self).__init__() + + assert n_embd % n_head == 0 + self.masked = masked + self.n_head = n_head + # key, query, value projections for all heads + self.key = init_(nn.Linear(n_embd, n_embd)) + self.query = init_(nn.Linear(n_embd, n_embd)) + self.value = init_(nn.Linear(n_embd, n_embd)) + # output projection + self.proj = init_(nn.Linear(n_embd, n_embd)) + # if self.masked: + # causal mask to ensure that attention is only applied to the left in the input sequence + self.register_buffer("mask", torch.tril(torch.ones(n_agent + 1, n_agent + 1)) + .view(1, 1, n_agent + 1, n_agent + 1)) + + self.att_bp = None + + def forward(self, key, value, query): + B, L, D = query.size() + + # calculate query, key, values for all heads in batch and move head forward to be the batch dim + k = self.key(key).view(B, L, self.n_head, D // self.n_head).transpose(1, 2) # (B, nh, L, hs) + q = self.query(query).view(B, L, self.n_head, D // self.n_head).transpose(1, 2) # (B, nh, L, hs) + v = self.value(value).view(B, L, self.n_head, D // self.n_head).transpose(1, 2) # (B, nh, L, hs) + + # causal attention: (B, nh, L, hs) x (B, nh, hs, L) -> (B, nh, L, L) + att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1))) + + # self.att_bp = F.softmax(att, dim=-1) + + if self.masked: + att = att.masked_fill(self.mask[:, :, :L, :L] == 0, float('-inf')) + att = F.softmax(att, dim=-1) + + y = att @ v # (B, nh, L, L) x (B, nh, L, hs) -> (B, nh, L, hs) + y = y.transpose(1, 2).contiguous().view(B, L, D) # re-assemble all head outputs side by side + + # output projection + y = self.proj(y) + return y + + +class EncodeBlock(nn.Module): + """ an unassuming Transformer block """ + + def __init__(self, n_embd, n_head, n_agent): + super(EncodeBlock, self).__init__() + + self.ln1 = nn.LayerNorm(n_embd) + self.ln2 = nn.LayerNorm(n_embd) + # self.attn = SelfAttention(n_embd, n_head, n_agent, masked=True) + self.attn = SelfAttention(n_embd, n_head, n_agent, masked=False) + self.mlp = nn.Sequential( + init_(nn.Linear(n_embd, 1 * n_embd), activate=True), + nn.GELU(), + init_(nn.Linear(1 * n_embd, n_embd)) + ) + + def forward(self, x): + x = self.ln1(x + self.attn(x, x, x)) + x = self.ln2(x + self.mlp(x)) + return x + + +class DecodeBlock(nn.Module): + """ an unassuming Transformer block """ + + def __init__(self, n_embd, n_head, n_agent): + super(DecodeBlock, self).__init__() + + self.ln1 = nn.LayerNorm(n_embd) + self.ln2 = nn.LayerNorm(n_embd) + self.ln3 = nn.LayerNorm(n_embd) + self.attn1 = SelfAttention(n_embd, n_head, n_agent, masked=True) + self.attn2 = SelfAttention(n_embd, n_head, n_agent, masked=True) + self.mlp = nn.Sequential( + init_(nn.Linear(n_embd, 1 * n_embd), activate=True), + nn.GELU(), + init_(nn.Linear(1 * n_embd, n_embd)) + ) + + def forward(self, x, rep_enc): + x = self.ln1(x + self.attn1(x, x, x)) + x = self.ln2(rep_enc + self.attn2(key=x, value=x, query=rep_enc)) + x = self.ln3(x + self.mlp(x)) + return x + + +class Encoder(nn.Module): + + def __init__(self, state_dim, obs_dim, n_block, n_embd, n_head, n_agent, encode_state): + super(Encoder, self).__init__() + + self.state_dim = state_dim + self.obs_dim = obs_dim + self.n_embd = n_embd + self.n_agent = n_agent + self.encode_state = encode_state + # self.agent_id_emb = nn.Parameter(torch.zeros(1, n_agent, n_embd)) + + self.state_encoder = nn.Sequential(nn.LayerNorm(state_dim), + init_(nn.Linear(state_dim, n_embd), activate=True), nn.GELU()) + self.obs_encoder = nn.Sequential(nn.LayerNorm(obs_dim), + init_(nn.Linear(obs_dim, n_embd), activate=True), nn.GELU()) + + self.ln = nn.LayerNorm(n_embd) + self.blocks = nn.Sequential(*[EncodeBlock(n_embd, n_head, n_agent) for _ in range(n_block)]) + self.head = nn.Sequential(init_(nn.Linear(n_embd, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, 1))) + + def forward(self, state, obs): + # state: (batch, n_agent, state_dim) + # obs: (batch, n_agent, obs_dim) + if self.encode_state: + state_embeddings = self.state_encoder(state) + x = state_embeddings + else: + obs_embeddings = self.obs_encoder(obs) + x = obs_embeddings + + rep = self.blocks(self.ln(x)) + v_loc = self.head(rep) + + return v_loc, rep + + +class Decoder(nn.Module): + + def __init__(self, obs_dim, action_dim, n_block, n_embd, n_head, n_agent, + action_type='Discrete', dec_actor=False, share_actor=False): + super(Decoder, self).__init__() + + self.action_dim = action_dim + self.n_embd = n_embd + self.dec_actor = dec_actor + self.share_actor = share_actor + self.action_type = action_type + + if action_type != 'Discrete': + log_std = torch.ones(action_dim) + # log_std = torch.zeros(action_dim) + self.log_std = torch.nn.Parameter(log_std) + # self.log_std = torch.nn.Parameter(torch.zeros(action_dim)) + + if self.dec_actor: + if self.share_actor: + print("mac_dec!!!!!") + self.mlp = nn.Sequential(nn.LayerNorm(obs_dim), + init_(nn.Linear(obs_dim, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, action_dim))) + else: + self.mlp = nn.ModuleList() + for n in range(n_agent): + actor = nn.Sequential(nn.LayerNorm(obs_dim), + init_(nn.Linear(obs_dim, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, action_dim))) + self.mlp.append(actor) + else: + # self.agent_id_emb = nn.Parameter(torch.zeros(1, n_agent, n_embd)) + if action_type == 'Discrete': + self.action_encoder = nn.Sequential(init_(nn.Linear(action_dim + 1, n_embd, bias=False), activate=True), + nn.GELU()) + else: + self.action_encoder = nn.Sequential(init_(nn.Linear(action_dim, n_embd), activate=True), nn.GELU()) + self.obs_encoder = nn.Sequential(nn.LayerNorm(obs_dim), + init_(nn.Linear(obs_dim, n_embd), activate=True), nn.GELU()) + self.ln = nn.LayerNorm(n_embd) + self.blocks = nn.Sequential(*[DecodeBlock(n_embd, n_head, n_agent) for _ in range(n_block)]) + self.head = nn.Sequential(init_(nn.Linear(n_embd, n_embd), activate=True), nn.GELU(), nn.LayerNorm(n_embd), + init_(nn.Linear(n_embd, action_dim))) + + def zero_std(self, device): + if self.action_type != 'Discrete': + log_std = torch.zeros(self.action_dim).to(device) + self.log_std.data = log_std + + # state, action, and return + def forward(self, action, obs_rep, obs): + # action: (batch, n_agent, action_dim), one-hot/logits? + # obs_rep: (batch, n_agent, n_embd) + if self.dec_actor: + if self.share_actor: + logit = self.mlp(obs) + else: + logit = [] + for n in range(len(self.mlp)): + logit_n = self.mlp[n](obs[:, n, :]) + logit.append(logit_n) + logit = torch.stack(logit, dim=1) + else: + action_embeddings = self.action_encoder(action) + x = self.ln(action_embeddings) + for block in self.blocks: + x = block(x, obs_rep) + logit = self.head(x) + + return logit + + +class MultiAgentTransformer(nn.Module): + + def __init__( + self, + state_dim: int, # actually unused + obs_dim: int, + action_dim: int, + n_agent: int, + n_block: int, + n_embd: int, + n_head: int, + encode_state: bool = False, + action_type: str = 'Discrete', + dec_actor: bool = False, + share_actor: bool = False + ): + super().__init__() + self.n_agent = n_agent + self.action_dim = action_dim + self.tpdv = dict(dtype=torch.float32) + self.action_type = action_type + + # state unused + state_dim = 37 + + self.encoder = Encoder(state_dim, obs_dim, n_block, n_embd, n_head, n_agent, encode_state) + self.decoder = Decoder(obs_dim, action_dim, n_block, n_embd, n_head, n_agent, + self.action_type, dec_actor=dec_actor, share_actor=share_actor) + + def zero_std(self): + if self.action_type != 'Discrete': + self.decoder.zero_std(self.device) + + def forward(self, state, obs, action, available_actions=None): + # state: (batch, n_agent, state_dim) + # obs: (batch, n_agent, obs_dim) + # action: (batch, n_agent, 1) + # available_actions: (batch, n_agent, act_dim) + + # state unused + ori_shape = np.shape(state) + state = np.zeros((*ori_shape[:-1], 37), dtype=np.float32) + + state = utils.from_numpy(state) + obs = utils.from_numpy(obs) + action = utils.from_numpy(action) + + if available_actions is not None: + available_actions = utils.from_numpy(available_actions) + + batch_size = np.shape(state)[0] + v_loc, obs_rep = self.encoder(state, obs) + if self.action_type == 'Discrete': + action = action.long() + action_log, entropy = self.discrete_parallel_act(self.decoder, obs_rep, obs, action, batch_size, + self.n_agent, self.action_dim, available_actions) + else: + action_log, entropy = self.continuous_parallel_act(self.decoder, obs_rep, obs, action, batch_size, + self.n_agent, self.action_dim) + return action_log, v_loc, entropy + + def get_actions(self, state, obs, available_actions=None, deterministic=False): + # state unused + ori_shape = np.shape(obs) + state = np.zeros((*ori_shape[:-1], 37), dtype=np.float32) + + state = utils.from_numpy(state) + obs = utils.from_numpy(obs) + if available_actions is not None: + available_actions = utils.from_numpy(available_actions) + + batch_size = np.shape(obs)[0] + v_loc, obs_rep = self.encoder(state, obs) + if self.action_type == "Discrete": + output_action, output_action_log = self.discrete_autoregreesive_act(self.decoder, obs_rep, obs, batch_size, + self.n_agent, self.action_dim, available_actions, deterministic) + else: + output_action, output_action_log = self.continuous_autoregreesive_act(self.decoder, obs_rep, obs, batch_size, + self.n_agent, self.action_dim, deterministic) + + return output_action, output_action_log, v_loc + + def get_values(self, state, obs, available_actions=None): + # state unused + ori_shape = np.shape(state) + state = np.zeros((*ori_shape[:-1], 37), dtype=np.float32) + + state = utils.from_numpy(state) + obs = utils.from_numpy(obs) + v_tot, obs_rep = self.encoder(state, obs) + return v_tot + + def discrete_autoregreesive_act(self, decoder, obs_rep, obs, batch_size, n_agent, action_dim, + available_actions=None, deterministic=False): + shifted_action = utils.zeros((batch_size, n_agent, action_dim + 1)) + shifted_action[:, 0, 0] = 1 + output_action = utils.zeros((batch_size, n_agent, 1), dtype=torch.long) + output_action_log = utils.zeros_like(output_action, dtype=torch.float32) + used_action_mask = utils.zeros_like(available_actions) + + for i in range(n_agent): + logit = decoder(shifted_action, obs_rep, obs)[:, i, :] + if available_actions is not None: + logit[available_actions[:, i, :] == 0] = -1e10 + logit[used_action_mask[:, i, :] == 1] = -1e10 + + distri = Categorical(logits=logit) + action = distri.probs.argmax(dim=-1) if deterministic else distri.sample() + action_log = distri.log_prob(action) + for j, a in enumerate(action): + if a < self.action_dim - 1: + used_action_mask[j, i:, a] = 1 + + output_action[:, i, :] = action.unsqueeze(-1) + output_action_log[:, i, :] = action_log.unsqueeze(-1) + if i + 1 < n_agent: + shifted_action[:, i + 1, 1:] = F.one_hot(action, num_classes=action_dim) + return output_action, output_action_log + + def discrete_parallel_act(self, decoder, obs_rep, obs, action, batch_size, n_agent, action_dim, + available_actions=None): + one_hot_action = F.one_hot(action.squeeze(-1), num_classes=action_dim) # (batch, n_agent, action_dim) + shifted_action = utils.zeros((batch_size, n_agent, action_dim + 1)) + shifted_action[:, 0, 0] = 1 + shifted_action[:, 1:, 1:] = one_hot_action[:, :-1, :] + logit = decoder(shifted_action, obs_rep, obs) + if available_actions is not None: + logit[available_actions == 0] = -1e10 + + distri = Categorical(logits=logit) + action_log = distri.log_prob(action.squeeze(-1)).unsqueeze(-1) + entropy = distri.entropy().unsqueeze(-1) + return action_log, entropy + + def continuous_autoregreesive_act(self, decoder, obs_rep, obs, batch_size, n_agent, action_dim, + deterministic=False): + shifted_action = utils.zeros((batch_size, n_agent, action_dim)) + output_action = utils.zeros((batch_size, n_agent, action_dim), dtype=torch.float32) + output_action_log = utils.zeros_like(output_action, dtype=torch.float32) + + for i in range(n_agent): + act_mean = decoder(shifted_action, obs_rep, obs)[:, i, :] + action_std = torch.sigmoid(decoder.log_std) * 0.5 + + # log_std = torch.zeros_like(act_mean).to(**tpdv) + decoder.log_std + # distri = Normal(act_mean, log_std.exp()) + distri = Normal(act_mean, action_std) + action = act_mean if deterministic else distri.sample() + action_log = distri.log_prob(action) + + output_action[:, i, :] = action + output_action_log[:, i, :] = action_log + if i + 1 < n_agent: + shifted_action[:, i + 1, :] = action + + # print("act_mean: ", act_mean) + # print("action: ", action) + + return output_action, output_action_log + + def continuous_parallel_act(self, decoder, obs_rep, obs, action, batch_size, n_agent, action_dim): + shifted_action = utils.zeros((batch_size, n_agent, action_dim)) + shifted_action[:, 1:, :] = action[:, :-1, :] + + act_mean = decoder(shifted_action, obs_rep, obs) + action_std = torch.sigmoid(decoder.log_std) * 0.5 + distri = Normal(act_mean, action_std) + + # log_std = torch.zeros_like(act_mean).to(**tpdv) + decoder.log_std + # distri = Normal(act_mean, log_std.exp()) + + action_log = distri.log_prob(action) + entropy = distri.entropy() + return action_log, entropy + + +class ValueNorm(nn.Module): + """ Normalize a vector of observations - across the first norm_axes dimensions""" + + def __init__(self, input_shape, norm_axes=1, beta=0.99999, per_element_update=False, epsilon=1e-5): + super().__init__() + + self.input_shape = input_shape + self.norm_axes = norm_axes + self.epsilon = epsilon + self.beta = beta + self.per_element_update = per_element_update + self.tpdv = dict(dtype=torch.float32) + + self.running_mean = nn.Parameter(torch.zeros(input_shape), requires_grad=False).to(**self.tpdv) + self.running_mean_sq = nn.Parameter(torch.zeros(input_shape), requires_grad=False).to(**self.tpdv) + self.debiasing_term = nn.Parameter(torch.tensor(0.0), requires_grad=False).to(**self.tpdv) + + self.reset_parameters() + + def reset_parameters(self): + self.running_mean.zero_() + self.running_mean_sq.zero_() + self.debiasing_term.zero_() + + def running_mean_var(self): + debiased_mean = self.running_mean / self.debiasing_term.clamp(min=self.epsilon) + debiased_mean_sq = self.running_mean_sq / self.debiasing_term.clamp(min=self.epsilon) + debiased_var = (debiased_mean_sq - debiased_mean ** 2).clamp(min=1e-2) + return debiased_mean, debiased_var + + @torch.no_grad() + def update(self, input_vector): + if type(input_vector) == np.ndarray: + input_vector = torch.from_numpy(input_vector) + input_vector = input_vector.to(**self.tpdv) + + batch_mean = input_vector.mean(dim=tuple(range(self.norm_axes))) + batch_sq_mean = (input_vector ** 2).mean(dim=tuple(range(self.norm_axes))) + + if self.per_element_update: + batch_size = np.prod(input_vector.size()[:self.norm_axes]) + weight = self.beta ** batch_size + else: + weight = self.beta + + self.running_mean.mul_(weight).add_(batch_mean * (1.0 - weight)) + self.running_mean_sq.mul_(weight).add_(batch_sq_mean * (1.0 - weight)) + self.debiasing_term.mul_(weight).add_(1.0 * (1.0 - weight)) + + def normalize(self, input_vector): + # Make sure input is float32 + if type(input_vector) == np.ndarray: + input_vector = torch.from_numpy(input_vector) + input_vector = input_vector.to(**self.tpdv) + + mean, var = self.running_mean_var() + out = (input_vector - mean[(None,) * self.norm_axes]) / torch.sqrt(var)[(None,) * self.norm_axes] + + return out + + def denormalize(self, input_vector): + """ Transform normalized data back into original distribution """ + if type(input_vector) == np.ndarray: + input_vector = utils.from_numpy(input_vector) + input_vector = input_vector.to(**self.tpdv) + + mean, var = self.running_mean_var() + out = input_vector * torch.sqrt(var)[(None,) * self.norm_axes] + mean[(None,) * self.norm_axes] + + out = out.cpu().numpy() + + return out diff --git a/StreamLearn/Simulator/policy/policy.py b/StreamLearn/Simulator/policy/policy.py new file mode 100644 index 0000000..f5d390d --- /dev/null +++ b/StreamLearn/Simulator/policy/policy.py @@ -0,0 +1,274 @@ +from abc import ABC +from typing import List +from collections import OrderedDict +import numpy as np +import torch +import torch.nn.functional as F + +from StreamLearn.Simulator.env.StreamEnv import Env +from StreamLearn.Simulator.policy import networks +from StreamLearn.Simulator.sim_utils import utils + + +class BasePolicy(ABC): + + def act(self, obs): + """ + Choose an action based on observation. + The action should be a list of integers, each integer represents the action of a computing node. + + :param obs: observation. + """ + raise NotImplementedError("The act() method of BasePolicy must be implemented.") + + +class EmptyPolicy(BasePolicy): + ''' + An empty policy for testing + Do nothing, return an empty action + ''' + + def act(self, obs): + nodes = obs['nodes'] + num_nodes = len(nodes) + return np.array([-1] * num_nodes, dtype=np.int32) + + +class UniformPolicy(BasePolicy): + ''' + An uniform policy for testing + Put the tasks to nodes sequentially, or do nothing if all nodes are busy + ''' + + def act(self, obs): + nodes = obs['nodes'] + num_nodes = len(nodes) + act = np.array([-1] * num_nodes, dtype=np.int32) + task_ids = obs['task_ids'] + for i, task_id in enumerate(task_ids): + if i < num_nodes: + act[i] = task_id + else: + break + return act + + +class EarliestDeadlinePolicy(BasePolicy): + ''' + An earliest deadline policy for testing + Put the task to the node with the earliest deadline + ''' + + def act(self, obs): + nodes = obs['nodes'] + num_nodes = len(nodes) + act = np.array([-1] * num_nodes, dtype=np.int32) + task_ids = obs['task_ids'] + # Sort tasks by their end time + sorted_tasks = sorted(task_ids, key=lambda tid: obs['tasks'][tid].end_time) + for i, task_id in enumerate(sorted_tasks): + if i < num_nodes: + act[i] = task_id + else: + break + return act + + +class RLPolicy(BasePolicy): + ''' + Base reinforcement learning policy + ''' + + def act(self, obs, *args, **kwargs): + raise NotImplementedError("The act() method of RLPolicy must be implemented.") + + def update(self, *args, **kwargs): + """ + Update the policy based on the given arguments. + This method should be implemented in the derived class. + """ + raise NotImplementedError("The update() method of RLPolicy must be implemented.") + + def save(self, path: str, *args, **kwargs): + raise NotImplementedError("The save() method of RLPolicy must be implemented.") + + def load(self, path: str, *args, **kwargs): + raise NotImplementedError("The load() method of RLPolicy must be implemented.") + + +class SACPolicy(RLPolicy): + ''' + Soft Actor-Critic policy + ''' + + def __init__( + self, + env: Env, + obs_dim: int, + action_dim: int, + hidden_dim: int = 256, + q_lr: float = 3e-4, + policy_lr: float = 3e-4, + gamma: float = 0.99, + tau: float = 0.005, + debug: bool = False, + **kwargs + ): + super().__init__() + self.env = env + self.qf1 = networks.SoftQ(obs_dim + env.num_nodes, 1, hidden_dim) + self.qf2 = networks.SoftQ(obs_dim + env.num_nodes, 1, hidden_dim) + self.target_qf1 = networks.SoftQ(obs_dim + env.num_nodes, 1, hidden_dim) + self.target_qf2 = networks.SoftQ(obs_dim + env.num_nodes, 1, hidden_dim) + self.target_qf1.load_state_dict(self.qf1.state_dict()) + self.target_qf2.load_state_dict(self.qf2.state_dict()) + self.q_optimizer = torch.optim.Adam(list(self.qf1.parameters()) + list(self.qf2.parameters()), lr=q_lr, eps=1e-4) + self.policy = networks.NCatActor(obs_dim, action_dim, hidden_dim) + self.policy_optimizer = torch.optim.Adam(self.policy.parameters(), lr=policy_lr, eps=1e-4) + self.log_alpha = utils.tensor(0.0, requires_grad=True) + self.alpha_optimizer = torch.optim.Adam([self.log_alpha], lr=policy_lr, eps=1e-4) + self.target_entropy = -torch.log(1 / torch.tensor(action_dim)) + + self.gamma = gamma + self.tau = tau + self.debug = debug + + self.statistics = OrderedDict() + + def act(self, obs, deterministic: bool = False, return_original_action: bool = False): + nodes = obs['nodes'] + tasks = obs['tasks'] + num_nodes = len(nodes) + num_task = len(tasks) + task_ids = obs['task_ids'] + # pred_rsc = obs['task_stats'][1] + # t = obs['t'] + # pred_time = [pred_rsc[i] / tasks[task_ids[i]].N for i in range(len(task_ids))] + # task_available_time = [tasks[tid].end_time - t for tid in task_ids] + + # full_obs = np.concatenate([ + # np.array([num_nodes, num_task], dtype=np.float32), + # pred_time, + # task_available_time, + # ]) + obs = self.env.get_rl_obs(obs) + obs = utils.from_numpy(obs).unsqueeze(0) + action, _, _ = self.policy.get_action(obs, deterministic=deterministic, sample_num=num_nodes) + action = utils.get_numpy(action.squeeze(0)) + + # Convert action to task IDs + act = np.array([-1] * num_nodes, dtype=np.int32) + for i, act_id in enumerate(action): + if act_id < num_task: + act[i] = task_ids[act_id] + else: + act[i] = -1 + if return_original_action: + return act, action + else: + return act + + def update(self, batch): + obs, actions, rewards, next_obs, dones = batch + + # Convert to tensors + obs = utils.from_numpy(obs) + actions = utils.from_numpy(actions) + rewards = utils.from_numpy(rewards) + next_obs = utils.from_numpy(next_obs) + dones = utils.from_numpy(dones) + + # Compute Q-values + q1_values = self.qf1(obs, actions) + q2_values = self.qf2(obs, actions) + + # Compute target Q-values + with torch.no_grad(): + next_actions, next_log_probs, _ = self.policy.get_action(next_obs, deterministic=False, sample_num=self.env.num_nodes) + target_q1_values = self.target_qf1(next_obs, next_actions) + target_q2_values = self.target_qf2(next_obs, next_actions) + target_q_values = torch.min(target_q1_values, target_q2_values) - self.log_alpha.exp() * next_log_probs + expected_q_values = rewards + (1 - dones) * self.gamma * target_q_values + + # Update Q-functions + q1_loss = F.mse_loss(q1_values, expected_q_values) + q2_loss = F.mse_loss(q2_values, expected_q_values) + q_loss = q1_loss + q2_loss + self.q_optimizer.zero_grad() + q_loss.backward() + self.q_optimizer.step() + + # Update policy + with torch.no_grad(): + next_actions, next_log_probs, _ = self.policy.get_action(obs, deterministic=False, sample_num=self.env.num_nodes) + q1_values = self.qf1(obs, next_actions) + q2_values = self.qf2(obs, next_actions) + min_q_values = torch.min(q1_values, q2_values) + policy_loss = (self.log_alpha.exp() * next_log_probs - min_q_values).mean() + self.policy_optimizer.zero_grad() + policy_loss.backward() + self.policy_optimizer.step() + + # Update alpha + alpha_loss = -(self.log_alpha.exp() * (next_log_probs + self.target_entropy)).mean() + self.alpha_optimizer.zero_grad() + alpha_loss.backward() + self.alpha_optimizer.step() + + # Update target networks + for target_param, param in zip(self.target_qf1.parameters(), self.qf1.parameters()): + target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data) + + for target_param, param in zip(self.target_qf2.parameters(), self.qf2.parameters()): + target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data) + + # Update statistics + self.statistics['q1_loss'] = q1_loss.item() + self.statistics['q2_loss'] = q2_loss.item() + self.statistics['policy_loss'] = policy_loss.item() + self.statistics['alpha_loss'] = alpha_loss.item() + self.statistics['alpha'] = self.log_alpha.exp().item() + self.statistics['q1_values'] = q1_values.mean().item() + self.statistics['q2_values'] = q2_values.mean().item() + + def to(self, device: torch.device): + """ + Move the policy's components to the specified device. + """ + self.qf1.to(device) + self.qf2.to(device) + self.target_qf1.to(device) + self.target_qf2.to(device) + self.policy.to(device) + self.log_alpha = self.log_alpha.to(device) + + def save(self, path, timestep: int): + """ + Save the policy to the given path. + """ + torch.save({ + 'qf1_state_dict': self.qf1.state_dict(), + 'qf2_state_dict': self.qf2.state_dict(), + 'target_qf1_state_dict': self.target_qf1.state_dict(), + 'target_qf2_state_dict': self.target_qf2.state_dict(), + 'policy_state_dict': self.policy.state_dict(), + 'log_alpha': self.log_alpha, + 'optimizer_state_dict': self.q_optimizer.state_dict(), + 'policy_optimizer_state_dict': self.policy_optimizer.state_dict(), + 'alpha_optimizer_state_dict': self.alpha_optimizer.state_dict(), + }, f"{path}/sac_policy_{timestep}.pth") + + def load(self, path): + """ + Load the policy from the given path. + """ + checkpoint = torch.load(path) + self.qf1.load_state_dict(checkpoint['qf1_state_dict']) + self.qf2.load_state_dict(checkpoint['qf2_state_dict']) + self.target_qf1.load_state_dict(checkpoint['target_qf1_state_dict']) + self.target_qf2.load_state_dict(checkpoint['target_qf2_state_dict']) + self.policy.load_state_dict(checkpoint['policy_state_dict']) + self.log_alpha = checkpoint['log_alpha'] + self.q_optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + self.policy_optimizer.load_state_dict(checkpoint['policy_optimizer_state_dict']) + self.alpha_optimizer.load_state_dict(checkpoint['alpha_optimizer_state_dict']) diff --git a/StreamLearn/Simulator/sim_utils/predictor.py b/StreamLearn/Simulator/sim_utils/predictor.py new file mode 100644 index 0000000..3f30526 --- /dev/null +++ b/StreamLearn/Simulator/sim_utils/predictor.py @@ -0,0 +1,58 @@ +from typing import Tuple +import numpy as np + +from StreamLearn.Simulator.task.task import Task + + +class Predictor: + """ + Base class for predictors. + """ + + def predict(self, task: Task) -> Tuple[float, float]: + """ + Predict the resource and loss based on the task. + """ + raise NotImplementedError("This method should be overridden by subclasses.") + + +class Online_WLS(Predictor): + """ + Online Weighted Least Squares (WLS) algorithm for predicting + the resource to achieve the target th reshold. + """ + + def __init__(self, gamma: float = 0.9): + self.gamma = gamma + self.eps = 1e-7 # Small value to avoid division by zero + + def predict(self, task: Task) -> Tuple[float, float]: + if len(task.losses) == 0 or len(task.resources) == 0: + return -1, 0 + + threshold = task.epsilon * 1.005 + loss = task.losses[-1] + resource = task.resources[-1] + lns = np.log(resource) + Xk = np.array([[lns], [1]]) + rk = np.log(loss) + + if task.first_time_prediction: + Vk = Xk @ Xk.T + np.eye(2) * 0.01 + task.Vk = np.linalg.inv(Vk) + task.theta = np.array([[0], [0]]) + task.first_time_prediction = False + # return -1, 0 + + theta = task.theta + task.Vk @ Xk * (rk - Xk.T @ task.theta) + Vk = 1 / self.gamma * (task.Vk - (task.Vk @ Xk @ Xk.T @ task.Vk) / (self.gamma + Xk.T @ task.Vk @ Xk)) + task.theta = theta + task.Vk = Vk + + a = np.exp(np.clip(task.theta[1][0], np.log(self.eps), -np.log(self.eps))) # Ensure a is not too small + b = -np.clip(task.theta[0][0], np.log(self.eps), -np.log(self.eps)) + pred_loss = a * resource ** (-b - self.eps) + pred_resource = (threshold / (a + self.eps)) ** (-1 / (b + self.eps)) + pred_resource = np.clip(pred_resource, 0, 1 / self.eps) + return pred_resource, pred_loss + \ No newline at end of file diff --git a/StreamLearn/Simulator/sim_utils/replay_buffer.py b/StreamLearn/Simulator/sim_utils/replay_buffer.py new file mode 100644 index 0000000..7e1716f --- /dev/null +++ b/StreamLearn/Simulator/sim_utils/replay_buffer.py @@ -0,0 +1,358 @@ +""" +A simple replay buffer implementation for storing and sampling transitions. +Modified from CleanRL: https://github.com/vwxyzjn/cleanrl +""" +from typing import Tuple +import numpy as np +import torch + +from StreamLearn.Simulator.env.StreamEnv import Env + + +class ReplayBuffer: + observations: np.ndarray + next_observations: np.ndarray + actions: np.ndarray + rewards: np.ndarray + dones: np.ndarray + timeouts: np.ndarray + + def __init__( + self, + observation_shape: Tuple[int, ...], + action_dim: int, + buffer_size: int, + debug: bool = False, + **kwargs: dict, + ): + self.buffer_size = buffer_size + self.observation_shape = observation_shape + self.action_dim = action_dim + self.pos = 0 + self.full = False + + self.observations = np.zeros((buffer_size, *observation_shape), dtype=np.float32) + self.next_observations = np.zeros((buffer_size, *observation_shape), dtype=np.float32) + self.actions = np.zeros((buffer_size, action_dim), dtype=np.float32) + self.rewards = np.zeros((buffer_size, 1), dtype=np.float32) + self.dones = np.zeros((buffer_size, 1), dtype=np.float32) + + self.debug = debug + + def add( + self, + obs: np.ndarray, + action: np.ndarray, + reward: np.ndarray, + next_obs: np.ndarray, + done: np.ndarray, + ) -> None: + # action = action.reshape(-1, self.action_dim) + # reward = reward.reshape(-1, 1) + # done = done.reshape(-1, 1) + self.observations[self.pos] = np.array(obs) + self.next_observations[self.pos] = np.array(next_obs) + self.actions[self.pos] = np.array(action) + self.rewards[self.pos] = np.array(reward) + self.dones[self.pos] = np.array(done) + self.pos += 1 + if self.pos >= self.buffer_size: + self.full = True + self.pos = 0 + + def sample(self, batch_size: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + upper_bound = self.buffer_size if self.full else self.pos + indices = np.random.randint(0, upper_bound, size=batch_size) + obs = self.observations[indices, :] + actions = self.actions[indices, :] + next_obs = self.next_observations[indices, :] + rewards = self.rewards[indices, :].reshape(-1, 1) + dones = self.dones[indices, :].reshape(-1, 1) + return obs, actions, rewards, next_obs, dones + + def __len__(self) -> int: + return self.buffer_size if self.full else self.pos + + +def _shuffle_agent_grid(x, y): + rows = np.indices((x, y))[0] + # cols = np.stack([np.random.permutation(y) for _ in range(x)]) + cols = np.stack([np.arange(y) for _ in range(x)]) + return rows, cols + + +class SharedReplayBuffer(object): + """ + Buffer to store training data. + """ + + def __init__( + self, + env: Env, + episode_length: int = 100, + n_rollout_threads: int = 1, + action_type: str = 'Discrete', + hidden_size: int = 64, + recurrent_N: int = 1, + gamma: float = 0.99, + gae_lambda: float = 0.95, + use_gae: bool = True, + use_popart: bool = False, + use_valuenorm: bool = True, + use_proper_time_limits: bool = False, + + debug: bool = False, + **kwargs, + ): + self.env = env + self.episode_length = episode_length + self.n_rollout_threads = n_rollout_threads + self.hidden_size = hidden_size + self.recurrent_N = recurrent_N + self.gamma = gamma + self.gae_lambda = gae_lambda + self._use_gae = use_gae + self._use_popart = use_popart + self._use_valuenorm = use_valuenorm + self._use_proper_time_limits = use_proper_time_limits + + num_agents = env.num_nodes + share_obs_shape = [env.obs_dim] + obs_shape = [env.obs_dim] + act_num = env.act_dim + + self.share_obs = np.zeros((self.episode_length + 1, self.n_rollout_threads, num_agents, *share_obs_shape), + dtype=np.float32) + self.obs = np.zeros((self.episode_length + 1, self.n_rollout_threads, num_agents, *obs_shape), dtype=np.float32) + + self.rnn_states = np.zeros( + (self.episode_length + 1, self.n_rollout_threads, num_agents, self.recurrent_N, self.hidden_size), + dtype=np.float32) + self.rnn_states_critic = np.zeros_like(self.rnn_states) + + self.value_preds = np.zeros( + (self.episode_length + 1, self.n_rollout_threads, num_agents, 1), dtype=np.float32) + self.returns = np.zeros_like(self.value_preds) + self.advantages = np.zeros( + (self.episode_length, self.n_rollout_threads, num_agents, 1), dtype=np.float32) + + if action_type == 'Discrete': + self.available_actions = np.ones((self.episode_length + 1, self.n_rollout_threads, num_agents, act_num), + dtype=np.float32) + act_shape = 1 + else: + self.available_actions = None + act_shape = env.act_dim + + # act_shape = get_shape_from_act_space(act_space) + + self.actions = np.zeros( + (self.episode_length, self.n_rollout_threads, num_agents, act_shape), dtype=np.float32) + self.action_log_probs = np.zeros( + (self.episode_length, self.n_rollout_threads, num_agents, act_shape), dtype=np.float32) + self.rewards = np.zeros( + (self.episode_length, self.n_rollout_threads, num_agents, 1), dtype=np.float32) + + self.masks = np.ones((self.episode_length + 1, self.n_rollout_threads, num_agents, 1), dtype=np.float32) + self.bad_masks = np.ones_like(self.masks) + self.active_masks = np.ones_like(self.masks) + + self.step = 0 + + def insert(self, share_obs, obs, rnn_states_actor, rnn_states_critic, actions, action_log_probs, + value_preds, rewards, masks, bad_masks=None, active_masks=None, available_actions=None): + """ + Insert data into the buffer. + :param share_obs: (argparse.Namespace) arguments containing relevant model, policy, and env information. + :param obs: (np.ndarray) local agent observations. + :param rnn_states_actor: (np.ndarray) RNN states for actor network. + :param rnn_states_critic: (np.ndarray) RNN states for critic network. + :param actions:(np.ndarray) actions taken by agents. + :param action_log_probs:(np.ndarray) log probs of actions taken by agents + :param value_preds: (np.ndarray) value function prediction at each step. + :param rewards: (np.ndarray) reward collected at each step. + :param masks: (np.ndarray) denotes whether the environment has terminated or not. + :param bad_masks: (np.ndarray) action space for agents. + :param active_masks: (np.ndarray) denotes whether an agent is active or dead in the env. + :param available_actions: (np.ndarray) actions available to each agent. If None, all actions are available. + """ + self.share_obs[self.step + 1] = share_obs.copy() + self.obs[self.step + 1] = obs.copy() + self.rnn_states[self.step + 1] = rnn_states_actor.copy() + self.rnn_states_critic[self.step + 1] = rnn_states_critic.copy() + self.actions[self.step] = actions.copy() + self.action_log_probs[self.step] = action_log_probs.copy() + self.value_preds[self.step] = value_preds.copy() + self.rewards[self.step] = rewards.copy() + self.masks[self.step + 1] = masks.copy() + if bad_masks is not None: + self.bad_masks[self.step + 1] = bad_masks.copy() + if active_masks is not None: + self.active_masks[self.step + 1] = active_masks.copy() + if available_actions is not None: + self.available_actions[self.step + 1] = available_actions.copy() + + self.step = (self.step + 1) % self.episode_length + + def chooseinsert(self, share_obs, obs, rnn_states, rnn_states_critic, actions, action_log_probs, + value_preds, rewards, masks, bad_masks=None, active_masks=None, available_actions=None): + """ + Insert data into the buffer. This insert function is used specifically for Hanabi, which is turn based. + :param share_obs: (argparse.Namespace) arguments containing relevant model, policy, and env information. + :param obs: (np.ndarray) local agent observations. + :param rnn_states_actor: (np.ndarray) RNN states for actor network. + :param rnn_states_critic: (np.ndarray) RNN states for critic network. + :param actions:(np.ndarray) actions taken by agents. + :param action_log_probs:(np.ndarray) log probs of actions taken by agents + :param value_preds: (np.ndarray) value function prediction at each step. + :param rewards: (np.ndarray) reward collected at each step. + :param masks: (np.ndarray) denotes whether the environment has terminated or not. + :param bad_masks: (np.ndarray) denotes indicate whether whether true terminal state or due to episode limit + :param active_masks: (np.ndarray) denotes whether an agent is active or dead in the env. + :param available_actions: (np.ndarray) actions available to each agent. If None, all actions are available. + """ + self.share_obs[self.step] = share_obs.copy() + self.obs[self.step] = obs.copy() + self.rnn_states[self.step + 1] = rnn_states.copy() + self.rnn_states_critic[self.step + 1] = rnn_states_critic.copy() + self.actions[self.step] = actions.copy() + self.action_log_probs[self.step] = action_log_probs.copy() + self.value_preds[self.step] = value_preds.copy() + self.rewards[self.step] = rewards.copy() + self.masks[self.step + 1] = masks.copy() + if bad_masks is not None: + self.bad_masks[self.step + 1] = bad_masks.copy() + if active_masks is not None: + self.active_masks[self.step] = active_masks.copy() + if available_actions is not None: + self.available_actions[self.step] = available_actions.copy() + + self.step = (self.step + 1) % self.episode_length + + def after_update(self): + """Copy last timestep data to first index. Called after update to model.""" + self.share_obs[0] = self.share_obs[-1].copy() + self.obs[0] = self.obs[-1].copy() + self.rnn_states[0] = self.rnn_states[-1].copy() + self.rnn_states_critic[0] = self.rnn_states_critic[-1].copy() + self.masks[0] = self.masks[-1].copy() + self.bad_masks[0] = self.bad_masks[-1].copy() + self.active_masks[0] = self.active_masks[-1].copy() + if self.available_actions is not None: + self.available_actions[0] = self.available_actions[-1].copy() + + def chooseafter_update(self): + """Copy last timestep data to first index. This method is used for Hanabi.""" + self.rnn_states[0] = self.rnn_states[-1].copy() + self.rnn_states_critic[0] = self.rnn_states_critic[-1].copy() + self.masks[0] = self.masks[-1].copy() + self.bad_masks[0] = self.bad_masks[-1].copy() + + def compute_returns(self, next_value, value_normalizer=None): + """ + Compute returns either as discounted sum of rewards, or using GAE. + :param next_value: (np.ndarray) value predictions for the step after the last episode step. + :param value_normalizer: (PopArt) If not None, PopArt value normalizer instance. + """ + self.value_preds[-1] = next_value + gae = 0 + for step in reversed(range(self.rewards.shape[0])): + if self._use_popart or self._use_valuenorm: + delta = self.rewards[step] + self.gamma * value_normalizer.denormalize( + self.value_preds[step + 1]) * self.masks[step + 1] \ + - value_normalizer.denormalize(self.value_preds[step]) + gae = delta + self.gamma * self.gae_lambda * self.masks[step + 1] * gae + + # here is a patch for mpe, whose last step is timeout instead of terminate + # if self.env_name == "MPE" and step == self.rewards.shape[0] - 1: + # gae = 0 + + self.advantages[step] = gae + self.returns[step] = gae + value_normalizer.denormalize(self.value_preds[step]) + else: + delta = self.rewards[step] + self.gamma * self.value_preds[step + 1] * \ + self.masks[step + 1] - self.value_preds[step] + gae = delta + self.gamma * self.gae_lambda * self.masks[step + 1] * gae + + # here is a patch for mpe, whose last step is timeout instead of terminate + if self.env_name == "MPE" and step == self.rewards.shape[0] - 1: + gae = 0 + + self.advantages[step] = gae + self.returns[step] = gae + self.value_preds[step] + + def feed_forward_generator_transformer(self, advantages, num_mini_batch=None, mini_batch_size=None): + """ + Yield training data for MLP policies. + :param advantages: (np.ndarray) advantage estimates. + :param num_mini_batch: (int) number of minibatches to split the batch into. + :param mini_batch_size: (int) number of samples in each minibatch. + """ + episode_length, n_rollout_threads, num_agents = self.rewards.shape[0:3] + batch_size = n_rollout_threads * episode_length + + if mini_batch_size is None: + assert batch_size >= num_mini_batch, ( + "PPO requires the number of processes ({}) " + "* number of steps ({}) = {} " + "to be greater than or equal to the number of PPO mini batches ({})." + "".format(n_rollout_threads, episode_length, + n_rollout_threads * episode_length, + num_mini_batch)) + mini_batch_size = batch_size // num_mini_batch + + rand = torch.randperm(batch_size).numpy() + sampler = [rand[i * mini_batch_size:(i + 1) * mini_batch_size] for i in range(num_mini_batch)] + rows, cols = _shuffle_agent_grid(batch_size, num_agents) + + # keep (num_agent, dim) + share_obs = self.share_obs[:-1].reshape(-1, *self.share_obs.shape[2:]) + share_obs = share_obs[rows, cols] + obs = self.obs[:-1].reshape(-1, *self.obs.shape[2:]) + obs = obs[rows, cols] + rnn_states = self.rnn_states[:-1].reshape(-1, *self.rnn_states.shape[2:]) + rnn_states = rnn_states[rows, cols] + rnn_states_critic = self.rnn_states_critic[:-1].reshape(-1, *self.rnn_states_critic.shape[2:]) + rnn_states_critic = rnn_states_critic[rows, cols] + actions = self.actions.reshape(-1, *self.actions.shape[2:]) + actions = actions[rows, cols] + if self.available_actions is not None: + available_actions = self.available_actions[:-1].reshape(-1, *self.available_actions.shape[2:]) + available_actions = available_actions[rows, cols] + value_preds = self.value_preds[:-1].reshape(-1, *self.value_preds.shape[2:]) + value_preds = value_preds[rows, cols] + returns = self.returns[:-1].reshape(-1, *self.returns.shape[2:]) + returns = returns[rows, cols] + masks = self.masks[:-1].reshape(-1, *self.masks.shape[2:]) + masks = masks[rows, cols] + active_masks = self.active_masks[:-1].reshape(-1, *self.active_masks.shape[2:]) + active_masks = active_masks[rows, cols] + action_log_probs = self.action_log_probs.reshape(-1, *self.action_log_probs.shape[2:]) + action_log_probs = action_log_probs[rows, cols] + advantages = advantages.reshape(-1, *advantages.shape[2:]) + advantages = advantages[rows, cols] + + for indices in sampler: + # [L,T,N,Dim]-->[L*T,N,Dim]-->[index,N,Dim]-->[index*N, Dim] + share_obs_batch = share_obs[indices].reshape(-1, *share_obs.shape[2:]) + obs_batch = obs[indices].reshape(-1, *obs.shape[2:]) + rnn_states_batch = rnn_states[indices].reshape(-1, *rnn_states.shape[2:]) + rnn_states_critic_batch = rnn_states_critic[indices].reshape(-1, *rnn_states_critic.shape[2:]) + actions_batch = actions[indices].reshape(-1, *actions.shape[2:]) + if self.available_actions is not None: + available_actions_batch = available_actions[indices].reshape(-1, *available_actions.shape[2:]) + else: + available_actions_batch = None + value_preds_batch = value_preds[indices].reshape(-1, *value_preds.shape[2:]) + return_batch = returns[indices].reshape(-1, *returns.shape[2:]) + masks_batch = masks[indices].reshape(-1, *masks.shape[2:]) + active_masks_batch = active_masks[indices].reshape(-1, *active_masks.shape[2:]) + old_action_log_probs_batch = action_log_probs[indices].reshape(-1, *action_log_probs.shape[2:]) + if advantages is None: + adv_targ = None + else: + adv_targ = advantages[indices].reshape(-1, *advantages.shape[2:]) + + yield share_obs_batch, obs_batch, rnn_states_batch, rnn_states_critic_batch, actions_batch, \ + value_preds_batch, return_batch, masks_batch, active_masks_batch, old_action_log_probs_batch, \ + adv_targ, available_actions_batch diff --git a/StreamLearn/Simulator/sim_utils/utils.py b/StreamLearn/Simulator/sim_utils/utils.py new file mode 100644 index 0000000..21e4692 --- /dev/null +++ b/StreamLearn/Simulator/sim_utils/utils.py @@ -0,0 +1,298 @@ +import importlib +import math +from typing import Any +import numpy as np +import random +import torch + +#---------------------------------------------------------------------------- +# Cached construction of constant tensors. Avoids CPU=>GPU copy when the +# same constant is used multiple times. + +_constant_cache = dict() + +def constant(value, shape=None, dtype=None, device=None, memory_format=None): + value = np.asarray(value) + if shape is not None: + shape = tuple(shape) + if dtype is None: + dtype = torch.get_default_dtype() + if device is None: + device = torch.device('cpu') + if memory_format is None: + memory_format = torch.contiguous_format + + key = (value.shape, value.dtype, value.tobytes(), shape, dtype, device, memory_format) + tensor = _constant_cache.get(key, None) + if tensor is None: + tensor = torch.as_tensor(value.copy(), dtype=dtype, device=device) + if shape is not None: + tensor, _ = torch.broadcast_tensors(tensor, torch.empty(shape)) + tensor = tensor.contiguous(memory_format=memory_format) + _constant_cache[key] = tensor + return tensor + + +class InfiniteSampler(torch.utils.data.Sampler): + def __init__(self, dataset, rank=0, num_replicas=1, shuffle=True, seed=0, window_size=0.5): + assert len(dataset) > 0 + assert num_replicas > 0 + assert 0 <= rank < num_replicas + assert 0 <= window_size <= 1 + super().__init__(dataset) + self.dataset = dataset + self.rank = rank + self.num_replicas = num_replicas + self.shuffle = shuffle + self.seed = seed + self.window_size = window_size + + def __iter__(self): + order = np.arange(len(self.dataset)) + rnd = None + window = 0 + if self.shuffle: + rnd = np.random.RandomState(self.seed) + rnd.shuffle(order) + window = int(np.rint(order.size * self.window_size)) + + idx = 0 + while True: + i = idx % order.size + if idx % self.num_replicas == self.rank: + yield order[i] + if window >= 2: + j = (i - rnd.randint(window)) % order.size + order[i], order[j] = order[j], order[i] + idx += 1 + + +def params_and_buffers(module): + assert isinstance(module, torch.nn.Module) + return list(module.parameters()) + list(module.buffers()) + +def named_params_and_buffers(module): + assert isinstance(module, torch.nn.Module) + return list(module.named_parameters()) + list(module.named_buffers()) + +@torch.inference_mode() +def copy_params_and_buffers(src_module, dst_module, require_all=False): + assert isinstance(src_module, torch.nn.Module) + assert isinstance(dst_module, torch.nn.Module) + src_tensors = dict(named_params_and_buffers(src_module)) + for name, tensor in named_params_and_buffers(dst_module): + assert (name in src_tensors) or (not require_all) + try: + if name in src_tensors: + tensor.copy_(src_tensors[name]) + except RuntimeError as err: + raise RuntimeError(f'Error copying "{name}" from {src_module} to {dst_module}') from err + + +class EasyDict(dict): + """Convenience class that behaves like a dict but allows access with the attribute syntax.""" + + def __getattr__(self, name: str) -> Any: + try: + return self[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name: str, value: Any) -> None: + self[name] = value + + def __delattr__(self, name: str) -> None: + del self[name] + + +def print_module_summary(module, inputs, max_nesting=3, skip_redundant=True): + assert isinstance(module, torch.nn.Module) + assert not isinstance(module, torch.jit.ScriptModule) + assert isinstance(inputs, (tuple, list)) + + # Register hooks. + entries = [] + nesting = [0] + def pre_hook(_mod, _inputs): + nesting[0] += 1 + def post_hook(mod, _inputs, outputs): + nesting[0] -= 1 + if nesting[0] <= max_nesting: + outputs = list(outputs) if isinstance(outputs, (tuple, list)) else [outputs] + outputs = [t for t in outputs if isinstance(t, torch.Tensor)] + entries.append(EasyDict(mod=mod, outputs=outputs)) + hooks = [mod.register_forward_pre_hook(pre_hook) for mod in module.modules()] + hooks += [mod.register_forward_hook(post_hook) for mod in module.modules()] + + # Run module. + outputs = module(*inputs) + for hook in hooks: + hook.remove() + + # Identify unique outputs, parameters, and buffers. + tensors_seen = set() + for e in entries: + e.unique_params = [t for t in e.mod.parameters() if id(t) not in tensors_seen] + e.unique_buffers = [t for t in e.mod.buffers() if id(t) not in tensors_seen] + e.unique_outputs = [t for t in e.outputs if id(t) not in tensors_seen] + tensors_seen |= {id(t) for t in e.unique_params + e.unique_buffers + e.unique_outputs} + + # Filter out redundant entries. + if skip_redundant: + entries = [e for e in entries if len(e.unique_params) or len(e.unique_buffers) or len(e.unique_outputs)] + + # Construct table. + rows = [[type(module).__name__, 'Parameters', 'Buffers', 'Output shape', 'Datatype']] + rows += [['---'] * len(rows[0])] + param_total = 0 + buffer_total = 0 + submodule_names = {mod: name for name, mod in module.named_modules()} + for e in entries: + name = '' if e.mod is module else submodule_names[e.mod] + param_size = sum(t.numel() for t in e.unique_params) + buffer_size = sum(t.numel() for t in e.unique_buffers) + output_shapes = [str(list(t.shape)) for t in e.outputs] + output_dtypes = [str(t.dtype).split('.')[-1] for t in e.outputs] + rows += [[ + name + (':0' if len(e.outputs) >= 2 else ''), + str(param_size) if param_size else '-', + str(buffer_size) if buffer_size else '-', + (output_shapes + ['-'])[0], + (output_dtypes + ['-'])[0], + ]] + for idx in range(1, len(e.outputs)): + rows += [[name + f':{idx}', '-', '-', output_shapes[idx], output_dtypes[idx]]] + param_total += param_size + buffer_total += buffer_size + rows += [['---'] * len(rows[0])] + rows += [['Total', str(param_total), str(buffer_total), '-', '-']] + + # Print table. + widths = [max(len(cell) for cell in column) for column in zip(*rows)] + print() + for row in rows: + print(' '.join(cell + ' ' * (width - len(cell)) for cell, width in zip(row, widths))) + print() + return outputs + + +def restrict_to_throughput(throughput, data): + if data is None: + return None + if data.shape[0] > throughput: + return data[:throughput] + return data + + +def get_class(class_path: str): + class_module, class_name = class_path.rsplit('.', 1) + module = importlib.import_module(class_module) + clss = getattr(module, class_name) + return clss + + +_use_gpu = False +device = None + + +def set_gpu_mode(mode, gpu_id=0): + global _use_gpu + global device + global _gpu_id + _gpu_id = gpu_id + _use_gpu = mode + device = torch.device("cuda:" + str(gpu_id) if _use_gpu else "cpu") + + +def gpu_enabled(): + return _use_gpu + + +def set_device(gpu_id): + torch.cuda.set_device(gpu_id) + + +def FloatTensor(*args, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.FloatTensor(*args, **kwargs, device=torch_device) + + +def from_numpy(*args, **kwargs): + return torch.from_numpy(*args, **kwargs).float().to(device) + + +def get_numpy(tensor): + return tensor.to('cpu').detach().numpy() + + +def randint(*sizes, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.randint(*sizes, **kwargs, device=torch_device) + + +def zeros(*sizes, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.zeros(*sizes, **kwargs, device=torch_device) + + +def ones(*sizes, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.ones(*sizes, **kwargs, device=torch_device) + + +def ones_like(*args, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.ones_like(*args, **kwargs, device=torch_device) + + +def randn(*args, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.randn(*args, **kwargs, device=torch_device) + + +def zeros_like(*args, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.zeros_like(*args, **kwargs, device=torch_device) + + +def tensor(*args, torch_device=None, **kwargs): + if torch_device is None: + torch_device = device + return torch.tensor(*args, **kwargs, device=torch_device) + + +def normal(*args, **kwargs): + return torch.normal(*args, **kwargs).to(device) + + +def set_seed(seed): + seed = int(seed) + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + +def get_gard_norm(it): + sum_grad = 0 + for x in it: + if x.grad is None: + continue + sum_grad += x.grad.norm() ** 2 + return math.sqrt(sum_grad) + + +def huber_loss(e, d): + a = (abs(e) <= d).float() + b = (e > d).float() + return a*e**2/2 + b*d*(abs(e)-d/2) + + +def mse_loss(e): + return e**2/2 \ No newline at end of file diff --git a/StreamLearn/Simulator/task/__init__.py b/StreamLearn/Simulator/task/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StreamLearn/Simulator/task/dataset.py b/StreamLearn/Simulator/task/dataset.py new file mode 100644 index 0000000..ea71945 --- /dev/null +++ b/StreamLearn/Simulator/task/dataset.py @@ -0,0 +1,209 @@ +import numpy as np +import torch +from torch.utils.data import Dataset, DataLoader +from torchvision import datasets, transforms +from torchaudio.datasets import YESNO +import torchaudio.transforms as T + +import torchtext; torchtext.disable_torchtext_deprecation_warning() +from torchtext.datasets import IMDB +from torchtext.data.utils import get_tokenizer +from torchtext.vocab import build_vocab_from_iterator +from torchtext.data.functional import to_map_style_dataset + + +def get_dataloader( + dataset_name: str, + root: str = './datasets', + batch_size: int = 64, + train: bool = True, + transform = None, + pin_memory: bool = True, + num_workers: int = 4, + **kwargs, +): + if dataset_name == 'mnist': + dataset_class = MnistDataset + elif dataset_name == 'cifar10': + dataset_class = Cifar10Dataset + elif dataset_name == 'cifar100': + dataset_class = Cifar100Dataset + elif dataset_name == 'rl': + dataset_class = RLDataset + elif dataset_name == 'imdb': + dataset_class = IMDBDataset + elif dataset_name == 'yesno': + dataset_class = YesNoDataset + else: + raise ValueError(f"Unknown dataset: {dataset_name}") + + if dataset_name == 'rl': + assert 'data_batch' in kwargs and 'label_batch' in kwargs, "data_batch and label_batch must be provided for RLDataset" + dataset = RLDataset( + train=train, + transform=transform, + **kwargs + ) + else: + dataset = dataset_class(train=train, root=root, transform=transform, **kwargs) + if dataset_name == 'yesno': + collate_fn = dataset.pad_seq + elif dataset_name == 'imdb': + collate_fn = dataset.collate_batch + else: + collate_fn = None + dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn, pin_memory=pin_memory, num_workers=num_workers) + return dataloader + + +class TransformedDataset(Dataset): + + def __init__(self, train=True, transform=None, root: str = './datasets', **kwargs): + self.data = None # Placeholder for data + self.transform = transform + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + return self.data[index] + + +class MnistDataset(TransformedDataset): + + def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): + if transform is None: + self.transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.5,), (0.5,)) + ]) + else: + self.transform = transform + self.data = datasets.MNIST(root=root, train=train, transform=self.transform, download=True) + + +class Cifar10Dataset(TransformedDataset): + + def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): + if transform is None: + self.transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) + ]) + else: + self.transform = transform + self.data = datasets.CIFAR10(root=root, train=train, transform=self.transform, download=True) + + +class Cifar100Dataset(TransformedDataset): + + def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): + if transform is None: + self.transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) + ]) + else: + self.transform = transform + self.data = datasets.CIFAR100(root=root, train=train, transform=self.transform, download=True) + + +class RLDataset(TransformedDataset): + + def __init__(self, data_batch, label_batch, train=True, transform=None, **kwargs): + self.train = train + if transform is None: + self.transform = transforms.Compose([ + transforms.Resize([196, 196]), + transforms.ToTensor(), + transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) + ]) + else: + self.transform = transform + + assert len(data_batch) == len(label_batch) + num_data = len(data_batch) + self.images = [] + self.labels = [] + index = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 11: 6, 12: 7} + for i in range(num_data): + self.images.append(data_batch[i]) + tmp = np.zeros(8) + tmp[index[label_batch[i]]] = 1 + self.labels.append(tmp) + + def __getitem__(self, item): + img = self.images[item] + label = self.labels[item] + if self.transform is not None: + img = self.transform(img) + label = torch.from_numpy(label).type(torch.LongTensor) + label = torch.argmax(label) # Convert one-hot to class index + return img, label + + def __len__(self): + return len(self.images) + + +class YesNoDataset(TransformedDataset): + + def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): + if transform is None: + self.transform = torch.nn.Sequential( + T.MelSpectrogram(sample_rate=8000, n_mels=64), + T.AmplitudeToDB() + ) + else: + self.transform = transform + self.data = YESNO(root=root, download=True) + + def pad_seq(self, batch): + max_len = max([x[0].shape[-1] for x in batch]) + batch_size = len(batch) + num_mels = batch[0][0].shape[1] + padded_batch = torch.zeros(batch_size, 1, num_mels, max_len) + labels = [] + for i, (x, y) in enumerate(batch): + padded_batch[i, :, :, :x.shape[-1]] = x + labels.append(y) + labels = torch.tensor(labels, dtype=torch.long) + return padded_batch, labels + + def __getitem__(self, index): + waveform, sample_rate, labels = self.data[index] + if self.transform is not None: + waveform = self.transform(waveform) + label = torch.tensor(labels[0], dtype=torch.long) + return waveform, label + + +class IMDBDataset(TransformedDataset): + + def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): + self.split = 'train' if train else 'test' + self.tokenizer = get_tokenizer('basic_english') + self.data = IMDB(root=root, split=self.split) + + self.vocab = build_vocab_from_iterator(self.yield_tokens(self.data), specials=["", ""]) + self.vocab.set_default_index(self.vocab[""]) + + self.data = to_map_style_dataset(self.data) + + def yield_tokens(self, data_iter): + for _, text in data_iter: + yield self.tokenizer(text) + + def _text_pipeline(self, x): + return self.vocab(self.tokenizer(x)) + + def _label_pipeline(self, y): + return 0 if y == 'neg' else 1 + + def collate_batch(self, batch): + labels, texts = [], [] + for label, text in batch: + labels.append(self._label_pipeline(label)) + texts.append(torch.tensor(self._text_pipeline(text), dtype=torch.int64)) + labels = torch.tensor(labels, dtype=torch.int64) + texts = torch.nn.utils.rnn.pad_sequence(texts, batch_first=True) + return texts, labels diff --git a/StreamLearn/Simulator/task/models.py b/StreamLearn/Simulator/task/models.py new file mode 100644 index 0000000..b316a56 --- /dev/null +++ b/StreamLearn/Simulator/task/models.py @@ -0,0 +1,250 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import torchvision + + +class SimpleCNN_Cifar10(nn.Module): + ''' + Simple 3-layer CNN for Cifar10 + ''' + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) + self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1) + self.conv3 = nn.Conv2d(128, 256, kernel_size=3, padding=1) + self.fc1 = nn.Linear(256*4*4, 512) + self.fc2 = nn.Linear(512, 10) + + self.pool = nn.MaxPool2d(2, 2) + self.relu = nn.ReLU() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.pool(self.relu(self.conv1(x))) + x = self.pool(self.relu(self.conv2(x))) + x = self.pool(self.relu(self.conv3(x))) + x = x.view(-1, 256*4*4) + x = self.relu(self.fc1(x)) + x = self.fc2(x) + return x + + +class SimpleCNN_MNIST(nn.Module): + ''' + Simple 2-layer CNN for MNIST + ''' + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) + self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1) + self.fc1 = nn.Linear(64*7*7, 128) + self.fc2 = nn.Linear(128, 10) + + self.pool = nn.MaxPool2d(2, 2) + self.relu = nn.ReLU() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.pool(self.relu(self.conv1(x))) + x = self.pool(self.relu(self.conv2(x))) + x = x.view(-1, 64*7*7) + x = self.relu(self.fc1(x)) + x = self.fc2(x) + return x + + +class ResNet18_Cifar10(nn.Module): + ''' + ResNet18 for Cifar10 + ''' + def __init__(self): + super().__init__() + self.resnet = torchvision.models.resnet18(num_classes=10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.resnet(x) + + +class ResNet34_Cifar10(nn.Module): + ''' + ResNet34 for Cifar10 + ''' + def __init__(self): + super().__init__() + self.resnet = torchvision.models.resnet34(num_classes=10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.resnet(x) + + +class ResNet18_MNIST(nn.Module): + ''' + ResNet18 for MNIST + ''' + def __init__(self): + super().__init__() + self.resnet = torchvision.models.resnet18() + self.resnet.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) + self.resnet.fc = nn.Linear(self.resnet.fc.in_features, 10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.resnet(x) + + +class ResNet34_MNIST(nn.Module): + ''' + ResNet18 for MNIST + ''' + def __init__(self): + super().__init__() + self.resnet = torchvision.models.resnet34() + self.resnet.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) + self.resnet.fc = nn.Linear(self.resnet.fc.in_features, 10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.resnet(x) + + +class LSTM_Cifar10(nn.Module): + ''' + This model is only for Cifar10 dataset, + so the sequence length is fixed to 32 + ''' + def __init__( + self, + input_size=3*32, + hidden_size=128, + num_layers=2, + lstm_dropout=0.2, + dropout=0.5, + num_classes=10, + ): + super().__init__() + self.input_size = input_size + self.lstm1 = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=lstm_dropout) + self.lstm2 = nn.LSTM(input_size=hidden_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=lstm_dropout) + self.fc1 = nn.Linear(hidden_size, 256) + self.fc2 = nn.Linear(256, 128) + self.fc3 = nn.Linear(128, num_classes) + + self.dropout = nn.Dropout(dropout) + self.relu = nn.ReLU() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.view(-1, 32, self.input_size) + x, _ = self.lstm1(x) + x = self.dropout(self.relu(x)) + x, _ = self.lstm2(x) + x = self.dropout(self.relu(x)) + x = self.fc1(x[:, -1, :]) + x = self.dropout(self.relu(x)) + x = self.fc2(x) + x = self.dropout(self.relu(x)) + x = self.fc3(x) + return x + + +class TransformerModel(nn.Module): + + def __init__(self, input_dim, num_heads, num_classes): + super().__init__() + self.encoder_layer = nn.TransformerEncoderLayer(d_model=input_dim, nhead=num_heads) + self.transformer_encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=1) + self.fc = nn.Linear(input_dim, num_classes) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.squeeze(1) + x = x.permute(2, 0, 1) + x = self.transformer_encoder(x) + x = x.mean(dim=0) + x = self.fc(x) + return x + + +class DAggerCNNNet(nn.Module): + + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(3, 6, (5, 5)) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, (5, 5)) + self.conv3 = nn.Conv2d(16, 32, (5, 5)) + self.conv4 = nn.Conv2d(32, 64, (4, 4)) + self.conv5 = nn.Conv2d(64, 128, (4, 4)) + self.fc1 = nn.Linear(128 * 3 * 3, 240) + self.fc2 = nn.Linear(240, 60) + self.fc3 = nn.Linear(60, 8) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = self.pool(F.relu(self.conv3(x))) + x = self.pool(F.relu(self.conv4(x))) + x = self.pool(F.relu(self.conv5(x))) + x = torch.flatten(x, 1) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + x = self.fc3(x) + return x + + +class SelfAttention(nn.Module): + + def __init__(self, embed_size, heads): + super().__init__() + self.embed_size = embed_size + self.heads = heads + self.head_dim = embed_size // heads + assert ( + self.head_dim * heads == embed_size + ), "Embedding size needs to be divisible by heads" + + self.values = nn.Linear(self.head_dim, self.head_dim, bias=False) + self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False) + self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False) + self.fc_out = nn.Linear(heads * self.head_dim, embed_size) + + def forward(self, v: torch.Tensor, k: torch.Tensor, q: torch.Tensor) -> torch.Tensor: + N = q.shape[0] + v_len, k_len, q_len = v.shape[1], k.shape[1], q.shape[1] + v = v.reshape(N, v_len, self.heads, self.head_dim) + k = k.reshape(N, k_len, self.heads, self.head_dim) + q = q.reshape(N, q_len, self.heads, self.head_dim) + + v = self.values(v) + k = self.keys(k) + q = self.queries(q) + + energy = torch.einsum("nqhd,nkhd->nhqk", [q, k]) + attention = torch.softmax(energy / (self.embed_size ** (1 / 2)), dim=3) + out = torch.einsum("nhql,nlhd->nqhd", [attention, v]).reshape( + N, q_len, self.heads * self.head_dim + ) + out = self.fc_out(out) + return out + + +class ALSTM(nn.Module): + + def __init__(self, vocab_size, embed_size, num_heads, hidden_dim, num_classes): + super().__init__() + self.embedding = nn.Embedding(vocab_size, embed_size) + self.attention = SelfAttention(embed_size, num_heads) + self.lstm = nn.LSTM(embed_size, hidden_dim, batch_first=True) + + self.fc1 = nn.Linear(hidden_dim, 256) + self.fc2 = nn.Linear(256, 128) + self.fc3 = nn.Linear(128, 64) + self.fc4 = nn.Linear(64, num_classes) + self.dropout = nn.Dropout(0.5) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + emb = self.embedding(x) + attn = self.attention(emb, emb, emb) + _, (hn, _) = self.lstm(attn) + out = hn[-1] + out = F.relu(self.fc1(out)) + out = self.dropout(out) + out = F.relu(self.fc2(out)) + out = F.relu(self.fc3(out)) + out = self.fc4(out) + return out diff --git a/StreamLearn/Simulator/task/rewards.py b/StreamLearn/Simulator/task/rewards.py new file mode 100644 index 0000000..8107fa6 --- /dev/null +++ b/StreamLearn/Simulator/task/rewards.py @@ -0,0 +1,91 @@ +import numpy as np + + +class RewardFunction: + def __call__(self, loss: float): + raise NotImplementedError("This method should be overridden by subclasses") + + +class IdentityReward(RewardFunction): + def __call__(self, loss: float) -> float: + return loss + + +class ConstantReward(RewardFunction): + def __init__(self, value: float): + self.value = value + + def __call__(self, loss: float) -> float: + return self.value + + +class ThresholdReward(RewardFunction): + def __init__(self, threshold: float, pos: float = 1.0, neg: float = 0.0): + self.threshold = threshold + self.pos = pos + self.neg = neg + + def __call__(self, loss: float) -> float: + if (loss - self.threshold) / self.threshold <= 0.025: + return self.pos + return self.neg + + +class LinearReward(RewardFunction): + def __init__(self, low: float, high: float, pos: float = 1.0, neg: float = 0.0): + assert low < high, "Low must be less than high" + self.low = low + self.high = high + self.pos = pos + self.neg = neg + + def __call__(self, loss: float) -> float: + if loss <= self.low: + return self.pos + if loss >= self.high: + return self.neg + return (self.high - loss) / (self.high - self.low) * (self.pos - self.neg) + self.neg + + +class ExpReward(RewardFunction): + """ + rew = (1 - exp(-k(loss - low))) / (1 - exp(-k(high - low))) * (pos - neg) + neg + + """ + def __init__(self, low: float, high: float, pos: float = 1.0, neg: float = 0.0, k: float = 1.0): + assert low < high, "Low must be less than high" + self.low = low + self.high = high + self.pos = pos + self.neg = neg + self.k = k + + def __call__(self, loss: float) -> float: + if loss <= self.low: + return self.pos + if loss >= self.high: + return self.neg + rew = (1 - np.exp(-self.k * (loss - self.low))) / (1 - np.exp(-self.k * (self.high - self.low))) * (self.pos - self.neg) + self.neg + return rew + + +class LogReward(RewardFunction): + """ + rew = log(1 + (loss - low)/k) / log(1 + (high - low)/k) * (pos - neg) + neg + + """ + def __init__(self, low: float, high: float, pos: float = 1.0, neg: float = 0.0, k: float = 1.0): + assert low < high, "Low must be less than high" + self.low = low + self.high = high + self.pos = pos + self.neg = neg + self.k = k + + def __call__(self, loss: float) -> float: + if loss <= self.low: + return self.pos + if loss >= self.high: + return self.neg + rew = np.log(1 + (loss - self.low) / self.k) / np.log(1 + (self.high - self.low) / self.k) * (self.pos - self.neg) + self.neg + return rew diff --git a/StreamLearn/Simulator/task/rl_agent.py b/StreamLearn/Simulator/task/rl_agent.py new file mode 100644 index 0000000..bb4af1c --- /dev/null +++ b/StreamLearn/Simulator/task/rl_agent.py @@ -0,0 +1,189 @@ +""" +Generate a simple DAgger agent for the RL task. +The default task is MontezumaRevengeNoFrameskip-v0 in Atari with image input. +""" + +import argparse +import gym +from PIL import Image +import time +import numpy as np + +import torch +import torch.nn as nn +import torch.nn.functional as F +import torchvision.transforms as transforms + +from StreamLearn.Simulator.task.dataset import get_dataloader +from StreamLearn.Simulator.task.models import DAggerCNNNet + + +def get_rl_args(): + parser = argparse.ArgumentParser(description='RL') + parser.add_argument( + '--env-name', + type=str, + default='MontezumaRevengeNoFrameskip-v0') + parser.add_argument( + '--num-stacks', + type=int, + default=8) + parser.add_argument( + '--num-steps', + type=int, + default=800) + parser.add_argument( + '--test-steps', + type=int, + default=2000) + parser.add_argument( + '--num-frames', + type=int, + default=800) + + ## other parameter + parser.add_argument( + '--log-interval', + type=int, + default=1, + help='log interval, one log per n updates (default: 10)') + parser.add_argument( + '--save-img', + type=bool, + default=False) + parser.add_argument( + '--save-interval', + type=int, + default=10, + help='save interval, one eval per n updates (default: None)') + return parser.parse_known_args()[0] + + +class StackedEnv(object): + def __init__(self, env_name, num_stacks): + self.env = gym.make(env_name) + # num_stacks: the agent acts every num_stacks frames + # it could be any positive integer + self.num_stacks = num_stacks + self.observation_space = self.env.observation_space + self.action_space = self.env.action_space + + def step(self, action): + reward_sum = 0 + for stack in range(self.num_stacks): + obs_next, reward, done, info = self.env.step(action) + reward_sum += reward + if done: + self.env.reset() + return obs_next, reward_sum, done, info + return obs_next, reward_sum, done, info + + def reset(self): + return self.env.reset() + + +class DAggerAgent: + def __init__(self, lr=1e-3): + # init your model + self.model = DAggerCNNNet() + self.optimizer = torch.optim.Adam(self.model.parameters(), lr=lr) + + # select actions by your model + def select_action(self, data_batch): + label_predict = self.model.predict(data_batch) + return label_predict + + +def get_rl_agent(): + args = get_rl_args() + num_updates = int(args.num_frames // args.num_steps) + # query_cnt counts queries to the expert + query_cnt = 0 + + # environment initial + envs = StackedEnv(args.env_name, args.num_stacks) + action_shape = envs.action_space.n + # observation_shape is the shape of the observation + # here is (210,160,3)=(height, weight, channels) + observation_shape = envs.observation_space.shape + print(action_shape, observation_shape) + + # agent initial + agent = DAggerAgent() + data_set = {'data': [], 'label': []} + + # an example of interacting with the environment + # we init the environment and receive the initial observation + obs = envs.reset() + # we get a trajectory with the length of args.num_steps + for step in range(args.num_steps): + # Sample expert actions + with open("./datasets/imgs/label.txt", "r") as f: + expert_action= int(f.readlines()[num_updates - 1].strip()) + action = expert_action + query_cnt += 1 + data_set['data'].append(obs) + data_set['label'].append(expert_action) + + # an example of saving observations + # if args.save_img: + # im = Image.fromarray(obs) + # im.save('imgs/' + 'screen' + str(i * args.num_steps + step) + '.jpeg') + + obs_next, reward, done, _ = envs.step(action) + # we view the new observation as current observation + obs = obs_next + # if the episode has terminated, we need to reset the environment. + if done: + envs.reset() + + img_data_batch = [] + for item in data_set['data']: + img_data_batch.append(Image.fromarray(item)) + data_set['data'] = img_data_batch + + return agent, data_set + + +if __name__ == '__main__': + """ + Test the training of the RL agent. + Not run when importing this file. + """ + from torch.autograd import Variable + args = get_rl_args() + envs = StackedEnv(args.env_name, args.num_stacks) + agent, dataset = get_rl_agent() + train_loader_rl = get_dataloader( + dataset_name="rl", + batch_size=64, + data_batch=dataset['data'], + label_batch=dataset['label'] + ) + criterion = nn.CrossEntropyLoss() + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + model = DAggerCNNNet().to(device) + model.train() + optimizer = torch.optim.Adam(model.parameters(), lr=0.001) + num_updates = int(args.num_frames // args.num_steps) + # epoch = 30 + running_loss = 0 + + for epoch_idx in range(num_updates): + correct = 0 + total = 0 + train_data_list = list(train_loader_rl) + for batch_idx, (data, target) in enumerate(train_loader_rl): + data, target = Variable(data).to(device), Variable(target).to(device) + + optimizer.zero_grad() + output = model(data) + _, predicted = torch.max(output.data, 1) + total += target.size(0) + correct += (predicted == torch.max(target.data, 1)[1]).sum().item() + loss = criterion(output, torch.max(target, 1)[1]) + running_loss += loss.item() + loss.backward() + optimizer.step() + print(f"loss: {running_loss / (epoch_idx * len(train_data_list) + batch_idx + 1)}") + print(f'epoch: {epoch_idx + 1}/{num_updates}, crr/tot: {correct}/{total}, acc: {correct / total:.3f}') diff --git a/StreamLearn/Simulator/task/task.py b/StreamLearn/Simulator/task/task.py new file mode 100644 index 0000000..e640404 --- /dev/null +++ b/StreamLearn/Simulator/task/task.py @@ -0,0 +1,126 @@ +from typing import Callable, List + +import numpy as np +import torch + +from StreamLearn.Simulator.task.task_configs import TaskConfig + + +class Task: + ''' + A task is similar to a stream task, but it is more general. + It contains a model and a dataset (dataloader), where the model is not necessarily a stream algorithm. + It has a start time and an end time. + For each timestep, the task can train a batch. + ''' + def __init__( + self, + config: TaskConfig, + start_time: int, + task_id: int = None, + device: torch.device = torch.device('cpu'), + ): + self.model = config.get_model() + dataloader = config.get_dataloader() + self.data = list(dataloader) # each element is a batch of data, which is a tuple (x, y) + self.start_time = start_time + self.available_time = config.available_time + self.end_time = start_time + self.available_time + self.task_id = task_id + self.device = device + + self.optimizer = torch.optim.Adam(self.model.parameters(), lr=0.001) + criterion = config.criterion + if isinstance(criterion, str): + criterion = criterion.upper() + if criterion is None or criterion == 'CE': + self.criterion = torch.nn.CrossEntropyLoss() + elif criterion == 'MSE': + self.criterion = torch.nn.MSELoss() + else: + self.criterion = criterion + self.losses: List[float] = [] # average loss over the history for each timestep + self.resources: List[int] = [] + self.num_batches = 0 + self.epsilon = config.epsilon + self.reward_func = config.reward_func + + self.success = False + self.time_out = False + + self._data_len = len(self.data) + self._idx = 0 + self._train_data = None + self._batch_per_unit = 5 # Number of batches per unit time, determined by exploration + + self.eta: float = 0 + self.eta_list: List[float] = [] + self.N: int = 100 + self.iter: int = 0 + self.iter_time: List[int] = [] + self.first_time_prediction: bool = True + # For WLS prediction + self.Vk: np.ndarray = 0 + self.theta: np.ndarray = np.array([[0], [0]]) + + def update_eta(self, eta: float) -> None: + self.eta = eta + self.eta_list.append(eta) + + def _prepare_data(self, num_data: int = 1) -> None: + assert num_data > 0, "num_data must be greater than 0" + ep = num_data // self._data_len + ed = (self._idx + num_data) % self._data_len + if self._idx <= ed: + if ep > 0: + self._train_data = self.data[self._idx:] + (ep - 1) * self.data + self.data[:ed] + else: + self._train_data = self.data[self._idx:ed] + else: + self._train_data = self.data[self._idx:] + ep * self.data + self.data[:ed] + + def stream_train(self, num_batches: int = 1) -> None: + ''' + Get data from the dataloader and train the model. + ''' + assert num_batches > 0, "num_batches must be greater than 0" + if self._batch_per_unit == -1: + num_batches = 1 + else: + num_batches = int(num_batches * self._batch_per_unit) + cur_loss = 0 + self._prepare_data(num_batches) + for x, y in self._train_data: + x = x.to(self.device) + y = y.to(self.device) + self.model.train() + self.optimizer.zero_grad() + pred_y = self.model(x) + loss = self.criterion(pred_y, y) + loss.backward() + self.optimizer.step() + cur_loss += loss.item() + + if len(self.losses) == 0: + self.losses.append(cur_loss / num_batches) + self.resources.append(num_batches) + else: + self.losses.append((self.losses[-1] * self.num_batches + cur_loss) / (self.num_batches + num_batches)) + self.resources.append(self.resources[-1] + num_batches) + self.num_batches += num_batches + + if (self.losses[-1] - self.epsilon) / self.epsilon <= 0.025: + self.success = True + + def get_reward(self) -> float: + if len(self.losses) == 0: + return 0 + return self.reward_func(self.losses[-1]) + + def set_task_id(self, task_id: int) -> None: + self.task_id = task_id + + def to(self, device: torch.device): + self.model.to(device) + self.device = device + return self diff --git a/StreamLearn/Simulator/task/task_configs.py b/StreamLearn/Simulator/task/task_configs.py new file mode 100644 index 0000000..19efd0c --- /dev/null +++ b/StreamLearn/Simulator/task/task_configs.py @@ -0,0 +1,250 @@ +from dataclasses import dataclass, field +from typing import Callable, Dict +from vit_pytorch import SimpleViT +import torch +import torch.nn as nn + +from StreamLearn.Simulator.task import rewards, models +from StreamLearn.Simulator.task.dataset import get_dataloader +from StreamLearn.Simulator.task.rl_agent import get_rl_agent + + +@dataclass +class TaskConfig: + model: nn.Module + reward_func: rewards.RewardFunction + model_config: Dict = field(default_factory=dict) + dataloader_config: Dict = field(default_factory=dict) + criterion: str | Callable = None + available_time: int = 100 + epsilon: float = 0.01 # Default epsilon for reward calculation + + def get_model(self) -> nn.Module: + return self.model(**self.model_config) + + def get_dataloader(self) -> torch.utils.data.DataLoader: + return get_dataloader(**self.dataloader_config) + + +@dataclass +class CNN_Cifar10(TaskConfig): + model: nn.Module = models.SimpleCNN_Cifar10 + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.15) + available_time: int = 590 + epsilon: float = 0.15 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class CNN_MNIST(TaskConfig): + model: nn.Module = models.SimpleCNN_MNIST + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.1) + available_time: int = 500 + epsilon: float = 0.1 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class ResNet18_Cifar10(TaskConfig): + model: nn.Module = models.ResNet18_Cifar10 + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.35) + available_time: int = 610 + epsilon: float = 0.35 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class ResNet34_Cifar10(TaskConfig): + model: nn.Module = models.ResNet34_Cifar10 + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.45) + available_time: int = 630 + epsilon: float = 0.45 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class LSTM_Cifar10(TaskConfig): + model: nn.Module = models.LSTM_Cifar10 + reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.18) + available_time: int = 570 + epsilon: float = 1.18 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class ViT_Cifar10(TaskConfig): + model: nn.Module = SimpleViT + reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.35) + available_time: int = 500 + epsilon: float = 1.35 + + def __post_init__(self): + self.model_config = dict( + image_size=32, + patch_size=4, + num_classes=10, + dim=16, + depth=2, + heads=2, + mlp_dim=8, + dim_head=4 + ) + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class ViT_Cifar10_1(TaskConfig): + model: nn.Module = SimpleViT + reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.1) + available_time: int = 530 + epsilon: float = 1.1 + + def __post_init__(self): + self.model_config = dict( + image_size=32, + patch_size=4, + num_classes=10, + dim=16, + depth=2, + heads=2, + mlp_dim=8, + dim_head=4 + ) + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class ViT_Cifar10_2(ViT_Cifar10_1): + reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.35) + available_time: int = 540 + epsilon: float = 1.35 + +@dataclass +class RL_1(TaskConfig): + model: nn.Module = models.DAggerCNNNet + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.0011) + available_time: int = 545 + epsilon: float = 0.0011 + + def __post_init__(self): + agent, dataset = get_rl_agent() + self.dataloader_config = dict( + dataset_name='rl', + data_batch=dataset['data'], + label_batch=dataset['label'] + ) + +@dataclass +class RL_2(RL_1): + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.0012) + available_time: int = 710 + epsilon: float = 0.0012 + +@dataclass +class TSFM_Audio_1(TaskConfig): + model: nn.Module = models.TransformerModel + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.06) + available_time: int = 585 + epsilon: float = 0.06 + + def __post_init__(self): + self.model_config = dict( + input_dim=64, + num_heads=64, + num_classes=2 + ) + self.dataloader_config = dict( + dataset_name='yesno', + batch_size=1, + ) + +@dataclass +class TSFM_Audio_2(TSFM_Audio_1): + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.08) + available_time: int = 700 + epsilon: float = 0.08 + +@dataclass +class ALSTM_Text_1(TaskConfig): + vb_size: int = 100683 + model: nn.Module = models.ALSTM + reward_func: rewards.RewardFunction = rewards.ThresholdReward(7e-4) + available_time: int = 625 + epsilon: float = 7e-4 + + def __post_init__(self): + self.model_config = dict( + vocab_size=self.vb_size, + embed_size=16, + num_heads=4, + hidden_dim=8, + num_classes=2, + ) + self.dataloader_config = dict( + dataset_name='imdb', + batch_size=8, + ) + +@dataclass +class ALSTM_Text_2(ALSTM_Text_1): + reward_func: rewards.RewardFunction = rewards.ThresholdReward(9e-4) + available_time: int = 690 + epsilon: float = 9e-4 + +@dataclass +class Res18_Cifar10_1(TaskConfig): + model: nn.Module = models.ResNet18_Cifar10 + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.55) + available_time: int = 655 + epsilon: float = 0.55 + + def __post_init__(self): + self.dataloader_config = dict( + dataset_name='cifar10', + ) + +@dataclass +class Res18_Cifar10_2(Res18_Cifar10_1): + reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.55) + available_time: int = 685 + epsilon: float = 0.55 + + +task_list_cifar10 = [ + ViT_Cifar10, + CNN_Cifar10, + ResNet18_Cifar10, + ResNet34_Cifar10, + LSTM_Cifar10, +] + + +task_list_mixed = [ + ViT_Cifar10_1, + ViT_Cifar10_2, + RL_1, + TSFM_Audio_1, + ALSTM_Text_1, + Res18_Cifar10_1, + Res18_Cifar10_2, + ALSTM_Text_2, + TSFM_Audio_2, + RL_2, +] \ No newline at end of file diff --git a/StreamLearn/Simulator/test/main.py b/StreamLearn/Simulator/test/main.py index 9dfd81d..95f8623 100644 --- a/StreamLearn/Simulator/test/main.py +++ b/StreamLearn/Simulator/test/main.py @@ -6,9 +6,10 @@ import importlib from StreamLearn.Simulator.StreamEnv import Env from StreamLearn.Simulator.Policy import IdentityPolicy, RandomPolicy, GreedyPolicy -from StreamLearn.Simulator.StreamGenerator import UniformStreamGenerator, ProbStreamGenerator +from StreamLearn.Simulator import StreamGenerator from StreamLearn.Simulator.TaskRepr import IdentityTaskRepr, RandomTaskRepr # from StreamLearn.Simulator.test.algo import CILEstimator +from StreamLearn.Simulator.sim_utils import utils from StreamLearn.Simulator.utils import get_class from StreamLearn.Simulator.dataset import set_dataset_configs, dataset_dispatch @@ -30,8 +31,7 @@ def main(args): seed = config.seed if seed is not None: print(f"Setting seed to {seed}") - np.random.seed(seed) - torch.random.manual_seed(seed) + utils.set_seed(seed) num_nodes = args.num_nodes print(f"tasks: {args.tasks}") @@ -40,10 +40,12 @@ def main(args): # print(f"Using dataset: {config.dataset}") set_dataset_configs(config) datasets = [dataset_dispatch(model_desc, config) for model_desc in tasks] - stream_generator = ProbStreamGenerator( + generator_name = args.generator_name + generator_class = getattr(StreamGenerator, generator_name) + stream_generator: StreamGenerator.StreamGenerator = generator_class( tasks, datasets, - generate_prob=0.8 + **args.generator_params ) # print(f"Using model: {config.model}") diff --git a/StreamLearn/Simulator/test/test_AEAGS.py b/StreamLearn/Simulator/test/test_AEAGS.py new file mode 100644 index 0000000..c17f296 --- /dev/null +++ b/StreamLearn/Simulator/test/test_AEAGS.py @@ -0,0 +1,103 @@ +import importlib +from types import ModuleType +from typing import Dict, Any +import argparse +import yaml + +from StreamLearn.Simulator.env import StreamGenerator +from StreamLearn.Simulator.env.StreamEnv import Env, Node +from StreamLearn.Simulator.task import task_configs +from StreamLearn.Simulator.sim_utils import utils +from StreamLearn.Simulator.sim_utils.predictor import Online_WLS +from StreamLearn.Simulator.policy.AEAGS import AEAGSPolicy +from StreamLearn.Config.AEAGS import args as aeags_args + + +def experiment(config: Dict[str, Any]) -> None: + # print(config) + debug = config.get('debug', False) + if debug: + print(f"{'='*6} Debug mode is ON {'='*6}") + + # task params + task_list_name = config['task_list'] + task_list = getattr(task_configs, task_list_name) + available_task_list = [] + task_ids = None + if task_ids is None: + task_ids = list(range(len(task_list))) + for task_id in task_ids: + available_task_list.append(task_list[task_id]) + + # environment params + num_nodes = config['num_nodes'] + node_comp_ability = config['node_comp_ability'] + if isinstance(node_comp_ability, float): + node_comp_ability = [node_comp_ability] * num_nodes + assert len(node_comp_ability) == num_nodes + nodes = [Node(comp_ability=c) for c in node_comp_ability] + predictor = Online_WLS(gamma=config.get('predictor_gamma', 0.9)) + final_time = config.get('final_time', 1000) + final_time = max(final_time, len(available_task_list)) + config['final_time'] = final_time + + # generator params + generator_name = config['generator_name'] + generator_class = getattr(StreamGenerator, generator_name) + generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, + ) + env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + rl_mode=True, + **config, + ) + policy = AEAGSPolicy(exploration_round=config["exploration_round"]) + + num_episodes = config['num_episodes'] + succ_list = [] + task_cnt_list = [] + for episode in range(num_episodes): + policy.reset() + obs, terminal = env.reset() + step = 0 + total_reward = 0 + while not terminal: + action = policy.act(obs) + step += 1 + obs, rew, terminal, *_ = env.step(action) + total_reward += rew + if step % 50 == 0: + env.log_statistics() + env.log_statistics() + succ_list.append(total_reward) + task_cnt_list.append(generator.task_cnt) + print(f"=== Episode {episode + 1}/{num_episodes} finished. " + f"Success tasks: {total_reward}/{generator.task_cnt}. " + f"Success rate: {total_reward / generator.task_cnt:.3f}\n") + print(f"Average success rate over {num_episodes} episodes: {sum(succ_list) / sum(task_cnt_list):.3f}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-g", "--gpu", type=int, default=-1, help="GPU ID to use") + parser.add_argument("--seed", type=int, default=None) + args, unknown = parser.parse_known_args() + + config = vars(aeags_args) + + if args.seed is not None: + config['seed'] = args.seed + if config.get('seed') is not None: + utils.set_seed(config['seed']) + if args.gpu >= 0: + utils.set_gpu_mode(True, args.gpu) + print(f"Using GPU: {args.gpu}") + else: + utils.set_gpu_mode(False) + print("Using CPU") + + experiment(config) \ No newline at end of file diff --git a/StreamLearn/Simulator/test/test_AOGS.py b/StreamLearn/Simulator/test/test_AOGS.py new file mode 100644 index 0000000..b26d01c --- /dev/null +++ b/StreamLearn/Simulator/test/test_AOGS.py @@ -0,0 +1,103 @@ +import importlib +from types import ModuleType +from typing import Dict, Any +import argparse +import yaml + +from StreamLearn.Simulator.env import StreamGenerator +from StreamLearn.Simulator.env.StreamEnv import Env, Node +from StreamLearn.Simulator.task import task_configs +from StreamLearn.Simulator.sim_utils import utils +from StreamLearn.Simulator.sim_utils.predictor import Online_WLS +from StreamLearn.Simulator.policy.AOGS import AOGSPolicy +from StreamLearn.Config.AOGS import args as aogs_args + + +def experiment(config: Dict[str, Any]) -> None: + # print(config) + debug = config.get('debug', False) + if debug: + print(f"{'='*6} Debug mode is ON {'='*6}") + + # task params + task_list_name = config['task_list'] + task_list = getattr(task_configs, task_list_name) + available_task_list = [] + task_ids = None + if task_ids is None: + task_ids = list(range(len(task_list))) + for task_id in task_ids: + available_task_list.append(task_list[task_id]) + + # environment params + num_nodes = config['num_nodes'] + node_comp_ability = config['node_comp_ability'] + if isinstance(node_comp_ability, float): + node_comp_ability = [node_comp_ability] * num_nodes + assert len(node_comp_ability) == num_nodes + nodes = [Node(comp_ability=c) for c in node_comp_ability] + predictor = Online_WLS(gamma=config.get('predictor_gamma', 0.9)) + final_time = config.get('final_time', 1000) + final_time = max(final_time, len(available_task_list)) + config['final_time'] = final_time + + # generator params + generator_name = config['generator_name'] + generator_class = getattr(StreamGenerator, generator_name) + generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, + ) + env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + rl_mode=True, + **config, + ) + policy = AOGSPolicy(exploration_round=config["exploration_round"]) + + num_episodes = config['num_episodes'] + succ_list = [] + task_cnt_list = [] + for episode in range(num_episodes): + policy.reset() + obs, terminal = env.reset() + step = 0 + total_reward = 0 + while not terminal: + action = policy.act(obs) + step += 1 + obs, rew, terminal, *_ = env.step(action) + total_reward += rew + if step % 50 == 0: + env.log_statistics() + env.log_statistics() + succ_list.append(total_reward) + task_cnt_list.append(generator.task_cnt) + print(f"=== Episode {episode + 1}/{num_episodes} finished. " + f"Success tasks: {total_reward}/{generator.task_cnt}. " + f"Success rate: {total_reward / generator.task_cnt:.3f}\n") + print(f"Average success rate over {num_episodes} episodes: {sum(succ_list) / sum(task_cnt_list):.3f}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-g", "--gpu", type=int, default=-1, help="GPU ID to use") + parser.add_argument("--seed", type=int, default=None) + args, unknown = parser.parse_known_args() + + config = vars(aogs_args) + + if args.seed is not None: + config['seed'] = args.seed + if config.get('seed') is not None: + utils.set_seed(config['seed']) + if args.gpu >= 0: + utils.set_gpu_mode(True, args.gpu) + print(f"Using GPU: {args.gpu}") + else: + utils.set_gpu_mode(False) + print("Using CPU") + + experiment(config) \ No newline at end of file diff --git a/StreamLearn/Simulator/test/test_MARA.py b/StreamLearn/Simulator/test/test_MARA.py new file mode 100644 index 0000000..94708f4 --- /dev/null +++ b/StreamLearn/Simulator/test/test_MARA.py @@ -0,0 +1,91 @@ +import importlib +from types import ModuleType +from typing import Dict, Any +import argparse +import yaml + +from StreamLearn.Simulator.env import StreamGenerator +from StreamLearn.Simulator.env.StreamEnv import Env, Node +from StreamLearn.Simulator.task import task_configs +from StreamLearn.Simulator.sim_utils import utils +from StreamLearn.Simulator.sim_utils.predictor import Online_WLS +from StreamLearn.Simulator.policy.mat_runner import Runner +from StreamLearn.Config.MARA import args as mara_args + + +def experiment(config: Dict[str, Any]) -> None: + # print(config) + debug = config.get('debug', False) + if debug: + print(f"{'='*6} Debug mode is ON {'='*6}") + + # task params + task_list_name = config['task_list'] + task_list = getattr(task_configs, task_list_name) + available_task_list = [] + task_ids = None + if task_ids is None: + task_ids = list(range(len(task_list))) + for task_id in task_ids: + available_task_list.append(task_list[task_id]) + + # environment params + num_nodes = config['num_nodes'] + node_comp_ability = config['node_comp_ability'] + if isinstance(node_comp_ability, float): + node_comp_ability = [node_comp_ability] * num_nodes + assert len(node_comp_ability) == num_nodes + nodes = [Node(comp_ability=c) for c in node_comp_ability] + predictor = Online_WLS(gamma=config.get('predictor_gamma', 0.9)) + final_time = config.get('final_time', 1000) + final_time = max(final_time, len(available_task_list)) + config['final_time'] = final_time + + # generator params + generator_name = config['generator_name'] + generator_class = getattr(StreamGenerator, generator_name) + generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, + ) + env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + marl_mode=True, + **config, + ) + eval_env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + marl_mode=True, + **config, + ) + + # runner + runner = Runner(env, config, eval_env) + runner.to(utils.device) + runner.simulate() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-g", "--gpu", type=int, default=-1, help="GPU ID to use") + parser.add_argument("--seed", type=int, default=None) + args, unknown = parser.parse_known_args() + + config = vars(mara_args) + + if args.seed is not None: + config['seed'] = args.seed + if config.get('seed') is not None: + utils.set_seed(config['seed']) + if args.gpu >= 0: + utils.set_gpu_mode(True, args.gpu) + print(f"Using GPU: {args.gpu}") + else: + utils.set_gpu_mode(False) + print("Using CPU") + + experiment(config) \ No newline at end of file diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index d71d59f..5f060cf 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -1116,6 +1116,168 @@ python scripts/script_inference.py --K 5 --dataset ml-1m --temp_type simple python scripts/script_finetune.py --dataset ml-1m --K 5 --train_size 64 --train_type simple --test_type simple --epochs 5 --lr 1e-3 --total_batch_size 64 ~~~ +### 4.4 在线匹配市场中的多臂赌博机最优算法AOGS + +自适应在线Gale-Shapley算法(AOGS)是一种双边匹配市场的多臂赌博机算法,它将传统的Gale-Shapley算法融入在线多臂赌博机算法中,依据每一轮的反馈动态调整GS算法的各个步骤。AOGS提升了已有的理论懊悔上界保证,这一结果首次在主阶项中消除了对手臂数量的依赖,在玩家数量远小于手臂数量的常见情况下显著提高了理论保证。将多节点的流数据任务调度问题建模为匹配问题后,AOGS算法同样可以用来完成任务调度。 + +该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 +- StreamLearn/Simulator/policy/AOGS.py +- StreamLearn/Simulator/test/test_AOGS.py + +首先,按照以下方式构造测试任务序列 +```python +task_list_name = config['task_list'] +task_list = getattr(task_configs, task_list_name) +available_task_list = [] +task_ids = None +if task_ids is None: + task_ids = list(range(len(task_list))) +for task_id in task_ids: + available_task_list.append(task_list[task_id]) +``` + +接下来创建任务生成器,配置调度环境 +```python +generator_name = config['generator_name'] +generator_class = getattr(StreamGenerator, generator_name) +generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, +) +env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + rl_mode=True, + **config, +) +``` +根据参数初始化AOGS调度策略 +```python +policy = AOGSPolicy(exploration_round=config["exploration_round"]) +``` +最后即可在流数据环境下执行AOGS调度算法 +```python +obs, terminal = env.reset() +step = 0 +total_reward = 0 +while not terminal: + action = policy.act(obs) + step += 1 + obs, rew, terminal, *_ = env.step(action) + total_reward += rew +``` + +### 4.5 在线匹配市场不敏感偏好场景下的多臂赌博机算法AE-AGS + +AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应多臂赌博机算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 + +该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 +- StreamLearn/Simulator/policy/AOGS.py +- StreamLearn/Simulator/test/test_AOGS.py + +首先,按照以下方式构造测试任务序列 +```python +task_list_name = config['task_list'] +task_list = getattr(task_configs, task_list_name) +available_task_list = [] +task_ids = None +if task_ids is None: + task_ids = list(range(len(task_list))) +for task_id in task_ids: + available_task_list.append(task_list[task_id]) +``` + +接下来创建任务生成器,配置调度环境 +```python +generator_name = config['generator_name'] +generator_class = getattr(StreamGenerator, generator_name) +generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, +) +env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + rl_mode=True, + **config, +) +``` +根据参数初始化AE-AGS调度策略 +```python +policy = AEAGSPolicy(exploration_round=config["exploration_round"]) +``` +最后即可在流数据环境下执行AE-AGS调度算法 +```python +obs, terminal = env.reset() +step = 0 +total_reward = 0 +while not terminal: + action = policy.act(obs) + step += 1 + obs, rew, terminal, *_ = env.step(action) + total_reward += rew +``` + +### 4.6 基于多智能体强化学习的自适应调度算法MARA + +在多节点的流数据调度任务中,如果将节点看作智能体、待调度的任务看作匹配目标,那么该调度任务可以建模为多智能体强化学习(MARL)问题。在每一时刻`t`,每个节点选择一个任务执行,由此实现任务调度。由于同一时刻待调度的任务数量不断变化,MARA进一步用序列决策模型来建模该MARL问题,结合多智能体Transformer算法,实现对任务的动态评估和调度。 + +该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分。其中,调度策略的模型和训练工具由多个文件组成。 +- StreamLearn/Simulator/policy/mat_policy.py +- StreamLearn/Simulator/policy/networks.py +- StreamLearn/Simulator/policy/mat_trainer.py +- StreamLearn/Simulator/policy/mat_runner.py +- StreamLearn/Simulator/sim_utils/predictor.py +- StreamLearn/Simulator/sim_utils/replay_buffer.py +- StreamLearn/Simulator/test/test_MARA.py + +首先,按照以下方式构造测试任务序列 +```python +task_list_name = config['task_list'] +task_list = getattr(task_configs, task_list_name) +available_task_list = [] +task_ids = None +if task_ids is None: + task_ids = list(range(len(task_list))) +for task_id in task_ids: + available_task_list.append(task_list[task_id]) +``` + +接下来创建任务生成器,配置调度环境 +```python +generator_name = config['generator_name'] +generator_class = getattr(StreamGenerator, generator_name) +generator: StreamGenerator.StreamGenerator = generator_class( + available_task_list=available_task_list, + **config, +) +env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + rl_mode=True, + **config, +) +eval_env = Env( + nodes=nodes, + stream_generator=generator, + predictor=predictor, + marl_mode=True, + **config, +) +``` +根据参数初始化MARA的执行模块 +```python +runner = Runner(env, config, eval_env) +``` +最后调用执行模块中的`simulate`方法,即可在流数据环境下执行MARA调度算法 +```python +runner.simulate() +``` + + ## 课题五:流数据增量学习算法 ### 5.1 增量学习算法MEMO -- Gitee From a0f777d4d272a68e8b1d1076e29dfd4341b19cf7 Mon Sep 17 00:00:00 2001 From: bwnzheng Date: Fri, 1 Aug 2025 08:59:08 +0000 Subject: [PATCH 2/9] update StreamLearn/Simulator/test/main.py. fix main.py Signed-off-by: bwnzheng --- StreamLearn/Simulator/test/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/StreamLearn/Simulator/test/main.py b/StreamLearn/Simulator/test/main.py index 95f8623..21f56b8 100644 --- a/StreamLearn/Simulator/test/main.py +++ b/StreamLearn/Simulator/test/main.py @@ -64,7 +64,7 @@ def main(args): env = Env( num_nodes=num_nodes, stream_generator=stream_generator, - tasks=tasks, + # tasks=tasks, task_repr=task_repr ) @@ -86,6 +86,8 @@ if __name__ == "__main__": parser.add_argument('--dataset_config', type=str, default='./StreamLearn/Config/Simulator.py') parser.add_argument('--seed', type=int, default=None) parser.add_argument('--num_nodes', type=int, default=2) + parser.add_argument('--generator_name', default='ProbStreamGenerator') + parser.add_argument('--generator_params', default=dict(generate_prob=0.8)) parser.add_argument('--tasks', nargs='+') args, unknown = parser.parse_known_args() -- Gitee From d863774f9c4d04c0a946fdf5630a647cc140a277 Mon Sep 17 00:00:00 2001 From: FineArtz Date: Wed, 6 Aug 2025 16:35:15 +0000 Subject: [PATCH 3/9] update GEAR --- .gitignore | 3 +- .../create_dataset/create_mario_negative.py | 313 +++++++++++++++++ .../create_dataset/create_mario_positive.py | 324 ++++++++++++++++++ .../Algorithm/AbdGen/mario_icons/README.txt | 1 + .../AbdGen/mario_icons/agent_icons.png | Bin 0 -> 74796 bytes .../AbdGen/mario_icons/background_tiles.png | Bin 0 -> 90957 bytes .../Algorithm/AbdGen/mario_icons/bomb.png | Bin 0 -> 5251 bytes .../Algorithm/AbdGen/mario_icons/brick.png | Bin 0 -> 5998 bytes .../Algorithm/AbdGen/mario_icons/brick2.png | Bin 0 -> 2685 bytes .../Algorithm/AbdGen/mario_icons/brick3.png | Bin 0 -> 2380 bytes .../Algorithm/AbdGen/mario_icons/brindle.png | Bin 0 -> 5657 bytes .../AbdGen/mario_icons/chessboard.png | Bin 0 -> 1685 bytes .../AbdGen/mario_icons/chessboard_blue.png | Bin 0 -> 1610 bytes .../AbdGen/mario_icons/chessboard_pink.png | Bin 0 -> 1723 bytes .../Algorithm/AbdGen/mario_icons/cloud.png | Bin 0 -> 4528 bytes .../Algorithm/AbdGen/mario_icons/coin.png | Bin 0 -> 4325 bytes .../Algorithm/AbdGen/mario_icons/concrete.png | Bin 0 -> 4517 bytes .../Algorithm/AbdGen/mario_icons/flowers1.png | Bin 0 -> 6520 bytes .../Algorithm/AbdGen/mario_icons/flowers2.png | Bin 0 -> 6010 bytes .../AbdGen/mario_icons/frame_tiles.png | Bin 0 -> 67161 bytes .../Algorithm/AbdGen/mario_icons/glass.png | Bin 0 -> 3907 bytes .../Algorithm/AbdGen/mario_icons/goomba.png | Bin 0 -> 7097 bytes .../Algorithm/AbdGen/mario_icons/grass.png | Bin 0 -> 12052 bytes .../AbdGen/mario_icons/green_mushroom.png | Bin 0 -> 6569 bytes .../AbdGen/mario_icons/green_panel.png | Bin 0 -> 6865 bytes .../Algorithm/AbdGen/mario_icons/lava.png | Bin 0 -> 7026 bytes .../Algorithm/AbdGen/mario_icons/luigi.png | Bin 0 -> 7885 bytes .../Algorithm/AbdGen/mario_icons/mario.png | Bin 0 -> 7269 bytes .../Algorithm/AbdGen/mario_icons/peach.png | Bin 0 -> 8246 bytes .../AbdGen/mario_icons/red_mushroom.png | Bin 0 -> 5388 bytes .../Algorithm/AbdGen/mario_icons/sand.png | Bin 0 -> 6165 bytes .../Algorithm/AbdGen/mario_icons/sea.png | Bin 0 -> 6443 bytes .../Algorithm/AbdGen/mario_icons/star.png | Bin 0 -> 4696 bytes .../AbdGen/mario_icons/target_icons.png | Bin 0 -> 62153 bytes .../AbdGen/mario_icons/white_panel.png | Bin 0 -> 7441 bytes .../Algorithm/AbdGen/mario_icons/wood.png | Bin 0 -> 78988 bytes StreamLearn/Algorithm/GEAR/README.md | 6 + .../GEAR/include/common/string_format.h | 2 + StreamLearn/Algorithm/GEAR/requirements.txt | 2 + StreamLearn/Algorithm/GEAR/setup.py | 2 +- .../GEAR => Algorithm/GEAR/tests}/README.md | 4 +- .../GEAR/tests/single_node}/README.md | 0 .../GEAR/tests/single_node}/config.json | 0 .../GEAR/tests/single_node}/config.py | 0 .../GEAR/tests/single_node}/create.py | 0 .../tests/single_node}/deepspeed_config.json | 0 .../GEAR/tests/single_node/main_bak.py} | 3 +- .../GEAR/tests/single_node}/misc.py | 0 .../tests/single_node}/models/__init__.py | 0 .../tests/single_node}/models/mat/README.md | 0 .../tests/single_node}/models/mat/__init__.py | 0 .../tests/single_node}/models/mat/funcs.py | 0 .../single_node}/models/mat/model_impl.py | 0 .../models/mat/transformer_act.py | 0 .../tests/single_node}/models/mat/utils.py | 0 .../tests/single_node}/models/mlp/__init__.py | 0 .../tests/single_node}/models/mlp/funcs.py | 0 .../single_node}/models/mlp/model_impl.py | 0 .../GEAR/tests/single_node}/run.sh | 2 +- .../single_node}/single_node_requirements.txt | 0 .../GEAR/tests/single_node}/test_env.py | 0 StreamLearn/Config/GEAR.py | 91 +++++ StreamLearn/Simulator/task/dataset.py | 210 ++++++------ StreamLearn/legacy/README.md | 33 +- StreamLearn/tests/test_GEAR.py | 189 ++++++++++ pyproject.toml | 1 - 66 files changed, 1058 insertions(+), 128 deletions(-) create mode 100644 StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py create mode 100644 StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/README.txt create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/agent_icons.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/background_tiles.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/bomb.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/brick.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/brick2.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/brick3.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/brindle.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/chessboard.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/chessboard_blue.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/chessboard_pink.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/cloud.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/coin.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/concrete.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/flowers1.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/flowers2.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/frame_tiles.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/glass.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/goomba.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/grass.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/green_mushroom.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/green_panel.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/lava.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/luigi.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/mario.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/peach.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/red_mushroom.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/sand.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/sea.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/star.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/target_icons.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/white_panel.png create mode 100644 StreamLearn/Algorithm/AbdGen/mario_icons/wood.png rename StreamLearn/{tests/GEAR => Algorithm/GEAR/tests}/README.md (87%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/README.md (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/config.json (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/config.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/create.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/deepspeed_config.json (100%) rename StreamLearn/{tests/GEAR/offline/single-node/main.py => Algorithm/GEAR/tests/single_node/main_bak.py} (99%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/misc.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/__init__.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/README.md (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/__init__.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/funcs.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/model_impl.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/transformer_act.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mat/utils.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mlp/__init__.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mlp/funcs.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/models/mlp/model_impl.py (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/run.sh (85%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/single_node_requirements.txt (100%) rename StreamLearn/{tests/GEAR/offline/single-node => Algorithm/GEAR/tests/single_node}/test_env.py (100%) create mode 100644 StreamLearn/Config/GEAR.py create mode 100644 StreamLearn/tests/test_GEAR.py diff --git a/.gitignore b/.gitignore index 4c811f8..ba96d40 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ build /dataset /datasets /logs -.DS_Store +logs +.DS_Store \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py new file mode 100644 index 0000000..424fd61 --- /dev/null +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py @@ -0,0 +1,313 @@ +""" +Adapted from "https://github.com/EleMisi/VAEL/blob/main/utils/mario_utils/create_mario_dataset.py" + +""" + +import json +import os +from itertools import product + +import numpy as np +from PIL import Image +from matplotlib import use +from numpy import random +from tqdm import tqdm + + +# Disable canvas visualization +use('Agg') + +ICONS = { + 'glass': f'./mario_icons/glass.png', + 'flowers1': f'./mario_icons/flowers1.png', + 'flowers2': f'./mario_icons/flowers2.png', + 'brick': f'./mario_icons/brick.png', + 'brick2': f'./mario_icons/brick2.png', + 'brick3': f'./mario_icons/brick3.png', + 'concrete': f'./mario_icons/concrete.png', + 'wood': f'./mario_icons/wood.png', + 'white_panel': f'./mario_icons/hite_panel.png', + 'green_panel': f'./mario_icons/green_panel.png', + + 'lava': f'./mario_icons/lava.png', + 'sea': f'./mario_icons/sea.png', + 'sand': f'./mario_icons/sand.png', + 'grass': f'./mario_icons/grass.png', + 'chessboard': f'./mario_icons/chessboard.png', + 'chessboard_blue': f'./mario_icons/chessboard_blue.png', + 'chessboard_pink': f'./mario_icons/chessboard_pink.png', + 'brindle': f'./mario_icons/brindle.png', + + 'mario': f'./mario_icons/mario.png', + 'luigi': f'./mario_icons/luigi.png', + 'peach': f'./mario_icons/peach.png', + 'bomb': f'./mario_icons/bomb.png', + 'goomba': f'./mario_icons/goomba.png', + + 'green_mushroom': f'./mario_icons/green_mushroom.png', + 'star': f'./mario_icons/star.png', + 'red_mushroom': f'./mario_icons/red_mushroom.png', + 'coin': f'./mario_icons/coin.png', + 'cloud': f'./mario_icons/cloud.png' +} + +def resize_with_transparency(img, size): + pal = img.getpalette() + width, height = img.size + actual_transp = img.info['actual_transparency'] # XXX This will fail. + + result = Image.new('LA', img.size) + + im = img.load() + res = result.load() + for x in range(width): + for y in range(height): + t = actual_transp[im[x, y]] + color = pal[im[x, y]] + res[x, y] = (color, t) + + return result.resize(size, Image.ANTIALIAS) + + +def PNG_ResizeKeepTransparency(img, new_width=0, new_height=0, resample="LANCZOS", RefFile=''): + # needs PIL + # Inputs: + # - SourceFile = initial PNG file (including the path) + # - ResizedFile = resized PNG file (including the path) + # - new_width = resized width in pixels; if you need % plz include it here: [your%] *initial width + # - new_height = resized hight in pixels ; default = 0 = it will be calculated using new_width + # - resample = "NEAREST", "BILINEAR", "BICUBIC" and "ANTIALIAS"; default = "ANTIALIAS" + # - RefFile = reference file to get the size for resize; default = '' + + img = img.convert("RGBA") # convert to RGBA channels + width, height = img.size # get initial size + + # if there is a reference file to get the new size + if RefFile != '': + imgRef = Image.open(RefFile) + new_width, new_height = imgRef.size + else: + # if we use only the new_width to resize in proportion the new_height + # if you want % of resize please use it into new_width (?% * initial width) + if new_height == 0: + new_height = new_width * width / height + + # split image by channels (bands) and resize by channels + img.load() + bands = img.split() + # resample mode + if resample == "NEAREST": + resample = Image.NEAREST + else: + if resample == "BILINEAR": + resample = Image.BILINEAR + else: + if resample == "BICUBIC": + resample = Image.BICUBIC + else: + if resample == "ANTIALIAS": + resample = Image.ANTIALIAS + else: + if resample == "LANCZOS": + resample = Image.LANCZOS + bands = [b.resize((new_width, new_height), resample) for b in bands] + # merge the channels after individual resize + img = Image.merge('RGBA', bands) + + return img + + +def draw_mario_world(X, Y, agent_x, agent_y, target_x, target_y, agent_icon='goomba', target_icon='green_mushroom', + background_tile='lava', frame_tile='glass'): + """ + This method creates the specified Mario's world. + """ + # Initialize canvas + W, H = 20, 20 + image = Image.new("RGBA", ((X + 2) * W, (Y + 2) * H), (255, 255, 255)) + # Define y offset for PIL + agent_y = - (agent_y - (Y - 1)) + target_y = - (target_y - (Y - 1)) + # Set off-set due to frame_dict + agent_x, agent_y = agent_x + 1, agent_y + 1 + target_x, target_y = target_x + 1, target_y + 1 + # Scale position to tile dimension + agent_x, agent_y = agent_x * W, agent_y * H + target_x, target_y = target_x * W, target_y * H + # Load mario_icons and tiles + agent_icon = Image.open(ICONS[agent_icon]) + target_icon = Image.open(ICONS[target_icon]) + background_tile = Image.open(ICONS[background_tile]) + frame_tile = Image.open(ICONS[frame_tile]) + # Resize mario_icons and tiles to fit the image + + background_tile = background_tile.resize((W, H), Image.LANCZOS) + frame_tile = frame_tile.resize((W, H), Image.LANCZOS) + agent_icon = PNG_ResizeKeepTransparency(agent_icon, new_width=int(W / 2), new_height=int(H / 2), resample="LANCZOS", + RefFile='') + target_icon = PNG_ResizeKeepTransparency(target_icon, new_width=int(W / 2) + 2, new_height=int(H / 2) + 2, + resample="LANCZOS", + RefFile='') + # Define frame_dict tiles left corners + frame_tiles_pos = [] + for i in range(Y + 2): + frame_tiles_pos.append((0, i * H)) + frame_tiles_pos.append(((X+1) * W, i * H)) + frame_tiles_pos.append((i * W, 0)) + frame_tiles_pos.append((i * W, (X+1) * H)) + # Define background_dict tiles left corners + bkg_tiles_pos = [] + for i in range(1, Y + 1): + for j in range(1, X + 1): + bkg_tiles_pos.append((j * W, i * H)) + # Draw frame_dict + for box in frame_tiles_pos: + image.paste(frame_tile, box=box) + # Draw background_dict + for box in bkg_tiles_pos: + image.paste(background_tile, box=box) + # Draw target_dict + # target_box = (target_x + 4, target_y + 4) + # image.paste(target_icon, box=target_box, mask=target_icon) + # Draw agent_dict + agent_box = (agent_x + 5, agent_y + 5) + image.paste(agent_icon, box=agent_box, mask=agent_icon) + + return np.array(image)[:, :, :3] + + +def define_program(traj): + """ + Translate the given trajectory in Mario program + + traj: list containing pairs of sequentially 2D Mario coordinates [((x0,y0),(x1,y1)), ((x1,y1),(x2,y2)),...] + """ + program = [] + # Tras + for (x0, y0), (x1, y1) in traj: + if x0 < x1: + program.append("right") + elif x0 > x1: + program.append("left") + elif y0 < y1: + program.append("up") + elif y0 > y1: + program.append("down") + + return program + +def create_mario_dataset(folder): + + # List of 9 pairs of agent positions in a 3x3 grid + position_set = set() + position_set.add(((0,0),(0,1),(0,2),(1,2),(2,2))) + position_set.add(((1,0),(1,1),(2,1),(2,2))) + position_set.add(((2,0),(2,1),(2,2))) + position_set.add(((1,0),(1,1),(1,2))) + position_set.add(((1,1),(1,2),(2,2))) + position_set.add(((0,1),(0,2),(1,2))) + position_set.add(((2,1),(1,1),(1,2))) + + + + # Order positions list for reproducibility + position_list = list(position_set) + position_list.sort(key=lambda y: (y[0], y[1])) + + positions_move = [] + position = [] + move = [] + for pos in position_list: + p = [] + m = [] + for i in range(len(pos)-1): + m.append(define_program([(pos[i], pos[i+1])])[0]) + p.append(pos[i]) + p.append(pos[len(pos)-1]) + p_m = p + m + p_m = tuple(p_m) + p = tuple(p) + m = tuple(m) + positions_move.append(p_m) + position.append(p) + move.append(m) + + # Create Dataset from positions and moves + if not os.path.exists(folder): + os.makedirs(folder) + + agents = ['peach', 'mario', 'luigi'] + targets = ['coin'] + backgrounds = ['chessboard_blue', 'sea', 'grass', 'chessboard_pink', 'chessboard', 'sand','flowers1','flowers2'] + frames = ['brick', 'brick2', 'brindle', 'brick3', 'glass','concrete','wood'] + images = {'all': []} + moves = {'all': []} + positions = {'all': []} + target_pos = {'all': []} + agent_dict = {'all': []} + target_dict = {'all': []} + background_dict = {'all': []} + frame_dict = {'all': []} + + info = { + 'agent_dict': {'all': {c: 0 for c in agents}}, + 'target_dict': {'all': {o: 0 for o in targets}}, + 'background_dict': {'all': {c: 0 for c in backgrounds}}, + 'frame_dict': {'all': {o: 0 for o in frames}}, + 'pos': {'all': {str(p): 0 for p in position}}, + 'moves': {'all': {str(m): 0 for m in move}}} + + configs = list(product(agents, targets, backgrounds, frames)) + # Order config list for reproducibility + configs.sort() + idxs_split = {'all': []} + tot_imgs = len(configs) * len(positions_move) + + idxs = list(range(tot_imgs)) + random.seed(88888) + idxs_split['all'] = random.choice(idxs, size=tot_imgs, replace=False) + + idx = 0 + for config in tqdm(configs): + (a, t, bg, f) = config + for i in range(len(position)): + p = position[i] + p_list = list(p) + m = move[i] + m_list = list(m) + imgs = [] + for pos in p: + img = draw_mario_world(X=3, Y=3, agent_x=pos[0], agent_y=pos[1], target_x=2, target_y=2, agent_icon=a, target_icon=t, background_tile=bg, frame_tile=f) + imgs.append(img) + for dataset in ['all']: + if idx in idxs_split[dataset]: + images[dataset].append(imgs) + moves[dataset].append(m_list) + positions[dataset].append(p_list) + target_pos[dataset].append(pos) + agent_dict[dataset].append(a) + target_dict[dataset].append(t) + background_dict[dataset].append(bg) + frame_dict[dataset].append(f) + info['target_dict'][dataset][t] += 1 + info['frame_dict'][dataset][f] += 1 + info['background_dict'][dataset][bg] += 1 + info['agent_dict'][dataset][a] += 1 + info['pos'][dataset][str(p)] += 1 + info['moves'][dataset][str(m)] += 1 + idx += 1 + # Check dimensions + assert len(images['all']) == tot_imgs + + # Save images, moves and positions + for dataset in ['all']: + np.savez(os.path.join(folder, f'{dataset}_mario_neg.npz'), images=images[dataset], + pos=positions[dataset], target_pos=target_pos[dataset], moves=moves[dataset], agents=agent_dict[dataset], targets=target_dict[dataset], + bkgs=background_dict[dataset], frames=frame_dict[dataset]) + + # Save info files + with open(os.path.join(folder, 'info.json'), 'w') as file: + json.dump(info, file, indent=4) + +if __name__ == '__main__': + create_mario_dataset('dataset/mario') \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py new file mode 100644 index 0000000..d22efa7 --- /dev/null +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py @@ -0,0 +1,324 @@ +""" +Adapted from "https://github.com/EleMisi/VAEL/blob/main/utils/mario_utils/create_mario_dataset.py" + +""" + +import os +from itertools import product + +import numpy as np +from PIL import Image +from matplotlib import use +from numpy import random +from tqdm import tqdm + + +# Disable canvas visualization +use('Agg') + +ICONS = { + 'glass': f'./mario_icons/glass.png', + 'flowers1': f'./mario_icons/flowers1.png', + 'flowers2': f'./mario_icons/flowers2.png', + 'brick': f'./mario_icons/brick.png', + 'brick2': f'./mario_icons/brick2.png', + 'brick3': f'./mario_icons/brick3.png', + 'concrete': f'./mario_icons/concrete.png', + 'wood': f'./mario_icons/wood.png', + 'white_panel': f'./mario_icons/hite_panel.png', + 'green_panel': f'./mario_icons/green_panel.png', + + 'lava': f'./mario_icons/lava.png', + 'sea': f'./mario_icons/sea.png', + 'sand': f'./mario_icons/sand.png', + 'grass': f'./mario_icons/grass.png', + 'chessboard': f'./mario_icons/chessboard.png', + 'chessboard_blue': f'./mario_icons/chessboard_blue.png', + 'chessboard_pink': f'./mario_icons/chessboard_pink.png', + 'brindle': f'./mario_icons/brindle.png', + + 'mario': f'./mario_icons/mario.png', + 'luigi': f'./mario_icons/luigi.png', + 'peach': f'./mario_icons/peach.png', + 'bomb': f'./mario_icons/bomb.png', + 'goomba': f'./mario_icons/goomba.png', + + 'green_mushroom': f'./mario_icons/green_mushroom.png', + 'star': f'./mario_icons/star.png', + 'red_mushroom': f'./mario_icons/red_mushroom.png', + 'coin': f'./mario_icons/coin.png', + 'cloud': f'./mario_icons/cloud.png' +} + +def resize_with_transparency(img, size): + pal = img.getpalette() + width, height = img.size + actual_transp = img.info['actual_transparency'] # XXX This will fail. + + result = Image.new('LA', img.size) + + im = img.load() + res = result.load() + for x in range(width): + for y in range(height): + t = actual_transp[im[x, y]] + color = pal[im[x, y]] + res[x, y] = (color, t) + + return result.resize(size, Image.ANTIALIAS) + + +def PNG_ResizeKeepTransparency(img, new_width=0, new_height=0, resample="LANCZOS", RefFile=''): + # needs PIL + # Inputs: + # - SourceFile = initial PNG file (including the path) + # - ResizedFile = resized PNG file (including the path) + # - new_width = resized width in pixels; if you need % plz include it here: [your%] *initial width + # - new_height = resized hight in pixels ; default = 0 = it will be calculated using new_width + # - resample = "NEAREST", "BILINEAR", "BICUBIC" and "ANTIALIAS"; default = "ANTIALIAS" + # - RefFile = reference file to get the size for resize; default = '' + + img = img.convert("RGBA") # convert to RGBA channels + width, height = img.size # get initial size + + # if there is a reference file to get the new size + if RefFile != '': + imgRef = Image.open(RefFile) + new_width, new_height = imgRef.size + else: + # if we use only the new_width to resize in proportion the new_height + # if you want % of resize please use it into new_width (?% * initial width) + if new_height == 0: + new_height = new_width * width / height + + # split image by channels (bands) and resize by channels + img.load() + bands = img.split() + # resample mode + if resample == "NEAREST": + resample = Image.NEAREST + else: + if resample == "BILINEAR": + resample = Image.BILINEAR + else: + if resample == "BICUBIC": + resample = Image.BICUBIC + else: + if resample == "ANTIALIAS": + resample = Image.ANTIALIAS + else: + if resample == "LANCZOS": + resample = Image.LANCZOS + bands = [b.resize((new_width, new_height), resample) for b in bands] + # merge the channels after individual resize + img = Image.merge('RGBA', bands) + + return img + + +def draw_mario_world(X, Y, agent_x, agent_y, target_x, target_y, agent_icon='goomba', target_icon='green_mushroom', + background_tile='lava', frame_tile='glass'): + """ + This method creates the specified Mario's world. + """ + # Initialize canvas + W, H = 20, 20 + image = Image.new("RGBA", ((X + 2) * W, (Y + 2) * H), (255, 255, 255)) + # Define y offset for PIL + agent_y = - (agent_y - (Y - 1)) + target_y = - (target_y - (Y - 1)) + # Set off-set due to frame_dict + agent_x, agent_y = agent_x + 1, agent_y + 1 + target_x, target_y = target_x + 1, target_y + 1 + # Scale position to tile dimension + agent_x, agent_y = agent_x * W, agent_y * H + target_x, target_y = target_x * W, target_y * H + # Load mario_icons and tiles + agent_icon = Image.open(ICONS[agent_icon]) + target_icon = Image.open(ICONS[target_icon]) + background_tile = Image.open(ICONS[background_tile]) + frame_tile = Image.open(ICONS[frame_tile]) + # Resize mario_icons and tiles to fit the image + + background_tile = background_tile.resize((W, H), Image.LANCZOS) + frame_tile = frame_tile.resize((W, H), Image.LANCZOS) + agent_icon = PNG_ResizeKeepTransparency(agent_icon, new_width=int(W / 2), new_height=int(H / 2), resample="LANCZOS", + RefFile='') + target_icon = PNG_ResizeKeepTransparency(target_icon, new_width=int(W / 2) + 2, new_height=int(H / 2) + 2, + resample="LANCZOS", + RefFile='') + # Define frame_dict tiles left corners + frame_tiles_pos = [] + for i in range(Y + 2): + frame_tiles_pos.append((0, i * H)) + frame_tiles_pos.append(((X+1) * W, i * H)) + frame_tiles_pos.append((i * W, 0)) + frame_tiles_pos.append((i * W, (X+1) * H)) + # Define background_dict tiles left corners + bkg_tiles_pos = [] + for i in range(1, Y + 1): + for j in range(1, X + 1): + bkg_tiles_pos.append((j * W, i * H)) + # Draw frame_dict + for box in frame_tiles_pos: + image.paste(frame_tile, box=box) + # Draw background_dict + for box in bkg_tiles_pos: + image.paste(background_tile, box=box) + # Draw target_dict + # target_box = (target_x + 4, target_y + 4) + # image.paste(target_icon, box=target_box, mask=target_icon) + # Draw agent_dict + agent_box = (agent_x + 5, agent_y + 5) + image.paste(agent_icon, box=agent_box, mask=agent_icon) + + return np.array(image)[:, :, :3] + +def define_program(traj): + """ + Translate the given trajectory in Mario program + + traj: list containing pairs of sequentially 2D Mario coordinates [((x0,y0),(x1,y1)), ((x1,y1),(x2,y2)),...] + """ + program = [] + # Tras + for (x0, y0), (x1, y1) in traj: + if x0 < x1: + program.append("right") + elif x0 > x1: + program.append("left") + elif y0 < y1: + program.append("up") + elif y0 > y1: + program.append("down") + + return program + +def create_mario_dataset(folder): + + # List of 9 pairs of agent positions in a 3x3 grid + position_set = set() + for x_start, y_start in product([0], [0, 1, 2]): + for x_finish, y_finish in [(2,2),(1,2)]: + if x_finish - x_start < 0 or y_finish - y_start < 0: + continue + if x_finish - x_start == 0 and y_finish - y_start == 0: + continue + pos = list() + x_now = x_start + y_now = y_start + pos.append((x_now, y_now)) + while x_finish - x_now > 0: + x_now = x_now + 1 + pos.append((x_now, y_now)) + while y_finish - y_now > 0: + y_now = y_now + 1 + pos.append((x_now, y_now)) + pos = tuple(pos) + if len(pos) > 2: + position_set.add(pos) + + # Order positions list for reproducibility + position_list = list(position_set) + position_list.sort(key=lambda y: (y[0], y[1])) + + positions_move = [] + position = [] + move = [] + for pos in position_list: + p = [] + m = [] + for i in range(len(pos)-1): + m.append(define_program([(pos[i], pos[i+1])])[0]) + p.append(pos[i]) + p.append(pos[len(pos)-1]) + p_m = p + m + p_m = tuple(p_m) + p = tuple(p) + m = tuple(m) + positions_move.append(p_m) + position.append(p) + move.append(m) + + # Create Dataset from positions and moves + if not os.path.exists(folder): + os.makedirs(folder) + + agents = ['peach', 'mario', 'luigi'] + targets = ['coin'] + backgrounds = ['chessboard_blue', 'sea', 'grass', 'chessboard_pink', 'chessboard', 'sand','flowers1','flowers2'] + frames = ['brick', 'brick2', 'brindle', 'brick3', 'glass','concrete','wood'] + images = {'all': []} + moves = {'all': []} + positions = {'all': []} + target_pos = {'all': []} + agent_dict = {'all': []} + target_dict = {'all': []} + background_dict = {'all': []} + frame_dict = {'all': []} + + info = { + 'agent_dict': {'all': {c: 0 for c in agents}}, + 'target_dict': {'all': {o: 0 for o in targets}}, + 'background_dict': {'all': {c: 0 for c in backgrounds}}, + 'frame_dict': {'all': {o: 0 for o in frames}}, + 'pos': {'all': {str(p): 0 for p in position}}, + 'moves': {'all': {str(m): 0 for m in move}}} + + configs = list(product(agents, targets, backgrounds, frames)) + # Order config list for reproducibility + configs.sort() + idxs_split = {'all': []} + tot_imgs = len(configs) * len(positions_move) + + idxs = list(range(tot_imgs)) + random.seed(88888) + idxs_split['all'] = random.choice(idxs, size=tot_imgs, replace=False) + + idx = 0 + for config in tqdm(configs): + (a, t, bg, f) = config + for i in range(len(position)): + p = position[i] + p_list = list(p) + m = move[i] + m_list = list(m) + imgs = [] + imgs_num = 0 + for pos in p: + img = draw_mario_world(X=3, Y=3, agent_x=pos[0], agent_y=pos[1], target_x=2, target_y=2, agent_icon=a, target_icon=t, background_tile=bg, frame_tile=f) + imgs.append(img) + imgs_num = imgs_num + 1 + while imgs_num < 5: + img = draw_mario_world(X=3, Y=3, agent_x=pos[0], agent_y=pos[1], target_x=2, target_y=2, agent_icon=a, + target_icon=t, background_tile=bg, frame_tile=f) + imgs.append(img) + imgs_num = imgs_num + 1 + for dataset in ['all']: + if idx in idxs_split[dataset]: + images[dataset].append(imgs) + moves[dataset].append(m_list) + positions[dataset].append(p_list) + target_pos[dataset].append(pos) + agent_dict[dataset].append(a) + target_dict[dataset].append(t) + background_dict[dataset].append(bg) + frame_dict[dataset].append(f) + info['target_dict'][dataset][t] += 1 + info['frame_dict'][dataset][f] += 1 + info['background_dict'][dataset][bg] += 1 + info['agent_dict'][dataset][a] += 1 + info['pos'][dataset][str(p)] += 1 + info['moves'][dataset][str(m)] += 1 + idx += 1 + # Check dimensions + assert len(images['all']) == tot_imgs + + # Save images, moves and positions + for dataset in ['all']: + np.savez(os.path.join(folder, f'mario_pos.npz'), images=images[dataset], + pos=positions[dataset], target_pos=target_pos[dataset], moves=moves[dataset], agents=agent_dict[dataset], targets=target_dict[dataset], + bkgs=background_dict[dataset], frames=frame_dict[dataset]) + +if __name__ == '__main__': + create_mario_dataset('dataset/mario') \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/README.txt b/StreamLearn/Algorithm/AbdGen/mario_icons/README.txt new file mode 100644 index 0000000..931f8e6 --- /dev/null +++ b/StreamLearn/Algorithm/AbdGen/mario_icons/README.txt @@ -0,0 +1 @@ +Agent's and target's icons source: https://www.deviantart.com/mudkat101/art/Pixel-Super-Mario-Sprites-668185698 \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/agent_icons.png b/StreamLearn/Algorithm/AbdGen/mario_icons/agent_icons.png new file mode 100644 index 0000000000000000000000000000000000000000..1d7aa111127d4365ad1d190eb81544851d0e6460 GIT binary patch literal 74796 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/background_tiles.png b/StreamLearn/Algorithm/AbdGen/mario_icons/background_tiles.png new file mode 100644 index 0000000000000000000000000000000000000000..7d9509f38fd25510442d75e4ebb88a9e217c3bd0 GIT binary patch literal 90957 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 z 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 QzkfgJY8s#`HLwx?1MVI_{Qv*} literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/brick.png b/StreamLearn/Algorithm/AbdGen/mario_icons/brick.png new file mode 100644 index 0000000000000000000000000000000000000000..eba0dc1cbe2d5ef2c3f28f13329524278e3e2f19 GIT binary patch literal 5998 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/brick2.png b/StreamLearn/Algorithm/AbdGen/mario_icons/brick2.png new file mode 100644 index 0000000000000000000000000000000000000000..56be6951c5a51da1d242cf91fb89a6e688c9d1d2 GIT binary patch literal 2685 zcmb_eXH-+!7QP`6n1F)5Q7Hler6Zx(2qhq)7akn`!Ufg1rdHDeC^)=00>L8v2hQxvA3a+D3P8K*dRQLNR1$2W86&uK>JKkU?AF2 zU%MTP4Ge65q^ARoq@z!tPDcmEx3{c6en$o;15N38Adv|ISz2kx7%Fc1y`il|>Jt1z zQa3_l7t@h>xaBeK%FJ+D4eEm7a2ZgYJ@o1bY(?_4=Q-(ACwX~!&jixM>{&6n25|dk z9Wey7y%>zUYV$f?5)WhsoS!yhJpFu?6T4m}Ry1$RG#+eaMtZQO5zm&D%F?B%R1h%+uMC zk@#{j5&{`>fIwbKTUqo!Ahy{B#@MN7K_DZs0ztfwuu&!5Ei?3c0zvbZ6@h?mtCgxC4M#sn|Eo zu~P7NS!5)U!IMG=gy@B#VO(7hL6mR<>=6nMZx9_GAp!vA(Fma!j;CQ^(cvUA6%lQr zzKTEyWsw@L4qJuL!YtG=uI?}!N+ce(+rZGkP~8#=gTc%rj|3yoD7&w8;h%*%fkul! zz~OW{-GFXlK#2^28=0D#!VQh##>P8@h#k}zG7THOgG}AF=H!2VPy1E zzgQe4ie{m%E(-K*t>sB027ix4rhY9;SRh`&RpaS zu|JZC7bYx9%hJevHNf9=-_tV}c|o`nqwypRiWrV3Q&%~r#)jtbpPGJ3_5Fcrxc6_= zA534V=5SG`e(2oVSXQ;dxmZHY;ooL#32lj*CIbK@$`NJl6%CpyvOfD7C1G+z<9c{(ABFn=<>2zuykN?R2Jz9ruRB= zS>+Yv!RM-a0vTE)=X@r&NH&-g+8r@=>vsFU`JqtXDn?;PD@GbaRU_zic)Y&9wBq4> zt8$PvM;i9rc~HRNy%!t5b~#st!LbO9x|6w`y(o?3MJOCBo2(vh?7jZ0V_!{7vxeyx0-264HYz(1dM}xR1`|}B-=Y>`zk$i9!jt~K{sc?J3n|$ z90lEuOsM<fF3lw*2Gp!WXQl_&Lv+P*g$Pwco7!Rz6F9*%Ywb z>`qz0zw9BMi7IUuw)?xOX3mXY44c2|eD!mjwj{#;EEl1D(6&hh#q3IS%Qf&9uYqeC zWl_u?yY&ot+$p?5LzRR!k$k)wwz6q&3L%ZL3ETaLY`hv&*fhqcdL+8|21KXHMUC*` zuy=ZG_X}nob0vNKu>6(Xnr%0F3#6@^4==Kklk>Y;F(-yBdxm;iF_yyNv1KP$_M~Co z8XNhjF|$mVCPwFFNBpFCUNd?~liZYA=ygWn^`ey8-~W6o*2kdl_^ddMNXX8=ar2As z4d|XbE48y=-TR-r=J~Nln`mLkg|68H?Yqx?oXW~kO;Dk9u%?mMTe|T$W4hwAz|at! zzH{ZRcWKv_)}0$5bOaR}_4*Vbdwp2v>ya>|cvYMec1k)0y)w4c7?AIP4I5Ydq@!m>h0;(EU zOSS{fmKC>vBUL|foXel=(WM8*IQN(Mmo_e=brv!X+Fh9To(tJRm~Qe)Wt2r`cO6RQ ze-xV<&2_4!3>iQBz2*|K!Kz`geB(!@oHD;LviOI(UFs&A8s;NFDUPt9*l>9VdVjq8#?E7bm=NN)7_uw!U`lGLYd%jL^5* zBef~#*U~e3>oO(Skc*ejitUUnzgr88!o0lD33k;x_iip#I-cqT?Q740JQSlPyL&i- z6mJcTV-m=6y1F`6+YTshmF`=&&sIa?pLR~<&iAXHi(YV8=+4+WBhRhGhH#Wii?;B-iQ$S}W4pWq$8JK3!U z^O)Z=R!_#IOr?9yhnTRvn#1Bl`uSal{-ejMQ=S8@6nRu6?65=b@)=TX33!olKb0QF^IfdF6lX}*cSs3>q8c&8*Po36vAE7 znVUstWHvVI&@&jJLbA5rK@(Y)ky1N?6CJ3y>7&j{K-DPg>H92m1xQb{7nt;4XV$wr literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/brick3.png b/StreamLearn/Algorithm/AbdGen/mario_icons/brick3.png new file mode 100644 index 0000000000000000000000000000000000000000..885115b5ba5919be6aefa4df2db96bcb29b1b095 GIT binary patch literal 2380 zcmb_dd0dj|7XDZyBPzAYiaw0Ooy&X zs99E4lg`}AB^wvU%w;Mq9kXl^b8FIU5QPElR=?le`ETCu_kHjA&Uv15&ikA{-i!c0 z>QXog4gkPXnl~j-yT|HYeVFzewVV1906+{jnH+F{OeOO&K52Z1*WHG1=5q`cb28qAi@yvjGvqIazG$=k|hF$v~;SbRqwsi6L$XXqhnVoXYh|ofXY)32gA{Gi_&+TT$1U7L?U-2b6&m6 z0~_3i$c=5mM7aDcB&?kLcok{XLSLmH?CX7;a6ADb5AtsJhDsWKhse8Wn~nUbVdODA z5U-$ZF!eu;K8FD&xY81Gsx;_Sa^%o#YOAbmD$7WZO1CLnD}HS`GHio1K}rbtr9&3(T$P`0{-#1M!R@vg*}O|BiW>)Z#Nl+p^OoT)L#(@>wgy+}-EkUCQjk`t;Hux|jG%oniG8?bmR9^(#2qtUK{@Cafc#p@$o`{#y<6bhq= zIGji%!ior3zF;5D!NtV|hj+v|I@)Uy_R$Ai!9LQ&$Up_RhNty zz!tMOyD4lgix;hJ&E3J#(RIGepCX?d{el_%m&`9BUodr@5d8&gmR2O46z&eLxIg(m z;$3k%oxad*VJP!n?Nr?1uDDMVc89;Oc#sDGdR{b&=T0$btf(R90m*<;G%A2uG=kVk zX}A}SCm@=pVzl4rVLQ&|b?IN-b1Pbtd#{qc(x|t;?Z4md=^$LcaiD{NJTm#JUTx|B ztgyyC|AR*gm5}jML%hrEpt9YftHNx<=3c|1+;bjAwdBL=*FCB5p>L3ddeoZM#j8Rr z1$|1TYVxh}MR{L=)xeK${2Ph0pdx=-5=`>-Cgy9#wUyI%es8BNo{8ePQuWP2F&3_Y zG2KhU?ptkjk#&^Ubu4#^S+soe1$kh_eWef{bZd`ut6@Uy^CcCq3@;(xz9>uYP=&(F zSr)3ULDVPCUbacOH1gYT4DsA^(}Ih~&eRPch6ihqn3^gS(C zag-z7qSBJ)ENfX65m(1P{hm|?aa+^Sl2a^>LKoe#_52XuH@aQ=Vq(Kl^ZtwE$F3vx z^>bFJoV}(kP{sN2qxa^T_-CIUYj0g#DD8M=x4TN+YQn`HmA62%>nNlVzF)*EQ_MRD z&*EN>-f9H%Jh6nUxl=eikZw8PCD*t$)xS@8R@Ex%g?c&oZS=4c=U3GZlGE5>P=}u z2GLFIjaO!NRI3v=4>1*eQq?iPoCyoMUw_N4UyHNOCaOB%-1oWN^*@!~eDaQ;8##UP zQK7_YxN3$v;@<{N#LxCgL8{X)>lR0Kbg3dv?w@!aH}wjYaa$f|rqF>(9yrY&c;j`{ zPv)_zYjh+qzdPpE>nCr_t|YzBmmWKX3%5H^s5T|WE)capbzY}NNa^g)r*Cg!o9ZOv9gmFe5ETI zrhE(|aYYm^xJdu}u1r$gu*Lor3JSh95Zb%ddixoYb?R_F=5J12!A~=5k}f|!I(O5! zRJk)Z(k6W3_-JXiQ}G)^H+Oiic=gnNWbmO(W4XlNNpYS2a;Ckk$wTHzm}2TgdHEtg zxF@vIB^jwx_+}kpw#(P4(RiBGRz4HXVT|>1hM*PHs&3JA#`V|CRIkgSqb)ahcD)`U zN&V~97r&Fn)i-*os3UxsBsO7))atA5n$gevQKF}rPCu_kejo6&?{#j^VC3AYlH+`? z#!B%Es?i`cUuMWZ;a=A7^+WcQ?Qlsb?jXUFaCKt!cbt&flJ{NGt!m%S$D#h!`6$Wh zu_ty_%&J5=gI9D3zP(seb^WvGkz; literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/brindle.png b/StreamLearn/Algorithm/AbdGen/mario_icons/brindle.png new file mode 100644 index 0000000000000000000000000000000000000000..88d26d302e1938e329451b82e6037224a624b1fa GIT binary patch literal 5657 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/chessboard.png b/StreamLearn/Algorithm/AbdGen/mario_icons/chessboard.png new file mode 100644 index 0000000000000000000000000000000000000000..e2cbffa4abc76dd1dd85b6812f9a99b904705a3f GIT binary patch literal 1685 zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1|(O@UD?OLz?hin>>QBo?CzYOlV22EkeHn6 zl384klUNyG%)p>AF*zY2$Wuq-L}Fq>!ilrmnp{QYK^+}EK?(II4)C??;Ys4*>O6Ur zwU3d}&Ow#OKuWY`_pt*Ui}YVNov~6i-RikD?!fh=Wgo6~uXmYkaCI@mx+zy4r6~Mm z?+BjE-Q^`AArah=^YqgsCV}0|C&Vhh=DNDkpG-N#g({yUlnJ+y; ztnLXKOSLMVslI-xAt5O-%h;gdkj$cA#R?WWl1I-e9GUgr*x<&y|3Hw}GUIw3TU)Qg z+!$-YBq=c-o~AoIJnCg)Dq^Zltuq>DXzKPhKD=DS#&*wxjqM?~gYCtmnaA7`D&6GN z+1RdC|Nmbf1`MeAXAXQQKlK0qe(}Hm|CbvEe3oTvW3FTnRZM+ndi>&VV3@n4MtG+A z`Z8z%*&GZ^j6w{|Ko%nqGPGwhuz=YN3~E4{fq`iO6I`Tb0W*RPl5~-B^ams&^GE;#L z206>l&<1P)iWtIqHXu{10x~O7b0S3~cnVN@3L+iKNxQ0!1s56jZB? zJ}9J+LJkrwU{PSa*m2qD!{gYF>plFzsbG9N*D;I9k%1-s%!EIu{zRWR z8qUg}7t{3Mmi-GY#g@0xr8ghnOY1Y-c3l2h@jtu%)8|GYV5h4e?ikm>m{Gay|?BsMC7KHuyeitCSAhS z@3o7$wfr})Rja$|-Nr+^|FX?Jw0P3HA!=N7@i~s$&AHBZ@04EH+3{hy@b`ph_P-77 zwYSSZ9m-_4>iu4Oxcf)8TTOmtSNgs!ci;Qh-T8m~P@;7GoR{*#e=dEi-o0?&)9bHG zYu4$DeAa#=HMwlh#ftdS{)4u+`)m{DiSz~j>QBo?CzYOlV22EkeHn6 zl384klUNyG%)p>AF*zY2$Wuq-L}Fq>!ilrmnp{QYK^+}EK?(II4)C??;Ys4*>O6Ur zwU3d}&Ow#OKuWY`_pt*Ui}YVNov~6i-RikD?!fh=Wgo6~uXmYkaCI@mx+zy4r6~Mm z?+BjE-Q^`AArah=^YqgsCV}0|C&Vhh=DNDkpG-N#g({yUlnJ+y; ztnLXKOSLMVslI-xAt5O-%h;gdkj$cA#R?WWl1I-e9GUgr*x<&y|3Hw}GUIw3TU)Qg z+!$-YBq=c-o~AoIJnCg)Dq^Zltuq>DXzKPhKD=DS#&*wxjqM?~gYCtmnaA7`D&6GN z+1RdC|Nmbf1`MeAXAXQQKlK0qe(}Hm|CbvEe3oTvW3FTnRZM+ndi>&VV3@n4MtG+A z`Z8z%*&GZ^j6w{|Ko%nqGPGwhuz=YN3~E4{fq`iO6I`Tb0W*RPl5~-B^am9LmNm2q7CdXh;=p~!>j@_D^hbJ zT{3f1^NN8^voka@u>o6yA%k!zlFkU6I+0|Mbb?%BA}U*#WAFU@$Fq(J=Q=5_m9?-UtY;-jY?z| z$~mQ$N3#`hu3~+N#;)2Av&)h zhmmLdA>lsVhKFGTqiJ`-p7^Y>)-uEM!j;!NAHI$_evGH)%cZtb zKQX&wCfl}K{hV=VyMB&#)pGsq*Qa0cw70R6JO6z7<7X%T z^ZE9}Pt$Ma*zeJ@{neZHRCn&1`UvrNU5D3y;n$MWIv;vSSsYY~c)I$ztaD0e0syit BToC{O literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/chessboard_pink.png b/StreamLearn/Algorithm/AbdGen/mario_icons/chessboard_pink.png new file mode 100644 index 0000000000000000000000000000000000000000..57958ecf90e0e4733b2482e491570aa8f8bdd343 GIT binary patch literal 1723 zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1|(O@UD?OLz?hin>>QBo?CzYOlV22EkeHn6 zl384klUNyG%)p>AF*zY2$Wuq-L}Fq>!ilrmnp{QYK^+}EK?(II4)C??;Ys4*>O6Ur zwU3d}&Ow#OKuWY`_pt*Ui}YVNov~6i-RikD?!fh=Wgo6~uXmYkaCI@mx+zy4r6~Mm z?+BjE-Q^`AArah=^YqgsCV}0|C&Vhh=DNDkpG-N#g({yUlnJ+y; ztnLXKOSLMVslI-xAt5O-%h;gdkj$cA#R?WWl1I-e9GUgr*x<&y|3Hw}GUIw3TU)Qg z+!$-YBq=c-o~AoIJnCg)Dq^Zltuq>DXzKPhKD=DS#&*wxjqM?~gYCtmnaA7`D&6GN z+1RdC|Nmbf1`MeAXAXQQKlK0qe(}Hm|CbvEe3oTvW3FTnRZM+ndi>&VV3@n4MtG+A z`Z8z%*&GZ^j6w{|Ko%nqGPGwhuz=YN3~E4{fq`iO6I`Tb0W*RPl5~-B^am9LmNm2q7CdXh;=p~!>j@_D^hbJ zT{3f1^NN8^voka@u>o6yA%k!zlFkU6I+0|Mbb?%B0zsnsi| z;#@bm>32XeQ%i<}pa832(&pPPHc#2|(>y=FOgnbobNlJ%+sluCwr~FRr^@#8tG{Qy zr8ySHCxpuw%G`Lcf&IV(wsy&ehr0@z5Az%-h~058ku70|-kk>q%my*yc?B~VCDw79 zTbyCwx!x)*lg7~Y`VcRlF~i}~hXbe0T;jd*yF!`be+O$f9K4=7xAbXkzNFa?yW?*T zwElLNezUYM_J`5)`tJ25M?UYnciyJPa{raa^gkOqEgFD!(Pqoy~vx zjm(?l`%ZsI)8DcK@6D;nxw)j1OM|>m~Qt6*5WL)DC}TxzpcY{C3!I!&>d^ zCu`O}t^TEuZ=Lh-%Xi7_d;anjcmC%bd4J(p=s*4+@{S9PHtzeO11tm?7(8A5T-G@y GGywp=SgT(E literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/cloud.png b/StreamLearn/Algorithm/AbdGen/mario_icons/cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..2535406252eb622a429dbeeb6c7b9f3f9afb441e GIT binary patch literal 4528 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 VX9ovkEpcIj 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 F>OV7bCHMdU literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/flowers1.png b/StreamLearn/Algorithm/AbdGen/mario_icons/flowers1.png new file mode 100644 index 0000000000000000000000000000000000000000..8ec4184e0bab490a22f9c6947ff527461e0849d5 GIT binary patch literal 6520 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 SA@a{pS2bmAr5XjRu>S(^cfaxg literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/frame_tiles.png b/StreamLearn/Algorithm/AbdGen/mario_icons/frame_tiles.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e633e51688c9ccf05cb786433c6c7ee9abf25f GIT binary patch literal 67161 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

kM@357+dQLjR=%j+pMf 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/glass.png b/StreamLearn/Algorithm/AbdGen/mario_icons/glass.png new file mode 100644 index 0000000000000000000000000000000000000000..05134aa399753b5a0fe3e78d862bac5c99741dc8 GIT binary patch literal 3907 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 G#Qy?{j5SIC literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/goomba.png b/StreamLearn/Algorithm/AbdGen/mario_icons/goomba.png new file mode 100644 index 0000000000000000000000000000000000000000..e1906b71f16064e0d7bad872cfc600b3df8d37d5 GIT binary patch literal 7097 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%0D2%m%!d4)2q3 z3lc{9owFIFPjs2W=f9yIA0D#fl&aFz@aE1h==&a1E1rrOcPa@Ow?z&w%T|_O^J^A-|m;ih>3|& z7r-%jc5*^#oKB0#g`MSJWJw2;tZ9Uc+krIQd!AC@%G`VzIW|bLclz)9c?zh~#|&<{ z<`wVwyl5h>5&b-Ilgzd3=-*;;0%h)tE*yb;zi%Iu2oN_JTm_IjxDQ$uv>vE@S5xl-}dV<}NFC8yj9jl(L1q42& zxZuTtb+M~6YL>k&I%-UOA89qkl*mxy>+xLievQuh7X^K2pwI5I=_;@2 z(a^7HP#b%q>1FK&JHG5zg|=-g*CwGU4$|F#*Vs(+%5}KA=;1JSiDlor;K00#&qvKp z%Zg9xDMV=DAAik=cpQ@SuaCZ<_z5~wws6^aV>av0U3J!Mh*pPCL#fGug3%YtvsocG z7cbl?Aj_*SX|d))O_;w@?zKqNaMSS;3#AES(-MTi%7w^meJnFr4a>6E_TtLmg5!Ob$(~$&o(}fT zHfEtd2SZGGhyD+jb1!?Ft^FtTr1$I;JYUEBaK4mn-ESMQoElf82$EVKG<}7kjL7F; zZ>Q=J=%#G4^cd|2-y`U>(wd&eMzg%_CwKZxo#*^;m?TFfp*-vLn|$1;*=J%~%d>YQ zZJkihm^=CjjW3>*b+s~4_eZ#r+G|68Y>%NG$j7f709E_V%aCo2Tyx%$pGjx7LVUt3 z4hA{DCEm^)19|iMa!)B57UZf?TqL%wF_DdsB285|1&1~%&U>K5vePnEYvvDob}&Rj zi;iJ+TAQhkJ+(>=HsQ}kMB;DVoXwYiOuH38w{Nz!Zt$_CICrT`kRx}##*T@$cFxIs zrMwbVStn_;;LOAIg(!9UR51||D$lOjtT;`D0}*q&LEkUb&GK~9vLZPJ<@v=yYJ~D1 zueiuyv?L~&nFmKpJonZ_Y|!w+ zU2nmliIdw?ETS=U=TBNgqGwV&rqN|yTvejfJDRl54>2FeuWr6AcT{YOy*B9Nn+O3N zt@nYCH{V0^{BM^()VG4R+ zUtk2oH}n8CqGrUtLQdt#0!O}{{E5-q2m=23W3@q<;C9_PQDY+!L6*jhx_UxjP)tTYS@QbQWhscCVS=-*(CtiJu}QBrMD}bwis@3G2=g`CiAy z@6mtkA<`M&BO=O*r3Yfl}1-Cpj;o zsE>>dd8-a*4M=6FvwYH5jZm=5Xmg3>9>DS|#P}BWrocoXqkJ2UvJV??XC1w{t|6zv zzF@6$>CuTN$%{2h*$)Rlr9}QZEqVS_uN~?qhQZekMAEhr2Uhn(#aHooA~I zT+yw!ouFIIz|Iy&^Ix$!*Y~)`UTQs?nOP+?wP&kV?Yh-T9o?7Lzz?XrqmagKgHcOD zn6fS*9^T5I~r?JqZCjwzwRael!Xk;+>z+ z^zOw3he-KEiR=5iq6lvMbQd z&>0|xv*X>#>3cIe06_76@NwJP8dOW>$nv#blY~2`C|imnDrJ9f$n6H9?A8@o{h%sh z7P}bb?J3C4$z~LrzP!I)M1mQR#~K8avAaYJp+ zzISUF<&pFkm)(kkqodql%d69Jng~*bwW<{76^Bx&ypmS9WRReLcJC(h2;WdLX)pGD zl;se5hWmZtsS2pd!0x&Y_VScZG{}Uf&;~jc4!=8U-mTSolpAGsL}W!jiKI!jNPBky z(Lj7b{D8-GO#{urD1UNd@AEUBsCqpWE{URBrGG{(NO-rT&DtoCtGatCnyt(j7*;>g zx{Vjl&_L|`9`HG3kaw`N>#mcNc*T5kc-Fp7b;Q1&%4e?t6gCl0`n#)3Fz%2Cw$Ig7 xN9B81M%4DP`_xJz&g7Bn~ncXxRC&$;j1 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

FKQgk!3`fjWz;Iv^MQf|g|ws*4{g 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 Si2nH>T2@L)vP#@2@c#hyg$u?2 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/green_mushroom.png b/StreamLearn/Algorithm/AbdGen/mario_icons/green_mushroom.png new file mode 100644 index 0000000000000000000000000000000000000000..042c3cde2e83ead4a291e148e8be2af25ef009a7 GIT binary patch literal 6569 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 HQP6(@kblLt literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/lava.png b/StreamLearn/Algorithm/AbdGen/mario_icons/lava.png new file mode 100644 index 0000000000000000000000000000000000000000..48c828457c96547c2c71b13b2408231adfd9fc65 GIT binary patch literal 7026 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/luigi.png b/StreamLearn/Algorithm/AbdGen/mario_icons/luigi.png new file mode 100644 index 0000000000000000000000000000000000000000..32b01e3ca83f3cd73a29f3d46cb6e2b76e5509cb GIT binary patch literal 7885 zcmZ{JWmFv9vi9KaP6+N~m|D7jEYaR+kH%!*~^or)D+`i#QUNR=qZjVqGxtqqYWUHq)9at%w!HRP#n`n)`GHnl-~o)QLEU{k_+^b z18lbc1P$enTtt&%D41{-a(isC?eB6@(@B& z$B^_{Kg`e=NcL+n5;Iy#eEgs#e0&Z!N>)m?p!ey4>0H2=z||=PCgzGFCgv(0L}H>B z-Y0M3DNoOViHY=nc<|SIY}Ch*juZE;hlh5myN3t&=bGnFF+iYRk(1oBsGF!EEE=@>UcCa(8#< zb$`L@gs=uZ7ZDKwf%!mue85K!ptGl=i>U|D(V696LH=JHSxaYg1pKWF+{uygZ(LI| zCs!8p#O_z=>h-$(EeKfMf+!8|4JwR zH<+jf0&e+O@ZYi|pNs#K;D71pRk7zp=UXnr&Z(?X3Iz{jBw`?(~iQIm*lH z-s2*-2d$Sz53R@ryO&jHueM!Ies<;*=XmX{$KMfuTpSJhfQlDZ#v=9LP5-S>)ZciySuAVuzM7r)%;$`b5i|HIG71y@XW4 zqKk&x>z}^0Zex?xLpFWVn}`!DgJOou6!P|nyTxiK zY=OnVq*bQ%GH20kzR00FT@FXHr@_nrxsnJf9TsQeAQtO5=DOPv68>Q?qb^sKZANCb z3tXMCP;j@AHh#Z&)pmQgn2$L5jE9G}{U}d9UiWbD3A|$U9d&ARIV1B=4(99J<7>8Di~~Zh zQD_^4kET^)IKXUl8^S3Uv@=MZp(w^8??sC=52-jW&y?Y$KHUD0^6Kb!eGyq|WOB({ ziV_P;!v$x8UHpg}TeeF~PXed4Q?E%^lv-_>Gz^kDS_F;0@u@kwbqLNW7U?7lQ4Fur zlhx*o)N=|}5?V$8o5{f^l5t&Re<7AjMqb>qx-MX{NZW6a9(mvD7(lLumEZ-7o_Mfy z%=G!W+`xpz+Euz`s;o8oxt1?2s@&@f9rQa6Kcmn~=oKnAE9##qeYaC5%Oy=Ce~3vK zWN>t;eR^M<1D^nIFnN@%PMkd*6_h&gvnsHKaV%P?b;TY8X3kSXO^GA#!U9WN+qlS_ zfy!GP0~7^ONM^!S?B>4tptmst-^m@W8+E^-_3q>yHI|JR?5jp6*uw+3wtXP2C#4&U zh*-bQN`>BpE0CYA1qUVKu4-{_{4V)q4bV1H7aVTyJ9b6uS(G_J0PFW4g7`)>H6IK$ z*tMqod^X_a2BV}RGg%M3lJ7&&6ECm5*q#%YX8f@uEZEg~M(b!N4m!wF9+2oklv$ipYtFbO6CVw@x+{+#bO~85AT`Mn; z!z@e1sEWeR@pyW<;Nu~@z691+1z4C!t>-R4xJgsbOTKqvdEl->KX-I zAjX2ay(wbMi>9ClUi3&U8I#woti;5x{h1`$CgLMbR%&ROeOi7M^PImT*J{F3)P}^T zEkoql6|fX{H*yPK>?a^ei02XxD(4b09K$)2xQ#w_j5uWH^kCVT4m{@yc_oLdw!#lm zGV9AM@{w0K7UK=04@YrVHSCaX!pQe!Mp*r>}*drU_GJWz} zJ(q!3#|;UArq^S^r98FlzfqsGgH1v7683d3>MS_JdeVIQ3)h`fM@TS|Hiz6Pyk^h5 zmH}G*Ju#`-_wxnWKrTWiw7!`-Gd~xLsO76ySBu%f&8_+kj>Q zGKPIK|4)?9Jek#o;k>?AX9{S8p(f0xt6;Bv#Rk&BO*b$TXRWFzzQRx;PHxA~O z!M%#fSFYAvSehmS4%AySR_S}&<5p;Ps)DrvGM6Y4Se4C}-^43<7{^Fj_m-47*{VDU ztyr%~E71n1P7FtFS{nCo`ROeP)1DSh4c)!BN%EO92K@?Hxj|z#ZNYOLO>$W}#A3S3 z>F2>giLrmHWx}ze3v=abooq(cgl~Ek{lX|9er@|sgo)g^LQ6aNlLBFt+~nP+k^ozq zVrfoJ11c<_LiqTB@38{hRQ^oejK*8da5_FRwbd|NiDxLGfM7D0Qc)=$*%AJZA0^vd z`ew`3Sed>JZy4Ipq}>{H))wrA92!NA9Vh}Ui779Te#ZKcp6h83g#G3%R?8O7BlfbS-o{wTPqM-Aa*$QB(+?j_wV<+ z=+bcT^2_0XCrskH)KSe|v{nV+>v|m$z4Ylg@;sK@0zP|Q7PSixWC3y|2nqGuFlhy| zu_VkLCdyfkgkpsqYS3Ssr!^VynNUSRd*}QR*V}G8o82Oe!`PhWqi|ysobE)2x$22V zhi@2_5L$PSdEt30i?3i;5`pm^L{l|$GY$8fY|#`%Z|IG}-1s1eS6R1;9K+{dF~oOU z%oaIra*=j}uk|6UUL!{E=4C{T%~X`BH0$7ju8MR-sBsmoe6t%EH&!3a62lC%!F>%= z(lsR7M|poZAy-Kn|9+kveb%p)7g9gD+^M8rZbp0DdGlEuKopX!O>W9$O?`H1Q&+Bc z|8pn-ukiNIC%XQ~ZRt96hdsYC&hZ8`%?3Y--ERO>d~SHR1Uu=vv0F;fjjUiPV%OX z{a3~IM+Bupw+}|c*kx-Z ziU15SE}emxQ7XD(0&umEjov+ecb~KORSp>vy5FpFd4%OuyC-i1K(9O+K4%{tV1JWQ zlQjOi_Qa>xh=Sl(*YK6^VS)nv{KfTp+(%d+fTlci7YV%?fdl3 zOWTi7EG#-ak`&x>=fwBZMO3tcB5cHq(Y!s33uR5wOew~n3k1WqZ1vV$6dj14_P`>- z0AwnLS+;45ZttwRGIdZrCv1oC*L8I2h`Rb`_0~4eWD)bj;0lH4Q;It{&&NcBk)cm# zwmLc-IV3QsUr*Ws3)e}B7>Er9KxefBzFgfP{N}#oe0_DP)zq&;e&-6drN)rFuKeuz}GwY})R+AjKfvA97PwnQlw`)7*3_^E;GZ47(`0EPAj>*uR^{ zHQl1!))(gJgNq3eF?4MSm8;MJVgB!(tPQ!9YCMMz;S8_{cR$W;b5}{rc)czHXto;{ z*P&D&MT)6mQHRI*^#?2gBCe|x;Hl=TXp7heamBg!KBFpTiVh{m7OLSA@7oo*sReK& zc}#O(LZ(ruG=BFA*|E~p*Jk?;DGf6|;p-YiU&=@4|JCi5 zVIEl{us=;~Q1KickI%ZS&fH*cHkw)!y#iI1bH7A3>F)ci|J@B$>A7cp`wq~W?Rsfj zGOf`F;X|6ys&nI>=Q9`^+iGD=sb2F6({U;257x;O^D1Lee;FyWICs6{-6d($&XS{i58(t z)C=+z0XuH0Z9gj@5Yh-2H4YMXpiK3iXD&$pLr1wK3Cizo#CK>A^E#?GDviD52t(QO z4DgI$`t4Eex{pkDHjHhdrcyZ!#8^D7Az5s3??4rO9jlBVDm<~JYV%q0cd8nFc5mv7 zyp8WJ`rKD@z~dX?=!1~JBB+so8_Sl2UxWhhT#ba0K)ZH|C)Q}C#%s#1r|^u%#A2~=7w60@V>9hAPt3^52HD@f(1 zR@Isbau3X7eVOQ3s$JYSPk0tCCH*_z?P;}D1z!LSp>~OiQfrxigivWSnLKCqNED5r z%6B}GBHB=UpD7vB$u7~K{G~=Cs4kR$v~$=uJ9O%e0@A#2_HLfPA{b29(G7M_R;6`b zo%FPW86`vTlgqPS?}&P#ONqP??eZQ9gNm04rp`D=5*i*4)<@K)c~AAOin#k2mhk?_ zSH7{Jht=um!*JB@QW87#&k$wBFKM-jzI}XKFZ;qeH@AD{4)1f%OL98$&qPFHFZCH1 zJqFaZKQSfFs;m?4BpeLZeQVG9g7+mIgwi14S(yBMSu3A&OyG+6DM-J4)GXzXFIEgH zk)@JBnu{wRcYS(GVjC_w6^z1t=Qt&i+pA7p-`B69AYB&6dQ+w%R>soMOwzll$O$?X zp8P-}7cR;$+YVI%B8;X-SPhe{Hp`3y$R$dO_Ta!pR>V7M+X*(GL;+190^^)k*^Yo6 zM70w#6tl>4T$SI7zvF9B^puiQQbk!qI!+idPr$yJ%1v3TNtSB<&rmHMi&SCV{#_z) zcV8VmBCn^qbS%SuY@S9p0}v27WEhf{oLULmsq^YUd3s;(mR}l@Wy6-)$=^~_wZ)Vt z1%EC#mlV3p7jsm4RO9V^%%3F^1Q#VB#!rj+V%o1_?eI+21|GObd0?_GAzXRuzwLj} z+JwT@UlfuVHjDi>Ppif{)}gjW=Pr6cfV#jn!@_#8d{Ib}y!=UHgZ~iWpB2L= zrC=$HXO$7eR|tl=RTgu%wb40RU`*`B;4Dg9!JN^8RlC@A0KwJe+pH+5pzC5~-=ZIM zzrDs{=hK024_=?S74bqAjM$3Rfi%@(H z1!pl+nujo3QG1LlE&9*mEY{HU5ku~UrB5h7DHU?ma6D2H_d69Cb){F28Zn17AhgH_Pu0QGV2)}sGMNbdwr*A-_iOjPQ_U;TGHD;Mh zY$J3mQygkdrShKg)`Kzb8KVt@I5rDVq7c($-A2HH^JmC~_c1G7?yT zqZSfZvtH1{%zrOcw=&WRWX>PV43z|8^^GLY$D9=oQ0vT;wPs$#AmFGmz1Qg@mO2U& zx)-EI#8_(TAkxm%-^5`yl)NI^8)uv0K0e3jGFdJtsQZi@QcyA_17h6y_bif|!JB2X zUyx=XWwj9aMo44?_DfQyI~9nrpSfm*$g*{X^pcKn5E#UfNEr2}Xr`WseVU%|a$Cd* zrFRILEBH366(|G7FPP&|~j_EI;Qzx8V zQ?aN-su)bxzhFA&a+t5(MC|aIk$!D2{z3UxwzznVZ_VZ-r4`_lEKv^Zi-p=)EH6~x z9m`MNv*9l_Nev_ zK*I$yAKYlUjI=CS;|Mm$tM@YpKLyPVmcA1WE>tOErbUnh{n8&@|IqmETwZTcSfhDP zyb8nmA;!sPJ?PN=4oN9bg&w0Q5%t5}!m!&- zm#~O5j9C+X$oH4&?KPVF{N)76iph_$3J8uvj_H-Yf`UU3*-OhFrEs!T_8*_~VjCq3 z_q6!TyV2AZTb;*lZb6Jf?JrL=94YBrevc`6Z(+69?{ZOQz!TB}dTY`Ag`Aemyd8fy z_>KgVv*Ple4k^bid)=xpx?a60N75`zU4TF5Y%kmO==ZA-z5H|HE=sd5x7(AU1N$@R zfB&N{~XHkq~3B>2>Bo`{v=2F)W;AR{H*9 zKJg>mNKz(A=FKmrpO(W)0+_ZrtG_3ERu-*hHE3JeWM#Djv?kX)R1#`42WtPc(dzGJ zP3xh`{6fE~-N+3=DPvX0JO|#Rbn__cqz*7M8zJxwsku8{xUzattX2iJV)`lffEZ9j z5JucEE!Cb+eP4b?iBEj2IZ5bgT&e3Od?dCI*hV#Ncm$tX6iQ(=GlDdrUg(%&HgfVlVKMA53mJUVaHS-Ysz?;&~{=#8$q7mXqY5QUOJPzf_`h+)vw@B73DLz(bC%RIy8ba zEuLZ6v#C+@&i1M|eAurA{SD3<%0lf|T7K_NOlaAy$wbPrk|g&_x*ySD!6^Mrt9$|r z#+138kw=@eJ0mivSx!UM9O259xYo+1$$3v$iqfX*Xo91}adzkj=RWi^!sGe0Qiey|xT0&t#l`*d^77zU0mEXN`bf1H&Gp^295y+R@qvk`M zk2mC49)cxgu!7cY9FU(>#p#6%kwpF8%263Dv&C5?Y+a85g*fl2|9uac#-C61Md$h% zW}MtL$O$3GLV_OrJ&LaF1FE?Z%XrtS&g5I>sppkF~CmT_e z+rU`85AkbO*4ZjR==xi1pGfR@hsDPEU?RzD&Zr$23O>cRBG%T{E!M6Nf(|#ug{>b- zSyjuQ1sLXB@S?Gd zpA0{7YB?RZLy<{AqK9*T?}+S%UaI?;ZSV1V8dM!|UzDJ(c~>gAmSmw+mSbPfLb(oq zwEIKf=$E%xL9(94MeDJ1rMM3E)}3b?HubAxF!s7ylByuiwd8tuZ?o-C(HCEPf6FKO ziuyIpqv-BkWxDI!Jp}yw&Il6ojq?9Uez=e2KIC!&d@eC{Oap;cNqCg z_DO*$>6yVqRoa!F^~@<(lpu!R8)MmG?Xg523^BLQ)SRQ!VH&nlo`En~k8lwA(n>-B z$-=16j}e=R;02xo;|P7|Pm1UALCWy%!M%|}Vb6MS;V&}ouSA{CPPr(Mw(?DJMsRh! z_IaB=HN2cRW;S;l%D~q^C%MKdz0z}uTy#*|wOIt3;NrM3-R<5w%|ql&f7oHW)fi)SSE@q2OCt$)8VmL;-&8gI*0%k|uP zrFr}AGUMg#NZl)W$D679`+N7x!^K(5=ouU;g4e6xtO!T|A`-hTl9tq_I3r#fITD_Z zC$Ki#=Xhtd@0M!1-sIC9YxZ6qwhl{HAEPDiLyvk)VLQoKhA4vZ{4*bH(?_eVoAmO^JLuBc&+TS0UN^c@%UaGj~WI5 zfR=Xh^4bpa%JMFbE^s|pOKYftox7`}K$z`b>& zqGEL+K?9xm;jMU9csS96z3A~kptKA#9xo-Se@kB{RzA-`=!iJ8P=jiNdFPzfmy?-y zRSKEBGX;Rkl<8lu>FzP3^pbI7)IcDRUa;fV>3bBy7SsVUb`)`(foxQ(BKdhLnL{S5 zTP!0Dl|+HW0Mr3Pl`$2xjIQ6P1GB1f#9GQ$@(6UG^M{dNp`X61MF6Z(Dw$DJJ{zV4 z!*=dMhw?|xV~Np~A#8;lKHJO(du$*IP=)|+a5r`SjXRy_bBf+Ey6+js0=)A_$j9Xz zo<0|d@itB->y-qN6(t!SUg%FeJXS9C*IviWC!^@66wvSeMW=v7WEB)lpcB{)IWLqr}I!F zRT~Wr0LLT70-yki0jQ4%`1k-K?Eq+hV*r5V@dN-+a#8**kL-k(oh$-c5&jieC=Wd<@RxM{bK--^bvoAPEZd^Iv*!TXLoTQDTco-#2@h=F%JXX z-zFXoQVd2K+H~?Ra44NHH!n9YgES5u9i1fnwT-y0g3`a}k2fg>n1_d}I1i7vw>P)9 z0JjU=mWNMFOpJ$@pNF5H>(PSC{f)DSr4N_0JL5l2{+}NOsJk`X&eg-t#hLDpUrQ?& zPY)>uhChM+z5dD5!_MY^BRRYOtE|TYdH!g4__%p_{u>SIWB31|{n7kG`@61x!b$#d zCawjygFYtwCoO3{$-e{qAKCw=C;7*VxVD`S)X_-6&I#)5{x?UApI4ISzpDN#)%ZWC zyrTby`X8!)Q6+i)bm~7k_s>}VmOjo!8b^}n-!qoR>GV8y1^_^2stU6DKERpG)nsb0 zXTO%W)_B$OOl9Ek4=YpQT0Wo{-Efy^bSbr5YKSPi1n50SX>sTp_#s$Q^vwVxw*G0i zJVyzkE;R6aNwNl@w*GnfvYaYaKdGbM*w_U~c@} zPO)0el9H(S>fJ$#%S^8J&@e@TKy^nOllUk!mfdt$q4;j`qu*xwP5IM>#;;Bh4To2| zS5yfv9=?8SUS}AnYLUD<=~>duk-pc?-f0r(_BLge&e%pU9yEN~vSeh4< ztmZGDp7eMfSSJ`?cP_!&R}d;PFk&vV#-~9@s01?0#kd_l$iALkfsHVcsQvK1_P$lW z9>a;|33byhV!355+wZ9PLbET$m0?uW@A7c{DXZq4drO-%ouYVaz&HC_FPA1$wRDs# zvDp5o#Qu`e;|1thSC_rQwQbjuUC(XV#LUsBn#dfIcAg@elx_}^F)1v&O%+xn;KG7GH z;4Y*5h%LabAPFKWAj-n3*l2&ko2CCm>|CyCfL2471*4#&t~jfOg=8{}VXO{Iy9S?f z&;ctQ-eO&dM+d<0xv`*iqcKJ)W@h5f#}=Db`yrr?_rtK8o;*ZIR?K=JPToyH8C=GS zE^p{I0~~t7z=}#D8Q!Xh@9osF!#-kro>KHpDgADdv4}-H1B({c^cQJxh%J{)mGX>Kh!5fc`au*31L-DBzpHY+R*nH%a<=MDAc2wkp*F$u1*7#wSlVF{he%Pt~;2TkM7 z#!1vY@NSj2AyUM!Z_VBv)8}v$()n8Ly^f}JrskG3de|7vIAWmm(ShGSp=GJVyf2I*z}hIXg~c1 zoxkJX)yN5RDKRFG>+6I*<9s?ZRS8)i z+_gwhZawb9Sc}Qk(ALrsb!HFwiaLX^6>M;L@evD8UvE#&0{emfcx@BX7_rO2E~jj! zxpk6~&&TMNd4*wmry?9NY!32*G`j@LB}*2dBN9)OlHV_(?VA!(Z%iscaw(`fwc*UN zZfUPnovQKY2+eYzJCn(Zn#A+P%EFEDw(>L9Y*60uQsM`l2w z)jI&F&?}d_GC*Gcg92nt0HgCHYO-*5RWt!?yss+Nf9*2EI<&RNMG=bb3A&OO$W7L5$ z+$P{?qS9xb$#`mUM_S}bftTwlQ2!NVcVa4h2OZPe({of7p4$jn5{l{_!#obiS-v_d zFv8HmnSrt2f2ZIvZ+;zgrWItnL$*?D^JOc?ZEZPU{+R|EMg||?ZEIWn{&ei<@rn!P z)ry+cv;l4>tOlxWTCudh&!Ojfs=6=+YIQ(DeeE+5m1&kFNFaz`mWEtSPZ<0WNG`s; zV1(AxXT{H3Jy#<8O`2RW+O*tCgv&ZX9#@p6SGK-j^jo-`eZ1fygEmuV!@H9aVAXOr zUM_1AlvOug82bYYOxmm`BIp% z$a6fU@b0H_(&SueMYyXhY-#;On}6VB%?wOzASXBM-)@aUG)MuoH7%2vhsMWjmgC=+ zQaV{1{*XYgS7g)FW&!pn&QO~(<}(y()w|**l<_VS>uqAebLqdUeH6;feX__`j;0o- zg;v^Kbf-SSc4M!DSxnK{57WSIraG@6twFhG|Dg`$<(F-C=FPs@Ro-wx9y0rlH*&Pg zEHKG@FSqI$IsBM>=`t?uRM?!Au0DYWWI_zqra`$H!!{*FQYLKVnVX!=?d%otKL$Hs ztO*X9p8Ag3jCI&0q!_Vjma6XnQJsk9NxSc&M>i{t>zKna@u}xOLKh-WR<7!z;5=nd zj{}McHSQlgdhrbgazO!*YJRu~sL+MGVgYOjK76w2RF}3+gs#dQ7#wUNHuIzqvu~S~ zAE+L-JINd@)+pI})WseY3cLoGX4FMgf1gKz{Z$v0{IIl@anG73_F8CVow9>x`OQvN zj?csOj+Qf|%SG9~FNsmvtL^S|7&mzRNkqmh^Q;Z0u#{=N6uMzqiDOT2+7*8=J0TzU ztbOA=mQ@nN#cR;)m4YX~YV!nI1G{FblStXb6uSD`?8SCU9-5d69n%JMfU((}f(HGw z^o)!f<*{R`*=rSEqy2~K@LtEZfn&Sy%}D_z?v94k#O97JF{c&(|uGiPPb>H&23w3**%B%bwFyPR0f|ECXY2aD%2r8ri;99LK zX3Xzmlr zk(!f_F$O6%RS;0+vhDx}_~P{{rwgzy76-<1D}^(#P(oIF0c6%+_@7~Fw3fMQp87!E zQ#=b_O@aV+`GRsrEqJWriLQ)gK794+O9j+0AJd5O>9ctaUzF0}C4^Z_D3Zs?cn({j zMD`TN8u+liwhstXTXlgFL`PT(SGlf~I^@%v$UWbGNipU#3)DFO=)OCd<9z7PX6ye% zue^T0iW=Fk$sXix@t=#6zuA z)U%?6D`a0zuXo!4&u;5iMQe3X<;a{d}XHALe>p)EYxE zLnd-=xa_haLh%-T%I2D6ror;_NXO8Tby_=uF)eLJh)@!h(57yxg43(~QtEbjjKE3S z1m}VEMC24qf1zvMSk-^rbgcdY(CMdIycYgRY0xOmqJD9&$~bV{?{^60Y7%>2uEQZW zg0VFKeyk}WEa9X_2$xw?Hi4I})bN}^$Irr-8$_>}@g#?DM#i!5S#fZ2amNLJla+x5 z89Yy;nh`jrdj>&?*5?y-6lfMo15kB2i|qln)O&8`kE1m{EmU7NQglWNe^6NWdTCkq zHO|;N5@gh8hO3&Z)Jrb9zKud&B8$I-e*-5Y4As1i+O#i51SU7t7i?(GUbAl35=(!2 z*S`H%QeHpG2Or#}JRX$a#fo7pVI-r>v$BTvoQ>5#=nl7=bS_l!d5+Ge)$rBcHLAHu zq0zU(vEm5j*8}B7rU;fJ^6QRf6tZwDymkaiQuQF`gUJDoJ+9!<1Zf{^hG;=fb2*)4a-oxoxp#oEhR z#S5ya@@iwHeU2`l%E@4oy_$cww4V- zx?$qjkPZ3+cg)S z#XM86F;_n%x5WC13(~`Xq8+{`V%!P!o6M>QQJbL|LT}y&9JcYyGZNhWxKuwQB}bid z@5CiH^}72_>bI5JI5Q^*304k2t3nRx3Ex&;%}0vM`iIY4F%z~VXC_HXk1(Pk~cQ2=4;b~TOK+rRGqbR4kBO032( zoy+y$tFs&=jK)k2s!{jGub35Ti5JQS)<{~ja7X0&ug`~Ck_?3xF5MS6% z)fpo#J^hxepDf)Lg91^2uSKHAIpN}9y49p?Z>Y0ZkQix;(E35Yb{!> z88=&Dw5RnK0GJs?QG;o9?bPua~JNt|m+XDuGnuA7f$tk>}i zK$~dV4+xIF?o?_7)eEFB15f92ka7nzU~_1_(6E`y?9k#Yr1mL`oh0!y(fX(t&5I34 z`Xv!$dg08HC3Og}i{O2Mp`nec1#Sd90RHYkMz^M9Kr%(t;E+FqE+-sejXe6zn_zgE zmL+x#ZTWg)#$rD_0`?6=&k5buk7mQ3RQHh6TEFsoSlQA--VJtFcbLO-YLcD`wIX)| z8e4~^)3@$*6g73tV6z+=1GV1`GxZFaT5q1~mc1^QO5~$!?dHn;h54z?(?9Y(IRXus zd0noYC=6gKce1Bkl^Zau+k6?z-$J&{kTFC-;v^_$YA5{McC9r^l{C9Tc+L?-=qpKr z7evt4Ta~X&Xxza4E{A!};OS`^+zJlH(kMP>FfsA2M?Bvx|bMXJlTdQ$03ZOl=f zG3seE4Gu1ZTH-G#!u;~JV_R@dqNZ=p9hZg+7N!$+&&%Rw@y*_EdQsRm@H=UNu6)1hjmIe zo_Jm9#l22d>~yT4#*5y!!y>;6S_^K_N|d!T)kbZSD|#!XlEISfn|#WCq0(ap0Z5$I~sYWhj1u&>X|6~*mS6y08-JSKJ)!6 zg(@+HpAHof%Hm0jg!%g3=q(>;6eU5b(Y-@vuTbUgdU*nC@aQB&A5MD7E$)VGv)}ZM zeG(JE!cx~be!kQ$?=IXBHW7nN{F+U&F9wCUYeh;ceftJOH3 zWVPtp7s~kY7Pn}%dWI8e+0ZLsc9y3KCVh=V^{!mHV$3AZzl zgIL`V$w3LE60&SjI#Vij(utaLe_r(B{@VQhsg~qq3-ufGx#=Vg3|h!D3)=D|b*%Vy zGEi#zr~HEG^!%u?#RJA}N0RR7z2Bc|-m;P17z-fPckcZX@Pr zhY);DBeTw)>?4~P{3q-yR@T4a|rEVsVh>;m>CGi*+rfIdc)vp&Y$w{h&S^VGC1=9zZV_z3hmb(=_dq zx{#c@TlbBz`Lg(NO1V0Noi(3=Z&H_Kf9dD>p5Baxeh{6s$c%ZxuWzFY=TRKAIayiv zAmMxEpU7rfrKf`jPmpmU1e>ldg>V9jf$Th79l?HTJTNefDHdjC8uF}+BVe$Z*~mBx6RNw9ModhChFS1P zFJ{CSbaQmn*wG=x53sOe5_IUCqy&MT!@bB5j-#+KAv)d``4*Gj1@nsEbMXzJJkGgN znELeDO-sP@n;6YBv^WJ~Vq(n@$DNB*IP6aN5ke+7A=HsVc=M0IMKXzFTI5G$J!QFM zp5!3-5goY+IfUH4JNS`#c_~~~S##hCBCK=P*k)M%ph6^!1zbHHe0rHqdWh}bQ`k4i z*i`~9qO2KXIkWFB-O&LfF$r-l4`)a}Ipo0uAizp8GyxdQJ>%hAJbnGMoFlRq-XldN zN)#9g`N>c+ zAR)~MyuJqO{ApCtSnqG2{@2$YqUYCFA1<{^3Zy7_KNtckt5e>uQ;&bUZ>1-1t*i{g z{0Ae$z`^3e!2dz8e=eA4I~at2FboX+pB)AUt{Cn=5>PSxf3WmlCI4t}$sZFWJ1sp= zJ!K^!3l}GLu%(N+6}zvK>t6#H5nrJ{(8fJ3W!VUWC-_e61YyKz2@6&L0181i3jyIQ~!7|4G&VA5>0( z|3&>Ds(({OIR19(e{}9&WBEt=XD(u>A{_sju^4Kv*O@a63^}_zNK(rewkuyh*;v;r zYN2aSMYQHki=oB$CRY7aSz5*6Fl-|1Pn?Yw3G(I0e!bfGev0SGA}N~~MDl{#+px&; z+Q-!q+z4`#04kJL;F6xZhu}i)bBfv_V&!Nt-ZptPC4J2xGlGa6PF^-`_YR# z;oz%nB{UL}A36o4t<{c*NTGc(3QYsj5r6M^q8+B54DYbwCxk5&|cKNgeQ^U(bLpZ)4mvauDN&?fxv&UvrQ320Bq% zY|jxfs(^Od-4vyYCplU`Aub^B9@9n_rX)o-5|%jblnEt!8x>@X=z)Skd{x;u%^Ql< z1VW0Ms=$B0F3di2P@#nMs9Y!-z$YvyxSG;nSt#IkTwo)#!r*in)%%UWj=KyG2lrEu z7yk!tQ|NSis(!V({1dEBUqJR$3{uM!BL+{t;o%#2KU)X+#kwv#G);YCe6Bi$A|*b% z_Tx_l!ar}v`UYI0a{#xc!^6Yh9Tw`@iUrjNZu@B8zOQ;NeE)Ee`TThMT8y80Ltib& z%4$e0r!TwGw$|)4rc=jCGldL=8b|PBrPn>`jpoWGGbboWa(!vSJxtcmYi?aT$!ssD z;$}k5%#2}yPxCt}%Kmh2PV(Yke%Y=ezZk0-N^#)OlH!(aIsrPvlUB58>bbl6DE1s} z`f`!=HB({baT>e-5X;1OBeLt>oRUDcZcUy3q`Q;!7>lxdjdFF(iG(_SGRvD7x@}6< zvhhj*-HeUXW@%H20m{0*)qX;oxS8-im4bX%N~x+n9bIHA&{yoZfl+_v%rP%<{PDV- z-*vqUJ`&c4+6x{seO#SPURmkvzI`L{Foq5#>onvBTGAIiAKTpGUa%&iV8ctO9Tv1| zFk#S)k*e?Em(0Zzhn+E6r*{03po)VpE#WvZvxXVS53r5;DOb*X$k=AB|6qVwryi-2 zjSHJ&tKH&W0=gT=gYE-8tKsy)ly7=;+t}vid4Nkqk#}-|f=hlgnyKfWxnJm^V++8SgGYEH6!km;1AUq(U)SS_0xY-Ros%HHtpU zz^ppLl+4CSA3?h#!G$C%M+DXnn9T47y}_XEAEdk`S5w>3#{n8-B*%_aY|{+VM;b|a zNN?zK!tSIW$r$XNK=?w(KJfJeml@F0J5}i7esw_Ijo(5%jYuj}*@@3V!LNotYL7`9 z1J1q~RAtk3r1#;Kxp^8oJ2dX%Pq;zQ@UWLSVa$Q_{tD7zh3bmMl;#!L8}BO2rjU|P zAx@&iS{ zmWzqs&K$_X;yQZAeODnkICx`}p~zFWl{jy34DmaE&;2;CY{(ET5)QIuDpkS@B}mln zd^}}oeIcYx60oeHMrW>Ln=#Mp6th2BT1m!l58q|E!a#}4&?q8^!ZP3vo+H`+rYvix zG}#Ao(hf(&u5i?`W*n3fTrkhlu4o+?p*$psAmSd!W)tPV=_nuX8I89e359>tif6zZ8b?CkgxA{s zd5ZFWmi}=q$x|OlsK2!q*-az9?zAz<*WAcv+mm1S0C2nFUrKMvblPiI;IWiRS?fHE z--$7HlT}&NB(8$B)?)f>DA2gp-)}F}Yq_k2^8sA#u*|-)oS5ZmSDJuDNKVQnPOzNA z^L#V>GT`kE(;SZ{gwx+7=`JjNirM8~1szBDkQEhk98~QV#3jYos(6 z8WNV()K5Xss~kK|3^z=YSm1{6kn`G1EpKxZq^EU6#bA_E!+s3umyNJ~`JBD02|ur9 zu8fm4=zpE&UDb8^tlw6-N&iimXAma|XR;%bJa3PtFm0brUG8@A%i|k6gr|#pqLHut z`BYT&ABIhIsnRoNYP&NI_GjbFxwJ-g2l08o$cx?S2^ptREoe5_F|Nv0ril?Sx!69WN|TM{-Pq>gEF;hFS9c?h?P_CeD;qV8COh>#}QkP7yWpsXe008AvE2E%Y2GiL|$3{wAw12chsiih2It@ZQd50?SZZpVfsx zW}`0XVvFfAfaWp~as&OS4+n-FNe(~YUbnd}`BpQyZj;%eG+61-PUpulAf zF$B|nS)inRNYEB?k?kmZ*1m^*P&nJeH+u5|%WuTAT;3-ZaJeQX?EQe?z&f>=N}Niy z{j;O|D?RGx4V&MpQX%f)PuNaqnGRea6uFtOF2X8&Dny-cXyp)sci>pXxVdKL2|6tS z9rnKJto{~*v1=-JFc*r`WGAXhOTw!fsTvr@Mm#Z<*2yZHwE9C7Z%}QbUx5Q#d+q}3 z?$H;2ZKFb6v_h3$^tu`4h{~t_K);$;p+jY3_}oreBp04NYq^D}iA$kHf z5CjKS2cF|JypuG%Qlyn?o?(T<aG&U{wN>){Sb_SOOYL7cu*zcQzTbfe$&HAk+fpHf2*3kS{Fa>NI=3pzvMPIQCQSOWJ-$74nRl3R-+xK6mywh=^C9D z=3eYXCIsVSuYYkbwVEKQy81*%JV#(ACj6yc2X4^zKANC3vGr*F(a~v-RKqn2E)!X? ziFzS3_Y?n5;+B`0(#@Qt3KZLv(KUR-;!bb8xzl7a1X(04alW<*$7Se&OHeVQiutWq zd?t*LkWj~Bz+-dC_n_dHqMip?VZW;wf|@ZDvzQ*%}ySs zX+#{Oqtx;~4R)=~0XYvWn|yELKEspDK?{m6=YH&Z_C8*VyDnPb`}j+m#1St(-{*86 zBzziRztZXy{5HI+#JkD+yB5D+AV1s;S zi-5DKWQo+|<)n9y)=&`Su8F5POBBm+{+4NnUF?w{jmnslxTHTj5>7x3=h-e$nFoG+ zwx|H({y`NdbHRmw&3td7fDQ{T*G!(C+nd>vl1dPs-}!M01B*LwY9heNra}+t*H~Iq z<`k=^T6`&rKx)~M{f4v>K7(HHHH!l~mD9V{n}qU@CW9biW~0(LGI5R^stblw%SM|z zKYQUR6hd0e^0gGcUk7VFBU?4GQuU7bKdBNS6k&y5KcqWELZZ!6B@{ICOHE|=D)Cf9 zg`JV=)Jr0@P~?z9i(GfW^5sZ}Q9|IAzTI)3KwkOb9Qkbly$8i|RzhA z%!Y(_&)6&xA8(X;Tg>Q+pc#7C8ov{eYkQ`%`M{9DdXtmzcIToNQK;_fK1Iec;r{#5yN?MQOj?CSrbA zcIfe6nz+Uav0WGbo>uAU1zn2PD==K^N@Xm~sY=hG8jP9I^xjF7tU<3)g}&W7+XWZz z<9*!D26`LEw**&y*DyX0ltu+0b3WZwrFq|VS$KOnuI4BoOA2zzbxSUJzv^^)+^)^9 zt|NT;t*o_c)p0`e=6+`@Nql0~Z-X1Fz@WJy=)}R;Q6F;$4N=#)a^O2do$#6+*3D@X z2HWQj9Y&_jNQbZOHAKf5791-}H4G@MBc>xqydznta(Ae;1F#oP$qusH2o|VYp`{p+ zBU-DUNCiHFZCUk&I46oXQRZ(9PoV`}!g_b72=v2|95XK}RQgaL(a%PBa+-SE`%3~#n7nC_-_;}#OLXkS1E+TB z&&ei598f&AE?cFf!43k5NWfX0T=9Hb1bog?<3{LanpPB)pwXZC45nM2i<_ZtoK~Gs zIgw{Hp02LrnDMl@H;_ygzvi0;5kv#Kru9{gt1b^B2@Bi~wrS$Ps7G_|DaKMzb?(W& zNBqaC9fdm>EJX`sutU-o3|xIJI|UbP(+Jb{@iu-9dabgJXe6g(kyO?GK-L59C`H{b zFMO|kuFg>Tz5aWzSEB6aKgHIPCQRk_ywEYSd$j&-z zJUolX!d*qWu2D=9tD0$1_#f{2Xr?3aV84f#Db!am)$89_^QL={8L15iN5Zm6^^ z#?}sq=*)O37duEiuCzy(g?)Av7ea-08Jh>qSUE^FKMonKxF}4`!=cJG;kI$twUH&H zosQUu^TviP)`vFPf~R~SXJnHPS+hSD`6lsxh~BPR)XF&ZhB|HB4~%RSKHWu71V0*+ ziSfr!m=nVg+LR*;UL+aDyi1g1Q8K?AoiamqPYYyn0n!jTHVMQcjTJX^)|Acc@gS1T z>}i4E5J=SpW)e`V0jn)CSWvs2yfm-!eBPlOI+^=JQE(UA0+?c)g8dzrA&!i@gb|-c zw|QwfOD5KR_AmubEZ|*8Q(ccmY2EswLpy!9Uen-;=RF(VMCtKifzt6(GeiEbv~mM= z!pXH+a8lqlp0~}Jwe^R=;N;ARxU$T>s5i$Sd+s7B1lrm-(<0`GAwj$l8#q3aGOlu$94v!+OjB9mAb3|;? zuj?GuaNPFTb5I7R$hoK3eic#hmiwv2=YVw**eiDaOSaBK`bs(?8(k_8#~4cVGjQm= z$F^4|w-~MkHA1!=AHU(@ND2)fAK#;9Eym~C=J_Q4<$15qhU|$35PRGI293Ko{ z%keTf2x&nKoB%~f1ztT}kIy``Y?`HM5Brx@px)2s<5_jdRt_mb){dAnICCCG>ALh~ zl23Mz5Syk^=dHB#>p&p=fv~Y_#R$KROOcovkqi=fR#P-?SL7#0_}$i(`Rm%2^+H}- zBLr1e3YP7Y$OV%|yswWZPdo9H_+Rio-rRUA+7)@sn=pi*AHbKO8p?*Df{z&iZu#_r zp*jQGbKfI$BF%#AYH3-`iO*ssgwnq`j7w?EIqI?4<(4-!(M*MxrM43j?Ek7$6yqjA z4j~cRCf9tY^ey;&8&mr>;eb@eI3m8ICaDQ$Bm4%=lvLG&AJ{idD2F^CT5iVmWgRMg zEAo-Fbf2Eu4HVy5OtlJ7u>RHNA>L_gtBdMVI>+v^Z-ZT4goaZL;jt&{%Z@5z_Zm79 z^lRDV?Gj0UC?JK?%|cX2XPsUO1#LY#tpd^Hxd@y6Ild*g)5N|EAr&Du&MTDuI0W3Q z?;L(t?8+ka-El;I;Zj}&xy(YnZww2JI*l4CY6yE`0jw|YcgHu;bL^T$e14T-h2f*5 zTiR1sq&Haewn%O_(27*Gz?JnkKjs^RW8HNmpr=$4v8P&~$q(IB$sGMMu-;)wOuC?* z82F|n&KAd%W;U$apY`cFuywBX4T|46l{L(u6Cr;^P6gT;8x4>U?(I}Ds5Ux6If8(B zTt!#mAUPAxBE0hYMwMF~$J8$xMVYfabdo0e;Z)py#0Z-!v;3CL|9^R}ycvZt;6Ll>^KPqtoTbuIRZ+yx7E(SMJeq zc1>tCM7mR17wfl`-V%Pxtf|eL=fnu=V}P(dEG;Va;OWzqKnZ4j2q0!S_-f3fyJ=4w2*(1s}UZBBvv zY!>=T#KTqN*kEOhkLG+e1lj=ypE|~^83>z=XkkUx7{$bK;`O@}T_WAQGOiA3 zSTO;UNbz=B!@@;fO7q^?ve6PWjf}-e6Sh@HLeNqYMT?{uYX}6F_ok}z##(hWShE?r zU#=UX?*pNFwM5^3$$gTV;qs zpM4xf75x=jjN|P~*)FwLO3v~HEqg>?axqlUVU25Jqy+uCcs=f=k2eDs9jAgP=5=-e7JxwOS?oU=}IUpn^&NMTfG0H7<$Ae6Ybk*msMy8OJ$;sk^aR)_GRlKx<86tmVBI~C$BWvV}{cPPp z4>FhsFK=z{5%mog^weHmN8KM{p8rp zHL(4d0a*_4M683}?Snq!kFc!w_NgUF@6$>-h_7>^tiSZ@Haq9|NbB0B?uC;y>wL3U z%Kk{zLWDa5rQOP@Ym%clFIy*a396&5Fr}cz4A%~V)j$8*Lsz1{odbw_APLGo0Q6X4 zZz@XlUXidXNB%gLx{8So^J0`j&=B}gTtDn^z!Z%DgcD6B@>=)D*ZkW z>{EN#$eZ-Ff7Vc7KEO<0Jd8Q;({ZbgLAU@?Dy}Z76Y3Qa{Neu3LKx zhhn*{nuk&w#Jy*Xd~BD%EQqwOomQJ_6CcTo2yZ0o|K#hxRvhLv+-E3!%r)l}B*g{6u>COu+^VU{8 zJH2c=)&`K%%V;t>4{M@PfP>xnpSQxZV@fRt+1}wSGV!C>EZO*zNRo@XuIVY3no4 ztZN~h8N4P^S?a5r;UhQk5_yM{ESM!9sYyo4_Unx@n5IpWAv5G_dUoF{^ZD0++tZVo zY>)4btqWbx(^=i`MriU@!R}^e0Dh9Ff}BrRmZq;&JrAdiE-N)3O?QRsQx#=mRZ=Z& z4gFhqy)H`=>AO)wSh=J?O}}hq8B`W~FrJ4oGm>bk;Oy&HQ$C-{9wOb0`x8fbn2G-s zOYQuj^J!`=Z{Xtyeh3rnsCswMbjRl=P zujFh=tT{dlEb-iB^IVw9aa}?$54|P8(yglgTXw?wWi2o&K0tx5k3oxK4k>S1h|VxZ z!QZ}`nQh7?kfVeSIHU;TSZ9i>{F#s2s75|qc_-yZE2uCmk1H2dJ8%B!6l%?_pWjr{ z{|Q4UsxsVK(8YHt-@cVu+$oYoJpR0h=i6qI7iy<(=>z|g=L18k25nFkIKpzg@f$HM z=6!l~e(jfzN>`2Fyp1yZ&GLeh*A3#cV|g{H5S)lIsC*DY8g@VN)FO@u>5=KG?DIQa zeP*87vkp>-%ylGNe9FV#K>*d(2VHq`Ibm;VnV3#WKg~5kVQDW4*csPD=RstOi=+2v zCp7A_Pg2I0R_(^zK=0#4^$$Rzce42MEL!#vhp*4B%)SkT{tfNP^)r5CVrG(2gF#ml zk8XH>zQ-tqkxD1XVMT^d{!XFxd?yYryVcDc!=B~b4o{;oHa0d?6wvmm#Qtb}>um^* zW%WRja?rmyt_&y6#k=Tin{OqUCrP!hN#kLxFe5zO$G*~v!5`%G#x%TxRY%QE_%+28 z#vFx!LE>;?`#D2FXb3(Lg~vTH@gZ2+TC!^@`W3!Za)E-K4&?p!kFmV83aDPnEbPAk D73^Cj literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/red_mushroom.png b/StreamLearn/Algorithm/AbdGen/mario_icons/red_mushroom.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1fbe8f64ea189d10735bce3bf356577281acf9 GIT binary patch literal 5388 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 JQK@Dh{y#XSzdQf{ literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/sand.png b/StreamLearn/Algorithm/AbdGen/mario_icons/sand.png new file mode 100644 index 0000000000000000000000000000000000000000..b34583eeedd62a645c038b159ec67e382e83851b GIT binary patch literal 6165 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/sea.png b/StreamLearn/Algorithm/AbdGen/mario_icons/sea.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2326a14b51d96c300b57f7142bfdd6d894612f GIT binary patch literal 6443 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/star.png b/StreamLearn/Algorithm/AbdGen/mario_icons/star.png new file mode 100644 index 0000000000000000000000000000000000000000..7545f68fe5fd62bff5c50b9be0f8f0c42679226f GIT binary patch literal 4696 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 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/target_icons.png b/StreamLearn/Algorithm/AbdGen/mario_icons/target_icons.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0663248096afcb4a6697989d091aa08be158ec GIT binary patch literal 62153 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 DJ9E)) literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/AbdGen/mario_icons/wood.png b/StreamLearn/Algorithm/AbdGen/mario_icons/wood.png new file mode 100644 index 0000000000000000000000000000000000000000..fc6833c62ffc9256d7c3452fe7b08ac35ab66f4b GIT binary patch literal 78988 zcmZ^~1z42N*Dy?%ASKczNJ@9EG=j7=(kWfiEQrz}0wR)}^=ggUj)74fZA)+I~z`!7Rrmn1yfq|KF_fI0g zyUW?vM;+b0D7z>r={{3ZV$t>SbaZiZz`)>2wX?RSc=m*Az{bYfdSH~7o5&|XKQ=Z| z-x}Q4In+7U*;~-Z^8S;B#X1eyI`)e$j3T{XO|Ha=EQmV(flA8j1dU{fq+yPWQ)L^v zY0u;NC07LR;|57ubpUg7X8TPc1s-1C0~Yh>WTu$|v z<+l{1ppHvKu1NV8IgLg+QC`qsur!e>o~kM_ae-jWS9B;g!^cEh27=lr1>tGBEZtPB zAI0+{H`W6RBqYggzs1nfNJ@wZGPD@KH%TW5z}S-bxG8GKMvxsMgN=Py4*;MT5y2W) zVL@a&8URj@0RVW}*V!4G5V_GQfib>f^YArh9sV6#Z5>RWIcjNP@Z6;dFt9KwFtG1Z zn0J5?wiiyQwp&EH=XpiqJn|avuAft13MoF z2M=FoPd{C*Ix`FmEIF4KCVnPb&!z1=-T7_oJ#8KM1Kqv;qF~4dO5Y{j9sF!q0^Qv_ ze5C{B9{p28`Y!!9Ti_APKUMr(9Qz!`Z%zN^9%9|K9VP5VPTQ=v3Hc#S62NG z`rVt{BWFK9FKGdRfPeu001e7->*el~%89=>eu!Joe@g^}_yq<2 zztH?#9RGjN{+9fU_RqNfB`5p0F=<_wKnFKdWf%85uHJP`UPMYl_8&6;SIPhB=)ahT zz79T0p6+*ue)9iEEdN3N@528#;y)x!{+p!Kf0O*LlK;Z|I|OMRAD267Hh+02FC;7Q zf93tBzO2As2LBhs|CQ!Hxp(4}Cz2KT-%mrHX#5~t2m|9O#xrGw7lD{Z?dHk$n%SjP zWh2XmVE+c^Ns2Ohk1R~=r^_Od9S(TQKBMf`G$Rq15kFrM5=N5oOs;(UR@E-D)pOBS zz37v&K+cqxMJ^-jo2>#jdksI5S#oN0aH4wd}h6n)5Xe z*w1SbIdGIN3mo4FWvN-bzkiY%?;ANls_}x*D^orJ-LwxNj}&K(ny7c3n&#)7nyjkE z`KwOH9ktrYmqCQcRI96O_&V^{2$j|G_DT|c0oHagK)$_<+y_#$5NYTj7m`~4~< znCJM(7=Nn5=gpt9%x4w16~jM%3@7{;9henqC2dd{fKg)KQ zIaG8{b^Z&VWj~TWA^8_V8}R0~&u4ke89(Cv3v=7Gq1G<{!g_^R4M$3~(c;1X5bL~U zPSylgAP9jA;rI^UH=g}VNm12>0}?w@*Ow1R)PhP>mhKsKwxzy}4=q^<|Km#*LlC%v zS6xZ=rI*9XWoq8B_*K`ENj=yWk?C|Du=l4dH8kH=?*vQ@m!5Hj+Nx92@%sJyj!?{h2aja ztnhi3OM!0J4-KD!LrBM&`N#P|7QZN~xd~C$_n1qp#76`GC2Bev-^%l{{7U!*-T5^b zo{y%^Iq;H346zAeTJ&M(&Q5H|I1)?Ww~qM{+5L3QT8Dn8C*^5Vb!RjsYNdW74ddOS z;}{PorOjxzcpwz~8Zr5pD5w(L@TUwhPsgLZlA4Xs>7hli@8NTVRUW=2r^m_jm;}vv zd!(GW9xhlRi(y527L6UsAW^xaN#?|bkVR071)3Rr@ziXKnK4o5>f?F!Z~0JJ#E?=^ z7V)FnBBy(yV{u$0G-6c(b%zP0=k45e`VSawATPjH)6KnLLkVE3<{5lZr8>6bovq2V{vE5T&=}V(;>2d7(Z-mObB`C(+w*z?ARE5pcb+VTik+A? zpbZ0qef>#ZI{E^3VU~v|H3Wp!`K$K*S8(K3@8WEX&Sa(e)BF?9!ava=BtkHzFrzXi zh!yaNmQp>(+RXTmGO&Q|%HGCbn;hA$vfcAAlww^*LF1wwfF0m6Lk!KF1n&FMasKe! zI0=%ToF|L^0l-1T{MeY9Dw0fzGT)OTzOcU((8tfI1@3VdsKW8dJ-+NM- z)nQAY`zEGQ1zZM*oCNeR7;-pz<9>VDwz+37bNN}MfmA=;byP52LxLoilX6LaBgpmr z$&pBfT992Vf=*};@~)%W(dx=L#o02Oy@GKLKHW6)>@}AWlGg6%a?>;C_+9rMmoATY z8F?5@nVB$q3ng@9IrCYHS<6BoJB|?Em)15BjKTC#{RpeEY zr*wV{=+nBD-)sJ3Squ&s%}8^B8{A{)rA_a3^}bn~0UvvEGu1A7aq*S9geQR-iWQBJ zK@4EMp@jXt!7bSe#xq*7i6#xu`0>>@D-CMNZy~3?U9tK4V+R;;8AK_+n~mVPmpWs` z&U*b*=4yHV<>C1oPVJ?KC@rALiS6e%ol6GPZO;d)V&4%Nm`mxB`R}T71alCK?(pm3 zp6Tr$Jk_CZ?ReN4w*(Jvm)82}Uc=g1>a<0rV$(+Ery$Z0(<>`<%HcX_GRi}Rseh`~ z2Po6x%X&I>tm~dF!9Q(Y!NZ`KwSdantk~*b{;~> zHwo@oJhU~L^k2NI#itM(RhIPa1K(A=#uxZkJmj|43g(@JN2A+ zWtDBf$>IQAOjeJgx;;)nkIJp3>i&50t2F+Fbstenb|y=MLaFxy^}#q*rROgVB&y8{ z0U>g&oLULv$Z%$*C})V@M{%i(=`N{$!m0P&O;Zp@RkJJOHNA5v7*>-&I=vh1w*tU0&cZs{8CEo)!yxLzHTe={gkk50Kk4cQ)xd6(! zLOgVHQ!RG>ex4BiiZ?Ek;{1T9!Xp~@IF4h&+ikF(j+5pGkLy;PY?#ceDhn~n}bTCk!@`w z6u70G@u6ZqCM8Ew4)M|N#BVlqK5Tk6*(EL#b#Y30O2bCj+GN=!n%)V%mbo}FUYJ{F zRIt;ZPCt$yNUQuf=AD{801dZ$>Y(2K8cDKBegCd|_iu41qMmZZk`vI05!^Rx)cO5s zI9%G_XtxxeD2qKNX^{CTvUgSc;eO}0&+f3$n;KW=fzJsg5FRO2iY`9#l2x4D5?ft6%Yo;ReCYOqXT=b4OQK4xRzT0H2!Q{Ndeky@XX#a{3BN2vGU&kwP>XEYAP z?yj2^v=bzDc#lJh`GS0Bv{*+PIl2~qzb;EhkHXVRRlxh;8>{FyF=FV%hcC^Pnfqd> zz`DJMJy!k*{W~4mU6AiiC@38Vn;g-`AiS<3c33t|{^4>G(DZl`%&&?)xzvVPobTiv zQh?EJ%Tx7FbNNyBNf0V9Is#6wBNHka4yF0gvbQ`oAdVFoeru_gshEXvu*$aQ#PoG~ zX<-F_csq+JxG^TKyvWdgFyRpigAf1Mzum$gq67E5#iLSs2Es{bju7+MZncfhqyi_W zKqlumy3f=4W4Bw}@_w=w#reGJ>`AThC^F`nTIR^sCRfCN$5JG>{>C=+Vr7GWHPQj2 zEarWrIc)MikvT$*P|^^|D&K2!vnJjl9XJOUX_PcGLdHE32XqBg=gKxcSxK2w@?`7M zaQGpQ^E`z=;&QpB*A#?72P-{aN{da9h+_*wTvk0(n#>!2av-;ZC~&JKOX~InPy#@9 z(XaD=Wa9Y%?8FfMC-iRO?aoFn8R9@|K6mG|`q}bSvNg}uJ~lwjv67g(c4E&&>gF(% zFX=W=$ex_@AvA$upm8I;wzhBWp{Iu9v)_U@Fn&0gY9lK`Qq zCpj2j4PWPOc7m@x(X4fGT3HsjA(z&B%l2!-X)7PiOw9xe%*Q^ZMh(A&N+#D|#NktF*Zr}X2o!RSmf%d_U* z1&{L;LGREutS>JgjD>S@fc@m{)jQmxhXvhtI*^^8AKkwMwV*PQG`@>qU;i+3>Qlvs zr*LiHx@0D(TkL!rIka!rgT-;*iV>EbpmWLmS?*|>Ivnb=#VcSyWgXM+;ST(fZzF^`N7X3^-VmR)`_<{F9Ni;- znKjKcGII^_6U}j;Duv>IF9>~%Xlm=&Dx!Q_eg5;R=~|HIVe7jE5h71$ujsayvtn*5y>Vr5l31KFgQXl=19;~L+y2$c zPobVbN+@jSB}|Vy@qybTk7znhCGO_Mi!W;TabqJsc&%P|z0r~h!j`~AzuRz=8q|P_ zr(B7KJw}nXL2u&;yEJ3y$>86(=J9t@Xwi7n=b*ul34^4aq9gMP@DGXtnqgUMC7~-; z8_XH59!u{w)P3oow-*_w<-pw}0CulHZ)Alwf73WT*$CYzA)xF>T{6n(^e|zW6h;bl z4|Ff)@|2OA{h-f#CoV=^bRPfE2D0q+KmsiOn8SmUzC#YJjwrbs#|F=`C~n+@FoTd3 z4=%SFxMxr{W`yPDh9xF+l-;h=0gjo0ds!Jy+Kc9I z=i5Ta(Bg>52b&+P!gk_uJKi?Tw^QQN=Xlr=j?ef(p^zptHA+Z*b@1<;&<65jiTO21 zNKbSvIM$$YA!Y$Mnf$Tz?-D=9DODD{Oz#u-h??kqdB0kgn5!>)SY~s)L}*doD1^AY zPwPTSl&s;Hchgh4%}X*E`t$U$!kZw0RN{|B3&VAFyTd;W$Rl5T%JO9se6ye`Ip$zd zALl1dRhokkH}S?=?PMf*&#Za|dj%8lX!e6Sf%xDBaeWDt01~r6gM^&=b4r|K3$MWZY>MfK$ebVN zBa)k_xgKh>3u035KVuC_{6fvMYKL2Ex)untH5SJN}yX z_9a+d4=ILohz5rT?2Xj61%AEO71Qy@Drfw|6oBCQP0+LX#ebyhYX5=3&-w>VzsdG_ zCwC8oWP{#qi4~^BN_4b;uCHqP$q_a-R48=Zk1z89lPEmbt1T*gclHDP)6+4rx-e&u z%FUWNkbpKxUuTmZSpJAALf(Bp?w4TAoT2vk9yL1*AqDfac01caIWjFzjFHh(WnDSU z=^k!TJ!l@T$XK3lbx~~w+?E4wC4r9q{*T2#y}Cke`u18i=~u?1oqG~DR15w;sjyT{ z=)bS&Y$}vwNxy6+4b=WxB00U%&g}ZRZpFdK9u`J|sKN;TwfDQTf~;wD#QTz^+@(`D z>PQ=s<4xqH08C zVq&qE{5F$*lUs@6C!*y(ZXBdt+b)BzA6y1W^E%a*@e0}oy+kY;Z=1St zQ2?k7Ru0M=?}Yo2Vx>rx7Z;uHjPF}UE(>XH$KPYKtnvA|-ZGW{mTHUI%L{a`b2X-o|3b_m^B!2*)R0 z+xH0p0jgD$CUHu#EBmPQx1ad98r&Q@DSmeFp*#Ej`Z7fns=uGJh;ThJ#$jF-pCYZ} zso?q1wVpE3s%|0cQrjLan(2JZjeGyqqpz0v9n%VXf~n0c3wwf6lF!5qlfBq^jbwf) z7eYmw7~UjuIJlp_`t{i$-Rfp&_jV6FX@vk>P${&(Xf`|W%*mREJYNVt6rFKH=PyQ? zbraR9MZ6utcs==fh#Z_K@nfaxPC4xy>mF2%vc^u%h0qbPm&yEkA9 zY^V&K#F3;^$ENCJiS;No@{MZa65eQgjDng$zvMqt=5-MThN|o2-se9waCk8Mi7Q<2 zhV5-ke8kgud3%xScENfv(rtXSQw{I|>}2yzo#hBx# zA4>LHp-Fr4B^ba}_O2&)w`aSulGvk)0T6rx2Uw|<|J6PnYS(6%gB0KbVZ#r+<2C)j3;;r5; zHEu|Tf6IR@0!_1!X=SDy+G{z&dv^VAB-2>1ls8AJo~TnhV@x>KySFi(XGVOY0!4Vz zfl0gsii@gc>qwm}*x&H5Wc?xjz)In+e72t+q&#wfRjl(uZvqzVrOP_@Fvs_YblQ7L ze24eHVH~h-o%`#jQu-QT!D<4!23WUqP$l&OonD<{8Gl;~sbfQ~*0zp5+J{>dI@ih1H}XzHWpcv;Rn!RM7otRvgqHbJA;-eI(!JEzh|eF{z!osCrCQu7ku>PK<)}%nrV{SAHXM=Ntut?9BtK!+%wf8lZbzpqa>W7|<;mttY_RD`^`yUIgE2 zzwS|Dt90+n_X}Tv^GXPm6rBdK54_!eL+) zVFg>8|JOk5d;1pqzI?Q6kE`q=F7B3T2eIvdbHVunjmQQze*x_{yY}z#2>8SECguBo zO?PBh{I{`UjIpuc@_mVMcY^}wr-hwq0bRPwjG>?1_~p$pi4y~A498xmla=}&SY@N} ziy_xpu6ws~Ny`YQN6p(|!p5@3$4gL6N~XSI%$VZl4V8SC=avpWA^Y^>&ReucnjJWP zd}{^WeA6bqs{xG0Kgn)o1S8y{Cupi$ zJlZ9zTi)CDTQZm)crWI(w%lv86YkKAs@`90w43JhEZC~h<){0S)=MpA&3K_TqZvQm z(|&w?FAV`hyP!^|e!danzu_teeF^K%jP)#sY;hp0ZdQ5Ri&hIMsG=HFRgP4n&iC;^ zXN0RM@Sew{q%Y4He}K1#7bmS@DSQNSyB*!i_Vw%3DDQpeWCdd>vxhLF z1Q0PO&}q*FZXhB*{B1SDVNRxFB)H$z5Bj*a{ak{Rf~~Q`aUGgtl0Fj4+YQw12p#3i zchUzqcJ5Nu93RoMLyN>VMHTht&S{xJP&n8XXx&MOc(oj-nBf49mh7I1)H)gOC|Em? zHY*EXt}bxaautj0s~F0XrjUV-y|grd%Am|KOJVBI-&DcB){GfVC!9A~ z4Pxb|b}0TZIK+UljTeB($noIYKKy!|uS#a$OMyTB8S){{t~l$vW(DFZg$F31mg5H6yIYAtF8ErC}CQvuV7}%RB|ylx4nAR&P`A_HWy+ z%z#M>-rP95V$3?Nc53!{eh%6BqE#gl(YVTnCen#nu%+3izZAfanY!Dt1TH+m1|OXZ z+EiL1%}?Muf=tBIb)>B8Zs%@8)K0<%$)O+B8z1P$4A;f^G!9#4p>gL?ru5dc&j}_Z zPOgQ2;2nyutlLc(3yz{vUpe_}*6~-C=9rtJ9~_5&d!Z=N-h@n$p9@w4Li7M_rYF`O zgyJ5CJMRL!gDmEMF1Tza0_|6DnJuCiuZ&}U!fh(qoC`BbEE;-2PYvLEKahX`#^4vf z!r1jWNgsYp4DEhLsa8!9$82ZYI0d(H6H_Dz@*WewqBZ!`Bj;2W^=Y#=KW1FQ3Wr0^ z9lU?S+5Q-&wqtX19U`Cm*RO)@M{RbNPP=4%?ntn3{xaL%Q>ci0K2PgU7VvOK;B1KZ z6GhgC{EBitfmN-|uW6fylaQUMHD;4ROPfzzIz!xcJQBjuDGC6THEhjp=StC-R-55M z;@Q`W<4ht)a2gygL-nskdFC62%`8+ff~oWI{QUgr>Mr#0nd%!RyEzrtXhsQ0cw4_W zU9Ia~Qw@p6fkASA)-)=j9OPkVz=ip1QJQrS`^hMde9khiVK`z&LREsJ&UJQhON|kT zeUQsl`|2(narljX+zts=LF&n=M(p9+#&JGkys@8c-zP)_83ZIflrKG=Vuo9lGCtT= zK;l}t@tEzu>w0fVnq!mGr9A5EV7JZWZ8oS!WsUjNxf)|(&hAuOtI zC%9W%Io+JhiPVme zTJ!vcwM~T5?TA$t1Iy;=ZkC9 zuQWbHQWTQw8X5=}*Ph&~g#k`F8Ko)GpC#^eDV?-GB7#nftx^5GBm*lj59fIa_R z=*$a`idKcax1kR~e#2bGeb1jX6U|eTki)G{TH`1rW-<>elJ!wj+ccChN!mEr3$Obf z6bRfDDMF`+#iGlYvGXbC+ROki4>;VrA27eQItlxB{v1mL{iTiuf0X1$Po9z5q5bpE z?3^NWUAN3Ri3XcRzw_JfbCH-W#y!3wDDA>oH{aBeW|C)+3t}%g&?Cr8`@sH7^210* zE5$eZ?2N~X++M+^td*BcboIE&MMW63EsZ{H%3=JTmDFF7s?`CkM+(hq-ykxpgX=_5 zgX9cqr3<|9p&yK1N_h{%~Q2xS;lJp3jnPa~ptekx-9z|*lYF6wjddan8 zd*#=LX19RwI|CkMyG+D%R;so?0@2_*B_HToqsNRvL`T5}SlX5Nac)9UY@V#BB z=?`=C`xcFwF4pPk>0v&wdxrQJk2>V#rq@(@@b!}=yw9q`DBAU#*y>!6(~y zInAewZ}&{p+CO;snE~0MGNl^B*++Ndr%;Q^6r`7z;aNP)R7v6St0xfMV=eWobQ>07 zJ?e~CsT1Rb3PA^~!rl`ENbap=BbtfH(efdIl)rlqKn=Q`)&0zy=_lmk+EH|2FD(?D zFm^7?Ktn^1C4QhvD?8||-bwI0EW6Z?x=Wlhabh2H{A4!SSkiLEqGeMK=$X}(Oh^G@ zx2(e2N`MfT$sJC$3^$(1)V2LJB2G-q*+y%RpvQ%DPoZB$ccY(pD4xH}Uf+HXqtIB2^6UV|$@S$`Nu>%G{fZ5;U%qjV@J^^K8 zdyii+@ZNv7)wdX3^yfuWcA6)DMR(K>;jqXo%4iAj@9Cl-S-*9fvYG1>eO78z2>^Nr zY@C>;N%0fI6W@kf_)ShU(2DsYNiddFa{|;IZ1+F;R64#j%?j=>U6K9GYqv;rk;54l zUo7gk>8y12{XE5o;;1w}JQ`lsPFl9bnX-JNz%4m?l%7G1esP`sR$8G)#a}Yl-*=&F zMj|GvY_!e9jok9*A75zAo)h~W-)FX{zqzsl>_=H2 zx1}i}^cQPgcy(zL#7keaasM7HghivLn_%;BV<{2TlXK6Z3uMq3_PA2wLr?hO3(Kmx zhHu6Dj;4k7xG&1SH!a%0b63lLT$KA>a|IV1O#H!E%6d~LS#AZZ_kI}|Cj9atCN_nbUk#9sedUL%Zcnir=(%`OGB#O046p4OswYO>OoAIo*s_fs^X zduaXrKm;jjrc}vCajByZg3z~<^EgNeo}_j%&yLn!<3nr7=gBk61;L?D<@EY>CAGIC zi<2^cztM+Av_zz@oYYmzO;sjb>k!XX^vPpB3LU8DR+9#K=o zTeKPMb~h-LML8A3O7CrDL|VdVe`*3?Y2a{XUGG-p2?K?lvZzx+xt-H?A6(%jts-N< zk|cw__-mQUEzeDLUm=YQi28~r zI6MgPrcNNsb!2|=XXmITWt9m)pQAg#gPRHCVQc7oo`S2hq4sXQi@@g?=!8p%r~=;`0qgN-%!Iji z&bQcy-S32~6Ne=a@(RKI5^pJkXL#)!Sk(};u>-ts!<3LXm(^`h4hTcopauNTy%*x4 z^?FuY$aExO@Ui7I2K+Mu(y_DR^34n9q2cHG`OY-|CPM*+i2@8L?}Y4H*jr|Z+rhP5 zD0=OD_qLQD8Y0pmD)h()mcf>mOsSYxQr2`cAT@#44AaxI(68(AdT1&(NE@1eDu01> zHY@@;IN)7BPCp0tW!kwwK-Xab$M7-+AOwB=5Nc&z02leF^Ptn}71wz9E?!zsl?m&X z)om&a_jkQZd*4V+x@=kljJ9A7i@W9>{QfT6Ut4x@7Jr$*MzO$em3vQFzSg@WY85Y#l>tv+YfCPD5|wDw}}U;dVy5`rFbU8cl9 z`;|KB!w|&^ojy+?{}GHR9>(J*)9yo*h9*z0 zM0)?P(0$_{YQfB?>!kV)ReHlea)x*g8BFsv`mBz0i1~|IV6>qQHf*cSz+v@nwMpFd zI6Udeee&HsN%CWcXPcyi9TwRR3WNCObzOK*&Fn@Nu|t_bp>K3Jo?7A^(7(Khf8@j% zQsGpZ0k@#Jy+{JjUVVhppTc&zvO_KPL>4S;^KYcsqZX zV29xh6c=5THQ_7P~INcD%GhTz^6xkYO*0|NXTAbHTnKz{37hzR?>A<^E&A7gew3qjlhC5I(#|JKHBE&_Y2qp8}kjt-xVVY{H;i|#zgv+O! zr^**2H+S*=HPn`quKJ78jKUj7R)%oZm)h^no%J!>-&O^_+4Cc;3JKiW6D1IsvIyrk z)IZNBLeVb}foY(93S%L$h4>4w%h_u*fs*~9;310vI70w>$U?=7ofkNDF_FpJxhS7* zea{p_F1rK_qk79{zQPW5+M~De`wZ(-AXH}j22ly(+7}*vQkyo^b-GmxN8# ztM5A$QO;;;vGREy4mHi+fa~Sy_-y>sr53Jr&>(M$3=ABSgslSfT17=KBzuIa(qkV) z3XMTZen4y`_N4&I&3|^&cHdg#q*CQtIu<@mVYgI}htrKnOMHl)IFY%RiE=`;HoPwt zk{apPZRTeVjPQV6%@~a$fkQeN+R4g!xtkHpy692l!hCsp-7*~9L%yS{?{h4TB{KfBvAPm=9=_5Q zWU1S+)=u{2&u6sIrYW3=B(cy)thwKLwF%|JDB;SxF0D=++YC4%te}QAy^xe@$(WMh zRHgJ;icI?f<)vX-H^5d3Z&gP3`>49!U-&{@fI*VBA2 z@Co38o~cMF3M1@2@9TP*KJu599gAz9gXim;$Rg9w5!IIZWO@%3u&Ah=_TK`jWw`E> zBrN;UiLm4IL}1k0qqX?x^TqlRJ~P0x;l9IJ)dMal+8kI5fv}1Hlxc_8m%zsNWyEkQSFG|n)fu)m1AKJTj zQ}fbGF#EmaPd|Rao{g_}YN6T2quB6)y;sJb*2RLj@UT=;%Jy2CIxe>djKz;vmiMLu z35zGk(5|DTZMU%kCcW#a+ZhW_PaAGEJnX(V4&o@DUCLkJ#?b%PHU%{H?WOQzH?%VL zislUSPs;#Jez8rqWfSq2ud*#f$$BOQ7SDL5FAmhv5`aX?gPW~O^y~ZzrLgY01ib#t z8`$>=zV}Umr&zr>y7|(Yd{iD~sckKo<9|F6xZr^0Q*}zOa~zj+!qh1>HO$v*K%e!5 z=X1~n9DE4RTr<~;`Su&v(IefCAMfa$xv}_B3V?BNYNpfr6`Zg=x}dEEjF5(W2s;A1 zgl^oO7}{`fy;{Lj%(}STL62&F6E?!TCk~C;pJ7#gIsJi`)%p=DmhM)2P9+z9jTT`9 zgHBR-{vTSMOKZVXUkm&E{`F4M8JW(~dNZ9xM=kr42>-gp-&h6FNBjAN0_ufs*w0XE zDTybY#j96WIl3`>!$p=EYe@y!H0P7+?e-3vRaf#TgK0p;(E#arAz;*%NF(;a_}Xdd zRp64$-kSF$eXfNTDcj*vrgYFyy{k*cNF0EV20d9)jf9VzfjbfO^x;9Ct88&P&Hx?B z+eGa>N!9EDp;Yz)O;7mU^i=M8$O#GA>_hngbek21;$>4*A(nJSJ zgXQX}(y{q-e`+>5369ro+PsY^-?PrJi#Q1kAS@S^03rJC{^{O*S%JesEduSw}24yYkLf+d=7#3$E9}hGZ%E63XmRh4uhA=FYz_RYS z-bl!GBcbvYY@emctiua?cjhvGhem^7S5N@3Og7(QkiXOiZnrMMBx=kPJF`1f3LV!1 zbzk2rA-mQ1c7$Gsb^%lajX~NIO*AGDPKrSu(51oV_EF|nv@=r3d-U8abOPTm8ImZ| z{Km_uQuIJ|_eT5u*0?z0ZPPQWc#+AcJZqq9c*@Fq{{xXXu1}_12Tm`(FMd`|T4lcI zKg=0T>RlD^xu^N+A~u6vi>|TgtAFkvA+rD)F4qYYZ3ab?%K;#M_w4*`N=DoFrX{m+ zk{oRC#t`WZoGsC^$Q>86P^vceNKRiRI2wH`An0gF9*LVwQ66}AMP+z%HI3F9%0Ik|@Sszmn|szk{dn+IEE$)e#~8q4BoKr^7?7T^1XkN0E}oo&`A5XKt? zua<%7u1<-bKeUm>8g`n#J+$(i=Y=N#6@y<4@lUjWZ3}JYq3q_l)e~(uU@+zY+8FL^ z71QtN{;#pfSGJsf>u(?P-Q?FmydNJZ8cahb?W)*6sc)Je+%%u3SRPl+U49QN6tb(} z6q&L^jU$wtGV#<(goPO1F!^rQ{7AMGr>eZkxx!PYane`UHE^RUaz-_^3?}m^Ib7x& zu<*h#WC;;bnF9<(vLCHhxZc~^DmE|%F%L0EXoU+S^tR`Z(A*HG7Efpeli7@(I>Grd zY}n!E)&^F+nZD)=ligNhP33GUx1U^V-Lv`=joKlP8hrWPK?(qs0><^Dz$1qlxjTEd zH0R@yksA~s-U7bw;6vST0R%4=-~=I0?~Y$A-p{@Bk=VJfEpbM- zOSJJL#p3&7I?E#XFkORQ*G$QH)jMkTARbDv@)=nQ;)5B_`wK`f?6@sz-^)TPA`hye zwIM&b0oTEPo7AK)=MpzMk-cataptl|V_fpKO0mPc`73a0Ru$VAy6KeON_D-e6}epB*= z2{c+;xJYy99*E-?=BJ0G5!5<4LfQn$8=pAUtUAPRy3y~Mxyjfhs*BjD*2^ySVr(0~ zNyjd0Sl#$jUmoUj7o^Wx9saW$;H~t?K;j+2`?9gz;rnz}^)%AoCbiG}?~Y z3huQiIytm5KoTREJuZKw08Xe=v7J-+e2gRV5Z?ctY2x&&JMufV7DQLS7415vNsYFS`dG@_UN}$D%is zr0W_ppSV6WfPxBun1B^t+WY4a&^R3Idvlo@c(HXpe~#RQr$o6FbPb=18g1#k%7Z}~ z8RVJdf<&Do-SBdW@tl;pa|!h@K36#4do?zQOR|hOJ;0<-q4)Q)i5Mbh=rZO1nMSmO z3D$sJyjb9X)Nw)DDw4Q?3DV1Eil^a8^2YF?1BupSgB$$3kGk^{&$oAL_N|EO+r`6z z43gC`)B>}Lb@YJAOsHB=)ajR$=}E}z-qPP|BYyzy-uV?mCZpt&%Q$8YU`Pzbfe^R` z-NM(9r{A~xRhW{&1W77Q9x~ zVC=aL;7d{3B)EQF2@hGr%upu$`t}P7z&T3cXI^05MK*8g;BOhHfLr{{BrMyu!6C`F z&#(5>{KKf~mFn2DGlPc18{F_}eocSP9K@pyIT1Vx{yoX5JPo!E=(VlapO_#{O-*4b zQvjm-GpaoizqbaC+4{DI)<49+!#;xUX$6iI)eydV_?|ecaFl$T4bs_JtaW3)>;>Z~ zlJ>OIRbiX^V;Q6Y`%cu$3*g$+z-?q0RE3ig-65f7I@A)xhDLndSGw&Gi+p z*JHzs5DQX9eRMAJfLlXN8xfX(E|OAzI6DCzYRt(ETQ`4=h&Y8@-ik2mdgYDx*R<`) zj-P45)h>Y*F0}8Rj{XQn2N!v-+qT6k1Kwc=bnI&_aghC?ZeTMB&U}2i|3dZ*NTlx{ z#8(~iMbCTu6Wa5zl4O5jvUtChTybMaC7LUUr;`4ca8z0y&C$viHxaQS&zPnAA&3+v z{UPd)Q-I%|Yv(`&D->^}t%jKP`|fxEuc5340bPc2_~W{2A5*>W_4B%n8zsNp%sb!A zLbcO2W6NTrnP$BXw~O}jOPOV}WT$A4a?>w16D{;Uj+Gy*39Mc=(gho(P6{m_R6Mx# zQL|Sf*V$h?od`=oABU&|vpnK)to0m&+Q{9K2;{(YF{s_ntSMT?9n}xI6xxm>O{2}u zN!4p#g8em9GMS0Ebn_?5vN!DW(yuMU`r(lA)38`>Hrd@#pfa45Q7-5?%va?wa6n(; z#6|&Asx@A-2u)edlcPvvFtN?3g|}FR{5{W4Sq#S^c~O%~fol7vz?~tr{Hx3sz99f% zcOJtad0y}+r;l@MI7xQg^XC2W)5?<(&z5IxD-DEJ!Yg^^q}`@OzHy-p_>)oc?9LaxQ#HC!iT=LA_M@2<6a5B zj2Ds7as%PH=mgJK>$OO+2keO$;eh`36b-DwO0;6M3;!55wf&2ZZ&^8TH%|ej?UZWb z+e4<7C+PLU{g0lXz;`PTH(Fy~_s$rDAI}S{kXo}yiu+%`{MLW1%6rq^sHW>e-jzPP z9{7m&28Y=^-mdAJ(zC{9W^Y?q(Y-fa-%n|w4i0SAJk%lZh~?#xcRXU;+$lzF=eXfan$r#YlEge-1{Hv)5RFy-JqQZbGo7~C z1;@bh2MfL`cS1y;m-zGxGz9VO5}o6zanCu6Q4R{h==rn_00WqM@xdb?z3#i-E=CUD zZ0gy-WmF0fAFb@k4i*rep~>4esyl5gg!bX?zbMYTB!PhvwoFl^MU0Ynl0|ldP()x~ zj01`&Uu~Ptir7FR;>ngMX8~THoyYHk*qa_QyD+U_NxuGc=jVx;KN+740EL6z zHPjf7ZJ(!KuH6!z9cUI_A$4kD)opO{?~mQEp$kC$G^7)W3*!RIN)RpjAR<1UduB?rd(Qmj`{2NHrEo((`AZ@i>gJ; z|7k|5eRV6su%o~1(Nkdr6Ofm-G9Sl}@X=6hQ1vywy7wHnPOd^Ep$MIRH>Y{A=A}UU zrRhViHD@K|cva~4nmfiA$}Fr9&n_Ho#z{VW^R!%PS}pom+BMEU$_@{R#hpJZ0s2|u zgpcUlvLDvWK$F9{?kf%6zICzguEetzT2S4c@s+2#tpJ{rp$SsDHo$1nijLxCwCZ!1 z+ye@lw}UK-1?L6UHhgK*vFC#`sz-V{y=*9B=909IZUA2@d<-|oorNI1KM3^4=?omk zFO5q3yT(}ou!AQFvM;la4KEoXngpe8mlDaq%|+jMuaz$I^E)mWZ~gJazF*}@Mk$8f z)3S>3qHgL!6yKG<8Oy=l%vp3PwEJ`Ef>?G#7mI5R4^^vPoRLmsxMhr1;HbNImT`6t ziF1~0rIKop0AR!t#cK#;FyFKls;kkPJB&#_ydjC$rZ`FmEfj(;UyfW+YGIYNhy}?% zg^UlVzeIfaqw{%_V{2Lg^aec}w|VGEE#Ff7gy_SMSLLA@x{_2okgo7n^o8h19vao5 zeYAmtbH$tIxUXETsQEJN{^pATyGrh|H#pj>AHGUDpfcAGYx&+tNCVOzJDl#Yto7>$nLXg;75wnU{g1cUm?Iy0G-b17NVCZXtSQB3dsy$+|Y?4o}>+VH4z(&g(Tu5_{@l5D)eTY*dHOr<8^QkJ4Y@csC z;t#1l2M_Kz(mDH5d|EFlE3hQ`7>>c{rJWs zN&ibMRJcSTS1~z?lc>f>Es^;daEq2Gox=I^ZQTXw(+6BC}DmoHmn&c&8>0ykkYOqtFW47!_tY(bE_uSudw_rd*( z7gB61-rT9$6ABb#BoZRDp~Mz?T{&D{bZPWDTCYFnPp;Y4Q<9j{wppH-|6tQpnkr-# zBbFm6DX1s`cEGO4@9?e7kiMb2TMr^t4ARV?paD{E8O&TaY6!`;RB(HKu< zH&0K*@Z&NMw*$7N&G0^8@Jq_wW)iaa?zg~g)1@Bwu3}RpFLg@ToTu6 zrIL+piVtn-e9A(U{o(NbSYfsJM|37I8hLl+BOb)|v_ln{`}KZKtA!kfU^Yc}(w6c$ zj(6BcTHBu2^s0WRjt}P_v^wj!A`cfv%={a=Ih&ROMM#MKTIkgC?u1zS(8e!I9m(NOv=I3P_i9 zmvqc9aryr4z5l^{-Z^KTefD1KIe#EWR0n>XY~g>lBo+3%X2%J8PC7bQ8A@dm@822i!aiK z5W`;tT|V1M-6qAGh<(dETFN?N`Hxx`b-^=9&6_OCdbvAl5p3Rj1c$_2)r&WUUshUJ zYespYax*7(&v7j}RAx)7wuo{;gjRnvw?6jBAf6^h_UEUOA<**2RT!(yK!ghxB$=KS z%rukvfUgBwR`Ck9_9S0PA&T?mLu8Ss4%?riF3Rp4qzYV~3x)dBp%cnw=G+>&J^J46 zT}?ztX)rI*r}n8a&#RSf|1ERUVbj{mgW3ObLla7Q`7RKcT#O&gLZVcs^>;|pllY$0K**nW!OT;K5$`z z5(qSg#niY-02V$YEtw|btQnzcE=(KeT-G0Yz5@pmA6C~IVbd_22pI=10bI&Pk;)Qy zDm>3vtwxCzzWBepD@+)<03=YG%FJ5pCd8Wgc8(VCV$}dK;K;$Lc)xg*@u7VNoS4?U zuG;()V=#-JEd!&pDb(lLFz(~Yg*4i|7~D-14kMcJ)z00XXv-ig*GYWJH(CFrJo=qF zpYmar)osq#796QiY13fT+Up&JM#byBv0chXG+fYFm>f!9o{}bd?g~#t-XJ%|QAdhd z6Op$7gi|JY)RYc}yC1pibnb5))0}z}hFn67z_%)I-_3JM@4eu#eZR~c=l64O9`-Hk zu~#dTp8d0Sng8?8O048Y=ImO>OYJCxC(9hD?<4|b-XX}X8b4@WO$l?~h&fzX!|uoc zfjo4%U7IiH)ObH6W-4LZp_13X>+Nj&SlhLy=f7qU-zG*uElY2VU8JEtPD`tg3hv4aLPpK`ZFDRjm8UIx9p#9Iu~~fHBH!p5x1i7!;%u<%KRx z&LaP#BiKk#$lS^ABuW_^eCt(R=yp(**geGe6-S<*a~onk*dl$ME``PgU%i(zcCdM* zn@JevPJNkWLjDi?q9r>p@di&-&R+cOw|_&MGJNHS#}{&NN#OIczS_yC( zoAO8@VX%cWJGS$iM%?;`b}+aUi0a8ySG{B@i?N-d(;dl=Tp0Uvs5fWoQ2C!US6>~* zJ|#v=`TQnEI1UUgw~Sl$OLtj|UACax^hj}r$E}E^Q z%9w=hLN8*YsCMqZJmK@YQHqvAkgw3_4pnDR{eASt-Z-rO=TcMAxm!`G*lH{)RAVG_ z#*C*k#h5J@7VyP;7Z4aqBuU`zD1(ki>5Ye=0Lo-y^S^>*5#jd8Yy0mA#5`m^tB>|; zSJaKy`epsaO|q_h#SyiU!V!WGI@&bV29QfTHjKd4DZ`r$;7lNv-v|Rxb^vdne3%EoW{f=rOJxA!z!V4d7Or!mx!6-(OK1sg32x2VL>357gkl%mF-f-gjDP+Z-j~{A$){3`*b+DL zH5Fbncxhc3E0)sdg|S>9B@Z^iqe<+!9z{^1@rTE_y)m>$*N7N-??!$*eM63thK?1D z2T)i!)Q)WkBPYk^kU37HkQsW_P=HO|{@(|2{wDV$9Vu!vj|vh*;E-RC0|ma_64hIz zN+whmrC~oe}Gpx9(6!A2l6m zuB*TOrCh64RukEffry%OrR4aGFx0QQ5+3GB~EQG9_C$zJ#T$ zDl8}rA?^2kG=IR4$t2vJ{6-;~mUM-cwQ!ATx#FQ(=+!?>55jTMAd(Sx!|xJR4-1g} z0zOi@`!BLfXtnq#zW%t|l3d7b{=>S1pf&}gImTsB>eoT%J`sk}Uk9t)%P{QaW-_sNs?IXh$e(5oh$mhMEq$%;2}dmG*(23bNCq)}!LaQsm>xe=@4^9&u}q zA)Fk)+l7@B73?emxVfS+B*N;}xC08EaXO?x{#5T?u%bkK__Ah|!X2u5hI5pu z7D7`nY*%QrdH?2~0;&MKd@c&y0!~v6Feu^HB}OV%TQ7O+cs=Bp!`Fy&J$lfV%Vyr{ zYJS5n=%2Fy6%YZ5MK5`vZXu;evC;p22O(8FJ1aSmkNu6%{vusjraI07G;Ia%$xfNRz1%d-pID4;MqQE&GB^Dn z1VSokbvw46&F0HmJy(c2pA!#vXcqVU8ZjH+%kqAiKP_rPjbi6v%XXxSLi*LML%CzH zBB{Hu=MsCD><;IaP>F(em!0H4%1Nb5mkq3adu`t%YG37cu>OfN_us+6*$(vnEZHTC zYOMo{vWEgxH8JJrDAzXKZ~Tr*L@EKi{lU}m28iN*(w!t@IWL|;Q>1y?_pGN^r!HXJ zNb8S1oH}jP%vCEaI1&7~!{@ms6^lCZBu(q;5+5aPC8_@AcaR^VP@)ZqR&_qh$jUCDV6Ex{s{u9jhlo@M-#!CNhkmCvU=C=Kn(3&&=Y_u z+PPk5q;>rgOKnfHKur#B@%0;nmJ2Abqq zkNgT!F1vnRtlYFTBt85#2=N~sBjBmsQ-W||WKFgojpg)Paz!m)luHC%n6uGqR8`{L zx8zxB%sX}5x@Nza0-;_W&bP+58HlUbJhHoAuJZSRpS7y5l(t0j9zA^S7+mJ>Vm<>V zH#X5-ayyx#58a{$UGbR^>Msbzsb~&KJV~P$QyNA!-ZVRaIg;YD1`pQZNhUCw$2sur ztc`(0;&I0BzW;_kPmHrp@1l~>5PEMWWP{Owt0&g1RfAF-j%SjaYxyt6w?#6qsqG!!%RY&eB+^4!$Z*Us zV4{zN-0qEC%-eHS-PI4VXZxP8JvYP)$9G1&);8}flSR(P(AXciM3fTVVQ_{Vti|(X zkTBIP?%SvvOFFV*k&_?Od%J0nsP%uBjMZLHMY8oZ{U_JYMaAKUC4-r$9Wx^7p*!L? z^`MgJ%#>v(v{tDy-plmG*Xd!f2KB20{!8>X4`e8P`OZ24djg1k}wxd%32?J?>Dy!Xd z2@F68W5|stjEs_7CM2Sz3L+@^B=FX&9{FJydDIj@VI}eZf|963Q)VozNoT*tRL7g* zv+9ko7RPN_$pX9#Xf!k<-Z*PM+jByC)$%+QH${oLc%Kmt(24)Hv;mBvh>#<`;IlK> z8|GEe{p0!CJ^V{C(3utD8CEqNK$gRtg-^XP#5N@Mzp*xc(M^C%uql>{yahvi z#WeRn8}xx3g$xw4r!ZhpA^o`$UHVkNgD=N;)pB)n^vL;$ef&6PDerFS+f$wCJJrXD zhVhTQkzJU#M09mvFcU|yI%VM7yEmEB_F*1Thj zAFD{oI~nH^XYnr@N2j9uaBW{+=o!urBJ0!H3e;|*u}o&_%Mcqy>h*>p!EHO$YmVCG!HPnVKZObu?yosXgh?=Tb ztg*B(W&c?Bzwj)wf9v~q*W`I0{a0cyy|aw_bY+YED?mmU9teafbn=!JgIf3oZ9$)l z-@d(0X&l<+O3eOTGE=Z5P%E2@|I?#^ys<2t5zyIcFU@+$VlrR8i z=N1j3ZbwsbP!DtJ>io(Fdr;FRq3617>of@?5QO^=J&LH$^YgNLkm%IS0OT3~auGs^ z8^$e$Dh2KB9;PS>!Udt=wrft!6EujQHF&xv$3h&f=L%b6 zu7x3zaE-~;Kpk$BRDKL=slpT5kaYcsyeZB8n#Zf&0y3ElN7)M&j)BrYNN1xvo!#6U zWIh0|nRHXbhr8I%X}u9Zpu&D5cj(;i#W0w06zS~o z>#AoJ;h(hYvdb!zJf097S4ki@!(Sx>djsz@f}sY(RnpQ)O%0PKAKM^i{;LSGF5sZs zg+XU6(~4Pz9>O8a{$yO^k?gV_IZYCLB+hg8&BHV|{!wlyPPJj~s(cd|0E_v=OZ{vU zn35_ObQK~de-$}jQEOjh98u`Z;c#l3M@h~d%Kn4gp1dbK#vWbGggfrwxOKa*V!*o> zt3p8;@Z4Pg=6QOGx|PLyo(p*MM&^_(;GbiLJjY8)u~KvIL~PS#5butNiu5m>NR}I_ zO_>F;!mtiUBiO(`(i9pzrdvj`-A~80Y(<}l2qeg5=RW!+W4Tx&FiF~mgVNO8DD?sV z2>ggnZ@u%9HFzgvT09~WVcST&s~NUd29MmtBI=CPTy=eITzHv<=HSq&PB%f{Tx_?M zt;{WFFYwNvl>g;tEdb=rc&UApW|jW23!A zkd>@NV#w6V(KMYah?=Akez>O(9i%3%Rop}=W_rrDs{TpMM0*m&%u0BqlrCvRH-W2o z@$d0H+2@@tPRuf{R`1e8URiN#2-pzW?Pdb;&=)xqSlaP z+vfeus%75#Y$(-5T<#Q<$#O`$qmj`-ANQtg8>~6y#bP1^zA1wiOa3WQ028R-n=U#x zLJ3%V3w9gSg^22vTYOifLn6}|{{4x^3D{aGziu{&ZJNlGg?M7^}C>(FBb z9ErkE4^+jk|D*(T6{Da@-h32*Z2#V`O9tg!&==tP&{<;aM;bX&c&I1iz{bhQ2@T?5;$%$tC@!Y>C*+*)?9cX_?$5PXeQbH=rTfDpu zel=c9(n0%vP#D^?wlGUzWEkSu|F}O&lXbjlLIjO9-@ zq13l>{^(QC+19+Y!r;!s`#Ge0^Gj!0)v*xzTc7|Esqtj!>-EqC6Be+7^uFy0^~RvO ztD3xxxu^##5^J)U#*5tYo{&$qEQ2iae#9}sM35-}dPp&*FJaDoz&{W@H|!);3nv^9n4(y z_mLm8h*^XJ1%(*(7Tz(2DzwRp9hnH4hAHJTo{sBN-(qSYO3ix&*_%|rr3w_Q@WUpc z*G(5JD`o+;Lr1*5pH;Z__9C;ZX9Tgz-hyU(`QAu2z#OUc8g5Iwvx*Phl6}&X!SHm;C{7R)^cO~I! z#E%6Q3j$K=VBXr~ES0p|h7R*Ku6Bz&aSw0K)E;@hen8{M#>roi=#yy!D{hrA!p&O| zEq#XAzYYcVkf!hexzC-a%X5fU3dhkygQ45U!rIlYSr?&r|Eq|?1I+vK8X2mWCtT$R zhch1kT+m1*+qB(65&XT99a}4q6?%emRJ-Y>iL#d27dOjt)Jc7QleM-wObNLjtNZIv zzOkEBa9zcQ|LYmk2$w_}z{Ngt(*c<=4BllJGmD{I@3{W;mn3u}X^ZME7zE$tmmBbi z!{x-q@P@j;^VRYHfcq0~F)e4{AC|JR&@Q>*-=~{{l&4puHkAi5hTp`H{Vl|Xr;$B_ z2+vT0#0HV#N;Y-%B9sVRI8S_jIo*8tKeU4T3`|um-Ij=-Y3Ls_M#-kA zt6-Mm;3p9RjO>e)D_sra?AQmJG1qBrLOK?5`w3II?}maKM?@<`B;SX)3uTpb&nTpeLs@l+s*u-=%t*=JIl7~gb1QsVGha@ zJLnu;86s2#II&}v`8Kem^^vyJ{Y?n(y;|LO>!|e4uw)~iha-FO5pwnX*Ni%hxO@1}tz<-?o7#<)=_!JX7% z{~b3H4`U%?gT#hj^!E3ZB(n9D38Nqh!D1u-cEFJ%Pt7ZknYgQcWpBXGlxWV>=lD0~ z0YaRyI|G=}?uNk9cwMEq zVZ%#6gMRdfgOlGY#vP2fJ+nn+m-k5?%IGiZqdfvse&LgOR4#V7Q;V#ByXQnL;)4|+ zp2Os|w4e+rkB*Lbodxo`{Kr8ktSTTDM9kpypR3c>j4Dd6qvCxIMuB5 z$y2++#e~mIq(0DM+iw(c#cAz~VcM>V!8;tt^u3EV87R5#r3zM)EvyAHv3}g!egkJm zwhqzFKgvEkP}(B2eyrDK8&+8%5pzXkln)p#K^}QYK>D2-)iD7#sLGBmaJV zX<L`+c`>{VJO4Qd;Jgi}jH(dt@i;;I91oMkHWZyp{cvWAez-zxLQ{;`|2W z+m{exn}3tb>+g5uPae0%2YeYu=8&`#bge=O^r|ecp~g(|#-{^O?4PGJXOOhWl*a$Y zgGz7ZzlbdH@Usmos?5z4@w9yVGWu||@;BEL?RCENCwJGxsrOJ?4mZq&(>Y=4ZyXxc z9!{Qhj^GqVmoz5ou~?cWM`jMqPS*FYYpVLF$WKH-MX>$fyRAncPGQzS{lWx}OD%@G z^aVpaxnLP372)e+7D5t-(&AspfFH8W@K8i`+4OD031rJN>^feE0LQHKc8n$iir`fn<`6I(ru;Xs+yk%JOFj8~UzAjj3J*MwK)6|rfN zVP7B^x81&(xBDZ^r8exUQosuJT`fPe0BehQWpc?E=Qc4$PU&lsGT-X2;rmEucw^9R zL2RJgYb&cQ@m}MM(jj;H`SNqxkL)ka2^9J2Q0g*KXhSmYz%!bf{`k<@``xLpZPfRZ zKCZMg!dn04cy;+cYHHkpucc}=m>-ItkUndGQuE%QEs<66Vr6fLL8pbkht4!B6;6_nDx05=go~8EHRmFL8pN{;=PJBn#m~xBQRb`*#Y% z9Be&s`(4dJ!Y!@a1<{2HkP~qk4R{z)lvRN+spgrhx(~EOHn9XyX{Jw zBpbFtCEIIdQ2M@Au%d3wVeXl~1Qu;)gZs(xN~AURM*{KQ5!LAA%c?;{=Ok;(irA!+ zF<|VYyqvlSml^P4Om%OO1jau*p!K9sXnMdFTWPk}5IIVgW8eEP7#gZN%OU|f$h#4m zAb9C3)8zG;i!DyYTCKoQbK#ZIPHcrKgMfF~p<^KR{aGP*bOOG*r)x|Dl(}c2G!qug%3R5Y(QHFRv@4;LY&U9t-!?ti9+Lk%55OW$S@vK#!O zU7@=)E7|8fN@UIM|2!70U}M^Z^cB-Pyw!OrQqAZ!jP1CIGZEq5cRAxRTsB9XcDQ zL84?;W(PGiRM8-8hPBR8x&lb&RgHJfl)FKM(_9Z(K6-#otLVzyQiXJdx=hb6zw+;M zMsBsJ+o%jEGvj5Xm@27AY4&TeQc{hw$&E_~*CLfUu44f&i3{G){!WWuk6*#OBn~Eq zx^kTI8N=C&q09;hmnNVS(J7v>uI<)>Gn-aGg#Wy%2CXa@hq{dSQ4a27)I=CmiP(4M z*s_LRV|Uk0m=Dw0gVTcd5{DxZGEd7$#hbpl8BHVh5_U;vj4;ZP*L0A^ES+42aGns# zN-rUcR@tlN+vH0edY%Zf zwS%L+JWd9W!dDKlH|<{q_DGKjCa-?mR4cu*0L$xb6!X%`#EWZF_<6`XqmrAQ{QT1! zS{}N>-+Y+Bsh0dX6*9&?e@5LAd|?6Valx*Zdm4J{W>@^o6hs7B-!s6U z_Dk0;_py17LHJ_fEm4($K8h!$;?ugreI>~_d}B!&exfuVbhHYUcZ6)eky33^PJ^bQ3+-%4iF!<$bz0yXU|^&T;GOL+L<&hY zl4$pj69h|F(0+~eOu$CW!G{VX=tS}219U$3T@ZNkMF547&jVm|oAsT9;=&5%CLk=j zU+?$csNuhiGeG>G6@=Lga+XY>bRxrZ2wxpJ3>5d>%|=qB1Z&dqNSPUK{ezfBJ_d#T zmF&?Ok1~f%jR<7n@1;BWlJjlHKv>#uYat2W0cPDdM)y4nVf8l*yPRyG)J;S9qq_IU z>%6xjC@wueeM2*L9YGL_hTF|SE3g69k5FD4d`S9hS_+%ngND_KaGJ6UAN6sJn`zq585OFz5tKL~Gq0 ziBt!v`%QXKlaOD2@uT^!g zNoec)XaF@GmWn<1-}QD-?espH!pfF6&tIcpEL_q;`!UBiNQeET)v6(-Q{twXgCbSk zUo+Mr<1XM5yXhiwvCR?cb=(l?geR{G3s=Sw8A$Qr$*IyUvVH%-V0YTZa;YVbX{5}> zxc512VagsnwwBd1fbe$-4BUzac+wN|ewRK6ZKJ_wCy$emq8#TRB;F&GXaIjYF_mi6 z>?Rsw(N#(D>ETDhfAF7eV0?yV-7L#MSuLcb$c-^HxN@zL_b{;gJ_PpgW=fo=grJ@A zYK0{4-={k0tm25W>vBzk(ZI^~-}jS>UCcWb&ky`6TkNwrW1;q# zh;9POW#}{rr6i)q5N4lgNFO}#_kh!NJHS+ zQl<`hfbcB%rPk_;4618(=)DD3zEF6FE>A4iQKS^7%add=ur|_#ddu@RjlleS)?P9l zPbYL#e?Mco5S|N*p(j)jeztUvK41%}Q%bl*%+Q8WD_#f~LA7CE80TLmNE-73LEy4N zq53=xJ6KM|cat`!QtwIUn1g28^7_rz+Bry7F>|F}H<&*u90FbR5s-T!HIb;2tchN( z6>JVOvE?eH3Vwgf$4NXaY}rp2*GW&ifk@fyyd64RUZDemE^hus_&$gdMFzG{^nJg@ z5>#WR!=C`2@>0x>FkCpO2WAc!GC&3#ek*mmzk>VnMo&yuJ_71!{-Y)ZB7T#`r%TIZ z2TXkvzx9&FWsj>1n-276fJx8Xvvmp%w^Vzfc{%~j-Pc2DRO=)|k?fEL*1)dl&-Z#|ICBlg@xv#(VC2E(T+mkyUYXl}+V z;Fe}Zr=>0S;}BnLG4S1eoz02wgByssF_b2Wt3k@Tq!tZ?+ero54@B)IzEvBJdO%E> zeA3A_GBuOzMBGi7L)JGKRgM^|l1^gxYBN1uvIeQ?I!hirOO7Yxj1{O(3UHq{?pOS& z4-nf*eWjA1WBf2cFciz)&?ir3AO(|9%#ygj*cNS^zA4yUqg&l$O|^GVS2^4XyzD_hQKWA&}C zHG~LwZPzT7U!*c;O7<2wR84|$t3>0mQ8DC1N!lLs)B6`hH1WaGwv6SOJ#&RNH!q+W z?NxLk;lbA4d|l#Cm6|}3{BUwf+EXNxR}bl`p#xQ8%qADtOxapcRa4g?gW*25WR|Z9 z2|K=tQEeL&J8;$RDX8JP5YZjAYoyX`yGFZB%AD`sX4X*QO3jB(g?%Ulw|wsO;vys@ad#Sth(l4C3gF> zaK=qOGumDIBy#w1{4OylTLnbU54%|8#JS#(nn-IfEHc&D2K2mt1YM5@U;g^IEuDg? zB(DTP#Ss-MIej_7ZOWS1nUm9)b9&kc?uM3r8@Z~7QD8TTZgjg(>|x0Lb8j1Pn`~)g zZwatQV(D@<4a_33+iU5~FnHS6Ozpjehl4E%9XeBHiIZwk}3Gr6f5 z99zG4>$TY)tD|-^29?3LrebAG<=O^uabova1+Dgy9ESVo0$Th@t4-|!4|10@#1+k! zCw>EhAvsNkm`RdY{O_Z)knO8U`O9UnLZctE{B$T!C?X;`P(wo#NWsJl!`ftj&cV;w z&5=^8sWJI@^_Y`A&R<|1`z|oEJ_fkBOh=dwHy%Bm&>ss##j->;)d_uE6n@RCM?9?d z)je{0iyy&LMh^Umcf4U=>DD0P9QPij=?jd8VW40LHcdn!1Gy+;C4v~5f_46dRcl@y zioCzEee1`M3~)gL9iR_?iGCMPo{$ER%NkpMIy`}dfn+n&7j1V3PI6!ZlM+T(;!h*a zC{#X2OLEf`O22gy1k)hOrr9=X#$NTM6s!CwOrAwp67C<;!}ZB~w(=r?xCM7MkS6iCDV$GQs73npEM$b8FRuU>eJwF-+MDXn|71t)H-}rnB@(O#D8wi(4 z0RKr8)+v*0aKFzO{(It{mamY?Nx1!DRN_YA^{sXp^SiA0xcrGeRETQOyX|8?=sqd= zWi)%2(+6(>Q1OLJ-S^EYF)b;|lH{O^Ao93SVQ^-?;ZI`~ml18@7)ZiomX?c{EAp+Cxr0Ts$DE3Um%-^YL7brqT zME7y~g^>8Co7^v0FAyRL9VKVoq?qg}BP2rgHEk*N(%JzZrJ;IB@%X}22otn+Thl6( zu@=`*c06&!C<3{-d%*&Z+L14u)ynTnOfix+?!6J9TG%uP)(!XMAV)+5llT6OJl-g4 zf0)Uc*PtF|ad8A~@=MAASHX+GJ8i{(~YoT9zHy5F1G`d1^6_y z^OG-6vKxt3Fux&2?_0pAd+=Cb$0Og-_iw>hJc)c4Qm(f*zgFoI8B4xxM6B)M%wp4y z`iUcTJENYHNh4{AoNQ*)FG>=g{ckdEx&`eR(X=fnD>ddr2-L+8%Z>a=+_x446HHA> zn2ZKHu-MxmO+-SE9t*r?@X2YPA&K(`=5^z*(GAHy)x?GsxVHg1$k@#ffAy~a%~XvJ z(a6013D>kxdtokT1j;VA9ML%4hd(u67Qp2q{^Qh-iA-xw&ov)8>U^mqRLA4tXxr@4r({Ni}m`PCYig z3R+nKDdC;J6Fe&sJg@AC4EtLgKmna+I_~@H!fXv^Wr5W%W@rHWN)35?uGsi86(n8PckE>l3I28rG2icl| z2zfWO_wP#w?B1bnv$LHGsji4`B^KO*@@?zVs*Xf*wfRbm9AybsbgQqGZ1(ByQpLT9 zvgXy^IA%=l$+&ML+s-f_<GQ(cRxujH8uomuO(nP8^9Ae$%HG$nG=1Stg3zGv%NEcTK= zE=M>hU?j@w#Bz7V!)dlOr&mYJk;k=u15tymLKQgRe&h;B&K$nC?FM@@IB+aNx;O4f zYbzX)YN8UwUz&iLvxO#a^lR$X?uIy;V%B-d-?$`Dnd4NNRGvDL--G_6RV9o-)vai` z%=0Aa&+k1WnP01eWJ$a5}X+Z~W>eb6hQ-PslB(ZOsl=l6l})wH}a z>%(O$*jxPb4r}x)g+DO-VqgxeIqP{M_8w>tn*?$`HL707BGHarFH8P)^eV6@F)CK{ zXU7(Qcc?6WRhn)+AFH#Pxww+$T0W3skY~4>m&!HkR%*bhI+9Y-B%*}y9uF48aXjP9 zD*KWyzZ2TmkES!w&P}9RN8$NWPH3LiDeP4G`AOXJ!Fa`_>}z3!1k!{|MObqrVG-Tb zpU~1{`0>I{Rm?FXT;Fwd45@wRk^&Y3nI{fUU-?`-;?qdKxTWuC$lq~*RGd$rCZAYp zd@ctruQgpQJHuB+AyP@Aqy}_;za5miqm8}toH>Snq0UpiQVG<4F?UR^x}K`8zlqzn zu#5U)dAu+qKO?^v^cLy)rBn;${yXfoISX9q*C%-!(F(-pvfLL11UdV*_XUQ=r8DV5 zS_C)68lT1hvVEDuW5xq?k%^GCv~RPG{hxQ}Zc}sUMX(WLW^x-!i(qv`+*RMzQodF= z!qw*lt8IUpPrvkYsrAMv((fs}si~hXfpTVRQ*t!0uAJ5&9glz})bj+K2A>R;=mvc7 z^Ue8U(3ZyQi(k*=s;%QFef_T(MP_x5riG?ggfEP^V3su=V79i{`HE0ID@WEZ9Y&^5 zkmD(~^!BXY$A-ats$HkNJPE^fod4$AkpJ#HxkZM zqnK1&R$i(vXEp~B>D5rOFFP*HZ%eeDb>rf~)lS<=|8mq5GW~&?3`>1t2BbRHi+}vb&9^NP$p< z(X&<{=XRy^d{@_-Qhy0{z*+7MPsc4phwY@M{vc!4lj@+^QPZC~3OogKNO%J-vJ$4v zPv!U}uX38a$O{ALyj@Fv$IX$65PmLAZU&F56Ie@R&V3(ltPzO00XoCdmY&9gN=I;c zcRLS*-KWbB%;oB??&$76V7pzjev#Gw*q}}mOZA)4s$5C9pOC)!+q=+F5=mQt`fduKR5$l!C$`sjai=-_g zneIWY>=(zNS_;1yV%Unv(>$#>3&>VQ~A6^RN91{50OUhn0?Cw3KMOAdqg?TQ+5@ zoYFtbYhZU6ti|B`;Kb(R!iZ9z;mk)TNxfW*!ucf!a70|&7Jxyo0n!;!pxlv4TW1Nj zmD?&E@1nK?)p*1LF$#>92*OPUtLe2^T1!n6E{MpJ3KZP z$h|u(-Sx#jI$GpOQ;%$XI;(aYgm4r z6nJGD4TMQ{XU)TMgX8?9Q4E`4k>Qiy(cFThBV2Beww&+wuCf;HLK;KxG;3~tmk^i_ z)lGT6lok&~?bJ7bpaBxw`_Semlv^Q42KS*67|8BR6d7|qUfDB%J$ zwfzwq%qEq8(9fX@=n!}WF(5+2gW)yMhx|Rm!s3jtxh=C}DCZJp$e{GX@!#(Z%hw&~ zPRb74%wXl4bv;Dsyq34({6GC3pRRSB(sV|XDi{eYfbv1O(Ruf>KI8iiby)$1qu-L< z7z4fnm$InsRD(ILQrm*JDH}A{@?hhDnniQ$^FEXnos)dPwKdzC?voUR`c)+>{tW(4 zhUjP$N%~X^-5r6R5KZG=V+zXsjHWb4>4Y`w6FNJYDFSSI8xc`Q;*t#gOT`&d%cMUcDKSD+v@YkWRXA5W{1EnibUcI?~FYhBH^+Mz3x^c!iQ z56D+$OF4MidWE=Ak56GOKZgvVQt%3bN!40LNWF0582^ee z(ed~xLnRZx5r#@IN5}K)q&g$8u2|!znH(k6dYWswlx!VQ7BKgKoY-KNv-{rr<;#DZ zd{`cYqn23rN#ptMQ?0+Pk}(YQMoRSZIu#=a^`)`rDg{~;YEU@?Rgc6q=3fn>?=;_2 zg{gARw&>WrDV#BaN{P7(Jb#o+`_Tw_`!I`!34#}+PlnNP$L$zc2Xn)P9wD>C2PJU6 z4C(C8$@3`^qxa#``$XL;=qLFUz|J%WH;!fth>Cahw;FruR-S6CaTiD?VbSeDc1kz- zj3Pv!K#gl>cGgi&>T#JzzfZ-{$7wLgG^-(<8F%_>de7!ZJQp85N7LdjTbQ^|K1LYvsj-sHi@; zc%qvI`*R504~oBraBw$45YNv&aT)f|zyHR<&HnB^u9U}ahOG5_mKf{zY-COgV z#>8_?A+mNmEHH|0Pg#viZd7wyN|ucpUMAXzfT+~75_LIIEdsQcWCYmtcnz) zM2$+w_$zwQDr!uS{@ZVdt{rFz4|qnZ(luSfa32cr9y)LAGq(fatqJ@wM3DzG#cp@* zzIfyK5W4o&sYFJULi3UGM6awd$8YiL*l&Vslj`(I z>Ioe{199nmH&-F*7dL!JLr25g{hjl{TRN*}?;Q~+I}Mm1_VZ}n(!Kp& zLs3)nsZ@boVh5;!(1d`npXD+VkJ7S^kA_L;_4~c&yQ|7B{qitL6^REYwV&?(%iMn| zFxfdpj3Yvl!Bl0I+^O$C1Lk{gPw@mp(JTSyu4M@WQgO&wEG*4732B+SgJ5*mpk0)0Q_%KLra=Cj(d(`&GJr`M-JMM6tG=s?;z3NApg8qtGcU;$kero$BRJb!C9XN3>N_2b~NN3 zQQIxnWi7B65l5zfXd?BD9rPOMr$qg9Pw_^YZrAD+zj%9GQ)zqre>4kP?s*Q2|K-X;_fXl?Fi? zDFNwbk?!tBT9B58rBmtd?#^A<-RIl)obUVt&o$RP^P9Zqo?-|FiTP?LYWiQ4^zllv z?+G#=SrVvaI~51RhcXzkt8&ZpFaVO*G+#=qS zAG6sufk00}_vV?RA7R@g0hupTbvR$W6oV$S5_q6PGDFq;9kKt%1)wa<-H_o}#b++7x5=o|Cb0#NNJ8KNvrxBz*o#molhx(r2#TtYS-1&_Z5^xUvu zSv_(aBzt0{DcLS02@O#SVLjuMH|MKaF?g@U_Eh8j8~aj2Vb-9IJwkoQE$Bv-AECuZ z6@DaUWL*P@?uYMmbzL~|`;>8Zoa9e(+$TmO?mo6a;1exBAB4$QZ@+XFbM$$=lViE1 zQ6^$M-)~4~WunlT#DU%xUOs7=scg&y+)NgE`%)!GRgEGdiLoEGoQzkBFG$^e(q6ap zumAjRr5)|nYPFRx55p1n-}mc*OhN1ZKwXrXmCS1(ypH{gZA5|>pHVEHRsgNF$G7Z^ zdN}Jt>`gqW%I^Y{2VAHZa+BT%9@2%?KdsOA?$ zERr`nit;k{QzA55O`G4tgoVU};LDYA<9!Z-A0~s+(f%)^Y&_A@_w20|x!0-hh)icJ@5=Q=w4$SJSUc~td-V6Vwq zl4#RhYR!>l8)W#+lj z+dmgM9|BlK5pyOC2VK$vS!+KD(#^A6;(8otcA^IHQH~au>t|o!P=7y1nhPt-Fdn6;Mp6;09uZBjxjoL>zcnxpL$da&kQ z1D*Fg?!Z1HTlJ*DRB&i%>wmBUW~}YJIWaQ!B86Sab24rpgN(Qm=BnY@gf?7cq^^hD z#i8#D5`(W@=~C;*qymn8brexGr+$Xes{PInY-+RhAn)4J>X_ zii*36FgfU_hHP>1)1ahc`H>pbx7wq77Lbl0^A4XW1tvD20`=a0?kD0JbJ&>O`)4ny zPhbx>yv>&5On*zwZcQs$SgwCfu_Ho}{uJ3=QiY|kY&q(anwgxlbtVyTAE1=E!a0HPzo`{R4m4X$Srp{c5_z&)Gs!7i)t4h z3+a=JSzy^=vm^#nU`NBTYCdV1@S|u@nW%P}2j?N9y<_%#lf+56eInDdd7l*wI6Ki7 zW${{Iqg}|R)A}KXxPMJ}G7N+px+``I@*?$>bEu+2;h^Y$B=wu1bo>yQ9rt{`Q%-Zb zlh241-uPqyarA~sP!WLHE41fus3-N2;W-9k75Pab5bwGxS&ugA?|LeAT4_xb>5g?* zm`-~CdP%k`kF2>Dw@vYmSwcign&d)1qzA;z&X0^to^ctSMXy4cVwQw^uo@c@mJST` ziUr@KQ_}Tc*sN|Jfp`YE3s`bh#krUZ{YHI^fyOL>;bFry(g9w#F&M)h9x;#T;(jEx z?;8<5(csc222LXEOlf#6&gPN;jWaoEBbw zR|%zGrwXbz#ucR&yriA3#2<#FOIK`cZ+;9SIjPpe5gt80qIOn)~dmp<~Q6(9?R3zfP?Hxfp&f@;snNZx#jWMEOnz^7l*{hMFXXXz@ z`P4W}&2|Ct?n(a_a1TJp{7%VAtFkMHF9 z8y{K&1&JlqF{@q)jhNgL!5(#Xz8T4d+2VgZHmbl%Wp-Nu>)_tzF{VT*#7_HlYANk% zGN6>3yM`_@P#1G+)Ub6mO$?im+H^W{oK%;$iPMh|2FIL#%W@yP@af8!OCRL%$?hAw z`ioR>DDxi57wCuT-z(T7K14hPRDkMGXLR`waV$vfvp^wFLShSU>g(kw{AzfrWCjAB zPKCIRuTmF%@y<|s5b$&DE1%BaBa!<2X+Xmy^cZlC;S=@B$08KPhaWkNicQ>Z&MM@W z44~iDie}E+bOIBu`?bkXrOIG~i@snQ)e*YQHS4|XduY_~#y@mDa0YoGP$YbfS+jh? zYb`?D`$OY7N7yZoF~|A2vpe+mn|TdckZYJyW>=Qo>nXiO9=*`6|BTm5qll`d{$Q=O z2K3Lv5br|xP~#RNS2S=7xk~0b+h9a_{Xjv))*)>gheE&=Ippyy0}_dWno4b#0~)U; z#|FF%U_S85-XHQ|Y_UCaD`R( zBeCfnm(V5-evluRkNS42lbZj7oVqbMgfzRDf*+{s22Rht-!=GMQq{TbmPd8jKjWbi zvRn5TVs(k@9xr7Ys3OL&SOv$szYn6f`xv-9spu%@Q=cuzK?5n?c?!8ki#k7CpZyPY zXwDB@d7<2!Hul`mPw`BGo0~tKkO%9vZI;h)9hJw2kK!|FxHN~`rCp_q_7pg2?ezw7 z56@I<7e+;=hG-w6A)Tw|IM`Z_Q=q4%{qZYbA(1`vh_UZSuD?LrkeLcIlbh}L*DibG z<<0qP;_7A|h-?#$ikPv5Kxv1Y6Y(34yV%{+RJg?_B87MMh)4@=0)9#JJk8_JZ?zwP zun4z@Q4(~{UlSw0*A%N-8K@=yL!spz2Z#cNn!Fdb{}BB`h4|$;mc`VNWzgo3 z_fmx@&xhXTR5Kr+Ma>{(+72RPK?uX+W7a1$Q{z3)P~Ln2ytGbNpV8N^`ySmOs4Jis z#Pq8TwDPn+o&w50H){@ZUy|dhSl5Bn&}#k+5a`OG!;{oLZ~Plm-d6}>WN+%7i)Mu7 zm)njKP{?tGzx$Dl^DW=$(c?k;;H>k>$07BeweosDSAx3wq3hq!GKUg9>i!>PP(xFI znP3J2uZmkcek^BzS4yu#Rh->H* zeVRP``tzL2*+e0+gQ#(xPWH z5fCIo**`X>**>ZD8Xn=%o&Lq0M-B)^Qu|}qzZjfch zeJ}auI!lYEuX^Z%`(1-0L#?MZC6y#q}4>M29U#*u02U;oqt zCgk7p9g^3zX8zw3QI?UH@NMRp2rIoAy^Vphm%%i0=2gpdgK+v=Xa&s39&aY zy>Ss}M1SgBB>+Pl-UsPPK?q#(Q_E zsH&EanOTH&QeQ*PA-os)LvALhGnTJAodZ&QeF=CJ)V0Z`BF zS9*942=(zR1Tqp@Q;C#cP2U6{h+H5%hMuS#&62hQLV>kJgJZEVs;jgaBqAFy6MlA0 zHFdq^zZ@;6&5p`CNKnrbJ#u-r6kzw_Y(ks~rNdG{*S1bzan~p?Um}~488@L7Gs@GY z-Ququo5SCy*c|Yelj{=vI`Qe7ndKpDdkxe?!f{r5MhQ~>q#I?ddqDybFX>;pIABeb z>$sFf+{bd|o^}}U;s`(fILwf_P!J4?U-Lzbeer`*`#nUzN9|gk(Smgx=)fsL6}q8Y z%DYrsMx_v^Dq$V90<)??D4I~=NGNEBz#kQ$bSZ+3<<@tVn+C5$QMuyCl6v8KbfWHqy#GMA z?=PY-o&Sf8%u3yCIOcNg8*#2gm7MOo~cp`7T z$VuqO2$?0mE>GNC5xi`&EDDV9Gxbf>8So~mAqF*g-?pn#GjeF~*XV7_b;Oq`|7x5B zaR7b_?}$w6Iz)5#AUIniwx4=i>|r@qeH0Xk|d2!~Hda8i+M7G^;7V5#e_9 z$Pd~7r2F-zpln$^9p6NO>eZ?|p!yr|>rJsHBNXlKf14&KdVh)AJ2tYKC@}3khN_pb zJJ>_UCMG7zR)0MU@*VInK(5r0H&$SeJ%=y~l4^tLD{CWujrj|_4%t2U^~5`bAc-IB zfsqzL9b7b?pJCTt^b3_h`--x;@0Ypx%$2%O;>Q~4(O76nhVEZYzdl^Tya0p{Ek)el zc&*97aQ0>v3*U=8wVk?Jp(m1L9B$o{%hHSdAAZZOVj>i-rq=_Y^+XpA&eRg2(gEqx zuLw){F+?avpZzx1E$J&E-|n2L z-;X&h>fht)b~@+Yo1g9|WNuH6^7A@O@^dujj8&PoQ2;Db!b8I$zN(l8#ud1aKhtv7 z?S@sLSAWRtfCX>ejcs1@T5a;|7i!;=bJEhJ{n(zR+)7WTT+X^UD8cpPVUJPnF6)Oa ziP;P(9ma1Fe}y($ZNV*`8B#mUkju4xkOw*BnXohRE}2{zh$@Ikk(8DzXK>A^=v@2s z-w#||7a)g4I1axfWN_=V*^go3+{0H>Yc2t2v>3M5#GRAfL;Cb^?c`q2@9Z;({yyo3 zosy!@U1zhR;*o>sUhBk^_9q(SpwlR@|0iYeg83UetShoGL4T!;zd|f2WL1<;hE$Uk zKARWN-hNPfF)KjW;d~anZ!`oNmN05-^LdDtnid)3C@;z4%+tuhi1_Bljjx<*xwbIC z%GOhTf-n6Fzn$z<58h{tt>@TV&~w%+)%E&<5D*z{HuN+Jx!MqF@7nO;JoklXUG?gW z4%hpp0EM7i=nFumBN}pj_#R#4t$IC;A*jz4w3{g~*Fwv?e^3-j^U9$RQZftBP%VES zewXVkF&fFG{MtR|mswFE(ouzFcmHj}eG6*uGG|KTTJmy}-PJ+$B4dD~B)m#Y%CQ(W zHB*58I<+q1Jn2=1}!c#1^Gc&$=pImu4$&Lof6cIqur zZa7E}V+?SIF|3az-5q2krzMUCQ)=qPe{nQ~p)x+ue5>p)`{mqcv_d+(rUk@|oTvP&)$_NkZwr-xPg^odt52<->;U+qq;Pye!@q8QQO*`42wN%G!;Y=*tL7)G zdj7Z%t4|A}Vuu3dA(>p8qcwo7TDB&Q6E0D{v6?#_d!B!GM$c7=G1DZYjXcP-EDjo`LVI@mh4g@p{LSy3qjB^;uG6nP7Y5v5W(D=s(XUZ=jX zV&Bpi^I$_}dU?Dbfh8%8}7Glsm{O2wqVdxa5r`26kzgN_*sAT$#eC^zI?=jL~P6C+Z|jo z-qWDzBU)A}I0qMKvF$yCSme0&o`rr@#pD&$xInCEgj8{R@o!#>sKTVOk?q-R5mu|| z;yw&)67SwA@g$9m?J&=vf|wW25mJZLC>K;l!CSCVGL%j0NYQBLxmkD-CMz`^6Y3TQ zy@BP507@~mVs?|Zjye2}-{{6|@~q7qiXqIqkg?xa^p4wnJT1rZ*U6M-L#5qnQ=1^VVS|uIc&5hizn1in#xxY}A%eV2)(M}jrYHJ>;B zoNaHG{O6oAL}#a7e)F5UWtV!T=wsP6Ee>mP6(ZU~UqXRy69iAu@SbZTlgB0q*o%Rz zo74?f9S2L#n^trw=^<2lt*%I}&#Doz1CU{GxxgIMC?B|(`-9nd!SFc>|B9_23L8=W zqj~oWvKIqQaTcrqU5+0;Q3&@#nI%y_yP~vQ<9Gd4;G5yr-}qbNc3a7sAACrf zvi2w|xwHAQ(dfYHo&bNaiqCZ?sRaYik0d;S#Hp8Oq+dKO%IP3I8x(K#;Au|UvcUr@ zx{QIAaHb5wBDp(5bUp7oqe0~K!igUqA_z|cP7Z!W`$SnWF zU`YL?>eS%BWaahuAiP(BV27vno9ysdXX$1!>aHwaCI%BBf9#pvPn{9|Cu5p80)1LpY>tmZTyzcG}ULnZtQOC+`2{EgSf5_S0s@VVd;4{cD(mZEU8c()pGxXDtGs(r}v==qI&Lve&~U`;XY-zT^4 zRIFZC?af;{mi^V<`Q+xKnnuSdl`cPl{O|#D4 zGfl4eNw$Zg1*^!YQA(iwi?j1JH6<|1AH#=4Ke9idgG4*X(FF($=XIhRB%1yS@!m?i zV2CJ+fp2lyfh!?US-=b@UP+CO7NK~&^`p`CdqbZv`oIzofs6l zVnEhs9ys$SySo{+{j6tkJiVdE4ePCp{*(E_642qbn2Z85-4DtsGyp147yQFL)@lJ$aShg6qmD=srpn-*~U>b1{+VJ@VXKz6u_ z>my1qn-R1i0uNWelbrpgRK#!vDWd*m=>SZfg)MGW`&~Ygc%QXIJ6RScP7rr2*85KY zocF{BQS@1H-&nSU^E*-D@01^z1p<~S&btmJQy0U(cC}FQ*Ppxyviy^|=8qQ(-9`hS zH;xlB9$cNVlNcsccmAN4NGH^p>=NtSPV$>iSFGEENp(#@LnPe@HRVHPJP1+K3;lr4 z4Rr5r4`%xxL9Iu_y&RyL2fTAmsTnk+qrV3WKj(W(nk;&rhnhNJCwbAK2{`)(YQN$F z(vqZCAER3Q%}j1S5L0qs=yPYHo5w%Qnf>2cr4`qHhns!+Zk2*>7RtyHQ^RQg$6lRN z%+}*iRdp^ep!nS6r!OuUoJnF^jhf=Upv*NR@JWHrZ;{xe;^>RbH#R8Q$H%OUmA+4a zXW6(4xr){)!%l?JC2((gU7{Fb5=;c|dyd3G?@ps;Olj_L+Q;Rd@#j)UYg3oy>U6pT z?#tp`3b%)0!7;#iqQC^SFOWsz;XdP@K|k(gtq4kQe^&eO%Y47+IvsLMcKjz;a+et~ zHmzVWB8>rfL_BkIHid6|>atm@!Nwvw>Oo9h|Cc~H0y48$U5?L>H)h@ZCcOa;s-?1L zNcp5__5j&M4Km5MK6J`SdYkPL(=GXOtuao+Mv=gWSRXW>dFBXMz{pny2mIr*vP3DT zt$mCBV#hSaYQ(5N_y@8NeOe9tXysk#UE)-VT)nf((wwh>0lebLT~Z0F)6)FyB$H3a zefJTQht^|P-^Uvtz;0Lf*%ck9o?-i$RU+wxJ0>FcaYxschAh!=8U^Tj93JybivqQs zo#F?9+upEWv_&BeWA170?p-vxaO|Kpucn6*P-(h$JQD}wYmPdOI=-zTLnb@b#xmR! zXF8y97Mgek9Ic5|8c}gFKMiHs3hAI$lT+dKXDtv^O?hwIPi1wp^W>f~Ta*#9_Tt#x z)n`?PWxZmA#p*+pG!HJOjK$L+G+~p#+in$NSn5*&G z`lF!JQ65tXdjUHdgYn;Aasl_TU|{14;=uwE>G@*soi4&DJ<~|Gu0(B~A7z2vNS$u{PRtWJzD`+{CS|zu zsfn7bnIk(MZiw zy(gVHG^*tItYl|z>W#Y(?lF%;*^U+~C%)eDVhIF32cQms2MA!ZmQx44K863+C(Z?e ztP67_jwNs(eZwH8)60T3E#RJeb)_iU<SvnZ<6!ZpaU~lfhuQ*Ax z;gy}~$u+!dJTxgLgEq_IPf+RP2eSAl>^s9&bEKBf&+W8@R(e_;J-k&fxwuykeAt3+ z@T4k~-j=3Ah6){#U}burA+8a>x5+deJ#jiAxDl^USV$6`TpX-&quE@+IOYEv$!8k(ED*2ucnInYQz zqsvsnm4gRgqW5?rjSDG1PWQr(`G&4njU3X7p7?*#d15BKS03UjuY@ZvwI(ZivIuA@ zfwq#u)GRNQG?Xk`(;H@W+?oZ^p?8b)C{yI?=3}z=YA(M~;MU%HO7-}er@pOMES)S+jrFtF z8%lM^m&pd_oabGZ`$l08H=lZ9KsU!ObsBC~=)q}RJERru&a1N8If zxcuO($Fre#OKk^yBf7!<&{1t1Ejm?^5iC}*sHwyCg_l+Gs_E;)a(hb7q1MezOxu5tK$@MofWFE$sF&iS>Q?vlcn&G3a%d{ zE}K1{O0!GUnDKA$k?I@XZ~#Y+R9Z5P`E^1ohI2K~Yi*xOS>pgn#9q`T*8+6Jxga=q zl9RIkHewNGnKdO@>RosfIRG$S0hv=dbaU?!QAVLn_wL^iW)LL}KKmFl7r$Usb_T%U z=k+Ks{KN*QssdsJy%{XNWr|bm-Ta<5f6`v^w*HrO$%fY;F+PVMQHo695ByFm7N?-vCwSr2w@r#xv4P`fyL;zf7EVr< znFr?4A!XUsAOyH#aZgV?BQ2^&vh9Y=MYCCaosbe`Hsr@cA^WrzHOLB63N z+ef*>RNu>a6o?))1{B0+jc*&EZ-4m;N6rCMDF-;W*tzKZaDL)HYp}bb$y3cwp4Uupmn&`rbiMO;xRcx? za6OLAT|XBg+>ls^0g@UEcLkLk`s=_1o_cWr7O*@K?=!;MmhF`K)v9YPZ_={J^CQ9; zL2K{8i(<$DMgFu}t%jcU8+{=_5qU7(Ye;41jILlbD*cV=SeyZhTjq(wm$d5u0$_Ly6S`Z z{)T#lB+TcXurCLDzr_J7LAEN{h`hv+@E!15`qQ}10B8H!N6Dc2By_ukH-1IH+`JQt zcmqyHR*wa~_ugbJ6!o@JV16o>I|+XzYE}QQ6m|Gtc@J?xH44>dZ7tONl^?sBjRf zzhC|Z4P9ysoG}sqja_--C6~@TG0Kh6_Z^@-Ez^FfAH&0W8Tl0XWt*7<&P@M|WjuB} zbkx|6{V!jM5hwaC2K1upsWMVZ<=RyZYfBpZ3!p(UJcqc-VL64>!D9Gj{$x17$N$`~ zY4Y7!d342xIv0LXa6dE%_g+s-k?zzzgQH9-tE33NK8f{z7{Ifk5#o$q$X0lfoH%$VlfsOQx&7QDA)oZ^CVyYQpIhG4O#!_Qv9G!f3aL&>Y?HSak`#tT%-oba~ z@xX;T7f*9d^h2B_*^7VQkZRQagrGhK(wrA@q1(s*L~X*93!>uXL+Q_r7UPfNl`&56 zCL^-=X;xHzga@ZkAl>uGRyJ7D<4l>NzZJr|&SOPI;Co`&@xF;r#p-CF7^pG_gflEA zgbhYIfQczug7R2V(_XdJ1VSdP8ZyKmt6tON9=y+ePM3$5R$xQ$$?LlEBU}lUHapHy zrJmfNyRFwxtGa+m{miC^PP~8XUvv(NM5<&pXaVuz)Q-a0q>utiuPMDW!=lF&>Ln-8 zL`cel6Av@joxXF8=-e?SM!{uow@-B63}ba#kt&99ro{ zA8o~9s5A)oD_cl2?JGAfsFX>e*|zO&ZzZq4-h+*JoM$f|2RTM|+kNX-3-&zd-*3#0 zL6m*9hN!*>*SC*!6aKCLb#+}WATD9HvXY#XT_D~oQBw(`>UZ!Fr^O)<_}{sy#)1*z z?>Wf1rWhI_H{9W8(VTX=*=Qde(1ULOkaz`F^J;LCkKhb!1zXaYX|HHUU1eq)=UyVk zJ@Y-eR7A0*m$E9+UPU%ndi*zuVK#(n!0`p|{LFbos#}6>b}-ZxHWH0NE*1xJm3s3h zD%pee6wI9%I!!TR>`6bZkfn^@u?5bK4TSU(Nuzx<4e&A#Rpv7R{V~K<1APiKcCT$% zG>sVQY@J^@L-{Mj?QV9uQ#};2vvbz0nU~E!G`9*^gKrPncT%ETlvb^H2b&01q@!Y; z++HgJe_#(2`1T$ht!?mY<_ibPj zv#Fin@NaPCffBCqIu%v9nmb&{fwhZ#63QB%u)V9DR50W+$JY~{HI%v&>^81x?p|M4 z1fk@<4o=$KUP_S~6W5*djoeQpDG|brYD)=K-1ps-WYwW-ZXIbiN3U1E$OZw2Nk1nY zcDB!AU^8!o%VXZjxlHbdad{RG3HkxJFU<8&ebu2O?cD>IQU`;J9E0eG z8_;jYk)xj-QLyOAt$gnZIcKQoT=C02_5jd~swD_U%` zO8b5ZSJ1cYoMj9%;McXHZe-)yJHzzy>)8rPRbCMcFEtSVG1Pgu?JO;m(l$B9_)%Cn zr%L!%gx>ICyvx96mw-Xq2X;OyS!>2W)_|Q=P`Ay}I2EPr+w-PMVK;o-mKKF|_H*b? z(sX%jkl_VSu<+}5ka$87fiCOf<2DLwDgB4nm7pjHOBN1!h{}~><*8IM$!^P_v2lQ-l^)ojBwjNEm z#*uoL`FzZAk+!to2LyA=1oMD9cKRgH9$waWge>ti-}Sy?hz=SJ_a61n&kLb5c-FR+ z9?15+7OgubE&`DG1RTYU8qSv!eGQe`HtH|iIxA1klCVFBBqvN}$UFYd)8A2M*@$ig zK;T@!=N2+^b+gBDs!4SE!4qkTBmV0ZPBx+Uw;ZpS)n)iI-ln`X-_(Bfio)hilK8@O zra!F8u`Q<>@J0tAiGm2Iy2}mk=xGeb{_|eEL%;)bx0QE};(ar~$qC2OU z+VKos!?gZ3vyt3sD$ddVeP0#S(|koIV?I?`aXK6zW1_BE83Kz(xYkHbT&N zM?gKs8e-JWLnGA6cCY74xoumFwn3Pvn7O3<_F@Pobj)XL)AITZm8(~l-HZtBUpt$E z5nf?MGR5*t|0x^m=gC)2Ch0T!2AP0jB3xBf{TEZYINGiwJ&w<+H7r`EWDjob5zIA< zLvP4bdOvC#^2*T&=tjS+K7^X(yD0)!Pi1wox;irpb8mHWzi-HGris3SS|C(SAnAPl z9|Z-wSQ4>Y)}?Q863Z{SO!~20WYD9bF?3;Z8{{aWxB>ha1= z>+{n7Hw|7sp1+O;1T)Vk-C%XoQzpq4G*E0nB&$6)D|wURrIJx6uLa57KBJ`{yDqHJ zduOfbtmRUwQl~=G7p%3K4pz$+Ng(!gK76S%7`A!kMSAoUekO)7sjpUnc|Dyj%um7Q z|4G+LXq7d4dz>>PZz}bzeR8{EA|t^<9!3o2ALFsupi#8MsN5E0#}t>YV_l5AV*t0P(=i}gkwf;Ioj%g3zF5Jo+KbiawgtGxw6(BqAud7Pplv3TT{-D!NpVK2|9p7bV-e_pXk$?ZTg zY?FXRm>ivu%n9qs#ja~JmrvdpGzW5P{tzazBw}S9WA*2&rjzl z?r=I>W}F;P{1vf8O)v_7Ce({3-KZrYX~!4ZG0ePA>i4mCntrEf>oC6+2t zCzr`F>HUFysBWx>Qmu@Ga7uf6-R2?pBMVYdnj)7YIi7aHZeD$i9VNzA$O@FR>Ekz@ zea$bg#(Q3qMOb20rpY@gfBy)#-MgHqG*i@z{xZ%S`?KQ^q-q6u)v^zAq-=f>8tGoL z8eZG7 z1h{#LYRj~Ip{2a~Lois^UtoVXRWzfo( z!~-kIo^ooEPv43W;jQ@*T6OPtZjz(eeseoL4Tg#ssmtmS%kHwWzZ<5ZpLE5d_?E_< zLV!rxamh*(uA?foLwYn#|1cVLC%<2AHVd-Q?ik$mu$E25tI3a#7#{sGeat!fHj0Vp~&{X1PGW5$%|j2 zFiN_Y)?1<1Yj~kAHa1AYuDgi?)pyyhx1IqkU~X!D?14utws6~sYfxIT$l~}Y;JTq} z=rVoU?ViMjPtE)X3NvdRY6?iNeCg;|EwTt*dC516>NWcCuD3eDLa3-m}pchJBek-UM)xZLA)hIZdK53T33mGbN{T;wbI zw%;|JXozvKIUyzFBag7@V`aLO>Y}_rCB>lQ>r#hIY+i~mQksKjT48E7?DeuO%kn{E zM;Fjde?i%w@sc7r@~hl;pMx-MULh6bhB7<#Lu1>e?~YrmbaoBYW`${MJmjQQy;*;m zf10Nvz?-tK$mCajrTjccrU{4lp|G~x9Q9@}7gFfp_(L(n&h;aSbK+_jJ(wm|CVlpar7h2HhQ>aCC{ zwtAx(=O40*Lsx~D-rFU9-QXqXS66>w*@;x{Bu0+V8P=0C({mk^HNg1oL0lHQFhLop1+NT-0U=oy1|r$LQH)9om_QnH;WuZa+hqh#XOEl3TacbBlazMErs6 z1<)e1kU5B%S=v!qdMAc+2WZl9e{1<9*VsU9MP7DD8Rm#24}Na33n;N%k?W(mtW$Ib zNt?;$OM&`v`{12!&(BekjXyZn|A|W%@kLpEii}r~GrjtL?~UTIAsC4gG6{xYy-|%w zHq>F#6aWa}V@wUhfh@ug>zI}6sjcC~QJZ~618HeI*7HdqdY}wKxYgPZTy7GyxCZ`t z*E-ftr6DvUa^=L2(t}g>Y9_r$b;Cd3x)Va$>?qwY`xiuV+(0FM218pRKQrYw1%-#2 z+%=GM8m-&~91R`>&)0PGSInGq^^RqJCtKE++mDM<7Z;A8aCTB-eK&HSS}Ey(BDhoU z{InvkJB)UQ7$FJk%y@*{Mt-u{}~-$ ziXDl7JE5xW0wo&_6 zi`!uX!X%Zo&&fjihi4fc72@MFChP{UQh8XPTs5u`LYV+-+yGjmXVc1X=8A8z2Wz37 zKnJs1m~%&CrE)HAo6-G^5*|jf1^AqRzJQwae7E0IybjT|HFtbRu9ok=si+lNVpdnk%ciGCs@YXh z8>IZ#j_h?w%_zPmXZf#R3l=7sl6Ozxd$=+pYH#o2GtjWQ*r}|&18+jsUu!MtfVK3j z^sw&7%P58FQvCs%=M`l2J>Tp9WV~5O?YFO07z*88BiNBb3F2*Tj#VXcALLFz^H4b# za%_7}8owhVJTB88aiU~Acj=SMp8u)6(O3Fn^HBL0r*dYn-qZIX)I`{f_}9n@H2pJ; zcdbT(?>kGxiEZqP92PHbLu}8iv952fx*v3t^0+OJ550lNps;!knPSilJyw(q8g3wr zHnIl_dT`Jv?pT(P0alESM`0feT>{hJ>Y3amYwVvJ9!{7i{Q=jH)DWMK4~j@AI@f=- zpz5AX?MlDY5v7>yF>5{eL;60<^j#^%v1ocSh#GzQ%=X={Xc_y{goWM!fxcO5VLaZy zKd@mMUsgki$3SvK$y#t;6E-L;(N37LwLcP47r6KsIij{hmFON0xsO@Kr`uAK`^ie? zU)KO_yYcE;n-X&o2`NI6#TroTup$}yWm?fUX^@gulVaScW`4e-W=KAu$IgwM75G^| z08BWHj834I+5?v)+^hT9%^&e#Yv}qvi>dfUr$qCfUS({K0s^KEuU3 zCW}R_KYIqpW8xxyGmx#Lo3&!a)Zeu){_W}vbAcb$a;#eMj>ncza-iIWcH>Kl$@Smf zmt$g#kD9o+7y1v%10r9Tju3gU?QICskUkeqq=L)Z=|#-e(Sdjh9XSzVG~Bz5E;UyJ zl~6HH8M-9dcDxr6?~^@-9TIVEdoSabEcBj7V{+^;bL3ce^N%-{m5t+?o1GwA&#O2Y zp_q?Vk!Ih=T-daQP0j!l^d}Z*&R<=1lvqp zw4Y4>;9kfx+F}F&#&g2pnkb|C0qj8)4O>q94@?pE$u&<=n%A=%U&lfU{|a0QY%wU4 z8;HGs5*iMr?$eRe9K=CsZ-aJ-ugBO4^5%g4g$^9VUNdqrSARz?4$ZfeABuo3quwqj zf~;`@sps zMn>5TD~nI@zoex)1GrtxaB0izG=twQdzVY|;tP5o{3xF>QNv1-a#A-W2MI#@RuYYl z3A;eTf;$XWZ4$u8FoI-_R;g3E4F)nL$RDWV13o3SZNcXD?^Y?Jif=2twV;8Si>Q^? z_6cErCc&sXzWAr{U*J+w@Q2|^F@k{IqY(x~T~lV;j3m63(zD@Kp-*wGw(Ku?oDynj zKeysP+9@0`wG3?~jGX+5)>o|9q}oFX9D+j`>#ie@K_eD03}A7hL_B|RiqNo<@- zRBrUhkxJBk>Dz&>2CDsB(yNRp$g$Hs_@GFOFW+2kCh1lTJAUpRpf+ z47ncdJ~h7QMxdPZl`5V2<%8Zqc_vDZMXUc=+dP{ed4NZh!{8c%yx4xQS}$s(39{Ms zwoDB1Ms(!0^@kq04aks?>Q1cj+RzwT`y7PbV%D<^1@}$Cxn~YJ_vC`Dork%u&V(dC zOe4``8HVRquGDJJC^)>N5}?+Q``h8F4`ko-YShX6J!4T1&z~d;MJLrzdC8yB5qBSm z?rg}zbsH!xGwx#x5w<*QFvoRiad^ z-C}MTnu-3KuLE-cGB^H{V&2Q|0aV=>DS*t6p!YwrxmAttGGbF&y|S&aRU~)0>||C# zymrJe_Bw%tOolQ^D4z0s8tDw$`M|2neg z7H5|rJ)WSqc&Z5E0X(;vWx+#dN6dbT!1vE*)DW)uU-G_6EBEEKy=3KH-s=QKufNkV|Ylt4MJ)rmQ$;Z(E~8RL2JX)7}^q`P8V@K$5vyyksI-cci%Rp!i0sT95Bl!@qY{Za$Eg^mkeZ zd!5t>0xLp%hpuZ16T>r459Yzxm%&g&cADfB&44n8p#;XEMC@^}m-q<5dueO{A0 z91jM8#z102u&LpY+W|Vzj)3nVu+XSVtL+>1|5!Q;zo^=$jVnk>BhuX^DGf_^3ep17 zAt2qXNOyO4cZtMGcXxMpFR<*s`}{ude{jy(oqJ}kd#>*Q>$0%vFmJvz!^tLzNIzM7 zFA|NRUFO$ukbp~+b70;7Z1{#Zc;ZsxQdBZgWvJSe0(4mo2{v9Uu5z~0KZM^XVUf;i zjD!=u6nmGsx~M_`uzUns0z`9lYO=Z!2|XiSYl(v%{x=)**di=zSB#TBEX zbxwQVJ2<<*L18ryRxJ_e2T?Kskzdm<)pZ?=S)~-G^O25<8$1L%NDC~JM6*eJy#6~4m ztlX}3=kdU(G!!`s!FN;nUj|bju0u+Bof>O_F{!uHpGyNU3$<711JPagXH&_LaKnG{ za+?~M;f#?UhmUTy8n*`hNyG)N^Sv-bsim*|J;PzwO)0#6dg~bw7mJu0s6y(PTjL}k zeI!Ye^$Z2QU<$6(`lU?v>8Mc{?PU-;%C@2mE2h6478GwFn3x_n;a1jG$fLg=OQU6% zB*w`oGUBzHYI?<(65)qFcBQKXIDgNIkp+Am9l7kGLaz(Uht#Ykc_Hua`ZlGrjMeD&tJ<`Sh7f3#u>sPs(%8Tv& z(z`?rP$xpe#_L{l>|Md`hvfA}paH1}=8sM`@utfNet*@4(QfbmJ=Cz;!fDB7i1 zd4<3pM96Mxm8p7&yZQ&4+eugB`v9_PB?T+nOjODx>z z96Om)s=4s1U-c|QBI~zk=se=gTZbe&UfRl^_iMS8?R#}fY&IigVNnwx_dIh2cMh3)SW{!qwavdrNt|QE|aX1lhij2l+TCc5=i%cJ1~^ikNcxO z1eFCx9a`KEg28m0lf%Z%Ri8|Kl~KC2PVdzkzB;&fe)%|Fv9b(n+d*F=U^Co)4^*_K7e|j$O0}0$yUN{_}^& zMw0y&Q^EYPQ<$ZW#MMjB@DV(k$Jxl_h%Ve)%jUC!?nZZ=`zj`Z{PAvdGdCY7 z4d&O?td#rt-E-UqUhd6lkVtFAmx-@R=FxBV0Q4YfRK@DwpBLIA`bzr?XN*`-L?a=5 z6x)cmxpK(jHNtZof1z{gSCm{A_*GalQ=0?<_3M$KjS5+}hZ*AszXBnKnvw zmoUh}0YPc+NN)Bwg|1#58Zk5gt80(#2uXl1nRRcN?(!G)n;=8e=N z8nj$MsBCQ#X-M=K=iD~QN%;| zuQk8fPlC3a#|z~56sA|={)+7h+E85Q9QT$UBJ3eb(#1FC5L8&~h_OZ?@NN5*Sd#Is z)+EA(wA&fwajrfC`BW@a5M7~BDe^HCR&1;~qe&z2ktKXQno_TVT<72A{v)sQ)W7Rv z-*5Lm*CLM0405rgKk^Ep#}cC|XJDss8p6)D4ctTJHaTIT2_-VC+>!cE_Vxlavrvvt zCpirBgr~qyvN6|V%rt5^lUv74qEES7o6N&TdPCaZyaQk_zQ=AmI0FooGCa(O!gZ+U zdLR56L9`vsuao{Fjho+}l!Mc=U83X(c6n!sML~4j7j1O}S)tr2MDIj`*w}bW5()oV zR#=io*yF6X_7710cw^cNWEp_lD_Q}0?ibTg=hmqyz^u@MvON2k2&r6}q$%y@w>v)j z1mfSbJHir^bCxivcDGs?j3McBofnWZpyM0dDig0vU4YidLS~1W4M00o8l~llADLKi zw-HtvcRBwaE!1bbVx1L2n+q11{C~0m;VR`>165MBJH;wp_gLK5Kder-%whL-k95MS z(Sy*?>QM2c9}mC_Xsm=N2|V#@pbo2yTfbZXH(jYS!N8)LLaa?AL5^*3`oPAPtfing zJRUAj(k7M9?($r0(d(1|x+V-V&4eRQN`Pg(3TGUN()TK(^S-ZMH*%3#?uKr3a`>?U z`XwSBo!WZ%Jo_Kkh=|iNU zs>bKCk00bD-bgEhW%TgAE|1;WM$8qVd^=TVCU@Z>rIXaBSx|bFm}+QR1v`Q(X^{y^ z|9+6tvJd&Ii}$*Grxujc3g9ArAhct&|5z#H-wd?>jBMBPpeDrkqpwPXLp>4%xDicQ zJ~QUU-hzcdC+mej8gY7wS7hL4n6bx?u}IC^Pz>usl_OF` zPnTZPv|55?nV9zca1Ib104>7=pFuGf+vlSUvHN8O2ZAF0N%RqTMz+d87W{bCHrdky=ne5uYQN9KB;5cvJr+9ljf#*xC`UnLE*2=y}g9sSGQit7ctkqqF-*?ponpT$`mudy+dsazTN(q)NNrV8f}6r? z*g*D0SH`lGU-9S%g`iasZDVb)?r+_99IP$BL!k%H$oY6g_x&r}D`1v)Re%q zuxocayp7AtuD+YCY%u;GrCi9vKJ5?So9e#ckwHxi=TOR*eEgL?U*)@|jnIr9?^^P#YcDA^7 ze|zm_z0~pe=FHxeX}ucS9qmk*`msAG{1d(uxyE@$77lK=H|3g-MmYU^R$ z3J0oe^@p(ph?P)SOAl2}4Bj^@$0&7nQ^;JszOYDd)FQ?U@nR^E|E{ZH*RYAPyr`lq zroW}Ve(VqTQsGHE+PnQkfpoV`?XT0Nah?a!E!uj|nyHM5FOV5w78xEUtQi}*vtQ>0_W%X`vOE_0nOO( zQC`Ev0$=jKCqKixFq?^<|D_4npI@sbMZY+n(6QfW&~ii8ez8W2t$m!RA}A>X@rGAJM)F{R75H^u^l1N*6#;!H5nxmxSO=KB4~(G05toqtvy# zsw{o(L^#v__>sN$gEx~_Qe;vxDzk@Muz2D?6{$BLeZdY#Lt^p|o06it4IbD~$l=f6 zqdlkH1{|sB*2RAz0YX<&-jDdielIoV2z1f$?>82j`>!+=>@K;F-(OTwllQp({4%d5 zKjQm)f4~Exkoc5y4QPY<%^zQfkYstr>==F!s53tZlYY%zijlzX;`%VqUr}C1x=#)* z?qp3zlnxYxPTj3*`*(lZ9hBuO$*OaZALDX$b$R#W_9_X4pZm(lL7rc{KlYBoV{Ol+ zMwqzbPkG#g@5@OM5^d6D#AA?OaXnVmCUIN7Hz0>$obgF+m2LHoW0#Pu4gzF7U0TD@nM15ur8ZY5JrQ+J1)&~-H`}b+bbt9_! zh@BoI#-TOhBbS*{j-u6Z^95<&bG-QD+*tmUV!mrUux+{VK(ANDm}e32uf$U_4+Yt~ zqVE~l-fA$)37a2T`vzoduC&fIAH=lGV;=Vk4+0gPSzgv9iFoyFJ2^d$O>1)wnn9F%uR!4%sA(7aqt**)008k(3L(0Gov9c8mCa~RuP;QGoG z_xZE2;Sz-!xzq1D%vXzUW~9>u3FU9qP~t;yByVTa4_xmrv{Jqa0Gm=F(o>8n?%SY% zq+Y8Vwf^9cy&tqH2KWj~U_dbJ&EbFa+U*ls#AB9s38K01lj8fB)?EH-)+$}{wH9+& zB~Orrg_JL->s?Af>-QvPLp;hNF*4Xv!V)!za&TMJHfNgX<^>&+SEBGsEk+HG6{?N+ z8E+J6I1lw8OTRAqa3ZkHsYg7i6L~GFM+MFITlBLKo!sDj!DV$Lez{_D(=r^Mf)KLn zA`cTC%+(Ylid}l2chSH+7|zDSM8*{8hBClaz#F{YSZ%`8J6_Z zRFj0*yu6&+@tQ;dYABGdCk@qt-SVXjVwsK{uc-WiwBJ6r%*wngR#7b&HZD04d69z9 z!t*C0tSpvFo9E3%8dPsv?val378m`5%H1SuF$-;w}>NcT*jeXDZ<-8ncTYo0#&P?Rukc^>Hgc+KuNt{$-9%)oy z`A;lJza|p8d-pO)jy@4Mu$q?s`1^7+HpG6$kzRe!=?xInxkY6Bl5NpxAonP>lbd>% ztq!}W71aC8h=t8V#DOBzVwOPLaRfk|<|30rIqLR!rmS?jT8srShLAmxsSGH|)y^O( zdTTikLFX_D!>18kdtQB;J@1kxd50vtZe*H#zf~k=-ErAPXVbEg494pT&Lo~4H%UQe zJ!e5CRsQC@yGEf{0ma)Np!`9x|A{#oAAMqx$6fL%zAV9vj^MLrAp>Cr(p5~D>8sBJL63e6XD7J|!RspK-pg;}>fRi6MHG^~yKy0p{O3 zq{-Z?B{L}`_l?g~Lf7#9Br)ZGUcTpOAG*FXE7>(2&uF0}fuYty`oHgJ{*b;tyu%I) zY$F^~djB$1rz?5HY4REfc%AZ{Vfnf=^&mG0*!d2KvFIw2lU4O=6QWXMjS8C_+H?#6 zuVad8Th#lX;Y#$`otS>)t%!PU!OFZ!U`(#Ek~rIB72nqD6efVCPsMGD1-~8uBVg#T zQ)_ZKdo$LHuMm;zLxB|DE{{;OnuSKdpHfGr1PkVICWenv)CU{%in6JEMFyEIEfov6 zYR*hOgIl=ALQ0V^KKJw?RAPObC14>Biq3Ku*$JCOY1}((e=n(j z2H(v?EUjiS6vwb}YKNly?4o=_`y6nM{b|ID2A(DvY$wlzjnc(`iFpbMT)8D^!+Zc$ zh1MpcVAnp|4}i~5|ISJ3;H0}zso>kD1j4G^)J@tDL8yD8tRf=rc`U|HP3%UnNv=?Y zU2F0-3x9lKSyFzf(Gh9Ng@{CT9W4;BZts`{TPrY6mt#G)RBR=QL^ zbM_JuhkDjGnZ{>yX2x~3WS99Y|8E52eVAZYCbJaTHqJBcwN6XcL2t_^Cr-i*Ee^3L zudZ)6h7$C3Y`xfuBxdz2vi*R%V$9+lnQgYqAvci-E{nCdyA?BneeJ+MSQ(eHz=iTX z*T~m=BRvT(QW)VeiUe!o#n~bXUA6NA6Xkd`~>HTMn&=M_fsVw#3naQ zdrN@7s9F&1?@dEYwO)Way(WkAZW`&?PFlnpdX2MWqWrFC2q5HK28r(V!aM0{ z*%kxJv_QL$K(CSUrt6t(h*ZCO(gk=@NQ?5m6>jgvhkeTKk0!f{#ERUVg6-I6E}%Ui z0$muOG@_yKM5ZN-3`bFiB_*8mzr4SC55wQ%Z821Uo;a2rhw+NUVwz5jPaCXI@^;(Au!e$fZ6)BM;-smY!V-= zFdxtaD}~hR{ui{^8rWCsK@My2-Zt<0`OmaAwHQ;u%&Yra=x+L-&qd~w3wR%D;u3g> zsn>~1zv!ujR4o2gCuDRosreA@y1R6ByM^y)a9h9t4)1;LLnb1I^;t|pKAnT zLvOI4$I9T?1xhj)RZtY%4zsnc@ur{g+KQ|Ywt(+>&CdWI=d+83xj-z?0D6K`wd?>b z>`~A)zz#AWj<}7Kh=N{Su>GyT+Uc9R_VRdLT6P711?Lto`z^g9`InD}d?4*c|A;lV zW>TfDW#rBk$8O*jmlGPpP6_0qM2dLVWE$(AWNx}VVbdS#Jlao~EQGL@%_~YzxCv!y z_43+EfTt+-DfX^13V5!z`mzCSx%L?1e+5&zVLmtda~eR%`@TNMS=@l;YWAV}_GvupF#g^gZjfM-~qe zaVj0WhRJ*-7(DhfWi}PMGxmFYTz$?(yg?lM4{-QX(t7H2%h!3leCxERp`yA~F;&ag zEgR1tgC?}^T8biCz~5y`UuZ;rsxT$GghKJqz0K-@}+8^LODOu9L1E z@Mr_#-{s8%pwR(vJ`3(ucA*eND&{*`D?F55(s>OXqz-Ifw&O|JAJ!W|jXZB67F4ET zc6Os8=T|Q@&GDmJ#v{>{8U%NPH=lpVFnry62>2)9!@1Y}ss3qTw5E^Y<}gDx`bz8 zvT^A7+2!0WY~wYT#fb?Xna>zJwpD>&3Y{#S?lgpX)LrFhC*%pW89!VmBkQpZVx7f} zoY<*IvYp#9ZD1y$loyPDY@PTSNf`l(p}Q=_!}LRT_{QiwSXt9gUpw=0gt+(9BL7m>xQwnR_bWmli(dxn>QGTf z-CR+5RQ0c-bwLg7a|kzMsaJl61XvgH`Zp^vw6a$?SKPfeKyKNv;CEzCY#bBmBeaU7 zC=rFP$u3!n8T@ho;XHtVk+kEVIvUhufHF`NaQ zMtj-!M{UkS{XJkVy3mug{ByWrx3<=Jz9BdzBrbAOAkUiiq{?)`6HlRywTicpb0ChS z&|X`_GReU@X3tPvN`ze!5_s*g6DB1ARi^R^Ze8^J(3UvDyr1hR&n_kCH0R{@a-1+3 zXapWu#26jyH;$?_DxC&7s#0(c5i2|h+J;)47nwomU`?ipSG!u!g^WDp!U@1*{Ia+Ibve`TE8X5uK%QHs!lJkb&@xz%sSq%x zIk>|-t6ODCL={WzBh**7!}K>#q`0j7ag9t`Aca?ti0V(OBY*gCqmp93nqnP=xNv+h zwZ*KBEHk{7mLp*I5=8m5kkx`9n=M%r5L71~GxSIPLN^TfM`dj}G}>0Bt&od<0U{56 zTz*flRnv2u@$AuWU_);xB20ngwpVPzy|GJ)VqBUtQ}1;Qwz{~PW9aP~N@l&kqVc4D z^=N^I0_>;r7g)S&SztXiZ8z@s-u5*HtBAUWZsQl(cf|)bi(G|&o#4QH zo|wERh2?O7g27rZ?PDK%2feK5>R&=2wB~UHKm#2yPkUKh003CeB&onwXUIIi+{qsJ zoUXPV{W@N2)U?TVZ+fmLaoIaWdQdq$ib`L-#w(lJf%Z?dK_Vs4V3;KgA0{~90#pS9 zcU=(vqf-}7e!y_P7VmRYf2U*roL=SNYU)yM;pDL9M39L-T}mXEWuPqYL7-nE^3@bI zPOsTU*~w;@GPoJ+F$uz90TJdQ4PHvVAPIoDOV_{NnBrkK=Bb}4|NaE#Ay;OkX17AP zvEP-Z$u3$xSY`Mci4w3Q&|?j7@>H^43eMd;$n5u zNP2sPU{lY)dM__!0E)kk4~aO<9XH&64*Z6*v3qGgXE>^9+t8tzV1z^alKr3lcB|xT z%5x7$!+!O!**Z21rQYqP5>zGjaJhOYcg*Ro-@Po}-7Mk!og4Yqad|u~QLGulnIzh& zjUCN;WeMw{0^{T1o`BqCA!LwTL=QSR6H~oDS;366E{CRCpU6s0y1TUJaJH!kf9amI zLt0k5jEFzsoeZyb=kAu|q=-K^18^4`9S)@~hSSc^9B%=CCp5vEM9c9i*!b{Il!kP& zv$s3FJFLg9G6-OZ*Q&_{Z@5ShnkghdYH`lvl)dUu*n?xU?2o$Z0Hv^@3l!X%wky~{Z7yUfo&ApCIE zHu9SV7aU#s!1y5(CP8qQ*sF8GT*%0iEt(03B1Q{+T!269PJ$iBSsd1w>fW7Z0aIOe zL!2Isiu@`~ppZeLHL=bY#jx!ABP=9Q8Ev6>4RliA6i?VeP&D{I?SCoJJ)zwj&X+|& z&V)TN!$>(?JR6^Xdo}UIYedk~wjP#suYc_0=h?CIx$OrFff?00xqwsyVH~)btfs)W8Lx%lR#176)S_T3m_+^=my$x_S- zN4R9{*pK(&-vvs0Ud5ghMI!a9%lnzbZlz(9;7?PYzc3VYNKGN4y@^(_Q5D2+)1J8fZ-2aWtjaO*dselc5V zjXqb*+@b8d_#4gX(v7I4kIVY8#N%Rg{>O^TQaJnf}sP~D~UDu$uL)PGDJ zc7N!w#Iz(faOZqjMTsZke<}_cqwCO#ceI!~`0@ZQ-kR zY#X*EU_-nWIhEd!F`DR~>PmPWocP?p&jNFPrhKds-$m0^JZ713xNT_a0r{Q+bKvaw zOaW18YCXAw&3?UAleM)rHus*X=yab%*~Z`U^>)E&Tv!^>t=st}fxmk^!>ZKu>xLYj zH3hZcE7|tBgBhIX$KDS=2Cud(dCg<<-&=<+U_O!l^DBkvO1yAXzdF{g0MomprJs3T z)ZA~@@!jvSLywf4xXPs6pRo(Sb_r2{4entzS02$^hCG&nGRI869odH%A8$84jkL4` zV){k`tuMDIeJWqO_YuLl{m}Cyv8`2TBLAe~pPvFk?)8xz^OsXFGS4Pkzlj*wHX@l? z$ZM>~Cyw2D*EN#D=&OFOXgBrKJ8N3H0Y^VTqP;`%91n|ImdOZmJreSb$!Hg`=&dG0 zBJf*HX;_|KiH1J+Nm8rDl~5kUfWd08J4M!2NR=%A?cc&o3A2Jia-KB%;t$b&X>r+N zA=FOXl}cdD;)@ew2ES(`eb#nrwhk-vWY7s{>gS99)nI`#B|=?ush6yj=2h4#{})in zE2stLrPvj@`VCgNafkbi_qs4nzY)~Ye~58&99ad`UGljB$HKNZ7q>@M7=!0#YdzJf z8ltlRVLb=)8j7LjsIN6{xCIuAQn4)V|_EDBzR4`IYuA zO;O}^jHjQVR!=U=FuRxB&i&oOU6QNOGR)7M)a_T3nKPG<0N61Art=wW{ol*+17G;v z49$GkUZML5ZGj=%?6qykI}tBMIc%z2H4|KJ>Tc#V)W;X+VSD3^|2~JP1?h_?!Tx=if<3D2UlkM6DX@Rfj6B4ja)7XTtaTYT>0s04X0v|= zL{1KR_4s*M&QU(M`^8bZtCp4H_^8ON0GUpa$`rO-oa0pqIWc82p9iIR8i0i@^!|3| z+;PY$lldJ9c|>y7TQ2R4YIQ0IR112@3Q zPGZ0L!|Cl}8;fm3HUPK9TZ>MyOAhIfGG7Ek6uZ@ zK%6RDc*4mXJg`&o!P#eO{HJpX=GAaLc9PcLak^Q^+=QSHE4J5+cdwY9l21?S4fW)K zjMz(RLheTdTMkK!pI5gn+cmx!{`LD}I%$0*O_XBPTAw z+D%Q!%PXSyG$&nrm48y5HoP9mVvx~u3sE=%qdAaE0$+I+g&?w#?VJ?8JL=&E_!$() zBQnny>?+GtiS8pq6-FVoESm>2$SeG-7N)B}VUyk2#In&WcdwHvjM$X~``;!oEB0e$ zR`a7xYrTYTD#8s!7M(fbGGnLP| zx$z)uXucQS8NjP=$Mph(cz0iM37?sG%tAfXYasr3>(wsjPB6P?YqP=II^06i1Sj~f z&1fRv1_6hgCO4tji0q&Bp!u!j%{frI7#6+yNuOB!agX4MiH%6jfA$OU3^xAp4;Q8O zL;@)N$~>@Q==)GEWKAb&@ON_Shz`AVAaGEslSup3%<66UN}2Ws9(0F?p=+6u+>96+ zz~IQ3{GErx999qVU;*W9e}_bdl=eUqQ8FSL2KeMqRq1ZczoM^eWh&rRXKtzzAk2nS z?JAX%m1u;L#Qw+uUjO-So(0sPL4>w0n&2czwkvkOA91Xe-7Jw(#9v!VdXS2cka@p4 zKyr2jOqM`WWLAxpZT&dP05WV>fz3NQ^f#VVm90s%kHmJtbFvOZWvYGA%9?WA=lw%% zzD;*MU&hx;TJrWxcU>4m@;)DI91I{yq*&Vp&a5%=`550`{i7w1&5KVvq+~u*MwY*w zz`ZX@DJVayEUY%3{K`WT<<|{aFtxjO?{FUOqqFm!1pV^cDWfxLLD`PWM zPLp8OjEAj%to?K}0Qw5ssR!WDNpG+~U4;-^R|2|mqtM!i<{01Y%u3Qxw(N;flVg07 z*VL0u;rKY$WN|qJE4o*^2SkqMm%5UEllfc-)4+CmT(83pAdU@rWyGF1US7hm+&`c= z>)@uq)$z*5=V09n5Trb6W3cZyH?a+G42(gf8E2!+5g=x6B-ZnR{@01#I_wm`eGxmrt-WF@3!}Tzsw@hJqt{j%V{13 zcH)h@fs2OaeQXYAfEB?r&xsou)7Zqr4vvT8Qe9e=rOkVo;SJhPOgoe1K#3k&yvW_V zbTmH&)z;s!i2_UE013WxwJ~SaZWNP#=0EM*`PHPktvDVg%bm4ts2CP$ki-7X#c{Wd z?*u-NNQ6>3YN@xr&J+WFDlb5L*lm*UWyQ^Tx1Qw=>CG<_qx`u?+=H)QywhJG*w20} z@NBj=g?b%OCM3j=7|FU}@78~+;OX`&wmX*5`;CzG4rE525fp}OQIym%?cF?Q>_F;_(>gJbXPdO(#;&pswLR%ZMa z1l+#YUpd?3=m`b~JOinKgXGYry zjOPdz!4L3si2zQ#XN-gRUBB~rnPdFJtGa{+JMmcezDIzK!W0m-- zwboIUJgdRNi}Hj|;XqnYwH6X}roAI_I`h!LO3;F%ySG3E?}q8_jSB`|`uxz3*J4ytnYvtXp%)@Z{J}Y*@wjPc{uSYp-|m zDFyUV_wO*kE=~vCJ{V}}I;mvyJk%>>?onQ6#-mgY)*_( zX#20o@Jd;=GfyZ|n-@;IU#Ql_ao$nR6sxg+(RRh#$3|Wb_Qn|oz$zh3XN4!AhvRse z&ML=W1IMO$D;Hd>kKZ6Rl)=%7sPs!Ml!iA#3P!t_-VK~P+42n{7D6j4b8oLax~0Dd zT32G@ynJE>pzb$qJNYoi?SMa@0rit!TG;@B#odL?hNaHp_BWRuOUVH8GY=@^hgio4 z__nPF<7ZK9aio7D#Jlwa>F;sCJ{kk_Ai^b#5=UKXPxMmzSs$7H!uKU;a~j9IepBiE z)>i(gQe;ml@5_EIoE3=P^nSWyF;5~UpWxg3QNW20Vgg)RC#vZWj1KVmufD&y+q}#q z%gv8%qRmvV2zF#db7PcA=CQWQwEEKJyg3czwS^I8%e*6HDK@&Vi1D}wjSxyzghva! zz_#vFx8hu3?_k` zDH)P!Z*a<+_XdMU-g#Hb=bG=uPb)5H`M$?b&A~V=mWSR@(4i!3wlvJzNHhJuTk_!P!Q{RbB=D+-v2_ND7G#Evh=y9CRzIf-Aj z?`fS&Tpz}Zo~tE_AQadz=9JRp@JS1#ue`D@0kcIL=v2Eb1VlZ*Npt_ zjUq3=lnpr;@DstR%K(U6u+j5KaCoE$&R8~c@foojYD?>1TCuJ@JCw|fnO8t~Mp$@= z0qQgq55=Y}KtNqk`A(;$q9bkb? zmcvN1Ug+h>33UkNQr8I5HN^&fIp0)ykKEs&A99$ODn}2{A$@Sq;z1QSJ!|{tKN|mw zQtbSDJ5*{tYN_*FAgX?q9bUIEd(z8Lz}C zn6!SrD&QWsFA+}v`im!yco#HA3uv1QjPc6%b2msRVWvI2x-s)n?3pnPY9_Xc+|=z- zMaEPD6|G_lzqVF}Ts~tPSIEd+J!``}o?|x%2$>&p<(oL~ZML)PMvSi>CGLT~Bg_=k zd+{NqS-+9UI9@UfBYlT{)C20K=_5vPSc@DcrC2XipNckz8OsDQ6BNQ|<3_#s^Y2jp zZ8B4`a`!F1c|d}V5&ID>)0U|(t+j(0g~6@=u85$T69>XCfj-bt$oNw(-(<+NfFqIR7mD&hsC@ro!cu4=GxL@jlB_ z+)R}}_>ir!AE~0Lv+?&U3veFz^UGoDj+&JB8nj_**5TvobcmhDO7*>HbJlD-$tPGk zZW9k|oH$d)eEXMPAf&m={Uy7U5v8D1rdZxsIDF?ata7F6aUKH_2aSznJRO*Yz7^ zy>XTbW+-~K5h6-VEfHi*ZqO|(Ef^>@+81x0AkQg@eibaQdU(Wr#YTjfFTFH<{Z}wa z5n~|7@vm=6{DUw#lxld4Goo1O+e~2ux|&~0jsJ)mRf{Tz$fyq_DkzlHt_Ci-Vb>ka zOZP{B9M5Vs6@C^hW4O)7$y@*EP2nBOB1y5#AldjnU}mo!KP*6^cU{_mAi{Toex9Z+ zbGZgv&MocPkfYE@P$ZVnMV3#0hd`^CLAp{DG5H3&IEF;SA%?T$_de-ANi_XVKVhf| zR?rJEqpj|K?X&T9XugOgP!$Ve3nbA~cGQh^in1N75xJSF=`iDdIMk3&NhL9_nEZCs zbItWM(uk(MILBjZd56(!3!7$jQGFmT=ih+|t;9M$0&a-NUAHh}UZj(FE_Tj>b7xi=rw=d%XBEHS-l(0V4ns^*4pXzxItSFmjq` zTg~)*%(icEV{OU!=8L4Nt~`+tf90zTt3yF8b|~oX?7x#d^v^K2WLACFA14{wt*Mo8 z`JL2AYrKIclxS^5y>4cFM8apwdW9WM?o3TxOB`fu9jT9W!U)vH|9j;}vxicG`LLD; z>9IV$e)wMc2f}uj_*Qlv))LhPTNvdU%wi2L68Wv1?2XJT=E~oP<*Rw#+%f-(%9T^) zn4tfXH?aKZ5Uub8$7r>*eLZbOv8IWc>ko5v9hVBJA4fdiB&@aSA@!#+3XF;Zd1bvA zy<(e%u=HKJJa7Kn3Ll3i%8A|Vk($}>|6sCFUz{IdE9-kRLvbBs{btxC*F^s52TSlr zr24xxJ2LJN&6`qgItBUkf5!Wji?Ky80bX2gp%jA#M6AEZ?VM+yEIu)&*nPJQrC3C& zj+sO&6;O)a(@QbXNbJ{~pBttL>P(csMp3aMKB1LbFmtZY*rt9GD;8Z|OID*NNt9bM z!i9CctGz6Ao({F2=760!7_;aK8@g&#?~zB$mkAxNPkV-I4kA$2wJ~&q?j3zS*5BS^ zp9WP!ryF>hk-&JXP0zW#;7wcT^*KEE_hYIRAsYrz*0;rGyNawMOc{+%CFO^IXUG)L zqyD~o64Rjk;nQ8ocYb}bcuC~%td1kuFJHSdw40rfF6Cie484m>eL9oZr99F+P+y|p znUG~ryFlG$v5nWM{WVH07uS!`a%##Ma-MeXj z{1ao6@z1?$T^M3cg>{H|0NpjJcM)d}Rn8u~+d-UT^HqWBBw+w;m8|OfdJ4y7E5rhrt9?FmN zBsOkCAYRcE`i&}_@eZ7L${wG+7xGl$=Tx-rvo#ea2U@!2bv8-tSRm%)B;{eH53hYM zGeWkbQLQS_cRWndi90$`>HQ!sMbaYC4fA0QBF0cQ?UKkoXECK z>xsugSL{xdGJEifrP*h8=|e-%GMf;)+b;}Y=V^N2KmDwagteh{3rq%NEz3;-L)M^o zaa5|~smeJniz}ra^Zn9i(=ASfyECD>1K9vZ~({}#x$xaEn$aZD6?#+J> zv^UkSaO8+BihyYCGLv0AtOpDduj4B%+~4`ddZQe5a81qlTJyKr&jn2$?eEbhT4}I| z=zTch{L?m%{mQUd=|2H-4_TShjcG>WM%YOsAHY?_d_CWBy7PH5T7Yj1?OmIyKTeFD zvZ}dR)Sg}D_m1b>lWm>XOQIAx4|r7vJG_MEN0|=G6895hKNE=|Hy)-jn+Xt_2WN@I z+r^VYdETnr73cj+w#QM(G5J)d>BQQIG2)mU>(;6EdP+pLoA;Z#>*2f_>XiM+pL&&) zjzvdD5>GGMK=qb;MsJYBU`4AZUM9sgr5?@Kf;Y?B+zX7T+FB~+Zv<`D$+}Mp8DbO* zmJ{p14uf@M;fp@sER%vbcDcm@?5K{GhpERa12iOkjzF8dty>A2^Fmh~$|A;f=VbI> zT*r=CecX6DLY-dj&z=tvLdALxo){i{kyACT?LWOLybZ2=EhHPb5PYpQd;VWT2ZF&- z;I_a0Ndg3TIDX=Z2T;Wy^y8*6zAhayNjV6MD1RX{a`}SJk!F>^?DT}dWPwFp3CTaT z-pf8VM%k#Hv$%6n?KCdkvmY1g5QNSSPq%KZ@ ziNhfzxlcqxT*tHBqaH=Z*D6k|WFEZ=WACgnJU+yx}BMke1ML{hESp zM0&d-f+$n|YB=jH$ssw9 zZ;S!Wk2Ey$KJ|EIUJLD6F5OhmRJ^x6MU=WjB6!GVjU$w{l8#^ZUlu@>kd7SL(qD2{ z?MwcY2gu6y0uu4F@-coA{SIm757tyTW;{3jUoMXdW5AIqRfxbR0kkv^6ksHG>wbPo zN@M0W#S?WvHrB|Nw9b?L;xO~Ue8HB$boWJ#FKg^(p4v*4g4)^@E%=e9l>ovpz{qSEo7f;F=gcitPvbGlnUmqT~a%7W< zFe278_nPN8zd)4TaqnBzbc3!dzb$gmHoI2}Z!Tz$0lMD}Yu`LIQ{tI{I}z;=muhs6 z4^-LW@o|o&rQ;=qYmSevZJj;X>xsK=64SbR344;#;*+YVHy8>me{4)e$#zY zHvKVt-LiBF`-_>zC15e?nXK!rMnqI24?2m zdGA{HZ@A~f`Eb_r;jE{Ax%Z}bSSdjU?J?`x$EmZ1Y5SXXT)Rq$iul(+TdCqG&-OeAdgb4Qg}|u(w^Khm znEp6v&(FZ~BzKE&p-fHMm$n#M_;0M@>(c$gtH^)u=EaAo^Lu+tP_z!nj&JjF;l&hH zr?D;+r0Jt@rXjR)NlBN4nxVKM%tG8&?IVJ5Z->C9H{HCfl6QjdWp%#j9#IJY{XJpg zu2A60&#r%1^p1u4Z*k$voCoFI#7|!4$y&42Q5?WI0ib?7->Zi-bnp5BxiOTc`h8b@ zTdlNBx~&z~mCq(HH+^+yCXDi1d>EEz#wBuY{*#e<01Z3(yu&-u>Fpr~btO~z?avu2 zT*%5jyA%J^0~;rGUcL^)C#^cPO!CDGkPR?#ea>-ZK2Q*?NdbZcSVuJ`l&Gi}Tq~;IxzD+jl>bB0r zo{nb2lz|bvd8f_wy}jy=L!IxL2k?!1t^Fu$+TVxZxg|>Pd27|%JyJ8I*7X#z)ioQ6 z9Erw&)80^HN_!FuA%BC*i#{`qt=%k~FP_JB;V|c#Fgo@0X{&86aVQ~1ws}xH@@p^E zUrX;Tnj(l7s3M)`Zl#vHt#^>&flCAFH8t-?tCJMgq)_DzF`~NnAk8aYC4 zhLc4Asb;dv%{dm(L(9r(^)f}pM3w|TRXDaqU_0M&4}DpDXPAs1()9f`w5OT5nLF3| z-bUE6~=94asu3RGye@u4& zMH7NPQzD-)KMZzlcZw0nVN8WKyf`FRd6?CE`n@9bOOA2l08|JOX~=XHpjw6SQkl+|m-eZ`v%8(p4N%6E2kV7Iq1*Ac6P zfs;@-Se}Rh?VMZ6Gyao-C*cWQLbqTDCWIV|-qoZj_^YY73Dbw|ddAz=?&^i~fw~}D zW*2{Pk@Ni>3Lq3*F7eO}$t|Kx_;}mZn`+}f+-NM)-`e;GmGW8Vp-xBM^#&SO6cjZF z(3TtlY4^bLJp>D$aG_@bjL3IKnl`my|6tsa<9X&o?ABRN%?WvI9@<@BOuUkxz}8yv zvandpR@YP~cx)fC?LkyCYP?&3-}^&s>u8|eHxFWZe|ndde!EhlR69Aao7~ia-87&a zqgt}AM@S0cEWC?luBSfS(29r`@-1fL<-W6ldn_drc3tR)!c@=%!cLaHCzEj$^N`3= z8pT9@CaVU>(BmN5QQ+q`IviY(dUqF{m0(W#c>R^64$j7lvkdjELYQzP8D^TkzEr>0K~4xZ=0fn!a%2;1}0Mj>xGX z(KJ=L@-8}*i;ioGK4v}d8!z=xNpGEbiyO0$KDW1iL&Jj=w~3#yKY_kts1{-iPirY_ z42Wxj_aK6|TF&Tm7pVI8-RZ)apF4Fm?=OpOF#fHqbku?Er>JP9Zu6nilqMIQ3yh~H zRlA*ncGNX$%`pu6d2yT;-%>JfjrPKdVN+*8nzA3$MK6a-WFKQ=oqAA0=O|cWv$A-ttr}ycOpbBUY4q?{yb5r> z090xbmC$`kd4XRHpQ|zJBta%2!O~IS_!}XD!~%|-Mo&PdhQ=BSPonYnmm6rWvy-(J zYDU&4`YioXx>PMhx3Z0_R#-QNrOO--g4X{C1spnAf5Li_|4e zS+xdnH`~pLZo7gZZC2$&5Wmyq^^rTe*hG7KGZ~#+!$hNaThQevZ(r|M>fz{%zJvA2e80)i&)q&se}0}amO1RFC2mE{5Q(8)5IqjV z<8)DHPXV>YwX`NBtji+_HPsl2`{zy)?v7Ww9JQ}-4gu{2tF{ib#17|djd`%M@VLy% zMXf?w)(0_`LQ6bQMqFN`01iJl|0zH@_v`zO?p)8epP003U!qSpbk2{3&)0}YKdC=` zU%>i=vY0<>&Tbpk#?TSmos7rah;@pq$kAA1A<(Mxq0vlx!Kf%l$R+CCGN5C7>ANYu zGYiBpi4Bi`c6SBXl2zsLQHtoG*#h< z!(uqYdnfm9as;?uaSJ+_UT`>Y9-P*-7RY?UEMlF!zSqAr{jKDp>l%!-KlVL6XWA|a zmid^a`ow#D;p^(%WBvdn2CCXQn(*n5FeiGCxBsTQ?%zW6W<~M>bM=6IjrP&o-4=EY zYX3ATTnf^KMi);w_DP}+?Y-~y93+u!M4|eKBiI{wpNAku;;`Qe2B(Y8Eh)^%At!y` zbg)~Yi&=UG*B%&rUMi#=u*w`qcEF2gnCWLk$-a6Q|dzgoZRclu1>Sk4Tq*mX-05mWeSS+wF2YBqjf`1L&%@1vbR%ibX3+1#l5*L zfE549aYq_tD=H$%h8{6Yc)lhn(UuQMcH_lQ>slGwkxNOguZz=PUVYY6rj~IaX!>%o zYs0BuKc#19C%NVU3w{KV2WR)Y8rK}R4|G63Haw!amu~YilqsqUA}`c2Sc`NKsX@#x zyPcfRF2QFYnZttp^2$R)4T(Eiaq0q>kXzHP^&S?l@qj^CxI{lseeFhbqo+{9HRCl9 z!@PYgLcn)nus$wMUu;RD)Qs8}v6G=KTBrr%?1ha|;An++uqX0TY`-AG8^|NLo)xba z!|?Lf4s}w~%V!p6`bo-+?fb~oLpR^XAlS$j#V#=umR^CEAGL8jKtJVXW2_(G{INmt zBtfU1uTNSIw@K9?e@=C9bu#>&DOg-&s#V1OR%F1XsN{}-+iZJ1?fkmo$qzfq71!Ar zwx^x5UFXuFYvSN?<9o*SNi#~%wAPC^IOVzf2VgFbE>T89FoHDy<+y$oJxGFzYx_E8 zD<3u5^hRhg>TIT;2%kFkwM>@Gzp!E(YXaxDQRKyi9&r=bqSLiu8@TX&TK~IGT(4Hy<57x&fj}#_Z*+^BVLtG3l;ydAxnc zF(sM;Nt&z{J{SHaC;8Beni1*OU(o~T6+YMB-P!(Ei%lsH3|pSrNK8awWM3xD8-9r* z2smp=$iGW3eR;-Nv@^o>IDCZme(EUD*O>RG$@6ucv(XYa8<$gjwB|tX;h?W&Ny}1` z@JI|JmHL;|u)JZK@i-R(_>A+4ob$EWtjQ5&)M&A)*`pZN$%_4N5RcePhTg{XHkh&` z-w{{_qH0Pv(^bMd!zRH+Xr{9GxwS)j1}^NAfeHxXoQRY0d&7{!Kf%GZUJ-DcHW0%3 zj@qK%OE4=wjB1Ia;2=Rh)0;K6wwIys6!|TEL2T6V=e0Tv*uV;3XkV?y-x1mr>%3C; zE78bqF|xyfkN~H=H!BA$eVQ@DVnZ^klZp{mr~O6LFnpPlzO^=Y1jVc5MpY;RC4SVg z7d_Zyx%<7O_gSe%Y!$pKg;!O;<{c^qk%#Jn@%^}qmZA`&G11ChXiE(kOrD;x_4u%i z`mll`ygtD9s zR&(rIrxsH`tY=|bYRv0+AslZ}cZkec(7T(Bq-8hKa66+Z3I3dFX!2z|qN%sJxV!wiOP;k|{XEIqXUhmf!e7gbb$C%@9f&RfvRMyqQR)30z)%9wsux8YaX| z&0Tj%F7`Rjmxt+VeW)Vt#lMFaD+PL<(zr9Kue=PP*W`(JlfwWAxj&3CWEb#$aZJx~ z>-LW(>M_dY1OGGVOAir4aXN8JdauF8eI=562jA=r{PzQHr>`O;@49q(8xyl^Tt(^d z4XDH;2R&HQkk zV`JD{sw|#8t2Sl8VJi$5%YP>rKT!g)yfmoCphzCIGh;7HyZ7v1)1jA{wQ=$-i`_Y! zzk?h84}VFa)43UllXKsW>+FVBo68pCE;QS3TwWY`_(B?_GZd36eF9Mc=p4oj>>^@$ z>gYAC1kwB=lJHfe@&`O)>FEi<8Ij>xmd5ew1^qz}VBjABDnZX`%R>-ez?O1+;B3`Gz#K$Ex=r5E4N=nbHg!Z> zr#S}9e9icUOP9v*^yE~FO)D+gKy~qg8`UwGm)jAUd8|tt;%9yQqVS;I!Elps7|=%^ zXSYj^FZ?k-kcP?nCR8uH*6gUU#9dKpPeuOKxlbUG2}LMYCMuveG3x9bc*+3!K?R2y z%f{wVJDjm)GGM=dZ-cv|ZEbeH$9+#A_QZmNeV9ASlO^@k`a`>fxHSyJ$c{4V9Wqg{ z=+EWI!bKb=WngNTm-&{YBEQf+@^RzOy z!?Jd23a%w75hGIpaAnt%d1JE&mPkPAS5`m;3*+em)EzwRwVk+=(~!xnMS`1S!*f@6 zyz~;N%8FpqS(99AW{Mc@LuHko<+8ml;U*yy8XjyT;%Ixd-sa)z?b|*b&Zb#HVT0-Od9pbIA{7c3N4x zR(-|qX6U|(_94i_mDKzP5KgR1S0xTA#?l?VRt|Kh%ZBU^E;61!vFgEOiwB(}|j3N5>pOHD= z`>QMZf3E3usS?vNk5>qC$veHL$y=mS8VqKzzR;xvo}==c)an_fHUH)yX81H8{7F|1 zTa@>$ERqGB7JZ;sNj^^%7kN;StM5^W(9KaTKo4t3W{eAOIKUNYHtQOe@=d@s$7MQ; zzM%H6C?3oJNNWFu>}W*N8+=Yh9Sq+5`C#$y`ZXuUo|v^PDdy88#UzJFgFcWF|1ZM} zdA=HxRTJV5Cb-v+!kjfkxLyfY!-z%FYPigbTORZ6Sk;B=4sMnt9PY`j`Meb6|F_b} zv65G5yq%AtXxEnQ8jjlkGMXs=3%8(V&{>GaH}Tjk6ogE{%lbD5p+RuoSGyMcQvAH~ zRW;j35fE$dmi);*xa`b%cC`Ks@%bqbSBF4Td+_+t?vK1%M(Mh%x~$w*U6?6$<|2b+ z;oaLK=rr53r00ex2Ia?s`rx=X1Xy^4OT8{DtLcKw_|fPFteMBoQ7~H z-<cwo*!B}NJUKjF})juS+Jjv?l;?w zicTureglG0r{F-rk**umto*~R<9%Y_O7|pTm;zu6Q~yZoH7gGsT){8tiNwr75FFlZ zSLWit0+ET3hRU6iS0gW|NCnTI0ba*9L-ZZ_%L9N|H`UHh;OP6<(YlB_Z16@)5c*#O zM?@wfQg=D%BAQL1@X>`L|B_s%wKZ=-kw41 z_kj_%G@+2?k9LSOErv;q5&KD;UZ6Z4lKRu2VBN3|z#Jwt%3VZs?>s7bBln7C>hLxeTcE|0yxe;4NB9eN=Rk@D=GJ#*j|)(o3=hL1_@1VgZPe`x{j^ss ztl(Pbh7s$8cAbI09Z24wohTvN=n+UiAuAYB61bxAD-rS81h5gfTl|u9@#h{q^aja; z50WU^#aV#taG4_Tk;+ysVK8yxa1t6xJQNkZ&7><$iy&(BA3b#qaVudcAj*#_z1F|< zeF>NJ0c5?-N94!mXY0PG&Rqs9kFJnBH-g38mvA?bEJoT=tiRA`SGUUKDlqSnKsd&2 zzF3P*=R+*D^)`Ok*?$)=RYF7sQ|HT#42N79`4cB3EM#{`RA$NwP{-bX2 zwoa;!8v!BJ*1^IBFR4GlgRHG9UkqPn36*@K3+(lkv-)q_qrY2FnGVEVK|@?uEXp1> zFtVL53Il7V=t5aDFi7FK%Om-`k)GQFy|_khzbLXzl$}QyZ!CF6v1&SP^UBCKI9IKL zwh5@RM}Pj2IJ?0bJcte zIw*zp#r>%N#S&dV@ls1VdZZ4RP?~=xl-lAV+gqC6*?mU@3nFGrUdN!j1_>GgInTO_{b2l!EJe;3Sk3^fy z|GFkuKh_%rrMFi$Vlg&-)tCH}xfg{pmE%xURF+v`1mnTpVNX)ry}kW_^o>m`=&6JS zl_BEDuinH-Nn4nesaqbJY9*qiP%FxQ_?gMvMp1}%=+m_r=zD zQJ|fL`Yu9ldh7pB2tg!d~@hwRv*x9|M|6G96lx*j>?5{~zh&W)($9ymgjjLHq2~g1TIisM~9_Q-E zM3mN)*18r8;o#uNd`Xl|#wQ2(=KXqz`qkMi8hmn?P9Vdx%x1&Tulp&NzY3N0%_5c( zLE8P6ESkPhn_>Q2?a7lj>@Qk*rk*yAoK7ZmRmWa#j<>6GdQdUb!UG|O%Ytk#a88vk zp&u6Ce$T9FGOV-OalyU|BJF$A8MbLv-yYMmP$=8s*WjP_3AO>B>RSA#eV7ua5uK{2tD z2KPQS;DdS?4mfDO+{_Akp!c-g`vnWSWg#FF@7&?hbC6lyK33HJ;u4?$ zbc(^qk>i&aXo4m)lFoVFluD_SZ$YY}<@{|A<;e5o9&OMVF^#ZNQjp2D8<b1Papbk{WiBfum#sE~PaIQ_lKnvS)?|qBL$&Z&RCEB%M zB=EkIe-Yqr!pFG^&HvX%QJO0=^UaCx+t&;S&SB<+?8SG)5_$J+Ac=`jWs zViGxeoPWei1N&`m2a{o(xQRlw8Or=sLOF7D^ALl)Q$u@)>c9Uw3=T2Rh18nNegd7h zd!}#@Z3%v#^u1_054xs8tTy}eHQ^QYi#F-IlG#Ggy30G6qkJ#0Ac}Udrn4oXoS@iQ z)|xYp9_R`MY`#AARGt{D_0~1l-%BR6VD!be z$2}AdoLPE6vEc5@aWOo-O7sKSy3}fTQSaCjCnnSue%YB zkI5e9tJ#tWBmADl$NuSIjZ%B8nMYx%{l;JOnVk;2af^5wz)89gQ zUZaM7ltfKDfi{1A63H_SuvG}-8+4OfYk%aoA$@zx9{tAAbvTV%0QK(wf=PosBnbYV zl<`h?$iXED48JF2fj%`l*~nbbTRXsAY&zDFD;#_i0dNiwJI$3!0mR+!I1li3w7=Fq2TvCMg-O8VxWC>`d9>n zW8=Ma%nH1vnVI9SM8?i$l;XXF^8VK_MbU`A3cJj@tg?iq<0iJ;lyb zMj&ScXgED*{*6PY6^YK-;jeRPJyoOW=w0$7_b#ZLi*Y|1mbV1A9Hx}9w`@FRh_b)~ z8$t>huXZ7m`;Dk2s4p%NnRwf!`OLPF zUusn4omhf6tG_H*>>8S$P{LXzp19%=&Bp)eS&k&$Jks5Ih$BVYf>aIcf3986Evy z4lMX4KJuL#2<0*Z&67nk+}-GlR^1sV+uiIlKH3%6XsqpwdoMi@O;{{ZLn~h>#U{0! zb3-EaGKp>1`%`-KviS*5hM?K6E_agY%cp9)da7CW(kTKxk|S}zlLz^J&UkG*gV42t z{G?jk_-kf)$+3Zp)R*Nsc<&c{aZ=8?7PUHl_26_#zdr!Ef}UT?VIu4D2@k;~ZU-I) ztp~91M*6XBq9VTEq1FCek~i9b>0q3yr6*)t zJw&UG5-w3}k60#q)$8%8--+(?RX5qe2sV~96Qu7S@!KF7vWYc&t0TKgkhcoQI zzfI9Dp&Wq+YJYDQt=B#tRdK(+jG;KbXMKz9YOI6{a`P_fnDyoZstyLvUz%MgSJf3U zA?MpjJEZTCOQd(6tgw=Q&=<-PvmGc?4$uW|josbDz-y31DXGdIYhKGJ|FVI7LmnPp zPnNVl@$Qc5Tv{WFg$`DFGW=~8RX8C9^+Bm&`PZOddqIg^f2!-)6YA1t^gD0n^+z>i z<^c5~HIOLi9nSCMbtkVcD8}r$fNlD<)5KXKcagJkA#e&s!1v_aPXe{qQ{pj&p&hT& ze_C-MHBg&F;P}it(_K5-wK$YquO4I79>)*k51y68nG~flt;C-;#}Nz`wqZKQc+9L# z?&b;C)EfQ`;bKzn)5mAT87pArLyol)S5GY>eM^8i&z1F4a&}9L>yO4nDs@GanuK(E z^kuKVZ&lCzDb?fFKUQZ(6{fn*Wv*_AEENWweC0V;l<`l_n<8GzK^Z=*P(g5}r)}N| zo*)VAqPeJXkbWW0wb74&)^;^wl4 zP5mTG>rk9P-vKm#jPu_Fx=t4Sda$ct?UGP;HJb4DCzr8CZqUVR&bzv*2GzDvx=S(a znx-%?N7_`(qTjJgO%Nf7)H?5iQ+UlYtY2fsr5~xBNXeOh!&eXW3ceUFhl~ zA+Ar!(gRi}5Z*7I{1d0=D|)pMV;9zy%X`rXIcs%*c%~ z-Qz!{U(#YSV*2LN`a14b(zM>#oR8A0v~#Ve*24)`k}B_f&9x)$-SP0tk%ly-TOSaa z+>s@T0ahqptGkOm95%uYbLGo3PV3(O+!g<6UB!5n+E0wU4A1aw$m>2?H%*lck3Vi1 z{OZ=&V{DTKuup;SEH`pwB~VNeW`pZFa1=hR$&1&b18*rn9v5yKaWTWKBovT2c5!&P za%qzX7uflyq0)1dU`?{}z$m>3i?d#I<(;7mXFvZqWm)b#iaqiJ1jrJ zs?J~+Z;A^M0C!m8W27W}I;6}A8Gzh~Rf3rtmb*5%EBY5fh~!b5N1l2t(bU8V`sU9F zPa$N$TQu_PTY>;deW^`< z<1C zB7I+C9w`w$k}6$#9)E&k9(c`hOBWW=d5}-tKQ}~RRi(+)f}yNtPu2P?nO9#)59f&7{SGwI4(z#T!K2VpuvCNWI94t0AN zLCMkw3ydGQ zsb=&&aN#1>P)MB}DHgAN_B|8(*in}f-_ps#g}TG%$P9ma*K##-Lf#{O_PyhfbxLDv zWLBF`O2?=nKfN4K@`@x7lB#(>nEvl_zucgIK1Qo$WyEzDS7oa?jHKq9hic?Dwa_02 zeFud!@d3jvTe&IRLBC^mJ+(y6;gyT)0LmCJ*H40e)Y8dH~p;{Xo|KEUY#DSfRypypZ-N_KK!%vU!8T|;Um!{XGtXNh((0PH`{$0D* zEnmP~KA&(oQZkvOQ}&kX>b0|RhGE}w8>{ZjPJys{uny|&A_O)}BPPuJ8c#>CFK*bE z!0540#A!TkI9ZXq3%=6{F84UCDDjuM41N>PQ{eN_AB2px3hS{+8uX#%{cd^kziJ4` z??c(4HtD{nfj?P0FUj?OTY;3v@Tx_68wv+ov0rMG8x>2`LvkO&zb)>_T6sQ6EBkIO zGWXM{JC^>CLs@Fb`xOjK_v7C`_7&YHPqkkC_jeFk4nGnIeGkweh(?N_Thm3WyoALJ z?fd6-d&O8{(lpzOQ@3bONJn(~^7N-AVk1W;HBf!ADz;ZuPl{62qH0E=_xF_dzEW57 zua)j16;&9X$oBVe@TzFIE%i49{I%#VR(?q;H(WVEprrG@HpX+L}enH2gytZ&48&3tyDIZ@&TDu{yS_ zFZT0VJgSL#H71PM1|dyC23}~NSVj34e&2|vz>c0h-x1|aqI5&ES89^v-jYE4Z}=oB zxc7d0e~dd)P%N!W>>%<=m*HvM$$AqiB0)OO%!@<9_|)wN+&6}CSMZ-V zdTiKg5jiFuaS ziFj>|qmhy3*4CE5O~0je@<)o5Av0Nszo|~HjyDaf#75kC9;p9qx$ro^SKiK zi@u8Yj=)BWqB*($>>{J_DG&tj!b6f~p+iysGf~9j43FFtHFj)TsYTVx45BAyvg8cz z`G(HKlm5;V76GE_N47s_C}~@NgnUyT)JQu&8RIl#WvjSXCKD?mgK%#8pPo0ZPbBj2 zRwu|n&h*&{U-sufp+2uI$mOglygQsKf2F>vG5127k^?{>T+6v#s?qQ%!zzoVFcabn zAv+%QS~%HKJRw>u+cei0v*g@yWNCoIYArHzUs@%q{D!4wW>w3l;MGt+$GhF~Wqp7P zMPLCL!&f$Iao7xX%dZ>T_L`4mj-Cy?g&!BWg%Jf%77=rsqzK2mu>D@0N39nDo(j-s z=%uv~A}0QLfI=(0kQ@?6WzOI)Ym!PH?$nLM>HdfT8zx#!9H8mY~vL{?}dLkn*TQ#NBEZ} v9buU*x6%K7i4ywni-Rqip#A@4%mwg>d-$HZ|4s822KrG|)KsW?Z65kRnPLN9 literal 0 HcmV?d00001 diff --git a/StreamLearn/Algorithm/GEAR/README.md b/StreamLearn/Algorithm/GEAR/README.md index fd47bd7..b517030 100644 --- a/StreamLearn/Algorithm/GEAR/README.md +++ b/StreamLearn/Algorithm/GEAR/README.md @@ -153,6 +153,12 @@ $ pip install -r requirements.txt $ pip install . ``` +请确保系统内已经安装了gcc。如果安装过程中出现头文件缺失问题,可以安装: + +```shell +apt install gcc-multilib +``` + 通过以下命令检查GEAR的安装: ```shell diff --git a/StreamLearn/Algorithm/GEAR/include/common/string_format.h b/StreamLearn/Algorithm/GEAR/include/common/string_format.h index 7ec84c5..9708c24 100644 --- a/StreamLearn/Algorithm/GEAR/include/common/string_format.h +++ b/StreamLearn/Algorithm/GEAR/include/common/string_format.h @@ -3,6 +3,8 @@ #include #include +#include + // https://stackoverflow.com/questions/2342162/stdstring-formatting-like-sprintf template std::string string_format(const std::string &format, Args... args) { diff --git a/StreamLearn/Algorithm/GEAR/requirements.txt b/StreamLearn/Algorithm/GEAR/requirements.txt index b2af4cf..3e7c195 100644 --- a/StreamLearn/Algorithm/GEAR/requirements.txt +++ b/StreamLearn/Algorithm/GEAR/requirements.txt @@ -3,3 +3,5 @@ deepspeed pybind11 tqdm tensorboard +h5py +gymnasium[mujoco] \ No newline at end of file diff --git a/StreamLearn/Algorithm/GEAR/setup.py b/StreamLearn/Algorithm/GEAR/setup.py index aee725e..b1683b2 100644 --- a/StreamLearn/Algorithm/GEAR/setup.py +++ b/StreamLearn/Algorithm/GEAR/setup.py @@ -57,7 +57,7 @@ def create_extension(with_cuda=False): srcs += infinity_srcs include_dirs = [os.path.abspath("./include/"), os.path.abspath("./third-party/")] - library_dirs = ["/usr/local/lib64"] + library_dirs = ["/usr/local/lib64", "/usr/lib/x86_64-linux-gnu"] libraries = ["ibverbs"] extra_cxx_flags = [ "-std=c++17", diff --git a/StreamLearn/tests/GEAR/README.md b/StreamLearn/Algorithm/GEAR/tests/README.md similarity index 87% rename from StreamLearn/tests/GEAR/README.md rename to StreamLearn/Algorithm/GEAR/tests/README.md index adefbcb..07432aa 100644 --- a/StreamLearn/tests/GEAR/README.md +++ b/StreamLearn/Algorithm/GEAR/tests/README.md @@ -5,9 +5,9 @@ ## 准备样例数据集 -参考运行`StreamLearn/tests/GEAR/offline/single-node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 +参考运行`StreamLearn/Algorithm/GEAR/tests/offline/single-node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 ```shell -cd StreamLearn/tests/GEAR/offline/single-node/ +cd StreamLearn/Algorithm/GEAR/tests/offline/single-node/ python create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt ``` diff --git a/StreamLearn/tests/GEAR/offline/single-node/README.md b/StreamLearn/Algorithm/GEAR/tests/single_node/README.md similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/README.md rename to StreamLearn/Algorithm/GEAR/tests/single_node/README.md diff --git a/StreamLearn/tests/GEAR/offline/single-node/config.json b/StreamLearn/Algorithm/GEAR/tests/single_node/config.json similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/config.json rename to StreamLearn/Algorithm/GEAR/tests/single_node/config.json diff --git a/StreamLearn/tests/GEAR/offline/single-node/config.py b/StreamLearn/Algorithm/GEAR/tests/single_node/config.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/config.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/config.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/create.py b/StreamLearn/Algorithm/GEAR/tests/single_node/create.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/create.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/create.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/deepspeed_config.json b/StreamLearn/Algorithm/GEAR/tests/single_node/deepspeed_config.json similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/deepspeed_config.json rename to StreamLearn/Algorithm/GEAR/tests/single_node/deepspeed_config.json diff --git a/StreamLearn/tests/GEAR/offline/single-node/main.py b/StreamLearn/Algorithm/GEAR/tests/single_node/main_bak.py similarity index 99% rename from StreamLearn/tests/GEAR/offline/single-node/main.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/main_bak.py index cf7a4dc..99f5ea3 100644 --- a/StreamLearn/tests/GEAR/offline/single-node/main.py +++ b/StreamLearn/Algorithm/GEAR/tests/single_node/main_bak.py @@ -177,7 +177,8 @@ if __name__ == "__main__": loader=loader, model=model, optimizer=optimizer, - num_iter=10000, + # num_iter=10000, + num_iter=500, tensorboard_writer=tbwriter, ) diff --git a/StreamLearn/tests/GEAR/offline/single-node/misc.py b/StreamLearn/Algorithm/GEAR/tests/single_node/misc.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/misc.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/misc.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/__init__.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/__init__.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/__init__.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/__init__.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/README.md b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/README.md similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/README.md rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/README.md diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/__init__.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/__init__.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/__init__.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/__init__.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/funcs.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/funcs.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/funcs.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/funcs.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/model_impl.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/model_impl.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/model_impl.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/model_impl.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/transformer_act.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/transformer_act.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/transformer_act.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/transformer_act.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mat/utils.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/utils.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mat/utils.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mat/utils.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mlp/__init__.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/__init__.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mlp/__init__.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/__init__.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mlp/funcs.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/funcs.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mlp/funcs.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/funcs.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/models/mlp/model_impl.py b/StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/model_impl.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/models/mlp/model_impl.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/model_impl.py diff --git a/StreamLearn/tests/GEAR/offline/single-node/run.sh b/StreamLearn/Algorithm/GEAR/tests/single_node/run.sh similarity index 85% rename from StreamLearn/tests/GEAR/offline/single-node/run.sh rename to StreamLearn/Algorithm/GEAR/tests/single_node/run.sh index 0504f80..5a76934 100644 --- a/StreamLearn/tests/GEAR/offline/single-node/run.sh +++ b/StreamLearn/Algorithm/GEAR/tests/single_node/run.sh @@ -2,7 +2,7 @@ deepspeed \ - --master_port 42000 \ + --master_port 42001 \ --include "localhost:0" \ --hostfile ./hostfile \ main.py \ diff --git a/StreamLearn/tests/GEAR/offline/single-node/single_node_requirements.txt b/StreamLearn/Algorithm/GEAR/tests/single_node/single_node_requirements.txt similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/single_node_requirements.txt rename to StreamLearn/Algorithm/GEAR/tests/single_node/single_node_requirements.txt diff --git a/StreamLearn/tests/GEAR/offline/single-node/test_env.py b/StreamLearn/Algorithm/GEAR/tests/single_node/test_env.py similarity index 100% rename from StreamLearn/tests/GEAR/offline/single-node/test_env.py rename to StreamLearn/Algorithm/GEAR/tests/single_node/test_env.py diff --git a/StreamLearn/Config/GEAR.py b/StreamLearn/Config/GEAR.py new file mode 100644 index 0000000..c65d41e --- /dev/null +++ b/StreamLearn/Config/GEAR.py @@ -0,0 +1,91 @@ +import argparse +from datetime import datetime + +def get_gear_config(): + parser = argparse.ArgumentParser() + + # DeepSpeed 相关参数 + parser.add_argument( + "--shared_memory_seed", + type=int, + default=None, + help="Seed to generate shared memory key SharedDataset" + ) + parser.add_argument( + "--data_path", + type=str, + default="/tmp/gear/checkpoints/example_shared_dataset.pt", + help="Path to shared dataset" + ) + parser.add_argument( + "--local_rank", + type=int, + default=-1, + help="local rank passed from distributed launcher" + ) + parser.add_argument( + "--enable_tensorboard", + action="store_true", + default=True, + help="Logging training losses to the tensorboard files" + ) + parser.add_argument( + "--tensorboard_logdir", + type=str, + default="./logs", + help="Tensorboard logging path" + ) + parser.add_argument( + "--model", + choices=["mlp", "mat"], + default="mlp", + help="Model architecture" + ) + parser.add_argument( + "--expr_name", + type=str, + default=datetime.now().strftime("%d-%m-%Y-%H:%M:%S"), + help="Experiment name, used as logging file name" + ) + parser.add_argument( + "--deepspeed_config", + type=str, + default="StreamLearn/Algorithm/GEAR/tests/single_node/deepspeed_config.json", + help="Path to DeepSpeed configuration file" + ) + + # 训练相关参数 + parser.add_argument( + "--num_iter", + type=int, + default=500, + help="Number of training iterations" + ) + parser.add_argument( + "--eval_interval", + type=int, + default=100, + help="Evaluation interval in iterations" + ) + parser.add_argument( + "--num_eval_trajectories", + type=int, + default=100, + help="Number of trajectories for evaluation" + ) + + # 新增示例参数 + parser.add_argument("--task_list", type=str, default="task_list_cifar10") + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--debug", action="store_true", default=False) + parser.add_argument("--num_nodes", type=int, default=3) + parser.add_argument("--node_comp_ability", type=float, default=1.0) + parser.add_argument("--predictor_gamma", type=float, default=0.9) + parser.add_argument("--final_time", type=int, default=5) + parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") + parser.add_argument("--generate_gap", type=int, default=1) + parser.add_argument("--pre_generate", action="store_true", default=True) + parser.add_argument("--num_episodes", type=int, default=2) + parser.add_argument("--exploration_round", type=int, default=10) + + return parser \ No newline at end of file diff --git a/StreamLearn/Simulator/task/dataset.py b/StreamLearn/Simulator/task/dataset.py index ea71945..0821f3e 100644 --- a/StreamLearn/Simulator/task/dataset.py +++ b/StreamLearn/Simulator/task/dataset.py @@ -2,14 +2,14 @@ import numpy as np import torch from torch.utils.data import Dataset, DataLoader from torchvision import datasets, transforms -from torchaudio.datasets import YESNO -import torchaudio.transforms as T +# from torchaudio.datasets import YESNO +# import torchaudio.transforms as T -import torchtext; torchtext.disable_torchtext_deprecation_warning() -from torchtext.datasets import IMDB -from torchtext.data.utils import get_tokenizer -from torchtext.vocab import build_vocab_from_iterator -from torchtext.data.functional import to_map_style_dataset +# import torchtext; torchtext.disable_torchtext_deprecation_warning() +# from torchtext.datasets import IMDB +# from torchtext.data.utils import get_tokenizer +# from torchtext.vocab import build_vocab_from_iterator +# from torchtext.data.functional import to_map_style_dataset def get_dataloader( @@ -28,12 +28,12 @@ def get_dataloader( dataset_class = Cifar10Dataset elif dataset_name == 'cifar100': dataset_class = Cifar100Dataset - elif dataset_name == 'rl': - dataset_class = RLDataset - elif dataset_name == 'imdb': - dataset_class = IMDBDataset - elif dataset_name == 'yesno': - dataset_class = YesNoDataset + # elif dataset_name == 'rl': + # dataset_class = RLDataset + # elif dataset_name == 'imdb': + # dataset_class = IMDBDataset + # elif dataset_name == 'yesno': + # dataset_class = YesNoDataset else: raise ValueError(f"Unknown dataset: {dataset_name}") @@ -108,102 +108,102 @@ class Cifar100Dataset(TransformedDataset): self.data = datasets.CIFAR100(root=root, train=train, transform=self.transform, download=True) -class RLDataset(TransformedDataset): - - def __init__(self, data_batch, label_batch, train=True, transform=None, **kwargs): - self.train = train - if transform is None: - self.transform = transforms.Compose([ - transforms.Resize([196, 196]), - transforms.ToTensor(), - transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) - ]) - else: - self.transform = transform - - assert len(data_batch) == len(label_batch) - num_data = len(data_batch) - self.images = [] - self.labels = [] - index = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 11: 6, 12: 7} - for i in range(num_data): - self.images.append(data_batch[i]) - tmp = np.zeros(8) - tmp[index[label_batch[i]]] = 1 - self.labels.append(tmp) +# class RLDataset(TransformedDataset): + +# def __init__(self, data_batch, label_batch, train=True, transform=None, **kwargs): +# self.train = train +# if transform is None: +# self.transform = transforms.Compose([ +# transforms.Resize([196, 196]), +# transforms.ToTensor(), +# transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) +# ]) +# else: +# self.transform = transform + +# assert len(data_batch) == len(label_batch) +# num_data = len(data_batch) +# self.images = [] +# self.labels = [] +# index = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 11: 6, 12: 7} +# for i in range(num_data): +# self.images.append(data_batch[i]) +# tmp = np.zeros(8) +# tmp[index[label_batch[i]]] = 1 +# self.labels.append(tmp) - def __getitem__(self, item): - img = self.images[item] - label = self.labels[item] - if self.transform is not None: - img = self.transform(img) - label = torch.from_numpy(label).type(torch.LongTensor) - label = torch.argmax(label) # Convert one-hot to class index - return img, label - - def __len__(self): - return len(self.images) - - -class YesNoDataset(TransformedDataset): - - def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): - if transform is None: - self.transform = torch.nn.Sequential( - T.MelSpectrogram(sample_rate=8000, n_mels=64), - T.AmplitudeToDB() - ) - else: - self.transform = transform - self.data = YESNO(root=root, download=True) - - def pad_seq(self, batch): - max_len = max([x[0].shape[-1] for x in batch]) - batch_size = len(batch) - num_mels = batch[0][0].shape[1] - padded_batch = torch.zeros(batch_size, 1, num_mels, max_len) - labels = [] - for i, (x, y) in enumerate(batch): - padded_batch[i, :, :, :x.shape[-1]] = x - labels.append(y) - labels = torch.tensor(labels, dtype=torch.long) - return padded_batch, labels - - def __getitem__(self, index): - waveform, sample_rate, labels = self.data[index] - if self.transform is not None: - waveform = self.transform(waveform) - label = torch.tensor(labels[0], dtype=torch.long) - return waveform, label - - -class IMDBDataset(TransformedDataset): - - def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): - self.split = 'train' if train else 'test' - self.tokenizer = get_tokenizer('basic_english') - self.data = IMDB(root=root, split=self.split) +# def __getitem__(self, item): +# img = self.images[item] +# label = self.labels[item] +# if self.transform is not None: +# img = self.transform(img) +# label = torch.from_numpy(label).type(torch.LongTensor) +# label = torch.argmax(label) # Convert one-hot to class index +# return img, label + +# def __len__(self): +# return len(self.images) + + +# class YesNoDataset(TransformedDataset): + +# def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): +# if transform is None: +# self.transform = torch.nn.Sequential( +# T.MelSpectrogram(sample_rate=8000, n_mels=64), +# T.AmplitudeToDB() +# ) +# else: +# self.transform = transform +# self.data = YESNO(root=root, download=True) + +# def pad_seq(self, batch): +# max_len = max([x[0].shape[-1] for x in batch]) +# batch_size = len(batch) +# num_mels = batch[0][0].shape[1] +# padded_batch = torch.zeros(batch_size, 1, num_mels, max_len) +# labels = [] +# for i, (x, y) in enumerate(batch): +# padded_batch[i, :, :, :x.shape[-1]] = x +# labels.append(y) +# labels = torch.tensor(labels, dtype=torch.long) +# return padded_batch, labels + +# def __getitem__(self, index): +# waveform, sample_rate, labels = self.data[index] +# if self.transform is not None: +# waveform = self.transform(waveform) +# label = torch.tensor(labels[0], dtype=torch.long) +# return waveform, label + + +# class IMDBDataset(TransformedDataset): + +# def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): +# self.split = 'train' if train else 'test' +# self.tokenizer = get_tokenizer('basic_english') +# self.data = IMDB(root=root, split=self.split) - self.vocab = build_vocab_from_iterator(self.yield_tokens(self.data), specials=["", ""]) - self.vocab.set_default_index(self.vocab[""]) +# self.vocab = build_vocab_from_iterator(self.yield_tokens(self.data), specials=["", ""]) +# self.vocab.set_default_index(self.vocab[""]) - self.data = to_map_style_dataset(self.data) +# self.data = to_map_style_dataset(self.data) - def yield_tokens(self, data_iter): - for _, text in data_iter: - yield self.tokenizer(text) +# def yield_tokens(self, data_iter): +# for _, text in data_iter: +# yield self.tokenizer(text) - def _text_pipeline(self, x): - return self.vocab(self.tokenizer(x)) +# def _text_pipeline(self, x): +# return self.vocab(self.tokenizer(x)) - def _label_pipeline(self, y): - return 0 if y == 'neg' else 1 +# def _label_pipeline(self, y): +# return 0 if y == 'neg' else 1 - def collate_batch(self, batch): - labels, texts = [], [] - for label, text in batch: - labels.append(self._label_pipeline(label)) - texts.append(torch.tensor(self._text_pipeline(text), dtype=torch.int64)) - labels = torch.tensor(labels, dtype=torch.int64) - texts = torch.nn.utils.rnn.pad_sequence(texts, batch_first=True) - return texts, labels +# def collate_batch(self, batch): +# labels, texts = [], [] +# for label, text in batch: +# labels.append(self._label_pipeline(label)) +# texts.append(torch.tensor(self._text_pipeline(text), dtype=torch.int64)) +# labels = torch.tensor(labels, dtype=torch.int64) +# texts = torch.nn.utils.rnn.pad_sequence(texts, batch_first=True) +# return texts, labels diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index 5f060cf..db48d13 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -941,38 +941,39 @@ def train_and_evaluate_stream_BBDM(args_address,runT): GEAR是一个以GPU为中心、针对现代高性能服务器GPU硬件特性和高性能网络链接设计优化的分布式流数据存储管理系统,以支持基于大规模流数据的交互式学习。 -该系统主要包含 `GEAR系统实现`和`最简样例运行代码`两部分 ,相关代码参见目录 -* StreamLearn/Algorithm/GEAR -* StreamLearn/tests/GEAR +该系统主要包含 `GEAR系统实现`、`GEAR样例运行参数`和`GEAR样例运行测试`三部分,相关代码参见目录 +* StreamLearn/Algorithm/GEAR/ +* StreamLearn/Config/GEAR.py +* StreamLearn/tests/test_GEAR.py **样例运行** -首先,参考`StreamLearn/Algorithm/GEAR/README.md`文档中搭建基本的PyTorch-GPU运行环境,然后运行在目录下运行`pip install`命令以编译安装GEAR系统到Python环境: +首先,参考`StreamLearn/Algorithm/GEAR/README.md`文档中搭建基本的PyTorch-GPU运行环境。该环境还包含了运行GEAR所必需的NCCL等组件。然后在目录下运行`pip install`命令以编译安装GEAR系统到Python环境: ```shell cd StreamLearn/Algorithm/GEAR/ pip install -r requirements. pip install . ``` -其次,参考运行`StreamLearn/tests/GEAR/offline/single-node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 +其次,参考运行`StreamLearn/Algorithm/GEAR/tests/single-node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 ```shell -cd StreamLearn/tests/GEAR/offline/single-node/ -python create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt +cd StreamLearn/Algorithm/GEAR/tests/single-node/create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt ``` -之后,运行`StreamLearn/tests/GEAR/offline/single-node/run.sh`脚本以快速运行样例程序。 +之后,运行`python -m StreamLearn.tests.test_GEAR`即可快速运行样例程序。 **和现有分布式训练工作流集成** -如果希望在DeepSpeed分布式训练工作流集成调用GEAR,请参考`StreamLearn/tests/GEAR/offline/single-node/main.py`中的`setup`方法进行环境初始化和数据层加载: +如果希望在DeepSpeed分布式训练工作流集成调用GEAR,请参考`StreamLearn/tests/test_GEAR.py`中的`setup`方法进行环境初始化和数据层加载: ```python def setup(): ... + if not dist.is_initialized(): - # 初始化DeepSpeed分布式环境 - deepspeed.init_distributed(dist_init_required=True) - # GEAR数据层运行时加载 - gear.init() + # 初始化DeepSpeed分布式环境 + deepspeed.init_distributed(dist_init_required=True) + # GEAR数据层运行时加载 + gear.init() # GEAR数据层定义 # 如此处GEAR从离线数据集中记载 @@ -1004,7 +1005,7 @@ def setup(): loader = gear.loader.OfflineLoader.create(**offline_loader_params) ``` -其后在模型训练/推理过程中,GEAR数据层的交互方式参考`StreamLearn/tests/GEAR/offline/single-node/models/mlp/funcs.py`文件中`train_step`方法: +其后在模型训练/推理过程中,GEAR数据层的交互方式参考`StreamLearn/Algorithm/GEAR/tests/single-node/models/mlp/funcs.py`文件中`train_step`方法: ```python def train_step( loader, @@ -1173,8 +1174,8 @@ while not terminal: AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应多臂赌博机算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 -- StreamLearn/Simulator/policy/AOGS.py -- StreamLearn/Simulator/test/test_AOGS.py +- StreamLearn/Simulator/policy/AEAGS.py +- StreamLearn/Simulator/test/test_AEAGS.py 首先,按照以下方式构造测试任务序列 ```python diff --git a/StreamLearn/tests/test_GEAR.py b/StreamLearn/tests/test_GEAR.py new file mode 100644 index 0000000..e31c56a --- /dev/null +++ b/StreamLearn/tests/test_GEAR.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +import os +import sys +import importlib +import subprocess +from datetime import datetime +from typing import Union, Callable + +import gymnasium as gym +import deepspeed +import numpy as np +import torch +import torch.nn as nn +import torch.distributed as dist +from torch.distributions import Normal +from torch.utils.tensorboard import SummaryWriter + +import gear +from gear.mpu import ModelParallelismUnit +from StreamLearn.Config.GEAR import get_gear_config +from StreamLearn.Algorithm.GEAR.tests.single_node import models + +def run_deepspeed(): + parser = get_gear_config() + args, unknown = parser.parse_known_args() + + cmd = [ + "deepspeed", + "--master_port", "42001", + "--include", "localhost:0", + "--hostfile", "./hostfile", + # os.path.abspath(__file__), + "--module", "StreamLearn.tests.test_GEAR", + "--deepspeed_config", args.deepspeed_config + ] + + for arg in unknown: + cmd.append(arg) + + print("Launching DeepSpeed with command:") + print(" ".join(cmd)) + + subprocess.check_call(cmd) + +train_step: Callable = None +eval_step: Callable = None + +def rank_0_evaluation( + model, + eval_env, + num_eval_trajectories, + step_id, + tensorboard_writer: Union[SummaryWriter, None], +): + if dist.get_rank() != 0: + dist.barrier() + else: + eval_ret = eval_step( + model, eval_env, num_eval_trajectories, step_id, tensorboard_writer + ) + dist.barrier() + return eval_ret + +def rank_0_get_tensorboard_writer(args) -> Union[SummaryWriter, None]: + if not dist.is_initialized(): + return None + if dist.get_rank() == 0 and args.enable_tensorboard: + return SummaryWriter( + log_dir=os.path.abspath(args.tensorboard_logdir) + f"/{args.expr_name}" + ) + else: + return None + +def setup(args): + global train_step + global eval_step + + if not dist.is_initialized(): + deepspeed.init_distributed(dist_init_required=True) + gear.init() + + offline_loader_params = { + "data_path": args.data_path, + "mpu": None, + "batch_size": 32, + "sampling_method": "Uniform", + "patterns": [ + { + "name": "observations", + "pad": "tail", + "offset": -1000, + "length": 1000, + }, + { + "name": "actions", + "pad": "tail", + "offset": -1000, + "length": 1000, + }, + ], + } + + mpu = ModelParallelismUnit(device=torch.device("cuda")) + mpu.build_ds_mpu() + offline_loader_params["mpu"] = mpu + offline_loader_params["attach"] = int(os.environ["LOCAL_RANK"]) != 0 + loader = gear.loader.OfflineLoader.create(**offline_loader_params) + + table_spec = loader.table_spec + in_dim = np.prod( + table_spec.column_specs[table_spec.index("observations")].shape + ).item() + # model_module = importlib.import_module("models." + args.model, models) + print(models, args.model) + model_module = getattr(models, args.model) + raw_model = model_module.Model(in_dim=in_dim, hidden_dim=128, out_dim=3) + train_step = model_module.train_step + eval_step = model_module.eval_step + model, optimizer, _, _ = deepspeed.initialize( + args=args, model=raw_model, model_parameters=raw_model.parameters() + ) + torch.cuda.synchronize() + tensorboard_writer = rank_0_get_tensorboard_writer(args) + + return loader, model, optimizer, tensorboard_writer + +def run_training( + loader, + model, + optimizer, + num_iter, + eval_interval, + num_eval_trajectories, + tensorboard_writer: Union[SummaryWriter, None], +): + model.train() + if dist.is_initialized(): + eval_env = gym.make("Hopper-v4") if dist.get_rank() == 0 else None + else: + eval_env = None + # eval_env = gym.make("Hopper-v4") if dist.get_rank() == 0 else None + + for step_id in range(num_iter): + train_step(loader, model, optimizer, step_id, tensorboard_writer) + + if step_id % eval_interval == 0: + rank_0_evaluation( + model=model, + eval_env=eval_env, + num_eval_trajectories=num_eval_trajectories, + step_id=step_id, + tensorboard_writer=tensorboard_writer, + ) + +if __name__ == "__main__": + if "RANK" not in os.environ: + print("Launching via DeepSpeed...") + run_deepspeed() + sys.exit(0) + + parser = get_gear_config() + args = parser.parse_args() + + if dist.is_initialized(): + print(f"[Rank {dist.get_rank()}] Starting training with config:") + else: + print("[Single Process] Starting training with config:") + for k, v in vars(args).items(): + print(f" {k}: {v}") + + loader, model, optimizer, tbwriter = setup(args) + run_training( + loader=loader, + model=model, + optimizer=optimizer, + num_iter=args.num_iter, + eval_interval=args.eval_interval, + num_eval_trajectories=args.num_eval_trajectories, + tensorboard_writer=tbwriter, + ) + + if tbwriter: + tbwriter.close() + # print(f"[Rank {dist.get_rank()}] Training completed successfully.") + + if dist.is_initialized(): + print(f"[Rank {dist.get_rank()}] Training completed successfully.") + else: + print("[Single Process] Training completed successfully.") \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f596737..34ff80f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "scikit-learn>=1.0.2", "torch>=1.11.0", "torchvision>=0.11.2", - "torchtext>=0.11.1", "torch_geometric>=2.3.0", "cvxopt>=1.3.0", "matplotlib>=3.5.0", -- Gitee From ecccd679e3d3f35eac17d70f4dc54aee17642247 Mon Sep 17 00:00:00 2001 From: FineArtz Date: Thu, 7 Aug 2025 12:30:44 +0000 Subject: [PATCH 4/9] update AbdGen --- StreamLearn/Algorithm/AbdGen/README.md | 13 +++ .../AbdGen/abd_functionals/abduction.py | 5 +- .../create_dataset/create_mario_negative.py | 16 ++- .../create_dataset/create_mario_positive.py | 17 +++- .../AbdGen/exp_config/rule_learning_config.py | 5 +- .../AbdGen/functionalities/rule_learning.py | 57 ++++++++--- StreamLearn/Algorithm/AbdGen/main.py | 25 ++--- StreamLearn/Algorithm/AbdGen/models/model.py | 4 +- StreamLearn/Config/AbdGen.py | 98 +++++++++++++++++++ StreamLearn/legacy/README.md | 14 +-- StreamLearn/tests/test_AbdGen.py | 52 ++++++++++ 11 files changed, 260 insertions(+), 46 deletions(-) create mode 100644 StreamLearn/Config/AbdGen.py create mode 100644 StreamLearn/tests/test_AbdGen.py diff --git a/StreamLearn/Algorithm/AbdGen/README.md b/StreamLearn/Algorithm/AbdGen/README.md index 1321063..3601d53 100644 --- a/StreamLearn/Algorithm/AbdGen/README.md +++ b/StreamLearn/Algorithm/AbdGen/README.md @@ -9,6 +9,19 @@ Algorithm/AbdGen ## 运行环境: - 神经生成式模型与逻辑推理模块分别基于PyTorch及Swi-Prolog框架。 +### 安装Swi-Prolog框架 +对于Linux系统,可以直接通过添加PPA来安装。首先执行 +```shell +apt install software-properties-common +``` +安装后,可以使用`apt-add-repository`命令添加Swi-Prolog的PPA,再通过`apt`安装 +```shell +apt-add-repository ppa:swi-prolog/stable +apt update +apt install swi-prolog +``` +安装后,可以通过`--swipl`参数设置运行AbdGen时swipl的路径。 + ## 执行训练: - 运行根目录下“main.py"文件。 - 需根据"main.py"下的parser arguments设计正确的文件路径。 diff --git a/StreamLearn/Algorithm/AbdGen/abd_functionals/abduction.py b/StreamLearn/Algorithm/AbdGen/abd_functionals/abduction.py index abf29ec..97e5917 100644 --- a/StreamLearn/Algorithm/AbdGen/abd_functionals/abduction.py +++ b/StreamLearn/Algorithm/AbdGen/abd_functionals/abduction.py @@ -63,7 +63,8 @@ class AbdTrainer: fact_str = "nn('{}',{},{}).\n".format(vname, lname, prob[k]) prob_facts_str = prob_facts_str + fact_str else: - lname = list(term_grds[i]) + # lname = list(term_grds[i]) + lname = term_grds[i].tolist() fact_str = "nn('{}',{},{}).\n".format(vname, lname, 1.0) prob_facts_str = prob_facts_str + fact_str prob_facts_str = prob_facts_str + "\n" @@ -72,7 +73,7 @@ class AbdTrainer: # Run prolog to get the output. # Return the STDOUT and error codes (-1 for runtime error, -2 for timeout) async def run_pl(self, file_path): - cmd = self.swipl+ " --stack-limit=8g -s {} -g a -t halt".format(file_path) + cmd = self.swipl+ " --stack-limit=8g -s {} -g a -t halt --traditional".format(file_path) proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py index 424fd61..1d216a2 100644 --- a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py @@ -301,13 +301,23 @@ def create_mario_dataset(folder): # Save images, moves and positions for dataset in ['all']: - np.savez(os.path.join(folder, f'{dataset}_mario_neg.npz'), images=images[dataset], - pos=positions[dataset], target_pos=target_pos[dataset], moves=moves[dataset], agents=agent_dict[dataset], targets=target_dict[dataset], - bkgs=background_dict[dataset], frames=frame_dict[dataset]) + np.savez(os.path.join(folder, f'mario_neg.npz'), + images=np.array(images[dataset], dtype=object), + pos=np.array(positions[dataset], dtype=object), + target_pos=target_pos[dataset], + moves=np.array(moves[dataset], dtype=object), + agents=agent_dict[dataset], + targets=target_dict[dataset], + bkgs=background_dict[dataset], + frames=frame_dict[dataset] + ) # Save info files with open(os.path.join(folder, 'info.json'), 'w') as file: json.dump(info, file, indent=4) if __name__ == '__main__': + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + for icon in ICONS: + ICONS[icon] = os.path.join(script_dir, ICONS[icon]) create_mario_dataset('dataset/mario') \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py index d22efa7..d31639f 100644 --- a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py @@ -312,13 +312,24 @@ def create_mario_dataset(folder): info['moves'][dataset][str(m)] += 1 idx += 1 # Check dimensions + assert len(images['all']) == tot_imgs # Save images, moves and positions for dataset in ['all']: - np.savez(os.path.join(folder, f'mario_pos.npz'), images=images[dataset], - pos=positions[dataset], target_pos=target_pos[dataset], moves=moves[dataset], agents=agent_dict[dataset], targets=target_dict[dataset], - bkgs=background_dict[dataset], frames=frame_dict[dataset]) + np.savez(os.path.join(folder, f'mario_pos.npz'), + images=images[dataset], + pos=np.array(positions[dataset], dtype=object), + target_pos=target_pos[dataset], + moves=np.array(moves[dataset], dtype=object), + agents=agent_dict[dataset], + targets=target_dict[dataset], + bkgs=background_dict[dataset], + frames=frame_dict[dataset] + ) if __name__ == '__main__': + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + for icon in ICONS: + ICONS[icon] = os.path.join(script_dir, ICONS[icon]) create_mario_dataset('dataset/mario') \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/exp_config/rule_learning_config.py b/StreamLearn/Algorithm/AbdGen/exp_config/rule_learning_config.py index ca0f14f..f17fcf1 100644 --- a/StreamLearn/Algorithm/AbdGen/exp_config/rule_learning_config.py +++ b/StreamLearn/Algorithm/AbdGen/exp_config/rule_learning_config.py @@ -7,7 +7,8 @@ https://arxiv.org/abs/2310.17451 """ import torch -from data_management.dataloader import * + +from StreamLearn.Algorithm.AbdGen.data_management.dataloader import * mario_config = {'dataset': 'mario', @@ -18,7 +19,7 @@ mario_config = {'dataset': 'mario', 'sym_z_dim': 32, 'subsym_z_dim': 32, 'train_lr': 1e-4, - 'num_train_iteration': 5000, + 'num_train_iteration': 5, #5000, 'abd_num_pos_case_per_bag': 5, 'abd_num_neg_case_per_bag': 20, 'abd_num_bag_per_batch': 8, diff --git a/StreamLearn/Algorithm/AbdGen/functionalities/rule_learning.py b/StreamLearn/Algorithm/AbdGen/functionalities/rule_learning.py index 5c2a083..918df11 100644 --- a/StreamLearn/Algorithm/AbdGen/functionalities/rule_learning.py +++ b/StreamLearn/Algorithm/AbdGen/functionalities/rule_learning.py @@ -12,23 +12,23 @@ from __future__ import print_function from __future__ import print_function import os, time, itertools import numpy as np -import torch -import torch.optim as optim -import torch.nn.functional as F from matplotlib import pyplot as plt - -from models.model import Model -from abd_functionals.abduction import AbdTrainer from tqdm import tqdm -from torch.optim.lr_scheduler import MultiStepLR import pickle as pkl import pandas as pd from datetime import timedelta +import torch +import torch.optim as optim +import torch.nn.functional as F +from torch.optim.lr_scheduler import MultiStepLR + +from StreamLearn.Algorithm.AbdGen.models.model import Model +from StreamLearn.Algorithm.AbdGen.abd_functionals.abduction import AbdTrainer # Suppress the warning class TrainingProcessMonitor: - def __init__(self, num_total_iterations, num_sym_factor, plot_gap=10): + def __init__(self, num_total_iterations, num_sym_factor, plot_gap=1): self.num_total_iterations, self.num_sym_factor = num_total_iterations, num_sym_factor self.plot_gap = plot_gap self.singleton_names = ['recon_loss'] @@ -193,7 +193,7 @@ def get_valid_train_labels(label_pos, abd_labels, num_bag_per_batch, num_pos_cas valid_label_idx[i][k][:] = 0 break else: - abd_labels[i] = [np.zeros(true_labels[i][j].shape, dtype=np.int) + abd_labels[i] = [np.zeros(true_labels[i][j].shape, dtype=int) # TODO: only for placeholder. Can be improved. for j in range(len(true_labels[i]))] # true_labels[i] for j in range(num_pos_case_per_bag): @@ -283,12 +283,31 @@ def train(config, path_manager, model_object): # 合并图像,调整维度为(batch_size, channels, height, width),转移成为pytorch张量 imgs = torch.FloatTensor(np.concatenate(img_pos + img_neg, axis=0)).to(device).permute(0, 3, 1, 2) # 分割pos图像与标签 - grd_pos_per_bag = np.split(grd_pos, batch_part_pos) + # grd_pos_per_bag = np.split(grd_pos, batch_part_pos) + # grd_pos_per_bag = [grd_pos_per_bag[i * num_pos_case_per_bag:(i + 1) * num_pos_case_per_bag] + # for i in range(num_bag_per_batch)] + # label_pos_per_bag = np.split(label_pos, batch_part_pos) + # label_pos_per_bag = [label_pos_per_bag[i * num_pos_case_per_bag:(i + 1) * num_pos_case_per_bag] + # for i in range(num_bag_per_batch)] + grd_pos_per_bag = [] + start_idx = 0 + for end_idx in batch_part_pos: + grd_pos_per_bag.append(grd_pos[start_idx:end_idx]) + start_idx = end_idx + if start_idx < len(grd_pos): + grd_pos_per_bag.append(grd_pos[start_idx:]) grd_pos_per_bag = [grd_pos_per_bag[i * num_pos_case_per_bag:(i + 1) * num_pos_case_per_bag] - for i in range(num_bag_per_batch)] - label_pos_per_bag = np.split(label_pos, batch_part_pos) + for i in range(num_bag_per_batch)] + + label_pos_per_bag = [] + start_idx = 0 + for end_idx in batch_part_pos: + label_pos_per_bag.append(label_pos[start_idx:end_idx]) + start_idx = end_idx + if start_idx < len(label_pos): + label_pos_per_bag.append(label_pos[start_idx:]) label_pos_per_bag = [label_pos_per_bag[i * num_pos_case_per_bag:(i + 1) * num_pos_case_per_bag] - for i in range(num_bag_per_batch)] + for i in range(num_bag_per_batch)] # -------------------------------- model forward pass ----------------------------- adv_alpha = 10 @@ -462,6 +481,15 @@ def train(config, path_manager, model_object): with open(os.path.join(result_save_path, 'record_' + str(train_itr)), 'wb') as f: pkl.dump(train_monitor.recorder, f) + # remove ununsed tensor + del sym_z, subsym_z, adversarial_label, all_recon_x + if not learn_flag: + del abd_vq_losses, recon_loss + # del vq_losses, grounding_pred, grounding_pred_softmax, grounding_label, adversarial_pred, \ + # adversarial_pred_softmax, adversarial_label, adversarial_pred_detached, recon_x + model.zero_grad() + torch.cuda.empty_cache() + # 每五十轮记录一下所花时间 if (train_itr + 1) % 50 == 0: # 计算当前时间与开始时间的差值,得到训练所用时间 @@ -473,6 +501,3 @@ def train(config, path_manager, model_object): # 将训练时间保存到txt文件中 with open(os.path.join(path_manager.get_spec_path('result'), 'training_time_test.txt'), 'a') as file: file.write(f"Iteration {train_itr + 1}: {formatted_time}\n") - - - diff --git a/StreamLearn/Algorithm/AbdGen/main.py b/StreamLearn/Algorithm/AbdGen/main.py index 62e447d..e254418 100644 --- a/StreamLearn/Algorithm/AbdGen/main.py +++ b/StreamLearn/Algorithm/AbdGen/main.py @@ -8,9 +8,10 @@ https://arxiv.org/abs/2310.17451 import os import argparse -import exp_config.rule_learning_config as rule_learning_config -import functionalities.rule_learning as rule_learning -from utils.utils import set_seed, PathManager + +import StreamLearn.Algorithm.AbdGen.exp_config.rule_learning_config as rule_learning_config +import StreamLearn.Algorithm.AbdGen.functionalities.rule_learning as rule_learning +from StreamLearn.Algorithm.AbdGen.utils.utils import set_seed, PathManager if __name__ == "__main__": @@ -36,21 +37,21 @@ if __name__ == "__main__": help='location of swipl', default='/usr/bin/swipl') parser.add_argument('--bk_file', type=str, help='location of background knowledge file, need to be absolute path', - default='/home/worker/code/AbdGen_Github/prolog/mario_rule_learning_bk.pl') + default='prolog/mario_rule_learning_bk.pl') parser.add_argument('--exp_root_path', type=str, - help='exp root path', default='/home/worker/exp/AbdGen/') + help='exp root path', default='./') parser.add_argument('--dataset_path', type=str, - help='dataset path', default='dataset/') + help='dataset path', default='dataset/mario/') parser.add_argument('--model_path', type=str, - help='model path', default='model/') + help='model path', default='logs/AbdGen/model/') parser.add_argument('--tmp_path', type=str, - help='temp path', default='tmp/') + help='temp path', default='logs/AbdGentmp/') parser.add_argument('--rule_path', type=str, - help='rule path', default='rule/') + help='rule path', default='logs/AbdGenrule/') parser.add_argument('--result_path', type=str, - help='result path', default='result/') + help='result path', default='logs/AbdGenresult/') parser.add_argument('--pl_tmp_path', type=str, - help='prolog tmp file path', default='pl_tmp/') + help='prolog tmp file path', default='logs/AbdGenpl_tmp/') config_mapper = {'rule_learning': {'mario': rule_learning_config.mario_config}, } @@ -63,6 +64,8 @@ if __name__ == "__main__": config_object['GPU'] = args.GPU config_object['num_code_heads'] = args.num_code_heads config_object['swipl'] = args.swipl + script_dir = os.path.dirname(os.path.abspath(__file__)) + args.bk_file = os.path.join(script_dir, args.bk_file) config_object['bk_file'] = args.bk_file path_object = { 'exp_root_path': args.exp_root_path, diff --git a/StreamLearn/Algorithm/AbdGen/models/model.py b/StreamLearn/Algorithm/AbdGen/models/model.py index 25c48fe..8a59e58 100644 --- a/StreamLearn/Algorithm/AbdGen/models/model.py +++ b/StreamLearn/Algorithm/AbdGen/models/model.py @@ -126,8 +126,8 @@ class CNNEncoder(nn.Module): kernel_size = (4, 4, 4, 4) strides = (2, 2, 2, 2) n_channels = (16, 32, 64, 128) - self.output_height_list = np.zeros(4, dtype=np.int) - self.padding_list = np.zeros(4, dtype=np.int) + self.output_height_list = np.zeros(4, dtype=int) + self.padding_list = np.zeros(4, dtype=int) self.input_size, self.output_dim = input_size, output_dim self.output_height_list[0] = int(np.floor(self.input_size[0]/strides[0])) self.padding_list[0] = np.ceil(((self.output_height_list[0]-1)*strides[0]-self.input_size[0]+kernel_size[0])/2) diff --git a/StreamLearn/Config/AbdGen.py b/StreamLearn/Config/AbdGen.py new file mode 100644 index 0000000..2dc640a --- /dev/null +++ b/StreamLearn/Config/AbdGen.py @@ -0,0 +1,98 @@ +import argparse +import torch + +from StreamLearn.Algorithm.AbdGen.data_management.dataloader import * + + +parser = argparse.ArgumentParser(description='AbdGen Configuration') + +# =================== basic setup ======================= +parser.add_argument('--task', type=str, help='learning task', default='rule_learning') +parser.add_argument('--dataset', type=str, + help='dataset to work on', default='mario') +parser.add_argument('--exp_name', type=str, + help='name of experiments', default='exp_0') +parser.add_argument('--GPU', type=str, help='# of GPU to use', default='0') +parser.add_argument('--num_cpu_core', type=int, help='CPU core to use', default='16') +parser.add_argument('--seed', type=int, help='random seed to use in the experiments', default=42) +# =================== training setup ======================== +parser.add_argument('--load_model', action='store_true') +parser.add_argument('--model_name', type=str, help='name of the model to load', default='model.999') +parser.add_argument('--start_train_iteration', type=int, help='start train iteration', default=0) +# ==================== parameter setup ========================== +parser.add_argument('--num_code_heads', type=int, help='number of heads in codebooks', default=1) +# =================== path setup ======================== +parser.add_argument('--swipl', type=str, + help='location of swipl', default='/usr/bin/swipl') +parser.add_argument('--bk_file', type=str, + help='location of background knowledge file, need to be absolute path', + default='StreamLearn/Algorithm/AbdGen/prolog/mario_rule_learning_bk.pl') +parser.add_argument('--exp_root_path', type=str, + help='exp root path', default='./') +parser.add_argument('--dataset_path', type=str, + help='dataset path', default='dataset/mario/') +parser.add_argument('--model_path', type=str, + help='model path', default='logs/AbdGen/model/') +parser.add_argument('--tmp_path', type=str, + help='temp path', default='logs/AbdGentmp/') +parser.add_argument('--rule_path', type=str, + help='rule path', default='logs/AbdGenrule/') +parser.add_argument('--result_path', type=str, + help='result path', default='logs/AbdGenresult/') +parser.add_argument('--pl_tmp_path', type=str, + help='prolog tmp file path', default='logs/AbdGenpl_tmp/') + +args, unknown = parser.parse_known_args() + +mario_config = {'dataset': 'mario', + 'input_size': [100, 100, 3], + 'dataloader': MarioDataLoader, + 'num_sym_factor': 1, + 'sym_dim_list': [9], + 'sym_z_dim': 32, + 'subsym_z_dim': 32, + 'train_lr': 1e-4, + 'num_train_iteration': 5, #5000, + 'abd_num_pos_case_per_bag': 5, + 'abd_num_neg_case_per_bag': 20, + 'abd_num_bag_per_batch': 8, + 'grounding_to_label_table': { + (0, 0): 0, + (1, 0): 1, + (2, 0): 2, + (2, 1): 3, + (1, 1): 4, + (0, 1): 5, + (0, 2): 6, + (1, 2): 7, + (2, 2): 8}, + 'grounding_to_label_table_str': { + '[0,0]': 0, + '[1,0]': 1, + '[2,0]': 2, + '[2,1]': 3, + '[1,1]': 4, + '[0,1]': 5, + '[0,2]': 6, + '[1,2]': 7, + '[2,2]': 8}, + 'label_to_grounding_table': {-1: (-1, -1), + 0: (0, 0), + 1: (1, 0), + 2: (2, 0), + 3: (2, 1), + 4: (1, 1), + 5: (0, 1), + 6: (0, 2), + 7: (1, 2), + 8: (2, 2)}, + 'integrity_table': + {8: ((3, 7), (2, 4, 6), (1, 5)), + 7: ((4, 6, 8), (1, 3, 5), (0, 2)), + 6: ((5, 7), (0, 4, 8), (1, 3))}, + 'label_names': ['[0, 0]', '[1, 0]', '[2, 0]', '[2, 1]', '[1, 1]', '[0, 1]', '[0, 2]', '[1, 2]', '[2, 2]'], + 'abd_time_limit': 60, + 'recon_loss': torch.nn.MSELoss(reduction='sum'), + 'CE_loss': torch.nn.CrossEntropyLoss(reduction='none'), + 'BCE_loss': torch.nn.BCELoss(reduction='none') +} diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index db48d13..bd0fe27 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -955,9 +955,9 @@ pip install -r requirements. pip install . ``` -其次,参考运行`StreamLearn/Algorithm/GEAR/tests/single-node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 +其次,参考运行`StreamLearn/Algorithm/GEAR/tests/single_node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 ```shell -cd StreamLearn/Algorithm/GEAR/tests/single-node/create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt +cd StreamLearn/Algorithm/GEAR/tests/single_node/create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt ``` 之后,运行`python -m StreamLearn.tests.test_GEAR`即可快速运行样例程序。 @@ -1005,7 +1005,7 @@ def setup(): loader = gear.loader.OfflineLoader.create(**offline_loader_params) ``` -其后在模型训练/推理过程中,GEAR数据层的交互方式参考`StreamLearn/Algorithm/GEAR/tests/single-node/models/mlp/funcs.py`文件中`train_step`方法: +其后在模型训练/推理过程中,GEAR数据层的交互方式参考`StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/funcs.py`文件中`train_step`方法: ```python def train_step( loader, @@ -1117,9 +1117,9 @@ python scripts/script_inference.py --K 5 --dataset ml-1m --temp_type simple python scripts/script_finetune.py --dataset ml-1m --K 5 --train_size 64 --train_type simple --test_type simple --epochs 5 --lr 1e-3 --total_batch_size 64 ~~~ -### 4.4 在线匹配市场中的多臂赌博机最优算法AOGS +### 4.4 在线匹配市场中的Multi-Arm Bandits最优算法AOGS -自适应在线Gale-Shapley算法(AOGS)是一种双边匹配市场的多臂赌博机算法,它将传统的Gale-Shapley算法融入在线多臂赌博机算法中,依据每一轮的反馈动态调整GS算法的各个步骤。AOGS提升了已有的理论懊悔上界保证,这一结果首次在主阶项中消除了对手臂数量的依赖,在玩家数量远小于手臂数量的常见情况下显著提高了理论保证。将多节点的流数据任务调度问题建模为匹配问题后,AOGS算法同样可以用来完成任务调度。 +自适应在线Gale-Shapley算法(AOGS)是一种双边匹配市场的Multi-Arm Bandits算法,它将传统的Gale-Shapley算法融入在线Multi-Arm Bandits算法中,依据每一轮的反馈动态调整GS算法的各个步骤。AOGS提升了已有的理论懊悔上界保证,这一结果首次在主阶项中消除了对手臂数量的依赖,在玩家数量远小于手臂数量的常见情况下显著提高了理论保证。将多节点的流数据任务调度问题建模为匹配问题后,AOGS算法同样可以用来完成任务调度。 该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 - StreamLearn/Simulator/policy/AOGS.py @@ -1169,9 +1169,9 @@ while not terminal: total_reward += rew ``` -### 4.5 在线匹配市场不敏感偏好场景下的多臂赌博机算法AE-AGS +### 4.5 在线匹配市场不敏感偏好场景下的Multi-Arm Bandits算法AE-AGS -AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应多臂赌博机算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 +AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应Multi-Arm Bandits算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 - StreamLearn/Simulator/policy/AEAGS.py diff --git a/StreamLearn/tests/test_AbdGen.py b/StreamLearn/tests/test_AbdGen.py new file mode 100644 index 0000000..39ac7d6 --- /dev/null +++ b/StreamLearn/tests/test_AbdGen.py @@ -0,0 +1,52 @@ +""" +This file is a copyrighted under the BSD 3-clause licence, details of which can be found in the root directory. +Code for +Generating by Understanding: Neural Visual Generation with Logical Symbol Groundings +https://arxiv.org/abs/2310.17451 + +""" + +import os + +from StreamLearn.Config.AbdGen import args, mario_config +import StreamLearn.Algorithm.AbdGen.functionalities.rule_learning as rule_learning +from StreamLearn.Algorithm.AbdGen.utils.utils import set_seed, PathManager + + +def main(): + config_mapper = {'rule_learning': {'mario': mario_config}, + } + if args.seed > 0: + set_seed(args.seed) + config_object = config_mapper[args.task][args.dataset] + config_object['num_cpu_core'] = args.num_cpu_core + config_object['start_train_iteration'] = args.start_train_iteration + config_object['GPU'] = args.GPU + config_object['num_code_heads'] = args.num_code_heads + config_object['swipl'] = args.swipl + # script_dir = os.path.dirname(os.path.abspath(__file__)) + # args.bk_file = os.path.join(script_dir, args.bk_file) + config_object['bk_file'] = args.bk_file + path_object = { + 'exp_root_path': args.exp_root_path, + 'dataset_path': args.dataset_path, + 'model_path': args.model_path, + 'tmp_path': args.tmp_path, + 'rule_path': args.rule_path, + 'result_path': args.result_path, + 'pl_tmp_path': args.pl_tmp_path + } + path_manager = PathManager(path_object, args.dataset, args.exp_name) + if args.load_model: + model_object = {'model_name': args.model_name} + else: + model_object = None + if args.task == 'rule_learning': + rule_learning.train(config_object, path_manager, model_object) + else: + raise NameError('unrecognized task.') + + +if __name__ == "__main__": + main() + \ No newline at end of file -- Gitee From 22ebaf48ca7629239bf4496122ab6f13e943892f Mon Sep 17 00:00:00 2001 From: FineArtz Date: Thu, 7 Aug 2025 12:44:17 +0000 Subject: [PATCH 5/9] update legacy readme --- StreamLearn/legacy/README.md | 146 +++++++++++++++++------------------ 1 file changed, 71 insertions(+), 75 deletions(-) diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index bd0fe27..f2b6b3d 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -1029,95 +1029,91 @@ def train_step( ### 4.2 知识融合流数据生成式学习模型 AbdGen -AbdGen基于反绎学习框架,利用逻辑推理结合生成式神经网络模型,支持知识融合条件下的流数据分布拟合与生成规则学习。 +AbdGen基于反绎学习框架,利用逻辑推理结合生成式神经网络模型,可以支持知识融合条件下的流数据分布拟合与生成规则学习。 -*代码路径*: -Algorithm/AbdGen +该系统主要包含 `AbdGen算法实现`、`AbdGen样例运行参数`和`AbdGen样例运行测试`三部分,相关代码参见目录 +* StreamLearn/Algorithm/AbdGen/ +* StreamLearn/Config/AbdGen.py +* StreamLearn/tests/test_AbdGen.py -*运行环境*: -神经生成式模型与逻辑推理模块分别基于PyTorch及Swi-Prolog框架。 +**安装Swi-Prolog框架** -*执行训练*: -运行根目录下“main.py"文件。 -需根据"main.py"下的parser arguments设计正确的文件路径。 -模型训练参数在"exp_config/rule_learning_config.py"下进行设置。 - -### 4.3 支撑轻量交互的长序列理解与知识融合的大模型框架 ReLLa - -**安装依赖** - -~~~python -pip install -r requirments.txt -~~~ - -**数据处理** - -你可以通过[这里](https://drive.google.com/drive/folders/1av6mZpk0ThmkOKy5Y_dUnsLRdRK8oBjQ?usp=sharing)使用处理好的数据,其中包括原始数据以及检索增强后的数据,。数据集构成:全量测试集,采样后的训练集。Ml-1m/Ml-25m/BookCrossing三个数据集对应的历史序列长度分别为30/30/60。 - -或者你可以通过提供的处理脚本自行处理。处理脚本可以参考[data_preprocess](./data_preprocess/)中[BookCrossing](http://www2.informatik.uni-freiburg.de/~cziegler/BX/), [MovieLens-1M](https://grouplens.org/datasets/movielens/1m/), [MovieLens-25M](https://grouplens.org/datasets/movielens/25m/)对应的jupyter notebook。 - -**语义表征生成** - -首先,利用LLM编码物品信息作为物品的语义表征,后续将被用于进行语义相似度计算以及相似物品检索。 - -```python -python get_semantic_embed.py --model_path XXX --data_set BookCrossing/ml-1m/ml-25m --pooling average +AbdGen除了Pytorch训练框架外,还需要逻辑编程语言框架SWI-Prolog。对于Linux系统,可以直接通过添加PPA来安装。首先执行 +```shell +apt install software-properties-common ``` - -**近邻检索** - -接着,利用上一步得到的语义表征计算物品相似度进行检索,并预存相似物品id。 - -- BookCrossing -```python -python topK_relevant_BookCrossing.py +安装后,可以使用`apt-add-repository`命令添加Swi-Prolog的PPA,再通过`apt`安装 +```shell +apt-add-repository ppa:swi-prolog/stable +apt update +apt install swi-prolog ``` +安装后,可以通过`--swipl`参数设置运行AbdGen时swipl的路径。 -- MovieLens-1M +在训练过程中,AbdGen会生成一系列关于当前状态的`.pl`文件,记录可执行动作和由神经网络计算出的概率。 ```python -python topK_relevant_ml1m.py +# Generate probabilistic facts +def gen_prob_facts_mario(self, grounding): + nn_probs, term_grds, var_names = grounding[0][0], grounding[1], grounding[2][0] + prob_facts_str = "" + for i in range(len(nn_probs)): + case_probs, case_names = nn_probs[i], var_names[i] + for j in range(case_probs.shape[0]): + vname = case_names[j] + if j < case_probs.shape[0]-1: + prob = case_probs[j] + for k in range(len(self.label_names)): + lname = self.label_names[k] + if prob[k] > 0: + fact_str = "nn('{}',{},{}).\n".format(vname, lname, prob[k]) + prob_facts_str = prob_facts_str + fact_str + else: + # lname = list(term_grds[i]) + lname = term_grds[i].tolist() + fact_str = "nn('{}',{},{}).\n".format(vname, lname, 1.0) + prob_facts_str = prob_facts_str + fact_str + prob_facts_str = prob_facts_str + "\n" + return prob_facts_str ``` -- MovieLens-25M -~~~python -python topK_relevant_ml25m.py -~~~ - -**文本数据构造** - -然后,将处理好的id型推荐数据根据定义的提示模板转换成文本型数据。 - -~~~python -python data2json.py --K 5 --temp_type simple --set test --dataset ml-1m -~~~ - -生成的部分文本样例可以参考[./data/ml-1m/proc_data/data/test/test_5_simple.json](./data/ml-1m/proc_data/data/test/test_5_simple.json)。 - -**混合训练集构建** - -作为数据增强,我们提出了构建同时包含原始数据和检索增强的数据的混合训练集。 - +再调用`swipl`命令读取这些文件进行分析,产生模型的训练数据 ```python -python training_set_construction.py --K 5 +# Run prolog to get the output. +# Return the STDOUT and error codes (-1 for runtime error, -2 for timeout) +async def run_pl(self, file_path): + cmd = self.swipl+ " --stack-limit=8g -s {} -g a -t halt --traditional".format(file_path) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + + try: + # 2 seconds timeout + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=self.abd_time_limit) + if proc.returncode == 0: + return 0, stdout.decode('UTF-8') + else: + return -1, stderr.decode('UTF-8') # runtime error + except asyncio.TimeoutError as e: + if proc.returncode is None: + try: + proc.kill() + except OSError: + # Ignore 'no such process' error + pass + return -2, "Timeout " + str(e) # timeout error ``` **样例运行** -你需要在脚本中指定模型权重路径,以及LoRA adapter的存储路径。 - -**推理** - -~~~python -python scripts/script_inference.py --K 5 --dataset ml-1m --temp_type simple -~~~ - -**微调** +直接执行 +```shell +python -m StreamLearn.tests.test_AbdGen +``` +即可运行样例程序。 -~~~python -python scripts/script_finetune.py --dataset ml-1m --K 5 --train_size 64 --train_type simple --test_type simple --epochs 5 --lr 1e-3 --total_batch_size 64 -~~~ -### 4.4 在线匹配市场中的Multi-Arm Bandits最优算法AOGS +### 4.3 在线匹配市场中的Multi-Arm Bandits最优算法AOGS 自适应在线Gale-Shapley算法(AOGS)是一种双边匹配市场的Multi-Arm Bandits算法,它将传统的Gale-Shapley算法融入在线Multi-Arm Bandits算法中,依据每一轮的反馈动态调整GS算法的各个步骤。AOGS提升了已有的理论懊悔上界保证,这一结果首次在主阶项中消除了对手臂数量的依赖,在玩家数量远小于手臂数量的常见情况下显著提高了理论保证。将多节点的流数据任务调度问题建模为匹配问题后,AOGS算法同样可以用来完成任务调度。 @@ -1169,7 +1165,7 @@ while not terminal: total_reward += rew ``` -### 4.5 在线匹配市场不敏感偏好场景下的Multi-Arm Bandits算法AE-AGS +### 4.4 在线匹配市场不敏感偏好场景下的Multi-Arm Bandits算法AE-AGS AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应Multi-Arm Bandits算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 @@ -1221,7 +1217,7 @@ while not terminal: total_reward += rew ``` -### 4.6 基于多智能体强化学习的自适应调度算法MARA +### 4.5 基于多智能体强化学习的自适应调度算法MARA 在多节点的流数据调度任务中,如果将节点看作智能体、待调度的任务看作匹配目标,那么该调度任务可以建模为多智能体强化学习(MARL)问题。在每一时刻`t`,每个节点选择一个任务执行,由此实现任务调度。由于同一时刻待调度的任务数量不断变化,MARA进一步用序列决策模型来建模该MARL问题,结合多智能体Transformer算法,实现对任务的动态评估和调度。 -- Gitee From 5fd35b046e4efcf96e32fd960a8def01f90b0efd Mon Sep 17 00:00:00 2001 From: FineArtz Date: Thu, 7 Aug 2025 12:46:45 +0000 Subject: [PATCH 6/9] update legacy readme --- StreamLearn/legacy/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index f2b6b3d..07670c0 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -948,6 +948,7 @@ GEAR是一个以GPU为中心、针对现代高性能服务器GPU硬件特性和 **样例运行** + 首先,参考`StreamLearn/Algorithm/GEAR/README.md`文档中搭建基本的PyTorch-GPU运行环境。该环境还包含了运行GEAR所必需的NCCL等组件。然后在目录下运行`pip install`命令以编译安装GEAR系统到Python环境: ```shell cd StreamLearn/Algorithm/GEAR/ @@ -963,12 +964,13 @@ cd StreamLearn/Algorithm/GEAR/tests/single_node/create.py --data_path /tmp/gear/ 之后,运行`python -m StreamLearn.tests.test_GEAR`即可快速运行样例程序。 **和现有分布式训练工作流集成** + 如果希望在DeepSpeed分布式训练工作流集成调用GEAR,请参考`StreamLearn/tests/test_GEAR.py`中的`setup`方法进行环境初始化和数据层加载: ```python def setup(): ... - if not dist.is_initialized(): + if not dist.is_initialized(): # 初始化DeepSpeed分布式环境 deepspeed.init_distributed(dist_init_required=True) @@ -1006,9 +1008,10 @@ def setup(): ``` 其后在模型训练/推理过程中,GEAR数据层的交互方式参考`StreamLearn/Algorithm/GEAR/tests/single_node/models/mlp/funcs.py`文件中`train_step`方法: + ```python def train_step( - loader, + loader, model, optimizer, step_id: int, -- Gitee From 3dfd30c9cc5ab84babaa0a2b7c7be7aaf241dca5 Mon Sep 17 00:00:00 2001 From: FineArtz Date: Fri, 8 Aug 2025 04:44:13 +0000 Subject: [PATCH 7/9] remove wandb --- StreamLearn/Config/MARA.py | 1 - StreamLearn/Simulator/policy/mat_runner.py | 32 ++-- StreamLearn/Simulator/task/dataset.py | 134 +-------------- StreamLearn/Simulator/task/models.py | 54 ------ StreamLearn/Simulator/task/rl_agent.py | 189 --------------------- StreamLearn/Simulator/task/task_configs.py | 133 --------------- 6 files changed, 12 insertions(+), 531 deletions(-) delete mode 100644 StreamLearn/Simulator/task/rl_agent.py diff --git a/StreamLearn/Config/MARA.py b/StreamLearn/Config/MARA.py index 49fcf86..276dbe6 100644 --- a/StreamLearn/Config/MARA.py +++ b/StreamLearn/Config/MARA.py @@ -58,7 +58,6 @@ parser.add_argument("--use_centralized_V", action="store_true", default=True) parser.add_argument("--use_linear_lr_decay", action="store_true", default=False) parser.add_argument("--num_env_steps", type=int, default=1000000) -parser.add_argument("--use_wandb", action="store_true", default=False) parser.add_argument("--eval_episodes", type=int, default=2) parser.add_argument("--save_interval", type=int, default=1000) parser.add_argument("--use_eval", action="store_true", default=True) diff --git a/StreamLearn/Simulator/policy/mat_runner.py b/StreamLearn/Simulator/policy/mat_runner.py index 95e1d41..c90edd6 100644 --- a/StreamLearn/Simulator/policy/mat_runner.py +++ b/StreamLearn/Simulator/policy/mat_runner.py @@ -2,7 +2,6 @@ from pathlib import Path import time from typing import Dict, Any import os -import wandb import numpy as np import torch from tensorboardX import SummaryWriter @@ -40,7 +39,6 @@ class Runner: self.n_rollout_threads = config["n_rollout_threads"] self.use_linear_lr_decay = config["use_linear_lr_decay"] self.hidden_size = config["hidden_size"] - self.use_wandb = config["use_wandb"] self.recurrent_N = config["recurrent_N"] self.eval_episodes = config["eval_episodes"] @@ -53,18 +51,14 @@ class Runner: # # dir self.model_dir = config.get("model_dir", None) - if self.use_wandb: - self.save_dir = str(wandb.run.dir) - self.run_dir = str(wandb.run.dir) - else: - self.run_dir = Path(config["run_dir"]) - self.log_dir = str(self.run_dir / 'logs') - if not os.path.exists(self.log_dir): - os.makedirs(self.log_dir) - self.writter = SummaryWriter(self.log_dir) - self.save_dir = str(self.run_dir / 'models') - if not os.path.exists(self.save_dir): - os.makedirs(self.save_dir) + self.run_dir = Path(config["run_dir"]) + self.log_dir = str(self.run_dir / 'logs') + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) + self.writter = SummaryWriter(self.log_dir) + self.save_dir = str(self.run_dir / 'models') + if not os.path.exists(self.save_dir): + os.makedirs(self.save_dir) # policy network self.policy = MATPolicy(self.env, **config) @@ -263,10 +257,7 @@ class Runner: :param total_num_steps: (int) total number of training env steps. """ for k, v in train_infos.items(): - if self.use_wandb: - wandb.log({k: v}, step=total_num_steps) - else: - self.writter.add_scalars(k, {k: v}, total_num_steps) + self.writter.add_scalars(k, {k: v}, total_num_steps) def log_env(self, env_infos, total_num_steps): """ @@ -276,10 +267,7 @@ class Runner: """ for k, v in env_infos.items(): if len(v)>0: - if self.use_wandb: - wandb.log({k: np.mean(v)}, step=total_num_steps) - else: - self.writter.add_scalars(k, {k: np.mean(v)}, total_num_steps) + self.writter.add_scalars(k, {k: np.mean(v)}, total_num_steps) @torch.no_grad() def eval(self, total_num_steps: int): diff --git a/StreamLearn/Simulator/task/dataset.py b/StreamLearn/Simulator/task/dataset.py index 0821f3e..3d7f43f 100644 --- a/StreamLearn/Simulator/task/dataset.py +++ b/StreamLearn/Simulator/task/dataset.py @@ -1,15 +1,5 @@ -import numpy as np -import torch from torch.utils.data import Dataset, DataLoader from torchvision import datasets, transforms -# from torchaudio.datasets import YESNO -# import torchaudio.transforms as T - -# import torchtext; torchtext.disable_torchtext_deprecation_warning() -# from torchtext.datasets import IMDB -# from torchtext.data.utils import get_tokenizer -# from torchtext.vocab import build_vocab_from_iterator -# from torchtext.data.functional import to_map_style_dataset def get_dataloader( @@ -28,30 +18,11 @@ def get_dataloader( dataset_class = Cifar10Dataset elif dataset_name == 'cifar100': dataset_class = Cifar100Dataset - # elif dataset_name == 'rl': - # dataset_class = RLDataset - # elif dataset_name == 'imdb': - # dataset_class = IMDBDataset - # elif dataset_name == 'yesno': - # dataset_class = YesNoDataset else: raise ValueError(f"Unknown dataset: {dataset_name}") - if dataset_name == 'rl': - assert 'data_batch' in kwargs and 'label_batch' in kwargs, "data_batch and label_batch must be provided for RLDataset" - dataset = RLDataset( - train=train, - transform=transform, - **kwargs - ) - else: - dataset = dataset_class(train=train, root=root, transform=transform, **kwargs) - if dataset_name == 'yesno': - collate_fn = dataset.pad_seq - elif dataset_name == 'imdb': - collate_fn = dataset.collate_batch - else: - collate_fn = None + dataset = dataset_class(train=train, root=root, transform=transform, **kwargs) + collate_fn = None dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn, pin_memory=pin_memory, num_workers=num_workers) return dataloader @@ -106,104 +77,3 @@ class Cifar100Dataset(TransformedDataset): else: self.transform = transform self.data = datasets.CIFAR100(root=root, train=train, transform=self.transform, download=True) - - -# class RLDataset(TransformedDataset): - -# def __init__(self, data_batch, label_batch, train=True, transform=None, **kwargs): -# self.train = train -# if transform is None: -# self.transform = transforms.Compose([ -# transforms.Resize([196, 196]), -# transforms.ToTensor(), -# transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) -# ]) -# else: -# self.transform = transform - -# assert len(data_batch) == len(label_batch) -# num_data = len(data_batch) -# self.images = [] -# self.labels = [] -# index = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 11: 6, 12: 7} -# for i in range(num_data): -# self.images.append(data_batch[i]) -# tmp = np.zeros(8) -# tmp[index[label_batch[i]]] = 1 -# self.labels.append(tmp) - -# def __getitem__(self, item): -# img = self.images[item] -# label = self.labels[item] -# if self.transform is not None: -# img = self.transform(img) -# label = torch.from_numpy(label).type(torch.LongTensor) -# label = torch.argmax(label) # Convert one-hot to class index -# return img, label - -# def __len__(self): -# return len(self.images) - - -# class YesNoDataset(TransformedDataset): - -# def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): -# if transform is None: -# self.transform = torch.nn.Sequential( -# T.MelSpectrogram(sample_rate=8000, n_mels=64), -# T.AmplitudeToDB() -# ) -# else: -# self.transform = transform -# self.data = YESNO(root=root, download=True) - -# def pad_seq(self, batch): -# max_len = max([x[0].shape[-1] for x in batch]) -# batch_size = len(batch) -# num_mels = batch[0][0].shape[1] -# padded_batch = torch.zeros(batch_size, 1, num_mels, max_len) -# labels = [] -# for i, (x, y) in enumerate(batch): -# padded_batch[i, :, :, :x.shape[-1]] = x -# labels.append(y) -# labels = torch.tensor(labels, dtype=torch.long) -# return padded_batch, labels - -# def __getitem__(self, index): -# waveform, sample_rate, labels = self.data[index] -# if self.transform is not None: -# waveform = self.transform(waveform) -# label = torch.tensor(labels[0], dtype=torch.long) -# return waveform, label - - -# class IMDBDataset(TransformedDataset): - -# def __init__(self, train=True, root: str = './datasets', transform=None, **kwargs): -# self.split = 'train' if train else 'test' -# self.tokenizer = get_tokenizer('basic_english') -# self.data = IMDB(root=root, split=self.split) - -# self.vocab = build_vocab_from_iterator(self.yield_tokens(self.data), specials=["", ""]) -# self.vocab.set_default_index(self.vocab[""]) - -# self.data = to_map_style_dataset(self.data) - -# def yield_tokens(self, data_iter): -# for _, text in data_iter: -# yield self.tokenizer(text) - -# def _text_pipeline(self, x): -# return self.vocab(self.tokenizer(x)) - -# def _label_pipeline(self, y): -# return 0 if y == 'neg' else 1 - -# def collate_batch(self, batch): -# labels, texts = [], [] -# for label, text in batch: -# labels.append(self._label_pipeline(label)) -# texts.append(torch.tensor(self._text_pipeline(text), dtype=torch.int64)) -# labels = torch.tensor(labels, dtype=torch.int64) -# texts = torch.nn.utils.rnn.pad_sequence(texts, batch_first=True) -# return texts, labels diff --git a/StreamLearn/Simulator/task/models.py b/StreamLearn/Simulator/task/models.py index b316a56..62f28c6 100644 --- a/StreamLearn/Simulator/task/models.py +++ b/StreamLearn/Simulator/task/models.py @@ -160,33 +160,6 @@ class TransformerModel(nn.Module): return x -class DAggerCNNNet(nn.Module): - - def __init__(self): - super().__init__() - self.conv1 = nn.Conv2d(3, 6, (5, 5)) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, (5, 5)) - self.conv3 = nn.Conv2d(16, 32, (5, 5)) - self.conv4 = nn.Conv2d(32, 64, (4, 4)) - self.conv5 = nn.Conv2d(64, 128, (4, 4)) - self.fc1 = nn.Linear(128 * 3 * 3, 240) - self.fc2 = nn.Linear(240, 60) - self.fc3 = nn.Linear(60, 8) - - def forward(self, x): - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = self.pool(F.relu(self.conv3(x))) - x = self.pool(F.relu(self.conv4(x))) - x = self.pool(F.relu(self.conv5(x))) - x = torch.flatten(x, 1) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - x = self.fc3(x) - return x - - class SelfAttention(nn.Module): def __init__(self, embed_size, heads): @@ -221,30 +194,3 @@ class SelfAttention(nn.Module): ) out = self.fc_out(out) return out - - -class ALSTM(nn.Module): - - def __init__(self, vocab_size, embed_size, num_heads, hidden_dim, num_classes): - super().__init__() - self.embedding = nn.Embedding(vocab_size, embed_size) - self.attention = SelfAttention(embed_size, num_heads) - self.lstm = nn.LSTM(embed_size, hidden_dim, batch_first=True) - - self.fc1 = nn.Linear(hidden_dim, 256) - self.fc2 = nn.Linear(256, 128) - self.fc3 = nn.Linear(128, 64) - self.fc4 = nn.Linear(64, num_classes) - self.dropout = nn.Dropout(0.5) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - emb = self.embedding(x) - attn = self.attention(emb, emb, emb) - _, (hn, _) = self.lstm(attn) - out = hn[-1] - out = F.relu(self.fc1(out)) - out = self.dropout(out) - out = F.relu(self.fc2(out)) - out = F.relu(self.fc3(out)) - out = self.fc4(out) - return out diff --git a/StreamLearn/Simulator/task/rl_agent.py b/StreamLearn/Simulator/task/rl_agent.py deleted file mode 100644 index bb4af1c..0000000 --- a/StreamLearn/Simulator/task/rl_agent.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Generate a simple DAgger agent for the RL task. -The default task is MontezumaRevengeNoFrameskip-v0 in Atari with image input. -""" - -import argparse -import gym -from PIL import Image -import time -import numpy as np - -import torch -import torch.nn as nn -import torch.nn.functional as F -import torchvision.transforms as transforms - -from StreamLearn.Simulator.task.dataset import get_dataloader -from StreamLearn.Simulator.task.models import DAggerCNNNet - - -def get_rl_args(): - parser = argparse.ArgumentParser(description='RL') - parser.add_argument( - '--env-name', - type=str, - default='MontezumaRevengeNoFrameskip-v0') - parser.add_argument( - '--num-stacks', - type=int, - default=8) - parser.add_argument( - '--num-steps', - type=int, - default=800) - parser.add_argument( - '--test-steps', - type=int, - default=2000) - parser.add_argument( - '--num-frames', - type=int, - default=800) - - ## other parameter - parser.add_argument( - '--log-interval', - type=int, - default=1, - help='log interval, one log per n updates (default: 10)') - parser.add_argument( - '--save-img', - type=bool, - default=False) - parser.add_argument( - '--save-interval', - type=int, - default=10, - help='save interval, one eval per n updates (default: None)') - return parser.parse_known_args()[0] - - -class StackedEnv(object): - def __init__(self, env_name, num_stacks): - self.env = gym.make(env_name) - # num_stacks: the agent acts every num_stacks frames - # it could be any positive integer - self.num_stacks = num_stacks - self.observation_space = self.env.observation_space - self.action_space = self.env.action_space - - def step(self, action): - reward_sum = 0 - for stack in range(self.num_stacks): - obs_next, reward, done, info = self.env.step(action) - reward_sum += reward - if done: - self.env.reset() - return obs_next, reward_sum, done, info - return obs_next, reward_sum, done, info - - def reset(self): - return self.env.reset() - - -class DAggerAgent: - def __init__(self, lr=1e-3): - # init your model - self.model = DAggerCNNNet() - self.optimizer = torch.optim.Adam(self.model.parameters(), lr=lr) - - # select actions by your model - def select_action(self, data_batch): - label_predict = self.model.predict(data_batch) - return label_predict - - -def get_rl_agent(): - args = get_rl_args() - num_updates = int(args.num_frames // args.num_steps) - # query_cnt counts queries to the expert - query_cnt = 0 - - # environment initial - envs = StackedEnv(args.env_name, args.num_stacks) - action_shape = envs.action_space.n - # observation_shape is the shape of the observation - # here is (210,160,3)=(height, weight, channels) - observation_shape = envs.observation_space.shape - print(action_shape, observation_shape) - - # agent initial - agent = DAggerAgent() - data_set = {'data': [], 'label': []} - - # an example of interacting with the environment - # we init the environment and receive the initial observation - obs = envs.reset() - # we get a trajectory with the length of args.num_steps - for step in range(args.num_steps): - # Sample expert actions - with open("./datasets/imgs/label.txt", "r") as f: - expert_action= int(f.readlines()[num_updates - 1].strip()) - action = expert_action - query_cnt += 1 - data_set['data'].append(obs) - data_set['label'].append(expert_action) - - # an example of saving observations - # if args.save_img: - # im = Image.fromarray(obs) - # im.save('imgs/' + 'screen' + str(i * args.num_steps + step) + '.jpeg') - - obs_next, reward, done, _ = envs.step(action) - # we view the new observation as current observation - obs = obs_next - # if the episode has terminated, we need to reset the environment. - if done: - envs.reset() - - img_data_batch = [] - for item in data_set['data']: - img_data_batch.append(Image.fromarray(item)) - data_set['data'] = img_data_batch - - return agent, data_set - - -if __name__ == '__main__': - """ - Test the training of the RL agent. - Not run when importing this file. - """ - from torch.autograd import Variable - args = get_rl_args() - envs = StackedEnv(args.env_name, args.num_stacks) - agent, dataset = get_rl_agent() - train_loader_rl = get_dataloader( - dataset_name="rl", - batch_size=64, - data_batch=dataset['data'], - label_batch=dataset['label'] - ) - criterion = nn.CrossEntropyLoss() - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - model = DAggerCNNNet().to(device) - model.train() - optimizer = torch.optim.Adam(model.parameters(), lr=0.001) - num_updates = int(args.num_frames // args.num_steps) - # epoch = 30 - running_loss = 0 - - for epoch_idx in range(num_updates): - correct = 0 - total = 0 - train_data_list = list(train_loader_rl) - for batch_idx, (data, target) in enumerate(train_loader_rl): - data, target = Variable(data).to(device), Variable(target).to(device) - - optimizer.zero_grad() - output = model(data) - _, predicted = torch.max(output.data, 1) - total += target.size(0) - correct += (predicted == torch.max(target.data, 1)[1]).sum().item() - loss = criterion(output, torch.max(target, 1)[1]) - running_loss += loss.item() - loss.backward() - optimizer.step() - print(f"loss: {running_loss / (epoch_idx * len(train_data_list) + batch_idx + 1)}") - print(f'epoch: {epoch_idx + 1}/{num_updates}, crr/tot: {correct}/{total}, acc: {correct / total:.3f}') diff --git a/StreamLearn/Simulator/task/task_configs.py b/StreamLearn/Simulator/task/task_configs.py index 19efd0c..4fea662 100644 --- a/StreamLearn/Simulator/task/task_configs.py +++ b/StreamLearn/Simulator/task/task_configs.py @@ -6,7 +6,6 @@ import torch.nn as nn from StreamLearn.Simulator.task import rewards, models from StreamLearn.Simulator.task.dataset import get_dataloader -from StreamLearn.Simulator.task.rl_agent import get_rl_agent @dataclass @@ -108,124 +107,6 @@ class ViT_Cifar10(TaskConfig): dataset_name='cifar10', ) -@dataclass -class ViT_Cifar10_1(TaskConfig): - model: nn.Module = SimpleViT - reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.1) - available_time: int = 530 - epsilon: float = 1.1 - - def __post_init__(self): - self.model_config = dict( - image_size=32, - patch_size=4, - num_classes=10, - dim=16, - depth=2, - heads=2, - mlp_dim=8, - dim_head=4 - ) - self.dataloader_config = dict( - dataset_name='cifar10', - ) - -@dataclass -class ViT_Cifar10_2(ViT_Cifar10_1): - reward_func: rewards.RewardFunction = rewards.ThresholdReward(1.35) - available_time: int = 540 - epsilon: float = 1.35 - -@dataclass -class RL_1(TaskConfig): - model: nn.Module = models.DAggerCNNNet - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.0011) - available_time: int = 545 - epsilon: float = 0.0011 - - def __post_init__(self): - agent, dataset = get_rl_agent() - self.dataloader_config = dict( - dataset_name='rl', - data_batch=dataset['data'], - label_batch=dataset['label'] - ) - -@dataclass -class RL_2(RL_1): - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.0012) - available_time: int = 710 - epsilon: float = 0.0012 - -@dataclass -class TSFM_Audio_1(TaskConfig): - model: nn.Module = models.TransformerModel - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.06) - available_time: int = 585 - epsilon: float = 0.06 - - def __post_init__(self): - self.model_config = dict( - input_dim=64, - num_heads=64, - num_classes=2 - ) - self.dataloader_config = dict( - dataset_name='yesno', - batch_size=1, - ) - -@dataclass -class TSFM_Audio_2(TSFM_Audio_1): - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.08) - available_time: int = 700 - epsilon: float = 0.08 - -@dataclass -class ALSTM_Text_1(TaskConfig): - vb_size: int = 100683 - model: nn.Module = models.ALSTM - reward_func: rewards.RewardFunction = rewards.ThresholdReward(7e-4) - available_time: int = 625 - epsilon: float = 7e-4 - - def __post_init__(self): - self.model_config = dict( - vocab_size=self.vb_size, - embed_size=16, - num_heads=4, - hidden_dim=8, - num_classes=2, - ) - self.dataloader_config = dict( - dataset_name='imdb', - batch_size=8, - ) - -@dataclass -class ALSTM_Text_2(ALSTM_Text_1): - reward_func: rewards.RewardFunction = rewards.ThresholdReward(9e-4) - available_time: int = 690 - epsilon: float = 9e-4 - -@dataclass -class Res18_Cifar10_1(TaskConfig): - model: nn.Module = models.ResNet18_Cifar10 - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.55) - available_time: int = 655 - epsilon: float = 0.55 - - def __post_init__(self): - self.dataloader_config = dict( - dataset_name='cifar10', - ) - -@dataclass -class Res18_Cifar10_2(Res18_Cifar10_1): - reward_func: rewards.RewardFunction = rewards.ThresholdReward(0.55) - available_time: int = 685 - epsilon: float = 0.55 - task_list_cifar10 = [ ViT_Cifar10, @@ -234,17 +115,3 @@ task_list_cifar10 = [ ResNet34_Cifar10, LSTM_Cifar10, ] - - -task_list_mixed = [ - ViT_Cifar10_1, - ViT_Cifar10_2, - RL_1, - TSFM_Audio_1, - ALSTM_Text_1, - Res18_Cifar10_1, - Res18_Cifar10_2, - ALSTM_Text_2, - TSFM_Audio_2, - RL_2, -] \ No newline at end of file -- Gitee From 6a45df217ef3d6f0e70fb5ebdf540cca992064d3 Mon Sep 17 00:00:00 2001 From: FineArtz Date: Fri, 8 Aug 2025 05:30:28 +0000 Subject: [PATCH 8/9] update abdgen --- .gitignore | 3 ++- .../create_dataset/create_mario_negative.py | 11 ++++++---- .../create_dataset/create_mario_positive.py | 11 ++++++---- StreamLearn/Config/AbdGen.py | 2 +- StreamLearn/Simulator/task/dataset.py | 3 ++- StreamLearn/legacy/README.md | 6 +++++ StreamLearn/tests/test_AbdGen.py | 22 ++++++++++++------- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index ba96d40..4ae24ee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build /datasets /logs logs -.DS_Store \ No newline at end of file +.DS_Store +StreamLearn/Dataset/datasets \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py index 1d216a2..51e80cb 100644 --- a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_negative.py @@ -196,7 +196,12 @@ def define_program(traj): return program -def create_mario_dataset(folder): +def create_mario_dataset(folder, script_dir=False): + if script_dir: + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + global ICONS + for icon in ICONS: + ICONS[icon] = os.path.join(script_dir, ICONS[icon]) # List of 9 pairs of agent positions in a 3x3 grid position_set = set() @@ -318,6 +323,4 @@ def create_mario_dataset(folder): if __name__ == '__main__': script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - for icon in ICONS: - ICONS[icon] = os.path.join(script_dir, ICONS[icon]) - create_mario_dataset('dataset/mario') \ No newline at end of file + create_mario_dataset('StreamLearn/Dataset/datasets/mario', script_dir) \ No newline at end of file diff --git a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py index d31639f..b2f710f 100644 --- a/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py +++ b/StreamLearn/Algorithm/AbdGen/create_dataset/create_mario_positive.py @@ -194,7 +194,12 @@ def define_program(traj): return program -def create_mario_dataset(folder): +def create_mario_dataset(folder, script_dir=False): + if script_dir: + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + global ICONS + for icon in ICONS: + ICONS[icon] = os.path.join(script_dir, ICONS[icon]) # List of 9 pairs of agent positions in a 3x3 grid position_set = set() @@ -330,6 +335,4 @@ def create_mario_dataset(folder): if __name__ == '__main__': script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - for icon in ICONS: - ICONS[icon] = os.path.join(script_dir, ICONS[icon]) - create_mario_dataset('dataset/mario') \ No newline at end of file + create_mario_dataset('StreamLearn/Dataset/datasets/mario', script_dir) \ No newline at end of file diff --git a/StreamLearn/Config/AbdGen.py b/StreamLearn/Config/AbdGen.py index 2dc640a..2dd7f24 100644 --- a/StreamLearn/Config/AbdGen.py +++ b/StreamLearn/Config/AbdGen.py @@ -30,7 +30,7 @@ parser.add_argument('--bk_file', type=str, parser.add_argument('--exp_root_path', type=str, help='exp root path', default='./') parser.add_argument('--dataset_path', type=str, - help='dataset path', default='dataset/mario/') + help='dataset path', default='StreamLearn/Dataset/datasets/mario/') parser.add_argument('--model_path', type=str, help='model path', default='logs/AbdGen/model/') parser.add_argument('--tmp_path', type=str, diff --git a/StreamLearn/Simulator/task/dataset.py b/StreamLearn/Simulator/task/dataset.py index f11410e..9d8c157 100644 --- a/StreamLearn/Simulator/task/dataset.py +++ b/StreamLearn/Simulator/task/dataset.py @@ -1,9 +1,10 @@ from torch.utils.data import Dataset, DataLoader from torchvision import datasets, transforms + def get_dataloader( dataset_name: str, - root: str = './datasets', + root: str = 'StreamLearn/Dataset/datasets', batch_size: int = 64, train: bool = True, transform = None, diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index 2e7a50c..01473d9 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -1169,6 +1169,8 @@ python -m StreamLearn.tests.test_AbdGen - StreamLearn/Simulator/policy/AOGS.py - StreamLearn/Simulator/test/test_AOGS.py +本测试使用CIFAR-10数据集进行演示,代码会自动下载数据集。如果无法下载,可以从([Link][https://pan.baidu.com/s/1Z7IzLN4K17SrPzA8MW5RTA?pwd=rvb6])下载CIFAR-10数据集,并解压到`stream-learn\StreamLearn\Dataset\datasets`目录下。 + 首先,按照以下方式构造测试任务序列 ```python task_list_name = config['task_list'] @@ -1221,6 +1223,8 @@ AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应Multi - StreamLearn/Simulator/policy/AEAGS.py - StreamLearn/Simulator/test/test_AEAGS.py +本测试与4.3相同,使用CIFAR-10数据集进行演示,下载地址和处理方法参见4.3节。 + 首先,按照以下方式构造测试任务序列 ```python task_list_name = config['task_list'] @@ -1278,6 +1282,8 @@ while not terminal: - StreamLearn/Simulator/sim_utils/replay_buffer.py - StreamLearn/Simulator/test/test_MARA.py +本测试与4.3相同,使用CIFAR-10数据集进行演示,下载地址和处理方法参见4.3节。 + 首先,按照以下方式构造测试任务序列 ```python task_list_name = config['task_list'] diff --git a/StreamLearn/tests/test_AbdGen.py b/StreamLearn/tests/test_AbdGen.py index 39ac7d6..7dc019f 100644 --- a/StreamLearn/tests/test_AbdGen.py +++ b/StreamLearn/tests/test_AbdGen.py @@ -1,16 +1,21 @@ -""" -This file is a copyrighted under the BSD 3-clause licence, details of which can be found in the root directory. -Code for -Generating by Understanding: Neural Visual Generation with Logical Symbol Groundings -https://arxiv.org/abs/2310.17451 - -""" - import os from StreamLearn.Config.AbdGen import args, mario_config import StreamLearn.Algorithm.AbdGen.functionalities.rule_learning as rule_learning from StreamLearn.Algorithm.AbdGen.utils.utils import set_seed, PathManager +from StreamLearn.Algorithm.AbdGen.create_dataset.create_mario_positive import create_mario_dataset as create_pos +from StreamLearn.Algorithm.AbdGen.create_dataset.create_mario_negative import create_mario_dataset as create_neg + + +def create_dataset(): + dataset_file = os.path.join(args.dataset_path, 'mario_pos.npz') + if not os.path.exists(dataset_file): + print("Create positive dataset...") + create_pos(args.dataset_path, True) + dataset_file = os.path.join(args.dataset_path, 'mario_neg.npz') + if not os.path.exists(dataset_file): + print("Create negative dataset...") + create_neg(args.dataset_path, True) def main(): @@ -48,5 +53,6 @@ def main(): if __name__ == "__main__": + create_dataset() main() \ No newline at end of file -- Gitee From 148e10361dba6fbc952031e5a80427a67ca09aab Mon Sep 17 00:00:00 2001 From: FineArtz Date: Fri, 8 Aug 2025 05:51:45 +0000 Subject: [PATCH 9/9] update gear --- .../GEAR/tests/single_node/create.py | 52 ++--- StreamLearn/Config/GEAR.py | 195 ++++++++++-------- StreamLearn/legacy/README.md | 167 +-------------- StreamLearn/tests/test_GEAR.py | 25 ++- 4 files changed, 151 insertions(+), 288 deletions(-) diff --git a/StreamLearn/Algorithm/GEAR/tests/single_node/create.py b/StreamLearn/Algorithm/GEAR/tests/single_node/create.py index 39b2133..376afe9 100644 --- a/StreamLearn/Algorithm/GEAR/tests/single_node/create.py +++ b/StreamLearn/Algorithm/GEAR/tests/single_node/create.py @@ -8,32 +8,6 @@ import numpy as np from gear.dataset import SharedDataset from gear.specs import TableSpec -parser = argparse.ArgumentParser() -parser.add_argument( - "--hdf5_data_url", - type=str, - default="http://rail.eecs.berkeley.edu/datasets/offline_rl/gym_mujoco/hopper_expert.hdf5", - help="D4RL's hopper-expert-v0 offline dataset.", -) -parser.add_argument( - "--hdf5_data_path", - type=str, - default="/tmp/gear/datasets/example.hdf5", - help="Path of the hdf5 dataset(folloing D4RL).", -) -parser.add_argument( - "--shared_memory_seed", - type=int, - default=None, - help="Seed to generate shared memory key SharedDataset, hash of local host name will be used if 'shared_memory_seed' is not set.", -) -parser.add_argument( - "--data_path", - type=str, - default="/tmp/gear/checkpoints/example_shared_dataset.pt", - help="Seed to generate shared memory key SharedDataset, hash of local host name will be used if 'shared_memory_seed' is not set.", -) - def convert_d4rl_dataset(args): """ @@ -93,6 +67,32 @@ def convert_d4rl_dataset(args): if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--hdf5_data_url", + type=str, + default="http://rail.eecs.berkeley.edu/datasets/offline_rl/gym_mujoco/hopper_expert.hdf5", + help="D4RL's hopper-expert-v0 offline dataset.", + ) + parser.add_argument( + "--hdf5_data_path", + type=str, + default="/tmp/gear/datasets/example.hdf5", + help="Path of the hdf5 dataset(folloing D4RL).", + ) + parser.add_argument( + "--shared_memory_seed", + type=int, + default=None, + help="Seed to generate shared memory key SharedDataset, hash of local host name will be used if 'shared_memory_seed' is not set.", + ) + parser.add_argument( + "--data_path", + type=str, + default="/tmp/gear/checkpoints/example_shared_dataset.pt", + help="Seed to generate shared memory key SharedDataset, hash of local host name will be used if 'shared_memory_seed' is not set.", + ) + args = parser.parse_args() dataset = convert_d4rl_dataset(args) diff --git a/StreamLearn/Config/GEAR.py b/StreamLearn/Config/GEAR.py index c65d41e..a574ea8 100644 --- a/StreamLearn/Config/GEAR.py +++ b/StreamLearn/Config/GEAR.py @@ -1,91 +1,110 @@ import argparse from datetime import datetime -def get_gear_config(): - parser = argparse.ArgumentParser() - - # DeepSpeed 相关参数 - parser.add_argument( - "--shared_memory_seed", - type=int, - default=None, - help="Seed to generate shared memory key SharedDataset" - ) - parser.add_argument( - "--data_path", - type=str, - default="/tmp/gear/checkpoints/example_shared_dataset.pt", - help="Path to shared dataset" - ) - parser.add_argument( - "--local_rank", - type=int, - default=-1, - help="local rank passed from distributed launcher" - ) - parser.add_argument( - "--enable_tensorboard", - action="store_true", - default=True, - help="Logging training losses to the tensorboard files" - ) - parser.add_argument( - "--tensorboard_logdir", - type=str, - default="./logs", - help="Tensorboard logging path" - ) - parser.add_argument( - "--model", - choices=["mlp", "mat"], - default="mlp", - help="Model architecture" - ) - parser.add_argument( - "--expr_name", - type=str, - default=datetime.now().strftime("%d-%m-%Y-%H:%M:%S"), - help="Experiment name, used as logging file name" - ) - parser.add_argument( - "--deepspeed_config", - type=str, - default="StreamLearn/Algorithm/GEAR/tests/single_node/deepspeed_config.json", - help="Path to DeepSpeed configuration file" - ) - - # 训练相关参数 - parser.add_argument( - "--num_iter", - type=int, - default=500, - help="Number of training iterations" - ) - parser.add_argument( - "--eval_interval", - type=int, - default=100, - help="Evaluation interval in iterations" - ) - parser.add_argument( - "--num_eval_trajectories", - type=int, - default=100, - help="Number of trajectories for evaluation" - ) - - # 新增示例参数 - parser.add_argument("--task_list", type=str, default="task_list_cifar10") - parser.add_argument("--seed", type=int, default=0) - parser.add_argument("--debug", action="store_true", default=False) - parser.add_argument("--num_nodes", type=int, default=3) - parser.add_argument("--node_comp_ability", type=float, default=1.0) - parser.add_argument("--predictor_gamma", type=float, default=0.9) - parser.add_argument("--final_time", type=int, default=5) - parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") - parser.add_argument("--generate_gap", type=int, default=1) - parser.add_argument("--pre_generate", action="store_true", default=True) - parser.add_argument("--num_episodes", type=int, default=2) - parser.add_argument("--exploration_round", type=int, default=10) - - return parser \ No newline at end of file + +parser = argparse.ArgumentParser() + +parser.add_argument( + "--hdf5_data_url", + type=str, + default="http://rail.eecs.berkeley.edu/datasets/offline_rl/gym_mujoco/hopper_expert.hdf5", + help="D4RL's hopper-expert-v0 offline dataset.", +) +parser.add_argument( + "--hdf5_data_path", + type=str, + default="StreamLearn/Dataset/datasets/gear/example.hdf5", + help="Path of the hdf5 dataset(folloing D4RL).", +) + +# DeepSpeed 相关参数 +parser.add_argument( + "--shared_memory_seed", + type=int, + default=None, + help="Seed to generate shared memory key SharedDataset" +) +parser.add_argument( + "--data_path", + type=str, + default="StreamLearn/Dataset/datasets/gear/checkpoints/example_shared_dataset.pt", + help="Path to shared dataset" +) +parser.add_argument( + "--iset_path", + type=str, + default="StreamLearn/Dataset/datasets/gear/checkpoints/iset.pt", + help="Path to shared dataset" +) +parser.add_argument( + "--local_rank", + type=int, + default=-1, + help="local rank passed from distributed launcher" +) +parser.add_argument( + "--enable_tensorboard", + action="store_true", + default=True, + help="Logging training losses to the tensorboard files" +) +parser.add_argument( + "--tensorboard_logdir", + type=str, + default="./logs", + help="Tensorboard logging path" +) +parser.add_argument( + "--model", + choices=["mlp", "mat"], + default="mlp", + help="Model architecture" +) +parser.add_argument( + "--expr_name", + type=str, + default=datetime.now().strftime("%d-%m-%Y-%H:%M:%S"), + help="Experiment name, used as logging file name" +) +parser.add_argument( + "--deepspeed_config", + type=str, + default="StreamLearn/Algorithm/GEAR/tests/single_node/deepspeed_config.json", + help="Path to DeepSpeed configuration file" +) + +# 训练相关参数 +parser.add_argument( + "--num_iter", + type=int, + default=500, + help="Number of training iterations" +) +parser.add_argument( + "--eval_interval", + type=int, + default=100, + help="Evaluation interval in iterations" +) +parser.add_argument( + "--num_eval_trajectories", + type=int, + default=100, + help="Number of trajectories for evaluation" +) + +# 新增示例参数 +parser.add_argument("--task_list", type=str, default="task_list_cifar10") +parser.add_argument("--seed", type=int, default=0) +parser.add_argument("--debug", action="store_true", default=False) +parser.add_argument("--num_nodes", type=int, default=3) +parser.add_argument("--node_comp_ability", type=float, default=1.0) +parser.add_argument("--predictor_gamma", type=float, default=0.9) +parser.add_argument("--final_time", type=int, default=5) +parser.add_argument("--generator_name", type=str, default="UniformStreamGenerator") +parser.add_argument("--generate_gap", type=int, default=1) +parser.add_argument("--pre_generate", action="store_true", default=True) +parser.add_argument("--num_episodes", type=int, default=2) +parser.add_argument("--exploration_round", type=int, default=10) + +args, unknown = parser.parse_known_args() diff --git a/StreamLearn/legacy/README.md b/StreamLearn/legacy/README.md index 01473d9..b8deea3 100644 --- a/StreamLearn/legacy/README.md +++ b/StreamLearn/legacy/README.md @@ -1001,10 +1001,7 @@ pip install -r requirements. pip install . ``` -其次,参考运行`StreamLearn/Algorithm/GEAR/tests/single_node/create.py`以下载并转换`hopper`数据集,该最小数据集会被存放于`/tmp/gear/checkpoints/example_shared_dataset.pt`的默认路径下(可通过`--data_path`参数指定存放路径)。 -```shell -cd StreamLearn/Algorithm/GEAR/tests/single_node/create.py --data_path /tmp/gear/checkpoints/example_shared_dataset.pt -``` +样例运行需要的数据集是D4RL的`hopper-expert`数据集,代码运行时会将数据集自动下载到`StreamLearn/Dataset/datasets/gear/example.hdf5`。可以通过`--data_path`参数指定存放路径。也可以从([Link][https://pan.baidu.com/s/15KQGT958t4gg9P9TQiDvrA?pwd=4a7h])手动下载数据集,放在上述目录下。 之后,运行`python -m StreamLearn.tests.test_GEAR`即可快速运行样例程序。 @@ -1329,168 +1326,6 @@ runner.simulate() ``` -### 4.4 在线匹配市场中的多臂赌博机最优算法AOGS - -自适应在线Gale-Shapley算法(AOGS)是一种双边匹配市场的多臂赌博机算法,它将传统的Gale-Shapley算法融入在线多臂赌博机算法中,依据每一轮的反馈动态调整GS算法的各个步骤。AOGS提升了已有的理论懊悔上界保证,这一结果首次在主阶项中消除了对手臂数量的依赖,在玩家数量远小于手臂数量的常见情况下显著提高了理论保证。将多节点的流数据任务调度问题建模为匹配问题后,AOGS算法同样可以用来完成任务调度。 - -该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 -- StreamLearn/Simulator/policy/AOGS.py -- StreamLearn/Simulator/test/test_AOGS.py - -首先,按照以下方式构造测试任务序列 -```python -task_list_name = config['task_list'] -task_list = getattr(task_configs, task_list_name) -available_task_list = [] -task_ids = None -if task_ids is None: - task_ids = list(range(len(task_list))) -for task_id in task_ids: - available_task_list.append(task_list[task_id]) -``` - -接下来创建任务生成器,配置调度环境 -```python -generator_name = config['generator_name'] -generator_class = getattr(StreamGenerator, generator_name) -generator: StreamGenerator.StreamGenerator = generator_class( - available_task_list=available_task_list, - **config, -) -env = Env( - nodes=nodes, - stream_generator=generator, - predictor=predictor, - rl_mode=True, - **config, -) -``` -根据参数初始化AOGS调度策略 -```python -policy = AOGSPolicy(exploration_round=config["exploration_round"]) -``` -最后即可在流数据环境下执行AOGS调度算法 -```python -obs, terminal = env.reset() -step = 0 -total_reward = 0 -while not terminal: - action = policy.act(obs) - step += 1 - obs, rew, terminal, *_ = env.step(action) - total_reward += rew -``` - -### 4.5 在线匹配市场不敏感偏好场景下的多臂赌博机算法AE-AGS - -AE-AGS算法是一种在线匹配市场不敏感偏好场景下的自适应多臂赌博机算法。该算法设计了由手臂发起匹配的Gale-Shapley引导机制,在偏好不敏感场景下引导玩家进行自适应探索。使用手臂引导的Gale-Shapley算法,玩家仅需匹配可选手臂,避免决策何时切换探索与利用。剔除非最优的可选手臂即可实现不同稳定匹配间的动态切换。AE-AGS在偏好不敏感场景下得到了稳定匹配的累积懊悔保证,相比已有工作实现了从不收敛到多项式累积懊悔上界的理论突破,显著拓宽了已有算法的适用范围。 - -该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分 -- StreamLearn/Simulator/policy/AOGS.py -- StreamLearn/Simulator/test/test_AOGS.py - -首先,按照以下方式构造测试任务序列 -```python -task_list_name = config['task_list'] -task_list = getattr(task_configs, task_list_name) -available_task_list = [] -task_ids = None -if task_ids is None: - task_ids = list(range(len(task_list))) -for task_id in task_ids: - available_task_list.append(task_list[task_id]) -``` - -接下来创建任务生成器,配置调度环境 -```python -generator_name = config['generator_name'] -generator_class = getattr(StreamGenerator, generator_name) -generator: StreamGenerator.StreamGenerator = generator_class( - available_task_list=available_task_list, - **config, -) -env = Env( - nodes=nodes, - stream_generator=generator, - predictor=predictor, - rl_mode=True, - **config, -) -``` -根据参数初始化AE-AGS调度策略 -```python -policy = AEAGSPolicy(exploration_round=config["exploration_round"]) -``` -最后即可在流数据环境下执行AE-AGS调度算法 -```python -obs, terminal = env.reset() -step = 0 -total_reward = 0 -while not terminal: - action = policy.act(obs) - step += 1 - obs, rew, terminal, *_ = env.step(action) - total_reward += rew -``` - -### 4.6 基于多智能体强化学习的自适应调度算法MARA - -在多节点的流数据调度任务中,如果将节点看作智能体、待调度的任务看作匹配目标,那么该调度任务可以建模为多智能体强化学习(MARL)问题。在每一时刻`t`,每个节点选择一个任务执行,由此实现任务调度。由于同一时刻待调度的任务数量不断变化,MARA进一步用序列决策模型来建模该MARL问题,结合多智能体Transformer算法,实现对任务的动态评估和调度。 - -该算法在已有的流数据调度框架下,主要包含`调度策略`、`性能测试`两部分。其中,调度策略的模型和训练工具由多个文件组成。 -- StreamLearn/Simulator/policy/mat_policy.py -- StreamLearn/Simulator/policy/networks.py -- StreamLearn/Simulator/policy/mat_trainer.py -- StreamLearn/Simulator/policy/mat_runner.py -- StreamLearn/Simulator/sim_utils/predictor.py -- StreamLearn/Simulator/sim_utils/replay_buffer.py -- StreamLearn/Simulator/test/test_MARA.py - -首先,按照以下方式构造测试任务序列 -```python -task_list_name = config['task_list'] -task_list = getattr(task_configs, task_list_name) -available_task_list = [] -task_ids = None -if task_ids is None: - task_ids = list(range(len(task_list))) -for task_id in task_ids: - available_task_list.append(task_list[task_id]) -``` - -接下来创建任务生成器,配置调度环境 -```python -generator_name = config['generator_name'] -generator_class = getattr(StreamGenerator, generator_name) -generator: StreamGenerator.StreamGenerator = generator_class( - available_task_list=available_task_list, - **config, -) -env = Env( - nodes=nodes, - stream_generator=generator, - predictor=predictor, - rl_mode=True, - **config, -) -eval_env = Env( - nodes=nodes, - stream_generator=generator, - predictor=predictor, - marl_mode=True, - **config, -) -``` -根据参数初始化MARA的执行模块 -```python -runner = Runner(env, config, eval_env) -``` -最后调用执行模块中的`simulate`方法,即可在流数据环境下执行MARA调度算法 -```python -runner.simulate() -``` - - ## 课题五:流数据增量学习算法 ### 5.1 增量学习算法MEMO diff --git a/StreamLearn/tests/test_GEAR.py b/StreamLearn/tests/test_GEAR.py index e31c56a..0cd3dae 100644 --- a/StreamLearn/tests/test_GEAR.py +++ b/StreamLearn/tests/test_GEAR.py @@ -5,6 +5,7 @@ import importlib import subprocess from datetime import datetime from typing import Union, Callable +import pickle as pkl import gymnasium as gym import deepspeed @@ -12,18 +13,27 @@ import numpy as np import torch import torch.nn as nn import torch.distributed as dist -from torch.distributions import Normal from torch.utils.tensorboard import SummaryWriter import gear from gear.mpu import ModelParallelismUnit -from StreamLearn.Config.GEAR import get_gear_config +from StreamLearn.Config.GEAR import args, unknown from StreamLearn.Algorithm.GEAR.tests.single_node import models +from StreamLearn.Algorithm.GEAR.tests.single_node.create import convert_d4rl_dataset -def run_deepspeed(): - parser = get_gear_config() - args, unknown = parser.parse_known_args() + +def create_dataset(): + if os.path.exists(args.iset_path): + return + dataset = convert_d4rl_dataset(args) + dataset.checkpoint(args.data_path) + + with open(args.iset_path, "wb") as f: + pkl.dump(dataset._iset.get_state(), f) + + +def run_deepspeed(): cmd = [ "deepspeed", "--master_port", "42001", @@ -152,15 +162,14 @@ def run_training( tensorboard_writer=tensorboard_writer, ) + if __name__ == "__main__": if "RANK" not in os.environ: + create_dataset() print("Launching via DeepSpeed...") run_deepspeed() sys.exit(0) - parser = get_gear_config() - args = parser.parse_args() - if dist.is_initialized(): print(f"[Rank {dist.get_rank()}] Starting training with config:") else: -- Gitee