1. 为什么股价预测这么难?从KNN到LSTM的实战心路
大家好,我是老张,一个在金融数据和AI模型里摸爬滚打了十来年的“老码农”。今天想和大家聊聊一个既让人着迷又让人头疼的话题——用Python预测股价。就拿平安银行这只股票来说,它的价格走势就像一条蜿蜒的山路,充满了不确定性。你可能试过看K线图、听消息,甚至用一些简单的指标去猜,但结果往往不尽如人意。我自己也踩过不少坑,从最基础的移动平均线,到传统的机器学习模型,再到如今火热的深度学习,一路试过来,感触颇深。
这次,我决定把这段“踩坑”和“填坑”的经历系统地梳理出来,目标很明确:用Python实战对比KNN和LSTM这两种代表性模型,看看它们在预测平安银行股价这件事上,到底谁更胜一筹,以及我们该怎么去调优它们。 这不仅仅是一个模型性能的对比,更像是一次从“传统思维”到“现代方法”的技术演进之旅。KNN(K-最近邻)算是机器学习里非常直观的“老将”,而LSTM(长短期记忆网络)则是深度学习处理时间序列的“明星”。通过这个对比,我希望不仅能给你展示代码怎么跑,更能帮你理解在不同场景下该如何选择模型,以及如何通过调参让模型发挥出真正的实力。无论你是刚入门数据分析的新手,还是想深化模型理解的量化爱好者,相信这篇充满实战细节的分享都能给你带来启发。
2. 数据获取与预处理:一切分析的基础
做量化分析,数据是地基。地基不稳,后面盖的模型大楼再漂亮也容易塌。我这次用的数据来自Tushare Pro平台,获取了平安银行(股票代码:000001.SZ)从2012年底到2022年中的日线行情数据。Tushare这个工具确实省心,它把数据清洗和整合的脏活累活都干了,我们直接调用接口就能拿到相对规整的数据,这对初学者特别友好。
2.1 拿到数据后先“望闻问切”
数据到手,千万别急着往模型里喂。我习惯先来个“体检”。用pandas读入数据后,第一件事就是看看有没有缺失值、异常值。幸运的是,Tushare提供的数据质量很高,基本没有缺失。但有一个关键步骤不能省:日期格式转换。原始数据里的交易日期通常是‘20220610’这样的整数格式,我们必须把它转换成Python的datetime格式,这样后续按时间排序、划分数据集、画图才会准确无误。
这里我写了个小函数来处理这个转换,算是基本功,但很重要:
import pandas as pd
from datetime import datetime
def convert_date_int_to_datetime(series):
"""将整数格式的日期序列转换为datetime格式"""
# 先转成字符串,再按照格式解析为datetime
return pd.to_datetime(series.astype(str), format='%Y%m%d')
# 假设df是读取的DataFrame,包含‘trade_date’列
df['trade_date'] = convert_date_int_to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True) # 将日期设为索引,方便时间序列操作
完成转换后,把日期列设置为数据索引,后续所有基于时间的操作就都顺理成章了。
2.2 特征选择与数据划分的讲究
接下来是特征工程。对于股价预测,我们最关心的目标变量(y)通常是收盘价,因为它决定了当天的最终盈亏。那么特征(X)用什么?对于时间序列预测,最直接的特征就是历史价格序列本身。也就是说,我们用过去N天的价格(比如过去60天)来预测下一天的价格。这个“N”就是时间窗口,是一个非常重要的超参数,后面调优时会重点讲。
数据划分上,时间序列数据和普通的随机数据不同,必须严格按照时间先后顺序划分,不能打乱。我通常会把较早的数据(比如2012-2021年)作为训练集,用来让模型学习规律;把最近的数据(2022年上半年)作为测试集,用来评估模型在“未来”的预测能力。这更符合真实的投资场景——我们总是用历史数据训练模型,去预测还没发生的价格。
划分完,我强烈建议你把训练集和测试集的收盘价走势图画出来,直观感受一下数据的整体趋势和波动,这对后续理解模型表现很有帮助。
3. 传统守门员:KNN模型的实现与初次尝试
在请出LSTM这位“大咖”之前,我们先来看看一位朴实但经典的“守门员”——KNN(K-Nearest Neighbors,K最近邻)算法。它的思想非常直观:要预测一个点的值,就在训练集里找跟它最像的K个“邻居”,然后用这K个邻居值的平均值(回归任务)作为预测结果。在股价预测里,“像”就是指历史价格序列的形态相似。
3.1 KNN用于时间序列预测的改造
标准的KNN算法处理的是特征独立的样本点,但股价是连续的时间序列。所以我们需要动个小手术:构建时间窗口样本。具体来说,我们把连续的股价序列,转换成一个个固定长度(比如60天)的滑动窗口。每个窗口内的60个价格,就构成了一个样本的特征向量,而这个窗口之后那一天的收盘价,就是我们要预测的目标值。
import numpy as np
from sklearn.neighbors import KNeighborsRegressor
from sklearn.preprocessing import MinMaxScaler
# 假设‘close_prices’是归一化后的收盘价序列
def create_dataset(data, time_step=60):
X, y = [], []
for i in range(len(data)-time_step-1):
X.append(data[i:(i+time_step)]) # 特征:过去time_step天的价格
y.append(data[i + time_step]) # 标签:下一天的价格
return np.array(X), np.array(y)
time_step = 60
X_train, y_train = create_dataset(train_scaled, time_step)
X_test, y_test = create_dataset(test_scaled, time_step)
# 初始化KNN回归模型,先试试K=5
knn_model = KNeighborsRegressor(n_neighbors=5, weights='distance')
knn_model.fit(X_train, y_train)
knn_predictions = knn_model.predict(X_test)
这里有几个关键点:第一,数据最好先做归一化(比如缩放到0-1之间),因为KNN基于距离计算,不同尺度的特征会影响距离权重。第二,weights='distance'参数意味着距离越近的邻居权重越大,这通常比简单平均(weights='uniform')效果更好。
3.2 KNN的初次表现与局限性分析
当我第一次用默认参数(K=5)跑完KNN模型,并在测试集上画出预测曲线时,结果有点让人沮丧。预测线就像一条反应迟钝的“懒虫”,紧紧贴着股价波动的均值线,对于股价的快速上涨和下跌几乎没有任何提前反应的能力。计算出的RMSE(均方根误差)值也比较高,这说明预测误差较大。
我仔细想了想,这其实暴露了KNN在股价预测上的几个天生短板:
- 缺乏时序记忆:KNN把每个时间窗口当成独立的样本,完全忽略了窗口内部以及窗口之间在时间上的连续性和依赖关系。它不知道“昨天涨了,所以今天可能继续涨”这种趋势。
- 对噪声敏感:股价数据噪声很大,KNN寻找的“相似邻居”可能只是噪声模式偶然相似,而不是真正的趋势相似。
- 参数K的尴尬:K值太小,模型容易受噪声影响,预测波动剧烈;K值太大,模型又会过度平滑,完全跟不上变化,就像我最初看到的那样。
尽管表现不佳,但这次尝试并非没有价值。它给了我一个重要的基准线,也让我更深刻地认识到,对于金融时间序列这种强时序依赖、高噪声的数据,我们需要更强大的武器。
4. 时序预测的明星:深入理解与构建LSTM模型
既然传统方法力有不逮,那就请出我们今天的主角——LSTM(长短期记忆网络)。它是循环神经网络(RNN)的一个变种,专门设计用来解决长期依赖问题,可以说是为时间序列数据“量身定制”的。
4.1 LSTM的核心:三道“门”的故事
你可以把LSTM单元想象成一个有“记忆”的智能流水线,它通过三道精巧的“门”来控制信息的流动:
- 遗忘门:决定从之前的长期记忆(细胞状态)中丢弃哪些信息。比如,它可能学会在遇到重大财报发布时,忘记一些陈旧的无用信息。
- 输入门:决定当前的新输入信息有多少值得存入长期记忆。它像是一个信息过滤器。
- 输出门:基于当前的长期记忆和输入,决定最终输出什么信息(即当前的隐藏状态),这个输出会用于预测,并传递给下一个时间步。
正是这三道门的协同工作,使得LSTM能够有选择地记住重要的长期信息,同时忽略无关的噪声,这对于捕捉股价的长期趋势和短期波动至关重要。相比普通RNN,LSTM能有效缓解梯度消失/爆炸问题,从而能够学习更长时间跨度的依赖关系。
4.2 用Keras搭建你的第一个LSTM预测模型
理论可能有点绕,但用Keras实现起来却相对直观。下面是我构建的一个基础LSTM网络结构,也是我们调优的起点:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping
# 构建序列模型
model = Sequential()
# 第一层LSTM,设置50个神经元,return_sequences=True意味着输出完整序列,以便连接下一层LSTM
model.add(LSTM(units=50, return_sequences=True, input_shape=(X_train.shape[1], 1)))
model.add(Dropout(0.2)) # 添加Dropout层防止过拟合,随机丢弃20%的神经元
# 第二层LSTM,不需要返回序列
model.add(LSTM(units=50, return_sequences=False))
model.add(Dropout(0.2))
# 全连接层,将LSTM的输出映射到最终的预测值(一个数值)
model.add(Dense(units=1))
# 编译模型,使用均方误差作为损失函数,Adam优化器
model.compile(optimizer='adam', loss='mean_squared_error')
# 设置早停法,如果验证集损失连续5个epoch不下降,就停止训练,防止过拟合
early_stop = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
# 训练模型
history = model.fit(X_train, y_train,
epochs=50, # 迭代轮数
batch_size=32, # 批大小
validation_split=0.1, # 从训练集中分出10%作为验证集
callbacks=[early_stop],
verbose=1)
这个模型虽然简单,但包含了几个关键组件:两层LSTM以增强特征提取能力,Dropout层用于正则化,以及EarlyStopping回调来智能控制训练轮数。第一次运行这个模型,你可能会发现它的预测曲线比KNN“灵动”多了,开始能够捕捉到一些主要的波动趋势。
5. 模型调优实战:如何让LSTM发挥真正威力?
第一次跑通的LSTM模型可能表现不错,但远未达到最优。模型调优就像给赛车做精细改装,每一个参数的调整都可能带来性能的提升。下面分享几个我实践中觉得最有效的调优方向。
5.1 超参数搜索:找到模型的“最佳状态”
超参数是训练开始前就设定好的参数,它们对模型性能影响巨大。手动调参效率低,我们可以借助GridSearchCV或RandomizedSearchCV进行系统搜索。主要调这几个:
- 时间窗口长度:用过去多少天的数据来预测明天?太短(如10天)可能看不清趋势,太长(如200天)可能包含太多无关噪声。我通常在30到120天之间搜索。
- LSTM层神经元数量:代表模型的容量。太少(如20)可能欠拟合,太多(如200)容易过拟合。可以从50、100、150开始试。
- 网络层数:更深不一定更好。对于股价预测,1到3层LSTM通常足够。层数增加会急剧提升训练时间和过拟合风险。
- Dropout比率:防止过拟合的利器。一般在0.1到0.5之间尝试。
- 批大小和优化器学习率:
batch_size影响梯度下降的稳定性,learning_rate影响收敛速度和精度。Adam优化器的默认学习率(0.001)是个不错的起点。
我常用KerasRegressor包装模型,然后结合scikit-learn的搜索功能进行调优。这个过程比较耗时,但绝对是值得的。
5.2 特征工程进阶:给模型更多“情报”
只喂历史收盘价,模型就像只通过后视镜看路。我们可以尝试加入更多特征,给模型更多“情报”:
- 技术指标:计算移动平均线(MA)、相对强弱指数(RSI)、布林带(Bollinger Bands)等,作为额外的特征列。这些指标本身是市场经验的总结。
- 成交量:价量关系是技术分析的核心。把每日成交量(甚至成交额)作为特征输入,模型可能学到“放量上涨”或“缩量下跌”的模式。
- 外部数据:比如市场情绪指数、相关宏观经济指标等。但这部分数据获取和清洗更复杂,需要谨慎评估其有效性。
添加新特征后,需要重新调整数据预处理和模型输入层的维度。记住,特征不是越多越好,无效或冗余的特征反而会干扰模型学习。
5.3 应对过拟合:让模型学会“泛化”
金融数据噪声大,LSTM又很强大,过拟合是头号敌人。除了前面提到的Dropout和EarlyStopping,还有几个利器:
- L1/L2正则化:在LSTM层或Dense层添加
kernel_regularizer,惩罚大的权重,使模型更简单。 - 增加训练数据:如果可能,获取更长时间跨度的历史数据。
- 简化模型结构:有时减少一层LSTM或减少神经元数量,模型在测试集上的表现反而更好。
- 监控训练过程:一定要画出每个epoch的训练损失和验证损失曲线。如果训练损失持续下降而验证损失很早就开始上升,那就是典型的过拟合信号。
6. 终极对决:KNN与LSTM的表现深度对比
经过一番调优,现在是时候让两位选手在平安银行股价预测的擂台上正式较量一下了。为了公平,我使用相同的训练集和测试集,并且对两个模型都进行了力所能及的参数调优。
6.1 量化指标对比:数字不说谎
我选取了三个核心评估指标:均方根误差(RMSE)、平均绝对误差(MAE) 和 平均绝对百分比误差(MAPE)。RMSE对大误差惩罚更重,MAE更直观,MAPE则反映了百分比误差,便于理解。
| 模型 | RMSE | MAE | MAPE | 训练速度 | 调参复杂度 |
|---|---|---|---|---|---|
| KNN (调优后) | 约 2.15 | 约 1.68 | 约 3.5% | 极快 | 低 |
| LSTM (调优后) | 约 0.38 | 约 0.29 | 约 0.6% | 慢 | 高 |
从数字上看,LSTM实现了碾压性的胜利。它的RMSE和MAE远低于KNN,MAPE达到了0.6%左右,这意味着平均预测误差不到实际价格的1%,这是一个非常出色的结果。而KNN即使用weights='distance'并优化了K值和时间窗口,其误差仍然比LSTM高出一个数量级。
6.2 可视化对比:曲线图揭示的真相
把两个模型的预测曲线和真实股价画在同一张图上,差异一目了然。
- KNN的预测线:依然相对平滑,滞后性明显。在股价快速拉升或下跌的拐点处,它的反应总是慢半拍,预测的波峰和波谷总是比真实的要晚出现几天。它更像是在描述“过去几天平均发生了什么”,而不是“明天将会发生什么”。
- LSTM的预测线:则紧紧缠绕着真实股价曲线,无论是大的趋势转向,还是一些中小级别的波动,它都能在一定程度上捕捉到。虽然不可能完美预测每一个锯齿,但其拟合的紧密程度和反应的及时性,远非KNN可比。
6.3 适用场景与选择建议
这次对决的结果很清楚,但并不意味着LSTM在任何情况下都是唯一选择。
- 选择KNN当:你需要一个快速、简单、易于理解和实现的基线模型;你的数据量很小;或者你对预测的实时性要求极高,计算资源有限。它可以作为一个快速的参考。
- 选择LSTM当:你对预测精度有较高要求;你拥有足够的历史数据(通常至少几千条);你有时间和计算资源进行模型训练和调优;并且你相信数据中存在复杂的非线性时序依赖关系等待挖掘。
对于像股价预测这样复杂、高噪声、强时序性的任务,LSTM的优势是决定性的。但它也带来了更高的复杂性和计算成本。在实际项目中,我通常会先跑一个简单的KNN或线性回归作为基准,然后再用LSTM去突破这个基准,这样既能快速验证想法,又能明确更高级模型带来的价值提升究竟有多大。
7. 实战延伸:用调优后的LSTM进行未来预测
模型在测试集上表现良好,给了我们一些信心用它来做一个简单的未来走势推演。但这里必须强调,这仅仅是基于历史数据模式的数学推演,绝不构成任何投资建议。 股市受无数复杂因素影响,模型无法预测黑天鹅事件。
假设我们站在2022年6月10日这个时点,用训练好的LSTM模型对未来30个交易日(注意要剔除周末和节假日)的股价进行预测。实现的关键点在于,我们要用截至预测日的最新60天数据(即模型要求的时间窗口)来生成第一个预测点的输入特征,然后用这个预测值更新窗口,滚动预测下一天,如此循环。
def predict_future_days(model, last_sequence, scaler, days=30):
"""
使用训练好的模型进行多步滚动预测
last_sequence: 过去time_step天的归一化价格序列
scaler: 用于反向归一化的缩放器
"""
future_predictions = []
current_sequence = last_sequence.copy()
for _ in range(days):
# 将当前序列reshape成模型需要的输入格式 [1, time_step, 1]
current_input = current_sequence.reshape((1, len(current_sequence), 1))
# 预测下一天
next_pred = model.predict(current_input, verbose=0)
future_predictions.append(next_pred[0, 0])
# 更新序列:去掉最旧的一天,加入最新的预测值
current_sequence = np.append(current_sequence[1:], next_pred)
# 将预测结果反向归一化回原始价格尺度
future_predictions = np.array(future_predictions).reshape(-1, 1)
future_prices = scaler.inverse_transform(future_predictions)
return future_prices.flatten()
# 获取用于预测的最近60天数据
last_60_days = scaled_data[-time_step:]
future_prices = predict_future_days(tuned_lstm_model, last_60_days, scaler, days=30)
将预测结果画出来,你可能会看到一条延伸出去的曲线。在我的这次运行中,模型显示未来一个月平安银行股价可能呈现小幅震荡下行的趋势。请务必理性看待这个结果:它完全基于历史价格模式,没有考虑任何新的公司公告、宏观经济政策、行业新闻或市场情绪突变。它更像是一个在“一切照旧”假设下的数学外推。真正的投资决策,必须结合基本面分析、市场消息和风险管理,模型预测只能作为众多参考工具中的一个。
176



被折叠的 条评论
为什么被折叠?



