mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-10 06:20:57 +08:00
DDG-DA paper code (#743)
* Merge data selection to main * Update trainer for reweighter * Typos fixed. * update data selection interface * successfully run exp after refactor some interface * data selection share handler & trainer * fix meta model time series bug * fix online workflow set_uri bug * fix set_uri bug * updawte ds docs and delay trainer bug * docs * resume reweighter * add reweighting result * fix qlib model import * make recorder more friendly * fix experiment workflow bug * commit for merging master incase of conflictions * Successful run DDG-DA with a single command * remove unused code * asdd more docs * Update README.md * Update & fix some bugs. * Update configuration & remove debug functions * Update README.md * Modfify horizon from code rather than yaml * Update performance in README.md * fix part comments * Remove unfinished TCTS. * Fix some details. * Update meta docs * Update README.md of the benchmarks_dynamic * Update README.md files * Add README.md to the rolling_benchmark baseline. * Refine the docs and link * Rename README.md in benchmarks_dynamic. * Remove comments. * auto download data Co-authored-by: wendili-cs <wendili.academic@qq.com> Co-authored-by: demon143 <785696300@qq.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import abc
|
||||
from typing import Text, Union
|
||||
from ..utils.serial import Serializable
|
||||
from ..data.dataset import Dataset
|
||||
from ..data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class BaseModel(Serializable, metaclass=abc.ABCMeta):
|
||||
@@ -22,7 +23,7 @@ class BaseModel(Serializable, metaclass=abc.ABCMeta):
|
||||
class Model(BaseModel):
|
||||
"""Learnable Models"""
|
||||
|
||||
def fit(self, dataset: Dataset):
|
||||
def fit(self, dataset: Dataset, reweighter: Reweighter):
|
||||
"""
|
||||
Learn model from the base model
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ class RollingGroup(Group):
|
||||
for key, values in rolling_dict.items():
|
||||
if isinstance(key, tuple):
|
||||
grouped_dict.setdefault(key[:-1], {})[key[-1]] = values
|
||||
else:
|
||||
raise TypeError(f"Expected `tuple` type, but got a value `{key}`")
|
||||
return grouped_dict
|
||||
|
||||
def __init__(self, ens=RollingEnsemble()):
|
||||
|
||||
5
qlib/model/meta/__init__.py
Normal file
5
qlib/model/meta/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from .task import MetaTask
|
||||
from .dataset import MetaTaskDataset
|
||||
76
qlib/model/meta/dataset.py
Normal file
76
qlib/model/meta/dataset.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import abc
|
||||
from qlib.model.meta.task import MetaTask
|
||||
from typing import Dict, Union, List, Tuple, Text
|
||||
from ...workflow.task.gen import RollingGen, task_generator
|
||||
from ...data.dataset.handler import DataHandler
|
||||
from ...utils.serial import Serializable
|
||||
|
||||
|
||||
class MetaTaskDataset(Serializable, metaclass=abc.ABCMeta):
|
||||
"""
|
||||
A dataset fetching the data in a meta-level.
|
||||
|
||||
A Meta Dataset is responsible for
|
||||
- input tasks(e.g. Qlib tasks) and prepare meta tasks
|
||||
- meta task contains more information than normal tasks (e.g. input data for meta model)
|
||||
|
||||
The learnt pattern could transfer to other meta dataset. The following cases should be supported
|
||||
- A meta-model trained on meta-dataset A and then applied to meta-dataset B
|
||||
- Some pattern are shared between meta-dataset A and B, so meta-input on meta-dataset A are used when meta model are applied on meta-dataset-B
|
||||
"""
|
||||
|
||||
def __init__(self, segments: Union[Dict[Text, Tuple], float], *args, **kwargs):
|
||||
"""
|
||||
The meta-dataset maintains a list of meta-tasks when it is initialized.
|
||||
|
||||
The segments indicates the way to divide the data
|
||||
|
||||
The duty of the `__init__` function of MetaTaskDataset
|
||||
- initialize the tasks
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.segments = segments
|
||||
|
||||
def prepare_tasks(self, segments: Union[List[Text], Text], *args, **kwargs) -> List[MetaTask]:
|
||||
"""
|
||||
Prepare the data in each meta-task and ready for training.
|
||||
|
||||
The following code example shows how to retrieve a list of meta-tasks from the `meta_dataset`:
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
# get the train segment and the test segment, both of them are lists
|
||||
train_meta_tasks, test_meta_tasks = meta_dataset.prepare_tasks(["train", "test"])
|
||||
|
||||
Parameters
|
||||
----------
|
||||
segments: Union[List[Text], Tuple[Text], Text]
|
||||
the info to select data
|
||||
|
||||
Returns
|
||||
-------
|
||||
list:
|
||||
A list of the prepared data of each meta-task for training the meta-model. For multiple segments [seg1, seg2, ... , segN], the returned list will be [[tasks in seg1], [tasks in seg2], ... , [tasks in segN]].
|
||||
Each task is a meta task
|
||||
"""
|
||||
if isinstance(segments, (list, tuple)):
|
||||
return [self._prepare_seg(seg) for seg in segments]
|
||||
elif isinstance(segments, str):
|
||||
return self._prepare_seg(segments)
|
||||
else:
|
||||
raise NotImplementedError(f"This type of input is not supported")
|
||||
|
||||
@abc.abstractmethod
|
||||
def _prepare_seg(self, segment: Text):
|
||||
"""
|
||||
prepare a single segment of data for training data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seg : Text
|
||||
the name of the segment
|
||||
"""
|
||||
pass
|
||||
79
qlib/model/meta/model.py
Normal file
79
qlib/model/meta/model.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import abc
|
||||
from qlib.contrib.meta.data_selection.dataset import MetaDatasetDS
|
||||
from typing import Union, List, Tuple
|
||||
|
||||
from qlib.model.meta.task import MetaTask
|
||||
from .dataset import MetaTaskDataset
|
||||
|
||||
|
||||
class MetaModel(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
The meta-model guiding the model learning.
|
||||
|
||||
The word `Guiding` can be categorized into two types based on the stage of model learning
|
||||
- The definition of learning tasks: Please refer to docs of `MetaTaskModel`
|
||||
- Controlling the learning process of models: Please refer to the docs of `MetaGuideModel`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def fit(self, *args, **kwargs):
|
||||
"""
|
||||
The training process of the meta-model.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def inference(self, *args, **kwargs) -> object:
|
||||
"""
|
||||
The inference process of the meta-model.
|
||||
|
||||
Returns
|
||||
-------
|
||||
object:
|
||||
Some information to guide the model learning
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MetaTaskModel(MetaModel):
|
||||
"""
|
||||
This type of meta-model deals with base task definitions. The meta-model creates tasks for training new base forecasting models after it is trained. `prepare_tasks` directly modifies the task definitions.
|
||||
"""
|
||||
|
||||
def fit(self, meta_dataset: MetaTaskDataset):
|
||||
"""
|
||||
The MetaTaskModel is expected to get prepared MetaTask from meta_dataset.
|
||||
And then it will learn knowledge from the meta tasks
|
||||
"""
|
||||
raise NotImplementedError(f"Please implement the `fit` method")
|
||||
|
||||
def inference(self, meta_dataset: MetaTaskDataset) -> List[dict]:
|
||||
"""
|
||||
MetaTaskModel will make inference on the meta_dataset
|
||||
The MetaTaskModel is expected to get prepared MetaTask from meta_dataset.
|
||||
Then it will create modified task with Qlib format which can be executed by Qlib trainer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[dict]:
|
||||
A list of modified task definitions.
|
||||
|
||||
"""
|
||||
raise NotImplementedError(f"Please implement the `inference` method")
|
||||
|
||||
|
||||
class MetaGuideModel(MetaModel):
|
||||
"""
|
||||
This type of meta-model aims to guide the training process of the base model. The meta-model interacts with the base forecasting models during their training process.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def fit(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def inference(self, *args, **kwargs):
|
||||
pass
|
||||
53
qlib/model/meta/task.py
Normal file
53
qlib/model/meta/task.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import abc
|
||||
from typing import Union, List, Tuple
|
||||
|
||||
from qlib.data.dataset import Dataset
|
||||
from ...utils import init_instance_by_config
|
||||
|
||||
|
||||
class MetaTask:
|
||||
"""
|
||||
A single meta-task, a meta-dataset contains a list of them.
|
||||
It serves as a component as in MetaDatasetDS
|
||||
|
||||
The data processing is different
|
||||
- the processed input may be different between training and testing
|
||||
- When training, the X, y, X_test, y_test in training tasks are necessary (# PROC_MODE_FULL #)
|
||||
but not necessary in test tasks. (# PROC_MODE_TEST #)
|
||||
- When the meta model can be transferred into other dataset, only meta_info is necessary (# PROC_MODE_TRANSFER #)
|
||||
"""
|
||||
|
||||
PROC_MODE_FULL = "full"
|
||||
PROC_MODE_TEST = "test"
|
||||
PROC_MODE_TRANSFER = "transfer"
|
||||
|
||||
def __init__(self, task: dict, meta_info: object, mode: str = PROC_MODE_FULL):
|
||||
"""
|
||||
The `__init__` func is responsible for
|
||||
- store the task
|
||||
- store the origin input data for
|
||||
- process the input data for meta data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
task : dict
|
||||
the task to be enhanced by meta model
|
||||
|
||||
meta_info : object
|
||||
the input for meta model
|
||||
"""
|
||||
self.task = task
|
||||
self.meta_info = meta_info # the original meta input information, it will be processed later
|
||||
self.mode = mode
|
||||
|
||||
def get_dataset(self) -> Dataset:
|
||||
return init_instance_by_config(self.task["dataset"], accept_types=Dataset)
|
||||
|
||||
def get_meta_input(self) -> object:
|
||||
"""
|
||||
Return the **processed** meta_info
|
||||
"""
|
||||
return self.meta_info
|
||||
@@ -20,14 +20,12 @@ from tqdm.auto import tqdm
|
||||
from qlib.data.dataset import Dataset
|
||||
from qlib.log import get_module_logger
|
||||
from qlib.model.base import Model
|
||||
from qlib.utils import flatten_dict, get_callable_kwargs, init_instance_by_config
|
||||
from qlib.utils import flatten_dict, get_callable_kwargs, init_instance_by_config, auto_filter_kwargs, fill_placeholder
|
||||
from qlib.workflow import R
|
||||
from qlib.workflow.record_temp import SignalRecord
|
||||
from qlib.workflow.recorder import Recorder
|
||||
from qlib.workflow.task.manage import TaskManager, run_task
|
||||
|
||||
|
||||
# from qlib.data.dataset.weight import Reweighter
|
||||
from qlib.data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
def _log_task_info(task_config: dict):
|
||||
@@ -41,11 +39,9 @@ def _exe_task(task_config: dict):
|
||||
# model & dataset initiation
|
||||
model: Model = init_instance_by_config(task_config["model"])
|
||||
dataset: Dataset = init_instance_by_config(task_config["dataset"])
|
||||
# FIXME: resume reweighter after merging data selection
|
||||
# reweighter: Reweighter = task_config.get("reweighter", None)
|
||||
reweighter: Reweighter = task_config.get("reweighter", None)
|
||||
# model training
|
||||
# auto_filter_kwargs(model.fit)(dataset, reweighter=reweighter)
|
||||
model.fit(dataset)
|
||||
auto_filter_kwargs(model.fit)(dataset, reweighter=reweighter)
|
||||
R.save_objects(**{"params.pkl": model})
|
||||
# this dataset is saved for online inference. So the concrete data should not be dumped
|
||||
dataset.config(dump_all=False, recursive=True)
|
||||
@@ -87,103 +83,6 @@ def begin_task_train(task_config: dict, experiment_name: str, recorder_name: str
|
||||
return R.get_recorder()
|
||||
|
||||
|
||||
def get_item_from_obj(config: dict, name_path: str) -> object:
|
||||
"""
|
||||
Follow the name_path to get values from config
|
||||
For example:
|
||||
If we follow the example in in the Parameters section,
|
||||
Timestamp('2008-01-02 00:00:00') will be returned
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config : dict
|
||||
e.g.
|
||||
{'dataset': {'class': 'DatasetH',
|
||||
'kwargs': {'handler': {'class': 'Alpha158',
|
||||
'kwargs': {'end_time': '2020-08-01',
|
||||
'fit_end_time': '<dataset.kwargs.segments.train.1>',
|
||||
'fit_start_time': '<dataset.kwargs.segments.train.0>',
|
||||
'instruments': 'csi100',
|
||||
'start_time': '2008-01-01'},
|
||||
'module_path': 'qlib.contrib.data.handler'},
|
||||
'segments': {'test': (Timestamp('2017-01-03 00:00:00'),
|
||||
Timestamp('2019-04-08 00:00:00')),
|
||||
'train': (Timestamp('2008-01-02 00:00:00'),
|
||||
Timestamp('2014-12-31 00:00:00')),
|
||||
'valid': (Timestamp('2015-01-05 00:00:00'),
|
||||
Timestamp('2016-12-30 00:00:00'))}}
|
||||
}}
|
||||
name_path : str
|
||||
e.g.
|
||||
"dataset.kwargs.segments.train.1"
|
||||
|
||||
Returns
|
||||
-------
|
||||
object
|
||||
the retrieved object
|
||||
"""
|
||||
cur_cfg = config
|
||||
for k in name_path.split("."):
|
||||
if isinstance(cur_cfg, dict):
|
||||
cur_cfg = cur_cfg[k]
|
||||
elif k.isdigit():
|
||||
cur_cfg = cur_cfg[int(k)]
|
||||
else:
|
||||
raise ValueError(f"Error when getting {k} from cur_cfg")
|
||||
return cur_cfg
|
||||
|
||||
|
||||
def fill_placeholder(config: dict, config_extend: dict):
|
||||
"""
|
||||
Detect placeholder in config and fill them with config_extend.
|
||||
The item of dict must be single item(int, str, etc), dict and list. Tuples are not supported.
|
||||
There are two type of variables:
|
||||
- user-defined variables :
|
||||
e.g. when config_extend is `{"<MODEL>": model, "<DATASET>": dataset}`, "<MODEL>" and "<DATASET>" in `config` will be replaced with `model` `dataset`
|
||||
- variables extracted from `config` :
|
||||
e.g. the variables like "<dataset.kwargs.segments.train.0>" will be replaced with the values from `config`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config : dict
|
||||
the parameter dict will be filled
|
||||
config_extend : dict
|
||||
the value of all placeholders
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
the parameter dict
|
||||
"""
|
||||
# check the format of config_extend
|
||||
for placeholder in config_extend.keys():
|
||||
assert re.match(r"<[^<>]+>", placeholder)
|
||||
|
||||
# bfs
|
||||
top = 0
|
||||
tail = 1
|
||||
item_queue = [config]
|
||||
while top < tail:
|
||||
now_item = item_queue[top]
|
||||
top += 1
|
||||
if isinstance(now_item, list):
|
||||
item_keys = range(len(now_item))
|
||||
elif isinstance(now_item, dict):
|
||||
item_keys = now_item.keys()
|
||||
for key in item_keys:
|
||||
if isinstance(now_item[key], list) or isinstance(now_item[key], dict):
|
||||
item_queue.append(now_item[key])
|
||||
tail += 1
|
||||
elif isinstance(now_item[key], str):
|
||||
if now_item[key] in config_extend.keys():
|
||||
now_item[key] = config_extend[now_item[key]]
|
||||
else:
|
||||
m = re.match(r"<(?P<name_path>[^<>]+)>", now_item[key])
|
||||
if m is not None:
|
||||
now_item[key] = get_item_from_obj(config, m.groupdict()["name_path"])
|
||||
return config
|
||||
|
||||
|
||||
def end_task_train(rec: Recorder, experiment_name: str) -> Recorder:
|
||||
"""
|
||||
Finish task training with real model fitting and saving.
|
||||
@@ -349,7 +248,7 @@ class TrainerR(Trainer):
|
||||
if experiment_name is None:
|
||||
experiment_name = self.experiment_name
|
||||
recs = []
|
||||
for task in tqdm(tasks):
|
||||
for task in tqdm(tasks, desc="train tasks"):
|
||||
rec = train_func(task, experiment_name, **kwargs)
|
||||
rec.set_tags(**{self.STATUS_KEY: self.STATUS_BEGIN})
|
||||
recs.append(rec)
|
||||
@@ -606,13 +505,17 @@ class DelayTrainerRM(TrainerRM):
|
||||
tasks = [tasks]
|
||||
if len(tasks) == 0:
|
||||
return []
|
||||
return super().train(
|
||||
_skip_run_task = self.skip_run_task
|
||||
self.skip_run_task = False # The task preparation can't be skipped
|
||||
res = super().train(
|
||||
tasks,
|
||||
train_func=train_func,
|
||||
experiment_name=experiment_name,
|
||||
after_status=TaskManager.STATUS_PART_DONE,
|
||||
**kwargs,
|
||||
)
|
||||
self.skip_run_task = _skip_run_task
|
||||
return res
|
||||
|
||||
def end_train(self, recs, end_train_func=None, experiment_name: str = None, **kwargs) -> List[Recorder]:
|
||||
"""
|
||||
|
||||
15
qlib/model/utils.py
Normal file
15
qlib/model/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from torch.utils.data import Dataset
|
||||
|
||||
|
||||
class ConcatDataset(Dataset):
|
||||
def __init__(self, *datasets):
|
||||
self.datasets = datasets
|
||||
|
||||
def __getitem__(self, i):
|
||||
return tuple(d[i] for d in self.datasets)
|
||||
|
||||
def __len__(self):
|
||||
return min(len(d) for d in self.datasets)
|
||||
Reference in New Issue
Block a user