☰
机器学习股票预测实战:从特征工程到LSTM与LightGBM避坑指南
2026/10/2 21:13:26 网站建设 项目流程

简介:基于机器学习的股票预测方法研究论文文档,是一份面向毕业设计、课程设计及科研入门的参考资料。内容围绕股市预测这一交叉领域,系统梳理了从传统机器学习到深度学习、从单一历史数据到融合金融新闻与股民评论的演进脉络,涉及分段线性表示、高斯过程分类、随机森林、卷积神经网络、双重注意力机制、事件结构化表示及情感极性分析等关键方法和典型文献,逻辑严密、术语规范,适合初学者、工程师、在校师生阅读借鉴。资源为1个docx文件,压缩包大小35KB,便于下载后直接研读,目前已有586人学习。文档既可用于毕业论文或课程设计的框架参考,也可作为股票预测领域的前期调研提纲,能帮助读者快速掌握核心问题、主流模型与未来挑战,为后续项目开发或学术写作提供清晰的思路支持。

1. 机器学习预测股票,毕业设计里最容易被高估也最容易被做废的方向

打开任意一个论文检索系统,输入“股票预测”四个字,出来的结果里十个有八个是机器学习相关。这个题目之所以被毕设和课设反复选中,是因为它有清晰的数据来源、有公开的对比基准、有现成的第三方库可以直接调用,从开题到答辩的整个流程都有成熟套路。但反过来说,这个方向也是翻车重灾区:贴一张拟合完美的收益曲线,答辩时被问“你的测试集用了哪段数据”,当场答不上来的,每年都有。

这篇文章想解决的,不是“用机器学习炒股能不能赚钱”——那是另一个话题。这里聚焦的是:在你只有一只股票的日线数据、一台普通笔记本、两周到一个月开发时间的前提下,怎样把“基于机器学习的股票预测方法研究”做成一个站得住脚的论文项目。内容会覆盖数据处理、特征工程、模型选型、评估策略和隐藏最深的几个坑,整个过程按你打开这个标题后真正会遇到的操作顺序来讲。

2. 数据清洗与特征工程:预测模型的分数有一半在进模型之前就定了

2.1 拿到行情数据后先做这三件事:对齐、去缺口、定义标签

从Tushare、AkShare或者通达信导出的日线数据,表面上是规整的表格,实际上问题很多。第一个问题是复权方式不统一,前复权和后复权在同一段历史区间里计算出的涨跌幅完全不一样;第二个问题是停牌日留下的空行;第三个问题是除权除息导致的跳空缺口。这三件事不处理干净,后面所有模型结果都是失真的。

import pandas as pd import numpy as np df = pd.read_csv("600000.csv", parse_dates=["trade_date"]) df = df.sort_values("trade_date").reset_index(drop=True) # 1. 统一按后复权计算,避免除权跳空带来的假信号 df["adj_close"] = df["close"] * df["adj_factor"] # 2. 只保留连续交易的股票,剔除停牌超过10天的样本 df["date_gap"] = df["trade_date"].diff().dt.days df = df[df["date_gap"] <= 10].reset_index(drop=True) # 3. 定义预测标签:未来5日收益率是否为正 df["future_ret_5d"] = df["adj_close"].shift(-5) / df["adj_close"] - 1 df["label"] = (df["future_ret_5d"] > 0).astype(int) # 最后一行标签为空,直接丢弃 df = df.dropna(subset=["label"]).reset_index(drop=True)

这里的关键是后复权因子adj_factor,Tushare的日线接口会返回这个字段。很多人图省事直接用原始收盘价,遇到分红配股的股票,模型会把除权当暴跌学进去,这个错误非常隐蔽。标签生成上,我推荐用未来5日收益而非次日收益,因为次日预测的噪声极大,模型基本学不到有效信号,而5日窗口有平滑作用,答辩时也更容易解释。如果做回归任务,标签就保留future_ret_5d本身,不要转成0/1分类。

2.2 技术指标与量价特征:让特征工程不要堆砌出几十个高度相关的列

特征工程阶段最常见的错误是“指标越多越好”。常见做法是从TA-Lib或自写函数里生成MACD、RSI、KDJ、布林带等几十个指标,然后把所有列一股脑灌进模型。这些指标本质都是价格和成交量的不同变换,彼此之间相关度极高。LightGBM勉强能扛住,但LSTM这类模型面对冗余特征会严重过拟合,因为序列模型会把这些相关特征当成独立信息源。

我的做法是分三层构建特征:第一层是原始量价,包括开盘价、收盘价、最高价、最低价、成交量、成交额;第二层是技术指标,只保留MACD、RSI、BIAS这三个,分别代表趋势、超买超卖和偏离度;第三层是衍生特征,包括5日收益率、20日收益率、量比、换手率、振幅,以及一个关键的“相对大盘强弱”特征。

# 以一个特征为例:计算20日相对大盘强弱 df["ret_20d_stock"] = df["adj_close"].pct_change(20) df["ret_20d_index"] = index_df["adj_close"].pct_change(20) df["rel_strength"] = df["ret_20d_stock"] - df["ret_20d_index"] # 计算MACD,用默认参数就可,不必反复调优 ema_12 = df["adj_close"].ewm(span=12, adjust=False).mean() ema_26 = df["adj_close"].ewm(span=26, adjust=False).mean() df["macd"] = ema_12 - ema_26 # 特征列统一做标准化,注意这里的fit必须在训练集上完成 feature_cols = ["adj_close", "volume", "macd", "rsi", "bias", "rel_strength"] scaler = StandardScaler().fit(train_df[feature_cols]) train_df[feature_cols] = scaler.transform(train_df[feature_cols]) test_df[feature_cols] = scaler.transform(test_df[feature_cols])

最后一个标准化步骤是论文里最容易扣分的细节。标准化的均值和标准差只能用训练集计算,然后应用到测试集上。如果对全量数据做fit_transform,等于让模型在训练阶段就看到了测试集的分布信息,这在学术上叫数据泄漏,在答辩现场会被直接指出来。

2.3 特征筛选与时间窗口切片:给模型一个不偷看未来的训练集

特征做完之后,不要急着建模。先用一个简单的随机森林跑一遍,输出特征重要度,把重要度低于某个阈值的列删掉。这一步不是为了提升精度,而是为了在论文里多一张图、多一段分析,同时降低模型复杂度。

时间窗口切片是股票预测任务里最容易被忽略的环节。LSTM需要把连续序列切成固定长度的样本,一般用20到60个交易日作为时间步长。注意相邻样本之间有大量重叠(滑动窗口每次移动1天),这会造成训练集和验证集之间的信息重叠。严格的做法是让验证集的起始时间在训练集最后一天之后至少60个交易日,这样验证集里的任何样本都不会和训练集样本共享时间窗口。

3. 模型选型与训练策略:从LSTM到LightGBM,不同模型的适用边界

3.1 拿LightGBM当基准,再用LSTM做序列增强,这是最稳妥的组合

模型选择是毕业设计里最容易纠结的部分。很多同学一上来就直奔LSTM,觉得“深度学习+股票预测”听起来高级。但实际做下来你会发现,在几千到几万个样本量级上,LSTM的训练很不稳定,同样的代码跑两次结果差异很大,而且对特征标准化和初始化方式极度敏感。我的建议是做一个双模型对比:用LightGBM作为基线,用LSTM或GRU作为改进模型,这样的组合既保底又能体现工作量。

import lightgbm as lgb train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 31, "max_depth": 5, "feature_fraction": 0.8, "bagging_fraction": 0.8, "verbosity": -1, "seed": 42 } model = lgb.train( params, train_data, num_boost_round=500, valid_sets=[val_data], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)] )

这里有两个参数值得说明。feature_fraction设为0.8,每棵树随机采样80%的特征,这能显著降低过拟合;early_stopping在验证集AUC连续50轮不提升时停止训练。LightGBM完全不用做特征标准化,它对数值尺度不敏感,这也省了很多事。

3.2 LSTM要做的事情比想象中多:窗口长度、归一化、学习率都要单独调

LSTM的输入格式是(样本数, 时间步长, 特征数)。窗口长度我一般用20天,太短捕捉不到趋势,太长则样本量急剧减少。归一化这里必须强调:LSTM的输入输出都必须做标准化,而且是在训练集上拟合scaler再转换所有集合,跟第2章说的方法要贯彻到底。

import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm(input_shape): model = Sequential([ LSTM(64, return_sequences=True, input_shape=input_shape), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(16, activation="relu"), Dense(1, activation="sigmoid") ]) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss="binary_crossentropy", metrics=["accuracy"]) return model

learning_rate从0.001起步,如果损失在验证集上震荡,降到0.0005甚至0.0001。LSTM层从64开始,太大容易过拟合,太小则学不到序列特征。Dropout放在两个LSTM层之间,可以理解为让模型不要过度依赖某一个时间步的信息。这种结构在股票数据上不会刷出惊人的准确率,但它能稳定地表达“用近期趋势判断未来涨跌”的假设,这对论文来说是好事。

3.3 分类和回归,哪个更适合毕设?多数情况下分类更友好

股票预测有两种标签设计方式。分类是“未来5天涨还是跌”,回归是“未来5天收益率是多少”。分类的优点是评估简单,准确率、F1、AUC都能直接算,答辩时容易讲清楚;缺点是忽略了涨幅信息,大涨和大跌都被当成同一类。回归的优点是信息更完整,但评估指标很尴尬——预测收益率和真实收益率的相关系数通常只有0.05到0.1,这个数字在论文里不好看。

我的建议是:主题以分类为主,同时用回归做辅助分析。在论文里可以这样表述:分类模型用于判断方向,回归模型用于估计幅度,后者不作为主要评估依据。这种写法既增加了工作量展示,又避免了回归指标过低带来的尴尬。

4. 机器学习股票预测的避坑指南:数据泄漏、涨跌停与样本不均衡

4.1 数据泄漏:训练集准确率90%,实盘全废

这是整个股票预测方向里最致命的问题。现象是模型在训练集和验证集上都表现极好,AUC达到0.95以上,但换到新数据上预测效果和抛硬币差不多。原因几乎都是数据处理阶段引入了未来信息:用全量数据做归一化、标签计算时用了未来价格但没删掉最后几行、或者特征里包含了当天收盘后才知道的“未来”统计量。

解决办法是严格按时间顺序切分数据集。训练集用前70%的时间段,验证集用中间15%,测试集用最后15%,切分点之间至少留出60个交易日的缓冲区间。所有特征计算和预处理都必须在训练集内部完成后,再向验证集和测试集映射。

注意:做特征筛选时也要只用训练集数据。用全量数据算特征重要度再筛选,本质上也是一种数据泄漏。

4.2 涨跌停与停牌:模型会把“买不到”学成“不能买”

A股有涨跌停制度,涨停时买单无法成交,跌停时卖单无法成交。如果数据里有大量涨跌停样本,模型会学到“涨停后第二天大概率继续涨”这个规律,但这个规律对你来说没有可操作性——因为你在涨停板上根本买不进去。这在论文中会被答辩老师指出,而且确实会影响模型的真实可用性。

常见做法有两个:要么剔除涨跌停当天的样本,要么单独做一个标志位特征is_limit_up、is_limit_down。我倾向于后者,因为剔除样本会造成时间序列不连续,滑动窗口切片时会产生跨缺口的问题。单独加特征则让模型自己去学习不同状态下的转移规律,更合理。

4.3 样本不均衡:涨跌比接近1:1时,准确率没有参考价值

股票预测的标签分布在不同市场环境下差异很大。牛市里“涨”的样本可能占55%到60%,熊市里可能只有40%。如果不做处理,模型会倾向预测样本量大的那一类,准确率看起来还行,但实际上什么都没学会。

处理方法要克制。不建议用SMOTE做过采样,因为股票数据是时间序列,人为合成样本会破坏时间依赖结构,LSTM尤其敏感。我建议用类别权重:scale_pos_weight = 负样本数 / 正样本数,这个参数在LightGBM和XGBoost里可以直接设置。或者调低分类阈值——验证集上找F1最高的阈值,而不是默认的0.5。

4.4 评估指标的陷阱:准确率不是第一优先级的指标

如果验证集准确率是53%,而测试集准确率是51%,这种结果正常吗?太正常了。股票预测的难点在于信噪比极低,预测次日涨跌的准确率能达到55%就已经是相当强的信号了。伴随的问题是,准确率这个指标本身有误导性,因为它没有区分“预测大涨”“预测小涨”“预测大跌”各自的对错。

我推荐的评估组合是:AUC作为主要指标,F1-score作为辅助指标,方向准确率作为解释性指标。AUC不容易受阈值影响,能比较稳定地反映模型排序能力。在论文里可以画ROC曲线和累计收益曲线(按预测概率从高到低排序,做多Top 10%样本的累计收益),这两张图比单纯列准确率有说服力得多。

5. 把实验设计成论文能过审的样子:基准、消融与对照曲线

5.1 三组实验对照:从随机基线到模型对比再到特征消融

论文实验部分最怕只有一张结果表:LSTM准确率55%,LightGBM准确率53%,没了。这等于告诉老师你没有认真思考。完整的实验设计至少包含三组:第一组是随机基线(用上一日的收益率做预测,或者直接预测均值),第二组是模型对比(LightGBM、LSTM、以及它们的组合),第三组是消融实验(去掉某一类特征后模型效果的变化)。

# 随机基线:预测每个样本为正的概率等于训练集正样本占比 baseline_prob = y_train.mean() baseline_auc = roc_auc_score(y_test, np.full(len(y_test), baseline_prob)) # 跑三次独立实验,记录均值和标准差,避免“运气好”的质疑 from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=3) results = [] for train_idx, val_idx in tscv.split(X): # 每次用不同的训练/验证切分,单独训练并评估 model.fit(X.iloc[train_idx], y.iloc[train_idx]) pred = model.predict_proba(X.iloc[val_idx])[:, 1] results.append(roc_auc_score(y.iloc[val_idx], pred)) print(f"AUC: {np.mean(results):.4f} ± {np.std(results):.4f}")

注意这里用的是TimeSeriesSplit,不是普通的KFold。KFold随机打乱样本后切分,在时间序列数据上会造成严重的未来信息泄漏——训练集里混入了测试集时间之后的样本,模型相当于在“偷看未来”。这是毕设论文里最高频的错误之一,用错交叉验证方式会让整个实验设计被质疑。

5.2 消融实验怎么做:证明你加的每一个特征都不是摆设

消融实验的基本思路是:从完整特征集开始,每次删除一类特征,观察模型性能变化。例如完整特征集包含量价、技术指标、相对强弱三个维度,那就分别测试“去掉技术指标”“去掉相对强弱”和“去掉量价”三种情况,共四个模型。

提示:消融实验不追求每个结果都显著下降。如果去掉某个特征后模型性能不降反升,说明该特征在引入噪声,这同样是有价值的分析结论,可以写进论文的讨论部分,体现你的思考深度。

操作上注意:所有消融实验必须使用完全相同的模型参数、随机种子和训练切分方式。如果每次随机种子不一样,对比结果就没有意义。建议在代码开头固定np.random.seed(42)和random.seed(42),TensorFlow还需要设置tf.random.set_seed(42),才能保证实验可复现。

5.3 论文级图表:ROC曲线、累计收益曲线、回撤表

图表是毕业设计的加分项,比大段文字有效得多。需要准备四类图:第一个是ROC曲线,训练集和验证集各画一条,标注AUC数值;第二个是累计收益曲线,按预测概率从高到低排序,画前20%样本的累计收益对比买入持有;第三个是预测概率分布图,用直方图展示正负样本的预测值分布是否可分;第四个是特征重要度条形图。

累计收益曲线的计算代码如下:

# 按预测概率降序排列,取前20%作为“买入池” df_result = pd.DataFrame({"y_true": y_test, "pred": pred_prob}) df_result = df_result.sort_values("pred", ascending=False).reset_index(drop=True) top_20 = df_result.iloc[:int(len(df_result) * 0.2)] # 用未来5日真实收益计算累计收益 top_20["future_ret"] = top_20["y_true"].map(lambda x: actual_ret_5d[x.index]) cum_ret = top_20["future_ret"].sum()

这张图的含义是“如果只买入模型认为最有可能上涨的前20%交易日,累计收益是多少”,它比单看准确率更直观地展示了模型价值。注意图里一定要同时画买入持有曲线作为基准线,有对比才有说服力。

6. 验证模型的最后三件事:不看准确率,看这三条测试

6.1 换一只股票跑同一套完整流程

如果你用的训练数据是浦发银行,那训练结束后,不要只测试浦发银行的未来数据。把同一套特征工程和模型参数原封不动地套到另一只股票上(比如工商银行或招商银行),跑一遍完整流程,看测试集AUC。如果换股后AUC接近0.5,说明你的模型学到的可能是单只股票的序列特异模式,而不是泛化的市场规律。如果还能保持在0.53以上,那模型的说服力会显著增强。

df_other = load_stock_data("601398.csv") df_other = feature_engineer(df_other, scaler) # 复用之前训练好的scaler X_other = build_sequences(df_other, window=20) y_other = df_other["label"].values auc_other = roc_auc_score(y_other, lstm_model.predict(X_other)) print(f"换股验证AUC: {auc_other:.4f}")

这段代码里最容易被忽略的是复用了之前训练好的scaler,而不是重新fit_transform。如果重新拟合,等于把新股票的数据分布信息也泄漏进了模型,验证结果就不可信了。

6.2 随机打乱标签顺序做一次对照实验

把训练集和测试集的标签顺序完全随机打乱,重新训练一次,记录AUC。这一步听起来反直觉,但它是检验模型是否真正学到序列规律的试金石。如果打乱标签后模型的AUC和原始数据的AUC差不多,说明原始结果很可能是数据泄漏或巧合导致的,而不是模型真的学到了规律。

y_train_shuffled = np.random.permutation(y_train) model.fit(X_train, y_train_shuffled) auc_shuffled = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) print(f"打乱标签后的AUC: {auc_shuffled:.4f}")

6.3 一个回测习惯:定期更新模型比调参更重要

实验做完之后,最后一个习惯是模拟真实使用场景:每30个交易日重新训练一次模型,把前一个批次的数据纳入训练集,然后预测下一个批次的收益方向。这个滚动训练过程在论文里可以作为“模型更新策略”章节,很多评委喜欢看到这种贴近实际应用的设计。我自己做完这套流程的最大体会是:与其花大量时间微调学习率和网络层数,不如把精力花在特征设计和数据清洗上——前者带来的提升很难超过2%,后者做好了能直接把AUC从0.52推到0.56以上,而且每一个处理步骤都能写进论文,成为说得清道得明的贡献点。希望这些经验对你能有实际帮助,动手跑一遍,比读十篇文献都有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询