Shark 符号回归

符号回归是一种机器学习方法,无需假定函数的具体形式即可自动发现给定数据集的数学表达式。符号回归的核心在于通过搜索算法,预先设定目标函数,在可能的数学表达式空间中找到最佳表达式,使其能够最大程度地拟合数据。

CPU-GPU 异构计算平台 Shark 应用:GPLearn 自动因子挖掘可以用于实现符号回归,但存在一定的局限性,比如功能过于耦合、扩展性差等。因此,自 3.00.6 版本起,Shark 将 GPLearn 和符号回归进行剥离,以提升符号回归的性能和扩展性。本教程介绍如何使用新版符号回归功能。

注:

社区版 License 暂不支持 Shark。如需使用,请前往官网下载页面,在 Shark 区域单击申请试用

1. 符号回归流程

实现符号回归所需的 Shark 函数包括 getGlobalSharkEngineskSymbolRegressorskSymbolFitskSymbolEvalContextskSymbolEval。详细的函数介绍请查看对应的函数文档,下面介绍符号回归的整体流程:

  1. 准备用于符号回归的数据集。目标是发现能够描述数据集中自变量与因变量之间关系的数学表达式。

  2. 调用 getGlobalSharkEngine 获取 Shark 引擎,即负责执行计算的 Shark 后端。

  3. 调用 skSymbolRegressor 配置符号回归的方法和参数,得到一个符号回归器句柄,供 skSymbolFit 使用。

  4. 调用 skSymbolFit 执行符号回归训练,即寻找最优数学表达式。

  5. 调用 skSymbolEvalContext 创建表达式计算上下文,得到一个句柄,供 skSymbolEval 使用。上下文保存了表达式计算所需的函数定义,使表达式计算与符号回归过程解耦。

  6. 调用 skSymbolEval 使用已有表达式对输入数据进行计算,得到输出结果。

2. 应用案例

本案例模拟一批股票的日频行情,从几个基础技术因子中自动挖掘一个 Alpha 表达式,用来预测未来收益。

2.1 生成模拟数据

生成 20 只股票的模拟日频行情数据,涵盖 260 天的数据。其中每一行代表某只股票在某一天的一条日 K 线。

def mockStock(symbol, startDate, nDays){
    tradeDate = startDate + 0..(nDays - 1)

    basePrice = 20 + rand(80.0, 1)[0]
    dailyRet = randNormal(0.0005, 0.015, nDays)
    close = basePrice * exp(cumsum(dailyRet))

    open = close * (1 + randNormal(0, 0.003, nDays))
    high = max(open, close) * (1 + rand(0.01, nDays))
    low = min(open, close) * (1 - rand(0.01, nDays))
    volume = long(1000000 + rand(9000000, nDays))
    amount = close * volume

    return table(take(symbol, nDays) as symbol, // 股票代码
                 tradeDate, // 交易日期
                 open, // 开盘价
                 high, // 最高价
                 low, // 最低价
                 close, // 收盘价
                 volume, // 成交量
                 amount) // 成交额
}

nStocks = 20
nDays = 260
symbols = format(1..nStocks, "000000") + ".XSHG"

quotes = each(mockStock{, 2025.01.02, nDays}, symbols).unionAll(false)
quotes = select * from quotes order by symbol, tradeDate

2.2 构造输入变量

从原始日频行情表 quotes 里,按每只股票的历史价格和成交量,计算 3 个可以作为符号回归输入变量的股票因子。

  • ret5:近 5 日收益率。

  • vol10:近 10 日收益率波动率。

  • volChg:成交量相对近 10 日均量的放大程度。

factorData =
    select symbol,
           tradeDate,
           close,
           volume,
           close \ move(close, 5) - 1 as ret5,
           mstdp(close \ prev(close) - 1, 10) as vol10,
           volume \ mavg(volume, 10) - 1 as volChg
    from quotes
    context by symbol
    csort tradeDate
// 去除包含 NULL 值的行,只保留 3 个输入变量都有效的数据
factorData =
    select *
    from factorData
    where isValid(ret5)
      and isValid(vol10)
      and isValid(volChg)
    order by symbol, tradeDate

2.3 构造目标变量和数据集

在 factorData 中增加目标变量 nextRet,构成完整的数据集,并将数据集按日期划分为训练集和测试集。

  • dataset:带有输入因子和目标变量的完整数据集。

  • train:训练集,用于在符号回归训练中寻找表达式。

  • test:测试集,用于评估表达式效果。

dataset 里每一行样本可以理解为:某只股票在某一天,已知 ret5、vol10、volChg,目标是 nextRet。符号回归就是在训练集中寻找一个最优表达式,比如 f(ret5, vol10, volChg) ≈ nextRet。后续可以通过该表达式用 ret5、vol10、volChg 3 个因子来预测 nextRet。

注:

在本案例中,nextRet 是人为构造出来的目标变量,便于观察 Shark 是否能够通过符号回归找回类似 ret5 * volChg 这样的组合表达式。在真实应用中,应根据自己的预测目标来定义 nextRet。如果希望预测未来 1 日收益,可以定义为:nextRet = move(close, -1) / close - 1

dataset = select *, double(NULL) as nextRet from factorData
// 增加噪声,目的是让数据更接近真实场景
noise = randNormal(0, 0.001, dataset.rows())

// 定义一个隐藏关系,Shark 需要学习其中的规律,找到最优表达式
dataset[`nextRet] =
      0.60 * dataset[`ret5] * dataset[`volChg] - 0.40 * dataset[`vol10] + 0.20 * dataset[`ret5] + noise

/*拆分数据:
2025.06.30 及以前的数据作为训练集
2025.06.30 之后的数据作为测试集*/
splitDate = 2025.06.30
train = select * from dataset where tradeDate <= splitDate order by symbol, tradeDate
test = select * from dataset where tradeDate > splitDate order by symbol, tradeDate

2.4 获取 Shark 引擎并创建符号回归器

使用 getGlobalSharkEngine() 用来获取环境中的全局 Shark 引擎句柄。可以把引擎理解成 Shark 计算资源的入口。不管是训练符号回归表达式,还是在测试集上计算表达式,都需要通过这个 Shark 引擎来完成。符号回归器配置的说明请参考 skSymbolRegressor 文档中的 config 参数。

// 获取 Shark 引擎
skengine = getGlobalSharkEngine()

// 符号回归器配置
regressorConfig = {
    "functionSet": [add, sub, mul],
    "selection": {
        "name": "tournament",
        "param": {"size": 8}
    },
    "mutations": [
        {"name": "crossover", "weight": 70},
        {"name": "subtree", "weight": 10},
        {"name": "hoist", "weight": 5},
        {"name": "point", "weight": 10, "param": {"rate": 0.15}},
        {"name": "reproduction", "weight": 5}
    ],
    "verbose": true
}

symbolRegressor = skSymbolRegressor(skengine, "gplearn", regressorConfig)

2.5 执行符号回归训练

符号回归训练的作用是,让 Shark 在 ret5、vol10、volChg 这 3 个输入变量之间自动搜索一个数学表达式,使这个表达式的计算结果尽量接近目标变量 nextRet。训练配置的说明请参考 skSymbolFit 文档中的 config 参数。

/*适应度函数用均方误差 MSE。
MSE 越小,说明表达式预测值越接近目标 nextRet。*/
@gpu
def fit_mse(pred, y){
    err = pred - y
    return mean(err * err)
}

/*把训练集中的列取成向量。
变量名 ret5、vol10、volChg 会保留在最终表达式中。*/
ret5 = train["ret5"]
vol10 = train["vol10"]
volChg = train["volChg"]

inputs = {
    "varTerminalSet": [ret5, vol10, volChg]
}

/*本案例中未设置 groupTerminalSet。
groupTerminalSet 是可选的分组变量集合,用于挖掘带聚合算子的分组因子。
分组变量长度必须和 varTerminalSet 中的输入变量长度一致;指定 groupTerminalSet 后,
target 的长度必须等于分组数量。

只有当 functionSet 中包含聚合函数,且 groupTerminalSet 不为空时,Shark 才会进行聚合因子挖掘。*/

target = train["nextRet"]

fitConfig = {
    "fitnessFunction": fit_mse,
    "initializer": {
        "name": "half",
        "param": {
            "minDepth": 2,
            "maxDepth": 5,
            "probConstant": 0.40
        }
    },
    "constantRange": {
        "floatRange": {"min": -1.0, "max": 1.0, "weight": 1},
        "intRange": {"min": -2.0, "max": 2.0, "weight": 1}
    },
    "epsilon": 1e-8,
    "parsimony": 0.0,
    "seed": long(20260625),
    "population": 2000,
    "generation": 60,
    "maxDepth": 7,
    "elites": 10,
    "simplify": true,
    "verbose": true
}

syms, info = skSymbolFit(symbolRegressor, inputs, target, fitConfig)

bestSymbol = syms[0]

print("Best expression:")
print(string(bestSymbol))

print("Training summary:")
print(info)

2.6 计算和评估表达式

在测试集上计算表达式,得到 testPred,将其与之前模拟生成的收益进行对比,评估表达式的预测效果。本案例中给出 3 个常用的评价指标:

  • MSE:均方误差,越小越好。它衡量预测值和真实目标之间的数值距离。

  • Corr:相关系数,越接近 1 越好。

  • Direction Accuracy:方向命中率,越高越好。它统计 testPred 和 nextRet 同号的比例,直观表示涨跌方向判断对了多少。

同时计算一个简单基准:直接用 ret5 作为预测值。

/*在测试集上计算表达式
skSymbolEval 的输入表列名必须和训练变量名一致:ret5、vol10、volChg*/
evalContext = skSymbolEvalContext(skengine)

testInput = select ret5, vol10, volChg from test
testPred = skSymbolEval(evalContext, bestSymbol, testInput)
testY = test["nextRet"]


// 评估表达式的预测效果

testErr = testPred - testY
testMse = mean(testErr * testErr)
testCorr = corr(testPred, testY)
testDirectionAcc = mean(iif(testPred * testY > 0, 1.0, 0.0))

benchPred = test["ret5"]
benchErr = benchPred - testY
benchMse = mean(benchErr * benchErr)
benchCorr = corr(benchPred, testY)
benchDirectionAcc = mean(iif(benchPred * testY > 0, 1.0, 0.0))

metrics = table(
    ["SharkSymbol", "Benchmark_ret5"] as model,
    [testMse, benchMse] as mse,
    [testCorr, benchCorr] as corr,
    [testDirectionAcc, benchDirectionAcc] as directionAcc
)

下面以某一次符号回归训练的结果为例解释训练结果。 经过训练得到的最优表达式如下:

((vol10 * ((-0.271591 * ((ret5 - 1.000000) - -0.271591)) * -2.000000)) + ((-0.271591 * (ret5 * 1.000000)) * ((-0.711353 - (1.000000 * volChg)) - ((0.513379 + 0.708364) * (1.000000 * volChg)))))

使用最优表达式计算测试集中的变量并评估计算结果。 metrics 表格如下:

model mse corr directionAcc
SharkSymbol 0.00000103402082676871 0.9964976353912933 0.97875
Benchmark_ret5 0.0009395193485851182 0.5799083129952723 0.64875
  • MSE:SharkSymbol 的误差远小于 Benchmark_ret5。mse 是均方误差,表示预测值和真实目标 nextRet 之间的平均平方误差。它越小,说明预测值越接近目标值。

  • Corr:SharkSymbol 和目标收益几乎完全相关。corr 是预测值和目标值 nextRet 的相关系数,越接近 1,说明两者走势越一致。

  • Direction Accuracy:SharkSymbol 的方向判断非常准确。0.97875 表示 Shark 表达式在测试集上有 97.875% 的样本方向判断正确。而 Benchmark_ret5 的方向命中率仅为 64.875%。

Shark 找到的表达式几乎复现了我们构造 nextRet 时隐藏的真实公式,因此在测试集上表现非常好;相比之下,Benchmark_ret5 只用了单个动量因子,缺少 ret5 * volChg 和 vol10 的信息,所以误差更大、相关性更低、方向命中率也明显更差。

3. 总结

本教程通过一个简化的股票日频 Alpha 挖掘案例,演示了如何使用 DolphinDB Shark 符号回归从输入因子中自动搜索可解释的数学表达式。示例首先使用 DolphinDB 函数生成多只股票的模拟日频行情,再基于价格和成交量构造 ret5、vol10、volChg 三个基础因子,并人为构造目标变量 nextRet。随后,使用 getGlobalSharkEngine 获取 Shark 引擎,通过 skSymbolRegressor 配置符号回归器,并调用 skSymbolFit 训练表达式。最后,借助 skSymbolEvalContextskSymbolEval 在测试集上计算表达式结果,并使用 MSE、相关系数和方向命中率评估模型效果。