简介:这是一份面向金融预测与机器学习学习者的工程硕士学位论文PDF,主题为基于XGBoost的混合模型在股票预测中的应用研究。压缩包内共1个PDF文件,大小约3.48MB,正文覆盖绪论、股票预测基础理论、集成学习算法(CART、GBDT、XGBoost)与时间序列模型(AR、MA、ARIMA)等完整章节,结构清晰。已有656人学习浏览。内容不仅对比了单一模型与混合模型的差异,还详细描述了特征工程、参数调整、MSE/MAE/R²分数等评价指标,以及将XGBoost与随机森林、支持向量机结合进行集成预测的实验过程。读者可从中获得完整的模型构建与调优思路、实验设计与结果分析方法,并了解股票非线性数据预测中的常见问题与改进方向。
1. 股票预测的混合模型:为什么单模型总是差一口气
做股票预测的人多半都经历过这种状态:用 ARIMA 拟合得挺顺滑,一到拐点就追不上;换成 XGBoost,非线性学得动,但遇到强的趋势项又开始过拟合。基于 XGBoost 的混合模型在股票预测中的应用研究这篇论文,解决的正是这个“单模型各有脾气”的问题。它的思路很直接:先用离散小波变换把噪声和趋势拆开,再用 ARIMA 抓线性骨架,XGBoost 抓非线性残差,最后用网格搜索把两组模型的超参数一起调明白。适合正在做金融时序预测、量化研究或者机器学习毕业设计的读者,尤其适合那些被单一模型反复折磨的人。
2. 理论与选型:XGBoost、ARIMA、DWT 到底解决什么问题
2.1 集成学习路线:从 CART 到 XGBoost
股票数据里最明显的特点就是“非线性”和“交互项”。比如某只股票的涨跌可能同时受到前三天涨跌幅、成交量变化、大盘指数波动的影响,而且这些影响不是简单叠加。线性模型抓不住这类关系,传统的单棵决策树又容易过拟合,于是集成学习成了绕不开的选择。
决策树本身是个贪心算法:每次选一个特征和阈值,把样本按某种纯度指标二分,递归到叶子节点。单棵树的毛病是方差大、容易碎,但把很多棵树放在一起投票或加权,方差就能被压下来。GBDT(梯度提升决策树)走的是另一条路:每一棵树拟合的并不是原始标签,而是前面所有树的残差。这样一来,模型在每一轮都在修正之前的错误。XGBoost 在 GBDT 基础上做了三件关键的事:目标函数里加入正则项控制树复杂度,用二阶泰勒展开逼近损失函数,支持列采样和并行化。正则项直接惩罚树的叶节点数和权重,这让 XGBoost 在高维特征、弱相关性数据上的泛化能力明显强于早期 GBDT 实现。
从选型角度说,股票预测场景里特征之间往往存在较强的耦合,比如均线金叉、量价背离这类组合信号,单棵决策树很难表达。XGBoost 的自适应分裂机制会在每棵树上自动寻找这些耦合点。相比 SVM 和随机森林,XGBoost 对缺失值不敏感,内置了缺失值分支方向学习,而股票数据因为停牌、复牌经常出现空值,这一特性很实用。
2.2 时间序列路线:ARIMA 的适用边界
ARIMA 是经典的时间序列线性建模工具。它把非平稳序列先做 d 阶差分变成平稳序列,再对差分后的序列建 AR(自回归)和 MA(移动平均)的组合模型。
AR 部分描述的是当前值与过去 p 个值之间的线性关系:
# AR(p) 伪代码示意 y_t = phi_1 * y_{t-1} + phi_2 * y_{t-2} + ... + phi_p * y_{t-p} + eps_tMA 部分描述的是当前值与过去 q 个预测误差之间的线性关系:
# MA(q) 伪代码示意 y_t = eps_t + theta_1 * eps_{t-1} + theta_2 * eps_{t-2} + ... + theta_q * eps_{t-q}两者合并就是 ARMA,加上差分就是 ARIMA(p,d,q)。ARIMA 最大的优势是解释性强、计算快、在小样本上表现稳。但它的硬边界也很清楚:它假设序列可以经过差分变成线性平稳序列,可股票价格受外部消息、政策、情绪影响,这种非线性突变不是一阶差分能消除的。所以单独用 ARIMA 做股票预测,往往只适合短周期、强趋势的行情,一旦进入震荡市,残差里会剩下大量非线性信号。
论文里把 ARIMA 定位成“抓线性骨架”的角色,而不是最终答案。这个定位很关键:先让 ARIMA 把趋势和周期吃进去,剩下的残差交给 XGBoost,两个模型的优势各管一段,各不越界。
2.3 离散小波变换:降噪与分解的价值
股票价格序列里有大量高频噪声,来源于日内交易摩擦、市场情绪波动、数据采集误差。如果直接把这种噪声喂给 XGBoost,模型很容易去拟合噪声里的随机模式,导致验证集上看着不错,换到下一个时间段就失效。
离散小波变换(DWT)把原始信号分解成一个低频近似分量和多个高频细节分量。低频近似分量对应趋势和主要周期,高频细节分量对应噪声和短期波动。对高频细节做阈值收缩后再重构,就能得到一条更“干净”的价格序列。
选择小波而不是普通低通滤波的原因在于:小波变换在时域和频域同时保有局部信息。它能把某一天的“跳空缺口”和高频噪声区分开,而傅里叶变换只做频域分解,遇到突变点会振铃。论文里用的离散小波变换也是这个逻辑——先降噪,再进模型,让模型学的不是噪声,而是真正的价格驱动因素。
小波变换的实操里有几个自由参数:小波基函数类型(db1、db4、sym5 等)、分解层数、阈值方式。这些参数不是随便选的。后面第 4 章会专门讨论它们怎么设、设错了会出什么问题。
2.4 网格搜索:超参数寻优的笨办法为何有效
XGBoost 的超参数很多:树的数量 n_estimators、树深 max_depth、学习率 learning_rate、特征采样比例 colsample_bytree、样本采样比例 subsample、正则参数 lambda 和 alpha。每个参数对模型性能的影响不是独立的,比如学习率调低之后往往要增加树的数量才能保持拟合能力。这种耦合关系让“逐个参数调优”的贪心策略很容易陷入局部最优。
网格搜索(Grid Search)是最笨也最可靠的办法:把候选参数组合成一个笛卡尔积空间,逐组训练交叉验证,选平均得分最高的参数。它的计算量随参数数量指数增长,但优点是结果可复现、可解释,而且不依赖人工经验。论文里的 GS-XGBoost 就是把这个思路搬到了 XGBoost 上,先设定参数组合区间,循环训练并用评价函数打分,最终拿最优参数重新训练模型。
现在有很多贝叶斯优化工具声称比网格搜索更快,但在股票预测这种数据量不大、参数空间可控的场景下,网格搜索的稳定性反而更好。贝叶斯优化可能因为前几次评估碰巧遇到差参数,导致采样方向偏掉;网格搜索至少能保证覆盖整个区间,不会遗漏角落里的好参数。
3. GS-XGBoost 实战:网格搜索怎么把参数调明白
3.1 参数空间与流程设计
复现 GS-XGBoost 的第一步不是写代码,而是设计参数空间。我一般会根据数据量来定范围:数据只有几百条时,树的数量不用超过 200,树深超过 5 就容易把噪声也背下来;数据上千条,可以把 max_depth 放开到 7,再加一点 subsample 缓解过拟合。
论文的核心参数组合可以整理成一张表:
| 参数 | 候选值 | 作用 | 调整倾向 |
|---|---|---|---|
| n_estimators | 50, 100, 200 | 树的数量 | 学习率低时取大,避免欠拟合 |
| max_depth | 3, 5, 7 | 树的深度 | 数据量少时取小 |
| learning_rate | 0.01, 0.05, 0.1 | 步长收缩 | 越小越稳,但需要更多树 |
| subsample | 0.7, 0.8, 1.0 | 样本采样比例 | 过拟合明显时往下调 |
| colsample_bytree | 0.7, 1.0 | 特征采样比例 | 特征冗余时往下调 |
整个流程是:先对原始价格数据做特征工程,构造历史收益率、移动平均、成交量变化等特征;然后把训练集和测试集按时间顺序切分;接着用 TimeSeriesSplit 做交叉验证,遍历参数组合,选出 RMSE 最低的一组;最后用最优参数在完整训练集上重训模型,在测试集上评估。
3.2 代码实现:训练、评估与对比
下面这段代码是 GS-XGBoost 的核心部分,我用的是 xgboost、scikit-learn 和 pandas:
import pandas as pd import numpy as np from xgboost import XGBRegressor from sklearn.model_selection import GridSearchCV, TimeSeriesSplit from sklearn.metrics import mean_squared_error, mean_absolute_error # 构造特征:假设 df 中已有 close 和 volume 两列 df['return'] = df['close'].pct_change() # 当日收益率 df['ma5'] = df['close'].rolling(5).mean() # 5日均线 df['ma10'] = df['close'].rolling(10).mean() # 10日均线 df['volume_ratio'] = df['volume'] / df['volume'].rolling(5).mean() feature_cols = ['return', 'ma5', 'ma10', 'volume_ratio'] X = df[feature_cols].iloc[10:] # 跳过滚动窗口前无效行 df['target'] = df['close'].shift(-1) # 预测下一日收盘价 y = df['target'].iloc[10:-1] # 按时间顺序切分,前 70% 训练,后 30% 测试 split_idx = int(len(X) * 0.7) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] # 时间序列交叉验证:不能用普通 KFold,会打乱时间顺序 tscv = TimeSeriesSplit(n_splits=5) param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [3, 5, 7], 'learning_rate': [0.01, 0.05, 0.1], 'subsample': [0.7, 0.8, 1.0], } model = XGBRegressor(random_state=42) search = GridSearchCV( model, param_grid, cv=tscv, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1 ) search.fit(X_train, y_train) print('Best params:', search.best_params_) best_model = search.best_estimator_ y_pred = best_model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) print(f'RMSE: {rmse:.4f}, MAE: {mae:.4f}')逻辑说明:代码先用 5 日均线和 10 日均线构造趋势特征,再用成交量与 5 日均量的比值构造量能特征。目标值df['close'].shift(-1)表示用今天及之前的数据预测明天收盘价。这里有个关键点必须注意:iloc[10:-1]里的-1删掉了最后一行没有目标的样本,而iloc[10:]删掉了前 10 行因滚动窗口产生 NaN 的行。
参数说明:TimeSeriesSplit是时间序列交叉验证的标准做法,它保证训练集永远在测试集之前,不会把未来的数据泄露给模型。scoring='neg_mean_squared_error'是因为 GridSearchCV 默认按“越大越好”选择参数,负 MSE 取最大值等价于取最小 MSE。n_jobs=-1表示用全部 CPU 核并行搜索,参数空间有 108 组组合,5 折交叉验证就是 540 次训练,不开并行会很慢。
3.3 结果解读:为什么网格搜索有效
复现之后我通常会把默认参数的 XGBoost 和 GS-XGBoost 放在同一张测试集上对比。常见的结果是默认参数跑出来的 RMSE 偏高,原因是默认max_depth=6在数据量不大的股票数据上容易过拟合。网格搜索选出的参数往往偏向max_depth=3或5、learning_rate=0.05这类更保守的组合。
网格搜索的另一个隐性收益是它强迫你观察参数对模型的敏感度。比如最优参数里subsample=0.8而不是1.0,说明样本里存在不少异常值,采样降一点反而更稳。这种信息是黑匣子调参给不了的。
4. DWT-GS-XGBoost 实战:先降噪再预测的收益与边界
4.1 小波分解与重构实操
如果说网格搜索是给模型内部做手术,小波降噪就是在数据入口加一道净化筛网。股票价格序列的噪声比例高,直接喂给 XGBoost 会让树分裂时优先选择那些碰巧跟噪声对齐的特征切分点。DWT 的作用就是把这些高频噪声先卸掉。
小波降噪的代码流程很固定,我用pywt库来实现:
import pywt import numpy as np def denoise_signal(signal, wavelet='db4', level=3): """ 对输入序列做离散小波分解,软阈值去噪,再重构。 signal: 1D numpy array,通常是收盘价序列。 wavelet: 小波基函数类型,db4 是 Daubechies 4 小波。 level: 分解层数,决定多细的高频成分被视为噪声。 """ # 离散小波分解:返回低频系数 + 各层高频系数 coeffs = pywt.wavedec(signal, wavelet, level=level) # 用最细一层高频系数的标准差估算噪声阈值 sigma = np.std(coeffs[-1]) threshold = sigma * np.sqrt(2 * np.log(len(signal))) # 保留低频近似分量,对每层高频细节做软阈值收缩 coeffs_th = [coeffs[0]] for c in coeffs[1:]: coeffs_th.append(pywt.threshold(c, threshold, mode='soft')) # 用处理后的系数重构信号 return pywt.waverec(coeffs_th, wavelet)逻辑说明:wavedec把一条价格序列拆成一个近似系数(低频趋势)和多个细节系数(不同尺度的高频波动)。噪声主要集中在细节系数里,所以用细节系数的标准差估算噪声强度,然后算一个自适应阈值。pywt.threshold的soft模式会把系数向零收缩,而不是直接切成零,这样重构出来的曲线更平滑,不会出现突兀的阶跃。
参数说明:wavelet='db4'是常见的 Daubechies 家族小波,波形比较平滑,适合价格序列这种没有剧烈振荡的信号。level=3意味着分解 3 层,最细一层粒度是原始序列的一半,第二层四分之一,第三层八分之一。层数太浅降噪不干净,层数太深会把真实的趋势拐点当成噪声滤掉。论文里的实验没有意外的话走的是这个流程:先 DWT 重构出去噪序列,再和 GS-XGBoost 衔接训练。
4.2 代码实现:先切分再降噪
这里有个很多复现者都会栽跟头的细节:小波分解是全局变换,如果先对完整序列做 DWT 再切分训练集和测试集,测试集的信息其实已经参与到了训练部分的降噪中。这是典型的未来信息泄露,会让测试集指标虚高。正确做法是先按时间切分,再分别对训练段和测试段做 DWT。
# 错误做法:全量降噪后切分,测试集信息泄露 denoised_full = denoise_signal(df['close'].values, level=3) df['close_denoised'] = denoised_full # 正确做法:先切分,再降噪 train_close = df['close'].iloc[:split_idx].values test_close = df['close'].iloc[split_idx:].values train_denoised = denoise_signal(train_close, level=3) test_denoised = denoise_signal(test_close, level=3)逻辑说明:先切分再降噪意味着测试集的噪声水平独立于训练集估计。这个做法的代价是测试段首尾边缘的小波重构会有边界效应,但相比信息泄露,这点代价完全可接受。如果测试段太短,重构出的边缘波形会失真,可以考虑对测试段做小幅扩展再截断。
参数说明:split_idx和第 3 章保持一致,同时要注意降噪之后特征构造的顺序不能乱:先降噪,再计算收益率和均线,这对避免“噪声被特征放大”很有帮助。很多复现者先算收益率再降噪,结果收益率序列的噪声尺度被小波误判成真实信号,降噪效果大打折扣。
4.3 实验对比:降噪带来了什么
论文里 DWT-GS-XGBoost 对比 GS-XGBoost 的结论是预测效果提升。实际复现时,这个提升主要来自对测试集 RMSE 的改善,尤其是价格剧烈波动的那几天。小波降噪把异常跳空修成了相对平滑的过渡,模型不会为了拟合一个孤立的极端值而扭曲整棵树的结构。
但降噪不是万能的。如果股票处于持续放量突破行情,真实的价格突变会被误判为噪声滤掉,导致模型预测滞后。我复现时习惯对比两组 RMSE:一组用去噪序列训练、原始序列评估,另一组用去噪序列训练、去噪序列评估。如果前者远差于后者,说明降噪过度,需要降低 level 或换更短的小波基。
5. 避坑与排查:DWT-ARIMA-GSXGB 复现中的 5 个关键问题
5.1 模型怎么拼
DWT-ARIMA-GSXGB 是论文里最复杂的组合,它的思路不是简单地把三个模型预测结果加权平均,而是接力配合。第一步用 DWT 对原始价格序列降噪;第二步用 ARIMA 拟合去噪序列中的线性趋势,得到 ARIMA 预测值;第三步计算原始价格与 ARIMA 预测值的残差,这个残差里主要剩下非线性成分;第四步用 GS-XGBoost 拟合残差,学习残差中的非线性规律;最后把 ARIMA 的预测和 XGBoost 的残差预测相加,得到最终结果。
这个结构的核心逻辑是:ARIMA 负责“趋势跟随”,XGBoost 负责“误差修正”。如果残差序列接近白噪声,XGBoost 学不到有效信号,相加之后甚至可能不如单纯 ARIMA,这时候就需要检查组合是否成立。
5.2 常见问题与避坑记录
现象:验证集指标非常好,但换到另一段时间窗口后模型预测结果崩掉。 原因:先对全量数据做 DWT,再做特征工程和切分,测试集信息混入了小波系数估计过程,属于未来信息泄露。 解决:严格按“先切分、再降噪、再构造特征”的顺序走,测试段独立降噪。
现象:网格搜索最优参数永远落在参数空间的边界上,比如 n_estimators 取 200 还是不够。 原因:参数范围设得太窄,边界值只是被强行截断。常见于数据量大、学习率低但树的上限没放开。 解决:跑完第一次网格后检查
best_params_是否触碰边界,如果是,把该参数范围扩大一倍再跑一次,直到最优参数落在区间内部。现象:ARIMA 残差序列画出来仍有明显的周期性或趋势,ADF 检验 p 值大于 0.05。 原因:ARIMA 的 d 阶数不足,或者 DWT 分解层数太少,没把非平稳成分彻底滤掉。 解决:对残差做 ADF 检验,p 值不达标就尝试增加 ARIMA 的差分阶数 d,或者把 DWT 的 level 加一层。不要急着上 XGBoost,残差里还有线性结构时,XGBoost 拟合的其实是本应由 ARIMA 负责的部分。
现象:最终混合模型预测结果比单一 ARIMA 还差。 原因:最常见的是残差里已经没有可用信号,XGBoost 硬拟合出一堆噪声模式;其次是 XGBoost 训练集特征和目标之间存在时序错位。 解决:训练 XGBoost 之前,先对残差序列做自相关分析(ACF/PACF 图)。如果残差自相关系数都落在置信区间内,说明残差是白噪声,直接放弃 XGBoost 环节,用 ARIMA 结果作为最终预测。
现象:同一套参数在训练集上 RMSE 很低,但实盘或后续时间段回测表现不稳定。 原因:金融序列非平稳,过去的最优参数未必适应当前市场状态,也可能训练集里包含了强趋势行情,模型过度拟合了那一波走势。 解决:采用滚动重训策略,每隔一段时间窗口用最近的数据重新做一次网格搜索,而不是一套参数用到头。
5.3 结果对比怎么看
论文的结论是 DWT-ARIMA-GSXGB 比单一 ARIMA、单一 XGBoost 都有明显优势,主要体现在拟合能力和泛化能力上。复现时验证这个结论有一个技巧:不要只对比一种评价指标。RMSE 可能因为少数几天的极端值而大幅波动,MAE 对异常值更鲁棒,MAPE 看的是相对误差。如果三个指标同时改善,才能说混合模型确实有效。
6. 复现验证的细节技巧:评价指标、数据切分与结果解读
6.1 评价指标不要只看 RMSE
单看 RMSE 会掩盖模型的真实表现。我习惯同时算 RMSE、MAE、MAPE 和方向准确率 DA:
from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate(y_true, y_pred): rmse = np.sqrt(mean_squared_error(y_true, y_pred)) mae = mean_absolute_error(y_true, y_pred) mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8))) * 100 # 方向准确率:预测值相邻时刻的涨跌方向是否一致 pred_diff = np.sign(np.diff(y_pred)) true_diff = np.sign(np.diff(y_true)) da = np.mean(pred_diff == true_diff) return {'RMSE': rmse, 'MAE': mae, 'MAPE': mape, 'DA': da}逻辑说明:np.diff计算相邻时刻的差值,np.sign把差值映射到 一1、0、1,对比预测和真实的方向。方向准确率能反映模型在“明天涨还是跌”这件事上的能力,对实际决策比 RMSE 更直观。参数说明里用y_true + 1e-8是为了防止价格序列出现 0 值导致除零。
6.2 基线模型先跑三个
复现这套混合模型之前,我会强制自己先跑三个简单基线:随机游走(用今天的收盘价预测明天)、ARIMA(p,d,q)、默认参数 XGBoost。混合模型如果在这三个基线面前没有显著改善,说明问题大概率出在特征工程或者数据切分上,而不是模型组合本身。从那以后我每次做金融时序预测都强制走一遍这个基线对比流程,这个习惯帮我筛掉过好几个表面光鲜、实际没用的模型。希望帮到你。
本文还有配套的精品资源,点击获取