1
0
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:
you-n-g
2022-01-10 16:52:37 +08:00
committed by GitHub
parent 184ce34a34
commit cf35562e84
52 changed files with 2441 additions and 456 deletions

View File

@@ -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

View File

@@ -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()):

View File

@@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
from .task import MetaTask
from .dataset import MetaTaskDataset

View 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
View 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
View 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

View File

@@ -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
View 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)