深度学习模型有能力自动发现变量之间的关系,而这些关系通常是不可见的,这使得深度学习可以挖掘新的因子和规律,为量化投资策略提供更多可能性。在传统的量化策略开发流程中,通常会使用 Python 或第三方工具生成因子,并将其存储为文件。这些因子是构建深度学习模型的基础输入,包括技术指标、波动性指标和市场情绪指标等。随着证券交易规模不断扩大以及因子数据量的激增。传统的文件存储因子作为深度学习模型的输入,面临以下问题:
为应对这些挑战,DolphinDB将数据库与深度学习相结合,开发了 AI Dataloader(为了便于描述,下文使用DDBDataLoader这一更贴近功能实现的类名指代)。该工具用于因子数据的管理和深度学习模型的集成,旨在提高因子数据的效率和管理,并简化与深度学习模型的交互。
主要包括以下功能模块:
具体内部详细工作流程将在下一节 工作原理 中详细介绍。
在构造时,DDBDataLoader 接收用户提供的 SQL 查询语句,并将其拆分为多个子查询组。DDBDataLoader 使用后台线程从服务端获取数据,并将其处理为 PyTorch Tensor 格式的批量数据。
DDBDataLoader 提供了 groupCol 和 groupScheme 参数,用于将单个查询 SQL 分成多组查询。其中,每一个组定义了一个时间序列,例如一支股票的交易数据。若不指定,则认为所有的数据定义了一个时间序列。例如,一种典型的情况是表里的数据包含了全部的股票,而我们在训练模型的时候只希望每支股票仅利用自己的历史数据来对未来进行预测。在这种情况下,我们需要将 groupCol 设置为股票标的列,将 groupScheme 设置为所有的股票标的,也即每一个组是一支股票的交易数据。换言之,假如原始的查询 SQL 为:
select * from loadTable(dbName, tableName)
假设 groupScheme 为 stockID, groupScheme 为 ["apple", "google", "amazon"],则原始的查询会被拆分成以下三组查询:
select * from loadTable(dbName, tableName) where stockID = "apple"
select * from loadTable(dbName, tableName) where stockID = "google"
select * from loadTable(dbName, tableName) where stockID = "amazon"
模型训练的时候,每组训练数据则只会来源于以上三组中的某一组,而不会出现跨组的情况。
另一方面,为了避免加载大量数据至内存中,DDBDataLoader 采用分区粒度管理数据。也就是说,每组查询每次只加载一个分区到内存中。通常来说, DolphinDB 的一个分区的大小在 100MB 到 1GB 之间,因此这种设计可以很好地限制内存的使用量。另一方面,基于这种分区粒度的管理方式,使得 DDBDataLoader 实现的 shuffle 和传统的 dataloader 实现的 shuffle 会有一些区别。具体地说,传统的 shuffle 是将数据进行完全随机地打乱,但这样会引入大量的随机 IO,会使得效率偏低。而 DDBDataLoader 使用的 shuffle 方式,则是先随机地选取一个数据的分区并读取,随后在这个分区内部进行 shuffle。使用这种方式,可以最大化地减少随机的 IO,以提升整个训练过程的效率。
另一组要说明的参数是 repartitionCol 和 repartitionScheme 。这组参数主要处理的是单个查询没办法直接拆成多个分区的情况。例如,一种常见的使用DolphinDB管理因子数据的方法是使用纵表来对因子数据进行管理(详见 best_practices_for_multi_factor.md)。使用这种方法,则在获取可用的训练数据之前,需要先对所有的数据做一个 pivot by 将数据从纵表转化为宽表。然而,用户的数据量可能非常大,例如几百 G 甚至更多,在这种情况下,服务器的内存完全可能不够完成对所有数据的 pivot by 。针对这类情况,DDBDataLoader 提供了 repartitionCol 和repartitionScheme 参数这组参数,这组参数的作用是可以对全表的数据做进一步的切分,将全表数据按照 repartitionCol 和 repartitionScheme 拆成多个子表,然后对于每一个子表再做单独的 pivot by。换言之,假设原始的查询 SQL 为:
select val from loadTable(dbName, tableName) pivot by datetime, stockID, factorName
假设 repartitionCol 为 date(datetime),repartitionScheme 为 ["2023.09.05", "2023.09.06", "2023.09.07"],则上述 的查询相当于会被拆成以下几个子查询:
select val from loadTable(dbName, tableName) pivot by datetime, stockID, factorName where date(datetime) = 2023.09.05
select val from loadTable(dbName, tableName) pivot by datetime, stockID, factorName where date(datetime) = 2023.09.06
select val from loadTable(dbName, tableName) pivot by datetime, stockID, factorName where date(datetime) = 2023.09.07
可以认为,设置了 repartitionCol 和 repartitionScheme 参数之后,相当于对上述的 pivot by 查询语句的结果又做了一个“重分区”,使得每个分区占用的空间不至于特别大。
下面介绍 DDBDataLoader 的各个组件。
在 DDBDataLoader 内部,每组子查询由数据管理器 DataManager 管理,每个 DataManager 又对应一个 DataSource。这里的 DataSource,可视为一个分区的元数据。DataSource 通过传入的 Session 会话从 DolphinDB 服务端获取一个分区的数据,并将该分区的数据放入一个预载队列中。DataManager 则根据选取数据的顺序从 DataSource 产生的预载队列中获取预载的分区粒度数据,并将其根据滑动窗口大小和步长处理为相应的 PyTorch 的 Tensor 格式。
DDBDataLoader 维护了一个包含多个数据管理器 DataManager 的数据池,数据池的大小由参数 groupPoolSize 控制。后台工作线程从这些数据管理器中提取批量数据,并将其组装成用于训练的数据格式和形状,然后放入整个 AI Dataloader 的预准备队列中。最后,迭代时,DDBDataLoader 从预准备队列中获取已准备好的批量数据,将其传递给客户端,供神经网络训练使用。
DolphinDB Python API 自 1.30.22.2 版本起提供深度学习工具类 DDBDataLoader,提供对 DolphinDB SQL 对应的数据集进行批量拆分和重新洗牌的易用接口,将 DolphinDB 中的数据直接对接到 PyTorch 中。
pip install dolphindb-tools
期待输出:
DolphinDB 类型 |
Tensor 类型 |
BOOL [不含空值] |
torch.bool |
CHAR [不含空值] |
torch.int8 |
SHORT [不含空值] |
torch.int16 |
INT [不含空值] |
torch.int32 |
LONG [不含空值] |
torch.int64 |
FLOAT |
torch.float32 |
DOUBLE |
torch.float64 |
CHAR/SHORT/INT/LONG [包含空值] |
torch.float64 |
注意:
提供 DDBDataLoader 类来加载和访问数据,接口如下:
DDBDataLoader(
ddbSession: Session,
sql: str,
targetCol: List[str],
batchSize: int = 1,
shuffle: bool = True,
windowSize: Union[List[int], int, None] = None,
windowStride: Union[List[int], int, None] = None,
*,
inputCol: Optional[List[str]] = None,
excludeCol: Optional[List[str]] = None,
repartitionCol: str = None,
repartitionScheme: List[str] = None,
groupCol: str = None,
groupScheme: List[str] = None,
seed: Optional[int] = None,
dropLast: bool = False,
offset: int = None,
device: str = "cpu",
prefetchBatch: int = 1,
prepartitionNum: int = 2,
groupPoolSize: int = 3,
**kwargs
)
注意:
1. 其中 repartitionCol 和 repartitionScheme 功能可用于解决单个分区数据较多,无法直接进行全量运算的情况。通过将数据根据 repartitionScheme 的值进行筛选,可以将数据分割成多个子分区,每个子分区将按照 repartitionScheme 中的顺序排列。例如,如果 repartitionCol 为 date(TradeTime), repartitionScheme 为 ["2020.01.01", "2020.01.02", "2020.01.03"],则数据将被细分为三个分区,每个分区对应一个日期值。
2. 不同于 repartitionCol/repartitionScheme,其中 groupCol 和 groupScheme 的分组之间不会出现跨分组的数据,例如,如果 groupCol 为 Code,groupScheme 为 [“`000001.SH”, “`000002.SH”, “`000003.SH”],则数据将被划分为三个不相交的分组,每个分组对应一个股票代码。
下面将提供一个简单供您快速体验 DDBDataLoader 使用,示例代码如下:
import dolphindb as ddb
from dolphindb_tools.dataloader import DDBDataLoader
sess = ddb.Session()
sess.connect("localhost", 8848, "admin", "123456")
sess.run("""
t = table(1..10 as a, 2..11 as b, 3..12 as c)
""")
sql = 'select * from objByName("t")'
dataloader = DDBDataLoader(sess, sql, ["c"])
for X, y in dataloader:
print(X, y)
--------------------------------------------------
tensor([[1, 2, 3]], dtype=torch.int32) tensor([[3]], dtype=torch.int32)
tensor([[2, 3, 4]], dtype=torch.int32) tensor([[4]], dtype=torch.int32)
tensor([[3, 4, 5]], dtype=torch.int32) tensor([[5]], dtype=torch.int32)
tensor([[4, 5, 6]], dtype=torch.int32) tensor([[6]], dtype=torch.int32)
tensor([[5, 6, 7]], dtype=torch.int32) tensor([[7]], dtype=torch.int32)
tensor([[6, 7, 8]], dtype=torch.int32) tensor([[8]], dtype=torch.int32)
tensor([[7, 8, 9]], dtype=torch.int32) tensor([[9]], dtype=torch.int32)
tensor([[ 8, 9, 10]], dtype=torch.int32) tensor([[10]], dtype=torch.int32)
tensor([[ 9, 10, 11]], dtype=torch.int32) tensor([[11]], dtype=torch.int32)
tensor([[10, 11, 12]], dtype=torch.int32) tensor([[12]], dtype=torch.int32)
在这个示例中,您使用一个内存表作为待加载的训练数据,并定义了 targetCol=["c"]。这表示将使用同一行的 "a", "b", "c" 三列作为训练的输入数据 X,并将 "c" 列作为训练的目标数据 y。
如果您指定了offset=5,那么每一份数据都将使用某一行的 "a", "b", "c" 作为训练输入数据 X,并且使用距离当前行5行之后的 "c" 列数据作为训练的目标数据 y。
dataloader = DDBDataLoader(sess, sql, ["c"], offset=5)
for X, y in dataloader:
print(X, y)
输出如下:
tensor([[1, 2, 3]], dtype=torch.int32) tensor([[8]], dtype=torch.int32)
tensor([[2, 3, 4]], dtype=torch.int32) tensor([[9]], dtype=torch.int32)
tensor([[3, 4, 5]], dtype=torch.int32) tensor([[10]], dtype=torch.int32)
tensor([[4, 5, 6]], dtype=torch.int32) tensor([[11]], dtype=torch.int32)
tensor([[5, 6, 7]], dtype=torch.int32) tensor([[12]], dtype=torch.int32)
如果指定滑动窗口大小为 3,步长为 1,同时不输入 offset,则表示每份数据使用前三行的 “a“, “b“, “c“ 三列的数据和后一行的 “c“ 列数据,示例如下:
dataloader = DDBDataLoader(sess, sql, ["c"], windowSize=3, windowStride=1)
for X, y in dataloader:
print(X, y)
输出如下:
tensor([[[1, 2, 3],
[2, 3, 4],
[3, 4, 5]]], dtype=torch.int32) tensor([[[6]]], dtype=torch.int32)
tensor([[[2, 3, 4],
[3, 4, 5],
[4, 5, 6]]], dtype=torch.int32) tensor([[[7]]], dtype=torch.int32)
tensor([[[3, 4, 5],
[4, 5, 6],
[5, 6, 7]]], dtype=torch.int32) tensor([[[8]]], dtype=torch.int32)
tensor([[[4, 5, 6],
[5, 6, 7],
[6, 7, 8]]], dtype=torch.int32) tensor([[[9]]], dtype=torch.int32)
......
在深度学习中,数据加载和处理的效率对总体训练时长有重要影响。在本节性能测试中,重点关注了传统数据加载方式 (PyTorch DataLoader) 和 DolphinDB 集成 PyTorch (DDBDataLoader) 之间的耗时差异。
测试场景:为前 200 个时间点因子数据来预测下一个时间点 f000001 因子值。
示例步骤:
性能测试分为两个部分:
在每种数据加载方式下,进行了 2000 次数据批次的迭代。通过比较两种数据加载方式的耗时差异,可以更清楚地了解 DDBDataLoader 性能优势。这种性能测试有助于评估 DDBDataLoader 在数据加载和处理方面的效率,为深度学习模型的训练提供参考和优化的方向。
对比测试功能模块代码目录结构
服务端
硬件名称 |
配置信息 |
主机名 |
HostName |
外网 IP |
xxx.xxx.xxx.218 |
操作系统 |
Linux(内核版本3.10以上) |
内存 |
500 GB |
CPU |
x86_64(64核心) |
GPU |
A100 |
网络 |
万兆以太网 |
软件名称 |
版本信息 |
DolphinDB |
2.00.10.1 |
ddbtools |
0.1a1 |
python |
3.8.17 |
dolphindb |
1.30.22.2 |
numpy,torch ,pandas |
1.24.4, 2.0.0+cu118, 1.5.2 |
使用 Python 中的第三方库 line_profiler (4.0.3),将待测试代码封装为函数后添加 @profile 装饰器,在终端执行 kernprof -l -v test.py 进行性能测试。
快照 3 秒频因子数据,生成总数据约为 277G,测试数据生成脚本如下:
在 DolphinDB 客户端执行,指定 Datetime 和 Symbol 为分区列和排序列,在数据库 dfs://test_ai_dataloader 中创建分区表 wide_factor_table。表中包含 Datetime 时间列和 Symbol 股票名称列,以及 1000 列因子列(名称从 f000001 到 f001000)。类型分别为 DATETIME 和 SYMBOL,因子列类型全部使用 DOUBLE。详细代码见工程代码中 ddb_scripts.dos,核心代码如下:
dbName = "dfs://test_ai_dataloader"
tbName = "wide_factor_table"
if (existsDatabase(dbName)) {
dropDatabase(dbName)
}
// 股票数量
numSymbols = 250
// 因子数量
numFactors = 1000
dateBegin = 2020.01.01
dateEnd = 2020.01.31
symbolList = symbol(lpad(string(1..numSymbols), 6, "0") + ".SH")
factorList = lpad(string(1..numFactors), 7,"f000000")
colNames = ["Datetime", "Symbol"] join factorList
colTypes = [DATETIME, SYMBOL] join take(DOUBLE, numFactors)
schema = table(1:0, colNames, colTypes)
写入完成后,使用下面的脚本打印 SQL 查询结果,确认已经写入成功。
select DateTime, Symbol, f000001 from loadTable("dfs://test_ai_dataloader", "wide_factor_table") where Symbol=`000001.SH, date(DateTime)=2020.01.31
示例数据如下:
在传统深度学习中,通常会采取以下步骤来处理训练数据:
首先使用 numpy 库生成二进制数据文件,此阶段耗时约为 83分钟,详细见 prepare_data.py ,核心代码如下:
st = time.time()
for symbol in symbols:
for t in times:
sql_tmp = sql + f""" where Symbol={symbol}, date(DateTime)={t}"""
data = s.run(sql_tmp, pickleTableToList=True)
data = np.array(data[2:])
data.tofile(f"datas/{symbol[1:]}-{t}.bin")
print(f"[{symbol}-{t}] LOAD OVER {data.shape}")
ed = time.time()
print("total time: ", ed-st) # 耗时约 4950s
在数据处理过程中,通常需要计算滑动窗口的大小和步长。这两个参数决定了如何从数据中切割出训练样本。滑动窗口的大小定义了每个训练样本的时间窗口长度,而步长定义了滑动窗口之间的间隔, 一旦确定了滑动窗口的大小和步长,接下来会计算每份数据需要从哪些文件中获取数据。这个计算过程通常涉及到迭代数据并根据滑动窗口的设置来确定数据的切割方式。然后,将这些索引信息保存在 index.pkl, 以供后续使用,此阶段耗时约为 4分钟。核心代码如下,详细见 prepare_index.py:
with open("index.pkl", 'wb') as f:
pickle.dump(index_list, f)
ed = time.time()
print("total time: ", ed-st) # 约 234s
最后在 Python 代码中,定义一个数据集(DataSet),用于管理和加载训练数据,将 index.pkl 内容读取至内存,使用 mmap 方式打开数据文件,使得能够通过下标访问快速将数据从文件中读取到内存,此阶段耗时约为 4 分钟,核心代码如下,详细见test_wide_old.py:
def main():
torch.set_default_device("cuda")
torch.set_default_tensor_type(torch.DoubleTensor)
model = SimpleNet()
model = model.to("cuda")
loss_fn = nn.MSELoss()
loss_fn = loss_fn.to("cuda")
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
dataset = MyDataset(4802)
dataloader = DataLoader(
dataset, batch_size=64, shuffle=False, num_workers=3,
pin_memory=True, pin_memory_device="cuda",
prefetch_factor=5,
)
epoch = 10
for _ in range(epoch):
for x, y in tqdm(dataloader, mininterval=1):
x = x.to("cuda")
y = y.to("cuda")
y_pred = model(x)
loss = loss_fn(y_pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if __name__ == "__main__":
main()
至此,基于 PytorchDataLoader 深度学习训练数据流程全部结束,第一阶段生成二进制文件大约为 83 分钟,第二阶段生成索引数据信息为 4分钟,第三阶段迭代训练 2 万次耗时为 25 分钟,总耗时为 112 分钟。
在 DDBDataLoader 中,通常会采取以下步骤来处理训练数据:
本次测试中,使用了 DDBDataLoader 来获取训练数据。与传统方法不同,无需将数据保存为文件并在客户端进行处理。相反,通过 Session 将 SQL 查询结果直接转换为 torch.Tensor,这可以减少数据传输和存储成本,在测试代码中,使用 Python 中的第三方库 line_profiler 统计各个部分的执行时间,例如数据加载、模型训练等。测试步骤如下:
1. 定义 DDBDataLoader
在 Python 客户端执行以下代码,使用已建立的数据库表执行 SQL 查询后的结果作为数据集。该数据集指定了目标列为 ["f000001"],并排除了 Symbol 列和 DateTime 列的数据。此外,还配置了以下参数:
这些配置将有助于提高训练效果并充分利用 GPU 和后台线程资源。
import dolphindb as ddb
from dolphindb_tools.dataloader import DDBDataLoader
sess = ddb.Session()
sess.connect('localhost', 8848, "admin", "123456")
dbPath = "dfs://test_ai_dataloader"
tbName = "wide_factor_table"
symbols = ["`" + f"{i}.SH".zfill(9) for i in range(1, 251)]
times = ["2020.01." + f"{i+1}".zfill(2) for i in range(31)]
sql = f"""select * from loadTable("{dbPath}", "{tbName}")"""
dataloader = DDBDataLoader(
s, sql, targetCol=["f000001"], batchSize=64, shuffle=True,
windowSize=[200, 1], windowStride=[1, 1],
repartitionCol="date(DateTime)", repartitionScheme=times,
groupCol="Symbol", groupScheme=symbols,
seed=0,
offset=200, excludeCol=["DateTime", "Symbol"], device="cuda",
prefetchBatch=5, prepartitionNum=3
)
2. 定义网络并训练
下述代码在 Python 客户端执行,它定义了一个简单的 CNN 神经网络结构,并定义了损失函数和优化器。最后像使用 torch 中 DataLoader 一样,迭代 DDBDataLoader 获取数据,输入到网络中进行训练,核心代码如下,详细见 test_wide_new.py:
model = SimpleNet()
model = model.to("cuda")
loss_fn = nn.MSELoss()
loss_fn = loss_fn.to("cuda")
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
num_epochs = 10
model.train()
for epoch in range(num_epochs):
for X, y in dataloader:
X = X.to("cuda")
y = y.to("cuda")
y_pred = model(X)
loss = loss_fn(y_pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
通过将数据直接转换为 torch.Tensor 并使用 DDBDataLoader 管理数据,可以更高效地获取和使用训练数据,从而提高深度学习模型的训练效率。这种方法减少了数据传输和存储的开销,并使训练过程更加灵活和高效。此种方式总耗时为 25 分钟。
从比对结果可以看到,本次测试中,对比了传统方式(PyTorch DataLoader)和 DDBDataLoader,DDBDataLoader 一体化集成 PyTorch 耗时约为 25分钟,内存占用约为 0.8 GB,代码行数约为 70 行, PyTorch DataLoader 总耗时 112 分钟,内存占用约为 4GB,代码行数约为 200 多行。考虑两种方式的特点,原因大概如下:
综上,DDBDataLoader 可以提升性能以及大幅降低 DolphinDB 内数据用于 PyTorch 训练的开发运维成本。
DDBDataLoader 是 DolphinDB 在深度学习和数据库结合方面的一次探索。它旨在解决以下问题:
综上所述,将 DolphinDB 和 DDBDataLoader 集成到因子数据管理流程中,有助于更好地满足量化投资策略的需求,充分发挥深度学习模型的潜力。这种集成方式能够提高效率,降低成本,并提供更强大的因子数据管理和应用能力。