用卷积网络处理序列数据
我们知道卷积神经网络(convnet)在计算机视觉问题上表现出色,原因在于它能够进行卷积运算,从局部输入图块中提取特征,并能够将表示模块化,同时可以高效地利用数据。这些性质让卷积神经网络在计算机视觉领域表现优异,同样也让它对序列处理特别有效。时间可以被看作一个空间维度,就像二维图像的高度或宽度。
对于某些序列处理问题,比如金融时间序列数据,这种一维卷积神经网络的效果可以媲美RNN[循环神经网络],而且计算代价通常要小很多。最近,一维卷积神经网络[通常与空洞卷积核(dilated kernel)一起使用]已经在音频生成和机器翻译领域取得了巨大成功。除了这些具体的成就,人们还早已知道,对于文本分类和时间序列预测等简单任务,小型的一维卷积神经网络可以替代RNN,而且速度更快。
理解序列数据的一维卷积
通常我们遇见的卷积层都是二维卷积,从图像张量中提取二维图块并对每个图块应用相同的变换。按照同样的方法,你也可以使用一维卷积,从序列中提取局部一维序列段(即子序列),见下图:
注:图片来自《Deep Learning with Python 》弗朗索瓦·肖莱,Keras之父
这种一维卷积层可以识别序列中的局部模式。因为对每个序列段执行相同的输入变换,所以在句子中某个位置学到的模式稍后可以在其他位置被识别,这使得一维卷积神经网络具有平移不变性(对于时间平移而言)。
举个例子,使用大小为 5 的卷积窗口处理字符序列的一维卷积神经网络,应该能够学习长度不大于 5 的单词或单词片段,并且应该能够在输入句子中的任何位置识别这些单词或单词段。因此,字符级的一维卷积神经网络能够学会单词构词法。在金融时序预测中,卷积网络可以提取近期时序特征(局部特征)来预测短期走势,这是浅层机器学习模型不具备的优势。
序列数据的一维池化
二维池化运算,比如二维平均池化和二维最大池化,在卷积神经网络中用于对图像张量进行空间下采样。一维也可以做相同的池化运算:从输入中提取一维序列段(即子序列), 然后输出其最大值(最大池化)或平均值(平均池化)。与二维卷积神经网络一样,该运算也是用于降低一维输入的长度(子采样)。
实现一维卷积神经网络
BigQuant中的一维卷积神经网络是Conv1D层,其接口类似于Conv2D。它接收的输入是形状为 (samples, time, features)的三维张量,并返回类似形状的三维张量。卷积窗口是时间轴上的一维窗口(时间轴是输入张量的第二个轴)。
我们来构建一个简单的两层一维卷积神经网络预测股票价格,回测结果图如下,源代码见文末。
策略比较基准
为比较深度学习模型的预测效果,我们以默认可视化机器学习模板为基准进行比较,默认可视化机器学习模板是StockRanker的浅层机器学习策略。
因此,本文的训练集时间、预测集时间、特征完全和默认可视化策略模板一致。基准回测结果如下:
可以看出基于卷积神经网络的深度学习策略有明显的提升效果,策略年化收益从109%提升到118%,夏普比率也有所提升,这确实是很amazing的一件事情,因为stockranker策略的参数和模型我们是经过大量的测试给出了一个比较通用的版本,这主要得益于深度网络多层表示的强大学习能力,这和我们大脑大量的神经元机制是相似的。当然,本文只是一个demo,更多开发和提升还需依赖每一位developer.
代码如下,可直接克隆:
# 本代码由可视化策略环境自动生成 2019年4月5日 10:30
# 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
# Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
def m4_run_bigquant_run(input_1, input_2, input_3):
# 示例代码如下。在这里编写您的代码
df = input_1.read_pickle()
feature_len = len(input_2.read_pickle())
df['x'] = df['x'].reshape(df['x'].shape[0], int(feature_len), int(df['x'].shape[1]/feature_len))
data_1 = DataSource.write_pickle(df)
return Outputs(data_1=data_1)
# 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
def m4_post_run_bigquant_run(outputs):
return outputs
# Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
def m8_run_bigquant_run(input_1, input_2, input_3):
# 示例代码如下。在这里编写您的代码
df = input_1.read_pickle()
feature_len = len(input_2.read_pickle())
df['x'] = df['x'].reshape(df['x'].shape[0], int(feature_len), int(df['x'].shape[1]/feature_len))
data_1 = DataSource.write_pickle(df)
return Outputs(data_1=data_1)
# 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
def m8_post_run_bigquant_run(outputs):
return outputs
# Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
def m24_run_bigquant_run(input_1, input_2, input_3):
# 示例代码如下。在这里编写您的代码
pred_label = input_1.read_pickle()
df = input_2.read_df()
df = pd.DataFrame({'pred_label':pred_label[:,0], 'instrument':df.instrument, 'date':df.date})
df.sort_values(['date','pred_label'],inplace=True, ascending=[True,False])
return Outputs(data_1=DataSource.write_df(df), data_2=None, data_3=None)
# 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
def m24_post_run_bigquant_run(outputs):
return outputs
# 回测引擎:每日数据处理函数,每天执行一次
def m19_handle_data_bigquant_run(context, data):
# 按日期过滤得到今日的预测数据
ranker_prediction = context.ranker_prediction[
context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]
# 1. 资金分配
# 平均持仓时间是hold_days,每日都将买入股票,每日预期使用 1/hold_days 的资金
# 实际操作中,会存在一定的买入误差,所以在前hold_days天,等量使用资金;之后,尽量使用剩余资金(这里设置最多用等量的1.5倍)
is_staging = context.trading_day_index < context.options['hold_days'] # 是否在建仓期间(前 hold_days 天)
cash_avg = context.portfolio.portfolio_value / context.options['hold_days']
cash_for_buy = min(context.portfolio.cash, (1 if is_staging else 1.5) * cash_avg)
cash_for_sell = cash_avg - (context.portfolio.cash - cash_for_buy)
positions = {e.symbol: p.amount * p.last_sale_price
for e, p in context.perf_tracker.position_tracker.positions.items()}
# 2. 生成卖出订单:hold_days天之后才开始卖出;对持仓的股票,按机器学习算法预测的排序末位淘汰
if not is_staging and cash_for_sell > 0:
equities = {e.symbol: e for e, p in context.perf_tracker.position_tracker.positions.items()}
instruments = list(reversed(list(ranker_prediction.instrument[ranker_prediction.instrument.apply(
lambda x: x in equities and not context.has_unfinished_sell_order(equities[x]))])))
# print('rank order for sell %s' % instruments)
for instrument in instruments:
context.order_target(context.symbol(instrument), 0)
cash_for_sell -= positions[instrument]
if cash_for_sell <= 0:
break
# 3. 生成买入订单:按机器学习算法预测的排序,买入前面的stock_count只股票
buy_cash_weights = context.stock_weights
buy_instruments = list(ranker_prediction.instrument[:len(buy_cash_weights)])
max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument
for i, instrument in enumerate(buy_instruments):
cash = cash_for_buy * buy_cash_weights[i]
if cash > max_cash_per_instrument - positions.get(instrument, 0):
# 确保股票持仓量不会超过每次股票最大的占用资金量
cash = max_cash_per_instrument - positions.get(instrument, 0)
if cash > 0:
context.order_value(context.symbol(instrument), cash)
# 回测引擎:准备数据,只执行一次
def m19_prepare_bigquant_run(context):
pass
# 回测引擎:初始化函数,只执行一次
def m19_initialize_bigquant_run(context):
# 加载预测数据
context.ranker_prediction = context.options['data'].read_df()
# 系统已经设置了默认的交易手续费和滑点,要修改手续费可使用如下函数
context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
# 预测数据,通过options传入进来,使用 read_df 函数,加载到内存 (DataFrame)
# 设置买入的股票数量,这里买入预测股票列表排名靠前的5只
stock_count = 20
# 每只的股票的权重,如下的权重分配会使得靠前的股票分配多一点的资金,[0.339160, 0.213986, 0.169580, ..]
context.stock_weights = T.norm([1 / math.log(i + 2) for i in range(0, stock_count)])
# 设置每只股票占用的最大资金比例
context.max_cash_per_instrument = 0.2
context.options['hold_days'] = 2
m1 = M.instruments.v2(
start_date='2010-01-01',
end_date='2015-01-01',
market='CN_STOCK_A',
instrument_list=' ',
max_count=0
)
m2 = M.advanced_auto_labeler.v2(
instruments=m1.data,
label_expr="""# #号开始的表示注释
# 0. 每行一个,顺序执行,从第二个开始,可以使用label字段
# 1. 可用数据字段见 https://bigquant.com/docs/data_history_data.html
# 添加benchmark_前缀,可使用对应的benchmark数据
# 2. 可用操作符和函数见 `表达式引擎 <https://bigquant.com/docs/big_expr.html>`_
# 计算收益:5日收盘价(作为卖出价格)除以明日开盘价(作为买入价格)
shift(close, -2) / shift(open, -1)-1
# 极值处理:用1%和99%分位的值做clip
clip(label, all_quantile(label, 0.01), all_quantile(label, 0.99))
# 过滤掉一字涨停的情况 (设置label为NaN,在后续处理和训练中会忽略NaN的label)
where(shift(high, -1) == shift(low, -1), NaN, label)
""",
start_date='',
end_date='',
benchmark='000300.SHA',
drop_na_label=True,
cast_label_int=False
)
m13 = M.standardlize.v8(
input_1=m2.data,
columns_input=['label']
)
m3 = M.input_features.v1(
features="""return_5
return_10
return_20
avg_amount_0/avg_amount_5
avg_amount_5/avg_amount_20
rank_avg_amount_0/rank_avg_amount_5
rank_avg_amount_5/rank_avg_amount_10
rank_return_0
rank_return_5
rank_return_10
rank_return_0/rank_return_5
rank_return_5/rank_return_10
pe_ttm_0
"""
)
m15 = M.general_feature_extractor.v7(
instruments=m1.data,
features=m3.data,
start_date='',
end_date='',
before_start_days=0
)
m16 = M.derived_feature_extractor.v3(
input_data=m15.data,
features=m3.data,
date_col='date',
instrument_col='instrument',
drop_na=True,
remove_extra_columns=False
)
m14 = M.standardlize.v8(
input_1=m16.data,
input_2=m3.data,
columns_input=[]
)
m7 = M.join.v3(
data1=m13.data,
data2=m14.data,
on='date,instrument',
how='inner',
sort=False
)
m26 = M.dl_convert_to_bin.v2(
input_data=m7.data,
features=m3.data,
window_size=5,
feature_clip=5,
flatten=True,
window_along_col='instrument'
)
m4 = M.cached.v3(
input_1=m26.data,
input_2=m3.data,
run=m4_run_bigquant_run,
post_run=m4_post_run_bigquant_run,
input_ports='',
params='{}',
output_ports=''
)
m9 = M.instruments.v2(
start_date=T.live_run_param('trading_date', '2015-01-01'),
end_date=T.live_run_param('trading_date', '2017-01-01'),
market='CN_STOCK_A',
instrument_list='',
max_count=0
)
m17 = M.general_feature_extractor.v7(
instruments=m9.data,
features=m3.data,
start_date='',
end_date='',
before_start_days=0
)
m18 = M.derived_feature_extractor.v3(
input_data=m17.data,
features=m3.data,
date_col='date',
instrument_col='instrument',
drop_na=True,
remove_extra_columns=False
)
m25 = M.standardlize.v8(
input_1=m18.data,
input_2=m3.data,
columns_input=[]
)
m27 = M.dl_convert_to_bin.v2(
input_data=m25.data,
features=m3.data,
window_size=5,
feature_clip=5,
flatten=True,
window_along_col='instrument'
)
m8 = M.cached.v3(
input_1=m27.data,
input_2=m3.data,
run=m8_run_bigquant_run,
post_run=m8_post_run_bigquant_run,
input_ports='',
params='{}',
output_ports=''
)
m6 = M.dl_layer_input.v1(
shape='13,5',
batch_shape='',
dtype='float32',
sparse=False,
name=''
)
m10 = M.dl_layer_conv1d.v1(
inputs=m6.data,
filters=32,
kernel_size='7',
strides='1',
padding='valid',
dilation_rate=1,
activation='relu',
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='Zeros',
kernel_regularizer='None',
kernel_regularizer_l1=0,
kernel_regularizer_l2=0,
bias_regularizer='None',
bias_regularizer_l1=0,
bias_regularizer_l2=0,
activity_regularizer='None',
activity_regularizer_l1=0,
activity_regularizer_l2=0,
kernel_constraint='None',
bias_constraint='None',
name=''
)
m12 = M.dl_layer_maxpooling1d.v1(
inputs=m10.data,
pool_size=1,
padding='valid',
name=''
)
m32 = M.dl_layer_conv1d.v1(
inputs=m12.data,
filters=32,
kernel_size='7',
strides='1',
padding='valid',
dilation_rate=1,
activation='relu',
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='Zeros',
kernel_regularizer='None',
kernel_regularizer_l1=0,
kernel_regularizer_l2=0,
bias_regularizer='None',
bias_regularizer_l1=0,
bias_regularizer_l2=0,
activity_regularizer='None',
activity_regularizer_l1=0,
activity_regularizer_l2=0,
kernel_constraint='None',
bias_constraint='None',
name=''
)
m33 = M.dl_layer_maxpooling1d.v1(
inputs=m32.data,
pool_size=1,
padding='valid',
name=''
)
m28 = M.dl_layer_globalmaxpooling1d.v1(
inputs=m33.data,
name=''
)
m30 = M.dl_layer_dense.v1(
inputs=m28.data,
units=1,
activation='linear',
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='Zeros',
kernel_regularizer='None',
kernel_regularizer_l1=0,
kernel_regularizer_l2=0,
bias_regularizer='None',
bias_regularizer_l1=0,
bias_regularizer_l2=0,
activity_regularizer='None',
activity_regularizer_l1=0,
activity_regularizer_l2=0,
kernel_constraint='None',
bias_constraint='None',
name=''
)
m34 = M.dl_model_init.v1(
inputs=m6.data,
outputs=m30.data
)
m5 = M.dl_model_train.v1(
input_model=m34.data,
training_data=m4.data_1,
optimizer='RMSprop',
loss='mean_squared_error',
metrics='mae',
batch_size=256,
epochs=5,
n_gpus=0,
verbose='2:每个epoch输出一行记录'
)
m11 = M.dl_model_predict.v1(
trained_model=m5.data,
input_data=m8.data_1,
batch_size=1024,
n_gpus=0,
verbose='2:每个epoch输出一行记录'
)
m24 = M.cached.v3(
input_1=m11.data,
input_2=m18.data,
run=m24_run_bigquant_run,
post_run=m24_post_run_bigquant_run,
input_ports='',
params='{}',
output_ports=''
)
m19 = M.trade.v4(
instruments=m9.data,
options_data=m24.data_1,
start_date='',
end_date='',
handle_data=m19_handle_data_bigquant_run,
prepare=m19_prepare_bigquant_run,
initialize=m19_initialize_bigquant_run,
volume_limit=0.025,
order_price_field_buy='open',
order_price_field_sell='close',
capital_base=1000000,
auto_cancel_non_tradable_orders=True,
data_frequency='daily',
price_type='后复权',
product_type='股票',
plot_charts=True,
backtest_only=False,
benchmark='000300.SHA'
)
2019-04-05 10:13:26.809494 INFO: bigquant: instruments.v2 开始运行.
2019-04-05 10:13:26.814503 INFO: bigquant: 命中缓
2019-04-05 10:13:26.816040 INFO: bigquant: instruments.v2 运行完成[0.006543s]
2019-04-05 10:13:26.818700 INFO: bigquant: advanced_auto_labeler.v2 开始运行.
2019-04-05 10:13:26.823347 INFO: bigquant: 命中缓
2019-04-05 10:13:26.824605 INFO: bigquant: advanced_auto_labeler.v2 运行完成[0.005899s]
2019-04-05 10:13:26.827605 INFO: bigquant: standardlize.v8 开始运行.
2019-04-05 10:13:26.831346 INFO: bigquant: 命中缓
2019-04-05 10:13:26.832557 INFO: bigquant: standardlize.v8 运行完成[0.004946s]
2019-04-05 10:13:26.835016 INFO: bigquant: input_features.v1 开始运行.
2019-04-05 10:13:26.838509 INFO: bigquant: 命中缓
2019-04-05 10:13:26.839779 INFO: bigquant: input_features.v1 运行完成[0.004758s]
2019-04-05 10:13:26.844880 INFO: bigquant: general_feature_extractor.v7 开始运行.
2019-04-05 10:13:26.848864 INFO: bigquant: 命中缓
2019-04-05 10:13:26.850028 INFO: bigquant: general_feature_extractor.v7 运行完成[0.005144s]
2019-04-05 10:13:26.852530 INFO: bigquant: derived_feature_extractor.v3 开始运行.
2019-04-05 10:13:26.858042 INFO: bigquant: 命中缓
2019-04-05 10:13:26.859938 INFO: bigquant: derived_feature_extractor.v3 运行完成[0.0074s]
2019-04-05 10:13:26.863241 INFO: bigquant: standardlize.v8 开始运行.
2019-04-05 10:13:26.869466 INFO: bigquant: 命中缓
2019-04-05 10:13:26.871091 INFO: bigquant: standardlize.v8 运行完成[0.007842s]
2019-04-05 10:13:26.874001 INFO: bigquant: join.v3 开始运行.
2019-04-05 10:13:26.880091 INFO: bigquant: 命中缓
2019-04-05 10:13:26.881712 INFO: bigquant: join.v3 运行完成[0.007701s]
2019-04-05 10:13:26.885932 INFO: bigquant: dl_convert_to_bin.v2 开始运行.
2019-04-05 10:13:26.890322 INFO: bigquant: 命中缓
2019-04-05 10:13:26.891538 INFO: bigquant: dl_convert_to_bin.v2 运行完成[0.005602s]
2019-04-05 10:13:26.895200 INFO: bigquant: cached.v3 开始运行.
2019-04-05 10:13:30.206732 INFO: bigquant: cached.v3 运行完成[3.311517s]
2019-04-05 10:13:30.210149 INFO: bigquant: instruments.v2 开始运行.
2019-04-05 10:13:30.217214 INFO: bigquant: 命中缓
2019-04-05 10:13:30.219263 INFO: bigquant: instruments.v2 运行完成[0.009101s]
2019-04-05 10:13:30.226565 INFO: bigquant: general_feature_extractor.v7 开始运行.
2019-04-05 10:13:30.233384 INFO: bigquant: 命中缓
2019-04-05 10:13:30.235101 INFO: bigquant: general_feature_extractor.v7 运行完成[0.008529s]
2019-04-05 10:13:30.238458 INFO: bigquant: derived_feature_extractor.v3 开始运行.
2019-04-05 10:13:30.243841 INFO: bigquant: 命中缓
2019-04-05 10:13:30.245263 INFO: bigquant: derived_feature_extractor.v3 运行完成[0.006803s]
2019-04-05 10:13:30.248603 INFO: bigquant: standardlize.v8 开始运行.
2019-04-05 10:13:30.252893 INFO: bigquant: 命中缓
2019-04-05 10:13:30.254429 INFO: bigquant: standardlize.v8 运行完成[0.005813s]
2019-04-05 10:13:30.261565 INFO: bigquant: dl_convert_to_bin.v2 开始运行.
2019-04-05 10:14:28.679472 INFO: bigquant: dl_convert_to_bin.v2 运行完成[58.417895s]
2019-04-05 10:14:28.685200 INFO: bigquant: cached.v3 开始运行.
2019-04-05 10:14:30.462071 INFO: bigquant: cached.v3 运行完成[1.776864s]
2019-04-05 10:14:30.700412 INFO: bigquant: cached.v3 开始运行.
2019-04-05 10:14:30.705653 INFO: bigquant: 命中缓
2019-04-05 10:14:30.707200 INFO: bigquant: cached.v3 运行完成[0.006762s]
2019-04-05 10:14:30.710042 INFO: bigquant: dl_model_train.v1 开始运行.
2019-04-05 10:14:32.317834 INFO: dl_model_train: 准备训练,训练样本个数:2613812,迭代次数:
2019-04-05 10:19:29.268156 INFO: dl_model_train: 训练结束,耗时:296.95
2019-04-05 10:19:29.394656 INFO: bigquant: dl_model_train.v1 运行完成[298.6846s]
2019-04-05 10:19:29.426576 INFO: bigquant: dl_model_predict.v1 开始运行.
2019-04-05 10:19:33.503825 INFO: bigquant: dl_model_predict.v1 运行完成[4.077232s]
2019-04-05 10:19:33.511205 INFO: bigquant: cached.v3 开始运行.
2019-04-05 10:19:37.313999 INFO: bigquant: cached.v3 运行完成[3.802763s]
2019-04-05 10:19:37.494094 INFO: bigquant: backtest.v8 开始运行.
2019-04-05 10:19:37.496684 INFO: bigquant: biglearning backtest:V8.1.1
2019-04-05 10:19:37.498476 INFO: bigquant: product_type:stock by specifie
2019-04-05 10:19:47.727329 INFO: bigquant: 读取股票行情完成:199027
2019-04-05 10:20:06.271409 INFO: algo: TradingAlgorithm V1.4.1
2019-04-05 10:20:15.945127 INFO: algo: trading transform..
2019-04-05 10:20:51.068766 INFO: Performance: Simulated 488 trading days out of 488
2019-04-05 10:20:51.071617 INFO: Performance: first open: 2015-01-05 09:30:00+00:0
2019-04-05 10:20:51.074027 INFO: Performance: last close: 2016-12-30 15:00:00+00:0
2019-04-05 10:20:55.996850 INFO: bigquant: backtest.v8 运行完成[78.502742s]
参考文献:
- 《Deep Learning with Python 》弗朗索瓦·肖莱,Keras之父
- A Comprehensive Guide to Convolutional Neural Networks