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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user