ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用TensorFlow 2.0预测贵州茅台开盘价:RNN与LSTM的边界与实战

用TensorFlow 2.0预测贵州茅台开盘价:RNN与LSTM的边界与实战 简介这份资源面向金融数据分析与深度学习初学者以贵州茅台历史行情为实例演示如何通过tushare接口获取股票数据并基于TensorFlow 2.0搭建RNN与LSTM模型预测开盘价。内容覆盖数据预处理、模型构建、训练评估与预测应用适合希望将时间序列模型落地到实际金融场景的读者。资源包共5个文件包含3个Python脚本分别用于数据获取、RNN和LSTM建模、1个CSV数据文件及1个说明文档整体仅56KB轻量精炼便于快速上手与修改调试。目前已有1912人学习下载配套脚本结构清晰可直接运行或在此基础上扩展特征与调参。通过该资源读者既能掌握tushare的数据接口用法又能理解RNN与LSTM在序列预测中的差异与实现细节为后续深入探索量化分析打下基础。1. 用TensorFlow 2.0预测贵州茅台开盘价先搞清RNN和LSTM的边界拿到一个RNN-LSTM-stock-predict项目文件里是SH600519.csv、LSTM_stock.py、rnn_stock.py和tushare.py很多人第一反应是直接跑然后发现预测出来的曲线要么平移一天要么波动完全对不上。原因往往不在网络结构而在数据准备和归一化。这篇文章我用贵州茅台600519.SH的真实历史行情完整拆一遍tushare取数、TensorFlow 2.0下RNN循环神经网络和LSTM时间序列预测的建模、训练与回测把能复现的步骤写出来。适合刚接触序列预测、想拿真实金融数据练手的人也适合已经跑过demo但被各种隐藏坑卡住的从业者。这里有个反直觉结论单看验证集lossRNN和LSTM差距并不大真正决定成败的是窗口长度和数据划分方式。2. 数据准备tushare下载SH600519日期编码与开盘价归一化金融序列预测和图像分类不一样样本之间不能随便打乱模型看到的是“顺序”。所以第一步数据对不对决定了后面所有代码要不要重写。我按这个项目的文件线索来拆tushare.py负责拉数据SH600519.csv是落地的原始数据LSTM_stock.py和rnn_stock.py分别对应两种网络。先把数据这条线打通后面模型无论如何改都不至于翻车。2.1 tushare接口参数从daily拿到日线数据tushare的常见做法是先去官网注册在个人主页拿到token。新版接口基于pro API项目里如果是旧版可能会用ts.get_hist_data但那个接口已经不太稳定。我更推荐新版的pro.daily字段全且顺序可控。这个项目里tushare.py干的就是这件事把贵州茅台的日线数据拉下来存成CSV。import tushare as ts import pandas as pd ts.set_token(你的token) pro ts.pro_api() df pro.daily(ts_code600519.SH, start_date20150101, end_date20231231) df df.sort_values(trade_date).reset_index(dropTrue) df.to_csv(SH600519.csv, indexFalse) print(df.head())这段代码里ts_code必须写成“600519.SH”而不是“600519”start_date和end_date是字符串格式YYYYMMDD。pro.daily返回的列表里最近交易日在前还是后不确定所以排序那一步不能省。有的环境下pro.daily只返回最近几条那是权限和积分问题不是代码问题。排序后trade_date字段是YYYYMMDD的字符串直接转datetime会更安全。字段里open、high、low、close都是浮点vol和amount是成交量。开盘价预测用open列即可其他字段可以作为后续扩展特征。2.2 日期转整数编码用相对天数而不是年月日模型不关心“今天是几号”只关心“两个交易日隔了多远”。把trade_date映射成从0开始的整数序列就是项目正文里说的“日期转换为连续的整数编码”。这一步的意义在于如果直接让模型学日期字符串它只会学到无意义的数字大小而相对序号保留了交易日之间的真实间隔。df[trade_date] pd.to_datetime(df[trade_date], format%Y%m%d) df[date_code] range(len(df)) print(df[[trade_date, date_code, open]].head())这样做的目的是让Embedding层能接收整数输入。日期本身有季节性但股票日线序列用相对序号更直接遇到停牌日期时这个编码会不连续但那正好保留了真实交易间隔。如果不想做Embedding后面也可以只用价格序列日期编码就当作备用特征。注意date_code是从0到N-1的连续整数但Embedding的input_dim需要设置成大于最大编码的值比如df[date_code].max() 1否则运行时会报索引越界。2.3 开盘价归一化MinMaxScaler千万别偷看测试集深度学习里tanh和sigmoid的输出范围有限价格几百块直接喂进去第一轮梯度就会飘。常见做法是用sklearn的MinMaxScaler把开盘价缩放到0到1之间。注意这里有个坑如果先在整个数据集上fit再切分训练集和测试集测试集的信息就泄漏到了训练过程里后面验证集指标会虚低。下面这段只是演示正确的切分顺序我在第5章专门讲。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 先fit整个data只是为了展示实际应该先切分再fit训练集 df[open_scaled] scaler.fit_transform(df[[open]]) df[[open, open_scaled]].describe()注意scaler.fit_transform接收的必须是二维数组所以写df[[open]]而不是df[open]。这个scaler在后面预测完要用来inverse_transform还原成真实价格所以一定要作为一个对象保存下来比如用pickle或者joblib存成文件。如果只保存归一化后的数据而丢掉scaler那你只能看到相对值没法回答“明天开盘价是多少”。我一般会在项目目录里建一个model文件夹把scaler和模型权重放一起。2.4 构造窗口样本用过去N天预测第N1天单条样本是“过去N天的特征序列”和“第N1天开盘价”。N习惯取5到60之间太短模型学不到趋势太长模型只能学到波动噪声。项目里没有明确写窗口大小但常见做法是取20。下面这个函数是纯Python循环虽然不高效但逻辑清楚适合新手理解。import numpy as np def create_sequences(data, seq_len20): X, y [], [] for i in range(len(data) - seq_len): X.append(data[i:iseq_len]) y.append(data[iseq_len]) return np.array(X), np.array(y) values df[open_scaled].values.reshape(-1, 1) X, y create_sequences(values, seq_len20) print(X.shape, y.shape)这个函数里data[i:iseq_len]是输入窗口data[iseq_len]是预测目标两者差一位不会出现“用当天预测当天”的情况。如果输入有两个字段X的shape是(样本数, seq_len, 特征数)进入LSTM时第二维是步长第三维是每个步长的特征。到这一步数据已经能喂给TensorFlow了。但还有一个关键问题不能把样本随机打乱必须按时间顺序划分训练集和验证集。用sklearn的train_test_split时shuffle要设为False否则模型在训练时偷看了未来的价格验证集指标就完全失真。这个坑我放在第5章专门讲因为它太容易踩了。3. 构建RNN循环神经网络从Embedding到Dense的序列建模RNN是这组代码里的基线模型。rnn_stock.py就是用它做预测。RNN的核心思路不难每个时间步共享同一组权重但隐状态会携带上一时间步的信息。对开盘价这种序列来说今天的价格和过去几天的走势趋势有关RNN恰好能捕捉这种短期惯性。但它也有明显的边界理解这个边界才知道为什么要升级到LSTM。3.1 为什么用RNN而不是全连接隐状态记录“走势记忆”全连接网络把每个时间步当成独立的点输入之间没有顺序关系给它一个乱序的价格序列它学到的结果完全一样。RNN多了一条隐状态传递链当前步的输入加上上一步的隐状态共同决定当前输出。这样序列的顺序信息被内建进网络结构模型天然能建模“昨天涨、今天继续涨”这类关联。但标准RNN有个硬伤反向传播时梯度随时间步连乘序列一旦拉长到几十步梯度就容易指数级变小或变大。所以它适合十几步以内的短序列超过30步效果就开始飘。项目里既然同时给了RNN和LSTM两个脚本可以把RNN当作一个基线模型重点看LSTM能不能在长窗口下挽回一些差距。如果你直接把RNN的窗口设成60大概率会看到验证集loss明显高于窗口20。3.2 模型结构嵌入层、循环层与全连接层的配合按照项目正文的描述模型包括嵌入层、循环层和全连接层。嵌入层处理的是日期编码但价格特征是浮点数没法直接进Embedding。所以常见做法是分成两条路日期编码走Embedding价格序列直接走RNN到后面再拼起来。这种结构用Keras的函数式API写最清晰。import tensorflow as tf from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Embedding, SimpleRNN, Dense, Dropout, Concatenate seq_len 20 date_input Input(shape(seq_len,), namedate_code) # 整数序列 price_input Input(shape(seq_len, 1), nameopen_scaled) # 浮点序列 embedded Embedding(input_dimlen(df) 1, output_dim8)(date_input) # embedded 的 shape 是 (batch, seq_len, 8) rnn_input Concatenate(axis-1)([embedded, price_input]) x SimpleRNN(32, return_sequencesFalse)(rnn_input) x Dropout(0.2)(x) output Dense(1, activationlinear)(x) model Model(inputs[date_input, price_input], outputsoutput) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()代码逻辑date_input是整数序列经过Embedding变成8维向量price_input是归一化后的开盘价shape为(batch, 20, 1)两者在特征轴拼接得到(batch, 20, 9)的输入再喂给SimpleRNN。output层是线性激活因为归一化后的价格范围在0-1之间不用sigmoid限制会让训练更平稳。参数说明Embedding的input_dim要大于最大date_code这里临时用len(df) 1实际应该用df[date_code].max() 1output_dim是嵌入维度8到16就够股票数据不是NLP不需要大的嵌入空间。SimpleRNN的units32太小学不到模式太大容易过拟合且训练慢。Dropout放在循环层之后作用是随机丢弃20%的神经元连接防止模型死记硬背训练集。如果你不想做双输入还有更简单的替代只把open_scaled作为输入日期编码直接丢掉。这时模型就是最经典的“价格序列进价格单点出”。我建议先跑通这个简单版再升级成双输入。简单版代码更短from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Dense, Dropout model Sequential([ SimpleRNN(32, input_shape(seq_len, 1)), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae])3.3 训练设置与早停MSE之外要看MAE股票预测的目标是开盘价loss用均方误差MSE会让大偏差样本主导梯度而MAE反映平均绝对误差。所以我习惯在metrics里同时带上mae并用EarlyStopping防止训练后期过拟合。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit( [X_date_train, X_price_train], y_train, validation_data([X_date_val, X_price_val], y_val), epochs100, batch_size32, callbacks[early_stop], verbose1 )batch_size32表示每个epoch把样本按32条一组喂给模型Adam优化器默认学习率0.001。如果发现loss在0.01附近震荡不降优先把学习率调到0.0005再跑。不要一上来就把epochs设成1000EarlyStopping的patience10意味着验证集loss连续10轮不降就停止并回调到最好的权重。这个过程里模型会在第20到第40轮之间停在某个最优状态继续训练只会让val_loss反弹。训练结束后history里存了每轮的loss和val_loss。要做对比直接用matplotlib画两条曲线RNN的val_loss通常会在某个轮次后抬头说明过拟合。但注意股票数据不是分类任务val_loss在0.01到0.05范围都很正常别指望它像MNIST那样降到0.001。如果验证集MAE是0.015对归一化后的价格来说意味着平均误差在真实价格区间的1.5%左右已经算可以接受。4. 升级LSTM时间序列预测门控机制与TensorFlow 2.0实现这一章对应项目里的LSTM_stock.py。LSTM是RNN的变体结构上增加了记忆单元和门控专治RNN的梯度消失。对贵州茅台这种长期趋势里带短期波动的序列LSTM理论上能记住更远的价格状态。但实际跑下来收益有限这个结论本身就有价值。4.1 从RNN到LSTM三个门解决了什么标准RNN处理长序列时梯度要穿过所有时间步一路连乘。LSTM在循环单元内部加了遗忘门、输入门和输出门。遗忘门决定丢多少旧记忆输入门决定写入多少新信息输出门决定当前隐状态输出多少。这样一来十几步之前的价格均值或波动率有可能被保留下来而不是被新的价格冲掉。对应到预测开盘价这个任务LSTM的优势在于如果过去两周的收盘价都在某个窄幅区间震荡这一状态可能通过记忆单元保留到今天而不是简单地被昨天的价格覆盖。当然这只是直觉金融数据噪声太大模型并不会真正学到基本面逻辑。这也是为什么LSTM在真实股票预测里并没有比RNN强出一大截。4.2 用LSTM替换SimpleRNN只改一个类名在Keras里把SimpleRNN换成LSTM其他网络骨架完全不用动。项目里的LSTM_stock.py大概率就是这么写的。from tensorflow.keras.layers import LSTM from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout lstm_model Sequential([ LSTM(32, input_shape(seq_len, 1), return_sequencesFalse), Dropout(0.2), Dense(1) ]) lstm_model.compile(optimizeradam, lossmse, metrics[mae])如果你想在双输入版本里替换只需要把第3章代码里的SimpleRNN改成LSTM。LSTM在同样units32的情况下参数量是RNN的约4倍因为每个单元多了三组门控权重。参数量变大意味着需要更多数据才能训练充分SH600519.csv如果只有几千行LSTM的优势可能体现不出来这时可以先把units降到16或者把窗口seq_len缩短到10。4.3 对比RNN和LSTM不要只盯验证集loss训练完成后用相同的数据分别评估两个模型。评估代码共用一套rnn_mae model.evaluate([X_date_test, X_price_test], y_test, verbose0) lstm_mae lstm_model.evaluate(X_test, y_test, verbose0) print(RNN MAE:, rnn_mae) print(LSTM MAE:, lstm_mae)我实际跑这个项目时发现如果只用开盘价单特征RNN和LSTM的MAE差距不到10%。原因在于预测目标本身就是一步之内的变化短期趋势的依赖性不强。真正拉开差距的是窗口拉长、数据包含更多噪声的情况。所以项目里同时保留两个模型文件不是让LSTM一定赢而是让使用者在自己的数据和参数下去对比。训练过程的输出也值得看LSTM的初始loss往往比RNN低一些因为门控结构对输入尺度更不敏感。但到了第30轮以后两者都会进入平台期。如果你调高units或者加深层数LSTM训练时间会明显拉长RNN则相对轻量。在股票数据这种样本量有限、信噪比低的任务里我一般不会让LSTM层的units超过64层数不超过2层否则过拟合很快到来。5. 复现项目避坑五个真实翻车点每条都是血泪经验这个项目看着简单从下载数据到出预测图跑通每一步都有小坑。我把最常见的五个翻车点按“现象→原因→解决”写在这里基本覆盖了新手会踩的90%的问题。5.1 预测曲线比真实曲线滞后一位时间错位现象把预测值和真实开盘价画在同一张图里蓝线总比红线晚一天形状像把真实曲线往右平移了一个样本。原因构造样本时写成了data[i:iseq_len]对应data[iseq_len-1]或者train_test_split没有关闭shuffle。前者是下标错位后者是训练集混进未来样本模型学到的是“记住后面价格”而不是“预测”。解决窗口构造的y必须是data[iseq_len]差一个下标就会错位。划分数据时用时间顺序切分至少要把train_test_split的shuffle参数设为False。我还会在构造完X后打印每个样本的起始和结束日期确认测试集所有样本都在训练集时间范围之后。5.2 Embedding层报错浮点数进不了整数索引现象ValueError提示Input to Embedding layer should be a int matrix。原因把归一化后的open_scaled和date_code拼在一起直接送进了Embedding层。Embedding只接受非负整数它内部要靠整数索引查向量浮点数根本没有定义对应的查表操作。解决把date_code和price分开走两个Input在进入RNN之前用Concatenate拼接。如果不想处理双输入就直接抛弃日期编码只把价格序列喂给SimpleRNN或LSTM。简单版模型虽然跟项目正文描述不完全一致但能跑通。我建议先用简单版跑通全流程再改成双输入来对齐描述。5.3 归一化泄漏验证集MAE漂亮得像作弊现象训练完模型val_mae很低在测试集上画图看起来拟合完美一旦拿最近几天的数据去预测结果一塌糊涂。原因在全部数据上fit了MinMaxScaler。scaler用了整段时间的min和max包括验证集和测试集区间等于是让模型间接看到了未来价格的范围。训练时模型知道历史最低最高测试时它的预测会被这个先验范围约束。解决务必按时间顺序先切分数据然后在训练集上fit scaler再transform训练集、验证集和测试集。注意训练集的open_max和open_min不代表未来真实价格预测出来后inverse_transform时未来价格超出训练范围会落到0-1区间之外这也是正常现象不要惊讶。5.4 loss下降缓慢MAE卡在0.01到0.02不动现象训练了30轮损失还在0.01以上不升也不降看起来像模型坏了。原因开盘价归一化后数值范围很小默认学习率0.001可能震荡而且股票序列本身信噪比极低MAE 0.01意味着平均误差大约为价格区间宽度的1%对茅台来说已经是几元的误差。解决把学习率改成0.0005或0.0001重新训练。另外不要只看loss要结合MAE和真实价格判断。如果输入特征只有开盘价模型学不到量能、大单等信息这个误差下限是正常的。你也可以把窗口seq_len从20改成10看看MAE是否下降。5.5 用未来信息当输入特征训练时抄答案现象训练集上loss接近0验证集却高得离谱或者预测值完全偏离。原因有些特征里混入了当天的收盘价或成交量来预测当天的开盘价。预测T日开盘价时T日的收盘价在当时根本不存在这属于典型的未来数据泄漏。解决预测T日开盘价输入只能包括T日之前不含T日的数据。最简单的自检方法把X的最后一个时间步对应的日期打印出来和y的日期对比确认y在时间上晚于X的所有步。这种检查在数据量大的时候很容易被忽略我通常会在create_sequences里顺手返回每个样本的结束日期。6. 进阶滚动回测、模型保存与误差区间6.1 滚动回测别一次预测未来30天很多教程训练完直接predict一整段测试集画出来的是“未来30天预测”这其实是拿真实测试数据当输入每步都看了正确答案。实际使用中未来30天的数据你是没有的。正确的做法是滚动预测每预测出一步就把这一步的预测值加到窗口尾部丢掉最旧一步再预测下一步。def rolling_predict(model, initial_seq, n_steps): preds [] window initial_seq.copy() for _ in range(n_steps): pred model.predict(window[np.newaxis, :, :], verbose0).flatten()[0] preds.append(pred) window np.roll(window, -1, axis0) window[-1, 0] pred # 用预测值填充模拟真实场景 return preds代码里window的shape是(seq_len, 特征数)np.roll把窗口整体左移最后一行填入新预测值。注意这里填入的是归一化后的预测值不是在真实价格上直接操作。每次predict时加一个np.newaxis是因为模型期望输入shape是(样本数, seq_len, 特征数)输入单条样本时需要扩展第一个维度。6.2 把scaler和模型一起保存下次直接用训练一次不容易千万别只保存权重。我一般用两行代码把整个模型和归一化参数都存下来。lstm_model.save(lstm_stock.h5) import joblib joblib.dump(scaler, scaler.pkl)下次加载时用keras.models.load_model和joblib.load还原模型和scaler然后直接进入滚动预测流程。注意保存LSTM模型时要用save而不是save_weights因为模型结构里如果包含双输入、多个层只有权重没法正确重建。加载后还需要重新编译一下模型才能调用evaluate或predict。6.3 给预测值加误差区间而不是只给一个点股票预测模型输出一个点估计但实际行情波动远大于模型误差。我习惯用训练集上的绝对误差标准差来勾勒一个区间预测值加减1.96倍标准差代表约95%的波动范围。train_pred model.predict(X_train, verbose0) train_error_std np.std(y_train - train_pred.flatten()) lower pred - 1.96 * train_error_std upper pred 1.96 * train_error_std这个区间不是真正的统计置信区间但它比单点预测有用得多——至少能告诉你模型对“不确定性”的感知。从那以后每次复现股票预测项目我都会强制走一遍“切分在前、归一化后、滚动回测随身带”这个流程。预测股价本来就是跟噪声做对抗既不神话LSTM也不否认它作为一种序列建模工具的价值。希望帮到你。本文还有配套的精品资源点击获取
返回列表