1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-11 14:56:55 +08:00

Online Serving V4

This commit is contained in:
lzh222333
2021-03-26 04:20:25 +00:00
parent 8abdd63869
commit 46cd57688e
12 changed files with 366 additions and 323 deletions

View File

@@ -1,116 +1,172 @@
from qlib.workflow import R
from abc import abstractmethod
from typing import Callable, Union
import pandas as pd
from typing import Union
from typing import Callable
from qlib import get_module_logger
from qlib.workflow.task.utils import list_recorders
class TaskCollector:
class Collector:
"""
Collect the record (or its results) of the tasks
This class will divide disorderly records or anything worth collecting into different groups based on the group_key.
After grouping, we can reduce the useful information from different groups.
"""
def group(self, *args, **kwargs):
"""
According to the get_group_key_func, divide disorderly things into different groups.
For example:
.. code-block:: python
input:
[thing1, thing2, thing3, thing4, thing5]
output:
{
"group_name1": [thing3, thing5, thing1]
"group_name2": [thing2, thing4]
}
Args:
get_group_key_func (Callable): get a group key based on a thing
things_list (list): a list of things
Returns:
dict: a dict including the group key and members of the group.
"""
raise NotImplementedError(f"Please implement the `group` method.")
def reduce(self, things_group: dict):
"""
Using the dict from `group`, reduce useful information.
Args:
things_group (dict): a dict after grouping
Returns:
dict: a dict including the group key, the information key and the information value
"""
raise NotImplementedError(f"Please implement the `reduce` method.")
def collect(self, *args, **kwargs):
"""group and reduce
Returns:
dict: a dict including the group key, the information key and the information value
"""
grouped = self.group(*args, **kwargs)
return self.reduce(grouped)
class RecorderCollector(Collector):
"""
The Recorder's Collector. This class is a implementation of Collector, collecting some artifacts saved by Recorder.
"""
def __init__(self, experiment_name: str) -> None:
self.exp_name = experiment_name
self.exp = R.get_exp(experiment_name=experiment_name)
self.logger = get_module_logger("TaskCollector")
self.logger = get_module_logger(self.__class__.__name__)
def list_recorders(self, rec_filter_func=None):
_artifacts_key_path = {"pred": "pred.pkl", "IC": "sig_analysis/ic.pkl"}
_artifacts_key_merge_method = {}
recs = self.exp.list_recorders()
recs_flt = {}
for rid, rec in recs.items():
if rec_filter_func is None or rec_filter_func(rec):
recs_flt[rid] = rec
def default_merge(self, artifact_list):
"""Merge disorderly artifacts in artifact list.
return recs_flt
Args:
artifact_list (list): A artifact list.
def list_recorders_by_task(self, task_filter_func=None):
def rec_filter(recorder):
return task_filter_func(self.get_task(recorder))
return self.list_recorders(rec_filter)
def list_latest_recorders(self, rec_filter_func=None):
recs_flt = self.list_recorders(rec_filter_func)
max_test = self.latest_time(recs_flt)
latest_rec = {}
for rid, rec in recs_flt.items():
if self.get_task(rec)["dataset"]["kwargs"]["segments"]["test"] == max_test:
latest_rec[rid] = rec
return latest_rec
def get_recorder_by_id(self, recorder_id):
return self.exp.get_recorder(recorder_id, create=False)
def get_task(self, recorder):
if isinstance(recorder, str):
recorder = self.get_recorder_by_id(recorder_id=recorder)
try:
task = recorder.load_object("task")
except OSError:
raise OSError(f"Can't find task in {recorder.info['id']}, have you trained with model.trainer.task_train?")
return task
def latest_time(self, recorders):
if len(recorders) == 0:
raise Exception(f"Can't find any recorder in {self.exp_name}")
max_test = max(self.get_task(rec)["dataset"]["kwargs"]["segments"]["test"] for rec in recorders.values())
return max_test
class RollingCollector(TaskCollector):
"""
Collect the record results of the rolling tasks
"""
def __init__(
self,
experiment_name: str,
) -> None:
super().__init__(experiment_name)
self.logger = get_module_logger("RollingCollector")
def collect_rolling_predictions(self, get_key_func, rec_filter_func=None):
"""For rolling tasks, the predictions will be in the diffierent recorder.
To collect and concat the predictions of one rolling task, get_key_func will help this method see which group a recorder will be.
Parameters
----------
get_key_func : Callable[dict,str]
a function that get task config and return its group str
rec_filter_func : Callable[Recorder,bool], optional
a function that decide whether filter a recorder, by default None
Returns
-------
dict
a dict of {group: predictions}
Raises:
NotImplementedError: [description]
"""
raise NotImplementedError(f"Please implement the `default_merge` method.")
def group(self, get_group_key_func, rec_filter_func=None):
"""
Filter recorders and group recorders by group key.
Args:
get_group_key_func (Callable): get a group key based on a recorder
rec_filter_func (Callable, optional): filter the recorders in this experiment. Defaults to None.
Returns:
dict: a dict including the group key and recorders of the group
"""
# filter records
recs_flt = self.list_recorders(rec_filter_func)
recs_flt = list_recorders(self.exp_name, rec_filter_func)
# group
recs_group = {}
for _, rec in recs_flt.items():
task = self.get_task(rec)
group_key = get_key_func(task)
group_key = get_group_key_func(rec)
recs_group.setdefault(group_key, []).append(rec)
# reduce group
reduce_group = {}
for k, rec_l in recs_group.items():
pred_l = []
for rec in rec_l:
pred_l.append(rec.load_object("pred.pkl").iloc[:, 0])
# Make sure the pred are sorted according to the rolling start time
pred_l.sort(key=lambda pred: pred.index.get_level_values("datetime").min())
pred = pd.concat(pred_l)
# If there are duplicated predition, we use the latest perdiction
pred = pred[~pred.index.duplicated(keep="last")]
pred = pred.sort_index()
reduce_group[k] = pred
return recs_group
return reduce_group
def reduce(self, recs_group: dict, artifact_keys_list: list = None):
"""
Reduce artifacts based on the dict of grouped recorder.
The artifacts need be declared by artifact_keys_list.
The artifacts path in recorder need be declared by _artifacts_key_path.
If there is no declartion in _artifacts_key_merge_method, then use default_merge method to merge it.
Args:
recs_group (dict): The dict grouped by `group`
artifact_keys_list (list): The list of artifact keys. If it is None, then use all artifacts in _artifacts_key_path.
Returns:
a dict including the group key, the artifact key and the artifact value.
For example:
.. code-block:: python
{
group_key: {"pred": <VALUE>, "IC": <VALUE>}
}
"""
if artifact_keys_list == None:
artifact_keys_list = self._artifacts_key_path.keys()
reduce_group = {}
for group_key, recorder_list in recs_group.items():
reduced_artifacts = {}
for artifact_key in artifact_keys_list:
artifact_list = []
for recorder in recorder_list:
artifact_list.append(recorder.load_object(self._artifacts_key_path[artifact_key]))
merge_method = self._artifacts_key_merge_method.get(artifact_key, self.default_merge)
artifact = merge_method(artifact_list)
reduced_artifacts[artifact_key] = artifact
reduce_group[group_key] = reduced_artifacts
return reduce_group
class RollingCollector(RecorderCollector):
"""
Collect the record results of the rolling tasks
"""
def __init__(self, experiment_name: str):
super().__init__(experiment_name)
self.logger = get_module_logger(self.__class__.__name__)
def default_merge(self, artifact_list):
"""merge disorderly artifacts based on the datetime.
Args:
artifact_list (list): a list of artifacts from different recorders
Returns:
merged artifact
"""
# Make sure the pred are sorted according to the rolling start time
artifact_list.sort(key=lambda x: x.index.get_level_values("datetime").min())
artifact = pd.concat(artifact_list)
# If there are duplicated predition, we use the latest perdiction
artifact = artifact[~artifact.index.duplicated(keep="last")]
artifact = artifact.sort_index()
return artifact