mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-13 15:56: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
qlib/contrib/meta/__init__.py
Normal file
4
qlib/contrib/meta/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from .data_selection import MetaTaskDS, MetaDatasetDS, MetaModelDS
|
||||
5
qlib/contrib/meta/data_selection/__init__.py
Normal file
5
qlib/contrib/meta/data_selection/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from .dataset import MetaDatasetDS, MetaTaskDS
|
||||
from .model import MetaModelDS
|
||||
325
qlib/contrib/meta/data_selection/dataset.py
Normal file
325
qlib/contrib/meta/data_selection/dataset.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
from copy import deepcopy
|
||||
from qlib.data.dataset.utils import init_task_handler
|
||||
from qlib.utils.data import deepcopy_basic_type
|
||||
from qlib.contrib.torch import data_to_tensor
|
||||
from qlib.workflow.task.utils import TimeAdjuster
|
||||
from qlib.model.meta.task import MetaTask
|
||||
from typing import Dict, List, Union, Text, Tuple
|
||||
from qlib.data.dataset.handler import DataHandler
|
||||
from qlib.log import get_module_logger
|
||||
from qlib.utils import auto_filter_kwargs, get_date_by_shift, init_instance_by_config
|
||||
from qlib.workflow import R
|
||||
from qlib.workflow.task.gen import RollingGen, task_generator
|
||||
from joblib import Parallel, delayed
|
||||
from qlib.model.meta.dataset import MetaTaskDataset
|
||||
from qlib.model.trainer import task_train, TrainerR
|
||||
from qlib.data.dataset import DatasetH
|
||||
from tqdm.auto import tqdm
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
class InternalData:
|
||||
def __init__(self, task_tpl: dict, step: int, exp_name: str):
|
||||
self.task_tpl = task_tpl
|
||||
self.step = step
|
||||
self.exp_name = exp_name
|
||||
|
||||
def setup(self, trainer=TrainerR, trainer_kwargs={}):
|
||||
"""
|
||||
after running this function `self.data_ic_df` will become set.
|
||||
Each col represents a data.
|
||||
Each row represents the Timestamp of performance of that data.
|
||||
For example,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
2021-06-21 2021-06-04 2021-05-21 2021-05-07 2021-04-20 2021-04-06 2021-03-22 2021-03-08 ...
|
||||
2021-07-02 2021-06-18 2021-06-03 2021-05-20 2021-05-06 2021-04-19 2021-04-02 2021-03-19 ...
|
||||
datetime ...
|
||||
2018-01-02 0.079782 0.115975 0.070866 0.028849 -0.081170 0.140380 0.063864 0.110987 ...
|
||||
2018-01-03 0.123386 0.107789 0.071037 0.045278 -0.060782 0.167446 0.089779 0.124476 ...
|
||||
2018-01-04 0.140775 0.097206 0.063702 0.042415 -0.078164 0.173218 0.098914 0.114389 ...
|
||||
2018-01-05 0.030320 -0.037209 -0.044536 -0.047267 -0.081888 0.045648 0.059947 0.047652 ...
|
||||
2018-01-08 0.107201 0.009219 -0.015995 -0.036594 -0.086633 0.108965 0.122164 0.108508 ...
|
||||
... ... ... ... ... ... ... ... ... ...
|
||||
|
||||
"""
|
||||
|
||||
# 1) prepare the prediction of proxy models
|
||||
perf_task_tpl = deepcopy(self.task_tpl) # this task is supposed to contains no complicated objects
|
||||
|
||||
trainer = auto_filter_kwargs(trainer)(experiment_name=self.exp_name, **trainer_kwargs)
|
||||
# NOTE:
|
||||
# The handler is initialized for only once.
|
||||
if not trainer.has_worker():
|
||||
self.dh = init_task_handler(perf_task_tpl)
|
||||
else:
|
||||
self.dh = init_instance_by_config(perf_task_tpl["dataset"]["kwargs"]["handler"])
|
||||
|
||||
seg = perf_task_tpl["dataset"]["kwargs"]["segments"]
|
||||
|
||||
# We want to split the training time period into small segments.
|
||||
perf_task_tpl["dataset"]["kwargs"]["segments"] = {
|
||||
"train": (DatasetH.get_min_time(seg), DatasetH.get_max_time(seg)),
|
||||
"test": (None, None),
|
||||
}
|
||||
|
||||
# NOTE:
|
||||
# we play a trick here
|
||||
# treat the training segments as test to create the rolling tasks
|
||||
rg = RollingGen(step=self.step, test_key="train", train_key=None, task_copy_func=deepcopy_basic_type)
|
||||
gen_task = task_generator(perf_task_tpl, [rg])
|
||||
|
||||
recorders = R.list_recorders(experiment_name=self.exp_name)
|
||||
if len(gen_task) == len(recorders):
|
||||
get_module_logger("Internal Data").info("the data has been initialized")
|
||||
else:
|
||||
# train new models
|
||||
assert 0 == len(recorders), "An empty experiment is required for setup `InternalData``"
|
||||
trainer.train(gen_task)
|
||||
|
||||
# 2) extract the similarity matrix
|
||||
label_df = self.dh.fetch(col_set="label")
|
||||
# for
|
||||
recorders = R.list_recorders(experiment_name=self.exp_name)
|
||||
|
||||
key_l = []
|
||||
ic_l = []
|
||||
for _, rec in tqdm(recorders.items(), desc="calc"):
|
||||
pred = rec.load_object("pred.pkl")
|
||||
task = rec.load_object("task")
|
||||
data_key = task["dataset"]["kwargs"]["segments"]["train"]
|
||||
key_l.append(data_key)
|
||||
ic_l.append(delayed(self._calc_perf)(pred.iloc[:, 0], label_df.iloc[:, 0]))
|
||||
|
||||
ic_l = Parallel(n_jobs=-1)(ic_l)
|
||||
self.data_ic_df = pd.DataFrame(dict(zip(key_l, ic_l)))
|
||||
self.data_ic_df = self.data_ic_df.sort_index().sort_index(axis=1)
|
||||
|
||||
del self.dh # handler is not useful now
|
||||
|
||||
def _calc_perf(self, pred, label):
|
||||
df = pd.DataFrame({"pred": pred, "label": label})
|
||||
df = df.groupby("datetime").corr(method="spearman")
|
||||
corr = df.loc(axis=0)[:, "pred"]["label"].droplevel(axis=0, level=-1)
|
||||
return corr
|
||||
|
||||
def update(self):
|
||||
"""update the data for online trading"""
|
||||
# TODO:
|
||||
# when new data are totally(including label) available
|
||||
# - update the prediction
|
||||
# - update the data similarity map(if applied)
|
||||
|
||||
|
||||
class MetaTaskDS(MetaTask):
|
||||
"""Meta Task for Data Selection"""
|
||||
|
||||
def __init__(self, task: dict, meta_info: pd.DataFrame, mode: str = MetaTask.PROC_MODE_FULL, fill_method="max"):
|
||||
"""
|
||||
The description of the processed data
|
||||
|
||||
time_perf: A array with shape <hist_step_n * step, data pieces> -> data piece performance
|
||||
|
||||
time_belong: A array with shape <sample, data pieces> -> belong or not (1. or 0.)
|
||||
array([[1., 0., 0., ..., 0., 0., 0.],
|
||||
[1., 0., 0., ..., 0., 0., 0.],
|
||||
[1., 0., 0., ..., 0., 0., 0.],
|
||||
...,
|
||||
[0., 0., 0., ..., 0., 0., 1.],
|
||||
[0., 0., 0., ..., 0., 0., 1.],
|
||||
[0., 0., 0., ..., 0., 0., 1.]])
|
||||
|
||||
"""
|
||||
super().__init__(task, meta_info)
|
||||
self.fill_method = fill_method
|
||||
|
||||
time_perf = self._get_processed_meta_info()
|
||||
self.processed_meta_input = {"time_perf": time_perf}
|
||||
# FIXME: memory issue in this step
|
||||
if mode == MetaTask.PROC_MODE_FULL:
|
||||
# process metainfo_
|
||||
ds = self.get_dataset()
|
||||
|
||||
# these three lines occupied 70% of the time of initializing MetaTaskDS
|
||||
d_train, d_test = ds.prepare(["train", "test"], col_set=["feature", "label"])
|
||||
prev_size = d_test.shape[0]
|
||||
d_train = d_train.dropna(axis=0)
|
||||
d_test = d_test.dropna(axis=0)
|
||||
if prev_size == 0 or d_test.shape[0] / prev_size <= 0.1:
|
||||
raise ValueError(f"Most of samples are dropped. Please check this task: {task}")
|
||||
|
||||
assert (
|
||||
d_test.groupby("datetime").size().shape[0] >= 5
|
||||
), "In this segment, this trading dates is less than 5, you'd better check the data."
|
||||
|
||||
sample_time_belong = np.zeros((d_train.shape[0], time_perf.shape[1]))
|
||||
for i, col in enumerate(time_perf.columns):
|
||||
# these two lines of code occupied 20% of the time of initializing MetaTaskDS
|
||||
slc = slice(*d_train.index.slice_locs(start=col[0], end=col[1]))
|
||||
sample_time_belong[slc, i] = 1.0
|
||||
|
||||
# If you want that last month also belongs to the last time_perf
|
||||
# Assumptions: the latest data has similar performance like the last month
|
||||
sample_time_belong[sample_time_belong.sum(axis=1) != 1, -1] = 1.0
|
||||
|
||||
self.processed_meta_input.update(
|
||||
dict(
|
||||
X=d_train["feature"],
|
||||
y=d_train["label"].iloc[:, 0],
|
||||
X_test=d_test["feature"],
|
||||
y_test=d_test["label"].iloc[:, 0],
|
||||
time_belong=sample_time_belong,
|
||||
test_idx=d_test["label"].index,
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: set device: I think this is not necessary to converting data format.
|
||||
self.processed_meta_input = data_to_tensor(self.processed_meta_input)
|
||||
|
||||
def _get_processed_meta_info(self):
|
||||
meta_info_norm = self.meta_info.sub(self.meta_info.mean(axis=1), axis=0) # .fillna(0.)
|
||||
if self.fill_method == "max":
|
||||
meta_info_norm = meta_info_norm.T.fillna(
|
||||
meta_info_norm.max(axis=1)
|
||||
).T # fill it with row max to align with previous implementation
|
||||
elif self.fill_method == "zero":
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError(f"This type of input is not supported")
|
||||
meta_info_norm = meta_info_norm.fillna(0.0) # always fill zero in case of NaN
|
||||
return meta_info_norm
|
||||
|
||||
def get_meta_input(self):
|
||||
return self.processed_meta_input
|
||||
|
||||
|
||||
class MetaDatasetDS(MetaTaskDataset):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
task_tpl: Union[dict, list],
|
||||
step: int,
|
||||
trunc_days: int = None,
|
||||
rolling_ext_days: int = 0,
|
||||
exp_name: Union[str, InternalData],
|
||||
segments: Union[Dict[Text, Tuple], float],
|
||||
hist_step_n: int = 10,
|
||||
task_mode: str = MetaTask.PROC_MODE_FULL,
|
||||
fill_method: str = "max",
|
||||
):
|
||||
"""
|
||||
A dataset for meta model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
task_tpl : Union[dict, list]
|
||||
Decide what tasks are used.
|
||||
- dict : the task template, the prepared task is generated with `step`, `trunc_days` and `RollingGen`
|
||||
- list : when list, use the list of tasks directly
|
||||
the list is supposed to be sorted according timeline
|
||||
step : int
|
||||
the rolling step
|
||||
trunc_days: int
|
||||
days to be truncated based on the test start
|
||||
rolling_ext_days: int
|
||||
sometimes users want to train meta models for a longer test period but with smaller rolling steps for more task samples.
|
||||
the total length of test periods will be `step + rolling_ext_days`
|
||||
|
||||
exp_name : Union[str, InternalData]
|
||||
Decide what meta_info are used for prediction.
|
||||
- str: the name of the experiment to store the performance of data
|
||||
- InternalData: a prepared internal data
|
||||
segments: Union[Dict[Text, Tuple], float]
|
||||
the segments to divide data
|
||||
both left and right
|
||||
if segments is a float:
|
||||
the float represents the percentage of data for training
|
||||
hist_step_n: int
|
||||
length of historical steps for the meta infomation
|
||||
task_mode : str
|
||||
Please refer to the docs of MetaTask
|
||||
"""
|
||||
super().__init__(segments=segments)
|
||||
if isinstance(exp_name, InternalData):
|
||||
self.internal_data = exp_name
|
||||
else:
|
||||
self.internal_data = InternalData(task_tpl, step=step, exp_name=exp_name)
|
||||
self.internal_data.setup()
|
||||
self.task_tpl = deepcopy(task_tpl) # FIXME: if the handler is shared, how to avoid the explosion of the memroy.
|
||||
self.trunc_days = trunc_days
|
||||
self.hist_step_n = hist_step_n
|
||||
self.step = step
|
||||
|
||||
if isinstance(task_tpl, dict):
|
||||
rg = RollingGen(
|
||||
step=step, trunc_days=trunc_days, task_copy_func=deepcopy_basic_type
|
||||
) # NOTE: trunc_days is very important !!!!
|
||||
task_iter = rg(task_tpl)
|
||||
if rolling_ext_days > 0:
|
||||
self.ta = TimeAdjuster(future=True)
|
||||
for t in task_iter:
|
||||
t["dataset"]["kwargs"]["segments"]["test"] = self.ta.shift(
|
||||
t["dataset"]["kwargs"]["segments"]["test"], step=rolling_ext_days, rtype=RollingGen.ROLL_EX
|
||||
)
|
||||
if task_mode == MetaTask.PROC_MODE_FULL:
|
||||
# Only pre initializing the task when full task is req
|
||||
# initializing handler and share it.
|
||||
init_task_handler(task_tpl)
|
||||
else:
|
||||
assert isinstance(task_tpl, list)
|
||||
task_iter = task_tpl
|
||||
|
||||
self.task_list = []
|
||||
self.meta_task_l = []
|
||||
logger = get_module_logger("MetaDatasetDS")
|
||||
logger.info(f"Example task for training meta model: {task_iter[0]}")
|
||||
for t in tqdm(task_iter, desc="creating meta tasks"):
|
||||
try:
|
||||
self.meta_task_l.append(
|
||||
MetaTaskDS(t, meta_info=self._prepare_meta_ipt(t), mode=task_mode, fill_method=fill_method)
|
||||
)
|
||||
self.task_list.append(t)
|
||||
except ValueError as e:
|
||||
logger.warning(f"ValueError: {e}")
|
||||
assert len(self.meta_task_l) > 0, "No meta tasks found. Please check the data and setting"
|
||||
|
||||
def _prepare_meta_ipt(self, task):
|
||||
ic_df = self.internal_data.data_ic_df
|
||||
|
||||
segs = task["dataset"]["kwargs"]["segments"]
|
||||
end = max([segs[k][1] for k in ("train", "valid") if k in segs])
|
||||
ic_df_avail = ic_df.loc[:end, pd.IndexSlice[:, :end]]
|
||||
|
||||
# meta data set focus on the **information** instead of preprocess
|
||||
# 1) filter the future info
|
||||
def mask_future(s):
|
||||
"""mask future information"""
|
||||
# from qlib.utils import get_date_by_shift
|
||||
start, end = s.name
|
||||
end = get_date_by_shift(trading_date=end, shift=self.trunc_days - 1, future=True)
|
||||
return s.mask((s.index >= start) & (s.index <= end))
|
||||
|
||||
ic_df_avail = ic_df_avail.apply(mask_future) # apply to each col
|
||||
|
||||
# 2) filter the info with too long periods
|
||||
total_len = self.step * self.hist_step_n
|
||||
if ic_df_avail.shape[0] >= total_len:
|
||||
return ic_df_avail.iloc[-total_len:]
|
||||
else:
|
||||
raise ValueError("the history of distribution data is not long enough.")
|
||||
|
||||
def _prepare_seg(self, segment: Text) -> List[MetaTask]:
|
||||
if isinstance(self.segments, float):
|
||||
train_task_n = int(len(self.meta_task_l) * self.segments)
|
||||
if segment == "train":
|
||||
return self.meta_task_l[:train_task_n]
|
||||
elif segment == "test":
|
||||
return self.meta_task_l[train_task_n:]
|
||||
else:
|
||||
raise NotImplementedError(f"This type of input is not supported")
|
||||
else:
|
||||
raise NotImplementedError(f"This type of input is not supported")
|
||||
182
qlib/contrib/meta/data_selection/model.py
Normal file
182
qlib/contrib/meta/data_selection/model.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
from qlib.log import get_module_logger
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from qlib.model.meta.task import MetaTask
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch import optim
|
||||
from tqdm.auto import tqdm
|
||||
import collections
|
||||
import copy
|
||||
from typing import Union, List, Tuple, Dict
|
||||
|
||||
from ....data.dataset.weight import Reweighter
|
||||
from ....model.meta.dataset import MetaTaskDataset
|
||||
from ....model.meta.model import MetaModel, MetaTaskModel
|
||||
from ....workflow import R
|
||||
|
||||
from .utils import ICLoss
|
||||
from .dataset import MetaDatasetDS
|
||||
from qlib.contrib.meta.data_selection.net import PredNet
|
||||
from qlib.data.dataset.weight import Reweighter
|
||||
from qlib.log import get_module_logger
|
||||
|
||||
logger = get_module_logger("data selection")
|
||||
|
||||
|
||||
class TimeReweighter(Reweighter):
|
||||
def __init__(self, time_weight: pd.Series):
|
||||
self.time_weight = time_weight
|
||||
|
||||
def reweight(self, data: Union[pd.DataFrame, pd.Series]):
|
||||
# TODO: handling TSDataSampler
|
||||
w_s = pd.Series(1.0, index=data.index)
|
||||
for k, w in self.time_weight.items():
|
||||
w_s.loc[slice(*k)] = w
|
||||
logger.info(f"Reweighting result: {w_s}")
|
||||
return w_s
|
||||
|
||||
|
||||
class MetaModelDS(MetaTaskModel):
|
||||
"""
|
||||
The meta-model for meta-learning-based data selection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
step,
|
||||
hist_step_n,
|
||||
clip_method="tanh",
|
||||
clip_weight=2.0,
|
||||
criterion="ic_loss",
|
||||
lr=0.0001,
|
||||
max_epoch=100,
|
||||
seed=43,
|
||||
):
|
||||
self.step = step
|
||||
self.hist_step_n = hist_step_n
|
||||
self.clip_method = clip_method
|
||||
self.clip_weight = clip_weight
|
||||
self.criterion = criterion
|
||||
self.lr = lr
|
||||
self.max_epoch = max_epoch
|
||||
self.fitted = False
|
||||
torch.manual_seed(seed)
|
||||
|
||||
def run_epoch(self, phase, task_list, epoch, opt, loss_l, ignore_weight=False):
|
||||
if phase == "train":
|
||||
self.tn.train()
|
||||
torch.set_grad_enabled(True)
|
||||
else:
|
||||
self.tn.eval()
|
||||
torch.set_grad_enabled(False)
|
||||
running_loss = 0.0
|
||||
pred_y_all = []
|
||||
for task in tqdm(task_list, desc=f"{phase} Task", leave=False):
|
||||
meta_input = task.get_meta_input()
|
||||
pred, weights = self.tn(
|
||||
meta_input["X"],
|
||||
meta_input["y"],
|
||||
meta_input["time_perf"],
|
||||
meta_input["time_belong"],
|
||||
meta_input["X_test"],
|
||||
ignore_weight=ignore_weight,
|
||||
)
|
||||
if self.criterion == "mse":
|
||||
criterion = nn.MSELoss()
|
||||
loss = criterion(pred, meta_input["y_test"])
|
||||
elif self.criterion == "ic_loss":
|
||||
criterion = ICLoss()
|
||||
try:
|
||||
loss = criterion(pred, meta_input["y_test"], meta_input["test_idx"], skip_size=50)
|
||||
except ValueError as e:
|
||||
get_module_logger("MetaModelDS").warning(f"Exception `{e}` when calculating IC loss")
|
||||
continue
|
||||
|
||||
assert not np.isnan(loss.detach().item()), "NaN loss!"
|
||||
|
||||
if phase == "train":
|
||||
opt.zero_grad()
|
||||
norm_loss = nn.MSELoss()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
elif phase == "test":
|
||||
pass
|
||||
|
||||
pred_y_all.append(
|
||||
pd.DataFrame(
|
||||
{
|
||||
"pred": pd.Series(pred.detach().cpu().numpy(), index=meta_input["test_idx"]),
|
||||
"label": pd.Series(meta_input["y_test"].detach().cpu().numpy(), index=meta_input["test_idx"]),
|
||||
}
|
||||
)
|
||||
)
|
||||
running_loss += loss.detach().item()
|
||||
running_loss = running_loss / len(task_list)
|
||||
loss_l.setdefault(phase, []).append(running_loss)
|
||||
|
||||
pred_y_all = pd.concat(pred_y_all)
|
||||
ic = pred_y_all.groupby("datetime").apply(lambda df: df["pred"].corr(df["label"], method="spearman")).mean()
|
||||
|
||||
R.log_metrics(**{f"loss/{phase}": running_loss, "step": epoch})
|
||||
R.log_metrics(**{f"ic/{phase}": ic, "step": epoch})
|
||||
|
||||
def fit(self, meta_dataset: MetaDatasetDS):
|
||||
"""
|
||||
The meta-learning-based data selection interacts directly with meta-dataset due to the close-form proxy measurement.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
meta_dataset : MetaDatasetDS
|
||||
The meta-model takes the meta-dataset for its training process.
|
||||
"""
|
||||
|
||||
if not self.fitted:
|
||||
for k in set(["lr", "step", "hist_step_n", "clip_method", "clip_weight", "criterion", "max_epoch"]):
|
||||
R.log_params(**{k: getattr(self, k)})
|
||||
|
||||
# FIXME: get test tasks for just checking the performance
|
||||
phases = ["train", "test"]
|
||||
meta_tasks_l = meta_dataset.prepare_tasks(phases)
|
||||
|
||||
if len(meta_tasks_l[1]):
|
||||
R.log_params(
|
||||
**dict(proxy_test_begin=meta_tasks_l[1][0].task["dataset"]["kwargs"]["segments"]["test"])
|
||||
) # debug: record when the test phase starts
|
||||
|
||||
self.tn = PredNet(
|
||||
step=self.step, hist_step_n=self.hist_step_n, clip_weight=self.clip_weight, clip_method=self.clip_method
|
||||
)
|
||||
|
||||
opt = optim.Adam(self.tn.parameters(), lr=self.lr)
|
||||
|
||||
# run weight with no weight
|
||||
for phase, task_list in zip(phases, meta_tasks_l):
|
||||
self.run_epoch(f"{phase}_noweight", task_list, 0, opt, {}, ignore_weight=True)
|
||||
self.run_epoch(f"{phase}_init", task_list, 0, opt, {})
|
||||
|
||||
# run training
|
||||
loss_l = {}
|
||||
for epoch in tqdm(range(self.max_epoch), desc="epoch"):
|
||||
for phase, task_list in zip(phases, meta_tasks_l):
|
||||
self.run_epoch(phase, task_list, epoch, opt, loss_l)
|
||||
R.save_objects(**{"model.pkl": self.tn})
|
||||
self.fitted = True
|
||||
|
||||
def _prepare_task(self, task: MetaTask) -> dict:
|
||||
meta_ipt = task.get_meta_input()
|
||||
weights = self.tn.twm(meta_ipt["time_perf"])
|
||||
|
||||
weight_s = pd.Series(weights.detach().cpu().numpy(), index=task.meta_info.columns)
|
||||
task = copy.copy(task.task) # NOTE: this is a shallow copy.
|
||||
task["reweighter"] = TimeReweighter(weight_s)
|
||||
return task
|
||||
|
||||
def inference(self, meta_dataset: MetaTaskDataset) -> List[dict]:
|
||||
res = []
|
||||
for mt in meta_dataset.prepare_tasks("test"):
|
||||
res.append(self._prepare_task(mt))
|
||||
return res
|
||||
68
qlib/contrib/meta/data_selection/net.py
Normal file
68
qlib/contrib/meta/data_selection/net.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from .utils import preds_to_weight_with_clamp, SingleMetaBase
|
||||
|
||||
|
||||
class TimeWeightMeta(SingleMetaBase):
|
||||
def __init__(self, hist_step_n, clip_weight=None, clip_method="clamp"):
|
||||
# clip_method includes "tanh" or "clamp"
|
||||
super().__init__(hist_step_n, clip_weight, clip_method)
|
||||
self.linear = nn.Linear(hist_step_n, 1)
|
||||
self.k = nn.Parameter(torch.Tensor([8.0]))
|
||||
|
||||
def forward(self, time_perf, time_belong=None, return_preds=False):
|
||||
hist_step_n = self.linear.in_features
|
||||
# NOTE: the reshape order is very important
|
||||
time_perf = time_perf.reshape(hist_step_n, time_perf.shape[0] // hist_step_n, *time_perf.shape[1:])
|
||||
time_perf = torch.mean(time_perf, dim=1, keepdim=False)
|
||||
|
||||
preds = []
|
||||
for i in range(time_perf.shape[1]):
|
||||
preds.append(self.linear(time_perf[:, i]))
|
||||
preds = torch.cat(preds)
|
||||
preds = preds - torch.mean(preds) # avoid using future information
|
||||
preds = preds * self.k
|
||||
if return_preds:
|
||||
if time_belong is None:
|
||||
return preds
|
||||
else:
|
||||
return time_belong @ preds
|
||||
else:
|
||||
weights = preds_to_weight_with_clamp(preds, self.clip_weight, self.clip_method)
|
||||
if time_belong is None:
|
||||
return weights
|
||||
else:
|
||||
return time_belong @ weights
|
||||
|
||||
|
||||
class PredNet(nn.Module):
|
||||
def __init__(self, step, hist_step_n, clip_weight=None, clip_method="tanh"):
|
||||
super().__init__()
|
||||
self.step = step
|
||||
self.twm = TimeWeightMeta(hist_step_n=hist_step_n, clip_weight=clip_weight, clip_method=clip_method)
|
||||
self.init_paramters(hist_step_n)
|
||||
|
||||
def get_sample_weights(self, X, time_perf, time_belong, ignore_weight=False):
|
||||
weights = torch.from_numpy(np.ones(X.shape[0])).float().to(X.device)
|
||||
if not ignore_weight:
|
||||
if time_perf is not None:
|
||||
weights_t = self.twm(time_perf, time_belong)
|
||||
weights = weights * weights_t
|
||||
return weights
|
||||
|
||||
def forward(self, X, y, time_perf, time_belong, X_test, ignore_weight=False):
|
||||
"""Please refer to the docs of MetaTaskDS for the description of the variables"""
|
||||
weights = self.get_sample_weights(X, time_perf, time_belong, ignore_weight=ignore_weight)
|
||||
X_w = X.T * weights.view(1, -1)
|
||||
theta = torch.inverse(X_w @ X) @ X_w @ y
|
||||
return X_test @ theta, weights
|
||||
|
||||
def init_paramters(self, hist_step_n):
|
||||
self.twm.linear.weight.data = 1.0 / hist_step_n + self.twm.linear.weight.data * 0.01
|
||||
self.twm.linear.bias.data.fill_(0.0)
|
||||
98
qlib/contrib/meta/data_selection/utils.py
Normal file
98
qlib/contrib/meta/data_selection/utils.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
from qlib.contrib.torch import data_to_tensor
|
||||
|
||||
|
||||
class ICLoss(nn.Module):
|
||||
def forward(self, pred, y, idx, skip_size=50):
|
||||
"""forward.
|
||||
|
||||
:param pred:
|
||||
:param y:
|
||||
:param idx: Assume the level of the idx is (date, inst), and it is sorted
|
||||
"""
|
||||
prev = None
|
||||
diff_point = []
|
||||
for i, (date, inst) in enumerate(idx):
|
||||
if date != prev:
|
||||
diff_point.append(i)
|
||||
prev = date
|
||||
diff_point.append(None)
|
||||
|
||||
ic_all = 0.0
|
||||
skip_n = 0
|
||||
for start_i, end_i in zip(diff_point, diff_point[1:]):
|
||||
pred_focus = pred[start_i:end_i] # TODO: just for fake
|
||||
if pred_focus.shape[0] < skip_size:
|
||||
# skip some days which have very small amount of stock.
|
||||
skip_n += 1
|
||||
continue
|
||||
y_focus = y[start_i:end_i]
|
||||
ic_day = torch.dot(
|
||||
(pred_focus - pred_focus.mean()) / np.sqrt(pred_focus.shape[0]) / pred_focus.std(),
|
||||
(y_focus - y_focus.mean()) / np.sqrt(y_focus.shape[0]) / y_focus.std(),
|
||||
)
|
||||
ic_all += ic_day
|
||||
if len(diff_point) - 1 - skip_n <= 0:
|
||||
raise ValueError("No enough data for calculating iC")
|
||||
ic_mean = ic_all / (len(diff_point) - 1 - skip_n)
|
||||
return -ic_mean # ic loss
|
||||
|
||||
|
||||
def preds_to_weight_with_clamp(preds, clip_weight=None, clip_method="tanh"):
|
||||
"""
|
||||
Clip the weights.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
clip_weight: float
|
||||
The clip threshold.
|
||||
clip_method: str
|
||||
The clip method. Current available: "clamp", "tanh", and "sigmoid".
|
||||
"""
|
||||
if clip_weight is not None:
|
||||
if clip_method == "clamp":
|
||||
weights = torch.exp(preds)
|
||||
weights = weights.clamp(1.0 / clip_weight, clip_weight)
|
||||
elif clip_method == "tanh":
|
||||
weights = torch.exp(torch.tanh(preds) * np.log(clip_weight))
|
||||
elif clip_method == "sigmoid":
|
||||
# intuitively assume its sum is 1
|
||||
if clip_weight == 0.0:
|
||||
weights = torch.ones_like(preds)
|
||||
else:
|
||||
sm = nn.Sigmoid()
|
||||
weights = sm(preds) * clip_weight # TODO: The clip_weight is useless here.
|
||||
weights = weights / torch.sum(weights) * weights.numel()
|
||||
else:
|
||||
raise ValueError("Unknown clip_method")
|
||||
else:
|
||||
weights = torch.exp(preds)
|
||||
return weights
|
||||
|
||||
|
||||
class SingleMetaBase(nn.Module):
|
||||
def __init__(self, hist_n, clip_weight=None, clip_method="clamp"):
|
||||
# method can be tanh or clamp
|
||||
super().__init__()
|
||||
self.clip_weight = clip_weight
|
||||
if clip_method in ["tanh", "clamp"]:
|
||||
if self.clip_weight is not None and self.clip_weight < 1.0:
|
||||
self.clip_weight = 1 / self.clip_weight
|
||||
self.clip_method = clip_method
|
||||
|
||||
def is_enabled(self):
|
||||
if self.clip_weight is None:
|
||||
return True
|
||||
if self.clip_method == "sigmoid":
|
||||
if self.clip_weight > 0.0:
|
||||
return True
|
||||
else:
|
||||
if self.clip_weight > 1.0:
|
||||
return True
|
||||
return False
|
||||
@@ -11,6 +11,7 @@ from ...model.base import Model
|
||||
from ...data.dataset import DatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.interpret.base import FeatureInt
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class CatBoostModel(Model, FeatureInt):
|
||||
@@ -31,6 +32,7 @@ class CatBoostModel(Model, FeatureInt):
|
||||
early_stopping_rounds=50,
|
||||
verbose_eval=20,
|
||||
evals_result=dict(),
|
||||
reweighter=None,
|
||||
**kwargs
|
||||
):
|
||||
df_train, df_valid = dataset.prepare(
|
||||
@@ -49,8 +51,17 @@ class CatBoostModel(Model, FeatureInt):
|
||||
else:
|
||||
raise ValueError("CatBoost doesn't support multi-label training")
|
||||
|
||||
train_pool = Pool(data=x_train, label=y_train_1d)
|
||||
valid_pool = Pool(data=x_valid, label=y_valid_1d)
|
||||
if reweighter is None:
|
||||
w_train = None
|
||||
w_valid = None
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
w_train = reweighter.reweight(df_train).values
|
||||
w_valid = reweighter.reweight(df_valid).values
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
train_pool = Pool(data=x_train, label=y_train_1d, weight=w_train)
|
||||
valid_pool = Pool(data=x_valid, label=y_valid_1d, weight=w_valid)
|
||||
|
||||
# Initialize the catboost model
|
||||
self._params["iterations"] = num_boost_round
|
||||
|
||||
@@ -4,59 +4,73 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import lightgbm as lgb
|
||||
from typing import Text, Union
|
||||
from typing import List, Text, Tuple, Union
|
||||
from ...model.base import ModelFT
|
||||
from ...data.dataset import DatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.interpret.base import LightGBMFInt
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class LGBModel(ModelFT, LightGBMFInt):
|
||||
"""LightGBM Model"""
|
||||
|
||||
def __init__(self, loss="mse", early_stopping_rounds=50, **kwargs):
|
||||
def __init__(self, loss="mse", early_stopping_rounds=50, num_boost_round=1000, **kwargs):
|
||||
if loss not in {"mse", "binary"}:
|
||||
raise NotImplementedError
|
||||
self.params = {"objective": loss, "verbosity": -1}
|
||||
self.params.update(kwargs)
|
||||
self.early_stopping_rounds = early_stopping_rounds
|
||||
self.num_boost_round = num_boost_round
|
||||
self.model = None
|
||||
|
||||
def _prepare_data(self, dataset: DatasetH):
|
||||
df_train, df_valid = dataset.prepare(
|
||||
["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L
|
||||
)
|
||||
if df_train.empty or df_valid.empty:
|
||||
raise ValueError("Empty data from dataset, please check your dataset config.")
|
||||
x_train, y_train = df_train["feature"], df_train["label"]
|
||||
x_valid, y_valid = df_valid["feature"], df_valid["label"]
|
||||
def _prepare_data(self, dataset: DatasetH, reweighter=None) -> List[Tuple[lgb.Dataset, str]]:
|
||||
"""
|
||||
The motivation of current version is to make validation optional
|
||||
- train segment is necessary;
|
||||
"""
|
||||
ds_l = []
|
||||
assert "train" in dataset.segments
|
||||
for key in ["train", "valid"]:
|
||||
if key in dataset.segments:
|
||||
df = dataset.prepare(key, col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
if df.empty:
|
||||
raise ValueError("Empty data from dataset, please check your dataset config.")
|
||||
x, y = df["feature"], df["label"]
|
||||
|
||||
# Lightgbm need 1D array as its label
|
||||
if y_train.values.ndim == 2 and y_train.values.shape[1] == 1:
|
||||
y_train, y_valid = np.squeeze(y_train.values), np.squeeze(y_valid.values)
|
||||
else:
|
||||
raise ValueError("LightGBM doesn't support multi-label training")
|
||||
# Lightgbm need 1D array as its label
|
||||
if y.values.ndim == 2 and y.values.shape[1] == 1:
|
||||
y = np.squeeze(y.values)
|
||||
else:
|
||||
raise ValueError("LightGBM doesn't support multi-label training")
|
||||
|
||||
dtrain = lgb.Dataset(x_train, label=y_train)
|
||||
dvalid = lgb.Dataset(x_valid, label=y_valid)
|
||||
return dtrain, dvalid
|
||||
if reweighter is None:
|
||||
w = None
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
w = reweighter.reweight(df)
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
ds_l.append((lgb.Dataset(x.values, label=y, weight=w), key))
|
||||
return ds_l
|
||||
|
||||
def fit(
|
||||
self,
|
||||
dataset: DatasetH,
|
||||
num_boost_round=1000,
|
||||
num_boost_round=None,
|
||||
early_stopping_rounds=None,
|
||||
verbose_eval=20,
|
||||
evals_result=dict(),
|
||||
reweighter=None,
|
||||
**kwargs
|
||||
):
|
||||
dtrain, dvalid = self._prepare_data(dataset)
|
||||
ds_l = self._prepare_data(dataset, reweighter)
|
||||
ds, names = list(zip(*ds_l))
|
||||
self.model = lgb.train(
|
||||
self.params,
|
||||
dtrain,
|
||||
num_boost_round=num_boost_round,
|
||||
valid_sets=[dtrain, dvalid],
|
||||
valid_names=["train", "valid"],
|
||||
ds[0], # training dataset
|
||||
num_boost_round=self.num_boost_round if num_boost_round is None else num_boost_round,
|
||||
valid_sets=ds,
|
||||
valid_names=names,
|
||||
early_stopping_rounds=(
|
||||
self.early_stopping_rounds if early_stopping_rounds is None else early_stopping_rounds
|
||||
),
|
||||
@@ -64,8 +78,8 @@ class LGBModel(ModelFT, LightGBMFInt):
|
||||
evals_result=evals_result,
|
||||
**kwargs
|
||||
)
|
||||
evals_result["train"] = list(evals_result["train"].values())[0]
|
||||
evals_result["valid"] = list(evals_result["valid"].values())[0]
|
||||
for k in names:
|
||||
evals_result[k] = list(evals_result[k].values())[0]
|
||||
|
||||
def predict(self, dataset: DatasetH, segment: Union[Text, slice] = "test"):
|
||||
if self.model is None:
|
||||
@@ -73,7 +87,7 @@ class LGBModel(ModelFT, LightGBMFInt):
|
||||
x_test = dataset.prepare(segment, col_set="feature", data_key=DataHandlerLP.DK_I)
|
||||
return pd.Series(self.model.predict(x_test.values), index=x_test.index)
|
||||
|
||||
def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20):
|
||||
def finetune(self, dataset: DatasetH, num_boost_round=10, verbose_eval=20, reweighter=None):
|
||||
"""
|
||||
finetune model
|
||||
|
||||
@@ -87,7 +101,7 @@ class LGBModel(ModelFT, LightGBMFInt):
|
||||
verbose level
|
||||
"""
|
||||
# Based on existing model and finetune by train more rounds
|
||||
dtrain, _ = self._prepare_data(dataset)
|
||||
dtrain, _ = self._prepare_data(dataset, reweighter)
|
||||
if dtrain.empty:
|
||||
raise ValueError("Empty data from dataset, please check your dataset config.")
|
||||
self.model = lgb.train(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Text, Union
|
||||
from qlib.data.dataset.weight import Reweighter
|
||||
from scipy.optimize import nnls
|
||||
from sklearn.linear_model import LinearRegression, Ridge, Lasso
|
||||
|
||||
@@ -49,33 +50,40 @@ class LinearModel(Model):
|
||||
|
||||
self.coef_ = None
|
||||
|
||||
def fit(self, dataset: DatasetH):
|
||||
def fit(self, dataset: DatasetH, reweighter: Reweighter = None):
|
||||
df_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
if df_train.empty:
|
||||
raise ValueError("Empty data from dataset, please check your dataset config.")
|
||||
if reweighter is not None:
|
||||
w: pd.Series = reweighter.reweight(df_train)
|
||||
w = w.values
|
||||
else:
|
||||
w = None
|
||||
X, y = df_train["feature"].values, np.squeeze(df_train["label"].values)
|
||||
|
||||
if self.estimator in [self.OLS, self.RIDGE, self.LASSO]:
|
||||
self._fit(X, y)
|
||||
self._fit(X, y, w)
|
||||
elif self.estimator == self.NNLS:
|
||||
self._fit_nnls(X, y)
|
||||
self._fit_nnls(X, y, w)
|
||||
else:
|
||||
raise ValueError(f"unknown estimator `{self.estimator}`")
|
||||
|
||||
return self
|
||||
|
||||
def _fit(self, X, y):
|
||||
def _fit(self, X, y, w):
|
||||
if self.estimator == self.OLS:
|
||||
model = LinearRegression(fit_intercept=self.fit_intercept, copy_X=False)
|
||||
else:
|
||||
model = {self.RIDGE: Ridge, self.LASSO: Lasso}[self.estimator](
|
||||
alpha=self.alpha, fit_intercept=self.fit_intercept, copy_X=False
|
||||
)
|
||||
model.fit(X, y)
|
||||
model.fit(X, y, sample_weight=w)
|
||||
self.coef_ = model.coef_
|
||||
self.intercept_ = model.intercept_
|
||||
|
||||
def _fit_nnls(self, X, y):
|
||||
def _fit_nnls(self, X, y, w=None):
|
||||
if w is not None:
|
||||
raise NotImplementedError("TODO: support nnls with weight") # TODO
|
||||
if self.fit_intercept:
|
||||
X = np.c_[X, np.ones(len(X))] # NOTE: mem copy
|
||||
coef = nnls(X, y)[0]
|
||||
|
||||
@@ -22,6 +22,8 @@ from .pytorch_utils import count_parameters
|
||||
from ...model.base import Model
|
||||
from ...data.dataset import DatasetH, TSDatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.utils import ConcatDataset
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class ALSTM(Model):
|
||||
@@ -139,15 +141,18 @@ class ALSTM(Model):
|
||||
def use_gpu(self):
|
||||
return self.device != torch.device("cpu")
|
||||
|
||||
def mse(self, pred, label):
|
||||
loss = (pred - label) ** 2
|
||||
def mse(self, pred, label, weight):
|
||||
loss = weight * (pred - label) ** 2
|
||||
return torch.mean(loss)
|
||||
|
||||
def loss_fn(self, pred, label):
|
||||
def loss_fn(self, pred, label, weight=None):
|
||||
mask = ~torch.isnan(label)
|
||||
|
||||
if weight is None:
|
||||
weight = torch.ones_like(label)
|
||||
|
||||
if self.loss == "mse":
|
||||
return self.mse(pred[mask], label[mask])
|
||||
return self.mse(pred[mask], label[mask], weight[mask])
|
||||
|
||||
raise ValueError("unknown loss `%s`" % self.loss)
|
||||
|
||||
@@ -164,12 +169,12 @@ class ALSTM(Model):
|
||||
|
||||
self.ALSTM_model.train()
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
label = data[:, -1, -1].to(self.device)
|
||||
|
||||
pred = self.ALSTM_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
|
||||
self.train_optimizer.zero_grad()
|
||||
loss.backward()
|
||||
@@ -183,7 +188,7 @@ class ALSTM(Model):
|
||||
scores = []
|
||||
losses = []
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
# feature[torch.isnan(feature)] = 0
|
||||
@@ -191,7 +196,7 @@ class ALSTM(Model):
|
||||
|
||||
with torch.no_grad():
|
||||
pred = self.ALSTM_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
losses.append(loss.item())
|
||||
|
||||
score = self.metric_fn(pred, label)
|
||||
@@ -204,6 +209,7 @@ class ALSTM(Model):
|
||||
dataset,
|
||||
evals_result=dict(),
|
||||
save_path=None,
|
||||
reweighter=None,
|
||||
):
|
||||
dl_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
dl_valid = dataset.prepare("valid", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
@@ -213,11 +219,28 @@ class ALSTM(Model):
|
||||
dl_train.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
dl_valid.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
|
||||
if reweighter is None:
|
||||
wl_train = np.ones(len(dl_train))
|
||||
wl_valid = np.ones(len(dl_valid))
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
wl_train = reweighter.reweight(dl_train)
|
||||
wl_valid = reweighter.reweight(dl_valid)
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
train_loader = DataLoader(
|
||||
dl_train, batch_size=self.batch_size, shuffle=True, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_train, wl_train),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=True,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
valid_loader = DataLoader(
|
||||
dl_valid, batch_size=self.batch_size, shuffle=False, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_valid, wl_valid),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=False,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
|
||||
save_path = get_or_create_path(save_path)
|
||||
|
||||
@@ -21,6 +21,8 @@ from .pytorch_utils import count_parameters
|
||||
from ...model.base import Model
|
||||
from ...data.dataset import DatasetH, TSDatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.utils import ConcatDataset
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class GRU(Model):
|
||||
@@ -138,15 +140,18 @@ class GRU(Model):
|
||||
def use_gpu(self):
|
||||
return self.device != torch.device("cpu")
|
||||
|
||||
def mse(self, pred, label):
|
||||
loss = (pred - label) ** 2
|
||||
def mse(self, pred, label, weight):
|
||||
loss = weight * (pred - label) ** 2
|
||||
return torch.mean(loss)
|
||||
|
||||
def loss_fn(self, pred, label):
|
||||
def loss_fn(self, pred, label, weight=None):
|
||||
mask = ~torch.isnan(label)
|
||||
|
||||
if weight is None:
|
||||
weight = torch.ones_like(label)
|
||||
|
||||
if self.loss == "mse":
|
||||
return self.mse(pred[mask], label[mask])
|
||||
return self.mse(pred[mask], label[mask], weight[mask])
|
||||
|
||||
raise ValueError("unknown loss `%s`" % self.loss)
|
||||
|
||||
@@ -163,12 +168,12 @@ class GRU(Model):
|
||||
|
||||
self.GRU_model.train()
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
label = data[:, -1, -1].to(self.device)
|
||||
|
||||
pred = self.GRU_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
|
||||
self.train_optimizer.zero_grad()
|
||||
loss.backward()
|
||||
@@ -182,7 +187,7 @@ class GRU(Model):
|
||||
scores = []
|
||||
losses = []
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
# feature[torch.isnan(feature)] = 0
|
||||
@@ -190,7 +195,7 @@ class GRU(Model):
|
||||
|
||||
with torch.no_grad():
|
||||
pred = self.GRU_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
losses.append(loss.item())
|
||||
|
||||
score = self.metric_fn(pred, label)
|
||||
@@ -203,6 +208,7 @@ class GRU(Model):
|
||||
dataset,
|
||||
evals_result=dict(),
|
||||
save_path=None,
|
||||
reweighter=None,
|
||||
):
|
||||
dl_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
dl_valid = dataset.prepare("valid", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
@@ -212,11 +218,28 @@ class GRU(Model):
|
||||
dl_train.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
dl_valid.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
|
||||
if reweighter is None:
|
||||
wl_train = np.ones(len(dl_train))
|
||||
wl_valid = np.ones(len(dl_valid))
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
wl_train = reweighter.reweight(dl_train)
|
||||
wl_valid = reweighter.reweight(dl_valid)
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
train_loader = DataLoader(
|
||||
dl_train, batch_size=self.batch_size, shuffle=True, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_train, wl_train),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=True,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
valid_loader = DataLoader(
|
||||
dl_valid, batch_size=self.batch_size, shuffle=False, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_valid, wl_valid),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=False,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
|
||||
save_path = get_or_create_path(save_path)
|
||||
|
||||
@@ -20,6 +20,8 @@ from torch.utils.data import DataLoader
|
||||
from ...model.base import Model
|
||||
from ...data.dataset import DatasetH, TSDatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.utils import ConcatDataset
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class LSTM(Model):
|
||||
@@ -134,15 +136,18 @@ class LSTM(Model):
|
||||
def use_gpu(self):
|
||||
return self.device != torch.device("cpu")
|
||||
|
||||
def mse(self, pred, label):
|
||||
loss = (pred - label) ** 2
|
||||
def mse(self, pred, label, weight):
|
||||
loss = weight * (pred - label) ** 2
|
||||
return torch.mean(loss)
|
||||
|
||||
def loss_fn(self, pred, label):
|
||||
mask = ~torch.isnan(label)
|
||||
|
||||
if weight is None:
|
||||
weight = torch.ones_like(label)
|
||||
|
||||
if self.loss == "mse":
|
||||
return self.mse(pred[mask], label[mask])
|
||||
return self.mse(pred[mask], label[mask], weight[mask])
|
||||
|
||||
raise ValueError("unknown loss `%s`" % self.loss)
|
||||
|
||||
@@ -159,12 +164,12 @@ class LSTM(Model):
|
||||
|
||||
self.LSTM_model.train()
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
label = data[:, -1, -1].to(self.device)
|
||||
|
||||
pred = self.LSTM_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
|
||||
self.train_optimizer.zero_grad()
|
||||
loss.backward()
|
||||
@@ -178,14 +183,14 @@ class LSTM(Model):
|
||||
scores = []
|
||||
losses = []
|
||||
|
||||
for data in data_loader:
|
||||
for (data, weight) in data_loader:
|
||||
|
||||
feature = data[:, :, 0:-1].to(self.device)
|
||||
# feature[torch.isnan(feature)] = 0
|
||||
label = data[:, -1, -1].to(self.device)
|
||||
|
||||
pred = self.LSTM_model(feature.float())
|
||||
loss = self.loss_fn(pred, label)
|
||||
loss = self.loss_fn(pred, label, weight.to(self.device))
|
||||
losses.append(loss.item())
|
||||
|
||||
score = self.metric_fn(pred, label)
|
||||
@@ -198,6 +203,7 @@ class LSTM(Model):
|
||||
dataset,
|
||||
evals_result=dict(),
|
||||
save_path=None,
|
||||
reweighter=None,
|
||||
):
|
||||
dl_train = dataset.prepare("train", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
dl_valid = dataset.prepare("valid", col_set=["feature", "label"], data_key=DataHandlerLP.DK_L)
|
||||
@@ -207,11 +213,28 @@ class LSTM(Model):
|
||||
dl_train.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
dl_valid.config(fillna_type="ffill+bfill") # process nan brought by dataloader
|
||||
|
||||
if reweighter is None:
|
||||
wl_train = np.ones(len(dl_train))
|
||||
wl_valid = np.ones(len(dl_valid))
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
wl_train = reweighter.reweight(dl_train)
|
||||
wl_valid = reweighter.reweight(dl_valid)
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
train_loader = DataLoader(
|
||||
dl_train, batch_size=self.batch_size, shuffle=True, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_train, wl_train),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=True,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
valid_loader = DataLoader(
|
||||
dl_valid, batch_size=self.batch_size, shuffle=False, num_workers=self.n_jobs, drop_last=True
|
||||
ConcatDataset(dl_valid, wl_valid),
|
||||
batch_size=self.batch_size,
|
||||
shuffle=False,
|
||||
num_workers=self.n_jobs,
|
||||
drop_last=True,
|
||||
)
|
||||
|
||||
save_path = get_or_create_path(save_path)
|
||||
|
||||
@@ -19,6 +19,7 @@ from .pytorch_utils import count_parameters
|
||||
from ...model.base import Model
|
||||
from ...data.dataset import DatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...data.dataset.weight import Reweighter
|
||||
from ...utils import unpack_archive_with_buffer, save_multiple_parts_file, get_or_create_path
|
||||
from ...log import get_module_logger
|
||||
from ...workflow import R
|
||||
@@ -166,18 +167,22 @@ class DNNModelPytorch(Model):
|
||||
evals_result=dict(),
|
||||
verbose=True,
|
||||
save_path=None,
|
||||
reweighter=None,
|
||||
):
|
||||
df_train, df_valid = dataset.prepare(
|
||||
["train", "valid"], col_set=["feature", "label"], data_key=DataHandlerLP.DK_L
|
||||
)
|
||||
x_train, y_train = df_train["feature"], df_train["label"]
|
||||
x_valid, y_valid = df_valid["feature"], df_valid["label"]
|
||||
try:
|
||||
wdf_train, wdf_valid = dataset.prepare(["train", "valid"], col_set=["weight"], data_key=DataHandlerLP.DK_L)
|
||||
w_train, w_valid = wdf_train["weight"], wdf_valid["weight"]
|
||||
except KeyError as e:
|
||||
|
||||
if reweighter is None:
|
||||
w_train = pd.DataFrame(np.ones_like(y_train.values), index=y_train.index)
|
||||
w_valid = pd.DataFrame(np.ones_like(y_valid.values), index=y_valid.index)
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
w_train = pd.DataFrame(reweighter.reweight(df_train))
|
||||
w_valid = pd.DataFrame(reweighter.reweight(df_valid))
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
save_path = get_or_create_path(save_path)
|
||||
stop_steps = 0
|
||||
|
||||
@@ -9,6 +9,7 @@ from ...model.base import Model
|
||||
from ...data.dataset import DatasetH
|
||||
from ...data.dataset.handler import DataHandlerLP
|
||||
from ...model.interpret.base import FeatureInt
|
||||
from ...data.dataset.weight import Reweighter
|
||||
|
||||
|
||||
class XGBModel(Model, FeatureInt):
|
||||
@@ -26,6 +27,7 @@ class XGBModel(Model, FeatureInt):
|
||||
early_stopping_rounds=50,
|
||||
verbose_eval=20,
|
||||
evals_result=dict(),
|
||||
reweighter=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
@@ -43,8 +45,17 @@ class XGBModel(Model, FeatureInt):
|
||||
else:
|
||||
raise ValueError("XGBoost doesn't support multi-label training")
|
||||
|
||||
dtrain = xgb.DMatrix(x_train, label=y_train_1d)
|
||||
dvalid = xgb.DMatrix(x_valid, label=y_valid_1d)
|
||||
if reweighter is None:
|
||||
w_train = None
|
||||
w_valid = None
|
||||
elif isinstance(reweighter, Reweighter):
|
||||
w_train = reweighter.reweight(df_train)
|
||||
w_valid = reweighter.reweight(df_valid)
|
||||
else:
|
||||
raise ValueError("Unsupported reweighter type.")
|
||||
|
||||
dtrain = xgb.DMatrix(x_train.values, label=y_train_1d, weight=w_train)
|
||||
dvalid = xgb.DMatrix(x_valid.values, label=y_valid_1d, weight=w_valid)
|
||||
self.model = xgb.train(
|
||||
self._params,
|
||||
dtrain=dtrain,
|
||||
|
||||
@@ -124,6 +124,10 @@ class TopkDropoutStrategy(BaseSignalStrategy):
|
||||
trade_start_time, trade_end_time = self.trade_calendar.get_step_time(trade_step)
|
||||
pred_start_time, pred_end_time = self.trade_calendar.get_step_time(trade_step, shift=1)
|
||||
pred_score = self.signal.get_signal(start_time=pred_start_time, end_time=pred_end_time)
|
||||
# NOTE: the current version of topk dropout strategy can't handle pd.DataFrame(multiple signal)
|
||||
# So it only leverage the first col of signal
|
||||
if isinstance(pred_score, pd.DataFrame):
|
||||
pred_score = pred_score.iloc[:, 0]
|
||||
if pred_score is None:
|
||||
return TradeDecisionWO([], self)
|
||||
if self.only_tradable:
|
||||
|
||||
31
qlib/contrib/torch.py
Normal file
31
qlib/contrib/torch.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
"""
|
||||
This module is not a necessary part of Qlib.
|
||||
They are just some tools for convenience
|
||||
It is should not imported into the core part of qlib
|
||||
"""
|
||||
import torch
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def data_to_tensor(data, device="cpu", raise_error=False):
|
||||
if isinstance(data, torch.Tensor):
|
||||
if device == "cpu":
|
||||
return data.cpu()
|
||||
else:
|
||||
return data.to(device)
|
||||
if isinstance(data, (pd.DataFrame, pd.Series)):
|
||||
return data_to_tensor(torch.from_numpy(data.values).float(), device)
|
||||
elif isinstance(data, np.ndarray):
|
||||
return data_to_tensor(torch.from_numpy(data).float(), device)
|
||||
elif isinstance(data, (tuple, list)):
|
||||
return [data_to_tensor(i, device) for i in data]
|
||||
elif isinstance(data, dict):
|
||||
return {k: data_to_tensor(v, device) for k, v in data.items()}
|
||||
else:
|
||||
if raise_error:
|
||||
raise ValueError(f"Unsupported data type: {type(data)}.")
|
||||
else:
|
||||
return data
|
||||
Reference in New Issue
Block a user