上篇报告介绍了集成学习里Bagging方法的代表算法随机森林,本文将着眼于另一种集成学习方法:Boosting,并深入介绍Boosting里的“王牌” XGBoost 模型。最后,以一个实例介绍XGBoost模型在智能选股方面的应用。
$$目录$$
1、Boosting V.S. Bagging
2、XGBoost原理解析
2.1 XGBoost的主要原理
2.2 XGBoost的重要参数
2.3 XGBoost的优势
2.4 其他
3、XGBoost智能选股
1、Boosting V.S. Bagging
作为集成学习的两大分支,Boosting和Bagging都秉持着“三个臭皮匠顶个诸葛亮”的想法,致力于将单个弱学习器组合成为一个强学习器。他们的不同主要在组合方式上:Bagging如上篇报告介绍的,采用bootstrap随机抽样从整体数据集中得到很多个小数据集(小袋),用每一个小数据集分别并列训练弱学习器,最后采纳投票或者取均值的方式得到强学习器。
而Boosting的想法是加强,用整体数据集训练出第一个弱学习器后,在此基础上采用调整权值或者拟合残差的方法降低数据集的损失,最后采用加权平均或者加法原理得到强学习器。Boosting每一步的优化都要在得到上一步弱学习器的基础上进行,是一种“串行”的方法。
$$图1:Boosting 和 Bagging 示意$$
这两种方法的主要区别如下:
方法 | Boosting | Bagging |
---|---|---|
结构 | 串行 | 并行 |
训练集 | 依赖 | 独立 |
作用 | 减少bias | 减少variance |
2、XGBoost原理解析
2.1 XGBoost的主要原理
XGBoost(eXtreme Gradient Boosting)全名为极端梯度提升,是一种Boosting集成算法。它通过最小化损失函数(目标函数)训练单个弱学习器,然后计算得出残差,用残差集再去训练新的弱学习器,目的在于让样本的损失变得更小。在所有的弱学习器训练完成后,将得分加总得到强学习器的得分。
XGBoost的创新之处在于在单纯的损失函数:$ \sum_{i=1}^{n}L(y_i,\hat{y_i})$ 基础上增加了惩罚项:$ \sum_{k=1}^{K}\Omega(f_k)$,降低模型的复杂度,从而避免过拟合问题。
$$图2:XGBoost算法流程$$
XGBoost 的思想可以用一个通俗的例子解释,假如某人有170厘米身高,我们首先用160厘米去拟合,发现残差有10厘米,这时我们用6厘米去拟合剩下的残差,发现残差还有4厘米,第三轮我们用3厘米拟合剩下的残差,残差就只有1厘米了。如果迭代轮数还没有完,可以继续迭代下去,每一轮迭代,拟合的身高残差都会减小。
2.2 XGBoost的重要参数
框架参数
- n_estimators:最大弱学习器的个数。一般来说n_estimators太小容易欠拟合,数量太多容易过拟合。实际调参时n_estimators与learning_rate一起考虑。
- learning_rate:学习率,每个弱学习器的权重缩减系数v,也称作步长,取值在0到1之间。v越小,弱学习器迭代次数越多。
- subsample:子采样率,取值在0到1之间。若取值为1,则使用全部样本。如果取值小于1,则只有部分样本会做XGBoost拟合,可以减少方差,防止过拟合;但同时会增大样本拟合的偏差,因此取值不能太低。
弱学习器参数
- max_features:决策树划分时考虑的最大特征个数,默认全部考虑。若特征过多可以取部分特征以加快生成时间。
- colsample_bytree:构建树时的列抽样比例。
- colsample_bylevel:决策树每层分裂时的列抽样比例。
2.3 XGBoost的优势
- XGBoost 在损失函数里加入了正则项,用于控制模型的复杂度。从方差和偏差权衡的角度来讲,正则项降低了模型的方差,使训练得出的模型更加简单,能防止过拟合。
- XGBoost 除了决策树外,还支持线性分类器作为弱分类器,此时XGBoost 相当于包含了L1 和L2 正则项的Logistic 回归(分类问题)或者线性回归(回归问题)。
- XGBoost 借鉴了随机森林的做法,支持特征抽样,在训练弱学习器时,只使用抽样出来的部分特征。这样不仅能降低过拟合,还能减少计算。
- XGBoost 支持并行。但是XGBoost 的并行不是指能够并行地训练决策树,XGBoost 也是训练完一棵决策树再训练下一棵决策树的。XGBoost 是在处理特征的层面上实现并行的。我们知道,训练决策树最耗时的一步就是对各个特征的值进行排序(为了确定最佳分割点)并计算信息增益,XGBoost 对于各个特征的信息增益计算就可以在多线程中进行。
2.4 其他
3、XGBoost智能选股
$$图4:XGBoost智能选股策略$$
如图4所示,XGBoost的策略构建包含下列步骤:
- 获取数据 :A股所有股票。
- 特征和标签提取 :计算18个因子作为样本特征;计算未来5日的个股收益作为样本的标签。
- 特征预处理 :进行缺失值处理。
- 模型训练与预测 :使用XGBoost模型进行训练和预测。
- 策略回测 :利用2010到2017年数据进行训练,预测2017到2019年的股票表现。每日买入预测排名最靠前的5只股票,至少持有五日,同时淘汰排名靠后的股票。具体而言,预测排名越靠前,分配到的资金越多且最大资金占用比例不超过20%;初始5日平均分配资金,之后,尽量使用剩余资金(这里设置最多用等量的1.5倍)。
- 模型评价 :查看模型回测结果,并与随机森林进行对比。
回测结果如下:
$$图5:XGBoost回测结果$$
对比随机森林在同一时段的回测结果:
$$图6:随机森林回测结果$$
XGBoost与随机森林相比,明显的优势在于回测速度很快,这和其并行处理特征的特点相关。从回测结果来看,XGBoost策略不及随机森林,收益率比随机森林低10%,且最大回撤稍稍高出随机森林2%。当然,经过进一步调参,相信XGBoost的效果会有比较大的提升。
模型评估如下:
$$图7:XGBoost训练集评估$$
$$图8:XGBoost预测集评估$$
模型评估的结果也表明随机森林的拟合程度更高一些,但是从误差的角度来看,随机森林和XGBoost的差别不大。
欢迎进一步尝试:
# 本代码由可视化策略环境自动生成 2019年4月24日 18:01
# 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
# 回测引擎:每日数据处理函数,每天执行一次
def m19_handle_data_bigquant_run(context, data):
# 按日期过滤得到今日的预测数据
ranker_prediction = context.ranker_prediction[
context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]
ranker_prediction = ranker_prediction.sort_values('pred_label', ascending=False)
# 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.portfolio.positions.items()}
# 2. 生成卖出订单:hold_days天之后才开始卖出;对持仓的股票,按机器学习算法预测的排序末位淘汰
if not is_staging and cash_for_sell > 0:
equities = {e.symbol: e for e, p in context.portfolio.positions.items()}
instruments = list(reversed(list(ranker_prediction.instrument[ranker_prediction.instrument.apply(
lambda x: x in equities)])))
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 = 5
# 每只的股票的权重,如下的权重分配会使得靠前的股票分配多一点的资金,[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'] = 5
m1 = M.instruments.v2(
start_date='2010-01-01',
end_date='2017-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/develop/datasource/deprecated/history_data.html
# 添加benchmark_前缀,可使用对应的benchmark数据
# 2. 可用操作符和函数见 `表达式引擎 <https://bigquant.com/docs/develop/bigexpr/usage.html>`_
# 计算收益:5日收盘价(作为卖出价格)除以明日开盘价(作为买入价格)
shift(close, -5) / shift(open, -1)
# 极值处理:用1%和99%分位的值做clip
clip(label, all_quantile(label, 0.01), all_quantile(label, 0.99))
# 将分数映射到分类,这里使用20个分类
all_wbins(label, 20)
# 过滤掉一字涨停的情况 (设置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=True
)
m3 = M.input_features.v1(
features="""(close_0-mean(close_0,12))/mean(close_0,12)*100
rank(std(amount_0,15))
rank_avg_amount_0/rank_avg_amount_8
ts_argmin(low_0,20)
rank_return_30
(low_1-close_0)/close_0
ta_bbands_lowerband_14_0
mean(mf_net_pct_s_0,4)
amount_0/avg_amount_3
return_0/return_5
return_1/return_5
rank_avg_amount_7/rank_avg_amount_10
ta_sma_10_0/close_0
sqrt(high_0*low_0)-amount_0/volume_0*adjust_factor_0
avg_turn_15/(turn_0+1e-5)
return_10
mf_net_pct_s_0
(close_0-open_0)/close_1"""
)
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=False,
remove_extra_columns=False
)
m7 = M.join.v3(
data1=m2.data,
data2=m16.data,
on='date,instrument',
how='inner',
sort=False
)
m13 = M.dropnan.v1(
input_data=m7.data
)
m9 = M.instruments.v2(
start_date=T.live_run_param('trading_date', '2017-01-01'),
end_date=T.live_run_param('trading_date', '2019-04-17'),
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=False,
remove_extra_columns=False
)
m14 = M.dropnan.v1(
input_data=m18.data
)
m5 = M.gradient_boosting_regressor.v1(
training_ds=m13.data,
features=m3.data,
predict_ds=m14.data,
loss='ls',
learning_rate=0.1,
iterations=50,
subsample=1,
min_samples_per_leaf=1,
max_depth=3,
feature_fraction=1,
key_cols='date,instrument',
other_train_parameters={}
)
m19 = M.trade.v4(
instruments=m9.data,
options_data=m5.predictions,
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-24 17:59:50.796855 INFO: bigquant: instruments.v2 开始运行.
2019-04-24 17:59:50.837211 INFO: bigquant: 命中缓
2019-04-24 17:59:50.839022 INFO: bigquant: instruments.v2 运行完成[0.042172s]
2019-04-24 17:59:50.843806 INFO: bigquant: advanced_auto_labeler.v2 开始运行.
2019-04-24 17:59:50.882667 INFO: bigquant: 命中缓
2019-04-24 17:59:50.885541 INFO: bigquant: advanced_auto_labeler.v2 运行完成[0.041717s]
2019-04-24 17:59:50.889886 INFO: bigquant: input_features.v1 开始运行.
2019-04-24 17:59:50.916990 INFO: bigquant: 命中缓
2019-04-24 17:59:50.918976 INFO: bigquant: input_features.v1 运行完成[0.029083s]
2019-04-24 17:59:50.967577 INFO: bigquant: general_feature_extractor.v7 开始运行.
2019-04-24 17:59:50.999469 INFO: bigquant: 命中缓
2019-04-24 17:59:51.001499 INFO: bigquant: general_feature_extractor.v7 运行完成[0.033922s]
2019-04-24 17:59:51.020940 INFO: bigquant: derived_feature_extractor.v3 开始运行.
2019-04-24 17:59:51.051521 INFO: bigquant: 命中缓
2019-04-24 17:59:51.053400 INFO: bigquant: derived_feature_extractor.v3 运行完成[0.03246s]
2019-04-24 17:59:51.058304 INFO: bigquant: join.v3 开始运行.
2019-04-24 17:59:51.092530 INFO: bigquant: 命中缓
2019-04-24 17:59:51.096213 INFO: bigquant: join.v3 运行完成[0.037897s]
2019-04-24 17:59:51.100644 INFO: bigquant: dropnan.v1 开始运行.
2019-04-24 17:59:51.130689 INFO: bigquant: 命中缓
2019-04-24 17:59:51.132674 INFO: bigquant: dropnan.v1 运行完成[0.032046s]
2019-04-24 17:59:51.135888 INFO: bigquant: instruments.v2 开始运行.
2019-04-24 17:59:51.162821 INFO: bigquant: 命中缓
2019-04-24 17:59:51.164490 INFO: bigquant: instruments.v2 运行完成[0.028596s]
2019-04-24 17:59:51.202456 INFO: bigquant: general_feature_extractor.v7 开始运行.
2019-04-24 17:59:51.233786 INFO: bigquant: 命中缓
2019-04-24 17:59:51.236185 INFO: bigquant: general_feature_extractor.v7 运行完成[0.033706s]
2019-04-24 17:59:51.240379 INFO: bigquant: derived_feature_extractor.v3 开始运行.
2019-04-24 17:59:51.272273 INFO: bigquant: 命中缓
2019-04-24 17:59:51.274288 INFO: bigquant: derived_feature_extractor.v3 运行完成[0.033903s]
2019-04-24 17:59:51.276931 INFO: bigquant: dropnan.v1 开始运行.
2019-04-24 17:59:51.310317 INFO: bigquant: 命中缓
2019-04-24 17:59:51.312565 INFO: bigquant: dropnan.v1 运行完成[0.03562s]
2019-04-24 17:59:51.559499 INFO: bigquant: gradient_boosting_regressor.v1 开始运行.
2019-04-24 17:59:51.625751 INFO: bigquant: 命中缓
2019-04-24 17:59:51.628878 INFO: bigquant: gradient_boosting_regressor.v1 运行完成[0.069371s]
2019-04-24 17:59:51.696097 INFO: bigquant: backtest.v8 开始运行.
2019-04-24 17:59:51.731763 INFO: bigquant: 命中缓
2019-04-24 17:59:53.376138 INFO: bigquant: backtest.v8 运行完成[1.680007s]
参考文献
- 华泰证券 《人工智能选股之Boosting 模型》2017-09-11
- 知乎《集成学习-Boosting,Bagging与Stacking》
- 《XGBoost算法原理小结》